diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ConfigureTagResolversListener.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ConfigureTagResolversListener.kt index f9c8fc2..866ff24 100644 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ConfigureTagResolversListener.kt +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ConfigureTagResolversListener.kt @@ -17,8 +17,9 @@ class ConfigureTagResolversListener( val serverName = player?.server?.info?.name ?: "unknown" val ping = player?.ping ?: -1 val pingColors = plugin.proxyPlugin.placeHolderConfiguration.get().pingColors - val onlinePlayers = plugin.proxy.players.size - val realMaxPlayers = plugin.proxy.config.playerLimit + val playerCountHandler = plugin.proxyPlugin.playerCountHandler + val onlinePlayers = playerCountHandler.onlinePlayersOr(plugin.proxy.players.size) + val realMaxPlayers = playerCountHandler.maxPlayersOr(plugin.proxy.config.playerLimit) event.withTagResolvers( TagResolverHelper.getDefaultTagResolvers( diff --git a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ProxyPingListener.kt b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ProxyPingListener.kt index 3e05d5b..fcd004b 100644 --- a/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ProxyPingListener.kt +++ b/proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ProxyPingListener.kt @@ -54,15 +54,17 @@ class ProxyPingListener( } // slots - val onlinePlayers = response.players.online + val playerCountHandler = plugin.proxyPlugin.playerCountHandler + val onlinePlayers = playerCountHandler.onlinePlayersOr(response.players.online) + val realMax = playerCountHandler.maxPlayersOr(response.players.max) val maxPlayers = if (layout.versionSettings.slots.enabled) { when (layout.versionSettings.slots.type) { - MaxPlayerDisplayType.REAL -> response.players.max + MaxPlayerDisplayType.REAL -> realMax MaxPlayerDisplayType.FAKE -> layout.versionSettings.slots.fakeSlots MaxPlayerDisplayType.DYNAMIC -> onlinePlayers + layout.versionSettings.slots.dynamicPlayerRange } } else { - response.players.max + realMax } response.players = Players(maxPlayers, onlinePlayers, samplePlayers) diff --git a/proxy-bungeecord/src/main/resources/config/config.yml b/proxy-bungeecord/src/main/resources/config/config.yml index f8d68c6..749f299 100644 --- a/proxy-bungeecord/src/main/resources/config/config.yml +++ b/proxy-bungeecord/src/main/resources/config/config.yml @@ -32,6 +32,21 @@ whitelist: players: - Notch +# ─────────────────────────────────────────────────────────────────────────────── +# Player Count +# Displays the summed player count for this proxy group and optional extra targets. +# +# Read more @ https://docs.simplecloud.app/manual/plugins/proxy-essentials +# ─────────────────────────────────────────────────────────────────────────────── + +player-count: + # Additional SimpleCloud groups included in the displayed player count. + additional-groups: [] + # Persistent servers included in the displayed player count. + additional-persistent-servers: [] + # Player count update interval in ticks. + update-time: 20 + # ─────────────────────────────────────────────────────────────────────────────── # Tablist # Configures tablist layouts and update intervals for connected players. diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlugin.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlugin.kt index 1ad2565..9ca0cf4 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlugin.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlugin.kt @@ -12,6 +12,7 @@ import app.simplecloud.plugin.proxy.shared.handler.CloudControllerHandler import app.simplecloud.plugin.proxy.shared.handler.JoinStateHandler import app.simplecloud.plugin.proxy.shared.handler.JoinStateResolver import app.simplecloud.plugin.proxy.shared.handler.MotdLayoutHandler +import app.simplecloud.plugin.proxy.shared.handler.PlayerCountHandler import app.simplecloud.plugin.proxy.shared.handler.TabListResolver import java.io.File import java.nio.file.Path @@ -36,10 +37,12 @@ class ProxyPlugin( val motdLayoutHandler = MotdLayoutHandler(File("$dirPath/layout").toPath(), this) val joinStateHandler = JoinStateHandler(this) val cloudControllerHandler = CloudControllerHandler(this, joinStateHandler) + val playerCountHandler = PlayerCountHandler(this).also { it.start() } val joinStateResolver = JoinStateResolver(this) val tabListResolver = TabListResolver { proxyEssentialsConfig.get().tablist } fun shutdown() { + playerCountHandler.stop() joinStateHandler.stop() cloudControllerHandler.close() motdLayoutHandler.close() diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/OldConfigMigrator.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/OldConfigMigrator.kt index d7b3eb4..4e6887a 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/OldConfigMigrator.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/OldConfigMigrator.kt @@ -47,6 +47,7 @@ object OldConfigMigrator { node.node("version").set(CURRENT_CONFIG_VERSION) migrateJoinStates(joinStateNode, node) + node.node("player-count").set(defaultPlayerCount()) migrateTabList(tabListNode, node) node.applyMainConfigComments() @@ -436,6 +437,10 @@ object OldConfigMigrator { node("initial-state").comment(JOIN_STATES_COMMENT) node("whitelist").comment(WHITELIST_COMMENT) node("whitelist", "players").comment("Supports player names and UUIDs.") + node("player-count").comment(PLAYER_COUNT_COMMENT) + node("player-count", "additional-groups").comment("Additional SimpleCloud groups included in the displayed player count.") + node("player-count", "additional-persistent-servers").comment("Persistent servers included in the displayed player count.") + node("player-count", "update-time").comment("Player count update interval in ticks.") node("tablist").comment(TABLIST_COMMENT) node("tablist").childrenList().forEach { group -> group.node("update-time").comment("Update interval in ticks.") @@ -489,6 +494,15 @@ object OldConfigMigrator { } } + private fun defaultPlayerCount(): Map { + val playerCount = ProxyEssentialsConfig().playerCount + return mapOf( + "additional-groups" to playerCount.additionalGroups, + "additional-persistent-servers" to playerCount.additionalPersistentServers, + "update-time" to playerCount.updateTime + ) + } + private fun Path.loadYaml(): CommentedConfigurationNode { return createLoader(this).load() } @@ -506,8 +520,12 @@ object OldConfigMigrator { .insertBefore("initial-state:", JOIN_STATES_YAML_COMMENT) .insertBefore("whitelist:", WHITELIST_YAML_COMMENT) .insertBefore(" players:", " # Supports player names and UUIDs.\n") + .insertBefore("player-count:", PLAYER_COUNT_YAML_COMMENT) + .insertBeforeInSection("player-count:", " additional-groups:", " # Additional SimpleCloud groups included in the displayed player count.\n") + .insertBeforeInSection("player-count:", " additional-persistent-servers:", " # Persistent servers included in the displayed player count.\n") + .insertBeforeInSection("player-count:", " update-time:", " # Player count update interval in ticks.\n") .insertBefore("tablist:", TABLIST_YAML_COMMENT) - .insertBefore(" update-time:", " # Update interval in ticks.\n") + .insertBeforeInSection("tablist:", " update-time:", " # Update interval in ticks.\n") } private fun decorateLayoutYaml(content: String): String { @@ -538,6 +556,22 @@ object OldConfigMigrator { return lines.joinToString("\n").trimEnd() + "\n" } + private fun String.insertBeforeInSection(sectionPrefix: String, linePrefix: String, comment: String): String { + if (comment.isEmpty() || contains(comment.trimEnd())) return this + val lines = lines().toMutableList() + val sectionIndex = lines.indexOfFirst { it.startsWith(sectionPrefix) } + if (sectionIndex == -1) return this + + val relativeIndex = lines + .drop(sectionIndex + 1) + .indexOfFirst { it.startsWith(linePrefix) } + if (relativeIndex == -1) return this + + val commentLines = comment.trimEnd().lines() + lines.addAll(sectionIndex + 1 + relativeIndex, commentLines) + return lines.joinToString("\n").trimEnd() + "\n" + } + private fun createLoader(path: Path): YamlConfigurationLoader { return YamlConfigurationLoader.builder() .path(path) @@ -563,6 +597,13 @@ object OldConfigMigrator { "Prefer permission-based access for regular users.\n" + "Use this list only for administrators or emergency access." + private const val PLAYER_COUNT_COMMENT = + "───────────────────────────────────────────────────────────────────────────────\n" + + "Player Count\n" + + "Displays the summed player count for this proxy group and optional extra targets.\n\n" + + "Read more @ https://docs.simplecloud.app/manual/plugins/proxy-essentials\n" + + "───────────────────────────────────────────────────────────────────────────────" + private const val TABLIST_COMMENT = "───────────────────────────────────────────────────────────────────────────────\n" + "Tablist\n" + @@ -610,6 +651,14 @@ object OldConfigMigrator { "# Prefer permission-based access for regular users.\n" + "# Use this list only for administrators or emergency access.\n" + private const val PLAYER_COUNT_YAML_COMMENT = + "\n# ───────────────────────────────────────────────────────────────────────────────\n" + + "# Player Count\n" + + "# Displays the summed player count for this proxy group and optional extra targets.\n" + + "#\n" + + "# Read more @ https://docs.simplecloud.app/manual/plugins/proxy-essentials\n" + + "# ───────────────────────────────────────────────────────────────────────────────\n" + private const val TABLIST_YAML_COMMENT = "\n# ───────────────────────────────────────────────────────────────────────────────\n" + "# Tablist\n" + diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/PlayerCountConfig.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/PlayerCountConfig.kt new file mode 100644 index 0000000..f2bbe49 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/PlayerCountConfig.kt @@ -0,0 +1,11 @@ +package app.simplecloud.plugin.proxy.shared.config + +import org.spongepowered.configurate.objectmapping.ConfigSerializable +import org.spongepowered.configurate.objectmapping.meta.Setting + +@ConfigSerializable +data class PlayerCountConfig( + @Setting("additional-groups") val additionalGroups: List = emptyList(), + @Setting("additional-persistent-servers") val additionalPersistentServers: List = emptyList(), + @Setting("update-time") val updateTime: Long = 20L +) diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/ProxyEssentialsConfig.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/ProxyEssentialsConfig.kt index ecebb71..79609b0 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/ProxyEssentialsConfig.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/config/ProxyEssentialsConfig.kt @@ -25,6 +25,7 @@ data class ProxyEssentialsConfig( ) ), val whitelist: WhitelistConfig = WhitelistConfig(), + @Setting("player-count") val playerCount: PlayerCountConfig = PlayerCountConfig(), val tablist: List = listOf( TabListGroup( name = "global", @@ -37,4 +38,8 @@ data class ProxyEssentialsConfig( val ticks = tablist.minOfOrNull { it.updateTime } ?: 20L return ticks * 50L } + + fun playerCountUpdateTimeMillis(): Long { + return playerCount.updateTime.coerceAtLeast(1L) * 50L + } } diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/CloudControllerHandler.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/CloudControllerHandler.kt index 98dd371..7dcefe4 100644 --- a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/CloudControllerHandler.kt +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/CloudControllerHandler.kt @@ -113,6 +113,13 @@ class CloudControllerHandler( null } + suspend fun getServerById(serverId: String): Server? = try { + plugin.api.server().getServerById(serverId).await() + } catch (e: Exception) { + logger.severe("Error retrieving server by ID '$serverId': ${e.message}") + null + } + suspend fun getServersByGroup(groupName: String): List = try { plugin.api.server().getAllServers( ServerQuery.create() @@ -198,4 +205,18 @@ class CloudControllerHandler( null } } + + suspend fun getPersistentServersByNames(names: Set): List { + if (names.isEmpty()) { + return emptyList() + } + + return try { + val servers = plugin.api.server().getAllServers(ServerQuery.create()).await() ?: return emptyList() + servers.filter { server -> server.persistentServer?.name?.let { it in names } == true } + } catch (e: Exception) { + logger.severe("Error retrieving persistent servers by name: ${e.message}") + emptyList() + } + } } diff --git a/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/PlayerCountHandler.kt b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/PlayerCountHandler.kt new file mode 100644 index 0000000..8e5bbe5 --- /dev/null +++ b/proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/PlayerCountHandler.kt @@ -0,0 +1,131 @@ +package app.simplecloud.plugin.proxy.shared.handler + +import app.simplecloud.api.server.Server +import app.simplecloud.plugin.proxy.shared.ProxyPlugin +import kotlinx.coroutines.* +import java.util.logging.Logger + +class PlayerCountHandler( + private val proxyPlugin: ProxyPlugin +) { + private val logger = Logger.getLogger(PlayerCountHandler::class.java.name) + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private var syncJob: Job? = null + + @Volatile + private var snapshot: PlayerCountSnapshot? = null + + fun start() { + if (syncJob?.isActive == true) { + return + } + + syncJob = scope.launch { + while (isActive) { + refresh() + delay(proxyPlugin.proxyEssentialsConfig.get().playerCountUpdateTimeMillis()) + } + } + } + + fun stop() { + syncJob?.cancel() + scope.cancel() + } + + fun onlinePlayersOr(fallback: Int): Int { + return snapshot?.onlinePlayers ?: fallback + } + + fun maxPlayersOr(fallback: Int): Int { + return snapshot?.maxPlayers?.takeIf { it > 0 } ?: fallback + } + + private suspend fun refresh() { + try { + snapshot = resolveSnapshot() + } catch (e: Exception) { + logger.severe("Error while syncing player count: ${e.message}") + } + } + + private suspend fun resolveSnapshot(): PlayerCountSnapshot? { + val snapshots = resolveCountSnapshots() + + if (snapshots.isEmpty()) { + return null + } + + return PlayerCountSnapshot( + onlinePlayers = snapshots.sumOf { it.onlinePlayers }, + maxPlayers = snapshots.sumOf { it.maxPlayers ?: 0 }.takeIf { it > 0 } + ) + } + + private suspend fun resolveCountSnapshots(): List { + val currentServer = proxyPlugin.cloudControllerHandler.currentServer + val config = proxyPlugin.proxyEssentialsConfig.get().playerCount + val currentGroupName = currentServer + ?.takeIf { it.isFromGroup } + ?.group + ?.name + val currentPersistentServerName = currentServer + ?.takeIf { it.isFromPersistentServer } + ?.persistentServer + ?.name + + val groupNames = ( + listOfNotNull(currentGroupName) + + config.additionalGroups + ) + .map { it.trim() } + .filter { it.isNotEmpty() } + .distinct() + val groupSnapshots = groupNames.mapNotNull { resolveGroupSnapshot(it) } + + val additionalPersistentServerNames = config.additionalPersistentServers + .map { it.trim() } + .filter { it.isNotEmpty() } + .filter { it != currentPersistentServerName } + .toSet() + val persistentServerSnapshots = proxyPlugin.cloudControllerHandler + .getPersistentServersByNames(additionalPersistentServerNames) + .map { resolveServerSnapshot(it) } + + return listOfNotNull(resolveCurrentServerSnapshot(currentServer)) + groupSnapshots + persistentServerSnapshots + } + + private suspend fun resolveCurrentServerSnapshot(currentServer: Server?): PlayerCountSnapshot? { + if (currentServer == null || currentServer.isFromGroup) { + return null + } + + val server = proxyPlugin.cloudControllerHandler.getServerById(currentServer.serverId) ?: currentServer + return resolveServerSnapshot(server) + } + + private suspend fun resolveGroupSnapshot(groupName: String): PlayerCountSnapshot? { + val onlinePlayers = proxyPlugin.cloudControllerHandler.getOnlinePlayersInGroup(groupName) + val maxPlayers = proxyPlugin.cloudControllerHandler.getMaxPlayersInGroup(groupName) + if (onlinePlayers <= 0 && maxPlayers <= 0) { + return null + } + + return PlayerCountSnapshot( + onlinePlayers = onlinePlayers, + maxPlayers = maxPlayers.takeIf { it > 0 } + ) + } + + private fun resolveServerSnapshot(server: Server): PlayerCountSnapshot { + return PlayerCountSnapshot( + onlinePlayers = server.playerCount?.toInt() ?: 0, + maxPlayers = server.maxPlayers?.toInt()?.takeIf { it > 0 } + ) + } + + private data class PlayerCountSnapshot( + val onlinePlayers: Int, + val maxPlayers: Int? + ) +} diff --git a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ConfigureTagResolversListener.kt b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ConfigureTagResolversListener.kt index 1cb5fec..6d11af5 100644 --- a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ConfigureTagResolversListener.kt +++ b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ConfigureTagResolversListener.kt @@ -19,8 +19,9 @@ class ConfigureTagResolversListener( val serverName = player?.currentServer?.getOrNull()?.serverInfo?.name ?: "unknown" val ping = player?.ping ?: -1 val pingColors = proxyPlugin.placeHolderConfiguration.get().pingColors - val onlinePlayers = plugin.proxyServer.allPlayers.size - val realMaxPlayers = plugin.proxyServer.configuration.showMaxPlayers + val playerCountHandler = proxyPlugin.playerCountHandler + val onlinePlayers = playerCountHandler.onlinePlayersOr(plugin.proxyServer.allPlayers.size) + val realMaxPlayers = playerCountHandler.maxPlayersOr(plugin.proxyServer.configuration.showMaxPlayers) event.withTagResolvers( TagResolverHelper.getDefaultTagResolvers( diff --git a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ProxyPingListener.kt b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ProxyPingListener.kt index eab3d29..b98c0df 100644 --- a/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ProxyPingListener.kt +++ b/proxy-velocity/src/main/kotlin/app/simplecloud/plugin/proxy/velocity/listener/ProxyPingListener.kt @@ -49,8 +49,9 @@ class ProxyPingListener( // player list (hover text) if (!isLocalPing) { val players = event.ping.players.getOrNull() - val onlinePlayers = players?.online ?: 0 - val realMax = players?.max ?: 0 + val playerCountHandler = proxyPlugin.playerCountHandler + val onlinePlayers = playerCountHandler.onlinePlayersOr(players?.online ?: 0) + val realMax = playerCountHandler.maxPlayersOr(players?.max ?: 0) val samplePlayers: List = if (layout.playerList.enabled && layout.playerList.entries.isNotEmpty()) { layout.playerList.entries.map { SamplePlayer(it, UUID.randomUUID()) } diff --git a/proxy-velocity/src/main/resources/config/config.yml b/proxy-velocity/src/main/resources/config/config.yml index f8d68c6..749f299 100644 --- a/proxy-velocity/src/main/resources/config/config.yml +++ b/proxy-velocity/src/main/resources/config/config.yml @@ -32,6 +32,21 @@ whitelist: players: - Notch +# ─────────────────────────────────────────────────────────────────────────────── +# Player Count +# Displays the summed player count for this proxy group and optional extra targets. +# +# Read more @ https://docs.simplecloud.app/manual/plugins/proxy-essentials +# ─────────────────────────────────────────────────────────────────────────────── + +player-count: + # Additional SimpleCloud groups included in the displayed player count. + additional-groups: [] + # Persistent servers included in the displayed player count. + additional-persistent-servers: [] + # Player count update interval in ticks. + update-time: 20 + # ─────────────────────────────────────────────────────────────────────────────── # Tablist # Configures tablist layouts and update intervals for connected players.