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