From 7cbc35b6a03b9412f110b44f91c6fe6a135c1639 Mon Sep 17 00:00:00 2001 From: valgulnecron <39313199+ValgulNecron@users.noreply.github.com> Date: Mon, 5 Oct 2026 05:15:40 +0200 Subject: [PATCH 1/8] fix(tmodloader,terraria): start tModLoader with a built-in empty modpack passivelemon's tModLoader scripts exit when MODPACK is blank ("Modpack name was not provided") or when ModPacks//Mods/enabled.json is missing ("Modpack was not detected"). Both templates default MODPACK to "" and the wizard stores that value, so every default tModLoader server crash-looped. A small command wrapper, active only when GP_LOADER=tmodloader (template env for tmodloader, per-version env for terraria's three tModLoader versions), turns a blank MODPACK into "vanilla", creates ModPacks/vanilla/Mods/enabled.json as [] if it is missing (never overwriting it), then execs the image's own /opt/terraria/entrypoint.sh. Vanilla Terraria runs the same entrypoint unchanged. The tmodloader template also mounted its volume at /root/.local/share/Terraria/tModLoader, while the image keeps worlds, modpacks and serverconfig under /opt/terraria/config, so nothing was persisted. It now mounts /opt/terraria/config, and its Mods tab manages ModPacks/vanilla/Mods. Versions: tmodloader 1.0.2, terraria 2.2.3. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NsN6bnAMRM9XQtrTQ6KC7U Signed-off-by: valgulnecron <39313199+ValgulNecron@users.noreply.github.com> --- terraria/README.md | 2 +- terraria/module.yaml | 2 +- terraria/template.yaml | 28 +++++++++++++++++++++++++++- tmodloader/README.md | 8 ++++---- tmodloader/module.yaml | 2 +- tmodloader/samples/gameserver.yaml | 1 - tmodloader/specs.md | 4 ++-- tmodloader/template.yaml | 30 ++++++++++++++++++++++++++---- 8 files changed, 62 insertions(+), 15 deletions(-) diff --git a/terraria/README.md b/terraria/README.md index 02f5fde..2f263dd 100644 --- a/terraria/README.md +++ b/terraria/README.md @@ -18,7 +18,7 @@ override) or add an entry to `spec.versions`. tModLoader servers get a **Mods** tab that manages modpacks under `/opt/terraria/config/ModPacks` — its own per-(version+loader) volume, so each tModLoader build keeps its own set. Select the active modpack with the -**Active modpack** (`MODPACK`) config field. Vanilla Terraria has no mods, so +**Active modpack** (`MODPACK`) config field; left blank, tModLoader servers start a built-in empty `vanilla` pack. Vanilla Terraria has no mods, so the tab is hidden for vanilla servers. Installs are allowed from GitHub (max 512 MiB), subject to the agent's SSRF guard. diff --git a/terraria/module.yaml b/terraria/module.yaml index b3b884a..61f7840 100644 --- a/terraria/module.yaml +++ b/terraria/module.yaml @@ -2,7 +2,7 @@ apiVersion: gameplane.local/module/v1 name: terraria displayName: Terraria -version: 2.2.2 +version: 2.2.3 game: terraria categories: [Sandbox, Survival, Adventure, Modded] summary: Terraria + tModLoader dedicated server (passivelemon/terraria-docker) with a vanilla/tModLoader version picker and a tModLoader ModPacks volume. Console via container stdin. diff --git a/terraria/template.yaml b/terraria/template.yaml index babdee9..158a7e0 100644 --- a/terraria/template.yaml +++ b/terraria/template.yaml @@ -36,6 +36,23 @@ spec: # authoritative source for the wizard's picker. image: passivelemon/terraria-docker:terraria-latest@sha256:d60f280522d6c71079638b5036bdd6c4ac9f93c9f3250477ddb345fc2c6933f3 + # passivelemon's tModLoader scripts exit when MODPACK is blank or its + # ModPacks//Mods/enabled.json is missing. On tModLoader images + # (GP_LOADER=tmodloader) a blank MODPACK selects a built-in empty "vanilla" + # pack, created on first start and never overwritten; then the image's own + # entrypoint runs as before. + command: ["/bin/sh", "-c"] + args: + - | + if [ "${GP_LOADER:-}" = "tmodloader" ]; then + : "${MODPACK:=vanilla}"; export MODPACK + if [ "$MODPACK" = "vanilla" ]; then + mkdir -p /opt/terraria/config/ModPacks/vanilla/Mods + [ -e /opt/terraria/config/ModPacks/vanilla/Mods/enabled.json ] || printf '[]\n' > /opt/terraria/config/ModPacks/vanilla/Mods/enabled.json + fi + fi + exec /opt/terraria/entrypoint.sh + ports: - name: game containerPort: 7777 @@ -78,6 +95,9 @@ spec: displayName: "tModLoader · latest" image: passivelemon/terraria-docker:tmodloader-latest@sha256:3f2d8703421159f1037084bd2c0901a3a63b85a00801cf36f6f928e8b666b44e loader: tmodloader + env: + - name: GP_LOADER + value: tmodloader # Year channel: follows tModLoader's 2026.x stable line without jumping # to the next yearly major — steadier for servers with version-sensitive # mod sets than the plain latest tag. @@ -85,10 +105,16 @@ spec: displayName: "tModLoader · 2026 stable" image: passivelemon/terraria-docker:tmodloader-2026@sha256:3f2d8703421159f1037084bd2c0901a3a63b85a00801cf36f6f928e8b666b44e loader: tmodloader + env: + - name: GP_LOADER + value: tmodloader - id: tmodloader-preview displayName: "tModLoader · latest preview" image: passivelemon/terraria-docker:tmodloader-latest-pre # gameplane:floating loader: tmodloader + env: + - name: GP_LOADER + value: tmodloader probes: readiness: @@ -253,6 +279,6 @@ spec: default: "en-US" - name: MODPACK displayName: Active modpack - description: tModLoader only — a modpack folder name under ModPacks. Leave blank for none. + description: tModLoader only — a modpack folder name under ModPacks. Leave blank for the built-in empty "vanilla" pack. type: string default: "" diff --git a/tmodloader/README.md b/tmodloader/README.md index 083bc94..26deebd 100644 --- a/tmodloader/README.md +++ b/tmodloader/README.md @@ -12,7 +12,7 @@ kubectl apply -f modules/tmodloader/template.yaml ## Mod Management -tModLoader mods (`.tmod`) and modpacks are managed through the **Mods** tab. Mod files are stored under `/root/.local/share/Terraria/tModLoader/Mods`. Installs are allowed from GitHub release archives (max 512 MiB). +A fresh server starts the built-in empty `vanilla` modpack (no mods). The **Mods** tab manages `.tmod` files in that pack, under `/opt/terraria/config/ModPacks/vanilla/Mods`; a mod loads once its name is listed in that folder's `enabled.json`. To run another modpack, put it under `/opt/terraria/config/ModPacks//Mods` (with its `enabled.json`) and set **Active modpack** (`MODPACK`) to ``. Installs are allowed from GitHub release archives (max 512 MiB). ## Console (PTY) @@ -26,10 +26,10 @@ Terraria engines do not provide an RCON TCP port. The **Console** tab attaches d ## Storage -Storage is mounted at `/root/.local/share/Terraria/tModLoader` (4 GiB default), holding: +Storage is mounted at `/opt/terraria/config` (4 GiB default), holding: - World files (`Worlds/`) -- Installed mods (`Mods/`) -- Mod configurations and loadouts +- Modpacks (`ModPacks//Mods/`, including the default `vanilla` pack) +- The generated `serverconfig.txt` ## Sample diff --git a/tmodloader/module.yaml b/tmodloader/module.yaml index 4e1b63f..c1fc794 100644 --- a/tmodloader/module.yaml +++ b/tmodloader/module.yaml @@ -2,7 +2,7 @@ apiVersion: gameplane.local/module/v1 name: tmodloader displayName: tModLoader -version: 1.0.1 +version: 1.0.2 game: tmodloader categories: [Sandbox, Survival, Adventure, Modded] summary: tModLoader dedicated server for modded Terraria gameplay with PTY console, .tmod mod manager, and world saves. diff --git a/tmodloader/samples/gameserver.yaml b/tmodloader/samples/gameserver.yaml index 174860e..107c374 100644 --- a/tmodloader/samples/gameserver.yaml +++ b/tmodloader/samples/gameserver.yaml @@ -13,7 +13,6 @@ spec: DIFFICULTY: "1" MAXPLAYERS: "8" MOTD: "Welcome to Gameplane tModLoader Server" - MODPACK: "" networking: expose: NodePort diff --git a/tmodloader/specs.md b/tmodloader/specs.md index 1c9f64d..e4cabb7 100644 --- a/tmodloader/specs.md +++ b/tmodloader/specs.md @@ -14,7 +14,7 @@ - **Base Image**: `passivelemon/terraria-docker:tmodloader-latest@sha256:3f2d8703421159f1037084bd2c0901a3a63b85a00801cf36f6f928e8b666b44e` - **Architecture**: `linux/amd64` - **Runtime Model**: .NET 6 runtime host running `tModLoader.dll` with integrated mono/native runtime dependencies. -- **User & Execution Context**: Root container execution (`/`). +- **User & Execution Context**: Starts as root; a small wrapper picks the default `vanilla` modpack when `MODPACK` is blank, then the image entrypoint drops to the `terraria` user (uid/gid 1000) via su-exec. --- @@ -30,7 +30,7 @@ Declared ports under `spec.ports`: ## 4. Storage & Persistence Layout -- **Mount Path**: `/root/.local/share/Terraria/tModLoader` +- **Mount Path**: `/opt/terraria/config` - **Default Sizing**: `4Gi` - **Persisted Content**: - Generated modded worlds (`Worlds/*.wld`) diff --git a/tmodloader/template.yaml b/tmodloader/template.yaml index 81da7a6..538c972 100644 --- a/tmodloader/template.yaml +++ b/tmodloader/template.yaml @@ -3,7 +3,8 @@ # # Backed by passivelemon/terraria-docker:tmodloader-latest. Dedicated server # running the tModLoader .NET 6 modding framework for Terraria. Console -# attaches to container stdin (pty); no RCON. +# attaches to container stdin (pty); no RCON. A blank MODPACK starts the +# built-in empty "vanilla" modpack (see the command wrapper below). # # Cluster-scoped; apply once per cluster: # kubectl apply -f modules/tmodloader/template.yaml @@ -26,6 +27,27 @@ spec: image: passivelemon/terraria-docker:tmodloader-latest@sha256:3f2d8703421159f1037084bd2c0901a3a63b85a00801cf36f6f928e8b666b44e + env: + - name: GP_LOADER + value: tmodloader + + # passivelemon's tModLoader scripts exit when MODPACK is blank or its + # ModPacks//Mods/enabled.json is missing. On tModLoader images + # (GP_LOADER=tmodloader) a blank MODPACK selects a built-in empty "vanilla" + # pack, created on first start and never overwritten; then the image's own + # entrypoint runs as before. + command: ["/bin/sh", "-c"] + args: + - | + if [ "${GP_LOADER:-}" = "tmodloader" ]; then + : "${MODPACK:=vanilla}"; export MODPACK + if [ "$MODPACK" = "vanilla" ]; then + mkdir -p /opt/terraria/config/ModPacks/vanilla/Mods + [ -e /opt/terraria/config/ModPacks/vanilla/Mods/enabled.json ] || printf '[]\n' > /opt/terraria/config/ModPacks/vanilla/Mods/enabled.json + fi + fi + exec /opt/terraria/entrypoint.sh + security: fsGroup: 1000 @@ -38,7 +60,7 @@ spec: storage: size: 4Gi - mountPath: /root/.local/share/Terraria/tModLoader + mountPath: /opt/terraria/config resources: requests: @@ -84,7 +106,7 @@ spec: transport: stdin command: "save" mods: - path: Mods + path: ModPacks/vanilla/Mods extensions: [".tmod", ".zip"] install: allowedHosts: @@ -124,6 +146,6 @@ spec: default: "Welcome to Gameplane tModLoader Server" - name: MODPACK displayName: Active modpack - description: Active modpack name under ModPacks. Leave blank for none. + description: Active modpack folder name under ModPacks. Leave blank for the built-in empty "vanilla" pack. type: string default: "" From 86451ceaa60d912cd1b348a7aebcb0163d2d26af Mon Sep 17 00:00:00 2001 From: valgulnecron <39313199+ValgulNecron@users.noreply.github.com> Date: Mon, 5 Oct 2026 05:57:30 +0200 Subject: [PATCH 2/8] fix(terraria,tmodloader): keep template spec.version in lockstep with module.yaml The wizard shows GameTemplate spec.version, which had drifted from the module.yaml versions (terraria 2.2.0 vs 2.2.3, tmodloader 1.0.0 vs 1.0.2) although the template header says they move in lockstep. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NsN6bnAMRM9XQtrTQ6KC7U Signed-off-by: valgulnecron <39313199+ValgulNecron@users.noreply.github.com> --- terraria/template.yaml | 2 +- tmodloader/template.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/terraria/template.yaml b/terraria/template.yaml index 158a7e0..74054f6 100644 --- a/terraria/template.yaml +++ b/terraria/template.yaml @@ -21,7 +21,7 @@ spec: displayName: Terraria game: terraria # Kept in lockstep with module.yaml's bundle version from 2.1.0 onward. - version: 2.2.0 + version: 2.2.3 categories: [Sandbox, Survival, Adventure, Modded] # Brand color the dashboard tints this game's icon/accents with. accentColor: "#4a90d9" diff --git a/tmodloader/template.yaml b/tmodloader/template.yaml index 538c972..47167f8 100644 --- a/tmodloader/template.yaml +++ b/tmodloader/template.yaml @@ -18,7 +18,7 @@ metadata: spec: displayName: tModLoader game: tmodloader - version: 1.0.0 + version: 1.0.2 categories: [Sandbox, Survival, Adventure, Modded] accentColor: "#10b981" description: | From 420ad70d1b29d0df61a8a0cc673f3eed41c7df95 Mon Sep 17 00:00:00 2001 From: valgulnecron <39313199+ValgulNecron@users.noreply.github.com> Date: Mon, 5 Oct 2026 05:59:46 +0200 Subject: [PATCH 3/8] fix(nuclear-option): correct players capability YAML structure The kick, ban, and unban fields were incorrectly nested as objects with a 'command' key, but the GameTemplate CRD defines these as string fields directly. Restructured to match the CRD and the format used by other modules (minecraft-java, rust). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NsN6bnAMRM9XQtrTQ6KC7U Signed-off-by: valgulnecron <39313199+ValgulNecron@users.noreply.github.com> --- nuclear-option/module.yaml | 2 +- nuclear-option/template.yaml | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/nuclear-option/module.yaml b/nuclear-option/module.yaml index 5daf788..0c83516 100644 --- a/nuclear-option/module.yaml +++ b/nuclear-option/module.yaml @@ -2,7 +2,7 @@ apiVersion: gameplane.local/module/v1 name: nuclear-option displayName: Nuclear Option -version: 1.0.0 +version: 1.0.1 game: nuclear-option categories: [Simulation, PvP] summary: Nuclear Option dedicated server with remote-command console, runtime-downloaded via SteamCMD. diff --git a/nuclear-option/template.yaml b/nuclear-option/template.yaml index 55ebfa6..9abfd13 100644 --- a/nuclear-option/template.yaml +++ b/nuclear-option/template.yaml @@ -166,12 +166,9 @@ spec: list: command: "get-player-list" entryRegex: '"steamId":\s*"(?P[^"]*)"' - kick: - command: "kick-player {{.Player}}" - ban: - command: "banlist-add {{.Player}}" - unban: - command: "banlist-remove {{.Player}}" + kick: "kick-player {{.Player}}" + ban: "banlist-add {{.Player}}" + unban: "banlist-remove {{.Player}}" # Note: the server returns 2000 (success) for kick/ban/unban even when # the Steam ID is not present, so the agent cannot distinguish a # successful moderation from a silent no-op. From 83100b19c52891377fa2600ec2a670a35cb01ea2 Mon Sep 17 00:00:00 2001 From: valgulnecron <39313199+ValgulNecron@users.noreply.github.com> Date: Mon, 5 Oct 2026 06:00:39 +0200 Subject: [PATCH 4/8] chore(validate): enforce GameTemplate spec.version == module.yaml version Add rule 10 to validate.py: GameTemplate spec.version must match the module.yaml version exactly, preventing drift like minecraft-java's (2.8.1 vs 2.9.0) that caused the wizard to display outdated versions. Also fix minecraft-java template spec.version to 2.9.0. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NsN6bnAMRM9XQtrTQ6KC7U Signed-off-by: valgulnecron <39313199+ValgulNecron@users.noreply.github.com> --- minecraft-java/template.yaml | 2 +- validate.py | 42 ++++++++++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/minecraft-java/template.yaml b/minecraft-java/template.yaml index 70866f1..083f722 100644 --- a/minecraft-java/template.yaml +++ b/minecraft-java/template.yaml @@ -23,7 +23,7 @@ spec: displayName: Minecraft (Java Edition) game: minecraft-java # Kept in lockstep with module.yaml's bundle version from 2.5.0 onward. - version: 2.8.1 + version: 2.9.0 categories: [Sandbox, Survival, Building, Modded, Creative] # Brand color the dashboard tints this game's icon/accents with. accentColor: "#5b9a3e" diff --git a/validate.py b/validate.py index b55d477..5dbfcc2 100644 --- a/validate.py +++ b/validate.py @@ -1026,6 +1026,31 @@ def rule_credential_fields_must_be_password(spec: dict) -> list[Finding]: return findings +def rule_template_version_matches_module(spec: dict, module_version: str | None, template_path: Path) -> list[Finding]: + """Rule 10: GameTemplate spec.version must match module.yaml version. + + Historical bug: minecraft-java template.yaml had spec.version 2.8.1 while + module.yaml was 2.9.0, causing the wizard to display an outdated version. + The template must stay in lockstep with the module's declared version. + """ + if module_version is None: + return [] + + spec_version = spec.get("version", "") + if spec_version == module_version: + return [] + + return [ + Finding( + ERROR, + "template-version-drift", + f"{template_path.name}: spec.version={spec_version!r} but module.yaml version={module_version!r} — " + "these must match exactly (template versions say 'lockstep'). Update template spec.version to " + f"{module_version!r}.", + ) + ] + + LAYOUT_ENFORCED_MODULES = { "cs2", "palworld", @@ -1103,7 +1128,7 @@ def collect_image_refs(spec: dict) -> list[str]: return out -def validate_module(spec: dict, cache: dict[str, dict]) -> list[Finding]: +def validate_module(spec: dict, cache: dict[str, dict], module_dir: Path | None = None) -> list[Finding]: findings: list[Finding] = [] image_refs = collect_image_refs(spec) if not image_refs: @@ -1129,6 +1154,19 @@ def validate_module(spec: dict, cache: dict[str, dict]) -> list[Finding]: findings += rule_mods_loaders_requires_versions(spec) findings += rule_credential_fields_must_be_password(spec) findings += rule_images_pinned(spec) + + # Check template version matches module.yaml if we have the module dir + if module_dir: + module_yaml_path = module_dir / "module.yaml" + if module_yaml_path.exists(): + try: + module_doc = yaml.safe_load(module_yaml_path.read_text()) + module_version = (module_doc or {}).get("version") + template_path = module_dir / "template.yaml" + findings += rule_template_version_matches_module(spec, module_version, template_path) + except yaml.YAMLError: + pass # module.yaml parse error already reported elsewhere if relevant + return findings @@ -1348,7 +1386,7 @@ def main(argv: list[str]) -> int: continue spec = (doc or {}).get("spec") or {} - findings = rule_directory_layout(module_dir) + validate_module(spec, cache) + findings = rule_directory_layout(module_dir) + validate_module(spec, cache, module_dir) if module_dir.name in offline_findings: findings.extend(offline_findings[module_dir.name]) print(f"== {module_dir.name} ==") From e3c0986aa143fc13a9756a9169e76053ce3eb16d Mon Sep 17 00:00:00 2001 From: valgulnecron <39313199+ValgulNecron@users.noreply.github.com> Date: Mon, 5 Oct 2026 06:00:58 +0200 Subject: [PATCH 5/8] fix(modules): unify dinosaur category spelling to Dinosaurs (plural) Standardize both ark-survival-evolved and the-isle to use "Dinosaurs" (plural) for the dinosaur-themed game category. Avoids fragmenting the catalog with two spellings of the same concept; aligns with English conventions for game categories (Survival, Sandbox, etc.). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NsN6bnAMRM9XQtrTQ6KC7U Signed-off-by: valgulnecron <39313199+ValgulNecron@users.noreply.github.com> --- the-isle/module.yaml | 2 +- the-isle/template.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/the-isle/module.yaml b/the-isle/module.yaml index a06d9d6..15541d8 100644 --- a/the-isle/module.yaml +++ b/the-isle/module.yaml @@ -4,7 +4,7 @@ name: the-isle displayName: The Isle version: 1.0.0 game: the-isle -categories: [Survival, Dinosaur, Open World, Multiplayer] +categories: [Survival, Dinosaurs, Open World, Multiplayer] summary: The Isle dedicated server (Unreal Engine 4) with Source RCON remote administration and persistent world save management. homepage: https://findtheisle.com/ license: MIT diff --git a/the-isle/template.yaml b/the-isle/template.yaml index 501b014..4cf6272 100644 --- a/the-isle/template.yaml +++ b/the-isle/template.yaml @@ -17,7 +17,7 @@ spec: displayName: The Isle game: the-isle version: 1.0.0 - categories: [Survival, Dinosaur, Open World, Multiplayer] + categories: [Survival, Dinosaurs, Open World, Multiplayer] accentColor: "#22543d" description: | The Isle dedicated multiplayer server running on Unreal Engine. From 42c4bea9cead5560608ee5bd97bbc1c4362b3dcf Mon Sep 17 00:00:00 2001 From: valgulnecron <39313199+ValgulNecron@users.noreply.github.com> Date: Mon, 5 Oct 2026 06:12:29 +0200 Subject: [PATCH 6/8] fix(nuclear-option): keep template spec.version in lockstep with module.yaml The new validate.py rule requires spec.version to equal module.yaml, which the players fix bumped to 1.0.1. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NsN6bnAMRM9XQtrTQ6KC7U Signed-off-by: valgulnecron <39313199+ValgulNecron@users.noreply.github.com> --- nuclear-option/template.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nuclear-option/template.yaml b/nuclear-option/template.yaml index 9abfd13..434a2d6 100644 --- a/nuclear-option/template.yaml +++ b/nuclear-option/template.yaml @@ -28,7 +28,7 @@ spec: displayName: Nuclear Option game: nuclear-option # Kept in lockstep with module.yaml's bundle version. - version: 1.0.0 + version: 1.0.1 categories: [Simulation, PvP] # Brand color for the dashboard. accentColor: "#ff6b6b" From 0c88a710007b8ad901d0fa8a1874e79f8c9ce439 Mon Sep 17 00:00:00 2001 From: valgulnecron <39313199+ValgulNecron@users.noreply.github.com> Date: Mon, 5 Oct 2026 06:12:37 +0200 Subject: [PATCH 7/8] chore(validate): number the version-lockstep rule 11 Rule 10 was already taken by rule_images_pinned. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NsN6bnAMRM9XQtrTQ6KC7U Signed-off-by: valgulnecron <39313199+ValgulNecron@users.noreply.github.com> --- validate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validate.py b/validate.py index 5dbfcc2..91614c1 100644 --- a/validate.py +++ b/validate.py @@ -1027,7 +1027,7 @@ def rule_credential_fields_must_be_password(spec: dict) -> list[Finding]: def rule_template_version_matches_module(spec: dict, module_version: str | None, template_path: Path) -> list[Finding]: - """Rule 10: GameTemplate spec.version must match module.yaml version. + """Rule 11: GameTemplate spec.version must match module.yaml version. Historical bug: minecraft-java template.yaml had spec.version 2.8.1 while module.yaml was 2.9.0, causing the wizard to display an outdated version. From 48722d30eaa3207f7bf4b56d8ac868b5156dd7fe Mon Sep 17 00:00:00 2001 From: valgulnecron <39313199+ValgulNecron@users.noreply.github.com> Date: Mon, 5 Oct 2026 15:20:06 +0200 Subject: [PATCH 8/8] fix(terraria,tmodloader): bridge the console into the game's tmux session passivelemon runs the server in a detached tmux session and nothing reads container stdin, so console input and the Stop button's exit never reached the game (stop always waited out the 30 s grace). The start wrapper now forwards each pty line with tmux send-keys and forwards SIGTERM to the entrypoint. Verified live on both images. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018MnGTGHzHhFQCa22wRknUS Signed-off-by: valgulnecron <39313199+ValgulNecron@users.noreply.github.com> --- terraria/README.md | 3 ++- terraria/specs.md | 1 + terraria/template.yaml | 41 +++++++++++++++++++++++++++++++++++++--- tmodloader/README.md | 2 +- tmodloader/specs.md | 1 + tmodloader/template.yaml | 41 +++++++++++++++++++++++++++++++++++++--- 6 files changed, 81 insertions(+), 8 deletions(-) diff --git a/terraria/README.md b/terraria/README.md index 2f263dd..325d139 100644 --- a/terraria/README.md +++ b/terraria/README.md @@ -36,7 +36,8 @@ offered. ## Console No RCON. The **Console** tab attaches to the container's stdin/stdout (pty) — -type `help` for the command list. +type `help` for the command list. The image runs the server inside a detached tmux session, so the +template's start wrapper forwards each console line into that session; the Stop button's `exit` saves the world and stops within seconds. ## Ports diff --git a/terraria/specs.md b/terraria/specs.md index bf5f964..56b9f79 100644 --- a/terraria/specs.md +++ b/terraria/specs.md @@ -45,6 +45,7 @@ Declared ports under `spec.ports`: - **Protocol**: `none` - **Console Mode**: `pty` +- **Console Bridge**: The start wrapper forwards pty lines to the game's tmux session with `tmux send-keys -l` (as `PUID:PGID` via gosu/su-exec) and forwards SIGTERM to the entrypoint. - **Authentication**: N/A (interactive stdin/stdout console). - **Command Support**: Standard Terraria CLI commands (`say`, `save`, `kick`, `ban`, `settle`, `motd`, `dawn`, `noon`, `dusk`, `midnight`). diff --git a/terraria/template.yaml b/terraria/template.yaml index 74054f6..0d897d2 100644 --- a/terraria/template.yaml +++ b/terraria/template.yaml @@ -39,11 +39,16 @@ spec: # passivelemon's tModLoader scripts exit when MODPACK is blank or its # ModPacks//Mods/enabled.json is missing. On tModLoader images # (GP_LOADER=tmodloader) a blank MODPACK selects a built-in empty "vanilla" - # pack, created on first start and never overwritten; then the image's own - # entrypoint runs as before. + # pack, created on first start and never overwritten. The image runs the + # game in a detached tmux session that never reads container stdin, so the + # wrapper also bridges the console: each line from the pty is sent to the + # game with tmux send-keys (as the game user), and SIGTERM is forwarded to + # the entrypoint so its save-and-exit trap still runs. command: ["/bin/sh", "-c"] args: - | + pid= + trap 'if [ -n "$pid" ]; then kill -TERM "$pid" 2>/dev/null; else exit 143; fi' TERM if [ "${GP_LOADER:-}" = "tmodloader" ]; then : "${MODPACK:=vanilla}"; export MODPACK if [ "$MODPACK" = "vanilla" ]; then @@ -51,7 +56,37 @@ spec: [ -e /opt/terraria/config/ModPacks/vanilla/Mods/enabled.json ] || printf '[]\n' > /opt/terraria/config/ModPacks/vanilla/Mods/enabled.json fi fi - exec /opt/terraria/entrypoint.sh + /opt/terraria/entrypoint.sh & + pid=$! + rpid= + if [ -t 0 ]; then + exec 3<&0 + ( + cr=$(printf '\r') + as= + if [ "$(id -u)" = 0 ]; then + if command -v gosu >/dev/null 2>&1; then as="gosu"; else as="su-exec"; fi + as="$as ${PUID:-1000}:${PGID:-1000}" + fi + while :; do + while IFS= read -r line <&3; do + line=${line%"$cr"} + [ -n "$line" ] || continue + if ! { $as /usr/bin/tmux send-keys -l -- "$line" && $as /usr/bin/tmux send-keys Enter; } 2>/dev/null; then + echo "[gameplane] console not ready; input ignored" + fi + done + sleep 1 + done + ) & + rpid=$! + fi + while :; do + wait "$pid"; rc=$? + kill -0 "$pid" 2>/dev/null || break + done + [ -z "$rpid" ] || kill "$rpid" 2>/dev/null + exit "$rc" ports: - name: game diff --git a/tmodloader/README.md b/tmodloader/README.md index 26deebd..cdfa2ce 100644 --- a/tmodloader/README.md +++ b/tmodloader/README.md @@ -16,7 +16,7 @@ A fresh server starts the built-in empty `vanilla` modpack (no mods). The **Mods ## Console (PTY) -Terraria engines do not provide an RCON TCP port. The **Console** tab attaches directly to the container's stdin/stdout (pty) using the kubelet pod-attach API. Stop sequence issues `exit` to trigger world flushing before shutdown. +Terraria engines do not provide an RCON TCP port. The **Console** tab attaches directly to the container's stdin/stdout (pty) using the kubelet pod-attach API. Stop sequence issues `exit` to trigger world flushing before shutdown. The image runs the server inside a detached tmux session, so the template's start wrapper forwards each console line into that session. ## Ports diff --git a/tmodloader/specs.md b/tmodloader/specs.md index e4cabb7..6db02e8 100644 --- a/tmodloader/specs.md +++ b/tmodloader/specs.md @@ -44,6 +44,7 @@ Declared ports under `spec.ports`: - **Protocol**: `none` - **Console Mode**: `pty` +- **Console Bridge**: The start wrapper forwards pty lines to the game's tmux session with `tmux send-keys -l` (as `PUID:PGID` via gosu/su-exec) and forwards SIGTERM to the entrypoint. - **Authentication**: N/A (interactive terminal console). - **Command Support**: Standard Terraria and tModLoader CLI commands (`say`, `save`, `exit`, mod reload commands). diff --git a/tmodloader/template.yaml b/tmodloader/template.yaml index 47167f8..2cf97a5 100644 --- a/tmodloader/template.yaml +++ b/tmodloader/template.yaml @@ -34,11 +34,16 @@ spec: # passivelemon's tModLoader scripts exit when MODPACK is blank or its # ModPacks//Mods/enabled.json is missing. On tModLoader images # (GP_LOADER=tmodloader) a blank MODPACK selects a built-in empty "vanilla" - # pack, created on first start and never overwritten; then the image's own - # entrypoint runs as before. + # pack, created on first start and never overwritten. The image runs the + # game in a detached tmux session that never reads container stdin, so the + # wrapper also bridges the console: each line from the pty is sent to the + # game with tmux send-keys (as the game user), and SIGTERM is forwarded to + # the entrypoint so its save-and-exit trap still runs. command: ["/bin/sh", "-c"] args: - | + pid= + trap 'if [ -n "$pid" ]; then kill -TERM "$pid" 2>/dev/null; else exit 143; fi' TERM if [ "${GP_LOADER:-}" = "tmodloader" ]; then : "${MODPACK:=vanilla}"; export MODPACK if [ "$MODPACK" = "vanilla" ]; then @@ -46,7 +51,37 @@ spec: [ -e /opt/terraria/config/ModPacks/vanilla/Mods/enabled.json ] || printf '[]\n' > /opt/terraria/config/ModPacks/vanilla/Mods/enabled.json fi fi - exec /opt/terraria/entrypoint.sh + /opt/terraria/entrypoint.sh & + pid=$! + rpid= + if [ -t 0 ]; then + exec 3<&0 + ( + cr=$(printf '\r') + as= + if [ "$(id -u)" = 0 ]; then + if command -v gosu >/dev/null 2>&1; then as="gosu"; else as="su-exec"; fi + as="$as ${PUID:-1000}:${PGID:-1000}" + fi + while :; do + while IFS= read -r line <&3; do + line=${line%"$cr"} + [ -n "$line" ] || continue + if ! { $as /usr/bin/tmux send-keys -l -- "$line" && $as /usr/bin/tmux send-keys Enter; } 2>/dev/null; then + echo "[gameplane] console not ready; input ignored" + fi + done + sleep 1 + done + ) & + rpid=$! + fi + while :; do + wait "$pid"; rc=$? + kill -0 "$pid" 2>/dev/null || break + done + [ -z "$rpid" ] || kill "$rpid" 2>/dev/null + exit "$rc" security: fsGroup: 1000