diff --git a/astro.config.ts b/astro.config.ts index 719ce8d8a..1d43c8646 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -263,6 +263,7 @@ export default defineConfig({ "paper/dev/data-component-api", "paper/dev/pdc", "paper/dev/scheduler", + "paper/dev/scoreboard-api", "paper/dev/plugin-messaging", "paper/dev/plugin-configurations", "paper/dev/registries", diff --git a/src/content/docs/paper/dev/api/assets/scoreboards/below-name-preview-no-score.png b/src/content/docs/paper/dev/api/assets/scoreboards/below-name-preview-no-score.png new file mode 100644 index 000000000..6cbdd4fee Binary files /dev/null and b/src/content/docs/paper/dev/api/assets/scoreboards/below-name-preview-no-score.png differ diff --git a/src/content/docs/paper/dev/api/assets/scoreboards/below-name-preview.png b/src/content/docs/paper/dev/api/assets/scoreboards/below-name-preview.png new file mode 100644 index 000000000..a98440b6d Binary files /dev/null and b/src/content/docs/paper/dev/api/assets/scoreboards/below-name-preview.png differ diff --git a/src/content/docs/paper/dev/api/assets/scoreboards/player-list-preview-hp-0.png b/src/content/docs/paper/dev/api/assets/scoreboards/player-list-preview-hp-0.png new file mode 100644 index 000000000..8686ae805 Binary files /dev/null and b/src/content/docs/paper/dev/api/assets/scoreboards/player-list-preview-hp-0.png differ diff --git a/src/content/docs/paper/dev/api/assets/scoreboards/player-list-preview-hp-100.png b/src/content/docs/paper/dev/api/assets/scoreboards/player-list-preview-hp-100.png new file mode 100644 index 000000000..d8cb0973d Binary files /dev/null and b/src/content/docs/paper/dev/api/assets/scoreboards/player-list-preview-hp-100.png differ diff --git a/src/content/docs/paper/dev/api/assets/scoreboards/player-list-preview-hp-20.png b/src/content/docs/paper/dev/api/assets/scoreboards/player-list-preview-hp-20.png new file mode 100644 index 000000000..8973698be Binary files /dev/null and b/src/content/docs/paper/dev/api/assets/scoreboards/player-list-preview-hp-20.png differ diff --git a/src/content/docs/paper/dev/api/assets/scoreboards/player-list-preview-hp-43.png b/src/content/docs/paper/dev/api/assets/scoreboards/player-list-preview-hp-43.png new file mode 100644 index 000000000..d1df8fa85 Binary files /dev/null and b/src/content/docs/paper/dev/api/assets/scoreboards/player-list-preview-hp-43.png differ diff --git a/src/content/docs/paper/dev/api/assets/scoreboards/player-list-preview-hp-5.png b/src/content/docs/paper/dev/api/assets/scoreboards/player-list-preview-hp-5.png new file mode 100644 index 000000000..748857017 Binary files /dev/null and b/src/content/docs/paper/dev/api/assets/scoreboards/player-list-preview-hp-5.png differ diff --git a/src/content/docs/paper/dev/api/assets/scoreboards/player-list-preview.png b/src/content/docs/paper/dev/api/assets/scoreboards/player-list-preview.png new file mode 100644 index 000000000..dcfb3f7c4 Binary files /dev/null and b/src/content/docs/paper/dev/api/assets/scoreboards/player-list-preview.png differ diff --git a/src/content/docs/paper/dev/api/assets/scoreboards/score.png b/src/content/docs/paper/dev/api/assets/scoreboards/score.png new file mode 100644 index 000000000..3e80aed3a Binary files /dev/null and b/src/content/docs/paper/dev/api/assets/scoreboards/score.png differ diff --git a/src/content/docs/paper/dev/api/assets/scoreboards/sidebar-preview-tutorial.png b/src/content/docs/paper/dev/api/assets/scoreboards/sidebar-preview-tutorial.png new file mode 100644 index 000000000..9151cf38f Binary files /dev/null and b/src/content/docs/paper/dev/api/assets/scoreboards/sidebar-preview-tutorial.png differ diff --git a/src/content/docs/paper/dev/api/assets/scoreboards/sidebar-preview.png b/src/content/docs/paper/dev/api/assets/scoreboards/sidebar-preview.png new file mode 100644 index 000000000..ef89cc129 Binary files /dev/null and b/src/content/docs/paper/dev/api/assets/scoreboards/sidebar-preview.png differ diff --git a/src/content/docs/paper/dev/api/scoreboard-api.mdx b/src/content/docs/paper/dev/api/scoreboard-api.mdx new file mode 100644 index 000000000..8a4d7e8ba --- /dev/null +++ b/src/content/docs/paper/dev/api/scoreboard-api.mdx @@ -0,0 +1,359 @@ +--- +title: Scoreboard API +description: A full guide to using Paper's scoreboard API. +slug: paper/dev/scoreboard-api +tableOfContents: + minHeadingLevel: 2 + maxHeadingLevel: 4 +--- + +import { Image } from "astro:assets"; +import BelowNamePreview from "./assets/scoreboards/below-name-preview.png"; +import BelowNamePreviewNoScore from "./assets/scoreboards/below-name-preview-no-score.png"; +import PlayerListPreview from "./assets/scoreboards/player-list-preview.png"; +import PlayerListPreview0 from "./assets/scoreboards/player-list-preview-hp-0.png"; +import PlayerListPreview5 from "./assets/scoreboards/player-list-preview-hp-5.png"; +import PlayerListPreview20 from "./assets/scoreboards/player-list-preview-hp-20.png"; +import PlayerListPreview43 from "./assets/scoreboards/player-list-preview-hp-43.png"; +import PlayerListPreview100 from "./assets/scoreboards/player-list-preview-hp-100.png"; +import Score from "./assets/scoreboards/score.png"; +import SidebarPreview from "./assets/scoreboards/sidebar-preview.png"; +import SidebarPreviewTutorial from "./assets/scoreboards/sidebar-preview-tutorial.png"; + +## Introduction + +Scoreboards are a great way to display information to the client. Each player can view exactly one scoreboard, and one +scoreboard can be viewed by multiple players. + +[](jd:paper:org.bukkit.scoreboard.Scoreboard) objects can be retrieved in two ways: + +1. Via the [](jd:paper:org.bukkit.scoreboard.ScoreboardManager) (which can be retrieved from either the [](jd:paper:org.bukkit.Bukkit) +or [](jd:paper:org.bukkit.Server) interfaces using [`getScoreboardManager()`](jd:paper:org.bukkit.Server#getScoreboardManager()) +respectively). +2. A player's currently shown scoreboard with the [`Player#getScoreboard()`](jd:paper:org.bukkit.entity.Player#getScoreboard()) +method. + +Scoreboards can be categorized in two ways: + +1. The main scoreboard (retrieved with [`ScoreboardManager#getMainScoreboard()`](jd:paper:org.bukkit.scoreboard.ScoreboardManager#getMainScoreboard())), +which is saved across restarts and is the default scoreboard shown to players. +2. New scoreboards (created with [`ScoreboardManager#getNewScoreboard()`](jd:paper:org.bukkit.scoreboard.ScoreboardManager#getNewScoreboard())), +which are not saved and only exist for the duration of you either keeping a reference to the object or a player viewing the scoreboard. + +## Objectives + +To display information, a scoreboard must register an **objective**. This can be done by using +[`Scoreboard#registerNewObjective`](jd:paper:org.bukkit.scoreboard.Scoreboard#registerNewObjective(java.lang.String,org.bukkit.scoreboard.Criteria,net.kyori.adventure.text.Component)). + +This method has the following parameters: + +- The name of the objective, which is used for identifying it. You can use the same name in [`Scoreboard#getObjective`](jd:paper:org.bukkit.scoreboard.Scoreboard#getObjective(java.lang.String)) +to retrieve the same objective. +- The criteria. Traditional "Vanilla-style" scoreboards use this to automatically set the score of a scoreboard entry +to match the criteria. When designing a custom scoreboard, you probably want to set this to [](jd:paper:org.bukkit.scoreboard.Criteria#DUMMY), +which has no built-in handling. +- The display name of the scoreboard. Can be set to `null`, which defaults the display name to the of the objective. +This is used when displaying the objective anywhere, like a command response or the sidebar. +- OPTIONALLY: The render type, which is either [](jd:paper:org.bukkit.scoreboard.RenderType#INTEGER) or [](jd:paper:org.bukkit.scoreboard.RenderType#HEARTS). +Used for when the objective is displayed in the player list or below a player's name. Defaults to `INTEGER`. + :::note + The HEARTS render type has no effect unless a [player list display slot](#player-list-display-slot) is used. + ::: + +### Display slots + +A scoreboard can have multiple objectives, however, only one objective can be set to a particular display slot at a +time. When you try to set the display slot of an objective to one which is already occupied, it will simply +override that display slot. + +To set the display slot, you can call [`setDisplaySlot(DisplaySlot)`](jd:paper:org.bukkit.scoreboard.Objective#setDisplaySlot(org.bukkit.scoreboard.DisplaySlot)) +on the [](jd:paper:org.bukkit.scoreboard.Objective) object. + +There are three different display slots. + +#### Sidebar display slot + +The probably most well-known display slot of an objective is the sidebar. That is also what most players generally +understand as the "scoreboard". It shows up on the right-side of a player's client and consists of a title and +up to 15 lines of scores. The display information is the same for all players viewing the same scoreboard instance. + +A preview of the sidebar display slot + +#### Player list display slot + +Objectives with this display slot show up in the player list (sometimes called TAB list) next to the name of the player. +In order for a score to be visible, the score name needs to be the same as the player's name. The score value is +what gets displayed. All players have an implicit score value of 0. + +A preview of the player list display slot + +
+ Source code + + ```java + Scoreboard board = ...; + Objective obj = board.registerNewObjective( + "playerlist", + Criteria.DUMMY, + (Component) null, + RenderType.HEARTS + ); + obj.setDisplaySlot(DisplaySlot.PLAYER_LIST); + + Score score = obj.getScore(player); + score.setScore(125); + score.numberFormat(NumberFormat.styled(style -> style + .color(TestPlugin.C_PRIMARY) + .shadowColor(ShadowColor.shadowColor(0xAA8F618E)) + .decorate(TextDecoration.ITALIC) + )); + ``` + +
+ +If you set the `RenderType` of the objective to `HEARTS`, it will instead display the provided score as a health bar. +This health bar score behaves differently depending on the score value: + +**Values 0 and below**: The score is hidden; instead, only a big space is visible. +A preview of the player list display slot with 0 score + +**Values 1-20**: A regular full health bar is visible. +A preview of the player list display slot with 5 score +A preview of the player list display slot with 20 score + +**Values 21-43**: The regular hearts get appended with "absorption" hearts. +A preview of the player list display slot with 43 score + +**Value 44+**: Instead of a health bar, text displaying the health points is visible. +A preview of the player list display slot with 100 score + +:::caution + +When using [](jd:paper:org.bukkit.scoreboard.RenderType#HEARTS), the number format provided on a score is completely +ignored. + +::: + +#### Below name display slot + +[](jd:paper:org.bukkit.scoreboard.DisplaySlot#BELOW_NAME) makes the score of a player render below the player's +own display name. + +A preview of the below name display slot + +Similar to the [player list display slot](#player-list-display-slot), all players implicitly have a score of 0, +if not set. Therefore, if you wish to have custom number formatting applied, you will have to manually set it +for every single online player for every single scoreboard you have. + +A preview of the below name display slot if a player has no score + +
+ Source code + + ```java + Player player = ...; + Scoreboard board = ...; + + Objective obj = board.registerNewObjective( + "below-name", + Criteria.DUMMY, + (Component) null + ); + obj.setDisplaySlot(DisplaySlot.BELOW_NAME); + + Score score = obj.getScore(player); + score.setScore(0); + score.numberFormat(NumberFormat.fixed(plugin.mm("Kills: 0"))); + ``` + +
+ +### Number format + +You can define a default number format for an objective. All scores under that objective will inherit the default number format set, +however a score's own number format will override the objective one. + +For example, to default to a blank number format, you can do this: +```java +Objective objective = ...; +objective.numberFormat(NumberFormat.blank()); +``` + +## Scores + +You can retrieve a score from an `Objective` instance using the `getScore` methods. +Scores are saved using a `String` identifier. For players, their name is used. For entities, +their UUID is used instead. A `Score` instance consists of four parts: the score name, displayname, +the score value as an integer, and the optional number format. + +**For example:** + +```java +Objective obj = ...; + +// The name of the score can be whatever. For sidebars with custom +// lines, you usually call the score the same as the line number. +Score score = obj.getScore("2"); + +// Set the score value. A higher value makes it appear higher on the sidebar. +score.setScore(2); + +// The custom name is what actually gets displayed in the sidebar. +score.customName(Component.text("Custom Value", TextColor.color(0xAABB24))); + +// Set the number format of this score. +score.numberFormat(NumberFormat.fixed(Component.text("25", TextColor.color(0x24FFAA)))); +``` + +A preview of the score example above + +## Tutorial: Sidebars done correctly + +Sidebar scoreboards are very popular for displaying data. However, one must interface with sidebars in a very specific way +to avoid flickering and other oddities. + +To do this, you need to keep track of **two** objectives, where you edit the one not shown, and only when ready, switch +the visible one. If you want to do this in your own plugin, it is advised to write a small wrapper to do this. An example is +given below: + +```java title="BufferedScoreboard.java" showlinenumbers +@NullMarked +public class BufferedScoreboard { + // Create a new Bukkit scoreboard. + public final Scoreboard board = Bukkit.getScoreboardManager().getNewScoreboard(); + + // We keep track of two objectives, and the currently shown one via the buffer variable. + private int buffer = 0; + private final Objective first = board.registerNewObjective("first", Criteria.DUMMY, (Component) null); + private final Objective second = board.registerNewObjective("second", Criteria.DUMMY, (Component) null); + + /// Sets the title of this scoreboard. This is done one both objectives. + public void setTitle(Component title) { + first.displayName(title); + second.displayName(title); + } + + /// Directly updates both objectives. + public void updateObjectives(Consumer update) { + update.accept(first); + update.accept(second); + } + + /// Updates a score for a specific line. + public void updateLine(int line, Consumer update) { + Objective next = buffer == 0 ? second : first; + + // The score name is the line value. + Score score = next.getScore(Integer.toString(line)); + // We subtract the line value so that line 0 is at the top, and line 15 at the bottom. + // Minecraft sorts the score lines in a descending order. + score.setScore(15 - line); + // Update the next score before it is shown. + update.accept(score); + // Set the next objective's display slot, which makes it be shown + // on the client. The previous objective is automatically hidden. + next.setDisplaySlot(DisplaySlot.SIDEBAR); + + Objective curr = buffer == 0 ? first : second; + Score currScore = curr.getScore(Integer.toString(line)); + currScore.setScore(15 - line); + // Repeat the update on the now hidden score to keep both objectives synced. + update.accept(currScore); + + buffer = buffer == 0 ? 1 : 0; + } +} +``` + +When writing your actual scoreboard, it is strongly advised to do per-line updates instead of re-drawing the entire scoreboard from scratch. A scoreboard +typically consists of some parts, that never change, and lines that hold data, which may change frequently. A good convention is to hide the scoreboard updates +behind methods, which each simply take in input for the lines that need changing, and doing very selected updates. + +For some example for the shown image here, see the preview below. + +A preview of the sidebar display slot + +
+ Click to show the code. + + ```java title="ScoreboardManager.java" showlinenumbers + @NullMarked + public final class ScoreboardManager implements Listener { + private static final TextColor SECTION_COLOR = TextColor.color(0xE57CFF); + private static final TextColor KEY_COLOR = TextColor.color(0xCE69DB); + private static final TextColor VALUE_COLOR = TextColor.color(0xEFAFFF); + + private static final java.text.NumberFormat FORMAT = DecimalFormat.getIntegerInstance(Locale.US); + + /// A map holding the BufferedScoreboard objects for every player. We can use the Player object + /// as a key here, as the entry is removed when the player quits. If you want to keep references + /// across player joins, use an UUID key instead. + private final Map scoreboards = new HashMap<>(); + + /// Util method to get an existing scoreboard, or to create a new one for a player. + private BufferedScoreboard getScoreboard(Player player) { + return scoreboards.computeIfAbsent(player, _ -> { + BufferedScoreboard scoreboard = new BufferedScoreboard(); + // Set the title of the scoreboard. + scoreboard.setTitle(MiniMessage.miniMessage().deserialize("", + Placeholder.component("player", player.displayName()) + )); + // show the scoreboard to the player. + player.setScoreboard(scoreboard.board); + return scoreboard; + }); + } + + /// Initializes the scoreboard. This sets initial values for all lines. You might want to fetch + /// some player data before this, if you display player data. + public void initScoreboard(Player player) { + final BufferedScoreboard scoreboard = getScoreboard(player); + // Set the default number format to blank, so it doesn't show the red numbers. + scoreboard.updateObjectives(obj -> obj.numberFormat(NumberFormat.blank())); + scoreboard.updateLine(0, score -> score.customName(Component.empty())); + scoreboard.updateLine(1, score -> score.customName(Component.text("DATA", SECTION_COLOR, TextDecoration.BOLD))); + updatePurse(scoreboard, 0); + updateBank(scoreboard, 0); + scoreboard.updateLine(4, score -> score.customName(Component.empty())); + scoreboard.updateLine(5, score -> score.customName(Component.text("docs.papermc.io", NamedTextColor.DARK_GRAY))); + } + + /// Public method to update the purse value. + public void updatePurse(Player player, int value) { + updatePurse(getScoreboard(player), value); + } + + /// Public method to update the bank value. + public void updateBank(Player player, int value) { + updateBank(getScoreboard(player), value); + } + + private void updatePurse(BufferedScoreboard scoreboard, int value) { + scoreboard.updateLine(2, score -> { + score.customName(Component.text("├ Purse", KEY_COLOR)); + score.numberFormat(NumberFormat.fixed(Component.text("$" + FORMAT.format(value), VALUE_COLOR))); + }); + } + + private void updateBank(BufferedScoreboard scoreboard, int value) { + scoreboard.updateLine(3, score -> { + score.customName(Component.text("└ Bank", KEY_COLOR)); + score.numberFormat(NumberFormat.fixed(Component.text("$" + FORMAT.format(value), VALUE_COLOR))); + }); + } + + /// Initializes the scoreboard when a player joins. + @EventHandler + void onPlayerJoin(PlayerJoinEvent event) { + initScoreboard(event.getPlayer()); + } + + /// Removes the scoreboard data when a player quits. + @EventHandler + void onPlayerQuit(PlayerQuitEvent event) { + scoreboards.remove(event.getPlayer()); + } +} + ``` + +
diff --git a/src/styles/custom.css b/src/styles/custom.css index 4d169d6ce..f71741089 100644 --- a/src/styles/custom.css +++ b/src/styles/custom.css @@ -36,6 +36,19 @@ width: auto; } +.img-20-left { + max-width: 20rem; + padding-top: 1rem; +} +.img-30-left { + max-width: 30rem; + padding-top: 1rem; +} +.img-40-left { + max-width: 40rem; + padding-top: 1rem; +} + /* use custom font first instead of system */ .sl-badge { font-family: var(--__sl-font-mono);