diff --git a/Sources/SwiftScaffolding/Client/ScaffoldingClient.swift b/Sources/SwiftScaffolding/Client/ScaffoldingClient.swift index 65ffc91..79fbb09 100644 --- a/Sources/SwiftScaffolding/Client/ScaffoldingClient.swift +++ b/Sources/SwiftScaffolding/Client/ScaffoldingClient.swift @@ -16,7 +16,6 @@ public final class ScaffoldingClient { private let encoder: JSONEncoder private let decoder: JSONDecoder private let player: Member - private let roomCode: String private var connection: NWConnection! private var serverNodeIp: String! private var protocols: [String] @@ -32,12 +31,10 @@ public final class ScaffoldingClient { /// - easyTier: 使用的 EasyTier。 /// - playerName: 玩家名。 /// - vendor: 联机客户端信息。 - /// - roomCode: 房间的房间码。 public init( easyTier: EasyTier, playerName: String, - vendor: String, - roomCode: String + vendor: String ) { self.easyTier = easyTier self.player = .init( @@ -46,7 +43,6 @@ public final class ScaffoldingClient { vendor: vendor, kind: .guest ) - self.roomCode = roomCode self.encoder = .init() self.encoder.outputFormatting = .withoutEscapingSlashes @@ -59,8 +55,9 @@ public final class ScaffoldingClient { /// 该方法返回后,必须每隔 5s 调用一次 `heartbeat()` 方法。 /// https://github.com/Scaffolding-MC/Scaffolding-MC/blob/main/README.md#拓展协议 /// - Parameters: + /// - roomCode: 房间码。 /// - checkServer: 是否检查联机中心返回的 Minecraft 服务器端口号。 - public func connect(checkServer: Bool = true, terminationHandler: ((Process) -> Void)? = nil) async throws { + public func connect(to roomCode: String, checkServer: Bool = true, terminationHandler: ((Process) -> Void)? = nil) async throws { guard RoomCode.isValid(code: roomCode) else { throw RoomError.invalidRoomCode } @@ -105,6 +102,22 @@ public final class ScaffoldingClient { } } + /// 不使用 EasyTier,直接连接到本地联机大厅。 + /// - Parameter port: 联机大厅端口号。 + public func connectDirectly(port: UInt16) async throws { + Logger.info("Directly connecting to scaffolding server...") + self.connection = try await ConnectionUtil.makeConnection(host: "127.0.0.1", port: port) + Logger.info("Connected to scaffolding server") + + room = Room(members: [], serverPort: 0) + try await heartbeat() + try await fetchProtocols() + + let serverPort: UInt16 = ByteBuffer(data: try await sendRequest("c:server_port").data).readUInt16() + room.serverPort = serverPort + Logger.info("Minecraft server is ready: 127.0.0.1:\(serverPort)") + } + /// 向联机中心发送请求。 /// - Parameters: /// - name: 请求类型。 diff --git a/Sources/SwiftScaffolding/Errors.swift b/Sources/SwiftScaffolding/Errors.swift index 35f755f..c949ee5 100644 --- a/Sources/SwiftScaffolding/Errors.swift +++ b/Sources/SwiftScaffolding/Errors.swift @@ -54,6 +54,7 @@ public enum ConnectionError: LocalizedError, Equatable { public enum RoomError: LocalizedError { case invalidRoomCode case roomClosed + case playerInfoMismatch public var errorDescription: String? { switch self { @@ -69,6 +70,12 @@ public enum RoomError: LocalizedError { bundle: Bundle.module, comment: "c:ping 超时" ) + case .playerInfoMismatch: + return NSLocalizedString( + "RoomError.playerInfoMismatch", + bundle: Bundle.module, + comment: "两次 c:player_ping 发送的信息不一致" + ) } } } diff --git a/Sources/SwiftScaffolding/Room/Member.swift b/Sources/SwiftScaffolding/Room/Member.swift index 676cbbd..073512b 100644 --- a/Sources/SwiftScaffolding/Room/Member.swift +++ b/Sources/SwiftScaffolding/Room/Member.swift @@ -7,7 +7,7 @@ import Foundation -public struct Member: Codable { +public struct Member: Codable, Hashable, Equatable { /// 玩家名。 public let name: String /// 玩家的 `machine_id`。 diff --git a/Sources/SwiftScaffolding/Scaffolding.swift b/Sources/SwiftScaffolding/Scaffolding.swift index 0b7b2b8..bd6ea81 100644 --- a/Sources/SwiftScaffolding/Scaffolding.swift +++ b/Sources/SwiftScaffolding/Scaffolding.swift @@ -16,9 +16,12 @@ public final class Scaffolding { /// 根据设备的主板唯一标识符生成设备标识符。 /// https://github.com/Scaffolding-MC/Scaffolding-MC/blob/main/README.md#machine_id /// - Returns: 设备标识符。 - public static func getMachineID() -> String { + public static func getMachineID(forHost: Bool = false) -> String { let service: io_service_t = IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching("IOPlatformExpertDevice")) - let uuid: String = IORegistryEntryCreateCFProperty(service, "IOPlatformUUID" as CFString, kCFAllocatorDefault, 0).takeUnretainedValue() as? String ?? UUID().uuidString + let uuid: String = (IORegistryEntryCreateCFProperty(service, "IOPlatformUUID" as CFString, kCFAllocatorDefault, 0) + .takeUnretainedValue() as? String ?? UUID().uuidString) + (forHost ? "HOST" : "") + + IOObjectRelease(service) return Insecure.SHA1.hash(data: Data(uuid.utf8)).map { String(format: "%02hhx", $0) }.joined() } diff --git a/Sources/SwiftScaffolding/Server/RequestHandler.swift b/Sources/SwiftScaffolding/Server/RequestHandler.swift index fa16da2..8881164 100644 --- a/Sources/SwiftScaffolding/Server/RequestHandler.swift +++ b/Sources/SwiftScaffolding/Server/RequestHandler.swift @@ -76,14 +76,41 @@ public class RequestHandler { registerHandler(for: "c:player_ping") { sender, requestBody in let connection: NWConnection = sender.connection - let member: Member = try self.server.decoder.decode(Member.self, from: requestBody.data) - Logger.info("Player info for \(connection.endpoint.debugDescription) is \(String(data: requestBody.data, encoding: .utf8)!)") - self.server.machineIdMap[ObjectIdentifier(connection)] = member.machineId - if !self.server.room.members.contains(where: { $0.machineId == member.machineId }) { + let rawMember: Member = try self.server.decoder.decode(Member.self, from: requestBody.data) + + let member: Member = .init( + name: rawMember.name, + machineID: rawMember.machineId, + vendor: rawMember.vendor, + kind: .guest + ) + + let identifier: ObjectIdentifier = .init(connection) + + if self.server.machineIdMap[identifier] == nil + && self.server.machineIdMap.values.contains(member.machineId) { + Logger.warn("Detected a machine_id collision") + throw RoomError.playerInfoMismatch + } + if let machineId = self.server.machineIdMap[identifier], machineId != member.machineId { + Logger.warn("machine_id mismatch detected") + throw RoomError.playerInfoMismatch + } + + self.server.machineIdMap[identifier] = member.machineId + + if let storedMember: Member = self.server.room.members.first(where: { $0.machineId == member.machineId }) { + if storedMember != member { + Logger.warn("Member info mismatch for \(storedMember.name)") + throw RoomError.playerInfoMismatch + } + } else { + Logger.info("Received player info from \(connection.endpoint.debugDescription): { \"name\": \"\(member.name)\", \"vendor\": \"\(member.vendor)\", \"machine_id\": \"\(member.machineId)\"}") DispatchQueue.main.async { self.server.room.members.append(member) } } + return .init(status: 0, data: Data()) } diff --git a/Sources/SwiftScaffolding/Server/ScaffoldingServer.swift b/Sources/SwiftScaffolding/Server/ScaffoldingServer.swift index 84a8087..b1d6746 100644 --- a/Sources/SwiftScaffolding/Server/ScaffoldingServer.swift +++ b/Sources/SwiftScaffolding/Server/ScaffoldingServer.swift @@ -18,6 +18,7 @@ public final class ScaffoldingServer { private let easyTier: EasyTier private var listener: NWListener! private var connections: [NWConnection] = [] + private var connectionTasks: [ObjectIdentifier: Task] = [:] deinit { Logger.debug("ScaffoldingServer is being deallocated") @@ -40,7 +41,7 @@ public final class ScaffoldingServer { serverPort: UInt16 ) { self.room = Room( - members: [.init(name: playerName, machineID: Scaffolding.getMachineID(), vendor: vendor, kind: .host)], + members: [.init(name: playerName, machineID: Scaffolding.getMachineID(forHost: true), vendor: vendor, kind: .host)], serverPort: serverPort ) self.easyTier = easyTier @@ -63,18 +64,7 @@ public final class ScaffoldingServer { guard let self = self else { return } switch state { case .ready: - if !self.connections.contains(where: { $0 === connection }) { self.connections.append(connection) } - Task { - do { - try await self.startReceiving(from: connection) - } catch { - Logger.error("An error occurred while receiving the request: \(error)") - guard case ConnectionError.timeout = error else { - connection.cancel() - return - } - } - } + handleConnection(connection) return case .failed(let error): Logger.error("Failed to create connection: \(error)") @@ -83,7 +73,9 @@ public final class ScaffoldingServer { default: return } - cleanup(connection) + Task { @MainActor in + self.cleanup(connection) + } } connection.start(queue: Scaffolding.networkQueue) } @@ -147,6 +139,9 @@ public final class ScaffoldingServer { listener = nil for connection in connections { connection.cancel() + DispatchQueue.main.async { + self.cleanup(connection) + } } connections = [] handler.destroy() @@ -154,24 +149,39 @@ public final class ScaffoldingServer { + @MainActor private func cleanup(_ connection: NWConnection) { - if let machineId = machineIdMap[ObjectIdentifier(connection)] { - DispatchQueue.main.async { - if let index = self.room.members.firstIndex(where: { $0.machineId == machineId }) { - Logger.info("Removed player \(self.room.members[index].name) from the room") - self.room.members.remove(at: index) - } - } - machineIdMap.removeValue(forKey: ObjectIdentifier(connection)) + connection.stateUpdateHandler = nil + if connection.state != .cancelled { connection.cancel() } + let identifier: ObjectIdentifier = .init(connection) + + if let machineId = machineIdMap[identifier] { + self.room.members.removeAll(where: { $0.machineId == machineId }) } - if let index = self.connections.firstIndex(where: { $0 === connection }) { - self.connections.remove(at: index) + + self.connectionTasks[identifier]?.cancel() + self.connectionTasks.removeValue(forKey: identifier) + self.connections.removeAll(where: { $0 === connection }) + } + + private func handleConnection(_ connection: NWConnection) { + if !self.connections.contains(where: { $0 === connection }) { self.connections.append(connection) } + let task: Task = Task.detached { + do { + try await self.startReceiving(from: connection) + } catch { + Logger.error("An error occurred while processing requests: \(error.localizedDescription)") + } + await MainActor.run { + self.cleanup(connection) + } } + connectionTasks[ObjectIdentifier(connection)] = task } // 该方法只会在连接发生异常或连接断开时返回。 private func startReceiving(from connection: NWConnection) async throws { - while true { + while !Task.isCancelled { let headerBuffer: ByteBuffer = .init() headerBuffer.writeData(try await ConnectionUtil.receiveData(from: connection, length: 1)) @@ -183,9 +193,7 @@ public final class ScaffoldingServer { let bodyData: Data = try await ConnectionUtil.receiveData(from: connection, length: bodyLength) let responseBuffer: ByteBuffer = .init() - if handler.protocols().contains(type) { - Logger.info("Received \(type) request from \(connection.endpoint.debugDescription)") - } else { + if !handler.protocols().contains(type) { Logger.info("Received unknown request: \(type)") } diff --git a/Sources/SwiftScaffolding/en.lproj/Localizable.strings b/Sources/SwiftScaffolding/en.lproj/Localizable.strings index afeafa9..8fa55a3 100644 --- a/Sources/SwiftScaffolding/en.lproj/Localizable.strings +++ b/Sources/SwiftScaffolding/en.lproj/Localizable.strings @@ -14,5 +14,6 @@ "RoomError.invalidRoomCode" = "The room code was invalid."; "RoomError.roomClosed" = "The room has been closed, or the network is unstable."; +"RoomError.playerInfoMismatch" = "Player information mismatch."; "EasyTierError.cliError" = "EasyTier CLI returned an error: %@"; diff --git a/Sources/SwiftScaffolding/zh-Hans.lproj/Localizable.strings b/Sources/SwiftScaffolding/zh-Hans.lproj/Localizable.strings index b848c89..7cd1e77 100644 --- a/Sources/SwiftScaffolding/zh-Hans.lproj/Localizable.strings +++ b/Sources/SwiftScaffolding/zh-Hans.lproj/Localizable.strings @@ -14,5 +14,6 @@ "RoomError.invalidRoomCode" = "无效的房间码。"; "RoomError.roomClosed" = "房间已被关闭,或者网络不稳定。"; +"RoomError.playerInfoMismatch" = "玩家信息不一致。"; "EasyTierError.cliError" = "EasyTier CLI 返回了一个错误:%@";