Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
38 changes: 38 additions & 0 deletions .schema/gametemplate.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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<value>[0-9.]+)`.",
"minLength": 1,
Expand Down
6 changes: 6 additions & 0 deletions palworld/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions palworld/module.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 10 additions & 0 deletions palworld/samples/metrics.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"currentplayernum": 0,
"serverfps": 59,
"serverfpsaverage": 64.69000244140625,
"serverframetime": 16.755241394042969,
"days": 0,
"maxplayernum": 4,
"basecampnum": 0,
"uptime": 2
}
4 changes: 3 additions & 1 deletion palworld/specs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down
12 changes: 11 additions & 1 deletion palworld/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -133,6 +133,16 @@ spec:
loader: pak

capabilities:
status:
metrics:
- id: server-fps
displayName: Server frame rate
command: metrics
regex: '"serverfps"\s*:\s*(?P<value>[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
Expand Down
30 changes: 30 additions & 0 deletions test-palworld-performance.py
Original file line number Diff line number Diff line change
@@ -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()
Loading