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 {