From 12e5227e6285bb875069202a5b2f47624eae758c Mon Sep 17 00:00:00 2001 From: Brent P Date: Mon, 2 Jan 2023 20:48:10 -0500 Subject: [PATCH 01/22] Velocity somewhat working (ALPHA) release --- api-velocity/build.gradle | 19 + .../api/velocity/BungeeTabListPlusAPI.java | 223 ++ .../api/velocity/CustomTablist.java | 181 ++ .../api/velocity/FakePlayerManager.java | 64 + .../bungeetablistplus/api/velocity/Icon.java | 42 + .../api/velocity/ServerVariable.java | 65 + .../api/velocity/Variable.java | 64 + .../api/velocity/tablist/FakePlayer.java | 104 + bootstrap-velocity/build.gradle | 39 + .../bungeetablistplus/BootstrapPlugin.java | 81 + build.gradle | 5 + example/velocity/build.gradle | 5 + .../bungeetablistplus/demo/ColorUtil.java | 77 + .../bungeetablistplus/demo/DemoPlugin.java | 224 ++ .../velocity/src/main/resources/bungee.yml | 5 + example/velocity/src/main/resources/icon.png | Bin 0 -> 228 bytes settings.gradle | 8 + velocity/build.gradle | 42 + .../bungeetablistplus/BTLPContextKeys.java | 27 + .../bungeetablistplus/BungeeTabListPlus.java | 561 ++++ .../bridge/BukkitBridge.java | 709 +++++ .../bridge/NetDataKeyIdMap.java | 47 + .../bungeetablistplus/cache/Cache.java | 109 + .../command/CommandBungeeTabListPlus.java | 169 ++ .../command/CommandDebug.java | 122 + .../command/CommandFakePlayers.java | 117 + .../command/CommandHide.java | 97 + .../compat/SortingRuleAliasProcessor.java | 74 + .../bungeetablistplus/config/Comment.java | 30 + .../bungeetablistplus/config/MainConfig.java | 203 ++ .../bungeetablistplus/config/Path.java | 30 + ...PlayersByServerComponentConfiguration.java | 298 ++ .../data/AbstractCompositeDataProvider.java | 66 + .../bungeetablistplus/data/BTLPDataTypes.java | 59 + .../data/BTLPVelocityDataKeys.java | 63 + .../data/NullDataHolder.java | 45 + .../data/PermissionDataProvider.java | 32 + .../data/ServerDataHolder.java | 67 + .../data/TrackingDataCache.java | 54 + .../AbstractLegacyTabOverlayHandler.java | 850 ++++++ .../handler/AbstractTabOverlayHandler.java | 2562 +++++++++++++++++ .../handler/GetGamemodeLogic.java | 75 + .../handler/LegacyTabOverlayHandlerImpl.java | 44 + .../LowMemoryTabOverlayHandlerImpl.java | 56 + .../handler/NewTabOverlayHandler.java | 1241 ++++++++ .../handler/OperationModeHandler.java | 32 + .../handler/RewriteLogic.java | 162 ++ .../handler/TabOverlayHandlerImpl.java | 77 + .../listener/TabListListener.java | 87 + .../bungeetablistplus/managers/API.java | 194 ++ .../managers/BungeePlayerProvider.java | 108 + .../managers/DataManager.java | 200 ++ .../managers/HiddenPlayersManager.java | 95 + .../managers/RedisPlayerManager.java | 302 ++ .../managers/ServerStateManager.java | 138 + .../managers/TabViewManager.java | 157 + .../ComponentServerPlaceholderResolver.java | 95 + .../GlobalServerPlaceholderResolver.java | 50 + .../PlayerPlaceholderResolver.java | 278 ++ .../ServerCountPlaceholderResolver.java | 56 + .../ServerPlaceholderResolver.java | 87 + .../player/AbstractPlayer.java | 175 ++ .../bungeetablistplus/player/FakePlayer.java | 133 + .../player/FakePlayerManagerImpl.java | 206 ++ .../bungeetablistplus/player/RedisPlayer.java | 62 + .../player/VelocityPlayer.java | 96 + .../protocol/AbstractPacketHandler.java | 69 + .../protocol/PacketHandler.java | 39 + .../protocol/PacketListener.java | 95 + .../protocol/PacketListenerResult.java | 22 + .../protocol/PacketWrapper.java | 26 + .../tablist/AbstractCustomTablist.java | 218 ++ .../tablist/DefaultCustomTablist.java | 146 + .../ExcludedServersTabOverlayProvider.java | 99 + .../PlayersByServerComponentTemplate.java | 111 + .../updater/UpdateChecker.java | 166 ++ .../updater/UpdateNotifier.java | 78 + .../bungeetablistplus/util/BitSet.java | 164 ++ .../bungeetablistplus/util/ChatUtil.java | 392 +++ .../bungeetablistplus/util/ColorParser.java | 113 + .../util/ConcurrentBitSet.java | 65 + .../util/ContextAwareOrdering.java | 81 + .../util/EmptyOrderedPlayerSet.java | 46 + .../util/EmptyPlayerSet.java | 64 + .../util/ExceptionHandlingEventExecutor.java | 50 + .../bungeetablistplus/util/Functions.java | 30 + .../bungeetablistplus/util/GeyserCompat.java | 51 + .../bungeetablistplus/util/IconUtil.java | 65 + .../util/IntToIntFunction.java | 24 + .../bungeetablistplus/util/MapFunction.java | 38 + .../util/MatchingStringsCollection.java | 70 + .../util/Object2IntHashMultimap.java | 57 + .../util/Property119Handler.java | 27 + .../bungeetablistplus/util/ProxyServer.java | 14 + .../util/ReflectionUtil.java | 73 + .../util/VelocityPlugin.java | 42 + .../version/ProtocolVersionProvider.java | 36 + .../VelocityProtocolVersionProvider.java | 58 + .../ViaVersionProtocolVersionProvider.java | 59 + .../view/PlayersByServerComponentView.java | 276 ++ velocity/src/main/resources/default.yml | 93 + velocity/src/main/resources/heads/cache.txt | 22 + .../src/main/resources/heads/colors/aqua.png | Bin 0 -> 118 bytes .../src/main/resources/heads/colors/black.png | Bin 0 -> 109 bytes .../src/main/resources/heads/colors/blue.png | Bin 0 -> 118 bytes .../main/resources/heads/colors/dark_aqua.png | Bin 0 -> 118 bytes .../main/resources/heads/colors/dark_blue.png | Bin 0 -> 118 bytes .../main/resources/heads/colors/dark_gray.png | Bin 0 -> 118 bytes .../resources/heads/colors/dark_green.png | Bin 0 -> 116 bytes .../resources/heads/colors/dark_purple.png | Bin 0 -> 118 bytes .../main/resources/heads/colors/dark_red.png | Bin 0 -> 114 bytes .../src/main/resources/heads/colors/gold.png | Bin 0 -> 116 bytes .../src/main/resources/heads/colors/gray.png | Bin 0 -> 118 bytes .../src/main/resources/heads/colors/green.png | Bin 0 -> 118 bytes .../resources/heads/colors/light_purple.png | Bin 0 -> 118 bytes .../src/main/resources/heads/colors/red.png | Bin 0 -> 118 bytes .../src/main/resources/heads/colors/white.png | Bin 0 -> 118 bytes .../main/resources/heads/colors/yellow.png | Bin 0 -> 118 bytes .../main/resources/heads/default/balance.png | Bin 0 -> 293 bytes .../main/resources/heads/default/clock.png | Bin 0 -> 224 bytes .../src/main/resources/heads/default/ping.png | Bin 0 -> 138 bytes .../main/resources/heads/default/players.png | Bin 0 -> 163 bytes .../src/main/resources/heads/default/rank.png | Bin 0 -> 228 bytes .../main/resources/heads/default/server.png | Bin 0 -> 257 bytes .../src/main/resources/version.properties | 1 + 125 files changed, 14925 insertions(+) create mode 100644 api-velocity/build.gradle create mode 100644 api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/BungeeTabListPlusAPI.java create mode 100644 api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/CustomTablist.java create mode 100644 api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/FakePlayerManager.java create mode 100644 api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/Icon.java create mode 100644 api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/ServerVariable.java create mode 100644 api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/Variable.java create mode 100644 api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/tablist/FakePlayer.java create mode 100644 bootstrap-velocity/build.gradle create mode 100644 bootstrap-velocity/src/main/java/codecrafter47/bungeetablistplus/BootstrapPlugin.java create mode 100644 example/velocity/build.gradle create mode 100644 example/velocity/src/main/java/de/codecrafter47/bungeetablistplus/demo/ColorUtil.java create mode 100644 example/velocity/src/main/java/de/codecrafter47/bungeetablistplus/demo/DemoPlugin.java create mode 100644 example/velocity/src/main/resources/bungee.yml create mode 100644 example/velocity/src/main/resources/icon.png create mode 100644 velocity/build.gradle create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/BTLPContextKeys.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/bridge/BukkitBridge.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/bridge/NetDataKeyIdMap.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/cache/Cache.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandBungeeTabListPlus.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandDebug.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandFakePlayers.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandHide.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/compat/SortingRuleAliasProcessor.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/config/Comment.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/config/MainConfig.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/config/Path.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/config/PlayersByServerComponentConfiguration.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/data/AbstractCompositeDataProvider.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/data/BTLPDataTypes.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/data/BTLPVelocityDataKeys.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/data/NullDataHolder.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/data/PermissionDataProvider.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/data/ServerDataHolder.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/data/TrackingDataCache.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractLegacyTabOverlayHandler.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/handler/GetGamemodeLogic.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LegacyTabOverlayHandlerImpl.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LowMemoryTabOverlayHandlerImpl.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/handler/OperationModeHandler.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/handler/RewriteLogic.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/handler/TabOverlayHandlerImpl.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/listener/TabListListener.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/managers/API.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/managers/BungeePlayerProvider.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/managers/DataManager.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/managers/HiddenPlayersManager.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/managers/RedisPlayerManager.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/managers/ServerStateManager.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/managers/TabViewManager.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/ComponentServerPlaceholderResolver.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/GlobalServerPlaceholderResolver.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/PlayerPlaceholderResolver.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/ServerCountPlaceholderResolver.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/ServerPlaceholderResolver.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/player/AbstractPlayer.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/player/FakePlayer.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/player/FakePlayerManagerImpl.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/player/RedisPlayer.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/player/VelocityPlayer.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/AbstractPacketHandler.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketHandler.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListener.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListenerResult.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketWrapper.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/tablist/AbstractCustomTablist.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/tablist/DefaultCustomTablist.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/tablist/ExcludedServersTabOverlayProvider.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/template/PlayersByServerComponentTemplate.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/updater/UpdateChecker.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/updater/UpdateNotifier.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/util/BitSet.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/util/ChatUtil.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/util/ColorParser.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/util/ConcurrentBitSet.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/util/ContextAwareOrdering.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/util/EmptyOrderedPlayerSet.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/util/EmptyPlayerSet.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/util/ExceptionHandlingEventExecutor.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/util/Functions.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/util/GeyserCompat.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/util/IconUtil.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/util/IntToIntFunction.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/util/MapFunction.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/util/MatchingStringsCollection.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/util/Object2IntHashMultimap.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/util/Property119Handler.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/util/ProxyServer.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/util/ReflectionUtil.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/util/VelocityPlugin.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/version/ProtocolVersionProvider.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/version/VelocityProtocolVersionProvider.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/version/ViaVersionProtocolVersionProvider.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/view/PlayersByServerComponentView.java create mode 100644 velocity/src/main/resources/default.yml create mode 100644 velocity/src/main/resources/heads/cache.txt create mode 100644 velocity/src/main/resources/heads/colors/aqua.png create mode 100644 velocity/src/main/resources/heads/colors/black.png create mode 100644 velocity/src/main/resources/heads/colors/blue.png create mode 100644 velocity/src/main/resources/heads/colors/dark_aqua.png create mode 100644 velocity/src/main/resources/heads/colors/dark_blue.png create mode 100644 velocity/src/main/resources/heads/colors/dark_gray.png create mode 100644 velocity/src/main/resources/heads/colors/dark_green.png create mode 100644 velocity/src/main/resources/heads/colors/dark_purple.png create mode 100644 velocity/src/main/resources/heads/colors/dark_red.png create mode 100644 velocity/src/main/resources/heads/colors/gold.png create mode 100644 velocity/src/main/resources/heads/colors/gray.png create mode 100644 velocity/src/main/resources/heads/colors/green.png create mode 100644 velocity/src/main/resources/heads/colors/light_purple.png create mode 100644 velocity/src/main/resources/heads/colors/red.png create mode 100644 velocity/src/main/resources/heads/colors/white.png create mode 100644 velocity/src/main/resources/heads/colors/yellow.png create mode 100644 velocity/src/main/resources/heads/default/balance.png create mode 100644 velocity/src/main/resources/heads/default/clock.png create mode 100644 velocity/src/main/resources/heads/default/ping.png create mode 100644 velocity/src/main/resources/heads/default/players.png create mode 100644 velocity/src/main/resources/heads/default/rank.png create mode 100644 velocity/src/main/resources/heads/default/server.png create mode 100644 velocity/src/main/resources/version.properties diff --git a/api-velocity/build.gradle b/api-velocity/build.gradle new file mode 100644 index 00000000..df9bf521 --- /dev/null +++ b/api-velocity/build.gradle @@ -0,0 +1,19 @@ + +dependencies { + compileOnly "com.velocitypowered:velocity-api:${rootProject.ext.velocityVersion}" + annotationProcessor "com.velocitypowered:velocity-api:${rootProject.ext.velocityVersion}" + api "de.codecrafter47.taboverlay:taboverlaycommon-api:1.0-SNAPSHOT" +} + +java { + withJavadocJar() + withSourcesJar() +} + +publishing { + publications { + maven(MavenPublication) { + from(components.java) + } + } +} diff --git a/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/BungeeTabListPlusAPI.java b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/BungeeTabListPlusAPI.java new file mode 100644 index 00000000..563cffe2 --- /dev/null +++ b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/BungeeTabListPlusAPI.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.api.velocity; + +import com.google.common.base.Preconditions; +import com.velocitypowered.api.proxy.Player; +import de.codecrafter47.taboverlay.TabView; + +import javax.annotation.Nonnull; +import java.awt.image.BufferedImage; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +public abstract class BungeeTabListPlusAPI { + private static BungeeTabListPlusAPI instance = null; + + /** + * Registers a custom variable + *

+ * You cannot use this to replace existing variables. If registering a variable which already + * exists there may be an exception thrown but there is no guarantee that an exception + * is thrown in that case. + * + * @param plugin your plugin + * @param variable your variable + */ + public static void registerVariable(Object plugin, Variable variable) { + Preconditions.checkState(instance != null, "instance is null, is the plugin enabled?"); + instance.registerVariable0(plugin, variable); + } + + protected abstract void registerVariable0(Object plugin, Variable variable); + + /** + * Registers a custom variable bound to a specific server + *

+ * You cannot use this to replace existing variables. If registering a variable which already + * exists there may be an exception thrown but there is no guarantee that an exception + * is thrown in that case. + * + * @param plugin your plugin + * @param variable your variable + */ + public static void registerVariable(Object plugin, ServerVariable variable) { + Preconditions.checkState(instance != null, "instance is null, is the plugin enabled?"); + instance.registerVariable0(plugin, variable); + } + + protected abstract void registerVariable0(Object plugin, ServerVariable variable); + + /** + * Create a new {@link CustomTablist} + * + * @return thte created {@link CustomTablist} + * @deprecated The custom tab list api has been changed. See {@link #getTabViewForPlayer(Player)} + */ + @Deprecated + public static CustomTablist createCustomTablist() { + Preconditions.checkState(instance != null, "BungeeTabListPlus not initialized"); + return instance.createCustomTablist0(); + } + + @SuppressWarnings("deprecation") + protected abstract CustomTablist createCustomTablist0(); + + /** + * Set a custom tab list for a player + * + * @param player the player + * @param customTablist the CustomTablist to use + * @deprecated The custom tab list api has been changed. See {@link #getTabViewForPlayer(Player)} + */ + @Deprecated + public static void setCustomTabList(Player player, CustomTablist customTablist) { + Preconditions.checkState(instance != null, "BungeeTabListPlus not initialized"); + instance.setCustomTabList0(player, customTablist); + } + + @SuppressWarnings("deprecation") + protected abstract void setCustomTabList0(Player player, CustomTablist customTablist); + + /** + * Get the face part of the players skin as an icon for use in the tab list. + * + * @param player the player + * @return the icon + * @deprecated Use {@link #getPlayerIcon(Player)} + */ + @Deprecated + @Nonnull + public static Icon getIconFromPlayer(Player player) { + Preconditions.checkState(instance != null, "BungeeTabListPlus not initialized"); + return instance.getIconFromPlayer0(player); + } + + @Nonnull + protected abstract Icon getIconFromPlayer0(Player player); + + /** + * Get the face part of the players skin as an icon for use in the tab list. + * + * @param player the player + * @return the icon + */ + @Nonnull + public static de.codecrafter47.taboverlay.Icon getPlayerIcon(Player player) { + Preconditions.checkState(instance != null, "BungeeTabListPlus not initialized"); + return instance.getPlayerIcon0(player); + } + + @Nonnull + protected abstract de.codecrafter47.taboverlay.Icon getPlayerIcon0(Player player); + + /** + * Creates an icon from an 8x8 px image. The creation of the icon can take several + * minutes. When the icon has been created the callback is invoked. + * + * @param image the image + * @param callback called when the icon is ready + * @deprecated use {@link #getIconFromImage(BufferedImage)} + */ + @Deprecated + public static void createIcon(BufferedImage image, Consumer callback) { + Preconditions.checkState(instance != null, "BungeeTabListPlus not initialized"); + instance.createIcon0(image, callback); + } + + protected abstract void createIcon0(BufferedImage image, Consumer callback); + + /** + * Creates an icon from an 8x8 px image. The creation of the icon can take several + * minutes. When the icon has been created the callback is invoked. + * + * @param image the image + * @return a completable future providing the icon is ready + */ + public static CompletableFuture getIconFromImage(BufferedImage image) { + Preconditions.checkState(instance != null, "BungeeTabListPlus not initialized"); + return instance.getIconFromImage0(image); + } + + protected abstract CompletableFuture getIconFromImage0(BufferedImage image); + + /** + * Removes a custom tab list from a player. + * If the player hasn't got a custom tab list associated with it this will do nothing. + * + * @param player the player + * @deprecated The custom tab list api has been changed. See {@link #getTabViewForPlayer(Player)} + */ + @Deprecated + public static void removeCustomTabList(Player player) { + Preconditions.checkState(instance != null, "BungeeTabListPlus not initialized"); + instance.removeCustomTabList0(player); + } + + protected abstract void removeCustomTabList0(Player player); + + /** + * Get the tab view of a player. The tab view object allows registering and unregistering custom tab overlay + * handlers. + * + * @param player the player + * @return tab view of that player + * @throws IllegalStateException is the player is not found + * @see TabView + * @see de.codecrafter47.taboverlay.TabOverlayProviderSet + * @see de.codecrafter47.taboverlay.TabOverlayProvider + * @see de.codecrafter47.taboverlay.AbstractPlayerTabOverlayProvider + */ + public static TabView getTabViewForPlayer(Player player) { + Preconditions.checkState(instance != null, "BungeeTabListPlus not initialized"); + return instance.getTabViewForPlayer0(player); + } + + protected abstract TabView getTabViewForPlayer0(Player player); + + /** + * Get the FakePlayerManager instance + * + * @return the FakePlayerManager instance + */ + public static FakePlayerManager getFakePlayerManager() { + Preconditions.checkState(instance != null, "BungeeTabListPlus not initialized"); + return instance.getFakePlayerManager0(); + } + + protected abstract FakePlayerManager getFakePlayerManager0(); + + /** + * Check if a player is hidden from the tab list. + *

+ * A player is regarded as hidden if one of the following conditions is true: + * - The player is hidden using a vanish plugin(e.g. SuperVanish, Essentials, ...) + * - The player has been hidden using the /btlp hide command + * - The player is in the list of hidden players in the configuration + * - The player is on one of the hidden servers(configuration) + * + * @param player the player + * @return true if hidden, false otherwise + */ + public static boolean isHidden(Player player) { + Preconditions.checkState(instance != null, "BungeeTabListPlus not initialized"); + return instance.isHidden0(player); + } + + protected abstract boolean isHidden0(Player player); +} diff --git a/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/CustomTablist.java b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/CustomTablist.java new file mode 100644 index 00000000..b1621b0c --- /dev/null +++ b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/CustomTablist.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.api.velocity; + +import com.velocitypowered.api.proxy.Player; + +import lombok.NonNull; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * @deprecated The custom tab list api has been changed. See {@link BungeeTabListPlusAPI#getTabViewForPlayer(Player)} + */ +@Deprecated +public interface CustomTablist { + /** + * Set the size of the tab list. + *

+ * Recommended values: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
SizeColumnsRows
000
10110
20120
30215
40220
60320
80420
+ * + * @param size new size of the tab list + * @throws IllegalArgumentException if the given size is not allowed + */ + void setSize(int size); + + /** + * Get the size of the tab list + * This is the same as getRows() * getColumns() + * + * @return the size of the tablist + */ + int getSize(); + + /** + * Get the number of rows in the tab list + * + * @return the number of rows + */ + int getRows(); + + /** + * Get the number of columns in the tab list + * + * @return the number of columns + */ + int getColumns(); + + /** + * Get the icon of the slot at the position specified by row and column + * + * @param row the row + * @param column the column + * @return the icon at the given position + */ + @Nonnull + Icon getIcon(int row, int column); + + /** + * Get the text of the slot at the position specified by row and column + * + * @param row the row + * @param column the column + * @return the text at the given position + */ + @Nonnull + String getText(int row, int column); + + /** + * Get the ping of the slot at the position specified by row and column + * + * @param row the row + * @param column the column + * @return the ping at the given position + */ + int getPing(int row, int column); + + /** + * Set the slot at a position specified by row and column + * + * @param row the row + * @param column the column + * @param icon the icon + * @param text the text + * @param ping the ping + */ + void setSlot(int row, int column, @Nonnull @NonNull Icon icon, @Nonnull @NonNull String text, int ping); + + /** + * Get the header set for the tab list + * + * @return the header + */ + @Nullable + String getHeader(); + + /** + * Set the header for the tab list + * may contain color codes like &6 + * + * @param header the header + */ + void setHeader(@Nullable String header); + + /** + * Get the footer for the tab list + * + * @return the footer + */ + @Nullable + String getFooter(); + + /** + * Set the footer + * + * @param footer the footer + */ + void setFooter(@Nullable String footer); +} diff --git a/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/FakePlayerManager.java b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/FakePlayerManager.java new file mode 100644 index 00000000..78f18631 --- /dev/null +++ b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/FakePlayerManager.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.api.velocity; + +import codecrafter47.bungeetablistplus.api.velocity.tablist.FakePlayer; +import com.velocitypowered.api.proxy.server.ServerInfo; + +import java.util.Collection; + +/** + * Controls fake players + */ +public interface FakePlayerManager { + + /** + * Get all fake players which are currently displayed on the tab list + * + * @return collection of all fake players + */ + Collection getOnlineFakePlayers(); + + /** + * @return whether the plugin will randomly add fake players it finds in the config, and randomly removes fake players + */ + boolean isRandomJoinLeaveEnabled(); + + /** + * set whether the plugin should randomly add fake players it finds in the config, and randomly removes fake players + * + * @param value whether random join/leave events for fake players should be enabled + */ + void setRandomJoinLeaveEnabled(boolean value); + + /** + * Creates a fake player which is immediately visible on the tab list + * + * @param name name of the fake player + * @param server server of the fake player + * @return the fake player + */ + FakePlayer createFakePlayer(String name, ServerInfo server); + + /** + * remove a fake player + * + * @param fakePlayer the fake player to be removed + */ + void removeFakePlayer(FakePlayer fakePlayer); +} diff --git a/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/Icon.java b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/Icon.java new file mode 100644 index 00000000..af9cc58c --- /dev/null +++ b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/Icon.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.api.velocity; + +import lombok.Data; +import lombok.NonNull; + +import java.io.Serializable; +import java.util.UUID; + +/** + * An icon shown in the tab list. + */ +@Data +public class Icon implements Serializable { + + private static final long serialVersionUID = 1L; + + private final UUID player; + @NonNull + private final String[][] properties; + + /** + * The default icon. The client will show a random Alex/ Steve face when using this. + */ + public static final Icon DEFAULT = new Icon(null, new String[0][]); +} diff --git a/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/ServerVariable.java b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/ServerVariable.java new file mode 100644 index 00000000..9eb13a7b --- /dev/null +++ b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/ServerVariable.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.api.velocity; + +/** + * Base class for creating custom Variables bound to a server. + *

+ * To create a custom (per server) Variable you need to create a subclass of this class + * and register an instance of it with {@link BungeeTabListPlusAPI#registerVariable} + *

+ * After registration the variable can be used in the config file in several ways: + * Use {@code ${viewer server }} to resolve the variable for the server of the + * player looking at the tab list. + * Use {@code ${player server }} to resolve the variable for the server of a + * player displayed on the tab list, this one can only be used inside the playerComponent. + * Use {@code ${server }} to resolve the variable for a particular server inside the + * serverHeader option of the players by server component. + * Use {@code ${server: }} to resolve the variable for a specific server. + */ +public abstract class ServerVariable { + private final String name; + + /** + * invoked by the subclass to set the name of the variable + * + * @param name name of the variable without { } + */ + public ServerVariable(String name) { + this.name = name; + } + + /** + * This method is periodically invoked by BungeeTabListPlus to check whether the replacement for the variable changed. + *

+ * The implementation is expected to be thread safe. + * + * @param serverName name of the server for which the variable should be replaced + * @return the replacement for the variable + */ + public abstract String getReplacement(String serverName); + + /** + * Getter for the variable name. + * + * @return the name of the variable + */ + public final String getName() { + return name; + } +} diff --git a/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/Variable.java b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/Variable.java new file mode 100644 index 00000000..fc0a3f1a --- /dev/null +++ b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/Variable.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.api.velocity; + +import com.velocitypowered.api.proxy.Player; + +/** + * Base class for creating custom Variables. + *

+ * To create a custom Variable you need to create a subclass of this class + * and register an instance of it with {@link BungeeTabListPlusAPI#registerVariable} + *

+ * After registration the variable can be used in the config file in several ways: + * Use {@code ${viewer }} to resolve the variable for the + * player looking at the tab list. + * Use {@code ${player }} to resolve the variable for a player displayed on + * the tab list, this one can only be used inside the playerComponent. + */ +public abstract class Variable { + private final String name; + + /** + * invoked by the subclass to set the name of the variable + * + * @param name name of the variable without { } + */ + public Variable(String name) { + this.name = name; + } + + /** + * This method is periodically invoked by BungeeTabListPlus to check whether the replacement for the variable changed. + *

+ * The implementation is expected to be thread safe. + * + * @param player the player for which the variable should be replaced + * @return the replacement for the variable + */ + public abstract String getReplacement(Player player); + + /** + * Getter for the variable name. + * + * @return the name of the variable + */ + public final String getName() { + return name; + } +} diff --git a/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/tablist/FakePlayer.java b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/tablist/FakePlayer.java new file mode 100644 index 00000000..5b8aca81 --- /dev/null +++ b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/tablist/FakePlayer.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.api.velocity.tablist; + +import codecrafter47.bungeetablistplus.api.velocity.Icon; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import com.velocitypowered.api.proxy.server.ServerInfo; + +import java.util.Optional; +import java.util.UUID; + +/** + * Represents a fake player on the tab list. + */ +public interface FakePlayer { + /** + * get the username of the player + * + * @return the username + */ + String getName(); + + /** + * get the uuid of the player + * + * @return the uuid of the player + */ + UUID getUniqueID(); + + /** + * get the server the player is connected to + * + * @return the server the player is connected to + */ + Optional getServer(); + + /** + * get the ping of the player + * + * @return the ping + */ + int getPing(); + + /** + * get the icon displayed on the tab list + * + * @return the icon + */ + Icon getIcon(); + + /** + * Change the server + * + * @param newServer new server + */ + void changeServer(ServerInfo newServer); + + /** + * Set the icon displayed on the tab list. + * + * @param icon the icon + * @deprecated use {@link #setIcon(de.codecrafter47.taboverlay.Icon)} + */ + void setIcon(Icon icon); + + /** + * Set the icon displayed on the tab list. + * + * @param icon the icon + */ + void setIcon(de.codecrafter47.taboverlay.Icon icon); + + /** + * Set the ping of the fake player. + * + * @param ping the ping + */ + void setPing(int ping); + + /** + * @return whether the fake player will randomly switch servers + */ + boolean isRandomServerSwitchEnabled(); + + /** + * @param value whether the fake player will randomly switch servers + */ + void setRandomServerSwitchEnabled(boolean value); +} diff --git a/bootstrap-velocity/build.gradle b/bootstrap-velocity/build.gradle new file mode 100644 index 00000000..3dbca21e --- /dev/null +++ b/bootstrap-velocity/build.gradle @@ -0,0 +1,39 @@ +import org.apache.tools.ant.filters.ReplaceTokens + +plugins { + id "org.jetbrains.gradle.plugin.idea-ext" version "1.0.1" + id "com.github.johnrengelman.shadow" version "5.2.0" +} + +dependencies { + implementation project(':velocity-plugin') + compileOnly "com.velocitypowered:velocity-api:${rootProject.ext.velocityVersion}" + annotationProcessor "com.velocitypowered:velocity-api:${rootProject.ext.velocityVersion}" + implementation "org.bstats:bstats-velocity:3.0.0" +} + +compileJava { + sourceCompatibility = '1.8' + targetCompatibility = '1.8' +} + +task processSource(type: Sync) { + from sourceSets.main.java + inputs.property 'version', version + filter(ReplaceTokens, tokens: [VERSION: version]) + into "$buildDir/src" +} + +compileJava { + source = processSource.outputs +} + +shadowJar { + relocate 'codecrafter47.util', 'codecrafter47.bungeetablistplus.util' + relocate 'org.bstats', 'codecrafter47.bungeetablistplus.libs.bstats' + relocate 'it.unimi.dsi.fastutil', 'codecrafter47.bungeetablistplus.libs.fastutil' + relocate 'org.yaml.snakeyaml', 'codecrafter47.bungeetablistplus.libs.snakeyaml' + relocate 'org.mineskin', 'codecrafter47.bungeetablistplus.libs.mineskin' + archiveClassifier.set(null) + minimize() +} \ No newline at end of file diff --git a/bootstrap-velocity/src/main/java/codecrafter47/bungeetablistplus/BootstrapPlugin.java b/bootstrap-velocity/src/main/java/codecrafter47/bungeetablistplus/BootstrapPlugin.java new file mode 100644 index 00000000..8f244b06 --- /dev/null +++ b/bootstrap-velocity/src/main/java/codecrafter47/bungeetablistplus/BootstrapPlugin.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus; + +import codecrafter47.bungeetablistplus.util.VelocityPlugin; +import com.google.inject.Inject; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; +import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; +import com.velocitypowered.api.plugin.Dependency; +import com.velocitypowered.api.plugin.Plugin; +import com.velocitypowered.api.plugin.annotation.DataDirectory; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ProxyServer; +import net.kyori.adventure.text.Component; +import org.bstats.velocity.Metrics; +import org.slf4j.Logger; + +import java.nio.file.Path; + +@Plugin( + id = "bungeetablistplus", + name = "BungeeTabListPlus", + version = "@VERSION@", + dependencies = { + @Dependency(id = "RedisBungee", optional = true), + @Dependency(id = "LuckPerms", optional = true), + @Dependency(id = "Geyser", optional = true), + @Dependency(id = "Floodgate", optional = true) + } +) +public class BootstrapPlugin extends VelocityPlugin { + + private final Metrics.Factory metricsFactory; + + @Inject + public BootstrapPlugin(final ProxyServer server, final Logger logger, final @DataDirectory Path dataDirectory, final Metrics.Factory metricsFactory) { + super(server, logger, dataDirectory, BootstrapPlugin.class.getAnnotation(Plugin.class).version()); + this.metricsFactory = metricsFactory; + } + + @Subscribe + public void onProxyInitialization(final ProxyInitializeEvent event) { + if (Float.parseFloat(System.getProperty("java.class.version")) < 52.0) { + getLogger().error("§cBungeeTabListPlus requires Java 8 or above. Please download and install it!"); + getLogger().error("Disabling plugin!"); + return; + } + if (!getProxy().getAllPlayers().isEmpty()) { + for (Player player : getProxy().getAllPlayers()) { + player.disconnect(Component.text("Cannot reload BungeeTabListPlus while players are online.")); + } + } + getProxy().getPluginManager().getPlugin("BungeeTabListPlus"); + BungeeTabListPlus.getInstance(this).onLoad(); + BungeeTabListPlus.getInstance(this).onEnable(); + // Metrics + metricsFactory.make(this, 4332); + } + + @Subscribe + public void onProxyShutdown(final ProxyShutdownEvent event) { + BungeeTabListPlus.getInstance().onDisable(); + + } +} diff --git a/build.gradle b/build.gradle index ad3943b4..62718c10 100644 --- a/build.gradle +++ b/build.gradle @@ -11,6 +11,7 @@ buildscript { ext { spigotVersion = '1.11-R0.1-SNAPSHOT' bungeeVersion = '1.19-R0.1-SNAPSHOT' + velocityVersion = '3.1.2-SNAPSHOT' spongeVersion = '7.0.0' dataApiVersion = '1.0.2-SNAPSHOT' } @@ -47,6 +48,10 @@ subprojects { url = 'https://papermc.io/repo/repository/maven-snapshots/' } + maven { + url = 'https://repo.papermc.io/repository/maven-public/' + } + maven { url = 'https://repo.spongepowered.org/maven' } diff --git a/example/velocity/build.gradle b/example/velocity/build.gradle new file mode 100644 index 00000000..38597948 --- /dev/null +++ b/example/velocity/build.gradle @@ -0,0 +1,5 @@ + +dependencies { + compileOnly project(':bungeetablistplus-api-bungee') + compileOnly "net.md-5:bungeecord-api:${rootProject.ext.bungeeVersion}" +} diff --git a/example/velocity/src/main/java/de/codecrafter47/bungeetablistplus/demo/ColorUtil.java b/example/velocity/src/main/java/de/codecrafter47/bungeetablistplus/demo/ColorUtil.java new file mode 100644 index 00000000..6d32596a --- /dev/null +++ b/example/velocity/src/main/java/de/codecrafter47/bungeetablistplus/demo/ColorUtil.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.codecrafter47.bungeetablistplus.demo; + +import com.google.common.collect.BiMap; +import com.google.common.collect.ImmutableBiMap; +import net.md_5.bungee.api.ChatColor; + +import java.awt.*; + +public class ColorUtil { + private static BiMap colors = ImmutableBiMap.builder() + .put(new Color(0x000000), ChatColor.BLACK) + .put(new Color(0x0000aa), ChatColor.DARK_BLUE) + .put(new Color(0x00aa00), ChatColor.DARK_GREEN) + .put(new Color(0x00aaaa), ChatColor.DARK_AQUA) + .put(new Color(0xaa0000), ChatColor.DARK_RED) + .put(new Color(0xaa00aa), ChatColor.DARK_PURPLE) + .put(new Color(0xffaa00), ChatColor.GOLD) + .put(new Color(0xaaaaaa), ChatColor.GRAY) + .put(new Color(0x555555), ChatColor.DARK_GRAY) + .put(new Color(0x5555ff), ChatColor.BLUE) + .put(new Color(0x55ff55), ChatColor.GREEN) + .put(new Color(0x55ffff), ChatColor.AQUA) + .put(new Color(0xff5555), ChatColor.RED) + .put(new Color(0xff55ff), ChatColor.LIGHT_PURPLE) + .put(new Color(0xffff55), ChatColor.YELLOW) + .put(new Color(0xffffff), ChatColor.WHITE) + .build(); + + public static Color getAWTColor(ChatColor chatColor) { + return colors.inverse().get(chatColor); + } + + public static ChatColor getSimilarChatColor(Color color) { + if (color.getAlpha() < 128) { + return null; + } + Color bestColor = null; + double bestDist = Double.POSITIVE_INFINITY; + for (Color c : colors.keySet()) { + double dist; + if ((dist = ColourDistance(c, color)) < bestDist) { + bestColor = c; + bestDist = dist; + } + } + return colors.get(bestColor); + } + + // from http://stackoverflow.com/questions/2103368/color-logic-algorithm + public static double ColourDistance(Color c1, Color c2) { + double rmean = (c1.getRed() + c2.getRed()) / 2; + int r = c1.getRed() - c2.getRed(); + int g = c1.getGreen() - c2.getGreen(); + int b = c1.getBlue() - c2.getBlue(); + double weightR = 2 + rmean / 256; + double weightG = 4.0; + double weightB = 2 + (255 - rmean) / 256; + return Math.sqrt(weightR * r * r + weightG * g * g + weightB * b * b); + } +} diff --git a/example/velocity/src/main/java/de/codecrafter47/bungeetablistplus/demo/DemoPlugin.java b/example/velocity/src/main/java/de/codecrafter47/bungeetablistplus/demo/DemoPlugin.java new file mode 100644 index 00000000..8ff44326 --- /dev/null +++ b/example/velocity/src/main/java/de/codecrafter47/bungeetablistplus/demo/DemoPlugin.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.codecrafter47.bungeetablistplus.demo; + +import codecrafter47.bungeetablistplus.api.bungee.BungeeTabListPlusAPI; +import codecrafter47.bungeetablistplus.api.bungee.Variable; +import de.codecrafter47.taboverlay.AbstractPlayerTabOverlayProvider; +import de.codecrafter47.taboverlay.Icon; +import de.codecrafter47.taboverlay.TabView; +import de.codecrafter47.taboverlay.handler.*; +import lombok.NonNull; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.plugin.Command; +import net.md_5.bungee.api.plugin.Plugin; +import net.md_5.bungee.api.scheduler.ScheduledTask; + +import javax.annotation.Nonnull; +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.Calendar; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; + +import static de.codecrafter47.bungeetablistplus.demo.ColorUtil.getAWTColor; +import static de.codecrafter47.bungeetablistplus.demo.ColorUtil.getSimilarChatColor; +import static java.lang.Math.*; + +public class DemoPlugin extends Plugin { + + private CompletableFuture customIcon; + + @Override + public void onLoad() { + + // variables should be registered in onLoad to avoid warnings when BTLP loads your config files + BungeeTabListPlusAPI.registerVariable(this, new Variable("uppercase_name") { + @Override + public String getReplacement(ProxiedPlayer player) { + return player.getName().toUpperCase(); + } + }); + + // Create our icon. + try { + // read the image file + BufferedImage image = ImageIO.read(getResourceAsStream("icon.png")); + // call getIconFromImage, this gives use a future that will hold the icon once completed + customIcon = BungeeTabListPlusAPI.getIconFromImage(image); + } catch (IOException ex) { + getLogger().log(Level.SEVERE, "Failed to load icon.", ex); + } + } + + @Override + public void onEnable() { + + // register the /tabdemo command. + // It will display the custom tab list to players + getProxy().getPluginManager().registerCommand(this, new Command("tabdemo") { + @Override + public void execute(CommandSender sender, String[] args) { + if (sender instanceof ProxiedPlayer) { + // get the tab view for the player + TabView tabView = BungeeTabListPlusAPI.getTabViewForPlayer((ProxiedPlayer) sender); + // create a new instance of our CustomTabOverlayProvider and add it to the tab view + tabView.getTabOverlayProviders().addProvider(new CustomTabOverlayProvider(tabView)); + } + } + }); + } + + /** + * Our custom tab overlay provider. + */ + private class CustomTabOverlayProvider extends AbstractPlayerTabOverlayProvider { + + // In this field we will store the handle to access the header and footer + private HeaderAndFooterHandle headerFooterHandle; + // In the contentHandle field we store the handle to modify the content of the tab list + private RectangularTabOverlay contentHandle; + // The updateTask field stores the task hande for the update task + private ScheduledTask updateTask; + + public CustomTabOverlayProvider(@Nonnull @NonNull TabView tabView) { + // set the name to "custom-taboverlay-example", you can use any name, but it needs to be unique + // set the priority to 1000. The plugin will display the tab overlay with the highest priority. So using a + // high number here is good. It ensures our custom tab overlay is displayed and not a tab list provided by + // a config file. The priority should be between 0 and 10000. + super(tabView, "custom-taboverlay-example", 1000); + } + + @Override + protected void onAttach() { + // This informs us that the tab overlay provider has been added to a tab view. + // We can access the tab view using super.getTabView(). After this has been called BungeeTabListPlus + // expects shouldActivate() to return correct values. + + // In this example we don't need to do anything here. However if you create a more complex tab overlay + // provider, that isn't always active, but should activate depending on some condition, you might want to + // do some stuff here. + } + + @Override + protected void onActivate(TabOverlayHandler handler) { + // Our tab overlay provider has been activated + + // Configure that tab overlay of the player and store the handles so we can modify it later + // IMPORTANT: Do not store a copy of handler anywhere!!! + + // for the header and footer we can choose either custom or pass through + // we choose custom + headerFooterHandle = handler.enterHeaderAndFooterOperationMode(HeaderAndFooterOperationMode.CUSTOM); + + // for the content we can choose between rectangular, simple and pass through. + // we choose rectangular + contentHandle = handler.enterContentOperationMode(ContentOperationMode.RECTANGULAR); + + // We set the header text + headerFooterHandle.setHeader("&6Super &eAwesome &cClock"); + + // now we set the size of the content to 1 column, 19 rows + contentHandle.setSize(new RectangularTabOverlay.Dimension(1, 19)); + + // we schedule a task to update the content every second + // we store the task handle in a field so we can cancel it later + updateTask = getProxy().getScheduler().schedule(DemoPlugin.this, this::updateContent, 0, 1, TimeUnit.SECONDS); + } + + @Override + protected void onDeactivate() { + // Our tab overlay provider has been deactivated + // We should now stop modifying the tab list + + // We cancel the update task + updateTask.cancel(); + } + + @Override + protected void onDetach() { + // Our tab overlay provider has been detached from the tab view. + // This means it is no longer used at all. + + // You can use this method to free any resources you might have acquired. + + // In this example we don't have to do anything here. + } + + @Override + protected boolean shouldActivate() { + // This method is used by the plugin to check whether this tab overlay provider should be active. + + // In our example we want our custom tab overlay provider to always be used so we always return true. + return true; + } + + // This method renders an analogue clock to the tab list. + private void updateContent() { + // First we draw to a buffered image + BufferedImage image = renderClock(); + + // now we convert the image to text lines and set the appropriate slot of the tab list + for (int row = 0; row < 19; row++) { + String text = ""; + for (int x = 0; x < 19; x++) { + ChatColor chatColor = getSimilarChatColor(new Color(image.getRGB(x, row))); + text += chatColor == null ? ' ' : chatColor.toString() + '█'; + } + + // get our custom icon. If it's not ready yet use the default alex icon + Icon icon = customIcon.getNow(Icon.DEFAULT_ALEX); + + // set the icon, text and ping of the slot + contentHandle.setSlot(0, row, icon, text, 0); + } + } + } + + // This method renders an analogue clock to a buffered image. + @Nonnull + private BufferedImage renderClock() { + BufferedImage image = new BufferedImage(19, 19, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = image.createGraphics(); + // background + g.setColor(getAWTColor(ChatColor.DARK_GRAY)); + g.fillRect(0, 0, 19, 19); + // circle + g.setColor(getAWTColor(ChatColor.GRAY)); + for (int x = 0; x < 19; x++) + for (int y = 0; y < 19; y++) + if ((8.5 - x) * (8.5 - x) + (8.5 - y) * (8.5 - y) < 81) + g.drawRect(x, y, 1, 1); + // arrows + int hour = Calendar.getInstance().get(Calendar.HOUR); + g.setColor(getAWTColor(ChatColor.DARK_RED)); + g.drawLine(9, 9, (int) round(9 + 8 * sin(hour / 6.0 * PI)), (int) round(9 - 8 * cos(hour / 6.0 * PI))); + int minute = Calendar.getInstance().get(Calendar.MINUTE); + g.setColor(getAWTColor(ChatColor.RED)); + g.drawLine(9, 9, (int) round(9 + 8 * sin(minute / 30.0 * PI)), (int) round(9 - 8 * cos(minute / 30.0 * PI))); + int second = Calendar.getInstance().get(Calendar.SECOND); + g.setColor(getAWTColor(ChatColor.GOLD)); + g.drawLine(9, 9, (int) round(9 + 9 * sin(second / 30.0 * PI)), (int) round(9 - 9 * cos(second / 30.0 * PI))); + return image; + } +} diff --git a/example/velocity/src/main/resources/bungee.yml b/example/velocity/src/main/resources/bungee.yml new file mode 100644 index 00000000..a9be36fa --- /dev/null +++ b/example/velocity/src/main/resources/bungee.yml @@ -0,0 +1,5 @@ +name: BungeeTabListPlus-API-Demo +version: ${version} +author: CodeCrafter47 +main: de.codecrafter47.bungeetablistplus.demo.DemoPlugin +depends: [BungeeTabListPlus] \ No newline at end of file diff --git a/example/velocity/src/main/resources/icon.png b/example/velocity/src/main/resources/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8fcd3a4bd6c2d6b5e02c13bd480c2c0614c482dd GIT binary patch literal 228 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1SGw4HSYi^&H|6fVg?3oVGw3ym^DWND9B#o z>Fdh=fSrj=TQ6sl=31aov!{z=2*>r(K0~fU4g#+K*BLkOUUq|JwS(Wozx@wbbC_?< zY;Ey9!Lz1S!L0J>W6|@BFI;{J2pKo5`4w~h`@Y?NPp)lq?%@?|IGUsL)Xw)xyTOqP zQAUObI!q6?=Gqz7-. + */ + +package codecrafter47.bungeetablistplus; + +import de.codecrafter47.taboverlay.config.context.ContextKey; +import de.codecrafter47.taboverlay.config.player.PlayerSet; + +public class BTLPContextKeys { + + public static final ContextKey SERVER_ID = new ContextKey<>("SERVER_ID"); + public static final ContextKey SERVER_PLAYER_SET = new ContextKey<>("SERVER_PLAYER_SET"); +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java new file mode 100644 index 00000000..667b7661 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java @@ -0,0 +1,561 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package codecrafter47.bungeetablistplus; + +import codecrafter47.bungeetablistplus.api.velocity.BungeeTabListPlusAPI; +import codecrafter47.bungeetablistplus.bridge.BukkitBridge; +import codecrafter47.bungeetablistplus.cache.Cache; +import codecrafter47.bungeetablistplus.command.CommandBungeeTabListPlus; +import codecrafter47.bungeetablistplus.common.network.BridgeProtocolConstants; +import codecrafter47.bungeetablistplus.compat.SortingRuleAliasProcessor; +import codecrafter47.bungeetablistplus.config.MainConfig; +import codecrafter47.bungeetablistplus.config.PlayersByServerComponentConfiguration; +import codecrafter47.bungeetablistplus.data.BTLPVelocityDataKeys; +import codecrafter47.bungeetablistplus.data.PermissionDataProvider; +import codecrafter47.bungeetablistplus.listener.TabListListener; +import codecrafter47.bungeetablistplus.managers.*; +import codecrafter47.bungeetablistplus.placeholder.GlobalServerPlaceholderResolver; +import codecrafter47.bungeetablistplus.placeholder.PlayerPlaceholderResolver; +import codecrafter47.bungeetablistplus.placeholder.ServerCountPlaceholderResolver; +import codecrafter47.bungeetablistplus.placeholder.ServerPlaceholderResolver; +import codecrafter47.bungeetablistplus.player.FakePlayerManagerImpl; +import codecrafter47.bungeetablistplus.tablist.ExcludedServersTabOverlayProvider; +import codecrafter47.bungeetablistplus.updater.UpdateChecker; +import codecrafter47.bungeetablistplus.updater.UpdateNotifier; +import codecrafter47.bungeetablistplus.util.ExceptionHandlingEventExecutor; +import codecrafter47.bungeetablistplus.util.MatchingStringsCollection; +import codecrafter47.bungeetablistplus.util.VelocityPlugin; +import codecrafter47.bungeetablistplus.version.VelocityProtocolVersionProvider; +import codecrafter47.bungeetablistplus.version.ProtocolVersionProvider; +import codecrafter47.bungeetablistplus.version.ViaVersionProtocolVersionProvider; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.velocitypowered.api.command.CommandManager; +import com.velocitypowered.api.proxy.messages.ChannelIdentifier; +import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; +import de.codecrafter47.data.bukkit.api.BukkitData; +import de.codecrafter47.data.velocity.api.VelocityData; +import de.codecrafter47.data.sponge.api.SpongeData; +import de.codecrafter47.taboverlay.config.ComponentSpec; +import de.codecrafter47.taboverlay.config.ConfigTabOverlayManager; +import de.codecrafter47.taboverlay.config.ErrorHandler; +import de.codecrafter47.taboverlay.config.dsl.customplaceholder.CustomPlaceholderConfiguration; +import de.codecrafter47.taboverlay.config.icon.DefaultIconManager; +import de.codecrafter47.taboverlay.config.platform.EventListener; +import de.codecrafter47.taboverlay.config.platform.Platform; +import de.codecrafter47.taboverlay.config.player.JoinedPlayerProvider; +import de.codecrafter47.taboverlay.config.player.PlayerProvider; +import de.codecrafter47.taboverlay.config.spectator.SpectatorPassthroughTabOverlayManager; +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.EventExecutorGroup; +import io.netty.util.concurrent.MultithreadEventExecutorGroup; +import lombok.Getter; +import lombok.val; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.error.YAMLException; + +import java.io.*; +import java.lang.reflect.Field; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +/** + * Main Class of BungeeTabListPlus + * + * @author Florian Stober + */ +public class BungeeTabListPlus { + + /** + * Holds an INSTANCE of itself if the plugin is enabled + */ + private static BungeeTabListPlus INSTANCE; + @Getter + private final VelocityPlugin plugin; + + public PlayerProvider playerProvider; + @Getter + private EventExecutor mainThreadExecutor; + @Getter + private EventExecutorGroup asyncExecutor; + + @Getter + private RedisPlayerManager redisPlayerManager; + @Getter + private DataManager dataManager; + private ServerStateManager serverStateManager; + @Getter + private ServerPlaceholderResolver serverPlaceholderResolver; + + private Yaml yaml; + @Getter + private HiddenPlayersManager hiddenPlayersManager; + private PlayerPlaceholderResolver playerPlaceholderResolver; + private API api; + private Logger logger = Logger.getLogger(BungeeTabListPlus.class.getSimpleName()); + + public BungeeTabListPlus(VelocityPlugin plugin) { + this.plugin = plugin; + } + + /** + * Static getter for the current instance of the plugin + * + * @return the current instance of the plugin, null if the plugin is + * disabled + */ + public static BungeeTabListPlus getInstance(VelocityPlugin plugin) { + if (INSTANCE == null) { + INSTANCE = new BungeeTabListPlus(plugin); + } + return INSTANCE; + } + + public static BungeeTabListPlus getInstance() { + return INSTANCE; + } + + @Getter + private MainConfig config; + + private Cache cache; + @Getter + MatchingStringsCollection excludedServers; + + @Getter + private FakePlayerManagerImpl fakePlayerManagerImpl; + + private BukkitBridge bukkitBridge; + + private UpdateChecker updateChecker = null; + + @Getter + private DefaultIconManager iconManager; + + @Getter + private BungeePlayerProvider bungeePlayerProvider; + + @Getter + private ProtocolVersionProvider protocolVersionProvider; + + @Getter + private TabViewManager tabViewManager; + + private ConfigTabOverlayManager configTabOverlayManager; + private SpectatorPassthroughTabOverlayManager spectatorPassthroughTabOverlayManager; + + @Getter + private List listeners = new ArrayList<>(); + + private transient boolean scheduledSoftReload; + @Getter + private final ChannelIdentifier channelIdentifier = MinecraftChannelIdentifier.from(BridgeProtocolConstants.CHANNEL); + + public void onLoad() { + codecrafter47.bungeetablistplus.util.ProxyServer.setProxyServer(plugin.getProxy()); + if (!plugin.getDataDirectory().toFile().exists()) { + plugin.getDataDirectory().toFile().mkdirs(); + } + + try { + Class.forName("com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo"); + } catch (ClassNotFoundException ex) { + throw new RuntimeException("You need to run at least Velocity version #196"); + } + + INSTANCE = this; + + Executor executor = (task) -> getProxy().getScheduler().buildTask(getPlugin(), task).schedule(); + + asyncExecutor = new MultithreadEventExecutorGroup(4, executor) { + @Override + protected EventExecutor newChild(Executor executor, Object... args) { + return new ExceptionHandlingEventExecutor(this, executor, logger); + } + }; + mainThreadExecutor = new ExceptionHandlingEventExecutor(null, executor, logger); + + if (getProxy().getPluginManager().getPlugin("ViaVersion").isPresent()) { + protocolVersionProvider = new ViaVersionProtocolVersionProvider(); + } else { + protocolVersionProvider = new VelocityProtocolVersionProvider(); + } + + this.tabViewManager = new TabViewManager(this, protocolVersionProvider); + + File headsFolder = new File(plugin.getDataDirectory().toFile(), "heads"); + extractDefaultIcons(headsFolder); + + iconManager = new DefaultIconManager(asyncExecutor, mainThreadExecutor, headsFolder.toPath(), logger); + + cache = Cache.load(new File(plugin.getDataDirectory().toFile(), "cache.dat")); + + serverPlaceholderResolver = new ServerPlaceholderResolver(cache); + playerPlaceholderResolver = new PlayerPlaceholderResolver(serverPlaceholderResolver, cache); + + api = new API(tabViewManager, iconManager, playerPlaceholderResolver, serverPlaceholderResolver, logger, this); + + try { + Field field = BungeeTabListPlusAPI.class.getDeclaredField("instance"); + field.setAccessible(true); + field.set(null, api); + } catch (NoSuchFieldException | IllegalAccessException ex) { + getLogger().log(Level.SEVERE, "Failed to initialize API", ex); + } + } + + public void onEnable() { + + ConfigTabOverlayManager.Options options = ConfigTabOverlayManager.Options.createBuilderWithDefaults() + .playerIconDataKey(BTLPVelocityDataKeys.DATA_KEY_ICON) + .playerPingDataKey(VelocityData.Velocity_Ping) + .playerInvisibleDataKey(BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN) + .playerCanSeeInvisibleDataKey(BTLPVelocityDataKeys.permission("bungeetablistplus.seevanished")) + .component(new ComponentSpec("!players_by_server", PlayersByServerComponentConfiguration.class)) + .sortingRulePreprocessor(new SortingRuleAliasProcessor()) + .build(); + yaml = ConfigTabOverlayManager.constructYamlInstance(options); + + if (readMainConfig()) + return; + + bungeePlayerProvider = new BungeePlayerProvider(mainThreadExecutor); + + hiddenPlayersManager = new HiddenPlayersManager(); + hiddenPlayersManager.addVanishProvider("/btlp hide", BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN_PLAYER_COMMAND); + hiddenPlayersManager.addVanishProvider("config.yml (hiddenPlayers)", BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN_PLAYER_CONFIG); + hiddenPlayersManager.addVanishProvider("config.yml (hiddenServers)", BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN_SERVER_CONFIG); + hiddenPlayersManager.addVanishProvider("VanishNoPacket", BukkitData.VanishNoPacket_IsVanished); + hiddenPlayersManager.addVanishProvider("SuperVanish", BukkitData.SuperVanish_IsVanished); + hiddenPlayersManager.addVanishProvider("CMI", BukkitData.CMI_IsVanished); + hiddenPlayersManager.addVanishProvider("Essentials", BukkitData.Essentials_IsVanished); + hiddenPlayersManager.addVanishProvider("ProtocolVanish", BukkitData.ProtocolVanish_IsVanished); + hiddenPlayersManager.addVanishProvider("Bukkit Player Metadata `vanished`", BukkitData.BukkitPlayerMetadataVanished); + hiddenPlayersManager.addVanishProvider("Sponge VANISH", SpongeData.Sponge_IsVanished); + hiddenPlayersManager.enable(); + + fakePlayerManagerImpl = new FakePlayerManagerImpl(plugin, iconManager, mainThreadExecutor); + + List playerProviders = new ArrayList<>(); + if (getProxy().getPluginManager().getPlugin("RedisBungee").isPresent()) { + redisPlayerManager = new RedisPlayerManager(bungeePlayerProvider, this, logger); + playerProviders.add(redisPlayerManager); + plugin.getLogger().info("Hooked RedisBungee"); + } + playerProviders.add(bungeePlayerProvider); + playerProviders.add(fakePlayerManagerImpl); + this.playerProvider = new JoinedPlayerProvider(playerProviders); + + getProxy().getChannelRegistrar().register(channelIdentifier); + bukkitBridge = new BukkitBridge(asyncExecutor, mainThreadExecutor, playerPlaceholderResolver, serverPlaceholderResolver, getPlugin(), logger, bungeePlayerProvider, this, cache); + serverStateManager = new ServerStateManager(config, plugin); + dataManager = new DataManager(api, plugin, logger, bungeePlayerProvider, mainThreadExecutor, serverStateManager, bukkitBridge); + dataManager.addCompositeDataProvider(hiddenPlayersManager); + dataManager.addCompositeDataProvider(new PermissionDataProvider()); + + updateExcludedAndHiddenServerLists(); + + // register commands and update Notifier + CommandManager commandManager = getProxy().getCommandManager(); + commandManager.register(new CommandBungeeTabListPlus(plugin).register("bungeetablistplus")); + commandManager.register(new CommandBungeeTabListPlus(plugin).register("btlp")); + + getProxy().getScheduler().buildTask(plugin, new UpdateNotifier(this)).delay(15, TimeUnit.MINUTES).repeat(15, TimeUnit.MINUTES).schedule(); + + // Load updateCheck thread + if (config.checkForUpdates) { + updateChecker = new UpdateChecker(plugin); + plugin.getLogger().info("Starting UpdateChecker Task"); + getProxy().getScheduler().buildTask(plugin, updateChecker).delay(0, TimeUnit.MINUTES).repeat(UpdateChecker.interval, TimeUnit.MINUTES).schedule(); + } + + int[] serversHash = {getProxy().getAllServers().hashCode()}; + + getProxy().getScheduler().buildTask(plugin, () -> { + int hash = getProxy().getAllServers().hashCode(); + if (hash != serversHash[0]) { + serversHash[0] = hash; + scheduleSoftReload(); + } + }).delay(1, TimeUnit.MINUTES).repeat(1, TimeUnit.MINUTES).schedule(); + + MyPlatform platform = new MyPlatform(); + configTabOverlayManager = new ConfigTabOverlayManager(platform, + playerProvider, + playerPlaceholderResolver, + ImmutableList.of(new ServerCountPlaceholderResolver(dataManager), + new GlobalServerPlaceholderResolver(dataManager, serverPlaceholderResolver)), + yaml, + options, + logger, + mainThreadExecutor, + iconManager); + spectatorPassthroughTabOverlayManager = new SpectatorPassthroughTabOverlayManager(platform, mainThreadExecutor, BTLPVelocityDataKeys.DATA_KEY_GAMEMODE); + if (config.disableCustomTabListForSpectators) { + spectatorPassthroughTabOverlayManager.enable(); + } else { + spectatorPassthroughTabOverlayManager.disable(); + } + + updateTimeZoneAndGlobalCustomPlaceholders(); + + Path tabLists = getPlugin().getDataDirectory().resolve("tabLists"); + if (!Files.exists(tabLists)) { + try { + Files.createDirectories(tabLists); + } catch (IOException e) { + getLogger().log(Level.SEVERE, "Failed to create tabLists directory", e); + return; + } + try { + Files.copy(getClass().getClassLoader().getResourceAsStream("default.yml"), tabLists.resolve("default.yml")); + } catch (IOException e) { + plugin.getLogger().warn("Failed to save default config.", e); + } + } + configTabOverlayManager.reloadConfigs(ImmutableSet.of(tabLists)); + + getProxy().getEventManager().register(plugin, new TabListListener(this)); + } + + private void updateTimeZoneAndGlobalCustomPlaceholders() { + configTabOverlayManager.setTimeZone(config.getTimeZone()); + + if (config.customPlaceholders != null) { + val customPlaceholders = new HashMap(); + for (val entry : config.customPlaceholders.entrySet()) { + if (entry.getKey() != null && entry.getValue() != null) { + customPlaceholders.put(entry.getKey(), entry.getValue()); + } + } + configTabOverlayManager.setGlobalCustomPlaceholders(customPlaceholders); + } + } + + private void updateExcludedAndHiddenServerLists() { + excludedServers = new MatchingStringsCollection( + config.excludeServers != null + ? config.excludeServers + : Collections.emptyList() + ); + ExcludedServersTabOverlayProvider.onReload(); + dataManager.setHiddenServers(new MatchingStringsCollection( + config.hiddenServers != null + ? config.hiddenServers + : Collections.emptyList() + )); + dataManager.setPermanentlyHiddenPlayers(config.hiddenPlayers != null ? config.hiddenPlayers : Collections.emptyList()); + } + + // get jar file path + private String getJarName(){ + try { + Class klass = BungeeTabListPlus.class; + URL location = klass.getResource('/' + klass.getName().replace('.', '/') + ".class"); + if(location == null) return null; + String jarName = URLDecoder.decode(location.getFile(), "UTF8").split("!")[0]; + return jarName.substring(jarName.lastIndexOf("/")); + } catch (Exception e) { + return null; + } + } + + private void extractDefaultIcons(File headsFolder) { + if (!headsFolder.exists()) { + headsFolder.mkdirs(); + + try { + String jarName = getJarName(); + if(jarName == null){ + throw new IOException("Failed to get jar name from class path"); + } + + // copy default heads + ZipInputStream zipInputStream = new ZipInputStream(Files.newInputStream(new File("plugins/" + jarName).toPath())); + + ZipEntry entry; + while ((entry = zipInputStream.getNextEntry()) != null) { + if (!entry.isDirectory() && entry.getName().startsWith("heads/")) { + try { + File targetFile = new File(plugin.getDataDirectory().toFile(), entry.getName()); + targetFile.getParentFile().mkdirs(); + if (!targetFile.exists()) { + Files.copy(zipInputStream, targetFile.toPath()); + getLogger().info("Extracted " + entry.getName()); + } + } catch (IOException ex) { + getLogger().log(Level.SEVERE, "Failed to extract file " + entry.getName(), ex); + } + } + } + + zipInputStream.close(); + } catch (IOException ex) { + getLogger().log(Level.SEVERE, "Error extracting files", ex); + } + } + } + + private boolean readMainConfig() { + try { + File file = new File(plugin.getDataDirectory().toFile(), "config.yml"); + if (!file.exists()) { + config = new MainConfig(); + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)); + config.writeWithComments(writer, yaml); + } else { + ErrorHandler.set(new ErrorHandler()); + config = yaml.loadAs(new FileInputStream(file), MainConfig.class); + ErrorHandler errorHandler = ErrorHandler.get(); + ErrorHandler.set(null); + if (!errorHandler.getEntries().isEmpty()) { + plugin.getLogger().warn(errorHandler.formatErrors(file.getName())); + } + if (config.needWrite) { + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)); + config.writeWithComments(writer, yaml); + } + } + } catch (IOException | YAMLException ex) { + plugin.getLogger().warn("Unable to load config.yml", ex); + return true; + } + return false; + } + + public void onDisable() { + // save cache + cache.save(); + } + + /** + * Reloads most settings of the plugin + */ + public boolean reload() { + fakePlayerManagerImpl.removeConfigFakePlayers(); + + if (readMainConfig()) { + plugin.getLogger().warn("Unable to reload Config"); + return false; + } else { + updateExcludedAndHiddenServerLists(); + updateTimeZoneAndGlobalCustomPlaceholders(); + + // clear cache to force image files to be read from disk again + iconManager.clearCache(); + + Path tabLists = getPlugin().getDataDirectory().resolve("tabLists"); + configTabOverlayManager.reloadConfigs(ImmutableSet.of(tabLists)); + + fakePlayerManagerImpl.reload(); + + serverStateManager.updateConfig(config); + + if (config.disableCustomTabListForSpectators) { + spectatorPassthroughTabOverlayManager.enable(); + } else { + spectatorPassthroughTabOverlayManager.disable(); + } + return true; + } + } + + public void scheduleSoftReload() { + if (!scheduledSoftReload) { + scheduledSoftReload = true; + asyncExecutor.execute(this::softReload); + } + } + + private void softReload() { + scheduledSoftReload = false; + + if (configTabOverlayManager != null) { + configTabOverlayManager.refreshConfigs(); + + // this is a good time to save the cache + asyncExecutor.execute(cache::save); + } + } + + @Deprecated + public final void failIfNotMainThread() { + if (!mainThreadExecutor.inEventLoop()) { + getLogger().log(Level.SEVERE, "Not in main thread", new IllegalStateException("Not in main thread")); + } + } + + /** + * Getter for BukkitBridge. For internal use only. + * + * @return an instance of BukkitBridge + */ + public BukkitBridge getBridge() { + return this.bukkitBridge; + } + + /** + * Checks whether an update for BungeeTabListPlus is available. Actually + * the check is performed in a background task and this only returns the + * result. + * + * @return true if a newer version of BungeeTabListPlus is available + */ + public boolean isUpdateAvailable() { + return updateChecker != null && updateChecker.isUpdateAvailable(); + } + + public boolean isNewDevBuildAvailable() { + return updateChecker != null && updateChecker.isNewDevBuildAvailable(); + } + + public void reportError(Throwable th) { + plugin.getLogger().error("An internal error occurred! Please send the " + + "following StackTrace to the developer in order to help" + + " resolving the problem", + th); + } + + public Logger getLogger() { + return logger; + } + + public com.velocitypowered.api.proxy.ProxyServer getProxy() { + return plugin.getProxy(); + } + + private final class MyPlatform implements Platform { + + @Override + public void addEventListener(EventListener listener) { + BungeeTabListPlus.this.listeners.add(listener); + } + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/bridge/BukkitBridge.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/bridge/BukkitBridge.java new file mode 100644 index 00000000..c81d5c32 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/bridge/BukkitBridge.java @@ -0,0 +1,709 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package codecrafter47.bungeetablistplus.bridge; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.cache.Cache; +import codecrafter47.bungeetablistplus.common.BTLPDataKeys; +import codecrafter47.bungeetablistplus.common.network.BridgeProtocolConstants; +import codecrafter47.bungeetablistplus.common.network.DataStreamUtils; +import codecrafter47.bungeetablistplus.common.network.TypeAdapterRegistry; +import codecrafter47.bungeetablistplus.common.util.RateLimitedExecutor; +import codecrafter47.bungeetablistplus.data.TrackingDataCache; +import codecrafter47.bungeetablistplus.managers.BungeePlayerProvider; +import codecrafter47.bungeetablistplus.placeholder.PlayerPlaceholderResolver; +import codecrafter47.bungeetablistplus.placeholder.ServerPlaceholderResolver; +import codecrafter47.bungeetablistplus.player.VelocityPlayer; +import codecrafter47.bungeetablistplus.util.ProxyServer; +import codecrafter47.bungeetablistplus.util.VelocityPlugin; +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.DisconnectEvent; +import com.velocitypowered.api.event.connection.PluginMessageEvent; +import com.velocitypowered.api.event.connection.PostLoginEvent; +import com.velocitypowered.api.event.player.ServerConnectedEvent; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ServerConnection; +import com.velocitypowered.api.proxy.messages.ChannelIdentifier; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import de.codecrafter47.data.api.DataCache; +import de.codecrafter47.data.api.DataHolder; +import de.codecrafter47.data.api.DataKey; +import it.unimi.dsi.fastutil.objects.ObjectIterator; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import it.unimi.dsi.fastutil.objects.ReferenceSet; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.*; +import java.util.*; +import java.util.concurrent.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class BukkitBridge { + + private static final TypeAdapterRegistry typeAdapterRegistry = TypeAdapterRegistry.DEFAULT_TYPE_ADAPTERS; + private static final RateLimitedExecutor rlExecutor = new RateLimitedExecutor(5000); + + private final Map playerPlayerConnectionInfoMap = new ConcurrentHashMap<>(); + private final Map serverInformation = new ConcurrentHashMap<>(); + private final int proxyIdentifier = ThreadLocalRandom.current().nextInt(); + private final NetDataKeyIdMap idMap = new NetDataKeyIdMap(); + + private final ScheduledExecutorService asyncExecutor; + private final ScheduledExecutorService mainLoop; + private final PlayerPlaceholderResolver playerPlaceholderResolver; + private final ServerPlaceholderResolver serverPlaceholderResolver; + private final VelocityPlugin plugin; + private final Logger logger; + private final BungeePlayerProvider bungeePlayerProvider; + private final BungeeTabListPlus btlp; + private final Cache cache; + + public BukkitBridge(ScheduledExecutorService asyncExecutor, ScheduledExecutorService mainLoop, PlayerPlaceholderResolver playerPlaceholderResolver, ServerPlaceholderResolver serverPlaceholderResolver, VelocityPlugin plugin, Logger logger, BungeePlayerProvider bungeePlayerProvider, BungeeTabListPlus btlp, Cache cache) { + this.asyncExecutor = asyncExecutor; + this.mainLoop = mainLoop; + this.playerPlaceholderResolver = playerPlaceholderResolver; + this.serverPlaceholderResolver = serverPlaceholderResolver; + this.plugin = plugin; + this.logger = logger; + this.bungeePlayerProvider = bungeePlayerProvider; + this.btlp = btlp; + this.cache = cache; + ProxyServer.getInstance().getEventManager().register(plugin, this); + asyncExecutor.scheduleWithFixedDelay(this::sendIntroducePackets, 100, 100, TimeUnit.MILLISECONDS); + asyncExecutor.scheduleWithFixedDelay(this::resendUnconfirmedMessages, 2, 2, TimeUnit.SECONDS); + asyncExecutor.scheduleWithFixedDelay(this::removeObsoleteServerConnections, 5, 5, TimeUnit.SECONDS); + } + + @Subscribe + public void onPlayerConnect(PostLoginEvent event) { + playerPlayerConnectionInfoMap.put(event.getPlayer(), new PlayerConnectionInfo()); + } + + @Subscribe + public void onServerChange(ServerConnectedEvent event) { + PlayerConnectionInfo previous = playerPlayerConnectionInfoMap.put(event.getPlayer(), new PlayerConnectionInfo()); + if (previous != null) { + PlayerBridgeDataCache playerBridgeData = previous.playerBridgeData; + if (playerBridgeData != null) { + synchronized (playerBridgeData) { + playerBridgeData.connection = null; + playerBridgeData.reset(); + } + } + } + } + + @Subscribe + public void onPlayerDisconnect(DisconnectEvent event) { + playerPlayerConnectionInfoMap.remove(event.getPlayer()); + } + + @Subscribe + public void onPluginMessage(PluginMessageEvent event) { + if (event.getIdentifier().equals(btlp.getChannelIdentifier())) { + if (event.getTarget() instanceof Player && event.getSource() instanceof ServerConnection) { + + Player player = (Player) event.getTarget(); + ServerConnection server = (ServerConnection) event.getSource(); + + event.setResult(PluginMessageEvent.ForwardResult.handled()); + + DataInput input = new DataInputStream(new ByteArrayInputStream(event.getData())); + + try { + handlePluginMessage(player, server, input); + } catch (Throwable e) { + rlExecutor.execute(() -> { + logger.log(Level.SEVERE, "Unexpected error: ", e); + }); + } + } else { + // hacking attempt + event.setResult(PluginMessageEvent.ForwardResult.handled()); + } + } + } + + public String getStatus(RegisteredServer server) { + Collection players = server.getPlayersConnected(); + if (players.isEmpty()) { + return "no players"; + } + int error_bungee = 0, unavailable = 0, working = 0, stale = 0, incomplete = 0; + for (Player player : players) { + PlayerConnectionInfo connectionInfo = playerPlayerConnectionInfoMap.get(player); + + if (connectionInfo == null) { + error_bungee++; + continue; + } + + if (connectionInfo.isConnectionValid == false) { + unavailable++; + continue; + } + + if (connectionInfo.hasReceived == false) { + incomplete++; + continue; + } + + PlayerBridgeDataCache bridgeData = connectionInfo.playerBridgeData; + if (bridgeData == null) { + error_bungee++; + continue; + } + + if (bridgeData.nextOutgoingMessageId - bridgeData.lastConfirmed > 2 && System.currentTimeMillis() - bridgeData.lastMessageSent > 5000) { + stale++; + continue; + } + } + + if (unavailable == players.size()) { + return "not installed or incompatible version"; + } + + String status = "working: " + working; + if (unavailable != 0) { + status += ", unavailable: " + unavailable; + } + if (incomplete != 0) { + status += ", unavailable: " + incomplete; + } + if (stale != 0) { + status += ", stale: " + stale; + } + if (error_bungee != 0) { + status += ", error_bungee: " + error_bungee; + } + return status; + } + + private void handlePluginMessage(Player player, ServerConnection server, DataInput input) throws IOException { + PlayerConnectionInfo connectionInfo = playerPlayerConnectionInfoMap.get(player); + + if (connectionInfo == null) { + return; + } + + int messageId = input.readUnsignedByte(); + + if (messageId == BridgeProtocolConstants.MESSAGE_ID_INTRODUCE) { + + int serverIdentifier = input.readInt(); + int protocolVersion = input.readInt(); + int minimumCompatibleProtocolVersion = input.readInt(); + String proxyPluginVersion = input.readUTF(); + + int connectionId = serverIdentifier + proxyIdentifier; + + if (connectionId == connectionInfo.connectionIdentifier) { + return; + } + + if (BridgeProtocolConstants.VERSION < minimumCompatibleProtocolVersion) { + rlExecutor.execute(() -> { + logger.log(Level.WARNING, "Incompatible version of BTLP on server " + server.getServerInfo().getName() + " detected: " + proxyPluginVersion); + }); + return; + } + + // reset connection state + connectionInfo.connectionIdentifier = connectionId; + connectionInfo.isConnectionValid = true; + connectionInfo.nextIntroducePacketDelay = 1; + connectionInfo.introducePacketDelay = 1; + connectionInfo.hasReceived = false; + connectionInfo.protocolVersion = Integer.min(BridgeProtocolConstants.VERSION, protocolVersion); + + VelocityPlayer velocityPlayer = bungeePlayerProvider.getPlayerIfPresent(player); + if (velocityPlayer == null) { + logger.severe("Internal error - Bridge functionality not available for " + player.getUsername()); + } else { + connectionInfo.playerBridgeData = velocityPlayer.getBridgeDataCache(); + connectionInfo.playerBridgeData.setConnectionId(connectionId); + connectionInfo.playerBridgeData.connection = server; + connectionInfo.playerBridgeData.requestMissingData(); + } + connectionInfo.serverBridgeData = getServerDataCache(server.getServerInfo().getName()); + connectionInfo.serverBridgeData.setConnectionId(connectionId); + connectionInfo.serverBridgeData.addConnection(server); + connectionInfo.serverBridgeData.requestMissingData(); + + // send ACK 0 + ByteArrayOutputStream byteArrayOutput = new ByteArrayOutputStream(); + DataOutput output = new DataOutputStream(byteArrayOutput); + + output.writeByte(BridgeProtocolConstants.MESSAGE_ID_ACK); + output.writeInt(connectionId); + output.writeInt(0); + + byte[] message = byteArrayOutput.toByteArray(); + server.sendPluginMessage(btlp.getChannelIdentifier(), message); + } else { + + if (!connectionInfo.isConnectionValid) { + return; + } + + int connectionId = input.readInt(); + + if (connectionId != connectionInfo.connectionIdentifier) { + return; + } + + connectionInfo.hasReceived = true; + + int sequenceNumber = input.readInt(); + + BridgeData bridgeData; + boolean isServerMessage; + + if ((messageId & 0x80) == 0) { + bridgeData = connectionInfo.playerBridgeData; + isServerMessage = false; + } else { + bridgeData = connectionInfo.serverBridgeData; + isServerMessage = true; + } + + if (bridgeData == null) { + return; + } + + messageId = messageId & 0x7f; + + synchronized (bridgeData) { + + if (messageId == BridgeProtocolConstants.MESSAGE_ID_ACK) { + + int confirmed = sequenceNumber - bridgeData.lastConfirmed; + + if (confirmed > bridgeData.messagesPendingConfirmation.size()) { + return; + } + + while (confirmed-- > 0) { + bridgeData.lastConfirmed++; + bridgeData.messagesPendingConfirmation.remove(); + } + } else if (messageId == BridgeProtocolConstants.MESSAGE_ID_UPDATE_DATA) { + + if (sequenceNumber > bridgeData.nextIncomingMessageId) { + // ignore messages from the future + return; + } + + ByteArrayOutputStream byteArrayOutput = new ByteArrayOutputStream(); + DataOutput output = new DataOutputStream(byteArrayOutput); + + output.writeByte(BridgeProtocolConstants.MESSAGE_ID_ACK | (isServerMessage ? 0x80 : 0x00)); + output.writeInt(connectionId); + output.writeInt(sequenceNumber); + + byte[] message = byteArrayOutput.toByteArray(); + server.sendPluginMessage(btlp.getChannelIdentifier(), message); + + if (sequenceNumber < bridgeData.nextIncomingMessageId) { + // ignore messages from the past after sending ACK + return; + } + + bridgeData.nextIncomingMessageId++; + + int size = input.readInt(); + if (size > 0) { + onDataReceived(bridgeData, input, size); + } + } else { + throw new IllegalArgumentException("Unexpected message id: " + messageId); + } + } + } + } + + @SuppressWarnings("unchecked") + private void onDataReceived(DataCache cache, DataInput input, int size) throws IOException { + if (size == 1) { + int netId = input.readInt(); + DataKey key = idMap.getKey(netId); + + if (key == null) { + throw new AssertionError("Received unexpected data key net id " + netId); + } + + boolean removed = input.readBoolean(); + + if (removed) { + + mainLoop.execute(() -> cache.updateValue(key, null)); + } else { + + Object value = typeAdapterRegistry.getTypeAdapter(key.getType()).read(input); + mainLoop.execute(() -> cache.updateValue((DataKey) key, value)); + } + } else { + Object[] update = new Object[size * 2]; + + for (int i = 0; i < update.length; i += 2) { + int netId = input.readInt(); + DataKey key = idMap.getKey(netId); + + if (key == null) { + throw new AssertionError("Received unexpected data key net id " + netId); + } + + boolean removed = input.readBoolean(); + Object value = null; + + if (!removed) { + value = typeAdapterRegistry.getTypeAdapter(key.getType()).read(input); + } + + update[i] = key; + update[i + 1] = value; + } + + mainLoop.execute(() -> { + for (int i = 0; i < update.length; i += 2) { + cache.updateValue((DataKey) update[i], update[i + 1]); + } + }); + } + } + + /** + * Sends introduce packets to the proxy to try to establish a connection. + *

+ * Should be called periodically, recommended interval is 100ms to 1s. + */ + private void sendIntroducePackets() { + for (Map.Entry entry : playerPlayerConnectionInfoMap.entrySet()) { + ServerConnection server = entry.getKey().getCurrentServer().orElse(null); + PlayerConnectionInfo connectionInfo = entry.getValue(); + if (server != null && !connectionInfo.hasReceived) { + if (--connectionInfo.nextIntroducePacketDelay <= 0) { + connectionInfo.nextIntroducePacketDelay = connectionInfo.introducePacketDelay++; + + try { + + ByteArrayOutputStream byteArrayOutput = new ByteArrayOutputStream(); + DataOutput output = new DataOutputStream(byteArrayOutput); + + output.writeByte(BridgeProtocolConstants.MESSAGE_ID_INTRODUCE); + output.writeInt(proxyIdentifier); + output.writeInt(BridgeProtocolConstants.VERSION); + output.writeInt(BridgeProtocolConstants.VERSION); + output.writeUTF(plugin.getVersion()); + + byte[] message = byteArrayOutput.toByteArray(); + server.sendPluginMessage(btlp.getChannelIdentifier(), message); + } catch (Throwable th) { + rlExecutor.execute(() -> { + logger.log(Level.SEVERE, "Unexpected error", th); + }); + } + } + } + } + } + + /** + * Sends unconfirmed messages to the proxy yet another time, to ensure their arrival. + *

+ * Should be called periodically, recommended interval is 1s to 10s. + */ + private void resendUnconfirmedMessages() { + long now = System.currentTimeMillis(); + + for (Map.Entry e : playerPlayerConnectionInfoMap.entrySet()) { + Player player = e.getKey(); + ServerConnection server = player.getCurrentServer().orElse(null); + PlayerConnectionInfo connectionInfo = e.getValue(); + + if (!connectionInfo.isConnectionValid) { + continue; + } + + BridgeData bridgeData = connectionInfo.playerBridgeData; + + if (bridgeData == null) { + continue; + } + + if (server != null && bridgeData.messagesPendingConfirmation.size() > 0 && (now > bridgeData.lastMessageSent + 1000 || bridgeData.messagesPendingConfirmation.size() > 5)) { + for (byte[] message : bridgeData.messagesPendingConfirmation) { + server.sendPluginMessage(btlp.getChannelIdentifier(), message); + } + } + } + + for (ServerBridgeDataCache bridgeData : serverInformation.values()) { + ServerConnection server = bridgeData.getConnection(); + + if (server != null && bridgeData.messagesPendingConfirmation.size() > 0 && (now > bridgeData.lastMessageSent + 1000 || bridgeData.messagesPendingConfirmation.size() > 5)) { + for (byte[] message : bridgeData.messagesPendingConfirmation) { + server.sendPluginMessage(btlp.getChannelIdentifier(), message); + } + } + } + } + + private void checkForThirdPartyVariables(String serverName, ServerBridgeDataCache dataCache) { + mainLoop.execute(() -> { + dataCache.addDataChangeListener(BTLPDataKeys.REGISTERED_THIRD_PARTY_VARIABLES, () -> updateBridgePlaceholders(serverName, dataCache)); + updateBridgePlaceholders(serverName, dataCache); + dataCache.addDataChangeListener(BTLPDataKeys.REGISTERED_THIRD_PARTY_SERVER_VARIABLES, () -> updateBridgeServerPlaceholders(serverName, dataCache)); + updateBridgeServerPlaceholders(serverName, dataCache); + dataCache.addDataChangeListener(BTLPDataKeys.PAPI_REGISTERED_PLACEHOLDER_PLUGINS, () -> updatePlaceholderAPIPlaceholders(serverName, dataCache)); + updatePlaceholderAPIPlaceholders(serverName, dataCache); + }); + } + + private void updateBridgePlaceholders(String serverName, ServerBridgeDataCache dataCache) { + List variables = dataCache.get(BTLPDataKeys.REGISTERED_THIRD_PARTY_VARIABLES); + if (variables != null) { + for (String variable : variables) { + playerPlaceholderResolver.addBridgeCustomPlaceholderDataKey(variable, BTLPDataKeys.createThirdPartyVariableDataKey(variable)); + } + cache.updateCustomPlaceholdersBridge(serverName, variables); + btlp.scheduleSoftReload(); + } + } + + private void updateBridgeServerPlaceholders(String serverName, ServerBridgeDataCache dataCache) { + List variables = dataCache.get(BTLPDataKeys.REGISTERED_THIRD_PARTY_SERVER_VARIABLES); + if (variables != null) { + for (String variable : variables) { + serverPlaceholderResolver.addBridgeCustomPlaceholderServerDataKey(variable, BTLPDataKeys.createThirdPartyServerVariableDataKey(variable)); + } + cache.updateCustomServerPlaceholdersBridge(serverName, variables); + btlp.scheduleSoftReload(); + } + } + + private void updatePlaceholderAPIPlaceholders(String serverName, ServerBridgeDataCache dataCache) { + List plugins = dataCache.get(BTLPDataKeys.PAPI_REGISTERED_PLACEHOLDER_PLUGINS); + if (plugins != null) { + playerPlaceholderResolver.addPlaceholderAPIPluginPrefixes(plugins); + cache.updatePAPIPrefixes(serverName, plugins); + btlp.scheduleSoftReload(); + } + } + + private void removeObsoleteServerConnections() { + for (ServerBridgeDataCache cache : serverInformation.values()) { + cache.removeObsoleteConnections(); + } + } + + public PlayerBridgeDataCache createDataCacheForPlayer(@Nonnull VelocityPlayer player) { + return new PlayerBridgeDataCache(); + } + + public DataHolder getServerDataHolder(@Nonnull String server) { + return getServerDataCache(server); + } + + private ServerBridgeDataCache getServerDataCache(@Nonnull String serverName) { + if (!serverInformation.containsKey(serverName)) { + serverInformation.computeIfAbsent(serverName, key -> { + ServerBridgeDataCache dataCache = new ServerBridgeDataCache(); + checkForThirdPartyVariables(serverName, dataCache); + return dataCache; + }); + } + return serverInformation.get(serverName); + } + + private static class PlayerConnectionInfo { + boolean isConnectionValid = false; + boolean hasReceived = false; + int connectionIdentifier = 0; + int protocolVersion = 0; + int nextIntroducePacketDelay = 1; + int introducePacketDelay = 1; + @Nullable + PlayerBridgeDataCache playerBridgeData = null; + @Nullable + ServerBridgeDataCache serverBridgeData = null; + } + + private abstract class BridgeData extends TrackingDataCache { + + final Queue messagesPendingConfirmation = new ConcurrentLinkedQueue<>(); + int lastConfirmed = 0; + int nextOutgoingMessageId = 1; + int nextIncomingMessageId = 1; + long lastMessageSent = 0; + int connectionId; + + boolean requestAll = false; + + @Nullable + abstract ServerConnection getConnection(); + + @Override + protected void addActiveKey(DataKey key) { + super.addActiveKey(key); + + try { + synchronized (this) { + ServerConnection connection = getConnection(); + if (connection != null) { + ByteArrayDataOutput data = ByteStreams.newDataOutput(); + data.writeByte(this instanceof PlayerBridgeDataCache ? BridgeProtocolConstants.MESSAGE_ID_REQUEST_DATA : BridgeProtocolConstants.MESSAGE_ID_REQUEST_DATA_SERVER); + data.writeInt(connectionId); + data.writeInt(nextOutgoingMessageId++); + data.writeInt(1); + DataStreamUtils.writeDataKey(data, key); + data.writeInt(idMap.getNetId(key)); + byte[] message = data.toByteArray(); + messagesPendingConfirmation.add(message); + lastMessageSent = System.currentTimeMillis(); + connection.sendPluginMessage(btlp.getChannelIdentifier(), message); + } else { + requestAll = true; + } + } + } catch (Throwable th) { + rlExecutor.execute(() -> { + logger.log(Level.SEVERE, "Unexpected exception", th); + }); + requestAll = true; + } + } + + void setConnectionId(int connectionId) { + if (this.connectionId != connectionId) { + reset(); + } + this.connectionId = connectionId; + } + + void reset() { + synchronized (this) { + requestAll = true; + messagesPendingConfirmation.clear(); + lastConfirmed = 0; + nextOutgoingMessageId = 1; + nextIncomingMessageId = 1; + lastMessageSent = 0; + Collection> queriedKeys = new ArrayList<>(getActiveKeys()); + mainLoop.execute(() -> { + for (DataKey key : queriedKeys) { + updateValue(key, null); + } + }); + } + } + + void requestMissingData() throws IOException { + synchronized (this) { + if (requestAll) { + ServerConnection connection = getConnection(); + if (connection != null) { + List> keys = new ArrayList<>(getActiveKeys()); + + ByteArrayDataOutput data = ByteStreams.newDataOutput(); + data.writeByte(this instanceof PlayerBridgeDataCache ? BridgeProtocolConstants.MESSAGE_ID_REQUEST_DATA : BridgeProtocolConstants.MESSAGE_ID_REQUEST_DATA_SERVER); + data.writeInt(connectionId); + data.writeInt(nextOutgoingMessageId++); + data.writeInt(keys.size()); + for (DataKey key : keys) { + DataStreamUtils.writeDataKey(data, key); + data.writeInt(idMap.getNetId(key)); + } + byte[] message = data.toByteArray(); + messagesPendingConfirmation.add(message); + lastMessageSent = System.currentTimeMillis(); + connection.sendPluginMessage(btlp.getChannelIdentifier(), message); + } + requestAll = false; + } + } + } + } + + public class PlayerBridgeDataCache extends BridgeData { + @Nullable + private volatile ServerConnection connection = null; + + @Override + @Nullable + ServerConnection getConnection() { + return connection; + } + } + + private class ServerBridgeDataCache extends BridgeData { + private final ReferenceSet connections = new ReferenceOpenHashSet<>(); + + private void addConnection(@Nonnull ServerConnection server) { + synchronized (this) { + connections.add(server); + } + } + + @Override + @Nullable + ServerConnection getConnection() { + synchronized (this) { + ObjectIterator iterator = connections.iterator(); + while (iterator.hasNext()) { + try { + ServerConnection server = iterator.next(); + if (server.getServer().ping().join() != null) { + return server; + } + } catch (Exception ignored){} + iterator.remove(); + } + return null; + } + } + + private void removeObsoleteConnections() { + synchronized (this) { + ObjectIterator iterator = connections.iterator(); + while (iterator.hasNext()) { + ServerConnection server = iterator.next(); + try { + if (server.getServer().ping().join() == null) { + iterator.remove(); + } + } catch (Exception ignored) { + iterator.remove(); + } + } + } + } + + @Override + void reset() { + synchronized (this) { + super.reset(); + connections.clear(); + } + } + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/bridge/NetDataKeyIdMap.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/bridge/NetDataKeyIdMap.java new file mode 100644 index 00000000..c05858d1 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/bridge/NetDataKeyIdMap.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.bridge; + +import de.codecrafter47.data.api.DataKey; + +import javax.annotation.Nullable; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +public class NetDataKeyIdMap { + + private final ConcurrentHashMap, Integer> map = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> mapReversed = new ConcurrentHashMap<>(); + + private final AtomicInteger nextId = new AtomicInteger(0); + + public int getNetId(DataKey key) { + return map.computeIfAbsent(key, this::computeNetId); + } + + private int computeNetId(DataKey key) { + int id = nextId.getAndIncrement(); + mapReversed.put(id, key); + return id; + } + + @Nullable + public DataKey getKey(int id) { + return mapReversed.get(id); + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/cache/Cache.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/cache/Cache.java new file mode 100644 index 00000000..82d0ad29 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/cache/Cache.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.cache; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.util.ProxyServer; + +import java.io.*; +import java.util.*; +import java.util.logging.Level; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class Cache implements Serializable { + + private transient File file; + + private Map> cachedPAPIPrefixes = new HashMap<>(); + private Map> cachedCustomPlaceholdersBridge = new HashMap<>(); + private Map> cachedCustomServerPlaceholdersBridge = new HashMap<>(); + + public synchronized void updatePAPIPrefixes(String server, List prefixes) { + cachedPAPIPrefixes.put(server, new ArrayList<>(prefixes)); + } + + public synchronized Set getPAPIPrefixes() { + return getServerNames() + .map(cachedPAPIPrefixes::get) + .filter(Objects::nonNull) + .flatMap(List::stream) + .collect(Collectors.toSet()); + } + + private Stream getServerNames() { + for (int i = 0; i < 3; i++) { + try { + return ProxyServer.getInstance().getAllServers() + .stream() + .filter(Objects::nonNull).map(registeredServer -> registeredServer.getServerInfo().getName()); + } catch (Throwable ignored) { + } + } + return Stream.empty(); + } + + public synchronized void updateCustomPlaceholdersBridge(String server, List prefixes) { + cachedCustomPlaceholdersBridge.put(server, new ArrayList<>(prefixes)); + } + + public synchronized Set getCustomPlaceholdersBridge() { + return getServerNames() + .map(cachedCustomPlaceholdersBridge::get) + .filter(Objects::nonNull) + .flatMap(List::stream) + .collect(Collectors.toSet()); + } + + public synchronized void updateCustomServerPlaceholdersBridge(String server, List prefixes) { + cachedCustomServerPlaceholdersBridge.put(server, new ArrayList<>(prefixes)); + } + + public synchronized Set getCustomServerPlaceholdersBridge() { + return getServerNames() + .map(cachedCustomServerPlaceholdersBridge::get) + .filter(Objects::nonNull) + .flatMap(List::stream) + .collect(Collectors.toSet()); + } + + private Cache(File file) { + this.file = file; + } + + public static Cache load(File file) { + try (FileInputStream is = new FileInputStream(file)) { + ObjectInputStream ois = new ObjectInputStream(is); + Cache cache = (Cache) ois.readObject(); + cache.file = file; + return cache; + } catch (Throwable th) { + return new Cache(file); + } + } + + public synchronized void save() { + try (FileOutputStream os = new FileOutputStream(file)) { + ObjectOutputStream oos = new ObjectOutputStream(os); + oos.writeObject(this); + oos.flush(); + } catch (Throwable th) { + BungeeTabListPlus.getInstance().getLogger().log(Level.SEVERE, "Failed to write file: " + th.getMessage(), th); + } + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandBungeeTabListPlus.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandBungeeTabListPlus.java new file mode 100644 index 00000000..622f7ecd --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandBungeeTabListPlus.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.command; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.bridge.BukkitBridge; +import codecrafter47.bungeetablistplus.common.BTLPDataKeys; +import codecrafter47.bungeetablistplus.updater.UpdateChecker; +import codecrafter47.bungeetablistplus.util.VelocityPlugin; +import codecrafter47.bungeetablistplus.util.ChatUtil; +import com.google.common.base.Joiner; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.tree.LiteralCommandNode; +import com.velocitypowered.api.command.BrigadierCommand; +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import de.codecrafter47.data.api.DataHolder; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class CommandBungeeTabListPlus { + + private final VelocityPlugin plugin; + public CommandBungeeTabListPlus(VelocityPlugin plugin) { + this.plugin = plugin; + } + + public BrigadierCommand register(String base) { + LiteralCommandNode btlpCommand = LiteralArgumentBuilder.literal(base) + .requires(source -> source.hasPermission("bungeetablistplus.command")) + .then(LiteralArgumentBuilder.literal("reload").requires(source -> source.hasPermission("bungeetablistplus.admin")).executes(this::commandReload)) + .then(LiteralArgumentBuilder.literal("status").executes(this::commandStatus)) + .then(LiteralArgumentBuilder.literal("help").executes(this::commandHelp)) + .then(LiteralArgumentBuilder.literal("debug").requires(source -> source.hasPermission("bungeetablistplus.admin")) + .then(LiteralArgumentBuilder.literal("hidden").executes(CommandDebug::commandHidden)) + .then(LiteralArgumentBuilder.literal("pipeline").executes(CommandDebug::commandPipeline)) + ).then(LiteralArgumentBuilder.literal("hide").requires(source -> source.hasPermission("bungeetablistplus.hide")) + .then(LiteralArgumentBuilder.literal("on").executes(CommandHide::commandHide)) + .then(LiteralArgumentBuilder.literal("true").executes(CommandHide::commandHide)) + .then(LiteralArgumentBuilder.literal("enable").executes(CommandHide::commandHide)) + .then(LiteralArgumentBuilder.literal("off").executes(CommandHide::commandUnhide)) + .then(LiteralArgumentBuilder.literal("false").executes(CommandHide::commandUnhide)) + .then(LiteralArgumentBuilder.literal("disable").executes(CommandHide::commandUnhide)) + .then(LiteralArgumentBuilder.literal("toggle").executes(CommandHide::commandToggle)) + .executes(CommandHide::commandToggle) + ).then(LiteralArgumentBuilder.literal("fakeplayers").requires(source -> source.hasPermission("bungeetablistplus.admin")) + .then(LiteralArgumentBuilder.literal("add") + .then(RequiredArgumentBuilder.argument("name", StringArgumentType.word()).executes(CommandFakePlayers::commandAdd))) + .then(LiteralArgumentBuilder.literal("remove") + .then(RequiredArgumentBuilder.argument("name", StringArgumentType.word()).executes(CommandFakePlayers::commandRemove))) + .then(LiteralArgumentBuilder.literal("list").executes(CommandFakePlayers::commandList)) + .then(LiteralArgumentBuilder.literal("removeall").executes(CommandFakePlayers::commandRemoveAll)) + .then(LiteralArgumentBuilder.literal("help").executes(CommandFakePlayers::commandHelp)) + .executes(CommandFakePlayers::commandRemove) + ) + .executes(this::commandHelp) + .build(); + return new BrigadierCommand(btlpCommand); + } + + private int commandReload(CommandContext ctx) { + CommandSource sender = ctx.getSource(); + boolean success = BungeeTabListPlus.getInstance().reload(); + if (success) { + sender.sendMessage(Component.text("Successfully reloaded BungeeTabListPlus.", NamedTextColor.GREEN)); + } else { + sender.sendMessage(Component.text("An error occurred while reloaded BungeeTabListPlus.", NamedTextColor.RED)); + } + return Command.SINGLE_SUCCESS; + } + + private int commandHelp(CommandContext ctx) { + CommandSource sender = ctx.getSource(); + sender.sendMessage(ChatUtil.parseBBCode("&e&lAvailable Commands:")); + sender.sendMessage(ChatUtil.parseBBCode("&e[suggest]/btlp reload[/suggest] &f&oReload the configuration")); + sender.sendMessage(ChatUtil.parseBBCode("&e[suggest=/btlp hide]/btlp hide [on|off|toggle][/suggest] &f&oHide yourself from the tab list.")); + sender.sendMessage(ChatUtil.parseBBCode("&e[suggest]/btlp fake[/suggest] &f&oManage fake players.")); + sender.sendMessage(ChatUtil.parseBBCode("&e[suggest]/btlp status[/suggest] &f&oDisplays info about plugin version, updates and the bridge plugin.")); + sender.sendMessage(ChatUtil.parseBBCode("&e[suggest]/btlp help[/suggest] &f&oYou already found this one :P")); + return Command.SINGLE_SUCCESS; + } + + private int commandStatus(CommandContext ctx) { + CommandSource sender = ctx.getSource(); + // Version + String version = plugin.getVersion(); + sender.sendMessage(ChatUtil.parseBBCode("&eYou are running BungeeTabListPlus version " + version)); + + // Update? + sender.sendMessage(ChatUtil.parseBBCode("&eLooking for an update...")); + UpdateChecker updateChecker = new UpdateChecker(BungeeTabListPlus.getInstance().getPlugin()); + updateChecker.run(); + if (updateChecker.isUpdateAvailable()) { + sender.sendMessage(ChatUtil.parseBBCode("&aAn update is available at [url]http://www.spigotmc.org/resources/bungeetablistplus.313/[/url]")); + } else if (updateChecker.isNewDevBuildAvailable()) { + sender.sendMessage(ChatUtil.parseBBCode("&aA new dev-build is available at [url]https://ci.codecrafter47.de/job/BungeeTabListPlus/[/url]")); + } else { + sender.sendMessage(ChatUtil.parseBBCode("&aYou are already running the latest version.")); + } + + // Bridge plugin status + BukkitBridge bridge = BungeeTabListPlus.getInstance().getBridge(); + List servers = new ArrayList<>(plugin.getProxy().getAllServers()); + List withBridge = new ArrayList<>(); + List withoutBridge = new ArrayList<>(); + List maybeBridge = new ArrayList<>(); + for (RegisteredServer server : servers) { + DataHolder dataHolder = bridge.getServerDataHolder(server.getServerInfo().getName()); + if (dataHolder != null && dataHolder.get(BTLPDataKeys.REGISTERED_THIRD_PARTY_VARIABLES) != null) { + withBridge.add(server.getServerInfo().getName()); + } else { + if (server.getPlayersConnected().isEmpty()) { + maybeBridge.add(server.getServerInfo().getName()); + } else { + withoutBridge.add(server); + } + } + } + List withPAPI = servers.stream() + .filter(server -> { + DataHolder dataHolder = bridge.getServerDataHolder(server.getServerInfo().getName()); + Boolean b; + return dataHolder != null && (b = dataHolder.get(BTLPDataKeys.PLACEHOLDERAPI_PRESENT)) != null && b; + }) + .map((server) -> server.getServerInfo().getName()) + .collect(Collectors.toList()); + + sender.sendMessage(ChatUtil.parseBBCode("&eBridge plugin status:")); + if (!withBridge.isEmpty()) { + sender.sendMessage(ChatUtil.parseBBCode("&fInstalled on: &a" + Joiner.on("&f,&a ").join(withBridge))); + } + if (!withPAPI.isEmpty()) { + sender.sendMessage(ChatUtil.parseBBCode("&fServers with PlaceholderAPI: &a" + Joiner.on("&f, &a").join(withPAPI))); + } + for (RegisteredServer server : withoutBridge) { + sender.sendMessage(ChatUtil.parseBBCode("&c" + server.getServerInfo().getName() + "&f: " + bridge.getStatus(server))); + } + if (!maybeBridge.isEmpty()) { + sender.sendMessage(ChatUtil.parseBBCode("&eBridge status is not available for servers without players.")); + } + + // That's it + sender.sendMessage(ChatUtil.parseBBCode("&aThank you for using BungeeTabListPlus.")); + return Command.SINGLE_SUCCESS; + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandDebug.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandDebug.java new file mode 100644 index 00000000..3935e903 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandDebug.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.command; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.data.BTLPVelocityDataKeys; +import codecrafter47.bungeetablistplus.player.VelocityPlayer; +import codecrafter47.bungeetablistplus.util.ProxyServer; +import codecrafter47.bungeetablistplus.util.ReflectionUtil; +import codecrafter47.bungeetablistplus.util.ChatUtil; +import com.google.common.base.Joiner; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.context.CommandContext; +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import io.netty.channel.ChannelHandler; +import lombok.SneakyThrows; +import lombok.val; + + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +public class CommandDebug { + public static int commandHidden(CommandContext context) { + CommandSource sender = context.getSource(); + Player target = null; + String name = context.getArgument("name", String.class); + + + if (name == null || name.trim().isEmpty()) { + if (sender instanceof Player) { + target = (Player) sender; + } else { + sender.sendMessage(ChatUtil.parseBBCode("&cUsage: [suggest=/btlp debug hidden ]/btlp debug hidden [/suggest]")); + return Command.SINGLE_SUCCESS; + } + } else { + target = ProxyServer.getInstance().getPlayer(name).orElse(null); + if (target == null) { + sender.sendMessage(ChatUtil.parseBBCode("&cUnknown player: " + name)); + return Command.SINGLE_SUCCESS; + } + } + + val btlp = BungeeTabListPlus.getInstance(); + VelocityPlayer player = btlp.getBungeePlayerProvider().getPlayerIfPresent(target); + + if (player == null) { + sender.sendMessage(ChatUtil.parseBBCode("&cUnknown player: " + name)); + return Command.SINGLE_SUCCESS; + } + + Runnable dummyListener = () -> { + }; + CompletableFuture.runAsync(() -> { + player.addDataChangeListener(BTLPVelocityDataKeys.permission("bungeetablistplus.seevanished"), dummyListener); + player.addDataChangeListener(BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN, dummyListener); + }, btlp.getMainThreadExecutor()) + .thenRun(() -> { + btlp.getMainThreadExecutor().schedule(() -> { + Boolean canSeeHiddenPlayers = player.get(BTLPVelocityDataKeys.permission("bungeetablistplus.seevanished")); + Boolean isHidden = player.get(BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN); + List activeVanishProviders = btlp.getHiddenPlayersManager().getActiveVanishProviders(player); + + player.removeDataChangeListener(BTLPVelocityDataKeys.permission("bungeetablistplus.seevanished"), dummyListener); + player.removeDataChangeListener(BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN, dummyListener); + + sender.sendMessage(ChatUtil.parseBBCode("&bPlayer: &f" + player.getName() + "\n" + + "&bCan see hidden players: &f" + Boolean.TRUE.equals(canSeeHiddenPlayers) + "\n" + + "&bIs hidden: &f" + Boolean.TRUE.equals(isHidden) + ((!activeVanishProviders.isEmpty()) ? "\n" + + "&bHidden by: &f" + Joiner.on(", ").join(activeVanishProviders) + : ""))); + }, 1, TimeUnit.SECONDS); + }); + return Command.SINGLE_SUCCESS; + } + + @SneakyThrows + public static int commandPipeline(CommandContext context) { + if(!(context.getSource() instanceof Player)) { + context.getSource().sendMessage(ChatUtil.parseBBCode("&cThis command can only be run as a player!")); + return Command.SINGLE_SUCCESS; + } + Player player = (Player) context.getSource(); + ConnectedPlayer userConnection = (ConnectedPlayer) player; + List userPipeline = new ArrayList<>(); + for (Map.Entry entry : ReflectionUtil.getChannelWrapper(userConnection).getChannel().pipeline()) { + userPipeline.add(entry.getKey()); + } + + VelocityServerConnection serverConnection = userConnection.getConnectedServer(); + List serverPipeline = new ArrayList<>(); + for (Map.Entry entry : serverConnection.getConnection().getChannel().pipeline()) { + serverPipeline.add(entry.getKey()); + } + + player.sendMessage(ChatUtil.parseBBCode("&bUser: &f" + Joiner.on(", ").join(userPipeline) + "\n" + + "&bServer: &f" + Joiner.on(", ").join(serverPipeline))); + return Command.SINGLE_SUCCESS; + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandFakePlayers.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandFakePlayers.java new file mode 100644 index 00000000..8d8888ee --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandFakePlayers.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.command; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.api.velocity.tablist.FakePlayer; +import codecrafter47.bungeetablistplus.player.FakePlayerManagerImpl; +import codecrafter47.bungeetablistplus.util.ProxyServer; +import codecrafter47.bungeetablistplus.util.ChatUtil; +import com.google.common.base.Joiner; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.context.CommandContext; +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import com.velocitypowered.api.proxy.server.ServerInfo; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; + +public class CommandFakePlayers { + public static int commandAdd(CommandContext context) { + CommandSource sender = context.getSource(); + String name = context.getArgument("name", String.class); + if (name == null) { + sender.sendMessage(ChatUtil.parseBBCode("&cUsage: [suggest=/btlp fake add ]/btlp fake add [/suggest]")); + } else { + FakePlayer fakePlayer = manager().createFakePlayer(name, randomServer(), false, true); + fakePlayer.setRandomServerSwitchEnabled(true); + sender.sendMessage(ChatUtil.parseBBCode("&aAdded fake player " + name + ".")); + } + return Command.SINGLE_SUCCESS; + } + + public static int commandRemove(CommandContext context) { + CommandSource sender = context.getSource(); + String name = context.getArgument("name", String.class); + if (name == null) { + sender.sendMessage(ChatUtil.parseBBCode("&cUsage: [suggest=/btlp fake add ]/btlp fake remove [/suggest]")); + } else { + List list = manager().getOnlineFakePlayers() + .stream() + .filter(player -> player.getName().equals(name)) + .collect(Collectors.toList()); + if (list.isEmpty()) { + sender.sendMessage(ChatUtil.parseBBCode("&cNo fake player with name " + name + " found.")); + } else { + for (FakePlayer fakePlayer : list) { + manager().removeFakePlayer(fakePlayer); + } + if (list.size() == 1) { + sender.sendMessage(ChatUtil.parseBBCode("&aRemoved fake player " + name + ".")); + } else { + sender.sendMessage(ChatUtil.parseBBCode("&aRemoved " + list.size() + " fake players with name " + name + ".")); + } + } + } + return Command.SINGLE_SUCCESS; + } + + public static int commandList(CommandContext context) { + CommandSource sender = context.getSource(); + Collection fakePlayers = manager().getOnlineFakePlayers(); + sender.sendMessage(ChatUtil.parseBBCode("&eThere are " + fakePlayers.size() + " fake players online: &f" + Joiner.on(", ").join(fakePlayers))); + return Command.SINGLE_SUCCESS; + } + + public static int commandRemoveAll(CommandContext context) { + CommandSource sender = context.getSource(); + Collection fakePlayers = manager().getOnlineFakePlayers(); + int count = 0; + for (FakePlayer fakePlayer : fakePlayers) { + manager().removeFakePlayer(fakePlayer); + count++; + } + sender.sendMessage(ChatUtil.parseBBCode("&aRemoved " + count + " fake players.")); + return Command.SINGLE_SUCCESS; + } + + public static int commandHelp(CommandContext context) { + CommandSource sender = context.getSource(); + sender.sendMessage(ChatUtil.parseBBCode("&e&lAvailable Commands:")); + sender.sendMessage(ChatUtil.parseBBCode("&e[suggest=/btlp fake add]/btlp fake add [/suggest] &f&oAdd a fake player.")); + sender.sendMessage(ChatUtil.parseBBCode("&e[suggest=/btlp fake remove]/btlp fake remove [/suggest] &f&oRemove a fake player.")); + sender.sendMessage(ChatUtil.parseBBCode("&e[suggest]/btlp fake list[/suggest] &f&oShows a list of all fake players.")); + sender.sendMessage(ChatUtil.parseBBCode("&e[suggest]/btlp fake removeall[/suggest] &f&oRemoves all fake players.")); + sender.sendMessage(ChatUtil.parseBBCode("&e[suggest]/btlp fake help[/suggest] &f&oYou already found this one :P")); + return Command.SINGLE_SUCCESS; + } + + private static FakePlayerManagerImpl manager() { + return BungeeTabListPlus.getInstance().getFakePlayerManagerImpl(); + } + + private static ServerInfo randomServer() { + List servers = new ArrayList<>(); + for(RegisteredServer server : ProxyServer.getInstance().getAllServers()) servers.add(server.getServerInfo()); + return servers.get(ThreadLocalRandom.current().nextInt(servers.size())); + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandHide.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandHide.java new file mode 100644 index 00000000..ee250a8e --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandHide.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.command; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.data.BTLPVelocityDataKeys; +import codecrafter47.bungeetablistplus.player.VelocityPlayer; +import codecrafter47.bungeetablistplus.util.ChatUtil; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.context.CommandContext; +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.proxy.Player; + +public class CommandHide { + + public static int commandToggle(CommandContext context) { + if(!(context.getSource() instanceof Player)) { + context.getSource().sendMessage(ChatUtil.parseBBCode("&cThis command can only be run as a player!")); + return Command.SINGLE_SUCCESS; + } + Player player = (Player) context.getSource(); + BungeeTabListPlus.getInstance().getMainThreadExecutor().execute(() -> { + if (isHidden(player)) { + unhidePlayer(player); + player.sendMessage(ChatUtil.parseBBCode("&aYour name is no longer hidden from the tab list.")); + } else { + hidePlayer(player); + player.sendMessage(ChatUtil.parseBBCode("&aYou've been hidden from the tab list.")); + } + }); + return Command.SINGLE_SUCCESS; + } + + public static int commandHide(CommandContext context) { + if(!(context.getSource() instanceof Player)) { + context.getSource().sendMessage(ChatUtil.parseBBCode("&cThis command can only be run as a player!")); + return Command.SINGLE_SUCCESS; + } + Player player = (Player) context.getSource(); + BungeeTabListPlus.getInstance().getMainThreadExecutor().execute(() -> { + if (isHidden(player)) { + player.sendMessage(ChatUtil.parseBBCode("&cYou're already hidden.")); + } else { + hidePlayer(player); + player.sendMessage(ChatUtil.parseBBCode("&aYou've been hidden from the tab list.")); + } + }); + return Command.SINGLE_SUCCESS; + } + + public static int commandUnhide(CommandContext context) { + if(!(context.getSource() instanceof Player)) { + context.getSource().sendMessage(ChatUtil.parseBBCode("&cThis command can only be run as a player!")); + return Command.SINGLE_SUCCESS; + } + Player player = (Player) context.getSource(); + BungeeTabListPlus.getInstance().getMainThreadExecutor().execute(() -> { + if (isHidden(player)) { + unhidePlayer(player); + player.sendMessage(ChatUtil.parseBBCode("&aYour name is no longer hidden from the tab list.")); + } else { + player.sendMessage(ChatUtil.parseBBCode("&cYou've not been hidden.")); + } + }); + return Command.SINGLE_SUCCESS; + } + + private static boolean isHidden(Player player) { + VelocityPlayer velocityPlayer = BungeeTabListPlus.getInstance().getBungeePlayerProvider().getPlayer(player); + return Boolean.TRUE.equals(velocityPlayer.get(BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN_PLAYER_COMMAND)); + } + + private static void hidePlayer(Player player) { + VelocityPlayer velocityPlayer = BungeeTabListPlus.getInstance().getBungeePlayerProvider().getPlayer(player); + velocityPlayer.getLocalDataCache().updateValue(BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN_PLAYER_COMMAND, true); + } + + private static void unhidePlayer(Player player) { + VelocityPlayer velocityPlayer = BungeeTabListPlus.getInstance().getBungeePlayerProvider().getPlayer(player); + velocityPlayer.getLocalDataCache().updateValue(BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN_PLAYER_COMMAND, false); + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/compat/SortingRuleAliasProcessor.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/compat/SortingRuleAliasProcessor.java new file mode 100644 index 00000000..5dade956 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/compat/SortingRuleAliasProcessor.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.compat; + +import com.google.common.collect.ImmutableMap; +import de.codecrafter47.taboverlay.config.ErrorHandler; +import de.codecrafter47.taboverlay.config.SortingRulePreprocessor; +import lombok.Value; +import org.yaml.snakeyaml.error.Mark; + +public class SortingRuleAliasProcessor implements SortingRulePreprocessor { + + private static final ImmutableMap map = ImmutableMap.builder() + .put("you", new RewriteData("name viewer-first", true)) + .put("youfirst", new RewriteData("name viewer-first", false)) + .put("alpha", new RewriteData("name", true)) + .put("alphabet", new RewriteData("name", true)) + .put("alphabetic", new RewriteData("name", true)) + .put("alphabetical", new RewriteData("name", true)) + .put("alphabetically", new RewriteData("name", false)) + .put("teamfirst", new RewriteData("team viewer-first", false)) + .put("teams", new RewriteData("team", false)) + .put("factionfirst", new RewriteData("faction_name viewer-first", false)) + .put("factions", new RewriteData("faction_name", false)) + .put("worldname", new RewriteData("world", true)) + .put("playerworld", new RewriteData("world viewer-first", true)) + .put("playerworldfirst", new RewriteData("world viewer-first", true)) + .put("serveralphabetically", new RewriteData("server", true)) + .put("playerserverfirst", new RewriteData("server viewer-first", true)) + .put("afklast", new RewriteData("essentials_afk as number asc", true)) + .put("vaultgroupinfo", new RewriteData("vault_primary_group_weight asc", true)) + .put("vaultgroupinforeversed", new RewriteData("vault_primary_group_weight desc", true)) + .put("bungeepermsgroupinfo", new RewriteData("bungeeperms_primary_group_weight asc", true)) + .put("bungeepermsgroupinforeversed", new RewriteData("bungeeperms_primary_group_weight desc", true)) + .put("luckpermsgroupinfo", new RewriteData("luckpermsbungee_primary_group_weight asc", true)) + .put("luckpermsgroupinforeversed", new RewriteData("luckpermsbungee_primary_group_weight desc", true)) + .put("vaultprefix", new RewriteData("vault_prefix asc", true)) + .put("connectedfirst", new RewriteData("session_duration_total_seconds desc", false)) + .put("connectedlast", new RewriteData("session_duration_total_seconds asc", false)) + .build(); + + @Override + public String process(String sortingRule, ErrorHandler errorHandler, Mark mark) { + RewriteData rewriteData = map.get(sortingRule.toLowerCase()); + if (rewriteData != null) { + if (rewriteData.deprecated) { + errorHandler.addWarning("Sorting rule '" + sortingRule + "' has been deprecated. Use '" + rewriteData.rewrite + "' instead.", mark); + } + return rewriteData.rewrite; + } + return sortingRule; + } + + @Value + private static class RewriteData { + String rewrite; + boolean deprecated; + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/config/Comment.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/config/Comment.java new file mode 100644 index 00000000..ef099195 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/config/Comment.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Comment { + + String[] value(); +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/config/MainConfig.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/config/MainConfig.java new file mode 100644 index 00000000..32b2c4a3 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/config/MainConfig.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package codecrafter47.bungeetablistplus.config; + +import com.google.common.collect.ImmutableList; +import de.codecrafter47.taboverlay.config.dsl.customplaceholder.CustomPlaceholderConfiguration; +import de.codecrafter47.taboverlay.config.dsl.yaml.UpdateableConfig; +import de.codecrafter47.taboverlay.config.dsl.yaml.YamlUtil; +import lombok.val; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.nodes.MappingNode; +import org.yaml.snakeyaml.nodes.Tag; + +import java.io.IOException; +import java.io.Writer; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.*; + +public class MainConfig implements UpdateableConfig { + + @Comment({ + "if enabled the plugin checks for new versions automatically.", + "Use /BTLP to see whether a new version is available", + "this does NOT automatically install an update" + }) + public boolean checkForUpdates = true; + + @Comment({ + "this notifies admins (everyone with the permission `bungeetablistplus.admin`) if an update is available" + }) + + public boolean notifyAdminsIfUpdateAvailable = true; + + @Comment({ + "Interval (in seconds) at which all servers of your network get pinged to check whether they are online", + "If you intend to use the {onlineState:SERVER} variable set this to 2 or any value you like", + "setting this to -1 disables this feature" + }) + public int pingDelay = -1; + + @Comment({ + "those fakeplayers will randomly appear on the tablist. If you don't put any names there then no fakeplayers will appear" + }) + public List fakePlayers = new ArrayList<>(); + + @Comment({ + "Servers which you wish to show their own tabList (The one provided by bukkit)", + "Players on these servers don't see the custom tab list provided by BungeeTabListPlus" + }) + public List excludeServers = new ArrayList<>(); + + @Comment({ + "Players on these servers are hidden from the tab list.", + "Doesn't necessarily hide the server from the tab list." + }) + public List hiddenServers = new ArrayList<>(); + + @Comment({ + "players which are permanently hidden from the tab list", + "you can either put your username or your uuid (with dashes) here", + "don't use this. you have absolutely no reason to hide from anyone. on your own server." + }) + public List hiddenPlayers = new ArrayList<>(); + + @Comment({ + "Time zone to use for the {time} variable", + "Can be full name like \"America/Los_Angeles\"", + "or custom id like \"GMT+8\"" + }) + @Path("time-zone") + public String time_zone = TimeZone.getDefault().getID(); + + @Comment("Custom placeholders") + public Map customPlaceholders = new HashMap<>(); + + public TimeZone getTimeZone() { + return TimeZone.getTimeZone(time_zone); + } + + @Comment({ + "Disables the custom tab list for players in spectators mode.", + "As a result those players will see the vanilla tab list of the server.", + "If you do not use this option players in spectator mode will see the ", + "fake players created by BungeeTabListPlus in the teleport menu." + }) + public boolean disableCustomTabListForSpectators = true; + + @Comment({ + "Removes the `~BTLP Slot ##` entries from tab completion if the.", + "size of the tab list is 80 slots." + }) + public boolean experimentalTabCompleteFixForTabSize80 = false; + + @Comment({ + "Replaces the `~BTLP Slot ##` entries in tab completion with smileys" + }) + public boolean experimentalTabCompleteSmileys = false; + + public transient boolean needWrite = false; + + @Override + public void update(MappingNode node) { + val outdatedConfigOptions = ImmutableList.of("tablistUpdateIntervall", + "tablistUpdateInterval", + "updateOnPlayerJoinLeave", + "updateOnServerChange", + "offline", + "offline-text", + "online", + "online-text", + "permissionSource", + "useScoreboardToBypass16CharLimit", + "autoExcludeServers", + "showPlayersInGamemode3", + "serverAlias", + "worldAlias", + "serverPrefixes", + "prefixes", + "charLimit", + "automaticallySendBugReports"); + + for (String option : outdatedConfigOptions) { + needWrite |= YamlUtil.contains(node, option); + YamlUtil.remove(node, option); + } + + val newConfigOptions = ImmutableList.of( + "disableCustomTabListForSpectators", + "experimentalTabCompleteFixForTabSize80", + "experimentalTabCompleteSmileys" + ); + + for (String option : newConfigOptions) { + needWrite |= !YamlUtil.contains(node, option); + } + } + + public void writeWithComments(Writer writer, Yaml yaml) throws IOException { + writeCommentLine(writer, "This is the configuration file of BungeeTabListPlus"); + writeCommentLine(writer, "See https://github.com/CodeCrafter47/BungeeTabListPlus/wiki for additional information"); + + String ser = yaml.dumpAs(this, Tag.MAP, null); + + Map comments = new HashMap<>(); + for (Field field : MainConfig.class.getDeclaredFields()) { + Comment comment = field.getAnnotation(Comment.class); + if (comment != null) { + int modifiers = field.getModifiers(); + if (!Modifier.isStatic(modifiers) && !Modifier.isTransient(modifiers)) { + if (Modifier.isPublic(modifiers)) { + Path path = field.getAnnotation(Path.class); + comments.put(path != null ? path.value() : field.getName(), comment.value()); + } + } + } + } + + ArrayList lines = new ArrayList<>(Arrays.asList(ser.split("\n"))); + + ListIterator iterator = lines.listIterator(); + + while (iterator.hasNext()) { + String line = iterator.next(); + for (Map.Entry entry : comments.entrySet()) { + if (line.startsWith(entry.getKey())) { + String[] value = entry.getValue(); + iterator.previous(); + iterator.add(""); + for (String comment : value) { + iterator.add("# " + comment); + } + iterator.next(); + } + } + } + + for (String line : lines) { + writer.write(line); + writer.write("\n"); + } + + writer.close(); + } + + private static void writeCommentLine(Writer writer, String comment) throws IOException { + writer.write("# " + comment + "\n"); + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/config/Path.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/config/Path.java new file mode 100644 index 00000000..5c7ee777 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/config/Path.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Path { + + String value(); +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/config/PlayersByServerComponentConfiguration.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/config/PlayersByServerComponentConfiguration.java new file mode 100644 index 00000000..c14f0e16 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/config/PlayersByServerComponentConfiguration.java @@ -0,0 +1,298 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.config; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.data.BTLPVelocityDataKeys; +import codecrafter47.bungeetablistplus.placeholder.ComponentServerPlaceholderResolver; +import codecrafter47.bungeetablistplus.template.PlayersByServerComponentTemplate; +import codecrafter47.bungeetablistplus.util.ContextAwareOrdering; +import codecrafter47.bungeetablistplus.util.MapFunction; +import codecrafter47.bungeetablistplus.util.VelocityPlugin; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import de.codecrafter47.data.velocity.api.VelocityData; +import de.codecrafter47.taboverlay.config.context.Context; +import de.codecrafter47.taboverlay.config.dsl.ComponentConfiguration; +import de.codecrafter47.taboverlay.config.dsl.PlayerOrderConfiguration; +import de.codecrafter47.taboverlay.config.dsl.components.BasicComponentConfiguration; +import de.codecrafter47.taboverlay.config.dsl.util.ConfigValidationUtil; +import de.codecrafter47.taboverlay.config.dsl.yaml.*; +import de.codecrafter47.taboverlay.config.placeholder.OtherCountPlaceholderResolver; +import de.codecrafter47.taboverlay.config.placeholder.PlayerPlaceholderResolver; +import de.codecrafter47.taboverlay.config.player.PlayerSet; +import de.codecrafter47.taboverlay.config.player.PlayerSetPartition; +import de.codecrafter47.taboverlay.config.template.PlayerOrderTemplate; +import de.codecrafter47.taboverlay.config.template.TemplateCreationContext; +import de.codecrafter47.taboverlay.config.template.component.ComponentTemplate; +import de.codecrafter47.taboverlay.config.template.icon.PlayerIconTemplate; +import de.codecrafter47.taboverlay.config.template.ping.PlayerPingTemplate; +import lombok.Getter; +import lombok.Setter; +import lombok.val; +import org.yaml.snakeyaml.error.Mark; + +import javax.annotation.Nullable; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Getter +@Setter +public class PlayersByServerComponentConfiguration extends MarkedPropertyBase implements ComponentConfiguration { + + private PlayerOrderConfiguration playerOrder = PlayerOrderConfiguration.DEFAULT; + private MarkedStringProperty playerSet; + private ComponentConfiguration playerComponent = new BasicComponentConfiguration("${player name}"); + @Nullable + private ComponentConfiguration morePlayersComponent; + private boolean fillSlotsVertical = false; + private MarkedIntegerProperty minSize = new MarkedIntegerProperty(0); + private MarkedIntegerProperty maxSize = new MarkedIntegerProperty(-1); + private MarkedIntegerProperty minSizePerServer = new MarkedIntegerProperty(0); + private MarkedIntegerProperty maxSizePerServer = new MarkedIntegerProperty(-1); + @Nullable + private ComponentConfiguration serverHeader; + @Nullable + private ComponentConfiguration serverFooter; + @Nullable + private ComponentConfiguration serverSeparator; + + private MarkedListProperty hiddenServers = new MarkedListProperty<>(); + + private ServerOptions showServers = null; + private MarkedBooleanProperty includeEmptyServers = null; + + private MarkedStringProperty serverOrder = null; + private Map customServerOrder = null; + private boolean prioritizeViewerServer = true; + + private Map> mergeServers = null; + + public List getCustomServerOrder() { + // dummy method for snakeyaml to detect property type + return null; + } + + public void setCustomServerOrder(List customServerOrder) { + if (customServerOrder == null) { + this.customServerOrder = null; + } else { + val builder = ImmutableMap.builder(); + int rank = 0; + for (String server : customServerOrder) { + builder.put(server, rank++); + } + this.customServerOrder = builder.build(); + } + } + + @Override + public ComponentTemplate toTemplate(TemplateCreationContext tcc) { + if (ConfigValidationUtil.checkNotNull(tcc, "!players_by_server component", "playerSet", playerSet, getStartMark())) { + if (!tcc.getPlayerSets().containsKey(playerSet.getValue())) { + tcc.getErrorHandler().addError("No player set definition available for player set \"" + playerSet.getValue() + "\"", playerSet.getStartMark()); + } + } + + PlayerOrderTemplate playerOrderTemplate = PlayerOrderConfiguration.DEFAULT.toTemplate(tcc); + if (ConfigValidationUtil.checkNotNull(tcc, "!players_by_server component", "playerOrder", playerOrder, getStartMark())) { + playerOrderTemplate = this.playerOrder.toTemplate(tcc); + + } + if (minSize.getValue() < 0) { + tcc.getErrorHandler().addError("Failed to configure players component. MinSize is negative", minSize.getStartMark()); + } + if (maxSize.getValue() != -1 && minSize.getValue() > maxSize.getValue()) { + tcc.getErrorHandler().addError("Failed to configure players component. MaxSize is lower than minSize", maxSize.getStartMark()); + } + + if (minSizePerServer.getValue() < 0) { + tcc.getErrorHandler().addError("Failed to configure players component. MinSizePerWorld is negative", minSizePerServer.getStartMark()); + } + if (maxSizePerServer.getValue() != -1 && minSizePerServer.getValue() > maxSizePerServer.getValue()) { + tcc.getErrorHandler().addError("Failed to configure players component. MaxSizePerWorld is lower than minSizePerWorld", maxSizePerServer.getStartMark()); + } + + + TemplateCreationContext childContextS = tcc.clone(); + if (fillSlotsVertical) { + childContextS.setColumns(1); + } + BungeeTabListPlus btlp = BungeeTabListPlus.getInstance(); + childContextS.addPlaceholderResolver(new ComponentServerPlaceholderResolver(btlp.getServerPlaceholderResolver(), btlp.getDataManager())); + + TemplateCreationContext childContextP = childContextS.clone(); + childContextP.setDefaultIcon(new PlayerIconTemplate(PlayerPlaceholderResolver.BindPoint.PLAYER, tcc.getPlayerIconDataKey())); + childContextP.setDefaultPing(new PlayerPingTemplate(PlayerPlaceholderResolver.BindPoint.PLAYER, tcc.getPlayerPingDataKey())); + childContextP.setPlayerAvailable(true); + + TemplateCreationContext childContextM = tcc.clone(); + if (fillSlotsVertical) { + childContextM.setColumns(1); + } + childContextM.addPlaceholderResolver(new OtherCountPlaceholderResolver()); + + ComponentTemplate playerComponentTemplate = tcc.emptyComponent(); // dummy + if (ConfigValidationUtil.checkNotNull(tcc, "!players_by_server component", "playerComponent", playerComponent, getStartMark())) { + playerComponentTemplate = this.playerComponent.toTemplate(childContextP); + ComponentTemplate.LayoutInfo layoutInfo = playerComponentTemplate.getLayoutInfo(); + if (!layoutInfo.isConstantSize()) { + tcc.getErrorHandler().addError("Failed to configure !players_by_server component. Attribute playerComponent must not have variable size.", playerComponent.getStartMark()); + } + if (layoutInfo.isBlockAligned()) { + tcc.getErrorHandler().addError("Failed to configure !players_by_server component. Attribute playerComponent must not require block alignment.", playerComponent.getStartMark()); + } + } + + if (!ConfigValidationUtil.checkNotNull(tcc, "!players_by_server component", "hiddenServers", hiddenServers, getStartMark())) { + hiddenServers = new MarkedListProperty<>(); + } + + if (includeEmptyServers != null) { + if (includeEmptyServers.isValue()) { + tcc.getErrorHandler().addWarning("'includeEmptyServers: true' is deprecated and will be removed. Use 'showServers: ALL' instead.", includeEmptyServers.getStartMark()); + } else { + tcc.getErrorHandler().addWarning("'includeEmptyServers: false' is deprecated and will be removed. Use 'showServers: NON_EMPTY' instead.", includeEmptyServers.getStartMark()); + } + } + + if (showServers == null) { + if (includeEmptyServers != null) { + showServers = includeEmptyServers.isValue() ? ServerOptions.ALL : ServerOptions.NON_EMPTY; + } else { + showServers = ServerOptions.ALL; + } + } + + ContextAwareOrdering serverComparator = null; + + if (serverOrder != null) { + List> list = Stream.of(serverOrder.getValue().split(",")) + .filter((s1) -> !s1.isEmpty()) + .map(String::toLowerCase) + .map((String rule) -> getServerComparator(rule, tcc, serverOrder.getStartMark())) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + if (!list.isEmpty()) { + serverComparator = ContextAwareOrdering.compound(list); + } + } + + ComponentTemplate morePlayersComponentTemplate; + if (this.morePlayersComponent != null) { + + morePlayersComponentTemplate = this.morePlayersComponent.toTemplate(childContextM); + ComponentTemplate.LayoutInfo layoutInfo = morePlayersComponentTemplate.getLayoutInfo(); + if (!layoutInfo.isConstantSize()) { + tcc.getErrorHandler().addError("Failed to configure !players_by_server component. Attribute playerComponent cannot have variable size.", morePlayersComponent.getStartMark()); + } + if (layoutInfo.isBlockAligned()) { + tcc.getErrorHandler().addError("Failed to configure !players_by_server component. Attribute playerComponent must not require block alignment.", morePlayersComponent.getStartMark()); + } + } else { + morePlayersComponentTemplate = childContextM.emptyComponent(); + } + + Map serverMap = new HashMap<>(); + if (mergeServers != null) { + for (Map.Entry> entry : mergeServers.entrySet()) { + String groupName = entry.getKey(); + List serverNames = entry.getValue(); + if (serverNames != null) { + for (String serverName : serverNames) { + serverMap.put(serverName, groupName); + } + } + } + } + + return PlayersByServerComponentTemplate.builder() + .playerOrder(playerOrderTemplate) + .playerSet(tcc.getPlayerSets().get(playerSet.getValue())) + .playerComponent(playerComponentTemplate) + .morePlayersComponent(morePlayersComponentTemplate) + .serverHeader(serverHeader != null ? serverHeader.toTemplate(childContextS) : null) + .serverFooter(serverFooter != null ? serverFooter.toTemplate(childContextS) : null) + .serverSeparator(serverSeparator != null ? serverSeparator.toTemplate(tcc) : null) + .fillSlotsVertical(fillSlotsVertical) + .minSize(minSize.getValue()) + .maxSize(maxSize.getValue()) + .minSizePerServer(minSizePerServer.getValue()) + .maxSizePerServer(maxSizePerServer.getValue()) + .columns(tcc.getColumns().orElse(1)) + .defaultIcon(tcc.getDefaultIcon()) + .defaultText(tcc.getDefaultText()) + .defaultPing(tcc.getDefaultPing()) + .partitionFunction(tcc.getExpressionEngine().compile(childContextP, "${player server}", null)) + .mergeSections(new MapFunction(serverMap)) + .hiddenServers(ImmutableSet.copyOf(hiddenServers)) + .showServers(showServers) + .serverComparator(serverComparator) + .prioritizeViewerServer(prioritizeViewerServer) + .build(); + } + + private ContextAwareOrdering getServerComparator(String rule, TemplateCreationContext tcc, Mark mark) { + switch (rule) { + case "alphabetically": + return ContextAwareOrdering.from(Comparator.naturalOrder()); + case "playercount": + return new ContextAwareOrdering() { + @Override + public int compare(Context context, PlayerSetPartition partition, String first, String second) { + PlayerSet ps1 = partition.getPartition(first); + PlayerSet ps2 = partition.getPartition(second); + int pc1 = ps1 != null ? ps1.getCount() : 0; + int pc2 = ps2 != null ? ps2.getCount() : 0; + return -Integer.compare(pc1, pc2); + } + }; + case "online": + return ContextAwareOrdering.from(Comparator.comparing(server -> BungeeTabListPlus.getInstance().getDataManager().getServerDataHolder(server).get(BTLPVelocityDataKeys.DATA_KEY_SERVER_ONLINE), Comparator.reverseOrder())); + case "custom": + if (customServerOrder == null) { + tcc.getErrorHandler().addWarning("Selected serverOrder option 'custom', but 'customServerOrder' option is not set.", serverOrder.getStartMark()); + } + Map order = customServerOrder != null ? customServerOrder : Collections.emptyMap(); + return ContextAwareOrdering.from(Comparator.comparing(server -> order.getOrDefault(server, Integer.MAX_VALUE))); + case "yourserverfirst": + return new ContextAwareOrdering() { + @Override + public int compare(Context context, PlayerSetPartition partition, String first, String second) { + String server = context.getViewer().get(VelocityData.Velocity_Server); + if (first.equals(server)) { + return -1; + } else if (second.equals(server)) { + return 1; + } else { + return 0; + } + } + }; + default: + tcc.getErrorHandler().addWarning("Unknown serverOrder option: '" + rule + "'", mark); + return null; + } + } + + public enum ServerOptions { + ALL, ONLINE, NON_EMPTY + } + +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/data/AbstractCompositeDataProvider.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/AbstractCompositeDataProvider.java new file mode 100644 index 00000000..5738be12 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/AbstractCompositeDataProvider.java @@ -0,0 +1,66 @@ +package codecrafter47.bungeetablistplus.data; + +import codecrafter47.bungeetablistplus.player.VelocityPlayer; +import de.codecrafter47.data.api.DataKey; +import de.codecrafter47.taboverlay.config.player.Player; +import lombok.Getter; +import lombok.Value; + +import java.util.HashMap; +import java.util.Map; + +public abstract class AbstractCompositeDataProvider { + + @Getter + private final DataKey compositeDataKey; + private final Map playerDataListenerMap = new HashMap<>(); + + protected AbstractCompositeDataProvider(DataKey compositeDataKey) { + this.compositeDataKey = compositeDataKey; + } + + public final void onPlayerAdded(Player player, DataKey key) { + if (player instanceof VelocityPlayer) { + PlayerDataListener playerDataListener = new PlayerDataListener((VelocityPlayer) player, key); + playerDataListenerMap.put(new CompositeKey((VelocityPlayer) player, key), playerDataListener); + registerListener(player, key, playerDataListener); + playerDataListener.run(); + } + } + + protected abstract void registerListener(Player player, DataKey key, Runnable playerDataListener); + + public final void onPlayerRemoved(Player player, DataKey key) { + if (player instanceof VelocityPlayer) { + PlayerDataListener playerDataListener = playerDataListenerMap.remove(new CompositeKey((VelocityPlayer) player, key)); + if (playerDataListener != null) { + unregisterListener(player, key, playerDataListener); + } + } + } + + protected abstract void unregisterListener(Player player, DataKey key, Runnable listener); + + protected abstract T computeCompositeData(VelocityPlayer player, DataKey key); + + protected class PlayerDataListener implements Runnable { + private final VelocityPlayer player; + private final DataKey dataKey; + + private PlayerDataListener(VelocityPlayer player, DataKey dataKey) { + this.player = player; + this.dataKey = dataKey; + } + + @Override + public void run() { + player.getLocalDataCache().updateValue(dataKey, computeCompositeData(player, dataKey)); + } + } + + @Value + private static class CompositeKey { + VelocityPlayer player; + DataKey dataKey; + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/data/BTLPDataTypes.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/BTLPDataTypes.java new file mode 100644 index 00000000..d0cfed20 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/BTLPDataTypes.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.data; + +import codecrafter47.bungeetablistplus.common.network.DataStreamUtils; +import codecrafter47.bungeetablistplus.common.network.TypeAdapter; +import codecrafter47.bungeetablistplus.common.network.TypeAdapterRegistry; +import com.google.common.collect.ImmutableMap; +import de.codecrafter47.data.api.TypeToken; +import de.codecrafter47.taboverlay.Icon; +import de.codecrafter47.taboverlay.ProfileProperty; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.util.UUID; + +public class BTLPDataTypes { + + public static final TypeToken ICON = TypeToken.create(); + + public static final TypeAdapterRegistry REGISTRY = new TypeAdapterRegistry(ImmutableMap.of( + ICON, new TypeAdapter() { + @Override + public Icon read(DataInput input) throws IOException { + UUID uuid = null; + if (input.readBoolean()) { + uuid = DataStreamUtils.readUUID(input); + } + String value = input.readUTF(); + String signature = input.readUTF(); + return new Icon(new ProfileProperty("textures", value, signature)); + } + + @Override + public void write(DataOutput output, Icon icon) throws IOException { + output.writeBoolean(false); + ProfileProperty property = icon.getTextureProperty(); + output.writeUTF(property == null ? "" : property.getValue()); + output.writeUTF(property == null ? "" : property.getSignature()); + } + } + )); +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/data/BTLPVelocityDataKeys.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/BTLPVelocityDataKeys.java new file mode 100644 index 00000000..969df084 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/BTLPVelocityDataKeys.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.data; + +import de.codecrafter47.data.api.DataKey; +import de.codecrafter47.data.api.DataKeyCatalogue; +import de.codecrafter47.data.api.TypeToken; +import de.codecrafter47.data.velocity.api.VelocityData; +import de.codecrafter47.taboverlay.Icon; + +public class BTLPVelocityDataKeys implements DataKeyCatalogue { + public static final DataKey DATA_KEY_Server_Count = new DataKey<>("btlp:server_count", VelocityData.SCOPE_VELOCITY_PROXY, TypeToken.INTEGER); + public static final DataKey DATA_KEY_Server_Count_Online = new DataKey<>("btlp:server_count_online", VelocityData.SCOPE_VELOCITY_PROXY, TypeToken.INTEGER); + + public static final DataKey ThirdPartyPlaceholderBungee = new DataKey<>("btlp:thirdPartyPlaceholderBungee", VelocityData.SCOPE_VELOCITY_PLAYER, TypeToken.STRING); + + public static final DataKey ThirdPartyServerPlaceholderBungee = new DataKey<>("btlp:thirdPartyServerPlaceholderBungee", VelocityData.SCOPE_VELOCITY_SERVER, TypeToken.STRING); + + public static final DataKey DATA_KEY_GAMEMODE = new DataKey<>("btlp:gamemode", VelocityData.SCOPE_VELOCITY_PLAYER, TypeToken.INTEGER); + public static final DataKey DATA_KEY_ICON = new DataKey<>("btlp:icon", VelocityData.SCOPE_VELOCITY_PLAYER, BTLPDataTypes.ICON); + + public static final DataKey DATA_KEY_RedisBungee_ServerId = new DataKey<>("btlp:redisbungee:serverId", VelocityData.SCOPE_VELOCITY_PLAYER, TypeToken.STRING); + + public static final DataKey DATA_KEY_ServerName = new DataKey<>("btlp:serverName", VelocityData.SCOPE_VELOCITY_SERVER, TypeToken.STRING); + + public static final DataKey DATA_KEY_SERVER_ONLINE = new DataKey<>("btlp:server_online", VelocityData.SCOPE_VELOCITY_SERVER, TypeToken.BOOLEAN); + + public static final DataKey DATA_KEY_IS_HIDDEN = new DataKey<>("btlp:is_hidden", VelocityData.SCOPE_VELOCITY_PLAYER, TypeToken.BOOLEAN); + public static final DataKey DATA_KEY_CLIENT_VERSION = new DataKey<>("btlp:client_version", VelocityData.SCOPE_VELOCITY_PLAYER, TypeToken.STRING); + public static final DataKey DATA_KEY_CLIENT_VERSION_BELOW_1_8 = new DataKey<>("btlp:client_version_below_1_8", VelocityData.SCOPE_VELOCITY_PLAYER, TypeToken.BOOLEAN); + + public static final DataKey DATA_KEY_IS_HIDDEN_PLAYER_CONFIG = new DataKey<>("btlp:is_hidden_player_config", VelocityData.SCOPE_VELOCITY_PLAYER, TypeToken.BOOLEAN); + public static final DataKey DATA_KEY_IS_HIDDEN_PLAYER_COMMAND = new DataKey<>("btlp:is_hidden_player_command", VelocityData.SCOPE_VELOCITY_PLAYER, TypeToken.BOOLEAN); + public static final DataKey DATA_KEY_IS_HIDDEN_SERVER_CONFIG = new DataKey<>("btlp:is_hidden_server_config", VelocityData.SCOPE_VELOCITY_SERVER, TypeToken.BOOLEAN); + public static final DataKey Permission = new DataKey<>("btlp:permission", VelocityData.SCOPE_VELOCITY_PLAYER, TypeToken.BOOLEAN); + + public static DataKey createBungeeThirdPartyVariableDataKey(String name) { + return ThirdPartyPlaceholderBungee.withParameter(name); + } + + public static DataKey createBungeeThirdPartyServerVariableDataKey(String name) { + return ThirdPartyServerPlaceholderBungee.withParameter(name); + } + + public static DataKey permission(String permission) { + return Permission.withParameter(permission); + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/data/NullDataHolder.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/NullDataHolder.java new file mode 100644 index 00000000..53be589e --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/NullDataHolder.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.data; + +import de.codecrafter47.data.api.DataHolder; +import de.codecrafter47.data.api.DataKey; + +public class NullDataHolder implements DataHolder { + + public static final NullDataHolder INSTANCE = new NullDataHolder(); + + private NullDataHolder() { + + } + + @Override + public V get(DataKey key) { + return null; + } + + @Override + public void addDataChangeListener(DataKey key, Runnable listener) { + + } + + @Override + public void removeDataChangeListener(DataKey key, Runnable listener) { + + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/data/PermissionDataProvider.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/PermissionDataProvider.java new file mode 100644 index 00000000..aa15b0c1 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/PermissionDataProvider.java @@ -0,0 +1,32 @@ +package codecrafter47.bungeetablistplus.data; + +import codecrafter47.bungeetablistplus.player.VelocityPlayer; +import de.codecrafter47.data.api.DataKey; +import de.codecrafter47.data.minecraft.api.MinecraftData; +import de.codecrafter47.data.velocity.api.VelocityData; +import de.codecrafter47.taboverlay.config.player.Player; + +public class PermissionDataProvider extends AbstractCompositeDataProvider { + + public PermissionDataProvider() { + super(BTLPVelocityDataKeys.Permission); + } + + @Override + protected void registerListener(Player player, DataKey key, Runnable listener) { + player.addDataChangeListener(MinecraftData.permission(key.getParameter()), listener); + player.addDataChangeListener(VelocityData.permission(key.getParameter()), listener); + } + + @Override + protected void unregisterListener(Player player, DataKey key, Runnable listener) { + player.removeDataChangeListener(MinecraftData.permission(key.getParameter()), listener); + player.removeDataChangeListener(VelocityData.permission(key.getParameter()), listener); + } + + @Override + protected Boolean computeCompositeData(VelocityPlayer player, DataKey key) { + return player.get(MinecraftData.permission(key.getParameter())) == Boolean.TRUE + || player.get(VelocityData.permission(key.getParameter())) == Boolean.TRUE; + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/data/ServerDataHolder.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/ServerDataHolder.java new file mode 100644 index 00000000..08f326fe --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/ServerDataHolder.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.data; + +import de.codecrafter47.data.api.DataHolder; +import de.codecrafter47.data.api.DataKey; +import de.codecrafter47.data.minecraft.api.MinecraftData; +import de.codecrafter47.data.velocity.api.VelocityData; + +public class ServerDataHolder implements DataHolder { + + private final DataHolder local; + private final DataHolder bridge; + + public ServerDataHolder(DataHolder local, DataHolder bridge) { + this.local = local; + this.bridge = bridge; + } + + @Override + public V get(DataKey key) { + if (key.getScope() == MinecraftData.SCOPE_SERVER) { + return bridge.get(key); + } else if (key.getScope() == VelocityData.SCOPE_VELOCITY_SERVER) { + return local.get(key); + } else { + throw new IllegalArgumentException("Unexpected scope " + key.getScope()); + } + } + + @Override + public void addDataChangeListener(DataKey key, Runnable listener) { + if (key.getScope() == MinecraftData.SCOPE_SERVER) { + bridge.addDataChangeListener(key, listener); + } else if (key.getScope() == VelocityData.SCOPE_VELOCITY_SERVER) { + local.addDataChangeListener(key, listener); + } else { + throw new IllegalArgumentException("Unexpected scope " + key.getScope()); + } + } + + @Override + public void removeDataChangeListener(DataKey key, Runnable listener) { + if (key.getScope() == MinecraftData.SCOPE_SERVER) { + bridge.removeDataChangeListener(key, listener); + } else if (key.getScope() == VelocityData.SCOPE_VELOCITY_SERVER) { + local.removeDataChangeListener(key, listener); + } else { + throw new IllegalArgumentException("Unexpected scope " + key.getScope()); + } + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/data/TrackingDataCache.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/TrackingDataCache.java new file mode 100644 index 00000000..0ab75b9d --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/TrackingDataCache.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.data; + +import com.google.common.collect.Sets; +import de.codecrafter47.data.api.DataCache; +import de.codecrafter47.data.api.DataKey; +import lombok.Getter; + +import java.util.Set; + +public class TrackingDataCache extends DataCache { + @Getter + private Set> activeKeys = Sets.newConcurrentHashSet(); + + @Override + public void addDataChangeListener(DataKey key, Runnable listener) { + if (!hasListeners(key)) { + addActiveKey(key); + } + super.addDataChangeListener(key, listener); + } + + protected void addActiveKey(DataKey key) { + activeKeys.add(key); + } + + @Override + public void removeDataChangeListener(DataKey key, Runnable listener) { + super.removeDataChangeListener(key, listener); + if (!hasListeners(key)) { + removeActiveKey(key); + } + } + + protected void removeActiveKey(DataKey key) { + activeKeys.remove(key); + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractLegacyTabOverlayHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractLegacyTabOverlayHandler.java new file mode 100644 index 00000000..74d73b9c --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractLegacyTabOverlayHandler.java @@ -0,0 +1,850 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.handler; + +import codecrafter47.bungeetablistplus.protocol.PacketHandler; +import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; +import codecrafter47.bungeetablistplus.util.ColorParser; +import codecrafter47.bungeetablistplus.util.ConcurrentBitSet; +import codecrafter47.bungeetablistplus.util.Property119Handler; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableSet; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; +import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; +import com.velocitypowered.proxy.protocol.packet.Team; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; +import de.codecrafter47.taboverlay.Icon; +import de.codecrafter47.taboverlay.config.misc.ChatFormat; +import de.codecrafter47.taboverlay.config.misc.Unchecked; +import de.codecrafter47.taboverlay.handler.*; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import lombok.AllArgsConstructor; +import lombok.val; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.*; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.IntConsumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Implementation of {@link de.codecrafter47.taboverlay.handler.TabOverlayHandler} for pre 1.8 Minecraft Versions. + */ +public abstract class AbstractLegacyTabOverlayHandler implements PacketHandler, TabOverlayHandler { + + + private static final Component[] slotID; + private static final UUID[] slotUUID; + private static final Set slotIDSet = new HashSet<>(); + + private static final Int2ObjectMap> playerListSizeToSupportedSizesMap = new Int2ObjectOpenHashMap<>(); + + + static { + + // add a random character to the player and team names to prevent issues in multi-velocity setup (stupid!). + int random = ThreadLocalRandom.current().nextInt(0x1e00, 0x2c00); + + slotID = new Component[256]; + slotUUID = new UUID[256]; + + for (int i = 0; i < 256; i++) { + String hex = String.format("%02x", i); + slotID[i] = LegacyComponentSerializer.legacySection().deserialize(String.format("§B§T§L§P§%c§%c§%c§r", random, hex.charAt(0), hex.charAt(1))); + slotIDSet.add(slotID[i]); + slotUUID[i] = UUID.randomUUID(); + } + } + + private static final String[][] EMPTY_PROPERTIES = new String[0][]; + private static final String EMPTY_JSON_TEXT = "{\"text\":\"\"}"; + + private static Collection getSupportedSizesByPlayerListSize(int playerListSize) { + Preconditions.checkArgument(playerListSize >= 0, "playerListSize is negative"); + synchronized (playerListSizeToSupportedSizesMap) { + Collection collection = playerListSizeToSupportedSizesMap.get(playerListSize); + if (collection != null) { + return collection; + } + val builder = ImmutableSet.builder(); + if (playerListSize == 0) { + builder.add(new RectangularTabOverlay.Dimension(1, 0)); + } else { + int columns = (playerListSize + 19) / 20; + for (int rows = 0; rows <= 20; rows++) { + builder.add(new RectangularTabOverlay.Dimension(columns, rows)); + } + } + collection = builder.build(); + playerListSizeToSupportedSizesMap.put(playerListSize, collection); + return collection; + } + } + + private final Logger logger; + private final int playerListSize; + private final Executor eventLoopExecutor; + + private final Object2IntMap serverPlayerList = new Object2IntLinkedOpenHashMap<>(); + private final Object2ObjectMap modernServerPlayerList = new Object2ObjectOpenHashMap<>(); + + private boolean is13OrLater; + + private final Queue> nextActiveHandlerQueue = new ConcurrentLinkedQueue<>(); + private AbstractContentOperationModeHandler activeHandler; + + private final AtomicBoolean updateScheduledFlag = new AtomicBoolean(false); + private final Runnable updateTask = this::update; + + AbstractLegacyTabOverlayHandler(Logger logger, int playerListSize, Executor eventLoopExecutor, boolean is13OrLater) { + this.logger = logger; + this.eventLoopExecutor = eventLoopExecutor; + this.is13OrLater = is13OrLater; + Preconditions.checkElementIndex(playerListSize, 256, "playerListSize"); + this.playerListSize = playerListSize; + this.activeHandler = new PassThroughHandlerContent(); + } + + protected abstract void sendPacket(MinecraftPacket packet); + + @Override + public PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { + if (packet.getAction() == LegacyPlayerListItem.ADD_PLAYER) { + for (LegacyPlayerListItem.Item item : packet.getItems()) { + if (item.getUuid() != null) { + modernServerPlayerList.put(item.getUuid(), new ModernPlayerListEntry(item.getName(), item.getLatency(), item.getGameMode())); + } else { + serverPlayerList.put(getName(item), item.getLatency()); + } + } + } else { + for (LegacyPlayerListItem.Item item : packet.getItems()) { + if (item.getUuid() != null) { + modernServerPlayerList.remove(item.getUuid()); + } else { + serverPlayerList.removeInt(getName(item)); + } + } + } + return activeHandler.onPlayerListPacket(packet); + } + + @Override + public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { + if (packet.getActions().contains(UpsertPlayerInfo.Action.ADD_PLAYER)) { + for (UpsertPlayerInfo.Entry entry : packet.getEntries()) { + if (entry.getProfileId() != null) { + modernServerPlayerList.put(entry.getProfileId(), new ModernPlayerListEntry(entry.getProfile().getName(), entry.getLatency(), entry.getGameMode())); + } else { + serverPlayerList.put(getName(entry), entry.getLatency()); + } + } + } + return activeHandler.onPlayerListUpdatePacket(packet); + } + + @Override + public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet) { + for (UUID uuid : packet.getProfilesToRemove()) { + modernServerPlayerList.remove(uuid); + } + + return activeHandler.onPlayerListRemovePacket(packet); + } + + @Override + public PacketListenerResult onTeamPacket(Team packet) { + if (slotIDSet.contains(packet.getName())) { + logger.log(Level.WARNING, "Team name collision, using multi-velocity setup? Packet: {0}", packet); + return PacketListenerResult.CANCEL; + } + return PacketListenerResult.PASS; + } + + @Override + public PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { + logger.log(Level.WARNING, "1.7 players should not receive tab list header/ footer"); + return PacketListenerResult.CANCEL; + } + + @Override + public void onServerSwitch(boolean is13OrLater) { + this.is13OrLater = is13OrLater; + this.activeHandler.onServerSwitch(); + serverPlayerList.clear(); + modernServerPlayerList.clear(); + } + + @Override + public R enterContentOperationMode(ContentOperationMode operationMode) { + AbstractContentOperationModeHandler handler; + if (operationMode == ContentOperationMode.PASS_TROUGH) { + handler = new PassThroughHandlerContent(); + } else if (operationMode == ContentOperationMode.SIMPLE) { + handler = new SimpleContentOperationModeHandler(); + } else if (operationMode == ContentOperationMode.RECTANGULAR) { + handler = new RectangularSizeHandlerContent(); + } else { + throw new AssertionError("Missing operation mode handler for " + operationMode.getName()); + } + nextActiveHandlerQueue.add(handler); + scheduleUpdate(); + return Unchecked.cast(handler.getTabOverlay()); + } + + @Override + public R enterHeaderAndFooterOperationMode(HeaderAndFooterOperationMode operationMode) { + return Unchecked.cast(DummyHeaderFooterHandle.INSTANCE); + } + + private void scheduleUpdate() { + if (this.updateScheduledFlag.compareAndSet(false, true)) { + try { + eventLoopExecutor.execute(updateTask); + } catch (RejectedExecutionException ignored) { + } + } + } + + private void update() { + this.updateScheduledFlag.set(false); + AbstractContentOperationModeHandler handler; + while (null != (handler = nextActiveHandlerQueue.poll())) { + this.activeHandler.invalidate(); + this.activeHandler = handler; + this.activeHandler.onActivated(); + } + this.activeHandler.update(); + } + + private void removeEntry(UUID uuid, Component player) { + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(); + item.setName(PlainTextComponentSerializer.plainText().serialize(player)); + item.setDisplayName(player); + item.setLatency(9999); + LegacyPlayerListItem pli = new LegacyPlayerListItem(LegacyPlayerListItem.REMOVE_PLAYER, Collections.singletonList(item)); + sendPacket(pli); + } + + private abstract static class AbstractContentOperationModeHandler extends OperationModeHandler { + + abstract PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet); + + abstract void onServerSwitch(); + + abstract void update(); + + final void invalidate() { + getTabOverlay().invalidate(); + onDeactivated(); + } + + abstract void onDeactivated(); + + abstract void onActivated(); + + public abstract PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet); + + public abstract PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet); + } + + private abstract static class AbstractTabOverlay implements TabOverlayHandle { + private boolean valid = true; + + @Override + public boolean isValid() { + return valid; + } + + final void invalidate() { + valid = false; + } + } + + private class PassThroughHandlerContent extends AbstractContentOperationModeHandler { + + @Override + PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { + return PacketListenerResult.PASS; + } + + @Override + public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { + return PacketListenerResult.PASS; + } + + @Override + public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet) { + return PacketListenerResult.PASS; + } + + @Override + void onActivated() { + for (val entry : serverPlayerList.object2IntEntrySet()) { + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(); + item.setDisplayName(entry.getKey()); // TODO: Check Formatting + item.setLatency(entry.getIntValue()); + LegacyPlayerListItem pli = new LegacyPlayerListItem(LegacyPlayerListItem.ADD_PLAYER, Collections.singletonList(item)); + sendPacket(pli); + } + for (val entry : modernServerPlayerList.entrySet()) { + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(entry.getKey()); + item.setName(entry.getValue().name); + item.setGameMode(entry.getValue().gamemode); + item.setLatency(entry.getValue().latency); + Property119Handler.setProperties(item, EMPTY_PROPERTIES); + LegacyPlayerListItem pli = new LegacyPlayerListItem(LegacyPlayerListItem.ADD_PLAYER, Collections.singletonList(item)); + sendPacket(pli); + } + } + + @Override + void onDeactivated() { + removeAllEntries(); + } + + private void removeAllEntries() { + for (Component player : serverPlayerList.keySet()) { + removeEntry(null, player); + } + for (UUID player : modernServerPlayerList.keySet()) { + removeEntry(player, null); + } + } + + @Override + public void onServerSwitch() { + removeAllEntries(); + } + + @Override + void update() { + // nothing to do + } + + @Override + public PassThroughTabOverlay createTabOverlay() { + return new PassThroughTabOverlay(); + } + } + + private static final class PassThroughTabOverlay extends AbstractTabOverlay { + + } + + private abstract class CustomTabOverlayHandlerContent extends AbstractContentOperationModeHandler { + private final IntConsumer updateTextTask; + private final IntConsumer updatePingTask; + private int size = 0; + + private CustomTabOverlayHandlerContent() { + this.updateTextTask = index -> updateText(getTabOverlay(), index); + this.updatePingTask = index -> updatePing(getTabOverlay(), index); + } + + @Override + PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { + return PacketListenerResult.CANCEL; + } + + @Override + public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { + return PacketListenerResult.CANCEL; + } + + @Override + public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet) { + return PacketListenerResult.CANCEL; + } + + @Override + void onServerSwitch() { + // nothing to do + } + + @Override + void update() { + CustomTabOverlayHandlerContent.this.updateSize(); + getTabOverlay().dirtyFlagsText.iterateAndClear(updateTextTask); + getTabOverlay().dirtyFlagsPing.iterateAndClear(updatePingTask); + } + + @Override + void onActivated() { + // nothing to do + } + + @Override + void onDeactivated() { + for (int index = this.size - 1; index >= 0; index--) { + removeEntry(slotUUID[index], slotID[index]); + Team t = new Team(); + t.setName(PlainTextComponentSerializer.plainText().serialize(slotID[index])); + t.setMode((byte) 1); + sendPacket(t); + } + } + + private void updateSize() { + CustomTabOverlay tabOverlay = getTabOverlay(); + int size = tabOverlay.size; + if (size != this.size) { + if (size > this.size) { + for (int index = this.size; index < size; index++) { + tabOverlay.dirtyFlagsText.clear(index); + tabOverlay.dirtyFlagsPing.clear(index); + // create new slot + updateSlot(tabOverlay, index); + Team t = new Team(); + t.setName(PlainTextComponentSerializer.plainText().serialize(slotID[index])); + t.setMode((byte) 0); + t.setPrefix(tabOverlay.text0[index]); + t.setDisplayName(""); + t.setSuffix(tabOverlay.text1[index]); + t.setPlayers(new String[]{PlainTextComponentSerializer.plainText().serialize(slotID[index])}); + t.setNameTagVisibility("always"); + t.setCollisionRule("always"); + if (is13OrLater) { + t.setDisplayName(EMPTY_JSON_TEXT); + t.setPrefix("{\"text\":\"" + tabOverlay.text0[index] + "\"}"); + t.setSuffix("{\"text\":\"" + tabOverlay.text1[index] + "\"}"); + } + sendPacket(t); + } + } else { + for (int index = this.size - 1; index >= size; index--) { + removeEntry(slotUUID[index], slotID[index]); + Team t = new Team(); + t.setName(PlainTextComponentSerializer.plainText().serialize(slotID[index])); + t.setMode((byte) 1); + sendPacket(t); + } + } + this.size = size; + } + } + + private void updateSlot(CustomTabOverlay tabOverlay, int index) { + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUUID[index]); + item.setName(PlainTextComponentSerializer.plainText().serialize(slotID[index])); + Property119Handler.setProperties(item, EMPTY_PROPERTIES); + item.setDisplayName(slotID[index]); // TODO: Check Formatting + item.setLatency(tabOverlay.ping[index]); + LegacyPlayerListItem pli = new LegacyPlayerListItem(LegacyPlayerListItem.ADD_PLAYER, Collections.singletonList(item)); + sendPacket(pli); + } + + private void updateText(CustomTabOverlay tabOverlay, int index) { + if (index < size) { + Team packet = new Team(); + packet.setName(PlainTextComponentSerializer.plainText().serialize(slotID[index])); + packet.setMode((byte) 2); + packet.setPrefix(tabOverlay.text0[index]); + packet.setDisplayName(""); + packet.setSuffix(tabOverlay.text1[index]); + packet.setNameTagVisibility("always"); + packet.setCollisionRule("always"); + if (is13OrLater) { + packet.setDisplayName(EMPTY_JSON_TEXT); + packet.setPrefix("{\"text\":\"" + tabOverlay.text0[index] + "\"}"); + packet.setSuffix("{\"text\":\"" + tabOverlay.text1[index] + "\"}"); + } + sendPacket(packet); + } + } + + private void updatePing(CustomTabOverlay tabOverlay, int index) { + if (index < size) { + updateSlot(tabOverlay, index); + } + } + } + + private abstract class CustomTabOverlay extends AbstractTabOverlay implements TabOverlayHandle.BatchModifiable { + + final String[] text0; + final String[] text1; + final int[] ping; + final AtomicInteger batchUpdateRecursionLevel; + final ConcurrentBitSet dirtyFlagsText; + final ConcurrentBitSet dirtyFlagsPing; + + protected int size; + + private CustomTabOverlay() { + this.batchUpdateRecursionLevel = new AtomicInteger(0); + this.text0 = new String[playerListSize]; + this.text1 = new String[playerListSize]; + this.ping = new int[playerListSize]; + this.dirtyFlagsText = new ConcurrentBitSet(playerListSize); + this.dirtyFlagsPing = new ConcurrentBitSet(playerListSize); + this.size = 0; + } + + @Override + public void beginBatchModification() { + if (isValid()) { + if (batchUpdateRecursionLevel.incrementAndGet() < 0) { + throw new AssertionError("Recursion level overflow"); + } + } + } + + @Override + public void completeBatchModification() { + if (isValid()) { + int level = batchUpdateRecursionLevel.decrementAndGet(); + if (level == 0) { + scheduleUpdate(); + } else if (level < 0) { + throw new AssertionError("Recursion level underflow"); + } + } + } + + private void scheduleUpdateIfNotInBatch() { + if (batchUpdateRecursionLevel.get() == 0) { + scheduleUpdate(); + } + } + + void setTextInternal(int index, String text) { + // convert to legacy format + text = ChatFormat.formattedTextToLegacy(text); + + // split string into two parts of at most 16 characters each to be displayed as prefix and suffix + String text0, text1; + if (text.length() <= 16) { + text0 = text; + text1 = ""; + } else { + int end = text.charAt(15) == LegacyComponentSerializer.SECTION_CHAR ? 15 : 16; + text0 = text.substring(0, end); + int start = ColorParser.endofColor(text, end); + String colors = ColorParser.extractColorCodes(text.substring(0, start)); + end = start + 16 - colors.length(); + if (end >= text.length()) { + end = text.length(); + } + text1 = colors + text.substring(start, end); + } + + if (!text0.equals(this.text0[index]) || !text1.equals(this.text1[index])) { + this.text0[index] = text0; + this.text1[index] = text1; + dirtyFlagsText.set(index); + scheduleUpdateIfNotInBatch(); + } + } + + void setPingInternal(int index, int ping) { + if (ping != this.ping[index]) { + this.ping[index] = ping; + dirtyFlagsPing.set(index); + scheduleUpdateIfNotInBatch(); + } + } + + void setSizeInternal(int newSize) { + int oldSize = this.size; + if (newSize > oldSize) { + for (int index = oldSize; index < newSize; index++) { + text0[index] = ""; + text1[index] = ""; + ping[index] = 0; + } + } + this.size = newSize; + if (newSize < oldSize) { + for (int index = oldSize - 1; index >= newSize; index--) { + text0[index] = ""; + text1[index] = ""; + ping[index] = 0; + } + } + scheduleUpdateIfNotInBatch(); + } + } + + private class RectangularSizeHandlerContent extends CustomTabOverlayHandlerContent { + + @Override + public RectangularTabOverlayImpl createTabOverlay() { + return new RectangularTabOverlayImpl(); + } + } + + private final class RectangularTabOverlayImpl extends CustomTabOverlay implements RectangularTabOverlay { + + private final Collection supportedSizes; + + private Dimension sizeAsDimension; + + private RectangularTabOverlayImpl() { + super(); + this.supportedSizes = getSupportedSizesByPlayerListSize(playerListSize); + Optional dimensionZero = supportedSizes.stream().filter(size -> size.getSize() == 0).findAny(); + if (!dimensionZero.isPresent()) { + throw new AssertionError(); + } + this.sizeAsDimension = dimensionZero.get(); + } + + @Override + public Dimension getSize() { + return sizeAsDimension; + } + + @Override + public Collection getSupportedSizes() { + return supportedSizes; + } + + @Override + public void setSize(@Nonnull Dimension size) { + if (!getSupportedSizes().contains(size)) { + throw new IllegalArgumentException("Unsupported size " + size); + } + if (isValid() && !this.sizeAsDimension.equals(size)) { + this.sizeAsDimension = size; + setSizeInternal(size.getSize()); + } + } + + @Override + public void setSlot(int column, int row, @Nullable UUID uuid, @Nonnull Icon icon, @Nonnull String text, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(column, sizeAsDimension.getColumns(), "column"); + Preconditions.checkElementIndex(row, sizeAsDimension.getRows(), "row"); + int index = row * sizeAsDimension.getColumns() + column; + + beginBatchModification(); + try { + setTextInternal(index, text); + setPingInternal(index, ping); + } finally { + completeBatchModification(); + } + } + } + + @Override + public void setUuid(int column, int row, UUID uuid) { + // nothing to do - not supported in 1.7 + } + + @Override + public void setIcon(int column, int row, @Nonnull Icon icon) { + // nothing to do - not supported in 1.7 + } + + @Override + public void setText(int column, int row, @Nonnull String text) { + if (isValid()) { + Preconditions.checkElementIndex(column, sizeAsDimension.getColumns(), "column"); + Preconditions.checkElementIndex(row, sizeAsDimension.getRows(), "row"); + int index = row * sizeAsDimension.getColumns() + column; + setTextInternal(index, text); + } + } + + @Override + public void setPing(int column, int row, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(column, sizeAsDimension.getColumns(), "column"); + Preconditions.checkElementIndex(row, sizeAsDimension.getRows(), "row"); + int index = row * sizeAsDimension.getColumns() + column; + setPingInternal(index, ping); + } + } + + } + + private class SimpleContentOperationModeHandler extends CustomTabOverlayHandlerContent { + + @Override + public SimpleTabOverlayImpl createTabOverlay() { + return new SimpleTabOverlayImpl(); + } + } + + private final class SimpleTabOverlayImpl extends CustomTabOverlay implements SimpleTabOverlay { + + @Override + public int getSize() { + return size; + } + + @Override + public int getMaxSize() { + return playerListSize; + } + + @Override + public void setSize(int size) { + if (size < 0 || size > playerListSize) { + throw new IllegalArgumentException("Unsupported size " + size); + } + if (isValid() && this.size != size) { + setSizeInternal(size); + } + } + + @Override + public void setSlot(int index, @Nullable UUID uuid, @Nonnull Icon icon, @Nonnull String text, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(index, size); + + beginBatchModification(); + try { + setTextInternal(index, text); + setPingInternal(index, ping); + } finally { + completeBatchModification(); + } + } + } + + @Override + public void setSlot(int index, @Nonnull Icon icon, @Nonnull String text, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(index, size); + + beginBatchModification(); + try { + setTextInternal(index, text); + setPingInternal(index, ping); + } finally { + completeBatchModification(); + } + } + } + + @Override + public void setUuid(int index, UUID uuid) { + // nothing to do - not supported in 1.7 + } + + @Override + public void setIcon(int index, @Nonnull Icon icon) { + // nothing to do - not supported in 1.7 + } + + @Override + public void setText(int index, @Nonnull String text) { + if (isValid()) { + Preconditions.checkElementIndex(index, size); + setTextInternal(index, text); + } + } + + @Override + public void setPing(int index, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(index, size); + setPingInternal(index, ping); + } + } + + } + + private static class DummyHeaderFooterHandle implements HeaderAndFooterHandle { + private static final DummyHeaderFooterHandle INSTANCE = new DummyHeaderFooterHandle(); + + @Override + public void setHeaderFooter(@Nullable String header, @Nullable String footer) { + // dummy + } + + @Override + public void setHeader(@Nullable String header) { + // dummy + } + + @Override + public void setFooter(@Nullable String footer) { + // dummy + } + + @Override + public void beginBatchModification() { + // dummy + } + + @Override + public void completeBatchModification() { + // dummy + } + + @Override + public boolean isValid() { + // dummy + return true; + } + } + + /** + * Utility method to get the name from an {@link LegacyPlayerListItem.Item}. + * + * @param item the item + * @return the name + */ + private static Component getName(LegacyPlayerListItem.Item item) { + if (item.getDisplayName() != null) { + return item.getDisplayName(); + } else if (item.getName() != null) { + return Component.text(item.getName()); + } else { + throw new AssertionError("DisplayName and Username are null"); + } + } + + private static Component getName(UpsertPlayerInfo.Entry entry) { + if (entry.getDisplayName() != null) { + return entry.getDisplayName(); + } else if (entry.getProfile().getName() != null) { + return Component.text(entry.getProfile().getName()); + } else { + throw new AssertionError("DisplayName and Username are null"); + } + } + + @AllArgsConstructor + private static class ModernPlayerListEntry { + String name; + int latency; + int gamemode; + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java new file mode 100644 index 00000000..968737f8 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java @@ -0,0 +1,2562 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.handler; + +import codecrafter47.bungeetablistplus.protocol.PacketHandler; +import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; +import codecrafter47.bungeetablistplus.util.BitSet; +import codecrafter47.bungeetablistplus.util.ConcurrentBitSet; +import codecrafter47.bungeetablistplus.util.Property119Handler; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; +import com.velocitypowered.proxy.protocol.packet.Team; +import de.codecrafter47.taboverlay.Icon; +import de.codecrafter47.taboverlay.ProfileProperty; +import de.codecrafter47.taboverlay.config.misc.ChatFormat; +import de.codecrafter47.taboverlay.config.misc.Unchecked; +import de.codecrafter47.taboverlay.handler.*; +import it.unimi.dsi.fastutil.objects.*; +import lombok.*; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.*; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem.ADD_PLAYER; +import static com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem.REMOVE_PLAYER; +import static com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem.UPDATE_DISPLAY_NAME; +import static com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem.UPDATE_GAMEMODE; +import static com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem.UPDATE_LATENCY; + +public abstract class AbstractTabOverlayHandler implements PacketHandler, TabOverlayHandler { + + // some options + private static final boolean OPTION_ENABLE_CUSTOM_SLOT_USERNAME_COLLISION_CHECK = true; + private static final boolean OPTION_ENABLE_CUSTOM_SLOT_UUID_COLLISION_CHECK = true; + private static final boolean OPTION_ENABLE_CONSISTENCY_CHECKS = true; + + private static final String EMPTY_JSON_TEXT = "{\"text\":\"\"}"; + protected static final String[][] EMPTY_PROPERTIES_ARRAY = new String[0][]; + + private static final boolean TEAM_COLLISION_RULE_SUPPORTED; + private static final boolean USE_PROTOCOL_PROPERTY_TYPE; + + private static final ImmutableMap DIMENSION_TO_USED_SLOTS; + private static final BitSet[] SIZE_TO_USED_SLOTS; + + private static final UUID[] CUSTOM_SLOT_UUID_STEVE; + private static final UUID[] CUSTOM_SLOT_UUID_ALEX; + private static final UUID[] CUSTOM_SLOT_UUID_SPACER; + private static final Set CUSTOM_SLOT_UUIDS; + private static final String[] CUSTOM_SLOT_USERNAME; + private static final String[] CUSTOM_SLOT_USERNAME_SMILEYS; + private static final Set CUSTOM_SLOT_USERNAMES; + private static final String[] CUSTOM_SLOT_TEAMNAME; + private static final Set CUSTOM_SLOT_TEAMNAMES; + + private static final Set blockedTeams = new HashSet<>(); + + static { + TEAM_COLLISION_RULE_SUPPORTED = true; + USE_PROTOCOL_PROPERTY_TYPE = true; + + // build the dimension to used slots map (for the rectangular tab overlay) + val builder = ImmutableMap.builder(); + for (int columns = 1; columns <= 4; columns++) { + for (int rows = 0; rows <= 20; rows++) { + if (columns != 1 && rows != 0 && columns * rows <= (columns - 1) * 20) + continue; + BitSet usedSlots = new BitSet(80); + for (int column = 0; column < columns; column++) { + for (int row = 0; row < rows; row++) { + usedSlots.set(index(column, row)); + } + } + builder.put(new RectangularTabOverlay.Dimension(columns, rows), usedSlots); + } + } + DIMENSION_TO_USED_SLOTS = builder.build(); + + // build the size to used slots map (for the simple tab overlay) + SIZE_TO_USED_SLOTS = new BitSet[81]; + for (int size = 0; size <= 80; size++) { + BitSet usedSlots = new BitSet(80); + usedSlots.set(0, size); + SIZE_TO_USED_SLOTS[size] = usedSlots; + } + + // generate random uuids for our custom slots + CUSTOM_SLOT_UUID_ALEX = new UUID[80]; + CUSTOM_SLOT_UUID_STEVE = new UUID[80]; + CUSTOM_SLOT_UUID_SPACER = new UUID[17]; + UUID base = UUID.randomUUID(); + long msb = base.getMostSignificantBits(); + long lsb = base.getLeastSignificantBits(); + lsb ^= base.hashCode(); + for (int i = 0; i < 80; i++) { + CUSTOM_SLOT_UUID_STEVE[i] = new UUID(msb, lsb ^ (2 * i)); + CUSTOM_SLOT_UUID_ALEX[i] = new UUID(msb, lsb ^ (2 * i + 1)); + } + for (int i = 0; i < 17; i++) { + CUSTOM_SLOT_UUID_SPACER[i] = new UUID(msb, lsb ^ (160 + i)); + } + if (OPTION_ENABLE_CUSTOM_SLOT_UUID_COLLISION_CHECK) { + CUSTOM_SLOT_UUIDS = ImmutableSet.builder() + .add(CUSTOM_SLOT_UUID_ALEX) + .add(CUSTOM_SLOT_UUID_STEVE) + .add(CUSTOM_SLOT_UUID_SPACER).build(); + } else { + CUSTOM_SLOT_UUIDS = null; + } + + // generate usernames for custom slots + int unique = ThreadLocalRandom.current().nextInt(); + CUSTOM_SLOT_USERNAME = new String[81]; + for (int i = 0; i < 81; i++) { + CUSTOM_SLOT_USERNAME[i] = String.format("~BTLP%08x %02d", unique, i); + } + if (OPTION_ENABLE_CUSTOM_SLOT_USERNAME_COLLISION_CHECK) { + CUSTOM_SLOT_USERNAMES = ImmutableSet.copyOf(CUSTOM_SLOT_USERNAME); + } else { + CUSTOM_SLOT_USERNAMES = null; + } + CUSTOM_SLOT_USERNAME_SMILEYS = new String[80]; + String emojis = "\u263a\u2639\u2620\u2763\u2764\u270c\u261d\u270d\u2618\u2615\u2668\u2693\u2708\u231b\u231a\u2600\u2b50\u2601\u2602\u2614\u26a1\u2744\u2603\u2604\u2660\u2665\u2666\u2663\u265f\u260e\u2328\u2709\u270f\u2712\u2702\u2692\u2694\u2699\u2696\u2697\u26b0\u26b1\u267f\u26a0\u2622\u2623\u2640\u2642\u267e\u267b\u269c\u303d\u2733\u2734\u2747\u203c\u2b1c\u2b1b\u25fc\u25fb\u25aa\u25ab\u2049\u26ab\u26aa\u3030\u00a9\u00ae\u2122\u2139\u24c2\u3297\u2716\u2714\u2611\u2695\u2b06\u2197\u27a1\u2198\u2b07\u2199\u3299\u2b05\u2196\u2195\u2194\u21a9\u21aa\u2934\u2935\u269b\u2721\u2638\u262f\u271d\u2626\u262a\u262e\u2648\u2649\u264a\u264b\u264c\u264d\u264e\u264f\u2650\u2651\u2652\u2653\u25b6\u25c0\u23cf"; + for (int i = 0; i < 80; i++) { + CUSTOM_SLOT_USERNAME_SMILEYS[i] = String.format("" + emojis.charAt(i), unique, i); + } + + // generate teams for custom slots + CUSTOM_SLOT_TEAMNAME = new String[81]; + for (int i = 0; i < 81; i++) { + CUSTOM_SLOT_TEAMNAME[i] = String.format(" BTLP%08x %02d", unique, i); + } + CUSTOM_SLOT_TEAMNAMES = ImmutableSet.copyOf(CUSTOM_SLOT_TEAMNAME); + } + + private final Logger logger; + private final Executor eventLoopExecutor; + private final UUID viewerUuid; + + private final Object2ObjectMap serverPlayerList = new Object2ObjectOpenHashMap<>(); + protected final Set serverTabListPlayers = new ObjectOpenHashSet<>(); + @Nullable + protected String serverHeader = null; + @Nullable + protected String serverFooter = null; + protected final Object2ObjectMap serverTeams = new Object2ObjectOpenHashMap<>(); + protected final Object2ObjectMap playerToTeamMap = new Object2ObjectOpenHashMap<>(); + + private final Queue> nextActiveContentHandlerQueue = new ConcurrentLinkedQueue<>(); + private final Queue> nextActiveHeaderFooterHandlerQueue = new ConcurrentLinkedQueue<>(); + private AbstractContentOperationModeHandler activeContentHandler; + private AbstractHeaderFooterOperationModeHandler activeHeaderFooterHandler; + + private boolean hasCreatedCustomTeams = false; + private boolean areCustomSlotUsersPartOfTeams = false; + + private final AtomicBoolean updateScheduledFlag = new AtomicBoolean(false); + private final Runnable updateTask = this::update; + + private final boolean is18; + private boolean is13OrLater; + private boolean is119OrLater; + protected boolean active; + + public AbstractTabOverlayHandler(Logger logger, Executor eventLoopExecutor, UUID viewerUuid, boolean is18, boolean is13OrLater, boolean is119OrLater) { + this.logger = logger; + this.eventLoopExecutor = eventLoopExecutor; + this.viewerUuid = viewerUuid; + this.is18 = is18; + this.is13OrLater = is13OrLater; + this.is119OrLater = is119OrLater; + this.activeContentHandler = new PassThroughContentHandler(); + this.activeHeaderFooterHandler = new PassThroughHeaderFooterHandler(); + } + + protected abstract void sendPacket(MinecraftPacket packet); + + @Override + public PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { + switch (packet.getAction()) { + case ADD_PLAYER: + for (LegacyPlayerListItem.Item item : packet.getItems()) { + if (OPTION_ENABLE_CUSTOM_SLOT_UUID_COLLISION_CHECK) { + if (CUSTOM_SLOT_UUIDS.contains(item.getUuid())) { + throw new AssertionError("UUID collision " + item.getUuid()); + } + } + if (OPTION_ENABLE_CUSTOM_SLOT_USERNAME_COLLISION_CHECK) { + if (CUSTOM_SLOT_USERNAMES.contains(item.getName())) { + throw new AssertionError("Username collision" + item.getName()); + } + } + PlayerListEntry old = serverPlayerList.put(item.getUuid(), new PlayerListEntry(item)); + if (old != null) { + serverTabListPlayers.remove(old.getUsername()); + } + serverTabListPlayers.add(item.getName()); + } + break; + case UPDATE_GAMEMODE: + for (LegacyPlayerListItem.Item item : packet.getItems()) { + PlayerListEntry playerListEntry = serverPlayerList.get(item.getUuid()); + if (playerListEntry != null) { + playerListEntry.setGamemode(item.getGameMode()); + } + } + break; + case UPDATE_LATENCY: + for (LegacyPlayerListItem.Item item : packet.getItems()) { + PlayerListEntry playerListEntry = serverPlayerList.get(item.getUuid()); + if (playerListEntry != null) { + playerListEntry.setPing(item.getLatency()); + } + } + break; + case UPDATE_DISPLAY_NAME: + for (LegacyPlayerListItem.Item item : packet.getItems()) { + PlayerListEntry playerListEntry = serverPlayerList.get(item.getUuid()); + if (playerListEntry != null) { + playerListEntry.setDisplayName(item.getDisplayName()); + } + } + break; + case REMOVE_PLAYER: + for (LegacyPlayerListItem.Item item : packet.getItems()) { + PlayerListEntry removed = serverPlayerList.remove(item.getUuid()); + if (removed != null) { + serverTabListPlayers.remove(removed.getUsername()); + } + } + break; + } + + try { + return this.activeContentHandler.onPlayerListPacket(packet); + } catch (Throwable th) { + logger.log(Level.SEVERE, "Unexpected error", th); + // try recover + enterContentOperationMode(ContentOperationMode.PASS_TROUGH); + return PacketListenerResult.PASS; + } + } + + @Override + public PacketListenerResult onTeamPacket(Team packet) { + + if (packet.getPlayers() != null) { + boolean block = false; + for (String player : packet.getPlayers()) { + if (player.equals("")) { + block = true; + } + } + if (block) { + if (!blockedTeams.contains(packet.getName())) { + logger.warning("Blocking Team Packet for Team " + packet.getName() + ", as it is incompatible with BungeeTabListPlus."); + blockedTeams.add(packet.getName()); + } + return PacketListenerResult.CANCEL; + } + } + + try { + this.activeContentHandler.onTeamPreprocess(packet); + } catch (Throwable th) { + logger.log(Level.SEVERE, "Unexpected error", th); + // try recover + enterContentOperationMode(ContentOperationMode.PASS_TROUGH); + } + + if (packet.getMode() == 1) { + TeamEntry team = serverTeams.remove(packet.getName()); + if (team != null) { + for (String player : team.getPlayers()) { + playerToTeamMap.remove(player, packet.getName()); + } + } + } else { + // Create or get old team + TeamEntry teamEntry; + if (packet.getMode() == 0) { + teamEntry = new TeamEntry(); + serverTeams.put(packet.getName(), teamEntry); + } else { + teamEntry = serverTeams.get(packet.getName()); + } + + if (teamEntry != null) { + if (packet.getMode() == 0 || packet.getMode() == 2) { + teamEntry.setDisplayName(packet.getDisplayName()); + teamEntry.setPrefix(packet.getPrefix()); + teamEntry.setSuffix(packet.getSuffix()); + teamEntry.setFriendlyFire(packet.getFriendlyFire()); + teamEntry.setNameTagVisibility(packet.getNameTagVisibility()); + if (TEAM_COLLISION_RULE_SUPPORTED) { + teamEntry.setCollisionRule(packet.getCollisionRule()); + } + teamEntry.setColor(packet.getColor()); + } + if (packet.getPlayers() != null) { + for (String s : packet.getPlayers()) { + if (packet.getMode() == 0 || packet.getMode() == 3) { + if (playerToTeamMap.containsKey(s)) { + TeamEntry previousTeam = serverTeams.get(playerToTeamMap.get(s)); + // previousTeam shouldn't be null (that's inconsistent with playerToTeamMap, but apparently it happens) + if (previousTeam != null) { + previousTeam.removePlayer(s); + } + } + teamEntry.addPlayer(s); + playerToTeamMap.put(s, packet.getName()); + } else { + teamEntry.removePlayer(s); + playerToTeamMap.remove(s, packet.getName()); + } + } + } + } + } + + try { + return this.activeContentHandler.onTeam(packet); + } catch (Throwable th) { + logger.log(Level.SEVERE, "Unexpected error", th); + // try recover + enterContentOperationMode(ContentOperationMode.PASS_TROUGH); + return PacketListenerResult.PASS; + } + } + + @Override + public PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { + PacketListenerResult result = PacketListenerResult.PASS; + try { + result = this.activeHeaderFooterHandler.onPlayerListHeaderFooterPacket(packet); + if (result == PacketListenerResult.MODIFIED) { + throw new AssertionError("PacketListenerResult.MODIFIED must not be used"); + } + } catch (Throwable th) { + logger.log(Level.SEVERE, "Unexpected error", th); + // try recover + enterHeaderAndFooterOperationMode(HeaderAndFooterOperationMode.PASS_TROUGH); + } + + this.serverHeader = packet.getHeader() != null ? packet.getHeader() : EMPTY_JSON_TEXT; + this.serverFooter = packet.getFooter() != null ? packet.getFooter() : EMPTY_JSON_TEXT; + + return result; + } + + @Override + public void onServerSwitch(boolean is13OrLater) { + this.is13OrLater = is13OrLater; + if (!active) { + active = true; + update(); + } else { + + serverTeams.clear(); + playerToTeamMap.clear(); + + if (isUsingAltRespawn()) { + hasCreatedCustomTeams = false; + areCustomSlotUsersPartOfTeams = false; + } + + try { + this.activeContentHandler.onServerSwitch(); + } catch (Throwable th) { + logger.log(Level.SEVERE, "Unexpected error", th); + // try recover + enterContentOperationMode(ContentOperationMode.PASS_TROUGH); + } + try { + this.activeHeaderFooterHandler.onServerSwitch(); + } catch (Throwable th) { + logger.log(Level.SEVERE, "Unexpected error", th); + // try recover + enterContentOperationMode(ContentOperationMode.PASS_TROUGH); + } + + if (!serverPlayerList.isEmpty()) { + List items = new ArrayList<>(); + for(UUID uuid : serverPlayerList.keySet()){ + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(uuid); + items.add(item); + } + LegacyPlayerListItem packet = new LegacyPlayerListItem(REMOVE_PLAYER, items); + sendPacket(packet); + } + + serverPlayerList.clear(); + if (serverHeader != null) { + serverHeader = EMPTY_JSON_TEXT; + } + if (serverFooter != null) { + serverFooter = EMPTY_JSON_TEXT; + } + + serverTabListPlayers.clear(); + } + } + + protected boolean isUsingAltRespawn() { + return false; + } + + @Override + public R enterContentOperationMode(ContentOperationMode operationMode) { + AbstractContentOperationModeHandler handler; + if (operationMode == ContentOperationMode.PASS_TROUGH) { + handler = new PassThroughContentHandler(); + } else if (operationMode == ContentOperationMode.SIMPLE) { + handler = new SimpleOperationModeHandler(); + } else if (operationMode == ContentOperationMode.RECTANGULAR) { + handler = new RectangularSizeHandler(); + } else { + throw new UnsupportedOperationException(); + } + nextActiveContentHandlerQueue.add(handler); + scheduleUpdate(); + return Unchecked.cast(handler.getTabOverlay()); + } + + @Override + public R enterHeaderAndFooterOperationMode(HeaderAndFooterOperationMode operationMode) { + AbstractHeaderFooterOperationModeHandler handler; + if (operationMode == HeaderAndFooterOperationMode.PASS_TROUGH) { + handler = new PassThroughHeaderFooterHandler(); + } else if (operationMode == HeaderAndFooterOperationMode.CUSTOM) { + handler = new CustomHeaderAndFooterOperationModeHandler(); + } else { + throw new UnsupportedOperationException(Objects.toString(operationMode)); + } + nextActiveHeaderFooterHandlerQueue.add(handler); + scheduleUpdate(); + return Unchecked.cast(handler.getTabOverlay()); + } + + private void scheduleUpdate() { + if (this.updateScheduledFlag.compareAndSet(false, true)) { + try { + eventLoopExecutor.execute(updateTask); + } catch (RejectedExecutionException ignored) { + } + } + } + + private void update() { + if (!active) { + return; + } + updateScheduledFlag.set(false); + + // update content handler + AbstractContentOperationModeHandler contentHandler; + while (null != (contentHandler = nextActiveContentHandlerQueue.poll())) { + this.activeContentHandler.invalidate(); + contentHandler.onActivated(this.activeContentHandler); + this.activeContentHandler = contentHandler; + } + this.activeContentHandler.update(); + + // update header and footer handler + AbstractHeaderFooterOperationModeHandler heaerFooterHandler; + while (null != (heaerFooterHandler = nextActiveHeaderFooterHandlerQueue.poll())) { + this.activeHeaderFooterHandler.invalidate(); + heaerFooterHandler.onActivated(this.activeHeaderFooterHandler); + this.activeHeaderFooterHandler = heaerFooterHandler; + } + this.activeHeaderFooterHandler.update(); + } + + private abstract class AbstractContentOperationModeHandler extends OperationModeHandler { + + /** + * Called when the player receives a {@link LegacyPlayerListItem} packet. + *

+ * This method is called after this {@link AbstractTabOverlayHandler} has updated the {@code serverPlayerList}. + */ + abstract PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet); + + /** + * Called when the player receives a {@link Team} packet. + *

+ * This method is called before this {@link AbstractTabOverlayHandler} executes its own logic to update the + * server team info. + */ + abstract void onTeamPreprocess(Team packet); + + /** + * Called when the player receives a {@link Team} packet. + *

+ * This method is called after this {@link AbstractTabOverlayHandler} executes its own logic to update the + * server team info. + */ + abstract PacketListenerResult onTeam(Team packet); + + /** + * Called when the player switches the server. + *

+ * This method is called before this {@link AbstractTabOverlayHandler} executes its own logic to clear the + * server player list info. + */ + abstract void onServerSwitch(); + + abstract void update(); + + final void invalidate() { + getTabOverlay().invalidate(); + onDeactivated(); + } + + /** + * Called when this {@link OperationModeHandler} is deactivated. + *

+ * This method must put the client player list in the state expected by {@link #onActivated(AbstractContentOperationModeHandler)}. It must + * especially remove all custom entries and players must be part of the correct teams. + */ + abstract void onDeactivated(); + + /** + * Called when this {@link OperationModeHandler} becomes the active one. + *

+ * State of the player list when this method is called: + * - there are no custom entries on the client + * - all entries from {@link #serverPlayerList} are known to the client, but the client may know the wrong displayname, gamemode and ping + * - player list header/ footer may be wrong + *

+ * Additional information about the state of the player list may be obtained from the previous handler + * + * @param previous previous handler + */ + abstract void onActivated(AbstractContentOperationModeHandler previous); + } + + private abstract class AbstractHeaderFooterOperationModeHandler extends OperationModeHandler { + + /** + * Called when the player receives a {@link HeaderAndFooter} packet. + *

+ * This method is called before this {@link AbstractTabOverlayHandler} executes its own logic to update the + * server player list info. + */ + abstract PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet); + + /** + * Called when the player switches the server. + *

+ * This method is called before this {@link AbstractTabOverlayHandler} executes its own logic to clear the + * server player list info. + */ + abstract void onServerSwitch(); + + abstract void update(); + + final void invalidate() { + getTabOverlay().invalidate(); + onDeactivated(); + } + + /** + * Called when this {@link OperationModeHandler} is deactivated. + *

+ * This method must put the client player list in the state expected by {@link #onActivated(AbstractHeaderFooterOperationModeHandler)}. It must + * especially remove all custom entries and players must be part of the correct teams. + */ + abstract void onDeactivated(); + + /** + * Called when this {@link OperationModeHandler} becomes the active one. + *

+ * State of the player list when this method is called: + * - there are no custom entries on the client + * - all entries from {@link #serverPlayerList} are known to the client, but the client may know the wrong displayname, gamemode and ping + * - player list header/ footer may be wrong + *

+ * Additional information about the state of the player list may be obtained from the previous handler + * + * @param previous previous handler + */ + abstract void onActivated(AbstractHeaderFooterOperationModeHandler previous); + } + + private abstract static class AbstractContentTabOverlay implements TabOverlayHandle { + private boolean valid = true; + + @Override + public boolean isValid() { + return valid; + } + + final void invalidate() { + valid = false; + } + } + + private abstract static class AbstractHeaderFooterTabOverlay implements TabOverlayHandle { + private boolean valid = true; + + @Override + public boolean isValid() { + return valid; + } + + final void invalidate() { + valid = false; + } + } + + private final class PassThroughContentHandler extends AbstractContentOperationModeHandler { + + @Override + protected PassThroughContentTabOverlay createTabOverlay() { + return new PassThroughContentTabOverlay(); + } + + @Override + PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { + return PacketListenerResult.PASS; + } + + @Override + void onTeamPreprocess(Team packet) { + // nothing to do + } + + @Override + PacketListenerResult onTeam(Team packet) { + return PacketListenerResult.PASS; + } + + @Override + void onServerSwitch() { + sendPacket(new HeaderAndFooter(EMPTY_JSON_TEXT, EMPTY_JSON_TEXT)); + } + + @Override + void update() { + // nothing to do + } + + @Override + void onDeactivated() { + // nothing to do + } + + @Override + void onActivated(AbstractContentOperationModeHandler previous) { + if (previous instanceof PassThroughContentHandler) { + // we're lucky, nothing to do + return; + } + + // fix player list entries + if (!serverPlayerList.isEmpty()) { + // restore player ping + LegacyPlayerListItem packet; + List items = new ArrayList<>(serverPlayerList.size()); + for (PlayerListEntry entry : serverPlayerList.values()) { + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(entry.getUuid()); + item.setLatency(entry.getPing()); + items.add(item); + } + packet = new LegacyPlayerListItem(UPDATE_LATENCY, items); + sendPacket(packet); + + // restore player gamemode + items.clear(); + for (PlayerListEntry entry : serverPlayerList.values()) { + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(entry.getUuid()); + item.setGameMode(entry.getGamemode()); + items.add(item); + } + packet = new LegacyPlayerListItem(UPDATE_GAMEMODE, items); + sendPacket(packet); + + // restore player display name + items.clear(); + for (PlayerListEntry entry : serverPlayerList.values()) { + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(entry.getUuid()); + item.setDisplayName(entry.getDisplayName()); + items.add(item); + } + packet = new LegacyPlayerListItem(UPDATE_DISPLAY_NAME, items); + sendPacket(packet); + } + } + } + + private final class PassThroughContentTabOverlay extends AbstractContentTabOverlay { + + } + + private final class PassThroughHeaderFooterHandler extends AbstractHeaderFooterOperationModeHandler { + + @Override + protected PassThroughHeaderFooterTabOverlay createTabOverlay() { + return new PassThroughHeaderFooterTabOverlay(); + } + + @Override + PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { + return PacketListenerResult.PASS; + } + + @Override + void onServerSwitch() { + sendPacket(new HeaderAndFooter(EMPTY_JSON_TEXT, EMPTY_JSON_TEXT)); + } + + @Override + void update() { + // nothing to do + } + + @Override + void onDeactivated() { + // nothing to do + } + + @Override + void onActivated(AbstractHeaderFooterOperationModeHandler previous) { + if (previous instanceof PassThroughHeaderFooterHandler) { + // we're lucky, nothing to do + return; + } + + // fix header/ footer + sendPacket(new HeaderAndFooter(serverHeader != null ? serverHeader : EMPTY_JSON_TEXT, serverFooter != null ? serverFooter : EMPTY_JSON_TEXT)); + } + } + + private final class PassThroughHeaderFooterTabOverlay extends AbstractHeaderFooterTabOverlay { + + } + + private abstract class CustomContentTabOverlayHandler extends AbstractContentOperationModeHandler { + + boolean viewerIsSpectator = false; + final ObjectSet freePlayers; + + @Nonnull + BitSet usedSlots; + BitSet dirtySlots; + int highestUsedSlotIndex; + private boolean using80Slots; + private int usedSlotsCount; + final SlotState[] slotState; + /** + * Uuid of the player list entry used for the slot. + */ + final UUID[] slotUuid; + /** + * Username of the player list entry used for the slot. + */ + final String[] slotUsername; + /** + * Player uuid mapped to the slot it is used for + */ + final Object2IntMap playerUuidToSlotMap; + /** + * Player username mapped to the slot it is used for + */ + final Object2IntMap playerUsernameToSlotMap; + boolean canShrink = false; + + private final List itemQueueAddPlayer; + private final List itemQueueRemovePlayer; + private final List itemQueueUpdateDisplayName; + private final List itemQueueUpdatePing; + + private final boolean experimentalTabCompleteFixForTabSize80 = isExperimentalTabCompleteFixForTabSize80(); + private final boolean experimentalTabCompleteSmileys = isExperimentalTabCompleteSmileys(); + + private CustomContentTabOverlayHandler() { + this.dirtySlots = new BitSet(80); + this.usedSlots = SIZE_TO_USED_SLOTS[0]; + this.usedSlotsCount = 0; + this.using80Slots = false; + this.slotState = new SlotState[80]; + Arrays.fill(this.slotState, SlotState.UNUSED); + this.slotUuid = new UUID[80]; + this.slotUsername = new String[80]; + this.highestUsedSlotIndex = -1; + this.freePlayers = new ObjectOpenHashSet<>(); + this.playerUuidToSlotMap = new Object2IntOpenHashMap<>(); + this.playerUuidToSlotMap.defaultReturnValue(-1); + this.playerUsernameToSlotMap = new Object2IntOpenHashMap<>(); + this.playerUsernameToSlotMap.defaultReturnValue(-1); + this.itemQueueAddPlayer = new ArrayList<>(80); + this.itemQueueRemovePlayer = new ArrayList<>(80); + this.itemQueueUpdateDisplayName = new ArrayList<>(80); + this.itemQueueUpdatePing = new ArrayList<>(80); + } + + @Override + PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { + + int action = packet.getAction(); + + if (using80Slots && action == UPDATE_GAMEMODE) { + return PacketListenerResult.PASS; + } + + // check whether viewer gamemode changed + boolean viewerGamemodeChanged = false; + T tabOverlay = getTabOverlay(); + if (action == ADD_PLAYER || action == UPDATE_GAMEMODE || action == REMOVE_PLAYER) { + PlayerListEntry entry = serverPlayerList.get(viewerUuid); + boolean viewerIsSpectator = entry != null && entry.getGamemode() == 3; + if (this.viewerIsSpectator != viewerIsSpectator) { + this.viewerIsSpectator = viewerIsSpectator; + if (!using80Slots) { + if (highestUsedSlotIndex >= 0) { + dirtySlots.set(highestUsedSlotIndex); + } + + if (viewerIsSpectator) { + // mark player slot as dirty + int i = playerUuidToSlotMap.getInt(viewerUuid); + if (i >= 0) { + dirtySlots.set(i); + } + } else { + // mark slots with player uuid as dirty + if (action == UPDATE_GAMEMODE) { + // if action is ADD_PLAYER slots are marked dirty below, so only do it here if action is UPDATE_GAMEMODE + for (int slot = 0; slot < 80; slot++) { + UUID uuid = tabOverlay.uuid[slot]; + if (viewerUuid.equals(uuid)) { + dirtySlots.set(slot); + } + } + } + } + } + viewerGamemodeChanged = true; + } + } + + PacketListenerResult result; + boolean needUpdate = !using80Slots && viewerGamemodeChanged; + + switch (action) { + case ADD_PLAYER: + List items = packet.getItems(); + if (!using80Slots) { + for (LegacyPlayerListItem.Item item : items) { + if (!viewerUuid.equals(item.getUuid())) { + item.setGameMode(0); + } + } + + for (LegacyPlayerListItem.Item item : items) { + UUID uuid = item.getUuid(); + int index = playerUuidToSlotMap.getInt(uuid); + if (index == -1) { + freePlayers.add(uuid); + needUpdate = true; + } else if (!item.getName().equals(slotUsername[index])) { + dirtySlots.set(index); + needUpdate = true; + } else { + item.setDisplayName(tabOverlay.text[index]); // TODO: check formatting + item.setLatency(tabOverlay.ping[index]); + tabOverlay.dirtyFlagsText.clear(index); + tabOverlay.dirtyFlagsPing.clear(index); + } + } + + // mark slot to use for player as dirty + // a new player joining the server shouldn't happen too frequently, so we can accept + // the cost of searching all 80 slots + if (!freePlayers.isEmpty()) { + for (int slot = 0; slot < 80; slot++) { + UUID uuid = tabOverlay.uuid[slot]; + if (uuid != null && freePlayers.contains(uuid)) { + dirtySlots.set(slot); + } + } + } + + // request size update if tab list too small + if (usedSlotsCount < serverPlayerList.size()) { + tabOverlay.dirtyFlagSize = true; + } + } else { + for (LegacyPlayerListItem.Item item : packet.getItems()) { + if (!playerToTeamMap.containsKey(item.getName())) { + sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[80], new String[]{item.getName()})); + } + } + } + if (needUpdate) { + sendPacket(packet); + result = PacketListenerResult.CANCEL; + } else { + result = PacketListenerResult.MODIFIED; + } + break; + case UPDATE_GAMEMODE: + if (viewerGamemodeChanged) { + items = packet.getItems(); + for (LegacyPlayerListItem.Item item : items) { + if (!viewerUuid.equals(item.getUuid())) { + item.setGameMode(0); + } + } + sendPacket(packet); + } + result = PacketListenerResult.CANCEL; + break; + case UPDATE_LATENCY: + result = PacketListenerResult.CANCEL; + break; + case UPDATE_DISPLAY_NAME: + result = PacketListenerResult.CANCEL; + break; + case REMOVE_PLAYER: + if (!using80Slots) { + items = packet.getItems(); + for (LegacyPlayerListItem.Item item : items) { + int index = playerUuidToSlotMap.removeInt(item.getUuid()); + if (index == -1) { + if (OPTION_ENABLE_CONSISTENCY_CHECKS) { + if (serverPlayerList.containsKey(item.getUuid())) { + logger.severe("Inconsistent data: player in serverPlayerList but not in playerUuidToSlotMap"); + } + } + } else { + // Switch slot 'index' from player to custom mode - restoring player teams + + // 1. remove player from team + if (item.getUuid().version() != 2) { // hack for Citizens compatibility + sendPacket(createPacketTeamRemovePlayers(CUSTOM_SLOT_TEAMNAME[index], new String[]{slotUsername[index]})); + playerUsernameToSlotMap.removeInt(slotUsername[index]); + String playerTeamName; + if ((playerTeamName = playerToTeamMap.get(slotUsername[index])) != null) { + // 2. add player to correct team + sendPacket(createPacketTeamAddPlayers(playerTeamName, new String[]{slotUsername[index]})); + // 3. reset custom slot team + if (is13OrLater) { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); + } else { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], "", "", "", "always", "always", 0, (byte) 1)); + } + } + } + + // 4. create new custom slot + tabOverlay.dirtyFlagsIcon.clear(index); + tabOverlay.dirtyFlagsText.clear(index); + tabOverlay.dirtyFlagsPing.clear(index); + Icon icon = tabOverlay.icon[index]; + UUID customSlotUuid; + if (icon.isAlex()) { + customSlotUuid = CUSTOM_SLOT_UUID_ALEX[index]; + } else { + customSlotUuid = CUSTOM_SLOT_UUID_STEVE[index]; + } + slotState[index] = SlotState.CUSTOM; + slotUuid[index] = customSlotUuid; + LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); + item1.setName(slotUsername[index] = getCustomSlotUsername(index)); + Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); + item1.setDisplayName(tabOverlay.text[index]); // TODO: Check formatting + item1.setLatency(tabOverlay.ping[index]); + item1.setGameMode(0); + LegacyPlayerListItem packet1 = new LegacyPlayerListItem(ADD_PLAYER, Collections.singletonList(item1)); + sendPacket(packet1); + if (is18) { + packet1 = new LegacyPlayerListItem(UPDATE_DISPLAY_NAME, Collections.singletonList(item1)); + sendPacket(packet1); + } + } + } + } + if (canShrink) { + sendPacket(packet); + tabOverlay.dirtyFlagSize = true; + needUpdate = true; + result = PacketListenerResult.CANCEL; + } else { + result = PacketListenerResult.PASS; + } + break; + default: + throw new AssertionError("Unknown action: " + action); + } + if (needUpdate) { + update(); + } + return result; + } + + private String getCustomSlotUsername(int index) { + if (experimentalTabCompleteFixForTabSize80 && using80Slots) { + return ""; + } + if (experimentalTabCompleteSmileys) { + return CUSTOM_SLOT_USERNAME_SMILEYS[index]; + } else { + return CUSTOM_SLOT_USERNAME[index]; + } + } + + @Override + void onTeamPreprocess(Team packet) { + if (!using80Slots) { + if (packet.getMode() == 1) { + TeamEntry teamEntry = serverTeams.get(packet.getName()); + if (teamEntry != null) { + for (String playerName : teamEntry.getPlayers()) { + int slot = playerUsernameToSlotMap.getInt(playerName); + if (slot != -1) { + // reset slot team + if (is13OrLater) { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[slot], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); + } else { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[slot], "", "", "", "always", "always", 0, (byte) 1)); + } + } + } + } + } + } else { + if (packet.getMode() == 1) { + TeamEntry teamEntry = serverTeams.get(packet.getName()); + if (teamEntry != null) { + for (String playerName : teamEntry.getPlayers()) { + if (serverTabListPlayers.contains(playerName)) { + // reset slot team + sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[80], new String[]{playerName})); + } + } + } + } + } + } + + @Override + PacketListenerResult onTeam(Team packet) { + if (CUSTOM_SLOT_TEAMNAMES.contains(packet.getName())) { + throw new AssertionError("Team name collision: " + packet); + } + if (!using80Slots) { + boolean modified = false; + switch (packet.getMode()) { + case 0: + case 3: + int count = 0; + String[] players = packet.getPlayers(); + for (int i = 0; i < players.length; i++) { + String playerName = players[i]; + if (-1 == playerUsernameToSlotMap.getInt(playerName)) { + count++; + } + } + if (count < players.length) { + modified = true; + String[] filteredPlayers = new String[count]; + int j = 0; + for (int i = 0; i < players.length; i++) { + String playerName = players[i]; + int slot; + if (-1 == (slot = playerUsernameToSlotMap.getInt(playerName))) { + filteredPlayers[j++] = playerName; + } else { + // update slot team + TeamEntry teamEntry = serverTeams.get(packet.getName()); + if (teamEntry != null) { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[slot], teamEntry.getDisplayName(), teamEntry.getPrefix(), teamEntry.getSuffix(), teamEntry.getNameTagVisibility(), teamEntry.getCollisionRule(), teamEntry.getColor(), teamEntry.getFriendlyFire())); + } + } + } + packet.setPlayers(filteredPlayers); + } + break; + case 4: + count = 0; + players = packet.getPlayers(); + for (int i = 0; i < players.length; i++) { + String playerName = players[i]; + if (-1 == playerUsernameToSlotMap.getInt(playerName)) { + count++; + } + } + if (count < players.length) { + modified = true; + String[] filteredPlayers = new String[count]; + int j = 0; + for (int i = 0; i < players.length; i++) { + String playerName = players[i]; + int slot; + if (-1 == (slot = playerUsernameToSlotMap.getInt(playerName))) { + filteredPlayers[j++] = playerName; + } else { + // reset slot team + if (is13OrLater) { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[slot], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); + } else { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[slot], "", "", "", "always", "always", 0, (byte) 1)); + } + } + } + packet.setPlayers(filteredPlayers); + } + break; + case 2: + TeamEntry teamEntry = serverTeams.get(packet.getName()); + if (teamEntry != null) { + for (String playerName : teamEntry.getPlayers()) { + int slot = playerUsernameToSlotMap.getInt(playerName); + if (slot != -1) { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[slot], teamEntry.getDisplayName(), teamEntry.getPrefix(), teamEntry.getSuffix(), teamEntry.getNameTagVisibility(), teamEntry.getCollisionRule(), teamEntry.getColor(), teamEntry.getFriendlyFire())); + } + } + } + + break; + + } + if (modified) { + return PacketListenerResult.MODIFIED; + } + } else { + + switch (packet.getMode()) { + case 0: + case 3: + /* + // Don't need this. Adding the player to another team will remove him from the current one. + String[] players = packet.getPlayers(); + for (int i = 0; i < players.length; i++) { + String playerName = players[i]; + if (serverTabListPlayers.contains(playerName)) { + // remove player from overflow team + sendPacket(createPacketTeamRemovePlayers(CUSTOM_SLOT_TEAMNAME[80], new String[]{playerName})); + } + }*/ + break; + case 4: + String[] players = packet.getPlayers(); + for (int i = 0; i < players.length; i++) { + String playerName = players[i]; + if (serverTabListPlayers.contains(playerName)) { + // add player to overflow team + sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[80], new String[]{playerName})); + } + } + break; + } + } + return PacketListenerResult.PASS; + } + + @Override + void onServerSwitch() { + boolean altRespawn = isUsingAltRespawn(); + + if (altRespawn) { + createTeamsIfNecessary(); + } + + if (!using80Slots) { + T tabOverlay = getTabOverlay(); + + // all players are gone + for (int index = 0; index < 80; index++) { + if (slotState[index] == SlotState.PLAYER) { + // Switch slot 'index' from player to custom mode + + if (!altRespawn) { + // 1. remove player from team + sendPacket(createPacketTeamRemovePlayers(CUSTOM_SLOT_TEAMNAME[index], new String[]{slotUsername[index]})); + // reset slot team + if (is13OrLater) { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); + } else { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], "", "", "", "always", "always", 0, (byte) 1)); + } + } + + // 2. create new custom slot + tabOverlay.dirtyFlagsIcon.clear(index); + tabOverlay.dirtyFlagsText.clear(index); + tabOverlay.dirtyFlagsPing.clear(index); + Icon icon = tabOverlay.icon[index]; + UUID customSlotUuid; + if (icon.isAlex()) { + customSlotUuid = CUSTOM_SLOT_UUID_ALEX[index]; + } else { + customSlotUuid = CUSTOM_SLOT_UUID_STEVE[index]; + } + slotState[index] = SlotState.CUSTOM; + slotUuid[index] = customSlotUuid; + LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); + item1.setName(slotUsername[index] = getCustomSlotUsername(index)); + Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); + item1.setDisplayName(tabOverlay.text[index]); // TODO: Check formatting + item1.setLatency(tabOverlay.ping[index]); + item1.setGameMode(0); + LegacyPlayerListItem packet1 = new LegacyPlayerListItem(ADD_PLAYER, Collections.singletonList(item1)); + sendPacket(packet1); + if (is18) { + packet1 = new LegacyPlayerListItem(UPDATE_DISPLAY_NAME, Collections.singletonList(item1)); + sendPacket(packet1); + } + } + } + freePlayers.clear(); + playerUuidToSlotMap.clear(); + playerUsernameToSlotMap.clear(); + } + viewerIsSpectator = false; + } + + @Override + void onActivated(AbstractContentOperationModeHandler previous) { + if (previous instanceof CustomContentTabOverlayHandler) { + this.viewerIsSpectator = ((CustomContentTabOverlayHandler) previous).viewerIsSpectator; + } else { + PlayerListEntry viewerEntry = serverPlayerList.get(viewerUuid); + this.viewerIsSpectator = viewerEntry != null && viewerEntry.getGamemode() == 3; + + // switch all players except for viewer in survival mode if they are in spectator mode + if (!using80Slots) { + int count = 0; + for (PlayerListEntry entry : serverPlayerList.values()) { + if (entry != viewerEntry && entry.getGamemode() == 3) { + count++; + } + } + + if (count > 0) { + LegacyPlayerListItem.Item[] items = new LegacyPlayerListItem.Item[count]; + int index = 0; + + for (Map.Entry mEntry : serverPlayerList.entrySet()) { + PlayerListEntry entry = mEntry.getValue(); + if (entry != viewerEntry && entry.getGamemode() == 3) { + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(mEntry.getKey()); + item.setGameMode(0); + items[index++] = item; + } + } + + LegacyPlayerListItem packet = new LegacyPlayerListItem(UPDATE_GAMEMODE, Arrays.asList(items)); + sendPacket(packet); + } + } + createTeamsIfNecessary(); + + } + + if (!using80Slots) { + this.freePlayers.addAll(serverPlayerList.keySet()); + + if (!this.freePlayers.isEmpty()) { + + for (PlayerListEntry entry : serverPlayerList.values()) { + if (!playerToTeamMap.containsKey(entry.username)) { + sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[80], new String[]{entry.username})); + } + } + getTabOverlay().dirtyFlagSize = true; + } + } + } + + private void createTeamsIfNecessary() { + // create teams if not already created + if (!hasCreatedCustomTeams) { + hasCreatedCustomTeams = true; + + if (is13OrLater) { + sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[0], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[0], CUSTOM_SLOT_USERNAME_SMILEYS[0], ""})); + } else { + sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[0], "", "", "", "always", "always", 0, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[0], CUSTOM_SLOT_USERNAME_SMILEYS[0], ""})); + } + + for (int i = 1; i < 80; i++) { + if (is13OrLater) { + sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[i], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[i], CUSTOM_SLOT_USERNAME_SMILEYS[i]})); + } else { + sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[i], "", "", "", "always", "always", 0, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[i], CUSTOM_SLOT_USERNAME_SMILEYS[i]})); + } + } + if (is13OrLater) { + sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[80], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[80]})); + } else { + sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[80], "", "", "", "always", "always", 0, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[80]})); + } + + areCustomSlotUsersPartOfTeams = true; + } + } + + @Override + void onDeactivated() { + int customSlots = 0; + for (int index = 0; index < 80; index++) { + if (slotState[index] != SlotState.UNUSED) { + if (slotState[index] == SlotState.PLAYER) { + // switch slot from player to unused permanently freeing the associated player + + // 1. remove player from team + sendPacket(createPacketTeamRemovePlayers(CUSTOM_SLOT_TEAMNAME[index], new String[]{slotUsername[index]})); + String playerTeamName; + if ((playerTeamName = playerToTeamMap.get(slotUsername[index])) != null) { + // 2. add player to correct team + sendPacket(createPacketTeamAddPlayers(playerTeamName, new String[]{slotUsername[index]})); + // 3. reset custom slot team + if (is13OrLater) { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); + } else { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], "", "", "", "always", "always", 0, (byte) 1)); + } + } + } else { + customSlots++; + } + } + } + + if (using80Slots) { + for (String player : serverTabListPlayers) { + if (playerToTeamMap.get(player) == null) { + // remove player from overflow team + sendPacket(createPacketTeamRemovePlayers(CUSTOM_SLOT_TEAMNAME[80], new String[]{player})); + } + } + // account for spacer players + if (experimentalTabCompleteFixForTabSize80) { + customSlots += 17; + } + } + + int i = 0; + if (customSlots > 0) { + LegacyPlayerListItem.Item[] items = new LegacyPlayerListItem.Item[customSlots]; + for (int index = 0; index < 80; index++) { + // switch slot from custom to unused + if (slotState[index] == SlotState.CUSTOM) { + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUuid[index]); + items[i++] = item; + } + } + if (experimentalTabCompleteFixForTabSize80 && using80Slots) { + for (int j = 0; j < 17; j++) { + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(CUSTOM_SLOT_UUID_SPACER[j]); + items[i++] = item; + } + } + LegacyPlayerListItem packet = new LegacyPlayerListItem(REMOVE_PLAYER, Arrays.asList(items)); + sendPacket(packet); + } + } + + @Override + void update() { + T tabOverlay = getTabOverlay(); + + if (OPTION_ENABLE_CONSISTENCY_CHECKS) { + if (!using80Slots && usedSlotsCount < serverPlayerList.size() && !tabOverlay.dirtyFlagSize) { + logger.severe("tabOverlay.dirtyFlagSize not set but resize required"); + tabOverlay.dirtyFlagSize = true; + } + } + + boolean updateAllCustomSlots = false; + + if (tabOverlay.dirtyFlagSize) { + tabOverlay.dirtyFlagSize = false; + updateSize(); + if (usedSlotsCount != (usedSlotsCount = usedSlots.cardinality())) { + + if (using80Slots) { + // when previously using 80 slots + for (int index = 0; index < 80; index++) { + if (tabOverlay.uuid[index] != null) { + dirtySlots.set(index); + } + } + freePlayers.addAll(serverPlayerList.keySet()); + + // switch all players except for viewer in survival mode if they are in spectator mode + PlayerListEntry viewerEntry = serverPlayerList.get(viewerUuid); + int count = 0; + for (PlayerListEntry entry : serverPlayerList.values()) { + if (entry != viewerEntry && entry.getGamemode() == 3) { + count++; + } + } + + if (count > 0) { + LegacyPlayerListItem.Item[] items = new LegacyPlayerListItem.Item[count]; + int index = 0; + + for (Map.Entry mEntry : serverPlayerList.entrySet()) { + PlayerListEntry entry = mEntry.getValue(); + if (entry != viewerEntry && entry.getGamemode() == 3) { + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(mEntry.getKey()); + item.setGameMode(0); + items[index++] = item; + } + } + + LegacyPlayerListItem packet = new LegacyPlayerListItem(UPDATE_GAMEMODE, Arrays.asList(items)); + sendPacket(packet); + } + + // remove spacer slots + if (experimentalTabCompleteFixForTabSize80) { + for (int i = 0; i < 17; i++) { + LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(CUSTOM_SLOT_UUID_SPACER[i]); + itemQueueRemovePlayer.add(item1); + } + } + + dirtySlots.set(0, 80); + updateAllCustomSlots = true; + } else if (viewerIsSpectator && highestUsedSlotIndex >= 0) { + dirtySlots.set(highestUsedSlotIndex); + } + + highestUsedSlotIndex = usedSlots.previousSetBit(79); + using80Slots = this.usedSlotsCount == 80; + if (using80Slots) { + // we switched to 80 slots + for (int index = 0; index < 80; index++) { + if (slotState[index] == SlotState.PLAYER) { + + // 1. remove player from team + sendPacket(createPacketTeamRemovePlayers(CUSTOM_SLOT_TEAMNAME[index], new String[]{slotUsername[index]})); + playerUsernameToSlotMap.removeInt(slotUsername[index]); + String playerTeamName; + if ((playerTeamName = playerToTeamMap.get(slotUsername[index])) != null) { + // 2. add player to correct team + sendPacket(createPacketTeamAddPlayers(playerTeamName, new String[]{slotUsername[index]})); + // 3. reset custom slot team + if (is13OrLater) { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); + } else { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], "", "", "", "always", "always", 0, (byte) 1)); + } + } else { + // 2. add player to overflow team + sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[80], new String[]{slotUsername[index]})); + } + + // 4. create new custom slot + tabOverlay.dirtyFlagsIcon.clear(index); + tabOverlay.dirtyFlagsText.clear(index); + tabOverlay.dirtyFlagsPing.clear(index); + Icon icon = tabOverlay.icon[index]; + UUID customSlotUuid; + if (icon.isAlex()) { + customSlotUuid = CUSTOM_SLOT_UUID_ALEX[index]; + } else { + customSlotUuid = CUSTOM_SLOT_UUID_STEVE[index]; + } + slotState[index] = SlotState.CUSTOM; + slotUuid[index] = customSlotUuid; + LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); + item1.setName(slotUsername[index] = getCustomSlotUsername(index)); + Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); + item1.setDisplayName(tabOverlay.text[index]); // TODO: Check Formatting + item1.setLatency(tabOverlay.ping[index]); + item1.setGameMode(0); + itemQueueAddPlayer.add(item1); + } else { + // custom + if (slotState[index] == SlotState.CUSTOM) { + LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(slotUuid[index]); + itemQueueRemovePlayer.add(item1); + } + // unused + tabOverlay.dirtyFlagsIcon.clear(index); + tabOverlay.dirtyFlagsText.clear(index); + tabOverlay.dirtyFlagsPing.clear(index); + Icon icon = tabOverlay.icon[index]; + UUID customSlotUuid; + if (icon.isAlex()) { + customSlotUuid = CUSTOM_SLOT_UUID_ALEX[index]; + } else { + customSlotUuid = CUSTOM_SLOT_UUID_STEVE[index]; + } + slotState[index] = SlotState.CUSTOM; + slotUuid[index] = customSlotUuid; + LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); + item1.setName(slotUsername[index] = getCustomSlotUsername(index)); + Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); + item1.setDisplayName(tabOverlay.text[index]); // TODO: Check Formatting + item1.setLatency(tabOverlay.ping[index]); + item1.setGameMode(0); + itemQueueAddPlayer.add(item1); + } + } + + // restore player gamemode + LegacyPlayerListItem packet; + List items = new ArrayList<>(serverPlayerList.size()); + items.clear(); + for (PlayerListEntry entry : serverPlayerList.values()) { + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(entry.getUuid()); + item.setGameMode(entry.getGamemode()); + items.add(item); + } + packet = new LegacyPlayerListItem(UPDATE_GAMEMODE, items); + sendPacket(packet); + + for (UUID player : freePlayers) { + String username = serverPlayerList.get(player).username; + String playerTeamName = playerToTeamMap.get(username); + if (playerTeamName != null) { + // add player to correct team + sendPacket(createPacketTeamAddPlayers(playerTeamName, new String[]{username})); + } else { + // add player to overflow team + sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[80], new String[]{username})); + } + } + + // create spacer slots + if (experimentalTabCompleteFixForTabSize80) { + for (int i = 0; i < 17; i++) { + LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(CUSTOM_SLOT_UUID_SPACER[i]); + item1.setName(""); + Property119Handler.setProperties(item1, EMPTY_PROPERTIES_ARRAY); + item1.setDisplayName(null); + item1.setLatency(0); + item1.setGameMode(0); + itemQueueAddPlayer.add(item1); + } + } + + // save some memory + freePlayers.clear(); + playerUuidToSlotMap.clear(); + playerUsernameToSlotMap.clear(); + } else { + + if (viewerIsSpectator && highestUsedSlotIndex >= 0) { + dirtySlots.set(highestUsedSlotIndex); + } + } + + sendQueuedItems(); + } + } + + + if (OPTION_ENABLE_CONSISTENCY_CHECKS) { + if (!using80Slots && usedSlotsCount < serverPlayerList.size()) { + throw new AssertionError("resize failed"); + } + } + + if (OPTION_ENABLE_CONSISTENCY_CHECKS) { + if (!using80Slots && freePlayers.size() + playerUuidToSlotMap.size() != serverPlayerList.size()) { + // inconsistent data -> rebuild + logger.severe("Detected inconsistency: freePlayers set or playerUuidToSlotMap is inconsistent"); + freePlayers.clear(); + freePlayers.addAll(serverPlayerList.keySet()); + playerUuidToSlotMap.clear(); + for (int index = 0; index <= highestUsedSlotIndex; index++) { + if (freePlayers.remove(slotUuid[index])) { + playerUuidToSlotMap.put(slotUuid[index], index); + } + } + } + } + + if (!using80Slots) { + dirtySlots.orAndClear(tabOverlay.dirtyFlagsUuid); + + if (!dirtySlots.isEmpty() || !freePlayers.isEmpty()) { + // mark slots as dirty currently being used with the uuid of dirty slots + for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { + int i = index; + do { + UUID uuid = tabOverlay.uuid[i]; + if (uuid == null) { + break; + } + i = playerUuidToSlotMap.getInt(uuid); + if (i == -1) { + break; + } + if (dirtySlots.get(i)) { + break; + } else { + dirtySlots.set(i); + } + } while (i < index); + } + + if (OPTION_ENABLE_CONSISTENCY_CHECKS) { + if (viewerIsSpectator) { + int i; + if (highestUsedSlotIndex != (i = playerUuidToSlotMap.getInt(viewerUuid))) { + if (!dirtySlots.get(highestUsedSlotIndex)) { + logger.severe("Spectator mode handling issue: highestUsedSlotIndex not marked as dirty"); + dirtySlots.set(highestUsedSlotIndex); + } else if (i == -1 && !freePlayers.contains(viewerUuid)) { + logger.severe("Spectator mode handling issue: viewer neither in freePlayers set and nor in tab list"); + } else if (i != -1 && !dirtySlots.get(i)) { + logger.severe("Spectator mode handling issue: viewer slot not marked as dirty"); + dirtySlots.set(i); + } + } + } + } + + // pass 1: free players + for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { + if (usedSlots.get(index)) { + if (slotState[index] == SlotState.PLAYER) { + // temporarily free associated player - set slot state to unused + + // 1. remove player from team + if (slotUuid[index].version() != 2) { // dirty hack for Citizens compatibility + sendPacket(createPacketTeamRemovePlayers(CUSTOM_SLOT_TEAMNAME[index], new String[]{slotUsername[index]})); + playerUsernameToSlotMap.removeInt(slotUsername[index]); + + // reset slot team + if (is13OrLater) { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); + } else { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], "", "", "", "always", "always", 0, (byte) 1)); + } + } + + // 2. update slot state + slotState[index] = SlotState.UNUSED; + freePlayers.add(slotUuid[index]); + slotUuid[index] = null; + slotUsername[index] = null; + } + } else { + if (slotState[index] != SlotState.UNUSED) { + // remove slot - free (temporarily) associated player if any - set slot state to unused + if (slotState[index] == SlotState.PLAYER) { + // temporarily free associated player - set slot state to unused + + // 1. remove player from team + if (slotUuid[index].version() != 2) { // dirty hack for Citizens compatibility + sendPacket(createPacketTeamRemovePlayers(CUSTOM_SLOT_TEAMNAME[index], new String[]{slotUsername[index]})); + playerUsernameToSlotMap.removeInt(slotUsername[index]); + + // reset slot team + if (is13OrLater) { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); + } else { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], "", "", "", "always", "always", 0, (byte) 1)); + } + } + + freePlayers.add(slotUuid[index]); + } else { + // 1. remove custom slot player + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUuid[index]); + itemQueueRemovePlayer.add(item); + } + + // 2. update slot state + slotState[index] = SlotState.UNUSED; + slotUuid[index] = null; + slotUsername[index] = null; + } + } + } + + if (viewerIsSpectator && !viewerUuid.equals(slotUuid[highestUsedSlotIndex])) { + if (slotState[highestUsedSlotIndex] != SlotState.UNUSED) { + if (OPTION_ENABLE_CONSISTENCY_CHECKS) { + if (slotState[highestUsedSlotIndex] == SlotState.PLAYER) { + throw new AssertionError("slotState[highestUsedSlotIndex] == SlotState.PLAYER"); + } + } + // switch slot 'highestUsedSlotIndex' from custom to unused + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUuid[highestUsedSlotIndex]); + itemQueueRemovePlayer.add(item); + } + // switch slot 'highestUsedSlotIndex' from unused to player with 'viewerUuid' + String playerUsername = serverPlayerList.get(viewerUuid).getUsername(); + String playerTeamName; + if (null != (playerTeamName = playerToTeamMap.get(playerUsername))) { + // 1. remove player from old team + sendPacket(createPacketTeamRemovePlayers(playerTeamName, new String[]{playerUsername})); + // 2. update properties of new team + TeamEntry teamEntry = serverTeams.get(playerTeamName); + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[highestUsedSlotIndex], teamEntry.getDisplayName(), teamEntry.getPrefix(), teamEntry.getSuffix(), teamEntry.getNameTagVisibility(), teamEntry.getCollisionRule(), teamEntry.getColor(), teamEntry.getFriendlyFire())); + } + // 3. Add to new team + sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[highestUsedSlotIndex], new String[]{playerUsername})); + // 4. Update display name + LegacyPlayerListItem.Item itemUpdateDisplayName = new LegacyPlayerListItem.Item(viewerUuid); + tabOverlay.dirtyFlagsText.clear(highestUsedSlotIndex); + itemUpdateDisplayName.setDisplayName(tabOverlay.text[highestUsedSlotIndex]); // TODO: Check Formatting + itemQueueUpdateDisplayName.add(itemUpdateDisplayName); + // 5. Update ping + LegacyPlayerListItem.Item itemUpdatePing = new LegacyPlayerListItem.Item(viewerUuid); + tabOverlay.dirtyFlagsPing.clear(highestUsedSlotIndex); + itemUpdatePing.setLatency(tabOverlay.ping[highestUsedSlotIndex]); + itemQueueUpdatePing.add(itemUpdatePing); + // 6. Update slot state + slotState[highestUsedSlotIndex] = SlotState.PLAYER; + slotUuid[highestUsedSlotIndex] = viewerUuid; + slotUsername[highestUsedSlotIndex] = playerUsername; + playerUsernameToSlotMap.put(playerUsername, highestUsedSlotIndex); + playerUuidToSlotMap.put(viewerUuid, highestUsedSlotIndex); + + freePlayers.remove(viewerUuid); + } + + // pass 2: assign players to new slots + for (int repeat = 1; repeat > 0; repeat--) { + for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { + if (usedSlots.get(index) && slotState[index] != SlotState.PLAYER) { + UUID uuid = tabOverlay.uuid[index]; + if (uuid != null && freePlayers.remove(uuid)) { + // switch slot to player mode using player with 'uuid' + if (slotState[index] == SlotState.CUSTOM) { + // custom -> unused + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUuid[index]); + itemQueueRemovePlayer.add(item); + } + String playerUsername = serverPlayerList.get(uuid).getUsername(); + String playerTeamName; + if (null != (playerTeamName = playerToTeamMap.get(playerUsername))) { + // 1. remove player from old team + sendPacket(createPacketTeamRemovePlayers(playerTeamName, new String[]{playerUsername})); + // 2. update properties of new team + TeamEntry teamEntry = serverTeams.get(playerTeamName); + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], teamEntry.getDisplayName(), teamEntry.getPrefix(), teamEntry.getSuffix(), teamEntry.getNameTagVisibility(), teamEntry.getCollisionRule(), teamEntry.getColor(), teamEntry.getFriendlyFire())); + } + // 3. Add to new team + sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[index], new String[]{playerUsername})); + // 4. Update display name + LegacyPlayerListItem.Item itemUpdateDisplayName = new LegacyPlayerListItem.Item(uuid); + tabOverlay.dirtyFlagsText.clear(index); + itemUpdateDisplayName.setDisplayName(tabOverlay.text[index]); // TODO: Check Formatting + itemQueueUpdateDisplayName.add(itemUpdateDisplayName); + // 5. Update ping + LegacyPlayerListItem.Item itemUpdatePing = new LegacyPlayerListItem.Item(uuid); + tabOverlay.dirtyFlagsPing.clear(index); + itemUpdatePing.setLatency(tabOverlay.ping[index]); + itemQueueUpdatePing.add(itemUpdatePing); + // 6. Update slot state + slotState[index] = SlotState.PLAYER; + slotUuid[index] = uuid; + slotUsername[index] = playerUsername; + playerUsernameToSlotMap.put(playerUsername, index); + playerUuidToSlotMap.put(uuid, index); + } + } + } + + // should not happen too often + if (!freePlayers.isEmpty()) { + for (int slot = 0; slot < 80; slot++) { + UUID uuid; + if (slotState[slot] == SlotState.CUSTOM && (uuid = tabOverlay.uuid[slot]) != null && freePlayers.contains(uuid)) { + dirtySlots.set(slot); + repeat = 2; + } + } + } + } + + // pass 3: distribute remaining 'freePlayers' on the tab list + int index = 80; + for (ObjectIterator iterator = freePlayers.iterator(); iterator.hasNext(); ) { + UUID uuid = iterator.next(); + for (index = usedSlots.previousSetBit(index - 1); index >= 0; index = usedSlots.previousSetBit(index - 1)) { + if (slotState[index] != SlotState.PLAYER) { + // switch slot to player mode using the player 'uuid' + if (slotState[index] == SlotState.CUSTOM) { + // custom -> unused + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUuid[index]); + itemQueueRemovePlayer.add(item); + } + String playerUsername = serverPlayerList.get(uuid).getUsername(); + if (uuid.version() != 2) { // dirty hack for Citizens compatibility + String playerTeamName; + if (null != (playerTeamName = playerToTeamMap.get(playerUsername))) { + // 1. remove player from old team + sendPacket(createPacketTeamRemovePlayers(playerTeamName, new String[]{playerUsername})); + // 2. update properties of new team + TeamEntry teamEntry = serverTeams.get(playerTeamName); + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], teamEntry.getDisplayName(), teamEntry.getPrefix(), teamEntry.getSuffix(), teamEntry.getNameTagVisibility(), teamEntry.getCollisionRule(), teamEntry.getColor(), teamEntry.getFriendlyFire())); + } + // 3. Add to new team + sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[index], new String[]{playerUsername})); + playerUsernameToSlotMap.put(playerUsername, index); + } + // 4. Update display name + LegacyPlayerListItem.Item itemUpdateDisplayName = new LegacyPlayerListItem.Item(uuid); + tabOverlay.dirtyFlagsText.clear(index); + itemUpdateDisplayName.setDisplayName(tabOverlay.text[index]); // TODO: Check Formatting + itemQueueUpdateDisplayName.add(itemUpdateDisplayName); + // 5. Update ping + LegacyPlayerListItem.Item itemUpdatePing = new LegacyPlayerListItem.Item(uuid); + tabOverlay.dirtyFlagsPing.clear(index); + itemUpdatePing.setLatency(tabOverlay.ping[index]); + itemQueueUpdatePing.add(itemUpdatePing); + // 6. Update slot state + slotState[index] = SlotState.PLAYER; + slotUuid[index] = uuid; + slotUsername[index] = playerUsername; + playerUuidToSlotMap.put(uuid, index); + iterator.remove(); + break; + } + } + if (index < 0) { + throw new AssertionError("Not enough space on player list."); + } + } + + // pass 4: switch some slots from unused to custom + for (index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { + if (usedSlots.get(index)) { + if (slotState[index] == SlotState.UNUSED || (updateAllCustomSlots && slotState[index] == SlotState.CUSTOM)) { + if (slotState[index] == SlotState.CUSTOM) { + LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(slotUuid[index]); + itemQueueRemovePlayer.add(item1); + } + tabOverlay.dirtyFlagsIcon.clear(index); + tabOverlay.dirtyFlagsText.clear(index); + tabOverlay.dirtyFlagsPing.clear(index); + Icon icon = tabOverlay.icon[index]; + UUID customSlotUuid; + if (icon.isAlex()) { + customSlotUuid = CUSTOM_SLOT_UUID_ALEX[index]; + } else { + customSlotUuid = CUSTOM_SLOT_UUID_STEVE[index]; + } + slotState[index] = SlotState.CUSTOM; + slotUuid[index] = customSlotUuid; + LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); + item1.setName(slotUsername[index] = getCustomSlotUsername(index)); + Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); + item1.setDisplayName(tabOverlay.text[index]); // TODO: Check Formatting + item1.setLatency(tabOverlay.ping[index]); + item1.setGameMode(0); + itemQueueAddPlayer.add(item1); + } + } + } + + // send first packet batch here to avoid conflicts when updating icons + sendQueuedItems(); + } + } + + // update icons + dirtySlots.copyAndClear(tabOverlay.dirtyFlagsIcon); + for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { + if (slotState[index] == SlotState.CUSTOM) { + Icon icon = tabOverlay.icon[index]; + UUID customSlotUuid; + if (icon.hasTextureProperty()) { + customSlotUuid = slotUuid[index]; + } else if (icon.isAlex()) { + customSlotUuid = CUSTOM_SLOT_UUID_ALEX[index]; + } else { // steve + customSlotUuid = CUSTOM_SLOT_UUID_STEVE[index]; + } + if (!customSlotUuid.equals(slotUuid[index]) || is119OrLater) { + LegacyPlayerListItem.Item itemRemove = new LegacyPlayerListItem.Item(slotUuid[index]); + itemQueueRemovePlayer.add(itemRemove); + } + tabOverlay.dirtyFlagsText.clear(index); + tabOverlay.dirtyFlagsPing.clear(index); + slotState[index] = SlotState.CUSTOM; + slotUuid[index] = customSlotUuid; + LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); + item1.setName(slotUsername[index] = getCustomSlotUsername(index)); + Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); + item1.setDisplayName(tabOverlay.text[index]); // TODO: Check Formatting + item1.setLatency(tabOverlay.ping[index]); + item1.setGameMode(0); + itemQueueAddPlayer.add(item1); + } + } + + // update text + dirtySlots.copyAndClear(tabOverlay.dirtyFlagsText); + for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { + if (slotState[index] != SlotState.UNUSED) { + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUuid[index]); + item.setDisplayName(tabOverlay.text[index]); // TODO: Check Formatting + itemQueueUpdateDisplayName.add(item); + } + } + + // update ping + dirtySlots.copyAndClear(tabOverlay.dirtyFlagsPing); + for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { + if (slotState[index] != SlotState.UNUSED) { + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUuid[index]); + item.setLatency(tabOverlay.ping[index]); + itemQueueUpdatePing.add(item); + } + } + + dirtySlots.clear(); + + // send packets + sendQueuedItems(); + } + + private void sendQueuedItems() { + if (!itemQueueRemovePlayer.isEmpty()) { + LegacyPlayerListItem packet = new LegacyPlayerListItem(REMOVE_PLAYER, itemQueueRemovePlayer); + sendPacket(packet); + itemQueueRemovePlayer.clear(); + } + if (!itemQueueAddPlayer.isEmpty()) { + LegacyPlayerListItem packet = new LegacyPlayerListItem(ADD_PLAYER, itemQueueAddPlayer); + sendPacket(packet); + if (is18) { + packet = new LegacyPlayerListItem(UPDATE_DISPLAY_NAME, itemQueueAddPlayer); + sendPacket(packet); + } + itemQueueAddPlayer.clear(); + } + if (!itemQueueUpdateDisplayName.isEmpty()) { + LegacyPlayerListItem packet = new LegacyPlayerListItem(UPDATE_DISPLAY_NAME, itemQueueUpdateDisplayName); + sendPacket(packet); + itemQueueUpdateDisplayName.clear(); + } + if (!itemQueueUpdatePing.isEmpty()) { + LegacyPlayerListItem packet = new LegacyPlayerListItem(UPDATE_LATENCY, itemQueueUpdatePing); + sendPacket(packet); + itemQueueUpdatePing.clear(); + } + } + + /** + * Updates the usedSlots BitSet. Sets the {@link #dirtySlots uuid dirty flag} for all added + * and removed slots. + */ + abstract void updateSize(); + } + + protected abstract boolean isExperimentalTabCompleteSmileys(); + + protected abstract boolean isExperimentalTabCompleteFixForTabSize80(); + + private abstract class CustomContentTabOverlay extends AbstractContentTabOverlay implements TabOverlayHandle.BatchModifiable { + final UUID[] uuid; + final Icon[] icon; + final Component[] text; + final int[] ping; + + final AtomicInteger batchUpdateRecursionLevel; + volatile boolean dirtyFlagSize; + final ConcurrentBitSet dirtyFlagsUuid; + final ConcurrentBitSet dirtyFlagsIcon; + final ConcurrentBitSet dirtyFlagsText; + final ConcurrentBitSet dirtyFlagsPing; + + private CustomContentTabOverlay() { + this.uuid = new UUID[80]; + this.icon = new Icon[80]; + Arrays.fill(this.icon, Icon.DEFAULT_STEVE); + this.text = new Component[80]; + Arrays.fill(this.text, Component.empty()); + this.ping = new int[80]; + this.batchUpdateRecursionLevel = new AtomicInteger(0); + this.dirtyFlagSize = true; + this.dirtyFlagsUuid = new ConcurrentBitSet(80); + this.dirtyFlagsIcon = new ConcurrentBitSet(80); + this.dirtyFlagsText = new ConcurrentBitSet(80); + this.dirtyFlagsPing = new ConcurrentBitSet(80); + } + + @Override + public void beginBatchModification() { + if (isValid()) { + if (batchUpdateRecursionLevel.incrementAndGet() < 0) { + throw new AssertionError("Recursion level overflow"); + } + } + } + + @Override + public void completeBatchModification() { + if (isValid()) { + int level = batchUpdateRecursionLevel.decrementAndGet(); + if (level == 0) { + scheduleUpdate(); + } else if (level < 0) { + throw new AssertionError("Recursion level underflow"); + } + } + } + + void scheduleUpdateIfNotInBatch() { + if (batchUpdateRecursionLevel.get() == 0) { + scheduleUpdate(); + } + } + + void setUuidInternal(int index, @Nullable UUID uuid) { + if (!Objects.equals(uuid, this.uuid[index])) { + this.uuid[index] = uuid; + dirtyFlagsUuid.set(index); + scheduleUpdateIfNotInBatch(); + } + } + + void setIconInternal(int index, @Nonnull @NonNull Icon icon) { + if (!icon.equals(this.icon[index])) { + this.icon[index] = icon; + dirtyFlagsIcon.set(index); + scheduleUpdateIfNotInBatch(); + } + } + + void setTextInternal(int index, @Nonnull @NonNull String text) { + Component jsonText = LegacyComponentSerializer.legacyAmpersand().deserialize(text.replace(LegacyComponentSerializer.SECTION_CHAR, LegacyComponentSerializer.AMPERSAND_CHAR)); + if (!jsonText.equals(this.text[index])) { + this.text[index] = jsonText; + dirtyFlagsText.set(index); + scheduleUpdateIfNotInBatch(); + } + } + + void setPingInternal(int index, int ping) { + if (ping != this.ping[index]) { + this.ping[index] = ping; + dirtyFlagsPing.set(index); + scheduleUpdateIfNotInBatch(); + } + } + } + + private class RectangularSizeHandler extends CustomContentTabOverlayHandler { + + @Override + void updateSize() { + RectangularTabOverlayImpl tabOverlay = getTabOverlay(); + RectangularTabOverlay.Dimension size = tabOverlay.getSize(); + if (size.getSize() < serverPlayerList.size() && size.getSize() != 80) { + for (RectangularTabOverlay.Dimension dimension : tabOverlay.getSupportedSizes()) { + if (dimension.getColumns() < tabOverlay.getSize().getColumns()) + continue; + if (dimension.getRows() < tabOverlay.getSize().getRows()) + continue; + if (size.getSize() < serverPlayerList.size() && size.getSize() != 80) { + size = dimension; + } else if (size.getSize() > dimension.getSize() && dimension.getSize() > serverPlayerList.size()) { + size = dimension; + } + } + canShrink = true; + } else { + canShrink = false; + } + BitSet newUsedSlots = DIMENSION_TO_USED_SLOTS.get(size); + dirtySlots.orXor(usedSlots, newUsedSlots); + usedSlots = newUsedSlots; + } + + @Override + protected RectangularTabOverlayImpl createTabOverlay() { + return new RectangularTabOverlayImpl(); + } + } + + private class RectangularTabOverlayImpl extends CustomContentTabOverlay implements RectangularTabOverlay { + + @Nonnull + private Dimension size; + + private RectangularTabOverlayImpl() { + Optional dimensionZero = getSupportedSizes().stream().filter(size -> size.getSize() == 0).findAny(); + if (!dimensionZero.isPresent()) { + throw new AssertionError(); + } + this.size = dimensionZero.get(); + } + + @Nonnull + @Override + public Dimension getSize() { + return size; + } + + @Override + public Collection getSupportedSizes() { + return DIMENSION_TO_USED_SLOTS.keySet(); + } + + @Override + public void setSize(@Nonnull Dimension size) { + if (!getSupportedSizes().contains(size)) { + throw new IllegalArgumentException("Unsupported size " + size); + } + if (isValid() && !this.size.equals(size)) { + BitSet oldUsedSlots = DIMENSION_TO_USED_SLOTS.get(this.size); + BitSet newUsedSlots = DIMENSION_TO_USED_SLOTS.get(size); + for (int index = newUsedSlots.nextSetBit(0); index >= 0; index = newUsedSlots.nextSetBit(index + 1)) { + if (!oldUsedSlots.get(index)) { + uuid[index] = null; + icon[index] = Icon.DEFAULT_STEVE; + text[index] = Component.empty(); + ping[index] = 0; + } + } + this.size = size; + this.dirtyFlagSize = true; + scheduleUpdateIfNotInBatch(); + for (int index = oldUsedSlots.nextSetBit(0); index >= 0; index = oldUsedSlots.nextSetBit(index + 1)) { + if (!newUsedSlots.get(index)) { + uuid[index] = null; + icon[index] = Icon.DEFAULT_STEVE; + text[index] = Component.empty(); + ping[index] = 0; + } + } + } + } + + @Override + public void setSlot(int column, int row, @Nullable UUID uuid, @Nonnull Icon icon, @Nonnull String text, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(column, size.getColumns(), "column"); + Preconditions.checkElementIndex(row, size.getRows(), "row"); + beginBatchModification(); + try { + int index = index(column, row); + setUuidInternal(index, uuid); + setIconInternal(index, icon); + setTextInternal(index, text); + setPingInternal(index, ping); + } finally { + completeBatchModification(); + } + } + } + + @Override + public void setSlot(int column, int row, @Nonnull Icon icon, @Nonnull String text, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(column, size.getColumns(), "column"); + Preconditions.checkElementIndex(row, size.getRows(), "row"); + beginBatchModification(); + try { + int index = index(column, row); + setUuidInternal(index, null); + setIconInternal(index, icon); + setTextInternal(index, text); + setPingInternal(index, ping); + } finally { + completeBatchModification(); + } + } + } + + @Override + public void setUuid(int column, int row, @Nullable UUID uuid) { + if (isValid()) { + Preconditions.checkElementIndex(column, size.getColumns(), "column"); + Preconditions.checkElementIndex(row, size.getRows(), "row"); + setUuidInternal(index(column, row), uuid); + } + } + + @Override + public void setIcon(int column, int row, @Nonnull Icon icon) { + if (isValid()) { + Preconditions.checkElementIndex(column, size.getColumns(), "column"); + Preconditions.checkElementIndex(row, size.getRows(), "row"); + setIconInternal(index(column, row), icon); + } + } + + @Override + public void setText(int column, int row, @Nonnull String text) { + if (isValid()) { + Preconditions.checkElementIndex(column, size.getColumns(), "column"); + Preconditions.checkElementIndex(row, size.getRows(), "row"); + setTextInternal(index(column, row), text); + } + } + + @Override + public void setPing(int column, int row, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(column, size.getColumns(), "column"); + Preconditions.checkElementIndex(row, size.getRows(), "row"); + setPingInternal(index(column, row), ping); + } + } + } + + private class SimpleOperationModeHandler extends CustomContentTabOverlayHandler { + + @Override + void updateSize() { + int newSize = getTabOverlay().size; + if (newSize != 80 && newSize < serverPlayerList.size()) { + newSize = Integer.min(serverPlayerList.size(), 80); + canShrink = true; + } else { + canShrink = false; + } + if (newSize > highestUsedSlotIndex + 1) { + dirtySlots.set(highestUsedSlotIndex + 1, newSize); + } else if (newSize <= highestUsedSlotIndex) { + dirtySlots.set(newSize, highestUsedSlotIndex + 1); + } + usedSlots = SIZE_TO_USED_SLOTS[newSize]; + } + + @Override + protected SimpleTabOverlayImpl createTabOverlay() { + return new SimpleTabOverlayImpl(); + } + } + + private class SimpleTabOverlayImpl extends CustomContentTabOverlay implements SimpleTabOverlay { + int size = 0; + + @Override + public int getSize() { + return size; + } + + @Override + public int getMaxSize() { + return 80; + } + + @Override + public void setSize(int size) { + if (size < 0 || size > 80) { + throw new IllegalArgumentException("size"); + } + this.size = size; + dirtyFlagSize = true; + scheduleUpdateIfNotInBatch(); + } + + @Override + public void setSlot(int index, @Nullable UUID uuid, @Nonnull Icon icon, @Nonnull String text, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(index, size, "index"); + beginBatchModification(); + try { + setUuidInternal(index, uuid); + setIconInternal(index, icon); + setTextInternal(index, text); + setPingInternal(index, ping); + } finally { + completeBatchModification(); + } + } + } + + @Override + public void setSlot(int index, @Nonnull Icon icon, @Nonnull String text, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(index, size, "index"); + beginBatchModification(); + try { + setUuidInternal(index, null); + setIconInternal(index, icon); + setTextInternal(index, text); + setPingInternal(index, ping); + } finally { + completeBatchModification(); + } + } + } + + @Override + public void setUuid(int index, UUID uuid) { + if (isValid()) { + Preconditions.checkElementIndex(index, size, "index"); + setUuidInternal(index, uuid); + } + } + + @Override + public void setIcon(int index, @Nonnull @NonNull Icon icon) { + if (isValid()) { + Preconditions.checkElementIndex(index, size, "index"); + setIconInternal(index, icon); + } + } + + @Override + public void setText(int index, @Nonnull @NonNull String text) { + if (isValid()) { + Preconditions.checkElementIndex(index, size, "index"); + setTextInternal(index, text); + } + } + + @Override + public void setPing(int index, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(index, size, "index"); + setPingInternal(index, ping); + } + } + } + + private final class CustomHeaderAndFooterOperationModeHandler extends AbstractHeaderFooterOperationModeHandler { + + @Override + protected CustomHeaderAndFooterImpl createTabOverlay() { + return new CustomHeaderAndFooterImpl(); + } + + @Override + PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { + return PacketListenerResult.CANCEL; + } + + @Override + void onServerSwitch() { + // do nothing + } + + @Override + void onDeactivated() { + //do nothing + } + + @Override + void onActivated(AbstractHeaderFooterOperationModeHandler previous) { + // remove header/ footer + sendPacket(new HeaderAndFooter(EMPTY_JSON_TEXT, EMPTY_JSON_TEXT)); + } + + @Override + void update() { + CustomHeaderAndFooterImpl tabOverlay = getTabOverlay(); + if (tabOverlay.headerOrFooterDirty) { + tabOverlay.headerOrFooterDirty = false; + sendPacket(new HeaderAndFooter(tabOverlay.header, tabOverlay.footer)); + } + } + } + + private final class CustomHeaderAndFooterImpl extends AbstractHeaderFooterTabOverlay implements HeaderAndFooterHandle { + private String header = EMPTY_JSON_TEXT; + private String footer = EMPTY_JSON_TEXT; + + private volatile boolean headerOrFooterDirty = false; + + final AtomicInteger batchUpdateRecursionLevel = new AtomicInteger(0); + + @Override + public void beginBatchModification() { + if (isValid()) { + if (batchUpdateRecursionLevel.incrementAndGet() < 0) { + throw new AssertionError("Recursion level overflow"); + } + } + } + + @Override + public void completeBatchModification() { + if (isValid()) { + int level = batchUpdateRecursionLevel.decrementAndGet(); + if (level == 0) { + scheduleUpdate(); + } else if (level < 0) { + throw new AssertionError("Recursion level underflow"); + } + } + } + + void scheduleUpdateIfNotInBatch() { + if (batchUpdateRecursionLevel.get() == 0) { + scheduleUpdate(); + } + } + + @Override + public void setHeaderFooter(@Nullable String header, @Nullable String footer) { + this.header = ChatFormat.formattedTextToJson(header); + this.footer = ChatFormat.formattedTextToJson(footer); + headerOrFooterDirty = true; + scheduleUpdateIfNotInBatch(); + } + + @Override + public void setHeader(@Nullable String header) { + this.header = ChatFormat.formattedTextToJson(header); + headerOrFooterDirty = true; + scheduleUpdateIfNotInBatch(); + } + + @Override + public void setFooter(@Nullable String footer) { + this.footer = ChatFormat.formattedTextToJson(footer); + headerOrFooterDirty = true; + scheduleUpdateIfNotInBatch(); + } + } + + private static int index(int column, int row) { + return column * 20 + row; + } + + private static String[][] toPropertiesArray(ProfileProperty textureProperty) { + if (textureProperty == null) { + return EMPTY_PROPERTIES_ARRAY; + } else if (textureProperty.isSigned()) { + return new String[][]{{textureProperty.getName(), textureProperty.getValue(), textureProperty.getSignature()}}; + } else { + // todo maybe add warning on unsigned properties? + return new String[][]{{textureProperty.getName(), textureProperty.getValue()}}; + } + } + + private static Team createPacketTeamCreate(String name, String displayName, String prefix, String suffix, String nameTagVisibility, String collisionRule, int color, byte friendlyFire, String[] players) { + Team team = new Team(); + team.setName(name); + team.setMode((byte) 0); + team.setDisplayName(displayName); + team.setPrefix(prefix); + team.setSuffix(suffix); + team.setNameTagVisibility(nameTagVisibility); + if (TEAM_COLLISION_RULE_SUPPORTED) { + team.setCollisionRule(collisionRule); + } + team.setColor(color); + team.setFriendlyFire(friendlyFire); + team.setPlayers(players); + return team; + } + + private static Team createPacketTeamRemove(String name) { + Team team = new Team(); + team.setName(name); + team.setMode((byte) 1); + return team; + } + + private static Team createPacketTeamUpdate(String name, String displayName, String prefix, String suffix, String nameTagVisibility, String collisionRule, int color, byte friendlyFire) { + Team team = new Team(); + team.setName(name); + team.setMode((byte) 2); + team.setDisplayName(displayName); + team.setPrefix(prefix); + team.setSuffix(suffix); + team.setNameTagVisibility(nameTagVisibility); + if (TEAM_COLLISION_RULE_SUPPORTED) { + team.setCollisionRule(collisionRule); + } + team.setColor(color); + team.setFriendlyFire(friendlyFire); + return team; + } + + private static Team createPacketTeamAddPlayers(String name, String[] players) { + Team team = new Team(); + team.setName(name); + team.setMode((byte) 3); + team.setPlayers(players); + return team; + } + + private static Team createPacketTeamRemovePlayers(String name, String[] players) { + Team team = new Team(); + team.setName(name); + team.setMode((byte) 4); + team.setPlayers(players); + return team; + } + + private enum SlotState { + UNUSED, CUSTOM, PLAYER + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + static class PlayerListEntry { + private UUID uuid; + private String[][] properties; + private String username; + private Component displayName; + private int ping; + private int gamemode; + + private PlayerListEntry(LegacyPlayerListItem.Item item) { + this(item.getUuid(), null, item.getName(), item.getDisplayName(), item.getLatency(), item.getGameMode()); // TODO: Check Display Name + properties = Property119Handler.getProperties(item); + } + } + + @Data + static class TeamEntry { + private String displayName; + private String prefix; + private String suffix; + private byte friendlyFire; + private String nameTagVisibility; + private String collisionRule; + private int color; + private Set players = new ObjectOpenHashSet<>(); + + void addPlayer(String name) { + players.add(name); + } + + void removePlayer(String name) { + players.remove(name); + } + + public void setNameTagVisibility(String nameTagVisibility) { + this.nameTagVisibility = nameTagVisibility.intern(); + } + + public void setCollisionRule(String collisionRule) { + this.collisionRule = collisionRule == null ? null : collisionRule.intern(); + } + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/GetGamemodeLogic.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/GetGamemodeLogic.java new file mode 100644 index 00000000..58fad601 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/GetGamemodeLogic.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.handler; + +import codecrafter47.bungeetablistplus.protocol.AbstractPacketHandler; +import codecrafter47.bungeetablistplus.protocol.PacketHandler; +import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; +import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class GetGamemodeLogic extends AbstractPacketHandler { + + // Velocity doesn't track game mode in player's connection + private static final Map gameModes = new HashMap<>(); + + private final UUID uuid; + + public GetGamemodeLogic(PacketHandler parent, UUID uuid) { + super(parent); + this.uuid = uuid; + } + + @Override + public PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { + if (packet.getAction() == LegacyPlayerListItem.ADD_PLAYER || packet.getAction() == LegacyPlayerListItem.UPDATE_GAMEMODE) { + for (LegacyPlayerListItem.Item item : packet.getItems()) { + if (uuid.equals(item.getUuid())) { + gameModes.put(uuid, item.getGameMode()); + } + } + } + return super.onPlayerListPacket(packet); + } + + @Override + public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { + if (packet.getActions().contains(UpsertPlayerInfo.Action.UPDATE_GAME_MODE)) { + for (UpsertPlayerInfo.Entry entry : packet.getEntries()) { + if (uuid.equals(entry.getProfileId())) { + gameModes.put(uuid, entry.getGameMode()); + } + } + } + return super.onPlayerListUpdatePacket(packet); + } + + @Override + public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet) { + return super.onPlayerListRemovePacket(packet); + } + + public static int getGameMode(UUID uuid){ + return gameModes.getOrDefault(uuid,0); + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LegacyTabOverlayHandlerImpl.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LegacyTabOverlayHandlerImpl.java new file mode 100644 index 00000000..2d6244a8 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LegacyTabOverlayHandlerImpl.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.handler; + +import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; + +import codecrafter47.bungeetablistplus.util.ReflectionUtil; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import lombok.SneakyThrows; + +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +public class LegacyTabOverlayHandlerImpl extends AbstractLegacyTabOverlayHandler { + + private final Player player; + + public LegacyTabOverlayHandlerImpl(Logger logger, int playerListSize, Executor eventLoopExecutor, Player player, boolean is13OrLater) { + super(logger, playerListSize, eventLoopExecutor, is13OrLater); + this.player = player; + } + + @SneakyThrows + @Override + protected void sendPacket(MinecraftPacket packet) { + ReflectionUtil.getChannelWrapper(player).write(packet); + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LowMemoryTabOverlayHandlerImpl.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LowMemoryTabOverlayHandlerImpl.java new file mode 100644 index 00000000..28cbdac7 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LowMemoryTabOverlayHandlerImpl.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.handler; + +import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.proxy.protocol.packet.Team; +import de.codecrafter47.bungeetablistplus.bungee.compat.WaterfallCompat; + +import java.util.UUID; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +/** + * Dirty hack to reduce memory usage of the plugin. Should be removed as soon as + * the underlying problem is fixed in BungeeCord. + */ +public class LowMemoryTabOverlayHandlerImpl extends TabOverlayHandlerImpl { + + public LowMemoryTabOverlayHandlerImpl(Logger logger, Executor eventLoopExecutor, UUID viewerUuid, Player player, boolean is18, boolean has113OrLater, boolean has119OrLater) { + super(logger, eventLoopExecutor, viewerUuid, player, is18, has113OrLater, has119OrLater); + } + + @Override + public PacketListenerResult onTeamPacket(Team packet) { + if (super.onTeamPacket(packet) != PacketListenerResult.CANCEL) { + sendPacket(packet); + } + return PacketListenerResult.CANCEL; + } + + @Override + public void onServerSwitch(boolean is13OrLater) { + if (!WaterfallCompat.isDisableEntityMetadataRewrite()) { + for (String team : serverTeams.keySet()) { + sendPacket(new Team(team)); + } + } + super.onServerSwitch(is13OrLater); + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java new file mode 100644 index 00000000..b3189434 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java @@ -0,0 +1,1241 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.handler; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.protocol.PacketHandler; +import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; +import codecrafter47.bungeetablistplus.util.BitSet; +import codecrafter47.bungeetablistplus.util.ConcurrentBitSet; +import codecrafter47.bungeetablistplus.util.ReflectionUtil; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.util.GameProfile; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; +import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; +import com.velocitypowered.proxy.protocol.packet.Team; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; +import de.codecrafter47.taboverlay.Icon; +import de.codecrafter47.taboverlay.ProfileProperty; +import de.codecrafter47.taboverlay.config.misc.ChatFormat; +import de.codecrafter47.taboverlay.config.misc.Unchecked; +import de.codecrafter47.taboverlay.handler.*; +import it.unimi.dsi.fastutil.objects.*; +import lombok.*; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.*; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class NewTabOverlayHandler implements PacketHandler, TabOverlayHandler { + + // some options + private static final boolean OPTION_ENABLE_CUSTOM_SLOT_USERNAME_COLLISION_CHECK = true; + private static final boolean OPTION_ENABLE_CUSTOM_SLOT_UUID_COLLISION_CHECK = true; + + private static final String EMPTY_JSON_TEXT = "{\"text\":\"\"}"; + protected static final String[][] EMPTY_PROPERTIES_ARRAY = new String[0][]; + + private static final ImmutableMap DIMENSION_TO_USED_SLOTS; + private static final BitSet[] SIZE_TO_USED_SLOTS; + + private static final UUID[] CUSTOM_SLOT_UUID_STEVE; + private static final UUID[] CUSTOM_SLOT_UUID_ALEX; + @Nonnull + private static final Set CUSTOM_SLOT_UUIDS; + private static final String[] CUSTOM_SLOT_USERNAME; + private static final String[] CUSTOM_SLOT_USERNAME_SMILEYS; + @Nonnull + private static final Set CUSTOM_SLOT_USERNAMES; + private static final String[] CUSTOM_SLOT_TEAMNAME; + + static { + + // build the dimension to used slots map (for the rectangular tab overlay) + val builder = ImmutableMap.builder(); + for (int columns = 1; columns <= 4; columns++) { + for (int rows = 0; rows <= 20; rows++) { + if (columns != 1 && rows != 0 && columns * rows <= (columns - 1) * 20) + continue; + BitSet usedSlots = new BitSet(80); + for (int column = 0; column < columns; column++) { + for (int row = 0; row < rows; row++) { + usedSlots.set(index(column, row)); + } + } + builder.put(new RectangularTabOverlay.Dimension(columns, rows), usedSlots); + } + } + DIMENSION_TO_USED_SLOTS = builder.build(); + + // build the size to used slots map (for the simple tab overlay) + SIZE_TO_USED_SLOTS = new BitSet[81]; + for (int size = 0; size <= 80; size++) { + BitSet usedSlots = new BitSet(80); + usedSlots.set(0, size); + SIZE_TO_USED_SLOTS[size] = usedSlots; + } + + // generate random uuids for our custom slots + CUSTOM_SLOT_UUID_ALEX = new UUID[80]; + CUSTOM_SLOT_UUID_STEVE = new UUID[80]; + UUID base = UUID.randomUUID(); + long msb = base.getMostSignificantBits(); + long lsb = base.getLeastSignificantBits(); + lsb ^= base.hashCode(); + for (int i = 0; i < 80; i++) { + CUSTOM_SLOT_UUID_STEVE[i] = new UUID(msb, lsb ^ (2 * i)); + CUSTOM_SLOT_UUID_ALEX[i] = new UUID(msb, lsb ^ (2 * i + 1)); + } + if (OPTION_ENABLE_CUSTOM_SLOT_UUID_COLLISION_CHECK) { + CUSTOM_SLOT_UUIDS = ImmutableSet.builder() + .add(CUSTOM_SLOT_UUID_ALEX) + .add(CUSTOM_SLOT_UUID_STEVE).build(); + } else { + CUSTOM_SLOT_UUIDS = Collections.emptySet(); + } + + // generate usernames for custom slots + int unique = ThreadLocalRandom.current().nextInt(); + CUSTOM_SLOT_USERNAME = new String[81]; + for (int i = 0; i < 81; i++) { + CUSTOM_SLOT_USERNAME[i] = String.format("~BTLP%08x %02d", unique, i); + } + if (OPTION_ENABLE_CUSTOM_SLOT_USERNAME_COLLISION_CHECK) { + CUSTOM_SLOT_USERNAMES = ImmutableSet.copyOf(CUSTOM_SLOT_USERNAME); + } else { + CUSTOM_SLOT_USERNAMES = Collections.emptySet(); + } + CUSTOM_SLOT_USERNAME_SMILEYS = new String[80]; + String emojis = "\u263a\u2639\u2620\u2763\u2764\u270c\u261d\u270d\u2618\u2615\u2668\u2693\u2708\u231b\u231a\u2600\u2b50\u2601\u2602\u2614\u26a1\u2744\u2603\u2604\u2660\u2665\u2666\u2663\u265f\u260e\u2328\u2709\u270f\u2712\u2702\u2692\u2694\u2699\u2696\u2697\u26b0\u26b1\u267f\u26a0\u2622\u2623\u2640\u2642\u267e\u267b\u269c\u303d\u2733\u2734\u2747\u203c\u2b1c\u2b1b\u25fc\u25fb\u25aa\u25ab\u2049\u26ab\u26aa\u3030\u00a9\u00ae\u2122\u2139\u24c2\u3297\u2716\u2714\u2611\u2695\u2b06\u2197\u27a1\u2198\u2b07\u2199\u3299\u2b05\u2196\u2195\u2194\u21a9\u21aa\u2934\u2935\u269b\u2721\u2638\u262f\u271d\u2626\u262a\u262e\u2648\u2649\u264a\u264b\u264c\u264d\u264e\u264f\u2650\u2651\u2652\u2653\u25b6\u25c0\u23cf"; + for (int i = 0; i < 80; i++) { + CUSTOM_SLOT_USERNAME_SMILEYS[i] = String.format("" + emojis.charAt(i), unique, i); + } + + // generate teams for custom slots + CUSTOM_SLOT_TEAMNAME = new String[81]; + for (int i = 0; i < 81; i++) { + CUSTOM_SLOT_TEAMNAME[i] = String.format(" BTLP%08x %02d", unique, i); + } + } + + private final Logger logger; + private final Executor eventLoopExecutor; + + private final Object2BooleanMap serverPlayerListListed = new Object2BooleanOpenHashMap<>(); + @Nullable + protected String serverHeader = null; + @Nullable + protected String serverFooter = null; + + private final Queue> nextActiveContentHandlerQueue = new ConcurrentLinkedQueue<>(); + private final Queue> nextActiveHeaderFooterHandlerQueue = new ConcurrentLinkedQueue<>(); + private AbstractContentOperationModeHandler activeContentHandler; + private AbstractHeaderFooterOperationModeHandler activeHeaderFooterHandler; + + private boolean hasCreatedCustomTeams = false; + + private final AtomicBoolean updateScheduledFlag = new AtomicBoolean(false); + private final Runnable updateTask = this::update; + + protected boolean active; + + private final Player player; + + public NewTabOverlayHandler(Logger logger, Executor eventLoopExecutor, Player player) { + this.logger = logger; + this.eventLoopExecutor = eventLoopExecutor; + this.player = player; + this.activeContentHandler = new PassThroughContentHandler(); + this.activeHeaderFooterHandler = new PassThroughHeaderFooterHandler(); + } + + @SneakyThrows + private void sendPacket(MinecraftPacket packet) { + ReflectionUtil.getChannelWrapper(player).write(packet); + } + + @Override + public PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { + return PacketListenerResult.PASS; + } + + @Override + public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { + if (packet.getActions().contains(UpsertPlayerInfo.Action.ADD_PLAYER)) { + for (UpsertPlayerInfo.Entry entry : packet.getEntries()) { + if (OPTION_ENABLE_CUSTOM_SLOT_UUID_COLLISION_CHECK) { + if (CUSTOM_SLOT_UUIDS.contains(entry.getProfileId())) { + throw new AssertionError("UUID collision " + entry.getProfileId()); + } + } + if (OPTION_ENABLE_CUSTOM_SLOT_USERNAME_COLLISION_CHECK) { + if (CUSTOM_SLOT_USERNAMES.contains(entry.getProfile().getName())) { + throw new AssertionError("Username collision" + entry.getProfile().getName()); + } + } + serverPlayerListListed.putIfAbsent(entry.getProfileId(), false); + } + } + if (packet.getActions().contains(UpsertPlayerInfo.Action.UPDATE_LISTED)) { + for (UpsertPlayerInfo.Entry entry : packet.getEntries()) { + serverPlayerListListed.put(entry.getProfileId(), entry.isListed()); + } + } + + try { + return this.activeContentHandler.onPlayerListUpdatePacket(packet); + } catch (Throwable th) { + logger.log(Level.SEVERE, "Unexpected error", th); + // try recover + enterContentOperationMode(ContentOperationMode.PASS_TROUGH); + return PacketListenerResult.PASS; + } + } + + @Override + public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet) { + for (UUID uuid : packet.getProfilesToRemove()) { + serverPlayerListListed.removeBoolean(uuid); + } + return PacketListenerResult.PASS; + } + + @Override + public PacketListenerResult onTeamPacket(Team packet) { + return PacketListenerResult.PASS; + } + + @Override + public PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { + PacketListenerResult result = PacketListenerResult.PASS; + try { + result = this.activeHeaderFooterHandler.onPlayerListHeaderFooterPacket(packet); + if (result == PacketListenerResult.MODIFIED) { + throw new AssertionError("PacketListenerResult.MODIFIED must not be used"); + } + } catch (Throwable th) { + logger.log(Level.SEVERE, "Unexpected error", th); + // try recover + enterHeaderAndFooterOperationMode(HeaderAndFooterOperationMode.PASS_TROUGH); + } + + this.serverHeader = packet.getHeader() != null ? packet.getHeader() : EMPTY_JSON_TEXT; + this.serverFooter = packet.getFooter() != null ? packet.getFooter() : EMPTY_JSON_TEXT; + + return result; + } + + @Override + public void onServerSwitch(boolean is13OrLater) { + if (!active) { + active = true; + update(); + } else { + + hasCreatedCustomTeams = false; + + try { + this.activeContentHandler.onServerSwitch(); + } catch (Throwable th) { + logger.log(Level.SEVERE, "Unexpected error", th); + // try recover + enterContentOperationMode(ContentOperationMode.PASS_TROUGH); + } + try { + this.activeHeaderFooterHandler.onServerSwitch(); + } catch (Throwable th) { + logger.log(Level.SEVERE, "Unexpected error", th); + // try recover + enterContentOperationMode(ContentOperationMode.PASS_TROUGH); + } + + if (!serverPlayerListListed.isEmpty()) { + RemovePlayerInfo packet = new RemovePlayerInfo(); + packet.setProfilesToRemove(serverPlayerListListed.keySet()); + sendPacket(packet); + } + + serverPlayerListListed.clear(); + if (serverHeader != null) { + serverHeader = EMPTY_JSON_TEXT; + } + if (serverFooter != null) { + serverFooter = EMPTY_JSON_TEXT; + } + } + } + + @Override + public R enterContentOperationMode(ContentOperationMode operationMode) { + AbstractContentOperationModeHandler handler; + if (operationMode == ContentOperationMode.PASS_TROUGH) { + handler = new PassThroughContentHandler(); + } else if (operationMode == ContentOperationMode.SIMPLE) { + handler = new SimpleOperationModeHandler(); + } else if (operationMode == ContentOperationMode.RECTANGULAR) { + handler = new RectangularSizeHandler(); + } else { + throw new UnsupportedOperationException(); + } + nextActiveContentHandlerQueue.add(handler); + scheduleUpdate(); + return Unchecked.cast(handler.getTabOverlay()); + } + + @Override + public R enterHeaderAndFooterOperationMode(HeaderAndFooterOperationMode operationMode) { + AbstractHeaderFooterOperationModeHandler handler; + if (operationMode == HeaderAndFooterOperationMode.PASS_TROUGH) { + handler = new PassThroughHeaderFooterHandler(); + } else if (operationMode == HeaderAndFooterOperationMode.CUSTOM) { + handler = new CustomHeaderAndFooterOperationModeHandler(); + } else { + throw new UnsupportedOperationException(Objects.toString(operationMode)); + } + nextActiveHeaderFooterHandlerQueue.add(handler); + scheduleUpdate(); + return Unchecked.cast(handler.getTabOverlay()); + } + + private void scheduleUpdate() { + if (this.updateScheduledFlag.compareAndSet(false, true)) { + try { + eventLoopExecutor.execute(updateTask); + } catch (RejectedExecutionException ignored) { + } + } + } + + private void update() { + if (!active) { + return; + } + updateScheduledFlag.set(false); + + // update content handler + AbstractContentOperationModeHandler contentHandler; + while (null != (contentHandler = nextActiveContentHandlerQueue.poll())) { + this.activeContentHandler.invalidate(); + contentHandler.onActivated(this.activeContentHandler); + this.activeContentHandler = contentHandler; + } + this.activeContentHandler.update(); + + // update header and footer handler + AbstractHeaderFooterOperationModeHandler heaerFooterHandler; + while (null != (heaerFooterHandler = nextActiveHeaderFooterHandlerQueue.poll())) { + this.activeHeaderFooterHandler.invalidate(); + heaerFooterHandler.onActivated(this.activeHeaderFooterHandler); + this.activeHeaderFooterHandler = heaerFooterHandler; + } + this.activeHeaderFooterHandler.update(); + } + + private abstract static class AbstractContentOperationModeHandler extends OperationModeHandler { + + /** + * Called when the player receives a {@link LegacyPlayerListItem} packet. + *

+ * This method is called after this {@link NewTabOverlayHandler} has updated the {@code serverPlayerList}. + */ + abstract PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet); + + /** + * Called when the player switches the server. + *

+ * This method is called before this {@link NewTabOverlayHandler} executes its own logic to clear the + * server player list info. + */ + abstract void onServerSwitch(); + + abstract void update(); + + final void invalidate() { + getTabOverlay().invalidate(); + onDeactivated(); + } + + /** + * Called when this {@link OperationModeHandler} is deactivated. + *

+ * This method must put the client player list in the state expected by {@link #onActivated(AbstractContentOperationModeHandler)}. It must + * especially remove all custom entries and players must be part of the correct teams. + */ + abstract void onDeactivated(); + + /** + * Called when this {@link OperationModeHandler} becomes the active one. + *

+ * State of the player list when this method is called: + * - there are no custom entries on the client + * - all entries from {@link #serverPlayerListListed} but may not be listed + * - player list header/ footer may be wrong + *

+ * Additional information about the state of the player list may be obtained from the previous handler + * + * @param previous previous handler + */ + abstract void onActivated(AbstractContentOperationModeHandler previous); + } + + private abstract static class AbstractHeaderFooterOperationModeHandler extends OperationModeHandler { + + /** + * Called when the player receives a {@link HeaderAndFooter} packet. + *

+ * This method is called before this {@link NewTabOverlayHandler} executes its own logic to update the + * server player list info. + */ + abstract PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet); + + /** + * Called when the player switches the server. + *

+ * This method is called before this {@link NewTabOverlayHandler} executes its own logic to clear the + * server player list info. + */ + abstract void onServerSwitch(); + + abstract void update(); + + final void invalidate() { + getTabOverlay().invalidate(); + onDeactivated(); + } + + /** + * Called when this {@link OperationModeHandler} is deactivated. + *

+ * This method must put the client player list in the state expected by {@link #onActivated(AbstractHeaderFooterOperationModeHandler)}. It must + * especially remove all custom entries and players must be part of the correct teams. + */ + abstract void onDeactivated(); + + /** + * Called when this {@link OperationModeHandler} becomes the active one. + *

+ * State of the player list when this method is called: + * - there are no custom entries on the client + * - all entries from {@link #serverPlayerListListed} are known to the client, but might not be listed + * - player list header/ footer may be wrong + *

+ * Additional information about the state of the player list may be obtained from the previous handler + * + * @param previous previous handler + */ + abstract void onActivated(AbstractHeaderFooterOperationModeHandler previous); + } + + private abstract static class AbstractContentTabOverlay implements TabOverlayHandle { + private boolean valid = true; + + @Override + public boolean isValid() { + return valid; + } + + final void invalidate() { + valid = false; + } + } + + private abstract static class AbstractHeaderFooterTabOverlay implements TabOverlayHandle { + private boolean valid = true; + + @Override + public boolean isValid() { + return valid; + } + + final void invalidate() { + valid = false; + } + } + + private final class PassThroughContentHandler extends AbstractContentOperationModeHandler { + + @Override + protected PassThroughContentTabOverlay createTabOverlay() { + return new PassThroughContentTabOverlay(); + } + + @Override + PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { + return PacketListenerResult.PASS; + } + + @Override + void onServerSwitch() { + sendPacket(new HeaderAndFooter(EMPTY_JSON_TEXT, EMPTY_JSON_TEXT)); + } + + @Override + void update() { + // nothing to do + } + + @Override + void onDeactivated() { + // nothing to do + } + + @Override + void onActivated(AbstractContentOperationModeHandler previous) { + if (previous instanceof PassThroughContentHandler) { + // we're lucky, nothing to do + return; + } + + // update visibility + if (!serverPlayerListListed.isEmpty()) { + List items = new ArrayList<>(serverPlayerListListed.size()); + for (Object2BooleanMap.Entry entry : serverPlayerListListed.object2BooleanEntrySet()) { + + UpsertPlayerInfo.Entry item = new UpsertPlayerInfo.Entry(entry.getKey()); + item.setListed(entry.getBooleanValue()); + items.add(item); + } + UpsertPlayerInfo packet = new UpsertPlayerInfo(); + packet.addAction(UpsertPlayerInfo.Action.UPDATE_LISTED); + packet.addAllEntries(items); + sendPacket(packet); + } + } + } + + private static final class PassThroughContentTabOverlay extends AbstractContentTabOverlay { + + } + + private final class PassThroughHeaderFooterHandler extends AbstractHeaderFooterOperationModeHandler { + + @Override + protected PassThroughHeaderFooterTabOverlay createTabOverlay() { + return new PassThroughHeaderFooterTabOverlay(); + } + + @Override + PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { + return PacketListenerResult.PASS; + } + + @Override + void onServerSwitch() { + sendPacket(new HeaderAndFooter(EMPTY_JSON_TEXT, EMPTY_JSON_TEXT)); + } + + @Override + void update() { + // nothing to do + } + + @Override + void onDeactivated() { + // nothing to do + } + + @Override + void onActivated(AbstractHeaderFooterOperationModeHandler previous) { + if (previous instanceof PassThroughHeaderFooterHandler) { + // we're lucky, nothing to do + return; + } + + // fix header/ footer + sendPacket(new HeaderAndFooter(serverHeader != null ? serverHeader : EMPTY_JSON_TEXT, serverFooter != null ? serverFooter : EMPTY_JSON_TEXT)); + } + } + + private static final class PassThroughHeaderFooterTabOverlay extends AbstractHeaderFooterTabOverlay { + + } + + private abstract class CustomContentTabOverlayHandler extends AbstractContentOperationModeHandler { + + @Nonnull + BitSet usedSlots; + BitSet dirtySlots; + final SlotState[] slotState; + /** + * Uuid of the player list entry used for the slot. + */ + final UUID[] slotUuid; + /** + * Username of the player list entry used for the slot. + */ + final String[] slotUsername; + + private final List itemQueueAddPlayer; + private final List itemQueueRemovePlayer; + private final List itemQueueUpdateDisplayName; + private final List itemQueueUpdatePing; + + private final boolean experimentalTabCompleteSmileys = isExperimentalTabCompleteSmileys(); + + private CustomContentTabOverlayHandler() { + this.dirtySlots = new BitSet(80); + this.usedSlots = SIZE_TO_USED_SLOTS[0]; + this.slotState = new SlotState[80]; + Arrays.fill(this.slotState, SlotState.UNUSED); + this.slotUuid = new UUID[80]; + this.slotUsername = new String[80]; + this.itemQueueAddPlayer = new ArrayList<>(80); + this.itemQueueRemovePlayer = new ArrayList<>(80); + this.itemQueueUpdateDisplayName = new ArrayList<>(80); + this.itemQueueUpdatePing = new ArrayList<>(80); + } + + @Override + PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { + + if (packet.getActions().contains(UpsertPlayerInfo.Action.UPDATE_LISTED)) { + for (UpsertPlayerInfo.Entry entry : packet.getEntries()) { + entry.setListed(false); + } + } + return PacketListenerResult.MODIFIED; + } + + private String getCustomSlotUsername(int index) { + if (experimentalTabCompleteSmileys) { + return CUSTOM_SLOT_USERNAME_SMILEYS[index]; + } else { + return CUSTOM_SLOT_USERNAME[index]; + } + } + + @Override + void onServerSwitch() { + + createTeamsIfNecessary(); + } + + @Override + void onActivated(AbstractContentOperationModeHandler previous) { + + // make all players unlisted + if (!serverPlayerListListed.isEmpty()) { + List items = new ArrayList<>(serverPlayerListListed.size()); + for (Object2BooleanMap.Entry entry : serverPlayerListListed.object2BooleanEntrySet()) { + UpsertPlayerInfo.Entry item = new UpsertPlayerInfo.Entry(entry.getKey()); + item.setListed(false); + items.add(item); + } + UpsertPlayerInfo packet = new UpsertPlayerInfo(); + packet.addAction(UpsertPlayerInfo.Action.UPDATE_LISTED); + packet.addAllEntries(items); + sendPacket(packet); + } + + createTeamsIfNecessary(); + } + + private void createTeamsIfNecessary() { + // create teams if not already created + if (!hasCreatedCustomTeams) { + hasCreatedCustomTeams = true; + + for (int i = 0; i < 80; i++) { + sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[i], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[i], CUSTOM_SLOT_USERNAME_SMILEYS[i]})); + } + } + } + + @Override + void onDeactivated() { + int customSlots = 0; + for (int index = 0; index < 80; index++) { + if (slotState[index] != SlotState.UNUSED) { + customSlots++; + } + } + + int i = 0; + if (customSlots > 0) { + UUID[] uuids = new UUID[customSlots]; + for (int index = 0; index < 80; index++) { + // switch slot from custom to unused + if (slotState[index] == SlotState.CUSTOM) { + uuids[i++] = slotUuid[index]; + } + } + RemovePlayerInfo packet = new RemovePlayerInfo(); + packet.setProfilesToRemove(Arrays.asList(uuids)); + sendPacket(packet); + } + } + + @Override + void update() { + T tabOverlay = getTabOverlay(); + + if (tabOverlay.dirtyFlagSize) { + tabOverlay.dirtyFlagSize = false; + updateSize(); + } + + // update icons + dirtySlots.orAndClear(tabOverlay.dirtyFlagsIcon); + for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { + if (slotState[index] == SlotState.CUSTOM) { + // remove item + itemQueueRemovePlayer.add(slotUuid[index]); + slotState[index] = SlotState.UNUSED; + } + + if (usedSlots.get(index)) { + Icon icon = tabOverlay.icon[index]; + UUID customSlotUuid; + if (icon.isAlex()) { + customSlotUuid = CUSTOM_SLOT_UUID_ALEX[index]; + } else { // steve + customSlotUuid = CUSTOM_SLOT_UUID_STEVE[index]; + } + tabOverlay.dirtyFlagsText.clear(index); + tabOverlay.dirtyFlagsPing.clear(index); + slotState[index] = SlotState.CUSTOM; + slotUuid[index] = customSlotUuid; + UpsertPlayerInfo.Entry item = new UpsertPlayerInfo.Entry(customSlotUuid); + GameProfile profile = new GameProfile(customSlotUuid, slotUsername[index] = getCustomSlotUsername(index), toPropertiesList(icon.getTextureProperty())); + item.setProfile(profile); + item.setDisplayName(tabOverlay.text[index]); + item.setLatency(tabOverlay.ping[index]); + item.setGameMode(0); + item.setListed(true); + itemQueueAddPlayer.add(item); + } + } + + // update text + dirtySlots.copyAndClear(tabOverlay.dirtyFlagsText); + for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { + if (slotState[index] != SlotState.UNUSED) { + UpsertPlayerInfo.Entry item = new UpsertPlayerInfo.Entry(slotUuid[index]); + item.setDisplayName(tabOverlay.text[index]); + itemQueueUpdateDisplayName.add(item); + } + } + + // update ping + dirtySlots.copyAndClear(tabOverlay.dirtyFlagsPing); + for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { + if (slotState[index] != SlotState.UNUSED) { + UpsertPlayerInfo.Entry item = new UpsertPlayerInfo.Entry(slotUuid[index]); + item.setLatency(tabOverlay.ping[index]); + itemQueueUpdatePing.add(item); + } + } + + dirtySlots.clear(); + + // send packets + sendQueuedItems(); + } + + private void sendQueuedItems() { + if (!itemQueueRemovePlayer.isEmpty()) { + RemovePlayerInfo packet = new RemovePlayerInfo(); + packet.setProfilesToRemove(itemQueueRemovePlayer); + sendPacket(packet); + itemQueueRemovePlayer.clear(); + } + if (!itemQueueAddPlayer.isEmpty()) { + UpsertPlayerInfo packet = new UpsertPlayerInfo(); + packet.addAllActions(EnumSet.of(UpsertPlayerInfo.Action.ADD_PLAYER, UpsertPlayerInfo.Action.UPDATE_DISPLAY_NAME, UpsertPlayerInfo.Action.UPDATE_LATENCY, UpsertPlayerInfo.Action.UPDATE_LISTED)); + packet.addAllEntries(itemQueueAddPlayer); + sendPacket(packet); + itemQueueAddPlayer.clear(); + } + if (!itemQueueUpdateDisplayName.isEmpty()) { + UpsertPlayerInfo packet = new UpsertPlayerInfo(); + packet.addAction(UpsertPlayerInfo.Action.UPDATE_DISPLAY_NAME); + packet.addAllEntries(itemQueueUpdateDisplayName); + sendPacket(packet); + itemQueueUpdateDisplayName.clear(); + } + if (!itemQueueUpdatePing.isEmpty()) { + UpsertPlayerInfo packet = new UpsertPlayerInfo(); + packet.addAction(UpsertPlayerInfo.Action.UPDATE_LATENCY); + packet.addAllEntries(itemQueueUpdatePing); + sendPacket(packet); + itemQueueUpdatePing.clear(); + } + } + + /** + * Updates the usedSlots BitSet. Sets the {@link #dirtySlots uuid dirty flag} for all added + * and removed slots. + */ + abstract void updateSize(); + } + + private boolean isExperimentalTabCompleteSmileys() { + return BungeeTabListPlus.getInstance().getConfig().experimentalTabCompleteSmileys; + } + + private abstract class CustomContentTabOverlay extends AbstractContentTabOverlay implements TabOverlayHandle.BatchModifiable { + final Icon[] icon; + final Component[] text; + final int[] ping; + + final AtomicInteger batchUpdateRecursionLevel; + volatile boolean dirtyFlagSize; + final ConcurrentBitSet dirtyFlagsIcon; + final ConcurrentBitSet dirtyFlagsText; + final ConcurrentBitSet dirtyFlagsPing; + + private CustomContentTabOverlay() { + this.icon = new Icon[80]; + Arrays.fill(this.icon, Icon.DEFAULT_STEVE); + this.text = new Component[80]; + Arrays.fill(this.text, Component.empty()); + this.ping = new int[80]; + this.batchUpdateRecursionLevel = new AtomicInteger(0); + this.dirtyFlagSize = true; + this.dirtyFlagsIcon = new ConcurrentBitSet(80); + this.dirtyFlagsText = new ConcurrentBitSet(80); + this.dirtyFlagsPing = new ConcurrentBitSet(80); + } + + @Override + public void beginBatchModification() { + if (isValid()) { + if (batchUpdateRecursionLevel.incrementAndGet() < 0) { + throw new AssertionError("Recursion level overflow"); + } + } + } + + @Override + public void completeBatchModification() { + if (isValid()) { + int level = batchUpdateRecursionLevel.decrementAndGet(); + if (level == 0) { + scheduleUpdate(); + } else if (level < 0) { + throw new AssertionError("Recursion level underflow"); + } + } + } + + void scheduleUpdateIfNotInBatch() { + if (batchUpdateRecursionLevel.get() == 0) { + scheduleUpdate(); + } + } + + void setIconInternal(int index, @Nonnull @NonNull Icon icon) { + if (!icon.equals(this.icon[index])) { + this.icon[index] = icon; + dirtyFlagsIcon.set(index); + scheduleUpdateIfNotInBatch(); + } + } + + void setTextInternal(int index, @Nonnull @NonNull String text) { + Component jsonText = LegacyComponentSerializer.legacyAmpersand().deserialize(text.replace(LegacyComponentSerializer.SECTION_CHAR, LegacyComponentSerializer.AMPERSAND_CHAR)); + if (!jsonText.equals(this.text[index])) { + this.text[index] = jsonText; + dirtyFlagsText.set(index); + scheduleUpdateIfNotInBatch(); + } + } + + void setPingInternal(int index, int ping) { + if (ping != this.ping[index]) { + this.ping[index] = ping; + dirtyFlagsPing.set(index); + scheduleUpdateIfNotInBatch(); + } + } + } + + private class RectangularSizeHandler extends CustomContentTabOverlayHandler { + + @Override + void updateSize() { + RectangularTabOverlayImpl tabOverlay = getTabOverlay(); + RectangularTabOverlay.Dimension size = tabOverlay.getSize(); + BitSet newUsedSlots = DIMENSION_TO_USED_SLOTS.get(size); + dirtySlots.orXor(usedSlots, newUsedSlots); + usedSlots = newUsedSlots; + } + + @Override + protected RectangularTabOverlayImpl createTabOverlay() { + return new RectangularTabOverlayImpl(); + } + } + + private class RectangularTabOverlayImpl extends CustomContentTabOverlay implements RectangularTabOverlay { + + @Nonnull + private Dimension size; + + private RectangularTabOverlayImpl() { + Optional dimensionZero = getSupportedSizes().stream().filter(size -> size.getSize() == 0).findAny(); + if (!dimensionZero.isPresent()) { + throw new AssertionError(); + } + this.size = dimensionZero.get(); + } + + @Nonnull + @Override + public Dimension getSize() { + return size; + } + + @Override + public Collection getSupportedSizes() { + return DIMENSION_TO_USED_SLOTS.keySet(); + } + + @Override + public void setSize(@Nonnull Dimension size) { + if (!getSupportedSizes().contains(size)) { + throw new IllegalArgumentException("Unsupported size " + size); + } + if (isValid() && !this.size.equals(size)) { + BitSet oldUsedSlots = DIMENSION_TO_USED_SLOTS.get(this.size); + BitSet newUsedSlots = DIMENSION_TO_USED_SLOTS.get(size); + for (int index = newUsedSlots.nextSetBit(0); index >= 0; index = newUsedSlots.nextSetBit(index + 1)) { + if (!oldUsedSlots.get(index)) { + icon[index] = Icon.DEFAULT_STEVE; + text[index] = Component.empty(); + ping[index] = 0; + } + } + this.size = size; + this.dirtyFlagSize = true; + scheduleUpdateIfNotInBatch(); + for (int index = oldUsedSlots.nextSetBit(0); index >= 0; index = oldUsedSlots.nextSetBit(index + 1)) { + if (!newUsedSlots.get(index)) { + icon[index] = Icon.DEFAULT_STEVE; + text[index] = Component.empty(); + ping[index] = 0; + } + } + } + } + + @Override + public void setSlot(int column, int row, @Nullable UUID uuid, @Nonnull Icon icon, @Nonnull String text, int ping) { + setSlot(column, row, icon, text, ping); + } + + @Override + public void setSlot(int column, int row, @Nonnull Icon icon, @Nonnull String text, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(column, size.getColumns(), "column"); + Preconditions.checkElementIndex(row, size.getRows(), "row"); + beginBatchModification(); + try { + int index = index(column, row); + setIconInternal(index, icon); + setTextInternal(index, text); + setPingInternal(index, ping); + } finally { + completeBatchModification(); + } + } + } + + @Override + public void setUuid(int column, int row, @Nullable UUID uuid) { + // no op + } + + @Override + public void setIcon(int column, int row, @Nonnull Icon icon) { + if (isValid()) { + Preconditions.checkElementIndex(column, size.getColumns(), "column"); + Preconditions.checkElementIndex(row, size.getRows(), "row"); + setIconInternal(index(column, row), icon); + } + } + + @Override + public void setText(int column, int row, @Nonnull String text) { + if (isValid()) { + Preconditions.checkElementIndex(column, size.getColumns(), "column"); + Preconditions.checkElementIndex(row, size.getRows(), "row"); + setTextInternal(index(column, row), text); + } + } + + @Override + public void setPing(int column, int row, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(column, size.getColumns(), "column"); + Preconditions.checkElementIndex(row, size.getRows(), "row"); + setPingInternal(index(column, row), ping); + } + } + } + + private class SimpleOperationModeHandler extends CustomContentTabOverlayHandler { + + private int size = 0; + + @Override + void updateSize() { + int newSize = getTabOverlay().size; + if (newSize > size) { + dirtySlots.set(size, newSize); + } else if (newSize < size) { + dirtySlots.set(newSize, size); + } + usedSlots = SIZE_TO_USED_SLOTS[newSize]; + size = newSize; + } + + @Override + protected SimpleTabOverlayImpl createTabOverlay() { + return new SimpleTabOverlayImpl(); + } + } + + private class SimpleTabOverlayImpl extends CustomContentTabOverlay implements SimpleTabOverlay { + int size = 0; + + @Override + public int getSize() { + return size; + } + + @Override + public int getMaxSize() { + return 80; + } + + @Override + public void setSize(int size) { + if (size < 0 || size > 80) { + throw new IllegalArgumentException("size"); + } + this.size = size; + dirtyFlagSize = true; + scheduleUpdateIfNotInBatch(); + } + + @Override + public void setSlot(int index, @Nullable UUID uuid, @Nonnull Icon icon, @Nonnull String text, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(index, size, "index"); + beginBatchModification(); + try { + setIconInternal(index, icon); + setTextInternal(index, text); + setPingInternal(index, ping); + } finally { + completeBatchModification(); + } + } + } + + @Override + public void setSlot(int index, @Nonnull Icon icon, @Nonnull String text, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(index, size, "index"); + beginBatchModification(); + try { + setIconInternal(index, icon); + setTextInternal(index, text); + setPingInternal(index, ping); + } finally { + completeBatchModification(); + } + } + } + + @Override + public void setUuid(int index, UUID uuid) { + // no op + } + + @Override + public void setIcon(int index, @Nonnull @NonNull Icon icon) { + if (isValid()) { + Preconditions.checkElementIndex(index, size, "index"); + setIconInternal(index, icon); + } + } + + @Override + public void setText(int index, @Nonnull @NonNull String text) { + if (isValid()) { + Preconditions.checkElementIndex(index, size, "index"); + setTextInternal(index, text); + } + } + + @Override + public void setPing(int index, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(index, size, "index"); + setPingInternal(index, ping); + } + } + } + + private final class CustomHeaderAndFooterOperationModeHandler extends AbstractHeaderFooterOperationModeHandler { + + @Override + protected CustomHeaderAndFooterImpl createTabOverlay() { + return new CustomHeaderAndFooterImpl(); + } + + @Override + PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { + return PacketListenerResult.CANCEL; + } + + @Override + void onServerSwitch() { + // do nothing + } + + @Override + void onDeactivated() { + //do nothing + } + + @Override + void onActivated(AbstractHeaderFooterOperationModeHandler previous) { + // remove header/ footer + sendPacket(new HeaderAndFooter(EMPTY_JSON_TEXT, EMPTY_JSON_TEXT)); + } + + @Override + void update() { + CustomHeaderAndFooterImpl tabOverlay = getTabOverlay(); + if (tabOverlay.headerOrFooterDirty) { + tabOverlay.headerOrFooterDirty = false; + sendPacket(new HeaderAndFooter(tabOverlay.header, tabOverlay.footer)); + } + } + } + + private final class CustomHeaderAndFooterImpl extends AbstractHeaderFooterTabOverlay implements HeaderAndFooterHandle { + private String header = EMPTY_JSON_TEXT; + private String footer = EMPTY_JSON_TEXT; + + private volatile boolean headerOrFooterDirty = false; + + final AtomicInteger batchUpdateRecursionLevel = new AtomicInteger(0); + + @Override + public void beginBatchModification() { + if (isValid()) { + if (batchUpdateRecursionLevel.incrementAndGet() < 0) { + throw new AssertionError("Recursion level overflow"); + } + } + } + + @Override + public void completeBatchModification() { + if (isValid()) { + int level = batchUpdateRecursionLevel.decrementAndGet(); + if (level == 0) { + scheduleUpdate(); + } else if (level < 0) { + throw new AssertionError("Recursion level underflow"); + } + } + } + + void scheduleUpdateIfNotInBatch() { + if (batchUpdateRecursionLevel.get() == 0) { + scheduleUpdate(); + } + } + + @Override + public void setHeaderFooter(@Nullable String header, @Nullable String footer) { + this.header = ChatFormat.formattedTextToJson(header); + this.footer = ChatFormat.formattedTextToJson(footer); + headerOrFooterDirty = true; + scheduleUpdateIfNotInBatch(); + } + + @Override + public void setHeader(@Nullable String header) { + this.header = ChatFormat.formattedTextToJson(header); + headerOrFooterDirty = true; + scheduleUpdateIfNotInBatch(); + } + + @Override + public void setFooter(@Nullable String footer) { + this.footer = ChatFormat.formattedTextToJson(footer); + headerOrFooterDirty = true; + scheduleUpdateIfNotInBatch(); + } + } + + private static int index(int column, int row) { + return column * 20 + row; + } + + private static List toPropertiesList(ProfileProperty textureProperty) { + if (textureProperty == null) { + return new ArrayList<>(); + } else if (textureProperty.isSigned()) { + return Collections.singletonList(new GameProfile.Property(textureProperty.getName(), textureProperty.getValue(), textureProperty.getSignature())); + } else { + return Collections.singletonList(new GameProfile.Property(textureProperty.getName(), textureProperty.getValue(), "")); + } + } + + private static Team createPacketTeamCreate(String name, String displayName, String prefix, String suffix, String nameTagVisibility, String collisionRule, int color, byte friendlyFire, String[] players) { + Team team = new Team(); + team.setName(name); + team.setMode((byte) 0); + team.setDisplayName(displayName); + team.setPrefix(prefix); + team.setSuffix(suffix); + team.setNameTagVisibility(nameTagVisibility); + team.setCollisionRule(collisionRule); + team.setColor(color); + team.setFriendlyFire(friendlyFire); + team.setPlayers(players); + return team; + } + + private enum SlotState { + UNUSED, CUSTOM + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/OperationModeHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/OperationModeHandler.java new file mode 100644 index 00000000..7e89584e --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/OperationModeHandler.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.handler; + +import lombok.Getter; + +public abstract class OperationModeHandler { + + @Getter + private final T tabOverlay; + + public OperationModeHandler() { + this.tabOverlay = createTabOverlay(); + } + + protected abstract T createTabOverlay(); +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/RewriteLogic.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/RewriteLogic.java new file mode 100644 index 00000000..7bdf99ba --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/RewriteLogic.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.handler; + +import codecrafter47.bungeetablistplus.protocol.AbstractPacketHandler; +import codecrafter47.bungeetablistplus.protocol.PacketHandler; +import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; +import codecrafter47.bungeetablistplus.util.Property119Handler; +import codecrafter47.bungeetablistplus.util.ProxyServer; +import com.google.common.base.MoreObjects; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; +import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; + +import java.util.HashMap; +import java.util.ListIterator; +import java.util.Map; +import java.util.UUID; + +public class RewriteLogic extends AbstractPacketHandler { + + private final Map rewriteMap = new HashMap<>(); + + public RewriteLogic(PacketHandler parent) { + super(parent); + } + + @Override + public PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { + + if (packet.getAction() == LegacyPlayerListItem.ADD_PLAYER) { + for (LegacyPlayerListItem.Item item : packet.getItems()) { + UUID uuid = item.getUuid(); + Player player = ProxyServer.getInstance().getPlayer(uuid).orElse(null); + if (player != null) { + rewriteMap.put(uuid, player.getUniqueId()); + } + } + } + + boolean modified = false; + + if (packet.getAction() == LegacyPlayerListItem.REMOVE_PLAYER) { + ListIterator it = packet.getItems().listIterator(); + while(it.hasNext()){ + LegacyPlayerListItem.Item item = it.next(); + UUID uuid = rewriteMap.remove(item.getUuid()); + modified |= uuid != null; + it.set(copyToNewItem(MoreObjects.firstNonNull(uuid, item.getUuid()), item)); + } + } else { + ListIterator it = packet.getItems().listIterator(); + while(it.hasNext()){ + LegacyPlayerListItem.Item item = it.next(); + UUID uuid = rewriteMap.get(item.getUuid()); + if (uuid != null) { + modified = true; + if (packet.getAction() == LegacyPlayerListItem.ADD_PLAYER) { + Player player = ProxyServer.getInstance().getPlayer(item.getUuid()).orElse(null); + if (player != null) { + String[][] properties = Property119Handler.getProperties(player.getGameProfile()); + Property119Handler.setProperties(item, properties); + } + } + it.set(copyToNewItem(uuid, item)); + } + } + } + + PacketListenerResult result = super.onPlayerListPacket(packet); + return result == PacketListenerResult.PASS && modified ? PacketListenerResult.MODIFIED : result; + } + + @Override + public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { + if (packet.getActions().contains(UpsertPlayerInfo.Action.ADD_PLAYER)) { + for (UpsertPlayerInfo.Entry item : packet.getEntries()) { + UUID uuid = item.getProfileId(); + Player player = ProxyServer.getInstance().getPlayer(uuid).orElse(null); + if (player != null) { + rewriteMap.put(uuid, player.getUniqueId()); + String[][] properties = Property119Handler.getProperties(player.getGameProfile()); + Property119Handler.setProperties(item, properties); + } + } + } + boolean modified = false; + ListIterator it = packet.getEntries().listIterator(); + while(it.hasNext()){ + UpsertPlayerInfo.Entry item = it.next(); + UUID uuid = rewriteMap.get(item.getProfileId()); + if (uuid != null) { + modified = true; + it.set(copyToNewEntry(uuid, item)); + } + } + PacketListenerResult result = super.onPlayerListUpdatePacket(packet); + return result == PacketListenerResult.PASS && modified ? PacketListenerResult.MODIFIED : result; + } + + @Override + public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet) { + boolean modified = false; + + UUID[] uuids = packet.getProfilesToRemove().toArray(new UUID[0]); + for (int i = 0; i < uuids.length; i++) { + UUID uuid = rewriteMap.remove(uuids[i]); + if (uuid != null) { + modified = true; + uuids[i] = uuid; + } + } + + PacketListenerResult result = super.onPlayerListRemovePacket(packet); + return result == PacketListenerResult.PASS && modified ? PacketListenerResult.MODIFIED : result; + } + + @Override + public void onServerSwitch(boolean is13OrLater) { + rewriteMap.clear(); + + super.onServerSwitch(is13OrLater); + } + + private LegacyPlayerListItem.Item copyToNewItem(UUID uuid, LegacyPlayerListItem.Item item){ + LegacyPlayerListItem.Item newItem = new LegacyPlayerListItem.Item(uuid); + newItem.setName(item.getName()); + newItem.setLatency(item.getLatency()); + newItem.setGameMode(item.getGameMode()); + newItem.setDisplayName(item.getDisplayName()); + newItem.setPlayerKey(item.getPlayerKey()); + newItem.setProperties(item.getProperties()); + return newItem; + } + + private UpsertPlayerInfo.Entry copyToNewEntry(UUID uuid, UpsertPlayerInfo.Entry item){ + UpsertPlayerInfo.Entry newItem = new UpsertPlayerInfo.Entry(uuid); + newItem.setProfile(item.getProfile()); + newItem.setLatency(item.getLatency()); + newItem.setGameMode(item.getGameMode()); + newItem.setDisplayName(item.getDisplayName()); + newItem.setListed(item.isListed()); + newItem.setChatSession(item.getChatSession()); + return newItem; + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/TabOverlayHandlerImpl.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/TabOverlayHandlerImpl.java new file mode 100644 index 00000000..bb89ff99 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/TabOverlayHandlerImpl.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.handler; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; +import codecrafter47.bungeetablistplus.util.ReflectionUtil; +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; +import de.codecrafter47.bungeetablistplus.bungee.compat.WaterfallCompat; +import lombok.SneakyThrows; + +import java.util.UUID; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +public class TabOverlayHandlerImpl extends AbstractTabOverlayHandler { + + private final Player player; + + public TabOverlayHandlerImpl(Logger logger, Executor eventLoopExecutor, UUID viewerUuid, Player player, boolean is18, boolean is13OrLater, boolean is119OrLater) { + super(logger, eventLoopExecutor, viewerUuid, is18, is13OrLater, is119OrLater); + this.player = player; + } + + @SneakyThrows + @Override + protected void sendPacket(MinecraftPacket packet) { + ReflectionUtil.getChannelWrapper(player).write(packet); + } + + @Override + protected boolean isExperimentalTabCompleteSmileys() { + return BungeeTabListPlus.getInstance().getConfig().experimentalTabCompleteSmileys; + } + + @Override + protected boolean isExperimentalTabCompleteFixForTabSize80() { + return BungeeTabListPlus.getInstance().getConfig().experimentalTabCompleteFixForTabSize80; + } + + @SneakyThrows + @Override + protected boolean isUsingAltRespawn() { + return WaterfallCompat.isDisableEntityMetadataRewrite() + || player.getProtocolVersion().getProtocol() >= 735 + && ProtocolVersion.isSupported(736); + } + + @Override + public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { + return PacketListenerResult.PASS; + } + + @Override + public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet) { + return PacketListenerResult.PASS; + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/listener/TabListListener.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/listener/TabListListener.java new file mode 100644 index 00000000..74e82271 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/listener/TabListListener.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package codecrafter47.bungeetablistplus.listener; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.player.VelocityPlayer; +import codecrafter47.bungeetablistplus.tablist.ExcludedServersTabOverlayProvider; +import codecrafter47.bungeetablistplus.util.GeyserCompat; +import com.velocitypowered.api.event.PostOrder; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.DisconnectEvent; +import com.velocitypowered.api.event.connection.PostLoginEvent; +import com.velocitypowered.api.event.proxy.ProxyReloadEvent; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import de.codecrafter47.taboverlay.TabView; +import de.codecrafter47.taboverlay.config.platform.EventListener; +import net.kyori.adventure.text.Component; + +public class TabListListener { + + private final BungeeTabListPlus btlp; + + public TabListListener(BungeeTabListPlus btlp) { + this.btlp = btlp; + } + + @Subscribe(order = PostOrder.LATE) + public void onPlayerJoin(PostLoginEvent e) { + try { + VelocityPlayer player = btlp.getBungeePlayerProvider().onPlayerConnected(e.getPlayer()); + + if (GeyserCompat.isBedrockPlayer(e.getPlayer().getUniqueId())) { + return; + } + + TabView tabView = btlp.getTabViewManager().onPlayerJoin(e.getPlayer()); + tabView.getTabOverlayProviders().addProvider(new ExcludedServersTabOverlayProvider(player, btlp)); + for (EventListener listener : btlp.getListeners()) { + listener.onTabViewAdded(tabView, player); + } + } catch (Throwable th) { + BungeeTabListPlus.getInstance().reportError(th); + } + } + + @Subscribe(order = PostOrder.FIRST) + public void onPlayerDisconnect(DisconnectEvent e) { + try { + btlp.getBungeePlayerProvider().onPlayerDisconnected(e.getPlayer()); + + if (GeyserCompat.isBedrockPlayer(e.getPlayer().getUniqueId())) { + return; + } + + TabView tabView = btlp.getTabViewManager().onPlayerDisconnect(e.getPlayer()); + tabView.deactivate(); + for (EventListener listener : btlp.getListeners()) { + listener.onTabViewRemoved(tabView); + } + + // hack to revert changes from https://github.com/SpigotMC/BungeeCord/commit/830f18a35725f637d623594eaaad50b566376e59 + e.getPlayer().getCurrentServer().ifPresent(server -> server.getPlayer().disconnect(Component.text("Quitting"))); + ((ConnectedPlayer) e.getPlayer()).setConnectedServer(null); + } catch (Throwable th) { + BungeeTabListPlus.getInstance().reportError(th); + } + } + + @Subscribe + public void onReload(ProxyReloadEvent event) { + btlp.reload(); + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/API.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/API.java new file mode 100644 index 00000000..4659e967 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/API.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.managers; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.api.velocity.BungeeTabListPlusAPI; +import codecrafter47.bungeetablistplus.api.velocity.CustomTablist; +import codecrafter47.bungeetablistplus.api.velocity.FakePlayerManager; +import codecrafter47.bungeetablistplus.api.velocity.Icon; +import codecrafter47.bungeetablistplus.api.velocity.ServerVariable; +import codecrafter47.bungeetablistplus.api.velocity.Variable; +import codecrafter47.bungeetablistplus.data.BTLPVelocityDataKeys; +import codecrafter47.bungeetablistplus.placeholder.PlayerPlaceholderResolver; +import codecrafter47.bungeetablistplus.placeholder.ServerPlaceholderResolver; +import codecrafter47.bungeetablistplus.player.VelocityPlayer; +import codecrafter47.bungeetablistplus.player.FakePlayerManagerImpl; +import codecrafter47.bungeetablistplus.tablist.DefaultCustomTablist; +import codecrafter47.bungeetablistplus.util.IconUtil; +import com.google.common.base.Preconditions; +import com.velocitypowered.api.proxy.Player; +import de.codecrafter47.data.api.DataKey; +import de.codecrafter47.taboverlay.TabView; +import de.codecrafter47.taboverlay.config.icon.IconManager; + +import javax.annotation.Nonnull; +import java.awt.image.BufferedImage; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class API extends BungeeTabListPlusAPI { + + private final TabViewManager tabViewManager; + private final IconManager iconManager; + private final PlayerPlaceholderResolver playerPlaceholderResolver; + private final ServerPlaceholderResolver serverPlaceholderResolver; + private final Logger logger; + private final BungeeTabListPlus btlp; + + private final Map variablesByName = new HashMap<>(); + private final Map serverVariablesByName = new HashMap<>(); + + public API(TabViewManager tabViewManager, IconManager iconManager, PlayerPlaceholderResolver playerPlaceholderResolver, ServerPlaceholderResolver serverPlaceholderResolver, Logger logger, BungeeTabListPlus btlp) { + this.tabViewManager = tabViewManager; + this.iconManager = iconManager; + this.playerPlaceholderResolver = playerPlaceholderResolver; + this.serverPlaceholderResolver = serverPlaceholderResolver; + this.logger = logger; + this.btlp = btlp; + } + + @Override + @SuppressWarnings("deprecation") + protected void setCustomTabList0(Player player, CustomTablist customTablist) { + TabView tabView = tabViewManager.getTabView(player); + if (tabView == null) { + throw new IllegalStateException("unknown player"); + } + if (customTablist instanceof DefaultCustomTablist) { + tabView.getTabOverlayProviders().removeProviders(DefaultCustomTablist.TabOverlayProviderImpl.class); + ((DefaultCustomTablist) customTablist).addToPlayer(tabView); + } else { + throw new IllegalArgumentException("customTablist not created by createCustomTablist()"); + } + } + + @Override + protected void removeCustomTabList0(Player player) { + TabView tabView = tabViewManager.getTabView(player); + if (tabView == null) { + throw new IllegalStateException("unknown player"); + } + tabView.getTabOverlayProviders().removeProviders(DefaultCustomTablist.TabOverlayProviderImpl.class); + } + + @Override + protected TabView getTabViewForPlayer0(Player player) { + TabView tabView = tabViewManager.getTabView(player); + if (tabView == null) { + throw new IllegalStateException("unknown player"); + } + return tabView; + } + + @Nonnull + @Override + protected Icon getIconFromPlayer0(Player player) { + return IconUtil.convert(IconUtil.getIconFromPlayer(player)); + } + + @Nonnull + @Override + protected de.codecrafter47.taboverlay.Icon getPlayerIcon0(Player player) { + return IconUtil.getIconFromPlayer(player); + } + + @Override + protected void createIcon0(BufferedImage image, Consumer callback) { + CompletableFuture future = iconManager.createIcon(image); + future.thenAccept(icon -> callback.accept(IconUtil.convert(icon))); + } + + @Override + protected CompletableFuture getIconFromImage0(BufferedImage image) { + return iconManager.createIcon(image); + } + + @Override + protected void registerVariable0(Object plugin, Variable variable) { + Preconditions.checkNotNull(plugin, "plugin"); + Preconditions.checkNotNull(variable, "variable"); + String id = variable.getName().toLowerCase(); + Preconditions.checkArgument(!variablesByName.containsKey(id), "Variable name already registered."); + DataKey dataKey = BTLPVelocityDataKeys.createBungeeThirdPartyVariableDataKey(id); + playerPlaceholderResolver.addCustomPlaceholderDataKey(id, dataKey); + btlp.scheduleSoftReload(); + variablesByName.put(id, variable); + } + + String resolveCustomPlaceholder(String id, Player player) { + Variable variable = variablesByName.get(id); + if (variable != null) { + try { + return variable.getReplacement(player); + } catch (Throwable th) { + logger.log(Level.SEVERE, "Failed to query custom placeholder replacement " + id, th); + } + } + return ""; + } + + @Override + protected void registerVariable0(Object plugin, ServerVariable variable) { + Preconditions.checkNotNull(plugin, "plugin"); + Preconditions.checkNotNull(variable, "variable"); + String id = variable.getName().toLowerCase(); + Preconditions.checkArgument(!serverVariablesByName.containsKey(id), "Variable name already registered."); + DataKey dataKey = BTLPVelocityDataKeys.createBungeeThirdPartyServerVariableDataKey(id); + serverPlaceholderResolver.addCustomPlaceholderServerDataKey(id, dataKey); + btlp.scheduleSoftReload(); + serverVariablesByName.put(id, variable); + } + + String resolveCustomPlaceholderServer(String id, String serverName) { + ServerVariable variable = serverVariablesByName.get(id); + if (variable != null) { + try { + return variable.getReplacement(serverName); + } catch (Throwable th) { + logger.log(Level.SEVERE, "Failed to query custom placeholder replacement " + id, th); + } + } + return ""; + } + + @Override + @SuppressWarnings("deprecation") + protected CustomTablist createCustomTablist0() { + return new DefaultCustomTablist(); + } + + @Override + protected FakePlayerManager getFakePlayerManager0() { + FakePlayerManagerImpl fakePlayerManager = btlp.getFakePlayerManagerImpl(); + if (fakePlayerManager == null) { + throw new IllegalStateException("Cannot call getFakePlayerManager() before onEnable()"); + } + return fakePlayerManager; + } + + @Override + protected boolean isHidden0(Player player) { + VelocityPlayer velocityPlayer = BungeeTabListPlus.getInstance().getBungeePlayerProvider().getPlayerIfPresent(player); + return velocityPlayer != null && velocityPlayer.get(BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN); + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/BungeePlayerProvider.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/BungeePlayerProvider.java new file mode 100644 index 00000000..a72ed97c --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/BungeePlayerProvider.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.managers; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.data.BTLPVelocityDataKeys; +import codecrafter47.bungeetablistplus.player.VelocityPlayer; +import com.velocitypowered.api.proxy.Player; +import de.codecrafter47.taboverlay.config.player.PlayerProvider; +import io.netty.util.concurrent.EventExecutor; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; + +public class BungeePlayerProvider implements PlayerProvider { + + private final EventExecutor mainThread; + + private final Map players = new ConcurrentHashMap<>(); + private final Set listeners = new ReferenceOpenHashSet<>(); + + public BungeePlayerProvider(EventExecutor mainThread) { + this.mainThread = mainThread; + mainThread.scheduleWithFixedDelay(this::checkForStalePlayers, 5, 5, TimeUnit.MINUTES); + } + + private void checkForStalePlayers() { + for (Map.Entry entry : players.entrySet()) { + if (!entry.getKey().isActive()) { + if (!entry.getValue().isStale()) { + entry.getValue().setStale(true); + } else { + BungeeTabListPlus.getInstance().getLogger().log(Level.SEVERE, "Player " + entry.getKey().getUsername() + " is no longer connected to the network, but PlayerDisconnectEvent has not been called."); + onPlayerDisconnected(entry.getKey()); + } + } + } + + } + + @Override + public Collection getPlayers() { + return Collections.unmodifiableCollection(players.values()); + } + + @Nonnull + public VelocityPlayer getPlayer(Player player) { + return Objects.requireNonNull(getPlayerIfPresent(player)); + } + + @Nullable + public VelocityPlayer getPlayerIfPresent(Player player) { + return players.get(player); + } + + public VelocityPlayer onPlayerConnected(Player player) { + VelocityPlayer velocityPlayer = new VelocityPlayer(player); + String version = BungeeTabListPlus.getInstance().getProtocolVersionProvider().getVersion(player); + boolean version_below_1_8 = !BungeeTabListPlus.getInstance().getProtocolVersionProvider().has18OrLater(player); + mainThread.execute(() -> { + velocityPlayer.getLocalDataCache().updateValue(BTLPVelocityDataKeys.DATA_KEY_CLIENT_VERSION, version); + velocityPlayer.getLocalDataCache().updateValue(BTLPVelocityDataKeys.DATA_KEY_CLIENT_VERSION_BELOW_1_8, version_below_1_8); + players.put(player, velocityPlayer); + listeners.forEach(listener -> listener.onPlayerAdded(velocityPlayer)); + }); + return velocityPlayer; + } + + public void onPlayerDisconnected(Player player) { + mainThread.execute(() -> { + VelocityPlayer velocityPlayer; + if (null == (velocityPlayer = players.remove(player))) { + return; + } + listeners.forEach(listener -> listener.onPlayerRemoved(velocityPlayer)); + }); + } + + @Override + public void registerListener(Listener listener) { + listeners.add(listener); + } + + @Override + public void unregisterListener(Listener listener) { + listeners.remove(listener); + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/DataManager.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/DataManager.java new file mode 100644 index 00000000..0ec226f7 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/DataManager.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.managers; + +import codecrafter47.bungeetablistplus.bridge.BukkitBridge; +import codecrafter47.bungeetablistplus.data.AbstractCompositeDataProvider; +import codecrafter47.bungeetablistplus.data.BTLPVelocityDataKeys; +import codecrafter47.bungeetablistplus.data.ServerDataHolder; +import codecrafter47.bungeetablistplus.data.TrackingDataCache; +import codecrafter47.bungeetablistplus.handler.GetGamemodeLogic; +import codecrafter47.bungeetablistplus.player.VelocityPlayer; +import codecrafter47.bungeetablistplus.util.IconUtil; +import codecrafter47.bungeetablistplus.util.MatchingStringsCollection; +import codecrafter47.bungeetablistplus.util.VelocityPlugin; +import com.imaginarycode.minecraft.redisbungee.RedisBungeeAPI; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ProxyServer; +import de.codecrafter47.data.api.*; +import de.codecrafter47.data.velocity.AbstractVelocityDataAccess; +import de.codecrafter47.data.velocity.PlayerDataAccess; +import de.codecrafter47.taboverlay.config.misc.Unchecked; +import io.netty.util.concurrent.EventExecutor; +import lombok.Getter; +import lombok.Setter; + +import javax.annotation.Nonnull; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +public class DataManager { + + private final API api; + private final EventExecutor mainThreadExecutor; + private final BungeePlayerProvider bungeePlayerProvider; + private final ServerStateManager serverStateManager; + private final BukkitBridge bukkitBridge; + + private final DataAccess playerDataAccess; + private final DataAccess serverDataAccess; + private final DataAccess proxyDataAccess; + private final Map> compositeDataProviders = new HashMap<>(); + + private final Map serverData = new ConcurrentHashMap<>(); + private final Map combinedServerData = new ConcurrentHashMap<>(); + + @Getter + private final TrackingDataCache proxyData = new TrackingDataCache(); + + @Setter + MatchingStringsCollection hiddenServers = new MatchingStringsCollection(Collections.emptyList()); + @Setter + List permanentlyHiddenPlayers = Collections.emptyList(); + + public DataManager(API api, VelocityPlugin plugin, Logger logger, BungeePlayerProvider bungeePlayerProvider, EventExecutor mainThreadExecutor, ServerStateManager serverStateManager, BukkitBridge bukkitBridge) { + this.api = api; + this.bungeePlayerProvider = bungeePlayerProvider; + this.mainThreadExecutor = mainThreadExecutor; + this.serverStateManager = serverStateManager; + this.bukkitBridge = bukkitBridge; + this.playerDataAccess = JoinedDataAccess.of(new PlayerDataAccess(plugin.getProxy(), plugin, logger), new LocalPlayerDataAccess(plugin, logger)); + this.serverDataAccess = new LocalServerDataAccess(plugin, logger); + this.proxyDataAccess = new ProxyDataAccess(plugin, logger); + + plugin.getProxy().getScheduler().buildTask(plugin, () -> updateData(plugin.getProxy())).delay(1, TimeUnit.SECONDS).repeat(1, TimeUnit.SECONDS).schedule(); + } + + public LocalDataCache createDataCacheForPlayer(VelocityPlayer player) { + return new LocalDataCache(player); + } + + public DataHolder getServerDataHolder(@Nonnull String serverName) { + if (!combinedServerData.containsKey(serverName)) { + combinedServerData.put(serverName, new ServerDataHolder(getLocalServerDataHolder(serverName), bukkitBridge.getServerDataHolder(serverName))); + } + return combinedServerData.get(serverName); + } + + public void addCompositeDataProvider(@Nonnull AbstractCompositeDataProvider provider) { + compositeDataProviders.put(provider.getCompositeDataKey().getId(), provider); + } + + private DataHolder getLocalServerDataHolder(@Nonnull String serverName) { + if (!serverData.containsKey(serverName)) { + serverData.putIfAbsent(serverName, new TrackingDataCache()); + } + return serverData.get(serverName); + } + + private void updateData(ProxyServer server) { + for (VelocityPlayer player : bungeePlayerProvider.getPlayers()) { + for (DataKey dataKey : player.getLocalDataCache().getActiveKeys()) { + if (playerDataAccess.provides(dataKey)) { + DataKey key = Unchecked.cast(dataKey); + updateIfNecessary(player.getLocalDataCache(), key, playerDataAccess.get(key, player.getPlayer())); + } + } + } + for (Map.Entry entry : serverData.entrySet()) { + String serverName = entry.getKey(); + TrackingDataCache dataCache = entry.getValue(); + for (DataKey dataKey : dataCache.getActiveKeys()) { + DataKey key = Unchecked.cast(dataKey); + updateIfNecessary(dataCache, key, serverDataAccess.get(key, serverName)); + } + } + for (DataKey dataKey : proxyData.getActiveKeys()) { + DataKey key = Unchecked.cast(dataKey); + updateIfNecessary(proxyData, key, proxyDataAccess.get(key, server)); + } + + } + + private void updateIfNecessary(DataCache data, DataKey key, T value) { + if (!Objects.equals(data.get(key), value)) { + mainThreadExecutor.execute(() -> data.updateValue(key, value)); + } + } + + private class LocalPlayerDataAccess extends AbstractVelocityDataAccess { + + LocalPlayerDataAccess(VelocityPlugin plugin, Logger logger) { + super(plugin.getProxy(), plugin, logger); + + addProvider(BTLPVelocityDataKeys.DATA_KEY_GAMEMODE, p -> GetGamemodeLogic.getGameMode(p.getUniqueId())); + addProvider(BTLPVelocityDataKeys.DATA_KEY_ICON, IconUtil::getIconFromPlayer); + addProvider(BTLPVelocityDataKeys.ThirdPartyPlaceholderBungee, (player, dataKey) -> api.resolveCustomPlaceholder(dataKey.getParameter(), player)); + addProvider(BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN_PLAYER_CONFIG, (player, dataKey) -> permanentlyHiddenPlayers.contains(player.getUsername()) || permanentlyHiddenPlayers.contains(player.getUniqueId().toString())); + + if (plugin.getProxy().getPluginManager().getPlugin("RedisBungee").isPresent()) { + addProvider(BTLPVelocityDataKeys.DATA_KEY_RedisBungee_ServerId, player -> RedisBungeeAPI.getRedisBungeeApi().getServerNameFor(player.getUniqueId())); + } + } + } + + private class LocalServerDataAccess extends AbstractVelocityDataAccess { + + LocalServerDataAccess(VelocityPlugin plugin, Logger logger) { + super(plugin.getProxy(), plugin, logger); + addProvider(BTLPVelocityDataKeys.DATA_KEY_SERVER_ONLINE, (serverName, dataKey) -> serverStateManager.isOnline(serverName)); + addProvider(BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN_SERVER_CONFIG, (serverName, dataKey) -> hiddenServers.contains(serverName)); + addProvider(BTLPVelocityDataKeys.ThirdPartyServerPlaceholderBungee, (serverName, dataKey) -> api.resolveCustomPlaceholderServer(dataKey.getParameter(), serverName)); + addProvider(BTLPVelocityDataKeys.DATA_KEY_ServerName, (serverName, dataKey) -> serverName); + } + } + + private class ProxyDataAccess extends AbstractVelocityDataAccess { + + ProxyDataAccess(VelocityPlugin plugin, Logger logger) { + super(plugin.getProxy(), plugin, logger); + addProvider(BTLPVelocityDataKeys.DATA_KEY_Server_Count, (proxy, dataKey) -> proxy.getAllServers().size()); + addProvider(BTLPVelocityDataKeys.DATA_KEY_Server_Count_Online, (proxy, dataKey) -> (int) proxy.getAllServers().stream().filter((key) -> serverStateManager.isOnline(key.getServerInfo().getName())).count()); + } + } + + public class LocalDataCache extends TrackingDataCache { + + private final de.codecrafter47.taboverlay.config.player.Player player; + + private LocalDataCache(de.codecrafter47.taboverlay.config.player.Player player) { + this.player = player; + } + + @Override + protected void addActiveKey(DataKey key) { + AbstractCompositeDataProvider compositeDataProvider = compositeDataProviders.get(key.getId()); + if (compositeDataProvider != null) { + compositeDataProvider.onPlayerAdded(player, Unchecked.cast(key)); + } else { + super.addActiveKey(key); + } + } + + @Override + protected void removeActiveKey(DataKey key) { + AbstractCompositeDataProvider compositeDataProvider = compositeDataProviders.get(key.getId()); + if (compositeDataProvider != null) { + compositeDataProvider.onPlayerRemoved(player, Unchecked.cast(key)); + } else { + super.removeActiveKey(key); + } + } + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/HiddenPlayersManager.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/HiddenPlayersManager.java new file mode 100644 index 00000000..55113290 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/HiddenPlayersManager.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.managers; + +import codecrafter47.bungeetablistplus.data.AbstractCompositeDataProvider; +import codecrafter47.bungeetablistplus.data.BTLPVelocityDataKeys; +import codecrafter47.bungeetablistplus.player.VelocityPlayer; +import de.codecrafter47.data.api.DataKey; +import de.codecrafter47.taboverlay.config.player.Player; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +public class HiddenPlayersManager extends AbstractCompositeDataProvider { + + private final List vanishProviders = new ArrayList<>(); + private boolean active = false; + + public HiddenPlayersManager() { + super(BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN); + } + + public void enable() { + active = true; + } + + public void disable() { + active = false; + } + + /** + * Adds a vanish provider. To be called during setup. + */ + public void addVanishProvider(String name, DataKey dataIsHidden) { + if (active) { + throw new IllegalStateException("Cannot call addVanishProvider() after enable()"); + } + this.vanishProviders.add(new VanishProvider(name, dataIsHidden)); + } + + @Override + protected void registerListener(Player player, DataKey key, Runnable listener) { + for (VanishProvider vanishProvider : vanishProviders) { + player.addDataChangeListener(vanishProvider.dataIsHidden, listener); + } + } + + @Override + protected void unregisterListener(Player player, DataKey key, Runnable listener) { + for (VanishProvider vanishProvider : vanishProviders) { + player.removeDataChangeListener(vanishProvider.dataIsHidden, listener); + } + } + + @Override + protected Boolean computeCompositeData(VelocityPlayer player, DataKey key) { + boolean hidden = false; + for (VanishProvider vanishProvider : vanishProviders) { + hidden = hidden || Boolean.TRUE.equals(player.get(vanishProvider.dataIsHidden)); + } + return hidden; + } + + public List getActiveVanishProviders(VelocityPlayer player) { + List activeVanishProviders = new ArrayList<>(); + for (VanishProvider vanishProvider : vanishProviders) { + if (Boolean.TRUE.equals(player.get(vanishProvider.dataIsHidden))) { + activeVanishProviders.add(vanishProvider.name); + } + } + return activeVanishProviders; + } + + @Data + private static class VanishProvider { + private final String name; + private final DataKey dataIsHidden; + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/RedisPlayerManager.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/RedisPlayerManager.java new file mode 100644 index 00000000..1f87d47e --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/RedisPlayerManager.java @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.managers; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.common.BTLPDataKeys; +import codecrafter47.bungeetablistplus.common.network.DataStreamUtils; +import codecrafter47.bungeetablistplus.common.network.TypeAdapterRegistry; +import codecrafter47.bungeetablistplus.data.BTLPDataTypes; +import codecrafter47.bungeetablistplus.data.BTLPVelocityDataKeys; +import codecrafter47.bungeetablistplus.player.VelocityPlayer; +import codecrafter47.bungeetablistplus.player.RedisPlayer; +import codecrafter47.bungeetablistplus.util.ProxyServer; +import com.google.common.collect.Sets; +import com.google.common.io.ByteArrayDataInput; +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; +import com.imaginarycode.minecraft.redisbungee.RedisBungeeAPI; +import com.imaginarycode.minecraft.redisbungee.events.PubSubMessageEvent; +import com.velocitypowered.api.event.Subscribe; +import de.codecrafter47.data.api.DataCache; +import de.codecrafter47.data.api.DataKey; +import de.codecrafter47.data.api.DataKeyRegistry; +import de.codecrafter47.data.bukkit.api.BukkitData; +import de.codecrafter47.data.minecraft.api.MinecraftData; +import de.codecrafter47.data.sponge.api.SpongeData; +import de.codecrafter47.data.velocity.api.VelocityData; +import de.codecrafter47.taboverlay.config.player.Player; +import de.codecrafter47.taboverlay.config.player.PlayerProvider; +import io.netty.util.concurrent.EventExecutor; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class RedisPlayerManager implements PlayerProvider { + + private static final TypeAdapterRegistry typeRegistry = TypeAdapterRegistry.of( + TypeAdapterRegistry.DEFAULT_TYPE_ADAPTERS, + BTLPDataTypes.REGISTRY); + + private static final DataKeyRegistry keyRegistry = DataKeyRegistry.of( + MinecraftData.class, + BukkitData.class, + SpongeData.class, + VelocityData.class, + BTLPDataKeys.class, + BTLPVelocityDataKeys.class); + + private static String CHANNEL_REQUEST_DATA_OLD = "btlp-data-request"; + private static String CHANNEL_DATA_OLD = "btlp-data"; + private static String CHANNEL_DATA_REQUEST = "btlp-data-req"; + private static String CHANNEL_DATA_UPDATE = "btlp-data-upd"; + + private final Map byUUID = new ConcurrentHashMap<>(); + private final BungeePlayerProvider bungeePlayerProvider; + private final BungeeTabListPlus plugin; + private final EventExecutor mainThread; + private final Logger logger; + private final Set listeners = new ReferenceOpenHashSet<>(); + + private boolean redisBungeeAPIError = false; + + private final Consumer missingDataKeyLogger = new Consumer() { + + private final Set missingKeys = Sets.newConcurrentHashSet(); + + @Override + public void accept(String id) { + if (missingKeys.add(id)) { + logger.warning("Missing data key with id " + id + ". Is the plugin up-to-date?"); + } + } + }; + + private boolean redisConnectionSuccessful = false; + + public RedisPlayerManager(BungeePlayerProvider bungeePlayerProvider, BungeeTabListPlus plugin, Logger logger) { + this.bungeePlayerProvider = bungeePlayerProvider; + this.plugin = plugin; + this.logger = logger; + this.mainThread = plugin.getMainThreadExecutor(); + + RedisBungeeAPI.getRedisBungeeApi().registerPubSubChannels(CHANNEL_REQUEST_DATA_OLD, CHANNEL_DATA_OLD); + RedisBungeeAPI.getRedisBungeeApi().registerPubSubChannels(CHANNEL_DATA_REQUEST, CHANNEL_DATA_UPDATE); + + plugin.getProxy().getScheduler().buildTask(BungeeTabListPlus.getInstance().getPlugin(), this::updatePlayers).delay(5, TimeUnit.SECONDS).repeat(5, TimeUnit.SECONDS).schedule(); + + plugin.getProxy().getEventManager().register(BungeeTabListPlus.getInstance().getPlugin(), this); + } + + @Override + public Collection getPlayers() { + return byUUID.values(); + } + + @Subscribe + @SuppressWarnings("unchecked") + public void onRedisMessage(PubSubMessageEvent event) { + String channel = event.getChannel(); + if (channel.equals(CHANNEL_DATA_REQUEST)) { + ByteArrayDataInput input = ByteStreams.newDataInput(Base64.getDecoder().decode(event.getMessage())); + try { + UUID uuid = DataStreamUtils.readUUID(input); + + com.velocitypowered.api.proxy.Player Player = ProxyServer.getInstance().getPlayer(uuid).orElse(null); + if (Player != null) { + VelocityPlayer player = bungeePlayerProvider.getPlayerIfPresent(Player); + if (player != null) { + DataKey key = DataStreamUtils.readDataKey(input, keyRegistry, missingDataKeyLogger); + + if (key != null) { + player.addDataChangeListener((DataKey) key, new DataChangeListener(player, (DataKey) key)); + updateData(uuid, (DataKey) key, player.get(key)); + } + + } + } + } catch (IOException ex) { + logger.log(Level.SEVERE, "Unexpected error reading redis message", ex); + } + } else if (channel.equals(CHANNEL_DATA_UPDATE)) { + ByteArrayDataInput input = ByteStreams.newDataInput(Base64.getDecoder().decode(event.getMessage())); + try { + UUID uuid = DataStreamUtils.readUUID(input); + + RedisPlayer player = byUUID.get(uuid); + if (player != null) { + DataCache cache = player.getData(); + DataKey key = DataStreamUtils.readDataKey(input, keyRegistry, missingDataKeyLogger); + + if (key != null) { + boolean removed = input.readBoolean(); + + if (removed) { + + mainThread.execute(() -> cache.updateValue(key, null)); + } else { + + Object value = typeRegistry.getTypeAdapter(key.getType()).read(input); + mainThread.execute(() -> cache.updateValue((DataKey) key, value)); + } + } + } + } catch (IOException ex) { + logger.log(Level.SEVERE, "Unexpected error reading redis message", ex); + } + } else if (channel.equals(CHANNEL_DATA_OLD) || channel.equals(CHANNEL_REQUEST_DATA_OLD)) { + logger.warning("BungeeTabListPlus on at least one proxy in your network is outdated."); + } + } + + private void updatePlayers() { + Set playersOnline; + try { + playersOnline = RedisBungeeAPI.getRedisBungeeApi().getPlayersOnline(); + } catch (Throwable th) { + if (!redisBungeeAPIError) { + logger.log(Level.WARNING, "Error using RedisBungee API", th); + redisBungeeAPIError = true; + } + return; + } + redisBungeeAPIError = false; + + // fetch names for new players + Map uuidToNameMap = new Object2ObjectOpenHashMap<>(); + for (UUID uuid : playersOnline) { + if (!byUUID.containsKey(uuid) && ProxyServer.getInstance().getPlayer(uuid) == null) { + try { + uuidToNameMap.put(uuid, RedisBungeeAPI.getRedisBungeeApi().getNameFromUuid(uuid)); + } catch (Throwable ex) { + logger.log(Level.WARNING, "Error while using RedisBungee API", ex); + } + } + } + + redisConnectionSuccessful = true; + + try { + mainThread.submit(() -> { + // remove players which have gone offline + for (Iterator iterator = byUUID.keySet().iterator(); iterator.hasNext(); ) { + UUID uuid = iterator.next(); + if (!playersOnline.contains(uuid) || ProxyServer.getInstance().getPlayer(uuid) != null) { + RedisPlayer redisPlayer = byUUID.get(uuid); + iterator.remove(); + listeners.forEach(listener -> listener.onPlayerRemoved(redisPlayer)); + } + } + + // add new players + for (UUID uuid : uuidToNameMap.keySet()) { + if (!byUUID.containsKey(uuid) && ProxyServer.getInstance().getPlayer(uuid) == null) { + RedisPlayer redisPlayer = new RedisPlayer(uuid, uuidToNameMap.get(uuid)); + byUUID.put(uuid, redisPlayer); + listeners.forEach(listener -> listener.onPlayerAdded(redisPlayer)); + } + } + }).sync(); + } catch (InterruptedException ignored) { + } + } + + @Override + public void registerListener(Listener listener) { + listeners.add(listener); + } + + @Override + public void unregisterListener(Listener listener) { + listeners.remove(listener); + } + + public void request(UUID uuid, DataKey key) { + try { + ByteArrayDataOutput data = ByteStreams.newDataOutput(); + DataStreamUtils.writeUUID(data, uuid); + DataStreamUtils.writeDataKey(data, key); + RedisBungeeAPI.getRedisBungeeApi().sendChannelMessage(CHANNEL_DATA_REQUEST, Base64.getEncoder().encodeToString(data.toByteArray())); + redisBungeeAPIError = false; + } catch (RuntimeException ex) { + if (!redisBungeeAPIError) { + logger.log(Level.WARNING, "Error using RedisBungee API", ex); + redisBungeeAPIError = true; + } + } catch (Throwable th) { + BungeeTabListPlus.getInstance().getLogger().log(Level.SEVERE, "Failed to request data", th); + } + } + + private void updateData(UUID uuid, DataKey key, T value) { + try { + ByteArrayDataOutput data = ByteStreams.newDataOutput(); + DataStreamUtils.writeUUID(data, uuid); + DataStreamUtils.writeDataKey(data, key); + data.writeBoolean(value == null); + if (value != null) { + typeRegistry.getTypeAdapter(key.getType()).write(data, value); + } + RedisBungeeAPI.getRedisBungeeApi().sendChannelMessage(CHANNEL_DATA_UPDATE, Base64.getEncoder().encodeToString(data.toByteArray())); + } catch (RuntimeException ex) { + BungeeTabListPlus.getInstance().getLogger().log(Level.WARNING, "RedisBungee Error", ex); + } catch (Throwable th) { + BungeeTabListPlus.getInstance().getLogger().log(Level.SEVERE, "Failed to send data", th); + } + } + + private class DataChangeListener implements Runnable { + private final Player player; + private final DataKey dataKey; + + private DataChangeListener(Player player, DataKey dataKey) { + this.player = player; + this.dataKey = dataKey; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + DataChangeListener that = (DataChangeListener) o; + + return player.equals(that.player) && dataKey.equals(that.dataKey); + + } + + @Override + public int hashCode() { + int result = player.hashCode(); + result = 31 * result + dataKey.hashCode(); + return result; + } + + @Override + public void run() { + RedisPlayerManager.this.updateData(player.getUniqueID(), dataKey, player.get(dataKey)); + } + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/ServerStateManager.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/ServerStateManager.java new file mode 100644 index 00000000..43b9955e --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/ServerStateManager.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.managers; + +import codecrafter47.bungeetablistplus.config.MainConfig; +import codecrafter47.bungeetablistplus.util.VelocityPlugin; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import com.velocitypowered.api.proxy.server.ServerInfo; +import com.velocitypowered.api.proxy.server.ServerPing; +import com.velocitypowered.api.scheduler.ScheduledTask; +import lombok.NonNull; + +import javax.annotation.Nonnull; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class ServerStateManager { + + private MainConfig config; + private final VelocityPlugin plugin; + + private final Map serverState = new HashMap<>(); + + public ServerStateManager(MainConfig config, VelocityPlugin plugin) { + this.config = config; + this.plugin = plugin; + } + + public boolean isOnline(@Nonnull @NonNull String name) { + PingTask state = getServerState(name); + return (state != null && state.isOnline()) || hasPlayers(name); + } + + private boolean hasPlayers(String name) { + RegisteredServer server = plugin.getProxy().getServer(name).orElse(null); + if (server == null) + return false; + Collection players = server.getPlayersConnected(); + if (players == null) + return false; + return !players.isEmpty(); + } + + public synchronized void updateConfig(MainConfig config) { + this.serverState.values().forEach(pingTask -> pingTask.task.cancel()); + this.serverState.clear(); + this.config = config; + } + + private synchronized PingTask getServerState(String serverName) { + PingTask task = serverState.get(serverName); + if (task != null) { + return task; + } + RegisteredServer server = plugin.getProxy().getServer(serverName).orElse(null); + if (server != null) { + // start server ping tasks + int delay = config.pingDelay; + if (delay <= 0 || delay > 10) { + delay = 10; + } + task = new PingTask(plugin.getProxy(), server); + serverState.put(serverName, task); + task.task = plugin.getProxy().getScheduler().buildTask(plugin, task).delay(delay, TimeUnit.SECONDS).repeat(delay, TimeUnit.SECONDS).schedule(); + } + return task; + } + + public static class PingTask implements Runnable { + + private final ProxyServer proxyServer; + private final RegisteredServer server; + private boolean online = true; + private int maxPlayers = Integer.MAX_VALUE; + private int onlinePlayers = 0; + private ScheduledTask task; + + public PingTask(ProxyServer proxyServer, RegisteredServer server) { + this.proxyServer = proxyServer; + this.server = server; + } + + public boolean isOnline() { + return online; + } + + public int getMaxPlayers() { + return maxPlayers; + } + + public int getOnlinePlayers() { + return onlinePlayers; + } + + @Override + public void run() { + if (!VelocityPlugin.isProxyRunning(proxyServer)) return; + server.ping().whenComplete((serverPing, throwable) -> { + if (throwable != null) { + online = false; + return; + } + if (serverPing == null) { + PingTask.this.online = false; + return; + } + online = true; + ServerPing.Players players = serverPing.getPlayers().orElse(null); + if (players != null) { + maxPlayers = players.getMax(); + onlinePlayers = players.getOnline(); + } else { + maxPlayers = 0; + onlinePlayers = 0; + } + }); + } + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/TabViewManager.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/TabViewManager.java new file mode 100644 index 00000000..cf4fedcc --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/TabViewManager.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.managers; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.handler.*; +import codecrafter47.bungeetablistplus.protocol.PacketHandler; +import codecrafter47.bungeetablistplus.protocol.PacketListener; +import codecrafter47.bungeetablistplus.util.GeyserCompat; +import codecrafter47.bungeetablistplus.util.ReflectionUtil; +import codecrafter47.bungeetablistplus.version.ProtocolVersionProvider; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.player.ServerPostConnectEvent; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; +import com.velocitypowered.proxy.network.Connections; +import de.codecrafter47.taboverlay.TabView; +import de.codecrafter47.taboverlay.config.misc.ChildLogger; +import de.codecrafter47.taboverlay.handler.TabOverlayHandler; +import io.netty.channel.EventLoop; + +import javax.annotation.Nullable; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class TabViewManager { + + private final BungeeTabListPlus btlp; + private final ProtocolVersionProvider protocolVersionProvider; + + private final Map playerTabViewMap = new ConcurrentHashMap<>(); + + public TabViewManager(BungeeTabListPlus btlp, ProtocolVersionProvider protocolVersionProvider) { + this.btlp = btlp; + this.protocolVersionProvider = protocolVersionProvider; + btlp.getPlugin().getProxy().getEventManager().register(btlp.getPlugin(), this); + } + + public TabView onPlayerJoin(Player player) { + if (playerTabViewMap.containsKey(player)) { + throw new AssertionError("Duplicate PostLoginEvent for player " + player.getUsername()); + } + + if (player.getCurrentServer().isPresent()) { + throw new AssertionError("Player already connected to server in PostLoginEvent: " + player.getUsername()); + } + + PlayerTabView tabView = createTabView(player); + playerTabViewMap.put(player, tabView); + return tabView; + } + + public TabView onPlayerDisconnect(Player player) { + PlayerTabView tabView = playerTabViewMap.remove(player); + + if (null == tabView) { + throw new AssertionError("Received PlayerDisconnectEvent for non-existent player " + player.getUsername()); + } + + tabView.deactivate(); + + return tabView; + } + + @Subscribe + public void onServerConnected(ServerPostConnectEvent event) { + if (GeyserCompat.isBedrockPlayer(event.getPlayer().getUniqueId())) { + return; + } + try { + Player player = event.getPlayer(); + + PlayerTabView tabView = playerTabViewMap.get(player); + + if (tabView == null) { + throw new AssertionError("Received ServerSwitchEvent for non-existent player " + player.getUsername()); + } + + VelocityServerConnection server = (VelocityServerConnection) event.getPlayer().getCurrentServer().orElse(null); + + MinecraftConnection wrapper = server.getConnection(); + + PacketHandler packetHandler = tabView.packetHandler; + PacketListener packetListener = new PacketListener(server, packetHandler, player); + + wrapper.getChannel().pipeline().addBefore(Connections.HANDLER, "btlp-packet-listener", packetListener); + + packetHandler.onServerSwitch(protocolVersionProvider.has113OrLater(player)); + + } catch (Exception ex) { + btlp.getLogger().log(Level.SEVERE, "Failed to inject packet listener", ex); + } + } + + @Nullable + public TabView getTabView(Player player) { + return playerTabViewMap.get(player); + } + + private PlayerTabView createTabView(Player player) { + try { + TabOverlayHandler tabOverlayHandler; + PacketHandler packetHandler; + + Logger logger = new ChildLogger(btlp.getLogger(), player.getUsername()); + EventLoop eventLoop = ReflectionUtil.getChannelWrapper(player).eventLoop(); + + if (protocolVersionProvider.has1193OrLater(player)) { + NewTabOverlayHandler handler = new NewTabOverlayHandler(logger, eventLoop, player); + tabOverlayHandler = handler; + packetHandler = new RewriteLogic(new GetGamemodeLogic(handler, player.getUniqueId())); + } else if (protocolVersionProvider.has18OrLater(player)) { + LowMemoryTabOverlayHandlerImpl tabOverlayHandlerImpl = new LowMemoryTabOverlayHandlerImpl(logger, eventLoop, player.getUniqueId(), player, protocolVersionProvider.is18(player), protocolVersionProvider.has113OrLater(player), protocolVersionProvider.has119OrLater(player)); + tabOverlayHandler = tabOverlayHandlerImpl; + packetHandler = new RewriteLogic(new GetGamemodeLogic(tabOverlayHandlerImpl, player.getUniqueId())); + } else { + LegacyTabOverlayHandlerImpl legacyTabOverlayHandler = new LegacyTabOverlayHandlerImpl(logger, ReflectionUtil.getTablistHandler(player).getEntries().size(), eventLoop, player, protocolVersionProvider.has113OrLater(player)); + tabOverlayHandler = legacyTabOverlayHandler; + packetHandler = legacyTabOverlayHandler; + } + + return new PlayerTabView(tabOverlayHandler, logger, btlp.getAsyncExecutor(), packetHandler); + + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new AssertionError("Failed to create tab view", e); + } + } + + private static class PlayerTabView extends TabView { + + private final PacketHandler packetHandler; + + private PlayerTabView(TabOverlayHandler tabOverlayHandler, Logger logger, Executor updateExecutor, PacketHandler packetHandler) { + super(tabOverlayHandler, logger, updateExecutor); + this.packetHandler = packetHandler; + } + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/ComponentServerPlaceholderResolver.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/ComponentServerPlaceholderResolver.java new file mode 100644 index 00000000..00b0a7a9 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/ComponentServerPlaceholderResolver.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.placeholder; + +import codecrafter47.bungeetablistplus.BTLPContextKeys; +import codecrafter47.bungeetablistplus.managers.DataManager; +import de.codecrafter47.data.api.TypeToken; +import de.codecrafter47.taboverlay.config.context.Context; +import de.codecrafter47.taboverlay.config.placeholder.*; +import de.codecrafter47.taboverlay.config.player.Player; +import de.codecrafter47.taboverlay.config.player.PlayerSet; +import de.codecrafter47.taboverlay.config.template.TemplateCreationContext; +import de.codecrafter47.taboverlay.config.view.AbstractActiveElement; + +import javax.annotation.Nonnull; +import java.util.List; + +public class ComponentServerPlaceholderResolver implements PlaceholderResolver { + private final ServerPlaceholderResolver serverPlaceholderResolver; + private final DataManager dataManager; + + public ComponentServerPlaceholderResolver(ServerPlaceholderResolver serverPlaceholderResolver, DataManager dataManager) { + this.serverPlaceholderResolver = serverPlaceholderResolver; + this.dataManager = dataManager; + } + + @Nonnull + @Override + public PlaceholderBuilder resolve(PlaceholderBuilder builder, List args, TemplateCreationContext tcc) throws UnknownPlaceholderException, PlaceholderException { + if (args.size() >= 1 && "server".equalsIgnoreCase(args.get(0).getText())) { + args.remove(0); + try { + return serverPlaceholderResolver.resolve(builder.transformContext(context -> dataManager.getServerDataHolder(context.getCustomObject(BTLPContextKeys.SERVER_ID))), args, tcc); + } catch (UnknownPlaceholderException e) { + throw new PlaceholderException("Unknown Placeholder"); + } + } + if (args.size() >= 1 && "server_player_count".equalsIgnoreCase(args.get(0).getText())) { + args.remove(0); + + return builder.acquireData(ServerPlayerCountPlaceholder::new, TypeToken.INTEGER); + } + throw new UnknownPlaceholderException(); + } + + private static class ServerPlayerCountPlaceholder extends AbstractActiveElement implements PlaceholderDataProvider, PlayerSet.Listener { + + private PlayerSet playerSet; + + @Override + protected void onActivation() { + playerSet = getContext().getCustomObject(BTLPContextKeys.SERVER_PLAYER_SET); + playerSet.addListener(this); + } + + @Override + public Integer getData() { + return playerSet.getCount(); + } + + @Override + protected void onDeactivation() { + playerSet.removeListener(this); + } + + @Override + public void onPlayerAdded(Player player) { + if (hasListener()) { + getListener().run(); + } + } + + @Override + public void onPlayerRemoved(Player player) { + if (hasListener()) { + getListener().run(); + } + } + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/GlobalServerPlaceholderResolver.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/GlobalServerPlaceholderResolver.java new file mode 100644 index 00000000..7b3b1e40 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/GlobalServerPlaceholderResolver.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.placeholder; + +import codecrafter47.bungeetablistplus.managers.DataManager; +import de.codecrafter47.data.api.DataHolder; +import de.codecrafter47.taboverlay.config.context.Context; +import de.codecrafter47.taboverlay.config.placeholder.*; +import de.codecrafter47.taboverlay.config.template.TemplateCreationContext; + +import javax.annotation.Nonnull; +import java.util.List; + +public class GlobalServerPlaceholderResolver implements PlaceholderResolver { + + private final DataManager dataManager; + private final ServerPlaceholderResolver serverPlaceholderResolver; + + public GlobalServerPlaceholderResolver(DataManager dataManager, ServerPlaceholderResolver serverPlaceholderResolver) { + this.dataManager = dataManager; + this.serverPlaceholderResolver = serverPlaceholderResolver; + } + + @Nonnull + @Override + public PlaceholderBuilder resolve(PlaceholderBuilder builder, List args, TemplateCreationContext tcc) throws UnknownPlaceholderException, PlaceholderException { + if (args.size() >= 1 && args.get(0) instanceof PlaceholderArg.Text && args.get(0).getText().startsWith("server:")) { + String serverName = args.remove(0).getText().substring(7); + DataHolder serverDataHolder = dataManager.getServerDataHolder(serverName); + return serverPlaceholderResolver.resolve(builder.transformContext(context -> serverDataHolder), args, tcc); + } + throw new UnknownPlaceholderException(); + } + +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/PlayerPlaceholderResolver.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/PlayerPlaceholderResolver.java new file mode 100644 index 00000000..f7727fcc --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/PlayerPlaceholderResolver.java @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.placeholder; + +import codecrafter47.bungeetablistplus.cache.Cache; +import codecrafter47.bungeetablistplus.common.BTLPDataKeys; +import codecrafter47.bungeetablistplus.data.BTLPVelocityDataKeys; +import de.codecrafter47.data.api.DataKey; +import de.codecrafter47.data.api.TypeToken; +import de.codecrafter47.data.bukkit.api.BukkitData; +import de.codecrafter47.data.minecraft.api.MinecraftData; +import de.codecrafter47.data.velocity.api.VelocityData; +import de.codecrafter47.taboverlay.config.context.Context; +import de.codecrafter47.taboverlay.config.expression.ExpressionUpdateListener; +import de.codecrafter47.taboverlay.config.expression.ToStringExpression; +import de.codecrafter47.taboverlay.config.expression.template.ExpressionTemplate; +import de.codecrafter47.taboverlay.config.misc.ChatFormat; +import de.codecrafter47.taboverlay.config.placeholder.*; +import de.codecrafter47.taboverlay.config.player.Player; +import de.codecrafter47.taboverlay.config.template.TemplateCreationContext; +import de.codecrafter47.taboverlay.config.view.AbstractActiveElement; +import lombok.val; + +import javax.annotation.Nonnull; +import java.util.*; +import java.util.function.Function; + +public class PlayerPlaceholderResolver extends AbstractPlayerPlaceholderResolver { + + private final ServerPlaceholderResolver serverPlaceholderResolver; + private final Cache cache; + + private final Set placeholderAPIPluginPrefixes = Collections.synchronizedSet(new HashSet<>()); + private final Map> bridgeCustomPlaceholderDataKeys = Collections.synchronizedMap(new HashMap<>()); + private final Map> customPlaceholderDataKeys = Collections.synchronizedMap(new HashMap<>()); + + private final Map aliasMap = new HashMap<>(); + + public PlayerPlaceholderResolver(ServerPlaceholderResolver serverPlaceholderResolver, Cache cache) { + super(); + this.serverPlaceholderResolver = serverPlaceholderResolver; + this.cache = cache; + addPlaceholder("ping", create(VelocityData.Velocity_Ping)); + addPlaceholder("luckpermsbungee_prefix", create(VelocityData.LuckPerms_Prefix)); + addPlaceholder("luckpermsbungee_suffix", create(VelocityData.LuckPerms_Suffix)); + addPlaceholder("luckpermsbungee_primary_group", create(VelocityData.LuckPerms_PrimaryGroup)); + addPlaceholder("luckpermsbungee_primary_group_weight", create(VelocityData.LuckPerms_Weight)); + addPlaceholder("client_version", create(BTLPVelocityDataKeys.DATA_KEY_CLIENT_VERSION)); + addPlaceholder("client_version_below_1_8", create(BTLPVelocityDataKeys.DATA_KEY_CLIENT_VERSION_BELOW_1_8)); + addPlaceholder("client_version_atleast_1_8", create(BTLPVelocityDataKeys.DATA_KEY_CLIENT_VERSION_BELOW_1_8, b -> !b, TypeToken.BOOLEAN)); + addPlaceholder("world", create(MinecraftData.World)); + addPlaceholder("team", create(MinecraftData.Team)); + addPlaceholder("team_color", create(MinecraftData.TeamColor)); + addPlaceholder("team_display_name", create(MinecraftData.TeamDisplayName)); + addPlaceholder("team_prefix", create(MinecraftData.TeamPrefix)); + addPlaceholder("team_suffix", create(MinecraftData.TeamSuffix)); + addPlaceholder("vault_balance", create(MinecraftData.Economy_Balance)); + addPlaceholder("vault_balance2", create(MinecraftData.Economy_Balance, b -> { + if (b == null) { + return ""; + } else if (b >= 10_000_000) { + return String.format("%1.0fM", b / 1_000_000); + } else if (b >= 10_000) { + return String.format("%1.0fK", b / 1_000); + } else if (b >= 100) { + return String.format("%1.0f", b); + } else { + return String.format("%1.2f", b); + } + }, TypeToken.STRING)); + addPlaceholder("multiverse_world_alias", create(BukkitData.Multiverse_WorldAlias)); + addPlaceholder("faction_name", create(BukkitData.Factions_FactionName)); + addPlaceholder("faction_member_count", create(BukkitData.Factions_FactionMembers)); + addPlaceholder("faction_online_member_count", create(BukkitData.Factions_OnlineFactionMembers)); + addPlaceholder("faction_at_current_location", create(BukkitData.Factions_FactionsWhere)); + addPlaceholder("faction_power", create(BukkitData.Factions_FactionPower)); + addPlaceholder("faction_player_power", create(BukkitData.Factions_PlayerPower)); + addPlaceholder("faction_rank", create(BukkitData.Factions_FactionsRank)); + addPlaceholder("SimpleClans_ClanName", create(BukkitData.SimpleClans_ClanName)); + addPlaceholder("SimpleClans_ClanMembers", create(BukkitData.SimpleClans_ClanMembers)); + addPlaceholder("SimpleClans_OnlineClanMembers", create(BukkitData.SimpleClans_OnlineClanMembers)); + addPlaceholder("SimpleClans_ClanTag", create(BukkitData.SimpleClans_ClanTag)); + addPlaceholder("SimpleClans_ClanTagLabel", create(BukkitData.SimpleClans_ClanTagLabel)); + addPlaceholder("SimpleClans_ClanColorTag", create(BukkitData.SimpleClans_ClanColorTag)); + addPlaceholder("vault_primary_group", create(MinecraftData.Permissions_PermissionGroup)); + addPlaceholder("vault_prefix", create(MinecraftData.Permissions_Prefix)); + addPlaceholder("vault_prefix_color", create(MinecraftData.Permissions_Prefix, p -> { + return ChatFormat.lastFormatCode(p); + }, TypeToken.STRING)); + addPlaceholder("vault_suffix", create(MinecraftData.Permissions_Suffix)); + addPlaceholder("vault_primary_group_prefix", create(MinecraftData.Permissions_PrimaryGroupPrefix)); + addPlaceholder("vault_primary_group_prefix_color", create(MinecraftData.Permissions_PrimaryGroupPrefix, p -> { + return ChatFormat.lastFormatCode(p); + }, TypeToken.STRING)); + addPlaceholder("vault_player_prefix", create(MinecraftData.Permissions_PlayerPrefix)); + addPlaceholder("vault_player_prefix_color", create(MinecraftData.Permissions_PlayerPrefix, p -> { + return ChatFormat.lastFormatCode(p); + }, TypeToken.STRING)); + addPlaceholder("vault_primary_group_weight", create(MinecraftData.Permissions_PermissionGroupWeight)); + addPlaceholder("health", create(MinecraftData.Health)); + addPlaceholder("max_health", create(MinecraftData.MaxHealth)); + addPlaceholder("location_x", create(MinecraftData.PosX)); + addPlaceholder("location_y", create(MinecraftData.PosY)); + addPlaceholder("location_z", create(MinecraftData.PosZ)); + addPlaceholder("xp", create(MinecraftData.XP)); + addPlaceholder("total_xp", create(MinecraftData.TotalXP)); + addPlaceholder("level", create(MinecraftData.Level)); + addPlaceholder("player_points", create(BukkitData.PlayerPoints_Points)); + addPlaceholder("vault_currency", create(MinecraftData.Economy_CurrencyNameSingular)); + addPlaceholder("vault_currency_plural", create(MinecraftData.Economy_CurrencyNamePlural)); + addPlaceholder("tab_name", create(BukkitData.PlayerListName, (player, name) -> name == null ? player.getName() : name, TypeToken.STRING)); + addPlaceholder("display_name", create(MinecraftData.DisplayName, (player, name) -> name == null ? player.getName() : name, TypeToken.STRING)); + addPlaceholder("velocity_display_name", create(VelocityData.Velocity_DisplayName, (player, name) -> name == null ? player.getName() : name, TypeToken.STRING)); + addPlaceholder("session_duration_seconds", create(VelocityData.Velocity_SessionDuration, duration -> duration == null ? null : (int) (duration.getSeconds() % 60), TypeToken.INTEGER)); + addPlaceholder("session_duration_total_seconds", create(VelocityData.Velocity_SessionDuration, duration -> duration == null ? null : (int) duration.getSeconds(), TypeToken.INTEGER)); + addPlaceholder("session_duration_minutes", create(VelocityData.Velocity_SessionDuration, duration -> duration == null ? null : (int) ((duration.getSeconds() % 3600) / 60), TypeToken.INTEGER)); + addPlaceholder("session_duration_hours", create(VelocityData.Velocity_SessionDuration, duration -> duration == null ? null : (int) (duration.getSeconds() / 3600), TypeToken.INTEGER)); + addPlaceholder("essentials_afk", create(BukkitData.Essentials_IsAFK)); + addPlaceholder("is_hidden", create(BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN)); + addPlaceholder("gamemode", create(BTLPVelocityDataKeys.DATA_KEY_GAMEMODE)); + addPlaceholder("redisbungee_server_id", create(BTLPVelocityDataKeys.DATA_KEY_RedisBungee_ServerId)); + addPlaceholder("askyblock_island_level", create(BukkitData.ASkyBlock_IslandLevel)); + addPlaceholder("askyblock_island_name", create(BukkitData.ASkyBlock_IslandName)); + addPlaceholder("askyblock_team_leader", create(BukkitData.ASkyBlock_TeamLeader)); + addPlaceholder("permission", this::resolvePermissionPlaceholder); + + // Server + addPlaceholder("server", this::resolveServerPlaceholder); + } + + @Nonnull + private PlaceholderBuilder resolveServerPlaceholder(PlaceholderBuilder builder, List args, TemplateCreationContext tcc) throws PlaceholderException { + try { + // The player's data holder should allow transparent access to the server data, so this is fine + return serverPlaceholderResolver.resolve(builder.transformContext(c -> c), args, tcc); + } catch (UnknownPlaceholderException e) { + throw new PlaceholderException("Unknown placeholder"); + } + } + + @Nonnull + private PlaceholderBuilder resolvePermissionPlaceholder(PlaceholderBuilder builder, List args, TemplateCreationContext tcc) throws PlaceholderException { + if (args.isEmpty()) { + throw new PlaceholderException("Use of permission placeholder lacks specification of specific permission"); + } + if (args.get(0) instanceof PlaceholderArg.Text) { + String permission = args.remove(0).getText(); + return builder.acquireData(new PlayerPlaceholderDataProviderSupplier<>(TypeToken.BOOLEAN, BTLPVelocityDataKeys.permission(permission), (p, d) -> d), TypeToken.BOOLEAN); + } else { + ExpressionTemplate permission = args.remove(0).getExpression(); + Function playerFunction = builder.getContextTransformation(); + return PlaceholderBuilder.create().acquireData(() -> new PermissionDataProvider(permission.instantiateWithStringResult(), playerFunction), TypeToken.BOOLEAN, builder.isRequiresViewerContext() || permission.requiresViewerContext()); + } + } + + @Nonnull + @Override + public PlaceholderBuilder resolve(PlaceholderBuilder builder, List args, TemplateCreationContext tcc) throws UnknownPlaceholderException, PlaceholderException { + if (!args.isEmpty() && aliasMap.containsKey(args.get(0).getText())) { + String replacement = aliasMap.get(args.get(0).getText()); + tcc.getErrorHandler().addWarning("Placeholder '" + args.get(0).getText() + "' has been deprecated. Use '" + replacement + "' instead.", null); + args.set(0, new PlaceholderArg.Text(replacement)); + } + try { + return super.resolve(builder, args, tcc); + } catch (UnknownPlaceholderException e) { + if (!args.isEmpty()) { + String id = args.get(0).getText().toLowerCase(); + PlaceholderBuilder result = null; + if (customPlaceholderDataKeys.containsKey(id)) { + DataKey dataKey = customPlaceholderDataKeys.get(id); + result = builder.acquireData(new PlayerPlaceholderDataProviderSupplier<>(TypeToken.STRING, dataKey, (player, replacement) -> replacement), TypeToken.STRING); + } else if (bridgeCustomPlaceholderDataKeys.containsKey(id)) { + DataKey dataKey = bridgeCustomPlaceholderDataKeys.get(id); + result = builder.acquireData(new PlayerPlaceholderDataProviderSupplier<>(TypeToken.STRING, dataKey, (player, replacement) -> replacement), TypeToken.STRING); + } else { + for (String prefix : placeholderAPIPluginPrefixes) { + if (id.length() >= prefix.length() && id.substring(0, prefix.length()).equalsIgnoreCase(prefix)) { + id = args.remove(0).getText(); + val resolver = create(BTLPDataKeys.createPlaceholderAPIDataKey("%" + id + "%")); + addPlaceholder(id, resolver); + return resolver.resolve(builder, args, tcc); + } + } + } + if (result == null) { + // prevent errors because bridge information has not been synced yet + if (cache.getCustomPlaceholdersBridge().contains(id)) { + result = builder.acquireData(new PlayerPlaceholderDataProviderSupplier<>(TypeToken.STRING, BTLPDataKeys.createThirdPartyVariableDataKey(id), (player, replacement) -> replacement), TypeToken.STRING); + } else { + for (String prefix : cache.getPAPIPrefixes()) { + if (id.length() >= prefix.length() && id.substring(0, prefix.length()).equalsIgnoreCase(prefix)) { + result = builder.acquireData(new PlayerPlaceholderDataProviderSupplier<>(TypeToken.STRING, BTLPDataKeys.createPlaceholderAPIDataKey("%" + id + "%"), (player, replacement) -> replacement), TypeToken.STRING); + } + } + } + } + if (result != null) { + args.remove(0); + return result; + } + } + throw e; + } + } + + public void addPlaceholderAPIPluginPrefixes(Collection prefixes) { + placeholderAPIPluginPrefixes.addAll(prefixes); + } + + public void addCustomPlaceholderDataKey(String id, DataKey dataKey) { + customPlaceholderDataKeys.put(id.toLowerCase(), dataKey); + } + + public void addBridgeCustomPlaceholderDataKey(String id, DataKey dataKey) { + bridgeCustomPlaceholderDataKeys.put(id.toLowerCase(), dataKey); + } + + private static class PermissionDataProvider extends AbstractActiveElement implements PlaceholderDataProvider, ExpressionUpdateListener { + private final ToStringExpression permission; + private final Function playerFunction; + private DataKey permissionDataKey; + + private PermissionDataProvider(ToStringExpression permission, Function playerFunction) { + this.permission = permission; + this.playerFunction = playerFunction; + } + + @Override + protected void onActivation() { + permission.activate(getContext(), this); + permissionDataKey = BTLPVelocityDataKeys.permission(permission.evaluate()); + if (hasListener()) { + playerFunction.apply(getContext()).addDataChangeListener(permissionDataKey, getListener()); + } + } + + @Override + protected void onDeactivation() { + permission.deactivate(); + if (hasListener()) { + playerFunction.apply(getContext()).removeDataChangeListener(permissionDataKey, getListener()); + } + } + + @Override + public Boolean getData() { + return playerFunction.apply(getContext()).get(permissionDataKey); + } + + @Override + public void onExpressionUpdate() { + if (hasListener()) { + playerFunction.apply(getContext()).removeDataChangeListener(permissionDataKey, getListener()); + } + permissionDataKey = BTLPVelocityDataKeys.permission(permission.evaluate()); + if (hasListener()) { + playerFunction.apply(getContext()).addDataChangeListener(permissionDataKey, getListener()); + getListener().run(); + } + } + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/ServerCountPlaceholderResolver.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/ServerCountPlaceholderResolver.java new file mode 100644 index 00000000..73669690 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/ServerCountPlaceholderResolver.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.placeholder; + +import codecrafter47.bungeetablistplus.data.BTLPVelocityDataKeys; +import codecrafter47.bungeetablistplus.managers.DataManager; +import de.codecrafter47.data.api.TypeToken; +import de.codecrafter47.taboverlay.config.context.Context; +import de.codecrafter47.taboverlay.config.placeholder.*; +import de.codecrafter47.taboverlay.config.template.TemplateCreationContext; + +import javax.annotation.Nonnull; +import java.util.List; + +public class ServerCountPlaceholderResolver implements PlaceholderResolver { + + private final DataManager dataManager; + + public ServerCountPlaceholderResolver(DataManager dataManager) { + this.dataManager = dataManager; + } + + @Nonnull + @Override + public PlaceholderBuilder resolve(PlaceholderBuilder builder, List args, TemplateCreationContext tcc) throws UnknownPlaceholderException, PlaceholderException { + if (args.size() >= 2 && "server_count".equalsIgnoreCase(args.get(0).getText()) && "total".equalsIgnoreCase(args.get(1).getText())) { + args.remove(0); + args.remove(0); + return builder.transformContext(context -> dataManager.getProxyData()) + .acquireData(new DataHolderPlaceholderDataProviderSupplier<>(TypeToken.INTEGER, BTLPVelocityDataKeys.DATA_KEY_Server_Count, (server, replacement) -> replacement), TypeToken.INTEGER); + } + if (args.size() >= 2 && "server_count".equalsIgnoreCase(args.get(0).getText()) && "online".equalsIgnoreCase(args.get(1).getText())) { + args.remove(0); + args.remove(0); + return builder.transformContext(context -> dataManager.getProxyData()) + .acquireData(new DataHolderPlaceholderDataProviderSupplier<>(TypeToken.INTEGER, BTLPVelocityDataKeys.DATA_KEY_Server_Count_Online, (server, replacement) -> replacement), TypeToken.INTEGER); + } + throw new UnknownPlaceholderException(); + } + +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/ServerPlaceholderResolver.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/ServerPlaceholderResolver.java new file mode 100644 index 00000000..5453ff0a --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/ServerPlaceholderResolver.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.placeholder; + +import codecrafter47.bungeetablistplus.cache.Cache; +import codecrafter47.bungeetablistplus.common.BTLPDataKeys; +import codecrafter47.bungeetablistplus.data.BTLPVelocityDataKeys; +import de.codecrafter47.data.api.DataHolder; +import de.codecrafter47.data.api.DataKey; +import de.codecrafter47.data.api.TypeToken; +import de.codecrafter47.data.minecraft.api.MinecraftData; +import de.codecrafter47.taboverlay.config.placeholder.*; +import de.codecrafter47.taboverlay.config.template.TemplateCreationContext; + +import javax.annotation.Nonnull; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ServerPlaceholderResolver extends AbstractDataHolderPlaceholderResolver { + + private final Map> bridgeCustomPlaceholderServerDataKeys = Collections.synchronizedMap(new HashMap<>()); + private final Map> customPlaceholderServerDataKeys = Collections.synchronizedMap(new HashMap<>()); + + private final Cache cache; + + public ServerPlaceholderResolver(Cache cache) { + this.cache = cache; + + setDefaultPlaceholder(builder -> builder.acquireData(new DataHolderPlaceholderDataProviderSupplier<>(TypeToken.STRING, BTLPVelocityDataKeys.DATA_KEY_ServerName, (c, d) -> d), TypeToken.STRING)); + addPlaceholder("tps", create(MinecraftData.TPS)); + addPlaceholder("name", create(BTLPVelocityDataKeys.DATA_KEY_ServerName)); + addPlaceholder("online", create(BTLPVelocityDataKeys.DATA_KEY_SERVER_ONLINE)); + } + + @Nonnull + @Override + public PlaceholderBuilder resolve(PlaceholderBuilder builder, List args, TemplateCreationContext tcc) throws UnknownPlaceholderException, PlaceholderException { + try { + return super.resolve(builder, args, tcc); + } catch (UnknownPlaceholderException e) { + if (!args.isEmpty()) { + String id = args.get(0).getText().toLowerCase(); + PlaceholderBuilder result = null; + if (customPlaceholderServerDataKeys.containsKey(id)) { + DataKey dataKey = customPlaceholderServerDataKeys.get(id); + result = builder.acquireData(new DataHolderPlaceholderDataProviderSupplier<>(TypeToken.STRING, dataKey, (server, replacement) -> replacement), TypeToken.STRING); + } else if (bridgeCustomPlaceholderServerDataKeys.containsKey(id)) { + DataKey dataKey = bridgeCustomPlaceholderServerDataKeys.get(id); + result = builder.acquireData(new DataHolderPlaceholderDataProviderSupplier<>(TypeToken.STRING, dataKey, (player, replacement) -> replacement), TypeToken.STRING); + } else if (cache.getCustomServerPlaceholdersBridge().contains(id)) { + // prevent warnings because bridge data has not been synced yet + result = builder.acquireData(new DataHolderPlaceholderDataProviderSupplier<>(TypeToken.STRING, BTLPDataKeys.createThirdPartyServerVariableDataKey(id), (player, replacement) -> replacement), TypeToken.STRING); + } + if (result != null) { + args.remove(0); + return result; + } + } + throw e; + } + } + + public void addCustomPlaceholderServerDataKey(String id, DataKey dataKey) { + customPlaceholderServerDataKeys.put(id.toLowerCase(), dataKey); + } + + public void addBridgeCustomPlaceholderServerDataKey(String id, DataKey dataKey) { + bridgeCustomPlaceholderServerDataKeys.put(id.toLowerCase(), dataKey); + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/player/AbstractPlayer.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/player/AbstractPlayer.java new file mode 100644 index 00000000..bebd5a74 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/player/AbstractPlayer.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.player; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.data.NullDataHolder; +import de.codecrafter47.data.api.DataHolder; +import de.codecrafter47.data.api.DataKey; +import de.codecrafter47.data.velocity.api.VelocityData; +import de.codecrafter47.taboverlay.config.player.Player; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import lombok.Value; + +import javax.annotation.Nonnull; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.function.Function; + +public abstract class AbstractPlayer implements Player { + private final UUID uuid; + private final String name; + + final DataHolder serverData; + + AbstractPlayer(UUID uuid, String name) { + this.uuid = uuid; + this.name = name; + this.serverData = new ServerDataHolder(AbstractPlayer::getServerDataHolder); + } + + @Nonnull + @Override + public final String getName() { + return name; + } + + @Nonnull + @Override + public final UUID getUniqueID() { + return uuid; + } + + protected abstract DataHolder getResponsibleDataHolder(DataKey key); + + @Override + public final V get(DataKey key) { + return getResponsibleDataHolder(key).get(key); + } + + @Override + public final void addDataChangeListener(DataKey key, Runnable listener) { + getResponsibleDataHolder(key).addDataChangeListener(key, listener); + } + + @Override + public final void removeDataChangeListener(DataKey key, Runnable listener) { + getResponsibleDataHolder(key).removeDataChangeListener(key, listener); + } + + private class ServerDataHolder implements DataHolder { + + private final Function serverDataHolderResolver; + + private final Map> listenerMap = new Object2ObjectOpenHashMap<>(); + + private ServerDataHolder(Function serverDataHolderResolver) { + this.serverDataHolderResolver = serverDataHolderResolver; + } + + @Override + public V get(DataKey dataKey) { + String server = AbstractPlayer.this.get(VelocityData.Velocity_Server); + if (server != null) { + return serverDataHolderResolver.apply(server).get(dataKey); + } + return null; + } + + @Override + public void addDataChangeListener(DataKey dataKey, Runnable dataChangeListener) { + ListenerKey key = new ListenerKey(AbstractPlayer.this, dataKey, dataChangeListener); + if (!listenerMap.containsKey(key)) { + ManagedDataChangeListener managedListener = new ManagedDataChangeListener<>(dataChangeListener, dataKey, serverDataHolderResolver); + listenerMap.put(key, managedListener); + } + } + + @Override + public void removeDataChangeListener(DataKey dataKey, Runnable dataChangeListener) { + ListenerKey key = new ListenerKey(AbstractPlayer.this, dataKey, dataChangeListener); + ManagedDataChangeListener managedListener = listenerMap.remove(key); + if (managedListener != null) { + managedListener.deactivate(); + } + } + } + + private class ManagedDataChangeListener implements Runnable { + private final Runnable delegate; + private final DataKey dataKey; + private final Function serverDataHolderResolver; + + private DataHolder activeDataHolder = NullDataHolder.INSTANCE; + + private ManagedDataChangeListener(Runnable delegate, DataKey dataKey, Function serverDataHolderResolver) { + this.delegate = delegate; + this.dataKey = dataKey; + this.serverDataHolderResolver = serverDataHolderResolver; + + AbstractPlayer.this.addDataChangeListener(VelocityData.Velocity_Server, this); + update(false); + } + + public void run() { + update(true); + } + + private void update(boolean notify) { + String server = AbstractPlayer.this.get(VelocityData.Velocity_Server); + T oldVal = activeDataHolder.get(dataKey); + activeDataHolder.removeDataChangeListener(dataKey, delegate); + if (server == null) { + activeDataHolder = NullDataHolder.INSTANCE; + } else { + activeDataHolder = serverDataHolderResolver.apply(server); + } + activeDataHolder.addDataChangeListener(dataKey, delegate); + if (notify) { + T newVal = activeDataHolder.get(dataKey); + if (!Objects.equals(oldVal, newVal)) { + delegate.run(); + } + } + } + + void deactivate() { + AbstractPlayer.this.removeDataChangeListener(VelocityData.Velocity_Server, this); + activeDataHolder.removeDataChangeListener(dataKey, delegate); + } + } + + @Value + private static class ListenerKey { + private AbstractPlayer player; + private DataKey dataKey; + private Runnable listener; + } + + private static DataHolder getServerDataHolder(String server) { + DataHolder serverDataHolder = null; + if (server != null) { + serverDataHolder = BungeeTabListPlus.getInstance().getDataManager().getServerDataHolder(server); + } + if (serverDataHolder == null) { + serverDataHolder = NullDataHolder.INSTANCE; + } + return serverDataHolder; + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/player/FakePlayer.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/player/FakePlayer.java new file mode 100644 index 00000000..ee55843d --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/player/FakePlayer.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.player; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.api.velocity.Icon; +import codecrafter47.bungeetablistplus.data.BTLPVelocityDataKeys; +import codecrafter47.bungeetablistplus.data.NullDataHolder; +import codecrafter47.bungeetablistplus.util.IconUtil; +import codecrafter47.bungeetablistplus.util.ProxyServer; +import com.google.common.base.Charsets; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import com.velocitypowered.api.proxy.server.ServerInfo; +import de.codecrafter47.data.api.DataCache; +import de.codecrafter47.data.api.DataHolder; +import de.codecrafter47.data.api.DataKey; +import de.codecrafter47.data.minecraft.api.MinecraftData; +import de.codecrafter47.data.velocity.api.VelocityData; +import io.netty.util.concurrent.EventExecutor; +import lombok.SneakyThrows; + +import java.util.Optional; +import java.util.UUID; +import java.util.logging.Level; + +public class FakePlayer extends AbstractPlayer implements codecrafter47.bungeetablistplus.api.velocity.tablist.FakePlayer { + + private boolean randomServerSwitchEnabled; + + final DataCache data = new DataCache(); + + private final EventExecutor mainThread; + + public FakePlayer(String name, ServerInfo server, boolean randomServerSwitchEnabled, EventExecutor mainThread) { + super(UUID.nameUUIDFromBytes(("OfflinePlayer:" + name).getBytes(Charsets.UTF_8)), name); + this.randomServerSwitchEnabled = randomServerSwitchEnabled; + this.mainThread = mainThread; + data.updateValue(VelocityData.Velocity_Server, server.getName()); + } + + @Override + public Optional getServer() { + return Optional.ofNullable(get(VelocityData.Velocity_Server)).map(ProxyServer.getInstance()::getServer).orElse(null); + } + + @Override + public int getPing() { + Integer ping = get(VelocityData.Velocity_Ping); + return ping != null ? ping : 0; + } + + @Override + public Icon getIcon() { + return IconUtil.convert(data.get(BTLPVelocityDataKeys.DATA_KEY_ICON)); + } + + @Override + @SneakyThrows + public void setPing(int ping) { + if (!mainThread.inEventLoop()) { + mainThread.submit(() -> setPing(ping)).sync(); + return; + } + data.updateValue(VelocityData.Velocity_Ping, ping); + } + + @Override + public boolean isRandomServerSwitchEnabled() { + return randomServerSwitchEnabled; + } + + @Override + public void setRandomServerSwitchEnabled(boolean value) { + randomServerSwitchEnabled = value; + } + + @Override + @SneakyThrows + public void changeServer(ServerInfo newServer) { + if (!mainThread.inEventLoop()) { + mainThread.submit(() -> changeServer(newServer)).sync(); + return; + } + data.updateValue(VelocityData.Velocity_Server, newServer.getName()); + } + + @Override + @SneakyThrows + @SuppressWarnings("deprecation") + public void setIcon(Icon icon) { + setIcon(IconUtil.convert(icon)); + } + + @Override + @SneakyThrows + public void setIcon(de.codecrafter47.taboverlay.Icon icon) { + if (!mainThread.inEventLoop()) { + mainThread.submit(() -> setIcon(icon)).sync(); + return; + } + data.updateValue(BTLPVelocityDataKeys.DATA_KEY_ICON, icon); + } + + @Override + protected DataHolder getResponsibleDataHolder(DataKey key) { + + if (key.getScope().equals(VelocityData.SCOPE_VELOCITY_PLAYER) || key.getScope().equals(MinecraftData.SCOPE_PLAYER)) { + return data; + } + + if (key.getScope().equals(MinecraftData.SCOPE_SERVER) || key.getScope().equals(VelocityData.SCOPE_VELOCITY_SERVER)) { + return serverData; + } + + BungeeTabListPlus.getInstance().getLogger().log(Level.WARNING, "Data key with unknown scope: " + key); + return NullDataHolder.INSTANCE; + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/player/FakePlayerManagerImpl.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/player/FakePlayerManagerImpl.java new file mode 100644 index 00000000..9f542016 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/player/FakePlayerManagerImpl.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.player; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.api.velocity.FakePlayerManager; +import codecrafter47.bungeetablistplus.data.BTLPVelocityDataKeys; +import codecrafter47.bungeetablistplus.util.ProxyServer; +import codecrafter47.bungeetablistplus.util.VelocityPlugin; +import com.google.common.collect.ImmutableList; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import com.velocitypowered.api.proxy.server.ServerInfo; +import de.codecrafter47.taboverlay.config.icon.IconManager; +import de.codecrafter47.taboverlay.config.player.PlayerProvider; +import io.netty.util.concurrent.EventExecutor; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import lombok.SneakyThrows; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +public class FakePlayerManagerImpl implements FakePlayerManager, PlayerProvider { + private List online = new ArrayList<>(); + private List offline; + private boolean randomJoinLeaveEventsEnabled; + + private final Set listeners = new ReferenceOpenHashSet<>(); + + private final VelocityPlugin plugin; + private final IconManager iconManager; + private final EventExecutor mainThread; + + public FakePlayerManagerImpl(final VelocityPlugin plugin, IconManager iconManager, EventExecutor mainThread) { + this.plugin = plugin; + this.iconManager = iconManager; + this.mainThread = mainThread; + + randomJoinLeaveEventsEnabled = true; + if (BungeeTabListPlus.getInstance().getConfig().fakePlayers.size() > 0) { + offline = new ArrayList<>(BungeeTabListPlus.getInstance().getConfig().fakePlayers); + sanitizeFakePlayerNames(); + } else { + offline = new ArrayList<>(); + } + mainThread.scheduleAtFixedRate(this::triggerRandomEvent, 10, 10, TimeUnit.SECONDS); + } + + private void triggerRandomEvent() { + try { + if (Math.random() <= 0.5 && online.size() > 0) { + // do a server switch + FakePlayer player = online.get((int) (Math.random() * online.size())); + if (player.isRandomServerSwitchEnabled()) { + player.changeServer(getRandomServer()); + } + } else if (randomJoinLeaveEventsEnabled) { + if (Math.random() < 0.8 && offline.size() > 0) { + // add player + String name = offline.remove((int) (Math.random() * offline.size())); + createFakePlayer(name, getRandomServer(), true, true); + } else if (online.size() > 0) { + // remove player + FakePlayer fakePlayer = online.get((int) (online.size() * Math.random())); + if (BungeeTabListPlus.getInstance().getConfig().fakePlayers.contains(fakePlayer.getName())) { + removeFakePlayer(fakePlayer); + } + } + } + } catch (Throwable th) { + plugin.getLogger().error("An error occurred while processing random fake player events", th); + } + } + + private static ServerInfo getRandomServer() { + ArrayList servers = new ArrayList<>(); + for(RegisteredServer server : ProxyServer.getInstance().getAllServers()){ + servers.add(server.getServerInfo()); + } + return servers.get((int) (Math.random() * servers.size())); + } + + public void removeConfigFakePlayers() { + Set configFakePlayers = new HashSet<>(BungeeTabListPlus.getInstance().getConfig().fakePlayers); + mainThread.execute(() -> { + offline.clear(); + for (FakePlayer fakePlayer : ImmutableList.copyOf(online)) { + if (configFakePlayers.contains(fakePlayer.getName())) { + removeFakePlayer(fakePlayer); + } + } + }); + } + + public void reload() { + mainThread.execute(() -> { + offline = new ArrayList<>(BungeeTabListPlus.getInstance().getConfig().fakePlayers); + sanitizeFakePlayerNames(); + for (int i = offline.size() * 4; i > 0; i--) { + triggerRandomEvent(); + } + }); + } + + private void sanitizeFakePlayerNames() { + for (Iterator iterator = offline.iterator(); iterator.hasNext(); ) { + Object name = iterator.next(); + if (!(name instanceof String)) { + plugin.getLogger().warn("Invalid name used for fake player, removing. (" + name + ")"); + iterator.remove(); + } + } + } + + @Override + @SneakyThrows + public Collection getPlayers() { + if (!mainThread.inEventLoop()) { + return mainThread.submit(this::getPlayers).get(); + } + return ImmutableList.copyOf(online); + } + + @Override + public void registerListener(Listener listener) { + listeners.add(listener); + } + + @Override + public void unregisterListener(Listener listener) { + listeners.remove(listener); + } + + @Override + @SneakyThrows + public Collection getOnlineFakePlayers() { + if (!mainThread.inEventLoop()) { + return mainThread.submit(this::getOnlineFakePlayers).get(); + } + return ImmutableList.copyOf(online); + } + + @Override + public boolean isRandomJoinLeaveEnabled() { + return randomJoinLeaveEventsEnabled; + } + + @Override + public void setRandomJoinLeaveEnabled(boolean value) { + this.randomJoinLeaveEventsEnabled = value; + } + + @Override + @SneakyThrows + public codecrafter47.bungeetablistplus.api.velocity.tablist.FakePlayer createFakePlayer(String name, ServerInfo server) { + return createFakePlayer(name, server, false, false); + } + + @SneakyThrows + public FakePlayer createFakePlayer(String name, ServerInfo server, boolean randomServerSwitch, boolean skinFromName) { + if (!mainThread.inEventLoop()) { + return mainThread.submit(() -> createFakePlayer(name, server, randomServerSwitch, skinFromName)).get(); + } + FakePlayer fakePlayer = new FakePlayer(name, server, randomServerSwitch, mainThread); + online.add(fakePlayer); + listeners.forEach(listener -> listener.onPlayerAdded(fakePlayer)); + if (skinFromName) { + iconManager.createIconFromName(fakePlayer.getName()).thenAcceptAsync(icon -> { + if (null != fakePlayer.get(BTLPVelocityDataKeys.DATA_KEY_ICON)) { + fakePlayer.data.updateValue(BTLPVelocityDataKeys.DATA_KEY_ICON, icon); + } + }, mainThread); + } + return fakePlayer; + } + + @Override + @SneakyThrows + public void removeFakePlayer(codecrafter47.bungeetablistplus.api.velocity.tablist.FakePlayer fakePlayer) { + if (!mainThread.inEventLoop()) { + mainThread.submit(() -> removeFakePlayer(fakePlayer)).sync(); + return; + } + FakePlayer player = (FakePlayer) fakePlayer; + if (online.remove(player)) { + if (BungeeTabListPlus.getInstance().getConfig().fakePlayers.contains(player.getName())) { + offline.add(player.getName()); + } + listeners.forEach(listener -> listener.onPlayerRemoved(player)); + } + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/player/RedisPlayer.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/player/RedisPlayer.java new file mode 100644 index 00000000..8cdd1cc3 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/player/RedisPlayer.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.player; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.data.NullDataHolder; +import codecrafter47.bungeetablistplus.data.TrackingDataCache; +import de.codecrafter47.data.api.DataCache; +import de.codecrafter47.data.api.DataHolder; +import de.codecrafter47.data.api.DataKey; +import de.codecrafter47.data.minecraft.api.MinecraftData; +import de.codecrafter47.data.velocity.api.VelocityData; +import lombok.Getter; + +import java.util.UUID; +import java.util.logging.Level; + +public class RedisPlayer extends AbstractPlayer { + @Getter + private final DataCache data = new TrackingDataCache() { + + @Override + protected void addActiveKey(DataKey key) { + super.addActiveKey(key); + BungeeTabListPlus.getInstance().getRedisPlayerManager().request(getUniqueID(), key); + } + }; + + public RedisPlayer(UUID uuid, String name) { + super(uuid, name); + } + + @Override + protected DataHolder getResponsibleDataHolder(DataKey key) { + + if (key.getScope().equals(VelocityData.SCOPE_VELOCITY_PLAYER) || key.getScope().equals(MinecraftData.SCOPE_PLAYER)) { + return data; + } + + if (key.getScope().equals(MinecraftData.SCOPE_SERVER) || key.getScope().equals(VelocityData.SCOPE_VELOCITY_SERVER)) { + return serverData; + } + + BungeeTabListPlus.getInstance().getLogger().log(Level.WARNING, "Data key with unknown scope: " + key); + return NullDataHolder.INSTANCE; + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/player/VelocityPlayer.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/player/VelocityPlayer.java new file mode 100644 index 00000000..52a6e1a8 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/player/VelocityPlayer.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.player; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.bridge.BukkitBridge; +import codecrafter47.bungeetablistplus.data.NullDataHolder; +import codecrafter47.bungeetablistplus.managers.DataManager; +import com.velocitypowered.api.proxy.Player; +import de.codecrafter47.data.api.DataHolder; +import de.codecrafter47.data.api.DataKey; +import de.codecrafter47.data.minecraft.api.MinecraftData; +import de.codecrafter47.data.velocity.api.VelocityData; +import lombok.Getter; +import lombok.Setter; + +import javax.annotation.Nonnull; +import java.util.Objects; +import java.util.logging.Level; + +public class VelocityPlayer extends AbstractPlayer { + + @Nonnull + private final Player player; + + @Getter + private final BukkitBridge.PlayerBridgeDataCache bridgeDataCache; + + @Getter + private final DataManager.LocalDataCache localDataCache; + + /** + * The player is no longer connected to the proxy, but a DisconnectEvent has not been called. + */ + @Getter + @Setter + private boolean stale; + + public VelocityPlayer(@Nonnull Player player) { + super(player.getUniqueId(), player.getUsername()); + this.player = player; + this.localDataCache = BungeeTabListPlus.getInstance().getDataManager().createDataCacheForPlayer(this); + this.bridgeDataCache = BungeeTabListPlus.getInstance().getBridge().createDataCacheForPlayer(this); + } + + public Player getPlayer() { + return player; + } + + @Override + protected DataHolder getResponsibleDataHolder(DataKey key) { + + if (key.getScope().equals(VelocityData.SCOPE_VELOCITY_PLAYER)) { + return localDataCache; + } + + if (key.getScope().equals(MinecraftData.SCOPE_PLAYER)) { + return bridgeDataCache; + } + + if (key.getScope().equals(MinecraftData.SCOPE_SERVER) || key.getScope().equals(VelocityData.SCOPE_VELOCITY_SERVER)) { + return serverData; + } + + BungeeTabListPlus.getInstance().getLogger().log(Level.WARNING, "Data key with unknown scope: " + key); + return NullDataHolder.INSTANCE; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + VelocityPlayer that = (VelocityPlayer) o; + return player.equals(that.player); + } + + @Override + public int hashCode() { + return Objects.hash(player); + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/AbstractPacketHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/AbstractPacketHandler.java new file mode 100644 index 00000000..432828e7 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/AbstractPacketHandler.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.protocol; + +import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; +import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; +import com.velocitypowered.proxy.protocol.packet.Team; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; +import lombok.NonNull; + +import javax.annotation.Nonnull; + +/** + * Base class for a custom packet handler. Passes all calls to the parent packet handler by default. + */ +public abstract class AbstractPacketHandler implements PacketHandler { + + private final PacketHandler parent; + + public AbstractPacketHandler(@Nonnull @NonNull PacketHandler parent) { + this.parent = parent; + } + + @Override + public PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { + return parent.onPlayerListPacket(packet); + } + + @Override + public PacketListenerResult onTeamPacket(Team packet) { + return parent.onTeamPacket(packet); + } + + @Override + public PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { + return parent.onPlayerListHeaderFooterPacket(packet); + } + + @Override + public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { + return parent.onPlayerListUpdatePacket(packet); + } + + @Override + public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet) { + return parent.onPlayerListRemovePacket(packet); + } + + @Override + public void onServerSwitch(boolean is13OrLater) { + parent.onServerSwitch(is13OrLater); + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketHandler.java new file mode 100644 index 00000000..d8d27cea --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketHandler.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.protocol; + +import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; +import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; +import com.velocitypowered.proxy.protocol.packet.Team; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; + +public interface PacketHandler { + + PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet); + + PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet); + + PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet); + + PacketListenerResult onTeamPacket(Team packet); + + PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet); + + void onServerSwitch(boolean is13OrLater); +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListener.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListener.java new file mode 100644 index 00000000..6123da7d --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListener.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.protocol; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.util.ReflectionUtil; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; +import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; +import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; +import com.velocitypowered.proxy.protocol.packet.Team; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageDecoder; + +import java.util.List; + +public class PacketListener extends MessageToMessageDecoder { + private final VelocityServerConnection connection; + private final PacketHandler handler; + private final Player player; + + public PacketListener(VelocityServerConnection connection, PacketHandler handler, Player player) { + this.connection = connection; + this.handler = handler; + this.player = player; + } + + @Override + protected void decode(ChannelHandlerContext ctx, PacketWrapper packetWrapper, List out) { + boolean shouldRelease = true; + try { + if (connection.isActive()) { + if (packetWrapper.packet != null) { + + PacketListenerResult result = PacketListenerResult.PASS; + boolean handled = false; + + if (packetWrapper.packet instanceof Team) { + result = handler.onTeamPacket((Team) packetWrapper.packet); + if (result == PacketListenerResult.MODIFIED) { + ReflectionUtil.getChannelWrapper(player).getChannel().write(packetWrapper.packet); + } + if (result != PacketListenerResult.PASS) { + return; + } + } else if (packetWrapper.packet instanceof LegacyPlayerListItem) { + result = handler.onPlayerListPacket((LegacyPlayerListItem) packetWrapper.packet); + handled = true; + } else if (packetWrapper.packet instanceof HeaderAndFooter) { + result = handler.onPlayerListHeaderFooterPacket((HeaderAndFooter) packetWrapper.packet); + handled = true; + } else if (packetWrapper.packet instanceof UpsertPlayerInfo) { + result = handler.onPlayerListUpdatePacket((UpsertPlayerInfo) packetWrapper.packet); + handled = true; + } else if (packetWrapper.packet instanceof RemovePlayerInfo) { + result = handler.onPlayerListRemovePacket((RemovePlayerInfo) packetWrapper.packet); + handled = true; + } + + if (handled) { + if (result != PacketListenerResult.CANCEL) { + ReflectionUtil.getChannelWrapper(player).getChannel().write(packetWrapper.packet); + } + return; + } + } + } + out.add(packetWrapper); + shouldRelease = false; + } catch (Throwable th) { + BungeeTabListPlus.getInstance().reportError(th); + } finally { + if (shouldRelease) { + packetWrapper.trySingleRelease(); + } + } + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListenerResult.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListenerResult.java new file mode 100644 index 00000000..0a78fae1 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListenerResult.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.protocol; + +public enum PacketListenerResult { + PASS, MODIFIED, CANCEL; +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketWrapper.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketWrapper.java new file mode 100644 index 00000000..283b2712 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketWrapper.java @@ -0,0 +1,26 @@ +package codecrafter47.bungeetablistplus.protocol; + +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import io.netty.buffer.ByteBuf; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +@RequiredArgsConstructor +public class PacketWrapper +{ + + public final MinecraftPacket packet; + public final ByteBuf buf; + @Setter + private boolean released; + + public void trySingleRelease() + { + if ( !released ) + { + buf.release(); + released = true; + } + } +} + diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/tablist/AbstractCustomTablist.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/tablist/AbstractCustomTablist.java new file mode 100644 index 00000000..9d52166d --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/tablist/AbstractCustomTablist.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.tablist; + +import codecrafter47.bungeetablistplus.api.velocity.CustomTablist; +import codecrafter47.bungeetablistplus.util.IconUtil; +import de.codecrafter47.taboverlay.Icon; +import lombok.NonNull; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Arrays; +import java.util.Objects; + +import static java.lang.Integer.min; + +/** + * Represents a custom tab list. + */ +@SuppressWarnings("deprecation") +public abstract class AbstractCustomTablist implements CustomTablist { + private static Icon[] EMPTY_ICON_ARRAY = new Icon[0]; + private static String[] EMPTY_STRING_ARRAY = new String[0]; + private static int[] EMPTY_INT_ARRAY = new int[0]; + + private int size; + private int columns; + private int rows; + private Icon[] icon; + private String[] text; + private int[] ping; + private String header; + private String footer; + + /** + * Create a new custom tab list with size 0. + */ + AbstractCustomTablist() { + this.size = 0; + this.columns = 1; + this.rows = 0; + this.icon = EMPTY_ICON_ARRAY; + this.text = EMPTY_STRING_ARRAY; + this.ping = EMPTY_INT_ARRAY; + this.header = null; + this.footer = null; + } + + /** + * Create a new custom tab list with the given size. See {@link #setSize(int)}. + * + * @param size the size + * @throws IllegalArgumentException if the size is not allowed + */ + AbstractCustomTablist(int size) { + this(); + setSize(size); + } + + int index(int row, int column) { + int index = row * this.columns + column; + if (index >= size) { + throw new IndexOutOfBoundsException(String.format("Index [row=%s,column=%s] not inside tab list [rows=%s,columns=%s]", row, column, this.rows, this.columns)); + } + return index; + } + + @Override + public synchronized void setSize(int size) { + if (size < 0) { + throw new IllegalArgumentException("size is negative"); + } else if (size == 0) { + setSize(0, 0); + } else { + int columns = (size + 19) / 20; + int rows = size / columns; + if (columns * rows != size) { + throw new IllegalArgumentException("size is not rectangular"); + } + setSize(columns, rows); + } + } + + protected void setSize(int columns, int rows) { + int size = columns * rows; + if (size == 0) { + this.size = 0; + this.columns = 1; + this.rows = 0; + this.icon = EMPTY_ICON_ARRAY; + this.text = EMPTY_STRING_ARRAY; + this.ping = EMPTY_INT_ARRAY; + } else { + Icon[] icon = new Icon[size]; + String[] text = new String[size]; + int[] ping = new int[size]; + Arrays.fill(icon, Icon.DEFAULT_STEVE); + Arrays.fill(text, ""); + Arrays.fill(ping, 0); + for (int col = min(this.columns, columns) - 1; col >= 0; col--) { + for (int row = min(this.rows, rows) - 1; row >= 0; row--) { + icon[row * columns + col] = this.icon[row * this.columns + col]; + text[row * columns + col] = this.text[row * this.columns + col]; + ping[row * columns + col] = this.ping[row * this.columns + col]; + } + } + this.size = size; + this.columns = columns; + this.rows = rows; + this.icon = icon; + this.text = text; + this.ping = ping; + } + onSizeChanged(); + } + + @Override + public int getSize() { + return size; + } + + @Override + public int getRows() { + return rows; + } + + @Override + public int getColumns() { + return columns; + } + + @Override + @Nonnull + public codecrafter47.bungeetablistplus.api.velocity.Icon getIcon(int row, int column) { + return IconUtil.convert(this.icon[index(row, column)]); + } + + Icon getIcon(int index) { + return this.icon[index]; + } + + @Override + @Nonnull + public String getText(int row, int column) { + return this.text[index(row, column)]; + } + + String getText(int index) { + return this.text[index]; + } + + @Override + public int getPing(int row, int column) { + return this.ping[index(row, column)]; + } + + int getPing(int index) { + return this.ping[index]; + } + + @Override + public synchronized void setSlot(int row, int column, @Nonnull @NonNull codecrafter47.bungeetablistplus.api.velocity.Icon icon, @Nonnull @NonNull String text, int ping) { + int index = index(row, column); + this.icon[index] = IconUtil.convert(icon); + this.text[index] = text; + this.ping[index] = ping; + onSlotChanged(index); + } + + @Override + @Nullable + public String getHeader() { + return this.header; + } + + @Override + public synchronized void setHeader(@Nullable String header) { + if (!Objects.equals(this.header, header)) { + this.header = header; + onHeaderOrFooterChanged(); + } + } + + @Override + @Nullable + public String getFooter() { + return footer; + } + + @Override + public synchronized void setFooter(@Nullable String footer) { + if (!Objects.equals(this.footer, footer)) { + this.footer = footer; + onHeaderOrFooterChanged(); + } + } + + protected abstract void onSizeChanged(); + + protected abstract void onSlotChanged(int index); + + protected abstract void onHeaderOrFooterChanged(); +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/tablist/DefaultCustomTablist.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/tablist/DefaultCustomTablist.java new file mode 100644 index 00000000..33943e78 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/tablist/DefaultCustomTablist.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.tablist; + +import codecrafter47.bungeetablistplus.util.IconUtil; +import de.codecrafter47.taboverlay.Icon; +import de.codecrafter47.taboverlay.TabOverlayProvider; +import de.codecrafter47.taboverlay.TabView; +import de.codecrafter47.taboverlay.handler.*; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import it.unimi.dsi.fastutil.objects.ReferenceSet; + +import static java.lang.Integer.min; + +public class DefaultCustomTablist extends AbstractCustomTablist { + private final ReferenceSet handlers = new ReferenceOpenHashSet<>(); + + public DefaultCustomTablist() { + } + + public DefaultCustomTablist(int size) { + super(size); + } + + @Override + protected void onSizeChanged() { + for (TabOverlayProviderImpl handler : handlers) { + handler.onSizeChanged(); + } + } + + @Override + protected void onSlotChanged(int index) { + Icon icon = getIcon(index); + String text = getText(index); + int ping = getPing(index); + for (TabOverlayProviderImpl handler : handlers) { + handler.onSlotChanged(index, icon, text, ping); + } + } + + @Override + protected void onHeaderOrFooterChanged() { + String header = getHeader(); + String footer = getFooter(); + for (TabOverlayProviderImpl handler : handlers) { + handler.setHeaderFooter(header, footer); + } + } + + public void addToPlayer(TabView tabView) { + TabOverlayProviderImpl provider = new TabOverlayProviderImpl(); + tabView.getTabOverlayProviders().addProvider(provider); + } + + public class TabOverlayProviderImpl extends TabOverlayProvider { + + private SimpleTabOverlay tabOverlay; + private HeaderAndFooterHandle headerAndFooterHandle; + + TabOverlayProviderImpl() { + super("custom-tab-overlay", 10001); + } + + @Override + protected void attach(TabView tabView) { + handlers.add(this); + } + + @Override + protected void detach(TabView tabView) { + handlers.remove(this); + } + + @Override + protected void activate(TabView tabView, TabOverlayHandler handler) { + synchronized (DefaultCustomTablist.this) { + tabOverlay = handler.enterContentOperationMode(ContentOperationMode.SIMPLE); + headerAndFooterHandle = handler.enterHeaderAndFooterOperationMode(HeaderAndFooterOperationMode.CUSTOM); + int size = min(80, getSize()); + tabOverlay.setSize(size); + updateAllSlots(); + headerAndFooterHandle.setHeaderFooter(getHeader(), getFooter()); + } + } + + @Override + protected void deactivate(TabView tabView) { + + } + + @Override + protected boolean shouldActivate(TabView tabView) { + return true; + } + + private void updateAllSlots() { + for (int column = 0; column < getColumns(); column++) { + for (int row = 0; row < getRows(); row++) { + Icon icon = IconUtil.convert(getIcon(row, column)); + String text = getText(row, column); + int ping = getPing(row, column); + + tabOverlay.setSlot(index(row, column), icon, text, ping); + } + } + } + + void onSizeChanged() { + synchronized (DefaultCustomTablist.this) { + if (tabOverlay != null) { + int size = getSize(); + tabOverlay.setSize(size); + updateAllSlots(); + } + } + } + + void onSlotChanged(int index, Icon icon, String text, int ping) { + if (tabOverlay != null) { + tabOverlay.setSlot(index, icon, text, ping); + } + } + + void setHeaderFooter(String header, String footer) { + if (headerAndFooterHandle != null) { + headerAndFooterHandle.setHeaderFooter(header, footer); + } + } + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/tablist/ExcludedServersTabOverlayProvider.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/tablist/ExcludedServersTabOverlayProvider.java new file mode 100644 index 00000000..7d8b3811 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/tablist/ExcludedServersTabOverlayProvider.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.tablist; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import de.codecrafter47.data.velocity.api.VelocityData; +import de.codecrafter47.taboverlay.AbstractTabOverlayProvider; +import de.codecrafter47.taboverlay.TabOverlayProviderSet; +import de.codecrafter47.taboverlay.TabView; +import de.codecrafter47.taboverlay.config.player.Player; +import de.codecrafter47.taboverlay.handler.ContentOperationMode; +import de.codecrafter47.taboverlay.handler.HeaderAndFooterOperationMode; +import de.codecrafter47.taboverlay.handler.TabOverlayHandle; +import lombok.SneakyThrows; + +import java.util.HashSet; +import java.util.Set; + +public class ExcludedServersTabOverlayProvider extends AbstractTabOverlayProvider implements Runnable { + + private final Player player; + private final BungeeTabListPlus btlp; + private boolean shouldBeActive = false; + private TabOverlayProviderSet tabOverlayProviderSet; + private static Set attachedProviders = new HashSet<>(); + + public ExcludedServersTabOverlayProvider(Player player, BungeeTabListPlus btlp) { + super("excluded-servers", 10003, ContentOperationMode.PASS_TROUGH, HeaderAndFooterOperationMode.PASS_TROUGH); + this.player = player; + this.btlp = btlp; + } + + + @Override + protected void activate(TabView tabView, TabOverlayHandle contentHandle, TabOverlayHandle headerAndFooterHandle) { + + } + + @Override + @SneakyThrows + protected void attach(TabView tabView) { + btlp.getMainThreadExecutor().submit(() -> { + tabOverlayProviderSet = tabView.getTabOverlayProviders(); + player.addDataChangeListener(VelocityData.Velocity_Server, this); + String server = player.get(VelocityData.Velocity_Server); + shouldBeActive = server == null || btlp.getExcludedServers().contains(server); + attachedProviders.add(this); + }).sync(); + } + + @Override + @SneakyThrows + protected void detach(TabView tabView) { + btlp.getMainThreadExecutor().submit(() -> { + attachedProviders.remove(this); + player.removeDataChangeListener(VelocityData.Velocity_Server, this); + }).sync(); + } + + @Override + protected void deactivate(TabView tabView) { + + } + + @Override + protected boolean shouldActivate(TabView tabView) { + return shouldBeActive; + } + + @Override + public void run() { + String server = player.get(VelocityData.Velocity_Server); + shouldBeActive = server == null || btlp.getExcludedServers().contains(server); + tabOverlayProviderSet.scheduleUpdate(); + } + + public static void onReload() { + BungeeTabListPlus.getInstance().getMainThreadExecutor().execute(() -> { + for (ExcludedServersTabOverlayProvider provider : attachedProviders) { + provider.run(); + } + }); + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/template/PlayersByServerComponentTemplate.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/template/PlayersByServerComponentTemplate.java new file mode 100644 index 00000000..2c7864db --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/template/PlayersByServerComponentTemplate.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.template; + +import codecrafter47.bungeetablistplus.BTLPContextKeys; +import codecrafter47.bungeetablistplus.config.PlayersByServerComponentConfiguration; +import codecrafter47.bungeetablistplus.util.ContextAwareOrdering; +import codecrafter47.bungeetablistplus.view.PlayersByServerComponentView; +import de.codecrafter47.taboverlay.config.context.Context; +import de.codecrafter47.taboverlay.config.expression.template.ExpressionTemplate; +import de.codecrafter47.taboverlay.config.player.PlayerSetFactory; +import de.codecrafter47.taboverlay.config.player.PlayerSetPartition; +import de.codecrafter47.taboverlay.config.template.PlayerOrderTemplate; +import de.codecrafter47.taboverlay.config.template.PlayerSetTemplate; +import de.codecrafter47.taboverlay.config.template.component.ComponentTemplate; +import de.codecrafter47.taboverlay.config.template.icon.IconTemplate; +import de.codecrafter47.taboverlay.config.template.ping.PingTemplate; +import de.codecrafter47.taboverlay.config.template.text.TextTemplate; +import de.codecrafter47.taboverlay.config.view.components.ComponentView; +import de.codecrafter47.taboverlay.config.view.components.ContainerComponentView; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Set; +import java.util.function.Function; + +@Value +@Builder +public class PlayersByServerComponentTemplate implements ComponentTemplate { + PlayerSetTemplate playerSet; + + PlayerSetFactory playerSetFactory; + + PlayerOrderTemplate playerOrder; + + ComponentTemplate playerComponent; + + @NonNull + @Nonnull + ComponentTemplate morePlayersComponent; + + @Nullable + ComponentTemplate serverHeader; + + @Nullable + ComponentTemplate serverFooter; + + @Nullable + ComponentTemplate serverSeparator; + + boolean fillSlotsVertical; + int minSize; + /* A value of -1 indicates no limit. */ + int maxSize; + int minSizePerServer; + /* A value of -1 indicates no limit. */ + int maxSizePerServer; + int columns; + + TextTemplate defaultText; + PingTemplate defaultPing; + IconTemplate defaultIcon; + + ExpressionTemplate partitionFunction; + Function mergeSections; + + Set hiddenServers; + PlayersByServerComponentConfiguration.ServerOptions showServers; + @Nullable + ContextAwareOrdering serverComparator; + + boolean prioritizeViewerServer; + + @Override + public LayoutInfo getLayoutInfo() { + return LayoutInfo.builder() + .constantSize(false) + .minSize(0) + .blockAligned(true) + .build(); + } + + @Override + public ComponentView instantiate() { + return new ContainerComponentView(new PlayersByServerComponentView(fillSlotsVertical ? 1 : columns, playerSet, playerComponent, playerComponent.getLayoutInfo().getMinSize(), morePlayersComponent, morePlayersComponent.getLayoutInfo().getMinSize(), playerOrder, defaultText, defaultPing, defaultIcon, partitionFunction, mergeSections, serverHeader, serverFooter, serverSeparator, minSizePerServer, maxSizePerServer, (parent, sectionId, playerSet1) -> { + Context child = parent.clone(); + child.setCustomObject(BTLPContextKeys.SERVER_ID, sectionId); + child.setCustomObject(BTLPContextKeys.SERVER_PLAYER_SET, playerSet1); + return child; + }, hiddenServers, showServers, serverComparator, prioritizeViewerServer), + fillSlotsVertical, minSize, maxSize, columns, false); + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/updater/UpdateChecker.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/updater/UpdateChecker.java new file mode 100644 index 00000000..43a169b7 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/updater/UpdateChecker.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package codecrafter47.bungeetablistplus.updater; + +import codecrafter47.bungeetablistplus.util.VelocityPlugin; +import com.google.common.base.Charsets; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Objects; +import java.util.Properties; + +public class UpdateChecker implements Runnable { + + private final VelocityPlugin plugin; + + private boolean updateAvailable = false; + private boolean newDevBuildAvailable = false; + + public static final long interval = 120; + + private boolean error = false; + + public UpdateChecker(VelocityPlugin plugin) { + this.plugin = plugin; + } + + @Override + public void run() { + if (updateAvailable) { + return; + } + if (error) { + return; + } + try { + InputStreamReader ir; + URL url = new URL( "http://updates.codecrafter47.dyndns.eu/" + plugin.getVersion() + "/version.txt"); + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + con.connect(); + int res = con.getResponseCode(); + if (res != 200) { + con.disconnect(); + return; + } + InputStream inputStream = con.getInputStream(); + ir = new InputStreamReader(inputStream, Charsets.UTF_8); + + BufferedReader input = new BufferedReader(ir); + + String newVersion = ""; + for (int i = 0; i < 10; i++) { + String s = input.readLine(); + if (s == null) { + break; + } + if (!s.isEmpty() && s.length() > 2) { + newVersion = s; + } + } + + // compare versions + String runningVersion = plugin.getVersion(); + updateAvailable = compareVersions(newVersion, runningVersion); + + input.close(); + ir.close(); + inputStream.close(); + con.disconnect(); + + if (updateAvailable) { + plugin.getLogger().info("A new version of BungeeTabListPlus (" + newVersion + ") is available. Download from http://www.spigotmc.org/resources/bungeetablistplus.313/"); + } + + if (!updateAvailable && !newDevBuildAvailable && runningVersion.endsWith("-SNAPSHOT")) { + // Check whether there is a new dev-build available + try { + Properties current = new Properties(); + current.load(getClass().getClassLoader().getResourceAsStream("version.properties")); + String currentVersion = current.getProperty("build", "unknown"); + if (!currentVersion.equals("unknown")) { + int buildNumber = Integer.valueOf(currentVersion); + Properties latest = new Properties(); + latest.load(new URL("http://ci.codecrafter47.dyndns.eu/job/BungeeTabListPlus/lastSuccessfulBuild/artifact/bungee/target/classes/version.properties").openStream()); + String latestVersion = latest.getProperty("build", "unknown"); + if (!latestVersion.isEmpty() && !latestVersion.equals("unknown")) { + int latestBuildNumber = Integer.valueOf(latestVersion); + if (latestBuildNumber > buildNumber) { + newDevBuildAvailable = true; + plugin.getLogger().info("A new dev-build is available at http://ci.codecrafter47.dyndns.eu/job/BungeeTabListPlus/"); + } + } + } + } catch (Throwable ignored) { + } + } + } catch (Throwable t) { + error = true; + } + } + + static boolean compareVersions(String newVersion, String runningVersion) { + boolean usesDevBuild = false; + if (runningVersion.endsWith("-SNAPSHOT")) { + usesDevBuild = true; + runningVersion = runningVersion.replace("-SNAPSHOT", ""); + } + + String[] current = runningVersion.split("\\."); + String[] latest = newVersion.split("\\."); + + int i = 0; + boolean higher = false; + boolean equal = true; + for (; i < current.length && i < latest.length; i++) { + if (Integer.valueOf(current[i]) < Integer.valueOf(latest[i])) { + higher = true; + break; + } else if (Objects.equals(Integer.valueOf(current[i]), Integer.valueOf(latest[i]))) { + equal = true; + } else { + equal = false; + break; + } + } + + boolean updateAvailable = false; + if (higher) { + updateAvailable = true; + } + + if (equal) { + if (current.length < latest.length) { + updateAvailable = true; + } else if (current.length == latest.length && usesDevBuild) { + updateAvailable = true; + } + } + return updateAvailable; + } + + public boolean isUpdateAvailable() { + return updateAvailable; + } + + public boolean isNewDevBuildAvailable() { + return newDevBuildAvailable; + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/updater/UpdateNotifier.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/updater/UpdateNotifier.java new file mode 100644 index 00000000..27be7390 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/updater/UpdateNotifier.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package codecrafter47.bungeetablistplus.updater; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import com.velocitypowered.api.proxy.Player; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; + + +/** + * @author florian + */ +public class UpdateNotifier implements Runnable { + + private final BungeeTabListPlus plugin; + + public UpdateNotifier(BungeeTabListPlus plugin) { + this.plugin = plugin; + } + + @Override + public void run() { + if (!plugin.getConfig().notifyAdminsIfUpdateAvailable) { + return; + } + if (!plugin.isUpdateAvailable() && !plugin.isNewDevBuildAvailable()) { + return; + } + for (Player player : plugin.getProxy().getAllPlayers()) { + if (player.hasPermission("bungeetablistplus.admin")) { + if (plugin.isUpdateAvailable()) { + player.sendMessage(getPrefix() + .append(Component.text("A new version is available. Download ", NamedTextColor.GOLD)) + .append(Component.text("here", NamedTextColor.LIGHT_PURPLE) + .decorate(TextDecoration.UNDERLINED).clickEvent( + ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, "http://www.spigotmc.org/resources/bungeetablistplus.313/")) + )); + } else { + player.sendMessage(getPrefix() + .append(Component.text("A new dev-build is available. Download ", NamedTextColor.GOLD)) + .append(Component.text("here", NamedTextColor.LIGHT_PURPLE) + .decorate(TextDecoration.UNDERLINED).clickEvent( + ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL,"http://ci.codecrafter47.dyndns.eu/job/BungeeTabListPlus/")) + )); + } + } + } + if (plugin.isUpdateAvailable()) { + plugin.getLogger().info("A new version of BungeeTabListPlus is available. Download from http://www.spigotmc.org/resources/bungeetablistplus.313/"); + } else { + plugin.getLogger().info("A new dev-build is available at http://ci.codecrafter47.dyndns.eu/job/BungeeTabListPlus/"); + } + } + + private Component getPrefix() { + return Component.text("[", NamedTextColor.BLUE).append(Component.text("BungeeTabListPlus", NamedTextColor.YELLOW)).clickEvent( + ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, "http://www.spigotmc.org/resources/bungeetablistplus.313/")). + append(Component.text("] ", NamedTextColor.BLUE)); + } + +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/BitSet.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/BitSet.java new file mode 100644 index 00000000..6b9fd58a --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/BitSet.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.util; + +import com.google.common.base.Preconditions; + +/** + * Simple BitSet implementation + */ +public class BitSet { + private final int size; + private final long[] array; + + public BitSet(int size) { + Preconditions.checkArgument(size >= 0, "size must not be negative"); + this.size = size; + this.array = new long[(size + 63) / 64]; + } + + public void set(int index) { + Preconditions.checkElementIndex(index, size); + int longIndex = index >> 6; + long mask = 1L << (index & 0x3F); + array[longIndex] |= mask; + } + + public void set(int fromIndex, int toIndex) { + Preconditions.checkElementIndex(fromIndex, size); + Preconditions.checkPositionIndex(toIndex, size); + if (fromIndex >= toIndex) + return; + + int startWordIndex = fromIndex >> 6; + int endWordIndex = (toIndex - 1) >> 6; + + long firstWordMask = -1L << fromIndex; + long lastWordMask = -1L >>> -toIndex; + if (startWordIndex == endWordIndex) { + array[startWordIndex] |= (firstWordMask & lastWordMask); + } else { + array[startWordIndex] |= firstWordMask; + + for (int i = startWordIndex + 1; i < endWordIndex; i++) + array[i] = -1L; + + array[endWordIndex] |= lastWordMask; + } + } + + public void clear(int index) { + Preconditions.checkElementIndex(index, size); + int longIndex = index >> 6; + long mask = ~(1L << (index & 0x3F)); + array[longIndex] &= mask; + } + + public void clear() { + for (int i = 0; i < array.length; i++) { + array[i] = 0; + } + } + + public boolean get(int index) { + Preconditions.checkElementIndex(index, size); + int longIndex = index >> 6; + long mask = 1L << (index & 0x3F); + return 0 != (array[longIndex] & mask); + } + + public int cardinality() { + int sum = 0; + for (int i = 0; i < array.length; i++) + sum += Long.bitCount(array[i]); + return sum; + } + + public boolean isEmpty() { + for (int i = 0; i < array.length; i++) { + long l = array[i]; + if (l != 0) + return false; + } + return true; + } + + public int nextSetBit(int previous) { + Preconditions.checkPositionIndex(previous, size); + int longIndex = previous >> 6; + + if (longIndex >= array.length) { + return -1; + } + + long word = array[longIndex] & (-1L << previous); + + while (true) { + if (word != 0) + return (longIndex * 64) + Long.numberOfTrailingZeros(word); + if (++longIndex == array.length) + return -1; + word = array[longIndex]; + } + } + + public int previousSetBit(int fromIndex) { + Preconditions.checkElementIndex(fromIndex, size); + + int longIndex = fromIndex >> 6; + + long word = array[longIndex] & (-1L >>> -(fromIndex + 1)); + + while (true) { + if (word != 0) + return (longIndex + 1) * 64 - 1 - Long.numberOfLeadingZeros(word); + if (longIndex-- == 0) + return -1; + word = array[longIndex]; + } + } + + public void copyAndClear(ConcurrentBitSet source) { + Preconditions.checkArgument(source.size == this.size); + for (int i = 0; i < this.array.length; i++) { + this.array[i] = source.array.getAndSet(i, 0); + } + } + + public void orAndClear(ConcurrentBitSet source) { + Preconditions.checkArgument(source.size == this.size); + for (int i = 0; i < this.array.length; i++) { + this.array[i] |= source.array.getAndSet(i, 0); + } + } + + public void orXor(BitSet a, BitSet b) { + Preconditions.checkArgument(a.size == this.size); + Preconditions.checkArgument(b.size == this.size); + for (int i = 0; i < this.array.length; i++) { + this.array[i] |= (a.array[i] ^ b.array[i]); + } + } + + public void or(BitSet a) { + Preconditions.checkArgument(a.size == this.size); + for (int i = 0; i < this.array.length; i++) { + this.array[i] |= a.array[i]; + } + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ChatUtil.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ChatUtil.java new file mode 100644 index 00000000..6b4ca6e9 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ChatUtil.java @@ -0,0 +1,392 @@ +package codecrafter47.bungeetablistplus.util; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.kyori.adventure.text.format.TextFormat; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.*; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utility class providing a chat formatting syntax similar to bbcode. + * + * Details: + * For example [b]this is bold[/b], [i]this is italic[/i], [u]this is underlined[/u] and [s]this is crossed out[/s]. + * The difference between the above and making something &lbold&r the vanilla way is, that the above makes all the + * enclosed text bold, while &b makes bold everything until reaching the next color code. + * Same for [color=...] + * + * How links will work is easy to guess, e.g. it's just [url]spigotmc.org[/url] or [url=spigotmc.org]click here[/url]. + * Executing commands works similar [command=/tp CodeCrafter47]click here[/command]. + * + * Suggesting commands works with [suggest=/tp ]...[/suggest] + * To create tooltips do [hover=Text magically appears when moving the mouse over]this[/hover]. + * + * It is possible to use [nocolor][/nocolor] to prevent the use of legacy color codes in a block; + * [nobbcode][/nobbcode] will prevent the use of bbcode in a block; + * + * Vanilla color codes still work and can be mixed with the [color=..] and other formatting tags without problems. + */ +public class ChatUtil { + + public static final Map BY_CHAR = new HashMap() {{ + put('0', NamedTextColor.BLACK); + put('1', NamedTextColor.DARK_BLUE); + put('2', NamedTextColor.DARK_GREEN); + put('3', NamedTextColor.DARK_AQUA); + put('4', NamedTextColor.DARK_RED); + put('5', NamedTextColor.DARK_PURPLE); + put('6', NamedTextColor.GOLD); + put('7', NamedTextColor.GRAY); + put('8', NamedTextColor.DARK_GRAY); + put('9', NamedTextColor.BLUE); + put('a', NamedTextColor.GREEN); + put('b', NamedTextColor.AQUA); + put('c', NamedTextColor.RED); + put('d', NamedTextColor.LIGHT_PURPLE); + put('e', NamedTextColor.YELLOW); + put('f', NamedTextColor.WHITE); + put('k', TextDecoration.OBFUSCATED); + put('l', TextDecoration.BOLD); + put('m', TextDecoration.STRIKETHROUGH); + put('n', TextDecoration.UNDERLINED); + put('o', TextDecoration.ITALIC); + put('r', CustomFormat.RESET); + }}; + + private static final String NON_UNICODE_CHARS = "\u00c0\u00c1\u00c2\u00c8\u00ca\u00cb\u00cd\u00d3\u00d4\u00d5\u00da\u00df\u00e3\u00f5\u011f\u0130\u0131\u0152\u0153\u015e\u015f\u0174\u0175\u017e\u0207\u0000\u0000\u0000\u0000\u0000\u0000\u0000 !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u0000\u00c7\u00fc\u00e9\u00e2\u00e4\u00e0\u00e5\u00e7\u00ea\u00eb\u00e8\u00ef\u00ee\u00ec\u00c4\u00c5\u00c9\u00e6\u00c6\u00f4\u00f6\u00f2\u00fb\u00f9\u00ff\u00d6\u00dc\u00f8\u00a3\u00d8\u00d7\u0192\u00e1\u00ed\u00f3\u00fa\u00f1\u00d1\u00aa\u00ba\u00bf\u00ae\u00ac\u00bd\u00bc\u00a1\u00ab\u00bb\u2591\u2592\u2593\u2502\u2524\u2561\u2562\u2556\u2555\u2563\u2551\u2557\u255d\u255c\u255b\u2510\u2514\u2534\u252c\u251c\u2500\u253c\u255e\u255f\u255a\u2554\u2569\u2566\u2560\u2550\u256c\u2567\u2568\u2564\u2565\u2559\u2558\u2552\u2553\u256b\u256a\u2518\u250c\u2588\u2584\u258c\u2590\u2580\u03b1\u03b2\u0393\u03c0\u03a3\u03c3\u03bc\u03c4\u03a6\u0398\u03a9\u03b4\u221e\u2205\u2208\u2229\u2261\u00b1\u2265\u2264\u2320\u2321\u00f7\u2248\u00b0\u2219\u00b7\u221a\u207f\u00b2\u25a0\u0000"; + + private static final int[] NON_UNICODE_CHAR_WIDTHS = new int[]{6, 6, 6, 6, 6, 6, 4, 6, 6, 6, 6, 6, 6, 6, 6, 4, 4, 6, 7, 6, 6, 6, 6, 6, 6, 1, 1, 1, 1, 1, 1, 1, 4, 2, 5, 6, 6, 6, 6, 3, 5, 5, 5, 6, 2, 6, 2, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 2, 2, 5, 6, 5, 6, 7, 6, 6, 6, 6, 6, 6, 6, 6, 4, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 4, 6, 4, 6, 6, 3, 6, 6, 6, 6, 6, 5, 6, 6, 2, 6, 5, 3, 6, 6, 6, 6, 6, 6, 6, 4, 6, 6, 6, 6, 6, 6, 5, 2, 5, 7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 4, 6, 3, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 4, 6, 6, 3, 6, 6, 6, 6, 6, 6, 6, 7, 6, 6, 6, 2, 6, 6, 8, 9, 9, 6, 6, 6, 8, 8, 6, 8, 8, 8, 8, 8, 6, 6, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 6, 9, 9, 9, 5, 9, 9, 8, 7, 7, 8, 7, 8, 8, 8, 7, 8, 8, 7, 9, 9, 6, 7, 7, 7, 7, 7, 9, 6, 7, 8, 7, 6, 6, 9, 7, 6, 7, 1}; + + private static final byte[] UNICODE_CHAR_WIDTHS = new byte[65536]; + + static { + InputStream resourceAsStream = ChatUtil.class.getResourceAsStream("unicode.txt"); + InputStreamReader inputStreamReader = new InputStreamReader(resourceAsStream); + BufferedReader bufferedReader = new BufferedReader(inputStreamReader); + String line; + try { + int i = 0; + while ((line = bufferedReader.readLine()) != null) { + UNICODE_CHAR_WIDTHS[i++] = Byte.valueOf(line); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static final int DEFAULT_CHAT_LINE_WIDTH = 320; + + private static final Pattern pattern = Pattern.compile("(?is)(?=\\n)|(?:[&\u00A7](?[0-9A-FK-OR]))|" + + "(?:\\[(?/?(?:b|i|u|s|nocolor|nobbcode)|(?:url|command|hover|suggest|color)=(?(?:(?:[^]\\[]*)\\[(?:[^]\\[]*)\\])*(?:[^]\\[]*))|/(?:url|command|hover|suggest|color))\\])|" + + "(?:\\[(?url|command|suggest)\\](?=(?.*?)\\[/\\k\\]))"); + + private static final Pattern strip_bbcode_pattern = Pattern.compile("(?is)(?:\\[(?/?(?:b|i|u|s|nocolor|nobbcode)|(?:url|command|hover|suggest|color)=(?(?:(?:[^]\\[]*)\\[(?:[^]\\[]*)\\])*(?:[^]\\[]*))|/(?:url|command|hover|suggest|color))\\])|" + + "(?:\\[(?url|command|suggest)\\](?=(?.*?)\\[/\\k\\]))"); + + private static final Logger logger = Logger.getLogger("Minecraft"); + + public static Component parseBBCode(String text) { + Matcher matcher = pattern.matcher(text); + Component current = Component.text(""); + List components = new LinkedList<>(); + int forceBold = 0; + int forceItalic = 0; + int forceUnderlined = 0; + int forceStrikethrough = 0; + int nocolorLevel = 0; + int nobbcodeLevel = 0; + Deque colorDeque = new LinkedList<>(); + Deque clickEventDeque = new LinkedList<>(); + Deque> hoverEventDeque = new LinkedList<>(); + while (matcher.find()) { + boolean parsed = false; + { + StringBuffer stringBuffer = new StringBuffer(); + matcher.appendReplacement(stringBuffer, ""); + Component component = Component.text(stringBuffer.toString()); + components.add(component.mergeStyle(current)); + } + String group_color = matcher.group("color"); + String group_tag = matcher.group("tag"); + String group_value = matcher.group("value"); + String group_implicitTag = matcher.group("implicitTag"); + String group_implicitValue = matcher.group("implicitValue"); + if (group_color != null && nocolorLevel <= 0) { + TextFormat color = BY_CHAR.get(group_color.charAt(0)); + if (color != null) { + if (TextDecoration.OBFUSCATED.equals(color)) { + current = current.decorate(TextDecoration.OBFUSCATED); + } else if (TextDecoration.BOLD.equals(color)) { + current = current.decorate(TextDecoration.BOLD); + } else if (TextDecoration.STRIKETHROUGH.equals(color)) { + current = current.decorate(TextDecoration.STRIKETHROUGH); + } else if (TextDecoration.UNDERLINED.equals(color)) { + current = current.decorate(TextDecoration.UNDERLINED); + } else if (TextDecoration.ITALIC.equals(color)) { + current = current.decorate(TextDecoration.ITALIC); + } else { + if (CustomFormat.RESET.equals(color)) + color = NamedTextColor.WHITE; + + current = Component.text(""); + current = current.color((TextColor) color); + current = current.decoration(TextDecoration.BOLD, forceBold > 0); + current = current.decoration(TextDecoration.ITALIC, forceItalic > 0); + current = current.decoration(TextDecoration.UNDERLINED, forceUnderlined > 0); + current = current.decoration(TextDecoration.STRIKETHROUGH, forceStrikethrough > 0); + if (!colorDeque.isEmpty()) { + current = current.color(colorDeque.peek()); + } + if (!clickEventDeque.isEmpty()) { + current = current.clickEvent(clickEventDeque.peek()); + } + if (!hoverEventDeque.isEmpty()) { + current = current.hoverEvent(hoverEventDeque.peek()); + } + } + parsed = true; + } + } + if (group_tag != null && nobbcodeLevel <= 0) { + // [b]this is bold[/b] + if (group_tag.matches("(?is)^b$")) { + forceBold++; + current = current.decoration(TextDecoration.BOLD, forceBold > 0); + parsed = true; + } else if (group_tag.matches("(?is)^/b$")) { + forceBold--; + current = current.decoration(TextDecoration.BOLD, forceBold > 0); + parsed = true; + } + // [i]this is italic[/i] + if (group_tag.matches("(?is)^i$")) { + forceItalic++; + current = current.decoration(TextDecoration.ITALIC, forceItalic > 0); + parsed = true; + } else if (group_tag.matches("(?is)^/i$")) { + forceItalic--; + current = current.decoration(TextDecoration.ITALIC, forceItalic > 0); + parsed = true; + } + // [u]this is underlined[/u] + if (group_tag.matches("(?is)^u$")) { + forceUnderlined++; + current = current.decoration(TextDecoration.UNDERLINED, forceUnderlined > 0); + parsed = true; + } else if (group_tag.matches("(?is)^/u$")) { + forceUnderlined--; + current = current.decoration(TextDecoration.UNDERLINED, forceUnderlined > 0); + parsed = true; + } + // [s]this is crossed out[/s] + if (group_tag.matches("(?is)^s$")) { + forceStrikethrough++; + current = current.decoration(TextDecoration.STRIKETHROUGH, forceStrikethrough > 0); + parsed = true; + } else if (group_tag.matches("(?is)^/s$")) { + forceStrikethrough--; + current = current.decoration(TextDecoration.STRIKETHROUGH, forceStrikethrough > 0); + parsed = true; + } + // [color=red]huh this is red...[/color] + if (group_tag.matches("(?is)^color=.*$")) { + NamedTextColor color = null; + for (TextFormat color1 : BY_CHAR.values()) { + if (color1 instanceof NamedTextColor && ((NamedTextColor)color1).toString().equalsIgnoreCase(group_value)) { + color = (NamedTextColor) color1; + } + } + colorDeque.push(current.color()); + if (color != null) { + colorDeque.push(color); + current = current.color(color); + } else { + logger.warning("Invalid color tag: [" + group_tag + "] UNKNOWN COLOR '" + group_value + "'"); + colorDeque.push(NamedTextColor.WHITE); + current = current.color(NamedTextColor.WHITE); + } + parsed = true; + } else if (group_tag.matches("(?is)^/color$")) { + if (!colorDeque.isEmpty()) { + colorDeque.pop(); + current = current.color(colorDeque.pop()); + } + parsed = true; + } + // [url=....] + if (group_tag.matches("(?is)^url=.*$")) { + String url = group_value; + url = url.replaceAll("(?is)\\[/?nobbcode\\]", ""); + if (!url.startsWith("http")) { + url = "http://" + url; + } + ClickEvent clickEvent = ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, url); + clickEventDeque.push(clickEvent); + current = current.clickEvent(clickEvent); + parsed = true; + } + // [/url], [/command], [/suggest] + if (group_tag.matches("(?is)^/(?:url|command|suggest)$")) { + if (!clickEventDeque.isEmpty()) clickEventDeque.pop(); + current = current.clickEvent(clickEventDeque.isEmpty() ? null : clickEventDeque.peek()); + parsed = true; + } + // [command=....] + if (group_tag.matches("(?is)^command=.*")) { + group_value = group_value.replaceAll("(?is)\\[/?nobbcode\\]", ""); + ClickEvent clickEvent = ClickEvent.clickEvent(ClickEvent.Action.RUN_COMMAND, group_value); + clickEventDeque.push(clickEvent); + current = current.clickEvent(clickEvent); + parsed = true; + } + // [suggest=....] + if (group_tag.matches("(?is)^suggest=.*")) { + group_value = group_value.replaceAll("(?is)\\[/?nobbcode\\]", ""); + ClickEvent clickEvent = ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND, group_value); + clickEventDeque.push(clickEvent); + current = current.clickEvent(clickEvent); + parsed = true; + } + // [hover=....]...[/hover] + if (group_tag.matches("(?is)^hover=.*$")) { + Component components1 = parseBBCode(group_value); + if (!hoverEventDeque.isEmpty()) { + components1 = Component.text().append(hoverEventDeque.getLast().value()).append(Component.text("\n")).append(components).build(); + } + HoverEvent hoverEvent = HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, components1); + hoverEventDeque.push(hoverEvent); + current = current.hoverEvent(hoverEvent); + parsed = true; + } else if (group_tag.matches("(?is)^/hover$")) { + if (!hoverEventDeque.isEmpty()) hoverEventDeque.pop(); + current = current.hoverEvent(hoverEventDeque.isEmpty() ? null : hoverEventDeque.peek()); + parsed = true; + } + } + if (group_implicitTag != null && nobbcodeLevel <= 0) { + // [url]spigotmc.org[/url] + if (group_implicitTag.matches("(?is)^url$")) { + String url = group_implicitValue; + if (!url.startsWith("http")) { + url = "http://" + url; + } + ClickEvent clickEvent = ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, url); + clickEventDeque.push(clickEvent); + current = current.clickEvent(clickEvent); + parsed = true; + } + // [command]/spawn[/command] + if (group_implicitTag.matches("(?is)^command$")) { + ClickEvent clickEvent = ClickEvent.clickEvent(ClickEvent.Action.RUN_COMMAND, group_implicitValue); + clickEventDeque.push(clickEvent); + current = current.clickEvent(clickEvent); + parsed = true; + } + // [suggest]/friend add [/suggest] + if (group_implicitTag.matches("(?is)^suggest$")) { + ClickEvent clickEvent = ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND, group_implicitValue); + clickEventDeque.push(clickEvent); + current = current.clickEvent(clickEvent); + parsed = true; + } + } + if (group_tag != null) { + if (group_tag.matches("(?is)^nocolor$")) { + nocolorLevel++; + parsed = true; + } + if (group_tag.matches("(?is)^/nocolor$")) { + nocolorLevel--; + parsed = true; + } + if (group_tag.matches("(?is)^nobbcode$")) { + nobbcodeLevel++; + parsed = true; + } + if (group_tag.matches("(?is)^/nobbcode$")) { + nobbcodeLevel--; + parsed = true; + } + } + if (!parsed) { + Component component = Component.text(matcher.group(0)); + components.add(component.mergeStyle(current)); + } + } + StringBuffer stringBuffer = new StringBuffer(); + matcher.appendTail(stringBuffer); + Component component = Component.text(stringBuffer.toString()); + components.add(component.mergeStyle(current)); + Component output = Component.text(""); + for(Component comp : components){ + output = output.append(comp); + } + return output; + } + + public static String stripBBCode(String string){ + return strip_bbcode_pattern.matcher(string).replaceAll(""); + } + + public static double getCharWidth(int codePoint, boolean isBold) { + int nonUnicodeIdx = NON_UNICODE_CHARS.indexOf(codePoint); + double width; + if (nonUnicodeIdx != -1) { + width = NON_UNICODE_CHAR_WIDTHS[nonUnicodeIdx]; + if (isBold) { + width += 1; + } + } else { + // MC unicode -- what does this even do? but it's client-only so we can't use it directly :/ + int j = UNICODE_CHAR_WIDTHS[codePoint] >>> 4; + int k = UNICODE_CHAR_WIDTHS[codePoint] & 15; + + if (k > 7) { + k = 15; + j = 0; + } + width = ((k + 1) - j) / 2 + 1; + if (isBold) { + width += 0.5; + } + } + return width; + } + + public static int getLength(Component text) { + double length = 0; + for (Component child : text.children()) { + final String txt; + if (child instanceof TextComponent) { + txt = ((TextComponent) child).content(); + } else { // TODO translatable components + continue; + } + boolean isBold = child.hasDecoration(TextDecoration.BOLD); + for (int i = 0; i < txt.length(); ++i) { + length += getCharWidth(txt.codePointAt(i), isBold); + } + } + return (int) Math.ceil(length); + } + + public enum CustomFormat implements TextFormat { + RESET("reset"); + private final String name; + CustomFormat(String name){ + this.name = name; + } + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ColorParser.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ColorParser.java new file mode 100644 index 00000000..577aa10d --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ColorParser.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package codecrafter47.bungeetablistplus.util; + +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.kyori.adventure.text.format.TextFormat; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +public class ColorParser { + + public static String extractColorCodes(String s) { + boolean bold = false; + boolean underlined = false; + boolean magic = false; + boolean italic = false; + boolean strikethrough = false; + NamedTextColor color = NamedTextColor.WHITE; + + boolean escaped = false; + + for (int i = 0; i < s.length(); i++) { + char ch = s.charAt(i); + if (escaped) { + TextFormat code = ChatUtil.BY_CHAR.get(ch); + if (code == null) { + // ignore + } else if (code.equals(TextDecoration.BOLD)) { + bold = true; + } else if (code.equals(TextDecoration.ITALIC)) { + italic = true; + } else if (code.equals(TextDecoration.UNDERLINED)) { + underlined = true; + } else if (code.equals(TextDecoration.STRIKETHROUGH)) { + strikethrough = true; + } else if (code.equals(TextDecoration.OBFUSCATED)) { + magic = true; + } else if (code.equals(ChatUtil.CustomFormat.RESET)) { + bold = false; + italic = false; + underlined = false; + strikethrough = false; + magic = false; + color = NamedTextColor.WHITE; + } else { + bold = false; + italic = false; + underlined = false; + strikethrough = false; + magic = false; + color = (NamedTextColor) code; + } + escaped = false; + } + if (ch == LegacyComponentSerializer.SECTION_CHAR) { + escaped = true; + } + } + + StringBuilder string = new StringBuilder(); + if (!color.equals(NamedTextColor.WHITE)) { + string.append(color); + } + if (bold) { + string.append(TextDecoration.BOLD); + } + if (italic) { + string.append(TextDecoration.ITALIC); + } + if (underlined) { + string.append(TextDecoration.UNDERLINED); + } + if (strikethrough) { + string.append(TextDecoration.STRIKETHROUGH); + } + if (magic) { + string.append(TextDecoration.OBFUSCATED); + } + + return string.toString(); + } + + public static int endofColor(String s, int start) { + boolean escaped = false; + for (int i = start - 1; i < s.length(); i++) { + char ch = s.charAt(i); + if (escaped) { + escaped = false; + } else if (ch == LegacyComponentSerializer.SECTION_CHAR) { + escaped = true; + } else if (i < start) { + + } else { + return i; + } + } + return s.length() - 1; + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ConcurrentBitSet.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ConcurrentBitSet.java new file mode 100644 index 00000000..beea34cd --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ConcurrentBitSet.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.util; + +import com.google.common.base.Preconditions; + +import java.util.concurrent.atomic.AtomicLongArray; +import java.util.function.IntConsumer; + +public final class ConcurrentBitSet { + final int size; + final AtomicLongArray array; + + public ConcurrentBitSet(int size) { + Preconditions.checkArgument(size >= 0, "size must not be negative"); + this.size = size; + this.array = new AtomicLongArray((size + 63) / 64); + } + + public void set(int index) { + Preconditions.checkElementIndex(index, size); + int longIndex = index >> 6; + long mask = 1L << (index & 0x3F); + long expect; + do { + expect = array.get(longIndex); + } while (!array.compareAndSet(longIndex, expect, expect | mask)); + } + + public void clear(int index) { + Preconditions.checkElementIndex(index, size); + int longIndex = index >> 6; + long mask = ~(1L << (index & 0x3F)); + long expect; + do { + expect = array.get(longIndex); + } while (!array.compareAndSet(longIndex, expect, expect & mask)); + } + + public void iterateAndClear(IntConsumer consumer) { + for (int longIndex = 0; longIndex < array.length(); longIndex++) { + long l = array.getAndSet(longIndex, 0); + while (l != 0) { + int i = Long.numberOfTrailingZeros(l); + consumer.accept(longIndex << 6 | i); + l = l & ~(1L << i); + } + } + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ContextAwareOrdering.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ContextAwareOrdering.java new file mode 100644 index 00000000..0d8ec2f8 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ContextAwareOrdering.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.util; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Ordering; + +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; + +public abstract class ContextAwareOrdering { + + public abstract int compare(C1 c1, C2 c2, T first, T second); + + public static ContextAwareOrdering from(Comparator comparator) { + return new ComparatorContextAwareOrdering<>(comparator); + } + + public static ContextAwareOrdering compound(Iterable> comparators) { + return new CompoundContextAwareOrdering<>(ImmutableList.copyOf(comparators)); + } + + public List immutableSortedCopy(C1 c1, C2 c2, Collection elements) { + return new Ordering() { + + @Override + public int compare(@Nullable T left, @Nullable T right) { + return ContextAwareOrdering.this.compare(c1, c2, left, right); + } + }.immutableSortedCopy(elements); + } + + private static class ComparatorContextAwareOrdering extends ContextAwareOrdering { + private final Comparator comperator; + + public ComparatorContextAwareOrdering(Comparator comperator) { + this.comperator = comperator; + } + + @Override + public int compare(C1 c1, C2 c2, T first, T second) { + return comperator.compare(first, second); + } + } + + private static class CompoundContextAwareOrdering extends ContextAwareOrdering { + private final ImmutableList> comperators; + + private CompoundContextAwareOrdering(ImmutableList> comperators) { + this.comperators = comperators; + } + + @Override + public int compare(C1 c1, C2 c2, T first, T second) { + int result; + for (int i = 0; i < comperators.size(); i++) { + if (0 != (result = comperators.get(i).compare(c1, c2, first, second))) { + return result; + } + } + return 0; + } + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/EmptyOrderedPlayerSet.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/EmptyOrderedPlayerSet.java new file mode 100644 index 00000000..ad3093b2 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/EmptyOrderedPlayerSet.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.util; + +import de.codecrafter47.taboverlay.config.player.OrderedPlayerSet; +import de.codecrafter47.taboverlay.config.player.Player; + +public class EmptyOrderedPlayerSet implements OrderedPlayerSet { + + public static final EmptyOrderedPlayerSet INSTANCE = new EmptyOrderedPlayerSet(); + + @Override + public int getCount() { + return 0; + } + + @Override + public void addListener(Listener listener) { + + } + + @Override + public void removeListener(Listener listener) { + + } + + @Override + public Player getPlayer(int index) { + throw new IndexOutOfBoundsException(); + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/EmptyPlayerSet.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/EmptyPlayerSet.java new file mode 100644 index 00000000..b004f702 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/EmptyPlayerSet.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.util; + +import de.codecrafter47.taboverlay.config.context.Context; +import de.codecrafter47.taboverlay.config.expression.template.ExpressionTemplate; +import de.codecrafter47.taboverlay.config.player.OrderedPlayerSet; +import de.codecrafter47.taboverlay.config.player.Player; +import de.codecrafter47.taboverlay.config.player.PlayerSet; +import de.codecrafter47.taboverlay.config.player.PlayerSetPartition; +import de.codecrafter47.taboverlay.config.template.PlayerOrderTemplate; + +import java.util.Collection; +import java.util.Collections; + +public class EmptyPlayerSet implements PlayerSet { + + public static final EmptyPlayerSet INSTANCE = new EmptyPlayerSet(); + + @Override + public int getCount() { + return 0; + } + + @Override + public void addListener(Listener listener) { + + } + + @Override + public void removeListener(Listener listener) { + + } + + @Override + public Collection getPlayers() { + return Collections.emptyList(); + } + + @Override + public OrderedPlayerSet getOrderedPlayerSet(Context context, PlayerOrderTemplate playerOrderTemplate) { + return EmptyOrderedPlayerSet.INSTANCE; + } + + @Override + public PlayerSetPartition getPartition(ExpressionTemplate partitionFunction) { + throw new UnsupportedOperationException(); + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ExceptionHandlingEventExecutor.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ExceptionHandlingEventExecutor.java new file mode 100644 index 00000000..37b895be --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ExceptionHandlingEventExecutor.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.util; + +import io.netty.util.concurrent.EventExecutorGroup; +import io.netty.util.concurrent.SingleThreadEventExecutor; + +import java.util.concurrent.Executor; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class ExceptionHandlingEventExecutor extends SingleThreadEventExecutor { + + private final Logger logger; + + public ExceptionHandlingEventExecutor(EventExecutorGroup parent, Executor executor, Logger logger) { + super(parent, executor, true); + this.logger = logger; + } + + @Override + protected void run() { + do { + Runnable task = this.takeTask(); + if (task != null) { + try { + task.run(); + } catch (Throwable th) { + logger.log(Level.WARNING, "An unexpected error occurred: " + th.getMessage(), th); + } + this.updateLastExecutionTime(); + } + } while (!this.confirmShutdown()); + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/Functions.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/Functions.java new file mode 100644 index 00000000..93f2a68e --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/Functions.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.util; + +import java.util.function.Function; + +public class Functions { + + public static Function composeNullable(Function g, Function f) { + return (A a) -> { + B b = f.apply(a); + return b != null ? g.apply(b) : null; + }; + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/GeyserCompat.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/GeyserCompat.java new file mode 100644 index 00000000..00af2924 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/GeyserCompat.java @@ -0,0 +1,51 @@ +package codecrafter47.bungeetablistplus.util; + +import org.geysermc.api.Geyser; +import org.geysermc.api.GeyserApiBase; +import org.geysermc.api.connection.Connection; +import org.geysermc.floodgate.api.FloodgateApi; + +import java.util.UUID; +import java.util.function.Function; + +public class GeyserCompat { + + private static Function geyserHook; + private static Function floodgateHook; + + static { + + // Geyser + try { + Class.forName("org.geysermc.api.connection.Connection"); + geyserHook = uuid -> { + GeyserApiBase instance = Geyser.api(); + if (instance == null) { + return false; + } + Connection session = instance.connectionByUuid(uuid); + return session != null; + }; + } catch (Throwable ignored) { + geyserHook = uuid -> false; + } + + // Floodgate + try { + Class.forName("org.geysermc.floodgate.api.FloodgateApi"); + floodgateHook = uuid -> { + FloodgateApi api = FloodgateApi.getInstance(); + if (api == null) { + return false; + } + return api.isFloodgatePlayer(uuid); + }; + } catch (Throwable ignored) { + floodgateHook = uuid -> false; + } + } + + public static boolean isBedrockPlayer(UUID uuid) { + return geyserHook.apply(uuid) || floodgateHook.apply(uuid); + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/IconUtil.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/IconUtil.java new file mode 100644 index 00000000..dbe51af9 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/IconUtil.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.util; + +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.util.GameProfile; +import de.codecrafter47.taboverlay.Icon; +import de.codecrafter47.taboverlay.ProfileProperty; +import lombok.experimental.UtilityClass; + +import javax.annotation.Nonnull; + +@UtilityClass +public class IconUtil { + + public Icon convert(codecrafter47.bungeetablistplus.api.velocity.Icon icon) { + String[][] properties = icon.getProperties(); + if (properties.length == 0) { + return Icon.DEFAULT_STEVE; + } + return new Icon(new ProfileProperty(properties[0][0], properties[0][1], properties[0][2])); + } + + public codecrafter47.bungeetablistplus.api.velocity.Icon convert(Icon icon) { + if (icon.hasTextureProperty()) { + ProfileProperty property = icon.getTextureProperty(); + return new codecrafter47.bungeetablistplus.api.velocity.Icon(null, new String[][]{{property.getName(), property.getValue(), property.getSignature()}}); + } else { + return new codecrafter47.bungeetablistplus.api.velocity.Icon(null, new String[0][]); + } + } + + @Nonnull + public Icon getIconFromPlayer(Player player) { + GameProfile profile = player.getGameProfile(); + if (profile != null) { + String[][] properties = Property119Handler.getProperties(profile); + for (String[] s : properties) { + if (s[0].equals("textures")) { + return new Icon(new ProfileProperty(s[0], s[1], s[2])); + } + } + } + if ((player.getUniqueId().hashCode() & 1) == 1) { + return Icon.DEFAULT_ALEX; + } else { + return Icon.DEFAULT_STEVE; + } + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/IntToIntFunction.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/IntToIntFunction.java new file mode 100644 index 00000000..b91aa305 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/IntToIntFunction.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.util; + +@FunctionalInterface +public interface IntToIntFunction { + + int apply(int i); +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/MapFunction.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/MapFunction.java new file mode 100644 index 00000000..7bb68922 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/MapFunction.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.util; + +import lombok.EqualsAndHashCode; + +import java.util.Map; +import java.util.function.Function; + +@EqualsAndHashCode +public class MapFunction implements Function { + + private final Map map; + + public MapFunction(Map map) { + this.map = map; + } + + @Override + public String apply(String s) { + return map.getOrDefault(s, s); + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/MatchingStringsCollection.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/MatchingStringsCollection.java new file mode 100644 index 00000000..63000354 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/MatchingStringsCollection.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.util; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import it.unimi.dsi.fastutil.objects.Object2BooleanMap; +import it.unimi.dsi.fastutil.objects.Object2BooleanOpenHashMap; + +import java.util.List; +import java.util.logging.Level; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import java.util.stream.Collectors; + +public class MatchingStringsCollection { + + private final List patterns; + private final Object2BooleanMap cache = new Object2BooleanOpenHashMap<>(); + + public MatchingStringsCollection(List patterns) { + this.patterns = patterns.stream() + .filter(regex -> { + try { + Pattern.compile(regex); + return true; + } catch (PatternSyntaxException e) { + BungeeTabListPlus.getInstance().getLogger().log(Level.WARNING, "Illegal regex", e); + return false; + } + }).collect(Collectors.toList()); + } + + public boolean contains(String s) { + if (cache.containsKey(s)) { + return cache.getBoolean(s); + } else { + boolean r = compute(s); + cache.put(s, r); + return r; + } + } + + private boolean compute(String s) { + for (String pattern : patterns) { + try { + if (s.matches(pattern)) { + return true; + } + } catch (PatternSyntaxException e) { + e.printStackTrace(); + } + } + return false; + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/Object2IntHashMultimap.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/Object2IntHashMultimap.java new file mode 100644 index 00000000..c251e113 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/Object2IntHashMultimap.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.util; + +import it.unimi.dsi.fastutil.ints.IntCollection; +import it.unimi.dsi.fastutil.ints.IntLinkedOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSets; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; + +public class Object2IntHashMultimap { + + private final Object2ObjectMap map = new Object2ObjectOpenHashMap<>(); + + public boolean contains(T key, int value) { + IntCollection collection = map.get(key); + return collection != null && collection.contains(value); + } + + public IntCollection get(T key) { + return map.getOrDefault(key, IntSets.EMPTY_SET); + } + + public void remove(T key, int value) { + IntCollection collection = map.get(key); + if (collection != null) { + collection.rem(value); + if (collection.isEmpty()) { + map.remove(key); + } + } + } + + public void put(T key, int value) { + IntCollection collection = map.computeIfAbsent(key, k -> new IntLinkedOpenHashSet(2, .75f)); + collection.add(value); + } + + public boolean containsKey(T oldUuid) { + return map.containsKey(oldUuid); + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/Property119Handler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/Property119Handler.java new file mode 100644 index 00000000..68940c4f --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/Property119Handler.java @@ -0,0 +1,27 @@ +package codecrafter47.bungeetablistplus.util; + +import com.velocitypowered.api.util.GameProfile; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; + +import java.util.Arrays; + +public class Property119Handler { + public static String[][] getProperties(LegacyPlayerListItem.Item item) { + return Arrays.stream(item.getProperties().toArray(new GameProfile.Property[0])).map(prop -> new String[]{prop.getName(), prop.getValue(), prop.getSignature()}).toArray(String[][]::new); + } + + public static String[][] getProperties(GameProfile profile) { + return profile.getProperties().stream().map(prop -> new String[]{prop.getName(), prop.getValue(), prop.getSignature()}).toArray(String[][]::new); + } + + public static void setProperties(LegacyPlayerListItem.Item item, String[][] properties) { + item.setProperties(Arrays.asList(Arrays.stream(properties).map(array -> new GameProfile.Property(array[0], array[1], array.length >= 3 ? array[2] : null)).toArray(GameProfile.Property[]::new))); + } + + public static void setProperties(UpsertPlayerInfo.Entry item, String[][] properties) { + GameProfile profile = item.getProfile(); + profile.addProperties(Arrays.asList(Arrays.stream(properties).map(array -> new GameProfile.Property(array[0], array[1], array.length >= 3 ? array[2] : null)).toArray(GameProfile.Property[]::new))); + item.setProfile(profile); + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ProxyServer.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ProxyServer.java new file mode 100644 index 00000000..22f8d01b --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ProxyServer.java @@ -0,0 +1,14 @@ +package codecrafter47.bungeetablistplus.util; + +public class ProxyServer { + + private static com.velocitypowered.api.proxy.ProxyServer _proxyServer; + + public static com.velocitypowered.api.proxy.ProxyServer getInstance() { + return _proxyServer; + } + + public static void setProxyServer(com.velocitypowered.api.proxy.ProxyServer _proxyServer) { + ProxyServer._proxyServer = _proxyServer; + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ReflectionUtil.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ReflectionUtil.java new file mode 100644 index 00000000..f12a72d6 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ReflectionUtil.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.util; + +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.player.TabList; +import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; + +import java.lang.reflect.Field; + +public class ReflectionUtil { + public static void setTablistHandler(Player player, TabList tablistHandler) throws NoSuchFieldException, IllegalAccessException { + setField(ConnectedPlayer.class, player, "tabList", tablistHandler, 5); + } + + public static TabList getTablistHandler(Player player) throws NoSuchFieldException, IllegalAccessException { + return getField(ConnectedPlayer.class, player, "tabList", 5); + } + + public static MinecraftConnection getChannelWrapper(Player player) throws NoSuchFieldException, IllegalAccessException { + return getField(ConnectedPlayer.class, player, "connection", 50); + } + + public static void setField(Class clazz, Object instance, String field, Object value) throws NoSuchFieldException, IllegalAccessException { + Field f = clazz.getDeclaredField(field); + f.setAccessible(true); + f.set(instance, value); + } + + public static void setField(Class clazz, Object instance, String field, Object value, int tries) throws NoSuchFieldException, IllegalAccessException { + while (--tries > 0) { + try { + setField(clazz, instance, field, value); + return; + } catch (NoSuchFieldException | IllegalAccessException ignored) { + } + } + setField(clazz, instance, field, value); + } + + @SuppressWarnings("unchecked") + public static T getField(Class clazz, Object instance, String field) throws NoSuchFieldException, IllegalAccessException { + Field f = clazz.getDeclaredField(field); + f.setAccessible(true); + return (T) f.get(instance); + } + + public static T getField(Class clazz, Object instance, String field, int tries) throws NoSuchFieldException, IllegalAccessException { + while (--tries > 0) { + try { + return getField(clazz, instance, field); + } catch (NoSuchFieldException | IllegalAccessException ignored) { + } + } + return getField(clazz, instance, field); + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/VelocityPlugin.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/VelocityPlugin.java new file mode 100644 index 00000000..1d07bc4d --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/VelocityPlugin.java @@ -0,0 +1,42 @@ +package codecrafter47.bungeetablistplus.util; + +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ProxyServer; +import lombok.Getter; +import net.kyori.adventure.text.Component; +import org.slf4j.Logger; + +import java.lang.reflect.Field; +import java.nio.file.Path; +import java.util.concurrent.atomic.AtomicBoolean; + +public class VelocityPlugin { + + @Getter + private final ProxyServer proxy; + @Getter + private final Logger logger; + @Getter + private final Path dataDirectory; + @Getter + private final String version; + + public VelocityPlugin(ProxyServer proxy, Logger logger, Path dataDirectory, String version){ + this.proxy = proxy; + this.logger = logger; + this.dataDirectory = dataDirectory; + this.version = version; + } + + public static boolean isProxyRunning(ProxyServer proxyServer){ + try { + Class velocityServer = Class.forName("com.velocitypowered.proxy.VelocityServer"); + Field shutdownInProgress = velocityServer.getDeclaredField("shutdownInProgress"); + shutdownInProgress.setAccessible(true); + + return !((AtomicBoolean) shutdownInProgress.get(proxyServer)).get(); + } catch (NoSuchFieldException | ClassNotFoundException | IllegalAccessException ignored) { } + // Return not running if it can't grab the shutdownInProgress value; + return false; + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ProtocolVersionProvider.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ProtocolVersionProvider.java new file mode 100644 index 00000000..7ba26a76 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ProtocolVersionProvider.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.version; + + +import com.velocitypowered.api.proxy.Player; + +public interface ProtocolVersionProvider { + + boolean has18OrLater(Player player); + + boolean has113OrLater(Player player); + + boolean has119OrLater(Player player); + + boolean is18(Player player); + + String getVersion(Player player); + + boolean has1193OrLater(Player player); +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/version/VelocityProtocolVersionProvider.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/version/VelocityProtocolVersionProvider.java new file mode 100644 index 00000000..9465ebee --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/version/VelocityProtocolVersionProvider.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.version; + +import com.velocitypowered.api.proxy.Player; +import de.codecrafter47.data.velocity.VelocityClientVersionProvider; + + +public class VelocityProtocolVersionProvider implements ProtocolVersionProvider { + + private final VelocityClientVersionProvider clientVersionProvider = new VelocityClientVersionProvider(); + + @Override + public boolean has18OrLater(Player player) { + return player.getProtocolVersion().getProtocol() >= 47; + } + + @Override + public boolean has113OrLater(Player player) { + return player.getProtocolVersion().getProtocol() >= 393; + } + + @Override + public boolean has119OrLater(Player player) { + return player.getProtocolVersion().getProtocol() >= 759; + } + + @Override + public boolean is18(Player player) { + return player.getProtocolVersion().getProtocol() == 47; + } + + @Override + public String getVersion(Player player) { + return clientVersionProvider.apply(player); + } + + @Override + public boolean has1193OrLater(Player player) { + return player.getProtocolVersion().getProtocol() >= 761; + } + +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ViaVersionProtocolVersionProvider.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ViaVersionProtocolVersionProvider.java new file mode 100644 index 00000000..786b1247 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ViaVersionProtocolVersionProvider.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.version; + +import com.velocitypowered.api.proxy.Player; +import com.viaversion.viaversion.api.Via; +import com.viaversion.viaversion.api.protocol.version.ProtocolVersion; + + +public class ViaVersionProtocolVersionProvider implements ProtocolVersionProvider { + + @Override + public boolean has18OrLater(Player player) { + int version = Via.getAPI().getPlayerVersion(player); + return ProtocolVersion.getIndex(ProtocolVersion.getProtocol(version)) >= ProtocolVersion.getIndex(ProtocolVersion.v1_8); + } + + @Override + public boolean has113OrLater(Player player) { + // Note this doesn't care about the client version, but about the server version + return player.getProtocolVersion().getProtocol() >= 393; + } + + @Override + public boolean has119OrLater(Player player) { + return Via.getAPI().getPlayerVersion(player) >= 759; + } + + @Override + public boolean has1193OrLater(Player player) { + return Via.getAPI().getPlayerVersion(player) >= 761; + } + + @Override + public boolean is18(Player player) { + return Via.getAPI().getPlayerVersion(player) == 47; + } + + @Override + public String getVersion(Player player) { + return ProtocolVersion.getProtocol(Via.getAPI().getPlayerVersion(player)).getName(); + } + +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/view/PlayersByServerComponentView.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/view/PlayersByServerComponentView.java new file mode 100644 index 00000000..78f77a2f --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/view/PlayersByServerComponentView.java @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.view; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.config.PlayersByServerComponentConfiguration; +import codecrafter47.bungeetablistplus.data.BTLPVelocityDataKeys; +import codecrafter47.bungeetablistplus.util.ContextAwareOrdering; +import codecrafter47.bungeetablistplus.util.EmptyPlayerSet; +import codecrafter47.bungeetablistplus.util.ProxyServer; +import de.codecrafter47.data.api.DataHolder; +import de.codecrafter47.data.velocity.api.VelocityData; +import de.codecrafter47.taboverlay.config.context.Context; +import de.codecrafter47.taboverlay.config.expression.template.ExpressionTemplate; +import de.codecrafter47.taboverlay.config.expression.template.ExpressionTemplates; +import de.codecrafter47.taboverlay.config.player.PlayerSet; +import de.codecrafter47.taboverlay.config.player.PlayerSetPartition; +import de.codecrafter47.taboverlay.config.template.PlayerOrderTemplate; +import de.codecrafter47.taboverlay.config.template.PlayerSetTemplate; +import de.codecrafter47.taboverlay.config.template.component.ComponentTemplate; +import de.codecrafter47.taboverlay.config.template.icon.IconTemplate; +import de.codecrafter47.taboverlay.config.template.ping.PingTemplate; +import de.codecrafter47.taboverlay.config.template.text.TextTemplate; +import de.codecrafter47.taboverlay.config.view.components.ComponentView; +import de.codecrafter47.taboverlay.config.view.components.ContainerComponentView; +import de.codecrafter47.taboverlay.config.view.components.ListComponentView; +import de.codecrafter47.taboverlay.config.view.components.PartitionedPlayersView; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class PlayersByServerComponentView extends PartitionedPlayersView { + + private final Function mergeSections; + private final Set hiddenServers; + private final PlayersByServerComponentConfiguration.ServerOptions showServers; + private final ContextAwareOrdering serverComparator; + + private final Set persistentSections = new HashSet<>(); + protected final Map emptySectionMap = new HashMap<>(); + + private final Runnable onlineStateUpdateListener = this::updateOnlineServers; + private List servers; + + private final boolean prioritizeViewerServer; + + public PlayersByServerComponentView(int columns, PlayerSetTemplate playerSetTemplate, ComponentTemplate playerComponentTemplate, int playerComponentSize, ComponentTemplate morePlayerComponentTemplate, int morePlayerComponentSize, PlayerOrderTemplate playerOrderTemplate, TextTemplate defaultTextTemplate, PingTemplate defaultPingTemplate, IconTemplate defaultIconTemplate, ExpressionTemplate partitionFunction, Function mergeSections, ComponentTemplate sectionHeader, ComponentTemplate sectionFooter, ComponentTemplate sectionSeparator, int minSizePerSection, int maxSizePerSection, SectionContextFactory sectionContextFactory, Set hiddenServers, PlayersByServerComponentConfiguration.ServerOptions showServers, ContextAwareOrdering serverComparator, boolean prioritizeViewerServer) { + super(columns, playerSetTemplate, playerComponentTemplate, playerComponentSize, morePlayerComponentTemplate, morePlayerComponentSize, playerOrderTemplate, defaultTextTemplate, defaultPingTemplate, defaultIconTemplate, ExpressionTemplates.applyStringToStringFunction(partitionFunction, mergeSections), sectionHeader, sectionFooter, sectionSeparator, minSizePerSection, maxSizePerSection, sectionContextFactory); + this.mergeSections = mergeSections; + this.hiddenServers = hiddenServers; + this.showServers = showServers; + this.serverComparator = serverComparator; + this.prioritizeViewerServer = prioritizeViewerServer; + } + + @Override + protected void onActivation() { + super.onActivation(); + + if (showServers == PlayersByServerComponentConfiguration.ServerOptions.ALL) { + servers = ProxyServer.getInstance().getAllServers().stream().map((registeredServer -> registeredServer.getServerInfo().getName())).collect(Collectors.toList()); + for (String serverName : servers) { + if (hiddenServers.contains(serverName)) { + continue; + } + addPersistentSection(mergeSections.apply(serverName), false); + } + } else if (showServers == PlayersByServerComponentConfiguration.ServerOptions.ONLINE) { + servers = ProxyServer.getInstance().getAllServers().stream().map((registeredServer -> registeredServer.getServerInfo().getName())).collect(Collectors.toList()); + for (String serverName : servers) { + serverName = mergeSections.apply(serverName); + if (hiddenServers.contains(serverName)) { + continue; + } + DataHolder serverDataHolder = BungeeTabListPlus.getInstance().getDataManager().getServerDataHolder(serverName); + serverDataHolder.addDataChangeListener(BTLPVelocityDataKeys.DATA_KEY_SERVER_ONLINE, onlineStateUpdateListener); + Boolean online = serverDataHolder.get(BTLPVelocityDataKeys.DATA_KEY_SERVER_ONLINE); + if (online == null) { + online = true; + } + if (online) { + addPersistentSection(serverName, false); + } + } + } + + updateLayoutRequirements(false); + } + + @Override + protected void onDeactivation() { + super.onDeactivation(); + if (showServers == PlayersByServerComponentConfiguration.ServerOptions.ONLINE) { + for (String serverName : servers) { + if (hiddenServers.contains(serverName)) { + continue; + } + DataHolder serverDataHolder = BungeeTabListPlus.getInstance().getDataManager().getServerDataHolder(serverName); + serverDataHolder.removeDataChangeListener(BTLPVelocityDataKeys.DATA_KEY_SERVER_ONLINE, onlineStateUpdateListener); + } + } + } + + private void updateOnlineServers() { + for (String serverName : servers) { + serverName = mergeSections.apply(serverName); + if (hiddenServers.contains(serverName)) { + continue; + } + DataHolder serverDataHolder = BungeeTabListPlus.getInstance().getDataManager().getServerDataHolder(serverName); + serverDataHolder.addDataChangeListener(BTLPVelocityDataKeys.DATA_KEY_SERVER_ONLINE, onlineStateUpdateListener); + Boolean online = serverDataHolder.get(BTLPVelocityDataKeys.DATA_KEY_SERVER_ONLINE); + if (online == null) { + online = true; + } + if (online && !persistentSections.contains(serverName)) { + addPersistentSection(serverName, true); + } else if (!online && persistentSections.contains(serverName)) { + removePersistentSection(serverName, true); + } + } + } + + private void addPersistentSection(String id, boolean notify) { + if (persistentSections.contains(id)) { + return; + } + persistentSections.add(id); + if (!sectionMap.containsKey(id)) { + addEmptySection(id, notify); + } + } + + private void removePersistentSection(String id, boolean notify) { + if (!persistentSections.contains(id)) { + return; + } + persistentSections.remove(id); + if (persistentSections.contains(id)) { + removeEmptySection(id, notify); + } + } + + private void addEmptySection(String id, boolean notify) { + Context sectionContext = sectionContextFactory.createSectionContext(getContext(), id, EmptyPlayerSet.INSTANCE); + ComponentView componentView = createEmptySectionView(); + componentView.activate(sectionContext, this); + emptySectionMap.put(id, componentView); + if (sectionSeparator != null && !super.components.isEmpty()) { + ComponentView separator = sectionSeparator.instantiate(); + separator.activate(getContext(), this); + super.components.add(separator); + } + super.components.add(componentView); + if (notify) { + requestLayoutUpdate(this); + } + } + + private void removeEmptySection(String id, boolean notify) { + ComponentView componentView = emptySectionMap.remove(id); + int index = super.components.indexOf(componentView); + if (sectionSeparator == null) { + super.components.remove(index); + } else if (index != 0 && super.components.size() > 1) { + super.components.remove(index); + ComponentView separator = super.components.remove(index - 1); + separator.deactivate(); + } else if (index != super.components.size() - 1 && super.components.size() > 1) { + super.components.remove(index); + ComponentView separator = super.components.remove(index); + separator.deactivate(); + } else { + super.components.remove(index); + } + componentView.deactivate(); + if (notify) { + requestLayoutUpdate(this); + } + } + + private ComponentView createEmptySectionView() { + List components = new ArrayList<>(); + if (sectionHeader != null) { + components.add(sectionHeader.instantiate()); + } + if (sectionFooter != null) { + components.add(sectionFooter.instantiate()); + } + return new ContainerComponentView(new ListComponentView(components, super.columns, defaultTextTemplate.instantiate(), defaultPingTemplate.instantiate(), defaultIconTemplate.instantiate()), false, minSizePerSection, maxSizePerSection, super.columns, true); + } + + @Override + protected void addPartition(String id, PlayerSet playerSet, boolean notify) { + if (id == null || hiddenServers.contains(id)) { + return; + } + if (persistentSections.contains(id)) { + removeEmptySection(id, false); + } + super.addPartition(id, playerSet, notify); + } + + @Override + public void onPartitionRemoved(String id) { + if (id == null || hiddenServers.contains(id)) { + return; + } + super.onPartitionRemoved(id); + if (persistentSections.contains(id)) { + addEmptySection(id, true); + } + } + + @Override + protected void updateLayoutRequirements(boolean notify) { + if (serverComparator != null) { + List serverNames = new ArrayList<>(); + serverNames.addAll(sectionMap.keySet()); + serverNames.addAll(emptySectionMap.keySet()); + List sortedServers = serverComparator.immutableSortedCopy(getContext(), playerSetPartition, serverNames); + + List elements = new ArrayList<>(); + for (String server : sortedServers) { + if (sectionMap.containsKey(server)) { + elements.add(sectionMap.get(server)); + } else { + elements.add(emptySectionMap.get(server)); + } + } + + if (sectionSeparator == null) { + for (int i = 0; i < elements.size(); i++) { + ComponentView element = elements.get(i); + super.components.set(i, element); + } + } else { + for (int i = 0; i < elements.size(); i++) { + ComponentView element = elements.get(i); + super.components.set(2 * i, element); + } + } + } + super.updateLayoutRequirements(notify); + } + + @Override + protected int getInitialSizeEstimate(ComponentView componentView) { + int initialSize = super.getInitialSizeEstimate(componentView); + if (prioritizeViewerServer) { + String server = getContext().getViewer().get(VelocityData.Velocity_Server); + if (server != null && componentView == sectionMap.get(server)) { + int preferredSize = componentView.getPreferredSize(); + preferredSize = Integer.min(preferredSize, getArea().getSize() - minSize + initialSize); + initialSize = Integer.max(initialSize, preferredSize); + } + } + return initialSize; + } +} diff --git a/velocity/src/main/resources/default.yml b/velocity/src/main/resources/default.yml new file mode 100644 index 00000000..b90bb802 --- /dev/null +++ b/velocity/src/main/resources/default.yml @@ -0,0 +1,93 @@ +# This is the default configuration file of BungeeTabListPlus. +# +# Since the configuration of the plugin is quite complex you +# might want to have a look at the wiki from time to time. +# +# Wiki: https://github.com/CodeCrafter47/BungeeTabListPlus/wiki +# Placeholders: https://github.com/CodeCrafter47/BungeeTabListPlus/wiki/Placeholders +# Examples: https://github.com/CodeCrafter47/BungeeTabListPlus/wiki/Examples +# + +showTo: all +priority: 0 + +showHeaderFooter: true +header: + - "&cWelcome &f${viewer name}" + - "&eW&celcome &f${viewer name}" + - "&eWe&clcome &f${viewer name}" + - "&eWel&ccome &f${viewer name}" + - "&eWelc&come &f${viewer name}" + - "&eWelco&cme &f${viewer name}" + - "&eWelcom&ce &f${viewer name}" + - "&eWelcome &f${viewer name}" + - "&cW&eelcome &f${viewer name}" + - "&cWe&elcome &f${viewer name}" + - "&cWel&ecome &f${viewer name}" + - "&cWelc&eome &f${viewer name}" + - "&cWelco&eme &f${viewer name}" + - "&cWelcom&ee &f${viewer name}" + - "&cWelcome &f${viewer name}" +headerAnimationUpdateInterval: 0.2 +footer: |- + &4Powered by BungeeTabListPlus + &fWiki:&7 https://github.com/CodeCrafter47/BungeeTabListPlus/wiki + +# Configure whether hidden players appear on the tab list +# see https://github.com/CodeCrafter47/BungeeTabListPlus/wiki/Hidden-Players +hiddenPlayers: VISIBLE_TO_ADMINS + +playerSets: + all_players: all + +type: FIXED_SIZE +size: 60 + +defaultIcon: colors/dark_gray.png +defaultPing: 1000 + +components: + - {text: "&cServer: &6${viewer server}", icon: "default/server.png", ping: 0} + - {text: "&cRank: &6${viewer vault_primary_group}", icon: "default/rank.png", ping: 0} + - {text: "&cPing: ${viewer_colored_ping}ms", icon: "default/ping.png", ping: 0} + - + - + - + - !players_by_server + playerSet: all_players + serverHeader: + - {text: "&e&n${server}&f&o (${server_player_count}):", icon: "colors/yellow.png", ping: 0} + serverSeparator: + - + - + - + showServers: ALL + playerComponent: "${player vault_prefix}${player name}${afk_tag}" + morePlayersComponent: {text: "&7... and &e${other_count} &7others", icon: "colors/gray.png", ping: 0} + - !spacer {} + - + - + - + - {text: "&cTime: &6${time H:mm:ss}", icon: "default/clock.png", ping: 0} + - {text: "&cPlayers: &6${playerset:all_players size}", icon: "default/players.png", ping: 0} + - {text: "&cBalance: &6${viewer vault_balance}", icon: "default/balance.png", ping: 0} + +# Custom placeholders are a powerful mechanism to add more dynamic content +# to the tab list. +# See https://github.com/CodeCrafter47/BungeeTabListPlus/wiki/Custom=Placeholders +customPlaceholders: + afk_tag: + !conditional + condition: ${player essentials_afk} + true: "&7|&oaway" + false: "" + viewer_colored_ping: + !conditional + condition: "${viewer ping} < 150" + true: ${viewer_colored_ping0} + false: "&c${viewer ping}" + viewer_colored_ping0: + !conditional + condition: "${viewer ping} < 50" + true: "&a${viewer ping}" + false: "&e${viewer ping}" \ No newline at end of file diff --git a/velocity/src/main/resources/heads/cache.txt b/velocity/src/main/resources/heads/cache.txt new file mode 100644 index 00000000..b8e66fbc --- /dev/null +++ b/velocity/src/main/resources/heads/cache.txt @@ -0,0 +1,22 @@ +//+qAP//VVX//1VV//+qAP//qgD//6oA//+qAP//qgD//1VV//+qAP//qgD//1VV//+qAP//qgD//6oA//+qAP//VVX//6oA//+qAP//VVX//6oA//+qAP//qgD//6oA//+qAP//VVX//1VV//+qAP//qgD/qgAA/6oAAP//qgD//1VV//9VVf//VVX//1VV/6oAAP//qgD//6oA/6oAAP//qgD//6oA//+qAP//qgD/qgAA//+qAP//qgD/qgAA//+qAP//qgD//6oA//+qAP//qgD/qgAA/6oAAP//qgD//6oA//+qAP//qgD//6oA/6oAAP+qAAD/qgAA/6oAAA== eyJ0aW1lc3RhbXAiOjE0NTU2MjM5NDAzMjgsInByb2ZpbGVJZCI6ImIzYjE4MzQ1MzViZjRiNzU4ZTBjZGJmMGY4MjA2NTZlIiwicHJvZmlsZU5hbWUiOiIxMDExMTEiLCJzaWduYXR1cmVSZXF1aXJlZCI6dHJ1ZSwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzE0ZjM5YTI5ZGY1OThkYzgyNzJkMWNhNTkyYWRiMDVlOTM1ZWMwYmI4NjU4YjJiNmUwMTUxYjkyYjU3OWUyNzAifX19 xvQp91FVaIOishDubo+1z9bZRBQRM4BvQh/3NVD0IlJ5fVyeLOF+KmSX4Zt990YYxPcuRniUrh2SGY13r9u8AgspC1OHAVHijts7BfE841V3bfK73EHi7AZSQsx5gXiu/fdCV0uvJyYYSVR1DJQ1AKOzLHtUqz98z6MgPoYCs4F0od60oIjpqSXXW6ZkKfqE6SGrmGNn+rdsIud1k68xufFXjgHt3ZNKbnKuqNQkdgIlm/UvHTGRksfQlDbCtyHdYb7+8U3HB6GlN0YfPqv2eO3EuvBOPEgHwav/O52Fw14Irs7bn7/Sl/xvjY8HSzhtukEj0zdszcDs8T4FfCVxJuu+af1YpZ0itelT091/SzT8YJhskCq9poqVUIPJafCJaf4ckGO7qD5dWWl4E45xW4AqpplcOi78yhbK6zSd51jwNV3MolCzuJkYfw6mE/f99DsDw03gQljiF2+Tetx0oRQFxSR2YzsKJmJvU8eZxZcnYFE+ij+LHZWGW/3UYlKzCsgBGGFy3D2tL9CYJvf9SqrSixpRQNajL7CFcOfJZ6xLnQpbtNZ5PxXR+2q/GyESwGk26ihmrtI4+H3hcWbqeCOFYM43EpasvjENk1ZaaXOz7I0pRPzrh4tiEmpvaWmyCPl0Jl5cm7bgVMO7Ofq+6SWujLeMa/aVGjTiztHzpvU= +//+qAP//qgD//6oA//+qAP//qgD//6oA//+qAP//qgD//6oA//+qAP//qgD//6oA//+qAP//qgD//6oA//+qAP//qgD//6oA//+qAP//qgD//6oA//+qAP//VVX//4gi//+qAP//qgD//6oA//+qAP//VVX//4gi//9VVf//iCL//6oA//+qAP//VVX//4gi//9VVf//iCL//1VV//+IIv//VVX//4gi//9VVf//iCL//1VV//+IIv//VVX//4gi//9VVf//iCL//1VV//+IIv//VVX//4gi//9VVf//iCL//6oA//+qAP//qgD//6oA//+qAP//qgD//6oA//+qAA== eyJ0aW1lc3RhbXAiOjE0NTU2MjM5MDc4MzAsInByb2ZpbGVJZCI6ImIzYjE4MzQ1MzViZjRiNzU4ZTBjZGJmMGY4MjA2NTZlIiwicHJvZmlsZU5hbWUiOiIxMDExMTEiLCJzaWduYXR1cmVSZXF1aXJlZCI6dHJ1ZSwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzNmMjRmNDJlMjc3MzEyMjNjMjFkN2ZmNDNhYTVmYjA0YjY0NjM1ZmZmMzY2MzU1ODUxMWRiZjZkNzFlZWQifX19 GOme0sRWJN4bncHQ3AtRhpFCZoAB0fq+1/+Te9vgOFeGdXjL5nQ/XH2fmG/rawEilEotfSTavCobvIRiISZZUSD9RitaDCqDHhq99OG+XXHNoWm2/+UJsy755455VmpECiwmUZjHOMZjDcxGcd3rEDLSfcyfQHC51YyOED4rBc0WUAKcA7nqjFayDYq4ARpd4gtM6kDnfZ5vmGERSL0lBcX2oevkGrlCkg+H8kJurh9A3C+tt0TiKJSkEdGqUBOwoW+A9HSbn/wj6uCssDp8AvWHPw0ArYErs4egTxJt/rFJu5ScbLCFcHENXy6vr9mKAatpnGpNbB9fuXfd27tNyOTH+l+usbfg8MKCn643y/LRBsGRYu+X3nbFLjRmYVbCjUWB4bbLaSR2g9GlwT8QPFXhoXcepo3UXQHlN03IpP5zSgrW1lsycRWxzIGpgzznTmldcgkpRZnCT8vmjC6e7dXizV0UKGXYp2ilycKSJlBsRCxtw8Rp2viC6U7TYU6fdQYocL0ajMEYtUf1ang2/jCuvwzm0cxqGMit56enCD/I5PVcs1nCpHwOmWdFXuvcgKt/XCEkj/KUbx6b8/nUjdVM2N8lmX353MP/CmBvIKJ2U9zMDNccsPv5ymTTOZSTBPH8+VfkVC/jjXzXZMZoS3EUE6Ur0hU2ScF/JsBugRw= +//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVQ== eyJ0aW1lc3RhbXAiOjE0NTU1NzM5ODgzOTUsInByb2ZpbGVJZCI6ImIzYjE4MzQ1MzViZjRiNzU4ZTBjZGJmMGY4MjA2NTZlIiwicHJvZmlsZU5hbWUiOiIxMDExMTEiLCJzaWduYXR1cmVSZXF1aXJlZCI6dHJ1ZSwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlL2YxYTM3ZTUxZjk5ZmFmOTZjNjk2Yzc2NThlMzM2NjZkNjUxYjc2NTJmMWFkZjZkOGVkOTNmOTE4OTUxY2M0YiJ9fX0= jrO4S4A0AcL0jL7w+7iF1LIdTSRrkL8YAFziyaLQ6YpDUyCtNkZXbhJOkDZAMuX8iohN3G2q28d/6ceC1RRiblAicvDWASCmJFxXhVw6e5l52H/1e6ySvVaCJcqxQwvs13+IzR9n1js5OSyocOD3OxJjge3clIR+akldVFnAgfu3i7JouYMqkuHGg6XEeaLPoVoI5Ohcadgl897Z1ITqmKHMNvD1GvkfU2cS2ZEentVAAFnn17VRm+OYT8/L+gKfzHq9H9WsycBJ3sC+Hrx+seXbp/9ay8aP7waUOEOE9crB5MIPXj+P288HjqsoHc162ry82mlxnV+ZpisNq8e+LiwZyWiq3qOgFZUwGhWFczAgWeiiPhl1ODo6wgYawyS6wGhIn9btL2hYU3zLwEXuMIbudeve02dFsycH1tVtSlQWLDDQJNn5cF/hgVwFKIqVF8RDn/tUm/v1tSd9EtzI1u/tbDyexlUlCjwUK9rcO463uvIF4/g73ItBLF/hQClnV05XOBtjY5d4mWp/WWMEmHIVZ5QufbR+hJjM46JuPNDw9xOdEo9UZNxmbGnxfQFMztkWc6ptBlSe52qllmi0sniUKiXwLXRuc0umrGlnnSFPvDIti6becbTlxTCo97EsJUYOpdIBGPMtjc8dbyykd1/iXkm46D/NfPo4JhaIwzs= +//+pAf//mBL//15M//9oQv//YEr//19L//+hCf//qAL//5kR//9VVf/vRkT/+lBQ//9VVf//VVX//1VV//+ZEf//VVX/qgAA/64JAP//VVX/tRYA/6oAAP//VVX//15M//9bT///VVX/uyIA/+5IQP+6IAD/wS4A//JMRP//VVX//287//9VVf+zDAX/60NA/7wjAP+xDwD/80tI//9cTv//V1P//1VV/6oAAP/xSEb/tRUA/7sREf/1TEv//2hC//+VFf//VVX//1VV//9VVf//VVX//1VV//9rP///khj//6oA//+VFf//VVX//2ZE//1aTP//VVX//5sP//+oAg== eyJ0aW1lc3RhbXAiOjE0NTU2MjQzMDIzMDAsInByb2ZpbGVJZCI6ImIzYjE4MzQ1MzViZjRiNzU4ZTBjZGJmMGY4MjA2NTZlIiwicHJvZmlsZU5hbWUiOiIxMDExMTEiLCJzaWduYXR1cmVSZXF1aXJlZCI6dHJ1ZSwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzM4NWU0Zjc4NTg2YzRkZDA5ZWVlZTk1M2MxZDQ5NDZmMjhkNWU1YzI4ZmJhYzM2MjM4OTc5NDk2NTljZjU4In19fQ== kEETv8JpKy/OgwwGIStSBl8NCwFNC0oQ2ViahBbNTp7BX65/kXciiOIq367fZ6kfeHmyjCSFmqdzCTmfk8Vz1+pNkqYWzttUWrqLHmHe+LwvuJMeswpjJnTH+xYlm72vBEipUK5l+6UHWBtlAL0WYHlTpdqIiCqsKtN8H1ozEzB9xE3C0hFY52s0E0VTAnyTsAUJ/oBaP5JBu5eaLY2Nf22frPz7uCCDNAdUauCbd0+c+E2c/awy1ya6IpuAGAOGYwNnkNlIb3Zc709z2d61+TDjZATRHmHfNn4MXMdDuePygjJsAkRjQrOoDE4qWI9Sv9Ifrx7mHqyTrQ8wPoQIFOIaxjhDoFVfhCv6wRi553H0W+VQyDTYDVQGb9hYiLeA3qp5VkYdHFevelB/LHG0Tc871joOrt61C2QCgSjSDPzj5TXA7nOeX9Ewr3KYpWDa3gaYBxVElJNQ3luCiJKLH4utz4+I3CepZTuifP/50YF9BNVwuIDF8O1Omm2FyamSgtsJWNDFPdQxyzdrGxzsM36FxE7cJF++5rs/XqbTAAvf3N0ykqvc8VVETLUKCWm1+/jmHGiEiSvVfPEo0VDtKLpCVxQaCh+UiLtVOkQboA+062kgjPLUSwKnQ72SElrMEIA/x6/0w3hQ/bjO+SgoHJPcZ1RBad7GBg0WGY9efYw= +//+qAP//qgD//1VV//9VVf//VVX//1VV//+bD///qgD//6oA//9VVf//VVX//6oA//+qAP/pUUr/Rh8P//+qAP//VVX//1VV/ywdAP/flQD/nGgA/wAAAP/JSzv//1VV//9VVf//qgD/35UA/wAAAP8AAAD/nGgA//+qAP//VVX//1VV//+qAP//qgD/AAAA/wAAAP//qgD//6oA//9VVf//VVX//1VV//2pAP/9qQD//6oA//+qAP//VVX//1VV//+qAP//VVX//1VV//2pAP//qgD//1VV//9VVf//qgD//6oA//+qAP//VVX//1VV//9VVf//VVX//6oA//+qAA== eyJ0aW1lc3RhbXAiOjE0NTU2MjQwMzg4MjUsInByb2ZpbGVJZCI6ImIzYjE4MzQ1MzViZjRiNzU4ZTBjZGJmMGY4MjA2NTZlIiwicHJvZmlsZU5hbWUiOiIxMDExMTEiLCJzaWduYXR1cmVSZXF1aXJlZCI6dHJ1ZSwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzMzODA1NmM3NDQzM2E2M2RjNjc4ZGRhNzc4ZWRmYzdkNzM0ZWZlOWJlYTg3MTJkZDAzNjJjZTg5ZmFiYWM5In19fQ== fftAB8M68msGRpSMTIpQ/6jYSpTW8ysWyWiidu+xg5IATwiAbUpf6H76qNeRqt+WY7/eGwtUHBt2cxQDgVOfOwv/8H7ZBzZi/IDDx8qDYsfhbMcVYBtls5UNDWHAkv9NK+yGHxxZLIILaVJD3tJswnGN8c1NsdjcfaqgvGyX0y3/vIfk8dclPljDimh4tDaNGjwiLiEZnXw8f+7TRcbbfN2OXAq4nqKDEwC22o4wsDbgkl+vrP43Qtc3qyNQjZXVhn3JclXI3rmgwv1ob7iGotUVL3Q1KtXNHIbtQp3hjeLHpi1yw7OhRlnIGUNKNYbGsJlhWUy5jo+tiA3rTJ+O9hG0a0/JmrMGd6ZbKnP4QRTPLy6gCYG71XdKyanR+NIimvqs8EolOzavmy0nZ1iI2+nnGxNBcMnrV+mHaR+xynzqpUv4QXUnoGxP2g1A2RzT+IPH44le0dBf47EcGcAmrb4R06key0B3vav/BeHAWTM+BMhroTRfoy3mqn+CU0ZTjqT9m8pbbj0L1owFqhd2ulE+fA6uTCeHKyMunIWu7cGBAn17eMTu8VEAKmnEHionw8hjodHJL4+VhPV7+oPEFmAj7UGnh7PPPeSC/wp5lP51bQx6Fw+fy0B4nNnxxIeb8PfsMo4STIBIHO5zV8jj8vGunRGp32sT4MQZp1oLbDY= +////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////VQ== eyJ0aW1lc3RhbXAiOjE0NTU2MjAzODk3NDEsInByb2ZpbGVJZCI6ImIzYjE4MzQ1MzViZjRiNzU4ZTBjZGJmMGY4MjA2NTZlIiwicHJvZmlsZU5hbWUiOiIxMDExMTEiLCJzaWduYXR1cmVSZXF1aXJlZCI6dHJ1ZSwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlL2FhZjhiYmZhZGM1Y2Y5MGI3MTA3MWEzNDg3ZGIyZjNhNjZmNDYzZjdhMWZlMzk2N2NmZDE2MWVkNDMzMSJ9fX0= DI9OxgLNTgIbk5fSEFeVhRO2OWD9NKS0N4A/LpkzrRKicpTtW2+qtVD4IOsr8oGllVia/BpOxscwE/rpF9myrsAWX1SXhrlHQ5CnQ3WBomHOaM3SIKC9Y3e23h4Uzt8/kZV9zsvFwkAMnxzqmc8u11050Yw4wx4VdwUefcTpPk/3FQFQf5q23Ch4Rw9sI4CRybpr1xFOm83jZkFvUjjeQGNnxTBNY6ACsZaAe2ZLXZ4hMnPf0spxY1V6+XZ2YwPkFyF7sn8rj+rLCRw7VdGgQf0G0QqZChhPpYXqeqNDK1tKfeNXxSov8IfRQhxXvxVf+wGvpsnHDAzgLn5pNk4gDwURGksUb1NEkNnKwyU5rdF5+dID3T+i/LtceuwiBDrhTyYWj+mjjhkTxWp1h6uhdxW2HMJH49xBguSB446i7cKggNNbxtbD3hZjWISXjzREArNUgh/e3xVh5Gls7RSNXKWDFiLeI4z7L1362ZirKYbK+pdsGEwrDf6tdKGR02ZiGthpoFvCGWpS/KQvjj5xY47DqtkL8wldQT6RuUGEEn3jfuw+BI1u9jgS7YJJrGwEUjsf1tZqC94uh+2HqeLRvefGzwu07wuiJu9mZvrcCWVy9GV/96JzAZCcx9o9evmk0dGVdWO69m/hGfnXgacHT6V702JTl9ln9q1ymiHBMSw= +//+qAP//qgD//6oA//+qAP//qgD//6oA//+qAP//qgD//6oA//+qAP//qgD//6oA//+qAP//qgD//6oA//+qAP//qgD//6oA//+qAP//qgD//6oA//+qAP//qgD//6oA//+qAP//qgD//6oA//+qAP//qgD//6oA//+qAP//qgD//6oA//+qAP//qgD//6oA//+qAP//qgD//6oA//+qAP//qgD//6oA//+qAP//qgD//6oA//+qAP//qgD//6oA//+qAP//qgD//6oA//+qAP//qgD//6oA//+qAP//qgD//6oA//+qAP//qgD//6oA//+qAP//qgD//6oA//+qAA== eyJ0aW1lc3RhbXAiOjE0NTU1NzQwMjEwNjAsInByb2ZpbGVJZCI6ImIzYjE4MzQ1MzViZjRiNzU4ZTBjZGJmMGY4MjA2NTZlIiwicHJvZmlsZU5hbWUiOiIxMDExMTEiLCJzaWduYXR1cmVSZXF1aXJlZCI6dHJ1ZSwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzhmZmZkMmMxMWM3ODU0YTBjNDNjZWQ3ZDgzNDlmYWRjYzI4NTM2ODE1ODU4NGRlMTJkZGRiNTFjZWJjZGZiZGMifX19 dL+2BDKeKZJgTxSnepkkzQxT+qRumh6RybPqBVzg4Lt+b0J2Id+KozPE9/Uuqhx2sWicTCLZSaF+NcR6epUgjAHNhWOWQRYjxlxuu4Qpy5nzQw1Qzag10xX9RypZgu+c1qiM83HgvL7ytnm+lMJhGp21Y4yJ6QO/pa59orashCTzkUCNYFxplePsjJ9a/ezfT1uQjs0/1G4YyPq/ctR+FqrgHn5bPdeq4IxsNQDcF/xvZvD5mFwBGbtbLPTJ//CKGc+ddgqFietjEzpeT5fq3z2BCpPkE1383OeoaTpn14t40bQdjIGyPY1pXLb70KYXzK8K8i3/dBnSJivitoMzBgEzmNdPxrM21fmv3XuyRAo4TXwhRFf2ff5BroIWTuxGXVYjHQ2XzfRVpXtiqYEduIM527VWkoxPRB8DaOlDfOelxUrvmVnrxII4dwACHlUoJpD/Uvg/Fa9JzMGOsFmGf5AIQDDwHH76tjstpqB0IT8T1lEIdPFqglb5zOOLtamv2rv0L2m22k4eDW0/R6SldbHiOAXcei+kR9lwE2JygViIHFdnbQLT5Khd+MJEQ/mdmKrmzX8jvIGOUTRi5BvfTVe5/VXqZd5rN5WmvvKDiJTrGf6LjVyq1Oxo5ekjYWWEoGMx7myips0BU9JxzonFq9SyB59VZh3zw4QbkgKcadI= +/wCqqv8Aqqr/AKqq/wCqqv8Aqqr/AKqq/wCqqv8Aqqr/AKqq/wCqqv8Aqqr/AKqq/wCqqv8Aqqr/AKqq/wCqqv8Aqqr/AKqq/wCqqv8Aqqr/AKqq/wCqqv8Aqqr/AKqq/wCqqv8Aqqr/AKqq/wCqqv8Aqqr/AKqq/wCqqv8Aqqr/AKqq/wCqqv8Aqqr/AKqq/wCqqv8Aqqr/AKqq/wCqqv8Aqqr/AKqq/wCqqv8Aqqr/AKqq/wCqqv8Aqqr/AKqq/wCqqv8Aqqr/AKqq/wCqqv8Aqqr/AKqq/wCqqv8Aqqr/AKqq/wCqqv8Aqqr/AKqq/wCqqv8Aqqr/AKqq/wCqqg== eyJ0aW1lc3RhbXAiOjE0NTU2MTk5NDYyMTksInByb2ZpbGVJZCI6ImIzYjE4MzQ1MzViZjRiNzU4ZTBjZGJmMGY4MjA2NTZlIiwicHJvZmlsZU5hbWUiOiIxMDExMTEiLCJzaWduYXR1cmVSZXF1aXJlZCI6dHJ1ZSwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzI4MjRhZTlkMzRlMDY1NzBhODE3OGY3ZDFmZTdlZDA2NjJmYWViOWNjY2YyYzljNjIzYjNlNzg1MjNmNGQifX19 MKEFWlvoh1XW7DSu+hDOKxTPiHgc811u5LT/dh4TFDCC1CgR+xxAd0crnOE3GXYFmyDBfGsYrnNBvGpTOrh80LdJdpfJcAC/HpEmxf6N1F4AjDc08PGTqWmiWeXrg2p9qiIKO6iqCYlmoyVqE3buw8VhT7bzCALX1l82rVRXyCRJphVocvpeLVF659cwv797o86yAReqc7GWg5qvKeNE/D6RbTVgvrUhVbfJiG0O4bHyeduN/67yflGj691JqAuU9/SPRK8S3PGqV9qiDFJJF5OMxA35J+40zqPVhyByUEnSFAJXnIsCChvfk67UJyCvAbtCaSrTOjeuJilD+e6BZpHNuqk4iUbatn69dfSJCShMBNeYnuXandIHlI64SYbhefBB9aUBfJo22dy/74TMrqG3/gPx2ee4p2cm+WdQaYC+PI872dzyKDgQdHoatgWgXDNWZUT1pCHYvP+lmJQLTfw8o2H4hTay795/O8vfDd9qUHr7hgHBiEtQ3AMDXriMA2C0EBFYJ+ekA1df5Ql70KXwwOUPqm3dHEkWSdeROe2NitgrPyer8cYvHYXqqPQIOAIyOEYYIicTN2mv/aWS0sPtKYWw2XoINwCSe07rn++RG/j7wAMFnK3eT6pkgsTqtULjYtVHSWmx/kxaydB3VY0j8i+JXRUc+P67lb0YQ0E= +/6oAqv+qAKr/qgCq/6oAqv+qAKr/qgCq/6oAqv+qAKr/qgCq/6oAqv+qAKr/qgCq/6oAqv+qAKr/qgCq/6oAqv+qAKr/qgCq/6oAqv+qAKr/qgCq/6oAqv+qAKr/qgCq/6oAqv+qAKr/qgCq/6oAqv+qAKr/qgCq/6oAqv+qAKr/qgCq/6oAqv+qAKr/qgCq/6oAqv+qAKr/qgCq/6oAqv+qAKr/qgCq/6oAqv+qAKr/qgCq/6oAqv+qAKr/qgCq/6oAqv+qAKr/qgCq/6oAqv+qAKr/qgCq/6oAqv+qAKr/qgCq/6oAqv+qAKr/qgCq/6oAqv+qAKr/qgCq/6oAqg== eyJ0aW1lc3RhbXAiOjE0NTU1NzQwNTM1MzEsInByb2ZpbGVJZCI6ImIzYjE4MzQ1MzViZjRiNzU4ZTBjZGJmMGY4MjA2NTZlIiwicHJvZmlsZU5hbWUiOiIxMDExMTEiLCJzaWduYXR1cmVSZXF1aXJlZCI6dHJ1ZSwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzIwM2YyMzkwODI2NmU0ODY5OGFmOTNlY2NjYzA2MWU4ODhjNGRiN2NiZWY3NjZiMGYxZjI1ZGM1YTIxMDU0In19fQ== UooDjbdj9LynjgtLMHZXDUYToDqi6AtHHfyqwTa5GlfILJ9J4n055QLupEpxGNkZhMRvnOZiD9KUl96aH6XGfD+opfwpS/wg0uUkU+Lk/wHcb7iBEzdLEy2RqPUpue83ApppYPc/AZcPieHPue/agOgSgmKUZXGV4CAOLIEcFJnpcYZLkWnLF6tjJwMMxRN4D0zGkTY17212eqBmpb8kIqkruyl7cmm0tsMpLdxkAQD0K9ToGwcWqzKrwAeSn8kzqQPsRGKaMuf9NGXl6Q3Gi43FrMntOI/ghjcZuTdAoODmFnwU2jdUMgge/afw1hrTu4sYbB/Gn15viH5dspfBrkGeVu+JBPeNKk+H9UHvslgPIRCd75a41jNkcUAQ8lA/rmqgdSau+REPyPEKrLwjSmYnwOo3Hv14RoJ1opCuqHKTZdr1co3xPFV8EnDkNnuH7N4jdqK/EfOzKUWPLz5NQJU8DPGWeuuR2N4b87ZsT6VBgqRR3PSNu+2iYwAzHgB+j/aXqBLzk///TznB7yIGpbE4otTfzuHKAsu0P3QjKJhrHAMO3sbmNJ/Pun2ViIsGsrzKgmVW0B5uVsqPCTLbxRDa3rUvn7vIdt/7P1SDPkcNIK0mBhpyaIljRIDNcNYYb48msDgX7EWzFC5nBEh+SyNUblDYIY2Vn56SYCceRVs= +/wCqAP8AqgD/AKoA/wCqAP8AqgD/AKoA/wCqAP8AqgD/AKoA/wCqAP8AqgD/AKoA/wCqAP8AqgD/AKoA/wCqAP8AqgD/AKoA/wCqAP8AqgD/AKoA/wCqAP8AqgD/AKoA/wCqAP8AqgD/AKoA/wCqAP8AqgD/AKoA/wCqAP8AqgD/AKoA/wCqAP8AqgD/AKoA/wCqAP8AqgD/AKoA/wCqAP8AqgD/AKoA/wCqAP8AqgD/AKoA/wCqAP8AqgD/AKoA/wCqAP8AqgD/AKoA/wCqAP8AqgD/AKoA/wCqAP8AqgD/AKoA/wCqAP8AqgD/AKoA/wCqAP8AqgD/AKoA/wCqAA== eyJ0aW1lc3RhbXAiOjE0NTU1NzQwODcwMDAsInByb2ZpbGVJZCI6ImIzYjE4MzQ1MzViZjRiNzU4ZTBjZGJmMGY4MjA2NTZlIiwicHJvZmlsZU5hbWUiOiIxMDExMTEiLCJzaWduYXR1cmVSZXF1aXJlZCI6dHJ1ZSwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzgyNDdmN2Y2NmJiYzRkNWVlYzk4ZDQ1NGY2NzYyMzQ2NjhjNjEyYTRhMDgxMjM1NDRkMzY4MTI2ZDQ2MGVkIn19fQ== Q7emyZMI6EBWDakoKpR5av2kTKswmFL5jjsDuJyDGE5W8jAtD23rDlEEzewsYsKYtwzwknITYlG16D+njW/a8v1vm7dmcyys7TofEUepDIWkgcgf0gRZmOH1B8Owd7sIOCsCWoeXkYQWzUPAo1kZrxN3PwkgWSDOkPy1+QMAt5ubEuZLALjRJuxNTjPXe/vDzCK3vTZee3TYmWSNOh28+IMTcvOD9twk1y1InWqVvRN7iL5wRhVOGgoVtr6b/dxznCcI7IP/UhBHH5XZvPzzU1Ejm0xQenGbn8JuynYhZSgcn9BqCx41CfFHm6AzsHTE0VvTUl5/bSTSi5rxad52dljqt7BrB4U2eM5Co8KLTQsLgYd9VIXPFdvepc8/v3opv7U27JxafpAATH72XmiefoAIQUrqizi2AymYnE8mA/bFefwAgiwQUZ51iHAOHcctZDSI9oliZBfcbOD8ABSftP+MQzPZsSczo0oBTSJydBZIhcA6WZ26A9A7Hr/rXRdaRcE9qNFZ/7swjGlMlwZ76CKdOYfYIWBBqcytuYA35bdq/oOiiM73B81bAOC/v8DR8mBRd4AklCVb2GABQth/wuCrgw4xdc83Xu7ndbOS7Op6b42GIciqxXBaPb+OKjOz6KjtUAM5VsYdhrkHTHrGoIGEZYMibm9uzsOrxAVvatI= +/1VVVf9VVVX/VVVV/1VVVf9VVVX/VVVV/1VVVf9VVVX/VVVV/1VVVf9VVVX/VVVV/1VVVf9VVVX/VVVV/1VVVf9VVVX/VVVV/1VVVf9VVVX/VVVV/1VVVf9VVVX/VVVV/1VVVf9VVVX/VVVV/1VVVf9VVVX/VVVV/1VVVf9VVVX/VVVV/1VVVf9VVVX/VVVV/1VVVf9VVVX/VVVV/1VVVf9VVVX/VVVV/1VVVf9VVVX/VVVV/1VVVf9VVVX/VVVV/1VVVf9VVVX/VVVV/1VVVf9VVVX/VVVV/1VVVf9VVVX/VVVV/1VVVf9VVVX/VVVV/1VVVf9VVVX/VVVV/1VVVQ== eyJ0aW1lc3RhbXAiOjE0NTU1NzQxMTk0MzMsInByb2ZpbGVJZCI6ImIzYjE4MzQ1MzViZjRiNzU4ZTBjZGJmMGY4MjA2NTZlIiwicHJvZmlsZU5hbWUiOiIxMDExMTEiLCJzaWduYXR1cmVSZXF1aXJlZCI6dHJ1ZSwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzZlNzJkMzE0NzczMmQ5NzFkZWZhZTIzMWIzOGQ5NDI0MTRiMDU3YTcxNTFjNTNjNWZkNjI5NmEzYjllZGEwYWIifX19 ro/ZKHt7278yhCr+CFTcPp/q6wAUlef//85k2DzkfRaZqy0CtGgwisDs2U4pVKvQ2pfXvitzWgbJvD0bLeQ12xWi4c1Fc29LCArosVJoFmrJDHz7N2MlstHT+ynQROb9d2aiFA6uOXfLjPKb1noUZ/YQoZjqcPIvD5oFZtD5DHV5O4hYz0IvgHbIjDqjz6ITsTcKiBlbxNg2loTFxSlW1ZfnNCO+kcAmeyB5NFY3j0e+/AqVANiNoiC3OKsECM/yEx/acf+vKWcT8mQn4wRoIGtxfEU7ZjNtgdh73NvXXBygW+K9AiJ242g8Y06Xxuk8kaNEGmT6H/mM7nbwjZmQQXpi/Pao2gYqyeIofeCPfr8RsGXoDX3nXDAw8/LyhTCHgx+sp6IQYSfGcSMJtoNeTJ0liIFxqn1V9/zKmzOZAPzR6qrQPOjoRFljLAlv7rfzotaEqh/1ldd40GdS8tstczn7f29OQerNDaqvbDb00Gy0STdUr1bVyCDptA54XKjT9WFv7QpBikEculxqSppAXPxD2Fb/ZmphbZx8WEGfG6bVFhf6fQdDAUXlcv8BxjElNPwlolF86M2KJd5VquLluhrCjwID7OK/pffNultAVH+Lxw4QOAXmJqjUrA1KHgyG1S0Cwj/f4E2hdxZJBvkfVtq9qPkd9nignhEoTCTOHf0= +/1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X//w== eyJ0aW1lc3RhbXAiOjE0NTU2MTk4MzQ4MTUsInByb2ZpbGVJZCI6ImIzYjE4MzQ1MzViZjRiNzU4ZTBjZGJmMGY4MjA2NTZlIiwicHJvZmlsZU5hbWUiOiIxMDExMTEiLCJzaWduYXR1cmVSZXF1aXJlZCI6dHJ1ZSwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzc2M2Y3N2U0MzRjZDI5ZDFmNjU0MjcwNjExNTQyMTIyNGY3M2EwOTFmZGRjZDRlZTZkOTZkNDhmZWViNDhlIn19fQ== UlRS+/U23NAcOTZ9dZH29bk8sOfBDFvHdR3XS3H2QZuHVWVPRgXpioLf0lHIlapA0uFiiBouX8YO4Lj5q3/QCsSZAKNHoy9KG2DT8kCJOUOt8vNpfpz+OPi2QvKYnZRhzx/2oMMy/pLMyKLHfvyG3uzWlJ9FWMab6ZH08QmuND+O2SVPkBh6H6ZZquyUD0BPCo6tWtCOjWb4AII5EQgSni7bHl1Woc2Y5oFX43b9CiDCVZgEn3mRQrzFSjGVxDbJjME4kz2AbFU0noU5IeMz4vUW+pJqdwb2CqbE4GtGAfbLOlW3WxHOrRKsQ1tuh5u16tU1+A+fYFcZvWo2te5hY1wJIND1KRkFwU+Kk/2kMqbxAgDzDXHFX7h0cykpCntob6MhC/suopht1VV3GmwqCeYh6Gh96O2yGA+4TKm3HePGpzg76bvAEw/UdzwWZ9jXlOY5ivqaU3lyUlsYCkgK4rJSpxvkYaLcEjL3G1xLa8vNeqxobCqln2JHh9I/bT1LMQNlLfGUw90eQW4AbDi8qA8qj4kp4FCh7/oRCpAP9YxvUmzdr/maKyOeQpGumEtNxUBcSDigl7E6jBgsJHXGxVZr7VJ+CrwZOSvLbkGyPrIFh8qXyO6EZmIzFi7/V8F6EIQJDE7UEK1pvhjEru7bFKqLmyOtlZ8U6FOaWmUXEr0= +//+qAP//qgD//6oA//+qAP//qgD//6oA//+qAP//qgD//6oA//+qAP//qgD//2ZE//9dTf//oAr//6kB//+qAP//qgD//6oA//9gSv//V1P//1ZU//9dTf//pgX//6oA//+qAP//VVX//1VV//+qAP//qgD//1VV//9fS///oQn//6oA//+qAP//qgD//6oA//+qAP//pwP//6oA//+qAP//qgD//6oA//+iCP//YEr//2BK//+lBf//qQH//6oA//+qAP//nQ3//2JI//9mRP//WVH//11N//+jB///pwP//6oA//9VVf//VVX//6oA//+qAP//VVX//1tP//+gCg== eyJ0aW1lc3RhbXAiOjE0NTU2MjQwMDYzNDIsInByb2ZpbGVJZCI6ImIzYjE4MzQ1MzViZjRiNzU4ZTBjZGJmMGY4MjA2NTZlIiwicHJvZmlsZU5hbWUiOiIxMDExMTEiLCJzaWduYXR1cmVSZXF1aXJlZCI6dHJ1ZSwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzQzZTMyZjc2NTYxNDFhYjE5ZDI5OTliOGI4M2M5YzFiZDcyZmE5OWQ2MjQ2ZTQ0MTNhZDk4ZjU0MTE1MTFmIn19fQ== NUSKXEyeDkkFYtgYDi4ycUN5hFzRk/HAQX7leJy8aUQjfnHVowbZL/gf0JOg2Bsn/8keW0jTJRsTlJt25qsd83iuw7bdiD/qHxzWAUwcrl7YGHZAlfp0dRhowzlKyOPlDlWBgLNmo717Ly4ExXgzVlH4LzGTGk6DssRP+8BV3CVRSkBthnEMaGZVA3eNp1FHKEItInlbHUN9B7jgYvjcQQBc7Apam2/WIvnRdDsCDMViuFTK5tfgPyvqsZKgXeZ7gSlEDAOYkqihWzcdlKYblaGBl6qklGHQHAsUzErQ0bgFfElnOsRK6l6c2pCrtDJyRTq9bqF1/P/ARKrmZQZZYOXslN2QagXEVE8pnzGJBxsg/F6NemS5At7jiSoR7BVpE8kTKs7J6SJIlkB8oqkJW6IGaByzMkHX+E+fHvKhqcjQ+p+dofL/AnGdpWbylg9EYRdHJX99A1YC2KitSpw0mqxphZIrnH6CjRamjYp7+kQ/Aig6PydJynrFsX54ELQwbFNlgiGGFCMgocC9m96cYWhg5/iHQJFl1xS3YlWXVBoemwhNcZYCADA8hR48hTen30ZQh6gOLg/FXdoew83/pJG9eN6/lnGXwmmz9Bx35QURGatC0cJApv1aIl70oJ6ZuhMnj/YdVwOl1PNpRqwC5paJRbyK4wPZ6mKN1Y+tr7c= +/wAAqv8AAKr/AACq/wAAqv8AAKr/AACq/wAAqv8AAKr/AACq/wAAqv8AAKr/AACq/wAAqv8AAKr/AACq/wAAqv8AAKr/AACq/wAAqv8AAKr/AACq/wAAqv8AAKr/AACq/wAAqv8AAKr/AACq/wAAqv8AAKr/AACq/wAAqv8AAKr/AACq/wAAqv8AAKr/AACq/wAAqv8AAKr/AACq/wAAqv8AAKr/AACq/wAAqv8AAKr/AACq/wAAqv8AAKr/AACq/wAAqv8AAKr/AACq/wAAqv8AAKr/AACq/wAAqv8AAKr/AACq/wAAqv8AAKr/AACq/wAAqv8AAKr/AACq/wAAqg== eyJ0aW1lc3RhbXAiOjE0NTU2MTk4MDIxMzQsInByb2ZpbGVJZCI6ImIzYjE4MzQ1MzViZjRiNzU4ZTBjZGJmMGY4MjA2NTZlIiwicHJvZmlsZU5hbWUiOiIxMDExMTEiLCJzaWduYXR1cmVSZXF1aXJlZCI6dHJ1ZSwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzliODNmOWU2NjkwYTBkZmIyNmQ0NGEwYTE2MDQ4Yzg2YjQ0YjY2MDU5OGEzOTQxNGJhODU1ZGMxZTEyMjdhIn19fQ== JwRyGXqic6NmXo150rdDqdh1QD2g1d/eZr3g8WCUIfN1BUd2dL7WJugphnzlkLkyLZwIxmdNFyLTZzD3gPa/1EpDSD5t2kblaHeo6doy3GnVZujT0hSoxvfvcfLbFAc257Uu6X2oFiFDc0bUFaW2ZSEkLGyCmD/WhknBP2e6Rc/DNKWcaWUOVRkbOd+bOjxG35WlHXmFV1piUy7rXUHf61sjMT0AZL90Mm861huCqd5E5PSInq44pcWKDnRJV4nlExa4lOe26VM/3h+rVTXzHv7dvfsm7u1IuI+v5lolnSc8ZNFIdin7b78D+np0YOVA17oSUiKszBktSAOFxdtVPh/W4WvNng0dui4kmTZ5xhNOVXKOaEuhHUa6e5hYBw3UeZ2JH2/Ihbq6OozAI6O5cafuKJAwJQZMpAKZQ07aeMSU5X3wvMj8WtFVVMrT9PYHsotfAeoFkoIjReqBhVpe32NtA+nlJRWjG9HLnHY1C1wppG2W4UlRunKBUbpOwv/HY5bfvj0GajhJHhHQcdPtco/yyfP7AekjgqQjOWa8xHhm7WGd6NYrW+hy4e38qqyFbIjHgq3q1fhtmmns0UoFbycNGGLwDq6iTvrNl63/eNLG3KmHqwTTVw1VEQ0RUYPlmKrhwGxeOruUrCDRLqVGJNIbCpHuSWtivNNZBp9a/10= +//+qAP//W0///2BK//9pQv//V1P//1VV//9fS///pwP//6oA//9pQf//lxP//6cE//+qAP//pQX//19L//+mBP//qgD//2ZE//9aUP//VVX//2ZE//9eTP//YUn//6oA//+qAP//VVX//6oA//+qAP//oAr//6UF//9oQv//qgD//6oA//9VVf//qgD//1lR//9VVf//nwv//2FJ//+qAP//qgD//2ZE//+qAP//ZkT//1VV//+fC///WVH//6oA//+qAP//Xkz//6EJ//+lBf//qgD//6oA//9mRP//qgD//6oA//9VVf//VVX//1VV//9ZUf//VVX//11N//+qAA== eyJ0aW1lc3RhbXAiOjE0NTU2MjQyNjk4MzAsInByb2ZpbGVJZCI6ImIzYjE4MzQ1MzViZjRiNzU4ZTBjZGJmMGY4MjA2NTZlIiwicHJvZmlsZU5hbWUiOiIxMDExMTEiLCJzaWduYXR1cmVSZXF1aXJlZCI6dHJ1ZSwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzNmOTZlYmY5NmU1ZTk2ZWMxODVlZjUzYTk0YjY0ZjA3YmJmZDI0ZTJkMzc1ODk3ZDMzYWJiNWU5NTE4NTQifX19 Nt8fbJ2sxAFGQLpXHmybKXuhq9RtWYlbol9DnXlANbaZHXgOa+DHvVPWLD802Op07po1YPPecDzVt9PkItG6scQOXqmzVX2hUopDBg2qfQ8Xcaz6393dk3NAN6Wfw6O3+sDfeMyh2pWb7A9ePBDKTkxgmsAZgXVu8ziw8APGk32c4rnfC2yOpEd1aQlZz+QfHJO692HpmIYcffsCjLRK0dAlpb/pCtl2UQu17K+oOCqAtxAFygLFGTBhhvsgh8aqF6irQfwXMHF5xOsp1+5GIv1roZXbGRnVTnWEvZOL4zHYskhrYt12VMBoTVVKiCvjCIGjJANJoJMGJX0tckn42n4Uq8WY5LHRhJw97vtZt8wbIiIVvPN30CGxP209LJ6TnBcZl0b0XIDsAZYZe2k8P5HwLJApyBlYjdKn/uHmv5YPJdkud4kF8zB5rjpw7qSOI+mp0PqDSnV9ZvESM8rRe0Vsxi9WC8tN7QJmVloMTvoFtlDZ8NXH2MHXQRXY2ZlKTrXrxl6r12kStC38/v2c3+15XWn7v/vPRPFeKiOhcr8ZW5Hfa9mFRZNbqEW6nkjOL7Lxq6vX9xPehcv8oZxgm3cQG0sfjoeCa/3XD0l+oJeVw6R7+xi0BNfk/QS2rCNGVjISc1jP4cC4GrcjZedCoOk9mFuuEkVCTUh5iXNfmtY= +/1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV//9VVf//VVX//1VV/w== eyJ0aW1lc3RhbXAiOjE0NTU2MTk5NzkxMTMsInByb2ZpbGVJZCI6ImIzYjE4MzQ1MzViZjRiNzU4ZTBjZGJmMGY4MjA2NTZlIiwicHJvZmlsZU5hbWUiOiIxMDExMTEiLCJzaWduYXR1cmVSZXF1aXJlZCI6dHJ1ZSwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlL2QwM2Q4MWY0YjBiNTJlMzNmNTY3YjkwZTdjY2Q5YWM4MDI3MTA1MDEwMjUzZGE3OWMyMGRjN2I3NmE5YzFjOSJ9fX0= DLPx8WKUXvXXQ337Youf1tNWMJRTcX4zfN0g79wyfT+HlYCItiDBJykfmwIktJ3+kEWft3tc/vK7TSgY0yV9gkUIoTKkpvYYTXxvj+7R8bwPMaU8YtKR4T6q9sn+bkCXEQ13ub6bjVlj9mdXVhei/rhTxS6fWlNL/5y4CsAjS8JhRFMaUZhwpVnURfRx6A9p5cslslfAU5pyBN+ROpBxryPfn2Cf8ocZ12n/YamYQgcmiZCP6IJbFC50ToaARvDgNLE6EkWGGxcFzRZ/9f3rwLQFnHe2miFGatH5mHC+iON6u/EfgO+8nCjK0A05waD79xDCpvnmO3OJDUN/gTnvr+nTwwA0hFENkVxE1iQvZueN9Ubpnbn7B0pkqsFEmNQGBDPpwFRWhnACR7vtmmkKcPhOiqv5Wyj4tEkcRPyDGxl/9DafG9wUbEzDUs9WV++N62BfEfs5LIckUZLqVJHM/6tjsqIXMz042iL+azDkhVyX6Q+17MOJX6w5kA5HYGYhP2jNa0pCzdqxVhWMfYtbXLVx2l4KtYGnpC6x1XjzT3GIxoH7iLNbOAmovVihtFDVDqDx4mAtfNBnC931PqrQJx3TO14PtLo9K8vU8l1HwKDPVa+ZushDi9uPFVCHpWxXt+VloQ/zq/u6Sek+Y96evt9dQ1X1AjNQGgUpLbqQ3yQ= +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////w== eyJ0aW1lc3RhbXAiOjE0NTU2MjU0OTU3MzEsInByb2ZpbGVJZCI6ImIzYjE4MzQ1MzViZjRiNzU4ZTBjZGJmMGY4MjA2NTZlIiwicHJvZmlsZU5hbWUiOiIxMDExMTEiLCJzaWduYXR1cmVSZXF1aXJlZCI6dHJ1ZSwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlL2ZhOWFkZjEwODI0YjY4YTRmYWVjZDIyZDZhNjk5MzcxYmZiN2E2YzRjZDAyYmRjZWZjM2Y3Zjc5NGE5NmVkNSJ9fX0= ulOm30Pu9RXkQ2FnK3HpjZ0QSJlOCvycHaYR1wgT5jIHPeOKY9MOxuF+fCYcdLWaJBFSO7JfPLdA1ZhM1BcMB2YyQab4ozP70epNhVYogmLfi4bGLszfL3j3ivpoJbVGA62T3/PGuGfZCUFdQtYPrCOAzynzhLrXiMhpjpJWZDCfir9jzgoVB6PfubktGNnyOvkMfqxtinSO1y7a21hwMW6hc2254gP9qk2E/k0QMC8iZCleEdscHKKVt+ZjHxjigeqwfGvm7uarMDeKRTy3qshXYR8cYAJT5ErjSxcCbBzstFybH89azjm7Qhyn1gkUM6O7pi8f7FCH7z4B36kJnOcD+htAh9JY2rMdkXwC3f7UQEEq4xYTrcjfPA/hOBT6LU7gwULSUflMjBn0D/BjBgVZMuWfUFL/MU8tHZ4Mnzmbc+XkwCoSpFgr8XaJfhblsOoN7+bLVgR0TstyLthmbABdylCSaFcY1rzTgSQUYOG/K+aR58Ywu+DqEMa39oUzh9+euDTj01s8VufbupTnPcpba0+p4UnoT7TihHmGkWOm8hl4MNHOHa3J/B3xMLH9Mx469fwh7uXnaDTFjAb/m9AGUMQzLzk3FRyCc9GKAq+DqbOSxpvwvxMUFAn7caCM2P0rWSN3XEZSOF/WoANrmdImrF/Zsdfy0EvmGZzq66s= +/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/Vf9V/1X/VQ== eyJ0aW1lc3RhbXAiOjE0NTU2MjU1MjgyMzcsInByb2ZpbGVJZCI6ImIzYjE4MzQ1MzViZjRiNzU4ZTBjZGJmMGY4MjA2NTZlIiwicHJvZmlsZU5hbWUiOiIxMDExMTEiLCJzaWduYXR1cmVSZXF1aXJlZCI6dHJ1ZSwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzZjOTY3N2I4Mjk1MGZhYTk3YzljNDc0MzI4MzE2YzRkMzg3YzA2N2ZjMDFlODU5ZDVhY2JiNzI2ZWY4MTMifX19 JNkwpvuB8GmSxxJSGM7pPKTEq29ph06GoHB5mtxcVG1bPBGbgB0P04K3A4Qp+4NCNA06OijT8l/e16cjB6F8XbGtmUXfJ6Nzyut/tgQyAyz5iiQmayI/sC8H2oDV/4TkgarEcDVUBv9FItg+TCaYNw7DSI6Z9q7YEHxVF4vkjBDVKRzyP63gZszPMqVzk9kKKu1759OPAn96qQ+QJOaczHkJObPgPOMu5KusYi3Mpknjr+KEpezGrPVeH5OJd+9+vqfLkM9dfBhG+R42d1vHdBcDuDeO0rWiZ0NqEt1iJyVUCetRQt5kNDYZEL8z/6eB7oBCOCUHMPIWncFu5ZW1hlGhjBaefKD9oCo8kJda9TDRKCSGP721N4s5p8m+3X3EVlWO6OAhCpIjYN2GrkELeJCAujjfx2XLCEzJ8IItJl88wCNCYVSSCzcRSB2xvtgBCidX6vfkDz+NMT3qWAm/E8n0fq146HrwT8Eomtyjyywaz6I5PRVC4LckIiamlZn2yy2PhdlMnLD6UqtVtTB+fwcog9iIV5JF7Na97+ea7b+JlSWp+T6++9+CCqLf0pLShF9vM62HSjfvEdAUYo1JOIywDH0gReeZ2yLDDez2ujj27IMDmZflj/nfTgDUjMxlGgUaVkqshLO9KTDpFIft3Jbnke8s1g4fw2iO4XeMso0= +//9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V////Vf///1X///9V/w== eyJ0aW1lc3RhbXAiOjE0NTU2MjU1NjA2ODYsInByb2ZpbGVJZCI6ImIzYjE4MzQ1MzViZjRiNzU4ZTBjZGJmMGY4MjA2NTZlIiwicHJvZmlsZU5hbWUiOiIxMDExMTEiLCJzaWduYXR1cmVSZXF1aXJlZCI6dHJ1ZSwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlL2E0NzU3ZjZjYjhkMjQzYmNmOWJiYWVkY2QyMWRiZTIxOGI1MmQ5NzE4MTUzZTdiYzI4OGMxOTgzY2E2MGIyIn19fQ== VLtkg/+LW7HV6XrAclb3/Ki+AfSY9oCggKOiTNlKKXPdF/nPKEB5QSAakc1L9HCyX3r8UGMmWjaXgWS6/N/4KTBaleGbaIweBkB9wjcu1CioNe8lBVx9KRnEKxKNz3teSuruBTZCiliNHqwWOFz+JOhzp8v4qNEPNLr3UEvRmcs0joAfIuOscwtkHHN42lqgOLtqIZLUpXAdvSdDQnz8fFp/u8NsZ9zTsz3V3KxPiGUfeqUC+4oKoMsyjznoWlFgkC2HrGCfOuZymveAJ7avBIYj8tidFnwghe4IBVOg97MDBM+yp/GFbgxQIEEXtcBkgqLsqlzueHFgoiwIKTeX7Jz3f83LlA1Bpei8Hk1DHeH/9y+qmQV7e32rsCZPvuX7YaScoCFojlA3UbY+WOALtEQw6Ps7ormthsdlvDC93p0+L6Rx2pz3AW8VfPlqFoycOXj5wqnLJNTZ6XPjUrYO98kafZ16qpUAaFlUAcvyVNqSxsRY8ITWZF7ii2HjNRTTmPAC3U1vPEXwbR7Bb/c29lGx2tmGvaPnmc6N3kiOxI9RrzZMyQww9ILrhlaMWIVbZKjwXUtCgSgj/wtei9JJCk3IHl3xi/iZy99D/m8UqZgPclildjvzdjpdCAPw1NQGdBdv+F7NvUR2uW4/zNATTFhb9Q5YvdZAwQkrHbqXT7k= +/6qqqv+qqqr/qqqq/6qqqv+qqqr/qqqq/6qqqv+qqqr/qqqq/6qqqv+qqqr/qqqq/6qqqv+qqqr/qqqq/6qqqv+qqqr/qqqq/6qqqv+qqqr/qqqq/6qqqv+qqqr/qqqq/6qqqv+qqqr/qqqq/6qqqv+qqqr/qqqq/6qqqv+qqqr/qqqq/6qqqv+qqqr/qqqq/6qqqv+qqqr/qqqq/6qqqv+qqqr/qqqq/6qqqv+qqqr/qqqq/6qqqv+qqqr/qqqq/6qqqv+qqqr/qqqq/6qqqv+qqqr/qqqq/6qqqv+qqqr/qqqq/6qqqv+qqqr/qqqq/6qqqv+qqqr/qqqq/6qqqg== eyJ0aW1lc3RhbXAiOjE0NTU2MjU1OTM5NjIsInByb2ZpbGVJZCI6ImIzYjE4MzQ1MzViZjRiNzU4ZTBjZGJmMGY4MjA2NTZlIiwicHJvZmlsZU5hbWUiOiIxMDExMTEiLCJzaWduYXR1cmVSZXF1aXJlZCI6dHJ1ZSwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzc4Y2I3ZmMyMDhiMzM4NTUwNGE4MTQ0MjA0NDI4ZmRjZDYzMjRiZWIzMWNhMmNlODZjYzQyNGI5NjNkODVjIn19fQ== R/wZUZRC1dishRdM9a2SSxxW3oYa0XSb/MxHbQpEUA791HxyqjaKLDu0wFX2r2a8ZTeVjzXpNzkg3+PkrA11o8h7lt86MTD1pi/rQqj/WRuoqf2LP+ypbssKV+LU15cYez2cj3QQVcJDXgWEnfSLNuBv6NG8BDUpUAjTWldvu99NCJHUoD0jNMHxY/fu4k5vCgOjaBaKgkjVk2bmUhegusmtMwco+3pYx+y8+gUW8ptx5SnePG+dOwTqLyBFiOt2AQ+gSvbU/jP9aAXgxOwz/b1pMaBWtzVhFU865NHlIdSpIHg/sh3uNah3a7gTgtTvxPQv1OzM/KtqYKiamsrRzAQMzRcs4A7Tp0GakLuxEaz401IwvQ7UGVYLFzGUVLB2MyqtPgifiqQSQxZpiqj9sM5QadhsUw00nfX7mTdW46U0MtNIbby1rLrvgQKoj08zt6LJlhI3yjyawy4iZkgF4oc+PCNwZc93GIbVL9LJaGkXk3RVA+JpGwfMJrGVbL7hl8ibbAcUv7uCEWdkAgZCd6w75jEE4tlhDSPDD4rXbn+FeTZRg2n/PGKtnoTZRzbniiFaNoSAHDZSVRG39xvBDFvtmL3SPaKhzKaifiYrgNn453WtR3kymqdAtPf1GN9d1VltGZ/+vMPwqPJb6thcrlcU64UGHbg1olRkiyZHvY8= +/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAA== eyJ0aW1lc3RhbXAiOjE0NTU2MjU2MjY0MjYsInByb2ZpbGVJZCI6ImIzYjE4MzQ1MzViZjRiNzU4ZTBjZGJmMGY4MjA2NTZlIiwicHJvZmlsZU5hbWUiOiIxMDExMTEiLCJzaWduYXR1cmVSZXF1aXJlZCI6dHJ1ZSwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzVmYjJjMjljMTYxNjc4ZTM4NGVlZDE4ZjAxNWNjYTY5M2UwNTI5ZGMwZjA2NWVlNGJjNzQyZmZjYTFmOCJ9fX0= v8DS9JW5cw3M4oJVdVIyFoy6pguwGca0pNx+cbatI9eGDyVQ39HcPenJ0JOEA6tCysdO4D5WThP4KIioeeBWs3I9TerSKTUw8WC3NZkqspKnAJvSc/h8vh6U8cjgK6Wr4gkgROGv51e04wbXXxFWbuIDCzQ5U1Ykbrw5JGMokLkHN5gEpzJkIzFaxdpzf0+hu5rCM5Qlu1Awo+ThQxBcvR9XLePLqpy8DLpJUQGBN6C+LfaHRkOJNO0NQtXF07I42hi5//+lNclAYy25zR9t6fgtBj8JbYFVOSNYuBQhGuq7TfdkGD8mWtahWXbTITG3Me6IhxjpKwvHqwk3khqSnswD6yNxqMX+BwgE0jREYQyTg9UgPoqMRnIl9dm7HPO8NcUzztjGbWWBoAUK++uaTEnINPc36W1JwuaVYsIBuCKCcrMvQRv3QcOruIimspa4nce5rB3bIVUCMtFUEtL0ZPQBuCKCt75hRqGwl43o3TWUliJAZHbUk4J10aAHE5+q0fKboemxiORPwIq7wXHGeEiq3E/7G5IP6Ss/l5NsFrxFbwGMwy5tCGkMRxyJSQh/lBH2xHKE9h73Ej2hfBQJPTuCOYGlo+MG90solaFlJi33n1xeNa+bZeNYV5Ac2gF2wFwClDJOKSXyx5x+z8qZWSIowg9NOLP4Ihph91c0CWc= +/6oAAP+qAAD/qgAA/6oAAP+qAAD/qgAA/6oAAP+qAAD/qgAA/6oAAP+qAAD/qgAA/6oAAP+qAAD/qgAA/6oAAP+qAAD/qgAA/6oAAP+qAAD/qgAA/6oAAP+qAAD/qgAA/6oAAP+qAAD/qgAA/6oAAP+qAAD/qgAA/6oAAP+qAAD/qgAA/6oAAP+qAAD/qgAA/6oAAP+qAAD/qgAA/6oAAP+qAAD/qgAA/6oAAP+qAAD/qgAA/6oAAP+qAAD/qgAA/6oAAP+qAAD/qgAA/6oAAP+qAAD/qgAA/6oAAP+qAAD/qgAA/6oAAP+qAAD/qgAA/6oAAP+qAAD/qgAA/6oAAA== eyJ0aW1lc3RhbXAiOjE0NTU2MjU2NTk5MTksInByb2ZpbGVJZCI6ImIzYjE4MzQ1MzViZjRiNzU4ZTBjZGJmMGY4MjA2NTZlIiwicHJvZmlsZU5hbWUiOiIxMDExMTEiLCJzaWduYXR1cmVSZXF1aXJlZCI6dHJ1ZSwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzVmZDU1MjgxMTdkMWU0YzMxZmUyMzNhZDMxZTI0Y2MxMTAyNjg5MjAyZmQxODg1Y2Y3ODNjNTNhZGI4NmVlIn19fQ== GExnpC5aai5Agm8sJBfK7mR5Gi0xDUOX68Dutw5RoWvGbyPcCKdsTNiEj70a5d/VGxsPtwyIkUpqfLqahbf6y3Wc87O3Wd4ZSyyQ2dBv8TELPysqZdhAoj/4vEH+FCM87QT/98biF7oIT7RksNbI3IOm/sufkKa4JM4IqBKBt6IOznrKJdu9oGftWcFRmclKRbRrOk4woBobgBV8JgU10LKnKIYwWwhT7exjXK+XOrM1bt/VUmTER1H8k7FYw3wmwmKFR6OdmCRqsUYCh+rxdx/Ecs2lqPUPQpFNiz4TBkYtEp1YJr/MK69BDXQymB6tgbRaSgVcqFXV7beH3eoX+rrF4cwdaJxcNUf3mKdkIEga+zJlZnSjQRFR0VT6tUiTuaDytlmkYtZnFmzzvM7wuIUkpqHGmyFtuDmK210+9UTDfXl19P2cIA5KzV7FZu6ytjejijrwjrsBq80NhIG7oPcletddjxVXNFbZPBnzOkbVlYDLiXHu7TD/CX3sLBh34ZBS/4OdDTQqtQgskyf9DomYl7dpxcY3WwqA354I65RrpQi/FfhcnHwpkiEyak0Ll9NDrXqVFsAUl6fomVpz24aG70LzFakq3OL0LrzM3qQS2+RGyDEaCGwnk2IPKrTgLQMDaV3t8GTCtZpQkwlCkAfjUn0Dziuc5S1unnGnbxc= diff --git a/velocity/src/main/resources/heads/colors/aqua.png b/velocity/src/main/resources/heads/colors/aqua.png new file mode 100644 index 0000000000000000000000000000000000000000..0ff54e87a4eddaa8304725aa41c88503206c377b GIT binary patch literal 118 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1SGw4HSYi^&H|6fVg?3oVGw3ym^DWND9B#o z>Fdh=fQet!Op<@)Dl4FnsHcl#2*>qgp1=R+GfF2U9bjZz&cbkMK2!3s=t;+c3K%?H L{an^LB{Ts5=pY)+ literal 0 HcmV?d00001 diff --git a/velocity/src/main/resources/heads/colors/black.png b/velocity/src/main/resources/heads/colors/black.png new file mode 100644 index 0000000000000000000000000000000000000000..03c10683f48220318be10915c58fce42fe3275eb GIT binary patch literal 109 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1SGw4HSYi^&H|6fVg?3oVGw3ym^DWND9B#o z>Fdh=fQet!fYEFI)K@?u9#0p?5RU7~2@-NZ-U$ZAn&iw{AdA7%)z4*}Q$iB}+%OeS literal 0 HcmV?d00001 diff --git a/velocity/src/main/resources/heads/colors/blue.png b/velocity/src/main/resources/heads/colors/blue.png new file mode 100644 index 0000000000000000000000000000000000000000..f80a645bbacf299a156ec5209102f069cf49d0db GIT binary patch literal 118 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1SGw4HSYi^&H|6fVg?3oVGw3ym^DWND9B#o z>Fdh=fQet!RBCVU3TL2@sHcl#2*>qg9^Sv_8+Z*2A22irGcoupGw$Ry{_Y7>z~JfX K=d#Wzp$P!lff-f+ literal 0 HcmV?d00001 diff --git a/velocity/src/main/resources/heads/colors/dark_aqua.png b/velocity/src/main/resources/heads/colors/dark_aqua.png new file mode 100644 index 0000000000000000000000000000000000000000..19665f44ffdef9ca45558a5ecea3bd0f321a5442 GIT binary patch literal 118 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1SGw4HSYi^&H|6fVg?3oVGw3ym^DWND9B#o z>Fdh=fQet!QszgrQx#B1)YHW=gyVX0O2pPKM(Kp41B`6TSr~Xz8B;o3MfU*}FnGH9 KxvXX&2 literal 0 HcmV?d00001 diff --git a/velocity/src/main/resources/heads/colors/dark_blue.png b/velocity/src/main/resources/heads/colors/dark_blue.png new file mode 100644 index 0000000000000000000000000000000000000000..2cae6fc3e54e08e45a4f5bd5fafc110e6ecedbdb GIT binary patch literal 118 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1SGw4HSYi^&H|6fVg?3oVGw3ym^DWND9B#o z>Fdh=fQet!P}aTm#}lBCsHcl#2*>s0l!S<*4ZH@14;Y$*nHV~6G3;9Ge(E1k0fVQj KpUXO@geCwG(i`Uh literal 0 HcmV?d00001 diff --git a/velocity/src/main/resources/heads/colors/dark_gray.png b/velocity/src/main/resources/heads/colors/dark_gray.png new file mode 100644 index 0000000000000000000000000000000000000000..a0bdd6d72186585d18cb62f7a4813941dde46879 GIT binary patch literal 118 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1SGw4HSYi^&H|6fVg?3oVGw3ym^DWND9B#o z>Fdh=fQet!#G>%qjb}h1QBN1g5RU7~JiNTA2iRsv6f_*vWMnww#K=FV-~KyL0fVQj KpUXO@geCw02pWd~ literal 0 HcmV?d00001 diff --git a/velocity/src/main/resources/heads/colors/dark_green.png b/velocity/src/main/resources/heads/colors/dark_green.png new file mode 100644 index 0000000000000000000000000000000000000000..a746aa0fd45f4adf3742346ade8691802b119aba GIT binary patch literal 116 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1SGw4HSYi^&H|6fVg?3oVGw3ym^DWND9B#o z>Fdh=fQet!h_Pbj_Y9zru&0Y-2*>s0l!&7Zq6UTrJgS@wT`df|T3?nh1Em=}UHx3v IIVCg!0H*2~+5i9m literal 0 HcmV?d00001 diff --git a/velocity/src/main/resources/heads/colors/dark_purple.png b/velocity/src/main/resources/heads/colors/dark_purple.png new file mode 100644 index 0000000000000000000000000000000000000000..bacb5b1b44195ea27cfb19f2b48c3cd67b2202dc GIT binary patch literal 118 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1SGw4HSYi^&H|6fVg?3oVGw3ym^DWND9B#o z>Fdh=fQet!*w8)uTPsjV)YHW=gyVX0%+U>9jM5272N>CwvoP@SGp5wpO#*6WVDNPH Kb6Mw<&;$U~Vi>vr literal 0 HcmV?d00001 diff --git a/velocity/src/main/resources/heads/colors/dark_red.png b/velocity/src/main/resources/heads/colors/dark_red.png new file mode 100644 index 0000000000000000000000000000000000000000..2b34412b501537116fc15c845e44a42daf6616ec GIT binary patch literal 114 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1SGw4HSYi^&H|6fVg?3oVGw3ym^DWND9B#o z>Fdh=fQet!Sm-HF`*)y_pr?yt2*>s0n4=A{28KMEoDAK147>hnZBqhDGI+ZBxvXFdh=fQet!gm0gZZ#_^**we)^gyVYhpZ#5o+6hSs5?(?K0cRL@`hUvh1WGe_y85}S Ib4q9e0IFRXMF0Q* literal 0 HcmV?d00001 diff --git a/velocity/src/main/resources/heads/colors/gray.png b/velocity/src/main/resources/heads/colors/gray.png new file mode 100644 index 0000000000000000000000000000000000000000..532308ef007fd030991c8aaf659f8e7537f1b34b GIT binary patch literal 118 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1SGw4HSYi^&H|6fVg?3oVGw3ym^DWND9B#o z>Fdh=fQet!M5$+r;vArmsHcl#2*>s0n5|i&%w`9UG%)k{aWYKoX5zcoD;Ez`z~JfX K=d#Wzp$P!XTNxDq literal 0 HcmV?d00001 diff --git a/velocity/src/main/resources/heads/colors/green.png b/velocity/src/main/resources/heads/colors/green.png new file mode 100644 index 0000000000000000000000000000000000000000..baf2e9c9e61712a715e0a61179d619804477e750 GIT binary patch literal 118 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1SGw4HSYi^&H|6fVg?3oVGw3ym^DWND9B#o z>Fdh=fQet!)T-@qM>kMN)YHW=gyVWL&tLgd4ZH@14;Y$*nHc=fGVWaX=F4NC0tQc4 KKbLh*2~7YOYaCbr literal 0 HcmV?d00001 diff --git a/velocity/src/main/resources/heads/colors/light_purple.png b/velocity/src/main/resources/heads/colors/light_purple.png new file mode 100644 index 0000000000000000000000000000000000000000..2e17ad7e0950cebcf794ac77075720f8efb56851 GIT binary patch literal 118 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1SGw4HSYi^&H|6fVg?3oVGw3ym^DWND9B#o z>Fdh=fQet!oNe~5X?;K;QBN1g5RU7~f8>A7XOvDzI>5-boQ2`CCsXqGH}dy@3K%?H L{an^LB{Ts55ZD{G literal 0 HcmV?d00001 diff --git a/velocity/src/main/resources/heads/colors/red.png b/velocity/src/main/resources/heads/colors/red.png new file mode 100644 index 0000000000000000000000000000000000000000..cf064359d9ce4c234bc2571af20dd515b4a78e49 GIT binary patch literal 118 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1SGw4HSYi^&H|6fVg?3oVGw3ym^DWND9B#o z>Fdh=fQet!O#67kSzpMz~JfX K=d#Wzp$Py8)f!3w literal 0 HcmV?d00001 diff --git a/velocity/src/main/resources/heads/colors/white.png b/velocity/src/main/resources/heads/colors/white.png new file mode 100644 index 0000000000000000000000000000000000000000..d1f00aa1fd843880d88f2e9d9f4225fddd5b0f5c GIT binary patch literal 118 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1SGw4HSYi^&H|6fVg?3oVGw3ym^DWND9B#o z>Fdh=fQet!Ks0KmY&RGn*Ya(!k8)$H@?Og_$?;bw(Xf0fVQj KpUXO@geCw7UK<<$ literal 0 HcmV?d00001 diff --git a/velocity/src/main/resources/heads/colors/yellow.png b/velocity/src/main/resources/heads/colors/yellow.png new file mode 100644 index 0000000000000000000000000000000000000000..6e4c8d39b2fe8d94c31eb9603f295421c6d26ba8 GIT binary patch literal 118 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1SGw4HSYi^&H|6fVg?3oVGw3ym^DWND9B#o z>Fdh=fQet!T>K-~wi=+2sHcl#2*>s0KmYBgFiIyR9bjZz&cbl{GgGoc(hQIu22WQ% Jmvv4FO#uD~8j%11 literal 0 HcmV?d00001 diff --git a/velocity/src/main/resources/heads/default/balance.png b/velocity/src/main/resources/heads/default/balance.png new file mode 100644 index 0000000000000000000000000000000000000000..5978994c86ef3d436dc794ab0ba4f2173f2d0531 GIT binary patch literal 293 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1SGw4HSYi^wj^(N7l!{JxM1({$v_d#0*}aI z1_o|n5N2eUHAey{$X?><>&pIsNq~o$$)=z#3@Eh8)5S4_k5cS})}l+8N3{vvKy!A-Tt=47lZ z==wRUZnb86&7LWdms89C#1tE?5#sx9F;Qe1^PCgaE?j;`_NvW#prf?CU*zgN^Pc4) z+Ya!V@ZWcs{Chgz+rYYoG5vL`PpUPn-F<-nmaFdh=fJuN`fvJ6({3f7Ky{C&~2*>r@zSV+<0z_QS@8(^gG;<+?)d9t(18LeFo{C!p zrgPm}+En5o`q6?#?{M#fR*A5;>1ofGoM71TW2M~9y*b%WPt5=PyNFp~$%MY@W%Cv? zSH$=$3I?St+joaYb|UA=eLn-HF0>V&?8Ic(%RJ3Yz?ttx?b0a6OY$+68yIfsXsw^9 S_F^&6DGZ*jelF{r5}E+#cT4#I literal 0 HcmV?d00001 diff --git a/velocity/src/main/resources/heads/default/ping.png b/velocity/src/main/resources/heads/default/ping.png new file mode 100644 index 0000000000000000000000000000000000000000..f02b4394e5bb02f120828f9654ff4c908a3b602f GIT binary patch literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1SGw4HSYi^&H|6fVg?3oVGw3ym^DWND9B#o z>Fdh=fJuN`+sJA6x2r%QO-~oc5RU7~fA)7VY9}NmD8wa|Y8Fdh=fJuN`MeY7iyL6zClc$Sg2*>r(UPrD220VwSY}DV?-y?rhHY!7GMW5ISLt!0; znTMKNd-i-46koK|hGEK)E1EaLHhua1yTn`6OY8Tp9}Lp5(j3Po8z=xxWAJqKb6Mw< G&;$TjvoMSR literal 0 HcmV?d00001 diff --git a/velocity/src/main/resources/heads/default/rank.png b/velocity/src/main/resources/heads/default/rank.png new file mode 100644 index 0000000000000000000000000000000000000000..d071983f9fcb491c2b87bcb096ce1db5de5bc1e7 GIT binary patch literal 228 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1SGw4HSYi^&H|6fVg?3oVGw3ym^DWND9B#o z>Fdh=fJuN`$BJ3;g#u8h+0(@_gyVYdS#PdG1`MtjXKyj#4Z1VwhStHED`tObHul^o zx?RO}l1S2AjjkVV)6FfDZhR_#sXm))xn}ehXP!@A=ZW1p#^M?+6zstC#_W#!8oo8( zf6S@syZq>S2dC#BqndN6Tn5(4^7)Ikcle#CYC0tKt7nD>L-q@<>&pIsNq~o0yYA34E1*!Hr;B3<$93D&2fdgAB^oX+{v1C` zuFrV1pfYo!Zrc)v$Sah24qp7_meI!8M#A~peC#^CAd=d#Wzp$PzQWMfVM literal 0 HcmV?d00001 diff --git a/velocity/src/main/resources/version.properties b/velocity/src/main/resources/version.properties new file mode 100644 index 00000000..245c2ca7 --- /dev/null +++ b/velocity/src/main/resources/version.properties @@ -0,0 +1 @@ +build=${project.findProperty("ciBuildNumber") ?: "unknown"} From 22fc3a9403c6acf46d7f95f48c1952cf2c4286af Mon Sep 17 00:00:00 2001 From: Brent P Date: Mon, 2 Jan 2023 20:53:23 -0500 Subject: [PATCH 02/22] Remove Un-ported Example --- example/velocity/build.gradle | 5 - .../bungeetablistplus/demo/ColorUtil.java | 77 ------ .../bungeetablistplus/demo/DemoPlugin.java | 224 ------------------ .../velocity/src/main/resources/bungee.yml | 5 - example/velocity/src/main/resources/icon.png | Bin 228 -> 0 bytes settings.gradle | 2 - 6 files changed, 313 deletions(-) delete mode 100644 example/velocity/build.gradle delete mode 100644 example/velocity/src/main/java/de/codecrafter47/bungeetablistplus/demo/ColorUtil.java delete mode 100644 example/velocity/src/main/java/de/codecrafter47/bungeetablistplus/demo/DemoPlugin.java delete mode 100644 example/velocity/src/main/resources/bungee.yml delete mode 100644 example/velocity/src/main/resources/icon.png diff --git a/example/velocity/build.gradle b/example/velocity/build.gradle deleted file mode 100644 index 38597948..00000000 --- a/example/velocity/build.gradle +++ /dev/null @@ -1,5 +0,0 @@ - -dependencies { - compileOnly project(':bungeetablistplus-api-bungee') - compileOnly "net.md-5:bungeecord-api:${rootProject.ext.bungeeVersion}" -} diff --git a/example/velocity/src/main/java/de/codecrafter47/bungeetablistplus/demo/ColorUtil.java b/example/velocity/src/main/java/de/codecrafter47/bungeetablistplus/demo/ColorUtil.java deleted file mode 100644 index 6d32596a..00000000 --- a/example/velocity/src/main/java/de/codecrafter47/bungeetablistplus/demo/ColorUtil.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (C) 2020 Florian Stober - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package de.codecrafter47.bungeetablistplus.demo; - -import com.google.common.collect.BiMap; -import com.google.common.collect.ImmutableBiMap; -import net.md_5.bungee.api.ChatColor; - -import java.awt.*; - -public class ColorUtil { - private static BiMap colors = ImmutableBiMap.builder() - .put(new Color(0x000000), ChatColor.BLACK) - .put(new Color(0x0000aa), ChatColor.DARK_BLUE) - .put(new Color(0x00aa00), ChatColor.DARK_GREEN) - .put(new Color(0x00aaaa), ChatColor.DARK_AQUA) - .put(new Color(0xaa0000), ChatColor.DARK_RED) - .put(new Color(0xaa00aa), ChatColor.DARK_PURPLE) - .put(new Color(0xffaa00), ChatColor.GOLD) - .put(new Color(0xaaaaaa), ChatColor.GRAY) - .put(new Color(0x555555), ChatColor.DARK_GRAY) - .put(new Color(0x5555ff), ChatColor.BLUE) - .put(new Color(0x55ff55), ChatColor.GREEN) - .put(new Color(0x55ffff), ChatColor.AQUA) - .put(new Color(0xff5555), ChatColor.RED) - .put(new Color(0xff55ff), ChatColor.LIGHT_PURPLE) - .put(new Color(0xffff55), ChatColor.YELLOW) - .put(new Color(0xffffff), ChatColor.WHITE) - .build(); - - public static Color getAWTColor(ChatColor chatColor) { - return colors.inverse().get(chatColor); - } - - public static ChatColor getSimilarChatColor(Color color) { - if (color.getAlpha() < 128) { - return null; - } - Color bestColor = null; - double bestDist = Double.POSITIVE_INFINITY; - for (Color c : colors.keySet()) { - double dist; - if ((dist = ColourDistance(c, color)) < bestDist) { - bestColor = c; - bestDist = dist; - } - } - return colors.get(bestColor); - } - - // from http://stackoverflow.com/questions/2103368/color-logic-algorithm - public static double ColourDistance(Color c1, Color c2) { - double rmean = (c1.getRed() + c2.getRed()) / 2; - int r = c1.getRed() - c2.getRed(); - int g = c1.getGreen() - c2.getGreen(); - int b = c1.getBlue() - c2.getBlue(); - double weightR = 2 + rmean / 256; - double weightG = 4.0; - double weightB = 2 + (255 - rmean) / 256; - return Math.sqrt(weightR * r * r + weightG * g * g + weightB * b * b); - } -} diff --git a/example/velocity/src/main/java/de/codecrafter47/bungeetablistplus/demo/DemoPlugin.java b/example/velocity/src/main/java/de/codecrafter47/bungeetablistplus/demo/DemoPlugin.java deleted file mode 100644 index 8ff44326..00000000 --- a/example/velocity/src/main/java/de/codecrafter47/bungeetablistplus/demo/DemoPlugin.java +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright (C) 2020 Florian Stober - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package de.codecrafter47.bungeetablistplus.demo; - -import codecrafter47.bungeetablistplus.api.bungee.BungeeTabListPlusAPI; -import codecrafter47.bungeetablistplus.api.bungee.Variable; -import de.codecrafter47.taboverlay.AbstractPlayerTabOverlayProvider; -import de.codecrafter47.taboverlay.Icon; -import de.codecrafter47.taboverlay.TabView; -import de.codecrafter47.taboverlay.handler.*; -import lombok.NonNull; -import net.md_5.bungee.api.ChatColor; -import net.md_5.bungee.api.CommandSender; -import net.md_5.bungee.api.connection.ProxiedPlayer; -import net.md_5.bungee.api.plugin.Command; -import net.md_5.bungee.api.plugin.Plugin; -import net.md_5.bungee.api.scheduler.ScheduledTask; - -import javax.annotation.Nonnull; -import javax.imageio.ImageIO; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.IOException; -import java.util.Calendar; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; - -import static de.codecrafter47.bungeetablistplus.demo.ColorUtil.getAWTColor; -import static de.codecrafter47.bungeetablistplus.demo.ColorUtil.getSimilarChatColor; -import static java.lang.Math.*; - -public class DemoPlugin extends Plugin { - - private CompletableFuture customIcon; - - @Override - public void onLoad() { - - // variables should be registered in onLoad to avoid warnings when BTLP loads your config files - BungeeTabListPlusAPI.registerVariable(this, new Variable("uppercase_name") { - @Override - public String getReplacement(ProxiedPlayer player) { - return player.getName().toUpperCase(); - } - }); - - // Create our icon. - try { - // read the image file - BufferedImage image = ImageIO.read(getResourceAsStream("icon.png")); - // call getIconFromImage, this gives use a future that will hold the icon once completed - customIcon = BungeeTabListPlusAPI.getIconFromImage(image); - } catch (IOException ex) { - getLogger().log(Level.SEVERE, "Failed to load icon.", ex); - } - } - - @Override - public void onEnable() { - - // register the /tabdemo command. - // It will display the custom tab list to players - getProxy().getPluginManager().registerCommand(this, new Command("tabdemo") { - @Override - public void execute(CommandSender sender, String[] args) { - if (sender instanceof ProxiedPlayer) { - // get the tab view for the player - TabView tabView = BungeeTabListPlusAPI.getTabViewForPlayer((ProxiedPlayer) sender); - // create a new instance of our CustomTabOverlayProvider and add it to the tab view - tabView.getTabOverlayProviders().addProvider(new CustomTabOverlayProvider(tabView)); - } - } - }); - } - - /** - * Our custom tab overlay provider. - */ - private class CustomTabOverlayProvider extends AbstractPlayerTabOverlayProvider { - - // In this field we will store the handle to access the header and footer - private HeaderAndFooterHandle headerFooterHandle; - // In the contentHandle field we store the handle to modify the content of the tab list - private RectangularTabOverlay contentHandle; - // The updateTask field stores the task hande for the update task - private ScheduledTask updateTask; - - public CustomTabOverlayProvider(@Nonnull @NonNull TabView tabView) { - // set the name to "custom-taboverlay-example", you can use any name, but it needs to be unique - // set the priority to 1000. The plugin will display the tab overlay with the highest priority. So using a - // high number here is good. It ensures our custom tab overlay is displayed and not a tab list provided by - // a config file. The priority should be between 0 and 10000. - super(tabView, "custom-taboverlay-example", 1000); - } - - @Override - protected void onAttach() { - // This informs us that the tab overlay provider has been added to a tab view. - // We can access the tab view using super.getTabView(). After this has been called BungeeTabListPlus - // expects shouldActivate() to return correct values. - - // In this example we don't need to do anything here. However if you create a more complex tab overlay - // provider, that isn't always active, but should activate depending on some condition, you might want to - // do some stuff here. - } - - @Override - protected void onActivate(TabOverlayHandler handler) { - // Our tab overlay provider has been activated - - // Configure that tab overlay of the player and store the handles so we can modify it later - // IMPORTANT: Do not store a copy of handler anywhere!!! - - // for the header and footer we can choose either custom or pass through - // we choose custom - headerFooterHandle = handler.enterHeaderAndFooterOperationMode(HeaderAndFooterOperationMode.CUSTOM); - - // for the content we can choose between rectangular, simple and pass through. - // we choose rectangular - contentHandle = handler.enterContentOperationMode(ContentOperationMode.RECTANGULAR); - - // We set the header text - headerFooterHandle.setHeader("&6Super &eAwesome &cClock"); - - // now we set the size of the content to 1 column, 19 rows - contentHandle.setSize(new RectangularTabOverlay.Dimension(1, 19)); - - // we schedule a task to update the content every second - // we store the task handle in a field so we can cancel it later - updateTask = getProxy().getScheduler().schedule(DemoPlugin.this, this::updateContent, 0, 1, TimeUnit.SECONDS); - } - - @Override - protected void onDeactivate() { - // Our tab overlay provider has been deactivated - // We should now stop modifying the tab list - - // We cancel the update task - updateTask.cancel(); - } - - @Override - protected void onDetach() { - // Our tab overlay provider has been detached from the tab view. - // This means it is no longer used at all. - - // You can use this method to free any resources you might have acquired. - - // In this example we don't have to do anything here. - } - - @Override - protected boolean shouldActivate() { - // This method is used by the plugin to check whether this tab overlay provider should be active. - - // In our example we want our custom tab overlay provider to always be used so we always return true. - return true; - } - - // This method renders an analogue clock to the tab list. - private void updateContent() { - // First we draw to a buffered image - BufferedImage image = renderClock(); - - // now we convert the image to text lines and set the appropriate slot of the tab list - for (int row = 0; row < 19; row++) { - String text = ""; - for (int x = 0; x < 19; x++) { - ChatColor chatColor = getSimilarChatColor(new Color(image.getRGB(x, row))); - text += chatColor == null ? ' ' : chatColor.toString() + '█'; - } - - // get our custom icon. If it's not ready yet use the default alex icon - Icon icon = customIcon.getNow(Icon.DEFAULT_ALEX); - - // set the icon, text and ping of the slot - contentHandle.setSlot(0, row, icon, text, 0); - } - } - } - - // This method renders an analogue clock to a buffered image. - @Nonnull - private BufferedImage renderClock() { - BufferedImage image = new BufferedImage(19, 19, BufferedImage.TYPE_INT_ARGB); - Graphics2D g = image.createGraphics(); - // background - g.setColor(getAWTColor(ChatColor.DARK_GRAY)); - g.fillRect(0, 0, 19, 19); - // circle - g.setColor(getAWTColor(ChatColor.GRAY)); - for (int x = 0; x < 19; x++) - for (int y = 0; y < 19; y++) - if ((8.5 - x) * (8.5 - x) + (8.5 - y) * (8.5 - y) < 81) - g.drawRect(x, y, 1, 1); - // arrows - int hour = Calendar.getInstance().get(Calendar.HOUR); - g.setColor(getAWTColor(ChatColor.DARK_RED)); - g.drawLine(9, 9, (int) round(9 + 8 * sin(hour / 6.0 * PI)), (int) round(9 - 8 * cos(hour / 6.0 * PI))); - int minute = Calendar.getInstance().get(Calendar.MINUTE); - g.setColor(getAWTColor(ChatColor.RED)); - g.drawLine(9, 9, (int) round(9 + 8 * sin(minute / 30.0 * PI)), (int) round(9 - 8 * cos(minute / 30.0 * PI))); - int second = Calendar.getInstance().get(Calendar.SECOND); - g.setColor(getAWTColor(ChatColor.GOLD)); - g.drawLine(9, 9, (int) round(9 + 9 * sin(second / 30.0 * PI)), (int) round(9 - 9 * cos(second / 30.0 * PI))); - return image; - } -} diff --git a/example/velocity/src/main/resources/bungee.yml b/example/velocity/src/main/resources/bungee.yml deleted file mode 100644 index a9be36fa..00000000 --- a/example/velocity/src/main/resources/bungee.yml +++ /dev/null @@ -1,5 +0,0 @@ -name: BungeeTabListPlus-API-Demo -version: ${version} -author: CodeCrafter47 -main: de.codecrafter47.bungeetablistplus.demo.DemoPlugin -depends: [BungeeTabListPlus] \ No newline at end of file diff --git a/example/velocity/src/main/resources/icon.png b/example/velocity/src/main/resources/icon.png deleted file mode 100644 index 8fcd3a4bd6c2d6b5e02c13bd480c2c0614c482dd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 228 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1SGw4HSYi^&H|6fVg?3oVGw3ym^DWND9B#o z>Fdh=fSrj=TQ6sl=31aov!{z=2*>r(K0~fU4g#+K*BLkOUUq|JwS(Wozx@wbbC_?< zY;Ey9!Lz1S!L0J>W6|@BFI;{J2pKo5`4w~h`@Y?NPp)lq?%@?|IGUsL)Xw)xyTOqP zQAUObI!q6?=Gqz7- Date: Mon, 2 Jan 2023 21:01:06 -0500 Subject: [PATCH 03/22] Remove Waterfall-Compat dependency --- velocity/build.gradle | 1 - .../handler/LowMemoryTabOverlayHandlerImpl.java | 7 ++----- .../bungeetablistplus/handler/TabOverlayHandlerImpl.java | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/velocity/build.gradle b/velocity/build.gradle index 6b17a0eb..c24b86a3 100644 --- a/velocity/build.gradle +++ b/velocity/build.gradle @@ -17,7 +17,6 @@ dependencies { implementation "de.codecrafter47.data:velocity:${rootProject.ext.dataApiVersion}" implementation project(':bungeetablistplus-common') implementation project(':bungeetablistplus-api-velocity') - implementation project(':waterfall-compat') implementation 'it.unimi.dsi:fastutil:8.5.11' implementation "de.codecrafter47.taboverlay:taboverlaycommon-config:1.0-SNAPSHOT" implementation 'org.yaml:snakeyaml:1.33' diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LowMemoryTabOverlayHandlerImpl.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LowMemoryTabOverlayHandlerImpl.java index 28cbdac7..1498419f 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LowMemoryTabOverlayHandlerImpl.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LowMemoryTabOverlayHandlerImpl.java @@ -20,7 +20,6 @@ import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.proxy.protocol.packet.Team; -import de.codecrafter47.bungeetablistplus.bungee.compat.WaterfallCompat; import java.util.UUID; import java.util.concurrent.Executor; @@ -46,10 +45,8 @@ public PacketListenerResult onTeamPacket(Team packet) { @Override public void onServerSwitch(boolean is13OrLater) { - if (!WaterfallCompat.isDisableEntityMetadataRewrite()) { - for (String team : serverTeams.keySet()) { - sendPacket(new Team(team)); - } + for (String team : serverTeams.keySet()) { + sendPacket(new Team(team)); } super.onServerSwitch(is13OrLater); } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/TabOverlayHandlerImpl.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/TabOverlayHandlerImpl.java index bb89ff99..a602842f 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/TabOverlayHandlerImpl.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/TabOverlayHandlerImpl.java @@ -25,7 +25,6 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; -import de.codecrafter47.bungeetablistplus.bungee.compat.WaterfallCompat; import lombok.SneakyThrows; import java.util.UUID; @@ -60,8 +59,7 @@ protected boolean isExperimentalTabCompleteFixForTabSize80() { @SneakyThrows @Override protected boolean isUsingAltRespawn() { - return WaterfallCompat.isDisableEntityMetadataRewrite() - || player.getProtocolVersion().getProtocol() >= 735 + return player.getProtocolVersion().getProtocol() >= 735 && ProtocolVersion.isSupported(736); } From 501394d701cbd9861677e845656b768f1cc272e6 Mon Sep 17 00:00:00 2001 From: Brent P Date: Mon, 2 Jan 2023 21:37:44 -0500 Subject: [PATCH 04/22] Move ChatUtil and add in unicode.txt --- .../command/CommandBungeeTabListPlus.java | 2 +- .../command/CommandDebug.java | 2 +- .../command/CommandFakePlayers.java | 2 +- .../command/CommandHide.java | 2 +- .../bungeetablistplus/util/ColorParser.java | 1 + .../util/{ => chat}/ChatUtil.java | 8 +- .../codecrafter47/util/chat/unicode.txt | 65536 ++++++++++++++++ 7 files changed, 65547 insertions(+), 6 deletions(-) rename velocity/src/main/java/codecrafter47/bungeetablistplus/util/{ => chat}/ChatUtil.java (99%) create mode 100644 velocity/src/main/resources/codecrafter47/util/chat/unicode.txt diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandBungeeTabListPlus.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandBungeeTabListPlus.java index 622f7ecd..92b5a508 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandBungeeTabListPlus.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandBungeeTabListPlus.java @@ -22,7 +22,7 @@ import codecrafter47.bungeetablistplus.common.BTLPDataKeys; import codecrafter47.bungeetablistplus.updater.UpdateChecker; import codecrafter47.bungeetablistplus.util.VelocityPlugin; -import codecrafter47.bungeetablistplus.util.ChatUtil; +import codecrafter47.bungeetablistplus.util.chat.ChatUtil; import com.google.common.base.Joiner; import com.mojang.brigadier.Command; import com.mojang.brigadier.arguments.StringArgumentType; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandDebug.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandDebug.java index 3935e903..38896627 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandDebug.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandDebug.java @@ -22,7 +22,7 @@ import codecrafter47.bungeetablistplus.player.VelocityPlayer; import codecrafter47.bungeetablistplus.util.ProxyServer; import codecrafter47.bungeetablistplus.util.ReflectionUtil; -import codecrafter47.bungeetablistplus.util.ChatUtil; +import codecrafter47.bungeetablistplus.util.chat.ChatUtil; import com.google.common.base.Joiner; import com.mojang.brigadier.Command; import com.mojang.brigadier.context.CommandContext; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandFakePlayers.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandFakePlayers.java index 8d8888ee..d998bc58 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandFakePlayers.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandFakePlayers.java @@ -21,7 +21,7 @@ import codecrafter47.bungeetablistplus.api.velocity.tablist.FakePlayer; import codecrafter47.bungeetablistplus.player.FakePlayerManagerImpl; import codecrafter47.bungeetablistplus.util.ProxyServer; -import codecrafter47.bungeetablistplus.util.ChatUtil; +import codecrafter47.bungeetablistplus.util.chat.ChatUtil; import com.google.common.base.Joiner; import com.mojang.brigadier.Command; import com.mojang.brigadier.context.CommandContext; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandHide.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandHide.java index ee250a8e..611af384 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandHide.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandHide.java @@ -20,7 +20,7 @@ import codecrafter47.bungeetablistplus.BungeeTabListPlus; import codecrafter47.bungeetablistplus.data.BTLPVelocityDataKeys; import codecrafter47.bungeetablistplus.player.VelocityPlayer; -import codecrafter47.bungeetablistplus.util.ChatUtil; +import codecrafter47.bungeetablistplus.util.chat.ChatUtil; import com.mojang.brigadier.Command; import com.mojang.brigadier.context.CommandContext; import com.velocitypowered.api.command.CommandSource; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ColorParser.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ColorParser.java index 577aa10d..20caefca 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ColorParser.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ColorParser.java @@ -16,6 +16,7 @@ */ package codecrafter47.bungeetablistplus.util; +import codecrafter47.bungeetablistplus.util.chat.ChatUtil; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextDecoration; import net.kyori.adventure.text.format.TextFormat; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ChatUtil.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/chat/ChatUtil.java similarity index 99% rename from velocity/src/main/java/codecrafter47/bungeetablistplus/util/ChatUtil.java rename to velocity/src/main/java/codecrafter47/bungeetablistplus/util/chat/ChatUtil.java index 6b4ca6e9..22ff7ad6 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ChatUtil.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/chat/ChatUtil.java @@ -1,4 +1,4 @@ -package codecrafter47.bungeetablistplus.util; +package codecrafter47.bungeetablistplus.util.chat; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; @@ -13,7 +13,11 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.util.*; +import java.util.Deque; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; diff --git a/velocity/src/main/resources/codecrafter47/util/chat/unicode.txt b/velocity/src/main/resources/codecrafter47/util/chat/unicode.txt new file mode 100644 index 00000000..71893dfe --- /dev/null +++ b/velocity/src/main/resources/codecrafter47/util/chat/unicode.txt @@ -0,0 +1,65536 @@ +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +68 +38 +22 +23 +23 +23 +68 +53 +36 +23 +23 +52 +22 +52 +22 +22 +38 +22 +22 +22 +22 +22 +22 +22 +22 +52 +52 +38 +22 +21 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +38 +23 +22 +22 +22 +22 +22 +22 +23 +22 +22 +23 +22 +23 +22 +22 +23 +22 +70 +22 +19 +22 +23 +36 +22 +22 +22 +22 +22 +21 +22 +22 +38 +21 +22 +38 +23 +22 +22 +22 +22 +22 +22 +21 +22 +22 +23 +22 +22 +22 +53 +68 +36 +23 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +68 +23 +23 +22 +23 +68 +22 +37 +7 +38 +22 +22 +15 +7 +22 +36 +23 +38 +38 +53 +38 +22 +52 +36 +36 +38 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +23 +22 +22 +22 +22 +22 +38 +38 +38 +38 +6 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +23 +22 +22 +22 +22 +22 +22 +22 +22 +23 +22 +22 +22 +22 +22 +38 +38 +38 +38 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +38 +22 +22 +22 +22 +22 +23 +23 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +6 +23 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +7 +6 +22 +22 +38 +38 +22 +22 +38 +38 +38 +38 +22 +38 +23 +22 +6 +6 +22 +22 +38 +22 +38 +22 +38 +22 +21 +6 +38 +22 +22 +6 +6 +22 +22 +22 +22 +22 +22 +22 +22 +22 +23 +23 +23 +23 +22 +22 +6 +6 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +23 +21 +23 +21 +23 +21 +22 +22 +22 +22 +22 +22 +22 +22 +23 +23 +22 +23 +22 +23 +23 +22 +23 +22 +22 +22 +22 +22 +22 +37 +6 +6 +22 +22 +22 +22 +22 +23 +23 +6 +6 +22 +22 +22 +22 +22 +22 +23 +21 +23 +22 +22 +21 +38 +22 +22 +38 +22 +23 +6 +22 +22 +22 +22 +22 +22 +23 +23 +23 +22 +22 +22 +21 +21 +23 +21 +23 +23 +23 +22 +22 +23 +23 +22 +22 +22 +22 +22 +22 +22 +22 +22 +21 +22 +51 +36 +21 +68 +23 +23 +23 +23 +23 +7 +23 +23 +23 +22 +22 +38 +38 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +23 +23 +23 +23 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +23 +23 +23 +22 +22 +22 +22 +22 +22 +22 +22 +23 +23 +22 +22 +6 +6 +22 +22 +6 +6 +22 +22 +6 +6 +22 +22 +6 +6 +22 +22 +6 +6 +22 +22 +6 +6 +22 +22 +22 +22 +23 +21 +22 +22 +22 +22 +22 +23 +22 +38 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +23 +22 +37 +23 +22 +21 +6 +6 +6 +6 +6 +22 +23 +23 +22 +22 +22 +22 +6 +22 +22 +6 +23 +23 +23 +23 +22 +22 +23 +23 +22 +22 +22 +22 +22 +22 +23 +23 +22 +22 +23 +22 +22 +23 +22 +23 +23 +22 +22 +22 +22 +22 +22 +22 +38 +53 +38 +22 +22 +54 +23 +23 +23 +23 +23 +23 +22 +22 +23 +23 +23 +22 +22 +23 +22 +22 +22 +22 +22 +22 +22 +21 +21 +21 +22 +21 +22 +23 +22 +22 +22 +23 +22 +23 +23 +23 +22 +22 +22 +22 +22 +22 +22 +22 +22 +23 +38 +22 +22 +22 +23 +22 +22 +23 +23 +23 +23 +23 +23 +23 +39 +22 +23 +22 +21 +23 +38 +38 +37 +38 +21 +23 +21 +21 +21 +36 +22 +52 +52 +52 +37 +37 +22 +22 +37 +37 +23 +23 +37 +37 +68 +37 +37 +37 +68 +37 +37 +37 +36 +36 +37 +37 +21 +21 +21 +21 +22 +52 +37 +70 +22 +23 +6 +22 +21 +36 +21 +21 +21 +21 +21 +21 +21 +21 +21 +20 +21 +22 +22 +21 +21 +53 +53 +37 +36 +22 +22 +22 +52 +36 +36 +36 +36 +22 +22 +22 +22 +22 +22 +22 +22 +7 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +23 +22 +22 +22 +22 +22 +22 +22 +22 +23 +22 +22 +22 +22 +22 +22 +7 +7 +22 +22 +7 +22 +7 +22 +22 +22 +6 +22 +22 +7 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +23 +22 +15 +22 +22 +22 +22 +22 +22 +7 +22 +22 +22 +22 +22 +7 +7 +7 +7 +7 +7 +7 +22 +22 +22 +22 +22 +22 +22 +22 +6 +22 +22 +22 +22 +21 +22 +6 +21 +37 +37 +22 +6 +0 +0 +52 +22 +22 +22 +52 +0 +0 +0 +0 +0 +51 +21 +22 +53 +22 +22 +22 +0 +22 +0 +23 +23 +21 +22 +22 +22 +23 +22 +22 +22 +22 +38 +22 +23 +22 +22 +22 +22 +23 +22 +0 +22 +23 +23 +23 +22 +23 +23 +38 +23 +22 +22 +22 +53 +22 +22 +22 +23 +22 +22 +38 +22 +22 +53 +38 +22 +22 +22 +22 +22 +22 +22 +22 +23 +22 +22 +23 +22 +23 +23 +37 +22 +22 +22 +23 +22 +22 +23 +23 +23 +23 +23 +23 +23 +22 +22 +22 +22 +22 +38 +22 +21 +22 +21 +7 +23 +22 +38 +22 +22 +22 +22 +7 +22 +22 +22 +23 +23 +22 +22 +22 +38 +22 +21 +21 +22 +22 +22 +22 +23 +22 +22 +22 +22 +22 +22 +23 +22 +22 +22 +38 +38 +22 +7 +23 +23 +22 +22 +23 +23 +22 +22 +22 +22 +7 +22 +23 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +23 +23 +23 +22 +23 +22 +23 +7 +23 +22 +22 +22 +22 +22 +22 +22 +22 +22 +23 +22 +23 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +23 +22 +23 +22 +23 +22 +23 +7 +23 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +38 +38 +21 +7 +23 +22 +22 +22 +22 +38 +23 +23 +23 +22 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +22 +22 +23 +23 +22 +22 +23 +22 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +22 +22 +23 +22 +6 +22 +22 +7 +13 +30 +23 +23 +22 +22 +22 +22 +22 +22 +23 +23 +22 +22 +23 +23 +22 +22 +22 +22 +23 +23 +23 +23 +7 +7 +23 +23 +23 +23 +23 +23 +22 +22 +22 +22 +23 +23 +23 +38 +23 +38 +23 +23 +7 +7 +23 +23 +22 +22 +22 +22 +22 +22 +22 +22 +38 +23 +23 +22 +22 +23 +23 +22 +22 +23 +23 +22 +22 +23 +23 +53 +22 +22 +22 +22 +23 +23 +22 +22 +22 +22 +22 +22 +23 +23 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +23 +22 +23 +22 +23 +23 +22 +22 +22 +22 +22 +22 +22 +21 +22 +22 +22 +22 +22 +22 +23 +23 +23 +23 +23 +23 +7 +7 +23 +23 +22 +22 +23 +23 +22 +22 +22 +22 +7 +7 +7 +7 +7 +7 +23 +22 +22 +23 +22 +22 +7 +7 +7 +7 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +23 +22 +23 +23 +22 +22 +22 +22 +23 +23 +22 +22 +23 +22 +22 +22 +22 +23 +22 +23 +22 +23 +22 +22 +22 +23 +22 +23 +22 +23 +22 +22 +22 +23 +21 +22 +22 +23 +0 +0 +35 +35 +36 +37 +36 +21 +21 +0 +23 +22 +23 +23 +22 +23 +21 +22 +23 +23 +22 +21 +23 +23 +22 +22 +22 +23 +23 +23 +21 +22 +23 +22 +39 +23 +23 +23 +22 +23 +23 +22 +22 +54 +23 +23 +22 +23 +23 +0 +52 +22 +0 +0 +0 +0 +0 +0 +22 +22 +22 +22 +22 +22 +22 +22 +6 +22 +22 +22 +23 +23 +7 +23 +6 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +23 +6 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +68 +22 +22 +53 +22 +22 +37 +22 +0 +0 +0 +0 +0 +0 +0 +0 +22 +23 +22 +22 +22 +36 +38 +22 +22 +36 +22 +22 +38 +6 +23 +36 +37 +22 +6 +22 +22 +38 +22 +22 +22 +23 +22 +0 +0 +0 +0 +0 +23 +23 +23 +70 +22 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +15 +15 +15 +15 +0 +0 +28 +27 +45 +21 +23 +60 +52 +21 +62 +40 +23 +76 +60 +44 +60 +74 +58 +90 +90 +90 +90 +52 +0 +0 +121 +23 +0 +37 +23 +36 +22 +53 +22 +68 +22 +38 +22 +22 +22 +22 +22 +22 +22 +22 +22 +23 +23 +23 +23 +22 +22 +22 +22 +6 +6 +22 +22 +22 +7 +22 +22 +22 +22 +38 +22 +26 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +23 +22 +22 +90 +90 +90 +90 +90 +90 +90 +90 +90 +0 +36 +37 +38 +23 +22 +22 +23 +23 +23 +23 +21 +52 +52 +23 +22 +24 +22 +38 +22 +22 +36 +38 +23 +23 +23 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +23 +22 +22 +23 +22 +22 +22 +22 +23 +23 +23 +23 +23 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +6 +30 +6 +22 +22 +22 +6 +6 +6 +6 +6 +6 +23 +22 +23 +22 +22 +22 +22 +22 +22 +22 +22 +37 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +6 +22 +22 +22 +22 +23 +23 +37 +37 +61 +60 +90 +74 +58 +74 +76 +15 +31 +90 +90 +90 +90 +76 +90 +38 +23 +58 +90 +31 +90 +90 +90 +90 +22 +23 +36 +37 +38 +23 +22 +23 +22 +23 +23 +23 +23 +23 +22 +37 +22 +26 +59 +52 +52 +52 +52 +22 +22 +22 +22 +22 +22 +30 +29 +30 +0 +15 +15 +22 +28 +30 +30 +39 +39 +30 +60 +70 +30 +30 +30 +37 +30 +63 +43 +29 +46 +29 +29 +30 +30 +13 +13 +61 +39 +30 +29 +30 +30 +23 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +59 +59 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +126 +22 +0 +0 +60 +30 +30 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +23 +23 +22 +22 +22 +22 +22 +6 +6 +6 +38 +38 +22 +22 +22 +22 +22 +22 +23 +22 +22 +23 +22 +22 +36 +38 +22 +23 +22 +22 +22 +23 +23 +23 +23 +23 +22 +19 +22 +6 +22 +21 +37 +37 +37 +37 +37 +54 +22 +22 +38 +37 +21 +44 +54 +38 +38 +22 +54 +21 +7 +6 +20 +19 +22 +22 +44 +44 +44 +22 +22 +37 +37 +37 +37 +22 +22 +22 +22 +22 +23 +22 +6 +22 +22 +22 +20 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +22 +22 +38 +37 +37 +36 +21 +22 +22 +21 +51 +22 +6 +21 +21 +21 +21 +22 +38 +37 +37 +21 +38 +20 +22 +38 +22 +22 +21 +22 +37 +22 +21 +21 +21 +38 +37 +37 +21 +21 +22 +22 +38 +22 +22 +22 +22 +22 +22 +22 +22 +22 +36 +36 +38 +23 +36 +36 +37 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +75 +90 +93 +47 +31 +31 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +31 +31 +31 +31 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +63 +15 +63 +15 +15 +15 +15 +15 +47 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +0 +0 +90 +127 +95 +10 +95 +74 +91 +90 +90 +90 +74 +74 +58 +95 +95 +95 +95 +90 +0 +0 +30 +90 +90 +90 +90 +0 +0 +0 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +75 +75 +120 +106 +75 +90 +76 +75 +45 +75 +75 +61 +60 +75 +106 +-119 +31 +0 +0 +0 +0 +0 +0 +0 +0 +15 +15 +75 +15 +15 +0 +90 +95 +94 +0 +15 +15 +15 +15 +15 +15 +31 +30 +0 +0 +30 +30 +0 +0 +30 +30 +15 +47 +47 +15 +45 +15 +15 +15 +15 +30 +15 +15 +15 +15 +63 +15 +31 +15 +79 +15 +0 +31 +15 +15 +15 +15 +15 +15 +0 +15 +0 +0 +0 +31 +15 +15 +15 +0 +0 +90 +13 +95 +12 +79 +90 +91 +90 +90 +0 +0 +10 +10 +0 +0 +15 +15 +90 +75 +0 +0 +0 +0 +0 +0 +0 +0 +95 +0 +0 +0 +0 +15 +15 +0 +15 +31 +75 +91 +94 +0 +0 +60 +76 +60 +45 +44 +44 +45 +76 +29 +45 +15 +15 +59 +28 +59 +45 +45 +-119 +44 +61 +30 +0 +0 +0 +0 +0 +0 +90 +90 +95 +0 +15 +15 +15 +14 +15 +15 +0 +0 +0 +0 +15 +15 +0 +0 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +0 +15 +15 +15 +15 +15 +15 +15 +0 +15 +15 +0 +15 +15 +0 +15 +15 +0 +0 +90 +0 +79 +9 +95 +74 +74 +0 +0 +0 +0 +92 +92 +0 +0 +92 +92 +94 +0 +0 +0 +74 +0 +0 +0 +0 +0 +0 +0 +15 +15 +15 +15 +0 +15 +0 +0 +0 +0 +0 +0 +0 +43 +75 +75 +42 +74 +45 +76 +75 +43 +43 +90 +90 +15 +15 +46 +90 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +90 +90 +95 +0 +30 +15 +92 +76 +75 +45 +46 +45 +30 +0 +30 +30 +15 +0 +15 +15 +73 +27 +60 +93 +42 +45 +42 +27 +59 +60 +21 +72 +39 +42 +27 +75 +59 +89 +75 +58 +0 +43 +57 +46 +62 +76 +76 +57 +0 +42 +43 +0 +75 +60 +75 +44 +42 +0 +0 +90 +21 +95 +10 +95 +91 +91 +90 +90 +94 +0 +94 +94 +95 +0 +95 +95 +91 +0 +0 +30 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +30 +45 +92 +94 +0 +0 +43 +75 +75 +59 +60 +43 +60 +43 +57 +41 +0 +46 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +90 +95 +95 +0 +27 +29 +26 +26 +26 +27 +42 +90 +0 +0 +22 +43 +0 +0 +74 +44 +27 +75 +75 +59 +28 +75 +59 +75 +59 +43 +75 +57 +59 +59 +59 +26 +59 +26 +91 +42 +0 +59 +28 +42 +26 +27 +27 +75 +0 +27 +27 +0 +42 +59 +59 +43 +26 +0 +0 +90 +21 +92 +76 +92 +75 +91 +90 +74 +0 +0 +10 +13 +0 +0 +12 +13 +90 +0 +0 +0 +0 +0 +0 +0 +0 +93 +93 +0 +0 +0 +0 +59 +59 +0 +29 +43 +90 +93 +93 +0 +0 +43 +75 +75 +43 +92 +60 +75 +75 +74 +74 +27 +59 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +90 +45 +0 +43 +11 +43 +26 +27 +12 +0 +0 +0 +45 +45 +27 +0 +27 +27 +13 +27 +0 +0 +0 +27 +26 +0 +27 +0 +11 +59 +0 +0 +0 +15 +27 +0 +0 +0 +27 +29 +72 +0 +0 +0 +58 +59 +75 +60 +27 +29 +42 +27 +12 +28 +28 +14 +0 +0 +0 +0 +95 +94 +90 +95 +75 +0 +0 +0 +10 +10 +14 +0 +15 +15 +15 +90 +0 +0 +15 +0 +0 +0 +0 +0 +0 +95 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +90 +27 +29 +45 +29 +29 +30 +45 +27 +30 +42 +44 +29 +27 +46 +15 +75 +13 +15 +28 +47 +0 +0 +0 +0 +0 +0 +95 +95 +95 +0 +44 +44 +25 +27 +28 +13 +12 +43 +0 +60 +60 +60 +0 +59 +59 +43 +74 +26 +72 +28 +59 +28 +28 +59 +44 +43 +27 +57 +27 +27 +44 +44 +57 +57 +57 +43 +0 +60 +60 +28 +29 +28 +28 +57 +44 +27 +44 +0 +60 +58 +61 +60 +28 +0 +0 +0 +92 +95 +90 +74 +95 +95 +95 +95 +0 +74 +74 +75 +0 +43 +28 +43 +58 +0 +0 +0 +0 +0 +0 +0 +92 +75 +0 +28 +59 +0 +0 +0 +0 +0 +0 +15 +47 +76 +45 +0 +0 +90 +75 +44 +107 +75 +93 +28 +92 +29 +28 +0 +0 +0 +0 +0 +0 +0 +0 +91 +-120 +106 +74 +60 +92 +92 +60 +0 +0 +95 +95 +0 +44 +44 +25 +11 +11 +14 +13 +43 +0 +28 +28 +28 +0 +28 +28 +28 +58 +45 +43 +12 +28 +12 +12 +26 +44 +43 +27 +57 +27 +27 +28 +59 +57 +57 +57 +43 +0 +28 +28 +28 +28 +12 +12 +57 +44 +26 +27 +0 +28 +26 +29 +43 +76 +0 +0 +90 +22 +95 +90 +95 +95 +95 +95 +95 +0 +77 +79 +79 +0 +79 +79 +62 +92 +0 +0 +0 +0 +0 +0 +0 +95 +95 +0 +0 +0 +0 +0 +0 +0 +44 +0 +15 +31 +76 +61 +0 +0 +90 +75 +91 +60 +75 +45 +28 +92 +26 +28 +0 +74 +44 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +95 +95 +0 +13 +15 +27 +12 +57 +44 +59 +44 +0 +12 +12 +15 +0 +43 +45 +29 +28 +29 +29 +30 +28 +44 +45 +28 +14 +29 +74 +73 +45 +44 +28 +45 +28 +58 +44 +44 +0 +28 +28 +28 +58 +60 +45 +44 +59 +44 +43 +59 +28 +45 +28 +12 +12 +0 +0 +0 +108 +95 +95 +95 +95 +95 +95 +95 +0 +10 +10 +10 +0 +15 +15 +15 +95 +0 +0 +0 +0 +0 +0 +0 +0 +0 +95 +0 +0 +0 +0 +0 +0 +0 +0 +28 +29 +44 +44 +0 +0 +90 +44 +28 +29 +58 +60 +30 +74 +42 +27 +45 +44 +30 +60 +43 +44 +0 +0 +0 +30 +29 +44 +59 +28 +44 +29 +0 +0 +95 +95 +0 +60 +14 +31 +31 +59 +59 +59 +30 +15 +15 +30 +15 +42 +45 +15 +29 +29 +15 +0 +0 +0 +12 +44 +10 +26 +27 +30 +42 +46 +44 +14 +13 +14 +29 +42 +27 +44 +27 +14 +44 +12 +27 +92 +44 +13 +0 +74 +61 +61 +44 +29 +44 +44 +42 +57 +0 +44 +0 +0 +27 +11 +43 +26 +29 +62 +44 +0 +0 +0 +95 +0 +0 +0 +0 +95 +95 +95 +76 +76 +59 +0 +59 +0 +95 +10 +15 +10 +15 +15 +15 +95 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +95 +95 +14 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +22 +21 +22 +22 +22 +22 +21 +22 +22 +38 +22 +23 +22 +22 +22 +38 +22 +23 +23 +22 +22 +22 +22 +21 +22 +38 +38 +23 +23 +23 +23 +22 +22 +22 +21 +22 +22 +22 +21 +22 +23 +23 +22 +23 +22 +23 +21 +37 +23 +21 +22 +22 +22 +22 +22 +22 +22 +22 +0 +0 +0 +0 +22 +69 +55 +23 +22 +22 +21 +22 +22 +22 +23 +23 +22 +23 +22 +23 +22 +22 +22 +22 +22 +22 +22 +22 +22 +23 +22 +38 +23 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +22 +22 +0 +22 +0 +0 +21 +22 +0 +22 +0 +0 +22 +0 +0 +0 +0 +0 +0 +22 +22 +22 +6 +0 +22 +22 +22 +7 +6 +6 +6 +0 +22 +22 +22 +0 +22 +0 +22 +0 +0 +22 +7 +0 +22 +22 +38 +37 +23 +37 +6 +22 +22 +22 +22 +22 +22 +0 +23 +22 +22 +0 +0 +69 +38 +22 +22 +6 +0 +22 +0 +22 +22 +23 +22 +22 +22 +0 +0 +22 +22 +21 +22 +22 +22 +22 +22 +22 +22 +0 +0 +7 +7 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +23 +23 +12 +12 +7 +22 +7 +7 +22 +39 +23 +52 +15 +52 +38 +52 +52 +22 +23 +7 +37 +7 +30 +31 +31 +22 +37 +23 +23 +53 +23 +23 +39 +55 +39 +39 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +53 +23 +22 +22 +23 +23 +12 +46 +22 +39 +23 +6 +23 +23 +23 +23 +23 +23 +23 +23 +0 +23 +23 +39 +23 +23 +39 +23 +39 +23 +23 +39 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +39 +23 +23 +39 +23 +23 +39 +23 +23 +23 +23 +23 +23 +23 +0 +0 +0 +0 +22 +22 +23 +22 +22 +22 +23 +22 +23 +6 +6 +7 +7 +22 +22 +22 +23 +23 +23 +87 +23 +22 +22 +40 +23 +6 +23 +0 +0 +0 +0 +23 +23 +23 +23 +23 +23 +23 +23 +0 +23 +23 +22 +23 +23 +22 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +0 +23 +23 +23 +38 +22 +23 +23 +44 +29 +14 +44 +44 +14 +14 +14 +0 +44 +44 +29 +22 +89 +29 +75 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +12 +41 +41 +13 +40 +41 +12 +42 +9 +25 +28 +8 +6 +24 +26 +12 +10 +10 +40 +57 +25 +41 +57 +57 +26 +57 +10 +25 +10 +40 +26 +10 +6 +11 +27 +44 +11 +25 +25 +57 +58 +14 +15 +94 +95 +90 +90 +90 +91 +95 +90 +90 +90 +90 +90 +91 +95 +90 +90 +94 +30 +90 +90 +31 +42 +57 +55 +39 +73 +89 +89 +72 +57 +89 +68 +37 +22 +92 +78 +12 +57 +57 +57 +57 +39 +41 +95 +95 +90 +91 +91 +93 +75 +91 +90 +90 +90 +74 +94 +94 +94 +90 +91 +94 +94 +94 +94 +94 +94 +94 +29 +29 +27 +91 +90 +91 +90 +58 +58 +58 +11 +11 +11 +11 +27 +27 +27 +57 +28 +57 +91 +95 +10 +90 +90 +94 +94 +94 +95 +94 +95 +91 +74 +94 +90 +89 +73 +89 +90 +74 +74 +90 +90 +90 +0 +0 +0 +0 +123 +76 +6 +6 +6 +6 +21 +6 +6 +6 +21 +21 +6 +6 +21 +6 +6 +22 +6 +21 +22 +6 +6 +21 +22 +21 +21 +21 +22 +22 +22 +22 +21 +6 +6 +22 +22 +21 +6 +6 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +38 +38 +38 +23 +38 +38 +22 +23 +38 +38 +23 +38 +38 +23 +38 +22 +23 +38 +22 +22 +23 +38 +23 +38 +22 +38 +38 +38 +39 +22 +38 +38 +38 +38 +38 +38 +38 +22 +21 +38 +38 +39 +38 +38 +38 +0 +0 +0 +40 +40 +40 +40 +40 +40 +40 +40 +40 +55 +25 +55 +40 +41 +40 +40 +40 +24 +40 +45 +45 +45 +45 +45 +45 +45 +45 +45 +45 +61 +45 +45 +45 +45 +30 +30 +29 +30 +30 +45 +45 +45 +45 +76 +45 +61 +61 +61 +61 +61 +61 +30 +46 +61 +61 +61 +61 +61 +61 +61 +60 +60 +58 +61 +42 +61 +61 +62 +61 +62 +29 +61 +61 +61 +61 +61 +55 +45 +42 +44 +45 +45 +45 +45 +42 +43 +45 +60 +45 +44 +0 +0 +0 +0 +0 +15 +15 +-66 +-66 +-66 +-66 +-101 +-98 +-101 +-98 +46 +46 +46 +43 +46 +46 +43 +46 +43 +46 +46 +43 +-69 +44 +46 +46 +46 +46 +46 +43 +46 +46 +46 +46 +46 +46 +46 +46 +45 +46 +46 +46 +46 +46 +46 +46 +46 +46 +47 +46 +46 +46 +46 +47 +46 +46 +46 +-98 +-98 +46 +46 +46 +-117 +-117 +75 +46 +59 +60 +0 +0 +0 +0 +0 +91 +76 +77 +91 +76 +77 +91 +91 +76 +59 +59 +60 +59 +60 +60 +91 +91 +60 +59 +44 +106 +91 +76 +91 +91 +75 +91 +29 +30 +45 +45 +44 +30 +45 +45 +45 +31 +45 +45 +15 +45 +30 +31 +31 +15 +44 +30 +45 +45 +46 +45 +45 +45 +61 +31 +46 +46 +46 +74 +45 +45 +46 +91 +60 +61 +44 +61 +59 +61 +29 +45 +45 +89 +44 +29 +45 +75 +29 +29 +29 +29 +74 +0 +0 +0 +0 +0 +0 +41 +45 +75 +74 +74 +59 +44 +30 +74 +44 +44 +74 +44 +58 +45 +78 +60 +30 +30 +60 +29 +60 +60 +47 +45 +14 +30 +45 +30 +44 +44 +29 +60 +29 +30 +60 +45 +60 +60 +30 +58 +43 +59 +44 +60 +58 +60 +44 +74 +44 +44 +74 +60 +91 +74 +77 +42 +45 +45 +59 +45 +59 +59 +44 +60 +43 +43 +60 +43 +60 +43 +43 +61 +0 +31 +60 +60 +30 +0 +0 +60 +43 +43 +60 +43 +44 +60 +0 +44 +0 +31 +60 +60 +30 +0 +0 +74 +44 +44 +74 +61 +44 +74 +77 +42 +45 +45 +42 +28 +28 +42 +44 +28 +28 +28 +28 +28 +45 +45 +45 +45 +45 +45 +28 +28 +29 +45 +45 +58 +45 +45 +75 +45 +75 +45 +45 +29 +0 +30 +61 +45 +29 +0 +0 +89 +91 +74 +42 +90 +41 +44 +42 +60 +44 +44 +44 +43 +26 +44 +44 +74 +44 +44 +74 +44 +74 +74 +42 +59 +29 +29 +59 +29 +59 +59 +44 +29 +0 +30 +62 +29 +29 +0 +0 +27 +14 +14 +27 +31 +28 +27 +0 +15 +0 +15 +30 +31 +15 +0 +0 +44 +14 +44 +44 +44 +14 +44 +30 +75 +45 +28 +59 +28 +75 +75 +0 +74 +44 +44 +74 +61 +43 +74 +77 +45 +29 +29 +45 +29 +29 +45 +45 +74 +75 +60 +91 +60 +27 +44 +30 +60 +45 +44 +75 +44 +44 +59 +78 +75 +45 +44 +58 +44 +60 +59 +78 +43 +29 +28 +43 +28 +45 +43 +46 +57 +44 +60 +74 +60 +59 +74 +74 +45 +0 +45 +74 +60 +45 +0 +0 +75 +44 +60 +75 +60 +59 +74 +59 +59 +29 +29 +59 +29 +44 +59 +62 +14 +15 +15 +30 +15 +13 +30 +30 +75 +45 +45 +75 +45 +44 +75 +61 +75 +45 +45 +58 +44 +44 +58 +61 +75 +45 +11 +7 +11 +75 +75 +44 +60 +59 +43 +43 +44 +41 +43 +43 +60 +60 +60 +60 +60 +60 +60 +60 +60 +45 +44 +0 +0 +0 +0 +75 +60 +103 +90 +90 +90 +56 +89 +120 +75 +59 +76 +75 +75 +59 +59 +59 +60 +75 +75 +60 +60 +60 +75 +60 +75 +44 +59 +75 +45 +0 +0 +0 +15 +15 +29 +14 +44 +45 +42 +30 +61 +45 +60 +43 +61 +44 +60 +45 +52 +37 +52 +52 +37 +28 +22 +30 +30 +22 +0 +0 +0 +0 +0 +0 +22 +23 +23 +23 +23 +38 +23 +22 +22 +22 +22 +23 +22 +22 +22 +23 +22 +22 +23 +22 +22 +22 +22 +22 +22 +23 +22 +22 +22 +23 +22 +23 +22 +23 +22 +22 +22 +23 +23 +22 +22 +23 +23 +22 +23 +23 +22 +22 +23 +23 +22 +22 +23 +23 +23 +23 +23 +23 +22 +23 +23 +22 +22 +22 +23 +23 +22 +23 +23 +23 +22 +23 +23 +23 +23 +22 +23 +22 +22 +23 +22 +22 +22 +23 +22 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +23 +23 +23 +23 +22 +22 +22 +22 +22 +22 +22 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +22 +23 +23 +23 +23 +22 +23 +22 +21 +37 +37 +23 +23 +37 +37 +23 +37 +36 +21 +21 +21 +23 +23 +22 +22 +23 +23 +23 +23 +22 +22 +22 +22 +22 +22 +22 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +22 +37 +51 +20 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +21 +23 +23 +23 +23 +21 +21 +21 +21 +21 +21 +22 +21 +21 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +37 +37 +23 +23 +23 +23 +21 +21 +21 +21 +21 +21 +22 +21 +21 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +37 +37 +21 +21 +21 +21 +21 +21 +22 +21 +21 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +37 +37 +37 +37 +22 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +37 +22 +21 +22 +21 +21 +21 +21 +22 +21 +21 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +36 +22 +36 +22 +36 +36 +36 +44 +45 +44 +44 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +22 +22 +22 +22 +22 +22 +22 +22 +22 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +37 +21 +21 +21 +23 +23 +23 +23 +23 +23 +21 +21 +21 +21 +21 +21 +23 +23 +36 +37 +22 +23 +23 +23 +23 +23 +23 +23 +23 +7 +23 +21 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +21 +23 +23 +23 +23 +44 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +21 +22 +21 +44 +44 +44 +44 +44 +44 +44 +23 +21 +21 +21 +21 +23 +23 +23 +23 +31 +31 +31 +31 +31 +31 +31 +23 +31 +22 +22 +22 +22 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +21 +23 +23 +23 +23 +23 +21 +21 +21 +21 +22 +22 +22 +22 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +22 +22 +22 +22 +23 +23 +22 +22 +22 +22 +23 +23 +22 +22 +22 +22 +23 +23 +23 +6 +6 +23 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +6 +22 +22 +22 +6 +6 +6 +6 +21 +23 +23 +22 +22 +22 +22 +23 +23 +22 +22 +6 +22 +23 +23 +22 +22 +6 +22 +21 +22 +22 +6 +6 +6 +6 +23 +23 +22 +22 +6 +22 +22 +22 +22 +22 +22 +22 +6 +6 +6 +44 +44 +44 +44 +44 +75 +75 +46 +46 +46 +29 +60 +60 +21 +21 +21 +21 +60 +60 +6 +6 +6 +23 +6 +6 +6 +6 +6 +6 +76 +59 +7 +7 +7 +7 +45 +28 +12 +12 +12 +46 +6 +6 +60 +60 +60 +60 +6 +6 +6 +6 +60 +60 +60 +60 +44 +44 +44 +44 +44 +44 +44 +44 +44 +44 +44 +44 +21 +44 +44 +44 +44 +44 +44 +44 +44 +44 +44 +44 +44 +44 +44 +44 +44 +44 +44 +23 +36 +44 +31 +31 +31 +31 +31 +31 +31 +0 +0 +0 +0 +0 +0 +0 +0 +0 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +47 +13 +0 +0 +0 +6 +6 +5 +6 +5 +6 +4 +4 +4 +6 +6 +4 +6 +54 +20 +6 +6 +6 +21 +6 +6 +6 +6 +6 +6 +4 +6 +6 +6 +36 +6 +38 +6 +51 +36 +6 +6 +6 +4 +6 +6 +6 +20 +21 +51 +36 +6 +6 +4 +5 +21 +21 +4 +6 +6 +6 +6 +36 +22 +6 +6 +6 +6 +6 +12 +6 +12 +6 +6 +6 +6 +51 +6 +21 +6 +36 +19 +6 +6 +6 +6 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +30 +30 +75 +30 +45 +30 +30 +30 +59 +30 +28 +30 +30 +0 +30 +76 +46 +46 +93 +92 +90 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +29 +29 +91 +59 +59 +59 +60 +44 +28 +29 +41 +45 +28 +60 +43 +30 +30 +60 +93 +92 +62 +89 +60 +0 +0 +0 +0 +0 +0 +0 +0 +0 +44 +44 +75 +59 +91 +61 +30 +44 +92 +45 +74 +60 +45 +60 +43 +60 +44 +74 +93 +92 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +30 +30 +36 +44 +29 +30 +13 +30 +29 +30 +45 +30 +30 +0 +30 +30 +29 +0 +93 +92 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +75 +90 +75 +44 +74 +43 +75 +46 +61 +90 +91 +59 +46 +45 +45 +75 +75 +77 +59 +90 +76 +75 +76 +59 +59 +61 +89 +61 +90 +76 +60 +60 +61 +60 +44 +44 +29 +91 +62 +91 +75 +59 +75 +76 +78 +76 +77 +75 +76 +91 +75 +92 +15 +15 +94 +92 +91 +92 +92 +90 +90 +90 +11 +15 +15 +10 +10 +10 +14 +14 +90 +95 +94 +90 +75 +90 +92 +92 +90 +94 +93 +43 +15 +90 +56 +24 +20 +30 +15 +74 +15 +90 +38 +75 +0 +0 +74 +59 +44 +44 +77 +75 +59 +46 +60 +44 +0 +0 +0 +0 +0 +0 +20 +21 +34 +7 +21 +21 +36 +6 +36 +21 +0 +0 +0 +0 +0 +0 +29 +120 +74 +74 +75 +14 +120 +72 +60 +60 +120 +15 +15 +15 +15 +0 +45 +45 +45 +45 +45 +44 +45 +45 +44 +45 +0 +0 +0 +0 +0 +0 +60 +28 +44 +60 +60 +60 +60 +60 +28 +61 +60 +44 +60 +12 +78 +78 +76 +78 +75 +43 +40 +24 +40 +24 +56 +28 +44 +44 +40 +40 +44 +45 +45 +40 +56 +123 +60 +77 +60 +28 +60 +60 +24 +24 +60 +28 +28 +77 +75 +56 +24 +24 +40 +56 +40 +62 +78 +40 +29 +44 +24 +46 +40 +75 +61 +56 +24 +30 +61 +62 +60 +60 +77 +76 +24 +45 +62 +62 +78 +77 +60 +44 +28 +24 +46 +40 +45 +60 +0 +0 +0 +0 +0 +0 +0 +0 +60 +105 +75 +44 +44 +90 +14 +60 +61 +45 +28 +24 +40 +56 +40 +40 +40 +40 +61 +29 +30 +46 +45 +60 +56 +43 +62 +60 +78 +76 +45 +44 +44 +77 +28 +45 +62 +62 +72 +24 +61 +10 +62 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +8 +56 +9 +25 +8 +27 +25 +24 +24 +39 +42 +24 +25 +25 +24 +56 +10 +8 +25 +24 +24 +23 +57 +40 +8 +26 +38 +41 +25 +0 +0 +0 +75 +92 +75 +94 +95 +78 +79 +58 +42 +95 +94 +94 +0 +0 +0 +0 +95 +95 +90 +93 +95 +95 +95 +93 +95 +92 +90 +74 +0 +0 +0 +0 +25 +0 +0 +0 +20 +25 +74 +75 +59 +74 +44 +74 +74 +58 +59 +58 +21 +21 +21 +6 +6 +22 +6 +6 +22 +37 +6 +37 +6 +37 +6 +7 +21 +6 +22 +85 +37 +37 +37 +37 +37 +37 +37 +37 +37 +6 +0 +0 +23 +21 +21 +22 +21 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +75 +58 +59 +90 +59 +59 +58 +58 +90 +75 +93 +73 +74 +42 +42 +90 +59 +74 +59 +74 +60 +60 +59 +59 +75 +59 +57 +59 +43 +59 +75 +59 +59 +73 +59 +74 +73 +59 +59 +74 +59 +59 +0 +0 +0 +0 +0 +0 +95 +95 +95 +95 +95 +10 +10 +10 +95 +95 +10 +95 +95 +95 +95 +95 +95 +89 +58 +89 +59 +59 +73 +59 +95 +95 +0 +0 +0 +0 +0 +0 +75 +76 +90 +106 +75 +59 +90 +44 +30 +74 +0 +0 +0 +0 +45 +45 +76 +89 +59 +59 +73 +73 +89 +76 +74 +89 +45 +45 +30 +30 +45 +45 +74 +73 +59 +59 +72 +74 +73 +76 +74 +73 +45 +45 +30 +30 +45 +45 +23 +60 +38 +7 +60 +6 +23 +43 +23 +23 +23 +60 +60 +60 +29 +30 +29 +21 +29 +29 +108 +29 +29 +94 +95 +10 +95 +10 +0 +0 +7 +21 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +59 +74 +90 +91 +95 +45 +14 +44 +15 +75 +61 +60 +14 +46 +15 +58 +15 +10 +15 +30 +30 +29 +29 +27 +60 +28 +44 +15 +15 +27 +27 +28 +43 +15 +30 +43 +60 +45 +60 +43 +44 +14 +28 +44 +29 +43 +14 +60 +15 +27 +43 +14 +90 +95 +58 +58 +95 +95 +75 +79 +28 +31 +26 +10 +31 +15 +58 +63 +95 +30 +15 +30 +60 +60 +43 +15 +0 +0 +0 +0 +105 +31 +46 +10 +72 +75 +58 +14 +44 +30 +15 +15 +105 +91 +90 +45 +89 +-49 +89 +89 +89 +107 +58 +75 +36 +14 +53 +90 +90 +90 +90 +90 +74 +91 +90 +90 +21 +20 +5 +5 +20 +20 +20 +5 +35 +0 +0 +0 +90 +75 +94 +58 +73 +91 +61 +92 +57 +58 +45 +29 +92 +91 +61 +44 +93 +62 +60 +43 +106 +45 +77 +77 +46 +75 +61 +90 +75 +91 +45 +60 +79 +94 +92 +91 +90 +90 +10 +95 +90 +90 +95 +0 +0 +0 +31 +31 +75 +92 +91 +92 +91 +30 +91 +75 +60 +91 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +59 +57 +75 +60 +45 +108 +58 +60 +77 +44 +107 +75 +58 +59 +76 +59 +60 +60 +59 +74 +75 +59 +75 +60 +61 +44 +90 +76 +59 +60 +74 +74 +59 +74 +61 +59 +95 +95 +95 +10 +10 +13 +95 +95 +90 +90 +90 +90 +74 +90 +42 +42 +10 +10 +93 +90 +0 +0 +0 +-120 +105 +106 +91 +60 +89 +105 +75 +75 +75 +75 +59 +106 +90 +106 +0 +0 +0 +59 +59 +44 +21 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +6 +22 +22 +22 +6 +22 +22 +6 +22 +6 +22 +6 +6 +6 +6 +22 +7 +6 +6 +7 +22 +22 +6 +6 +6 +52 +52 +52 +6 +36 +21 +51 +37 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +21 +22 +6 +6 +22 +21 +5 +21 +22 +21 +21 +38 +22 +38 +21 +22 +22 +23 +23 +23 +23 +21 +38 +21 +22 +22 +22 +23 +22 +39 +23 +23 +23 +22 +22 +22 +22 +6 +22 +23 +23 +22 +23 +22 +21 +22 +21 +6 +21 +21 +21 +22 +22 +21 +21 +38 +38 +38 +21 +21 +22 +21 +22 +22 +23 +22 +22 +22 +22 +22 +6 +21 +21 +22 +22 +22 +22 +22 +21 +20 +23 +22 +22 +22 +38 +38 +22 +21 +22 +22 +23 +22 +6 +22 +23 +22 +23 +22 +21 +22 +22 +22 +22 +23 +21 +23 +22 +7 +6 +23 +21 +7 +23 +23 +23 +23 +7 +22 +6 +22 +22 +22 +45 +21 +21 +23 +23 +23 +23 +23 +21 +23 +22 +38 +23 +23 +22 +22 +23 +22 +22 +22 +22 +22 +23 +23 +23 +23 +22 +60 +39 +22 +21 +7 +22 +22 +21 +21 +38 +38 +21 +38 +20 +21 +21 +37 +21 +21 +22 +21 +36 +54 +23 +23 +23 +23 +38 +22 +21 +37 +21 +22 +23 +23 +38 +22 +38 +38 +23 +23 +22 +38 +22 +22 +22 +22 +22 +22 +22 +6 +6 +6 +22 +6 +6 +7 +22 +22 +23 +6 +22 +6 +6 +6 +7 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +22 +22 +6 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +21 +22 +22 +21 +22 +21 +22 +22 +22 +22 +22 +6 +23 +22 +22 +38 +38 +22 +22 +22 +22 +22 +22 +22 +38 +22 +38 +22 +38 +22 +38 +22 +23 +22 +23 +22 +23 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +23 +21 +23 +21 +23 +21 +23 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +23 +22 +23 +21 +22 +23 +22 +23 +22 +23 +21 +23 +21 +23 +21 +21 +22 +22 +23 +22 +22 +22 +22 +22 +22 +22 +22 +21 +23 +22 +22 +37 +21 +21 +22 +22 +21 +22 +22 +22 +22 +22 +22 +22 +22 +23 +39 +23 +38 +22 +21 +22 +21 +22 +6 +22 +6 +22 +21 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +38 +38 +38 +38 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +22 +22 +22 +22 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +22 +23 +22 +23 +22 +23 +22 +6 +7 +22 +21 +22 +22 +22 +22 +22 +22 +22 +22 +6 +6 +6 +6 +6 +6 +6 +6 +6 +6 +22 +22 +22 +22 +22 +22 +0 +0 +6 +6 +6 +6 +6 +6 +0 +0 +22 +22 +22 +22 +22 +22 +22 +22 +6 +6 +6 +6 +6 +6 +6 +6 +53 +53 +21 +21 +21 +21 +22 +22 +6 +6 +6 +6 +6 +6 +6 +6 +22 +22 +22 +22 +22 +22 +0 +0 +6 +6 +6 +6 +6 +6 +0 +0 +22 +22 +22 +22 +22 +22 +22 +22 +0 +7 +0 +7 +0 +7 +0 +7 +23 +23 +23 +23 +23 +23 +23 +23 +7 +7 +7 +7 +7 +7 +23 +23 +22 +22 +22 +22 +22 +22 +53 +53 +22 +22 +22 +22 +23 +23 +0 +0 +22 +22 +22 +22 +22 +22 +22 +22 +6 +6 +6 +6 +6 +6 +6 +6 +22 +22 +22 +22 +22 +22 +22 +22 +6 +6 +6 +6 +6 +6 +6 +6 +23 +23 +23 +23 +23 +23 +23 +23 +7 +7 +7 +7 +7 +7 +7 +7 +22 +22 +22 +22 +22 +0 +22 +22 +22 +22 +6 +6 +22 +52 +53 +52 +22 +22 +22 +22 +22 +0 +22 +22 +6 +6 +6 +6 +22 +21 +21 +22 +21 +37 +22 +22 +0 +0 +22 +22 +38 +38 +6 +6 +0 +21 +21 +22 +22 +22 +22 +22 +22 +22 +22 +22 +23 +23 +7 +7 +6 +22 +22 +52 +0 +0 +23 +23 +23 +0 +23 +23 +6 +6 +7 +7 +23 +52 +52 +0 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +37 +37 +22 +22 +23 +7 +53 +7 +52 +52 +52 +52 +22 +22 +22 +22 +23 +23 +21 +37 +52 +38 +23 +52 +15 +15 +15 +15 +15 +15 +15 +15 +7 +7 +37 +7 +7 +37 +7 +7 +21 +70 +19 +23 +37 +22 +22 +22 +22 +22 +23 +37 +22 +70 +19 +22 +22 +22 +21 +6 +38 +21 +23 +52 +22 +23 +22 +7 +22 +6 +37 +15 +21 +21 +51 +6 +21 +51 +51 +15 +15 +15 +15 +15 +15 +0 +0 +0 +0 +0 +15 +15 +15 +15 +15 +15 +38 +68 +0 +0 +38 +38 +38 +38 +38 +38 +38 +38 +38 +69 +52 +38 +38 +36 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +69 +52 +0 +21 +21 +21 +22 +21 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +22 +22 +22 +7 +23 +23 +23 +23 +23 +23 +22 +22 +22 +6 +23 +23 +39 +7 +22 +7 +7 +22 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +22 +22 +6 +6 +22 +22 +22 +22 +7 +7 +7 +22 +23 +14 +30 +14 +14 +7 +30 +30 +46 +22 +22 +46 +22 +7 +61 +22 +22 +22 +22 +22 +22 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +23 +23 +23 +6 +22 +23 +23 +22 +22 +6 +23 +7 +7 +23 +22 +6 +38 +23 +7 +22 +7 +23 +7 +7 +23 +22 +23 +23 +7 +23 +22 +22 +7 +7 +7 +7 +22 +22 +23 +23 +22 +22 +21 +22 +23 +22 +22 +21 +22 +22 +22 +7 +23 +22 +23 +22 +23 +22 +44 +30 +61 +61 +23 +44 +60 +22 +22 +22 +6 +60 +60 +6 +22 +6 +22 +23 +29 +7 +21 +29 +0 +0 +0 +22 +22 +22 +22 +22 +6 +22 +22 +22 +22 +22 +22 +22 +53 +22 +23 +23 +23 +7 +7 +7 +23 +23 +23 +23 +22 +22 +22 +23 +53 +22 +23 +23 +23 +23 +23 +7 +23 +23 +23 +23 +53 +22 +23 +23 +22 +22 +59 +22 +22 +6 +6 +7 +29 +0 +0 +0 +0 +0 +0 +0 +22 +38 +22 +38 +7 +38 +22 +22 +22 +22 +23 +23 +23 +23 +7 +38 +7 +38 +23 +23 +22 +38 +22 +38 +38 +23 +23 +23 +23 +7 +7 +22 +21 +38 +21 +38 +23 +21 +7 +7 +22 +22 +22 +22 +22 +22 +70 +36 +22 +22 +70 +36 +22 +7 +22 +22 +7 +22 +7 +22 +22 +6 +7 +23 +22 +38 +22 +38 +7 +38 +22 +22 +22 +22 +6 +23 +7 +7 +38 +38 +23 +38 +23 +38 +22 +22 +7 +23 +7 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +38 +29 +7 +7 +7 +7 +44 +28 +45 +29 +7 +7 +45 +23 +37 +22 +22 +22 +23 +23 +23 +22 +22 +22 +22 +22 +22 +22 +23 +23 +22 +22 +23 +23 +22 +22 +38 +37 +37 +23 +23 +23 +22 +23 +22 +22 +22 +22 +52 +22 +37 +22 +23 +23 +22 +22 +38 +23 +7 +38 +23 +7 +39 +39 +22 +22 +22 +52 +22 +22 +22 +22 +22 +22 +22 +23 +23 +52 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +23 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +7 +7 +37 +22 +38 +21 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +22 +22 +23 +23 +37 +37 +22 +22 +23 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +23 +23 +23 +23 +38 +38 +38 +38 +22 +22 +23 +23 +22 +22 +38 +52 +38 +38 +38 +38 +38 +38 +38 +22 +23 +23 +22 +22 +23 +23 +38 +22 +38 +21 +23 +23 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +52 +7 +7 +7 +27 +59 +22 +59 +59 +22 +25 +25 +44 +42 +22 +42 +22 +76 +23 +6 +22 +38 +38 +38 +38 +53 +54 +20 +54 +20 +71 +20 +71 +20 +22 +23 +23 +23 +23 +23 +23 +7 +23 +22 +7 +22 +20 +54 +20 +54 +70 +36 +22 +22 +23 +23 +7 +23 +22 +20 +54 +7 +23 +7 +23 +22 +23 +7 +23 +23 +22 +22 +23 +38 +23 +23 +23 +23 +23 +23 +23 +7 +7 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +38 +23 +23 +23 +23 +23 +38 +38 +23 +23 +23 +23 +23 +38 +38 +23 +38 +23 +23 +23 +23 +23 +23 +38 +38 +38 +23 +23 +23 +22 +22 +37 +23 +23 +22 +38 +23 +23 +22 +22 +54 +22 +23 +22 +22 +54 +23 +22 +30 +30 +29 +60 +38 +6 +29 +29 +29 +45 +59 +30 +30 +44 +12 +12 +12 +30 +45 +45 +59 +59 +59 +59 +29 +30 +23 +21 +29 +29 +14 +29 +38 +35 +38 +21 +69 +21 +38 +35 +38 +21 +69 +21 +54 +20 +54 +52 +20 +54 +20 +68 +7 +22 +22 +30 +30 +30 +30 +30 +23 +0 +119 +7 +7 +7 +7 +38 +38 +44 +44 +44 +44 +44 +44 +44 +44 +44 +44 +44 +39 +39 +29 +30 +6 +68 +6 +6 +6 +14 +14 +14 +30 +30 +30 +6 +15 +15 +15 +14 +14 +30 +30 +29 +76 +30 +29 +29 +14 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +30 +30 +29 +29 +30 +30 +29 +30 +26 +27 +27 +27 +27 +27 +27 +27 +28 +29 +29 +29 +29 +30 +30 +29 +29 +27 +30 +30 +25 +25 +25 +25 +25 +30 +22 +22 +22 +23 +22 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +38 +38 +38 +38 +38 +38 +22 +7 +23 +23 +7 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +73 +59 +59 +60 +59 +59 +60 +59 +59 +12 +11 +13 +13 +12 +13 +13 +13 +13 +13 +29 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +15 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +30 +7 +7 +68 +52 +7 +7 +68 +52 +6 +6 +68 +52 +71 +71 +55 +55 +4 +4 +4 +4 +71 +71 +55 +55 +4 +4 +4 +4 +71 +71 +55 +55 +55 +55 +55 +55 +4 +4 +4 +4 +4 +4 +4 +4 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +6 +6 +68 +52 +7 +53 +71 +55 +55 +4 +5 +5 +71 +55 +55 +4 +5 +5 +71 +55 +55 +4 +5 +5 +7 +7 +7 +7 +7 +7 +7 +7 +7 +71 +4 +4 +71 +7 +7 +7 +3 +68 +71 +68 +3 +52 +71 +52 +7 +52 +7 +52 +15 +15 +15 +15 +15 +15 +15 +15 +15 +13 +11 +9 +7 +5 +3 +1 +-113 +14 +15 +15 +15 +-17 +7 +-113 +7 +15 +15 +15 +15 +-113 +15 +15 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +37 +37 +23 +23 +37 +37 +7 +7 +22 +22 +22 +22 +22 +22 +37 +37 +22 +22 +22 +22 +22 +22 +22 +22 +37 +37 +22 +22 +23 +23 +23 +23 +22 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +20 +71 +7 +7 +7 +7 +20 +71 +71 +20 +23 +23 +22 +22 +22 +22 +21 +22 +22 +22 +22 +23 +23 +22 +22 +7 +22 +22 +22 +22 +7 +7 +7 +7 +22 +22 +22 +22 +22 +37 +37 +22 +23 +7 +23 +15 +23 +23 +23 +38 +22 +7 +7 +7 +21 +23 +23 +23 +23 +23 +23 +38 +23 +45 +44 +44 +29 +30 +23 +23 +23 +37 +23 +37 +7 +37 +29 +14 +29 +38 +38 +22 +38 +23 +7 +44 +61 +7 +23 +29 +29 +29 +29 +29 +29 +29 +29 +29 +23 +7 +7 +7 +23 +39 +22 +38 +38 +38 +23 +22 +22 +23 +7 +22 +23 +23 +23 +23 +7 +23 +23 +23 +23 +22 +22 +22 +23 +23 +23 +23 +22 +22 +23 +23 +23 +23 +22 +22 +23 +23 +38 +23 +23 +23 +38 +23 +7 +20 +22 +7 +7 +22 +22 +22 +23 +23 +15 +14 +14 +14 +14 +14 +14 +14 +14 +15 +14 +14 +12 +45 +59 +59 +59 +59 +59 +59 +12 +12 +12 +12 +29 +29 +29 +29 +29 +29 +22 +22 +15 +30 +29 +59 +14 +44 +44 +29 +14 +29 +14 +14 +0 +0 +29 +22 +44 +44 +60 +75 +44 +14 +6 +13 +6 +6 +38 +44 +61 +29 +47 +44 +6 +5 +6 +6 +29 +22 +22 +7 +6 +6 +23 +0 +0 +0 +45 +45 +30 +30 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +14 +13 +14 +14 +0 +14 +14 +14 +30 +0 +0 +14 +31 +28 +14 +28 +14 +14 +29 +14 +44 +14 +28 +28 +14 +14 +14 +14 +59 +29 +29 +14 +14 +14 +14 +14 +15 +14 +14 +0 +14 +14 +14 +14 +14 +14 +14 +15 +14 +14 +14 +14 +14 +29 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +30 +14 +14 +14 +14 +0 +46 +0 +62 +62 +62 +62 +0 +0 +0 +29 +0 +120 +105 +75 +74 +74 +14 +14 +0 +0 +30 +59 +59 +14 +29 +14 +30 +54 +37 +38 +38 +38 +38 +39 +22 +23 +6 +53 +36 +22 +22 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +0 +0 +0 +44 +14 +44 +14 +14 +14 +14 +14 +14 +14 +30 +30 +30 +14 +14 +91 +14 +30 +30 +14 +14 +14 +14 +15 +0 +15 +30 +14 +44 +30 +44 +44 +14 +44 +14 +14 +14 +14 +14 +0 +22 +44 +21 +60 +60 +38 +38 +6 +30 +30 +36 +0 +45 +0 +0 +0 +76 +6 +59 +22 +22 +45 +45 +30 +59 +59 +29 +29 +30 +30 +30 +121 +6 +29 +14 +31 +14 +31 +38 +21 +37 +37 +22 +22 +54 +20 +70 +19 +44 +44 +45 +45 +14 +14 +14 +13 +14 +14 +13 +13 +30 +13 +30 +14 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +38 +14 +14 +31 +14 +46 +30 +31 +30 +89 +89 +59 +59 +31 +14 +31 +14 +13 +14 +23 +23 +30 +30 +30 +30 +30 +30 +30 +30 +30 +31 +31 +30 +30 +46 +46 +45 +28 +28 +28 +30 +46 +30 +29 +45 +45 +46 +46 +46 +46 +30 +30 +30 +29 +29 +60 +60 +37 +37 +45 +45 +45 +45 +45 +45 +44 +44 +30 +30 +30 +14 +14 +14 +14 +21 +30 +30 +21 +21 +30 +36 +30 +36 +31 +14 +21 +21 +31 +14 +21 +21 +46 +29 +21 +21 +46 +29 +21 +21 +30 +76 +30 +76 +30 +30 +30 +30 +30 +30 +30 +30 +76 +76 +30 +14 +14 +31 +14 +14 +76 +30 +76 +76 +30 +76 +23 +23 +59 +59 +38 +105 +37 +22 +22 +37 +37 +37 +37 +37 +37 +70 +19 +54 +20 +54 +20 +38 +21 +61 +61 +61 +61 +37 +37 +119 +104 +46 +45 +45 +46 +46 +61 +61 +46 +46 +46 +46 +22 +22 +46 +46 +46 +46 +45 +45 +45 +45 +59 +59 +59 +59 +59 +44 +59 +59 +59 +59 +59 +59 +61 +59 +61 +61 +61 +61 +29 +45 +27 +27 +27 +27 +27 +45 +29 +29 +31 +14 +59 +44 +44 +6 +6 +6 +6 +6 +6 +6 +36 +36 +91 +91 +44 +59 +44 +30 +45 +46 +44 +30 +30 +30 +30 +59 +29 +29 +59 +6 +91 +91 +57 +57 +57 +57 +57 +57 +30 +21 +21 +21 +23 +6 +6 +6 +6 +6 +29 +29 +61 +61 +61 +58 +59 +59 +59 +30 +30 +44 +59 +59 +13 +74 +74 +74 +74 +61 +74 +74 +75 +74 +74 +59 +74 +74 +74 +74 +74 +44 +22 +37 +60 +37 +21 +21 +5 +21 +22 +74 +21 +21 +21 +21 +21 +74 +74 +38 +21 +21 +21 +59 +75 +75 +44 +14 +8 +29 +29 +29 +22 +22 +53 +60 +75 +75 +75 +75 +59 +59 +37 +37 +37 +37 +6 +6 +75 +75 +59 +59 +46 +59 +59 +44 +44 +45 +45 +60 +77 +29 +44 +44 +44 +44 +59 +59 +59 +59 +59 +59 +44 +44 +23 +23 +44 +44 +5 +5 +5 +5 +44 +5 +5 +5 +5 +5 +44 +29 +30 +5 +45 +59 +59 +43 +60 +74 +74 +74 +74 +74 +74 +74 +74 +75 +58 +74 +74 +75 +58 +5 +5 +75 +58 +5 +5 +21 +21 +22 +22 +74 +74 +74 +74 +74 +74 +74 +74 +75 +75 +75 +75 +59 +59 +45 +93 +29 +59 +59 +59 +59 +59 +59 +59 +59 +44 +59 +76 +59 +59 +59 +59 +59 +59 +59 +59 +59 +59 +30 +30 +44 +44 +22 +22 +22 +22 +22 +22 +22 +22 +44 +44 +22 +22 +22 +22 +30 +30 +59 +59 +59 +59 +22 +22 +22 +22 +29 +29 +59 +59 +59 +59 +59 +37 +59 +59 +44 +43 +45 +60 +45 +45 +59 +59 +59 +59 +59 +60 +60 +21 +21 +21 +21 +6 +75 +74 +44 +51 +44 +44 +74 +74 +44 +74 +75 +36 +36 +44 +44 +44 +44 +30 +29 +6 +6 +44 +44 +44 +44 +30 +6 +30 +30 +30 +30 +60 +60 +60 +60 +60 +60 +60 +60 +44 +30 +30 +37 +37 +45 +45 +44 +44 +30 +30 +6 +6 +6 +6 +21 +21 +21 +14 +14 +22 +22 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +14 +14 +30 +30 +30 +30 +30 +30 +0 +0 +0 +29 +44 +44 +61 +61 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +21 +6 +6 +6 +6 +21 +6 +6 +7 +6 +6 +6 +7 +37 +6 +61 +6 +38 +6 +22 +6 +6 +7 +6 +6 +6 +6 +6 +7 +6 +6 +60 +6 +6 +6 +22 +6 +6 +6 +45 +28 +13 +7 +6 +53 +6 +22 +0 +21 +38 +6 +6 +6 +21 +6 +23 +7 +6 +6 +6 +6 +37 +22 +44 +22 +38 +22 +21 +6 +6 +6 +6 +6 +6 +22 +38 +6 +6 +21 +60 +22 +6 +6 +21 +22 +22 +6 +45 +45 +30 +6 +22 +53 +22 +22 +0 +22 +21 +23 +22 +22 +7 +7 +22 +22 +23 +23 +22 +22 +22 +22 +22 +0 +7 +6 +23 +6 +21 +21 +23 +6 +22 +6 +38 +53 +21 +0 +0 +7 +7 +22 +21 +22 +21 +7 +6 +6 +6 +22 +22 +6 +6 +22 +22 +7 +7 +36 +36 +22 +22 +7 +6 +6 +21 +6 +21 +7 +6 +6 +6 +7 +22 +22 +22 +22 +22 +6 +6 +7 +7 +6 +6 +23 +23 +7 +7 +6 +6 +6 +21 +22 +21 +6 +22 +21 +37 +6 +22 +7 +7 +7 +7 +6 +6 +6 +6 +7 +5 +37 +37 +7 +7 +22 +22 +6 +6 +76 +75 +22 +21 +22 +22 +37 +37 +6 +22 +7 +22 +7 +23 +6 +6 +22 +22 +7 +7 +6 +6 +22 +7 +7 +14 +23 +23 +14 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +6 +21 +21 +6 +37 +22 +37 +22 +6 +6 +21 +6 +43 +22 +60 +22 +37 +60 +38 +21 +44 +7 +7 +59 +21 +5 +60 +60 +6 +6 +22 +22 +37 +5 +59 +60 +21 +22 +7 +44 +21 +21 +21 +22 +59 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +37 +6 +6 +6 +6 +6 +6 +21 +21 +22 +22 +22 +6 +6 +21 +6 +6 +6 +52 +7 +21 +6 +22 +6 +60 +36 +21 +6 +6 +37 +22 +51 +6 +51 +37 +37 +6 +6 +21 +52 +7 +6 +6 +23 +21 +6 +23 +22 +21 +21 +36 +6 +37 +6 +0 +0 +0 +0 +0 +0 +0 +0 +0 +37 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +14 +46 +29 +9 +10 +9 +43 +43 +61 +61 +43 +8 +44 +44 +44 +45 +30 +28 +44 +29 +13 +27 +30 +0 +0 +0 +0 +0 +0 +0 +0 +0 +44 +29 +29 +44 +29 +26 +45 +0 +60 +60 +60 +60 +60 +44 +28 +0 +30 +30 +30 +30 +30 +30 +30 +0 +15 +15 +15 +14 +15 +14 +14 +0 +60 +60 +60 +43 +60 +44 +60 +0 +76 +60 +60 +76 +60 +60 +76 +0 +11 +14 +14 +11 +14 +11 +11 +0 +42 +27 +27 +25 +27 +41 +25 +0 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +22 +23 +22 +22 +22 +22 +22 +22 +22 +22 +22 +6 +37 +37 +37 +37 +37 +37 +21 +21 +5 +5 +5 +21 +21 +21 +29 +30 +30 +31 +18 +44 +44 +44 +37 +22 +22 +7 +22 +23 +21 +21 +23 +23 +37 +37 +37 +37 +37 +37 +22 +22 +22 +5 +22 +22 +22 +7 +22 +52 +37 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +59 +13 +44 +59 +14 +73 +27 +14 +43 +-99 +91 +45 +59 +44 +14 +30 +14 +30 +45 +28 +14 +28 +5 +29 +5 +14 +0 +13 +44 +59 +30 +14 +45 +4 +13 +13 +29 +30 +22 +14 +5 +14 +5 +44 +31 +5 +14 +7 +5 +44 +44 +29 +14 +45 +14 +14 +14 +14 +14 +14 +28 +29 +14 +14 +14 +13 +5 +29 +14 +14 +12 +28 +5 +44 +14 +14 +14 +14 +14 +-83 +6 +14 +14 +14 +29 +60 +21 +14 +14 +30 +29 +14 +14 +14 +6 +7 +6 +14 +13 +14 +14 +14 +29 +46 +14 +14 +46 +14 +14 +14 +14 +14 +14 +14 +14 +14 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +15 +119 +90 +25 +14 +87 +15 +14 +30 +30 +14 +14 +45 +14 +5 +30 +29 +28 +29 +29 +60 +29 +45 +14 +108 +90 +14 +30 +14 +44 +29 +14 +14 +14 +14 +59 +14 +14 +14 +30 +14 +14 +14 +13 +44 +29 +29 +14 +46 +44 +14 +29 +14 +14 +30 +13 +44 +28 +29 +57 +14 +29 +13 +14 +30 +14 +14 +14 +14 +14 +14 +59 +46 +27 +30 +14 +14 +14 +30 +14 +46 +14 +45 +30 +14 +29 +14 +14 +14 +28 +14 +30 +30 +14 +30 +14 +14 +14 +14 +14 +13 +44 +14 +14 +14 +44 +14 +14 +60 +14 +14 +14 +14 +45 +14 +30 +14 +14 +14 +29 +30 +29 +30 +29 +14 +30 +30 +14 +30 +45 +46 +60 +14 +45 +14 +14 +14 +46 +29 +30 +14 +29 +14 +14 +14 +14 +46 +29 +30 +14 +14 +14 +29 +44 +14 +14 +14 +30 +14 +14 +14 +14 +46 +14 +14 +14 +14 +14 +45 +14 +30 +14 +14 +30 +14 +30 +14 +14 +14 +14 +30 +14 +14 +13 +14 +14 +29 +30 +14 +14 +29 +45 +29 +30 +29 +30 +46 +14 +14 +14 +14 +14 +14 +14 +30 +29 +14 +46 +14 +14 +14 +14 +14 +30 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +0 +0 +0 +0 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +0 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +0 +0 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +0 +0 +0 +0 +0 +44 +44 +44 +45 +44 +29 +44 +44 +58 +29 +13 +74 +89 +29 +29 +57 +12 +74 +43 +29 +44 +60 +29 +29 +29 +29 +60 +59 +59 +44 +60 +29 +44 +29 +119 +27 +44 +29 +29 +30 +29 +0 +0 +0 +45 +29 +15 +45 +14 +15 +29 +30 +44 +14 +30 +29 +14 +30 +15 +15 +44 +59 +45 +30 +45 +14 +59 +28 +14 +45 +45 +44 +29 +29 +-97 +124 +-97 +124 +107 +109 +107 +109 +31 +31 +29 +28 +31 +31 +28 +28 +28 +31 +31 +28 +-101 +15 +14 +15 +14 +15 +15 +30 +15 +14 +14 +29 +30 +31 +45 +15 +14 +15 +15 +14 +14 +44 +44 +14 +30 +14 +30 +15 +44 +30 +44 +14 +14 +29 +15 +29 +31 +28 +28 +28 +12 +28 +104 +92 +0 +-120 +106 +41 +41 +43 +42 +41 +41 +41 +41 +41 +42 +42 +42 +44 +41 +44 +44 +74 +45 +29 +29 +29 +29 +29 +60 +30 +30 +29 +30 +29 +60 +29 +29 +29 +55 +39 +23 +22 +23 +0 +0 +0 +0 +0 +0 +0 +0 +41 +87 +59 +14 +75 +43 +43 +43 +30 +44 +41 +45 +106 +30 +43 +77 +30 +119 +90 +73 +89 +60 +29 +61 +43 +90 +104 +90 +60 +46 +44 +76 +30 +45 +43 +29 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +22 +23 +6 +37 +6 +59 +22 +6 +75 +6 +60 +6 +5 +77 +5 +59 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +0 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +0 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +27 +27 +27 +44 +59 +74 +74 +75 +44 +74 +74 +59 +59 +59 +75 +59 +28 +28 +28 +44 +44 +59 +91 +59 +59 +91 +44 +59 +59 +76 +74 +59 +59 +59 +75 +75 +74 +59 +74 +74 +59 +59 +59 +74 +59 +74 +74 +90 +74 +74 +59 +75 +75 +43 +29 +29 +44 +58 +58 +59 +59 +59 +59 +59 +59 +59 +75 +75 +75 +75 +59 +74 +74 +75 +90 +58 +75 +75 +59 +59 +59 +44 +44 +43 +59 +59 +59 +59 +59 +44 +59 +28 +45 +60 +45 +30 +30 +59 +74 +74 +59 +59 +74 +74 +59 +43 +60 +45 +45 +43 +60 +60 +58 +58 +59 +59 +59 +74 +75 +75 +75 +60 +59 +59 +74 +45 +59 +59 +59 +44 +59 +59 +59 +75 +58 +58 +59 +29 +29 +75 +59 +59 +30 +44 +44 +74 +59 +59 +59 +59 +59 +75 +44 +45 +45 +45 +60 +60 +44 +60 +44 +44 +74 +74 +74 +74 +59 +59 +59 +75 +75 +75 +59 +59 +59 +59 +59 +60 +44 +44 +60 +60 +59 +59 +59 +59 +59 +59 +45 +59 +76 +76 +75 +45 +45 +60 +42 +59 +60 +59 +59 +75 +59 +59 +75 +59 +91 +91 +75 +29 +60 +60 +60 +58 +75 +59 +59 +44 +44 +59 +44 +44 +59 +43 +43 +43 +59 +44 +44 +59 +43 +59 +59 +44 +59 +59 +59 +44 +60 +60 +60 +45 +59 +59 +45 +43 +43 +59 +58 +58 +29 +59 +59 +45 +59 +59 +45 +59 +59 +59 +45 +45 +30 +60 +60 +60 +29 +74 +74 +74 +44 +44 +44 +59 +59 +60 +75 +44 +44 +59 +60 +60 +44 +43 +60 +60 +43 +43 +75 +75 +59 +59 +75 +29 +75 +75 +29 +74 +44 +44 +29 +60 +60 +59 +59 +59 +59 +60 +44 +44 +60 +59 +59 +60 +44 +44 +44 +59 +45 +45 +44 +74 +74 +74 +74 +59 +59 +59 +74 +74 +44 +59 +59 +59 +60 +59 +59 +60 +74 +74 +74 +44 +44 +60 +60 +60 +44 +75 +59 +59 +59 +45 +45 +44 +74 +59 +59 +59 +59 +59 +60 +60 +60 +44 +44 +59 +59 +30 +60 +44 +44 +74 +44 +44 +29 +45 +45 +74 +74 +45 +30 +44 +75 +75 +59 +59 +59 +44 +60 +60 +60 +44 +44 +75 +75 +30 +30 +45 +74 +74 +44 +75 +59 +59 +60 +59 +59 +60 +59 +59 +59 +43 +60 +75 +75 +59 +59 +60 +74 +74 +44 +45 +45 +44 +59 +59 +59 +60 +75 +75 +59 +44 +44 +75 +43 +60 +60 +29 +29 +44 +73 +73 +59 +74 +73 +73 +74 +60 +74 +74 +45 +75 +60 +60 +44 +59 +74 +74 +74 +74 +74 +60 +59 +59 +59 +29 +44 +44 +29 +60 +60 +60 +90 +90 +44 +75 +75 +44 +59 +59 +59 +44 +59 +59 +59 +59 +29 +29 +29 +59 +29 +44 +44 +59 +44 +74 +74 +44 +59 +59 +59 +59 +44 +44 +90 +59 +75 +59 +44 +44 +44 +60 +60 +60 +44 +60 +60 +74 +59 +59 +59 +59 +59 +74 +74 +59 +59 +59 +59 +59 +57 +74 +59 +74 +74 +60 +60 +60 +60 +75 +75 +44 +75 +44 +44 +59 +44 +74 +74 +73 +59 +59 +59 +44 +60 +59 +59 +59 +59 +59 +30 +30 +59 +60 +60 +45 +44 +44 +74 +74 +59 +59 +60 +59 +59 +44 +44 +74 +74 +59 +59 +59 +75 +44 +44 +59 +73 +59 +59 +75 +45 +45 +45 +45 +44 +90 +90 +59 +74 +74 +74 +44 +59 +59 +44 +60 +60 +30 +44 +60 +60 +44 +44 +89 +89 +59 +74 +74 +60 +74 +74 +59 +60 +60 +60 +59 +43 +60 +60 +60 +44 +44 +59 +60 +59 +59 +44 +45 +45 +74 +45 +60 +60 +60 +75 +75 +75 +75 +75 +74 +60 +60 +60 +59 +59 +59 +75 +60 +60 +60 +60 +60 +60 +59 +73 +73 +45 +74 +29 +29 +59 +60 +60 +74 +74 +89 +89 +59 +29 +29 +60 +73 +73 +75 +45 +45 +60 +59 +59 +74 +30 +91 +91 +59 +59 +59 +59 +60 +59 +59 +74 +44 +44 +44 +59 +59 +59 +59 +45 +29 +29 +59 +75 +75 +74 +90 +90 +74 +59 +59 +60 +44 +44 +90 +59 +59 +59 +60 +60 +59 +59 +59 +75 +75 +59 +75 +75 +59 +59 +59 +60 +90 +90 +59 +75 +75 +74 +57 +74 +75 +74 +74 +75 +59 +59 +59 +74 +74 +74 +60 +59 +59 +59 +59 +90 +90 +59 +59 +29 +59 +59 +59 +90 +106 +59 +59 +59 +44 +59 +59 +59 +29 +29 +59 +60 +59 +59 +59 +90 +90 +74 +90 +59 +59 +74 +75 +75 +59 +59 +58 +75 +43 +59 +59 +59 +59 +59 +59 +59 +59 +60 +91 +91 +60 +60 +60 +60 +59 +44 +44 +59 +60 +59 +59 +59 +74 +74 +59 +60 +59 +59 +59 +59 +44 +44 +44 +59 +59 +59 +59 +59 +44 +44 +44 +74 +44 +59 +59 +59 +44 +44 +44 +59 +45 +60 +60 +74 +60 +60 +74 +59 +59 +75 +60 +60 +74 +74 +74 +74 +44 +44 +44 +45 +14 +60 +60 +29 +59 +59 +59 +44 +60 +60 +75 +29 +29 +60 +60 +60 +75 +60 +60 +45 +45 +59 +59 +44 +74 +74 +59 +28 +74 +74 +60 +59 +59 +59 +44 +74 +74 +60 +75 +75 +75 +60 +60 +59 +59 +59 +75 +75 +75 +75 +60 +45 +45 +59 +74 +45 +45 +29 +59 +59 +75 +43 +43 +76 +60 +60 +60 +44 +44 +29 +74 +74 +60 +59 +59 +59 +45 +59 +60 +60 +60 +74 +44 +44 +75 +75 +75 +60 +90 +90 +59 +58 +75 +60 +60 +60 +74 +74 +74 +59 +44 +74 +74 +44 +75 +75 +59 +74 +43 +60 +59 +74 +74 +60 +91 +91 +45 +60 +60 +74 +43 +60 +60 +45 +29 +29 +75 +59 +59 +59 +60 +59 +59 +59 +59 +59 +90 +90 +75 +59 +59 +60 +74 +74 +74 +60 +60 +45 +74 +74 +59 +45 +44 +44 +60 +75 +59 +59 +59 +59 +59 +59 +59 +75 +59 +59 +59 +59 +59 +60 +74 +74 +74 +59 +59 +74 +59 +59 +59 +58 +44 +44 +74 +59 +59 +60 +45 +29 +29 +59 +59 +75 +75 +74 +75 +75 +44 +74 +74 +75 +59 +59 +59 +45 +59 +59 +59 +59 +59 +59 +29 +30 +30 +74 +90 +90 +75 +28 +45 +59 +59 +59 +74 +59 +59 +59 +60 +60 +73 +30 +30 +30 +29 +75 +75 +44 +44 +58 +58 +74 +59 +75 +75 +75 +59 +60 +60 +74 +59 +59 +59 +44 +74 +74 +59 +60 +60 +14 +74 +43 +60 +60 +43 +60 +44 +45 +45 +74 +45 +59 +59 +59 +59 +59 +59 +58 +59 +75 +75 +75 +74 +59 +59 +74 +74 +74 +74 +73 +73 +59 +44 +44 +0 +0 +0 +119 +73 +90 +90 +74 +90 +73 +44 +60 +60 +90 +105 +75 +75 +74 +90 +74 +74 +74 +60 +59 +74 +44 +74 +59 +89 +59 +74 +59 +59 +60 +90 +89 +89 +90 +59 +29 +59 +29 +44 +75 +58 +75 +90 +89 +74 +74 +44 +60 +44 +45 +74 +74 +59 +89 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +44 +44 +44 +44 +44 +29 +29 +76 +76 +59 +59 +59 +89 +89 +60 +60 +29 +29 +60 +60 +44 +60 +74 +74 +59 +59 +29 +29 +44 +44 +14 +44 +45 +76 +45 +29 +60 +61 +61 +90 +60 +62 +74 +30 +59 +59 +44 +44 +44 +59 +59 +61 +61 +75 +75 +90 +75 +60 +30 +44 +44 +43 +45 +74 +29 +29 +29 +74 +74 +44 +44 +91 +76 +89 +44 +45 +30 +29 +61 +44 +44 +30 +45 +44 +59 +44 +59 +75 +74 +60 +59 +59 +60 +45 +75 +75 +44 +45 +59 +59 +59 +29 +60 +44 +59 +29 +74 +45 +44 +30 +29 +74 +29 +60 +60 +60 +60 +60 +75 +75 +59 +30 +74 +44 +74 +44 +44 +58 +58 +30 +30 +44 +44 +29 +44 +44 +44 +89 +59 +74 +74 +60 +88 +29 +29 +44 +29 +29 +74 +45 +28 +28 +14 +44 +28 +45 +29 +60 +60 +44 +44 +44 +29 +30 +74 +58 +59 +29 +60 +60 +60 +60 +59 +59 +59 +59 +60 +60 +60 +60 +44 +89 +89 +74 +59 +74 +59 +44 +44 +45 +29 +59 +45 +30 +30 +60 +60 +45 +45 +29 +29 +75 +75 +59 +60 +59 +60 +90 +75 +44 +44 +75 +77 +75 +77 +75 +60 +60 +91 +76 +29 +59 +59 +75 +30 +60 +30 +45 +44 +30 +44 +44 +74 +77 +58 +59 +30 +30 +29 +29 +29 +76 +76 +29 +29 +60 +30 +59 +59 +59 +59 +29 +29 +29 +29 +44 +44 +44 +44 +44 +44 +44 +44 +44 +60 +59 +44 +61 +45 +89 +75 +29 +30 +59 +30 +90 +45 +29 +74 +44 +44 +76 +44 +61 +44 +43 +30 +43 +59 +-120 +93 +25 +74 +75 +58 +59 +90 +60 +29 +91 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +22 +22 +22 +22 +22 +22 +37 +37 +6 +21 +22 +22 +29 +29 +23 +23 +43 +43 +22 +22 +22 +22 +6 +22 +22 +22 +23 +23 +6 +23 +43 +7 +0 +0 +45 +60 +45 +44 +45 +44 +22 +22 +6 +23 +45 +45 +31 +22 +13 +22 +14 +6 +0 +0 +0 +0 +0 +0 +0 +0 +22 +22 +22 +70 +7 +23 +22 +37 +30 +30 +6 +6 +7 +23 +7 +7 +6 +6 +23 +23 +23 +23 +43 +60 +22 +21 +7 +7 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +37 +37 +37 +37 +37 +37 +37 +37 +19 +19 +19 +19 +19 +53 +53 +53 +53 +53 +54 +54 +54 +54 +54 +19 +54 +21 +21 +21 +21 +51 +51 +51 +22 +22 +21 +37 +22 +21 +22 +22 +44 +45 +22 +38 +21 +21 +7 +7 +21 +22 +45 +45 +45 +45 +45 +45 +44 +44 +44 +45 +44 +44 +22 +22 +6 +6 +22 +22 +6 +6 +22 +55 +6 +38 +7 +7 +7 +7 +45 +45 +6 +6 +7 +7 +7 +7 +23 +23 +60 +60 +22 +22 +6 +22 +7 +6 +22 +23 +22 +21 +6 +22 +6 +22 +21 +38 +21 +21 +22 +22 +22 +22 +22 +45 +44 +29 +28 +59 +28 +27 +6 +6 +22 +22 +21 +22 +22 +22 +22 +38 +23 +22 +21 +21 +6 +6 +22 +52 +37 +68 +68 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +22 +22 +22 +38 +15 +15 +15 +90 +15 +15 +15 +91 +15 +15 +15 +15 +92 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +95 +95 +15 +90 +95 +20 +20 +29 +59 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +58 +58 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +60 +59 +60 +61 +60 +60 +0 +0 +0 +0 +0 +0 +0 +0 +94 +94 +8 +9 +8 +8 +8 +8 +8 +9 +8 +8 +8 +8 +8 +8 +8 +8 +24 +10 +8 +8 +8 +8 +8 +8 +8 +8 +8 +8 +8 +8 +8 +8 +24 +8 +8 +8 +8 +8 +8 +9 +8 +8 +8 +8 +8 +8 +8 +8 +8 +8 +94 +94 +94 +94 +94 +94 +94 +94 +94 +94 +94 +94 +94 +94 +94 +94 +92 +0 +0 +0 +0 +0 +0 +0 +0 +0 +-120 +105 +75 +106 +75 +73 +106 +76 +90 +90 +90 +74 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +45 +45 +59 +76 +59 +60 +60 +60 +60 +61 +44 +45 +59 +45 +44 +60 +59 +45 +75 +60 +60 +45 +59 +44 +76 +75 +75 +44 +44 +60 +45 +59 +29 +60 +45 +60 +45 +45 +90 +74 +59 +58 +58 +90 +58 +58 +60 +-120 +58 +58 +29 +58 +59 +45 +75 +59 +44 +76 +29 +29 +45 +45 +60 +29 +43 +44 +60 +61 +45 +29 +59 +10 +10 +10 +94 +10 +10 +10 +94 +94 +94 +94 +94 +94 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +60 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +47 +27 +74 +61 +60 +60 +59 +46 +60 +46 +44 +44 +44 +47 +29 +45 +31 +31 +31 +60 +62 +44 +60 +46 +46 +46 +28 +45 +14 +44 +30 +30 +45 +45 +30 +76 +61 +45 +30 +45 +45 +93 +95 +95 +95 +94 +95 +10 +10 +94 +93 +95 +10 +93 +95 +0 +0 +0 +0 +0 +0 +0 +0 +0 +59 +43 +45 +93 +45 +60 +30 +44 +30 +77 +44 +29 +90 +95 +0 +0 +75 +43 +46 +46 +29 +57 +76 +77 +45 +45 +0 +0 +43 +-120 +105 +91 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +14 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +0 +0 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +0 +0 +0 +0 +0 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +23 +22 +22 +22 +22 +22 +22 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +23 +23 +23 +23 +7 +0 +0 +0 +0 +0 +53 +22 +23 +6 +45 +45 +45 +45 +77 +13 +45 +45 +23 +23 +23 +23 +23 +23 +23 +23 +23 +22 +22 +22 +36 +38 +0 +22 +36 +22 +22 +38 +0 +23 +0 +37 +22 +0 +22 +22 +0 +22 +22 +22 +23 +22 +36 +23 +22 +22 +22 +38 +7 +22 +23 +5 +7 +22 +23 +5 +7 +22 +23 +5 +7 +22 +23 +5 +7 +22 +23 +5 +7 +22 +23 +5 +7 +22 +23 +5 +7 +22 +23 +5 +7 +22 +23 +5 +7 +22 +23 +5 +7 +22 +23 +5 +7 +22 +23 +5 +7 +22 +23 +22 +23 +22 +23 +22 +23 +22 +23 +22 +23 +6 +7 +6 +7 +6 +7 +6 +7 +6 +7 +6 +7 +6 +7 +6 +7 +22 +23 +22 +23 +5 +7 +37 +39 +37 +23 +5 +7 +22 +23 +6 +7 +23 +23 +23 +23 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +22 +23 +6 +7 +22 +23 +22 +23 +22 +23 +7 +22 +23 +22 +23 +22 +23 +22 +23 +5 +7 +5 +7 +21 +23 +22 +23 +23 +23 +23 +23 +7 +7 +23 +23 +21 +23 +6 +22 +23 +6 +22 +23 +5 +7 +6 +6 +23 +22 +22 +6 +22 +22 +23 +23 +23 +22 +22 +6 +23 +22 +22 +6 +23 +22 +22 +22 +23 +22 +23 +22 +22 +23 +23 +23 +23 +45 +23 +45 +23 +23 +7 +45 +22 +23 +23 +22 +23 +6 +23 +7 +7 +7 +20 +22 +22 +22 +20 +22 +22 +6 +22 +22 +6 +6 +21 +59 +59 +23 +23 +23 +21 +22 +22 +22 +22 +22 +23 +22 +22 +22 +22 +22 +23 +22 +22 +22 +21 +23 +23 +22 +22 +22 +23 +23 +23 +22 +22 +23 +22 +22 +22 +22 +22 +21 +23 +7 +23 +7 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +39 +7 +39 +7 +7 +21 +22 +22 +22 +7 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +23 +6 +6 +6 +6 +7 +5 +5 +5 +6 +7 +6 +6 +6 +6 +7 +6 +6 +7 +6 +7 +6 +7 +13 +13 +13 +7 +13 +13 +11 +13 +13 +13 +11 +23 +7 +6 +5 +6 +5 +6 +6 +6 +4 +6 +4 +6 +6 +6 +6 +5 +5 +5 +5 +6 +6 +6 +6 +6 +6 +5 +5 +5 +6 +7 +7 +7 +6 +6 +6 +6 +6 +7 +7 +7 +7 +7 +7 +7 +7 +7 +15 +15 +15 +15 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +23 +23 +22 +22 +22 +22 +30 +30 +30 +30 +22 +22 +23 +23 +22 +22 +23 +23 +23 +23 +23 +23 +7 +23 +7 +7 +7 +7 +23 +23 +23 +23 +23 +23 +31 +31 +31 +31 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +7 +13 +13 +13 +13 +13 +13 +7 +15 +15 +15 +15 +15 +15 +7 +7 +55 +55 +30 +30 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +13 +31 +13 +13 +13 +13 +29 +13 +31 +13 +31 +31 +15 +15 +15 +15 +14 +14 +31 +14 +15 +15 +31 +15 +14 +15 +31 +14 +31 +14 +15 +15 +14 +31 +14 +14 +15 +31 +47 +14 +15 +47 +15 +15 +31 +13 +31 +31 +63 +47 +47 +12 +31 +31 +13 +47 +12 +13 +13 +15 +13 +13 +13 +13 +0 +0 +13 +14 +14 +12 +31 +31 +12 +15 +31 +31 +63 +14 +15 +15 +15 +15 +15 +31 +31 +15 +15 +15 +15 +15 +15 +15 +31 +31 +31 +31 +31 +31 +31 +15 +13 +12 +15 +15 +12 +15 +12 +31 +31 +47 +15 +15 +15 +31 +15 +15 +15 +14 +15 +15 +0 +0 +0 +0 +0 +0 +0 +0 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +30 +44 +46 +29 +62 +30 +15 +30 +30 +30 +15 +31 +46 +15 +0 +0 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +15 +-51 +-84 +-83 +-51 +-51 +-35 +-115 +45 +45 +-51 +0 +0 +0 +0 +0 +0 +23 +6 +23 +6 +23 +6 +7 +0 +0 +0 +0 +0 +0 +0 +0 +0 +120 +-120 +-120 +17 +2 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +-100 +-99 +30 +30 +46 +30 +15 +15 +46 +30 +15 +104 +104 +120 +0 +104 +120 +104 +-120 +89 +120 +120 +121 +104 +121 +104 +89 +105 +104 +89 +105 +121 +104 +105 +0 +105 +89 +105 +105 +0 +0 +0 +0 +37 +7 +38 +53 +37 +0 +37 +7 +21 +7 +37 +7 +21 +7 +37 +7 +37 +23 +7 +36 +23 +22 +23 +53 +23 +22 +23 +5 +7 +68 +39 +22 +23 +5 +7 +38 +39 +22 +23 +5 +7 +22 +23 +5 +7 +22 +23 +6 +7 +22 +23 +5 +7 +22 +23 +5 +7 +22 +23 +22 +23 +22 +23 +22 +23 +23 +23 +6 +7 +23 +23 +6 +7 +23 +23 +6 +7 +23 +23 +6 +7 +22 +23 +6 +7 +22 +23 +6 +7 +22 +23 +6 +7 +22 +23 +6 +7 +22 +23 +5 +7 +22 +23 +5 +7 +22 +23 +6 +7 +22 +23 +5 +7 +38 +39 +6 +7 +22 +23 +5 +7 +37 +39 +6 +7 +22 +23 +22 +23 +22 +23 +5 +7 +6 +7 +22 +23 +38 +23 +38 +23 +0 +0 +15 +0 +87 +90 +44 +59 +29 +30 +104 +-100 +54 +59 +61 +52 +46 +52 +29 +60 +91 +76 +60 +61 +76 +59 +59 +60 +59 +120 +120 +44 +45 +61 +75 +45 +46 +60 +44 +60 +60 +76 +46 +45 +74 +77 +61 +60 +29 +29 +45 +77 +46 +61 +60 +44 +45 +45 +30 +44 +44 +60 +-116 +29 +55 +89 +15 +104 +76 +75 +74 +75 +74 +75 +75 +76 +121 +73 +59 +104 +61 +76 +75 +75 +75 +91 +91 +58 +76 +59 +45 +59 +76 +75 +-116 +-120 +55 +45 +60 +60 +19 +54 +37 +20 +52 +22 +22 +22 +22 +22 +22 +22 +22 +37 +22 +37 +7 +6 +22 +7 +7 +22 +7 +22 +7 +22 +7 +23 +7 +6 +22 +22 +7 +7 +7 +54 +7 +7 +22 +7 +22 +7 +22 +22 +7 +7 +7 +37 +7 +6 +6 +7 +7 +22 +22 +38 +7 +39 +22 +22 +23 +36 +36 +15 +22 +21 +23 +22 +39 +39 +22 +21 +37 +22 +6 +6 +6 +6 +7 +6 +21 +22 +6 +23 +21 +7 +21 +21 +7 +21 +22 +22 +22 +6 +0 +0 +0 +36 +36 +36 +36 +36 +21 +0 +0 +36 +21 +21 +22 +6 +21 +0 +0 +21 +21 +37 +22 +37 +21 +0 +0 +22 +36 +68 +0 +0 +0 +59 +47 +45 +60 +-120 +61 +46 +0 +51 +22 +38 +22 +38 +22 +22 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +15 +15 +15 +15 +22 +15 +15 \ No newline at end of file From 8b6be73d9407dbf6347eb5a84aef16286ef039ef Mon Sep 17 00:00:00 2001 From: Brent P Date: Tue, 3 Jan 2023 23:03:05 -0500 Subject: [PATCH 05/22] Fix some formatting and refactoring --- .../bungeetablistplus/BootstrapPlugin.java | 21 +++++-- .../bungeetablistplus/BungeeTabListPlus.java | 4 +- .../compat/SortingRuleAliasProcessor.java | 4 +- .../AbstractLegacyTabOverlayHandler.java | 49 ++++++++------- .../handler/AbstractTabOverlayHandler.java | 61 +++++++++---------- .../handler/NewTabOverlayHandler.java | 20 +++--- .../managers/DataManager.java | 2 +- .../managers/ServerStateManager.java | 10 +-- .../PlayerPlaceholderResolver.java | 8 +-- .../util/VelocityPlugin.java | 4 +- 10 files changed, 94 insertions(+), 89 deletions(-) diff --git a/bootstrap-velocity/src/main/java/codecrafter47/bungeetablistplus/BootstrapPlugin.java b/bootstrap-velocity/src/main/java/codecrafter47/bungeetablistplus/BootstrapPlugin.java index 8f244b06..61c778cd 100644 --- a/bootstrap-velocity/src/main/java/codecrafter47/bungeetablistplus/BootstrapPlugin.java +++ b/bootstrap-velocity/src/main/java/codecrafter47/bungeetablistplus/BootstrapPlugin.java @@ -38,11 +38,13 @@ name = "BungeeTabListPlus", version = "@VERSION@", dependencies = { - @Dependency(id = "RedisBungee", optional = true), - @Dependency(id = "LuckPerms", optional = true), - @Dependency(id = "Geyser", optional = true), - @Dependency(id = "Floodgate", optional = true) - } + @Dependency(id = "redisbungee", optional = true), + @Dependency(id = "luckperms", optional = true), + @Dependency(id = "geyser", optional = true), + @Dependency(id = "floodgate", optional = true), + @Dependency(id = "viaversion", optional = true) + }, + authors = "CodeCrafter47 & proferabg" ) public class BootstrapPlugin extends VelocityPlugin { @@ -76,6 +78,13 @@ public void onProxyInitialization(final ProxyInitializeEvent event) { @Subscribe public void onProxyShutdown(final ProxyShutdownEvent event) { BungeeTabListPlus.getInstance().onDisable(); - + if (isProxyRunning()) { + getLogger().error("You cannot use ServerUtils to reload BungeeTabListPlus. Use /btlp reload instead."); + if (!getProxy().getAllPlayers().isEmpty()) { + for (Player proxiedPlayer : getProxy().getAllPlayers()) { + proxiedPlayer.disconnect(Component.text("Cannot reload BungeeTabListPlus while players are online.")); + } + } + } } } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java index 667b7661..721fe7a8 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java @@ -202,7 +202,7 @@ protected EventExecutor newChild(Executor executor, Object... args) { }; mainThreadExecutor = new ExceptionHandlingEventExecutor(null, executor, logger); - if (getProxy().getPluginManager().getPlugin("ViaVersion").isPresent()) { + if (getProxy().getPluginManager().getPlugin("viaversion").isPresent()) { protocolVersionProvider = new ViaVersionProtocolVersionProvider(); } else { protocolVersionProvider = new VelocityProtocolVersionProvider(); @@ -264,7 +264,7 @@ public void onEnable() { fakePlayerManagerImpl = new FakePlayerManagerImpl(plugin, iconManager, mainThreadExecutor); List playerProviders = new ArrayList<>(); - if (getProxy().getPluginManager().getPlugin("RedisBungee").isPresent()) { + if (getProxy().getPluginManager().getPlugin("redisbungee").isPresent()) { redisPlayerManager = new RedisPlayerManager(bungeePlayerProvider, this, logger); playerProviders.add(redisPlayerManager); plugin.getLogger().info("Hooked RedisBungee"); diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/compat/SortingRuleAliasProcessor.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/compat/SortingRuleAliasProcessor.java index 5dade956..f0c20541 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/compat/SortingRuleAliasProcessor.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/compat/SortingRuleAliasProcessor.java @@ -47,8 +47,8 @@ public class SortingRuleAliasProcessor implements SortingRulePreprocessor { .put("vaultgroupinforeversed", new RewriteData("vault_primary_group_weight desc", true)) .put("bungeepermsgroupinfo", new RewriteData("bungeeperms_primary_group_weight asc", true)) .put("bungeepermsgroupinforeversed", new RewriteData("bungeeperms_primary_group_weight desc", true)) - .put("luckpermsgroupinfo", new RewriteData("luckpermsbungee_primary_group_weight asc", true)) - .put("luckpermsgroupinforeversed", new RewriteData("luckpermsbungee_primary_group_weight desc", true)) + .put("luckpermsgroupinfo", new RewriteData("luckpermsvelocity_primary_group_weight asc", true)) + .put("luckpermsgroupinforeversed", new RewriteData("luckpermsvelocity_primary_group_weight desc", true)) .put("vaultprefix", new RewriteData("vault_prefix asc", true)) .put("connectedfirst", new RewriteData("session_duration_total_seconds desc", false)) .put("connectedlast", new RewriteData("session_duration_total_seconds asc", false)) diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractLegacyTabOverlayHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractLegacyTabOverlayHandler.java index 74d73b9c..fc88e5c4 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractLegacyTabOverlayHandler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractLegacyTabOverlayHandler.java @@ -42,9 +42,8 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import lombok.AllArgsConstructor; import lombok.val; -import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; -import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -65,9 +64,9 @@ public abstract class AbstractLegacyTabOverlayHandler implements PacketHandler, TabOverlayHandler { - private static final Component[] slotID; + private static final String[] slotID; private static final UUID[] slotUUID; - private static final Set slotIDSet = new HashSet<>(); + private static final Set slotIDSet = new HashSet<>(); private static final Int2ObjectMap> playerListSizeToSupportedSizesMap = new Int2ObjectOpenHashMap<>(); @@ -77,12 +76,12 @@ public abstract class AbstractLegacyTabOverlayHandler implements PacketHandler, // add a random character to the player and team names to prevent issues in multi-velocity setup (stupid!). int random = ThreadLocalRandom.current().nextInt(0x1e00, 0x2c00); - slotID = new Component[256]; + slotID = new String[256]; slotUUID = new UUID[256]; for (int i = 0; i < 256; i++) { String hex = String.format("%02x", i); - slotID[i] = LegacyComponentSerializer.legacySection().deserialize(String.format("§B§T§L§P§%c§%c§%c§r", random, hex.charAt(0), hex.charAt(1))); + slotID[i] = String.format("§B§T§L§P§%c§%c§%c§r", random, hex.charAt(0), hex.charAt(1)); slotIDSet.add(slotID[i]); slotUUID[i] = UUID.randomUUID(); } @@ -117,7 +116,7 @@ private static Collection getSupportedSizesByPl private final int playerListSize; private final Executor eventLoopExecutor; - private final Object2IntMap serverPlayerList = new Object2IntLinkedOpenHashMap<>(); + private final Object2IntMap serverPlayerList = new Object2IntLinkedOpenHashMap<>(); private final Object2ObjectMap modernServerPlayerList = new Object2ObjectOpenHashMap<>(); private boolean is13OrLater; @@ -249,10 +248,10 @@ private void update() { this.activeHandler.update(); } - private void removeEntry(UUID uuid, Component player) { + private void removeEntry(UUID uuid, String player) { LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(); - item.setName(PlainTextComponentSerializer.plainText().serialize(player)); - item.setDisplayName(player); + item.setName(player); + item.setDisplayName(GsonComponentSerializer.gson().deserialize(player)); item.setLatency(9999); LegacyPlayerListItem pli = new LegacyPlayerListItem(LegacyPlayerListItem.REMOVE_PLAYER, Collections.singletonList(item)); sendPacket(pli); @@ -314,7 +313,7 @@ public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet) { void onActivated() { for (val entry : serverPlayerList.object2IntEntrySet()) { LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(); - item.setDisplayName(entry.getKey()); // TODO: Check Formatting + item.setDisplayName(GsonComponentSerializer.gson().deserialize(entry.getKey())); // TODO: Check Formatting item.setLatency(entry.getIntValue()); LegacyPlayerListItem pli = new LegacyPlayerListItem(LegacyPlayerListItem.ADD_PLAYER, Collections.singletonList(item)); sendPacket(pli); @@ -336,7 +335,7 @@ void onDeactivated() { } private void removeAllEntries() { - for (Component player : serverPlayerList.keySet()) { + for (String player : serverPlayerList.keySet()) { removeEntry(null, player); } for (UUID player : modernServerPlayerList.keySet()) { @@ -411,7 +410,7 @@ void onDeactivated() { for (int index = this.size - 1; index >= 0; index--) { removeEntry(slotUUID[index], slotID[index]); Team t = new Team(); - t.setName(PlainTextComponentSerializer.plainText().serialize(slotID[index])); + t.setName(slotID[index]); t.setMode((byte) 1); sendPacket(t); } @@ -428,12 +427,12 @@ private void updateSize() { // create new slot updateSlot(tabOverlay, index); Team t = new Team(); - t.setName(PlainTextComponentSerializer.plainText().serialize(slotID[index])); + t.setName(slotID[index]); t.setMode((byte) 0); t.setPrefix(tabOverlay.text0[index]); t.setDisplayName(""); t.setSuffix(tabOverlay.text1[index]); - t.setPlayers(new String[]{PlainTextComponentSerializer.plainText().serialize(slotID[index])}); + t.setPlayers(new String[]{slotID[index]}); t.setNameTagVisibility("always"); t.setCollisionRule("always"); if (is13OrLater) { @@ -447,7 +446,7 @@ private void updateSize() { for (int index = this.size - 1; index >= size; index--) { removeEntry(slotUUID[index], slotID[index]); Team t = new Team(); - t.setName(PlainTextComponentSerializer.plainText().serialize(slotID[index])); + t.setName(slotID[index]); t.setMode((byte) 1); sendPacket(t); } @@ -458,9 +457,9 @@ private void updateSize() { private void updateSlot(CustomTabOverlay tabOverlay, int index) { LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUUID[index]); - item.setName(PlainTextComponentSerializer.plainText().serialize(slotID[index])); + item.setName(slotID[index]); Property119Handler.setProperties(item, EMPTY_PROPERTIES); - item.setDisplayName(slotID[index]); // TODO: Check Formatting + item.setDisplayName(GsonComponentSerializer.gson().deserialize(slotID[index])); // TODO: Check Formatting item.setLatency(tabOverlay.ping[index]); LegacyPlayerListItem pli = new LegacyPlayerListItem(LegacyPlayerListItem.ADD_PLAYER, Collections.singletonList(item)); sendPacket(pli); @@ -469,7 +468,7 @@ private void updateSlot(CustomTabOverlay tabOverlay, int index) { private void updateText(CustomTabOverlay tabOverlay, int index) { if (index < size) { Team packet = new Team(); - packet.setName(PlainTextComponentSerializer.plainText().serialize(slotID[index])); + packet.setName(slotID[index]); packet.setMode((byte) 2); packet.setPrefix(tabOverlay.text0[index]); packet.setDisplayName(""); @@ -821,21 +820,21 @@ public boolean isValid() { * @param item the item * @return the name */ - private static Component getName(LegacyPlayerListItem.Item item) { + private static String getName(LegacyPlayerListItem.Item item) { if (item.getDisplayName() != null) { - return item.getDisplayName(); + return GsonComponentSerializer.gson().serialize(item.getDisplayName()); } else if (item.getName() != null) { - return Component.text(item.getName()); + return item.getName(); } else { throw new AssertionError("DisplayName and Username are null"); } } - private static Component getName(UpsertPlayerInfo.Entry entry) { + private static String getName(UpsertPlayerInfo.Entry entry) { if (entry.getDisplayName() != null) { - return entry.getDisplayName(); + return GsonComponentSerializer.gson().serialize(entry.getDisplayName()); } else if (entry.getProfile().getName() != null) { - return Component.text(entry.getProfile().getName()); + return entry.getProfile().getName(); } else { throw new AssertionError("DisplayName and Username are null"); } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java index 968737f8..0c1f2d33 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java @@ -36,8 +36,7 @@ import de.codecrafter47.taboverlay.handler.*; import it.unimi.dsi.fastutil.objects.*; import lombok.*; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -247,7 +246,7 @@ public PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { for (LegacyPlayerListItem.Item item : packet.getItems()) { PlayerListEntry playerListEntry = serverPlayerList.get(item.getUuid()); if (playerListEntry != null) { - playerListEntry.setDisplayName(item.getDisplayName()); + playerListEntry.setDisplayName(GsonComponentSerializer.gson().serialize(item.getDisplayName())); } } break; @@ -291,7 +290,7 @@ public PacketListenerResult onTeamPacket(Team packet) { } try { - this.activeContentHandler.onTeamPreprocess(packet); + this.activeContentHandler.onTeamPacketPreprocess(packet); } catch (Throwable th) { logger.log(Level.SEVERE, "Unexpected error", th); // try recover @@ -349,7 +348,7 @@ public PacketListenerResult onTeamPacket(Team packet) { } try { - return this.activeContentHandler.onTeam(packet); + return this.activeContentHandler.onTeamPacket(packet); } catch (Throwable th) { logger.log(Level.SEVERE, "Unexpected error", th); // try recover @@ -516,7 +515,7 @@ private abstract class AbstractContentOperationModeHandler previous) { items.clear(); for (PlayerListEntry entry : serverPlayerList.values()) { LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(entry.getUuid()); - item.setDisplayName(entry.getDisplayName()); + item.setDisplayName(GsonComponentSerializer.gson().deserialize(entry.getDisplayName())); items.add(item); } packet = new LegacyPlayerListItem(UPDATE_DISPLAY_NAME, items); @@ -892,7 +891,7 @@ PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { dirtySlots.set(index); needUpdate = true; } else { - item.setDisplayName(tabOverlay.text[index]); // TODO: check formatting + item.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: check formatting item.setLatency(tabOverlay.ping[index]); tabOverlay.dirtyFlagsText.clear(index); tabOverlay.dirtyFlagsPing.clear(index); @@ -994,7 +993,7 @@ PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); item1.setName(slotUsername[index] = getCustomSlotUsername(index)); Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); - item1.setDisplayName(tabOverlay.text[index]); // TODO: Check formatting + item1.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check formatting item1.setLatency(tabOverlay.ping[index]); item1.setGameMode(0); LegacyPlayerListItem packet1 = new LegacyPlayerListItem(ADD_PLAYER, Collections.singletonList(item1)); @@ -1036,7 +1035,7 @@ private String getCustomSlotUsername(int index) { } @Override - void onTeamPreprocess(Team packet) { + void onTeamPacketPreprocess(Team packet) { if (!using80Slots) { if (packet.getMode() == 1) { TeamEntry teamEntry = serverTeams.get(packet.getName()); @@ -1070,7 +1069,7 @@ void onTeamPreprocess(Team packet) { } @Override - PacketListenerResult onTeam(Team packet) { + PacketListenerResult onTeamPacket(Team packet) { if (CUSTOM_SLOT_TEAMNAMES.contains(packet.getName())) { throw new AssertionError("Team name collision: " + packet); } @@ -1228,7 +1227,7 @@ void onServerSwitch() { LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); item1.setName(slotUsername[index] = getCustomSlotUsername(index)); Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); - item1.setDisplayName(tabOverlay.text[index]); // TODO: Check formatting + item1.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check formatting item1.setLatency(tabOverlay.ping[index]); item1.setGameMode(0); LegacyPlayerListItem packet1 = new LegacyPlayerListItem(ADD_PLAYER, Collections.singletonList(item1)); @@ -1496,7 +1495,7 @@ void update() { LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); item1.setName(slotUsername[index] = getCustomSlotUsername(index)); Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); - item1.setDisplayName(tabOverlay.text[index]); // TODO: Check Formatting + item1.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check Formatting item1.setLatency(tabOverlay.ping[index]); item1.setGameMode(0); itemQueueAddPlayer.add(item1); @@ -1522,7 +1521,7 @@ void update() { LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); item1.setName(slotUsername[index] = getCustomSlotUsername(index)); Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); - item1.setDisplayName(tabOverlay.text[index]); // TODO: Check Formatting + item1.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check Formatting item1.setLatency(tabOverlay.ping[index]); item1.setGameMode(0); itemQueueAddPlayer.add(item1); @@ -1729,7 +1728,7 @@ void update() { // 4. Update display name LegacyPlayerListItem.Item itemUpdateDisplayName = new LegacyPlayerListItem.Item(viewerUuid); tabOverlay.dirtyFlagsText.clear(highestUsedSlotIndex); - itemUpdateDisplayName.setDisplayName(tabOverlay.text[highestUsedSlotIndex]); // TODO: Check Formatting + itemUpdateDisplayName.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[highestUsedSlotIndex])); // TODO: Check Formatting itemQueueUpdateDisplayName.add(itemUpdateDisplayName); // 5. Update ping LegacyPlayerListItem.Item itemUpdatePing = new LegacyPlayerListItem.Item(viewerUuid); @@ -1772,7 +1771,7 @@ void update() { // 4. Update display name LegacyPlayerListItem.Item itemUpdateDisplayName = new LegacyPlayerListItem.Item(uuid); tabOverlay.dirtyFlagsText.clear(index); - itemUpdateDisplayName.setDisplayName(tabOverlay.text[index]); // TODO: Check Formatting + itemUpdateDisplayName.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check Formatting itemQueueUpdateDisplayName.add(itemUpdateDisplayName); // 5. Update ping LegacyPlayerListItem.Item itemUpdatePing = new LegacyPlayerListItem.Item(uuid); @@ -1830,7 +1829,7 @@ void update() { // 4. Update display name LegacyPlayerListItem.Item itemUpdateDisplayName = new LegacyPlayerListItem.Item(uuid); tabOverlay.dirtyFlagsText.clear(index); - itemUpdateDisplayName.setDisplayName(tabOverlay.text[index]); // TODO: Check Formatting + itemUpdateDisplayName.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check Formatting itemQueueUpdateDisplayName.add(itemUpdateDisplayName); // 5. Update ping LegacyPlayerListItem.Item itemUpdatePing = new LegacyPlayerListItem.Item(uuid); @@ -1874,7 +1873,7 @@ void update() { LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); item1.setName(slotUsername[index] = getCustomSlotUsername(index)); Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); - item1.setDisplayName(tabOverlay.text[index]); // TODO: Check Formatting + item1.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check Formatting item1.setLatency(tabOverlay.ping[index]); item1.setGameMode(0); itemQueueAddPlayer.add(item1); @@ -1911,7 +1910,7 @@ void update() { LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); item1.setName(slotUsername[index] = getCustomSlotUsername(index)); Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); - item1.setDisplayName(tabOverlay.text[index]); // TODO: Check Formatting + item1.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check Formatting item1.setLatency(tabOverlay.ping[index]); item1.setGameMode(0); itemQueueAddPlayer.add(item1); @@ -1923,7 +1922,7 @@ void update() { for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { if (slotState[index] != SlotState.UNUSED) { LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUuid[index]); - item.setDisplayName(tabOverlay.text[index]); // TODO: Check Formatting + item.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check Formatting itemQueueUpdateDisplayName.add(item); } } @@ -1985,7 +1984,7 @@ private void sendQueuedItems() { private abstract class CustomContentTabOverlay extends AbstractContentTabOverlay implements TabOverlayHandle.BatchModifiable { final UUID[] uuid; final Icon[] icon; - final Component[] text; + final String[] text; final int[] ping; final AtomicInteger batchUpdateRecursionLevel; @@ -1999,8 +1998,8 @@ private CustomContentTabOverlay() { this.uuid = new UUID[80]; this.icon = new Icon[80]; Arrays.fill(this.icon, Icon.DEFAULT_STEVE); - this.text = new Component[80]; - Arrays.fill(this.text, Component.empty()); + this.text = new String[80]; + Arrays.fill(this.text, EMPTY_JSON_TEXT); this.ping = new int[80]; this.batchUpdateRecursionLevel = new AtomicInteger(0); this.dirtyFlagSize = true; @@ -2054,7 +2053,7 @@ void setIconInternal(int index, @Nonnull @NonNull Icon icon) { } void setTextInternal(int index, @Nonnull @NonNull String text) { - Component jsonText = LegacyComponentSerializer.legacyAmpersand().deserialize(text.replace(LegacyComponentSerializer.SECTION_CHAR, LegacyComponentSerializer.AMPERSAND_CHAR)); + String jsonText = ChatFormat.formattedTextToJson(text); if (!jsonText.equals(this.text[index])) { this.text[index] = jsonText; dirtyFlagsText.set(index); @@ -2140,7 +2139,7 @@ public void setSize(@Nonnull Dimension size) { if (!oldUsedSlots.get(index)) { uuid[index] = null; icon[index] = Icon.DEFAULT_STEVE; - text[index] = Component.empty(); + text[index] = EMPTY_JSON_TEXT; ping[index] = 0; } } @@ -2151,7 +2150,7 @@ public void setSize(@Nonnull Dimension size) { if (!newUsedSlots.get(index)) { uuid[index] = null; icon[index] = Icon.DEFAULT_STEVE; - text[index] = Component.empty(); + text[index] = EMPTY_JSON_TEXT; ping[index] = 0; } } @@ -2522,12 +2521,12 @@ static class PlayerListEntry { private UUID uuid; private String[][] properties; private String username; - private Component displayName; + private String displayName; private int ping; private int gamemode; private PlayerListEntry(LegacyPlayerListItem.Item item) { - this(item.getUuid(), null, item.getName(), item.getDisplayName(), item.getLatency(), item.getGameMode()); // TODO: Check Display Name + this(item.getUuid(), null, item.getName(), GsonComponentSerializer.gson().serialize(item.getDisplayName()), item.getLatency(), item.getGameMode()); // TODO: Check Display Name properties = Property119Handler.getProperties(item); } } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java index b3189434..a059d7fb 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java @@ -41,9 +41,7 @@ import de.codecrafter47.taboverlay.handler.*; import it.unimi.dsi.fastutil.objects.*; import lombok.*; - -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -729,7 +727,7 @@ void update() { UpsertPlayerInfo.Entry item = new UpsertPlayerInfo.Entry(customSlotUuid); GameProfile profile = new GameProfile(customSlotUuid, slotUsername[index] = getCustomSlotUsername(index), toPropertiesList(icon.getTextureProperty())); item.setProfile(profile); - item.setDisplayName(tabOverlay.text[index]); + item.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); item.setLatency(tabOverlay.ping[index]); item.setGameMode(0); item.setListed(true); @@ -742,7 +740,7 @@ void update() { for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { if (slotState[index] != SlotState.UNUSED) { UpsertPlayerInfo.Entry item = new UpsertPlayerInfo.Entry(slotUuid[index]); - item.setDisplayName(tabOverlay.text[index]); + item.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); itemQueueUpdateDisplayName.add(item); } } @@ -806,7 +804,7 @@ private boolean isExperimentalTabCompleteSmileys() { private abstract class CustomContentTabOverlay extends AbstractContentTabOverlay implements TabOverlayHandle.BatchModifiable { final Icon[] icon; - final Component[] text; + final String[] text; final int[] ping; final AtomicInteger batchUpdateRecursionLevel; @@ -818,8 +816,8 @@ private abstract class CustomContentTabOverlay extends AbstractContentTabOverlay private CustomContentTabOverlay() { this.icon = new Icon[80]; Arrays.fill(this.icon, Icon.DEFAULT_STEVE); - this.text = new Component[80]; - Arrays.fill(this.text, Component.empty()); + this.text = new String[80]; + Arrays.fill(this.text, EMPTY_JSON_TEXT); this.ping = new int[80]; this.batchUpdateRecursionLevel = new AtomicInteger(0); this.dirtyFlagSize = true; @@ -864,7 +862,7 @@ void setIconInternal(int index, @Nonnull @NonNull Icon icon) { } void setTextInternal(int index, @Nonnull @NonNull String text) { - Component jsonText = LegacyComponentSerializer.legacyAmpersand().deserialize(text.replace(LegacyComponentSerializer.SECTION_CHAR, LegacyComponentSerializer.AMPERSAND_CHAR)); + String jsonText = ChatFormat.formattedTextToJson(text); if (!jsonText.equals(this.text[index])) { this.text[index] = jsonText; dirtyFlagsText.set(index); @@ -933,7 +931,7 @@ public void setSize(@Nonnull Dimension size) { for (int index = newUsedSlots.nextSetBit(0); index >= 0; index = newUsedSlots.nextSetBit(index + 1)) { if (!oldUsedSlots.get(index)) { icon[index] = Icon.DEFAULT_STEVE; - text[index] = Component.empty(); + text[index] = EMPTY_JSON_TEXT; ping[index] = 0; } } @@ -943,7 +941,7 @@ public void setSize(@Nonnull Dimension size) { for (int index = oldUsedSlots.nextSetBit(0); index >= 0; index = oldUsedSlots.nextSetBit(index + 1)) { if (!newUsedSlots.get(index)) { icon[index] = Icon.DEFAULT_STEVE; - text[index] = Component.empty(); + text[index] = EMPTY_JSON_TEXT; ping[index] = 0; } } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/DataManager.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/DataManager.java index 0ec226f7..cac3bada 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/DataManager.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/DataManager.java @@ -143,7 +143,7 @@ private class LocalPlayerDataAccess extends AbstractVelocityDataAccess { addProvider(BTLPVelocityDataKeys.ThirdPartyPlaceholderBungee, (player, dataKey) -> api.resolveCustomPlaceholder(dataKey.getParameter(), player)); addProvider(BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN_PLAYER_CONFIG, (player, dataKey) -> permanentlyHiddenPlayers.contains(player.getUsername()) || permanentlyHiddenPlayers.contains(player.getUniqueId().toString())); - if (plugin.getProxy().getPluginManager().getPlugin("RedisBungee").isPresent()) { + if (plugin.getProxy().getPluginManager().getPlugin("redisbungee").isPresent()) { addProvider(BTLPVelocityDataKeys.DATA_KEY_RedisBungee_ServerId, player -> RedisBungeeAPI.getRedisBungeeApi().getServerNameFor(player.getUniqueId())); } } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/ServerStateManager.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/ServerStateManager.java index 43b9955e..783e033b 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/ServerStateManager.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/ServerStateManager.java @@ -78,7 +78,7 @@ private synchronized PingTask getServerState(String serverName) { if (delay <= 0 || delay > 10) { delay = 10; } - task = new PingTask(plugin.getProxy(), server); + task = new PingTask(plugin, server); serverState.put(serverName, task); task.task = plugin.getProxy().getScheduler().buildTask(plugin, task).delay(delay, TimeUnit.SECONDS).repeat(delay, TimeUnit.SECONDS).schedule(); } @@ -87,15 +87,15 @@ private synchronized PingTask getServerState(String serverName) { public static class PingTask implements Runnable { - private final ProxyServer proxyServer; + private final VelocityPlugin plugin; private final RegisteredServer server; private boolean online = true; private int maxPlayers = Integer.MAX_VALUE; private int onlinePlayers = 0; private ScheduledTask task; - public PingTask(ProxyServer proxyServer, RegisteredServer server) { - this.proxyServer = proxyServer; + public PingTask(VelocityPlugin plugin, RegisteredServer server) { + this.plugin = plugin; this.server = server; } @@ -113,7 +113,7 @@ public int getOnlinePlayers() { @Override public void run() { - if (!VelocityPlugin.isProxyRunning(proxyServer)) return; + if (!plugin.isProxyRunning()) return; server.ping().whenComplete((serverPing, throwable) -> { if (throwable != null) { online = false; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/PlayerPlaceholderResolver.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/PlayerPlaceholderResolver.java index f7727fcc..34628370 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/PlayerPlaceholderResolver.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/PlayerPlaceholderResolver.java @@ -56,10 +56,10 @@ public PlayerPlaceholderResolver(ServerPlaceholderResolver serverPlaceholderReso this.serverPlaceholderResolver = serverPlaceholderResolver; this.cache = cache; addPlaceholder("ping", create(VelocityData.Velocity_Ping)); - addPlaceholder("luckpermsbungee_prefix", create(VelocityData.LuckPerms_Prefix)); - addPlaceholder("luckpermsbungee_suffix", create(VelocityData.LuckPerms_Suffix)); - addPlaceholder("luckpermsbungee_primary_group", create(VelocityData.LuckPerms_PrimaryGroup)); - addPlaceholder("luckpermsbungee_primary_group_weight", create(VelocityData.LuckPerms_Weight)); + addPlaceholder("luckpermsvelocity_prefix", create(VelocityData.LuckPerms_Prefix)); + addPlaceholder("luckpermsvelocity_suffix", create(VelocityData.LuckPerms_Suffix)); + addPlaceholder("luckpermsvelocity_primary_group", create(VelocityData.LuckPerms_PrimaryGroup)); + addPlaceholder("luckpermsvelocity_primary_group_weight", create(VelocityData.LuckPerms_Weight)); addPlaceholder("client_version", create(BTLPVelocityDataKeys.DATA_KEY_CLIENT_VERSION)); addPlaceholder("client_version_below_1_8", create(BTLPVelocityDataKeys.DATA_KEY_CLIENT_VERSION_BELOW_1_8)); addPlaceholder("client_version_atleast_1_8", create(BTLPVelocityDataKeys.DATA_KEY_CLIENT_VERSION_BELOW_1_8, b -> !b, TypeToken.BOOLEAN)); diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/VelocityPlugin.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/VelocityPlugin.java index 1d07bc4d..644d1ccd 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/VelocityPlugin.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/VelocityPlugin.java @@ -28,13 +28,13 @@ public VelocityPlugin(ProxyServer proxy, Logger logger, Path dataDirectory, Stri this.version = version; } - public static boolean isProxyRunning(ProxyServer proxyServer){ + public boolean isProxyRunning(){ try { Class velocityServer = Class.forName("com.velocitypowered.proxy.VelocityServer"); Field shutdownInProgress = velocityServer.getDeclaredField("shutdownInProgress"); shutdownInProgress.setAccessible(true); - return !((AtomicBoolean) shutdownInProgress.get(proxyServer)).get(); + return !((AtomicBoolean) shutdownInProgress.get(proxy)).get(); } catch (NoSuchFieldException | ClassNotFoundException | IllegalAccessException ignored) { } // Return not running if it can't grab the shutdownInProgress value; return false; From 404e0f9711bed17f6189738d626d347a3b7d9cda Mon Sep 17 00:00:00 2001 From: Brent P Date: Tue, 3 Jan 2023 23:14:47 -0500 Subject: [PATCH 06/22] Fix Redis Server ID --- .../codecrafter47/bungeetablistplus/managers/DataManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/DataManager.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/DataManager.java index cac3bada..6889f7de 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/DataManager.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/DataManager.java @@ -144,7 +144,7 @@ private class LocalPlayerDataAccess extends AbstractVelocityDataAccess { addProvider(BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN_PLAYER_CONFIG, (player, dataKey) -> permanentlyHiddenPlayers.contains(player.getUsername()) || permanentlyHiddenPlayers.contains(player.getUniqueId().toString())); if (plugin.getProxy().getPluginManager().getPlugin("redisbungee").isPresent()) { - addProvider(BTLPVelocityDataKeys.DATA_KEY_RedisBungee_ServerId, player -> RedisBungeeAPI.getRedisBungeeApi().getServerNameFor(player.getUniqueId())); + addProvider(BTLPVelocityDataKeys.DATA_KEY_RedisBungee_ServerId, player -> RedisBungeeAPI.getRedisBungeeApi().getServerId()); } } } From 62590b48b928ad489a8524afab5e33702d6dd828 Mon Sep 17 00:00:00 2001 From: Brent P Date: Thu, 5 Jan 2023 00:42:20 -0500 Subject: [PATCH 07/22] Fix Packet Listener and target java 11 like Velocity --- bootstrap-velocity/build.gradle | 4 +- .../bungeetablistplus/BootstrapPlugin.java | 32 ++++++++------- .../AbstractLegacyTabOverlayHandler.java | 8 ++-- .../handler/AbstractTabOverlayHandler.java | 11 +++--- .../handler/NewTabOverlayHandler.java | 4 +- .../listener/TabListListener.java | 11 ++++++ .../managers/TabViewManager.java | 1 - .../protocol/PacketListener.java | 39 ++++++++----------- .../protocol/PacketWrapper.java | 26 ------------- 9 files changed, 59 insertions(+), 77 deletions(-) delete mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketWrapper.java diff --git a/bootstrap-velocity/build.gradle b/bootstrap-velocity/build.gradle index 3dbca21e..c2b7b893 100644 --- a/bootstrap-velocity/build.gradle +++ b/bootstrap-velocity/build.gradle @@ -13,8 +13,8 @@ dependencies { } compileJava { - sourceCompatibility = '1.8' - targetCompatibility = '1.8' + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } task processSource(type: Sync) { diff --git a/bootstrap-velocity/src/main/java/codecrafter47/bungeetablistplus/BootstrapPlugin.java b/bootstrap-velocity/src/main/java/codecrafter47/bungeetablistplus/BootstrapPlugin.java index 61c778cd..0928c09f 100644 --- a/bootstrap-velocity/src/main/java/codecrafter47/bungeetablistplus/BootstrapPlugin.java +++ b/bootstrap-velocity/src/main/java/codecrafter47/bungeetablistplus/BootstrapPlugin.java @@ -34,22 +34,24 @@ import java.nio.file.Path; @Plugin( - id = "bungeetablistplus", - name = "BungeeTabListPlus", - version = "@VERSION@", - dependencies = { - @Dependency(id = "redisbungee", optional = true), - @Dependency(id = "luckperms", optional = true), - @Dependency(id = "geyser", optional = true), - @Dependency(id = "floodgate", optional = true), - @Dependency(id = "viaversion", optional = true) - }, - authors = "CodeCrafter47 & proferabg" + id = "bungeetablistplus", + name = "BungeeTabListPlus", + authors = "CodeCrafter47 & proferabg", + version = "@VERSION@", + dependencies = { + @Dependency(id = "redisbungee", optional = true), + @Dependency(id = "luckperms", optional = true), + @Dependency(id = "geyser", optional = true), + @Dependency(id = "floodgate", optional = true), + @Dependency(id = "viaversion", optional = true) + } ) public class BootstrapPlugin extends VelocityPlugin { private final Metrics.Factory metricsFactory; + private static final String NO_RELOAD_PLAYERS = "Cannot reload BungeeTabListPlus while players are online."; + @Inject public BootstrapPlugin(final ProxyServer server, final Logger logger, final @DataDirectory Path dataDirectory, final Metrics.Factory metricsFactory) { super(server, logger, dataDirectory, BootstrapPlugin.class.getAnnotation(Plugin.class).version()); @@ -58,14 +60,14 @@ public BootstrapPlugin(final ProxyServer server, final Logger logger, final @Dat @Subscribe public void onProxyInitialization(final ProxyInitializeEvent event) { - if (Float.parseFloat(System.getProperty("java.class.version")) < 52.0) { - getLogger().error("§cBungeeTabListPlus requires Java 8 or above. Please download and install it!"); + if (Float.parseFloat(System.getProperty("java.class.version")) < 55.0) { + getLogger().error("§cBungeeTabListPlus requires Java 11 or above. Please download and install it!"); getLogger().error("Disabling plugin!"); return; } if (!getProxy().getAllPlayers().isEmpty()) { for (Player player : getProxy().getAllPlayers()) { - player.disconnect(Component.text("Cannot reload BungeeTabListPlus while players are online.")); + player.disconnect(Component.text(NO_RELOAD_PLAYERS)); } } getProxy().getPluginManager().getPlugin("BungeeTabListPlus"); @@ -82,7 +84,7 @@ public void onProxyShutdown(final ProxyShutdownEvent event) { getLogger().error("You cannot use ServerUtils to reload BungeeTabListPlus. Use /btlp reload instead."); if (!getProxy().getAllPlayers().isEmpty()) { for (Player proxiedPlayer : getProxy().getAllPlayers()) { - proxiedPlayer.disconnect(Component.text("Cannot reload BungeeTabListPlus while players are online.")); + proxiedPlayer.disconnect(Component.text(NO_RELOAD_PLAYERS)); } } } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractLegacyTabOverlayHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractLegacyTabOverlayHandler.java index fc88e5c4..06abcaeb 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractLegacyTabOverlayHandler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractLegacyTabOverlayHandler.java @@ -253,7 +253,7 @@ private void removeEntry(UUID uuid, String player) { item.setName(player); item.setDisplayName(GsonComponentSerializer.gson().deserialize(player)); item.setLatency(9999); - LegacyPlayerListItem pli = new LegacyPlayerListItem(LegacyPlayerListItem.REMOVE_PLAYER, Collections.singletonList(item)); + LegacyPlayerListItem pli = new LegacyPlayerListItem(LegacyPlayerListItem.REMOVE_PLAYER, List.of(item)); sendPacket(pli); } @@ -315,7 +315,7 @@ void onActivated() { LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(); item.setDisplayName(GsonComponentSerializer.gson().deserialize(entry.getKey())); // TODO: Check Formatting item.setLatency(entry.getIntValue()); - LegacyPlayerListItem pli = new LegacyPlayerListItem(LegacyPlayerListItem.ADD_PLAYER, Collections.singletonList(item)); + LegacyPlayerListItem pli = new LegacyPlayerListItem(LegacyPlayerListItem.ADD_PLAYER, List.of(item)); sendPacket(pli); } for (val entry : modernServerPlayerList.entrySet()) { @@ -324,7 +324,7 @@ void onActivated() { item.setGameMode(entry.getValue().gamemode); item.setLatency(entry.getValue().latency); Property119Handler.setProperties(item, EMPTY_PROPERTIES); - LegacyPlayerListItem pli = new LegacyPlayerListItem(LegacyPlayerListItem.ADD_PLAYER, Collections.singletonList(item)); + LegacyPlayerListItem pli = new LegacyPlayerListItem(LegacyPlayerListItem.ADD_PLAYER, List.of(item)); sendPacket(pli); } } @@ -461,7 +461,7 @@ private void updateSlot(CustomTabOverlay tabOverlay, int index) { Property119Handler.setProperties(item, EMPTY_PROPERTIES); item.setDisplayName(GsonComponentSerializer.gson().deserialize(slotID[index])); // TODO: Check Formatting item.setLatency(tabOverlay.ping[index]); - LegacyPlayerListItem pli = new LegacyPlayerListItem(LegacyPlayerListItem.ADD_PLAYER, Collections.singletonList(item)); + LegacyPlayerListItem pli = new LegacyPlayerListItem(LegacyPlayerListItem.ADD_PLAYER, List.of(item)); sendPacket(pli); } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java index 0c1f2d33..0046fe21 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java @@ -36,6 +36,7 @@ import de.codecrafter47.taboverlay.handler.*; import it.unimi.dsi.fastutil.objects.*; import lombok.*; +import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import javax.annotation.Nonnull; @@ -708,7 +709,7 @@ void onActivated(AbstractContentOperationModeHandler previous) { items.clear(); for (PlayerListEntry entry : serverPlayerList.values()) { LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(entry.getUuid()); - item.setDisplayName(GsonComponentSerializer.gson().deserialize(entry.getDisplayName())); + item.setDisplayName((entry.getDisplayName() != null && !entry.getDisplayName().equalsIgnoreCase("null")) ? GsonComponentSerializer.gson().deserialize(entry.getDisplayName()) : Component.empty()); items.add(item); } packet = new LegacyPlayerListItem(UPDATE_DISPLAY_NAME, items); @@ -996,10 +997,10 @@ PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { item1.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check formatting item1.setLatency(tabOverlay.ping[index]); item1.setGameMode(0); - LegacyPlayerListItem packet1 = new LegacyPlayerListItem(ADD_PLAYER, Collections.singletonList(item1)); + LegacyPlayerListItem packet1 = new LegacyPlayerListItem(ADD_PLAYER, List.of(item1)); sendPacket(packet1); if (is18) { - packet1 = new LegacyPlayerListItem(UPDATE_DISPLAY_NAME, Collections.singletonList(item1)); + packet1 = new LegacyPlayerListItem(UPDATE_DISPLAY_NAME, List.of(item1)); sendPacket(packet1); } } @@ -1230,10 +1231,10 @@ void onServerSwitch() { item1.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check formatting item1.setLatency(tabOverlay.ping[index]); item1.setGameMode(0); - LegacyPlayerListItem packet1 = new LegacyPlayerListItem(ADD_PLAYER, Collections.singletonList(item1)); + LegacyPlayerListItem packet1 = new LegacyPlayerListItem(ADD_PLAYER, List.of(item1)); sendPacket(packet1); if (is18) { - packet1 = new LegacyPlayerListItem(UPDATE_DISPLAY_NAME, Collections.singletonList(item1)); + packet1 = new LegacyPlayerListItem(UPDATE_DISPLAY_NAME, List.of(item1)); sendPacket(packet1); } } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java index a059d7fb..33c7c682 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java @@ -1212,9 +1212,9 @@ private static List toPropertiesList(ProfileProperty textu if (textureProperty == null) { return new ArrayList<>(); } else if (textureProperty.isSigned()) { - return Collections.singletonList(new GameProfile.Property(textureProperty.getName(), textureProperty.getValue(), textureProperty.getSignature())); + return List.of(new GameProfile.Property(textureProperty.getName(), textureProperty.getValue(), textureProperty.getSignature())); } else { - return Collections.singletonList(new GameProfile.Property(textureProperty.getName(), textureProperty.getValue(), "")); + return List.of(new GameProfile.Property(textureProperty.getName(), textureProperty.getValue(), "")); } } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/listener/TabListListener.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/listener/TabListListener.java index 74e82271..f7ab06ef 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/listener/TabListListener.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/listener/TabListListener.java @@ -20,16 +20,20 @@ import codecrafter47.bungeetablistplus.player.VelocityPlayer; import codecrafter47.bungeetablistplus.tablist.ExcludedServersTabOverlayProvider; import codecrafter47.bungeetablistplus.util.GeyserCompat; +import codecrafter47.bungeetablistplus.util.ProxyServer; import com.velocitypowered.api.event.PostOrder; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.connection.DisconnectEvent; import com.velocitypowered.api.event.connection.PostLoginEvent; import com.velocitypowered.api.event.proxy.ProxyReloadEvent; +import com.velocitypowered.api.proxy.Player; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import de.codecrafter47.taboverlay.TabView; import de.codecrafter47.taboverlay.config.platform.EventListener; import net.kyori.adventure.text.Component; +import java.util.concurrent.TimeUnit; + public class TabListListener { private final BungeeTabListPlus btlp; @@ -41,6 +45,13 @@ public TabListListener(BungeeTabListPlus btlp) { @Subscribe(order = PostOrder.LATE) public void onPlayerJoin(PostLoginEvent e) { try { + // Hacks -> Remove everyone from all tablists + btlp.getProxy().getScheduler().buildTask(btlp.getPlugin(), () -> { + for(Player tmpPlayer : btlp.getProxy().getAllPlayers()){ + tmpPlayer.getTabList().clearAll(); + } + }).delay(2, TimeUnit.SECONDS).schedule(); + VelocityPlayer player = btlp.getBungeePlayerProvider().onPlayerConnected(e.getPlayer()); if (GeyserCompat.isBedrockPlayer(e.getPlayer().getUniqueId())) { diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/TabViewManager.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/TabViewManager.java index cf4fedcc..c052ab17 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/TabViewManager.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/TabViewManager.java @@ -101,7 +101,6 @@ public void onServerConnected(ServerPostConnectEvent event) { PacketHandler packetHandler = tabView.packetHandler; PacketListener packetListener = new PacketListener(server, packetHandler, player); - wrapper.getChannel().pipeline().addBefore(Connections.HANDLER, "btlp-packet-listener", packetListener); packetHandler.onServerSwitch(protocolVersionProvider.has113OrLater(player)); diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListener.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListener.java index 6123da7d..4da5e33f 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListener.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListener.java @@ -21,6 +21,7 @@ import codecrafter47.bungeetablistplus.util.ReflectionUtil; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; +import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; @@ -31,7 +32,7 @@ import java.util.List; -public class PacketListener extends MessageToMessageDecoder { +public class PacketListener extends MessageToMessageDecoder { private final VelocityServerConnection connection; private final PacketHandler handler; private final Player player; @@ -43,53 +44,47 @@ public PacketListener(VelocityServerConnection connection, PacketHandler handler } @Override - protected void decode(ChannelHandlerContext ctx, PacketWrapper packetWrapper, List out) { - boolean shouldRelease = true; + protected void decode(ChannelHandlerContext ctx, MinecraftPacket packet, List out) { try { if (connection.isActive()) { - if (packetWrapper.packet != null) { + if (packet != null) { PacketListenerResult result = PacketListenerResult.PASS; boolean handled = false; - if (packetWrapper.packet instanceof Team) { - result = handler.onTeamPacket((Team) packetWrapper.packet); + if (packet instanceof Team) { + result = handler.onTeamPacket((Team) packet); if (result == PacketListenerResult.MODIFIED) { - ReflectionUtil.getChannelWrapper(player).getChannel().write(packetWrapper.packet); + ReflectionUtil.getChannelWrapper(player).getChannel().write(packet); } if (result != PacketListenerResult.PASS) { return; } - } else if (packetWrapper.packet instanceof LegacyPlayerListItem) { - result = handler.onPlayerListPacket((LegacyPlayerListItem) packetWrapper.packet); + } else if (packet instanceof LegacyPlayerListItem) { + result = handler.onPlayerListPacket((LegacyPlayerListItem) packet); handled = true; - } else if (packetWrapper.packet instanceof HeaderAndFooter) { - result = handler.onPlayerListHeaderFooterPacket((HeaderAndFooter) packetWrapper.packet); + } else if (packet instanceof HeaderAndFooter) { + result = handler.onPlayerListHeaderFooterPacket((HeaderAndFooter) packet); handled = true; - } else if (packetWrapper.packet instanceof UpsertPlayerInfo) { - result = handler.onPlayerListUpdatePacket((UpsertPlayerInfo) packetWrapper.packet); + } else if (packet instanceof UpsertPlayerInfo) { + result = handler.onPlayerListUpdatePacket((UpsertPlayerInfo) packet); handled = true; - } else if (packetWrapper.packet instanceof RemovePlayerInfo) { - result = handler.onPlayerListRemovePacket((RemovePlayerInfo) packetWrapper.packet); + } else if (packet instanceof RemovePlayerInfo) { + result = handler.onPlayerListRemovePacket((RemovePlayerInfo) packet); handled = true; } if (handled) { if (result != PacketListenerResult.CANCEL) { - ReflectionUtil.getChannelWrapper(player).getChannel().write(packetWrapper.packet); + ReflectionUtil.getChannelWrapper(player).getChannel().write(packet); } return; } } } - out.add(packetWrapper); - shouldRelease = false; + out.add(packet); } catch (Throwable th) { BungeeTabListPlus.getInstance().reportError(th); - } finally { - if (shouldRelease) { - packetWrapper.trySingleRelease(); - } } } } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketWrapper.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketWrapper.java deleted file mode 100644 index 283b2712..00000000 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketWrapper.java +++ /dev/null @@ -1,26 +0,0 @@ -package codecrafter47.bungeetablistplus.protocol; - -import com.velocitypowered.proxy.protocol.MinecraftPacket; -import io.netty.buffer.ByteBuf; -import lombok.RequiredArgsConstructor; -import lombok.Setter; - -@RequiredArgsConstructor -public class PacketWrapper -{ - - public final MinecraftPacket packet; - public final ByteBuf buf; - @Setter - private boolean released; - - public void trySingleRelease() - { - if ( !released ) - { - buf.release(); - released = true; - } - } -} - From 0f1839cdddbaed699f692bf8230412d858e6b9c4 Mon Sep 17 00:00:00 2001 From: Brent P Date: Thu, 5 Jan 2023 11:08:49 -0500 Subject: [PATCH 08/22] Remove SortingRuleAliasProcessor + Add Team Packet Registration --- .../bungeetablistplus/BungeeTabListPlus.java | 6 +- .../compat/SortingRuleAliasProcessor.java | 74 ------ .../AbstractLegacyTabOverlayHandler.java | 2 +- .../handler/AbstractTabOverlayHandler.java | 2 +- .../LowMemoryTabOverlayHandlerImpl.java | 2 +- .../handler/NewTabOverlayHandler.java | 2 +- .../protocol/AbstractPacketHandler.java | 1 - .../protocol/PacketHandler.java | 1 - .../protocol/PacketListener.java | 1 - .../bungeetablistplus/protocol/Team.java | 220 ++++++++++++++++++ .../util/ReflectionUtil.java | 51 ++++ 11 files changed, 279 insertions(+), 83 deletions(-) delete mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/compat/SortingRuleAliasProcessor.java create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/Team.java diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java index 721fe7a8..0eef023a 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java @@ -21,7 +21,6 @@ import codecrafter47.bungeetablistplus.cache.Cache; import codecrafter47.bungeetablistplus.command.CommandBungeeTabListPlus; import codecrafter47.bungeetablistplus.common.network.BridgeProtocolConstants; -import codecrafter47.bungeetablistplus.compat.SortingRuleAliasProcessor; import codecrafter47.bungeetablistplus.config.MainConfig; import codecrafter47.bungeetablistplus.config.PlayersByServerComponentConfiguration; import codecrafter47.bungeetablistplus.data.BTLPVelocityDataKeys; @@ -38,6 +37,7 @@ import codecrafter47.bungeetablistplus.updater.UpdateNotifier; import codecrafter47.bungeetablistplus.util.ExceptionHandlingEventExecutor; import codecrafter47.bungeetablistplus.util.MatchingStringsCollection; +import codecrafter47.bungeetablistplus.util.ReflectionUtil; import codecrafter47.bungeetablistplus.util.VelocityPlugin; import codecrafter47.bungeetablistplus.version.VelocityProtocolVersionProvider; import codecrafter47.bungeetablistplus.version.ProtocolVersionProvider; @@ -239,7 +239,6 @@ public void onEnable() { .playerInvisibleDataKey(BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN) .playerCanSeeInvisibleDataKey(BTLPVelocityDataKeys.permission("bungeetablistplus.seevanished")) .component(new ComponentSpec("!players_by_server", PlayersByServerComponentConfiguration.class)) - .sortingRulePreprocessor(new SortingRuleAliasProcessor()) .build(); yaml = ConfigTabOverlayManager.constructYamlInstance(options); @@ -342,6 +341,9 @@ public void onEnable() { } configTabOverlayManager.reloadConfigs(ImmutableSet.of(tabLists)); + // Hacks to get around no Team packet in Velocity + ReflectionUtil.injectTeamPacketRegistry(); + getProxy().getEventManager().register(plugin, new TabListListener(this)); } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/compat/SortingRuleAliasProcessor.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/compat/SortingRuleAliasProcessor.java deleted file mode 100644 index f0c20541..00000000 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/compat/SortingRuleAliasProcessor.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (C) 2020 Florian Stober - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package codecrafter47.bungeetablistplus.compat; - -import com.google.common.collect.ImmutableMap; -import de.codecrafter47.taboverlay.config.ErrorHandler; -import de.codecrafter47.taboverlay.config.SortingRulePreprocessor; -import lombok.Value; -import org.yaml.snakeyaml.error.Mark; - -public class SortingRuleAliasProcessor implements SortingRulePreprocessor { - - private static final ImmutableMap map = ImmutableMap.builder() - .put("you", new RewriteData("name viewer-first", true)) - .put("youfirst", new RewriteData("name viewer-first", false)) - .put("alpha", new RewriteData("name", true)) - .put("alphabet", new RewriteData("name", true)) - .put("alphabetic", new RewriteData("name", true)) - .put("alphabetical", new RewriteData("name", true)) - .put("alphabetically", new RewriteData("name", false)) - .put("teamfirst", new RewriteData("team viewer-first", false)) - .put("teams", new RewriteData("team", false)) - .put("factionfirst", new RewriteData("faction_name viewer-first", false)) - .put("factions", new RewriteData("faction_name", false)) - .put("worldname", new RewriteData("world", true)) - .put("playerworld", new RewriteData("world viewer-first", true)) - .put("playerworldfirst", new RewriteData("world viewer-first", true)) - .put("serveralphabetically", new RewriteData("server", true)) - .put("playerserverfirst", new RewriteData("server viewer-first", true)) - .put("afklast", new RewriteData("essentials_afk as number asc", true)) - .put("vaultgroupinfo", new RewriteData("vault_primary_group_weight asc", true)) - .put("vaultgroupinforeversed", new RewriteData("vault_primary_group_weight desc", true)) - .put("bungeepermsgroupinfo", new RewriteData("bungeeperms_primary_group_weight asc", true)) - .put("bungeepermsgroupinforeversed", new RewriteData("bungeeperms_primary_group_weight desc", true)) - .put("luckpermsgroupinfo", new RewriteData("luckpermsvelocity_primary_group_weight asc", true)) - .put("luckpermsgroupinforeversed", new RewriteData("luckpermsvelocity_primary_group_weight desc", true)) - .put("vaultprefix", new RewriteData("vault_prefix asc", true)) - .put("connectedfirst", new RewriteData("session_duration_total_seconds desc", false)) - .put("connectedlast", new RewriteData("session_duration_total_seconds asc", false)) - .build(); - - @Override - public String process(String sortingRule, ErrorHandler errorHandler, Mark mark) { - RewriteData rewriteData = map.get(sortingRule.toLowerCase()); - if (rewriteData != null) { - if (rewriteData.deprecated) { - errorHandler.addWarning("Sorting rule '" + sortingRule + "' has been deprecated. Use '" + rewriteData.rewrite + "' instead.", mark); - } - return rewriteData.rewrite; - } - return sortingRule; - } - - @Value - private static class RewriteData { - String rewrite; - boolean deprecated; - } -} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractLegacyTabOverlayHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractLegacyTabOverlayHandler.java index 06abcaeb..5bc16a07 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractLegacyTabOverlayHandler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractLegacyTabOverlayHandler.java @@ -19,6 +19,7 @@ import codecrafter47.bungeetablistplus.protocol.PacketHandler; import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; +import codecrafter47.bungeetablistplus.protocol.Team; import codecrafter47.bungeetablistplus.util.ColorParser; import codecrafter47.bungeetablistplus.util.ConcurrentBitSet; import codecrafter47.bungeetablistplus.util.Property119Handler; @@ -28,7 +29,6 @@ import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; -import com.velocitypowered.proxy.protocol.packet.Team; import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; import de.codecrafter47.taboverlay.Icon; import de.codecrafter47.taboverlay.config.misc.ChatFormat; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java index 0046fe21..e65c1721 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java @@ -19,6 +19,7 @@ import codecrafter47.bungeetablistplus.protocol.PacketHandler; import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; +import codecrafter47.bungeetablistplus.protocol.Team; import codecrafter47.bungeetablistplus.util.BitSet; import codecrafter47.bungeetablistplus.util.ConcurrentBitSet; import codecrafter47.bungeetablistplus.util.Property119Handler; @@ -28,7 +29,6 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; -import com.velocitypowered.proxy.protocol.packet.Team; import de.codecrafter47.taboverlay.Icon; import de.codecrafter47.taboverlay.ProfileProperty; import de.codecrafter47.taboverlay.config.misc.ChatFormat; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LowMemoryTabOverlayHandlerImpl.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LowMemoryTabOverlayHandlerImpl.java index 1498419f..7dc7282d 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LowMemoryTabOverlayHandlerImpl.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LowMemoryTabOverlayHandlerImpl.java @@ -18,8 +18,8 @@ package codecrafter47.bungeetablistplus.handler; import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; +import codecrafter47.bungeetablistplus.protocol.Team; import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.proxy.protocol.packet.Team; import java.util.UUID; import java.util.concurrent.Executor; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java index 33c7c682..fbbd886c 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java @@ -20,6 +20,7 @@ import codecrafter47.bungeetablistplus.BungeeTabListPlus; import codecrafter47.bungeetablistplus.protocol.PacketHandler; import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; +import codecrafter47.bungeetablistplus.protocol.Team; import codecrafter47.bungeetablistplus.util.BitSet; import codecrafter47.bungeetablistplus.util.ConcurrentBitSet; import codecrafter47.bungeetablistplus.util.ReflectionUtil; @@ -32,7 +33,6 @@ import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; -import com.velocitypowered.proxy.protocol.packet.Team; import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; import de.codecrafter47.taboverlay.Icon; import de.codecrafter47.taboverlay.ProfileProperty; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/AbstractPacketHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/AbstractPacketHandler.java index 432828e7..50b6812a 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/AbstractPacketHandler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/AbstractPacketHandler.java @@ -20,7 +20,6 @@ import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; -import com.velocitypowered.proxy.protocol.packet.Team; import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; import lombok.NonNull; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketHandler.java index d8d27cea..2488d985 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketHandler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketHandler.java @@ -20,7 +20,6 @@ import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; -import com.velocitypowered.proxy.protocol.packet.Team; import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; public interface PacketHandler { diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListener.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListener.java index 4da5e33f..2ca2f474 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListener.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListener.java @@ -25,7 +25,6 @@ import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; -import com.velocitypowered.proxy.protocol.packet.Team; import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToMessageDecoder; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/Team.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/Team.java new file mode 100644 index 00000000..3a29b7a2 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/Team.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2018-2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.protocol; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +public class Team implements MinecraftPacket { + + public static final byte CREATE = 0; + public static final byte REMOVE = 1; + public static final byte UPDATE_INFO = 2; + public static final byte ADD_PLAYER = 3; + public static final byte REMOVE_PLAYER = 4; + + private String name; + private byte mode; + private String displayName; + private String prefix; + private String suffix; + private String nameTagVisibility; + private String collisionRule; + private int color; + private byte friendlyFire; + private String[] players; + + public Team() { + } + + public Team(String name) { + this.name = name; + this.mode = REMOVE; + } + + public Team(String name, byte mode) { + this.name = name; + this.mode = mode; + } + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { + name = ProtocolUtils.readString(buf); + mode = buf.readByte(); + if (mode == CREATE || mode == UPDATE_INFO) { + displayName = ProtocolUtils.readString(buf); + if (version.compareTo(ProtocolVersion.MINECRAFT_1_13) < 0) { + prefix = ProtocolUtils.readString(buf); + suffix = ProtocolUtils.readString(buf); + } + friendlyFire = buf.readByte(); + nameTagVisibility = ProtocolUtils.readString(buf); + if (version.compareTo(ProtocolVersion.MINECRAFT_1_9) >= 0) { + collisionRule = ProtocolUtils.readString(buf); + } + color = ( version.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0 ) ? ProtocolUtils.readVarInt(buf) : buf.readByte(); + if ( version.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0 ) { + prefix = ProtocolUtils.readString(buf); + suffix = ProtocolUtils.readString(buf); + } + } + if (mode == CREATE || mode == ADD_PLAYER || mode == REMOVE_PLAYER) { + int len = ProtocolUtils.readVarInt(buf); + players = new String[len]; + for (int i = 0; i < len; i++) { + players[i] = ProtocolUtils.readString(buf); + } + } + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { + ProtocolUtils.writeString(buf, name); + buf.writeByte(mode); + if (mode == CREATE || mode == UPDATE_INFO) { + ProtocolUtils.writeString(buf, displayName); + if (version.compareTo(ProtocolVersion.MINECRAFT_1_13) < 0) { + ProtocolUtils.writeString(buf, prefix); + ProtocolUtils.writeString(buf, suffix); + } + buf.writeByte(friendlyFire); + ProtocolUtils.writeString(buf, nameTagVisibility); + if (version.compareTo(ProtocolVersion.MINECRAFT_1_9) >= 0) { + ProtocolUtils.writeString(buf, collisionRule); + } + + if (version.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0) { + ProtocolUtils.writeVarInt(buf, color); + ProtocolUtils.writeString(buf, prefix); + ProtocolUtils.writeString(buf, suffix); + } else { + buf.writeByte( color ); + } + } + if (mode == CREATE || mode == ADD_PLAYER || mode == REMOVE_PLAYER) { + ProtocolUtils.writeVarInt(buf, players.length); + for (String player : players) { + ProtocolUtils.writeString(buf, player); + } + } + } + + @Override + public boolean handle(MinecraftSessionHandler minecraftSessionHandler) { + return false; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public byte getMode() { + return mode; + } + + public void setMode(byte mode) { + this.mode = mode; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public String getPrefix() { + return prefix; + } + + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + public String getSuffix() { + return suffix; + } + + public void setSuffix(String suffix) { + this.suffix = suffix; + } + + public String getNameTagVisibility() { + return nameTagVisibility; + } + + public void setNameTagVisibility(String nameTagVisibility) { + this.nameTagVisibility = nameTagVisibility; + } + + public String getCollisionRule() { + return collisionRule; + } + + public void setCollisionRule(String collisionRule) { + this.collisionRule = collisionRule; + } + + public int getColor() { + return color; + } + + public void setColor(int color) { + this.color = color; + } + + public byte getFriendlyFire() { + return friendlyFire; + } + + public void setFriendlyFire(byte friendlyFire) { + this.friendlyFire = friendlyFire; + } + + public String[] getPlayers() { + return players; + } + + public void setPlayers(String[] players) { + this.players = players; + } + + @Override + public String toString() { + return "Team{" + + "name=" + name + + ", mode=" + mode + + ", displayName=" + displayName + + ", prefix=" + prefix + + ", suffix=" + suffix + + ", friendlyFire=" + friendlyFire + + ", nameTagVisibility=" + nameTagVisibility + + ", collisionRule=" + collisionRule + + ", color=" + color + + ", players=[" + String.join(",", players) + "]" + + '}'; + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ReflectionUtil.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ReflectionUtil.java index f12a72d6..7f7274fa 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ReflectionUtil.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ReflectionUtil.java @@ -17,12 +17,31 @@ package codecrafter47.bungeetablistplus.util; +import codecrafter47.bungeetablistplus.protocol.Team; +import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.player.TabList; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.protocol.StateRegistry; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.function.Supplier; +import java.util.logging.Logger; + +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_12; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_12_1; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_13; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_14; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_15; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_17; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_1; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_3; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9; public class ReflectionUtil { public static void setTablistHandler(Player player, TabList tablistHandler) throws NoSuchFieldException, IllegalAccessException { @@ -70,4 +89,36 @@ public static T getField(Class clazz, Object instance, String field, int } return getField(clazz, instance, field); } + + // Five from Velocity team said this to adding the Team packet + // "Most instances won't need this feature so why should we weigh them down with baggage that's entirely optional?" + public static void injectTeamPacketRegistry() { + int tries = 5; + while (--tries > 0) { + try { + StateRegistry.PacketRegistry clientbound = getField(StateRegistry.class, StateRegistry.PLAY, "clientbound", tries); + + Method register = StateRegistry.PacketRegistry.class.getDeclaredMethod("register", Class.class, Supplier.class, StateRegistry.PacketMapping[].class); + register.setAccessible(true); + + Constructor packetMapping = StateRegistry.PacketMapping.class.getDeclaredConstructor(int.class, ProtocolVersion.class, ProtocolVersion.class, boolean.class); + packetMapping.setAccessible(true); + + register.invoke(clientbound, Team.class, (Supplier) Team::new, new StateRegistry.PacketMapping[]{ + (StateRegistry.PacketMapping) packetMapping.newInstance(0x3E, MINECRAFT_1_8, null, false), + (StateRegistry.PacketMapping) packetMapping.newInstance(0x41, MINECRAFT_1_9, null, false), + (StateRegistry.PacketMapping) packetMapping.newInstance(0x43, MINECRAFT_1_12, null, false), + (StateRegistry.PacketMapping) packetMapping.newInstance(0x44, MINECRAFT_1_12_1, null, false), + (StateRegistry.PacketMapping) packetMapping.newInstance(0x47, MINECRAFT_1_13, null, false), + (StateRegistry.PacketMapping) packetMapping.newInstance(0x4B, MINECRAFT_1_14, null, false), + (StateRegistry.PacketMapping) packetMapping.newInstance(0x4C, MINECRAFT_1_15, null, false), + (StateRegistry.PacketMapping) packetMapping.newInstance(0x55, MINECRAFT_1_17, null, false), + (StateRegistry.PacketMapping) packetMapping.newInstance(0x58, MINECRAFT_1_19_1, null, false), + (StateRegistry.PacketMapping) packetMapping.newInstance(0x56, MINECRAFT_1_19_3, null, false) + }); + } catch (Exception e) { + e.printStackTrace(); + } + } + } } From 3bbc79799668dc8ca72f91164f38fc88df1a18fe Mon Sep 17 00:00:00 2001 From: Brent P Date: Thu, 5 Jan 2023 11:15:25 -0500 Subject: [PATCH 09/22] Remove deprecated API functions --- .../api/velocity/BungeeTabListPlusAPI.java | 79 -------- .../api/velocity/CustomTablist.java | 181 ------------------ .../bungeetablistplus/managers/API.java | 45 ----- .../tablist/AbstractCustomTablist.java | 1 - 4 files changed, 306 deletions(-) delete mode 100644 api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/CustomTablist.java diff --git a/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/BungeeTabListPlusAPI.java b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/BungeeTabListPlusAPI.java index 563cffe2..c112d471 100644 --- a/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/BungeeTabListPlusAPI.java +++ b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/BungeeTabListPlusAPI.java @@ -24,7 +24,6 @@ import javax.annotation.Nonnull; import java.awt.image.BufferedImage; import java.util.concurrent.CompletableFuture; -import java.util.function.Consumer; public abstract class BungeeTabListPlusAPI { private static BungeeTabListPlusAPI instance = null; @@ -63,54 +62,6 @@ public static void registerVariable(Object plugin, ServerVariable variable) { protected abstract void registerVariable0(Object plugin, ServerVariable variable); - /** - * Create a new {@link CustomTablist} - * - * @return thte created {@link CustomTablist} - * @deprecated The custom tab list api has been changed. See {@link #getTabViewForPlayer(Player)} - */ - @Deprecated - public static CustomTablist createCustomTablist() { - Preconditions.checkState(instance != null, "BungeeTabListPlus not initialized"); - return instance.createCustomTablist0(); - } - - @SuppressWarnings("deprecation") - protected abstract CustomTablist createCustomTablist0(); - - /** - * Set a custom tab list for a player - * - * @param player the player - * @param customTablist the CustomTablist to use - * @deprecated The custom tab list api has been changed. See {@link #getTabViewForPlayer(Player)} - */ - @Deprecated - public static void setCustomTabList(Player player, CustomTablist customTablist) { - Preconditions.checkState(instance != null, "BungeeTabListPlus not initialized"); - instance.setCustomTabList0(player, customTablist); - } - - @SuppressWarnings("deprecation") - protected abstract void setCustomTabList0(Player player, CustomTablist customTablist); - - /** - * Get the face part of the players skin as an icon for use in the tab list. - * - * @param player the player - * @return the icon - * @deprecated Use {@link #getPlayerIcon(Player)} - */ - @Deprecated - @Nonnull - public static Icon getIconFromPlayer(Player player) { - Preconditions.checkState(instance != null, "BungeeTabListPlus not initialized"); - return instance.getIconFromPlayer0(player); - } - - @Nonnull - protected abstract Icon getIconFromPlayer0(Player player); - /** * Get the face part of the players skin as an icon for use in the tab list. * @@ -126,21 +77,6 @@ public static de.codecrafter47.taboverlay.Icon getPlayerIcon(Player player) { @Nonnull protected abstract de.codecrafter47.taboverlay.Icon getPlayerIcon0(Player player); - /** - * Creates an icon from an 8x8 px image. The creation of the icon can take several - * minutes. When the icon has been created the callback is invoked. - * - * @param image the image - * @param callback called when the icon is ready - * @deprecated use {@link #getIconFromImage(BufferedImage)} - */ - @Deprecated - public static void createIcon(BufferedImage image, Consumer callback) { - Preconditions.checkState(instance != null, "BungeeTabListPlus not initialized"); - instance.createIcon0(image, callback); - } - - protected abstract void createIcon0(BufferedImage image, Consumer callback); /** * Creates an icon from an 8x8 px image. The creation of the icon can take several @@ -156,21 +92,6 @@ public static CompletableFuture getIconFromIma protected abstract CompletableFuture getIconFromImage0(BufferedImage image); - /** - * Removes a custom tab list from a player. - * If the player hasn't got a custom tab list associated with it this will do nothing. - * - * @param player the player - * @deprecated The custom tab list api has been changed. See {@link #getTabViewForPlayer(Player)} - */ - @Deprecated - public static void removeCustomTabList(Player player) { - Preconditions.checkState(instance != null, "BungeeTabListPlus not initialized"); - instance.removeCustomTabList0(player); - } - - protected abstract void removeCustomTabList0(Player player); - /** * Get the tab view of a player. The tab view object allows registering and unregistering custom tab overlay * handlers. diff --git a/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/CustomTablist.java b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/CustomTablist.java deleted file mode 100644 index b1621b0c..00000000 --- a/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/CustomTablist.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright (C) 2020 Florian Stober - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package codecrafter47.bungeetablistplus.api.velocity; - -import com.velocitypowered.api.proxy.Player; - -import lombok.NonNull; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * @deprecated The custom tab list api has been changed. See {@link BungeeTabListPlusAPI#getTabViewForPlayer(Player)} - */ -@Deprecated -public interface CustomTablist { - /** - * Set the size of the tab list. - *

- * Recommended values: - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
SizeColumnsRows
000
10110
20120
30215
40220
60320
80420
- * - * @param size new size of the tab list - * @throws IllegalArgumentException if the given size is not allowed - */ - void setSize(int size); - - /** - * Get the size of the tab list - * This is the same as getRows() * getColumns() - * - * @return the size of the tablist - */ - int getSize(); - - /** - * Get the number of rows in the tab list - * - * @return the number of rows - */ - int getRows(); - - /** - * Get the number of columns in the tab list - * - * @return the number of columns - */ - int getColumns(); - - /** - * Get the icon of the slot at the position specified by row and column - * - * @param row the row - * @param column the column - * @return the icon at the given position - */ - @Nonnull - Icon getIcon(int row, int column); - - /** - * Get the text of the slot at the position specified by row and column - * - * @param row the row - * @param column the column - * @return the text at the given position - */ - @Nonnull - String getText(int row, int column); - - /** - * Get the ping of the slot at the position specified by row and column - * - * @param row the row - * @param column the column - * @return the ping at the given position - */ - int getPing(int row, int column); - - /** - * Set the slot at a position specified by row and column - * - * @param row the row - * @param column the column - * @param icon the icon - * @param text the text - * @param ping the ping - */ - void setSlot(int row, int column, @Nonnull @NonNull Icon icon, @Nonnull @NonNull String text, int ping); - - /** - * Get the header set for the tab list - * - * @return the header - */ - @Nullable - String getHeader(); - - /** - * Set the header for the tab list - * may contain color codes like &6 - * - * @param header the header - */ - void setHeader(@Nullable String header); - - /** - * Get the footer for the tab list - * - * @return the footer - */ - @Nullable - String getFooter(); - - /** - * Set the footer - * - * @param footer the footer - */ - void setFooter(@Nullable String footer); -} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/API.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/API.java index 4659e967..2da9e2d7 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/API.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/API.java @@ -19,9 +19,7 @@ import codecrafter47.bungeetablistplus.BungeeTabListPlus; import codecrafter47.bungeetablistplus.api.velocity.BungeeTabListPlusAPI; -import codecrafter47.bungeetablistplus.api.velocity.CustomTablist; import codecrafter47.bungeetablistplus.api.velocity.FakePlayerManager; -import codecrafter47.bungeetablistplus.api.velocity.Icon; import codecrafter47.bungeetablistplus.api.velocity.ServerVariable; import codecrafter47.bungeetablistplus.api.velocity.Variable; import codecrafter47.bungeetablistplus.data.BTLPVelocityDataKeys; @@ -42,7 +40,6 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; @@ -67,30 +64,6 @@ public API(TabViewManager tabViewManager, IconManager iconManager, PlayerPlaceho this.btlp = btlp; } - @Override - @SuppressWarnings("deprecation") - protected void setCustomTabList0(Player player, CustomTablist customTablist) { - TabView tabView = tabViewManager.getTabView(player); - if (tabView == null) { - throw new IllegalStateException("unknown player"); - } - if (customTablist instanceof DefaultCustomTablist) { - tabView.getTabOverlayProviders().removeProviders(DefaultCustomTablist.TabOverlayProviderImpl.class); - ((DefaultCustomTablist) customTablist).addToPlayer(tabView); - } else { - throw new IllegalArgumentException("customTablist not created by createCustomTablist()"); - } - } - - @Override - protected void removeCustomTabList0(Player player) { - TabView tabView = tabViewManager.getTabView(player); - if (tabView == null) { - throw new IllegalStateException("unknown player"); - } - tabView.getTabOverlayProviders().removeProviders(DefaultCustomTablist.TabOverlayProviderImpl.class); - } - @Override protected TabView getTabViewForPlayer0(Player player) { TabView tabView = tabViewManager.getTabView(player); @@ -100,24 +73,12 @@ protected TabView getTabViewForPlayer0(Player player) { return tabView; } - @Nonnull - @Override - protected Icon getIconFromPlayer0(Player player) { - return IconUtil.convert(IconUtil.getIconFromPlayer(player)); - } - @Nonnull @Override protected de.codecrafter47.taboverlay.Icon getPlayerIcon0(Player player) { return IconUtil.getIconFromPlayer(player); } - @Override - protected void createIcon0(BufferedImage image, Consumer callback) { - CompletableFuture future = iconManager.createIcon(image); - future.thenAccept(icon -> callback.accept(IconUtil.convert(icon))); - } - @Override protected CompletableFuture getIconFromImage0(BufferedImage image) { return iconManager.createIcon(image); @@ -171,12 +132,6 @@ String resolveCustomPlaceholderServer(String id, String serverName) { return ""; } - @Override - @SuppressWarnings("deprecation") - protected CustomTablist createCustomTablist0() { - return new DefaultCustomTablist(); - } - @Override protected FakePlayerManager getFakePlayerManager0() { FakePlayerManagerImpl fakePlayerManager = btlp.getFakePlayerManagerImpl(); diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/tablist/AbstractCustomTablist.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/tablist/AbstractCustomTablist.java index 9d52166d..c8a083d5 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/tablist/AbstractCustomTablist.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/tablist/AbstractCustomTablist.java @@ -17,7 +17,6 @@ package codecrafter47.bungeetablistplus.tablist; -import codecrafter47.bungeetablistplus.api.velocity.CustomTablist; import codecrafter47.bungeetablistplus.util.IconUtil; import de.codecrafter47.taboverlay.Icon; import lombok.NonNull; From f52e89bf0fe3f7b9749e03ae1a5e9a4a577c4ab4 Mon Sep 17 00:00:00 2001 From: Brent P Date: Fri, 6 Jan 2023 23:53:16 -0500 Subject: [PATCH 10/22] Fix Compile Errors --- velocity/build.gradle | 1 + .../bungeetablistplus/BungeeTabListPlus.java | 18 +- .../bridge/BukkitBridge.java | 11 +- .../command/CommandDebug.java | 2 +- .../command/CommandHide.java | 6 +- .../listener/TabListListener.java | 5 +- .../bungeetablistplus/managers/API.java | 3 +- .../managers/DataManager.java | 8 +- .../managers/RedisPlayerManager.java | 8 +- ...vider.java => VelocityPlayerProvider.java} | 4 +- .../tablist/AbstractCustomTablist.java | 217 ------------------ .../tablist/DefaultCustomTablist.java | 146 ------------ .../util/ReflectionUtil.java | 1 + 13 files changed, 33 insertions(+), 397 deletions(-) rename velocity/src/main/java/codecrafter47/bungeetablistplus/managers/{BungeePlayerProvider.java => VelocityPlayerProvider.java} (97%) delete mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/tablist/AbstractCustomTablist.java delete mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/tablist/DefaultCustomTablist.java diff --git a/velocity/build.gradle b/velocity/build.gradle index c24b86a3..53a6733e 100644 --- a/velocity/build.gradle +++ b/velocity/build.gradle @@ -30,6 +30,7 @@ dependencies { compileOnly "com.viaversion:viaversion-api:4.0.0" compileOnly group: "org.geysermc.geyser", name: "api", version: "2.1.0-SNAPSHOT" compileOnly "org.geysermc.floodgate:api:2.0-SNAPSHOT" + compileOnly "io.netty:netty-all:4.1.86.Final" // This imports velocity proxy compileOnly fileTree(dir: '../libs', include: '*.jar') } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java index 0eef023a..b8501aee 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java @@ -160,7 +160,7 @@ public static BungeeTabListPlus getInstance() { private DefaultIconManager iconManager; @Getter - private BungeePlayerProvider bungeePlayerProvider; + private VelocityPlayerProvider velocityPlayerProvider; @Getter private ProtocolVersionProvider protocolVersionProvider; @@ -192,6 +192,9 @@ public void onLoad() { INSTANCE = this; + // Hacks to get around no Team packet in Velocity + ReflectionUtil.injectTeamPacketRegistry(); + Executor executor = (task) -> getProxy().getScheduler().buildTask(getPlugin(), task).schedule(); asyncExecutor = new MultithreadEventExecutorGroup(4, executor) { @@ -245,7 +248,7 @@ public void onEnable() { if (readMainConfig()) return; - bungeePlayerProvider = new BungeePlayerProvider(mainThreadExecutor); + velocityPlayerProvider = new VelocityPlayerProvider(mainThreadExecutor); hiddenPlayersManager = new HiddenPlayersManager(); hiddenPlayersManager.addVanishProvider("/btlp hide", BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN_PLAYER_COMMAND); @@ -264,18 +267,18 @@ public void onEnable() { List playerProviders = new ArrayList<>(); if (getProxy().getPluginManager().getPlugin("redisbungee").isPresent()) { - redisPlayerManager = new RedisPlayerManager(bungeePlayerProvider, this, logger); + redisPlayerManager = new RedisPlayerManager(velocityPlayerProvider, this, logger); playerProviders.add(redisPlayerManager); plugin.getLogger().info("Hooked RedisBungee"); } - playerProviders.add(bungeePlayerProvider); + playerProviders.add(velocityPlayerProvider); playerProviders.add(fakePlayerManagerImpl); this.playerProvider = new JoinedPlayerProvider(playerProviders); getProxy().getChannelRegistrar().register(channelIdentifier); - bukkitBridge = new BukkitBridge(asyncExecutor, mainThreadExecutor, playerPlaceholderResolver, serverPlaceholderResolver, getPlugin(), logger, bungeePlayerProvider, this, cache); + bukkitBridge = new BukkitBridge(asyncExecutor, mainThreadExecutor, playerPlaceholderResolver, serverPlaceholderResolver, getPlugin(), logger, velocityPlayerProvider, this, cache); serverStateManager = new ServerStateManager(config, plugin); - dataManager = new DataManager(api, plugin, logger, bungeePlayerProvider, mainThreadExecutor, serverStateManager, bukkitBridge); + dataManager = new DataManager(api, plugin, logger, velocityPlayerProvider, mainThreadExecutor, serverStateManager, bukkitBridge); dataManager.addCompositeDataProvider(hiddenPlayersManager); dataManager.addCompositeDataProvider(new PermissionDataProvider()); @@ -341,9 +344,6 @@ public void onEnable() { } configTabOverlayManager.reloadConfigs(ImmutableSet.of(tabLists)); - // Hacks to get around no Team packet in Velocity - ReflectionUtil.injectTeamPacketRegistry(); - getProxy().getEventManager().register(plugin, new TabListListener(this)); } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/bridge/BukkitBridge.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/bridge/BukkitBridge.java index c81d5c32..aa45999f 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/bridge/BukkitBridge.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/bridge/BukkitBridge.java @@ -24,7 +24,7 @@ import codecrafter47.bungeetablistplus.common.network.TypeAdapterRegistry; import codecrafter47.bungeetablistplus.common.util.RateLimitedExecutor; import codecrafter47.bungeetablistplus.data.TrackingDataCache; -import codecrafter47.bungeetablistplus.managers.BungeePlayerProvider; +import codecrafter47.bungeetablistplus.managers.VelocityPlayerProvider; import codecrafter47.bungeetablistplus.placeholder.PlayerPlaceholderResolver; import codecrafter47.bungeetablistplus.placeholder.ServerPlaceholderResolver; import codecrafter47.bungeetablistplus.player.VelocityPlayer; @@ -39,7 +39,6 @@ import com.velocitypowered.api.event.player.ServerConnectedEvent; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ServerConnection; -import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.server.RegisteredServer; import de.codecrafter47.data.api.DataCache; import de.codecrafter47.data.api.DataHolder; @@ -72,18 +71,18 @@ public class BukkitBridge { private final ServerPlaceholderResolver serverPlaceholderResolver; private final VelocityPlugin plugin; private final Logger logger; - private final BungeePlayerProvider bungeePlayerProvider; + private final VelocityPlayerProvider velocityPlayerProvider; private final BungeeTabListPlus btlp; private final Cache cache; - public BukkitBridge(ScheduledExecutorService asyncExecutor, ScheduledExecutorService mainLoop, PlayerPlaceholderResolver playerPlaceholderResolver, ServerPlaceholderResolver serverPlaceholderResolver, VelocityPlugin plugin, Logger logger, BungeePlayerProvider bungeePlayerProvider, BungeeTabListPlus btlp, Cache cache) { + public BukkitBridge(ScheduledExecutorService asyncExecutor, ScheduledExecutorService mainLoop, PlayerPlaceholderResolver playerPlaceholderResolver, ServerPlaceholderResolver serverPlaceholderResolver, VelocityPlugin plugin, Logger logger, VelocityPlayerProvider velocityPlayerProvider, BungeeTabListPlus btlp, Cache cache) { this.asyncExecutor = asyncExecutor; this.mainLoop = mainLoop; this.playerPlaceholderResolver = playerPlaceholderResolver; this.serverPlaceholderResolver = serverPlaceholderResolver; this.plugin = plugin; this.logger = logger; - this.bungeePlayerProvider = bungeePlayerProvider; + this.velocityPlayerProvider = velocityPlayerProvider; this.btlp = btlp; this.cache = cache; ProxyServer.getInstance().getEventManager().register(plugin, this); @@ -235,7 +234,7 @@ private void handlePluginMessage(Player player, ServerConnection server, DataInp connectionInfo.hasReceived = false; connectionInfo.protocolVersion = Integer.min(BridgeProtocolConstants.VERSION, protocolVersion); - VelocityPlayer velocityPlayer = bungeePlayerProvider.getPlayerIfPresent(player); + VelocityPlayer velocityPlayer = velocityPlayerProvider.getPlayerIfPresent(player); if (velocityPlayer == null) { logger.severe("Internal error - Bridge functionality not available for " + player.getUsername()); } else { diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandDebug.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandDebug.java index 38896627..6eec82f9 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandDebug.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandDebug.java @@ -64,7 +64,7 @@ public static int commandHidden(CommandContext context) { } val btlp = BungeeTabListPlus.getInstance(); - VelocityPlayer player = btlp.getBungeePlayerProvider().getPlayerIfPresent(target); + VelocityPlayer player = btlp.getVelocityPlayerProvider().getPlayerIfPresent(target); if (player == null) { sender.sendMessage(ChatUtil.parseBBCode("&cUnknown player: " + name)); diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandHide.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandHide.java index 611af384..bbc60eed 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandHide.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandHide.java @@ -81,17 +81,17 @@ public static int commandUnhide(CommandContext context) { } private static boolean isHidden(Player player) { - VelocityPlayer velocityPlayer = BungeeTabListPlus.getInstance().getBungeePlayerProvider().getPlayer(player); + VelocityPlayer velocityPlayer = BungeeTabListPlus.getInstance().getVelocityPlayerProvider().getPlayer(player); return Boolean.TRUE.equals(velocityPlayer.get(BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN_PLAYER_COMMAND)); } private static void hidePlayer(Player player) { - VelocityPlayer velocityPlayer = BungeeTabListPlus.getInstance().getBungeePlayerProvider().getPlayer(player); + VelocityPlayer velocityPlayer = BungeeTabListPlus.getInstance().getVelocityPlayerProvider().getPlayer(player); velocityPlayer.getLocalDataCache().updateValue(BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN_PLAYER_COMMAND, true); } private static void unhidePlayer(Player player) { - VelocityPlayer velocityPlayer = BungeeTabListPlus.getInstance().getBungeePlayerProvider().getPlayer(player); + VelocityPlayer velocityPlayer = BungeeTabListPlus.getInstance().getVelocityPlayerProvider().getPlayer(player); velocityPlayer.getLocalDataCache().updateValue(BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN_PLAYER_COMMAND, false); } } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/listener/TabListListener.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/listener/TabListListener.java index f7ab06ef..4e52c05a 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/listener/TabListListener.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/listener/TabListListener.java @@ -20,7 +20,6 @@ import codecrafter47.bungeetablistplus.player.VelocityPlayer; import codecrafter47.bungeetablistplus.tablist.ExcludedServersTabOverlayProvider; import codecrafter47.bungeetablistplus.util.GeyserCompat; -import codecrafter47.bungeetablistplus.util.ProxyServer; import com.velocitypowered.api.event.PostOrder; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.connection.DisconnectEvent; @@ -52,7 +51,7 @@ public void onPlayerJoin(PostLoginEvent e) { } }).delay(2, TimeUnit.SECONDS).schedule(); - VelocityPlayer player = btlp.getBungeePlayerProvider().onPlayerConnected(e.getPlayer()); + VelocityPlayer player = btlp.getVelocityPlayerProvider().onPlayerConnected(e.getPlayer()); if (GeyserCompat.isBedrockPlayer(e.getPlayer().getUniqueId())) { return; @@ -71,7 +70,7 @@ public void onPlayerJoin(PostLoginEvent e) { @Subscribe(order = PostOrder.FIRST) public void onPlayerDisconnect(DisconnectEvent e) { try { - btlp.getBungeePlayerProvider().onPlayerDisconnected(e.getPlayer()); + btlp.getVelocityPlayerProvider().onPlayerDisconnected(e.getPlayer()); if (GeyserCompat.isBedrockPlayer(e.getPlayer().getUniqueId())) { return; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/API.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/API.java index 2da9e2d7..dee2acf1 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/API.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/API.java @@ -27,7 +27,6 @@ import codecrafter47.bungeetablistplus.placeholder.ServerPlaceholderResolver; import codecrafter47.bungeetablistplus.player.VelocityPlayer; import codecrafter47.bungeetablistplus.player.FakePlayerManagerImpl; -import codecrafter47.bungeetablistplus.tablist.DefaultCustomTablist; import codecrafter47.bungeetablistplus.util.IconUtil; import com.google.common.base.Preconditions; import com.velocitypowered.api.proxy.Player; @@ -143,7 +142,7 @@ protected FakePlayerManager getFakePlayerManager0() { @Override protected boolean isHidden0(Player player) { - VelocityPlayer velocityPlayer = BungeeTabListPlus.getInstance().getBungeePlayerProvider().getPlayerIfPresent(player); + VelocityPlayer velocityPlayer = BungeeTabListPlus.getInstance().getVelocityPlayerProvider().getPlayerIfPresent(player); return velocityPlayer != null && velocityPlayer.get(BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN); } } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/DataManager.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/DataManager.java index 6889f7de..86f4aaf3 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/DataManager.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/DataManager.java @@ -48,7 +48,7 @@ public class DataManager { private final API api; private final EventExecutor mainThreadExecutor; - private final BungeePlayerProvider bungeePlayerProvider; + private final VelocityPlayerProvider velocityPlayerProvider; private final ServerStateManager serverStateManager; private final BukkitBridge bukkitBridge; @@ -68,9 +68,9 @@ public class DataManager { @Setter List permanentlyHiddenPlayers = Collections.emptyList(); - public DataManager(API api, VelocityPlugin plugin, Logger logger, BungeePlayerProvider bungeePlayerProvider, EventExecutor mainThreadExecutor, ServerStateManager serverStateManager, BukkitBridge bukkitBridge) { + public DataManager(API api, VelocityPlugin plugin, Logger logger, VelocityPlayerProvider velocityPlayerProvider, EventExecutor mainThreadExecutor, ServerStateManager serverStateManager, BukkitBridge bukkitBridge) { this.api = api; - this.bungeePlayerProvider = bungeePlayerProvider; + this.velocityPlayerProvider = velocityPlayerProvider; this.mainThreadExecutor = mainThreadExecutor; this.serverStateManager = serverStateManager; this.bukkitBridge = bukkitBridge; @@ -104,7 +104,7 @@ private DataHolder getLocalServerDataHolder(@Nonnull String serverName) { } private void updateData(ProxyServer server) { - for (VelocityPlayer player : bungeePlayerProvider.getPlayers()) { + for (VelocityPlayer player : velocityPlayerProvider.getPlayers()) { for (DataKey dataKey : player.getLocalDataCache().getActiveKeys()) { if (playerDataAccess.provides(dataKey)) { DataKey key = Unchecked.cast(dataKey); diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/RedisPlayerManager.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/RedisPlayerManager.java index 1f87d47e..9d3c81be 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/RedisPlayerManager.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/RedisPlayerManager.java @@ -74,7 +74,7 @@ public class RedisPlayerManager implements PlayerProvider { private static String CHANNEL_DATA_UPDATE = "btlp-data-upd"; private final Map byUUID = new ConcurrentHashMap<>(); - private final BungeePlayerProvider bungeePlayerProvider; + private final VelocityPlayerProvider velocityPlayerProvider; private final BungeeTabListPlus plugin; private final EventExecutor mainThread; private final Logger logger; @@ -96,8 +96,8 @@ public void accept(String id) { private boolean redisConnectionSuccessful = false; - public RedisPlayerManager(BungeePlayerProvider bungeePlayerProvider, BungeeTabListPlus plugin, Logger logger) { - this.bungeePlayerProvider = bungeePlayerProvider; + public RedisPlayerManager(VelocityPlayerProvider velocityPlayerProvider, BungeeTabListPlus plugin, Logger logger) { + this.velocityPlayerProvider = velocityPlayerProvider; this.plugin = plugin; this.logger = logger; this.mainThread = plugin.getMainThreadExecutor(); @@ -126,7 +126,7 @@ public void onRedisMessage(PubSubMessageEvent event) { com.velocitypowered.api.proxy.Player Player = ProxyServer.getInstance().getPlayer(uuid).orElse(null); if (Player != null) { - VelocityPlayer player = bungeePlayerProvider.getPlayerIfPresent(Player); + VelocityPlayer player = velocityPlayerProvider.getPlayerIfPresent(Player); if (player != null) { DataKey key = DataStreamUtils.readDataKey(input, keyRegistry, missingDataKeyLogger); diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/BungeePlayerProvider.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/VelocityPlayerProvider.java similarity index 97% rename from velocity/src/main/java/codecrafter47/bungeetablistplus/managers/BungeePlayerProvider.java rename to velocity/src/main/java/codecrafter47/bungeetablistplus/managers/VelocityPlayerProvider.java index a72ed97c..b6b95fc7 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/BungeePlayerProvider.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/VelocityPlayerProvider.java @@ -32,14 +32,14 @@ import java.util.concurrent.TimeUnit; import java.util.logging.Level; -public class BungeePlayerProvider implements PlayerProvider { +public class VelocityPlayerProvider implements PlayerProvider { private final EventExecutor mainThread; private final Map players = new ConcurrentHashMap<>(); private final Set listeners = new ReferenceOpenHashSet<>(); - public BungeePlayerProvider(EventExecutor mainThread) { + public VelocityPlayerProvider(EventExecutor mainThread) { this.mainThread = mainThread; mainThread.scheduleWithFixedDelay(this::checkForStalePlayers, 5, 5, TimeUnit.MINUTES); } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/tablist/AbstractCustomTablist.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/tablist/AbstractCustomTablist.java deleted file mode 100644 index c8a083d5..00000000 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/tablist/AbstractCustomTablist.java +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Copyright (C) 2020 Florian Stober - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package codecrafter47.bungeetablistplus.tablist; - -import codecrafter47.bungeetablistplus.util.IconUtil; -import de.codecrafter47.taboverlay.Icon; -import lombok.NonNull; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.Arrays; -import java.util.Objects; - -import static java.lang.Integer.min; - -/** - * Represents a custom tab list. - */ -@SuppressWarnings("deprecation") -public abstract class AbstractCustomTablist implements CustomTablist { - private static Icon[] EMPTY_ICON_ARRAY = new Icon[0]; - private static String[] EMPTY_STRING_ARRAY = new String[0]; - private static int[] EMPTY_INT_ARRAY = new int[0]; - - private int size; - private int columns; - private int rows; - private Icon[] icon; - private String[] text; - private int[] ping; - private String header; - private String footer; - - /** - * Create a new custom tab list with size 0. - */ - AbstractCustomTablist() { - this.size = 0; - this.columns = 1; - this.rows = 0; - this.icon = EMPTY_ICON_ARRAY; - this.text = EMPTY_STRING_ARRAY; - this.ping = EMPTY_INT_ARRAY; - this.header = null; - this.footer = null; - } - - /** - * Create a new custom tab list with the given size. See {@link #setSize(int)}. - * - * @param size the size - * @throws IllegalArgumentException if the size is not allowed - */ - AbstractCustomTablist(int size) { - this(); - setSize(size); - } - - int index(int row, int column) { - int index = row * this.columns + column; - if (index >= size) { - throw new IndexOutOfBoundsException(String.format("Index [row=%s,column=%s] not inside tab list [rows=%s,columns=%s]", row, column, this.rows, this.columns)); - } - return index; - } - - @Override - public synchronized void setSize(int size) { - if (size < 0) { - throw new IllegalArgumentException("size is negative"); - } else if (size == 0) { - setSize(0, 0); - } else { - int columns = (size + 19) / 20; - int rows = size / columns; - if (columns * rows != size) { - throw new IllegalArgumentException("size is not rectangular"); - } - setSize(columns, rows); - } - } - - protected void setSize(int columns, int rows) { - int size = columns * rows; - if (size == 0) { - this.size = 0; - this.columns = 1; - this.rows = 0; - this.icon = EMPTY_ICON_ARRAY; - this.text = EMPTY_STRING_ARRAY; - this.ping = EMPTY_INT_ARRAY; - } else { - Icon[] icon = new Icon[size]; - String[] text = new String[size]; - int[] ping = new int[size]; - Arrays.fill(icon, Icon.DEFAULT_STEVE); - Arrays.fill(text, ""); - Arrays.fill(ping, 0); - for (int col = min(this.columns, columns) - 1; col >= 0; col--) { - for (int row = min(this.rows, rows) - 1; row >= 0; row--) { - icon[row * columns + col] = this.icon[row * this.columns + col]; - text[row * columns + col] = this.text[row * this.columns + col]; - ping[row * columns + col] = this.ping[row * this.columns + col]; - } - } - this.size = size; - this.columns = columns; - this.rows = rows; - this.icon = icon; - this.text = text; - this.ping = ping; - } - onSizeChanged(); - } - - @Override - public int getSize() { - return size; - } - - @Override - public int getRows() { - return rows; - } - - @Override - public int getColumns() { - return columns; - } - - @Override - @Nonnull - public codecrafter47.bungeetablistplus.api.velocity.Icon getIcon(int row, int column) { - return IconUtil.convert(this.icon[index(row, column)]); - } - - Icon getIcon(int index) { - return this.icon[index]; - } - - @Override - @Nonnull - public String getText(int row, int column) { - return this.text[index(row, column)]; - } - - String getText(int index) { - return this.text[index]; - } - - @Override - public int getPing(int row, int column) { - return this.ping[index(row, column)]; - } - - int getPing(int index) { - return this.ping[index]; - } - - @Override - public synchronized void setSlot(int row, int column, @Nonnull @NonNull codecrafter47.bungeetablistplus.api.velocity.Icon icon, @Nonnull @NonNull String text, int ping) { - int index = index(row, column); - this.icon[index] = IconUtil.convert(icon); - this.text[index] = text; - this.ping[index] = ping; - onSlotChanged(index); - } - - @Override - @Nullable - public String getHeader() { - return this.header; - } - - @Override - public synchronized void setHeader(@Nullable String header) { - if (!Objects.equals(this.header, header)) { - this.header = header; - onHeaderOrFooterChanged(); - } - } - - @Override - @Nullable - public String getFooter() { - return footer; - } - - @Override - public synchronized void setFooter(@Nullable String footer) { - if (!Objects.equals(this.footer, footer)) { - this.footer = footer; - onHeaderOrFooterChanged(); - } - } - - protected abstract void onSizeChanged(); - - protected abstract void onSlotChanged(int index); - - protected abstract void onHeaderOrFooterChanged(); -} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/tablist/DefaultCustomTablist.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/tablist/DefaultCustomTablist.java deleted file mode 100644 index 33943e78..00000000 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/tablist/DefaultCustomTablist.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright (C) 2020 Florian Stober - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package codecrafter47.bungeetablistplus.tablist; - -import codecrafter47.bungeetablistplus.util.IconUtil; -import de.codecrafter47.taboverlay.Icon; -import de.codecrafter47.taboverlay.TabOverlayProvider; -import de.codecrafter47.taboverlay.TabView; -import de.codecrafter47.taboverlay.handler.*; -import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; -import it.unimi.dsi.fastutil.objects.ReferenceSet; - -import static java.lang.Integer.min; - -public class DefaultCustomTablist extends AbstractCustomTablist { - private final ReferenceSet handlers = new ReferenceOpenHashSet<>(); - - public DefaultCustomTablist() { - } - - public DefaultCustomTablist(int size) { - super(size); - } - - @Override - protected void onSizeChanged() { - for (TabOverlayProviderImpl handler : handlers) { - handler.onSizeChanged(); - } - } - - @Override - protected void onSlotChanged(int index) { - Icon icon = getIcon(index); - String text = getText(index); - int ping = getPing(index); - for (TabOverlayProviderImpl handler : handlers) { - handler.onSlotChanged(index, icon, text, ping); - } - } - - @Override - protected void onHeaderOrFooterChanged() { - String header = getHeader(); - String footer = getFooter(); - for (TabOverlayProviderImpl handler : handlers) { - handler.setHeaderFooter(header, footer); - } - } - - public void addToPlayer(TabView tabView) { - TabOverlayProviderImpl provider = new TabOverlayProviderImpl(); - tabView.getTabOverlayProviders().addProvider(provider); - } - - public class TabOverlayProviderImpl extends TabOverlayProvider { - - private SimpleTabOverlay tabOverlay; - private HeaderAndFooterHandle headerAndFooterHandle; - - TabOverlayProviderImpl() { - super("custom-tab-overlay", 10001); - } - - @Override - protected void attach(TabView tabView) { - handlers.add(this); - } - - @Override - protected void detach(TabView tabView) { - handlers.remove(this); - } - - @Override - protected void activate(TabView tabView, TabOverlayHandler handler) { - synchronized (DefaultCustomTablist.this) { - tabOverlay = handler.enterContentOperationMode(ContentOperationMode.SIMPLE); - headerAndFooterHandle = handler.enterHeaderAndFooterOperationMode(HeaderAndFooterOperationMode.CUSTOM); - int size = min(80, getSize()); - tabOverlay.setSize(size); - updateAllSlots(); - headerAndFooterHandle.setHeaderFooter(getHeader(), getFooter()); - } - } - - @Override - protected void deactivate(TabView tabView) { - - } - - @Override - protected boolean shouldActivate(TabView tabView) { - return true; - } - - private void updateAllSlots() { - for (int column = 0; column < getColumns(); column++) { - for (int row = 0; row < getRows(); row++) { - Icon icon = IconUtil.convert(getIcon(row, column)); - String text = getText(row, column); - int ping = getPing(row, column); - - tabOverlay.setSlot(index(row, column), icon, text, ping); - } - } - } - - void onSizeChanged() { - synchronized (DefaultCustomTablist.this) { - if (tabOverlay != null) { - int size = getSize(); - tabOverlay.setSize(size); - updateAllSlots(); - } - } - } - - void onSlotChanged(int index, Icon icon, String text, int ping) { - if (tabOverlay != null) { - tabOverlay.setSlot(index, icon, text, ping); - } - } - - void setHeaderFooter(String header, String footer) { - if (headerAndFooterHandle != null) { - headerAndFooterHandle.setHeaderFooter(header, footer); - } - } - } -} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ReflectionUtil.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ReflectionUtil.java index 7f7274fa..bba1b041 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ReflectionUtil.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ReflectionUtil.java @@ -116,6 +116,7 @@ public static void injectTeamPacketRegistry() { (StateRegistry.PacketMapping) packetMapping.newInstance(0x58, MINECRAFT_1_19_1, null, false), (StateRegistry.PacketMapping) packetMapping.newInstance(0x56, MINECRAFT_1_19_3, null, false) }); + return; } catch (Exception e) { e.printStackTrace(); } From 1833ee5b26867689aa334812be2de63f09b8618e Mon Sep 17 00:00:00 2001 From: Brent P Date: Sun, 1 Oct 2023 10:22:02 -0400 Subject: [PATCH 11/22] Update Velocity to 1.20.1 --- build.gradle | 6 +- minecraft-data-api | 2 +- .../bungeetablistplus/BungeeTabListPlus.java | 1129 ++-- .../bridge/BukkitBridge.java | 1423 ++--- .../AbstractLegacyTabOverlayHandler.java | 1698 +++--- .../handler/AbstractTabOverlayHandler.java | 5124 ++++++++--------- .../handler/LegacyTabOverlayHandlerImpl.java | 97 +- .../handler/NewTabOverlayHandler.java | 2488 ++++---- .../handler/TabOverlayHandlerImpl.java | 160 +- .../listener/TabListListener.java | 196 +- .../managers/TabViewManager.java | 304 +- .../protocol/PacketListener.java | 178 +- .../bungeetablistplus/protocol/Team.java | 440 +- .../bungeetablistplus/util/GeyserCompat.java | 115 +- .../util/ReflectionUtil.java | 250 +- 15 files changed, 6828 insertions(+), 6782 deletions(-) diff --git a/build.gradle b/build.gradle index 98135ce0..5354c049 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ buildscript { ext { spigotVersion = '1.11-R0.1-SNAPSHOT' bungeeVersion = '1.19-R0.1-SNAPSHOT' - velocityVersion = '3.1.2-SNAPSHOT' + velocityVersion = '3.2.0-SNAPSHOT' spongeVersion = '7.0.0' dataApiVersion = '1.0.2-SNAPSHOT' } @@ -75,8 +75,8 @@ subprojects { } compileJava { - sourceCompatibility = '1.8' - targetCompatibility = '1.8' + sourceCompatibility = '11' + targetCompatibility = '11' } publishing { diff --git a/minecraft-data-api b/minecraft-data-api index c2086ccf..3acdda80 160000 --- a/minecraft-data-api +++ b/minecraft-data-api @@ -1 +1 @@ -Subproject commit c2086ccf3f150e2cf88e3c145d05cd6391bfc0cd +Subproject commit 3acdda806579e911bde9aa679e432bf65985b8f2 diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java index b8501aee..6e983f59 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java @@ -1,563 +1,566 @@ -/* - * Copyright (C) 2020 Florian Stober - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package codecrafter47.bungeetablistplus; - -import codecrafter47.bungeetablistplus.api.velocity.BungeeTabListPlusAPI; -import codecrafter47.bungeetablistplus.bridge.BukkitBridge; -import codecrafter47.bungeetablistplus.cache.Cache; -import codecrafter47.bungeetablistplus.command.CommandBungeeTabListPlus; -import codecrafter47.bungeetablistplus.common.network.BridgeProtocolConstants; -import codecrafter47.bungeetablistplus.config.MainConfig; -import codecrafter47.bungeetablistplus.config.PlayersByServerComponentConfiguration; -import codecrafter47.bungeetablistplus.data.BTLPVelocityDataKeys; -import codecrafter47.bungeetablistplus.data.PermissionDataProvider; -import codecrafter47.bungeetablistplus.listener.TabListListener; -import codecrafter47.bungeetablistplus.managers.*; -import codecrafter47.bungeetablistplus.placeholder.GlobalServerPlaceholderResolver; -import codecrafter47.bungeetablistplus.placeholder.PlayerPlaceholderResolver; -import codecrafter47.bungeetablistplus.placeholder.ServerCountPlaceholderResolver; -import codecrafter47.bungeetablistplus.placeholder.ServerPlaceholderResolver; -import codecrafter47.bungeetablistplus.player.FakePlayerManagerImpl; -import codecrafter47.bungeetablistplus.tablist.ExcludedServersTabOverlayProvider; -import codecrafter47.bungeetablistplus.updater.UpdateChecker; -import codecrafter47.bungeetablistplus.updater.UpdateNotifier; -import codecrafter47.bungeetablistplus.util.ExceptionHandlingEventExecutor; -import codecrafter47.bungeetablistplus.util.MatchingStringsCollection; -import codecrafter47.bungeetablistplus.util.ReflectionUtil; -import codecrafter47.bungeetablistplus.util.VelocityPlugin; -import codecrafter47.bungeetablistplus.version.VelocityProtocolVersionProvider; -import codecrafter47.bungeetablistplus.version.ProtocolVersionProvider; -import codecrafter47.bungeetablistplus.version.ViaVersionProtocolVersionProvider; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import com.velocitypowered.api.command.CommandManager; -import com.velocitypowered.api.proxy.messages.ChannelIdentifier; -import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; -import de.codecrafter47.data.bukkit.api.BukkitData; -import de.codecrafter47.data.velocity.api.VelocityData; -import de.codecrafter47.data.sponge.api.SpongeData; -import de.codecrafter47.taboverlay.config.ComponentSpec; -import de.codecrafter47.taboverlay.config.ConfigTabOverlayManager; -import de.codecrafter47.taboverlay.config.ErrorHandler; -import de.codecrafter47.taboverlay.config.dsl.customplaceholder.CustomPlaceholderConfiguration; -import de.codecrafter47.taboverlay.config.icon.DefaultIconManager; -import de.codecrafter47.taboverlay.config.platform.EventListener; -import de.codecrafter47.taboverlay.config.platform.Platform; -import de.codecrafter47.taboverlay.config.player.JoinedPlayerProvider; -import de.codecrafter47.taboverlay.config.player.PlayerProvider; -import de.codecrafter47.taboverlay.config.spectator.SpectatorPassthroughTabOverlayManager; -import io.netty.util.concurrent.EventExecutor; -import io.netty.util.concurrent.EventExecutorGroup; -import io.netty.util.concurrent.MultithreadEventExecutorGroup; -import lombok.Getter; -import lombok.val; -import org.yaml.snakeyaml.Yaml; -import org.yaml.snakeyaml.error.YAMLException; - -import java.io.*; -import java.lang.reflect.Field; -import java.net.URL; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.concurrent.Executor; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -/** - * Main Class of BungeeTabListPlus - * - * @author Florian Stober - */ -public class BungeeTabListPlus { - - /** - * Holds an INSTANCE of itself if the plugin is enabled - */ - private static BungeeTabListPlus INSTANCE; - @Getter - private final VelocityPlugin plugin; - - public PlayerProvider playerProvider; - @Getter - private EventExecutor mainThreadExecutor; - @Getter - private EventExecutorGroup asyncExecutor; - - @Getter - private RedisPlayerManager redisPlayerManager; - @Getter - private DataManager dataManager; - private ServerStateManager serverStateManager; - @Getter - private ServerPlaceholderResolver serverPlaceholderResolver; - - private Yaml yaml; - @Getter - private HiddenPlayersManager hiddenPlayersManager; - private PlayerPlaceholderResolver playerPlaceholderResolver; - private API api; - private Logger logger = Logger.getLogger(BungeeTabListPlus.class.getSimpleName()); - - public BungeeTabListPlus(VelocityPlugin plugin) { - this.plugin = plugin; - } - - /** - * Static getter for the current instance of the plugin - * - * @return the current instance of the plugin, null if the plugin is - * disabled - */ - public static BungeeTabListPlus getInstance(VelocityPlugin plugin) { - if (INSTANCE == null) { - INSTANCE = new BungeeTabListPlus(plugin); - } - return INSTANCE; - } - - public static BungeeTabListPlus getInstance() { - return INSTANCE; - } - - @Getter - private MainConfig config; - - private Cache cache; - @Getter - MatchingStringsCollection excludedServers; - - @Getter - private FakePlayerManagerImpl fakePlayerManagerImpl; - - private BukkitBridge bukkitBridge; - - private UpdateChecker updateChecker = null; - - @Getter - private DefaultIconManager iconManager; - - @Getter - private VelocityPlayerProvider velocityPlayerProvider; - - @Getter - private ProtocolVersionProvider protocolVersionProvider; - - @Getter - private TabViewManager tabViewManager; - - private ConfigTabOverlayManager configTabOverlayManager; - private SpectatorPassthroughTabOverlayManager spectatorPassthroughTabOverlayManager; - - @Getter - private List listeners = new ArrayList<>(); - - private transient boolean scheduledSoftReload; - @Getter - private final ChannelIdentifier channelIdentifier = MinecraftChannelIdentifier.from(BridgeProtocolConstants.CHANNEL); - - public void onLoad() { - codecrafter47.bungeetablistplus.util.ProxyServer.setProxyServer(plugin.getProxy()); - if (!plugin.getDataDirectory().toFile().exists()) { - plugin.getDataDirectory().toFile().mkdirs(); - } - - try { - Class.forName("com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo"); - } catch (ClassNotFoundException ex) { - throw new RuntimeException("You need to run at least Velocity version #196"); - } - - INSTANCE = this; - - // Hacks to get around no Team packet in Velocity - ReflectionUtil.injectTeamPacketRegistry(); - - Executor executor = (task) -> getProxy().getScheduler().buildTask(getPlugin(), task).schedule(); - - asyncExecutor = new MultithreadEventExecutorGroup(4, executor) { - @Override - protected EventExecutor newChild(Executor executor, Object... args) { - return new ExceptionHandlingEventExecutor(this, executor, logger); - } - }; - mainThreadExecutor = new ExceptionHandlingEventExecutor(null, executor, logger); - - if (getProxy().getPluginManager().getPlugin("viaversion").isPresent()) { - protocolVersionProvider = new ViaVersionProtocolVersionProvider(); - } else { - protocolVersionProvider = new VelocityProtocolVersionProvider(); - } - - this.tabViewManager = new TabViewManager(this, protocolVersionProvider); - - File headsFolder = new File(plugin.getDataDirectory().toFile(), "heads"); - extractDefaultIcons(headsFolder); - - iconManager = new DefaultIconManager(asyncExecutor, mainThreadExecutor, headsFolder.toPath(), logger); - - cache = Cache.load(new File(plugin.getDataDirectory().toFile(), "cache.dat")); - - serverPlaceholderResolver = new ServerPlaceholderResolver(cache); - playerPlaceholderResolver = new PlayerPlaceholderResolver(serverPlaceholderResolver, cache); - - api = new API(tabViewManager, iconManager, playerPlaceholderResolver, serverPlaceholderResolver, logger, this); - - try { - Field field = BungeeTabListPlusAPI.class.getDeclaredField("instance"); - field.setAccessible(true); - field.set(null, api); - } catch (NoSuchFieldException | IllegalAccessException ex) { - getLogger().log(Level.SEVERE, "Failed to initialize API", ex); - } - } - - public void onEnable() { - - ConfigTabOverlayManager.Options options = ConfigTabOverlayManager.Options.createBuilderWithDefaults() - .playerIconDataKey(BTLPVelocityDataKeys.DATA_KEY_ICON) - .playerPingDataKey(VelocityData.Velocity_Ping) - .playerInvisibleDataKey(BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN) - .playerCanSeeInvisibleDataKey(BTLPVelocityDataKeys.permission("bungeetablistplus.seevanished")) - .component(new ComponentSpec("!players_by_server", PlayersByServerComponentConfiguration.class)) - .build(); - yaml = ConfigTabOverlayManager.constructYamlInstance(options); - - if (readMainConfig()) - return; - - velocityPlayerProvider = new VelocityPlayerProvider(mainThreadExecutor); - - hiddenPlayersManager = new HiddenPlayersManager(); - hiddenPlayersManager.addVanishProvider("/btlp hide", BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN_PLAYER_COMMAND); - hiddenPlayersManager.addVanishProvider("config.yml (hiddenPlayers)", BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN_PLAYER_CONFIG); - hiddenPlayersManager.addVanishProvider("config.yml (hiddenServers)", BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN_SERVER_CONFIG); - hiddenPlayersManager.addVanishProvider("VanishNoPacket", BukkitData.VanishNoPacket_IsVanished); - hiddenPlayersManager.addVanishProvider("SuperVanish", BukkitData.SuperVanish_IsVanished); - hiddenPlayersManager.addVanishProvider("CMI", BukkitData.CMI_IsVanished); - hiddenPlayersManager.addVanishProvider("Essentials", BukkitData.Essentials_IsVanished); - hiddenPlayersManager.addVanishProvider("ProtocolVanish", BukkitData.ProtocolVanish_IsVanished); - hiddenPlayersManager.addVanishProvider("Bukkit Player Metadata `vanished`", BukkitData.BukkitPlayerMetadataVanished); - hiddenPlayersManager.addVanishProvider("Sponge VANISH", SpongeData.Sponge_IsVanished); - hiddenPlayersManager.enable(); - - fakePlayerManagerImpl = new FakePlayerManagerImpl(plugin, iconManager, mainThreadExecutor); - - List playerProviders = new ArrayList<>(); - if (getProxy().getPluginManager().getPlugin("redisbungee").isPresent()) { - redisPlayerManager = new RedisPlayerManager(velocityPlayerProvider, this, logger); - playerProviders.add(redisPlayerManager); - plugin.getLogger().info("Hooked RedisBungee"); - } - playerProviders.add(velocityPlayerProvider); - playerProviders.add(fakePlayerManagerImpl); - this.playerProvider = new JoinedPlayerProvider(playerProviders); - - getProxy().getChannelRegistrar().register(channelIdentifier); - bukkitBridge = new BukkitBridge(asyncExecutor, mainThreadExecutor, playerPlaceholderResolver, serverPlaceholderResolver, getPlugin(), logger, velocityPlayerProvider, this, cache); - serverStateManager = new ServerStateManager(config, plugin); - dataManager = new DataManager(api, plugin, logger, velocityPlayerProvider, mainThreadExecutor, serverStateManager, bukkitBridge); - dataManager.addCompositeDataProvider(hiddenPlayersManager); - dataManager.addCompositeDataProvider(new PermissionDataProvider()); - - updateExcludedAndHiddenServerLists(); - - // register commands and update Notifier - CommandManager commandManager = getProxy().getCommandManager(); - commandManager.register(new CommandBungeeTabListPlus(plugin).register("bungeetablistplus")); - commandManager.register(new CommandBungeeTabListPlus(plugin).register("btlp")); - - getProxy().getScheduler().buildTask(plugin, new UpdateNotifier(this)).delay(15, TimeUnit.MINUTES).repeat(15, TimeUnit.MINUTES).schedule(); - - // Load updateCheck thread - if (config.checkForUpdates) { - updateChecker = new UpdateChecker(plugin); - plugin.getLogger().info("Starting UpdateChecker Task"); - getProxy().getScheduler().buildTask(plugin, updateChecker).delay(0, TimeUnit.MINUTES).repeat(UpdateChecker.interval, TimeUnit.MINUTES).schedule(); - } - - int[] serversHash = {getProxy().getAllServers().hashCode()}; - - getProxy().getScheduler().buildTask(plugin, () -> { - int hash = getProxy().getAllServers().hashCode(); - if (hash != serversHash[0]) { - serversHash[0] = hash; - scheduleSoftReload(); - } - }).delay(1, TimeUnit.MINUTES).repeat(1, TimeUnit.MINUTES).schedule(); - - MyPlatform platform = new MyPlatform(); - configTabOverlayManager = new ConfigTabOverlayManager(platform, - playerProvider, - playerPlaceholderResolver, - ImmutableList.of(new ServerCountPlaceholderResolver(dataManager), - new GlobalServerPlaceholderResolver(dataManager, serverPlaceholderResolver)), - yaml, - options, - logger, - mainThreadExecutor, - iconManager); - spectatorPassthroughTabOverlayManager = new SpectatorPassthroughTabOverlayManager(platform, mainThreadExecutor, BTLPVelocityDataKeys.DATA_KEY_GAMEMODE); - if (config.disableCustomTabListForSpectators) { - spectatorPassthroughTabOverlayManager.enable(); - } else { - spectatorPassthroughTabOverlayManager.disable(); - } - - updateTimeZoneAndGlobalCustomPlaceholders(); - - Path tabLists = getPlugin().getDataDirectory().resolve("tabLists"); - if (!Files.exists(tabLists)) { - try { - Files.createDirectories(tabLists); - } catch (IOException e) { - getLogger().log(Level.SEVERE, "Failed to create tabLists directory", e); - return; - } - try { - Files.copy(getClass().getClassLoader().getResourceAsStream("default.yml"), tabLists.resolve("default.yml")); - } catch (IOException e) { - plugin.getLogger().warn("Failed to save default config.", e); - } - } - configTabOverlayManager.reloadConfigs(ImmutableSet.of(tabLists)); - - getProxy().getEventManager().register(plugin, new TabListListener(this)); - } - - private void updateTimeZoneAndGlobalCustomPlaceholders() { - configTabOverlayManager.setTimeZone(config.getTimeZone()); - - if (config.customPlaceholders != null) { - val customPlaceholders = new HashMap(); - for (val entry : config.customPlaceholders.entrySet()) { - if (entry.getKey() != null && entry.getValue() != null) { - customPlaceholders.put(entry.getKey(), entry.getValue()); - } - } - configTabOverlayManager.setGlobalCustomPlaceholders(customPlaceholders); - } - } - - private void updateExcludedAndHiddenServerLists() { - excludedServers = new MatchingStringsCollection( - config.excludeServers != null - ? config.excludeServers - : Collections.emptyList() - ); - ExcludedServersTabOverlayProvider.onReload(); - dataManager.setHiddenServers(new MatchingStringsCollection( - config.hiddenServers != null - ? config.hiddenServers - : Collections.emptyList() - )); - dataManager.setPermanentlyHiddenPlayers(config.hiddenPlayers != null ? config.hiddenPlayers : Collections.emptyList()); - } - - // get jar file path - private String getJarName(){ - try { - Class klass = BungeeTabListPlus.class; - URL location = klass.getResource('/' + klass.getName().replace('.', '/') + ".class"); - if(location == null) return null; - String jarName = URLDecoder.decode(location.getFile(), "UTF8").split("!")[0]; - return jarName.substring(jarName.lastIndexOf("/")); - } catch (Exception e) { - return null; - } - } - - private void extractDefaultIcons(File headsFolder) { - if (!headsFolder.exists()) { - headsFolder.mkdirs(); - - try { - String jarName = getJarName(); - if(jarName == null){ - throw new IOException("Failed to get jar name from class path"); - } - - // copy default heads - ZipInputStream zipInputStream = new ZipInputStream(Files.newInputStream(new File("plugins/" + jarName).toPath())); - - ZipEntry entry; - while ((entry = zipInputStream.getNextEntry()) != null) { - if (!entry.isDirectory() && entry.getName().startsWith("heads/")) { - try { - File targetFile = new File(plugin.getDataDirectory().toFile(), entry.getName()); - targetFile.getParentFile().mkdirs(); - if (!targetFile.exists()) { - Files.copy(zipInputStream, targetFile.toPath()); - getLogger().info("Extracted " + entry.getName()); - } - } catch (IOException ex) { - getLogger().log(Level.SEVERE, "Failed to extract file " + entry.getName(), ex); - } - } - } - - zipInputStream.close(); - } catch (IOException ex) { - getLogger().log(Level.SEVERE, "Error extracting files", ex); - } - } - } - - private boolean readMainConfig() { - try { - File file = new File(plugin.getDataDirectory().toFile(), "config.yml"); - if (!file.exists()) { - config = new MainConfig(); - BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)); - config.writeWithComments(writer, yaml); - } else { - ErrorHandler.set(new ErrorHandler()); - config = yaml.loadAs(new FileInputStream(file), MainConfig.class); - ErrorHandler errorHandler = ErrorHandler.get(); - ErrorHandler.set(null); - if (!errorHandler.getEntries().isEmpty()) { - plugin.getLogger().warn(errorHandler.formatErrors(file.getName())); - } - if (config.needWrite) { - BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)); - config.writeWithComments(writer, yaml); - } - } - } catch (IOException | YAMLException ex) { - plugin.getLogger().warn("Unable to load config.yml", ex); - return true; - } - return false; - } - - public void onDisable() { - // save cache - cache.save(); - } - - /** - * Reloads most settings of the plugin - */ - public boolean reload() { - fakePlayerManagerImpl.removeConfigFakePlayers(); - - if (readMainConfig()) { - plugin.getLogger().warn("Unable to reload Config"); - return false; - } else { - updateExcludedAndHiddenServerLists(); - updateTimeZoneAndGlobalCustomPlaceholders(); - - // clear cache to force image files to be read from disk again - iconManager.clearCache(); - - Path tabLists = getPlugin().getDataDirectory().resolve("tabLists"); - configTabOverlayManager.reloadConfigs(ImmutableSet.of(tabLists)); - - fakePlayerManagerImpl.reload(); - - serverStateManager.updateConfig(config); - - if (config.disableCustomTabListForSpectators) { - spectatorPassthroughTabOverlayManager.enable(); - } else { - spectatorPassthroughTabOverlayManager.disable(); - } - return true; - } - } - - public void scheduleSoftReload() { - if (!scheduledSoftReload) { - scheduledSoftReload = true; - asyncExecutor.execute(this::softReload); - } - } - - private void softReload() { - scheduledSoftReload = false; - - if (configTabOverlayManager != null) { - configTabOverlayManager.refreshConfigs(); - - // this is a good time to save the cache - asyncExecutor.execute(cache::save); - } - } - - @Deprecated - public final void failIfNotMainThread() { - if (!mainThreadExecutor.inEventLoop()) { - getLogger().log(Level.SEVERE, "Not in main thread", new IllegalStateException("Not in main thread")); - } - } - - /** - * Getter for BukkitBridge. For internal use only. - * - * @return an instance of BukkitBridge - */ - public BukkitBridge getBridge() { - return this.bukkitBridge; - } - - /** - * Checks whether an update for BungeeTabListPlus is available. Actually - * the check is performed in a background task and this only returns the - * result. - * - * @return true if a newer version of BungeeTabListPlus is available - */ - public boolean isUpdateAvailable() { - return updateChecker != null && updateChecker.isUpdateAvailable(); - } - - public boolean isNewDevBuildAvailable() { - return updateChecker != null && updateChecker.isNewDevBuildAvailable(); - } - - public void reportError(Throwable th) { - plugin.getLogger().error("An internal error occurred! Please send the " - + "following StackTrace to the developer in order to help" - + " resolving the problem", - th); - } - - public Logger getLogger() { - return logger; - } - - public com.velocitypowered.api.proxy.ProxyServer getProxy() { - return plugin.getProxy(); - } - - private final class MyPlatform implements Platform { - - @Override - public void addEventListener(EventListener listener) { - BungeeTabListPlus.this.listeners.add(listener); - } - } -} +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package codecrafter47.bungeetablistplus; + +import codecrafter47.bungeetablistplus.api.velocity.BungeeTabListPlusAPI; +import codecrafter47.bungeetablistplus.bridge.BukkitBridge; +import codecrafter47.bungeetablistplus.cache.Cache; +import codecrafter47.bungeetablistplus.command.CommandBungeeTabListPlus; +import codecrafter47.bungeetablistplus.common.network.BridgeProtocolConstants; +import codecrafter47.bungeetablistplus.config.MainConfig; +import codecrafter47.bungeetablistplus.config.PlayersByServerComponentConfiguration; +import codecrafter47.bungeetablistplus.data.BTLPVelocityDataKeys; +import codecrafter47.bungeetablistplus.data.PermissionDataProvider; +import codecrafter47.bungeetablistplus.listener.TabListListener; +import codecrafter47.bungeetablistplus.managers.*; +import codecrafter47.bungeetablistplus.placeholder.GlobalServerPlaceholderResolver; +import codecrafter47.bungeetablistplus.placeholder.PlayerPlaceholderResolver; +import codecrafter47.bungeetablistplus.placeholder.ServerCountPlaceholderResolver; +import codecrafter47.bungeetablistplus.placeholder.ServerPlaceholderResolver; +import codecrafter47.bungeetablistplus.player.FakePlayerManagerImpl; +import codecrafter47.bungeetablistplus.tablist.ExcludedServersTabOverlayProvider; +import codecrafter47.bungeetablistplus.updater.UpdateChecker; +import codecrafter47.bungeetablistplus.updater.UpdateNotifier; +import codecrafter47.bungeetablistplus.util.ExceptionHandlingEventExecutor; +import codecrafter47.bungeetablistplus.util.GeyserCompat; +import codecrafter47.bungeetablistplus.util.MatchingStringsCollection; +import codecrafter47.bungeetablistplus.util.ReflectionUtil; +import codecrafter47.bungeetablistplus.util.VelocityPlugin; +import codecrafter47.bungeetablistplus.version.VelocityProtocolVersionProvider; +import codecrafter47.bungeetablistplus.version.ProtocolVersionProvider; +import codecrafter47.bungeetablistplus.version.ViaVersionProtocolVersionProvider; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.velocitypowered.api.command.CommandManager; +import com.velocitypowered.api.proxy.messages.ChannelIdentifier; +import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; +import de.codecrafter47.data.bukkit.api.BukkitData; +import de.codecrafter47.data.velocity.api.VelocityData; +import de.codecrafter47.data.sponge.api.SpongeData; +import de.codecrafter47.taboverlay.config.ComponentSpec; +import de.codecrafter47.taboverlay.config.ConfigTabOverlayManager; +import de.codecrafter47.taboverlay.config.ErrorHandler; +import de.codecrafter47.taboverlay.config.dsl.customplaceholder.CustomPlaceholderConfiguration; +import de.codecrafter47.taboverlay.config.icon.DefaultIconManager; +import de.codecrafter47.taboverlay.config.platform.EventListener; +import de.codecrafter47.taboverlay.config.platform.Platform; +import de.codecrafter47.taboverlay.config.player.JoinedPlayerProvider; +import de.codecrafter47.taboverlay.config.player.PlayerProvider; +import de.codecrafter47.taboverlay.config.spectator.SpectatorPassthroughTabOverlayManager; +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.EventExecutorGroup; +import io.netty.util.concurrent.MultithreadEventExecutorGroup; +import lombok.Getter; +import lombok.val; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.error.YAMLException; + +import java.io.*; +import java.lang.reflect.Field; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +/** + * Main Class of BungeeTabListPlus + * + * @author Florian Stober + */ +public class BungeeTabListPlus { + + /** + * Holds an INSTANCE of itself if the plugin is enabled + */ + private static BungeeTabListPlus INSTANCE; + @Getter + private final VelocityPlugin plugin; + + public PlayerProvider playerProvider; + @Getter + private EventExecutor mainThreadExecutor; + @Getter + private EventExecutorGroup asyncExecutor; + + @Getter + private RedisPlayerManager redisPlayerManager; + @Getter + private DataManager dataManager; + private ServerStateManager serverStateManager; + @Getter + private ServerPlaceholderResolver serverPlaceholderResolver; + + private Yaml yaml; + @Getter + private HiddenPlayersManager hiddenPlayersManager; + private PlayerPlaceholderResolver playerPlaceholderResolver; + private API api; + private Logger logger = Logger.getLogger(BungeeTabListPlus.class.getSimpleName()); + + public BungeeTabListPlus(VelocityPlugin plugin) { + this.plugin = plugin; + } + + /** + * Static getter for the current instance of the plugin + * + * @return the current instance of the plugin, null if the plugin is + * disabled + */ + public static BungeeTabListPlus getInstance(VelocityPlugin plugin) { + if (INSTANCE == null) { + INSTANCE = new BungeeTabListPlus(plugin); + } + return INSTANCE; + } + + public static BungeeTabListPlus getInstance() { + return INSTANCE; + } + + @Getter + private MainConfig config; + + private Cache cache; + @Getter + MatchingStringsCollection excludedServers; + + @Getter + private FakePlayerManagerImpl fakePlayerManagerImpl; + + private BukkitBridge bukkitBridge; + + private UpdateChecker updateChecker = null; + + @Getter + private DefaultIconManager iconManager; + + @Getter + private VelocityPlayerProvider velocityPlayerProvider; + + @Getter + private ProtocolVersionProvider protocolVersionProvider; + + @Getter + private TabViewManager tabViewManager; + + private ConfigTabOverlayManager configTabOverlayManager; + private SpectatorPassthroughTabOverlayManager spectatorPassthroughTabOverlayManager; + + @Getter + private List listeners = new ArrayList<>(); + + private transient boolean scheduledSoftReload; + @Getter + private final ChannelIdentifier channelIdentifier = MinecraftChannelIdentifier.from(BridgeProtocolConstants.CHANNEL); + + public void onLoad() { + codecrafter47.bungeetablistplus.util.ProxyServer.setProxyServer(plugin.getProxy()); + if (!plugin.getDataDirectory().toFile().exists()) { + plugin.getDataDirectory().toFile().mkdirs(); + } + + try { + Class.forName("com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo"); + } catch (ClassNotFoundException ex) { + throw new RuntimeException("You need to run at least Velocity version #196"); + } + + INSTANCE = this; + + // Hacks to get around no Team packet in Velocity + ReflectionUtil.injectTeamPacketRegistry(); + + Executor executor = (task) -> getProxy().getScheduler().buildTask(getPlugin(), task).schedule(); + + asyncExecutor = new MultithreadEventExecutorGroup(4, executor) { + @Override + protected EventExecutor newChild(Executor executor, Object... args) { + return new ExceptionHandlingEventExecutor(this, executor, logger); + } + }; + mainThreadExecutor = new ExceptionHandlingEventExecutor(null, executor, logger); + + if (getProxy().getPluginManager().getPlugin("viaversion").isPresent()) { + protocolVersionProvider = new ViaVersionProtocolVersionProvider(); + } else { + protocolVersionProvider = new VelocityProtocolVersionProvider(); + } + + this.tabViewManager = new TabViewManager(this, protocolVersionProvider); + + File headsFolder = new File(plugin.getDataDirectory().toFile(), "heads"); + extractDefaultIcons(headsFolder); + + iconManager = new DefaultIconManager(asyncExecutor, mainThreadExecutor, headsFolder.toPath(), logger); + + cache = Cache.load(new File(plugin.getDataDirectory().toFile(), "cache.dat")); + + serverPlaceholderResolver = new ServerPlaceholderResolver(cache); + playerPlaceholderResolver = new PlayerPlaceholderResolver(serverPlaceholderResolver, cache); + + api = new API(tabViewManager, iconManager, playerPlaceholderResolver, serverPlaceholderResolver, logger, this); + + try { + Field field = BungeeTabListPlusAPI.class.getDeclaredField("instance"); + field.setAccessible(true); + field.set(null, api); + } catch (NoSuchFieldException | IllegalAccessException ex) { + getLogger().log(Level.SEVERE, "Failed to initialize API", ex); + } + } + + public void onEnable() { + + ConfigTabOverlayManager.Options options = ConfigTabOverlayManager.Options.createBuilderWithDefaults() + .playerIconDataKey(BTLPVelocityDataKeys.DATA_KEY_ICON) + .playerPingDataKey(VelocityData.Velocity_Ping) + .playerInvisibleDataKey(BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN) + .playerCanSeeInvisibleDataKey(BTLPVelocityDataKeys.permission("bungeetablistplus.seevanished")) + .component(new ComponentSpec("!players_by_server", PlayersByServerComponentConfiguration.class)) + .build(); + yaml = ConfigTabOverlayManager.constructYamlInstance(options); + + if (readMainConfig()) + return; + + velocityPlayerProvider = new VelocityPlayerProvider(mainThreadExecutor); + + hiddenPlayersManager = new HiddenPlayersManager(); + hiddenPlayersManager.addVanishProvider("/btlp hide", BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN_PLAYER_COMMAND); + hiddenPlayersManager.addVanishProvider("config.yml (hiddenPlayers)", BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN_PLAYER_CONFIG); + hiddenPlayersManager.addVanishProvider("config.yml (hiddenServers)", BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN_SERVER_CONFIG); + hiddenPlayersManager.addVanishProvider("VanishNoPacket", BukkitData.VanishNoPacket_IsVanished); + hiddenPlayersManager.addVanishProvider("SuperVanish", BukkitData.SuperVanish_IsVanished); + hiddenPlayersManager.addVanishProvider("CMI", BukkitData.CMI_IsVanished); + hiddenPlayersManager.addVanishProvider("Essentials", BukkitData.Essentials_IsVanished); + hiddenPlayersManager.addVanishProvider("ProtocolVanish", BukkitData.ProtocolVanish_IsVanished); + hiddenPlayersManager.addVanishProvider("Bukkit Player Metadata `vanished`", BukkitData.BukkitPlayerMetadataVanished); + hiddenPlayersManager.addVanishProvider("Sponge VANISH", SpongeData.Sponge_IsVanished); + hiddenPlayersManager.enable(); + + fakePlayerManagerImpl = new FakePlayerManagerImpl(plugin, iconManager, mainThreadExecutor); + + List playerProviders = new ArrayList<>(); + if (getProxy().getPluginManager().getPlugin("redisbungee").isPresent()) { + redisPlayerManager = new RedisPlayerManager(velocityPlayerProvider, this, logger); + playerProviders.add(redisPlayerManager); + plugin.getLogger().info("Hooked RedisBungee"); + } + playerProviders.add(velocityPlayerProvider); + playerProviders.add(fakePlayerManagerImpl); + this.playerProvider = new JoinedPlayerProvider(playerProviders); + + getProxy().getChannelRegistrar().register(channelIdentifier); + bukkitBridge = new BukkitBridge(asyncExecutor, mainThreadExecutor, playerPlaceholderResolver, serverPlaceholderResolver, getPlugin(), logger, velocityPlayerProvider, this, cache); + serverStateManager = new ServerStateManager(config, plugin); + dataManager = new DataManager(api, plugin, logger, velocityPlayerProvider, mainThreadExecutor, serverStateManager, bukkitBridge); + dataManager.addCompositeDataProvider(hiddenPlayersManager); + dataManager.addCompositeDataProvider(new PermissionDataProvider()); + + updateExcludedAndHiddenServerLists(); + + // register commands and update Notifier + CommandManager commandManager = getProxy().getCommandManager(); + commandManager.register(new CommandBungeeTabListPlus(plugin).register("bungeetablistplus")); + commandManager.register(new CommandBungeeTabListPlus(plugin).register("btlp")); + + getProxy().getScheduler().buildTask(plugin, new UpdateNotifier(this)).delay(15, TimeUnit.MINUTES).repeat(15, TimeUnit.MINUTES).schedule(); + + // Load updateCheck thread + if (config.checkForUpdates) { + updateChecker = new UpdateChecker(plugin); + plugin.getLogger().info("Starting UpdateChecker Task"); + getProxy().getScheduler().buildTask(plugin, updateChecker).delay(0, TimeUnit.MINUTES).repeat(UpdateChecker.interval, TimeUnit.MINUTES).schedule(); + } + + int[] serversHash = {getProxy().getAllServers().hashCode()}; + + getProxy().getScheduler().buildTask(plugin, () -> { + int hash = getProxy().getAllServers().hashCode(); + if (hash != serversHash[0]) { + serversHash[0] = hash; + scheduleSoftReload(); + } + }).delay(1, TimeUnit.MINUTES).repeat(1, TimeUnit.MINUTES).schedule(); + + MyPlatform platform = new MyPlatform(); + configTabOverlayManager = new ConfigTabOverlayManager(platform, + playerProvider, + playerPlaceholderResolver, + ImmutableList.of(new ServerCountPlaceholderResolver(dataManager), + new GlobalServerPlaceholderResolver(dataManager, serverPlaceholderResolver)), + yaml, + options, + logger, + mainThreadExecutor, + iconManager); + spectatorPassthroughTabOverlayManager = new SpectatorPassthroughTabOverlayManager(platform, mainThreadExecutor, BTLPVelocityDataKeys.DATA_KEY_GAMEMODE); + if (config.disableCustomTabListForSpectators) { + spectatorPassthroughTabOverlayManager.enable(); + } else { + spectatorPassthroughTabOverlayManager.disable(); + } + + updateTimeZoneAndGlobalCustomPlaceholders(); + + Path tabLists = getPlugin().getDataDirectory().resolve("tabLists"); + if (!Files.exists(tabLists)) { + try { + Files.createDirectories(tabLists); + } catch (IOException e) { + getLogger().log(Level.SEVERE, "Failed to create tabLists directory", e); + return; + } + try { + Files.copy(getClass().getClassLoader().getResourceAsStream("default.yml"), tabLists.resolve("default.yml")); + } catch (IOException e) { + plugin.getLogger().warn("Failed to save default config.", e); + } + } + configTabOverlayManager.reloadConfigs(ImmutableSet.of(tabLists)); + + getProxy().getEventManager().register(plugin, new TabListListener(this)); + + GeyserCompat.init(); + } + + private void updateTimeZoneAndGlobalCustomPlaceholders() { + configTabOverlayManager.setTimeZone(config.getTimeZone()); + + if (config.customPlaceholders != null) { + val customPlaceholders = new HashMap(); + for (val entry : config.customPlaceholders.entrySet()) { + if (entry.getKey() != null && entry.getValue() != null) { + customPlaceholders.put(entry.getKey(), entry.getValue()); + } + } + configTabOverlayManager.setGlobalCustomPlaceholders(customPlaceholders); + } + } + + private void updateExcludedAndHiddenServerLists() { + excludedServers = new MatchingStringsCollection( + config.excludeServers != null + ? config.excludeServers + : Collections.emptyList() + ); + ExcludedServersTabOverlayProvider.onReload(); + dataManager.setHiddenServers(new MatchingStringsCollection( + config.hiddenServers != null + ? config.hiddenServers + : Collections.emptyList() + )); + dataManager.setPermanentlyHiddenPlayers(config.hiddenPlayers != null ? config.hiddenPlayers : Collections.emptyList()); + } + + // get jar file path + private String getJarName(){ + try { + Class klass = BungeeTabListPlus.class; + URL location = klass.getResource('/' + klass.getName().replace('.', '/') + ".class"); + if(location == null) return null; + String jarName = URLDecoder.decode(location.getFile(), "UTF8").split("!")[0]; + return jarName.substring(jarName.lastIndexOf("/")); + } catch (Exception e) { + return null; + } + } + + private void extractDefaultIcons(File headsFolder) { + if (!headsFolder.exists()) { + headsFolder.mkdirs(); + + try { + String jarName = getJarName(); + if(jarName == null){ + throw new IOException("Failed to get jar name from class path"); + } + + // copy default heads + ZipInputStream zipInputStream = new ZipInputStream(Files.newInputStream(new File("plugins/" + jarName).toPath())); + + ZipEntry entry; + while ((entry = zipInputStream.getNextEntry()) != null) { + if (!entry.isDirectory() && entry.getName().startsWith("heads/")) { + try { + File targetFile = new File(plugin.getDataDirectory().toFile(), entry.getName()); + targetFile.getParentFile().mkdirs(); + if (!targetFile.exists()) { + Files.copy(zipInputStream, targetFile.toPath()); + getLogger().info("Extracted " + entry.getName()); + } + } catch (IOException ex) { + getLogger().log(Level.SEVERE, "Failed to extract file " + entry.getName(), ex); + } + } + } + + zipInputStream.close(); + } catch (IOException ex) { + getLogger().log(Level.SEVERE, "Error extracting files", ex); + } + } + } + + private boolean readMainConfig() { + try { + File file = new File(plugin.getDataDirectory().toFile(), "config.yml"); + if (!file.exists()) { + config = new MainConfig(); + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)); + config.writeWithComments(writer, yaml); + } else { + ErrorHandler.set(new ErrorHandler()); + config = yaml.loadAs(new FileInputStream(file), MainConfig.class); + ErrorHandler errorHandler = ErrorHandler.get(); + ErrorHandler.set(null); + if (!errorHandler.getEntries().isEmpty()) { + plugin.getLogger().warn(errorHandler.formatErrors(file.getName())); + } + if (config.needWrite) { + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)); + config.writeWithComments(writer, yaml); + } + } + } catch (IOException | YAMLException ex) { + plugin.getLogger().warn("Unable to load config.yml", ex); + return true; + } + return false; + } + + public void onDisable() { + // save cache + cache.save(); + } + + /** + * Reloads most settings of the plugin + */ + public boolean reload() { + fakePlayerManagerImpl.removeConfigFakePlayers(); + + if (readMainConfig()) { + plugin.getLogger().warn("Unable to reload Config"); + return false; + } else { + updateExcludedAndHiddenServerLists(); + updateTimeZoneAndGlobalCustomPlaceholders(); + + // clear cache to force image files to be read from disk again + iconManager.clearCache(); + + Path tabLists = getPlugin().getDataDirectory().resolve("tabLists"); + configTabOverlayManager.reloadConfigs(ImmutableSet.of(tabLists)); + + fakePlayerManagerImpl.reload(); + + serverStateManager.updateConfig(config); + + if (config.disableCustomTabListForSpectators) { + spectatorPassthroughTabOverlayManager.enable(); + } else { + spectatorPassthroughTabOverlayManager.disable(); + } + return true; + } + } + + public void scheduleSoftReload() { + if (!scheduledSoftReload) { + scheduledSoftReload = true; + asyncExecutor.execute(this::softReload); + } + } + + private void softReload() { + scheduledSoftReload = false; + + if (configTabOverlayManager != null) { + configTabOverlayManager.refreshConfigs(); + + // this is a good time to save the cache + asyncExecutor.execute(cache::save); + } + } + + @Deprecated + public final void failIfNotMainThread() { + if (!mainThreadExecutor.inEventLoop()) { + getLogger().log(Level.SEVERE, "Not in main thread", new IllegalStateException("Not in main thread")); + } + } + + /** + * Getter for BukkitBridge. For internal use only. + * + * @return an instance of BukkitBridge + */ + public BukkitBridge getBridge() { + return this.bukkitBridge; + } + + /** + * Checks whether an update for BungeeTabListPlus is available. Actually + * the check is performed in a background task and this only returns the + * result. + * + * @return true if a newer version of BungeeTabListPlus is available + */ + public boolean isUpdateAvailable() { + return updateChecker != null && updateChecker.isUpdateAvailable(); + } + + public boolean isNewDevBuildAvailable() { + return updateChecker != null && updateChecker.isNewDevBuildAvailable(); + } + + public void reportError(Throwable th) { + plugin.getLogger().error("An internal error occurred! Please send the " + + "following StackTrace to the developer in order to help" + + " resolving the problem", + th); + } + + public Logger getLogger() { + return logger; + } + + public com.velocitypowered.api.proxy.ProxyServer getProxy() { + return plugin.getProxy(); + } + + private final class MyPlatform implements Platform { + + @Override + public void addEventListener(EventListener listener) { + BungeeTabListPlus.this.listeners.add(listener); + } + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/bridge/BukkitBridge.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/bridge/BukkitBridge.java index aa45999f..603ac664 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/bridge/BukkitBridge.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/bridge/BukkitBridge.java @@ -1,708 +1,715 @@ -/* - * Copyright (C) 2020 Florian Stober - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package codecrafter47.bungeetablistplus.bridge; - -import codecrafter47.bungeetablistplus.BungeeTabListPlus; -import codecrafter47.bungeetablistplus.cache.Cache; -import codecrafter47.bungeetablistplus.common.BTLPDataKeys; -import codecrafter47.bungeetablistplus.common.network.BridgeProtocolConstants; -import codecrafter47.bungeetablistplus.common.network.DataStreamUtils; -import codecrafter47.bungeetablistplus.common.network.TypeAdapterRegistry; -import codecrafter47.bungeetablistplus.common.util.RateLimitedExecutor; -import codecrafter47.bungeetablistplus.data.TrackingDataCache; -import codecrafter47.bungeetablistplus.managers.VelocityPlayerProvider; -import codecrafter47.bungeetablistplus.placeholder.PlayerPlaceholderResolver; -import codecrafter47.bungeetablistplus.placeholder.ServerPlaceholderResolver; -import codecrafter47.bungeetablistplus.player.VelocityPlayer; -import codecrafter47.bungeetablistplus.util.ProxyServer; -import codecrafter47.bungeetablistplus.util.VelocityPlugin; -import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteStreams; -import com.velocitypowered.api.event.Subscribe; -import com.velocitypowered.api.event.connection.DisconnectEvent; -import com.velocitypowered.api.event.connection.PluginMessageEvent; -import com.velocitypowered.api.event.connection.PostLoginEvent; -import com.velocitypowered.api.event.player.ServerConnectedEvent; -import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.api.proxy.ServerConnection; -import com.velocitypowered.api.proxy.server.RegisteredServer; -import de.codecrafter47.data.api.DataCache; -import de.codecrafter47.data.api.DataHolder; -import de.codecrafter47.data.api.DataKey; -import it.unimi.dsi.fastutil.objects.ObjectIterator; -import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; -import it.unimi.dsi.fastutil.objects.ReferenceSet; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.io.*; -import java.util.*; -import java.util.concurrent.*; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class BukkitBridge { - - private static final TypeAdapterRegistry typeAdapterRegistry = TypeAdapterRegistry.DEFAULT_TYPE_ADAPTERS; - private static final RateLimitedExecutor rlExecutor = new RateLimitedExecutor(5000); - - private final Map playerPlayerConnectionInfoMap = new ConcurrentHashMap<>(); - private final Map serverInformation = new ConcurrentHashMap<>(); - private final int proxyIdentifier = ThreadLocalRandom.current().nextInt(); - private final NetDataKeyIdMap idMap = new NetDataKeyIdMap(); - - private final ScheduledExecutorService asyncExecutor; - private final ScheduledExecutorService mainLoop; - private final PlayerPlaceholderResolver playerPlaceholderResolver; - private final ServerPlaceholderResolver serverPlaceholderResolver; - private final VelocityPlugin plugin; - private final Logger logger; - private final VelocityPlayerProvider velocityPlayerProvider; - private final BungeeTabListPlus btlp; - private final Cache cache; - - public BukkitBridge(ScheduledExecutorService asyncExecutor, ScheduledExecutorService mainLoop, PlayerPlaceholderResolver playerPlaceholderResolver, ServerPlaceholderResolver serverPlaceholderResolver, VelocityPlugin plugin, Logger logger, VelocityPlayerProvider velocityPlayerProvider, BungeeTabListPlus btlp, Cache cache) { - this.asyncExecutor = asyncExecutor; - this.mainLoop = mainLoop; - this.playerPlaceholderResolver = playerPlaceholderResolver; - this.serverPlaceholderResolver = serverPlaceholderResolver; - this.plugin = plugin; - this.logger = logger; - this.velocityPlayerProvider = velocityPlayerProvider; - this.btlp = btlp; - this.cache = cache; - ProxyServer.getInstance().getEventManager().register(plugin, this); - asyncExecutor.scheduleWithFixedDelay(this::sendIntroducePackets, 100, 100, TimeUnit.MILLISECONDS); - asyncExecutor.scheduleWithFixedDelay(this::resendUnconfirmedMessages, 2, 2, TimeUnit.SECONDS); - asyncExecutor.scheduleWithFixedDelay(this::removeObsoleteServerConnections, 5, 5, TimeUnit.SECONDS); - } - - @Subscribe - public void onPlayerConnect(PostLoginEvent event) { - playerPlayerConnectionInfoMap.put(event.getPlayer(), new PlayerConnectionInfo()); - } - - @Subscribe - public void onServerChange(ServerConnectedEvent event) { - PlayerConnectionInfo previous = playerPlayerConnectionInfoMap.put(event.getPlayer(), new PlayerConnectionInfo()); - if (previous != null) { - PlayerBridgeDataCache playerBridgeData = previous.playerBridgeData; - if (playerBridgeData != null) { - synchronized (playerBridgeData) { - playerBridgeData.connection = null; - playerBridgeData.reset(); - } - } - } - } - - @Subscribe - public void onPlayerDisconnect(DisconnectEvent event) { - playerPlayerConnectionInfoMap.remove(event.getPlayer()); - } - - @Subscribe - public void onPluginMessage(PluginMessageEvent event) { - if (event.getIdentifier().equals(btlp.getChannelIdentifier())) { - if (event.getTarget() instanceof Player && event.getSource() instanceof ServerConnection) { - - Player player = (Player) event.getTarget(); - ServerConnection server = (ServerConnection) event.getSource(); - - event.setResult(PluginMessageEvent.ForwardResult.handled()); - - DataInput input = new DataInputStream(new ByteArrayInputStream(event.getData())); - - try { - handlePluginMessage(player, server, input); - } catch (Throwable e) { - rlExecutor.execute(() -> { - logger.log(Level.SEVERE, "Unexpected error: ", e); - }); - } - } else { - // hacking attempt - event.setResult(PluginMessageEvent.ForwardResult.handled()); - } - } - } - - public String getStatus(RegisteredServer server) { - Collection players = server.getPlayersConnected(); - if (players.isEmpty()) { - return "no players"; - } - int error_bungee = 0, unavailable = 0, working = 0, stale = 0, incomplete = 0; - for (Player player : players) { - PlayerConnectionInfo connectionInfo = playerPlayerConnectionInfoMap.get(player); - - if (connectionInfo == null) { - error_bungee++; - continue; - } - - if (connectionInfo.isConnectionValid == false) { - unavailable++; - continue; - } - - if (connectionInfo.hasReceived == false) { - incomplete++; - continue; - } - - PlayerBridgeDataCache bridgeData = connectionInfo.playerBridgeData; - if (bridgeData == null) { - error_bungee++; - continue; - } - - if (bridgeData.nextOutgoingMessageId - bridgeData.lastConfirmed > 2 && System.currentTimeMillis() - bridgeData.lastMessageSent > 5000) { - stale++; - continue; - } - } - - if (unavailable == players.size()) { - return "not installed or incompatible version"; - } - - String status = "working: " + working; - if (unavailable != 0) { - status += ", unavailable: " + unavailable; - } - if (incomplete != 0) { - status += ", unavailable: " + incomplete; - } - if (stale != 0) { - status += ", stale: " + stale; - } - if (error_bungee != 0) { - status += ", error_bungee: " + error_bungee; - } - return status; - } - - private void handlePluginMessage(Player player, ServerConnection server, DataInput input) throws IOException { - PlayerConnectionInfo connectionInfo = playerPlayerConnectionInfoMap.get(player); - - if (connectionInfo == null) { - return; - } - - int messageId = input.readUnsignedByte(); - - if (messageId == BridgeProtocolConstants.MESSAGE_ID_INTRODUCE) { - - int serverIdentifier = input.readInt(); - int protocolVersion = input.readInt(); - int minimumCompatibleProtocolVersion = input.readInt(); - String proxyPluginVersion = input.readUTF(); - - int connectionId = serverIdentifier + proxyIdentifier; - - if (connectionId == connectionInfo.connectionIdentifier) { - return; - } - - if (BridgeProtocolConstants.VERSION < minimumCompatibleProtocolVersion) { - rlExecutor.execute(() -> { - logger.log(Level.WARNING, "Incompatible version of BTLP on server " + server.getServerInfo().getName() + " detected: " + proxyPluginVersion); - }); - return; - } - - // reset connection state - connectionInfo.connectionIdentifier = connectionId; - connectionInfo.isConnectionValid = true; - connectionInfo.nextIntroducePacketDelay = 1; - connectionInfo.introducePacketDelay = 1; - connectionInfo.hasReceived = false; - connectionInfo.protocolVersion = Integer.min(BridgeProtocolConstants.VERSION, protocolVersion); - - VelocityPlayer velocityPlayer = velocityPlayerProvider.getPlayerIfPresent(player); - if (velocityPlayer == null) { - logger.severe("Internal error - Bridge functionality not available for " + player.getUsername()); - } else { - connectionInfo.playerBridgeData = velocityPlayer.getBridgeDataCache(); - connectionInfo.playerBridgeData.setConnectionId(connectionId); - connectionInfo.playerBridgeData.connection = server; - connectionInfo.playerBridgeData.requestMissingData(); - } - connectionInfo.serverBridgeData = getServerDataCache(server.getServerInfo().getName()); - connectionInfo.serverBridgeData.setConnectionId(connectionId); - connectionInfo.serverBridgeData.addConnection(server); - connectionInfo.serverBridgeData.requestMissingData(); - - // send ACK 0 - ByteArrayOutputStream byteArrayOutput = new ByteArrayOutputStream(); - DataOutput output = new DataOutputStream(byteArrayOutput); - - output.writeByte(BridgeProtocolConstants.MESSAGE_ID_ACK); - output.writeInt(connectionId); - output.writeInt(0); - - byte[] message = byteArrayOutput.toByteArray(); - server.sendPluginMessage(btlp.getChannelIdentifier(), message); - } else { - - if (!connectionInfo.isConnectionValid) { - return; - } - - int connectionId = input.readInt(); - - if (connectionId != connectionInfo.connectionIdentifier) { - return; - } - - connectionInfo.hasReceived = true; - - int sequenceNumber = input.readInt(); - - BridgeData bridgeData; - boolean isServerMessage; - - if ((messageId & 0x80) == 0) { - bridgeData = connectionInfo.playerBridgeData; - isServerMessage = false; - } else { - bridgeData = connectionInfo.serverBridgeData; - isServerMessage = true; - } - - if (bridgeData == null) { - return; - } - - messageId = messageId & 0x7f; - - synchronized (bridgeData) { - - if (messageId == BridgeProtocolConstants.MESSAGE_ID_ACK) { - - int confirmed = sequenceNumber - bridgeData.lastConfirmed; - - if (confirmed > bridgeData.messagesPendingConfirmation.size()) { - return; - } - - while (confirmed-- > 0) { - bridgeData.lastConfirmed++; - bridgeData.messagesPendingConfirmation.remove(); - } - } else if (messageId == BridgeProtocolConstants.MESSAGE_ID_UPDATE_DATA) { - - if (sequenceNumber > bridgeData.nextIncomingMessageId) { - // ignore messages from the future - return; - } - - ByteArrayOutputStream byteArrayOutput = new ByteArrayOutputStream(); - DataOutput output = new DataOutputStream(byteArrayOutput); - - output.writeByte(BridgeProtocolConstants.MESSAGE_ID_ACK | (isServerMessage ? 0x80 : 0x00)); - output.writeInt(connectionId); - output.writeInt(sequenceNumber); - - byte[] message = byteArrayOutput.toByteArray(); - server.sendPluginMessage(btlp.getChannelIdentifier(), message); - - if (sequenceNumber < bridgeData.nextIncomingMessageId) { - // ignore messages from the past after sending ACK - return; - } - - bridgeData.nextIncomingMessageId++; - - int size = input.readInt(); - if (size > 0) { - onDataReceived(bridgeData, input, size); - } - } else { - throw new IllegalArgumentException("Unexpected message id: " + messageId); - } - } - } - } - - @SuppressWarnings("unchecked") - private void onDataReceived(DataCache cache, DataInput input, int size) throws IOException { - if (size == 1) { - int netId = input.readInt(); - DataKey key = idMap.getKey(netId); - - if (key == null) { - throw new AssertionError("Received unexpected data key net id " + netId); - } - - boolean removed = input.readBoolean(); - - if (removed) { - - mainLoop.execute(() -> cache.updateValue(key, null)); - } else { - - Object value = typeAdapterRegistry.getTypeAdapter(key.getType()).read(input); - mainLoop.execute(() -> cache.updateValue((DataKey) key, value)); - } - } else { - Object[] update = new Object[size * 2]; - - for (int i = 0; i < update.length; i += 2) { - int netId = input.readInt(); - DataKey key = idMap.getKey(netId); - - if (key == null) { - throw new AssertionError("Received unexpected data key net id " + netId); - } - - boolean removed = input.readBoolean(); - Object value = null; - - if (!removed) { - value = typeAdapterRegistry.getTypeAdapter(key.getType()).read(input); - } - - update[i] = key; - update[i + 1] = value; - } - - mainLoop.execute(() -> { - for (int i = 0; i < update.length; i += 2) { - cache.updateValue((DataKey) update[i], update[i + 1]); - } - }); - } - } - - /** - * Sends introduce packets to the proxy to try to establish a connection. - *

- * Should be called periodically, recommended interval is 100ms to 1s. - */ - private void sendIntroducePackets() { - for (Map.Entry entry : playerPlayerConnectionInfoMap.entrySet()) { - ServerConnection server = entry.getKey().getCurrentServer().orElse(null); - PlayerConnectionInfo connectionInfo = entry.getValue(); - if (server != null && !connectionInfo.hasReceived) { - if (--connectionInfo.nextIntroducePacketDelay <= 0) { - connectionInfo.nextIntroducePacketDelay = connectionInfo.introducePacketDelay++; - - try { - - ByteArrayOutputStream byteArrayOutput = new ByteArrayOutputStream(); - DataOutput output = new DataOutputStream(byteArrayOutput); - - output.writeByte(BridgeProtocolConstants.MESSAGE_ID_INTRODUCE); - output.writeInt(proxyIdentifier); - output.writeInt(BridgeProtocolConstants.VERSION); - output.writeInt(BridgeProtocolConstants.VERSION); - output.writeUTF(plugin.getVersion()); - - byte[] message = byteArrayOutput.toByteArray(); - server.sendPluginMessage(btlp.getChannelIdentifier(), message); - } catch (Throwable th) { - rlExecutor.execute(() -> { - logger.log(Level.SEVERE, "Unexpected error", th); - }); - } - } - } - } - } - - /** - * Sends unconfirmed messages to the proxy yet another time, to ensure their arrival. - *

- * Should be called periodically, recommended interval is 1s to 10s. - */ - private void resendUnconfirmedMessages() { - long now = System.currentTimeMillis(); - - for (Map.Entry e : playerPlayerConnectionInfoMap.entrySet()) { - Player player = e.getKey(); - ServerConnection server = player.getCurrentServer().orElse(null); - PlayerConnectionInfo connectionInfo = e.getValue(); - - if (!connectionInfo.isConnectionValid) { - continue; - } - - BridgeData bridgeData = connectionInfo.playerBridgeData; - - if (bridgeData == null) { - continue; - } - - if (server != null && bridgeData.messagesPendingConfirmation.size() > 0 && (now > bridgeData.lastMessageSent + 1000 || bridgeData.messagesPendingConfirmation.size() > 5)) { - for (byte[] message : bridgeData.messagesPendingConfirmation) { - server.sendPluginMessage(btlp.getChannelIdentifier(), message); - } - } - } - - for (ServerBridgeDataCache bridgeData : serverInformation.values()) { - ServerConnection server = bridgeData.getConnection(); - - if (server != null && bridgeData.messagesPendingConfirmation.size() > 0 && (now > bridgeData.lastMessageSent + 1000 || bridgeData.messagesPendingConfirmation.size() > 5)) { - for (byte[] message : bridgeData.messagesPendingConfirmation) { - server.sendPluginMessage(btlp.getChannelIdentifier(), message); - } - } - } - } - - private void checkForThirdPartyVariables(String serverName, ServerBridgeDataCache dataCache) { - mainLoop.execute(() -> { - dataCache.addDataChangeListener(BTLPDataKeys.REGISTERED_THIRD_PARTY_VARIABLES, () -> updateBridgePlaceholders(serverName, dataCache)); - updateBridgePlaceholders(serverName, dataCache); - dataCache.addDataChangeListener(BTLPDataKeys.REGISTERED_THIRD_PARTY_SERVER_VARIABLES, () -> updateBridgeServerPlaceholders(serverName, dataCache)); - updateBridgeServerPlaceholders(serverName, dataCache); - dataCache.addDataChangeListener(BTLPDataKeys.PAPI_REGISTERED_PLACEHOLDER_PLUGINS, () -> updatePlaceholderAPIPlaceholders(serverName, dataCache)); - updatePlaceholderAPIPlaceholders(serverName, dataCache); - }); - } - - private void updateBridgePlaceholders(String serverName, ServerBridgeDataCache dataCache) { - List variables = dataCache.get(BTLPDataKeys.REGISTERED_THIRD_PARTY_VARIABLES); - if (variables != null) { - for (String variable : variables) { - playerPlaceholderResolver.addBridgeCustomPlaceholderDataKey(variable, BTLPDataKeys.createThirdPartyVariableDataKey(variable)); - } - cache.updateCustomPlaceholdersBridge(serverName, variables); - btlp.scheduleSoftReload(); - } - } - - private void updateBridgeServerPlaceholders(String serverName, ServerBridgeDataCache dataCache) { - List variables = dataCache.get(BTLPDataKeys.REGISTERED_THIRD_PARTY_SERVER_VARIABLES); - if (variables != null) { - for (String variable : variables) { - serverPlaceholderResolver.addBridgeCustomPlaceholderServerDataKey(variable, BTLPDataKeys.createThirdPartyServerVariableDataKey(variable)); - } - cache.updateCustomServerPlaceholdersBridge(serverName, variables); - btlp.scheduleSoftReload(); - } - } - - private void updatePlaceholderAPIPlaceholders(String serverName, ServerBridgeDataCache dataCache) { - List plugins = dataCache.get(BTLPDataKeys.PAPI_REGISTERED_PLACEHOLDER_PLUGINS); - if (plugins != null) { - playerPlaceholderResolver.addPlaceholderAPIPluginPrefixes(plugins); - cache.updatePAPIPrefixes(serverName, plugins); - btlp.scheduleSoftReload(); - } - } - - private void removeObsoleteServerConnections() { - for (ServerBridgeDataCache cache : serverInformation.values()) { - cache.removeObsoleteConnections(); - } - } - - public PlayerBridgeDataCache createDataCacheForPlayer(@Nonnull VelocityPlayer player) { - return new PlayerBridgeDataCache(); - } - - public DataHolder getServerDataHolder(@Nonnull String server) { - return getServerDataCache(server); - } - - private ServerBridgeDataCache getServerDataCache(@Nonnull String serverName) { - if (!serverInformation.containsKey(serverName)) { - serverInformation.computeIfAbsent(serverName, key -> { - ServerBridgeDataCache dataCache = new ServerBridgeDataCache(); - checkForThirdPartyVariables(serverName, dataCache); - return dataCache; - }); - } - return serverInformation.get(serverName); - } - - private static class PlayerConnectionInfo { - boolean isConnectionValid = false; - boolean hasReceived = false; - int connectionIdentifier = 0; - int protocolVersion = 0; - int nextIntroducePacketDelay = 1; - int introducePacketDelay = 1; - @Nullable - PlayerBridgeDataCache playerBridgeData = null; - @Nullable - ServerBridgeDataCache serverBridgeData = null; - } - - private abstract class BridgeData extends TrackingDataCache { - - final Queue messagesPendingConfirmation = new ConcurrentLinkedQueue<>(); - int lastConfirmed = 0; - int nextOutgoingMessageId = 1; - int nextIncomingMessageId = 1; - long lastMessageSent = 0; - int connectionId; - - boolean requestAll = false; - - @Nullable - abstract ServerConnection getConnection(); - - @Override - protected void addActiveKey(DataKey key) { - super.addActiveKey(key); - - try { - synchronized (this) { - ServerConnection connection = getConnection(); - if (connection != null) { - ByteArrayDataOutput data = ByteStreams.newDataOutput(); - data.writeByte(this instanceof PlayerBridgeDataCache ? BridgeProtocolConstants.MESSAGE_ID_REQUEST_DATA : BridgeProtocolConstants.MESSAGE_ID_REQUEST_DATA_SERVER); - data.writeInt(connectionId); - data.writeInt(nextOutgoingMessageId++); - data.writeInt(1); - DataStreamUtils.writeDataKey(data, key); - data.writeInt(idMap.getNetId(key)); - byte[] message = data.toByteArray(); - messagesPendingConfirmation.add(message); - lastMessageSent = System.currentTimeMillis(); - connection.sendPluginMessage(btlp.getChannelIdentifier(), message); - } else { - requestAll = true; - } - } - } catch (Throwable th) { - rlExecutor.execute(() -> { - logger.log(Level.SEVERE, "Unexpected exception", th); - }); - requestAll = true; - } - } - - void setConnectionId(int connectionId) { - if (this.connectionId != connectionId) { - reset(); - } - this.connectionId = connectionId; - } - - void reset() { - synchronized (this) { - requestAll = true; - messagesPendingConfirmation.clear(); - lastConfirmed = 0; - nextOutgoingMessageId = 1; - nextIncomingMessageId = 1; - lastMessageSent = 0; - Collection> queriedKeys = new ArrayList<>(getActiveKeys()); - mainLoop.execute(() -> { - for (DataKey key : queriedKeys) { - updateValue(key, null); - } - }); - } - } - - void requestMissingData() throws IOException { - synchronized (this) { - if (requestAll) { - ServerConnection connection = getConnection(); - if (connection != null) { - List> keys = new ArrayList<>(getActiveKeys()); - - ByteArrayDataOutput data = ByteStreams.newDataOutput(); - data.writeByte(this instanceof PlayerBridgeDataCache ? BridgeProtocolConstants.MESSAGE_ID_REQUEST_DATA : BridgeProtocolConstants.MESSAGE_ID_REQUEST_DATA_SERVER); - data.writeInt(connectionId); - data.writeInt(nextOutgoingMessageId++); - data.writeInt(keys.size()); - for (DataKey key : keys) { - DataStreamUtils.writeDataKey(data, key); - data.writeInt(idMap.getNetId(key)); - } - byte[] message = data.toByteArray(); - messagesPendingConfirmation.add(message); - lastMessageSent = System.currentTimeMillis(); - connection.sendPluginMessage(btlp.getChannelIdentifier(), message); - } - requestAll = false; - } - } - } - } - - public class PlayerBridgeDataCache extends BridgeData { - @Nullable - private volatile ServerConnection connection = null; - - @Override - @Nullable - ServerConnection getConnection() { - return connection; - } - } - - private class ServerBridgeDataCache extends BridgeData { - private final ReferenceSet connections = new ReferenceOpenHashSet<>(); - - private void addConnection(@Nonnull ServerConnection server) { - synchronized (this) { - connections.add(server); - } - } - - @Override - @Nullable - ServerConnection getConnection() { - synchronized (this) { - ObjectIterator iterator = connections.iterator(); - while (iterator.hasNext()) { - try { - ServerConnection server = iterator.next(); - if (server.getServer().ping().join() != null) { - return server; - } - } catch (Exception ignored){} - iterator.remove(); - } - return null; - } - } - - private void removeObsoleteConnections() { - synchronized (this) { - ObjectIterator iterator = connections.iterator(); - while (iterator.hasNext()) { - ServerConnection server = iterator.next(); - try { - if (server.getServer().ping().join() == null) { - iterator.remove(); - } - } catch (Exception ignored) { - iterator.remove(); - } - } - } - } - - @Override - void reset() { - synchronized (this) { - super.reset(); - connections.clear(); - } - } - } -} +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package codecrafter47.bungeetablistplus.bridge; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.cache.Cache; +import codecrafter47.bungeetablistplus.common.BTLPDataKeys; +import codecrafter47.bungeetablistplus.common.network.BridgeProtocolConstants; +import codecrafter47.bungeetablistplus.common.network.DataStreamUtils; +import codecrafter47.bungeetablistplus.common.network.TypeAdapterRegistry; +import codecrafter47.bungeetablistplus.common.util.RateLimitedExecutor; +import codecrafter47.bungeetablistplus.data.TrackingDataCache; +import codecrafter47.bungeetablistplus.managers.VelocityPlayerProvider; +import codecrafter47.bungeetablistplus.placeholder.PlayerPlaceholderResolver; +import codecrafter47.bungeetablistplus.placeholder.ServerPlaceholderResolver; +import codecrafter47.bungeetablistplus.player.VelocityPlayer; +import codecrafter47.bungeetablistplus.util.ProxyServer; +import codecrafter47.bungeetablistplus.util.VelocityPlugin; +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.DisconnectEvent; +import com.velocitypowered.api.event.connection.PluginMessageEvent; +import com.velocitypowered.api.event.connection.PostLoginEvent; +import com.velocitypowered.api.event.player.ServerConnectedEvent; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ServerConnection; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import de.codecrafter47.data.api.DataCache; +import de.codecrafter47.data.api.DataHolder; +import de.codecrafter47.data.api.DataKey; +import it.unimi.dsi.fastutil.objects.ObjectIterator; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import it.unimi.dsi.fastutil.objects.ReferenceSet; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.*; +import java.util.*; +import java.util.concurrent.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class BukkitBridge { + + private static final TypeAdapterRegistry typeAdapterRegistry = TypeAdapterRegistry.DEFAULT_TYPE_ADAPTERS; + private static final RateLimitedExecutor rlExecutor = new RateLimitedExecutor(5000); + + private final Map playerPlayerConnectionInfoMap = new ConcurrentHashMap<>(); + private final Map serverInformation = new ConcurrentHashMap<>(); + private final int proxyIdentifier = ThreadLocalRandom.current().nextInt(); + private final NetDataKeyIdMap idMap = new NetDataKeyIdMap(); + + private final ScheduledExecutorService asyncExecutor; + private final ScheduledExecutorService mainLoop; + private final PlayerPlaceholderResolver playerPlaceholderResolver; + private final ServerPlaceholderResolver serverPlaceholderResolver; + private final VelocityPlugin plugin; + private final Logger logger; + private final VelocityPlayerProvider velocityPlayerProvider; + private final BungeeTabListPlus btlp; + private final Cache cache; + + public BukkitBridge(ScheduledExecutorService asyncExecutor, ScheduledExecutorService mainLoop, PlayerPlaceholderResolver playerPlaceholderResolver, ServerPlaceholderResolver serverPlaceholderResolver, VelocityPlugin plugin, Logger logger, VelocityPlayerProvider velocityPlayerProvider, BungeeTabListPlus btlp, Cache cache) { + this.asyncExecutor = asyncExecutor; + this.mainLoop = mainLoop; + this.playerPlaceholderResolver = playerPlaceholderResolver; + this.serverPlaceholderResolver = serverPlaceholderResolver; + this.plugin = plugin; + this.logger = logger; + this.velocityPlayerProvider = velocityPlayerProvider; + this.btlp = btlp; + this.cache = cache; + ProxyServer.getInstance().getEventManager().register(plugin, this); + asyncExecutor.scheduleWithFixedDelay(this::sendIntroducePackets, 100, 100, TimeUnit.MILLISECONDS); + asyncExecutor.scheduleWithFixedDelay(this::resendUnconfirmedMessages, 2, 2, TimeUnit.SECONDS); + asyncExecutor.scheduleWithFixedDelay(this::removeObsoleteServerConnections, 5, 5, TimeUnit.SECONDS); + } + + @Subscribe + public void onPlayerConnect(PostLoginEvent event) { + playerPlayerConnectionInfoMap.put(event.getPlayer(), new PlayerConnectionInfo()); + } + + @Subscribe + public void onServerChange(ServerConnectedEvent event) { + PlayerConnectionInfo previous = playerPlayerConnectionInfoMap.put(event.getPlayer(), new PlayerConnectionInfo()); + if (previous != null) { + PlayerBridgeDataCache playerBridgeData = previous.playerBridgeData; + if (playerBridgeData != null) { + synchronized (playerBridgeData) { + playerBridgeData.connection = null; + playerBridgeData.reset(); + } + } + } + } + + @Subscribe + public void onPlayerDisconnect(DisconnectEvent event) { + playerPlayerConnectionInfoMap.remove(event.getPlayer()); + } + + @Subscribe + public void onPluginMessage(PluginMessageEvent event) { + if (event.getIdentifier().equals(btlp.getChannelIdentifier())) { + if (event.getTarget() instanceof Player && event.getSource() instanceof ServerConnection) { + + Player player = (Player) event.getTarget(); + ServerConnection server = (ServerConnection) event.getSource(); + + event.setResult(PluginMessageEvent.ForwardResult.handled()); + + DataInput input = new DataInputStream(new ByteArrayInputStream(event.getData())); + + try { + handlePluginMessage(player, server, input); + } catch (Throwable e) { + rlExecutor.execute(() -> { + logger.log(Level.SEVERE, "Unexpected error: ", e); + }); + } + } else { + // hacking attempt + event.setResult(PluginMessageEvent.ForwardResult.handled()); + } + } + } + + public String getStatus(RegisteredServer server) { + Collection players = server.getPlayersConnected(); + if (players.isEmpty()) { + return "no players"; + } + int error_bungee = 0, unavailable = 0, working = 0, stale = 0, incomplete = 0; + for (Player player : players) { + PlayerConnectionInfo connectionInfo = playerPlayerConnectionInfoMap.get(player); + + if (connectionInfo == null) { + error_bungee++; + continue; + } + + if (connectionInfo.isConnectionValid == false) { + unavailable++; + continue; + } + + if (connectionInfo.hasReceived == false) { + incomplete++; + continue; + } + + PlayerBridgeDataCache bridgeData = connectionInfo.playerBridgeData; + if (bridgeData == null) { + error_bungee++; + continue; + } + + if (bridgeData.nextOutgoingMessageId - bridgeData.lastConfirmed > 2 && System.currentTimeMillis() - bridgeData.lastMessageSent > 5000) { + stale++; + continue; + } + } + + if (unavailable == players.size()) { + return "not installed or incompatible version"; + } + + String status = "working: " + working; + if (unavailable != 0) { + status += ", unavailable: " + unavailable; + } + if (incomplete != 0) { + status += ", unavailable: " + incomplete; + } + if (stale != 0) { + status += ", stale: " + stale; + } + if (error_bungee != 0) { + status += ", error_bungee: " + error_bungee; + } + return status; + } + + private void handlePluginMessage(Player player, ServerConnection server, DataInput input) throws IOException { + PlayerConnectionInfo connectionInfo = playerPlayerConnectionInfoMap.get(player); + + if (connectionInfo == null) { + return; + } + + int messageId = input.readUnsignedByte(); + + if (messageId == BridgeProtocolConstants.MESSAGE_ID_INTRODUCE) { + + int serverIdentifier = input.readInt(); + int protocolVersion = input.readInt(); + int minimumCompatibleProtocolVersion = input.readInt(); + String proxyPluginVersion = input.readUTF(); + + int connectionId = serverIdentifier + proxyIdentifier; + + if (connectionId == connectionInfo.connectionIdentifier) { + return; + } + + if (BridgeProtocolConstants.VERSION < minimumCompatibleProtocolVersion) { + rlExecutor.execute(() -> { + logger.log(Level.WARNING, "Incompatible version of BTLP on server " + server.getServerInfo().getName() + " detected: " + proxyPluginVersion); + }); + return; + } + + // reset connection state + connectionInfo.connectionIdentifier = connectionId; + connectionInfo.isConnectionValid = true; + connectionInfo.nextIntroducePacketDelay = 1; + connectionInfo.introducePacketDelay = 1; + connectionInfo.hasReceived = false; + connectionInfo.protocolVersion = Integer.min(BridgeProtocolConstants.VERSION, protocolVersion); + + VelocityPlayer velocityPlayer = velocityPlayerProvider.getPlayerIfPresent(player); + if (velocityPlayer == null) { + logger.severe("Internal error - Bridge functionality not available for " + player.getUsername()); + } else { + connectionInfo.playerBridgeData = velocityPlayer.getBridgeDataCache(); + connectionInfo.playerBridgeData.setConnectionId(connectionId); + connectionInfo.playerBridgeData.connection = server; + connectionInfo.playerBridgeData.requestMissingData(); + } + connectionInfo.serverBridgeData = getServerDataCache(server.getServerInfo().getName()); + connectionInfo.serverBridgeData.setConnectionId(connectionId); + connectionInfo.serverBridgeData.addConnection(server); + connectionInfo.serverBridgeData.requestMissingData(); + + // send ACK 0 + ByteArrayOutputStream byteArrayOutput = new ByteArrayOutputStream(); + DataOutput output = new DataOutputStream(byteArrayOutput); + + output.writeByte(BridgeProtocolConstants.MESSAGE_ID_ACK); + output.writeInt(connectionId); + output.writeInt(0); + + byte[] message = byteArrayOutput.toByteArray(); + server.sendPluginMessage(btlp.getChannelIdentifier(), message); + } else { + + if (!connectionInfo.isConnectionValid) { + return; + } + + int connectionId = input.readInt(); + + if (connectionId != connectionInfo.connectionIdentifier) { + return; + } + + connectionInfo.hasReceived = true; + + int sequenceNumber = input.readInt(); + + BridgeData bridgeData; + boolean isServerMessage; + + if ((messageId & 0x80) == 0) { + bridgeData = connectionInfo.playerBridgeData; + isServerMessage = false; + } else { + bridgeData = connectionInfo.serverBridgeData; + isServerMessage = true; + } + + if (bridgeData == null) { + return; + } + + messageId = messageId & 0x7f; + + synchronized (bridgeData) { + + if (messageId == BridgeProtocolConstants.MESSAGE_ID_ACK) { + + int confirmed = sequenceNumber - bridgeData.lastConfirmed; + + if (confirmed > bridgeData.messagesPendingConfirmation.size()) { + return; + } + + while (confirmed-- > 0) { + bridgeData.lastConfirmed++; + bridgeData.messagesPendingConfirmation.remove(); + } + } else if (messageId == BridgeProtocolConstants.MESSAGE_ID_UPDATE_DATA) { + + if (sequenceNumber > bridgeData.nextIncomingMessageId) { + // ignore messages from the future + return; + } + + ByteArrayOutputStream byteArrayOutput = new ByteArrayOutputStream(); + DataOutput output = new DataOutputStream(byteArrayOutput); + + output.writeByte(BridgeProtocolConstants.MESSAGE_ID_ACK | (isServerMessage ? 0x80 : 0x00)); + output.writeInt(connectionId); + output.writeInt(sequenceNumber); + + byte[] message = byteArrayOutput.toByteArray(); + server.sendPluginMessage(btlp.getChannelIdentifier(), message); + + if (sequenceNumber < bridgeData.nextIncomingMessageId) { + // ignore messages from the past after sending ACK + return; + } + + bridgeData.nextIncomingMessageId++; + + int size = input.readInt(); + if (size > 0) { + onDataReceived(bridgeData, input, size); + } + } else { + throw new IllegalArgumentException("Unexpected message id: " + messageId); + } + } + } + } + + @SuppressWarnings("unchecked") + private void onDataReceived(DataCache cache, DataInput input, int size) throws IOException { + if (size == 1) { + int netId = input.readInt(); + DataKey key = idMap.getKey(netId); + + if (key == null) { + throw new AssertionError("Received unexpected data key net id " + netId); + } + + boolean removed = input.readBoolean(); + + if (removed) { + + mainLoop.execute(() -> cache.updateValue(key, null)); + } else { + + Object value = typeAdapterRegistry.getTypeAdapter(key.getType()).read(input); + mainLoop.execute(() -> cache.updateValue((DataKey) key, value)); + } + } else { + Object[] update = new Object[size * 2]; + + for (int i = 0; i < update.length; i += 2) { + int netId = input.readInt(); + DataKey key = idMap.getKey(netId); + + if (key == null) { + throw new AssertionError("Received unexpected data key net id " + netId); + } + + boolean removed = input.readBoolean(); + Object value = null; + + if (!removed) { + value = typeAdapterRegistry.getTypeAdapter(key.getType()).read(input); + } + + update[i] = key; + update[i + 1] = value; + } + + mainLoop.execute(() -> { + for (int i = 0; i < update.length; i += 2) { + cache.updateValue((DataKey) update[i], update[i + 1]); + } + }); + } + } + + /** + * Sends introduce packets to the proxy to try to establish a connection. + *

+ * Should be called periodically, recommended interval is 100ms to 1s. + */ + private void sendIntroducePackets() { + for (Map.Entry entry : playerPlayerConnectionInfoMap.entrySet()) { + ServerConnection server = entry.getKey().getCurrentServer().orElse(null); + PlayerConnectionInfo connectionInfo = entry.getValue(); + if (server != null && !connectionInfo.hasReceived) { + if (--connectionInfo.nextIntroducePacketDelay <= 0) { + connectionInfo.nextIntroducePacketDelay = connectionInfo.introducePacketDelay++; + + try { + + ByteArrayOutputStream byteArrayOutput = new ByteArrayOutputStream(); + DataOutput output = new DataOutputStream(byteArrayOutput); + + output.writeByte(BridgeProtocolConstants.MESSAGE_ID_INTRODUCE); + output.writeInt(proxyIdentifier); + output.writeInt(BridgeProtocolConstants.VERSION); + output.writeInt(BridgeProtocolConstants.VERSION); + output.writeUTF(plugin.getVersion()); + + byte[] message = byteArrayOutput.toByteArray(); + server.sendPluginMessage(btlp.getChannelIdentifier(), message); + } catch (Throwable th) { + rlExecutor.execute(() -> { + logger.log(Level.SEVERE, "Unexpected error", th); + }); + } + } + } + } + } + + /** + * Sends unconfirmed messages to the proxy yet another time, to ensure their arrival. + *

+ * Should be called periodically, recommended interval is 1s to 10s. + */ + private void resendUnconfirmedMessages() { + long now = System.currentTimeMillis(); + + for (Map.Entry e : playerPlayerConnectionInfoMap.entrySet()) { + Player player = e.getKey(); + ServerConnection server = player.getCurrentServer().orElse(null); + PlayerConnectionInfo connectionInfo = e.getValue(); + + if (!connectionInfo.isConnectionValid) { + continue; + } + + BridgeData bridgeData = connectionInfo.playerBridgeData; + + if (bridgeData == null) { + continue; + } + + if (server != null && bridgeData.messagesPendingConfirmation.size() > 0 && (now > bridgeData.lastMessageSent + 1000 || bridgeData.messagesPendingConfirmation.size() > 5)) { + for (byte[] message : bridgeData.messagesPendingConfirmation) { + server.sendPluginMessage(btlp.getChannelIdentifier(), message); + } + } + } + + for (ServerBridgeDataCache bridgeData : serverInformation.values()) { + ServerConnection server = bridgeData.getConnection(); + + if (server != null && bridgeData.messagesPendingConfirmation.size() > 0 && (now > bridgeData.lastMessageSent + 1000 || bridgeData.messagesPendingConfirmation.size() > 5)) { + for (byte[] message : bridgeData.messagesPendingConfirmation) { + server.sendPluginMessage(btlp.getChannelIdentifier(), message); + } + } + } + } + + private void checkForThirdPartyVariables(String serverName, ServerBridgeDataCache dataCache) { + mainLoop.execute(() -> { + dataCache.addDataChangeListener(BTLPDataKeys.REGISTERED_THIRD_PARTY_VARIABLES, () -> updateBridgePlaceholders(serverName, dataCache)); + updateBridgePlaceholders(serverName, dataCache); + dataCache.addDataChangeListener(BTLPDataKeys.REGISTERED_THIRD_PARTY_SERVER_VARIABLES, () -> updateBridgeServerPlaceholders(serverName, dataCache)); + updateBridgeServerPlaceholders(serverName, dataCache); + dataCache.addDataChangeListener(BTLPDataKeys.PAPI_REGISTERED_PLACEHOLDER_PLUGINS, () -> updatePlaceholderAPIPlaceholders(serverName, dataCache)); + updatePlaceholderAPIPlaceholders(serverName, dataCache); + }); + } + + private void updateBridgePlaceholders(String serverName, ServerBridgeDataCache dataCache) { + List variables = dataCache.get(BTLPDataKeys.REGISTERED_THIRD_PARTY_VARIABLES); + if (variables != null) { + for (String variable : variables) { + playerPlaceholderResolver.addBridgeCustomPlaceholderDataKey(variable, BTLPDataKeys.createThirdPartyVariableDataKey(variable)); + } + cache.updateCustomPlaceholdersBridge(serverName, variables); + btlp.scheduleSoftReload(); + } + } + + private void updateBridgeServerPlaceholders(String serverName, ServerBridgeDataCache dataCache) { + List variables = dataCache.get(BTLPDataKeys.REGISTERED_THIRD_PARTY_SERVER_VARIABLES); + if (variables != null) { + for (String variable : variables) { + serverPlaceholderResolver.addBridgeCustomPlaceholderServerDataKey(variable, BTLPDataKeys.createThirdPartyServerVariableDataKey(variable)); + } + cache.updateCustomServerPlaceholdersBridge(serverName, variables); + btlp.scheduleSoftReload(); + } + } + + private void updatePlaceholderAPIPlaceholders(String serverName, ServerBridgeDataCache dataCache) { + List plugins = dataCache.get(BTLPDataKeys.PAPI_REGISTERED_PLACEHOLDER_PLUGINS); + if (plugins != null) { + playerPlaceholderResolver.addPlaceholderAPIPluginPrefixes(plugins); + cache.updatePAPIPrefixes(serverName, plugins); + btlp.scheduleSoftReload(); + } + } + + private void removeObsoleteServerConnections() { + for (ServerBridgeDataCache cache : serverInformation.values()) { + cache.removeObsoleteConnections(); + } + } + + public PlayerBridgeDataCache createDataCacheForPlayer(@Nonnull VelocityPlayer player) { + return new PlayerBridgeDataCache(); + } + + public DataHolder getServerDataHolder(@Nonnull String server) { + return getServerDataCache(server); + } + + private ServerBridgeDataCache getServerDataCache(@Nonnull String serverName) { + if (!serverInformation.containsKey(serverName)) { + serverInformation.computeIfAbsent(serverName, key -> { + ServerBridgeDataCache dataCache = new ServerBridgeDataCache(); + checkForThirdPartyVariables(serverName, dataCache); + return dataCache; + }); + } + return serverInformation.get(serverName); + } + + private static class PlayerConnectionInfo { + boolean isConnectionValid = false; + boolean hasReceived = false; + int connectionIdentifier = 0; + int protocolVersion = 0; + int nextIntroducePacketDelay = 1; + int introducePacketDelay = 1; + @Nullable + PlayerBridgeDataCache playerBridgeData = null; + @Nullable + ServerBridgeDataCache serverBridgeData = null; + } + + private abstract class BridgeData extends TrackingDataCache { + + final Queue messagesPendingConfirmation = new ConcurrentLinkedQueue<>(); + int lastConfirmed = 0; + int nextOutgoingMessageId = 1; + int nextIncomingMessageId = 1; + long lastMessageSent = 0; + int connectionId; + + boolean requestAll = false; + + Set> requestedDataKeys = new HashSet<>(); + + @Nullable + abstract ServerConnection getConnection(); + + @Override + protected void addActiveKey(DataKey key) { + super.addActiveKey(key); + + try { + synchronized (this) { + ServerConnection connection = getConnection(); + if (connection != null) { + if (!requestedDataKeys.contains(key)) { + requestedDataKeys.add(key); + ByteArrayDataOutput data = ByteStreams.newDataOutput(); + data.writeByte(this instanceof PlayerBridgeDataCache ? BridgeProtocolConstants.MESSAGE_ID_REQUEST_DATA : BridgeProtocolConstants.MESSAGE_ID_REQUEST_DATA_SERVER); + data.writeInt(connectionId); + data.writeInt(nextOutgoingMessageId++); + data.writeInt(1); + DataStreamUtils.writeDataKey(data, key); + data.writeInt(idMap.getNetId(key)); + byte[] message = data.toByteArray(); + messagesPendingConfirmation.add(message); + lastMessageSent = System.currentTimeMillis(); + connection.sendPluginMessage(btlp.getChannelIdentifier(), message); + } + } else { + requestAll = true; + } + } + } catch (Throwable th) { + rlExecutor.execute(() -> { + logger.log(Level.SEVERE, "Unexpected exception", th); + }); + requestAll = true; + } + } + + void setConnectionId(int connectionId) { + if (this.connectionId != connectionId) { + reset(); + } + this.connectionId = connectionId; + } + + void reset() { + synchronized (this) { + requestedDataKeys.clear(); + requestAll = true; + messagesPendingConfirmation.clear(); + lastConfirmed = 0; + nextOutgoingMessageId = 1; + nextIncomingMessageId = 1; + lastMessageSent = 0; + Collection> queriedKeys = new ArrayList<>(getActiveKeys()); + mainLoop.execute(() -> { + for (DataKey key : queriedKeys) { + updateValue(key, null); + } + }); + } + } + + void requestMissingData() throws IOException { + synchronized (this) { + if (requestAll) { + ServerConnection connection = getConnection(); + if (connection != null) { + List> keys = new ArrayList<>(getActiveKeys()); + + ByteArrayDataOutput data = ByteStreams.newDataOutput(); + data.writeByte(this instanceof PlayerBridgeDataCache ? BridgeProtocolConstants.MESSAGE_ID_REQUEST_DATA : BridgeProtocolConstants.MESSAGE_ID_REQUEST_DATA_SERVER); + data.writeInt(connectionId); + data.writeInt(nextOutgoingMessageId++); + data.writeInt(keys.size()); + for (DataKey key : keys) { + requestedDataKeys.add(key); + DataStreamUtils.writeDataKey(data, key); + data.writeInt(idMap.getNetId(key)); + } + byte[] message = data.toByteArray(); + messagesPendingConfirmation.add(message); + lastMessageSent = System.currentTimeMillis(); + connection.sendPluginMessage(btlp.getChannelIdentifier(), message); + } + requestAll = false; + } + } + } + } + + public class PlayerBridgeDataCache extends BridgeData { + @Nullable + private volatile ServerConnection connection = null; + + @Override + @Nullable + ServerConnection getConnection() { + return connection; + } + } + + private class ServerBridgeDataCache extends BridgeData { + private final ReferenceSet connections = new ReferenceOpenHashSet<>(); + + private void addConnection(@Nonnull ServerConnection server) { + synchronized (this) { + connections.add(server); + } + } + + @Override + @Nullable + ServerConnection getConnection() { + synchronized (this) { + ObjectIterator iterator = connections.iterator(); + while (iterator.hasNext()) { + try { + ServerConnection server = iterator.next(); + if (server.getServer().ping().join() != null) { + return server; + } + } catch (Exception ignored){} + iterator.remove(); + } + return null; + } + } + + private void removeObsoleteConnections() { + synchronized (this) { + ObjectIterator iterator = connections.iterator(); + while (iterator.hasNext()) { + ServerConnection server = iterator.next(); + try { + if (server.getServer().ping().join() == null) { + iterator.remove(); + } + } catch (Exception ignored) { + iterator.remove(); + } + } + } + } + + @Override + void reset() { + synchronized (this) { + super.reset(); + connections.clear(); + } + } + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractLegacyTabOverlayHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractLegacyTabOverlayHandler.java index 5bc16a07..44774faa 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractLegacyTabOverlayHandler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractLegacyTabOverlayHandler.java @@ -1,849 +1,849 @@ -/* - * Copyright (C) 2020 Florian Stober - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package codecrafter47.bungeetablistplus.handler; - -import codecrafter47.bungeetablistplus.protocol.PacketHandler; -import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; -import codecrafter47.bungeetablistplus.protocol.Team; -import codecrafter47.bungeetablistplus.util.ColorParser; -import codecrafter47.bungeetablistplus.util.ConcurrentBitSet; -import codecrafter47.bungeetablistplus.util.Property119Handler; -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableSet; -import com.velocitypowered.proxy.protocol.MinecraftPacket; -import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; -import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; -import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; -import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; -import de.codecrafter47.taboverlay.Icon; -import de.codecrafter47.taboverlay.config.misc.ChatFormat; -import de.codecrafter47.taboverlay.config.misc.Unchecked; -import de.codecrafter47.taboverlay.handler.*; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap; -import it.unimi.dsi.fastutil.objects.Object2IntMap; -import it.unimi.dsi.fastutil.objects.Object2ObjectMap; -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; -import lombok.AllArgsConstructor; -import lombok.val; -import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; -import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.*; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.Executor; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.ThreadLocalRandom; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.IntConsumer; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Implementation of {@link de.codecrafter47.taboverlay.handler.TabOverlayHandler} for pre 1.8 Minecraft Versions. - */ -public abstract class AbstractLegacyTabOverlayHandler implements PacketHandler, TabOverlayHandler { - - - private static final String[] slotID; - private static final UUID[] slotUUID; - private static final Set slotIDSet = new HashSet<>(); - - private static final Int2ObjectMap> playerListSizeToSupportedSizesMap = new Int2ObjectOpenHashMap<>(); - - - static { - - // add a random character to the player and team names to prevent issues in multi-velocity setup (stupid!). - int random = ThreadLocalRandom.current().nextInt(0x1e00, 0x2c00); - - slotID = new String[256]; - slotUUID = new UUID[256]; - - for (int i = 0; i < 256; i++) { - String hex = String.format("%02x", i); - slotID[i] = String.format("§B§T§L§P§%c§%c§%c§r", random, hex.charAt(0), hex.charAt(1)); - slotIDSet.add(slotID[i]); - slotUUID[i] = UUID.randomUUID(); - } - } - - private static final String[][] EMPTY_PROPERTIES = new String[0][]; - private static final String EMPTY_JSON_TEXT = "{\"text\":\"\"}"; - - private static Collection getSupportedSizesByPlayerListSize(int playerListSize) { - Preconditions.checkArgument(playerListSize >= 0, "playerListSize is negative"); - synchronized (playerListSizeToSupportedSizesMap) { - Collection collection = playerListSizeToSupportedSizesMap.get(playerListSize); - if (collection != null) { - return collection; - } - val builder = ImmutableSet.builder(); - if (playerListSize == 0) { - builder.add(new RectangularTabOverlay.Dimension(1, 0)); - } else { - int columns = (playerListSize + 19) / 20; - for (int rows = 0; rows <= 20; rows++) { - builder.add(new RectangularTabOverlay.Dimension(columns, rows)); - } - } - collection = builder.build(); - playerListSizeToSupportedSizesMap.put(playerListSize, collection); - return collection; - } - } - - private final Logger logger; - private final int playerListSize; - private final Executor eventLoopExecutor; - - private final Object2IntMap serverPlayerList = new Object2IntLinkedOpenHashMap<>(); - private final Object2ObjectMap modernServerPlayerList = new Object2ObjectOpenHashMap<>(); - - private boolean is13OrLater; - - private final Queue> nextActiveHandlerQueue = new ConcurrentLinkedQueue<>(); - private AbstractContentOperationModeHandler activeHandler; - - private final AtomicBoolean updateScheduledFlag = new AtomicBoolean(false); - private final Runnable updateTask = this::update; - - AbstractLegacyTabOverlayHandler(Logger logger, int playerListSize, Executor eventLoopExecutor, boolean is13OrLater) { - this.logger = logger; - this.eventLoopExecutor = eventLoopExecutor; - this.is13OrLater = is13OrLater; - Preconditions.checkElementIndex(playerListSize, 256, "playerListSize"); - this.playerListSize = playerListSize; - this.activeHandler = new PassThroughHandlerContent(); - } - - protected abstract void sendPacket(MinecraftPacket packet); - - @Override - public PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { - if (packet.getAction() == LegacyPlayerListItem.ADD_PLAYER) { - for (LegacyPlayerListItem.Item item : packet.getItems()) { - if (item.getUuid() != null) { - modernServerPlayerList.put(item.getUuid(), new ModernPlayerListEntry(item.getName(), item.getLatency(), item.getGameMode())); - } else { - serverPlayerList.put(getName(item), item.getLatency()); - } - } - } else { - for (LegacyPlayerListItem.Item item : packet.getItems()) { - if (item.getUuid() != null) { - modernServerPlayerList.remove(item.getUuid()); - } else { - serverPlayerList.removeInt(getName(item)); - } - } - } - return activeHandler.onPlayerListPacket(packet); - } - - @Override - public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { - if (packet.getActions().contains(UpsertPlayerInfo.Action.ADD_PLAYER)) { - for (UpsertPlayerInfo.Entry entry : packet.getEntries()) { - if (entry.getProfileId() != null) { - modernServerPlayerList.put(entry.getProfileId(), new ModernPlayerListEntry(entry.getProfile().getName(), entry.getLatency(), entry.getGameMode())); - } else { - serverPlayerList.put(getName(entry), entry.getLatency()); - } - } - } - return activeHandler.onPlayerListUpdatePacket(packet); - } - - @Override - public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet) { - for (UUID uuid : packet.getProfilesToRemove()) { - modernServerPlayerList.remove(uuid); - } - - return activeHandler.onPlayerListRemovePacket(packet); - } - - @Override - public PacketListenerResult onTeamPacket(Team packet) { - if (slotIDSet.contains(packet.getName())) { - logger.log(Level.WARNING, "Team name collision, using multi-velocity setup? Packet: {0}", packet); - return PacketListenerResult.CANCEL; - } - return PacketListenerResult.PASS; - } - - @Override - public PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { - logger.log(Level.WARNING, "1.7 players should not receive tab list header/ footer"); - return PacketListenerResult.CANCEL; - } - - @Override - public void onServerSwitch(boolean is13OrLater) { - this.is13OrLater = is13OrLater; - this.activeHandler.onServerSwitch(); - serverPlayerList.clear(); - modernServerPlayerList.clear(); - } - - @Override - public R enterContentOperationMode(ContentOperationMode operationMode) { - AbstractContentOperationModeHandler handler; - if (operationMode == ContentOperationMode.PASS_TROUGH) { - handler = new PassThroughHandlerContent(); - } else if (operationMode == ContentOperationMode.SIMPLE) { - handler = new SimpleContentOperationModeHandler(); - } else if (operationMode == ContentOperationMode.RECTANGULAR) { - handler = new RectangularSizeHandlerContent(); - } else { - throw new AssertionError("Missing operation mode handler for " + operationMode.getName()); - } - nextActiveHandlerQueue.add(handler); - scheduleUpdate(); - return Unchecked.cast(handler.getTabOverlay()); - } - - @Override - public R enterHeaderAndFooterOperationMode(HeaderAndFooterOperationMode operationMode) { - return Unchecked.cast(DummyHeaderFooterHandle.INSTANCE); - } - - private void scheduleUpdate() { - if (this.updateScheduledFlag.compareAndSet(false, true)) { - try { - eventLoopExecutor.execute(updateTask); - } catch (RejectedExecutionException ignored) { - } - } - } - - private void update() { - this.updateScheduledFlag.set(false); - AbstractContentOperationModeHandler handler; - while (null != (handler = nextActiveHandlerQueue.poll())) { - this.activeHandler.invalidate(); - this.activeHandler = handler; - this.activeHandler.onActivated(); - } - this.activeHandler.update(); - } - - private void removeEntry(UUID uuid, String player) { - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(); - item.setName(player); - item.setDisplayName(GsonComponentSerializer.gson().deserialize(player)); - item.setLatency(9999); - LegacyPlayerListItem pli = new LegacyPlayerListItem(LegacyPlayerListItem.REMOVE_PLAYER, List.of(item)); - sendPacket(pli); - } - - private abstract static class AbstractContentOperationModeHandler extends OperationModeHandler { - - abstract PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet); - - abstract void onServerSwitch(); - - abstract void update(); - - final void invalidate() { - getTabOverlay().invalidate(); - onDeactivated(); - } - - abstract void onDeactivated(); - - abstract void onActivated(); - - public abstract PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet); - - public abstract PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet); - } - - private abstract static class AbstractTabOverlay implements TabOverlayHandle { - private boolean valid = true; - - @Override - public boolean isValid() { - return valid; - } - - final void invalidate() { - valid = false; - } - } - - private class PassThroughHandlerContent extends AbstractContentOperationModeHandler { - - @Override - PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { - return PacketListenerResult.PASS; - } - - @Override - public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { - return PacketListenerResult.PASS; - } - - @Override - public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet) { - return PacketListenerResult.PASS; - } - - @Override - void onActivated() { - for (val entry : serverPlayerList.object2IntEntrySet()) { - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(); - item.setDisplayName(GsonComponentSerializer.gson().deserialize(entry.getKey())); // TODO: Check Formatting - item.setLatency(entry.getIntValue()); - LegacyPlayerListItem pli = new LegacyPlayerListItem(LegacyPlayerListItem.ADD_PLAYER, List.of(item)); - sendPacket(pli); - } - for (val entry : modernServerPlayerList.entrySet()) { - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(entry.getKey()); - item.setName(entry.getValue().name); - item.setGameMode(entry.getValue().gamemode); - item.setLatency(entry.getValue().latency); - Property119Handler.setProperties(item, EMPTY_PROPERTIES); - LegacyPlayerListItem pli = new LegacyPlayerListItem(LegacyPlayerListItem.ADD_PLAYER, List.of(item)); - sendPacket(pli); - } - } - - @Override - void onDeactivated() { - removeAllEntries(); - } - - private void removeAllEntries() { - for (String player : serverPlayerList.keySet()) { - removeEntry(null, player); - } - for (UUID player : modernServerPlayerList.keySet()) { - removeEntry(player, null); - } - } - - @Override - public void onServerSwitch() { - removeAllEntries(); - } - - @Override - void update() { - // nothing to do - } - - @Override - public PassThroughTabOverlay createTabOverlay() { - return new PassThroughTabOverlay(); - } - } - - private static final class PassThroughTabOverlay extends AbstractTabOverlay { - - } - - private abstract class CustomTabOverlayHandlerContent extends AbstractContentOperationModeHandler { - private final IntConsumer updateTextTask; - private final IntConsumer updatePingTask; - private int size = 0; - - private CustomTabOverlayHandlerContent() { - this.updateTextTask = index -> updateText(getTabOverlay(), index); - this.updatePingTask = index -> updatePing(getTabOverlay(), index); - } - - @Override - PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { - return PacketListenerResult.CANCEL; - } - - @Override - public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { - return PacketListenerResult.CANCEL; - } - - @Override - public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet) { - return PacketListenerResult.CANCEL; - } - - @Override - void onServerSwitch() { - // nothing to do - } - - @Override - void update() { - CustomTabOverlayHandlerContent.this.updateSize(); - getTabOverlay().dirtyFlagsText.iterateAndClear(updateTextTask); - getTabOverlay().dirtyFlagsPing.iterateAndClear(updatePingTask); - } - - @Override - void onActivated() { - // nothing to do - } - - @Override - void onDeactivated() { - for (int index = this.size - 1; index >= 0; index--) { - removeEntry(slotUUID[index], slotID[index]); - Team t = new Team(); - t.setName(slotID[index]); - t.setMode((byte) 1); - sendPacket(t); - } - } - - private void updateSize() { - CustomTabOverlay tabOverlay = getTabOverlay(); - int size = tabOverlay.size; - if (size != this.size) { - if (size > this.size) { - for (int index = this.size; index < size; index++) { - tabOverlay.dirtyFlagsText.clear(index); - tabOverlay.dirtyFlagsPing.clear(index); - // create new slot - updateSlot(tabOverlay, index); - Team t = new Team(); - t.setName(slotID[index]); - t.setMode((byte) 0); - t.setPrefix(tabOverlay.text0[index]); - t.setDisplayName(""); - t.setSuffix(tabOverlay.text1[index]); - t.setPlayers(new String[]{slotID[index]}); - t.setNameTagVisibility("always"); - t.setCollisionRule("always"); - if (is13OrLater) { - t.setDisplayName(EMPTY_JSON_TEXT); - t.setPrefix("{\"text\":\"" + tabOverlay.text0[index] + "\"}"); - t.setSuffix("{\"text\":\"" + tabOverlay.text1[index] + "\"}"); - } - sendPacket(t); - } - } else { - for (int index = this.size - 1; index >= size; index--) { - removeEntry(slotUUID[index], slotID[index]); - Team t = new Team(); - t.setName(slotID[index]); - t.setMode((byte) 1); - sendPacket(t); - } - } - this.size = size; - } - } - - private void updateSlot(CustomTabOverlay tabOverlay, int index) { - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUUID[index]); - item.setName(slotID[index]); - Property119Handler.setProperties(item, EMPTY_PROPERTIES); - item.setDisplayName(GsonComponentSerializer.gson().deserialize(slotID[index])); // TODO: Check Formatting - item.setLatency(tabOverlay.ping[index]); - LegacyPlayerListItem pli = new LegacyPlayerListItem(LegacyPlayerListItem.ADD_PLAYER, List.of(item)); - sendPacket(pli); - } - - private void updateText(CustomTabOverlay tabOverlay, int index) { - if (index < size) { - Team packet = new Team(); - packet.setName(slotID[index]); - packet.setMode((byte) 2); - packet.setPrefix(tabOverlay.text0[index]); - packet.setDisplayName(""); - packet.setSuffix(tabOverlay.text1[index]); - packet.setNameTagVisibility("always"); - packet.setCollisionRule("always"); - if (is13OrLater) { - packet.setDisplayName(EMPTY_JSON_TEXT); - packet.setPrefix("{\"text\":\"" + tabOverlay.text0[index] + "\"}"); - packet.setSuffix("{\"text\":\"" + tabOverlay.text1[index] + "\"}"); - } - sendPacket(packet); - } - } - - private void updatePing(CustomTabOverlay tabOverlay, int index) { - if (index < size) { - updateSlot(tabOverlay, index); - } - } - } - - private abstract class CustomTabOverlay extends AbstractTabOverlay implements TabOverlayHandle.BatchModifiable { - - final String[] text0; - final String[] text1; - final int[] ping; - final AtomicInteger batchUpdateRecursionLevel; - final ConcurrentBitSet dirtyFlagsText; - final ConcurrentBitSet dirtyFlagsPing; - - protected int size; - - private CustomTabOverlay() { - this.batchUpdateRecursionLevel = new AtomicInteger(0); - this.text0 = new String[playerListSize]; - this.text1 = new String[playerListSize]; - this.ping = new int[playerListSize]; - this.dirtyFlagsText = new ConcurrentBitSet(playerListSize); - this.dirtyFlagsPing = new ConcurrentBitSet(playerListSize); - this.size = 0; - } - - @Override - public void beginBatchModification() { - if (isValid()) { - if (batchUpdateRecursionLevel.incrementAndGet() < 0) { - throw new AssertionError("Recursion level overflow"); - } - } - } - - @Override - public void completeBatchModification() { - if (isValid()) { - int level = batchUpdateRecursionLevel.decrementAndGet(); - if (level == 0) { - scheduleUpdate(); - } else if (level < 0) { - throw new AssertionError("Recursion level underflow"); - } - } - } - - private void scheduleUpdateIfNotInBatch() { - if (batchUpdateRecursionLevel.get() == 0) { - scheduleUpdate(); - } - } - - void setTextInternal(int index, String text) { - // convert to legacy format - text = ChatFormat.formattedTextToLegacy(text); - - // split string into two parts of at most 16 characters each to be displayed as prefix and suffix - String text0, text1; - if (text.length() <= 16) { - text0 = text; - text1 = ""; - } else { - int end = text.charAt(15) == LegacyComponentSerializer.SECTION_CHAR ? 15 : 16; - text0 = text.substring(0, end); - int start = ColorParser.endofColor(text, end); - String colors = ColorParser.extractColorCodes(text.substring(0, start)); - end = start + 16 - colors.length(); - if (end >= text.length()) { - end = text.length(); - } - text1 = colors + text.substring(start, end); - } - - if (!text0.equals(this.text0[index]) || !text1.equals(this.text1[index])) { - this.text0[index] = text0; - this.text1[index] = text1; - dirtyFlagsText.set(index); - scheduleUpdateIfNotInBatch(); - } - } - - void setPingInternal(int index, int ping) { - if (ping != this.ping[index]) { - this.ping[index] = ping; - dirtyFlagsPing.set(index); - scheduleUpdateIfNotInBatch(); - } - } - - void setSizeInternal(int newSize) { - int oldSize = this.size; - if (newSize > oldSize) { - for (int index = oldSize; index < newSize; index++) { - text0[index] = ""; - text1[index] = ""; - ping[index] = 0; - } - } - this.size = newSize; - if (newSize < oldSize) { - for (int index = oldSize - 1; index >= newSize; index--) { - text0[index] = ""; - text1[index] = ""; - ping[index] = 0; - } - } - scheduleUpdateIfNotInBatch(); - } - } - - private class RectangularSizeHandlerContent extends CustomTabOverlayHandlerContent { - - @Override - public RectangularTabOverlayImpl createTabOverlay() { - return new RectangularTabOverlayImpl(); - } - } - - private final class RectangularTabOverlayImpl extends CustomTabOverlay implements RectangularTabOverlay { - - private final Collection supportedSizes; - - private Dimension sizeAsDimension; - - private RectangularTabOverlayImpl() { - super(); - this.supportedSizes = getSupportedSizesByPlayerListSize(playerListSize); - Optional dimensionZero = supportedSizes.stream().filter(size -> size.getSize() == 0).findAny(); - if (!dimensionZero.isPresent()) { - throw new AssertionError(); - } - this.sizeAsDimension = dimensionZero.get(); - } - - @Override - public Dimension getSize() { - return sizeAsDimension; - } - - @Override - public Collection getSupportedSizes() { - return supportedSizes; - } - - @Override - public void setSize(@Nonnull Dimension size) { - if (!getSupportedSizes().contains(size)) { - throw new IllegalArgumentException("Unsupported size " + size); - } - if (isValid() && !this.sizeAsDimension.equals(size)) { - this.sizeAsDimension = size; - setSizeInternal(size.getSize()); - } - } - - @Override - public void setSlot(int column, int row, @Nullable UUID uuid, @Nonnull Icon icon, @Nonnull String text, int ping) { - if (isValid()) { - Preconditions.checkElementIndex(column, sizeAsDimension.getColumns(), "column"); - Preconditions.checkElementIndex(row, sizeAsDimension.getRows(), "row"); - int index = row * sizeAsDimension.getColumns() + column; - - beginBatchModification(); - try { - setTextInternal(index, text); - setPingInternal(index, ping); - } finally { - completeBatchModification(); - } - } - } - - @Override - public void setUuid(int column, int row, UUID uuid) { - // nothing to do - not supported in 1.7 - } - - @Override - public void setIcon(int column, int row, @Nonnull Icon icon) { - // nothing to do - not supported in 1.7 - } - - @Override - public void setText(int column, int row, @Nonnull String text) { - if (isValid()) { - Preconditions.checkElementIndex(column, sizeAsDimension.getColumns(), "column"); - Preconditions.checkElementIndex(row, sizeAsDimension.getRows(), "row"); - int index = row * sizeAsDimension.getColumns() + column; - setTextInternal(index, text); - } - } - - @Override - public void setPing(int column, int row, int ping) { - if (isValid()) { - Preconditions.checkElementIndex(column, sizeAsDimension.getColumns(), "column"); - Preconditions.checkElementIndex(row, sizeAsDimension.getRows(), "row"); - int index = row * sizeAsDimension.getColumns() + column; - setPingInternal(index, ping); - } - } - - } - - private class SimpleContentOperationModeHandler extends CustomTabOverlayHandlerContent { - - @Override - public SimpleTabOverlayImpl createTabOverlay() { - return new SimpleTabOverlayImpl(); - } - } - - private final class SimpleTabOverlayImpl extends CustomTabOverlay implements SimpleTabOverlay { - - @Override - public int getSize() { - return size; - } - - @Override - public int getMaxSize() { - return playerListSize; - } - - @Override - public void setSize(int size) { - if (size < 0 || size > playerListSize) { - throw new IllegalArgumentException("Unsupported size " + size); - } - if (isValid() && this.size != size) { - setSizeInternal(size); - } - } - - @Override - public void setSlot(int index, @Nullable UUID uuid, @Nonnull Icon icon, @Nonnull String text, int ping) { - if (isValid()) { - Preconditions.checkElementIndex(index, size); - - beginBatchModification(); - try { - setTextInternal(index, text); - setPingInternal(index, ping); - } finally { - completeBatchModification(); - } - } - } - - @Override - public void setSlot(int index, @Nonnull Icon icon, @Nonnull String text, int ping) { - if (isValid()) { - Preconditions.checkElementIndex(index, size); - - beginBatchModification(); - try { - setTextInternal(index, text); - setPingInternal(index, ping); - } finally { - completeBatchModification(); - } - } - } - - @Override - public void setUuid(int index, UUID uuid) { - // nothing to do - not supported in 1.7 - } - - @Override - public void setIcon(int index, @Nonnull Icon icon) { - // nothing to do - not supported in 1.7 - } - - @Override - public void setText(int index, @Nonnull String text) { - if (isValid()) { - Preconditions.checkElementIndex(index, size); - setTextInternal(index, text); - } - } - - @Override - public void setPing(int index, int ping) { - if (isValid()) { - Preconditions.checkElementIndex(index, size); - setPingInternal(index, ping); - } - } - - } - - private static class DummyHeaderFooterHandle implements HeaderAndFooterHandle { - private static final DummyHeaderFooterHandle INSTANCE = new DummyHeaderFooterHandle(); - - @Override - public void setHeaderFooter(@Nullable String header, @Nullable String footer) { - // dummy - } - - @Override - public void setHeader(@Nullable String header) { - // dummy - } - - @Override - public void setFooter(@Nullable String footer) { - // dummy - } - - @Override - public void beginBatchModification() { - // dummy - } - - @Override - public void completeBatchModification() { - // dummy - } - - @Override - public boolean isValid() { - // dummy - return true; - } - } - - /** - * Utility method to get the name from an {@link LegacyPlayerListItem.Item}. - * - * @param item the item - * @return the name - */ - private static String getName(LegacyPlayerListItem.Item item) { - if (item.getDisplayName() != null) { - return GsonComponentSerializer.gson().serialize(item.getDisplayName()); - } else if (item.getName() != null) { - return item.getName(); - } else { - throw new AssertionError("DisplayName and Username are null"); - } - } - - private static String getName(UpsertPlayerInfo.Entry entry) { - if (entry.getDisplayName() != null) { - return GsonComponentSerializer.gson().serialize(entry.getDisplayName()); - } else if (entry.getProfile().getName() != null) { - return entry.getProfile().getName(); - } else { - throw new AssertionError("DisplayName and Username are null"); - } - } - - @AllArgsConstructor - private static class ModernPlayerListEntry { - String name; - int latency; - int gamemode; - } -} +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.handler; + +import codecrafter47.bungeetablistplus.protocol.PacketHandler; +import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; +import codecrafter47.bungeetablistplus.protocol.Team; +import codecrafter47.bungeetablistplus.util.ColorParser; +import codecrafter47.bungeetablistplus.util.ConcurrentBitSet; +import codecrafter47.bungeetablistplus.util.Property119Handler; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableSet; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; +import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; +import de.codecrafter47.taboverlay.Icon; +import de.codecrafter47.taboverlay.config.misc.ChatFormat; +import de.codecrafter47.taboverlay.config.misc.Unchecked; +import de.codecrafter47.taboverlay.handler.*; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import lombok.AllArgsConstructor; +import lombok.val; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.*; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.IntConsumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Implementation of {@link de.codecrafter47.taboverlay.handler.TabOverlayHandler} for pre 1.8 Minecraft Versions. + */ +public abstract class AbstractLegacyTabOverlayHandler implements PacketHandler, TabOverlayHandler { + + + private static final String[] slotID; + private static final UUID[] slotUUID; + private static final Set slotIDSet = new HashSet<>(); + + private static final Int2ObjectMap> playerListSizeToSupportedSizesMap = new Int2ObjectOpenHashMap<>(); + + + static { + + // add a random character to the player and team names to prevent issues in multi-velocity setup (stupid!). + int random = ThreadLocalRandom.current().nextInt(0x1e00, 0x2c00); + + slotID = new String[256]; + slotUUID = new UUID[256]; + + for (int i = 0; i < 256; i++) { + String hex = String.format("%02x", i); + slotID[i] = String.format("§B§T§L§P§%c§%c§%c§r", random, hex.charAt(0), hex.charAt(1)); + slotIDSet.add(slotID[i]); + slotUUID[i] = UUID.randomUUID(); + } + } + + private static final String[][] EMPTY_PROPERTIES = new String[0][]; + private static final String EMPTY_JSON_TEXT = "{\"text\":\"\"}"; + + private static Collection getSupportedSizesByPlayerListSize(int playerListSize) { + Preconditions.checkArgument(playerListSize >= 0, "playerListSize is negative"); + synchronized (playerListSizeToSupportedSizesMap) { + Collection collection = playerListSizeToSupportedSizesMap.get(playerListSize); + if (collection != null) { + return collection; + } + val builder = ImmutableSet.builder(); + if (playerListSize == 0) { + builder.add(new RectangularTabOverlay.Dimension(1, 0)); + } else { + int columns = (playerListSize + 19) / 20; + for (int rows = 0; rows <= 20; rows++) { + builder.add(new RectangularTabOverlay.Dimension(columns, rows)); + } + } + collection = builder.build(); + playerListSizeToSupportedSizesMap.put(playerListSize, collection); + return collection; + } + } + + protected final Logger logger; + private final int playerListSize; + private final Executor eventLoopExecutor; + + private final Object2IntMap serverPlayerList = new Object2IntLinkedOpenHashMap<>(); + private final Object2ObjectMap modernServerPlayerList = new Object2ObjectOpenHashMap<>(); + + private boolean is13OrLater; + + private final Queue> nextActiveHandlerQueue = new ConcurrentLinkedQueue<>(); + private AbstractContentOperationModeHandler activeHandler; + + private final AtomicBoolean updateScheduledFlag = new AtomicBoolean(false); + private final Runnable updateTask = this::update; + + AbstractLegacyTabOverlayHandler(Logger logger, int playerListSize, Executor eventLoopExecutor, boolean is13OrLater) { + this.logger = logger; + this.eventLoopExecutor = eventLoopExecutor; + this.is13OrLater = is13OrLater; + Preconditions.checkElementIndex(playerListSize, 256, "playerListSize"); + this.playerListSize = playerListSize; + this.activeHandler = new PassThroughHandlerContent(); + } + + protected abstract void sendPacket(MinecraftPacket packet); + + @Override + public PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { + if (packet.getAction() == LegacyPlayerListItem.ADD_PLAYER) { + for (LegacyPlayerListItem.Item item : packet.getItems()) { + if (item.getUuid() != null) { + modernServerPlayerList.put(item.getUuid(), new ModernPlayerListEntry(item.getName(), item.getLatency(), item.getGameMode())); + } else { + serverPlayerList.put(getName(item), item.getLatency()); + } + } + } else { + for (LegacyPlayerListItem.Item item : packet.getItems()) { + if (item.getUuid() != null) { + modernServerPlayerList.remove(item.getUuid()); + } else { + serverPlayerList.removeInt(getName(item)); + } + } + } + return activeHandler.onPlayerListPacket(packet); + } + + @Override + public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { + if (packet.getActions().contains(UpsertPlayerInfo.Action.ADD_PLAYER)) { + for (UpsertPlayerInfo.Entry entry : packet.getEntries()) { + if (entry.getProfileId() != null) { + modernServerPlayerList.put(entry.getProfileId(), new ModernPlayerListEntry(entry.getProfile().getName(), entry.getLatency(), entry.getGameMode())); + } else { + serverPlayerList.put(getName(entry), entry.getLatency()); + } + } + } + return activeHandler.onPlayerListUpdatePacket(packet); + } + + @Override + public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet) { + for (UUID uuid : packet.getProfilesToRemove()) { + modernServerPlayerList.remove(uuid); + } + + return activeHandler.onPlayerListRemovePacket(packet); + } + + @Override + public PacketListenerResult onTeamPacket(Team packet) { + if (slotIDSet.contains(packet.getName())) { + logger.log(Level.WARNING, "Team name collision, using multi-velocity setup? Packet: {0}", packet); + return PacketListenerResult.CANCEL; + } + return PacketListenerResult.PASS; + } + + @Override + public PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { + logger.log(Level.WARNING, "1.7 players should not receive tab list header/ footer"); + return PacketListenerResult.CANCEL; + } + + @Override + public void onServerSwitch(boolean is13OrLater) { + this.is13OrLater = is13OrLater; + this.activeHandler.onServerSwitch(); + serverPlayerList.clear(); + modernServerPlayerList.clear(); + } + + @Override + public R enterContentOperationMode(ContentOperationMode operationMode) { + AbstractContentOperationModeHandler handler; + if (operationMode == ContentOperationMode.PASS_TROUGH) { + handler = new PassThroughHandlerContent(); + } else if (operationMode == ContentOperationMode.SIMPLE) { + handler = new SimpleContentOperationModeHandler(); + } else if (operationMode == ContentOperationMode.RECTANGULAR) { + handler = new RectangularSizeHandlerContent(); + } else { + throw new AssertionError("Missing operation mode handler for " + operationMode.getName()); + } + nextActiveHandlerQueue.add(handler); + scheduleUpdate(); + return Unchecked.cast(handler.getTabOverlay()); + } + + @Override + public R enterHeaderAndFooterOperationMode(HeaderAndFooterOperationMode operationMode) { + return Unchecked.cast(DummyHeaderFooterHandle.INSTANCE); + } + + private void scheduleUpdate() { + if (this.updateScheduledFlag.compareAndSet(false, true)) { + try { + eventLoopExecutor.execute(updateTask); + } catch (RejectedExecutionException ignored) { + } + } + } + + private void update() { + this.updateScheduledFlag.set(false); + AbstractContentOperationModeHandler handler; + while (null != (handler = nextActiveHandlerQueue.poll())) { + this.activeHandler.invalidate(); + this.activeHandler = handler; + this.activeHandler.onActivated(); + } + this.activeHandler.update(); + } + + private void removeEntry(UUID uuid, String player) { + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(); + item.setName(player); + item.setDisplayName(GsonComponentSerializer.gson().deserialize(player)); + item.setLatency(9999); + LegacyPlayerListItem pli = new LegacyPlayerListItem(LegacyPlayerListItem.REMOVE_PLAYER, List.of(item)); + sendPacket(pli); + } + + private abstract static class AbstractContentOperationModeHandler extends OperationModeHandler { + + abstract PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet); + + abstract void onServerSwitch(); + + abstract void update(); + + final void invalidate() { + getTabOverlay().invalidate(); + onDeactivated(); + } + + abstract void onDeactivated(); + + abstract void onActivated(); + + public abstract PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet); + + public abstract PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet); + } + + private abstract static class AbstractTabOverlay implements TabOverlayHandle { + private boolean valid = true; + + @Override + public boolean isValid() { + return valid; + } + + final void invalidate() { + valid = false; + } + } + + private class PassThroughHandlerContent extends AbstractContentOperationModeHandler { + + @Override + PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { + return PacketListenerResult.PASS; + } + + @Override + public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { + return PacketListenerResult.PASS; + } + + @Override + public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet) { + return PacketListenerResult.PASS; + } + + @Override + void onActivated() { + for (val entry : serverPlayerList.object2IntEntrySet()) { + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(); + item.setDisplayName(GsonComponentSerializer.gson().deserialize(entry.getKey())); // TODO: Check Formatting + item.setLatency(entry.getIntValue()); + LegacyPlayerListItem pli = new LegacyPlayerListItem(LegacyPlayerListItem.ADD_PLAYER, List.of(item)); + sendPacket(pli); + } + for (val entry : modernServerPlayerList.entrySet()) { + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(entry.getKey()); + item.setName(entry.getValue().name); + item.setGameMode(entry.getValue().gamemode); + item.setLatency(entry.getValue().latency); + Property119Handler.setProperties(item, EMPTY_PROPERTIES); + LegacyPlayerListItem pli = new LegacyPlayerListItem(LegacyPlayerListItem.ADD_PLAYER, List.of(item)); + sendPacket(pli); + } + } + + @Override + void onDeactivated() { + removeAllEntries(); + } + + private void removeAllEntries() { + for (String player : serverPlayerList.keySet()) { + removeEntry(null, player); + } + for (UUID player : modernServerPlayerList.keySet()) { + removeEntry(player, null); + } + } + + @Override + public void onServerSwitch() { + removeAllEntries(); + } + + @Override + void update() { + // nothing to do + } + + @Override + public PassThroughTabOverlay createTabOverlay() { + return new PassThroughTabOverlay(); + } + } + + private static final class PassThroughTabOverlay extends AbstractTabOverlay { + + } + + private abstract class CustomTabOverlayHandlerContent extends AbstractContentOperationModeHandler { + private final IntConsumer updateTextTask; + private final IntConsumer updatePingTask; + private int size = 0; + + private CustomTabOverlayHandlerContent() { + this.updateTextTask = index -> updateText(getTabOverlay(), index); + this.updatePingTask = index -> updatePing(getTabOverlay(), index); + } + + @Override + PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { + return PacketListenerResult.CANCEL; + } + + @Override + public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { + return PacketListenerResult.CANCEL; + } + + @Override + public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet) { + return PacketListenerResult.CANCEL; + } + + @Override + void onServerSwitch() { + // nothing to do + } + + @Override + void update() { + CustomTabOverlayHandlerContent.this.updateSize(); + getTabOverlay().dirtyFlagsText.iterateAndClear(updateTextTask); + getTabOverlay().dirtyFlagsPing.iterateAndClear(updatePingTask); + } + + @Override + void onActivated() { + // nothing to do + } + + @Override + void onDeactivated() { + for (int index = this.size - 1; index >= 0; index--) { + removeEntry(slotUUID[index], slotID[index]); + Team t = new Team(); + t.setName(slotID[index]); + t.setMode((byte) 1); + sendPacket(t); + } + } + + private void updateSize() { + CustomTabOverlay tabOverlay = getTabOverlay(); + int size = tabOverlay.size; + if (size != this.size) { + if (size > this.size) { + for (int index = this.size; index < size; index++) { + tabOverlay.dirtyFlagsText.clear(index); + tabOverlay.dirtyFlagsPing.clear(index); + // create new slot + updateSlot(tabOverlay, index); + Team t = new Team(); + t.setName(slotID[index]); + t.setMode((byte) 0); + t.setPrefix(tabOverlay.text0[index]); + t.setDisplayName(""); + t.setSuffix(tabOverlay.text1[index]); + t.setPlayers(new String[]{slotID[index]}); + t.setNameTagVisibility("always"); + t.setCollisionRule("always"); + if (is13OrLater) { + t.setDisplayName(EMPTY_JSON_TEXT); + t.setPrefix("{\"text\":\"" + tabOverlay.text0[index] + "\"}"); + t.setSuffix("{\"text\":\"" + tabOverlay.text1[index] + "\"}"); + } + sendPacket(t); + } + } else { + for (int index = this.size - 1; index >= size; index--) { + removeEntry(slotUUID[index], slotID[index]); + Team t = new Team(); + t.setName(slotID[index]); + t.setMode((byte) 1); + sendPacket(t); + } + } + this.size = size; + } + } + + private void updateSlot(CustomTabOverlay tabOverlay, int index) { + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUUID[index]); + item.setName(slotID[index]); + Property119Handler.setProperties(item, EMPTY_PROPERTIES); + item.setDisplayName(GsonComponentSerializer.gson().deserialize(slotID[index])); // TODO: Check Formatting + item.setLatency(tabOverlay.ping[index]); + LegacyPlayerListItem pli = new LegacyPlayerListItem(LegacyPlayerListItem.ADD_PLAYER, List.of(item)); + sendPacket(pli); + } + + private void updateText(CustomTabOverlay tabOverlay, int index) { + if (index < size) { + Team packet = new Team(); + packet.setName(slotID[index]); + packet.setMode((byte) 2); + packet.setPrefix(tabOverlay.text0[index]); + packet.setDisplayName(""); + packet.setSuffix(tabOverlay.text1[index]); + packet.setNameTagVisibility("always"); + packet.setCollisionRule("always"); + if (is13OrLater) { + packet.setDisplayName(EMPTY_JSON_TEXT); + packet.setPrefix("{\"text\":\"" + tabOverlay.text0[index] + "\"}"); + packet.setSuffix("{\"text\":\"" + tabOverlay.text1[index] + "\"}"); + } + sendPacket(packet); + } + } + + private void updatePing(CustomTabOverlay tabOverlay, int index) { + if (index < size) { + updateSlot(tabOverlay, index); + } + } + } + + private abstract class CustomTabOverlay extends AbstractTabOverlay implements TabOverlayHandle.BatchModifiable { + + final String[] text0; + final String[] text1; + final int[] ping; + final AtomicInteger batchUpdateRecursionLevel; + final ConcurrentBitSet dirtyFlagsText; + final ConcurrentBitSet dirtyFlagsPing; + + protected int size; + + private CustomTabOverlay() { + this.batchUpdateRecursionLevel = new AtomicInteger(0); + this.text0 = new String[playerListSize]; + this.text1 = new String[playerListSize]; + this.ping = new int[playerListSize]; + this.dirtyFlagsText = new ConcurrentBitSet(playerListSize); + this.dirtyFlagsPing = new ConcurrentBitSet(playerListSize); + this.size = 0; + } + + @Override + public void beginBatchModification() { + if (isValid()) { + if (batchUpdateRecursionLevel.incrementAndGet() < 0) { + throw new AssertionError("Recursion level overflow"); + } + } + } + + @Override + public void completeBatchModification() { + if (isValid()) { + int level = batchUpdateRecursionLevel.decrementAndGet(); + if (level == 0) { + scheduleUpdate(); + } else if (level < 0) { + throw new AssertionError("Recursion level underflow"); + } + } + } + + private void scheduleUpdateIfNotInBatch() { + if (batchUpdateRecursionLevel.get() == 0) { + scheduleUpdate(); + } + } + + void setTextInternal(int index, String text) { + // convert to legacy format + text = ChatFormat.formattedTextToLegacy(text); + + // split string into two parts of at most 16 characters each to be displayed as prefix and suffix + String text0, text1; + if (text.length() <= 16) { + text0 = text; + text1 = ""; + } else { + int end = text.charAt(15) == LegacyComponentSerializer.SECTION_CHAR ? 15 : 16; + text0 = text.substring(0, end); + int start = ColorParser.endofColor(text, end); + String colors = ColorParser.extractColorCodes(text.substring(0, start)); + end = start + 16 - colors.length(); + if (end >= text.length()) { + end = text.length(); + } + text1 = colors + text.substring(start, end); + } + + if (!text0.equals(this.text0[index]) || !text1.equals(this.text1[index])) { + this.text0[index] = text0; + this.text1[index] = text1; + dirtyFlagsText.set(index); + scheduleUpdateIfNotInBatch(); + } + } + + void setPingInternal(int index, int ping) { + if (ping != this.ping[index]) { + this.ping[index] = ping; + dirtyFlagsPing.set(index); + scheduleUpdateIfNotInBatch(); + } + } + + void setSizeInternal(int newSize) { + int oldSize = this.size; + if (newSize > oldSize) { + for (int index = oldSize; index < newSize; index++) { + text0[index] = ""; + text1[index] = ""; + ping[index] = 0; + } + } + this.size = newSize; + if (newSize < oldSize) { + for (int index = oldSize - 1; index >= newSize; index--) { + text0[index] = ""; + text1[index] = ""; + ping[index] = 0; + } + } + scheduleUpdateIfNotInBatch(); + } + } + + private class RectangularSizeHandlerContent extends CustomTabOverlayHandlerContent { + + @Override + public RectangularTabOverlayImpl createTabOverlay() { + return new RectangularTabOverlayImpl(); + } + } + + private final class RectangularTabOverlayImpl extends CustomTabOverlay implements RectangularTabOverlay { + + private final Collection supportedSizes; + + private Dimension sizeAsDimension; + + private RectangularTabOverlayImpl() { + super(); + this.supportedSizes = getSupportedSizesByPlayerListSize(playerListSize); + Optional dimensionZero = supportedSizes.stream().filter(size -> size.getSize() == 0).findAny(); + if (!dimensionZero.isPresent()) { + throw new AssertionError(); + } + this.sizeAsDimension = dimensionZero.get(); + } + + @Override + public Dimension getSize() { + return sizeAsDimension; + } + + @Override + public Collection getSupportedSizes() { + return supportedSizes; + } + + @Override + public void setSize(@Nonnull Dimension size) { + if (!getSupportedSizes().contains(size)) { + throw new IllegalArgumentException("Unsupported size " + size); + } + if (isValid() && !this.sizeAsDimension.equals(size)) { + this.sizeAsDimension = size; + setSizeInternal(size.getSize()); + } + } + + @Override + public void setSlot(int column, int row, @Nullable UUID uuid, @Nonnull Icon icon, @Nonnull String text, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(column, sizeAsDimension.getColumns(), "column"); + Preconditions.checkElementIndex(row, sizeAsDimension.getRows(), "row"); + int index = row * sizeAsDimension.getColumns() + column; + + beginBatchModification(); + try { + setTextInternal(index, text); + setPingInternal(index, ping); + } finally { + completeBatchModification(); + } + } + } + + @Override + public void setUuid(int column, int row, UUID uuid) { + // nothing to do - not supported in 1.7 + } + + @Override + public void setIcon(int column, int row, @Nonnull Icon icon) { + // nothing to do - not supported in 1.7 + } + + @Override + public void setText(int column, int row, @Nonnull String text) { + if (isValid()) { + Preconditions.checkElementIndex(column, sizeAsDimension.getColumns(), "column"); + Preconditions.checkElementIndex(row, sizeAsDimension.getRows(), "row"); + int index = row * sizeAsDimension.getColumns() + column; + setTextInternal(index, text); + } + } + + @Override + public void setPing(int column, int row, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(column, sizeAsDimension.getColumns(), "column"); + Preconditions.checkElementIndex(row, sizeAsDimension.getRows(), "row"); + int index = row * sizeAsDimension.getColumns() + column; + setPingInternal(index, ping); + } + } + + } + + private class SimpleContentOperationModeHandler extends CustomTabOverlayHandlerContent { + + @Override + public SimpleTabOverlayImpl createTabOverlay() { + return new SimpleTabOverlayImpl(); + } + } + + private final class SimpleTabOverlayImpl extends CustomTabOverlay implements SimpleTabOverlay { + + @Override + public int getSize() { + return size; + } + + @Override + public int getMaxSize() { + return playerListSize; + } + + @Override + public void setSize(int size) { + if (size < 0 || size > playerListSize) { + throw new IllegalArgumentException("Unsupported size " + size); + } + if (isValid() && this.size != size) { + setSizeInternal(size); + } + } + + @Override + public void setSlot(int index, @Nullable UUID uuid, @Nonnull Icon icon, @Nonnull String text, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(index, size); + + beginBatchModification(); + try { + setTextInternal(index, text); + setPingInternal(index, ping); + } finally { + completeBatchModification(); + } + } + } + + @Override + public void setSlot(int index, @Nonnull Icon icon, @Nonnull String text, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(index, size); + + beginBatchModification(); + try { + setTextInternal(index, text); + setPingInternal(index, ping); + } finally { + completeBatchModification(); + } + } + } + + @Override + public void setUuid(int index, UUID uuid) { + // nothing to do - not supported in 1.7 + } + + @Override + public void setIcon(int index, @Nonnull Icon icon) { + // nothing to do - not supported in 1.7 + } + + @Override + public void setText(int index, @Nonnull String text) { + if (isValid()) { + Preconditions.checkElementIndex(index, size); + setTextInternal(index, text); + } + } + + @Override + public void setPing(int index, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(index, size); + setPingInternal(index, ping); + } + } + + } + + private static class DummyHeaderFooterHandle implements HeaderAndFooterHandle { + private static final DummyHeaderFooterHandle INSTANCE = new DummyHeaderFooterHandle(); + + @Override + public void setHeaderFooter(@Nullable String header, @Nullable String footer) { + // dummy + } + + @Override + public void setHeader(@Nullable String header) { + // dummy + } + + @Override + public void setFooter(@Nullable String footer) { + // dummy + } + + @Override + public void beginBatchModification() { + // dummy + } + + @Override + public void completeBatchModification() { + // dummy + } + + @Override + public boolean isValid() { + // dummy + return true; + } + } + + /** + * Utility method to get the name from an {@link LegacyPlayerListItem.Item}. + * + * @param item the item + * @return the name + */ + private static String getName(LegacyPlayerListItem.Item item) { + if (item.getDisplayName() != null) { + return GsonComponentSerializer.gson().serialize(item.getDisplayName()); + } else if (item.getName() != null) { + return item.getName(); + } else { + throw new AssertionError("DisplayName and Username are null"); + } + } + + private static String getName(UpsertPlayerInfo.Entry entry) { + if (entry.getDisplayName() != null) { + return GsonComponentSerializer.gson().serialize(entry.getDisplayName()); + } else if (entry.getProfile().getName() != null) { + return entry.getProfile().getName(); + } else { + throw new AssertionError("DisplayName and Username are null"); + } + } + + @AllArgsConstructor + private static class ModernPlayerListEntry { + String name; + int latency; + int gamemode; + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java index e65c1721..74a135b6 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java @@ -1,2562 +1,2562 @@ -/* - * Copyright (C) 2020 Florian Stober - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package codecrafter47.bungeetablistplus.handler; - -import codecrafter47.bungeetablistplus.protocol.PacketHandler; -import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; -import codecrafter47.bungeetablistplus.protocol.Team; -import codecrafter47.bungeetablistplus.util.BitSet; -import codecrafter47.bungeetablistplus.util.ConcurrentBitSet; -import codecrafter47.bungeetablistplus.util.Property119Handler; -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import com.velocitypowered.proxy.protocol.MinecraftPacket; -import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; -import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; -import de.codecrafter47.taboverlay.Icon; -import de.codecrafter47.taboverlay.ProfileProperty; -import de.codecrafter47.taboverlay.config.misc.ChatFormat; -import de.codecrafter47.taboverlay.config.misc.Unchecked; -import de.codecrafter47.taboverlay.handler.*; -import it.unimi.dsi.fastutil.objects.*; -import lombok.*; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.*; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.Executor; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.ThreadLocalRandom; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.logging.Level; -import java.util.logging.Logger; - -import static com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem.ADD_PLAYER; -import static com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem.REMOVE_PLAYER; -import static com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem.UPDATE_DISPLAY_NAME; -import static com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem.UPDATE_GAMEMODE; -import static com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem.UPDATE_LATENCY; - -public abstract class AbstractTabOverlayHandler implements PacketHandler, TabOverlayHandler { - - // some options - private static final boolean OPTION_ENABLE_CUSTOM_SLOT_USERNAME_COLLISION_CHECK = true; - private static final boolean OPTION_ENABLE_CUSTOM_SLOT_UUID_COLLISION_CHECK = true; - private static final boolean OPTION_ENABLE_CONSISTENCY_CHECKS = true; - - private static final String EMPTY_JSON_TEXT = "{\"text\":\"\"}"; - protected static final String[][] EMPTY_PROPERTIES_ARRAY = new String[0][]; - - private static final boolean TEAM_COLLISION_RULE_SUPPORTED; - private static final boolean USE_PROTOCOL_PROPERTY_TYPE; - - private static final ImmutableMap DIMENSION_TO_USED_SLOTS; - private static final BitSet[] SIZE_TO_USED_SLOTS; - - private static final UUID[] CUSTOM_SLOT_UUID_STEVE; - private static final UUID[] CUSTOM_SLOT_UUID_ALEX; - private static final UUID[] CUSTOM_SLOT_UUID_SPACER; - private static final Set CUSTOM_SLOT_UUIDS; - private static final String[] CUSTOM_SLOT_USERNAME; - private static final String[] CUSTOM_SLOT_USERNAME_SMILEYS; - private static final Set CUSTOM_SLOT_USERNAMES; - private static final String[] CUSTOM_SLOT_TEAMNAME; - private static final Set CUSTOM_SLOT_TEAMNAMES; - - private static final Set blockedTeams = new HashSet<>(); - - static { - TEAM_COLLISION_RULE_SUPPORTED = true; - USE_PROTOCOL_PROPERTY_TYPE = true; - - // build the dimension to used slots map (for the rectangular tab overlay) - val builder = ImmutableMap.builder(); - for (int columns = 1; columns <= 4; columns++) { - for (int rows = 0; rows <= 20; rows++) { - if (columns != 1 && rows != 0 && columns * rows <= (columns - 1) * 20) - continue; - BitSet usedSlots = new BitSet(80); - for (int column = 0; column < columns; column++) { - for (int row = 0; row < rows; row++) { - usedSlots.set(index(column, row)); - } - } - builder.put(new RectangularTabOverlay.Dimension(columns, rows), usedSlots); - } - } - DIMENSION_TO_USED_SLOTS = builder.build(); - - // build the size to used slots map (for the simple tab overlay) - SIZE_TO_USED_SLOTS = new BitSet[81]; - for (int size = 0; size <= 80; size++) { - BitSet usedSlots = new BitSet(80); - usedSlots.set(0, size); - SIZE_TO_USED_SLOTS[size] = usedSlots; - } - - // generate random uuids for our custom slots - CUSTOM_SLOT_UUID_ALEX = new UUID[80]; - CUSTOM_SLOT_UUID_STEVE = new UUID[80]; - CUSTOM_SLOT_UUID_SPACER = new UUID[17]; - UUID base = UUID.randomUUID(); - long msb = base.getMostSignificantBits(); - long lsb = base.getLeastSignificantBits(); - lsb ^= base.hashCode(); - for (int i = 0; i < 80; i++) { - CUSTOM_SLOT_UUID_STEVE[i] = new UUID(msb, lsb ^ (2 * i)); - CUSTOM_SLOT_UUID_ALEX[i] = new UUID(msb, lsb ^ (2 * i + 1)); - } - for (int i = 0; i < 17; i++) { - CUSTOM_SLOT_UUID_SPACER[i] = new UUID(msb, lsb ^ (160 + i)); - } - if (OPTION_ENABLE_CUSTOM_SLOT_UUID_COLLISION_CHECK) { - CUSTOM_SLOT_UUIDS = ImmutableSet.builder() - .add(CUSTOM_SLOT_UUID_ALEX) - .add(CUSTOM_SLOT_UUID_STEVE) - .add(CUSTOM_SLOT_UUID_SPACER).build(); - } else { - CUSTOM_SLOT_UUIDS = null; - } - - // generate usernames for custom slots - int unique = ThreadLocalRandom.current().nextInt(); - CUSTOM_SLOT_USERNAME = new String[81]; - for (int i = 0; i < 81; i++) { - CUSTOM_SLOT_USERNAME[i] = String.format("~BTLP%08x %02d", unique, i); - } - if (OPTION_ENABLE_CUSTOM_SLOT_USERNAME_COLLISION_CHECK) { - CUSTOM_SLOT_USERNAMES = ImmutableSet.copyOf(CUSTOM_SLOT_USERNAME); - } else { - CUSTOM_SLOT_USERNAMES = null; - } - CUSTOM_SLOT_USERNAME_SMILEYS = new String[80]; - String emojis = "\u263a\u2639\u2620\u2763\u2764\u270c\u261d\u270d\u2618\u2615\u2668\u2693\u2708\u231b\u231a\u2600\u2b50\u2601\u2602\u2614\u26a1\u2744\u2603\u2604\u2660\u2665\u2666\u2663\u265f\u260e\u2328\u2709\u270f\u2712\u2702\u2692\u2694\u2699\u2696\u2697\u26b0\u26b1\u267f\u26a0\u2622\u2623\u2640\u2642\u267e\u267b\u269c\u303d\u2733\u2734\u2747\u203c\u2b1c\u2b1b\u25fc\u25fb\u25aa\u25ab\u2049\u26ab\u26aa\u3030\u00a9\u00ae\u2122\u2139\u24c2\u3297\u2716\u2714\u2611\u2695\u2b06\u2197\u27a1\u2198\u2b07\u2199\u3299\u2b05\u2196\u2195\u2194\u21a9\u21aa\u2934\u2935\u269b\u2721\u2638\u262f\u271d\u2626\u262a\u262e\u2648\u2649\u264a\u264b\u264c\u264d\u264e\u264f\u2650\u2651\u2652\u2653\u25b6\u25c0\u23cf"; - for (int i = 0; i < 80; i++) { - CUSTOM_SLOT_USERNAME_SMILEYS[i] = String.format("" + emojis.charAt(i), unique, i); - } - - // generate teams for custom slots - CUSTOM_SLOT_TEAMNAME = new String[81]; - for (int i = 0; i < 81; i++) { - CUSTOM_SLOT_TEAMNAME[i] = String.format(" BTLP%08x %02d", unique, i); - } - CUSTOM_SLOT_TEAMNAMES = ImmutableSet.copyOf(CUSTOM_SLOT_TEAMNAME); - } - - private final Logger logger; - private final Executor eventLoopExecutor; - private final UUID viewerUuid; - - private final Object2ObjectMap serverPlayerList = new Object2ObjectOpenHashMap<>(); - protected final Set serverTabListPlayers = new ObjectOpenHashSet<>(); - @Nullable - protected String serverHeader = null; - @Nullable - protected String serverFooter = null; - protected final Object2ObjectMap serverTeams = new Object2ObjectOpenHashMap<>(); - protected final Object2ObjectMap playerToTeamMap = new Object2ObjectOpenHashMap<>(); - - private final Queue> nextActiveContentHandlerQueue = new ConcurrentLinkedQueue<>(); - private final Queue> nextActiveHeaderFooterHandlerQueue = new ConcurrentLinkedQueue<>(); - private AbstractContentOperationModeHandler activeContentHandler; - private AbstractHeaderFooterOperationModeHandler activeHeaderFooterHandler; - - private boolean hasCreatedCustomTeams = false; - private boolean areCustomSlotUsersPartOfTeams = false; - - private final AtomicBoolean updateScheduledFlag = new AtomicBoolean(false); - private final Runnable updateTask = this::update; - - private final boolean is18; - private boolean is13OrLater; - private boolean is119OrLater; - protected boolean active; - - public AbstractTabOverlayHandler(Logger logger, Executor eventLoopExecutor, UUID viewerUuid, boolean is18, boolean is13OrLater, boolean is119OrLater) { - this.logger = logger; - this.eventLoopExecutor = eventLoopExecutor; - this.viewerUuid = viewerUuid; - this.is18 = is18; - this.is13OrLater = is13OrLater; - this.is119OrLater = is119OrLater; - this.activeContentHandler = new PassThroughContentHandler(); - this.activeHeaderFooterHandler = new PassThroughHeaderFooterHandler(); - } - - protected abstract void sendPacket(MinecraftPacket packet); - - @Override - public PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { - switch (packet.getAction()) { - case ADD_PLAYER: - for (LegacyPlayerListItem.Item item : packet.getItems()) { - if (OPTION_ENABLE_CUSTOM_SLOT_UUID_COLLISION_CHECK) { - if (CUSTOM_SLOT_UUIDS.contains(item.getUuid())) { - throw new AssertionError("UUID collision " + item.getUuid()); - } - } - if (OPTION_ENABLE_CUSTOM_SLOT_USERNAME_COLLISION_CHECK) { - if (CUSTOM_SLOT_USERNAMES.contains(item.getName())) { - throw new AssertionError("Username collision" + item.getName()); - } - } - PlayerListEntry old = serverPlayerList.put(item.getUuid(), new PlayerListEntry(item)); - if (old != null) { - serverTabListPlayers.remove(old.getUsername()); - } - serverTabListPlayers.add(item.getName()); - } - break; - case UPDATE_GAMEMODE: - for (LegacyPlayerListItem.Item item : packet.getItems()) { - PlayerListEntry playerListEntry = serverPlayerList.get(item.getUuid()); - if (playerListEntry != null) { - playerListEntry.setGamemode(item.getGameMode()); - } - } - break; - case UPDATE_LATENCY: - for (LegacyPlayerListItem.Item item : packet.getItems()) { - PlayerListEntry playerListEntry = serverPlayerList.get(item.getUuid()); - if (playerListEntry != null) { - playerListEntry.setPing(item.getLatency()); - } - } - break; - case UPDATE_DISPLAY_NAME: - for (LegacyPlayerListItem.Item item : packet.getItems()) { - PlayerListEntry playerListEntry = serverPlayerList.get(item.getUuid()); - if (playerListEntry != null) { - playerListEntry.setDisplayName(GsonComponentSerializer.gson().serialize(item.getDisplayName())); - } - } - break; - case REMOVE_PLAYER: - for (LegacyPlayerListItem.Item item : packet.getItems()) { - PlayerListEntry removed = serverPlayerList.remove(item.getUuid()); - if (removed != null) { - serverTabListPlayers.remove(removed.getUsername()); - } - } - break; - } - - try { - return this.activeContentHandler.onPlayerListPacket(packet); - } catch (Throwable th) { - logger.log(Level.SEVERE, "Unexpected error", th); - // try recover - enterContentOperationMode(ContentOperationMode.PASS_TROUGH); - return PacketListenerResult.PASS; - } - } - - @Override - public PacketListenerResult onTeamPacket(Team packet) { - - if (packet.getPlayers() != null) { - boolean block = false; - for (String player : packet.getPlayers()) { - if (player.equals("")) { - block = true; - } - } - if (block) { - if (!blockedTeams.contains(packet.getName())) { - logger.warning("Blocking Team Packet for Team " + packet.getName() + ", as it is incompatible with BungeeTabListPlus."); - blockedTeams.add(packet.getName()); - } - return PacketListenerResult.CANCEL; - } - } - - try { - this.activeContentHandler.onTeamPacketPreprocess(packet); - } catch (Throwable th) { - logger.log(Level.SEVERE, "Unexpected error", th); - // try recover - enterContentOperationMode(ContentOperationMode.PASS_TROUGH); - } - - if (packet.getMode() == 1) { - TeamEntry team = serverTeams.remove(packet.getName()); - if (team != null) { - for (String player : team.getPlayers()) { - playerToTeamMap.remove(player, packet.getName()); - } - } - } else { - // Create or get old team - TeamEntry teamEntry; - if (packet.getMode() == 0) { - teamEntry = new TeamEntry(); - serverTeams.put(packet.getName(), teamEntry); - } else { - teamEntry = serverTeams.get(packet.getName()); - } - - if (teamEntry != null) { - if (packet.getMode() == 0 || packet.getMode() == 2) { - teamEntry.setDisplayName(packet.getDisplayName()); - teamEntry.setPrefix(packet.getPrefix()); - teamEntry.setSuffix(packet.getSuffix()); - teamEntry.setFriendlyFire(packet.getFriendlyFire()); - teamEntry.setNameTagVisibility(packet.getNameTagVisibility()); - if (TEAM_COLLISION_RULE_SUPPORTED) { - teamEntry.setCollisionRule(packet.getCollisionRule()); - } - teamEntry.setColor(packet.getColor()); - } - if (packet.getPlayers() != null) { - for (String s : packet.getPlayers()) { - if (packet.getMode() == 0 || packet.getMode() == 3) { - if (playerToTeamMap.containsKey(s)) { - TeamEntry previousTeam = serverTeams.get(playerToTeamMap.get(s)); - // previousTeam shouldn't be null (that's inconsistent with playerToTeamMap, but apparently it happens) - if (previousTeam != null) { - previousTeam.removePlayer(s); - } - } - teamEntry.addPlayer(s); - playerToTeamMap.put(s, packet.getName()); - } else { - teamEntry.removePlayer(s); - playerToTeamMap.remove(s, packet.getName()); - } - } - } - } - } - - try { - return this.activeContentHandler.onTeamPacket(packet); - } catch (Throwable th) { - logger.log(Level.SEVERE, "Unexpected error", th); - // try recover - enterContentOperationMode(ContentOperationMode.PASS_TROUGH); - return PacketListenerResult.PASS; - } - } - - @Override - public PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { - PacketListenerResult result = PacketListenerResult.PASS; - try { - result = this.activeHeaderFooterHandler.onPlayerListHeaderFooterPacket(packet); - if (result == PacketListenerResult.MODIFIED) { - throw new AssertionError("PacketListenerResult.MODIFIED must not be used"); - } - } catch (Throwable th) { - logger.log(Level.SEVERE, "Unexpected error", th); - // try recover - enterHeaderAndFooterOperationMode(HeaderAndFooterOperationMode.PASS_TROUGH); - } - - this.serverHeader = packet.getHeader() != null ? packet.getHeader() : EMPTY_JSON_TEXT; - this.serverFooter = packet.getFooter() != null ? packet.getFooter() : EMPTY_JSON_TEXT; - - return result; - } - - @Override - public void onServerSwitch(boolean is13OrLater) { - this.is13OrLater = is13OrLater; - if (!active) { - active = true; - update(); - } else { - - serverTeams.clear(); - playerToTeamMap.clear(); - - if (isUsingAltRespawn()) { - hasCreatedCustomTeams = false; - areCustomSlotUsersPartOfTeams = false; - } - - try { - this.activeContentHandler.onServerSwitch(); - } catch (Throwable th) { - logger.log(Level.SEVERE, "Unexpected error", th); - // try recover - enterContentOperationMode(ContentOperationMode.PASS_TROUGH); - } - try { - this.activeHeaderFooterHandler.onServerSwitch(); - } catch (Throwable th) { - logger.log(Level.SEVERE, "Unexpected error", th); - // try recover - enterContentOperationMode(ContentOperationMode.PASS_TROUGH); - } - - if (!serverPlayerList.isEmpty()) { - List items = new ArrayList<>(); - for(UUID uuid : serverPlayerList.keySet()){ - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(uuid); - items.add(item); - } - LegacyPlayerListItem packet = new LegacyPlayerListItem(REMOVE_PLAYER, items); - sendPacket(packet); - } - - serverPlayerList.clear(); - if (serverHeader != null) { - serverHeader = EMPTY_JSON_TEXT; - } - if (serverFooter != null) { - serverFooter = EMPTY_JSON_TEXT; - } - - serverTabListPlayers.clear(); - } - } - - protected boolean isUsingAltRespawn() { - return false; - } - - @Override - public R enterContentOperationMode(ContentOperationMode operationMode) { - AbstractContentOperationModeHandler handler; - if (operationMode == ContentOperationMode.PASS_TROUGH) { - handler = new PassThroughContentHandler(); - } else if (operationMode == ContentOperationMode.SIMPLE) { - handler = new SimpleOperationModeHandler(); - } else if (operationMode == ContentOperationMode.RECTANGULAR) { - handler = new RectangularSizeHandler(); - } else { - throw new UnsupportedOperationException(); - } - nextActiveContentHandlerQueue.add(handler); - scheduleUpdate(); - return Unchecked.cast(handler.getTabOverlay()); - } - - @Override - public R enterHeaderAndFooterOperationMode(HeaderAndFooterOperationMode operationMode) { - AbstractHeaderFooterOperationModeHandler handler; - if (operationMode == HeaderAndFooterOperationMode.PASS_TROUGH) { - handler = new PassThroughHeaderFooterHandler(); - } else if (operationMode == HeaderAndFooterOperationMode.CUSTOM) { - handler = new CustomHeaderAndFooterOperationModeHandler(); - } else { - throw new UnsupportedOperationException(Objects.toString(operationMode)); - } - nextActiveHeaderFooterHandlerQueue.add(handler); - scheduleUpdate(); - return Unchecked.cast(handler.getTabOverlay()); - } - - private void scheduleUpdate() { - if (this.updateScheduledFlag.compareAndSet(false, true)) { - try { - eventLoopExecutor.execute(updateTask); - } catch (RejectedExecutionException ignored) { - } - } - } - - private void update() { - if (!active) { - return; - } - updateScheduledFlag.set(false); - - // update content handler - AbstractContentOperationModeHandler contentHandler; - while (null != (contentHandler = nextActiveContentHandlerQueue.poll())) { - this.activeContentHandler.invalidate(); - contentHandler.onActivated(this.activeContentHandler); - this.activeContentHandler = contentHandler; - } - this.activeContentHandler.update(); - - // update header and footer handler - AbstractHeaderFooterOperationModeHandler heaerFooterHandler; - while (null != (heaerFooterHandler = nextActiveHeaderFooterHandlerQueue.poll())) { - this.activeHeaderFooterHandler.invalidate(); - heaerFooterHandler.onActivated(this.activeHeaderFooterHandler); - this.activeHeaderFooterHandler = heaerFooterHandler; - } - this.activeHeaderFooterHandler.update(); - } - - private abstract class AbstractContentOperationModeHandler extends OperationModeHandler { - - /** - * Called when the player receives a {@link LegacyPlayerListItem} packet. - *

- * This method is called after this {@link AbstractTabOverlayHandler} has updated the {@code serverPlayerList}. - */ - abstract PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet); - - /** - * Called when the player receives a {@link Team} packet. - *

- * This method is called before this {@link AbstractTabOverlayHandler} executes its own logic to update the - * server team info. - */ - abstract void onTeamPacketPreprocess(Team packet); - - /** - * Called when the player receives a {@link Team} packet. - *

- * This method is called after this {@link AbstractTabOverlayHandler} executes its own logic to update the - * server team info. - */ - abstract PacketListenerResult onTeamPacket(Team packet); - - /** - * Called when the player switches the server. - *

- * This method is called before this {@link AbstractTabOverlayHandler} executes its own logic to clear the - * server player list info. - */ - abstract void onServerSwitch(); - - abstract void update(); - - final void invalidate() { - getTabOverlay().invalidate(); - onDeactivated(); - } - - /** - * Called when this {@link OperationModeHandler} is deactivated. - *

- * This method must put the client player list in the state expected by {@link #onActivated(AbstractContentOperationModeHandler)}. It must - * especially remove all custom entries and players must be part of the correct teams. - */ - abstract void onDeactivated(); - - /** - * Called when this {@link OperationModeHandler} becomes the active one. - *

- * State of the player list when this method is called: - * - there are no custom entries on the client - * - all entries from {@link #serverPlayerList} are known to the client, but the client may know the wrong displayname, gamemode and ping - * - player list header/ footer may be wrong - *

- * Additional information about the state of the player list may be obtained from the previous handler - * - * @param previous previous handler - */ - abstract void onActivated(AbstractContentOperationModeHandler previous); - } - - private abstract class AbstractHeaderFooterOperationModeHandler extends OperationModeHandler { - - /** - * Called when the player receives a {@link HeaderAndFooter} packet. - *

- * This method is called before this {@link AbstractTabOverlayHandler} executes its own logic to update the - * server player list info. - */ - abstract PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet); - - /** - * Called when the player switches the server. - *

- * This method is called before this {@link AbstractTabOverlayHandler} executes its own logic to clear the - * server player list info. - */ - abstract void onServerSwitch(); - - abstract void update(); - - final void invalidate() { - getTabOverlay().invalidate(); - onDeactivated(); - } - - /** - * Called when this {@link OperationModeHandler} is deactivated. - *

- * This method must put the client player list in the state expected by {@link #onActivated(AbstractHeaderFooterOperationModeHandler)}. It must - * especially remove all custom entries and players must be part of the correct teams. - */ - abstract void onDeactivated(); - - /** - * Called when this {@link OperationModeHandler} becomes the active one. - *

- * State of the player list when this method is called: - * - there are no custom entries on the client - * - all entries from {@link #serverPlayerList} are known to the client, but the client may know the wrong displayname, gamemode and ping - * - player list header/ footer may be wrong - *

- * Additional information about the state of the player list may be obtained from the previous handler - * - * @param previous previous handler - */ - abstract void onActivated(AbstractHeaderFooterOperationModeHandler previous); - } - - private abstract static class AbstractContentTabOverlay implements TabOverlayHandle { - private boolean valid = true; - - @Override - public boolean isValid() { - return valid; - } - - final void invalidate() { - valid = false; - } - } - - private abstract static class AbstractHeaderFooterTabOverlay implements TabOverlayHandle { - private boolean valid = true; - - @Override - public boolean isValid() { - return valid; - } - - final void invalidate() { - valid = false; - } - } - - private final class PassThroughContentHandler extends AbstractContentOperationModeHandler { - - @Override - protected PassThroughContentTabOverlay createTabOverlay() { - return new PassThroughContentTabOverlay(); - } - - @Override - PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { - return PacketListenerResult.PASS; - } - - @Override - void onTeamPacketPreprocess(Team packet) { - // nothing to do - } - - @Override - PacketListenerResult onTeamPacket(Team packet) { - return PacketListenerResult.PASS; - } - - @Override - void onServerSwitch() { - sendPacket(new HeaderAndFooter(EMPTY_JSON_TEXT, EMPTY_JSON_TEXT)); - } - - @Override - void update() { - // nothing to do - } - - @Override - void onDeactivated() { - // nothing to do - } - - @Override - void onActivated(AbstractContentOperationModeHandler previous) { - if (previous instanceof PassThroughContentHandler) { - // we're lucky, nothing to do - return; - } - - // fix player list entries - if (!serverPlayerList.isEmpty()) { - // restore player ping - LegacyPlayerListItem packet; - List items = new ArrayList<>(serverPlayerList.size()); - for (PlayerListEntry entry : serverPlayerList.values()) { - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(entry.getUuid()); - item.setLatency(entry.getPing()); - items.add(item); - } - packet = new LegacyPlayerListItem(UPDATE_LATENCY, items); - sendPacket(packet); - - // restore player gamemode - items.clear(); - for (PlayerListEntry entry : serverPlayerList.values()) { - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(entry.getUuid()); - item.setGameMode(entry.getGamemode()); - items.add(item); - } - packet = new LegacyPlayerListItem(UPDATE_GAMEMODE, items); - sendPacket(packet); - - // restore player display name - items.clear(); - for (PlayerListEntry entry : serverPlayerList.values()) { - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(entry.getUuid()); - item.setDisplayName((entry.getDisplayName() != null && !entry.getDisplayName().equalsIgnoreCase("null")) ? GsonComponentSerializer.gson().deserialize(entry.getDisplayName()) : Component.empty()); - items.add(item); - } - packet = new LegacyPlayerListItem(UPDATE_DISPLAY_NAME, items); - sendPacket(packet); - } - } - } - - private final class PassThroughContentTabOverlay extends AbstractContentTabOverlay { - - } - - private final class PassThroughHeaderFooterHandler extends AbstractHeaderFooterOperationModeHandler { - - @Override - protected PassThroughHeaderFooterTabOverlay createTabOverlay() { - return new PassThroughHeaderFooterTabOverlay(); - } - - @Override - PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { - return PacketListenerResult.PASS; - } - - @Override - void onServerSwitch() { - sendPacket(new HeaderAndFooter(EMPTY_JSON_TEXT, EMPTY_JSON_TEXT)); - } - - @Override - void update() { - // nothing to do - } - - @Override - void onDeactivated() { - // nothing to do - } - - @Override - void onActivated(AbstractHeaderFooterOperationModeHandler previous) { - if (previous instanceof PassThroughHeaderFooterHandler) { - // we're lucky, nothing to do - return; - } - - // fix header/ footer - sendPacket(new HeaderAndFooter(serverHeader != null ? serverHeader : EMPTY_JSON_TEXT, serverFooter != null ? serverFooter : EMPTY_JSON_TEXT)); - } - } - - private final class PassThroughHeaderFooterTabOverlay extends AbstractHeaderFooterTabOverlay { - - } - - private abstract class CustomContentTabOverlayHandler extends AbstractContentOperationModeHandler { - - boolean viewerIsSpectator = false; - final ObjectSet freePlayers; - - @Nonnull - BitSet usedSlots; - BitSet dirtySlots; - int highestUsedSlotIndex; - private boolean using80Slots; - private int usedSlotsCount; - final SlotState[] slotState; - /** - * Uuid of the player list entry used for the slot. - */ - final UUID[] slotUuid; - /** - * Username of the player list entry used for the slot. - */ - final String[] slotUsername; - /** - * Player uuid mapped to the slot it is used for - */ - final Object2IntMap playerUuidToSlotMap; - /** - * Player username mapped to the slot it is used for - */ - final Object2IntMap playerUsernameToSlotMap; - boolean canShrink = false; - - private final List itemQueueAddPlayer; - private final List itemQueueRemovePlayer; - private final List itemQueueUpdateDisplayName; - private final List itemQueueUpdatePing; - - private final boolean experimentalTabCompleteFixForTabSize80 = isExperimentalTabCompleteFixForTabSize80(); - private final boolean experimentalTabCompleteSmileys = isExperimentalTabCompleteSmileys(); - - private CustomContentTabOverlayHandler() { - this.dirtySlots = new BitSet(80); - this.usedSlots = SIZE_TO_USED_SLOTS[0]; - this.usedSlotsCount = 0; - this.using80Slots = false; - this.slotState = new SlotState[80]; - Arrays.fill(this.slotState, SlotState.UNUSED); - this.slotUuid = new UUID[80]; - this.slotUsername = new String[80]; - this.highestUsedSlotIndex = -1; - this.freePlayers = new ObjectOpenHashSet<>(); - this.playerUuidToSlotMap = new Object2IntOpenHashMap<>(); - this.playerUuidToSlotMap.defaultReturnValue(-1); - this.playerUsernameToSlotMap = new Object2IntOpenHashMap<>(); - this.playerUsernameToSlotMap.defaultReturnValue(-1); - this.itemQueueAddPlayer = new ArrayList<>(80); - this.itemQueueRemovePlayer = new ArrayList<>(80); - this.itemQueueUpdateDisplayName = new ArrayList<>(80); - this.itemQueueUpdatePing = new ArrayList<>(80); - } - - @Override - PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { - - int action = packet.getAction(); - - if (using80Slots && action == UPDATE_GAMEMODE) { - return PacketListenerResult.PASS; - } - - // check whether viewer gamemode changed - boolean viewerGamemodeChanged = false; - T tabOverlay = getTabOverlay(); - if (action == ADD_PLAYER || action == UPDATE_GAMEMODE || action == REMOVE_PLAYER) { - PlayerListEntry entry = serverPlayerList.get(viewerUuid); - boolean viewerIsSpectator = entry != null && entry.getGamemode() == 3; - if (this.viewerIsSpectator != viewerIsSpectator) { - this.viewerIsSpectator = viewerIsSpectator; - if (!using80Slots) { - if (highestUsedSlotIndex >= 0) { - dirtySlots.set(highestUsedSlotIndex); - } - - if (viewerIsSpectator) { - // mark player slot as dirty - int i = playerUuidToSlotMap.getInt(viewerUuid); - if (i >= 0) { - dirtySlots.set(i); - } - } else { - // mark slots with player uuid as dirty - if (action == UPDATE_GAMEMODE) { - // if action is ADD_PLAYER slots are marked dirty below, so only do it here if action is UPDATE_GAMEMODE - for (int slot = 0; slot < 80; slot++) { - UUID uuid = tabOverlay.uuid[slot]; - if (viewerUuid.equals(uuid)) { - dirtySlots.set(slot); - } - } - } - } - } - viewerGamemodeChanged = true; - } - } - - PacketListenerResult result; - boolean needUpdate = !using80Slots && viewerGamemodeChanged; - - switch (action) { - case ADD_PLAYER: - List items = packet.getItems(); - if (!using80Slots) { - for (LegacyPlayerListItem.Item item : items) { - if (!viewerUuid.equals(item.getUuid())) { - item.setGameMode(0); - } - } - - for (LegacyPlayerListItem.Item item : items) { - UUID uuid = item.getUuid(); - int index = playerUuidToSlotMap.getInt(uuid); - if (index == -1) { - freePlayers.add(uuid); - needUpdate = true; - } else if (!item.getName().equals(slotUsername[index])) { - dirtySlots.set(index); - needUpdate = true; - } else { - item.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: check formatting - item.setLatency(tabOverlay.ping[index]); - tabOverlay.dirtyFlagsText.clear(index); - tabOverlay.dirtyFlagsPing.clear(index); - } - } - - // mark slot to use for player as dirty - // a new player joining the server shouldn't happen too frequently, so we can accept - // the cost of searching all 80 slots - if (!freePlayers.isEmpty()) { - for (int slot = 0; slot < 80; slot++) { - UUID uuid = tabOverlay.uuid[slot]; - if (uuid != null && freePlayers.contains(uuid)) { - dirtySlots.set(slot); - } - } - } - - // request size update if tab list too small - if (usedSlotsCount < serverPlayerList.size()) { - tabOverlay.dirtyFlagSize = true; - } - } else { - for (LegacyPlayerListItem.Item item : packet.getItems()) { - if (!playerToTeamMap.containsKey(item.getName())) { - sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[80], new String[]{item.getName()})); - } - } - } - if (needUpdate) { - sendPacket(packet); - result = PacketListenerResult.CANCEL; - } else { - result = PacketListenerResult.MODIFIED; - } - break; - case UPDATE_GAMEMODE: - if (viewerGamemodeChanged) { - items = packet.getItems(); - for (LegacyPlayerListItem.Item item : items) { - if (!viewerUuid.equals(item.getUuid())) { - item.setGameMode(0); - } - } - sendPacket(packet); - } - result = PacketListenerResult.CANCEL; - break; - case UPDATE_LATENCY: - result = PacketListenerResult.CANCEL; - break; - case UPDATE_DISPLAY_NAME: - result = PacketListenerResult.CANCEL; - break; - case REMOVE_PLAYER: - if (!using80Slots) { - items = packet.getItems(); - for (LegacyPlayerListItem.Item item : items) { - int index = playerUuidToSlotMap.removeInt(item.getUuid()); - if (index == -1) { - if (OPTION_ENABLE_CONSISTENCY_CHECKS) { - if (serverPlayerList.containsKey(item.getUuid())) { - logger.severe("Inconsistent data: player in serverPlayerList but not in playerUuidToSlotMap"); - } - } - } else { - // Switch slot 'index' from player to custom mode - restoring player teams - - // 1. remove player from team - if (item.getUuid().version() != 2) { // hack for Citizens compatibility - sendPacket(createPacketTeamRemovePlayers(CUSTOM_SLOT_TEAMNAME[index], new String[]{slotUsername[index]})); - playerUsernameToSlotMap.removeInt(slotUsername[index]); - String playerTeamName; - if ((playerTeamName = playerToTeamMap.get(slotUsername[index])) != null) { - // 2. add player to correct team - sendPacket(createPacketTeamAddPlayers(playerTeamName, new String[]{slotUsername[index]})); - // 3. reset custom slot team - if (is13OrLater) { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); - } else { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], "", "", "", "always", "always", 0, (byte) 1)); - } - } - } - - // 4. create new custom slot - tabOverlay.dirtyFlagsIcon.clear(index); - tabOverlay.dirtyFlagsText.clear(index); - tabOverlay.dirtyFlagsPing.clear(index); - Icon icon = tabOverlay.icon[index]; - UUID customSlotUuid; - if (icon.isAlex()) { - customSlotUuid = CUSTOM_SLOT_UUID_ALEX[index]; - } else { - customSlotUuid = CUSTOM_SLOT_UUID_STEVE[index]; - } - slotState[index] = SlotState.CUSTOM; - slotUuid[index] = customSlotUuid; - LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); - item1.setName(slotUsername[index] = getCustomSlotUsername(index)); - Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); - item1.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check formatting - item1.setLatency(tabOverlay.ping[index]); - item1.setGameMode(0); - LegacyPlayerListItem packet1 = new LegacyPlayerListItem(ADD_PLAYER, List.of(item1)); - sendPacket(packet1); - if (is18) { - packet1 = new LegacyPlayerListItem(UPDATE_DISPLAY_NAME, List.of(item1)); - sendPacket(packet1); - } - } - } - } - if (canShrink) { - sendPacket(packet); - tabOverlay.dirtyFlagSize = true; - needUpdate = true; - result = PacketListenerResult.CANCEL; - } else { - result = PacketListenerResult.PASS; - } - break; - default: - throw new AssertionError("Unknown action: " + action); - } - if (needUpdate) { - update(); - } - return result; - } - - private String getCustomSlotUsername(int index) { - if (experimentalTabCompleteFixForTabSize80 && using80Slots) { - return ""; - } - if (experimentalTabCompleteSmileys) { - return CUSTOM_SLOT_USERNAME_SMILEYS[index]; - } else { - return CUSTOM_SLOT_USERNAME[index]; - } - } - - @Override - void onTeamPacketPreprocess(Team packet) { - if (!using80Slots) { - if (packet.getMode() == 1) { - TeamEntry teamEntry = serverTeams.get(packet.getName()); - if (teamEntry != null) { - for (String playerName : teamEntry.getPlayers()) { - int slot = playerUsernameToSlotMap.getInt(playerName); - if (slot != -1) { - // reset slot team - if (is13OrLater) { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[slot], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); - } else { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[slot], "", "", "", "always", "always", 0, (byte) 1)); - } - } - } - } - } - } else { - if (packet.getMode() == 1) { - TeamEntry teamEntry = serverTeams.get(packet.getName()); - if (teamEntry != null) { - for (String playerName : teamEntry.getPlayers()) { - if (serverTabListPlayers.contains(playerName)) { - // reset slot team - sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[80], new String[]{playerName})); - } - } - } - } - } - } - - @Override - PacketListenerResult onTeamPacket(Team packet) { - if (CUSTOM_SLOT_TEAMNAMES.contains(packet.getName())) { - throw new AssertionError("Team name collision: " + packet); - } - if (!using80Slots) { - boolean modified = false; - switch (packet.getMode()) { - case 0: - case 3: - int count = 0; - String[] players = packet.getPlayers(); - for (int i = 0; i < players.length; i++) { - String playerName = players[i]; - if (-1 == playerUsernameToSlotMap.getInt(playerName)) { - count++; - } - } - if (count < players.length) { - modified = true; - String[] filteredPlayers = new String[count]; - int j = 0; - for (int i = 0; i < players.length; i++) { - String playerName = players[i]; - int slot; - if (-1 == (slot = playerUsernameToSlotMap.getInt(playerName))) { - filteredPlayers[j++] = playerName; - } else { - // update slot team - TeamEntry teamEntry = serverTeams.get(packet.getName()); - if (teamEntry != null) { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[slot], teamEntry.getDisplayName(), teamEntry.getPrefix(), teamEntry.getSuffix(), teamEntry.getNameTagVisibility(), teamEntry.getCollisionRule(), teamEntry.getColor(), teamEntry.getFriendlyFire())); - } - } - } - packet.setPlayers(filteredPlayers); - } - break; - case 4: - count = 0; - players = packet.getPlayers(); - for (int i = 0; i < players.length; i++) { - String playerName = players[i]; - if (-1 == playerUsernameToSlotMap.getInt(playerName)) { - count++; - } - } - if (count < players.length) { - modified = true; - String[] filteredPlayers = new String[count]; - int j = 0; - for (int i = 0; i < players.length; i++) { - String playerName = players[i]; - int slot; - if (-1 == (slot = playerUsernameToSlotMap.getInt(playerName))) { - filteredPlayers[j++] = playerName; - } else { - // reset slot team - if (is13OrLater) { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[slot], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); - } else { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[slot], "", "", "", "always", "always", 0, (byte) 1)); - } - } - } - packet.setPlayers(filteredPlayers); - } - break; - case 2: - TeamEntry teamEntry = serverTeams.get(packet.getName()); - if (teamEntry != null) { - for (String playerName : teamEntry.getPlayers()) { - int slot = playerUsernameToSlotMap.getInt(playerName); - if (slot != -1) { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[slot], teamEntry.getDisplayName(), teamEntry.getPrefix(), teamEntry.getSuffix(), teamEntry.getNameTagVisibility(), teamEntry.getCollisionRule(), teamEntry.getColor(), teamEntry.getFriendlyFire())); - } - } - } - - break; - - } - if (modified) { - return PacketListenerResult.MODIFIED; - } - } else { - - switch (packet.getMode()) { - case 0: - case 3: - /* - // Don't need this. Adding the player to another team will remove him from the current one. - String[] players = packet.getPlayers(); - for (int i = 0; i < players.length; i++) { - String playerName = players[i]; - if (serverTabListPlayers.contains(playerName)) { - // remove player from overflow team - sendPacket(createPacketTeamRemovePlayers(CUSTOM_SLOT_TEAMNAME[80], new String[]{playerName})); - } - }*/ - break; - case 4: - String[] players = packet.getPlayers(); - for (int i = 0; i < players.length; i++) { - String playerName = players[i]; - if (serverTabListPlayers.contains(playerName)) { - // add player to overflow team - sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[80], new String[]{playerName})); - } - } - break; - } - } - return PacketListenerResult.PASS; - } - - @Override - void onServerSwitch() { - boolean altRespawn = isUsingAltRespawn(); - - if (altRespawn) { - createTeamsIfNecessary(); - } - - if (!using80Slots) { - T tabOverlay = getTabOverlay(); - - // all players are gone - for (int index = 0; index < 80; index++) { - if (slotState[index] == SlotState.PLAYER) { - // Switch slot 'index' from player to custom mode - - if (!altRespawn) { - // 1. remove player from team - sendPacket(createPacketTeamRemovePlayers(CUSTOM_SLOT_TEAMNAME[index], new String[]{slotUsername[index]})); - // reset slot team - if (is13OrLater) { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); - } else { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], "", "", "", "always", "always", 0, (byte) 1)); - } - } - - // 2. create new custom slot - tabOverlay.dirtyFlagsIcon.clear(index); - tabOverlay.dirtyFlagsText.clear(index); - tabOverlay.dirtyFlagsPing.clear(index); - Icon icon = tabOverlay.icon[index]; - UUID customSlotUuid; - if (icon.isAlex()) { - customSlotUuid = CUSTOM_SLOT_UUID_ALEX[index]; - } else { - customSlotUuid = CUSTOM_SLOT_UUID_STEVE[index]; - } - slotState[index] = SlotState.CUSTOM; - slotUuid[index] = customSlotUuid; - LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); - item1.setName(slotUsername[index] = getCustomSlotUsername(index)); - Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); - item1.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check formatting - item1.setLatency(tabOverlay.ping[index]); - item1.setGameMode(0); - LegacyPlayerListItem packet1 = new LegacyPlayerListItem(ADD_PLAYER, List.of(item1)); - sendPacket(packet1); - if (is18) { - packet1 = new LegacyPlayerListItem(UPDATE_DISPLAY_NAME, List.of(item1)); - sendPacket(packet1); - } - } - } - freePlayers.clear(); - playerUuidToSlotMap.clear(); - playerUsernameToSlotMap.clear(); - } - viewerIsSpectator = false; - } - - @Override - void onActivated(AbstractContentOperationModeHandler previous) { - if (previous instanceof CustomContentTabOverlayHandler) { - this.viewerIsSpectator = ((CustomContentTabOverlayHandler) previous).viewerIsSpectator; - } else { - PlayerListEntry viewerEntry = serverPlayerList.get(viewerUuid); - this.viewerIsSpectator = viewerEntry != null && viewerEntry.getGamemode() == 3; - - // switch all players except for viewer in survival mode if they are in spectator mode - if (!using80Slots) { - int count = 0; - for (PlayerListEntry entry : serverPlayerList.values()) { - if (entry != viewerEntry && entry.getGamemode() == 3) { - count++; - } - } - - if (count > 0) { - LegacyPlayerListItem.Item[] items = new LegacyPlayerListItem.Item[count]; - int index = 0; - - for (Map.Entry mEntry : serverPlayerList.entrySet()) { - PlayerListEntry entry = mEntry.getValue(); - if (entry != viewerEntry && entry.getGamemode() == 3) { - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(mEntry.getKey()); - item.setGameMode(0); - items[index++] = item; - } - } - - LegacyPlayerListItem packet = new LegacyPlayerListItem(UPDATE_GAMEMODE, Arrays.asList(items)); - sendPacket(packet); - } - } - createTeamsIfNecessary(); - - } - - if (!using80Slots) { - this.freePlayers.addAll(serverPlayerList.keySet()); - - if (!this.freePlayers.isEmpty()) { - - for (PlayerListEntry entry : serverPlayerList.values()) { - if (!playerToTeamMap.containsKey(entry.username)) { - sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[80], new String[]{entry.username})); - } - } - getTabOverlay().dirtyFlagSize = true; - } - } - } - - private void createTeamsIfNecessary() { - // create teams if not already created - if (!hasCreatedCustomTeams) { - hasCreatedCustomTeams = true; - - if (is13OrLater) { - sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[0], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[0], CUSTOM_SLOT_USERNAME_SMILEYS[0], ""})); - } else { - sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[0], "", "", "", "always", "always", 0, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[0], CUSTOM_SLOT_USERNAME_SMILEYS[0], ""})); - } - - for (int i = 1; i < 80; i++) { - if (is13OrLater) { - sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[i], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[i], CUSTOM_SLOT_USERNAME_SMILEYS[i]})); - } else { - sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[i], "", "", "", "always", "always", 0, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[i], CUSTOM_SLOT_USERNAME_SMILEYS[i]})); - } - } - if (is13OrLater) { - sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[80], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[80]})); - } else { - sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[80], "", "", "", "always", "always", 0, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[80]})); - } - - areCustomSlotUsersPartOfTeams = true; - } - } - - @Override - void onDeactivated() { - int customSlots = 0; - for (int index = 0; index < 80; index++) { - if (slotState[index] != SlotState.UNUSED) { - if (slotState[index] == SlotState.PLAYER) { - // switch slot from player to unused permanently freeing the associated player - - // 1. remove player from team - sendPacket(createPacketTeamRemovePlayers(CUSTOM_SLOT_TEAMNAME[index], new String[]{slotUsername[index]})); - String playerTeamName; - if ((playerTeamName = playerToTeamMap.get(slotUsername[index])) != null) { - // 2. add player to correct team - sendPacket(createPacketTeamAddPlayers(playerTeamName, new String[]{slotUsername[index]})); - // 3. reset custom slot team - if (is13OrLater) { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); - } else { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], "", "", "", "always", "always", 0, (byte) 1)); - } - } - } else { - customSlots++; - } - } - } - - if (using80Slots) { - for (String player : serverTabListPlayers) { - if (playerToTeamMap.get(player) == null) { - // remove player from overflow team - sendPacket(createPacketTeamRemovePlayers(CUSTOM_SLOT_TEAMNAME[80], new String[]{player})); - } - } - // account for spacer players - if (experimentalTabCompleteFixForTabSize80) { - customSlots += 17; - } - } - - int i = 0; - if (customSlots > 0) { - LegacyPlayerListItem.Item[] items = new LegacyPlayerListItem.Item[customSlots]; - for (int index = 0; index < 80; index++) { - // switch slot from custom to unused - if (slotState[index] == SlotState.CUSTOM) { - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUuid[index]); - items[i++] = item; - } - } - if (experimentalTabCompleteFixForTabSize80 && using80Slots) { - for (int j = 0; j < 17; j++) { - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(CUSTOM_SLOT_UUID_SPACER[j]); - items[i++] = item; - } - } - LegacyPlayerListItem packet = new LegacyPlayerListItem(REMOVE_PLAYER, Arrays.asList(items)); - sendPacket(packet); - } - } - - @Override - void update() { - T tabOverlay = getTabOverlay(); - - if (OPTION_ENABLE_CONSISTENCY_CHECKS) { - if (!using80Slots && usedSlotsCount < serverPlayerList.size() && !tabOverlay.dirtyFlagSize) { - logger.severe("tabOverlay.dirtyFlagSize not set but resize required"); - tabOverlay.dirtyFlagSize = true; - } - } - - boolean updateAllCustomSlots = false; - - if (tabOverlay.dirtyFlagSize) { - tabOverlay.dirtyFlagSize = false; - updateSize(); - if (usedSlotsCount != (usedSlotsCount = usedSlots.cardinality())) { - - if (using80Slots) { - // when previously using 80 slots - for (int index = 0; index < 80; index++) { - if (tabOverlay.uuid[index] != null) { - dirtySlots.set(index); - } - } - freePlayers.addAll(serverPlayerList.keySet()); - - // switch all players except for viewer in survival mode if they are in spectator mode - PlayerListEntry viewerEntry = serverPlayerList.get(viewerUuid); - int count = 0; - for (PlayerListEntry entry : serverPlayerList.values()) { - if (entry != viewerEntry && entry.getGamemode() == 3) { - count++; - } - } - - if (count > 0) { - LegacyPlayerListItem.Item[] items = new LegacyPlayerListItem.Item[count]; - int index = 0; - - for (Map.Entry mEntry : serverPlayerList.entrySet()) { - PlayerListEntry entry = mEntry.getValue(); - if (entry != viewerEntry && entry.getGamemode() == 3) { - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(mEntry.getKey()); - item.setGameMode(0); - items[index++] = item; - } - } - - LegacyPlayerListItem packet = new LegacyPlayerListItem(UPDATE_GAMEMODE, Arrays.asList(items)); - sendPacket(packet); - } - - // remove spacer slots - if (experimentalTabCompleteFixForTabSize80) { - for (int i = 0; i < 17; i++) { - LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(CUSTOM_SLOT_UUID_SPACER[i]); - itemQueueRemovePlayer.add(item1); - } - } - - dirtySlots.set(0, 80); - updateAllCustomSlots = true; - } else if (viewerIsSpectator && highestUsedSlotIndex >= 0) { - dirtySlots.set(highestUsedSlotIndex); - } - - highestUsedSlotIndex = usedSlots.previousSetBit(79); - using80Slots = this.usedSlotsCount == 80; - if (using80Slots) { - // we switched to 80 slots - for (int index = 0; index < 80; index++) { - if (slotState[index] == SlotState.PLAYER) { - - // 1. remove player from team - sendPacket(createPacketTeamRemovePlayers(CUSTOM_SLOT_TEAMNAME[index], new String[]{slotUsername[index]})); - playerUsernameToSlotMap.removeInt(slotUsername[index]); - String playerTeamName; - if ((playerTeamName = playerToTeamMap.get(slotUsername[index])) != null) { - // 2. add player to correct team - sendPacket(createPacketTeamAddPlayers(playerTeamName, new String[]{slotUsername[index]})); - // 3. reset custom slot team - if (is13OrLater) { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); - } else { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], "", "", "", "always", "always", 0, (byte) 1)); - } - } else { - // 2. add player to overflow team - sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[80], new String[]{slotUsername[index]})); - } - - // 4. create new custom slot - tabOverlay.dirtyFlagsIcon.clear(index); - tabOverlay.dirtyFlagsText.clear(index); - tabOverlay.dirtyFlagsPing.clear(index); - Icon icon = tabOverlay.icon[index]; - UUID customSlotUuid; - if (icon.isAlex()) { - customSlotUuid = CUSTOM_SLOT_UUID_ALEX[index]; - } else { - customSlotUuid = CUSTOM_SLOT_UUID_STEVE[index]; - } - slotState[index] = SlotState.CUSTOM; - slotUuid[index] = customSlotUuid; - LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); - item1.setName(slotUsername[index] = getCustomSlotUsername(index)); - Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); - item1.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check Formatting - item1.setLatency(tabOverlay.ping[index]); - item1.setGameMode(0); - itemQueueAddPlayer.add(item1); - } else { - // custom - if (slotState[index] == SlotState.CUSTOM) { - LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(slotUuid[index]); - itemQueueRemovePlayer.add(item1); - } - // unused - tabOverlay.dirtyFlagsIcon.clear(index); - tabOverlay.dirtyFlagsText.clear(index); - tabOverlay.dirtyFlagsPing.clear(index); - Icon icon = tabOverlay.icon[index]; - UUID customSlotUuid; - if (icon.isAlex()) { - customSlotUuid = CUSTOM_SLOT_UUID_ALEX[index]; - } else { - customSlotUuid = CUSTOM_SLOT_UUID_STEVE[index]; - } - slotState[index] = SlotState.CUSTOM; - slotUuid[index] = customSlotUuid; - LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); - item1.setName(slotUsername[index] = getCustomSlotUsername(index)); - Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); - item1.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check Formatting - item1.setLatency(tabOverlay.ping[index]); - item1.setGameMode(0); - itemQueueAddPlayer.add(item1); - } - } - - // restore player gamemode - LegacyPlayerListItem packet; - List items = new ArrayList<>(serverPlayerList.size()); - items.clear(); - for (PlayerListEntry entry : serverPlayerList.values()) { - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(entry.getUuid()); - item.setGameMode(entry.getGamemode()); - items.add(item); - } - packet = new LegacyPlayerListItem(UPDATE_GAMEMODE, items); - sendPacket(packet); - - for (UUID player : freePlayers) { - String username = serverPlayerList.get(player).username; - String playerTeamName = playerToTeamMap.get(username); - if (playerTeamName != null) { - // add player to correct team - sendPacket(createPacketTeamAddPlayers(playerTeamName, new String[]{username})); - } else { - // add player to overflow team - sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[80], new String[]{username})); - } - } - - // create spacer slots - if (experimentalTabCompleteFixForTabSize80) { - for (int i = 0; i < 17; i++) { - LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(CUSTOM_SLOT_UUID_SPACER[i]); - item1.setName(""); - Property119Handler.setProperties(item1, EMPTY_PROPERTIES_ARRAY); - item1.setDisplayName(null); - item1.setLatency(0); - item1.setGameMode(0); - itemQueueAddPlayer.add(item1); - } - } - - // save some memory - freePlayers.clear(); - playerUuidToSlotMap.clear(); - playerUsernameToSlotMap.clear(); - } else { - - if (viewerIsSpectator && highestUsedSlotIndex >= 0) { - dirtySlots.set(highestUsedSlotIndex); - } - } - - sendQueuedItems(); - } - } - - - if (OPTION_ENABLE_CONSISTENCY_CHECKS) { - if (!using80Slots && usedSlotsCount < serverPlayerList.size()) { - throw new AssertionError("resize failed"); - } - } - - if (OPTION_ENABLE_CONSISTENCY_CHECKS) { - if (!using80Slots && freePlayers.size() + playerUuidToSlotMap.size() != serverPlayerList.size()) { - // inconsistent data -> rebuild - logger.severe("Detected inconsistency: freePlayers set or playerUuidToSlotMap is inconsistent"); - freePlayers.clear(); - freePlayers.addAll(serverPlayerList.keySet()); - playerUuidToSlotMap.clear(); - for (int index = 0; index <= highestUsedSlotIndex; index++) { - if (freePlayers.remove(slotUuid[index])) { - playerUuidToSlotMap.put(slotUuid[index], index); - } - } - } - } - - if (!using80Slots) { - dirtySlots.orAndClear(tabOverlay.dirtyFlagsUuid); - - if (!dirtySlots.isEmpty() || !freePlayers.isEmpty()) { - // mark slots as dirty currently being used with the uuid of dirty slots - for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { - int i = index; - do { - UUID uuid = tabOverlay.uuid[i]; - if (uuid == null) { - break; - } - i = playerUuidToSlotMap.getInt(uuid); - if (i == -1) { - break; - } - if (dirtySlots.get(i)) { - break; - } else { - dirtySlots.set(i); - } - } while (i < index); - } - - if (OPTION_ENABLE_CONSISTENCY_CHECKS) { - if (viewerIsSpectator) { - int i; - if (highestUsedSlotIndex != (i = playerUuidToSlotMap.getInt(viewerUuid))) { - if (!dirtySlots.get(highestUsedSlotIndex)) { - logger.severe("Spectator mode handling issue: highestUsedSlotIndex not marked as dirty"); - dirtySlots.set(highestUsedSlotIndex); - } else if (i == -1 && !freePlayers.contains(viewerUuid)) { - logger.severe("Spectator mode handling issue: viewer neither in freePlayers set and nor in tab list"); - } else if (i != -1 && !dirtySlots.get(i)) { - logger.severe("Spectator mode handling issue: viewer slot not marked as dirty"); - dirtySlots.set(i); - } - } - } - } - - // pass 1: free players - for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { - if (usedSlots.get(index)) { - if (slotState[index] == SlotState.PLAYER) { - // temporarily free associated player - set slot state to unused - - // 1. remove player from team - if (slotUuid[index].version() != 2) { // dirty hack for Citizens compatibility - sendPacket(createPacketTeamRemovePlayers(CUSTOM_SLOT_TEAMNAME[index], new String[]{slotUsername[index]})); - playerUsernameToSlotMap.removeInt(slotUsername[index]); - - // reset slot team - if (is13OrLater) { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); - } else { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], "", "", "", "always", "always", 0, (byte) 1)); - } - } - - // 2. update slot state - slotState[index] = SlotState.UNUSED; - freePlayers.add(slotUuid[index]); - slotUuid[index] = null; - slotUsername[index] = null; - } - } else { - if (slotState[index] != SlotState.UNUSED) { - // remove slot - free (temporarily) associated player if any - set slot state to unused - if (slotState[index] == SlotState.PLAYER) { - // temporarily free associated player - set slot state to unused - - // 1. remove player from team - if (slotUuid[index].version() != 2) { // dirty hack for Citizens compatibility - sendPacket(createPacketTeamRemovePlayers(CUSTOM_SLOT_TEAMNAME[index], new String[]{slotUsername[index]})); - playerUsernameToSlotMap.removeInt(slotUsername[index]); - - // reset slot team - if (is13OrLater) { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); - } else { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], "", "", "", "always", "always", 0, (byte) 1)); - } - } - - freePlayers.add(slotUuid[index]); - } else { - // 1. remove custom slot player - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUuid[index]); - itemQueueRemovePlayer.add(item); - } - - // 2. update slot state - slotState[index] = SlotState.UNUSED; - slotUuid[index] = null; - slotUsername[index] = null; - } - } - } - - if (viewerIsSpectator && !viewerUuid.equals(slotUuid[highestUsedSlotIndex])) { - if (slotState[highestUsedSlotIndex] != SlotState.UNUSED) { - if (OPTION_ENABLE_CONSISTENCY_CHECKS) { - if (slotState[highestUsedSlotIndex] == SlotState.PLAYER) { - throw new AssertionError("slotState[highestUsedSlotIndex] == SlotState.PLAYER"); - } - } - // switch slot 'highestUsedSlotIndex' from custom to unused - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUuid[highestUsedSlotIndex]); - itemQueueRemovePlayer.add(item); - } - // switch slot 'highestUsedSlotIndex' from unused to player with 'viewerUuid' - String playerUsername = serverPlayerList.get(viewerUuid).getUsername(); - String playerTeamName; - if (null != (playerTeamName = playerToTeamMap.get(playerUsername))) { - // 1. remove player from old team - sendPacket(createPacketTeamRemovePlayers(playerTeamName, new String[]{playerUsername})); - // 2. update properties of new team - TeamEntry teamEntry = serverTeams.get(playerTeamName); - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[highestUsedSlotIndex], teamEntry.getDisplayName(), teamEntry.getPrefix(), teamEntry.getSuffix(), teamEntry.getNameTagVisibility(), teamEntry.getCollisionRule(), teamEntry.getColor(), teamEntry.getFriendlyFire())); - } - // 3. Add to new team - sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[highestUsedSlotIndex], new String[]{playerUsername})); - // 4. Update display name - LegacyPlayerListItem.Item itemUpdateDisplayName = new LegacyPlayerListItem.Item(viewerUuid); - tabOverlay.dirtyFlagsText.clear(highestUsedSlotIndex); - itemUpdateDisplayName.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[highestUsedSlotIndex])); // TODO: Check Formatting - itemQueueUpdateDisplayName.add(itemUpdateDisplayName); - // 5. Update ping - LegacyPlayerListItem.Item itemUpdatePing = new LegacyPlayerListItem.Item(viewerUuid); - tabOverlay.dirtyFlagsPing.clear(highestUsedSlotIndex); - itemUpdatePing.setLatency(tabOverlay.ping[highestUsedSlotIndex]); - itemQueueUpdatePing.add(itemUpdatePing); - // 6. Update slot state - slotState[highestUsedSlotIndex] = SlotState.PLAYER; - slotUuid[highestUsedSlotIndex] = viewerUuid; - slotUsername[highestUsedSlotIndex] = playerUsername; - playerUsernameToSlotMap.put(playerUsername, highestUsedSlotIndex); - playerUuidToSlotMap.put(viewerUuid, highestUsedSlotIndex); - - freePlayers.remove(viewerUuid); - } - - // pass 2: assign players to new slots - for (int repeat = 1; repeat > 0; repeat--) { - for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { - if (usedSlots.get(index) && slotState[index] != SlotState.PLAYER) { - UUID uuid = tabOverlay.uuid[index]; - if (uuid != null && freePlayers.remove(uuid)) { - // switch slot to player mode using player with 'uuid' - if (slotState[index] == SlotState.CUSTOM) { - // custom -> unused - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUuid[index]); - itemQueueRemovePlayer.add(item); - } - String playerUsername = serverPlayerList.get(uuid).getUsername(); - String playerTeamName; - if (null != (playerTeamName = playerToTeamMap.get(playerUsername))) { - // 1. remove player from old team - sendPacket(createPacketTeamRemovePlayers(playerTeamName, new String[]{playerUsername})); - // 2. update properties of new team - TeamEntry teamEntry = serverTeams.get(playerTeamName); - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], teamEntry.getDisplayName(), teamEntry.getPrefix(), teamEntry.getSuffix(), teamEntry.getNameTagVisibility(), teamEntry.getCollisionRule(), teamEntry.getColor(), teamEntry.getFriendlyFire())); - } - // 3. Add to new team - sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[index], new String[]{playerUsername})); - // 4. Update display name - LegacyPlayerListItem.Item itemUpdateDisplayName = new LegacyPlayerListItem.Item(uuid); - tabOverlay.dirtyFlagsText.clear(index); - itemUpdateDisplayName.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check Formatting - itemQueueUpdateDisplayName.add(itemUpdateDisplayName); - // 5. Update ping - LegacyPlayerListItem.Item itemUpdatePing = new LegacyPlayerListItem.Item(uuid); - tabOverlay.dirtyFlagsPing.clear(index); - itemUpdatePing.setLatency(tabOverlay.ping[index]); - itemQueueUpdatePing.add(itemUpdatePing); - // 6. Update slot state - slotState[index] = SlotState.PLAYER; - slotUuid[index] = uuid; - slotUsername[index] = playerUsername; - playerUsernameToSlotMap.put(playerUsername, index); - playerUuidToSlotMap.put(uuid, index); - } - } - } - - // should not happen too often - if (!freePlayers.isEmpty()) { - for (int slot = 0; slot < 80; slot++) { - UUID uuid; - if (slotState[slot] == SlotState.CUSTOM && (uuid = tabOverlay.uuid[slot]) != null && freePlayers.contains(uuid)) { - dirtySlots.set(slot); - repeat = 2; - } - } - } - } - - // pass 3: distribute remaining 'freePlayers' on the tab list - int index = 80; - for (ObjectIterator iterator = freePlayers.iterator(); iterator.hasNext(); ) { - UUID uuid = iterator.next(); - for (index = usedSlots.previousSetBit(index - 1); index >= 0; index = usedSlots.previousSetBit(index - 1)) { - if (slotState[index] != SlotState.PLAYER) { - // switch slot to player mode using the player 'uuid' - if (slotState[index] == SlotState.CUSTOM) { - // custom -> unused - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUuid[index]); - itemQueueRemovePlayer.add(item); - } - String playerUsername = serverPlayerList.get(uuid).getUsername(); - if (uuid.version() != 2) { // dirty hack for Citizens compatibility - String playerTeamName; - if (null != (playerTeamName = playerToTeamMap.get(playerUsername))) { - // 1. remove player from old team - sendPacket(createPacketTeamRemovePlayers(playerTeamName, new String[]{playerUsername})); - // 2. update properties of new team - TeamEntry teamEntry = serverTeams.get(playerTeamName); - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], teamEntry.getDisplayName(), teamEntry.getPrefix(), teamEntry.getSuffix(), teamEntry.getNameTagVisibility(), teamEntry.getCollisionRule(), teamEntry.getColor(), teamEntry.getFriendlyFire())); - } - // 3. Add to new team - sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[index], new String[]{playerUsername})); - playerUsernameToSlotMap.put(playerUsername, index); - } - // 4. Update display name - LegacyPlayerListItem.Item itemUpdateDisplayName = new LegacyPlayerListItem.Item(uuid); - tabOverlay.dirtyFlagsText.clear(index); - itemUpdateDisplayName.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check Formatting - itemQueueUpdateDisplayName.add(itemUpdateDisplayName); - // 5. Update ping - LegacyPlayerListItem.Item itemUpdatePing = new LegacyPlayerListItem.Item(uuid); - tabOverlay.dirtyFlagsPing.clear(index); - itemUpdatePing.setLatency(tabOverlay.ping[index]); - itemQueueUpdatePing.add(itemUpdatePing); - // 6. Update slot state - slotState[index] = SlotState.PLAYER; - slotUuid[index] = uuid; - slotUsername[index] = playerUsername; - playerUuidToSlotMap.put(uuid, index); - iterator.remove(); - break; - } - } - if (index < 0) { - throw new AssertionError("Not enough space on player list."); - } - } - - // pass 4: switch some slots from unused to custom - for (index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { - if (usedSlots.get(index)) { - if (slotState[index] == SlotState.UNUSED || (updateAllCustomSlots && slotState[index] == SlotState.CUSTOM)) { - if (slotState[index] == SlotState.CUSTOM) { - LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(slotUuid[index]); - itemQueueRemovePlayer.add(item1); - } - tabOverlay.dirtyFlagsIcon.clear(index); - tabOverlay.dirtyFlagsText.clear(index); - tabOverlay.dirtyFlagsPing.clear(index); - Icon icon = tabOverlay.icon[index]; - UUID customSlotUuid; - if (icon.isAlex()) { - customSlotUuid = CUSTOM_SLOT_UUID_ALEX[index]; - } else { - customSlotUuid = CUSTOM_SLOT_UUID_STEVE[index]; - } - slotState[index] = SlotState.CUSTOM; - slotUuid[index] = customSlotUuid; - LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); - item1.setName(slotUsername[index] = getCustomSlotUsername(index)); - Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); - item1.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check Formatting - item1.setLatency(tabOverlay.ping[index]); - item1.setGameMode(0); - itemQueueAddPlayer.add(item1); - } - } - } - - // send first packet batch here to avoid conflicts when updating icons - sendQueuedItems(); - } - } - - // update icons - dirtySlots.copyAndClear(tabOverlay.dirtyFlagsIcon); - for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { - if (slotState[index] == SlotState.CUSTOM) { - Icon icon = tabOverlay.icon[index]; - UUID customSlotUuid; - if (icon.hasTextureProperty()) { - customSlotUuid = slotUuid[index]; - } else if (icon.isAlex()) { - customSlotUuid = CUSTOM_SLOT_UUID_ALEX[index]; - } else { // steve - customSlotUuid = CUSTOM_SLOT_UUID_STEVE[index]; - } - if (!customSlotUuid.equals(slotUuid[index]) || is119OrLater) { - LegacyPlayerListItem.Item itemRemove = new LegacyPlayerListItem.Item(slotUuid[index]); - itemQueueRemovePlayer.add(itemRemove); - } - tabOverlay.dirtyFlagsText.clear(index); - tabOverlay.dirtyFlagsPing.clear(index); - slotState[index] = SlotState.CUSTOM; - slotUuid[index] = customSlotUuid; - LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); - item1.setName(slotUsername[index] = getCustomSlotUsername(index)); - Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); - item1.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check Formatting - item1.setLatency(tabOverlay.ping[index]); - item1.setGameMode(0); - itemQueueAddPlayer.add(item1); - } - } - - // update text - dirtySlots.copyAndClear(tabOverlay.dirtyFlagsText); - for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { - if (slotState[index] != SlotState.UNUSED) { - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUuid[index]); - item.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check Formatting - itemQueueUpdateDisplayName.add(item); - } - } - - // update ping - dirtySlots.copyAndClear(tabOverlay.dirtyFlagsPing); - for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { - if (slotState[index] != SlotState.UNUSED) { - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUuid[index]); - item.setLatency(tabOverlay.ping[index]); - itemQueueUpdatePing.add(item); - } - } - - dirtySlots.clear(); - - // send packets - sendQueuedItems(); - } - - private void sendQueuedItems() { - if (!itemQueueRemovePlayer.isEmpty()) { - LegacyPlayerListItem packet = new LegacyPlayerListItem(REMOVE_PLAYER, itemQueueRemovePlayer); - sendPacket(packet); - itemQueueRemovePlayer.clear(); - } - if (!itemQueueAddPlayer.isEmpty()) { - LegacyPlayerListItem packet = new LegacyPlayerListItem(ADD_PLAYER, itemQueueAddPlayer); - sendPacket(packet); - if (is18) { - packet = new LegacyPlayerListItem(UPDATE_DISPLAY_NAME, itemQueueAddPlayer); - sendPacket(packet); - } - itemQueueAddPlayer.clear(); - } - if (!itemQueueUpdateDisplayName.isEmpty()) { - LegacyPlayerListItem packet = new LegacyPlayerListItem(UPDATE_DISPLAY_NAME, itemQueueUpdateDisplayName); - sendPacket(packet); - itemQueueUpdateDisplayName.clear(); - } - if (!itemQueueUpdatePing.isEmpty()) { - LegacyPlayerListItem packet = new LegacyPlayerListItem(UPDATE_LATENCY, itemQueueUpdatePing); - sendPacket(packet); - itemQueueUpdatePing.clear(); - } - } - - /** - * Updates the usedSlots BitSet. Sets the {@link #dirtySlots uuid dirty flag} for all added - * and removed slots. - */ - abstract void updateSize(); - } - - protected abstract boolean isExperimentalTabCompleteSmileys(); - - protected abstract boolean isExperimentalTabCompleteFixForTabSize80(); - - private abstract class CustomContentTabOverlay extends AbstractContentTabOverlay implements TabOverlayHandle.BatchModifiable { - final UUID[] uuid; - final Icon[] icon; - final String[] text; - final int[] ping; - - final AtomicInteger batchUpdateRecursionLevel; - volatile boolean dirtyFlagSize; - final ConcurrentBitSet dirtyFlagsUuid; - final ConcurrentBitSet dirtyFlagsIcon; - final ConcurrentBitSet dirtyFlagsText; - final ConcurrentBitSet dirtyFlagsPing; - - private CustomContentTabOverlay() { - this.uuid = new UUID[80]; - this.icon = new Icon[80]; - Arrays.fill(this.icon, Icon.DEFAULT_STEVE); - this.text = new String[80]; - Arrays.fill(this.text, EMPTY_JSON_TEXT); - this.ping = new int[80]; - this.batchUpdateRecursionLevel = new AtomicInteger(0); - this.dirtyFlagSize = true; - this.dirtyFlagsUuid = new ConcurrentBitSet(80); - this.dirtyFlagsIcon = new ConcurrentBitSet(80); - this.dirtyFlagsText = new ConcurrentBitSet(80); - this.dirtyFlagsPing = new ConcurrentBitSet(80); - } - - @Override - public void beginBatchModification() { - if (isValid()) { - if (batchUpdateRecursionLevel.incrementAndGet() < 0) { - throw new AssertionError("Recursion level overflow"); - } - } - } - - @Override - public void completeBatchModification() { - if (isValid()) { - int level = batchUpdateRecursionLevel.decrementAndGet(); - if (level == 0) { - scheduleUpdate(); - } else if (level < 0) { - throw new AssertionError("Recursion level underflow"); - } - } - } - - void scheduleUpdateIfNotInBatch() { - if (batchUpdateRecursionLevel.get() == 0) { - scheduleUpdate(); - } - } - - void setUuidInternal(int index, @Nullable UUID uuid) { - if (!Objects.equals(uuid, this.uuid[index])) { - this.uuid[index] = uuid; - dirtyFlagsUuid.set(index); - scheduleUpdateIfNotInBatch(); - } - } - - void setIconInternal(int index, @Nonnull @NonNull Icon icon) { - if (!icon.equals(this.icon[index])) { - this.icon[index] = icon; - dirtyFlagsIcon.set(index); - scheduleUpdateIfNotInBatch(); - } - } - - void setTextInternal(int index, @Nonnull @NonNull String text) { - String jsonText = ChatFormat.formattedTextToJson(text); - if (!jsonText.equals(this.text[index])) { - this.text[index] = jsonText; - dirtyFlagsText.set(index); - scheduleUpdateIfNotInBatch(); - } - } - - void setPingInternal(int index, int ping) { - if (ping != this.ping[index]) { - this.ping[index] = ping; - dirtyFlagsPing.set(index); - scheduleUpdateIfNotInBatch(); - } - } - } - - private class RectangularSizeHandler extends CustomContentTabOverlayHandler { - - @Override - void updateSize() { - RectangularTabOverlayImpl tabOverlay = getTabOverlay(); - RectangularTabOverlay.Dimension size = tabOverlay.getSize(); - if (size.getSize() < serverPlayerList.size() && size.getSize() != 80) { - for (RectangularTabOverlay.Dimension dimension : tabOverlay.getSupportedSizes()) { - if (dimension.getColumns() < tabOverlay.getSize().getColumns()) - continue; - if (dimension.getRows() < tabOverlay.getSize().getRows()) - continue; - if (size.getSize() < serverPlayerList.size() && size.getSize() != 80) { - size = dimension; - } else if (size.getSize() > dimension.getSize() && dimension.getSize() > serverPlayerList.size()) { - size = dimension; - } - } - canShrink = true; - } else { - canShrink = false; - } - BitSet newUsedSlots = DIMENSION_TO_USED_SLOTS.get(size); - dirtySlots.orXor(usedSlots, newUsedSlots); - usedSlots = newUsedSlots; - } - - @Override - protected RectangularTabOverlayImpl createTabOverlay() { - return new RectangularTabOverlayImpl(); - } - } - - private class RectangularTabOverlayImpl extends CustomContentTabOverlay implements RectangularTabOverlay { - - @Nonnull - private Dimension size; - - private RectangularTabOverlayImpl() { - Optional dimensionZero = getSupportedSizes().stream().filter(size -> size.getSize() == 0).findAny(); - if (!dimensionZero.isPresent()) { - throw new AssertionError(); - } - this.size = dimensionZero.get(); - } - - @Nonnull - @Override - public Dimension getSize() { - return size; - } - - @Override - public Collection getSupportedSizes() { - return DIMENSION_TO_USED_SLOTS.keySet(); - } - - @Override - public void setSize(@Nonnull Dimension size) { - if (!getSupportedSizes().contains(size)) { - throw new IllegalArgumentException("Unsupported size " + size); - } - if (isValid() && !this.size.equals(size)) { - BitSet oldUsedSlots = DIMENSION_TO_USED_SLOTS.get(this.size); - BitSet newUsedSlots = DIMENSION_TO_USED_SLOTS.get(size); - for (int index = newUsedSlots.nextSetBit(0); index >= 0; index = newUsedSlots.nextSetBit(index + 1)) { - if (!oldUsedSlots.get(index)) { - uuid[index] = null; - icon[index] = Icon.DEFAULT_STEVE; - text[index] = EMPTY_JSON_TEXT; - ping[index] = 0; - } - } - this.size = size; - this.dirtyFlagSize = true; - scheduleUpdateIfNotInBatch(); - for (int index = oldUsedSlots.nextSetBit(0); index >= 0; index = oldUsedSlots.nextSetBit(index + 1)) { - if (!newUsedSlots.get(index)) { - uuid[index] = null; - icon[index] = Icon.DEFAULT_STEVE; - text[index] = EMPTY_JSON_TEXT; - ping[index] = 0; - } - } - } - } - - @Override - public void setSlot(int column, int row, @Nullable UUID uuid, @Nonnull Icon icon, @Nonnull String text, int ping) { - if (isValid()) { - Preconditions.checkElementIndex(column, size.getColumns(), "column"); - Preconditions.checkElementIndex(row, size.getRows(), "row"); - beginBatchModification(); - try { - int index = index(column, row); - setUuidInternal(index, uuid); - setIconInternal(index, icon); - setTextInternal(index, text); - setPingInternal(index, ping); - } finally { - completeBatchModification(); - } - } - } - - @Override - public void setSlot(int column, int row, @Nonnull Icon icon, @Nonnull String text, int ping) { - if (isValid()) { - Preconditions.checkElementIndex(column, size.getColumns(), "column"); - Preconditions.checkElementIndex(row, size.getRows(), "row"); - beginBatchModification(); - try { - int index = index(column, row); - setUuidInternal(index, null); - setIconInternal(index, icon); - setTextInternal(index, text); - setPingInternal(index, ping); - } finally { - completeBatchModification(); - } - } - } - - @Override - public void setUuid(int column, int row, @Nullable UUID uuid) { - if (isValid()) { - Preconditions.checkElementIndex(column, size.getColumns(), "column"); - Preconditions.checkElementIndex(row, size.getRows(), "row"); - setUuidInternal(index(column, row), uuid); - } - } - - @Override - public void setIcon(int column, int row, @Nonnull Icon icon) { - if (isValid()) { - Preconditions.checkElementIndex(column, size.getColumns(), "column"); - Preconditions.checkElementIndex(row, size.getRows(), "row"); - setIconInternal(index(column, row), icon); - } - } - - @Override - public void setText(int column, int row, @Nonnull String text) { - if (isValid()) { - Preconditions.checkElementIndex(column, size.getColumns(), "column"); - Preconditions.checkElementIndex(row, size.getRows(), "row"); - setTextInternal(index(column, row), text); - } - } - - @Override - public void setPing(int column, int row, int ping) { - if (isValid()) { - Preconditions.checkElementIndex(column, size.getColumns(), "column"); - Preconditions.checkElementIndex(row, size.getRows(), "row"); - setPingInternal(index(column, row), ping); - } - } - } - - private class SimpleOperationModeHandler extends CustomContentTabOverlayHandler { - - @Override - void updateSize() { - int newSize = getTabOverlay().size; - if (newSize != 80 && newSize < serverPlayerList.size()) { - newSize = Integer.min(serverPlayerList.size(), 80); - canShrink = true; - } else { - canShrink = false; - } - if (newSize > highestUsedSlotIndex + 1) { - dirtySlots.set(highestUsedSlotIndex + 1, newSize); - } else if (newSize <= highestUsedSlotIndex) { - dirtySlots.set(newSize, highestUsedSlotIndex + 1); - } - usedSlots = SIZE_TO_USED_SLOTS[newSize]; - } - - @Override - protected SimpleTabOverlayImpl createTabOverlay() { - return new SimpleTabOverlayImpl(); - } - } - - private class SimpleTabOverlayImpl extends CustomContentTabOverlay implements SimpleTabOverlay { - int size = 0; - - @Override - public int getSize() { - return size; - } - - @Override - public int getMaxSize() { - return 80; - } - - @Override - public void setSize(int size) { - if (size < 0 || size > 80) { - throw new IllegalArgumentException("size"); - } - this.size = size; - dirtyFlagSize = true; - scheduleUpdateIfNotInBatch(); - } - - @Override - public void setSlot(int index, @Nullable UUID uuid, @Nonnull Icon icon, @Nonnull String text, int ping) { - if (isValid()) { - Preconditions.checkElementIndex(index, size, "index"); - beginBatchModification(); - try { - setUuidInternal(index, uuid); - setIconInternal(index, icon); - setTextInternal(index, text); - setPingInternal(index, ping); - } finally { - completeBatchModification(); - } - } - } - - @Override - public void setSlot(int index, @Nonnull Icon icon, @Nonnull String text, int ping) { - if (isValid()) { - Preconditions.checkElementIndex(index, size, "index"); - beginBatchModification(); - try { - setUuidInternal(index, null); - setIconInternal(index, icon); - setTextInternal(index, text); - setPingInternal(index, ping); - } finally { - completeBatchModification(); - } - } - } - - @Override - public void setUuid(int index, UUID uuid) { - if (isValid()) { - Preconditions.checkElementIndex(index, size, "index"); - setUuidInternal(index, uuid); - } - } - - @Override - public void setIcon(int index, @Nonnull @NonNull Icon icon) { - if (isValid()) { - Preconditions.checkElementIndex(index, size, "index"); - setIconInternal(index, icon); - } - } - - @Override - public void setText(int index, @Nonnull @NonNull String text) { - if (isValid()) { - Preconditions.checkElementIndex(index, size, "index"); - setTextInternal(index, text); - } - } - - @Override - public void setPing(int index, int ping) { - if (isValid()) { - Preconditions.checkElementIndex(index, size, "index"); - setPingInternal(index, ping); - } - } - } - - private final class CustomHeaderAndFooterOperationModeHandler extends AbstractHeaderFooterOperationModeHandler { - - @Override - protected CustomHeaderAndFooterImpl createTabOverlay() { - return new CustomHeaderAndFooterImpl(); - } - - @Override - PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { - return PacketListenerResult.CANCEL; - } - - @Override - void onServerSwitch() { - // do nothing - } - - @Override - void onDeactivated() { - //do nothing - } - - @Override - void onActivated(AbstractHeaderFooterOperationModeHandler previous) { - // remove header/ footer - sendPacket(new HeaderAndFooter(EMPTY_JSON_TEXT, EMPTY_JSON_TEXT)); - } - - @Override - void update() { - CustomHeaderAndFooterImpl tabOverlay = getTabOverlay(); - if (tabOverlay.headerOrFooterDirty) { - tabOverlay.headerOrFooterDirty = false; - sendPacket(new HeaderAndFooter(tabOverlay.header, tabOverlay.footer)); - } - } - } - - private final class CustomHeaderAndFooterImpl extends AbstractHeaderFooterTabOverlay implements HeaderAndFooterHandle { - private String header = EMPTY_JSON_TEXT; - private String footer = EMPTY_JSON_TEXT; - - private volatile boolean headerOrFooterDirty = false; - - final AtomicInteger batchUpdateRecursionLevel = new AtomicInteger(0); - - @Override - public void beginBatchModification() { - if (isValid()) { - if (batchUpdateRecursionLevel.incrementAndGet() < 0) { - throw new AssertionError("Recursion level overflow"); - } - } - } - - @Override - public void completeBatchModification() { - if (isValid()) { - int level = batchUpdateRecursionLevel.decrementAndGet(); - if (level == 0) { - scheduleUpdate(); - } else if (level < 0) { - throw new AssertionError("Recursion level underflow"); - } - } - } - - void scheduleUpdateIfNotInBatch() { - if (batchUpdateRecursionLevel.get() == 0) { - scheduleUpdate(); - } - } - - @Override - public void setHeaderFooter(@Nullable String header, @Nullable String footer) { - this.header = ChatFormat.formattedTextToJson(header); - this.footer = ChatFormat.formattedTextToJson(footer); - headerOrFooterDirty = true; - scheduleUpdateIfNotInBatch(); - } - - @Override - public void setHeader(@Nullable String header) { - this.header = ChatFormat.formattedTextToJson(header); - headerOrFooterDirty = true; - scheduleUpdateIfNotInBatch(); - } - - @Override - public void setFooter(@Nullable String footer) { - this.footer = ChatFormat.formattedTextToJson(footer); - headerOrFooterDirty = true; - scheduleUpdateIfNotInBatch(); - } - } - - private static int index(int column, int row) { - return column * 20 + row; - } - - private static String[][] toPropertiesArray(ProfileProperty textureProperty) { - if (textureProperty == null) { - return EMPTY_PROPERTIES_ARRAY; - } else if (textureProperty.isSigned()) { - return new String[][]{{textureProperty.getName(), textureProperty.getValue(), textureProperty.getSignature()}}; - } else { - // todo maybe add warning on unsigned properties? - return new String[][]{{textureProperty.getName(), textureProperty.getValue()}}; - } - } - - private static Team createPacketTeamCreate(String name, String displayName, String prefix, String suffix, String nameTagVisibility, String collisionRule, int color, byte friendlyFire, String[] players) { - Team team = new Team(); - team.setName(name); - team.setMode((byte) 0); - team.setDisplayName(displayName); - team.setPrefix(prefix); - team.setSuffix(suffix); - team.setNameTagVisibility(nameTagVisibility); - if (TEAM_COLLISION_RULE_SUPPORTED) { - team.setCollisionRule(collisionRule); - } - team.setColor(color); - team.setFriendlyFire(friendlyFire); - team.setPlayers(players); - return team; - } - - private static Team createPacketTeamRemove(String name) { - Team team = new Team(); - team.setName(name); - team.setMode((byte) 1); - return team; - } - - private static Team createPacketTeamUpdate(String name, String displayName, String prefix, String suffix, String nameTagVisibility, String collisionRule, int color, byte friendlyFire) { - Team team = new Team(); - team.setName(name); - team.setMode((byte) 2); - team.setDisplayName(displayName); - team.setPrefix(prefix); - team.setSuffix(suffix); - team.setNameTagVisibility(nameTagVisibility); - if (TEAM_COLLISION_RULE_SUPPORTED) { - team.setCollisionRule(collisionRule); - } - team.setColor(color); - team.setFriendlyFire(friendlyFire); - return team; - } - - private static Team createPacketTeamAddPlayers(String name, String[] players) { - Team team = new Team(); - team.setName(name); - team.setMode((byte) 3); - team.setPlayers(players); - return team; - } - - private static Team createPacketTeamRemovePlayers(String name, String[] players) { - Team team = new Team(); - team.setName(name); - team.setMode((byte) 4); - team.setPlayers(players); - return team; - } - - private enum SlotState { - UNUSED, CUSTOM, PLAYER - } - - @Data - @NoArgsConstructor - @AllArgsConstructor - static class PlayerListEntry { - private UUID uuid; - private String[][] properties; - private String username; - private String displayName; - private int ping; - private int gamemode; - - private PlayerListEntry(LegacyPlayerListItem.Item item) { - this(item.getUuid(), null, item.getName(), GsonComponentSerializer.gson().serialize(item.getDisplayName()), item.getLatency(), item.getGameMode()); // TODO: Check Display Name - properties = Property119Handler.getProperties(item); - } - } - - @Data - static class TeamEntry { - private String displayName; - private String prefix; - private String suffix; - private byte friendlyFire; - private String nameTagVisibility; - private String collisionRule; - private int color; - private Set players = new ObjectOpenHashSet<>(); - - void addPlayer(String name) { - players.add(name); - } - - void removePlayer(String name) { - players.remove(name); - } - - public void setNameTagVisibility(String nameTagVisibility) { - this.nameTagVisibility = nameTagVisibility.intern(); - } - - public void setCollisionRule(String collisionRule) { - this.collisionRule = collisionRule == null ? null : collisionRule.intern(); - } - } -} +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.handler; + +import codecrafter47.bungeetablistplus.protocol.PacketHandler; +import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; +import codecrafter47.bungeetablistplus.protocol.Team; +import codecrafter47.bungeetablistplus.util.BitSet; +import codecrafter47.bungeetablistplus.util.ConcurrentBitSet; +import codecrafter47.bungeetablistplus.util.Property119Handler; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; +import de.codecrafter47.taboverlay.Icon; +import de.codecrafter47.taboverlay.ProfileProperty; +import de.codecrafter47.taboverlay.config.misc.ChatFormat; +import de.codecrafter47.taboverlay.config.misc.Unchecked; +import de.codecrafter47.taboverlay.handler.*; +import it.unimi.dsi.fastutil.objects.*; +import lombok.*; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.*; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem.ADD_PLAYER; +import static com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem.REMOVE_PLAYER; +import static com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem.UPDATE_DISPLAY_NAME; +import static com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem.UPDATE_GAMEMODE; +import static com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem.UPDATE_LATENCY; + +public abstract class AbstractTabOverlayHandler implements PacketHandler, TabOverlayHandler { + + // some options + private static final boolean OPTION_ENABLE_CUSTOM_SLOT_USERNAME_COLLISION_CHECK = true; + private static final boolean OPTION_ENABLE_CUSTOM_SLOT_UUID_COLLISION_CHECK = true; + private static final boolean OPTION_ENABLE_CONSISTENCY_CHECKS = true; + + private static final String EMPTY_JSON_TEXT = "{\"text\":\"\"}"; + protected static final String[][] EMPTY_PROPERTIES_ARRAY = new String[0][]; + + private static final boolean TEAM_COLLISION_RULE_SUPPORTED; + private static final boolean USE_PROTOCOL_PROPERTY_TYPE; + + private static final ImmutableMap DIMENSION_TO_USED_SLOTS; + private static final BitSet[] SIZE_TO_USED_SLOTS; + + private static final UUID[] CUSTOM_SLOT_UUID_STEVE; + private static final UUID[] CUSTOM_SLOT_UUID_ALEX; + private static final UUID[] CUSTOM_SLOT_UUID_SPACER; + private static final Set CUSTOM_SLOT_UUIDS; + private static final String[] CUSTOM_SLOT_USERNAME; + private static final String[] CUSTOM_SLOT_USERNAME_SMILEYS; + private static final Set CUSTOM_SLOT_USERNAMES; + private static final String[] CUSTOM_SLOT_TEAMNAME; + private static final Set CUSTOM_SLOT_TEAMNAMES; + + private static final Set blockedTeams = new HashSet<>(); + + static { + TEAM_COLLISION_RULE_SUPPORTED = true; + USE_PROTOCOL_PROPERTY_TYPE = true; + + // build the dimension to used slots map (for the rectangular tab overlay) + val builder = ImmutableMap.builder(); + for (int columns = 1; columns <= 4; columns++) { + for (int rows = 0; rows <= 20; rows++) { + if (columns != 1 && rows != 0 && columns * rows <= (columns - 1) * 20) + continue; + BitSet usedSlots = new BitSet(80); + for (int column = 0; column < columns; column++) { + for (int row = 0; row < rows; row++) { + usedSlots.set(index(column, row)); + } + } + builder.put(new RectangularTabOverlay.Dimension(columns, rows), usedSlots); + } + } + DIMENSION_TO_USED_SLOTS = builder.build(); + + // build the size to used slots map (for the simple tab overlay) + SIZE_TO_USED_SLOTS = new BitSet[81]; + for (int size = 0; size <= 80; size++) { + BitSet usedSlots = new BitSet(80); + usedSlots.set(0, size); + SIZE_TO_USED_SLOTS[size] = usedSlots; + } + + // generate random uuids for our custom slots + CUSTOM_SLOT_UUID_ALEX = new UUID[80]; + CUSTOM_SLOT_UUID_STEVE = new UUID[80]; + CUSTOM_SLOT_UUID_SPACER = new UUID[17]; + UUID base = UUID.randomUUID(); + long msb = base.getMostSignificantBits(); + long lsb = base.getLeastSignificantBits(); + lsb ^= base.hashCode(); + for (int i = 0; i < 80; i++) { + CUSTOM_SLOT_UUID_STEVE[i] = new UUID(msb, lsb ^ (2 * i)); + CUSTOM_SLOT_UUID_ALEX[i] = new UUID(msb, lsb ^ (2 * i + 1)); + } + for (int i = 0; i < 17; i++) { + CUSTOM_SLOT_UUID_SPACER[i] = new UUID(msb, lsb ^ (160 + i)); + } + if (OPTION_ENABLE_CUSTOM_SLOT_UUID_COLLISION_CHECK) { + CUSTOM_SLOT_UUIDS = ImmutableSet.builder() + .add(CUSTOM_SLOT_UUID_ALEX) + .add(CUSTOM_SLOT_UUID_STEVE) + .add(CUSTOM_SLOT_UUID_SPACER).build(); + } else { + CUSTOM_SLOT_UUIDS = null; + } + + // generate usernames for custom slots + int unique = ThreadLocalRandom.current().nextInt(); + CUSTOM_SLOT_USERNAME = new String[81]; + for (int i = 0; i < 81; i++) { + CUSTOM_SLOT_USERNAME[i] = String.format("~BTLP%08x %02d", unique, i); + } + if (OPTION_ENABLE_CUSTOM_SLOT_USERNAME_COLLISION_CHECK) { + CUSTOM_SLOT_USERNAMES = ImmutableSet.copyOf(CUSTOM_SLOT_USERNAME); + } else { + CUSTOM_SLOT_USERNAMES = null; + } + CUSTOM_SLOT_USERNAME_SMILEYS = new String[80]; + String emojis = "\u263a\u2639\u2620\u2763\u2764\u270c\u261d\u270d\u2618\u2615\u2668\u2693\u2708\u231b\u231a\u2600\u2b50\u2601\u2602\u2614\u26a1\u2744\u2603\u2604\u2660\u2665\u2666\u2663\u265f\u260e\u2328\u2709\u270f\u2712\u2702\u2692\u2694\u2699\u2696\u2697\u26b0\u26b1\u267f\u26a0\u2622\u2623\u2640\u2642\u267e\u267b\u269c\u303d\u2733\u2734\u2747\u203c\u2b1c\u2b1b\u25fc\u25fb\u25aa\u25ab\u2049\u26ab\u26aa\u3030\u00a9\u00ae\u2122\u2139\u24c2\u3297\u2716\u2714\u2611\u2695\u2b06\u2197\u27a1\u2198\u2b07\u2199\u3299\u2b05\u2196\u2195\u2194\u21a9\u21aa\u2934\u2935\u269b\u2721\u2638\u262f\u271d\u2626\u262a\u262e\u2648\u2649\u264a\u264b\u264c\u264d\u264e\u264f\u2650\u2651\u2652\u2653\u25b6\u25c0\u23cf"; + for (int i = 0; i < 80; i++) { + CUSTOM_SLOT_USERNAME_SMILEYS[i] = String.format("" + emojis.charAt(i), unique, i); + } + + // generate teams for custom slots + CUSTOM_SLOT_TEAMNAME = new String[81]; + for (int i = 0; i < 81; i++) { + CUSTOM_SLOT_TEAMNAME[i] = String.format(" BTLP%08x %02d", unique, i); + } + CUSTOM_SLOT_TEAMNAMES = ImmutableSet.copyOf(CUSTOM_SLOT_TEAMNAME); + } + + protected final Logger logger; + private final Executor eventLoopExecutor; + private final UUID viewerUuid; + + private final Object2ObjectMap serverPlayerList = new Object2ObjectOpenHashMap<>(); + protected final Set serverTabListPlayers = new ObjectOpenHashSet<>(); + @Nullable + protected String serverHeader = null; + @Nullable + protected String serverFooter = null; + protected final Object2ObjectMap serverTeams = new Object2ObjectOpenHashMap<>(); + protected final Object2ObjectMap playerToTeamMap = new Object2ObjectOpenHashMap<>(); + + private final Queue> nextActiveContentHandlerQueue = new ConcurrentLinkedQueue<>(); + private final Queue> nextActiveHeaderFooterHandlerQueue = new ConcurrentLinkedQueue<>(); + private AbstractContentOperationModeHandler activeContentHandler; + private AbstractHeaderFooterOperationModeHandler activeHeaderFooterHandler; + + private boolean hasCreatedCustomTeams = false; + private boolean areCustomSlotUsersPartOfTeams = false; + + private final AtomicBoolean updateScheduledFlag = new AtomicBoolean(false); + private final Runnable updateTask = this::update; + + private final boolean is18; + private boolean is13OrLater; + private boolean is119OrLater; + protected boolean active; + + public AbstractTabOverlayHandler(Logger logger, Executor eventLoopExecutor, UUID viewerUuid, boolean is18, boolean is13OrLater, boolean is119OrLater) { + this.logger = logger; + this.eventLoopExecutor = eventLoopExecutor; + this.viewerUuid = viewerUuid; + this.is18 = is18; + this.is13OrLater = is13OrLater; + this.is119OrLater = is119OrLater; + this.activeContentHandler = new PassThroughContentHandler(); + this.activeHeaderFooterHandler = new PassThroughHeaderFooterHandler(); + } + + protected abstract void sendPacket(MinecraftPacket packet); + + @Override + public PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { + switch (packet.getAction()) { + case ADD_PLAYER: + for (LegacyPlayerListItem.Item item : packet.getItems()) { + if (OPTION_ENABLE_CUSTOM_SLOT_UUID_COLLISION_CHECK) { + if (CUSTOM_SLOT_UUIDS.contains(item.getUuid())) { + throw new AssertionError("UUID collision " + item.getUuid()); + } + } + if (OPTION_ENABLE_CUSTOM_SLOT_USERNAME_COLLISION_CHECK) { + if (CUSTOM_SLOT_USERNAMES.contains(item.getName())) { + throw new AssertionError("Username collision" + item.getName()); + } + } + PlayerListEntry old = serverPlayerList.put(item.getUuid(), new PlayerListEntry(item)); + if (old != null) { + serverTabListPlayers.remove(old.getUsername()); + } + serverTabListPlayers.add(item.getName()); + } + break; + case UPDATE_GAMEMODE: + for (LegacyPlayerListItem.Item item : packet.getItems()) { + PlayerListEntry playerListEntry = serverPlayerList.get(item.getUuid()); + if (playerListEntry != null) { + playerListEntry.setGamemode(item.getGameMode()); + } + } + break; + case UPDATE_LATENCY: + for (LegacyPlayerListItem.Item item : packet.getItems()) { + PlayerListEntry playerListEntry = serverPlayerList.get(item.getUuid()); + if (playerListEntry != null) { + playerListEntry.setPing(item.getLatency()); + } + } + break; + case UPDATE_DISPLAY_NAME: + for (LegacyPlayerListItem.Item item : packet.getItems()) { + PlayerListEntry playerListEntry = serverPlayerList.get(item.getUuid()); + if (playerListEntry != null) { + playerListEntry.setDisplayName(GsonComponentSerializer.gson().serialize(item.getDisplayName())); + } + } + break; + case REMOVE_PLAYER: + for (LegacyPlayerListItem.Item item : packet.getItems()) { + PlayerListEntry removed = serverPlayerList.remove(item.getUuid()); + if (removed != null) { + serverTabListPlayers.remove(removed.getUsername()); + } + } + break; + } + + try { + return this.activeContentHandler.onPlayerListPacket(packet); + } catch (Throwable th) { + logger.log(Level.SEVERE, "Unexpected error", th); + // try recover + enterContentOperationMode(ContentOperationMode.PASS_TROUGH); + return PacketListenerResult.PASS; + } + } + + @Override + public PacketListenerResult onTeamPacket(Team packet) { + + if (packet.getPlayers() != null) { + boolean block = false; + for (String player : packet.getPlayers()) { + if (player.equals("")) { + block = true; + } + } + if (block) { + if (!blockedTeams.contains(packet.getName())) { + logger.warning("Blocking Team Packet for Team " + packet.getName() + ", as it is incompatible with BungeeTabListPlus."); + blockedTeams.add(packet.getName()); + } + return PacketListenerResult.CANCEL; + } + } + + try { + this.activeContentHandler.onTeamPacketPreprocess(packet); + } catch (Throwable th) { + logger.log(Level.SEVERE, "Unexpected error", th); + // try recover + enterContentOperationMode(ContentOperationMode.PASS_TROUGH); + } + + if (packet.getMode() == 1) { + TeamEntry team = serverTeams.remove(packet.getName()); + if (team != null) { + for (String player : team.getPlayers()) { + playerToTeamMap.remove(player, packet.getName()); + } + } + } else { + // Create or get old team + TeamEntry teamEntry; + if (packet.getMode() == 0) { + teamEntry = new TeamEntry(); + serverTeams.put(packet.getName(), teamEntry); + } else { + teamEntry = serverTeams.get(packet.getName()); + } + + if (teamEntry != null) { + if (packet.getMode() == 0 || packet.getMode() == 2) { + teamEntry.setDisplayName(packet.getDisplayName()); + teamEntry.setPrefix(packet.getPrefix()); + teamEntry.setSuffix(packet.getSuffix()); + teamEntry.setFriendlyFire(packet.getFriendlyFire()); + teamEntry.setNameTagVisibility(packet.getNameTagVisibility()); + if (TEAM_COLLISION_RULE_SUPPORTED) { + teamEntry.setCollisionRule(packet.getCollisionRule()); + } + teamEntry.setColor(packet.getColor()); + } + if (packet.getPlayers() != null) { + for (String s : packet.getPlayers()) { + if (packet.getMode() == 0 || packet.getMode() == 3) { + if (playerToTeamMap.containsKey(s)) { + TeamEntry previousTeam = serverTeams.get(playerToTeamMap.get(s)); + // previousTeam shouldn't be null (that's inconsistent with playerToTeamMap, but apparently it happens) + if (previousTeam != null) { + previousTeam.removePlayer(s); + } + } + teamEntry.addPlayer(s); + playerToTeamMap.put(s, packet.getName()); + } else { + teamEntry.removePlayer(s); + playerToTeamMap.remove(s, packet.getName()); + } + } + } + } + } + + try { + return this.activeContentHandler.onTeamPacket(packet); + } catch (Throwable th) { + logger.log(Level.SEVERE, "Unexpected error", th); + // try recover + enterContentOperationMode(ContentOperationMode.PASS_TROUGH); + return PacketListenerResult.PASS; + } + } + + @Override + public PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { + PacketListenerResult result = PacketListenerResult.PASS; + try { + result = this.activeHeaderFooterHandler.onPlayerListHeaderFooterPacket(packet); + if (result == PacketListenerResult.MODIFIED) { + throw new AssertionError("PacketListenerResult.MODIFIED must not be used"); + } + } catch (Throwable th) { + logger.log(Level.SEVERE, "Unexpected error", th); + // try recover + enterHeaderAndFooterOperationMode(HeaderAndFooterOperationMode.PASS_TROUGH); + } + + this.serverHeader = packet.getHeader() != null ? packet.getHeader() : EMPTY_JSON_TEXT; + this.serverFooter = packet.getFooter() != null ? packet.getFooter() : EMPTY_JSON_TEXT; + + return result; + } + + @Override + public void onServerSwitch(boolean is13OrLater) { + this.is13OrLater = is13OrLater; + if (!active) { + active = true; + update(); + } else { + + serverTeams.clear(); + playerToTeamMap.clear(); + + if (isUsingAltRespawn()) { + hasCreatedCustomTeams = false; + areCustomSlotUsersPartOfTeams = false; + } + + try { + this.activeContentHandler.onServerSwitch(); + } catch (Throwable th) { + logger.log(Level.SEVERE, "Unexpected error", th); + // try recover + enterContentOperationMode(ContentOperationMode.PASS_TROUGH); + } + try { + this.activeHeaderFooterHandler.onServerSwitch(); + } catch (Throwable th) { + logger.log(Level.SEVERE, "Unexpected error", th); + // try recover + enterContentOperationMode(ContentOperationMode.PASS_TROUGH); + } + + if (!serverPlayerList.isEmpty()) { + List items = new ArrayList<>(); + for(UUID uuid : serverPlayerList.keySet()){ + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(uuid); + items.add(item); + } + LegacyPlayerListItem packet = new LegacyPlayerListItem(REMOVE_PLAYER, items); + sendPacket(packet); + } + + serverPlayerList.clear(); + if (serverHeader != null) { + serverHeader = EMPTY_JSON_TEXT; + } + if (serverFooter != null) { + serverFooter = EMPTY_JSON_TEXT; + } + + serverTabListPlayers.clear(); + } + } + + protected boolean isUsingAltRespawn() { + return false; + } + + @Override + public R enterContentOperationMode(ContentOperationMode operationMode) { + AbstractContentOperationModeHandler handler; + if (operationMode == ContentOperationMode.PASS_TROUGH) { + handler = new PassThroughContentHandler(); + } else if (operationMode == ContentOperationMode.SIMPLE) { + handler = new SimpleOperationModeHandler(); + } else if (operationMode == ContentOperationMode.RECTANGULAR) { + handler = new RectangularSizeHandler(); + } else { + throw new UnsupportedOperationException(); + } + nextActiveContentHandlerQueue.add(handler); + scheduleUpdate(); + return Unchecked.cast(handler.getTabOverlay()); + } + + @Override + public R enterHeaderAndFooterOperationMode(HeaderAndFooterOperationMode operationMode) { + AbstractHeaderFooterOperationModeHandler handler; + if (operationMode == HeaderAndFooterOperationMode.PASS_TROUGH) { + handler = new PassThroughHeaderFooterHandler(); + } else if (operationMode == HeaderAndFooterOperationMode.CUSTOM) { + handler = new CustomHeaderAndFooterOperationModeHandler(); + } else { + throw new UnsupportedOperationException(Objects.toString(operationMode)); + } + nextActiveHeaderFooterHandlerQueue.add(handler); + scheduleUpdate(); + return Unchecked.cast(handler.getTabOverlay()); + } + + private void scheduleUpdate() { + if (this.updateScheduledFlag.compareAndSet(false, true)) { + try { + eventLoopExecutor.execute(updateTask); + } catch (RejectedExecutionException ignored) { + } + } + } + + private void update() { + if (!active) { + return; + } + updateScheduledFlag.set(false); + + // update content handler + AbstractContentOperationModeHandler contentHandler; + while (null != (contentHandler = nextActiveContentHandlerQueue.poll())) { + this.activeContentHandler.invalidate(); + contentHandler.onActivated(this.activeContentHandler); + this.activeContentHandler = contentHandler; + } + this.activeContentHandler.update(); + + // update header and footer handler + AbstractHeaderFooterOperationModeHandler heaerFooterHandler; + while (null != (heaerFooterHandler = nextActiveHeaderFooterHandlerQueue.poll())) { + this.activeHeaderFooterHandler.invalidate(); + heaerFooterHandler.onActivated(this.activeHeaderFooterHandler); + this.activeHeaderFooterHandler = heaerFooterHandler; + } + this.activeHeaderFooterHandler.update(); + } + + private abstract class AbstractContentOperationModeHandler extends OperationModeHandler { + + /** + * Called when the player receives a {@link LegacyPlayerListItem} packet. + *

+ * This method is called after this {@link AbstractTabOverlayHandler} has updated the {@code serverPlayerList}. + */ + abstract PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet); + + /** + * Called when the player receives a {@link Team} packet. + *

+ * This method is called before this {@link AbstractTabOverlayHandler} executes its own logic to update the + * server team info. + */ + abstract void onTeamPacketPreprocess(Team packet); + + /** + * Called when the player receives a {@link Team} packet. + *

+ * This method is called after this {@link AbstractTabOverlayHandler} executes its own logic to update the + * server team info. + */ + abstract PacketListenerResult onTeamPacket(Team packet); + + /** + * Called when the player switches the server. + *

+ * This method is called before this {@link AbstractTabOverlayHandler} executes its own logic to clear the + * server player list info. + */ + abstract void onServerSwitch(); + + abstract void update(); + + final void invalidate() { + getTabOverlay().invalidate(); + onDeactivated(); + } + + /** + * Called when this {@link OperationModeHandler} is deactivated. + *

+ * This method must put the client player list in the state expected by {@link #onActivated(AbstractContentOperationModeHandler)}. It must + * especially remove all custom entries and players must be part of the correct teams. + */ + abstract void onDeactivated(); + + /** + * Called when this {@link OperationModeHandler} becomes the active one. + *

+ * State of the player list when this method is called: + * - there are no custom entries on the client + * - all entries from {@link #serverPlayerList} are known to the client, but the client may know the wrong displayname, gamemode and ping + * - player list header/ footer may be wrong + *

+ * Additional information about the state of the player list may be obtained from the previous handler + * + * @param previous previous handler + */ + abstract void onActivated(AbstractContentOperationModeHandler previous); + } + + private abstract class AbstractHeaderFooterOperationModeHandler extends OperationModeHandler { + + /** + * Called when the player receives a {@link HeaderAndFooter} packet. + *

+ * This method is called before this {@link AbstractTabOverlayHandler} executes its own logic to update the + * server player list info. + */ + abstract PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet); + + /** + * Called when the player switches the server. + *

+ * This method is called before this {@link AbstractTabOverlayHandler} executes its own logic to clear the + * server player list info. + */ + abstract void onServerSwitch(); + + abstract void update(); + + final void invalidate() { + getTabOverlay().invalidate(); + onDeactivated(); + } + + /** + * Called when this {@link OperationModeHandler} is deactivated. + *

+ * This method must put the client player list in the state expected by {@link #onActivated(AbstractHeaderFooterOperationModeHandler)}. It must + * especially remove all custom entries and players must be part of the correct teams. + */ + abstract void onDeactivated(); + + /** + * Called when this {@link OperationModeHandler} becomes the active one. + *

+ * State of the player list when this method is called: + * - there are no custom entries on the client + * - all entries from {@link #serverPlayerList} are known to the client, but the client may know the wrong displayname, gamemode and ping + * - player list header/ footer may be wrong + *

+ * Additional information about the state of the player list may be obtained from the previous handler + * + * @param previous previous handler + */ + abstract void onActivated(AbstractHeaderFooterOperationModeHandler previous); + } + + private abstract static class AbstractContentTabOverlay implements TabOverlayHandle { + private boolean valid = true; + + @Override + public boolean isValid() { + return valid; + } + + final void invalidate() { + valid = false; + } + } + + private abstract static class AbstractHeaderFooterTabOverlay implements TabOverlayHandle { + private boolean valid = true; + + @Override + public boolean isValid() { + return valid; + } + + final void invalidate() { + valid = false; + } + } + + private final class PassThroughContentHandler extends AbstractContentOperationModeHandler { + + @Override + protected PassThroughContentTabOverlay createTabOverlay() { + return new PassThroughContentTabOverlay(); + } + + @Override + PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { + return PacketListenerResult.PASS; + } + + @Override + void onTeamPacketPreprocess(Team packet) { + // nothing to do + } + + @Override + PacketListenerResult onTeamPacket(Team packet) { + return PacketListenerResult.PASS; + } + + @Override + void onServerSwitch() { + sendPacket(new HeaderAndFooter(EMPTY_JSON_TEXT, EMPTY_JSON_TEXT)); + } + + @Override + void update() { + // nothing to do + } + + @Override + void onDeactivated() { + // nothing to do + } + + @Override + void onActivated(AbstractContentOperationModeHandler previous) { + if (previous instanceof PassThroughContentHandler) { + // we're lucky, nothing to do + return; + } + + // fix player list entries + if (!serverPlayerList.isEmpty()) { + // restore player ping + LegacyPlayerListItem packet; + List items = new ArrayList<>(serverPlayerList.size()); + for (PlayerListEntry entry : serverPlayerList.values()) { + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(entry.getUuid()); + item.setLatency(entry.getPing()); + items.add(item); + } + packet = new LegacyPlayerListItem(UPDATE_LATENCY, items); + sendPacket(packet); + + // restore player gamemode + items.clear(); + for (PlayerListEntry entry : serverPlayerList.values()) { + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(entry.getUuid()); + item.setGameMode(entry.getGamemode()); + items.add(item); + } + packet = new LegacyPlayerListItem(UPDATE_GAMEMODE, items); + sendPacket(packet); + + // restore player display name + items.clear(); + for (PlayerListEntry entry : serverPlayerList.values()) { + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(entry.getUuid()); + item.setDisplayName((entry.getDisplayName() != null && !entry.getDisplayName().equalsIgnoreCase("null")) ? GsonComponentSerializer.gson().deserialize(entry.getDisplayName()) : Component.empty()); + items.add(item); + } + packet = new LegacyPlayerListItem(UPDATE_DISPLAY_NAME, items); + sendPacket(packet); + } + } + } + + private final class PassThroughContentTabOverlay extends AbstractContentTabOverlay { + + } + + private final class PassThroughHeaderFooterHandler extends AbstractHeaderFooterOperationModeHandler { + + @Override + protected PassThroughHeaderFooterTabOverlay createTabOverlay() { + return new PassThroughHeaderFooterTabOverlay(); + } + + @Override + PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { + return PacketListenerResult.PASS; + } + + @Override + void onServerSwitch() { + sendPacket(new HeaderAndFooter(EMPTY_JSON_TEXT, EMPTY_JSON_TEXT)); + } + + @Override + void update() { + // nothing to do + } + + @Override + void onDeactivated() { + // nothing to do + } + + @Override + void onActivated(AbstractHeaderFooterOperationModeHandler previous) { + if (previous instanceof PassThroughHeaderFooterHandler) { + // we're lucky, nothing to do + return; + } + + // fix header/ footer + sendPacket(new HeaderAndFooter(serverHeader != null ? serverHeader : EMPTY_JSON_TEXT, serverFooter != null ? serverFooter : EMPTY_JSON_TEXT)); + } + } + + private final class PassThroughHeaderFooterTabOverlay extends AbstractHeaderFooterTabOverlay { + + } + + private abstract class CustomContentTabOverlayHandler extends AbstractContentOperationModeHandler { + + boolean viewerIsSpectator = false; + final ObjectSet freePlayers; + + @Nonnull + BitSet usedSlots; + BitSet dirtySlots; + int highestUsedSlotIndex; + private boolean using80Slots; + private int usedSlotsCount; + final SlotState[] slotState; + /** + * Uuid of the player list entry used for the slot. + */ + final UUID[] slotUuid; + /** + * Username of the player list entry used for the slot. + */ + final String[] slotUsername; + /** + * Player uuid mapped to the slot it is used for + */ + final Object2IntMap playerUuidToSlotMap; + /** + * Player username mapped to the slot it is used for + */ + final Object2IntMap playerUsernameToSlotMap; + boolean canShrink = false; + + private final List itemQueueAddPlayer; + private final List itemQueueRemovePlayer; + private final List itemQueueUpdateDisplayName; + private final List itemQueueUpdatePing; + + private final boolean experimentalTabCompleteFixForTabSize80 = isExperimentalTabCompleteFixForTabSize80(); + private final boolean experimentalTabCompleteSmileys = isExperimentalTabCompleteSmileys(); + + private CustomContentTabOverlayHandler() { + this.dirtySlots = new BitSet(80); + this.usedSlots = SIZE_TO_USED_SLOTS[0]; + this.usedSlotsCount = 0; + this.using80Slots = false; + this.slotState = new SlotState[80]; + Arrays.fill(this.slotState, SlotState.UNUSED); + this.slotUuid = new UUID[80]; + this.slotUsername = new String[80]; + this.highestUsedSlotIndex = -1; + this.freePlayers = new ObjectOpenHashSet<>(); + this.playerUuidToSlotMap = new Object2IntOpenHashMap<>(); + this.playerUuidToSlotMap.defaultReturnValue(-1); + this.playerUsernameToSlotMap = new Object2IntOpenHashMap<>(); + this.playerUsernameToSlotMap.defaultReturnValue(-1); + this.itemQueueAddPlayer = new ArrayList<>(80); + this.itemQueueRemovePlayer = new ArrayList<>(80); + this.itemQueueUpdateDisplayName = new ArrayList<>(80); + this.itemQueueUpdatePing = new ArrayList<>(80); + } + + @Override + PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { + + int action = packet.getAction(); + + if (using80Slots && action == UPDATE_GAMEMODE) { + return PacketListenerResult.PASS; + } + + // check whether viewer gamemode changed + boolean viewerGamemodeChanged = false; + T tabOverlay = getTabOverlay(); + if (action == ADD_PLAYER || action == UPDATE_GAMEMODE || action == REMOVE_PLAYER) { + PlayerListEntry entry = serverPlayerList.get(viewerUuid); + boolean viewerIsSpectator = entry != null && entry.getGamemode() == 3; + if (this.viewerIsSpectator != viewerIsSpectator) { + this.viewerIsSpectator = viewerIsSpectator; + if (!using80Slots) { + if (highestUsedSlotIndex >= 0) { + dirtySlots.set(highestUsedSlotIndex); + } + + if (viewerIsSpectator) { + // mark player slot as dirty + int i = playerUuidToSlotMap.getInt(viewerUuid); + if (i >= 0) { + dirtySlots.set(i); + } + } else { + // mark slots with player uuid as dirty + if (action == UPDATE_GAMEMODE) { + // if action is ADD_PLAYER slots are marked dirty below, so only do it here if action is UPDATE_GAMEMODE + for (int slot = 0; slot < 80; slot++) { + UUID uuid = tabOverlay.uuid[slot]; + if (viewerUuid.equals(uuid)) { + dirtySlots.set(slot); + } + } + } + } + } + viewerGamemodeChanged = true; + } + } + + PacketListenerResult result; + boolean needUpdate = !using80Slots && viewerGamemodeChanged; + + switch (action) { + case ADD_PLAYER: + List items = packet.getItems(); + if (!using80Slots) { + for (LegacyPlayerListItem.Item item : items) { + if (!viewerUuid.equals(item.getUuid())) { + item.setGameMode(0); + } + } + + for (LegacyPlayerListItem.Item item : items) { + UUID uuid = item.getUuid(); + int index = playerUuidToSlotMap.getInt(uuid); + if (index == -1) { + freePlayers.add(uuid); + needUpdate = true; + } else if (!item.getName().equals(slotUsername[index])) { + dirtySlots.set(index); + needUpdate = true; + } else { + item.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: check formatting + item.setLatency(tabOverlay.ping[index]); + tabOverlay.dirtyFlagsText.clear(index); + tabOverlay.dirtyFlagsPing.clear(index); + } + } + + // mark slot to use for player as dirty + // a new player joining the server shouldn't happen too frequently, so we can accept + // the cost of searching all 80 slots + if (!freePlayers.isEmpty()) { + for (int slot = 0; slot < 80; slot++) { + UUID uuid = tabOverlay.uuid[slot]; + if (uuid != null && freePlayers.contains(uuid)) { + dirtySlots.set(slot); + } + } + } + + // request size update if tab list too small + if (usedSlotsCount < serverPlayerList.size()) { + tabOverlay.dirtyFlagSize = true; + } + } else { + for (LegacyPlayerListItem.Item item : packet.getItems()) { + if (!playerToTeamMap.containsKey(item.getName())) { + sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[80], new String[]{item.getName()})); + } + } + } + if (needUpdate) { + sendPacket(packet); + result = PacketListenerResult.CANCEL; + } else { + result = PacketListenerResult.MODIFIED; + } + break; + case UPDATE_GAMEMODE: + if (viewerGamemodeChanged) { + items = packet.getItems(); + for (LegacyPlayerListItem.Item item : items) { + if (!viewerUuid.equals(item.getUuid())) { + item.setGameMode(0); + } + } + sendPacket(packet); + } + result = PacketListenerResult.CANCEL; + break; + case UPDATE_LATENCY: + result = PacketListenerResult.CANCEL; + break; + case UPDATE_DISPLAY_NAME: + result = PacketListenerResult.CANCEL; + break; + case REMOVE_PLAYER: + if (!using80Slots) { + items = packet.getItems(); + for (LegacyPlayerListItem.Item item : items) { + int index = playerUuidToSlotMap.removeInt(item.getUuid()); + if (index == -1) { + if (OPTION_ENABLE_CONSISTENCY_CHECKS) { + if (serverPlayerList.containsKey(item.getUuid())) { + logger.severe("Inconsistent data: player in serverPlayerList but not in playerUuidToSlotMap"); + } + } + } else { + // Switch slot 'index' from player to custom mode - restoring player teams + + // 1. remove player from team + if (item.getUuid().version() != 2) { // hack for Citizens compatibility + sendPacket(createPacketTeamRemovePlayers(CUSTOM_SLOT_TEAMNAME[index], new String[]{slotUsername[index]})); + playerUsernameToSlotMap.removeInt(slotUsername[index]); + String playerTeamName; + if ((playerTeamName = playerToTeamMap.get(slotUsername[index])) != null) { + // 2. add player to correct team + sendPacket(createPacketTeamAddPlayers(playerTeamName, new String[]{slotUsername[index]})); + // 3. reset custom slot team + if (is13OrLater) { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); + } else { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], "", "", "", "always", "always", 0, (byte) 1)); + } + } + } + + // 4. create new custom slot + tabOverlay.dirtyFlagsIcon.clear(index); + tabOverlay.dirtyFlagsText.clear(index); + tabOverlay.dirtyFlagsPing.clear(index); + Icon icon = tabOverlay.icon[index]; + UUID customSlotUuid; + if (icon.isAlex()) { + customSlotUuid = CUSTOM_SLOT_UUID_ALEX[index]; + } else { + customSlotUuid = CUSTOM_SLOT_UUID_STEVE[index]; + } + slotState[index] = SlotState.CUSTOM; + slotUuid[index] = customSlotUuid; + LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); + item1.setName(slotUsername[index] = getCustomSlotUsername(index)); + Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); + item1.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check formatting + item1.setLatency(tabOverlay.ping[index]); + item1.setGameMode(0); + LegacyPlayerListItem packet1 = new LegacyPlayerListItem(ADD_PLAYER, List.of(item1)); + sendPacket(packet1); + if (is18) { + packet1 = new LegacyPlayerListItem(UPDATE_DISPLAY_NAME, List.of(item1)); + sendPacket(packet1); + } + } + } + } + if (canShrink) { + sendPacket(packet); + tabOverlay.dirtyFlagSize = true; + needUpdate = true; + result = PacketListenerResult.CANCEL; + } else { + result = PacketListenerResult.PASS; + } + break; + default: + throw new AssertionError("Unknown action: " + action); + } + if (needUpdate) { + update(); + } + return result; + } + + private String getCustomSlotUsername(int index) { + if (experimentalTabCompleteFixForTabSize80 && using80Slots) { + return ""; + } + if (experimentalTabCompleteSmileys) { + return CUSTOM_SLOT_USERNAME_SMILEYS[index]; + } else { + return CUSTOM_SLOT_USERNAME[index]; + } + } + + @Override + void onTeamPacketPreprocess(Team packet) { + if (!using80Slots) { + if (packet.getMode() == 1) { + TeamEntry teamEntry = serverTeams.get(packet.getName()); + if (teamEntry != null) { + for (String playerName : teamEntry.getPlayers()) { + int slot = playerUsernameToSlotMap.getInt(playerName); + if (slot != -1) { + // reset slot team + if (is13OrLater) { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[slot], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); + } else { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[slot], "", "", "", "always", "always", 0, (byte) 1)); + } + } + } + } + } + } else { + if (packet.getMode() == 1) { + TeamEntry teamEntry = serverTeams.get(packet.getName()); + if (teamEntry != null) { + for (String playerName : teamEntry.getPlayers()) { + if (serverTabListPlayers.contains(playerName)) { + // reset slot team + sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[80], new String[]{playerName})); + } + } + } + } + } + } + + @Override + PacketListenerResult onTeamPacket(Team packet) { + if (CUSTOM_SLOT_TEAMNAMES.contains(packet.getName())) { + throw new AssertionError("Team name collision: " + packet); + } + if (!using80Slots) { + boolean modified = false; + switch (packet.getMode()) { + case 0: + case 3: + int count = 0; + String[] players = packet.getPlayers(); + for (int i = 0; i < players.length; i++) { + String playerName = players[i]; + if (-1 == playerUsernameToSlotMap.getInt(playerName)) { + count++; + } + } + if (count < players.length) { + modified = true; + String[] filteredPlayers = new String[count]; + int j = 0; + for (int i = 0; i < players.length; i++) { + String playerName = players[i]; + int slot; + if (-1 == (slot = playerUsernameToSlotMap.getInt(playerName))) { + filteredPlayers[j++] = playerName; + } else { + // update slot team + TeamEntry teamEntry = serverTeams.get(packet.getName()); + if (teamEntry != null) { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[slot], teamEntry.getDisplayName(), teamEntry.getPrefix(), teamEntry.getSuffix(), teamEntry.getNameTagVisibility(), teamEntry.getCollisionRule(), teamEntry.getColor(), teamEntry.getFriendlyFire())); + } + } + } + packet.setPlayers(filteredPlayers); + } + break; + case 4: + count = 0; + players = packet.getPlayers(); + for (int i = 0; i < players.length; i++) { + String playerName = players[i]; + if (-1 == playerUsernameToSlotMap.getInt(playerName)) { + count++; + } + } + if (count < players.length) { + modified = true; + String[] filteredPlayers = new String[count]; + int j = 0; + for (int i = 0; i < players.length; i++) { + String playerName = players[i]; + int slot; + if (-1 == (slot = playerUsernameToSlotMap.getInt(playerName))) { + filteredPlayers[j++] = playerName; + } else { + // reset slot team + if (is13OrLater) { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[slot], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); + } else { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[slot], "", "", "", "always", "always", 0, (byte) 1)); + } + } + } + packet.setPlayers(filteredPlayers); + } + break; + case 2: + TeamEntry teamEntry = serverTeams.get(packet.getName()); + if (teamEntry != null) { + for (String playerName : teamEntry.getPlayers()) { + int slot = playerUsernameToSlotMap.getInt(playerName); + if (slot != -1) { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[slot], teamEntry.getDisplayName(), teamEntry.getPrefix(), teamEntry.getSuffix(), teamEntry.getNameTagVisibility(), teamEntry.getCollisionRule(), teamEntry.getColor(), teamEntry.getFriendlyFire())); + } + } + } + + break; + + } + if (modified) { + return PacketListenerResult.MODIFIED; + } + } else { + + switch (packet.getMode()) { + case 0: + case 3: + /* + // Don't need this. Adding the player to another team will remove him from the current one. + String[] players = packet.getPlayers(); + for (int i = 0; i < players.length; i++) { + String playerName = players[i]; + if (serverTabListPlayers.contains(playerName)) { + // remove player from overflow team + sendPacket(createPacketTeamRemovePlayers(CUSTOM_SLOT_TEAMNAME[80], new String[]{playerName})); + } + }*/ + break; + case 4: + String[] players = packet.getPlayers(); + for (int i = 0; i < players.length; i++) { + String playerName = players[i]; + if (serverTabListPlayers.contains(playerName)) { + // add player to overflow team + sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[80], new String[]{playerName})); + } + } + break; + } + } + return PacketListenerResult.PASS; + } + + @Override + void onServerSwitch() { + boolean altRespawn = isUsingAltRespawn(); + + if (altRespawn) { + createTeamsIfNecessary(); + } + + if (!using80Slots) { + T tabOverlay = getTabOverlay(); + + // all players are gone + for (int index = 0; index < 80; index++) { + if (slotState[index] == SlotState.PLAYER) { + // Switch slot 'index' from player to custom mode + + if (!altRespawn) { + // 1. remove player from team + sendPacket(createPacketTeamRemovePlayers(CUSTOM_SLOT_TEAMNAME[index], new String[]{slotUsername[index]})); + // reset slot team + if (is13OrLater) { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); + } else { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], "", "", "", "always", "always", 0, (byte) 1)); + } + } + + // 2. create new custom slot + tabOverlay.dirtyFlagsIcon.clear(index); + tabOverlay.dirtyFlagsText.clear(index); + tabOverlay.dirtyFlagsPing.clear(index); + Icon icon = tabOverlay.icon[index]; + UUID customSlotUuid; + if (icon.isAlex()) { + customSlotUuid = CUSTOM_SLOT_UUID_ALEX[index]; + } else { + customSlotUuid = CUSTOM_SLOT_UUID_STEVE[index]; + } + slotState[index] = SlotState.CUSTOM; + slotUuid[index] = customSlotUuid; + LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); + item1.setName(slotUsername[index] = getCustomSlotUsername(index)); + Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); + item1.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check formatting + item1.setLatency(tabOverlay.ping[index]); + item1.setGameMode(0); + LegacyPlayerListItem packet1 = new LegacyPlayerListItem(ADD_PLAYER, List.of(item1)); + sendPacket(packet1); + if (is18) { + packet1 = new LegacyPlayerListItem(UPDATE_DISPLAY_NAME, List.of(item1)); + sendPacket(packet1); + } + } + } + freePlayers.clear(); + playerUuidToSlotMap.clear(); + playerUsernameToSlotMap.clear(); + } + viewerIsSpectator = false; + } + + @Override + void onActivated(AbstractContentOperationModeHandler previous) { + if (previous instanceof CustomContentTabOverlayHandler) { + this.viewerIsSpectator = ((CustomContentTabOverlayHandler) previous).viewerIsSpectator; + } else { + PlayerListEntry viewerEntry = serverPlayerList.get(viewerUuid); + this.viewerIsSpectator = viewerEntry != null && viewerEntry.getGamemode() == 3; + + // switch all players except for viewer in survival mode if they are in spectator mode + if (!using80Slots) { + int count = 0; + for (PlayerListEntry entry : serverPlayerList.values()) { + if (entry != viewerEntry && entry.getGamemode() == 3) { + count++; + } + } + + if (count > 0) { + LegacyPlayerListItem.Item[] items = new LegacyPlayerListItem.Item[count]; + int index = 0; + + for (Map.Entry mEntry : serverPlayerList.entrySet()) { + PlayerListEntry entry = mEntry.getValue(); + if (entry != viewerEntry && entry.getGamemode() == 3) { + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(mEntry.getKey()); + item.setGameMode(0); + items[index++] = item; + } + } + + LegacyPlayerListItem packet = new LegacyPlayerListItem(UPDATE_GAMEMODE, Arrays.asList(items)); + sendPacket(packet); + } + } + createTeamsIfNecessary(); + + } + + if (!using80Slots) { + this.freePlayers.addAll(serverPlayerList.keySet()); + + if (!this.freePlayers.isEmpty()) { + + for (PlayerListEntry entry : serverPlayerList.values()) { + if (!playerToTeamMap.containsKey(entry.username)) { + sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[80], new String[]{entry.username})); + } + } + getTabOverlay().dirtyFlagSize = true; + } + } + } + + private void createTeamsIfNecessary() { + // create teams if not already created + if (!hasCreatedCustomTeams) { + hasCreatedCustomTeams = true; + + if (is13OrLater) { + sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[0], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[0], CUSTOM_SLOT_USERNAME_SMILEYS[0], ""})); + } else { + sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[0], "", "", "", "always", "always", 0, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[0], CUSTOM_SLOT_USERNAME_SMILEYS[0], ""})); + } + + for (int i = 1; i < 80; i++) { + if (is13OrLater) { + sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[i], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[i], CUSTOM_SLOT_USERNAME_SMILEYS[i]})); + } else { + sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[i], "", "", "", "always", "always", 0, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[i], CUSTOM_SLOT_USERNAME_SMILEYS[i]})); + } + } + if (is13OrLater) { + sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[80], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[80]})); + } else { + sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[80], "", "", "", "always", "always", 0, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[80]})); + } + + areCustomSlotUsersPartOfTeams = true; + } + } + + @Override + void onDeactivated() { + int customSlots = 0; + for (int index = 0; index < 80; index++) { + if (slotState[index] != SlotState.UNUSED) { + if (slotState[index] == SlotState.PLAYER) { + // switch slot from player to unused permanently freeing the associated player + + // 1. remove player from team + sendPacket(createPacketTeamRemovePlayers(CUSTOM_SLOT_TEAMNAME[index], new String[]{slotUsername[index]})); + String playerTeamName; + if ((playerTeamName = playerToTeamMap.get(slotUsername[index])) != null) { + // 2. add player to correct team + sendPacket(createPacketTeamAddPlayers(playerTeamName, new String[]{slotUsername[index]})); + // 3. reset custom slot team + if (is13OrLater) { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); + } else { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], "", "", "", "always", "always", 0, (byte) 1)); + } + } + } else { + customSlots++; + } + } + } + + if (using80Slots) { + for (String player : serverTabListPlayers) { + if (playerToTeamMap.get(player) == null) { + // remove player from overflow team + sendPacket(createPacketTeamRemovePlayers(CUSTOM_SLOT_TEAMNAME[80], new String[]{player})); + } + } + // account for spacer players + if (experimentalTabCompleteFixForTabSize80) { + customSlots += 17; + } + } + + int i = 0; + if (customSlots > 0) { + LegacyPlayerListItem.Item[] items = new LegacyPlayerListItem.Item[customSlots]; + for (int index = 0; index < 80; index++) { + // switch slot from custom to unused + if (slotState[index] == SlotState.CUSTOM) { + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUuid[index]); + items[i++] = item; + } + } + if (experimentalTabCompleteFixForTabSize80 && using80Slots) { + for (int j = 0; j < 17; j++) { + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(CUSTOM_SLOT_UUID_SPACER[j]); + items[i++] = item; + } + } + LegacyPlayerListItem packet = new LegacyPlayerListItem(REMOVE_PLAYER, Arrays.asList(items)); + sendPacket(packet); + } + } + + @Override + void update() { + T tabOverlay = getTabOverlay(); + + if (OPTION_ENABLE_CONSISTENCY_CHECKS) { + if (!using80Slots && usedSlotsCount < serverPlayerList.size() && !tabOverlay.dirtyFlagSize) { + logger.severe("tabOverlay.dirtyFlagSize not set but resize required"); + tabOverlay.dirtyFlagSize = true; + } + } + + boolean updateAllCustomSlots = false; + + if (tabOverlay.dirtyFlagSize) { + tabOverlay.dirtyFlagSize = false; + updateSize(); + if (usedSlotsCount != (usedSlotsCount = usedSlots.cardinality())) { + + if (using80Slots) { + // when previously using 80 slots + for (int index = 0; index < 80; index++) { + if (tabOverlay.uuid[index] != null) { + dirtySlots.set(index); + } + } + freePlayers.addAll(serverPlayerList.keySet()); + + // switch all players except for viewer in survival mode if they are in spectator mode + PlayerListEntry viewerEntry = serverPlayerList.get(viewerUuid); + int count = 0; + for (PlayerListEntry entry : serverPlayerList.values()) { + if (entry != viewerEntry && entry.getGamemode() == 3) { + count++; + } + } + + if (count > 0) { + LegacyPlayerListItem.Item[] items = new LegacyPlayerListItem.Item[count]; + int index = 0; + + for (Map.Entry mEntry : serverPlayerList.entrySet()) { + PlayerListEntry entry = mEntry.getValue(); + if (entry != viewerEntry && entry.getGamemode() == 3) { + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(mEntry.getKey()); + item.setGameMode(0); + items[index++] = item; + } + } + + LegacyPlayerListItem packet = new LegacyPlayerListItem(UPDATE_GAMEMODE, Arrays.asList(items)); + sendPacket(packet); + } + + // remove spacer slots + if (experimentalTabCompleteFixForTabSize80) { + for (int i = 0; i < 17; i++) { + LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(CUSTOM_SLOT_UUID_SPACER[i]); + itemQueueRemovePlayer.add(item1); + } + } + + dirtySlots.set(0, 80); + updateAllCustomSlots = true; + } else if (viewerIsSpectator && highestUsedSlotIndex >= 0) { + dirtySlots.set(highestUsedSlotIndex); + } + + highestUsedSlotIndex = usedSlots.previousSetBit(79); + using80Slots = this.usedSlotsCount == 80; + if (using80Slots) { + // we switched to 80 slots + for (int index = 0; index < 80; index++) { + if (slotState[index] == SlotState.PLAYER) { + + // 1. remove player from team + sendPacket(createPacketTeamRemovePlayers(CUSTOM_SLOT_TEAMNAME[index], new String[]{slotUsername[index]})); + playerUsernameToSlotMap.removeInt(slotUsername[index]); + String playerTeamName; + if ((playerTeamName = playerToTeamMap.get(slotUsername[index])) != null) { + // 2. add player to correct team + sendPacket(createPacketTeamAddPlayers(playerTeamName, new String[]{slotUsername[index]})); + // 3. reset custom slot team + if (is13OrLater) { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); + } else { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], "", "", "", "always", "always", 0, (byte) 1)); + } + } else { + // 2. add player to overflow team + sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[80], new String[]{slotUsername[index]})); + } + + // 4. create new custom slot + tabOverlay.dirtyFlagsIcon.clear(index); + tabOverlay.dirtyFlagsText.clear(index); + tabOverlay.dirtyFlagsPing.clear(index); + Icon icon = tabOverlay.icon[index]; + UUID customSlotUuid; + if (icon.isAlex()) { + customSlotUuid = CUSTOM_SLOT_UUID_ALEX[index]; + } else { + customSlotUuid = CUSTOM_SLOT_UUID_STEVE[index]; + } + slotState[index] = SlotState.CUSTOM; + slotUuid[index] = customSlotUuid; + LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); + item1.setName(slotUsername[index] = getCustomSlotUsername(index)); + Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); + item1.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check Formatting + item1.setLatency(tabOverlay.ping[index]); + item1.setGameMode(0); + itemQueueAddPlayer.add(item1); + } else { + // custom + if (slotState[index] == SlotState.CUSTOM) { + LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(slotUuid[index]); + itemQueueRemovePlayer.add(item1); + } + // unused + tabOverlay.dirtyFlagsIcon.clear(index); + tabOverlay.dirtyFlagsText.clear(index); + tabOverlay.dirtyFlagsPing.clear(index); + Icon icon = tabOverlay.icon[index]; + UUID customSlotUuid; + if (icon.isAlex()) { + customSlotUuid = CUSTOM_SLOT_UUID_ALEX[index]; + } else { + customSlotUuid = CUSTOM_SLOT_UUID_STEVE[index]; + } + slotState[index] = SlotState.CUSTOM; + slotUuid[index] = customSlotUuid; + LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); + item1.setName(slotUsername[index] = getCustomSlotUsername(index)); + Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); + item1.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check Formatting + item1.setLatency(tabOverlay.ping[index]); + item1.setGameMode(0); + itemQueueAddPlayer.add(item1); + } + } + + // restore player gamemode + LegacyPlayerListItem packet; + List items = new ArrayList<>(serverPlayerList.size()); + items.clear(); + for (PlayerListEntry entry : serverPlayerList.values()) { + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(entry.getUuid()); + item.setGameMode(entry.getGamemode()); + items.add(item); + } + packet = new LegacyPlayerListItem(UPDATE_GAMEMODE, items); + sendPacket(packet); + + for (UUID player : freePlayers) { + String username = serverPlayerList.get(player).username; + String playerTeamName = playerToTeamMap.get(username); + if (playerTeamName != null) { + // add player to correct team + sendPacket(createPacketTeamAddPlayers(playerTeamName, new String[]{username})); + } else { + // add player to overflow team + sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[80], new String[]{username})); + } + } + + // create spacer slots + if (experimentalTabCompleteFixForTabSize80) { + for (int i = 0; i < 17; i++) { + LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(CUSTOM_SLOT_UUID_SPACER[i]); + item1.setName(""); + Property119Handler.setProperties(item1, EMPTY_PROPERTIES_ARRAY); + item1.setDisplayName(null); + item1.setLatency(0); + item1.setGameMode(0); + itemQueueAddPlayer.add(item1); + } + } + + // save some memory + freePlayers.clear(); + playerUuidToSlotMap.clear(); + playerUsernameToSlotMap.clear(); + } else { + + if (viewerIsSpectator && highestUsedSlotIndex >= 0) { + dirtySlots.set(highestUsedSlotIndex); + } + } + + sendQueuedItems(); + } + } + + + if (OPTION_ENABLE_CONSISTENCY_CHECKS) { + if (!using80Slots && usedSlotsCount < serverPlayerList.size()) { + throw new AssertionError("resize failed"); + } + } + + if (OPTION_ENABLE_CONSISTENCY_CHECKS) { + if (!using80Slots && freePlayers.size() + playerUuidToSlotMap.size() != serverPlayerList.size()) { + // inconsistent data -> rebuild + logger.severe("Detected inconsistency: freePlayers set or playerUuidToSlotMap is inconsistent"); + freePlayers.clear(); + freePlayers.addAll(serverPlayerList.keySet()); + playerUuidToSlotMap.clear(); + for (int index = 0; index <= highestUsedSlotIndex; index++) { + if (freePlayers.remove(slotUuid[index])) { + playerUuidToSlotMap.put(slotUuid[index], index); + } + } + } + } + + if (!using80Slots) { + dirtySlots.orAndClear(tabOverlay.dirtyFlagsUuid); + + if (!dirtySlots.isEmpty() || !freePlayers.isEmpty()) { + // mark slots as dirty currently being used with the uuid of dirty slots + for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { + int i = index; + do { + UUID uuid = tabOverlay.uuid[i]; + if (uuid == null) { + break; + } + i = playerUuidToSlotMap.getInt(uuid); + if (i == -1) { + break; + } + if (dirtySlots.get(i)) { + break; + } else { + dirtySlots.set(i); + } + } while (i < index); + } + + if (OPTION_ENABLE_CONSISTENCY_CHECKS) { + if (viewerIsSpectator) { + int i; + if (highestUsedSlotIndex != (i = playerUuidToSlotMap.getInt(viewerUuid))) { + if (!dirtySlots.get(highestUsedSlotIndex)) { + logger.severe("Spectator mode handling issue: highestUsedSlotIndex not marked as dirty"); + dirtySlots.set(highestUsedSlotIndex); + } else if (i == -1 && !freePlayers.contains(viewerUuid)) { + logger.severe("Spectator mode handling issue: viewer neither in freePlayers set and nor in tab list"); + } else if (i != -1 && !dirtySlots.get(i)) { + logger.severe("Spectator mode handling issue: viewer slot not marked as dirty"); + dirtySlots.set(i); + } + } + } + } + + // pass 1: free players + for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { + if (usedSlots.get(index)) { + if (slotState[index] == SlotState.PLAYER) { + // temporarily free associated player - set slot state to unused + + // 1. remove player from team + if (slotUuid[index].version() != 2) { // dirty hack for Citizens compatibility + sendPacket(createPacketTeamRemovePlayers(CUSTOM_SLOT_TEAMNAME[index], new String[]{slotUsername[index]})); + playerUsernameToSlotMap.removeInt(slotUsername[index]); + + // reset slot team + if (is13OrLater) { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); + } else { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], "", "", "", "always", "always", 0, (byte) 1)); + } + } + + // 2. update slot state + slotState[index] = SlotState.UNUSED; + freePlayers.add(slotUuid[index]); + slotUuid[index] = null; + slotUsername[index] = null; + } + } else { + if (slotState[index] != SlotState.UNUSED) { + // remove slot - free (temporarily) associated player if any - set slot state to unused + if (slotState[index] == SlotState.PLAYER) { + // temporarily free associated player - set slot state to unused + + // 1. remove player from team + if (slotUuid[index].version() != 2) { // dirty hack for Citizens compatibility + sendPacket(createPacketTeamRemovePlayers(CUSTOM_SLOT_TEAMNAME[index], new String[]{slotUsername[index]})); + playerUsernameToSlotMap.removeInt(slotUsername[index]); + + // reset slot team + if (is13OrLater) { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); + } else { + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], "", "", "", "always", "always", 0, (byte) 1)); + } + } + + freePlayers.add(slotUuid[index]); + } else { + // 1. remove custom slot player + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUuid[index]); + itemQueueRemovePlayer.add(item); + } + + // 2. update slot state + slotState[index] = SlotState.UNUSED; + slotUuid[index] = null; + slotUsername[index] = null; + } + } + } + + if (viewerIsSpectator && !viewerUuid.equals(slotUuid[highestUsedSlotIndex])) { + if (slotState[highestUsedSlotIndex] != SlotState.UNUSED) { + if (OPTION_ENABLE_CONSISTENCY_CHECKS) { + if (slotState[highestUsedSlotIndex] == SlotState.PLAYER) { + throw new AssertionError("slotState[highestUsedSlotIndex] == SlotState.PLAYER"); + } + } + // switch slot 'highestUsedSlotIndex' from custom to unused + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUuid[highestUsedSlotIndex]); + itemQueueRemovePlayer.add(item); + } + // switch slot 'highestUsedSlotIndex' from unused to player with 'viewerUuid' + String playerUsername = serverPlayerList.get(viewerUuid).getUsername(); + String playerTeamName; + if (null != (playerTeamName = playerToTeamMap.get(playerUsername))) { + // 1. remove player from old team + sendPacket(createPacketTeamRemovePlayers(playerTeamName, new String[]{playerUsername})); + // 2. update properties of new team + TeamEntry teamEntry = serverTeams.get(playerTeamName); + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[highestUsedSlotIndex], teamEntry.getDisplayName(), teamEntry.getPrefix(), teamEntry.getSuffix(), teamEntry.getNameTagVisibility(), teamEntry.getCollisionRule(), teamEntry.getColor(), teamEntry.getFriendlyFire())); + } + // 3. Add to new team + sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[highestUsedSlotIndex], new String[]{playerUsername})); + // 4. Update display name + LegacyPlayerListItem.Item itemUpdateDisplayName = new LegacyPlayerListItem.Item(viewerUuid); + tabOverlay.dirtyFlagsText.clear(highestUsedSlotIndex); + itemUpdateDisplayName.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[highestUsedSlotIndex])); // TODO: Check Formatting + itemQueueUpdateDisplayName.add(itemUpdateDisplayName); + // 5. Update ping + LegacyPlayerListItem.Item itemUpdatePing = new LegacyPlayerListItem.Item(viewerUuid); + tabOverlay.dirtyFlagsPing.clear(highestUsedSlotIndex); + itemUpdatePing.setLatency(tabOverlay.ping[highestUsedSlotIndex]); + itemQueueUpdatePing.add(itemUpdatePing); + // 6. Update slot state + slotState[highestUsedSlotIndex] = SlotState.PLAYER; + slotUuid[highestUsedSlotIndex] = viewerUuid; + slotUsername[highestUsedSlotIndex] = playerUsername; + playerUsernameToSlotMap.put(playerUsername, highestUsedSlotIndex); + playerUuidToSlotMap.put(viewerUuid, highestUsedSlotIndex); + + freePlayers.remove(viewerUuid); + } + + // pass 2: assign players to new slots + for (int repeat = 1; repeat > 0; repeat--) { + for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { + if (usedSlots.get(index) && slotState[index] != SlotState.PLAYER) { + UUID uuid = tabOverlay.uuid[index]; + if (uuid != null && freePlayers.remove(uuid)) { + // switch slot to player mode using player with 'uuid' + if (slotState[index] == SlotState.CUSTOM) { + // custom -> unused + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUuid[index]); + itemQueueRemovePlayer.add(item); + } + String playerUsername = serverPlayerList.get(uuid).getUsername(); + String playerTeamName; + if (null != (playerTeamName = playerToTeamMap.get(playerUsername))) { + // 1. remove player from old team + sendPacket(createPacketTeamRemovePlayers(playerTeamName, new String[]{playerUsername})); + // 2. update properties of new team + TeamEntry teamEntry = serverTeams.get(playerTeamName); + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], teamEntry.getDisplayName(), teamEntry.getPrefix(), teamEntry.getSuffix(), teamEntry.getNameTagVisibility(), teamEntry.getCollisionRule(), teamEntry.getColor(), teamEntry.getFriendlyFire())); + } + // 3. Add to new team + sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[index], new String[]{playerUsername})); + // 4. Update display name + LegacyPlayerListItem.Item itemUpdateDisplayName = new LegacyPlayerListItem.Item(uuid); + tabOverlay.dirtyFlagsText.clear(index); + itemUpdateDisplayName.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check Formatting + itemQueueUpdateDisplayName.add(itemUpdateDisplayName); + // 5. Update ping + LegacyPlayerListItem.Item itemUpdatePing = new LegacyPlayerListItem.Item(uuid); + tabOverlay.dirtyFlagsPing.clear(index); + itemUpdatePing.setLatency(tabOverlay.ping[index]); + itemQueueUpdatePing.add(itemUpdatePing); + // 6. Update slot state + slotState[index] = SlotState.PLAYER; + slotUuid[index] = uuid; + slotUsername[index] = playerUsername; + playerUsernameToSlotMap.put(playerUsername, index); + playerUuidToSlotMap.put(uuid, index); + } + } + } + + // should not happen too often + if (!freePlayers.isEmpty()) { + for (int slot = 0; slot < 80; slot++) { + UUID uuid; + if (slotState[slot] == SlotState.CUSTOM && (uuid = tabOverlay.uuid[slot]) != null && freePlayers.contains(uuid)) { + dirtySlots.set(slot); + repeat = 2; + } + } + } + } + + // pass 3: distribute remaining 'freePlayers' on the tab list + int index = 80; + for (ObjectIterator iterator = freePlayers.iterator(); iterator.hasNext(); ) { + UUID uuid = iterator.next(); + for (index = usedSlots.previousSetBit(index - 1); index >= 0; index = usedSlots.previousSetBit(index - 1)) { + if (slotState[index] != SlotState.PLAYER) { + // switch slot to player mode using the player 'uuid' + if (slotState[index] == SlotState.CUSTOM) { + // custom -> unused + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUuid[index]); + itemQueueRemovePlayer.add(item); + } + String playerUsername = serverPlayerList.get(uuid).getUsername(); + if (uuid.version() != 2) { // dirty hack for Citizens compatibility + String playerTeamName; + if (null != (playerTeamName = playerToTeamMap.get(playerUsername))) { + // 1. remove player from old team + sendPacket(createPacketTeamRemovePlayers(playerTeamName, new String[]{playerUsername})); + // 2. update properties of new team + TeamEntry teamEntry = serverTeams.get(playerTeamName); + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], teamEntry.getDisplayName(), teamEntry.getPrefix(), teamEntry.getSuffix(), teamEntry.getNameTagVisibility(), teamEntry.getCollisionRule(), teamEntry.getColor(), teamEntry.getFriendlyFire())); + } + // 3. Add to new team + sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[index], new String[]{playerUsername})); + playerUsernameToSlotMap.put(playerUsername, index); + } + // 4. Update display name + LegacyPlayerListItem.Item itemUpdateDisplayName = new LegacyPlayerListItem.Item(uuid); + tabOverlay.dirtyFlagsText.clear(index); + itemUpdateDisplayName.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check Formatting + itemQueueUpdateDisplayName.add(itemUpdateDisplayName); + // 5. Update ping + LegacyPlayerListItem.Item itemUpdatePing = new LegacyPlayerListItem.Item(uuid); + tabOverlay.dirtyFlagsPing.clear(index); + itemUpdatePing.setLatency(tabOverlay.ping[index]); + itemQueueUpdatePing.add(itemUpdatePing); + // 6. Update slot state + slotState[index] = SlotState.PLAYER; + slotUuid[index] = uuid; + slotUsername[index] = playerUsername; + playerUuidToSlotMap.put(uuid, index); + iterator.remove(); + break; + } + } + if (index < 0) { + throw new AssertionError("Not enough space on player list."); + } + } + + // pass 4: switch some slots from unused to custom + for (index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { + if (usedSlots.get(index)) { + if (slotState[index] == SlotState.UNUSED || (updateAllCustomSlots && slotState[index] == SlotState.CUSTOM)) { + if (slotState[index] == SlotState.CUSTOM) { + LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(slotUuid[index]); + itemQueueRemovePlayer.add(item1); + } + tabOverlay.dirtyFlagsIcon.clear(index); + tabOverlay.dirtyFlagsText.clear(index); + tabOverlay.dirtyFlagsPing.clear(index); + Icon icon = tabOverlay.icon[index]; + UUID customSlotUuid; + if (icon.isAlex()) { + customSlotUuid = CUSTOM_SLOT_UUID_ALEX[index]; + } else { + customSlotUuid = CUSTOM_SLOT_UUID_STEVE[index]; + } + slotState[index] = SlotState.CUSTOM; + slotUuid[index] = customSlotUuid; + LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); + item1.setName(slotUsername[index] = getCustomSlotUsername(index)); + Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); + item1.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check Formatting + item1.setLatency(tabOverlay.ping[index]); + item1.setGameMode(0); + itemQueueAddPlayer.add(item1); + } + } + } + + // send first packet batch here to avoid conflicts when updating icons + sendQueuedItems(); + } + } + + // update icons + dirtySlots.copyAndClear(tabOverlay.dirtyFlagsIcon); + for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { + if (slotState[index] == SlotState.CUSTOM) { + Icon icon = tabOverlay.icon[index]; + UUID customSlotUuid; + if (icon.hasTextureProperty()) { + customSlotUuid = slotUuid[index]; + } else if (icon.isAlex()) { + customSlotUuid = CUSTOM_SLOT_UUID_ALEX[index]; + } else { // steve + customSlotUuid = CUSTOM_SLOT_UUID_STEVE[index]; + } + if (!customSlotUuid.equals(slotUuid[index]) || is119OrLater) { + LegacyPlayerListItem.Item itemRemove = new LegacyPlayerListItem.Item(slotUuid[index]); + itemQueueRemovePlayer.add(itemRemove); + } + tabOverlay.dirtyFlagsText.clear(index); + tabOverlay.dirtyFlagsPing.clear(index); + slotState[index] = SlotState.CUSTOM; + slotUuid[index] = customSlotUuid; + LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); + item1.setName(slotUsername[index] = getCustomSlotUsername(index)); + Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); + item1.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check Formatting + item1.setLatency(tabOverlay.ping[index]); + item1.setGameMode(0); + itemQueueAddPlayer.add(item1); + } + } + + // update text + dirtySlots.copyAndClear(tabOverlay.dirtyFlagsText); + for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { + if (slotState[index] != SlotState.UNUSED) { + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUuid[index]); + item.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check Formatting + itemQueueUpdateDisplayName.add(item); + } + } + + // update ping + dirtySlots.copyAndClear(tabOverlay.dirtyFlagsPing); + for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { + if (slotState[index] != SlotState.UNUSED) { + LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUuid[index]); + item.setLatency(tabOverlay.ping[index]); + itemQueueUpdatePing.add(item); + } + } + + dirtySlots.clear(); + + // send packets + sendQueuedItems(); + } + + private void sendQueuedItems() { + if (!itemQueueRemovePlayer.isEmpty()) { + LegacyPlayerListItem packet = new LegacyPlayerListItem(REMOVE_PLAYER, itemQueueRemovePlayer); + sendPacket(packet); + itemQueueRemovePlayer.clear(); + } + if (!itemQueueAddPlayer.isEmpty()) { + LegacyPlayerListItem packet = new LegacyPlayerListItem(ADD_PLAYER, itemQueueAddPlayer); + sendPacket(packet); + if (is18) { + packet = new LegacyPlayerListItem(UPDATE_DISPLAY_NAME, itemQueueAddPlayer); + sendPacket(packet); + } + itemQueueAddPlayer.clear(); + } + if (!itemQueueUpdateDisplayName.isEmpty()) { + LegacyPlayerListItem packet = new LegacyPlayerListItem(UPDATE_DISPLAY_NAME, itemQueueUpdateDisplayName); + sendPacket(packet); + itemQueueUpdateDisplayName.clear(); + } + if (!itemQueueUpdatePing.isEmpty()) { + LegacyPlayerListItem packet = new LegacyPlayerListItem(UPDATE_LATENCY, itemQueueUpdatePing); + sendPacket(packet); + itemQueueUpdatePing.clear(); + } + } + + /** + * Updates the usedSlots BitSet. Sets the {@link #dirtySlots uuid dirty flag} for all added + * and removed slots. + */ + abstract void updateSize(); + } + + protected abstract boolean isExperimentalTabCompleteSmileys(); + + protected abstract boolean isExperimentalTabCompleteFixForTabSize80(); + + private abstract class CustomContentTabOverlay extends AbstractContentTabOverlay implements TabOverlayHandle.BatchModifiable { + final UUID[] uuid; + final Icon[] icon; + final String[] text; + final int[] ping; + + final AtomicInteger batchUpdateRecursionLevel; + volatile boolean dirtyFlagSize; + final ConcurrentBitSet dirtyFlagsUuid; + final ConcurrentBitSet dirtyFlagsIcon; + final ConcurrentBitSet dirtyFlagsText; + final ConcurrentBitSet dirtyFlagsPing; + + private CustomContentTabOverlay() { + this.uuid = new UUID[80]; + this.icon = new Icon[80]; + Arrays.fill(this.icon, Icon.DEFAULT_STEVE); + this.text = new String[80]; + Arrays.fill(this.text, EMPTY_JSON_TEXT); + this.ping = new int[80]; + this.batchUpdateRecursionLevel = new AtomicInteger(0); + this.dirtyFlagSize = true; + this.dirtyFlagsUuid = new ConcurrentBitSet(80); + this.dirtyFlagsIcon = new ConcurrentBitSet(80); + this.dirtyFlagsText = new ConcurrentBitSet(80); + this.dirtyFlagsPing = new ConcurrentBitSet(80); + } + + @Override + public void beginBatchModification() { + if (isValid()) { + if (batchUpdateRecursionLevel.incrementAndGet() < 0) { + throw new AssertionError("Recursion level overflow"); + } + } + } + + @Override + public void completeBatchModification() { + if (isValid()) { + int level = batchUpdateRecursionLevel.decrementAndGet(); + if (level == 0) { + scheduleUpdate(); + } else if (level < 0) { + throw new AssertionError("Recursion level underflow"); + } + } + } + + void scheduleUpdateIfNotInBatch() { + if (batchUpdateRecursionLevel.get() == 0) { + scheduleUpdate(); + } + } + + void setUuidInternal(int index, @Nullable UUID uuid) { + if (!Objects.equals(uuid, this.uuid[index])) { + this.uuid[index] = uuid; + dirtyFlagsUuid.set(index); + scheduleUpdateIfNotInBatch(); + } + } + + void setIconInternal(int index, @Nonnull @NonNull Icon icon) { + if (!icon.equals(this.icon[index])) { + this.icon[index] = icon; + dirtyFlagsIcon.set(index); + scheduleUpdateIfNotInBatch(); + } + } + + void setTextInternal(int index, @Nonnull @NonNull String text) { + String jsonText = ChatFormat.formattedTextToJson(text); + if (!jsonText.equals(this.text[index])) { + this.text[index] = jsonText; + dirtyFlagsText.set(index); + scheduleUpdateIfNotInBatch(); + } + } + + void setPingInternal(int index, int ping) { + if (ping != this.ping[index]) { + this.ping[index] = ping; + dirtyFlagsPing.set(index); + scheduleUpdateIfNotInBatch(); + } + } + } + + private class RectangularSizeHandler extends CustomContentTabOverlayHandler { + + @Override + void updateSize() { + RectangularTabOverlayImpl tabOverlay = getTabOverlay(); + RectangularTabOverlay.Dimension size = tabOverlay.getSize(); + if (size.getSize() < serverPlayerList.size() && size.getSize() != 80) { + for (RectangularTabOverlay.Dimension dimension : tabOverlay.getSupportedSizes()) { + if (dimension.getColumns() < tabOverlay.getSize().getColumns()) + continue; + if (dimension.getRows() < tabOverlay.getSize().getRows()) + continue; + if (size.getSize() < serverPlayerList.size() && size.getSize() != 80) { + size = dimension; + } else if (size.getSize() > dimension.getSize() && dimension.getSize() > serverPlayerList.size()) { + size = dimension; + } + } + canShrink = true; + } else { + canShrink = false; + } + BitSet newUsedSlots = DIMENSION_TO_USED_SLOTS.get(size); + dirtySlots.orXor(usedSlots, newUsedSlots); + usedSlots = newUsedSlots; + } + + @Override + protected RectangularTabOverlayImpl createTabOverlay() { + return new RectangularTabOverlayImpl(); + } + } + + private class RectangularTabOverlayImpl extends CustomContentTabOverlay implements RectangularTabOverlay { + + @Nonnull + private Dimension size; + + private RectangularTabOverlayImpl() { + Optional dimensionZero = getSupportedSizes().stream().filter(size -> size.getSize() == 0).findAny(); + if (!dimensionZero.isPresent()) { + throw new AssertionError(); + } + this.size = dimensionZero.get(); + } + + @Nonnull + @Override + public Dimension getSize() { + return size; + } + + @Override + public Collection getSupportedSizes() { + return DIMENSION_TO_USED_SLOTS.keySet(); + } + + @Override + public void setSize(@Nonnull Dimension size) { + if (!getSupportedSizes().contains(size)) { + throw new IllegalArgumentException("Unsupported size " + size); + } + if (isValid() && !this.size.equals(size)) { + BitSet oldUsedSlots = DIMENSION_TO_USED_SLOTS.get(this.size); + BitSet newUsedSlots = DIMENSION_TO_USED_SLOTS.get(size); + for (int index = newUsedSlots.nextSetBit(0); index >= 0; index = newUsedSlots.nextSetBit(index + 1)) { + if (!oldUsedSlots.get(index)) { + uuid[index] = null; + icon[index] = Icon.DEFAULT_STEVE; + text[index] = EMPTY_JSON_TEXT; + ping[index] = 0; + } + } + this.size = size; + this.dirtyFlagSize = true; + scheduleUpdateIfNotInBatch(); + for (int index = oldUsedSlots.nextSetBit(0); index >= 0; index = oldUsedSlots.nextSetBit(index + 1)) { + if (!newUsedSlots.get(index)) { + uuid[index] = null; + icon[index] = Icon.DEFAULT_STEVE; + text[index] = EMPTY_JSON_TEXT; + ping[index] = 0; + } + } + } + } + + @Override + public void setSlot(int column, int row, @Nullable UUID uuid, @Nonnull Icon icon, @Nonnull String text, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(column, size.getColumns(), "column"); + Preconditions.checkElementIndex(row, size.getRows(), "row"); + beginBatchModification(); + try { + int index = index(column, row); + setUuidInternal(index, uuid); + setIconInternal(index, icon); + setTextInternal(index, text); + setPingInternal(index, ping); + } finally { + completeBatchModification(); + } + } + } + + @Override + public void setSlot(int column, int row, @Nonnull Icon icon, @Nonnull String text, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(column, size.getColumns(), "column"); + Preconditions.checkElementIndex(row, size.getRows(), "row"); + beginBatchModification(); + try { + int index = index(column, row); + setUuidInternal(index, null); + setIconInternal(index, icon); + setTextInternal(index, text); + setPingInternal(index, ping); + } finally { + completeBatchModification(); + } + } + } + + @Override + public void setUuid(int column, int row, @Nullable UUID uuid) { + if (isValid()) { + Preconditions.checkElementIndex(column, size.getColumns(), "column"); + Preconditions.checkElementIndex(row, size.getRows(), "row"); + setUuidInternal(index(column, row), uuid); + } + } + + @Override + public void setIcon(int column, int row, @Nonnull Icon icon) { + if (isValid()) { + Preconditions.checkElementIndex(column, size.getColumns(), "column"); + Preconditions.checkElementIndex(row, size.getRows(), "row"); + setIconInternal(index(column, row), icon); + } + } + + @Override + public void setText(int column, int row, @Nonnull String text) { + if (isValid()) { + Preconditions.checkElementIndex(column, size.getColumns(), "column"); + Preconditions.checkElementIndex(row, size.getRows(), "row"); + setTextInternal(index(column, row), text); + } + } + + @Override + public void setPing(int column, int row, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(column, size.getColumns(), "column"); + Preconditions.checkElementIndex(row, size.getRows(), "row"); + setPingInternal(index(column, row), ping); + } + } + } + + private class SimpleOperationModeHandler extends CustomContentTabOverlayHandler { + + @Override + void updateSize() { + int newSize = getTabOverlay().size; + if (newSize != 80 && newSize < serverPlayerList.size()) { + newSize = Integer.min(serverPlayerList.size(), 80); + canShrink = true; + } else { + canShrink = false; + } + if (newSize > highestUsedSlotIndex + 1) { + dirtySlots.set(highestUsedSlotIndex + 1, newSize); + } else if (newSize <= highestUsedSlotIndex) { + dirtySlots.set(newSize, highestUsedSlotIndex + 1); + } + usedSlots = SIZE_TO_USED_SLOTS[newSize]; + } + + @Override + protected SimpleTabOverlayImpl createTabOverlay() { + return new SimpleTabOverlayImpl(); + } + } + + private class SimpleTabOverlayImpl extends CustomContentTabOverlay implements SimpleTabOverlay { + int size = 0; + + @Override + public int getSize() { + return size; + } + + @Override + public int getMaxSize() { + return 80; + } + + @Override + public void setSize(int size) { + if (size < 0 || size > 80) { + throw new IllegalArgumentException("size"); + } + this.size = size; + dirtyFlagSize = true; + scheduleUpdateIfNotInBatch(); + } + + @Override + public void setSlot(int index, @Nullable UUID uuid, @Nonnull Icon icon, @Nonnull String text, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(index, size, "index"); + beginBatchModification(); + try { + setUuidInternal(index, uuid); + setIconInternal(index, icon); + setTextInternal(index, text); + setPingInternal(index, ping); + } finally { + completeBatchModification(); + } + } + } + + @Override + public void setSlot(int index, @Nonnull Icon icon, @Nonnull String text, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(index, size, "index"); + beginBatchModification(); + try { + setUuidInternal(index, null); + setIconInternal(index, icon); + setTextInternal(index, text); + setPingInternal(index, ping); + } finally { + completeBatchModification(); + } + } + } + + @Override + public void setUuid(int index, UUID uuid) { + if (isValid()) { + Preconditions.checkElementIndex(index, size, "index"); + setUuidInternal(index, uuid); + } + } + + @Override + public void setIcon(int index, @Nonnull @NonNull Icon icon) { + if (isValid()) { + Preconditions.checkElementIndex(index, size, "index"); + setIconInternal(index, icon); + } + } + + @Override + public void setText(int index, @Nonnull @NonNull String text) { + if (isValid()) { + Preconditions.checkElementIndex(index, size, "index"); + setTextInternal(index, text); + } + } + + @Override + public void setPing(int index, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(index, size, "index"); + setPingInternal(index, ping); + } + } + } + + private final class CustomHeaderAndFooterOperationModeHandler extends AbstractHeaderFooterOperationModeHandler { + + @Override + protected CustomHeaderAndFooterImpl createTabOverlay() { + return new CustomHeaderAndFooterImpl(); + } + + @Override + PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { + return PacketListenerResult.CANCEL; + } + + @Override + void onServerSwitch() { + // do nothing + } + + @Override + void onDeactivated() { + //do nothing + } + + @Override + void onActivated(AbstractHeaderFooterOperationModeHandler previous) { + // remove header/ footer + sendPacket(new HeaderAndFooter(EMPTY_JSON_TEXT, EMPTY_JSON_TEXT)); + } + + @Override + void update() { + CustomHeaderAndFooterImpl tabOverlay = getTabOverlay(); + if (tabOverlay.headerOrFooterDirty) { + tabOverlay.headerOrFooterDirty = false; + sendPacket(new HeaderAndFooter(tabOverlay.header, tabOverlay.footer)); + } + } + } + + private final class CustomHeaderAndFooterImpl extends AbstractHeaderFooterTabOverlay implements HeaderAndFooterHandle { + private String header = EMPTY_JSON_TEXT; + private String footer = EMPTY_JSON_TEXT; + + private volatile boolean headerOrFooterDirty = false; + + final AtomicInteger batchUpdateRecursionLevel = new AtomicInteger(0); + + @Override + public void beginBatchModification() { + if (isValid()) { + if (batchUpdateRecursionLevel.incrementAndGet() < 0) { + throw new AssertionError("Recursion level overflow"); + } + } + } + + @Override + public void completeBatchModification() { + if (isValid()) { + int level = batchUpdateRecursionLevel.decrementAndGet(); + if (level == 0) { + scheduleUpdate(); + } else if (level < 0) { + throw new AssertionError("Recursion level underflow"); + } + } + } + + void scheduleUpdateIfNotInBatch() { + if (batchUpdateRecursionLevel.get() == 0) { + scheduleUpdate(); + } + } + + @Override + public void setHeaderFooter(@Nullable String header, @Nullable String footer) { + this.header = ChatFormat.formattedTextToJson(header); + this.footer = ChatFormat.formattedTextToJson(footer); + headerOrFooterDirty = true; + scheduleUpdateIfNotInBatch(); + } + + @Override + public void setHeader(@Nullable String header) { + this.header = ChatFormat.formattedTextToJson(header); + headerOrFooterDirty = true; + scheduleUpdateIfNotInBatch(); + } + + @Override + public void setFooter(@Nullable String footer) { + this.footer = ChatFormat.formattedTextToJson(footer); + headerOrFooterDirty = true; + scheduleUpdateIfNotInBatch(); + } + } + + private static int index(int column, int row) { + return column * 20 + row; + } + + private static String[][] toPropertiesArray(ProfileProperty textureProperty) { + if (textureProperty == null) { + return EMPTY_PROPERTIES_ARRAY; + } else if (textureProperty.isSigned()) { + return new String[][]{{textureProperty.getName(), textureProperty.getValue(), textureProperty.getSignature()}}; + } else { + // todo maybe add warning on unsigned properties? + return new String[][]{{textureProperty.getName(), textureProperty.getValue()}}; + } + } + + private static Team createPacketTeamCreate(String name, String displayName, String prefix, String suffix, String nameTagVisibility, String collisionRule, int color, byte friendlyFire, String[] players) { + Team team = new Team(); + team.setName(name); + team.setMode((byte) 0); + team.setDisplayName(displayName); + team.setPrefix(prefix); + team.setSuffix(suffix); + team.setNameTagVisibility(nameTagVisibility); + if (TEAM_COLLISION_RULE_SUPPORTED) { + team.setCollisionRule(collisionRule); + } + team.setColor(color); + team.setFriendlyFire(friendlyFire); + team.setPlayers(players); + return team; + } + + private static Team createPacketTeamRemove(String name) { + Team team = new Team(); + team.setName(name); + team.setMode((byte) 1); + return team; + } + + private static Team createPacketTeamUpdate(String name, String displayName, String prefix, String suffix, String nameTagVisibility, String collisionRule, int color, byte friendlyFire) { + Team team = new Team(); + team.setName(name); + team.setMode((byte) 2); + team.setDisplayName(displayName); + team.setPrefix(prefix); + team.setSuffix(suffix); + team.setNameTagVisibility(nameTagVisibility); + if (TEAM_COLLISION_RULE_SUPPORTED) { + team.setCollisionRule(collisionRule); + } + team.setColor(color); + team.setFriendlyFire(friendlyFire); + return team; + } + + private static Team createPacketTeamAddPlayers(String name, String[] players) { + Team team = new Team(); + team.setName(name); + team.setMode((byte) 3); + team.setPlayers(players); + return team; + } + + private static Team createPacketTeamRemovePlayers(String name, String[] players) { + Team team = new Team(); + team.setName(name); + team.setMode((byte) 4); + team.setPlayers(players); + return team; + } + + private enum SlotState { + UNUSED, CUSTOM, PLAYER + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + static class PlayerListEntry { + private UUID uuid; + private String[][] properties; + private String username; + private String displayName; + private int ping; + private int gamemode; + + private PlayerListEntry(LegacyPlayerListItem.Item item) { + this(item.getUuid(), null, item.getName(), GsonComponentSerializer.gson().serialize(item.getDisplayName()), item.getLatency(), item.getGameMode()); // TODO: Check Display Name + properties = Property119Handler.getProperties(item); + } + } + + @Data + static class TeamEntry { + private String displayName; + private String prefix; + private String suffix; + private byte friendlyFire; + private String nameTagVisibility; + private String collisionRule; + private int color; + private Set players = new ObjectOpenHashSet<>(); + + void addPlayer(String name) { + players.add(name); + } + + void removePlayer(String name) { + players.remove(name); + } + + public void setNameTagVisibility(String nameTagVisibility) { + this.nameTagVisibility = nameTagVisibility.intern(); + } + + public void setCollisionRule(String collisionRule) { + this.collisionRule = collisionRule == null ? null : collisionRule.intern(); + } + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LegacyTabOverlayHandlerImpl.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LegacyTabOverlayHandlerImpl.java index 2d6244a8..e7511626 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LegacyTabOverlayHandlerImpl.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LegacyTabOverlayHandlerImpl.java @@ -1,44 +1,53 @@ -/* - * Copyright (C) 2020 Florian Stober - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package codecrafter47.bungeetablistplus.handler; - -import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; - -import codecrafter47.bungeetablistplus.util.ReflectionUtil; -import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.proxy.protocol.MinecraftPacket; -import lombok.SneakyThrows; - -import java.util.concurrent.Executor; -import java.util.logging.Logger; - -public class LegacyTabOverlayHandlerImpl extends AbstractLegacyTabOverlayHandler { - - private final Player player; - - public LegacyTabOverlayHandlerImpl(Logger logger, int playerListSize, Executor eventLoopExecutor, Player player, boolean is13OrLater) { - super(logger, playerListSize, eventLoopExecutor, is13OrLater); - this.player = player; - } - - @SneakyThrows - @Override - protected void sendPacket(MinecraftPacket packet) { - ReflectionUtil.getChannelWrapper(player).write(packet); - } -} +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.handler; + +import codecrafter47.bungeetablistplus.util.ReflectionUtil; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; +import lombok.SneakyThrows; + +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +public class LegacyTabOverlayHandlerImpl extends AbstractLegacyTabOverlayHandler { + + private final Player player; + + private boolean logVersionMismatch = false; + + public LegacyTabOverlayHandlerImpl(Logger logger, int playerListSize, Executor eventLoopExecutor, Player player, boolean is13OrLater) { + super(logger, playerListSize, eventLoopExecutor, is13OrLater); + this.player = player; + } + + @SneakyThrows + @Override + protected void sendPacket(MinecraftPacket packet) { + if ((packet instanceof LegacyPlayerListItem) && (player.getProtocolVersion().getProtocol() >= 761)) { + // error + if (!logVersionMismatch) { + logVersionMismatch = true; + this.logger.warning("Cannot correctly update tablist for player " + player.getUsername() + "\nThe client and server versions do not match. Client <= 1.7.10, server >= 1.19.3.\nUse ViaVersion on the spigot server for the best experience."); + } + } else { + ReflectionUtil.getChannelWrapper(player).write(packet); + } + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java index fbbd886c..3392602f 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java @@ -1,1239 +1,1249 @@ -/* - * Copyright (C) 2020 Florian Stober - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package codecrafter47.bungeetablistplus.handler; - -import codecrafter47.bungeetablistplus.BungeeTabListPlus; -import codecrafter47.bungeetablistplus.protocol.PacketHandler; -import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; -import codecrafter47.bungeetablistplus.protocol.Team; -import codecrafter47.bungeetablistplus.util.BitSet; -import codecrafter47.bungeetablistplus.util.ConcurrentBitSet; -import codecrafter47.bungeetablistplus.util.ReflectionUtil; -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.api.util.GameProfile; -import com.velocitypowered.proxy.protocol.MinecraftPacket; -import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; -import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; -import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; -import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; -import de.codecrafter47.taboverlay.Icon; -import de.codecrafter47.taboverlay.ProfileProperty; -import de.codecrafter47.taboverlay.config.misc.ChatFormat; -import de.codecrafter47.taboverlay.config.misc.Unchecked; -import de.codecrafter47.taboverlay.handler.*; -import it.unimi.dsi.fastutil.objects.*; -import lombok.*; -import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.*; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.Executor; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.ThreadLocalRandom; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class NewTabOverlayHandler implements PacketHandler, TabOverlayHandler { - - // some options - private static final boolean OPTION_ENABLE_CUSTOM_SLOT_USERNAME_COLLISION_CHECK = true; - private static final boolean OPTION_ENABLE_CUSTOM_SLOT_UUID_COLLISION_CHECK = true; - - private static final String EMPTY_JSON_TEXT = "{\"text\":\"\"}"; - protected static final String[][] EMPTY_PROPERTIES_ARRAY = new String[0][]; - - private static final ImmutableMap DIMENSION_TO_USED_SLOTS; - private static final BitSet[] SIZE_TO_USED_SLOTS; - - private static final UUID[] CUSTOM_SLOT_UUID_STEVE; - private static final UUID[] CUSTOM_SLOT_UUID_ALEX; - @Nonnull - private static final Set CUSTOM_SLOT_UUIDS; - private static final String[] CUSTOM_SLOT_USERNAME; - private static final String[] CUSTOM_SLOT_USERNAME_SMILEYS; - @Nonnull - private static final Set CUSTOM_SLOT_USERNAMES; - private static final String[] CUSTOM_SLOT_TEAMNAME; - - static { - - // build the dimension to used slots map (for the rectangular tab overlay) - val builder = ImmutableMap.builder(); - for (int columns = 1; columns <= 4; columns++) { - for (int rows = 0; rows <= 20; rows++) { - if (columns != 1 && rows != 0 && columns * rows <= (columns - 1) * 20) - continue; - BitSet usedSlots = new BitSet(80); - for (int column = 0; column < columns; column++) { - for (int row = 0; row < rows; row++) { - usedSlots.set(index(column, row)); - } - } - builder.put(new RectangularTabOverlay.Dimension(columns, rows), usedSlots); - } - } - DIMENSION_TO_USED_SLOTS = builder.build(); - - // build the size to used slots map (for the simple tab overlay) - SIZE_TO_USED_SLOTS = new BitSet[81]; - for (int size = 0; size <= 80; size++) { - BitSet usedSlots = new BitSet(80); - usedSlots.set(0, size); - SIZE_TO_USED_SLOTS[size] = usedSlots; - } - - // generate random uuids for our custom slots - CUSTOM_SLOT_UUID_ALEX = new UUID[80]; - CUSTOM_SLOT_UUID_STEVE = new UUID[80]; - UUID base = UUID.randomUUID(); - long msb = base.getMostSignificantBits(); - long lsb = base.getLeastSignificantBits(); - lsb ^= base.hashCode(); - for (int i = 0; i < 80; i++) { - CUSTOM_SLOT_UUID_STEVE[i] = new UUID(msb, lsb ^ (2 * i)); - CUSTOM_SLOT_UUID_ALEX[i] = new UUID(msb, lsb ^ (2 * i + 1)); - } - if (OPTION_ENABLE_CUSTOM_SLOT_UUID_COLLISION_CHECK) { - CUSTOM_SLOT_UUIDS = ImmutableSet.builder() - .add(CUSTOM_SLOT_UUID_ALEX) - .add(CUSTOM_SLOT_UUID_STEVE).build(); - } else { - CUSTOM_SLOT_UUIDS = Collections.emptySet(); - } - - // generate usernames for custom slots - int unique = ThreadLocalRandom.current().nextInt(); - CUSTOM_SLOT_USERNAME = new String[81]; - for (int i = 0; i < 81; i++) { - CUSTOM_SLOT_USERNAME[i] = String.format("~BTLP%08x %02d", unique, i); - } - if (OPTION_ENABLE_CUSTOM_SLOT_USERNAME_COLLISION_CHECK) { - CUSTOM_SLOT_USERNAMES = ImmutableSet.copyOf(CUSTOM_SLOT_USERNAME); - } else { - CUSTOM_SLOT_USERNAMES = Collections.emptySet(); - } - CUSTOM_SLOT_USERNAME_SMILEYS = new String[80]; - String emojis = "\u263a\u2639\u2620\u2763\u2764\u270c\u261d\u270d\u2618\u2615\u2668\u2693\u2708\u231b\u231a\u2600\u2b50\u2601\u2602\u2614\u26a1\u2744\u2603\u2604\u2660\u2665\u2666\u2663\u265f\u260e\u2328\u2709\u270f\u2712\u2702\u2692\u2694\u2699\u2696\u2697\u26b0\u26b1\u267f\u26a0\u2622\u2623\u2640\u2642\u267e\u267b\u269c\u303d\u2733\u2734\u2747\u203c\u2b1c\u2b1b\u25fc\u25fb\u25aa\u25ab\u2049\u26ab\u26aa\u3030\u00a9\u00ae\u2122\u2139\u24c2\u3297\u2716\u2714\u2611\u2695\u2b06\u2197\u27a1\u2198\u2b07\u2199\u3299\u2b05\u2196\u2195\u2194\u21a9\u21aa\u2934\u2935\u269b\u2721\u2638\u262f\u271d\u2626\u262a\u262e\u2648\u2649\u264a\u264b\u264c\u264d\u264e\u264f\u2650\u2651\u2652\u2653\u25b6\u25c0\u23cf"; - for (int i = 0; i < 80; i++) { - CUSTOM_SLOT_USERNAME_SMILEYS[i] = String.format("" + emojis.charAt(i), unique, i); - } - - // generate teams for custom slots - CUSTOM_SLOT_TEAMNAME = new String[81]; - for (int i = 0; i < 81; i++) { - CUSTOM_SLOT_TEAMNAME[i] = String.format(" BTLP%08x %02d", unique, i); - } - } - - private final Logger logger; - private final Executor eventLoopExecutor; - - private final Object2BooleanMap serverPlayerListListed = new Object2BooleanOpenHashMap<>(); - @Nullable - protected String serverHeader = null; - @Nullable - protected String serverFooter = null; - - private final Queue> nextActiveContentHandlerQueue = new ConcurrentLinkedQueue<>(); - private final Queue> nextActiveHeaderFooterHandlerQueue = new ConcurrentLinkedQueue<>(); - private AbstractContentOperationModeHandler activeContentHandler; - private AbstractHeaderFooterOperationModeHandler activeHeaderFooterHandler; - - private boolean hasCreatedCustomTeams = false; - - private final AtomicBoolean updateScheduledFlag = new AtomicBoolean(false); - private final Runnable updateTask = this::update; - - protected boolean active; - - private final Player player; - - public NewTabOverlayHandler(Logger logger, Executor eventLoopExecutor, Player player) { - this.logger = logger; - this.eventLoopExecutor = eventLoopExecutor; - this.player = player; - this.activeContentHandler = new PassThroughContentHandler(); - this.activeHeaderFooterHandler = new PassThroughHeaderFooterHandler(); - } - - @SneakyThrows - private void sendPacket(MinecraftPacket packet) { - ReflectionUtil.getChannelWrapper(player).write(packet); - } - - @Override - public PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { - return PacketListenerResult.PASS; - } - - @Override - public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { - if (packet.getActions().contains(UpsertPlayerInfo.Action.ADD_PLAYER)) { - for (UpsertPlayerInfo.Entry entry : packet.getEntries()) { - if (OPTION_ENABLE_CUSTOM_SLOT_UUID_COLLISION_CHECK) { - if (CUSTOM_SLOT_UUIDS.contains(entry.getProfileId())) { - throw new AssertionError("UUID collision " + entry.getProfileId()); - } - } - if (OPTION_ENABLE_CUSTOM_SLOT_USERNAME_COLLISION_CHECK) { - if (CUSTOM_SLOT_USERNAMES.contains(entry.getProfile().getName())) { - throw new AssertionError("Username collision" + entry.getProfile().getName()); - } - } - serverPlayerListListed.putIfAbsent(entry.getProfileId(), false); - } - } - if (packet.getActions().contains(UpsertPlayerInfo.Action.UPDATE_LISTED)) { - for (UpsertPlayerInfo.Entry entry : packet.getEntries()) { - serverPlayerListListed.put(entry.getProfileId(), entry.isListed()); - } - } - - try { - return this.activeContentHandler.onPlayerListUpdatePacket(packet); - } catch (Throwable th) { - logger.log(Level.SEVERE, "Unexpected error", th); - // try recover - enterContentOperationMode(ContentOperationMode.PASS_TROUGH); - return PacketListenerResult.PASS; - } - } - - @Override - public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet) { - for (UUID uuid : packet.getProfilesToRemove()) { - serverPlayerListListed.removeBoolean(uuid); - } - return PacketListenerResult.PASS; - } - - @Override - public PacketListenerResult onTeamPacket(Team packet) { - return PacketListenerResult.PASS; - } - - @Override - public PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { - PacketListenerResult result = PacketListenerResult.PASS; - try { - result = this.activeHeaderFooterHandler.onPlayerListHeaderFooterPacket(packet); - if (result == PacketListenerResult.MODIFIED) { - throw new AssertionError("PacketListenerResult.MODIFIED must not be used"); - } - } catch (Throwable th) { - logger.log(Level.SEVERE, "Unexpected error", th); - // try recover - enterHeaderAndFooterOperationMode(HeaderAndFooterOperationMode.PASS_TROUGH); - } - - this.serverHeader = packet.getHeader() != null ? packet.getHeader() : EMPTY_JSON_TEXT; - this.serverFooter = packet.getFooter() != null ? packet.getFooter() : EMPTY_JSON_TEXT; - - return result; - } - - @Override - public void onServerSwitch(boolean is13OrLater) { - if (!active) { - active = true; - update(); - } else { - - hasCreatedCustomTeams = false; - - try { - this.activeContentHandler.onServerSwitch(); - } catch (Throwable th) { - logger.log(Level.SEVERE, "Unexpected error", th); - // try recover - enterContentOperationMode(ContentOperationMode.PASS_TROUGH); - } - try { - this.activeHeaderFooterHandler.onServerSwitch(); - } catch (Throwable th) { - logger.log(Level.SEVERE, "Unexpected error", th); - // try recover - enterContentOperationMode(ContentOperationMode.PASS_TROUGH); - } - - if (!serverPlayerListListed.isEmpty()) { - RemovePlayerInfo packet = new RemovePlayerInfo(); - packet.setProfilesToRemove(serverPlayerListListed.keySet()); - sendPacket(packet); - } - - serverPlayerListListed.clear(); - if (serverHeader != null) { - serverHeader = EMPTY_JSON_TEXT; - } - if (serverFooter != null) { - serverFooter = EMPTY_JSON_TEXT; - } - } - } - - @Override - public R enterContentOperationMode(ContentOperationMode operationMode) { - AbstractContentOperationModeHandler handler; - if (operationMode == ContentOperationMode.PASS_TROUGH) { - handler = new PassThroughContentHandler(); - } else if (operationMode == ContentOperationMode.SIMPLE) { - handler = new SimpleOperationModeHandler(); - } else if (operationMode == ContentOperationMode.RECTANGULAR) { - handler = new RectangularSizeHandler(); - } else { - throw new UnsupportedOperationException(); - } - nextActiveContentHandlerQueue.add(handler); - scheduleUpdate(); - return Unchecked.cast(handler.getTabOverlay()); - } - - @Override - public R enterHeaderAndFooterOperationMode(HeaderAndFooterOperationMode operationMode) { - AbstractHeaderFooterOperationModeHandler handler; - if (operationMode == HeaderAndFooterOperationMode.PASS_TROUGH) { - handler = new PassThroughHeaderFooterHandler(); - } else if (operationMode == HeaderAndFooterOperationMode.CUSTOM) { - handler = new CustomHeaderAndFooterOperationModeHandler(); - } else { - throw new UnsupportedOperationException(Objects.toString(operationMode)); - } - nextActiveHeaderFooterHandlerQueue.add(handler); - scheduleUpdate(); - return Unchecked.cast(handler.getTabOverlay()); - } - - private void scheduleUpdate() { - if (this.updateScheduledFlag.compareAndSet(false, true)) { - try { - eventLoopExecutor.execute(updateTask); - } catch (RejectedExecutionException ignored) { - } - } - } - - private void update() { - if (!active) { - return; - } - updateScheduledFlag.set(false); - - // update content handler - AbstractContentOperationModeHandler contentHandler; - while (null != (contentHandler = nextActiveContentHandlerQueue.poll())) { - this.activeContentHandler.invalidate(); - contentHandler.onActivated(this.activeContentHandler); - this.activeContentHandler = contentHandler; - } - this.activeContentHandler.update(); - - // update header and footer handler - AbstractHeaderFooterOperationModeHandler heaerFooterHandler; - while (null != (heaerFooterHandler = nextActiveHeaderFooterHandlerQueue.poll())) { - this.activeHeaderFooterHandler.invalidate(); - heaerFooterHandler.onActivated(this.activeHeaderFooterHandler); - this.activeHeaderFooterHandler = heaerFooterHandler; - } - this.activeHeaderFooterHandler.update(); - } - - private abstract static class AbstractContentOperationModeHandler extends OperationModeHandler { - - /** - * Called when the player receives a {@link LegacyPlayerListItem} packet. - *

- * This method is called after this {@link NewTabOverlayHandler} has updated the {@code serverPlayerList}. - */ - abstract PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet); - - /** - * Called when the player switches the server. - *

- * This method is called before this {@link NewTabOverlayHandler} executes its own logic to clear the - * server player list info. - */ - abstract void onServerSwitch(); - - abstract void update(); - - final void invalidate() { - getTabOverlay().invalidate(); - onDeactivated(); - } - - /** - * Called when this {@link OperationModeHandler} is deactivated. - *

- * This method must put the client player list in the state expected by {@link #onActivated(AbstractContentOperationModeHandler)}. It must - * especially remove all custom entries and players must be part of the correct teams. - */ - abstract void onDeactivated(); - - /** - * Called when this {@link OperationModeHandler} becomes the active one. - *

- * State of the player list when this method is called: - * - there are no custom entries on the client - * - all entries from {@link #serverPlayerListListed} but may not be listed - * - player list header/ footer may be wrong - *

- * Additional information about the state of the player list may be obtained from the previous handler - * - * @param previous previous handler - */ - abstract void onActivated(AbstractContentOperationModeHandler previous); - } - - private abstract static class AbstractHeaderFooterOperationModeHandler extends OperationModeHandler { - - /** - * Called when the player receives a {@link HeaderAndFooter} packet. - *

- * This method is called before this {@link NewTabOverlayHandler} executes its own logic to update the - * server player list info. - */ - abstract PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet); - - /** - * Called when the player switches the server. - *

- * This method is called before this {@link NewTabOverlayHandler} executes its own logic to clear the - * server player list info. - */ - abstract void onServerSwitch(); - - abstract void update(); - - final void invalidate() { - getTabOverlay().invalidate(); - onDeactivated(); - } - - /** - * Called when this {@link OperationModeHandler} is deactivated. - *

- * This method must put the client player list in the state expected by {@link #onActivated(AbstractHeaderFooterOperationModeHandler)}. It must - * especially remove all custom entries and players must be part of the correct teams. - */ - abstract void onDeactivated(); - - /** - * Called when this {@link OperationModeHandler} becomes the active one. - *

- * State of the player list when this method is called: - * - there are no custom entries on the client - * - all entries from {@link #serverPlayerListListed} are known to the client, but might not be listed - * - player list header/ footer may be wrong - *

- * Additional information about the state of the player list may be obtained from the previous handler - * - * @param previous previous handler - */ - abstract void onActivated(AbstractHeaderFooterOperationModeHandler previous); - } - - private abstract static class AbstractContentTabOverlay implements TabOverlayHandle { - private boolean valid = true; - - @Override - public boolean isValid() { - return valid; - } - - final void invalidate() { - valid = false; - } - } - - private abstract static class AbstractHeaderFooterTabOverlay implements TabOverlayHandle { - private boolean valid = true; - - @Override - public boolean isValid() { - return valid; - } - - final void invalidate() { - valid = false; - } - } - - private final class PassThroughContentHandler extends AbstractContentOperationModeHandler { - - @Override - protected PassThroughContentTabOverlay createTabOverlay() { - return new PassThroughContentTabOverlay(); - } - - @Override - PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { - return PacketListenerResult.PASS; - } - - @Override - void onServerSwitch() { - sendPacket(new HeaderAndFooter(EMPTY_JSON_TEXT, EMPTY_JSON_TEXT)); - } - - @Override - void update() { - // nothing to do - } - - @Override - void onDeactivated() { - // nothing to do - } - - @Override - void onActivated(AbstractContentOperationModeHandler previous) { - if (previous instanceof PassThroughContentHandler) { - // we're lucky, nothing to do - return; - } - - // update visibility - if (!serverPlayerListListed.isEmpty()) { - List items = new ArrayList<>(serverPlayerListListed.size()); - for (Object2BooleanMap.Entry entry : serverPlayerListListed.object2BooleanEntrySet()) { - - UpsertPlayerInfo.Entry item = new UpsertPlayerInfo.Entry(entry.getKey()); - item.setListed(entry.getBooleanValue()); - items.add(item); - } - UpsertPlayerInfo packet = new UpsertPlayerInfo(); - packet.addAction(UpsertPlayerInfo.Action.UPDATE_LISTED); - packet.addAllEntries(items); - sendPacket(packet); - } - } - } - - private static final class PassThroughContentTabOverlay extends AbstractContentTabOverlay { - - } - - private final class PassThroughHeaderFooterHandler extends AbstractHeaderFooterOperationModeHandler { - - @Override - protected PassThroughHeaderFooterTabOverlay createTabOverlay() { - return new PassThroughHeaderFooterTabOverlay(); - } - - @Override - PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { - return PacketListenerResult.PASS; - } - - @Override - void onServerSwitch() { - sendPacket(new HeaderAndFooter(EMPTY_JSON_TEXT, EMPTY_JSON_TEXT)); - } - - @Override - void update() { - // nothing to do - } - - @Override - void onDeactivated() { - // nothing to do - } - - @Override - void onActivated(AbstractHeaderFooterOperationModeHandler previous) { - if (previous instanceof PassThroughHeaderFooterHandler) { - // we're lucky, nothing to do - return; - } - - // fix header/ footer - sendPacket(new HeaderAndFooter(serverHeader != null ? serverHeader : EMPTY_JSON_TEXT, serverFooter != null ? serverFooter : EMPTY_JSON_TEXT)); - } - } - - private static final class PassThroughHeaderFooterTabOverlay extends AbstractHeaderFooterTabOverlay { - - } - - private abstract class CustomContentTabOverlayHandler extends AbstractContentOperationModeHandler { - - @Nonnull - BitSet usedSlots; - BitSet dirtySlots; - final SlotState[] slotState; - /** - * Uuid of the player list entry used for the slot. - */ - final UUID[] slotUuid; - /** - * Username of the player list entry used for the slot. - */ - final String[] slotUsername; - - private final List itemQueueAddPlayer; - private final List itemQueueRemovePlayer; - private final List itemQueueUpdateDisplayName; - private final List itemQueueUpdatePing; - - private final boolean experimentalTabCompleteSmileys = isExperimentalTabCompleteSmileys(); - - private CustomContentTabOverlayHandler() { - this.dirtySlots = new BitSet(80); - this.usedSlots = SIZE_TO_USED_SLOTS[0]; - this.slotState = new SlotState[80]; - Arrays.fill(this.slotState, SlotState.UNUSED); - this.slotUuid = new UUID[80]; - this.slotUsername = new String[80]; - this.itemQueueAddPlayer = new ArrayList<>(80); - this.itemQueueRemovePlayer = new ArrayList<>(80); - this.itemQueueUpdateDisplayName = new ArrayList<>(80); - this.itemQueueUpdatePing = new ArrayList<>(80); - } - - @Override - PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { - - if (packet.getActions().contains(UpsertPlayerInfo.Action.UPDATE_LISTED)) { - for (UpsertPlayerInfo.Entry entry : packet.getEntries()) { - entry.setListed(false); - } - } - return PacketListenerResult.MODIFIED; - } - - private String getCustomSlotUsername(int index) { - if (experimentalTabCompleteSmileys) { - return CUSTOM_SLOT_USERNAME_SMILEYS[index]; - } else { - return CUSTOM_SLOT_USERNAME[index]; - } - } - - @Override - void onServerSwitch() { - - createTeamsIfNecessary(); - } - - @Override - void onActivated(AbstractContentOperationModeHandler previous) { - - // make all players unlisted - if (!serverPlayerListListed.isEmpty()) { - List items = new ArrayList<>(serverPlayerListListed.size()); - for (Object2BooleanMap.Entry entry : serverPlayerListListed.object2BooleanEntrySet()) { - UpsertPlayerInfo.Entry item = new UpsertPlayerInfo.Entry(entry.getKey()); - item.setListed(false); - items.add(item); - } - UpsertPlayerInfo packet = new UpsertPlayerInfo(); - packet.addAction(UpsertPlayerInfo.Action.UPDATE_LISTED); - packet.addAllEntries(items); - sendPacket(packet); - } - - createTeamsIfNecessary(); - } - - private void createTeamsIfNecessary() { - // create teams if not already created - if (!hasCreatedCustomTeams) { - hasCreatedCustomTeams = true; - - for (int i = 0; i < 80; i++) { - sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[i], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[i], CUSTOM_SLOT_USERNAME_SMILEYS[i]})); - } - } - } - - @Override - void onDeactivated() { - int customSlots = 0; - for (int index = 0; index < 80; index++) { - if (slotState[index] != SlotState.UNUSED) { - customSlots++; - } - } - - int i = 0; - if (customSlots > 0) { - UUID[] uuids = new UUID[customSlots]; - for (int index = 0; index < 80; index++) { - // switch slot from custom to unused - if (slotState[index] == SlotState.CUSTOM) { - uuids[i++] = slotUuid[index]; - } - } - RemovePlayerInfo packet = new RemovePlayerInfo(); - packet.setProfilesToRemove(Arrays.asList(uuids)); - sendPacket(packet); - } - } - - @Override - void update() { - T tabOverlay = getTabOverlay(); - - if (tabOverlay.dirtyFlagSize) { - tabOverlay.dirtyFlagSize = false; - updateSize(); - } - - // update icons - dirtySlots.orAndClear(tabOverlay.dirtyFlagsIcon); - for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { - if (slotState[index] == SlotState.CUSTOM) { - // remove item - itemQueueRemovePlayer.add(slotUuid[index]); - slotState[index] = SlotState.UNUSED; - } - - if (usedSlots.get(index)) { - Icon icon = tabOverlay.icon[index]; - UUID customSlotUuid; - if (icon.isAlex()) { - customSlotUuid = CUSTOM_SLOT_UUID_ALEX[index]; - } else { // steve - customSlotUuid = CUSTOM_SLOT_UUID_STEVE[index]; - } - tabOverlay.dirtyFlagsText.clear(index); - tabOverlay.dirtyFlagsPing.clear(index); - slotState[index] = SlotState.CUSTOM; - slotUuid[index] = customSlotUuid; - UpsertPlayerInfo.Entry item = new UpsertPlayerInfo.Entry(customSlotUuid); - GameProfile profile = new GameProfile(customSlotUuid, slotUsername[index] = getCustomSlotUsername(index), toPropertiesList(icon.getTextureProperty())); - item.setProfile(profile); - item.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); - item.setLatency(tabOverlay.ping[index]); - item.setGameMode(0); - item.setListed(true); - itemQueueAddPlayer.add(item); - } - } - - // update text - dirtySlots.copyAndClear(tabOverlay.dirtyFlagsText); - for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { - if (slotState[index] != SlotState.UNUSED) { - UpsertPlayerInfo.Entry item = new UpsertPlayerInfo.Entry(slotUuid[index]); - item.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); - itemQueueUpdateDisplayName.add(item); - } - } - - // update ping - dirtySlots.copyAndClear(tabOverlay.dirtyFlagsPing); - for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { - if (slotState[index] != SlotState.UNUSED) { - UpsertPlayerInfo.Entry item = new UpsertPlayerInfo.Entry(slotUuid[index]); - item.setLatency(tabOverlay.ping[index]); - itemQueueUpdatePing.add(item); - } - } - - dirtySlots.clear(); - - // send packets - sendQueuedItems(); - } - - private void sendQueuedItems() { - if (!itemQueueRemovePlayer.isEmpty()) { - RemovePlayerInfo packet = new RemovePlayerInfo(); - packet.setProfilesToRemove(itemQueueRemovePlayer); - sendPacket(packet); - itemQueueRemovePlayer.clear(); - } - if (!itemQueueAddPlayer.isEmpty()) { - UpsertPlayerInfo packet = new UpsertPlayerInfo(); - packet.addAllActions(EnumSet.of(UpsertPlayerInfo.Action.ADD_PLAYER, UpsertPlayerInfo.Action.UPDATE_DISPLAY_NAME, UpsertPlayerInfo.Action.UPDATE_LATENCY, UpsertPlayerInfo.Action.UPDATE_LISTED)); - packet.addAllEntries(itemQueueAddPlayer); - sendPacket(packet); - itemQueueAddPlayer.clear(); - } - if (!itemQueueUpdateDisplayName.isEmpty()) { - UpsertPlayerInfo packet = new UpsertPlayerInfo(); - packet.addAction(UpsertPlayerInfo.Action.UPDATE_DISPLAY_NAME); - packet.addAllEntries(itemQueueUpdateDisplayName); - sendPacket(packet); - itemQueueUpdateDisplayName.clear(); - } - if (!itemQueueUpdatePing.isEmpty()) { - UpsertPlayerInfo packet = new UpsertPlayerInfo(); - packet.addAction(UpsertPlayerInfo.Action.UPDATE_LATENCY); - packet.addAllEntries(itemQueueUpdatePing); - sendPacket(packet); - itemQueueUpdatePing.clear(); - } - } - - /** - * Updates the usedSlots BitSet. Sets the {@link #dirtySlots uuid dirty flag} for all added - * and removed slots. - */ - abstract void updateSize(); - } - - private boolean isExperimentalTabCompleteSmileys() { - return BungeeTabListPlus.getInstance().getConfig().experimentalTabCompleteSmileys; - } - - private abstract class CustomContentTabOverlay extends AbstractContentTabOverlay implements TabOverlayHandle.BatchModifiable { - final Icon[] icon; - final String[] text; - final int[] ping; - - final AtomicInteger batchUpdateRecursionLevel; - volatile boolean dirtyFlagSize; - final ConcurrentBitSet dirtyFlagsIcon; - final ConcurrentBitSet dirtyFlagsText; - final ConcurrentBitSet dirtyFlagsPing; - - private CustomContentTabOverlay() { - this.icon = new Icon[80]; - Arrays.fill(this.icon, Icon.DEFAULT_STEVE); - this.text = new String[80]; - Arrays.fill(this.text, EMPTY_JSON_TEXT); - this.ping = new int[80]; - this.batchUpdateRecursionLevel = new AtomicInteger(0); - this.dirtyFlagSize = true; - this.dirtyFlagsIcon = new ConcurrentBitSet(80); - this.dirtyFlagsText = new ConcurrentBitSet(80); - this.dirtyFlagsPing = new ConcurrentBitSet(80); - } - - @Override - public void beginBatchModification() { - if (isValid()) { - if (batchUpdateRecursionLevel.incrementAndGet() < 0) { - throw new AssertionError("Recursion level overflow"); - } - } - } - - @Override - public void completeBatchModification() { - if (isValid()) { - int level = batchUpdateRecursionLevel.decrementAndGet(); - if (level == 0) { - scheduleUpdate(); - } else if (level < 0) { - throw new AssertionError("Recursion level underflow"); - } - } - } - - void scheduleUpdateIfNotInBatch() { - if (batchUpdateRecursionLevel.get() == 0) { - scheduleUpdate(); - } - } - - void setIconInternal(int index, @Nonnull @NonNull Icon icon) { - if (!icon.equals(this.icon[index])) { - this.icon[index] = icon; - dirtyFlagsIcon.set(index); - scheduleUpdateIfNotInBatch(); - } - } - - void setTextInternal(int index, @Nonnull @NonNull String text) { - String jsonText = ChatFormat.formattedTextToJson(text); - if (!jsonText.equals(this.text[index])) { - this.text[index] = jsonText; - dirtyFlagsText.set(index); - scheduleUpdateIfNotInBatch(); - } - } - - void setPingInternal(int index, int ping) { - if (ping != this.ping[index]) { - this.ping[index] = ping; - dirtyFlagsPing.set(index); - scheduleUpdateIfNotInBatch(); - } - } - } - - private class RectangularSizeHandler extends CustomContentTabOverlayHandler { - - @Override - void updateSize() { - RectangularTabOverlayImpl tabOverlay = getTabOverlay(); - RectangularTabOverlay.Dimension size = tabOverlay.getSize(); - BitSet newUsedSlots = DIMENSION_TO_USED_SLOTS.get(size); - dirtySlots.orXor(usedSlots, newUsedSlots); - usedSlots = newUsedSlots; - } - - @Override - protected RectangularTabOverlayImpl createTabOverlay() { - return new RectangularTabOverlayImpl(); - } - } - - private class RectangularTabOverlayImpl extends CustomContentTabOverlay implements RectangularTabOverlay { - - @Nonnull - private Dimension size; - - private RectangularTabOverlayImpl() { - Optional dimensionZero = getSupportedSizes().stream().filter(size -> size.getSize() == 0).findAny(); - if (!dimensionZero.isPresent()) { - throw new AssertionError(); - } - this.size = dimensionZero.get(); - } - - @Nonnull - @Override - public Dimension getSize() { - return size; - } - - @Override - public Collection getSupportedSizes() { - return DIMENSION_TO_USED_SLOTS.keySet(); - } - - @Override - public void setSize(@Nonnull Dimension size) { - if (!getSupportedSizes().contains(size)) { - throw new IllegalArgumentException("Unsupported size " + size); - } - if (isValid() && !this.size.equals(size)) { - BitSet oldUsedSlots = DIMENSION_TO_USED_SLOTS.get(this.size); - BitSet newUsedSlots = DIMENSION_TO_USED_SLOTS.get(size); - for (int index = newUsedSlots.nextSetBit(0); index >= 0; index = newUsedSlots.nextSetBit(index + 1)) { - if (!oldUsedSlots.get(index)) { - icon[index] = Icon.DEFAULT_STEVE; - text[index] = EMPTY_JSON_TEXT; - ping[index] = 0; - } - } - this.size = size; - this.dirtyFlagSize = true; - scheduleUpdateIfNotInBatch(); - for (int index = oldUsedSlots.nextSetBit(0); index >= 0; index = oldUsedSlots.nextSetBit(index + 1)) { - if (!newUsedSlots.get(index)) { - icon[index] = Icon.DEFAULT_STEVE; - text[index] = EMPTY_JSON_TEXT; - ping[index] = 0; - } - } - } - } - - @Override - public void setSlot(int column, int row, @Nullable UUID uuid, @Nonnull Icon icon, @Nonnull String text, int ping) { - setSlot(column, row, icon, text, ping); - } - - @Override - public void setSlot(int column, int row, @Nonnull Icon icon, @Nonnull String text, int ping) { - if (isValid()) { - Preconditions.checkElementIndex(column, size.getColumns(), "column"); - Preconditions.checkElementIndex(row, size.getRows(), "row"); - beginBatchModification(); - try { - int index = index(column, row); - setIconInternal(index, icon); - setTextInternal(index, text); - setPingInternal(index, ping); - } finally { - completeBatchModification(); - } - } - } - - @Override - public void setUuid(int column, int row, @Nullable UUID uuid) { - // no op - } - - @Override - public void setIcon(int column, int row, @Nonnull Icon icon) { - if (isValid()) { - Preconditions.checkElementIndex(column, size.getColumns(), "column"); - Preconditions.checkElementIndex(row, size.getRows(), "row"); - setIconInternal(index(column, row), icon); - } - } - - @Override - public void setText(int column, int row, @Nonnull String text) { - if (isValid()) { - Preconditions.checkElementIndex(column, size.getColumns(), "column"); - Preconditions.checkElementIndex(row, size.getRows(), "row"); - setTextInternal(index(column, row), text); - } - } - - @Override - public void setPing(int column, int row, int ping) { - if (isValid()) { - Preconditions.checkElementIndex(column, size.getColumns(), "column"); - Preconditions.checkElementIndex(row, size.getRows(), "row"); - setPingInternal(index(column, row), ping); - } - } - } - - private class SimpleOperationModeHandler extends CustomContentTabOverlayHandler { - - private int size = 0; - - @Override - void updateSize() { - int newSize = getTabOverlay().size; - if (newSize > size) { - dirtySlots.set(size, newSize); - } else if (newSize < size) { - dirtySlots.set(newSize, size); - } - usedSlots = SIZE_TO_USED_SLOTS[newSize]; - size = newSize; - } - - @Override - protected SimpleTabOverlayImpl createTabOverlay() { - return new SimpleTabOverlayImpl(); - } - } - - private class SimpleTabOverlayImpl extends CustomContentTabOverlay implements SimpleTabOverlay { - int size = 0; - - @Override - public int getSize() { - return size; - } - - @Override - public int getMaxSize() { - return 80; - } - - @Override - public void setSize(int size) { - if (size < 0 || size > 80) { - throw new IllegalArgumentException("size"); - } - this.size = size; - dirtyFlagSize = true; - scheduleUpdateIfNotInBatch(); - } - - @Override - public void setSlot(int index, @Nullable UUID uuid, @Nonnull Icon icon, @Nonnull String text, int ping) { - if (isValid()) { - Preconditions.checkElementIndex(index, size, "index"); - beginBatchModification(); - try { - setIconInternal(index, icon); - setTextInternal(index, text); - setPingInternal(index, ping); - } finally { - completeBatchModification(); - } - } - } - - @Override - public void setSlot(int index, @Nonnull Icon icon, @Nonnull String text, int ping) { - if (isValid()) { - Preconditions.checkElementIndex(index, size, "index"); - beginBatchModification(); - try { - setIconInternal(index, icon); - setTextInternal(index, text); - setPingInternal(index, ping); - } finally { - completeBatchModification(); - } - } - } - - @Override - public void setUuid(int index, UUID uuid) { - // no op - } - - @Override - public void setIcon(int index, @Nonnull @NonNull Icon icon) { - if (isValid()) { - Preconditions.checkElementIndex(index, size, "index"); - setIconInternal(index, icon); - } - } - - @Override - public void setText(int index, @Nonnull @NonNull String text) { - if (isValid()) { - Preconditions.checkElementIndex(index, size, "index"); - setTextInternal(index, text); - } - } - - @Override - public void setPing(int index, int ping) { - if (isValid()) { - Preconditions.checkElementIndex(index, size, "index"); - setPingInternal(index, ping); - } - } - } - - private final class CustomHeaderAndFooterOperationModeHandler extends AbstractHeaderFooterOperationModeHandler { - - @Override - protected CustomHeaderAndFooterImpl createTabOverlay() { - return new CustomHeaderAndFooterImpl(); - } - - @Override - PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { - return PacketListenerResult.CANCEL; - } - - @Override - void onServerSwitch() { - // do nothing - } - - @Override - void onDeactivated() { - //do nothing - } - - @Override - void onActivated(AbstractHeaderFooterOperationModeHandler previous) { - // remove header/ footer - sendPacket(new HeaderAndFooter(EMPTY_JSON_TEXT, EMPTY_JSON_TEXT)); - } - - @Override - void update() { - CustomHeaderAndFooterImpl tabOverlay = getTabOverlay(); - if (tabOverlay.headerOrFooterDirty) { - tabOverlay.headerOrFooterDirty = false; - sendPacket(new HeaderAndFooter(tabOverlay.header, tabOverlay.footer)); - } - } - } - - private final class CustomHeaderAndFooterImpl extends AbstractHeaderFooterTabOverlay implements HeaderAndFooterHandle { - private String header = EMPTY_JSON_TEXT; - private String footer = EMPTY_JSON_TEXT; - - private volatile boolean headerOrFooterDirty = false; - - final AtomicInteger batchUpdateRecursionLevel = new AtomicInteger(0); - - @Override - public void beginBatchModification() { - if (isValid()) { - if (batchUpdateRecursionLevel.incrementAndGet() < 0) { - throw new AssertionError("Recursion level overflow"); - } - } - } - - @Override - public void completeBatchModification() { - if (isValid()) { - int level = batchUpdateRecursionLevel.decrementAndGet(); - if (level == 0) { - scheduleUpdate(); - } else if (level < 0) { - throw new AssertionError("Recursion level underflow"); - } - } - } - - void scheduleUpdateIfNotInBatch() { - if (batchUpdateRecursionLevel.get() == 0) { - scheduleUpdate(); - } - } - - @Override - public void setHeaderFooter(@Nullable String header, @Nullable String footer) { - this.header = ChatFormat.formattedTextToJson(header); - this.footer = ChatFormat.formattedTextToJson(footer); - headerOrFooterDirty = true; - scheduleUpdateIfNotInBatch(); - } - - @Override - public void setHeader(@Nullable String header) { - this.header = ChatFormat.formattedTextToJson(header); - headerOrFooterDirty = true; - scheduleUpdateIfNotInBatch(); - } - - @Override - public void setFooter(@Nullable String footer) { - this.footer = ChatFormat.formattedTextToJson(footer); - headerOrFooterDirty = true; - scheduleUpdateIfNotInBatch(); - } - } - - private static int index(int column, int row) { - return column * 20 + row; - } - - private static List toPropertiesList(ProfileProperty textureProperty) { - if (textureProperty == null) { - return new ArrayList<>(); - } else if (textureProperty.isSigned()) { - return List.of(new GameProfile.Property(textureProperty.getName(), textureProperty.getValue(), textureProperty.getSignature())); - } else { - return List.of(new GameProfile.Property(textureProperty.getName(), textureProperty.getValue(), "")); - } - } - - private static Team createPacketTeamCreate(String name, String displayName, String prefix, String suffix, String nameTagVisibility, String collisionRule, int color, byte friendlyFire, String[] players) { - Team team = new Team(); - team.setName(name); - team.setMode((byte) 0); - team.setDisplayName(displayName); - team.setPrefix(prefix); - team.setSuffix(suffix); - team.setNameTagVisibility(nameTagVisibility); - team.setCollisionRule(collisionRule); - team.setColor(color); - team.setFriendlyFire(friendlyFire); - team.setPlayers(players); - return team; - } - - private enum SlotState { - UNUSED, CUSTOM - } -} +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.handler; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.protocol.PacketHandler; +import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; +import codecrafter47.bungeetablistplus.protocol.Team; +import codecrafter47.bungeetablistplus.util.BitSet; +import codecrafter47.bungeetablistplus.util.ConcurrentBitSet; +import codecrafter47.bungeetablistplus.util.ReflectionUtil; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.util.GameProfile; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; +import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; +import de.codecrafter47.taboverlay.Icon; +import de.codecrafter47.taboverlay.ProfileProperty; +import de.codecrafter47.taboverlay.config.misc.ChatFormat; +import de.codecrafter47.taboverlay.config.misc.Unchecked; +import de.codecrafter47.taboverlay.handler.*; +import it.unimi.dsi.fastutil.objects.*; +import lombok.*; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.*; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class NewTabOverlayHandler implements PacketHandler, TabOverlayHandler { + + // some options + private static final boolean OPTION_ENABLE_CUSTOM_SLOT_USERNAME_COLLISION_CHECK = true; + private static final boolean OPTION_ENABLE_CUSTOM_SLOT_UUID_COLLISION_CHECK = true; + + private static final String EMPTY_JSON_TEXT = "{\"text\":\"\"}"; + protected static final String[][] EMPTY_PROPERTIES_ARRAY = new String[0][]; + + private static final ImmutableMap DIMENSION_TO_USED_SLOTS; + private static final BitSet[] SIZE_TO_USED_SLOTS; + + private static final UUID[] CUSTOM_SLOT_UUID_STEVE; + private static final UUID[] CUSTOM_SLOT_UUID_ALEX; + @Nonnull + private static final Set CUSTOM_SLOT_UUIDS; + private static final String[] CUSTOM_SLOT_USERNAME; + private static final String[] CUSTOM_SLOT_USERNAME_SMILEYS; + @Nonnull + private static final Set CUSTOM_SLOT_USERNAMES; + private static final String[] CUSTOM_SLOT_TEAMNAME; + + static { + + // build the dimension to used slots map (for the rectangular tab overlay) + val builder = ImmutableMap.builder(); + for (int columns = 1; columns <= 4; columns++) { + for (int rows = 0; rows <= 20; rows++) { + if (columns != 1 && rows != 0 && columns * rows <= (columns - 1) * 20) + continue; + BitSet usedSlots = new BitSet(80); + for (int column = 0; column < columns; column++) { + for (int row = 0; row < rows; row++) { + usedSlots.set(index(column, row)); + } + } + builder.put(new RectangularTabOverlay.Dimension(columns, rows), usedSlots); + } + } + DIMENSION_TO_USED_SLOTS = builder.build(); + + // build the size to used slots map (for the simple tab overlay) + SIZE_TO_USED_SLOTS = new BitSet[81]; + for (int size = 0; size <= 80; size++) { + BitSet usedSlots = new BitSet(80); + usedSlots.set(0, size); + SIZE_TO_USED_SLOTS[size] = usedSlots; + } + + // generate random uuids for our custom slots + CUSTOM_SLOT_UUID_ALEX = new UUID[80]; + CUSTOM_SLOT_UUID_STEVE = new UUID[80]; + UUID base = UUID.randomUUID(); + long msb = base.getMostSignificantBits(); + long lsb = base.getLeastSignificantBits(); + lsb ^= base.hashCode(); + for (int i = 0; i < 80; i++) { + CUSTOM_SLOT_UUID_STEVE[i] = new UUID(msb, lsb ^ (2 * i)); + CUSTOM_SLOT_UUID_ALEX[i] = new UUID(msb, lsb ^ (2 * i + 1)); + } + if (OPTION_ENABLE_CUSTOM_SLOT_UUID_COLLISION_CHECK) { + CUSTOM_SLOT_UUIDS = ImmutableSet.builder() + .add(CUSTOM_SLOT_UUID_ALEX) + .add(CUSTOM_SLOT_UUID_STEVE).build(); + } else { + CUSTOM_SLOT_UUIDS = Collections.emptySet(); + } + + // generate usernames for custom slots + int unique = ThreadLocalRandom.current().nextInt(); + CUSTOM_SLOT_USERNAME = new String[81]; + for (int i = 0; i < 81; i++) { + CUSTOM_SLOT_USERNAME[i] = String.format("~BTLP%08x %02d", unique, i); + } + if (OPTION_ENABLE_CUSTOM_SLOT_USERNAME_COLLISION_CHECK) { + CUSTOM_SLOT_USERNAMES = ImmutableSet.copyOf(CUSTOM_SLOT_USERNAME); + } else { + CUSTOM_SLOT_USERNAMES = Collections.emptySet(); + } + CUSTOM_SLOT_USERNAME_SMILEYS = new String[80]; + String emojis = "\u263a\u2639\u2620\u2763\u2764\u270c\u261d\u270d\u2618\u2615\u2668\u2693\u2708\u231b\u231a\u2600\u2b50\u2601\u2602\u2614\u26a1\u2744\u2603\u2604\u2660\u2665\u2666\u2663\u265f\u260e\u2328\u2709\u270f\u2712\u2702\u2692\u2694\u2699\u2696\u2697\u26b0\u26b1\u267f\u26a0\u2622\u2623\u2640\u2642\u267e\u267b\u269c\u303d\u2733\u2734\u2747\u203c\u2b1c\u2b1b\u25fc\u25fb\u25aa\u25ab\u2049\u26ab\u26aa\u3030\u00a9\u00ae\u2122\u2139\u24c2\u3297\u2716\u2714\u2611\u2695\u2b06\u2197\u27a1\u2198\u2b07\u2199\u3299\u2b05\u2196\u2195\u2194\u21a9\u21aa\u2934\u2935\u269b\u2721\u2638\u262f\u271d\u2626\u262a\u262e\u2648\u2649\u264a\u264b\u264c\u264d\u264e\u264f\u2650\u2651\u2652\u2653\u25b6\u25c0\u23cf"; + for (int i = 0; i < 80; i++) { + CUSTOM_SLOT_USERNAME_SMILEYS[i] = String.format("" + emojis.charAt(i), unique, i); + } + + // generate teams for custom slots + CUSTOM_SLOT_TEAMNAME = new String[81]; + for (int i = 0; i < 81; i++) { + CUSTOM_SLOT_TEAMNAME[i] = String.format(" BTLP%08x %02d", unique, i); + } + } + + private final Logger logger; + private final Executor eventLoopExecutor; + + private final Object2BooleanMap serverPlayerListListed = new Object2BooleanOpenHashMap<>(); + @Nullable + protected String serverHeader = null; + @Nullable + protected String serverFooter = null; + + private final Queue> nextActiveContentHandlerQueue = new ConcurrentLinkedQueue<>(); + private final Queue> nextActiveHeaderFooterHandlerQueue = new ConcurrentLinkedQueue<>(); + private AbstractContentOperationModeHandler activeContentHandler; + private AbstractHeaderFooterOperationModeHandler activeHeaderFooterHandler; + + private boolean hasCreatedCustomTeams = false; + + private final AtomicBoolean updateScheduledFlag = new AtomicBoolean(false); + private final Runnable updateTask = this::update; + + protected boolean active; + + private boolean logVersionMismatch = false; + + private final Player player; + + public NewTabOverlayHandler(Logger logger, Executor eventLoopExecutor, Player player) { + this.logger = logger; + this.eventLoopExecutor = eventLoopExecutor; + this.player = player; + this.activeContentHandler = new PassThroughContentHandler(); + this.activeHeaderFooterHandler = new PassThroughHeaderFooterHandler(); + } + + @SneakyThrows + private void sendPacket(MinecraftPacket packet) { + if (((packet instanceof UpsertPlayerInfo) || (packet instanceof RemovePlayerInfo)) && (player.getProtocolVersion().getProtocol() < 761)) { + // error + if (!logVersionMismatch) { + logVersionMismatch = true; + logger.warning("Cannot correctly update tablist for player " + player.getUsername() + "\nThe client and server versions do not match. Client >= 1.19.3, server < 1.19.3.\nUse ViaVersion on the spigot server for the best experience."); + } + } else { + ReflectionUtil.getChannelWrapper(player).write(packet); + } + } + + @Override + public PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { + return PacketListenerResult.PASS; + } + + @Override + public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { + if (packet.getActions().contains(UpsertPlayerInfo.Action.ADD_PLAYER)) { + for (UpsertPlayerInfo.Entry entry : packet.getEntries()) { + if (OPTION_ENABLE_CUSTOM_SLOT_UUID_COLLISION_CHECK) { + if (CUSTOM_SLOT_UUIDS.contains(entry.getProfileId())) { + throw new AssertionError("UUID collision " + entry.getProfileId()); + } + } + if (OPTION_ENABLE_CUSTOM_SLOT_USERNAME_COLLISION_CHECK) { + if (CUSTOM_SLOT_USERNAMES.contains(entry.getProfile().getName())) { + throw new AssertionError("Username collision" + entry.getProfile().getName()); + } + } + serverPlayerListListed.putIfAbsent(entry.getProfileId(), false); + } + } + if (packet.getActions().contains(UpsertPlayerInfo.Action.UPDATE_LISTED)) { + for (UpsertPlayerInfo.Entry entry : packet.getEntries()) { + serverPlayerListListed.put(entry.getProfileId(), entry.isListed()); + } + } + + try { + return this.activeContentHandler.onPlayerListUpdatePacket(packet); + } catch (Throwable th) { + logger.log(Level.SEVERE, "Unexpected error", th); + // try recover + enterContentOperationMode(ContentOperationMode.PASS_TROUGH); + return PacketListenerResult.PASS; + } + } + + @Override + public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet) { + for (UUID uuid : packet.getProfilesToRemove()) { + serverPlayerListListed.removeBoolean(uuid); + } + return PacketListenerResult.PASS; + } + + @Override + public PacketListenerResult onTeamPacket(Team packet) { + return PacketListenerResult.PASS; + } + + @Override + public PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { + PacketListenerResult result = PacketListenerResult.PASS; + try { + result = this.activeHeaderFooterHandler.onPlayerListHeaderFooterPacket(packet); + if (result == PacketListenerResult.MODIFIED) { + throw new AssertionError("PacketListenerResult.MODIFIED must not be used"); + } + } catch (Throwable th) { + logger.log(Level.SEVERE, "Unexpected error", th); + // try recover + enterHeaderAndFooterOperationMode(HeaderAndFooterOperationMode.PASS_TROUGH); + } + + this.serverHeader = packet.getHeader() != null ? packet.getHeader() : EMPTY_JSON_TEXT; + this.serverFooter = packet.getFooter() != null ? packet.getFooter() : EMPTY_JSON_TEXT; + + return result; + } + + @Override + public void onServerSwitch(boolean is13OrLater) { + if (!active) { + active = true; + update(); + } else { + + hasCreatedCustomTeams = false; + + try { + this.activeContentHandler.onServerSwitch(); + } catch (Throwable th) { + logger.log(Level.SEVERE, "Unexpected error", th); + // try recover + enterContentOperationMode(ContentOperationMode.PASS_TROUGH); + } + try { + this.activeHeaderFooterHandler.onServerSwitch(); + } catch (Throwable th) { + logger.log(Level.SEVERE, "Unexpected error", th); + // try recover + enterContentOperationMode(ContentOperationMode.PASS_TROUGH); + } + + if (!serverPlayerListListed.isEmpty()) { + RemovePlayerInfo packet = new RemovePlayerInfo(); + packet.setProfilesToRemove(serverPlayerListListed.keySet()); + sendPacket(packet); + } + + serverPlayerListListed.clear(); + if (serverHeader != null) { + serverHeader = EMPTY_JSON_TEXT; + } + if (serverFooter != null) { + serverFooter = EMPTY_JSON_TEXT; + } + } + } + + @Override + public R enterContentOperationMode(ContentOperationMode operationMode) { + AbstractContentOperationModeHandler handler; + if (operationMode == ContentOperationMode.PASS_TROUGH) { + handler = new PassThroughContentHandler(); + } else if (operationMode == ContentOperationMode.SIMPLE) { + handler = new SimpleOperationModeHandler(); + } else if (operationMode == ContentOperationMode.RECTANGULAR) { + handler = new RectangularSizeHandler(); + } else { + throw new UnsupportedOperationException(); + } + nextActiveContentHandlerQueue.add(handler); + scheduleUpdate(); + return Unchecked.cast(handler.getTabOverlay()); + } + + @Override + public R enterHeaderAndFooterOperationMode(HeaderAndFooterOperationMode operationMode) { + AbstractHeaderFooterOperationModeHandler handler; + if (operationMode == HeaderAndFooterOperationMode.PASS_TROUGH) { + handler = new PassThroughHeaderFooterHandler(); + } else if (operationMode == HeaderAndFooterOperationMode.CUSTOM) { + handler = new CustomHeaderAndFooterOperationModeHandler(); + } else { + throw new UnsupportedOperationException(Objects.toString(operationMode)); + } + nextActiveHeaderFooterHandlerQueue.add(handler); + scheduleUpdate(); + return Unchecked.cast(handler.getTabOverlay()); + } + + private void scheduleUpdate() { + if (this.updateScheduledFlag.compareAndSet(false, true)) { + try { + eventLoopExecutor.execute(updateTask); + } catch (RejectedExecutionException ignored) { + } + } + } + + private void update() { + if (!active) { + return; + } + updateScheduledFlag.set(false); + + // update content handler + AbstractContentOperationModeHandler contentHandler; + while (null != (contentHandler = nextActiveContentHandlerQueue.poll())) { + this.activeContentHandler.invalidate(); + contentHandler.onActivated(this.activeContentHandler); + this.activeContentHandler = contentHandler; + } + this.activeContentHandler.update(); + + // update header and footer handler + AbstractHeaderFooterOperationModeHandler heaerFooterHandler; + while (null != (heaerFooterHandler = nextActiveHeaderFooterHandlerQueue.poll())) { + this.activeHeaderFooterHandler.invalidate(); + heaerFooterHandler.onActivated(this.activeHeaderFooterHandler); + this.activeHeaderFooterHandler = heaerFooterHandler; + } + this.activeHeaderFooterHandler.update(); + } + + private abstract static class AbstractContentOperationModeHandler extends OperationModeHandler { + + /** + * Called when the player receives a {@link LegacyPlayerListItem} packet. + *

+ * This method is called after this {@link NewTabOverlayHandler} has updated the {@code serverPlayerList}. + */ + abstract PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet); + + /** + * Called when the player switches the server. + *

+ * This method is called before this {@link NewTabOverlayHandler} executes its own logic to clear the + * server player list info. + */ + abstract void onServerSwitch(); + + abstract void update(); + + final void invalidate() { + getTabOverlay().invalidate(); + onDeactivated(); + } + + /** + * Called when this {@link OperationModeHandler} is deactivated. + *

+ * This method must put the client player list in the state expected by {@link #onActivated(AbstractContentOperationModeHandler)}. It must + * especially remove all custom entries and players must be part of the correct teams. + */ + abstract void onDeactivated(); + + /** + * Called when this {@link OperationModeHandler} becomes the active one. + *

+ * State of the player list when this method is called: + * - there are no custom entries on the client + * - all entries from {@link #serverPlayerListListed} but may not be listed + * - player list header/ footer may be wrong + *

+ * Additional information about the state of the player list may be obtained from the previous handler + * + * @param previous previous handler + */ + abstract void onActivated(AbstractContentOperationModeHandler previous); + } + + private abstract static class AbstractHeaderFooterOperationModeHandler extends OperationModeHandler { + + /** + * Called when the player receives a {@link HeaderAndFooter} packet. + *

+ * This method is called before this {@link NewTabOverlayHandler} executes its own logic to update the + * server player list info. + */ + abstract PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet); + + /** + * Called when the player switches the server. + *

+ * This method is called before this {@link NewTabOverlayHandler} executes its own logic to clear the + * server player list info. + */ + abstract void onServerSwitch(); + + abstract void update(); + + final void invalidate() { + getTabOverlay().invalidate(); + onDeactivated(); + } + + /** + * Called when this {@link OperationModeHandler} is deactivated. + *

+ * This method must put the client player list in the state expected by {@link #onActivated(AbstractHeaderFooterOperationModeHandler)}. It must + * especially remove all custom entries and players must be part of the correct teams. + */ + abstract void onDeactivated(); + + /** + * Called when this {@link OperationModeHandler} becomes the active one. + *

+ * State of the player list when this method is called: + * - there are no custom entries on the client + * - all entries from {@link #serverPlayerListListed} are known to the client, but might not be listed + * - player list header/ footer may be wrong + *

+ * Additional information about the state of the player list may be obtained from the previous handler + * + * @param previous previous handler + */ + abstract void onActivated(AbstractHeaderFooterOperationModeHandler previous); + } + + private abstract static class AbstractContentTabOverlay implements TabOverlayHandle { + private boolean valid = true; + + @Override + public boolean isValid() { + return valid; + } + + final void invalidate() { + valid = false; + } + } + + private abstract static class AbstractHeaderFooterTabOverlay implements TabOverlayHandle { + private boolean valid = true; + + @Override + public boolean isValid() { + return valid; + } + + final void invalidate() { + valid = false; + } + } + + private final class PassThroughContentHandler extends AbstractContentOperationModeHandler { + + @Override + protected PassThroughContentTabOverlay createTabOverlay() { + return new PassThroughContentTabOverlay(); + } + + @Override + PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { + return PacketListenerResult.PASS; + } + + @Override + void onServerSwitch() { + sendPacket(new HeaderAndFooter(EMPTY_JSON_TEXT, EMPTY_JSON_TEXT)); + } + + @Override + void update() { + // nothing to do + } + + @Override + void onDeactivated() { + // nothing to do + } + + @Override + void onActivated(AbstractContentOperationModeHandler previous) { + if (previous instanceof PassThroughContentHandler) { + // we're lucky, nothing to do + return; + } + + // update visibility + if (!serverPlayerListListed.isEmpty()) { + List items = new ArrayList<>(serverPlayerListListed.size()); + for (Object2BooleanMap.Entry entry : serverPlayerListListed.object2BooleanEntrySet()) { + + UpsertPlayerInfo.Entry item = new UpsertPlayerInfo.Entry(entry.getKey()); + item.setListed(entry.getBooleanValue()); + items.add(item); + } + UpsertPlayerInfo packet = new UpsertPlayerInfo(); + packet.addAction(UpsertPlayerInfo.Action.UPDATE_LISTED); + packet.addAllEntries(items); + sendPacket(packet); + } + } + } + + private static final class PassThroughContentTabOverlay extends AbstractContentTabOverlay { + + } + + private final class PassThroughHeaderFooterHandler extends AbstractHeaderFooterOperationModeHandler { + + @Override + protected PassThroughHeaderFooterTabOverlay createTabOverlay() { + return new PassThroughHeaderFooterTabOverlay(); + } + + @Override + PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { + return PacketListenerResult.PASS; + } + + @Override + void onServerSwitch() { + sendPacket(new HeaderAndFooter(EMPTY_JSON_TEXT, EMPTY_JSON_TEXT)); + } + + @Override + void update() { + // nothing to do + } + + @Override + void onDeactivated() { + // nothing to do + } + + @Override + void onActivated(AbstractHeaderFooterOperationModeHandler previous) { + if (previous instanceof PassThroughHeaderFooterHandler) { + // we're lucky, nothing to do + return; + } + + // fix header/ footer + sendPacket(new HeaderAndFooter(serverHeader != null ? serverHeader : EMPTY_JSON_TEXT, serverFooter != null ? serverFooter : EMPTY_JSON_TEXT)); + } + } + + private static final class PassThroughHeaderFooterTabOverlay extends AbstractHeaderFooterTabOverlay { + + } + + private abstract class CustomContentTabOverlayHandler extends AbstractContentOperationModeHandler { + + @Nonnull + BitSet usedSlots; + BitSet dirtySlots; + final SlotState[] slotState; + /** + * Uuid of the player list entry used for the slot. + */ + final UUID[] slotUuid; + /** + * Username of the player list entry used for the slot. + */ + final String[] slotUsername; + + private final List itemQueueAddPlayer; + private final List itemQueueRemovePlayer; + private final List itemQueueUpdateDisplayName; + private final List itemQueueUpdatePing; + + private final boolean experimentalTabCompleteSmileys = isExperimentalTabCompleteSmileys(); + + private CustomContentTabOverlayHandler() { + this.dirtySlots = new BitSet(80); + this.usedSlots = SIZE_TO_USED_SLOTS[0]; + this.slotState = new SlotState[80]; + Arrays.fill(this.slotState, SlotState.UNUSED); + this.slotUuid = new UUID[80]; + this.slotUsername = new String[80]; + this.itemQueueAddPlayer = new ArrayList<>(80); + this.itemQueueRemovePlayer = new ArrayList<>(80); + this.itemQueueUpdateDisplayName = new ArrayList<>(80); + this.itemQueueUpdatePing = new ArrayList<>(80); + } + + @Override + PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { + + if (packet.getActions().contains(UpsertPlayerInfo.Action.UPDATE_LISTED)) { + for (UpsertPlayerInfo.Entry entry : packet.getEntries()) { + entry.setListed(false); + } + } + return PacketListenerResult.MODIFIED; + } + + private String getCustomSlotUsername(int index) { + if (experimentalTabCompleteSmileys) { + return CUSTOM_SLOT_USERNAME_SMILEYS[index]; + } else { + return CUSTOM_SLOT_USERNAME[index]; + } + } + + @Override + void onServerSwitch() { + + createTeamsIfNecessary(); + } + + @Override + void onActivated(AbstractContentOperationModeHandler previous) { + + // make all players unlisted + if (!serverPlayerListListed.isEmpty()) { + List items = new ArrayList<>(serverPlayerListListed.size()); + for (Object2BooleanMap.Entry entry : serverPlayerListListed.object2BooleanEntrySet()) { + UpsertPlayerInfo.Entry item = new UpsertPlayerInfo.Entry(entry.getKey()); + item.setListed(false); + items.add(item); + } + UpsertPlayerInfo packet = new UpsertPlayerInfo(); + packet.addAction(UpsertPlayerInfo.Action.UPDATE_LISTED); + packet.addAllEntries(items); + sendPacket(packet); + } + + createTeamsIfNecessary(); + } + + private void createTeamsIfNecessary() { + // create teams if not already created + if (!hasCreatedCustomTeams) { + hasCreatedCustomTeams = true; + + for (int i = 0; i < 80; i++) { + sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[i], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[i], CUSTOM_SLOT_USERNAME_SMILEYS[i]})); + } + } + } + + @Override + void onDeactivated() { + int customSlots = 0; + for (int index = 0; index < 80; index++) { + if (slotState[index] != SlotState.UNUSED) { + customSlots++; + } + } + + int i = 0; + if (customSlots > 0) { + UUID[] uuids = new UUID[customSlots]; + for (int index = 0; index < 80; index++) { + // switch slot from custom to unused + if (slotState[index] == SlotState.CUSTOM) { + uuids[i++] = slotUuid[index]; + } + } + RemovePlayerInfo packet = new RemovePlayerInfo(); + packet.setProfilesToRemove(Arrays.asList(uuids)); + sendPacket(packet); + } + } + + @Override + void update() { + T tabOverlay = getTabOverlay(); + + if (tabOverlay.dirtyFlagSize) { + tabOverlay.dirtyFlagSize = false; + updateSize(); + } + + // update icons + dirtySlots.orAndClear(tabOverlay.dirtyFlagsIcon); + for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { + if (slotState[index] == SlotState.CUSTOM) { + // remove item + itemQueueRemovePlayer.add(slotUuid[index]); + slotState[index] = SlotState.UNUSED; + } + + if (usedSlots.get(index)) { + Icon icon = tabOverlay.icon[index]; + UUID customSlotUuid; + if (icon.isAlex()) { + customSlotUuid = CUSTOM_SLOT_UUID_ALEX[index]; + } else { // steve + customSlotUuid = CUSTOM_SLOT_UUID_STEVE[index]; + } + tabOverlay.dirtyFlagsText.clear(index); + tabOverlay.dirtyFlagsPing.clear(index); + slotState[index] = SlotState.CUSTOM; + slotUuid[index] = customSlotUuid; + UpsertPlayerInfo.Entry item = new UpsertPlayerInfo.Entry(customSlotUuid); + GameProfile profile = new GameProfile(customSlotUuid, slotUsername[index] = getCustomSlotUsername(index), toPropertiesList(icon.getTextureProperty())); + item.setProfile(profile); + item.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); + item.setLatency(tabOverlay.ping[index]); + item.setGameMode(0); + item.setListed(true); + itemQueueAddPlayer.add(item); + } + } + + // update text + dirtySlots.copyAndClear(tabOverlay.dirtyFlagsText); + for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { + if (slotState[index] != SlotState.UNUSED) { + UpsertPlayerInfo.Entry item = new UpsertPlayerInfo.Entry(slotUuid[index]); + item.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); + itemQueueUpdateDisplayName.add(item); + } + } + + // update ping + dirtySlots.copyAndClear(tabOverlay.dirtyFlagsPing); + for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { + if (slotState[index] != SlotState.UNUSED) { + UpsertPlayerInfo.Entry item = new UpsertPlayerInfo.Entry(slotUuid[index]); + item.setLatency(tabOverlay.ping[index]); + itemQueueUpdatePing.add(item); + } + } + + dirtySlots.clear(); + + // send packets + sendQueuedItems(); + } + + private void sendQueuedItems() { + if (!itemQueueRemovePlayer.isEmpty()) { + RemovePlayerInfo packet = new RemovePlayerInfo(); + packet.setProfilesToRemove(itemQueueRemovePlayer); + sendPacket(packet); + itemQueueRemovePlayer.clear(); + } + if (!itemQueueAddPlayer.isEmpty()) { + UpsertPlayerInfo packet = new UpsertPlayerInfo(); + packet.addAllActions(EnumSet.of(UpsertPlayerInfo.Action.ADD_PLAYER, UpsertPlayerInfo.Action.UPDATE_DISPLAY_NAME, UpsertPlayerInfo.Action.UPDATE_LATENCY, UpsertPlayerInfo.Action.UPDATE_LISTED)); + packet.addAllEntries(itemQueueAddPlayer); + sendPacket(packet); + itemQueueAddPlayer.clear(); + } + if (!itemQueueUpdateDisplayName.isEmpty()) { + UpsertPlayerInfo packet = new UpsertPlayerInfo(); + packet.addAction(UpsertPlayerInfo.Action.UPDATE_DISPLAY_NAME); + packet.addAllEntries(itemQueueUpdateDisplayName); + sendPacket(packet); + itemQueueUpdateDisplayName.clear(); + } + if (!itemQueueUpdatePing.isEmpty()) { + UpsertPlayerInfo packet = new UpsertPlayerInfo(); + packet.addAction(UpsertPlayerInfo.Action.UPDATE_LATENCY); + packet.addAllEntries(itemQueueUpdatePing); + sendPacket(packet); + itemQueueUpdatePing.clear(); + } + } + + /** + * Updates the usedSlots BitSet. Sets the {@link #dirtySlots uuid dirty flag} for all added + * and removed slots. + */ + abstract void updateSize(); + } + + private boolean isExperimentalTabCompleteSmileys() { + return BungeeTabListPlus.getInstance().getConfig().experimentalTabCompleteSmileys; + } + + private abstract class CustomContentTabOverlay extends AbstractContentTabOverlay implements TabOverlayHandle.BatchModifiable { + final Icon[] icon; + final String[] text; + final int[] ping; + + final AtomicInteger batchUpdateRecursionLevel; + volatile boolean dirtyFlagSize; + final ConcurrentBitSet dirtyFlagsIcon; + final ConcurrentBitSet dirtyFlagsText; + final ConcurrentBitSet dirtyFlagsPing; + + private CustomContentTabOverlay() { + this.icon = new Icon[80]; + Arrays.fill(this.icon, Icon.DEFAULT_STEVE); + this.text = new String[80]; + Arrays.fill(this.text, EMPTY_JSON_TEXT); + this.ping = new int[80]; + this.batchUpdateRecursionLevel = new AtomicInteger(0); + this.dirtyFlagSize = true; + this.dirtyFlagsIcon = new ConcurrentBitSet(80); + this.dirtyFlagsText = new ConcurrentBitSet(80); + this.dirtyFlagsPing = new ConcurrentBitSet(80); + } + + @Override + public void beginBatchModification() { + if (isValid()) { + if (batchUpdateRecursionLevel.incrementAndGet() < 0) { + throw new AssertionError("Recursion level overflow"); + } + } + } + + @Override + public void completeBatchModification() { + if (isValid()) { + int level = batchUpdateRecursionLevel.decrementAndGet(); + if (level == 0) { + scheduleUpdate(); + } else if (level < 0) { + throw new AssertionError("Recursion level underflow"); + } + } + } + + void scheduleUpdateIfNotInBatch() { + if (batchUpdateRecursionLevel.get() == 0) { + scheduleUpdate(); + } + } + + void setIconInternal(int index, @Nonnull @NonNull Icon icon) { + if (!icon.equals(this.icon[index])) { + this.icon[index] = icon; + dirtyFlagsIcon.set(index); + scheduleUpdateIfNotInBatch(); + } + } + + void setTextInternal(int index, @Nonnull @NonNull String text) { + String jsonText = ChatFormat.formattedTextToJson(text); + if (!jsonText.equals(this.text[index])) { + this.text[index] = jsonText; + dirtyFlagsText.set(index); + scheduleUpdateIfNotInBatch(); + } + } + + void setPingInternal(int index, int ping) { + if (ping != this.ping[index]) { + this.ping[index] = ping; + dirtyFlagsPing.set(index); + scheduleUpdateIfNotInBatch(); + } + } + } + + private class RectangularSizeHandler extends CustomContentTabOverlayHandler { + + @Override + void updateSize() { + RectangularTabOverlayImpl tabOverlay = getTabOverlay(); + RectangularTabOverlay.Dimension size = tabOverlay.getSize(); + BitSet newUsedSlots = DIMENSION_TO_USED_SLOTS.get(size); + dirtySlots.orXor(usedSlots, newUsedSlots); + usedSlots = newUsedSlots; + } + + @Override + protected RectangularTabOverlayImpl createTabOverlay() { + return new RectangularTabOverlayImpl(); + } + } + + private class RectangularTabOverlayImpl extends CustomContentTabOverlay implements RectangularTabOverlay { + + @Nonnull + private Dimension size; + + private RectangularTabOverlayImpl() { + Optional dimensionZero = getSupportedSizes().stream().filter(size -> size.getSize() == 0).findAny(); + if (!dimensionZero.isPresent()) { + throw new AssertionError(); + } + this.size = dimensionZero.get(); + } + + @Nonnull + @Override + public Dimension getSize() { + return size; + } + + @Override + public Collection getSupportedSizes() { + return DIMENSION_TO_USED_SLOTS.keySet(); + } + + @Override + public void setSize(@Nonnull Dimension size) { + if (!getSupportedSizes().contains(size)) { + throw new IllegalArgumentException("Unsupported size " + size); + } + if (isValid() && !this.size.equals(size)) { + BitSet oldUsedSlots = DIMENSION_TO_USED_SLOTS.get(this.size); + BitSet newUsedSlots = DIMENSION_TO_USED_SLOTS.get(size); + for (int index = newUsedSlots.nextSetBit(0); index >= 0; index = newUsedSlots.nextSetBit(index + 1)) { + if (!oldUsedSlots.get(index)) { + icon[index] = Icon.DEFAULT_STEVE; + text[index] = EMPTY_JSON_TEXT; + ping[index] = 0; + } + } + this.size = size; + this.dirtyFlagSize = true; + scheduleUpdateIfNotInBatch(); + for (int index = oldUsedSlots.nextSetBit(0); index >= 0; index = oldUsedSlots.nextSetBit(index + 1)) { + if (!newUsedSlots.get(index)) { + icon[index] = Icon.DEFAULT_STEVE; + text[index] = EMPTY_JSON_TEXT; + ping[index] = 0; + } + } + } + } + + @Override + public void setSlot(int column, int row, @Nullable UUID uuid, @Nonnull Icon icon, @Nonnull String text, int ping) { + setSlot(column, row, icon, text, ping); + } + + @Override + public void setSlot(int column, int row, @Nonnull Icon icon, @Nonnull String text, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(column, size.getColumns(), "column"); + Preconditions.checkElementIndex(row, size.getRows(), "row"); + beginBatchModification(); + try { + int index = index(column, row); + setIconInternal(index, icon); + setTextInternal(index, text); + setPingInternal(index, ping); + } finally { + completeBatchModification(); + } + } + } + + @Override + public void setUuid(int column, int row, @Nullable UUID uuid) { + // no op + } + + @Override + public void setIcon(int column, int row, @Nonnull Icon icon) { + if (isValid()) { + Preconditions.checkElementIndex(column, size.getColumns(), "column"); + Preconditions.checkElementIndex(row, size.getRows(), "row"); + setIconInternal(index(column, row), icon); + } + } + + @Override + public void setText(int column, int row, @Nonnull String text) { + if (isValid()) { + Preconditions.checkElementIndex(column, size.getColumns(), "column"); + Preconditions.checkElementIndex(row, size.getRows(), "row"); + setTextInternal(index(column, row), text); + } + } + + @Override + public void setPing(int column, int row, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(column, size.getColumns(), "column"); + Preconditions.checkElementIndex(row, size.getRows(), "row"); + setPingInternal(index(column, row), ping); + } + } + } + + private class SimpleOperationModeHandler extends CustomContentTabOverlayHandler { + + private int size = 0; + + @Override + void updateSize() { + int newSize = getTabOverlay().size; + if (newSize > size) { + dirtySlots.set(size, newSize); + } else if (newSize < size) { + dirtySlots.set(newSize, size); + } + usedSlots = SIZE_TO_USED_SLOTS[newSize]; + size = newSize; + } + + @Override + protected SimpleTabOverlayImpl createTabOverlay() { + return new SimpleTabOverlayImpl(); + } + } + + private class SimpleTabOverlayImpl extends CustomContentTabOverlay implements SimpleTabOverlay { + int size = 0; + + @Override + public int getSize() { + return size; + } + + @Override + public int getMaxSize() { + return 80; + } + + @Override + public void setSize(int size) { + if (size < 0 || size > 80) { + throw new IllegalArgumentException("size"); + } + this.size = size; + dirtyFlagSize = true; + scheduleUpdateIfNotInBatch(); + } + + @Override + public void setSlot(int index, @Nullable UUID uuid, @Nonnull Icon icon, @Nonnull String text, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(index, size, "index"); + beginBatchModification(); + try { + setIconInternal(index, icon); + setTextInternal(index, text); + setPingInternal(index, ping); + } finally { + completeBatchModification(); + } + } + } + + @Override + public void setSlot(int index, @Nonnull Icon icon, @Nonnull String text, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(index, size, "index"); + beginBatchModification(); + try { + setIconInternal(index, icon); + setTextInternal(index, text); + setPingInternal(index, ping); + } finally { + completeBatchModification(); + } + } + } + + @Override + public void setUuid(int index, UUID uuid) { + // no op + } + + @Override + public void setIcon(int index, @Nonnull @NonNull Icon icon) { + if (isValid()) { + Preconditions.checkElementIndex(index, size, "index"); + setIconInternal(index, icon); + } + } + + @Override + public void setText(int index, @Nonnull @NonNull String text) { + if (isValid()) { + Preconditions.checkElementIndex(index, size, "index"); + setTextInternal(index, text); + } + } + + @Override + public void setPing(int index, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(index, size, "index"); + setPingInternal(index, ping); + } + } + } + + private final class CustomHeaderAndFooterOperationModeHandler extends AbstractHeaderFooterOperationModeHandler { + + @Override + protected CustomHeaderAndFooterImpl createTabOverlay() { + return new CustomHeaderAndFooterImpl(); + } + + @Override + PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { + return PacketListenerResult.CANCEL; + } + + @Override + void onServerSwitch() { + // do nothing + } + + @Override + void onDeactivated() { + //do nothing + } + + @Override + void onActivated(AbstractHeaderFooterOperationModeHandler previous) { + // remove header/ footer + sendPacket(new HeaderAndFooter(EMPTY_JSON_TEXT, EMPTY_JSON_TEXT)); + } + + @Override + void update() { + CustomHeaderAndFooterImpl tabOverlay = getTabOverlay(); + if (tabOverlay.headerOrFooterDirty) { + tabOverlay.headerOrFooterDirty = false; + sendPacket(new HeaderAndFooter(tabOverlay.header, tabOverlay.footer)); + } + } + } + + private final class CustomHeaderAndFooterImpl extends AbstractHeaderFooterTabOverlay implements HeaderAndFooterHandle { + private String header = EMPTY_JSON_TEXT; + private String footer = EMPTY_JSON_TEXT; + + private volatile boolean headerOrFooterDirty = false; + + final AtomicInteger batchUpdateRecursionLevel = new AtomicInteger(0); + + @Override + public void beginBatchModification() { + if (isValid()) { + if (batchUpdateRecursionLevel.incrementAndGet() < 0) { + throw new AssertionError("Recursion level overflow"); + } + } + } + + @Override + public void completeBatchModification() { + if (isValid()) { + int level = batchUpdateRecursionLevel.decrementAndGet(); + if (level == 0) { + scheduleUpdate(); + } else if (level < 0) { + throw new AssertionError("Recursion level underflow"); + } + } + } + + void scheduleUpdateIfNotInBatch() { + if (batchUpdateRecursionLevel.get() == 0) { + scheduleUpdate(); + } + } + + @Override + public void setHeaderFooter(@Nullable String header, @Nullable String footer) { + this.header = ChatFormat.formattedTextToJson(header); + this.footer = ChatFormat.formattedTextToJson(footer); + headerOrFooterDirty = true; + scheduleUpdateIfNotInBatch(); + } + + @Override + public void setHeader(@Nullable String header) { + this.header = ChatFormat.formattedTextToJson(header); + headerOrFooterDirty = true; + scheduleUpdateIfNotInBatch(); + } + + @Override + public void setFooter(@Nullable String footer) { + this.footer = ChatFormat.formattedTextToJson(footer); + headerOrFooterDirty = true; + scheduleUpdateIfNotInBatch(); + } + } + + private static int index(int column, int row) { + return column * 20 + row; + } + + private static List toPropertiesList(ProfileProperty textureProperty) { + if (textureProperty == null) { + return new ArrayList<>(); + } else if (textureProperty.isSigned()) { + return List.of(new GameProfile.Property(textureProperty.getName(), textureProperty.getValue(), textureProperty.getSignature())); + } else { + return List.of(new GameProfile.Property(textureProperty.getName(), textureProperty.getValue(), "")); + } + } + + private static Team createPacketTeamCreate(String name, String displayName, String prefix, String suffix, String nameTagVisibility, String collisionRule, int color, byte friendlyFire, String[] players) { + Team team = new Team(); + team.setName(name); + team.setMode((byte) 0); + team.setDisplayName(displayName); + team.setPrefix(prefix); + team.setSuffix(suffix); + team.setNameTagVisibility(nameTagVisibility); + team.setCollisionRule(collisionRule); + team.setColor(color); + team.setFriendlyFire(friendlyFire); + team.setPlayers(players); + return team; + } + + private enum SlotState { + UNUSED, CUSTOM + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/TabOverlayHandlerImpl.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/TabOverlayHandlerImpl.java index a602842f..1459af43 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/TabOverlayHandlerImpl.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/TabOverlayHandlerImpl.java @@ -1,75 +1,85 @@ -/* - * Copyright (C) 2020 Florian Stober - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package codecrafter47.bungeetablistplus.handler; - -import codecrafter47.bungeetablistplus.BungeeTabListPlus; -import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; -import codecrafter47.bungeetablistplus.util.ReflectionUtil; -import com.velocitypowered.api.network.ProtocolVersion; -import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.proxy.protocol.MinecraftPacket; -import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; -import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; -import lombok.SneakyThrows; - -import java.util.UUID; -import java.util.concurrent.Executor; -import java.util.logging.Logger; - -public class TabOverlayHandlerImpl extends AbstractTabOverlayHandler { - - private final Player player; - - public TabOverlayHandlerImpl(Logger logger, Executor eventLoopExecutor, UUID viewerUuid, Player player, boolean is18, boolean is13OrLater, boolean is119OrLater) { - super(logger, eventLoopExecutor, viewerUuid, is18, is13OrLater, is119OrLater); - this.player = player; - } - - @SneakyThrows - @Override - protected void sendPacket(MinecraftPacket packet) { - ReflectionUtil.getChannelWrapper(player).write(packet); - } - - @Override - protected boolean isExperimentalTabCompleteSmileys() { - return BungeeTabListPlus.getInstance().getConfig().experimentalTabCompleteSmileys; - } - - @Override - protected boolean isExperimentalTabCompleteFixForTabSize80() { - return BungeeTabListPlus.getInstance().getConfig().experimentalTabCompleteFixForTabSize80; - } - - @SneakyThrows - @Override - protected boolean isUsingAltRespawn() { - return player.getProtocolVersion().getProtocol() >= 735 - && ProtocolVersion.isSupported(736); - } - - @Override - public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { - return PacketListenerResult.PASS; - } - - @Override - public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet) { - return PacketListenerResult.PASS; - } -} +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.handler; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; +import codecrafter47.bungeetablistplus.util.ReflectionUtil; +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; +import lombok.SneakyThrows; + +import java.util.UUID; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +public class TabOverlayHandlerImpl extends AbstractTabOverlayHandler { + + private final Player player; + + private boolean logVersionMismatch = false; + + public TabOverlayHandlerImpl(Logger logger, Executor eventLoopExecutor, UUID viewerUuid, Player player, boolean is18, boolean is13OrLater, boolean is119OrLater) { + super(logger, eventLoopExecutor, viewerUuid, is18, is13OrLater, is119OrLater); + this.player = player; + } + + @SneakyThrows + @Override + protected void sendPacket(MinecraftPacket packet) { + if ((packet instanceof UpsertPlayerInfo) && (player.getProtocolVersion().getProtocol() >= 761)) { + // error + if (!logVersionMismatch) { + logVersionMismatch = true; + this.logger.warning("Cannot correctly update tablist for player " + player.getUsername() + "\nThe client and server versions do not match. Client < 1.19.3, server >= 1.19.3.\nUse ViaVersion on the spigot server for the best experience."); + } + } else { + ReflectionUtil.getChannelWrapper(player).write(packet); + } + } + + @Override + protected boolean isExperimentalTabCompleteSmileys() { + return BungeeTabListPlus.getInstance().getConfig().experimentalTabCompleteSmileys; + } + + @Override + protected boolean isExperimentalTabCompleteFixForTabSize80() { + return BungeeTabListPlus.getInstance().getConfig().experimentalTabCompleteFixForTabSize80; + } + + @SneakyThrows + @Override + protected boolean isUsingAltRespawn() { + return player.getProtocolVersion().getProtocol() >= 735 + && ProtocolVersion.isSupported(736); + } + + @Override + public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { + return PacketListenerResult.PASS; + } + + @Override + public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet) { + return PacketListenerResult.PASS; + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/listener/TabListListener.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/listener/TabListListener.java index 4e52c05a..437db0b2 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/listener/TabListListener.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/listener/TabListListener.java @@ -1,97 +1,99 @@ -/* - * Copyright (C) 2020 Florian Stober - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package codecrafter47.bungeetablistplus.listener; - -import codecrafter47.bungeetablistplus.BungeeTabListPlus; -import codecrafter47.bungeetablistplus.player.VelocityPlayer; -import codecrafter47.bungeetablistplus.tablist.ExcludedServersTabOverlayProvider; -import codecrafter47.bungeetablistplus.util.GeyserCompat; -import com.velocitypowered.api.event.PostOrder; -import com.velocitypowered.api.event.Subscribe; -import com.velocitypowered.api.event.connection.DisconnectEvent; -import com.velocitypowered.api.event.connection.PostLoginEvent; -import com.velocitypowered.api.event.proxy.ProxyReloadEvent; -import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.proxy.connection.client.ConnectedPlayer; -import de.codecrafter47.taboverlay.TabView; -import de.codecrafter47.taboverlay.config.platform.EventListener; -import net.kyori.adventure.text.Component; - -import java.util.concurrent.TimeUnit; - -public class TabListListener { - - private final BungeeTabListPlus btlp; - - public TabListListener(BungeeTabListPlus btlp) { - this.btlp = btlp; - } - - @Subscribe(order = PostOrder.LATE) - public void onPlayerJoin(PostLoginEvent e) { - try { - // Hacks -> Remove everyone from all tablists - btlp.getProxy().getScheduler().buildTask(btlp.getPlugin(), () -> { - for(Player tmpPlayer : btlp.getProxy().getAllPlayers()){ - tmpPlayer.getTabList().clearAll(); - } - }).delay(2, TimeUnit.SECONDS).schedule(); - - VelocityPlayer player = btlp.getVelocityPlayerProvider().onPlayerConnected(e.getPlayer()); - - if (GeyserCompat.isBedrockPlayer(e.getPlayer().getUniqueId())) { - return; - } - - TabView tabView = btlp.getTabViewManager().onPlayerJoin(e.getPlayer()); - tabView.getTabOverlayProviders().addProvider(new ExcludedServersTabOverlayProvider(player, btlp)); - for (EventListener listener : btlp.getListeners()) { - listener.onTabViewAdded(tabView, player); - } - } catch (Throwable th) { - BungeeTabListPlus.getInstance().reportError(th); - } - } - - @Subscribe(order = PostOrder.FIRST) - public void onPlayerDisconnect(DisconnectEvent e) { - try { - btlp.getVelocityPlayerProvider().onPlayerDisconnected(e.getPlayer()); - - if (GeyserCompat.isBedrockPlayer(e.getPlayer().getUniqueId())) { - return; - } - - TabView tabView = btlp.getTabViewManager().onPlayerDisconnect(e.getPlayer()); - tabView.deactivate(); - for (EventListener listener : btlp.getListeners()) { - listener.onTabViewRemoved(tabView); - } - - // hack to revert changes from https://github.com/SpigotMC/BungeeCord/commit/830f18a35725f637d623594eaaad50b566376e59 - e.getPlayer().getCurrentServer().ifPresent(server -> server.getPlayer().disconnect(Component.text("Quitting"))); - ((ConnectedPlayer) e.getPlayer()).setConnectedServer(null); - } catch (Throwable th) { - BungeeTabListPlus.getInstance().reportError(th); - } - } - - @Subscribe - public void onReload(ProxyReloadEvent event) { - btlp.reload(); - } -} +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package codecrafter47.bungeetablistplus.listener; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.player.VelocityPlayer; +import codecrafter47.bungeetablistplus.tablist.ExcludedServersTabOverlayProvider; +import codecrafter47.bungeetablistplus.util.GeyserCompat; +import com.velocitypowered.api.event.PostOrder; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.DisconnectEvent; +import com.velocitypowered.api.event.connection.PostLoginEvent; +import com.velocitypowered.api.event.proxy.ProxyReloadEvent; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import de.codecrafter47.taboverlay.TabView; +import de.codecrafter47.taboverlay.config.platform.EventListener; +import net.kyori.adventure.text.Component; + +import java.util.concurrent.TimeUnit; + +public class TabListListener { + + private final BungeeTabListPlus btlp; + + public TabListListener(BungeeTabListPlus btlp) { + this.btlp = btlp; + } + + @Subscribe(order = PostOrder.LATE) + public void onPlayerJoin(PostLoginEvent e) { + try { + // Hacks -> Remove everyone from all tablists + btlp.getProxy().getScheduler().buildTask(btlp.getPlugin(), () -> { + for(Player tmpPlayer : btlp.getProxy().getAllPlayers()){ + tmpPlayer.getTabList().clearAll(); + } + }).delay(2, TimeUnit.SECONDS).schedule(); + + VelocityPlayer player = btlp.getVelocityPlayerProvider().onPlayerConnected(e.getPlayer()); + + if (GeyserCompat.isBedrockPlayer(e.getPlayer().getUniqueId())) { + return; + } + + TabView tabView = btlp.getTabViewManager().onPlayerJoin(e.getPlayer()); + tabView.getTabOverlayProviders().addProvider(new ExcludedServersTabOverlayProvider(player, btlp)); + for (EventListener listener : btlp.getListeners()) { + listener.onTabViewAdded(tabView, player); + } + } catch (Throwable th) { + BungeeTabListPlus.getInstance().reportError(th); + } + } + + @Subscribe(order = PostOrder.FIRST) + public void onPlayerDisconnect(DisconnectEvent e) { + try { + btlp.getVelocityPlayerProvider().onPlayerDisconnected(e.getPlayer()); + + if (GeyserCompat.isBedrockPlayer(e.getPlayer().getUniqueId())) { + return; + } + + TabView tabView = btlp.getTabViewManager().onPlayerDisconnect(e.getPlayer()); + if (tabView != null) { + tabView.deactivate(); + for (EventListener listener : btlp.getListeners()) { + listener.onTabViewRemoved(tabView); + } + } + + // hack to revert changes from https://github.com/SpigotMC/BungeeCord/commit/830f18a35725f637d623594eaaad50b566376e59 + e.getPlayer().getCurrentServer().ifPresent(server -> server.getPlayer().disconnect(Component.empty())); + ((ConnectedPlayer) e.getPlayer()).setConnectedServer(null); + } catch (Throwable th) { + BungeeTabListPlus.getInstance().reportError(th); + } + } + + @Subscribe + public void onReload(ProxyReloadEvent event) { + btlp.reload(); + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/TabViewManager.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/TabViewManager.java index c052ab17..97279323 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/TabViewManager.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/TabViewManager.java @@ -1,156 +1,148 @@ -/* - * Copyright (C) 2020 Florian Stober - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package codecrafter47.bungeetablistplus.managers; - -import codecrafter47.bungeetablistplus.BungeeTabListPlus; -import codecrafter47.bungeetablistplus.handler.*; -import codecrafter47.bungeetablistplus.protocol.PacketHandler; -import codecrafter47.bungeetablistplus.protocol.PacketListener; -import codecrafter47.bungeetablistplus.util.GeyserCompat; -import codecrafter47.bungeetablistplus.util.ReflectionUtil; -import codecrafter47.bungeetablistplus.version.ProtocolVersionProvider; -import com.velocitypowered.api.event.Subscribe; -import com.velocitypowered.api.event.player.ServerPostConnectEvent; -import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.proxy.connection.MinecraftConnection; -import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; -import com.velocitypowered.proxy.network.Connections; -import de.codecrafter47.taboverlay.TabView; -import de.codecrafter47.taboverlay.config.misc.ChildLogger; -import de.codecrafter47.taboverlay.handler.TabOverlayHandler; -import io.netty.channel.EventLoop; - -import javax.annotation.Nullable; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executor; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class TabViewManager { - - private final BungeeTabListPlus btlp; - private final ProtocolVersionProvider protocolVersionProvider; - - private final Map playerTabViewMap = new ConcurrentHashMap<>(); - - public TabViewManager(BungeeTabListPlus btlp, ProtocolVersionProvider protocolVersionProvider) { - this.btlp = btlp; - this.protocolVersionProvider = protocolVersionProvider; - btlp.getPlugin().getProxy().getEventManager().register(btlp.getPlugin(), this); - } - - public TabView onPlayerJoin(Player player) { - if (playerTabViewMap.containsKey(player)) { - throw new AssertionError("Duplicate PostLoginEvent for player " + player.getUsername()); - } - - if (player.getCurrentServer().isPresent()) { - throw new AssertionError("Player already connected to server in PostLoginEvent: " + player.getUsername()); - } - - PlayerTabView tabView = createTabView(player); - playerTabViewMap.put(player, tabView); - return tabView; - } - - public TabView onPlayerDisconnect(Player player) { - PlayerTabView tabView = playerTabViewMap.remove(player); - - if (null == tabView) { - throw new AssertionError("Received PlayerDisconnectEvent for non-existent player " + player.getUsername()); - } - - tabView.deactivate(); - - return tabView; - } - - @Subscribe - public void onServerConnected(ServerPostConnectEvent event) { - if (GeyserCompat.isBedrockPlayer(event.getPlayer().getUniqueId())) { - return; - } - try { - Player player = event.getPlayer(); - - PlayerTabView tabView = playerTabViewMap.get(player); - - if (tabView == null) { - throw new AssertionError("Received ServerSwitchEvent for non-existent player " + player.getUsername()); - } - - VelocityServerConnection server = (VelocityServerConnection) event.getPlayer().getCurrentServer().orElse(null); - - MinecraftConnection wrapper = server.getConnection(); - - PacketHandler packetHandler = tabView.packetHandler; - PacketListener packetListener = new PacketListener(server, packetHandler, player); - wrapper.getChannel().pipeline().addBefore(Connections.HANDLER, "btlp-packet-listener", packetListener); - - packetHandler.onServerSwitch(protocolVersionProvider.has113OrLater(player)); - - } catch (Exception ex) { - btlp.getLogger().log(Level.SEVERE, "Failed to inject packet listener", ex); - } - } - - @Nullable - public TabView getTabView(Player player) { - return playerTabViewMap.get(player); - } - - private PlayerTabView createTabView(Player player) { - try { - TabOverlayHandler tabOverlayHandler; - PacketHandler packetHandler; - - Logger logger = new ChildLogger(btlp.getLogger(), player.getUsername()); - EventLoop eventLoop = ReflectionUtil.getChannelWrapper(player).eventLoop(); - - if (protocolVersionProvider.has1193OrLater(player)) { - NewTabOverlayHandler handler = new NewTabOverlayHandler(logger, eventLoop, player); - tabOverlayHandler = handler; - packetHandler = new RewriteLogic(new GetGamemodeLogic(handler, player.getUniqueId())); - } else if (protocolVersionProvider.has18OrLater(player)) { - LowMemoryTabOverlayHandlerImpl tabOverlayHandlerImpl = new LowMemoryTabOverlayHandlerImpl(logger, eventLoop, player.getUniqueId(), player, protocolVersionProvider.is18(player), protocolVersionProvider.has113OrLater(player), protocolVersionProvider.has119OrLater(player)); - tabOverlayHandler = tabOverlayHandlerImpl; - packetHandler = new RewriteLogic(new GetGamemodeLogic(tabOverlayHandlerImpl, player.getUniqueId())); - } else { - LegacyTabOverlayHandlerImpl legacyTabOverlayHandler = new LegacyTabOverlayHandlerImpl(logger, ReflectionUtil.getTablistHandler(player).getEntries().size(), eventLoop, player, protocolVersionProvider.has113OrLater(player)); - tabOverlayHandler = legacyTabOverlayHandler; - packetHandler = legacyTabOverlayHandler; - } - - return new PlayerTabView(tabOverlayHandler, logger, btlp.getAsyncExecutor(), packetHandler); - - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new AssertionError("Failed to create tab view", e); - } - } - - private static class PlayerTabView extends TabView { - - private final PacketHandler packetHandler; - - private PlayerTabView(TabOverlayHandler tabOverlayHandler, Logger logger, Executor updateExecutor, PacketHandler packetHandler) { - super(tabOverlayHandler, logger, updateExecutor); - this.packetHandler = packetHandler; - } - } -} +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.managers; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.handler.*; +import codecrafter47.bungeetablistplus.protocol.PacketHandler; +import codecrafter47.bungeetablistplus.protocol.PacketListener; +import codecrafter47.bungeetablistplus.util.GeyserCompat; +import codecrafter47.bungeetablistplus.util.ReflectionUtil; +import codecrafter47.bungeetablistplus.version.ProtocolVersionProvider; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.player.ServerPostConnectEvent; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; +import com.velocitypowered.proxy.network.Connections; +import de.codecrafter47.taboverlay.TabView; +import de.codecrafter47.taboverlay.config.misc.ChildLogger; +import de.codecrafter47.taboverlay.handler.TabOverlayHandler; +import io.netty.channel.EventLoop; + +import javax.annotation.Nullable; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class TabViewManager { + + private final BungeeTabListPlus btlp; + private final ProtocolVersionProvider protocolVersionProvider; + + private final Map playerTabViewMap = new ConcurrentHashMap<>(); + + public TabViewManager(BungeeTabListPlus btlp, ProtocolVersionProvider protocolVersionProvider) { + this.btlp = btlp; + this.protocolVersionProvider = protocolVersionProvider; + btlp.getPlugin().getProxy().getEventManager().register(btlp.getPlugin(), this); + } + + public TabView onPlayerJoin(Player player) { + if (playerTabViewMap.containsKey(player)) { + throw new AssertionError("Duplicate PostLoginEvent for player " + player.getUsername()); + } + + if (player.getCurrentServer().isPresent()) { + throw new AssertionError("Player already connected to server in PostLoginEvent: " + player.getUsername()); + } + + PlayerTabView tabView = createTabView(player); + playerTabViewMap.put(player, tabView); + return tabView; + } + + public TabView onPlayerDisconnect(Player player) { + return playerTabViewMap.remove(player); + } + + @Subscribe + public void onServerConnected(ServerPostConnectEvent event) { + if (GeyserCompat.isBedrockPlayer(event.getPlayer().getUniqueId())) { + return; + } + try { + Player player = event.getPlayer(); + + PlayerTabView tabView = playerTabViewMap.get(player); + + if (tabView == null) { + throw new AssertionError("Received ServerSwitchEvent for non-existent player " + player.getUsername()); + } + + VelocityServerConnection server = (VelocityServerConnection) event.getPlayer().getCurrentServer().orElse(null); + + MinecraftConnection wrapper = server.getConnection(); + + PacketHandler packetHandler = tabView.packetHandler; + PacketListener packetListener = new PacketListener(server, packetHandler, player); + wrapper.getChannel().pipeline().addBefore(Connections.HANDLER, "btlp-packet-listener", packetListener); + + packetHandler.onServerSwitch(protocolVersionProvider.has113OrLater(player)); + + } catch (Exception ex) { + btlp.getLogger().log(Level.SEVERE, "Failed to inject packet listener", ex); + } + } + + @Nullable + public TabView getTabView(Player player) { + return playerTabViewMap.get(player); + } + + private PlayerTabView createTabView(Player player) { + try { + TabOverlayHandler tabOverlayHandler; + PacketHandler packetHandler; + + Logger logger = new ChildLogger(btlp.getLogger(), player.getUsername()); + EventLoop eventLoop = ReflectionUtil.getChannelWrapper(player).eventLoop(); + + if (protocolVersionProvider.has1193OrLater(player)) { + NewTabOverlayHandler handler = new NewTabOverlayHandler(logger, eventLoop, player); + tabOverlayHandler = handler; + packetHandler = new RewriteLogic(new GetGamemodeLogic(handler, player.getUniqueId())); + } else if (protocolVersionProvider.has18OrLater(player)) { + LowMemoryTabOverlayHandlerImpl tabOverlayHandlerImpl = new LowMemoryTabOverlayHandlerImpl(logger, eventLoop, player.getUniqueId(), player, protocolVersionProvider.is18(player), protocolVersionProvider.has113OrLater(player), protocolVersionProvider.has119OrLater(player)); + tabOverlayHandler = tabOverlayHandlerImpl; + packetHandler = new RewriteLogic(new GetGamemodeLogic(tabOverlayHandlerImpl, player.getUniqueId())); + } else { + LegacyTabOverlayHandlerImpl legacyTabOverlayHandler = new LegacyTabOverlayHandlerImpl(logger, ReflectionUtil.getTablistHandler(player).getEntries().size(), eventLoop, player, protocolVersionProvider.has113OrLater(player)); + tabOverlayHandler = legacyTabOverlayHandler; + packetHandler = legacyTabOverlayHandler; + } + + return new PlayerTabView(tabOverlayHandler, logger, btlp.getAsyncExecutor(), packetHandler); + + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new AssertionError("Failed to create tab view", e); + } + } + + private static class PlayerTabView extends TabView { + + private final PacketHandler packetHandler; + + private PlayerTabView(TabOverlayHandler tabOverlayHandler, Logger logger, Executor updateExecutor, PacketHandler packetHandler) { + super(tabOverlayHandler, logger, updateExecutor); + this.packetHandler = packetHandler; + } + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListener.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListener.java index 2ca2f474..d60485c4 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListener.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListener.java @@ -1,89 +1,89 @@ -/* - * Copyright (C) 2020 Florian Stober - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package codecrafter47.bungeetablistplus.protocol; - -import codecrafter47.bungeetablistplus.BungeeTabListPlus; -import codecrafter47.bungeetablistplus.util.ReflectionUtil; -import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; -import com.velocitypowered.proxy.protocol.MinecraftPacket; -import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; -import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; -import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; -import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.MessageToMessageDecoder; - -import java.util.List; - -public class PacketListener extends MessageToMessageDecoder { - private final VelocityServerConnection connection; - private final PacketHandler handler; - private final Player player; - - public PacketListener(VelocityServerConnection connection, PacketHandler handler, Player player) { - this.connection = connection; - this.handler = handler; - this.player = player; - } - - @Override - protected void decode(ChannelHandlerContext ctx, MinecraftPacket packet, List out) { - try { - if (connection.isActive()) { - if (packet != null) { - - PacketListenerResult result = PacketListenerResult.PASS; - boolean handled = false; - - if (packet instanceof Team) { - result = handler.onTeamPacket((Team) packet); - if (result == PacketListenerResult.MODIFIED) { - ReflectionUtil.getChannelWrapper(player).getChannel().write(packet); - } - if (result != PacketListenerResult.PASS) { - return; - } - } else if (packet instanceof LegacyPlayerListItem) { - result = handler.onPlayerListPacket((LegacyPlayerListItem) packet); - handled = true; - } else if (packet instanceof HeaderAndFooter) { - result = handler.onPlayerListHeaderFooterPacket((HeaderAndFooter) packet); - handled = true; - } else if (packet instanceof UpsertPlayerInfo) { - result = handler.onPlayerListUpdatePacket((UpsertPlayerInfo) packet); - handled = true; - } else if (packet instanceof RemovePlayerInfo) { - result = handler.onPlayerListRemovePacket((RemovePlayerInfo) packet); - handled = true; - } - - if (handled) { - if (result != PacketListenerResult.CANCEL) { - ReflectionUtil.getChannelWrapper(player).getChannel().write(packet); - } - return; - } - } - } - out.add(packet); - } catch (Throwable th) { - BungeeTabListPlus.getInstance().reportError(th); - } - } -} +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.protocol; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.util.ReflectionUtil; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; +import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageDecoder; + +import java.util.List; + +public class PacketListener extends MessageToMessageDecoder { + private final VelocityServerConnection connection; + private final PacketHandler handler; + private final Player player; + + public PacketListener(VelocityServerConnection connection, PacketHandler handler, Player player) { + this.connection = connection; + this.handler = handler; + this.player = player; + } + + @Override + protected void decode(ChannelHandlerContext ctx, MinecraftPacket packet, List out) { + try { + if (connection.isActive()) { + if (packet != null) { + + PacketListenerResult result = PacketListenerResult.PASS; + boolean handled = false; + + if (packet instanceof Team) { + result = handler.onTeamPacket((Team) packet); + if (result == PacketListenerResult.MODIFIED) { + ReflectionUtil.getChannelWrapper(player).getChannel().write(packet); + } + if (result != PacketListenerResult.PASS) { + return; + } + } else if (packet instanceof LegacyPlayerListItem) { + result = handler.onPlayerListPacket((LegacyPlayerListItem) packet); + handled = true; + } else if (packet instanceof HeaderAndFooter) { + result = handler.onPlayerListHeaderFooterPacket((HeaderAndFooter) packet); + handled = true; + } else if (packet instanceof UpsertPlayerInfo) { + result = handler.onPlayerListUpdatePacket((UpsertPlayerInfo) packet); + handled = true; + } else if (packet instanceof RemovePlayerInfo) { + result = handler.onPlayerListRemovePacket((RemovePlayerInfo) packet); + handled = true; + } + + if (handled) { + if (result != PacketListenerResult.CANCEL) { + ReflectionUtil.getChannelWrapper(player).getChannel().write(packet); + } + return; + } + } + } + out.add(packet); + } catch (Throwable th) { + BungeeTabListPlus.getInstance().reportError(th); + } + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/Team.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/Team.java index 3a29b7a2..ee54aa66 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/Team.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/Team.java @@ -1,220 +1,220 @@ -/* - * Copyright (C) 2018-2023 Velocity Contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package codecrafter47.bungeetablistplus.protocol; - -import com.velocitypowered.api.network.ProtocolVersion; -import com.velocitypowered.proxy.connection.MinecraftSessionHandler; -import com.velocitypowered.proxy.protocol.MinecraftPacket; -import com.velocitypowered.proxy.protocol.ProtocolUtils; -import io.netty.buffer.ByteBuf; - -public class Team implements MinecraftPacket { - - public static final byte CREATE = 0; - public static final byte REMOVE = 1; - public static final byte UPDATE_INFO = 2; - public static final byte ADD_PLAYER = 3; - public static final byte REMOVE_PLAYER = 4; - - private String name; - private byte mode; - private String displayName; - private String prefix; - private String suffix; - private String nameTagVisibility; - private String collisionRule; - private int color; - private byte friendlyFire; - private String[] players; - - public Team() { - } - - public Team(String name) { - this.name = name; - this.mode = REMOVE; - } - - public Team(String name, byte mode) { - this.name = name; - this.mode = mode; - } - - @Override - public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { - name = ProtocolUtils.readString(buf); - mode = buf.readByte(); - if (mode == CREATE || mode == UPDATE_INFO) { - displayName = ProtocolUtils.readString(buf); - if (version.compareTo(ProtocolVersion.MINECRAFT_1_13) < 0) { - prefix = ProtocolUtils.readString(buf); - suffix = ProtocolUtils.readString(buf); - } - friendlyFire = buf.readByte(); - nameTagVisibility = ProtocolUtils.readString(buf); - if (version.compareTo(ProtocolVersion.MINECRAFT_1_9) >= 0) { - collisionRule = ProtocolUtils.readString(buf); - } - color = ( version.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0 ) ? ProtocolUtils.readVarInt(buf) : buf.readByte(); - if ( version.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0 ) { - prefix = ProtocolUtils.readString(buf); - suffix = ProtocolUtils.readString(buf); - } - } - if (mode == CREATE || mode == ADD_PLAYER || mode == REMOVE_PLAYER) { - int len = ProtocolUtils.readVarInt(buf); - players = new String[len]; - for (int i = 0; i < len; i++) { - players[i] = ProtocolUtils.readString(buf); - } - } - } - - @Override - public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { - ProtocolUtils.writeString(buf, name); - buf.writeByte(mode); - if (mode == CREATE || mode == UPDATE_INFO) { - ProtocolUtils.writeString(buf, displayName); - if (version.compareTo(ProtocolVersion.MINECRAFT_1_13) < 0) { - ProtocolUtils.writeString(buf, prefix); - ProtocolUtils.writeString(buf, suffix); - } - buf.writeByte(friendlyFire); - ProtocolUtils.writeString(buf, nameTagVisibility); - if (version.compareTo(ProtocolVersion.MINECRAFT_1_9) >= 0) { - ProtocolUtils.writeString(buf, collisionRule); - } - - if (version.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0) { - ProtocolUtils.writeVarInt(buf, color); - ProtocolUtils.writeString(buf, prefix); - ProtocolUtils.writeString(buf, suffix); - } else { - buf.writeByte( color ); - } - } - if (mode == CREATE || mode == ADD_PLAYER || mode == REMOVE_PLAYER) { - ProtocolUtils.writeVarInt(buf, players.length); - for (String player : players) { - ProtocolUtils.writeString(buf, player); - } - } - } - - @Override - public boolean handle(MinecraftSessionHandler minecraftSessionHandler) { - return false; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public byte getMode() { - return mode; - } - - public void setMode(byte mode) { - this.mode = mode; - } - - public String getDisplayName() { - return displayName; - } - - public void setDisplayName(String displayName) { - this.displayName = displayName; - } - - public String getPrefix() { - return prefix; - } - - public void setPrefix(String prefix) { - this.prefix = prefix; - } - - public String getSuffix() { - return suffix; - } - - public void setSuffix(String suffix) { - this.suffix = suffix; - } - - public String getNameTagVisibility() { - return nameTagVisibility; - } - - public void setNameTagVisibility(String nameTagVisibility) { - this.nameTagVisibility = nameTagVisibility; - } - - public String getCollisionRule() { - return collisionRule; - } - - public void setCollisionRule(String collisionRule) { - this.collisionRule = collisionRule; - } - - public int getColor() { - return color; - } - - public void setColor(int color) { - this.color = color; - } - - public byte getFriendlyFire() { - return friendlyFire; - } - - public void setFriendlyFire(byte friendlyFire) { - this.friendlyFire = friendlyFire; - } - - public String[] getPlayers() { - return players; - } - - public void setPlayers(String[] players) { - this.players = players; - } - - @Override - public String toString() { - return "Team{" + - "name=" + name + - ", mode=" + mode + - ", displayName=" + displayName + - ", prefix=" + prefix + - ", suffix=" + suffix + - ", friendlyFire=" + friendlyFire + - ", nameTagVisibility=" + nameTagVisibility + - ", collisionRule=" + collisionRule + - ", color=" + color + - ", players=[" + String.join(",", players) + "]" + - '}'; - } -} +/* + * Copyright (C) 2018-2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.protocol; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +public class Team implements MinecraftPacket { + + public static final byte CREATE = 0; + public static final byte REMOVE = 1; + public static final byte UPDATE_INFO = 2; + public static final byte ADD_PLAYER = 3; + public static final byte REMOVE_PLAYER = 4; + + private String name; + private byte mode; + private String displayName; + private String prefix; + private String suffix; + private String nameTagVisibility; + private String collisionRule; + private int color; + private byte friendlyFire; + private String[] players; + + public Team() { + } + + public Team(String name) { + this.name = name; + this.mode = REMOVE; + } + + public Team(String name, byte mode) { + this.name = name; + this.mode = mode; + } + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { + name = ProtocolUtils.readString(buf); + mode = buf.readByte(); + if (mode == CREATE || mode == UPDATE_INFO) { + displayName = ProtocolUtils.readString(buf); + if (version.compareTo(ProtocolVersion.MINECRAFT_1_13) < 0) { + prefix = ProtocolUtils.readString(buf); + suffix = ProtocolUtils.readString(buf); + } + friendlyFire = buf.readByte(); + nameTagVisibility = ProtocolUtils.readString(buf); + if (version.compareTo(ProtocolVersion.MINECRAFT_1_9) >= 0) { + collisionRule = ProtocolUtils.readString(buf); + } + color = ( version.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0 ) ? ProtocolUtils.readVarInt(buf) : buf.readByte(); + if ( version.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0 ) { + prefix = ProtocolUtils.readString(buf); + suffix = ProtocolUtils.readString(buf); + } + } + if (mode == CREATE || mode == ADD_PLAYER || mode == REMOVE_PLAYER) { + int len = ProtocolUtils.readVarInt(buf); + players = new String[len]; + for (int i = 0; i < len; i++) { + players[i] = ProtocolUtils.readString(buf); + } + } + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { + ProtocolUtils.writeString(buf, name); + buf.writeByte(mode); + if (mode == CREATE || mode == UPDATE_INFO) { + ProtocolUtils.writeString(buf, displayName); + if (version.compareTo(ProtocolVersion.MINECRAFT_1_13) < 0) { + ProtocolUtils.writeString(buf, prefix); + ProtocolUtils.writeString(buf, suffix); + } + buf.writeByte(friendlyFire); + ProtocolUtils.writeString(buf, nameTagVisibility); + if (version.compareTo(ProtocolVersion.MINECRAFT_1_9) >= 0) { + ProtocolUtils.writeString(buf, collisionRule); + } + + if (version.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0) { + ProtocolUtils.writeVarInt(buf, color); + ProtocolUtils.writeString(buf, prefix); + ProtocolUtils.writeString(buf, suffix); + } else { + buf.writeByte( color ); + } + } + if (mode == CREATE || mode == ADD_PLAYER || mode == REMOVE_PLAYER) { + ProtocolUtils.writeVarInt(buf, players.length); + for (String player : players) { + ProtocolUtils.writeString(buf, player); + } + } + } + + @Override + public boolean handle(MinecraftSessionHandler minecraftSessionHandler) { + return false; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public byte getMode() { + return mode; + } + + public void setMode(byte mode) { + this.mode = mode; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public String getPrefix() { + return prefix; + } + + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + public String getSuffix() { + return suffix; + } + + public void setSuffix(String suffix) { + this.suffix = suffix; + } + + public String getNameTagVisibility() { + return nameTagVisibility; + } + + public void setNameTagVisibility(String nameTagVisibility) { + this.nameTagVisibility = nameTagVisibility; + } + + public String getCollisionRule() { + return collisionRule; + } + + public void setCollisionRule(String collisionRule) { + this.collisionRule = collisionRule; + } + + public int getColor() { + return color; + } + + public void setColor(int color) { + this.color = color; + } + + public byte getFriendlyFire() { + return friendlyFire; + } + + public void setFriendlyFire(byte friendlyFire) { + this.friendlyFire = friendlyFire; + } + + public String[] getPlayers() { + return players; + } + + public void setPlayers(String[] players) { + this.players = players; + } + + @Override + public String toString() { + return "Team{" + + "name=" + name + + ", mode=" + mode + + ", displayName=" + displayName + + ", prefix=" + prefix + + ", suffix=" + suffix + + ", friendlyFire=" + friendlyFire + + ", nameTagVisibility=" + nameTagVisibility + + ", collisionRule=" + collisionRule + + ", color=" + color + + ", players=[" + String.join(",", players) + "]" + + '}'; + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/GeyserCompat.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/GeyserCompat.java index 00af2924..938a2167 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/GeyserCompat.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/GeyserCompat.java @@ -1,51 +1,64 @@ -package codecrafter47.bungeetablistplus.util; - -import org.geysermc.api.Geyser; -import org.geysermc.api.GeyserApiBase; -import org.geysermc.api.connection.Connection; -import org.geysermc.floodgate.api.FloodgateApi; - -import java.util.UUID; -import java.util.function.Function; - -public class GeyserCompat { - - private static Function geyserHook; - private static Function floodgateHook; - - static { - - // Geyser - try { - Class.forName("org.geysermc.api.connection.Connection"); - geyserHook = uuid -> { - GeyserApiBase instance = Geyser.api(); - if (instance == null) { - return false; - } - Connection session = instance.connectionByUuid(uuid); - return session != null; - }; - } catch (Throwable ignored) { - geyserHook = uuid -> false; - } - - // Floodgate - try { - Class.forName("org.geysermc.floodgate.api.FloodgateApi"); - floodgateHook = uuid -> { - FloodgateApi api = FloodgateApi.getInstance(); - if (api == null) { - return false; - } - return api.isFloodgatePlayer(uuid); - }; - } catch (Throwable ignored) { - floodgateHook = uuid -> false; - } - } - - public static boolean isBedrockPlayer(UUID uuid) { - return geyserHook.apply(uuid) || floodgateHook.apply(uuid); - } -} +package codecrafter47.bungeetablistplus.util; + +import org.geysermc.api.connection.Connection; +import org.geysermc.floodgate.api.FloodgateApi; +import org.geysermc.geyser.api.GeyserApi; + +import java.util.UUID; +import java.util.function.Function; + +public class GeyserCompat { + + private static Function geyserHook = uuid -> false;; + private static Function floodgateHook = uuid -> false;; + + public static void init() { + + // Geyser + try { + Class.forName("org.geysermc.api.connection.Connection"); + geyserHook = new Function() { + @Override + public Boolean apply(UUID uuid) { + + // Geyser documentation says, this will return null when not initialized. + // In reality, it throws an exception + try { + + GeyserApi instance = GeyserApi.api(); + if (instance == null) { + return false; + } + Connection session = instance.connectionByUuid(uuid); + return session != null; + } catch (Throwable ignored) { + + } + + return false; + } + }; + } catch (Throwable ignored) { + } + + // Floodgate + try { + Class.forName("org.geysermc.floodgate.api.FloodgateApi"); + floodgateHook = new Function() { + @Override + public Boolean apply(UUID uuid) { + FloodgateApi api = FloodgateApi.getInstance(); + if (api == null) { + return false; + } + return api.isFloodgatePlayer(uuid); + } + }; + } catch (Throwable ignored) { + } + } + + public static boolean isBedrockPlayer(UUID uuid) { + return geyserHook.apply(uuid) || floodgateHook.apply(uuid); + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ReflectionUtil.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ReflectionUtil.java index bba1b041..a0698c7a 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ReflectionUtil.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ReflectionUtil.java @@ -1,125 +1,125 @@ -/* - * Copyright (C) 2020 Florian Stober - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package codecrafter47.bungeetablistplus.util; - -import codecrafter47.bungeetablistplus.protocol.Team; -import com.velocitypowered.api.network.ProtocolVersion; -import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.api.proxy.player.TabList; -import com.velocitypowered.proxy.connection.MinecraftConnection; -import com.velocitypowered.proxy.connection.client.ConnectedPlayer; -import com.velocitypowered.proxy.protocol.StateRegistry; - -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Type; -import java.util.function.Supplier; -import java.util.logging.Logger; - -import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_12; -import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_12_1; -import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_13; -import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_14; -import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_15; -import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_17; -import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_1; -import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_3; -import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8; -import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9; - -public class ReflectionUtil { - public static void setTablistHandler(Player player, TabList tablistHandler) throws NoSuchFieldException, IllegalAccessException { - setField(ConnectedPlayer.class, player, "tabList", tablistHandler, 5); - } - - public static TabList getTablistHandler(Player player) throws NoSuchFieldException, IllegalAccessException { - return getField(ConnectedPlayer.class, player, "tabList", 5); - } - - public static MinecraftConnection getChannelWrapper(Player player) throws NoSuchFieldException, IllegalAccessException { - return getField(ConnectedPlayer.class, player, "connection", 50); - } - - public static void setField(Class clazz, Object instance, String field, Object value) throws NoSuchFieldException, IllegalAccessException { - Field f = clazz.getDeclaredField(field); - f.setAccessible(true); - f.set(instance, value); - } - - public static void setField(Class clazz, Object instance, String field, Object value, int tries) throws NoSuchFieldException, IllegalAccessException { - while (--tries > 0) { - try { - setField(clazz, instance, field, value); - return; - } catch (NoSuchFieldException | IllegalAccessException ignored) { - } - } - setField(clazz, instance, field, value); - } - - @SuppressWarnings("unchecked") - public static T getField(Class clazz, Object instance, String field) throws NoSuchFieldException, IllegalAccessException { - Field f = clazz.getDeclaredField(field); - f.setAccessible(true); - return (T) f.get(instance); - } - - public static T getField(Class clazz, Object instance, String field, int tries) throws NoSuchFieldException, IllegalAccessException { - while (--tries > 0) { - try { - return getField(clazz, instance, field); - } catch (NoSuchFieldException | IllegalAccessException ignored) { - } - } - return getField(clazz, instance, field); - } - - // Five from Velocity team said this to adding the Team packet - // "Most instances won't need this feature so why should we weigh them down with baggage that's entirely optional?" - public static void injectTeamPacketRegistry() { - int tries = 5; - while (--tries > 0) { - try { - StateRegistry.PacketRegistry clientbound = getField(StateRegistry.class, StateRegistry.PLAY, "clientbound", tries); - - Method register = StateRegistry.PacketRegistry.class.getDeclaredMethod("register", Class.class, Supplier.class, StateRegistry.PacketMapping[].class); - register.setAccessible(true); - - Constructor packetMapping = StateRegistry.PacketMapping.class.getDeclaredConstructor(int.class, ProtocolVersion.class, ProtocolVersion.class, boolean.class); - packetMapping.setAccessible(true); - - register.invoke(clientbound, Team.class, (Supplier) Team::new, new StateRegistry.PacketMapping[]{ - (StateRegistry.PacketMapping) packetMapping.newInstance(0x3E, MINECRAFT_1_8, null, false), - (StateRegistry.PacketMapping) packetMapping.newInstance(0x41, MINECRAFT_1_9, null, false), - (StateRegistry.PacketMapping) packetMapping.newInstance(0x43, MINECRAFT_1_12, null, false), - (StateRegistry.PacketMapping) packetMapping.newInstance(0x44, MINECRAFT_1_12_1, null, false), - (StateRegistry.PacketMapping) packetMapping.newInstance(0x47, MINECRAFT_1_13, null, false), - (StateRegistry.PacketMapping) packetMapping.newInstance(0x4B, MINECRAFT_1_14, null, false), - (StateRegistry.PacketMapping) packetMapping.newInstance(0x4C, MINECRAFT_1_15, null, false), - (StateRegistry.PacketMapping) packetMapping.newInstance(0x55, MINECRAFT_1_17, null, false), - (StateRegistry.PacketMapping) packetMapping.newInstance(0x58, MINECRAFT_1_19_1, null, false), - (StateRegistry.PacketMapping) packetMapping.newInstance(0x56, MINECRAFT_1_19_3, null, false) - }); - return; - } catch (Exception e) { - e.printStackTrace(); - } - } - } -} +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.util; + +import codecrafter47.bungeetablistplus.protocol.Team; +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.player.TabList; +import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.protocol.StateRegistry; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.function.Supplier; + +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_12; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_12_1; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_13; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_14; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_15; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_17; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_1; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_3; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_4; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9; + +public class ReflectionUtil { + public static void setTablistHandler(Player player, TabList tablistHandler) throws NoSuchFieldException, IllegalAccessException { + setField(ConnectedPlayer.class, player, "tabList", tablistHandler, 5); + } + + public static TabList getTablistHandler(Player player) throws NoSuchFieldException, IllegalAccessException { + return getField(ConnectedPlayer.class, player, "tabList", 5); + } + + public static MinecraftConnection getChannelWrapper(Player player) throws NoSuchFieldException, IllegalAccessException { + return getField(ConnectedPlayer.class, player, "connection", 50); + } + + public static void setField(Class clazz, Object instance, String field, Object value) throws NoSuchFieldException, IllegalAccessException { + Field f = clazz.getDeclaredField(field); + f.setAccessible(true); + f.set(instance, value); + } + + public static void setField(Class clazz, Object instance, String field, Object value, int tries) throws NoSuchFieldException, IllegalAccessException { + while (--tries > 0) { + try { + setField(clazz, instance, field, value); + return; + } catch (NoSuchFieldException | IllegalAccessException ignored) { + } + } + setField(clazz, instance, field, value); + } + + @SuppressWarnings("unchecked") + public static T getField(Class clazz, Object instance, String field) throws NoSuchFieldException, IllegalAccessException { + Field f = clazz.getDeclaredField(field); + f.setAccessible(true); + return (T) f.get(instance); + } + + public static T getField(Class clazz, Object instance, String field, int tries) throws NoSuchFieldException, IllegalAccessException { + while (--tries > 0) { + try { + return getField(clazz, instance, field); + } catch (NoSuchFieldException | IllegalAccessException ignored) { + } + } + return getField(clazz, instance, field); + } + + // Five from Velocity team said this to adding the Team packet + // "Most instances won't need this feature so why should we weigh them down with baggage that's entirely optional?" + public static void injectTeamPacketRegistry() { + int tries = 5; + while (--tries > 0) { + try { + StateRegistry.PacketRegistry clientbound = getField(StateRegistry.class, StateRegistry.PLAY, "clientbound", tries); + + Method register = StateRegistry.PacketRegistry.class.getDeclaredMethod("register", Class.class, Supplier.class, StateRegistry.PacketMapping[].class); + register.setAccessible(true); + + Constructor packetMapping = StateRegistry.PacketMapping.class.getDeclaredConstructor(int.class, ProtocolVersion.class, ProtocolVersion.class, boolean.class); + packetMapping.setAccessible(true); + + register.invoke(clientbound, Team.class, (Supplier) Team::new, new StateRegistry.PacketMapping[]{ + (StateRegistry.PacketMapping) packetMapping.newInstance(0x3E, MINECRAFT_1_8, null, false), + (StateRegistry.PacketMapping) packetMapping.newInstance(0x41, MINECRAFT_1_9, null, false), + (StateRegistry.PacketMapping) packetMapping.newInstance(0x43, MINECRAFT_1_12, null, false), + (StateRegistry.PacketMapping) packetMapping.newInstance(0x44, MINECRAFT_1_12_1, null, false), + (StateRegistry.PacketMapping) packetMapping.newInstance(0x47, MINECRAFT_1_13, null, false), + (StateRegistry.PacketMapping) packetMapping.newInstance(0x4B, MINECRAFT_1_14, null, false), + (StateRegistry.PacketMapping) packetMapping.newInstance(0x4C, MINECRAFT_1_15, null, false), + (StateRegistry.PacketMapping) packetMapping.newInstance(0x55, MINECRAFT_1_17, null, false), + (StateRegistry.PacketMapping) packetMapping.newInstance(0x58, MINECRAFT_1_19_1, null, false), + (StateRegistry.PacketMapping) packetMapping.newInstance(0x56, MINECRAFT_1_19_3, null, false), + (StateRegistry.PacketMapping) packetMapping.newInstance(0x5A, MINECRAFT_1_19_4, null, false) + }); + return; + } catch (Exception e) { + e.printStackTrace(); + } + } + } +} From d117a133c3e68c8db40bcbf688debe633b125f3f Mon Sep 17 00:00:00 2001 From: Brent P Date: Sun, 1 Oct 2023 10:22:27 -0400 Subject: [PATCH 12/22] Update minecraft-data-api version --- minecraft-data-api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minecraft-data-api b/minecraft-data-api index 3acdda80..56cc7f4a 160000 --- a/minecraft-data-api +++ b/minecraft-data-api @@ -1 +1 @@ -Subproject commit 3acdda806579e911bde9aa679e432bf65985b8f2 +Subproject commit 56cc7f4afde29565b387781abcdb202c2001ae4a From f1ba70d10f0c0ef008c3689a905835876009a6e3 Mon Sep 17 00:00:00 2001 From: Brent P Date: Sun, 1 Oct 2023 10:37:20 -0400 Subject: [PATCH 13/22] Update Submodules --- TabOverlayCommon | 2 +- minecraft-data-api | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/TabOverlayCommon b/TabOverlayCommon index 75e4529f..ab86bc6f 160000 --- a/TabOverlayCommon +++ b/TabOverlayCommon @@ -1 +1 @@ -Subproject commit 75e4529f58d0bf1d8ace537af7d5a49bbc2e8308 +Subproject commit ab86bc6f29caf1b44a981ccfcde45a8f193f3689 diff --git a/minecraft-data-api b/minecraft-data-api index 56cc7f4a..fa9a5ed5 160000 --- a/minecraft-data-api +++ b/minecraft-data-api @@ -1 +1 @@ -Subproject commit 56cc7f4afde29565b387781abcdb202c2001ae4a +Subproject commit fa9a5ed51fe814850e206c3cf6be7c0380642150 From 326b399071a4e73ad58adf0b5fdb273253c11a46 Mon Sep 17 00:00:00 2001 From: proferabg Date: Fri, 7 Feb 2025 15:03:05 -0500 Subject: [PATCH 14/22] Updated but not tested --- TabOverlayCommon | 2 +- api-velocity/build.gradle | 5 + bootstrap-bukkit/build.gradle | 4 +- bootstrap-bungee/build.gradle | 4 +- bootstrap-velocity/build.gradle | 11 +- build.gradle | 17 +- gradle/wrapper/gradle-wrapper.properties | 3 +- minecraft-data-api | 2 +- velocity/build.gradle | 9 + .../bungeetablistplus/BungeeTabListPlus.java | 4 +- .../AbstractLegacyTabOverlayHandler.java | 102 +++-- .../handler/AbstractTabOverlayHandler.java | 366 ++++++++---------- .../handler/GetGamemodeLogic.java | 20 +- .../handler/LegacyTabOverlayHandlerImpl.java | 4 +- .../LowMemoryTabOverlayHandlerImpl.java | 4 +- .../handler/NewTabOverlayHandler.java | 155 ++++---- .../handler/RewriteLogic.java | 44 +-- .../handler/TabOverlayHandlerImpl.java | 19 +- .../managers/TabViewManager.java | 2 +- .../protocol/AbstractPacketHandler.java | 16 +- .../protocol/PacketHandler.java | 16 +- .../protocol/PacketListener.java | 24 +- .../bungeetablistplus/protocol/Team.java | 224 +++++------ .../util/Property119Handler.java | 10 +- .../util/ReflectionUtil.java | 10 +- .../version/ProtocolVersionProvider.java | 2 + .../VelocityProtocolVersionProvider.java | 4 + .../ViaVersionProtocolVersionProvider.java | 5 + 28 files changed, 531 insertions(+), 557 deletions(-) diff --git a/TabOverlayCommon b/TabOverlayCommon index fb984e1c..b939ebaa 160000 --- a/TabOverlayCommon +++ b/TabOverlayCommon @@ -1 +1 @@ -Subproject commit fb984e1cdc6836d3c07fd8b10043b10e72f866eb +Subproject commit b939ebaa875a765c8be88620adbfcd54869815dd diff --git a/api-velocity/build.gradle b/api-velocity/build.gradle index df9bf521..eb01d6e0 100644 --- a/api-velocity/build.gradle +++ b/api-velocity/build.gradle @@ -5,6 +5,11 @@ dependencies { api "de.codecrafter47.taboverlay:taboverlaycommon-api:1.0-SNAPSHOT" } +compileJava { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + java { withJavadocJar() withSourcesJar() diff --git a/bootstrap-bukkit/build.gradle b/bootstrap-bukkit/build.gradle index 0cfd9016..8afedd55 100644 --- a/bootstrap-bukkit/build.gradle +++ b/bootstrap-bukkit/build.gradle @@ -9,8 +9,8 @@ dependencies { } compileJava { - sourceCompatibility = '1.8' - targetCompatibility = '1.8' + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } shadowJar { diff --git a/bootstrap-bungee/build.gradle b/bootstrap-bungee/build.gradle index 93d2b739..2a1de0b4 100644 --- a/bootstrap-bungee/build.gradle +++ b/bootstrap-bungee/build.gradle @@ -10,8 +10,8 @@ dependencies { } compileJava { - sourceCompatibility = '1.8' - targetCompatibility = '1.8' + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } shadowJar { diff --git a/bootstrap-velocity/build.gradle b/bootstrap-velocity/build.gradle index c2b7b893..aa163d70 100644 --- a/bootstrap-velocity/build.gradle +++ b/bootstrap-velocity/build.gradle @@ -1,8 +1,8 @@ import org.apache.tools.ant.filters.ReplaceTokens plugins { - id "org.jetbrains.gradle.plugin.idea-ext" version "1.0.1" - id "com.github.johnrengelman.shadow" version "5.2.0" + id "org.jetbrains.gradle.plugin.idea-ext" version "1.1.10" + id "com.github.johnrengelman.shadow" version "7.1.2" } dependencies { @@ -12,11 +12,6 @@ dependencies { implementation "org.bstats:bstats-velocity:3.0.0" } -compileJava { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 -} - task processSource(type: Sync) { from sourceSets.main.java inputs.property 'version', version @@ -26,6 +21,8 @@ task processSource(type: Sync) { compileJava { source = processSource.outputs + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } shadowJar { diff --git a/build.gradle b/build.gradle index e432c2d8..2f99b264 100644 --- a/build.gradle +++ b/build.gradle @@ -63,20 +63,23 @@ subprojects { maven { url = 'https://oss.sonatype.org/content/repositories/snapshots' } + + maven { + url = 'https://nexus.prgm.in/repository/maven-public/' + } } dependencies { - - compileOnly 'org.projectlombok:lombok:1.18.20' - annotationProcessor 'org.projectlombok:lombok:1.18.20' - testCompileOnly 'org.projectlombok:lombok:1.18.20' - testAnnotationProcessor 'org.projectlombok:lombok:1.18.20' + compileOnly 'org.projectlombok:lombok:1.18.34' + annotationProcessor 'org.projectlombok:lombok:1.18.34' + testCompileOnly 'org.projectlombok:lombok:1.18.34' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.34' compileOnly 'com.google.code.findbugs:jsr305:3.0.1' } compileJava { - sourceCompatibility = '11' - targetCompatibility = '11' + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } publishing { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 69a97150..7549bdc3 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Fri Feb 07 11:47:49 EST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/minecraft-data-api b/minecraft-data-api index e23ae53c..b0e4d1f5 160000 --- a/minecraft-data-api +++ b/minecraft-data-api @@ -1 +1 @@ -Subproject commit e23ae53c0d6ecf2d39a71a10fdb72423db28b39f +Subproject commit b0e4d1f55dcc298fa13254bf8dde509248a9a4bd diff --git a/velocity/build.gradle b/velocity/build.gradle index 53a6733e..4697cbb2 100644 --- a/velocity/build.gradle +++ b/velocity/build.gradle @@ -8,6 +8,11 @@ repositories { } } +compileJava { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + dependencies { implementation "de.codecrafter47.data:api:${rootProject.ext.dataApiVersion}" implementation "de.codecrafter47.data.bukkit:api:${rootProject.ext.dataApiVersion}" @@ -31,6 +36,10 @@ dependencies { compileOnly group: "org.geysermc.geyser", name: "api", version: "2.1.0-SNAPSHOT" compileOnly "org.geysermc.floodgate:api:2.0-SNAPSHOT" compileOnly "io.netty:netty-all:4.1.86.Final" + compileOnly 'org.projectlombok:lombok:1.18.34' + annotationProcessor 'org.projectlombok:lombok:1.18.34' + testCompileOnly 'org.projectlombok:lombok:1.18.34' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.34' // This imports velocity proxy compileOnly fileTree(dir: '../libs', include: '*.jar') } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java index 6e983f59..8d58d0c9 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java @@ -186,9 +186,9 @@ public void onLoad() { } try { - Class.forName("com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo"); + Class.forName("com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket"); } catch (ClassNotFoundException ex) { - throw new RuntimeException("You need to run at least Velocity version #196"); + throw new RuntimeException("You need to run at least Velocity version #329"); } INSTANCE = this; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractLegacyTabOverlayHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractLegacyTabOverlayHandler.java index 44774faa..70b8e95f 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractLegacyTabOverlayHandler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractLegacyTabOverlayHandler.java @@ -25,11 +25,13 @@ import codecrafter47.bungeetablistplus.util.Property119Handler; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; +import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.protocol.MinecraftPacket; -import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; -import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; -import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; -import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; +import com.velocitypowered.proxy.protocol.packet.HeaderAndFooterPacket; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket; +import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfoPacket; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket; +import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; import de.codecrafter47.taboverlay.Icon; import de.codecrafter47.taboverlay.config.misc.ChatFormat; import de.codecrafter47.taboverlay.config.misc.Unchecked; @@ -139,9 +141,9 @@ private static Collection getSupportedSizesByPl protected abstract void sendPacket(MinecraftPacket packet); @Override - public PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { - if (packet.getAction() == LegacyPlayerListItem.ADD_PLAYER) { - for (LegacyPlayerListItem.Item item : packet.getItems()) { + public PacketListenerResult onPlayerListPacket(LegacyPlayerListItemPacket packet) { + if (packet.getAction() == LegacyPlayerListItemPacket.ADD_PLAYER) { + for (LegacyPlayerListItemPacket.Item item : packet.getItems()) { if (item.getUuid() != null) { modernServerPlayerList.put(item.getUuid(), new ModernPlayerListEntry(item.getName(), item.getLatency(), item.getGameMode())); } else { @@ -149,7 +151,7 @@ public PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { } } } else { - for (LegacyPlayerListItem.Item item : packet.getItems()) { + for (LegacyPlayerListItemPacket.Item item : packet.getItems()) { if (item.getUuid() != null) { modernServerPlayerList.remove(item.getUuid()); } else { @@ -161,9 +163,9 @@ public PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { } @Override - public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { - if (packet.getActions().contains(UpsertPlayerInfo.Action.ADD_PLAYER)) { - for (UpsertPlayerInfo.Entry entry : packet.getEntries()) { + public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfoPacket packet) { + if (packet.getActions().contains(UpsertPlayerInfoPacket.Action.ADD_PLAYER)) { + for (UpsertPlayerInfoPacket.Entry entry : packet.getEntries()) { if (entry.getProfileId() != null) { modernServerPlayerList.put(entry.getProfileId(), new ModernPlayerListEntry(entry.getProfile().getName(), entry.getLatency(), entry.getGameMode())); } else { @@ -175,7 +177,7 @@ public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { } @Override - public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet) { + public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfoPacket packet) { for (UUID uuid : packet.getProfilesToRemove()) { modernServerPlayerList.remove(uuid); } @@ -193,7 +195,7 @@ public PacketListenerResult onTeamPacket(Team packet) { } @Override - public PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { + public PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooterPacket packet) { logger.log(Level.WARNING, "1.7 players should not receive tab list header/ footer"); return PacketListenerResult.CANCEL; } @@ -249,17 +251,17 @@ private void update() { } private void removeEntry(UUID uuid, String player) { - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(); + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(uuid); item.setName(player); item.setDisplayName(GsonComponentSerializer.gson().deserialize(player)); item.setLatency(9999); - LegacyPlayerListItem pli = new LegacyPlayerListItem(LegacyPlayerListItem.REMOVE_PLAYER, List.of(item)); + LegacyPlayerListItemPacket pli = new LegacyPlayerListItemPacket(LegacyPlayerListItemPacket.REMOVE_PLAYER, List.of(item)); sendPacket(pli); } private abstract static class AbstractContentOperationModeHandler extends OperationModeHandler { - abstract PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet); + abstract PacketListenerResult onPlayerListPacket(LegacyPlayerListItemPacket packet); abstract void onServerSwitch(); @@ -274,9 +276,9 @@ final void invalidate() { abstract void onActivated(); - public abstract PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet); + public abstract PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfoPacket packet); - public abstract PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet); + public abstract PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfoPacket packet); } private abstract static class AbstractTabOverlay implements TabOverlayHandle { @@ -295,36 +297,36 @@ final void invalidate() { private class PassThroughHandlerContent extends AbstractContentOperationModeHandler { @Override - PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { + PacketListenerResult onPlayerListPacket(LegacyPlayerListItemPacket packet) { return PacketListenerResult.PASS; } @Override - public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { + public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfoPacket packet) { return PacketListenerResult.PASS; } @Override - public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet) { + public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfoPacket packet) { return PacketListenerResult.PASS; } @Override void onActivated() { for (val entry : serverPlayerList.object2IntEntrySet()) { - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(); + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(); item.setDisplayName(GsonComponentSerializer.gson().deserialize(entry.getKey())); // TODO: Check Formatting item.setLatency(entry.getIntValue()); - LegacyPlayerListItem pli = new LegacyPlayerListItem(LegacyPlayerListItem.ADD_PLAYER, List.of(item)); + LegacyPlayerListItemPacket pli = new LegacyPlayerListItemPacket(LegacyPlayerListItemPacket.ADD_PLAYER, List.of(item)); sendPacket(pli); } for (val entry : modernServerPlayerList.entrySet()) { - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(entry.getKey()); + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(entry.getKey()); item.setName(entry.getValue().name); item.setGameMode(entry.getValue().gamemode); item.setLatency(entry.getValue().latency); Property119Handler.setProperties(item, EMPTY_PROPERTIES); - LegacyPlayerListItem pli = new LegacyPlayerListItem(LegacyPlayerListItem.ADD_PLAYER, List.of(item)); + LegacyPlayerListItemPacket pli = new LegacyPlayerListItemPacket(LegacyPlayerListItemPacket.ADD_PLAYER, List.of(item)); sendPacket(pli); } } @@ -374,17 +376,17 @@ private CustomTabOverlayHandlerContent() { } @Override - PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { + PacketListenerResult onPlayerListPacket(LegacyPlayerListItemPacket packet) { return PacketListenerResult.CANCEL; } @Override - public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { + public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfoPacket packet) { return PacketListenerResult.CANCEL; } @Override - public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet) { + public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfoPacket packet) { return PacketListenerResult.CANCEL; } @@ -429,17 +431,12 @@ private void updateSize() { Team t = new Team(); t.setName(slotID[index]); t.setMode((byte) 0); - t.setPrefix(tabOverlay.text0[index]); - t.setDisplayName(""); - t.setSuffix(tabOverlay.text1[index]); + t.setPrefix(new ComponentHolder(is13OrLater ? ProtocolVersion.MINECRAFT_1_13 : ProtocolVersion.MINECRAFT_1_12_2, tabOverlay.text0[index])); + t.setDisplayName(new ComponentHolder(is13OrLater ? ProtocolVersion.MINECRAFT_1_13 : ProtocolVersion.MINECRAFT_1_12_2, "")); + t.setSuffix(new ComponentHolder(is13OrLater ? ProtocolVersion.MINECRAFT_1_13 : ProtocolVersion.MINECRAFT_1_12_2, tabOverlay.text1[index])); t.setPlayers(new String[]{slotID[index]}); - t.setNameTagVisibility("always"); - t.setCollisionRule("always"); - if (is13OrLater) { - t.setDisplayName(EMPTY_JSON_TEXT); - t.setPrefix("{\"text\":\"" + tabOverlay.text0[index] + "\"}"); - t.setSuffix("{\"text\":\"" + tabOverlay.text1[index] + "\"}"); - } + t.setNameTagVisibility(Team.NameTagVisibility.ALWAYS); + t.setCollisionRule(Team.CollisionRule.ALWAYS); sendPacket(t); } } else { @@ -456,12 +453,12 @@ private void updateSize() { } private void updateSlot(CustomTabOverlay tabOverlay, int index) { - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUUID[index]); + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(slotUUID[index]); item.setName(slotID[index]); Property119Handler.setProperties(item, EMPTY_PROPERTIES); item.setDisplayName(GsonComponentSerializer.gson().deserialize(slotID[index])); // TODO: Check Formatting item.setLatency(tabOverlay.ping[index]); - LegacyPlayerListItem pli = new LegacyPlayerListItem(LegacyPlayerListItem.ADD_PLAYER, List.of(item)); + LegacyPlayerListItemPacket pli = new LegacyPlayerListItemPacket(LegacyPlayerListItemPacket.ADD_PLAYER, List.of(item)); sendPacket(pli); } @@ -470,16 +467,11 @@ private void updateText(CustomTabOverlay tabOverlay, int index) { Team packet = new Team(); packet.setName(slotID[index]); packet.setMode((byte) 2); - packet.setPrefix(tabOverlay.text0[index]); - packet.setDisplayName(""); - packet.setSuffix(tabOverlay.text1[index]); - packet.setNameTagVisibility("always"); - packet.setCollisionRule("always"); - if (is13OrLater) { - packet.setDisplayName(EMPTY_JSON_TEXT); - packet.setPrefix("{\"text\":\"" + tabOverlay.text0[index] + "\"}"); - packet.setSuffix("{\"text\":\"" + tabOverlay.text1[index] + "\"}"); - } + packet.setPrefix(new ComponentHolder(is13OrLater ? ProtocolVersion.MINECRAFT_1_13 : ProtocolVersion.MINECRAFT_1_12_2, tabOverlay.text0[index])); + packet.setDisplayName(new ComponentHolder(is13OrLater ? ProtocolVersion.MINECRAFT_1_13 : ProtocolVersion.MINECRAFT_1_12_2, "")); + packet.setSuffix(new ComponentHolder(is13OrLater ? ProtocolVersion.MINECRAFT_1_13 : ProtocolVersion.MINECRAFT_1_12_2, tabOverlay.text1[index])); + packet.setNameTagVisibility(Team.NameTagVisibility.ALWAYS); + packet.setCollisionRule(Team.CollisionRule.ALWAYS); sendPacket(packet); } } @@ -815,14 +807,14 @@ public boolean isValid() { } /** - * Utility method to get the name from an {@link LegacyPlayerListItem.Item}. + * Utility method to get the name from an {@link LegacyPlayerListItemPacket.Item}. * * @param item the item * @return the name */ - private static String getName(LegacyPlayerListItem.Item item) { + private static String getName(LegacyPlayerListItemPacket.Item item) { if (item.getDisplayName() != null) { - return GsonComponentSerializer.gson().serialize(item.getDisplayName()); + return LegacyComponentSerializer.legacySection().serialize(item.getDisplayName()); } else if (item.getName() != null) { return item.getName(); } else { @@ -830,9 +822,9 @@ private static String getName(LegacyPlayerListItem.Item item) { } } - private static String getName(UpsertPlayerInfo.Entry entry) { + private static String getName(UpsertPlayerInfoPacket.Entry entry) { if (entry.getDisplayName() != null) { - return GsonComponentSerializer.gson().serialize(entry.getDisplayName()); + return entry.getDisplayName().getJson(); } else if (entry.getProfile().getName() != null) { return entry.getProfile().getName(); } else { diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java index 74a135b6..343a9556 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java @@ -26,9 +26,11 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.protocol.MinecraftPacket; -import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; -import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; +import com.velocitypowered.proxy.protocol.packet.HeaderAndFooterPacket; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket; +import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; import de.codecrafter47.taboverlay.Icon; import de.codecrafter47.taboverlay.ProfileProperty; import de.codecrafter47.taboverlay.config.misc.ChatFormat; @@ -51,11 +53,11 @@ import java.util.logging.Level; import java.util.logging.Logger; -import static com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem.ADD_PLAYER; -import static com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem.REMOVE_PLAYER; -import static com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem.UPDATE_DISPLAY_NAME; -import static com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem.UPDATE_GAMEMODE; -import static com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem.UPDATE_LATENCY; +import static com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket.ADD_PLAYER; +import static com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket.REMOVE_PLAYER; +import static com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket.UPDATE_DISPLAY_NAME; +import static com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket.UPDATE_GAMEMODE; +import static com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket.UPDATE_LATENCY; public abstract class AbstractTabOverlayHandler implements PacketHandler, TabOverlayHandler { @@ -64,7 +66,7 @@ public abstract class AbstractTabOverlayHandler implements PacketHandler, TabOve private static final boolean OPTION_ENABLE_CUSTOM_SLOT_UUID_COLLISION_CHECK = true; private static final boolean OPTION_ENABLE_CONSISTENCY_CHECKS = true; - private static final String EMPTY_JSON_TEXT = "{\"text\":\"\"}"; + private static ComponentHolder EMPTY_COMPONENT; protected static final String[][] EMPTY_PROPERTIES_ARRAY = new String[0][]; private static final boolean TEAM_COLLISION_RULE_SUPPORTED; @@ -170,9 +172,9 @@ public abstract class AbstractTabOverlayHandler implements PacketHandler, TabOve private final Object2ObjectMap serverPlayerList = new Object2ObjectOpenHashMap<>(); protected final Set serverTabListPlayers = new ObjectOpenHashSet<>(); @Nullable - protected String serverHeader = null; + protected Component serverHeader = null; @Nullable - protected String serverFooter = null; + protected Component serverFooter = null; protected final Object2ObjectMap serverTeams = new Object2ObjectOpenHashMap<>(); protected final Object2ObjectMap playerToTeamMap = new Object2ObjectOpenHashMap<>(); @@ -182,7 +184,6 @@ public abstract class AbstractTabOverlayHandler implements PacketHandler, TabOve private AbstractHeaderFooterOperationModeHandler activeHeaderFooterHandler; private boolean hasCreatedCustomTeams = false; - private boolean areCustomSlotUsersPartOfTeams = false; private final AtomicBoolean updateScheduledFlag = new AtomicBoolean(false); private final Runnable updateTask = this::update; @@ -190,26 +191,31 @@ public abstract class AbstractTabOverlayHandler implements PacketHandler, TabOve private final boolean is18; private boolean is13OrLater; private boolean is119OrLater; + private boolean is1203OrLater; protected boolean active; - public AbstractTabOverlayHandler(Logger logger, Executor eventLoopExecutor, UUID viewerUuid, boolean is18, boolean is13OrLater, boolean is119OrLater) { + public AbstractTabOverlayHandler(Logger logger, Executor eventLoopExecutor, UUID viewerUuid, boolean is18, boolean is13OrLater, boolean is119OrLater, boolean is1203OrLater) { this.logger = logger; this.eventLoopExecutor = eventLoopExecutor; this.viewerUuid = viewerUuid; this.is18 = is18; this.is13OrLater = is13OrLater; this.is119OrLater = is119OrLater; + this.is1203OrLater = is1203OrLater; this.activeContentHandler = new PassThroughContentHandler(); this.activeHeaderFooterHandler = new PassThroughHeaderFooterHandler(); + EMPTY_COMPONENT = new ComponentHolder(is13OrLater ? (is1203OrLater ? ProtocolVersion.MINECRAFT_1_20_3 : ProtocolVersion.MINECRAFT_1_13) : ProtocolVersion.MINECRAFT_1_12_2, Component.empty()); } protected abstract void sendPacket(MinecraftPacket packet); + protected abstract ProtocolVersion getProtocol(); + @Override - public PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { + public PacketListenerResult onPlayerListPacket(LegacyPlayerListItemPacket packet) { switch (packet.getAction()) { case ADD_PLAYER: - for (LegacyPlayerListItem.Item item : packet.getItems()) { + for (LegacyPlayerListItemPacket.Item item : packet.getItems()) { if (OPTION_ENABLE_CUSTOM_SLOT_UUID_COLLISION_CHECK) { if (CUSTOM_SLOT_UUIDS.contains(item.getUuid())) { throw new AssertionError("UUID collision " + item.getUuid()); @@ -228,7 +234,7 @@ public PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { } break; case UPDATE_GAMEMODE: - for (LegacyPlayerListItem.Item item : packet.getItems()) { + for (LegacyPlayerListItemPacket.Item item : packet.getItems()) { PlayerListEntry playerListEntry = serverPlayerList.get(item.getUuid()); if (playerListEntry != null) { playerListEntry.setGamemode(item.getGameMode()); @@ -236,7 +242,7 @@ public PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { } break; case UPDATE_LATENCY: - for (LegacyPlayerListItem.Item item : packet.getItems()) { + for (LegacyPlayerListItemPacket.Item item : packet.getItems()) { PlayerListEntry playerListEntry = serverPlayerList.get(item.getUuid()); if (playerListEntry != null) { playerListEntry.setPing(item.getLatency()); @@ -244,15 +250,15 @@ public PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { } break; case UPDATE_DISPLAY_NAME: - for (LegacyPlayerListItem.Item item : packet.getItems()) { + for (LegacyPlayerListItemPacket.Item item : packet.getItems()) { PlayerListEntry playerListEntry = serverPlayerList.get(item.getUuid()); if (playerListEntry != null) { - playerListEntry.setDisplayName(GsonComponentSerializer.gson().serialize(item.getDisplayName())); + playerListEntry.setDisplayName(item.getDisplayName()); } } break; case REMOVE_PLAYER: - for (LegacyPlayerListItem.Item item : packet.getItems()) { + for (LegacyPlayerListItemPacket.Item item : packet.getItems()) { PlayerListEntry removed = serverPlayerList.remove(item.getUuid()); if (removed != null) { serverTabListPlayers.remove(removed.getUsername()); @@ -277,8 +283,9 @@ public PacketListenerResult onTeamPacket(Team packet) { if (packet.getPlayers() != null) { boolean block = false; for (String player : packet.getPlayers()) { - if (player.equals("")) { + if (player.isEmpty()) { block = true; + break; } } if (block) { @@ -359,7 +366,7 @@ public PacketListenerResult onTeamPacket(Team packet) { } @Override - public PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { + public PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooterPacket packet) { PacketListenerResult result = PacketListenerResult.PASS; try { result = this.activeHeaderFooterHandler.onPlayerListHeaderFooterPacket(packet); @@ -372,8 +379,8 @@ public PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packe enterHeaderAndFooterOperationMode(HeaderAndFooterOperationMode.PASS_TROUGH); } - this.serverHeader = packet.getHeader() != null ? packet.getHeader() : EMPTY_JSON_TEXT; - this.serverFooter = packet.getFooter() != null ? packet.getFooter() : EMPTY_JSON_TEXT; + this.serverHeader = packet.getHeader() != null ? packet.getHeader().getComponent() : Component.empty(); + this.serverFooter = packet.getFooter() != null ? packet.getFooter().getComponent() : Component.empty(); return result; } @@ -391,7 +398,6 @@ public void onServerSwitch(boolean is13OrLater) { if (isUsingAltRespawn()) { hasCreatedCustomTeams = false; - areCustomSlotUsersPartOfTeams = false; } try { @@ -410,21 +416,21 @@ public void onServerSwitch(boolean is13OrLater) { } if (!serverPlayerList.isEmpty()) { - List items = new ArrayList<>(); + List items = new ArrayList<>(); for(UUID uuid : serverPlayerList.keySet()){ - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(uuid); + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(uuid); items.add(item); } - LegacyPlayerListItem packet = new LegacyPlayerListItem(REMOVE_PLAYER, items); + LegacyPlayerListItemPacket packet = new LegacyPlayerListItemPacket(REMOVE_PLAYER, items); sendPacket(packet); } serverPlayerList.clear(); if (serverHeader != null) { - serverHeader = EMPTY_JSON_TEXT; + serverHeader = Component.empty(); } if (serverFooter != null) { - serverFooter = EMPTY_JSON_TEXT; + serverFooter = Component.empty(); } serverTabListPlayers.clear(); @@ -504,11 +510,11 @@ private void update() { private abstract class AbstractContentOperationModeHandler extends OperationModeHandler { /** - * Called when the player receives a {@link LegacyPlayerListItem} packet. + * Called when the player receives a {@link LegacyPlayerListItemPacket} packet. *

* This method is called after this {@link AbstractTabOverlayHandler} has updated the {@code serverPlayerList}. */ - abstract PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet); + abstract PacketListenerResult onPlayerListPacket(LegacyPlayerListItemPacket packet); /** * Called when the player receives a {@link Team} packet. @@ -567,12 +573,12 @@ final void invalidate() { private abstract class AbstractHeaderFooterOperationModeHandler extends OperationModeHandler { /** - * Called when the player receives a {@link HeaderAndFooter} packet. + * Called when the player receives a {@link HeaderAndFooterPacket} packet. *

* This method is called before this {@link AbstractTabOverlayHandler} executes its own logic to update the * server player list info. */ - abstract PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet); + abstract PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooterPacket packet); /** * Called when the player switches the server. @@ -646,7 +652,7 @@ protected PassThroughContentTabOverlay createTabOverlay() { } @Override - PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { + PacketListenerResult onPlayerListPacket(LegacyPlayerListItemPacket packet) { return PacketListenerResult.PASS; } @@ -662,7 +668,7 @@ PacketListenerResult onTeamPacket(Team packet) { @Override void onServerSwitch() { - sendPacket(new HeaderAndFooter(EMPTY_JSON_TEXT, EMPTY_JSON_TEXT)); + sendPacket(HeaderAndFooterPacket.create(Component.empty(), Component.empty(), getProtocol())); } @Override @@ -685,34 +691,34 @@ void onActivated(AbstractContentOperationModeHandler previous) { // fix player list entries if (!serverPlayerList.isEmpty()) { // restore player ping - LegacyPlayerListItem packet; - List items = new ArrayList<>(serverPlayerList.size()); + LegacyPlayerListItemPacket packet; + List items = new ArrayList<>(serverPlayerList.size()); for (PlayerListEntry entry : serverPlayerList.values()) { - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(entry.getUuid()); + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(entry.getUuid()); item.setLatency(entry.getPing()); items.add(item); } - packet = new LegacyPlayerListItem(UPDATE_LATENCY, items); + packet = new LegacyPlayerListItemPacket(UPDATE_LATENCY, items); sendPacket(packet); // restore player gamemode items.clear(); for (PlayerListEntry entry : serverPlayerList.values()) { - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(entry.getUuid()); + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(entry.getUuid()); item.setGameMode(entry.getGamemode()); items.add(item); } - packet = new LegacyPlayerListItem(UPDATE_GAMEMODE, items); + packet = new LegacyPlayerListItemPacket(UPDATE_GAMEMODE, items); sendPacket(packet); // restore player display name items.clear(); for (PlayerListEntry entry : serverPlayerList.values()) { - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(entry.getUuid()); - item.setDisplayName((entry.getDisplayName() != null && !entry.getDisplayName().equalsIgnoreCase("null")) ? GsonComponentSerializer.gson().deserialize(entry.getDisplayName()) : Component.empty()); + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(entry.getUuid()); + item.setDisplayName(entry.getDisplayName()); items.add(item); } - packet = new LegacyPlayerListItem(UPDATE_DISPLAY_NAME, items); + packet = new LegacyPlayerListItemPacket(UPDATE_DISPLAY_NAME, items); sendPacket(packet); } } @@ -730,13 +736,13 @@ protected PassThroughHeaderFooterTabOverlay createTabOverlay() { } @Override - PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { + PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooterPacket packet) { return PacketListenerResult.PASS; } @Override void onServerSwitch() { - sendPacket(new HeaderAndFooter(EMPTY_JSON_TEXT, EMPTY_JSON_TEXT)); + sendPacket(HeaderAndFooterPacket.create(Component.empty(), Component.empty(), getProtocol())); } @Override @@ -757,7 +763,7 @@ void onActivated(AbstractHeaderFooterOperationModeHandler previous) { } // fix header/ footer - sendPacket(new HeaderAndFooter(serverHeader != null ? serverHeader : EMPTY_JSON_TEXT, serverFooter != null ? serverFooter : EMPTY_JSON_TEXT)); + sendPacket(HeaderAndFooterPacket.create(serverHeader != null ? serverHeader : Component.empty(), serverFooter != null ? serverFooter : Component.empty(), getProtocol())); } } @@ -795,10 +801,10 @@ private abstract class CustomContentTabOverlayHandler playerUsernameToSlotMap; boolean canShrink = false; - private final List itemQueueAddPlayer; - private final List itemQueueRemovePlayer; - private final List itemQueueUpdateDisplayName; - private final List itemQueueUpdatePing; + private final List itemQueueAddPlayer; + private final List itemQueueRemovePlayer; + private final List itemQueueUpdateDisplayName; + private final List itemQueueUpdatePing; private final boolean experimentalTabCompleteFixForTabSize80 = isExperimentalTabCompleteFixForTabSize80(); private final boolean experimentalTabCompleteSmileys = isExperimentalTabCompleteSmileys(); @@ -825,7 +831,7 @@ private CustomContentTabOverlayHandler() { } @Override - PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { + PacketListenerResult onPlayerListPacket(LegacyPlayerListItemPacket packet) { int action = packet.getAction(); @@ -874,15 +880,15 @@ PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { switch (action) { case ADD_PLAYER: - List items = packet.getItems(); + List items = packet.getItems(); if (!using80Slots) { - for (LegacyPlayerListItem.Item item : items) { + for (LegacyPlayerListItemPacket.Item item : items) { if (!viewerUuid.equals(item.getUuid())) { item.setGameMode(0); } } - for (LegacyPlayerListItem.Item item : items) { + for (LegacyPlayerListItemPacket.Item item : items) { UUID uuid = item.getUuid(); int index = playerUuidToSlotMap.getInt(uuid); if (index == -1) { @@ -892,7 +898,7 @@ PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { dirtySlots.set(index); needUpdate = true; } else { - item.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: check formatting + item.setDisplayName(tabOverlay.text[index]); item.setLatency(tabOverlay.ping[index]); tabOverlay.dirtyFlagsText.clear(index); tabOverlay.dirtyFlagsPing.clear(index); @@ -916,7 +922,7 @@ PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { tabOverlay.dirtyFlagSize = true; } } else { - for (LegacyPlayerListItem.Item item : packet.getItems()) { + for (LegacyPlayerListItemPacket.Item item : packet.getItems()) { if (!playerToTeamMap.containsKey(item.getName())) { sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[80], new String[]{item.getName()})); } @@ -932,7 +938,7 @@ PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { case UPDATE_GAMEMODE: if (viewerGamemodeChanged) { items = packet.getItems(); - for (LegacyPlayerListItem.Item item : items) { + for (LegacyPlayerListItemPacket.Item item : items) { if (!viewerUuid.equals(item.getUuid())) { item.setGameMode(0); } @@ -950,7 +956,7 @@ PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { case REMOVE_PLAYER: if (!using80Slots) { items = packet.getItems(); - for (LegacyPlayerListItem.Item item : items) { + for (LegacyPlayerListItemPacket.Item item : items) { int index = playerUuidToSlotMap.removeInt(item.getUuid()); if (index == -1) { if (OPTION_ENABLE_CONSISTENCY_CHECKS) { @@ -970,11 +976,7 @@ PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { // 2. add player to correct team sendPacket(createPacketTeamAddPlayers(playerTeamName, new String[]{slotUsername[index]})); // 3. reset custom slot team - if (is13OrLater) { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); - } else { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], "", "", "", "always", "always", 0, (byte) 1)); - } + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_COMPONENT, EMPTY_COMPONENT, EMPTY_COMPONENT, Team.NameTagVisibility.ALWAYS, Team.CollisionRule.ALWAYS, is13OrLater ? 21 : 0, (byte) 1)); } } @@ -991,16 +993,16 @@ PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { } slotState[index] = SlotState.CUSTOM; slotUuid[index] = customSlotUuid; - LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); + LegacyPlayerListItemPacket.Item item1 = new LegacyPlayerListItemPacket.Item(customSlotUuid); item1.setName(slotUsername[index] = getCustomSlotUsername(index)); Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); - item1.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check formatting + item1.setDisplayName(tabOverlay.text[index]); item1.setLatency(tabOverlay.ping[index]); item1.setGameMode(0); - LegacyPlayerListItem packet1 = new LegacyPlayerListItem(ADD_PLAYER, List.of(item1)); + LegacyPlayerListItemPacket packet1 = new LegacyPlayerListItemPacket(ADD_PLAYER, List.of(item1)); sendPacket(packet1); if (is18) { - packet1 = new LegacyPlayerListItem(UPDATE_DISPLAY_NAME, List.of(item1)); + packet1 = new LegacyPlayerListItemPacket(UPDATE_DISPLAY_NAME, List.of(item1)); sendPacket(packet1); } } @@ -1045,11 +1047,7 @@ void onTeamPacketPreprocess(Team packet) { int slot = playerUsernameToSlotMap.getInt(playerName); if (slot != -1) { // reset slot team - if (is13OrLater) { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[slot], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); - } else { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[slot], "", "", "", "always", "always", 0, (byte) 1)); - } + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[slot], EMPTY_COMPONENT, EMPTY_COMPONENT, EMPTY_COMPONENT, Team.NameTagVisibility.ALWAYS, Team.CollisionRule.ALWAYS, is13OrLater ? 21 : 0, (byte) 1)); } } } @@ -1127,11 +1125,7 @@ PacketListenerResult onTeamPacket(Team packet) { filteredPlayers[j++] = playerName; } else { // reset slot team - if (is13OrLater) { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[slot], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); - } else { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[slot], "", "", "", "always", "always", 0, (byte) 1)); - } + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[slot], EMPTY_COMPONENT, EMPTY_COMPONENT, EMPTY_COMPONENT, Team.NameTagVisibility.ALWAYS, Team.CollisionRule.ALWAYS, is13OrLater ? 21 : 0, (byte) 1)); } } packet.setPlayers(filteredPlayers); @@ -1205,11 +1199,7 @@ void onServerSwitch() { // 1. remove player from team sendPacket(createPacketTeamRemovePlayers(CUSTOM_SLOT_TEAMNAME[index], new String[]{slotUsername[index]})); // reset slot team - if (is13OrLater) { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); - } else { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], "", "", "", "always", "always", 0, (byte) 1)); - } + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_COMPONENT, EMPTY_COMPONENT, EMPTY_COMPONENT, Team.NameTagVisibility.ALWAYS, Team.CollisionRule.ALWAYS, is13OrLater ? 21 : 0, (byte) 1)); } // 2. create new custom slot @@ -1225,16 +1215,16 @@ void onServerSwitch() { } slotState[index] = SlotState.CUSTOM; slotUuid[index] = customSlotUuid; - LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); + LegacyPlayerListItemPacket.Item item1 = new LegacyPlayerListItemPacket.Item(customSlotUuid); item1.setName(slotUsername[index] = getCustomSlotUsername(index)); Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); - item1.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check formatting + item1.setDisplayName(tabOverlay.text[index]); item1.setLatency(tabOverlay.ping[index]); item1.setGameMode(0); - LegacyPlayerListItem packet1 = new LegacyPlayerListItem(ADD_PLAYER, List.of(item1)); + LegacyPlayerListItemPacket packet1 = new LegacyPlayerListItemPacket(ADD_PLAYER, List.of(item1)); sendPacket(packet1); if (is18) { - packet1 = new LegacyPlayerListItem(UPDATE_DISPLAY_NAME, List.of(item1)); + packet1 = new LegacyPlayerListItemPacket(UPDATE_DISPLAY_NAME, List.of(item1)); sendPacket(packet1); } } @@ -1264,19 +1254,19 @@ void onActivated(AbstractContentOperationModeHandler previous) { } if (count > 0) { - LegacyPlayerListItem.Item[] items = new LegacyPlayerListItem.Item[count]; + LegacyPlayerListItemPacket.Item[] items = new LegacyPlayerListItemPacket.Item[count]; int index = 0; for (Map.Entry mEntry : serverPlayerList.entrySet()) { PlayerListEntry entry = mEntry.getValue(); if (entry != viewerEntry && entry.getGamemode() == 3) { - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(mEntry.getKey()); + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(mEntry.getKey()); item.setGameMode(0); items[index++] = item; } } - LegacyPlayerListItem packet = new LegacyPlayerListItem(UPDATE_GAMEMODE, Arrays.asList(items)); + LegacyPlayerListItemPacket packet = new LegacyPlayerListItemPacket(UPDATE_GAMEMODE, Arrays.asList(items)); sendPacket(packet); } } @@ -1304,26 +1294,12 @@ private void createTeamsIfNecessary() { if (!hasCreatedCustomTeams) { hasCreatedCustomTeams = true; - if (is13OrLater) { - sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[0], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[0], CUSTOM_SLOT_USERNAME_SMILEYS[0], ""})); - } else { - sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[0], "", "", "", "always", "always", 0, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[0], CUSTOM_SLOT_USERNAME_SMILEYS[0], ""})); - } + sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[0], EMPTY_COMPONENT, EMPTY_COMPONENT, EMPTY_COMPONENT, Team.NameTagVisibility.ALWAYS, Team.CollisionRule.ALWAYS, is13OrLater ? 21 : 0, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[0], CUSTOM_SLOT_USERNAME_SMILEYS[0], ""})); for (int i = 1; i < 80; i++) { - if (is13OrLater) { - sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[i], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[i], CUSTOM_SLOT_USERNAME_SMILEYS[i]})); - } else { - sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[i], "", "", "", "always", "always", 0, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[i], CUSTOM_SLOT_USERNAME_SMILEYS[i]})); - } + sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[i], EMPTY_COMPONENT, EMPTY_COMPONENT, EMPTY_COMPONENT, Team.NameTagVisibility.ALWAYS, Team.CollisionRule.ALWAYS, is13OrLater ? 21 : 0, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[i], CUSTOM_SLOT_USERNAME_SMILEYS[i]})); } - if (is13OrLater) { - sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[80], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[80]})); - } else { - sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[80], "", "", "", "always", "always", 0, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[80]})); - } - - areCustomSlotUsersPartOfTeams = true; + sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[80], EMPTY_COMPONENT, EMPTY_COMPONENT, EMPTY_COMPONENT, Team.NameTagVisibility.ALWAYS, Team.CollisionRule.ALWAYS, is13OrLater ? 21 : 0, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[80]})); } } @@ -1342,11 +1318,7 @@ void onDeactivated() { // 2. add player to correct team sendPacket(createPacketTeamAddPlayers(playerTeamName, new String[]{slotUsername[index]})); // 3. reset custom slot team - if (is13OrLater) { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); - } else { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], "", "", "", "always", "always", 0, (byte) 1)); - } + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_COMPONENT, EMPTY_COMPONENT, EMPTY_COMPONENT, Team.NameTagVisibility.ALWAYS, Team.CollisionRule.ALWAYS, is13OrLater ? 21 : 0, (byte) 1)); } } else { customSlots++; @@ -1369,21 +1341,21 @@ void onDeactivated() { int i = 0; if (customSlots > 0) { - LegacyPlayerListItem.Item[] items = new LegacyPlayerListItem.Item[customSlots]; + LegacyPlayerListItemPacket.Item[] items = new LegacyPlayerListItemPacket.Item[customSlots]; for (int index = 0; index < 80; index++) { // switch slot from custom to unused if (slotState[index] == SlotState.CUSTOM) { - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUuid[index]); + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(slotUuid[index]); items[i++] = item; } } if (experimentalTabCompleteFixForTabSize80 && using80Slots) { for (int j = 0; j < 17; j++) { - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(CUSTOM_SLOT_UUID_SPACER[j]); + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(CUSTOM_SLOT_UUID_SPACER[j]); items[i++] = item; } } - LegacyPlayerListItem packet = new LegacyPlayerListItem(REMOVE_PLAYER, Arrays.asList(items)); + LegacyPlayerListItemPacket packet = new LegacyPlayerListItemPacket(REMOVE_PLAYER, Arrays.asList(items)); sendPacket(packet); } } @@ -1425,26 +1397,26 @@ void update() { } if (count > 0) { - LegacyPlayerListItem.Item[] items = new LegacyPlayerListItem.Item[count]; + LegacyPlayerListItemPacket.Item[] items = new LegacyPlayerListItemPacket.Item[count]; int index = 0; for (Map.Entry mEntry : serverPlayerList.entrySet()) { PlayerListEntry entry = mEntry.getValue(); if (entry != viewerEntry && entry.getGamemode() == 3) { - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(mEntry.getKey()); + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(mEntry.getKey()); item.setGameMode(0); items[index++] = item; } } - LegacyPlayerListItem packet = new LegacyPlayerListItem(UPDATE_GAMEMODE, Arrays.asList(items)); + LegacyPlayerListItemPacket packet = new LegacyPlayerListItemPacket(UPDATE_GAMEMODE, Arrays.asList(items)); sendPacket(packet); } // remove spacer slots if (experimentalTabCompleteFixForTabSize80) { for (int i = 0; i < 17; i++) { - LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(CUSTOM_SLOT_UUID_SPACER[i]); + LegacyPlayerListItemPacket.Item item1 = new LegacyPlayerListItemPacket.Item(CUSTOM_SLOT_UUID_SPACER[i]); itemQueueRemovePlayer.add(item1); } } @@ -1470,11 +1442,7 @@ void update() { // 2. add player to correct team sendPacket(createPacketTeamAddPlayers(playerTeamName, new String[]{slotUsername[index]})); // 3. reset custom slot team - if (is13OrLater) { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); - } else { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], "", "", "", "always", "always", 0, (byte) 1)); - } + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_COMPONENT, EMPTY_COMPONENT, EMPTY_COMPONENT, Team.NameTagVisibility.ALWAYS, Team.CollisionRule.ALWAYS, is13OrLater ? 21 : 0, (byte) 1)); } else { // 2. add player to overflow team sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[80], new String[]{slotUsername[index]})); @@ -1493,17 +1461,17 @@ void update() { } slotState[index] = SlotState.CUSTOM; slotUuid[index] = customSlotUuid; - LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); + LegacyPlayerListItemPacket.Item item1 = new LegacyPlayerListItemPacket.Item(customSlotUuid); item1.setName(slotUsername[index] = getCustomSlotUsername(index)); Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); - item1.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check Formatting + item1.setDisplayName(tabOverlay.text[index]); item1.setLatency(tabOverlay.ping[index]); item1.setGameMode(0); itemQueueAddPlayer.add(item1); } else { // custom if (slotState[index] == SlotState.CUSTOM) { - LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(slotUuid[index]); + LegacyPlayerListItemPacket.Item item1 = new LegacyPlayerListItemPacket.Item(slotUuid[index]); itemQueueRemovePlayer.add(item1); } // unused @@ -1519,10 +1487,10 @@ void update() { } slotState[index] = SlotState.CUSTOM; slotUuid[index] = customSlotUuid; - LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); + LegacyPlayerListItemPacket.Item item1 = new LegacyPlayerListItemPacket.Item(customSlotUuid); item1.setName(slotUsername[index] = getCustomSlotUsername(index)); Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); - item1.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check Formatting + item1.setDisplayName(tabOverlay.text[index]); item1.setLatency(tabOverlay.ping[index]); item1.setGameMode(0); itemQueueAddPlayer.add(item1); @@ -1530,15 +1498,15 @@ void update() { } // restore player gamemode - LegacyPlayerListItem packet; - List items = new ArrayList<>(serverPlayerList.size()); + LegacyPlayerListItemPacket packet; + List items = new ArrayList<>(serverPlayerList.size()); items.clear(); for (PlayerListEntry entry : serverPlayerList.values()) { - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(entry.getUuid()); + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(entry.getUuid()); item.setGameMode(entry.getGamemode()); items.add(item); } - packet = new LegacyPlayerListItem(UPDATE_GAMEMODE, items); + packet = new LegacyPlayerListItemPacket(UPDATE_GAMEMODE, items); sendPacket(packet); for (UUID player : freePlayers) { @@ -1556,7 +1524,7 @@ void update() { // create spacer slots if (experimentalTabCompleteFixForTabSize80) { for (int i = 0; i < 17; i++) { - LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(CUSTOM_SLOT_UUID_SPACER[i]); + LegacyPlayerListItemPacket.Item item1 = new LegacyPlayerListItemPacket.Item(CUSTOM_SLOT_UUID_SPACER[i]); item1.setName(""); Property119Handler.setProperties(item1, EMPTY_PROPERTIES_ARRAY); item1.setDisplayName(null); @@ -1656,11 +1624,7 @@ void update() { playerUsernameToSlotMap.removeInt(slotUsername[index]); // reset slot team - if (is13OrLater) { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); - } else { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], "", "", "", "always", "always", 0, (byte) 1)); - } + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_COMPONENT, EMPTY_COMPONENT, EMPTY_COMPONENT, Team.NameTagVisibility.ALWAYS, Team.CollisionRule.ALWAYS, is13OrLater ? 21 : 0, (byte) 1)); } // 2. update slot state @@ -1681,17 +1645,13 @@ void update() { playerUsernameToSlotMap.removeInt(slotUsername[index]); // reset slot team - if (is13OrLater) { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1)); - } else { - sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], "", "", "", "always", "always", 0, (byte) 1)); - } + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_COMPONENT, EMPTY_COMPONENT, EMPTY_COMPONENT, Team.NameTagVisibility.ALWAYS, Team.CollisionRule.ALWAYS, is13OrLater ? 21 : 0, (byte) 1)); } freePlayers.add(slotUuid[index]); } else { // 1. remove custom slot player - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUuid[index]); + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(slotUuid[index]); itemQueueRemovePlayer.add(item); } @@ -1711,7 +1671,7 @@ void update() { } } // switch slot 'highestUsedSlotIndex' from custom to unused - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUuid[highestUsedSlotIndex]); + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(slotUuid[highestUsedSlotIndex]); itemQueueRemovePlayer.add(item); } // switch slot 'highestUsedSlotIndex' from unused to player with 'viewerUuid' @@ -1727,12 +1687,12 @@ void update() { // 3. Add to new team sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[highestUsedSlotIndex], new String[]{playerUsername})); // 4. Update display name - LegacyPlayerListItem.Item itemUpdateDisplayName = new LegacyPlayerListItem.Item(viewerUuid); + LegacyPlayerListItemPacket.Item itemUpdateDisplayName = new LegacyPlayerListItemPacket.Item(viewerUuid); tabOverlay.dirtyFlagsText.clear(highestUsedSlotIndex); - itemUpdateDisplayName.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[highestUsedSlotIndex])); // TODO: Check Formatting + itemUpdateDisplayName.setDisplayName(tabOverlay.text[highestUsedSlotIndex]); itemQueueUpdateDisplayName.add(itemUpdateDisplayName); // 5. Update ping - LegacyPlayerListItem.Item itemUpdatePing = new LegacyPlayerListItem.Item(viewerUuid); + LegacyPlayerListItemPacket.Item itemUpdatePing = new LegacyPlayerListItemPacket.Item(viewerUuid); tabOverlay.dirtyFlagsPing.clear(highestUsedSlotIndex); itemUpdatePing.setLatency(tabOverlay.ping[highestUsedSlotIndex]); itemQueueUpdatePing.add(itemUpdatePing); @@ -1755,7 +1715,7 @@ void update() { // switch slot to player mode using player with 'uuid' if (slotState[index] == SlotState.CUSTOM) { // custom -> unused - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUuid[index]); + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(slotUuid[index]); itemQueueRemovePlayer.add(item); } String playerUsername = serverPlayerList.get(uuid).getUsername(); @@ -1770,12 +1730,12 @@ void update() { // 3. Add to new team sendPacket(createPacketTeamAddPlayers(CUSTOM_SLOT_TEAMNAME[index], new String[]{playerUsername})); // 4. Update display name - LegacyPlayerListItem.Item itemUpdateDisplayName = new LegacyPlayerListItem.Item(uuid); + LegacyPlayerListItemPacket.Item itemUpdateDisplayName = new LegacyPlayerListItemPacket.Item(uuid); tabOverlay.dirtyFlagsText.clear(index); - itemUpdateDisplayName.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check Formatting + itemUpdateDisplayName.setDisplayName(tabOverlay.text[index]); itemQueueUpdateDisplayName.add(itemUpdateDisplayName); // 5. Update ping - LegacyPlayerListItem.Item itemUpdatePing = new LegacyPlayerListItem.Item(uuid); + LegacyPlayerListItemPacket.Item itemUpdatePing = new LegacyPlayerListItemPacket.Item(uuid); tabOverlay.dirtyFlagsPing.clear(index); itemUpdatePing.setLatency(tabOverlay.ping[index]); itemQueueUpdatePing.add(itemUpdatePing); @@ -1810,7 +1770,7 @@ void update() { // switch slot to player mode using the player 'uuid' if (slotState[index] == SlotState.CUSTOM) { // custom -> unused - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUuid[index]); + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(slotUuid[index]); itemQueueRemovePlayer.add(item); } String playerUsername = serverPlayerList.get(uuid).getUsername(); @@ -1828,12 +1788,12 @@ void update() { playerUsernameToSlotMap.put(playerUsername, index); } // 4. Update display name - LegacyPlayerListItem.Item itemUpdateDisplayName = new LegacyPlayerListItem.Item(uuid); + LegacyPlayerListItemPacket.Item itemUpdateDisplayName = new LegacyPlayerListItemPacket.Item(uuid); tabOverlay.dirtyFlagsText.clear(index); - itemUpdateDisplayName.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check Formatting + itemUpdateDisplayName.setDisplayName(tabOverlay.text[index]); itemQueueUpdateDisplayName.add(itemUpdateDisplayName); // 5. Update ping - LegacyPlayerListItem.Item itemUpdatePing = new LegacyPlayerListItem.Item(uuid); + LegacyPlayerListItemPacket.Item itemUpdatePing = new LegacyPlayerListItemPacket.Item(uuid); tabOverlay.dirtyFlagsPing.clear(index); itemUpdatePing.setLatency(tabOverlay.ping[index]); itemQueueUpdatePing.add(itemUpdatePing); @@ -1856,7 +1816,7 @@ void update() { if (usedSlots.get(index)) { if (slotState[index] == SlotState.UNUSED || (updateAllCustomSlots && slotState[index] == SlotState.CUSTOM)) { if (slotState[index] == SlotState.CUSTOM) { - LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(slotUuid[index]); + LegacyPlayerListItemPacket.Item item1 = new LegacyPlayerListItemPacket.Item(slotUuid[index]); itemQueueRemovePlayer.add(item1); } tabOverlay.dirtyFlagsIcon.clear(index); @@ -1871,10 +1831,10 @@ void update() { } slotState[index] = SlotState.CUSTOM; slotUuid[index] = customSlotUuid; - LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); + LegacyPlayerListItemPacket.Item item1 = new LegacyPlayerListItemPacket.Item(customSlotUuid); item1.setName(slotUsername[index] = getCustomSlotUsername(index)); Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); - item1.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check Formatting + item1.setDisplayName(tabOverlay.text[index]); item1.setLatency(tabOverlay.ping[index]); item1.setGameMode(0); itemQueueAddPlayer.add(item1); @@ -1901,17 +1861,17 @@ void update() { customSlotUuid = CUSTOM_SLOT_UUID_STEVE[index]; } if (!customSlotUuid.equals(slotUuid[index]) || is119OrLater) { - LegacyPlayerListItem.Item itemRemove = new LegacyPlayerListItem.Item(slotUuid[index]); + LegacyPlayerListItemPacket.Item itemRemove = new LegacyPlayerListItemPacket.Item(slotUuid[index]); itemQueueRemovePlayer.add(itemRemove); } tabOverlay.dirtyFlagsText.clear(index); tabOverlay.dirtyFlagsPing.clear(index); slotState[index] = SlotState.CUSTOM; slotUuid[index] = customSlotUuid; - LegacyPlayerListItem.Item item1 = new LegacyPlayerListItem.Item(customSlotUuid); + LegacyPlayerListItemPacket.Item item1 = new LegacyPlayerListItemPacket.Item(customSlotUuid); item1.setName(slotUsername[index] = getCustomSlotUsername(index)); Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); - item1.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check Formatting + item1.setDisplayName(tabOverlay.text[index]); item1.setLatency(tabOverlay.ping[index]); item1.setGameMode(0); itemQueueAddPlayer.add(item1); @@ -1922,8 +1882,8 @@ void update() { dirtySlots.copyAndClear(tabOverlay.dirtyFlagsText); for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { if (slotState[index] != SlotState.UNUSED) { - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUuid[index]); - item.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); // TODO: Check Formatting + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(slotUuid[index]); + item.setDisplayName(tabOverlay.text[index]); itemQueueUpdateDisplayName.add(item); } } @@ -1932,7 +1892,7 @@ void update() { dirtySlots.copyAndClear(tabOverlay.dirtyFlagsPing); for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { if (slotState[index] != SlotState.UNUSED) { - LegacyPlayerListItem.Item item = new LegacyPlayerListItem.Item(slotUuid[index]); + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(slotUuid[index]); item.setLatency(tabOverlay.ping[index]); itemQueueUpdatePing.add(item); } @@ -1946,26 +1906,26 @@ void update() { private void sendQueuedItems() { if (!itemQueueRemovePlayer.isEmpty()) { - LegacyPlayerListItem packet = new LegacyPlayerListItem(REMOVE_PLAYER, itemQueueRemovePlayer); + LegacyPlayerListItemPacket packet = new LegacyPlayerListItemPacket(REMOVE_PLAYER, itemQueueRemovePlayer); sendPacket(packet); itemQueueRemovePlayer.clear(); } if (!itemQueueAddPlayer.isEmpty()) { - LegacyPlayerListItem packet = new LegacyPlayerListItem(ADD_PLAYER, itemQueueAddPlayer); + LegacyPlayerListItemPacket packet = new LegacyPlayerListItemPacket(ADD_PLAYER, itemQueueAddPlayer); sendPacket(packet); if (is18) { - packet = new LegacyPlayerListItem(UPDATE_DISPLAY_NAME, itemQueueAddPlayer); + packet = new LegacyPlayerListItemPacket(UPDATE_DISPLAY_NAME, itemQueueAddPlayer); sendPacket(packet); } itemQueueAddPlayer.clear(); } if (!itemQueueUpdateDisplayName.isEmpty()) { - LegacyPlayerListItem packet = new LegacyPlayerListItem(UPDATE_DISPLAY_NAME, itemQueueUpdateDisplayName); + LegacyPlayerListItemPacket packet = new LegacyPlayerListItemPacket(UPDATE_DISPLAY_NAME, itemQueueUpdateDisplayName); sendPacket(packet); itemQueueUpdateDisplayName.clear(); } if (!itemQueueUpdatePing.isEmpty()) { - LegacyPlayerListItem packet = new LegacyPlayerListItem(UPDATE_LATENCY, itemQueueUpdatePing); + LegacyPlayerListItemPacket packet = new LegacyPlayerListItemPacket(UPDATE_LATENCY, itemQueueUpdatePing); sendPacket(packet); itemQueueUpdatePing.clear(); } @@ -1985,7 +1945,7 @@ private void sendQueuedItems() { private abstract class CustomContentTabOverlay extends AbstractContentTabOverlay implements TabOverlayHandle.BatchModifiable { final UUID[] uuid; final Icon[] icon; - final String[] text; + final Component[] text; final int[] ping; final AtomicInteger batchUpdateRecursionLevel; @@ -1999,8 +1959,8 @@ private CustomContentTabOverlay() { this.uuid = new UUID[80]; this.icon = new Icon[80]; Arrays.fill(this.icon, Icon.DEFAULT_STEVE); - this.text = new String[80]; - Arrays.fill(this.text, EMPTY_JSON_TEXT); + this.text = new Component[80]; + Arrays.fill(this.text, Component.empty()); this.ping = new int[80]; this.batchUpdateRecursionLevel = new AtomicInteger(0); this.dirtyFlagSize = true; @@ -2054,9 +2014,9 @@ void setIconInternal(int index, @Nonnull @NonNull Icon icon) { } void setTextInternal(int index, @Nonnull @NonNull String text) { - String jsonText = ChatFormat.formattedTextToJson(text); - if (!jsonText.equals(this.text[index])) { - this.text[index] = jsonText; + Component component = GsonComponentSerializer.gson().deserialize(ChatFormat.formattedTextToJson(text)); + if (!component.equals(this.text[index])) { + this.text[index] = component; dirtyFlagsText.set(index); scheduleUpdateIfNotInBatch(); } @@ -2140,7 +2100,7 @@ public void setSize(@Nonnull Dimension size) { if (!oldUsedSlots.get(index)) { uuid[index] = null; icon[index] = Icon.DEFAULT_STEVE; - text[index] = EMPTY_JSON_TEXT; + text[index] = Component.empty(); ping[index] = 0; } } @@ -2151,7 +2111,7 @@ public void setSize(@Nonnull Dimension size) { if (!newUsedSlots.get(index)) { uuid[index] = null; icon[index] = Icon.DEFAULT_STEVE; - text[index] = EMPTY_JSON_TEXT; + text[index] = Component.empty(); ping[index] = 0; } } @@ -2352,7 +2312,7 @@ protected CustomHeaderAndFooterImpl createTabOverlay() { } @Override - PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { + PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooterPacket packet) { return PacketListenerResult.CANCEL; } @@ -2369,7 +2329,7 @@ void onDeactivated() { @Override void onActivated(AbstractHeaderFooterOperationModeHandler previous) { // remove header/ footer - sendPacket(new HeaderAndFooter(EMPTY_JSON_TEXT, EMPTY_JSON_TEXT)); + sendPacket(HeaderAndFooterPacket.create(Component.empty(), Component.empty(), getProtocol())); } @Override @@ -2377,14 +2337,14 @@ void update() { CustomHeaderAndFooterImpl tabOverlay = getTabOverlay(); if (tabOverlay.headerOrFooterDirty) { tabOverlay.headerOrFooterDirty = false; - sendPacket(new HeaderAndFooter(tabOverlay.header, tabOverlay.footer)); + sendPacket(HeaderAndFooterPacket.create(tabOverlay.header, tabOverlay.footer, getProtocol())); } } } private final class CustomHeaderAndFooterImpl extends AbstractHeaderFooterTabOverlay implements HeaderAndFooterHandle { - private String header = EMPTY_JSON_TEXT; - private String footer = EMPTY_JSON_TEXT; + private Component header = Component.empty(); + private Component footer = Component.empty(); private volatile boolean headerOrFooterDirty = false; @@ -2419,22 +2379,22 @@ void scheduleUpdateIfNotInBatch() { @Override public void setHeaderFooter(@Nullable String header, @Nullable String footer) { - this.header = ChatFormat.formattedTextToJson(header); - this.footer = ChatFormat.formattedTextToJson(footer); + this.header = GsonComponentSerializer.gson().deserialize(ChatFormat.formattedTextToJson(header)); + this.footer = GsonComponentSerializer.gson().deserialize(ChatFormat.formattedTextToJson(footer)); headerOrFooterDirty = true; scheduleUpdateIfNotInBatch(); } @Override public void setHeader(@Nullable String header) { - this.header = ChatFormat.formattedTextToJson(header); + this.header = GsonComponentSerializer.gson().deserialize(ChatFormat.formattedTextToJson(header)); headerOrFooterDirty = true; scheduleUpdateIfNotInBatch(); } @Override public void setFooter(@Nullable String footer) { - this.footer = ChatFormat.formattedTextToJson(footer); + this.footer = GsonComponentSerializer.gson().deserialize(ChatFormat.formattedTextToJson(footer)); headerOrFooterDirty = true; scheduleUpdateIfNotInBatch(); } @@ -2455,7 +2415,7 @@ private static String[][] toPropertiesArray(ProfileProperty textureProperty) { } } - private static Team createPacketTeamCreate(String name, String displayName, String prefix, String suffix, String nameTagVisibility, String collisionRule, int color, byte friendlyFire, String[] players) { + private static Team createPacketTeamCreate(String name, ComponentHolder displayName, ComponentHolder prefix, ComponentHolder suffix, Team.NameTagVisibility nameTagVisibility, Team.CollisionRule collisionRule, int color, byte friendlyFire, String[] players) { Team team = new Team(); team.setName(name); team.setMode((byte) 0); @@ -2479,7 +2439,7 @@ private static Team createPacketTeamRemove(String name) { return team; } - private static Team createPacketTeamUpdate(String name, String displayName, String prefix, String suffix, String nameTagVisibility, String collisionRule, int color, byte friendlyFire) { + private static Team createPacketTeamUpdate(String name, ComponentHolder displayName, ComponentHolder prefix, ComponentHolder suffix, Team.NameTagVisibility nameTagVisibility, Team.CollisionRule collisionRule, int color, byte friendlyFire) { Team team = new Team(); team.setName(name); team.setMode((byte) 2); @@ -2522,24 +2482,24 @@ static class PlayerListEntry { private UUID uuid; private String[][] properties; private String username; - private String displayName; + private Component displayName; private int ping; private int gamemode; - private PlayerListEntry(LegacyPlayerListItem.Item item) { - this(item.getUuid(), null, item.getName(), GsonComponentSerializer.gson().serialize(item.getDisplayName()), item.getLatency(), item.getGameMode()); // TODO: Check Display Name + private PlayerListEntry(LegacyPlayerListItemPacket.Item item) { + this(item.getUuid(), null, item.getName(), item.getDisplayName(), item.getLatency(), item.getGameMode()); // TODO: Check Display Name properties = Property119Handler.getProperties(item); } } @Data static class TeamEntry { - private String displayName; - private String prefix; - private String suffix; + private ComponentHolder displayName; + private ComponentHolder prefix; + private ComponentHolder suffix; private byte friendlyFire; - private String nameTagVisibility; - private String collisionRule; + private Team.NameTagVisibility nameTagVisibility; + private Team.CollisionRule collisionRule; private int color; private Set players = new ObjectOpenHashSet<>(); @@ -2550,13 +2510,5 @@ void addPlayer(String name) { void removePlayer(String name) { players.remove(name); } - - public void setNameTagVisibility(String nameTagVisibility) { - this.nameTagVisibility = nameTagVisibility.intern(); - } - - public void setCollisionRule(String collisionRule) { - this.collisionRule = collisionRule == null ? null : collisionRule.intern(); - } } } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/GetGamemodeLogic.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/GetGamemodeLogic.java index 58fad601..86342572 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/GetGamemodeLogic.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/GetGamemodeLogic.java @@ -20,9 +20,9 @@ import codecrafter47.bungeetablistplus.protocol.AbstractPacketHandler; import codecrafter47.bungeetablistplus.protocol.PacketHandler; import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; -import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; -import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; -import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket; +import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfoPacket; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket; import java.util.HashMap; import java.util.Map; @@ -41,9 +41,9 @@ public GetGamemodeLogic(PacketHandler parent, UUID uuid) { } @Override - public PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { - if (packet.getAction() == LegacyPlayerListItem.ADD_PLAYER || packet.getAction() == LegacyPlayerListItem.UPDATE_GAMEMODE) { - for (LegacyPlayerListItem.Item item : packet.getItems()) { + public PacketListenerResult onPlayerListPacket(LegacyPlayerListItemPacket packet) { + if (packet.getAction() == LegacyPlayerListItemPacket.ADD_PLAYER || packet.getAction() == LegacyPlayerListItemPacket.UPDATE_GAMEMODE) { + for (LegacyPlayerListItemPacket.Item item : packet.getItems()) { if (uuid.equals(item.getUuid())) { gameModes.put(uuid, item.getGameMode()); } @@ -53,9 +53,9 @@ public PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { } @Override - public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { - if (packet.getActions().contains(UpsertPlayerInfo.Action.UPDATE_GAME_MODE)) { - for (UpsertPlayerInfo.Entry entry : packet.getEntries()) { + public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfoPacket packet) { + if (packet.getActions().contains(UpsertPlayerInfoPacket.Action.UPDATE_GAME_MODE)) { + for (UpsertPlayerInfoPacket.Entry entry : packet.getEntries()) { if (uuid.equals(entry.getProfileId())) { gameModes.put(uuid, entry.getGameMode()); } @@ -65,7 +65,7 @@ public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { } @Override - public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet) { + public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfoPacket packet) { return super.onPlayerListRemovePacket(packet); } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LegacyTabOverlayHandlerImpl.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LegacyTabOverlayHandlerImpl.java index e7511626..89ee6337 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LegacyTabOverlayHandlerImpl.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LegacyTabOverlayHandlerImpl.java @@ -20,7 +20,7 @@ import codecrafter47.bungeetablistplus.util.ReflectionUtil; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.proxy.protocol.MinecraftPacket; -import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket; import lombok.SneakyThrows; import java.util.concurrent.Executor; @@ -40,7 +40,7 @@ public LegacyTabOverlayHandlerImpl(Logger logger, int playerListSize, Executor e @SneakyThrows @Override protected void sendPacket(MinecraftPacket packet) { - if ((packet instanceof LegacyPlayerListItem) && (player.getProtocolVersion().getProtocol() >= 761)) { + if ((packet instanceof LegacyPlayerListItemPacket) && (player.getProtocolVersion().getProtocol() >= 761)) { // error if (!logVersionMismatch) { logVersionMismatch = true; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LowMemoryTabOverlayHandlerImpl.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LowMemoryTabOverlayHandlerImpl.java index 7dc7282d..7135d100 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LowMemoryTabOverlayHandlerImpl.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LowMemoryTabOverlayHandlerImpl.java @@ -31,8 +31,8 @@ */ public class LowMemoryTabOverlayHandlerImpl extends TabOverlayHandlerImpl { - public LowMemoryTabOverlayHandlerImpl(Logger logger, Executor eventLoopExecutor, UUID viewerUuid, Player player, boolean is18, boolean has113OrLater, boolean has119OrLater) { - super(logger, eventLoopExecutor, viewerUuid, player, is18, has113OrLater, has119OrLater); + public LowMemoryTabOverlayHandlerImpl(Logger logger, Executor eventLoopExecutor, UUID viewerUuid, Player player, boolean is18, boolean has113OrLater, boolean has119OrLater, boolean has1203OrLater) { + super(logger, eventLoopExecutor, viewerUuid, player, is18, has113OrLater, has119OrLater, has1203OrLater); } @Override diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java index 3392602f..fe2b749b 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java @@ -30,10 +30,11 @@ import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.proxy.protocol.MinecraftPacket; -import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; -import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; -import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; -import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; +import com.velocitypowered.proxy.protocol.packet.HeaderAndFooterPacket; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket; +import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfoPacket; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket; +import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; import de.codecrafter47.taboverlay.Icon; import de.codecrafter47.taboverlay.ProfileProperty; import de.codecrafter47.taboverlay.config.misc.ChatFormat; @@ -41,6 +42,7 @@ import de.codecrafter47.taboverlay.handler.*; import it.unimi.dsi.fastutil.objects.*; import lombok.*; +import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import javax.annotation.Nonnull; @@ -61,7 +63,7 @@ public class NewTabOverlayHandler implements PacketHandler, TabOverlayHandler { private static final boolean OPTION_ENABLE_CUSTOM_SLOT_USERNAME_COLLISION_CHECK = true; private static final boolean OPTION_ENABLE_CUSTOM_SLOT_UUID_COLLISION_CHECK = true; - private static final String EMPTY_JSON_TEXT = "{\"text\":\"\"}"; + private static ComponentHolder EMPTY_COMPONENT; protected static final String[][] EMPTY_PROPERTIES_ARRAY = new String[0][]; private static final ImmutableMap DIMENSION_TO_USED_SLOTS; @@ -152,9 +154,9 @@ public class NewTabOverlayHandler implements PacketHandler, TabOverlayHandler { private final Object2BooleanMap serverPlayerListListed = new Object2BooleanOpenHashMap<>(); @Nullable - protected String serverHeader = null; + protected ComponentHolder serverHeader = null; @Nullable - protected String serverFooter = null; + protected ComponentHolder serverFooter = null; private final Queue> nextActiveContentHandlerQueue = new ConcurrentLinkedQueue<>(); private final Queue> nextActiveHeaderFooterHandlerQueue = new ConcurrentLinkedQueue<>(); @@ -178,11 +180,12 @@ public NewTabOverlayHandler(Logger logger, Executor eventLoopExecutor, Player pl this.player = player; this.activeContentHandler = new PassThroughContentHandler(); this.activeHeaderFooterHandler = new PassThroughHeaderFooterHandler(); + EMPTY_COMPONENT = new ComponentHolder(player.getProtocolVersion(), Component.empty()); } @SneakyThrows private void sendPacket(MinecraftPacket packet) { - if (((packet instanceof UpsertPlayerInfo) || (packet instanceof RemovePlayerInfo)) && (player.getProtocolVersion().getProtocol() < 761)) { + if (((packet instanceof UpsertPlayerInfoPacket) || (packet instanceof RemovePlayerInfoPacket)) && (player.getProtocolVersion().getProtocol() < 761)) { // error if (!logVersionMismatch) { logVersionMismatch = true; @@ -194,14 +197,14 @@ private void sendPacket(MinecraftPacket packet) { } @Override - public PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { + public PacketListenerResult onPlayerListPacket(LegacyPlayerListItemPacket packet) { return PacketListenerResult.PASS; } @Override - public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { - if (packet.getActions().contains(UpsertPlayerInfo.Action.ADD_PLAYER)) { - for (UpsertPlayerInfo.Entry entry : packet.getEntries()) { + public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfoPacket packet) { + if (packet.getActions().contains(UpsertPlayerInfoPacket.Action.ADD_PLAYER)) { + for (UpsertPlayerInfoPacket.Entry entry : packet.getEntries()) { if (OPTION_ENABLE_CUSTOM_SLOT_UUID_COLLISION_CHECK) { if (CUSTOM_SLOT_UUIDS.contains(entry.getProfileId())) { throw new AssertionError("UUID collision " + entry.getProfileId()); @@ -215,8 +218,8 @@ public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { serverPlayerListListed.putIfAbsent(entry.getProfileId(), false); } } - if (packet.getActions().contains(UpsertPlayerInfo.Action.UPDATE_LISTED)) { - for (UpsertPlayerInfo.Entry entry : packet.getEntries()) { + if (packet.getActions().contains(UpsertPlayerInfoPacket.Action.UPDATE_LISTED)) { + for (UpsertPlayerInfoPacket.Entry entry : packet.getEntries()) { serverPlayerListListed.put(entry.getProfileId(), entry.isListed()); } } @@ -232,7 +235,7 @@ public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { } @Override - public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet) { + public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfoPacket packet) { for (UUID uuid : packet.getProfilesToRemove()) { serverPlayerListListed.removeBoolean(uuid); } @@ -245,7 +248,7 @@ public PacketListenerResult onTeamPacket(Team packet) { } @Override - public PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { + public PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooterPacket packet) { PacketListenerResult result = PacketListenerResult.PASS; try { result = this.activeHeaderFooterHandler.onPlayerListHeaderFooterPacket(packet); @@ -258,8 +261,8 @@ public PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packe enterHeaderAndFooterOperationMode(HeaderAndFooterOperationMode.PASS_TROUGH); } - this.serverHeader = packet.getHeader() != null ? packet.getHeader() : EMPTY_JSON_TEXT; - this.serverFooter = packet.getFooter() != null ? packet.getFooter() : EMPTY_JSON_TEXT; + this.serverHeader = packet.getHeader() != null ? packet.getHeader() : EMPTY_COMPONENT; + this.serverFooter = packet.getFooter() != null ? packet.getFooter() : EMPTY_COMPONENT; return result; } @@ -289,17 +292,17 @@ public void onServerSwitch(boolean is13OrLater) { } if (!serverPlayerListListed.isEmpty()) { - RemovePlayerInfo packet = new RemovePlayerInfo(); + RemovePlayerInfoPacket packet = new RemovePlayerInfoPacket(); packet.setProfilesToRemove(serverPlayerListListed.keySet()); sendPacket(packet); } serverPlayerListListed.clear(); if (serverHeader != null) { - serverHeader = EMPTY_JSON_TEXT; + serverHeader = EMPTY_COMPONENT; } if (serverFooter != null) { - serverFooter = EMPTY_JSON_TEXT; + serverFooter = EMPTY_COMPONENT; } } } @@ -373,11 +376,11 @@ private void update() { private abstract static class AbstractContentOperationModeHandler extends OperationModeHandler { /** - * Called when the player receives a {@link LegacyPlayerListItem} packet. + * Called when the player receives a {@link LegacyPlayerListItemPacket} packet. *

* This method is called after this {@link NewTabOverlayHandler} has updated the {@code serverPlayerList}. */ - abstract PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet); + abstract PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfoPacket packet); /** * Called when the player switches the server. @@ -420,12 +423,12 @@ final void invalidate() { private abstract static class AbstractHeaderFooterOperationModeHandler extends OperationModeHandler { /** - * Called when the player receives a {@link HeaderAndFooter} packet. + * Called when the player receives a {@link HeaderAndFooterPacket} packet. *

* This method is called before this {@link NewTabOverlayHandler} executes its own logic to update the * server player list info. */ - abstract PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet); + abstract PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooterPacket packet); /** * Called when the player switches the server. @@ -499,13 +502,13 @@ protected PassThroughContentTabOverlay createTabOverlay() { } @Override - PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { + PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfoPacket packet) { return PacketListenerResult.PASS; } @Override void onServerSwitch() { - sendPacket(new HeaderAndFooter(EMPTY_JSON_TEXT, EMPTY_JSON_TEXT)); + sendPacket(new HeaderAndFooterPacket(EMPTY_COMPONENT, EMPTY_COMPONENT)); } @Override @@ -527,15 +530,15 @@ void onActivated(AbstractContentOperationModeHandler previous) { // update visibility if (!serverPlayerListListed.isEmpty()) { - List items = new ArrayList<>(serverPlayerListListed.size()); + List items = new ArrayList<>(serverPlayerListListed.size()); for (Object2BooleanMap.Entry entry : serverPlayerListListed.object2BooleanEntrySet()) { - UpsertPlayerInfo.Entry item = new UpsertPlayerInfo.Entry(entry.getKey()); + UpsertPlayerInfoPacket.Entry item = new UpsertPlayerInfoPacket.Entry(entry.getKey()); item.setListed(entry.getBooleanValue()); items.add(item); } - UpsertPlayerInfo packet = new UpsertPlayerInfo(); - packet.addAction(UpsertPlayerInfo.Action.UPDATE_LISTED); + UpsertPlayerInfoPacket packet = new UpsertPlayerInfoPacket(); + packet.addAction(UpsertPlayerInfoPacket.Action.UPDATE_LISTED); packet.addAllEntries(items); sendPacket(packet); } @@ -554,13 +557,13 @@ protected PassThroughHeaderFooterTabOverlay createTabOverlay() { } @Override - PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { + PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooterPacket packet) { return PacketListenerResult.PASS; } @Override void onServerSwitch() { - sendPacket(new HeaderAndFooter(EMPTY_JSON_TEXT, EMPTY_JSON_TEXT)); + sendPacket(new HeaderAndFooterPacket(EMPTY_COMPONENT, EMPTY_COMPONENT)); } @Override @@ -581,7 +584,7 @@ void onActivated(AbstractHeaderFooterOperationModeHandler previous) { } // fix header/ footer - sendPacket(new HeaderAndFooter(serverHeader != null ? serverHeader : EMPTY_JSON_TEXT, serverFooter != null ? serverFooter : EMPTY_JSON_TEXT)); + sendPacket(new HeaderAndFooterPacket(serverHeader != null ? serverHeader : EMPTY_COMPONENT, serverFooter != null ? serverFooter : EMPTY_COMPONENT)); } } @@ -604,10 +607,10 @@ private abstract class CustomContentTabOverlayHandler itemQueueAddPlayer; + private final List itemQueueAddPlayer; private final List itemQueueRemovePlayer; - private final List itemQueueUpdateDisplayName; - private final List itemQueueUpdatePing; + private final List itemQueueUpdateDisplayName; + private final List itemQueueUpdatePing; private final boolean experimentalTabCompleteSmileys = isExperimentalTabCompleteSmileys(); @@ -625,10 +628,10 @@ private CustomContentTabOverlayHandler() { } @Override - PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { + PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfoPacket packet) { - if (packet.getActions().contains(UpsertPlayerInfo.Action.UPDATE_LISTED)) { - for (UpsertPlayerInfo.Entry entry : packet.getEntries()) { + if (packet.getActions().contains(UpsertPlayerInfoPacket.Action.UPDATE_LISTED)) { + for (UpsertPlayerInfoPacket.Entry entry : packet.getEntries()) { entry.setListed(false); } } @@ -654,14 +657,14 @@ void onActivated(AbstractContentOperationModeHandler previous) { // make all players unlisted if (!serverPlayerListListed.isEmpty()) { - List items = new ArrayList<>(serverPlayerListListed.size()); + List items = new ArrayList<>(serverPlayerListListed.size()); for (Object2BooleanMap.Entry entry : serverPlayerListListed.object2BooleanEntrySet()) { - UpsertPlayerInfo.Entry item = new UpsertPlayerInfo.Entry(entry.getKey()); + UpsertPlayerInfoPacket.Entry item = new UpsertPlayerInfoPacket.Entry(entry.getKey()); item.setListed(false); items.add(item); } - UpsertPlayerInfo packet = new UpsertPlayerInfo(); - packet.addAction(UpsertPlayerInfo.Action.UPDATE_LISTED); + UpsertPlayerInfoPacket packet = new UpsertPlayerInfoPacket(); + packet.addAction(UpsertPlayerInfoPacket.Action.UPDATE_LISTED); packet.addAllEntries(items); sendPacket(packet); } @@ -675,7 +678,7 @@ private void createTeamsIfNecessary() { hasCreatedCustomTeams = true; for (int i = 0; i < 80; i++) { - sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[i], EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, EMPTY_JSON_TEXT, "always", "always", 21, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[i], CUSTOM_SLOT_USERNAME_SMILEYS[i]})); + sendPacket(createPacketTeamCreate(CUSTOM_SLOT_TEAMNAME[i], EMPTY_COMPONENT, EMPTY_COMPONENT, EMPTY_COMPONENT, Team.NameTagVisibility.ALWAYS, Team.CollisionRule.ALWAYS, 21, (byte) 1, new String[]{CUSTOM_SLOT_USERNAME[i], CUSTOM_SLOT_USERNAME_SMILEYS[i]})); } } } @@ -698,7 +701,7 @@ void onDeactivated() { uuids[i++] = slotUuid[index]; } } - RemovePlayerInfo packet = new RemovePlayerInfo(); + RemovePlayerInfoPacket packet = new RemovePlayerInfoPacket(); packet.setProfilesToRemove(Arrays.asList(uuids)); sendPacket(packet); } @@ -734,10 +737,10 @@ void update() { tabOverlay.dirtyFlagsPing.clear(index); slotState[index] = SlotState.CUSTOM; slotUuid[index] = customSlotUuid; - UpsertPlayerInfo.Entry item = new UpsertPlayerInfo.Entry(customSlotUuid); + UpsertPlayerInfoPacket.Entry item = new UpsertPlayerInfoPacket.Entry(customSlotUuid); GameProfile profile = new GameProfile(customSlotUuid, slotUsername[index] = getCustomSlotUsername(index), toPropertiesList(icon.getTextureProperty())); item.setProfile(profile); - item.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); + item.setDisplayName(new ComponentHolder(player.getProtocolVersion(), tabOverlay.text[index])); item.setLatency(tabOverlay.ping[index]); item.setGameMode(0); item.setListed(true); @@ -749,8 +752,8 @@ void update() { dirtySlots.copyAndClear(tabOverlay.dirtyFlagsText); for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { if (slotState[index] != SlotState.UNUSED) { - UpsertPlayerInfo.Entry item = new UpsertPlayerInfo.Entry(slotUuid[index]); - item.setDisplayName(GsonComponentSerializer.gson().deserialize(tabOverlay.text[index])); + UpsertPlayerInfoPacket.Entry item = new UpsertPlayerInfoPacket.Entry(slotUuid[index]); + item.setDisplayName(new ComponentHolder(player.getProtocolVersion(), tabOverlay.text[index])); itemQueueUpdateDisplayName.add(item); } } @@ -759,7 +762,7 @@ void update() { dirtySlots.copyAndClear(tabOverlay.dirtyFlagsPing); for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { if (slotState[index] != SlotState.UNUSED) { - UpsertPlayerInfo.Entry item = new UpsertPlayerInfo.Entry(slotUuid[index]); + UpsertPlayerInfoPacket.Entry item = new UpsertPlayerInfoPacket.Entry(slotUuid[index]); item.setLatency(tabOverlay.ping[index]); itemQueueUpdatePing.add(item); } @@ -773,28 +776,28 @@ void update() { private void sendQueuedItems() { if (!itemQueueRemovePlayer.isEmpty()) { - RemovePlayerInfo packet = new RemovePlayerInfo(); + RemovePlayerInfoPacket packet = new RemovePlayerInfoPacket(); packet.setProfilesToRemove(itemQueueRemovePlayer); sendPacket(packet); itemQueueRemovePlayer.clear(); } if (!itemQueueAddPlayer.isEmpty()) { - UpsertPlayerInfo packet = new UpsertPlayerInfo(); - packet.addAllActions(EnumSet.of(UpsertPlayerInfo.Action.ADD_PLAYER, UpsertPlayerInfo.Action.UPDATE_DISPLAY_NAME, UpsertPlayerInfo.Action.UPDATE_LATENCY, UpsertPlayerInfo.Action.UPDATE_LISTED)); + UpsertPlayerInfoPacket packet = new UpsertPlayerInfoPacket(); + packet.addAllActions(EnumSet.of(UpsertPlayerInfoPacket.Action.ADD_PLAYER, UpsertPlayerInfoPacket.Action.UPDATE_DISPLAY_NAME, UpsertPlayerInfoPacket.Action.UPDATE_LATENCY, UpsertPlayerInfoPacket.Action.UPDATE_LISTED)); packet.addAllEntries(itemQueueAddPlayer); sendPacket(packet); itemQueueAddPlayer.clear(); } if (!itemQueueUpdateDisplayName.isEmpty()) { - UpsertPlayerInfo packet = new UpsertPlayerInfo(); - packet.addAction(UpsertPlayerInfo.Action.UPDATE_DISPLAY_NAME); + UpsertPlayerInfoPacket packet = new UpsertPlayerInfoPacket(); + packet.addAction(UpsertPlayerInfoPacket.Action.UPDATE_DISPLAY_NAME); packet.addAllEntries(itemQueueUpdateDisplayName); sendPacket(packet); itemQueueUpdateDisplayName.clear(); } if (!itemQueueUpdatePing.isEmpty()) { - UpsertPlayerInfo packet = new UpsertPlayerInfo(); - packet.addAction(UpsertPlayerInfo.Action.UPDATE_LATENCY); + UpsertPlayerInfoPacket packet = new UpsertPlayerInfoPacket(); + packet.addAction(UpsertPlayerInfoPacket.Action.UPDATE_LATENCY); packet.addAllEntries(itemQueueUpdatePing); sendPacket(packet); itemQueueUpdatePing.clear(); @@ -814,7 +817,7 @@ private boolean isExperimentalTabCompleteSmileys() { private abstract class CustomContentTabOverlay extends AbstractContentTabOverlay implements TabOverlayHandle.BatchModifiable { final Icon[] icon; - final String[] text; + final Component[] text; final int[] ping; final AtomicInteger batchUpdateRecursionLevel; @@ -826,8 +829,8 @@ private abstract class CustomContentTabOverlay extends AbstractContentTabOverlay private CustomContentTabOverlay() { this.icon = new Icon[80]; Arrays.fill(this.icon, Icon.DEFAULT_STEVE); - this.text = new String[80]; - Arrays.fill(this.text, EMPTY_JSON_TEXT); + this.text = new Component[80]; + Arrays.fill(this.text, Component.empty()); this.ping = new int[80]; this.batchUpdateRecursionLevel = new AtomicInteger(0); this.dirtyFlagSize = true; @@ -872,9 +875,9 @@ void setIconInternal(int index, @Nonnull @NonNull Icon icon) { } void setTextInternal(int index, @Nonnull @NonNull String text) { - String jsonText = ChatFormat.formattedTextToJson(text); - if (!jsonText.equals(this.text[index])) { - this.text[index] = jsonText; + Component component = GsonComponentSerializer.gson().deserialize(ChatFormat.formattedTextToJson(text)); + if (!component.equals(this.text[index])) { + this.text[index] = component; dirtyFlagsText.set(index); scheduleUpdateIfNotInBatch(); } @@ -941,7 +944,7 @@ public void setSize(@Nonnull Dimension size) { for (int index = newUsedSlots.nextSetBit(0); index >= 0; index = newUsedSlots.nextSetBit(index + 1)) { if (!oldUsedSlots.get(index)) { icon[index] = Icon.DEFAULT_STEVE; - text[index] = EMPTY_JSON_TEXT; + text[index] = Component.empty(); ping[index] = 0; } } @@ -951,7 +954,7 @@ public void setSize(@Nonnull Dimension size) { for (int index = oldUsedSlots.nextSetBit(0); index >= 0; index = oldUsedSlots.nextSetBit(index + 1)) { if (!newUsedSlots.get(index)) { icon[index] = Icon.DEFAULT_STEVE; - text[index] = EMPTY_JSON_TEXT; + text[index] = Component.empty(); ping[index] = 0; } } @@ -1126,7 +1129,7 @@ protected CustomHeaderAndFooterImpl createTabOverlay() { } @Override - PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { + PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooterPacket packet) { return PacketListenerResult.CANCEL; } @@ -1143,7 +1146,7 @@ void onDeactivated() { @Override void onActivated(AbstractHeaderFooterOperationModeHandler previous) { // remove header/ footer - sendPacket(new HeaderAndFooter(EMPTY_JSON_TEXT, EMPTY_JSON_TEXT)); + sendPacket(new HeaderAndFooterPacket(EMPTY_COMPONENT, EMPTY_COMPONENT)); } @Override @@ -1151,14 +1154,14 @@ void update() { CustomHeaderAndFooterImpl tabOverlay = getTabOverlay(); if (tabOverlay.headerOrFooterDirty) { tabOverlay.headerOrFooterDirty = false; - sendPacket(new HeaderAndFooter(tabOverlay.header, tabOverlay.footer)); + sendPacket(new HeaderAndFooterPacket(tabOverlay.header, tabOverlay.footer)); } } } private final class CustomHeaderAndFooterImpl extends AbstractHeaderFooterTabOverlay implements HeaderAndFooterHandle { - private String header = EMPTY_JSON_TEXT; - private String footer = EMPTY_JSON_TEXT; + private ComponentHolder header = EMPTY_COMPONENT; + private ComponentHolder footer = EMPTY_COMPONENT; private volatile boolean headerOrFooterDirty = false; @@ -1193,22 +1196,22 @@ void scheduleUpdateIfNotInBatch() { @Override public void setHeaderFooter(@Nullable String header, @Nullable String footer) { - this.header = ChatFormat.formattedTextToJson(header); - this.footer = ChatFormat.formattedTextToJson(footer); + this.header = new ComponentHolder(player.getProtocolVersion(), ChatFormat.formattedTextToJson(header)); + this.footer = new ComponentHolder(player.getProtocolVersion(), ChatFormat.formattedTextToJson(footer)); headerOrFooterDirty = true; scheduleUpdateIfNotInBatch(); } @Override public void setHeader(@Nullable String header) { - this.header = ChatFormat.formattedTextToJson(header); + this.header = new ComponentHolder(player.getProtocolVersion(), ChatFormat.formattedTextToJson(header)); headerOrFooterDirty = true; scheduleUpdateIfNotInBatch(); } @Override public void setFooter(@Nullable String footer) { - this.footer = ChatFormat.formattedTextToJson(footer); + this.footer = new ComponentHolder(player.getProtocolVersion(), ChatFormat.formattedTextToJson(footer)); headerOrFooterDirty = true; scheduleUpdateIfNotInBatch(); } @@ -1228,7 +1231,7 @@ private static List toPropertiesList(ProfileProperty textu } } - private static Team createPacketTeamCreate(String name, String displayName, String prefix, String suffix, String nameTagVisibility, String collisionRule, int color, byte friendlyFire, String[] players) { + private static Team createPacketTeamCreate(String name, ComponentHolder displayName, ComponentHolder prefix, ComponentHolder suffix, Team.NameTagVisibility nameTagVisibility, Team.CollisionRule collisionRule, int color, byte friendlyFire, String[] players) { Team team = new Team(); team.setName(name); team.setMode((byte) 0); diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/RewriteLogic.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/RewriteLogic.java index 7bdf99ba..576f0bcc 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/RewriteLogic.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/RewriteLogic.java @@ -24,9 +24,9 @@ import codecrafter47.bungeetablistplus.util.ProxyServer; import com.google.common.base.MoreObjects; import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; -import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; -import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket; +import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfoPacket; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket; import java.util.HashMap; import java.util.ListIterator; @@ -42,10 +42,10 @@ public RewriteLogic(PacketHandler parent) { } @Override - public PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { + public PacketListenerResult onPlayerListPacket(LegacyPlayerListItemPacket packet) { - if (packet.getAction() == LegacyPlayerListItem.ADD_PLAYER) { - for (LegacyPlayerListItem.Item item : packet.getItems()) { + if (packet.getAction() == LegacyPlayerListItemPacket.ADD_PLAYER) { + for (LegacyPlayerListItemPacket.Item item : packet.getItems()) { UUID uuid = item.getUuid(); Player player = ProxyServer.getInstance().getPlayer(uuid).orElse(null); if (player != null) { @@ -56,22 +56,22 @@ public PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { boolean modified = false; - if (packet.getAction() == LegacyPlayerListItem.REMOVE_PLAYER) { - ListIterator it = packet.getItems().listIterator(); + if (packet.getAction() == LegacyPlayerListItemPacket.REMOVE_PLAYER) { + ListIterator it = packet.getItems().listIterator(); while(it.hasNext()){ - LegacyPlayerListItem.Item item = it.next(); + LegacyPlayerListItemPacket.Item item = it.next(); UUID uuid = rewriteMap.remove(item.getUuid()); modified |= uuid != null; it.set(copyToNewItem(MoreObjects.firstNonNull(uuid, item.getUuid()), item)); } } else { - ListIterator it = packet.getItems().listIterator(); + ListIterator it = packet.getItems().listIterator(); while(it.hasNext()){ - LegacyPlayerListItem.Item item = it.next(); + LegacyPlayerListItemPacket.Item item = it.next(); UUID uuid = rewriteMap.get(item.getUuid()); if (uuid != null) { modified = true; - if (packet.getAction() == LegacyPlayerListItem.ADD_PLAYER) { + if (packet.getAction() == LegacyPlayerListItemPacket.ADD_PLAYER) { Player player = ProxyServer.getInstance().getPlayer(item.getUuid()).orElse(null); if (player != null) { String[][] properties = Property119Handler.getProperties(player.getGameProfile()); @@ -88,9 +88,9 @@ public PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { } @Override - public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { - if (packet.getActions().contains(UpsertPlayerInfo.Action.ADD_PLAYER)) { - for (UpsertPlayerInfo.Entry item : packet.getEntries()) { + public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfoPacket packet) { + if (packet.getActions().contains(UpsertPlayerInfoPacket.Action.ADD_PLAYER)) { + for (UpsertPlayerInfoPacket.Entry item : packet.getEntries()) { UUID uuid = item.getProfileId(); Player player = ProxyServer.getInstance().getPlayer(uuid).orElse(null); if (player != null) { @@ -101,9 +101,9 @@ public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { } } boolean modified = false; - ListIterator it = packet.getEntries().listIterator(); + ListIterator it = packet.getEntries().listIterator(); while(it.hasNext()){ - UpsertPlayerInfo.Entry item = it.next(); + UpsertPlayerInfoPacket.Entry item = it.next(); UUID uuid = rewriteMap.get(item.getProfileId()); if (uuid != null) { modified = true; @@ -115,7 +115,7 @@ public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { } @Override - public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet) { + public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfoPacket packet) { boolean modified = false; UUID[] uuids = packet.getProfilesToRemove().toArray(new UUID[0]); @@ -138,8 +138,8 @@ public void onServerSwitch(boolean is13OrLater) { super.onServerSwitch(is13OrLater); } - private LegacyPlayerListItem.Item copyToNewItem(UUID uuid, LegacyPlayerListItem.Item item){ - LegacyPlayerListItem.Item newItem = new LegacyPlayerListItem.Item(uuid); + private LegacyPlayerListItemPacket.Item copyToNewItem(UUID uuid, LegacyPlayerListItemPacket.Item item){ + LegacyPlayerListItemPacket.Item newItem = new LegacyPlayerListItemPacket.Item(uuid); newItem.setName(item.getName()); newItem.setLatency(item.getLatency()); newItem.setGameMode(item.getGameMode()); @@ -149,8 +149,8 @@ private LegacyPlayerListItem.Item copyToNewItem(UUID uuid, LegacyPlayerListItem. return newItem; } - private UpsertPlayerInfo.Entry copyToNewEntry(UUID uuid, UpsertPlayerInfo.Entry item){ - UpsertPlayerInfo.Entry newItem = new UpsertPlayerInfo.Entry(uuid); + private UpsertPlayerInfoPacket.Entry copyToNewEntry(UUID uuid, UpsertPlayerInfoPacket.Entry item){ + UpsertPlayerInfoPacket.Entry newItem = new UpsertPlayerInfoPacket.Entry(uuid); newItem.setProfile(item.getProfile()); newItem.setLatency(item.getLatency()); newItem.setGameMode(item.getGameMode()); diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/TabOverlayHandlerImpl.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/TabOverlayHandlerImpl.java index 1459af43..2a96269e 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/TabOverlayHandlerImpl.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/TabOverlayHandlerImpl.java @@ -23,8 +23,8 @@ import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.proxy.protocol.MinecraftPacket; -import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; -import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; +import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfoPacket; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket; import lombok.SneakyThrows; import java.util.UUID; @@ -37,15 +37,15 @@ public class TabOverlayHandlerImpl extends AbstractTabOverlayHandler { private boolean logVersionMismatch = false; - public TabOverlayHandlerImpl(Logger logger, Executor eventLoopExecutor, UUID viewerUuid, Player player, boolean is18, boolean is13OrLater, boolean is119OrLater) { - super(logger, eventLoopExecutor, viewerUuid, is18, is13OrLater, is119OrLater); + public TabOverlayHandlerImpl(Logger logger, Executor eventLoopExecutor, UUID viewerUuid, Player player, boolean is18, boolean is13OrLater, boolean is119OrLater, boolean is1203OrLater) { + super(logger, eventLoopExecutor, viewerUuid, is18, is13OrLater, is119OrLater, is1203OrLater); this.player = player; } @SneakyThrows @Override protected void sendPacket(MinecraftPacket packet) { - if ((packet instanceof UpsertPlayerInfo) && (player.getProtocolVersion().getProtocol() >= 761)) { + if ((packet instanceof UpsertPlayerInfoPacket) && (player.getProtocolVersion().getProtocol() >= 761)) { // error if (!logVersionMismatch) { logVersionMismatch = true; @@ -56,6 +56,11 @@ protected void sendPacket(MinecraftPacket packet) { } } + @Override + protected ProtocolVersion getProtocol(){ + return player.getProtocolVersion(); + } + @Override protected boolean isExperimentalTabCompleteSmileys() { return BungeeTabListPlus.getInstance().getConfig().experimentalTabCompleteSmileys; @@ -74,12 +79,12 @@ protected boolean isUsingAltRespawn() { } @Override - public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { + public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfoPacket packet) { return PacketListenerResult.PASS; } @Override - public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet) { + public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfoPacket packet) { return PacketListenerResult.PASS; } } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/TabViewManager.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/TabViewManager.java index 97279323..85fac3e6 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/TabViewManager.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/TabViewManager.java @@ -120,7 +120,7 @@ private PlayerTabView createTabView(Player player) { tabOverlayHandler = handler; packetHandler = new RewriteLogic(new GetGamemodeLogic(handler, player.getUniqueId())); } else if (protocolVersionProvider.has18OrLater(player)) { - LowMemoryTabOverlayHandlerImpl tabOverlayHandlerImpl = new LowMemoryTabOverlayHandlerImpl(logger, eventLoop, player.getUniqueId(), player, protocolVersionProvider.is18(player), protocolVersionProvider.has113OrLater(player), protocolVersionProvider.has119OrLater(player)); + LowMemoryTabOverlayHandlerImpl tabOverlayHandlerImpl = new LowMemoryTabOverlayHandlerImpl(logger, eventLoop, player.getUniqueId(), player, protocolVersionProvider.is18(player), protocolVersionProvider.has113OrLater(player), protocolVersionProvider.has119OrLater(player), protocolVersionProvider.has1203OrLater(player)); tabOverlayHandler = tabOverlayHandlerImpl; packetHandler = new RewriteLogic(new GetGamemodeLogic(tabOverlayHandlerImpl, player.getUniqueId())); } else { diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/AbstractPacketHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/AbstractPacketHandler.java index 50b6812a..fe0b9e88 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/AbstractPacketHandler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/AbstractPacketHandler.java @@ -17,10 +17,10 @@ package codecrafter47.bungeetablistplus.protocol; -import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; -import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; -import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; -import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; +import com.velocitypowered.proxy.protocol.packet.HeaderAndFooterPacket; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket; +import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfoPacket; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket; import lombok.NonNull; import javax.annotation.Nonnull; @@ -37,7 +37,7 @@ public AbstractPacketHandler(@Nonnull @NonNull PacketHandler parent) { } @Override - public PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet) { + public PacketListenerResult onPlayerListPacket(LegacyPlayerListItemPacket packet) { return parent.onPlayerListPacket(packet); } @@ -47,17 +47,17 @@ public PacketListenerResult onTeamPacket(Team packet) { } @Override - public PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet) { + public PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooterPacket packet) { return parent.onPlayerListHeaderFooterPacket(packet); } @Override - public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet) { + public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfoPacket packet) { return parent.onPlayerListUpdatePacket(packet); } @Override - public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet) { + public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfoPacket packet) { return parent.onPlayerListRemovePacket(packet); } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketHandler.java index 2488d985..ec0c9ff6 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketHandler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketHandler.java @@ -17,22 +17,22 @@ package codecrafter47.bungeetablistplus.protocol; -import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; -import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; -import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; -import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; +import com.velocitypowered.proxy.protocol.packet.HeaderAndFooterPacket; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket; +import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfoPacket; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket; public interface PacketHandler { - PacketListenerResult onPlayerListPacket(LegacyPlayerListItem packet); + PacketListenerResult onPlayerListPacket(LegacyPlayerListItemPacket packet); - PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfo packet); + PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfoPacket packet); - PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfo packet); + PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfoPacket packet); PacketListenerResult onTeamPacket(Team packet); - PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooter packet); + PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooterPacket packet); void onServerSwitch(boolean is13OrLater); } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListener.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListener.java index d60485c4..14c8cb77 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListener.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListener.java @@ -22,10 +22,10 @@ import com.velocitypowered.api.proxy.Player; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; import com.velocitypowered.proxy.protocol.MinecraftPacket; -import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; -import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; -import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; -import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; +import com.velocitypowered.proxy.protocol.packet.HeaderAndFooterPacket; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket; +import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfoPacket; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToMessageDecoder; @@ -59,17 +59,17 @@ protected void decode(ChannelHandlerContext ctx, MinecraftPacket packet, List= 0) { - collisionRule = ProtocolUtils.readString(buf); + // TODO: Replace this when released + if (version.getProtocol() >= MINECRAFT_1_21_5) { + nameTagVisibility = NameTagVisibility.values()[ProtocolUtils.readVarInt(buf)]; + collisionRule = CollisionRule.values()[ProtocolUtils.readVarInt(buf)]; + } else { + String nameTagVisibilityStr = ProtocolUtils.readString(buf); + NameTagVisibility nameTagVisibility = NameTagVisibility.BY_NAME.get(nameTagVisibilityStr); + + + if (version.compareTo(ProtocolVersion.MINECRAFT_1_9) >= 0) { + String collisionRuleStr = ProtocolUtils.readString(buf); + collisionRule = CollisionRule.BY_NAME.get(collisionRuleStr); + } } - color = ( version.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0 ) ? ProtocolUtils.readVarInt(buf) : buf.readByte(); - if ( version.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0 ) { - prefix = ProtocolUtils.readString(buf); - suffix = ProtocolUtils.readString(buf); + color = (version.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0) ? ProtocolUtils.readVarInt(buf) : buf.readByte(); + if (version.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0) { + prefix = ComponentHolder.read(buf, version); + suffix = ComponentHolder.read(buf, version); } } if (mode == CREATE || mode == ADD_PLAYER || mode == REMOVE_PLAYER) { @@ -90,23 +118,29 @@ public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersi ProtocolUtils.writeString(buf, name); buf.writeByte(mode); if (mode == CREATE || mode == UPDATE_INFO) { - ProtocolUtils.writeString(buf, displayName); + displayName.write(buf); if (version.compareTo(ProtocolVersion.MINECRAFT_1_13) < 0) { - ProtocolUtils.writeString(buf, prefix); - ProtocolUtils.writeString(buf, suffix); + prefix.write(buf); + suffix.write(buf); } buf.writeByte(friendlyFire); - ProtocolUtils.writeString(buf, nameTagVisibility); - if (version.compareTo(ProtocolVersion.MINECRAFT_1_9) >= 0) { - ProtocolUtils.writeString(buf, collisionRule); + // TODO: Replace this when released + if (version.getProtocol() >= MINECRAFT_1_21_5) { + ProtocolUtils.writeVarInt(buf, nameTagVisibility.ordinal()); + ProtocolUtils.writeVarInt(buf, collisionRule.ordinal()); + } else { + ProtocolUtils.writeString(buf, nameTagVisibility.getKey()); + if (version.compareTo(ProtocolVersion.MINECRAFT_1_9) >= 0) { + ProtocolUtils.writeString(buf, collisionRule.getKey()); + } } if (version.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0) { ProtocolUtils.writeVarInt(buf, color); - ProtocolUtils.writeString(buf, prefix); - ProtocolUtils.writeString(buf, suffix); + prefix.write(buf); + suffix.write(buf); } else { - buf.writeByte( color ); + buf.writeByte(color); } } if (mode == CREATE || mode == ADD_PLAYER || mode == REMOVE_PLAYER) { @@ -122,99 +156,53 @@ public boolean handle(MinecraftSessionHandler minecraftSessionHandler) { return false; } - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public byte getMode() { - return mode; - } - - public void setMode(byte mode) { - this.mode = mode; - } - - public String getDisplayName() { - return displayName; - } - - public void setDisplayName(String displayName) { - this.displayName = displayName; - } - - public String getPrefix() { - return prefix; - } - - public void setPrefix(String prefix) { - this.prefix = prefix; - } - - public String getSuffix() { - return suffix; - } - - public void setSuffix(String suffix) { - this.suffix = suffix; - } - - public String getNameTagVisibility() { - return nameTagVisibility; - } + @Getter + @RequiredArgsConstructor + public enum NameTagVisibility { - public void setNameTagVisibility(String nameTagVisibility) { - this.nameTagVisibility = nameTagVisibility; - } + ALWAYS("always"), + NEVER("never"), + HIDE_FOR_OTHER_TEAMS("hideForOtherTeams"), + HIDE_FOR_OWN_TEAM("hideForOwnTeam"); + // + private final String key; + // + private static final Map BY_NAME; - public String getCollisionRule() { - return collisionRule; - } + static { + NameTagVisibility[] values = NameTagVisibility.values(); + ImmutableMap.Builder builder = ImmutableMap.builderWithExpectedSize(values.length); - public void setCollisionRule(String collisionRule) { - this.collisionRule = collisionRule; - } + for (NameTagVisibility e : values) { + builder.put(e.key, e); + } - public int getColor() { - return color; + BY_NAME = builder.build(); + } } - public void setColor(int color) { - this.color = color; - } + @Getter + @RequiredArgsConstructor + public enum CollisionRule { - public byte getFriendlyFire() { - return friendlyFire; - } + ALWAYS("always"), + NEVER("never"), + PUSH_OTHER_TEAMS("pushOtherTeams"), + PUSH_OWN_TEAM("pushOwnTeam"); + // + private final String key; + // + private static final Map BY_NAME; - public void setFriendlyFire(byte friendlyFire) { - this.friendlyFire = friendlyFire; - } + static { + CollisionRule[] values = CollisionRule.values(); + ImmutableMap.Builder builder = ImmutableMap.builderWithExpectedSize(values.length); - public String[] getPlayers() { - return players; - } - - public void setPlayers(String[] players) { - this.players = players; - } + for (CollisionRule e : values) { + builder.put(e.key, e); + } - @Override - public String toString() { - return "Team{" + - "name=" + name + - ", mode=" + mode + - ", displayName=" + displayName + - ", prefix=" + prefix + - ", suffix=" + suffix + - ", friendlyFire=" + friendlyFire + - ", nameTagVisibility=" + nameTagVisibility + - ", collisionRule=" + collisionRule + - ", color=" + color + - ", players=[" + String.join(",", players) + "]" + - '}'; + BY_NAME = builder.build(); + } } } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/Property119Handler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/Property119Handler.java index 68940c4f..41f79b73 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/Property119Handler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/Property119Handler.java @@ -1,13 +1,13 @@ package codecrafter47.bungeetablistplus.util; import com.velocitypowered.api.util.GameProfile; -import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; -import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket; import java.util.Arrays; public class Property119Handler { - public static String[][] getProperties(LegacyPlayerListItem.Item item) { + public static String[][] getProperties(LegacyPlayerListItemPacket.Item item) { return Arrays.stream(item.getProperties().toArray(new GameProfile.Property[0])).map(prop -> new String[]{prop.getName(), prop.getValue(), prop.getSignature()}).toArray(String[][]::new); } @@ -15,11 +15,11 @@ public static String[][] getProperties(GameProfile profile) { return profile.getProperties().stream().map(prop -> new String[]{prop.getName(), prop.getValue(), prop.getSignature()}).toArray(String[][]::new); } - public static void setProperties(LegacyPlayerListItem.Item item, String[][] properties) { + public static void setProperties(LegacyPlayerListItemPacket.Item item, String[][] properties) { item.setProperties(Arrays.asList(Arrays.stream(properties).map(array -> new GameProfile.Property(array[0], array[1], array.length >= 3 ? array[2] : null)).toArray(GameProfile.Property[]::new))); } - public static void setProperties(UpsertPlayerInfo.Entry item, String[][] properties) { + public static void setProperties(UpsertPlayerInfoPacket.Entry item, String[][] properties) { GameProfile profile = item.getProfile(); profile.addProperties(Arrays.asList(Arrays.stream(properties).map(array -> new GameProfile.Property(array[0], array[1], array.length >= 3 ? array[2] : null)).toArray(GameProfile.Property[]::new))); item.setProfile(profile); diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ReflectionUtil.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ReflectionUtil.java index a0698c7a..9a40b6d2 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ReflectionUtil.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ReflectionUtil.java @@ -39,6 +39,10 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_1; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_3; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_4; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_2; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_3; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_5; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_2; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9; @@ -114,7 +118,11 @@ public static void injectTeamPacketRegistry() { (StateRegistry.PacketMapping) packetMapping.newInstance(0x55, MINECRAFT_1_17, null, false), (StateRegistry.PacketMapping) packetMapping.newInstance(0x58, MINECRAFT_1_19_1, null, false), (StateRegistry.PacketMapping) packetMapping.newInstance(0x56, MINECRAFT_1_19_3, null, false), - (StateRegistry.PacketMapping) packetMapping.newInstance(0x5A, MINECRAFT_1_19_4, null, false) + (StateRegistry.PacketMapping) packetMapping.newInstance(0x5A, MINECRAFT_1_19_4, null, false), + (StateRegistry.PacketMapping) packetMapping.newInstance(0x5C, MINECRAFT_1_20_2, null, false), + (StateRegistry.PacketMapping) packetMapping.newInstance(0x5E, MINECRAFT_1_20_3, null, false), + (StateRegistry.PacketMapping) packetMapping.newInstance(0x60, MINECRAFT_1_20_5, null, false), + (StateRegistry.PacketMapping) packetMapping.newInstance(0x67, MINECRAFT_1_21_2, null, false) }); return; } catch (Exception e) { diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ProtocolVersionProvider.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ProtocolVersionProvider.java index 7ba26a76..9a793da3 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ProtocolVersionProvider.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ProtocolVersionProvider.java @@ -33,4 +33,6 @@ public interface ProtocolVersionProvider { String getVersion(Player player); boolean has1193OrLater(Player player); + + boolean has1203OrLater(Player player); } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/version/VelocityProtocolVersionProvider.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/version/VelocityProtocolVersionProvider.java index 9465ebee..039f9901 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/version/VelocityProtocolVersionProvider.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/version/VelocityProtocolVersionProvider.java @@ -54,5 +54,9 @@ public String getVersion(Player player) { public boolean has1193OrLater(Player player) { return player.getProtocolVersion().getProtocol() >= 761; } + @Override + public boolean has1203OrLater(Player player) { + return player.getProtocolVersion().getProtocol() >= 765; + } } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ViaVersionProtocolVersionProvider.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ViaVersionProtocolVersionProvider.java index 786b1247..1c7b58ee 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ViaVersionProtocolVersionProvider.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ViaVersionProtocolVersionProvider.java @@ -51,6 +51,11 @@ public boolean is18(Player player) { return Via.getAPI().getPlayerVersion(player) == 47; } + @Override + public boolean has1203OrLater(Player player) { + return Via.getAPI().getPlayerVersion(player) >= 765; + } + @Override public String getVersion(Player player) { return ProtocolVersion.getProtocol(Via.getAPI().getPlayerVersion(player)).getName(); From d1b686deca6e5942dd39e3167cfdc0a104f3ee68 Mon Sep 17 00:00:00 2001 From: proferabg Date: Sun, 9 Feb 2025 00:31:57 -0500 Subject: [PATCH 15/22] Working? Seriously? --- .../bungeetablistplus/BootstrapPlugin.java | 30 +- .../handler/LegacyTabOverlayHandlerImpl.java | 3 - minecraft-data-api | 2 +- .../bungeetablistplus/BungeeTabListPlus.java | 16 +- .../command/CommandDebug.java | 5 +- .../handler/AbstractTabOverlayHandler.java | 19 +- .../handler/LegacyTabOverlayHandlerImpl.java | 4 +- .../handler/NewTabOverlayHandler.java | 93 +- .../handler/OrderedTabOverlayHandler.java | 1202 +++++++++++++++++ .../handler/TabOverlayHandlerImpl.java | 4 +- .../managers/RedisPlayerManager.java | 4 +- .../managers/ServerStateManager.java | 2 +- .../managers/TabViewManager.java | 76 +- .../PlayerPlaceholderResolver.java | 1 + .../protocol/PacketListener.java | 25 +- .../bungeetablistplus/protocol/Team.java | 35 +- .../util/ReflectionUtil.java | 32 - .../util/VelocityPlugin.java | 39 +- .../version/ProtocolVersionProvider.java | 2 + .../VelocityProtocolVersionProvider.java | 6 + .../ViaVersionProtocolVersionProvider.java | 5 + 21 files changed, 1413 insertions(+), 192 deletions(-) create mode 100644 velocity/src/main/java/codecrafter47/bungeetablistplus/handler/OrderedTabOverlayHandler.java diff --git a/bootstrap-velocity/src/main/java/codecrafter47/bungeetablistplus/BootstrapPlugin.java b/bootstrap-velocity/src/main/java/codecrafter47/bungeetablistplus/BootstrapPlugin.java index 0928c09f..7aea52c2 100644 --- a/bootstrap-velocity/src/main/java/codecrafter47/bungeetablistplus/BootstrapPlugin.java +++ b/bootstrap-velocity/src/main/java/codecrafter47/bungeetablistplus/BootstrapPlugin.java @@ -24,9 +24,11 @@ import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; import com.velocitypowered.api.plugin.Dependency; import com.velocitypowered.api.plugin.Plugin; +import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.plugin.annotation.DataDirectory; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; +import lombok.Getter; import net.kyori.adventure.text.Component; import org.bstats.velocity.Metrics; import org.slf4j.Logger; @@ -46,22 +48,38 @@ @Dependency(id = "viaversion", optional = true) } ) -public class BootstrapPlugin extends VelocityPlugin { +public class BootstrapPlugin implements VelocityPlugin { private final Metrics.Factory metricsFactory; private static final String NO_RELOAD_PLAYERS = "Cannot reload BungeeTabListPlus while players are online."; + @Getter + private final Logger logger; + + @Getter + private final ProxyServer proxy; + + @Getter + private final Path dataDirectory; + + @Getter + private final String version; + + @Inject - public BootstrapPlugin(final ProxyServer server, final Logger logger, final @DataDirectory Path dataDirectory, final Metrics.Factory metricsFactory) { - super(server, logger, dataDirectory, BootstrapPlugin.class.getAnnotation(Plugin.class).version()); + public BootstrapPlugin(final ProxyServer proxy, final Logger logger, final @DataDirectory Path dataDirectory, final Metrics.Factory metricsFactory) { + this.proxy = proxy; + this.logger = logger; + this.dataDirectory = dataDirectory; + this.version = BootstrapPlugin.class.getAnnotation(Plugin.class).version(); this.metricsFactory = metricsFactory; } @Subscribe public void onProxyInitialization(final ProxyInitializeEvent event) { - if (Float.parseFloat(System.getProperty("java.class.version")) < 55.0) { - getLogger().error("§cBungeeTabListPlus requires Java 11 or above. Please download and install it!"); + if (Float.parseFloat(System.getProperty("java.class.version")) < 61.0) { + getLogger().error("§cBungeeTabListPlus requires Java 17 or above. Please download and install it!"); getLogger().error("Disabling plugin!"); return; } @@ -80,7 +98,7 @@ public void onProxyInitialization(final ProxyInitializeEvent event) { @Subscribe public void onProxyShutdown(final ProxyShutdownEvent event) { BungeeTabListPlus.getInstance().onDisable(); - if (isProxyRunning()) { + if (!getProxy().isShuttingDown()) { getLogger().error("You cannot use ServerUtils to reload BungeeTabListPlus. Use /btlp reload instead."); if (!getProxy().getAllPlayers().isEmpty()) { for (Player proxiedPlayer : getProxy().getAllPlayers()) { diff --git a/bungee/src/main/java/codecrafter47/bungeetablistplus/handler/LegacyTabOverlayHandlerImpl.java b/bungee/src/main/java/codecrafter47/bungeetablistplus/handler/LegacyTabOverlayHandlerImpl.java index 3b226092..061de31b 100644 --- a/bungee/src/main/java/codecrafter47/bungeetablistplus/handler/LegacyTabOverlayHandlerImpl.java +++ b/bungee/src/main/java/codecrafter47/bungeetablistplus/handler/LegacyTabOverlayHandlerImpl.java @@ -17,12 +17,9 @@ package codecrafter47.bungeetablistplus.handler; -import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.protocol.DefinedPacket; import net.md_5.bungee.protocol.packet.PlayerListItem; -import net.md_5.bungee.protocol.packet.PlayerListItemRemove; -import net.md_5.bungee.protocol.packet.PlayerListItemUpdate; import java.util.concurrent.Executor; import java.util.logging.Logger; diff --git a/minecraft-data-api b/minecraft-data-api index b0e4d1f5..e2fec2f0 160000 --- a/minecraft-data-api +++ b/minecraft-data-api @@ -1 +1 @@ -Subproject commit b0e4d1f55dcc298fa13254bf8dde509248a9a4bd +Subproject commit e2fec2f0030cf192ae5af49830bbe83ab6dfe359 diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java index 8d58d0c9..e2189294 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java @@ -46,8 +46,10 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.velocitypowered.api.command.CommandManager; +import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; +import com.velocitypowered.api.scheduler.ScheduledTask; import de.codecrafter47.data.bukkit.api.BukkitData; import de.codecrafter47.data.velocity.api.VelocityData; import de.codecrafter47.data.sponge.api.SpongeData; @@ -120,6 +122,7 @@ public class BungeeTabListPlus { private HiddenPlayersManager hiddenPlayersManager; private PlayerPlaceholderResolver playerPlaceholderResolver; private API api; + @Getter private Logger logger = Logger.getLogger(BungeeTabListPlus.class.getSimpleName()); public BungeeTabListPlus(VelocityPlugin plugin) { @@ -186,9 +189,9 @@ public void onLoad() { } try { - Class.forName("com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket"); - } catch (ClassNotFoundException ex) { - throw new RuntimeException("You need to run at least Velocity version #329"); + plugin.getProxy().isShuttingDown(); + } catch (NoSuchMethodError ex) { + throw new RuntimeException("You need to run at least Velocity version #464"); } INSTANCE = this; @@ -458,6 +461,9 @@ private boolean readMainConfig() { public void onDisable() { // save cache cache.save(); + plugin.getProxy().getScheduler().tasksByPlugin(plugin).forEach(ScheduledTask::cancel); + mainThreadExecutor.shutdownGracefully(); + asyncExecutor.shutdownGracefully(); } /** @@ -548,10 +554,6 @@ public void reportError(Throwable th) { th); } - public Logger getLogger() { - return logger; - } - public com.velocitypowered.api.proxy.ProxyServer getProxy() { return plugin.getProxy(); } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandDebug.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandDebug.java index 6eec82f9..05fc6a20 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandDebug.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandDebug.java @@ -21,13 +21,13 @@ import codecrafter47.bungeetablistplus.data.BTLPVelocityDataKeys; import codecrafter47.bungeetablistplus.player.VelocityPlayer; import codecrafter47.bungeetablistplus.util.ProxyServer; -import codecrafter47.bungeetablistplus.util.ReflectionUtil; import codecrafter47.bungeetablistplus.util.chat.ChatUtil; import com.google.common.base.Joiner; import com.mojang.brigadier.Command; import com.mojang.brigadier.context.CommandContext; import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import io.netty.channel.ChannelHandler; @@ -105,7 +105,8 @@ public static int commandPipeline(CommandContext context) { Player player = (Player) context.getSource(); ConnectedPlayer userConnection = (ConnectedPlayer) player; List userPipeline = new ArrayList<>(); - for (Map.Entry entry : ReflectionUtil.getChannelWrapper(userConnection).getChannel().pipeline()) { + MinecraftConnection connection = ((ConnectedPlayer) player).getConnection(); + for (Map.Entry entry : connection.getChannel().pipeline()) { userPipeline.add(entry.getKey()); } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java index 343a9556..ac351713 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java @@ -69,9 +69,6 @@ public abstract class AbstractTabOverlayHandler implements PacketHandler, TabOve private static ComponentHolder EMPTY_COMPONENT; protected static final String[][] EMPTY_PROPERTIES_ARRAY = new String[0][]; - private static final boolean TEAM_COLLISION_RULE_SUPPORTED; - private static final boolean USE_PROTOCOL_PROPERTY_TYPE; - private static final ImmutableMap DIMENSION_TO_USED_SLOTS; private static final BitSet[] SIZE_TO_USED_SLOTS; @@ -88,9 +85,6 @@ public abstract class AbstractTabOverlayHandler implements PacketHandler, TabOve private static final Set blockedTeams = new HashSet<>(); static { - TEAM_COLLISION_RULE_SUPPORTED = true; - USE_PROTOCOL_PROPERTY_TYPE = true; - // build the dimension to used slots map (for the rectangular tab overlay) val builder = ImmutableMap.builder(); for (int columns = 1; columns <= 4; columns++) { @@ -329,9 +323,7 @@ public PacketListenerResult onTeamPacket(Team packet) { teamEntry.setSuffix(packet.getSuffix()); teamEntry.setFriendlyFire(packet.getFriendlyFire()); teamEntry.setNameTagVisibility(packet.getNameTagVisibility()); - if (TEAM_COLLISION_RULE_SUPPORTED) { - teamEntry.setCollisionRule(packet.getCollisionRule()); - } + teamEntry.setCollisionRule(packet.getCollisionRule()); teamEntry.setColor(packet.getColor()); } if (packet.getPlayers() != null) { @@ -1500,7 +1492,6 @@ void update() { // restore player gamemode LegacyPlayerListItemPacket packet; List items = new ArrayList<>(serverPlayerList.size()); - items.clear(); for (PlayerListEntry entry : serverPlayerList.values()) { LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(entry.getUuid()); item.setGameMode(entry.getGamemode()); @@ -2423,9 +2414,7 @@ private static Team createPacketTeamCreate(String name, ComponentHolder displayN team.setPrefix(prefix); team.setSuffix(suffix); team.setNameTagVisibility(nameTagVisibility); - if (TEAM_COLLISION_RULE_SUPPORTED) { - team.setCollisionRule(collisionRule); - } + team.setCollisionRule(collisionRule); team.setColor(color); team.setFriendlyFire(friendlyFire); team.setPlayers(players); @@ -2447,9 +2436,7 @@ private static Team createPacketTeamUpdate(String name, ComponentHolder displayN team.setPrefix(prefix); team.setSuffix(suffix); team.setNameTagVisibility(nameTagVisibility); - if (TEAM_COLLISION_RULE_SUPPORTED) { - team.setCollisionRule(collisionRule); - } + team.setCollisionRule(collisionRule); team.setColor(color); team.setFriendlyFire(friendlyFire); return team; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LegacyTabOverlayHandlerImpl.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LegacyTabOverlayHandlerImpl.java index 89ee6337..8ab1f7fc 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LegacyTabOverlayHandlerImpl.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LegacyTabOverlayHandlerImpl.java @@ -17,8 +17,10 @@ package codecrafter47.bungeetablistplus.handler; +import codecrafter47.bungeetablistplus.protocol.PacketListener; import codecrafter47.bungeetablistplus.util.ReflectionUtil; import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket; import lombok.SneakyThrows; @@ -47,7 +49,7 @@ protected void sendPacket(MinecraftPacket packet) { this.logger.warning("Cannot correctly update tablist for player " + player.getUsername() + "\nThe client and server versions do not match. Client <= 1.7.10, server >= 1.19.3.\nUse ViaVersion on the spigot server for the best experience."); } } else { - ReflectionUtil.getChannelWrapper(player).write(packet); + PacketListener.sendPacket(player, packet); } } } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java index fe2b749b..5b93198b 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java @@ -19,17 +19,21 @@ import codecrafter47.bungeetablistplus.BungeeTabListPlus; import codecrafter47.bungeetablistplus.protocol.PacketHandler; +import codecrafter47.bungeetablistplus.protocol.PacketListener; import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; import codecrafter47.bungeetablistplus.protocol.Team; import codecrafter47.bungeetablistplus.util.BitSet; import codecrafter47.bungeetablistplus.util.ConcurrentBitSet; -import codecrafter47.bungeetablistplus.util.ReflectionUtil; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.util.GameProfile; +import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.HeaderAndFooterPacket; import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket; import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfoPacket; @@ -191,8 +195,11 @@ private void sendPacket(MinecraftPacket packet) { logVersionMismatch = true; logger.warning("Cannot correctly update tablist for player " + player.getUsername() + "\nThe client and server versions do not match. Client >= 1.19.3, server < 1.19.3.\nUse ViaVersion on the spigot server for the best experience."); } +// } else if (player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { +// // queue packet? +// ReflectionUtil.getChannelWrapper(player).write(packet); } else { - ReflectionUtil.getChannelWrapper(player).write(packet); + PacketListener.sendPacket(player, packet); } } @@ -203,6 +210,12 @@ public PacketListenerResult onPlayerListPacket(LegacyPlayerListItemPacket packet @Override public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfoPacket packet) { + + if (!active) { + active = true; + scheduleUpdate(); + } + if (packet.getActions().contains(UpsertPlayerInfoPacket.Action.ADD_PLAYER)) { for (UpsertPlayerInfoPacket.Entry entry : packet.getEntries()) { if (OPTION_ENABLE_CUSTOM_SLOT_UUID_COLLISION_CHECK) { @@ -269,42 +282,35 @@ public PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooterPacket @Override public void onServerSwitch(boolean is13OrLater) { - if (!active) { - active = true; - update(); - } else { - hasCreatedCustomTeams = false; + hasCreatedCustomTeams = false; - try { - this.activeContentHandler.onServerSwitch(); - } catch (Throwable th) { - logger.log(Level.SEVERE, "Unexpected error", th); - // try recover - enterContentOperationMode(ContentOperationMode.PASS_TROUGH); - } - try { - this.activeHeaderFooterHandler.onServerSwitch(); - } catch (Throwable th) { - logger.log(Level.SEVERE, "Unexpected error", th); - // try recover - enterContentOperationMode(ContentOperationMode.PASS_TROUGH); - } + try { + this.activeContentHandler.onServerSwitch(); + } catch (Throwable th) { + logger.log(Level.SEVERE, "Unexpected error", th); + } + try { + this.activeHeaderFooterHandler.onServerSwitch(); + } catch (Throwable th) { + logger.log(Level.SEVERE, "Unexpected error", th); + } - if (!serverPlayerListListed.isEmpty()) { - RemovePlayerInfoPacket packet = new RemovePlayerInfoPacket(); - packet.setProfilesToRemove(serverPlayerListListed.keySet()); - sendPacket(packet); - } + if (!serverPlayerListListed.isEmpty()) { + RemovePlayerInfoPacket packet = new RemovePlayerInfoPacket(); + packet.setProfilesToRemove(serverPlayerListListed.keySet()); + sendPacket(packet); + } - serverPlayerListListed.clear(); - if (serverHeader != null) { - serverHeader = EMPTY_COMPONENT; - } - if (serverFooter != null) { - serverFooter = EMPTY_COMPONENT; - } + serverPlayerListListed.clear(); + if (serverHeader != null) { + serverHeader = EMPTY_COMPONENT; + } + if (serverFooter != null) { + serverFooter = EMPTY_COMPONENT; } + + active = false; } @Override @@ -349,10 +355,12 @@ private void scheduleUpdate() { } private void update() { - if (!active) { + updateScheduledFlag.set(false); + + MinecraftConnection connection = ((ConnectedPlayer) player).getConnection(); + if(!active || connection.isClosed() || connection.getState() != StateRegistry.PLAY){ return; } - updateScheduledFlag.set(false); // update content handler AbstractContentOperationModeHandler contentHandler; @@ -376,7 +384,7 @@ private void update() { private abstract static class AbstractContentOperationModeHandler extends OperationModeHandler { /** - * Called when the player receives a {@link LegacyPlayerListItemPacket} packet. + * Called when the player receives a {@link UpsertPlayerInfoPacket} packet. *

* This method is called after this {@link NewTabOverlayHandler} has updated the {@code serverPlayerList}. */ @@ -648,8 +656,9 @@ private String getCustomSlotUsername(int index) { @Override void onServerSwitch() { - - createTeamsIfNecessary(); + if(player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0){ + clearCustomSlots(); + } } @Override @@ -685,10 +694,15 @@ private void createTeamsIfNecessary() { @Override void onDeactivated() { + clearCustomSlots(); + } + + private void clearCustomSlots() { int customSlots = 0; for (int index = 0; index < 80; index++) { if (slotState[index] != SlotState.UNUSED) { customSlots++; + dirtySlots.set(index); } } @@ -709,6 +723,9 @@ void onDeactivated() { @Override void update() { + + createTeamsIfNecessary(); + T tabOverlay = getTabOverlay(); if (tabOverlay.dirtyFlagSize) { diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/OrderedTabOverlayHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/OrderedTabOverlayHandler.java new file mode 100644 index 00000000..bc06a1b4 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/OrderedTabOverlayHandler.java @@ -0,0 +1,1202 @@ +/* + * Copyright (C) 2020 Florian Stober + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package codecrafter47.bungeetablistplus.handler; + +import codecrafter47.bungeetablistplus.protocol.PacketHandler; +import codecrafter47.bungeetablistplus.protocol.PacketListener; +import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; +import codecrafter47.bungeetablistplus.protocol.Team; +import codecrafter47.bungeetablistplus.util.BitSet; +import codecrafter47.bungeetablistplus.util.ConcurrentBitSet; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.util.GameProfile; +import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.StateRegistry; +import com.velocitypowered.proxy.protocol.packet.HeaderAndFooterPacket; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket; +import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfoPacket; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket; +import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; +import de.codecrafter47.taboverlay.Icon; +import de.codecrafter47.taboverlay.ProfileProperty; +import de.codecrafter47.taboverlay.config.misc.ChatFormat; +import de.codecrafter47.taboverlay.config.misc.Unchecked; +import de.codecrafter47.taboverlay.handler.ContentOperationMode; +import de.codecrafter47.taboverlay.handler.HeaderAndFooterHandle; +import de.codecrafter47.taboverlay.handler.HeaderAndFooterOperationMode; +import de.codecrafter47.taboverlay.handler.RectangularTabOverlay; +import de.codecrafter47.taboverlay.handler.SimpleTabOverlay; +import de.codecrafter47.taboverlay.handler.TabOverlayHandle; +import de.codecrafter47.taboverlay.handler.TabOverlayHandler; +import it.unimi.dsi.fastutil.objects.Object2BooleanMap; +import it.unimi.dsi.fastutil.objects.Object2BooleanOpenHashMap; +import lombok.NonNull; +import lombok.SneakyThrows; +import lombok.val; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Queue; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_3; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_2; + +public class OrderedTabOverlayHandler implements PacketHandler, TabOverlayHandler { + + // some options + private static final boolean OPTION_ENABLE_CUSTOM_SLOT_UUID_COLLISION_CHECK = true; + + private static ComponentHolder EMPTY_COMPONENT; + protected static final String[][] EMPTY_PROPERTIES_ARRAY = new String[0][]; + + private static final ImmutableMap DIMENSION_TO_USED_SLOTS; + private static final BitSet[] SIZE_TO_USED_SLOTS; + + private static final UUID[] CUSTOM_SLOT_UUID_STEVE; + private static final UUID[] CUSTOM_SLOT_UUID_ALEX; + @Nonnull + private static final Set CUSTOM_SLOT_UUIDS; + + static { + + // build the dimension to used slots map (for the rectangular tab overlay) + val builder = ImmutableMap.builder(); + for (int columns = 1; columns <= 4; columns++) { + for (int rows = 0; rows <= 20; rows++) { + if (columns != 1 && rows != 0 && columns * rows <= (columns - 1) * 20) + continue; + BitSet usedSlots = new BitSet(80); + for (int column = 0; column < columns; column++) { + for (int row = 0; row < rows; row++) { + usedSlots.set(index(column, row)); + } + } + builder.put(new RectangularTabOverlay.Dimension(columns, rows), usedSlots); + } + } + DIMENSION_TO_USED_SLOTS = builder.build(); + + // build the size to used slots map (for the simple tab overlay) + SIZE_TO_USED_SLOTS = new BitSet[81]; + for (int size = 0; size <= 80; size++) { + BitSet usedSlots = new BitSet(80); + usedSlots.set(0, size); + SIZE_TO_USED_SLOTS[size] = usedSlots; + } + + // generate random uuids for our custom slots + CUSTOM_SLOT_UUID_ALEX = new UUID[80]; + CUSTOM_SLOT_UUID_STEVE = new UUID[80]; + UUID base = UUID.randomUUID(); + long msb = base.getMostSignificantBits(); + long lsb = base.getLeastSignificantBits(); + lsb ^= base.hashCode(); + for (int i = 0; i < 80; i++) { + CUSTOM_SLOT_UUID_STEVE[i] = new UUID(msb, lsb ^ (2 * i)); + CUSTOM_SLOT_UUID_ALEX[i] = new UUID(msb, lsb ^ (2 * i + 1)); + } + if (OPTION_ENABLE_CUSTOM_SLOT_UUID_COLLISION_CHECK) { + CUSTOM_SLOT_UUIDS = ImmutableSet.builder() + .add(CUSTOM_SLOT_UUID_ALEX) + .add(CUSTOM_SLOT_UUID_STEVE).build(); + } else { + CUSTOM_SLOT_UUIDS = Collections.emptySet(); + } + } + + private final Logger logger; + private final Executor eventLoopExecutor; + + private final Object2BooleanMap serverPlayerListListed = new Object2BooleanOpenHashMap<>(); + @Nullable + protected ComponentHolder serverHeader = null; + @Nullable + protected ComponentHolder serverFooter = null; + + private final Queue> nextActiveContentHandlerQueue = new ConcurrentLinkedQueue<>(); + private final Queue> nextActiveHeaderFooterHandlerQueue = new ConcurrentLinkedQueue<>(); + private AbstractContentOperationModeHandler activeContentHandler; + private AbstractHeaderFooterOperationModeHandler activeHeaderFooterHandler; + + private final AtomicBoolean updateScheduledFlag = new AtomicBoolean(false); + private final Runnable updateTask = this::update; + + protected boolean active; + + private boolean logVersionMismatch = false; + + private final Player player; + + public OrderedTabOverlayHandler(Logger logger, Executor eventLoopExecutor, Player player) { + this.logger = logger; + this.eventLoopExecutor = eventLoopExecutor; + this.player = player; + this.activeContentHandler = new PassThroughContentHandler(); + this.activeHeaderFooterHandler = new PassThroughHeaderFooterHandler(); + EMPTY_COMPONENT = new ComponentHolder(player.getProtocolVersion(), Component.empty()); + } + + @SneakyThrows + private void sendPacket(MinecraftPacket packet) { + if (((packet instanceof UpsertPlayerInfoPacket) || (packet instanceof RemovePlayerInfoPacket)) && (player.getProtocolVersion().compareTo(MINECRAFT_1_19_3) < 0)) { + // error + if (!logVersionMismatch) { + logVersionMismatch = true; + logger.warning("Cannot correctly update tablist for player " + player.getUsername() + "\nThe client and server versions do not match. Client >= 1.19.3, server < 1.19.3.\nUse ViaVersion on the spigot server for the best experience."); + } +// } else if (player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { +// // queue packet? +// ReflectionUtil.getChannelWrapper(player).write(packet); + } else { + PacketListener.sendPacket(player, packet); + } + } + + @Override + public PacketListenerResult onPlayerListPacket(LegacyPlayerListItemPacket packet) { + return PacketListenerResult.PASS; + } + + @Override + public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfoPacket packet) { + + if (!active) { + active = true; + scheduleUpdate(); + } + + if (packet.getActions().contains(UpsertPlayerInfoPacket.Action.ADD_PLAYER)) { + for (UpsertPlayerInfoPacket.Entry item : packet.getEntries()) { + if (OPTION_ENABLE_CUSTOM_SLOT_UUID_COLLISION_CHECK) { + if (CUSTOM_SLOT_UUIDS.contains(item.getProfileId())) { + throw new AssertionError("UUID collision " + item.getProfileId()); + } + } + serverPlayerListListed.putIfAbsent(item.getProfileId(), false); + } + } + if (packet.getActions().contains(UpsertPlayerInfoPacket.Action.UPDATE_LISTED)) { + for (UpsertPlayerInfoPacket.Entry item : packet.getEntries()) { + serverPlayerListListed.put(item.getProfileId(), item.isListed()); + } + } + + try { + return this.activeContentHandler.onPlayerListUpdatePacket(packet); + } catch (Throwable th) { + logger.log(Level.SEVERE, "Unexpected error", th); + // try recover + enterContentOperationMode(ContentOperationMode.PASS_TROUGH); + return PacketListenerResult.PASS; + } + } + + @Override + public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfoPacket packet) { + for (UUID uuid : packet.getProfilesToRemove()) { + serverPlayerListListed.removeBoolean(uuid); + } + return PacketListenerResult.PASS; + } + + @Override + public PacketListenerResult onTeamPacket(Team packet) { + return PacketListenerResult.PASS; + } + + @Override + public PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooterPacket packet) { + PacketListenerResult result = PacketListenerResult.PASS; + try { + result = this.activeHeaderFooterHandler.onPlayerListHeaderFooterPacket(packet); + if (result == PacketListenerResult.MODIFIED) { + throw new AssertionError("PacketListenerResult.MODIFIED must not be used"); + } + } catch (Throwable th) { + logger.log(Level.SEVERE, "Unexpected error", th); + // try recover + enterHeaderAndFooterOperationMode(HeaderAndFooterOperationMode.PASS_TROUGH); + } + + this.serverHeader = packet.getHeader() != null ? packet.getHeader() : EMPTY_COMPONENT; + this.serverFooter = packet.getFooter() != null ? packet.getFooter() : EMPTY_COMPONENT; + + return result; + } + + @Override + public void onServerSwitch(boolean is13OrLater) { + + try { + this.activeContentHandler.onServerSwitch(); + } catch (Throwable th) { + logger.log(Level.SEVERE, "Unexpected error", th); + } + try { + this.activeHeaderFooterHandler.onServerSwitch(); + } catch (Throwable th) { + logger.log(Level.SEVERE, "Unexpected error", th); + } + + if (!serverPlayerListListed.isEmpty()) { + RemovePlayerInfoPacket packet = new RemovePlayerInfoPacket(); + packet.setProfilesToRemove(serverPlayerListListed.keySet()); + sendPacket(packet); + } + + serverPlayerListListed.clear(); + if (serverHeader != null) { + serverHeader = EMPTY_COMPONENT; + } + if (serverFooter != null) { + serverFooter = EMPTY_COMPONENT; + } + + active = false; + } + + @Override + public R enterContentOperationMode(ContentOperationMode operationMode) { + AbstractContentOperationModeHandler handler; + if (operationMode == ContentOperationMode.PASS_TROUGH) { + handler = new PassThroughContentHandler(); + } else if (operationMode == ContentOperationMode.SIMPLE) { + handler = new SimpleOperationModeHandler(); + } else if (operationMode == ContentOperationMode.RECTANGULAR) { + handler = new RectangularSizeHandler(); + } else { + throw new UnsupportedOperationException(); + } + nextActiveContentHandlerQueue.add(handler); + scheduleUpdate(); + return Unchecked.cast(handler.getTabOverlay()); + } + + @Override + public R enterHeaderAndFooterOperationMode(HeaderAndFooterOperationMode operationMode) { + AbstractHeaderFooterOperationModeHandler handler; + if (operationMode == HeaderAndFooterOperationMode.PASS_TROUGH) { + handler = new PassThroughHeaderFooterHandler(); + } else if (operationMode == HeaderAndFooterOperationMode.CUSTOM) { + handler = new CustomHeaderAndFooterOperationModeHandler(); + } else { + throw new UnsupportedOperationException(Objects.toString(operationMode)); + } + nextActiveHeaderFooterHandlerQueue.add(handler); + scheduleUpdate(); + return Unchecked.cast(handler.getTabOverlay()); + } + + private void scheduleUpdate() { + if (this.updateScheduledFlag.compareAndSet(false, true)) { + try { + eventLoopExecutor.execute(updateTask); + } catch (RejectedExecutionException ignored) { + } + } + } + + private void update() { + updateScheduledFlag.set(false); + + MinecraftConnection connection = ((ConnectedPlayer) player).getConnection(); + if(!active || connection.isClosed() || connection.getState() != StateRegistry.PLAY){ + return; + } + + // update content handler + AbstractContentOperationModeHandler contentHandler; + while (null != (contentHandler = nextActiveContentHandlerQueue.poll())) { + this.activeContentHandler.invalidate(); + contentHandler.onActivated(this.activeContentHandler); + this.activeContentHandler = contentHandler; + } + this.activeContentHandler.update(); + + // update header and footer handler + AbstractHeaderFooterOperationModeHandler heaerFooterHandler; + while (null != (heaerFooterHandler = nextActiveHeaderFooterHandlerQueue.poll())) { + this.activeHeaderFooterHandler.invalidate(); + heaerFooterHandler.onActivated(this.activeHeaderFooterHandler); + this.activeHeaderFooterHandler = heaerFooterHandler; + } + this.activeHeaderFooterHandler.update(); + } + + private abstract static class AbstractContentOperationModeHandler extends OperationModeHandler { + + /** + * Called when the player receives a {@link UpsertPlayerInfoPacket} packet. + *

+ * This method is called after this {@link OrderedTabOverlayHandler} has updated the {@code serverPlayerList}. + */ + abstract PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfoPacket packet); + + /** + * Called when the player switches the server. + *

+ * This method is called before this {@link OrderedTabOverlayHandler} executes its own logic to clear the + * server player list info. + */ + abstract void onServerSwitch(); + + abstract void update(); + + final void invalidate() { + getTabOverlay().invalidate(); + onDeactivated(); + } + + /** + * Called when this {@link OperationModeHandler} is deactivated. + *

+ * This method must put the client player list in the state expected by {@link #onActivated(AbstractContentOperationModeHandler)}. It must + * especially remove all custom entries and players must be part of the correct teams. + */ + abstract void onDeactivated(); + + /** + * Called when this {@link OperationModeHandler} becomes the active one. + *

+ * State of the player list when this method is called: + * - there are no custom entries on the client + * - all entries from {@link #serverPlayerListListed} but may not be listed + * - player list header/ footer may be wrong + *

+ * Additional information about the state of the player list may be obtained from the previous handler + * + * @param previous previous handler + */ + abstract void onActivated(AbstractContentOperationModeHandler previous); + } + + private abstract static class AbstractHeaderFooterOperationModeHandler extends OperationModeHandler { + + /** + * Called when the player receives a {@link HeaderAndFooterPacket} packet. + *

+ * This method is called before this {@link OrderedTabOverlayHandler} executes its own logic to update the + * server player list info. + */ + abstract PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooterPacket packet); + + /** + * Called when the player switches the server. + *

+ * This method is called before this {@link OrderedTabOverlayHandler} executes its own logic to clear the + * server player list info. + */ + abstract void onServerSwitch(); + + abstract void update(); + + final void invalidate() { + getTabOverlay().invalidate(); + onDeactivated(); + } + + /** + * Called when this {@link OperationModeHandler} is deactivated. + *

+ * This method must put the client player list in the state expected by {@link #onActivated(AbstractHeaderFooterOperationModeHandler)}. It must + * especially remove all custom entries and players must be part of the correct teams. + */ + abstract void onDeactivated(); + + /** + * Called when this {@link OperationModeHandler} becomes the active one. + *

+ * State of the player list when this method is called: + * - there are no custom entries on the client + * - all entries from {@link #serverPlayerListListed} are known to the client, but might not be listed + * - player list header/ footer may be wrong + *

+ * Additional information about the state of the player list may be obtained from the previous handler + * + * @param previous previous handler + */ + abstract void onActivated(AbstractHeaderFooterOperationModeHandler previous); + } + + private abstract static class AbstractContentTabOverlay implements TabOverlayHandle { + private boolean valid = true; + + @Override + public boolean isValid() { + return valid; + } + + final void invalidate() { + valid = false; + } + } + + private abstract static class AbstractHeaderFooterTabOverlay implements TabOverlayHandle { + private boolean valid = true; + + @Override + public boolean isValid() { + return valid; + } + + final void invalidate() { + valid = false; + } + } + + private final class PassThroughContentHandler extends AbstractContentOperationModeHandler { + + @Override + protected PassThroughContentTabOverlay createTabOverlay() { + return new PassThroughContentTabOverlay(); + } + + @Override + PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfoPacket packet) { + return PacketListenerResult.PASS; + } + + @Override + void onServerSwitch() { + sendPacket(new HeaderAndFooterPacket(EMPTY_COMPONENT, EMPTY_COMPONENT)); + } + + @Override + void update() { + // nothing to do + } + + @Override + void onDeactivated() { + // nothing to do + } + + @Override + void onActivated(AbstractContentOperationModeHandler previous) { + if (previous instanceof PassThroughContentHandler) { + // we're lucky, nothing to do + return; + } + + // update visibility + if (!serverPlayerListListed.isEmpty()) { + List items = new ArrayList<>(serverPlayerListListed.size()); + for (Object2BooleanMap.Entry entry : serverPlayerListListed.object2BooleanEntrySet()) { + UpsertPlayerInfoPacket.Entry item = new UpsertPlayerInfoPacket.Entry(entry.getKey()); + item.setListed(entry.getBooleanValue()); + items.add(item); + } + UpsertPlayerInfoPacket packet = new UpsertPlayerInfoPacket(); + packet.addAction(UpsertPlayerInfoPacket.Action.UPDATE_LISTED); + packet.addAllEntries(items); + sendPacket(packet); + } + } + } + + private static final class PassThroughContentTabOverlay extends AbstractContentTabOverlay { + + } + + private final class PassThroughHeaderFooterHandler extends AbstractHeaderFooterOperationModeHandler { + + @Override + protected PassThroughHeaderFooterTabOverlay createTabOverlay() { + return new PassThroughHeaderFooterTabOverlay(); + } + + @Override + PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooterPacket packet) { + return PacketListenerResult.PASS; + } + + @Override + void onServerSwitch() { + sendPacket(new HeaderAndFooterPacket(EMPTY_COMPONENT, EMPTY_COMPONENT)); + } + + @Override + void update() { + // nothing to do + } + + @Override + void onDeactivated() { + // nothing to do + } + + @Override + void onActivated(AbstractHeaderFooterOperationModeHandler previous) { + if (previous instanceof PassThroughHeaderFooterHandler) { + // we're lucky, nothing to do + return; + } + + // fix header/ footer + sendPacket(new HeaderAndFooterPacket(serverHeader != null ? serverHeader : EMPTY_COMPONENT, serverFooter != null ? serverFooter : EMPTY_COMPONENT)); + } + } + + private static final class PassThroughHeaderFooterTabOverlay extends AbstractHeaderFooterTabOverlay { + + } + + private abstract class CustomContentTabOverlayHandler extends AbstractContentOperationModeHandler { + + @Nonnull + BitSet usedSlots; + BitSet dirtySlots; + final SlotState[] slotState; + /** + * Uuid of the player list entry used for the slot. + */ + final UUID[] slotUuid; + + private final List itemQueueAddPlayer; + private final List itemQueueRemovePlayer; + private final List itemQueueUpdateDisplayName; + private final List itemQueueUpdatePing; + + private CustomContentTabOverlayHandler() { + this.dirtySlots = new BitSet(80); + this.usedSlots = SIZE_TO_USED_SLOTS[0]; + this.slotState = new SlotState[80]; + Arrays.fill(this.slotState, SlotState.UNUSED); + this.slotUuid = new UUID[80]; + this.itemQueueAddPlayer = new ArrayList<>(80); + this.itemQueueRemovePlayer = new ArrayList<>(80); + this.itemQueueUpdateDisplayName = new ArrayList<>(80); + this.itemQueueUpdatePing = new ArrayList<>(80); + } + + @Override + PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfoPacket packet) { + + if (packet.getActions().contains(UpsertPlayerInfoPacket.Action.UPDATE_LISTED)) { + for (UpsertPlayerInfoPacket.Entry item : packet.getEntries()) { + item.setListed(false); + } + } + return PacketListenerResult.MODIFIED; + } + + @Override + void onServerSwitch() { + if (player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0){ + clearCustomSlots(); + } + } + + @Override + void onActivated(AbstractContentOperationModeHandler previous) { + + // make all players unlisted + if (!serverPlayerListListed.isEmpty()) { + List items = new ArrayList<>(serverPlayerListListed.size()); + for (Object2BooleanMap.Entry entry : serverPlayerListListed.object2BooleanEntrySet()) { + UpsertPlayerInfoPacket.Entry item = new UpsertPlayerInfoPacket.Entry(entry.getKey()); + item.setListed(false); + items.add(item); + } + UpsertPlayerInfoPacket packet = new UpsertPlayerInfoPacket(); + packet.addAction(UpsertPlayerInfoPacket.Action.UPDATE_LISTED); + packet.addAllEntries(items); + sendPacket(packet); + } + } + + @Override + void onDeactivated() { + clearCustomSlots(); + } + + private void clearCustomSlots() { + int customSlots = 0; + for (int index = 0; index < 80; index++) { + if (slotState[index] != SlotState.UNUSED) { + customSlots++; + dirtySlots.set(index); + } + } + + int i = 0; + if (customSlots > 0) { + UUID[] uuids = new UUID[customSlots]; + for (int index = 0; index < 80; index++) { + // switch slot from custom to unused + if (slotState[index] == SlotState.CUSTOM) { + uuids[i++] = slotUuid[index]; + } + } + RemovePlayerInfoPacket packet = new RemovePlayerInfoPacket(); + packet.setProfilesToRemove(Arrays.asList(uuids)); + sendPacket(packet); + } + } + + @Override + void update() { + + T tabOverlay = getTabOverlay(); + + if (tabOverlay.dirtyFlagSize) { + tabOverlay.dirtyFlagSize = false; + updateSize(); + } + + // update icons + dirtySlots.orAndClear(tabOverlay.dirtyFlagsIcon); + for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { + if (slotState[index] == SlotState.CUSTOM) { + // remove item + itemQueueRemovePlayer.add(slotUuid[index]); + slotState[index] = SlotState.UNUSED; + } + + if (usedSlots.get(index)) { + Icon icon = tabOverlay.icon[index]; + UUID customSlotUuid; + if (icon.isAlex()) { + customSlotUuid = CUSTOM_SLOT_UUID_ALEX[index]; + } else { // steve + customSlotUuid = CUSTOM_SLOT_UUID_STEVE[index]; + } + tabOverlay.dirtyFlagsText.clear(index); + tabOverlay.dirtyFlagsPing.clear(index); + slotState[index] = SlotState.CUSTOM; + slotUuid[index] = customSlotUuid; + UpsertPlayerInfoPacket.Entry item = new UpsertPlayerInfoPacket.Entry(customSlotUuid); + GameProfile profile = new GameProfile(customSlotUuid, "", toPropertiesList(icon.getTextureProperty())); + item.setProfile(profile); + item.setDisplayName(new ComponentHolder(player.getProtocolVersion(), tabOverlay.text[index])); + item.setLatency(tabOverlay.ping[index]); + item.setGameMode(0); + item.setListed(true); + item.setListOrder(-index); + itemQueueAddPlayer.add(item); + } + } + + // update text + dirtySlots.copyAndClear(tabOverlay.dirtyFlagsText); + for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { + if (slotState[index] != SlotState.UNUSED) { + UpsertPlayerInfoPacket.Entry item = new UpsertPlayerInfoPacket.Entry(slotUuid[index]); + item.setDisplayName(new ComponentHolder(player.getProtocolVersion(), tabOverlay.text[index])); + itemQueueUpdateDisplayName.add(item); + } + } + + // update ping + dirtySlots.copyAndClear(tabOverlay.dirtyFlagsPing); + for (int index = dirtySlots.nextSetBit(0); index >= 0; index = dirtySlots.nextSetBit(index + 1)) { + if (slotState[index] != SlotState.UNUSED) { + UpsertPlayerInfoPacket.Entry item = new UpsertPlayerInfoPacket.Entry(slotUuid[index]); + item.setLatency(tabOverlay.ping[index]); + itemQueueUpdatePing.add(item); + } + } + + dirtySlots.clear(); + + // send packets + sendQueuedItems(); + } + + private void sendQueuedItems() { + if (!itemQueueRemovePlayer.isEmpty()) { + RemovePlayerInfoPacket packet = new RemovePlayerInfoPacket(); + packet.setProfilesToRemove(itemQueueRemovePlayer); + sendPacket(packet); + itemQueueRemovePlayer.clear(); + } + if (!itemQueueAddPlayer.isEmpty()) { + UpsertPlayerInfoPacket packet = new UpsertPlayerInfoPacket(); + packet.addAllActions(EnumSet.of(UpsertPlayerInfoPacket.Action.ADD_PLAYER, UpsertPlayerInfoPacket.Action.UPDATE_DISPLAY_NAME, UpsertPlayerInfoPacket.Action.UPDATE_LATENCY, UpsertPlayerInfoPacket.Action.UPDATE_LISTED, UpsertPlayerInfoPacket.Action.UPDATE_LIST_ORDER)); + packet.addAllEntries(itemQueueAddPlayer); + sendPacket(packet); + itemQueueAddPlayer.clear(); + } + if (!itemQueueUpdateDisplayName.isEmpty()) { + UpsertPlayerInfoPacket packet = new UpsertPlayerInfoPacket(); + packet.addAction(UpsertPlayerInfoPacket.Action.UPDATE_DISPLAY_NAME); + packet.addAllEntries(itemQueueUpdateDisplayName); + sendPacket(packet); + itemQueueUpdateDisplayName.clear(); + } + if (!itemQueueUpdatePing.isEmpty()) { + UpsertPlayerInfoPacket packet = new UpsertPlayerInfoPacket(); + packet.addAction(UpsertPlayerInfoPacket.Action.UPDATE_LATENCY); + packet.addAllEntries(itemQueueUpdatePing); + sendPacket(packet); + itemQueueUpdatePing.clear(); + } + } + + /** + * Updates the usedSlots BitSet. Sets the {@link #dirtySlots uuid dirty flag} for all added + * and removed slots. + */ + abstract void updateSize(); + } + + private abstract class CustomContentTabOverlay extends AbstractContentTabOverlay implements TabOverlayHandle.BatchModifiable { + final Icon[] icon; + final Component[] text; + final int[] ping; + + final AtomicInteger batchUpdateRecursionLevel; + volatile boolean dirtyFlagSize; + final ConcurrentBitSet dirtyFlagsIcon; + final ConcurrentBitSet dirtyFlagsText; + final ConcurrentBitSet dirtyFlagsPing; + + private CustomContentTabOverlay() { + this.icon = new Icon[80]; + Arrays.fill(this.icon, Icon.DEFAULT_STEVE); + this.text = new Component[80]; + Arrays.fill(this.text, Component.empty()); + this.ping = new int[80]; + this.batchUpdateRecursionLevel = new AtomicInteger(0); + this.dirtyFlagSize = true; + this.dirtyFlagsIcon = new ConcurrentBitSet(80); + this.dirtyFlagsText = new ConcurrentBitSet(80); + this.dirtyFlagsPing = new ConcurrentBitSet(80); + } + + @Override + public void beginBatchModification() { + if (isValid()) { + if (batchUpdateRecursionLevel.incrementAndGet() < 0) { + throw new AssertionError("Recursion level overflow"); + } + } + } + + @Override + public void completeBatchModification() { + if (isValid()) { + int level = batchUpdateRecursionLevel.decrementAndGet(); + if (level == 0) { + scheduleUpdate(); + } else if (level < 0) { + throw new AssertionError("Recursion level underflow"); + } + } + } + + void scheduleUpdateIfNotInBatch() { + if (batchUpdateRecursionLevel.get() == 0) { + scheduleUpdate(); + } + } + + void setIconInternal(int index, @Nonnull @NonNull Icon icon) { + if (!icon.equals(this.icon[index])) { + this.icon[index] = icon; + dirtyFlagsIcon.set(index); + scheduleUpdateIfNotInBatch(); + } + } + + void setTextInternal(int index, @Nonnull @NonNull String text) { + Component component = GsonComponentSerializer.gson().deserialize(ChatFormat.formattedTextToJson(text)); + if (!component.equals(this.text[index])) { + this.text[index] = component; + dirtyFlagsText.set(index); + scheduleUpdateIfNotInBatch(); + } + } + + void setPingInternal(int index, int ping) { + if (ping != this.ping[index]) { + this.ping[index] = ping; + dirtyFlagsPing.set(index); + scheduleUpdateIfNotInBatch(); + } + } + } + + private class RectangularSizeHandler extends CustomContentTabOverlayHandler { + + @Override + void updateSize() { + RectangularTabOverlayImpl tabOverlay = getTabOverlay(); + RectangularTabOverlay.Dimension size = tabOverlay.getSize(); + BitSet newUsedSlots = DIMENSION_TO_USED_SLOTS.get(size); + dirtySlots.orXor(usedSlots, newUsedSlots); + usedSlots = newUsedSlots; + } + + @Override + protected RectangularTabOverlayImpl createTabOverlay() { + return new RectangularTabOverlayImpl(); + } + } + + private class RectangularTabOverlayImpl extends CustomContentTabOverlay implements RectangularTabOverlay { + + @Nonnull + private Dimension size; + + private RectangularTabOverlayImpl() { + Optional dimensionZero = getSupportedSizes().stream().filter(size -> size.getSize() == 0).findAny(); + if (!dimensionZero.isPresent()) { + throw new AssertionError(); + } + this.size = dimensionZero.get(); + } + + @Nonnull + @Override + public Dimension getSize() { + return size; + } + + @Override + public Collection getSupportedSizes() { + return DIMENSION_TO_USED_SLOTS.keySet(); + } + + @Override + public void setSize(@Nonnull Dimension size) { + if (!getSupportedSizes().contains(size)) { + throw new IllegalArgumentException("Unsupported size " + size); + } + if (isValid() && !this.size.equals(size)) { + BitSet oldUsedSlots = DIMENSION_TO_USED_SLOTS.get(this.size); + BitSet newUsedSlots = DIMENSION_TO_USED_SLOTS.get(size); + for (int index = newUsedSlots.nextSetBit(0); index >= 0; index = newUsedSlots.nextSetBit(index + 1)) { + if (!oldUsedSlots.get(index)) { + icon[index] = Icon.DEFAULT_STEVE; + text[index] = Component.empty(); + ping[index] = 0; + } + } + this.size = size; + this.dirtyFlagSize = true; + scheduleUpdateIfNotInBatch(); + for (int index = oldUsedSlots.nextSetBit(0); index >= 0; index = oldUsedSlots.nextSetBit(index + 1)) { + if (!newUsedSlots.get(index)) { + icon[index] = Icon.DEFAULT_STEVE; + text[index] = Component.empty(); + ping[index] = 0; + } + } + } + } + + @Override + public void setSlot(int column, int row, @Nullable UUID uuid, @Nonnull Icon icon, @Nonnull String text, int ping) { + setSlot(column, row, icon, text, ping); + } + + @Override + public void setSlot(int column, int row, @Nonnull Icon icon, @Nonnull String text, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(column, size.getColumns(), "column"); + Preconditions.checkElementIndex(row, size.getRows(), "row"); + beginBatchModification(); + try { + int index = index(column, row); + setIconInternal(index, icon); + setTextInternal(index, text); + setPingInternal(index, ping); + } finally { + completeBatchModification(); + } + } + } + + @Override + public void setUuid(int column, int row, @Nullable UUID uuid) { + // no op + } + + @Override + public void setIcon(int column, int row, @Nonnull Icon icon) { + if (isValid()) { + Preconditions.checkElementIndex(column, size.getColumns(), "column"); + Preconditions.checkElementIndex(row, size.getRows(), "row"); + setIconInternal(index(column, row), icon); + } + } + + @Override + public void setText(int column, int row, @Nonnull String text) { + if (isValid()) { + Preconditions.checkElementIndex(column, size.getColumns(), "column"); + Preconditions.checkElementIndex(row, size.getRows(), "row"); + setTextInternal(index(column, row), text); + } + } + + @Override + public void setPing(int column, int row, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(column, size.getColumns(), "column"); + Preconditions.checkElementIndex(row, size.getRows(), "row"); + setPingInternal(index(column, row), ping); + } + } + } + + private class SimpleOperationModeHandler extends CustomContentTabOverlayHandler { + + private int size = 0; + + @Override + void updateSize() { + int newSize = getTabOverlay().size; + if (newSize > size) { + dirtySlots.set(size, newSize); + } else if (newSize < size) { + dirtySlots.set(newSize, size); + } + usedSlots = SIZE_TO_USED_SLOTS[newSize]; + size = newSize; + } + + @Override + protected SimpleTabOverlayImpl createTabOverlay() { + return new SimpleTabOverlayImpl(); + } + } + + private class SimpleTabOverlayImpl extends CustomContentTabOverlay implements SimpleTabOverlay { + int size = 0; + + @Override + public int getSize() { + return size; + } + + @Override + public int getMaxSize() { + return 80; + } + + @Override + public void setSize(int size) { + if (size < 0 || size > 80) { + throw new IllegalArgumentException("size"); + } + this.size = size; + dirtyFlagSize = true; + scheduleUpdateIfNotInBatch(); + } + + @Override + public void setSlot(int index, @Nullable UUID uuid, @Nonnull Icon icon, @Nonnull String text, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(index, size, "index"); + beginBatchModification(); + try { + setIconInternal(index, icon); + setTextInternal(index, text); + setPingInternal(index, ping); + } finally { + completeBatchModification(); + } + } + } + + @Override + public void setSlot(int index, @Nonnull Icon icon, @Nonnull String text, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(index, size, "index"); + beginBatchModification(); + try { + setIconInternal(index, icon); + setTextInternal(index, text); + setPingInternal(index, ping); + } finally { + completeBatchModification(); + } + } + } + + @Override + public void setUuid(int index, UUID uuid) { + // no op + } + + @Override + public void setIcon(int index, @Nonnull @NonNull Icon icon) { + if (isValid()) { + Preconditions.checkElementIndex(index, size, "index"); + setIconInternal(index, icon); + } + } + + @Override + public void setText(int index, @Nonnull @NonNull String text) { + if (isValid()) { + Preconditions.checkElementIndex(index, size, "index"); + setTextInternal(index, text); + } + } + + @Override + public void setPing(int index, int ping) { + if (isValid()) { + Preconditions.checkElementIndex(index, size, "index"); + setPingInternal(index, ping); + } + } + } + + private final class CustomHeaderAndFooterOperationModeHandler extends AbstractHeaderFooterOperationModeHandler { + + @Override + protected CustomHeaderAndFooterImpl createTabOverlay() { + return new CustomHeaderAndFooterImpl(); + } + + @Override + PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooterPacket packet) { + return PacketListenerResult.CANCEL; + } + + @Override + void onServerSwitch() { + // do nothing + } + + @Override + void onDeactivated() { + //do nothing + } + + @Override + void onActivated(AbstractHeaderFooterOperationModeHandler previous) { + // remove header/ footer + sendPacket(new HeaderAndFooterPacket(EMPTY_COMPONENT, EMPTY_COMPONENT)); + } + + @Override + void update() { + CustomHeaderAndFooterImpl tabOverlay = getTabOverlay(); + if (tabOverlay.headerOrFooterDirty) { + tabOverlay.headerOrFooterDirty = false; + sendPacket(new HeaderAndFooterPacket(tabOverlay.header, tabOverlay.footer)); + } + } + } + + private final class CustomHeaderAndFooterImpl extends AbstractHeaderFooterTabOverlay implements HeaderAndFooterHandle { + private ComponentHolder header = EMPTY_COMPONENT; + private ComponentHolder footer = EMPTY_COMPONENT; + + private volatile boolean headerOrFooterDirty = false; + + final AtomicInteger batchUpdateRecursionLevel = new AtomicInteger(0); + + @Override + public void beginBatchModification() { + if (isValid()) { + if (batchUpdateRecursionLevel.incrementAndGet() < 0) { + throw new AssertionError("Recursion level overflow"); + } + } + } + + @Override + public void completeBatchModification() { + if (isValid()) { + int level = batchUpdateRecursionLevel.decrementAndGet(); + if (level == 0) { + scheduleUpdate(); + } else if (level < 0) { + throw new AssertionError("Recursion level underflow"); + } + } + } + + void scheduleUpdateIfNotInBatch() { + if (batchUpdateRecursionLevel.get() == 0) { + scheduleUpdate(); + } + } + + @Override + public void setHeaderFooter(@Nullable String header, @Nullable String footer) { + this.header = new ComponentHolder(player.getProtocolVersion(), ChatFormat.formattedTextToJson(header)); + this.footer = new ComponentHolder(player.getProtocolVersion(), ChatFormat.formattedTextToJson(footer)); + headerOrFooterDirty = true; + scheduleUpdateIfNotInBatch(); + } + + @Override + public void setHeader(@Nullable String header) { + this.header = new ComponentHolder(player.getProtocolVersion(), ChatFormat.formattedTextToJson(header)); + headerOrFooterDirty = true; + scheduleUpdateIfNotInBatch(); + } + + @Override + public void setFooter(@Nullable String footer) { + this.footer = new ComponentHolder(player.getProtocolVersion(), ChatFormat.formattedTextToJson(footer)); + headerOrFooterDirty = true; + scheduleUpdateIfNotInBatch(); + } + } + + private static int index(int column, int row) { + return column * 20 + row; + } + + private static List toPropertiesList(ProfileProperty textureProperty) { + if (textureProperty == null) { + return new ArrayList<>(); + } else if (textureProperty.isSigned()) { + return List.of(new GameProfile.Property(textureProperty.getName(), textureProperty.getValue(), textureProperty.getSignature())); + } else { + return List.of(new GameProfile.Property(textureProperty.getName(), textureProperty.getValue(), "")); + } + } + + private enum SlotState { + UNUSED, CUSTOM + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/TabOverlayHandlerImpl.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/TabOverlayHandlerImpl.java index 2a96269e..d7c6bf66 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/TabOverlayHandlerImpl.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/TabOverlayHandlerImpl.java @@ -18,10 +18,12 @@ package codecrafter47.bungeetablistplus.handler; import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.protocol.PacketListener; import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; import codecrafter47.bungeetablistplus.util.ReflectionUtil; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfoPacket; import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket; @@ -52,7 +54,7 @@ protected void sendPacket(MinecraftPacket packet) { this.logger.warning("Cannot correctly update tablist for player " + player.getUsername() + "\nThe client and server versions do not match. Client < 1.19.3, server >= 1.19.3.\nUse ViaVersion on the spigot server for the best experience."); } } else { - ReflectionUtil.getChannelWrapper(player).write(packet); + PacketListener.sendPacket(player, packet); } } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/RedisPlayerManager.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/RedisPlayerManager.java index 9d3c81be..31e209eb 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/RedisPlayerManager.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/RedisPlayerManager.java @@ -105,9 +105,9 @@ public RedisPlayerManager(VelocityPlayerProvider velocityPlayerProvider, BungeeT RedisBungeeAPI.getRedisBungeeApi().registerPubSubChannels(CHANNEL_REQUEST_DATA_OLD, CHANNEL_DATA_OLD); RedisBungeeAPI.getRedisBungeeApi().registerPubSubChannels(CHANNEL_DATA_REQUEST, CHANNEL_DATA_UPDATE); - plugin.getProxy().getScheduler().buildTask(BungeeTabListPlus.getInstance().getPlugin(), this::updatePlayers).delay(5, TimeUnit.SECONDS).repeat(5, TimeUnit.SECONDS).schedule(); + plugin.getProxy().getScheduler().buildTask(plugin, this::updatePlayers).delay(5, TimeUnit.SECONDS).repeat(5, TimeUnit.SECONDS).schedule(); - plugin.getProxy().getEventManager().register(BungeeTabListPlus.getInstance().getPlugin(), this); + plugin.getProxy().getEventManager().register(plugin, this); } @Override diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/ServerStateManager.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/ServerStateManager.java index 783e033b..bc2c4881 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/ServerStateManager.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/ServerStateManager.java @@ -113,7 +113,7 @@ public int getOnlinePlayers() { @Override public void run() { - if (!plugin.isProxyRunning()) return; + if (plugin.getProxy().isShuttingDown()) return; server.ping().whenComplete((serverPing, throwable) -> { if (throwable != null) { online = false; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/TabViewManager.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/TabViewManager.java index 85fac3e6..fe268f6b 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/TabViewManager.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/TabViewManager.java @@ -25,10 +25,12 @@ import codecrafter47.bungeetablistplus.util.ReflectionUtil; import codecrafter47.bungeetablistplus.version.ProtocolVersionProvider; import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.player.ServerConnectedEvent; import com.velocitypowered.api.event.player.ServerPostConnectEvent; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.network.Connections; import de.codecrafter47.taboverlay.TabView; import de.codecrafter47.taboverlay.config.misc.ChildLogger; @@ -73,6 +75,29 @@ public TabView onPlayerDisconnect(Player player) { return playerTabViewMap.remove(player); } + @Subscribe + public void onServerConnected(ServerConnectedEvent event) { + if (GeyserCompat.isBedrockPlayer(event.getPlayer().getUniqueId())) { + return; + } + try { + Player player = event.getPlayer(); + + PlayerTabView tabView = playerTabViewMap.get(player); + + if (tabView == null) { + throw new AssertionError("Received ServerSwitchEvent for non-existent player " + player.getUsername()); + } + + PacketHandler packetHandler = tabView.packetHandler; + + packetHandler.onServerSwitch(protocolVersionProvider.has113OrLater(player)); + + } catch (Exception ex) { + btlp.getLogger().log(Level.SEVERE, "Failed to inject packet listener", ex); + } + } + @Subscribe public void onServerConnected(ServerPostConnectEvent event) { if (GeyserCompat.isBedrockPlayer(event.getPlayer().getUniqueId())) { @@ -93,9 +118,8 @@ public void onServerConnected(ServerPostConnectEvent event) { PacketHandler packetHandler = tabView.packetHandler; PacketListener packetListener = new PacketListener(server, packetHandler, player); - wrapper.getChannel().pipeline().addBefore(Connections.HANDLER, "btlp-packet-listener", packetListener); - packetHandler.onServerSwitch(protocolVersionProvider.has113OrLater(player)); + wrapper.getChannel().pipeline().addBefore(Connections.HANDLER, "btlp-packet-listener", packetListener); } catch (Exception ex) { btlp.getLogger().log(Level.SEVERE, "Failed to inject packet listener", ex); @@ -108,32 +132,32 @@ public TabView getTabView(Player player) { } private PlayerTabView createTabView(Player player) { - try { - TabOverlayHandler tabOverlayHandler; - PacketHandler packetHandler; - - Logger logger = new ChildLogger(btlp.getLogger(), player.getUsername()); - EventLoop eventLoop = ReflectionUtil.getChannelWrapper(player).eventLoop(); - - if (protocolVersionProvider.has1193OrLater(player)) { - NewTabOverlayHandler handler = new NewTabOverlayHandler(logger, eventLoop, player); - tabOverlayHandler = handler; - packetHandler = new RewriteLogic(new GetGamemodeLogic(handler, player.getUniqueId())); - } else if (protocolVersionProvider.has18OrLater(player)) { - LowMemoryTabOverlayHandlerImpl tabOverlayHandlerImpl = new LowMemoryTabOverlayHandlerImpl(logger, eventLoop, player.getUniqueId(), player, protocolVersionProvider.is18(player), protocolVersionProvider.has113OrLater(player), protocolVersionProvider.has119OrLater(player), protocolVersionProvider.has1203OrLater(player)); - tabOverlayHandler = tabOverlayHandlerImpl; - packetHandler = new RewriteLogic(new GetGamemodeLogic(tabOverlayHandlerImpl, player.getUniqueId())); - } else { - LegacyTabOverlayHandlerImpl legacyTabOverlayHandler = new LegacyTabOverlayHandlerImpl(logger, ReflectionUtil.getTablistHandler(player).getEntries().size(), eventLoop, player, protocolVersionProvider.has113OrLater(player)); - tabOverlayHandler = legacyTabOverlayHandler; - packetHandler = legacyTabOverlayHandler; - } + TabOverlayHandler tabOverlayHandler; + PacketHandler packetHandler; + + Logger logger = new ChildLogger(btlp.getLogger(), player.getUsername()); + EventLoop eventLoop = ((ConnectedPlayer) player).getConnection().eventLoop(); + + if (protocolVersionProvider.has1214OrLater(player)) { + OrderedTabOverlayHandler handler = new OrderedTabOverlayHandler(logger, eventLoop, player); + tabOverlayHandler = handler; + packetHandler = new RewriteLogic(new GetGamemodeLogic(handler, player.getUniqueId())); + } else if (protocolVersionProvider.has1193OrLater(player)) { + NewTabOverlayHandler handler = new NewTabOverlayHandler(logger, eventLoop, player); + tabOverlayHandler = handler; + packetHandler = new RewriteLogic(new GetGamemodeLogic(handler, player.getUniqueId())); + } else if (protocolVersionProvider.has18OrLater(player)) { + LowMemoryTabOverlayHandlerImpl tabOverlayHandlerImpl = new LowMemoryTabOverlayHandlerImpl(logger, eventLoop, player.getUniqueId(), player, protocolVersionProvider.is18(player), protocolVersionProvider.has113OrLater(player), protocolVersionProvider.has119OrLater(player), protocolVersionProvider.has1203OrLater(player)); + tabOverlayHandler = tabOverlayHandlerImpl; + packetHandler = new RewriteLogic(new GetGamemodeLogic(tabOverlayHandlerImpl, player.getUniqueId())); + } else { + LegacyTabOverlayHandlerImpl legacyTabOverlayHandler = new LegacyTabOverlayHandlerImpl(logger, ((ConnectedPlayer) player).getTabList().getEntries().size(), eventLoop, player, protocolVersionProvider.has113OrLater(player)); + tabOverlayHandler = legacyTabOverlayHandler; + packetHandler = legacyTabOverlayHandler; + } - return new PlayerTabView(tabOverlayHandler, logger, btlp.getAsyncExecutor(), packetHandler); + return new PlayerTabView(tabOverlayHandler, logger, btlp.getAsyncExecutor(), packetHandler); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new AssertionError("Failed to create tab view", e); - } } private static class PlayerTabView extends TabView { diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/PlayerPlaceholderResolver.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/PlayerPlaceholderResolver.java index 34628370..d06065d6 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/PlayerPlaceholderResolver.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/PlayerPlaceholderResolver.java @@ -130,6 +130,7 @@ public PlayerPlaceholderResolver(ServerPlaceholderResolver serverPlaceholderReso addPlaceholder("session_duration_total_seconds", create(VelocityData.Velocity_SessionDuration, duration -> duration == null ? null : (int) duration.getSeconds(), TypeToken.INTEGER)); addPlaceholder("session_duration_minutes", create(VelocityData.Velocity_SessionDuration, duration -> duration == null ? null : (int) ((duration.getSeconds() % 3600) / 60), TypeToken.INTEGER)); addPlaceholder("session_duration_hours", create(VelocityData.Velocity_SessionDuration, duration -> duration == null ? null : (int) (duration.getSeconds() / 3600), TypeToken.INTEGER)); + addPlaceholder("locale", create(VelocityData.Velocity_Ping)); addPlaceholder("essentials_afk", create(BukkitData.Essentials_IsAFK)); addPlaceholder("is_hidden", create(BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN)); addPlaceholder("gamemode", create(BTLPVelocityDataKeys.DATA_KEY_GAMEMODE)); diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListener.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListener.java index 14c8cb77..e52a0272 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListener.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListener.java @@ -18,9 +18,9 @@ package codecrafter47.bungeetablistplus.protocol; import codecrafter47.bungeetablistplus.BungeeTabListPlus; -import codecrafter47.bungeetablistplus.util.ReflectionUtil; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.packet.HeaderAndFooterPacket; import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket; @@ -28,6 +28,7 @@ import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToMessageDecoder; +import io.netty.util.ReferenceCountUtil; import java.util.List; @@ -44,6 +45,7 @@ public PacketListener(VelocityServerConnection connection, PacketHandler handler @Override protected void decode(ChannelHandlerContext ctx, MinecraftPacket packet, List out) { + boolean shouldRelease = true; try { if (connection.isActive()) { if (packet != null) { @@ -54,10 +56,7 @@ protected void decode(ChannelHandlerContext ctx, MinecraftPacket packet, List= MINECRAFT_1_21_5) { - nameTagVisibility = NameTagVisibility.values()[ProtocolUtils.readVarInt(buf)]; - collisionRule = CollisionRule.values()[ProtocolUtils.readVarInt(buf)]; + nameTagVisibility = NameTagVisibility.BY_ID[ProtocolUtils.readVarInt( buf )]; + collisionRule = CollisionRule.BY_ID[ProtocolUtils.readVarInt( buf )]; } else { - String nameTagVisibilityStr = ProtocolUtils.readString(buf); - NameTagVisibility nameTagVisibility = NameTagVisibility.BY_NAME.get(nameTagVisibilityStr); - - + nameTagVisibility =readStringMapKey( buf, NameTagVisibility.BY_NAME ); if (version.compareTo(ProtocolVersion.MINECRAFT_1_9) >= 0) { - String collisionRuleStr = ProtocolUtils.readString(buf); - collisionRule = CollisionRule.BY_NAME.get(collisionRuleStr); + collisionRule = readStringMapKey( buf, CollisionRule.BY_NAME ); } } color = (version.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0) ? ProtocolUtils.readVarInt(buf) : buf.readByte(); @@ -134,7 +132,6 @@ public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersi ProtocolUtils.writeString(buf, collisionRule.getKey()); } } - if (version.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0) { ProtocolUtils.writeVarInt(buf, color); prefix.write(buf); @@ -163,11 +160,15 @@ public enum NameTagVisibility { ALWAYS("always"), NEVER("never"), HIDE_FOR_OTHER_TEAMS("hideForOtherTeams"), - HIDE_FOR_OWN_TEAM("hideForOwnTeam"); + HIDE_FOR_OWN_TEAM("hideForOwnTeam"), + // 1.9 (and possibly other versions) appear to treat unknown values differently (always render rather than subject to spectator mode, friendly invisibles, etc). + // we allow the empty value to achieve this in case it is potentially useful even though this is unsupported and its usage may be a bug (#3780). + UNKNOWN( "" ); // private final String key; // private static final Map BY_NAME; + private static final NameTagVisibility[] BY_ID; static { NameTagVisibility[] values = NameTagVisibility.values(); @@ -178,6 +179,7 @@ public enum NameTagVisibility { } BY_NAME = builder.build(); + BY_ID = Arrays.copyOf( values, values.length - 1 ); // Ignore dummy UNKNOWN value } } @@ -193,10 +195,11 @@ public enum CollisionRule { private final String key; // private static final Map BY_NAME; + private static final CollisionRule[] BY_ID; static { - CollisionRule[] values = CollisionRule.values(); - ImmutableMap.Builder builder = ImmutableMap.builderWithExpectedSize(values.length); + CollisionRule[] values = BY_ID = CollisionRule.values(); + ImmutableMap.Builder builder = ImmutableMap.builderWithExpectedSize( values.length ); for (CollisionRule e : values) { builder.put(e.key, e); @@ -205,4 +208,12 @@ public enum CollisionRule { BY_NAME = builder.build(); } } + + public static T readStringMapKey(ByteBuf buf, Map map) { + String string = ProtocolUtils.readString( buf ); + T result = map.get( string ); + Preconditions.checkArgument( result != null, "Unknown string key %s", string ); + + return result; + } } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ReflectionUtil.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ReflectionUtil.java index 9a40b6d2..617483a1 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ReflectionUtil.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ReflectionUtil.java @@ -19,10 +19,6 @@ import codecrafter47.bungeetablistplus.protocol.Team; import com.velocitypowered.api.network.ProtocolVersion; -import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.api.proxy.player.TabList; -import com.velocitypowered.proxy.connection.MinecraftConnection; -import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.StateRegistry; import java.lang.reflect.Constructor; @@ -47,34 +43,6 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9; public class ReflectionUtil { - public static void setTablistHandler(Player player, TabList tablistHandler) throws NoSuchFieldException, IllegalAccessException { - setField(ConnectedPlayer.class, player, "tabList", tablistHandler, 5); - } - - public static TabList getTablistHandler(Player player) throws NoSuchFieldException, IllegalAccessException { - return getField(ConnectedPlayer.class, player, "tabList", 5); - } - - public static MinecraftConnection getChannelWrapper(Player player) throws NoSuchFieldException, IllegalAccessException { - return getField(ConnectedPlayer.class, player, "connection", 50); - } - - public static void setField(Class clazz, Object instance, String field, Object value) throws NoSuchFieldException, IllegalAccessException { - Field f = clazz.getDeclaredField(field); - f.setAccessible(true); - f.set(instance, value); - } - - public static void setField(Class clazz, Object instance, String field, Object value, int tries) throws NoSuchFieldException, IllegalAccessException { - while (--tries > 0) { - try { - setField(clazz, instance, field, value); - return; - } catch (NoSuchFieldException | IllegalAccessException ignored) { - } - } - setField(clazz, instance, field, value); - } @SuppressWarnings("unchecked") public static T getField(Class clazz, Object instance, String field) throws NoSuchFieldException, IllegalAccessException { diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/VelocityPlugin.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/VelocityPlugin.java index 644d1ccd..f7483702 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/VelocityPlugin.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/VelocityPlugin.java @@ -1,42 +1,13 @@ package codecrafter47.bungeetablistplus.util; -import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; -import lombok.Getter; -import net.kyori.adventure.text.Component; import org.slf4j.Logger; -import java.lang.reflect.Field; import java.nio.file.Path; -import java.util.concurrent.atomic.AtomicBoolean; -public class VelocityPlugin { - - @Getter - private final ProxyServer proxy; - @Getter - private final Logger logger; - @Getter - private final Path dataDirectory; - @Getter - private final String version; - - public VelocityPlugin(ProxyServer proxy, Logger logger, Path dataDirectory, String version){ - this.proxy = proxy; - this.logger = logger; - this.dataDirectory = dataDirectory; - this.version = version; - } - - public boolean isProxyRunning(){ - try { - Class velocityServer = Class.forName("com.velocitypowered.proxy.VelocityServer"); - Field shutdownInProgress = velocityServer.getDeclaredField("shutdownInProgress"); - shutdownInProgress.setAccessible(true); - - return !((AtomicBoolean) shutdownInProgress.get(proxy)).get(); - } catch (NoSuchFieldException | ClassNotFoundException | IllegalAccessException ignored) { } - // Return not running if it can't grab the shutdownInProgress value; - return false; - } +public interface VelocityPlugin { + ProxyServer getProxy(); + Logger getLogger(); + Path getDataDirectory(); + String getVersion(); } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ProtocolVersionProvider.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ProtocolVersionProvider.java index 9a793da3..82afe280 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ProtocolVersionProvider.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ProtocolVersionProvider.java @@ -35,4 +35,6 @@ public interface ProtocolVersionProvider { boolean has1193OrLater(Player player); boolean has1203OrLater(Player player); + + boolean has1214OrLater(Player player); } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/version/VelocityProtocolVersionProvider.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/version/VelocityProtocolVersionProvider.java index 039f9901..fc729bb5 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/version/VelocityProtocolVersionProvider.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/version/VelocityProtocolVersionProvider.java @@ -54,9 +54,15 @@ public String getVersion(Player player) { public boolean has1193OrLater(Player player) { return player.getProtocolVersion().getProtocol() >= 761; } + @Override public boolean has1203OrLater(Player player) { return player.getProtocolVersion().getProtocol() >= 765; } + @Override + public boolean has1214OrLater(Player player) { + return player.getProtocolVersion().getProtocol() >= 769; + } + } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ViaVersionProtocolVersionProvider.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ViaVersionProtocolVersionProvider.java index 1c7b58ee..487b7ab7 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ViaVersionProtocolVersionProvider.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ViaVersionProtocolVersionProvider.java @@ -56,6 +56,11 @@ public boolean has1203OrLater(Player player) { return Via.getAPI().getPlayerVersion(player) >= 765; } + @Override + public boolean has1214OrLater(Player player) { + return Via.getAPI().getPlayerVersion(player) >= 769; + } + @Override public String getVersion(Player player) { return ProtocolVersion.getProtocol(Via.getAPI().getPlayerVersion(player)).getName(); From 5d8e9411aa25600764439691928187eccce1ba36 Mon Sep 17 00:00:00 2001 From: proferabg Date: Sun, 9 Feb 2025 13:10:31 -0500 Subject: [PATCH 16/22] Fix github workflow --- .github/workflows/build.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4916c123..288f3918 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,11 +13,11 @@ jobs: with: submodules: recursive - - name: Set up JDK 16 + - name: Set up JDK 17 uses: actions/setup-java@v2 with: distribution: 'temurin' - java-version: '16' + java-version: '17' cache: 'gradle' - name: Build with Gradle @@ -30,6 +30,13 @@ jobs: name: BungeeTabListPlus Bungee path: bootstrap-bungee/build/libs/BungeeTabListPlus-*-SNAPSHOT.jar + - name: Archive artifacts (Velocity) + uses: actions/upload-artifact@v4 + if: success() + with: + name: BungeeTabListPlus Velocity + path: bootstrap-velocity/build/libs/BungeeTabListPlus-*-SNAPSHOT.jar + - name: Archive artifacts (Bukkit) uses: actions/upload-artifact@v4 if: success() From 10d61aae793faa98ab294a3fd81f4d937a742089 Mon Sep 17 00:00:00 2001 From: proferabg Date: Sun, 9 Feb 2025 14:01:29 -0500 Subject: [PATCH 17/22] Make all projects build with JDK 8, make Bungee build with JDK 16, and make Velocity build with JDK 17 --- TabOverlayCommon | 2 +- api-velocity/build.gradle | 7 ++++--- bootstrap-bukkit/build.gradle | 7 +------ bootstrap-bungee/build.gradle | 13 +++++++------ bootstrap-velocity/build.gradle | 8 ++++++-- build.gradle | 7 ++++--- bungee/build.gradle | 6 ++++++ minecraft-data-api | 2 +- sponge/build.gradle | 8 +++++++- velocity/build.gradle | 7 ++++--- 10 files changed, 41 insertions(+), 26 deletions(-) diff --git a/TabOverlayCommon b/TabOverlayCommon index b939ebaa..3d56d654 160000 --- a/TabOverlayCommon +++ b/TabOverlayCommon @@ -1 +1 @@ -Subproject commit b939ebaa875a765c8be88620adbfcd54869815dd +Subproject commit 3d56d654b04617748d555410b30f06ac20f33ffd diff --git a/api-velocity/build.gradle b/api-velocity/build.gradle index eb01d6e0..2dfc5608 100644 --- a/api-velocity/build.gradle +++ b/api-velocity/build.gradle @@ -5,9 +5,10 @@ dependencies { api "de.codecrafter47.taboverlay:taboverlaycommon-api:1.0-SNAPSHOT" } -compileJava { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } } java { diff --git a/bootstrap-bukkit/build.gradle b/bootstrap-bukkit/build.gradle index 8afedd55..63cb6a3e 100644 --- a/bootstrap-bukkit/build.gradle +++ b/bootstrap-bukkit/build.gradle @@ -1,6 +1,6 @@ plugins { - id "com.github.johnrengelman.shadow" version "5.2.0" + id "com.github.johnrengelman.shadow" version "7.1.2" } dependencies { @@ -8,11 +8,6 @@ dependencies { compileOnly "org.spigotmc:spigot-api:${rootProject.ext.spigotVersion}" } -compileJava { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 -} - shadowJar { archiveClassifier.set(null) } diff --git a/bootstrap-bungee/build.gradle b/bootstrap-bungee/build.gradle index 2a1de0b4..25600228 100644 --- a/bootstrap-bungee/build.gradle +++ b/bootstrap-bungee/build.gradle @@ -1,6 +1,12 @@ plugins { - id "com.github.johnrengelman.shadow" version "5.2.0" + id "com.github.johnrengelman.shadow" version "7.1.2" +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(16)) + } } dependencies { @@ -9,11 +15,6 @@ dependencies { compileOnly "net.md-5:bungeecord-proxy:${rootProject.ext.bungeeVersion}" } -compileJava { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 -} - shadowJar { relocate 'codecrafter47.util', 'codecrafter47.bungeetablistplus.util' relocate 'org.bstats', 'codecrafter47.bungeetablistplus.libs.bstats' diff --git a/bootstrap-velocity/build.gradle b/bootstrap-velocity/build.gradle index aa163d70..c1124e4a 100644 --- a/bootstrap-velocity/build.gradle +++ b/bootstrap-velocity/build.gradle @@ -19,10 +19,14 @@ task processSource(type: Sync) { into "$buildDir/src" } +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + compileJava { source = processSource.outputs - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 } shadowJar { diff --git a/build.gradle b/build.gradle index 2f99b264..ed374b4a 100644 --- a/build.gradle +++ b/build.gradle @@ -77,9 +77,10 @@ subprojects { compileOnly 'com.google.code.findbugs:jsr305:3.0.1' } - compileJava { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(8)) + } } publishing { diff --git a/bungee/build.gradle b/bungee/build.gradle index 2e2c3068..6d5bb9b6 100644 --- a/bungee/build.gradle +++ b/bungee/build.gradle @@ -8,6 +8,12 @@ repositories { } } +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(16)) + } +} + dependencies { implementation "de.codecrafter47.data:api:${rootProject.ext.dataApiVersion}" implementation "de.codecrafter47.data.bukkit:api:${rootProject.ext.dataApiVersion}" diff --git a/minecraft-data-api b/minecraft-data-api index e2fec2f0..3e2cf1a2 160000 --- a/minecraft-data-api +++ b/minecraft-data-api @@ -1 +1 @@ -Subproject commit e2fec2f0030cf192ae5af49830bbe83ab6dfe359 +Subproject commit 3e2cf1a244b0dd76e5863f328e9245f6df14fc01 diff --git a/sponge/build.gradle b/sponge/build.gradle index c8b50235..427b9944 100644 --- a/sponge/build.gradle +++ b/sponge/build.gradle @@ -1,6 +1,12 @@ plugins { - id "com.github.johnrengelman.shadow" version "5.2.0" + id "com.github.johnrengelman.shadow" version "7.1.2" +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(8)) + } } dependencies { diff --git a/velocity/build.gradle b/velocity/build.gradle index 4697cbb2..37f0a010 100644 --- a/velocity/build.gradle +++ b/velocity/build.gradle @@ -8,9 +8,10 @@ repositories { } } -compileJava { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } } dependencies { From 52618b0752c6c098e866a68874da103c419f3a4d Mon Sep 17 00:00:00 2001 From: proferabg Date: Sun, 9 Feb 2025 14:25:58 -0500 Subject: [PATCH 18/22] Make Velocity build without jar library --- build.gradle | 1 + libs/.gitkeep | 0 velocity/build.gradle | 8 ++++++-- 3 files changed, 7 insertions(+), 2 deletions(-) delete mode 100644 libs/.gitkeep diff --git a/build.gradle b/build.gradle index ed374b4a..77253c38 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,7 @@ ext { spigotVersion = '1.11-R0.1-SNAPSHOT' bungeeVersion = '1.21-R0.1-SNAPSHOT' velocityVersion = '3.4.0-SNAPSHOT' + adventureVersion = '4.18.0' spongeVersion = '7.0.0' dataApiVersion = '1.0.2-SNAPSHOT' } diff --git a/libs/.gitkeep b/libs/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/velocity/build.gradle b/velocity/build.gradle index 37f0a010..3d923b79 100644 --- a/velocity/build.gradle +++ b/velocity/build.gradle @@ -30,6 +30,12 @@ dependencies { compileOnly "com.velocitypowered:velocity-api:${rootProject.ext.velocityVersion}" testImplementation "com.velocitypowered:velocity-api:${rootProject.ext.velocityVersion}" annotationProcessor "com.velocitypowered:velocity-api:${rootProject.ext.velocityVersion}" + compileOnly "com.velocitypowered:velocity-proxy:${rootProject.ext.velocityVersion}" + annotationProcessor "com.velocitypowered:velocity-proxy:${rootProject.ext.velocityVersion}" + compileOnly "net.kyori:adventure-api:${rootProject.ext.adventureVersion}" + compileOnly "net.kyori:adventure-nbt:${rootProject.ext.adventureVersion}" + compileOnly "net.kyori:adventure-text-serializer-legacy:${rootProject.ext.adventureVersion}" + compileOnly "net.kyori:adventure-text-serializer-gson:${rootProject.ext.adventureVersion}" compileOnly "com.github.proxiodev.redisbungee:RedisBungee-Velocity:0.10.1" compileOnly 'com.google.guava:guava:23.0' testImplementation 'com.google.guava:guava:23.0' @@ -41,8 +47,6 @@ dependencies { annotationProcessor 'org.projectlombok:lombok:1.18.34' testCompileOnly 'org.projectlombok:lombok:1.18.34' testAnnotationProcessor 'org.projectlombok:lombok:1.18.34' - // This imports velocity proxy - compileOnly fileTree(dir: '../libs', include: '*.jar') } processResources { From f8e469ba743d9598841436be9d30cd3a78069cff Mon Sep 17 00:00:00 2001 From: proferabg Date: Sun, 9 Feb 2025 14:37:10 -0500 Subject: [PATCH 19/22] Update submodule --- minecraft-data-api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minecraft-data-api b/minecraft-data-api index 3e2cf1a2..57eb0473 160000 --- a/minecraft-data-api +++ b/minecraft-data-api @@ -1 +1 @@ -Subproject commit 3e2cf1a244b0dd76e5863f328e9245f6df14fc01 +Subproject commit 57eb0473faf254155c1947c3f2866749318cc91d From 766a309862b91c180d4da2fa6caa495250c0e282 Mon Sep 17 00:00:00 2001 From: proferabg Date: Sat, 15 Feb 2025 15:01:47 -0500 Subject: [PATCH 20/22] Update Submodules --- TabOverlayCommon | 2 +- minecraft-data-api | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/TabOverlayCommon b/TabOverlayCommon index 3d56d654..46aca5b1 160000 --- a/TabOverlayCommon +++ b/TabOverlayCommon @@ -1 +1 @@ -Subproject commit 3d56d654b04617748d555410b30f06ac20f33ffd +Subproject commit 46aca5b17b3e9ed96a4c118022eb1bf8f1aa3bd6 diff --git a/minecraft-data-api b/minecraft-data-api index 57eb0473..ef372f5e 160000 --- a/minecraft-data-api +++ b/minecraft-data-api @@ -1 +1 @@ -Subproject commit 57eb0473faf254155c1947c3f2866749318cc91d +Subproject commit ef372f5e360b30712ea8b7da0c20f62b05c595f8 From 17d63267998e5f59194fa01f523a0ad240d8c60e Mon Sep 17 00:00:00 2001 From: proferabg Date: Sat, 15 Feb 2025 22:28:46 -0500 Subject: [PATCH 21/22] Remove check for plugin. --- .../java/codecrafter47/bungeetablistplus/BootstrapPlugin.java | 1 - 1 file changed, 1 deletion(-) diff --git a/bootstrap-velocity/src/main/java/codecrafter47/bungeetablistplus/BootstrapPlugin.java b/bootstrap-velocity/src/main/java/codecrafter47/bungeetablistplus/BootstrapPlugin.java index 7aea52c2..84749c30 100644 --- a/bootstrap-velocity/src/main/java/codecrafter47/bungeetablistplus/BootstrapPlugin.java +++ b/bootstrap-velocity/src/main/java/codecrafter47/bungeetablistplus/BootstrapPlugin.java @@ -88,7 +88,6 @@ public void onProxyInitialization(final ProxyInitializeEvent event) { player.disconnect(Component.text(NO_RELOAD_PLAYERS)); } } - getProxy().getPluginManager().getPlugin("BungeeTabListPlus"); BungeeTabListPlus.getInstance(this).onLoad(); BungeeTabListPlus.getInstance(this).onEnable(); // Metrics From 048669414f75b110cab2793b7e679db89279558a Mon Sep 17 00:00:00 2001 From: proferabg Date: Sun, 16 Feb 2025 18:25:02 -0500 Subject: [PATCH 22/22] Copyrights, Velocity Metrics, and Team Packet Mode Enum --- .../api/velocity/BungeeTabListPlusAPI.java | 2 +- .../api/velocity/FakePlayerManager.java | 2 +- .../bungeetablistplus/api/velocity/Icon.java | 2 +- .../api/velocity/ServerVariable.java | 2 +- .../api/velocity/Variable.java | 2 +- .../api/velocity/tablist/FakePlayer.java | 2 +- .../bungeetablistplus/BootstrapPlugin.java | 5 +- .../bungeetablistplus/BTLPContextKeys.java | 2 +- .../bungeetablistplus/BungeeTabListPlus.java | 3 +- .../bridge/BukkitBridge.java | 3 +- .../bridge/NetDataKeyIdMap.java | 2 +- .../bungeetablistplus/cache/Cache.java | 2 +- .../command/CommandBungeeTabListPlus.java | 2 +- .../command/CommandDebug.java | 2 +- .../command/CommandFakePlayers.java | 2 +- .../command/CommandHide.java | 2 +- .../bungeetablistplus/config/Comment.java | 2 +- .../bungeetablistplus/config/MainConfig.java | 3 +- .../bungeetablistplus/config/Path.java | 2 +- ...PlayersByServerComponentConfiguration.java | 2 +- .../data/AbstractCompositeDataProvider.java | 17 ++++ .../bungeetablistplus/data/BTLPDataTypes.java | 2 +- .../data/BTLPVelocityDataKeys.java | 2 +- .../data/NullDataHolder.java | 2 +- .../data/PermissionDataProvider.java | 17 ++++ .../data/ServerDataHolder.java | 2 +- .../data/TrackingDataCache.java | 2 +- .../AbstractLegacyTabOverlayHandler.java | 10 +- .../handler/AbstractTabOverlayHandler.java | 38 ++++---- .../handler/GetGamemodeLogic.java | 2 +- .../handler/LegacyTabOverlayHandlerImpl.java | 4 +- .../LowMemoryTabOverlayHandlerImpl.java | 2 +- .../handler/NewTabOverlayHandler.java | 4 +- .../handler/OperationModeHandler.java | 2 +- .../handler/OrderedTabOverlayHandler.java | 2 +- .../handler/RewriteLogic.java | 2 +- .../handler/TabOverlayHandlerImpl.java | 4 +- .../listener/TabListListener.java | 3 +- .../bungeetablistplus/managers/API.java | 2 +- .../managers/DataManager.java | 2 +- .../managers/HiddenPlayersManager.java | 2 +- .../managers/RedisPlayerManager.java | 2 +- .../managers/ServerStateManager.java | 2 +- .../managers/TabViewManager.java | 3 +- .../managers/VelocityPlayerProvider.java | 2 +- .../ComponentServerPlaceholderResolver.java | 2 +- .../GlobalServerPlaceholderResolver.java | 2 +- .../PlayerPlaceholderResolver.java | 2 +- .../ServerCountPlaceholderResolver.java | 2 +- .../ServerPlaceholderResolver.java | 2 +- .../player/AbstractPlayer.java | 2 +- .../bungeetablistplus/player/FakePlayer.java | 2 +- .../player/FakePlayerManagerImpl.java | 2 +- .../bungeetablistplus/player/RedisPlayer.java | 2 +- .../player/VelocityPlayer.java | 2 +- .../protocol/AbstractPacketHandler.java | 2 +- .../protocol/PacketHandler.java | 2 +- .../protocol/PacketListener.java | 2 +- .../protocol/PacketListenerResult.java | 2 +- .../bungeetablistplus/protocol/Team.java | 91 +++++++------------ .../ExcludedServersTabOverlayProvider.java | 2 +- .../PlayersByServerComponentTemplate.java | 2 +- .../updater/UpdateChecker.java | 3 +- .../updater/UpdateNotifier.java | 3 +- .../bungeetablistplus/util/BitSet.java | 2 +- .../bungeetablistplus/util/ColorParser.java | 3 +- .../util/ConcurrentBitSet.java | 2 +- .../util/ContextAwareOrdering.java | 2 +- .../util/EmptyOrderedPlayerSet.java | 2 +- .../util/EmptyPlayerSet.java | 2 +- .../util/ExceptionHandlingEventExecutor.java | 2 +- .../bungeetablistplus/util/Functions.java | 2 +- .../bungeetablistplus/util/GeyserCompat.java | 17 ++++ .../bungeetablistplus/util/IconUtil.java | 2 +- .../util/IntToIntFunction.java | 2 +- .../bungeetablistplus/util/MapFunction.java | 2 +- .../util/MatchingStringsCollection.java | 2 +- .../util/Object2IntHashMultimap.java | 2 +- .../util/Property119Handler.java | 17 ++++ .../bungeetablistplus/util/ProxyServer.java | 17 ++++ .../util/ReflectionUtil.java | 2 +- .../util/VelocityPlugin.java | 17 ++++ .../bungeetablistplus/util/chat/ChatUtil.java | 17 ++++ .../version/ProtocolVersionProvider.java | 2 +- .../VelocityProtocolVersionProvider.java | 2 +- .../ViaVersionProtocolVersionProvider.java | 2 +- .../view/PlayersByServerComponentView.java | 2 +- 87 files changed, 264 insertions(+), 165 deletions(-) diff --git a/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/BungeeTabListPlusAPI.java b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/BungeeTabListPlusAPI.java index c112d471..55f99a0d 100644 --- a/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/BungeeTabListPlusAPI.java +++ b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/BungeeTabListPlusAPI.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/FakePlayerManager.java b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/FakePlayerManager.java index 78f18631..2d984261 100644 --- a/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/FakePlayerManager.java +++ b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/FakePlayerManager.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/Icon.java b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/Icon.java index af9cc58c..34c83a68 100644 --- a/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/Icon.java +++ b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/Icon.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/ServerVariable.java b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/ServerVariable.java index 9eb13a7b..1cdcbe0b 100644 --- a/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/ServerVariable.java +++ b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/ServerVariable.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/Variable.java b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/Variable.java index fc0a3f1a..b3c4224d 100644 --- a/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/Variable.java +++ b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/Variable.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/tablist/FakePlayer.java b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/tablist/FakePlayer.java index 5b8aca81..0d6d802e 100644 --- a/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/tablist/FakePlayer.java +++ b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/tablist/FakePlayer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/bootstrap-velocity/src/main/java/codecrafter47/bungeetablistplus/BootstrapPlugin.java b/bootstrap-velocity/src/main/java/codecrafter47/bungeetablistplus/BootstrapPlugin.java index 84749c30..2def5d8b 100644 --- a/bootstrap-velocity/src/main/java/codecrafter47/bungeetablistplus/BootstrapPlugin.java +++ b/bootstrap-velocity/src/main/java/codecrafter47/bungeetablistplus/BootstrapPlugin.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -24,7 +24,6 @@ import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; import com.velocitypowered.api.plugin.Dependency; import com.velocitypowered.api.plugin.Plugin; -import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.plugin.annotation.DataDirectory; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; @@ -91,7 +90,7 @@ public void onProxyInitialization(final ProxyInitializeEvent event) { BungeeTabListPlus.getInstance(this).onLoad(); BungeeTabListPlus.getInstance(this).onEnable(); // Metrics - metricsFactory.make(this, 4332); + metricsFactory.make(this, 24808); } @Subscribe diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/BTLPContextKeys.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/BTLPContextKeys.java index 786e9035..7239cda5 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/BTLPContextKeys.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/BTLPContextKeys.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java index e2189294..79bc988e 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,6 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + package codecrafter47.bungeetablistplus; import codecrafter47.bungeetablistplus.api.velocity.BungeeTabListPlusAPI; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/bridge/BukkitBridge.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/bridge/BukkitBridge.java index 603ac664..f8c4deec 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/bridge/BukkitBridge.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/bridge/BukkitBridge.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,6 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + package codecrafter47.bungeetablistplus.bridge; import codecrafter47.bungeetablistplus.BungeeTabListPlus; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/bridge/NetDataKeyIdMap.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/bridge/NetDataKeyIdMap.java index c05858d1..4088d719 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/bridge/NetDataKeyIdMap.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/bridge/NetDataKeyIdMap.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/cache/Cache.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/cache/Cache.java index 82d0ad29..1c76534e 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/cache/Cache.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/cache/Cache.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandBungeeTabListPlus.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandBungeeTabListPlus.java index 92b5a508..78dd98aa 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandBungeeTabListPlus.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandBungeeTabListPlus.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandDebug.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandDebug.java index 05fc6a20..2ce01dc9 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandDebug.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandDebug.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandFakePlayers.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandFakePlayers.java index d998bc58..e8c9bef7 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandFakePlayers.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandFakePlayers.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandHide.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandHide.java index bbc60eed..8f51b379 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandHide.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandHide.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/config/Comment.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/config/Comment.java index ef099195..19af2a88 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/config/Comment.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/config/Comment.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/config/MainConfig.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/config/MainConfig.java index 32b2c4a3..2e753135 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/config/MainConfig.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/config/MainConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,6 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + package codecrafter47.bungeetablistplus.config; import com.google.common.collect.ImmutableList; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/config/Path.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/config/Path.java index 5c7ee777..42fe46fe 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/config/Path.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/config/Path.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/config/PlayersByServerComponentConfiguration.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/config/PlayersByServerComponentConfiguration.java index c14f0e16..53df0979 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/config/PlayersByServerComponentConfiguration.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/config/PlayersByServerComponentConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/data/AbstractCompositeDataProvider.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/AbstractCompositeDataProvider.java index 5738be12..1fcfb4de 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/data/AbstractCompositeDataProvider.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/AbstractCompositeDataProvider.java @@ -1,3 +1,20 @@ +/* + * Copyright (C) 2025 proferabg + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package codecrafter47.bungeetablistplus.data; import codecrafter47.bungeetablistplus.player.VelocityPlayer; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/data/BTLPDataTypes.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/BTLPDataTypes.java index d0cfed20..31a84dc7 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/data/BTLPDataTypes.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/BTLPDataTypes.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/data/BTLPVelocityDataKeys.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/BTLPVelocityDataKeys.java index 969df084..8ce55de2 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/data/BTLPVelocityDataKeys.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/BTLPVelocityDataKeys.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/data/NullDataHolder.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/NullDataHolder.java index 53be589e..3429e93a 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/data/NullDataHolder.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/NullDataHolder.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/data/PermissionDataProvider.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/PermissionDataProvider.java index aa15b0c1..19e9a732 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/data/PermissionDataProvider.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/PermissionDataProvider.java @@ -1,3 +1,20 @@ +/* + * Copyright (C) 2025 proferabg + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package codecrafter47.bungeetablistplus.data; import codecrafter47.bungeetablistplus.player.VelocityPlayer; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/data/ServerDataHolder.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/ServerDataHolder.java index 08f326fe..d1fcbf16 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/data/ServerDataHolder.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/ServerDataHolder.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/data/TrackingDataCache.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/TrackingDataCache.java index 0ab75b9d..75402eea 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/data/TrackingDataCache.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/TrackingDataCache.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractLegacyTabOverlayHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractLegacyTabOverlayHandler.java index 70b8e95f..150565d9 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractLegacyTabOverlayHandler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractLegacyTabOverlayHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -413,7 +413,7 @@ void onDeactivated() { removeEntry(slotUUID[index], slotID[index]); Team t = new Team(); t.setName(slotID[index]); - t.setMode((byte) 1); + t.setMode(Team.Mode.REMOVE); sendPacket(t); } } @@ -430,7 +430,7 @@ private void updateSize() { updateSlot(tabOverlay, index); Team t = new Team(); t.setName(slotID[index]); - t.setMode((byte) 0); + t.setMode(Team.Mode.CREATE); t.setPrefix(new ComponentHolder(is13OrLater ? ProtocolVersion.MINECRAFT_1_13 : ProtocolVersion.MINECRAFT_1_12_2, tabOverlay.text0[index])); t.setDisplayName(new ComponentHolder(is13OrLater ? ProtocolVersion.MINECRAFT_1_13 : ProtocolVersion.MINECRAFT_1_12_2, "")); t.setSuffix(new ComponentHolder(is13OrLater ? ProtocolVersion.MINECRAFT_1_13 : ProtocolVersion.MINECRAFT_1_12_2, tabOverlay.text1[index])); @@ -444,7 +444,7 @@ private void updateSize() { removeEntry(slotUUID[index], slotID[index]); Team t = new Team(); t.setName(slotID[index]); - t.setMode((byte) 1); + t.setMode(Team.Mode.REMOVE); sendPacket(t); } } @@ -466,7 +466,7 @@ private void updateText(CustomTabOverlay tabOverlay, int index) { if (index < size) { Team packet = new Team(); packet.setName(slotID[index]); - packet.setMode((byte) 2); + packet.setMode(Team.Mode.UPDATE_INFO); packet.setPrefix(new ComponentHolder(is13OrLater ? ProtocolVersion.MINECRAFT_1_13 : ProtocolVersion.MINECRAFT_1_12_2, tabOverlay.text0[index])); packet.setDisplayName(new ComponentHolder(is13OrLater ? ProtocolVersion.MINECRAFT_1_13 : ProtocolVersion.MINECRAFT_1_12_2, "")); packet.setSuffix(new ComponentHolder(is13OrLater ? ProtocolVersion.MINECRAFT_1_13 : ProtocolVersion.MINECRAFT_1_12_2, tabOverlay.text1[index])); diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java index ac351713..b878b23c 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -299,7 +299,7 @@ public PacketListenerResult onTeamPacket(Team packet) { enterContentOperationMode(ContentOperationMode.PASS_TROUGH); } - if (packet.getMode() == 1) { + if (Team.Mode.REMOVE.equals(packet.getMode())) { TeamEntry team = serverTeams.remove(packet.getName()); if (team != null) { for (String player : team.getPlayers()) { @@ -309,7 +309,7 @@ public PacketListenerResult onTeamPacket(Team packet) { } else { // Create or get old team TeamEntry teamEntry; - if (packet.getMode() == 0) { + if (Team.Mode.CREATE.equals(packet.getMode())) { teamEntry = new TeamEntry(); serverTeams.put(packet.getName(), teamEntry); } else { @@ -317,7 +317,7 @@ public PacketListenerResult onTeamPacket(Team packet) { } if (teamEntry != null) { - if (packet.getMode() == 0 || packet.getMode() == 2) { + if (Team.Mode.CREATE.equals(packet.getMode()) || Team.Mode.UPDATE_INFO.equals(packet.getMode())) { teamEntry.setDisplayName(packet.getDisplayName()); teamEntry.setPrefix(packet.getPrefix()); teamEntry.setSuffix(packet.getSuffix()); @@ -328,7 +328,7 @@ public PacketListenerResult onTeamPacket(Team packet) { } if (packet.getPlayers() != null) { for (String s : packet.getPlayers()) { - if (packet.getMode() == 0 || packet.getMode() == 3) { + if (Team.Mode.CREATE.equals(packet.getMode()) || Team.Mode.ADD_PLAYER.equals(packet.getMode())) { if (playerToTeamMap.containsKey(s)) { TeamEntry previousTeam = serverTeams.get(playerToTeamMap.get(s)); // previousTeam shouldn't be null (that's inconsistent with playerToTeamMap, but apparently it happens) @@ -1032,7 +1032,7 @@ private String getCustomSlotUsername(int index) { @Override void onTeamPacketPreprocess(Team packet) { if (!using80Slots) { - if (packet.getMode() == 1) { + if (Team.Mode.REMOVE.equals(packet.getMode())) { TeamEntry teamEntry = serverTeams.get(packet.getName()); if (teamEntry != null) { for (String playerName : teamEntry.getPlayers()) { @@ -1045,7 +1045,7 @@ void onTeamPacketPreprocess(Team packet) { } } } else { - if (packet.getMode() == 1) { + if (Team.Mode.REMOVE.equals(packet.getMode())) { TeamEntry teamEntry = serverTeams.get(packet.getName()); if (teamEntry != null) { for (String playerName : teamEntry.getPlayers()) { @@ -1067,8 +1067,8 @@ PacketListenerResult onTeamPacket(Team packet) { if (!using80Slots) { boolean modified = false; switch (packet.getMode()) { - case 0: - case 3: + case CREATE: + case ADD_PLAYER: int count = 0; String[] players = packet.getPlayers(); for (int i = 0; i < players.length; i++) { @@ -1097,7 +1097,7 @@ PacketListenerResult onTeamPacket(Team packet) { packet.setPlayers(filteredPlayers); } break; - case 4: + case REMOVE_PLAYER: count = 0; players = packet.getPlayers(); for (int i = 0; i < players.length; i++) { @@ -1123,7 +1123,7 @@ PacketListenerResult onTeamPacket(Team packet) { packet.setPlayers(filteredPlayers); } break; - case 2: + case UPDATE_INFO: TeamEntry teamEntry = serverTeams.get(packet.getName()); if (teamEntry != null) { for (String playerName : teamEntry.getPlayers()) { @@ -1143,8 +1143,8 @@ PacketListenerResult onTeamPacket(Team packet) { } else { switch (packet.getMode()) { - case 0: - case 3: + case CREATE: + case ADD_PLAYER: /* // Don't need this. Adding the player to another team will remove him from the current one. String[] players = packet.getPlayers(); @@ -1156,7 +1156,7 @@ PacketListenerResult onTeamPacket(Team packet) { } }*/ break; - case 4: + case REMOVE_PLAYER: String[] players = packet.getPlayers(); for (int i = 0; i < players.length; i++) { String playerName = players[i]; @@ -2409,7 +2409,7 @@ private static String[][] toPropertiesArray(ProfileProperty textureProperty) { private static Team createPacketTeamCreate(String name, ComponentHolder displayName, ComponentHolder prefix, ComponentHolder suffix, Team.NameTagVisibility nameTagVisibility, Team.CollisionRule collisionRule, int color, byte friendlyFire, String[] players) { Team team = new Team(); team.setName(name); - team.setMode((byte) 0); + team.setMode(Team.Mode.CREATE); team.setDisplayName(displayName); team.setPrefix(prefix); team.setSuffix(suffix); @@ -2424,14 +2424,14 @@ private static Team createPacketTeamCreate(String name, ComponentHolder displayN private static Team createPacketTeamRemove(String name) { Team team = new Team(); team.setName(name); - team.setMode((byte) 1); + team.setMode(Team.Mode.REMOVE); return team; } private static Team createPacketTeamUpdate(String name, ComponentHolder displayName, ComponentHolder prefix, ComponentHolder suffix, Team.NameTagVisibility nameTagVisibility, Team.CollisionRule collisionRule, int color, byte friendlyFire) { Team team = new Team(); team.setName(name); - team.setMode((byte) 2); + team.setMode(Team.Mode.UPDATE_INFO); team.setDisplayName(displayName); team.setPrefix(prefix); team.setSuffix(suffix); @@ -2445,7 +2445,7 @@ private static Team createPacketTeamUpdate(String name, ComponentHolder displayN private static Team createPacketTeamAddPlayers(String name, String[] players) { Team team = new Team(); team.setName(name); - team.setMode((byte) 3); + team.setMode(Team.Mode.ADD_PLAYER); team.setPlayers(players); return team; } @@ -2453,7 +2453,7 @@ private static Team createPacketTeamAddPlayers(String name, String[] players) { private static Team createPacketTeamRemovePlayers(String name, String[] players) { Team team = new Team(); team.setName(name); - team.setMode((byte) 4); + team.setMode(Team.Mode.REMOVE_PLAYER); team.setPlayers(players); return team; } diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/GetGamemodeLogic.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/GetGamemodeLogic.java index 86342572..9db5fa3e 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/GetGamemodeLogic.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/GetGamemodeLogic.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LegacyTabOverlayHandlerImpl.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LegacyTabOverlayHandlerImpl.java index 8ab1f7fc..0974b32f 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LegacyTabOverlayHandlerImpl.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LegacyTabOverlayHandlerImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,9 +18,7 @@ package codecrafter47.bungeetablistplus.handler; import codecrafter47.bungeetablistplus.protocol.PacketListener; -import codecrafter47.bungeetablistplus.util.ReflectionUtil; import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket; import lombok.SneakyThrows; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LowMemoryTabOverlayHandlerImpl.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LowMemoryTabOverlayHandlerImpl.java index 7135d100..bec680dd 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LowMemoryTabOverlayHandlerImpl.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LowMemoryTabOverlayHandlerImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java index 5b93198b..34507198 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -1251,7 +1251,7 @@ private static List toPropertiesList(ProfileProperty textu private static Team createPacketTeamCreate(String name, ComponentHolder displayName, ComponentHolder prefix, ComponentHolder suffix, Team.NameTagVisibility nameTagVisibility, Team.CollisionRule collisionRule, int color, byte friendlyFire, String[] players) { Team team = new Team(); team.setName(name); - team.setMode((byte) 0); + team.setMode(Team.Mode.CREATE); team.setDisplayName(displayName); team.setPrefix(prefix); team.setSuffix(suffix); diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/OperationModeHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/OperationModeHandler.java index 7e89584e..36db02b1 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/OperationModeHandler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/OperationModeHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/OrderedTabOverlayHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/OrderedTabOverlayHandler.java index bc06a1b4..b37d0bc4 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/OrderedTabOverlayHandler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/OrderedTabOverlayHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/RewriteLogic.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/RewriteLogic.java index 576f0bcc..2446d66f 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/RewriteLogic.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/RewriteLogic.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/TabOverlayHandlerImpl.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/TabOverlayHandlerImpl.java index d7c6bf66..838b1a6c 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/TabOverlayHandlerImpl.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/TabOverlayHandlerImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,10 +20,8 @@ import codecrafter47.bungeetablistplus.BungeeTabListPlus; import codecrafter47.bungeetablistplus.protocol.PacketListener; import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; -import codecrafter47.bungeetablistplus.util.ReflectionUtil; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfoPacket; import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/listener/TabListListener.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/listener/TabListListener.java index 437db0b2..b61e5bde 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/listener/TabListListener.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/listener/TabListListener.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,6 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + package codecrafter47.bungeetablistplus.listener; import codecrafter47.bungeetablistplus.BungeeTabListPlus; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/API.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/API.java index dee2acf1..839309ae 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/API.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/API.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/DataManager.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/DataManager.java index 86f4aaf3..a2690e24 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/DataManager.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/DataManager.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/HiddenPlayersManager.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/HiddenPlayersManager.java index 55113290..3e8ddff9 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/HiddenPlayersManager.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/HiddenPlayersManager.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/RedisPlayerManager.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/RedisPlayerManager.java index 31e209eb..b4d80470 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/RedisPlayerManager.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/RedisPlayerManager.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/ServerStateManager.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/ServerStateManager.java index bc2c4881..bb2f9b4c 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/ServerStateManager.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/ServerStateManager.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/TabViewManager.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/TabViewManager.java index fe268f6b..d4702a51 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/TabViewManager.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/TabViewManager.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,7 +22,6 @@ import codecrafter47.bungeetablistplus.protocol.PacketHandler; import codecrafter47.bungeetablistplus.protocol.PacketListener; import codecrafter47.bungeetablistplus.util.GeyserCompat; -import codecrafter47.bungeetablistplus.util.ReflectionUtil; import codecrafter47.bungeetablistplus.version.ProtocolVersionProvider; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.player.ServerConnectedEvent; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/VelocityPlayerProvider.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/VelocityPlayerProvider.java index b6b95fc7..e9c2f849 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/VelocityPlayerProvider.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/VelocityPlayerProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/ComponentServerPlaceholderResolver.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/ComponentServerPlaceholderResolver.java index 00b0a7a9..90287b87 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/ComponentServerPlaceholderResolver.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/ComponentServerPlaceholderResolver.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/GlobalServerPlaceholderResolver.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/GlobalServerPlaceholderResolver.java index 7b3b1e40..cdbd8571 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/GlobalServerPlaceholderResolver.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/GlobalServerPlaceholderResolver.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/PlayerPlaceholderResolver.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/PlayerPlaceholderResolver.java index d06065d6..ac405028 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/PlayerPlaceholderResolver.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/PlayerPlaceholderResolver.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/ServerCountPlaceholderResolver.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/ServerCountPlaceholderResolver.java index 73669690..a251e140 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/ServerCountPlaceholderResolver.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/ServerCountPlaceholderResolver.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/ServerPlaceholderResolver.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/ServerPlaceholderResolver.java index 5453ff0a..36464b8b 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/ServerPlaceholderResolver.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/ServerPlaceholderResolver.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/player/AbstractPlayer.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/player/AbstractPlayer.java index bebd5a74..9e8c07d3 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/player/AbstractPlayer.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/player/AbstractPlayer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/player/FakePlayer.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/player/FakePlayer.java index ee55843d..5c4b7027 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/player/FakePlayer.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/player/FakePlayer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/player/FakePlayerManagerImpl.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/player/FakePlayerManagerImpl.java index 9f542016..3bbfd4fb 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/player/FakePlayerManagerImpl.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/player/FakePlayerManagerImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/player/RedisPlayer.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/player/RedisPlayer.java index 8cdd1cc3..d691d639 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/player/RedisPlayer.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/player/RedisPlayer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/player/VelocityPlayer.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/player/VelocityPlayer.java index 52a6e1a8..0e900c2a 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/player/VelocityPlayer.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/player/VelocityPlayer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/AbstractPacketHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/AbstractPacketHandler.java index fe0b9e88..f4f95cf3 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/AbstractPacketHandler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/AbstractPacketHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketHandler.java index ec0c9ff6..c629e224 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketHandler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListener.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListener.java index e52a0272..672925ac 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListener.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListener.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListenerResult.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListenerResult.java index 0a78fae1..5d3b9eeb 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListenerResult.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListenerResult.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/Team.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/Team.java index 891d696d..5b59e133 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/Team.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/Team.java @@ -1,24 +1,23 @@ /* - * Copyright (C) 2018-2023 Velocity Contributors + * Copyright (C) 2025 proferabg * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . */ package codecrafter47.bungeetablistplus.protocol; import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableMap; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.protocol.MinecraftPacket; @@ -34,6 +33,7 @@ import java.util.Arrays; import java.util.Map; +import java.util.stream.Collectors; @Data @NoArgsConstructor @@ -41,17 +41,16 @@ @EqualsAndHashCode(callSuper = false) public class Team implements MinecraftPacket { - public static final byte CREATE = 0; - public static final byte REMOVE = 1; - public static final byte UPDATE_INFO = 2; - public static final byte ADD_PLAYER = 3; - public static final byte REMOVE_PLAYER = 4; + public enum Mode { + CREATE, + REMOVE, + UPDATE_INFO, + ADD_PLAYER, + REMOVE_PLAYER + } private String name; - /** - * 0 - create, 1 remove, 2 info update, 3 player add, 4 player remove. - */ - private byte mode; + private Mode mode; private ComponentHolder displayName; private ComponentHolder prefix; private ComponentHolder suffix; @@ -61,25 +60,20 @@ public class Team implements MinecraftPacket { private byte friendlyFire; private String[] players; - // placeholder until release + // TODO: placeholder until release private int MINECRAFT_1_21_5 = 770; - /** - * Packet to destroy a team. - * - * @param name team name - */ public Team(String name) { this.name = name; - this.mode = 1; + this.mode = Mode.REMOVE; } @Override public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { name = ProtocolUtils.readString(buf); - mode = buf.readByte(); - if (mode == CREATE || mode == UPDATE_INFO) { + mode = Mode.values()[buf.readByte()]; + if (mode == Mode.CREATE || mode == Mode.UPDATE_INFO) { displayName = ComponentHolder.read(buf, version); if (version.compareTo(ProtocolVersion.MINECRAFT_1_13) < 0) { prefix = ComponentHolder.read(buf, version); @@ -91,9 +85,9 @@ public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersi nameTagVisibility = NameTagVisibility.BY_ID[ProtocolUtils.readVarInt( buf )]; collisionRule = CollisionRule.BY_ID[ProtocolUtils.readVarInt( buf )]; } else { - nameTagVisibility =readStringMapKey( buf, NameTagVisibility.BY_NAME ); + nameTagVisibility = readStringToMap( buf, NameTagVisibility.BY_NAME ); if (version.compareTo(ProtocolVersion.MINECRAFT_1_9) >= 0) { - collisionRule = readStringMapKey( buf, CollisionRule.BY_NAME ); + collisionRule = readStringToMap( buf, CollisionRule.BY_NAME ); } } color = (version.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0) ? ProtocolUtils.readVarInt(buf) : buf.readByte(); @@ -102,7 +96,7 @@ public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersi suffix = ComponentHolder.read(buf, version); } } - if (mode == CREATE || mode == ADD_PLAYER || mode == REMOVE_PLAYER) { + if (mode == Mode.CREATE || mode == Mode.ADD_PLAYER || mode == Mode.REMOVE_PLAYER) { int len = ProtocolUtils.readVarInt(buf); players = new String[len]; for (int i = 0; i < len; i++) { @@ -114,8 +108,8 @@ public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersi @Override public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { ProtocolUtils.writeString(buf, name); - buf.writeByte(mode); - if (mode == CREATE || mode == UPDATE_INFO) { + buf.writeByte(mode.ordinal()); + if (mode == Mode.CREATE || mode == Mode.UPDATE_INFO) { displayName.write(buf); if (version.compareTo(ProtocolVersion.MINECRAFT_1_13) < 0) { prefix.write(buf); @@ -140,7 +134,7 @@ public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersi buf.writeByte(color); } } - if (mode == CREATE || mode == ADD_PLAYER || mode == REMOVE_PLAYER) { + if (mode == Mode.CREATE || mode == Mode.ADD_PLAYER || mode == Mode.REMOVE_PLAYER) { ProtocolUtils.writeVarInt(buf, players.length); for (String player : players) { ProtocolUtils.writeString(buf, player); @@ -161,25 +155,16 @@ public enum NameTagVisibility { NEVER("never"), HIDE_FOR_OTHER_TEAMS("hideForOtherTeams"), HIDE_FOR_OWN_TEAM("hideForOwnTeam"), - // 1.9 (and possibly other versions) appear to treat unknown values differently (always render rather than subject to spectator mode, friendly invisibles, etc). - // we allow the empty value to achieve this in case it is potentially useful even though this is unsupported and its usage may be a bug (#3780). UNKNOWN( "" ); - // + private final String key; - // private static final Map BY_NAME; private static final NameTagVisibility[] BY_ID; static { NameTagVisibility[] values = NameTagVisibility.values(); - ImmutableMap.Builder builder = ImmutableMap.builderWithExpectedSize(values.length); - - for (NameTagVisibility e : values) { - builder.put(e.key, e); - } - - BY_NAME = builder.build(); - BY_ID = Arrays.copyOf( values, values.length - 1 ); // Ignore dummy UNKNOWN value + BY_ID = Arrays.copyOf( values, values.length - 1 ); + BY_NAME = Arrays.stream(values).collect(Collectors.toUnmodifiableMap(e -> e.key, e -> e)); } } @@ -199,17 +184,11 @@ public enum CollisionRule { static { CollisionRule[] values = BY_ID = CollisionRule.values(); - ImmutableMap.Builder builder = ImmutableMap.builderWithExpectedSize( values.length ); - - for (CollisionRule e : values) { - builder.put(e.key, e); - } - - BY_NAME = builder.build(); + BY_NAME = Arrays.stream(values).collect(Collectors.toUnmodifiableMap(e -> e.key, e -> e)); } } - public static T readStringMapKey(ByteBuf buf, Map map) { + public static T readStringToMap(ByteBuf buf, Map map) { String string = ProtocolUtils.readString( buf ); T result = map.get( string ); Preconditions.checkArgument( result != null, "Unknown string key %s", string ); diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/tablist/ExcludedServersTabOverlayProvider.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/tablist/ExcludedServersTabOverlayProvider.java index 7d8b3811..87e85b3d 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/tablist/ExcludedServersTabOverlayProvider.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/tablist/ExcludedServersTabOverlayProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/template/PlayersByServerComponentTemplate.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/template/PlayersByServerComponentTemplate.java index 2c7864db..dfeefae5 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/template/PlayersByServerComponentTemplate.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/template/PlayersByServerComponentTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/updater/UpdateChecker.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/updater/UpdateChecker.java index 43a169b7..3c50c404 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/updater/UpdateChecker.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/updater/UpdateChecker.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,6 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + package codecrafter47.bungeetablistplus.updater; import codecrafter47.bungeetablistplus.util.VelocityPlugin; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/updater/UpdateNotifier.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/updater/UpdateNotifier.java index 27be7390..94553302 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/updater/UpdateNotifier.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/updater/UpdateNotifier.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,6 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + package codecrafter47.bungeetablistplus.updater; import codecrafter47.bungeetablistplus.BungeeTabListPlus; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/BitSet.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/BitSet.java index 6b9fd58a..af1f310c 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/BitSet.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/BitSet.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ColorParser.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ColorParser.java index 20caefca..28a84124 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ColorParser.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ColorParser.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,6 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + package codecrafter47.bungeetablistplus.util; import codecrafter47.bungeetablistplus.util.chat.ChatUtil; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ConcurrentBitSet.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ConcurrentBitSet.java index beea34cd..deb10782 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ConcurrentBitSet.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ConcurrentBitSet.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ContextAwareOrdering.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ContextAwareOrdering.java index 0d8ec2f8..7aa820e0 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ContextAwareOrdering.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ContextAwareOrdering.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/EmptyOrderedPlayerSet.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/EmptyOrderedPlayerSet.java index ad3093b2..450d37ac 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/EmptyOrderedPlayerSet.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/EmptyOrderedPlayerSet.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/EmptyPlayerSet.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/EmptyPlayerSet.java index b004f702..a79d192a 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/EmptyPlayerSet.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/EmptyPlayerSet.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ExceptionHandlingEventExecutor.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ExceptionHandlingEventExecutor.java index 37b895be..86911c77 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ExceptionHandlingEventExecutor.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ExceptionHandlingEventExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/Functions.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/Functions.java index 93f2a68e..28170dcf 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/Functions.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/Functions.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/GeyserCompat.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/GeyserCompat.java index 938a2167..6bd9af97 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/GeyserCompat.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/GeyserCompat.java @@ -1,3 +1,20 @@ +/* + * Copyright (C) 2025 proferabg + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package codecrafter47.bungeetablistplus.util; import org.geysermc.api.connection.Connection; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/IconUtil.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/IconUtil.java index dbe51af9..5ff8f4a8 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/IconUtil.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/IconUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/IntToIntFunction.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/IntToIntFunction.java index b91aa305..8cab35d0 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/IntToIntFunction.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/IntToIntFunction.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/MapFunction.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/MapFunction.java index 7bb68922..9a528f09 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/MapFunction.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/MapFunction.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/MatchingStringsCollection.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/MatchingStringsCollection.java index 63000354..24f779be 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/MatchingStringsCollection.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/MatchingStringsCollection.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/Object2IntHashMultimap.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/Object2IntHashMultimap.java index c251e113..19eb7ad4 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/Object2IntHashMultimap.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/Object2IntHashMultimap.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/Property119Handler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/Property119Handler.java index 41f79b73..1b8e56d0 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/Property119Handler.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/Property119Handler.java @@ -1,3 +1,20 @@ +/* + * Copyright (C) 2025 proferabg + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package codecrafter47.bungeetablistplus.util; import com.velocitypowered.api.util.GameProfile; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ProxyServer.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ProxyServer.java index 22f8d01b..f7f9f6d9 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ProxyServer.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ProxyServer.java @@ -1,3 +1,20 @@ +/* + * Copyright (C) 2025 proferabg + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package codecrafter47.bungeetablistplus.util; public class ProxyServer { diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ReflectionUtil.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ReflectionUtil.java index 617483a1..62cf920d 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ReflectionUtil.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ReflectionUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/VelocityPlugin.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/VelocityPlugin.java index f7483702..d5750034 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/VelocityPlugin.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/VelocityPlugin.java @@ -1,3 +1,20 @@ +/* + * Copyright (C) 2025 proferabg + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package codecrafter47.bungeetablistplus.util; import com.velocitypowered.api.proxy.ProxyServer; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/chat/ChatUtil.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/chat/ChatUtil.java index 22ff7ad6..30324594 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/chat/ChatUtil.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/chat/ChatUtil.java @@ -1,3 +1,20 @@ +/* + * Copyright (C) 2025 proferabg + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package codecrafter47.bungeetablistplus.util.chat; import net.kyori.adventure.text.Component; diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ProtocolVersionProvider.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ProtocolVersionProvider.java index 82afe280..95b03cf6 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ProtocolVersionProvider.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ProtocolVersionProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/version/VelocityProtocolVersionProvider.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/version/VelocityProtocolVersionProvider.java index fc729bb5..b18e5190 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/version/VelocityProtocolVersionProvider.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/version/VelocityProtocolVersionProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ViaVersionProtocolVersionProvider.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ViaVersionProtocolVersionProvider.java index 487b7ab7..6ed57ecc 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ViaVersionProtocolVersionProvider.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ViaVersionProtocolVersionProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/view/PlayersByServerComponentView.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/view/PlayersByServerComponentView.java index 78f77a2f..cdd410e9 100644 --- a/velocity/src/main/java/codecrafter47/bungeetablistplus/view/PlayersByServerComponentView.java +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/view/PlayersByServerComponentView.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Florian Stober + * Copyright (C) 2025 proferabg * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by