Files
skate-3-rust-engine/docs/hails-additions/06-map-validator.md
T
Hails 18f41a33c5 Tooling: streaming map validator (--validate-maps) wired into setup
One skate3rust process validates TEST_WORLD and every converted map over
stdin/stdout (SKATE_MAP_CHECK lines). Existing checks still fail a map;
new spawn-support / neutral-startup / invisible-collision checks report
warnings. Setup falls back to per-map --check-assets with an older exe.
See docs/hails-additions/06-map-validator.md (PR F).

Note: install.py also carries PR G's customiser-overlap hunks
(available_memory, overlap_customiser, Background); they are interleaved
with the validator wiring in _install and are documented in doc 08.
2026-10-01 00:13:53 -06:00

6.8 KiB
Raw Blame History

6. Streaming map validator (PR F, tooling)

Problem

Setup validated every converted map by launching the game once per map (skate3rust --assets <stage> --map <map> --check-assets, inside each map_job subprocess), plus a --test-world --check-assets launch before and after the map stage and one per recovered old map. Each launch cost ~7–8 s, of which only 0.1–2 s depended on the map: ~6 s was the same stock loading (skater runtime 3.2 s, controls + camera 2.5 s) repeated every time.

The check was also weak: a map passed as long as it loaded. It did not catch the spawn problems found in play (skater falling into the void on MegaPark, standing on an invisible collision-only floor in Industrial).

Change

Game: skate3rust --assets <dir> --validate-maps

New module crates/skate-game/src/map_validation.rs, dispatched from main.rs after the stock loads (config, gameplay config, manifest, graphs):

  • Loads once: animation source, controls and camera (fail fast, like --check-assets). Prints SKATE_VALIDATOR_READY seconds=… on stdout.
  • Reads one request per stdin line (a .skate path or TEST_WORLD) and answers with one SKATE_MAP_CHECK {json} stdout line; exits 0 at EOF. All other game output stays on stderr. Works through the crash supervisor, which forwards the child's stdout/stderr unchanged and lets it inherit stdin.
  • Per request:
    • errors (the map is rejected, as before): map load, skate_world::validate_runtime, physics with the map's collision, skater runtime. A panic inside one map becomes that map's error (catch_unwind).
    • warnings (reported, never rejecting): from physics/startup_check.rs — no collision within 10 m below the spawn; deck spawned away from the map spawn/heading; a neutral startup (zero actions, as with no controller) must settle with wheel support for 12 consecutive ticks within 240 ticks (4 s at 60 Hz), with finite body state, no physics failure and a solved wheel contact; and the collision floor under the spawn must have a visible (render) surface within 1 m.
    • JSON fields: path, ok, errors, warnings, collision_triangles, spawn, support_drop, grounded_ticks, visible_floor_gap, seconds, timings.
  • --check-assets is unchanged.

Setup (tools/asset_pipeline/install.py)

  • MapValidator runs one validator process; start_validator falls back to per-map --check-assets launches if it cannot start (e.g. an older game exe), and validate() falls back for the rest of the run if the process dies.
  • The pre-map stock check, every map (as soon as its conversion finishes, while the remaining maps keep converting), recovered old maps and the final test-world check all go to the same process. The per-map launch was removed from convert_map.
  • Process launching (packaged-exe DLL/PATH handling) moved from run() into a shared spawn(); run() behaves as before.
  • Warnings are written to assets/private/map-status/<map>-validation.json (status: "warning") by record_validation. New module validation_report.py wraps optional_content.summary and adds them to setup-report.json and the setup message; install.py and tools/setup.py call the wrapper. optional_content.py itself is deliberately untouched: it is part of the hud/environment/maps fingerprints and of every character-customiser stage version (customiser_cache.SHARED), so editing it would make every existing installation rebuild the customiser (~4 min), which has no equivalence mechanism. (A first version edited it; the fingerprint check that caught this compares against the committed tree, not a local install.)
  • pipeline-equivalence.json: removing validation from convert_map changes the maps fingerprint without changing any output, so an old→new pair is added (combined with PR G's stream change, see doc 8). Verified: an installation built from the committed code rebuilds no group, and the customiser fingerprint and all five stage versions are unchanged.

Verification

  • Validator on the test world and all 10 maps: all ok, no warnings; collision triangle counts equal the post-volume-fix baseline.
  • Negative cases (bad spawns written into copies of real maps): MegaPark's old void spawn → "no collision within 10 m below the spawn" + "wheels never found support"; Industrial's old harbour-bed spawn → "no visible surface … (invisible collision)".
  • Known limit: a spawn on a solid, visible roof (Industrial Skate Park's old roof spawn) is not flagged — rooftops are legitimate skate spots; only authored start data (doc 4) distinguishes them.
  • Unit tests: map_validation::tests (visible-surface heights, single-line JSON with the documented fields); Python test_map_validator.py with a fake validator speaking the protocol (results, warnings into setup-report.json, start failure fallback, process death, request injection, pipes closed under -W error::ResourceWarning). Existing setup/pipeline suites pass (40 tests).
  • Fingerprints: with the equivalence pairs, an existing installation rebuilds no groups.
  • Full setup with the real game: see "Setup run" below.

Setup run

Full fresh setup from the extracted disc with the real game exe (Windows 11, i7-14700KF, 3 map workers): 466 s, 0 warnings; the validator was used throughout (no fallback).

Step Before After
Pre-map stock check (--test-world --check-assets → validator start + TEST_WORLD) ~6 s ~1 s
Per-map validation ~7–9 s each, inside each map job 0.28–1.79 s each, in the parent while other maps convert
Map stage (convert + validate 10 maps) ~217 s 183 s

Per-map phase_seconds.validate in maps.json: University 1.54, DownTown 1.79, Industrial 1.11, parks 0.28–0.37 s. Afterwards check_maps-style validation of all maps: all ok, no warnings, collision triangles identical; baked spawns identical to the authored values (doc 4).

Notes

  • The ignored private test private_extracted_map_supports_production_gameplay_startup uses a fixed 24-tick window and fails on Industrial Skate Park, whose authored start is 0.75 m above the floor (≈0.39 s ≈ 23–24 ticks of fall). The map is fine in play; the validator uses the settle rule above instead. The test was left unchanged.
  • Without PR E (collection index) every per-map check would still take ~5.8 s.

Files

  • crates/skate-game/src/map_validation.rs (new), physics/startup_check.rs (new), main.rs, config.rs, physics.rs, skater_animation.rs (AnimationSource::load visibility), retail_character.rs + tests/map_transition.rs (new Config field in test literals)
  • tools/asset_pipeline/install.py, validation_report.py (new), pipeline-equivalence.json, test_map_validator.py (new), tools/setup.py (message wording)