Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions Sources/SwiftScaffolding/Client/ScaffoldingClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public final class ScaffoldingClient {
/// - checkServer: 是否检查联机中心返回的 Minecraft 服务器端口号。
public func connect(checkServer: Bool = true, terminationHandler: ((Process) -> Void)? = nil) async throws {
guard RoomCode.isValid(code: roomCode) else {
throw RoomCodeError.invalidRoomCode
throw RoomError.invalidRoomCode
}
let networkName: String = "scaffolding-mc-\(roomCode.dropFirst(2).prefix(9))"
let networkSecret: String = String(roomCode.dropFirst(2).suffix(9))
Expand Down Expand Up @@ -111,16 +111,26 @@ public final class ScaffoldingClient {
/// - body: 请求体构造函数。
/// - Returns: 联机中心的响应。
@discardableResult
public func sendRequest(_ name: String, body: (ByteBuffer) throws -> Void = { _ in }) async throws -> Scaffolding.Response {
public func sendRequest(_ name: String, timeout: Double = 5, body: (ByteBuffer) throws -> Void = { _ in }) async throws -> Scaffolding.Response {
try assertReady()
return try await Scaffolding.sendRequest(name, to: connection, body: body)
return try await Scaffolding.sendRequest(name, to: connection, timeout: timeout, body: body)
}

/// 发送 `c:player_ping` 请求并同步玩家列表。
public func heartbeat() async throws {
try assertReady()
try await sendRequest("c:player_ping") { buf in
buf.writeData(try encoder.encode(player))
do {
try await sendRequest("c:player_ping") { buf in
buf.writeData(try encoder.encode(player))
}
} catch ConnectionError.timeout {
Logger.error("Timeout occurred while sending c:player_ping request")
do {
try await sendRequest("c:ping", timeout: 1)
} catch ConnectionError.timeout {
Logger.info("The room has been closed")
stop()
throw RoomError.roomClosed
}
}
let memberList: [Member] = try decoder.decode([Member].self, from: await sendRequest("c:player_profiles_list").data)
await MainActor.run {
Expand Down
24 changes: 17 additions & 7 deletions Sources/SwiftScaffolding/Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

public enum ConnectionError: LocalizedError {
public enum ConnectionError: LocalizedError, Equatable {
case invalidPort
case timeout
case cancelled
Expand Down Expand Up @@ -51,14 +51,24 @@ public enum ConnectionError: LocalizedError {
}
}

public enum RoomCodeError: LocalizedError {
public enum RoomError: LocalizedError {
case invalidRoomCode
case roomClosed

public var errorDescription: String? {
return NSLocalizedString(
"RoomCodeError.invalidRoomCode",
bundle: Bundle.module,
comment: "房间码无效"
)
switch self {
case .invalidRoomCode:
return NSLocalizedString(
"RoomError.invalidRoomCode",
bundle: Bundle.module,
comment: "房间码无效"
)
case .roomClosed:
return NSLocalizedString(
"RoomError.roomClosed",
bundle: Bundle.module,
comment: "c:ping 超时"
)
}
}
}
61 changes: 28 additions & 33 deletions Sources/SwiftScaffolding/Scaffolding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public final class Scaffolding {
public static func sendRequest(
_ type: String,
to connection: NWConnection,
timeout: Double = 5,
body: (ByteBuffer) throws -> Void
) async throws -> Response {
let buffer: ByteBuffer = ByteBuffer()
Expand All @@ -42,55 +43,49 @@ public final class Scaffolding {
buffer.writeData(bodyBuffer.data)

return try await withCheckedThrowingContinuation { continuation in
var didResume: Bool = false
func safeResume(_ block: () -> Void) {
if !didResume {
didResume = true
block()
}
}
networkQueue.asyncAfter(deadline: .now() + 5) {
safeResume {
continuation.resume(throwing: ConnectionError.timeout)
let once: Once = .init()
func finish(with result: Result<Response, Error>) {
Task {
await once.run {
continuation.resume(with: result)
}
}
}
connection.send(content: buffer.data, completion: .contentProcessed({ error in
if let error: NWError = error {
safeResume { continuation.resume(throwing: error) }
finish(with: .failure(error))
} else {
receive(from: connection) { result in
safeResume { continuation.resume(with: result) }
receive(from: connection, timeout: timeout) { result in
finish(with: result)
}
}
}))

Task {
try await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
await once.run {
continuation.resume(throwing: ConnectionError.timeout)
}
}
}
}



private static func receive(from connection: NWConnection, completion: @escaping (Result<Response, Error>) -> Void) {
connection.receive(minimumIncompleteLength: 5, maximumLength: 5) { data, context, isComplete, error in
if let error = error {
completion(.failure(error))
return
}
if let data = data {
let buffer: ByteBuffer = ByteBuffer(data: data)
let status: UInt8 = buffer.readUInt8()
let bodyLength: Int = Int(buffer.readUInt32())
private static func receive(from connection: NWConnection, timeout: Double = 10, completion: @escaping (Result<Response, Error>) -> Void) {
Task {
do {
let headerBuffer: ByteBuffer = .init(data: try await ConnectionUtil.receiveData(from: connection, length: 5, timeout: timeout))
let status: UInt8 = headerBuffer.readUInt8()
let bodyLength: Int = .init(headerBuffer.readUInt32())
if bodyLength == 0 {
completion(.success(Response(status: status, data: Data())))
completion(.success(.init(status: 0, data: .init())))
return
}
connection.receive(minimumIncompleteLength: bodyLength, maximumLength: bodyLength) { data, context, isComplete, error in
if let error = error {
completion(.failure(error))
return
}
if let data = data {
completion(.success(Response(status: status, data: data)))
}
}
let bodyData: Data = try await ConnectionUtil.receiveData(from: connection, length: bodyLength, timeout: timeout)
completion(.success(.init(status: status, data: bodyData)))
} catch {
completion(.failure(error))
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/SwiftScaffolding/Server/ScaffoldingServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public final class ScaffoldingServer {
/// 使用指定的 EasyTier 创建联机中心。
/// - Parameters:
/// - easyTier: 使用的 EasyTier。
/// - roomCode: 房间码。若不合法,将在 `createRoom()` 中抛出 `RoomCodeError.invalidRoomCode` 错误。
/// - roomCode: 房间码。若不合法,将在 `createRoom()` 中抛出 `RoomError.invalidRoomCode` 错误。
/// - playerName: 玩家名。
/// - vendor: 联机客户端信息。
/// - serverPort: Minecraft 服务器端口号。
Expand Down Expand Up @@ -118,7 +118,7 @@ public final class ScaffoldingServer {
throw ConnectionError.invalidConnectionState
}
guard RoomCode.isValid(code: roomCode) else {
throw RoomCodeError.invalidRoomCode
throw RoomError.invalidRoomCode
}
let networkName: String = "scaffolding-mc-\(roomCode.dropFirst(2).prefix(9))"
let networkSecret: String = String(roomCode.dropFirst(2).suffix(9))
Expand Down
3 changes: 2 additions & 1 deletion Sources/SwiftScaffolding/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"ConnectionError.connectionIsNotReady" = "The connection is not established, or the connection state is invalid.";
"ConnectionError.failedToAllocatePort" = "Failed to allocate port.";

"RoomCodeError.invalidRoomCode" = "The room code was invalid.";
"RoomError.invalidRoomCode" = "The room code was invalid.";
"RoomError.roomClosed" = "The room has been closed, or the network is unstable.";

"EasyTierError.cliError" = "EasyTier CLI returned an error: %@";
3 changes: 2 additions & 1 deletion Sources/SwiftScaffolding/zh-Hans.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"ConnectionError.connectionIsNotReady" = "连接未被建立,或者连接状态异常。";
"ConnectionError.failedToAllocatePort" = "端口分配失败。";

"RoomCodeError.invalidRoomCode" = "无效的房间码。";
"RoomError.invalidRoomCode" = "无效的房间码。";
"RoomError.roomClosed" = "房间已被关闭,或者网络不稳定。";

"EasyTierError.cliError" = "EasyTier CLI 返回了一个错误:%@";