diff --git a/crates/skate-core/src/physics/board_runtime.rs b/crates/skate-core/src/physics/board_runtime.rs index 92a3ee66..f63de2f3 100644 --- a/crates/skate-core/src/physics/board_runtime.rs +++ b/crates/skate-core/src/physics/board_runtime.rs @@ -97,9 +97,13 @@ impl BoardRuntime { } } - pub fn collision_group(&self) -> u32 { self.collision_group } + pub fn collision_group(&self) -> u32 { + self.collision_group + } ///All seven native board parts share the current assembly group. - pub fn set_collision_group(&mut self, group: u32) { self.collision_group = group } + pub fn set_collision_group(&mut self, group: u32) { + self.collision_group = group + } pub fn bodies(&self) -> &[BodySnapshot; BODY_COUNT] { &self.bodies @@ -142,9 +146,8 @@ impl BoardRuntime { body.rates.angular_velocity = Vector3::ZERO; body.rates.force_acceleration = gravity; body.rates.torque_acceleration = Vector3::ZERO; - body.rates.world_inverse_inertia = world_inverse_inertia( - body.rates.basis, body.inertia.inverse_tensor, - ); + body.rates.world_inverse_inertia = + world_inverse_inertia(body.rates.basis, body.inertia.inverse_tensor); } copy_pose(&hook, &mut self.hook.body.rates); self.forces.clear(); @@ -263,6 +266,30 @@ impl BoardRuntime { set_part_transform(&mut part, pose_words(requested)); copy_pose(&part, &mut self.hook.body.rates); } + ///82C0B2C8, used by board retrieval and hiding. Changes live part poses + ///without Reset's rate, contact-history, or force-queue clearing. + pub fn set_transform(&mut self, requested: RetailAffineTransform) { + let poses = self.part_transforms(); + let mut parts = core::array::from_fn(|i| PartPose { + transform: pose_words(poses[i]), + local_mass_frame: Some(mass_frame_words(self.mass_frames[i])), + body: Some(body_pose_words(self.bodies[i].rates)), + inertia: Some(inertia_words(self.bodies[i].inertia)), + }); + let mut hook = PartPose { + transform: pose_words(self.hook_transform()), + local_mass_frame: None, + body: Some(body_pose_words(self.hook.body.rates)), + inertia: None, + }; + set_board_transform(&mut parts, &mut hook, pose_words(requested)); + for (body, part) in self.bodies.iter_mut().zip(&parts) { + copy_pose(part, &mut body.rates); + body.rates.world_inverse_inertia = + world_inverse_inertia(body.rates.basis, body.inertia.inverse_tensor); + } + copy_pose(&hook, &mut self.hook.body.rates); + } } const ZERO_INERTIA: RetailInertiaDynamics = RetailInertiaDynamics { diff --git a/crates/skate-core/src/physics/deck_angular_correction.rs b/crates/skate-core/src/physics/deck_angular_correction.rs index 1428f6ff..f5a6b35d 100644 --- a/crates/skate-core/src/physics/deck_angular_correction.rs +++ b/crates/skate-core/src/physics/deck_angular_correction.rs @@ -10,6 +10,20 @@ use crate::math::{Basis3, Vector3}; const STEP: f32 = f32::from_bits(0x3c88_8889); const NORMAL_MINIMUM: f32 = f32::from_bits(0x3586_37bd); //82F826F8 ->830BD350. +///82C07328 subtracts the current angular displacement along the requested +///axis, without07000's directional clamps, then performs the same tensor and +///fixed-step conversion as075B8. The thrown-board controller calls this leaf. +pub fn apply_axis_displacement(body: &mut RetailBodyRates, requested: Vector3) { + let squared = dot(requested, requested); + let inverse = inverse_length_squared(squared, 2); + let length = if squared == 0.0 { 0.0 } else { squared * inverse }; + let direction = if length > NORMAL_MINIMUM { + scale(requested, inverse) + } else { Vector3::ZERO }; + let existing = scale(direction, dot(direction, scale(body.angular_velocity, STEP))); + apply_angular_displacement(body, subtract(requested, existing)); +} + /// 82C07000 first limits the requested angular displacement against the /// displacement already supplied by angular velocity along that same axis. pub fn apply_limited_displacement(body: &mut RetailBodyRates, requested: Vector3) { @@ -130,6 +144,21 @@ mod tests { } } #[test] + fn thrown_board_correction_brakes_axis_overshoot_without_touching_other_motion() { + let mut state = body(); + state.angular_velocity = Vector3::new(4., 3., 5.); + apply_axis_displacement(&mut state, Vector3::new(0., 0.02, 0.)); + assert!((state.torque_acceleration.y + 108.).abs() < 0.0001); + assert!(state.torque_acceleration.x.abs() < 0.0001); + assert!(state.torque_acceleration.z.abs() < 0.0001); + assert_eq!(state.angular_velocity, Vector3::new(4., 3., 5.)); + assert_eq!(state.cool_down, 0); + let mut reverse = body(); + reverse.angular_velocity = Vector3::new(0., -3., 0.); + apply_axis_displacement(&mut reverse, Vector3::new(0., 0.02, 0.)); + assert!((reverse.torque_acceleration.y - 252.).abs() < 0.0001); + } + #[test] fn correction_enters_accumulator_and_respects_existing_angular_motion() { let request = Vector3::new(0.02, 0.01, -0.03); let mut state = body(); diff --git a/crates/skate-core/src/physics/skateboard_controller.rs b/crates/skate-core/src/physics/skateboard_controller.rs index a76d3433..fbbd790c 100644 --- a/crates/skate-core/src/physics/skateboard_controller.rs +++ b/crates/skate-core/src/physics/skateboard_controller.rs @@ -33,7 +33,7 @@ impl SkateboardController { let previous = self.fields.state_448; self.fields.word_444 = 0; if previous != 0 { - actions.let_go_of_skateboard(); + actions.let_go_of_skateboard(&mut self.fields); self.fields.state_448 = 0; } self.fields.system_on_452 = false; diff --git a/crates/skate-core/src/physics/tests/board_runtime.rs b/crates/skate-core/src/physics/tests/board_runtime.rs index 1e269445..dde90a91 100644 --- a/crates/skate-core/src/physics/tests/board_runtime.rs +++ b/crates/skate-core/src/physics/tests/board_runtime.rs @@ -59,6 +59,43 @@ fn close(actual: Vector3, expected: Vector3) { ); } +#[test] +fn moving_board_transform_preserves_rates_and_pending_forces() { + let mut board = runtime(Vector3::new(0., -9.8, 0.), BoardMotion::Active); + for (i, body) in board.bodies_mut().iter_mut().enumerate() { + body.rates.linear_velocity = Vector3::new(i as f32, 2., 3.); + body.rates.angular_velocity = Vector3::new(1., i as f32, 2.); + body.rates.force_acceleration = Vector3::new(5., 6., 7.); + body.rates.torque_acceleration = Vector3::new(8., 9., 10.); + } + let before = *board.bodies(); + let parts = board.part_transforms(); + let delta = Vector3::new(4., 5., 6.); + let mut target = parts[BodyId::Deck.index()]; + target.translation = delta; + board.set_transform(target); + for (i, body) in board.bodies().iter().enumerate() { + close(body.rates.linear_velocity, before[i].rates.linear_velocity); + close( + body.rates.angular_velocity, + before[i].rates.angular_velocity, + ); + close( + body.rates.force_acceleration, + before[i].rates.force_acceleration, + ); + close( + body.rates.torque_acceleration, + before[i].rates.torque_acceleration, + ); + let p = parts[i].translation; + close( + board.part_transforms()[i].translation, + Vector3::new(p.x + delta.x, p.y + delta.y, p.z + delta.z), + ); + } +} + #[test] fn spawn_preserves_each_mass_frame_and_keeps_the_hook_separate() { let mut data = masses(); diff --git a/crates/skate-core/src/player/input_phase/types.rs b/crates/skate-core/src/player/input_phase/types.rs index 06e9bd99..1e3c6722 100644 --- a/crates/skate-core/src/player/input_phase/types.rs +++ b/crates/skate-core/src/player/input_phase/types.rs @@ -118,8 +118,14 @@ pub struct GrindOutputFields { #[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct OffBoardOutputFields { pub scalar_32: f32, + pub board_angle_36: f32, + pub board_angle_40: f32, pub flag_304: u8, pub flag_311: u8, + pub free_board_312: u8, + pub returning_board_313: u8, + pub hidden_board_321: u8, + pub board_request_324: u8, pub flag_314: u8, pub flag_315: u8, pub flag_316: u8, diff --git a/crates/skate-core/src/player/lifecycle.rs b/crates/skate-core/src/player/lifecycle.rs index 05033b17..3fb7adb7 100644 --- a/crates/skate-core/src/player/lifecycle.rs +++ b/crates/skate-core/src/player/lifecycle.rs @@ -79,8 +79,8 @@ pub struct StateChangeData { /// implementations are `HoldSkateboard` `0x82D75370` and /// `LetGoOfSkateboard` `0x82D75440`; this lifecycle never replaces them. pub trait SkateboardControllerActions { - fn hold_skateboard(&mut self); - fn let_go_of_skateboard(&mut self); + fn hold_skateboard(&mut self, _fields: &mut SkateboardControllerFields); + fn let_go_of_skateboard(&mut self, _fields: &mut SkateboardControllerFields); } /// Context supplied to an owned state's Enter or Exit implementation. @@ -196,11 +196,11 @@ fn prepare_skateboard_controller( controller.word_444 = 0; if processed_flags_2480 & (0x80 | 0x100) != 0 { if controller.state_448 != 2 { - actions.let_go_of_skateboard(); + actions.let_go_of_skateboard(controller); controller.state_448 = 2; } } else if controller.state_448 != 1 { - actions.hold_skateboard(); + actions.hold_skateboard(controller); controller.state_448 = 1; } controller.system_on_452 = true; @@ -218,7 +218,7 @@ fn stop_skateboard_controller( let previous_state = controller.state_448; controller.word_444 = 0; if previous_state != 0 { - actions.let_go_of_skateboard(); + actions.let_go_of_skateboard(controller); controller.state_448 = 0; } controller.system_on_452 = false; diff --git a/crates/skate-core/src/player/tests/lifecycle.rs b/crates/skate-core/src/player/tests/lifecycle.rs index 0c05560f..bcb5c936 100644 --- a/crates/skate-core/src/player/tests/lifecycle.rs +++ b/crates/skate-core/src/player/tests/lifecycle.rs @@ -37,11 +37,11 @@ impl PhysicalStateCalls for SharedRecorder { } impl SkateboardControllerActions for SharedRecorder { - fn hold_skateboard(&mut self) { + fn hold_skateboard(&mut self, _fields: &mut SkateboardControllerFields) { self.events.borrow_mut().push(Event::Hold); } - fn let_go_of_skateboard(&mut self) { + fn let_go_of_skateboard(&mut self, _fields: &mut SkateboardControllerFields) { self.events.borrow_mut().push(Event::LetGo); } } @@ -62,11 +62,11 @@ impl PhysicalStateCalls for Recorder { } impl SkateboardControllerActions for Recorder { - fn hold_skateboard(&mut self) { + fn hold_skateboard(&mut self, _fields: &mut SkateboardControllerFields) { self.events.push(Event::Hold); } - fn let_go_of_skateboard(&mut self) { + fn let_go_of_skateboard(&mut self, _fields: &mut SkateboardControllerFields) { self.events.push(Event::LetGo); } } @@ -97,6 +97,41 @@ fn seeded_data() -> StateChangeData { } } +#[test] +fn release_callback_retains_its_throw_countdown_through_state_publication() { + struct Throw; + impl SkateboardControllerActions for Throw { + fn hold_skateboard(&mut self, _: &mut SkateboardControllerFields) { + panic!("unexpected hold"); + } + fn let_go_of_skateboard(&mut self, fields: &mut SkateboardControllerFields) { + assert_eq!(fields.word_444, 0); + fields.word_444 = 8; + } + } + let mut data = seeded_data(); + data.skateboard_controller = SkateboardControllerFields { + word_444: 123, + state_448: 1, + system_on_452: true, + }; + let mut states = Recorder { + reported_type: PhysicalStateId::BipedGround, + events: vec![], + }; + PhysicalPlayerStateLifecycle::new(PhysicalStateId::BipedGround) + .set_physics_state( + PhysicalStateId::PhysicsGround as u32, + &mut data, + &mut states, + &mut Throw, + ) + .unwrap(); + assert_eq!(data.skateboard_controller.word_444, 8); + assert_eq!(data.skateboard_controller.state_448, 0); + assert!(!data.skateboard_controller.system_on_452); +} + fn recorder(reported_type: PhysicalStateId) -> Recorder { Recorder { reported_type, diff --git a/crates/skate-core/src/player/tests/logic.rs b/crates/skate-core/src/player/tests/logic.rs index 3929ed5d..61740dcd 100644 --- a/crates/skate-core/src/player/tests/logic.rs +++ b/crates/skate-core/src/player/tests/logic.rs @@ -43,8 +43,8 @@ impl PhysicalStateCalls for States { struct Controller; impl SkateboardControllerActions for Controller { - fn hold_skateboard(&mut self) {} - fn let_go_of_skateboard(&mut self) {} + fn hold_skateboard(&mut self, _fields: &mut SkateboardControllerFields) {} + fn let_go_of_skateboard(&mut self, _fields: &mut SkateboardControllerFields) {} } fn input() -> StateSelectionInput { diff --git a/crates/skate-game/src/physics.rs b/crates/skate-game/src/physics.rs index 577ac132..2033011f 100644 --- a/crates/skate-game/src/physics.rs +++ b/crates/skate-game/src/physics.rs @@ -276,6 +276,10 @@ impl GamePhysics { self.processed_flags_2468, self.settings.step.simulation.time_step, )?; + offboard::board_effects::classify_alignment( + &skater.offboard.board_policy, + &mut self.riding.ground, + ); let partial = skateboard_controller::partial_request( &skater.skateboard_controller, &self.riding.ground, diff --git a/crates/skate-game/src/physics/frame.rs b/crates/skate-game/src/physics/frame.rs index 5c5a8957..41557863 100644 --- a/crates/skate-game/src/physics/frame.rs +++ b/crates/skate-game/src/physics/frame.rs @@ -123,6 +123,7 @@ pub(super) fn advance( } state => return Err(format!("Selected {state:?} requires its physical update")), } + super::offboard::possession::update(physics, skater)?; // Source State82DB6120 submits the queue once, then advances state time. // BoardRuntime applies that same queue during its shared solve. skater.player_input.player.state_timer_1344 += skater.player_input.processed.timestep_2604; diff --git a/crates/skate-game/src/physics/offboard/board_effects.rs b/crates/skate-game/src/physics/offboard/board_effects.rs new file mode 100644 index 00000000..5260d889 --- /dev/null +++ b/crates/skate-game/src/physics/offboard/board_effects.rs @@ -0,0 +1,142 @@ +//! Board-controller effects applied to the existing board and hook bodies. +use skate_core::{ + math::{Basis3, Vector3}, + physics::{board::BodyId, board_runtime::BoardRuntime, drive_frames::RetailAffineTransform}, + player::offboard::board_possession::{ + Frame, Vector, + lifecycle::{Alignment, Effects}, + }, +}; + +pub(crate) struct Policy { + pub volumes_enabled: bool, + pub released: bool, + pub alignment: Option, +} +impl Default for Policy { + fn default() -> Self { + Self { + volumes_enabled: true, + released: false, + alignment: None, + } + } +} +pub(crate) struct BoardEffects<'a> { + pub board: &'a mut BoardRuntime, + pub animated: &'a mut u8, + pub policy: &'a mut Policy, + pub standard_deck_drag: f32, + pub timestep: f32, +} +fn xyz(v: Vector) -> Vector3 { + Vector3::new(v[0], v[1], v[2]) +} +fn transform(f: Frame) -> RetailAffineTransform { + RetailAffineTransform { + basis: Basis3 { + columns: std::array::from_fn(|i| [f[i][0], f[i][1], f[i][2]]), + }, + translation: xyz(f[3]), + } +} +impl Effects for BoardEffects<'_> { + fn enable_animation_soft(&mut self) { + self.board + .hook_mut() + .drive + .enable_animation_soft(self.animated); + } + fn enable_animation_angular_only(&mut self) { + self.board + .hook_mut() + .drive + .enable_angular_only(self.animated); + } + fn disable_animation(&mut self) { + self.board.hook_mut().drive.disable_animation(self.animated); + } + fn disable_linear_drive(&mut self) { + self.board.hook_mut().drive.disable_linear(); + } + fn standard_board(&mut self) { + self.policy.released = false; + self.board.set_collision_group(4); + self.board.bodies_mut()[BodyId::Deck.index()] + .inertia + .angular_drag = self.standard_deck_drag; + self.policy.volumes_enabled = true; + } + fn released_board(&mut self) { + self.policy.released = true; + self.board.bodies_mut()[BodyId::Deck.index()] + .inertia + .angular_drag = f32::from_bits(0x416f_ffff); + self.board.set_collision_group(7); + self.policy.volumes_enabled = true; + } + fn collision_volumes(&mut self, enabled: bool) { + self.policy.volumes_enabled = enabled; + } + fn clear_alignment(&mut self) { + self.policy.alignment = None; + } + fn alignment(&mut self, value: Alignment) { + self.policy.alignment = Some(value); + } + fn velocity(&mut self, value: Vector) { + for body in self.board.bodies_mut() { + body.rates.linear_velocity = xyz(value); + } + } + fn position(&mut self, value: Vector) { + let mut target = self.board.part_transforms()[BodyId::Deck.index()]; + target.translation = xyz(value); + self.board.set_transform(target); + } + fn hook_frame(&mut self, value: Frame) { + self.board.set_hook_transform(transform(value)); + } + fn target_position_velocity(&mut self, value: Vector) { + //82C04270 reads Body+16 (mass-frame position), not GetPartTransform. + let origin = self.board.bodies()[BodyId::Deck.index()].rates.position; + let frequency = 1.0 / self.timestep; + self.velocity([ + (value[0] - origin.x) * frequency, + (value[1] - origin.y) * frequency, + (value[2] - origin.z) * frequency, + 0., + ]); + } + fn torque(&mut self, value: Vector) { + //750F0 passes all three767D0 outputs to07328. They are requested + //angular displacements, not direct torques or inverse-inertia products. + skate_core::physics::deck_angular_correction::apply_axis_displacement( + &mut self.board.bodies_mut()[BodyId::Deck.index()].rates, + xyz(value), + ); + } +} + +///82C08370..8428 classifies retained per-part normals against the controller's +///two directions. This is a release observation, not an extra force or joint. +pub(crate) fn classify_alignment( + policy: &Policy, + ground: &mut skate_core::physics::board_ground::BoardGroundState, +) { + let Some(a) = policy.alignment else { + return; + }; + for part in &ground.parts { + if !part.in_contact { + continue; + } + let normal = [part.normal.x, part.normal.y, part.normal.z, 0.]; + if [a.first_1008, a.second_1024].into_iter().any(|direction| { + (normal[0] * -direction[0] + normal[1] * -direction[1]) + normal[2] * -direction[2] + > a.factor_1040 + }) { + ground.collision_flags |= 0x0400_0000; + } + } +} diff --git a/crates/skate-game/src/physics/offboard/hand_drives.rs b/crates/skate-game/src/physics/offboard/hand_drives.rs new file mode 100644 index 00000000..f5a3999a --- /dev/null +++ b/crates/skate-game/src/physics/offboard/hand_drives.rs @@ -0,0 +1,153 @@ +//! Native SkateboardController drives82D74FD8 /82D751D8 in the shared island. +//! Factory82AE64F8 stores argument5 at drive16 (A), argument4 at20 (B): +//! hand bodies3/7 are A and the deck is B, despite the factory argument order. +use skate_core::{ + math::{Basis3, Vector3}, + physics::{ + assembly::BodySnapshot, + board::BodyId, + board_step::ATTACHED_REACTION_BASE, + drive_frames::{RetailDriveFrame, RetailDriveFrames, retail_quaternion_from_basis}, + drive_parameters::{RetailDriveDynamics, RetailDriveParams, RetailDriveType}, + drive_solver::{RetailDriveBodyState, RetailDriveRows, build_drive_rows}, + rigid_body::pack_world_inverse_inertia, + skeleton_body::prepare_bone_drive_frames, + }, + player::offboard::board_possession::{Frame, State}, +}; + +pub(crate) fn append( + rows: &mut Vec, + state: &State, + board: &[BodySnapshot; 7], + skeleton: &[BodySnapshot; 26], + dt: f32, +) { + for (hand, part) in [3, 7].into_iter().enumerate() { + let drive = state.hands[hand]; + let a = skeleton[part]; + let b = board[BodyId::Deck.index()]; + if (a.state_flags | b.state_flags) & 4 == 0 { + continue; + } + let parameters = |words: [u32; 4]| RetailDriveParams { + spring_or_max_velocity: f32::from_bits(words[0]), + damping: f32::from_bits(words[1]), + max_strength: f32::from_bits(words[2]), + drive_type: match words[3] { + 1 => RetailDriveType::SoftDrive, + 2 => RetailDriveType::HardDrive, + _ => RetailDriveType::NoDrive, + }, + }; + rows.push(build_drive_rows( + body(a, ATTACHED_REACTION_BASE + part), + body(b, BodyId::Deck.index()), + prepare_bone_drive_frames(RetailDriveFrames { + body_a: frame(drive.child), + body_b: frame(drive.parent), + }), + RetailDriveDynamics { + linear: parameters(drive.dynamics[0]), + angular: parameters(drive.dynamics[1]), + }, + dt, + )); + } +} +fn frame(f: Frame) -> RetailDriveFrame { + RetailDriveFrame { + orientation: retail_quaternion_from_basis(Basis3 { + columns: std::array::from_fn(|i| [f[i][0], f[i][1], f[i][2]]), + }), + translation: Vector3::new(f[3][0], f[3][1], f[3][2]), + } +} +fn body(b: BodySnapshot, index: usize) -> RetailDriveBodyState { + let r = b.rates; + RetailDriveBodyState { + reaction_index: index, + state: b.state_flags, + orientation: r.orientation, + basis: r.basis, + center_of_mass: r.position, + linear_velocity: r.linear_velocity, + angular_velocity: r.angular_velocity, + force_acceleration: r.force_acceleration, + torque_acceleration: r.torque_acceleration, + inverse_mass: b.inertia.inverse_mass, + world_inverse_inertia: pack_world_inverse_inertia(r.world_inverse_inertia), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use skate_core::physics::{ + drive_solver::solve_drive_rows, + rigid_body::{ + RetailBodyRates, RetailInertiaDynamics, RetailQuaternion, RetailReactionCorrections, + }, + }; + fn active() -> BodySnapshot { + let basis = Basis3 { + columns: [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]], + }; + BodySnapshot { + state_flags: 4, + rates: RetailBodyRates { + orientation: RetailQuaternion::IDENTITY, + basis, + world_inverse_inertia: basis, + position: Vector3::ZERO, + linear_velocity: Vector3::ZERO, + angular_velocity: Vector3::ZERO, + force_acceleration: Vector3::ZERO, + torque_acceleration: Vector3::ZERO, + kinetic_energy: 0., + cool_down: 0, + }, + inertia: RetailInertiaDynamics { + inverse_tensor: Vector3::new(1., 1., 1.), + inverse_mass: 1., + spherical: 0., + maximum_linear_velocity: 1000., + maximum_angular_velocity: 1000., + linear_drag: 0., + angular_drag: 0., + }, + } + } + #[test] + fn held_hand_and_deck_exchange_reactions_and_release_removes_force() { + let board = [active(); 7]; + let mut skeleton = [active(); 26]; + skeleton[3].rates.position.x = 1.; + let mut state = State::default(); + state.selected_hand_424 = 0; + state.hands[0].dynamics = [[0x4395ffff, 0, 0x468c9fff, 2]; 2]; + let mut rows = Vec::new(); + append(&mut rows, &state, &board, &skeleton, 1. / 60.); + let mut reactions = [RetailReactionCorrections::default(); 34]; + solve_drive_rows(&mut rows, &mut reactions, 4); + let hand = reactions[ATTACHED_REACTION_BASE + 3].linear_displacement.x; + let deck = reactions[BodyId::Deck.index()].linear_displacement.x; + assert!(hand < 0. && deck > 0., "hand={hand}, deck={deck}"); + assert!((hand + deck).abs() < 1e-6); + assert_eq!( + reactions[ATTACHED_REACTION_BASE + 7].linear_displacement, + Vector3::ZERO + ); + state.disable_hand(); + rows.clear(); + append(&mut rows, &state, &board, &skeleton, 1. / 60.); + reactions.fill(RetailReactionCorrections::default()); + solve_drive_rows(&mut rows, &mut reactions, 4); + assert!( + reactions + .iter() + .all(|r| r.linear_displacement == Vector3::ZERO + && r.angular_displacement == Vector3::ZERO) + ); + } +} diff --git a/crates/skate-game/src/physics/offboard/mod.rs b/crates/skate-game/src/physics/offboard/mod.rs index 9f5569af..2a6b181d 100644 --- a/crates/skate-game/src/physics/offboard/mod.rs +++ b/crates/skate-game/src/physics/offboard/mod.rs @@ -1,8 +1,11 @@ //! Off-board game adapters over the shared physical and scene owners. +pub(crate) mod board_effects; +pub(crate) mod contact_queries; +pub(crate) mod ground_input; pub(crate) mod ground_query; +pub(crate) mod hand_drives; pub(crate) mod pose_adjust; +pub(crate) mod possession; +pub(crate) mod runtime; pub(crate) mod settings; pub(crate) mod skeleton_ground; -pub(crate) mod contact_queries; -pub(crate) mod runtime; -pub(crate) mod ground_input; diff --git a/crates/skate-game/src/physics/offboard/possession.rs b/crates/skate-game/src/physics/offboard/possession.rs new file mode 100644 index 00000000..d64f038d --- /dev/null +++ b/crates/skate-game/src/physics/offboard/possession.rs @@ -0,0 +1,117 @@ +//! Native hand controller over the player's live pose and board observations. +use crate::physics::{GamePhysics, SkaterRuntime}; +use skate_core::{ + physics::{contact_feedback::choose_surface, skeleton_animation_record::compose_affine}, + player::offboard::board_possession::{Processed, lifecycle::Observation}, +}; + +pub(crate) struct Actions<'a> { + pub state: &'a mut skate_core::player::offboard::board_possession::State, + pub settings: &'a skate_core::player::offboard::board_possession::lifecycle::Settings, + pub observation: &'a Observation, + pub effects: super::board_effects::BoardEffects<'a>, +} +impl skate_core::player::lifecycle::SkateboardControllerActions for Actions<'_> { + fn hold_skateboard( + &mut self, + fields: &mut skate_core::player::lifecycle::SkateboardControllerFields, + ) { + self.state.hold(fields, self.observation, &mut self.effects); + } + fn let_go_of_skateboard( + &mut self, + fields: &mut skate_core::player::lifecycle::SkateboardControllerFields, + ) { + self.state + .let_go(fields, self.observation, self.settings, &mut self.effects); + } +} + +pub(crate) fn observe( + physics: &GamePhysics, + skater: &SkaterRuntime, +) -> Result { + let p = &skater.player_input.processed; + let toolkit = skater + .player_input + .toolkit + .as_ref() + .ok_or("Board controller requires this update's board toolkit")?; + let roots = &skater.animated_skeleton.roots; + let frames = &skater.skeleton_input.drive_frames; + let physical_parts = skater.skeleton.part_transforms(); + Ok(Observation { + processed: Processed { + board_frame_64: toolkit.deck, + player_frame_192: p + .effective_anim_transform_192 + .map(|v| v.map(f32::from_bits)), + position_592: p.vectors_544_560_592_608[2].map(f32::from_bits), + velocity_912: p.vectors_880_896_912_928_944[2].map(f32::from_bits), + direction_400: p.vectors_400_416[0].map(f32::from_bits), + hide_direction_464: p.vectors_464_480_496_512_528[0].map(f32::from_bits), + flags_2476: p.flags_2476, + flags_2480: p.flags_2480, + flags_2488: p.flags_2488, + }, + board_collision_flags_872: physics.riding.ground.collision_flags, + //82C08818 produces840 by voting the four retained wheel surfaces. + board_state_840: choose_surface( + physics.riding.wheel_lines.physics_surfaces, + std::array::from_fn(|i| physics.riding.ground.parts[i].in_contact), + physics.riding.ground.collision_flags & 0x0200_0000 != 0, + ), + hand_contacts: [3, 7].map(|i| skater.collision_feedback.current[i]), + physical_hand_positions: [3, 7].map(|i| physical_parts[i][3]), + animation_board_frame_12624: frames[0], + animation_hand_frames: [frames[3], frames[7]], + attachment_frame_0: compose_affine(&roots.animation_to_world, &frames[0]), + }) +} + +pub(crate) fn update(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> Result<(), String> { + if !skater.skateboard_controller.fields.system_on_452 { + return Ok(()); + } + let observation = observe(physics, skater)?; + let offboard = &mut skater.offboard; + let mut effects = super::board_effects::BoardEffects { + board: &mut physics.board, + animated: &mut skater.ground_lifecycle.board_animated_290, + policy: &mut offboard.board_policy, + standard_deck_drag: offboard.standard_deck_drag, + timestep: skater.player_input.processed.timestep_2604, + }; + offboard.possession.update( + &mut skater.skateboard_controller.fields, + &observation, + &offboard.possession_settings, + &mut effects, + ); + Ok(()) +} + +pub(crate) fn publish(physics: &GamePhysics, skater: &mut SkaterRuntime) -> Result<(), String> { + let observation = observe(physics, skater)?; + let bone = compose_affine( + &skater.animated_skeleton.roots.animation_to_world, + &skater.skeleton_input.drive_frames[11], + ); + let value = skate_core::player::offboard::board_possession::fill( + &skater.skateboard_controller.fields, + &skater.offboard.possession, + &observation.processed, + bone, + ); + let output = &mut skater.player_input.physical.off_board; + output.board_angle_36 = value.angle_36; + output.board_angle_40 = value.angle_40; + output.flag_311 = u8::from(value.held_311); + output.free_board_312 = u8::from(value.free_312); + output.returning_board_313 = u8::from(value.returning_313); + output.hidden_board_321 = u8::from(value.hiding_321); + output.dropping_board_322 = u8::from(value.flag_322); + output.retrieving_board_323 = u8::from(value.flag_323); + output.board_request_324 = u8::from(value.flag_324); + Ok(()) +} diff --git a/crates/skate-game/src/physics/offboard/runtime.rs b/crates/skate-game/src/physics/offboard/runtime.rs index 1a8b645f..331ca803 100644 --- a/crates/skate-game/src/physics/offboard/runtime.rs +++ b/crates/skate-game/src/physics/offboard/runtime.rs @@ -10,6 +10,10 @@ use skate_core::point_graph::PointGraph; use skate_data::{animation_metadata::AnimationMetadata, collections::Collections}; pub(crate) struct Runtime { + pub board_policy: super::board_effects::Policy, + pub standard_deck_drag: f32, + pub possession: skate_core::player::offboard::board_possession::State, + pub possession_settings: skate_core::player::offboard::board_possession::lifecycle::Settings, pub controller: Controller, pub ground: ground_entry::State, pub contacts: Toolkit, @@ -136,6 +140,11 @@ impl Runtime { pub(crate) fn load(data: &Collections, metadata: &AnimationMetadata) -> Result { let settings = super::settings::Settings::load(data, metadata)?; Ok(Self { + board_policy: Default::default(), + standard_deck_drag: data.float("physicsdeck", "default", "DeckAngularDrag")? + * f32::from_bits(0x426f_ffff), + possession: Default::default(), + possession_settings: settings.possession, controller: Controller::new(settings.controller, settings.metrics), ground: Default::default(), contacts: Default::default(), diff --git a/crates/skate-game/src/physics/offboard/settings.rs b/crates/skate-game/src/physics/offboard/settings.rs index 7d6458c4..bf435340 100644 --- a/crates/skate-game/src/physics/offboard/settings.rs +++ b/crates/skate-game/src/physics/offboard/settings.rs @@ -2,6 +2,7 @@ mod board; mod curves; mod metrics; +mod possession; #[cfg(test)] mod tests; use skate_core::{ @@ -11,6 +12,7 @@ use skate_core::{ use skate_data::{animation_metadata::AnimationMetadata, collections::Collections}; pub(crate) struct Settings { + pub possession: skate_core::player::offboard::board_possession::lifecycle::Settings, pub controller: controller::Settings, pub board: skate_core::player::offboard::ground_sync::BoardSettings, pub metrics: [Option; 3], @@ -26,6 +28,7 @@ impl Settings { let biped = |name| curves::load::<8>(data, "physics_biped", name); let (sprint_blend, bounds) = biped("Hash_6B93C51256A30FB4")?; Ok(Self { + possession: possession::load(data)?, controller: controller::Settings { movement_intent: movement_intent::Settings { sprint_speed: curves::load::<4>( diff --git a/crates/skate-game/src/physics/offboard/settings/possession.rs b/crates/skate-game/src/physics/offboard/settings/possession.rs new file mode 100644 index 00000000..525d4344 --- /dev/null +++ b/crates/skate-game/src/physics/offboard/settings/possession.rs @@ -0,0 +1,24 @@ +//! Stock physics_skatecontroller/default data used by the board controller. +use skate_core::player::offboard::board_possession::lifecycle::Settings; +use skate_data::collections::Collections; +pub(super) fn load(data: &Collections) -> Result { + let float = |name| data.float("physics_skatecontroller", "default", name); + let curve = |name| super::curves::load::<8>(data, "physics_skatecontroller", name).map(|v| v.0); + Ok(Settings { + //75F00 reads893DA1B966231D76 for the hide trigger;75BB8 reads + //6CFF6F2C43F0AD41 for the hidden position. Lookup8 verifies both names. + hide_distance: float("MaxDistance")?, + hide_offset: float("DistanceToHideSkateboard")?, + return_distance: float("DistanceForBoardReturn")?, + mounted_return_distance: float("DistanceForMountedReturn")?, + mounting_time: float("MinTimeToRetrieveBoardWhileMounting")?, + retrieval_time: curve("RetrievalTimeVsDist")?, + retrieval_weight: curve("RetrievalSpeedCurve")?, + throw_pitch: float("ThrownSkateboardLaunchPitch")?, + throw_velocity: curve("ThrownSkateboardVelocity")?, + throw_target_pitch: float("ThrownSkateboardPitchTarget")?, + throw_pitch_scalar: float("ThrownSkateboardPitchScalar")?, + throw_roll_scalar: float("ThrownSkateboardRollScalar")?, + throw_yaw_scalar: float("ThrownSkateboardYawScalar")?, + }) +} diff --git a/crates/skate-game/src/physics/offboard/settings/tests.rs b/crates/skate-game/src/physics/offboard/settings/tests.rs index df156b07..7a9c2f9d 100644 --- a/crates/skate-game/src/physics/offboard/settings/tests.rs +++ b/crates/skate-game/src/physics/offboard/settings/tests.rs @@ -85,4 +85,7 @@ fn actual_stock_settings_and_metrics_load_together() { }); assert!(speeds[0] > 0.0 && speeds[0] < speeds[1] && speeds[1] < speeds[2]); assert_eq!(settings.controller.movement_intent.sprint_time_cap, 4.0); + //Independent native lookup8 keys:893DA1B966231D76 /6CFF6F2C43F0AD41. + assert_eq!(settings.possession.hide_distance, 30.0); + assert_eq!(settings.possession.hide_offset, 1000.0); } diff --git a/crates/skate-game/src/physics/offboard/skeleton_ground.rs b/crates/skate-game/src/physics/offboard/skeleton_ground.rs index e32c1e4a..9c9452bf 100644 --- a/crates/skate-game/src/physics/offboard/skeleton_ground.rs +++ b/crates/skate-game/src/physics/offboard/skeleton_ground.rs @@ -25,6 +25,51 @@ pub(crate) struct ReckoningUpdate { pub forward: [f32; 4], pub blend: f32, } + +///Borrow the same bodies, IK, roots, collision feedback and camera reckoning as +///the riding phases. Sync supplies the completed Biped job frame and COM. +pub(crate) fn update( + physics: &mut crate::physics::GamePhysics, + skater: &mut crate::physics::SkaterRuntime, + frame: Transform, + centre_of_mass: [f32; 4], +) -> Result<(), String> { + let collision = crate::physics::input_phase::collision(skater); + let flags = skater.player_input.processed.flags_2468; + let spin = skater.animation_input.extra.physical_body_spin; + let mut owners = SkeletonOwners { + animated: &mut skater.animated_skeleton, + body: &mut skater.skeleton, + drives: &mut skater.skeleton_drives, + ik: &mut skater.foot_ik, + animation_input: &mut skater.animation_input, + correction: &mut skater.skeleton_output.correction, + pose_errors: &mut skater.pose_errors, + }; + skater.skeleton_input.update_biped_ground( + &mut skater.skeleton_air, + &mut physics.board, + Input { + world_frame: &frame, + centre_of_mass_1056: centre_of_mass, + }, + &mut skater.player_input.processed, + &mut owners, + &skater.animation.packet.hierarchy, + &collision, + physics.settings.step.simulation, + |update| { + physics.riding.update_biped_reckoning( + &mut skater.air_reckoning.state, + update, + flags, + spin, + ); + Ok(()) + }, + )?; + Ok(()) +} impl SkeletonInputRuntime { pub(crate) fn update_biped_ground( &mut self, diff --git a/crates/skate-game/src/physics/player_state/publication.rs b/crates/skate-game/src/physics/player_state/publication.rs index 4bbbc9d0..8a20fdc9 100644 --- a/crates/skate-game/src/physics/player_state/publication.rs +++ b/crates/skate-game/src/physics/player_state/publication.rs @@ -124,14 +124,6 @@ pub(super) fn publish(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> //State Fill82D3B010 is a single byte from this retained Slide owner. physical.state.signed_ground_step_84 = u8::from(skater.slide_state.state.wall_riding); } - //ProcessOutput82DB71C4 calls SkateboardController::FillPhysOut82D76D20 - //for every selected state. The constructor82D74DD8 seeds448=0; use the - //same controller owner as state transitions and post-physics ragdoll. - physical.off_board.dropping_board_322 = u8::from(p.flags_2476 & (1 << 9) != 0); - physical.off_board.retrieving_board_323 = u8::from( - p.flags_2476 & (1 << 10) != 0 || skater.skateboard_controller.fields.state_448 == 4, - ); - //ProcessOutput82DB7130..7180 copies the HandPlantManager animation //flags1876 into Air304+20 one bit at a time, preserving the low28 bits. //The same manager word is consumed by the actual input publication. @@ -177,5 +169,6 @@ pub(super) fn publish(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> if state == PhysicalStateId::Teleporting { skater.teleport_state.publish_output(&mut skater.player_input.physical); } + super::super::offboard::possession::publish(physics, skater)?; Ok(()) } diff --git a/crates/skate-game/src/physics/player_state/transition.rs b/crates/skate-game/src/physics/player_state/transition.rs index d4155d41..e90afb6a 100644 --- a/crates/skate-game/src/physics/player_state/transition.rs +++ b/crates/skate-game/src/physics/player_state/transition.rs @@ -35,15 +35,6 @@ impl PhysicalStateCalls for Calls { )); } } -struct InactiveController; -impl SkateboardControllerActions for InactiveController { - fn hold_skateboard(&mut self) { - unreachable!("active controller rejected before transition") - } - fn let_go_of_skateboard(&mut self) { - unreachable!("active controller rejected before transition") - } -} pub(super) fn set( physics: &mut GamePhysics, skater: &mut SkaterRuntime, @@ -81,11 +72,29 @@ pub(super) fn set( "Physical state transition {current:?} -> {requested:?} requires its native Enter/Exit production adapter" )); } - if skater.skateboard_controller.fields.system_on_452 { - return Err( - "Ground transition requires active SkateboardController::LetGoOfSkateboard".into(), - ); + if skater.player_input.toolkit.is_none() { + skater.player_input.toolkit = Some(BoardToolkit::from_board( + &physics.board, + skater.player_input.processed.flags_2468, + skater.player_input.processed.scalar_2612, + skater.player_input.processed.vectors_464_480_496_512_528[0].map(f32::from_bits), + [0.0, 1.0, 0.0, 0.0], + )); } + let observation = super::super::offboard::possession::observe(physics, skater)?; + let offboard = &mut skater.offboard; + let mut controller_actions = super::super::offboard::possession::Actions { + state: &mut offboard.possession, + settings: &offboard.possession_settings, + observation: &observation, + effects: super::super::offboard::board_effects::BoardEffects { + board: &mut physics.board, + animated: &mut skater.ground_lifecycle.board_animated_290, + policy: &mut offboard.board_policy, + standard_deck_drag: offboard.standard_deck_drag, + timestep: skater.player_input.processed.timestep_2604, + }, + }; let p = &mut skater.player_input.processed; let player = &mut skater.player_input.player; let mut data = StateChangeData { @@ -114,7 +123,7 @@ pub(super) fn set( requested as u32, &mut data, &mut Calls, - &mut InactiveController, + &mut controller_actions, ) .map_err(|e| format!("Unknown physical state {}", e.0))?; player.state_count_1312 = data.player.word_1312; @@ -129,15 +138,6 @@ pub(super) fn set( p.state_count_2564 = data.processed.word_2564; p.state_timer_2664 = data.processed.scalar_2664; skater.skateboard_controller.fields = data.skateboard_controller; - if skater.player_input.toolkit.is_none() { - skater.player_input.toolkit = Some(BoardToolkit::from_board( - &physics.board, - p.flags_2468, - p.scalar_2612, - p.vectors_464_480_496_512_528[0].map(f32::from_bits), - [0.0, 1.0, 0.0, 0.0], - )); - } //Native82DB8540 publishes Processed state/history BEFORE old Exit. The //same retained objects then receive Exit followed by the new Enter. match current { @@ -159,7 +159,10 @@ pub(super) fn set( PhysicalStateId::GroundAnimation => super::super::ground_animation::enter(physics, skater), PhysicalStateId::SlideGround => super::super::slide_state::enter(physics, skater), PhysicalStateId::WipeoutGround => super::super::wipeout_states::enter(physics, skater), - PhysicalStateId::Teleporting => { skater.teleport_state.enter(); Ok(()) }, + PhysicalStateId::Teleporting => { + skater.teleport_state.enter(); + Ok(()) + } _ => unreachable!("state support checked before publication"), } } diff --git a/crates/skate-game/src/physics/riding_outputs.rs b/crates/skate-game/src/physics/riding_outputs.rs index dc3dfa60..30eded60 100644 --- a/crates/skate-game/src/physics/riding_outputs.rs +++ b/crates/skate-game/src/physics/riding_outputs.rs @@ -52,6 +52,30 @@ pub(crate) struct RidingOutputs { pending_wheel_queries: Option<[Option; 4]>, } impl RidingOutputs { + ///Skeleton82BDE060's final82D8E3E0 call, argument7=true. + pub(crate) fn update_biped_reckoning( + &mut self, + air: &mut skate_core::air::reckoning::AirState, + input: super::offboard::skeleton_ground::ReckoningUpdate, + flags_2468: u32, + body_spin: f32, + ) { + let previous_up = lanes(self.reckoning.up); + skate_core::player::offboard::ground_reckoning::update( + &mut self.reckoning, &mut self.reckoning_frames, &mut self.body_spin, air, + skate_core::player::offboard::ground_reckoning::Settings { + ground_normal_smoothing: self.orientation_settings.ground_normal_smoothing, + tilt_vs_rotation: &self.tilt_vs_rotation, + tilt_vs_slope: &self.tilt_vs_slope, + }, + skate_core::player::offboard::ground_reckoning::Input { + previous_up, + requested_up: input.up, requested_forward: input.forward, blend: input.blend, + reverse_stance: flags_2468 & 0x0010_0000 != 0, + enable_body_spin_input: true, physical_body_spin_2812: body_spin, + }, + ); + } ///Board82C0D680 resets CollisionInfo and both probes, preserving7692. pub fn reset_for_teleport(&mut self) { let elapsed = self.ground.time_without_wheel_contact; diff --git a/crates/skate-game/src/physics/solve.rs b/crates/skate-game/src/physics/solve.rs index cbe86f04..a6932a02 100644 --- a/crates/skate-game/src/physics/solve.rs +++ b/crates/skate-game/src/physics/solve.rs @@ -14,7 +14,11 @@ pub(super) fn advance( skater: &mut SkaterRuntime, truck_targets: [f32; 2], ) -> Result<(), String> { - let board_volumes = colliders::world_volumes(&physics.board, &physics.settings); + let board_volumes = if skater.offboard.board_policy.volumes_enabled { + colliders::world_volumes(&physics.board, &physics.settings) + } else { + Vec::new() + }; let skeleton_volumes = skeleton_colliders::world_volumes(&skater.skeleton, &skater.skeleton_collision)?; // Each native assembly has its own query record and retention buffer. @@ -55,6 +59,16 @@ pub(super) fn advance( ATTACHED_REACTION_BASE + PART_COUNT, dt, ); + let skeleton_drive_count = drives.rows.len(); + if skater.skateboard_controller.fields.system_on_452 { + super::offboard::hand_drives::append( + &mut drives.rows, + &skater.offboard.possession, + physics.board.bodies(), + skater.skeleton.bodies(), + dt, + ); + } let bodies = skater .skeleton .bodies_mut() @@ -77,6 +91,9 @@ pub(super) fn advance( .publish_physical_record(deck_frame(&physics.board)); // These solved rows are consumed by the actual collision/drive feedback // phase; keep their identity and impulses after the shared solve. + //Controller drive descriptors have flags28=0: they are not Skeleton's + //spy drives and must not be paired with SkeletonDriveIdentity feedback. + drives.rows.truncate(skeleton_drive_count); skater.solved_drives = Some(drives); Ok(()) } diff --git a/docs/RUNOUT.md b/docs/RUNOUT.md index c05b4dda..9167e33c 100644 --- a/docs/RUNOUT.md +++ b/docs/RUNOUT.md @@ -109,3 +109,40 @@ stock controller's real-floor test runs60 updates each for idle, half input and full input. This is controller/query coverage; production transitions still reject BipedGround until its complete physical adapter is available. No playable binary has been staged or launched from these changes. + +## Board possession and shared solver integration + +Native hand drives82D74FD8/82D751D8 now append to the existing board/skeleton +island. Factory82AE64F8 stores its fifth argument as body A and fourth as B: +hands3/7 therefore react against deck6. The skeleton's drive-spy results retain +their original row count. Tests verify opposite hand/deck reactions and zero +reaction after releasing the hand drive. + +The controller now has production hold/release, update and output adapters over +the actual animated hand frames, physical hand contacts, board toolkit and +retained collision normals. Its standard/released drag, hook drive and collision +volume policies feed the existing physical owners. Native82C08818's surface vote +is the producer of the controller's observed board840; it is not a sleeping flag. +The lifecycle callback accepts the live controller fields so LetGo can preserve +its eight-update throw countdown across a state change. Board positioning uses +82C0B2C8 without clearing rates or pending forces, verified by regression test. + +Observed code comes from the generated TU3 functions and raw image +default_82000000_011B0000.bin, SHA256 +f4aa113eb541bfba03dbc108cf5ab43f58c965b20fa3b82f9c40938a0ad841c4. +Lookup8 proves893DA1B966231D76 is MaxDistance (hide trigger) and +6CFF6F2C43F0AD41 is DistanceToHideSkateboard (hidden position offset), correcting +the earlier reversed inference. The biped skeleton adapter also borrows the live roots, +IK, collision and shared reckoning owners, including native body-spin input. + +The thrown-board update750F0 calls82C07328 three times. These outputs are angular +displacements:07328 removes the current per-step motion along the requested axis +and uses inverse-tensor/step/tensor conversion to add angular acceleration. It +does not apply the outputs as raw torque. A regression covers braking overshoot +and correcting opposite motion while preserving perpendicular angular velocity. + +Validation:459 core tests and83 game tests passed, including the final +output/skeleton-owner wiring, private map and stock difficulty tests. These +are still component and existing-state tests: BipedGround/BipedAir state +integration and remount remain unfinished. The runout crash is not yet fixed +in a staged playable build.