mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Fix FingerFlipOut crash with native grab exit curve lifecycle
This commit is contained in:
1 parent
1a6e95b188
commit
dff672aee4
8 files changed
+229
-2
No files matched your search
@@ -30,6 +30,7 @@ pub(crate) mod motion_ground_slope;
|
||||
pub(crate) mod motion_hand_services;
|
||||
mod motion_hooks;
|
||||
mod motion_hippy_jump;
|
||||
mod motion_finger_flip;
|
||||
pub(crate) mod motion_intent_filter;
|
||||
mod motion_kickturn;
|
||||
pub(crate) mod motion_landing;
|
||||
|
||||
@@ -114,6 +114,7 @@ pub struct MotionHost {
|
||||
pub physical_stance: Option<(bool, bool)>,
|
||||
pub flags: super::motion_landing::Flags,
|
||||
hippy_jump: super::motion_hippy_jump::Settings,
|
||||
finger_flip: super::motion_finger_flip::Settings,
|
||||
pub landing_physical: Option<super::motion_landing::Physical>,
|
||||
pub wipeout_physical: Option<super::motion_wipeout::Physical>,
|
||||
pub wipeout_controls: super::motion_wipeout::Controls,
|
||||
@@ -290,6 +291,7 @@ impl MotionHost {
|
||||
kickturn: super::motion_kickturn::load_settings(data)?,
|
||||
manual: super::motion_manual::settings(data)?,
|
||||
hippy_jump: super::motion_hippy_jump::Settings::load(data)?,
|
||||
finger_flip: super::motion_finger_flip::Settings::load(data)?,
|
||||
next_instance: 1,
|
||||
})
|
||||
}
|
||||
@@ -424,3 +426,7 @@ mod jump_into_tests;
|
||||
#[cfg(test)]
|
||||
#[path = "tests/motion_deck_angles.rs"]
|
||||
mod deck_angles_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/motion_finger_flip.rs"]
|
||||
mod finger_flip_tests;
|
||||
@@ -290,6 +290,23 @@ pub(super) fn execute(
|
||||
// matching conditions; the authored node itself must still
|
||||
// advance that lifecycle instead of aborting graph execution.
|
||||
match operation {
|
||||
crate::graph_host::motion_stock_gameplay::Operation::FingerFlipOut { grab_intent } => {
|
||||
let Instance::FingerFlipOut(state) = instance else {
|
||||
return Err("FingerFlipOut operation/instance mismatch".into());
|
||||
};
|
||||
if phase == 0 {
|
||||
state.begin();
|
||||
} else if phase == 1 {
|
||||
let value = state.update(
|
||||
host.animation.motion_intents.contains_key(&grab_intent),
|
||||
frame.dt, &host.finger_flip,
|
||||
);
|
||||
host.animation.set_attribute(SettableAttribute {
|
||||
name: encode(b"Grabbing"), value,
|
||||
normalized: false, sequence_id: -1,
|
||||
});
|
||||
}
|
||||
}
|
||||
crate::graph_host::motion_stock_gameplay::Operation::MatchAirTime => {
|
||||
let Instance::MatchAirTime(state) = instance else {
|
||||
return Err("MatchAirTime operation/instance mismatch".into());
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
//! FingerFlipOut: TU3 Begin82BB3198, Update82BB7C40, empty End82B61BB8.
|
||||
use skate_core::point_graph::PointGraph;
|
||||
use skate_data::collections::Collections;
|
||||
|
||||
pub(super) struct Settings {
|
||||
minimum: f32,
|
||||
maximum: f32,
|
||||
curve: PointGraph<8>,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub(super) fn load(data: &Collections) -> Result<Self, String> {
|
||||
// GlobalAttributeCollection164 is selected at8289FD10..30. The
|
||||
// anim_motion schema places this PointNegGraphData8 field at190.
|
||||
// Preserve numeric identities because the retail vault omits names.
|
||||
let words = data.words::<20>(
|
||||
"anim_motion", "Hash_41DB0C4F82003A15", "Hash_E0C1407B688858AD",
|
||||
)?;
|
||||
let minimum = f32::from_bits(words[0]);
|
||||
let maximum = f32::from_bits(words[2]);
|
||||
if !minimum.is_finite() || !maximum.is_finite() || minimum > maximum {
|
||||
return Err("FingerFlipOut has invalid stock timer bounds".into());
|
||||
}
|
||||
Ok(Self {
|
||||
minimum,
|
||||
maximum,
|
||||
curve: PointGraph {
|
||||
x: std::array::from_fn(|i| f32::from_bits(words[4 + i])),
|
||||
y: std::array::from_fn(|i| f32::from_bits(words[12 + i])),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct State {
|
||||
elapsed: f32,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub(super) fn begin(&mut self) {
|
||||
self.elapsed = 0.0;
|
||||
}
|
||||
|
||||
pub(super) fn update(&mut self, grab_present: bool, dt: f32, settings: &Settings) -> f32 {
|
||||
//82454FD8 tests membership, including a zero-valued intent. The time
|
||||
//service supplies dt; this is not the magnitude of the grab input.
|
||||
self.elapsed = if grab_present { self.elapsed - dt } else { self.elapsed + dt };
|
||||
self.elapsed = self.elapsed.clamp(settings.minimum, settings.maximum);
|
||||
settings.curve.evaluate(self.elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn finger_flip_release_regrab_bounds_and_reentry() {
|
||||
let settings = Settings {
|
||||
minimum: 0.0,
|
||||
maximum: 0.5,
|
||||
curve: PointGraph {
|
||||
x: std::array::from_fn(|i| i as f32 / 14.0),
|
||||
y: std::array::from_fn(|i| 1.0 - i as f32 / 7.0),
|
||||
},
|
||||
};
|
||||
let mut state = State::default();
|
||||
state.begin();
|
||||
assert_eq!(state.update(true, 0.25, &settings), 1.0);
|
||||
assert!((state.update(false, 0.125, &settings) - 0.75).abs() < 1e-6);
|
||||
assert!((state.update(false, 0.125, &settings) - 0.5).abs() < 1e-6);
|
||||
assert!((state.update(true, 0.125, &settings) - 0.75).abs() < 1e-6);
|
||||
assert_eq!(state.update(false, 1.0, &settings), 0.0);
|
||||
assert_eq!(state.update(false, 1.0, &settings), 0.0);
|
||||
assert_eq!(state.elapsed, 0.5);
|
||||
state.begin();
|
||||
assert_eq!(state.elapsed, 0.0);
|
||||
assert_eq!(state.update(true, 0.25, &settings), 1.0);
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ pub(super) enum Instance {
|
||||
Shove(super::super::motion_shove::ShoveState),
|
||||
SetManualAngle(skate_core::animation::manual::State),
|
||||
HippyJumpAntic(super::super::motion_hippy_jump::State),
|
||||
FingerFlipOut(super::super::motion_finger_flip::State),
|
||||
///82BB5930 initializes byte8 once per allocated behavior instance.
|
||||
JumpInto { first_update: bool },
|
||||
}
|
||||
@@ -95,6 +96,9 @@ impl Instance {
|
||||
MotionOperation::StockGameplay(
|
||||
super::super::motion_stock_gameplay::Operation::HippyJumpAntic,
|
||||
) => Self::HippyJumpAntic(Default::default()),
|
||||
MotionOperation::StockGameplay(
|
||||
super::super::motion_stock_gameplay::Operation::FingerFlipOut { .. },
|
||||
) => Self::FingerFlipOut(Default::default()),
|
||||
MotionOperation::StockGameplay(
|
||||
super::super::motion_stock_gameplay::Operation::JumpInto { .. },
|
||||
) => Self::JumpInto { first_update: true },
|
||||
|
||||
@@ -146,7 +146,7 @@ pub enum Operation {
|
||||
attribute_y: AttributeName,
|
||||
},
|
||||
FootPlantAbsorb,
|
||||
FingerFlipOut,
|
||||
FingerFlipOut { grab_intent: String },
|
||||
EnterSkitchingBehaviour,
|
||||
SkitchingBehaviour,
|
||||
SkitchShimmyingBehaviour,
|
||||
@@ -217,7 +217,10 @@ impl Operation {
|
||||
attribute_x: encode(b"tweak_x"),
|
||||
attribute_y: encode(b"tweak_y"),
|
||||
}, "FootPlantAbsorb" => Self::FootPlantAbsorb,
|
||||
"FingerFlipOut" => Self::FingerFlipOut,
|
||||
"FingerFlipOut" => Self::FingerFlipOut {
|
||||
//Factory82BC7C18 binds authored grabintent and literal Grabbing.
|
||||
grab_intent: required_text(a, "grabintent"),
|
||||
},
|
||||
"EnterSkitchingBehaviour" => Self::EnterSkitchingBehaviour,
|
||||
"SkitchingBehaviour" => Self::SkitchingBehaviour,
|
||||
"SkitchShimmyingBehaviour" => Self::SkitchShimmyingBehaviour,
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires private stock graphs and animation banks"]
|
||||
fn stock_finger_flip_crash_node_3080_lifecycle_and_intent_source() {
|
||||
use crate::graph_runtime::CompiledGraph;
|
||||
use skate_data::state_graph::{StateGraph, binding::Binding};
|
||||
use super::super::motion_stock_gameplay::Operation;
|
||||
let root = std::path::PathBuf::from(std::env::var_os("SKATE3_ASSET_ROOT").unwrap());
|
||||
let source = StateGraph::load(&root.join(
|
||||
"private/stock/data/state/MotionGraph_OnBoard.stategraph",
|
||||
)).unwrap();
|
||||
let binding = Binding::from_graph(&source).unwrap();
|
||||
let runtime = CompiledGraph::from_binding(&binding).unwrap();
|
||||
let graph = LoadedGraph { source, binding, runtime };
|
||||
let mut host = MotionHost::from_graph(
|
||||
&graph, &Collections::load(&root).unwrap(),
|
||||
skate_data::animation_banks::AnimationBanks::load(&root).unwrap().metadata().unwrap(),
|
||||
PlaybackContext { is_switch: Some(false), is_mirrored: Some(false),
|
||||
board_available: Some(true), pro_skater: encode(b""), transition_override: None },
|
||||
).unwrap();
|
||||
let behavior = 3080;
|
||||
let MotionOperation::StockGameplay(Operation::FingerFlipOut { grab_intent }) =
|
||||
&host.operations[host.remap.behaviors[behavior]] else {
|
||||
panic!("The reported stock behavior must bind FingerFlipOut");
|
||||
};
|
||||
let intent = grab_intent.clone();
|
||||
assert!(!intent.is_empty() && !intent.contains('$'), "authored intent must be resolved");
|
||||
let frame = Frame { dt: 0.125, current: None, last: None, state_times: vec![] };
|
||||
host.allocate(behavior, &frame);
|
||||
host.execute(behavior, &frame, 0).unwrap();
|
||||
assert_eq!(host.animation.pending_parameter(encode(b"Grabbing")), None);
|
||||
|
||||
// Raw AG input cannot substitute for the MG intent read by virtual16.
|
||||
host.action_intents.insert(&intent, 1.0);
|
||||
host.execute(behavior, &frame, 1).unwrap();
|
||||
let released = host.animation.pending_parameter(encode(b"Grabbing")).unwrap();
|
||||
assert!((0.0..1.0).contains(&released));
|
||||
// Native membership test treats zero as present and reverses the timer.
|
||||
host.animation.motion_intents.insert(&intent, 0.0);
|
||||
host.execute(behavior, &frame, 1).unwrap();
|
||||
assert_eq!(host.animation.pending_parameter(encode(b"Grabbing")), Some(1.0));
|
||||
host.animation.motion_intents.remove(&intent);
|
||||
for _ in 0..6 { host.execute(behavior, &frame, 1).unwrap(); }
|
||||
assert_eq!(host.animation.pending_parameter(encode(b"Grabbing")), Some(0.0));
|
||||
host.execute(behavior, &frame, 2).unwrap();
|
||||
assert_eq!(host.animation.pending_parameter(encode(b"Grabbing")), Some(0.0));
|
||||
// Reentry resets private time, but Begin/End do not publish a parameter.
|
||||
host.execute(behavior, &frame, 0).unwrap();
|
||||
assert_eq!(host.animation.pending_parameter(encode(b"Grabbing")), Some(0.0));
|
||||
host.execute(behavior, &frame, 1).unwrap();
|
||||
assert_eq!(host.animation.pending_parameter(encode(b"Grabbing")), Some(released));
|
||||
|
||||
let second = host.remap.behaviors.iter().enumerate().find_map(|(id, &op)| {
|
||||
(id != behavior && matches!(host.operations[op],
|
||||
MotionOperation::StockGameplay(Operation::FingerFlipOut { .. }))).then_some(id)
|
||||
}).expect("stock graph has separate finger-flip exit nodes");
|
||||
host.allocate(second, &frame);
|
||||
host.execute(second, &frame, 0).unwrap();
|
||||
host.execute(second, &frame, 1).unwrap();
|
||||
assert_eq!(host.animation.pending_parameter(encode(b"Grabbing")), Some(released));
|
||||
host.execute(behavior, &frame, 1).unwrap();
|
||||
assert!(host.animation.pending_parameter(encode(b"Grabbing")).unwrap() < released);
|
||||
assert!(host.animation.motion_attributes.is_empty(), "writes a tree parameter, not a physics packet");
|
||||
}
|
||||
@@ -12,6 +12,56 @@ Coverage limits: a Windows unhandled-exception filter attempts a fixed, allocati
|
||||
|
||||
For manual UI validation, run the executable with `--crash-report-preview`. This generates a clearly synthetic report and opens the popup without initializing the game, setup, Steam or assets. Check Copy report, clipboard text, Open folder, saved UTF-8 text and Close. Static tests do not validate desktop presentation.
|
||||
|
||||
## FingerFlipOut (2026-09-09)
|
||||
|
||||
Observed user failure: clean revision `1a6e95b`, build timestamp
|
||||
`1788967727931108700`, report timestamp `1788968141`, University fingerprint
|
||||
`c911af6099e02b65`. At tick3467 in KnownAir, MotionGraph behavior3080 returned
|
||||
`MotionGraph stock gameplay producer FingerFlipOut is not implemented`.
|
||||
This was an explicit unsupported-operation exit, with no recorded panic or
|
||||
native exception. The matching launch stderr contains the same error.
|
||||
|
||||
Native recovery from the TU3 image mapped at `0x82000000`:
|
||||
|
||||
- Factory82BC7C18 binds authored `grabintent` and literal `Grabbing`.
|
||||
Vtable8232072C allocates a private float through82BBCB50, calls
|
||||
Begin82BB3198, Update82BB7C40 and empty End82B61BB8.
|
||||
- Begin sets the float to zero. Update reads the MotionGraph intent map
|
||||
through Actor20/virtual16. Membership lookup82454FD8 counts a zero-valued
|
||||
intent as present. Presence subtracts the time-service delta; absence adds
|
||||
it. This does not read raw ActionGraph or filtered tweak input.
|
||||
- Update clamps the retained time to layout190/198, evaluates eight curve
|
||||
keys at1A0 and values at1C0 through PointGraph82481E10, then writes
|
||||
`Grabbing` through ISkaterAnim80 (unnormalized, sequence -1).
|
||||
- Global constructor8289FD10..30 selects collection `41DB0C4F82003A15` at
|
||||
offset164. Helper8289D4E8 identifies class `anim_motion`. The retail schema
|
||||
independently maps layout190 to field `E0C1407B688858AD`, type
|
||||
`Sk8::PointNegGraphData8`. Its stock timer bounds are 0 and 0.5 seconds,
|
||||
with a descending grab blend from 1 to 0. Numeric names remain supported
|
||||
directly; no shared asset edit or invented curve is required.
|
||||
|
||||
The implementation retains one timer per bound behavior and publishes only
|
||||
on Update. Begin resets time; End leaves the last parameter alone. Other
|
||||
unimplemented operations still report errors.
|
||||
|
||||
Evidence identities (SHA-256): TU3 mapped image
|
||||
`f4aa113eb541bfba03dbc108cf5ab43f58c965b20fa3b82f9c40938a0ad841c4`;
|
||||
tested stock MotionGraph
|
||||
`806b2665e435be4e313d70a06fab4538e0adf60a2dfd3e80e82ae8d0b4a2e7c1`.
|
||||
Bounded decoded instructions, schema lookup and the original report are
|
||||
retained privately with this test build. IDA changes were confined to a
|
||||
disposable database copy. The instruction/data mapping is observed;
|
||||
successful gameplay reproduction after the fix remains user validation.
|
||||
|
||||
Focused regressions cover release/regrab timing, clamping, entry reset,
|
||||
the actual stock behavior3080, zero-valued intent membership, raw-input
|
||||
separation, independent behavior instances and Begin/End publication rules.
|
||||
They do not launch the renderer or replay the user's full input sequence.
|
||||
|
||||
Validation: `cargo test --release --locked --target x86_64-pc-windows-msvc
|
||||
-p skate-game --bin skate3rust --no-default-features finger_flip --
|
||||
--include-ignored` passed both tests against the prepared assets (0.18 seconds).
|
||||
|
||||
## SetDeckPitchAndYaw (2026-09-08)
|
||||
|
||||
The supplied report stopped in KnownAir at tick4813, MotionGraph behavior3373:
|
||||
|
||||
Reference in new issue
Block a user