use bevy::prelude::*; use crate::{ load_project_settings_with_source, save_project_settings_to_string, PhysicsSettings, ProjectSettings, DEFAULT_PROJECT_PATH, }; /// Runtime copy of physics tuning consumed by `sim` systems. #[derive(Resource, Debug, Clone, Copy)] pub struct SimTuning { pub player_radius: f32, pub player_length: f32, pub eye_offset_stand: f32, pub eye_offset_crouch: f32, pub gravity: f32, pub walk_speed: f32, pub run_speed: f32, pub crouch_speed: f32, pub jump_speed: f32, pub ground_accel: f32, pub air_accel: f32, pub ground_check_dist: f32, pub skin: f32, pub max_slide_iter: u32, pub ground_normal_y: f32, pub coyote_time: f32, pub jump_buffer: f32, } impl SimTuning { pub fn from_physics(physics: &PhysicsSettings) -> Self { Self { player_radius: physics.player_radius, player_length: physics.player_length, eye_offset_stand: physics.eye_offset_stand, eye_offset_crouch: physics.eye_offset_crouch, gravity: physics.gravity, walk_speed: physics.walk_speed, run_speed: physics.run_speed, crouch_speed: physics.crouch_speed, jump_speed: physics.jump_speed, ground_accel: physics.ground_accel, air_accel: physics.air_accel, ground_check_dist: physics.ground_check_dist, skin: physics.skin, max_slide_iter: physics.max_slide_iter, ground_normal_y: physics.ground_normal_y, coyote_time: physics.coyote_time, jump_buffer: physics.jump_buffer, } } } /// Tracks project settings file path and dirty state (editor persists via UI). #[derive(Resource, Debug)] pub struct ProjectSettingsIo { pub path: String, pub dirty: bool, /// Exact source loaded or last saved by this process; collaboration guards hash it. pub loaded_source: Option, } impl Default for ProjectSettingsIo { fn default() -> Self { Self { path: DEFAULT_PROJECT_PATH.into(), dirty: false, loaded_source: None, } } } /// Fired after project settings are saved or reloaded from disk. #[derive(Message, Debug, Clone, Copy, Default)] pub struct ProjectSettingsChanged; pub struct ProjectSettingsPlugin; impl Plugin for ProjectSettingsPlugin { fn build(&self, app: &mut App) { app.init_resource::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() .add_message::() .add_systems(PreStartup, load_settings); } } fn load_settings(mut commands: Commands, mut io: ResMut) { let (settings, source) = load_project_settings_with_source(&io.path); io.loaded_source = source; let tuning = SimTuning::from_physics(&settings.physics); commands.insert_resource(settings); commands.insert_resource(tuning); } /// Refreshes [`SimTuning`] from the current [`ProjectSettings`] resource. pub fn sync_sim_tuning(settings: &ProjectSettings, mut tuning: ResMut) { *tuning = SimTuning::from_physics(&settings.physics); } /// Low-level unconditional persistence for non-editor callers. /// /// The editor uses its guarded authored-file publication path instead so external revisions cannot /// be overwritten. pub fn save_project_settings( settings: &ProjectSettings, io: &mut ProjectSettingsIo, ) -> Result<(), String> { let text = save_project_settings_to_string(settings).map_err(|error| error.to_string())?; std::fs::write(&io.path, &text).map_err(|error| error.to_string())?; io.loaded_source = Some(text); io.dirty = false; Ok(()) }