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
25 changes: 19 additions & 6 deletions Sources/SwiftScaffolding/Client/ScaffoldingClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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(
Expand All @@ -46,7 +43,6 @@ public final class ScaffoldingClient {
vendor: vendor,
kind: .guest
)
self.roomCode = roomCode

self.encoder = .init()
self.encoder.outputFormatting = .withoutEscapingSlashes
Expand All @@ -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
}
Expand Down Expand Up @@ -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: 请求类型。
Expand Down
7 changes: 7 additions & 0 deletions Sources/SwiftScaffolding/Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 发送的信息不一致"
)
}
}
}
2 changes: 1 addition & 1 deletion Sources/SwiftScaffolding/Room/Member.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

public struct Member: Codable {
public struct Member: Codable, Hashable, Equatable {
/// 玩家名。
public let name: String
/// 玩家的 `machine_id`。
Expand Down
7 changes: 5 additions & 2 deletions Sources/SwiftScaffolding/Scaffolding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down
35 changes: 31 additions & 4 deletions Sources/SwiftScaffolding/Server/RequestHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}

Expand Down
64 changes: 36 additions & 28 deletions Sources/SwiftScaffolding/Server/ScaffoldingServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Void, Never>] = [:]

deinit {
Logger.debug("ScaffoldingServer is being deallocated")
Expand All @@ -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
Expand All @@ -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)")
Expand All @@ -83,7 +73,9 @@ public final class ScaffoldingServer {
default:
return
}
cleanup(connection)
Task { @MainActor in
self.cleanup(connection)
}
}
connection.start(queue: Scaffolding.networkQueue)
}
Expand Down Expand Up @@ -147,31 +139,49 @@ public final class ScaffoldingServer {
listener = nil
for connection in connections {
connection.cancel()
DispatchQueue.main.async {
self.cleanup(connection)
}
}
connections = []
handler.destroy()
}



@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<Void, Never> = 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))

Expand All @@ -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)")
}

Expand Down
1 change: 1 addition & 0 deletions Sources/SwiftScaffolding/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -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: %@";
1 change: 1 addition & 0 deletions Sources/SwiftScaffolding/zh-Hans.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@

"RoomError.invalidRoomCode" = "无效的房间码。";
"RoomError.roomClosed" = "房间已被关闭,或者网络不稳定。";
"RoomError.playerInfoMismatch" = "玩家信息不一致。";

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