Add persistent native difficulty selection and refresh mode-dependent runtime settings

This commit is contained in:
chazmm committed 2026-09-06 17:07:45 +01:00
1 parent a21f78aa52
commit c818285b4d
20 files changed
+339 -30

No files matched your search

+12 -1
View File
@@ -6,6 +6,7 @@ pub(crate) struct Config {
pub asset_root: PathBuf,
pub verification_capture: Option<PathBuf>,
pub map: Option<skate_data::skate_map::SkateMap>,
pub difficulty: crate::difficulty::Difficulty,
}
impl Config {
@@ -14,7 +15,9 @@ impl Config {
asset_root: PathBuf::from("assets"),
verification_capture: None,
map: None,
difficulty: crate::difficulty::Difficulty::default(),
};
let mut difficulty_override = None;
let mut args = std::env::args_os().skip(1);
while let Some(arg) = args.next() {
match arg.to_str() {
@@ -25,6 +28,10 @@ impl Config {
let path = PathBuf::from(args.next().ok_or("--map requires a .skate file")?);
config.map = Some(skate_data::skate_map::SkateMap::load(&path)?);
}
Some("--difficulty") => {
let value = args.next().ok_or("--difficulty requires easy, normal or hardcore")?;
difficulty_override = Some(crate::difficulty::Difficulty::parse(&value.to_string_lossy())?);
}
Some("--verify") => {
config.verification_capture = Some(
args.next()
@@ -34,7 +41,7 @@ impl Config {
}
_ => {
return Err(format!(
"Unknown argument {arg:?}. Usage: skate-game [--assets DIRECTORY] [--map MAP.skate] [--verify CAPTURE.png]"
"Unknown argument {arg:?}. Usage: skate-game [--assets DIRECTORY] [--map MAP.skate] [--difficulty easy|normal|hardcore] [--verify CAPTURE.png]"
));
}
}
@@ -43,6 +50,10 @@ impl Config {
.asset_root
.canonicalize()
.map_err(|e| format!("Asset root {}: {e}", config.asset_root.display()))?;
config.difficulty = match difficulty_override {
Some(mode) => mode,
None => crate::difficulty::Difficulty::load(&config.asset_root)?,
};
if let Some(path) = &mut config.verification_capture {
if path.extension().and_then(|x| x.to_str()) != Some("png") {
return Err("--verify output must be a PNG file".into());
+61
View File
@@ -0,0 +1,61 @@
//! Host preference selects the native physics_mode collection, never a multiplier.
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
pub(crate) const NATIVE_MODES: [&str; 5] = ["easy", "normal", "hardcore", "motorized", "test"];
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[repr(u32)]
pub(crate) enum Difficulty {
#[default]
Easy = 0,
Normal = 1,
Hardcore = 2,
}
impl Difficulty {
pub const ALL: [Self; 3] = [Self::Easy, Self::Normal, Self::Hardcore];
pub fn key(self) -> &'static str { NATIVE_MODES[self as usize] }
pub fn label(self) -> &'static str {
match self { Self::Easy => "Easy", Self::Normal => "Normal", Self::Hardcore => "Hardcore" }
}
pub fn parse(value: &str) -> Result<Self, String> {
Self::ALL.into_iter().find(|d| d.key().eq_ignore_ascii_case(value))
.ok_or_else(|| format!("Unknown difficulty {value:?}; expected easy, normal or hardcore"))
}
pub fn path(root: &Path) -> PathBuf {
root.parent().unwrap_or(root).join("settings/gameplay.json")
}
pub fn load(root: &Path) -> Result<Self, String> {
match std::fs::read(Self::path(root)) {
Ok(bytes) => serde_json::from_slice::<Saved>(&bytes).map(|s| s.difficulty)
.map_err(|e| format!("Invalid saved gameplay settings: {e}")),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()),
Err(e) => Err(format!("Cannot read gameplay settings: {e}")),
}
}
pub fn save(self, root: &Path) -> Result<(), String> {
let path = Self::path(root);
std::fs::create_dir_all(path.parent().unwrap()).map_err(|e| e.to_string())?;
std::fs::write(path, serde_json::to_vec_pretty(&Saved { difficulty: self }).map_err(|e| e.to_string())?)
.map_err(|e| e.to_string())
}
}
#[derive(Serialize, Deserialize)]
struct Saved { difficulty: Difficulty }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn retail_indices_and_saved_names_agree() {
for (index, d) in Difficulty::ALL.into_iter().enumerate() {
assert_eq!(d as usize, index);
assert_eq!(Difficulty::parse(d.label()).unwrap(), d);
let bytes = serde_json::to_vec(&Saved { difficulty: d }).unwrap();
assert_eq!(serde_json::from_slice::<Saved>(&bytes).unwrap().difficulty, d);
}
assert!(Difficulty::parse("motorized").is_err());
assert!(serde_json::from_str::<Saved>(r#"{"difficulty":"made-up"}"#).is_err());
}
}
+25 -11
View File
@@ -12,6 +12,7 @@ use bevy::{
window::{PresentMode, PrimaryWindow},
};
use serde::{Deserialize, Serialize};
use crate::difficulty::Difficulty;
use std::{
path::PathBuf,
time::{Duration, Instant},
@@ -79,6 +80,7 @@ pub(crate) struct Menu {
settings: GraphicsSettings,
path: PathBuf,
supported_msaa: Vec<u32>,
difficulty: Difficulty,
status: String,
}
pub(crate) fn gameplay_active(menu: Option<Res<Menu>>) -> bool {
@@ -190,14 +192,14 @@ fn setup(
display: Display::None, width:percent(100), height:percent(100), align_items:AlignItems::Center,
justify_content:JustifyContent::Center, position_type:PositionType::Absolute, ..default()
}, BackgroundColor(Color::srgba(0.015,0.025,0.04,0.88)))).with_children(|root| {
root.spawn((Node { width:px(560),max_width:percent(95),padding:UiRect::all(px(28)),flex_direction:FlexDirection::Column,row_gap:px(10),border_radius:BorderRadius::all(px(12)),..default() },
root.spawn((Node { width:px(560),max_width:percent(95),padding:UiRect::all(px(18)),flex_direction:FlexDirection::Column,row_gap:px(6),border_radius:BorderRadius::all(px(12)),..default() },
BackgroundColor(Color::srgb(0.035,0.055,0.08)))).with_children(|panel| {
panel.spawn((Text::new("PAUSED"),TextFont {font_size:32.,..default()},TextColor(Color::WHITE)));
panel.spawn((Text::new("GRAPHICS"),TextFont {font_size:16.,..default()},TextColor(Color::srgb(0.4,0.85,0.85))));
for i in 0..7 {
panel.spawn((Button, MenuRow(i), Node {width:percent(100),min_height:px(48),padding:UiRect::all(px(12)),align_items:AlignItems::Center,border_radius:BorderRadius::all(px(5)),..default()},
panel.spawn((Text::new("GAMEPLAY & GRAPHICS"),TextFont {font_size:16.,..default()},TextColor(Color::srgb(0.4,0.85,0.85))));
for i in 0..8 {
panel.spawn((Button, MenuRow(i), Node {width:percent(100),min_height:px(36),padding:UiRect::all(px(8)),align_items:AlignItems::Center,border_radius:BorderRadius::all(px(5)),..default()},
BackgroundColor(Color::srgb(0.08,0.11,0.15)))).with_children(|row| {
row.spawn((MenuLabel(i),Text::new(""),TextFont {font_size:20.,..default()},TextColor(Color::WHITE)));
row.spawn((MenuLabel(i),Text::new(""),TextFont {font_size:18.,..default()},TextColor(Color::WHITE)));
});
}
panel.spawn((StatusLabel,Text::new(""),TextFont {font_size:15.,..default()},TextColor(Color::srgb(0.65,0.75,0.8))));
@@ -211,6 +213,7 @@ fn setup(
settings,
path,
supported_msaa,
difficulty: config.difficulty,
status: String::new(),
});
}
@@ -227,6 +230,8 @@ fn cycle<T: PartialEq + Copy>(values: &[T], value: T, direction: i32) -> T {
values[(index + direction).rem_euclid(values.len() as i32) as usize]
}
fn interact(
config: Res<crate::config::Config>,
mut physics: ResMut<crate::physics::GamePhysics>,
keys: Res<ButtonInput<KeyCode>>,
mut menu: ResMut<Menu>,
mut time: ResMut<Time<Virtual>>,
@@ -239,10 +244,10 @@ fn interact(
let mut action = None;
if menu.open {
if keys.just_pressed(KeyCode::ArrowUp) {
menu.selected = (menu.selected + 6) % 7;
menu.selected = (menu.selected + 7) % 8;
}
if keys.just_pressed(KeyCode::ArrowDown) {
menu.selected = (menu.selected + 1) % 7;
menu.selected = (menu.selected + 1) % 8;
}
if keys.just_pressed(KeyCode::ArrowLeft) {
action = Some((menu.selected, -1));
@@ -274,8 +279,16 @@ fn interact(
}
3 => menu.settings.fps = cycle(LIMITS, menu.settings.fps, direction),
4 => menu.settings.occlusion = !menu.settings.occlusion,
5 => menu.open = false,
6 => {
5 => {
menu.difficulty = cycle(&Difficulty::ALL, menu.difficulty, direction);
physics.set_difficulty(menu.difficulty);
menu.status = match menu.difficulty.save(&config.asset_root) {
Ok(()) => "Difficulty saved".into(),
Err(e) => format!("Applied, but could not save: {e}"),
};
}
6 => menu.open = false,
7 => {
exit.write(AppExit::Success);
}
_ => {}
@@ -390,7 +403,8 @@ fn labels(
}
),
4 => format!("Occlusion culling {}", if s.occlusion { "On" } else { "Off" }),
5 => "Resume".into(),
5 => format!("Difficulty {}", menu.difficulty.label()),
6 => "Resume".into(),
_ => "Quit game".into(),
};
}
@@ -428,7 +442,7 @@ mod tests {
.insert_resource(images)
.insert_resource(Menu {
open: false, selected: 0, settings: GraphicsSettings::default(),
path: PathBuf::new(), supported_msaa: vec![1, 2, 4, 8], status: String::new(),
difficulty: Difficulty::Easy, path: PathBuf::new(), supported_msaa: vec![1, 2, 4, 8], status: String::new(),
})
.add_systems(Update, apply);
app.world_mut().spawn((Window::default(), PrimaryWindow));
+4 -2
View File
@@ -4,6 +4,7 @@ mod app;
mod assets;
mod camera;
mod config;
mod difficulty;
mod graph_host;
mod graph_runtime;
mod input;
@@ -48,14 +49,15 @@ fn main() -> bevy::app::AppExit {
return bevy::app::AppExit::error();
}
}
let physics = match physics::GamePhysics::load_with_map(&config.asset_root, config.map.as_ref()) {
eprintln!("SKATE_DIFFICULTY mode={} native_index={}", config.difficulty.key(), config.difficulty as u32);
let physics = match physics::GamePhysics::load_with_difficulty(&config.asset_root, config.map.as_ref(), config.difficulty) {
Ok(physics) => physics,
Err(error) => {
eprintln!("{error}");
return bevy::app::AppExit::error();
}
};
let skater = match physics::SkaterRuntime::load(&config.asset_root, &graphs, &physics, "easy") {
let skater = match physics::SkaterRuntime::load(&config.asset_root, &graphs, &physics, config.difficulty.key()) {
Ok(skater) => skater,
Err(error) => {
eprintln!("{error}");
+17 -1
View File
@@ -86,6 +86,14 @@ pub(crate) struct GamePhysics {
}
impl GamePhysics {
pub(crate) fn set_difficulty(&mut self, difficulty: crate::difficulty::Difficulty) {
// Actor publication carries this selector into the next physical packet.
// Keep the board, active trick, equipment preferences and controller history.
self.animation_profile.physics_mode = difficulty as u32;
}
pub(crate) fn difficulty_index(&self) -> u32 { self.animation_profile.physics_mode }
pub(crate) fn world(&self) -> &BoardWorld {
&self.world
}
@@ -99,9 +107,13 @@ impl GamePhysics {
}
pub fn load_with_map(asset_root: &std::path::Path, map: Option<&skate_data::skate_map::SkateMap>) -> Result<Self, String> {
Self::load_with_difficulty(asset_root, map, crate::difficulty::Difficulty::Easy)
}
pub fn load_with_difficulty(asset_root: &std::path::Path, map: Option<&skate_data::skate_map::SkateMap>, difficulty: crate::difficulty::Difficulty) -> Result<Self, String> {
let data = Collections::load(asset_root)?;
let settings = PhysicsSettings::load(&data)?;
let animation_profile = animation_phase::AnimationProfile::load(&data, "easy")?;
let animation_profile = animation_phase::AnimationProfile::load(&data, difficulty.key())?;
let mut spawn = RetailAffineTransform {
translation: Vector3::new(
0.0,
@@ -314,6 +326,10 @@ mod air_tests;
#[path = "tests/wipeout_playback.rs"]
mod wipeout_tests;
#[cfg(test)]
#[path = "tests/difficulty.rs"]
mod difficulty_tests;
fn present(
physics: Res<GamePhysics>,
skater: Res<SkaterRuntime>,
@@ -29,6 +29,7 @@ pub(crate) struct AnimationInput {
bone_names: Vec<AttributeName>,
right_toe: usize,
settings: FinalizationInput,
height_overrides: [bool; 5],
}
impl AnimationInput {
@@ -39,6 +40,9 @@ impl AnimationInput {
.position(|n| n.eq_ignore_ascii_case("RightToeBase"))
.ok_or("Stock skeleton is missing RightToeBase")?;
Ok(Self {
height_overrides: crate::difficulty::NATIVE_MODES.map(|key|
data.boolean("physics_mode", key, "JumpHeightOverrideEnabled"))
.into_iter().collect::<Result<Vec<_>, _>>()?.try_into().unwrap(),
fields: ScalarAttributeInputs::reset(0, 0),
extra: ExtendedAttributes::reset(0.0),
contacts: ContactEventState {
@@ -84,6 +88,12 @@ impl AnimationInput {
})
}
pub fn select_physics_mode(&mut self, mode: u32) -> Result<(), String> {
self.settings.allow_height_override = *self.height_overrides.get(mode as usize)
.ok_or_else(|| format!("Invalid animation physics mode {mode}"))?;
Ok(())
}
/// ProcessedPhysIn Reset precedes physical/packet flag publication. The
/// caller then publishes those flags before dispatching the current pose.
pub fn reset_processed(&mut self) {
+4
View File
@@ -60,6 +60,10 @@ pub(super) fn advance(
actions,
input_available,
)?;
skater.ground_settings = skater.ground_profiles.select(
skater.player_input.processed.state_variant_index_2528,
skater.player_input.processed.surface_mode_2540,
)?;
if teleported {
skater.player_state.reset_for_teleport();
skater.centre_of_mass_filter.reset();
@@ -22,6 +22,7 @@ mod settings;
pub(crate) use launch::{GroundLaunchInfo, GroundLaunchPhysical};
pub(crate) use services::{GroundControllers, GroundPhysicalFrame};
pub(crate) use settings::GroundSettings;
pub(crate) use settings::GroundProfiles;
use skate_core::{
math::Vector3,
physics::{
@@ -20,6 +20,24 @@ use skate_core::{
},
};
use skate_data::collections::Collections;
use std::sync::Arc;
/// Immutable stock tables selected by the processed packet, including its
/// difficulty-dependent SurfacePhysics normalization. No per-tick parsing.
pub(crate) struct GroundProfiles(Vec<Vec<Arc<GroundSettings>>>);
impl GroundProfiles {
pub fn load(data: &Collections) -> Result<Self, String> {
crate::difficulty::NATIVE_MODES.into_iter().map(|mode| {
(1..=5).map(|surface| {
GroundSettings::load(data, mode, super::surface_key(surface)?).map(Arc::new)
}).collect::<Result<Vec<_>, String>>()
}).collect::<Result<Vec<_>, String>>().map(Self)
}
pub fn select(&self, mode: u32, surface: u32) -> Result<Arc<GroundSettings>, String> {
surface.checked_sub(1).and_then(|s| self.0.get(mode as usize)?.get(s as usize))
.cloned().ok_or_else(|| format!("Invalid processed physics mode/surface {mode}/{surface}"))
}
}
pub(crate) struct GroundSettings {
steering: SteeringSettings,
@@ -30,12 +30,12 @@ pub(crate) struct GroundState {
pub heading_previous: f32,
pub pumping_settings: GroundPumping,
output_settings: GroundOutputSettings,
auto_push_enabled: bool,
auto_push_enabled: [bool; 5],
pub entered: bool,
pub(super) entry_settings: super::entry::EntrySettings,
}
impl GroundState {
pub fn load(data: &Collections, mode: &str, human_player: bool) -> Result<Self, String> {
pub fn load(data: &Collections, _mode: &str, human_player: bool) -> Result<Self, String> {
let push = |field| data.float("physics_push", "default", field);
Ok(Self {
world_grab: super::world_grab::GroundWorldGrabState::default(),
@@ -65,7 +65,9 @@ impl GroundState {
mode_speed_threshold_0: push("Hash_501D5581043D7D3C")?,
},
entry_settings: super::entry::EntrySettings::load(data)?,
auto_push_enabled: data.boolean("physics_mode", mode, "AutoPushEnabled")?,
auto_push_enabled: crate::difficulty::NATIVE_MODES.map(|mode|
data.boolean("physics_mode", mode, "AutoPushEnabled"))
.into_iter().collect::<Result<Vec<_>, _>>()?.try_into().unwrap(),
entered: false,
})
}
@@ -86,7 +88,7 @@ impl GroundState {
scalar_2720: animation.fields.balance,
flags_2476: p.flags_2476,
flags_2484: p.flags_2484,
selected_mode_flag_109: self.auto_push_enabled,
selected_mode_flag_109: self.auto_push_enabled[p.state_variant_index_2528 as usize],
},
self.output_settings,
)
@@ -177,6 +177,7 @@ impl PlayerInputCallbacks for Callbacks<'_, '_> {
physical: &mut PhysicalPlayerInput,
processed: &mut ProcessedPhysicsInput,
) -> Result<(), String> {
self.animation_input.select_physics_mode(processed.state_variant_index_2528)?;
self.skeleton_input.process_data(
board,
toolkit,
+7 -2
View File
@@ -65,7 +65,8 @@ pub(crate) struct SkaterRuntime {
pub skeleton_input: SkeletonInputRuntime,
pub ground: GroundState,
pub ground_runtime: GroundRuntime,
pub ground_settings: GroundSettings,
pub ground_settings: std::sync::Arc<GroundSettings>,
pub ground_profiles: super::ground_runtime::GroundProfiles,
pub ground_lifecycle: super::ground_phase::GroundLifecycle,
pub animation_input: AnimationInput,
pub animation_feedback: AnimationFeedback,
@@ -84,6 +85,9 @@ impl SkaterRuntime {
mode: &str,
) -> Result<Self, String> {
let data = Collections::load(asset_root)?;
let ground_profiles = super::ground_runtime::GroundProfiles::load(&data)?;
let mode_index = crate::difficulty::NATIVE_MODES.iter().position(|m| *m == mode)
.ok_or_else(|| format!("Invalid skater mode {mode}"))? as u32;
// The host's current character is a custom skater with no pro selector
// or equipped physical hat. These are profile choices, not force values.
let mut animation = SkaterAnimation::load(asset_root, &data, graphs, b"")?;
@@ -231,7 +235,8 @@ impl SkaterRuntime {
},
ground: GroundState::load(&data, mode, true)?,
ground_runtime: GroundRuntime::load(&data)?,
ground_settings: GroundSettings::load(&data, mode, "smooth")?,
ground_settings: ground_profiles.select(mode_index, 1)?,
ground_profiles,
ground_lifecycle: super::ground_phase::GroundLifecycle::new(),
animation_input,
animation_feedback: AnimationFeedback::load(&data)?,
+2 -2
View File
@@ -21,7 +21,7 @@ fn roll_off(air_stick: Option<i16>) {
let root = std::path::Path::new(&root);
let assets = skate_data::GameAssets::load(root).unwrap();
let graphs = crate::graph_runtime::StockGraphs::load(root, &assets).unwrap();
let mut physics = GamePhysics::load(root).unwrap();
let mut physics = GamePhysics::load_with_difficulty(root, None, crate::difficulty::Difficulty::Normal).unwrap();
// Keep this air fixture's open edge now that the production level has a
// downhill ramp. The dedicated ramp regression uses the unmodified world.
physics.world = BoardWorld::new(physics.world.triangles().iter().copied()
@@ -144,7 +144,7 @@ fn stock_starting_ramp_reaches_floor_without_air() {
let root = std::path::Path::new(&root);
let assets = skate_data::GameAssets::load(root).unwrap();
let graphs = crate::graph_runtime::StockGraphs::load(root, &assets).unwrap();
let mut physics = GamePhysics::load(root).unwrap();
let mut physics = GamePhysics::load_with_difficulty(root, None, crate::difficulty::Difficulty::Normal).unwrap();
let mut skater = SkaterRuntime::load(root, &graphs, &physics, "normal").unwrap();
let mut controls = PlayerControls::default();
let mut input = crate::input::ControllerInput::default();
+58
View File
@@ -0,0 +1,58 @@
//! Integration with the extracted stock data and the real simulation pipeline.
use super::*;
use crate::difficulty::Difficulty;
#[test]
#[ignore = "requires private stock animation banks and collections"]
fn stock_difficulties_switch_without_resetting_the_player() {
let root = std::env::var_os("SKATE3_ASSET_ROOT").expect("SKATE3_ASSET_ROOT");
let root = std::path::Path::new(&root);
let data = Collections::load(root).unwrap();
let assets = skate_data::GameAssets::load(root).unwrap();
let graphs = crate::graph_runtime::StockGraphs::load(root, &assets).unwrap();
let mut physics = GamePhysics::load_with_difficulty(root, None, Difficulty::Easy).unwrap();
let mut skater = SkaterRuntime::load(root, &graphs, &physics, "easy").unwrap();
let mut controls = PlayerControls::load(root).unwrap();
let mut input = crate::input::ControllerInput::default();
let mut camera = crate::camera::CameraRuntime::load(root).unwrap();
// Check the complete mode/surface product before exercising live switches.
for mode in Difficulty::ALL {
for surface in 1..=5 {
let settings = skater.ground_profiles.select(mode as u32, surface).unwrap();
let key = ground_runtime::surface_key(surface).unwrap();
assert_eq!(settings.wheel_material.dynamic_friction.to_bits(),
data.float("physics_surfaces", key, "WheelDynamicFriction").unwrap().to_bits());
assert_eq!(settings.board().propulsion.mode_speed_changes.map(f32::to_bits),
["MaxPushDVStart", "MaxPushDVEnd"].map(|f| data.float("physics_mode", mode.key(), f).unwrap().to_bits()));
assert_eq!(settings.wobble_amplitude.to_bits(),
data.float("physics_mode", mode.key(), "Hash_5B57F2CCCCEEF430").unwrap().to_bits());
}
}
assert!(skater.ground_profiles.select(5, 1).is_err());
assert!(skater.ground_profiles.select(0, 0).is_err());
for mode in [Difficulty::Easy, Difficulty::Normal, Difficulty::Hardcore, Difficulty::Easy] {
let before = physics.board.bodies().map(|b| (b.rates.position, b.rates.linear_velocity));
let ticks = physics.ticks;
let generation = skater.pose_generation;
physics.set_difficulty(mode);
assert_eq!(physics.ticks, ticks);
assert_eq!(skater.pose_generation, generation);
assert_eq!(physics.board.bodies().map(|b| (b.rates.position, b.rates.linear_velocity)), before);
for _ in 0..60 {
input.sample_raw_for_test(skate_core::input::xbox::XboxState {
buttons: 0, triggers: [0; 2], left: [0; 2], right: [0; 2],
});
let mut actions = input.player_actions();
controls.update(&mut actions, physics.settings.step.simulation.time_step,
physics.settings.input_magnitude_threshold, skater.player_input.physical.scoring.capabilities_204);
controls.publish_gestures(physics.difficulty_index(), skater.player_input.physical.state.state_16);
frame::advance(&mut physics, &mut skater, &mut controls, &graphs, &mut actions, true, &mut camera).unwrap();
assert_eq!(skater.player_input.processed.state_variant_index_2528, mode as u32);
let selected = skater.ground_profiles.select(mode as u32, skater.player_input.processed.surface_mode_2540).unwrap();
assert!(std::sync::Arc::ptr_eq(&selected, &skater.ground_settings));
}
assert_eq!(physics.ticks, ticks + 60);
}
}
@@ -7,7 +7,7 @@ fn stock_skater_startup_builds_the_graphs_and_physical_body_from_the_same_rig()
let root = std::path::Path::new(&root);
let manifest = skate_data::GameAssets::load(root).unwrap();
let graphs = crate::graph_runtime::StockGraphs::load(root, &manifest).unwrap();
let mut physics = GamePhysics::load(root).unwrap();
let mut physics = GamePhysics::load_with_difficulty(root, None, crate::difficulty::Difficulty::Normal).unwrap();
// This regression isolates grounded controls on the original flat fixture.
// The production box is only six metres wide: sustained kickturning leaves
// it at x=-3.32, correctly selecting KnownAir and ForcePhysics2. Separate
@@ -363,7 +363,7 @@ fn stock_wheel_hardness_reaches_ground_force_and_torque() {
let graphs = crate::graph_runtime::StockGraphs::load(root, &manifest).unwrap();
let mut samples = Vec::new();
for hardness in [0.0_f32, 0.7, 1.0] {
let mut physics = GamePhysics::load(root).unwrap();
let mut physics = GamePhysics::load_with_difficulty(root, None, crate::difficulty::Difficulty::Normal).unwrap();
let mut skater = SkaterRuntime::load(root, &graphs, &physics, "normal").unwrap();
let mut controls = PlayerControls::default();
let mut camera = crate::camera::CameraRuntime::load(root).unwrap();
@@ -9,7 +9,7 @@ fn raw_controller_bail_runs_stock_wipeout_for_120_ticks() {
let root = std::path::Path::new(&root);
let assets = skate_data::GameAssets::load(root).unwrap();
let graphs = crate::graph_runtime::StockGraphs::load(root, &assets).unwrap();
let mut physics = GamePhysics::load(root).unwrap();
let mut physics = GamePhysics::load_with_difficulty(root, None, crate::difficulty::Difficulty::Normal).unwrap();
let mut skater = SkaterRuntime::load(root, &graphs, &physics, "normal").unwrap();
let mut controls = PlayerControls::default();
let mut input = crate::input::ControllerInput::default();
@@ -173,7 +173,7 @@ fn authored_checkpoint_reply_runs_teleport_state_and_restores_riding() {
let root = std::path::Path::new(&root);
let assets = skate_data::GameAssets::load(root).unwrap();
let graphs = crate::graph_runtime::StockGraphs::load(root, &assets).unwrap();
let mut physics = GamePhysics::load(root).unwrap();
let mut physics = GamePhysics::load_with_difficulty(root, None, crate::difficulty::Difficulty::Normal).unwrap();
let mut skater = SkaterRuntime::load(root, &graphs, &physics, "normal").unwrap();
let mut controls = PlayerControls::default();
let mut input = crate::input::ControllerInput::default();
+2 -1
View File
@@ -55,7 +55,7 @@ fn verify(
}
if state.captured && state.elapsed > 6. {
let input_report = format!(
"Game integration capture (not a Skate 3 parity verdict)\nExecutable: {}\nPolls: {}\nConsumed batches: {}\nStatus: {:?}\nPacket numbers: {:?}\nActions 64..81 by device: {:?}\nDerived ticks: {}\nIntents: {:?}\nPhysics ticks: {}\nContacts: {}\nBody positions: {:?}\nPhysical pose publications: {}\nGraph animation ticks: {}\nTruck targets: {:?}\nGround speed: {}\nGameplay camera shot: {}\nCamera frame: {:?}\n",
"Game integration capture (not a Skate 3 parity verdict)\nExecutable: {}\nPolls: {}\nConsumed batches: {}\nStatus: {:?}\nPacket numbers: {:?}\nActions 64..81 by device: {:?}\nDerived ticks: {}\nIntents: {:?}\nDifficulty index: {}\nPhysics ticks: {}\nContacts: {}\nBody positions: {:?}\nPhysical pose publications: {}\nGraph animation ticks: {}\nTruck targets: {:?}\nGround speed: {}\nGameplay camera shot: {}\nCamera frame: {:?}\n",
std::env::current_exe()
.map_or_else(|_| "unavailable".into(), |p| p.display().to_string()),
input.publications,
@@ -65,6 +65,7 @@ fn verify(
input.mapped_actions,
controls.ticks,
controls.intents,
physics.difficulty_index(),
physics.ticks,
physics.contact_count,
physics.board.bodies().map(|body| body.rates.position),