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