Connect native board possession effects and hand drives to shared physics

This commit is contained in:
chazmm committed 2026-09-06 19:49:48 +01:00
1 parent 890d9001ad
commit fb78128153
24 files changed
+765 -53

No files matched your search

+32 -5
View File
@@ -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 {
@@ -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();
@@ -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;
@@ -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();
@@ -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,
+5 -5
View File
@@ -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<C: SkateboardControllerActions>(
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<C: SkateboardControllerActions>(
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;
@@ -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,
+2 -2
View File
@@ -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 {
+4
View File
@@ -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,
+1
View File
@@ -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;
@@ -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<Alignment>,
}
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;
}
}
}
@@ -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<RetailDriveRows>,
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)
);
}
}
@@ -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;
@@ -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<Observation, String> {
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(())
}
@@ -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<Self, String> {
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(),
@@ -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<controller::ClipMetric>; 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>(
@@ -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<Settings, String> {
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")?,
})
}
@@ -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);
}
@@ -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<F>(
&mut self,
@@ -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(())
}
@@ -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"),
}
}
@@ -52,6 +52,30 @@ pub(crate) struct RidingOutputs {
pending_wheel_queries: Option<[Option<WheelLineHit>; 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;
+18 -1
View File
@@ -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(())
}
+37
View File
@@ -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.