docs/paper/scoreboard/SKILL.md
# Scoreboard Skill — Paper ## Purpose Reference this skill when displaying scores, sidebars, nametag colours, or tab-list headers using the Paper/Bukkit Scoreboard API in Paper 1.21. Covers objectives, scores, sidebar display, and team management. ## When to Use This Skill - Showing a per-player sidebar with stats (kills, coins, time remaining) - Colouring player nametags above their head - Creating teams that share a colour, prefix, or collision rule - Displaying tab-list (player-list) header
npx skillsauth add MrPippi/MPS docs/paper/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.
Reference this skill when displaying scores, sidebars, nametag colours, or tab-list headers using the Paper/Bukkit Scoreboard API in Paper 1.21. Covers objectives, scores, sidebar display, and team management.
| Class / Method | Purpose | Notes |
|---------------|---------|-------|
| Bukkit.getScoreboardManager() | Get the ScoreboardManager | |
| ScoreboardManager#getNewScoreboard() | Create a fresh per-player scoreboard | Separate from server main scoreboard |
| ScoreboardManager#getMainScoreboard() | Shared server scoreboard | All plugins share; prefer per-player boards |
| Scoreboard#registerNewObjective(name, criteria, displayName) | Create an objective | criteria: "dummy", "health", etc. |
| Objective#setDisplaySlot(DisplaySlot) | Show in sidebar / below name / player list | |
| Objective#getScore(String) | Get a Score for an entry | entry = player name or string |
| Score#setScore(int) | Set the numeric score | Triggers sidebar re-render |
| Scoreboard#registerNewTeam(String) | Create a team | |
| Team#addEntry(String) | Add a player/entry to team | Pass player.getName() |
| Team#setColor(ChatColor) | Set team nametag colour | Legacy ChatColor for team colour |
| Team#setPrefix(Component) | Set prefix before player name | Adventure component |
| Team#setSuffix(Component) | Set suffix after player name | |
| Player#setScoreboard(Scoreboard) | Assign a scoreboard to a player | |
| Player#sendPlayerListHeaderAndFooter | Set tab-list header/footer | Adventure components |
package com.yourorg.myplugin.scoreboard;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
import org.bukkit.scoreboard.Criteria;
import org.bukkit.scoreboard.DisplaySlot;
import org.bukkit.scoreboard.Objective;
import org.bukkit.scoreboard.RenderType;
import org.bukkit.scoreboard.Scoreboard;
import org.bukkit.scoreboard.ScoreboardManager;
import org.bukkit.scoreboard.Team;
public class PlayerScoreboard {
private final Scoreboard board;
private final Objective sidebar;
private final Player player;
public PlayerScoreboard(Player player) {
this.player = player;
ScoreboardManager mgr = Bukkit.getScoreboardManager();
// Each player gets their own scoreboard so scores don't bleed across players
this.board = mgr.getNewScoreboard();
// Register a "dummy" sidebar objective (plugin-controlled scores)
this.sidebar = board.registerNewObjective(
"sidebar", // internal name
Criteria.DUMMY, // dummy = manually set
Component.text("★ My Server").color(NamedTextColor.GOLD)
.decoration(TextDecoration.BOLD, true),
RenderType.INTEGER // show as number
);
sidebar.setDisplaySlot(DisplaySlot.SIDEBAR);
player.setScoreboard(board);
setupLines();
}
// Team-based approach: entry key is fixed; dynamic value lives in team prefix.
// This avoids ghost lines because the scoreboard entry string never changes.
private void registerLine(String entryKey, int slot) {
sidebar.getScore(entryKey).setScore(slot);
if (board.getTeam(entryKey) == null) {
Team team = board.registerNewTeam(entryKey);
team.addEntry(entryKey);
}
}
private void setLinePrefix(String entryKey, String prefix) {
Team team = board.getTeam(entryKey);
if (team != null) {
team.setPrefix(prefix);
}
}
public void setupLines() {
// Register fixed entry keys once; score = sort order (higher = top)
sidebar.getScore(" ").setScore(7); // blank line (static, never changes)
registerLine("kills", 6);
registerLine("deaths", 5);
sidebar.getScore(" ").setScore(4); // blank line (static)
registerLine("coins", 3);
sidebar.getScore(" ").setScore(2); // blank line (static)
sidebar.getScore("§7play.example.com").setScore(1); // static line
}
public void update(int kills, int deaths, int coins) {
// Update only the team prefix — the entry key stays constant,
// so no old entry accumulates as a ghost line.
setLinePrefix("kills", "§fKills: §a" + kills);
setLinePrefix("deaths", "§fDeaths: §c" + deaths);
setLinePrefix("coins", "§fCoins: §6" + coins);
}
public void remove() {
// Reset player to main scoreboard before discarding
player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard());
}
}
Using the main scoreboard for per-player data: All players share the main scoreboard's scores. If two players have different kill counts, put each player on their own getNewScoreboard().
Score entry strings over 40 characters: Vanilla clients truncate scoreboard line strings at 40 characters. Keep sidebar entries short or abbreviate them.
Duplicate sidebar entries: Each unique entry string must be distinct. If you set kills to 10 and then call getScore("§fKills: §a10").setScore(6) again, you get two lines. Use resetScores(entry) to remove the old line before setting a new value, or always construct the same key and update its score.
Not resetting the player's scoreboard on disconnect: If you don't reset to the main scoreboard on PlayerQuitEvent, memory leaks occur because the per-player Scoreboard object is held alive.
Objective constructor using Criteria enum (e.g., Criteria.DUMMY, Criteria.HEALTH) is preferred over raw string criteria.RenderType.INTEGER shows integer scores; RenderType.HEARTS shows heart icons (only useful with health criteria).development
透過 NMS Scoreboard/Objective/Team API 操作 sidebar、tablist 顯示名稱與計分板(Paper NMS + Mojang-mapped)/ Operate sidebar, tablist, and scoreboard via NMS Scoreboard/Objective/Team API
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