From caebff4dca4c239c49f9bf99281e47be3728ba55 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Sun, 27 Sep 2026 00:38:57 -0500 Subject: [PATCH] fix: give the destroyable and buff components their real component types eReplicaComponentType had the destroyable component's registry type (7) named BUFF, the real buff component (98) as BUFF_REAL, and a made-up DESTROYABLE = 1000 that DestroyableComponent was stored under. Now DESTROYABLE = 7 and BUFF = 98, as in ComponentsRegistry and the client. Undone with it: - Destructible stats came from whichever of the "buff" (7), quick build and collectible registry ids was set, so the few objects without a type 7 entry read a DestructibleComponent row with an unrelated id (the NJ dragon relics 16482-16485 via their collectible id, 125 quick build LOTs when placed with is_smashable). The type 7 entry is used now; objects without one keep the defaults (is_smashable objects: 1 health, smashable, factions -1 and 6; collectibles: an empty destroyable). The client does the same (LWODestroyableComponent::AllocateComponents / DoObjectComponentLoad). - DestroyableComponent::Reinitialize, an unused copy of that pick order. - WriteComponents' destroyableSerialized flags: the components are written from a list in the client's order, and where the destroyable goes (its own place after the buff, right before a quick build that has no registry entry for it, or after the render component) is one function. - The dashboard's registry 7 -> DESTROYABLE mapping; the destroyable type also has a name there now (1000 was outside magic_enum's range). Component types are not stored or sent as enum numbers anywhere besides the CDClient's own values, which now match. migrations/cdserver/4 is unrelated (it restores LOT 12916's registry rows that migration 0 overwrote) and stays. Verified: dGameTests ReplicaComponentOrderTests serialize players, enemies, smashables, quick builds and collectibles with and without a registry entry, NPCs, pets, vehicles, models and an entity with every listed component with the new code and a frozen copy of the old WriteComponents, and expect the same bits for construction and serialization (a deliberately wrong destroyable place fails them). Full ctest passes. Co-Authored-By: Claude Opus 5.5 --- dCommon/dEnums/eReplicaComponentType.h | 5 +- dDashboardServer/routes/CDClientSchema.h | 2 +- dDashboardServer/routes/WorldScene.h | 7 +- dGame/Entity.cpp | 276 ++------- dGame/dComponents/DestroyableComponent.cpp | 42 -- dGame/dComponents/DestroyableComponent.h | 6 - tests/dGameTests/CMakeLists.txt | 2 +- .../dComponentsTests/CMakeLists.txt | 1 + .../ReplicaComponentOrderTests.cpp | 561 ++++++++++++++++++ tests/dWebTests/CDClientSchemaTests.cpp | 2 +- 10 files changed, 622 insertions(+), 282 deletions(-) create mode 100644 tests/dGameTests/dComponentsTests/ReplicaComponentOrderTests.cpp diff --git a/dCommon/dEnums/eReplicaComponentType.h b/dCommon/dEnums/eReplicaComponentType.h index 2b991dfbf..4da466800 100644 --- a/dCommon/dEnums/eReplicaComponentType.h +++ b/dCommon/dEnums/eReplicaComponentType.h @@ -11,7 +11,7 @@ enum class eReplicaComponentType : uint32_t { CHARACTER, SCRIPT, BOUNCER, - BUFF, // buff is really 98, this is DESTROYABLE + DESTROYABLE, GHOST, SKILL, SPAWNER, @@ -102,7 +102,7 @@ enum class eReplicaComponentType : uint32_t { USER_CONTROL, IGNORE_LIST, MULTI_ZONE_ENTRANCE, - BUFF_REAL, // the real buff component, should just be name BUFF + BUFF, INTERACTION_MANAGER, DONATION_VENDOR, COMBAT_MEDIATOR, @@ -121,7 +121,6 @@ enum class eReplicaComponentType : uint32_t { BUILD_BORDER, UNKNOWN_115, CULLING_PLANE, - DESTROYABLE = 1000 // Actually 7 }; #endif //!__EREPLICACOMPONENTTYPE__H__ diff --git a/dDashboardServer/routes/CDClientSchema.h b/dDashboardServer/routes/CDClientSchema.h index 26091a702..5f46638eb 100644 --- a/dDashboardServer/routes/CDClientSchema.h +++ b/dDashboardServer/routes/CDClientSchema.h @@ -199,7 +199,7 @@ namespace CDClientSchema { inline const Table* ComponentTable(const Schema& schema, eReplicaComponentType type) { using enum eReplicaComponentType; static const std::map DIFFERENT{ - { BUFF, "DestructibleComponent" }, // the enum's BUFF (7) is the destroyable component + { DESTROYABLE, "DestructibleComponent" }, { CONTROLLABLE_PHYSICS, "PhysicsComponent" }, { SIMPLE_PHYSICS, "PhysicsComponent" }, { PHANTOM_PHYSICS, "PhysicsComponent" }, { RIGID_BODY_PHANTOM_PHYSICS, "PhysicsComponent" }, { HAVOK_VEHICLE_PHYSICS, "VehiclePhysics" }, { QUICK_BUILD, "RebuildComponent" }, { MISSION_OFFER, "MissionNPCComponent" }, { ROCKET_LAUNCH, "RocketLaunchpadControlComponent" }, diff --git a/dDashboardServer/routes/WorldScene.h b/dDashboardServer/routes/WorldScene.h index ca44aad15..4cfa4bdc0 100644 --- a/dDashboardServer/routes/WorldScene.h +++ b/dDashboardServer/routes/WorldScene.h @@ -131,16 +131,11 @@ namespace WorldScene { eReplicaComponentType::SCRIPTED_ACTIVITY, eReplicaComponentType::DESTROYABLE }; - // ComponentsRegistry stores the destroyable component as 7 (the enum's BUFF; see eReplicaComponentType) - inline eReplicaComponentType FromRegistry(uint32_t componentType) { - return componentType == 7 ? eReplicaComponentType::DESTROYABLE : static_cast(componentType); - } - // Index into KINDS of an object with these ComponentsRegistry component types, or KINDS.size() for "other" inline size_t Classify(const std::vector& registryComponents) { size_t best = KINDS.size(); for (const auto component : registryComponents) { - const auto type = FromRegistry(component); + const auto type = static_cast(component); for (size_t i = 0; i < best; i++) { if (KINDS[i] == type) { best = i; break; } } diff --git a/dGame/Entity.cpp b/dGame/Entity.cpp index 8707d5285..652e308d0 100644 --- a/dGame/Entity.cpp +++ b/dGame/Entity.cpp @@ -10,6 +10,7 @@ #include "CDDestructibleComponentTable.h" #include "CDClientDatabase.h" #include +#include #include "dServer.h" #include "GameMessages.h" #include "EffectsMessages.h" @@ -383,11 +384,6 @@ void Entity::Initialize() { AddComponent(racingSoundTriggerID); } - const auto buffComponentID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::BUFF); - if (buffComponentID > 0) { - AddComponent(buffComponentID); - } - const auto collectibleComponentID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::COLLECTIBLE); if (collectibleComponentID > 0) { @@ -395,18 +391,18 @@ void Entity::Initialize() { } /** - * Multiple components require the destructible component. + * The destroyable component. The client makes one (with a buff component in front of it) for a DESTROYABLE + * registry entry, whose id is the DestructibleComponent row. Collectibles and smashable (is_smashable) objects + * without that entry get one too, with no row. */ - const auto quickBuildComponentID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::QUICK_BUILD); - - int32_t componentID = -1; - if (collectibleComponentID > 0) componentID = collectibleComponentID; - if (quickBuildComponentID > 0) componentID = quickBuildComponentID; - if (buffComponentID > 0) componentID = buffComponentID; - + const auto destroyableComponentID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::DESTROYABLE); + if (destroyableComponentID > 0) { + AddComponent(destroyableComponentID); + } bool isSmashable = GetVarAs(u"is_smashable") != 0; - if (buffComponentID > 0 || collectibleComponentID > 0 || isSmashable) { + if (destroyableComponentID > 0 || collectibleComponentID > 0 || isSmashable) { + const int32_t componentID = destroyableComponentID > 0 ? destroyableComponentID : -1; DestroyableComponent* comp = AddComponent(componentID); auto* const destCompTable = CDClientManager::GetTable(); std::vector destCompData = destCompTable->Query([componentID](const CDDestructibleComponent& entry) { return (entry.id == componentID); }); @@ -451,7 +447,8 @@ void Entity::Initialize() { comp->SetMaxCoins(currencyValues[0].maxvalue); } } - } else { + } else if (isSmashable) { + // A smashable object without a DestructibleComponent row comp->SetHealth(1); comp->SetArmor(0); @@ -599,6 +596,7 @@ void Entity::Initialize() { AddComponent(combatAiID); } + const auto quickBuildComponentID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::QUICK_BUILD); if (quickBuildComponentID > 0) { auto* const quickBuildComponent = AddComponent(quickBuildComponentID); @@ -1064,222 +1062,56 @@ void Entity::WriteBaseReplicaData(RakNet::BitStream& outBitStream, eReplicaPacke } } -void Entity::WriteComponents(RakNet::BitStream& outBitStream, eReplicaPacketType packetType) const { - +namespace { /** - * This has to be done in a specific order. + * The order the client reads components in (it builds an object's component list in this order in + * ObjectLoader2::DoObjectComponentLoad). CHARACTER is preceded by POSSESSOR, LEVEL_PROGRESSION and + * PLAYER_FORCED_MOVEMENT, which WriteComponents writes with it. */ + constexpr auto SERIALIZATION_ORDER = [] { + using enum eReplicaComponentType; + return std::array{ + POSSESSABLE, MODULE_ASSEMBLY, CONTROLLABLE_PHYSICS, SIMPLE_PHYSICS, RIGID_BODY_PHANTOM_PHYSICS, HAVOK_VEHICLE_PHYSICS, + PHANTOM_PHYSICS, SOUND_TRIGGER, RACING_SOUND_TRIGGER, BUFF, DESTROYABLE, COLLECTIBLE, PET, CHARACTER, ITEM, INVENTORY, + SCRIPT, SKILL, BASE_COMBAT_AI, QUICK_BUILD, MOVING_PLATFORM, SWITCH, VENDOR, DONATION_VENDOR, ACHIEVEMENT_VENDOR, + BOUNCER, SCRIPTED_ACTIVITY, SHOOTING_GALLERY, RACING_CONTROL, LUP_EXHIBIT, MODEL, RENDER, MINI_GAME_CONTROL, + }; + }(); - bool destroyableSerialized = false; - bool bIsInitialUpdate = packetType == eReplicaPacketType::CONSTRUCTION; - - PossessableComponent* possessableComponent; - if (TryGetComponent(eReplicaComponentType::POSSESSABLE, possessableComponent)) { - possessableComponent->Serialize(outBitStream, bIsInitialUpdate); + // The component in SERIALIZATION_ORDER the destroyable is written in front of (DESTROYABLE: at its own place) + eReplicaComponentType DestroyableSerializationSlot(const Entity& entity) { + using enum eReplicaComponentType; + // A DESTROYABLE registry entry puts the destroyable (with the buff in front of it) at its own place. Collectibles + // and quick builds without one get theirs right before themselves, which for a collectible is the same place. + if (entity.HasComponent(BUFF) || entity.HasComponent(COLLECTIBLE)) return DESTROYABLE; + if (entity.HasComponent(QUICK_BUILD)) return QUICK_BUILD; + // Anything else that has one (is_smashable objects, models) writes it after the others + return MINI_GAME_CONTROL; } +} - ModuleAssemblyComponent* moduleAssemblyComponent; - if (TryGetComponent(eReplicaComponentType::MODULE_ASSEMBLY, moduleAssemblyComponent)) { - moduleAssemblyComponent->Serialize(outBitStream, bIsInitialUpdate); - } +void Entity::WriteComponents(RakNet::BitStream& outBitStream, eReplicaPacketType packetType) const { + const bool bIsInitialUpdate = packetType == eReplicaPacketType::CONSTRUCTION; + auto* const destroyableComponent = GetComponent(); + const auto destroyableSlot = DestroyableSerializationSlot(*this); - ControllablePhysicsComponent* controllablePhysicsComponent; - if (TryGetComponent(eReplicaComponentType::CONTROLLABLE_PHYSICS, controllablePhysicsComponent)) { - controllablePhysicsComponent->Serialize(outBitStream, bIsInitialUpdate); - } + using enum eReplicaComponentType; + for (const auto type : SERIALIZATION_ORDER) { + if (type == destroyableSlot && destroyableComponent) destroyableComponent->Serialize(outBitStream, bIsInitialUpdate); + if (type == DESTROYABLE) continue; - SimplePhysicsComponent* simplePhysicsComponent; - if (TryGetComponent(eReplicaComponentType::SIMPLE_PHYSICS, simplePhysicsComponent)) { - simplePhysicsComponent->Serialize(outBitStream, bIsInitialUpdate); - } + auto* const component = GetComponent(type); + if (!component) continue; - RigidbodyPhantomPhysicsComponent* rigidbodyPhantomPhysics; - if (TryGetComponent(eReplicaComponentType::RIGID_BODY_PHANTOM_PHYSICS, rigidbodyPhantomPhysics)) { - rigidbodyPhantomPhysics->Serialize(outBitStream, bIsInitialUpdate); - } - - HavokVehiclePhysicsComponent* havokVehiclePhysicsComponent; - if (TryGetComponent(eReplicaComponentType::HAVOK_VEHICLE_PHYSICS, havokVehiclePhysicsComponent)) { - havokVehiclePhysicsComponent->Serialize(outBitStream, bIsInitialUpdate); - } - - PhantomPhysicsComponent* phantomPhysicsComponent; - if (TryGetComponent(eReplicaComponentType::PHANTOM_PHYSICS, phantomPhysicsComponent)) { - phantomPhysicsComponent->Serialize(outBitStream, bIsInitialUpdate); - } - - SoundTriggerComponent* soundTriggerComponent; - if (TryGetComponent(eReplicaComponentType::SOUND_TRIGGER, soundTriggerComponent)) { - soundTriggerComponent->Serialize(outBitStream, bIsInitialUpdate); - } - - RacingSoundTriggerComponent* racingSoundTriggerComponent; - if (TryGetComponent(eReplicaComponentType::RACING_SOUND_TRIGGER, racingSoundTriggerComponent)) { - racingSoundTriggerComponent->Serialize(outBitStream, bIsInitialUpdate); - } - - BuffComponent* buffComponent; - if (TryGetComponent(eReplicaComponentType::BUFF, buffComponent)) { - buffComponent->Serialize(outBitStream, bIsInitialUpdate); - - DestroyableComponent* destroyableComponent; - if (TryGetComponent(eReplicaComponentType::DESTROYABLE, destroyableComponent)) { - destroyableComponent->Serialize(outBitStream, bIsInitialUpdate); + if (type == CHARACTER) { + // Written with the character component; every character has them, a 0 bit stands in should one be missing + for (const auto characterPart : { POSSESSOR, LEVEL_PROGRESSION, PLAYER_FORCED_MOVEMENT }) { + auto* const part = GetComponent(characterPart); + if (part) part->Serialize(outBitStream, bIsInitialUpdate); + else outBitStream.Write0(); + } } - destroyableSerialized = true; - } - - CollectibleComponent* collectibleComponent; - if (TryGetComponent(eReplicaComponentType::COLLECTIBLE, collectibleComponent)) { - DestroyableComponent* destroyableComponent; - if (TryGetComponent(eReplicaComponentType::DESTROYABLE, destroyableComponent) && !destroyableSerialized) { - destroyableComponent->Serialize(outBitStream, bIsInitialUpdate); - } - destroyableSerialized = true; - collectibleComponent->Serialize(outBitStream, bIsInitialUpdate); - } - - PetComponent* petComponent; - if (TryGetComponent(eReplicaComponentType::PET, petComponent)) { - petComponent->Serialize(outBitStream, bIsInitialUpdate); - } - - CharacterComponent* characterComponent; - if (TryGetComponent(eReplicaComponentType::CHARACTER, characterComponent)) { - - PossessorComponent* possessorComponent; - if (TryGetComponent(eReplicaComponentType::POSSESSOR, possessorComponent)) { - possessorComponent->Serialize(outBitStream, bIsInitialUpdate); - } else { - // Should never happen, but just to be safe - outBitStream.Write0(); - } - - LevelProgressionComponent* levelProgressionComponent; - if (TryGetComponent(eReplicaComponentType::LEVEL_PROGRESSION, levelProgressionComponent)) { - levelProgressionComponent->Serialize(outBitStream, bIsInitialUpdate); - } else { - // Should never happen, but just to be safe - outBitStream.Write0(); - } - - PlayerForcedMovementComponent* playerForcedMovementComponent; - if (TryGetComponent(eReplicaComponentType::PLAYER_FORCED_MOVEMENT, playerForcedMovementComponent)) { - playerForcedMovementComponent->Serialize(outBitStream, bIsInitialUpdate); - } else { - // Should never happen, but just to be safe - outBitStream.Write0(); - } - - characterComponent->Serialize(outBitStream, bIsInitialUpdate); - } - - ItemComponent* itemComponent; - if (TryGetComponent(eReplicaComponentType::ITEM, itemComponent)) { - itemComponent->Serialize(outBitStream, bIsInitialUpdate); - } - - InventoryComponent* inventoryComponent; - if (TryGetComponent(eReplicaComponentType::INVENTORY, inventoryComponent)) { - inventoryComponent->Serialize(outBitStream, bIsInitialUpdate); - } - - ScriptComponent* scriptComponent; - if (TryGetComponent(eReplicaComponentType::SCRIPT, scriptComponent)) { - scriptComponent->Serialize(outBitStream, bIsInitialUpdate); - } - - SkillComponent* skillComponent; - if (TryGetComponent(eReplicaComponentType::SKILL, skillComponent)) { - skillComponent->Serialize(outBitStream, bIsInitialUpdate); - } - - BaseCombatAIComponent* baseCombatAiComponent; - if (TryGetComponent(eReplicaComponentType::BASE_COMBAT_AI, baseCombatAiComponent)) { - baseCombatAiComponent->Serialize(outBitStream, bIsInitialUpdate); - } - - QuickBuildComponent* quickBuildComponent; - if (TryGetComponent(eReplicaComponentType::QUICK_BUILD, quickBuildComponent)) { - DestroyableComponent* destroyableComponent; - if (TryGetComponent(eReplicaComponentType::DESTROYABLE, destroyableComponent) && !destroyableSerialized) { - destroyableComponent->Serialize(outBitStream, bIsInitialUpdate); - } - destroyableSerialized = true; - quickBuildComponent->Serialize(outBitStream, bIsInitialUpdate); - } - - MovingPlatformComponent* movingPlatformComponent; - if (TryGetComponent(eReplicaComponentType::MOVING_PLATFORM, movingPlatformComponent)) { - movingPlatformComponent->Serialize(outBitStream, bIsInitialUpdate); - } - - SwitchComponent* switchComponent; - if (TryGetComponent(eReplicaComponentType::SWITCH, switchComponent)) { - switchComponent->Serialize(outBitStream, bIsInitialUpdate); - } - - VendorComponent* vendorComponent; - if (TryGetComponent(eReplicaComponentType::VENDOR, vendorComponent)) { - vendorComponent->Serialize(outBitStream, bIsInitialUpdate); - } - - DonationVendorComponent* donationVendorComponent; - if (TryGetComponent(eReplicaComponentType::DONATION_VENDOR, donationVendorComponent)) { - donationVendorComponent->Serialize(outBitStream, bIsInitialUpdate); - } - - AchievementVendorComponent* achievementVendorComponent; - if (TryGetComponent(eReplicaComponentType::ACHIEVEMENT_VENDOR, achievementVendorComponent)) { - achievementVendorComponent->Serialize(outBitStream, bIsInitialUpdate); - } - - BouncerComponent* bouncerComponent; - if (TryGetComponent(eReplicaComponentType::BOUNCER, bouncerComponent)) { - bouncerComponent->Serialize(outBitStream, bIsInitialUpdate); - } - - ScriptedActivityComponent* scriptedActivityComponent; - if (TryGetComponent(eReplicaComponentType::SCRIPTED_ACTIVITY, scriptedActivityComponent)) { - scriptedActivityComponent->Serialize(outBitStream, bIsInitialUpdate); - } - - ShootingGalleryComponent* shootingGalleryComponent; - if (TryGetComponent(eReplicaComponentType::SHOOTING_GALLERY, shootingGalleryComponent)) { - shootingGalleryComponent->Serialize(outBitStream, bIsInitialUpdate); - } - - RacingControlComponent* racingControlComponent; - if (TryGetComponent(eReplicaComponentType::RACING_CONTROL, racingControlComponent)) { - racingControlComponent->Serialize(outBitStream, bIsInitialUpdate); - } - - LUPExhibitComponent* lupExhibitComponent; - if (TryGetComponent(eReplicaComponentType::LUP_EXHIBIT, lupExhibitComponent)) { - lupExhibitComponent->Serialize(outBitStream, bIsInitialUpdate); - } - - ModelComponent* modelComponent; - if (TryGetComponent(eReplicaComponentType::MODEL, modelComponent)) { - modelComponent->Serialize(outBitStream, bIsInitialUpdate); - } - - RenderComponent* renderComponent; - if (TryGetComponent(eReplicaComponentType::RENDER, renderComponent)) { - renderComponent->Serialize(outBitStream, bIsInitialUpdate); - } - - if (modelComponent || !destroyableSerialized) { - DestroyableComponent* destroyableComponent; - if (TryGetComponent(eReplicaComponentType::DESTROYABLE, destroyableComponent) && !destroyableSerialized) { - destroyableComponent->Serialize(outBitStream, bIsInitialUpdate); - destroyableSerialized = true; - } - } - - MiniGameControlComponent* miniGameControlComponent; - if (TryGetComponent(eReplicaComponentType::MINI_GAME_CONTROL, miniGameControlComponent)) { - miniGameControlComponent->Serialize(outBitStream, bIsInitialUpdate); + component->Serialize(outBitStream, bIsInitialUpdate); } // BBB Component, unused currently diff --git a/dGame/dComponents/DestroyableComponent.cpp b/dGame/dComponents/DestroyableComponent.cpp index 1579b7eb0..927889e47 100644 --- a/dGame/dComponents/DestroyableComponent.cpp +++ b/dGame/dComponents/DestroyableComponent.cpp @@ -97,48 +97,6 @@ DestroyableComponent::DestroyableComponent(Entity* parent, const int32_t compone DestroyableComponent::~DestroyableComponent() { } -void DestroyableComponent::Reinitialize(LOT templateID) { - CDComponentsRegistryTable* compRegistryTable = CDClientManager::GetTable(); - - int32_t buffComponentID = compRegistryTable->GetByIDAndType(templateID, eReplicaComponentType::BUFF); - int32_t collectibleComponentID = compRegistryTable->GetByIDAndType(templateID, eReplicaComponentType::COLLECTIBLE); - int32_t quickBuildComponentID = compRegistryTable->GetByIDAndType(templateID, eReplicaComponentType::QUICK_BUILD); - - int32_t componentID = 0; - if (collectibleComponentID > 0) componentID = collectibleComponentID; - if (quickBuildComponentID > 0) componentID = quickBuildComponentID; - if (buffComponentID > 0) componentID = buffComponentID; - - CDDestructibleComponentTable* destCompTable = CDClientManager::GetTable(); - std::vector destCompData = destCompTable->Query([=](CDDestructibleComponent entry) { return (entry.id == componentID); }); - - if (componentID > 0) { - std::vector destCompData = destCompTable->Query([=](CDDestructibleComponent entry) { return (entry.id == componentID); }); - - if (destCompData.size() > 0) { - SetHealth(destCompData[0].life); - SetImagination(destCompData[0].imagination); - SetArmor(destCompData[0].armor); - - SetMaxHealth(destCompData[0].life); - SetMaxImagination(destCompData[0].imagination); - SetMaxArmor(destCompData[0].armor); - - SetIsSmashable(destCompData[0].isSmashable); - } - } else { - SetHealth(1); - SetImagination(0); - SetArmor(0); - - SetMaxHealth(1); - SetMaxImagination(0); - SetMaxArmor(0); - - SetIsSmashable(true); - } -} - void DestroyableComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) { if (bIsInitialUpdate) { outBitStream.Write1(); // always write these on construction diff --git a/dGame/dComponents/DestroyableComponent.h b/dGame/dComponents/DestroyableComponent.h index 5091e9703..43c46a56c 100644 --- a/dGame/dComponents/DestroyableComponent.h +++ b/dGame/dComponents/DestroyableComponent.h @@ -36,12 +36,6 @@ public: void LoadFromXml(const tinyxml2::XMLDocument& doc) override; void UpdateXml(tinyxml2::XMLDocument& doc) override; - /** - * Initializes the component using a different LOT - * @param templateID the ID to use for initialization - */ - void Reinitialize(LOT templateID); - /** * Sets the health of this entity. Makes sure this is serialized on the next tick and if this is a character its * stats will also update. diff --git a/tests/dGameTests/CMakeLists.txt b/tests/dGameTests/CMakeLists.txt index 5f87aaa71..400dcffcf 100644 --- a/tests/dGameTests/CMakeLists.txt +++ b/tests/dGameTests/CMakeLists.txt @@ -25,7 +25,7 @@ file(COPY ${COMPONENT_TEST_DATA} DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) # Add the executable. Remember to add all tests above this! add_executable(dGameTests ${DGAMETEST_SOURCES}) -target_include_directories(dGameTests PRIVATE "." "${PROJECT_SOURCE_DIR}/dChatServer") +target_include_directories(dGameTests PRIVATE "." "${PROJECT_SOURCE_DIR}/dChatServer" "${PROJECT_SOURCE_DIR}/dScripts") if(APPLE) add_custom_target(dGameTestsLink diff --git a/tests/dGameTests/dComponentsTests/CMakeLists.txt b/tests/dGameTests/dComponentsTests/CMakeLists.txt index f73f1214c..5a55650ae 100644 --- a/tests/dGameTests/dComponentsTests/CMakeLists.txt +++ b/tests/dGameTests/dComponentsTests/CMakeLists.txt @@ -3,6 +3,7 @@ set(DCOMPONENTS_TESTS "PetComponentTests.cpp" "SimplePhysicsComponentTests.cpp" "SavingTests.cpp" + "ReplicaComponentOrderTests.cpp" ) # Get the folder name and prepend it to the files above diff --git a/tests/dGameTests/dComponentsTests/ReplicaComponentOrderTests.cpp b/tests/dGameTests/dComponentsTests/ReplicaComponentOrderTests.cpp new file mode 100644 index 000000000..1995aec34 --- /dev/null +++ b/tests/dGameTests/dComponentsTests/ReplicaComponentOrderTests.cpp @@ -0,0 +1,561 @@ +#include "GameDependencies.h" +#include + +#include +#include +#include + +#include "BitStream.h" +#include "CDClientDatabase.h" +#include "Character.h" +#include "Entity.h" +#include "eReplicaComponentType.h" +#include "eReplicaPacketType.h" +#include "tinyxml2.h" + +#include "AchievementVendorComponent.h" +#include "BaseCombatAIComponent.h" +#include "BouncerComponent.h" +#include "BuffComponent.h" +#include "CharacterComponent.h" +#include "CollectibleComponent.h" +#include "ControllablePhysicsComponent.h" +#include "DestroyableComponent.h" +#include "DonationVendorComponent.h" +#include "HavokVehiclePhysicsComponent.h" +#include "InventoryComponent.h" +#include "ItemComponent.h" +#include "LevelProgressionComponent.h" +#include "LUPExhibitComponent.h" +#include "MiniGameControlComponent.h" +#include "ModelComponent.h" +#include "ModuleAssemblyComponent.h" +#include "MovingPlatformComponent.h" +#include "PetComponent.h" +#include "PhantomPhysicsComponent.h" +#include "PlayerForcedMovementComponent.h" +#include "PossessableComponent.h" +#include "PossessorComponent.h" +#include "QuickBuildComponent.h" +#include "RacingControlComponent.h" +#include "RacingSoundTriggerComponent.h" +#include "RenderComponent.h" +#include "RigidbodyPhantomPhysicsComponent.h" +#include "ScriptComponent.h" +#include "ScriptedActivityComponent.h" +#include "ShootingGalleryComponent.h" +#include "SimplePhysicsComponent.h" +#include "SkillComponent.h" +#include "SoundTriggerComponent.h" +#include "SwitchComponent.h" +#include "VendorComponent.h" + +/** + * Entity::WriteComponents writes the components in the client's order from a list of component types. Before, the + * order was spelled out component by component, with flags for where the destroyable went. That code is kept below, + * unchanged apart from looking components up by class, as the oracle: the bytes must not change. + */ +namespace { + template + bool TryGet(const Entity& entity, T*& component) { + component = entity.GetComponent(); + return component != nullptr; + } + + void OldWriteComponents(const Entity& entity, RakNet::BitStream& outBitStream, eReplicaPacketType packetType) { + + /** + * This has to be done in a specific order. + */ + + bool destroyableSerialized = false; + bool bIsInitialUpdate = packetType == eReplicaPacketType::CONSTRUCTION; + + PossessableComponent* possessableComponent; + if (TryGet(entity, possessableComponent)) { + possessableComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + ModuleAssemblyComponent* moduleAssemblyComponent; + if (TryGet(entity, moduleAssemblyComponent)) { + moduleAssemblyComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + ControllablePhysicsComponent* controllablePhysicsComponent; + if (TryGet(entity, controllablePhysicsComponent)) { + controllablePhysicsComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + SimplePhysicsComponent* simplePhysicsComponent; + if (TryGet(entity, simplePhysicsComponent)) { + simplePhysicsComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + RigidbodyPhantomPhysicsComponent* rigidbodyPhantomPhysics; + if (TryGet(entity, rigidbodyPhantomPhysics)) { + rigidbodyPhantomPhysics->Serialize(outBitStream, bIsInitialUpdate); + } + + HavokVehiclePhysicsComponent* havokVehiclePhysicsComponent; + if (TryGet(entity, havokVehiclePhysicsComponent)) { + havokVehiclePhysicsComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + PhantomPhysicsComponent* phantomPhysicsComponent; + if (TryGet(entity, phantomPhysicsComponent)) { + phantomPhysicsComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + SoundTriggerComponent* soundTriggerComponent; + if (TryGet(entity, soundTriggerComponent)) { + soundTriggerComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + RacingSoundTriggerComponent* racingSoundTriggerComponent; + if (TryGet(entity, racingSoundTriggerComponent)) { + racingSoundTriggerComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + BuffComponent* buffComponent; + if (TryGet(entity, buffComponent)) { + buffComponent->Serialize(outBitStream, bIsInitialUpdate); + + DestroyableComponent* destroyableComponent; + if (TryGet(entity, destroyableComponent)) { + destroyableComponent->Serialize(outBitStream, bIsInitialUpdate); + } + destroyableSerialized = true; + } + + CollectibleComponent* collectibleComponent; + if (TryGet(entity, collectibleComponent)) { + DestroyableComponent* destroyableComponent; + if (TryGet(entity, destroyableComponent) && !destroyableSerialized) { + destroyableComponent->Serialize(outBitStream, bIsInitialUpdate); + } + destroyableSerialized = true; + collectibleComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + PetComponent* petComponent; + if (TryGet(entity, petComponent)) { + petComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + CharacterComponent* characterComponent; + if (TryGet(entity, characterComponent)) { + + PossessorComponent* possessorComponent; + if (TryGet(entity, possessorComponent)) { + possessorComponent->Serialize(outBitStream, bIsInitialUpdate); + } else { + // Should never happen, but just to be safe + outBitStream.Write0(); + } + + LevelProgressionComponent* levelProgressionComponent; + if (TryGet(entity, levelProgressionComponent)) { + levelProgressionComponent->Serialize(outBitStream, bIsInitialUpdate); + } else { + // Should never happen, but just to be safe + outBitStream.Write0(); + } + + PlayerForcedMovementComponent* playerForcedMovementComponent; + if (TryGet(entity, playerForcedMovementComponent)) { + playerForcedMovementComponent->Serialize(outBitStream, bIsInitialUpdate); + } else { + // Should never happen, but just to be safe + outBitStream.Write0(); + } + + characterComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + ItemComponent* itemComponent; + if (TryGet(entity, itemComponent)) { + itemComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + InventoryComponent* inventoryComponent; + if (TryGet(entity, inventoryComponent)) { + inventoryComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + ScriptComponent* scriptComponent; + if (TryGet(entity, scriptComponent)) { + scriptComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + SkillComponent* skillComponent; + if (TryGet(entity, skillComponent)) { + skillComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + BaseCombatAIComponent* baseCombatAiComponent; + if (TryGet(entity, baseCombatAiComponent)) { + baseCombatAiComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + QuickBuildComponent* quickBuildComponent; + if (TryGet(entity, quickBuildComponent)) { + DestroyableComponent* destroyableComponent; + if (TryGet(entity, destroyableComponent) && !destroyableSerialized) { + destroyableComponent->Serialize(outBitStream, bIsInitialUpdate); + } + destroyableSerialized = true; + quickBuildComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + MovingPlatformComponent* movingPlatformComponent; + if (TryGet(entity, movingPlatformComponent)) { + movingPlatformComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + SwitchComponent* switchComponent; + if (TryGet(entity, switchComponent)) { + switchComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + VendorComponent* vendorComponent; + if (TryGet(entity, vendorComponent)) { + vendorComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + DonationVendorComponent* donationVendorComponent; + if (TryGet(entity, donationVendorComponent)) { + donationVendorComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + AchievementVendorComponent* achievementVendorComponent; + if (TryGet(entity, achievementVendorComponent)) { + achievementVendorComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + BouncerComponent* bouncerComponent; + if (TryGet(entity, bouncerComponent)) { + bouncerComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + ScriptedActivityComponent* scriptedActivityComponent; + if (TryGet(entity, scriptedActivityComponent)) { + scriptedActivityComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + ShootingGalleryComponent* shootingGalleryComponent; + if (TryGet(entity, shootingGalleryComponent)) { + shootingGalleryComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + RacingControlComponent* racingControlComponent; + if (TryGet(entity, racingControlComponent)) { + racingControlComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + LUPExhibitComponent* lupExhibitComponent; + if (TryGet(entity, lupExhibitComponent)) { + lupExhibitComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + ModelComponent* modelComponent; + if (TryGet(entity, modelComponent)) { + modelComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + RenderComponent* renderComponent; + if (TryGet(entity, renderComponent)) { + renderComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + if (modelComponent || !destroyableSerialized) { + DestroyableComponent* destroyableComponent; + if (TryGet(entity, destroyableComponent) && !destroyableSerialized) { + destroyableComponent->Serialize(outBitStream, bIsInitialUpdate); + destroyableSerialized = true; + } + } + + MiniGameControlComponent* miniGameControlComponent; + if (TryGet(entity, miniGameControlComponent)) { + miniGameControlComponent->Serialize(outBitStream, bIsInitialUpdate); + } + + // BBB Component, unused currently + // Need to to write0 so that is serialized correctly + // TODO: Implement BBB Component + outBitStream.Write0(); + } + + + // Some components read the CDClient in their constructors; give them an empty one + void ConnectEmptyCDClient() { + if (CDClientDatabase::isConnected) return; + CDClientDatabase::Connect(":memory:"); + for (const auto* table : { + "ComponentsRegistry (id INTEGER, component_type INTEGER, component_id INTEGER)", + "BaseCombatAIComponent (id INTEGER, aggroRadius REAL, tetherSpeed REAL, pursuitSpeed REAL, softTetherRadius REAL, hardTetherRadius REAL, minRoundLength REAL, maxRoundLength REAL, combatRoundLength REAL)", + "ItemSets (setID INTEGER, itemIDs TEXT)", + "ObjectSkills (objectTemplate INTEGER, skillID INTEGER, castOnType INTEGER, AICombatWeight INTEGER)", + "SkillBehavior (skillID INTEGER, behaviorID INTEGER)", + "RenderComponent (id INTEGER)", + "BuffParameters (BuffID INTEGER)", + "PossessableComponent (id INTEGER, possessionType INTEGER, depossessOnHit INTEGER, skillSet INTEGER)", + }) { + CDClientDatabase::ExecuteDML(std::string("CREATE TABLE ") + table + ";"); + } + } + + std::vector Bytes(RakNet::BitStream& stream) { + return { stream.GetData(), stream.GetData() + stream.GetNumberOfBytesUsed() }; + } +} + +class ReplicaComponentOrderTest : public GameDependenciesTest { +protected: + std::vector> characters; + + void SetUp() override { + SetUpDependencies(); + ConnectEmptyCDClient(); + } + + void TearDown() override { + TearDownDependencies(); + } + + std::unique_ptr MakePlayer() { + auto entity = std::make_unique(1, info); + auto& character = characters.emplace_back(std::make_unique(1, nullptr)); + tinyxml2::XMLDocument doc; + doc.LoadFile("./test_xml_data.xml"); + tinyxml2::XMLPrinter printer{ 0, true, 0 }; + doc.Print(&printer); + character->_setXmlData(printer.CStr()); + character->_doQuickXMLDataParse(); + entity->SetCharacter(character.get()); + character->SetEntity(entity.get()); + entity->AddComponent(-1); + entity->AddComponent(-1); + entity->AddComponent(-1)->SetMaxHealth(4.0f); + entity->AddComponent(-1); + entity->AddComponent(-1); + entity->AddComponent(-1); + entity->AddComponent(-1, character.get(), UNASSIGNED_SYSTEM_ADDRESS); + entity->AddComponent(-1); + entity->AddComponent(-1); + entity->AddComponent(-1); + return entity; + } + + // Serializes an entity built by `build` with the old and the new code (each on its own copy, as serializing + // clears dirty flags) and expects the same bits, for the construction and a serialization. + void ExpectSameBytes(const std::function& build) { + for (const auto packetType : { eReplicaPacketType::CONSTRUCTION, eReplicaPacketType::SERIALIZATION }) { + Entity oldEntity(15, info); + Entity newEntity(15, info); + // Same random numbers for both (a vehicle picks a random end behavior) + Game::randomEngine.seed(1); + build(oldEntity); + Game::randomEngine.seed(1); + build(newEntity); + RakNet::BitStream oldStream; + RakNet::BitStream newStream; + OldWriteComponents(oldEntity, oldStream, packetType); + newEntity.WriteComponents(newStream, packetType); + EXPECT_GT(newStream.GetNumberOfBitsUsed(), 1u); + EXPECT_EQ(oldStream.GetNumberOfBitsUsed(), newStream.GetNumberOfBitsUsed()) << "packet type " << static_cast(packetType); + const auto oldBytes = Bytes(oldStream); + const auto newBytes = Bytes(newStream); + const auto mismatch = std::ranges::mismatch(oldBytes, newBytes); + EXPECT_EQ(mismatch.in1, oldBytes.end()) << "packet type " << static_cast(packetType) << ", first different byte " << (mismatch.in1 - oldBytes.begin()); + } + } +}; + +TEST(ReplicaComponentTypeTest, MatchesComponentsRegistry) { + // ComponentsRegistry.component_type values + EXPECT_EQ(static_cast(eReplicaComponentType::DESTROYABLE), 7u); + EXPECT_EQ(static_cast(eReplicaComponentType::BUFF), 98u); + EXPECT_EQ(DestroyableComponent::ComponentType, eReplicaComponentType::DESTROYABLE); + EXPECT_EQ(BuffComponent::ComponentType, eReplicaComponentType::BUFF); +} + +TEST_F(ReplicaComponentOrderTest, Player) { + for (const auto packetType : { eReplicaPacketType::CONSTRUCTION, eReplicaPacketType::SERIALIZATION }) { + auto oldEntity = MakePlayer(); + auto newEntity = MakePlayer(); + RakNet::BitStream oldStream; + RakNet::BitStream newStream; + OldWriteComponents(*oldEntity, oldStream, packetType); + newEntity->WriteComponents(newStream, packetType); + EXPECT_EQ(oldStream.GetNumberOfBitsUsed(), newStream.GetNumberOfBitsUsed()); + EXPECT_EQ(Bytes(oldStream), Bytes(newStream)); + oldEntity->SetCharacter(nullptr); + newEntity->SetCharacter(nullptr); + } +} + +TEST_F(ReplicaComponentOrderTest, Enemy) { + ExpectSameBytes([](Entity& entity) { + entity.AddComponent(-1); + entity.AddComponent(-1); + auto* destroyable = entity.AddComponent(-1); + destroyable->SetMaxHealth(8.0f); + destroyable->SetHealth(8); + destroyable->AddFactionNoLookup(4); + entity.AddComponent(-1, "", true); + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1); + }); +} + +TEST_F(ReplicaComponentOrderTest, SmashableWithDestructibleRow) { + ExpectSameBytes([](Entity& entity) { + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1)->SetIsSmashable(true); + entity.AddComponent(-1); + }); +} + +TEST_F(ReplicaComponentOrderTest, IsSmashableWithoutRegistryEntry) { + // Written after the render component + ExpectSameBytes([](Entity& entity) { + entity.AddComponent(-1); + entity.AddComponent(-1, "", true); + auto* destroyable = entity.AddComponent(-1); + destroyable->SetIsSmashable(true); + destroyable->SetMaxHealth(1.0f); + destroyable->SetHealth(1); + entity.AddComponent(-1); + entity.AddComponent(-1); + }); +} + +TEST_F(ReplicaComponentOrderTest, QuickBuildWithoutDestroyable) { + ExpectSameBytes([](Entity& entity) { + entity.AddComponent(-1); + entity.AddComponent(-1, "", true); + entity.AddComponent(-1); + entity.AddComponent(-1); + }); +} + +TEST_F(ReplicaComponentOrderTest, QuickBuildWithoutRegistryEntry) { + // Written right before the quick build, after the script + ExpectSameBytes([](Entity& entity) { + entity.AddComponent(-1); + entity.AddComponent(-1)->SetIsSmashable(true); + entity.AddComponent(-1, "", true); + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1, ""); + entity.AddComponent(-1); + }); +} + +TEST_F(ReplicaComponentOrderTest, QuickBuildWithRegistryEntry) { + ExpectSameBytes([](Entity& entity) { + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1)->SetMaxHealth(3.0f); + entity.AddComponent(-1, "", true); + entity.AddComponent(-1); + entity.AddComponent(-1); + }); +} + +TEST_F(ReplicaComponentOrderTest, CollectibleWithoutRegistryEntry) { + ExpectSameBytes([](Entity& entity) { + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1, 12); + entity.AddComponent(-1, "", true); + entity.AddComponent(-1); + }); +} + +TEST_F(ReplicaComponentOrderTest, CollectibleWithRegistryEntry) { + ExpectSameBytes([](Entity& entity) { + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1)->SetIsSmashable(true); + entity.AddComponent(-1, 3); + entity.AddComponent(-1); + }); +} + +TEST_F(ReplicaComponentOrderTest, Npc) { + ExpectSameBytes([](Entity& entity) { + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1, "", true); + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1); + }); +} + +TEST_F(ReplicaComponentOrderTest, Pet) { + ExpectSameBytes([](Entity& entity) { + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(1); + entity.AddComponent(-1); + entity.AddComponent(-1); + }); +} + +TEST_F(ReplicaComponentOrderTest, Vehicle) { + ExpectSameBytes([](Entity& entity) { + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1); + }); +} + +TEST_F(ReplicaComponentOrderTest, Model) { + // A model's destroyable is written after the render component + ExpectSameBytes([](Entity& entity) { + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1)->SetIsSmashable(true); + }); +} + +TEST_F(ReplicaComponentOrderTest, EveryListedComponent) { + ExpectSameBytes([](Entity& entity) { + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1, 1); + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1, "", true); + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1); + entity.AddComponent(-1); + }); +} diff --git a/tests/dWebTests/CDClientSchemaTests.cpp b/tests/dWebTests/CDClientSchemaTests.cpp index aac76cf04..f09dbab2e 100644 --- a/tests/dWebTests/CDClientSchemaTests.cpp +++ b/tests/dWebTests/CDClientSchemaTests.cpp @@ -74,7 +74,7 @@ TEST(CDClientSchemaTest, ComponentTables) { }; EXPECT_EQ(name(eReplicaComponentType::VENDOR), "VendorComponent"); EXPECT_EQ(name(eReplicaComponentType::RENDER), "RenderComponent"); - EXPECT_EQ(name(eReplicaComponentType::BUFF), "DestructibleComponent"); + EXPECT_EQ(name(eReplicaComponentType::DESTROYABLE), "DestructibleComponent"); EXPECT_EQ(name(eReplicaComponentType::SIMPLE_PHYSICS), "PhysicsComponent"); EXPECT_EQ(name(eReplicaComponentType::PET), "-"); }