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:
@@ -0,0 +1,163 @@
|
||||
//! [`LayerRegistry`]: human-readable names for the 32 layers.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{LayerMask, MAX_LAYERS};
|
||||
|
||||
/// Maps layer indices (`0..32`) to project-defined names.
|
||||
///
|
||||
/// A [`LayerMask`] is just bits; the registry is what lets a project, the
|
||||
/// editor, and scripts talk about layer **3** as `"Enemy"` instead of a magic
|
||||
/// number. It is project-level data (serialized with the project, later stages)
|
||||
/// and changing a name never moves an entity between layers — only the label
|
||||
/// changes.
|
||||
///
|
||||
/// Index `0` is seeded with the name `"Default"`, the layer every entity starts
|
||||
/// on (see [`Layer`](super::Layer)). The remaining slots are unnamed until a
|
||||
/// project assigns them.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct LayerRegistry {
|
||||
/// One slot per layer; `None` means the layer has no assigned name.
|
||||
names: Vec<Option<String>>,
|
||||
}
|
||||
|
||||
impl LayerRegistry {
|
||||
/// A registry with only layer 0 named (`"Default"`).
|
||||
pub fn new() -> Self {
|
||||
let mut names = vec![None; MAX_LAYERS as usize];
|
||||
names[0] = Some("Default".to_string());
|
||||
Self { names }
|
||||
}
|
||||
|
||||
/// Assigns `name` to layer `index`.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if `index >= 32`.
|
||||
pub fn set(&mut self, index: u32, name: impl Into<String>) {
|
||||
assert!(
|
||||
index < MAX_LAYERS,
|
||||
"layer index out of range (must be 0..32)"
|
||||
);
|
||||
self.names[index as usize] = Some(name.into());
|
||||
}
|
||||
|
||||
/// Clears the name of layer `index`, leaving it unnamed.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if `index >= 32`.
|
||||
pub fn clear(&mut self, index: u32) {
|
||||
assert!(
|
||||
index < MAX_LAYERS,
|
||||
"layer index out of range (must be 0..32)"
|
||||
);
|
||||
self.names[index as usize] = None;
|
||||
}
|
||||
|
||||
/// The name of layer `index`, or `None` if it is out of range or unnamed.
|
||||
pub fn name(&self, index: u32) -> Option<&str> {
|
||||
self.names.get(index as usize).and_then(|n| n.as_deref())
|
||||
}
|
||||
|
||||
/// The index of the layer named `name`, or `None` if no layer has it.
|
||||
///
|
||||
/// Names are not required to be unique; the lowest matching index wins.
|
||||
pub fn index_of(&self, name: &str) -> Option<u32> {
|
||||
self.names
|
||||
.iter()
|
||||
.position(|n| n.as_deref() == Some(name))
|
||||
.map(|i| i as u32)
|
||||
}
|
||||
|
||||
/// A [`LayerMask`] built from layer names, skipping any that are unknown.
|
||||
///
|
||||
/// Convenient for authoring filters by name, e.g.
|
||||
/// `registry.mask_of(["Player", "NPC"])`.
|
||||
pub fn mask_of<'a, I>(&self, names: I) -> LayerMask
|
||||
where
|
||||
I: IntoIterator<Item = &'a str>,
|
||||
{
|
||||
names.into_iter().filter_map(|n| self.index_of(n)).collect()
|
||||
}
|
||||
|
||||
/// Iterates `(index, name)` for every *named* layer, ascending by index.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (u32, &str)> {
|
||||
self.names
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, n)| n.as_deref().map(|name| (i as u32, name)))
|
||||
}
|
||||
|
||||
/// The number of named layers.
|
||||
pub fn named_count(&self) -> usize {
|
||||
self.names.iter().filter(|n| n.is_some()).count()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LayerRegistry {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_names_only_layer_zero() {
|
||||
let reg = LayerRegistry::new();
|
||||
assert_eq!(reg.name(0), Some("Default"));
|
||||
assert_eq!(reg.name(1), None);
|
||||
assert_eq!(reg.named_count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_and_look_up_by_name() {
|
||||
let mut reg = LayerRegistry::new();
|
||||
reg.set(1, "Player");
|
||||
reg.set(2, "NPC");
|
||||
reg.set(5, "Water");
|
||||
assert_eq!(reg.name(2), Some("NPC"));
|
||||
assert_eq!(reg.index_of("Water"), Some(5));
|
||||
assert_eq!(reg.index_of("Missing"), None);
|
||||
assert_eq!(reg.named_count(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mask_of_names_builds_a_filter() {
|
||||
let mut reg = LayerRegistry::new();
|
||||
reg.set(1, "Player");
|
||||
reg.set(2, "NPC");
|
||||
let mask = reg.mask_of(["Player", "NPC", "Unknown"]);
|
||||
assert_eq!(mask, LayerMask::NONE.with(1).with(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_unsets_a_name() {
|
||||
let mut reg = LayerRegistry::new();
|
||||
reg.set(3, "Trigger");
|
||||
assert_eq!(reg.index_of("Trigger"), Some(3));
|
||||
reg.clear(3);
|
||||
assert_eq!(reg.name(3), None);
|
||||
assert_eq!(reg.index_of("Trigger"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iter_visits_named_layers_in_order() {
|
||||
let mut reg = LayerRegistry::new();
|
||||
reg.set(4, "B");
|
||||
reg.set(2, "A");
|
||||
let pairs: Vec<_> = reg.iter().collect();
|
||||
assert_eq!(pairs, vec![(0, "Default"), (2, "A"), (4, "B")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trips_through_ron() {
|
||||
let mut reg = LayerRegistry::new();
|
||||
reg.set(1, "Player");
|
||||
reg.set(7, "Foliage");
|
||||
let ron = ron::to_string(®).unwrap();
|
||||
let back: LayerRegistry = ron::from_str(&ron).unwrap();
|
||||
assert_eq!(reg, back);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user