Fabric 1.20.1: server features (commands, LuckPerms, Placeholder API, economy API, client link) and client mod (HUD, editor, shop and market windows); Paper client link; docs and examples

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DjMbLQujBHunCCu5GpsHaT
This commit is contained in:
Claude committed 2026-09-30 19:38:18 +00:00
1 parent 84c9d46892
commit 2bcdf26351
43 files changed
+2112 -42

No files matched your search

@@ -194,6 +194,36 @@ public final class ClientLink {
send(p, m);
}
/**
* Events the panel queued for this server (level-ups, achievements, guild changes). Safe to call from any thread:
* each becomes a toast on the player's client and forces a fresh state.
*/
public void onPanelEvents(JsonArray events) {
for (JsonElement element : events) {
if (!element.isJsonObject()) continue;
JsonObject n = element.getAsJsonObject();
String kind = PlayerCache.str(n, "kind", "");
String uuidText = PlayerCache.str(n, "uuid", "");
JsonObject d = PlayerCache.obj(n, "data");
UUID uuid;
try { uuid = UUID.fromString(uuidText); } catch (IllegalArgumentException e) { continue; }
String title, text;
switch (kind) {
case "level_up" -> { title = "Level up!"; text = ("server".equals(PlayerCache.str(d, "scope", "")) ? "Server level " : "Level ") + (long) PlayerCache.num(d, "level", 0); }
case "achievement" -> { title = "Achievement unlocked"; text = PlayerCache.str(d, "title", ""); }
case "guild_join" -> { title = "Guild"; text = "Joined " + PlayerCache.str(d, "guild", "a guild"); }
case "guild_leave" -> { title = "Guild"; text = "Left " + PlayerCache.str(d, "guild", "your guild"); }
default -> { continue; }
}
final String t = title, x = text, k = kind;
env.platform.runMain(() -> env.platform.player(uuid).ifPresent(p -> {
notify(p, k, t, x);
lastState.remove(uuid);
cache.refresh(p, info -> pushState(p));
}));
}
}
/** About once a second: new chunk, new claims, or new state for every client. */
public void tick() {
for (UUID id : new ArrayList<>(clients)) {
@@ -114,6 +114,17 @@ class LinkTest {
assertEquals(35.0, shop.getAsJsonObject("held").get("unit").getAsDouble());
}
@Test void panelEventsBecomeToasts() {
link.receive(alex, msg("{\"t\":\"hello\"}"));
link.onPanelEvents(JsonParser.parseString("[{\"kind\":\"level_up\",\"uuid\":\"" + alex.id + "\",\"data\":{\"scope\":\"global\",\"level\":13}},"
+ "{\"kind\":\"achievement\",\"uuid\":\"" + alex.id + "\",\"data\":{\"title\":\"Deep Diver\"}},"
+ "{\"kind\":\"unknown\",\"uuid\":\"" + alex.id + "\",\"data\":{}},{\"kind\":\"level_up\",\"uuid\":\"nope\"}]").getAsJsonArray());
List<JsonObject> toasts = sent.stream().filter(m -> m.get("t").getAsString().equals("notify")).toList();
assertEquals(2, toasts.size());
assertEquals("Level 13", toasts.get(0).get("text").getAsString());
assertEquals("Deep Diver", toasts.get(1).get("text").getAsString());
}
@Test void disabledClientLinkIsSilent() {
kit.env.features = new Features(true, true, true, true, true, false, "$");
link.receive(alex, msg("{\"t\":\"hello\"}"));