From 8162e0e373808d250e720212c60ee564694808d3 Mon Sep 17 00:00:00 2001 From: Hendrik Brombeer Date: Mon, 13 Jul 2026 16:23:58 +0200 Subject: [PATCH] feat: forward the agones and matchmaking blocks to forge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The upload manifest is hand-built field by field, so a block nobody named there simply does not exist as far as forge is concerned. `agones:` and `matchmaking:` were both being dropped in silence. `agones.replicas` never mattered — forge defaults it to 1, which is what everyone wanted anyway. `matchmaking:` matters enormously: it is what makes forge render GROUNDS_MATCHMAKING onto the pod and DECLARE the Agones `matches` counter on the Fleet. Without the counter declared, the matchmaker's allocation selector matches nothing, every allocation comes back UnAllocated, and no match can ever be placed — on a fleet that is sitting there Ready. Verified live: the pushed `duel` fleet came up Ready with no counters and no GROUNDS_MATCHMAKING, and the block was right there in its grounds.yaml. --- .../gg/grounds/push/manifest/GroundsYaml.kt | 50 +++++++++++++++++++ .../gg/grounds/push/tasks/GroundsPushTask.kt | 14 ++++++ 2 files changed, 64 insertions(+) diff --git a/plugin/src/main/kotlin/gg/grounds/push/manifest/GroundsYaml.kt b/plugin/src/main/kotlin/gg/grounds/push/manifest/GroundsYaml.kt index 577db46..a6c1896 100644 --- a/plugin/src/main/kotlin/gg/grounds/push/manifest/GroundsYaml.kt +++ b/plugin/src/main/kotlin/gg/grounds/push/manifest/GroundsYaml.kt @@ -36,6 +36,17 @@ data class GroundsYaml( * Architecture). */ val services: Map? = null, + /** + * Agones knobs. Only meaningful for the types forge renders as a Fleet. + */ + val agones: Agones? = null, + /** + * Hands the GameServer's lifecycle to the matchmaker. Its mere presence makes forge render + * `GROUNDS_MATCHMAKING` onto the pod and declare the Agones `matches` counter with this + * capacity — and without the counter DECLARED, the matchmaker's allocation selector matches + * nothing and no match can ever be placed on the fleet. + */ + val matchmaking: Matchmaking? = null, ) { data class Resources(val cpu: String? = null, val memory: String? = null) @@ -68,6 +79,10 @@ data class GroundsYaml( } } +data class Agones(val replicas: Int? = null) + +data class Matchmaking(val matchesPerServer: Int) + class GroundsYamlParseException( message: String, cause: Throwable? = null, @@ -114,11 +129,14 @@ object GroundsYamlParser { val resources = raw.optResources("resources") val events = raw.optEvents("events") val services = raw.optServices("services") + val agones = raw.optAgones("agones") + val matchmaking = raw.optMatchmaking("matchmaking") return GroundsYaml( name = name, type = type, baseImage = baseImage, jar = jar, plugins = plugins, target = target, resources = resources, flavors = flavors, events = events, services = services, + agones = agones, matchmaking = matchmaking, ) } @@ -145,6 +163,38 @@ object GroundsYamlParser { return v.ifEmpty { null } } + private fun Map<*, *>.optAgones(key: String): Agones? { + val v = this[key] ?: return null + if (v !is Map<*, *>) throw GroundsYamlParseException( + "grounds.yaml: '$key' must be a mapping, got ${v.javaClass.simpleName}", + ) + val replicas = v["replicas"] + return Agones( + replicas = when (replicas) { + null -> null + is Int -> replicas + else -> throw GroundsYamlParseException( + "grounds.yaml: 'agones.replicas' must be an integer", + ) + }, + ) + } + + private fun Map<*, *>.optMatchmaking(key: String): Matchmaking? { + val v = this[key] ?: return null + if (v !is Map<*, *>) throw GroundsYamlParseException( + "grounds.yaml: '$key' must be a mapping, got ${v.javaClass.simpleName}", + ) + val perServer = v["matchesPerServer"] + ?: throw GroundsYamlParseException( + "grounds.yaml: 'matchmaking.matchesPerServer' is required", + ) + if (perServer !is Int) throw GroundsYamlParseException( + "grounds.yaml: 'matchmaking.matchesPerServer' must be an integer", + ) + return Matchmaking(matchesPerServer = perServer) + } + private fun Map<*, *>.optResources( key: String, label: String = key, diff --git a/plugin/src/main/kotlin/gg/grounds/push/tasks/GroundsPushTask.kt b/plugin/src/main/kotlin/gg/grounds/push/tasks/GroundsPushTask.kt index 389e819..89103a9 100644 --- a/plugin/src/main/kotlin/gg/grounds/push/tasks/GroundsPushTask.kt +++ b/plugin/src/main/kotlin/gg/grounds/push/tasks/GroundsPushTask.kt @@ -535,6 +535,20 @@ abstract class GroundsPushTask : DefaultTask() { pluginSources: List, ): String = Json.encodeToString(JsonObject.serializer(), buildJsonObject { put("name", JsonPrimitive(manifest.name)) + // Fleet knobs. Hand-built JSON means a block that is not named here simply + // does not exist as far as forge is concerned — `matchmaking:` was dropped + // in silence, so the fleet came up with no `matches` counter and no + // GROUNDS_MATCHMAKING, and the matchmaker could never place a match on it. + manifest.agones?.let { agones -> + put("agones", buildJsonObject { + agones.replicas?.let { put("replicas", JsonPrimitive(it)) } + }) + } + manifest.matchmaking?.let { mm -> + put("matchmaking", buildJsonObject { + put("matchesPerServer", JsonPrimitive(mm.matchesPerServer)) + }) + } // Forge maps these to the gg.grounds/events SA annotation + NATS_URL. manifest.events?.takeIf { it.isNotEmpty() }?.let { events -> put("events", buildJsonArray {