save/restore - see #27

This commit is contained in:
Michele Caini
2018-03-28 10:23:47 +02:00
parent 2d7443acaf
commit 2cc1850212
10 changed files with 1748 additions and 6 deletions

View File

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

248
README.md
View File

@@ -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.<br/>
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.<br/>
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<AComponent, AnotherComponent>(output)
.tag<MyTag>(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.<br/>
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.<br/>
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_.<br/>
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.<br/>
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<AComponent, AnotherComponent>(output)
.tag<MyTag>(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.<br/>
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).<br/>
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<entity_type> loader{registry};
InputArchive input;
loader.entities(input)
.destroyed(input)
.component<AComponent, AnotherComponent>(input)
.component<DirtyComponent>(input, &DirtyComponent::parent, &DirtyComponent::child)
.tag<MyTag>(input)
.tag<DirtyTag>(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.<br/>
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<ASimpleComponent, AnotherSimpleComponent, AMoreComplexComponent, TheLastComponent>();
// ...
loader
.component<ASimpleComponent, AnotherSimpleComponent>(input)
.component<AMoreComplexComponent>(input, &AMoreComplexComponent::entity);
.component<TheLastComponent>(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.<br/>
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.<br/>
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?<br/>
@@ -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.<br/>
to give it the highest priority, if you have any other reason to contact me:
do not hesitate. I'm available for hiring.<br/>
Feel free to take a look at my [profile](https://github.com/skypjack) and
contact me by mail.

19
cmake/in/cereal.in Normal file
View File

@@ -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 ""
)

View File

@@ -13,6 +13,7 @@
#include <type_traits>
#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<Entity, Component>{ensure<Component>()};
}
/**
* @brief Returns a temporary object to use to create snapshots.
*
* A snapshot is either a full or a partial dump of a registry.<br/>
* 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<Entity> 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 &registry, 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 &registry, 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.<br/>
* 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<Entity> restore() {
using ensure_fn_type = void(*)(Registry &, entity_type, bool);
ensure_fn_type ensure = [](Registry &registry, entity_type entity, bool destroyed) {
using promotion_type = std::conditional_t<sizeof(size_type) >= 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<std::unique_ptr<SparseSet<Entity>>> handlers;

View File

@@ -0,0 +1,713 @@
#ifndef ENTT_ENTITY_SNAPSHOT_HPP
#define ENTT_ENTITY_SNAPSHOT_HPP
#include <unordered_map>
#include <algorithm>
#include <cstddef>
#include <utility>
#include <cassert>
#include <iterator>
#include <type_traits>
#include "entt_traits.hpp"
namespace entt {
/**
* @brief Forward declaration of the registry class.
*/
template<typename>
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.<br/>
* 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<typename Entity>
class Snapshot final {
/*! @brief A registry is allowed to create snapshots. */
friend class Registry<Entity>;
using follow_fn_type = Entity(*)(const Registry<Entity> &, Entity);
using raw_fn_type = const Entity *(*)(const Registry<Entity> &, typename Registry<Entity>::component_type);
Snapshot(const Registry<Entity> &registry, 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<typename Component, typename Archive>
void get(Archive &archive, const Registry<Entity> &registry) {
const auto component = registry.template component<Component>();
const auto sz = registry.template size<Component>();
const auto *entities = raw(registry, component);
archive(static_cast<Entity>(sz));
for(std::remove_const_t<decltype(sz)> i{}; i < sz; ++i) {
const auto entity = entities[i];
archive(entity);
archive(registry.template get<Component>(entity));
};
}
template<typename Tag, typename Archive>
void get(Archive &archive) {
const bool has = registry.template has<Tag>();
// numerical length is forced for tags to facilitate loading
archive(has ? Entity(1): Entity{});
if(has) {
archive(registry.template attachee<Tag>());
archive(registry.template get<Tag>());
}
}
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<typename Archive>
Snapshot entities(Archive &archive) && {
archive(static_cast<Entity>(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<typename Archive>
Snapshot destroyed(Archive &archive) && {
archive(static_cast<Entity>(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<typename... Component, typename Archive>
Snapshot component(Archive &archive) && {
using accumulator_type = int[];
accumulator_type accumulator = { 0, (get<Component>(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<typename... Tag, typename Archive>
Snapshot tag(Archive &archive) && {
using accumulator_type = int[];
accumulator_type accumulator = { 0, (get<Tag>(archive), 0)... };
(void)accumulator;
return *this;
}
private:
const Registry<Entity> &registry;
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.<br/>
* 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<typename Entity>
class SnapshotLoader final {
/*! @brief A registry is allowed to create snapshot loaders. */
friend class Registry<Entity>;
using ensure_fn_type = void(*)(Registry<Entity> &, Entity, bool);
SnapshotLoader(Registry<Entity> &registry, 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<typename Archive, typename Func>
void each(Archive &archive, Func func) {
Entity length{};
archive(length);
while(length) {
Entity entity{};
archive(entity);
func(entity);
--length;
}
}
template<typename Component, typename Archive>
void assign(Archive &archive) {
each(archive, [&archive, this](auto entity) {
static constexpr auto destroyed = false;
ensure_fn(registry, entity, destroyed);
archive(registry.template assign<Component>(entity));
});
}
template<typename Tag, typename Archive>
void attach(Archive &archive) {
each(archive, [&archive, this](auto entity) {
static constexpr auto destroyed = false;
ensure_fn(registry, entity, destroyed);
archive(registry.template attach<Tag>(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<typename Archive>
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<typename Archive>
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<typename... Component, typename Archive>
SnapshotLoader component(Archive &archive) && {
using accumulator_type = int[];
accumulator_type accumulator = { 0, (assign<Component>(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<typename... Tag, typename Archive>
SnapshotLoader tag(Archive &archive) && {
using accumulator_type = int[];
accumulator_type accumulator = { 0, (attach<Tag>(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.<br/>
* 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<Entity> &registry;
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.<br/>
* Identifiers that entities originally had are not transferred to the target.
* Instead, the loader maps remote identifiers to local ones while restoring a
* snapshot.<br/>
* 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<typename Entity>
class ContinuousLoader final {
using traits_type = entt_traits<Entity>;
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<typename Instance, typename Type>
std::enable_if_t<std::is_same<Type, Entity>::value>
update(Instance &instance, Type Instance::*member) {
instance.*member = map(instance.*member);
}
template<typename Instance, typename Type>
std::enable_if_t<std::is_same<typename std::iterator_traits<typename Type::iterator>::value_type, Entity>::value>
update(Instance &instance, Type Instance::*member) {
for(auto &entity: (instance.*member)) {
entity = map(entity);
}
}
template<typename Archive, typename Func>
void each(Archive &archive, Func func) {
Entity length{};
archive(length);
while(length) {
Entity entity{};
archive(entity);
func(entity);
--length;
}
}
template<typename Component>
void reset() {
for(auto &&ref: remloc) {
const auto local = ref.second.first;
if(registry.valid(local)) {
registry.template reset<Component>(local);
}
}
}
template<typename Component, typename Archive>
void assign(Archive &archive) {
reset<Component>();
each(archive, [&archive, this](auto entity) {
entity = restore(entity);
archive(registry.template accommodate<Component>(entity));
});
}
template<typename Component, typename Archive, typename... Type>
void assign(Archive &archive, Type Component::*... member) {
reset<Component>();
each(archive, [&archive, member..., this](auto entity) {
entity = restore(entity);
auto &component = registry.template accommodate<Component>(entity);
archive(component);
using accumulator_type = int[];
accumulator_type accumulator = { 0, (update(component, member), 0)... };
(void)accumulator;
});
}
template<typename Tag, typename Archive>
void attach(Archive &archive) {
registry.template remove<Tag>();
each(archive, [&archive, this](auto entity) {
entity = restore(entity);
archive(registry.template attach<Tag>(entity));
});
}
template<typename Tag, typename Archive, typename... Type>
void attach(Archive &archive, Type Tag::*... member) {
registry.template remove<Tag>();
each(archive, [&archive, member..., this](auto entity) {
entity = restore(entity);
auto &tag = registry.template attach<Tag>(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<entity_type> &registry) 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<typename Archive>
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<typename Archive>
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<typename... Component, typename Archive>
ContinuousLoader & component(Archive &archive) {
using accumulator_type = int[];
accumulator_type accumulator = { 0, (assign<Component>(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.<br/>
* 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<typename Component, typename Archive, typename... Type>
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<typename... Tag, typename Archive>
ContinuousLoader & tag(Archive &archive) {
using accumulator_type = int[];
accumulator_type accumulator = { 0, (attach<Tag>(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.<br/>
* 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<typename Tag, typename Archive, typename... Type>
ContinuousLoader & tag(Archive &archive, Type Tag::*... member) {
attach<Tag>(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.<br/>
* 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.<br/>
* 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<Entity, std::pair<Entity, bool>> remloc;
Registry<Entity> &registry;
};
}
#endif // ENTT_ENTITY_SNAPSHOT_HPP

View File

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

View File

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

View File

@@ -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
$<TARGET_OBJECTS:odr>
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(
$<TARGET_OBJECTS:odr>
entt/entity/actor.cpp
entt/entity/registry.cpp
entt/entity/snapshot.cpp
entt/entity/sparse_set.cpp
entt/entity/view.cpp
)

View File

@@ -0,0 +1,489 @@
#include <tuple>
#include <queue>
#include <vector>
#include <gtest/gtest.h>
#include <entt/entity/registry.hpp>
template<typename Storage>
struct OutputArchive {
OutputArchive(Storage &storage)
: storage{storage}
{}
template<typename Value>
void operator()(const Value &value) {
std::get<std::queue<Value>>(storage).push(value);
}
private:
Storage &storage;
};
template<typename Storage>
struct InputArchive {
InputArchive(Storage &storage)
: storage{storage}
{}
template<typename Value>
void operator()(Value &value) {
auto &queue = std::get<std::queue<Value>>(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<entt::DefaultRegistry::entity_type> quux;
};
TEST(Snapshot, Dump) {
entt::DefaultRegistry registry;
auto e0 = registry.create();
registry.assign<int>(e0, 42);
registry.assign<char>(e0, 'c');
registry.assign<double>(e0, .1);
auto e1 = registry.create();
auto e2 = registry.create();
registry.assign<int>(e2, 3);
auto e3 = registry.create();
registry.assign<char>(e3, '0');
registry.attach<float>(e3, .3f);
auto e4 = registry.create();
registry.attach<AComponent>(e4);
registry.destroy(e1);
auto v1 = registry.current(e1);
using storage_type = std::tuple<
std::queue<entt::DefaultRegistry::entity_type>,
std::queue<int>,
std::queue<char>,
std::queue<double>,
std::queue<float>,
std::queue<bool>,
std::queue<AComponent>,
std::queue<AnotherComponent>,
std::queue<Foo>
>;
storage_type storage;
OutputArchive<storage_type> output{storage};
InputArchive<storage_type> input{storage};
registry.snapshot()
.entities(output)
.destroyed(output)
.component<int, char, AnotherComponent, double>(output)
.tag<float, bool, AComponent>(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<int, char, AnotherComponent, double>(input)
.tag<float, bool, AComponent>(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<int>(e0), 42);
ASSERT_EQ(registry.get<char>(e0), 'c');
ASSERT_EQ(registry.get<double>(e0), .1);
ASSERT_EQ(registry.current(e1), v1);
ASSERT_EQ(registry.get<int>(e2), 3);
ASSERT_EQ(registry.get<char>(e3), '0');
ASSERT_TRUE(registry.has<float>());
ASSERT_EQ(registry.attachee<float>(), e3);
ASSERT_EQ(registry.get<float>(), .3f);
ASSERT_TRUE(registry.has<AComponent>());
ASSERT_EQ(registry.attachee<AComponent>(), e4);
ASSERT_TRUE(registry.empty<AnotherComponent>());
ASSERT_FALSE(registry.has<long int>());
}
TEST(Snapshot, Partial) {
entt::DefaultRegistry registry;
auto e0 = registry.create();
registry.assign<int>(e0, 42);
registry.assign<char>(e0, 'c');
registry.assign<double>(e0, .1);
auto e1 = registry.create();
auto e2 = registry.create();
registry.assign<int>(e2, 3);
auto e3 = registry.create();
registry.assign<char>(e3, '0');
registry.attach<float>(e3, .3f);
auto e4 = registry.create();
registry.attach<AComponent>(e4);
registry.destroy(e1);
auto v1 = registry.current(e1);
using storage_type = std::tuple<
std::queue<entt::DefaultRegistry::entity_type>,
std::queue<int>,
std::queue<char>,
std::queue<double>,
std::queue<float>,
std::queue<bool>,
std::queue<AComponent>,
std::queue<Foo>
>;
storage_type storage;
OutputArchive<storage_type> output{storage};
InputArchive<storage_type> input{storage};
registry.snapshot()
.entities(output)
.destroyed(output)
.component<char, int>(output)
.tag<bool, float>(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<char, int>(input)
.tag<bool, float>(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<int>(e0), 42);
ASSERT_EQ(registry.get<char>(e0), 'c');
ASSERT_FALSE(registry.has<double>(e0));
ASSERT_EQ(registry.current(e1), v1);
ASSERT_EQ(registry.get<int>(e2), 3);
ASSERT_EQ(registry.get<char>(e3), '0');
ASSERT_TRUE(registry.orphan(e4));
ASSERT_TRUE(registry.has<float>());
ASSERT_EQ(registry.attachee<float>(), e3);
ASSERT_EQ(registry.get<float>(), .3f);
ASSERT_FALSE(registry.has<long int>());
registry.snapshot()
.tag<float>(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<float>(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<entity_type> loader{dst};
std::vector<entity_type> entities;
entity_type entity;
using storage_type = std::tuple<
std::queue<entity_type>,
std::queue<AComponent>,
std::queue<AnotherComponent>,
std::queue<Foo>,
std::queue<double>
>;
storage_type storage;
OutputArchive<storage_type> output{storage};
InputArchive<storage_type> 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<AComponent>(entity);
src.assign<AnotherComponent>(entity, i, i);
if(i % 2) {
src.assign<Foo>(entity, entity);
} else if(i == 2) {
src.attach<double>(entity, .3);
}
}
src.view<Foo>().each([&entities](auto, auto &foo) {
foo.quux.insert(foo.quux.begin(), entities.begin(), entities.end());
});
entity = dst.create();
dst.assign<AComponent>(entity);
dst.assign<AnotherComponent>(entity, -1, -1);
src.snapshot()
.entities(output)
.destroyed(output)
.component<AComponent, AnotherComponent, Foo>(output)
.tag<double>(output);
loader.entities(input)
.destroyed(input)
.component<AComponent, AnotherComponent>(input)
.component<Foo>(input, &Foo::bar, &Foo::quux)
.tag<double>(input)
.orphans();
decltype(dst.size()) aComponentCnt{};
decltype(dst.size()) anotherComponentCnt{};
decltype(dst.size()) fooCnt{};
dst.each([&dst, &aComponentCnt](auto entity) {
ASSERT_TRUE(dst.has<AComponent>(entity));
++aComponentCnt;
});
dst.view<AnotherComponent>().each([&anotherComponentCnt](auto, const auto &component) {
ASSERT_EQ(component.value, component.key < 0 ? -1 : component.key);
++anotherComponentCnt;
});
dst.view<Foo>().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<double>());
ASSERT_EQ(dst.get<double>(), .3);
src.view<AnotherComponent>().each([](auto, auto &component) {
component.value = 2 * component.key;
});
auto size = dst.size();
src.snapshot()
.entities(output)
.destroyed(output)
.component<AComponent, AnotherComponent, Foo>(output)
.tag<double>(output);
loader.entities(input)
.destroyed(input)
.component<AComponent, AnotherComponent>(input)
.component<Foo>(input, &Foo::bar, &Foo::quux)
.tag<double>(input)
.orphans();
ASSERT_EQ(size, dst.size());
ASSERT_EQ(dst.size<AComponent>(), aComponentCnt);
ASSERT_EQ(dst.size<AnotherComponent>(), anotherComponentCnt);
ASSERT_EQ(dst.size<Foo>(), fooCnt);
ASSERT_TRUE(dst.has<double>());
dst.view<AnotherComponent>().each([](auto, auto &component) {
ASSERT_EQ(component.value, component.key < 0 ? -1 : (2 * component.key));
});
entity = src.create();
src.view<Foo>().each([entity](auto, auto &component) {
component.bar = entity;
});
src.snapshot()
.entities(output)
.destroyed(output)
.component<AComponent, AnotherComponent, Foo>(output)
.tag<double>(output);
loader.entities(input)
.destroyed(input)
.component<AComponent, AnotherComponent>(input)
.component<Foo>(input, &Foo::bar, &Foo::quux)
.tag<double>(input)
.orphans();
dst.view<Foo>().each([&loader, entity](auto, auto &component) {
ASSERT_EQ(component.bar, loader.map(entity));
});
entities.clear();
for(auto entity: src.view<AComponent>()) {
entities.push_back(entity);
}
src.destroy(entity);
loader.shrink();
src.snapshot()
.entities(output)
.destroyed(output)
.component<AComponent, AnotherComponent, Foo>(output)
.tag<double>(output);
loader.entities(input)
.destroyed(input)
.component<AComponent, AnotherComponent>(input)
.component<Foo>(input, &Foo::bar, &Foo::quux)
.tag<double>(input)
.orphans()
.shrink();
dst.view<Foo>().each([&dst, &loader, entity](auto, auto &component) {
ASSERT_FALSE(dst.valid(component.bar));
});
ASSERT_FALSE(loader.has(entity));
entity = src.create();
src.view<Foo>().each([entity](auto, auto &component) {
component.bar = entity;
});
dst.reset<AComponent>();
aComponentCnt = src.size<AComponent>();
src.snapshot()
.entities(output)
.destroyed(output)
.component<AComponent, AnotherComponent, Foo>(output)
.tag<double>(output);
loader.entities(input)
.destroyed(input)
.component<AComponent, AnotherComponent>(input)
.component<Foo>(input, &Foo::bar, &Foo::quux)
.tag<double>(input)
.orphans();
ASSERT_EQ(dst.size<AComponent>(), aComponentCnt);
ASSERT_TRUE(dst.has<double>());
src.reset<AComponent>();
src.remove<double>();
aComponentCnt = {};
src.snapshot()
.entities(output)
.destroyed(output)
.component<AComponent, AnotherComponent, Foo>(output)
.tag<double>(output);
loader.entities(input)
.destroyed(input)
.component<AComponent, AnotherComponent>(input)
.component<Foo>(input, &Foo::bar, &Foo::quux)
.tag<double>(input)
.orphans();
ASSERT_EQ(dst.size<AComponent>(), aComponentCnt);
ASSERT_FALSE(dst.has<double>());
}
TEST(Snapshot, ContinuousMoreOnShrink) {
using entity_type = entt::DefaultRegistry::entity_type;
entt::DefaultRegistry src;
entt::DefaultRegistry dst;
entt::ContinuousLoader<entity_type> loader{dst};
using storage_type = std::tuple<
std::queue<entity_type>,
std::queue<AComponent>
>;
storage_type storage;
OutputArchive<storage_type> output{storage};
InputArchive<storage_type> 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));
}

182
test/snapshot/snapshot.cpp Normal file
View File

@@ -0,0 +1,182 @@
#include <gtest/gtest.h>
#include <sstream>
#include <vector>
#include <cereal/archives/json.hpp>
#include <entt/entity/registry.hpp>
struct Position {
float x;
float y;
};
struct Timer {
int duration;
int elapsed{0};
};
struct Relationship {
entt::DefaultRegistry::entity_type parent;
};
template<class Archive>
void serialize(Archive &archive, Position &position) {
archive(position.x, position.y);
}
template<class Archive>
void serialize(Archive &archive, Timer &timer) {
archive(timer.duration);
}
template<class Archive>
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<Position>(e0, 16.f, 16.f);
source.destroy(source.create());
auto e1 = source.create();
source.assign<Position>(e1, .8f, .0f);
source.assign<Relationship>(e1, e0);
auto e2 = source.create();
auto e3 = source.create();
source.assign<Timer>(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<Position, Timer, Relationship>(output);
}
cereal::JSONInputArchive input{storage};
destination.restore().entities(input).destroyed(input)
.component<Position, Timer, Relationship>(input);
ASSERT_TRUE(destination.valid(e0));
ASSERT_TRUE(destination.has<Position>(e0));
ASSERT_EQ(destination.get<Position>(e0).x, 16.f);
ASSERT_EQ(destination.get<Position>(e0).y, 16.f);
ASSERT_TRUE(destination.valid(e1));
ASSERT_TRUE(destination.has<Position>(e1));
ASSERT_EQ(destination.get<Position>(e1).x, .8f);
ASSERT_EQ(destination.get<Position>(e1).y, .0f);
ASSERT_TRUE(destination.has<Relationship>(e1));
ASSERT_EQ(destination.get<Relationship>(e1).parent, e0);
ASSERT_FALSE(destination.valid(e2));
ASSERT_EQ(destination.current(e2), v2);
ASSERT_TRUE(destination.valid(e3));
ASSERT_TRUE(destination.has<Timer>(e3));
ASSERT_EQ(destination.get<Timer>(e3).duration, 1000);
ASSERT_EQ(destination.get<Timer>(e3).elapsed, 0);
}
TEST(Snapshot, Continuous) {
std::stringstream storage;
entt::DefaultRegistry source;
entt::DefaultRegistry destination;
std::vector<entt::DefaultRegistry::entity_type> 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<Position>(e0, 0.f, 0.f);
source.assign<Relationship>(e0, e0);
auto e1 = source.create();
source.assign<Position>(e1, 1.f, 1.f);
source.assign<Relationship>(e1, e0);
auto e2 = source.create();
source.assign<Position>(e2, .2f, .2f);
source.assign<Relationship>(e2, e0);
auto e3 = source.create();
source.assign<Timer>(e3, 1000, 1000);
source.assign<Relationship>(e3, e2);
{
// output finishes flushing its contents when it goes out of scope
cereal::JSONOutputArchive output{storage};
source.snapshot().entities(output).component<Position, Relationship, Timer>(output);
}
cereal::JSONInputArchive input{storage};
entt::ContinuousLoader<entt::DefaultRegistry::entity_type> loader{destination};
loader.entities(input)
.component<Position>(input)
.component<Relationship>(input, &Relationship::parent)
.component<Timer>(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<Position>(l0));
ASSERT_EQ(destination.get<Position>(l0).x, 0.f);
ASSERT_EQ(destination.get<Position>(l0).y, 0.f);
ASSERT_TRUE(destination.has<Relationship>(l0));
ASSERT_EQ(destination.get<Relationship>(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<Position>(l1));
ASSERT_EQ(destination.get<Position>(l1).x, 1.f);
ASSERT_EQ(destination.get<Position>(l1).y, 1.f);
ASSERT_TRUE(destination.has<Relationship>(l1));
ASSERT_EQ(destination.get<Relationship>(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<Position>(l2));
ASSERT_EQ(destination.get<Position>(l2).x, .2f);
ASSERT_EQ(destination.get<Position>(l2).y, .2f);
ASSERT_TRUE(destination.has<Relationship>(l2));
ASSERT_EQ(destination.get<Relationship>(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<Timer>(l3));
ASSERT_EQ(destination.get<Timer>(l3).duration, 1000);
ASSERT_EQ(destination.get<Timer>(l3).elapsed, 0);
ASSERT_TRUE(destination.has<Relationship>(l3));
ASSERT_EQ(destination.get<Relationship>(l3).parent, l2);
}