refactor: 榜单改用原版计分板侧边栏显示

服务端读stats定期写入scoreboard objective;客户端/slsboard设本地sidebar display。删掉自绘HUD和网络包(GuiGraphicsExtractor渲染不走)。
This commit is contained in:
AxTps committed 2026-10-06 20:42:40 +08:00
1 parent 2097a241aa
commit 870223bc1e
8 files changed
+78 -411

No files matched your search

@@ -43,12 +43,6 @@ public class ServerMain implements ModInitializer, CarpetExtension {
LOGGER.info("[SLSA] onInitialize: managing Carpet extension"); LOGGER.info("[SLSA] onInitialize: managing Carpet extension");
INSTANCE = this; INSTANCE = this;
CarpetServer.manageExtension(this); CarpetServer.manageExtension(this);
com.github.zly2006.carpetslsaddition.net.BoardSyncPayload.register();
com.github.zly2006.carpetslsaddition.net.BoardTogglePayload.register();
// 玩家加入时同步榜单
net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents.JOIN.register(
(handler, sender, srv) ->
com.github.zly2006.carpetslsaddition.util.BoardSyncManager.syncTo(handler.getPlayer()));
} }
@Override @Override
@@ -60,7 +54,7 @@ public class ServerMain implements ModInitializer, CarpetExtension {
public void onServerLoaded(MinecraftServer server) { public void onServerLoaded(MinecraftServer server) {
ServerMain.server = server; ServerMain.server = server;
com.github.zly2006.carpetslsaddition.util.BotListManager.load(); com.github.zly2006.carpetslsaddition.util.BotListManager.load();
com.github.zly2006.carpetslsaddition.util.BoardSyncManager.syncToAll(server); com.github.zly2006.carpetslsaddition.util.BoardManager.ensureObjectives(server);
startBoardWatcher(server); startBoardWatcher(server);
} }
@@ -69,7 +63,7 @@ public class ServerMain implements ModInitializer, CarpetExtension {
Thread t = new Thread(() -> { Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) { while (!Thread.currentThread().isInterrupted()) {
try { try {
com.github.zly2006.carpetslsaddition.util.BoardSyncManager.tick(server); com.github.zly2006.carpetslsaddition.util.BoardManager.syncScores(server);
Thread.sleep(1000L); Thread.sleep(1000L);
} catch (InterruptedException e) { } catch (InterruptedException e) {
return; return;
@@ -1,7 +1,5 @@
package com.github.zly2006.carpetslsaddition.client; package com.github.zly2006.carpetslsaddition.client;
import com.github.zly2006.carpetslsaddition.net.BoardSyncPayload;
import com.github.zly2006.carpetslsaddition.net.BoardTogglePayload;
import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.arguments.StringArgumentType;
import net.fabricmc.api.ClientModInitializer; import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.api.EnvType; import net.fabricmc.api.EnvType;
@@ -9,75 +7,61 @@ import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.client.command.v2.ClientCommands; import net.fabricmc.fabric.api.client.command.v2.ClientCommands;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElementRegistry;
import net.minecraft.commands.SharedSuggestionProvider; import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.resources.Identifier; import net.minecraft.world.scores.DisplaySlot;
import net.minecraft.world.scores.Objective;
import java.util.ArrayList;
import java.util.List;
/** /**
* 客户端入口:接收榜单数据、注册 /slsboard 命令、注册 HUD 渲染。 * 客户端入口:/slsboard 命令,控制本地计分板侧边栏显示哪个榜。
* *
* <p>纯客户端功能:显示哪个榜由本地状态决定,各玩家可不同步。 * <p>利用原版计分板侧边栏渲染(可靠),各玩家本地切换、互不影响。
* 数据由服务端从 stats 同步到 scoreboard objective(见 BoardManager)。
*/ */
@Environment(EnvType.CLIENT) @Environment(EnvType.CLIENT)
public class BoardClient implements ClientModInitializer { public class BoardClient implements ClientModInitializer {
public static final Identifier HUD_ID = Identifier.fromNamespaceAndPath("slsa", "board_hud");
@Override @Override
public void onInitializeClient() { public void onInitializeClient() {
// 1) 接收服务端下发的榜单数据
ClientPlayNetworking.registerGlobalReceiver(BoardSyncPayload.TYPE, (payload, context) -> {
List<BoardClientState.Entry> entries = new ArrayList<>();
for (BoardSyncPayload.Entry e : payload.entries()) {
entries.add(new BoardClientState.Entry(e.name(), e.score()));
}
com.github.zly2006.carpetslsaddition.ServerMain.LOGGER.info("[SLSA-BOARD-C] recv board={} entries={}", payload.board(), entries.size());
context.client().execute(() -> BoardClientState.setBoard(payload.board(), entries));
});
// 接收开关状态
ClientPlayNetworking.registerGlobalReceiver(BoardTogglePayload.TYPE, (payload, context) -> {
context.client().execute(() -> BoardClientState.enabled = payload.enabled());
});
// 2) /slsboard 命令(纯客户端,非 OP 也能用)
ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> { ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> {
dispatcher.register( dispatcher.register(
ClientCommands.literal("slsboard") ClientCommands.literal("slsboard")
.then(ClientCommands.literal("deaths") .then(ClientCommands.literal("deaths").executes(c -> show(c.getSource(), "sls_deaths")))
.executes(c -> run(c.getSource(), () -> BoardClientState.toggle("deaths")))) .then(ClientCommands.literal("mined").executes(c -> show(c.getSource(), "sls_mined")))
.then(ClientCommands.literal("mined") .then(ClientCommands.literal("off").executes(c -> { clear(c.getSource()); return 1; }))
.executes(c -> run(c.getSource(), () -> BoardClientState.toggle("mined"))))
.then(ClientCommands.literal("off")
.executes(c -> run(c.getSource(), () -> BoardClientState.set(""))))
.then(ClientCommands.argument("board", StringArgumentType.word()) .then(ClientCommands.argument("board", StringArgumentType.word())
.suggests((c, b) -> SharedSuggestionProvider.suggest( .suggests((c, b) -> SharedSuggestionProvider.suggest(
new String[]{"deaths", "mined", "off"}, b)) new String[]{"deaths", "mined", "off"}, b))
.executes(c -> { .executes(c -> {
String v = StringArgumentType.getString(c, "board"); String v = StringArgumentType.getString(c, "board");
return run(c.getSource(), () -> BoardClientState.set("off".equals(v) ? "" : v)); if ("off".equals(v)) { clear(c.getSource()); return 1; }
return show(c.getSource(), "sls_" + v);
})) }))
); );
}); });
// 3) HUD 渲染
HudElementRegistry.addLast(HUD_ID, new BoardHudElement());
} }
/** 统一的命令入口:先检查服务端开关,再执行动作。 */ private static int show(FabricClientCommandSource source, String objectiveName) {
private static int run(FabricClientCommandSource source, Runnable action) { var mc = source.getClient();
if (!BoardClientState.enabled) { var level = mc.level;
source.sendError(Component.literal("[SLSA] 榜单功能未开启(服务端 /carpet slsBoardEnabled true)")); if (level == null) {
return 0; return 0;
} }
action.run(); Objective obj = level.getScoreboard().getObjective(objectiveName);
String d = BoardClientState.displaying; if (obj == null) {
source.sendFeedback(Component.literal("[SLSA] 榜单显示:" + (d.isEmpty() ? "关闭" : d))); source.sendError(Component.literal("[SLSA] 找不到计分板 " + objectiveName + "(服务端规则未开启?)"));
return 0;
}
level.getScoreboard().setDisplayObjective(DisplaySlot.SIDEBAR, obj);
source.sendFeedback(Component.literal("[SLSA] 榜单显示:" + objectiveName));
return 1; return 1;
} }
private static void clear(FabricClientCommandSource source) {
var level = source.getClient().level;
if (level != null) {
level.getScoreboard().setDisplayObjective(DisplaySlot.SIDEBAR, null);
}
source.sendFeedback(Component.literal("[SLSA] 榜单已关闭"));
}
} }
@@ -1,61 +0,0 @@
package com.github.zly2006.carpetslsaddition.client;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 客户端榜单状态(纯客户端,各玩家独立,不同步)。
*
* <ul>
* <li>{@link #displaying}:当前显示哪个榜("" = 不显示)。</li>
* <li>{@code deaths}/{@code mined}:从服务端收到的排行数据缓存。</li>
* </ul>
*/
public final class BoardClientState {
private BoardClientState() {}
/** 当前显示的榜:"" / "deaths" / "mined"。 */
public static volatile String displaying = "";
/** 服务端 Carpet 规则 slsBoardEnabled 的状态(未开启则禁止使用)。 */
public static volatile boolean enabled = false;
private static volatile List<Entry> deaths = Collections.emptyList();
private static volatile List<Entry> mined = Collections.emptyList();
public record Entry(String name, int score) {}
public static void setBoard(String board, List<Entry> entries) {
if ("deaths".equals(board)) {
deaths = entries;
} else if ("mined".equals(board)) {
mined = entries;
}
}
public static List<Entry> current() {
return switch (displaying) {
case "deaths" -> deaths;
case "mined" -> mined;
default -> Collections.emptyList();
};
}
public static void toggle(String board) {
displaying = displaying.equals(board) ? "" : board;
}
public static void set(String board) {
displaying = board;
}
public static String currentTitle() {
return switch (displaying) {
case "deaths" -> "死亡榜";
case "mined" -> "挖掘榜";
default -> "";
};
}
}
@@ -1,76 +0,0 @@
package com.github.zly2006.carpetslsaddition.client;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElement;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.network.chat.Component;
import java.util.List;
/**
* 右侧排行榜 HUD:显示死亡榜/挖掘榜的 Top N。
*
* <p>纯客户端渲染,内容来自 {@link BoardClientState}(服务端同步而来)。
*/
@Environment(EnvType.CLIENT)
public class BoardHudElement implements HudElement {
private static final int LINE_H = 10;
private static final int PAD = 4;
private static final int BG = 0x80000000; // 半透明黑
private static final int TITLE_COLOR = 0xFFFF55; // 黄
private static final int TEXT_COLOR = 0xFFFFFF; // 白
@Override
public void extractRenderState(GuiGraphicsExtractor g, DeltaTracker delta) {
String board = BoardClientState.displaying;
if (board == null || board.isEmpty()) {
return;
}
Minecraft mc = Minecraft.getInstance();
if (mc.player == null || mc.options.hideGui) {
return;
}
List<BoardClientState.Entry> list = BoardClientState.current();
if (list.isEmpty()) {
return;
}
Font font = mc.font;
// 关键:进入新的渲染层,否则画的内容不显示
g.nextStratum();
String title = BoardClientState.currentTitle();
int maxW = font.width(title);
for (BoardClientState.Entry e : list) {
maxW = Math.max(maxW, font.width(line(e)));
}
int screenW = g.guiWidth();
int screenH = g.guiHeight();
int x = screenW - maxW - PAD * 2 - 2;
// 背景(右侧垂直居中)
int h = PAD * 2 + LINE_H * (list.size() + 1);
int y = screenH / 2 - h / 2;
g.fill(x, y, x + maxW + PAD * 2, y + h, BG);
int ty = y + PAD;
g.text(font, Component.literal(title), x + PAD, ty, TITLE_COLOR);
ty += LINE_H;
for (int i = 0; i < list.size(); i++) {
BoardClientState.Entry e = list.get(i);
g.text(font, Component.literal(line(e)), x + PAD, ty, TEXT_COLOR);
ty += LINE_H;
}
}
private static String line(BoardClientState.Entry e) {
String name = e.name();
if (name.length() > 12) name = name.substring(0, 12);
return name + " " + e.score();
}
}
@@ -1,47 +0,0 @@
package com.github.zly2006.carpetslsaddition.net;
import com.github.zly2006.carpetslsaddition.ServerMain;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import java.util.List;
/**
* 服务端 → 客户端:同步排行榜数据。
*
* <p>携带某一种榜单(死亡/挖掘)的完整排行:条目为 (玩家名, 分数),已按分数降序。
* 客户端收到后缓存,供 HUD 渲染。
*/
public record BoardSyncPayload(String board, List<Entry> entries) implements CustomPacketPayload {
/** 单条排行数据。 */
public record Entry(String name, int score) {
public static final StreamCodec<RegistryFriendlyByteBuf, Entry> CODEC = StreamCodec.composite(
ByteBufCodecs.STRING_UTF8, Entry::name,
ByteBufCodecs.VAR_INT, Entry::score,
Entry::new
);
}
public static final CustomPacketPayload.Type<BoardSyncPayload> TYPE =
new CustomPacketPayload.Type<>(net.minecraft.resources.Identifier.fromNamespaceAndPath("slsa", "board_sync"));
public static final StreamCodec<RegistryFriendlyByteBuf, BoardSyncPayload> CODEC = StreamCodec.composite(
ByteBufCodecs.STRING_UTF8, BoardSyncPayload::board,
Entry.CODEC.apply(ByteBufCodecs.list()), BoardSyncPayload::entries,
BoardSyncPayload::new
);
@Override
public CustomPacketPayload.Type<? extends CustomPacketPayload> type() {
return TYPE;
}
/** 在 onInitialize 里调用,注册 payload 类型。 */
public static void register() {
net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry.clientboundPlay().register(TYPE, CODEC);
ServerMain.LOGGER.info("[SLSA] registered BoardSyncPayload");
}
}
@@ -1,31 +0,0 @@
package com.github.zly2006.carpetslsaddition.net;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
/**
* 服务端 → 客户端:榜单功能总开关状态(对应 Carpet 规则 {@code slsBoardEnabled})。
*
* <p>规则变化时下发;客户端收到后决定 {@code /slsboard} 是否可用。
*/
public record BoardTogglePayload(boolean enabled) implements CustomPacketPayload {
public static final CustomPacketPayload.Type<BoardTogglePayload> TYPE =
new CustomPacketPayload.Type<>(net.minecraft.resources.Identifier.fromNamespaceAndPath("slsa", "board_toggle"));
public static final StreamCodec<RegistryFriendlyByteBuf, BoardTogglePayload> CODEC = StreamCodec.composite(
ByteBufCodecs.BOOL, BoardTogglePayload::enabled,
BoardTogglePayload::new
);
@Override
public CustomPacketPayload.Type<? extends CustomPacketPayload> type() {
return TYPE;
}
public static void register() {
net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry.clientboundPlay().register(TYPE, CODEC);
}
}
@@ -4,6 +4,9 @@ import com.github.zly2006.carpetslsaddition.ServerMain;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.stats.ServerStatsCounter;
import net.minecraft.stats.Stat;
import net.minecraft.stats.Stats;
import net.minecraft.world.scores.Objective; import net.minecraft.world.scores.Objective;
import net.minecraft.world.scores.ScoreAccess; import net.minecraft.world.scores.ScoreAccess;
import net.minecraft.world.scores.ScoreHolder; import net.minecraft.world.scores.ScoreHolder;
@@ -13,12 +16,14 @@ import net.minecraft.world.scores.criteria.ObjectiveCriteria;
/** /**
* 死亡榜 / 挖掘榜的 scoreboard 管理器(服务端)。 * 死亡榜 / 挖掘榜的 scoreboard 管理器(服务端)。
* *
* <ul> * <p>数据源是 vanilla stats,但 stats 不会自动同步给其他客户端。
* <li>{@code sls_deaths}:使用原版 {@link ObjectiveCriteria#DEATH_COUNT} 判据,自动统计死亡次数。</li> * 因此本类定期把 stats 的聚合值写入 scoreboard objective,
* <li>{@code sls_mined}:使用 {@link ObjectiveCriteria#DUMMY} 判据,由 {@code BoardEvents} 在破坏方块时写入。</li> * 客户端再用原版计分板侧边栏显示(scoreboard 会自动同步给所有客户端)。
* </ul>
* *
* <p>objective 在服务器启动时自动创建;客户端会同步到这两个 objective,用于 HUD 显示。 * <ul>
* <li>{@code sls_deaths}:死亡次数(DUMMY 判据,从 stats 的 {@link Stats#DEATHS} 写入)。</li>
* <li>{@code sls_mined}:挖掘总数(DUMMY 判据,从 stats 的 {@link Stats#BLOCK_MINED} 聚合)。</li>
* </ul>
*/ */
public final class BoardManager { public final class BoardManager {
@@ -31,41 +36,54 @@ public final class BoardManager {
public static void ensureObjectives(MinecraftServer server) { public static void ensureObjectives(MinecraftServer server) {
Scoreboard sb = server.getScoreboard(); Scoreboard sb = server.getScoreboard();
if (sb.getObjective(OBJ_DEATHS) == null) { if (sb.getObjective(OBJ_DEATHS) == null) {
sb.addObjective( sb.addObjective(OBJ_DEATHS, ObjectiveCriteria.DUMMY,
OBJ_DEATHS, Component.literal("死亡榜"), ObjectiveCriteria.RenderType.INTEGER, true, null);
ObjectiveCriteria.DEATH_COUNT,
Component.literal("死亡榜"),
ObjectiveCriteria.RenderType.INTEGER,
true,
null
);
ServerMain.LOGGER.info("[SLSA] created scoreboard objective {}", OBJ_DEATHS); ServerMain.LOGGER.info("[SLSA] created scoreboard objective {}", OBJ_DEATHS);
} }
if (sb.getObjective(OBJ_MINED) == null) { if (sb.getObjective(OBJ_MINED) == null) {
sb.addObjective( sb.addObjective(OBJ_MINED, ObjectiveCriteria.DUMMY,
OBJ_MINED, Component.literal("挖掘榜"), ObjectiveCriteria.RenderType.INTEGER, true, null);
ObjectiveCriteria.DUMMY,
Component.literal("挖掘榜"),
ObjectiveCriteria.RenderType.INTEGER,
true,
null
);
ServerMain.LOGGER.info("[SLSA] created scoreboard objective {}", OBJ_MINED); ServerMain.LOGGER.info("[SLSA] created scoreboard objective {}", OBJ_MINED);
} }
} }
/** 玩家破坏一个方块 → 挖掘榜 +1。 */ /** 读取某玩家的挖掘总数(遍历 stats map 中 BLOCK_MINED 类型条目)。 */
public static void onBlockMined(ServerPlayer player, int count) { private static int totalMined(ServerStatsCounter counter) {
MinecraftServer server = ServerMain.server; int sum = 0;
if (server == null) { for (Stat<?> stat : counter.stats.keySet()) {
return; if (stat.getType() == Stats.BLOCK_MINED) {
sum += counter.getValue(stat);
} }
Scoreboard sb = server.getScoreboard(); }
Objective obj = sb.getObjective(OBJ_MINED); return sum;
}
/** 读取某玩家的死亡次数。 */
private static int totalDeaths(ServerStatsCounter counter) {
return counter.getValue(Stats.CUSTOM, Stats.DEATHS);
}
/** 把一个分数写入 objective(若不存在则创建/更新)。 */
private static void setScore(Scoreboard sb, Objective obj, ServerPlayer player, int value) {
if (obj == null) { if (obj == null) {
return; return;
} }
ScoreAccess access = sb.getOrCreatePlayerScore((ScoreHolder) player, obj); ScoreAccess access = sb.getOrCreatePlayerScore((ScoreHolder) player, obj);
access.add(count); access.set(value);
}
/** 从 stats 同步所有在线玩家的分数到 scoreboard(定期调用)。 */
public static void syncScores(MinecraftServer server) {
Scoreboard sb = server.getScoreboard();
Objective deaths = sb.getObjective(OBJ_DEATHS);
Objective mined = sb.getObjective(OBJ_MINED);
if (deaths == null && mined == null) {
return;
}
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
ServerStatsCounter counter = player.getStats();
setScore(sb, deaths, player, totalDeaths(counter));
setScore(sb, mined, player, totalMined(counter));
}
} }
} }
@@ -1,114 +0,0 @@
package com.github.zly2006.carpetslsaddition.util;
import com.github.zly2006.carpetslsaddition.ServerMain;
import com.github.zly2006.carpetslsaddition.net.BoardSyncPayload;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.stats.ServerStatsCounter;
import net.minecraft.stats.Stat;
import net.minecraft.stats.Stats;
import net.minecraft.world.level.block.Block;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/**
* 服务端:读取玩家原版统计(挖掘/死亡),打包排行榜发给客户端。
*
* <p>数据源是 vanilla stats({@code stats/<uuid>.json}),非 scoreboard。
* 客户端原生拿不到别人的 stats,故由服务端读取后通过网络包 {@link BoardSyncPayload} 下发。
*/
public final class BoardSyncManager {
private BoardSyncManager() {}
public static final String BOARD_DEATHS = "deaths";
public static final String BOARD_MINED = "mined";
/** 排行榜最多显示条目数。 */
public static final int TOP_N = 10;
/** 读取某玩家的挖掘总数(遍历该玩家的 stats,累加 BLOCK_MINED 类型条目)。 */
private static int totalMined(ServerStatsCounter counter) {
int sum = 0;
int cnt = 0;
for (Stat<?> stat : counter.stats.keySet()) {
if (stat.getType() == Stats.BLOCK_MINED) {
cnt++;
sum += counter.getValue(stat);
}
}
com.github.zly2006.carpetslsaddition.ServerMain.LOGGER.info("[SLSA-BOARD] totalMined: mapSize={} minedTypes={} sum={}", counter.stats.size(), cnt, sum);
return sum;
}
/** 读取某玩家的死亡次数。 */
private static int deaths(ServerStatsCounter counter) {
return counter.getValue(Stats.CUSTOM, Stats.DEATHS);
}
/** 构建某榜单的排行数据(降序,取前 TOP_N)。 */
public static List<BoardSyncPayload.Entry> buildBoard(MinecraftServer server, String board) {
List<BoardSyncPayload.Entry> list = new ArrayList<>();
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
ServerStatsCounter counter = player.getStats();
int score = BOARD_MINED.equals(board) ? totalMined(counter) : deaths(counter);
if (score > 0) {
list.add(new BoardSyncPayload.Entry(player.getScoreboardName(), score));
}
}
list.sort(Comparator.comparingInt(BoardSyncPayload.Entry::score).reversed());
if (list.size() > TOP_N) {
return new ArrayList<>(list.subList(0, TOP_N));
}
return list;
}
/** 把两个榜单 + 开关状态同步给某玩家。 */
public static void syncTo(ServerPlayer player) {
MinecraftServer server = ServerMain.server;
if (server == null) {
return;
}
boolean enabled = com.github.zly2006.carpetslsaddition.SLSCarpetSettings.slsBoardEnabled;
// 先发开关状态
ServerPlayNetworking.send(player, new com.github.zly2006.carpetslsaddition.net.BoardTogglePayload(enabled));
if (!enabled) {
return;
}
List<BoardSyncPayload.Entry> d = buildBoard(server, BOARD_DEATHS);
List<BoardSyncPayload.Entry> m = buildBoard(server, BOARD_MINED);
com.github.zly2006.carpetslsaddition.ServerMain.LOGGER.info("[SLSA-BOARD] syncTo {} deaths={} mined={}", player.getScoreboardName(), d.size(), m.size());
ServerPlayNetworking.send(player, new BoardSyncPayload(BOARD_DEATHS, d));
ServerPlayNetworking.send(player, new BoardSyncPayload(BOARD_MINED, m));
}
/** 同步给所有在线玩家。 */
public static void syncToAll(MinecraftServer server) {
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
syncTo(player);
}
}
private static volatile Boolean lastEnabled = null;
private static long lastSyncTick = 0;
/** 由轮询线程每秒调用:规则值变化时立即同步;否则每 5 秒定期同步一次数据。 */
public static void tick(MinecraftServer server) {
boolean now = com.github.zly2006.carpetslsaddition.SLSCarpetSettings.slsBoardEnabled;
if (lastEnabled == null || lastEnabled != now) {
lastEnabled = now;
syncToAll(server);
return;
}
if (now) {
long tick = server.getTickCount();
if (tick - lastSyncTick >= 100) { // 每 100 tick(5秒)同步一次数据
lastSyncTick = tick;
syncToAll(server);
}
}
}
}