From 54e9a11a694c10b0dc2550391782b95bf1a883de Mon Sep 17 00:00:00 2001 From: Baylem Date: Wed, 11 Nov 2026 00:40:19 -0500 Subject: [PATCH] feat(palworld): expose verified dedicated server frame rate Signed-off-by: Baylem --- .github/workflows/validate.yml | 3 +++ .schema/gametemplate.schema.json | 38 ++++++++++++++++++++++++++++++++ palworld/README.md | 6 +++++ palworld/module.yaml | 4 ++-- palworld/samples/metrics.json | 10 +++++++++ palworld/specs.md | 4 +++- palworld/template.yaml | 12 +++++++++- test-palworld-performance.py | 30 +++++++++++++++++++++++++ 8 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 palworld/samples/metrics.json create mode 100644 test-palworld-performance.py diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 5383574..c2f1168 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -49,6 +49,9 @@ jobs: - name: Test validate.py directory layout rules run: ./test-validate-py.sh + - name: Check Palworld frame rate against an engine response + run: python3 test-palworld-performance.py + - uses: actions/setup-go@v5 with: go-version: "1.26" diff --git a/.schema/gametemplate.schema.json b/.schema/gametemplate.schema.json index 871ae7c..36706b6 100644 --- a/.schema/gametemplate.schema.json +++ b/.schema/gametemplate.schema.json @@ -811,6 +811,44 @@ "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$", "type": "string" }, + "performance": { + "description": "Performance opts this existing readout into cached numeric monitoring.\nDisplay-only strings are never inferred to be performance measurements.", + "properties": { + "degradedThreshold": { + "description": "DegradedThreshold is a finite nonnegative decimal string in the input\nunit. Rates below it, or durations above it, are degraded. Omit to\ndisable alerts. A string keeps JSON/schema handling portable.", + "maxLength": 16, + "pattern": "^(0|[1-9][0-9]{0,8})(\\.[0-9]{1,6})?$", + "type": "string" + }, + "signal": { + "enum": [ + "tick_rate", + "tick_duration", + "frame_rate" + ], + "type": "string" + }, + "unit": { + "enum": [ + "per_second", + "seconds", + "milliseconds" + ], + "type": "string" + } + }, + "required": [ + "signal", + "unit" + ], + "type": "object", + "x-kubernetes-validations": [ + { + "message": "duration requires seconds/milliseconds; rates require per_second", + "rule": "self.signal == 'tick_duration' ? self.unit in ['seconds', 'milliseconds'] : self.unit == 'per_second'" + } + ] + }, "regex": { "description": "Regex extracts the value via the named group \"value\", e.g.\n`TPS: (?P[0-9.]+)`.", "minLength": 1, diff --git a/palworld/README.md b/palworld/README.md index e33c02b..afed40a 100644 --- a/palworld/README.md +++ b/palworld/README.md @@ -46,6 +46,12 @@ in-game admin commands, or the REST API directly, for kick/ban. (counts down, then shuts down cleanly) — see the server detail page's Actions panel. +The Overview and Prometheus expose the dedicated server's frame rate from +`GET /v1/api/metrics` in frames per second. An observed zero is a valid +reading. Missing data, failed authentication and stale responses leave the +numeric series absent. The module sets no universal frame-rate threshold. +[Palworld REST metrics](https://docs.palworldgame.com/api/rest-api/metrics/) + ## Mods (.pak) The Mods tab manages engine-native **`.pak` mods** in diff --git a/palworld/module.yaml b/palworld/module.yaml index b42f263..4307cea 100644 --- a/palworld/module.yaml +++ b/palworld/module.yaml @@ -2,10 +2,10 @@ apiVersion: gameplane.local/module/v1 name: palworld displayName: Palworld (Dedicated) -version: 1.2.0 +version: 1.3.0 game: palworld categories: [Survival, Sandbox, Co-op] summary: Palworld dedicated server (thijsvanloef/palworld-server-docker) with a REST-API console (RCON is deprecated upstream), save-aware graceful stop, and a .pak mod volume with archive install. First boot downloads the server via steamcmd. homepage: https://www.pocketpair.jp/palworld license: MIT -gameplaneMinVersion: 0.2.0-beta.7 +gameplaneMinVersion: 0.4.0 diff --git a/palworld/samples/metrics.json b/palworld/samples/metrics.json new file mode 100644 index 0000000..90edc00 --- /dev/null +++ b/palworld/samples/metrics.json @@ -0,0 +1,10 @@ +{ + "currentplayernum": 0, + "serverfps": 59, + "serverfpsaverage": 64.69000244140625, + "serverframetime": 16.755241394042969, + "days": 0, + "maxplayernum": 4, + "basecampnum": 0, + "uptime": 2 +} diff --git a/palworld/specs.md b/palworld/specs.md index fefc92b..4d0c0bc 100644 --- a/palworld/specs.md +++ b/palworld/specs.md @@ -48,7 +48,9 @@ Declared ports under `spec.ports`: - **Protocol**: `palworld` (REST admin API over HTTP Basic auth) - **Console Mode**: `rcon` - **Authentication**: HTTP Basic authentication (user `admin`), password supplied via `spec.rcon.passwordEnv: ADMIN_PASSWORD`. -- **Command Support**: REST endpoints for `announce`, `save`, `shutdown`, `stop`, `players`, and `info`. +- **Command Support**: REST endpoints for `announce`, `save`, `shutdown`, `stop`, `players`, `info`, and `metrics`. +- The `server-fps` readout parses `serverfps` from the metrics response as an integer frame rate in frames per second. Missing, null or malformed values yield no numeric sample. +- `samples/metrics.json` records the authenticated response of an empty Linux dedicated server, v1.0.5.102999 (Steam build 25247047). The instantaneous value was 59 FPS. The average FPS and frame time are separate engine fields. --- diff --git a/palworld/template.yaml b/palworld/template.yaml index 8e7407a..f09cc29 100644 --- a/palworld/template.yaml +++ b/palworld/template.yaml @@ -36,7 +36,7 @@ spec: displayName: Palworld (Dedicated) game: palworld # Kept in lockstep with module.yaml's bundle version. - version: 1.2.0 + version: 1.3.0 categories: [Survival, Sandbox, Co-op] # Brand color the dashboard tints this game's icon/accents with. accentColor: "#3d9dd9" @@ -133,6 +133,16 @@ spec: loader: pak capabilities: + status: + metrics: + - id: server-fps + displayName: Server frame rate + command: metrics + regex: '"serverfps"\s*:\s*(?P[0-9]+)\s*[,}]' + unit: FPS + performance: + signal: frame_rate + unit: per_second players: # /v1/api/players returns JSON; the agent applies entryRegex with # FindAll across the whole reply, so this lifts out every player's diff --git a/test-palworld-performance.py b/test-palworld-performance.py new file mode 100644 index 0000000..9800da2 --- /dev/null +++ b/test-palworld-performance.py @@ -0,0 +1,30 @@ +"""Validate the module against a real Palworld v1.0.5.102999 REST response.""" + +import json +from pathlib import Path +import re +import unittest + +import yaml + + +class PalworldFrameRate(unittest.TestCase): + def test_live_frame_rate_declaration(self): + root = Path(__file__).resolve().parent / "palworld" + template = yaml.safe_load((root / "template.yaml").read_text()) + metrics = template["spec"]["capabilities"]["status"]["metrics"] + metric, = [value for value in metrics if value["id"] == "server-fps"] + self.assertEqual(metric["command"], "metrics") + self.assertEqual(metric["performance"], {"signal": "frame_rate", "unit": "per_second"}) + pattern = re.compile(metric["regex"]) + raw = (root / "samples/metrics.json").read_text() + self.assertEqual(int(pattern.search(raw)["value"]), json.loads(raw)["serverfps"]) + self.assertEqual(pattern.search('{"serverfps":0}')["value"], "0") + for payload in ({}, {"serverfpsaverage": 60}, {"serverfps": None}, + {"serverfps": -1}, {"serverfps": 0.5}, {"serverfps": "57"}): + with self.subTest(payload=payload): + self.assertIsNone(pattern.search(json.dumps(payload))) + + +if __name__ == "__main__": + unittest.main()