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/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..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" @@ -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. 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. diff --git a/validate.py b/validate.py index b55d477..91614c1 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 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. + 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} ==")