diff --git a/api/src/main/java/app/simplecloud/api/internal/integration/presence/ProxyPresenceResponder.java b/api/src/main/java/app/simplecloud/api/internal/integration/presence/PresenceResponder.java similarity index 59% rename from api/src/main/java/app/simplecloud/api/internal/integration/presence/ProxyPresenceResponder.java rename to api/src/main/java/app/simplecloud/api/internal/integration/presence/PresenceResponder.java index ea794c4..5824376 100644 --- a/api/src/main/java/app/simplecloud/api/internal/integration/presence/ProxyPresenceResponder.java +++ b/api/src/main/java/app/simplecloud/api/internal/integration/presence/PresenceResponder.java @@ -1,7 +1,7 @@ package app.simplecloud.api.internal.integration.presence; -import app.simplecloud.api.presence.ProxyPresencePlayer; -import app.simplecloud.api.presence.ProxyPresencePlayerProvider; +import app.simplecloud.api.presence.PresencePlayer; +import app.simplecloud.api.presence.PresencePlayerProvider; import build.buf.gen.simplecloud.controller.v2.PresenceCompareRequest; import build.buf.gen.simplecloud.controller.v2.ProxyPresenceCompareResponse; import io.nats.client.Connection; @@ -18,11 +18,11 @@ import java.util.logging.Logger; /** - * Responds to controller presence-compare requests for a single proxy. + * Responds to controller presence-compare requests for a single proxy or game server. */ -public final class ProxyPresenceResponder { +public final class PresenceResponder { - private static final Logger LOGGER = Logger.getLogger(ProxyPresenceResponder.class.getName()); + private static final Logger LOGGER = Logger.getLogger(PresenceResponder.class.getName()); private static final int FNV_32A_OFFSET_BASIS = 0x811c9dc5; private static final int FNV_32A_PRIME = 0x01000193; @@ -30,15 +30,15 @@ public final class ProxyPresenceResponder { private final String serverId; private final String subject; private final AtomicBoolean running = new AtomicBoolean(false); - private volatile ProxyPresencePlayerProvider playerProvider; + private volatile PresencePlayerProvider playerProvider; private Dispatcher dispatcher; - public ProxyPresenceResponder( + public PresenceResponder( Connection natsConnection, String networkId, String serverId, - ProxyPresencePlayerProvider playerProvider + PresencePlayerProvider playerProvider ) { this.natsConnection = Objects.requireNonNull(natsConnection, "natsConnection"); this.serverId = serverId == null ? "" : serverId; @@ -46,7 +46,7 @@ public ProxyPresenceResponder( this.playerProvider = playerProvider; } - public ProxyPresenceResponder( + public PresenceResponder( Connection natsConnection, String networkId, String serverId @@ -67,7 +67,7 @@ public void start() { dispatcher.subscribe(subject, this::handleCompareRequest); } - public void registerPlayerProvider(ProxyPresencePlayerProvider playerProvider) { + public void registerPlayerProvider(PresencePlayerProvider playerProvider) { this.playerProvider = Objects.requireNonNull(playerProvider, "playerProvider"); } @@ -92,52 +92,65 @@ private void handleCompareRequest(Message message) { try { PresenceCompareRequest request = PresenceCompareRequest.parseFrom(message.getData()); - List players = currentPlayers(); - int localHash = computeHash(players); - boolean match = localHash == request.getHash(); - - ProxyPresenceCompareResponse.Builder response = ProxyPresenceCompareResponse.newBuilder() - .setMatch(match); - - if (!match) { - response.addAllPlayers(players.stream().map(ProxyPresencePlayer::toProto).toList()); - } - - natsConnection.publish(replyTo, response.build().toByteArray()); + List players = currentPlayers(); + natsConnection.publish(replyTo, buildResponse(request, players).toByteArray()); } catch (Exception e) { LOGGER.log(Level.WARNING, "Failed to process presence compare request for " + subject, e); } } - private List currentPlayers() { - ProxyPresencePlayerProvider currentProvider = playerProvider; + private List currentPlayers() { + PresencePlayerProvider currentProvider = playerProvider; if (currentProvider == null) { return List.of(); } - Collection suppliedPlayers = currentProvider.getProxyPresencePlayers(); + Collection suppliedPlayers = currentProvider.getPresencePlayers(); if (suppliedPlayers == null || suppliedPlayers.isEmpty()) { return List.of(); } return suppliedPlayers.stream() .filter(Objects::nonNull) - .sorted(Comparator.comparing(ProxyPresencePlayer::hashRecord)) + .sorted(Comparator.comparing(PresencePlayer::hashRecord)) .toList(); } - static int computeHash(Collection players) { + static ProxyPresenceCompareResponse buildResponse( + PresenceCompareRequest request, + Collection players + ) { + Objects.requireNonNull(request, "request"); + List currentPlayers = (players == null ? List.of() : players).stream() + .filter(Objects::nonNull) + .sorted(Comparator.comparing(PresencePlayer::hashRecord)) + .toList(); + boolean match = computeHash(currentPlayers) == request.getHash(); + + ProxyPresenceCompareResponse.Builder response = ProxyPresenceCompareResponse.newBuilder() + .setMatch(match); + if (!match) { + response.addAllPlayers(currentPlayers.stream().map(PresencePlayer::toProto).toList()); + } + return response.build(); + } + + static int computeHash(Collection players) { if (players == null || players.isEmpty()) { return 0; } + List records = players.stream() + .filter(Objects::nonNull) + .map(PresencePlayer::hashRecord) + .sorted() + .toList(); + String payload = records.size() + "\u001e" + String.join("\u001e", records); + int hash = FNV_32A_OFFSET_BASIS; - for (ProxyPresencePlayer player : players) { - byte[] bytes = player.hashRecord().getBytes(StandardCharsets.UTF_8); - for (byte currentByte : bytes) { - hash ^= currentByte & 0xff; - hash *= FNV_32A_PRIME; - } + for (byte currentByte : payload.getBytes(StandardCharsets.UTF_8)) { + hash ^= currentByte & 0xff; + hash *= FNV_32A_PRIME; } return hash; } diff --git a/api/src/main/java/app/simplecloud/api/internal/integration/presence/ProxyPresenceTracker.java b/api/src/main/java/app/simplecloud/api/internal/integration/presence/ProxyPresenceTracker.java index 613376c..46f854b 100644 --- a/api/src/main/java/app/simplecloud/api/internal/integration/presence/ProxyPresenceTracker.java +++ b/api/src/main/java/app/simplecloud/api/internal/integration/presence/ProxyPresenceTracker.java @@ -1,6 +1,6 @@ package app.simplecloud.api.internal.integration.presence; -import app.simplecloud.api.presence.ProxyPresencePlayer; +import app.simplecloud.api.presence.PresencePlayer; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -32,7 +32,7 @@ public void remove(String playerId) { players.remove(normalize(playerId)); } - public ProxyPresencePlayer createSnapshot( + public PresencePlayer createSnapshot( String playerId, String name, String displayName, @@ -44,7 +44,7 @@ public ProxyPresencePlayer createSnapshot( String normalizedPlayerId = normalize(playerId); TrackedPlayerMetadata metadata = players.get(normalizedPlayerId); - return new ProxyPresencePlayer( + return new PresencePlayer( normalizedPlayerId, name, displayName, diff --git a/api/src/main/java/app/simplecloud/api/presence/ProxyPresencePlayer.java b/api/src/main/java/app/simplecloud/api/presence/PresencePlayer.java similarity index 94% rename from api/src/main/java/app/simplecloud/api/presence/ProxyPresencePlayer.java rename to api/src/main/java/app/simplecloud/api/presence/PresencePlayer.java index 3117bc1..b41e6ed 100644 --- a/api/src/main/java/app/simplecloud/api/presence/ProxyPresencePlayer.java +++ b/api/src/main/java/app/simplecloud/api/presence/PresencePlayer.java @@ -3,9 +3,9 @@ import build.buf.gen.simplecloud.controller.v2.ProxyPresencePlayerSnapshot; /** - * Immutable player snapshot used for proxy presence reconciliation. + * Immutable player snapshot used for proxy and game-server presence reconciliation. */ -public final class ProxyPresencePlayer { +public final class PresencePlayer { private final String playerId; private final String name; @@ -18,7 +18,7 @@ public final class ProxyPresencePlayer { private final boolean onlineMode; private final String sessionId; - public ProxyPresencePlayer( + public PresencePlayer( String playerId, String name, String displayName, diff --git a/api/src/main/java/app/simplecloud/api/presence/PresencePlayerProvider.java b/api/src/main/java/app/simplecloud/api/presence/PresencePlayerProvider.java new file mode 100644 index 0000000..ad7536a --- /dev/null +++ b/api/src/main/java/app/simplecloud/api/presence/PresencePlayerProvider.java @@ -0,0 +1,11 @@ +package app.simplecloud.api.presence; + +import java.util.Collection; + +/** + * Supplies the current set of players visible to this proxy or game server. + */ +public interface PresencePlayerProvider { + + Collection getPresencePlayers(); +} diff --git a/api/src/main/java/app/simplecloud/api/presence/ProxyPresencePlayerProvider.java b/api/src/main/java/app/simplecloud/api/presence/ProxyPresencePlayerProvider.java deleted file mode 100644 index fc2f36a..0000000 --- a/api/src/main/java/app/simplecloud/api/presence/ProxyPresencePlayerProvider.java +++ /dev/null @@ -1,11 +0,0 @@ -package app.simplecloud.api.presence; - -import java.util.Collection; - -/** - * Supplies the current set of players connected through a proxy. - */ -public interface ProxyPresencePlayerProvider { - - Collection getProxyPresencePlayers(); -} diff --git a/api/src/test/java/app/simplecloud/api/internal/integration/presence/PresenceResponderTest.java b/api/src/test/java/app/simplecloud/api/internal/integration/presence/PresenceResponderTest.java new file mode 100644 index 0000000..26bba33 --- /dev/null +++ b/api/src/test/java/app/simplecloud/api/internal/integration/presence/PresenceResponderTest.java @@ -0,0 +1,96 @@ +package app.simplecloud.api.internal.integration.presence; + +import app.simplecloud.api.presence.PresencePlayer; +import build.buf.gen.simplecloud.controller.v2.PresenceCompareRequest; +import build.buf.gen.simplecloud.controller.v2.ProxyPresenceCompareResponse; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PresenceResponderTest { + + @Test + void returnsMatchWithoutSnapshotWhenSummaryMatches() { + ProxyPresenceCompareResponse response = PresenceResponder.buildResponse( + PresenceCompareRequest.newBuilder().setHash(0).build(), + List.of() + ); + + assertTrue(response.getMatch()); + assertTrue(response.getPlayersList().isEmpty()); + } + + @Test + void returnsEveryLivePlayerWhenSummaryDiffers() { + PresencePlayer player = new PresencePlayer( + "player-1", + "PlayerOne", + "PlayerOne", + "", + "Proxy-1", + 1L, + "en_US", + 765, + true, + "session-1" + ); + + ProxyPresenceCompareResponse response = PresenceResponder.buildResponse( + PresenceCompareRequest.newBuilder().setHash(0).build(), + List.of(player) + ); + + assertFalse(response.getMatch()); + assertTrue(response.getPlayersList().stream().anyMatch(snapshot -> snapshot.getPlayerId().equals("player-1"))); + } + + @Test + void equalCountsWithDifferentPlayersProduceDifferentHashes() { + PresencePlayer first = player("player-1"); + PresencePlayer second = player("player-2"); + + assertNotEquals( + PresenceResponder.computeHash(List.of(first)), + PresenceResponder.computeHash(List.of(second)) + ); + assertEquals(0, PresenceResponder.computeHash(List.of())); + } + + @Test + void hashMatchesControllerWireContract() { + PresencePlayer player = new PresencePlayer( + "player-1", + "PlayerOne", + "PlayerOne", + "Lobby-1", + "", + 0L, + "", + 0, + true, + "" + ); + + assertEquals(1745198624, PresenceResponder.computeHash(List.of(player))); + } + + private static PresencePlayer player(String playerId) { + return new PresencePlayer( + playerId, + playerId, + playerId, + "", + "Proxy-1", + 1L, + "en_US", + 765, + true, + "session-1" + ); + } +} diff --git a/platform/bungeecord/src/main/java/app/simplecloud/api/platform/bungeecord/BungeeCordApiProvider.java b/platform/bungeecord/src/main/java/app/simplecloud/api/platform/bungeecord/BungeeCordApiProvider.java index f902275..24970d3 100644 --- a/platform/bungeecord/src/main/java/app/simplecloud/api/platform/bungeecord/BungeeCordApiProvider.java +++ b/platform/bungeecord/src/main/java/app/simplecloud/api/platform/bungeecord/BungeeCordApiProvider.java @@ -3,10 +3,10 @@ import app.simplecloud.api.CloudApi; import app.simplecloud.api.internal.CloudApiImpl; import app.simplecloud.api.internal.integration.player.PlayerIntegration; -import app.simplecloud.api.internal.integration.presence.ProxyPresenceResponder; +import app.simplecloud.api.internal.integration.presence.PresenceResponder; import app.simplecloud.api.internal.integration.presence.ProxyPresenceTracker; -import app.simplecloud.api.presence.ProxyPresencePlayer; -import app.simplecloud.api.presence.ProxyPresencePlayerProvider; +import app.simplecloud.api.presence.PresencePlayer; +import app.simplecloud.api.presence.PresencePlayerProvider; import app.simplecloud.api.player.CloudPlayer; import app.simplecloud.api.runtime.SimpleCloudRuntime; import app.simplecloud.api.platform.shared.LuckPermsPlayerPropertySynchronizer; @@ -26,7 +26,7 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; -public class BungeeCordApiProvider extends Plugin implements ProxyPresencePlayerProvider { +public class BungeeCordApiProvider extends Plugin implements PresencePlayerProvider { private final BungeeContext fastStatsContext = new BungeeContext.Factory( this, @@ -37,7 +37,7 @@ public class BungeeCordApiProvider extends Plugin implements ProxyPresencePlayer private PlayerSynchronizer playerSynchronizer; private PlayerIntegration playerIntegration; private ProxyPresenceTracker proxyPresenceTracker; - private ProxyPresenceResponder proxyPresenceResponder; + private PresenceResponder presenceResponder; private LuckPermsPlayerPropertySynchronizer luckPermsSynchronizer; private BungeeAudiences bungeeAudiences; @@ -55,7 +55,7 @@ public void onEnable() { ); this.playerIntegration = new PlayerIntegration(cloudApi); this.proxyPresenceTracker = new ProxyPresenceTracker(proxyName); - this.proxyPresenceResponder = new ProxyPresenceResponder( + this.presenceResponder = new PresenceResponder( cloudApi.getNatsConnection(), cloudApi.getNetworkId(), SimpleCloudRuntime.serverId(), @@ -81,14 +81,14 @@ public void onEnable() { playerSynchronizer.start(); playerIntegration.start(); - proxyPresenceResponder.start(); + presenceResponder.start(); fastStatsContext.ready(); } @Override public void onDisable() { getLogger().info("SimpleCloud v3 API provider uninitialized!"); - proxyPresenceResponder.stop(); + presenceResponder.stop(); if (luckPermsSynchronizer != null) { luckPermsSynchronizer.stop(); } @@ -135,13 +135,13 @@ private void forgetLuckPermsProperties(UUID uniqueId) { } @Override - public List getProxyPresencePlayers() { + public List getPresencePlayers() { return getProxy().getPlayers().stream() .map(this::toPresencePlayer) .toList(); } - private ProxyPresencePlayer toPresencePlayer(ProxiedPlayer player) { + private PresencePlayer toPresencePlayer(ProxiedPlayer player) { String connectedServerName = player.getServer() != null ? player.getServer().getInfo().getName() : ""; Locale locale = player.getLocale(); var pendingConnection = player.getPendingConnection(); diff --git a/platform/fabric/build.gradle.kts b/platform/fabric/build.gradle.kts index 36a2414..3113dfd 100644 --- a/platform/fabric/build.gradle.kts +++ b/platform/fabric/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { implementation(rootProject.libs.fabric.loader) implementation(rootProject.libs.fabric.api) + compileOnly(rootProject.libs.jnats) implementation(project(":platform:shared")) implementation(project(":api")) diff --git a/platform/fabric/src/main/java/app/simplecloud/api/platform/fabric/FabricApiProvider.java b/platform/fabric/src/main/java/app/simplecloud/api/platform/fabric/FabricApiProvider.java index 6d083af..94a1202 100644 --- a/platform/fabric/src/main/java/app/simplecloud/api/platform/fabric/FabricApiProvider.java +++ b/platform/fabric/src/main/java/app/simplecloud/api/platform/fabric/FabricApiProvider.java @@ -1,17 +1,25 @@ package app.simplecloud.api.platform.fabric; import app.simplecloud.api.CloudApi; +import app.simplecloud.api.internal.CloudApiImpl; +import app.simplecloud.api.internal.integration.presence.PresenceResponder; import app.simplecloud.api.platform.shared.PlayerSynchronizer; +import app.simplecloud.api.presence.PresencePlayer; +import app.simplecloud.api.presence.PresencePlayerProvider; +import app.simplecloud.api.runtime.SimpleCloudRuntime; import net.fabricmc.api.DedicatedServerModInitializer; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.SharedConstants; import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.List; import java.util.concurrent.atomic.AtomicLong; -public final class FabricApiProvider implements DedicatedServerModInitializer { +public final class FabricApiProvider implements DedicatedServerModInitializer, PresencePlayerProvider { public static final String MOD_ID = "simplecloud_api"; private static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); @@ -19,8 +27,9 @@ public final class FabricApiProvider implements DedicatedServerModInitializer { private final AtomicLong onlinePlayerCount = new AtomicLong(); private MinecraftServer server; - private CloudApi cloudApi; + private CloudApiImpl cloudApi; private PlayerSynchronizer playerSynchronizer; + private PresenceResponder presenceResponder; @Override public void onInitializeServer() { @@ -33,14 +42,27 @@ public void onInitializeServer() { private void onServerStarted(MinecraftServer server) { this.server = server; this.onlinePlayerCount.set(server.getPlayerList().getPlayerCount()); - this.cloudApi = CloudApi.create(); + this.cloudApi = (CloudApiImpl) CloudApi.create(); this.playerSynchronizer = new PlayerSynchronizer(cloudApi, onlinePlayerCount::get); + this.presenceResponder = new PresenceResponder( + cloudApi.getNatsConnection(), + cloudApi.getNetworkId(), + SimpleCloudRuntime.serverId(), + this + ); this.playerSynchronizer.start(); + this.presenceResponder.start(); this.playerSynchronizer.updatePlayerCount(); LOGGER.info("SimpleCloud v3 API provider initialized"); } private void onServerStopping(MinecraftServer server) { + PresenceResponder responder = this.presenceResponder; + if (responder != null) { + responder.stop(); + this.presenceResponder = null; + } + PlayerSynchronizer synchronizer = this.playerSynchronizer; if (synchronizer != null) { synchronizer.stop(); @@ -70,4 +92,37 @@ private void refreshPlayerCount(MinecraftServer eventServer) { } }); } + + @Override + public List getPresencePlayers() { + MinecraftServer currentServer = this.server; + if (currentServer == null) { + return List.of(); + } + + String serverName = SimpleCloudRuntime.serverName(); + return currentServer.getPlayerList().getPlayers().stream() + .map(player -> toPresencePlayer(currentServer, player, serverName)) + .toList(); + } + + private PresencePlayer toPresencePlayer( + MinecraftServer currentServer, + ServerPlayer player, + String serverName + ) { + String name = player.getName().getString(); + return new PresencePlayer( + player.getStringUUID(), + name, + player.getDisplayName().getString(), + serverName, + "", + 0L, + player.clientInformation().language(), + SharedConstants.getProtocolVersion(), + currentServer.usesAuthentication(), + "" + ); + } } diff --git a/platform/folia/src/main/java/app/simplecloud/api/platform/folia/FoliaApiProvider.java b/platform/folia/src/main/java/app/simplecloud/api/platform/folia/FoliaApiProvider.java index e6db5c5..07f06b7 100644 --- a/platform/folia/src/main/java/app/simplecloud/api/platform/folia/FoliaApiProvider.java +++ b/platform/folia/src/main/java/app/simplecloud/api/platform/folia/FoliaApiProvider.java @@ -2,11 +2,18 @@ import app.simplecloud.api.CloudApi; import app.simplecloud.api.internal.CloudApiImpl; +import app.simplecloud.api.internal.integration.presence.PresenceResponder; +import app.simplecloud.api.presence.PresencePlayer; +import app.simplecloud.api.presence.PresencePlayerProvider; +import app.simplecloud.api.runtime.SimpleCloudRuntime; import dev.faststats.Metrics; import dev.faststats.bukkit.BukkitContext; +import org.bukkit.Bukkit; import org.bukkit.plugin.java.JavaPlugin; -public class FoliaApiProvider extends JavaPlugin { +import java.util.List; + +public class FoliaApiProvider extends JavaPlugin implements PresencePlayerProvider { private final BukkitContext fastStatsContext = new BukkitContext.Factory( this, @@ -14,12 +21,20 @@ public class FoliaApiProvider extends JavaPlugin { ).metrics(Metrics.Factory::create).create(); private CloudApiImpl cloudApi; private FoliaAdventureIntegration foliaAdventureIntegration; + private PresenceResponder presenceResponder; @Override public void onEnable() { this.cloudApi = (CloudApiImpl) CloudApi.create(); this.foliaAdventureIntegration = new FoliaAdventureIntegration(this, cloudApi); + this.presenceResponder = new PresenceResponder( + cloudApi.getNatsConnection(), + cloudApi.getNetworkId(), + SimpleCloudRuntime.serverId(), + this + ); this.foliaAdventureIntegration.start(); + this.presenceResponder.start(); fastStatsContext.ready(); getLogger().info("SimpleCloud v3 API provider initialized!"); @@ -27,6 +42,9 @@ public void onEnable() { @Override public void onDisable() { + if (presenceResponder != null) { + presenceResponder.stop(); + } if (foliaAdventureIntegration != null) { foliaAdventureIntegration.stop(); } @@ -37,4 +55,28 @@ public void onDisable() { getLogger().info("SimpleCloud v3 API provider uninitialized!"); } + + @Override + public List getPresencePlayers() { + String serverName = currentServerName(); + return Bukkit.getOnlinePlayers().stream() + .map(player -> new PresencePlayer( + player.getUniqueId().toString(), + player.getName(), + player.getName(), + serverName, + "", + 0L, + player.getLocale(), + 0, + Bukkit.getOnlineMode(), + "" + )) + .toList(); + } + + private String currentServerName() { + String serverName = SimpleCloudRuntime.serverName(); + return serverName == null || serverName.isBlank() ? SimpleCloudRuntime.serverId() : serverName; + } } diff --git a/platform/neoforge/build.gradle.kts b/platform/neoforge/build.gradle.kts index 9e6d0f4..95c7f7b 100644 --- a/platform/neoforge/build.gradle.kts +++ b/platform/neoforge/build.gradle.kts @@ -9,6 +9,7 @@ val shade = configurations.create("shade") dependencies { implementation(rootProject.libs.neoforge) + compileOnly(rootProject.libs.jnats) implementation(project(":platform:shared")) implementation(project(":api")) diff --git a/platform/neoforge/src/main/java/app/simplecloud/api/platform/neoforge/NeoForgeApiProvider.java b/platform/neoforge/src/main/java/app/simplecloud/api/platform/neoforge/NeoForgeApiProvider.java index 44169db..1c4fdc5 100644 --- a/platform/neoforge/src/main/java/app/simplecloud/api/platform/neoforge/NeoForgeApiProvider.java +++ b/platform/neoforge/src/main/java/app/simplecloud/api/platform/neoforge/NeoForgeApiProvider.java @@ -1,7 +1,13 @@ package app.simplecloud.api.platform.neoforge; import app.simplecloud.api.CloudApi; +import app.simplecloud.api.internal.CloudApiImpl; +import app.simplecloud.api.internal.integration.presence.PresenceResponder; import app.simplecloud.api.platform.shared.PlayerSynchronizer; +import app.simplecloud.api.presence.PresencePlayer; +import app.simplecloud.api.presence.PresencePlayerProvider; +import app.simplecloud.api.runtime.SimpleCloudRuntime; +import net.minecraft.SharedConstants; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; import net.neoforged.api.distmarker.Dist; @@ -13,10 +19,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.List; import java.util.concurrent.atomic.AtomicLong; @Mod(value = NeoForgeApiProvider.MOD_ID, dist = Dist.DEDICATED_SERVER) -public final class NeoForgeApiProvider { +public final class NeoForgeApiProvider implements PresencePlayerProvider { public static final String MOD_ID = "simplecloud_api"; private static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); @@ -24,8 +31,9 @@ public final class NeoForgeApiProvider { private final AtomicLong onlinePlayerCount = new AtomicLong(); private MinecraftServer server; - private CloudApi cloudApi; + private CloudApiImpl cloudApi; private PlayerSynchronizer playerSynchronizer; + private PresenceResponder presenceResponder; public NeoForgeApiProvider() { NeoForge.EVENT_BUS.addListener(this::onServerStarted); @@ -37,14 +45,27 @@ public NeoForgeApiProvider() { private void onServerStarted(ServerStartedEvent event) { this.server = event.getServer(); this.onlinePlayerCount.set(server.getPlayerList().getPlayerCount()); - this.cloudApi = CloudApi.create(); + this.cloudApi = (CloudApiImpl) CloudApi.create(); this.playerSynchronizer = new PlayerSynchronizer(cloudApi, onlinePlayerCount::get); + this.presenceResponder = new PresenceResponder( + cloudApi.getNatsConnection(), + cloudApi.getNetworkId(), + SimpleCloudRuntime.serverId(), + this + ); this.playerSynchronizer.start(); + this.presenceResponder.start(); this.playerSynchronizer.updatePlayerCount(); LOGGER.info("SimpleCloud v3 API provider initialized"); } private void onServerStopping(ServerStoppingEvent event) { + PresenceResponder responder = this.presenceResponder; + if (responder != null) { + responder.stop(); + this.presenceResponder = null; + } + PlayerSynchronizer synchronizer = this.playerSynchronizer; if (synchronizer != null) { synchronizer.stop(); @@ -86,4 +107,37 @@ private void refreshPlayerCount(MinecraftServer eventServer) { } }); } + + @Override + public List getPresencePlayers() { + MinecraftServer currentServer = this.server; + if (currentServer == null) { + return List.of(); + } + + String serverName = SimpleCloudRuntime.serverName(); + return currentServer.getPlayerList().getPlayers().stream() + .map(player -> toPresencePlayer(currentServer, player, serverName)) + .toList(); + } + + private PresencePlayer toPresencePlayer( + MinecraftServer currentServer, + ServerPlayer player, + String serverName + ) { + String name = player.getName().getString(); + return new PresencePlayer( + player.getStringUUID(), + name, + player.getDisplayName().getString(), + serverName, + "", + 0L, + player.clientInformation().language(), + SharedConstants.getProtocolVersion(), + currentServer.usesAuthentication(), + "" + ); + } } diff --git a/platform/paper/build.gradle.kts b/platform/paper/build.gradle.kts index 99a220e..36b8a7e 100644 --- a/platform/paper/build.gradle.kts +++ b/platform/paper/build.gradle.kts @@ -4,6 +4,7 @@ plugins { dependencies { compileOnly(rootProject.libs.paper.api) + compileOnly(rootProject.libs.jnats) implementation(project(":platform:shared")) implementation(project(":api")) { exclude(group = "net.kyori") diff --git a/platform/paper/src/main/java/app/simplecloud/api/provider/paper/PaperApiProvider.java b/platform/paper/src/main/java/app/simplecloud/api/provider/paper/PaperApiProvider.java index e12e58a..efb841f 100644 --- a/platform/paper/src/main/java/app/simplecloud/api/provider/paper/PaperApiProvider.java +++ b/platform/paper/src/main/java/app/simplecloud/api/provider/paper/PaperApiProvider.java @@ -1,7 +1,11 @@ package app.simplecloud.api.provider.paper; import app.simplecloud.api.CloudApi; +import app.simplecloud.api.internal.CloudApiImpl; import app.simplecloud.api.internal.integration.adventure.AdventureIntegration; +import app.simplecloud.api.internal.integration.presence.PresenceResponder; +import app.simplecloud.api.presence.PresencePlayer; +import app.simplecloud.api.presence.PresencePlayerProvider; import app.simplecloud.api.runtime.SimpleCloudRuntime; import dev.faststats.Metrics; import dev.faststats.bukkit.BukkitContext; @@ -9,18 +13,21 @@ import org.bukkit.Bukkit; import org.bukkit.plugin.java.JavaPlugin; -public class PaperApiProvider extends JavaPlugin { +import java.util.List; + +public class PaperApiProvider extends JavaPlugin implements PresencePlayerProvider { private final BukkitContext fastStatsContext = new BukkitContext.Factory( this, "2e8308cb6431a46a68fa0f59362978f7" ).metrics(Metrics.Factory::create).create(); - private CloudApi cloudApi; + private CloudApiImpl cloudApi; private AdventureIntegration adventureIntegration; + private PresenceResponder presenceResponder; @Override public void onEnable() { - this.cloudApi = CloudApi.create(); + this.cloudApi = (CloudApiImpl) CloudApi.create(); String serverId = SimpleCloudRuntime.serverId(); String groupName = SimpleCloudRuntime.groupName(); @@ -32,7 +39,14 @@ public void onEnable() { .forServer(serverId) .forGroup(groupName != null ? groupName : serverId) .build(); + this.presenceResponder = new PresenceResponder( + cloudApi.getNatsConnection(), + cloudApi.getNetworkId(), + serverId, + this + ); adventureIntegration.start(); + presenceResponder.start(); fastStatsContext.ready(); getLogger().info("SimpleCloud v3 API provider initialized!"); @@ -40,6 +54,9 @@ public void onEnable() { @Override public void onDisable() { + if (presenceResponder != null) { + presenceResponder.stop(); + } if (adventureIntegration != null) { adventureIntegration.stop(); } @@ -49,4 +66,28 @@ public void onDisable() { fastStatsContext.shutdown(); getLogger().info("SimpleCloud v3 API provider uninitialized!"); } + + @Override + public List getPresencePlayers() { + String serverName = currentServerName(); + return Bukkit.getOnlinePlayers().stream() + .map(player -> new PresencePlayer( + player.getUniqueId().toString(), + player.getName(), + player.getName(), + serverName, + "", + 0L, + player.getLocale(), + 0, + Bukkit.getOnlineMode(), + "" + )) + .toList(); + } + + private String currentServerName() { + String serverName = SimpleCloudRuntime.serverName(); + return serverName == null || serverName.isBlank() ? SimpleCloudRuntime.serverId() : serverName; + } } diff --git a/platform/spigot-legacy/build.gradle.kts b/platform/spigot-legacy/build.gradle.kts index e6dd8aa..815d4ef 100644 --- a/platform/spigot-legacy/build.gradle.kts +++ b/platform/spigot-legacy/build.gradle.kts @@ -5,6 +5,7 @@ plugins { dependencies { api(project(":platform:shared")) compileOnly(rootProject.libs.paper.api) + compileOnly(rootProject.libs.jnats) implementation(project(":api")) implementation(rootProject.libs.faststats.bukkit) } diff --git a/platform/spigot-legacy/src/main/java/app/simplecloud/api/platform/spigot/legacy/LegacySpigotApiProvider.java b/platform/spigot-legacy/src/main/java/app/simplecloud/api/platform/spigot/legacy/LegacySpigotApiProvider.java index d78d17f..e156b6c 100644 --- a/platform/spigot-legacy/src/main/java/app/simplecloud/api/platform/spigot/legacy/LegacySpigotApiProvider.java +++ b/platform/spigot-legacy/src/main/java/app/simplecloud/api/platform/spigot/legacy/LegacySpigotApiProvider.java @@ -1,23 +1,36 @@ package app.simplecloud.api.platform.spigot.legacy; import app.simplecloud.api.CloudApi; +import app.simplecloud.api.internal.CloudApiImpl; +import app.simplecloud.api.internal.integration.presence.PresenceResponder; import app.simplecloud.api.platform.shared.PlayerSynchronizer; +import app.simplecloud.api.presence.PresencePlayer; +import app.simplecloud.api.presence.PresencePlayerProvider; +import app.simplecloud.api.runtime.SimpleCloudRuntime; import dev.faststats.Metrics; import dev.faststats.bukkit.BukkitContext; import org.bukkit.Bukkit; import org.bukkit.plugin.java.JavaPlugin; -public class LegacySpigotApiProvider extends JavaPlugin { +import java.util.List; + +public class LegacySpigotApiProvider extends JavaPlugin implements PresencePlayerProvider { private final BukkitContext fastStatsContext = new BukkitContext.Factory( this, "2e8308cb6431a46a68fa0f59362978f7" ).metrics(Metrics.Factory::create).create(); - private final CloudApi cloudApi = CloudApi.create(); + private final CloudApiImpl cloudApi = (CloudApiImpl) CloudApi.create(); private final PlayerSynchronizer playerSynchronizer = new PlayerSynchronizer( cloudApi, () -> (long) Bukkit.getOnlinePlayers().size() ); + private final PresenceResponder presenceResponder = new PresenceResponder( + cloudApi.getNatsConnection(), + cloudApi.getNetworkId(), + SimpleCloudRuntime.serverId(), + this + ); @Override public void onEnable() { @@ -25,14 +38,40 @@ public void onEnable() { Bukkit.getPluginManager().registerEvents(new PlayerConnectionListener(playerSynchronizer), this); playerSynchronizer.start(); + presenceResponder.start(); fastStatsContext.ready(); } @Override public void onDisable() { getLogger().info("SimpleCloud v3 API provider uninitialized!"); + presenceResponder.stop(); playerSynchronizer.stop(); cloudApi.close(); fastStatsContext.shutdown(); } + + @Override + public List getPresencePlayers() { + String serverName = currentServerName(); + return Bukkit.getOnlinePlayers().stream() + .map(player -> new PresencePlayer( + player.getUniqueId().toString(), + player.getName(), + player.getDisplayName(), + serverName, + "", + 0L, + "", + 0, + Bukkit.getOnlineMode(), + "" + )) + .toList(); + } + + private String currentServerName() { + String serverName = SimpleCloudRuntime.serverName(); + return serverName == null || serverName.isBlank() ? SimpleCloudRuntime.serverId() : serverName; + } } diff --git a/platform/spigot/build.gradle.kts b/platform/spigot/build.gradle.kts index 45bffb4..8f4112b 100644 --- a/platform/spigot/build.gradle.kts +++ b/platform/spigot/build.gradle.kts @@ -5,6 +5,7 @@ plugins { dependencies { api(project(":platform:shared")) compileOnly(rootProject.libs.paper.api) + compileOnly(rootProject.libs.jnats) implementation(project(":api")) implementation(rootProject.libs.faststats.bukkit) } diff --git a/platform/spigot/src/main/java/app/simplecloud/api/platform/spigot/SpigotApiProvider.java b/platform/spigot/src/main/java/app/simplecloud/api/platform/spigot/SpigotApiProvider.java index 035d6c3..b56f9a4 100644 --- a/platform/spigot/src/main/java/app/simplecloud/api/platform/spigot/SpigotApiProvider.java +++ b/platform/spigot/src/main/java/app/simplecloud/api/platform/spigot/SpigotApiProvider.java @@ -1,23 +1,36 @@ package app.simplecloud.api.platform.spigot; import app.simplecloud.api.CloudApi; +import app.simplecloud.api.internal.CloudApiImpl; +import app.simplecloud.api.internal.integration.presence.PresenceResponder; import app.simplecloud.api.platform.shared.PlayerSynchronizer; +import app.simplecloud.api.presence.PresencePlayer; +import app.simplecloud.api.presence.PresencePlayerProvider; +import app.simplecloud.api.runtime.SimpleCloudRuntime; import dev.faststats.Metrics; import dev.faststats.bukkit.BukkitContext; import org.bukkit.Bukkit; import org.bukkit.plugin.java.JavaPlugin; -public class SpigotApiProvider extends JavaPlugin { +import java.util.List; + +public class SpigotApiProvider extends JavaPlugin implements PresencePlayerProvider { private final BukkitContext fastStatsContext = new BukkitContext.Factory( this, "2e8308cb6431a46a68fa0f59362978f7" ).metrics(Metrics.Factory::create).create(); - private final CloudApi cloudApi = CloudApi.create(); + private final CloudApiImpl cloudApi = (CloudApiImpl) CloudApi.create(); private final PlayerSynchronizer playerSynchronizer = new PlayerSynchronizer( cloudApi, () -> (long) Bukkit.getOnlinePlayers().size() ); + private final PresenceResponder presenceResponder = new PresenceResponder( + cloudApi.getNatsConnection(), + cloudApi.getNetworkId(), + SimpleCloudRuntime.serverId(), + this + ); @Override public void onEnable() { @@ -25,14 +38,40 @@ public void onEnable() { Bukkit.getPluginManager().registerEvents(new PlayerConnectionListener(playerSynchronizer), this); playerSynchronizer.start(); + presenceResponder.start(); fastStatsContext.ready(); } @Override public void onDisable() { getLogger().info("SimpleCloud v3 API provider uninitialized!"); + presenceResponder.stop(); playerSynchronizer.stop(); cloudApi.close(); fastStatsContext.shutdown(); } + + @Override + public List getPresencePlayers() { + String serverName = currentServerName(); + return Bukkit.getOnlinePlayers().stream() + .map(player -> new PresencePlayer( + player.getUniqueId().toString(), + player.getName(), + player.getDisplayName(), + serverName, + "", + 0L, + player.getLocale(), + 0, + Bukkit.getOnlineMode(), + "" + )) + .toList(); + } + + private String currentServerName() { + String serverName = SimpleCloudRuntime.serverName(); + return serverName == null || serverName.isBlank() ? SimpleCloudRuntime.serverId() : serverName; + } } diff --git a/platform/velocity/src/main/java/app/simplecloud/api/platform/velocity/CloudApiVelocityPlugin.java b/platform/velocity/src/main/java/app/simplecloud/api/platform/velocity/CloudApiVelocityPlugin.java index 9ed8406..fd0d6c1 100644 --- a/platform/velocity/src/main/java/app/simplecloud/api/platform/velocity/CloudApiVelocityPlugin.java +++ b/platform/velocity/src/main/java/app/simplecloud/api/platform/velocity/CloudApiVelocityPlugin.java @@ -3,10 +3,10 @@ import app.simplecloud.api.CloudApi; import app.simplecloud.api.internal.CloudApiImpl; import app.simplecloud.api.internal.integration.player.PlayerIntegration; -import app.simplecloud.api.internal.integration.presence.ProxyPresenceResponder; +import app.simplecloud.api.internal.integration.presence.PresenceResponder; import app.simplecloud.api.internal.integration.presence.ProxyPresenceTracker; -import app.simplecloud.api.presence.ProxyPresencePlayer; -import app.simplecloud.api.presence.ProxyPresencePlayerProvider; +import app.simplecloud.api.presence.PresencePlayer; +import app.simplecloud.api.presence.PresencePlayerProvider; import app.simplecloud.api.player.CloudPlayer; import app.simplecloud.api.runtime.SimpleCloudRuntime; import app.simplecloud.api.platform.shared.LuckPermsPlayerPropertySynchronizer; @@ -38,7 +38,7 @@ authors = {"Fllip"}, dependencies = {@Dependency(id = "luckperms", optional = true)} ) -public class CloudApiVelocityPlugin implements ProxyPresencePlayerProvider { +public class CloudApiVelocityPlugin implements PresencePlayerProvider { private final Logger logger; private final ProxyServer proxyServer; @@ -48,7 +48,7 @@ public class CloudApiVelocityPlugin implements ProxyPresencePlayerProvider { private final PlayerSynchronizer playerSynchronizer; private final PlayerIntegration playerIntegration; private final ProxyPresenceTracker proxyPresenceTracker; - private final ProxyPresenceResponder proxyPresenceResponder; + private final PresenceResponder presenceResponder; private LuckPermsPlayerPropertySynchronizer luckPermsSynchronizer; private final GsonComponentSerializer gsonComponentSerializer = GsonComponentSerializer.gson(); @@ -73,7 +73,7 @@ public CloudApiVelocityPlugin( ); this.playerIntegration = new PlayerIntegration(cloudApi); this.proxyPresenceTracker = new ProxyPresenceTracker(proxyName); - this.proxyPresenceResponder = new ProxyPresenceResponder( + this.presenceResponder = new PresenceResponder( cloudApi.getNatsConnection(), cloudApi.getNetworkId(), SimpleCloudRuntime.serverId(), @@ -99,14 +99,14 @@ public void onProxyInitialize(ProxyInitializeEvent event) { playerSynchronizer.start(); playerIntegration.start(); - proxyPresenceResponder.start(); + presenceResponder.start(); fastStatsContext.ready(); } @Subscribe public void onProxyShutdown(ProxyShutdownEvent event) { logger.info("SimpleCloud v3 API provider uninitialized!"); - proxyPresenceResponder.stop(); + presenceResponder.stop(); if (luckPermsSynchronizer != null) { luckPermsSynchronizer.stop(); } @@ -149,13 +149,13 @@ private void forgetLuckPermsProperties(UUID uniqueId) { } @Override - public List getProxyPresencePlayers() { + public List getPresencePlayers() { return proxyServer.getAllPlayers().stream() .map(this::toPresencePlayer) .toList(); } - private ProxyPresencePlayer toPresencePlayer(Player player) { + private PresencePlayer toPresencePlayer(Player player) { String connectedServerName = player.getCurrentServer() .map(serverConnection -> serverConnection.getServerInfo().getName()) .orElse("");