Skip to content

Commit

Permalink
Add support for 1.20.4 fixed formatting (#46)
Browse files Browse the repository at this point in the history
Co-authored-by: MrMicky <[email protected]>
  • Loading branch information
blubbarbs and MrMicky-FR authored Feb 24, 2024
1 parent 87ffa56 commit cb1a893
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 15 deletions.
24 changes: 16 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -59,7 +59,7 @@ Lightweight packet-based scoreboard API for Bukkit plugins, with 1.7.10 to 1.20.
<dependency>
<groupId>fr.mrmicky</groupId>
<artifactId>fastboard</artifactId>
<version>2.0.2</version>
<version>2.1.0</version>
</dependency>
</dependencies>
```
Expand All @@ -79,7 +79,7 @@ repositories {
}
dependencies {
implementation 'fr.mrmicky:fastboard:2.0.2'
implementation 'fr.mrmicky:fastboard:2.1.0'
}
shadowJar {
Expand Down Expand Up @@ -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;'
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>fr.mrmicky</groupId>
<artifactId>fastboard</artifactId>
<version>2.0.2</version>
<version>2.1.0</version>

<name>FastBoard</name>
<description>Lightweight packet-based scoreboard API for Bukkit plugins.</description>
Expand Down
173 changes: 167 additions & 6 deletions src/main/java/fr/mrmicky/fastboard/FastBoardBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
* The project is on <a href="https://github.com/MrMicky-FR/FastBoard">GitHub</a>.
*
* @author MrMicky
* @version 2.0.2
* @version 2.1.0
*/
public abstract class FastBoardBase<T> {

Expand All @@ -59,6 +59,7 @@ public abstract class FastBoardBase<T> {
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;
Expand Down Expand Up @@ -125,14 +126,18 @@ public abstract class FastBoardBase<T> {
Optional<Class<?>> 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<Field> 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;
Expand All @@ -148,6 +153,7 @@ public abstract class FastBoardBase<T> {
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)) {
Expand Down Expand Up @@ -188,6 +194,7 @@ public abstract class FastBoardBase<T> {
private final String id;

private final List<T> lines = new ArrayList<>();
private final List<T> scores = new ArrayList<>();
private T title = emptyLine();

private boolean deleted = false;
Expand Down Expand Up @@ -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<T> getScore(int line) {
checkLineNumber(line, true, false);

return Optional.ofNullable(this.scores.get(line));
}

/**
* Update a single scoreboard line.
*
Expand All @@ -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<T> newLines = new ArrayList<>(this.lines);
List<T> 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);
}
Expand All @@ -308,8 +350,10 @@ public synchronized void removeLine(int line) {
}

List<T> newLines = new ArrayList<>(this.lines);
List<T> newScores = new ArrayList<>(this.scores);
newLines.remove(line);
updateLines(newLines);
newScores.remove(line);
updateLines(newLines, newScores);
}

/**
Expand All @@ -331,13 +375,35 @@ public void updateLines(T... lines) {
* @throws IllegalStateException if {@link #delete()} was call before
*/
public synchronized void updateLines(Collection<T> 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<T> lines, Collection<T> 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<T> oldLines = new ArrayList<>(this.lines);
this.lines.clear();
this.lines.addAll(lines);

List<T> 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 {
Expand All @@ -348,7 +414,6 @@ public synchronized void updateLines(Collection<T> lines) {
for (int i = oldLinesCopy.size(); i > linesSize; i--) {
sendTeamPacket(i - 1, TeamMode.REMOVE);
sendScorePacket(i - 1, ScoreboardAction.REMOVE);

oldLines.remove(0);
}
} else {
Expand All @@ -363,12 +428,94 @@ public synchronized void updateLines(Collection<T> 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<T> 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<T> 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.
*
Expand Down Expand Up @@ -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).
*
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit cb1a893

Please sign in to comment.