Install prepared original HUD caches and report resolved asset paths

This commit is contained in:
chazmm committed 2026-09-09 00:18:07 +01:00
1 parent 57742e0e54
commit 4eb0acf786
6 files changed
+234 -15

No files matched your search

+2 -2
View File
@@ -211,9 +211,9 @@ fn setup(
},
));
commands.insert_resource(hud);
info!("Original scoring HUD loaded");
info!("Original scoring HUD loaded from {}", root.display());
}
Err(error) => error!("Original scoring HUD could not load: {error}"),
Err(error) => error!("Original scoring HUD could not load from {}: {error}. See docs/hud-installation.md", root.display()),
}
}
// Rasterize at output pixel resolution; retain the original 1280x720 APT
+2 -1
View File
@@ -77,7 +77,7 @@ fn load(
Ok(h) => h,
Err(e) => {
warn!(
"Original session-marker HUD unavailable at {}: {e}",
"Original session-marker HUD unavailable at {}: {e}. See docs/hud-installation.md",
folder.display()
);
return;
@@ -123,6 +123,7 @@ fn load(
error!("Invalid marker HUD geometry");
return;
}
info!("Original session-marker HUD loaded from {}", folder.display());
commands
.spawn((MarkerHudRoot, Transform::default(), Visibility::Hidden))
.with_children(|parent| {
+47
View File
@@ -0,0 +1,47 @@
# Original HUD installation
The runtime reads both caches from the selected `--assets` directory:
| HUD | Default manifest | Optional development override |
| --- | --- | --- |
| Trick scoring | `private/hud/runtime/trickdisplay.json` | `SKATE_SCORING_HUD_ROOT` |
| Session marker | `private/session-marker/hud.json` | `SKATE3_SESSION_MARKER_OVERLAY` |
Merging HUD code does not install ignored, extracted game assets. Earlier feature
launchers used private caches through environment overrides; the combined build
with only `--assets` could not find either cache. Setup now prepares both with
the existing extractors. The marker extractor defaults to the vendored UI toolkit;
the external `--ui-toolkit` argument remains supported.
For an existing installation, reuse prepared caches without reconverting maps:
```powershell
python tools/install_prepared_hud.py --assets <installed-assets> --scoring-cache <prepared-scoring-cache> --marker-cache <prepared-marker-overlay>
```
This validates both manifests and referenced texture sizes before copying any
files, checks available marker texture hashes, and verifies byte-identical copies.
It refuses differing installed files unless `--replace` is supplied. It installs
only the runtime files, preserving authored geometry, fonts, images and actions.
No copyrighted assets are committed or bundled with the source.
If no prepared caches exist, prepare only the two HUDs from the owned game folder:
```powershell
python tools/prepare_hud.py --game <owned-game> --output <installed-assets>/private/hud --collections <installed-assets>/private/stock/skater-collections.json
python tools/extract_session_marker.py --game <owned-game> --output <installed-assets>/private/session-marker
```
Startup logs report the loaded paths or a load failure. Check the executable and
asset paths in the launcher as well: `PLAY.bat` delegates to `Launch.ps1`, which
selects `bin/skate3rust.exe`, not a feature build elsewhere in `bin`.
Session-marker prompts appear while holding LB during gameplay; they are not an
always-visible panel. Scoring follows the original trickdisplay movie. Its camera
renders layer 31 offscreen before the presentation UI; the marker camera renders
layer 29 at order 1 after presentation. The existing scoring startup dependency
on `PresentationSetup` must be retained. Neither HUD uses the world mesh layers.
The installation repair and release compilation do not establish GPU visibility
or gameplay parity. Manual checks remain: resume gameplay, hold LB for the marker
panel, and perform and land a trick for the scoring display.
+19 -12
View File
@@ -9,6 +9,7 @@ import hashlib
import json
from pathlib import Path
import sys
import importlib
# Presentation calibration against the supplied retail HUD captures. These are
# host compositor adjustments, not recovered APT constants. Keep the original
@@ -17,10 +18,11 @@ PANEL_OPACITY = 0.75
SHADOW_COVERAGE_GAMMA = 2.2
def compile_hud(cache_root: Path, output: Path) -> None:
from skate3_ui_extract.scene_graph import AssetCache, SceneFlattener, transform_point
from skate3_ui_extract.bitmap_font import measure_bitmap_text
from skate3_ui_extract.timeline import playback_frame
def compile_hud(cache_root: Path, output: Path, toolkit="vendor.skate3_ui") -> None:
scene = importlib.import_module(toolkit + ".scene_graph")
AssetCache, SceneFlattener, transform_point = scene.AssetCache, scene.SceneFlattener, scene.transform_point
measure_bitmap_text = importlib.import_module(toolkit + ".bitmap_font").measure_bitmap_text
playback_frame = importlib.import_module(toolkit + ".timeline").playback_frame
cache = AssetCache(cache_root)
bundle = "data/fe/source/screens/hud2/hudphonelist"
labels = json.loads((cache_root / "metadata/languages/english_global.json").read_text())
@@ -148,21 +150,26 @@ def compile_hud(cache_root: Path, output: Path) -> None:
def prepare(game: Path, output: Path, toolkit="vendor.skate3_ui"):
extract_project = importlib.import_module(toolkit + ".project").extract_project
cache = output / "source-cache"
extract_project(game, cache, include_dynamic=True, prefixes=(
"data/fe/source/screens/hud2/hudphonelist", "data/fe/source/controls/button_item2",
"data/fe/source/images/buttons/xbox360"), update=True)
compile_hud(cache, output, toolkit)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--game", type=Path, required=True)
parser.add_argument("--ui-toolkit", type=Path, required=True)
parser.add_argument("--ui-toolkit", type=Path, help="Optional external toolkit; defaults to the vendored extractor")
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
if args.output.resolve().is_relative_to(args.game.resolve()):
parser.error("Output must be outside the owned game source")
sys.path.insert(0, str(args.ui_toolkit.resolve()))
from skate3_ui_extract.project import extract_project
cache = args.output / "source-cache"
extract_project(args.game, cache, include_dynamic=True, prefixes=(
"data/fe/source/screens/hud2/hudphonelist", "data/fe/source/controls/button_item2",
"data/fe/source/images/buttons/xbox360"), update=True)
compile_hud(cache, args.output)
if args.ui_toolkit:
sys.path.insert(0, str(args.ui_toolkit.resolve()))
prepare(args.game, args.output, "skate3_ui_extract" if args.ui_toolkit else "vendor.skate3_ui")
print(f"Private original session-marker HUD: {args.output / 'hud.json'}")
+90
View File
@@ -0,0 +1,90 @@
"""Install already extracted original HUD caches into an existing asset root.
Copies only runtime manifests and referenced RGBA files, byte for byte. No game,
disc extraction, map conversion or renderer is started. Both caches are checked
before writing; existing differing files require an explicit --replace.
"""
from pathlib import Path
import argparse
import hashlib
import json
import shutil
def digest(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
def contained(root, relative):
path = (root / relative).resolve()
if not path.is_relative_to(root.resolve()) or path == root.resolve():
raise ValueError(f"HUD path escapes cache: {relative}")
return path
def runtime_files(root, marker=False):
manifest = "hud.json" if marker else "runtime/trickdisplay.json"
data = json.loads((root / manifest).read_text(encoding="utf-8"))
if data.get("version") != 1:
raise ValueError(f"Unsupported HUD version: {root}")
textures = []
if marker:
if data.get("canvas") != [1280, 720] or not data.get("meshes"):
raise ValueError("Invalid session-marker HUD")
textures = [(t["file"], t["width"], t["height"], t.get("output_sha256"))
for t in data["textures"]]
else:
if data.get("format") != "skate3-scoring-hud" or data.get("unresolved_fonts"):
raise ValueError("Invalid scoring HUD or unresolved original fonts")
for shape in data["shapes"].values():
for primitive in shape:
t = primitive["texture"]
textures.append((t["rgba"], t["width"], t["height"], None))
for font in data["fonts"].values():
size = font["definition"]["textures"][0]
textures.append((font["texture"], size["width"], size["height"], None))
files = {manifest: digest(root / manifest)}
for relative, width, height, expected in textures:
path = contained(root, relative)
if width <= 0 or height <= 0 or path.stat().st_size != width * height * 4:
raise ValueError(f"Invalid RGBA size: {path}")
actual = digest(path)
if expected and expected != actual:
raise ValueError(f"Original HUD texture hash mismatch: {path}")
files[relative] = actual
return files
def install(assets, scoring_cache, marker_cache, replace=False):
plan = []
for source, name, marker in [(scoring_cache, "hud", False),
(marker_cache, "session-marker", True)]:
destination = assets / "private" / name
for relative, sha256 in runtime_files(source, marker).items():
target = contained(destination, relative)
if target.exists() and digest(target) != sha256 and not replace:
raise ValueError(f"Different installed HUD file (use --replace): {target}")
plan.append((contained(source, relative), target, sha256))
for source, target, sha256 in plan:
if target.exists() and digest(target) == sha256:
continue
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(source, target)
if digest(target) != sha256:
raise ValueError(f"Installed HUD hash mismatch: {target}")
return {str(target.relative_to(assets.resolve())): sha for _, target, sha in plan}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--assets", type=Path, required=True)
parser.add_argument("--scoring-cache", type=Path, required=True)
parser.add_argument("--marker-cache", type=Path, required=True)
parser.add_argument("--replace", action="store_true")
args = parser.parse_args()
files = install(args.assets, args.scoring_cache, args.marker_cache, args.replace)
print(json.dumps({"assets": str(args.assets.resolve()), "files": files}, indent=2))
if __name__ == "__main__":
main()
+74
View File
@@ -0,0 +1,74 @@
import hashlib
import json
from pathlib import Path
import tempfile
import unittest
from install_prepared_hud import install
class HudInstallationTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
root = Path(self.temp.name)
self.assets, self.score, self.marker = [root / p for p in ("assets", "score", "marker")]
(self.score / "runtime").mkdir(parents=True)
self.marker.mkdir()
self.pixels = bytes([12, 34, 56, 78])
for cache in (self.score, self.marker):
(cache / "atlas.rgba").write_bytes(self.pixels)
self.score_json = {"format": "skate3-scoring-hud", "version": 1,
"shapes": {"1": [{"texture": {"rgba": "atlas.rgba", "width": 1, "height": 1}}]},
"fonts": {"native": {"texture": "atlas.rgba", "definition": {
"textures": [{"width": 1, "height": 1}]}}}}
(self.score / "runtime/trickdisplay.json").write_text(json.dumps(self.score_json))
self.marker_json = {"version": 1, "canvas": [1280, 720], "meshes": [{}],
"textures": [{"file": "atlas.rgba", "width": 1, "height": 1,
"output_sha256": hashlib.sha256(self.pixels).hexdigest()}]}
self.write_marker()
def write_marker(self):
(self.marker / "hud.json").write_text(json.dumps(self.marker_json))
def run_install(self, **kwargs):
return install(self.assets, self.score, self.marker, **kwargs)
def test_preserves_bytes_and_repeat_install_is_a_noop(self):
files = self.run_install()
before = {p: (self.assets / p).stat().st_mtime_ns for p in files}
self.assertEqual(files, self.run_install())
self.assertEqual(before, {p: (self.assets / p).stat().st_mtime_ns for p in files})
self.assertEqual((self.assets / "private/hud/atlas.rgba").read_bytes(), self.pixels)
self.assertEqual((self.assets / "private/session-marker/hud.json").read_bytes(),
(self.marker / "hud.json").read_bytes())
def test_invalid_second_cache_does_not_partially_install_first(self):
(self.marker / "atlas.rgba").write_bytes(b"bad")
with self.assertRaisesRegex(ValueError, "RGBA size"):
self.run_install()
self.assertFalse(self.assets.exists())
def test_hash_and_path_failures(self):
(self.marker / "atlas.rgba").write_bytes(b"fake")
with self.assertRaisesRegex(ValueError, "hash mismatch"):
self.run_install()
self.marker_json["textures"][0]["file"] = "../outside.rgba"
self.write_marker()
with self.assertRaisesRegex(ValueError, "escapes cache"):
self.run_install()
self.assertFalse(self.assets.exists())
def test_differing_existing_file_requires_replace(self):
self.run_install()
target = self.assets / "private/hud/atlas.rgba"
target.write_bytes(b"keep")
with self.assertRaisesRegex(ValueError, "Different installed"):
self.run_install()
self.assertEqual(target.read_bytes(), b"keep")
self.run_install(replace=True)
self.assertEqual(target.read_bytes(), self.pixels)
if __name__ == "__main__":
unittest.main()