Loading plugins/paper/src/main/java/best/lolicon/felis/paper/FelisPaperPlugin.java +138 −25 Changes for plugins/paper/src/main/java/best/lolicon/felis/paper/FelisPaperPlugin.java: 138 added lines, 25 removed lines. Original line number Diff line number Diff line Loading @@ -23,6 +23,9 @@ import org.bukkit.plugin.messaging.PluginMessageListener; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; /** * FelisPaperPlugin is the felis-paper lobby face (spec §12): the {@code /menu} (and Loading @@ -42,9 +45,13 @@ import java.util.List; * compiled in from the shared core; {@code FelisApiClient} and the token config are * not on the lobby's classpath at all. * * <p><b>Flow.</b> Opening the menu paints a "loading" tile per configured server 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 * <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 Loading @@ -55,16 +62,22 @@ import java.util.List; */ public final class FelisPaperPlugin extends JavaPlugin implements Listener, PluginMessageListener { private static final int MAX_TILES = 54; // a double chest, the GUI ceiling // Bottom-row navigation slots on a paged menu. private static final int PREV_SLOT = 45; private static final int PAGE_SLOT = 49; private static final int NEXT_SLOT = 53; // How long /menu waits for the proxy's ListUpdate before saying the list is down. private static final long LIST_TIMEOUT_TICKS = 60L; /** Server names to show as tiles, in display order; loaded from config. */ private final List<String> servers = new ArrayList<>(); /** Players who ran /menu and are waiting for the proxy's ListUpdate. */ private final Map<UUID, Boolean> pendingOpen = new ConcurrentHashMap<>(); @Override public void onEnable() { saveDefaultConfig(); servers.clear(); servers.addAll(getConfig().getStringList("servers")); if (!getConfig().getStringList("servers").isEmpty()) { getLogger().info("config.yml 'servers' is no longer read: the menu lists what the proxy routes."); } // Open both ends of felis:control. Outgoing carries Wake/Claim/StatusQuery to // the proxy; incoming receives StatusUpdate/TransferReady/Error back. Loading @@ -72,8 +85,8 @@ public final class FelisPaperPlugin extends JavaPlugin implements Listener, Plug getServer().getMessenger().registerIncomingPluginChannel(this, Control.CHANNEL, this); getServer().getPluginManager().registerEvents(this, this); getLogger().info("felis-paper enabled: " + servers.size() + " server tile(s), felis:control open. Pure UI face — no felis-api token."); getLogger().info("felis-paper enabled: felis:control open, server list from the proxy. " + "Pure UI face — no felis-api token."); } // ---- commands: /menu and /server both open the GUI ---- Loading @@ -89,24 +102,50 @@ public final class FelisPaperPlugin extends JavaPlugin implements Listener, Plug } private void openMenu(Player player) { UUID id = player.getUniqueId(); pendingOpen.put(id, Boolean.TRUE); sendUpstream(player, ControlFrame.listRequest()); getServer().getScheduler().runTaskLater(this, () -> { if (pendingOpen.remove(id) != null && player.isOnline()) { boolean zh = zh(player); player.sendMessage(Component.text( zh ? "暂时拿不到服务器列表,请稍后再试。" : "The server list isn't available right now — please try again shortly.", NamedTextColor.YELLOW)); } }, LIST_TIMEOUT_TICKS); } private void openPage(Player player, List<String> all, int page) { boolean zh = zh(player); if (servers.isEmpty()) { if (all.isEmpty()) { player.sendMessage(Component.text( zh ? "还没有配置任何服务器——请管理员先配置 felis-paper。" : "No servers are configured yet — ask an operator to set up felis-paper.", zh ? "还没有可加入的服务器——在网页控制台创建一个吧。" : "There are no servers to join yet — create one on the web console.", NamedTextColor.YELLOW)); return; } int shown = Math.min(servers.size(), MAX_TILES); List<String> view = new ArrayList<>(servers.subList(0, shown)); MenuHolder holder = new MenuHolder(view); Inventory inv = Bukkit.createInventory(holder, invSize(shown), menuTitle(zh)); int pages = MenuHolder.pageCount(all.size()); int p = Math.max(0, Math.min(page, pages - 1)); MenuHolder holder = new MenuHolder(all, p); List<String> view = holder.servers(); int size = pages > 1 ? 54 : invSize(view.size()); Inventory inv = Bukkit.createInventory(holder, size, menuTitle(zh, p, pages)); holder.setInventory(inv); for (int i = 0; i < shown; i++) { for (int i = 0; i < view.size(); i++) { inv.setItem(i, loadingTile(view.get(i), zh)); } if (pages > 1) { if (p > 0) { inv.setItem(PREV_SLOT, navItem(Material.ARROW, zh ? "上一页" : "Previous page")); } inv.setItem(PAGE_SLOT, navItem(Material.PAPER, (p + 1) + " / " + pages)); if (p < pages - 1) { inv.setItem(NEXT_SLOT, navItem(Material.ARROW, zh ? "下一页" : "Next page")); } } player.openInventory(inv); // Ask the proxy for live status of every tile; answers repaint them. // Ask the proxy for live status of every tile on this page; answers repaint them. for (String server : view) { sendUpstream(player, ControlFrame.statusQuery(server)); } Loading @@ -132,6 +171,13 @@ public final class FelisPaperPlugin extends JavaPlugin implements Listener, Plug Player player = (Player) event.getWhoClicked(); MenuHolder holder = (MenuHolder) top.getHolder(); int slot = event.getSlot(); 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); } return; } if (slot < 0 || slot >= holder.servers().size()) { return; // padding slot } Loading Loading @@ -175,10 +221,20 @@ public final class FelisPaperPlugin extends JavaPlugin implements Listener, Plug return; } switch (frame.type()) { case ControlFrame.LIST_UPDATE: // 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); } break; case ControlFrame.STATUS_UPDATE: applyStatus(player, frame); break; case ControlFrame.ERROR: if (frame.server() != null && markUnavailable(player, frame)) { break; // a tile's status query failed: shown on the tile itself } // The proxy already sanitizes transport faults; this is the only place // a claim/quota/policy refusal becomes visible to the player. player.sendMessage(Component.text("⚠ " + errorText(frame, zh(player)), NamedTextColor.RED)); Loading Loading @@ -207,6 +263,31 @@ public final class FelisPaperPlugin extends JavaPlugin implements Listener, Plug top.setItem(slot, tile(frame, zh(player))); } // markUnavailable repaints a still-loading tile whose status query was refused, so // it stops saying "loading" forever. Returns false when there is no such tile (the // error belongs to a click, and goes to chat). private boolean markUnavailable(Player player, ControlFrame error) { Inventory top = player.getOpenInventory().getTopInventory(); if (!(top.getHolder() instanceof MenuHolder)) { return false; } MenuHolder holder = (MenuHolder) top.getHolder(); int slot = holder.servers().indexOf(error.server()); if (slot < 0 || holder.latest(error.server()) != null) { return false; } boolean zh = zh(player); ItemStack item = new ItemStack(Material.BARRIER); ItemMeta meta = item.getItemMeta(); meta.displayName(Component.text(error.server(), NamedTextColor.DARK_GRAY) .decoration(TextDecoration.ITALIC, false)); meta.lore(List.of(Component.text(errorText(error, zh), NamedTextColor.GRAY) .decoration(TextDecoration.ITALIC, false))); item.setItemMeta(meta); top.setItem(slot, item); return true; } private void closeIfMenu(Player player) { if (player.getOpenInventory().getTopInventory().getHolder() instanceof MenuHolder) { player.closeInventory(); Loading @@ -215,9 +296,20 @@ public final class FelisPaperPlugin extends JavaPlugin implements Listener, Plug // ---- rendering ---- private static Component menuTitle(boolean zh) { return Component.text(zh ? "Felis 服务器" : "Felis Servers", NamedTextColor.AQUA) .decoration(TextDecoration.ITALIC, false); private static Component menuTitle(boolean zh, int page, int pages) { String title = zh ? "Felis 服务器" : "Felis Servers"; if (pages > 1) { title += " (" + (page + 1) + "/" + pages + ")"; } return Component.text(title, NamedTextColor.AQUA).decoration(TextDecoration.ITALIC, false); } private static ItemStack navItem(Material material, String label) { ItemStack item = new ItemStack(material); ItemMeta meta = item.getItemMeta(); meta.displayName(Component.text(label, NamedTextColor.YELLOW).decoration(TextDecoration.ITALIC, false)); item.setItemMeta(meta); return item; } private ItemStack tile(ControlFrame f, boolean zh) { Loading Loading @@ -278,13 +370,34 @@ public final class FelisPaperPlugin extends JavaPlugin implements Listener, Plug case "already_claimed": return zh ? "该服务器已被认领。" : "That server is already claimed."; case "at_capacity": return zh ? "集群当前已满,请稍后再试。" : "The cluster is full right now — please try again later."; case "cooldown": return zh ? "这台服务器刚被唤醒过,请稍候再试。" : "That server was just woken — try again in a moment."; case "forbidden": return zh ? "你没有权限这样做。" : "You're not allowed to do that."; case "not_found": case "bad_name": case "invalid_server_name": return zh ? "这台服务器已不存在。" : "That server no longer exists."; case "transport_error": case "interrupted": return zh ? "Felis 暂时不可用,请稍后再试。" : "Felis is temporarily unavailable — please try again."; default: break; } } return f.message() != null && !f.message().isEmpty() ? f.message() : (code != null ? code : (zh ? "请求失败,请重试。" : "Request failed — please try again.")); // An unmapped code carries felis-api's own English message; a Chinese client // gets a generic line rather than untranslated text. if (!zh && f.message() != null && !f.message().isEmpty()) { return f.message(); } return zh ? "请求失败,请重试。" : "Request failed — please try again."; } // ---- helpers ---- Loading plugins/paper/src/main/java/best/lolicon/felis/paper/MenuHolder.java +33 −8 Changes for plugins/paper/src/main/java/best/lolicon/felis/paper/MenuHolder.java: 33 added lines, 8 removed lines. Original line number Diff line number Diff line Loading @@ -18,22 +18,47 @@ import java.util.Map; * on the plugin) means it is garbage-collected with the menu and never leaks across * reopen. * * <p>It holds two things: the ordered list of server names (the list index <em>is</em> * the slot, so a click slot maps straight to a server) and the latest * {@link ControlFrame} seen for each, so a click knows whether to send a Claim or a * Wake without re-querying. * <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. */ final class MenuHolder implements InventoryHolder { private final List<String> servers; // index = slot /** Tiles per page: five rows, leaving the bottom row for page navigation. */ static final int PAGE_SIZE = 45; private final List<String> all; 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> servers) { this.servers = servers; MenuHolder(List<String> all, int page) { this.all = all; this.page = page; int from = Math.min(page * PAGE_SIZE, all.size()); this.servers = all.subList(from, Math.min(from + PAGE_SIZE, all.size())); } static int pageCount(int total) { return Math.max(1, (total + PAGE_SIZE - 1) / PAGE_SIZE); } /** all is the whole list the proxy sent, across every page. */ List<String> all() { return all; } int page() { return page; } int pages() { return pageCount(all.size()); } /** servers returns the tile order; the list index is the inventory slot. */ /** servers returns this page's tile order; the list index is the inventory slot. */ List<String> servers() { return servers; } Loading plugins/paper/src/main/resources/config.yml +10 −11 Changes for plugins/paper/src/main/resources/config.yml: 10 added lines, 11 removed lines. Original line number Diff line number Diff line Loading @@ -2,15 +2,14 @@ # # This plugin is a PURE UI face: it speaks only the felis:control plugin-message # channel to the Velocity proxy. It holds no felis-api token and never contacts # felis-api directly. The list below is only which server tiles to show in the # /menu GUI; the proxy (and felis-api behind it) stay the source of truth for # status, ownership and autostart — every tile is filled in by a live StatusQuery # over felis:control when the menu opens, and acting on a tile sends a Wake or # Claim frame that the proxy authorizes against the player's verified identity. # felis-api directly. # # Replace the examples below with the names of your felis servers (the CRD # metadata.name / the server's felis name, not its display title). Up to 54 are # shown. An empty list makes /menu say there is nothing to show. servers: - smp - creative # There is nothing to configure. /menu asks the proxy which servers to show # (a ListRequest over felis:control) and the proxy answers from the same registry # it routes by, so a server created in the panel appears here on its own. Tiles are # paged 45 at a time. Status, ownership and autostart stay with the proxy and # felis-api: every tile is filled in by a live StatusQuery when the menu opens, and # acting on a tile sends a Wake or Claim frame that the proxy authorizes against the # player's verified identity. # # Older versions read a static `servers:` list from this file. It is ignored now. Loading
plugins/paper/src/main/java/best/lolicon/felis/paper/FelisPaperPlugin.java +138 −25 Changes for plugins/paper/src/main/java/best/lolicon/felis/paper/FelisPaperPlugin.java: 138 added lines, 25 removed lines. Original line number Diff line number Diff line Loading @@ -23,6 +23,9 @@ import org.bukkit.plugin.messaging.PluginMessageListener; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; /** * FelisPaperPlugin is the felis-paper lobby face (spec §12): the {@code /menu} (and Loading @@ -42,9 +45,13 @@ import java.util.List; * compiled in from the shared core; {@code FelisApiClient} and the token config are * not on the lobby's classpath at all. * * <p><b>Flow.</b> Opening the menu paints a "loading" tile per configured server 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 * <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 Loading @@ -55,16 +62,22 @@ import java.util.List; */ public final class FelisPaperPlugin extends JavaPlugin implements Listener, PluginMessageListener { private static final int MAX_TILES = 54; // a double chest, the GUI ceiling // Bottom-row navigation slots on a paged menu. private static final int PREV_SLOT = 45; private static final int PAGE_SLOT = 49; private static final int NEXT_SLOT = 53; // How long /menu waits for the proxy's ListUpdate before saying the list is down. private static final long LIST_TIMEOUT_TICKS = 60L; /** Server names to show as tiles, in display order; loaded from config. */ private final List<String> servers = new ArrayList<>(); /** Players who ran /menu and are waiting for the proxy's ListUpdate. */ private final Map<UUID, Boolean> pendingOpen = new ConcurrentHashMap<>(); @Override public void onEnable() { saveDefaultConfig(); servers.clear(); servers.addAll(getConfig().getStringList("servers")); if (!getConfig().getStringList("servers").isEmpty()) { getLogger().info("config.yml 'servers' is no longer read: the menu lists what the proxy routes."); } // Open both ends of felis:control. Outgoing carries Wake/Claim/StatusQuery to // the proxy; incoming receives StatusUpdate/TransferReady/Error back. Loading @@ -72,8 +85,8 @@ public final class FelisPaperPlugin extends JavaPlugin implements Listener, Plug getServer().getMessenger().registerIncomingPluginChannel(this, Control.CHANNEL, this); getServer().getPluginManager().registerEvents(this, this); getLogger().info("felis-paper enabled: " + servers.size() + " server tile(s), felis:control open. Pure UI face — no felis-api token."); getLogger().info("felis-paper enabled: felis:control open, server list from the proxy. " + "Pure UI face — no felis-api token."); } // ---- commands: /menu and /server both open the GUI ---- Loading @@ -89,24 +102,50 @@ public final class FelisPaperPlugin extends JavaPlugin implements Listener, Plug } private void openMenu(Player player) { UUID id = player.getUniqueId(); pendingOpen.put(id, Boolean.TRUE); sendUpstream(player, ControlFrame.listRequest()); getServer().getScheduler().runTaskLater(this, () -> { if (pendingOpen.remove(id) != null && player.isOnline()) { boolean zh = zh(player); player.sendMessage(Component.text( zh ? "暂时拿不到服务器列表,请稍后再试。" : "The server list isn't available right now — please try again shortly.", NamedTextColor.YELLOW)); } }, LIST_TIMEOUT_TICKS); } private void openPage(Player player, List<String> all, int page) { boolean zh = zh(player); if (servers.isEmpty()) { if (all.isEmpty()) { player.sendMessage(Component.text( zh ? "还没有配置任何服务器——请管理员先配置 felis-paper。" : "No servers are configured yet — ask an operator to set up felis-paper.", zh ? "还没有可加入的服务器——在网页控制台创建一个吧。" : "There are no servers to join yet — create one on the web console.", NamedTextColor.YELLOW)); return; } int shown = Math.min(servers.size(), MAX_TILES); List<String> view = new ArrayList<>(servers.subList(0, shown)); MenuHolder holder = new MenuHolder(view); Inventory inv = Bukkit.createInventory(holder, invSize(shown), menuTitle(zh)); int pages = MenuHolder.pageCount(all.size()); int p = Math.max(0, Math.min(page, pages - 1)); MenuHolder holder = new MenuHolder(all, p); List<String> view = holder.servers(); int size = pages > 1 ? 54 : invSize(view.size()); Inventory inv = Bukkit.createInventory(holder, size, menuTitle(zh, p, pages)); holder.setInventory(inv); for (int i = 0; i < shown; i++) { for (int i = 0; i < view.size(); i++) { inv.setItem(i, loadingTile(view.get(i), zh)); } if (pages > 1) { if (p > 0) { inv.setItem(PREV_SLOT, navItem(Material.ARROW, zh ? "上一页" : "Previous page")); } inv.setItem(PAGE_SLOT, navItem(Material.PAPER, (p + 1) + " / " + pages)); if (p < pages - 1) { inv.setItem(NEXT_SLOT, navItem(Material.ARROW, zh ? "下一页" : "Next page")); } } player.openInventory(inv); // Ask the proxy for live status of every tile; answers repaint them. // Ask the proxy for live status of every tile on this page; answers repaint them. for (String server : view) { sendUpstream(player, ControlFrame.statusQuery(server)); } Loading @@ -132,6 +171,13 @@ public final class FelisPaperPlugin extends JavaPlugin implements Listener, Plug Player player = (Player) event.getWhoClicked(); MenuHolder holder = (MenuHolder) top.getHolder(); int slot = event.getSlot(); 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); } return; } if (slot < 0 || slot >= holder.servers().size()) { return; // padding slot } Loading Loading @@ -175,10 +221,20 @@ public final class FelisPaperPlugin extends JavaPlugin implements Listener, Plug return; } switch (frame.type()) { case ControlFrame.LIST_UPDATE: // 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); } break; case ControlFrame.STATUS_UPDATE: applyStatus(player, frame); break; case ControlFrame.ERROR: if (frame.server() != null && markUnavailable(player, frame)) { break; // a tile's status query failed: shown on the tile itself } // The proxy already sanitizes transport faults; this is the only place // a claim/quota/policy refusal becomes visible to the player. player.sendMessage(Component.text("⚠ " + errorText(frame, zh(player)), NamedTextColor.RED)); Loading Loading @@ -207,6 +263,31 @@ public final class FelisPaperPlugin extends JavaPlugin implements Listener, Plug top.setItem(slot, tile(frame, zh(player))); } // markUnavailable repaints a still-loading tile whose status query was refused, so // it stops saying "loading" forever. Returns false when there is no such tile (the // error belongs to a click, and goes to chat). private boolean markUnavailable(Player player, ControlFrame error) { Inventory top = player.getOpenInventory().getTopInventory(); if (!(top.getHolder() instanceof MenuHolder)) { return false; } MenuHolder holder = (MenuHolder) top.getHolder(); int slot = holder.servers().indexOf(error.server()); if (slot < 0 || holder.latest(error.server()) != null) { return false; } boolean zh = zh(player); ItemStack item = new ItemStack(Material.BARRIER); ItemMeta meta = item.getItemMeta(); meta.displayName(Component.text(error.server(), NamedTextColor.DARK_GRAY) .decoration(TextDecoration.ITALIC, false)); meta.lore(List.of(Component.text(errorText(error, zh), NamedTextColor.GRAY) .decoration(TextDecoration.ITALIC, false))); item.setItemMeta(meta); top.setItem(slot, item); return true; } private void closeIfMenu(Player player) { if (player.getOpenInventory().getTopInventory().getHolder() instanceof MenuHolder) { player.closeInventory(); Loading @@ -215,9 +296,20 @@ public final class FelisPaperPlugin extends JavaPlugin implements Listener, Plug // ---- rendering ---- private static Component menuTitle(boolean zh) { return Component.text(zh ? "Felis 服务器" : "Felis Servers", NamedTextColor.AQUA) .decoration(TextDecoration.ITALIC, false); private static Component menuTitle(boolean zh, int page, int pages) { String title = zh ? "Felis 服务器" : "Felis Servers"; if (pages > 1) { title += " (" + (page + 1) + "/" + pages + ")"; } return Component.text(title, NamedTextColor.AQUA).decoration(TextDecoration.ITALIC, false); } private static ItemStack navItem(Material material, String label) { ItemStack item = new ItemStack(material); ItemMeta meta = item.getItemMeta(); meta.displayName(Component.text(label, NamedTextColor.YELLOW).decoration(TextDecoration.ITALIC, false)); item.setItemMeta(meta); return item; } private ItemStack tile(ControlFrame f, boolean zh) { Loading Loading @@ -278,13 +370,34 @@ public final class FelisPaperPlugin extends JavaPlugin implements Listener, Plug case "already_claimed": return zh ? "该服务器已被认领。" : "That server is already claimed."; case "at_capacity": return zh ? "集群当前已满,请稍后再试。" : "The cluster is full right now — please try again later."; case "cooldown": return zh ? "这台服务器刚被唤醒过,请稍候再试。" : "That server was just woken — try again in a moment."; case "forbidden": return zh ? "你没有权限这样做。" : "You're not allowed to do that."; case "not_found": case "bad_name": case "invalid_server_name": return zh ? "这台服务器已不存在。" : "That server no longer exists."; case "transport_error": case "interrupted": return zh ? "Felis 暂时不可用,请稍后再试。" : "Felis is temporarily unavailable — please try again."; default: break; } } return f.message() != null && !f.message().isEmpty() ? f.message() : (code != null ? code : (zh ? "请求失败,请重试。" : "Request failed — please try again.")); // An unmapped code carries felis-api's own English message; a Chinese client // gets a generic line rather than untranslated text. if (!zh && f.message() != null && !f.message().isEmpty()) { return f.message(); } return zh ? "请求失败,请重试。" : "Request failed — please try again."; } // ---- helpers ---- Loading
plugins/paper/src/main/java/best/lolicon/felis/paper/MenuHolder.java +33 −8 Changes for plugins/paper/src/main/java/best/lolicon/felis/paper/MenuHolder.java: 33 added lines, 8 removed lines. Original line number Diff line number Diff line Loading @@ -18,22 +18,47 @@ import java.util.Map; * on the plugin) means it is garbage-collected with the menu and never leaks across * reopen. * * <p>It holds two things: the ordered list of server names (the list index <em>is</em> * the slot, so a click slot maps straight to a server) and the latest * {@link ControlFrame} seen for each, so a click knows whether to send a Claim or a * Wake without re-querying. * <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. */ final class MenuHolder implements InventoryHolder { private final List<String> servers; // index = slot /** Tiles per page: five rows, leaving the bottom row for page navigation. */ static final int PAGE_SIZE = 45; private final List<String> all; 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> servers) { this.servers = servers; MenuHolder(List<String> all, int page) { this.all = all; this.page = page; int from = Math.min(page * PAGE_SIZE, all.size()); this.servers = all.subList(from, Math.min(from + PAGE_SIZE, all.size())); } static int pageCount(int total) { return Math.max(1, (total + PAGE_SIZE - 1) / PAGE_SIZE); } /** all is the whole list the proxy sent, across every page. */ List<String> all() { return all; } int page() { return page; } int pages() { return pageCount(all.size()); } /** servers returns the tile order; the list index is the inventory slot. */ /** servers returns this page's tile order; the list index is the inventory slot. */ List<String> servers() { return servers; } Loading
plugins/paper/src/main/resources/config.yml +10 −11 Changes for plugins/paper/src/main/resources/config.yml: 10 added lines, 11 removed lines. Original line number Diff line number Diff line Loading @@ -2,15 +2,14 @@ # # This plugin is a PURE UI face: it speaks only the felis:control plugin-message # channel to the Velocity proxy. It holds no felis-api token and never contacts # felis-api directly. The list below is only which server tiles to show in the # /menu GUI; the proxy (and felis-api behind it) stay the source of truth for # status, ownership and autostart — every tile is filled in by a live StatusQuery # over felis:control when the menu opens, and acting on a tile sends a Wake or # Claim frame that the proxy authorizes against the player's verified identity. # felis-api directly. # # Replace the examples below with the names of your felis servers (the CRD # metadata.name / the server's felis name, not its display title). Up to 54 are # shown. An empty list makes /menu say there is nothing to show. servers: - smp - creative # There is nothing to configure. /menu asks the proxy which servers to show # (a ListRequest over felis:control) and the proxy answers from the same registry # it routes by, so a server created in the panel appears here on its own. Tiles are # paged 45 at a time. Status, ownership and autostart stay with the proxy and # felis-api: every tile is filled in by a live StatusQuery when the menu opens, and # acting on a tile sends a Wake or Claim frame that the proxy authorizes against the # player's verified identity. # # Older versions read a static `servers:` list from this file. It is ignored now.