mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Install prepared original HUD caches and report resolved asset paths
This commit is contained in:
1 parent
57742e0e54
commit
4eb0acf786
6 files changed
+234
-15
No files matched your search
@@ -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
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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.
|
||||
@@ -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'}")
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user