From 2cc18502129529270cb81093f96b0c3fcf9cf86d Mon Sep 17 00:00:00 2001 From: Michele Caini Date: Wed, 28 Mar 2018 10:23:47 +0200 Subject: [PATCH] save/restore - see #27 --- CMakeLists.txt | 3 +- README.md | 248 +++++++++++- cmake/in/cereal.in | 19 + src/entt/entity/registry.hpp | 77 +++- src/entt/entity/snapshot.hpp | 713 ++++++++++++++++++++++++++++++++++ src/entt/entt.hpp | 1 + src/entt/signal/sigh.hpp | 2 +- test/CMakeLists.txt | 20 + test/entt/entity/snapshot.cpp | 489 +++++++++++++++++++++++ test/snapshot/snapshot.cpp | 182 +++++++++ 10 files changed, 1748 insertions(+), 6 deletions(-) create mode 100644 cmake/in/cereal.in create mode 100644 src/entt/entity/snapshot.hpp create mode 100644 test/entt/entity/snapshot.cpp create mode 100644 test/snapshot/snapshot.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ef6f3ebf6..fbffda56d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,7 +16,7 @@ endif() # Project configuration # -project(entt VERSION 2.4.2) +project(entt VERSION 2.5.0) if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Debug) @@ -86,6 +86,7 @@ if(BUILD_TESTING) option(BUILD_BENCHMARK "Build benchmark." OFF) option(BUILD_MOD "Build mod example." OFF) + option(BUILD_SNAPSHOT "Build snapshot example." OFF) # gtest, gtest_main, gmock and gmock_main targets are available from now on set(GOOGLETEST_DEPS_DIR ${entt_SOURCE_DIR}/deps/googletest) diff --git a/README.md b/README.md index 432a834ae..7ddb66c43 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,10 @@ * [Runtime components](#runtime-components) * [A journey through a plugin](#a-journey-through-a-plugin) * [Sorting: is it possible?](#sorting-is-it-possible) + * [Snapshot: complete vs continuous](#snapshot-complete-vs-continuous) + * [Snapshot loader](#snapshot-loader) + * [Continuous loader](#continuous-loader) + * [Archives](#archives) * [View: to persist or not to persist?](#view-to-persist-or-not-to-persist) * [Standard View](#standard-view) * [Single component standard view](#single-component-standard-view) @@ -45,6 +49,8 @@ * [Event emitter](#event-emitter) * [License](#license) * [Support](#support) + * [Donation](#donation) + * [Hire me](#hire-me) # Introduction @@ -657,6 +663,244 @@ In fact, there are two functions that respond to slightly different needs: In this case, instances of `Movement` are arranged in memory so that cache misses are minimized when the two components are iterated together. +### Snapshot: complete vs continuous + +The `Registry` class offers basic support to serialization.
+It doesn't convert components and tags to bytes directly, there wasn't the need +of another tool for serialization out there. Instead, it accepts an opaque +object with a suitable interface (namely an _archive_) to serialize its internal +data structures and restore them later. The way types and instances are +converted to a bunch of bytes is completely in charge to the archive and thus to +the users. + +The goal of the serialization part is to allow users to make both a dump of the +entire registry or a narrower snapshot, that is to select only the components +and the tags in which they are interested.
+Intuitively, the use cases are different. As an example, the first approach is +suitable for local save/restore functionalities while the latter is suitable for +creating client-server applications and for transferring somehow parts of the +representation side to side. + +To take a snapshot of the registry, use the `snapshot` member function. It +returns a temporary object properly initialized to _save_ the whole registry or +parts of it. + +Example of use: + +```cpp +OutputArchive output; + +registry.snapshot() + .entities(output) + .destroyed(output) + .component(output) + .tag(output); +``` + +It isn't necessary to invoke all these functions each and every time. What +functions to use in which case mostly depends on the goal and there is not a +golden rule to do that. + +The `entities` member function asks to the registry to serialize all the +entities that are still in use along with their versions. On the other side, the +`destroyed` member function tells to the registry to serialize the entities that +have been destroyed and are no longer in use.
+These two functions can be used to save and restore the whole set of entities +with the versions they had during serialization. + +The `component` member function is a function template the aim of which is to +store aside components. The presence of a template parameter list is a +consequence of a couple of design choices from the past and in the present: + +* First of all, there is no reason to force an user to serialize all the + components at once and most of the times it isn't desiderable. As an example, + in case the stuff for the HUD in a game is put into the registry for some + reasons, its components can be freely discarded during a serialization step + because probably the software already knows how to reconstruct the HUD + correctly from scratch. + +* Furthermore, the registry makes heavy use of _type-erasure_ techniques + internally and doesn't know at any time what types of components it contains. + Therefore being explicit at the call point is mandatory. + +The `tag` member function is similar to the previous one, apart from the fact +that it works with tags and not with components.
+Note also that both `component` and `tag` store items along with entities. It +means that they work properly without a call to the `entities` member function. + +Once a snapshot is created, there exist mainly two _ways_ to load it: as a whole +and in a kind of _continuous mode_.
+The following sections describe both loaders and archives in details. + +#### Snapshot loader + +A snapshot loader requires that the destination registry be empty and loads all +the data at once while keeping intact the identifiers that the entities +originally had.
+To do that, the registry offers a member function named `restore` that returns a +temporary object properly initialized to _restore_ a snapshot. + +Example of use: + +```cpp +InputArchive input; + +registry.restore() + .entities() + .destroyed() + .component(output) + .tag(output) + .orphans(); +``` + +It isn't necessary to invoke all these functions each and every time. What +functions to use in which case mostly depends on the goal and there is not a +golden rule to do that. For obvious reasons, what is important is that the data +are restored in exactly the same order in which they were serialized. + +The `entities` and `destroyed` member functions restore the sets of entities and +the versions that the entities originally had at the source. + +The `component` member function restores all and only the components specified +and assigns them to the right entities. Note that the template parameter list +must be exactly the same used during the serialization. The same applies to the +`tag` member function. + +The `orphans` member function literally destroys those entities that have +neither components nor tags. It's usually useless if the snapshot is a full dump +of the source. However, in case all the entities are serialized but only few +components and tags are saved, it could happen that some of the entities have +neither components nor tags once restored. The best users can do to deal with +them is to destroy those entities and thus update their versions. + +#### Continuous loader + +A continuous loader is designed to load data from a source registry to a +(possibly) non-empty destination. The loader can accomodate in a registry more +than one snapshot in a sort of _continuous loading_ that updates the +destination one step at a time.
+Identifiers that entities originally had are not transferred to the target. +Instead, the loader maps remote identifiers to local ones while restoring a +snapshot. Because of that, this kind of loader offers a way to update +automatically identifiers that are part of components or tags (as an example, as +data members or gathered in a container).
+Another difference with the snapshot loader is that the continuous loader does +not need to work with the private data structures of a registry. Furthermore, it +has an internal state that must persist over time. Therefore, there is no reason +to create it by means of a registry, or to limit its lifetime to that of a +temporary object. + +Example of use: + +```cpp +entt::ContinuousLoader loader{registry}; +InputArchive input; + +loader.entities(input) + .destroyed(input) + .component(input) + .component(input, &DirtyComponent::parent, &DirtyComponent::child) + .tag(input) + .tag(input, &DirtyTag::container) + .orphans() + .shrink(); +``` + +It isn't necessary to invoke all these functions each and every time. What +functions to use in which case mostly depends on the goal and there is not a +golden rule to do that. For obvious reasons, what is important is that the data +are restored in exactly the same order in which they were serialized. + +The `entities` and `destroyed` member functions restore groups of entities and +map each entity to a local counterpart when required. In other terms, for each +remote entity identifier not yet registered by the loader, the latter creates a +local identifier so that it can keep the local entity in sync with the remote +one. + +The `component` and `tag` member functions restore all and only the components +and the tags specified and assign them to the right entities.
+In case the component or the tag contains entities itself (either as data +members of type `entity_type` or as containers of entities), the loader can +update them automatically. To do that, it's enough to specify the data members +to update as shown in the example. If the component or the tag was in the middle +of the template parameter list during serialization, multiple commands are +required during a restore: + +```cpp +registry.snapshot().component(); + +// ... + +loader + .component(input) + .component(input, &AMoreComplexComponent::entity); + .component(input); +``` + +The `orphans` member function literally destroys those entities that have +neither components nor tags after a restore. It has exactly the same purpose +described in the previous section and works the same way. + +Finally, `shrink` helps to purge local entities that no longer have a remote +conterpart. Users should invoke this member function after restoring each +snapshot, unless they know exactly what they are doing. + +#### Archives + +Archives must publicly expose a predefined set of member functions. The API is +straightforward and consists only of a group of function call operators that +are invoked by the registry. + +In particular: + +* An output archive, the one used when creating a snapshot, must expose a + function call operator with the following signature to store entities: + + ```cpp + void operator()(Entity); + ``` + + Where `Entity` is the type of the entities used by the registry.
+ In addition, it must accept the types of both the components and the tags to + serialize. Therefore, given a type `T` (either a component or a tag), it must + contain a function call operator with the following signature: + + ```cpp + void operator()(const T &); + ``` + + The output archive can freely decide how to serialize the data. The register + is not affected at all by the decision. + +* An input archive, the one used when restoring a snapshot, must expose a + function call operator with the following signature to load entities: + + ```cpp + void operator()(Entity &); + ``` + + Where `Entity` is the type of the entities used by the registry. Each time the + function is invoked, the archive must read the next element from the + underlying storage and copy it in the given variable.
+ In addition, it must accept the types of both the components and the tags to + restore. Therefore, given a type `T` (either a component or a tag), it must + contain a function call operator with the following signature: + + ```cpp + void operator()(T &); + ``` + + Every time such an operator is invoked, the archive must read the next element + from the underlying storage and copy it in the given variable. + +`EnTT` comes with some examples (actually some tests) that show how to integrate +a well known library for serialization as an archive. It uses +[`Cereal C++`](https://uscilab.github.io/cereal/) under the hood, mainly +because I wanted to learn how it works at the time I was writing the code. + +The code is not production-ready and it isn't neither the only nor (probably) +the best way to do it. However, feel free to use it at your own risk. + ## View: to persist or not to persist? First of all, it is worth answering an obvious question: why views?
@@ -2223,7 +2467,7 @@ just click [here](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business= ## Hire me If you start using `EnTT` and need help, if you want a new feature and want me -to give it the highest priority, or for any other reason, I'm available for -hiring.
+to give it the highest priority, if you have any other reason to contact me: +do not hesitate. I'm available for hiring.
Feel free to take a look at my [profile](https://github.com/skypjack) and contact me by mail. diff --git a/cmake/in/cereal.in b/cmake/in/cereal.in new file mode 100644 index 000000000..ef353b7e9 --- /dev/null +++ b/cmake/in/cereal.in @@ -0,0 +1,19 @@ +project(cereal-download NONE) +cmake_minimum_required(VERSION 3.2) + +include(ExternalProject) + +ExternalProject_Add( + cereal + GIT_REPOSITORY https://github.com/USCiLab/cereal.git + GIT_TAG v1.2.2 + DOWNLOAD_DIR ${CEREAL_DEPS_DIR} + TMP_DIR ${CEREAL_DEPS_DIR}/tmp + STAMP_DIR ${CEREAL_DEPS_DIR}/stamp + SOURCE_DIR ${CEREAL_DEPS_DIR}/src + BINARY_DIR ${CEREAL_DEPS_DIR}/build + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND "" + TEST_COMMAND "" +) diff --git a/src/entt/entity/registry.hpp b/src/entt/entity/registry.hpp index d5c88f7a2..da8ad0773 100644 --- a/src/entt/entity/registry.hpp +++ b/src/entt/entity/registry.hpp @@ -13,6 +13,7 @@ #include #include "../core/family.hpp" #include "entt_traits.hpp" +#include "snapshot.hpp" #include "sparse_set.hpp" #include "view.hpp" @@ -949,10 +950,11 @@ public: if(available) { for(auto pos = entities.size(); pos; --pos) { const entity_type curr = pos - 1; - const auto entt = entities[curr] & traits_type::entity_mask; + const auto entity = entities[curr]; + const auto entt = entity & traits_type::entity_mask; if(curr == entt) { - func(entities[curr]); + func(entity); } } } else { @@ -1183,6 +1185,77 @@ public: return RawView{ensure()}; } + /** + * @brief Returns a temporary object to use to create snapshots. + * + * A snapshot is either a full or a partial dump of a registry.
+ * It can be used to save and restore its internal state or to keep two or + * more instances of this class in sync, as an example in a client-server + * architecture. + * + * @return A not movable and not copyable object to use to take snasphosts. + */ + Snapshot snapshot() const { + using follow_fn_type = entity_type(*)(const Registry &, entity_type); + using raw_fn_type = const entity_type *(*)(const Registry &, component_type); + const entity_type seed = available ? (next | (entities[next] & ~traits_type::entity_mask)) : next; + + follow_fn_type follow = [](const Registry ®istry, entity_type entity) -> entity_type { + const auto &entities = registry.entities; + const auto entt = entity & traits_type::entity_mask; + const auto next = entities[entt] & traits_type::entity_mask; + return (next | (entities[next] & ~traits_type::entity_mask)); + }; + + raw_fn_type raw = [](const Registry ®istry, component_type component) -> const entity_type * { + const auto &pools = registry.pools; + return (component < pools.size() && pools[component]) ? pools[component]->data() : nullptr; + }; + + return { *this, seed, available, follow, raw }; + } + + /** + * @brief Returns a temporary object to use to load snapshots. + * + * A snapshot is either a full or a partial dump of a registry.
+ * It can be used to save and restore its internal state or to keep two or + * more instances of this class in sync, as an example in a client-server + * architecture. + * + * @warning + * The loader returned by this function requires that the registry be empty. + * In case it isn't, all the data will be automatically deleted before to + * return. + * + * @return A not movable and not copyable object to use to load snasphosts. + */ + SnapshotLoader restore() { + using ensure_fn_type = void(*)(Registry &, entity_type, bool); + + ensure_fn_type ensure = [](Registry ®istry, entity_type entity, bool destroyed) { + using promotion_type = std::conditional_t= sizeof(entity_type), size_type, entity_type>; + // explicit promotion to avoid warnings with std::uint16_t + const auto entt = promotion_type{entity} & traits_type::entity_mask; + auto &entities = registry.entities; + + if(!(entt < entities.size())) { + auto curr = entities.size(); + entities.resize(entt + 1); + std::iota(entities.data() + curr, entities.data() + entt, entity_type(curr)); + } + + entities[entt] = entity; + + if(destroyed) { + registry.destroy(entity); + const auto version = (entity & (~traits_type::entity_mask)); + entities[entt] = ((entities[entt] & traits_type::entity_mask) | version); + } + }; + + return { (*this = {}), ensure }; + } private: std::vector>> handlers; diff --git a/src/entt/entity/snapshot.hpp b/src/entt/entity/snapshot.hpp new file mode 100644 index 000000000..80e57a1a3 --- /dev/null +++ b/src/entt/entity/snapshot.hpp @@ -0,0 +1,713 @@ +#ifndef ENTT_ENTITY_SNAPSHOT_HPP +#define ENTT_ENTITY_SNAPSHOT_HPP + + +#include +#include +#include +#include +#include +#include +#include +#include "entt_traits.hpp" + + +namespace entt { + + +/** + * @brief Forward declaration of the registry class. + */ +template +class Registry; + + +/** + * @brief Utility class to create snapshots from a registry. + * + * A _snapshot_ can be either a dump of the entire registry or a narrower + * selection of components and tags of interest.
+ * This type can be used in both cases if provided with a correctly configured + * output archive. + * + * @tparam Entity A valid entity type (see entt_traits for more details). + */ +template +class Snapshot final { + /*! @brief A registry is allowed to create snapshots. */ + friend class Registry; + + using follow_fn_type = Entity(*)(const Registry &, Entity); + using raw_fn_type = const Entity *(*)(const Registry &, typename Registry::component_type); + + Snapshot(const Registry ®istry, Entity seed, std::size_t size, follow_fn_type follow, raw_fn_type raw) noexcept + : registry{registry}, + seed{seed}, + size{size}, + follow{follow}, + raw{raw} + {} + + Snapshot(const Snapshot &) = default; + Snapshot(Snapshot &&) = default; + + Snapshot & operator=(const Snapshot &) = default; + Snapshot & operator=(Snapshot &&) = default; + + template + void get(Archive &archive, const Registry ®istry) { + const auto component = registry.template component(); + const auto sz = registry.template size(); + const auto *entities = raw(registry, component); + + archive(static_cast(sz)); + + for(std::remove_const_t i{}; i < sz; ++i) { + const auto entity = entities[i]; + archive(entity); + archive(registry.template get(entity)); + }; + } + + template + void get(Archive &archive) { + const bool has = registry.template has(); + + // numerical length is forced for tags to facilitate loading + archive(has ? Entity(1): Entity{}); + + if(has) { + archive(registry.template attachee()); + archive(registry.template get()); + } + } + +public: + /** + * @brief Puts aside all the entities that are still in use. + * + * Entities are serialized along with their versions. Destroyed entities are + * not taken in consideration by this function. + * + * @tparam Archive Type of output archive. + * @param archive A valid reference to an output archive. + * @return An object of this type to continue creating the snapshot. + */ + template + Snapshot entities(Archive &archive) && { + archive(static_cast(registry.size())); + registry.each([&archive, this](auto entity) { archive(entity); }); + return *this; + } + + /** + * @brief Puts aside destroyed entities. + * + * Entities are serialized along with their versions. Entities that are + * still in use are not taken in consideration by this function. + * + * @tparam Archive Type of output archive. + * @param archive A valid reference to an output archive. + * @return An object of this type to continue creating the snapshot. + */ + template + Snapshot destroyed(Archive &archive) && { + archive(static_cast(size)); + + if(size) { + auto curr = seed; + archive(curr); + + for(auto i = size - 1; i; --i) { + curr = follow(registry, curr); + archive(curr); + } + } + + return *this; + } + + /** + * @brief Puts aside the given components. + * + * Each component is serialized together with the entity to which it + * belongs. Entities are serialized along with their versions. + * + * @tparam Component Types of components to serialize. + * @tparam Archive Type of output archive. + * @param archive A valid reference to an output archive. + * @return An object of this type to continue creating the snapshot. + */ + template + Snapshot component(Archive &archive) && { + using accumulator_type = int[]; + accumulator_type accumulator = { 0, (get(archive, registry), 0)... }; + (void)accumulator; + return *this; + } + + /** + * @brief Puts aside the given tags. + * + * Each tag is serialized together with the entity to which it belongs. + * Entities are serialized along with their versions. + * + * @tparam Tag Types of tags to serialize. + * @tparam Archive Type of output archive. + * @param archive A valid reference to an output archive. + * @return An object of this type to continue creating the snapshot. + */ + template + Snapshot tag(Archive &archive) && { + using accumulator_type = int[]; + accumulator_type accumulator = { 0, (get(archive), 0)... }; + (void)accumulator; + return *this; + } + +private: + const Registry ®istry; + const Entity seed; + const std::size_t size; + follow_fn_type follow; + raw_fn_type raw; +}; + + +/** + * @brief Utility class to restore a snapshot as a whole. + * + * A snapshot loader requires that the destination registry be empty and loads + * all the data at once while keeping intact the identifiers that the entities + * originally had.
+ * An example of use is the implementation of a save/restore utility. + * + * @tparam Entity A valid entity type (see entt_traits for more details). + */ +template +class SnapshotLoader final { + /*! @brief A registry is allowed to create snapshot loaders. */ + friend class Registry; + + using ensure_fn_type = void(*)(Registry &, Entity, bool); + + SnapshotLoader(Registry ®istry, ensure_fn_type ensure_fn) noexcept + : registry{registry}, + ensure_fn{ensure_fn} + { + // restore a snapshot as a whole requires a clean registry + assert(!registry.capacity()); + } + + SnapshotLoader(const SnapshotLoader &) = default; + SnapshotLoader(SnapshotLoader &&) = default; + + SnapshotLoader & operator=(const SnapshotLoader &) = default; + SnapshotLoader & operator=(SnapshotLoader &&) = default; + + template + void each(Archive &archive, Func func) { + Entity length{}; + archive(length); + + while(length) { + Entity entity{}; + archive(entity); + func(entity); + --length; + } + } + + template + void assign(Archive &archive) { + each(archive, [&archive, this](auto entity) { + static constexpr auto destroyed = false; + ensure_fn(registry, entity, destroyed); + archive(registry.template assign(entity)); + }); + } + + template + void attach(Archive &archive) { + each(archive, [&archive, this](auto entity) { + static constexpr auto destroyed = false; + ensure_fn(registry, entity, destroyed); + archive(registry.template attach(entity)); + }); + } + +public: + /** + * @brief Restores entities that were in use during serialization. + * + * This function restores the entities that were in use during serialization + * and gives them the versions they originally had. + * + * @tparam Archive Type of input archive. + * @param archive A valid reference to an input archive. + * @return A valid loader to continue restoring data. + */ + template + SnapshotLoader entities(Archive &archive) && { + each(archive, [this](auto entity) { + static constexpr auto destroyed = false; + ensure_fn(registry, entity, destroyed); + }); + + return *this; + } + + /** + * @brief Restores entities that were destroyed during serialization. + * + * This function restores the entities that were destroyed during + * serialization and gives them the versions they originally had. + * + * @tparam Archive Type of input archive. + * @param archive A valid reference to an input archive. + * @return A valid loader to continue restoring data. + */ + template + SnapshotLoader destroyed(Archive &archive) && { + each(archive, [this](auto entity) { + static constexpr auto destroyed = true; + ensure_fn(registry, entity, destroyed); + }); + + return *this; + } + + /** + * @brief Restores components and assigns them to the right entities. + * + * The template parameter list must be exactly the same used during + * serialization. In the event that the entity to which the component is + * assigned doesn't exist yet, the loader will take care to create it with + * the version it originally had. + * + * @tparam Component Types of components to restore. + * @tparam Archive Type of input archive. + * @param archive A valid reference to an input archive. + * @return A valid loader to continue restoring data. + */ + template + SnapshotLoader component(Archive &archive) && { + using accumulator_type = int[]; + accumulator_type accumulator = { 0, (assign(archive), 0)... }; + (void)accumulator; + return *this; + } + + /** + * @brief Restores tags and assigns them to the right entities. + * + * The template parameter list must be exactly the same used during + * serialization. In the event that the entity to which the tag is assigned + * doesn't exist yet, the loader will take care to create it with the + * version it originally had. + * + * @tparam Tag Types of tags to restore. + * @tparam Archive Type of input archive. + * @param archive A valid reference to an input archive. + * @return A valid loader to continue restoring data. + */ + template + SnapshotLoader tag(Archive &archive) && { + using accumulator_type = int[]; + accumulator_type accumulator = { 0, (attach(archive), 0)... }; + (void)accumulator; + return *this; + } + + + /** + * @brief Destroys those entities that have neither components nor tags. + * + * In case all the entities were serialized but only part of the components + * and tags was saved, it could happen that some of the entities have + * neither components nor tags once restored.
+ * This functions helps to identify and destroy those entities. + * + * @return A valid loader to continue restoring data. + */ + SnapshotLoader orphans() && { + registry.orphans([this](auto entity) { + registry.destroy(entity); + }); + + return *this; + } + +private: + Registry ®istry; + ensure_fn_type ensure_fn; +}; + + +/** + * @brief Utility class for _continuous loading_. + * + * A _continuous loader_ is designed to load data from a source registry to a + * (possibly) non-empty destination. The loader can accomodate in a registry + * more than one snapshot in a sort of _continuous loading_ that updates the + * destination one step at a time.
+ * Identifiers that entities originally had are not transferred to the target. + * Instead, the loader maps remote identifiers to local ones while restoring a + * snapshot.
+ * An example of use is the implementation of a client-server applications with + * the requirement of transferring somehow parts of the representation side to + * side. + * + * @tparam Entity A valid entity type (see entt_traits for more details). + */ +template +class ContinuousLoader final { + using traits_type = entt_traits; + + Entity destroy(Entity entity) { + const auto it = remloc.find(entity); + + if(it == remloc.cend()) { + const auto local = registry.create(); + remloc.emplace(entity, std::make_pair(local, true)); + registry.destroy(local); + } + + return remloc[entity].first; + } + + Entity restore(Entity entity) { + const auto it = remloc.find(entity); + + if(it == remloc.cend()) { + const auto local = registry.create(); + remloc.emplace(entity, std::make_pair(local, true)); + } else { + remloc[entity].first = + registry.valid(remloc[entity].first) + ? remloc[entity].first + : registry.create(); + + // set the dirty flag + remloc[entity].second = true; + } + + return remloc[entity].first; + } + + template + std::enable_if_t::value> + update(Instance &instance, Type Instance::*member) { + instance.*member = map(instance.*member); + } + + template + std::enable_if_t::value_type, Entity>::value> + update(Instance &instance, Type Instance::*member) { + for(auto &entity: (instance.*member)) { + entity = map(entity); + } + } + + template + void each(Archive &archive, Func func) { + Entity length{}; + archive(length); + + while(length) { + Entity entity{}; + archive(entity); + func(entity); + --length; + } + } + + template + void reset() { + for(auto &&ref: remloc) { + const auto local = ref.second.first; + + if(registry.valid(local)) { + registry.template reset(local); + } + } + } + + template + void assign(Archive &archive) { + reset(); + + each(archive, [&archive, this](auto entity) { + entity = restore(entity); + archive(registry.template accommodate(entity)); + }); + } + + template + void assign(Archive &archive, Type Component::*... member) { + reset(); + + each(archive, [&archive, member..., this](auto entity) { + entity = restore(entity); + auto &component = registry.template accommodate(entity); + archive(component); + + using accumulator_type = int[]; + accumulator_type accumulator = { 0, (update(component, member), 0)... }; + (void)accumulator; + }); + } + + template + void attach(Archive &archive) { + registry.template remove(); + + each(archive, [&archive, this](auto entity) { + entity = restore(entity); + archive(registry.template attach(entity)); + }); + } + + template + void attach(Archive &archive, Type Tag::*... member) { + registry.template remove(); + + each(archive, [&archive, member..., this](auto entity) { + entity = restore(entity); + auto &tag = registry.template attach(entity); + archive(tag); + + using accumulator_type = int[]; + accumulator_type accumulator = { 0, (update(tag, member), 0)... }; + (void)accumulator; + }); + } + +public: + /*! @brief Underlying entity identifier. */ + using entity_type = Entity; + + /** + * @brief Constructs a loader that is bound to a given registry. + * @param registry A valid reference to a registry. + */ + ContinuousLoader(Registry ®istry) noexcept + : registry{registry} + {} + + /*! @brief Default copy constructor. */ + ContinuousLoader(const ContinuousLoader &) = default; + /*! @brief Default move constructor. */ + ContinuousLoader(ContinuousLoader &&) = default; + + /*! @brief Default copy assignment operator. @return This loader. */ + ContinuousLoader & operator=(const ContinuousLoader &) = default; + /*! @brief Default move assignment operator. @return This loader. */ + ContinuousLoader & operator=(ContinuousLoader &&) = default; + + /** + * @brief Restores entities that were in use during serialization. + * + * This function restores the entities that were in use during serialization + * and creates local counterparts for them if required. + * + * @tparam Archive Type of input archive. + * @param archive A valid reference to an input archive. + * @return A non-const reference to this loader. + */ + template + ContinuousLoader & entities(Archive &archive) { + each(archive, [this](auto entity) { restore(entity); }); + return *this; + } + + /** + * @brief Restores entities that were destroyed during serialization. + * + * This function restores the entities that were destroyed during + * serialization and creates local counterparts for them if required. + * + * @tparam Archive Type of input archive. + * @param archive A valid reference to an input archive. + * @return A non-const reference to this loader. + */ + template + ContinuousLoader & destroyed(Archive &archive) { + each(archive, [this](auto entity) { destroy(entity); }); + return *this; + } + + /** + * @brief Restores components and assigns them to the right entities. + * + * The template parameter list must be exactly the same used during + * serialization. In the event that the entity to which the component is + * assigned doesn't exist yet, the loader will take care to create a local + * counterpart for it. + * + * @tparam Component Types of components to restore. + * @tparam Archive Type of input archive. + * @param archive A valid reference to an input archive. + * @return A non-const reference to this loader. + */ + template + ContinuousLoader & component(Archive &archive) { + using accumulator_type = int[]; + accumulator_type accumulator = { 0, (assign(archive), 0)... }; + (void)accumulator; + return *this; + } + + /** + * @brief Restores components and assigns them to the right entities. + * + * The template parameter list must be exactly the same used during + * serialization. In the event that the entity to which the component is + * assigned doesn't exist yet, the loader will take care to create a local + * counterpart for it.
+ * Members can be either data members of type entity_type or containers of + * entities. In both cases, the loader will visit them and update the + * entities by replacing each one with its local counterpart. + * + * @tparam Component Type of component to restore. + * @tparam Archive Type of input archive. + * @tparam Type Types of members to update with their local counterparts. + * @param archive A valid reference to an input archive. + * @param member Members to update with their local counterparts. + * @return A non-const reference to this loader. + */ + template + ContinuousLoader & component(Archive &archive, Type Component::*... member) { + assign(archive, member...); + return *this; + } + + /** + * @brief Restores tags and assigns them to the right entities. + * + * The template parameter list must be exactly the same used during + * serialization. In the event that the entity to which the tag is assigned + * doesn't exist yet, the loader will take care to create a local + * counterpart for it. + * + * @tparam Tag Types of tags to restore. + * @tparam Archive Type of input archive. + * @param archive A valid reference to an input archive. + * @return A non-const reference to this loader. + */ + template + ContinuousLoader & tag(Archive &archive) { + using accumulator_type = int[]; + accumulator_type accumulator = { 0, (attach(archive), 0)... }; + (void)accumulator; + return *this; + } + + /** + * @brief Restores tags and assigns them to the right entities. + * + * The template parameter list must be exactly the same used during + * serialization. In the event that the entity to which the tag is assigned + * doesn't exist yet, the loader will take care to create a local + * counterpart for it.
+ * Members can be either data members of type entity_type or containers of + * entities. In both cases, the loader will visit them and update the + * entities by replacing each one with its local counterpart. + * + * @tparam Tag Type of tag to restore. + * @tparam Archive Type of input archive. + * @tparam Type Types of members to update with their local counterparts. + * @param archive A valid reference to an input archive. + * @param member Members to update with their local counterparts. + * @return A non-const reference to this loader. + */ + template + ContinuousLoader & tag(Archive &archive, Type Tag::*... member) { + attach(archive, member...); + return *this; + } + + /** + * @brief Helps to purge entities that no longer have a conterpart. + * + * Users should invoke this member function after restoring each snapshot, + * unless they know exactly what they are doing. + * + * @return A non-const reference to this loader. + */ + ContinuousLoader & shrink() { + auto it = remloc.begin(); + + while(it != remloc.cend()) { + const auto local = it->second.first; + bool &dirty = it->second.second; + + if(dirty) { + dirty = false; + ++it; + } else { + if(registry.valid(local)) { + registry.destroy(local); + } + + it = remloc.erase(it); + } + } + + return *this; + } + + /** + * @brief Destroys those entities that have neither components nor tags. + * + * In case all the entities were serialized but only part of the components + * and tags was saved, it could happen that some of the entities have + * neither components nor tags once restored.
+ * This functions helps to identify and destroy those entities. + * + * @return A non-const reference to this loader. + */ + ContinuousLoader & orphans() { + registry.orphans([this](auto entity) { + registry.destroy(entity); + }); + + return *this; + } + + /** + * @brief Tests if a loader knows about a given entity. + * @param entity An entity identifier. + * @return True if `entity` is managed by the loader, false otherwise. + */ + bool has(entity_type entity) { + return !(remloc.find(entity) == remloc.cend()); + } + + /** + * @brief Returns the identifier to which an entity refers. + * + * @warning + * Attempting to use an entity that isn't managed by the loader results in + * undefined behavior.
+ * An assertion will abort the execution at runtime in debug mode if the + * loader doesn't knows about the entity. + * + * @param entity An entity identifier. + * @return The identifier to which `entity` refers in the target registry. + */ + entity_type map(entity_type entity) { + assert(has(entity)); + return remloc[entity].first; + } + +private: + std::unordered_map> remloc; + Registry ®istry; +}; + + +} + + +#endif // ENTT_ENTITY_SNAPSHOT_HPP diff --git a/src/entt/entt.hpp b/src/entt/entt.hpp index 7e03e07fe..6e431402a 100644 --- a/src/entt/entt.hpp +++ b/src/entt/entt.hpp @@ -4,6 +4,7 @@ #include "entity/actor.hpp" #include "entity/entt_traits.hpp" #include "entity/registry.hpp" +#include "entity/snapshot.hpp" #include "entity/sparse_set.hpp" #include "entity/view.hpp" #include "locator/locator.hpp" diff --git a/src/entt/signal/sigh.hpp b/src/entt/signal/sigh.hpp index 75f7b35e8..ae18424eb 100644 --- a/src/entt/signal/sigh.hpp +++ b/src/entt/signal/sigh.hpp @@ -100,7 +100,7 @@ class SigH; * * * `Param` is a type to which `Ret` can be converted. * * The return type is true if the handler must stop collecting data, false - * otherwise. + * otherwise. * * @tparam Ret Return type of a function type. * @tparam Args Types of arguments of a function type. diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 32bd6d39b..04d3c438f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -36,6 +36,25 @@ if(BUILD_MOD) add_test(NAME mod COMMAND mod) endif() +# Test snapshot + +if(BUILD_SNAPSHOT) + set(CEREAL_DEPS_DIR ${entt_SOURCE_DIR}/deps/cereal) + configure_file(${entt_SOURCE_DIR}/cmake/in/cereal.in ${CEREAL_DEPS_DIR}/CMakeLists.txt) + execute_process(COMMAND ${CMAKE_COMMAND} -G "${CMAKE_GENERATOR}" . WORKING_DIRECTORY ${CEREAL_DEPS_DIR}) + execute_process(COMMAND ${CMAKE_COMMAND} --build . WORKING_DIRECTORY ${CEREAL_DEPS_DIR}) + set(CEREAL_SRC_DIR ${CEREAL_DEPS_DIR}/src/include) + + add_executable( + snapshot + $ + snapshot/snapshot.cpp + ) + target_include_directories(snapshot PRIVATE ${CEREAL_SRC_DIR}) + target_link_libraries(snapshot PRIVATE gtest_main Threads::Threads) + add_test(NAME snapshot COMMAND snapshot) +endif() + # Test core add_executable( @@ -55,6 +74,7 @@ add_executable( $ entt/entity/actor.cpp entt/entity/registry.cpp + entt/entity/snapshot.cpp entt/entity/sparse_set.cpp entt/entity/view.cpp ) diff --git a/test/entt/entity/snapshot.cpp b/test/entt/entity/snapshot.cpp new file mode 100644 index 000000000..ea9b91b8f --- /dev/null +++ b/test/entt/entity/snapshot.cpp @@ -0,0 +1,489 @@ +#include +#include +#include +#include +#include + +template +struct OutputArchive { + OutputArchive(Storage &storage) + : storage{storage} + {} + + template + void operator()(const Value &value) { + std::get>(storage).push(value); + } + +private: + Storage &storage; +}; + +template +struct InputArchive { + InputArchive(Storage &storage) + : storage{storage} + {} + + template + void operator()(Value &value) { + auto &queue = std::get>(storage); + value = queue.front(); + queue.pop(); + } + +private: + Storage &storage; +}; + +struct AComponent {}; + +struct AnotherComponent { + int key; + int value; +}; + +struct Foo { + entt::DefaultRegistry::entity_type bar; + std::vector quux; +}; + +TEST(Snapshot, Dump) { + entt::DefaultRegistry registry; + + auto e0 = registry.create(); + registry.assign(e0, 42); + registry.assign(e0, 'c'); + registry.assign(e0, .1); + + auto e1 = registry.create(); + + auto e2 = registry.create(); + registry.assign(e2, 3); + + auto e3 = registry.create(); + registry.assign(e3, '0'); + registry.attach(e3, .3f); + + auto e4 = registry.create(); + registry.attach(e4); + + registry.destroy(e1); + auto v1 = registry.current(e1); + + using storage_type = std::tuple< + std::queue, + std::queue, + std::queue, + std::queue, + std::queue, + std::queue, + std::queue, + std::queue, + std::queue + >; + + storage_type storage; + OutputArchive output{storage}; + InputArchive input{storage}; + + registry.snapshot() + .entities(output) + .destroyed(output) + .component(output) + .tag(output); + + registry.reset(); + + ASSERT_FALSE(registry.valid(e0)); + ASSERT_FALSE(registry.valid(e1)); + ASSERT_FALSE(registry.valid(e2)); + ASSERT_FALSE(registry.valid(e3)); + ASSERT_FALSE(registry.valid(e4)); + + registry.restore() + .entities(input) + .destroyed(input) + .component(input) + .tag(input) + .orphans(); + + ASSERT_TRUE(registry.valid(e0)); + ASSERT_FALSE(registry.valid(e1)); + ASSERT_TRUE(registry.valid(e2)); + ASSERT_TRUE(registry.valid(e3)); + ASSERT_TRUE(registry.valid(e4)); + + ASSERT_FALSE(registry.orphan(e0)); + ASSERT_FALSE(registry.orphan(e2)); + ASSERT_FALSE(registry.orphan(e3)); + ASSERT_FALSE(registry.orphan(e4)); + + ASSERT_EQ(registry.get(e0), 42); + ASSERT_EQ(registry.get(e0), 'c'); + ASSERT_EQ(registry.get(e0), .1); + ASSERT_EQ(registry.current(e1), v1); + ASSERT_EQ(registry.get(e2), 3); + ASSERT_EQ(registry.get(e3), '0'); + + ASSERT_TRUE(registry.has()); + ASSERT_EQ(registry.attachee(), e3); + ASSERT_EQ(registry.get(), .3f); + + ASSERT_TRUE(registry.has()); + ASSERT_EQ(registry.attachee(), e4); + + ASSERT_TRUE(registry.empty()); + ASSERT_FALSE(registry.has()); +} + +TEST(Snapshot, Partial) { + entt::DefaultRegistry registry; + + auto e0 = registry.create(); + registry.assign(e0, 42); + registry.assign(e0, 'c'); + registry.assign(e0, .1); + + auto e1 = registry.create(); + + auto e2 = registry.create(); + registry.assign(e2, 3); + + auto e3 = registry.create(); + registry.assign(e3, '0'); + registry.attach(e3, .3f); + + auto e4 = registry.create(); + registry.attach(e4); + + registry.destroy(e1); + auto v1 = registry.current(e1); + + using storage_type = std::tuple< + std::queue, + std::queue, + std::queue, + std::queue, + std::queue, + std::queue, + std::queue, + std::queue + >; + + storage_type storage; + OutputArchive output{storage}; + InputArchive input{storage}; + + registry.snapshot() + .entities(output) + .destroyed(output) + .component(output) + .tag(output); + + registry.reset(); + + ASSERT_FALSE(registry.valid(e0)); + ASSERT_FALSE(registry.valid(e1)); + ASSERT_FALSE(registry.valid(e2)); + ASSERT_FALSE(registry.valid(e3)); + ASSERT_FALSE(registry.valid(e4)); + + registry.restore() + .entities(input) + .destroyed(input) + .component(input) + .tag(input); + + ASSERT_TRUE(registry.valid(e0)); + ASSERT_FALSE(registry.valid(e1)); + ASSERT_TRUE(registry.valid(e2)); + ASSERT_TRUE(registry.valid(e3)); + ASSERT_TRUE(registry.valid(e4)); + + ASSERT_EQ(registry.get(e0), 42); + ASSERT_EQ(registry.get(e0), 'c'); + ASSERT_FALSE(registry.has(e0)); + ASSERT_EQ(registry.current(e1), v1); + ASSERT_EQ(registry.get(e2), 3); + ASSERT_EQ(registry.get(e3), '0'); + ASSERT_TRUE(registry.orphan(e4)); + + ASSERT_TRUE(registry.has()); + ASSERT_EQ(registry.attachee(), e3); + ASSERT_EQ(registry.get(), .3f); + ASSERT_FALSE(registry.has()); + + registry.snapshot() + .tag(output) + .destroyed(output) + .entities(output); + + registry.reset(); + + ASSERT_FALSE(registry.valid(e0)); + ASSERT_FALSE(registry.valid(e1)); + ASSERT_FALSE(registry.valid(e2)); + ASSERT_FALSE(registry.valid(e3)); + ASSERT_FALSE(registry.valid(e4)); + + registry.restore() + .tag(input) + .destroyed(input) + .entities(input) + .orphans(); + + ASSERT_FALSE(registry.valid(e0)); + ASSERT_FALSE(registry.valid(e1)); + ASSERT_FALSE(registry.valid(e2)); + ASSERT_TRUE(registry.valid(e3)); + ASSERT_FALSE(registry.valid(e4)); +} + +TEST(Snapshot, Continuous) { + using entity_type = entt::DefaultRegistry::entity_type; + + entt::DefaultRegistry src; + entt::DefaultRegistry dst; + + entt::ContinuousLoader loader{dst}; + + std::vector entities; + entity_type entity; + + using storage_type = std::tuple< + std::queue, + std::queue, + std::queue, + std::queue, + std::queue + >; + + storage_type storage; + OutputArchive output{storage}; + InputArchive input{storage}; + + for(int i = 0; i < 10; ++i) { + src.create(); + } + + src.each([&src](auto entity) { + src.destroy(entity); + }); + + for(int i = 0; i < 5; ++i) { + entity = src.create(); + entities.push_back(entity); + + src.assign(entity); + src.assign(entity, i, i); + + if(i % 2) { + src.assign(entity, entity); + } else if(i == 2) { + src.attach(entity, .3); + } + } + + src.view().each([&entities](auto, auto &foo) { + foo.quux.insert(foo.quux.begin(), entities.begin(), entities.end()); + }); + + entity = dst.create(); + dst.assign(entity); + dst.assign(entity, -1, -1); + + src.snapshot() + .entities(output) + .destroyed(output) + .component(output) + .tag(output); + + loader.entities(input) + .destroyed(input) + .component(input) + .component(input, &Foo::bar, &Foo::quux) + .tag(input) + .orphans(); + + decltype(dst.size()) aComponentCnt{}; + decltype(dst.size()) anotherComponentCnt{}; + decltype(dst.size()) fooCnt{}; + + dst.each([&dst, &aComponentCnt](auto entity) { + ASSERT_TRUE(dst.has(entity)); + ++aComponentCnt; + }); + + dst.view().each([&anotherComponentCnt](auto, const auto &component) { + ASSERT_EQ(component.value, component.key < 0 ? -1 : component.key); + ++anotherComponentCnt; + }); + + dst.view().each([&dst, &fooCnt](auto entity, const auto &component) { + ASSERT_EQ(entity, component.bar); + + for(auto entity: component.quux) { + ASSERT_TRUE(dst.valid(entity)); + } + + ++fooCnt; + }); + + ASSERT_TRUE(dst.has()); + ASSERT_EQ(dst.get(), .3); + + src.view().each([](auto, auto &component) { + component.value = 2 * component.key; + }); + + auto size = dst.size(); + + src.snapshot() + .entities(output) + .destroyed(output) + .component(output) + .tag(output); + + loader.entities(input) + .destroyed(input) + .component(input) + .component(input, &Foo::bar, &Foo::quux) + .tag(input) + .orphans(); + + ASSERT_EQ(size, dst.size()); + + ASSERT_EQ(dst.size(), aComponentCnt); + ASSERT_EQ(dst.size(), anotherComponentCnt); + ASSERT_EQ(dst.size(), fooCnt); + ASSERT_TRUE(dst.has()); + + dst.view().each([](auto, auto &component) { + ASSERT_EQ(component.value, component.key < 0 ? -1 : (2 * component.key)); + }); + + entity = src.create(); + + src.view().each([entity](auto, auto &component) { + component.bar = entity; + }); + + src.snapshot() + .entities(output) + .destroyed(output) + .component(output) + .tag(output); + + loader.entities(input) + .destroyed(input) + .component(input) + .component(input, &Foo::bar, &Foo::quux) + .tag(input) + .orphans(); + + dst.view().each([&loader, entity](auto, auto &component) { + ASSERT_EQ(component.bar, loader.map(entity)); + }); + + entities.clear(); + for(auto entity: src.view()) { + entities.push_back(entity); + } + + src.destroy(entity); + loader.shrink(); + + src.snapshot() + .entities(output) + .destroyed(output) + .component(output) + .tag(output); + + loader.entities(input) + .destroyed(input) + .component(input) + .component(input, &Foo::bar, &Foo::quux) + .tag(input) + .orphans() + .shrink(); + + dst.view().each([&dst, &loader, entity](auto, auto &component) { + ASSERT_FALSE(dst.valid(component.bar)); + }); + + ASSERT_FALSE(loader.has(entity)); + + entity = src.create(); + + src.view().each([entity](auto, auto &component) { + component.bar = entity; + }); + + dst.reset(); + aComponentCnt = src.size(); + + src.snapshot() + .entities(output) + .destroyed(output) + .component(output) + .tag(output); + + loader.entities(input) + .destroyed(input) + .component(input) + .component(input, &Foo::bar, &Foo::quux) + .tag(input) + .orphans(); + + ASSERT_EQ(dst.size(), aComponentCnt); + ASSERT_TRUE(dst.has()); + + src.reset(); + src.remove(); + aComponentCnt = {}; + + src.snapshot() + .entities(output) + .destroyed(output) + .component(output) + .tag(output); + + loader.entities(input) + .destroyed(input) + .component(input) + .component(input, &Foo::bar, &Foo::quux) + .tag(input) + .orphans(); + + ASSERT_EQ(dst.size(), aComponentCnt); + ASSERT_FALSE(dst.has()); +} + +TEST(Snapshot, ContinuousMoreOnShrink) { + using entity_type = entt::DefaultRegistry::entity_type; + + entt::DefaultRegistry src; + entt::DefaultRegistry dst; + + entt::ContinuousLoader loader{dst}; + + using storage_type = std::tuple< + std::queue, + std::queue + >; + + storage_type storage; + OutputArchive output{storage}; + InputArchive input{storage}; + + auto entity = src.create(); + src.snapshot().entities(output); + loader.entities(input).shrink(); + + ASSERT_TRUE(dst.valid(entity)); + + loader.shrink(); + + ASSERT_FALSE(dst.valid(entity)); +} diff --git a/test/snapshot/snapshot.cpp b/test/snapshot/snapshot.cpp new file mode 100644 index 000000000..35bc70d04 --- /dev/null +++ b/test/snapshot/snapshot.cpp @@ -0,0 +1,182 @@ +#include +#include +#include +#include +#include + +struct Position { + float x; + float y; +}; + +struct Timer { + int duration; + int elapsed{0}; +}; + +struct Relationship { + entt::DefaultRegistry::entity_type parent; +}; + +template +void serialize(Archive &archive, Position &position) { + archive(position.x, position.y); +} + +template +void serialize(Archive &archive, Timer &timer) { + archive(timer.duration); +} + +template +void serialize(Archive &archive, Relationship &relationship) { + archive(relationship.parent); +} + +TEST(Snapshot, Full) { + std::stringstream storage; + + entt::DefaultRegistry source; + entt::DefaultRegistry destination; + + auto e0 = source.create(); + source.assign(e0, 16.f, 16.f); + + source.destroy(source.create()); + + auto e1 = source.create(); + source.assign(e1, .8f, .0f); + source.assign(e1, e0); + + auto e2 = source.create(); + + auto e3 = source.create(); + source.assign(e3, 1000, 100); + + source.destroy(e2); + auto v2 = source.current(e2); + + { + // output finishes flushing its contents when it goes out of scope + cereal::JSONOutputArchive output{storage}; + source.snapshot().entities(output).destroyed(output) + .component(output); + } + + cereal::JSONInputArchive input{storage}; + destination.restore().entities(input).destroyed(input) + .component(input); + + ASSERT_TRUE(destination.valid(e0)); + ASSERT_TRUE(destination.has(e0)); + ASSERT_EQ(destination.get(e0).x, 16.f); + ASSERT_EQ(destination.get(e0).y, 16.f); + + ASSERT_TRUE(destination.valid(e1)); + ASSERT_TRUE(destination.has(e1)); + ASSERT_EQ(destination.get(e1).x, .8f); + ASSERT_EQ(destination.get(e1).y, .0f); + ASSERT_TRUE(destination.has(e1)); + ASSERT_EQ(destination.get(e1).parent, e0); + + ASSERT_FALSE(destination.valid(e2)); + ASSERT_EQ(destination.current(e2), v2); + + ASSERT_TRUE(destination.valid(e3)); + ASSERT_TRUE(destination.has(e3)); + ASSERT_EQ(destination.get(e3).duration, 1000); + ASSERT_EQ(destination.get(e3).elapsed, 0); +} + +TEST(Snapshot, Continuous) { + std::stringstream storage; + + entt::DefaultRegistry source; + entt::DefaultRegistry destination; + + std::vector entities; + for(auto i = 0; i < 10; ++i) { + entities.push_back(source.create()); + } + + for(auto entity: entities) { + source.destroy(entity); + } + + auto e0 = source.create(); + source.assign(e0, 0.f, 0.f); + source.assign(e0, e0); + + auto e1 = source.create(); + source.assign(e1, 1.f, 1.f); + source.assign(e1, e0); + + auto e2 = source.create(); + source.assign(e2, .2f, .2f); + source.assign(e2, e0); + + auto e3 = source.create(); + source.assign(e3, 1000, 1000); + source.assign(e3, e2); + + { + // output finishes flushing its contents when it goes out of scope + cereal::JSONOutputArchive output{storage}; + source.snapshot().entities(output).component(output); + } + + cereal::JSONInputArchive input{storage}; + entt::ContinuousLoader loader{destination}; + loader.entities(input) + .component(input) + .component(input, &Relationship::parent) + .component(input); + + ASSERT_FALSE(destination.valid(e0)); + ASSERT_TRUE(loader.has(e0)); + + auto l0 = loader.map(e0); + + ASSERT_TRUE(destination.valid(l0)); + ASSERT_TRUE(destination.has(l0)); + ASSERT_EQ(destination.get(l0).x, 0.f); + ASSERT_EQ(destination.get(l0).y, 0.f); + ASSERT_TRUE(destination.has(l0)); + ASSERT_EQ(destination.get(l0).parent, l0); + + ASSERT_FALSE(destination.valid(e1)); + ASSERT_TRUE(loader.has(e1)); + + auto l1 = loader.map(e1); + + ASSERT_TRUE(destination.valid(l1)); + ASSERT_TRUE(destination.has(l1)); + ASSERT_EQ(destination.get(l1).x, 1.f); + ASSERT_EQ(destination.get(l1).y, 1.f); + ASSERT_TRUE(destination.has(l1)); + ASSERT_EQ(destination.get(l1).parent, l0); + + ASSERT_FALSE(destination.valid(e2)); + ASSERT_TRUE(loader.has(e2)); + + auto l2 = loader.map(e2); + + ASSERT_TRUE(destination.valid(l2)); + ASSERT_TRUE(destination.has(l2)); + ASSERT_EQ(destination.get(l2).x, .2f); + ASSERT_EQ(destination.get(l2).y, .2f); + ASSERT_TRUE(destination.has(l2)); + ASSERT_EQ(destination.get(l2).parent, l0); + + ASSERT_FALSE(destination.valid(e3)); + ASSERT_TRUE(loader.has(e3)); + + auto l3 = loader.map(e3); + + ASSERT_TRUE(destination.valid(l3)); + ASSERT_TRUE(destination.has(l3)); + ASSERT_EQ(destination.get(l3).duration, 1000); + ASSERT_EQ(destination.get(l3).elapsed, 0); + ASSERT_TRUE(destination.has(l3)); + ASSERT_EQ(destination.get(l3).parent, l2); +}