//! Reflection / type registry — generic, name-keyed access to components. //! //! The engine's *dual-editable types* principle says every component must be //! readable and writable from the editor, from scripts, and from external tools //! through **one** representation — without each of those callers knowing the //! concrete Rust type. The [`TypeRegistry`] is that bridge. //! //! A type is registered **once** under a stable name: //! //! ``` //! use oxide_engine::reflect::TypeRegistry; //! use oxide_engine::prelude::*; //! //! let mut registry = TypeRegistry::new(); //! registry.register::("Transform"); //! registry.register::("Node"); //! ``` //! //! From then on, any caller holding only the *name* can round-trip the component //! on an entity as RON text — which is all a generic inspector or a script needs: //! //! ``` //! # use oxide_engine::reflect::TypeRegistry; //! # use oxide_engine::prelude::*; //! # let mut registry = TypeRegistry::new(); //! # registry.register::("Transform"); //! let mut scene = Scene::new(); //! let e = scene.spawn("thing", Transform::IDENTITY); //! //! // Read it generically... //! let ron = registry.get_ron(scene.world(), e, "Transform").unwrap(); //! // ...and write it back generically, no concrete type at the call site. //! registry.set_ron(scene.world_mut(), e, "Transform", &ron).unwrap(); //! ``` //! //! That path is **whole-value** reflection (the unit is one component, //! serialized). On top of it, [`register_reflected`](TypeRegistry::register_reflected) //! adds **per-field** reflection — named fields each addressable on their own — //! for types that derive [`Reflect`], which is what a Unity/Godot-style //! inspector needs to render one widget per field. Both coexist: the registry //! addresses *types* by name, while [`Reflect`] addresses *fields* within a //! value. use std::collections::BTreeMap; use hecs::{Component, Entity, World}; use serde::de::DeserializeOwned; use serde::Serialize; /// `#[derive(Reflect)]` — generates the [`Reflect`] impl for a struct's public /// fields. Shares its name with the [`Reflect`] trait (macro vs. type /// namespace), exactly like `serde`'s `Serialize`. pub use oxide_engine_derive::Reflect; /// `#[derive(ReflectEnum)]` — generates the [`ReflectEnum`] impl for a fieldless /// enum, exposing its variant names for inspector dropdowns. pub use oxide_engine_derive::ReflectEnum; /// Errors from generic, name-keyed component access. #[derive(Debug, thiserror::Error)] pub enum ReflectError { /// No type was registered under this name. #[error("no registered type named '{0}'")] UnknownType(String), /// The entity is not live in the world. #[error("entity is not live in this world")] NoSuchEntity, /// The entity is live but does not carry this component. #[error("entity has no component '{0}'")] Missing(String), /// The RON text could not be parsed into the named type. #[error("failed to parse '{type_name}': {message}")] Parse { /// The registered name being parsed. type_name: String, /// The underlying parser message. message: String, }, /// A per-field operation named a field this type does not reflect. #[error("no reflected field named '{0}'")] UnknownField(String), /// A per-field operation targeted a type registered for whole-value access /// only (registered with `register`, not `register_reflected`). #[error("type '{0}' is not field-reflected")] NotReflected(String), /// The RON text could not be parsed into a single field's type. #[error("failed to parse field '{field}': {message}")] FieldParse { /// The field being parsed. field: String, /// The underlying parser message. message: String, }, } /// Per-field reflection generated by `#[derive(Reflect)]`. /// /// Whole-value reflection ([`TypeRegistry::get_ron`] / [`set_ron`]) is enough /// for serialization and scripts, but a Unity/Godot-style inspector needs to /// see *named fields* so it can render one widget per field. `Reflect` /// provides exactly that, without exposing the concrete field types to the /// caller: each field is addressed by name and round-trips as RON (the same /// representation the whole-value path uses). /// /// Implement it with the derive — see /// [`oxide_engine_derive::Reflect`](Reflect) (re-exported here as the /// derive macro of the same name). Only **public** fields are reflected; /// annotate a public field with `#[reflect(skip)]` to exclude it. /// /// [`set_ron`]: TypeRegistry::set_ron pub trait Reflect { /// Static descriptors for every reflected field, in declaration order. fn fields(&self) -> &'static [FieldInfo]; /// Serialize one field's current value to RON, or `None` if no field of /// that name is reflected. fn get_field(&self, name: &str) -> Option; /// Parse `value` (RON) into the named field, replacing it. /// /// # Errors /// [`UnknownField`](ReflectError::UnknownField) if the name isn't a /// reflected field, or [`FieldParse`](ReflectError::FieldParse) if the /// text isn't valid for the field's type. fn set_field(&mut self, name: &str, value: &str) -> Result<(), ReflectError>; } /// A fieldless enum whose variants can be listed by name. /// /// Per-field reflection tells the inspector a field's *type name* but not, for /// an enum-typed field, the set of values it may take. `ReflectEnum` supplies /// that list so the inspector can render a dropdown instead of a free-text RON /// box. Register the enum with /// [`register_enum`](TypeRegistry::register_enum) and the inspector looks its /// variants up by type name. Derive it with `#[derive(ReflectEnum)]` (unit /// variants only). pub trait ReflectEnum { /// The enum's variant names, in declaration order. Each is valid RON for /// the corresponding unit variant, so it round-trips through /// [`get_field`](TypeRegistry::get_field) / [`set_field`](TypeRegistry::set_field). fn variants() -> &'static [&'static str]; } /// A static description of one reflected field. /// /// `type_name` is the field type's *syntactic* spelling (e.g. `"f32"`, /// `"bool"`, `"Vec3"`, `"Handle < Font >"`) as written in the source. A /// generic inspector dispatches a widget on it and falls back to a raw RON /// editor for types it doesn't recognize. #[derive(Debug, Clone, Copy, PartialEq)] pub struct FieldInfo { /// The field's identifier. pub name: &'static str, /// The field type's syntactic name. pub type_name: &'static str, /// `(min, max)` bounds set with `#[reflect(min = X, max = Y)]`. The /// inspector uses this to render a `Slider` for a `f32` field whose value /// is normalized (e.g. metallic / roughness in `0..=1`); for plain numeric /// fields it stays `None` and a `DragValue` is used instead. pub range: Option<(f32, f32)>, } /// Implementation detail of `#[derive(Reflect)]` — serialize a field to RON. /// /// Generated code calls this so it never needs `ron` in scope itself. #[doc(hidden)] pub fn __reflect_to_ron(value: &T) -> Option { ron::to_string(value).ok() } /// Implementation detail of `#[derive(Reflect)]` — parse a field from RON. #[doc(hidden)] pub fn __reflect_from_ron( field: &str, value: &str, ) -> Result { ron::from_str(value).map_err(|err| ReflectError::FieldParse { field: field.to_string(), message: err.to_string(), }) } /// The monomorphized operations for one registered type, stored as plain /// function pointers (the closures capture nothing, so they coerce to `fn`). struct ReflectedType { get_ron: fn(&World, Entity) -> Option, set_ron: fn(&mut World, Entity, &str) -> Result<(), String>, has: fn(&World, Entity) -> bool, remove: fn(&mut World, Entity) -> bool, /// Per-field operations, present only for types registered with /// [`register_reflected`](TypeRegistry::register_reflected) (i.e. `T: /// Reflect`). `None` for whole-value-only types. The registry callers /// guarantee the component is present before invoking `get`/`set`. fields: Option, /// Inserts a `T::default()` on an entity, present only for types registered /// with [`register_addable`](TypeRegistry::register_addable) (i.e. `T: /// Default`). `None` means the type can't be added from a generic "Add /// Component" menu (no zero-arg construction). add_default: Option, } /// The `T: Reflect` field operations, type-erased to function pointers. struct FieldOps { infos: fn(&World, Entity) -> Option<&'static [FieldInfo]>, get: fn(&World, Entity, &str) -> Option, set: fn(&mut World, Entity, &str, &str) -> Result<(), ReflectError>, } /// A registry mapping stable type names to type-erased component operations. /// /// Owned by the app/module system (Stage 5): each module registers the component /// types it introduces, so the editor and scripts can address any of them by /// name. Names are the identity used in serialized data and UI, so they should /// be stable across versions. #[derive(Default)] pub struct TypeRegistry { types: BTreeMap<&'static str, ReflectedType>, /// Variant lists for registered enum types, keyed by the same syntactic /// type name a [`FieldInfo::type_name`] carries, so the inspector can turn /// an enum-typed field into a dropdown. enums: BTreeMap<&'static str, &'static [&'static str]>, } impl TypeRegistry { /// An empty registry. pub fn new() -> Self { Self::default() } /// Registers component type `T` under `name`. /// /// `T` must be an ECS component (`Send + Sync + 'static`) and round-trip /// through `serde`. Re-registering the same name replaces the entry. pub fn register(&mut self, name: &'static str) where T: Component + Serialize + DeserializeOwned, { self.types.insert( name, ReflectedType { get_ron: |world, e| { world .get::<&T>(e) .ok() .and_then(|c| ron::to_string(&*c).ok()) }, set_ron: |world, e, text| { let value: T = ron::from_str(text).map_err(|err| err.to_string())?; // `contains` is checked by the caller, so insert cannot fail // for a missing entity; map defensively all the same. world .insert_one(e, value) .map_err(|_| "entity is not live".to_string()) }, has: |world, e| world.get::<&T>(e).is_ok(), remove: |world, e| world.remove_one::(e).is_ok(), fields: None, add_default: None, }, ); } /// Registers component type `T` with **per-field** reflection in addition /// to whole-value access. /// /// Identical to [`register`](Self::register) but also wires the /// [`Reflect`] field operations, so [`field_infos`](Self::field_infos) / /// [`get_field`](Self::get_field) / [`set_field`](Self::set_field) work for /// this type. This is what lets the editor render a widget per field. Use /// it for any type whose fields should be individually editable; use /// `register` for opaque types edited only as a whole. pub fn register_reflected(&mut self, name: &'static str) where T: Component + Serialize + DeserializeOwned + Reflect, { self.types.insert( name, ReflectedType { get_ron: |world, e| { world .get::<&T>(e) .ok() .and_then(|c| ron::to_string(&*c).ok()) }, set_ron: |world, e, text| { let value: T = ron::from_str(text).map_err(|err| err.to_string())?; world .insert_one(e, value) .map_err(|_| "entity is not live".to_string()) }, has: |world, e| world.get::<&T>(e).is_ok(), remove: |world, e| world.remove_one::(e).is_ok(), fields: Some(FieldOps { infos: |world, e| world.get::<&T>(e).ok().map(|c| c.fields()), get: |world, e, field| world.get::<&T>(e).ok().and_then(|c| c.get_field(field)), set: |world, e, field, ron| { // The registry verifies the component is present before // calling, so this access cannot fail. let mut c = world .get::<&mut T>(e) .expect("component present (checked by caller)"); c.set_field(field, ron) }, }), add_default: None, }, ); } /// Registers a reflected component type that can also be **added from a /// generic "Add Component" menu** — `T` must be `Default`, which supplies /// the value inserted on the entity. /// /// Equivalent to [`register_reflected`](Self::register_reflected) plus a /// zero-arg constructor. Use it for components a user can attach in the /// editor; use `register_reflected` for components that only exist /// implicitly (every entity already has them) or that have no sensible /// default. pub fn register_addable(&mut self, name: &'static str) where T: Component + Serialize + DeserializeOwned + Reflect + Default, { self.register_reflected::(name); if let Some(reflected) = self.types.get_mut(name) { reflected.add_default = Some(|world, e| { let _ = world.insert_one(e, T::default()); }); } } /// Removes the type registered under `name`. Returns whether it existed. pub fn unregister(&mut self, name: &str) -> bool { self.types.remove(name).is_some() } /// Whether a type is registered under `name`. pub fn is_registered(&self, name: &str) -> bool { self.types.contains_key(name) } /// The number of registered types. pub fn len(&self) -> usize { self.types.len() } /// Whether no types are registered. pub fn is_empty(&self) -> bool { self.types.is_empty() } /// The names of every registered type, sorted. pub fn names(&self) -> impl Iterator + '_ { self.types.keys().copied() } /// Serializes the named component on `entity` to RON. /// /// # Errors /// [`UnknownType`](ReflectError::UnknownType) if the name is not registered, /// [`NoSuchEntity`](ReflectError::NoSuchEntity) if the entity is dead, or /// [`Missing`](ReflectError::Missing) if the entity lacks the component. pub fn get_ron( &self, world: &World, entity: Entity, type_name: &str, ) -> Result { let reflected = self.lookup(type_name)?; if !world.contains(entity) { return Err(ReflectError::NoSuchEntity); } (reflected.get_ron)(world, entity) .ok_or_else(|| ReflectError::Missing(type_name.to_string())) } /// Parses `ron` into the named type and writes it onto `entity`, inserting /// the component if absent or replacing it if present. /// /// # Errors /// [`UnknownType`](ReflectError::UnknownType), [`NoSuchEntity`](ReflectError::NoSuchEntity), /// or [`Parse`](ReflectError::Parse) if the text is not valid for the type. pub fn set_ron( &self, world: &mut World, entity: Entity, type_name: &str, ron: &str, ) -> Result<(), ReflectError> { let reflected = self.lookup(type_name)?; if !world.contains(entity) { return Err(ReflectError::NoSuchEntity); } (reflected.set_ron)(world, entity, ron).map_err(|message| ReflectError::Parse { type_name: type_name.to_string(), message, }) } /// Whether `entity` carries the named component. /// /// # Errors /// [`UnknownType`](ReflectError::UnknownType) if the name is not registered. pub fn has( &self, world: &World, entity: Entity, type_name: &str, ) -> Result { let reflected = self.lookup(type_name)?; Ok(world.contains(entity) && (reflected.has)(world, entity)) } /// Removes the named component from `entity`. Returns whether it was present. /// /// # Errors /// [`UnknownType`](ReflectError::UnknownType) if the name is not registered. pub fn remove( &self, world: &mut World, entity: Entity, type_name: &str, ) -> Result { let reflected = self.lookup(type_name)?; if !world.contains(entity) { return Ok(false); } Ok((reflected.remove)(world, entity)) } /// The names of all *registered* component types currently on `entity`, /// sorted. This is what a generic inspector iterates to show every editable /// component without knowing any concrete types. pub fn components_on(&self, world: &World, entity: Entity) -> Vec<&'static str> { if !world.contains(entity) { return Vec::new(); } self.types .iter() .filter(|(_, r)| (r.has)(world, entity)) .map(|(name, _)| *name) .collect() } /// Whether the named type was registered with per-field reflection /// ([`register_reflected`](Self::register_reflected)). pub fn is_reflected(&self, type_name: &str) -> bool { matches!(self.types.get(type_name), Some(r) if r.fields.is_some()) } /// Registers a fieldless enum `E` under `name` (the syntactic type name its /// fields carry), so [`enum_variants`](Self::enum_variants) can list its /// values for an inspector dropdown. Independent of component registration — /// an enum is a field *type*, not a component. pub fn register_enum(&mut self, name: &'static str) where E: ReflectEnum, { self.enums.insert(name, E::variants()); } /// The variant names of an enum type registered with /// [`register_enum`](Self::register_enum), or `None` if the type name isn't /// a registered enum. The inspector renders a dropdown when this is `Some`. pub fn enum_variants(&self, type_name: &str) -> Option<&'static [&'static str]> { self.enums.get(type_name).copied() } /// Whether the named type can be added from a generic "Add Component" menu /// ([`register_addable`](Self::register_addable)). pub fn is_addable(&self, type_name: &str) -> bool { matches!(self.types.get(type_name), Some(r) if r.add_default.is_some()) } /// The names of every addable component type, sorted — what an "Add /// Component" menu lists. pub fn addable_names(&self) -> impl Iterator + '_ { self.types .iter() .filter(|(_, r)| r.add_default.is_some()) .map(|(name, _)| *name) } /// Adds a default-constructed instance of the named component to `entity`, /// if the type is addable and the entity doesn't already carry it. Returns /// whether a component was inserted. /// /// # Errors /// [`UnknownType`](ReflectError::UnknownType) if the name isn't registered, /// or [`NoSuchEntity`](ReflectError::NoSuchEntity) if the entity is dead. pub fn add_default( &self, world: &mut World, entity: Entity, type_name: &str, ) -> Result { let reflected = self.lookup(type_name)?; if !world.contains(entity) { return Err(ReflectError::NoSuchEntity); } let Some(make) = reflected.add_default else { return Ok(false); }; // Don't clobber an existing component — "add" is a no-op if present. if (reflected.has)(world, entity) { return Ok(false); } make(world, entity); Ok(true) } /// The field descriptors of the named component on `entity`. /// /// This is what a generic inspector iterates to render one widget per /// field. Returns [`NotReflected`](ReflectError::NotReflected) for types /// registered for whole-value access only. /// /// # Errors /// [`UnknownType`](ReflectError::UnknownType), /// [`NoSuchEntity`](ReflectError::NoSuchEntity), /// [`NotReflected`](ReflectError::NotReflected), or /// [`Missing`](ReflectError::Missing) if the entity lacks the component. pub fn field_infos( &self, world: &World, entity: Entity, type_name: &str, ) -> Result<&'static [FieldInfo], ReflectError> { let reflected = self.lookup(type_name)?; if !world.contains(entity) { return Err(ReflectError::NoSuchEntity); } let ops = reflected .fields .as_ref() .ok_or_else(|| ReflectError::NotReflected(type_name.to_string()))?; (ops.infos)(world, entity).ok_or_else(|| ReflectError::Missing(type_name.to_string())) } /// Serializes one field of the named component on `entity` to RON. /// /// # Errors /// As [`field_infos`](Self::field_infos), plus /// [`UnknownField`](ReflectError::UnknownField) if the type has no such /// field. pub fn get_field( &self, world: &World, entity: Entity, type_name: &str, field: &str, ) -> Result { let reflected = self.lookup(type_name)?; if !world.contains(entity) { return Err(ReflectError::NoSuchEntity); } let ops = reflected .fields .as_ref() .ok_or_else(|| ReflectError::NotReflected(type_name.to_string()))?; if !(reflected.has)(world, entity) { return Err(ReflectError::Missing(type_name.to_string())); } (ops.get)(world, entity, field).ok_or_else(|| ReflectError::UnknownField(field.to_string())) } /// Parses `ron` into one field of the named component on `entity`. /// /// Only the named field changes; the rest of the component is untouched — /// this is the granularity an inspector edit needs. /// /// # Errors /// As [`field_infos`](Self::field_infos), plus /// [`UnknownField`](ReflectError::UnknownField) or /// [`FieldParse`](ReflectError::FieldParse). pub fn set_field( &self, world: &mut World, entity: Entity, type_name: &str, field: &str, ron: &str, ) -> Result<(), ReflectError> { let reflected = self.lookup(type_name)?; if !world.contains(entity) { return Err(ReflectError::NoSuchEntity); } let ops = reflected .fields .as_ref() .ok_or_else(|| ReflectError::NotReflected(type_name.to_string()))?; if !(reflected.has)(world, entity) { return Err(ReflectError::Missing(type_name.to_string())); } (ops.set)(world, entity, field, ron) } fn lookup(&self, type_name: &str) -> Result<&ReflectedType, ReflectError> { self.types .get(type_name) .ok_or_else(|| ReflectError::UnknownType(type_name.to_string())) } } #[cfg(test)] mod tests { use super::*; use crate::math::{Transform, Vec3}; use crate::scene::{Node, Scene}; use serde::Deserialize; fn registry() -> TypeRegistry { let mut r = TypeRegistry::new(); r.register_reflected::("Transform"); r.register_reflected::("Node"); r } #[test] fn registration_is_listed_and_sorted() { let r = registry(); assert!(r.is_registered("Transform")); assert!(!r.is_registered("Nope")); assert_eq!(r.len(), 2); assert_eq!(r.names().collect::>(), vec!["Node", "Transform"]); } #[test] fn get_then_set_round_trips_generically() { let r = registry(); let mut scene = Scene::new(); let e = scene.spawn( "thing", Transform::from_translation(Vec3::new(1.0, 2.0, 3.0)), ); // Read generically (no Transform type named at this call site beyond the // string), then write it straight back. let ron = r.get_ron(scene.world(), e, "Transform").unwrap(); r.set_ron(scene.world_mut(), e, "Transform", &ron).unwrap(); // The value survived the round trip. let after = scene.local_transform(e).unwrap(); assert!((after.translation - Vec3::new(1.0, 2.0, 3.0)).length() < 1e-6); } #[test] fn set_can_mutate_through_the_text() { let r = registry(); let mut scene = Scene::new(); let e = scene.spawn("thing", Transform::IDENTITY); // Hand-edit the serialized form (as the inspector / a script would) and // apply it. let edited = ron::to_string(&Transform::from_translation(Vec3::new(5.0, 0.0, 0.0))).unwrap(); r.set_ron(scene.world_mut(), e, "Transform", &edited) .unwrap(); assert!((scene.local_transform(e).unwrap().translation.x - 5.0).abs() < 1e-6); } #[test] fn components_on_lists_present_registered_types() { let r = registry(); let mut scene = Scene::new(); let e = scene.spawn("thing", Transform::IDENTITY); // has Node + Transform assert_eq!(r.components_on(scene.world(), e), vec!["Node", "Transform"]); // Removing one drops it from the listing. assert!(r.remove(scene.world_mut(), e, "Transform").unwrap()); assert_eq!(r.components_on(scene.world(), e), vec!["Node"]); assert!(!r.has(scene.world(), e, "Transform").unwrap()); } #[test] fn errors_are_specific() { let r = registry(); let mut scene = Scene::new(); let e = scene.spawn("thing", Transform::IDENTITY); // Unknown type name. assert!(matches!( r.get_ron(scene.world(), e, "Ghost"), Err(ReflectError::UnknownType(_)) )); // Live entity missing the component. scene.world_mut().remove_one::(e).unwrap(); assert!(matches!( r.get_ron(scene.world(), e, "Node"), Err(ReflectError::Missing(_)) )); // Dead entity. let dead = scene.spawn("dead", Transform::IDENTITY); scene.despawn(dead, crate::scene::DespawnPolicy::Recursive); assert!(matches!( r.get_ron(scene.world(), dead, "Transform"), Err(ReflectError::NoSuchEntity) )); // Malformed RON. assert!(matches!( r.set_ron(scene.world_mut(), e, "Transform", "not valid ron"), Err(ReflectError::Parse { .. }) )); } // --- Per-field reflection (`#[derive(Reflect)]`) --- /// A representative component: a mix of field types, a skipped public /// field, and a private field — exercises the derive's selection rules. #[derive(Reflect, Serialize, Deserialize, PartialEq, Debug)] struct Timer { pub repeating: bool, pub duration: f32, pub label: String, #[reflect(skip)] pub elapsed: f32, // Private: never reflected regardless of `skip`. _internal: u32, } impl Timer { fn sample() -> Self { Self { repeating: true, duration: 2.5, label: "tick".to_string(), elapsed: 1.0, _internal: 7, } } } /// A struct whose normalized fields carry slider ranges via the new /// `#[reflect(min, max)]` attribute. The inspector dispatches a `Slider` /// instead of a `DragValue` when both bounds are present. #[derive(Reflect, Serialize, Deserialize)] struct Knobs { #[reflect(min = 0.0, max = 1.0)] pub gain: f32, pub bias: f32, } #[derive(Reflect, Serialize, Deserialize)] struct Wrap(pub i32, pub bool); #[test] fn derive_supports_tuple_structs_with_positional_field_names() { // Tuple-struct field names round-trip as "0", "1", ... — matching // Rust's own positional accessors. Lets one-field newtype components // like `Layer(pub LayerMask)` reflect without a wrapper. let mut w = Wrap(42, false); let names: Vec<_> = w.fields().iter().map(|f| f.name).collect(); assert_eq!(names, ["0", "1"]); assert_eq!(w.get_field("0").as_deref(), Some("42")); w.set_field("1", "true").unwrap(); assert!(w.1); } #[test] fn derive_captures_min_max_attributes_as_field_range() { let k = Knobs { gain: 0.5, bias: 0.0, }; let fields = k.fields(); let gain = fields.iter().find(|f| f.name == "gain").unwrap(); let bias = fields.iter().find(|f| f.name == "bias").unwrap(); assert_eq!(gain.range, Some((0.0_f32, 1.0_f32))); assert_eq!(bias.range, None); } #[test] fn derive_lists_only_public_non_skipped_fields_in_order() { let t = Timer::sample(); let names: Vec<_> = t.fields().iter().map(|f| f.name).collect(); assert_eq!(names, ["repeating", "duration", "label"]); // Syntactic type names are preserved for inspector widget dispatch. let types: Vec<_> = t.fields().iter().map(|f| f.type_name).collect(); assert_eq!(types, ["bool", "f32", "String"]); } #[test] fn derive_gets_each_field_as_ron() { let t = Timer::sample(); assert_eq!(t.get_field("repeating").as_deref(), Some("true")); assert_eq!(t.get_field("duration").as_deref(), Some("2.5")); assert_eq!(t.get_field("label").as_deref(), Some("\"tick\"")); // Skipped + private + unknown all read as None. assert_eq!(t.get_field("elapsed"), None); assert_eq!(t.get_field("_internal"), None); assert_eq!(t.get_field("nope"), None); } #[test] fn derive_sets_a_single_field_without_touching_others() { let mut t = Timer::sample(); t.set_field("duration", "9.0").unwrap(); t.set_field("repeating", "false").unwrap(); assert_eq!(t.duration, 9.0); assert!(!t.repeating); // Other fields are untouched. assert_eq!(t.label, "tick"); assert_eq!(t.elapsed, 1.0); } #[test] fn derive_set_reports_unknown_field_and_parse_errors() { let mut t = Timer::sample(); assert!(matches!( t.set_field("elapsed", "0.0"), // public but skipped → not reflected Err(ReflectError::UnknownField(f)) if f == "elapsed" )); assert!(matches!( t.set_field("missing", "0.0"), Err(ReflectError::UnknownField(_)) )); assert!(matches!( t.set_field("duration", "not a float"), Err(ReflectError::FieldParse { field, .. }) if field == "duration" )); } #[test] fn registry_lists_fields_of_a_reflected_component() { let r = registry(); let mut scene = Scene::new(); let e = scene.spawn("thing", Transform::IDENTITY); assert!(r.is_reflected("Transform")); let names: Vec<_> = r .field_infos(scene.world(), e, "Transform") .unwrap() .iter() .map(|f| f.name) .collect(); assert_eq!(names, ["translation", "rotation", "scale"]); } #[test] fn registry_gets_and_sets_one_field_through_the_world() { let r = registry(); let mut scene = Scene::new(); let e = scene.spawn("thing", Transform::IDENTITY); // Set just the translation; rotation/scale stay identity. glam's Vec3 // serializes as a tuple, so RON is `(1.0,2.0,3.0)`. r.set_field( scene.world_mut(), e, "Transform", "translation", "(1.0, 2.0, 3.0)", ) .unwrap(); let t = scene.world().get::<&Transform>(e).unwrap(); assert_eq!(t.translation, Vec3::new(1.0, 2.0, 3.0)); assert_eq!(t.scale, Vec3::ONE); drop(t); let got = r .get_field(scene.world(), e, "Transform", "translation") .unwrap(); assert_eq!(got, "(1.0,2.0,3.0)"); } #[test] fn registry_field_access_errors_are_specific() { let mut r = registry(); // A whole-value-only type → NotReflected on field access. r.register::("TimerWhole"); let mut scene = Scene::new(); let e = scene.spawn("thing", Transform::IDENTITY); scene.world_mut().insert_one(e, TimerWhole(1)).unwrap(); assert!(!r.is_reflected("TimerWhole")); assert!(matches!( r.field_infos(scene.world(), e, "TimerWhole"), Err(ReflectError::NotReflected(_)) )); // Unknown field on a reflected type. assert!(matches!( r.get_field(scene.world(), e, "Transform", "nope"), Err(ReflectError::UnknownField(_)) )); // Reflected type, but the entity lacks the component. let bare = scene.spawn("bare", Transform::IDENTITY); scene.world_mut().remove_one::(bare).unwrap(); assert!(matches!( r.get_field(scene.world(), bare, "Node", "name"), Err(ReflectError::Missing(_)) )); } #[derive(Serialize, Deserialize)] struct TimerWhole(u32); // --- Enum reflection (`#[derive(ReflectEnum)]`) --- #[derive(ReflectEnum, Serialize, Deserialize, PartialEq, Debug)] enum Facing { North, East, South, West, } #[test] fn derive_enum_lists_variants_in_order() { assert_eq!(Facing::variants(), &["North", "East", "South", "West"]); } #[test] fn variant_names_round_trip_as_ron() { // The names ReflectEnum returns must be valid RON for the variant, so // the inspector can write a chosen name straight back through set_field. for name in Facing::variants() { let value: Facing = ron::from_str(name).unwrap(); assert_eq!(&ron::to_string(&value).unwrap(), name); } } #[test] fn addable_component_can_be_added_by_name_and_listed() { use crate::render::MeshRenderer; let mut r = registry(); r.register_addable::("MeshRenderer"); // Listed as addable; Transform (register_reflected) is not. assert!(r.is_addable("MeshRenderer")); assert!(!r.is_addable("Transform")); let addable: Vec<_> = r.addable_names().collect(); assert_eq!(addable, ["MeshRenderer"]); let mut scene = Scene::new(); let e = scene.spawn("thing", Transform::IDENTITY); assert!(!r.has(scene.world(), e, "MeshRenderer").unwrap()); // First add inserts the default; a second add is a no-op (already there). assert!(r.add_default(scene.world_mut(), e, "MeshRenderer").unwrap()); assert!(r.has(scene.world(), e, "MeshRenderer").unwrap()); assert!(!r.add_default(scene.world_mut(), e, "MeshRenderer").unwrap()); // Its enum field is editable as a registered enum. r.register_enum::("PrimitiveShape"); let shape = r .get_field(scene.world(), e, "MeshRenderer", "shape") .unwrap(); assert_eq!(shape, "Cube"); // PrimitiveShape::default() assert_eq!( r.enum_variants("PrimitiveShape"), Some(["Cube", "Sphere", "Plane"].as_slice()) ); } #[test] fn non_addable_type_add_default_is_a_noop() { let r = registry(); // Transform/Node are register_reflected, not addable let mut scene = Scene::new(); let e = scene.spawn("thing", Transform::IDENTITY); // Transform isn't addable → Ok(false), nothing inserted. assert!(!r.add_default(scene.world_mut(), e, "Transform").unwrap()); // Unknown type → error. assert!(matches!( r.add_default(scene.world_mut(), e, "Ghost"), Err(ReflectError::UnknownType(_)) )); } #[test] fn registry_lists_enum_variants_by_type_name() { let mut r = registry(); r.register_enum::("Facing"); assert_eq!( r.enum_variants("Facing"), Some(["North", "East", "South", "West"].as_slice()) ); // Unregistered / non-enum type names return None. assert_eq!(r.enum_variants("Transform"), None); assert_eq!(r.enum_variants("Nope"), None); } #[test] fn derive_field_values_round_trip_through_get_then_set() { let original = Timer::sample(); let mut clone = Timer { repeating: false, duration: 0.0, label: String::new(), elapsed: 0.0, _internal: 0, }; for field in original.fields() { let ron = original.get_field(field.name).unwrap(); clone.set_field(field.name, &ron).unwrap(); } // Every reflected field now matches; non-reflected fields keep clone's. assert_eq!(clone.repeating, original.repeating); assert_eq!(clone.duration, original.duration); assert_eq!(clone.label, original.label); assert_eq!(clone.elapsed, 0.0); } }