Import Oxide engine (Stages 0–10) under MIT license

Full project snapshot migrated to new Gitea remote without history:
engine, editor, physics, script, examples, tests, docs, and assets.
Relicensed from GPLv3 to MIT and updated repo URLs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Homer Simpson
2026-07-05 20:41:02 +02:00
parent 2afb56b329
commit f56a1eea3b
128 changed files with 40493 additions and 2 deletions
+273
View File
@@ -0,0 +1,273 @@
//! The settings / preferences framework.
//!
//! A unified, serialized configuration store shared by the engine, the editor,
//! and modules. Each contributor registers a typed **section** (a plain
//! `serde`-serializable struct) under a name; the framework persists every
//! section to RON and restores it, without any central code knowing the
//! sections' shapes. This is what lets:
//!
//! - **engine** preferences (render/quality defaults),
//! - **editor** preferences (theme, layout, shortcuts), and
//! - **per-module** settings (each module's own options)
//!
//! all live in one place, while a [`Project`](crate::project::Project) persists
//! the per-project subset (it stores section → RON blobs that line up exactly
//! with [`Settings::export`]/[`Settings::import`]).
//!
//! ```
//! use oxide_engine::settings::Settings;
//! use serde::{Serialize, Deserialize};
//!
//! #[derive(Serialize, Deserialize, Default, PartialEq, Debug)]
//! struct EditorPrefs { theme: String, grid: bool }
//!
//! let mut settings = Settings::new();
//! settings.register::<EditorPrefs>("editor");
//! settings.get_mut::<EditorPrefs>("editor").unwrap().theme = "dark".into();
//!
//! // Persist every section to RON, and restore it later.
//! let saved = settings.export();
//! let mut restored = Settings::new();
//! restored.register::<EditorPrefs>("editor");
//! restored.import(&saved);
//! assert_eq!(restored.get::<EditorPrefs>("editor").unwrap().theme, "dark");
//! ```
use std::any::Any;
use std::collections::BTreeMap;
use serde::de::DeserializeOwned;
use serde::Serialize;
/// The monomorphized operations for one registered section, as plain function
/// pointers (the closures capture nothing).
struct SectionOps {
value: Box<dyn Any>,
to_ron: fn(&dyn Any) -> Option<String>,
from_ron: fn(&str) -> Option<Box<dyn Any>>,
default: fn() -> Box<dyn Any>,
}
/// A registry of typed, serializable settings sections keyed by name.
#[derive(Default)]
pub struct Settings {
sections: BTreeMap<&'static str, SectionOps>,
}
impl Settings {
/// An empty settings store.
pub fn new() -> Self {
Self::default()
}
/// Registers section type `T` under `name`, initialized to `T::default()`.
/// Re-registering the same name resets it to default.
pub fn register<T>(&mut self, name: &'static str)
where
T: Serialize + DeserializeOwned + Default + 'static,
{
self.sections.insert(
name,
SectionOps {
value: Box::new(T::default()),
to_ron: |any| any.downcast_ref::<T>().and_then(|v| ron::to_string(v).ok()),
from_ron: |text| {
ron::from_str::<T>(text)
.ok()
.map(|v| Box::new(v) as Box<dyn Any>)
},
default: || Box::new(T::default()) as Box<dyn Any>,
},
);
}
/// Whether a section is registered under `name`.
pub fn is_registered(&self, name: &str) -> bool {
self.sections.contains_key(name)
}
/// The registered section names, sorted.
pub fn names(&self) -> impl Iterator<Item = &'static str> + '_ {
self.sections.keys().copied()
}
/// Borrows section `name` as `T`, or `None` if absent or the type mismatches.
pub fn get<T: 'static>(&self, name: &str) -> Option<&T> {
self.sections.get(name)?.value.downcast_ref::<T>()
}
/// Mutably borrows section `name` as `T`.
pub fn get_mut<T: 'static>(&mut self, name: &str) -> Option<&mut T> {
self.sections.get_mut(name)?.value.downcast_mut::<T>()
}
/// Replaces the value of section `name`. Returns whether it was registered
/// (with a matching type).
pub fn set<T: 'static>(&mut self, name: &str, value: T) -> bool {
match self.sections.get_mut(name) {
// Only overwrite if the registered type matches.
Some(section) if section.value.is::<T>() => {
section.value = Box::new(value);
true
}
_ => false,
}
}
/// Resets section `name` to its default. Returns whether it was registered.
pub fn reset(&mut self, name: &str) -> bool {
match self.sections.get_mut(name) {
Some(section) => {
section.value = (section.default)();
true
}
None => false,
}
}
/// Serializes section `name` to RON, or `None` if it is not registered.
pub fn section_ron(&self, name: &str) -> Option<String> {
let section = self.sections.get(name)?;
(section.to_ron)(section.value.as_ref())
}
/// Loads section `name` from a RON blob, replacing its value. Returns `false`
/// if the section is not registered or the text fails to parse.
pub fn load_section(&mut self, name: &str, ron: &str) -> bool {
match self.sections.get_mut(name) {
Some(section) => match (section.from_ron)(ron) {
Some(value) => {
section.value = value;
true
}
None => false,
},
None => false,
}
}
/// Serializes every section to a `name → RON` map (the format a
/// [`Project`](crate::project::Project) stores).
pub fn export(&self) -> BTreeMap<String, String> {
self.sections
.iter()
.filter_map(|(name, section)| {
(section.to_ron)(section.value.as_ref()).map(|ron| (name.to_string(), ron))
})
.collect()
}
/// Loads every matching, registered section from a `name → RON` map.
/// Unknown sections are ignored (a module may be disabled); malformed
/// sections are skipped, leaving their current value.
pub fn import(&mut self, map: &BTreeMap<String, String>) {
for (name, ron) in map {
self.load_section(name, ron);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Default, PartialEq, Debug)]
struct EditorPrefs {
theme: String,
grid: bool,
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Render {
shadows: bool,
msaa: u32,
}
impl Default for Render {
fn default() -> Self {
Self {
shadows: true,
msaa: 4,
}
}
}
fn settings() -> Settings {
let mut s = Settings::new();
s.register::<EditorPrefs>("editor");
s.register::<Render>("render");
s
}
#[test]
fn defaults_and_typed_access() {
let mut s = settings();
assert_eq!(s.names().collect::<Vec<_>>(), vec!["editor", "render"]);
assert!(s.get::<Render>("render").unwrap().shadows);
s.get_mut::<EditorPrefs>("editor").unwrap().theme = "dark".into();
assert_eq!(s.get::<EditorPrefs>("editor").unwrap().theme, "dark");
// Wrong type → None.
assert!(s.get::<Render>("editor").is_none());
}
#[test]
fn set_and_reset() {
let mut s = settings();
assert!(s.set(
"render",
Render {
shadows: false,
msaa: 8
}
));
assert_eq!(s.get::<Render>("render").unwrap().msaa, 8);
// Setting an unregistered section fails.
assert!(!s.set("missing", 5u32));
// Reset returns to default.
assert!(s.reset("render"));
assert_eq!(s.get::<Render>("render").unwrap(), &Render::default());
}
#[test]
fn export_import_round_trips() {
let mut s = settings();
s.get_mut::<EditorPrefs>("editor").unwrap().theme = "light".into();
s.get_mut::<EditorPrefs>("editor").unwrap().grid = true;
s.set(
"render",
Render {
shadows: false,
msaa: 2,
},
);
let saved = s.export();
// A fresh store with the same sections restores the saved values.
let mut restored = settings();
restored.import(&saved);
assert_eq!(
restored.get::<EditorPrefs>("editor").unwrap(),
&EditorPrefs {
theme: "light".into(),
grid: true
}
);
assert_eq!(restored.get::<Render>("render").unwrap().msaa, 2);
}
#[test]
fn import_ignores_unknown_and_malformed() {
let mut s = settings();
let mut map = BTreeMap::new();
map.insert("editor".to_string(), "(theme:\"x\",grid:true)".to_string());
map.insert("disabled_module".to_string(), "(whatever:1)".to_string());
map.insert("render".to_string(), "not valid ron".to_string());
s.import(&map);
// Known + valid applied.
assert_eq!(s.get::<EditorPrefs>("editor").unwrap().theme, "x");
// Malformed left the section at its default (unchanged).
assert_eq!(s.get::<Render>("render").unwrap(), &Render::default());
// Unknown silently ignored.
assert!(!s.is_registered("disabled_module"));
}
}