-
Notifications
You must be signed in to change notification settings - Fork 0
feat: compact tab ranks and mark Bedrock players #58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
45 changes: 45 additions & 0 deletions
45
velocity/src/main/kotlin/gg/grounds/proxy/velocity/tab/BedrockPlayers.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package gg.grounds.proxy.velocity.tab | ||
|
|
||
| import java.util.UUID | ||
| import org.slf4j.Logger | ||
|
|
||
| /** Optional Floodgate lookup; this plugin also runs on proxies without Floodgate. */ | ||
| internal class BedrockPlayers(private val logger: Logger, private val lookup: () -> Class<*>?) { | ||
| @Volatile private var api: Class<*>? = null | ||
|
|
||
| fun isBedrock(id: UUID): Boolean { | ||
| val type = api ?: lookup()?.also { api = it } | ||
| if (type != null) { | ||
| try { | ||
| val instance = type.getMethod("getInstance").invoke(null) | ||
| if (instance != null) { | ||
| return type | ||
| .getMethod("isFloodgatePlayer", UUID::class.java) | ||
| .invoke(instance, id) == true | ||
| } | ||
| } catch (failure: ReflectiveOperationException) { | ||
| logger.debug("Could not read a player's edition from Floodgate", failure) | ||
| } | ||
| } | ||
| return isUnlinkedUuid(id) | ||
| } | ||
|
|
||
| companion object { | ||
| // Floodgate represents an unlinked XUID in the UUID's lower 64 bits. | ||
| fun isUnlinkedUuid(id: UUID): Boolean = | ||
| id.mostSignificantBits == 0L && id.leastSignificantBits != 0L | ||
|
|
||
| fun of(logger: Logger): BedrockPlayers = | ||
| BedrockPlayers(logger) { | ||
| try { | ||
| Class.forName( | ||
| "org.geysermc.floodgate.api.FloodgateApi", | ||
| false, | ||
| BedrockPlayers::class.java.classLoader, | ||
| ) | ||
| } catch (_: ClassNotFoundException) { | ||
| null | ||
| } | ||
| } | ||
| } | ||
| } |
106 changes: 106 additions & 0 deletions
106
velocity/src/main/kotlin/gg/grounds/proxy/velocity/tab/BedrockRoster.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| package gg.grounds.proxy.velocity.tab | ||
|
|
||
| import com.google.gson.JsonObject | ||
| import com.google.gson.JsonParser | ||
| import java.util.UUID | ||
| import java.util.concurrent.ConcurrentHashMap | ||
|
|
||
| /** Short-lived edition snapshots let Java proxies label linked Bedrock accounts too. */ | ||
| internal class BedrockRoster( | ||
| private val proxyId: String, | ||
| private val connectedPlayers: () -> Collection<UUID>, | ||
| private val detect: (UUID) -> Boolean, | ||
| private val publish: (String) -> Unit, | ||
| private val nowMillis: () -> Long = { System.nanoTime() / 1_000_000 }, | ||
| ) { | ||
| private data class Snapshot(val players: Map<UUID, Boolean>, val receivedAt: Long) | ||
|
|
||
| private val remote = ConcurrentHashMap<String, Snapshot>() | ||
| @Volatile private var local: Map<UUID, Boolean> = emptyMap() | ||
|
|
||
| fun refresh() { | ||
| local = connectedPlayers().associateWith(detect) | ||
| expire() | ||
| send(local) | ||
| } | ||
|
|
||
| fun close() { | ||
| local = emptyMap() | ||
| send(emptyMap()) | ||
| remote.clear() | ||
| } | ||
|
|
||
| fun isBedrock(id: UUID): Boolean { | ||
| local[id]?.let { | ||
| return it | ||
| } | ||
| val now = nowMillis() | ||
| val current = | ||
| remote.values | ||
| .filter { now - it.receivedAt < EXPIRY_MILLIS && id in it.players } | ||
| .maxByOrNull { it.receivedAt } | ||
| return current?.players?.get(id) ?: BedrockPlayers.isUnlinkedUuid(id) | ||
| } | ||
|
|
||
| fun receive(raw: String) { | ||
| if (raw.length > MAX_PAYLOAD) return | ||
| val parsed = | ||
| try { | ||
| val root = JsonParser.parseString(raw) | ||
| if (!root.isJsonObject) return | ||
| val obj = root.asJsonObject | ||
| if (obj.get("schemaVersion")?.asInt != 1) return | ||
| val owner = | ||
| obj.get("proxy") | ||
| ?.takeIf { it.isJsonPrimitive && it.asJsonPrimitive.isString } | ||
| ?.asString ?: return | ||
| if (owner == proxyId || !owner.matches(PROXY_ID)) return | ||
| val entries = obj.get("players")?.takeIf { it.isJsonObject }?.asJsonObject ?: return | ||
| if (entries.size() > MAX_PLAYERS) return | ||
| val players = | ||
| entries.entrySet().associate { (key, value) -> | ||
| val id = UUID.fromString(key) | ||
| if ( | ||
| id.toString() != key || | ||
| !value.isJsonPrimitive || | ||
| !value.asJsonPrimitive.isBoolean | ||
| ) | ||
| return | ||
| id to value.asBoolean | ||
| } | ||
| owner to Snapshot(players, nowMillis()) | ||
| } catch (_: RuntimeException) { | ||
| return | ||
| } | ||
| expire() | ||
| if (!remote.containsKey(parsed.first) && remote.size >= MAX_PROXIES) return | ||
| remote[parsed.first] = parsed.second | ||
| } | ||
|
|
||
| private fun send(players: Map<UUID, Boolean>) { | ||
| if (!proxyId.matches(PROXY_ID) || players.size > MAX_PLAYERS) return | ||
| val entries = JsonObject() | ||
| players.forEach { (id, bedrock) -> entries.addProperty(id.toString(), bedrock) } | ||
| val root = JsonObject() | ||
| root.addProperty("schemaVersion", 1) | ||
| root.addProperty("proxy", proxyId) | ||
| root.add("players", entries) | ||
| publish(root.toString()) | ||
| } | ||
|
|
||
| private fun expire() { | ||
| val now = nowMillis() | ||
| remote.entries.removeIf { now - it.value.receivedAt >= EXPIRY_MILLIS } | ||
| } | ||
|
|
||
| companion object { | ||
| const val EXPIRY_MILLIS = 30_000L | ||
| private const val MAX_PAYLOAD = 512 * 1024 | ||
| private const val MAX_PLAYERS = 10_000 | ||
| private const val MAX_PROXIES = 256 | ||
| private val PROXY_ID = Regex("[A-Za-z0-9_-]{1,128}") | ||
|
|
||
| fun subject(environment: String?): String? = | ||
| environment?.trim()?.takeIf { it.matches(PROXY_ID) }?.let { "proxy.platform.$it" } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
16 changes: 16 additions & 0 deletions
16
velocity/src/main/kotlin/gg/grounds/proxy/velocity/tab/TabLabelAdvances.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package gg.grounds.proxy.velocity.tab | ||
|
|
||
| object TabLabelAdvances { | ||
| fun width(text: String): Int = text.sumOf(::advance) | ||
|
|
||
| private fun advance(ch: Char): Int = | ||
| when { | ||
| ch in 'A'..'Z' && ch != 'I' -> 6 | ||
| ch in '0'..'9' -> 6 | ||
| ch == 'I' -> 4 | ||
| ch == ' ' || ch == '-' -> 4 | ||
| ch == '!' || ch == '.' || ch == ':' -> 2 | ||
| ch == '+' || ch == '_' || ch == '?' || ch == '/' -> 6 | ||
| else -> VanillaAdvances.width(ch.toString()) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When many players reconnect after a proxy restart, every
PostLoginEventcallsrefresh(), which rescans all connected players, serializes the entire roster, and publishes it to every proxy. A burst of n logins therefore sends O(n²) entries—at the supported 10,000-player limit, roughly 50 million UUID entries before NATS fan-out—and also performs the work on each login path. Update local state without publishing here, or debounce broadcasts and rely on the existing five-second refresh.Useful? React with 👍 / 👎.