Connect native stalled grind release phase and outward exit forces

This commit is contained in:
chazmm committed 2026-09-07 22:03:00 +01:00
1 parent c8a9a7caa2
commit e0b119d729
3 files changed
+111

No files matched your search

@@ -164,3 +164,41 @@ fn cross(a: V, b: V) -> V {
0.0,
]
}
/// Grind PreUpdate82D3F4E8 / exit decision82D40DA0. Counts are native
/// simulation updates, not render frames. Once leaving, the phase is latched.
#[derive(Default)]
pub struct Release {
updates: u32,
leaving_updates: u32,
pub leaving: bool,
}
impl Release {
pub fn begin_update(&mut self, speed: f32, explicit_exit: bool) {
self.leaving |= explicit_exit || (self.updates > 60 && speed < 0.15);
}
pub fn finish_update(&mut self) -> bool {
self.leaving_updates = if self.leaving { self.leaving_updates.wrapping_add(1) } else { 0 };
self.updates = self.updates.wrapping_add(1);
self.leaving_updates > 35
}
}
///82D3F850: push away from the contacted edge until the native lateral
///speed limit is reached. Ledge engagement may supply its outward direction.
pub fn release_force(
position: V, point: V, direction: V, normal: V, velocity: V,
surface_kind: u32, surface_side: V, force_across: bool,
strength: f32, speed_limit: f32, lift: f32,
) -> V {
let outward = if !force_across && surface_kind != 0 {
scale(surface_side, -1.0)
} else {
let across = cross(direction, normal);
scale(across, if dot3(across, sub(position, point)) > 0.0 { 1.0 } else { -1.0 })
};
if dot3(velocity, outward) >= speed_limit { return [0.0; 4]; }
let lateral = scale(outward, strength);
let upward = normal[1].max(0.0) * lift;
core::array::from_fn(|i| normal[i].mul_add(upward, lateral[i]))
}
+42
View File
@@ -37,6 +37,7 @@ pub(crate) struct Runtime {
test_depth_epsilon: f32,
test_depth: f32,
exiting: bool,
release: grind_forces::Release,
surface_kind: u32,
surface_normal: V,
surface_side: V,
@@ -80,6 +81,7 @@ impl Runtime {
test_depth_epsilon: data.float("physics_grinds", "default", "TestDepthEpsilon")?,
test_depth: data.float("physics_grinds", "default", "TestDepth")?,
exiting: false,
release: grind_forces::Release::default(),
surface_kind: 0,
surface_normal: [0.0, 1.0, 0.0, 0.0],
surface_side: [0.0; 4],
@@ -492,6 +494,7 @@ pub(super) fn enter(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> Re
| PhysicalStateId::GrindDarkslide
);
physics.grind.exiting = false;
physics.grind.release = grind_forces::Release::default();
physics.grind.distance = 0.0;
physics.grind.previous_position = super::solve::deck_frame(&physics.board)[3];
skater.ground_lifecycle.skeleton_elapsed_16505 = true;
@@ -530,6 +533,20 @@ pub(super) fn update(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> R
physics.grind.launched = false;
let board = super::solve::deck_frame(&physics.board);
let p = &skater.player_input.processed;
if physics.grind.active {
//82D40D90; Boardslide82D41788 only exposes this exit on kind2.
let explicit = p.flags_2476 & 0x2000_0000 != 0
&& (!matches!(physics.grind.kind, 1 | 5) || physics.grind.surface_kind == 2);
let was_leaving = physics.grind.release.leaving;
physics.grind.release.begin_update(p.scalar_2652, explicit);
if !was_leaving && physics.grind.release.leaving {
bevy::log::info!("GRIND_RELEASE kind={} speed={} explicit={}", physics.grind.kind, p.scalar_2652, explicit);
}
if physics.grind.release.finish_update() {
//82D3F638: byte37/value124, using the retained request owner.
skater.wipeout.state.request(17, 0.0);
}
}
if physics.grind.active && p.flags_2468 & 0x00400000 != 0 {
let c = physics
.grind
@@ -604,6 +621,31 @@ pub(super) fn update(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> R
grind_contact::upright_normal(c.direction)
};
physics.grind.crouch = dot3(board[2], normal).abs() * 0.4;
if physics.grind.release.leaving {
let surface = physics.grind.surface_kind;
let (strength, limit, lift, force_across) = match physics.grind.kind {
//50-50 exit82D41DF0, 5-O exit82D428C0.
0 => if surface < 2 { (100.0, 1.0, 125.0, false) } else { (10.0, 1.0, 0.0, false) },
3 => if surface < 2 { (120.0, 0.9, 125.0, false) } else { (10.0, 0.9, 0.0, false) },
//Static rail Tipslide82D420A0 and Backslash82D42A80.
2 => (100.0, 1.5, 0.0, true),
4 => (100.0, 0.75, if surface == 1 { 125.0 } else { 0.0 }, surface == 1),
//Boardslide82D41250 enters the existing runout/wipeout path
//on rail/ledge engagement; kind2 uses a physical side force.
_ => (90.0, 0.75, 0.0, false),
};
if matches!(physics.grind.kind, 1 | 5) && surface != 2 {
skater.wipeout.state.request(10, 0.0);
} else {
append(physics, grind_forces::release_force(
board[3], c.centre, c.direction, normal, velocity, surface,
physics.grind.surface_side, force_across, strength, limit, lift,
))?;
}
physics.riding.update_grind_reckoning(
&mut skater.air_reckoning.state, normal, board[2], p.flags_2468, 0.9);
return super::input_phase::update_ground(physics, skater);
}
let across = cross(c.direction, normal);
physics.grind.control.update(
physics.grind.kind,
+31
View File
@@ -0,0 +1,31 @@
# Grind release at low speed and rail ends
TU3 reference: default_82000000_011B0000.bin, base82000000,
SHA256 f4aa113eb541bfba03dbc108cf5ab43f58c965b20fa3b82f9c40938a0ad841c4.
Static generated instruction extracts are under logs/biped-<address>.txt.
Observed: PreUpdate82D3F4E8 owns a latched leaving phase. Decision82D40DA0
enters it after more than60 state updates when Processed2652 speed is below
0.15m/s, or on the family exit condition. It dispatches exit forces instead
of the ordinary holding-force update. More than35 leaving updates requests
native reason17. The previous host never ran this phase and continued pinning
and orienting the board while stationary.
Connected Release state and the existing physical body/force/request owners.
50-50 exit82D41DF0 uses100 side/125 lift below investigation kind2, otherwise
10 side/0 lift, capped by outward speed1.0. Helper82D3F850 uses the actual
contact side and support normal. Other static-rail families use their native
exit calls82D428C0/82D420A0/82D42A80/82D41250. Ordinary friction/pinning and
orientation updates do not run during release. GRIND_RELEASE records the
transition during user gameplay.
Endpoint queries already clip segment intersections to[0,1]; losing the last
contact makes the existing selector enter Nonspecific701 and disable the
hook, then choose airborne/ground from actual contacts. No infinite extension
or endpoint teleport was added. The release phase addresses a board that
stalls while its last truck/tip remains supported at an endpoint.
Scope: common slow-speed/explicit release and static-rail exit forces.
Specialized tipslide drop-in and investigation-driven early-exit branches
remain separate existing gaps; this does not claim full retail parity.
Build only, no tests or automated gameplay. Visual checking is left to user.