Skip to content
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

Merged
merged 4 commits into from
Oct 4, 2017
Merged

Conversation

zakarumych
Copy link
Member

@zakarumych zakarumych commented Sep 27, 2017

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 Components during serialization process.
    • It may contain additional data to be used for tracking owner of the component.
  • MarkerAllocator trait for allocating Markers, 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 SerializableComponents
    It allows to save all existing and mentioned in tuple components of one entity from ReadStorages into serializable structure. And load it into WriteStorages again.

Helper types WorldSerialize and WorldDeserialize are good example how to use this facility.

@torkleyy torkleyy requested review from kvark and csherratt September 28, 2017 13:43
Copy link
Member

@torkleyy torkleyy left a 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.

where
A: SeqAccess<'de>,
{
while let Some(()) = seq.next_element_seed(DeserializeEntity {
Copy link
Member

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.



/// `DeerializeSeed` implementation for `World`
//#[derive(SystemData)]
Copy link
Member

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?

Copy link
Member Author

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


/// `DeerializeSeed` implementation for `World`
//#[derive(SystemData)]
pub struct WorldDeserialize<'a, M: Marker, E: Error, T: Components<M::Identifier, E>> {
Copy link
Member

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.

/// Used as a placeholder for associated error types if
/// something cannot fail.
#[derive(Debug)]
pub enum NoError {}
Copy link
Member

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?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure


/// This trait is implemented by any tuple where all elements are
/// `Component + Serialize + DeserializeOwned`
pub trait Components<M, E: Error>: for<'a> Storages<'a> {
Copy link
Member

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.

/// `maintain` method can be implemented for cleanup and actualization
/// # Example
/// see docs for `Marker`
pub trait MarkerAllocator<M: Marker>: Resource {
Copy link
Member

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?

Copy link
Member Author

@zakarumych zakarumych Sep 29, 2017

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.

/// 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
Copy link
Member

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how 🤷‍♂️

/// 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>> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, too many bounds.


/// 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)
Copy link
Member

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.

}


impl<'a, M, E, T> SystemData<'a> for WorldSerialize<'a, M, E, T>
Copy link
Member

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?

@@ -0,0 +1,179 @@


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra lines?

///
/// Allowed to panic if `self.id() != update.id()`.
/// But usually implementer may ignore `update.id()` value
/// as deserialization algorithm ensures `id()`'s match.
Copy link
Member

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)

}
}

/// This allocator is used with `Marker` trait
Copy link
Member

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.

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`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"argumetn"

Copy link
Member Author

@zakarumych zakarumych Oct 2, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

¯\_(ツ)_/¯

Copy link
Member

@kvark kvark left a 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 {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be PhantomError

Copy link
Member Author

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?

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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?

Copy link
Member

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 {}
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newline missing

Copy link
Member Author

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;
Copy link
Member

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?

Copy link
Member Author

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.

@@ -0,0 +1,146 @@

use std::fmt::{self, Display, Formatter};
Copy link
Member

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.

Copy link
Member Author

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?

Copy link
Member

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.

use std::fmt::{self, Display, Formatter};
use std::marker::PhantomData;

use serde::de::{self, Deserialize, DeserializeSeed, Deserializer, SeqAccess, Visitor};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, but same here

/// 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);
Copy link
Member

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

.map(|(e, m)| (e, *m))
.collect::<Vec<_>>();
loop {
if to_serialize.is_empty() {
Copy link
Member

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() {...}

Copy link
Member Author

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>
Copy link
Member

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

Copy link
Member

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?

WorldSerialize {
entities: entities,
storages: ($($a,)*),
markers: markers,
Copy link
Member

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)?,
Copy link
Member

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)

Copy link
Member Author

@zakarumych zakarumych Oct 2, 2017

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")]
Copy link
Member

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.

@@ -0,0 +1,211 @@
//! Provides with `Marker` and `MarkerAllocator` traits
Copy link
Member

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
Copy link
Member

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.

Copy link
Member Author

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


use self::details::{Components, EntityData, SerializableComponent, Storages};

pub mod marker;
Copy link
Member

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?


use {Entities, FetchMut, Join, ReadStorage, WriteStorage};

use super::{Components, EntityData, Storages};
Copy link
Member

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::.


#[derive(Serialize, Deserialize)]
#[serde(bound = "")]
pub struct EntityData<M: Marker, E, T: Components<M::Identifier, E>> {
Copy link
Member

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

Copy link
Member Author

@zakarumych zakarumych Oct 2, 2017

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

@kvark
Copy link
Member

kvark commented Oct 2, 2017

@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:

saveload feature

This is not a cargo feature, so we could have a less ambiguous PR name.

@zakarumych zakarumych changed the title saveload feature Add an ability to save and load entities. Oct 2, 2017
Copy link
Member

@WaDelma WaDelma left a 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.

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)>,
Copy link
Member

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>.

Copy link
Member Author

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 😄

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)>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.


(@) => {};

(@ $ah:ident|$bh:ident $(,$a:ident|$b:ident)*) => {
Copy link
Member

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.

use serde::de::DeserializeOwned;


/// This trait should be implemetened by component which is gonna be used as marker.
Copy link
Member

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/

Copy link
Member

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/

/// 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`.
Copy link
Member

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as before.

/// 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/So/So the/

}

/// This type implements `Serialize` so that it may be used in generic environment
/// where `Serialize` implementer is expected.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/implementer/implementation/

/// 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`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/by/from/

Copy link
Member Author

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.

T: Components<M::Identifier, E>,
{
/// Remove all marked entities
/// Use it if you want to delete entities that was just serialized
Copy link
Member

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/

Copy link
Member

@torkleyy torkleyy left a 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 {}
Copy link
Member

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?

///
/// ## Example
///
/// ```rust,no_run
Copy link
Member

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.

match markers.get(entity).cloned() {
Some(marker) => Some(marker.id()),
None => {
let marker = allocator.mark(entity, markers);
Copy link
Member

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>
Copy link
Member

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?

Copy link
Member

@torkleyy torkleyy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thank you!

@torkleyy
Copy link
Member

torkleyy commented Oct 4, 2017

@omni-viral Thank you for this big PR!

bors r+

bors bot added a commit that referenced this pull request Oct 4, 2017
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.
@bors
Copy link
Contributor

bors bot commented Oct 4, 2017

Build succeeded

@bors bors bot merged commit b1ad528 into amethyst:master Oct 4, 2017
@zakarumych zakarumych deleted the serde branch January 19, 2018 12:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants