Add archive manual support and isolated riding fixes to existing build

This commit is contained in:
chazmm committed 2026-09-08 20:51:37 +01:00
1 parent ba929487c1
commit ae452ba3fe
24 files changed
+455 -45

No files matched your search

+42
View File
@@ -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<f32>, 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 }
}
+1
View File
@@ -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;
@@ -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<RidingIntent> {
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
}
+1
View File
@@ -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;
@@ -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
}
@@ -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);
}
@@ -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 }
}
}
@@ -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<skate_core::input::riding_intentions::RidingIntent> {
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);
}
+47 -7
View File
@@ -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<const N: usize>(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);
}
}
+1
View File
@@ -91,6 +91,7 @@ pub(crate) fn build(
)
.chain(),
)
.add_plugins(crate::fps_overlay::FpsOverlayPlugin)
.add_plugins((
crate::retail_render::RetailRenderPlugin,
input::InputPlugin,
+65
View File
@@ -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<Time<Real>>,
mut sample: Local<FrameSample>,
mut labels: Query<&mut Text, With<FpsText>>,
) {
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();
}
}
+1
View File
@@ -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;
@@ -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<bool>,
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)?,
@@ -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)?,
@@ -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(
@@ -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<Settings, String> {
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")?,
})
}
@@ -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),
+1
View File
@@ -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;
@@ -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;
@@ -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,
));
@@ -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;
@@ -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);
}
@@ -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,
);
@@ -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);