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
21 changes: 21 additions & 0 deletions Sources/ExarotonWebSocket/Models.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@

import Foundation

/// exaroton 服务器状态码(https://developers.exaroton.com/ → "Server status")。
///
/// ```
/// 0 = OFFLINE 1 = ONLINE 2 = STARTING 3 = STOPPING 4 = RESTARTING
/// 5 = SAVING 6 = LOADING 7 = CRASHED 8 = PENDING 9 = TRANSFERRING
/// 10 = PREPARING
/// ```
///
/// ⚠️ **必须覆盖全部官方取值**:这个枚举参与 WebSocket `status` 消息的解码,
/// 少一个值就意味着"整个状态消息解码失败 → 状态更新被丢掉"(实测漏了 9 = TRANSFERRING,
/// 服务器启动/停止期间的迁移阶段状态会卡住不动)。
/// 另外解码是**宽容**的:将来 exaroton 新增状态码时退化成 `.UNKNOWN`,而不是让整条消息失败。
public enum ServerStatus: Int, CaseIterable, Codable {
case OFFLINE = 0
case ONLINE = 1
Expand All @@ -17,7 +29,16 @@ public enum ServerStatus: Int, CaseIterable, Codable {
case LOADING = 6
case CRASHED = 7
case PENDING = 8
case TRANSFERRING = 9
case PREPARING = 10

/// 未知/未来新增的状态码。调用方应把它当成"过渡态"处理(不允许动作,等状态稳定)。
case UNKNOWN = -1

public init(from decoder: any Decoder) throws {
let raw = try decoder.singleValueContainer().decode(Int.self)
self = ServerStatus(rawValue: raw) ?? .UNKNOWN
}
}

public struct Server: Codable, Identifiable {
Expand Down
45 changes: 45 additions & 0 deletions Tests/ExarotonWebSocketTests/ServerStatusDecodingTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Foundation
import Testing
@testable import ExarotonWebSocket

/// 状态码解码:文档里的每个取值都必须能解出来。
///
/// 回归背景:枚举曾漏掉 `9 = TRANSFERRING`,导致服务器启动/停止迁移期间的
/// `status` 消息**整条解码失败**,上层看到的就是"状态卡住不动"。
@Suite(.tags(.unit, .websocket, .codec))
final class ServerStatusDecodingTests {

private func serverJSON(status: Int) -> Data {
Data("""
{"id":"X","name":"demo","address":"demo.exaroton.me","motd":"hi","status":\(status),
"players":{"max":20,"count":0,"list":[]},
"software":{"id":"1","name":"Paper","version":"1.0"},"shared":false}
""".utf8)
}

@Test(arguments: [
(0, ServerStatus.OFFLINE),
(1, ServerStatus.ONLINE),
(2, ServerStatus.STARTING),
(3, ServerStatus.STOPPING),
(4, ServerStatus.RESTARTING),
(5, ServerStatus.SAVING),
(6, ServerStatus.LOADING),
(7, ServerStatus.CRASHED),
(8, ServerStatus.PENDING),
(9, ServerStatus.TRANSFERRING),
(10, ServerStatus.PREPARING),
])
func decodesEveryDocumentedCode(raw: Int, expected: ServerStatus) throws {
let server = try JSONDecoder().decode(Server.self, from: serverJSON(status: raw))
#expect(server.status == expected)
#expect(server.status.rawValue == raw)
}

/// 将来 exaroton 新增状态码时不能把整条消息炸掉,退化成 `.UNKNOWN`。
@Test
func unknownCodeDegradesInsteadOfThrowing() throws {
let server = try JSONDecoder().decode(Server.self, from: serverJSON(status: 99))
#expect(server.status == .UNKNOWN)
}
}
Loading