use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
use ponko_ecs::{Component, ComponentId, Entity, World};
use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ReflectError {
UnknownComponent(String),
NoSuchEntity(Entity),
MissingComponent {
entity: Entity,
component: String,
},
BadValue {
component: String,
message: String,
},
UnknownResource(String),
MissingResource(String),
}
impl fmt::Display for ReflectError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnknownComponent(name) => write!(
f,
"no reflected component named `{name}` (register it with `world.reflect::<T>()`)"
),
Self::NoSuchEntity(e) => write!(f, "entity {} does not exist", e.to_bits()),
Self::MissingComponent { entity, component } => {
write!(f, "entity {} has no `{component}`", entity.to_bits())
}
Self::BadValue { component, message } => write!(f, "bad value for `{component}`: {message}"),
Self::UnknownResource(name) => write!(
f,
"no reflected resource named `{name}` (register it with `world.reflect_resource::<T>()`)"
),
Self::MissingResource(name) => write!(f, "resource `{name}` is not in the world"),
}
}
}
impl std::error::Error for ReflectError {}
type GetFn = Arc<dyn Fn(&World, Entity) -> Option<Result<Value, String>> + Send + Sync>;
type SetFn = Arc<dyn Fn(&mut World, Entity, Value) -> Result<(), String> + Send + Sync>;
type SpawnFn = Arc<dyn Fn(&mut World, Value) -> Result<Entity, String> + Send + Sync>;
type RemoveFn = Arc<dyn Fn(&mut World, Entity) -> bool + Send + Sync>;
type CheckFn = Arc<dyn Fn(&Value) -> Result<(), String> + Send + Sync>;
type SpawnAtFn = Arc<dyn Fn(&mut World, Entity, Value) -> Result<bool, String> + Send + Sync>;
const SAVE_FORMAT: &str = "crabby-world";
const SAVE_VERSION: u64 = 1;
#[derive(Clone)]
struct Reflected {
name: String,
type_name: String,
get: GetFn,
set: SetFn,
spawn: SpawnFn,
spawn_at: SpawnAtFn,
check: CheckFn,
remove: RemoveFn,
}
type ResGetFn = Arc<dyn Fn(&World) -> Option<Result<Value, String>> + Send + Sync>;
type ResSetFn = Arc<dyn Fn(&mut World, Value) -> Result<(), String> + Send + Sync>;
#[derive(Clone)]
struct ReflectedResource {
name: String,
type_name: String,
get: ResGetFn,
set: ResSetFn,
check: CheckFn,
}
#[derive(Default)]
struct Registry {
list: Vec<Reflected>,
by_name: HashMap<String, usize>,
resources: Vec<ReflectedResource>,
resource_by_name: HashMap<String, usize>,
layouts: HashMap<ComponentId, Arc<FieldLayout>>,
}
pub(crate) fn raw_name(world: &World, id: ComponentId) -> Option<&str> {
Some(registry(world)?.layouts.get(&id)?.component.as_str())
}
pub(crate) unsafe fn raw_json(world: &World, id: ComponentId, p: *const u8) -> Option<(&str, Value)> {
let layout = registry(world)?.layouts.get(&id)?;
Some((layout.component.as_str(), unsafe { layout.read(p) }))
}
fn get_resource<R: Send + Sync + 'static + Serialize>(world: &World) -> Option<Result<Value, String>> {
world
.get_resource::<R>()
.map(|v| serde_json::to_value(v).map_err(|err| err.to_string()))
}
fn set_resource<R: Send + Sync + 'static + DeserializeOwned>(world: &mut World, value: Value) -> Result<(), String> {
let value: R = serde_json::from_value(value).map_err(|err| err.to_string())?;
world.insert_resource(value);
Ok(())
}
fn get<T: Component + Serialize>(world: &World, e: Entity) -> Option<Result<Value, String>> {
world
.get::<T>(e)
.map(|v| serde_json::to_value(v).map_err(|err| err.to_string()))
}
fn set<T: Component + DeserializeOwned>(world: &mut World, e: Entity, value: Value) -> Result<(), String> {
let value: T = serde_json::from_value(value).map_err(|err| err.to_string())?;
world.insert(e, value);
Ok(())
}
fn spawn<T: Component + DeserializeOwned>(world: &mut World, value: Value) -> Result<Entity, String> {
let value: T = serde_json::from_value(value).map_err(|err| err.to_string())?;
Ok(world.spawn((value,)))
}
fn spawn_at<T: Component + DeserializeOwned>(world: &mut World, e: Entity, value: Value) -> Result<bool, String> {
let value: T = serde_json::from_value(value).map_err(|err| err.to_string())?;
Ok(world.spawn_at(e, (value,)))
}
fn check<T: DeserializeOwned>(value: &Value) -> Result<(), String> {
T::deserialize(value).map(drop).map_err(|err| err.to_string())
}
fn remove<T: Component>(world: &mut World, e: Entity) -> bool {
world.remove::<T>(e).is_some()
}
pub(crate) fn short_name(type_name: &str) -> String {
let end = type_name.find('<').unwrap_or(type_name.len());
let start = type_name[..end].rfind("::").map_or(0, |i| i + 2);
type_name[start..].to_string()
}
fn merge(base: &mut Value, patch: Value) {
match (base, patch) {
(Value::Object(base), Value::Object(patch)) => {
for (k, v) in patch {
match base.get_mut(&k) {
Some(slot) => merge(slot, v),
None => {
base.insert(k, v);
}
}
}
}
(base, patch) => *base = patch,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u32)]
#[non_exhaustive]
pub enum FieldType {
F32,
F64,
I8,
I16,
I32,
I64,
U8,
U16,
U32,
U64,
Bool,
Vec2,
Color,
Entity,
}
impl FieldType {
#[must_use]
pub fn size(self) -> usize {
match self {
Self::I8 | Self::U8 | Self::Bool => 1,
Self::I16 | Self::U16 => 2,
Self::F32 | Self::I32 | Self::U32 => 4,
Self::F64 | Self::I64 | Self::U64 | Self::Entity | Self::Vec2 => 8,
Self::Color => 16,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Field {
pub name: String,
pub ty: FieldType,
pub offset: usize,
}
impl Field {
pub fn new(name: impl Into<String>, ty: FieldType, offset: usize) -> Self {
Self {
name: name.into(),
ty,
offset,
}
}
}
struct FieldLayout {
component: String,
size: usize,
fields: Vec<Field>,
}
macro_rules! num {
($p:expr, $t:ty) => {
Value::from(unsafe { $p.cast::<$t>().read_unaligned() })
};
}
impl FieldLayout {
#[track_caller]
fn new(component: &str, size: usize, fields: &[Field]) -> Self {
let mut names = std::collections::HashSet::new();
for f in fields {
assert!(
f.offset.checked_add(f.ty.size()).is_some_and(|end| end <= size),
"World::reflect_fields: field `{}` of `{component}` (offset {}, {} bytes) doesn't fit in {size} bytes",
f.name,
f.offset,
f.ty.size()
);
assert!(
names.insert(f.name.as_str()),
"World::reflect_fields: `{component}` has two fields named `{}`",
f.name
);
}
Self {
component: component.to_string(),
size,
fields: fields.to_vec(),
}
}
unsafe fn read(&self, p: *const u8) -> Value {
let mut out = serde_json::Map::with_capacity(self.fields.len());
for f in &self.fields {
let p = unsafe { p.add(f.offset) };
let v = match f.ty {
FieldType::F32 => num!(p, f32),
FieldType::F64 => num!(p, f64),
FieldType::I8 => num!(p, i8),
FieldType::I16 => num!(p, i16),
FieldType::I32 => num!(p, i32),
FieldType::I64 => num!(p, i64),
FieldType::U8 => num!(p, u8),
FieldType::U16 => num!(p, u16),
FieldType::U32 => num!(p, u32),
FieldType::U64 | FieldType::Entity => num!(p, u64),
FieldType::Bool => Value::from(unsafe { p.read() } != 0),
FieldType::Vec2 => {
let [x, y] = [0, 4].map(|o| unsafe { p.add(o).cast::<f32>().read_unaligned() });
serde_json::json!([x, y])
}
FieldType::Color => {
let [r, g, b, a] = [0, 4, 8, 12].map(|o| unsafe { p.add(o).cast::<f32>().read_unaligned() });
serde_json::json!({ "r": r, "g": g, "b": b, "a": a })
}
};
out.insert(f.name.clone(), v);
}
Value::Object(out)
}
fn bytes(&self, current: Option<*mut u8>, value: &Value) -> Result<Vec<u8>, String> {
let Value::Object(map) = value else {
return Err(format!("expected an object with the fields of `{}`", self.component));
};
let mut bytes = vec![0u8; self.size];
if let Some(p) = current {
unsafe { std::ptr::copy_nonoverlapping(p, bytes.as_mut_ptr(), self.size) };
}
for (key, v) in map {
let Some(f) = self.fields.iter().find(|f| f.name == *key) else {
let known: Vec<&str> = self.fields.iter().map(|f| f.name.as_str()).collect();
return Err(format!(
"`{}` has no field `{key}` (fields: {})",
self.component,
known.join(", ")
));
};
let at = &mut bytes[f.offset..f.offset + f.ty.size()];
write_field(f, v, at).map_err(|e| format!("field `{key}`: {e}"))?;
}
Ok(bytes)
}
}
fn write_field(f: &Field, v: &Value, at: &mut [u8]) -> Result<(), String> {
fn int<T: TryFrom<i64> + TryFrom<u64>>(v: &Value) -> Result<T, String> {
let bad = || format!("expected an integer that fits, got {v}");
match (v.as_i64(), v.as_u64()) {
(_, Some(u)) => T::try_from(u).map_err(|_| bad()),
(Some(i), _) => T::try_from(i).map_err(|_| bad()),
_ => Err(bad()),
}
}
fn float(v: &Value) -> Result<f64, String> {
v.as_f64().ok_or_else(|| format!("expected a number, got {v}"))
}
match f.ty {
FieldType::F32 => at.copy_from_slice(&(float(v)? as f32).to_ne_bytes()),
FieldType::F64 => at.copy_from_slice(&float(v)?.to_ne_bytes()),
FieldType::I8 => at.copy_from_slice(&int::<i8>(v)?.to_ne_bytes()),
FieldType::I16 => at.copy_from_slice(&int::<i16>(v)?.to_ne_bytes()),
FieldType::I32 => at.copy_from_slice(&int::<i32>(v)?.to_ne_bytes()),
FieldType::I64 => at.copy_from_slice(&int::<i64>(v)?.to_ne_bytes()),
FieldType::U8 => at.copy_from_slice(&int::<u8>(v)?.to_ne_bytes()),
FieldType::U16 => at.copy_from_slice(&int::<u16>(v)?.to_ne_bytes()),
FieldType::U32 => at.copy_from_slice(&int::<u32>(v)?.to_ne_bytes()),
FieldType::U64 | FieldType::Entity => at.copy_from_slice(&int::<u64>(v)?.to_ne_bytes()),
FieldType::Bool => at[0] = v.as_bool().ok_or_else(|| format!("expected true or false, got {v}"))? as u8,
FieldType::Vec2 => {
let xy = v
.as_array()
.filter(|a| a.len() == 2)
.ok_or_else(|| format!("expected [x, y], got {v}"))?;
for (i, c) in xy.iter().enumerate() {
at[i * 4..i * 4 + 4].copy_from_slice(&(float(c)? as f32).to_ne_bytes());
}
}
FieldType::Color => {
let Value::Object(rgba) = v else {
return Err(format!("expected {{\"r\", \"g\", \"b\", \"a\"}}, got {v}"));
};
for (i, k) in ["r", "g", "b", "a"].iter().enumerate() {
if let Some(c) = rgba.get(*k) {
at[i * 4..i * 4 + 4].copy_from_slice(&(float(c)? as f32).to_ne_bytes());
}
}
if let Some(k) = rgba.keys().find(|k| !["r", "g", "b", "a"].contains(&k.as_str())) {
return Err(format!("colors have r, g, b and a, not `{k}`"));
}
}
}
Ok(())
}
pub trait WorldReflect {
fn reflect<T: Component + Serialize + DeserializeOwned>(&mut self);
unsafe fn reflect_fields(&mut self, id: ComponentId, fields: &[Field]);
fn reflected_components(&self) -> Vec<&str>;
fn get_json(&self, e: Entity, name: &str) -> Result<Value, ReflectError>;
fn set_json(&mut self, e: Entity, name: &str, value: Value) -> Result<(), ReflectError>;
fn remove_json(&mut self, e: Entity, name: &str) -> Result<bool, ReflectError>;
fn entity_json(&self, e: Entity) -> Result<Value, ReflectError>;
fn spawn_json(&mut self, components: Value) -> Result<Entity, ReflectError>;
fn save(&self) -> Value;
fn load(&mut self, save: &Value) -> Result<(), ReflectError>;
fn reflect_resource<R: Send + Sync + 'static + Serialize + DeserializeOwned>(&mut self);
fn reflected_resources(&self) -> Vec<&str>;
fn resource_json(&self, name: &str) -> Result<Value, ReflectError>;
fn set_resource_json(&mut self, name: &str, value: Value) -> Result<(), ReflectError>;
unsafe fn reflect_resource_fields(&mut self, name: &str, layout: std::alloc::Layout, fields: &[Field]);
}
fn find_resource(world: &World, name: &str) -> Result<ReflectedResource, ReflectError> {
registry(world)
.and_then(|r| r.resource_by_name.get(name).map(|&i| r.resources[i].clone()))
.ok_or_else(|| ReflectError::UnknownResource(name.to_string()))
}
fn registry(world: &World) -> Option<&Registry> {
world.get_resource::<Registry>()
}
fn find(world: &World, name: &str) -> Result<Reflected, ReflectError> {
registry(world)
.and_then(|r| r.by_name.get(name).map(|&i| r.list[i].clone()))
.ok_or_else(|| ReflectError::UnknownComponent(name.to_string()))
}
fn bad_value(component: &str) -> impl FnOnce(String) -> ReflectError + '_ {
move |message| ReflectError::BadValue {
component: component.to_string(),
message,
}
}
impl WorldReflect for World {
#[track_caller]
fn reflect<T: Component + Serialize + DeserializeOwned>(&mut self) {
let type_name = std::any::type_name::<T>();
let name = short_name(type_name);
if self.get_resource::<Registry>().is_none() {
self.insert_resource(Registry::default());
}
let reg = self.resource_mut::<Registry>();
if let Some(&i) = reg.by_name.get(&name) {
let existing = ®.list[i].type_name;
assert_eq!(
existing, type_name,
"World::reflect: `{type_name}` and `{existing}` would both be named `{name}`"
);
return;
}
reg.by_name.insert(name.clone(), reg.list.len());
reg.list.push(Reflected {
name,
type_name: type_name.to_string(),
get: Arc::new(get::<T>),
set: Arc::new(set::<T>),
spawn: Arc::new(spawn::<T>),
spawn_at: Arc::new(spawn_at::<T>),
check: Arc::new(check::<T>),
remove: Arc::new(remove::<T>),
});
}
#[track_caller]
unsafe fn reflect_fields(&mut self, id: ComponentId, fields: &[Field]) {
let info = self.component_info(id);
let name = info.name().to_string();
let size = info.layout().size();
assert!(
info.type_id().is_none() && !info.needs_drop(),
"World::reflect_fields: `{name}` is a Rust type or has a destructor; use `reflect::<T>()`"
);
let layout = Arc::new(FieldLayout::new(&name, size, fields));
if self.get_resource::<Registry>().is_none() {
self.insert_resource(Registry::default());
}
let reg = self.resource_mut::<Registry>();
reg.layouts.insert(id, layout.clone());
let entry = |name: &str| {
let (l, g, k, st) = (layout.clone(), layout.clone(), layout.clone(), layout.clone());
let sa = layout.clone();
Reflected {
name: name.to_string(),
type_name: name.to_string(),
get: Arc::new(move |w: &World, e| {
w.get_dynamic(e, id).map(|p| Ok(unsafe { g.read(p) }))
}),
set: Arc::new(move |w: &mut World, e, v| {
let bytes = l.bytes(w.get_dynamic(e, id), &v)?;
unsafe { w.insert_dynamic(e, id, bytes.as_ptr()) };
Ok(())
}),
spawn: Arc::new(move |w: &mut World, v| {
let bytes = st.bytes(None, &v)?;
Ok(unsafe { w.spawn_dynamic(&[(id, bytes.as_ptr())]) })
}),
spawn_at: Arc::new(move |w: &mut World, e, v| {
let bytes = sa.bytes(None, &v)?;
Ok(unsafe { w.spawn_at_dynamic(e, &[(id, bytes.as_ptr())]) })
}),
check: Arc::new(move |v: &Value| k.bytes(None, v).map(drop)),
remove: Arc::new(move |w: &mut World, e| w.remove_dynamic(e, id)),
}
};
match reg.by_name.get(&name) {
Some(&i) if reg.list[i].type_name == name => reg.list[i] = entry(&name),
Some(&i) => panic!(
"World::reflect_fields: `{name}` is already reflected as `{}`",
reg.list[i].type_name
),
None => {
reg.by_name.insert(name.clone(), reg.list.len());
reg.list.push(entry(&name));
}
}
}
fn reflected_components(&self) -> Vec<&str> {
registry(self).map_or_else(Vec::new, |r| r.list.iter().map(|c| c.name.as_str()).collect())
}
fn get_json(&self, e: Entity, name: &str) -> Result<Value, ReflectError> {
let c = find(self, name)?;
if !self.is_alive(e) {
return Err(ReflectError::NoSuchEntity(e));
}
match (c.get)(self, e) {
Some(v) => v.map_err(bad_value(name)),
None => Err(ReflectError::MissingComponent {
entity: e,
component: name.to_string(),
}),
}
}
fn set_json(&mut self, e: Entity, name: &str, value: Value) -> Result<(), ReflectError> {
let c = find(self, name)?;
if !self.is_alive(e) {
return Err(ReflectError::NoSuchEntity(e));
}
let value = match (c.get)(self, e) {
Some(current) if value.is_object() => {
let mut current = current.map_err(bad_value(name))?;
merge(&mut current, value);
current
}
_ => value,
};
(c.set)(self, e, value).map_err(bad_value(name))
}
fn remove_json(&mut self, e: Entity, name: &str) -> Result<bool, ReflectError> {
let c = find(self, name)?;
if !self.is_alive(e) {
return Err(ReflectError::NoSuchEntity(e));
}
Ok((c.remove)(self, e))
}
fn entity_json(&self, e: Entity) -> Result<Value, ReflectError> {
if !self.is_alive(e) {
return Err(ReflectError::NoSuchEntity(e));
}
let mut out = serde_json::Map::new();
for c in registry(self).map_or(&[][..], |r| &r.list) {
if let Some(v) = (c.get)(self, e) {
out.insert(c.name.clone(), v.map_err(bad_value(&c.name))?);
}
}
Ok(Value::Object(out))
}
fn spawn_json(&mut self, components: Value) -> Result<Entity, ReflectError> {
let Value::Object(map) = components else {
return Err(ReflectError::BadValue {
component: "<entity>".into(),
message: "expected an object keyed by component name".into(),
});
};
let parts = map
.into_iter()
.map(|(name, v)| find(self, &name).map(|c| (c, v)))
.collect::<Result<Vec<_>, _>>()?;
let mut parts = parts.into_iter();
let Some((first, value)) = parts.next() else {
return Err(ReflectError::BadValue {
component: "<entity>".into(),
message: "an entity needs at least one component".into(),
});
};
let e = (first.spawn)(self, value).map_err(bad_value(&first.name))?;
for (c, value) in parts {
if let Err(message) = (c.set)(self, e, value) {
self.despawn(e);
return Err(ReflectError::BadValue {
component: c.name,
message,
});
}
}
Ok(e)
}
#[track_caller]
fn reflect_resource<R: Send + Sync + 'static + Serialize + DeserializeOwned>(&mut self) {
let type_name = std::any::type_name::<R>();
let name = short_name(type_name);
if self.get_resource::<Registry>().is_none() {
self.insert_resource(Registry::default());
}
let reg = self.resource_mut::<Registry>();
if let Some(&i) = reg.resource_by_name.get(&name) {
let existing = ®.resources[i].type_name;
assert_eq!(
existing, type_name,
"World::reflect_resource: `{type_name}` and `{existing}` would both be named `{name}`"
);
return;
}
reg.resource_by_name.insert(name.clone(), reg.resources.len());
reg.resources.push(ReflectedResource {
name,
type_name: type_name.to_string(),
get: Arc::new(get_resource::<R>),
set: Arc::new(set_resource::<R>),
check: Arc::new(check::<R>),
});
}
fn reflected_resources(&self) -> Vec<&str> {
registry(self).map_or_else(Vec::new, |r| r.resources.iter().map(|c| c.name.as_str()).collect())
}
fn resource_json(&self, name: &str) -> Result<Value, ReflectError> {
let r = find_resource(self, name)?;
match (r.get)(self) {
Some(v) => v.map_err(bad_value(name)),
None => Err(ReflectError::MissingResource(name.to_string())),
}
}
fn set_resource_json(&mut self, name: &str, value: Value) -> Result<(), ReflectError> {
let r = find_resource(self, name)?;
let value = match (r.get)(self) {
Some(current) if value.is_object() => {
let mut current = current.map_err(bad_value(name))?;
merge(&mut current, value);
current
}
_ => value,
};
(r.set)(self, value).map_err(bad_value(name))
}
#[track_caller]
unsafe fn reflect_resource_fields(&mut self, name: &str, layout: std::alloc::Layout, fields: &[Field]) {
let fields = Arc::new(FieldLayout::new(name, layout.size(), fields));
if self.get_resource::<Registry>().is_none() {
self.insert_resource(Registry::default());
}
let (read, write, check) = (fields.clone(), fields.clone(), fields);
let (get_name, set_name) = (name.to_string(), name.to_string());
let entry = ReflectedResource {
name: name.to_string(),
type_name: name.to_string(),
get: Arc::new(move |w: &World| {
w.get_resource_dynamic(&get_name).map(|p| Ok(unsafe { read.read(p) }))
}),
set: Arc::new(move |w: &mut World, v| {
let bytes = write.bytes(w.get_resource_dynamic(&set_name), &v)?;
unsafe { w.insert_resource_dynamic(&set_name, layout, bytes.as_ptr()) };
Ok(())
}),
check: Arc::new(move |v: &Value| check.bytes(None, v).map(drop)),
};
let reg = self.resource_mut::<Registry>();
match reg.resource_by_name.get(name) {
Some(&i) if reg.resources[i].type_name == name => reg.resources[i] = entry,
Some(&i) => panic!(
"World::reflect_resource_fields: `{name}` is already reflected as `{}`",
reg.resources[i].type_name
),
None => {
reg.resource_by_name.insert(name.to_string(), reg.resources.len());
reg.resources.push(entry);
}
}
}
fn save(&self) -> Value {
let reflected = self.reflected_components();
let mut entities = Vec::new();
let mut not_saved: std::collections::BTreeMap<String, usize> = Default::default();
for a in self.archetypes().iter().filter(|a| !a.is_empty()) {
for &id in a.component_ids() {
let full = self.component_info(id).name();
let name = short_name(full);
if !reflected.contains(&name.as_str()) && !reflected.contains(&full) {
*not_saved.entry(name).or_default() += a.len();
}
}
for &e in a.entities() {
if let Ok(Value::Object(components)) = self.entity_json(e)
&& !components.is_empty()
{
entities.push((e.to_bits(), components));
}
}
}
entities.sort_unstable_by_key(|(id, _)| *id);
let entities: Vec<Value> = entities
.into_iter()
.map(|(id, components)| serde_json::json!({ "entity": id, "components": components }))
.collect();
let mut resources = serde_json::Map::new();
for r in registry(self).map_or(&[][..], |r| &r.resources) {
if let Some(Ok(v)) = (r.get)(self) {
resources.insert(r.name.clone(), v);
}
}
serde_json::json!({
"format": SAVE_FORMAT,
"version": SAVE_VERSION,
"entities": entities,
"resources": resources,
"not_saved": not_saved,
})
}
fn load(&mut self, save: &Value) -> Result<(), ReflectError> {
let bad = |message: String| ReflectError::BadValue {
component: "<save>".into(),
message,
};
if save["format"] != SAVE_FORMAT {
return Err(bad(format!(
"not a world save (expected \"format\": \"{SAVE_FORMAT}\")"
)));
}
if save["version"].as_u64() != Some(SAVE_VERSION) {
return Err(bad(format!(
"unsupported save version {} (this engine reads {SAVE_VERSION})",
save["version"]
)));
}
let list = save["entities"]
.as_array()
.ok_or_else(|| bad("missing \"entities\" list".into()))?;
let mut plan = Vec::with_capacity(list.len());
let mut seen = std::collections::HashSet::with_capacity(list.len());
for item in list {
let id = item["entity"]
.as_u64()
.ok_or_else(|| bad(format!("entity id missing or not a number: {}", item["entity"])))?;
if !seen.insert(id) || Entity::from_bits(id).index() == u32::MAX {
return Err(bad(format!("entity id {id} appears twice or is invalid")));
}
let Some(Value::Object(components)) = item.get("components") else {
return Err(bad(format!("entity {id} has no \"components\" object")));
};
if components.is_empty() {
return Err(bad(format!("entity {id} has no components")));
}
let mut parts = Vec::with_capacity(components.len());
for (name, value) in components {
let c = find(self, name)?;
(c.check)(value).map_err(bad_value(name))?;
parts.push((c, value.clone()));
}
plan.push((Entity::from_bits(id), parts));
}
let mut resource_plan = Vec::new();
if let Some(saved) = save.get("resources") {
let saved = saved
.as_object()
.ok_or_else(|| bad("\"resources\" is not an object".into()))?;
for (name, value) in saved {
let r = find_resource(self, name)?;
(r.check)(value).map_err(bad_value(name))?;
resource_plan.push((r, value.clone()));
}
}
self.reset();
for (e, parts) in plan {
let mut parts = parts.into_iter();
let (first, value) = parts.next().expect("checked: at least one component");
let placed = (first.spawn_at)(self, e, value).map_err(bad_value(&first.name))?;
debug_assert!(placed, "ids are unique and the world was reset");
for (c, value) in parts {
(c.set)(self, e, value).map_err(bad_value(&c.name))?;
}
}
for (r, value) in resource_plan {
(r.set)(self, value).map_err(bad_value(&r.name))?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn short_names_drop_the_path_but_keep_generics() {
assert_eq!(short_name("my_game::units::Health"), "Health");
assert_eq!(short_name("Health"), "Health");
assert_eq!(short_name("a::Wrapper<b::Inner>"), "Wrapper<b::Inner>");
}
#[test]
fn merge_replaces_only_given_fields() {
let mut base = json!({ "a": 1, "b": { "x": 1, "y": 2 }, "c": [1, 2] });
merge(&mut base, json!({ "b": { "y": 9 }, "c": [3] }));
assert_eq!(base, json!({ "a": 1, "b": { "x": 1, "y": 9 }, "c": [3] }));
}
}
#[derive(Default)]
pub(crate) struct TextureNames {
pub(crate) by_id: HashMap<u32, String>,
pub(crate) by_name: HashMap<String, crate::Texture>,
}
#[derive(Serialize, serde::Deserialize)]
#[serde(default, deny_unknown_fields)]
struct SpriteJson {
texture: Option<String>,
color: crate::Color,
z: f32,
pivot: glam::Vec2,
region: Option<crate::Rect>,
}
impl Default for SpriteJson {
fn default() -> Self {
Self {
texture: None,
color: crate::Color::WHITE,
z: 0.0,
pivot: glam::Vec2::splat(0.5),
region: None,
}
}
}
fn sprite_json(world: &World, s: &crate::Sprite) -> Option<Result<Value, String>> {
let name = world.get_resource::<TextureNames>()?.by_id.get(&s.texture.id)?;
let json = SpriteJson {
texture: Some(name.clone()),
color: s.color,
z: s.z,
pivot: s.pivot,
region: s.region,
};
Some(serde_json::to_value(json).map_err(|err| err.to_string()))
}
fn sprite_from_json(world: &World, value: Value) -> Result<crate::Sprite, String> {
let json: SpriteJson = serde_json::from_value(value).map_err(|err| err.to_string())?;
let name = json
.texture
.ok_or("a sprite needs \"texture\": an image in assets/, like \"crab.png\"")?;
let texture = world
.get_resource::<TextureNames>()
.and_then(|t| t.by_name.get(&name))
.copied()
.ok_or_else(|| format!("texture \"{name}\" isn't loaded: ctx.load_texture(\"{name}\") first"))?;
Ok(crate::Sprite {
texture,
color: json.color,
z: json.z,
pivot: json.pivot,
region: json.region,
})
}
pub(crate) fn reflect_sprite(world: &mut World) {
if world.get_resource::<TextureNames>().is_none() {
world.insert_resource(TextureNames::default());
}
if world.get_resource::<Registry>().is_none() {
world.insert_resource(Registry::default());
}
let reg = world.resource_mut::<Registry>();
if reg.by_name.contains_key("Sprite") {
return;
}
reg.by_name.insert("Sprite".into(), reg.list.len());
reg.list.push(Reflected {
name: "Sprite".into(),
type_name: std::any::type_name::<crate::Sprite>().into(),
get: Arc::new(|w: &World, e| w.get::<crate::Sprite>(e).and_then(|s| sprite_json(w, s))),
set: Arc::new(|w: &mut World, e, v| {
let sprite = sprite_from_json(w, v)?;
w.insert(e, sprite);
Ok(())
}),
spawn: Arc::new(|w: &mut World, v| {
let sprite = sprite_from_json(w, v)?;
Ok(w.spawn((sprite,)))
}),
spawn_at: Arc::new(|w: &mut World, e, v| {
let sprite = sprite_from_json(w, v)?;
Ok(w.spawn_at(e, (sprite,)))
}),
check: Arc::new(check::<SpriteJson>),
remove: Arc::new(remove::<crate::Sprite>),
});
}
#[cfg(feature = "3d")]
#[derive(Default)]
pub(crate) struct MeshNames {
pub(crate) by_id: HashMap<u32, String>,
pub(crate) by_name: HashMap<String, crate::Mesh>,
}
#[cfg(feature = "3d")]
#[derive(Serialize, serde::Deserialize)]
#[serde(default, deny_unknown_fields)]
struct ModelJson {
mesh: Option<String>,
color: crate::Color,
}
#[cfg(feature = "3d")]
impl Default for ModelJson {
fn default() -> Self {
Self {
mesh: None,
color: crate::Color::WHITE,
}
}
}
#[cfg(feature = "3d")]
fn model_json(world: &World, m: &crate::Model) -> Option<Result<Value, String>> {
let name = world.get_resource::<MeshNames>()?.by_id.get(&m.mesh.id)?;
let json = ModelJson {
mesh: Some(name.clone()),
color: m.color,
};
Some(serde_json::to_value(json).map_err(|err| err.to_string()))
}
#[cfg(feature = "3d")]
fn model_from_json(world: &World, value: Value) -> Result<crate::Model, String> {
let json: ModelJson = serde_json::from_value(value).map_err(|err| err.to_string())?;
let name = json
.mesh
.ok_or("a model needs \"mesh\": a glTF file in assets/, like \"tree.glb\"")?;
let mesh = world
.get_resource::<MeshNames>()
.and_then(|t| t.by_name.get(&name))
.copied()
.ok_or_else(|| format!("mesh \"{name}\" isn't loaded: ctx.load_mesh(\"{name}\") first"))?;
Ok(crate::Model {
mesh,
color: json.color,
})
}
#[cfg(feature = "3d")]
#[derive(Serialize, serde::Deserialize, Default)]
#[serde(deny_unknown_fields)]
struct ShadowMeshJson {
mesh: Option<String>,
}
#[cfg(feature = "3d")]
fn shadow_mesh_json(world: &World, s: &crate::ShadowMesh) -> Option<Result<Value, String>> {
let mesh = match s.0 {
Some(mesh) => Some(world.get_resource::<MeshNames>()?.by_id.get(&mesh.id)?.clone()),
None => None,
};
Some(serde_json::to_value(ShadowMeshJson { mesh }).map_err(|err| err.to_string()))
}
#[cfg(feature = "3d")]
fn shadow_mesh_from_json(world: &World, value: Value) -> Result<crate::ShadowMesh, String> {
let json: ShadowMeshJson = serde_json::from_value(value).map_err(|err| err.to_string())?;
let Some(name) = json.mesh else {
return Ok(crate::ShadowMesh(None));
};
let mesh = world
.get_resource::<MeshNames>()
.and_then(|t| t.by_name.get(&name))
.copied()
.ok_or_else(|| format!("mesh \"{name}\" isn't loaded: ctx.load_mesh(\"{name}\") first"))?;
Ok(crate::ShadowMesh(Some(mesh)))
}
#[cfg(feature = "3d")]
fn mesh_by_name(world: &World, name: &str) -> Result<crate::Mesh, String> {
world
.get_resource::<MeshNames>()
.and_then(|t| t.by_name.get(name))
.copied()
.ok_or_else(|| format!("mesh \"{name}\" isn't loaded: ctx.load_mesh(\"{name}\") first"))
}
#[cfg(feature = "3d")]
#[derive(Serialize, serde::Deserialize)]
#[serde(default, deny_unknown_fields)]
struct LodRendererJson {
mesh: Option<String>,
offset: crate::Transform3D,
color: crate::Color,
}
#[cfg(feature = "3d")]
impl Default for LodRendererJson {
fn default() -> Self {
Self {
mesh: None,
offset: crate::Transform3D::default(),
color: crate::Color::WHITE,
}
}
}
#[cfg(feature = "3d")]
#[derive(Serialize, serde::Deserialize, Default)]
#[serde(deny_unknown_fields)]
enum LodShadowJson {
#[default]
Same,
Casters(Vec<LodRendererJson>),
}
#[cfg(feature = "3d")]
#[derive(Serialize, serde::Deserialize, Default)]
#[serde(default, deny_unknown_fields)]
struct LodLevelJson {
screen_height: f32,
fade_width: f32,
renderers: Vec<LodRendererJson>,
shadow: LodShadowJson,
}
#[cfg(feature = "3d")]
#[derive(Serialize, serde::Deserialize)]
#[serde(default, deny_unknown_fields)]
struct LodGroupJson {
size: f32,
center: glam::Vec3,
levels: Vec<LodLevelJson>,
}
#[cfg(feature = "3d")]
impl Default for LodGroupJson {
fn default() -> Self {
Self {
size: 1.0,
center: glam::Vec3::ZERO,
levels: Vec::new(),
}
}
}
#[cfg(feature = "3d")]
fn lod_group_json(world: &World, g: &crate::LodGroup) -> Option<Result<Value, String>> {
let names = world.get_resource::<MeshNames>()?;
let renderers = |list: &[crate::LodRenderer]| -> Option<Vec<LodRendererJson>> {
list.iter()
.map(|r| {
Some(LodRendererJson {
mesh: Some(names.by_id.get(&r.mesh.id)?.clone()),
offset: r.offset,
color: r.color,
})
})
.collect()
};
let mut levels = Vec::with_capacity(g.levels.len());
for level in &g.levels {
levels.push(LodLevelJson {
screen_height: level.screen_height,
fade_width: level.fade_width,
renderers: renderers(&level.renderers)?,
shadow: match &level.shadow {
crate::LodShadow::Same => LodShadowJson::Same,
crate::LodShadow::Casters(list) => LodShadowJson::Casters(renderers(list)?),
},
});
}
let json = LodGroupJson {
size: g.size,
center: g.center,
levels,
};
Some(serde_json::to_value(json).map_err(|err| err.to_string()))
}
#[cfg(feature = "3d")]
fn lod_group_from_json(world: &World, value: Value) -> Result<crate::LodGroup, String> {
let json: LodGroupJson = serde_json::from_value(value).map_err(|err| err.to_string())?;
let renderers = |list: Vec<LodRendererJson>| -> Result<Vec<crate::LodRenderer>, String> {
list.into_iter()
.map(|r| {
let name = r
.mesh
.ok_or("a LOD renderer needs \"mesh\": a glTF file in assets/, like \"tree.glb\"")?;
Ok(crate::LodRenderer::new(mesh_by_name(world, &name)?)
.with_offset(r.offset)
.with_color(r.color))
})
.collect()
};
let mut levels = Vec::with_capacity(json.levels.len());
for level in json.levels {
levels.push(
crate::LodLevel::new(level.screen_height, renderers(level.renderers)?)
.with_fade(level.fade_width)
.with_shadow(match level.shadow {
LodShadowJson::Same => crate::LodShadow::Same,
LodShadowJson::Casters(list) => crate::LodShadow::Casters(renderers(list)?),
}),
);
}
Ok(crate::LodGroup::new(json.size, levels).with_center(json.center))
}
#[cfg(feature = "3d")]
pub(crate) fn lod_mesh_names(json: &Value, out: &mut Vec<String>) {
fn meshes(json: &Value, out: &mut Vec<String>) {
match json {
Value::Object(map) => {
for (k, value) in map {
match value {
Value::String(name) if k == "mesh" => out.push(name.clone()),
_ => meshes(value, out),
}
}
}
Value::Array(list) => list.iter().for_each(|v| meshes(v, out)),
_ => {}
}
}
match json {
Value::Object(map) => {
for (k, value) in map {
if k == "LodGroup" {
meshes(value, out);
} else {
lod_mesh_names(value, out);
}
}
}
Value::Array(list) => list.iter().for_each(|v| lod_mesh_names(v, out)),
_ => {}
}
}
#[cfg(feature = "3d")]
pub(crate) fn reflect_model(world: &mut World) {
if world.get_resource::<MeshNames>().is_none() {
world.insert_resource(MeshNames::default());
}
if world.get_resource::<Registry>().is_none() {
world.insert_resource(Registry::default());
}
let reg = world.resource_mut::<Registry>();
if reg.by_name.contains_key("Model") {
return;
}
reg.by_name.insert("Model".into(), reg.list.len());
reg.list.push(Reflected {
name: "Model".into(),
type_name: std::any::type_name::<crate::Model>().into(),
get: Arc::new(|w: &World, e| w.get::<crate::Model>(e).and_then(|m| model_json(w, m))),
set: Arc::new(|w: &mut World, e, v| {
let model = model_from_json(w, v)?;
w.insert(e, model);
Ok(())
}),
spawn: Arc::new(|w: &mut World, v| {
let model = model_from_json(w, v)?;
Ok(w.spawn((model,)))
}),
spawn_at: Arc::new(|w: &mut World, e, v| {
let model = model_from_json(w, v)?;
Ok(w.spawn_at(e, (model,)))
}),
check: Arc::new(check::<ModelJson>),
remove: Arc::new(remove::<crate::Model>),
});
reg.by_name.insert("ShadowMesh".into(), reg.list.len());
reg.list.push(Reflected {
name: "ShadowMesh".into(),
type_name: std::any::type_name::<crate::ShadowMesh>().into(),
get: Arc::new(|w: &World, e| w.get::<crate::ShadowMesh>(e).and_then(|s| shadow_mesh_json(w, s))),
set: Arc::new(|w: &mut World, e, v| {
let shadow = shadow_mesh_from_json(w, v)?;
w.insert(e, shadow);
Ok(())
}),
spawn: Arc::new(|w: &mut World, v| {
let shadow = shadow_mesh_from_json(w, v)?;
Ok(w.spawn((shadow,)))
}),
spawn_at: Arc::new(|w: &mut World, e, v| {
let shadow = shadow_mesh_from_json(w, v)?;
Ok(w.spawn_at(e, (shadow,)))
}),
check: Arc::new(check::<ShadowMeshJson>),
remove: Arc::new(remove::<crate::ShadowMesh>),
});
reg.by_name.insert("LodGroup".into(), reg.list.len());
reg.list.push(Reflected {
name: "LodGroup".into(),
type_name: std::any::type_name::<crate::LodGroup>().into(),
get: Arc::new(|w: &World, e| w.get::<crate::LodGroup>(e).and_then(|g| lod_group_json(w, g))),
set: Arc::new(|w: &mut World, e, v| {
let group = lod_group_from_json(w, v)?;
w.insert(e, group);
Ok(())
}),
spawn: Arc::new(|w: &mut World, v| {
let group = lod_group_from_json(w, v)?;
Ok(w.spawn((group,)))
}),
spawn_at: Arc::new(|w: &mut World, e, v| {
let group = lod_group_from_json(w, v)?;
Ok(w.spawn_at(e, (group,)))
}),
check: Arc::new(check::<LodGroupJson>),
remove: Arc::new(remove::<crate::LodGroup>),
});
}
#[cfg(feature = "physics3d")]
#[derive(Default)]
pub(crate) struct ColliderFiles {
pub(crate) by_mesh: HashMap<crate::CollisionMesh, Value>,
pub(crate) by_file: HashMap<String, crate::CollisionMesh>,
}
#[cfg(feature = "physics3d")]
pub(crate) fn collider_file_key(file: &Value) -> String {
file.to_string()
}
#[cfg(feature = "physics3d")]
pub(crate) fn collider_file(value: &Value) -> Option<&Value> {
value.pointer("/shape/Mesh").filter(|m| m.get("file").is_some())
}
#[cfg(feature = "physics3d")]
fn collider_json(world: &World, c: &crate::Collider3D) -> Option<Result<Value, String>> {
let mut value = match serde_json::to_value(c) {
Ok(v) => v,
Err(err) => return Some(Err(err.to_string())),
};
if let crate::Shape3D::Mesh(mesh) = c.shape
&& let Some(file) = world.get_resource::<ColliderFiles>().and_then(|f| f.by_mesh.get(&mesh))
{
value["shape"] = serde_json::json!({ "Mesh": file });
}
Some(Ok(value))
}
#[cfg(feature = "physics3d")]
fn collider_from_json(world: &World, mut value: Value) -> Result<crate::Collider3D, String> {
if let Some(file) = collider_file(&value) {
let mesh = world
.get_resource::<ColliderFiles>()
.and_then(|f| f.by_file.get(&collider_file_key(file)))
.copied()
.ok_or_else(|| format!("collision mesh {file} isn't loaded: load the scene with ctx.load_scene"))?;
value["shape"] = serde_json::json!({ "Mesh": mesh });
}
serde_json::from_value(value).map_err(|err| err.to_string())
}
#[cfg(feature = "physics3d")]
pub(crate) fn reflect_collider(world: &mut World) {
if world.get_resource::<ColliderFiles>().is_none() {
world.insert_resource(ColliderFiles::default());
}
if world.get_resource::<Registry>().is_none() {
world.insert_resource(Registry::default());
}
let reg = world.resource_mut::<Registry>();
if reg.by_name.contains_key("Collider3D") {
return;
}
reg.by_name.insert("Collider3D".into(), reg.list.len());
reg.list.push(Reflected {
name: "Collider3D".into(),
type_name: std::any::type_name::<crate::Collider3D>().into(),
get: Arc::new(|w: &World, e| w.get::<crate::Collider3D>(e).and_then(|c| collider_json(w, c))),
set: Arc::new(|w: &mut World, e, v| {
let c = collider_from_json(w, v)?;
w.insert(e, c);
Ok(())
}),
spawn: Arc::new(|w: &mut World, v| {
let c = collider_from_json(w, v)?;
Ok(w.spawn((c,)))
}),
spawn_at: Arc::new(|w: &mut World, e, v| {
let c = collider_from_json(w, v)?;
Ok(w.spawn_at(e, (c,)))
}),
check: Arc::new(|v: &Value| {
let mut v = v.clone();
if collider_file(&v).is_some() {
v["shape"] = serde_json::json!({ "Mesh": crate::CollisionMesh::default() });
}
check::<crate::Collider3D>(&v)
}),
remove: Arc::new(remove::<crate::Collider3D>),
});
}
pub(crate) fn asset_names(json: &Value, key: &str, field: &str, out: &mut Vec<String>) {
match json {
Value::Object(map) => {
for (k, value) in map {
if k == key
&& let Some(name) = value.get(field).and_then(Value::as_str)
{
out.push(name.to_string());
} else {
asset_names(value, key, field, out);
}
}
}
Value::Array(list) => list.iter().for_each(|v| asset_names(v, key, field, out)),
_ => {}
}
}