feat: /slsboard改发请求包,服务端给该玩家单独发display包(per-player显示)

客户端本地setDisplayObjective会被服务端覆盖,改用C2S请求包,服务端单独发ClientboundSetDisplayObjectivePacket给请求者。
This commit is contained in:
AxTps committed 2026-10-06 21:22:42 +08:00
1 parent d3bd73f7c5
commit 5001b2f5a2
4 files changed
+69 -31

No files matched your search

@@ -43,6 +43,13 @@ 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.BoardRequestPayload.register();
// 客户端请求显示榜单 → 给该玩家单独发 display 包
net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking.registerGlobalReceiver(
com.github.zly2006.carpetslsaddition.net.BoardRequestPayload.TYPE,
(payload, context) -> context.server().execute(() ->
com.github.zly2006.carpetslsaddition.util.BoardManager.onClientRequest(
context.player(), payload.objectiveName())));
} }
@Override @Override
@@ -1,5 +1,6 @@
package com.github.zly2006.carpetslsaddition.client; package com.github.zly2006.carpetslsaddition.client;
import com.github.zly2006.carpetslsaddition.net.BoardRequestPayload;
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;
@@ -7,15 +8,14 @@ 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.minecraft.commands.SharedSuggestionProvider; import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.world.scores.DisplaySlot;
import net.minecraft.world.scores.Objective;
/** /**
* 客户端入口:/slsboard 命令,控制本地计分板侧边栏显示哪个榜。 * 客户端入口:/slsboard 命令,请求服务端给自己显示某个榜单。
* *
* <p>利用原版计分板侧边栏渲染(可靠),各玩家本地切换、互不影响。 * <p>服务端收到请求后,给该玩家单独发 display 包,实现各玩家各自显示、互不影响。
* 数据由服务端从 stats 同步到 scoreboard objective(见 BoardManager)。 * 数据由服务端从 stats 同步到 scoreboard objective(见 BoardManager)。
*/ */
@Environment(EnvType.CLIENT) @Environment(EnvType.CLIENT)
@@ -26,42 +26,24 @@ public class BoardClient implements ClientModInitializer {
ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> { ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> {
dispatcher.register( dispatcher.register(
ClientCommands.literal("slsboard") ClientCommands.literal("slsboard")
.then(ClientCommands.literal("deaths").executes(c -> show(c.getSource(), "sls_deaths"))) .then(ClientCommands.literal("deaths").executes(c -> request(c.getSource(), "sls_deaths")))
.then(ClientCommands.literal("mined").executes(c -> show(c.getSource(), "sls_mined"))) .then(ClientCommands.literal("mined").executes(c -> request(c.getSource(), "sls_mined")))
.then(ClientCommands.literal("off").executes(c -> { clear(c.getSource()); return 1; })) .then(ClientCommands.literal("off").executes(c -> request(c.getSource(), "")))
.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");
if ("off".equals(v)) { clear(c.getSource()); return 1; } return request(c.getSource(), "off".equals(v) ? "" : "sls_" + v);
return show(c.getSource(), "sls_" + v);
})) }))
); );
}); });
} }
private static int show(FabricClientCommandSource source, String objectiveName) { private static int request(FabricClientCommandSource source, String objectiveName) {
var mc = source.getClient(); ClientPlayNetworking.send(new BoardRequestPayload(objectiveName));
var level = mc.level; source.sendFeedback(Component.literal(
if (level == null) { objectiveName.isEmpty() ? "[SLSA] 榜单已关闭" : "[SLSA] 请求显示:" + objectiveName));
return 0;
}
Objective obj = level.getScoreboard().getObjective(objectiveName);
if (obj == null) {
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] 榜单已关闭"));
}
} }
@@ -0,0 +1,35 @@
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;
import net.minecraft.resources.Identifier;
/**
* 客户端 → 服务端:请求显示某个榜单(或关闭)。
*
* <p>服务端收到后,给该玩家单独发送 {@code ClientboundSetDisplayObjectivePacket},
* 实现"各玩家各自显示、互不影响"。
*
* <p>{@code objectiveName} 为空字符串表示关闭。
*/
public record BoardRequestPayload(String objectiveName) implements CustomPacketPayload {
public static final CustomPacketPayload.Type<BoardRequestPayload> TYPE =
new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath("slsa", "board_request"));
public static final StreamCodec<RegistryFriendlyByteBuf, BoardRequestPayload> CODEC = StreamCodec.composite(
ByteBufCodecs.STRING_UTF8, BoardRequestPayload::objectiveName,
BoardRequestPayload::new
);
@Override
public CustomPacketPayload.Type<? extends CustomPacketPayload> type() {
return TYPE;
}
public static void register() {
net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry.serverboundPlay().register(TYPE, CODEC);
}
}
@@ -95,4 +95,18 @@ public final class BoardManager {
setScore(sb, mined, player, totalMined(counter)); setScore(sb, mined, player, totalMined(counter));
} }
} }
}
/** 客户端请求显示某榜单 → 给该玩家单独发 display 包(不影响其他玩家)。 */
public static void onClientRequest(ServerPlayer player, String objectiveName) {
MinecraftServer server = ServerMain.server;
if (server == null) {
return;
}
Objective obj = objectiveName == null || objectiveName.isEmpty()
? null
: server.getScoreboard().getObjective(objectiveName);
// 直接给该玩家发 display 包(slot=SIDEBAR,objective 可为 null 表示清除)
player.connection.send(new net.minecraft.network.protocol.game.ClientboundSetDisplayObjectivePacket(
net.minecraft.world.scores.DisplaySlot.SIDEBAR, obj));
}
}\n