mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Add archive manual support and isolated riding fixes to existing build
This commit is contained in:
1 parent
ba929487c1
commit
ae452ba3fe
24 files changed
+455
-45
No files matched your search
@@ -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,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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -91,6 +91,7 @@ pub(crate) fn build(
|
||||
)
|
||||
.chain(),
|
||||
)
|
||||
.add_plugins(crate::fps_overlay::FpsOverlayPlugin)
|
||||
.add_plugins((
|
||||
crate::retail_render::RetailRenderPlugin,
|
||||
input::InputPlugin,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user