From c0ee10907c9291f1fd9ef47a45f4cdd176fd9a0d Mon Sep 17 00:00:00 2001
From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com>
Date: Tue, 22 Sep 2026 19:31:24 +0000
Subject: [PATCH] fix: resolve Discord gate after RPCharacters enables
---
pom.xml | 19 ++-
.../java/net/tfminecraft/TFMCWeb/TFMCWeb.java | 4 +-
.../TFMCWeb/gate/DiscordGateService.java | 26 +++-
src/main/resources/plugin.yml | 3 +-
.../TFMCWeb/gate/DiscordGateServiceTest.java | 146 ++++++++++++++++++
5 files changed, 186 insertions(+), 12 deletions(-)
create mode 100644 src/test/java/net/tfminecraft/TFMCWeb/gate/DiscordGateServiceTest.java
diff --git a/pom.xml b/pom.xml
index f95865b..0905319 100644
--- a/pom.xml
+++ b/pom.xml
@@ -41,7 +41,19 @@
-
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.11.4
+ test
+
+
+ org.mockito
+ mockito-core
+ 5.14.2
+ test
+
@@ -74,6 +86,11 @@
maven-compiler-plugin
3.14.1
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.2
+
diff --git a/src/main/java/net/tfminecraft/TFMCWeb/TFMCWeb.java b/src/main/java/net/tfminecraft/TFMCWeb/TFMCWeb.java
index 311f00d..bca688e 100644
--- a/src/main/java/net/tfminecraft/TFMCWeb/TFMCWeb.java
+++ b/src/main/java/net/tfminecraft/TFMCWeb/TFMCWeb.java
@@ -39,8 +39,8 @@ public void onEnable() {
gateService = new DiscordGateService(this, linkCache);
if (!gateService.isRpcAvailable()) {
- getLogger().warning(
- "RPCharacters not found — Discord Survival gate disabled; link/HTTP still active."
+ getLogger().info(
+ "Discord Survival gate waiting for RPCharacters; availability is checked when used."
);
}
diff --git a/src/main/java/net/tfminecraft/TFMCWeb/gate/DiscordGateService.java b/src/main/java/net/tfminecraft/TFMCWeb/gate/DiscordGateService.java
index 46d66eb..d0146d2 100644
--- a/src/main/java/net/tfminecraft/TFMCWeb/gate/DiscordGateService.java
+++ b/src/main/java/net/tfminecraft/TFMCWeb/gate/DiscordGateService.java
@@ -15,29 +15,39 @@
import net.tfminecraft.TFMCWeb.cache.LinkCache;
/**
- * Applies Survival Discord gate via RPCharacters (soft-depend, reflection).
+ * Applies Survival Discord gate via RPCharacters (optional, reflection).
*/
public final class DiscordGateService {
private final JavaPlugin plugin;
private final LinkCache linkCache;
- private final boolean rpcAvailable;
+ private Plugin resolvedRpc;
private Method setGateUuid;
private Method setGatePlayer;
public DiscordGateService(JavaPlugin plugin, LinkCache linkCache) {
this.plugin = plugin;
this.linkCache = linkCache;
- this.rpcAvailable = resolveRpc();
}
private boolean resolveRpc() {
Plugin rpc = Bukkit.getPluginManager().getPlugin("RPCharacters");
if (rpc == null || !rpc.isEnabled()) {
+ resolvedRpc = null;
+ setGateUuid = null;
+ setGatePlayer = null;
return false;
}
+ if (rpc == resolvedRpc) {
+ return setGateUuid != null && setGatePlayer != null;
+ }
+ resolvedRpc = rpc;
+ setGateUuid = null;
+ setGatePlayer = null;
try {
- Class> cls = Class.forName("net.tfminecraft.RPCharacters.RPCharacters");
+ // RPC depends on TFMCWeb, so resolve only once it has enabled, using
+ // its own class rather than TFMCWeb's plugin class loader.
+ Class> cls = rpc.getClass();
setGateUuid = cls.getMethod("setDiscordGate", UUID.class, boolean.class);
setGatePlayer = cls.getMethod("setDiscordGate", Player.class, boolean.class);
return true;
@@ -52,7 +62,7 @@ private boolean resolveRpc() {
}
public boolean isRpcAvailable() {
- return rpcAvailable;
+ return resolveRpc();
}
/**
@@ -77,7 +87,7 @@ public void applyGate(Player player) {
}
public void applyGate(Player player, boolean eligible) {
- if (player == null || !rpcAvailable) {
+ if (player == null || !isRpcAvailable()) {
return;
}
boolean required = player.getGameMode() == GameMode.SURVIVAL && !eligible;
@@ -86,7 +96,7 @@ public void applyGate(Player player, boolean eligible) {
/** Offline-safe: set UUID gate flag; RPC reevaluates if online. */
public void applyGate(UUID uuid, boolean eligible) {
- if (uuid == null || !rpcAvailable) {
+ if (uuid == null || !isRpcAvailable()) {
return;
}
Player online = Bukkit.getPlayer(uuid);
@@ -99,7 +109,7 @@ public void applyGate(UUID uuid, boolean eligible) {
}
public void clearGate(UUID uuid) {
- if (uuid == null || !rpcAvailable) {
+ if (uuid == null || !isRpcAvailable()) {
return;
}
Player online = Bukkit.getPlayer(uuid);
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
index 79dba5d..7587643 100644
--- a/src/main/resources/plugin.yml
+++ b/src/main/resources/plugin.yml
@@ -5,7 +5,8 @@ version: ${project.version}
author: TFMC
depend: [TLibs]
-softdepend: [RPCharacters, Essentials]
+# RPCharacters depends on TFMCWeb; its optional gate API is resolved at runtime.
+softdepend: [Essentials]
commands:
linkdiscord:
diff --git a/src/test/java/net/tfminecraft/TFMCWeb/gate/DiscordGateServiceTest.java b/src/test/java/net/tfminecraft/TFMCWeb/gate/DiscordGateServiceTest.java
new file mode 100644
index 0000000..884b7ee
--- /dev/null
+++ b/src/test/java/net/tfminecraft/TFMCWeb/gate/DiscordGateServiceTest.java
@@ -0,0 +1,146 @@
+package net.tfminecraft.TFMCWeb.gate;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+import java.util.UUID;
+import java.util.logging.Logger;
+
+import org.bukkit.Bukkit;
+import org.bukkit.GameMode;
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.PluginManager;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+
+import net.tfminecraft.TFMCWeb.cache.LinkCache;
+
+class DiscordGateServiceTest {
+
+ private MockedStatic bukkit;
+ private PluginManager plugins;
+ private RpcPlugin rpc;
+ private Player player;
+ private UUID uuid;
+ private LinkCache cache;
+ private DiscordGateService gate;
+
+ @BeforeEach
+ void setUp() {
+ bukkit = mockStatic(Bukkit.class);
+ plugins = mock(PluginManager.class);
+ bukkit.when(Bukkit::getPluginManager).thenReturn(plugins);
+ rpc = mock(RpcPlugin.class);
+ when(plugins.getPlugin("RPCharacters")).thenReturn(rpc);
+ JavaPlugin web = mock(JavaPlugin.class);
+ when(web.getLogger()).thenReturn(Logger.getAnonymousLogger());
+ player = mock(Player.class);
+ uuid = UUID.randomUUID();
+ when(player.getUniqueId()).thenReturn(uuid);
+ when(player.isOnline()).thenReturn(true);
+ when(player.getGameMode()).thenReturn(GameMode.SURVIVAL);
+ cache = new LinkCache();
+ RpcPlugin.lastUuid = null;
+ RpcPlugin.lastRequired = null;
+ // TFMCWeb starts while its dependent RPCharacters is still disabled.
+ gate = new DiscordGateService(web, cache);
+ }
+
+ @AfterEach
+ void tearDown() {
+ bukkit.close();
+ }
+
+ @Test
+ void freezesUnlinkedSurvivalPlayerAfterRpcEnablesLater() {
+ assertFalse(gate.isRpcAvailable());
+ gate.applyGate(player);
+ assertNull(RpcPlugin.lastRequired);
+
+ when(rpc.isEnabled()).thenReturn(true);
+ gate.applyGate(player);
+
+ assertEquals(uuid, RpcPlugin.lastUuid);
+ assertEquals(Boolean.TRUE, RpcPlugin.lastRequired);
+ assertTrue(gate.isRpcAvailable());
+ }
+
+ @Test
+ void releasesEligiblePlayersAndDoesNotFreezeOtherGameModes() {
+ when(rpc.isEnabled()).thenReturn(true);
+ cache.putLinked(uuid, "discord-id", "player");
+ gate.applyGate(player);
+ assertEquals(Boolean.FALSE, RpcPlugin.lastRequired);
+
+ cache.putUnlinked(uuid);
+ for (GameMode mode : GameMode.values()) {
+ when(player.getGameMode()).thenReturn(mode);
+ gate.applyGate(player);
+ assertEquals(mode == GameMode.SURVIVAL, RpcPlugin.lastRequired);
+ }
+ }
+
+ @Test
+ void appliesAndClearsOfflineGateAfterRpcEnablesLater() {
+ assertFalse(gate.isRpcAvailable());
+ when(rpc.isEnabled()).thenReturn(true);
+ gate.applyGate(uuid, false);
+ assertEquals(uuid, RpcPlugin.lastUuid);
+ assertEquals(Boolean.TRUE, RpcPlugin.lastRequired);
+
+ gate.clearGate(uuid);
+ assertEquals(Boolean.FALSE, RpcPlugin.lastRequired);
+ }
+
+ @Test
+ void clearGateAlsoResolvesRpcAfterLateEnable() {
+ when(rpc.isEnabled()).thenReturn(true);
+ gate.clearGate(uuid);
+ assertEquals(uuid, RpcPlugin.lastUuid);
+ assertEquals(Boolean.FALSE, RpcPlugin.lastRequired);
+ }
+
+ @Test
+ void toleratesMissingRpcAndStopsCallingDisabledRpc() {
+ when(plugins.getPlugin("RPCharacters")).thenReturn(null);
+ gate.applyGate(player);
+ assertFalse(gate.isRpcAvailable());
+ assertNull(RpcPlugin.lastRequired);
+
+ when(plugins.getPlugin("RPCharacters")).thenReturn(rpc);
+ when(rpc.isEnabled()).thenReturn(true);
+ assertTrue(gate.isRpcAvailable());
+ when(rpc.isEnabled()).thenReturn(false);
+ gate.applyGate(player);
+ gate.applyGate(uuid, false);
+ gate.clearGate(uuid);
+ assertFalse(gate.isRpcAvailable());
+ assertNull(RpcPlugin.lastRequired);
+
+ when(rpc.isEnabled()).thenReturn(true);
+ gate.applyGate(player);
+ assertEquals(Boolean.TRUE, RpcPlugin.lastRequired);
+ }
+
+ public static class RpcPlugin extends JavaPlugin {
+ static UUID lastUuid;
+ static Boolean lastRequired;
+
+ public static void setDiscordGate(UUID uuid, boolean required) {
+ lastUuid = uuid;
+ lastRequired = required;
+ }
+
+ public static void setDiscordGate(Player player, boolean required) {
+ setDiscordGate(player.getUniqueId(), required);
+ }
+ }
+}