From 690ea97aa3bf1cc36a9f9952b4350a695603c374 Mon Sep 17 00:00:00 2001 From: Ajneb97 Date: Sun, 21 Jan 2024 11:12:38 -0300 Subject: [PATCH] 1.16.1 --- plugin.yml | 2 +- src/mc/ajneb97/juego/Tablero.java | 4 +- .../lib/actionbarapi/ActionBarAPI.java | 2 +- src/mc/ajneb97/lib/fastboard/FastBoard.java | 620 +++-------------- .../ajneb97/lib/fastboard/FastBoardBase.java | 641 ++++++++++++++++++ .../ajneb97/lib/fastboard/FastReflection.java | 29 +- src/mc/ajneb97/lib/titleapi/TitleAPI.java | 2 +- .../managers/InventarioCoronacion.java | 8 +- src/mc/ajneb97/otros/Utilidades.java | 16 +- src/mc/ajneb97/versiones/V1_20.java | 95 +++ 10 files changed, 872 insertions(+), 547 deletions(-) create mode 100644 src/mc/ajneb97/lib/fastboard/FastBoardBase.java create mode 100644 src/mc/ajneb97/versiones/V1_20.java diff --git a/plugin.yml b/plugin.yml index 403133f..b146766 100644 --- a/plugin.yml +++ b/plugin.yml @@ -1,5 +1,5 @@ main: mc.ajneb97.MineChess -version: 1.14.1 +version: 1.16.1 name: MineChess api-version: 1.13 softdepend: [Multiverse-Core,HolographicDisplays,PlaceholderAPI] diff --git a/src/mc/ajneb97/juego/Tablero.java b/src/mc/ajneb97/juego/Tablero.java index 7c1b9f2..059faaf 100644 --- a/src/mc/ajneb97/juego/Tablero.java +++ b/src/mc/ajneb97/juego/Tablero.java @@ -21,7 +21,7 @@ public class Tablero { if(color.equals("b")) { if(Bukkit.getVersion().contains("1.13") || Bukkit.getVersion().contains("1.14") || Bukkit.getVersion().contains("1.15") || Bukkit.getVersion().contains("1.16") || Bukkit.getVersion().contains("1.17") || Bukkit.getVersion().contains("1.18") - || Bukkit.getVersion().contains("1.19")) { + || Bukkit.getVersion().contains("1.19") || Bukkit.getVersion().contains("1.20")) { m = Material.valueOf("WHITE_WOOL"); }else { m = Material.WOOL; @@ -39,7 +39,7 @@ public class Tablero { }else { if(Bukkit.getVersion().contains("1.13") || Bukkit.getVersion().contains("1.14") || Bukkit.getVersion().contains("1.15") || Bukkit.getVersion().contains("1.16") || Bukkit.getVersion().contains("1.17") || Bukkit.getVersion().contains("1.18") - || Bukkit.getVersion().contains("1.19")) { + || Bukkit.getVersion().contains("1.19") || Bukkit.getVersion().contains("1.20")) { m = Material.valueOf("GRAY_WOOL"); l.getBlock().setType(m); l.clone().add(1,0,0).getBlock().setType(m); diff --git a/src/mc/ajneb97/lib/actionbarapi/ActionBarAPI.java b/src/mc/ajneb97/lib/actionbarapi/ActionBarAPI.java index 9086801..31d85dc 100644 --- a/src/mc/ajneb97/lib/actionbarapi/ActionBarAPI.java +++ b/src/mc/ajneb97/lib/actionbarapi/ActionBarAPI.java @@ -17,7 +17,7 @@ public class ActionBarAPI public static void sendActionBar(Player player, String message) { if(Bukkit.getVersion().contains("1.16") || Bukkit.getVersion().contains("1.17") || Bukkit.getVersion().contains("1.18") - || Bukkit.getVersion().contains("1.19")) { + || Bukkit.getVersion().contains("1.19") || Bukkit.getVersion().contains("1.20")) { TextComponent text_component = new TextComponent(message); player.spigot().sendMessage(ChatMessageType.ACTION_BAR, text_component); return; diff --git a/src/mc/ajneb97/lib/fastboard/FastBoard.java b/src/mc/ajneb97/lib/fastboard/FastBoard.java index 05e2630..30f2fe0 100644 --- a/src/mc/ajneb97/lib/fastboard/FastBoard.java +++ b/src/mc/ajneb97/lib/fastboard/FastBoard.java @@ -1,3 +1,26 @@ +/* + * This file is part of FastBoard, licensed under the MIT License. + * + * Copyright (c) 2019-2023 MrMicky + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package mc.ajneb97.lib.fastboard; import org.bukkit.ChatColor; @@ -5,294 +28,55 @@ import org.bukkit.entity.Player; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; import java.lang.reflect.Array; -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.concurrent.ThreadLocalRandom; /** - * Lightweight packet-based scoreboard API for Bukkit plugins. - * It can be safely used asynchronously as everything is at packet level. - *

- * The project is on GitHub. - * - * @author MrMicky - * @version 1.2.0-SNAPSHOT + * {@inheritDoc} */ -public class FastBoard { +public class FastBoard extends FastBoardBase { - private static final Map, Field[]> PACKETS = new HashMap<>(8); - private static final String[] COLOR_CODES = Arrays.stream(ChatColor.values()) - .map(Object::toString) - .toArray(String[]::new); - private static final VersionType VERSION_TYPE; - // Packets and components - private static final Class CHAT_COMPONENT_CLASS; - private static final Class CHAT_FORMAT_ENUM; - private static final Object EMPTY_MESSAGE; - private static final Object RESET_FORMATTING; private static final MethodHandle MESSAGE_FROM_STRING; - private static final MethodHandle PLAYER_CONNECTION; - private static final MethodHandle SEND_PACKET; - private static final MethodHandle PLAYER_GET_HANDLE; - // 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; - // Scoreboard enums - private static final Class ENUM_SB_HEALTH_DISPLAY; - private static final Class ENUM_SB_ACTION; - private static final Object ENUM_SB_HEALTH_DISPLAY_INTEGER; - private static final Object ENUM_SB_ACTION_CHANGE; - private static final Object ENUM_SB_ACTION_REMOVE; + private static final Object EMPTY_MESSAGE; static { try { MethodHandles.Lookup lookup = MethodHandles.lookup(); - - if (FastReflection.isRepackaged()) { - VERSION_TYPE = VersionType.V1_17; - } else if (FastReflection.nmsOptionalClass(null, "ScoreboardServer$Action").isPresent()) { - VERSION_TYPE = VersionType.V1_13; - } else if (FastReflection.nmsOptionalClass(null, "IScoreboardCriteria$EnumScoreboardHealthDisplay").isPresent()) { - VERSION_TYPE = VersionType.V1_8; - } else { - VERSION_TYPE = VersionType.V1_7; - } - - String gameProtocolPackage = "network.protocol.game"; - Class craftPlayerClass = FastReflection.obcClass("entity.CraftPlayer"); Class craftChatMessageClass = FastReflection.obcClass("util.CraftChatMessage"); - Class entityPlayerClass = FastReflection.nmsClass("server.level", "EntityPlayer"); - Class playerConnectionClass = FastReflection.nmsClass("server.network", "PlayerConnection"); - 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 sbTeamClass = VersionType.V1_17.isHigherOrEqual() - ? FastReflection.innerClass(packetSbTeamClass, innerClass -> !innerClass.isEnum()) : null; - Field playerConnectionField = Arrays.stream(entityPlayerClass.getFields()) - .filter(field -> field.getType().isAssignableFrom(playerConnectionClass)) - .findFirst().orElseThrow(NoSuchFieldException::new); - Method sendPacketMethod = Arrays.stream(playerConnectionClass.getMethods()) - .filter(m -> m.getParameterCount() == 1 && m.getParameterTypes()[0] == packetClass) - .findFirst().orElseThrow(NoSuchMethodException::new); - MESSAGE_FROM_STRING = lookup.unreflect(craftChatMessageClass.getMethod("fromString", String.class)); - CHAT_COMPONENT_CLASS = FastReflection.nmsClass("network.chat", "IChatBaseComponent"); - CHAT_FORMAT_ENUM = FastReflection.nmsClass(null, "EnumChatFormat"); EMPTY_MESSAGE = Array.get(MESSAGE_FROM_STRING.invoke(""), 0); - RESET_FORMATTING = FastReflection.enumValueOf(CHAT_FORMAT_ENUM, "RESET", 21); - PLAYER_GET_HANDLE = lookup.findVirtual(craftPlayerClass, "getHandle", MethodType.methodType(entityPlayerClass)); - PLAYER_CONNECTION = lookup.unreflectGetter(playerConnectionField); - 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); - PACKET_SB_TEAM = FastReflection.findPacketConstructor(packetSbTeamClass, lookup); - PACKET_SB_SERIALIZABLE_TEAM = sbTeamClass == null ? null : FastReflection.findPacketConstructor(sbTeamClass, lookup); - - for (Class clazz : Arrays.asList(packetSbObjClass, packetSbDisplayObjClass, packetSbScoreClass, packetSbTeamClass, sbTeamClass)) { - 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.isHigherOrEqual()) { - 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_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); - } else { - ENUM_SB_HEALTH_DISPLAY = null; - ENUM_SB_ACTION = null; - ENUM_SB_HEALTH_DISPLAY_INTEGER = null; - ENUM_SB_ACTION_CHANGE = null; - ENUM_SB_ACTION_REMOVE = null; - } } catch (Throwable t) { throw new ExceptionInInitializerError(t); } } - private final Player player; - private final String id; - - private final List lines = new ArrayList<>(); - private String title = ChatColor.RESET.toString(); - - private boolean deleted = false; - /** - * Creates a new FastBoard. - * - * @param player the owner of the scoreboard + * {@inheritDoc} */ public FastBoard(Player player) { - this.player = Objects.requireNonNull(player, "player"); - this.id = "fb-" + Integer.toHexString(ThreadLocalRandom.current().nextInt()); - - try { - sendObjectivePacket(ObjectiveMode.CREATE); - sendDisplayObjectivePacket(); - } catch (Throwable t) { - throw new RuntimeException("Unable to create scoreboard", t); - } + super(player); } /** - * Get the scoreboard title. - * - * @return the scoreboard title - */ - public String getTitle() { - return this.title; - } - - /** - * Update the scoreboard title. - * - * @param title the new scoreboard title - * @throws IllegalArgumentException if the title is longer than 32 chars on 1.12 or lower - * @throws IllegalStateException if {@link #delete()} was call before + * {@inheritDoc} */ + @Override public void updateTitle(String title) { - if (this.title.equals(Objects.requireNonNull(title, "title"))) { - return; - } + Objects.requireNonNull(title, "title"); if (!VersionType.V1_13.isHigherOrEqual() && title.length() > 32) { throw new IllegalArgumentException("Title is longer than 32 chars"); } - this.title = title; - - try { - sendObjectivePacket(ObjectiveMode.UPDATE); - } catch (Throwable t) { - throw new RuntimeException("Unable to update scoreboard title", t); - } + super.updateTitle(title); } /** - * Get the scoreboard lines. - * - * @return the scoreboard lines - */ - public List getLines() { - return new ArrayList<>(this.lines); - } - - /** - * Get the specified scoreboard line. - * - * @param line the line number - * @return the line - * @throws IndexOutOfBoundsException if the line is higher than {@code size} - */ - public String getLine(int line) { - checkLineNumber(line, true, false); - - return this.lines.get(line); - } - - /** - * Update a single scoreboard line. - * - * @param line the line number - * @param text the new line text - * @throws IndexOutOfBoundsException if the line is higher than {@link #size() size() + 1} - */ - public synchronized void updateLine(int line, String text) { - checkLineNumber(line, false, true); - - try { - if (line < size()) { - this.lines.set(line, text); - - sendTeamPacket(getScoreByLine(line), TeamMode.UPDATE); - return; - } - - List newLines = new ArrayList<>(this.lines); - - if (line > size()) { - for (int i = size(); i < line; i++) { - newLines.add(""); - } - } - - newLines.add(text); - - updateLines(newLines); - } catch (Throwable t) { - throw new RuntimeException("Unable to update scoreboard lines", t); - } - } - - /** - * Remove a scoreboard line. - * - * @param line the line number - */ - public synchronized void removeLine(int line) { - checkLineNumber(line, false, false); - - if (line >= size()) { - return; - } - - List newLines = new ArrayList<>(this.lines); - newLines.remove(line); - updateLines(newLines); - } - - /** - * Update all the scoreboard lines. - * - * @param lines the new lines - * @throws IllegalArgumentException if one line is longer than 30 chars on 1.12 or lower - * @throws IllegalStateException if {@link #delete()} was call before + * {@inheritDoc} */ + @Override public void updateLines(String... lines) { - updateLines(Arrays.asList(lines)); - } - - /** - * Update the lines of the scoreboard - * - * @param lines the new scoreboard lines - * @throws IllegalArgumentException if one line is longer than 30 chars on 1.12 or lower - * @throws IllegalStateException if {@link #delete()} was call before - */ - public synchronized void updateLines(Collection lines) { Objects.requireNonNull(lines, "lines"); - checkLineNumber(lines.size(), false, true); if (!VersionType.V1_13.isHigherOrEqual()) { int lineCount = 0; @@ -304,97 +88,64 @@ public class FastBoard { } } - List oldLines = new ArrayList<>(this.lines); - this.lines.clear(); - this.lines.addAll(lines); + super.updateLines(lines); + } - int linesSize = this.lines.size(); + @Override + protected void sendLineChange(int score) throws Throwable { + int maxLength = hasLinesMaxLength() ? 16 : 1024; + String line = getLineByScore(score); + String prefix; + String suffix = ""; - try { - if (oldLines.size() != linesSize) { - List oldLinesCopy = new ArrayList<>(oldLines); + if (line == null || line.isEmpty()) { + prefix = COLOR_CODES[score] + ChatColor.RESET; + } else if (line.length() <= maxLength) { + prefix = line; + } else { + // Prevent splitting color codes + int index = line.charAt(maxLength - 1) == ChatColor.COLOR_CHAR + ? (maxLength - 1) : maxLength; + prefix = line.substring(0, index); + String suffixTmp = line.substring(index); + ChatColor chatColor = null; - if (oldLines.size() > linesSize) { - for (int i = oldLinesCopy.size(); i > linesSize; i--) { - sendTeamPacket(i - 1, TeamMode.REMOVE); - sendScorePacket(i - 1, ScoreboardAction.REMOVE); - - oldLines.remove(0); - } - } else { - for (int i = oldLinesCopy.size(); i < linesSize; i++) { - sendScorePacket(i, ScoreboardAction.CHANGE); - sendTeamPacket(i, TeamMode.CREATE); - - oldLines.add(oldLines.size() - i, getLineByScore(i)); - } - } + if (suffixTmp.length() >= 2 && suffixTmp.charAt(0) == ChatColor.COLOR_CHAR) { + chatColor = ChatColor.getByChar(suffixTmp.charAt(1)); } - for (int i = 0; i < linesSize; i++) { - if (!Objects.equals(getLineByScore(oldLines, i), getLineByScore(i))) { - sendTeamPacket(i, TeamMode.UPDATE); - } - } - } catch (Throwable t) { - throw new RuntimeException("Unable to update scoreboard lines", t); - } - } + String color = ChatColor.getLastColors(prefix); + boolean addColor = chatColor == null || chatColor.isFormat(); - /** - * Get the player who has the scoreboard. - * - * @return current player for this FastBoard - */ - public Player getPlayer() { - return this.player; - } - - /** - * Get the scoreboard id. - * - * @return the id - */ - public String getId() { - return this.id; - } - - /** - * Get if the scoreboard is deleted. - * - * @return true if the scoreboard is deleted - */ - public boolean isDeleted() { - return this.deleted; - } - - /** - * Get the scoreboard size (the number of lines). - * - * @return the size - */ - public int size() { - return this.lines.size(); - } - - /** - * Delete this FastBoard, and will remove the scoreboard for the associated player if he is online. - * After this, all uses of {@link #updateLines} and {@link #updateTitle} will throws an {@link IllegalStateException} - * - * @throws IllegalStateException if this was already call before - */ - public void delete() { - try { - for (int i = 0; i < this.lines.size(); i++) { - sendTeamPacket(i, TeamMode.REMOVE); - } - - sendObjectivePacket(ObjectiveMode.REMOVE); - } catch (Throwable t) { - throw new RuntimeException("Unable to delete scoreboard", t); + suffix = (addColor ? (color.isEmpty() ? ChatColor.RESET.toString() : color) : "") + suffixTmp; } - this.deleted = true; + if (prefix.length() > maxLength || suffix.length() > maxLength) { + // Something went wrong, just cut to prevent client crash/kick + prefix = prefix.substring(0, maxLength); + suffix = suffix.substring(0, maxLength); + } + + sendTeamPacket(score, TeamMode.UPDATE, prefix, suffix); + } + + @Override + protected Object toMinecraftComponent(String line) throws Throwable { + if (line == null || line.isEmpty()) { + return EMPTY_MESSAGE; + } + + return Array.get(MESSAGE_FROM_STRING.invoke(line), 0); + } + + @Override + protected String serializeLine(String value) { + return value; + } + + @Override + protected String emptyLine() { + return ""; } /** @@ -407,205 +158,4 @@ public class FastBoard { protected boolean hasLinesMaxLength() { return !VersionType.V1_13.isHigherOrEqual(); } - - private void checkLineNumber(int line, boolean checkInRange, boolean checkMax) { - if (line < 0) { - throw new IllegalArgumentException("Line number must be positive"); - } - - if (checkInRange && line >= this.lines.size()) { - throw new IllegalArgumentException("Line number must be under " + this.lines.size()); - } - - if (checkMax && line >= COLOR_CODES.length - 1) { - throw new IllegalArgumentException("Line number is too high: " + line); - } - } - - private int getScoreByLine(int line) { - return this.lines.size() - line - 1; - } - - private String getLineByScore(int score) { - return getLineByScore(this.lines, score); - } - - private String getLineByScore(List lines, int score) { - return lines.get(lines.size() - score - 1); - } - - private void sendObjectivePacket(ObjectiveMode mode) throws Throwable { - Object packet = PACKET_SB_OBJ.invoke(); - - setField(packet, String.class, this.id); - setField(packet, int.class, mode.ordinal()); - - if (mode != ObjectiveMode.REMOVE) { - setComponentField(packet, this.title, 1); - - if (VersionType.V1_8.isHigherOrEqual()) { - setField(packet, ENUM_SB_HEALTH_DISPLAY, ENUM_SB_HEALTH_DISPLAY_INTEGER); - } - } else if (VERSION_TYPE == VersionType.V1_7) { - setField(packet, String.class, "", 1); - } - - sendPacket(packet); - } - - private void sendDisplayObjectivePacket() throws Throwable { - Object packet = PACKET_SB_DISPLAY_OBJ.invoke(); - - setField(packet, int.class, 1); // Position (1: sidebar) - setField(packet, String.class, this.id); // Score Name - - sendPacket(packet); - } - - private void sendScorePacket(int score, ScoreboardAction action) throws Throwable { - Object packet = PACKET_SB_SCORE.invoke(); - - setField(packet, String.class, COLOR_CODES[score], 0); // Player Name - - if (VersionType.V1_8.isHigherOrEqual()) { - setField(packet, ENUM_SB_ACTION, action == ScoreboardAction.REMOVE ? ENUM_SB_ACTION_REMOVE : ENUM_SB_ACTION_CHANGE); - } else { - setField(packet, int.class, action.ordinal(), 1); // Action - } - - if (action == ScoreboardAction.CHANGE) { - setField(packet, String.class, this.id, 1); // Objective Name - setField(packet, int.class, score); // Score - } - - sendPacket(packet); - } - - private void sendTeamPacket(int score, TeamMode mode) throws Throwable { - if (mode == TeamMode.ADD_PLAYERS || mode == TeamMode.REMOVE_PLAYERS) { - throw new UnsupportedOperationException(); - } - - int maxLength = hasLinesMaxLength() ? 16 : 1024; - Object packet = PACKET_SB_TEAM.invoke(); - - setField(packet, String.class, this.id + ':' + score); // Team name - setField(packet, int.class, mode.ordinal(), VERSION_TYPE == VersionType.V1_8 ? 1 : 0); // Update mode - - if (mode == TeamMode.CREATE || mode == TeamMode.UPDATE) { - String line = getLineByScore(score); - String prefix; - String suffix = null; - - if (line == null || line.isEmpty()) { - prefix = COLOR_CODES[score] + ChatColor.RESET; - } else if (line.length() <= maxLength) { - prefix = line; - } else { - // Prevent splitting color codes - int index = line.charAt(maxLength - 1) == ChatColor.COLOR_CHAR ? (maxLength - 1) : maxLength; - prefix = line.substring(0, index); - String suffixTmp = line.substring(index); - ChatColor chatColor = null; - - if (suffixTmp.length() >= 2 && suffixTmp.charAt(0) == ChatColor.COLOR_CHAR) { - chatColor = ChatColor.getByChar(suffixTmp.charAt(1)); - } - - String color = ChatColor.getLastColors(prefix); - boolean addColor = chatColor == null || chatColor.isFormat(); - - suffix = (addColor ? (color.isEmpty() ? ChatColor.RESET.toString() : color) : "") + suffixTmp; - } - - if (prefix.length() > maxLength || (suffix != null && suffix.length() > maxLength)) { - // Something went wrong, just cut to prevent client crash/kick - prefix = prefix.substring(0, maxLength); - suffix = (suffix != null) ? suffix.substring(0, maxLength) : null; - } - - if (VersionType.V1_17.isHigherOrEqual()) { - Object team = PACKET_SB_SERIALIZABLE_TEAM.invoke(); - // Since the packet is initialized with null values, we need to change more things. - setComponentField(team, "", 0); // Display name - setField(team, CHAT_FORMAT_ENUM, RESET_FORMATTING); // Color - setComponentField(team, prefix, 1); // Prefix - setComponentField(team, suffix == null ? "" : suffix, 2); // Suffix - setField(team, String.class, "always", 0); // Visibility - setField(team, String.class, "always", 1); // Collisions - setField(packet, Optional.class, Optional.of(team)); - } else { - setComponentField(packet, prefix, 2); // Prefix - setComponentField(packet, suffix == null ? "" : suffix, 3); // Suffix - setField(packet, String.class, "always", 4); // Visibility for 1.8+ - setField(packet, String.class, "always", 5); // Collisions for 1.9+ - } - - if (mode == TeamMode.CREATE) { - setField(packet, Collection.class, Collections.singletonList(COLOR_CODES[score])); // Players in the team - } - } - - sendPacket(packet); - } - - private void sendPacket(Object packet) throws Throwable { - if (this.deleted) { - throw new IllegalStateException("This FastBoard is deleted"); - } - - if (this.player.isOnline()) { - Object entityPlayer = PLAYER_GET_HANDLE.invoke(this.player); - Object playerConnection = PLAYER_CONNECTION.invoke(entityPlayer); - SEND_PACKET.invoke(playerConnection, packet); - } - } - - private void setField(Object object, Class fieldType, Object value) throws ReflectiveOperationException { - setField(object, fieldType, value, 0); - } - - private void setField(Object packet, Class fieldType, Object value, int count) throws ReflectiveOperationException { - int i = 0; - for (Field field : PACKETS.get(packet.getClass())) { - if (field.getType() == fieldType && count == i++) { - field.set(packet, value); - } - } - } - - private void setComponentField(Object packet, String value, int count) throws Throwable { - if (!VersionType.V1_13.isHigherOrEqual()) { - setField(packet, String.class, value, count); - return; - } - - int i = 0; - for (Field field : PACKETS.get(packet.getClass())) { - if ((field.getType() == String.class || field.getType() == CHAT_COMPONENT_CLASS) && count == i++) { - field.set(packet, value.isEmpty() ? EMPTY_MESSAGE : Array.get(MESSAGE_FROM_STRING.invoke(value), 0)); - } - } - } - - enum ObjectiveMode { - CREATE, REMOVE, UPDATE - } - - enum TeamMode { - CREATE, REMOVE, UPDATE, ADD_PLAYERS, REMOVE_PLAYERS - } - - enum ScoreboardAction { - CHANGE, REMOVE - } - - enum VersionType { - V1_7, V1_8, V1_13, V1_17; - - public boolean isHigherOrEqual() { - return VERSION_TYPE.ordinal() >= ordinal(); - } - } -} - +} \ No newline at end of file diff --git a/src/mc/ajneb97/lib/fastboard/FastBoardBase.java b/src/mc/ajneb97/lib/fastboard/FastBoardBase.java new file mode 100644 index 0000000..5b74fee --- /dev/null +++ b/src/mc/ajneb97/lib/fastboard/FastBoardBase.java @@ -0,0 +1,641 @@ + +/* + * This file is part of FastBoard, licensed under the MIT License. + * + * Copyright (c) 2019-2023 MrMicky + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package mc.ajneb97.lib.fastboard; + + +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Stream; + +/** + * Lightweight packet-based scoreboard API for Bukkit plugins. + * It can be safely used asynchronously as everything is at packet level. + *

+ * The project is on GitHub. + * + * @author MrMicky + * @version 2.0.2 + */ +public abstract class FastBoardBase { + + private static final Map, Field[]> PACKETS = new HashMap<>(8); + protected static final String[] COLOR_CODES = Arrays.stream(ChatColor.values()) + .map(Object::toString) + .toArray(String[]::new); + private static final VersionType VERSION_TYPE; + // Packets and components + private static final Class CHAT_COMPONENT_CLASS; + private static final Class CHAT_FORMAT_ENUM; + private static final Object RESET_FORMATTING; + private static final MethodHandle PLAYER_CONNECTION; + private static final MethodHandle SEND_PACKET; + private static final MethodHandle PLAYER_GET_HANDLE; + // 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_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; + // 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; + private static final Object ENUM_SB_ACTION_REMOVE; + + static { + try { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + + if (FastReflection.isRepackaged()) { + VERSION_TYPE = VersionType.V1_17; + } else if (FastReflection.nmsOptionalClass(null, "ScoreboardServer$Action").isPresent()) { + VERSION_TYPE = VersionType.V1_13; + } else if (FastReflection.nmsOptionalClass(null, "IScoreboardCriteria$EnumScoreboardHealthDisplay").isPresent()) { + VERSION_TYPE = VersionType.V1_8; + } else { + VERSION_TYPE = VersionType.V1_7; + } + + 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 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 sbTeamClass = VersionType.V1_17.isHigherOrEqual() + ? FastReflection.innerClass(packetSbTeamClass, innerClass -> !innerClass.isEnum()) : null; + Field playerConnectionField = Arrays.stream(entityPlayerClass.getFields()) + .filter(field -> field.getType().isAssignableFrom(playerConnectionClass)) + .findFirst().orElseThrow(NoSuchFieldException::new); + Method sendPacketMethod = Stream.concat( + Arrays.stream(playerConnectionClass.getSuperclass().getMethods()), + Arrays.stream(playerConnectionClass.getMethods()) + ) + .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"); + 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; + PLAYER_GET_HANDLE = lookup.findVirtual(craftPlayerClass, "getHandle", MethodType.methodType(entityPlayerClass)); + PLAYER_CONNECTION = lookup.unreflectGetter(playerConnectionField); + SEND_PACKET = lookup.unreflect(sendPacketMethod); + PACKET_SB_OBJ = FastReflection.findPacketConstructor(packetSbObjClass, lookup); + PACKET_SB_DISPLAY_OBJ = FastReflection.findPacketConstructor(packetSbDisplayObjClass, lookup); + + Optional> numberFormat = FastReflection.nmsOptionalClass("network.chat.numbers", "NumberFormat"); + MethodHandle packetSbSetScore; + MethodHandle packetSbResetScore = null; + Object blankNumberFormat = null; + + if (numberFormat.isPresent()) { // 1.20.3 + Class blankFormatClass = FastReflection.nmsClass("network.chat.numbers", "BlankFormat"); + 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); + Optional blankField = Arrays.stream(blankFormatClass.getFields()).filter(f -> f.getType() == blankFormatClass).findAny(); + packetSbSetScore = lookup.findConstructor(packetSbScoreClass, setScoreType); + 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"); + 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); + BLANK_NUMBER_FORMAT = blankNumberFormat; + + for (Class clazz : Arrays.asList(packetSbObjClass, packetSbDisplayObjClass, packetSbScoreClass, packetSbTeamClass, sbTeamClass)) { + 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.isHigherOrEqual()) { + 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_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); + } else { + ENUM_SB_HEALTH_DISPLAY = null; + ENUM_SB_ACTION = null; + ENUM_SB_HEALTH_DISPLAY_INTEGER = null; + ENUM_SB_ACTION_CHANGE = null; + ENUM_SB_ACTION_REMOVE = null; + } + } catch (Throwable t) { + throw new ExceptionInInitializerError(t); + } + } + + private final Player player; + private final String id; + + private final List lines = new ArrayList<>(); + private T title = emptyLine(); + + private boolean deleted = false; + + /** + * Creates a new FastBoard. + * + * @param player the owner of the scoreboard + */ + protected FastBoardBase(Player player) { + this.player = Objects.requireNonNull(player, "player"); + this.id = "fb-" + Integer.toHexString(ThreadLocalRandom.current().nextInt()); + + try { + sendObjectivePacket(ObjectiveMode.CREATE); + sendDisplayObjectivePacket(); + } catch (Throwable t) { + throw new RuntimeException("Unable to create scoreboard", t); + } + } + + /** + * Get the scoreboard title. + * + * @return the scoreboard title + */ + public T getTitle() { + return this.title; + } + + /** + * Update the scoreboard title. + * + * @param title the new scoreboard title + * @throws IllegalArgumentException if the title is longer than 32 chars on 1.12 or lower + * @throws IllegalStateException if {@link #delete()} was call before + */ + public void updateTitle(T title) { + if (this.title.equals(Objects.requireNonNull(title, "title"))) { + return; + } + + this.title = title; + + try { + sendObjectivePacket(ObjectiveMode.UPDATE); + } catch (Throwable t) { + throw new RuntimeException("Unable to update scoreboard title", t); + } + } + + /** + * Get the scoreboard lines. + * + * @return the scoreboard lines + */ + public List getLines() { + return new ArrayList<>(this.lines); + } + + /** + * Get the specified scoreboard line. + * + * @param line the line number + * @return the line + * @throws IndexOutOfBoundsException if the line is higher than {@code size} + */ + public T getLine(int line) { + checkLineNumber(line, true, false); + + return this.lines.get(line); + } + + /** + * Update a single scoreboard line. + * + * @param line the line number + * @param text the new line text + * @throws IndexOutOfBoundsException if the line is higher than {@link #size() size() + 1} + */ + public synchronized void updateLine(int line, T text) { + checkLineNumber(line, false, true); + + try { + if (line < size()) { + this.lines.set(line, text); + + sendLineChange(getScoreByLine(line)); + return; + } + + List newLines = new ArrayList<>(this.lines); + + if (line > size()) { + for (int i = size(); i < line; i++) { + newLines.add(emptyLine()); + } + } + + newLines.add(text); + + updateLines(newLines); + } catch (Throwable t) { + throw new RuntimeException("Unable to update scoreboard lines", t); + } + } + + /** + * Remove a scoreboard line. + * + * @param line the line number + */ + public synchronized void removeLine(int line) { + checkLineNumber(line, false, false); + + if (line >= size()) { + return; + } + + List newLines = new ArrayList<>(this.lines); + newLines.remove(line); + updateLines(newLines); + } + + /** + * Update all the scoreboard lines. + * + * @param lines the new lines + * @throws IllegalArgumentException if one line is longer than 30 chars on 1.12 or lower + * @throws IllegalStateException if {@link #delete()} was call before + */ + public void updateLines(T... lines) { + updateLines(Arrays.asList(lines)); + } + + /** + * Update the lines of the scoreboard + * + * @param lines the new scoreboard lines + * @throws IllegalArgumentException if one line is longer than 30 chars on 1.12 or lower + * @throws IllegalStateException if {@link #delete()} was call before + */ + public synchronized void updateLines(Collection lines) { + Objects.requireNonNull(lines, "lines"); + checkLineNumber(lines.size(), false, true); + + List oldLines = new ArrayList<>(this.lines); + this.lines.clear(); + this.lines.addAll(lines); + + int linesSize = this.lines.size(); + + try { + if (oldLines.size() != linesSize) { + List oldLinesCopy = new ArrayList<>(oldLines); + + if (oldLines.size() > linesSize) { + for (int i = oldLinesCopy.size(); i > linesSize; i--) { + sendTeamPacket(i - 1, TeamMode.REMOVE); + sendScorePacket(i - 1, ScoreboardAction.REMOVE); + + oldLines.remove(0); + } + } else { + for (int i = oldLinesCopy.size(); i < linesSize; i++) { + sendScorePacket(i, ScoreboardAction.CHANGE); + sendTeamPacket(i, TeamMode.CREATE, null, null); + } + } + } + + for (int i = 0; i < linesSize; i++) { + if (!Objects.equals(getLineByScore(oldLines, i), getLineByScore(i))) { + sendLineChange(i); + } + } + } catch (Throwable t) { + throw new RuntimeException("Unable to update scoreboard lines", t); + } + } + + /** + * Get the player who has the scoreboard. + * + * @return current player for this FastBoard + */ + public Player getPlayer() { + return this.player; + } + + /** + * Get the scoreboard id. + * + * @return the id + */ + public String getId() { + return this.id; + } + + /** + * Get if the scoreboard is deleted. + * + * @return true if the scoreboard is deleted + */ + public boolean isDeleted() { + return this.deleted; + } + + /** + * Get the scoreboard size (the number of lines). + * + * @return the size + */ + public int size() { + return this.lines.size(); + } + + /** + * Delete this FastBoard, and will remove the scoreboard for the associated player if he is online. + * After this, all uses of {@link #updateLines} and {@link #updateTitle} will throw an {@link IllegalStateException} + * + * @throws IllegalStateException if this was already call before + */ + public void delete() { + try { + for (int i = 0; i < this.lines.size(); i++) { + sendTeamPacket(i, TeamMode.REMOVE); + } + + sendObjectivePacket(ObjectiveMode.REMOVE); + } catch (Throwable t) { + throw new RuntimeException("Unable to delete scoreboard", t); + } + + this.deleted = true; + } + + protected abstract void sendLineChange(int score) throws Throwable; + + 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) { + if (line < 0) { + throw new IllegalArgumentException("Line number must be positive"); + } + + if (checkInRange && line >= this.lines.size()) { + throw new IllegalArgumentException("Line number must be under " + this.lines.size()); + } + + if (checkMax && line >= COLOR_CODES.length - 1) { + throw new IllegalArgumentException("Line number is too high: " + line); + } + } + + protected int getScoreByLine(int line) { + return this.lines.size() - line - 1; + } + + protected T getLineByScore(int score) { + return getLineByScore(this.lines, score); + } + + protected T getLineByScore(List lines, int score) { + return score < lines.size() ? lines.get(lines.size() - score - 1) : null; + } + + protected void sendObjectivePacket(ObjectiveMode mode) throws Throwable { + Object packet = PACKET_SB_OBJ.invoke(); + + setField(packet, String.class, this.id); + setField(packet, int.class, mode.ordinal()); + + if (mode != ObjectiveMode.REMOVE) { + setComponentField(packet, this.title, 1); + + if (VersionType.V1_8.isHigherOrEqual()) { + setField(packet, ENUM_SB_HEALTH_DISPLAY, ENUM_SB_HEALTH_DISPLAY_INTEGER); + } + } else if (VERSION_TYPE == VersionType.V1_7) { + setField(packet, String.class, "", 1); + } + + sendPacket(packet); + } + + protected void sendDisplayObjectivePacket() throws Throwable { + Object packet = PACKET_SB_DISPLAY_OBJ.invoke(); + + setField(packet, DISPLAY_SLOT_TYPE, SIDEBAR_DISPLAY_SLOT); // Position + setField(packet, String.class, this.id); // Score Name + + sendPacket(packet); + } + + protected void sendScorePacket(int score, ScoreboardAction action) throws Throwable { + 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 + + if (VersionType.V1_8.isHigherOrEqual()) { + Object enumAction = action == ScoreboardAction.REMOVE + ? ENUM_SB_ACTION_REMOVE : ENUM_SB_ACTION_CHANGE; + setField(packet, ENUM_SB_ACTION, enumAction); + } else { + setField(packet, int.class, action.ordinal(), 1); // Action + } + + if (action == ScoreboardAction.CHANGE) { + setField(packet, String.class, this.id, 1); // Objective Name + setField(packet, int.class, score); // Score + } + + 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; + } + + sendPacket(PACKET_SB_SET_SCORE.invoke(objName, this.id, score, null, BLANK_NUMBER_FORMAT)); + } + + protected void sendTeamPacket(int score, TeamMode mode) throws Throwable { + sendTeamPacket(score, mode, null, null); + } + + protected void sendTeamPacket(int score, TeamMode mode, T prefix, T suffix) + throws Throwable { + if (mode == TeamMode.ADD_PLAYERS || mode == TeamMode.REMOVE_PLAYERS) { + throw new UnsupportedOperationException(); + } + + Object packet = PACKET_SB_TEAM.invoke(); + + setField(packet, String.class, this.id + ':' + score); // Team name + setField(packet, int.class, mode.ordinal(), VERSION_TYPE == VersionType.V1_8 ? 1 : 0); // Update mode + + if (mode == TeamMode.REMOVE) { + sendPacket(packet); + return; + } + + if (VersionType.V1_17.isHigherOrEqual()) { + Object team = PACKET_SB_SERIALIZABLE_TEAM.invoke(); + // Since the packet is initialized with null values, we need to change more things. + setComponentField(team, null, 0); // Display name + setField(team, CHAT_FORMAT_ENUM, RESET_FORMATTING); // Color + setComponentField(team, prefix, 1); // Prefix + setComponentField(team, suffix, 2); // Suffix + setField(team, String.class, "always", 0); // Visibility + setField(team, String.class, "always", 1); // Collisions + setField(packet, Optional.class, Optional.of(team)); + } else { + setComponentField(packet, prefix, 2); // Prefix + setComponentField(packet, suffix, 3); // Suffix + setField(packet, String.class, "always", 4); // Visibility for 1.8+ + setField(packet, String.class, "always", 5); // Collisions for 1.9+ + } + + if (mode == TeamMode.CREATE) { + setField(packet, Collection.class, Collections.singletonList(COLOR_CODES[score])); // Players in the team + } + + sendPacket(packet); + } + + private void sendPacket(Object packet) throws Throwable { + if (this.deleted) { + throw new IllegalStateException("This FastBoard is deleted"); + } + + if (this.player.isOnline()) { + Object entityPlayer = PLAYER_GET_HANDLE.invoke(this.player); + Object playerConnection = PLAYER_CONNECTION.invoke(entityPlayer); + SEND_PACKET.invoke(playerConnection, packet); + } + } + + private void setField(Object object, Class fieldType, Object value) + throws ReflectiveOperationException { + setField(object, fieldType, value, 0); + } + + private void setField(Object packet, Class fieldType, Object value, int count) + throws ReflectiveOperationException { + int i = 0; + for (Field field : PACKETS.get(packet.getClass())) { + if (field.getType() == fieldType && count == i++) { + field.set(packet, value); + } + } + } + + private void setComponentField(Object packet, T value, int count) throws Throwable { + if (!VersionType.V1_13.isHigherOrEqual()) { + String line = value != null ? serializeLine(value) : ""; + setField(packet, String.class, line, count); + return; + } + + int i = 0; + for (Field field : PACKETS.get(packet.getClass())) { + if ((field.getType() == String.class || field.getType() == CHAT_COMPONENT_CLASS) && count == i++) { + field.set(packet, toMinecraftComponent(value)); + } + } + } + + public enum ObjectiveMode { + CREATE, REMOVE, UPDATE + } + + public enum TeamMode { + CREATE, REMOVE, UPDATE, ADD_PLAYERS, REMOVE_PLAYERS + } + + public enum ScoreboardAction { + CHANGE, REMOVE + } + + enum VersionType { + V1_7, V1_8, V1_13, V1_17; + + public boolean isHigherOrEqual() { + return VERSION_TYPE.ordinal() >= ordinal(); + } + } +} diff --git a/src/mc/ajneb97/lib/fastboard/FastReflection.java b/src/mc/ajneb97/lib/fastboard/FastReflection.java index c784a97..e4737c8 100644 --- a/src/mc/ajneb97/lib/fastboard/FastReflection.java +++ b/src/mc/ajneb97/lib/fastboard/FastReflection.java @@ -1,3 +1,28 @@ + + +/* + * This file is part of FastBoard, licensed under the MIT License. + * + * Copyright (c) 2019-2023 MrMicky + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package mc.ajneb97.lib.fastboard; import org.bukkit.Bukkit; @@ -6,7 +31,6 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.util.Optional; import java.util.function.Predicate; @@ -116,7 +140,8 @@ public final class FastReflection { } } - Method allocateMethod = theUnsafe.getClass().getMethod("allocateInstance", Class.class); + MethodType allocateMethodType = MethodType.methodType(Object.class, Class.class); + MethodHandle allocateMethod = lookup.findVirtual(theUnsafe.getClass(), "allocateInstance", allocateMethodType); return () -> allocateMethod.invoke(theUnsafe, packetClass); } diff --git a/src/mc/ajneb97/lib/titleapi/TitleAPI.java b/src/mc/ajneb97/lib/titleapi/TitleAPI.java index a2a009b..ef4ea29 100644 --- a/src/mc/ajneb97/lib/titleapi/TitleAPI.java +++ b/src/mc/ajneb97/lib/titleapi/TitleAPI.java @@ -50,7 +50,7 @@ public class TitleAPI implements Listener { TitleSendEvent titleSendEvent = new TitleSendEvent(player, title, subtitle); if(Bukkit.getVersion().contains("1.13") || Bukkit.getVersion().contains("1.14") || Bukkit.getVersion().contains("1.15") || Bukkit.getVersion().contains("1.16") || Bukkit.getVersion().contains("1.17") || Bukkit.getVersion().contains("1.18") - || Bukkit.getVersion().contains("1.19")) { + || Bukkit.getVersion().contains("1.19") || Bukkit.getVersion().contains("1.20")) { if(title.isEmpty()) { title = " "; } diff --git a/src/mc/ajneb97/managers/InventarioCoronacion.java b/src/mc/ajneb97/managers/InventarioCoronacion.java index 36c98c4..c1eab18 100644 --- a/src/mc/ajneb97/managers/InventarioCoronacion.java +++ b/src/mc/ajneb97/managers/InventarioCoronacion.java @@ -39,7 +39,7 @@ public class InventarioCoronacion implements Listener { //Torre if(Bukkit.getVersion().contains("1.13") || Bukkit.getVersion().contains("1.14") || Bukkit.getVersion().contains("1.15") || Bukkit.getVersion().contains("1.16") || Bukkit.getVersion().contains("1.17") || Bukkit.getVersion().contains("1.18") - || Bukkit.getVersion().contains("1.19")) { + || Bukkit.getVersion().contains("1.19") || Bukkit.getVersion().contains("1.20")) { item = new ItemStack(Material.valueOf("PLAYER_HEAD"),1); }else { item = new ItemStack(Material.SKULL_ITEM,1,(short) 3); @@ -53,7 +53,7 @@ public class InventarioCoronacion implements Listener { //Caballo if(Bukkit.getVersion().contains("1.13") || Bukkit.getVersion().contains("1.14") || Bukkit.getVersion().contains("1.15") || Bukkit.getVersion().contains("1.16") || Bukkit.getVersion().contains("1.17") || Bukkit.getVersion().contains("1.18") - || Bukkit.getVersion().contains("1.19")) { + || Bukkit.getVersion().contains("1.19") || Bukkit.getVersion().contains("1.20")) { item = new ItemStack(Material.valueOf("PLAYER_HEAD"),1); }else { item = new ItemStack(Material.SKULL_ITEM,1,(short) 3); @@ -67,7 +67,7 @@ public class InventarioCoronacion implements Listener { //Alfil if(Bukkit.getVersion().contains("1.13") || Bukkit.getVersion().contains("1.14") || Bukkit.getVersion().contains("1.15") || Bukkit.getVersion().contains("1.16") || Bukkit.getVersion().contains("1.17") || Bukkit.getVersion().contains("1.18") - || Bukkit.getVersion().contains("1.19")) { + || Bukkit.getVersion().contains("1.19") || Bukkit.getVersion().contains("1.20")) { item = new ItemStack(Material.valueOf("PLAYER_HEAD"),1); }else { item = new ItemStack(Material.SKULL_ITEM,1,(short) 3); @@ -86,7 +86,7 @@ public class InventarioCoronacion implements Listener { //Reina if(Bukkit.getVersion().contains("1.13") || Bukkit.getVersion().contains("1.14") || Bukkit.getVersion().contains("1.15") || Bukkit.getVersion().contains("1.16") || Bukkit.getVersion().contains("1.17") || Bukkit.getVersion().contains("1.18") - || Bukkit.getVersion().contains("1.19")) { + || Bukkit.getVersion().contains("1.19") || Bukkit.getVersion().contains("1.20")) { item = new ItemStack(Material.valueOf("PLAYER_HEAD"),1); }else { item = new ItemStack(Material.SKULL_ITEM,1,(short) 3); diff --git a/src/mc/ajneb97/otros/Utilidades.java b/src/mc/ajneb97/otros/Utilidades.java index 88e866f..68b3dde 100644 --- a/src/mc/ajneb97/otros/Utilidades.java +++ b/src/mc/ajneb97/otros/Utilidades.java @@ -27,6 +27,7 @@ import mc.ajneb97.versiones.V1_16_R3; import mc.ajneb97.versiones.V1_17; import mc.ajneb97.versiones.V1_18; import mc.ajneb97.versiones.V1_19; +import mc.ajneb97.versiones.V1_20; import mc.ajneb97.versiones.V1_8_R1; import mc.ajneb97.versiones.V1_8_R2; import mc.ajneb97.versiones.V1_8_R3; @@ -71,7 +72,7 @@ public class Utilidades { } meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES,ItemFlag.HIDE_ENCHANTS,ItemFlag.HIDE_UNBREAKABLE,ItemFlag.HIDE_POTION_EFFECTS); if(Bukkit.getVersion().contains("1.15") || Bukkit.getVersion().contains("1.16") || Bukkit.getVersion().contains("1.17") - || Bukkit.getVersion().contains("1.18") || Bukkit.getVersion().contains("1.19")) { + || Bukkit.getVersion().contains("1.18") || Bukkit.getVersion().contains("1.19") || Bukkit.getVersion().contains("1.20")) { meta.setUnbreakable(true); }else { meta.spigot().setUnbreakable(true); //SOLO FUNCIONA CON SPIGOT @@ -83,6 +84,10 @@ public class Utilidades { public static void setSkullBlock(Location l, String id, String textura, int rot) { String packageName = Bukkit.getServer().getClass().getPackage().getName(); + if(packageName.contains("1_20")){ + V1_20 u = new V1_20(); + u.setSkullBlock(l,id,textura,rot); + }else if(packageName.contains("1_19")){ V1_19 u = new V1_19(); u.setSkullBlock(l,id,textura,rot); @@ -159,6 +164,10 @@ public class Utilidades { public static void generarParticula(String particle, Location loc, float xOffset, float yOffset, float zOffset, float speed, int count, Player player) { String packageName = Bukkit.getServer().getClass().getPackage().getName(); + if(packageName.contains("1_20")){ + V1_20 u = new V1_20(); + u.generarParticula(particle, loc, xOffset, yOffset, zOffset, speed, count, player); + } if(packageName.contains("1_19")){ V1_19 u = new V1_19(); u.generarParticula(particle, loc, xOffset, yOffset, zOffset, speed, count, player); @@ -264,6 +273,11 @@ public class Utilidades { public static ItemStack getCabeza(ItemStack item, String id,String textura){ String packageName = Bukkit.getServer().getClass().getPackage().getName(); + if(packageName.contains("1_20")){ + V1_20 u = new V1_20(); + ItemStack stack = u.getCabeza(item,id,textura); + return stack; + } if(packageName.contains("1_19")){ V1_19 u = new V1_19(); ItemStack stack = u.getCabeza(item,id,textura); diff --git a/src/mc/ajneb97/versiones/V1_20.java b/src/mc/ajneb97/versiones/V1_20.java new file mode 100644 index 0000000..7644b16 --- /dev/null +++ b/src/mc/ajneb97/versiones/V1_20.java @@ -0,0 +1,95 @@ +package mc.ajneb97.versiones; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.Color; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Particle; +import org.bukkit.SkullType; +import org.bukkit.block.BlockFace; +import org.bukkit.block.Skull; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.SkullMeta; +import org.bukkit.profile.PlayerProfile; +import org.bukkit.profile.PlayerTextures; + +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.Property; + +public class V1_20 { + + public void generarParticula(String particle, Location loc, float xOffset, float yOffset, float zOffset, float speed, int count, Player player){ + player.spawnParticle(Particle.valueOf(particle),loc,count,xOffset,yOffset,zOffset,speed); + } + + + @SuppressWarnings("deprecation") + public void setSkullBlock(Location locBloque,String id,String textura,int rot) { + locBloque.getBlock().setType(Material.valueOf("PLAYER_HEAD")); + Skull skullBlock = (Skull) locBloque.getBlock().getState(); + //skullBlock.setSkullType(SkullType.PLAYER); + if(rot == 8) { + skullBlock.setRotation(BlockFace.NORTH); + } + + UUID uuid = UUID.randomUUID(); + try { + PlayerProfile profile = (PlayerProfile) Bukkit.class.getMethod("createPlayerProfile", UUID.class).invoke(null,uuid); + PlayerTextures textures = profile.getTextures(); + String decoded = new String(Base64.getDecoder().decode(textura)); + String decodedFormatted = decoded.replaceAll("\\s", ""); + int firstIndex = decodedFormatted.indexOf("\"SKIN\":{\"url\":")+15; + int lastIndex = decodedFormatted.indexOf("}",firstIndex+1); + URL url = new URL(decodedFormatted.substring(firstIndex,lastIndex-1)); + textures.setSkin(url); + profile.setTextures(textures); + skullBlock.getClass().getMethod("setOwnerProfile", PlayerProfile.class).invoke(skullBlock, profile); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException + | SecurityException | MalformedURLException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + skullBlock.update(); + } + + public ItemStack getCabeza(ItemStack item, String id,String textura) { + if (textura.isEmpty()) return item; + + SkullMeta skullMeta = (SkullMeta) item.getItemMeta(); + UUID uuid = UUID.randomUUID(); + try { + PlayerProfile profile = (PlayerProfile) Bukkit.class.getMethod("createPlayerProfile", UUID.class).invoke(null,uuid); + PlayerTextures textures = profile.getTextures(); + String decoded = new String(Base64.getDecoder().decode(textura)); + String decodedFormatted = decoded.replaceAll("\\s", ""); + int firstIndex = decodedFormatted.indexOf("\"SKIN\":{\"url\":")+15; + int lastIndex = decodedFormatted.indexOf("}",firstIndex+1); + URL url = new URL(decodedFormatted.substring(firstIndex,lastIndex-1)); + textures.setSkin(url); + profile.setTextures(textures); + Method method = skullMeta.getClass().getMethod("setOwnerProfile", PlayerProfile.class); + method.setAccessible(true); + method.invoke(skullMeta, profile); + item.setItemMeta(skullMeta); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException + | SecurityException | MalformedURLException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + return item; + } +}