diff --git a/doc/Tutorial.md b/doc/Tutorial.md index 52184e35a..ac32c4f79 100644 --- a/doc/Tutorial.md +++ b/doc/Tutorial.md @@ -551,9 +551,10 @@ struct Position { Serializer should support fundamental and container types by default but it depends on the serializer implementation. -| Serializer | Fundamental (integers, floats, doubles...) | strings | containers | -| ------------------------------------------ | ------------------------------------------ | ------- | ---------- | -| @ref ecstasy::serialization::RawSerializer | Ok | OK | Ok | +| Serializer | Fundamental (integers, floats, doubles...) | strings | containers | +| ------------------------------------------- | ------------------------------------------ | ------- | ---------- | +| @ref ecstasy::serialization::RawSerializer | Ok | OK | Ok | +| @ref ecstasy::serialization::JsonSerializer | Ok | OK | Ok | **1. Saving** diff --git a/src/ecstasy/serialization/EntityComponentSerializer.hpp b/src/ecstasy/serialization/EntityComponentSerializer.hpp index e92a1e524..60a0c9978 100644 --- a/src/ecstasy/serialization/EntityComponentSerializer.hpp +++ b/src/ecstasy/serialization/EntityComponentSerializer.hpp @@ -43,6 +43,16 @@ namespace ecstasy::serialization { } + /// + /// @brief Construct a new Component Rtti with a custom name. + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-04) + /// + EntityComponentSerializer(std::string_view name) : IEntityComponentSerializer(), _name(name) + { + } + /// /// @brief Destroy the Component Rtti /// @@ -72,11 +82,29 @@ namespace ecstasy::serialization .template update(dynamic_cast(storage).at(entity.getIndex())); } - /// @copydoc IEntityComponentSerializer::getStorageTypeIndex - std::type_index getStorageTypeIndex() const override final + /// @copydoc IEntityComponentSerializer::getStorageTypeInfo + const std::type_info &getStorageTypeInfo() const override final { - return std::type_index(typeid(StorageType)); + return typeid(StorageType); } + + /// @copydoc IEntityComponentSerializer::getComponentTypeInfo + const std::type_info &getComponentTypeInfo() const override final + { + return typeid(Component); + } + + /// @copydoc IEntityComponentSerializer::getTypeName + std::string_view getTypeName() const override final + { + if (_name.empty()) + return typeid(Component).name(); + return _name; + } + + private: + /// Name of the component type. + std::string_view _name; }; } // namespace ecstasy::serialization diff --git a/src/ecstasy/serialization/IEntityComponentSerializer.hpp b/src/ecstasy/serialization/IEntityComponentSerializer.hpp index a2da9a147..8b0827d29 100644 --- a/src/ecstasy/serialization/IEntityComponentSerializer.hpp +++ b/src/ecstasy/serialization/IEntityComponentSerializer.hpp @@ -74,14 +74,36 @@ namespace ecstasy virtual ISerializer &load(ISerializer &serializer, IStorage &storage, RegistryEntity &entity) const = 0; /// - /// @brief Get the Storage Type Index of the component. + /// @brief Get the type info of the component storage. /// - /// @return std::type_index Type index of the storage. + /// @return const std::type_info& Type info of the component storage. /// /// @author Andréas Leroux (andreas.leroux@epitech.eu) /// @since 1.0.0 (2024-10-04) /// - virtual std::type_index getStorageTypeIndex() const = 0; + virtual const std::type_info &getStorageTypeInfo() const = 0; + + /// + /// @brief Get the type info of the component. + /// + /// @return const std::type_info& Type info of the component. + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-11) + /// + virtual const std::type_info &getComponentTypeInfo() const = 0; + + //// + /// @brief Get the Component Type Name + /// + /// @note Return the explicit name of the component if any and fallback on the type name. + /// + /// @return std::string_view Name of the component type. + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-11) + /// + virtual std::string_view getTypeName() const = 0; }; } // namespace serialization diff --git a/src/ecstasy/serialization/JsonSerializer.hpp b/src/ecstasy/serialization/JsonSerializer.hpp index 457a01392..1abbb2c00 100644 --- a/src/ecstasy/serialization/JsonSerializer.hpp +++ b/src/ecstasy/serialization/JsonSerializer.hpp @@ -206,7 +206,16 @@ namespace ecstasy::serialization array.PushBack(elem, _document.GetAllocator()); addValue(std::move(array.Move())); } else if constexpr (std::is_same_v) { - save(object.hash_code()); + if (getWriteCursor().IsObject()) { + // Use name if the type is registered + auto componentSerializer = tryGetEntityComponentSerializer(object.hash_code()); + + if (componentSerializer) + save(componentSerializer->get().getTypeName()); + else + save(std::to_string(object.hash_code())); + } else + save(object.hash_code()); } else if constexpr (std::is_fundamental_v) { addValue(rapidjson::Value(object)); } else { @@ -364,12 +373,6 @@ namespace ecstasy::serialization } } - /// @copydoc loadComponentHash - std::size_t loadComponentHash() override final - { - return 0; // loadRaw(); - } - /// /// @brief Open a new nested object or array context in the current object (see @ref getWriteCursor). /// The context can be closed with @ref closeNested. @@ -502,7 +505,54 @@ namespace ecstasy::serialization rapidjson::Document _document; std::stack> _stack; std::stack _arrayIterators; + std::stack _objectIterators; std::string _nextKey; + + /// @copydoc loadComponentHash + std::size_t loadComponentHash() override final + { + if (getWriteCursor().IsObject()) { + if (_objectIterators.empty()) + throw std::logic_error( + "No object iterator. This function should be called in an updateEntity context."); + if (_objectIterators.top() == getWriteCursor().MemberEnd()) + return 0; + _nextKey = _objectIterators.top()->name.GetString(); + ++_objectIterators.top(); + return getEntityComponentSerializer(_nextKey).getComponentTypeInfo().hash_code(); + } else + return load(); + } + + /// @copydoc beforeSaveEntity + void beforeSaveEntity(RegistryEntity &entity) override final + { + newNestedObject(); + static_cast(entity); + } + + /// @copydoc afterSaveEntity + void afterSaveEntity(RegistryEntity &entity) override final + { + closeNested(); + static_cast(entity); + } + + /// @copydoc beforeUpdateEntity + void beforeUpdateEntity(RegistryEntity &entity) override final + { + newNestedObject(false); + _objectIterators.push(getWriteCursor().MemberBegin()); + static_cast(entity); + } + + /// @copydoc afterUpdateEntity + void afterUpdateEntity(RegistryEntity &entity) override final + { + _objectIterators.pop(); + closeNested(); + static_cast(entity); + } }; } // namespace ecstasy::serialization diff --git a/src/ecstasy/serialization/RawSerializer.hpp b/src/ecstasy/serialization/RawSerializer.hpp index 8c5192896..05fc3ad1d 100644 --- a/src/ecstasy/serialization/RawSerializer.hpp +++ b/src/ecstasy/serialization/RawSerializer.hpp @@ -308,12 +308,6 @@ namespace ecstasy::serialization return *result; } - /// @copydoc loadComponentHash - std::size_t loadComponentHash() override final - { - return loadRaw(); - } - /// /// @brief Get the string stream of the serializer. /// @@ -329,6 +323,20 @@ namespace ecstasy::serialization private: std::stringstream _stream; + + /// @copydoc afterSaveEntity + void afterSaveEntity(RegistryEntity &entity) override final + { + // Notify the end of the entity + save(0); + static_cast(entity); + } + + /// @copydoc loadComponentHash + std::size_t loadComponentHash() override final + { + return loadRaw(); + } }; } // namespace ecstasy::serialization diff --git a/src/ecstasy/serialization/Serializer.hpp b/src/ecstasy/serialization/Serializer.hpp index 8be7efd87..5a8b24eab 100644 --- a/src/ecstasy/serialization/Serializer.hpp +++ b/src/ecstasy/serialization/Serializer.hpp @@ -41,7 +41,7 @@ /// #define REGISTER_SERIALIZABLE(COMPONENT, SERIALIZER) \ static bool __CONCATENATE(registered_, __CONCATENATE(COMPONENT, _##SERIALIZER)) = \ - SERIALIZER::registerComponent(); + SERIALIZER::registerComponent(#COMPONENT); /// /// @brief Register a component to multiple serializers. @@ -222,6 +222,7 @@ namespace ecstasy::serialization /// S &saveEntity(RegistryEntity &entity) { + beforeSaveEntity(entity); auto storages = entity.getRegistry().getEntityStorages(entity); for (IStorage &storage : storages) { @@ -231,6 +232,7 @@ namespace ecstasy::serialization this->getEntityComponentSerializer(hash).save(*this, storage, entity); } } + afterSaveEntity(entity); return inner(); } @@ -325,18 +327,24 @@ namespace ecstasy::serialization /// S &updateEntity(RegistryEntity &entity) { + beforeUpdateEntity(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."); + while (component_hash != 0) { + if (this->hasEntityComponentSerializer(component_hash)) { + IEntityComponentSerializer &component = this->getEntityComponentSerializer(component_hash); + IStorage &storage = + entity.getRegistry().getStorages().get(std::type_index(component.getStorageTypeInfo())); + + 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."); + } + component_hash = loadComponentHash(); } + afterUpdateEntity(entity); return inner(); } @@ -376,16 +384,6 @@ 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. /// @@ -393,20 +391,37 @@ namespace ecstasy::serialization /// /// @tparam C Component type to register. /// + /// @param[in] name Name of the component type. + /// /// @author Andréas Leroux (andreas.leroux@epitech.eu) /// @since 1.0.0 (2024-10-04) /// template - static bool registerComponent() + static bool registerComponent(std::string_view name) { size_t hash = typeid(C).hash_code(); if (getRegisteredComponents().contains(hash)) return false; - getRegisteredComponents()[hash] = std::make_unique>(); + getRegisteredComponents()[hash] = std::make_unique>(name); return true; } + /// + /// @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); + } + /// /// @brief Get the Entity Component Serializer for a component type. /// @@ -425,18 +440,45 @@ namespace ecstasy::serialization } /// - /// @brief Check if a component type is registered to this serializer. + /// @brief Try to get the Entity Component Serializer for a component type. /// /// @param[in] hash Hash of the component type. /// - /// @return bool True if the component type is registered, false otherwise. + /// @return std::optional> Optional reference to the entity + /// component serializer. /// /// @author Andréas Leroux (andreas.leroux@epitech.eu) - /// @since 1.0.0 (2024-10-04) + /// @since 1.0.0 (2024-10-13) /// - static bool hasEntityComponentSerializer(std::size_t hash) + static std::optional> tryGetEntityComponentSerializer( + std::size_t hash) { - return getRegisteredComponents().contains(hash); + auto it = getRegisteredComponents().find(hash); + + if (it != getRegisteredComponents().end()) + return std::ref(*it->second); + return std::nullopt; + } + + /// + /// @brief Get the Entity Component Serializer from a component type name. + /// + /// @param[in] name Name 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-11) + /// + static IEntityComponentSerializer &getEntityComponentSerializer(std::string_view name) + { + for (auto &[hash, serializer] : getRegisteredComponents()) { + if (serializer->getTypeName() == name) + return *serializer; + } + throw std::out_of_range("Component with name " + std::string(name) + " not registered."); } protected: @@ -454,6 +496,70 @@ namespace ecstasy::serialization static std::unordered_map> registeredComponents; return registeredComponents; } + + /// + /// @brief Load the hash of the next component type from the stream. + /// + /// @note Return 0 if there is no more components to load. + /// + /// @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 Optional method triggered at the start of @ref saveEntity. + /// + /// @param[in] entity Entity being saved. + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-11) + /// + virtual void beforeSaveEntity(RegistryEntity &entity) + { + static_cast(entity); + } + + /// + /// @brief Optional method triggered at the end of @ref saveEntity. + /// + /// @param[in] entity Entity being saved. + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-11) + /// + virtual void afterSaveEntity(RegistryEntity &entity) + { + static_cast(entity); + } + + /// + /// @brief Optional method triggered at the start of @ref updateEntity. + /// + /// @param[in] entity Entity being updated. + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-11) + /// + virtual void beforeUpdateEntity(RegistryEntity &entity) + { + static_cast(entity); + } + + /// + /// @brief Optional method triggered at the end of @ref updateEntity. + /// + /// @param[in] entity Entity being updated. + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-11) + /// + virtual void afterUpdateEntity(RegistryEntity &entity) + { + static_cast(entity); + } }; } // namespace ecstasy::serialization diff --git a/tests/serialization/tests_Serializer.cpp b/tests/serialization/tests_Serializer.cpp index 0c000db31..64fa35699 100644 --- a/tests/serialization/tests_Serializer.cpp +++ b/tests/serialization/tests_Serializer.cpp @@ -46,7 +46,7 @@ struct Position { return *this; } }; -REGISTER_SERIALIZABLES(Position, RawSerializer) +REGISTER_SERIALIZABLES(Position, RawSerializer, JsonSerializer) struct NPC { Position pos; @@ -85,6 +85,7 @@ struct NPC { return *this; } }; +REGISTER_SERIALIZABLES(NPC, JsonSerializer) TEST(RawSerializer, fundamental_types) { @@ -438,4 +439,30 @@ TEST(JsonSerializer, all) GTEST_ASSERT_EQ(npcLoaded.name, "Steve"); GTEST_ASSERT_EQ(npcLoaded.pos.x, 42.f); GTEST_ASSERT_EQ(npcLoaded.pos.y, 0.f); + + jsonSerializer.clear(); + // Test with entire entities + ecstasy::Registry registry; + ecstasy::RegistryEntity entity( + registry.entityBuilder().with(1.0f, -8456.0f).with(Position(42.f, 0.f), "Steve").build(), + registry); + + // Save both components as they are registered (Position and NPC) + jsonSerializer.saveEntity(entity); + json = jsonSerializer.exportBytes(); +#ifndef _WIN32 + GTEST_ASSERT_EQ( + json, "[{\"NPC\":{\"pos\":{\"x\":42.0,\"y\":0.0},\"name\":\"Steve\"},\"Position\":{\"x\":1.0,\"y\":-8456.0}}]"); +#else + GTEST_ASSERT_EQ( + json, "[{\"Position\":{\"x\":1.0,\"y\":-8456.0},\"NPC\":{\"pos\":{\"x\":42.0,\"y\":0.0},\"name\":\"Steve\"}}]"); +#endif + + jsonSerializer.resetCursor(); + ecstasy::RegistryEntity e2 = jsonSerializer.loadEntity(registry); + GTEST_ASSERT_EQ(e2.get().name, "Steve"); + GTEST_ASSERT_EQ(e2.get().pos.x, 42.f); + GTEST_ASSERT_EQ(e2.get().pos.y, 0.f); + GTEST_ASSERT_EQ(e2.get().x, 1.0f); + GTEST_ASSERT_EQ(e2.get().y, -8456.0f); }