mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Merge native bail recovery with map transition support
This commit is contained in:
commit
778706b915
15 files changed
+922
-17
No files matched your search
@@ -22,3 +22,4 @@ pub mod offboard;
|
||||
|
||||
pub mod conditioner_capabilities;
|
||||
pub mod teleport_state;
|
||||
pub mod respawn;
|
||||
@@ -0,0 +1,236 @@
|
||||
//! TU3 actor checkpoint history (82BFB068..82BFCB38).
|
||||
//! Scene predicates are supplied by the host; absence is never acceptance.
|
||||
use crate::physics::skeleton_animation_record::{AnimationPartTransform as Matrix, IDENTITY};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Candidate {
|
||||
pub transform: Matrix,
|
||||
pub stance: u32,
|
||||
pub offboard: bool,
|
||||
pub score: f32,
|
||||
}
|
||||
|
||||
/// Separate native predicates: history selection does not repeat ground or edge
|
||||
/// validation, whereas current-position selection does (82BFC038/82BFC378).
|
||||
pub trait Validation {
|
||||
type Error;
|
||||
fn ground(&mut self, transform: &Matrix) -> Result<Option<Ground>, Self::Error>;
|
||||
fn location(&mut self, transform: &Matrix) -> Result<bool, Self::Error>;
|
||||
fn occupants(&mut self, transform: &Matrix) -> Result<bool, Self::Error>;
|
||||
fn edges(&mut self, transform: &Matrix) -> Result<bool, Self::Error>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Ground {
|
||||
pub position: [f32; 4],
|
||||
pub offboard: bool,
|
||||
}
|
||||
|
||||
///Completed native packet fields. The caller must provide actual producer
|
||||
///values, especially Ground323 and conditioner164; neither means grounded.
|
||||
pub struct Observation {
|
||||
pub measurements: i32,
|
||||
pub root_position: [f32; 4], //Skeleton416, unmirrored11920 translation
|
||||
pub com_position: [f32; 4], //Skeleton64, physical COM16144
|
||||
pub teleport_requested: bool, //State69
|
||||
pub physical_state: u32, //State16
|
||||
pub state_frames: u32, //State4
|
||||
pub ground_suppressed: bool, //Ground323
|
||||
pub offboard_correction: bool, //OffBoard333
|
||||
pub ground_category: u32, //Collision16 low16
|
||||
pub foot_categories: [u32; 2], //OffBoard52/56 packed categories
|
||||
pub riding_transform: Matrix, //82591E30(false)
|
||||
pub offboard_transform: Matrix, //82591E30(true)
|
||||
pub stance: u32, //82592B68
|
||||
pub alternate_world: bool, //SimController virtual0
|
||||
}
|
||||
|
||||
pub struct History {
|
||||
initial: Candidate,
|
||||
entries: Vec<Candidate>, // native33 slots reserve one sentinel:32 entries
|
||||
cooldown: i32,
|
||||
pub current_orientation: Matrix, //208; only eligible riding states write it
|
||||
pub current_position: [f32; 4], //272; every actor update writes Skeleton64
|
||||
}
|
||||
impl History {
|
||||
pub fn new(initial: Candidate) -> Self {
|
||||
Self {
|
||||
initial,
|
||||
entries: vec![initial],
|
||||
cooldown: 20,
|
||||
current_orientation: IDENTITY,
|
||||
current_position: initial.transform[3],
|
||||
}
|
||||
}
|
||||
|
||||
///82BFB1E0/82BFB3F8. Do not call this with last-grounded observations;
|
||||
///the native actor consumes one completed physical publication per update.
|
||||
pub fn observe<V: Validation>(
|
||||
&mut self,
|
||||
input: &Observation,
|
||||
minimum_state_frames: u32,
|
||||
scene: &mut V,
|
||||
) -> Result<(), V::Error> {
|
||||
self.current_position = input.com_position;
|
||||
if !self.recording_due(input.measurements, input.root_position) || input.teleport_requested
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
let (transform, offboard, score) = match input.physical_state {
|
||||
100 | 104 => {
|
||||
//This write precedes both the15-frame gate and terrain checks.
|
||||
self.current_orientation = input.riding_transform;
|
||||
if input.state_frames <= minimum_state_frames
|
||||
|| input.ground_suppressed
|
||||
|| !surface_allowed(input.ground_category)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
(
|
||||
input.riding_transform,
|
||||
input.ground_category == 8,
|
||||
surface_score(input.ground_category),
|
||||
)
|
||||
}
|
||||
500 | 502 => {
|
||||
if input.state_frames <= minimum_state_frames || input.offboard_correction {
|
||||
return Ok(());
|
||||
}
|
||||
(
|
||||
input.offboard_transform,
|
||||
true,
|
||||
surface_score(input.foot_categories[0])
|
||||
.max(surface_score(input.foot_categories[1])),
|
||||
)
|
||||
}
|
||||
_ => return Ok(()),
|
||||
};
|
||||
if !scene.location(&transform)? || !scene.edges(&transform)? {
|
||||
return Ok(());
|
||||
}
|
||||
if !input.alternate_world && scene.ground(&transform)?.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
//The recording path discards the ray's offboard byte and hit position.
|
||||
self.insert(Candidate {
|
||||
transform,
|
||||
stance: input.stance,
|
||||
offboard,
|
||||
score,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///82BFB1E0: call even when the physical state cannot supply a candidate.
|
||||
///The120 gate uses conditioner164 (EmpiricalMeasurements82DE5858), not
|
||||
///State4's time in the current state. Its reset lifecycle is caller-owned.
|
||||
pub fn recording_due(&mut self, measurements: i32, root_position: [f32; 4]) -> bool {
|
||||
let mut eligible = measurements >= 120;
|
||||
if !eligible {
|
||||
self.cooldown = 0;
|
||||
}
|
||||
if self.cooldown > 0 {
|
||||
self.cooldown -= 1;
|
||||
eligible = false;
|
||||
}
|
||||
eligible
|
||||
&& !self
|
||||
.entries
|
||||
.iter()
|
||||
.rev()
|
||||
.any(|entry| distance_squared(entry.transform[3], root_position) < 2.25)
|
||||
}
|
||||
|
||||
///82BFCB38. Construction and successful recording both refresh cooldown.
|
||||
pub fn insert(&mut self, candidate: Candidate) {
|
||||
if self.entries.len() == 32 {
|
||||
self.entries.remove(0);
|
||||
}
|
||||
self.entries.push(candidate);
|
||||
self.cooldown = 20;
|
||||
}
|
||||
|
||||
///82BFC550: descending score, newest wins ties; selected entry is removed
|
||||
///before validation, including successful selections. Repeated bails thus
|
||||
///cannot keep selecting the same history entry.
|
||||
fn pop_best(&mut self) -> Option<Candidate> {
|
||||
let mut best = None;
|
||||
let mut score = -99990.0;
|
||||
for (age, index) in (0..self.entries.len()).rev().enumerate() {
|
||||
let mut value = self.entries[index].score;
|
||||
if age < 1 {
|
||||
value -= 100.0;
|
||||
}
|
||||
if age > 5 {
|
||||
value -= 200.0;
|
||||
}
|
||||
if value > score {
|
||||
best = Some(index);
|
||||
score = value;
|
||||
}
|
||||
}
|
||||
best.map(|index| self.entries.remove(index))
|
||||
}
|
||||
|
||||
///82BFC9B0, used by automatic actor reset82592518. Manual/session-marker
|
||||
///selection82BFC828 has a different history walk and must not call this.
|
||||
pub fn automatic<V: Validation>(
|
||||
&mut self,
|
||||
stance: u32,
|
||||
scene: &mut V,
|
||||
) -> Result<Candidate, V::Error> {
|
||||
let mut transform = self.current_orientation;
|
||||
transform[3] = self.current_position;
|
||||
if let Some(ground) = scene.ground(&transform)? {
|
||||
if scene.location(&transform)?
|
||||
&& scene.occupants(&transform)?
|
||||
&& scene.edges(&transform)?
|
||||
{
|
||||
if ground.offboard {
|
||||
transform[3][1] = ground.position[1];
|
||||
}
|
||||
return Ok(Candidate {
|
||||
transform,
|
||||
stance,
|
||||
offboard: ground.offboard,
|
||||
score: 0.0,
|
||||
});
|
||||
}
|
||||
}
|
||||
while let Some(candidate) = self.pop_best() {
|
||||
if scene.location(&candidate.transform)? && scene.occupants(&candidate.transform)? {
|
||||
return Ok(candidate);
|
||||
}
|
||||
}
|
||||
//82BFC718 also serves non-destructive selectors; retain its actual rule.
|
||||
Ok(self.entries.last().copied().unwrap_or(Candidate {
|
||||
transform: self.initial.transform,
|
||||
stance,
|
||||
offboard: false,
|
||||
score: 0.0,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
///82BFB3F8 rejects these categories; category8 explicitly requests offboard.
|
||||
pub fn surface_allowed(category: u32) -> bool {
|
||||
!matches!(category, 5 | 6 | 9 | 12 | 13)
|
||||
}
|
||||
|
||||
///Jump table82BFB698: category1/2=100,3/4=50,11=20, all others=0.
|
||||
pub fn surface_score(category: u32) -> f32 {
|
||||
match category {
|
||||
1 | 2 => 100.0,
|
||||
3 | 4 => 50.0,
|
||||
11 => 20.0,
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn distance_squared(a: [f32; 4], b: [f32; 4]) -> f32 {
|
||||
(a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2) + (a[2] - b[2]).powi(2)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/respawn.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,126 @@
|
||||
use super::*;
|
||||
struct Scene {
|
||||
current: bool,
|
||||
rejected_x: Option<f32>,
|
||||
calls: Vec<&'static str>,
|
||||
}
|
||||
impl Validation for Scene {
|
||||
type Error = ();
|
||||
fn ground(&mut self, _: &Matrix) -> Result<Option<Ground>, ()> {
|
||||
self.calls.push("ground");
|
||||
Ok(self.current.then_some(Ground {
|
||||
position: [8., 2., 9., 0.],
|
||||
offboard: true,
|
||||
}))
|
||||
}
|
||||
fn location(&mut self, m: &Matrix) -> Result<bool, ()> {
|
||||
self.calls.push("location");
|
||||
Ok(self.rejected_x != Some(m[3][0]))
|
||||
}
|
||||
fn occupants(&mut self, _: &Matrix) -> Result<bool, ()> {
|
||||
self.calls.push("occupants");
|
||||
Ok(true)
|
||||
}
|
||||
fn edges(&mut self, _: &Matrix) -> Result<bool, ()> {
|
||||
self.calls.push("edges");
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
fn candidate(x: f32, score: f32) -> Candidate {
|
||||
let mut transform = IDENTITY;
|
||||
transform[3][0] = x;
|
||||
Candidate {
|
||||
transform,
|
||||
stance: 1,
|
||||
offboard: false,
|
||||
score,
|
||||
}
|
||||
}
|
||||
fn scene() -> Scene {
|
||||
Scene {
|
||||
current: false,
|
||||
rejected_x: None,
|
||||
calls: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scored_selection_consumes_entries_and_does_not_recheck_terrain() {
|
||||
let mut h = History::new(candidate(-10., 0.));
|
||||
h.insert(candidate(10., 100.));
|
||||
h.insert(candidate(20., 100.));
|
||||
let mut s = scene();
|
||||
assert_eq!(h.automatic(0, &mut s).unwrap().transform[3][0], 10.);
|
||||
assert_eq!(s.calls, ["ground", "location", "occupants"]);
|
||||
//Latest is now penalized; initial0 ties its adjusted100-100, newer wins.
|
||||
assert_eq!(h.automatic(0, &mut s).unwrap().transform[3][0], 20.);
|
||||
assert_eq!(h.automatic(0, &mut s).unwrap().transform[3][0], -10.);
|
||||
assert_eq!(h.automatic(0, &mut s).unwrap().transform[3][0], -10.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejection_removes_candidate_and_recomputes_age() {
|
||||
let mut h = History::new(candidate(-10., 0.));
|
||||
h.insert(candidate(10., 100.));
|
||||
h.insert(candidate(20., 50.));
|
||||
let mut s = scene();
|
||||
s.rejected_x = Some(10.);
|
||||
assert_eq!(h.automatic(1, &mut s).unwrap().transform[3][0], -10.);
|
||||
assert_eq!(h.entries.len(), 1);
|
||||
assert_eq!(h.entries[0].transform[3][0], 20.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_candidate_keeps_heading_and_horizontal_position_and_history() {
|
||||
let mut h = History::new(candidate(-10., 0.));
|
||||
h.current_position = [3., 4., 5., 0.];
|
||||
h.current_orientation[2] = [1., 0., 0., 0.];
|
||||
let mut s = scene();
|
||||
s.current = true;
|
||||
let c = h.automatic(7, &mut s).unwrap();
|
||||
assert_eq!(c.transform[3], [3., 2., 5., 0.]);
|
||||
assert_eq!(c.transform[2], [1., 0., 0., 0.]);
|
||||
assert_eq!((c.stance, c.offboard), (7, true));
|
||||
assert_eq!(h.entries.len(), 1);
|
||||
assert_eq!(s.calls, ["ground", "location", "occupants", "edges"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insertion_capacity_and_native_gate_boundaries() {
|
||||
let mut h = History::new(candidate(0., 0.));
|
||||
assert!(!h.recording_due(119, [10., 0., 0., 0.]));
|
||||
assert!(h.recording_due(120, [1.5, 0., 0., 0.]));
|
||||
assert!(!h.recording_due(120, [1.499, 0., 0., 0.]));
|
||||
h.insert(candidate(10., 0.));
|
||||
for _ in 0..20 {
|
||||
assert!(!h.recording_due(120, [1000., 0., 0., 0.]));
|
||||
}
|
||||
assert!(h.recording_due(120, [1000., 0., 0., 0.]));
|
||||
for n in 0..40 {
|
||||
h.insert(candidate(n as f32, 0.));
|
||||
}
|
||||
assert_eq!(h.entries.len(), 32);
|
||||
assert_eq!(h.entries[0].transform[3][0], 8.);
|
||||
assert_eq!(h.initial.transform[3][0], 0.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn category_dispatch_matches_decrementing_ppc_branch_table() {
|
||||
let accepted: Vec<_> = (0..=15).filter(|&x| surface_allowed(x)).collect();
|
||||
assert_eq!(accepted, [0, 1, 2, 3, 4, 7, 8, 10, 11, 14, 15]);
|
||||
assert_eq!(
|
||||
(0..=12).map(surface_score).collect::<Vec<_>>(),
|
||||
[0., 100., 100., 50., 50., 0., 0., 0., 0., 0., 0., 20., 0.]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exhausted_history_fallback_uses_current_actor_stance() {
|
||||
let mut h = History::new(candidate(-10., 100.));
|
||||
let mut s = scene();
|
||||
s.rejected_x = Some(-10.);
|
||||
let c = h.automatic(0, &mut s).unwrap();
|
||||
assert_eq!(c.transform, h.initial.transform);
|
||||
assert_eq!((c.stance, c.offboard, c.score), (0, false, 0.));
|
||||
assert!(h.entries.is_empty());
|
||||
}
|
||||
@@ -34,6 +34,11 @@ pub struct MotionAnimation {
|
||||
///Actual fullSkaterAnim15180 flags. AddBindPose consumes bits22/21 on
|
||||
///construction; the owner publishes the updated flags back to the actor.
|
||||
pub skater_animation_flags: Option<u32>,
|
||||
/// SkaterAnim15188/15196/15200 at the graph publication boundary.
|
||||
pub natural_stance: u32,
|
||||
pub relative_stance: u32,
|
||||
pub requested_stance: u32,
|
||||
pub reset_action_intents: bool,
|
||||
attribute_mirror: std::sync::Arc<skate_core::animation::playback_attributes::AttributeMirror>,
|
||||
settable: SettableAttributes,
|
||||
tree_attributes: Vec<AnimationAttribute>,
|
||||
@@ -72,7 +77,20 @@ impl MotionAnimation {
|
||||
self.motion_attributes.clear();
|
||||
self.tree_attributes.clear();
|
||||
self.settable.clear();
|
||||
self.skater_animation_flags = Some(0);
|
||||
//82B98050 preserves local-player and board-present flags, and saved15200.
|
||||
self.skater_animation_flags = self.skater_animation_flags.map(|f| f & 0x0ff7_ffff);
|
||||
self.relative_stance = 0;
|
||||
self.reset_action_intents = true;
|
||||
self.property = AdvanceResult { crossed_end: false, overshoot: -1.0, remaining_before_wrap: -1.0 };
|
||||
}
|
||||
pub fn reset_to_given_stance(&mut self) -> Result<(), String> {
|
||||
let flags = self.skater_animation_flags.as_mut().ok_or("Stance reset requires SkaterAnim flags")?;
|
||||
let mut relative = self.relative_stance != 0;
|
||||
skate_core::player::offboard::reset_animation::given_stance(
|
||||
self.natural_stance, &mut self.requested_stance, flags, &mut relative,
|
||||
);
|
||||
self.relative_stance = u32::from(relative);
|
||||
Ok(())
|
||||
}
|
||||
pub fn from_metadata(metadata: AnimationMetadata) -> Self {
|
||||
Self {
|
||||
@@ -88,6 +106,10 @@ impl MotionAnimation {
|
||||
posture: PendingPosture::default(),
|
||||
posture_bank_valid: false,
|
||||
skater_animation_flags: None,
|
||||
natural_stance: 1,
|
||||
relative_stance: 0,
|
||||
requested_stance: 0,
|
||||
reset_action_intents: false,
|
||||
attribute_mirror: std::sync::Arc::new(
|
||||
skate_core::animation::playback_attributes::AttributeMirror(Vec::new()),
|
||||
),
|
||||
|
||||
@@ -110,14 +110,25 @@ impl MotionHost {
|
||||
match operation {
|
||||
crate::graph_host::motion_reset::Operation::SkaterAnimation => {
|
||||
self.action_intents.clear();
|
||||
self.action_controls = Default::default();
|
||||
self.animation.motion_intents.clear();
|
||||
self.animation.filtered_intents.clear();
|
||||
//825953B0's represented selective MG output reset.
|
||||
self.flags = Default::default();
|
||||
self.is_power_sliding = false;
|
||||
self.riding.last_good_landing_velocity = 0.0;
|
||||
self.riding.manual_out_timer = 0.0;
|
||||
self.hand_services.busy_hands = [0; 2];
|
||||
self.gesture_publication = None;
|
||||
self.animation.reset_from_stock();
|
||||
}
|
||||
crate::graph_host::motion_reset::Operation::GivenStance => {
|
||||
self.animation.posture.set_requested(false);
|
||||
self.animation.reset_to_given_stance()?;
|
||||
}
|
||||
}
|
||||
//Later PlayAnimation nodes in this same traversal see the reset.
|
||||
self.playback_context.is_mirrored = self.animation.skater_animation_flags.map(|f| f & 0x4000_0000 != 0);
|
||||
self.playback_context.is_switch = Some(self.animation.relative_stance == 1);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -249,3 +249,29 @@ fn slide_direction_preserves_native_unordered_threshold_branch() {
|
||||
1.0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires installed stock graph assets; compile-only during bail work"]
|
||||
fn bail_reset_restores_saved_stance_in_the_real_graph_dispatch() {
|
||||
use crate::graph_host::motion_reset::Operation as Reset;
|
||||
let mut host = host();
|
||||
let reset = behavior(&host, |op| matches!(op, MotionOperation::ResetAnimation(Reset::SkaterAnimation)));
|
||||
let stance = behavior(&host, |op| matches!(op, MotionOperation::ResetAnimation(Reset::GivenStance)));
|
||||
for natural in [0, 1] {
|
||||
for requested in [0, 1] {
|
||||
host.animation.skater_animation_flags = Some(0xf80a_0000);
|
||||
host.animation.natural_stance = natural;
|
||||
host.animation.relative_stance = 1;
|
||||
host.animation.requested_stance = requested;
|
||||
host.begin(reset, [0; 6], &frame());
|
||||
assert_eq!(host.animation.requested_stance, requested);
|
||||
assert_eq!(host.animation.skater_animation_flags, Some(0x0802_0000));
|
||||
host.begin(stance, [0; 6], &frame());
|
||||
assert_eq!(host.animation.requested_stance, 0);
|
||||
let backwards = natural == requested;
|
||||
assert_eq!(host.playback_context.is_mirrored, Some(backwards));
|
||||
assert_eq!(host.animation.relative_stance, requested);
|
||||
assert!(host.errors.is_empty(), "{:?}", host.errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,7 @@ mod settings;
|
||||
mod wipeout;
|
||||
mod wipeout_states;
|
||||
mod teleport_state;
|
||||
mod respawn;
|
||||
mod offboard;
|
||||
mod known_air;
|
||||
use crate::{
|
||||
|
||||
@@ -76,6 +76,7 @@ pub(super) fn advance(
|
||||
skater.player_input.processed.surface_mode_2540,
|
||||
)?;
|
||||
if teleported {
|
||||
skater.respawn.reset_measurements();
|
||||
skater.player_state.reset_for_teleport();
|
||||
skater.centre_of_mass_filter.reset();
|
||||
skater.animation_feedback.reset();
|
||||
@@ -153,7 +154,9 @@ pub(super) fn advance(
|
||||
}
|
||||
skate_core::player::state::PhysicalStateId::Teleporting => {
|
||||
//VT823272FC slot8=82D431F0; slots12/40 are original empty leaves.
|
||||
skater.teleport_state.update(&skater.player_input.processed);
|
||||
if skater.teleport_state.update(&skater.player_input.processed) {
|
||||
super::respawn::request(physics, skater)?;
|
||||
}
|
||||
}
|
||||
state => return Err(format!("Selected {state:?} requires its physical update")),
|
||||
}
|
||||
@@ -189,6 +192,7 @@ pub(super) fn advance(
|
||||
physical.reckoning.vector_16.map(f32::from_bits),
|
||||
);
|
||||
let feedback = animation_phase::publish_feedback(physics, skater);
|
||||
super::respawn::observe(physics, skater)?;
|
||||
let deck = physics.board.bodies()[BodyId::Deck.index()];
|
||||
let rider = skater
|
||||
.skeleton
|
||||
|
||||
@@ -27,6 +27,8 @@ pub(crate) struct Runtime {
|
||||
pub candidate: Option<FiftyFiftyCandidate>,
|
||||
pub name: String,
|
||||
pub active: bool,
|
||||
///82D875A8 fallback result412 bit27: raw contact without accepted family.
|
||||
pub checkpoint_suppressed: bool,
|
||||
pub kind: u32,
|
||||
pub front_contact: bool,
|
||||
chromosome: chromosome::Chromosome,
|
||||
@@ -71,6 +73,7 @@ impl Runtime {
|
||||
candidate: None,
|
||||
name: String::new(),
|
||||
active: false,
|
||||
checkpoint_suppressed: false,
|
||||
kind: 0,
|
||||
front_contact: false,
|
||||
chromosome: chromosome::Chromosome::default(),
|
||||
@@ -192,6 +195,7 @@ pub(super) fn query(physics: &mut GamePhysics, skater: &mut SkaterRuntime) {
|
||||
let p = &mut skater.player_input.processed;
|
||||
let runtime = &mut physics.grind;
|
||||
runtime.entry_velocity = None;
|
||||
runtime.checkpoint_suppressed = false;
|
||||
let board = super::solve::deck_frame(&physics.board);
|
||||
let mut effective_board = board;
|
||||
if p.flags_2468 & 0x0010_0000 != 0 {
|
||||
@@ -258,15 +262,15 @@ pub(super) fn query(physics: &mut GamePhysics, skater: &mut SkaterRuntime) {
|
||||
&runtime.primitives,
|
||||
);
|
||||
let velocity = p.vectors_400_416[0].map(f32::from_bits);
|
||||
let slide = grind_contact::deck_contact(
|
||||
let deck_hit = grind_contact::deck_contact(
|
||||
board,
|
||||
p.flags_2484,
|
||||
runtime.test_depth_epsilon,
|
||||
runtime.test_depth,
|
||||
runtime.deck_to_truck,
|
||||
&edges,
|
||||
)
|
||||
.and_then(|mut hit| {
|
||||
);
|
||||
let slide = deck_hit.and_then(|mut hit| {
|
||||
hit.primitive = nearby[hit.primitive];
|
||||
grind_contact::boardslide_candidate(
|
||||
board,
|
||||
@@ -314,14 +318,14 @@ pub(super) fn query(physics: &mut GamePhysics, skater: &mut SkaterRuntime) {
|
||||
skater.animation_input.fields.balance,
|
||||
p.vectors_464_480_496_512_528[0].map(f32::from_bits),
|
||||
);
|
||||
let dark = families::inverted_contact(
|
||||
let inverted_hit = families::inverted_contact(
|
||||
board,
|
||||
p.flags_2484,
|
||||
runtime.test_depth_epsilon,
|
||||
runtime.test_depth,
|
||||
&edges,
|
||||
)
|
||||
.and_then(|mut hit| {
|
||||
);
|
||||
let dark = inverted_hit.and_then(|mut hit| {
|
||||
hit.primitive = nearby[hit.primitive];
|
||||
families::darkslide(
|
||||
board,
|
||||
@@ -355,6 +359,9 @@ pub(super) fn query(physics: &mut GamePhysics, skater: &mut SkaterRuntime) {
|
||||
None
|
||||
};
|
||||
let selected = current.or(fifty).or(five).or(tip).or(slide).or(dark);
|
||||
runtime.checkpoint_suppressed = (gated || selected.is_none())
|
||||
&& (hits.iter().any(Option::is_some) || deck_hit.is_some()
|
||||
|| inverted_hit.is_some() || tip_hits.iter().any(Option::is_some));
|
||||
let candidate = selected.map(|c| c.geometry);
|
||||
let mut kind = selected.map_or(0, |c| c.kind);
|
||||
runtime.front_contact = selected.is_some_and(|c| c.front);
|
||||
@@ -396,6 +403,7 @@ pub(super) fn query(physics: &mut GamePhysics, skater: &mut SkaterRuntime) {
|
||||
if p.category_2512 != 200
|
||||
&& !grind_contact::within_approach_angle(candidate.direction, velocity, angle)
|
||||
{
|
||||
runtime.checkpoint_suppressed = true;
|
||||
return;
|
||||
}
|
||||
runtime.candidate = Some(candidate);
|
||||
|
||||
@@ -118,7 +118,7 @@ pub(crate) fn submit(
|
||||
fn xyz(v: [f32; 4]) -> Vector3 {
|
||||
Vector3::new(v[0], v[1], v[2])
|
||||
}
|
||||
fn query(world: &BoardWorld, probe: Probe, matching_id: u32) -> Result<Option<Hit>, String> {
|
||||
pub(crate) fn query(world: &BoardWorld, probe: Probe, matching_id: u32) -> Result<Option<Hit>, String> {
|
||||
let metadata = world.query_metadata().map_err(str::to_owned)?;
|
||||
let start = xyz(probe.start);
|
||||
let end = xyz(probe.end);
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
//! TU3 actor checkpoint manager, bound to the single-player static scene.
|
||||
use super::{GamePhysics, SkaterRuntime, offboard::contact_queries, teleport_state::Checkpoint};
|
||||
use skate_core::{
|
||||
math::Vector3,
|
||||
physics::{
|
||||
board_world::{BoardWorld, query_metadata::Bounds},
|
||||
skeleton_animation_record::AnimationPartTransform as Matrix,
|
||||
},
|
||||
player::{
|
||||
offboard::contact_queries::Probe,
|
||||
respawn::{Candidate, Ground, History, Observation, Validation, surface_allowed},
|
||||
},
|
||||
};
|
||||
use skate_data::collections::Collections;
|
||||
|
||||
pub(super) struct Runtime {
|
||||
history: History,
|
||||
settings: Settings,
|
||||
measurements: i32,
|
||||
}
|
||||
struct Settings {
|
||||
height: f32,
|
||||
radius: f32,
|
||||
drop: f32,
|
||||
normal_y: f32,
|
||||
minimum_frames: u32,
|
||||
}
|
||||
impl Runtime {
|
||||
pub fn load(data: &Collections, transform: Matrix, stance: u32) -> Result<Self, String> {
|
||||
let class = "Hash_12B64C0E804B0853";
|
||||
let value = |hash| data.float(class, "default", hash);
|
||||
Ok(Self {
|
||||
history: History::new(Candidate {
|
||||
transform,
|
||||
stance,
|
||||
offboard: false,
|
||||
score: 0.,
|
||||
}),
|
||||
measurements: 0,
|
||||
settings: Settings {
|
||||
height: value("Hash_C0526C883AF0ECCA")?,
|
||||
radius: value("Hash_CEB092E418A5B001")?,
|
||||
drop: value("Hash_8ABE098D3806D273")?,
|
||||
normal_y: value("Hash_ADD032CACF6A1C15")?,
|
||||
minimum_frames: data.integer(class, "default", "Hash_10B7C3A9CC8D3721")? as u32,
|
||||
},
|
||||
})
|
||||
}
|
||||
pub fn reset_measurements(&mut self) {
|
||||
self.measurements = 0;
|
||||
}
|
||||
}
|
||||
|
||||
///Actor82592518 saves stance then queues the ordinary deferred reply825926F8.
|
||||
pub(super) fn request(physics: &GamePhysics, skater: &mut SkaterRuntime) -> Result<(), String> {
|
||||
let stance = skater.animation.checkpoint_stance();
|
||||
let runtime = &mut skater.respawn;
|
||||
let candidate = runtime.history.automatic(
|
||||
stance,
|
||||
&mut Scene {
|
||||
world: &physics.world,
|
||||
settings: &runtime.settings,
|
||||
},
|
||||
)?;
|
||||
skater.animation.request_checkpoint_stance(candidate.stance);
|
||||
skater.teleport_state.reply(Checkpoint {
|
||||
transform: candidate.transform,
|
||||
on_board: !candidate.offboard,
|
||||
});
|
||||
bevy::log::info!(
|
||||
"BAIL_CHECKPOINT position={:?} heading={:?} stance={} offboard={} score={}",
|
||||
candidate.transform[3],
|
||||
candidate.transform[2],
|
||||
candidate.stance,
|
||||
candidate.offboard,
|
||||
candidate.score
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///One completed physical output, including the empirical conditioner sample.
|
||||
pub(super) fn observe(physics: &GamePhysics, skater: &mut SkaterRuntime) -> Result<(), String> {
|
||||
let physical = &skater.player_input.physical;
|
||||
let processed = &skater.player_input.processed;
|
||||
//82592A00 -> SkateboardReckoning64 ->82C01BF8: solved deck, with bit20 flip.
|
||||
let mut deck = super::solve::deck_frame(&physics.board);
|
||||
if processed.flags_2468 & 0x0010_0000 != 0 {
|
||||
flip(&mut deck);
|
||||
}
|
||||
deck[3][1] += 0.2;
|
||||
let mut offboard = skater.animated_skeleton.roots.animation_to_world;
|
||||
if processed.flags_2476 & 4 != 0 {
|
||||
flip(&mut offboard);
|
||||
}
|
||||
let velocity = physical.skateboard.vector_80.map(f32::from_bits);
|
||||
let stance = skater.animation.checkpoint_stance();
|
||||
let runtime = &mut skater.respawn;
|
||||
runtime.measurements = runtime.measurements.wrapping_add(1);
|
||||
let observation = Observation {
|
||||
measurements: runtime.measurements,
|
||||
root_position: skater.animated_skeleton.roots.animation_to_world[3],
|
||||
com_position: physical.reckoning.vector_64.map(f32::from_bits),
|
||||
teleport_requested: physical.state.flag_69 != 0,
|
||||
physical_state: physical.state.state_16,
|
||||
state_frames: skater.player_state.state_count,
|
||||
ground_suppressed: physics.grind.checkpoint_suppressed,
|
||||
offboard_correction: skater.offboard.controller.state.contact.active,
|
||||
ground_category: super::ground_runtime::active_surface(&physics.riding, &physics.board)
|
||||
& 0xffff,
|
||||
//82DB6EC0 publishes the same processed foot record to both fields.
|
||||
foot_categories: [(processed.left_surface_2596 >> 7) & 31; 2],
|
||||
riding_transform: heading(deck, velocity),
|
||||
//82591E30 returns immediately for offboard, before the velocity branch.
|
||||
offboard_transform: offboard,
|
||||
stance,
|
||||
//No alternate-world/challenge controller exists in this local scene.
|
||||
alternate_world: false,
|
||||
};
|
||||
runtime.history.observe(
|
||||
&observation,
|
||||
runtime.settings.minimum_frames,
|
||||
&mut Scene {
|
||||
world: &physics.world,
|
||||
settings: &runtime.settings,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn flip(matrix: &mut Matrix) {
|
||||
for i in [0, 2] {
|
||||
matrix[i] = matrix[i].map(|v| -v);
|
||||
}
|
||||
}
|
||||
///82591E30 retains the source axes at low speed or near vertical travel.
|
||||
fn heading(mut matrix: Matrix, velocity: [f32; 4]) -> Matrix {
|
||||
let square = velocity[0] * velocity[0] + velocity[1] * velocity[1] + velocity[2] * velocity[2];
|
||||
if square > 0.25 {
|
||||
let inv = square.sqrt().recip();
|
||||
let forward = [velocity[0] * inv, 0., velocity[2] * inv, 0.];
|
||||
if forward[0] * forward[0] + forward[2] * forward[2] > 0.9 {
|
||||
matrix[0] = [forward[2], 0., -forward[0], 0.];
|
||||
matrix[1] = [0., 1., 0., 0.];
|
||||
matrix[2] = forward;
|
||||
}
|
||||
}
|
||||
matrix
|
||||
}
|
||||
|
||||
struct Scene<'a> {
|
||||
world: &'a BoardWorld,
|
||||
settings: &'a Settings,
|
||||
}
|
||||
impl Validation for Scene<'_> {
|
||||
type Error = String;
|
||||
fn ground(&mut self, transform: &Matrix) -> Result<Option<Ground>, String> {
|
||||
let mut start = transform[3];
|
||||
start[1] += 0.1;
|
||||
let mut end = start;
|
||||
end[1] -= 10.;
|
||||
let probe = |start, end, radius| Probe {
|
||||
start,
|
||||
end,
|
||||
radius,
|
||||
id: 0,
|
||||
reverse_id: None,
|
||||
};
|
||||
//The only actor has matching identity0; canonical map groups remain active.
|
||||
let Some(hit) = contact_queries::query(self.world, probe(start, end, 0.), 0)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let category = (u32::from(hit.packed_surface) >> 7) & 31;
|
||||
if hit.geometry.normal.y < self.settings.normal_y
|
||||
|| start[1] - hit.geometry.position.y > self.settings.drop
|
||||
|| !surface_allowed(category)
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
let mut bottom = start;
|
||||
bottom[1] += self.settings.radius;
|
||||
let mut top = bottom;
|
||||
top[1] += self.settings.height;
|
||||
if contact_queries::query(self.world, probe(bottom, top, self.settings.radius), 0)?
|
||||
.is_some()
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
let p = hit.geometry.position;
|
||||
Ok(Some(Ground {
|
||||
position: [p.x, p.y, p.z, 0.],
|
||||
offboard: category == 8,
|
||||
}))
|
||||
}
|
||||
fn location(&mut self, _: &Matrix) -> Result<bool, String> {
|
||||
//82BFBB48 returns true in normal-world mode, before provider invocation.
|
||||
Ok(true)
|
||||
}
|
||||
fn occupants(&mut self, _: &Matrix) -> Result<bool, String> {
|
||||
//82BFB928 queries LivingWorldManager pedestrians/vehicles, not terrain.
|
||||
//This BoardWorld has no living-world actors. Static obstacles are checked
|
||||
//by ground's capsule; the player and board must not reject themselves.
|
||||
self.world.query_metadata().map_err(str::to_owned)?;
|
||||
Ok(true)
|
||||
}
|
||||
fn edges(&mut self, transform: &Matrix) -> Result<bool, String> {
|
||||
let p = transform[3];
|
||||
let bounds = Bounds {
|
||||
min: Vector3::new(p[0] - 0.3, p[1] - 0.6, p[2] - 0.3),
|
||||
max: Vector3::new(p[0] + 0.3, p[1] + 0.6, p[2] + 0.3),
|
||||
};
|
||||
let metadata = self.world.query_metadata().map_err(str::to_owned)?;
|
||||
//82C1EAD8: authored static order, shared capacity40; no triangle diagonals.
|
||||
for edge in metadata
|
||||
.static_edges
|
||||
.iter()
|
||||
.filter(|e| e.local_bounds.overlaps(bounds))
|
||||
.take(40)
|
||||
{
|
||||
let start = [edge.start.x, edge.start.y, edge.start.z];
|
||||
let end = [edge.end.x, edge.end.y, edge.end.z];
|
||||
let delta: [f32; 3] = std::array::from_fn(|i| end[i] - start[i]);
|
||||
let square: f32 = delta.iter().map(|v| v * v).sum();
|
||||
let along: f32 = (0..3).map(|i| (p[i] - start[i]) * delta[i]).sum();
|
||||
let fraction = if square > 0. {
|
||||
(along / square).clamp(0., 1.)
|
||||
} else {
|
||||
0.
|
||||
};
|
||||
let distance: f32 = (0..3)
|
||||
.map(|i| (p[i] - start[i] - delta[i] * fraction).powi(2))
|
||||
.sum();
|
||||
if distance < 0.09 {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use skate_core::physics::{
|
||||
contact::RetailContactMaterial, skeleton_animation_record::IDENTITY,
|
||||
};
|
||||
#[test]
|
||||
fn real_scene_records_a_checkpoint_and_recovers_it_from_an_unsupported_position() {
|
||||
let world = super::super::ground::world(RetailContactMaterial {
|
||||
static_friction: 0.,
|
||||
dynamic_friction: 0.,
|
||||
restitution: 0.,
|
||||
});
|
||||
let settings = Settings {
|
||||
height: 0.4,
|
||||
radius: 0.5,
|
||||
drop: 1.,
|
||||
normal_y: 0.75,
|
||||
minimum_frames: 15,
|
||||
};
|
||||
let mut scene = Scene {
|
||||
world: &world,
|
||||
settings: &settings,
|
||||
};
|
||||
let mut history = History::new(Candidate {
|
||||
transform: IDENTITY,
|
||||
stance: 0,
|
||||
offboard: false,
|
||||
score: 0.,
|
||||
});
|
||||
//The first conditioning samples clear the construction cooldown.
|
||||
assert!(!history.recording_due(119, [2., 0., 0., 0.]));
|
||||
let mut transform = IDENTITY;
|
||||
transform[3] = [2., 0.2, 0., 0.];
|
||||
history
|
||||
.observe(
|
||||
&Observation {
|
||||
measurements: 120,
|
||||
root_position: transform[3],
|
||||
com_position: [1000., 0., 0., 0.],
|
||||
teleport_requested: false,
|
||||
physical_state: 100,
|
||||
state_frames: 16,
|
||||
ground_suppressed: false,
|
||||
offboard_correction: false,
|
||||
ground_category: 1,
|
||||
foot_categories: [1; 2],
|
||||
riding_transform: transform,
|
||||
offboard_transform: IDENTITY,
|
||||
stance: 1,
|
||||
alternate_world: false,
|
||||
},
|
||||
15,
|
||||
&mut scene,
|
||||
)
|
||||
.unwrap();
|
||||
let selected = history.automatic(0, &mut scene).unwrap();
|
||||
assert_eq!(selected.transform, transform);
|
||||
assert_eq!(selected.stance, 1);
|
||||
//Successful historical entries are consumed: a second failure uses spawn.
|
||||
assert_eq!(
|
||||
history.automatic(0, &mut scene).unwrap().transform,
|
||||
IDENTITY
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn native_heading_keeps_low_speed_and_vertical_axes() {
|
||||
let mut source = IDENTITY;
|
||||
source[0] = [0., 0., -1., 0.];
|
||||
source[2] = [1., 0., 0., 0.];
|
||||
assert_eq!(heading(source, [0., 0., 0.5, 0.]), source);
|
||||
assert_eq!(heading(source, [0., 5., 0.1, 0.]), source);
|
||||
let result = heading(source, [0., 0., 2., 0.]);
|
||||
assert_eq!(result[2], [0., 0., 1., 0.]);
|
||||
assert_eq!(result[3], source[3]);
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,7 @@ pub(crate) struct SkaterRuntime {
|
||||
pub footplant: super::footplant::Footplant,
|
||||
pub wipeout: super::wipeout::Wipeout,
|
||||
pub wipeout_state: super::wipeout_states::WipeoutState,
|
||||
pub respawn: super::respawn::Runtime,
|
||||
pub teleport_state: super::teleport_state::Runtime,
|
||||
pub offboard: super::offboard::runtime::Runtime,
|
||||
pub skeleton: SkeletonBody,
|
||||
@@ -190,7 +191,9 @@ impl SkaterRuntime {
|
||||
let wipeout_state = super::wipeout_states::WipeoutState::load(
|
||||
&data, asset_root, &animation.evaluator.frames.source_sha256,
|
||||
)?;
|
||||
let respawn = super::respawn::Runtime::load(&data, spawn, animation.checkpoint_stance())?;
|
||||
Ok(Self {
|
||||
respawn,
|
||||
climbing: super::climbing::Runtime::load(asset_root, &animation.evaluator.frames.bone_names)?,
|
||||
offboard,
|
||||
render_pose: initial_hierarchy,
|
||||
|
||||
@@ -36,20 +36,20 @@ impl Runtime {
|
||||
}
|
||||
/// Update82D431F0 calls the actor only without Processed2468 bit1.
|
||||
///825926F8 stores the reply and marks it for the next input publication.
|
||||
pub fn update(&mut self, input: &ProcessedPhysicsInput) {
|
||||
if self
|
||||
pub fn update(&mut self, input: &ProcessedPhysicsInput) -> bool {
|
||||
self
|
||||
.state
|
||||
.update(input.flags_2468, input.matrix_1536, input.byte_1600)
|
||||
== Update::RequestCheckpoint
|
||||
{
|
||||
self.request_checkpoint();
|
||||
}
|
||||
}
|
||||
/// Host response for an independently verified actor-reset request.
|
||||
pub fn request_checkpoint(&mut self) {
|
||||
self.reply(self.checkpoint);
|
||||
}
|
||||
pub fn reply(&mut self, checkpoint: Checkpoint) {
|
||||
self.pending_reply = Some(Target {
|
||||
transform: self.checkpoint.transform.map(|row| row.map(f32::to_bits)),
|
||||
on_board: self.checkpoint.on_board,
|
||||
transform: checkpoint.transform.map(|row| row.map(f32::to_bits)),
|
||||
on_board: checkpoint.on_board,
|
||||
});
|
||||
}
|
||||
/// Consume at the next actor-reset publication boundary. Publish this as
|
||||
|
||||
@@ -77,6 +77,17 @@ pub(crate) struct SkaterAnimation {
|
||||
}
|
||||
|
||||
impl SkaterAnimation {
|
||||
///82592B68: physical leading foot, including the fakie inversion.
|
||||
pub fn checkpoint_stance(&self) -> u32 {
|
||||
let p = &self.state.publication;
|
||||
u32::from(((p.natural_stance == 0 && p.relative_stance == 0)
|
||||
|| (p.natural_stance == 1 && p.relative_stance == 1)) ^ self.state.fakie())
|
||||
}
|
||||
///82592C08/82B97350: save15200; ResetToGivenStance consumes it later.
|
||||
pub fn request_checkpoint_stance(&mut self, foot: u32) {
|
||||
let natural = self.state.publication.natural_stance;
|
||||
self.motion.animation.requested_stance = u32::from(if foot == 0 { natural != 1 } else { natural == 1 });
|
||||
}
|
||||
pub fn stance(&self) -> (bool, bool) {
|
||||
(self.state.fakie(), self.state.mirrored())
|
||||
}
|
||||
@@ -211,6 +222,10 @@ impl SkaterAnimation {
|
||||
.animation
|
||||
.skater_animation_flags
|
||||
.ok_or("MotionGraph lost the SkaterAnim flag owner")?;
|
||||
self.state.publication.relative_stance = self.motion.animation.relative_stance as i32;
|
||||
if std::mem::take(&mut self.motion.animation.reset_action_intents) {
|
||||
self.action.action_intents.clear();
|
||||
}
|
||||
|
||||
self.motion.animation.apply_parameters()?;
|
||||
if let Some(phase) = self.motion.phase_write.take() {
|
||||
@@ -261,6 +276,8 @@ impl SkaterAnimation {
|
||||
}
|
||||
|
||||
fn publish_physical(&mut self, p: AnimationPhysical) -> Result<(), String> {
|
||||
self.motion.animation.natural_stance = self.state.publication.natural_stance as u32;
|
||||
self.motion.animation.relative_stance = self.state.publication.relative_stance as u32;
|
||||
let speed = p
|
||||
.conditions
|
||||
.speeds
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
# Native bail recovery
|
||||
|
||||
Automatic bail recovery now uses the TU3 actor checkpoint manager instead of
|
||||
always replying with the map's initial spawn. The integration targets the
|
||||
engine's existing single-player, static-map world. Gameplay validation is
|
||||
reserved for the user; no game, recomp, controller harness, screenshot capture,
|
||||
asset-check mode or test executable was launched during this work.
|
||||
|
||||
## Behavior
|
||||
|
||||
The actor first validates its current physical COM position using the retained
|
||||
riding heading. If that fails, it ranks recorded checkpoints and consumes the
|
||||
selected entry. Entries rejected by validation are also removed, so repeated
|
||||
bails can walk back through history. Exhaustion uses the initial map transform
|
||||
and current actor stance. Ordinary recovery preserves history; constructing a
|
||||
new skater for a map constructs a new history.
|
||||
|
||||
Recording uses completed physical publications, not render transforms or a
|
||||
last-grounded shortcut. It requires 120 empirical measurements, more than the
|
||||
stock 15 frames in an eligible state, expired 20-update cooldown, and at least
|
||||
1.5 units from every recorded position. The ring retains 32 entries. Surface
|
||||
categories 5, 6, 9, 12 and 13 are excluded; category 8 requests offboard recovery.
|
||||
Scores are 100 for categories 1/2, 50 for 3/4, 20 for 11 and zero otherwise. The
|
||||
newest entry receives a 100-point penalty and entries older than five receive a
|
||||
200-point penalty; newest wins ties.
|
||||
|
||||
The selected position, heading, stance and offboard byte go through the existing
|
||||
State702 request/reply publication boundary. Selection does not reset physical
|
||||
bodies. Existing wipeout timers, recovery countdown, physical reset and ragdoll
|
||||
restoration remain in place. The explicit fixed-checkpoint helper used by the
|
||||
existing manual test remains separate from automatic selection.
|
||||
|
||||
## Native evidence
|
||||
|
||||
Reference memory image `default_82000000_011B0000.bin`, mapped at 0x82000000:
|
||||
SHA256 `f4aa113eb541bfba03dbc108cf5ab43f58c965b20fa3b82f9c40938a0ad841c4`.
|
||||
Instruction comments in the matching generated PPC were inspected; numeric
|
||||
constants were checked in the raw image. A read-only IDA lookup found no defined
|
||||
function for 82BFB1E0 in the available dump database, so decompiler output was
|
||||
not used as independent confirmation. This is a scalar translation, not a
|
||||
claim of demonstrated bit-exact PPC floating-point equivalence.
|
||||
|
||||
| Address | Recovered behavior |
|
||||
| --- | --- |
|
||||
| 82BFB068 / 82BFCB38 | History construction and insertion |
|
||||
| 82BFB1E0 / 82BFB3F8 | Recording gates, current orientation and COM publication |
|
||||
| 82BFC9B0 | Current -> scored history -> fallback selector |
|
||||
| 82BFC038 / 82BFC378 / 82BFC550 / 82BFC718 | Current candidate, history validation, scoring/removal, fallback |
|
||||
| 82BFC828 | Separate non-destructive manual/session-marker query; not used here |
|
||||
| 82592518 / 825926F8 | Save selected stance, queue deferred actor reset reply |
|
||||
| 82D431F0 / 82D43280 | State702 reply capture and physical output |
|
||||
| 82591E30 / 82592A00 / 82C01BF8 | Recording transform and solved deck source |
|
||||
| 82BFBC18 / 82BFB6F8 | Ground/capsule and authored-edge validation |
|
||||
| 82DE5858 / 82DE5588 | Empirical measurement count and reset lifecycle |
|
||||
| 82B97350 / 82B97308 / 82B972A8 | Save15200 and deferred stance restoration |
|
||||
| 82B98050 / 825953B0 | Selective animation and motion-output reset |
|
||||
|
||||
## Host bindings
|
||||
|
||||
Riding checkpoints use the solved board frame, with Processed2468 bit20
|
||||
inverting X/Z, plus 0.2 Y. Above speed squared 0.25, a velocity-derived upright
|
||||
basis is accepted only when projected forward length squared exceeds 0.9.
|
||||
Offboard checkpoints use the animation-to-world frame with Processed2476 bit2
|
||||
inverting X/Z; the native function returns before the velocity-heading branch.
|
||||
|
||||
Current position is Skeleton64 (physical COM16144); spacing uses Skeleton416,
|
||||
the unmirrored animation-to-world translation. OffBoard333 comes from the
|
||||
canonical contact-correction owner. Both native OffBoard52/56 fields receive
|
||||
Processed2596; their category is packed bits7..11. State age comes from the
|
||||
physical state publication. The actor-owned empirical count increments once per
|
||||
completed physical output and resets with physical teleport, retaining history.
|
||||
|
||||
Ground323 is GrindManager460/result412 bit27. The producer was recovered in
|
||||
82D875A8: after accepted-family attempts fail, the raw truck/deck/inverted/tip
|
||||
contact path at 82D87DE8 sets 0x08000000. The host retains those same raw query
|
||||
results and publishes the rejection flag separately from active grind state.
|
||||
No new rail distance or last-grounded heuristic is used.
|
||||
|
||||
Ground validation casts from candidate +0.1 Y down ten units, checks normal Y,
|
||||
maximum drop and category, then tests a vertical capsule above the ray start.
|
||||
It reuses the canonical triangle query and matching groups. Edge validation
|
||||
uses authored static edges in provider order, capacity40, with bounds
|
||||
[0.3,0.6,0.3] and squared segment distance threshold0.09. It does not use triangle
|
||||
diagonals. Current candidates repeat all predicates; historical candidates
|
||||
repeat location/occupant checks only, matching native selection.
|
||||
|
||||
Normal-world location validation returns true before invoking the alternate
|
||||
provider. Occupancy predicates in 82BFB928 belong to LivingWorldManager, not
|
||||
static terrain. This engine scene has no pedestrian/vehicle actors or alternate
|
||||
challenge controller, so those providers are empty; static obstacles remain
|
||||
covered by the capsule. Adding living-world actors or alternate-world maps will
|
||||
require binding their providers here, as with the existing offboard adapters.
|
||||
|
||||
Saved stance survives ResetSkaterAnimation and is consumed by
|
||||
ResetToGivenStance at the actual MotionGraph dispatch. Updated flags/relative
|
||||
stance are visible to later nodes in the same traversal, including TELEPORT
|
||||
playback. The canonical actor publication then receives the modified values.
|
||||
The animation reset now preserves the native local-player/board flags and saved
|
||||
request rather than clearing all flags. The stock `teleport.xml` confirms the
|
||||
ordered ResetSkaterAnimation -> ResetToGivenStance -> PlayAnimation sequence.
|
||||
|
||||
## Stock data
|
||||
|
||||
Class `Hash_12B64C0E804B0853`, key `default`, already exists in installed data.
|
||||
No new tuning file or fallback tuning values were introduced.
|
||||
|
||||
| Field hash | Value | Meaning |
|
||||
| --- | --- | --- |
|
||||
| C0526C883AF0ECCA | 0.4 | Capsule height |
|
||||
| CEB092E418A5B001 | 0.5 | Capsule radius |
|
||||
| 8ABE098D3806D273 | 1.0 | Maximum ray drop |
|
||||
| ADD032CACF6A1C15 | 0.75 | Minimum normal Y |
|
||||
| 10B7C3A9CC8D3721 | 15 (Int32) | Minimum state age |
|
||||
|
||||
Native occupant-query radii E64C980EE2114070 and 59E1F2B3CDC4E658 are 1 and 2;
|
||||
the current empty living-world provider does not consume them.
|
||||
Source skatercollections.vlt SHA256:
|
||||
`3b7dbd062bb1c906a085514355afff35cfa22f486ae70820c5ad1a42a7aab25b`.
|
||||
|
||||
## Validation and user testing
|
||||
|
||||
Release compilation uses the requested x86_64-pc-windows-msvc static CRT flags,
|
||||
locked dependencies, and no default game features. The six core selector cases
|
||||
compile with --no-run; they have not been executed. Added host cases cover real
|
||||
scene recording/recovery, heading thresholds and the actual stock reset dispatch.
|
||||
The whole game test target cannot currently compile because pre-existing tests
|
||||
in graphics_menu.rs omit Menu.maps/selected_map, and skate_world.rs omits the
|
||||
RetailWorldMaterial argument. Those unrelated map/render fixtures were not edited.
|
||||
|
||||
The final executable is copied into this task's ignored bin/native-bail-recovery
|
||||
directory; Test-Native-Bail-Recovery.bat uses that exact copy and installed assets.
|
||||
It has not been executed. Its sibling gameplay.log records passive BAIL_CHECKPOINT
|
||||
selection messages. Test riding away from spawn for several seconds, bailing on
|
||||
open ground and near an edge, repeated bails, both stances, and offboard recovery.
|
||||
Gameplay parity and visual behavior remain unverified until the user runs it.
|
||||
Reference in new issue
Block a user