From 5046ddc5f3beae38db13afea9a8e5829d1648a11 Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Sat, 30 May 2026 12:11:36 -0400 Subject: [PATCH 01/16] Velocity get CorePlayer directly & debug log identity check --- .../networkjoinmessages/velocity/VelocityMain.java | 2 +- .../velocity/listeners/PlayerListener.java | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/velocity/VelocityMain.java b/src/main/java/xyz/earthcow/networkjoinmessages/velocity/VelocityMain.java index 3ea6adb4..7cf69bc0 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/velocity/VelocityMain.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/velocity/VelocityMain.java @@ -106,7 +106,7 @@ public void onProxyInitialization(ProxyInitializeEvent event) { velocityLogger.info("Successfully hooked into PremiumVanish!"); } - proxy.getEventManager().register(this, new PlayerListener(core.getCorePlayerListener())); + proxy.getEventManager().register(this, new PlayerListener(core.getCorePlayerListener(), velocityLogger)); registerCommands(); diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/velocity/listeners/PlayerListener.java b/src/main/java/xyz/earthcow/networkjoinmessages/velocity/listeners/PlayerListener.java index 43ab428f..937fcc60 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/velocity/listeners/PlayerListener.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/velocity/listeners/PlayerListener.java @@ -7,15 +7,18 @@ import xyz.earthcow.networkjoinmessages.common.abstraction.CorePlayer; import xyz.earthcow.networkjoinmessages.common.listeners.CorePlayerListener; import xyz.earthcow.networkjoinmessages.velocity.VelocityMain; +import xyz.earthcow.networkjoinmessages.velocity.abstraction.VelocityLogger; import xyz.earthcow.networkjoinmessages.velocity.abstraction.VelocityPlayer; import xyz.earthcow.networkjoinmessages.velocity.abstraction.VelocityServer; public class PlayerListener { private final CorePlayerListener corePlayerListener; + private final VelocityLogger logger; - public PlayerListener(CorePlayerListener corePlayerListener) { + public PlayerListener(CorePlayerListener corePlayerListener, VelocityLogger logger) { this.corePlayerListener = corePlayerListener; + this.logger = logger; } @Subscribe @@ -36,11 +39,12 @@ public void onServerConnected(ServerConnectedEvent event) { @Subscribe public void onDisconnect(DisconnectEvent event) { // Check that the player disconnected is not a duplicate user session (the same account tries to join the server while already joined) - CorePlayer corePlayer = VelocityMain.getInstance().getOrPutPlayer(new VelocityPlayer(event.getPlayer())); - if (corePlayer.getConnectionIdentity() != System.identityHashCode(event.getPlayer())) { + CorePlayer corePlayer = VelocityMain.getInstance().getPlayerManager().getPlayer(event.getPlayer().getUniqueId()); + if (corePlayer == null || corePlayer.getConnectionIdentity() != System.identityHashCode(event.getPlayer())) { + logger.debug("Ignoring disconnect event for " + (corePlayer == null ? "null" : corePlayer.getName()) + + " (" + event.getPlayer().getUsername() + ") - null or duplicate player identity"); return; } - corePlayerListener.onDisconnect(corePlayer); } } From 85513956aa2e20f02568f4afe11edc17d919abe2 Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Sat, 30 May 2026 12:16:57 -0400 Subject: [PATCH 02/16] Bungee get CorePlayer directly & debug log identity check --- .../networkjoinmessages/bungee/BungeeMain.java | 2 +- .../bungee/listeners/PlayerListener.java | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/bungee/BungeeMain.java b/src/main/java/xyz/earthcow/networkjoinmessages/bungee/BungeeMain.java index 2398af88..bbd1391a 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/bungee/BungeeMain.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/bungee/BungeeMain.java @@ -75,7 +75,7 @@ public void onEnable() { getProxy() .getPluginManager() - .registerListener(this, new PlayerListener(core.getCorePlayerListener())); + .registerListener(this, new PlayerListener(core.getCorePlayerListener(), bungeeLogger)); getProxy() .getPluginManager() diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/bungee/listeners/PlayerListener.java b/src/main/java/xyz/earthcow/networkjoinmessages/bungee/listeners/PlayerListener.java index 2aca76fb..b9a49f67 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/bungee/listeners/PlayerListener.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/bungee/listeners/PlayerListener.java @@ -8,6 +8,7 @@ import net.md_5.bungee.api.plugin.Listener; import net.md_5.bungee.event.EventHandler; import xyz.earthcow.networkjoinmessages.bungee.BungeeMain; +import xyz.earthcow.networkjoinmessages.bungee.abstraction.BungeeLogger; import xyz.earthcow.networkjoinmessages.bungee.abstraction.BungeePlayer; import xyz.earthcow.networkjoinmessages.bungee.abstraction.BungeeServer; import xyz.earthcow.networkjoinmessages.common.abstraction.CorePlayer; @@ -16,9 +17,11 @@ public class PlayerListener implements Listener { private final CorePlayerListener corePlayerListener; + private final BungeeLogger logger; - public PlayerListener(CorePlayerListener corePlayerListener) { + public PlayerListener(CorePlayerListener corePlayerListener, BungeeLogger logger) { this.corePlayerListener = corePlayerListener; + this.logger = logger; } @EventHandler @@ -42,8 +45,10 @@ public void onServerConnected(ServerConnectedEvent e) { @EventHandler public void onDisconnect(PlayerDisconnectEvent event) { // Check that the player disconnected is not a duplicate user session (the same account tries to join the server while already joined) - CorePlayer corePlayer = BungeeMain.getInstance().getOrPutPlayer(new BungeePlayer(event.getPlayer())); - if (corePlayer.getConnectionIdentity() != System.identityHashCode(event.getPlayer())) { + CorePlayer corePlayer = BungeeMain.getInstance().getPlayerManager().getPlayer(event.getPlayer().getUniqueId()); + if (corePlayer == null || corePlayer.getConnectionIdentity() != System.identityHashCode(event.getPlayer())) { + logger.debug("Ignoring disconnect event for " + (corePlayer == null ? "null" : corePlayer.getName()) + + " (" + event.getPlayer().getName() + ") - null or duplicate player identity"); return; } From 790bfd8c94743e776fd2107dd908690405977ab8 Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Sat, 30 May 2026 17:18:57 -0400 Subject: [PATCH 03/16] Remove unused CorePlugin#getCore --- .../xyz/earthcow/networkjoinmessages/bungee/BungeeMain.java | 5 ----- .../networkjoinmessages/common/abstraction/CorePlugin.java | 3 --- .../earthcow/networkjoinmessages/velocity/VelocityMain.java | 5 ----- 3 files changed, 13 deletions(-) diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/bungee/BungeeMain.java b/src/main/java/xyz/earthcow/networkjoinmessages/bungee/BungeeMain.java index 2398af88..23ff41ef 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/bungee/BungeeMain.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/bungee/BungeeMain.java @@ -124,11 +124,6 @@ public CoreCommandSender getConsole() { return console; } - @Override - public Core getCore() { - return core; - } - @Override public ServerType getServerType() { return ServerType.BUNGEE; diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlugin.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlugin.java index cafe8632..9345b748 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlugin.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlugin.java @@ -1,6 +1,5 @@ package xyz.earthcow.networkjoinmessages.common.abstraction; -import xyz.earthcow.networkjoinmessages.common.Core; import xyz.earthcow.networkjoinmessages.common.modules.DiscordIntegration; import java.io.File; @@ -8,8 +7,6 @@ import java.util.UUID; public interface CorePlugin { - Core getCore(); - void disable(); ServerType getServerType(); diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/velocity/VelocityMain.java b/src/main/java/xyz/earthcow/networkjoinmessages/velocity/VelocityMain.java index 3ea6adb4..f8f0cba3 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/velocity/VelocityMain.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/velocity/VelocityMain.java @@ -266,11 +266,6 @@ public ServerType getServerType() { return ServerType.VELOCITY; } - @Override - public Core getCore() { - return core; - } - @Override public CoreCommandSender getConsole() { return console; From 8ab4be679748d045958917fdb87d743ac6bcb715 Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Sat, 30 May 2026 17:19:57 -0400 Subject: [PATCH 04/16] Remove unused CorePlugin#getServerType & ServerType enum --- .../bungee/BungeeMain.java | 5 ----- .../common/abstraction/CorePlugin.java | 1 - .../common/abstraction/ServerType.java | 21 ------------------- .../velocity/VelocityMain.java | 5 ----- 4 files changed, 32 deletions(-) delete mode 100644 src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/ServerType.java diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/bungee/BungeeMain.java b/src/main/java/xyz/earthcow/networkjoinmessages/bungee/BungeeMain.java index 23ff41ef..00bb550b 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/bungee/BungeeMain.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/bungee/BungeeMain.java @@ -124,11 +124,6 @@ public CoreCommandSender getConsole() { return console; } - @Override - public ServerType getServerType() { - return ServerType.BUNGEE; - } - @Override public CoreLogger getCoreLogger() { return bungeeLogger; diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlugin.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlugin.java index 9345b748..da02d5df 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlugin.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlugin.java @@ -9,7 +9,6 @@ public interface CorePlugin { void disable(); - ServerType getServerType(); File getDataFolder(); CoreLogger getCoreLogger(); diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/ServerType.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/ServerType.java deleted file mode 100644 index 686a96b1..00000000 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/ServerType.java +++ /dev/null @@ -1,21 +0,0 @@ -package xyz.earthcow.networkjoinmessages.common.abstraction; - -public enum ServerType { - BUNGEE ("BungeeCord"), - VELOCITY("Velocity"); - - private final String displayName; - - ServerType() { - this(null); - } - - ServerType(String displayName) { - this.displayName = displayName; - } - - @Override - public String toString() { - return (displayName != null) ? displayName : super.toString(); - } -} \ No newline at end of file diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/velocity/VelocityMain.java b/src/main/java/xyz/earthcow/networkjoinmessages/velocity/VelocityMain.java index f8f0cba3..68aa682a 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/velocity/VelocityMain.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/velocity/VelocityMain.java @@ -261,11 +261,6 @@ public PremiumVanish getVanishAPI() { return premiumVanish; } - @Override - public ServerType getServerType() { - return ServerType.VELOCITY; - } - @Override public CoreCommandSender getConsole() { return console; From 52671d889c289ecf417516a1c94df58930caa3fb Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Sat, 30 May 2026 20:14:39 -0400 Subject: [PATCH 05/16] Add publish to Modrinth workflow --- .github/workflows/publish-modrinth.yml | 35 ++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/publish-modrinth.yml diff --git a/.github/workflows/publish-modrinth.yml b/.github/workflows/publish-modrinth.yml new file mode 100644 index 00000000..0ebf124d --- /dev/null +++ b/.github/workflows/publish-modrinth.yml @@ -0,0 +1,35 @@ +name: Publish to Modrinth + +on: + release: + types: [published] + +jobs: + publish: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Build with Gradle + run: ./gradlew build + + - name: Publish to Modrinth + uses: cloudnode-pro/modrinth-publish@v2 + with: + token: ${{ secrets.MODRINTH_TOKEN }} + project: ${{ secrets.MODRINTH_PROJECT_ID }} + version: ${{ github.event.release.tag_name }} + changelog: ${{ github.event.release.body }} + loaders: |- + velocity + bungeecord + game-versions: ">=1.8" + files: build/libs/*.jar \ No newline at end of file From d95bd5099fad554a652500bfc6adf7595c1ffee1 Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Sat, 30 May 2026 20:28:46 -0400 Subject: [PATCH 06/16] Disallow persist-credentials on checkout --- .github/workflows/publish-modrinth.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/publish-modrinth.yml b/.github/workflows/publish-modrinth.yml index 0ebf124d..f2bd9926 100644 --- a/.github/workflows/publish-modrinth.yml +++ b/.github/workflows/publish-modrinth.yml @@ -11,6 +11,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up JDK 21 uses: actions/setup-java@v4 From 27be75d2066d359182d56a11067856f37dca59e1 Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Sat, 30 May 2026 20:29:28 -0400 Subject: [PATCH 07/16] Use vars instead of secrets for MODRINTH_PROJECT_ID --- .github/workflows/publish-modrinth.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-modrinth.yml b/.github/workflows/publish-modrinth.yml index f2bd9926..c5a4e577 100644 --- a/.github/workflows/publish-modrinth.yml +++ b/.github/workflows/publish-modrinth.yml @@ -27,7 +27,7 @@ jobs: uses: cloudnode-pro/modrinth-publish@v2 with: token: ${{ secrets.MODRINTH_TOKEN }} - project: ${{ secrets.MODRINTH_PROJECT_ID }} + project: ${{ vars.MODRINTH_PROJECT_ID }} version: ${{ github.event.release.tag_name }} changelog: ${{ github.event.release.body }} loaders: |- From 5fc126a412be23d1ff6543967039596f4c5d0dab Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Sat, 30 May 2026 20:35:11 -0400 Subject: [PATCH 08/16] Set read only permissions --- .github/workflows/publish-modrinth.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/publish-modrinth.yml b/.github/workflows/publish-modrinth.yml index c5a4e577..559ecf4b 100644 --- a/.github/workflows/publish-modrinth.yml +++ b/.github/workflows/publish-modrinth.yml @@ -4,6 +4,9 @@ on: release: types: [published] +permissions: + contents: read + jobs: publish: From 4af31679093eb2f1789256195ead511d57b18808 Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Fri, 12 Jun 2026 22:31:05 -0400 Subject: [PATCH 09/16] Move connected state into CorePlayer class --- .../common/abstraction/CorePlayer.java | 1 + .../common/listeners/CorePlayerListener.java | 7 +++--- .../common/player/PlayerStateStore.java | 12 --------- .../common/player/PlayerStateStoreTest.java | 25 ------------------- 4 files changed, 4 insertions(+), 41 deletions(-) diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlayer.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlayer.java index 59a06000..87aeb300 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlayer.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlayer.java @@ -11,6 +11,7 @@ @Getter @Setter public abstract class CorePlayer implements CoreCommandSender { // Fields + private boolean connected = false; private CoreBackendServer lastKnownConnectedServer; private boolean disconnecting = false; private String cachedLeaveMessage; diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/listeners/CorePlayerListener.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/listeners/CorePlayerListener.java index 20fa79be..02e61bf8 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/listeners/CorePlayerListener.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/listeners/CorePlayerListener.java @@ -87,7 +87,7 @@ public void onPreConnect(@NotNull CorePlayer player, @Nullable String previousSe public void onServerConnected(@NotNull CorePlayer player, @NotNull CoreBackendServer server, @Nullable CoreBackendServer previousServer) { plugin.runTaskAsync(() -> { - if (!stateStore.isConnected(player)) { + if (!player.isConnected()) { handleJoin(player, server); } else { boolean fromLimbo = plugin.hasLimbo() && previousServer == null; @@ -119,7 +119,7 @@ public void onDisconnect(@NotNull CorePlayer player) { private void handleJoin(@NotNull CorePlayer player, @NotNull CoreBackendServer server) { stateStore.loadData(player.getUniqueId(), player.getName()); - stateStore.setConnected(player, true); + player.setConnected(true); player.setLastKnownConnectedServer(server); PremiumVanish pv = plugin.getVanishAPI(); @@ -243,7 +243,7 @@ private boolean shouldSkipSwap(CorePlayer player, String from, String to, boolea private boolean shouldSkipLeave(CorePlayer player) { if (player.getCurrentServer() == null) return true; - if (!stateStore.isConnected(player)) { + if (!player.isConnected()) { plugin.getCoreLogger().debug("Skipping leave for " + player.getName() + " — not marked as connected"); return true; } @@ -306,7 +306,6 @@ private void fireLeaveEvent(CorePlayer player, String serverName, String message private void cleanup(CorePlayer player) { plugin.getPlayerManager().removePlayer(player.getUniqueId()); - stateStore.setConnected(player, false); leaveMessageCache.stopFor(player); } } diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/player/PlayerStateStore.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/player/PlayerStateStore.java index 914f7e82..0419078c 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/player/PlayerStateStore.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/player/PlayerStateStore.java @@ -26,7 +26,6 @@ public final class PlayerStateStore { private final Map previousServer = new ConcurrentHashMap<>(); private final Map silentState = new ConcurrentHashMap<>(); - private final Set onlinePlayers = ConcurrentHashMap.newKeySet(); private final Set noJoinMessage = ConcurrentHashMap.newKeySet(); private final Set noLeaveMessage = ConcurrentHashMap.newKeySet(); private final Set noSwapMessage = ConcurrentHashMap.newKeySet(); @@ -101,17 +100,6 @@ private Boolean determineSaveState(Set set, boolean ignoreByDefault, UUID return null; } - // --- Online tracking --- - - public boolean isConnected(CorePlayer player) { - return onlinePlayers.contains(player.getUniqueId()); - } - - public void setConnected(CorePlayer player, boolean connected) { - if (connected) onlinePlayers.add(player.getUniqueId()); - else onlinePlayers.remove(player.getUniqueId()); - } - // --- Previous server --- public String getFrom(CorePlayer player) { diff --git a/src/test/java/xyz/earthcow/networkjoinmessages/common/player/PlayerStateStoreTest.java b/src/test/java/xyz/earthcow/networkjoinmessages/common/player/PlayerStateStoreTest.java index 545f7efe..83e46d86 100644 --- a/src/test/java/xyz/earthcow/networkjoinmessages/common/player/PlayerStateStoreTest.java +++ b/src/test/java/xyz/earthcow/networkjoinmessages/common/player/PlayerStateStoreTest.java @@ -290,31 +290,6 @@ void getSuppressedPlayers_firstJoinMapsToJoinSet() { "FIRST_JOIN should return the same set as JOIN"); } - // ----------------------------------------------------------------------- - // isConnected / setConnected - // ----------------------------------------------------------------------- - - @Test - void setConnected_true_marksPlayerOnline() { - PlayerStateStore stateStore = new PlayerStateStore(plugin, config, store); - stateStore.setConnected(player, true); - assertTrue(stateStore.isConnected(player)); - } - - @Test - void setConnected_false_marksPlayerOffline() { - PlayerStateStore stateStore = new PlayerStateStore(plugin, config, store); - stateStore.setConnected(player, true); - stateStore.setConnected(player, false); - assertFalse(stateStore.isConnected(player)); - } - - @Test - void isConnected_unknownPlayer_returnsFalse() { - PlayerStateStore stateStore = new PlayerStateStore(plugin, config, store); - assertFalse(stateStore.isConnected(player)); - } - // ----------------------------------------------------------------------- // getFrom / setFrom // ----------------------------------------------------------------------- From 91308ea67ede8df66391f84de6210ff852727cb4 Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Fri, 12 Jun 2026 23:09:49 -0400 Subject: [PATCH 10/16] Make CorePlayer connecting & disconnecting volatile --- .../networkjoinmessages/common/abstraction/CorePlayer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlayer.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlayer.java index 87aeb300..cc2970e5 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlayer.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlayer.java @@ -11,9 +11,9 @@ @Getter @Setter public abstract class CorePlayer implements CoreCommandSender { // Fields - private boolean connected = false; + private volatile boolean connected = false; private CoreBackendServer lastKnownConnectedServer; - private boolean disconnecting = false; + private volatile boolean disconnecting = false; private String cachedLeaveMessage; private Audience audience; private boolean premiumVanishHidden = false; From 9424faf39f66e01672e2c5b8388760a455b4865e Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Sat, 13 Jun 2026 00:02:14 -0400 Subject: [PATCH 11/16] Make CorePlayer disconnecting atomic --- .../common/abstraction/CorePlayer.java | 9 ++++++++- .../common/listeners/CorePlayerListener.java | 3 +-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlayer.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlayer.java index cc2970e5..61e17901 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlayer.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlayer.java @@ -1,5 +1,6 @@ package xyz.earthcow.networkjoinmessages.common.abstraction; +import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; import net.kyori.adventure.audience.Audience; @@ -7,13 +8,15 @@ import org.jetbrains.annotations.Nullable; import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; @Getter @Setter public abstract class CorePlayer implements CoreCommandSender { // Fields private volatile boolean connected = false; private CoreBackendServer lastKnownConnectedServer; - private volatile boolean disconnecting = false; + @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) + private AtomicBoolean disconnecting = new AtomicBoolean(false); private String cachedLeaveMessage; private Audience audience; private boolean premiumVanishHidden = false; @@ -25,6 +28,10 @@ public CorePlayer(CoreBackendServer lastKnownConnectedServer, Audience audience) this.audience = audience; } + public boolean markDisconnecting() { + return disconnecting.compareAndSet(false, true); + } + // Abstract @NotNull public abstract UUID getUniqueId(); diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/listeners/CorePlayerListener.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/listeners/CorePlayerListener.java index 02e61bf8..916f8e4c 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/listeners/CorePlayerListener.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/listeners/CorePlayerListener.java @@ -100,11 +100,10 @@ public void onServerConnected(@NotNull CorePlayer player, @NotNull CoreBackendSe * Called when a player disconnects from the network. */ public void onDisconnect(@NotNull CorePlayer player) { - if (player.isDisconnecting()) { + if (!player.markDisconnecting()) { plugin.getCoreLogger().debug("Duplicate disconnect ignored for " + player.getName()); return; } - player.setDisconnecting(true); if (shouldSkipLeave(player)) { cleanup(player); From 6fcf0269cc01e65a9cc11ea4bfcfd8fabfd4dcad Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Sat, 13 Jun 2026 00:14:50 -0400 Subject: [PATCH 12/16] Make CorePlayer connected atomic --- .../common/abstraction/CorePlayer.java | 11 ++++++++++- .../common/listeners/CorePlayerListener.java | 6 +++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlayer.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlayer.java index 61e17901..2dab426c 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlayer.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlayer.java @@ -13,7 +13,8 @@ @Getter @Setter public abstract class CorePlayer implements CoreCommandSender { // Fields - private volatile boolean connected = false; + @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) + private AtomicBoolean connected = new AtomicBoolean(false); private CoreBackendServer lastKnownConnectedServer; @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) private AtomicBoolean disconnecting = new AtomicBoolean(false); @@ -28,6 +29,14 @@ public CorePlayer(CoreBackendServer lastKnownConnectedServer, Audience audience) this.audience = audience; } + public boolean isConnected() { + return connected.get(); + } + + public void setConnected(boolean value) { + connected.set(value); + } + public boolean markDisconnecting() { return disconnecting.compareAndSet(false, true); } diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/listeners/CorePlayerListener.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/listeners/CorePlayerListener.java index 916f8e4c..d937ad33 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/listeners/CorePlayerListener.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/listeners/CorePlayerListener.java @@ -87,11 +87,11 @@ public void onPreConnect(@NotNull CorePlayer player, @Nullable String previousSe public void onServerConnected(@NotNull CorePlayer player, @NotNull CoreBackendServer server, @Nullable CoreBackendServer previousServer) { plugin.runTaskAsync(() -> { - if (!player.isConnected()) { - handleJoin(player, server); - } else { + if (player.isConnected()) { boolean fromLimbo = plugin.hasLimbo() && previousServer == null; handleSwap(player, server, fromLimbo); + } else { + handleJoin(player, server); } }); } From 3f7e18336dfb7e2145762ed1f0ce0e8707c152ac Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Wed, 12 Aug 2026 17:41:12 -0400 Subject: [PATCH 13/16] Add discord leave message cache --- .../networkjoinmessages/common/Core.java | 10 ++-- .../common/abstraction/CorePlayer.java | 1 + .../common/modules/DiscordIntegration.java | 46 +++++++++---------- .../common/modules/DiscordWebhookBuilder.java | 11 +++++ .../common/player/LeaveMessageCache.java | 36 +++++++++++++-- 5 files changed, 73 insertions(+), 31 deletions(-) diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/Core.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/Core.java index 0b9f8f4c..3a6f6d86 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/Core.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/Core.java @@ -84,14 +84,18 @@ public Core(CorePlugin plugin, PremiumVanish premiumVanish) { ReceiverResolver receiverResolver = new ReceiverResolver(plugin, config, sayanVanishHook != null, premiumVanish != null); MessageHandler messageHandler = new MessageHandler(plugin, config, stateStore, placeholderResolver, receiverResolver); + // Discord webhook builder, early for leave message cache usage + DiscordWebhookBuilder webhookBuilder = new DiscordWebhookBuilder(plugin, configManager.getDiscordConfig()); + // Player event helpers SilenceChecker silenceChecker = new SilenceChecker(plugin, config, stateStore, sayanVanishHook, premiumVanish); - LeaveMessageCache leaveMessageCache = new LeaveMessageCache(plugin, config, messageFormatter, placeholderResolver); + LeaveMessageCache leaveMessageCache = new LeaveMessageCache( + plugin, config, messageFormatter, placeholderResolver, webhookBuilder, configManager.getDiscordConfig()); LeaveJoinBufferManager leaveJoinBuffer = new LeaveJoinBufferManager(plugin, config); // Discord integration - DiscordWebhookBuilder webhookBuilder = new DiscordWebhookBuilder(plugin, configManager.getDiscordConfig()); - DiscordIntegration discordIntegration = new DiscordIntegration(plugin, placeholderResolver, messageFormatter, webhookBuilder, configManager.getDiscordConfig()); + DiscordIntegration discordIntegration = new DiscordIntegration( + plugin, placeholderResolver, messageFormatter, webhookBuilder, configManager.getDiscordConfig()); // Spoof SpoofManager spoofManager = new SpoofManager(plugin, config, messageHandler, messageFormatter, placeholderResolver); diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlayer.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlayer.java index 2dab426c..a67d6494 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlayer.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/abstraction/CorePlayer.java @@ -19,6 +19,7 @@ public abstract class CorePlayer implements CoreCommandSender { @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) private AtomicBoolean disconnecting = new AtomicBoolean(false); private String cachedLeaveMessage; + private String cachedDiscordLeavePayload; private Audience audience; private boolean premiumVanishHidden = false; private int premiumVanishUseLevel = 0; diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/modules/DiscordIntegration.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/modules/DiscordIntegration.java index 57d3361e..8fdeb1eb 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/modules/DiscordIntegration.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/modules/DiscordIntegration.java @@ -71,7 +71,7 @@ public void onSwapServer(SwapServerEvent event) { DiscordWebhook webhook = webhookBuilder.buildSwapWebhook(webhookUrl); if (webhook == null) return; - String avatarUrl = resolveAvatarUrl(player); + String avatarUrl = webhookBuilder.resolveAvatarUrl(player); String preparedJson = messageFormatter.prepareDiscordSwapTemplate( webhook.getJsonString(), player, event.serverFrom(), event.serverTo(), avatarUrl); executeWebhook(webhook, preparedJson, player); @@ -84,7 +84,7 @@ public void onNetworkJoin(NetworkJoinEvent event) { DiscordWebhook webhook = webhookBuilder.buildJoinWebhook(webhookUrl, key); if (webhook == null) return; - String avatarUrl = resolveAvatarUrl(player); + String avatarUrl = webhookBuilder.resolveAvatarUrl(player); String preparedJson = messageFormatter.prepareDiscordJoinLeaveTemplate( webhook.getJsonString(), player, false, avatarUrl); executeWebhook(webhook, preparedJson, player); @@ -96,10 +96,13 @@ public void onNetworkLeave(NetworkLeaveEvent event) { DiscordWebhook webhook = webhookBuilder.buildLeaveWebhook(webhookUrl); if (webhook == null) return; - String avatarUrl = resolveAvatarUrl(player); - String preparedJson = messageFormatter.prepareDiscordJoinLeaveTemplate( - webhook.getJsonString(), player, true, avatarUrl); - executeWebhook(webhook, preparedJson, player); + String cachedPayload = player.getCachedDiscordLeavePayload(); + if (cachedPayload == null) { + plugin.getCoreLogger().warn( + "No cached Discord leave payload for " + player.getName() + ", skipping webhook."); + return; + } + sendWebhook(webhook, cachedPayload); } // --- Webhook execution --- @@ -109,27 +112,20 @@ public void onNetworkLeave(NetworkLeaveEvent event) { * string, then executes the webhook asynchronously. */ private void executeWebhook(DiscordWebhook webhook, String preparedJson, CorePlayer parseTarget) { - placeholderResolver.resolve(preparedJson, parseTarget, fullyResolved -> - plugin.runTaskAsync(() -> { - try { - webhook.execute(fullyResolved); - } catch (IOException e) { - plugin.getCoreLogger().severe("[DiscordIntegration] " + describeHttpError(e)); - plugin.getCoreLogger().debug("Exception: " + e); - plugin.getCoreLogger().debug("Webhook payload: " + preparedJson); - } - }) - ); + placeholderResolver.resolve(preparedJson, parseTarget, fullyResolved -> sendWebhook(webhook, fullyResolved)); } - /** - * Resolves the avatar URL template from config, substituting {@code %uuid%} and - * {@code %player%} for the given player. - */ - private String resolveAvatarUrl(CorePlayer player) { - return discordConfig.getString("EmbedAvatarUrl") - .replace("%uuid%", player.getUniqueId().toString()) - .replace("%player%", player.getName()); + /** Sends an already fully-resolved JSON payload to Discord asynchronously. */ + private void sendWebhook(DiscordWebhook webhook, String fullyResolved) { + plugin.runTaskAsync(() -> { + try { + webhook.execute(fullyResolved); + } catch (IOException e) { + plugin.getCoreLogger().severe("[DiscordIntegration] " + describeHttpError(e)); + plugin.getCoreLogger().debug("Exception: " + e); + plugin.getCoreLogger().debug("Webhook payload: " + fullyResolved); + } + }); } /** Produces a human-readable error description for a failed webhook HTTP request. */ diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/modules/DiscordWebhookBuilder.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/modules/DiscordWebhookBuilder.java index 7678e4aa..ffe5b4ce 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/modules/DiscordWebhookBuilder.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/modules/DiscordWebhookBuilder.java @@ -3,6 +3,7 @@ import dev.dejvokep.boostedyaml.YamlDocument; import org.jetbrains.annotations.Nullable; import xyz.earthcow.discordwebhook.DiscordWebhook; +import xyz.earthcow.networkjoinmessages.common.abstraction.CorePlayer; import xyz.earthcow.networkjoinmessages.common.abstraction.CorePlugin; import java.awt.*; @@ -54,6 +55,16 @@ public DiscordWebhook buildLeaveWebhook(String webhookUrl) { return buildWebhook(webhookUrl, "Messages.LeaveNetwork"); } + /** + * Resolves the avatar URL template from config, substituting {@code %uuid%} and + * {@code %player%} for the given player. + */ + public String resolveAvatarUrl(CorePlayer player) { + return discordConfig.getString("EmbedAvatarUrl") + .replace("%uuid%", player.getUniqueId().toString()) + .replace("%player%", player.getName()); + } + // --- Internal builder --- /** diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/player/LeaveMessageCache.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/player/LeaveMessageCache.java index 7aee101e..c7a0cc6f 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/player/LeaveMessageCache.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/player/LeaveMessageCache.java @@ -1,9 +1,12 @@ package xyz.earthcow.networkjoinmessages.common.player; +import dev.dejvokep.boostedyaml.YamlDocument; +import xyz.earthcow.discordwebhook.DiscordWebhook; import xyz.earthcow.networkjoinmessages.common.abstraction.CorePlayer; import xyz.earthcow.networkjoinmessages.common.abstraction.CorePlugin; import xyz.earthcow.networkjoinmessages.common.broadcast.MessageFormatter; import xyz.earthcow.networkjoinmessages.common.config.PluginConfig; +import xyz.earthcow.networkjoinmessages.common.modules.DiscordWebhookBuilder; import xyz.earthcow.networkjoinmessages.common.util.PlaceholderResolver; import java.util.Map; @@ -11,11 +14,14 @@ import java.util.concurrent.ConcurrentHashMap; /** - * Pre-computes and caches the formatted leave message for each online player. + * Pre-computes and caches the formatted leave message for each online player, both for the + * in-game leave message and the Discord leave webhook payload. * *

Because a player's leave message cannot be computed after disconnect (no live * placeholder data), it is formatted periodically while the player is connected and - * stored on the player object, ready to be sent immediately on disconnect. + * stored on the player object, ready to be sent immediately on disconnect. Both caches + * are refreshed together on the same timer so they can never fall out of sync with + * each other. */ public final class LeaveMessageCache { @@ -23,6 +29,8 @@ public final class LeaveMessageCache { private final PluginConfig config; private final MessageFormatter messageFormatter; private final PlaceholderResolver placeholderResolver; + private final DiscordWebhookBuilder discordWebhookBuilder; + private final YamlDocument discordConfig; /** Maps player UUID -> repeating cache-refresh task ID */ private final Map refreshTasks = new ConcurrentHashMap<>(); @@ -31,12 +39,16 @@ public LeaveMessageCache( CorePlugin plugin, PluginConfig config, MessageFormatter messageFormatter, - PlaceholderResolver placeholderResolver + PlaceholderResolver placeholderResolver, + DiscordWebhookBuilder discordWebhookBuilder, + YamlDocument discordConfig ) { this.plugin = plugin; this.config = config; this.messageFormatter = messageFormatter; this.placeholderResolver = placeholderResolver; + this.discordWebhookBuilder = discordWebhookBuilder; + this.discordConfig = discordConfig; } /** Starts cache-refresh tasks for all currently online players. Called on reload. */ @@ -70,5 +82,23 @@ public void refresh(CorePlayer player) { plugin.getCoreLogger().debug("Refreshing cached leave message for " + player.getName()); String template = messageFormatter.formatLeaveMessage(player); placeholderResolver.resolve(template, player, player::setCachedLeaveMessage); + refreshDiscordPayload(player); + } + + private void refreshDiscordPayload(CorePlayer player) { + if (!discordConfig.getBoolean("Enabled")) { + player.setCachedDiscordLeavePayload(null); + return; + } + String webhookUrl = discordConfig.getString("WebhookUrl"); + DiscordWebhook webhook = discordWebhookBuilder.buildLeaveWebhook(webhookUrl); + if (webhook == null) { + player.setCachedDiscordLeavePayload(null); + return; + } + String avatarUrl = discordWebhookBuilder.resolveAvatarUrl(player); + String preparedJson = messageFormatter.prepareDiscordJoinLeaveTemplate( + webhook.getJsonString(), player, true, avatarUrl); + placeholderResolver.resolve(preparedJson, player, player::setCachedDiscordLeavePayload); } } From 0e12d28bd6eca569071bcddb91ff6ded7023cca4 Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Wed, 12 Aug 2026 17:41:40 -0400 Subject: [PATCH 14/16] Remove leave message cache debug line --- .../networkjoinmessages/common/player/LeaveMessageCache.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/player/LeaveMessageCache.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/player/LeaveMessageCache.java index c7a0cc6f..1f52c753 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/player/LeaveMessageCache.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/player/LeaveMessageCache.java @@ -79,7 +79,6 @@ public void stopFor(CorePlayer player) { /** Forces an immediate refresh of the cached leave message for the given player. */ public void refresh(CorePlayer player) { - plugin.getCoreLogger().debug("Refreshing cached leave message for " + player.getName()); String template = messageFormatter.formatLeaveMessage(player); placeholderResolver.resolve(template, player, player::setCachedLeaveMessage); refreshDiscordPayload(player); From 1078c4e885259d7195ec1e73582090c0c23ab3c5 Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Wed, 12 Aug 2026 17:53:48 -0400 Subject: [PATCH 15/16] Strip all formatting from discord messages --- .../common/modules/DiscordIntegration.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/xyz/earthcow/networkjoinmessages/common/modules/DiscordIntegration.java b/src/main/java/xyz/earthcow/networkjoinmessages/common/modules/DiscordIntegration.java index 8fdeb1eb..d7156c30 100644 --- a/src/main/java/xyz/earthcow/networkjoinmessages/common/modules/DiscordIntegration.java +++ b/src/main/java/xyz/earthcow/networkjoinmessages/common/modules/DiscordIntegration.java @@ -8,6 +8,7 @@ import xyz.earthcow.networkjoinmessages.common.events.NetworkJoinEvent; import xyz.earthcow.networkjoinmessages.common.events.NetworkLeaveEvent; import xyz.earthcow.networkjoinmessages.common.events.SwapServerEvent; +import xyz.earthcow.networkjoinmessages.common.util.Formatter; import xyz.earthcow.networkjoinmessages.common.util.PlaceholderResolver; import java.io.FileNotFoundException; @@ -118,12 +119,13 @@ private void executeWebhook(DiscordWebhook webhook, String preparedJson, CorePla /** Sends an already fully-resolved JSON payload to Discord asynchronously. */ private void sendWebhook(DiscordWebhook webhook, String fullyResolved) { plugin.runTaskAsync(() -> { + String cleanFullyResolved = Formatter.sanitize(fullyResolved); try { - webhook.execute(fullyResolved); + webhook.execute(cleanFullyResolved); } catch (IOException e) { plugin.getCoreLogger().severe("[DiscordIntegration] " + describeHttpError(e)); plugin.getCoreLogger().debug("Exception: " + e); - plugin.getCoreLogger().debug("Webhook payload: " + fullyResolved); + plugin.getCoreLogger().debug("Webhook payload: " + cleanFullyResolved); } }); } From e59cf8f1d2b7c2395d40ad861328819b9964de2b Mon Sep 17 00:00:00 2001 From: EarthCow <56940983+EarthCow@users.noreply.github.com> Date: Wed, 12 Aug 2026 18:11:58 -0400 Subject: [PATCH 16/16] v3.6.2 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e4b64156..7d9d800e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ # Project metadata group=xyz.earthcow.networkjoinmessages -version=3.6.1 +version=3.6.2 description=A plugin handling join, leave and swap messages for proxy servers. # Plugin.yml metadata