From 4163feb034055084dcb9858bcdcb8311c6a06343 Mon Sep 17 00:00:00 2001 From: JiveOff Date: Wed, 11 Sep 2024 23:44:10 +0200 Subject: [PATCH 1/2] chore: bump FastBoard & edit class path --- .../ecatup/utils/fastboard/FastBoard.java | 5 + .../ecatup/utils/fastboard/FastBoardBase.java | 281 ++++++++++++++++-- .../utils/fastboard/FastReflection.java | 25 +- 3 files changed, 274 insertions(+), 37 deletions(-) diff --git a/src/main/java/fr/efreicraft/ecatup/utils/fastboard/FastBoard.java b/src/main/java/fr/efreicraft/ecatup/utils/fastboard/FastBoard.java index ea8f4ab..571918a 100644 --- a/src/main/java/fr/efreicraft/ecatup/utils/fastboard/FastBoard.java +++ b/src/main/java/fr/efreicraft/ecatup/utils/fastboard/FastBoard.java @@ -138,6 +138,11 @@ protected Object toMinecraftComponent(String line) throws Throwable { return Array.get(MESSAGE_FROM_STRING.invoke(line), 0); } + @Override + protected String serializeLine(String value) { + return value; + } + @Override protected String emptyLine() { return ""; diff --git a/src/main/java/fr/efreicraft/ecatup/utils/fastboard/FastBoardBase.java b/src/main/java/fr/efreicraft/ecatup/utils/fastboard/FastBoardBase.java index 1a046bf..3026b93 100644 --- a/src/main/java/fr/efreicraft/ecatup/utils/fastboard/FastBoardBase.java +++ b/src/main/java/fr/efreicraft/ecatup/utils/fastboard/FastBoardBase.java @@ -32,15 +32,7 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; +import java.util.*; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Stream; @@ -51,7 +43,7 @@ * The project is on GitHub. * * @author MrMicky - * @version 2.0.0 + * @version 2.1.3 */ public abstract class FastBoardBase { @@ -67,16 +59,20 @@ 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; - private static final FastReflection.PacketConstructor PACKET_SB_SCORE; private static final FastReflection.PacketConstructor PACKET_SB_TEAM; private static final FastReflection.PacketConstructor PACKET_SB_SERIALIZABLE_TEAM; + private static final MethodHandle PACKET_SB_SET_SCORE; + private static final MethodHandle PACKET_SB_RESET_SCORE; + private static final boolean SCORE_OPTIONAL_COMPONENTS; // Scoreboard enums private static final Class DISPLAY_SLOT_TYPE; private static final Class ENUM_SB_HEALTH_DISPLAY; private static final Class ENUM_SB_ACTION; + private static final Object BLANK_NUMBER_FORMAT; private static final Object SIDEBAR_DISPLAY_SLOT; private static final Object ENUM_SB_HEALTH_DISPLAY_INTEGER; private static final Object ENUM_SB_ACTION_CHANGE; @@ -88,9 +84,11 @@ public abstract class FastBoardBase { if (FastReflection.isRepackaged()) { VERSION_TYPE = VersionType.V1_17; - } else if (FastReflection.nmsOptionalClass(null, "ScoreboardServer$Action").isPresent()) { + } else if (FastReflection.nmsOptionalClass(null, "ScoreboardServer$Action").isPresent() + || FastReflection.nmsOptionalClass(null, "ServerScoreboard$Method").isPresent()) { VERSION_TYPE = VersionType.V1_13; - } else if (FastReflection.nmsOptionalClass(null, "IScoreboardCriteria$EnumScoreboardHealthDisplay").isPresent()) { + } else if (FastReflection.nmsOptionalClass(null, "IScoreboardCriteria$EnumScoreboardHealthDisplay").isPresent() + || FastReflection.nmsOptionalClass(null, "ObjectiveCriteria$RenderType").isPresent()) { VERSION_TYPE = VersionType.V1_8; } else { VERSION_TYPE = VersionType.V1_7; @@ -98,13 +96,13 @@ public abstract class FastBoardBase { String gameProtocolPackage = "network.protocol.game"; Class craftPlayerClass = FastReflection.obcClass("entity.CraftPlayer"); - Class entityPlayerClass = FastReflection.nmsClass("server.level", "EntityPlayer"); - Class playerConnectionClass = FastReflection.nmsClass("server.network", "PlayerConnection"); + Class entityPlayerClass = FastReflection.nmsClass("server.level", "EntityPlayer", "ServerPlayer"); + Class playerConnectionClass = FastReflection.nmsClass("server.network", "PlayerConnection", "ServerGamePacketListenerImpl"); Class packetClass = FastReflection.nmsClass("network.protocol", "Packet"); - Class packetSbObjClass = FastReflection.nmsClass(gameProtocolPackage, "PacketPlayOutScoreboardObjective"); - Class packetSbDisplayObjClass = FastReflection.nmsClass(gameProtocolPackage, "PacketPlayOutScoreboardDisplayObjective"); - Class packetSbScoreClass = FastReflection.nmsClass(gameProtocolPackage, "PacketPlayOutScoreboardScore"); - Class packetSbTeamClass = FastReflection.nmsClass(gameProtocolPackage, "PacketPlayOutScoreboardTeam"); + Class packetSbObjClass = FastReflection.nmsClass(gameProtocolPackage, "PacketPlayOutScoreboardObjective", "ClientboundSetObjectivePacket"); + Class packetSbDisplayObjClass = FastReflection.nmsClass(gameProtocolPackage, "PacketPlayOutScoreboardDisplayObjective", "ClientboundSetDisplayObjectivePacket"); + Class packetSbScoreClass = FastReflection.nmsClass(gameProtocolPackage, "PacketPlayOutScoreboardScore", "ClientboundSetScorePacket"); + Class packetSbTeamClass = FastReflection.nmsClass(gameProtocolPackage, "PacketPlayOutScoreboardTeam", "ClientboundSetPlayerTeamPacket"); Class sbTeamClass = VersionType.V1_17.isHigherOrEqual() ? FastReflection.innerClass(packetSbTeamClass, innerClass -> !innerClass.isEnum()) : null; Field playerConnectionField = Arrays.stream(entityPlayerClass.getFields()) @@ -116,10 +114,9 @@ public abstract class FastBoardBase { ) .filter(m -> m.getParameterCount() == 1 && m.getParameterTypes()[0] == packetClass) .findFirst().orElseThrow(NoSuchMethodException::new); - Optional> displaySlotEnum = FastReflection.nmsOptionalClass("world.scores", "DisplaySlot"); - CHAT_COMPONENT_CLASS = FastReflection.nmsClass("network.chat", "IChatBaseComponent"); - CHAT_FORMAT_ENUM = FastReflection.nmsClass(null, "EnumChatFormat"); + CHAT_COMPONENT_CLASS = FastReflection.nmsClass("network.chat", "IChatBaseComponent","Component"); + CHAT_FORMAT_ENUM = FastReflection.nmsClass(null, "EnumChatFormat", "ChatFormatting"); DISPLAY_SLOT_TYPE = displaySlotEnum.orElse(int.class); RESET_FORMATTING = FastReflection.enumValueOf(CHAT_FORMAT_ENUM, "RESET", 21); SIDEBAR_DISPLAY_SLOT = displaySlotEnum.isPresent() ? FastReflection.enumValueOf(DISPLAY_SLOT_TYPE, "SIDEBAR", 1) : 1; @@ -128,9 +125,46 @@ public abstract class FastBoardBase { SEND_PACKET = lookup.unreflect(sendPacketMethod); PACKET_SB_OBJ = FastReflection.findPacketConstructor(packetSbObjClass, lookup); PACKET_SB_DISPLAY_OBJ = FastReflection.findPacketConstructor(packetSbDisplayObjClass, lookup); - PACKET_SB_SCORE = FastReflection.findPacketConstructor(packetSbScoreClass, lookup); + + Optional> numberFormat = FastReflection.nmsOptionalClass("network.chat.numbers", "NumberFormat"); + MethodHandle packetSbSetScore; + MethodHandle packetSbResetScore = null; + MethodHandle fixedFormatConstructor = null; + Object blankNumberFormat = null; + boolean scoreOptionalComponents = false; + + 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 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); + Optional blankField = Arrays.stream(blankFormatClass.getFields()).filter(f -> f.getType() == blankFormatClass).findAny(); + // Fields are of type Optional in 1.20.5+ + Optional optionalScorePacket = FastReflection.optionalConstructor(packetSbScoreClass, lookup, scoreTypeOptional); + fixedFormatConstructor = lookup.findConstructor(fixedFormatClass, fixedFormatType); + packetSbSetScore = optionalScorePacket.isPresent() ? optionalScorePacket.get() + : lookup.findConstructor(packetSbScoreClass, scoreType); + scoreOptionalComponents = optionalScorePacket.isPresent(); + packetSbResetScore = lookup.findConstructor(resetScoreClass, removeScoreType); + blankNumberFormat = blankField.isPresent() ? blankField.get().get(null) : null; + } else if (VersionType.V1_17.isHigherOrEqual()) { + 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); + } else { + packetSbSetScore = lookup.findConstructor(packetSbScoreClass, MethodType.methodType(void.class)); + } + + PACKET_SB_SET_SCORE = packetSbSetScore; + 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; + SCORE_OPTIONAL_COMPONENTS = scoreOptionalComponents; for (Class clazz : Arrays.asList(packetSbObjClass, packetSbDisplayObjClass, packetSbScoreClass, packetSbTeamClass, sbTeamClass)) { if (clazz == null) { @@ -149,8 +183,8 @@ public abstract class FastBoardBase { String enumSbActionClass = VersionType.V1_13.isHigherOrEqual() ? "ScoreboardServer$Action" : "PacketPlayOutScoreboardScore$EnumScoreboardAction"; - ENUM_SB_HEALTH_DISPLAY = FastReflection.nmsClass("world.scores.criteria", "IScoreboardCriteria$EnumScoreboardHealthDisplay"); - ENUM_SB_ACTION = FastReflection.nmsClass("server", enumSbActionClass); + ENUM_SB_HEALTH_DISPLAY = FastReflection.nmsClass("world.scores.criteria", "IScoreboardCriteria$EnumScoreboardHealthDisplay", "ObjectiveCriteria$RenderType"); + ENUM_SB_ACTION = FastReflection.nmsClass("server", enumSbActionClass, "ServerScoreboard$Method"); ENUM_SB_HEALTH_DISPLAY_INTEGER = FastReflection.enumValueOf(ENUM_SB_HEALTH_DISPLAY, "INTEGER", 0); ENUM_SB_ACTION_CHANGE = FastReflection.enumValueOf(ENUM_SB_ACTION, "CHANGE", 0); ENUM_SB_ACTION_REMOVE = FastReflection.enumValueOf(ENUM_SB_ACTION, "REMOVE", 1); @@ -170,6 +204,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; @@ -243,6 +278,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. * @@ -251,27 +299,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); } @@ -290,8 +360,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); } /** @@ -313,13 +385,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 { @@ -330,7 +424,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 { @@ -345,12 +438,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. * @@ -378,6 +553,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). * @@ -411,6 +595,8 @@ public void delete() { protected abstract Object toMinecraftComponent(T value) throws Throwable; + protected abstract String serializeLine(T value); + protected abstract T emptyLine(); private void checkLineNumber(int line, boolean checkInRange, boolean checkMax) { @@ -447,6 +633,7 @@ protected void sendObjectivePacket(ObjectiveMode mode) throws Throwable { if (mode != ObjectiveMode.REMOVE) { setComponentField(packet, this.title, 1); + setField(packet, Optional.class, Optional.empty()); // Number format for 1.20.5+, previously nullable if (VersionType.V1_8.isHigherOrEqual()) { setField(packet, ENUM_SB_HEALTH_DISPLAY, ENUM_SB_HEALTH_DISPLAY_INTEGER); @@ -468,7 +655,12 @@ protected void sendDisplayObjectivePacket() throws Throwable { } protected void sendScorePacket(int score, ScoreboardAction action) throws Throwable { - Object packet = PACKET_SB_SCORE.invoke(); + if (VersionType.V1_17.isHigherOrEqual()) { + sendModernScorePacket(score, action); + return; + } + + Object packet = PACKET_SB_SET_SCORE.invoke(); setField(packet, String.class, COLOR_CODES[score], 0); // Player Name @@ -488,6 +680,32 @@ protected void sendScorePacket(int score, ScoreboardAction action) throws Throwa sendPacket(packet); } + private void sendModernScorePacket(int score, ScoreboardAction action) throws Throwable { + String objName = COLOR_CODES[score]; + Object enumAction = action == ScoreboardAction.REMOVE + ? ENUM_SB_ACTION_REMOVE : ENUM_SB_ACTION_CHANGE; + + if (PACKET_SB_RESET_SCORE == null) { // Pre 1.20.3 + sendPacket(PACKET_SB_SET_SCORE.invoke(enumAction, this.id, objName, score)); + return; + } + + if (action == ScoreboardAction.REMOVE) { + sendPacket(PACKET_SB_RESET_SCORE.invoke(objName, this.id)); + return; + } + + T scoreFormat = getLineByScore(this.scores, score); + Object format = scoreFormat != null + ? FIXED_NUMBER_FORMAT.invoke(toMinecraftComponent(scoreFormat)) + : BLANK_NUMBER_FORMAT; + Object scorePacket = SCORE_OPTIONAL_COMPONENTS + ? PACKET_SB_SET_SCORE.invoke(objName, this.id, score, Optional.empty(), Optional.of(format)) + : PACKET_SB_SET_SCORE.invoke(objName, this.id, score, null, format); + + sendPacket(scorePacket); + } + protected void sendTeamPacket(int score, TeamMode mode) throws Throwable { sendTeamPacket(score, mode, null, null); } @@ -561,7 +779,8 @@ private void setField(Object packet, Class fieldType, Object value, int count private void setComponentField(Object packet, T value, int count) throws Throwable { if (!VersionType.V1_13.isHigherOrEqual()) { - setField(packet, String.class, value != null ? value : "", count); + String line = value != null ? serializeLine(value) : ""; + setField(packet, String.class, line, count); return; } diff --git a/src/main/java/fr/efreicraft/ecatup/utils/fastboard/FastReflection.java b/src/main/java/fr/efreicraft/ecatup/utils/fastboard/FastReflection.java index f4e41f7..866a966 100644 --- a/src/main/java/fr/efreicraft/ecatup/utils/fastboard/FastReflection.java +++ b/src/main/java/fr/efreicraft/ecatup/utils/fastboard/FastReflection.java @@ -40,13 +40,12 @@ public final class FastReflection { private static final String NM_PACKAGE = "net.minecraft"; - public static final String OBC_PACKAGE = "org.bukkit.craftbukkit"; - public static final String NMS_PACKAGE = NM_PACKAGE + ".server"; - - public static final String VERSION = Bukkit.getServer().getClass().getPackage().getName().substring(OBC_PACKAGE.length() + 1); + private static final String OBC_PACKAGE = Bukkit.getServer().getClass().getPackage().getName(); + private static final String NMS_PACKAGE = OBC_PACKAGE.replace("org.bukkit.craftbukkit", NM_PACKAGE + ".server"); private static final MethodType VOID_METHOD_TYPE = MethodType.methodType(void.class); private static final boolean NMS_REPACKAGED = optionalClass(NM_PACKAGE + ".network.protocol.Packet").isPresent(); + private static final boolean MOJANG_MAPPINGS = optionalClass(NM_PACKAGE + ".network.chat.Component").isPresent(); private static volatile Object theUnsafe; @@ -61,21 +60,27 @@ public static boolean isRepackaged() { public static String nmsClassName(String post1_17package, String className) { if (NMS_REPACKAGED) { String classPackage = post1_17package == null ? NM_PACKAGE : NM_PACKAGE + '.' + post1_17package; + return classPackage + '.' + className; } - return NMS_PACKAGE + '.' + VERSION + '.' + className; + + return NMS_PACKAGE + '.' + className; } public static Class nmsClass(String post1_17package, String className) throws ClassNotFoundException { return Class.forName(nmsClassName(post1_17package, className)); } + public static Class nmsClass(String post1_17package, String spigotClass, String mojangClass) throws ClassNotFoundException { + return nmsClass(post1_17package, MOJANG_MAPPINGS ? mojangClass : spigotClass); + } + public static Optional> nmsOptionalClass(String post1_17package, String className) { return optionalClass(nmsClassName(post1_17package, className)); } public static String obcClassName(String className) { - return OBC_PACKAGE + '.' + VERSION + '.' + className; + return OBC_PACKAGE + '.' + className; } public static Class obcClass(String className) throws ClassNotFoundException { @@ -119,6 +124,14 @@ static Class innerClass(Class parentClass, Predicate> classPredic throw new ClassNotFoundException("No class in " + parentClass.getCanonicalName() + " matches the predicate."); } + static Optional optionalConstructor(Class declaringClass, MethodHandles.Lookup lookup, MethodType type) throws IllegalAccessException { + try { + return Optional.of(lookup.findConstructor(declaringClass, type)); + } catch (NoSuchMethodException e) { + return Optional.empty(); + } + } + public static PacketConstructor findPacketConstructor(Class packetClass, MethodHandles.Lookup lookup) throws Exception { try { MethodHandle constructor = lookup.findConstructor(packetClass, VOID_METHOD_TYPE); From e4ffd45ccf933742255cc2c4276de83d58534241 Mon Sep 17 00:00:00 2001 From: JiveOff Date: Fri, 13 Sep 2024 02:49:38 +0200 Subject: [PATCH 2/2] feat: stats system --- .../efreicraft/ecatup/players/ECPlayer.java | 17 ++- .../players/statistics/PlayerStatistic.java | 106 ++++++++++++++++++ .../statistics/PlayerStatisticsManager.java | 91 +++++++++++++++ .../ecatup/players/statistics/Statistic.java | 47 ++++++++ .../interfaces/IPassiveStatisticValue.java | 13 +++ .../efreicraft/ecatup/utils/MessageUtils.java | 5 + 6 files changed, 277 insertions(+), 2 deletions(-) create mode 100644 src/main/java/fr/efreicraft/ecatup/players/statistics/PlayerStatistic.java create mode 100644 src/main/java/fr/efreicraft/ecatup/players/statistics/PlayerStatisticsManager.java create mode 100644 src/main/java/fr/efreicraft/ecatup/players/statistics/Statistic.java create mode 100644 src/main/java/fr/efreicraft/ecatup/players/statistics/interfaces/IPassiveStatisticValue.java diff --git a/src/main/java/fr/efreicraft/ecatup/players/ECPlayer.java b/src/main/java/fr/efreicraft/ecatup/players/ECPlayer.java index 0f6db87..87066a9 100644 --- a/src/main/java/fr/efreicraft/ecatup/players/ECPlayer.java +++ b/src/main/java/fr/efreicraft/ecatup/players/ECPlayer.java @@ -6,6 +6,7 @@ import fr.efreicraft.ecatup.ECATUP; import fr.efreicraft.ecatup.players.menus.PlayerMenus; import fr.efreicraft.ecatup.players.scoreboards.PlayerScoreboard; +import fr.efreicraft.ecatup.players.statistics.PlayerStatisticsManager; import fr.efreicraft.ecatup.utils.MessageUtils; import fr.efreicraft.ecatup.utils.SoundUtils; import fr.efreicraft.ecatup.utils.TitleUtils; @@ -48,6 +49,11 @@ public class ECPlayer { */ private final PlayerMenus playerMenus; + /** + * Instance du gestionnaire de statistiques du joueur. + */ + private final PlayerStatisticsManager statisticsManager; + /** * Instance de gestionnaire de permissions du joueur. */ @@ -59,10 +65,12 @@ public class ECPlayer { */ public ECPlayer(org.bukkit.entity.Player playerEntity, fr.efreicraft.animus.models.Player animusPlayer) throws ApiException { this.playerEntity = playerEntity; - this.playerMenus = new PlayerMenus(); + this.animusPlayer = animusPlayer; this.attachment = playerEntity.addAttachment(ECATUP.getInstance()); + + this.statisticsManager = new PlayerStatisticsManager(this); + this.playerMenus = new PlayerMenus(); this.scoreboard = new PlayerScoreboard(this); - this.animusPlayer = animusPlayer; this.setPrefix(this.animusPlayer.getPermGroups().get(0).getPrefix()); ECATUP.getInstance().getGroupManager().addPlayerToTeam(this); @@ -75,6 +83,7 @@ public ECPlayer(org.bukkit.entity.Player playerEntity, fr.efreicraft.animus.mode */ public void unload() { this.scoreboard.unload(); + this.statisticsManager.unload(); } /** @@ -281,4 +290,8 @@ public List getPermissions() { .map(Map.Entry::getKey) // Ne prendre que les clés .toList(); } + + public PlayerStatisticsManager getStatisticsManager() { + return statisticsManager; + } } diff --git a/src/main/java/fr/efreicraft/ecatup/players/statistics/PlayerStatistic.java b/src/main/java/fr/efreicraft/ecatup/players/statistics/PlayerStatistic.java new file mode 100644 index 0000000..364e520 --- /dev/null +++ b/src/main/java/fr/efreicraft/ecatup/players/statistics/PlayerStatistic.java @@ -0,0 +1,106 @@ +package fr.efreicraft.ecatup.players.statistics; + +import fr.efreicraft.animus.endpoints.PlayerService; +import fr.efreicraft.animus.invoker.ApiException; +import fr.efreicraft.animus.models.PlayerStatisticRecord; +import fr.efreicraft.ecatup.players.ECPlayer; +import fr.efreicraft.ecatup.utils.MessageUtils; + +import java.math.BigDecimal; + +public class PlayerStatistic { + + private Statistic statistic; + + private ECPlayer player; + + private BigDecimal value; + + public PlayerStatistic(ECPlayer player, PlayerStatisticRecord record) { + this.player = player; + this.statistic = Statistic.fromRecord(record); + this.value = record.getValue(); + } + + public PlayerStatistic(ECPlayer player, Statistic statistic) { + this.player = player; + this.statistic = statistic; + this.value = BigDecimal.ZERO; + } + + public BigDecimal getValue() { + return value; + } + + public void set(int value, String reason, Boolean sendMessage) { + try { + PlayerService.manipulatePlayerStatistic( + this.player.getAnimusPlayer().getUuid(), + this.statistic.getKey(), + BigDecimal.valueOf(value), + reason, + true, + this.statistic.getType(), + this.statistic.getDisplayName(), + this.statistic.getColor() + ); + + if(sendMessage) { + this.player.sendMessage(MessageUtils.ChatPrefix.STATS, "&7Ton montant de " + this.statistic.getColor() + this.statistic.getDisplayName() + " &7a été mis à jour à " + this.statistic.getColor() + value + "&7."); + } + + refreshStatistic(); + } catch (ApiException e) { + e.printStackTrace(); + } + } + + public void add(int value, String reason, Boolean sendMessage) { + try { + PlayerService.manipulatePlayerStatistic( + this.player.getAnimusPlayer().getUuid(), + this.statistic.getKey(), + BigDecimal.valueOf(value), + reason, + false, + this.statistic.getType(), + this.statistic.getDisplayName(), + this.statistic.getColor() + ); + + if(sendMessage) { + StringBuilder sb = new StringBuilder(" "); + + if (value > 0) + sb.append("&a+"); + else + sb.append("&c-"); + + sb.append(this.statistic.getColor()).append(Math.abs(value)).append(" ").append(this.statistic.getDisplayName()) + .append(" &7(").append(reason).append(")"); + + this.player.sendMessage(MessageUtils.ChatPrefix.EMPTY, sb.toString()); + } + + refreshStatistic(); + } catch (ApiException e) { + e.printStackTrace(); + } + } + + public void remove(int value, String reason, Boolean sendMessage) { + this.add(-value, reason, sendMessage); + } + + private void refreshStatistic() { + try { + PlayerStatisticRecord record = PlayerService.getPlayerStatistic(this.player.getAnimusPlayer().getUuid(), this.statistic.getKey()); + if(record != null) { + this.statistic = Statistic.fromRecord(record); + this.value = record.getValue(); + } + } catch (ApiException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/fr/efreicraft/ecatup/players/statistics/PlayerStatisticsManager.java b/src/main/java/fr/efreicraft/ecatup/players/statistics/PlayerStatisticsManager.java new file mode 100644 index 0000000..f83d8cd --- /dev/null +++ b/src/main/java/fr/efreicraft/ecatup/players/statistics/PlayerStatisticsManager.java @@ -0,0 +1,91 @@ +package fr.efreicraft.ecatup.players.statistics; + +import fr.efreicraft.animus.endpoints.PlayerService; +import fr.efreicraft.animus.invoker.ApiException; +import fr.efreicraft.ecatup.ECATUP; +import fr.efreicraft.ecatup.players.ECPlayer; +import fr.efreicraft.ecatup.players.statistics.interfaces.IPassiveStatisticValue; +import org.bukkit.Bukkit; +import org.bukkit.scheduler.BukkitTask; + +import java.util.HashMap; +import java.util.Map; + +public class PlayerStatisticsManager { + + private static final Integer PASSIVE_REFRESH_INTERVAL = 10; + + private ECPlayer player; + + private Map records = new HashMap<>(); + + private Map passiveValues = new HashMap<>(); + + private BukkitTask passiveRefreshTask; + + public PlayerStatisticsManager(ECPlayer player) { + this.player = player; + + this.initializeStatistics(); + this.startPassiveRefreshTask(); + } + + private void initializeStatistics() { + try { + this.records = PlayerService.getPlayerStatistics(this.player.getAnimusPlayer().getUuid()) + .stream() + .collect( + HashMap::new, + (map, record) -> map.put(record.getKey(), new PlayerStatistic(this.player, record)), + HashMap::putAll + ); + } catch (ApiException e) { + e.printStackTrace(); + } + } + + private void startPassiveRefreshTask() { + this.passiveRefreshTask = Bukkit.getScheduler().runTaskTimerAsynchronously( + ECATUP.getInstance(), + () -> { + this.passiveValues.forEach((key, value) -> { + this.get(key).set(value.value(this.player), "Passive refresh", false); + }); + }, + 0, + 20L * PASSIVE_REFRESH_INTERVAL + ); + } + + public void registerPassiveValue(Statistic statistic, IPassiveStatisticValue value, Boolean now) { + this.passiveValues.put(statistic.getKey(), value); + + if (now) { + this.get(statistic).set(value.value(this.player), "Passive registration", false); + } + } + + public void triggerPassiveRefresh() { + this.passiveValues.forEach((key, value) -> { + this.get(key).set(value.value(this.player), "Forced passive refresh", false); + }); + } + + public void unload() { + this.passiveRefreshTask.cancel(); + } + + public PlayerStatistic get(Statistic statistic) { + if (!this.records.containsKey(statistic.getKey())) { + PlayerStatistic stat = new PlayerStatistic(this.player, statistic); + this.records.put(statistic.getKey(), stat); + return stat; + } else { + return this.records.get(statistic.getKey()); + } + } + + private PlayerStatistic get(String key) { + return this.get(new Statistic(key)); + } +} diff --git a/src/main/java/fr/efreicraft/ecatup/players/statistics/Statistic.java b/src/main/java/fr/efreicraft/ecatup/players/statistics/Statistic.java new file mode 100644 index 0000000..6194442 --- /dev/null +++ b/src/main/java/fr/efreicraft/ecatup/players/statistics/Statistic.java @@ -0,0 +1,47 @@ +package fr.efreicraft.ecatup.players.statistics; + +import fr.efreicraft.animus.models.PlayerStatisticRecord; + +public class Statistic { + private String key; + + private String displayName; + + /** + * Minecraft Color Code + */ + private String color; + + private PlayerStatisticRecord.TypeEnum type; + + public Statistic(String key) { + this.key = key; + } + + public Statistic(String key, String displayName, String color, PlayerStatisticRecord.TypeEnum type) { + this(key); + this.displayName = displayName; + this.color = color; + this.type = type; + } + + public String getKey() { + return key; + } + + public String getDisplayName() { + return displayName; + } + + public String getColor() { + return color; + } + + public PlayerStatisticRecord.TypeEnum getType() { + return type; + } + + public static Statistic fromRecord(PlayerStatisticRecord record) { + return new Statistic(record.getKey(), record.getDisplayName(), record.getColor(), record.getType()); + } +} diff --git a/src/main/java/fr/efreicraft/ecatup/players/statistics/interfaces/IPassiveStatisticValue.java b/src/main/java/fr/efreicraft/ecatup/players/statistics/interfaces/IPassiveStatisticValue.java new file mode 100644 index 0000000..db0351f --- /dev/null +++ b/src/main/java/fr/efreicraft/ecatup/players/statistics/interfaces/IPassiveStatisticValue.java @@ -0,0 +1,13 @@ +package fr.efreicraft.ecatup.players.statistics.interfaces; + +import fr.efreicraft.ecatup.players.ECPlayer; + +/** + * Interface fonctionnelle pour les lambda de récupération de valeur de statistiques passives. + * + * @author Antoine B. {@literal } + * @project ECATUP + */ +public interface IPassiveStatisticValue { + int value(ECPlayer player); +} \ No newline at end of file diff --git a/src/main/java/fr/efreicraft/ecatup/utils/MessageUtils.java b/src/main/java/fr/efreicraft/ecatup/utils/MessageUtils.java index ac577cb..afbb0f4 100644 --- a/src/main/java/fr/efreicraft/ecatup/utils/MessageUtils.java +++ b/src/main/java/fr/efreicraft/ecatup/utils/MessageUtils.java @@ -57,6 +57,11 @@ public enum ChatPrefix { */ TEAM("&dÉquipe"), + /** + * Préfixe pour les messages concernant les statistiques. + */ + STATS("&eStatistiques"), + /** * Préfixe vide. */