Skills/nms/nms-scoreboard/SKILL.md
透過 NMS Scoreboard/Objective/Team API 操作 sidebar、tablist 顯示名稱與計分板(Paper NMS + Mojang-mapped)/ Operate sidebar, tablist, and scoreboard via NMS Scoreboard/Objective/Team API
npx skillsauth add MrPippi/MPS nms-scoreboardInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
nms-scoreboard
透過 NMS Scoreboard、Objective、Team 直接操作 sidebar 計分板、tablist 顯示名稱、玩家 prefix/suffix,繞過 Bukkit Scoreboard API 的封包延遲與限制。
| 參數 | 範例 | 說明 |
|------|------|------|
| package_name | com.example.display | 產出類別所在 package |
| manager_class_name | ScoreboardManager | 管理器類名稱 |
| display_slot | sidebar / list / below_name | 顯示位置 |
| per_player | true | 是否每人一個獨立計分板 |
ScoreboardManager.java — 計分板建立與更新工具SidebarDisplay.java — Sidebar 行內容管理TeamManager.java — Team prefix/suffix/tablist 管理參見 Skills/paper-nms/PLATFORM.md。關鍵依賴:
dependencies {
paperweight.paperDevBundle('1.21.1-R0.1-SNAPSHOT')
}
ScoreboardManager.javapackage com.example.display;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.scores.DisplaySlot;
import net.minecraft.world.scores.Objective;
import net.minecraft.world.scores.Scoreboard;
import net.minecraft.world.scores.criteria.ObjectiveCriteria;
import org.bukkit.craftbukkit.v1_21_R1.entity.CraftPlayer;
import org.bukkit.entity.Player;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@SuppressWarnings("UnstableApiUsage")
public class ScoreboardManager {
private static final String OBJECTIVE_NAME = "mps_sidebar";
private final Map<UUID, Scoreboard> playerBoards = new HashMap<>();
/** 取得或建立玩家專屬的 NMS Scoreboard(per-player 模式)。 */
public Scoreboard getOrCreate(Player player) {
return playerBoards.computeIfAbsent(player.getUniqueId(), k -> {
Scoreboard board = new Scoreboard();
Objective obj = board.addObjective(
OBJECTIVE_NAME,
ObjectiveCriteria.DUMMY,
Component.literal("§6§l我的伺服器"),
ObjectiveCriteria.RenderType.INTEGER,
true,
null
);
board.setDisplayObjective(DisplaySlot.SIDEBAR, obj);
return board;
});
}
/** 設定 sidebar 某行的分數(行 = 分數,數字大的在上方)。 */
public void setLine(Player player, String entry, int score) {
Scoreboard board = getOrCreate(player);
Objective obj = board.getObjective(OBJECTIVE_NAME);
if (obj == null) return;
board.getOrCreatePlayerScore(entry, obj).setScore(score);
}
/** 移除某行。 */
public void removeLine(Player player, String entry) {
Scoreboard board = getOrCreate(player);
board.resetPlayerScore(entry, board.getObjective(OBJECTIVE_NAME));
}
/** 將 NMS Scoreboard 套用至玩家(封包推送)。 */
public void apply(Player player) {
ServerPlayer nms = ((CraftPlayer) player).getHandle();
Scoreboard board = getOrCreate(player);
nms.setServerLevel(nms.serverLevel()); // 觸發重新同步
// 直接設定 playerScoreboard
nms.server.getScoreboard(); // 確保 server scoreboard 已初始化
// 使用 connection 發送 scoreboard 封包
nms.connection.send(new net.minecraft.network.protocol.game
.ClientboundSetDisplayObjectivePacket(
DisplaySlot.SIDEBAR,
board.getDisplayObjective(DisplaySlot.SIDEBAR)));
}
/** 清除玩家計分板資料。 */
public void remove(Player player) {
playerBoards.remove(player.getUniqueId());
}
}
SidebarDisplay.java(行內容封裝)package com.example.display;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.scores.DisplaySlot;
import net.minecraft.world.scores.Objective;
import net.minecraft.world.scores.Scoreboard;
import net.minecraft.world.scores.criteria.ObjectiveCriteria;
import org.bukkit.craftbukkit.v1_21_R1.entity.CraftPlayer;
import org.bukkit.entity.Player;
import java.util.LinkedHashMap;
import java.util.Map;
@SuppressWarnings("UnstableApiUsage")
public class SidebarDisplay {
private final String title;
private final LinkedHashMap<String, Integer> lines = new LinkedHashMap<>();
public SidebarDisplay(String title) {
this.title = title;
}
/** 新增一行(score 大的在上方)。 */
public SidebarDisplay addLine(String text, int score) {
lines.put(text, score);
return this;
}
/** 建立 NMS Scoreboard 並推送給玩家。 */
public void show(Player player) {
ServerPlayer nms = ((CraftPlayer) player).getHandle();
Scoreboard board = new Scoreboard();
Objective obj = board.addObjective(
"mps_sb_" + player.getName().hashCode(),
ObjectiveCriteria.DUMMY,
Component.literal(title),
ObjectiveCriteria.RenderType.INTEGER,
true, null
);
board.setDisplayObjective(DisplaySlot.SIDEBAR, obj);
for (Map.Entry<String, Integer> entry : lines.entrySet()) {
board.getOrCreatePlayerScore(entry.getKey(), obj).setScore(entry.getValue());
}
// 推送所有 scoreboard 封包
for (var packet : board.getStartTrackingPackets(obj)) {
nms.connection.send(packet);
}
}
}
TeamManager.java(prefix/suffix/tablist)package com.example.display;
import net.minecraft.network.chat.Component;
import net.minecraft.world.scores.PlayerTeam;
import net.minecraft.world.scores.Scoreboard;
import org.bukkit.Bukkit;
import org.bukkit.craftbukkit.v1_21_R1.CraftServer;
import org.bukkit.entity.Player;
@SuppressWarnings("UnstableApiUsage")
public class TeamManager {
private final Scoreboard scoreboard;
public TeamManager() {
this.scoreboard = ((CraftServer) Bukkit.getServer()).getServer().getScoreboard();
}
/** 建立或取得 Team,設定 prefix 與 suffix。 */
public PlayerTeam setTeam(String teamName, String prefix, String suffix) {
PlayerTeam team = scoreboard.getPlayerTeam(teamName);
if (team == null) {
team = scoreboard.addPlayerTeam(teamName);
}
team.setPlayerPrefix(Component.literal(prefix));
team.setPlayerSuffix(Component.literal(suffix));
return team;
}
/** 將玩家加入 Team。 */
public void addPlayer(Player player, String teamName) {
PlayerTeam team = scoreboard.getPlayerTeam(teamName);
if (team == null) team = scoreboard.addPlayerTeam(teamName);
scoreboard.addPlayerToTeam(player.getName(), team);
}
/** 移除玩家的 Team 歸屬。 */
public void removePlayer(Player player) {
scoreboard.removePlayerFromTeam(player.getName(),
scoreboard.getPlayersTeam(player.getName()));
}
}
src/main/java/com/example/
├── MyNmsPlugin.java
└── display/
├── ScoreboardManager.java
├── SidebarDisplay.java
└── TeamManager.java
connection.send() 可在任意執行緒呼叫,但封包建構需在主執行緒完成Skills/_shared/nms-threading.md| 錯誤 | 原因 | 解法 |
|------|------|------|
| Sidebar 不顯示 | Objective 未設到 SIDEBAR slot | 確認 setDisplayObjective(DisplaySlot.SIDEBAR, ...) |
| Team prefix 無效 | prefix 超過 64 字元限制 | 截短字串 |
| 玩家離線後 NPE | playerBoards 持有 UUID | 在 PlayerQuitEvent 呼叫 remove() |
| 分數行重複 | entry 字串相同 | 每行使用不同的 "§a" 等顏色前綴區分 |
research
操作 GameProfile 進行 skin 注入,用於 NPC 外觀設定與假玩家實體(Paper NMS + Mojang-mapped)/ Manipulate GameProfile for skin injection used in NPC appearance and fake player entities
tools
透過 ClientboundLevelParticlesPacket 實現進階 NMS 粒子效果:客戶端專屬、大量粒子、自定義參數(Paper NMS + Mojang-mapped)/ Advanced NMS particle effects via ClientboundLevelParticlesPacket with per-client and bulk support
documentation
直接操作 CompoundTag 讀寫物品、實體、方塊實體的 NBT 資料(Paper NMS + Mojang-mapped)/ Read and write NBT data on items, entities, and block entities via CompoundTag
documentation
操作 Minecraft 1.21 DataComponentType 物品組件系統,讀寫 CustomData、MaxStackSize、Enchantments 等組件(Paper NMS + Mojang-mapped)/ Read and write 1.21 DataComponentType item components including CustomData, MaxStackSize, Enchantments