Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
15 changes: 15 additions & 0 deletions proxy-bungeecord/src/main/resources/config/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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.")
Expand Down Expand Up @@ -489,6 +494,15 @@ object OldConfigMigrator {
}
}

private fun defaultPlayerCount(): Map<String, Any> {
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()
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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" +
Expand Down Expand Up @@ -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" +
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> = emptyList(),
@Setting("additional-persistent-servers") val additionalPersistentServers: List<String> = emptyList(),
@Setting("update-time") val updateTime: Long = 20L
)
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ data class ProxyEssentialsConfig(
)
),
val whitelist: WhitelistConfig = WhitelistConfig(),
@Setting("player-count") val playerCount: PlayerCountConfig = PlayerCountConfig(),
val tablist: List<TabListGroup> = listOf(
TabListGroup(
name = "global",
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Server> = try {
plugin.api.server().getAllServers(
ServerQuery.create()
Expand Down Expand Up @@ -198,4 +205,18 @@ class CloudControllerHandler(
null
}
}

suspend fun getPersistentServersByNames(names: Set<String>): List<Server> {
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()
}
}
}
Original file line number Diff line number Diff line change
@@ -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<PlayerCountSnapshot> {
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?
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<SamplePlayer> = if (layout.playerList.enabled && layout.playerList.entries.isNotEmpty()) {
layout.playerList.entries.map { SamplePlayer(it, UUID.randomUUID()) }
Expand Down
Loading