From 90fe105142931702d188f4fbc86b7dfbb9e07e0f Mon Sep 17 00:00:00 2001 From: Andreas Leroux Date: Fri, 4 Oct 2024 17:21:49 +0200 Subject: [PATCH] Introduce new entity serialization methods Linked: #150 --- doc/Tutorial.md | 14 +- src/ecstasy/registry/Registry.hpp | 17 +- src/ecstasy/serialization/CMakeLists.txt | 3 +- .../EntityComponentSerializer.hpp | 84 ++++++++ .../IEntityComponentSerializer.hpp | 90 +++++++++ src/ecstasy/serialization/RawSerializer.hpp | 6 + src/ecstasy/serialization/Serializer.hpp | 183 ++++++++++++++++++ tests/serialization/tests_Serializer.cpp | 41 ++++ 8 files changed, 432 insertions(+), 6 deletions(-) create mode 100644 src/ecstasy/serialization/EntityComponentSerializer.hpp create mode 100644 src/ecstasy/serialization/IEntityComponentSerializer.hpp diff --git a/doc/Tutorial.md b/doc/Tutorial.md index 5229825f0..52184e35a 100644 --- a/doc/Tutorial.md +++ b/doc/Tutorial.md @@ -654,14 +654,22 @@ Position &operator<<(RawSerializer &serializer) } ``` -#### Working with Entities (WIP) +If you need to (de)serialize the type from the save/update/loadEntity methods, you first need to register the type as serializable by the expected Serializer using the variadic macro @ref REGISTER_SERIALIZABLES + +For example, if you want you type Position to be serializable by the RawSerializer and the (maybe to come) JsonSerializer: + +```cpp +REGISTER_SERIALIZABLES(Position, RawSerializer, JsonSerializer) +``` + +#### Working with Entities Since you can serialize any type, you can serialize entity components manually using the functions above. -If you define **ECSTASY_ENABLE_ENTITY_SERIALIZERS**, you can serialize an entire entity. +You can save entity components explicitly using the templated saveEntity method, or every registered components with the classic saveEntity method. @warning -This is still work in progress and will certainly be refactored because the underlying code is shit. +To use the non templated `saveEntity` method, you need to register them using the @ref REGISTER_SERIALIZABLES macro (see below). ```cpp RawSerializer serializer(); diff --git a/src/ecstasy/registry/Registry.hpp b/src/ecstasy/registry/Registry.hpp index d9999c684..03844e97b 100644 --- a/src/ecstasy/registry/Registry.hpp +++ b/src/ecstasy/registry/Registry.hpp @@ -1037,7 +1037,7 @@ namespace ecstasy template S> S &getSystem() { - return _storages.get(); + return _systems.get(); } /// @@ -1179,7 +1179,7 @@ namespace ecstasy void runSystems(size_t group, size_t mask); /// - /// @brief Get a reference to the storages instances. + /// @brief Get a const reference to the storages instances. /// /// @return constexpr const Instances& Const reference to the storages instance. /// @@ -1191,6 +1191,19 @@ namespace ecstasy return _storages; } + /// + /// @brief Get a reference to the storages instances. + /// + /// @return constexpr Instances& Reference to the storages instance. + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-04) + /// + constexpr Instances &getStorages() + { + return _storages; + } + private: Instances _resources; Instances _storages; diff --git a/src/ecstasy/serialization/CMakeLists.txt b/src/ecstasy/serialization/CMakeLists.txt index 812c0156c..764ecdc97 100644 --- a/src/ecstasy/serialization/CMakeLists.txt +++ b/src/ecstasy/serialization/CMakeLists.txt @@ -6,7 +6,8 @@ add_subdirectory(traits) set(SRC ${SRC} - ${INCROOT}/ComponentSerializer.hpp + ${INCROOT}/EntityComponentSerializer.hpp + ${INCROOT}/IEntityComponentSerializer.hpp ${INCROOT}/include.hpp ${INCROOT}/ISerializer.hpp ${INCROOT}/RawSerializer.hpp diff --git a/src/ecstasy/serialization/EntityComponentSerializer.hpp b/src/ecstasy/serialization/EntityComponentSerializer.hpp new file mode 100644 index 000000000..e92a1e524 --- /dev/null +++ b/src/ecstasy/serialization/EntityComponentSerializer.hpp @@ -0,0 +1,84 @@ +/// +/// @file EntityComponentSerializer.hpp +/// @author Andréas Leroux (andreas.leroux@epitech.eu) +/// @brief +/// @version 1.0.0 +/// @date 2024-10-03 +/// +/// @copyright Copyright (c) ECSTASY 2024 +/// +/// + +#ifndef ECSTASY_COMPONENT_COMPONENT_SERIALIZER_HPP_ +#define ECSTASY_COMPONENT_COMPONENT_SERIALIZER_HPP_ + +#include "ecstasy/serialization/IEntityComponentSerializer.hpp" + +namespace ecstasy::serialization +{ + /// + /// @brief Entity component serializer class bound to a specific component and a serializer type. + /// + /// The class instance doesn't contains any data except its vtable. + /// + /// @tparam Component Type of the component to serialize. + /// @tparam Serializer Type of the serializer to use to serialize the component. + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-04) + /// + template Serializer> + class EntityComponentSerializer : public IEntityComponentSerializer { + public: + /// Type of the storage used to store the component. + using StorageType = getStorageType; + + /// + /// @brief Construct a new Component Rtti + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-04) + /// + EntityComponentSerializer() : IEntityComponentSerializer() + { + } + + /// + /// @brief Destroy the Component Rtti + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-04) + /// + ~EntityComponentSerializer() override = default; + + /// @copydoc IEntityComponentSerializer::save + ISerializer &save( + ISerializer &serializer, const IStorage &storage, const RegistryEntity &entity) const override final + { + return dynamic_cast(serializer) + .template saveEntityComponent( + dynamic_cast(storage).at(entity.getIndex())); + } + + /// @copydoc IEntityComponentSerializer::load + ISerializer &load(ISerializer &serializer, IStorage &storage, RegistryEntity &entity) const override final + { + if (!storage.contains(entity.getIndex())) { + dynamic_cast(storage).insert( + entity.getIndex(), dynamic_cast(serializer).template load()); + return serializer; + } else + return dynamic_cast(serializer) + .template update(dynamic_cast(storage).at(entity.getIndex())); + } + + /// @copydoc IEntityComponentSerializer::getStorageTypeIndex + std::type_index getStorageTypeIndex() const override final + { + return std::type_index(typeid(StorageType)); + } + }; + +} // namespace ecstasy::serialization + +#endif /* !ECSTASY_COMPONENT_COMPONENT_SERIALIZER_HPP_ */ diff --git a/src/ecstasy/serialization/IEntityComponentSerializer.hpp b/src/ecstasy/serialization/IEntityComponentSerializer.hpp new file mode 100644 index 000000000..a2da9a147 --- /dev/null +++ b/src/ecstasy/serialization/IEntityComponentSerializer.hpp @@ -0,0 +1,90 @@ +/// +/// @file IEntityComponentSerializer.hpp +/// @author Andréas Leroux (andreas.leroux@epitech.eu) +/// @brief File containing the IEntityComponentSerializer class, used to serialize entity components from an IStorage +/// instance. +/// @version 1.0.0 +/// @date 2024-10-03 +/// +/// @copyright Copyright (c) ECSTASY 2024 +/// +/// + +#ifndef ECSTASY_COMPONENT_ICOMPONESERIALIZERTTI_HPP_ +#define ECSTASY_COMPONENT_ICOMPONESERIALIZERTTI_HPP_ + +#include +#include + +namespace ecstasy +{ + class IStorage; + class RegistryEntity; + + namespace serialization + { + class ISerializer; + + /// + /// @brief Type erased interface for serializing entity components. + /// + /// This interface is used to serialize entity components from IStorage/ISerializer instances when we don't know + /// the type of the component/serializer. + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-04) + /// + class IEntityComponentSerializer { + public: + /// + /// @brief Destroy the IEntityComponentSerializer + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-04) + /// + virtual ~IEntityComponentSerializer() = default; + + /// + /// @brief Save the component to the serializer. + /// + /// @param[in] serializer Reference to the serializer to save the component to. + /// @param[in] storage Const reference to the storage containing the component. + /// @param[in] entity Const reference to the entity associated with the component. + /// + /// @return ISerializer& Reference to the serializer for chaining. + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-04) + /// + virtual ISerializer &save( + ISerializer &serializer, const IStorage &storage, const RegistryEntity &entity) const = 0; + + /// + /// @brief Load an entity component from the serializer. + /// + /// @param[in] serializer Reference to the serializer to load the component from. + /// @param[in] storage Reference to the storage to load the component to. + /// @param[in] entity Reference to the entity to load/update the component to. + /// + /// @return ISerializer& Reference to the serializer for chaining. + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-04) + /// + virtual ISerializer &load(ISerializer &serializer, IStorage &storage, RegistryEntity &entity) const = 0; + + /// + /// @brief Get the Storage Type Index of the component. + /// + /// @return std::type_index Type index of the storage. + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-04) + /// + virtual std::type_index getStorageTypeIndex() const = 0; + }; + + } // namespace serialization +} // namespace ecstasy + +#endif /* !ECSTASY_COMPONENT_ICOMPONENT_SERIALIZER_HPP_ */ diff --git a/src/ecstasy/serialization/RawSerializer.hpp b/src/ecstasy/serialization/RawSerializer.hpp index 218071cfd..1a09a397e 100644 --- a/src/ecstasy/serialization/RawSerializer.hpp +++ b/src/ecstasy/serialization/RawSerializer.hpp @@ -307,6 +307,12 @@ namespace ecstasy::serialization return *result; } + /// @copydoc loadComponentHash + std::size_t loadComponentHash() override final + { + return loadRaw(); + } + /// /// @brief Get the string stream of the serializer. /// diff --git a/src/ecstasy/serialization/Serializer.hpp b/src/ecstasy/serialization/Serializer.hpp index 15a625d5b..46c05012a 100644 --- a/src/ecstasy/serialization/Serializer.hpp +++ b/src/ecstasy/serialization/Serializer.hpp @@ -14,15 +14,43 @@ #include #include +#include #include "ecstasy/resources/entity/RegistryEntity.hpp" +#include "ecstasy/serialization/EntityComponentSerializer.hpp" #include "ecstasy/serialization/ISerializer.hpp" +#include "util/serialization/foreach.hpp" #include "ecstasy/serialization/concepts/has_extraction_operator.hpp" #include "ecstasy/serialization/concepts/has_insertion_operator.hpp" #include "ecstasy/serialization/traits/can_load_type.hpp" #include "ecstasy/serialization/traits/can_save_type.hpp" #include "ecstasy/serialization/traits/can_update_type.hpp" +#define __CONCATENATE_DETAIL(x, y) x##y +#define __CONCATENATE(x, y) __CONCATENATE_DETAIL(x, y) +#define _REGISTER_SERIALIZABLES_AGAIN() _REGISTER_SERIALIZABLES_HELPER +#define _REGISTER_SERIALIZABLES_HELPER(COMPONENT, a1, ...) \ + REGISTER_SERIALIZABLE(COMPONENT, a1) \ + __VA_OPT__(_REGISTER_SERIALIZABLES_AGAIN PARENS(COMPONENT, __VA_ARGS__)) + +/// +/// @brief Register a component to a serializer. +/// +/// @author Andréas Leroux (andreas.leroux@epitech.eu) +/// @since 1.0.0 (2024-10-04) +/// +#define REGISTER_SERIALIZABLE(COMPONENT, SERIALIZER) \ + static bool __CONCATENATE(registered_, __CONCATENATE(COMPONENT, _##SERIALIZER)) = \ + SERIALIZER::registerComponent(); + +/// +/// @brief Register a component to multiple serializers. +/// +/// @author Andréas Leroux (andreas.leroux@epitech.eu) +/// @since 1.0.0 (2024-10-04) +/// +#define REGISTER_SERIALIZABLES(COMPONENT, a1, ...) EXPAND(_REGISTER_SERIALIZABLES_HELPER(COMPONENT, a1, __VA_ARGS__)) + namespace ecstasy::serialization { @@ -180,6 +208,32 @@ namespace ecstasy::serialization return s; } + /// + /// @brief Save all registered components of an entity to the serializer. + /// + /// @note See @ref registerComponent for registering components. + /// + /// @param[in] entity Entity to save. + /// + /// @return S& Reference to @b this for chain calls. + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-04) + /// + S &saveEntity(RegistryEntity &entity) + { + auto storages = entity.getRegistry().getEntityStorages(entity); + + for (IStorage &storage : storages) { + std::size_t hash = storage.getComponentTypeInfos().hash_code(); + + if (this->hasEntityComponentSerializer(hash)) { + this->getEntityComponentSerializer(hash).save(*this, storage, entity); + } + } + return inner(); + } + /// /// @brief Load an object from the serializer. /// @@ -208,6 +262,24 @@ namespace ecstasy::serialization } } + /// + /// @brief Load an entity component from the serializer. + /// + /// @param[in] registry Registry to load the component to. + /// + /// @return RegistryEntity Loaded entity. + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-04) + /// + RegistryEntity loadEntity(Registry ®istry) + { + RegistryEntity entity(registry.entityBuilder().build(), registry); + + updateEntity(entity); + return entity; + } + /// /// @brief Update an existing object from the serializer. /// @@ -237,6 +309,37 @@ namespace ecstasy::serialization return inner(); } + /// + /// @brief Update all registered components of an entity from the serializer. + /// + /// @note Missing components are loaded, existing components are updated. + /// + /// @param[in] entity Entity to update. + /// + /// @return S& Reference to @b this for chain calls. + /// + /// @throws std::out_of_range If an entity component is not registered in the serializer. + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-04) + /// + S &updateEntity(RegistryEntity &entity) + { + std::size_t component_hash = loadComponentHash(); + + if (this->hasEntityComponentSerializer(component_hash)) { + IEntityComponentSerializer &component = this->getEntityComponentSerializer(component_hash); + IStorage &storage = entity.getRegistry().getStorages().get(component.getStorageTypeIndex()); + + component.load(*this, storage, entity); + } else { + throw std::out_of_range("Component with hash " + std::to_string(component_hash) + + " not registered. Use " + "registerComponent to register components."); + } + return inner(); + } + /// /// @brief Operator overload to simplify the save method. /// @@ -272,7 +375,87 @@ namespace ecstasy::serialization { return inner().update(object); } + + /// + /// @brief Load the hash of the component type from the stream. + /// + /// @return std::size_t Hash of the component type. + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-04) + /// + virtual std::size_t loadComponentHash() = 0; + + /// + /// @brief Register a component to this serializer type. + /// + /// This is required for calls to saveEntity calls without explicit component types. + /// + /// @tparam C Component type to register. + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-04) + /// + template + static bool registerComponent() + { + size_t hash = typeid(C).hash_code(); + + if (getRegisteredComponents().contains(hash)) + return false; + getRegisteredComponents()[hash] = std::make_unique>(); + return true; + } + + /// + /// @brief Get the Entity Component Serializer for a component type. + /// + /// @param[in] hash Hash of the component type. + /// + /// @return IEntityComponentSerializer& Reference to the entity component serializer. + /// + /// @throw std::out_of_range If the component type is not registered. + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-04) + /// + static IEntityComponentSerializer &getEntityComponentSerializer(std::size_t hash) + { + return *getRegisteredComponents().at(hash); + } + + /// + /// @brief Check if a component type is registered to this serializer. + /// + /// @param[in] hash Hash of the component type. + /// + /// @return bool True if the component type is registered, false otherwise. + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-04) + /// + static bool hasEntityComponentSerializer(std::size_t hash) + { + return getRegisteredComponents().contains(hash); + } + + protected: + /// + /// @brief Get a reference to the Registered Components map. + /// + /// @return std::unordered_map>& Reference to the + /// registered components map. + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-10) + /// + static std::unordered_map> &getRegisteredComponents() + { + static std::unordered_map> registeredComponents; + return registeredComponents; + } }; + } // namespace ecstasy::serialization #endif /* !ECSTASY_SERIALIZATION_SERIALIZER_HPP_ */ diff --git a/tests/serialization/tests_Serializer.cpp b/tests/serialization/tests_Serializer.cpp index be18614c0..5ade4181a 100644 --- a/tests/serialization/tests_Serializer.cpp +++ b/tests/serialization/tests_Serializer.cpp @@ -31,6 +31,7 @@ struct Position { return *this; } }; +REGISTER_SERIALIZABLES(Position, RawSerializer) struct NPC { Position pos; @@ -272,6 +273,46 @@ TEST(Serializer, compound_struct) GTEST_ASSERT_EQ(npcLoaded.pos.y, -8456.0f); } +TEST(Serializer, ComponentsRTTI) +{ + RawSerializer rawSerializer; + ecstasy::Registry registry; + + // Ensure components are registered (or not) + GTEST_ASSERT_TRUE(RawSerializer::hasEntityComponentSerializer(typeid(Position).hash_code())); + GTEST_ASSERT_FALSE(RawSerializer::hasEntityComponentSerializer(typeid(NPC).hash_code())); + + ecstasy::RegistryEntity entity( + registry.entityBuilder().with(1.0f, -8456.0f).with(Position(42.f, 0.f), "Steve").build(), + registry); + + // Only save the Position component (NPC is not registered) + rawSerializer.saveEntity(entity); + // Check the serialized data + rawSerializer.resetReadCursor(); + std::size_t component_hash = rawSerializer.load(); + GTEST_ASSERT_EQ(component_hash, typeid(Position).hash_code()); + Position posLoaded = Position(rawSerializer); + GTEST_ASSERT_EQ(posLoaded.x, 1.0f); + GTEST_ASSERT_EQ(posLoaded.y, -8456.0f); + + // Update the Position component + entity.get().x = 2.0f; + entity.get().y = -8457.0f; + rawSerializer.resetReadCursor(); + rawSerializer.updateEntity(entity); + + // Check the updated data + GTEST_ASSERT_EQ(entity.get().x, 1.0f); + GTEST_ASSERT_EQ(entity.get().y, -8456.0f); + + // Load the Position component + rawSerializer.resetReadCursor(); + ecstasy::RegistryEntity e2 = rawSerializer.loadEntity(registry); + GTEST_ASSERT_EQ(e2.get().x, 1.0f); + GTEST_ASSERT_EQ(e2.get().y, -8456.0f); +} + TEST(Serializer, entityComponents) { RawSerializer rawSerializer;