Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@

Lightweight packet-based scoreboard API for Bukkit plugins, compatible with all Minecraft versions starting with 1.7.10.

> [!WARNING]
> If you're using ViaBackwards, please read the [ViaBackwards compatibility](#viabackwards-compatibility) section.

> [!IMPORTANT]
> To use FastBoard on a 1.8 server, the server must be on 1.8.8.

## Features

* No flickering (without using a buffer)
* Compatible with all Minecraft versions starting with 1.7.10
* Small (around 750 lines of code with the Javadoc) and no dependencies
* Small 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
Expand Down Expand Up @@ -60,7 +63,7 @@ Lightweight packet-based scoreboard API for Bukkit plugins, compatible with all
<dependency>
<groupId>fr.mrmicky</groupId>
<artifactId>fastboard</artifactId>
<version>2.2.1</version>
<version>2.2.2</version>
</dependency>
</dependencies>
```
Expand All @@ -81,7 +84,7 @@ repositories {
}

dependencies {
implementation 'fr.mrmicky:fastboard:2.2.1'
implementation 'fr.mrmicky:fastboard:2.2.2'
}

shadowJar {
Expand Down Expand Up @@ -209,7 +212,9 @@ Passing a `null` value as a score will result in a reset to the default blank fo

## ViaBackwards compatibility

When using ViaBackwards on a post-1.13 server with pre-1.13 clients, older clients
### On server versions above 1.13 with clients pre 1.13

On a post-1.13 server with pre-1.13 clients, older clients
may receive incomplete lines. To solve this problem, you can override the `hasLinesMaxLength()` method and return `true` for older clients.
For example, using the ViaVersion API:
```java
Expand All @@ -218,5 +223,23 @@ FastBoard board = new FastBoard(player) {
public boolean hasLinesMaxLength() {
return Via.getAPI().getPlayerVersion(getPlayer()) < ProtocolVersion.v1_13.getVersion(); // or just 'return true;'
}
});
};
```

### On server versions above 1.20.3 with clients pre 1.20.3


On a post-1.20.3 server with pre-1.20.3 clients, older clients will receive empty lines.
To solve this problem, you can override the `hasCustomScores()` method and return `false` for older clients.
For example, using the ViaVersion API:

```java
FastBoard board = new FastBoard(player) {
@Override
public boolean hasCustomScores() {
return Via.getAPI().getPlayerVersion(getPlayer()) >= ProtocolVersion.v1_20_3.getVersion();
}
};
```

Note: if you don't want to use the score system, you can force `hasCustomScores()` to return `false` for all players, and FastBoard will always use the legacy team-based line format, which is compatible with all versions.
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.2.1</version>
<version>2.2.2</version>

<name>FastBoard</name>
<description>Lightweight packet-based scoreboard API for Bukkit plugins.</description>
Expand Down
3 changes: 1 addition & 2 deletions src/main/java/fr/mrmicky/fastboard/FastBoard.java
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,9 @@ protected void sendLineChange(int score) throws Throwable {
suffix = suffix.substring(0, Math.min(maxLength, suffix.length()));
}

if (VersionType.V1_20_3.isCurrentAtLeast()) {
if (VersionType.V1_20_3.isCurrentAtLeast() && hasCustomScores()) {
sendModernScorePacket(score, ScoreboardAction.CHANGE);
} else {
sendScorePacket(score, ScoreboardAction.CHANGE);
sendTeamPacket(score, TeamMode.UPDATE, prefix, suffix);
}
}
Expand Down
113 changes: 60 additions & 53 deletions src/main/java/fr/mrmicky/fastboard/FastBoardBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
* The project is on <a href="https://github.com/MrMicky-FR/FastBoard">GitHub</a>.
*
* @author MrMicky
* @version 2.2.1
* @version 2.2.2
*/
public abstract class FastBoardBase<T> {

Expand Down Expand Up @@ -107,9 +107,8 @@ public abstract class FastBoardBase<T> {
Class<?> packetSbObjClass = FastReflection.nmsClass(gameProtocolPackage, "PacketPlayOutScoreboardObjective", "ClientboundSetObjectivePacket");
Class<?> packetSbDisplayObjClass = FastReflection.nmsClass(gameProtocolPackage, "PacketPlayOutScoreboardDisplayObjective", "ClientboundSetDisplayObjectivePacket");
Class<?> packetSbScoreClass = FastReflection.nmsClass(gameProtocolPackage, "PacketPlayOutScoreboardScore", "ClientboundSetScorePacket");
Class<?> packetSbTeamClass = VersionType.V1_20_3.isCurrentAtLeast()
? null : FastReflection.nmsClass(gameProtocolPackage, "PacketPlayOutScoreboardTeam", "ClientboundSetPlayerTeamPacket");
Class<?> sbTeamClass = VersionType.V1_17.isCurrentAtLeast() && !VersionType.V1_20_3.isCurrentAtLeast()
Class<?> packetSbTeamClass = FastReflection.nmsClass(gameProtocolPackage, "PacketPlayOutScoreboardTeam", "ClientboundSetPlayerTeamPacket");
Class<?> sbTeamClass = VersionType.V1_17.isCurrentAtLeast()
? FastReflection.innerClass(packetSbTeamClass, innerClass -> !innerClass.isEnum()) : null;
Field playerConnectionField = Arrays.stream(entityPlayerClass.getFields())
.filter(field -> field.getType().isAssignableFrom(playerConnectionClass))
Expand All @@ -129,30 +128,29 @@ public abstract class FastBoardBase<T> {
SEND_PACKET = lookup.unreflect(sendPacketMethod);

Class<?> scoreboardClass = FastReflection.nmsClass("world.scores", "Scoreboard");
Class<?> playerTeamClass = VersionType.V1_20_3.isCurrentAtLeast() ?
null : FastReflection.nmsClass("world.scores", "ScoreboardTeam", "PlayerTeam");
Class<?> playerTeamClass = FastReflection.nmsClass("world.scores", "ScoreboardTeam", "PlayerTeam");
Class<?> objectiveClass = FastReflection.nmsClass("world.scores", "ScoreboardObjective", "Objective");
Class<?> objectiveCriteriaClass = FastReflection.nmsClass("world.scores.criteria", "IScoreboardCriteria", "ObjectiveCriteria");
PLAYER_TEAM = lookup.unreflectConstructor(playerTeamClass.getConstructor(scoreboardClass, String.class));

Class<?> objectiveRenderTypeClass = FastReflection.nmsOptionalClass("world.scores.criteria", "IScoreboardCriteria$EnumScoreboardHealthDisplay", "ObjectiveCriteria$RenderType").orElse(null);

Optional<Class<?>> numberFormat = FastReflection.nmsOptionalClass("network.chat.numbers", "NumberFormat");
MethodHandle packetSbSetScore;
MethodHandle packetSbResetScore = null;
MethodHandle fixedFormatConstructor = null;
MethodHandle packetSbSerializableTeam = null;
MethodHandle packetSbTeam = null;
MethodHandle playerTeam = null;
Object blankNumberFormat = null;
boolean scoreOptionalComponents = false;

if (VersionType.V1_20_3.isCurrentAtLeast()) {
Class<?> numberFormatClass = FastReflection.nmsClass("network.chat.numbers", "NumberFormat");
OBJECTIVE = lookup.unreflectConstructor(objectiveClass.getConstructor(scoreboardClass, String.class, objectiveCriteriaClass, CHAT_COMPONENT_CLASS, objectiveRenderTypeClass, boolean.class, numberFormatClass));
if (numberFormat.isPresent()) { // 1.20.3
OBJECTIVE = lookup.unreflectConstructor(objectiveClass.getConstructor(scoreboardClass, String.class, objectiveCriteriaClass, CHAT_COMPONENT_CLASS, objectiveRenderTypeClass, boolean.class, numberFormat.get()));
PACKET_SB_OBJ = lookup.unreflectConstructor(packetSbObjClass.getConstructor(objectiveClass, int.class));
PACKET_SB_DISPLAY_OBJ = lookup.unreflectConstructor(packetSbDisplayObjClass.getConstructor(DISPLAY_SLOT_TYPE, objectiveClass));

Class<?> blankFormatClass = FastReflection.nmsClass("network.chat.numbers", "BlankFormat");
Class<?> fixedFormatClass = FastReflection.nmsClass("network.chat.numbers", "FixedFormat");
Class<?> resetScoreClass = FastReflection.nmsClass(gameProtocolPackage, "ClientboundResetScorePacket");
MethodType scoreType = MethodType.methodType(void.class, String.class, String.class, int.class, CHAT_COMPONENT_CLASS, numberFormatClass);
MethodType scoreType = MethodType.methodType(void.class, String.class, String.class, int.class, CHAT_COMPONENT_CLASS, numberFormat.get());
MethodType scoreTypeOptional = MethodType.methodType(void.class, String.class, String.class, int.class, Optional.class, Optional.class);
MethodType removeScoreType = MethodType.methodType(void.class, String.class, String.class);
MethodType fixedFormatType = MethodType.methodType(void.class, CHAT_COMPONENT_CLASS);
Expand All @@ -165,51 +163,46 @@ public abstract class FastBoardBase<T> {
scoreOptionalComponents = optionalScorePacket.isPresent();
packetSbResetScore = lookup.findConstructor(resetScoreClass, removeScoreType);
blankNumberFormat = blankField.isPresent() ? blankField.get().get(null) : null;
} else if (VersionType.V1_17.isCurrentAtLeast()) {
Class<?> enumSbAction = FastReflection.nmsClass("server", "ScoreboardServer$Action", "ServerScoreboard$Method");
MethodType scoreType = MethodType.methodType(void.class, enumSbAction, String.class, String.class, int.class);
packetSbSetScore = lookup.findConstructor(packetSbScoreClass, scoreType);
OBJECTIVE = lookup.unreflectConstructor(objectiveClass.getConstructor(scoreboardClass, String.class, objectiveCriteriaClass, CHAT_COMPONENT_CLASS, objectiveRenderTypeClass));
PACKET_SB_OBJ = lookup.unreflectConstructor(packetSbObjClass.getConstructor(objectiveClass, int.class));
PACKET_SB_DISPLAY_OBJ = lookup.unreflectConstructor(packetSbDisplayObjClass.getConstructor(displaySlotEnum.orElse(int.class), objectiveClass));
} else {
Constructor<?> packetSbTeamConstructor = sbTeamClass != null ? packetSbTeamClass.getDeclaredConstructor(String.class, int.class, Optional.class, Collection.class) : packetSbTeamClass.getDeclaredConstructor();
packetSbTeamConstructor.setAccessible(true);
packetSbTeam = lookup.unreflectConstructor(packetSbTeamConstructor);
packetSbSerializableTeam = sbTeamClass != null ? lookup.unreflectConstructor(sbTeamClass.getConstructor(playerTeamClass)) : null;

playerTeam = lookup.unreflectConstructor(playerTeamClass.getConstructor(scoreboardClass, String.class));
if (VersionType.V1_17.isCurrentAtLeast()) {
Class<?> enumSbAction = FastReflection.nmsClass("server", "ScoreboardServer$Action", "ServerScoreboard$Method");
MethodType scoreType = MethodType.methodType(void.class, enumSbAction, String.class, String.class, int.class);
packetSbSetScore = lookup.findConstructor(packetSbScoreClass, scoreType);
packetSbSetScore = lookup.findConstructor(packetSbScoreClass, MethodType.methodType(void.class));
if (VersionType.V1_13.isCurrentAtLeast()) {
OBJECTIVE = lookup.unreflectConstructor(objectiveClass.getConstructor(scoreboardClass, String.class, objectiveCriteriaClass, CHAT_COMPONENT_CLASS, objectiveRenderTypeClass));
} else {
packetSbSetScore = lookup.findConstructor(packetSbScoreClass, MethodType.methodType(void.class));
if (VersionType.V1_13.isCurrentAtLeast()) {
OBJECTIVE = lookup.unreflectConstructor(objectiveClass.getConstructor(scoreboardClass, String.class, objectiveCriteriaClass, CHAT_COMPONENT_CLASS, objectiveRenderTypeClass));
} else {
OBJECTIVE = lookup.unreflectConstructor(objectiveClass.getConstructor(scoreboardClass, String.class, objectiveCriteriaClass));
}
}

for (Class<?> clazz : Arrays.asList(packetSbScoreClass, packetSbTeamClass, sbTeamClass, playerTeamClass, objectiveClass)) {
if (clazz == null) {
continue;
}
Field[] fields = Arrays.stream(clazz.getDeclaredFields())
.filter(field -> !Modifier.isStatic(field.getModifiers()))
.toArray(Field[]::new);
for (Field field : fields) {
field.setAccessible(true);
}
PACKETS.put(clazz, fields);
OBJECTIVE = lookup.unreflectConstructor(objectiveClass.getConstructor(scoreboardClass, String.class, objectiveCriteriaClass));
}
PACKET_SB_OBJ = lookup.unreflectConstructor(packetSbObjClass.getConstructor(objectiveClass, int.class));
PACKET_SB_DISPLAY_OBJ = lookup.unreflectConstructor(packetSbDisplayObjClass.getConstructor(int.class, objectiveClass));
}

PACKET_SB_OBJ = lookup.unreflectConstructor(packetSbObjClass.getConstructor(objectiveClass, int.class));
PACKET_SB_DISPLAY_OBJ = lookup.unreflectConstructor(packetSbDisplayObjClass.getConstructor(DISPLAY_SLOT_TYPE, objectiveClass));
PACKET_SB_SET_SCORE = packetSbSetScore;
PACKET_SB_RESET_SCORE = packetSbResetScore;
PACKET_SB_TEAM = packetSbTeam;
PACKET_SB_SERIALIZABLE_TEAM = packetSbSerializableTeam;
Constructor<?> packetSbTeamConstructor = sbTeamClass != null ? packetSbTeamClass.getDeclaredConstructor(String.class, int.class, Optional.class, Collection.class) : packetSbTeamClass.getDeclaredConstructor();
packetSbTeamConstructor.setAccessible(true);
PACKET_SB_TEAM = lookup.unreflectConstructor(packetSbTeamConstructor);
PACKET_SB_SERIALIZABLE_TEAM = sbTeamClass != null ? lookup.unreflectConstructor(sbTeamClass.getConstructor(playerTeamClass)) : null;
FIXED_NUMBER_FORMAT = fixedFormatConstructor;
BLANK_NUMBER_FORMAT = blankNumberFormat;
SCORE_OPTIONAL_COMPONENTS = scoreOptionalComponents;
PLAYER_TEAM = playerTeam;

for (Class<?> clazz : Arrays.asList(packetSbScoreClass, packetSbTeamClass, sbTeamClass, playerTeamClass, objectiveClass)) {
if (clazz == null) {
continue;
}
Field[] fields = Arrays.stream(clazz.getDeclaredFields())
.filter(field -> !Modifier.isStatic(field.getModifiers()))
.toArray(Field[]::new);
for (Field field : fields) {
field.setAccessible(true);
}
PACKETS.put(clazz, fields);
}

if (VersionType.V1_8.isCurrentAtLeast()) {
String enumSbActionClass = VersionType.V1_13.isCurrentAtLeast()
Expand Down Expand Up @@ -453,13 +446,13 @@ public synchronized void updateLines(Collection<T> lines, Collection<T> scores)

if (oldLines.size() > linesSize) {
for (int i = oldLinesCopy.size(); i > linesSize; i--) {
if (!VersionType.V1_20_3.isCurrentAtLeast()) {
if (!hasCustomScores()) {
sendTeamPacket(i - 1, TeamMode.REMOVE);
}
sendScorePacket(i - 1, ScoreboardAction.REMOVE);
oldLines.remove(0);
}
} else if (!VersionType.V1_20_3.isCurrentAtLeast()) {
} else if (!hasCustomScores()) {
for (int i = oldLinesCopy.size(); i < linesSize; i++) {
sendScorePacket(i, ScoreboardAction.CHANGE);
sendTeamPacket(i, TeamMode.CREATE, null, null);
Expand All @@ -470,7 +463,7 @@ public synchronized void updateLines(Collection<T> lines, Collection<T> scores)
for (int i = 0; i < linesSize; i++) {
boolean isNewTextDifferentFromOld = !Objects.equals(getLineByScore(oldLines, i), getLineByScore(i));
boolean isNewFormatDifferentFromOld = !Objects.equals(getLineByScore(oldScores, i), getLineByScore(this.scores, i));
if (VersionType.V1_20_3.isCurrentAtLeast() && (isNewTextDifferentFromOld || isNewFormatDifferentFromOld)) {
if (VersionType.V1_20_3.isCurrentAtLeast() && hasCustomScores() && (isNewTextDifferentFromOld || isNewFormatDifferentFromOld)) {
sendModernScorePacket(i, ScoreboardAction.CHANGE);
} else if (isNewTextDifferentFromOld) {
sendLineChange(i);
Expand All @@ -496,7 +489,7 @@ public synchronized void updateScore(int line, T score) {
this.scores.set(line, score);

try {
if (VersionType.V1_20_3.isCurrentAtLeast()) {
if (VersionType.V1_20_3.isCurrentAtLeast() && hasCustomScores()) {
sendModernScorePacket(getScoreByLine(line), ScoreboardAction.CHANGE);
}
} catch (Throwable e) {
Expand Down Expand Up @@ -551,7 +544,7 @@ public synchronized void updateScores(Collection<T> scores) {
this.scores.set(i, newScores.get(i));

try {
if (VersionType.V1_20_3.isCurrentAtLeast()) {
if (VersionType.V1_20_3.isCurrentAtLeast() && hasCustomScores()) {
sendModernScorePacket(getScoreByLine(i), ScoreboardAction.CHANGE);
}
} catch (Throwable e) {
Expand Down Expand Up @@ -607,7 +600,7 @@ public synchronized void delete() {

try {
for (int i = 0; i < this.lines.size(); i++) {
if (VersionType.V1_20_3.isCurrentAtLeast()) {
if (VersionType.V1_20_3.isCurrentAtLeast() && hasCustomScores()) {
sendScorePacket(i, ScoreboardAction.REMOVE);
} else {
sendTeamPacket(i, TeamMode.REMOVE);
Expand Down Expand Up @@ -635,6 +628,20 @@ public synchronized void delete() {

protected abstract T emptyLine();

/**
* Returns whether scoreboard lines should use the score as text.
* By default, this is true only on Minecraft 1.20.3 and higher.
* Override this method for compatibility with plugins that provide multi-version support.
* <p>
* If not overridden, with server above 1.20.3 and player under 1.20.3 will not see text lines.
* <p>
* example: {@code return Via.getAPI().getPlayerVersion(getPlayer()) >= ProtocolVersion.v1_20_3.getVersion();}
* @return true should use new score as text, false for legacy team
*/
protected boolean hasCustomScores() {
return true;
}

private void checkLineNumber(int line, boolean checkInRange, boolean checkMax) {
if (line < 0) {
throw new IllegalArgumentException("Line number must be positive");
Expand Down
3 changes: 1 addition & 2 deletions src/main/java/fr/mrmicky/fastboard/adventure/FastBoard.java
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,9 @@ public FastBoard(Player player) {
protected void sendLineChange(int score) throws Throwable {
Component line = getLineByScore(score);

if (VersionType.V1_20_3.isCurrentAtLeast()) {
if (VersionType.V1_20_3.isCurrentAtLeast() && hasCustomScores()) {
sendModernScorePacket(score, ScoreboardAction.CHANGE);
} else {
sendScorePacket(score, ScoreboardAction.CHANGE);
sendTeamPacket(score, TeamMode.UPDATE, line, null);
}
}
Expand Down