diff --git a/README.md b/README.md index 5033e94..79ac571 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,9 @@ 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. @@ -13,7 +16,7 @@ Lightweight packet-based scoreboard API for Bukkit plugins, compatible with all * 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 @@ -60,7 +63,7 @@ Lightweight packet-based scoreboard API for Bukkit plugins, compatible with all fr.mrmicky fastboard - 2.2.1 + 2.2.2 ``` @@ -81,7 +84,7 @@ repositories { } dependencies { - implementation 'fr.mrmicky:fastboard:2.2.1' + implementation 'fr.mrmicky:fastboard:2.2.2' } shadowJar { @@ -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 @@ -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. diff --git a/pom.xml b/pom.xml index b0fb214..6695de4 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ fr.mrmicky fastboard - 2.2.1 + 2.2.2 FastBoard Lightweight packet-based scoreboard API for Bukkit plugins. diff --git a/src/main/java/fr/mrmicky/fastboard/FastBoard.java b/src/main/java/fr/mrmicky/fastboard/FastBoard.java index 5cc67f0..31c25b2 100644 --- a/src/main/java/fr/mrmicky/fastboard/FastBoard.java +++ b/src/main/java/fr/mrmicky/fastboard/FastBoard.java @@ -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); } } diff --git a/src/main/java/fr/mrmicky/fastboard/FastBoardBase.java b/src/main/java/fr/mrmicky/fastboard/FastBoardBase.java index 4b98ee0..c71f0f0 100644 --- a/src/main/java/fr/mrmicky/fastboard/FastBoardBase.java +++ b/src/main/java/fr/mrmicky/fastboard/FastBoardBase.java @@ -44,7 +44,7 @@ * The project is on GitHub. * * @author MrMicky - * @version 2.2.1 + * @version 2.2.2 */ public abstract class FastBoardBase { @@ -107,9 +107,8 @@ public abstract class FastBoardBase { 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)) @@ -129,30 +128,29 @@ public abstract class FastBoardBase { 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> 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); @@ -165,51 +163,46 @@ public abstract class FastBoardBase { 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() @@ -453,13 +446,13 @@ public synchronized void updateLines(Collection lines, Collection 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); @@ -470,7 +463,7 @@ public synchronized void updateLines(Collection lines, Collection 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); @@ -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) { @@ -551,7 +544,7 @@ public synchronized void updateScores(Collection 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) { @@ -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); @@ -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. + *

+ * If not overridden, with server above 1.20.3 and player under 1.20.3 will not see text lines. + *

+ * 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"); diff --git a/src/main/java/fr/mrmicky/fastboard/adventure/FastBoard.java b/src/main/java/fr/mrmicky/fastboard/adventure/FastBoard.java index 6717ba7..c63a5e5 100644 --- a/src/main/java/fr/mrmicky/fastboard/adventure/FastBoard.java +++ b/src/main/java/fr/mrmicky/fastboard/adventure/FastBoard.java @@ -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); } }