Something went wrong. Try again.
Homebrew 4D Game Engine In Rust, inspired by Engine4D
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413//! Application configuration//!//! Configuration is loaded from multiple sources with the following priority (lowest to highest)://! 1. `config/default.toml` (version controlled)//! 2. `config/user.toml` (gitignored, user overrides)//! 3. Environment variables (`R4D_SECTION__KEY`)
use figment::{ providers::{Env, Format, Toml}, Figment,};use serde::{Deserialize, Serialize};use std::path::Path;
// Re-export PhysicsConfig from the physics crate for conveniencepub use rust4d_physics::PhysicsConfig;
/// Main application configuration#[derive(Debug, Clone, Default, Serialize, Deserialize)]pub struct AppConfig { /// Window configuration #[serde(default)] pub window: WindowConfig, /// Camera configuration #[serde(default)] pub camera: CameraConfig, /// Input configuration #[serde(default)] pub input: InputConfig, /// Physics configuration #[serde(default)] pub physics: PhysicsConfigToml, /// Rendering configuration #[serde(default)] pub rendering: RenderingConfig, /// Debug configuration #[serde(default)] pub debug: DebugConfig, /// Scene configuration #[serde(default)] pub scene: SceneConfig, /// Game / scripting configuration #[serde(default)] pub game: GameConfig, /// Audio configuration #[serde(default)] pub audio: AudioConfig,}
impl AppConfig { /// Load configuration from default locations /// /// Priority (lowest to highest): /// 1. `config/default.toml` /// 2. `config/user.toml` /// 3. Environment variables (`R4D_*`) pub fn load() -> Result<Self, ConfigError> { Self::load_from("config") }
/// Load configuration from a specific config directory pub fn load_from<P: AsRef<Path>>(config_dir: P) -> Result<Self, ConfigError> { let config_dir = config_dir.as_ref(); let default_path = config_dir.join("default.toml"); let user_path = config_dir.join("user.toml");
let mut figment = Figment::new();
// Load default config (required) if default_path.exists() { log::debug!("Loading default config from {:?}", default_path); figment = figment.merge(Toml::file(&default_path)); } else { log::warn!("Default config not found at {:?}", default_path); }
// Load user config (optional) if user_path.exists() { log::info!("Loading user config from {:?}", user_path); figment = figment.merge(Toml::file(&user_path)); } else { log::debug!("No user config at {:?}", user_path); }
// Environment variables override everything // R4D_WINDOW__TITLE=Test -> window.title = "Test" figment = figment.merge(Env::prefixed("R4D_").split("__"));
figment.extract().map_err(ConfigError::from) }}
/// Window configuration#[derive(Debug, Clone, Serialize, Deserialize)]pub struct WindowConfig { /// Window title pub title: String, /// Window width in pixels pub width: u32, /// Window height in pixels pub height: u32, /// Start in fullscreen mode pub fullscreen: bool, /// Enable VSync pub vsync: bool,}
impl Default for WindowConfig { fn default() -> Self { Self { title: "Rust4D - 4D Rendering Engine".to_string(), width: 1280, height: 720, fullscreen: false, vsync: true, } }}
/// Camera configuration#[derive(Debug, Clone, Serialize, Deserialize)]pub struct CameraConfig { /// Starting position [x, y, z, w] pub start_position: [f32; 4], /// Field of view in degrees pub fov: f32, /// Near clipping plane pub near: f32, /// Far clipping plane pub far: f32, /// Maximum pitch angle in degrees pub pitch_limit: f32,}
impl Default for CameraConfig { fn default() -> Self { Self { start_position: [0.0, 0.0, 5.0, 0.0], fov: 45.0, near: 0.1, far: 100.0, pitch_limit: 89.0, } }}
/// Input configuration#[derive(Debug, Clone, Serialize, Deserialize)]pub struct InputConfig { /// Movement speed (units per second) pub move_speed: f32, /// W-axis movement speed (units per second) pub w_move_speed: f32, /// Mouse sensitivity for 3D rotation pub mouse_sensitivity: f32, /// Mouse sensitivity for W rotation pub w_rotation_sensitivity: f32, /// Input smoothing half-life in seconds (lower = more responsive) pub smoothing_half_life: f32, /// Enable input smoothing by default pub smoothing_enabled: bool,}
impl Default for InputConfig { fn default() -> Self { Self { move_speed: 3.0, w_move_speed: 2.0, mouse_sensitivity: 0.002, w_rotation_sensitivity: 0.005, smoothing_half_life: 0.05, smoothing_enabled: false, } }}
/// Physics configuration from TOML////// This wraps the core PhysicsConfig. The `gravity` field is passed to the/// physics engine directly.////// `jump_velocity` lives here (rather than in a `[game]` section) because it is/// a physics-adjacent tunable: users typically adjust it alongside `gravity` to/// get the right feel, and both share the same units (m/s vs m/s^2). Game logic/// reads it from here and passes it per-entity to `CharacterController4D`.////// Note: `player_radius` is in `[scene]` section. Floor positions are defined/// per-scene in .ron files via Hyperplane entities.#[derive(Debug, Clone, Serialize, Deserialize)]pub struct PhysicsConfigToml { /// Gravity (negative = downward) pub gravity: f32, /// Jump velocity (used by game logic, not physics engine) #[serde(default = "PhysicsConfigToml::default_jump_velocity")] pub jump_velocity: f32,}
impl Default for PhysicsConfigToml { fn default() -> Self { Self { gravity: -20.0, jump_velocity: 8.0, } }}
impl PhysicsConfigToml { fn default_jump_velocity() -> f32 { 8.0 }
/// Convert to the physics engine's PhysicsConfig pub fn to_physics_config(&self) -> PhysicsConfig { PhysicsConfig::new(self.gravity) }}
/// Rendering configuration#[derive(Debug, Clone, Serialize, Deserialize)]pub struct RenderingConfig { /// Maximum triangles for slice output pub max_triangles: u32, /// Background color [r, g, b, a] pub background_color: [f32; 4], /// Light direction [x, y, z] pub light_dir: [f32; 3], /// Ambient light strength pub ambient_strength: f32, /// Diffuse light strength pub diffuse_strength: f32, /// W-axis color tinting strength (0.0 = no tint, 1.0 = full tint) pub w_color_strength: f32, /// W-axis distance for full color effect pub w_range: f32,}
impl Default for RenderingConfig { fn default() -> Self { Self { max_triangles: 900_000, background_color: [0.02, 0.02, 0.08, 1.0], light_dir: [0.5, 1.0, 0.3], ambient_strength: 0.3, diffuse_strength: 0.7, w_color_strength: 0.5, w_range: 2.0, } }}
/// Debug configuration#[derive(Debug, Clone, Serialize, Deserialize)]pub struct DebugConfig { /// Show debug overlay pub show_overlay: bool, /// Log level (error, warn, info, debug, trace) pub log_level: String, /// Show physics colliders pub show_colliders: bool,}
impl Default for DebugConfig { fn default() -> Self { Self { show_overlay: false, log_level: "info".to_string(), show_colliders: false, } }}
/// Scene configuration#[derive(Debug, Clone, Serialize, Deserialize)]pub struct SceneConfig { /// Path to the scene file to load pub path: String, /// Player collision radius pub player_radius: f32,}
impl Default for SceneConfig { fn default() -> Self { Self { path: "scenes/default.ron".to_string(), player_radius: 0.5, } }}
/// Game / scripting configuration////// Controls how the engine loads and runs a scripted game directory./// When `game_dir` is empty, scripting is disabled and the engine runs the/// configured scene without Lua callbacks.#[derive(Debug, Clone, Serialize, Deserialize)]pub struct GameConfig { /// Root directory of the active game (e.g. `"games/trivial"`). /// /// Empty string disables scripting entirely (backwards-compatible default). /// Overridden by the `--game <dir>` CLI argument. #[serde(default)] pub game_dir: String, /// Subdirectory within `game_dir` containing Lua modules. /// /// Empty string means modules (and `main.lua`) live at the `game_dir` root. /// A relative path is resolved against `game_dir`; an absolute path is used as-is. #[serde(default)] pub scripts_dir: String, /// Enable hot-reload file watching for Lua scripts (dev only). #[serde(default)] pub hot_reload: bool, /// Whether to attempt initializing the audio engine. /// /// Degrades gracefully to silent operation if no audio device is available. #[serde(default = "default_true")] pub audio_enabled: bool,}
impl Default for GameConfig { fn default() -> Self { Self { game_dir: String::new(), scripts_dir: String::new(), hot_reload: false, audio_enabled: true, } }}
/// Audio bus volume configuration////// Applied to the `AudioEngine4D` at startup if audio is enabled. Volumes are/// in the `0.0..=1.0` range.#[derive(Debug, Clone, Serialize, Deserialize)]pub struct AudioConfig { /// Master bus volume (affects all other buses) #[serde(default = "default_full_volume")] pub master_volume: f32, /// Sound effects bus volume #[serde(default = "default_full_volume")] pub sfx_volume: f32, /// Music bus volume #[serde(default = "default_full_volume")] pub music_volume: f32, /// Ambient bus volume #[serde(default = "default_full_volume")] pub ambient_volume: f32,}
impl Default for AudioConfig { fn default() -> Self { Self { master_volume: 1.0, sfx_volume: 1.0, music_volume: 0.8, ambient_volume: 0.7, } }}
fn default_true() -> bool { true}
fn default_full_volume() -> f32 { 1.0}
/// Configuration error#[derive(Debug)]pub struct ConfigError { message: String,}
impl From<figment::Error> for ConfigError { fn from(e: figment::Error) -> Self { ConfigError { message: e.to_string(), } }}
impl std::fmt::Display for ConfigError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Configuration error: {}", self.message) }}
impl std::error::Error for ConfigError {}
#[cfg(test)]mod tests { use super::*;
#[test] fn test_default_config() { let config = AppConfig::default(); assert_eq!(config.window.width, 1280); assert_eq!(config.physics.gravity, -20.0); assert!(config.game.game_dir.is_empty()); assert!(config.game.audio_enabled); assert_eq!(config.audio.master_volume, 1.0); }
#[test] fn test_config_serialization() { let config = AppConfig::default(); let toml = toml::to_string(&config).unwrap(); assert!(toml.contains("title")); assert!(toml.contains("gravity")); }}