diff --git a/README.md b/README.md
index 5bb7c2b..ecbc3c0 100644
--- a/README.md
+++ b/README.md
@@ -13,16 +13,16 @@ Lightweight packet-based scoreboard API for Bukkit plugins, with 1.7.10 to 1.20.
* No flickering (without using a buffer)
* Works with all versions from 1.7.10 to 1.20
-* Very small (around 600 lines of code with the JavaDoc) and no dependencies
+* Small (around 750 lines of code with the JavaDoc) and no dependencies
* Easy to use
* Dynamic scoreboard size: you don't need to add/remove lines, you can directly give a string list (or array) to change all the lines
* Everything is at the packet level, so it works with other plugins using scoreboard and/or teams
* Can be used asynchronously
* Supports up to 30 characters per line on 1.12.2 and below
* No character limit on 1.13 and higher
-* Supports hex colors on 1.16 and higher
-* No scoreboard scores on 1.20.3 and higher
-* [Adventure](https://github.com/KyoriPowered/adventure) components support
+* [RGB HEX colors support](#rgb-colors) on 1.16 and higher
+* [Custom number formatting](#custom-number-formatting) (including blank) for scores on 1.20.3 and higher
+* [Adventure components support](#adventure-support)
## Installation
@@ -59,7 +59,7 @@ Lightweight packet-based scoreboard API for Bukkit plugins, with 1.7.10 to 1.20.
fr.mrmicky
fastboard
- 2.0.2
+ 2.1.0
```
@@ -79,7 +79,7 @@ repositories {
}
dependencies {
- implementation 'fr.mrmicky:fastboard:2.0.2'
+ implementation 'fr.mrmicky:fastboard:2.1.0'
}
shadowJar {
@@ -186,20 +186,28 @@ public final class ExamplePlugin extends JavaPlugin implements Listener {
## Adventure support
For servers on modern [PaperMC](https://papermc.io) versions, FastBoard supports
-using [Adventure](https://github.com/KyoriPowered/adventure) components instead of strings,
+using [Adventure](https://github.com/KyoriPowered/adventure) components instead of strings,
by using the class `fr.mrmicky.fastboard.adventure.FastBoard`.
## RGB colors
When using the non-Adventure version of FastBoard, RGB colors can be added on 1.16 and higher with `ChatColor.of("#RRGGBB")` (`net.md_5.bungee.api.ChatColor` import).
+## Custom number formatting
+
+For servers on Minecraft 1.20.3 and higher, FastBoard supports custom number formatting for scores.
+By default, the blank format is used, so no score is visible, but it's also possible to specify custom scores using `FastBoard#updateLine(line, text, scoreText)`,
+`FastBoard#updateLines(lines, scores)` and `FastBoard#updateScore(line, text)`.
+
+Passing a `null` value as a score will result in a reset to the default blank formatting.
+
## ViaBackwards compatibility
When using ViaBackwards on a post-1.13 server with pre-1.13 clients, older clients
might get incomplete lines. To solve this issue, you can override the method `hasLinesMaxLength()` and return `true` for older clients.
For example using the ViaVersion API:
```java
-FastBoard board = new FastBoard(player) {
+FastBoard board = new FastBoard(player) {
@Override
public boolean hasLinesMaxLength() {
return Via.getAPI().getPlayerVersion(getPlayer()) < ProtocolVersion.v1_13.getVersion(); // or just 'return true;'
diff --git a/pom.xml b/pom.xml
index 1afc6f7..0bf689d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
fr.mrmicky
fastboard
- 2.0.2
+ 2.1.0
FastBoard
Lightweight packet-based scoreboard API for Bukkit plugins.
diff --git a/src/main/java/fr/mrmicky/fastboard/FastBoardBase.java b/src/main/java/fr/mrmicky/fastboard/FastBoardBase.java
index e804de8..e1c03dd 100644
--- a/src/main/java/fr/mrmicky/fastboard/FastBoardBase.java
+++ b/src/main/java/fr/mrmicky/fastboard/FastBoardBase.java
@@ -43,7 +43,7 @@
* The project is on GitHub.
*
* @author MrMicky
- * @version 2.0.2
+ * @version 2.1.0
*/
public abstract class FastBoardBase {
@@ -59,6 +59,7 @@ public abstract class FastBoardBase {
private static final MethodHandle PLAYER_CONNECTION;
private static final MethodHandle SEND_PACKET;
private static final MethodHandle PLAYER_GET_HANDLE;
+ private static final MethodHandle FIXED_NUMBER_FORMAT;
// Scoreboard packets
private static final FastReflection.PacketConstructor PACKET_SB_OBJ;
private static final FastReflection.PacketConstructor PACKET_SB_DISPLAY_OBJ;
@@ -125,14 +126,18 @@ public abstract class FastBoardBase {
Optional> numberFormat = FastReflection.nmsOptionalClass("network.chat.numbers", "NumberFormat");
MethodHandle packetSbSetScore;
MethodHandle packetSbResetScore = null;
+ MethodHandle fixedFormatConstructor = null;
Object blankNumberFormat = null;
if (numberFormat.isPresent()) { // 1.20.3
Class> blankFormatClass = FastReflection.nmsClass("network.chat.numbers", "BlankFormat");
+ Class> fixedFormatClass = FastReflection.nmsClass("network.chat.numbers", "FixedFormat");
Class> resetScoreClass = FastReflection.nmsClass(gameProtocolPackage, "ClientboundResetScorePacket");
MethodType setScoreType = MethodType.methodType(void.class, String.class, String.class, int.class, CHAT_COMPONENT_CLASS, numberFormat.get());
MethodType removeScoreType = MethodType.methodType(void.class, String.class, String.class);
+ MethodType fixedFormatType = MethodType.methodType(void.class, CHAT_COMPONENT_CLASS);
Optional blankField = Arrays.stream(blankFormatClass.getFields()).filter(f -> f.getType() == blankFormatClass).findAny();
+ fixedFormatConstructor = lookup.findConstructor(fixedFormatClass, fixedFormatType);
packetSbSetScore = lookup.findConstructor(packetSbScoreClass, setScoreType);
packetSbResetScore = lookup.findConstructor(resetScoreClass, removeScoreType);
blankNumberFormat = blankField.isPresent() ? blankField.get().get(null) : null;
@@ -148,6 +153,7 @@ public abstract class FastBoardBase {
PACKET_SB_RESET_SCORE = packetSbResetScore;
PACKET_SB_TEAM = FastReflection.findPacketConstructor(packetSbTeamClass, lookup);
PACKET_SB_SERIALIZABLE_TEAM = sbTeamClass == null ? null : FastReflection.findPacketConstructor(sbTeamClass, lookup);
+ FIXED_NUMBER_FORMAT = fixedFormatConstructor;
BLANK_NUMBER_FORMAT = blankNumberFormat;
for (Class> clazz : Arrays.asList(packetSbObjClass, packetSbDisplayObjClass, packetSbScoreClass, packetSbTeamClass, sbTeamClass)) {
@@ -188,6 +194,7 @@ public abstract class FastBoardBase {
private final String id;
private final List lines = new ArrayList<>();
+ private final List scores = new ArrayList<>();
private T title = emptyLine();
private boolean deleted = false;
@@ -261,6 +268,19 @@ public T getLine(int line) {
return this.lines.get(line);
}
+ /**
+ * Get how a specific line's score is displayed. On 1.20.2 or below, the value returned isn't used.
+ *
+ * @param line the line number
+ * @return the text of how the line is displayed
+ * @throws IndexOutOfBoundsException if the line is higher than {@code size}
+ */
+ public Optional getScore(int line) {
+ checkLineNumber(line, true, false);
+
+ return Optional.ofNullable(this.scores.get(line));
+ }
+
/**
* Update a single scoreboard line.
*
@@ -269,27 +289,49 @@ public T getLine(int line) {
* @throws IndexOutOfBoundsException if the line is higher than {@link #size() size() + 1}
*/
public synchronized void updateLine(int line, T text) {
- checkLineNumber(line, false, true);
+ updateLine(line, text, null);
+ }
+
+ /**
+ * Update a single scoreboard line including how its score is displayed.
+ * The score will only be displayed on 1.20.3 and higher.
+ *
+ * @param line the line number
+ * @param text the new line text
+ * @param scoreText the new line's score, if null will not change current value
+ * @throws IndexOutOfBoundsException if the line is higher than {@link #size() size() + 1}
+ */
+ public synchronized void updateLine(int line, T text, T scoreText) {
+ checkLineNumber(line, false, false);
try {
if (line < size()) {
this.lines.set(line, text);
+ this.scores.set(line, scoreText);
sendLineChange(getScoreByLine(line));
+
+ if (customScoresSupported()) {
+ sendScorePacket(getScoreByLine(line), ScoreboardAction.CHANGE);
+ }
+
return;
}
List newLines = new ArrayList<>(this.lines);
+ List newScores = new ArrayList<>(this.scores);
if (line > size()) {
for (int i = size(); i < line; i++) {
newLines.add(emptyLine());
+ newScores.add(null);
}
}
newLines.add(text);
+ newScores.add(scoreText);
- updateLines(newLines);
+ updateLines(newLines, newScores);
} catch (Throwable t) {
throw new RuntimeException("Unable to update scoreboard lines", t);
}
@@ -308,8 +350,10 @@ public synchronized void removeLine(int line) {
}
List newLines = new ArrayList<>(this.lines);
+ List newScores = new ArrayList<>(this.scores);
newLines.remove(line);
- updateLines(newLines);
+ newScores.remove(line);
+ updateLines(newLines, newScores);
}
/**
@@ -331,13 +375,35 @@ public void updateLines(T... lines) {
* @throws IllegalStateException if {@link #delete()} was call before
*/
public synchronized void updateLines(Collection lines) {
+ updateLines(lines, null);
+ }
+
+ /**
+ * Update the lines and how their score is displayed on the scoreboard.
+ * The scores will only be displayed for servers on 1.20.3 and higher.
+ *
+ * @param lines the new scoreboard lines
+ * @param scores the set for how each line's score should be, if null will fall back to default (blank)
+ * @throws IllegalArgumentException if one line is longer than 30 chars on 1.12 or lower
+ * @throws IllegalArgumentException if lines and scores are not the same size
+ * @throws IllegalStateException if {@link #delete()} was call before
+ */
+ public synchronized void updateLines(Collection lines, Collection scores) {
Objects.requireNonNull(lines, "lines");
checkLineNumber(lines.size(), false, true);
+ if (scores != null && scores.size() != lines.size()) {
+ throw new IllegalArgumentException("The size of the scores must match the size of the board");
+ }
+
List oldLines = new ArrayList<>(this.lines);
this.lines.clear();
this.lines.addAll(lines);
+ List oldScores = new ArrayList<>(this.scores);
+ this.scores.clear();
+ this.scores.addAll(scores != null ? scores : Collections.nCopies(lines.size(), null));
+
int linesSize = this.lines.size();
try {
@@ -348,7 +414,6 @@ public synchronized void updateLines(Collection lines) {
for (int i = oldLinesCopy.size(); i > linesSize; i--) {
sendTeamPacket(i - 1, TeamMode.REMOVE);
sendScorePacket(i - 1, ScoreboardAction.REMOVE);
-
oldLines.remove(0);
}
} else {
@@ -363,12 +428,94 @@ public synchronized void updateLines(Collection lines) {
if (!Objects.equals(getLineByScore(oldLines, i), getLineByScore(i))) {
sendLineChange(i);
}
+ if (!Objects.equals(getLineByScore(oldScores, i), getLineByScore(this.scores, i))) {
+ sendScorePacket(i, ScoreboardAction.CHANGE);
+ }
}
} catch (Throwable t) {
throw new RuntimeException("Unable to update scoreboard lines", t);
}
}
+ /**
+ * Update how a specified line's score is displayed on the scoreboard. A null value will reset the displayed
+ * text back to default. The scores will only be displayed for servers on 1.20.3 and higher.
+ *
+ * @param line the line number
+ * @param text the text to be displayed as the score. if null, no score will be displayed
+ * @throws IllegalArgumentException if the line number is not in range
+ * @throws IllegalStateException if {@link #delete()} was call before
+ */
+ public synchronized void updateScore(int line, T text) {
+ checkLineNumber(line, true, false);
+
+ this.scores.set(line, text);
+
+ try {
+ if (customScoresSupported()) {
+ sendScorePacket(getScoreByLine(line), ScoreboardAction.CHANGE);
+ }
+ } catch (Throwable e) {
+ throw new RuntimeException("Unable to update line score", e);
+ }
+ }
+
+ /**
+ * Reset a line's score back to default (blank). The score will only be displayed for servers on 1.20.3 and higher.
+ *
+ * @param line the line number
+ * @throws IllegalArgumentException if the line number is not in range
+ * @throws IllegalStateException if {@link #delete()} was call before
+ */
+ public synchronized void removeScore(int line) {
+ updateScore(line, null);
+ }
+
+ /**
+ * Update how all lines' scores are displayed. A value of null will reset the displayed text back to default.
+ * The scores will only be displayed for servers on 1.20.3 and higher.
+ *
+ * @param texts the set of texts to be displayed as the scores
+ * @throws IllegalArgumentException if the size of the texts does not match the current size of the board
+ * @throws IllegalStateException if {@link #delete()} was call before
+ */
+ public synchronized void updateScores(T... texts) {
+ updateScores(Arrays.asList(texts));
+ }
+
+ /**
+ * Update how all lines' scores are displayed. A null value will reset the displayed
+ * text back to default (blank). Only available on 1.20.3+ servers.
+ *
+ * @param texts the set of texts to be displayed as the scores
+ * @throws IllegalArgumentException if the size of the texts does not match the current size of the board
+ * @throws IllegalStateException if {@link #delete()} was call before
+ */
+ public synchronized void updateScores(Collection texts) {
+ Objects.requireNonNull(texts, "texts");
+
+ if (this.scores.size() != this.lines.size()) {
+ throw new IllegalArgumentException("The size of the scores must match the size of the board");
+ }
+
+ List newScores = new ArrayList<>(texts);
+ for (int i = 0; i < this.scores.size(); i++) {
+ if (Objects.equals(this.scores.get(i), newScores.get(i))) {
+ continue;
+ }
+
+ this.scores.set(i, newScores.get(i));
+
+ try {
+ if (customScoresSupported()) {
+ sendScorePacket(getScoreByLine(i), ScoreboardAction.CHANGE);
+ }
+ } catch (Throwable e) {
+ throw new RuntimeException("Unable to update scores", e);
+ }
+ }
+ }
+
/**
* Get the player who has the scoreboard.
*
@@ -396,6 +543,15 @@ public boolean isDeleted() {
return this.deleted;
}
+ /**
+ * Get if the server supports custom scoreboard scores (1.20.3+ servers only).
+ *
+ * @return true if the server supports custom scores
+ */
+ public boolean customScoresSupported() {
+ return BLANK_NUMBER_FORMAT != null;
+ }
+
/**
* Get the scoreboard size (the number of lines).
*
@@ -528,7 +684,12 @@ private void sendModernScorePacket(int score, ScoreboardAction action) throws Th
return;
}
- sendPacket(PACKET_SB_SET_SCORE.invoke(objName, this.id, score, null, BLANK_NUMBER_FORMAT));
+ T scoreFormat = getLineByScore(this.scores, score);
+ Object format = scoreFormat != null
+ ? FIXED_NUMBER_FORMAT.invoke(toMinecraftComponent(scoreFormat))
+ : BLANK_NUMBER_FORMAT;
+
+ sendPacket(PACKET_SB_SET_SCORE.invoke(objName, this.id, score, null, format));
}
protected void sendTeamPacket(int score, TeamMode mode) throws Throwable {