From a7090f9ef3b5f64f1f7bbb1c427c4b876302ffbd Mon Sep 17 00:00:00 2001 From: chazmm Date: Tue, 8 Sep 2026 04:44:05 +0100 Subject: [PATCH] Handle retail park streams and spline headers across all disc maps --- crates/skate-game/src/grind_world/spline.rs | 11 +++++++++-- docs/installation.md | 16 ++++++++++++++++ tools/asset_pipeline/install.py | 4 +++- .../tools/retail_grind_splines.py | 8 ++++++-- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/crates/skate-game/src/grind_world/spline.rs b/crates/skate-game/src/grind_world/spline.rs index 84373b9f..6c1c6268 100644 --- a/crates/skate-game/src/grind_world/spline.rs +++ b/crates/skate-game/src/grind_world/spline.rs @@ -102,8 +102,15 @@ pub(crate) fn primitives(map: Option<&SkateMap>) -> Result,String let [a,b,c,d] = coefficients; let end = std::array::from_fn(|j| (a[j]+b[j])+(c[j]+d[j])); let length: f32 = (0..3).map(|j| (end[j]-d[j]).powi(2)).sum(); - if !length.is_finite() || length <= 0.000001 { - return Err(format!("Spline segment {i} has a degenerate contact chord")); + if !length.is_finite() { + return Err(format!("Spline segment {i} has a non-finite contact chord")); + } + if length <= 0.000001 { + // Retail Downtown contains duplicate knots and sub-millimetre + // chords. They cannot provide a usable contact direction. Keep + // their cubic records and links in build(), but omit them from + // this adapter's independent contact-primitive array. + continue; } result.push(Primitive { start:d,end,owner:((word(r) as u64)<<32)|word(r+4) as u64 }); } diff --git a/docs/installation.md b/docs/installation.md index 0048ee05..7e521fe0 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -46,3 +46,19 @@ game process and closes the current session so physics and render resources are recreated. A fresh launch defaults to University. Development checkouts with `assets/private/game.json` retain their existing local assets and default test world; `--assets DIRECTORY` also selects a prepared asset set explicitly. + +## Conversion checks + +The direct converter was checked against an extracted Xbox 360 disc containing +the three main districts, Skate School, and six separately stored parks: +Black Box, Downtown Skate Park, Industrial Skate Park, Maloof, Mega Park and +Start Park. All ten outputs passed the runtime's asset, spline and collision +load checks. This does not establish visual or gameplay parity, and the +original ISO extraction step still needs checking with an ISO. + +Two retail spline details needed corrections. Downtown contains duplicate +knots and sub-millimetre chords. The native cubic records remain intact; +only unusable contact primitives are omitted. Skate School's first spline +header word is `0001 0002`, meaning two rails, not 65,538. TU3 function +`82C1EEF0` reads the count with `lhz +2` (also at `82C1EFBC`). The reader now +uses that halfword and retains the other halfword in the map metadata. diff --git a/tools/asset_pipeline/install.py b/tools/asset_pipeline/install.py index 9c22617d..5122602a 100644 --- a/tools/asset_pipeline/install.py +++ b/tools/asset_pipeline/install.py @@ -98,7 +98,9 @@ def convert_map(archive,work,maps,stage,game_exe,log,report): manifest_path=prepare(stream_directory=stream,output_root=district_work/'intermediate', utt_root=TOOLS/'vendor/utt',district_name=district,map_name=label, package_name='Skate 3 owned disc',cache_format='skate3-rust-map-v1', - texture_stream_names=('Tex',),excluded_normal_texture_ids=EXCLUDED_NORMAL_TEXTURE_IDS) + # Smaller parks keep their textures in Pres rather than a Tex stream. + texture_stream_names=('Tex',) if any(stream.glob('cTex_*.xsf')) else (), + excluded_normal_texture_ids=EXCLUDED_NORMAL_TEXTURE_IDS) collision=district_work/'collision.rwcmset' build_archive(manifest_path,collision) final=maps/(label+'.skate') diff --git a/tools/vendor/university/tools/vanilla_map_extraction/tools/retail_grind_splines.py b/tools/vendor/university/tools/vanilla_map_extraction/tools/retail_grind_splines.py index ad8d8180..73ff8e11 100644 --- a/tools/vendor/university/tools/vanilla_map_extraction/tools/retail_grind_splines.py +++ b/tools/vendor/university/tools/vanilla_map_extraction/tools/retail_grind_splines.py @@ -168,8 +168,11 @@ def decode_grind_splines(data: bytes) -> list[dict[str, object]]: ): raise ValueError("RX2 spline section extends beyond the file") - rail_count, segment_count, rail_table, segment_table = ( - struct.unpack_from(">4I", data, section_offset) + # TU3 GrindData::Add (82C1EEF0) reads lhz +2, including its loop + # bound at 82C1EFBC. The first halfword is not part of the count: + # Skate School has header 0001 0002, two rails, table end 0x50. + header_halfword, rail_count, segment_count, rail_table, segment_table = ( + struct.unpack_from(">HH3I", data, section_offset) ) expected_segment_table = ( SPLINE_HEADER_SIZE + rail_count * SPLINE_RAIL_SIZE @@ -271,6 +274,7 @@ def decode_grind_splines(data: bytes) -> list[dict[str, object]]: { "section_index": record_index, "section_offset": section_offset, + "section_header_halfword": header_halfword, "rail_index": rail_index, "spline_id": f"0x{spline_id:016X}", "type_signature": f"0x{type_signature:016X}",