From 12cf2ec5d4318072df5109bff7fc139d816ec832 Mon Sep 17 00:00:00 2001 From: Ethanw05 <46205865+Ethanw05@users.noreply.github.com> Date: Tue, 8 Sep 2026 17:55:26 -0600 Subject: [PATCH] Preserve animation safeguards and implement native deck angles after SDK merge Retain focused regression coverage and consolidate build/launch scripts with release workflow updates. Validated game build and four regressions including 5400 simulation ticks. --- .github/workflows/release.yml | 2 +- BUILD.bat | 5 +- PLAY-MAP.bat | 8 --- PLAY.bat | 5 +- README.md | 4 +- Select-Map.ps1 | 13 ----- crates/skate-core/src/animation/output/mod.rs | 2 + crates/skate-core/src/animation/pose_blend.rs | 9 ++++ .../src/animation/tests/pose_blend.rs | 12 +++++ .../player/input_phase/deck_angles_tests.rs | 19 +++++++ .../src/player/input_phase/pose_output.rs | 25 +++++++++ crates/skate-game/src/graph_host/motion.rs | 7 +++ .../src/graph_host/motion_execute_leaf.rs | 14 ++++- .../skate-game/src/graph_host/motion_nodes.rs | 8 ++- .../graph_host/tests/motion_deck_angles.rs | 48 +++++++++++++++++ .../skate-game/src/physics/animation_phase.rs | 4 ++ crates/skate-game/src/physics/frame.rs | 4 ++ .../THIRD_PARTY_NOTICES.md | 0 docs/crash-reports.md | 51 +++++++++++++++++++ .../Build-Release.ps1 | 25 ++++----- Build.ps1 => scripts/Build.ps1 | 7 +-- Launch.ps1 => scripts/Launch.ps1 | 13 ++--- 22 files changed, 233 insertions(+), 52 deletions(-) delete mode 100644 PLAY-MAP.bat delete mode 100644 Select-Map.ps1 create mode 100644 crates/skate-core/src/player/input_phase/deck_angles_tests.rs create mode 100644 crates/skate-game/src/graph_host/tests/motion_deck_angles.rs rename THIRD_PARTY_NOTICES.md => docs/THIRD_PARTY_NOTICES.md (100%) rename Build-Release.ps1 => scripts/Build-Release.ps1 (87%) rename Build.ps1 => scripts/Build.ps1 (94%) rename Launch.ps1 => scripts/Launch.ps1 (73%) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 89ecbdcb..61e8cbc2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: shell: pwsh env: RELEASE_TAG: ${{ github.event.release.tag_name }} - run: ./Build-Release.ps1 + run: ./scripts/Build-Release.ps1 - uses: actions/upload-artifact@v4 with: name: skate3rust-windows-x64 diff --git a/BUILD.bat b/BUILD.bat index a2280f87..aa1a0b3d 100644 --- a/BUILD.bat +++ b/BUILD.bat @@ -1,4 +1,7 @@ @echo off +setlocal cd /d "%~dp0" -powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Build.ps1" +set "PATH=%USERPROFILE%\.cargo\bin;%PATH%" +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\Build.ps1" if errorlevel 1 pause + diff --git a/PLAY-MAP.bat b/PLAY-MAP.bat deleted file mode 100644 index bfa6774b..00000000 --- a/PLAY-MAP.bat +++ /dev/null @@ -1,8 +0,0 @@ -@echo off -cd /d "%~dp0" -if "%~1"=="" ( - powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Select-Map.ps1" -) else ( - powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Select-Map.ps1" -Map "%~1" -) -if errorlevel 1 pause diff --git a/PLAY.bat b/PLAY.bat index d1990da6..eb5bb766 100644 --- a/PLAY.bat +++ b/PLAY.bat @@ -1,8 +1,9 @@ @echo off cd /d "%~dp0" if "%~1"=="" ( - powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Launch.ps1" + powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\Launch.ps1" ) else ( - powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Launch.ps1" -Map "%~1" + powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\Launch.ps1" -Map "%~1" ) if errorlevel 1 pause + diff --git a/README.md b/README.md index aaa67fa2..60028b51 100644 --- a/README.md +++ b/README.md @@ -34,11 +34,11 @@ settings. Maps can be switched without restarting the game. Requires Windows, Rust with the MSVC toolchain, and LLVM installed in its default location. Run `BUILD.bat` to build, then `PLAY.bat` to launch the test world. -Use `PLAY-MAP.bat` to select a map. An XInput controller is required for gameplay; +`PLAY.bat` opens your saved map (University by default); use the in-game menu to switch maps, or drag a `.skate` file onto `PLAY.bat`. An XInput controller is required for gameplay; Escape opens difficulty and graphics settings. Development builds use a prepared asset set in `assets/private/` or the -installed asset directory. `Build-Release.ps1` builds the portable Windows +installed asset directory. `scripts/Build-Release.ps1` builds the portable Windows package and requires Python 3.13. Published GitHub releases build and attach the package automatically. diff --git a/Select-Map.ps1 b/Select-Map.ps1 deleted file mode 100644 index 62b5615d..00000000 --- a/Select-Map.ps1 +++ /dev/null @@ -1,13 +0,0 @@ -param([string]$Map) -$ErrorActionPreference = 'Stop' -if (-not $Map) { - $maps = @(Get-ChildItem -LiteralPath (Join-Path $PSScriptRoot 'maps') -Filter '*.skate' -File -Recurse | Sort-Object FullName) - if ($maps.Count -eq 0) { throw 'Put a .skate file in maps/private, or drag one onto PLAY-MAP.bat.' } - for ($i = 0; $i -lt $maps.Count; $i++) { Write-Host "$($i + 1). $($maps[$i].Name)" } - $choice = Read-Host 'Map number (Enter cancels)' - if (-not $choice) { exit 0 } - $selected = 0 - if (-not [int]::TryParse($choice, [ref]$selected) -or $selected -lt 1 -or $selected -gt $maps.Count) { throw 'Invalid map number.' } - $Map = $maps[$selected - 1].FullName -} -& (Join-Path $PSScriptRoot 'Launch.ps1') -Map $Map diff --git a/crates/skate-core/src/animation/output/mod.rs b/crates/skate-core/src/animation/output/mod.rs index ed1c7371..e1302ee4 100644 --- a/crates/skate-core/src/animation/output/mod.rs +++ b/crates/skate-core/src/animation/output/mod.rs @@ -18,6 +18,8 @@ pub type NativeMatrix = [[f32; 4]; 4]; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum PoseBufferError { + NonFiniteWeight, + InvalidQuaternion, CountOverflow, ShortInput, ShortOutput, diff --git a/crates/skate-core/src/animation/pose_blend.rs b/crates/skate-core/src/animation/pose_blend.rs index 5d00abfc..72d2ece7 100644 --- a/crates/skate-core/src/animation/pose_blend.rs +++ b/crates/skate-core/src/animation/pose_blend.rs @@ -65,6 +65,9 @@ pub fn weighted(poses: &[Vec], weights: &[f32]) -> Result, PoseBuf if poses.len()!=weights.len() || poses.iter().any(|p| p.len()!=first.len()) { return Err(PoseBufferError::ShortInput); } + if weights.iter().any(|weight| !weight.is_finite()) { + return Err(PoseBufferError::NonFiniteWeight); + } let mut output=first.iter().map(|s| Sqt { scale:s.scale.map(|v| v*weights[0]), rotation:s.rotation.map(|v| v*weights[0]), @@ -81,11 +84,17 @@ pub fn weighted(poses: &[Vec], weights: &[f32]) -> Result, PoseBuf } for out in &mut output { let squared=native_arithmetic::dot4(out.rotation,out.rotation); + if !squared.is_finite() || squared <= 0.0 { + return Err(PoseBufferError::InvalidQuaternion); + } let mut inverse=native_arithmetic::reciprocal_square_root_estimate(squared); for _ in 0..2 { inverse=(inverse*0.5).mul_add((-squared).mul_add(inverse*inverse,1.),inverse); } out.rotation=out.rotation.map(|v| v*inverse); + if out.rotation.iter().any(|v| !v.is_finite()) { + return Err(PoseBufferError::InvalidQuaternion); + } } Ok(output) } diff --git a/crates/skate-core/src/animation/tests/pose_blend.rs b/crates/skate-core/src/animation/tests/pose_blend.rs index 28cd510f..726fd180 100644 --- a/crates/skate-core/src/animation/tests/pose_blend.rs +++ b/crates/skate-core/src/animation/tests/pose_blend.rs @@ -13,6 +13,18 @@ fn bump_multi_blend_accumulates_before_normalizing_without_hemisphere_flip() { for i in 0..4 {assert!((output.rotation[i]-raw[i]/norm).abs()<1e-6);} } +#[test] +fn weighted_rejects_invalid_weights_and_rotations() { + let identity = pose([0.0, 0.0, 0.0, 1.0]); + for weight in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] { + assert_eq!(super::weighted(&[vec![identity]], &[weight]), Err(super::PoseBufferError::NonFiniteWeight)); + } + for rotation in [[0.0; 4], [f32::INFINITY; 4], [f32::NAN; 4]] { + assert_eq!(super::weighted(&[vec![pose(rotation)]], &[1.0]), Err(super::PoseBufferError::InvalidQuaternion)); + } + assert!(super::weighted(&[vec![identity]], &[1.0]).is_ok()); +} + fn pose(rotation: [f32; 4]) -> Sqt { Sqt { scale: [1.0; 4], diff --git a/crates/skate-core/src/player/input_phase/deck_angles_tests.rs b/crates/skate-core/src/player/input_phase/deck_angles_tests.rs new file mode 100644 index 00000000..058e858e --- /dev/null +++ b/crates/skate-core/src/player/input_phase/deck_angles_tests.rs @@ -0,0 +1,19 @@ +use super::SkeletonOutputFields; + +#[test] +fn deck_angles_preserve_native_quadrants_stance_and_pitch_projection() { + let mut output = SkeletonOutputFields::default(); + let pi = std::f32::consts::PI; + for (x, z, expected) in [ + (1.0, 1.0, pi / 4.0), (-1.0, 1.0, -pi / 4.0), + (1.0, -1.0, 3.0 * pi / 4.0), (-1.0, -1.0, -3.0 * pi / 4.0), + (0.0, 0.0, pi / 2.0), (-0.0, -0.0, -pi / 2.0), + ] { + output.publish_deck_angles([x, 0.5, z, 0.0], false); + assert!((output.deck_yaw_536 - expected).abs() < 0.00001); + assert_eq!(output.deck_pitch_540, 0.5); + } + output.publish_deck_angles([1.0, 0.5, 1.0, 0.0], true); + assert!((output.deck_yaw_536 + 3.0 * pi / 4.0).abs() < 0.00001); + assert_eq!(output.deck_pitch_540, -0.5); +} diff --git a/crates/skate-core/src/player/input_phase/pose_output.rs b/crates/skate-core/src/player/input_phase/pose_output.rs index 0105565c..238949a7 100644 --- a/crates/skate-core/src/player/input_phase/pose_output.rs +++ b/crates/skate-core/src/player/input_phase/pose_output.rs @@ -8,6 +8,9 @@ pub struct SkeletonOutputFields { pub hips_up_angle_500: f32, /// Skeleton Fill82BE2268: signed physical twist, before animation mirroring. pub twist_504: f32, + /// Skeleton Fill82BE1C84/82BE1D50: animation deck forward, stance-adjusted. + pub deck_yaw_536: f32, + pub deck_pitch_540: f32, pub scalar_544: f32, pub no_support_time_548: f32, pub time_until_teleport_576: f32, @@ -24,6 +27,24 @@ pub struct SkeletonOutputFields { } impl SkeletonOutputFields { + /// Fill82BE1AE8: Skeleton6512 is animation-record pose0's forward column. + /// Packet10368 flips that vector; pitch is its dot with world up, not asin. + pub fn publish_deck_angles(&mut self, forward: [f32; 4], board_flipped: bool) { + use crate::physics::native_arithmetic::{dot3, reciprocal_estimate}; + let forward = if board_flipped { forward.map(|v| -v) } else { forward }; + let [x, _, z, _] = forward; + let reciprocal = reciprocal_estimate(z); + let refined = reciprocal.mul_add((-reciprocal).mul_add(z, 1.0), reciprocal); + let basic = crate::input::angle::atan(x.mul_add(refined, 0.0)); + let sign = x.to_bits() & 0x8000_0000; + let pi = f32::from_bits(0x4049_0fdb | sign); + let half_pi = f32::from_bits(0x3fc9_0fdb | sign); + let angle = if 0.0 > z { pi + basic } else { basic }; + // Native equality selection includes +/-zero, even when x is also zero. + self.deck_yaw_536 = if z == 0.0 { half_pi } else { angle }; + self.deck_pitch_540 = dot3(forward, [0.0, 1.0, 0.0, 0.0]); + } + /// Skeleton Fill82BE2148..2268. Read the completed physical record, the /// current input toolkit's Processed352 and Reckoning1152. No pose fitting /// or animation mirror: GrindControlFade applies that mirror once on Begin. @@ -74,3 +95,7 @@ impl Default for AnimationOutputFields { pub struct ScoringOutputFields { pub capabilities_204: u32, } + +#[cfg(test)] +#[path = "deck_angles_tests.rs"] +mod deck_angles_tests; diff --git a/crates/skate-game/src/graph_host/motion.rs b/crates/skate-game/src/graph_host/motion.rs index 985beab1..0110a7fd 100644 --- a/crates/skate-game/src/graph_host/motion.rs +++ b/crates/skate-game/src/graph_host/motion.rs @@ -65,6 +65,8 @@ pub struct MotionHost { pub condition_inputs: ConditionInputs, pub gameplay_conditions: Option, pub manual_exit: Option, + /// Completed Skeleton PhysOut536/540, in native yaw-then-pitch order. + pub deck_yaw_pitch: Option<[f32; 2]>, pub riding_conditions: Option, pub condition_random: super::motion_riding_conditions::MotionRandom, pub native_physical: Option, @@ -221,6 +223,7 @@ impl MotionHost { condition_inputs: ConditionInputs::default(), gameplay_conditions: None, manual_exit: None, + deck_yaw_pitch: None, riding_conditions: None, condition_random: super::motion_riding_conditions::MotionRandom::new(), native_physical: None, @@ -417,3 +420,7 @@ mod slide_tests; #[cfg(test)] #[path = "tests/motion_jump_into.rs"] mod jump_into_tests; + +#[cfg(test)] +#[path = "tests/motion_deck_angles.rs"] +mod deck_angles_tests; diff --git a/crates/skate-game/src/graph_host/motion_execute_leaf.rs b/crates/skate-game/src/graph_host/motion_execute_leaf.rs index 0b68aa9e..104fe461 100644 --- a/crates/skate-game/src/graph_host/motion_execute_leaf.rs +++ b/crates/skate-game/src/graph_host/motion_execute_leaf.rs @@ -45,8 +45,18 @@ pub(super) fn execute( _ => state.end(&mut host.animation), } } - (MotionOperation::SetDeckPitchAndYaw, _) => { - return Err("SetDeckPitchAndYaw has no recovered native implementation".into()); + (MotionOperation::SetDeckPitchAndYaw { yaw, pitch }, _) => { + // Begin82BAF100 copies completed Skeleton536 then540. + // Vtable8231FF24 Update/End both point to empty82B61BB8. + if phase == 0 { + let values = host.deck_yaw_pitch + .ok_or("SetDeckPitchAndYaw requires completed Skeleton output")?; + for (name, value) in [yaw, pitch].into_iter().zip(values) { + host.animation.set_attribute(SettableAttribute { + name, value, normalized: false, sequence_id: -1, + }); + } + } } //Retail ctor82BA58E8 retains only diagnostic text/layout. All three //lifecycle slots in82309664 are82B61BB8 (blr), with no state writes. diff --git a/crates/skate-game/src/graph_host/motion_nodes.rs b/crates/skate-game/src/graph_host/motion_nodes.rs index f07d511b..c4940341 100644 --- a/crates/skate-game/src/graph_host/motion_nodes.rs +++ b/crates/skate-game/src/graph_host/motion_nodes.rs @@ -16,7 +16,7 @@ use skate_data::state_graph::{ pub enum MotionOperation { Grind(super::motion_grind::Operation), Trick(super::motion_tricks::Operation), - SetDeckPitchAndYaw, + SetDeckPitchAndYaw { yaw: AttributeName, pitch: AttributeName }, Play(PlayAnimation), ///TU3 vtable82309664: Begin/Update/End all point to the empty82B61BB8. PrintText2D, @@ -115,7 +115,11 @@ impl OperationFactory for MotionFactory { Some(MotionOperation::Shove(operation)) } else { match name { - "SetDeckPitchAndYaw" => Some(MotionOperation::SetDeckPitchAndYaw), + "SetDeckPitchAndYaw" => Some(MotionOperation::SetDeckPitchAndYaw { + // Native constructor82BC7B20 retains authored FastStrings. + yaw: encode(a.text("skateyaw").unwrap_or("skateyaw").as_bytes()), + pitch: encode(a.text("skatepitch").unwrap_or("skatepitch").as_bytes()), + }), "ResetSkaterAnimation" | "ResetToGivenStance" => Some(MotionOperation::ResetAnimation( super::motion_reset::Operation::parse(a).ok_or("Invalid reset operation")?, )), diff --git a/crates/skate-game/src/graph_host/tests/motion_deck_angles.rs b/crates/skate-game/src/graph_host/tests/motion_deck_angles.rs new file mode 100644 index 00000000..86304522 --- /dev/null +++ b/crates/skate-game/src/graph_host/tests/motion_deck_angles.rs @@ -0,0 +1,48 @@ +use super::*; + +#[test] +#[ignore = "requires private stock graphs and animation banks"] +fn stock_deck_angles_copy_completed_output_only_on_begin() { + use crate::graph_runtime::CompiledGraph; + use skate_data::state_graph::{StateGraph, binding::Binding}; + let root = std::path::PathBuf::from(std::env::var_os("SKATE3_ASSET_ROOT").unwrap()); + let source = StateGraph::load(&root.join( + "private/stock/data/state/MotionGraph_OnBoard.stategraph", + )).unwrap(); + let binding = Binding::from_graph(&source).unwrap(); + let runtime = CompiledGraph::from_binding(&binding).unwrap(); + let graph = LoadedGraph { source, binding, runtime }; + let mut host = MotionHost::from_graph( + &graph, &Collections::load(&root).unwrap(), + skate_data::animation_banks::AnimationBanks::load(&root).unwrap().metadata().unwrap(), + PlaybackContext { is_switch: Some(false), is_mirrored: Some(false), + board_available: Some(true), pro_skater: encode(b""), transition_override: None }, + ).unwrap(); + let behavior = host.remap.behaviors.iter().position(|&id| matches!( + host.operations[id], MotionOperation::SetDeckPitchAndYaw { .. } + )).expect("stock graph must contain the reported behavior"); + let op = host.remap.behaviors[behavior]; + assert_eq!(host.operations[op], MotionOperation::SetDeckPitchAndYaw { + yaw: encode(b"skateyaw"), pitch: encode(b"skatepitch"), + }); + let frame = Frame { dt: 1.0 / 60.0, current: None, last: None, state_times: vec![] }; + let handle = host.allocate(behavior, &frame); + assert!(host.execute(behavior, &frame, 0).is_err()); + let mut output = skate_core::player::input_phase::SkeletonOutputFields::default(); + output.publish_deck_angles([1.0, 0.5, 1.0, 0.0], true); + let values = [output.deck_yaw_536, output.deck_pitch_540]; + host.deck_yaw_pitch = Some(values); + host.execute(behavior, &frame, 0).unwrap(); + assert_eq!(host.animation.pending_parameter(encode(b"skateyaw")), Some(values[0])); + assert_eq!(host.animation.pending_parameter(encode(b"skatepitch")), Some(values[1])); + host.deck_yaw_pitch = Some([9.0, 8.0]); + host.execute(behavior, &frame, 1).unwrap(); + host.execute(behavior, &frame, 2).unwrap(); + assert_eq!(host.animation.pending_parameter(encode(b"skateyaw")), Some(values[0])); + assert_eq!(host.animation.pending_parameter(encode(b"skatepitch")), Some(values[1])); + host.release(handle); + host.allocate(behavior, &frame); + host.execute(behavior, &frame, 0).unwrap(); + assert_eq!(host.animation.pending_parameter(encode(b"skateyaw")), Some(9.0)); + assert_eq!(host.animation.pending_parameter(encode(b"skatepitch")), Some(8.0)); +} diff --git a/crates/skate-game/src/physics/animation_phase.rs b/crates/skate-game/src/physics/animation_phase.rs index 2c370afb..f971996c 100644 --- a/crates/skate-game/src/physics/animation_phase.rs +++ b/crates/skate-game/src/physics/animation_phase.rs @@ -33,6 +33,10 @@ pub(crate) fn advance( let p = &skater.player_input.processed; let physical = &skater.player_input.physical; skater.animation.motion.manual_exit = Some(physical.animation.manual_opposition_168 != 0); + skater.animation.motion.deck_yaw_pitch = Some([ + physical.skeleton.deck_yaw_536, + physical.skeleton.deck_pitch_540, + ]); // ProcessOutput82DB7250/7258 publishes shared Biped720/716 as //OffBoard80/84. BipedCadence/MatchCadence/LocoState consume that completed //physical packet, preserving the publication boundary across transitions. diff --git a/crates/skate-game/src/physics/frame.rs b/crates/skate-game/src/physics/frame.rs index 0e6086cb..c155dca7 100644 --- a/crates/skate-game/src/physics/frame.rs +++ b/crates/skate-game/src/physics/frame.rs @@ -244,6 +244,10 @@ pub(super) fn advance( .player_input .update_dynamic_normal(&physics.riding, simulation.gravity_acceleration); skater.player_input.publish_board(&physics.riding)?; + skater.player_input.physical.skeleton.publish_deck_angles( + skater.animated_skeleton.record.pose[0][2], + skater.animation.packet.board_flipped, + ); let up = physics.riding.reckoning.up; skater.player_input.publish_grind_graph_outputs( &skater.skeleton.record, diff --git a/THIRD_PARTY_NOTICES.md b/docs/THIRD_PARTY_NOTICES.md similarity index 100% rename from THIRD_PARTY_NOTICES.md rename to docs/THIRD_PARTY_NOTICES.md diff --git a/docs/crash-reports.md b/docs/crash-reports.md index 561d1a5f..ddbb544a 100644 --- a/docs/crash-reports.md +++ b/docs/crash-reports.md @@ -11,3 +11,54 @@ Coverage limits: a Windows unhandled-exception filter attempts a fixed, allocati `Build-Release.ps1` links the release directly into fresh private staging and retains a matching EXE/PDB pair plus hashes and revision in the sibling `symbols` folder, outside the player ZIP. Keep that folder with each release for offline symbol matching; it contains no original Skate assets. Release line tables are enabled and stripping is disabled. Dirty builds are explicitly marked and carry a unique build timestamp; release from a clean commit for reproducible source matching. Development launches (`cargo run`, `PLAY.bat`, multiplayer and packaged executables) use the same supervisor automatically. For manual UI validation, run the executable with `--crash-report-preview`. This generates a clearly synthetic report and opens the popup without initializing the game, setup, Steam or assets. Check Copy report, clipboard text, Open folder, saved UTF-8 text and Close. Static tests do not validate desktop presentation. + +## SetDeckPitchAndYaw (2026-09-08) + +The supplied report stopped in KnownAir at tick4813, MotionGraph behavior3373: +`SetDeckPitchAndYaw has no recovered native implementation`. This was an +unimplemented graph behavior, not an animation overflow. + +TU3 factory82BC7B20 retains the authored `skateyaw`/`skatepitch` attribute names +(defaulting to those same strings). Vtable8231FF24 calls Begin82BAF100, which +copies completed Skeleton PhysOut536 then540 to scalar animation parameters. +Update and End both point to empty82B61BB8. Skate2 constructor82CB03E8 and +Begin82CB04D0 corroborate the same behavior and output offsets. + +The connected producer is Skeleton::Fill82BE1AE8: read Skeleton6512 +(animation-record pose0 column2), negate it when animation packet10368 is +nonzero, calculate yaw from X/Z with native atan82473B98 and quadrant/zero +selection, and calculate pitch as DOT3 with up82139A20. Pitch is the signed +forward-Y projection, not an Euler angle or asin. VMX estimate bit identity is +not claimed. + +`SkeletonOutputFields::publish_deck_angles` implements those calculations. +The frame output phase publishes them after reset/physics completion from +`animated_skeleton.record.pose[0][2]` and the current packet's `board_flipped`. +The next animation phase supplies that retained output to MotionHost; the +behavior copies it on Begin only. No extra per-frame attribute writer or +missing-output fallback was added. + +Saved native evidence: research root +`physics-comparison-2026-09-02/evidence/deck-pitch-yaw-20260908`; +the existing Fill decompilation is in `air-motion-graph-2026-09-05` and its +decoded instructions in `riding-outputs-2026-09-04/skate3.decoded-82be1ae8.txt`. +Read-only extraction verified the saved database hashes were unchanged. + +Focused regressions cover native quadrants, zero denominators, flipped stance, +raw pitch projection, stock graph binding, missing-output errors and +Begin/Update/End/reentry. These do not replay the user's original input sequence +or establish rendered gameplay parity. + +Validation (2026-09-08): + +- `cargo test -p skate-core deck_angles --locked`: 1 passed. +- `cargo test -p skate-game --bin skate3rust stock_deck_angles --locked -- --ignored`: + 1 passed against `SKATE3_ASSET_ROOT` pointing to the clone's `assets` directory. +- The existing + `raw_repeated_walk_turn_throw_recall_jump_remount_preserves_full_roots_and_joints` + test passed: 5,400 uninterrupted ticks, three walking/turning/throw/recall/jump/ + remount cycles, 7.92 seconds, with its original assertions and collision fixture. + This is not the University crash input replay. +- The first game-test compilation found the missing procedural + `maps/format-demo.skate` fixture. Regenerated it with `tools/make_skate_demo.py`; + no stock world or test assertions were replaced. diff --git a/Build-Release.ps1 b/scripts/Build-Release.ps1 similarity index 87% rename from Build-Release.ps1 rename to scripts/Build-Release.ps1 index 2784498b..d7c6b295 100644 --- a/Build-Release.ps1 +++ b/scripts/Build-Release.ps1 @@ -1,8 +1,9 @@ -param([string]$TargetDirectory = (Join-Path $PSScriptRoot 'target')) +param([string]$TargetDirectory = (Join-Path (Split-Path $PSScriptRoot -Parent) 'target')) +$ProjectRoot = Split-Path $PSScriptRoot -Parent $ErrorActionPreference = 'Stop' -Push-Location $PSScriptRoot +Push-Location $ProjectRoot try { - $packagePython = Join-Path $PSScriptRoot 'target/package-venv/Scripts/python.exe' + $packagePython = Join-Path $ProjectRoot 'target/package-venv/Scripts/python.exe' if (-not (Test-Path -LiteralPath $packagePython)) { & python -m venv target/package-venv if ($LASTEXITCODE -ne 0) { throw 'Could not create packaging environment' } @@ -11,8 +12,8 @@ try { if ($LASTEXITCODE -ne 0) { throw 'Could not install packaging dependencies' } $env:CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS = '-C target-feature=+crt-static' $stamp = Get-Date -Format 'yyyyMMdd-HHmmss-ffff' - $stage = Join-Path $PSScriptRoot "target/release-packages/$stamp/skate3rust-windows-x64" - $symbols = Join-Path $PSScriptRoot "target/release-packages/$stamp/symbols" + $stage = Join-Path $ProjectRoot "target/release-packages/$stamp/skate3rust-windows-x64" + $symbols = Join-Path $ProjectRoot "target/release-packages/$stamp/symbols" New-Item -ItemType Directory -Path "$stage/support",$symbols -Force | Out-Null # Link this invocation directly into private staging; never copy a generic cache EXE. & cargo rustc --release --locked --target x86_64-pc-windows-msvc --target-dir $TargetDirectory -p skate-game --bin skate3rust --no-default-features -- -C extra-filename= -o "$stage/skate3rust.exe" -C "link-arg=/PDB:$symbols/skate3rust.pdb" @@ -36,7 +37,7 @@ try { New-Item -ItemType Directory -Path "$stage/mods" -Force | Out-Null Copy-Item -LiteralPath mods/native-trainer.zip,mods/mario-kart.zip,mods/README.md -Destination "$stage/mods" $sourceStage = Join-Path $stage '../setup-source' - $toolsRoot = Join-Path $PSScriptRoot 'tools' + $toolsRoot = Join-Path $ProjectRoot 'tools' foreach ($source in Get-ChildItem -LiteralPath $toolsRoot -File -Recurse) { if ($source.FullName -match '[\\/]__pycache__[\\/]') { continue } if ($source.Extension -notin '.py','.json','.txt','.md','.toml' -and $source.Name -ne 'LICENSE') { continue } @@ -50,12 +51,12 @@ try { Copy-Item -LiteralPath $source.FullName -Destination $destination } & $packagePython -m PyInstaller --noconfirm --clean --onefile --name skate3setup ` - --icon "$PSScriptRoot/docs/images/skating-crab.ico" --paths $PSScriptRoot ` + --icon "$ProjectRoot/docs/images/skating-crab.ico" --paths $ProjectRoot ` --hidden-import numpy --hidden-import PIL.Image --hidden-import tkinter ` - --add-binary "$PSScriptRoot/target/native/refpack.dll;tools/asset_pipeline" ` + --add-binary "$ProjectRoot/target/native/refpack.dll;tools/asset_pipeline" ` --exclude-module bpy --exclude-module mathutils ` --copy-metadata numpy --copy-metadata Pillow --copy-metadata PyInstaller ` - --add-data "$sourceStage/tools;tools" --add-data "$PSScriptRoot/docs/images/skating-crab.ico;docs/images" ` + --add-data "$sourceStage/tools;tools" --add-data "$ProjectRoot/docs/images/skating-crab.ico;docs/images" ` --distpath "$stage/support" --workpath target/setup-build/work --specpath target/setup-build tools/setup.py if ($LASTEXITCODE -ne 0) { throw 'Setup packaging failed' } & $packagePython -m PyInstaller --noconfirm --clean --onefile --windowed --name skate3update ` @@ -73,8 +74,8 @@ try { schema = 1; repository = 'SK8-ENGINE/skate-3-rust-engine'; target = 'windows-x64' build = $build; tag = $tag; revision = (& git rev-parse HEAD).Trim(); files = $files } | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath "$stage/release.json" -Encoding utf8 - Copy-Item -LiteralPath "$stage/release.json" -Destination (Join-Path $PSScriptRoot 'target/release.json') - Copy-Item -LiteralPath README.md,THIRD_PARTY_NOTICES.md -Destination $stage + Copy-Item -LiteralPath "$stage/release.json" -Destination (Join-Path $ProjectRoot 'target/release.json') + Copy-Item -LiteralPath README.md,docs/THIRD_PARTY_NOTICES.md -Destination $stage New-Item -ItemType Directory -Path "$stage/docs/images" -Force | Out-Null Copy-Item -LiteralPath docs/images/skating-crab.png -Destination "$stage/docs/images/skating-crab.png" Copy-Item -LiteralPath docs/installation.md -Destination "$stage/docs/installation.md" @@ -91,7 +92,7 @@ try { foreach ($license in Get-ChildItem -LiteralPath "$pythonBase/tcl" -Filter license.terms -Recurse -ErrorAction SilentlyContinue) { Copy-Item -LiteralPath $license.FullName -Destination "$stage/licenses/$($license.Directory.Name).txt" } - $zip = Join-Path $PSScriptRoot 'target/skate3rust-windows-x64.zip' + $zip = Join-Path $ProjectRoot 'target/skate3rust-windows-x64.zip' Compress-Archive -LiteralPath $stage -DestinationPath $zip -Force (Get-FileHash -LiteralPath $zip -Algorithm SHA256).Hash.ToLower() + ' skate3rust-windows-x64.zip' | Set-Content -LiteralPath "$zip.sha256" -Encoding ascii diff --git a/Build.ps1 b/scripts/Build.ps1 similarity index 94% rename from Build.ps1 rename to scripts/Build.ps1 index d4b559d4..f8bc82c4 100644 --- a/Build.ps1 +++ b/scripts/Build.ps1 @@ -1,6 +1,7 @@ -param([switch]$StageOnly, [string]$TargetDirectory = (Join-Path $PSScriptRoot 'target')) +param([switch]$StageOnly, [string]$TargetDirectory = (Join-Path (Split-Path $PSScriptRoot -Parent) 'target')) +$ProjectRoot = Split-Path $PSScriptRoot -Parent $ErrorActionPreference = 'Stop' -Push-Location $PSScriptRoot +Push-Location $ProjectRoot try { if (-not $StageOnly) { & cargo build -p skate-game --locked --target-dir $TargetDirectory @@ -13,7 +14,7 @@ try { if (-not (Test-Path -LiteralPath $readobj)) { throw 'LLVM llvm-readobj is required to stage exact runtime DLL dependencies.' } $rustLibraries = (& rustc --print target-libdir).Trim() if ($LASTEXITCODE -ne 0) { throw 'Could not locate Rust runtime libraries.' } - $binDirectory = Join-Path $PSScriptRoot 'bin' + $binDirectory = Join-Path $ProjectRoot 'bin' New-Item -ItemType Directory -Path $binDirectory -Force | Out-Null $queue = [System.Collections.Generic.Queue[string]]::new() $queue.Enqueue($executable) diff --git a/Launch.ps1 b/scripts/Launch.ps1 similarity index 73% rename from Launch.ps1 rename to scripts/Launch.ps1 index 02ebffb2..70fed713 100644 --- a/Launch.ps1 +++ b/scripts/Launch.ps1 @@ -1,15 +1,16 @@ param([string]$Map) +$ProjectRoot = Split-Path $PSScriptRoot -Parent $ErrorActionPreference = 'Stop' -Push-Location $PSScriptRoot +Push-Location $ProjectRoot try { - $executable = Join-Path $PSScriptRoot 'bin/skate3rust.exe' + $executable = Join-Path $ProjectRoot 'bin/skate3rust.exe' if (-not (Test-Path -LiteralPath $executable)) { throw 'Game is not built. Run BUILD.bat first.' } - New-Item -ItemType Directory -Path (Join-Path $PSScriptRoot 'logs') -Force | Out-Null - $log = Join-Path $PSScriptRoot ('logs/game-' + (Get-Date -Format 'yyyyMMdd-HHmmss') + '.log') + New-Item -ItemType Directory -Path (Join-Path $ProjectRoot 'logs') -Force | Out-Null + $log = Join-Path $ProjectRoot ('logs/game-' + (Get-Date -Format 'yyyyMMdd-HHmmss') + '.log') Write-Host 'Starting Skate 3 Rust Engine. Use your XInput controller; Esc opens difficulty/graphics/pause settings.' Write-Host "Log: $log" $errorLog = [System.IO.Path]::ChangeExtension($log, 'stderr.log') - $assetArgument = '"' + (Join-Path $PSScriptRoot 'assets') + '"' + $assetArgument = '"' + (Join-Path $ProjectRoot 'assets') + '"' $gameArguments = @('--assets', $assetArgument) if ($Map) { $mapPath = (Resolve-Path -LiteralPath $Map).Path @@ -17,7 +18,7 @@ try { $gameArguments += @('--map', ('"' + $mapPath + '"')) Write-Host "Map: $mapPath" } - $game = Start-Process -FilePath $executable -WorkingDirectory $PSScriptRoot ` + $game = Start-Process -FilePath $executable -WorkingDirectory $ProjectRoot ` -ArgumentList $gameArguments -NoNewWindow -Wait -PassThru ` -RedirectStandardOutput $log -RedirectStandardError $errorLog if ($game.ExitCode -ne 0) {