fix(menu): 大厅菜单按玩家标出每台服能否启动及原因,自己的服排前面,状态改成中文

This commit is contained in:
Lemon-miaow committed 2026-09-27 05:02:33 +08:00
1 parent 82310d02ec
commit 4ae64cc2e8
21 files changed
+1081 -136

No files matched your search

@@ -34,6 +34,7 @@ public final class ControlRoundTripTest {
errorOmitsServerWhenAbsentButRoundTrips();
escapesAwkwardStrings();
listUpdateCarriesNamesInOrder();
listUpdateCarriesAccessByName();
rejectsMalformedAndUnknownFrames();
System.out.println("ControlRoundTripTest OK (" + checks + " checks)");
}
@@ -52,6 +53,7 @@ public final class ControlRoundTripTest {
roundTrip(ControlFrame.listRequest());
roundTrip(ControlFrame.listUpdate(Arrays.asList("alpha", "beta-2", "gamma")));
roundTrip(ControlFrame.listUpdate(List.of()));
roundTrip(ControlFrame.listUpdate(List.of("mine", "theirs"), List.of("owner", "owner_only")));
roundTrip(ControlFrame.loginRelease("Notch"));
}
@@ -81,6 +83,34 @@ public final class ControlRoundTripTest {
assertEq("non-string entries skipped", List.of("a", "b"), mixed.servers());
}
// ListUpdate's access list is the player's verdict per name, by index: it holds
// its alignment through a dropped name on either end, a missing or non-string
// verdict reads as unknown (""), and a list with no verdict at all is the plain
// name list on the wire.
private static void listUpdateCarriesAccessByName() {
ControlFrame f = decode(ControlFrame.listUpdate(
Arrays.asList("mine", null, "theirs", "fresh"),
Arrays.asList("owner", "wake", "owner_only")));
assertEq("names", List.of("mine", "theirs", "fresh"), f.servers());
assertEq("verdicts follow their names", List.of("owner", "owner_only", ""), f.access());
assertEq("names only: no verdicts", List.of(), decode(ControlFrame.listUpdate(List.of("a"))).access());
assertEq("all unknown: no verdicts", List.of(),
decode(ControlFrame.listUpdate(List.of("a", "b"), Arrays.asList("", null))).access());
assertEq("names only: nothing extra on the wire",
"{\"type\":\"ListUpdate\",\"servers\":[\"a\"]}",
new String(Control.encode(ControlFrame.listUpdate(List.of("a"), List.of(""))), StandardCharsets.UTF_8));
assertEq("extra verdicts are cut", List.of("wake"),
ControlFrame.listUpdate(List.of("a"), List.of("wake", "owner")).access());
ControlFrame mixed = Control.decode(("{\"type\":\"ListUpdate\",\"servers\":[\"a\",1,\"b\",\"c\"],"
+ "\"access\":[\"owner\",\"wake\",7,\"allowlist\"]}").getBytes(StandardCharsets.UTF_8));
assertEq("hand-written: names", List.of("a", "b", "c"), mixed.servers());
assertEq("hand-written: a skipped name takes its verdict", List.of("owner", "", "allowlist"), mixed.access());
ControlFrame shortList = Control.decode(("{\"type\":\"ListUpdate\",\"servers\":[\"a\",\"b\"],"
+ "\"access\":[\"retiring\"]}").getBytes(StandardCharsets.UTF_8));
assertEq("hand-written: a missing verdict is unknown", List.of("retiring", ""), shortList.access());
assertEq("access on a non-list frame", List.of(), decode(ControlFrame.statusQuery("s")).access());
}
// StatusUpdate refines the spec's "players" into ready + online + max; the GUI
// renders all three, so all three must survive the round-trip with exact values.
private static void wireCarriesRefinedStatusFields() {
@@ -44,6 +44,9 @@ public final class FelisApiClientTest {
if (path.endsWith("/wake")) {
status = 202;
body = "{\"name\":\"alpha\",\"phase\":\"Starting\",\"ready\":false}";
} else if (path.startsWith("/api/v1/internal/player/menu-access/")) {
status = 200;
body = "{\"servers\":{\"mine\":\"owner\",\"odd\":7,\"pub\":\"wake\"}}";
} else if (path.equals("/api/v1/internal/op-login/mismatch/approve")) {
status = 409;
body = "{\"error\":{\"code\":\"op_login_mismatch\",\"message\":\"that operator login is for a different account\"}}";
@@ -78,6 +81,7 @@ public final class FelisApiClientTest {
opaqueSegmentsArePercentEncoded(api);
opLoginShowNamesTheAccount(api);
opLoginApproveSendsTheTypedName(api);
menuAccessReadsVerdictsByName(api);
} finally {
stub.stop(0);
}
@@ -96,6 +100,15 @@ public final class FelisApiClientTest {
"GET /api/v1/internal/servers/abc/status"), seen);
}
// menuAccess reads the verdict map for the player's own UUID and keeps only the
// string verdicts, so one malformed entry cannot hide the others.
private static void menuAccessReadsVerdictsByName(FelisApiClient api) throws LinkException {
seen.clear();
UUID id = UUID.fromString("00000000-0000-0000-0000-00000000000a");
assertEq("verdicts", java.util.Map.of("mine", "owner", "pub", "wake"), api.menuAccess(id));
assertEq("route", List.of("GET /api/v1/internal/player/menu-access/" + id), seen);
}
private static void pathBendingNamesNeverLeaveTheClient(FelisApiClient api) {
UUID id = UUID.fromString("00000000-0000-0000-0000-000000000002");
String[] bad = {