feat: customize login and lobby spaces from the panel
This commit is contained in:
54 files changed
+1146
-158
No files matched your search
@@ -5,6 +5,7 @@ import best.lolicon.felis.link.FelisApiClient;
|
||||
import best.lolicon.felis.link.LinkClient;
|
||||
import best.lolicon.felis.link.LinkCode;
|
||||
import best.lolicon.felis.link.LinkConfig;
|
||||
import best.lolicon.felis.link.ExperienceConfig;
|
||||
import best.lolicon.felis.link.LinkConfigLoader;
|
||||
|
||||
import com.loohp.limbo.events.EventHandler;
|
||||
@@ -92,6 +93,7 @@ public final class FelisLimboPlugin extends LimboPlugin implements Listener {
|
||||
|
||||
private volatile Readiness readiness;
|
||||
private volatile LoginFlow flow;
|
||||
private ExperienceConfig experience;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
@@ -146,6 +148,7 @@ public final class FelisLimboPlugin extends LimboPlugin implements Listener {
|
||||
try {
|
||||
File props = new File(getDataFolder(), "felis-link.properties");
|
||||
cfg = LinkConfigLoader.load(props.toPath());
|
||||
experience = ExperienceConfig.load(ExperienceConfig.PATH);
|
||||
} catch (IOException e) {
|
||||
// Missing/half config: like the other Felis plugins, load un-crippled —
|
||||
// readiness stays up, the login flow just never turns on.
|
||||
@@ -161,7 +164,9 @@ public final class FelisLimboPlugin extends LimboPlugin implements Listener {
|
||||
}
|
||||
String consoleUrl = "https://" + panelHost;
|
||||
String lobby = GateConfig.lobby(System.getenv("FELIS_LOBBY_SERVER"));
|
||||
long timeoutMillis = GateConfig.loginTimeoutSeconds(System.getenv("FELIS_LOGIN_TIMEOUT_SECONDS")) * 1000L;
|
||||
long timeoutMillis = GateConfig.loginTimeoutSeconds(System.getenv("FELIS_LOGIN_TIMEOUT_SECONDS") != null
|
||||
? System.getenv("FELIS_LOGIN_TIMEOUT_SECONDS")
|
||||
: Long.toString(experience.number("loginTimeoutSeconds", GateConfig.DEFAULT_TIMEOUT_SECONDS))) * 1000L;
|
||||
this.flow = new LoginFlow(new LimboGate(), new FelisApiClient(cfg), new LinkClient(cfg),
|
||||
consoleUrl, timeoutMillis, System::currentTimeMillis, LOG);
|
||||
LOG.info("FelisLimbo: login flow ON — console=" + consoleUrl
|
||||
@@ -187,7 +192,7 @@ public final class FelisLimboPlugin extends LimboPlugin implements Listener {
|
||||
@Override
|
||||
public LoginFlow.Seat player(UUID id) {
|
||||
Player player = getServer().getPlayer(id);
|
||||
return player != null && player.isValid() ? new LimboSeat(player) : null;
|
||||
return player != null && player.isValid() ? new LimboSeat(player, experience) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -222,7 +227,7 @@ public final class FelisLimboPlugin extends LimboPlugin implements Listener {
|
||||
}
|
||||
|
||||
/** LimboSeat is one Limbo player as the flow sees them. */
|
||||
private record LimboSeat(Player player) implements LoginFlow.Seat {
|
||||
private record LimboSeat(Player player, ExperienceConfig experience) implements LoginFlow.Seat {
|
||||
@Override
|
||||
public String name() {
|
||||
return player.getName();
|
||||
@@ -235,7 +240,9 @@ public final class FelisLimboPlugin extends LimboPlugin implements Listener {
|
||||
|
||||
@Override
|
||||
public void showCode(LinkCode code, String url) {
|
||||
player.openBook(LoginBook.book(code.code(), url));
|
||||
if (experience.flag("openBook", true)) {
|
||||
player.openBook(LoginBook.book(code.code(), url, experience));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package best.lolicon.felis.limbo;
|
||||
|
||||
import best.lolicon.felis.link.ExperienceConfig;
|
||||
import java.util.Map;
|
||||
import net.kyori.adventure.inventory.Book;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
@@ -30,15 +32,20 @@ final class LoginBook {
|
||||
}
|
||||
|
||||
static Book book(String code, String url) {
|
||||
Component page = Component.text("Felis 登录 / Login\n\n")
|
||||
return book(code, url, new ExperienceConfig(Map.of()));
|
||||
}
|
||||
|
||||
static Book book(String code, String url, ExperienceConfig settings) {
|
||||
Component page = Component.text(settings.text("bookHeading", "Felis 登录 / Login") + "\n\n")
|
||||
.append(Component.text("绑定码 / Code:\n"))
|
||||
.append(Component.text(code + "\n\n").color(NamedTextColor.GOLD))
|
||||
.append(Component.text("▶ 点此打开登录页\n▶ Open login page\n")
|
||||
.append(Component.text(settings.text("bookAction", "▶ 点此打开登录页\n▶ Open login page") + "\n")
|
||||
.color(NamedTextColor.AQUA)
|
||||
.clickEvent(ClickEvent.openUrl(url)))
|
||||
.append(Component.text("\n在系统浏览器中完成。\nUse your SYSTEM browser —\nnot WeChat / QQ (passkey\nwon't work there).")
|
||||
.append(Component.text("\n" + settings.text("bookHelp", "在系统浏览器中完成。\nUse your SYSTEM browser —\nnot WeChat / QQ (passkey\nwon't work there)."))
|
||||
.color(NamedTextColor.GRAY));
|
||||
return Book.book(Component.text("Felis Login"), Component.text("Felis"), page);
|
||||
return Book.book(Component.text(settings.text("bookTitle", "Felis Login")),
|
||||
Component.text(settings.text("bookAuthor", "Felis")), page);
|
||||
}
|
||||
|
||||
/** chatLine renders one chat line with § colour codes, its web addresses clickable. */
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package best.lolicon.felis.limbo;
|
||||
|
||||
import best.lolicon.felis.link.ExperienceConfig;
|
||||
import best.lolicon.felis.link.Control;
|
||||
import best.lolicon.felis.link.ControlFrame;
|
||||
import best.lolicon.felis.link.FelisApiClient;
|
||||
@@ -24,6 +25,8 @@ import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -356,7 +359,22 @@ public final class LoginFlowTest {
|
||||
assertEq("lobby set", "hub", GateConfig.lobby(" hub "));
|
||||
}
|
||||
|
||||
private static void loginBook() {
|
||||
private static void loginBook() throws IOException {
|
||||
Path file = Files.createTempFile("felis-experience", ".json");
|
||||
ExperienceConfig settings;
|
||||
try {
|
||||
Files.writeString(file, "{\"bookTitle\":\"My Network\",\"bookHeading\":\"欢迎 / Welcome\"}");
|
||||
settings = ExperienceConfig.load(file);
|
||||
} finally {
|
||||
Files.deleteIfExists(file);
|
||||
}
|
||||
Book customized = LoginBook.book(CODE, CONSOLE + "/link", settings);
|
||||
assertEq("custom book title", "My Network", plain(customized.title()));
|
||||
assertTrue("custom book reads UTF-8 from the world volume", plain(customized.pages().get(0)).contains("欢迎 / Welcome"));
|
||||
assertEq("custom book keeps the code", true, plain(customized.pages().get(0)).contains(CODE));
|
||||
List<Component> customLinks = new ArrayList<>();
|
||||
collectLinks(customized.pages().get(0), customLinks);
|
||||
assertEq("custom book keeps the generated login URL", CONSOLE + "/link", customLinks.get(0).clickEvent().value());
|
||||
Book book = LoginBook.book(CODE, CONSOLE + "/link");
|
||||
assertEq("book title", "Felis Login", plain(book.title()));
|
||||
Component page = book.pages().get(0);
|
||||
|
||||
@@ -41,7 +41,8 @@ dependencies {
|
||||
// plugin-message channel and never holds a felis-api token or talks to felis-api
|
||||
// directly. We enforce that physically here — the shared source root is on the
|
||||
// path, but the include filter ships ONLY the paper package and the three codec
|
||||
// files (Control + ControlFrame + Json). FelisApiClient, LinkClient and the token
|
||||
// files (Control + ControlFrame + Json) plus the local ExperienceConfig reader.
|
||||
// FelisApiClient, LinkClient and the token
|
||||
// config are not compiled in at all, so the lobby cannot reach the API even by
|
||||
// mistake. If a codec class grows a new dependency, compilation fails loudly here
|
||||
// rather than silently widening the lobby's reach.
|
||||
@@ -53,6 +54,7 @@ sourceSets {
|
||||
include 'best/lolicon/felis/link/Control.java'
|
||||
include 'best/lolicon/felis/link/ControlFrame.java'
|
||||
include 'best/lolicon/felis/link/Json.java'
|
||||
include 'best/lolicon/felis/link/ExperienceConfig.java'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ package best.lolicon.felis.paper;
|
||||
|
||||
import best.lolicon.felis.link.Control;
|
||||
import best.lolicon.felis.link.ControlFrame;
|
||||
import best.lolicon.felis.link.ExperienceConfig;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
@@ -62,10 +65,16 @@ import java.util.List;
|
||||
public final class FelisPaperPlugin extends JavaPlugin {
|
||||
|
||||
private LobbyMenu menu;
|
||||
private ExperienceConfig experience;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
saveDefaultConfig();
|
||||
try {
|
||||
experience = ExperienceConfig.load(ExperienceConfig.PATH);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Cannot load lobby settings", e);
|
||||
}
|
||||
if (!getConfig().getStringList("servers").isEmpty()) {
|
||||
getLogger().info("config.yml 'servers' is no longer read: the menu lists what the proxy routes.");
|
||||
}
|
||||
@@ -81,7 +90,7 @@ public final class FelisPaperPlugin extends JavaPlugin {
|
||||
|
||||
// The lobby is a hub nobody can hurt or be hurt in (LobbyGuard). Worlds loaded
|
||||
// before this point get the rules here, later ones on their WorldLoadEvent.
|
||||
LobbyGuard guard = new LobbyGuard(getLogger());
|
||||
LobbyGuard guard = new LobbyGuard(getLogger(), experience);
|
||||
getServer().getPluginManager().registerEvents(guard, this);
|
||||
getServer().getWorlds().forEach(guard::protect);
|
||||
|
||||
@@ -125,6 +134,11 @@ public final class FelisPaperPlugin extends JavaPlugin {
|
||||
return FelisPaperPlugin.zh(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String menuTitle() {
|
||||
return experience.text(zh() ? "menuTitleZh" : "menuTitleEn", zh() ? "Felis 服务器" : "Felis Servers");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean online() {
|
||||
return player.isOnline();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package best.lolicon.felis.paper;
|
||||
|
||||
import best.lolicon.felis.link.ExperienceConfig;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
import net.kyori.adventure.text.event.HoverEvent;
|
||||
@@ -36,6 +37,7 @@ import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.vehicle.VehicleDestroyEvent;
|
||||
import org.bukkit.event.world.WorldLoadEvent;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
@@ -45,6 +47,8 @@ import java.util.logging.Logger;
|
||||
* Without it the lobby is a plain survival world: mobs at night, PvP, and every block
|
||||
* broken or placed by a passer-by stays in the world volume for the next player.
|
||||
*
|
||||
* <p>The following are defaults; felis-experience.json can adjust each behavior.
|
||||
*
|
||||
* <p><b>World.</b> Every world is made peaceful with natural spawning, PvP, mob
|
||||
* griefing and TNT off, time frozen at noon and the weather clear, and inventories
|
||||
* kept. These are world rules (level.dat), so they are set here on enable and on
|
||||
@@ -70,14 +74,20 @@ final class LobbyGuard implements Listener {
|
||||
/** Noon: the lobby is always lit. */
|
||||
private static final long NOON = 6000L;
|
||||
|
||||
static boolean guarded(Player player) {
|
||||
return !player.hasPermission(BUILD_PERMISSION);
|
||||
boolean guarded(Player player) {
|
||||
return settings.flag("protectBuild", true) && !player.hasPermission(BUILD_PERMISSION);
|
||||
}
|
||||
|
||||
private final Logger log;
|
||||
private final ExperienceConfig settings;
|
||||
|
||||
LobbyGuard(Logger log) {
|
||||
this(log, new ExperienceConfig(Map.of()));
|
||||
}
|
||||
|
||||
LobbyGuard(Logger log, ExperienceConfig settings) {
|
||||
this.log = log;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,25 +104,27 @@ final class LobbyGuard implements Listener {
|
||||
}
|
||||
}
|
||||
|
||||
static void applyRules(World world) {
|
||||
world.setDifficulty(Difficulty.PEACEFUL);
|
||||
void applyRules(World world) {
|
||||
world.setDifficulty(Difficulty.valueOf(settings.text("difficulty", "PEACEFUL")));
|
||||
// Only a world with its own clock has a time of day to set; the nether and the
|
||||
// end have none and refuse.
|
||||
if (!world.isFixedTime()) {
|
||||
world.setTime(NOON);
|
||||
if (settings.flag("freezeTime", true) && !world.isFixedTime()) {
|
||||
world.setTime(Math.floorMod(settings.number("time", NOON), 24000L));
|
||||
}
|
||||
world.setStorm(false);
|
||||
world.setThundering(false);
|
||||
world.setGameRule(GameRules.PVP, false);
|
||||
world.setGameRule(GameRules.SPAWN_MOBS, false);
|
||||
world.setGameRule(GameRules.SPAWN_WANDERING_TRADERS, false);
|
||||
world.setGameRule(GameRules.MOB_GRIEFING, false);
|
||||
world.setGameRule(GameRules.TNT_EXPLODES, false);
|
||||
world.setGameRule(GameRules.KEEP_INVENTORY, true);
|
||||
world.setGameRule(GameRules.IMMEDIATE_RESPAWN, true);
|
||||
world.setGameRule(GameRules.SHOW_ADVANCEMENT_MESSAGES, false);
|
||||
world.setGameRule(GameRules.ADVANCE_TIME, false);
|
||||
world.setGameRule(GameRules.ADVANCE_WEATHER, false);
|
||||
if (settings.flag("clearWeather", true)) {
|
||||
world.setStorm(false);
|
||||
world.setThundering(false);
|
||||
}
|
||||
world.setGameRule(GameRules.PVP, settings.flag("pvp", false));
|
||||
world.setGameRule(GameRules.SPAWN_MOBS, settings.flag("spawnMobs", false));
|
||||
world.setGameRule(GameRules.SPAWN_WANDERING_TRADERS, settings.flag("spawnMobs", false));
|
||||
world.setGameRule(GameRules.MOB_GRIEFING, settings.flag("mobGriefing", false));
|
||||
world.setGameRule(GameRules.TNT_EXPLODES, settings.flag("tntExplodes", false));
|
||||
world.setGameRule(GameRules.KEEP_INVENTORY, settings.flag("keepInventory", true));
|
||||
world.setGameRule(GameRules.IMMEDIATE_RESPAWN, settings.flag("immediateRespawn", true));
|
||||
world.setGameRule(GameRules.SHOW_ADVANCEMENT_MESSAGES, settings.flag("showAdvancementMessages", false));
|
||||
world.setGameRule(GameRules.ADVANCE_TIME, !settings.flag("freezeTime", true));
|
||||
world.setGameRule(GameRules.ADVANCE_WEATHER, !settings.flag("clearWeather", true));
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
@@ -125,20 +137,30 @@ final class LobbyGuard implements Listener {
|
||||
@EventHandler
|
||||
public void onJoin(PlayerJoinEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
player.setFoodLevel(20);
|
||||
if (guarded(player)) {
|
||||
player.setGameMode(GameMode.ADVENTURE);
|
||||
player.teleport(player.getWorld().getSpawnLocation());
|
||||
if (settings.flag("disableHunger", true)) {
|
||||
player.setFoodLevel(20);
|
||||
}
|
||||
if (!player.hasPermission(BUILD_PERMISSION)) {
|
||||
player.setGameMode(GameMode.valueOf(settings.text("gameMode", "ADVENTURE")));
|
||||
if (settings.flag("teleportOnJoin", true)) {
|
||||
player.teleport(player.getWorld().getSpawnLocation());
|
||||
}
|
||||
}
|
||||
if (settings.flag("showWelcome", true)) {
|
||||
player.sendMessage(hint(FelisPaperPlugin.zh(player)));
|
||||
}
|
||||
if (settings.flag("openMenuOnJoin", false)) {
|
||||
player.performCommand("menu");
|
||||
}
|
||||
player.sendMessage(hint(FelisPaperPlugin.zh(player)));
|
||||
}
|
||||
|
||||
static Component hint(boolean zh) {
|
||||
Component hint(boolean zh) {
|
||||
Component open = Component.text(zh ? "[打开服务器菜单]" : "[Open the server menu]",
|
||||
NamedTextColor.GREEN, TextDecoration.BOLD)
|
||||
.clickEvent(ClickEvent.runCommand("/menu"))
|
||||
.hoverEvent(HoverEvent.showText(Component.text(zh ? "点击运行 /menu" : "Click to run /menu")));
|
||||
return Component.text(zh ? "欢迎来到大厅。输入 /menu 或点击 " : "Welcome to the lobby. Type /menu or click ",
|
||||
return Component.text(settings.text(zh ? "welcomeZh" : "welcomeEn",
|
||||
zh ? "欢迎来到大厅。输入 /menu 或点击 " : "Welcome to the lobby. Type /menu or click "),
|
||||
NamedTextColor.GOLD)
|
||||
.append(open)
|
||||
.append(Component.text(zh ? ",选一个服务器进入。" : " to pick a server to join.",
|
||||
@@ -152,15 +174,17 @@ final class LobbyGuard implements Listener {
|
||||
if (!(event.getEntity() instanceof Player player)) {
|
||||
return;
|
||||
}
|
||||
event.setCancelled(true);
|
||||
if (event.getCause() == EntityDamageEvent.DamageCause.VOID) {
|
||||
if (settings.flag("invulnerable", true)) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
if (settings.flag("voidRescue", true) && event.getCause() == EntityDamageEvent.DamageCause.VOID) {
|
||||
player.teleport(player.getWorld().getSpawnLocation());
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW)
|
||||
public void onHunger(FoodLevelChangeEvent event) {
|
||||
if (event.getEntity() instanceof Player) {
|
||||
if (settings.flag("disableHunger", true) && event.getEntity() instanceof Player) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
@@ -211,14 +235,17 @@ final class LobbyGuard implements Listener {
|
||||
/** Fire spreads and burns with nobody behind it, so only a builder may start one. */
|
||||
@EventHandler(priority = EventPriority.LOW)
|
||||
public void onIgnite(BlockIgniteEvent event) {
|
||||
if (!(event.getIgnitingEntity() instanceof Player player) || guarded(player)) {
|
||||
if (settings.flag("protectBuild", true) &&
|
||||
(!(event.getIgnitingEntity() instanceof Player player) || guarded(player))) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW)
|
||||
public void onBurn(BlockBurnEvent event) {
|
||||
event.setCancelled(true);
|
||||
if (settings.flag("protectBuild", true)) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW)
|
||||
@@ -258,7 +285,7 @@ final class LobbyGuard implements Listener {
|
||||
}
|
||||
|
||||
/** guardedCulprit: the entity is a guarded player, or something one of them shot. */
|
||||
private static boolean guardedCulprit(Entity culprit) {
|
||||
private boolean guardedCulprit(Entity culprit) {
|
||||
if (culprit instanceof Projectile projectile && projectile.getShooter() instanceof Player shooter) {
|
||||
return guarded(shooter);
|
||||
}
|
||||
|
||||
@@ -42,6 +42,10 @@ final class LobbyMenu implements Listener, PluginMessageListener {
|
||||
/** zh mirrors the Velocity rule: Chinese when the client locale is zh-*. */
|
||||
boolean zh();
|
||||
|
||||
default String menuTitle() {
|
||||
return zh() ? "Felis 服务器" : "Felis Servers";
|
||||
}
|
||||
|
||||
boolean online();
|
||||
|
||||
void send(ControlFrame frame);
|
||||
@@ -189,7 +193,7 @@ final class LobbyMenu implements Listener, PluginMessageListener {
|
||||
slots[NEXT_SLOT] = new Nav(true, zh ? "下一页" : "Next page");
|
||||
}
|
||||
}
|
||||
screen.open(holder, title(zh, p, pages), size, slots);
|
||||
screen.open(holder, title(screen.menuTitle(), p, pages), size, slots);
|
||||
// Ask the proxy for live status of every tile on this page; answers repaint them.
|
||||
for (String server : view) {
|
||||
screen.send(ControlFrame.statusQuery(server));
|
||||
@@ -286,7 +290,10 @@ final class LobbyMenu implements Listener, PluginMessageListener {
|
||||
// ---- text ----
|
||||
|
||||
static String title(boolean zh, int page, int pages) {
|
||||
String title = zh ? "Felis 服务器" : "Felis Servers";
|
||||
return title(zh ? "Felis 服务器" : "Felis Servers", page, pages);
|
||||
}
|
||||
|
||||
static String title(String title, int page, int pages) {
|
||||
if (pages > 1) {
|
||||
title += " (" + (page + 1) + "/" + pages + ")";
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
# channel to the Velocity proxy. It holds no felis-api token and never contacts
|
||||
# felis-api directly.
|
||||
#
|
||||
# There is nothing to configure. /menu asks the proxy which servers to show
|
||||
# Player-facing settings are edited in the panel at /admin/lobby and stored in
|
||||
# /data/felis-experience.json. This file does not hold those settings.
|
||||
#
|
||||
# /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
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package best.lolicon.felis.paper;
|
||||
|
||||
import best.lolicon.felis.link.ExperienceConfig;
|
||||
import best.lolicon.felis.paper.Fakes.FakePlayer;
|
||||
import java.util.Map;
|
||||
import best.lolicon.felis.paper.Fakes.FakeWorld;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
@@ -82,6 +84,7 @@ public final class LobbyGuardTest {
|
||||
nobodyIsHurt();
|
||||
joining();
|
||||
worldRules();
|
||||
customization();
|
||||
System.out.println("LobbyGuardTest OK (" + checks + " checks)");
|
||||
}
|
||||
|
||||
@@ -196,6 +199,33 @@ public final class LobbyGuardTest {
|
||||
assertEq("... with the same click", List.of("/menu"), commands(zh.messages.get(0)));
|
||||
}
|
||||
|
||||
private static void customization() {
|
||||
LobbyGuard custom = new LobbyGuard(capturingLogger(), new ExperienceConfig(Map.of(
|
||||
"protectBuild", false, "invulnerable", false, "disableHunger", false,
|
||||
"teleportOnJoin", false, "gameMode", "SURVIVAL", "welcomeEn", "Hello, builders! ",
|
||||
"difficulty", "HARD", "freezeTime", false, "clearWeather", false)));
|
||||
FakePlayer player = new FakePlayer(false, world, Locale.US);
|
||||
custom.onJoin(new PlayerJoinEvent(player.player, Component.text("joined")));
|
||||
assertEq("custom mode", GameMode.SURVIVAL, player.gameMode);
|
||||
assertEq("custom join does not teleport", 0, player.teleports.size());
|
||||
assertEq("hunger remains enabled", 3, player.food);
|
||||
assertEq("custom welcome keeps menu click", List.of("/menu"), commands(player.messages.get(0)));
|
||||
assertEq("custom welcome", true, Fakes.text(player.messages.get(0)).startsWith("Hello, builders!"));
|
||||
BlockBreakEvent build = new BlockBreakEvent(Fakes.block(Material.STONE), player.player);
|
||||
custom.onBreak(build);
|
||||
assertEq("building can be opened", false, build.isCancelled());
|
||||
EntityDamageEvent damage = new EntityDamageEvent(player.player, DamageCause.FALL, HIT, 4);
|
||||
custom.onDamage(damage);
|
||||
assertEq("damage can be enabled", false, damage.isCancelled());
|
||||
FoodLevelChangeEvent hunger = new FoodLevelChangeEvent(player.player, 2);
|
||||
custom.onHunger(hunger);
|
||||
assertEq("hunger can be enabled", false, hunger.isCancelled());
|
||||
FakeWorld natural = new FakeWorld();
|
||||
custom.protect(natural.world);
|
||||
assertEq("custom difficulty, advancing time and weather", List.of("setDifficulty HARD"), natural.calls);
|
||||
assertEq("custom menu keeps pagination", "My Hub (2/3)", LobbyMenu.title("My Hub", 1, 3));
|
||||
}
|
||||
|
||||
private static void worldRules() {
|
||||
FakeWorld overworld = new FakeWorld("world", false, false);
|
||||
WARNINGS.clear();
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package best.lolicon.felis.link;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
|
||||
/** Player-facing settings on the persistent world volume, edited by the panel. */
|
||||
public final class ExperienceConfig {
|
||||
public static final Path PATH = Path.of("felis-experience.json");
|
||||
private final Map<?, ?> values;
|
||||
|
||||
public ExperienceConfig(Map<?, ?> values) {
|
||||
this.values = values;
|
||||
}
|
||||
|
||||
public static ExperienceConfig load(Path path) throws IOException {
|
||||
if (!Files.exists(path)) {
|
||||
return new ExperienceConfig(Map.of());
|
||||
}
|
||||
try {
|
||||
Object value = Json.parse(Files.readString(path));
|
||||
if (value instanceof Map<?, ?> map) {
|
||||
return new ExperienceConfig(map);
|
||||
}
|
||||
throw new IllegalArgumentException("expected an object");
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IOException("invalid " + path + ": " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public String text(String key, String fallback) {
|
||||
return values.get(key) instanceof String value ? value : fallback;
|
||||
}
|
||||
|
||||
public boolean flag(String key, boolean fallback) {
|
||||
return values.get(key) instanceof Boolean value ? value : fallback;
|
||||
}
|
||||
|
||||
public long number(String key, long fallback) {
|
||||
return values.get(key) instanceof Number value ? value.longValue() : fallback;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user