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() diff --git a/TabOverlayCommon b/TabOverlayCommon index fb984e1c..46aca5b1 160000 --- a/TabOverlayCommon +++ b/TabOverlayCommon @@ -1 +1 @@ -Subproject commit fb984e1cdc6836d3c07fd8b10043b10e72f866eb +Subproject commit 46aca5b17b3e9ed96a4c118022eb1bf8f1aa3bd6 diff --git a/api-velocity/build.gradle b/api-velocity/build.gradle new file mode 100644 index 00000000..2dfc5608 --- /dev/null +++ b/api-velocity/build.gradle @@ -0,0 +1,25 @@ + +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 { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +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..55f99a0d --- /dev/null +++ b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/BungeeTabListPlusAPI.java @@ -0,0 +1,144 @@ +/* + * 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.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; + +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); + + /** + * 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 + * @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); + + /** + * 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/FakePlayerManager.java b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/FakePlayerManager.java new file mode 100644 index 00000000..2d984261 --- /dev/null +++ b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/FakePlayerManager.java @@ -0,0 +1,64 @@ +/* + * 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.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..34c83a68 --- /dev/null +++ b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/Icon.java @@ -0,0 +1,42 @@ +/* + * 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.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..1cdcbe0b --- /dev/null +++ b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/ServerVariable.java @@ -0,0 +1,65 @@ +/* + * 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.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..b3c4224d --- /dev/null +++ b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/Variable.java @@ -0,0 +1,64 @@ +/* + * 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.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..0d6d802e --- /dev/null +++ b/api-velocity/src/main/java/codecrafter47/bungeetablistplus/api/velocity/tablist/FakePlayer.java @@ -0,0 +1,104 @@ +/* + * 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.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-bukkit/build.gradle b/bootstrap-bukkit/build.gradle index 0cfd9016..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 = '1.8' - targetCompatibility = '1.8' -} - shadowJar { archiveClassifier.set(null) } diff --git a/bootstrap-bungee/build.gradle b/bootstrap-bungee/build.gradle index 93d2b739..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 = '1.8' - targetCompatibility = '1.8' -} - 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 new file mode 100644 index 00000000..c1124e4a --- /dev/null +++ b/bootstrap-velocity/build.gradle @@ -0,0 +1,40 @@ +import org.apache.tools.ant.filters.ReplaceTokens + +plugins { + id "org.jetbrains.gradle.plugin.idea-ext" version "1.1.10" + id "com.github.johnrengelman.shadow" version "7.1.2" +} + +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" +} + +task processSource(type: Sync) { + from sourceSets.main.java + inputs.property 'version', version + filter(ReplaceTokens, tokens: [VERSION: version]) + into "$buildDir/src" +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +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..2def5d8b --- /dev/null +++ b/bootstrap-velocity/src/main/java/codecrafter47/bungeetablistplus/BootstrapPlugin.java @@ -0,0 +1,108 @@ +/* + * 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; + +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 lombok.Getter; +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", + 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 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 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")) < 61.0) { + getLogger().error("§cBungeeTabListPlus requires Java 17 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(NO_RELOAD_PLAYERS)); + } + } + BungeeTabListPlus.getInstance(this).onLoad(); + BungeeTabListPlus.getInstance(this).onEnable(); + // Metrics + metricsFactory.make(this, 24808); + } + + @Subscribe + public void onProxyShutdown(final ProxyShutdownEvent event) { + BungeeTabListPlus.getInstance().onDisable(); + 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()) { + proxiedPlayer.disconnect(Component.text(NO_RELOAD_PLAYERS)); + } + } + } + } +} diff --git a/build.gradle b/build.gradle index 9f7c61da..77253c38 100644 --- a/build.gradle +++ b/build.gradle @@ -11,6 +11,8 @@ buildscript { 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' } @@ -47,6 +49,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' } @@ -58,20 +64,24 @@ 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 = '1.8' - targetCompatibility = '1.8' + 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/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/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 f59fb17d..ef372f5e 160000 --- a/minecraft-data-api +++ b/minecraft-data-api @@ -1 +1 @@ -Subproject commit f59fb17dc3ae1d6b1234aadff2d9546f5119a936 +Subproject commit ef372f5e360b30712ea8b7da0c20f62b05c595f8 diff --git a/settings.gradle b/settings.gradle index 2f491e22..d9498f61 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,12 +13,15 @@ rootProject.name = 'BungeeTabListPlus-parent' include(':bungee-plugin') include(':waterfall-compat') include(':bukkit-plugin') +include(':velocity-plugin') include(':bungeetablistplus-common') include(':BungeeTabListPlus') +include(':BungeeTabListPlus-Velocity') include(':BungeeTabListPlus_BukkitBridge') include(':bungeetablistplus-api-bukkit') include(':bungeetablistplus-api-bungee') include(':bungeetablistplus-api-sponge') +include(':bungeetablistplus-api-velocity') include(':BungeeTabListPlus_SpongeBridge') include(':example:example-bungee-api') include(':example:example-bukkit-api') @@ -27,12 +30,15 @@ include(':bungeetablistplus-bridge') project(':bungee-plugin').projectDir = file('bungee') project(':waterfall-compat').projectDir = file('waterfall_compat') project(':bukkit-plugin').projectDir = file('bukkit') +project(':velocity-plugin').projectDir = file('velocity') project(':bungeetablistplus-common').projectDir = file('common') project(':BungeeTabListPlus').projectDir = file('bootstrap-bungee') +project(':BungeeTabListPlus-Velocity').projectDir = file('bootstrap-velocity') project(':BungeeTabListPlus_BukkitBridge').projectDir = file('bootstrap-bukkit') project(':bungeetablistplus-api-bukkit').projectDir = file('api-bukkit') project(':bungeetablistplus-api-bungee').projectDir = file('api-bungee') project(':bungeetablistplus-api-sponge').projectDir = file('api-sponge') +project(':bungeetablistplus-api-velocity').projectDir = file('api-velocity') project(':BungeeTabListPlus_SpongeBridge').projectDir = file('sponge') project(':example:example-bungee-api').projectDir = file('example/bungee') project(':example:example-bukkit-api').projectDir = file('example/bukkit') 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 new file mode 100644 index 00000000..3d923b79 --- /dev/null +++ b/velocity/build.gradle @@ -0,0 +1,56 @@ + +repositories { + maven { + url = "https://repo.viaversion.com" + } + maven { + url = "https://repo.opencollab.dev/main/" + } +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +dependencies { + implementation "de.codecrafter47.data:api:${rootProject.ext.dataApiVersion}" + implementation "de.codecrafter47.data.bukkit:api:${rootProject.ext.dataApiVersion}" + implementation "de.codecrafter47.data.sponge:api:${rootProject.ext.dataApiVersion}" + implementation "de.codecrafter47.data:minecraft:${rootProject.ext.dataApiVersion}" + implementation "de.codecrafter47.data.velocity:api:${rootProject.ext.dataApiVersion}" + implementation "de.codecrafter47.data:velocity:${rootProject.ext.dataApiVersion}" + implementation project(':bungeetablistplus-common') + implementation project(':bungeetablistplus-api-velocity') + implementation 'it.unimi.dsi:fastutil:8.5.11' + implementation "de.codecrafter47.taboverlay:taboverlaycommon-config:1.0-SNAPSHOT" + implementation 'org.yaml:snakeyaml:1.33' + testImplementation 'junit:junit:4.13.2' + 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' + 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" + 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' +} + +processResources { + filesMatching("version.properties") { + expand(project.properties) + } +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/BTLPContextKeys.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/BTLPContextKeys.java new file mode 100644 index 00000000..7239cda5 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/BTLPContextKeys.java @@ -0,0 +1,27 @@ +/* + * 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; + +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..79bc988e --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/BungeeTabListPlus.java @@ -0,0 +1,569 @@ +/* + * 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; + +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.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; +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; + @Getter + 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 { + plugin.getProxy().isShuttingDown(); + } catch (NoSuchMethodError ex) { + throw new RuntimeException("You need to run at least Velocity version #464"); + } + + 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(); + plugin.getProxy().getScheduler().tasksByPlugin(plugin).forEach(ScheduledTask::cancel); + mainThreadExecutor.shutdownGracefully(); + asyncExecutor.shutdownGracefully(); + } + + /** + * 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 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..f8c4deec --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/bridge/BukkitBridge.java @@ -0,0 +1,716 @@ +/* + * 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.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/bridge/NetDataKeyIdMap.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/bridge/NetDataKeyIdMap.java new file mode 100644 index 00000000..4088d719 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/bridge/NetDataKeyIdMap.java @@ -0,0 +1,47 @@ +/* + * 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.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..1c76534e --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/cache/Cache.java @@ -0,0 +1,109 @@ +/* + * 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.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..78dd98aa --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandBungeeTabListPlus.java @@ -0,0 +1,169 @@ +/* + * 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.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.chat.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..2ce01dc9 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandDebug.java @@ -0,0 +1,123 @@ +/* + * 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.command; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.data.BTLPVelocityDataKeys; +import codecrafter47.bungeetablistplus.player.VelocityPlayer; +import codecrafter47.bungeetablistplus.util.ProxyServer; +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; +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.getVelocityPlayerProvider().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<>(); + MinecraftConnection connection = ((ConnectedPlayer) player).getConnection(); + for (Map.Entry entry : connection.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..e8c9bef7 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandFakePlayers.java @@ -0,0 +1,117 @@ +/* + * 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.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.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.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..8f51b379 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/command/CommandHide.java @@ -0,0 +1,97 @@ +/* + * 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.command; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.data.BTLPVelocityDataKeys; +import codecrafter47.bungeetablistplus.player.VelocityPlayer; +import codecrafter47.bungeetablistplus.util.chat.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().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().getVelocityPlayerProvider().getPlayer(player); + velocityPlayer.getLocalDataCache().updateValue(BTLPVelocityDataKeys.DATA_KEY_IS_HIDDEN_PLAYER_COMMAND, true); + } + + private static void unhidePlayer(Player 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/config/Comment.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/config/Comment.java new file mode 100644 index 00000000..19af2a88 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/config/Comment.java @@ -0,0 +1,30 @@ +/* + * 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.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..2e753135 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/config/MainConfig.java @@ -0,0 +1,204 @@ +/* + * 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.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..42fe46fe --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/config/Path.java @@ -0,0 +1,30 @@ +/* + * 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.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..53df0979 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/config/PlayersByServerComponentConfiguration.java @@ -0,0 +1,298 @@ +/* + * 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.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..1fcfb4de --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/AbstractCompositeDataProvider.java @@ -0,0 +1,83 @@ +/* + * 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; +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..31a84dc7 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/BTLPDataTypes.java @@ -0,0 +1,59 @@ +/* + * 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.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..8ce55de2 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/BTLPVelocityDataKeys.java @@ -0,0 +1,63 @@ +/* + * 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 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..3429e93a --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/NullDataHolder.java @@ -0,0 +1,45 @@ +/* + * 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 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..19e9a732 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/PermissionDataProvider.java @@ -0,0 +1,49 @@ +/* + * 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; +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..d1fcbf16 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/ServerDataHolder.java @@ -0,0 +1,67 @@ +/* + * 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 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..75402eea --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/data/TrackingDataCache.java @@ -0,0 +1,54 @@ +/* + * 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 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..150565d9 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractLegacyTabOverlayHandler.java @@ -0,0 +1,841 @@ +/* + * 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.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.api.network.ProtocolVersion; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +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; +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(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 { + serverPlayerList.put(getName(item), item.getLatency()); + } + } + } else { + for (LegacyPlayerListItemPacket.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(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 { + serverPlayerList.put(getName(entry), entry.getLatency()); + } + } + } + return activeHandler.onPlayerListUpdatePacket(packet); + } + + @Override + public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfoPacket 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(HeaderAndFooterPacket 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) { + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(uuid); + item.setName(player); + item.setDisplayName(GsonComponentSerializer.gson().deserialize(player)); + item.setLatency(9999); + LegacyPlayerListItemPacket pli = new LegacyPlayerListItemPacket(LegacyPlayerListItemPacket.REMOVE_PLAYER, List.of(item)); + sendPacket(pli); + } + + private abstract static class AbstractContentOperationModeHandler extends OperationModeHandler { + + abstract PacketListenerResult onPlayerListPacket(LegacyPlayerListItemPacket packet); + + abstract void onServerSwitch(); + + abstract void update(); + + final void invalidate() { + getTabOverlay().invalidate(); + onDeactivated(); + } + + abstract void onDeactivated(); + + abstract void onActivated(); + + public abstract PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfoPacket packet); + + public abstract PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfoPacket 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(LegacyPlayerListItemPacket packet) { + return PacketListenerResult.PASS; + } + + @Override + public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfoPacket packet) { + return PacketListenerResult.PASS; + } + + @Override + public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfoPacket packet) { + return PacketListenerResult.PASS; + } + + @Override + void onActivated() { + for (val entry : serverPlayerList.object2IntEntrySet()) { + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(); + item.setDisplayName(GsonComponentSerializer.gson().deserialize(entry.getKey())); // TODO: Check Formatting + item.setLatency(entry.getIntValue()); + LegacyPlayerListItemPacket pli = new LegacyPlayerListItemPacket(LegacyPlayerListItemPacket.ADD_PLAYER, List.of(item)); + sendPacket(pli); + } + for (val entry : modernServerPlayerList.entrySet()) { + 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); + LegacyPlayerListItemPacket pli = new LegacyPlayerListItemPacket(LegacyPlayerListItemPacket.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(LegacyPlayerListItemPacket packet) { + return PacketListenerResult.CANCEL; + } + + @Override + public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfoPacket packet) { + return PacketListenerResult.CANCEL; + } + + @Override + public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfoPacket 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(Team.Mode.REMOVE); + 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(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])); + t.setPlayers(new String[]{slotID[index]}); + t.setNameTagVisibility(Team.NameTagVisibility.ALWAYS); + t.setCollisionRule(Team.CollisionRule.ALWAYS); + 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(Team.Mode.REMOVE); + sendPacket(t); + } + } + this.size = size; + } + } + + private void updateSlot(CustomTabOverlay tabOverlay, int 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]); + LegacyPlayerListItemPacket pli = new LegacyPlayerListItemPacket(LegacyPlayerListItemPacket.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(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])); + packet.setNameTagVisibility(Team.NameTagVisibility.ALWAYS); + packet.setCollisionRule(Team.CollisionRule.ALWAYS); + 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 LegacyPlayerListItemPacket.Item}. + * + * @param item the item + * @return the name + */ + private static String getName(LegacyPlayerListItemPacket.Item item) { + if (item.getDisplayName() != null) { + return LegacyComponentSerializer.legacySection().serialize(item.getDisplayName()); + } else if (item.getName() != null) { + return item.getName(); + } else { + throw new AssertionError("DisplayName and Username are null"); + } + } + + private static String getName(UpsertPlayerInfoPacket.Entry entry) { + if (entry.getDisplayName() != null) { + return entry.getDisplayName().getJson(); + } 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 new file mode 100644 index 00000000..b878b23c --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/AbstractTabOverlayHandler.java @@ -0,0 +1,2501 @@ +/* + * 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.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.api.network.ProtocolVersion; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +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; +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.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 { + + // 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 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; + 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 { + // 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 Component serverHeader = null; + @Nullable + protected Component 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 final AtomicBoolean updateScheduledFlag = new AtomicBoolean(false); + private final Runnable updateTask = this::update; + + 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, 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(LegacyPlayerListItemPacket packet) { + switch (packet.getAction()) { + case ADD_PLAYER: + 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()); + } + } + 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 (LegacyPlayerListItemPacket.Item item : packet.getItems()) { + PlayerListEntry playerListEntry = serverPlayerList.get(item.getUuid()); + if (playerListEntry != null) { + playerListEntry.setGamemode(item.getGameMode()); + } + } + break; + case UPDATE_LATENCY: + for (LegacyPlayerListItemPacket.Item item : packet.getItems()) { + PlayerListEntry playerListEntry = serverPlayerList.get(item.getUuid()); + if (playerListEntry != null) { + playerListEntry.setPing(item.getLatency()); + } + } + break; + case UPDATE_DISPLAY_NAME: + for (LegacyPlayerListItemPacket.Item item : packet.getItems()) { + PlayerListEntry playerListEntry = serverPlayerList.get(item.getUuid()); + if (playerListEntry != null) { + playerListEntry.setDisplayName(item.getDisplayName()); + } + } + break; + case REMOVE_PLAYER: + for (LegacyPlayerListItemPacket.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.isEmpty()) { + block = true; + break; + } + } + 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 (Team.Mode.REMOVE.equals(packet.getMode())) { + 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 (Team.Mode.CREATE.equals(packet.getMode())) { + teamEntry = new TeamEntry(); + serverTeams.put(packet.getName(), teamEntry); + } else { + teamEntry = serverTeams.get(packet.getName()); + } + + if (teamEntry != null) { + 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()); + teamEntry.setFriendlyFire(packet.getFriendlyFire()); + teamEntry.setNameTagVisibility(packet.getNameTagVisibility()); + teamEntry.setCollisionRule(packet.getCollisionRule()); + teamEntry.setColor(packet.getColor()); + } + if (packet.getPlayers() != null) { + for (String s : packet.getPlayers()) { + 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) + 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(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().getComponent() : Component.empty(); + this.serverFooter = packet.getFooter() != null ? packet.getFooter().getComponent() : Component.empty(); + + 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; + } + + 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()){ + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(uuid); + items.add(item); + } + LegacyPlayerListItemPacket packet = new LegacyPlayerListItemPacket(REMOVE_PLAYER, items); + sendPacket(packet); + } + + serverPlayerList.clear(); + if (serverHeader != null) { + serverHeader = Component.empty(); + } + if (serverFooter != null) { + serverFooter = Component.empty(); + } + + 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 LegacyPlayerListItemPacket} packet. + *

+ * This method is called after this {@link AbstractTabOverlayHandler} has updated the {@code serverPlayerList}. + */ + abstract PacketListenerResult onPlayerListPacket(LegacyPlayerListItemPacket 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 HeaderAndFooterPacket} packet. + *

+ * This method is called before this {@link AbstractTabOverlayHandler} 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 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(LegacyPlayerListItemPacket packet) { + return PacketListenerResult.PASS; + } + + @Override + void onTeamPacketPreprocess(Team packet) { + // nothing to do + } + + @Override + PacketListenerResult onTeamPacket(Team packet) { + return PacketListenerResult.PASS; + } + + @Override + void onServerSwitch() { + sendPacket(HeaderAndFooterPacket.create(Component.empty(), Component.empty(), getProtocol())); + } + + @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 + LegacyPlayerListItemPacket packet; + List items = new ArrayList<>(serverPlayerList.size()); + for (PlayerListEntry entry : serverPlayerList.values()) { + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(entry.getUuid()); + item.setLatency(entry.getPing()); + items.add(item); + } + packet = new LegacyPlayerListItemPacket(UPDATE_LATENCY, items); + sendPacket(packet); + + // restore player gamemode + items.clear(); + for (PlayerListEntry entry : serverPlayerList.values()) { + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(entry.getUuid()); + item.setGameMode(entry.getGamemode()); + items.add(item); + } + packet = new LegacyPlayerListItemPacket(UPDATE_GAMEMODE, items); + sendPacket(packet); + + // restore player display name + items.clear(); + for (PlayerListEntry entry : serverPlayerList.values()) { + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(entry.getUuid()); + item.setDisplayName(entry.getDisplayName()); + items.add(item); + } + packet = new LegacyPlayerListItemPacket(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(HeaderAndFooterPacket packet) { + return PacketListenerResult.PASS; + } + + @Override + void onServerSwitch() { + sendPacket(HeaderAndFooterPacket.create(Component.empty(), Component.empty(), getProtocol())); + } + + @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(HeaderAndFooterPacket.create(serverHeader != null ? serverHeader : Component.empty(), serverFooter != null ? serverFooter : Component.empty(), getProtocol())); + } + } + + 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(LegacyPlayerListItemPacket 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 (LegacyPlayerListItemPacket.Item item : items) { + if (!viewerUuid.equals(item.getUuid())) { + item.setGameMode(0); + } + } + + for (LegacyPlayerListItemPacket.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]); + 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 (LegacyPlayerListItemPacket.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 (LegacyPlayerListItemPacket.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 (LegacyPlayerListItemPacket.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 + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[index], EMPTY_COMPONENT, EMPTY_COMPONENT, EMPTY_COMPONENT, Team.NameTagVisibility.ALWAYS, Team.CollisionRule.ALWAYS, is13OrLater ? 21 : 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; + LegacyPlayerListItemPacket.Item item1 = new LegacyPlayerListItemPacket.Item(customSlotUuid); + item1.setName(slotUsername[index] = getCustomSlotUsername(index)); + Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); + item1.setDisplayName(tabOverlay.text[index]); + item1.setLatency(tabOverlay.ping[index]); + item1.setGameMode(0); + LegacyPlayerListItemPacket packet1 = new LegacyPlayerListItemPacket(ADD_PLAYER, List.of(item1)); + sendPacket(packet1); + if (is18) { + packet1 = new LegacyPlayerListItemPacket(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 (Team.Mode.REMOVE.equals(packet.getMode())) { + 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 + sendPacket(createPacketTeamUpdate(CUSTOM_SLOT_TEAMNAME[slot], EMPTY_COMPONENT, EMPTY_COMPONENT, EMPTY_COMPONENT, Team.NameTagVisibility.ALWAYS, Team.CollisionRule.ALWAYS, is13OrLater ? 21 : 0, (byte) 1)); + } + } + } + } + } else { + if (Team.Mode.REMOVE.equals(packet.getMode())) { + 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 CREATE: + case ADD_PLAYER: + 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 REMOVE_PLAYER: + 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 + 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); + } + break; + case UPDATE_INFO: + 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 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(); + 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 REMOVE_PLAYER: + 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 + 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 + 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; + LegacyPlayerListItemPacket.Item item1 = new LegacyPlayerListItemPacket.Item(customSlotUuid); + item1.setName(slotUsername[index] = getCustomSlotUsername(index)); + Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); + item1.setDisplayName(tabOverlay.text[index]); + item1.setLatency(tabOverlay.ping[index]); + item1.setGameMode(0); + LegacyPlayerListItemPacket packet1 = new LegacyPlayerListItemPacket(ADD_PLAYER, List.of(item1)); + sendPacket(packet1); + if (is18) { + packet1 = new LegacyPlayerListItemPacket(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) { + 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) { + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(mEntry.getKey()); + item.setGameMode(0); + items[index++] = item; + } + } + + LegacyPlayerListItemPacket packet = new LegacyPlayerListItemPacket(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; + + 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++) { + 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]})); + } + 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]})); + } + } + + @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 + 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++; + } + } + } + + 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) { + 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) { + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(slotUuid[index]); + items[i++] = item; + } + } + if (experimentalTabCompleteFixForTabSize80 && using80Slots) { + for (int j = 0; j < 17; j++) { + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(CUSTOM_SLOT_UUID_SPACER[j]); + items[i++] = item; + } + } + LegacyPlayerListItemPacket packet = new LegacyPlayerListItemPacket(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) { + 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) { + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(mEntry.getKey()); + item.setGameMode(0); + items[index++] = item; + } + } + + LegacyPlayerListItemPacket packet = new LegacyPlayerListItemPacket(UPDATE_GAMEMODE, Arrays.asList(items)); + sendPacket(packet); + } + + // remove spacer slots + if (experimentalTabCompleteFixForTabSize80) { + for (int i = 0; i < 17; i++) { + LegacyPlayerListItemPacket.Item item1 = new LegacyPlayerListItemPacket.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 + 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]})); + } + + // 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; + LegacyPlayerListItemPacket.Item item1 = new LegacyPlayerListItemPacket.Item(customSlotUuid); + item1.setName(slotUsername[index] = getCustomSlotUsername(index)); + Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); + item1.setDisplayName(tabOverlay.text[index]); + item1.setLatency(tabOverlay.ping[index]); + item1.setGameMode(0); + itemQueueAddPlayer.add(item1); + } else { + // custom + if (slotState[index] == SlotState.CUSTOM) { + LegacyPlayerListItemPacket.Item item1 = new LegacyPlayerListItemPacket.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; + LegacyPlayerListItemPacket.Item item1 = new LegacyPlayerListItemPacket.Item(customSlotUuid); + item1.setName(slotUsername[index] = getCustomSlotUsername(index)); + Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); + item1.setDisplayName(tabOverlay.text[index]); + item1.setLatency(tabOverlay.ping[index]); + item1.setGameMode(0); + itemQueueAddPlayer.add(item1); + } + } + + // restore player gamemode + LegacyPlayerListItemPacket packet; + List items = new ArrayList<>(serverPlayerList.size()); + for (PlayerListEntry entry : serverPlayerList.values()) { + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(entry.getUuid()); + item.setGameMode(entry.getGamemode()); + items.add(item); + } + packet = new LegacyPlayerListItemPacket(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++) { + LegacyPlayerListItemPacket.Item item1 = new LegacyPlayerListItemPacket.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 + 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 + 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 + 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 + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.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 + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.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 + LegacyPlayerListItemPacket.Item itemUpdateDisplayName = new LegacyPlayerListItemPacket.Item(viewerUuid); + tabOverlay.dirtyFlagsText.clear(highestUsedSlotIndex); + itemUpdateDisplayName.setDisplayName(tabOverlay.text[highestUsedSlotIndex]); + itemQueueUpdateDisplayName.add(itemUpdateDisplayName); + // 5. Update ping + LegacyPlayerListItemPacket.Item itemUpdatePing = new LegacyPlayerListItemPacket.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 + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.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 + LegacyPlayerListItemPacket.Item itemUpdateDisplayName = new LegacyPlayerListItemPacket.Item(uuid); + tabOverlay.dirtyFlagsText.clear(index); + itemUpdateDisplayName.setDisplayName(tabOverlay.text[index]); + itemQueueUpdateDisplayName.add(itemUpdateDisplayName); + // 5. Update ping + LegacyPlayerListItemPacket.Item itemUpdatePing = new LegacyPlayerListItemPacket.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 + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.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 + LegacyPlayerListItemPacket.Item itemUpdateDisplayName = new LegacyPlayerListItemPacket.Item(uuid); + tabOverlay.dirtyFlagsText.clear(index); + itemUpdateDisplayName.setDisplayName(tabOverlay.text[index]); + itemQueueUpdateDisplayName.add(itemUpdateDisplayName); + // 5. Update ping + LegacyPlayerListItemPacket.Item itemUpdatePing = new LegacyPlayerListItemPacket.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) { + LegacyPlayerListItemPacket.Item item1 = new LegacyPlayerListItemPacket.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; + LegacyPlayerListItemPacket.Item item1 = new LegacyPlayerListItemPacket.Item(customSlotUuid); + item1.setName(slotUsername[index] = getCustomSlotUsername(index)); + Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); + item1.setDisplayName(tabOverlay.text[index]); + 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) { + 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; + LegacyPlayerListItemPacket.Item item1 = new LegacyPlayerListItemPacket.Item(customSlotUuid); + item1.setName(slotUsername[index] = getCustomSlotUsername(index)); + Property119Handler.setProperties(item1, toPropertiesArray(icon.getTextureProperty())); + item1.setDisplayName(tabOverlay.text[index]); + 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) { + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(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) { + LegacyPlayerListItemPacket.Item item = new LegacyPlayerListItemPacket.Item(slotUuid[index]); + item.setLatency(tabOverlay.ping[index]); + itemQueueUpdatePing.add(item); + } + } + + dirtySlots.clear(); + + // send packets + sendQueuedItems(); + } + + private void sendQueuedItems() { + if (!itemQueueRemovePlayer.isEmpty()) { + LegacyPlayerListItemPacket packet = new LegacyPlayerListItemPacket(REMOVE_PLAYER, itemQueueRemovePlayer); + sendPacket(packet); + itemQueueRemovePlayer.clear(); + } + if (!itemQueueAddPlayer.isEmpty()) { + LegacyPlayerListItemPacket packet = new LegacyPlayerListItemPacket(ADD_PLAYER, itemQueueAddPlayer); + sendPacket(packet); + if (is18) { + packet = new LegacyPlayerListItemPacket(UPDATE_DISPLAY_NAME, itemQueueAddPlayer); + sendPacket(packet); + } + itemQueueAddPlayer.clear(); + } + if (!itemQueueUpdateDisplayName.isEmpty()) { + LegacyPlayerListItemPacket packet = new LegacyPlayerListItemPacket(UPDATE_DISPLAY_NAME, itemQueueUpdateDisplayName); + sendPacket(packet); + itemQueueUpdateDisplayName.clear(); + } + if (!itemQueueUpdatePing.isEmpty()) { + LegacyPlayerListItemPacket packet = new LegacyPlayerListItemPacket(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 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(); + 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(HeaderAndFooterPacket packet) { + return PacketListenerResult.CANCEL; + } + + @Override + void onServerSwitch() { + // do nothing + } + + @Override + void onDeactivated() { + //do nothing + } + + @Override + void onActivated(AbstractHeaderFooterOperationModeHandler previous) { + // remove header/ footer + sendPacket(HeaderAndFooterPacket.create(Component.empty(), Component.empty(), getProtocol())); + } + + @Override + void update() { + CustomHeaderAndFooterImpl tabOverlay = getTabOverlay(); + if (tabOverlay.headerOrFooterDirty) { + tabOverlay.headerOrFooterDirty = false; + sendPacket(HeaderAndFooterPacket.create(tabOverlay.header, tabOverlay.footer, getProtocol())); + } + } + } + + private final class CustomHeaderAndFooterImpl extends AbstractHeaderFooterTabOverlay implements HeaderAndFooterHandle { + private Component header = Component.empty(); + private Component footer = Component.empty(); + + 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 = 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 = GsonComponentSerializer.gson().deserialize(ChatFormat.formattedTextToJson(header)); + headerOrFooterDirty = true; + scheduleUpdateIfNotInBatch(); + } + + @Override + public void setFooter(@Nullable String footer) { + this.footer = GsonComponentSerializer.gson().deserialize(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, 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(Team.Mode.CREATE); + 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 static Team createPacketTeamRemove(String name) { + Team team = new Team(); + team.setName(name); + 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(Team.Mode.UPDATE_INFO); + team.setDisplayName(displayName); + team.setPrefix(prefix); + team.setSuffix(suffix); + team.setNameTagVisibility(nameTagVisibility); + 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(Team.Mode.ADD_PLAYER); + team.setPlayers(players); + return team; + } + + private static Team createPacketTeamRemovePlayers(String name, String[] players) { + Team team = new Team(); + team.setName(name); + team.setMode(Team.Mode.REMOVE_PLAYER); + 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(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 ComponentHolder displayName; + private ComponentHolder prefix; + private ComponentHolder suffix; + private byte friendlyFire; + private Team.NameTagVisibility nameTagVisibility; + private Team.CollisionRule collisionRule; + private int color; + private Set players = new ObjectOpenHashSet<>(); + + void addPlayer(String name) { + players.add(name); + } + + void removePlayer(String name) { + players.remove(name); + } + } +} 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..9db5fa3e --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/GetGamemodeLogic.java @@ -0,0 +1,75 @@ +/* + * 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.handler; + +import codecrafter47.bungeetablistplus.protocol.AbstractPacketHandler; +import codecrafter47.bungeetablistplus.protocol.PacketHandler; +import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; +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; +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(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()); + } + } + } + return super.onPlayerListPacket(packet); + } + + @Override + 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()); + } + } + } + return super.onPlayerListUpdatePacket(packet); + } + + @Override + public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfoPacket 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..0974b32f --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LegacyTabOverlayHandlerImpl.java @@ -0,0 +1,53 @@ +/* + * 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.handler; + +import codecrafter47.bungeetablistplus.protocol.PacketListener; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket; +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 LegacyPlayerListItemPacket) && (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 { + PacketListener.sendPacket(player, 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..bec680dd --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/LowMemoryTabOverlayHandlerImpl.java @@ -0,0 +1,53 @@ +/* + * 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.handler; + +import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; +import codecrafter47.bungeetablistplus.protocol.Team; +import com.velocitypowered.api.proxy.Player; + +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, boolean has1203OrLater) { + super(logger, eventLoopExecutor, viewerUuid, player, is18, has113OrLater, has119OrLater, has1203OrLater); + } + + @Override + public PacketListenerResult onTeamPacket(Team packet) { + if (super.onTeamPacket(packet) != PacketListenerResult.CANCEL) { + sendPacket(packet); + } + return PacketListenerResult.CANCEL; + } + + @Override + public void onServerSwitch(boolean is13OrLater) { + 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..34507198 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/NewTabOverlayHandler.java @@ -0,0 +1,1269 @@ +/* + * 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.handler; + +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 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.*; +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; + +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 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; + 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 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 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(); + EMPTY_COMPONENT = new ComponentHolder(player.getProtocolVersion(), Component.empty()); + } + + @SneakyThrows + private void sendPacket(MinecraftPacket packet) { + if (((packet instanceof UpsertPlayerInfoPacket) || (packet instanceof RemovePlayerInfoPacket)) && (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 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 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(UpsertPlayerInfoPacket.Action.UPDATE_LISTED)) { + for (UpsertPlayerInfoPacket.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(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) { + + hasCreatedCustomTeams = false; + + 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 NewTabOverlayHandler} has updated the {@code serverPlayerList}. + */ + abstract PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfoPacket 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 HeaderAndFooterPacket} packet. + *

+ * This method is called before this {@link NewTabOverlayHandler} 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 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(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; + /** + * 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(UpsertPlayerInfoPacket packet) { + + if (packet.getActions().contains(UpsertPlayerInfoPacket.Action.UPDATE_LISTED)) { + for (UpsertPlayerInfoPacket.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() { + 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); + } + + 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_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]})); + } + } + } + + @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() { + + createTeamsIfNecessary(); + + 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, slotUsername[index] = getCustomSlotUsername(index), 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); + 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)); + 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 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 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 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(Team.Mode.CREATE); + 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..36db02b1 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/OperationModeHandler.java @@ -0,0 +1,32 @@ +/* + * 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.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/OrderedTabOverlayHandler.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/OrderedTabOverlayHandler.java new file mode 100644 index 00000000..b37d0bc4 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/OrderedTabOverlayHandler.java @@ -0,0 +1,1202 @@ +/* + * 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.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/RewriteLogic.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/RewriteLogic.java new file mode 100644 index 00000000..2446d66f --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/RewriteLogic.java @@ -0,0 +1,162 @@ +/* + * 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.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.LegacyPlayerListItemPacket; +import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfoPacket; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket; + +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(LegacyPlayerListItemPacket packet) { + + 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) { + rewriteMap.put(uuid, player.getUniqueId()); + } + } + } + + boolean modified = false; + + if (packet.getAction() == LegacyPlayerListItemPacket.REMOVE_PLAYER) { + ListIterator it = packet.getItems().listIterator(); + while(it.hasNext()){ + 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(); + while(it.hasNext()){ + LegacyPlayerListItemPacket.Item item = it.next(); + UUID uuid = rewriteMap.get(item.getUuid()); + if (uuid != null) { + modified = true; + if (packet.getAction() == LegacyPlayerListItemPacket.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(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) { + 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()){ + UpsertPlayerInfoPacket.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(RemovePlayerInfoPacket 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 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()); + newItem.setDisplayName(item.getDisplayName()); + newItem.setPlayerKey(item.getPlayerKey()); + newItem.setProperties(item.getProperties()); + return newItem; + } + + 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()); + 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..838b1a6c --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/handler/TabOverlayHandlerImpl.java @@ -0,0 +1,90 @@ +/* + * 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.handler; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.protocol.PacketListener; +import codecrafter47.bungeetablistplus.protocol.PacketListenerResult; +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfoPacket; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket; +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, boolean is1203OrLater) { + super(logger, eventLoopExecutor, viewerUuid, is18, is13OrLater, is119OrLater, is1203OrLater); + this.player = player; + } + + @SneakyThrows + @Override + protected void sendPacket(MinecraftPacket packet) { + if ((packet instanceof UpsertPlayerInfoPacket) && (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 { + PacketListener.sendPacket(player, packet); + } + } + + @Override + protected ProtocolVersion getProtocol(){ + return player.getProtocolVersion(); + } + + @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(UpsertPlayerInfoPacket packet) { + return PacketListenerResult.PASS; + } + + @Override + public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfoPacket 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..b61e5bde --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/listener/TabListListener.java @@ -0,0 +1,100 @@ +/* + * 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.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/API.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/API.java new file mode 100644 index 00000000..839309ae --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/API.java @@ -0,0 +1,148 @@ +/* + * 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.managers; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +import codecrafter47.bungeetablistplus.api.velocity.BungeeTabListPlusAPI; +import codecrafter47.bungeetablistplus.api.velocity.FakePlayerManager; +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.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.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 + protected TabView getTabViewForPlayer0(Player player) { + TabView tabView = tabViewManager.getTabView(player); + if (tabView == null) { + throw new IllegalStateException("unknown player"); + } + return tabView; + } + + @Nonnull + @Override + protected de.codecrafter47.taboverlay.Icon getPlayerIcon0(Player player) { + return IconUtil.getIconFromPlayer(player); + } + + @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 + 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().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 new file mode 100644 index 00000000..a2690e24 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/DataManager.java @@ -0,0 +1,200 @@ +/* + * 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.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 VelocityPlayerProvider velocityPlayerProvider; + 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, VelocityPlayerProvider velocityPlayerProvider, EventExecutor mainThreadExecutor, ServerStateManager serverStateManager, BukkitBridge bukkitBridge) { + this.api = api; + this.velocityPlayerProvider = velocityPlayerProvider; + 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 : velocityPlayerProvider.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().getServerId()); + } + } + } + + 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..3e8ddff9 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/HiddenPlayersManager.java @@ -0,0 +1,95 @@ +/* + * 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.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..b4d80470 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/RedisPlayerManager.java @@ -0,0 +1,302 @@ +/* + * 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.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 VelocityPlayerProvider velocityPlayerProvider; + 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(VelocityPlayerProvider velocityPlayerProvider, BungeeTabListPlus plugin, Logger logger) { + this.velocityPlayerProvider = velocityPlayerProvider; + 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(plugin, this::updatePlayers).delay(5, TimeUnit.SECONDS).repeat(5, TimeUnit.SECONDS).schedule(); + + plugin.getProxy().getEventManager().register(plugin, 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 = velocityPlayerProvider.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..bb2f9b4c --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/ServerStateManager.java @@ -0,0 +1,138 @@ +/* + * 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.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, 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 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(VelocityPlugin plugin, RegisteredServer server) { + this.plugin = plugin; + this.server = server; + } + + public boolean isOnline() { + return online; + } + + public int getMaxPlayers() { + return maxPlayers; + } + + public int getOnlinePlayers() { + return onlinePlayers; + } + + @Override + public void run() { + if (plugin.getProxy().isShuttingDown()) 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..d4702a51 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/TabViewManager.java @@ -0,0 +1,171 @@ +/* + * 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.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.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; +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(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())) { + 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); + + } 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) { + 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); + + } + + 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/managers/VelocityPlayerProvider.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/VelocityPlayerProvider.java new file mode 100644 index 00000000..e9c2f849 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/managers/VelocityPlayerProvider.java @@ -0,0 +1,108 @@ +/* + * 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.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 VelocityPlayerProvider implements PlayerProvider { + + private final EventExecutor mainThread; + + private final Map players = new ConcurrentHashMap<>(); + private final Set listeners = new ReferenceOpenHashSet<>(); + + public VelocityPlayerProvider(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/placeholder/ComponentServerPlaceholderResolver.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/ComponentServerPlaceholderResolver.java new file mode 100644 index 00000000..90287b87 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/ComponentServerPlaceholderResolver.java @@ -0,0 +1,95 @@ +/* + * 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.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..cdbd8571 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/GlobalServerPlaceholderResolver.java @@ -0,0 +1,50 @@ +/* + * 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.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..ac405028 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/PlayerPlaceholderResolver.java @@ -0,0 +1,279 @@ +/* + * 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.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("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)); + 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("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)); + 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..a251e140 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/ServerCountPlaceholderResolver.java @@ -0,0 +1,56 @@ +/* + * 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.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..36464b8b --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/placeholder/ServerPlaceholderResolver.java @@ -0,0 +1,87 @@ +/* + * 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.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..9e8c07d3 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/player/AbstractPlayer.java @@ -0,0 +1,175 @@ +/* + * 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.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..5c4b7027 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/player/FakePlayer.java @@ -0,0 +1,133 @@ +/* + * 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.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..3bbfd4fb --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/player/FakePlayerManagerImpl.java @@ -0,0 +1,206 @@ +/* + * 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.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..d691d639 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/player/RedisPlayer.java @@ -0,0 +1,62 @@ +/* + * 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.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..0e900c2a --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/player/VelocityPlayer.java @@ -0,0 +1,96 @@ +/* + * 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.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..f4f95cf3 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/AbstractPacketHandler.java @@ -0,0 +1,68 @@ +/* + * 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.protocol; + +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; + +/** + * 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(LegacyPlayerListItemPacket packet) { + return parent.onPlayerListPacket(packet); + } + + @Override + public PacketListenerResult onTeamPacket(Team packet) { + return parent.onTeamPacket(packet); + } + + @Override + public PacketListenerResult onPlayerListHeaderFooterPacket(HeaderAndFooterPacket packet) { + return parent.onPlayerListHeaderFooterPacket(packet); + } + + @Override + public PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfoPacket packet) { + return parent.onPlayerListUpdatePacket(packet); + } + + @Override + public PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfoPacket 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..c629e224 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketHandler.java @@ -0,0 +1,38 @@ +/* + * 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.protocol; + +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(LegacyPlayerListItemPacket packet); + + PacketListenerResult onPlayerListUpdatePacket(UpsertPlayerInfoPacket packet); + + PacketListenerResult onPlayerListRemovePacket(RemovePlayerInfoPacket packet); + + PacketListenerResult onTeamPacket(Team 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 new file mode 100644 index 00000000..672925ac --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListener.java @@ -0,0 +1,94 @@ +/* + * 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.protocol; + +import codecrafter47.bungeetablistplus.BungeeTabListPlus; +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; +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; +import io.netty.util.ReferenceCountUtil; + +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) { + boolean shouldRelease = true; + 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) { + sendPacket(player, packet); + } + } else if (packet instanceof LegacyPlayerListItemPacket) { + result = handler.onPlayerListPacket((LegacyPlayerListItemPacket) packet); + handled = true; + } else if (packet instanceof HeaderAndFooterPacket) { + result = handler.onPlayerListHeaderFooterPacket((HeaderAndFooterPacket) packet); + handled = true; + } else if (packet instanceof UpsertPlayerInfoPacket) { + result = handler.onPlayerListUpdatePacket((UpsertPlayerInfoPacket) packet); + handled = true; + } else if (packet instanceof RemovePlayerInfoPacket) { + result = handler.onPlayerListRemovePacket((RemovePlayerInfoPacket) packet); + handled = true; + } + + if (handled && result != PacketListenerResult.CANCEL) { + sendPacket(player, packet); + } + } + } + out.add(packet); + shouldRelease = false; + } catch (Throwable th) { + BungeeTabListPlus.getInstance().reportError(th); + } finally { + if(!shouldRelease){ + ReferenceCountUtil.retain(packet); + } + } + } + + public static void sendPacket(Player player, MinecraftPacket packet) { + ((ConnectedPlayer) player).getConnection().write(packet); + } +} 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..5d3b9eeb --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/PacketListenerResult.java @@ -0,0 +1,22 @@ +/* + * 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.protocol; + +public enum PacketListenerResult { + PASS, MODIFIED, CANCEL; +} 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..5b59e133 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/protocol/Team.java @@ -0,0 +1,198 @@ +/* + * 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.protocol; + +import com.google.common.base.Preconditions; +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 com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; +import io.netty.buffer.ByteBuf; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) +public class Team implements MinecraftPacket { + + public enum Mode { + CREATE, + REMOVE, + UPDATE_INFO, + ADD_PLAYER, + REMOVE_PLAYER + } + + private String name; + private Mode mode; + private ComponentHolder displayName; + private ComponentHolder prefix; + private ComponentHolder suffix; + private NameTagVisibility nameTagVisibility; + private CollisionRule collisionRule; + private int color; + private byte friendlyFire; + private String[] players; + + // TODO: placeholder until release + private int MINECRAFT_1_21_5 = 770; + + public Team(String name) + { + this.name = name; + this.mode = Mode.REMOVE; + } + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { + name = ProtocolUtils.readString(buf); + 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); + suffix = ComponentHolder.read(buf, version); + } + friendlyFire = buf.readByte(); + // TODO: Replace this when released + if (version.getProtocol() >= MINECRAFT_1_21_5) { + nameTagVisibility = NameTagVisibility.BY_ID[ProtocolUtils.readVarInt( buf )]; + collisionRule = CollisionRule.BY_ID[ProtocolUtils.readVarInt( buf )]; + } else { + nameTagVisibility = readStringToMap( buf, NameTagVisibility.BY_NAME ); + if (version.compareTo(ProtocolVersion.MINECRAFT_1_9) >= 0) { + collisionRule = readStringToMap( buf, CollisionRule.BY_NAME ); + } + } + 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 == 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++) { + players[i] = ProtocolUtils.readString(buf); + } + } + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { + ProtocolUtils.writeString(buf, name); + 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); + suffix.write(buf); + } + buf.writeByte(friendlyFire); + // 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); + prefix.write(buf); + suffix.write(buf); + } else { + buf.writeByte(color); + } + } + 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); + } + } + } + + @Override + public boolean handle(MinecraftSessionHandler minecraftSessionHandler) { + return false; + } + + @Getter + @RequiredArgsConstructor + public enum NameTagVisibility { + + ALWAYS("always"), + NEVER("never"), + HIDE_FOR_OTHER_TEAMS("hideForOtherTeams"), + HIDE_FOR_OWN_TEAM("hideForOwnTeam"), + UNKNOWN( "" ); + + private final String key; + private static final Map BY_NAME; + private static final NameTagVisibility[] BY_ID; + + static { + NameTagVisibility[] values = NameTagVisibility.values(); + BY_ID = Arrays.copyOf( values, values.length - 1 ); + BY_NAME = Arrays.stream(values).collect(Collectors.toUnmodifiableMap(e -> e.key, e -> e)); + } + } + + @Getter + @RequiredArgsConstructor + public enum CollisionRule { + + ALWAYS("always"), + NEVER("never"), + PUSH_OTHER_TEAMS("pushOtherTeams"), + PUSH_OWN_TEAM("pushOwnTeam"); + // + private final String key; + // + private static final Map BY_NAME; + private static final CollisionRule[] BY_ID; + + static { + CollisionRule[] values = BY_ID = CollisionRule.values(); + BY_NAME = Arrays.stream(values).collect(Collectors.toUnmodifiableMap(e -> e.key, e -> e)); + } + } + + 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 ); + + return result; + } +} 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..87e85b3d --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/tablist/ExcludedServersTabOverlayProvider.java @@ -0,0 +1,99 @@ +/* + * 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.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..dfeefae5 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/template/PlayersByServerComponentTemplate.java @@ -0,0 +1,111 @@ +/* + * 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.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..3c50c404 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/updater/UpdateChecker.java @@ -0,0 +1,167 @@ +/* + * 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.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..94553302 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/updater/UpdateNotifier.java @@ -0,0 +1,79 @@ +/* + * 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.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..af1f310c --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/BitSet.java @@ -0,0 +1,164 @@ +/* + * 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.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/ColorParser.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ColorParser.java new file mode 100644 index 00000000..28a84124 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ColorParser.java @@ -0,0 +1,115 @@ +/* + * 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 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; +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..deb10782 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ConcurrentBitSet.java @@ -0,0 +1,65 @@ +/* + * 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.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..7aa820e0 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ContextAwareOrdering.java @@ -0,0 +1,81 @@ +/* + * 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.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..450d37ac --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/EmptyOrderedPlayerSet.java @@ -0,0 +1,46 @@ +/* + * 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 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..a79d192a --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/EmptyPlayerSet.java @@ -0,0 +1,64 @@ +/* + * 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 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..86911c77 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ExceptionHandlingEventExecutor.java @@ -0,0 +1,50 @@ +/* + * 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 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..28170dcf --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/Functions.java @@ -0,0 +1,30 @@ +/* + * 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 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..6bd9af97 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/GeyserCompat.java @@ -0,0 +1,81 @@ +/* + * 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; +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/IconUtil.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/IconUtil.java new file mode 100644 index 00000000..5ff8f4a8 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/IconUtil.java @@ -0,0 +1,65 @@ +/* + * 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.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..8cab35d0 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/IntToIntFunction.java @@ -0,0 +1,24 @@ +/* + * 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; + +@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..9a528f09 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/MapFunction.java @@ -0,0 +1,38 @@ +/* + * 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 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..24f779be --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/MatchingStringsCollection.java @@ -0,0 +1,70 @@ +/* + * 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 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..19eb7ad4 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/Object2IntHashMultimap.java @@ -0,0 +1,57 @@ +/* + * 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 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..1b8e56d0 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/Property119Handler.java @@ -0,0 +1,44 @@ +/* + * 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; +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(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); + } + + 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(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(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/ProxyServer.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ProxyServer.java new file mode 100644 index 00000000..f7f9f6d9 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ProxyServer.java @@ -0,0 +1,31 @@ +/* + * 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 { + + 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..62cf920d --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/ReflectionUtil.java @@ -0,0 +1,101 @@ +/* + * 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 codecrafter47.bungeetablistplus.protocol.Team; +import com.velocitypowered.api.network.ProtocolVersion; +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_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; + +public class ReflectionUtil { + + @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), + (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) { + e.printStackTrace(); + } + } + } +} 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..d5750034 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/VelocityPlugin.java @@ -0,0 +1,30 @@ +/* + * 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; +import org.slf4j.Logger; + +import java.nio.file.Path; + +public interface VelocityPlugin { + ProxyServer getProxy(); + Logger getLogger(); + Path getDataDirectory(); + String getVersion(); +} diff --git a/velocity/src/main/java/codecrafter47/bungeetablistplus/util/chat/ChatUtil.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/chat/ChatUtil.java new file mode 100644 index 00000000..30324594 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/util/chat/ChatUtil.java @@ -0,0 +1,413 @@ +/* + * 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; +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.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; + +/** + * 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/version/ProtocolVersionProvider.java b/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ProtocolVersionProvider.java new file mode 100644 index 00000000..95b03cf6 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ProtocolVersionProvider.java @@ -0,0 +1,40 @@ +/* + * 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.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); + + 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 new file mode 100644 index 00000000..b18e5190 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/version/VelocityProtocolVersionProvider.java @@ -0,0 +1,68 @@ +/* + * 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.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; + } + + @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 new file mode 100644 index 00000000..6ed57ecc --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/version/ViaVersionProtocolVersionProvider.java @@ -0,0 +1,69 @@ +/* + * 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.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 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(); + } + +} 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..cdd410e9 --- /dev/null +++ b/velocity/src/main/java/codecrafter47/bungeetablistplus/view/PlayersByServerComponentView.java @@ -0,0 +1,276 @@ +/* + * 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.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/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 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 00000000..0ff54e87 Binary files /dev/null and b/velocity/src/main/resources/heads/colors/aqua.png differ 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 00000000..03c10683 Binary files /dev/null and b/velocity/src/main/resources/heads/colors/black.png differ 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 00000000..f80a645b Binary files /dev/null and b/velocity/src/main/resources/heads/colors/blue.png differ 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 00000000..19665f44 Binary files /dev/null and b/velocity/src/main/resources/heads/colors/dark_aqua.png differ 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 00000000..2cae6fc3 Binary files /dev/null and b/velocity/src/main/resources/heads/colors/dark_blue.png differ 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 00000000..a0bdd6d7 Binary files /dev/null and b/velocity/src/main/resources/heads/colors/dark_gray.png differ 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 00000000..a746aa0f Binary files /dev/null and b/velocity/src/main/resources/heads/colors/dark_green.png differ 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 00000000..bacb5b1b Binary files /dev/null and b/velocity/src/main/resources/heads/colors/dark_purple.png differ 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 00000000..2b34412b Binary files /dev/null and b/velocity/src/main/resources/heads/colors/dark_red.png differ diff --git a/velocity/src/main/resources/heads/colors/gold.png b/velocity/src/main/resources/heads/colors/gold.png new file mode 100644 index 00000000..81703dd2 Binary files /dev/null and b/velocity/src/main/resources/heads/colors/gold.png differ 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 00000000..532308ef Binary files /dev/null and b/velocity/src/main/resources/heads/colors/gray.png differ 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 00000000..baf2e9c9 Binary files /dev/null and b/velocity/src/main/resources/heads/colors/green.png differ 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 00000000..2e17ad7e Binary files /dev/null and b/velocity/src/main/resources/heads/colors/light_purple.png differ 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 00000000..cf064359 Binary files /dev/null and b/velocity/src/main/resources/heads/colors/red.png differ 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 00000000..d1f00aa1 Binary files /dev/null and b/velocity/src/main/resources/heads/colors/white.png differ 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 00000000..6e4c8d39 Binary files /dev/null and b/velocity/src/main/resources/heads/colors/yellow.png differ 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 00000000..5978994c Binary files /dev/null and b/velocity/src/main/resources/heads/default/balance.png differ diff --git a/velocity/src/main/resources/heads/default/clock.png b/velocity/src/main/resources/heads/default/clock.png new file mode 100644 index 00000000..68b2ab46 Binary files /dev/null and b/velocity/src/main/resources/heads/default/clock.png differ 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 00000000..f02b4394 Binary files /dev/null and b/velocity/src/main/resources/heads/default/ping.png differ diff --git a/velocity/src/main/resources/heads/default/players.png b/velocity/src/main/resources/heads/default/players.png new file mode 100644 index 00000000..1036c8df Binary files /dev/null and b/velocity/src/main/resources/heads/default/players.png differ 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 00000000..d071983f Binary files /dev/null and b/velocity/src/main/resources/heads/default/rank.png differ diff --git a/velocity/src/main/resources/heads/default/server.png b/velocity/src/main/resources/heads/default/server.png new file mode 100644 index 00000000..53aff620 Binary files /dev/null and b/velocity/src/main/resources/heads/default/server.png differ 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"}