diff --git a/src/main/java/com/github/zly2006/carpetslsaddition/ServerMain.java b/src/main/java/com/github/zly2006/carpetslsaddition/ServerMain.java index f2a2c6a..205ae3d 100644 --- a/src/main/java/com/github/zly2006/carpetslsaddition/ServerMain.java +++ b/src/main/java/com/github/zly2006/carpetslsaddition/ServerMain.java @@ -43,6 +43,7 @@ public class ServerMain implements ModInitializer, CarpetExtension { LOGGER.info("[SLSA] onInitialize: managing Carpet extension"); INSTANCE = this; CarpetServer.manageExtension(this); + com.github.zly2006.carpetslsaddition.net.BoardSyncPayload.register(); } @Override @@ -53,6 +54,8 @@ public class ServerMain implements ModInitializer, CarpetExtension { @Override public void onServerLoaded(MinecraftServer server) { ServerMain.server = server; + com.github.zly2006.carpetslsaddition.util.BotListManager.load(); + com.github.zly2006.carpetslsaddition.util.BoardSyncManager.syncToAll(server); } @Override diff --git a/src/main/java/com/github/zly2006/carpetslsaddition/net/BoardSyncPayload.java b/src/main/java/com/github/zly2006/carpetslsaddition/net/BoardSyncPayload.java new file mode 100644 index 0000000..0ce6b8b --- /dev/null +++ b/src/main/java/com/github/zly2006/carpetslsaddition/net/BoardSyncPayload.java @@ -0,0 +1,47 @@ +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; + +/** + * 服务端 → 客户端:同步排行榜数据。 + * + *

携带某一种榜单(死亡/挖掘)的完整排行:条目为 (玩家名, 分数),已按分数降序。 + * 客户端收到后缓存,供 HUD 渲染。 + */ +public record BoardSyncPayload(String board, List entries) implements CustomPacketPayload { + + /** 单条排行数据。 */ + public record Entry(String name, int score) { + public static final StreamCodec CODEC = StreamCodec.composite( + ByteBufCodecs.STRING_UTF8, Entry::name, + ByteBufCodecs.VAR_INT, Entry::score, + Entry::new + ); + } + + public static final CustomPacketPayload.Type TYPE = + CustomPacketPayload.createType("slsa:board_sync"); + + public static final StreamCodec CODEC = StreamCodec.composite( + ByteBufCodecs.STRING_UTF8, BoardSyncPayload::board, + Entry.CODEC.apply(ByteBufCodecs.list()), BoardSyncPayload::entries, + BoardSyncPayload::new + ); + + @Override + public CustomPacketPayload.Type type() { + return TYPE; + } + + /** 在 onInitialize 里调用,注册 payload 类型。 */ + public static void register() { + net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry.playS2C().register(TYPE, CODEC); + ServerMain.LOGGER.info("[SLSA] registered BoardSyncPayload"); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/zly2006/carpetslsaddition/util/BoardManager.java b/src/main/java/com/github/zly2006/carpetslsaddition/util/BoardManager.java new file mode 100644 index 0000000..f8ad4f6 --- /dev/null +++ b/src/main/java/com/github/zly2006/carpetslsaddition/util/BoardManager.java @@ -0,0 +1,71 @@ +package com.github.zly2006.carpetslsaddition.util; + +import com.github.zly2006.carpetslsaddition.ServerMain; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.scores.Objective; +import net.minecraft.world.scores.ScoreAccess; +import net.minecraft.world.scores.ScoreHolder; +import net.minecraft.world.scores.Scoreboard; +import net.minecraft.world.scores.criteria.ObjectiveCriteria; + +/** + * 死亡榜 / 挖掘榜的 scoreboard 管理器(服务端)。 + * + *

+ * + *

objective 在服务器启动时自动创建;客户端会同步到这两个 objective,用于 HUD 显示。 + */ +public final class BoardManager { + + private BoardManager() {} + + public static final String OBJ_DEATHS = "sls_deaths"; + public static final String OBJ_MINED = "sls_mined"; + + /** 确保两个 objective 存在(服务器启动/重载时调用)。 */ + public static void ensureObjectives(MinecraftServer server) { + Scoreboard sb = server.getScoreboard(); + if (sb.getObjective(OBJ_DEATHS) == null) { + sb.addObjective( + OBJ_DEATHS, + ObjectiveCriteria.DEATH_COUNT, + Component.literal("死亡榜"), + ObjectiveCriteria.RenderType.INTEGER, + true, + null + ); + ServerMain.LOGGER.info("[SLSA] created scoreboard objective {}", OBJ_DEATHS); + } + if (sb.getObjective(OBJ_MINED) == null) { + sb.addObjective( + OBJ_MINED, + ObjectiveCriteria.DUMMY, + Component.literal("挖掘榜"), + ObjectiveCriteria.RenderType.INTEGER, + true, + null + ); + ServerMain.LOGGER.info("[SLSA] created scoreboard objective {}", OBJ_MINED); + } + } + + /** 玩家破坏一个方块 → 挖掘榜 +1。 */ + public static void onBlockMined(ServerPlayer player, int count) { + MinecraftServer server = ServerMain.server; + if (server == null) { + return; + } + Scoreboard sb = server.getScoreboard(); + Objective obj = sb.getObjective(OBJ_MINED); + if (obj == null) { + return; + } + ScoreAccess access = sb.getOrCreatePlayerScore((ScoreHolder) player, obj); + access.add(count); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/zly2006/carpetslsaddition/util/BoardSyncManager.java b/src/main/java/com/github/zly2006/carpetslsaddition/util/BoardSyncManager.java new file mode 100644 index 0000000..1c09faa --- /dev/null +++ b/src/main/java/com/github/zly2006/carpetslsaddition/util/BoardSyncManager.java @@ -0,0 +1,80 @@ +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; + +/** + * 服务端:读取玩家原版统计(挖掘/死亡),打包排行榜发给客户端。 + * + *

数据源是 vanilla stats({@code stats/.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; + + /** 读取某玩家的挖掘总数(遍历所有方块的 BLOCK_MINED 累加)。 */ + private static int totalMined(ServerStatsCounter counter) { + int sum = 0; + for (Block block : BuiltInRegistries.BLOCK) { + sum += counter.getValue(Stats.BLOCK_MINED, block); + } + return sum; + } + /** 读取某玩家的死亡次数。 */ + private static int deaths(ServerStatsCounter counter) { + return counter.getValue(Stats.CUSTOM, Stats.DEATHS); + } + + /** 构建某榜单的排行数据(降序,取前 TOP_N)。 */ + public static List buildBoard(MinecraftServer server, String board) { + List 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; + } + ServerPlayNetworking.send(player, new BoardSyncPayload(BOARD_DEATHS, buildBoard(server, BOARD_DEATHS))); + ServerPlayNetworking.send(player, new BoardSyncPayload(BOARD_MINED, buildBoard(server, BOARD_MINED))); + } + + /** 同步给所有在线玩家。 */ + public static void syncToAll(MinecraftServer server) { + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + syncTo(player); + } + } +}