Blacksite/crates/settings/src/lib.rs
Rbanh f29712d158
Some checks are pending
CI / Format, lint, test, build (push) Waiting to run
Initial project import
2026-06-05 21:44:45 -04:00

265 lines
8.2 KiB
Rust

//! Serializable project settings loaded from `assets/project.ron`.
mod plugin;
mod rendering;
pub use plugin::{
save_project_settings, sync_sim_tuning, ProjectSettingsChanged, ProjectSettingsIo,
ProjectSettingsPlugin, SimTuning,
};
pub use rendering::{
effective_auto_exposure, resolve_effective_render_stack, resolve_gi_path,
write_effective_render_stack, ActiveCameraRenderProfile, ActiveVolumeContribution,
EffectiveRenderStack, ExposureMode, GiMode, GiPath, RenderFallbackReason,
RenderingCapabilities, VolumeContribution,
};
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
pub const DEFAULT_PROJECT_PATH: &str = "assets/project.ron";
pub const SETTINGS_VERSION: u32 = 1;
/// Marker for cameras that receive the shared project rendering stack.
#[derive(Component, Debug, Clone, Copy, Default)]
pub struct ProjectRenderCamera;
/// Root project manifest and tunable sections.
#[derive(Resource, Debug, Clone, Serialize, Deserialize, Reflect)]
#[reflect(Resource, Clone)]
pub struct ProjectSettings {
pub version: u32,
pub name: String,
pub default_level: String,
pub asset_roots: Vec<String>,
pub rendering: RenderingSettings,
pub physics: PhysicsSettings,
pub input: InputSettings,
}
impl Default for ProjectSettings {
fn default() -> Self {
Self {
version: SETTINGS_VERSION,
name: "Bevy FPS Foundation".into(),
default_level: "assets/levels/editor_scene.scn.ron".into(),
asset_roots: vec![
"assets/models".into(),
"assets/textures".into(),
"assets/materials".into(),
"assets/levels".into(),
],
rendering: RenderingSettings::default(),
physics: PhysicsSettings::default(),
input: InputSettings::default(),
}
}
}
/// Camera post-processing and world lighting defaults.
#[derive(Debug, Clone, Serialize, Deserialize, Reflect)]
#[reflect(Clone)]
pub struct RenderingSettings {
#[serde(default)]
pub gi_mode: GiMode,
pub hdr: bool,
#[serde(default)]
pub exposure_mode: ExposureMode,
pub exposure_ev100: f32,
#[serde(default = "default_auto_exposure_speed_brighten")]
pub auto_exposure_speed_brighten: f32,
#[serde(default = "default_auto_exposure_speed_darken")]
pub auto_exposure_speed_darken: f32,
pub tonemapping_aces: bool,
pub bloom: bool,
pub taa: bool,
pub ssao: bool,
pub atmosphere: bool,
pub fog_color: [f32; 3],
pub fog_density: f32,
pub ambient_color: [f32; 3],
pub ambient_brightness: f32,
pub sun_illuminance: f32,
pub shadow_cascades: u32,
pub shadow_first_cascade: f32,
pub shadow_max_distance: f32,
}
fn default_auto_exposure_speed_brighten() -> f32 {
3.0
}
fn default_auto_exposure_speed_darken() -> f32 {
1.0
}
impl Default for RenderingSettings {
fn default() -> Self {
Self {
gi_mode: GiMode::default(),
hdr: true,
exposure_mode: ExposureMode::default(),
exposure_ev100: 13.0,
auto_exposure_speed_brighten: default_auto_exposure_speed_brighten(),
auto_exposure_speed_darken: default_auto_exposure_speed_darken(),
tonemapping_aces: true,
bloom: true,
taa: true,
ssao: true,
atmosphere: true,
fog_color: [0.66, 0.75, 0.86],
fog_density: 0.0012,
ambient_color: [0.6, 0.7, 0.85],
ambient_brightness: 60.0,
sun_illuminance: 100_000.0,
shadow_cascades: 4,
shadow_first_cascade: 6.0,
shadow_max_distance: 220.0,
}
}
}
/// Gameplay movement and capsule constants (mirrors former `sim::tuning`).
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Reflect)]
#[reflect(Clone)]
pub struct PhysicsSettings {
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 Default for PhysicsSettings {
fn default() -> Self {
Self {
player_radius: 0.4,
player_length: 1.0,
eye_offset_stand: 0.6,
eye_offset_crouch: 0.1,
gravity: 22.0,
walk_speed: 6.0,
run_speed: 9.5,
crouch_speed: 3.0,
jump_speed: 7.5,
ground_accel: 70.0,
air_accel: 16.0,
ground_check_dist: 0.2,
skin: 0.02,
max_slide_iter: 4,
ground_normal_y: 0.5,
coyote_time: 0.12,
jump_buffer: 0.12,
}
}
}
/// Input sensitivities and editor fly camera speeds.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Reflect)]
#[reflect(Clone)]
pub struct InputSettings {
pub yaw_sensitivity: f32,
pub pitch_sensitivity: f32,
pub editor_fly_speed: f32,
pub editor_look_sensitivity: f32,
pub editor_pan_speed: f32,
}
impl Default for InputSettings {
fn default() -> Self {
Self {
yaw_sensitivity: 0.003,
pitch_sensitivity: 0.0022,
editor_fly_speed: 14.0,
editor_look_sensitivity: 0.003,
editor_pan_speed: 0.015,
}
}
}
/// Loads settings from disk, falling back to defaults.
pub fn load_project_settings_from_path(path: &str) -> ProjectSettings {
match std::fs::read_to_string(path) {
Ok(contents) => match ron::from_str(&contents) {
Ok(settings) => settings,
Err(error) => {
warn!("Failed to parse {path}: {error}; using defaults");
ProjectSettings::default()
}
},
Err(error) => {
warn!("Could not read {path}: {error}; using defaults");
ProjectSettings::default()
}
}
}
/// Serializes settings to RON.
pub fn save_project_settings_to_string(settings: &ProjectSettings) -> Result<String, ron::Error> {
ron::ser::to_string_pretty(settings, ron::ser::PrettyConfig::default())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn settings_round_trip() {
let settings = ProjectSettings::default();
let text = save_project_settings_to_string(&settings).expect("serialize");
let parsed: ProjectSettings = ron::from_str(&text).expect("deserialize");
assert_eq!(parsed.name, settings.name);
assert_eq!(parsed.physics.walk_speed, settings.physics.walk_speed);
assert_eq!(parsed.rendering.gi_mode, settings.rendering.gi_mode);
}
#[test]
fn gi_mode_defaults_auto() {
assert_eq!(RenderingSettings::default().gi_mode, GiMode::Auto);
}
#[test]
fn exposure_mode_auto_round_trip() {
let mut settings = ProjectSettings::default();
settings.rendering.exposure_mode = ExposureMode::Auto;
settings.rendering.auto_exposure_speed_brighten = 4.0;
settings.rendering.auto_exposure_speed_darken = 0.5;
let text = save_project_settings_to_string(&settings).expect("serialize");
let parsed: ProjectSettings = ron::from_str(&text).expect("deserialize");
assert_eq!(parsed.rendering.exposure_mode, ExposureMode::Auto);
assert_eq!(parsed.rendering.auto_exposure_speed_brighten, 4.0);
assert_eq!(parsed.rendering.auto_exposure_speed_darken, 0.5);
}
#[test]
fn effective_auto_exposure_requires_hdr_and_compute() {
let mut profile = ActiveCameraRenderProfile::default();
profile.exposure_mode = ExposureMode::Auto;
profile.hdr = true;
let caps = RenderingCapabilities {
auto_exposure_supported: true,
..Default::default()
};
assert!(effective_auto_exposure(&profile, &caps));
profile.hdr = false;
assert!(!effective_auto_exposure(&profile, &caps));
profile.hdr = true;
let caps = RenderingCapabilities::default();
assert!(!effective_auto_exposure(&profile, &caps));
}
}