refactor: 榜单改用原版计分板侧边栏显示
服务端读stats定期写入scoreboard objective;客户端/slsboard设本地sidebar display。删掉自绘HUD和网络包(GuiGraphicsExtractor渲染不走)。
This commit is contained in:
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();
|
return sum;
|
||||||
Objective obj = sb.getObjective(OBJ_MINED);
|
}
|
||||||
|
|
||||||
|
/** 读取某玩家的死亡次数。 */
|
||||||
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user