-
Notifications
You must be signed in to change notification settings - Fork 220
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add an ability to save and load entities. #275
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks very nice! I still have to check some details of the marker system.
src/saveload/de.rs
Outdated
where | ||
A: SeqAccess<'de>, | ||
{ | ||
while let Some(()) = seq.next_element_seed(DeserializeEntity { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: replace with .is_some
.
src/saveload/de.rs
Outdated
|
||
|
||
/// `DeerializeSeed` implementation for `World` | ||
//#[derive(SystemData)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not use derive here? And further, if derive doesn't work here, why did you keep the line?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it should start to work. It requires PhantomData: SystemData
. So I kept the line as a remainder
src/saveload/de.rs
Outdated
|
||
/// `DeerializeSeed` implementation for `World` | ||
//#[derive(SystemData)] | ||
pub struct WorldDeserialize<'a, M: Marker, E: Error, T: Components<M::Identifier, E>> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You shouldn't create unnecessary bounds on the struct itself, e.g. E: Error
should be removed here.
src/saveload/details.rs
Outdated
/// Used as a placeholder for associated error types if | ||
/// something cannot fail. | ||
#[derive(Debug)] | ||
pub enum NoError {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this go into the errors
module?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure
src/saveload/details.rs
Outdated
|
||
/// This trait is implemented by any tuple where all elements are | ||
/// `Component + Serialize + DeserializeOwned` | ||
pub trait Components<M, E: Error>: for<'a> Storages<'a> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need for the Error
bound here.
src/saveload/marker.rs
Outdated
/// `maintain` method can be implemented for cleanup and actualization | ||
/// # Example | ||
/// see docs for `Marker` | ||
pub trait MarkerAllocator<M: Marker>: Resource { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not make the Marker
an associated?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One allocator can implement this trait for different M.
I prefer type parameter when there is no need to fetch for associated type or restrict implementation to only one type.
src/saveload/ser.rs
Outdated
/// with serializer `S` | ||
/// Doesn't recursively mark referenced entities. | ||
/// Closure passed in `SerializableComponent::save` returns `None` for unmarked `Entity` | ||
/// In this case `SerializableComponent::save` may perform workaround |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be a bit more detailed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure how 🤷♂️
src/saveload/ser.rs
Outdated
/// It may be constructed manually (TODO: Add `new` function) | ||
/// Or fetched by `System` as `SystemData` | ||
/// Serializes components in tuple `T` with marker `M` | ||
pub struct WorldSerialize<'a, M: Marker, E: Error, T: Components<M::Identifier, E>> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, too many bounds.
src/saveload/ser.rs
Outdated
|
||
/// This type implements `Serialize` so that it may be used in generic environment | ||
/// where `Serialize` implementer is expected | ||
/// It may be constructed manually (TODO: Add `new` function) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please fix the TODO or make it a comment.
src/saveload/ser.rs
Outdated
} | ||
|
||
|
||
impl<'a, M, E, T> SystemData<'a> for WorldSerialize<'a, M, E, T> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as before, could this be derived?
src/saveload/details.rs
Outdated
@@ -0,0 +1,179 @@ | |||
|
|||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra lines?
src/saveload/marker.rs
Outdated
/// | ||
/// Allowed to panic if `self.id() != update.id()`. | ||
/// But usually implementer may ignore `update.id()` value | ||
/// as deserialization algorithm ensures `id()`'s match. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be id()
s (without apostrophe)
src/saveload/marker.rs
Outdated
} | ||
} | ||
|
||
/// This allocator is used with `Marker` trait |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use full stops here.
src/saveload/marker.rs
Outdated
pub trait MarkerAllocator<M: Marker>: Resource { | ||
/// Allocate new `Marker`. | ||
/// Stores mapping `Marker` -> `Entity`. | ||
/// If _id_ arguemtn is `Some(id)` then new marker will have this `id`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"argumetn"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
¯\_(ツ)_/¯
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work! I've got a few notes/complains here.
My biggest concern is the ids
function. Could you look for a way to make it a part of one of the traits you have? Just anything to simplify it - passing that function around looks a bit hacky, and is certainly not very obvious or simple.
/// Used as a placeholder for associated error types if | ||
/// something cannot fail. | ||
#[derive(Debug)] | ||
pub enum NoError {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be PhantomError
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be !
but it isn't stable yet.
Why PhantomError
anyway?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, first, because you are implementing Error
for NoError
, which already looks like a hack. Secondly, because the "phantom" is rather established for such things that can't be instantiated, so PhantomError
is crystal clear here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PhantomData
is unit type and can be instantiated. And it is an example from std
.
IDK any other examples.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have a strong opinion here, @kvark do you insist on this request?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope. I think I might have confused a bit the semantics of phantom types. Pretty sure I saw them used in the context where one couldn't create instances (enum MyPhantomType {}
), but I don't want to push hard on that here. It's fine to ship as is.
fn description(&self) -> &str { | ||
match *self {} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
newline missing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added
src/lib.rs
Outdated
@@ -216,6 +219,9 @@ pub use world::{Component, CreateIter, CreateIterAtomic, EntitiesRes, Entity, En | |||
#[cfg(feature = "common")] | |||
pub mod common; | |||
|
|||
#[cfg(feature = "serialize")] | |||
pub mod saveload; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is it called saveload
and not serialize
/serial
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because it is also do some tricky data conversion by functions save
and load
.
If you insist I'll rename.
src/saveload/de.rs
Outdated
@@ -0,0 +1,146 @@ | |||
|
|||
use std::fmt::{self, Display, Formatter}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer either just use std::fmt
or no self
in the list for consistency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why don't you like self
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just find it inconsistent that some of the types in the same namespace are referred directly while others - via fmt::Type
. I believe it reads better through the code if all the imports from the same namespace are consistent. In case of fmt
, I find fmt::Display
and fmt::Formatter
quite nice, but that's a matter of taste.
src/saveload/de.rs
Outdated
use std::fmt::{self, Display, Formatter}; | ||
use std::marker::PhantomData; | ||
|
||
use serde::de::{self, Deserialize, DeserializeSeed, Deserializer, SeqAccess, Visitor}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit, but same here
src/saveload/marker.rs
Outdated
/// But usually implementer may ignore `update.id()` value | ||
/// as deserialization algorithm ensures `id()`s match. | ||
fn update(&mut self, update: Self) { | ||
::std::mem::drop(update); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: let's not use global imports
src/saveload/ser.rs
Outdated
.map(|(e, m)| (e, *m)) | ||
.collect::<Vec<_>>(); | ||
loop { | ||
if to_serialize.is_empty() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any reason to not write it as while !to_serialize.is_empty() {...}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No reason now.
/// Create serializable structure from storages | ||
#[allow(non_snake_case)] | ||
pub fn new(entities: Entities<'a>, | ||
markers: ReadStorage<'a, X> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: please use the new formatting
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not resolved, could you use the latest version of rustfmt-nightly and format this please?
src/saveload/ser.rs
Outdated
WorldSerialize { | ||
entities: entities, | ||
storages: ($($a,)*), | ||
markers: markers, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: could be just markers,
and entities,
for (entity, marker) in (&**entities, &*markers).join() { | ||
serseq.serialize_element(&EntityData::<M, E, T> { | ||
marker: *marker, | ||
components: T::save(entity, storages, &ids).map_err(ser::Error::custom)?, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this looks a bit convoluted, ids
gets an entity's marker while we are already iterating entities with markers (so we already have that marker)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If component contains some Entity
it must use this closure to get marker of that entity.
Then Entity
gets replaced with marker in SaveLoadComponent::Data
returned from save
src/lib.rs
Outdated
@@ -216,6 +219,9 @@ pub use world::{Component, CreateIter, CreateIterAtomic, EntitiesRes, Entity, En | |||
#[cfg(feature = "common")] | |||
pub mod common; | |||
|
|||
#[cfg(feature = "serialize")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This wasn't rebased correctly, it's called serde
now.
src/saveload/marker.rs
Outdated
@@ -0,0 +1,211 @@ | |||
//! Provides with `Marker` and `MarkerAllocator` traits |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Provides with Marker
@@ -0,0 +1,13 @@ | |||
//! Save and load entites from various formats with serde |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would really appreciate if you could add a top-level overview here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be better if someone with better writing skills do this
src/saveload/mod.rs
Outdated
|
||
use self::details::{Components, EntityData, SerializableComponent, Storages}; | ||
|
||
pub mod marker; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we not make the hierarchy any deeper and just reexport from marker
?
src/saveload/ser.rs
Outdated
|
||
use {Entities, FetchMut, Join, ReadStorage, WriteStorage}; | ||
|
||
use super::{Components, EntityData, Storages}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are all local imports, please move them together and use saveload::
instead of super::
.
src/saveload/details.rs
Outdated
|
||
#[derive(Serialize, Deserialize)] | ||
#[serde(bound = "")] | ||
pub struct EntityData<M: Marker, E, T: Components<M::Identifier, E>> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need for the Marker
bound
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Either bound or additional type parameter to use instead of M::Identifier
.
And bound them equal in all impls
rebased
@omni-viral thanks for addressing my notes! Re:naming. How do you feel about "loadable" component/feature/trait? In my understanding, this is the make thing you are trying to achieve. Serialization/deserialization are just means to be able to load components from disk. Even saving is rather useless if you can't load it. Also note:
This is not a cargo feature, so we could have a less ambiguous PR name. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't looked at the actual implementation at all, but I gave a pass over the documentation.
src/saveload/de.rs
Outdated
storages: &'a mut <T as Storages<'b>>::WriteStorages, | ||
markers: &'a mut WriteStorage<'b, M>, | ||
allocator: &'a mut FetchMut<'b, M::Allocator>, | ||
pd: PhantomData<(M, E, T)>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
M
shouldn't be needed in the PhantomData
as it's already used in &'a mut WriteStorage<'b, M>
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Types went serious changes and now it is indeed unnecessary 😄
src/saveload/de.rs
Outdated
storages: &'a mut <T as Storages<'b>>::WriteStorages, | ||
markers: &'a mut WriteStorage<'b, M>, | ||
allocator: &'a mut FetchMut<'b, M::Allocator>, | ||
pd: PhantomData<(M, E, T)>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here.
src/saveload/details.rs
Outdated
|
||
(@) => {}; | ||
|
||
(@ $ah:ident|$bh:ident $(,$a:ident|$b:ident)*) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add comment for explaining this.
src/saveload/marker.rs
Outdated
use serde::de::DeserializeOwned; | ||
|
||
|
||
/// This trait should be implemetened by component which is gonna be used as marker. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/gonna/going to be/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/implemetened by/implemented by a/
src/saveload/marker.rs
Outdated
/// This trait should be implemetened by component which is gonna be used as marker. | ||
/// This marker should be set to entity that should be serialized. | ||
/// If serialization strategy needs to set marker to some entity it should use | ||
/// new marker allocated for `Marker::Allocator`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/it should use new marker allocated for/then it should use newly allocated marker from/
|
||
|
||
/// Serialize components from specified storages via `SerializableComponent::save` | ||
/// of all marked entities with provided serializer. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as before.
src/saveload/ser.rs
Outdated
/// closure passed in `ids` arguemnt returns `None` for unmarked `Entity`. | ||
/// In this case `SerializableComponent::save` may perform workaround (forget about `Entity`) | ||
/// or fail. | ||
/// So function doesn't recursively mark referenced entities. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/So/So the/
src/saveload/ser.rs
Outdated
} | ||
|
||
/// This type implements `Serialize` so that it may be used in generic environment | ||
/// where `Serialize` implementer is expected. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/implementer/implementation/
src/saveload/ser.rs
Outdated
/// This type implements `Serialize` so that it may be used in generic environment | ||
/// where `Serialize` implementer is expected. | ||
/// It may be constructed manually with `WorldSerialize::new`. | ||
/// Or fetched by `System` as `SystemData`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/by/from/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure here. It's not fetched from System
as it isn't in System
. System
fetches SystemData
from World
.
src/saveload/ser.rs
Outdated
T: Components<M::Identifier, E>, | ||
{ | ||
/// Remove all marked entities | ||
/// Use it if you want to delete entities that was just serialized |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/Use it/Use this/
s/was/were/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This my last review, I will merge after you address my comments!
/// Used as a placeholder for associated error types if | ||
/// something cannot fail. | ||
#[derive(Debug)] | ||
pub enum NoError {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have a strong opinion here, @kvark do you insist on this request?
src/saveload/marker.rs
Outdated
/// | ||
/// ## Example | ||
/// | ||
/// ```rust,no_run |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, why no_run
? Please remove that.
src/saveload/ser.rs
Outdated
match markers.get(entity).cloned() { | ||
Some(marker) => Some(marker.id()), | ||
None => { | ||
let marker = allocator.mark(entity, markers); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you like to rewrite this to use the Entry API in this PR or in a follow-up?
/// Create serializable structure from storages | ||
#[allow(non_snake_case)] | ||
pub fn new(entities: Entities<'a>, | ||
markers: ReadStorage<'a, X> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not resolved, could you use the latest version of rustfmt-nightly and format this please?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, thank you!
@omni-viral Thank you for this big PR! bors r+ |
275: Add an ability to save and load entities. r=torkleyy a=omni-viral Add (de)serialization support for entities to specs. Main parts are: * `Marker` trait. `Marker` is used for several purposes: * Mark which entities should be serialized. * It contains ID to replace `Entity` stored in `Component`s during serialization process. * It may contain additional data to be used for tracking owner of the component. * `MarkerAllocator` trait for allocating `Marker`s, storing `Marker -> Entity` mapping and other stuff. * `SerializableComponent` trait that is auto implemented by components if they implements `Serialize`, `DeserializeOwned` and `Copy` (later is rather strict, maybe it should be replaced with `Clone` or removed somehow). Mainly `Component` implements `SerializableComponent` manually when it contains `Entity`. `Component` should replace `Entity` with provided `Marker` in `SerializableComponent::save` function. And replace `Marker` with `Entity` in `SerializableComponent::load` function. * `Components` trait is implemented for tuples of `SerializableComponent`s It allows to save all existing and mentioned in tuple components of one entity from `ReadStorage`s into serializable structure. And load it into `WriteStorage`s again. Helper types `WorldSerialize` and `WorldDeserialize` are good example how to use this facility.
Build succeeded |
Add (de)serialization support for entities to specs.
Main parts are:
Marker
trait.Marker
is used for several purposes:Entity
stored inComponent
s during serialization process.MarkerAllocator
trait for allocatingMarker
s, storingMarker -> Entity
mapping and other stuff.SerializableComponent
trait that is auto implemented by components if they implementsSerialize
,DeserializeOwned
andCopy
(later is rather strict, maybe it should be replaced withClone
or removed somehow).Mainly
Component
implementsSerializableComponent
manually when it containsEntity
.Component
should replaceEntity
with providedMarker
inSerializableComponent::save
function. And replaceMarker
withEntity
inSerializableComponent::load
function.Components
trait is implemented for tuples ofSerializableComponent
sIt allows to save all existing and mentioned in tuple components of one entity from
ReadStorage
s into serializable structure. And load it intoWriteStorage
s again.Helper types
WorldSerialize
andWorldDeserialize
are good example how to use this facility.