diff --git a/crates/skate-core/src/input/offboard_intentions.rs b/crates/skate-core/src/input/offboard_intentions.rs index 04e6de31..e5a1d140 100644 --- a/crates/skate-core/src/input/offboard_intentions.rs +++ b/crates/skate-core/src/input/offboard_intentions.rs @@ -87,6 +87,14 @@ pub fn produce_analog( ) -> [OffboardIntent; 4] { let words = controller.words(); let stick = [f32::from_bits(words[7]), 0.0, f32::from_bits(words[8]), 0.0]; + produce_analog_world(stick, observation) +} + +/// Host camera conversion precedes the native world-space Biped attributes. +pub fn produce_analog_world( + stick: [f32; 4], + observation: AnalogObservation, +) -> [OffboardIntent; 4] { let mut horizontal = observation.effective_skeleton_z; horizontal[1] = 0.0; let forward = safe_unit(horizontal); diff --git a/crates/skate-game/src/physics/animation_phase.rs b/crates/skate-game/src/physics/animation_phase.rs index 56bbfa80..9bb08516 100644 --- a/crates/skate-game/src/physics/animation_phase.rs +++ b/crates/skate-game/src/physics/animation_phase.rs @@ -268,7 +268,7 @@ pub(crate) fn advance( //Native Listener::Fill825999F0, after derived controller sampling and before AG. let mut action_intents = controls.action_intents.clone(); use skate_core::input::offboard_intentions::{ - AnalogObservation, produce_analog, produce_discrete, + AnalogObservation, produce_analog_world, produce_discrete, }; let biped = &skater.offboard.controller.state.contact; for intent in produce_discrete( @@ -277,8 +277,11 @@ pub(crate) fn advance( physical.air.use_air_reckoning_452 != 0, ) .into_iter() - .chain(produce_analog( - &controls.controller, + .chain(produce_analog_world( + controls.offboard_direction.unwrap_or_else(|| { + let words = controls.controller.words(); + [f32::from_bits(words[7]), 0., f32::from_bits(words[8]), 0.] + }), AnalogObservation { effective_skeleton_z: p.effective_anim_transform_192[2].map(f32::from_bits), biped_correction: biped.active.then_some(biped.direction), diff --git a/crates/skate-game/src/physics/controls.rs b/crates/skate-game/src/physics/controls.rs index 53c51f7f..985db8ed 100644 --- a/crates/skate-game/src/physics/controls.rs +++ b/crates/skate-game/src/physics/controls.rs @@ -12,6 +12,7 @@ use skate_core::input::{ #[derive(Resource)] pub(crate) struct PlayerControls { pub controller: DerivedControllerInput, + pub offboard_direction: Option<[f32; 4]>, pub intents: Vec, pub action_intents: IntentMap, pub ticks: u64, @@ -27,6 +28,7 @@ impl Default for PlayerControls { controller.initialize(); Self { controller, + offboard_direction: None, intents: Vec::new(), action_intents: IntentMap::new(), ticks: 0, @@ -43,6 +45,7 @@ pub(super) fn sample( input: Res, physics: Res, skater: Res, + camera: Res, mut player: ResMut, ) { let mut map = input.player_actions(); @@ -52,6 +55,18 @@ pub(super) fn sample( physics.settings.input_magnitude_threshold, skater.player_input.physical.scoring.capabilities_204, ); + if let Some(frame) = camera.frame { + let at = Vec3::from_array(frame.basis.columns[2]); + let forward = Vec3::new(at.x, 0., at.z).normalize_or_zero(); + if forward != Vec3::ZERO { + // Match camera::present: native camera Right becomes Bevy -Right. + let right = forward.cross(Vec3::Y); + let words = player.controller.words(); + let direction = right * f32::from_bits(words[7]) + + forward * f32::from_bits(words[8]); + player.offboard_direction = Some([direction.x, 0., direction.z, 0.]); + } + } player.publish_gestures(physics.animation_profile.physics_mode, skater.player_input.physical.state.state_16); } @@ -69,6 +84,7 @@ impl PlayerControls { } pub fn update(&mut self, map: &mut impl ActionMap, dt: f32, magnitude_threshold: f32, physical_capabilities: u32) { + self.offboard_direction = None; self.controller.update( map, dt, diff --git a/crates/skate-game/src/physics/offboard/air_diagnostics.rs b/crates/skate-game/src/physics/offboard/air_diagnostics.rs new file mode 100644 index 00000000..42731c07 --- /dev/null +++ b/crates/skate-game/src/physics/offboard/air_diagnostics.rs @@ -0,0 +1,59 @@ +//! Bounded numeric history for user-reproduced airborne bails. No per-frame IO. +use crate::physics::SkaterRuntime; +use std::collections::VecDeque; + +#[derive(Debug)] +#[allow(dead_code)] // Fields are serialized together only when a bail is requested. +struct Sample { + tick: i32, + flags: [u32; 3], + packet: skate_core::player::offboard::air_prediction::Packet, + root: [[f32; 4]; 4], + root_velocity: [f32; 4], + body: [f32; 4], + lift: f32, + extra: [[f32; 4]; 2], + maximum_error: Option, + reasons: [bool; 34], +} + +pub(crate) struct History { + samples: VecDeque, + reports: usize, +} +impl Default for History { + fn default() -> Self { + Self { samples: VecDeque::with_capacity(32), reports: 0 } + } +} +impl History { + pub(crate) fn clear(&mut self) { self.samples.clear(); } +} + +pub(crate) fn record(skater: &mut SkaterRuntime) { + let history = &mut skater.offboard.air_diagnostics; + if history.reports >= 3 { return; } + let state = &skater.offboard.air_state; + let p = &skater.player_input.processed; + if history.samples.len() == 32 { history.samples.pop_front(); } + history.samples.push_back(Sample { + tick: state.tick, + flags: [p.flags_2476, p.flags_2480, p.flags_2484], + packet: state.packet, + root: skater.animated_skeleton.roots.animation_to_world, + root_velocity: skater.skeleton_input.root_velocity, + body: state.body_position, + lift: state.lift, + extra: skater.collision_extra_displacements, + maximum_error: skater.collision_maximum_error, + reasons: skater.wipeout.state.reasons, + }); + if skater.wipeout.state.reasons.iter().any(|&requested| requested) { + // Format in memory, then one write; never send Debug's individual + // formatting fragments to the Windows redirected stderr handle. + let report = format!("SKATE_BIPED_AIR_BAIL history={:#?}", history.samples); + eprintln!("{report}"); + history.samples.clear(); + history.reports += 1; + } +} diff --git a/crates/skate-game/src/physics/offboard/air_state.rs b/crates/skate-game/src/physics/offboard/air_state.rs index 7df2aeac..b3e9694a 100644 --- a/crates/skate-game/src/physics/offboard/air_state.rs +++ b/crates/skate-game/src/physics/offboard/air_state.rs @@ -16,6 +16,7 @@ fn effective(skater: &SkaterRuntime) -> Frame { frame } pub(crate) fn enter(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> Result<(), String> { + skater.offboard.air_diagnostics.clear(); let frame = effective(skater); let p = &skater.player_input.processed; let body = skater.animated_skeleton.board_frames.com_frame[3]; diff --git a/crates/skate-game/src/physics/offboard/mod.rs b/crates/skate-game/src/physics/offboard/mod.rs index d63f5aff..212021df 100644 --- a/crates/skate-game/src/physics/offboard/mod.rs +++ b/crates/skate-game/src/physics/offboard/mod.rs @@ -1,5 +1,6 @@ //! Off-board game adapters over the shared physical and scene owners. pub(crate) mod air_state; +pub(crate) mod air_diagnostics; pub(crate) mod air_trajectory; pub(crate) mod board_effects; pub(crate) mod contact_queries; diff --git a/crates/skate-game/src/physics/offboard/runtime.rs b/crates/skate-game/src/physics/offboard/runtime.rs index 643c6924..51bdd1db 100644 --- a/crates/skate-game/src/physics/offboard/runtime.rs +++ b/crates/skate-game/src/physics/offboard/runtime.rs @@ -10,6 +10,7 @@ use skate_core::point_graph::PointGraph; use skate_data::{animation_metadata::AnimationMetadata, collections::Collections}; pub(crate) struct Runtime { + pub air_diagnostics: super::air_diagnostics::History, pub air_state: skate_core::player::offboard::air_state::State, pub air_prediction: Option, pub air_blend_curve: PointGraph<8>, @@ -148,6 +149,7 @@ impl Runtime { pub(crate) fn load(data: &Collections, metadata: &AnimationMetadata) -> Result { let settings = super::settings::Settings::load(data, metadata)?; Ok(Self { + air_diagnostics: Default::default(), air_state: Default::default(), air_prediction: None, air_blend_curve: settings.air_blend_curve, diff --git a/crates/skate-game/src/physics/wipeout.rs b/crates/skate-game/src/physics/wipeout.rs index 7496efb8..0425f944 100644 --- a/crates/skate-game/src/physics/wipeout.rs +++ b/crates/skate-game/src/physics/wipeout.rs @@ -117,6 +117,7 @@ pub(super) fn check_after_physics( ], }, ); + super::offboard::air_diagnostics::record(skater); Ok(()) } PhysicalStateId::PhysicsGround | PhysicalStateId::SlideGround => {