diff --git a/crates/skate-core/src/physics/board_ground.rs b/crates/skate-core/src/physics/board_ground.rs index 78ce0ad0..48745f82 100644 --- a/crates/skate-core/src/physics/board_ground.rs +++ b/crates/skate-core/src/physics/board_ground.rs @@ -293,7 +293,7 @@ impl BoardGroundState { /// Complete8296EBB0, including its small-vector gate and ONE reciprocal-square- /// root refinement per vector. Native82C07D20 compares this angle with pi/4. -pub(crate) fn angle_between(a: Vector3, b: Vector3) -> f32 { +pub fn angle_between(a: Vector3, b: Vector3) -> f32 { let a_squared = dot(a, a); let b_squared = dot(b, b); let epsilon = f32::from_bits(0x38D1_B717); diff --git a/crates/skate-core/src/player/wipeout/ground.rs b/crates/skate-core/src/player/wipeout/ground.rs index ee3bf1a1..e4c41400 100644 --- a/crates/skate-core/src/player/wipeout/ground.rs +++ b/crates/skate-core/src/player/wipeout/ground.rs @@ -127,7 +127,7 @@ pub fn check_animation(state: &mut Requests, s: &Settings, mode: &Mode, f: &Fram } } -///82D8FDC0, shared FootPlant/HandPlant postphysics collision checks. +///82D8FDC0, FootPlant postphysics checks. HandPlant calls check_air(false). pub fn check_plant(state: &mut Requests, s: &Settings, f: &Frame) { state.mode = 3; if f.maximum_pose_error > s.ground.max_squash { diff --git a/crates/skate-core/src/player/wipeout/tests.rs b/crates/skate-core/src/player/wipeout/tests.rs index 5c5d4041..de440445 100644 --- a/crates/skate-core/src/player/wipeout/tests.rs +++ b/crates/skate-core/src/player/wipeout/tests.rs @@ -1,6 +1,26 @@ use super::*; use crate::physics::skeleton_animation_record::IDENTITY; use crate::point_graph::PointGraph; +#[test] +fn handplant_air_check_preserves_contact_threshold_and_real_impact_bails() { + let mut f=input(); + f.category=600; + f.closing_velocity=[0.2,0.0,0.0,0.0]; + let mut s=settings(); + s.air.xz_trick=0.1; + s.air.y_trick=0.6; + let mut foot=Requests::new(); + check_plant(&mut foot,&s,&f); + assert!(foot.reasons[2]); + //HandPlant82D4C530 calls Air(false); a coping contact is not by itself + //the sideways-board danger branch that enables the tiny trick limits. + let mut hand=Requests::new(); + check_air(&mut hand,&s,&mode(),&f,false); + assert_eq!(hand.count,0); + f.closing_velocity=[11.0,0.0,0.0,0.0]; + check_air(&mut hand,&s,&mode(),&f,false); + assert!(hand.reasons[2]); +} fn input() -> Frame { Frame { flags_2468: 0, diff --git a/crates/skate-game/src/graphics_menu.rs b/crates/skate-game/src/graphics_menu.rs index a3d77f99..94948454 100644 --- a/crates/skate-game/src/graphics_menu.rs +++ b/crates/skate-game/src/graphics_menu.rs @@ -461,6 +461,7 @@ mod tests { .insert_resource(images) .insert_resource(Menu { open: false, selected: 0, settings: GraphicsSettings::default(), + maps: Vec::new(), selected_map: 0, difficulty: Difficulty::Easy, path: PathBuf::new(), supported_msaa: vec![1, 2, 4, 8], status: String::new(), }) .add_systems(Update, apply); diff --git a/crates/skate-game/src/physics.rs b/crates/skate-game/src/physics.rs index 1dbd4620..cdda0218 100644 --- a/crates/skate-game/src/physics.rs +++ b/crates/skate-game/src/physics.rs @@ -39,6 +39,7 @@ mod grind; mod ground_phase; mod ground_animation; mod slide_state; +mod revert_state; mod ground_exit; mod ground_runtime; mod input_phase; diff --git a/crates/skate-game/src/physics/frame.rs b/crates/skate-game/src/physics/frame.rs index 103974cf..637778ef 100644 --- a/crates/skate-game/src/physics/frame.rs +++ b/crates/skate-game/src/physics/frame.rs @@ -112,6 +112,7 @@ pub(super) fn advance( } player_state::pre_state(physics, skater)?; match skater.player_state.current() { + skate_core::player::state::PhysicalStateId::RevertGround => super::revert_state::update(physics,skater)?, skate_core::player::state::PhysicalStateId::HandPlant => super::handplant::update(physics,skater)?, skate_core::player::state::PhysicalStateId::FootPlant => super::footplant::ground::update(physics, skater)?, skate_core::player::state::PhysicalStateId::Boneless => super::boneless::update(physics, skater)?, diff --git a/crates/skate-game/src/physics/ground_runtime/pumping.rs b/crates/skate-game/src/physics/ground_runtime/pumping.rs index 8e9b3993..28af6bf9 100644 --- a/crates/skate-game/src/physics/ground_runtime/pumping.rs +++ b/crates/skate-game/src/physics/ground_runtime/pumping.rs @@ -27,6 +27,46 @@ pub(crate) struct GroundPumpingMode { pub unintentional_scalar: f32, } impl GroundPumping { + ///Revert82D435FC..36A0 uses the processed timestep and flag2472 bit16. + pub fn update_revert( + &self, + state: &mut PumpingState, + toolkit: &BoardToolkit, + riding: &RidingOutputs, + skeleton: &AnimatedSkeleton, + p: &skate_core::player::input_phase::ProcessedPhysicsInput, + balance: f32, + ) -> Result<(), String> { + let angle = if balance != 0.0 { + 0.0 + } else { + (skate_core::physics::board_ground::angle_between( + xyz(toolkit.deck[2]), + riding.reckoning.up, + ) + .abs() + - std::f32::consts::FRAC_PI_2) + .abs() + }; + let sample = PumpingSample { + position: toolkit.deck[3], + normal: v(riding.reckoning.ground_normal), + com_to_deck_world: skeleton.record.com_to_deck_world, + deck_angle: angle, + intentional_pumping: ((p.flags_2472 >> 16) & 1) as u8, + }; + match controller::update( + state, + &self.settings, + self.mode(p.state_variant_index_2528)?.controller, + sample, + p.timestep_2604, + &mut NativePumpingGeometry, + ) { + Ok(()) => Ok(()), + Err(never) => match never {}, + } + } pub fn load(data: &Collections) -> Result { let f = |field| data.float("physics_pumping", "default", field); let c = |field| super::settings::curve8(data, "physics_pumping", "default", field); diff --git a/crates/skate-game/src/physics/player_state/pre_state.rs b/crates/skate-game/src/physics/player_state/pre_state.rs index df0c7175..76c58600 100644 --- a/crates/skate-game/src/physics/player_state/pre_state.rs +++ b/crates/skate-game/src/physics/player_state/pre_state.rs @@ -18,6 +18,7 @@ pub(super) fn advance(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> | PhysicalStateId::FootPlant | PhysicalStateId::Boneless | PhysicalStateId::HandPlant | PhysicalStateId::GroundAnimation | PhysicalStateId::SlideGround + | PhysicalStateId::RevertGround | PhysicalStateId::WipeoutGround | PhysicalStateId::Teleporting | PhysicalStateId::BipedAir diff --git a/crates/skate-game/src/physics/player_state/publication.rs b/crates/skate-game/src/physics/player_state/publication.rs index ecd0a2e5..9a0f8a36 100644 --- a/crates/skate-game/src/physics/player_state/publication.rs +++ b/crates/skate-game/src/physics/player_state/publication.rs @@ -19,6 +19,7 @@ pub(super) fn publish(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> | PhysicalStateId::FootPlant | PhysicalStateId::Boneless | PhysicalStateId::HandPlant | PhysicalStateId::GroundAnimation | PhysicalStateId::SlideGround + | PhysicalStateId::RevertGround | PhysicalStateId::WipeoutGround | PhysicalStateId::Teleporting | PhysicalStateId::BipedAir @@ -58,6 +59,7 @@ pub(super) fn publish(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> // State63 is set only by LandingOnDeckManager::Fill82D4E0E8..E0F4 // when byte246 && time240<.02. Ordinary Ground never invokes that Fill, // so it retains the template zero here; State65 carries Ground requests. + set(66, state == PhysicalStateId::RevertGround && skater.revert_state.active); set(65, skater.wipeout.requests_wipeout(p)); set(78, skater.wipeout.requests_runout(p)); set(71, player.flags_1296 & (1 << 24) != 0); @@ -128,6 +130,7 @@ pub(super) fn publish(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> physical.state = CurrentStateFields { category_12: state.category(), state_16: state as u32, + flag_66: u8::from(state == PhysicalStateId::RevertGround && skater.revert_state.active), flag_74: u8::from(state==PhysicalStateId::HandPlant && skater.handplant.continuation), //ProcessOutput82DB7104..7128: selector57 requests State69 here. //The next input/selection enters702; never reset inside selection. diff --git a/crates/skate-game/src/physics/player_state/transition.rs b/crates/skate-game/src/physics/player_state/transition.rs index cb362c70..63eb1298 100644 --- a/crates/skate-game/src/physics/player_state/transition.rs +++ b/crates/skate-game/src/physics/player_state/transition.rs @@ -21,6 +21,7 @@ impl PhysicalStateCalls for Calls { | PhysicalStateId::FootPlant | PhysicalStateId::Boneless | PhysicalStateId::HandPlant | PhysicalStateId::GroundAnimation | PhysicalStateId::SlideGround + | PhysicalStateId::RevertGround | PhysicalStateId::WipeoutGround | PhysicalStateId::Teleporting | PhysicalStateId::BipedAir @@ -38,6 +39,7 @@ impl PhysicalStateCalls for Calls { | PhysicalStateId::FootPlant | PhysicalStateId::Boneless | PhysicalStateId::HandPlant | PhysicalStateId::GroundAnimation | PhysicalStateId::SlideGround + | PhysicalStateId::RevertGround | PhysicalStateId::WipeoutGround | PhysicalStateId::Teleporting | PhysicalStateId::BipedAir @@ -67,6 +69,7 @@ pub(super) fn set( | PhysicalStateId::FootPlant | PhysicalStateId::Boneless | PhysicalStateId::HandPlant | PhysicalStateId::GroundAnimation | PhysicalStateId::SlideGround + | PhysicalStateId::RevertGround | PhysicalStateId::WipeoutGround | PhysicalStateId::Teleporting | PhysicalStateId::BipedAir @@ -80,6 +83,7 @@ pub(super) fn set( | PhysicalStateId::FootPlant | PhysicalStateId::Boneless | PhysicalStateId::HandPlant | PhysicalStateId::GroundAnimation | PhysicalStateId::SlideGround + | PhysicalStateId::RevertGround | PhysicalStateId::WipeoutGround | PhysicalStateId::Teleporting | PhysicalStateId::BipedAir @@ -161,6 +165,7 @@ pub(super) fn set( //Native82DB8540 publishes Processed state/history BEFORE old Exit. The //same retained objects then receive Exit followed by the new Enter. match current { + PhysicalStateId::RevertGround => {}, //Exit82B61BB8 PhysicalStateId::HandPlant => skater.handplant.reset(), PhysicalStateId::FootPlant => skater.footplant.reset(), //Exit82D4C5A8 PhysicalStateId::Boneless => {}, //empty82D4C9B4 @@ -179,6 +184,7 @@ pub(super) fn set( _ => unreachable!("state support checked before publication"), } match requested { + PhysicalStateId::RevertGround => super::super::revert_state::enter(physics, skater), PhysicalStateId::HandPlant => super::super::handplant::enter(physics, skater), PhysicalStateId::FootPlant => super::super::footplant::ground::enter(physics, skater), PhysicalStateId::Boneless => super::super::boneless::enter(physics, skater), diff --git a/crates/skate-game/src/physics/revert_state.rs b/crates/skate-game/src/physics/revert_state.rs new file mode 100644 index 00000000..ac945dc9 --- /dev/null +++ b/crates/skate-game/src/physics/revert_state.rs @@ -0,0 +1,266 @@ +//! RevertGround102, TU3 vtable82327330: Enter82D43488, Update82D43518, +//! empty Exit, Ground Post82D4C070, Fill82D43B10. Stock layout018E8A2E5028AB3F. +use super::{GamePhysics, SkaterRuntime, ground_runtime::GroundInputObservations}; +use skate_core::{ + math::Vector3, + physics::{force_queue::QueuedPointForce, manual::controller}, + point_graph::PointGraph, + riding::{anti_flip, collision_response::signed_angle, pumping}, +}; +use skate_data::collections::Collections; +type V = [f32; 4]; +pub(crate) struct RevertState { + speed: PointGraph<8>, + tolerance: PointGraph<4>, + gain: f32, + delay: f32, + duration: f32, + direction: f32, + travel_sign: f32, + velocity: V, + elapsed: f32, + captured: bool, + pub active: bool, +} +impl RevertState { + pub fn load(d: &Collections) -> Result { + let class = "Hash_018E8A2E5028AB3F"; + let a = d + .words::<20>(class, "default", "Hash_DAA990E803A782A1")? + .map(f32::from_bits); + let b = d + .words::<12>(class, "default", "Hash_3387562E9BEA0AF6")? + .map(f32::from_bits); + Ok(Self { + speed: PointGraph { + x: a[4..12].try_into().unwrap(), + y: a[12..20].try_into().unwrap(), + }, + tolerance: PointGraph { + x: b[4..8].try_into().unwrap(), + y: b[8..12].try_into().unwrap(), + }, + gain: d.float(class, "default", "Hash_0E5B939FD4ECDBAA")?, + delay: d.float(class, "default", "Hash_6BF6544475D2A5AD")?, + duration: d.float(class, "default", "Hash_035D9805EB9E2EE2")?, + direction: 0.0, + travel_sign: 1.0, + velocity: [0.0; 4], + elapsed: 0.0, + captured: false, + active: false, + }) + } + ///82D43800: directed remaining angle, stock speed curve and rate damping. + fn correction(&mut self, forward: V, normal: V, angular: V) -> V { + if !self.captured { + return [0.0; 4]; + } + let desired = forward.map(|v| v * self.travel_sign); + //Preserve the two native self-scaled projections82D438B4..D0. + let project = |v: V| { + let d = dot(v, normal); + v.map(|x| x - x * d) + }; + let from = project(self.velocity); + let to = project(desired); + if dot(from, from) * dot(to, to) <= f32::from_bits(0x37800000) { + return [0.0; 4]; + } + let normalize = |v: V| { + let length = dot(v, v).sqrt(); + v.map(|x| x / length) + }; + let mut angle = signed_angle(xyz(normalize(from)), xyz(normalize(to)), xyz(normal)); + //8258DB98 folds the helper's[0,2pi] output before the direction test. + if angle >= std::f32::consts::PI { + angle -= std::f32::consts::TAU; + } + if angle * self.direction > 0.0 { + angle -= self.direction * std::f32::consts::TAU; + } + let target = self.speed.evaluate(angle.abs()) * self.direction; + if angle.abs() < self.tolerance.evaluate(normal[1]) * f32::from_bits(0x3c8efa35) + || angle.abs() > 4.5 + { + self.active = false; + } + let current = dot(angular, normal); + let correction = if current * target > 0.0 && current.abs() > target.abs() { + 0.0 + } else { + (target - current) * self.gain + }; + normal.map(|v| v * correction) + } +} +pub(super) fn enter(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> Result<(), String> { + skater.ground_lifecycle.skeleton_elapsed_16505 = true; + physics + .board + .hook_mut() + .drive + .disable_animation(&mut skater.ground_lifecycle.board_animated_290); + let r = &mut skater.revert_state; + r.direction = skater.animation_input.extra.revert_direction; + r.velocity = [0.0; 4]; + r.elapsed = 0.0; + r.captured = false; + r.active = true; + skater.wipeout.state.enter_ground(); + Ok(()) +} +pub(super) fn update(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> Result<(), String> { + let p = &skater.player_input.processed; + let t = skater + .player_input + .toolkit + .as_ref() + .ok_or("Revert requires current BoardToolkit")?; + let r = &mut skater.revert_state; + if !r.captured && r.elapsed >= r.delay { + r.captured = true; + r.velocity = p.vectors_400_416[0].map(f32::from_bits); + r.travel_sign = if dot(r.velocity, t.deck[2]) > 0.0 { + -1.0 + } else { + 1.0 + }; + } + physics.riding.update_slide_reckoning( + p, + t, + skater.animation_input.extra.physical_body_spin, + skater.animation_input.fields.balance, + ); + super::input_phase::update_ground(physics, skater)?; + let p = &skater.player_input.processed; + let t = skater + .player_input + .toolkit + .as_ref() + .ok_or("Revert requires current BoardToolkit")?; + skater.ground.pumping_settings.update_revert( + &mut skater.ground.pumping, + t, + &physics.riding, + &skater.animated_skeleton, + p, + skater.animation_input.fields.balance, + )?; + let input = skater.ground_settings.input( + t, + p, + &skater.animation_input, + &skater.ground.pumping, + skater + .ground + .pumping_settings + .mode(p.state_variant_index_2528)? + .unintentional_scalar, + &physics.riding, + &skater.animated_skeleton, + physics.settings.step.base_truck_transforms, + GroundInputObservations { + manual_drag_2724: skater.ground_lifecycle.manual_drag_2724, + trajectory_state_bits: p.external_physics_1616.flags, + edge_flags: 0, + edge_point: [0.0; 4], + }, + ); + let settings = skater.ground_settings.board(); + let correction = skater.revert_state.correction( + t.deck[2], + p.vectors_464_480_496_512_528[0].map(f32::from_bits), + p.vectors_720_784_800_816_832_864[0].map(f32::from_bits), + ); + //82C040F0(0): zero linear drag on every board body. + for body in physics.board.bodies_mut() { + body.inertia.linear_drag = 0.0; + } + let anti_flip = anti_flip::calculate(settings.anti_flip, &input.anti_flip); + let pump = pumping::calculate(&input.pump_force); + let manual = controller::calculate( + &mut skater.ground.manual, + settings.manual, + settings.manual_mode, + &input.manual, + &mut skater.ground_runtime, + ) + .map_err(|e| format!("Revert manual: {e:?}"))?; + for value in [correction, manual.angular_displacement, anti_flip] { + skater + .ground_runtime + .apply_angular_displacement(&mut physics.board, value); + } + physics.board.forces_mut().append(QueuedPointForce { + tag: 8, + force_world: Vector3::new(pump[0], pump[1], pump[2]), + point_body: Vector3::new(pump[4], pump[5], pump[6]), + }); + skater.revert_state.elapsed += p.timestep_2604; + if skater.revert_state.elapsed > skater.revert_state.duration { + skater.revert_state.active = false; + } + Ok(()) +} +fn dot(a: V, b: V) -> f32 { + a[2].mul_add(b[2], a[1].mul_add(b[1], a[0] * b[0])) +} +fn xyz(v: V) -> Vector3 { + Vector3::new(v[0], v[1], v[2]) +} + +#[cfg(test)] +mod tests { + use super::*; + fn state(direction: f32) -> RevertState { + RevertState { + speed: PointGraph { + x: [0.0, 0.1, 0.2, 0.3, 1.0, 2.0, 3.0, 7.0], + y: [5.0; 8], + }, + tolerance: PointGraph { + x: [0.0, 0.2, 0.6, 1.0], + y: [8.0; 4], + }, + gain: 0.06, + delay: 0.0, + duration: 1.0, + direction, + travel_sign: -1.0, + velocity: [0.0, 0.0, 6.0, 0.0], + elapsed: 0.0, + captured: true, + active: true, + } + } + #[test] + fn revert_steers_both_directions_and_does_not_brake_faster_matching_spin() { + for direction in [-1.0, 1.0] { + let mut r = state(direction); + let correction = r.correction([0.0, 0.0, 1.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0; 4]); + assert!((correction[1] - 0.3 * direction).abs() < 0.00001); + assert!(r.active); + assert_eq!( + r.correction( + [0.0, 0.0, 1.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 6.0 * direction, 0.0, 0.0] + ), + [0.0; 4] + ); + } + } + #[test] + fn revert_completes_when_board_faces_captured_reverse_heading() { + let mut r = state(1.0); + r.correction([0.0, 0.0, -1.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0; 4]); + assert!(!r.active); + r.velocity = [0.0; 4]; + assert_eq!( + r.correction([0.0, 0.0, 1.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0; 4]), + [0.0; 4] + ); + } +} diff --git a/crates/skate-game/src/physics/skater.rs b/crates/skate-game/src/physics/skater.rs index 7a9568cb..955d47ac 100644 --- a/crates/skate-game/src/physics/skater.rs +++ b/crates/skate-game/src/physics/skater.rs @@ -42,6 +42,7 @@ pub(crate) struct SkaterRuntime { pub known_air: super::known_air::KnownAir, pub ground_animation: super::ground_animation::GroundAnimationRuntime, pub ground_animation_settings: super::ground_animation::GroundAnimationSettings, + pub revert_state: super::revert_state::RevertState, pub slide_state: super::slide_state::SlideState, pub trajectory: super::air_trajectory::AirTrajectoryRuntime, pub footplant: super::footplant::Footplant, @@ -205,6 +206,7 @@ impl SkaterRuntime { known_air: super::known_air::KnownAir::load(&data)?, ground_animation: Default::default(), ground_animation_settings: super::ground_animation::GroundAnimationSettings::load(&data)?, + revert_state: super::revert_state::RevertState::load(&data)?, slide_state: super::slide_state::SlideState::load(&data)?, trajectory: super::air_trajectory::AirTrajectoryRuntime::load(&data)?, footplant: super::footplant::Footplant::load(&data)?, diff --git a/crates/skate-game/src/physics/wipeout.rs b/crates/skate-game/src/physics/wipeout.rs index 72b6a21b..54c766c4 100644 --- a/crates/skate-game/src/physics/wipeout.rs +++ b/crates/skate-game/src/physics/wipeout.rs @@ -95,7 +95,23 @@ pub(super) fn check_after_physics( }; match skater.player_state.current() { PhysicalStateId::FootPlant | PhysicalStateId::HandPlant => { - wipeout::check_plant(&mut skater.wipeout.state, &skater.wipeout.settings, &observations.frame()?); + let frame = observations.frame()?; + if skater.player_state.current() == PhysicalStateId::HandPlant { + //HandPlant vtable823273CC slot40 ->82D4C530 ->82D90358(false). + //FootPlant's82D4C6D8 alone uses82D8FDC0. Its low trick-impact + //thresholds must not replace HandPlant's normal air checks. + skater.wipeout.check_air(&observations, false)?; + } else { + wipeout::check_plant(&mut skater.wipeout.state, &skater.wipeout.settings, &frame); + } + if skater.wipeout.state.count != 0 { + let reasons: Vec<_> = skater.wipeout.state.reasons.iter().enumerate() + .filter_map(|(i, &active)| active.then_some(i)).collect(); + bevy::log::info!("PLANT_BAIL tick={} state={:?} reasons={reasons:?} pose_error={:?} max_pose_error={} regions={:?} closing={:?} board_contact={} world_to_animation={:?}", + physics.ticks, skater.player_state.current(), frame.pose_error, + frame.maximum_pose_error, frame.regions_force, frame.closing_velocity, + frame.board_contact, frame.world_to_animation); + } if skater.player_state.current() == PhysicalStateId::FootPlant { super::footplant::ground::post_physics(skater); } @@ -127,7 +143,7 @@ pub(super) fn check_after_physics( super::offboard::air_diagnostics::record(skater); Ok(()) } - PhysicalStateId::PhysicsGround | PhysicalStateId::SlideGround => { + PhysicalStateId::PhysicsGround | PhysicalStateId::SlideGround | PhysicalStateId::RevertGround => { skater.wipeout.check_ground(&observations) } PhysicalStateId::GroundAnimation => { diff --git a/crates/skate-game/src/skate_world.rs b/crates/skate-game/src/skate_world.rs index ee1903d2..b4d98cb7 100644 --- a/crates/skate-game/src/skate_world.rs +++ b/crates/skate-game/src/skate_world.rs @@ -853,12 +853,14 @@ mod tests { let mut queue = bevy::ecs::world::CommandQueue::default(); let mut meshes = Assets::::default(); let mut materials = Assets::::default(); + let mut retail_materials = Assets::::default(); let mut images = Assets::::default(); spawn( &map, &mut Commands::new(&mut queue, &world), &mut meshes, &mut materials, + &mut retail_materials, &mut images, ); queue.apply(&mut world); diff --git a/docs/plants.md b/docs/plants.md index 1c53a995..54a5224d 100644 --- a/docs/plants.md +++ b/docs/plants.md @@ -117,7 +117,7 @@ operands take precedence over unreliable decompiler output and community names. | Handplant skeleton | State600 vtable `823273CC`; entry/update `82D4C350/82D4C3D8`; reckoning `82D8E1C0`; hand IK `82D633B0`, target clamp `82BD9728/82BD97D0`, upper-body volume suppression `82D91298`. | | Shared skeleton | Anchored-foot path `82BDEC48`, COM path `82BDF090`, root positioning `82BDEEB8/82BE0E80`. Uses the existing completed animation, physical skeleton, board and four-limb IK owners. | | Graph timing | `IsFootPlanting` `82BBD888`; preparation `82BBD7F0`; FootPlantAbsorb Update `82BBE4E0`; handplant phase/new-position `82BBDA98/82BBDB98`; antic Begin `82BBE5D0`; handplant scoring Update `82BBF758`. | -| Lifecycle | Footplant release `82D8B970` reads big-endian byte2480 bit0, hence full-word bit24. Common handplant publication `82DB7130..82DB7190`; shared plant wipeout check `82D8FDC0`. Teleport/reset paths clear active plant state. | +| Lifecycle | Footplant release `82D8B970` reads big-endian byte2480 bit0, hence full-word bit24. Common handplant publication `82DB7130..82DB7190`; FootPlant wipeout check `82D8FDC0`; HandPlant post `82D4C530` calls Air `82D90358(false)`. Teleport/reset paths clear active plant state. | Settings are loaded from the installation, using the schema's cached layout: @@ -155,3 +155,25 @@ reuse existing engine math; bit-exact Xenon/VMX execution is not claimed. Handplant scoring updates the existing internal score packet, whose presentation remains outside this change. Stock assets and private research/build files are excluded from the commit. + +## Handplant bail and delayed revert crash (8 September) + +The supplied run entered HandPlant at tick745 and WipeoutGround at746, then +stopped at tick1318 on the missing PhysicsGround -> RevertGround adapter. +HandPlant incorrectly used FootPlant collision checks with unconditional trick +impact limits (stock XZ0.1/Y0.6). It now dispatches the native Air(false) checks, +including their contact, difficulty, cooldown and actual danger-state gates. +The original bail reason was not recorded, so the causal gameplay result still +requires a user retest. PLANT_BAIL now records reason indices and collision/pose +observations when either plant requests a bail. + +RevertGround102 now owns Enter82D43488, Update82D43518, empty Exit, Ground +Post82D4C070 and Fill82D43B10. It loads the five fields from stock class +018E8A2E5028AB3F, captures approach velocity after the authored delay, applies +the directed rotation-speed curve, manual/anti-flip corrections and pumping, +and publishes active State66 for the existing selector's normal exit. +No gameplay execution was performed during this repair. + +Validation for this repair: six core wipeout tests and two revert numerical +tests passed. Two pre-existing game test fixtures were updated for their current +map-menu and render-material API signatures so these tests could compile.