fix(menu): 大厅菜单按玩家标出每台服能否启动及原因,自己的服排前面,状态改成中文
This commit is contained in:
21 files changed
+1081
-136
No files matched your search
@@ -62,23 +62,33 @@ tasks.withType(JavaCompile).configureEach {
|
||||
}
|
||||
|
||||
// LobbyGuardTest drives the real LobbyGuard handlers with real paper-api events around
|
||||
// Proxy-built fakes (plain main, no framework, like the velocity routing tests). It is
|
||||
// not part of `build`, which the lobby image runs; plugins/test.sh runs `./gradlew lobbyTest`.
|
||||
// Proxy-built fakes, and MenuTilesTest checks the menu's tile judgement (plain mains, no
|
||||
// framework, like the velocity routing tests). Neither is part of `build`, which the
|
||||
// lobby image runs; plugins/test.sh runs `./gradlew lobbyTest`, which runs both.
|
||||
sourceSets {
|
||||
lobbyTest {
|
||||
java {
|
||||
srcDir 'test'
|
||||
include 'best/lolicon/felis/paper/Fakes.java'
|
||||
include 'best/lolicon/felis/paper/LobbyGuardTest.java'
|
||||
include 'best/lolicon/felis/paper/MenuTilesTest.java'
|
||||
}
|
||||
compileClasspath += sourceSets.main.output + configurations.compileClasspath
|
||||
runtimeClasspath += output + compileClasspath
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('menuTilesTest', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Runs the MenuTilesTest self-test main.'
|
||||
classpath = sourceSets.lobbyTest.runtimeClasspath
|
||||
mainClass = 'best.lolicon.felis.paper.MenuTilesTest'
|
||||
}
|
||||
|
||||
tasks.register('lobbyTest', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Runs the LobbyGuardTest self-test main.'
|
||||
description = 'Runs the LobbyGuardTest and MenuTilesTest self-test mains.'
|
||||
dependsOn 'menuTilesTest'
|
||||
classpath = sourceSets.lobbyTest.runtimeClasspath
|
||||
mainClass = 'best.lolicon.felis.paper.LobbyGuardTest'
|
||||
}
|
||||
@@ -48,14 +48,16 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
* <p><b>Flow.</b> {@code /menu} sends a {@code ListRequest}; the proxy answers with a
|
||||
* {@code ListUpdate} naming every user server it routes, built from the same registry
|
||||
* it routes by, so a server created in the panel shows up here without anyone editing
|
||||
* this plugin. The menu then paints a "loading" tile per server on the page (45 per
|
||||
* page, arrows in the bottom row) and fires a {@code StatusQuery} for each; the proxy
|
||||
* answers with {@code StatusUpdate} frames that repaint each tile by phase +
|
||||
* ownership. Clicking a tile sends a
|
||||
* {@code ClaimRequest} when it is claimable (ownerless + stopped → "Claim &
|
||||
* Start") or a {@code WakeRequest} otherwise (the single frame behind both the "Join"
|
||||
* of a running owned server and the "Wake" of a stopped owned one), then closes the
|
||||
* menu. A claim refusal comes back as an {@code Error} frame and is shown to the
|
||||
* this plugin. The list puts the player's own servers first and carries felis-api's
|
||||
* verdict on each: whether this player may start it, and why not. The menu then
|
||||
* paints a "loading" tile per server on the page (45 per page, arrows in the bottom
|
||||
* row) and fires a {@code StatusQuery} for each; the proxy answers with
|
||||
* {@code StatusUpdate} frames that repaint each tile ({@link MenuTiles}): Join when it
|
||||
* is up, Claim & Start when it is ownerless, a grey tile naming the reason when the
|
||||
* player may not start it, Start otherwise, with the phase in the player's language.
|
||||
* Claim sends a {@code ClaimRequest}; Join and Start both send a {@code WakeRequest};
|
||||
* each closes the menu. A grey tile sends nothing: its reason goes to chat and the
|
||||
* menu stays open. A claim refusal comes back as an {@code Error} frame and is shown to the
|
||||
* player here; wake-path refusals (policy gate, capacity) are chat messages the
|
||||
* proxy's waiting queue sends directly. Readiness arrives as {@code TransferReady}
|
||||
* just before the proxy Connects them.
|
||||
@@ -122,7 +124,7 @@ public final class FelisPaperPlugin extends JavaPlugin implements Listener, Plug
|
||||
}, LIST_TIMEOUT_TICKS);
|
||||
}
|
||||
|
||||
private void openPage(Player player, List<String> all, int page) {
|
||||
private void openPage(Player player, List<String> all, Map<String, String> access, int page) {
|
||||
boolean zh = zh(player);
|
||||
if (all.isEmpty()) {
|
||||
player.sendMessage(Component.text(
|
||||
@@ -133,7 +135,7 @@ public final class FelisPaperPlugin extends JavaPlugin implements Listener, Plug
|
||||
}
|
||||
int pages = MenuHolder.pageCount(all.size());
|
||||
int p = Math.max(0, Math.min(page, pages - 1));
|
||||
MenuHolder holder = new MenuHolder(all, p);
|
||||
MenuHolder holder = new MenuHolder(all, access, p);
|
||||
List<String> view = holder.servers();
|
||||
int size = pages > 1 ? 54 : invSize(view.size());
|
||||
Inventory inv = Bukkit.createInventory(holder, size, menuTitle(zh, p, pages));
|
||||
@@ -180,7 +182,7 @@ public final class FelisPaperPlugin extends JavaPlugin implements Listener, Plug
|
||||
if (holder.pages() > 1 && (slot == PREV_SLOT || slot == NEXT_SLOT)) {
|
||||
int target = holder.page() + (slot == PREV_SLOT ? -1 : 1);
|
||||
if (target >= 0 && target < holder.pages()) {
|
||||
openPage(player, holder.all(), target);
|
||||
openPage(player, holder.all(), holder.access(), target);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -192,13 +194,17 @@ public final class FelisPaperPlugin extends JavaPlugin implements Listener, Plug
|
||||
if (state == null) {
|
||||
return; // still loading — no status yet, so we don't know which frame to send
|
||||
}
|
||||
// Claimable (ownerless + stopped) → Claim & Start; everything else → Wake
|
||||
// (which the proxy treats as Join when the owned server is already running).
|
||||
if (state.claimable()) {
|
||||
sendUpstream(player, ControlFrame.claimRequest(player.getName(), server));
|
||||
} else {
|
||||
sendUpstream(player, ControlFrame.wakeRequest(player.getName(), server));
|
||||
boolean zh = zh(player);
|
||||
MenuTiles.Tile tile = MenuTiles.tile(state, holder.verdict(server), zh);
|
||||
ControlFrame click = MenuTiles.click(tile, player.getName(), server);
|
||||
if (click == null) {
|
||||
// A start felis-api would refuse: say why and leave the menu open, so the
|
||||
// player can pick another server.
|
||||
player.sendMessage(Component.text("⚠ " + server + ": " + MenuTiles.chatReason(tile, zh),
|
||||
NamedTextColor.YELLOW));
|
||||
return;
|
||||
}
|
||||
sendUpstream(player, click);
|
||||
player.closeInventory();
|
||||
}
|
||||
|
||||
@@ -231,7 +237,7 @@ public final class FelisPaperPlugin extends JavaPlugin implements Listener, Plug
|
||||
// Only a /menu that is still waiting opens; a late answer after the
|
||||
// timeout message is dropped rather than popping a menu up unasked.
|
||||
if (pendingOpen.remove(player.getUniqueId()) != null) {
|
||||
openPage(player, frame.servers(), 0);
|
||||
openPage(player, frame.servers(), MenuTiles.accessByName(frame), 0);
|
||||
}
|
||||
break;
|
||||
case ControlFrame.STATUS_UPDATE:
|
||||
@@ -266,7 +272,7 @@ public final class FelisPaperPlugin extends JavaPlugin implements Listener, Plug
|
||||
return; // a server we are not showing
|
||||
}
|
||||
holder.put(frame.server(), frame);
|
||||
top.setItem(slot, tile(frame, zh(player)));
|
||||
top.setItem(slot, tile(frame, holder.verdict(frame.server()), zh(player)));
|
||||
}
|
||||
|
||||
// markUnavailable repaints a still-loading tile whose status query was refused, so
|
||||
@@ -318,30 +324,42 @@ public final class FelisPaperPlugin extends JavaPlugin implements Listener, Plug
|
||||
return item;
|
||||
}
|
||||
|
||||
private ItemStack tile(ControlFrame f, boolean zh) {
|
||||
private ItemStack tile(ControlFrame f, String verdict, boolean zh) {
|
||||
MenuTiles.Tile t = MenuTiles.tile(f, verdict, zh);
|
||||
Material material;
|
||||
String action;
|
||||
NamedTextColor color;
|
||||
if (f.claimable()) {
|
||||
material = Material.GOLD_BLOCK;
|
||||
action = zh ? "认领并启动" : "Claim & Start";
|
||||
color = NamedTextColor.GOLD;
|
||||
} else if (f.ready()) {
|
||||
material = Material.LIME_CONCRETE;
|
||||
action = zh ? "加入" : "Join";
|
||||
color = NamedTextColor.GREEN;
|
||||
} else {
|
||||
material = Material.RED_CONCRETE;
|
||||
action = zh ? "唤醒" : "Wake";
|
||||
color = NamedTextColor.RED;
|
||||
switch (t.kind()) {
|
||||
case CLAIM -> {
|
||||
material = Material.GOLD_BLOCK;
|
||||
color = NamedTextColor.GOLD;
|
||||
}
|
||||
case JOIN -> {
|
||||
material = Material.LIME_CONCRETE;
|
||||
color = NamedTextColor.GREEN;
|
||||
}
|
||||
case LOCKED -> {
|
||||
material = Material.GRAY_CONCRETE;
|
||||
color = NamedTextColor.GRAY;
|
||||
}
|
||||
default -> {
|
||||
material = Material.RED_CONCRETE;
|
||||
color = NamedTextColor.RED;
|
||||
}
|
||||
}
|
||||
ItemStack item = new ItemStack(material);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
meta.displayName(Component.text(action + " · " + f.server(), color)
|
||||
meta.displayName(Component.text((t.mine() ? "★ " : "") + t.action() + " · " + f.server(), color)
|
||||
.decoration(TextDecoration.ITALIC, false));
|
||||
List<Component> lore = new ArrayList<>();
|
||||
lore.add(line(zh ? "状态" : "Status", f.phase() == null || f.phase().isEmpty() ? "?" : f.phase()));
|
||||
if (t.mine()) {
|
||||
lore.add(Component.text(zh ? "你的服务器" : "Your server", NamedTextColor.GOLD)
|
||||
.decoration(TextDecoration.ITALIC, false));
|
||||
}
|
||||
lore.add(line(zh ? "状态" : "Status", MenuTiles.phase(f.phase(), zh)));
|
||||
lore.add(line(zh ? "在线" : "Players", f.playersOnline() + "/" + f.playersMax()));
|
||||
for (String r : t.reason()) {
|
||||
lore.add(Component.text(r, NamedTextColor.YELLOW).decoration(TextDecoration.ITALIC, false));
|
||||
}
|
||||
meta.lore(lore);
|
||||
item.setItemMeta(meta);
|
||||
return item;
|
||||
|
||||
@@ -18,10 +18,11 @@ import java.util.Map;
|
||||
* on the plugin) means it is garbage-collected with the menu and never leaks across
|
||||
* reopen.
|
||||
*
|
||||
* <p>It holds the full server list the proxy sent (so paging needs no new request),
|
||||
* which page is showing, and the latest {@link ControlFrame} seen for each server on
|
||||
* it, so a click knows whether to send a Claim or a Wake without re-querying. Within
|
||||
* a page the list index <em>is</em> the slot.
|
||||
* <p>It holds the full server list the proxy sent and felis-api's verdict for this
|
||||
* player on each (so paging needs no new request), which page is showing, and the
|
||||
* latest {@link ControlFrame} seen for each server on it, so a click knows what to
|
||||
* send ({@link MenuTiles}) without re-querying. Within a page the list index
|
||||
* <em>is</em> the slot.
|
||||
*/
|
||||
final class MenuHolder implements InventoryHolder {
|
||||
|
||||
@@ -29,13 +30,15 @@ final class MenuHolder implements InventoryHolder {
|
||||
static final int PAGE_SIZE = 45;
|
||||
|
||||
private final List<String> all;
|
||||
private final Map<String, String> access; // server → verdict, when known
|
||||
private final int page;
|
||||
private final List<String> servers; // this page; index = slot
|
||||
private final Map<String, ControlFrame> latest = new HashMap<>();
|
||||
private Inventory inventory;
|
||||
|
||||
MenuHolder(List<String> all, int page) {
|
||||
MenuHolder(List<String> all, Map<String, String> access, int page) {
|
||||
this.all = all;
|
||||
this.access = access;
|
||||
this.page = page;
|
||||
int from = Math.min(page * PAGE_SIZE, all.size());
|
||||
this.servers = all.subList(from, Math.min(from + PAGE_SIZE, all.size()));
|
||||
@@ -50,6 +53,16 @@ final class MenuHolder implements InventoryHolder {
|
||||
return all;
|
||||
}
|
||||
|
||||
/** access is felis-api's verdict per server, as the proxy sent it. */
|
||||
Map<String, String> access() {
|
||||
return access;
|
||||
}
|
||||
|
||||
/** verdict is felis-api's verdict for one server, or null when none came. */
|
||||
String verdict(String server) {
|
||||
return access.get(server);
|
||||
}
|
||||
|
||||
int page() {
|
||||
return page;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
package best.lolicon.felis.paper;
|
||||
|
||||
import best.lolicon.felis.link.ControlFrame;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MenuTiles decides what a {@code /menu} tile says and what clicking it does, from the
|
||||
* server's live status ({@code StatusUpdate}) and felis-api's verdict for this player
|
||||
* (the {@code ListUpdate}'s access list). It is the whole of the menu's judgement, kept
|
||||
* free of Bukkit so it can be tested as plain values; {@link FelisPaperPlugin} only
|
||||
* turns a {@link Tile} into an item.
|
||||
*
|
||||
* <p>The order follows the proxy's wake. A server that is up is open to every linked
|
||||
* player, so it is Join whatever the verdict. An ownerless one is Claim & Start:
|
||||
* claiming makes the player its owner before the wake. Past those, a verdict that
|
||||
* refuses the wake locks the tile and names the reason, so nobody queues for a start
|
||||
* that cannot happen. Everything else is Wake, including a tile with no verdict (the
|
||||
* proxy could not ask felis-api), which the wake then judges as before.
|
||||
*/
|
||||
final class MenuTiles {
|
||||
|
||||
/** Kind is what a click on the tile does. */
|
||||
enum Kind { CLAIM, JOIN, WAKE, LOCKED }
|
||||
|
||||
/**
|
||||
* Tile is one tile's content: its kind, the action word in its title, whether it is
|
||||
* the player's own server, and for a locked tile the reason, one lore line each.
|
||||
*/
|
||||
record Tile(Kind kind, String action, boolean mine, List<String> reason) {
|
||||
}
|
||||
|
||||
// The verdicts felis-api's menu-access answers (handleInternalMenuAccess).
|
||||
static final String OWNER = "owner";
|
||||
static final String RETIRING = "retiring";
|
||||
static final String START_FAILED = "start_failed";
|
||||
static final String OWNER_ONLY = "owner_only";
|
||||
static final String ALLOWLIST = "allowlist";
|
||||
|
||||
private MenuTiles() {
|
||||
}
|
||||
|
||||
static Tile tile(ControlFrame status, String verdict, boolean zh) {
|
||||
boolean mine = OWNER.equals(verdict);
|
||||
if (status.ready()) {
|
||||
return new Tile(Kind.JOIN, zh ? "加入" : "Join", mine, List.of());
|
||||
}
|
||||
if (status.claimable()) {
|
||||
return new Tile(Kind.CLAIM, zh ? "认领并启动" : "Claim & Start", false, List.of());
|
||||
}
|
||||
List<String> reason = lockReason(verdict, zh);
|
||||
if (!reason.isEmpty()) {
|
||||
return new Tile(Kind.LOCKED, zh ? "无法启动" : "Can't start", mine, reason);
|
||||
}
|
||||
return new Tile(Kind.WAKE, zh ? "启动" : "Start", mine, List.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* click is the frame a click on the tile sends, or null for a locked tile, which
|
||||
* sends nothing and tells the player why ({@link #chatReason}).
|
||||
*/
|
||||
static ControlFrame click(Tile tile, String player, String server) {
|
||||
switch (tile.kind()) {
|
||||
case CLAIM:
|
||||
return ControlFrame.claimRequest(player, server);
|
||||
case LOCKED:
|
||||
return null;
|
||||
default:
|
||||
// Join and Start are both a wake: the proxy joins a server that is up.
|
||||
return ControlFrame.wakeRequest(player, server);
|
||||
}
|
||||
}
|
||||
|
||||
/** chatReason is a locked tile's reason as one sentence. */
|
||||
static String chatReason(Tile tile, boolean zh) {
|
||||
return zh ? String.join(",", tile.reason()) + "。" : String.join(". ", tile.reason()) + ".";
|
||||
}
|
||||
|
||||
// lockReason is why a verdict refuses the wake, or empty when it does not.
|
||||
private static List<String> lockReason(String verdict, boolean zh) {
|
||||
if (verdict == null) {
|
||||
return List.of();
|
||||
}
|
||||
switch (verdict) {
|
||||
case RETIRING:
|
||||
return zh ? List.of("这台服务器已被放弃或正在删除", "不能再启动")
|
||||
: List.of("This server was given up or is being deleted", "It can't be started");
|
||||
case START_FAILED:
|
||||
return zh ? List.of("多次启动失败,已停止自动重试", "需要在网页控制台处理")
|
||||
: List.of("It failed to start several times", "It needs a fix on the web console");
|
||||
case OWNER_ONLY:
|
||||
return zh ? List.of("只有主人能启动这台服务器", "它运行时任何人都能加入")
|
||||
: List.of("Only its owner can start it", "Anyone can join while it's running");
|
||||
case ALLOWLIST:
|
||||
return zh ? List.of("你不在这台服务器的启动名单上", "它运行时任何人都能加入")
|
||||
: List.of("You're not on its start list", "Anyone can join while it's running");
|
||||
default:
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/** phase is the tile's status line: the lifecycle phase in the player's language. */
|
||||
static String phase(String phase, boolean zh) {
|
||||
if (phase == null) {
|
||||
return zh ? "未知" : "Unknown";
|
||||
}
|
||||
switch (phase) {
|
||||
case "Running":
|
||||
return zh ? "运行中" : "Running";
|
||||
case "Starting":
|
||||
return zh ? "启动中" : "Starting";
|
||||
case "Stopping":
|
||||
return zh ? "停止中" : "Stopping";
|
||||
case "Stopped":
|
||||
return zh ? "已停止" : "Stopped";
|
||||
case "Failed":
|
||||
return zh ? "启动失败" : "Failed to start";
|
||||
default:
|
||||
return zh ? "未知" : "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* accessByName reads a {@code ListUpdate}'s verdicts into server → verdict, leaving
|
||||
* out the servers it gave none for (and every server when the proxy sent names only).
|
||||
*/
|
||||
static Map<String, String> accessByName(ControlFrame list) {
|
||||
Map<String, String> out = new HashMap<>();
|
||||
List<String> names = list.servers();
|
||||
List<String> access = list.access();
|
||||
for (int i = 0; i < names.size() && i < access.size(); i++) {
|
||||
if (!access.get(i).isEmpty()) {
|
||||
out.put(names.get(i), access.get(i));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package best.lolicon.felis.paper;
|
||||
|
||||
import best.lolicon.felis.link.Control;
|
||||
import best.lolicon.felis.link.ControlFrame;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MenuTilesTest checks the menu's judgement: which tile a server gets from its live
|
||||
* status and felis-api's verdict for the player, what a click on it sends, the reason
|
||||
* a grey tile gives, the phase in each language, and that the verdicts survive the
|
||||
* wire and the paging. Framework free: a failed assertion throws.
|
||||
*
|
||||
* <p>Run: {@code ./gradlew menuTilesTest} (or {@code lobbyTest}, which runs both mains)
|
||||
* in plugins/paper.
|
||||
*/
|
||||
public final class MenuTilesTest {
|
||||
|
||||
private static int checks;
|
||||
|
||||
// Every verdict felis-api answers, then none, an empty one, and one from a newer felis-api.
|
||||
private static final List<String> VERDICTS = Arrays.asList(
|
||||
"owner", "wake", "owner_only", "allowlist", "retiring", "start_failed", null, "", "future_verdict");
|
||||
|
||||
public static void main(String[] args) {
|
||||
upServers();
|
||||
ownerless();
|
||||
stopped();
|
||||
clicks();
|
||||
phases();
|
||||
wire();
|
||||
System.out.println("MenuTilesTest OK (" + checks + " checks)");
|
||||
}
|
||||
|
||||
// An up server is open to every linked player: Join whatever the verdict, ownerless or not.
|
||||
private static void upServers() {
|
||||
for (String v : VERDICTS) {
|
||||
for (boolean claimable : new boolean[] {false, true}) {
|
||||
MenuTiles.Tile t = MenuTiles.tile(status(true, claimable), v, true);
|
||||
assertEq("up, " + v + ", claimable=" + claimable, "JOIN 加入 " + "owner".equals(v) + " []", brief(t));
|
||||
}
|
||||
}
|
||||
assertEq("up, English", "JOIN Join true []", brief(MenuTiles.tile(status(true, false), "owner", false)));
|
||||
}
|
||||
|
||||
// An ownerless server that is down is Claim & Start: the claim makes the player its owner first.
|
||||
private static void ownerless() {
|
||||
for (String v : VERDICTS) {
|
||||
assertEq("ownerless, " + v, "CLAIM 认领并启动 false []", brief(MenuTiles.tile(status(false, true), v, true)));
|
||||
}
|
||||
assertEq("ownerless, English", "CLAIM Claim & Start false []",
|
||||
brief(MenuTiles.tile(status(false, true), "owner_only", false)));
|
||||
}
|
||||
|
||||
// Down and owned: the verdict decides between Start and a grey tile with its reason.
|
||||
private static void stopped() {
|
||||
Map<String, String> zh = new HashMap<>();
|
||||
zh.put("owner", "WAKE 启动 true []");
|
||||
zh.put("wake", "WAKE 启动 false []");
|
||||
zh.put(null, "WAKE 启动 false []");
|
||||
zh.put("", "WAKE 启动 false []");
|
||||
zh.put("future_verdict", "WAKE 启动 false []");
|
||||
zh.put("owner_only", "LOCKED 无法启动 false [只有主人能启动这台服务器, 它运行时任何人都能加入]");
|
||||
zh.put("allowlist", "LOCKED 无法启动 false [你不在这台服务器的启动名单上, 它运行时任何人都能加入]");
|
||||
zh.put("retiring", "LOCKED 无法启动 false [这台服务器已被放弃或正在删除, 不能再启动]");
|
||||
zh.put("start_failed", "LOCKED 无法启动 false [多次启动失败,已停止自动重试, 需要在网页控制台处理]");
|
||||
for (String v : VERDICTS) {
|
||||
assertEq("stopped, " + v, zh.get(v), brief(MenuTiles.tile(status(false, false), v, true)));
|
||||
}
|
||||
|
||||
Map<String, String> en = new HashMap<>();
|
||||
en.put("owner", "WAKE Start true []");
|
||||
en.put("owner_only", "LOCKED Can't start false [Only its owner can start it, Anyone can join while it's running]");
|
||||
en.put("allowlist", "LOCKED Can't start false [You're not on its start list, Anyone can join while it's running]");
|
||||
en.put("retiring", "LOCKED Can't start false [This server was given up or is being deleted, It can't be started]");
|
||||
en.put("start_failed", "LOCKED Can't start false [It failed to start several times, It needs a fix on the web console]");
|
||||
en.forEach((v, want) ->
|
||||
assertEq("stopped, English, " + v, want, brief(MenuTiles.tile(status(false, false), v, false))));
|
||||
}
|
||||
|
||||
// Claim sends a ClaimRequest, Join and Start a WakeRequest, a grey tile nothing.
|
||||
private static void clicks() {
|
||||
ControlFrame down = status(false, false);
|
||||
assertEq("claim click", ControlFrame.claimRequest("Steve", "alpha"),
|
||||
MenuTiles.click(MenuTiles.tile(status(false, true), "wake", true), "Steve", "alpha"));
|
||||
assertEq("join click", ControlFrame.wakeRequest("Steve", "alpha"),
|
||||
MenuTiles.click(MenuTiles.tile(status(true, false), "owner_only", true), "Steve", "alpha"));
|
||||
assertEq("start click", ControlFrame.wakeRequest("Steve", "alpha"),
|
||||
MenuTiles.click(MenuTiles.tile(down, "owner", true), "Steve", "alpha"));
|
||||
assertEq("start click, no verdict", ControlFrame.wakeRequest("Steve", "alpha"),
|
||||
MenuTiles.click(MenuTiles.tile(down, null, true), "Steve", "alpha"));
|
||||
for (String v : List.of("owner_only", "allowlist", "retiring", "start_failed")) {
|
||||
assertEq("locked click sends nothing: " + v, null,
|
||||
MenuTiles.click(MenuTiles.tile(down, v, true), "Steve", "alpha"));
|
||||
}
|
||||
assertEq("chat reason, zh", "只有主人能启动这台服务器,它运行时任何人都能加入。",
|
||||
MenuTiles.chatReason(MenuTiles.tile(down, "owner_only", true), true));
|
||||
assertEq("chat reason, en", "Only its owner can start it. Anyone can join while it's running.",
|
||||
MenuTiles.chatReason(MenuTiles.tile(down, "owner_only", false), false));
|
||||
}
|
||||
|
||||
private static void phases() {
|
||||
String[][] table = {
|
||||
{"Running", "运行中", "Running"},
|
||||
{"Starting", "启动中", "Starting"},
|
||||
{"Stopping", "停止中", "Stopping"},
|
||||
{"Stopped", "已停止", "Stopped"},
|
||||
{"Failed", "启动失败", "Failed to start"},
|
||||
{"Unknown", "未知", "Unknown"},
|
||||
{null, "未知", "Unknown"},
|
||||
{"", "未知", "Unknown"},
|
||||
{"Hibernating", "未知", "Unknown"},
|
||||
};
|
||||
for (String[] row : table) {
|
||||
assertEq("phase " + row[0] + ", zh", row[1], MenuTiles.phase(row[0], true));
|
||||
assertEq("phase " + row[0] + ", en", row[2], MenuTiles.phase(row[0], false));
|
||||
}
|
||||
}
|
||||
|
||||
// The verdicts ride the ListUpdate by position and must land on the right server,
|
||||
// on every page.
|
||||
private static void wire() {
|
||||
List<String> names = new ArrayList<>();
|
||||
List<String> access = new ArrayList<>();
|
||||
for (int i = 0; i < 50; i++) {
|
||||
names.add(String.format("s%02d", i));
|
||||
access.add(i == 0 ? "owner" : i == 47 ? "owner_only" : "");
|
||||
}
|
||||
ControlFrame list = Control.decode(Control.encode(ControlFrame.listUpdate(names, access)));
|
||||
Map<String, String> byName = MenuTiles.accessByName(list);
|
||||
assertEq("verdicts by name, unknowns left out", Map.of("s00", "owner", "s47", "owner_only"), byName);
|
||||
assertEq("names only: no verdicts",
|
||||
Map.of(), MenuTiles.accessByName(ControlFrame.listUpdate(List.of("a", "b"))));
|
||||
|
||||
MenuHolder second = new MenuHolder(list.servers(), byName, 1);
|
||||
assertEq("page 2 starts at s45", "s45", second.servers().get(0));
|
||||
assertEq("page 2 knows s47's verdict", "owner_only", second.verdict("s47"));
|
||||
assertEq("... and s46 has none", null, second.verdict("s46"));
|
||||
assertEq("paging keeps every verdict", byName, second.access());
|
||||
}
|
||||
|
||||
// status is a StatusUpdate for server "alpha" in the given state.
|
||||
private static ControlFrame status(boolean ready, boolean claimable) {
|
||||
return ControlFrame.statusUpdate("alpha", ready ? "Running" : "Stopped", ready, 0, 20, claimable);
|
||||
}
|
||||
|
||||
private static String brief(MenuTiles.Tile t) {
|
||||
return t.kind() + " " + t.action() + " " + t.mine() + " " + t.reason();
|
||||
}
|
||||
|
||||
private static void assertEq(String what, Object want, Object got) {
|
||||
if (want == null ? got != null : !want.equals(got)) {
|
||||
throw new AssertionError(what + ": got " + got + ", want " + want);
|
||||
}
|
||||
checks++;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user