From ae452ba3fe073bfe2b8cadd9be08d4e750105764 Mon Sep 17 00:00:00 2001 From: chazmm Date: Tue, 8 Sep 2026 20:51:37 +0100 Subject: [PATCH] Add archive manual support and isolated riding fixes to existing build --- crates/skate-core/src/animation/manual.rs | 42 +++++++++++ crates/skate-core/src/animation/mod.rs | 1 + .../skate-core/src/input/manual_intentions.rs | 30 ++++++++ crates/skate-core/src/input/mod.rs | 1 + .../src/physics/board_dynamic_normal.rs | 32 +++------ .../tests/board_dynamic_normal_history.rs | 64 +++++++++++++++++ .../src/player/input_phase/pose_output.rs | 4 +- .../tests/manual_input_animation.rs | 69 +++++++++++++++++++ crates/skate-data/src/collections.rs | 54 +++++++++++++-- crates/skate-game/src/app.rs | 1 + crates/skate-game/src/fps_overlay.rs | 65 +++++++++++++++++ crates/skate-game/src/graph_host/mod.rs | 1 + crates/skate-game/src/graph_host/motion.rs | 6 ++ .../src/graph_host/motion_conditions.rs | 3 + .../src/graph_host/motion_execute.rs | 14 ++++ .../src/graph_host/motion_manual.rs | 15 ++++ .../skate-game/src/graph_host/motion_nodes.rs | 2 + crates/skate-game/src/main.rs | 1 + .../skate-game/src/physics/animation_phase.rs | 1 + crates/skate-game/src/physics/controls.rs | 1 + .../src/physics/ground_runtime/entry.rs | 22 +++--- .../ground_runtime/entry_restoration_tests.rs | 64 +++++++++++++++++ .../src/physics/player_input/mod.rs | 6 +- .../src/physics/player_state/publication.rs | 1 + 24 files changed, 455 insertions(+), 45 deletions(-) create mode 100644 crates/skate-core/src/animation/manual.rs create mode 100644 crates/skate-core/src/input/manual_intentions.rs create mode 100644 crates/skate-core/src/physics/tests/board_dynamic_normal_history.rs create mode 100644 crates/skate-core/tests/manual_input_animation.rs create mode 100644 crates/skate-game/src/fps_overlay.rs create mode 100644 crates/skate-game/src/graph_host/motion_manual.rs create mode 100644 crates/skate-game/src/physics/ground_runtime/entry_restoration_tests.rs diff --git a/crates/skate-core/src/animation/manual.rs b/crates/skate-core/src/animation/manual.rs new file mode 100644 index 00000000..6e5de710 --- /dev/null +++ b/crates/skate-core/src/animation/manual.rs @@ -0,0 +1,42 @@ +//! SetManualAngle Begin82BA8C48/Update82BA8C60. Animation selection only; +//! physical balance is separately emitted by the authored AttachIntent node. +use crate::point_graph::PointGraph; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Settings { + /// anim_motion/manual layout1216..1276, native eight-point evaluator. + pub balance: PointGraph<8>, + /// Layout1800/1804. Limits are per graph update, not per second. + pub velocity_limit: f32, + pub acceleration_limit: f32, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct State { + pub angle: f32, + pub velocity: f32, +} + +impl State { + pub fn begin(&mut self) { + *self = Self::default(); + } + + pub fn update(&mut self, manual: Option, settings: &Settings) -> f32 { + // The native lookup begins with zero; an absent intent leaves it zero. + let manual = manual.unwrap_or(0.0); + let target = settings.balance.evaluate(manual.abs()); + let target = if manual >= -0.0 { target } else { -target }; + let desired_velocity = clamp(target - self.angle, settings.velocity_limit); + let acceleration = clamp(desired_velocity - self.velocity, settings.acceleration_limit); + self.velocity += acceleration; + self.angle += self.velocity; + self.angle + } +} + +fn clamp(value: f32, limit: f32) -> f32 { + // Preserve the source fsubs/fsel selection, including signed zero. + let lower = if -limit - value >= -0.0 { -limit } else { value }; + if limit - lower >= -0.0 { lower } else { limit } +} diff --git a/crates/skate-core/src/animation/mod.rs b/crates/skate-core/src/animation/mod.rs index 8a5b74f8..04d87d62 100644 --- a/crates/skate-core/src/animation/mod.rs +++ b/crates/skate-core/src/animation/mod.rs @@ -1,5 +1,6 @@ //! Animation timing and event semantics recovered from Skate 3 TU3. pub mod bump; +pub mod manual; pub mod clip_clock; pub mod commands; pub mod output; diff --git a/crates/skate-core/src/input/manual_intentions.rs b/crates/skate-core/src/input/manual_intentions.rs new file mode 100644 index 00000000..8014fd56 --- /dev/null +++ b/crates/skate-core/src/input/manual_intentions.rs @@ -0,0 +1,30 @@ +//! TU3 Fill825999F0 -> manual producer8259BA28, after Derived input. +use super::{controller::{DerivedControllerInput, magnitude}, riding_intentions::RidingIntent}; + +/// Continuous Manual/ManualBrake belong to the input listener. The authored +/// graphs own engagement time, allowed states, balance and animation selection. +pub fn produce(controller: &DerivedControllerInput, actor_flags: u32) -> Vec { + let mut intents = Vec::with_capacity(2); + if actor_flags & (1 << 9) != 0 { + return intents; + } + let words = controller.words(); + let [x, y] = [words[9], words[10]].map(f32::from_bits); + // Fill82599BC8..9C40: x*x + rounded(y*y), then two rsqrt refinements. + let length = magnitude(x.mul_add(x, y * y)); + if y != 0.0 { + let value = if y > 0.0 { length } else { -length }; + intents.push(RidingIntent { name: "Manual", value }); + } + // Original constants820997B8/822F9274; strictly greater, independent + // of the Manual presence gate. A horizontal stick can emit ManualBrake. + let threshold = f32::from_bits(0x3f66_6666); + if length > threshold { + let brake = (length - threshold) * f32::from_bits(0x411f_fffe); + intents.push(RidingIntent { + name: "ManualBrake", + value: if y > 0.0 { brake } else { -brake }, + }); + } + intents +} diff --git a/crates/skate-core/src/input/mod.rs b/crates/skate-core/src/input/mod.rs index d74a089a..68aac993 100644 --- a/crates/skate-core/src/input/mod.rs +++ b/crates/skate-core/src/input/mod.rs @@ -4,6 +4,7 @@ pub mod gesture; pub mod anticipation_intentions; pub mod animation_packet; pub mod controller; +pub mod manual_intentions; pub mod gameplay_map; pub mod tick; pub mod graph_intents; diff --git a/crates/skate-core/src/physics/board_dynamic_normal.rs b/crates/skate-core/src/physics/board_dynamic_normal.rs index a7ced8b9..f17b02f6 100644 --- a/crates/skate-core/src/physics/board_dynamic_normal.rs +++ b/crates/skate-core/src/physics/board_dynamic_normal.rs @@ -1,10 +1,8 @@ //! Dynamic wheel-response normal, original TU3 82C02388, with acceleration //! history from82C082AC..8348. This is Ground64, distinct from wheel normals. use super::{ - board::{BODY_COUNT, BodyId}, board_ground::BoardGroundState, board_motion_output::{add, dot, inverse_length_squared, scale, subtract}, - board_runtime::BoardRuntime, }; use crate::{math::Vector3, point_graph::PointGraph}; const UP: Vector3 = Vector3::new(0., 1., 0.); @@ -26,8 +24,6 @@ pub struct BoardDynamicNormal { /// Skateboard144 and160, retained independently by the source. pub acceleration: Vector3, pub last_contact_normal: Vector3, - previous_velocities: [Vector3; BODY_COUNT], - pub part_accelerations: [Vector3; BODY_COUNT], } impl BoardDynamicNormal { ///82C00ED0 initializes112/128 to UP and144/160 to zero. The full board @@ -38,32 +34,20 @@ impl BoardDynamicNormal { delta: UP, acceleration: ZERO, last_contact_normal: ZERO, - previous_velocities: [ZERO; BODY_COUNT], - part_accelerations: [ZERO; BODY_COUNT], } } - /// Board reset clears acceleration history but preserves wrapper112/128. - pub fn reset_body_history(&mut self) { - self.previous_velocities.fill(ZERO); - self.part_accelerations.fill(ZERO); - } pub fn update( &mut self, - board: &BoardRuntime, contacts: &BoardGroundState, gravity: Vector3, - dt: f32, previous_ground_speed: f32, settings: &DynamicNormalSettings, ) { - let frequency = 1.0 / dt; - for part in BodyId::ORDER { - let i = part.index(); - let velocity = board.bodies()[i].rates.linear_velocity; - self.part_accelerations[i] = - scale(subtract(velocity, self.previous_velocities[i]), frequency); - self.previous_velocities[i] = velocity; - } + //82C02344 runs Body::UpdatePostPhysics before82C0234C calls this + //filter.82C02444/60/7C read the existing Body432/448/464 array. + //Paired S2:82B37E5C/64 calls Body::UpdatePostPhysics then + //UpdateUpVector82B26158, which reads CollisionInfo acceleration. + //No live body, timestep, duplicate history or mirror belongs here. if contacts.wheel_contact_count == 0 { self.acceleration = ZERO; return; @@ -73,7 +57,7 @@ impl BoardDynamicNormal { //fourth-wheel accumulation in this function; retain that distinction. for i in 0..3 { if contacts.parts[i].in_contact { - sum = subtract(add(sum, self.part_accelerations[i]), gravity); + sum = subtract(add(sum, contacts.accelerations[i]), gravity); } } let target = normalize(sum); @@ -99,6 +83,10 @@ impl BoardDynamicNormal { self.last_contact_normal = contacts.overall_normal; } } + +#[cfg(test)] +#[path = "tests/board_dynamic_normal_history.rs"] +mod tests; fn normalize(v: Vector3) -> Vector3 { normalize_length(v).0 } diff --git a/crates/skate-core/src/physics/tests/board_dynamic_normal_history.rs b/crates/skate-core/src/physics/tests/board_dynamic_normal_history.rs new file mode 100644 index 00000000..18e4c939 --- /dev/null +++ b/crates/skate-core/src/physics/tests/board_dynamic_normal_history.rs @@ -0,0 +1,64 @@ +//! Regressions for the paired source boundary, not original-game fidelity proof. +use super::*; +use crate::physics::board::BODY_COUNT; + +fn settings() -> DynamicNormalSettings { + DynamicNormalSettings { + speed_damping: 0.0, + up_vector_damping: 1.0, + maximum_delta: 10.0, + speed_scale: 1.0, + maximum_delta_vs_speed: PointGraph { + x: [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0], + y: [1.0; 8], + }, + } +} + +#[test] +fn retained_normal_consumes_sampled_acceleration() { + let mut contacts = BoardGroundState::default(); + contacts.wheel_contact_count = 1; + contacts.parts[0].in_contact = true; + contacts.sample_accelerations([Vector3::new(1.0, 0.0, 0.0); BODY_COUNT], 1.0); + let mut filter = BoardDynamicNormal::new(); + filter.update(&contacts, ZERO, 0.0, &settings()); + assert!(filter.normal.x > 0.9999); + assert!(filter.normal.y.abs() < 0.0001 && filter.normal.z.abs() < 0.0001); + let once = filter.normal; + // There is no live-body/timestep input or second history to re-sample. + filter.update(&contacts, ZERO, 0.0, &settings()); + assert!((filter.normal.x - once.x).abs() < 0.0001); +} + +#[test] +fn body_history_reset_does_not_reset_retained_normal_or_delta() { + let mut contacts = BoardGroundState::default(); + contacts.sample_accelerations([Vector3::new(5.0, 0.0, 0.0); BODY_COUNT], 1.0); + let mut filter = BoardDynamicNormal::new(); + filter.normal = Vector3::new(0.6, 0.8, 0.0); + filter.delta = Vector3::new(0.1, 0.2, 0.3); + filter.update(&contacts, ZERO, 0.0, &settings()); + let retained = (filter.normal, filter.delta); + contacts = BoardGroundState::default(); + filter.update(&contacts, ZERO, 0.0, &settings()); + assert_eq!((filter.normal, filter.delta), retained); + contacts.wheel_contact_count = 1; + contacts.parts[0].in_contact = true; + contacts.sample_accelerations([Vector3::new(2.0, 0.0, 0.0); BODY_COUNT], 1.0); + filter.update(&contacts, ZERO, 0.0, &settings()); + // Reusing the old velocity5 would produce acceleration-3 instead of+2. + assert!(filter.normal.x > 0.9999); + assert!(filter.normal.y.abs() < 0.0001 && filter.normal.z.abs() < 0.0001); +} + +#[test] +fn fourth_wheel_contact_does_not_enter_native_three_wheel_sum() { + let mut contacts = BoardGroundState::default(); + contacts.wheel_contact_count = 1; + contacts.parts[3].in_contact = true; + contacts.accelerations[3] = Vector3::new(9.0, 0.0, 0.0); + let mut filter = BoardDynamicNormal::new(); + filter.update(&contacts, ZERO, 0.0, &settings()); + assert!(filter.normal.x.abs() < 0.0001); +} 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 be805241..84f9bca4 100644 --- a/crates/skate-core/src/player/input_phase/pose_output.rs +++ b/crates/skate-core/src/player/input_phase/pose_output.rs @@ -27,13 +27,15 @@ pub struct AnimationOutputFields { pub profile_148: u32, /// PhysOutAnimation+166, tested by OkToDoTrickOnStairs82BA6930. pub tricks_blocked_on_stairs_166: u8, + /// Ground manual opposition, read by PhysicsWantsManualExit82BA7930. + pub manual_opposition_168: u8, } impl Default for AnimationOutputFields { fn default() -> Self { // Animation82DE3F38 stores FLT_MAX at144 and zero at148. // Reset82DE3F38 clears byte166. Ordinary Ground Fill82D3A388 // and common ProcessOutput82DB6EC0 do not overwrite this byte. - Self { collision_time_144: f32::MAX, profile_148: 0, tricks_blocked_on_stairs_166: 0 } + Self { collision_time_144: f32::MAX, profile_148: 0, tricks_blocked_on_stairs_166: 0, manual_opposition_168: 0 } } } diff --git a/crates/skate-core/tests/manual_input_animation.rs b/crates/skate-core/tests/manual_input_animation.rs new file mode 100644 index 00000000..974b18dd --- /dev/null +++ b/crates/skate-core/tests/manual_input_animation.rs @@ -0,0 +1,69 @@ +use skate_core::{ + animation::manual::{Settings, State}, + input::{controller::DerivedControllerInput, manual_intentions}, + point_graph::PointGraph, +}; + +fn intents(x: f32, y: f32, flags: u32) -> Vec { + let mut words = [0; 26]; + words[9] = x.to_bits(); + words[10] = y.to_bits(); + manual_intentions::produce(&DerivedControllerInput::from_words(words), flags) +} + +#[test] +fn manual_uses_radial_magnitude_y_sign_and_its_own_suppression_bit() { + for sign in [-1.0, 1.0] { + let result = intents(0.3, sign * 0.4, 0); + assert_eq!(result.len(), 1); + assert_eq!(result[0].name, "Manual"); + assert!((result[0].value - sign * 0.5).abs() < 1e-6); + assert!(intents(0.3, sign * 0.4, 1 << 9).is_empty()); + assert_eq!(intents(0.3, sign * 0.4, 1 << 8), result); + } + assert!(intents(0.0, -0.0, 0).is_empty()); + assert!(intents(0.5, 0.0, 0).is_empty()); +} + +#[test] +fn brake_has_an_independent_strict_threshold_and_retains_horizontal_sign_rule() { + assert_eq!(intents(0.0, 0.9, 0).len(), 1); + for (x, y, sign) in [(0.0, 1.0, 1.0), (0.0, -1.0, -1.0), (1.0, 0.0, -1.0)] { + let result = intents(x, y, 0); + let brake = result.iter().find(|v| v.name == "ManualBrake").unwrap(); + assert!((brake.value - sign).abs() < 1e-6); + assert_eq!(result.iter().any(|v| v.name == "Manual"), y != 0.0); + } +} + +fn settings() -> Settings { + // Independent fixture: a piecewise curve exercises both the dead band + // and the signed response without requiring any private asset. + Settings { + balance: PointGraph { + x: [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.8, 1.0], + y: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], + }, + velocity_limit: 0.04, + acceleration_limit: 0.02, + } +} + +#[test] +fn manual_animation_limits_velocity_and_reversal_acceleration_per_update() { + let settings = settings(); + let mut state = State::default(); + assert_eq!(state.update(Some(0.6), &settings), 0.0); + assert_eq!(state.update(Some(1.0), &settings), 0.02); + assert_eq!(state.update(Some(1.0), &settings), 0.06); + assert_eq!(state.velocity, 0.04); + // Reversal initially continues forward while velocity decelerates. + assert_eq!(state.update(Some(-1.0), &settings), 0.08); + assert_eq!(state.velocity, 0.02); + assert_eq!(state.update(Some(-1.0), &settings), 0.08); + assert_eq!(state.velocity, 0.0); + assert_eq!(state.update(Some(-1.0), &settings), 0.06); + state.begin(); + assert_eq!(state, State::default()); + assert_eq!(state.update(None, &settings), 0.0); +} diff --git a/crates/skate-data/src/collections.rs b/crates/skate-data/src/collections.rs index 2c3708ee..ca85d9ee 100644 --- a/crates/skate-data/src/collections.rs +++ b/crates/skate-data/src/collections.rs @@ -43,7 +43,7 @@ impl Collections { } let mut identities = std::collections::BTreeSet::new(); for item in &data.collections { - if !identities.insert((&item.class_name, &item.key)) { + if !identities.insert((crate::attrib_hash::numeric_name(&item.class_name), crate::attrib_hash::numeric_name(&item.key))) { return Err(format!( "Duplicate collection {}/{}", item.class_name, item.key @@ -54,18 +54,25 @@ impl Collections { } pub fn field(&self, class: &str, key: &str, name: &str) -> Result<&Field, String> { + // Converted vaults can retain numeric identities when debug names were + // unavailable. Both spellings identify the same native AttribSys entry. + let class_id = crate::attrib_hash::numeric_name(class); + let field_id = crate::attrib_hash::numeric_name(name); let mut current = key; - let class_hash = crate::attrib_hash::numeric_name(class); - let field_hash = crate::attrib_hash::numeric_name(name); for _ in 0..=self.collections.len() { - let key_hash = crate::attrib_hash::numeric_name(current); + let current_id = crate::attrib_hash::numeric_name(current); let item = self .collections .iter() - .find(|c| (c.class_name == class || c.class_name == class_hash) - && (c.key == current || c.key == key_hash)) + .find(|c| { + (c.class_name == class || crate::attrib_hash::numeric_name(&c.class_name) == class_id) + && (c.key == current || crate::attrib_hash::numeric_name(&c.key) == current_id) + }) .ok_or_else(|| format!("Missing stock collection {class}/{current}"))?; - if let Some(field) = item.fields.get(name).or_else(|| item.fields.get(&field_hash)) { + if let Some(field) = item.fields.get(name).or_else(|| { + item.fields.iter().find(|(key, _)| crate::attrib_hash::numeric_name(key) == field_id) + .map(|(_, field)| field) + }) { return Ok(field); } if item.parent.is_empty() { @@ -139,3 +146,36 @@ fn decode_words(text: &str) -> Result<[u32; N], String> { } Ok(words) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn numeric_export_names_resolve_readable_manual_settings() { + // Identities and payload from the prepared vault; no asset IO or gameplay. + let data: Collections = serde_json::from_value(serde_json::json!({ + "version": 1, "collections": [{ + "class": "anim_motion", "key": crate::attrib_hash::numeric_name("manual"), + "parent": "", "source": "fixture", "sha256": "", + "fields": { "Hash_A353CE1670D3AA40": { "type": "EA::Reflection::Float", "data": "3D23D70A" } } + }] + })).unwrap(); + assert_eq!(data.float("anim_motion", "manual", "manual_clamp_vel").unwrap().to_bits(), 0x3d23d70a); + assert!(data.field("anim_motion", "manual", "missing").is_err()); + } + + #[test] + fn readable_names_resolve_numeric_queries_through_inheritance() { + let data: Collections = serde_json::from_value(serde_json::json!({ + "version": 1, "collections": [ + {"class":"example", "key":"child", "parent":crate::attrib_hash::numeric_name("base"), + "source":"fixture", "sha256":"", "fields":{}}, + {"class":"example", "key":"base", "parent":"", "source":"fixture", "sha256":"", + "fields":{"value":{"type":"EA::Reflection::Float", "data":"3F800000"}}} + ] + })).unwrap(); + assert_eq!(data.float(&crate::attrib_hash::numeric_name("example"), + &crate::attrib_hash::numeric_name("child"), &crate::attrib_hash::numeric_name("value")).unwrap(), 1.0); + } +} diff --git a/crates/skate-game/src/app.rs b/crates/skate-game/src/app.rs index 00bc203e..c2110232 100644 --- a/crates/skate-game/src/app.rs +++ b/crates/skate-game/src/app.rs @@ -91,6 +91,7 @@ pub(crate) fn build( ) .chain(), ) + .add_plugins(crate::fps_overlay::FpsOverlayPlugin) .add_plugins(( crate::retail_render::RetailRenderPlugin, input::InputPlugin, diff --git a/crates/skate-game/src/fps_overlay.rs b/crates/skate-game/src/fps_overlay.rs new file mode 100644 index 00000000..95eb0f4c --- /dev/null +++ b/crates/skate-game/src/fps_overlay.rs @@ -0,0 +1,65 @@ +//! Presentation-frame FPS, independent of fixed physics and HUD refresh cadence. +use bevy::prelude::*; + +pub(crate) struct FpsOverlayPlugin; + +#[derive(Component)] +struct FpsText; + +#[derive(Default)] +struct FrameSample { + frames: u32, + seconds: f64, +} + +impl Plugin for FpsOverlayPlugin { + fn build(&self, app: &mut App) { + app.add_systems(Startup, spawn) + .add_systems(Update, update); + } +} + +fn spawn(mut commands: Commands) { + // Use the existing gameplay camera; no independent overlay render loop. + commands.spawn(( + Name::new("FPS overlay"), + Node { + position_type: PositionType::Absolute, + top: px(12), + right: px(12), + padding: UiRect::axes(px(10), px(6)), + ..default() + }, + BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.7)), + GlobalZIndex(100), + )).with_children(|parent| { + parent.spawn(( + FpsText, + Text::new("FPS: --"), + TextFont { font_size: 20.0, ..default() }, + TextColor(Color::WHITE), + )); + }); +} + +fn update( + time: Res>, + mut sample: Local, + mut labels: Query<&mut Text, With>, +) { + let seconds = time.delta_secs_f64(); + if seconds <= 0.0 { + return; + } + // One sample for EVERY presentation frame, including long/stalled frames. + // Real time is unaffected by slow motion or the fixed simulation timestep. + sample.frames += 1; + sample.seconds += seconds; + if sample.seconds >= 0.5 { + let fps = f64::from(sample.frames) / sample.seconds; + for mut text in &mut labels { + text.0 = format!("FPS: {fps:.0}"); + } + *sample = FrameSample::default(); + } +} diff --git a/crates/skate-game/src/graph_host/mod.rs b/crates/skate-game/src/graph_host/mod.rs index 7d3531e0..d4b5ea90 100644 --- a/crates/skate-game/src/graph_host/mod.rs +++ b/crates/skate-game/src/graph_host/mod.rs @@ -31,6 +31,7 @@ pub(crate) mod motion_push_off; #[path = "motion_offboard/reset.rs"] pub(crate) mod motion_reset; mod motion_riding; +mod motion_manual; pub(crate) mod motion_riding_conditions; pub(crate) mod motion_runout; pub(crate) mod motion_shove; diff --git a/crates/skate-game/src/graph_host/motion.rs b/crates/skate-game/src/graph_host/motion.rs index f908e730..b99a238c 100644 --- a/crates/skate-game/src/graph_host/motion.rs +++ b/crates/skate-game/src/graph_host/motion.rs @@ -54,6 +54,7 @@ enum Instance { Trick(i32), JumpInto(bool), Stateless, + Manual(skate_core::animation::manual::State), IntentFilter(skate_core::animation::intent_filter::State), Landing(super::motion_landing::State), Wipeout(super::motion_wipeout::State), @@ -95,6 +96,7 @@ impl Instance { smoothed: 0.0, mode: 2, }), + MotionOperation::SetManualAngle => Self::Manual(Default::default()), MotionOperation::Crouching(_) => Self::Crouching(None), MotionOperation::SettingBodyTilt(_) => { Self::BodyTilt(skate_core::animation::body_tilt::State::default()) @@ -199,6 +201,8 @@ pub struct MotionHost { pushing: PushingSettings, turning: set_turning::Settings, crouching: skate_core::animation::crouching::Settings, + manual: skate_core::animation::manual::Settings, + pub manual_exit: Option, body_tilt: skate_core::animation::body_tilt::Settings, pumping: skate_core::animation::pumping_channel::Settings, sliding: super::motion_sliding::Settings, @@ -324,6 +328,8 @@ impl MotionHost { pushing, turning: super::turning_settings::load(data)?, crouching: super::crouching_settings::load(data)?, + manual: super::motion_manual::settings(data)?, + manual_exit: None, body_tilt: super::motion_riding::body_tilt_settings(data)?, pumping: super::pumping_settings::load(data)?, sliding: super::motion_sliding::Settings::load(data)?, diff --git a/crates/skate-game/src/graph_host/motion_conditions.rs b/crates/skate-game/src/graph_host/motion_conditions.rs index c897c90d..5b2b180b 100644 --- a/crates/skate-game/src/graph_host/motion_conditions.rs +++ b/crates/skate-game/src/graph_host/motion_conditions.rs @@ -24,6 +24,7 @@ pub enum MotionCondition { StandingOnMovingObject, LocoState(super::motion_cadence::LocoState), ManualOutTimerIsActive, + PhysicsWantsManualExit, Gesture(crate::input::gesture_catalog::Group), Landing(super::motion_landing::Condition), Wipeout(super::motion_wipeout::Condition), @@ -124,6 +125,7 @@ impl MotionCondition { "IsHoldingSkateboard" => Self::HoldingSkateboard, //82BA5E10 "IsBipedGroundThin" => Self::BipedGroundThin, //82BA8110: OffBoard+330 "LocoState" => Self::LocoState(super::motion_cadence::LocoState::parse(a)?), + "PhysicsWantsManualExit" => Self::PhysicsWantsManualExit, "ManualOutTimerIsActive" => Self::ManualOutTimerIsActive, "HasGestureIntent" => Self::Gesture(crate::input::gesture_catalog::Group::parse( a.text("group").unwrap_or(""), @@ -273,6 +275,7 @@ impl MotionCondition { ) .map_err(str::to_owned)?, //82BA78B0 -> specific getter: strictly positive retained timer. + Self::PhysicsWantsManualExit => host.manual_exit.ok_or("PhysicsWantsManualExit requires completed Animation168")?, Self::ManualOutTimerIsActive => host.riding.manual_out_timer > 0.0, Self::Gameplay(condition) => condition.evaluate(host)?, Self::Riding(condition) => condition.evaluate(host)?, diff --git a/crates/skate-game/src/graph_host/motion_execute.rs b/crates/skate-game/src/graph_host/motion_execute.rs index eec4d9fc..4b49d6d8 100644 --- a/crates/skate-game/src/graph_host/motion_execute.rs +++ b/crates/skate-game/src/graph_host/motion_execute.rs @@ -167,6 +167,20 @@ impl MotionHost { .get_mut(behavior) .ok_or("Unallocated MotionGraph behavior")?; match (operation, instance) { + (MotionOperation::SetManualAngle, Instance::Manual(state)) => { + match phase { + 0 => state.begin(), + 1 => { + let value = state.update(self.animation.motion_intent("Manual"), &self.manual); + self.animation.set_attribute(SettableAttribute { + // Factory82BC9678 binds the stock name without an authored override. + name: encode(b"manual_angle"), value, normalized: false, sequence_id: -1, + }); + } + _ => {} // Native End is the empty leaf. + } + } + (MotionOperation::AddRunoutAttribs, Instance::Runout(state)) => { if phase == 0 { *state = Some(super::super::motion_runout::capture( diff --git a/crates/skate-game/src/graph_host/motion_manual.rs b/crates/skate-game/src/graph_host/motion_manual.rs new file mode 100644 index 00000000..d2416809 --- /dev/null +++ b/crates/skate-game/src/graph_host/motion_manual.rs @@ -0,0 +1,15 @@ +//! Stock manual animation data. Physical controller state remains in Ground. +use skate_core::animation::manual::Settings; +use skate_data::collections::Collections; + +pub fn settings(data: &Collections) -> Result { + let words = data.words::<16>("anim_motion", "manual", "manual_balance")?; + Ok(Settings { + balance: skate_core::point_graph::PointGraph { + x: std::array::from_fn(|i| f32::from_bits(words[i])), + y: std::array::from_fn(|i| f32::from_bits(words[8 + i])), + }, + velocity_limit: data.float("anim_motion", "manual", "manual_clamp_vel")?, + acceleration_limit: data.float("anim_motion", "manual", "manual_clamp_acc")?, + }) +} diff --git a/crates/skate-game/src/graph_host/motion_nodes.rs b/crates/skate-game/src/graph_host/motion_nodes.rs index 5543ab89..0745ed85 100644 --- a/crates/skate-game/src/graph_host/motion_nodes.rs +++ b/crates/skate-game/src/graph_host/motion_nodes.rs @@ -36,6 +36,7 @@ pub enum MotionOperation { ApplyingBodyTilt, SettingBodyTilt(AttributeName), Crouching(AttributeName), + SetManualAngle, Pumping, DisallowPumping, FakieHeadChannel, @@ -134,6 +135,7 @@ impl OperationFactory for MotionFactory { super::motion_intent_filter::Operation::parse(a), )), "JumpInto" => Some(MotionOperation::JumpInto(key(a, "attribute", ""))), + "SetManualAngle" => Some(MotionOperation::SetManualAngle), "PlayAnimation" => Some(MotionOperation::Play(play(a))), "PrintText2D" => Some(MotionOperation::PrintText2D), "AddRunoutAttribs" => Some(MotionOperation::AddRunoutAttribs), diff --git a/crates/skate-game/src/main.rs b/crates/skate-game/src/main.rs index a73f6e3c..f8c958ef 100644 --- a/crates/skate-game/src/main.rs +++ b/crates/skate-game/src/main.rs @@ -23,6 +23,7 @@ mod difficulty; mod graph_host; mod graph_runtime; mod input; +mod fps_overlay; mod session_marker; mod physics; mod skater_animation; diff --git a/crates/skate-game/src/physics/animation_phase.rs b/crates/skate-game/src/physics/animation_phase.rs index 35cbb2da..6cace9ab 100644 --- a/crates/skate-game/src/physics/animation_phase.rs +++ b/crates/skate-game/src/physics/animation_phase.rs @@ -30,6 +30,7 @@ pub(crate) fn advance( skater.animation.motion.score_packet = Default::default(); let p = &skater.player_input.processed; let physical = &skater.player_input.physical; + skater.animation.motion.manual_exit = Some(physical.animation.manual_opposition_168 != 0); let deck = physics.board.part_transforms()[BodyId::Deck.index()]; let (fakie, mirrored) = skater.animation.stance(); let feedback = skater.physical_feedback; diff --git a/crates/skate-game/src/physics/controls.rs b/crates/skate-game/src/physics/controls.rs index be270d23..4a1e908a 100644 --- a/crates/skate-game/src/physics/controls.rs +++ b/crates/skate-game/src/physics/controls.rs @@ -98,6 +98,7 @@ impl PlayerControls { ); self.intents = riding_intentions::produce(&self.controller, self.actor_flags, self.preferences); + self.intents.extend(skate_core::input::manual_intentions::produce(&self.controller, self.actor_flags)); self.intents.extend(wipeout_intentions::produce( &self.controller, self.actor_flags, physical_capabilities, )); diff --git a/crates/skate-game/src/physics/ground_runtime/entry.rs b/crates/skate-game/src/physics/ground_runtime/entry.rs index 7ae28e9c..df76462d 100644 --- a/crates/skate-game/src/physics/ground_runtime/entry.rs +++ b/crates/skate-game/src/physics/ground_runtime/entry.rs @@ -61,17 +61,7 @@ impl GroundState { toolkit: &BoardToolkit, targets: GroundEntryTargets<'_>, ) -> Result<(), String> { - //SetStandard82C03AF8: standard dynamic part mode, deck damping and - //source-owned wrapper flag. Authored board volumes stay enabled. - *targets.board_flags_8384 &= 0x7f; - board.bodies_mut()[6].inertia.angular_drag = self.entry_settings.deck_angular_drag; - //82C090C0 restores assembly+24 and every part+92 to collision group4. - //Remount releases the carried board into group7; retaining that group - //allows the riding skeleton (group5) to collide with its own board. - board.set_collision_group(4); - for body in board.bodies_mut() { - body.state_flags = 4; - } + restore_standard_board_fields(board, targets.board_flags_8384, self.entry_settings.deck_angular_drag); targets.foot_ik.enable_feet(true); *targets.skeleton_elapsed_16505 = false; (targets.set_skeleton_collision_state)(6)?; @@ -155,3 +145,13 @@ impl ManualGroundProjection for Projection { Ok(dot_product(n, v)) } } + +// SetStandard restores collision groups; activation flags belong to body state. +fn restore_standard_board_fields(board: &mut BoardRuntime, flags: &mut u8, drag: f32) { + *flags &= 0x7f; + board.bodies_mut()[6].inertia.angular_drag = drag; + board.set_collision_group(4); +} +#[cfg(test)] +#[path = "entry_restoration_tests.rs"] +mod restoration_tests; diff --git a/crates/skate-game/src/physics/ground_runtime/entry_restoration_tests.rs b/crates/skate-game/src/physics/ground_runtime/entry_restoration_tests.rs new file mode 100644 index 00000000..648234ab --- /dev/null +++ b/crates/skate-game/src/physics/ground_runtime/entry_restoration_tests.rs @@ -0,0 +1,64 @@ +//! Independent field-ownership regression, not a stock gameplay/parity fixture. +use super::restore_standard_board_fields; +use skate_core::{ + math::Vector3, + physics::{ + board_runtime::{BoardMotion, BoardRuntime}, + drive_frames::RetailAffineTransform, + rigid_body::{ + RetailBodyMassProperties, RetailInertiaDynamics, RetailLocalMassFrame, + RetailSimulationStep, + }, + }, +}; + +#[test] +fn ground_reentry_restores_collision_group_without_reactivating_bodies() { + let mass = RetailBodyMassProperties { + local_mass_frame: RetailLocalMassFrame::IDENTITY, + dynamics: RetailInertiaDynamics { + inverse_tensor: Vector3::new(1.0, 2.0, 3.0), + inverse_mass: 1.0, + spherical: 0.0, + maximum_linear_velocity: 100.0, + maximum_angular_velocity: 100.0, + linear_drag: 0.25, + angular_drag: 0.5, + }, + }; + let mut board = BoardRuntime::new( + [mass; 7], + [RetailAffineTransform::IDENTITY; 7], + RetailAffineTransform::IDENTITY, + RetailSimulationStep::fixed_60_hz(30, 0.001, Vector3::ZERO), + BoardMotion::Active, + ); + board.set_collision_group(7); + for (i, body) in board.bodies_mut().iter_mut().enumerate() { + body.state_flags = [4, 12, 2, 10, 1, 9, 4][i]; + body.rates.cool_down = 10 + i as u32; + body.rates.linear_velocity = Vector3::new(i as f32, 3.0, 2.0); + body.rates.angular_velocity = Vector3::new(1.0, i as f32, 2.0); + } + let before = *board.bodies(); + let target_before = board.hook().body; + let mut flags = 0xd5; + let supplied_stock_drag = 2.75; + + restore_standard_board_fields(&mut board, &mut flags, supplied_stock_drag); + + assert_eq!(board.collision_group(), 4); + assert_eq!(flags, 0x55); + for (i, (body, old)) in board.bodies().iter().zip(before).enumerate() { + assert_eq!(body.state_flags, old.state_flags); + assert_eq!(body.rates, old.rates); + let mut expected_inertia = old.inertia; + if i == 6 { + expected_inertia.angular_drag = supplied_stock_drag; + } + assert_eq!(body.inertia, expected_inertia); + } + assert_eq!(board.hook().body.state_flags, target_before.state_flags); + assert_eq!(board.hook().body.rates, target_before.rates); + assert_eq!(board.hook().body.inertia, target_before.inertia); +} diff --git a/crates/skate-game/src/physics/player_input/mod.rs b/crates/skate-game/src/physics/player_input/mod.rs index 055cc3a1..48c03051 100644 --- a/crates/skate-game/src/physics/player_input/mod.rs +++ b/crates/skate-game/src/physics/player_input/mod.rs @@ -76,16 +76,14 @@ impl PlayerInputRuntime { ///Run after the current board/contact solve, before ProcessOutput publishes. pub fn update_dynamic_normal( &mut self, - board: &BoardRuntime, + _board: &BoardRuntime, riding: &RidingOutputs, gravity: Vector3, - dt: f32, + _dt: f32, ) { self.dynamic_normal.update( - board, &riding.ground, gravity, - dt, self.processed.scalar_2656, &self.normal_settings, ); diff --git a/crates/skate-game/src/physics/player_state/publication.rs b/crates/skate-game/src/physics/player_state/publication.rs index 9a0f8a36..23ef068a 100644 --- a/crates/skate-game/src/physics/player_state/publication.rs +++ b/crates/skate-game/src/physics/player_state/publication.rs @@ -146,6 +146,7 @@ pub(super) fn publish(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> physical.ground.flag_317 = u8::from(output.ground_32.anti_flip_nudge_present); physical.ground.flag_318 = u8::from(output.ground_32.is_pinning); physical.off_board.flag_304 = u8::from(output.is_grabbing_object_72_304); + physical.animation.manual_opposition_168 = u8::from(output.manual_opposition_56_168); } else if state == PhysicalStateId::PhysicsAir { let air = skate_core::air::state::fill_physics_output(&skater.air_state); physical.air.landing_normal_144 = air.landing_normal.map(f32::to_bits);