From 6a438ac41954e09fb786d93f25967f2f0bb068c7 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 03:09:52 -0500 Subject: [PATCH] fix(quickbuild): a finished quickbuild's EnableRebuild has no duration Live sent duration 0.0 in all 744 successful EnableRebuild (cancels carry the time spent building, which DLU already matches); DLU sent the reset time. Tests get EntityManager::_addEntity/_removeEntity to make an entity built outside CreateEntity findable, and helpers that read game messages back out of captured packets. Check: finish a quickbuild; the build completes, the celebrate animation plays and the built object stays up for its usual time before resetting. Co-Authored-By: Claude Opus 5.5 --- dGame/EntityManager.cpp | 4 + dGame/EntityManager.h | 4 + dGame/dComponents/QuickBuildComponent.cpp | 3 +- .../dComponentsTests/CMakeLists.txt | 1 + .../QuickBuildCompleteTests.cpp | 90 +++++++++++++++++++ .../dGameMessagesTests/GameMessageTestUtils.h | 29 ++++++ 6 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 tests/dGameTests/dComponentsTests/QuickBuildCompleteTests.cpp diff --git a/dGame/EntityManager.cpp b/dGame/EntityManager.cpp index cfd8c1604..e1990181c 100644 --- a/dGame/EntityManager.cpp +++ b/dGame/EntityManager.cpp @@ -184,6 +184,10 @@ Entity* EntityManager::CreateEntity(EntityInfo info, User* user, Entity* parentE return entity; } +void EntityManager::_addEntity(Entity* entity) { + m_Entities.insert_or_assign(entity->GetObjectID(), entity); +} + void EntityManager::DestroyEntity(const LWOOBJID& objectID) { DestroyEntity(GetEntity(objectID)); } diff --git a/dGame/EntityManager.h b/dGame/EntityManager.h index 437247920..6e597af1b 100644 --- a/dGame/EntityManager.h +++ b/dGame/EntityManager.h @@ -46,6 +46,10 @@ public: const std::unordered_map GetAllEntities() const { return m_Entities; } #endif + // Tests only: makes an entity built without CreateEntity (and still owned by the caller) findable with GetEntity. + void _addEntity(Entity* entity); + void _removeEntity(LWOOBJID objectID) { m_Entities.erase(objectID); } + void ConstructEntity(Entity* entity, const SystemAddress& sysAddr = UNASSIGNED_SYSTEM_ADDRESS); void DestructEntity(Entity* entity, const SystemAddress& sysAddr = UNASSIGNED_SYSTEM_ADDRESS); void SerializeEntity(Entity* entity); diff --git a/dGame/dComponents/QuickBuildComponent.cpp b/dGame/dComponents/QuickBuildComponent.cpp index 70c4a2ad4..e05811bf1 100644 --- a/dGame/dComponents/QuickBuildComponent.cpp +++ b/dGame/dComponents/QuickBuildComponent.cpp @@ -473,7 +473,8 @@ void QuickBuildComponent::CompleteQuickBuild(Entity* const user) { enableRebuild.bFail = false; enableRebuild.bSuccess = true; enableRebuild.eFailReason = eQuickBuildFailReason::NOT_GIVEN; - enableRebuild.fDuration = m_ResetTime; + // Live sent no duration on success (744 of 744); cancels carry the time spent building. + enableRebuild.fDuration = 0.0f; enableRebuild.user = user->GetObjectID(); enableRebuild.Send(UNASSIGNED_SYSTEM_ADDRESS); GameMessages::TerminateInteraction(user->GetObjectID(), eTerminateType::FROM_INTERACTION, m_Parent->GetObjectID()).Send(UNASSIGNED_SYSTEM_ADDRESS); diff --git a/tests/dGameTests/dComponentsTests/CMakeLists.txt b/tests/dGameTests/dComponentsTests/CMakeLists.txt index 5161ed7e7..67990031f 100644 --- a/tests/dGameTests/dComponentsTests/CMakeLists.txt +++ b/tests/dGameTests/dComponentsTests/CMakeLists.txt @@ -13,6 +13,7 @@ set(DCOMPONENTS_TESTS "CharacterSaveFieldsTests.cpp" "CollectibleComponentTests.cpp" "ReplicaComponentOrderTests.cpp" + "QuickBuildCompleteTests.cpp" ) # Get the folder name and prepend it to the files above diff --git a/tests/dGameTests/dComponentsTests/QuickBuildCompleteTests.cpp b/tests/dGameTests/dComponentsTests/QuickBuildCompleteTests.cpp new file mode 100644 index 000000000..7cfad2153 --- /dev/null +++ b/tests/dGameTests/dComponentsTests/QuickBuildCompleteTests.cpp @@ -0,0 +1,90 @@ +#include "GameDependencies.h" +#include "dGameMessagesTests/GameMessageTestUtils.h" + +#include "Character.h" +#include "CharacterComponent.h" +#include "CDClientDatabase.h" +#include "Entity.h" +#include "QuickBuildComponent.h" +#include "QuickBuildMessages.h" +#include "eQuickBuildState.h" + +#include + +#include + +using namespace GameMessageTestUtils; + +namespace { + // The quick build spawns its activator through the entity manager, which reads the CDClient: give it 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)", + "ObjectSkills (objectTemplate INTEGER, skillID INTEGER, castOnType INTEGER, AICombatWeight INTEGER)", + "SkillBehavior (skillID INTEGER, behaviorID INTEGER)", + "ItemSets (setID INTEGER, itemIDs TEXT)", + }) { + CDClientDatabase::ExecuteDML(std::string("CREATE TABLE ") + table + ";"); + } + } +} + +// What a finished quickbuild sends, compared with live (744 successful builds in the live captures). +class QuickBuildCompleteTests : public GameDependenciesTest { +protected: + void SetUp() override { + SetUpDependencies(); + ConnectEmptyCDClient(); + auto playerInfo = info; + playerInfo.lot = 1; + player = std::make_unique(0x1000000000000001LL, playerInfo); + Game::entityManager->_addEntity(player.get()); // the quickbuild looks its builder up there + character = std::make_unique(1, nullptr); + player->SetCharacter(character.get()); + character->SetEntity(player.get()); + player->AddComponent(-1, character.get(), ClientAddress()); + + auto buildInfo = info; + buildInfo.lot = 4717; + build = std::make_unique(0x0102030405060708LL, buildInfo); + quickBuild = build->AddComponent(-1); + quickBuild->SetResetTime(20.0f); + quickBuild->SetCompleteTime(0.0f); + } + + // Starts the build and runs updates until it completes (the first update only sets up the imagination drain). + std::vector Build() { + quickBuild->OnUse(player.get()); + return Capture([&] { + for (int i = 0; i < 3 && quickBuild->GetState() != eQuickBuildState::COMPLETED; i++) quickBuild->Update(0.1f); + }); + } + + void TearDown() override { + quickBuild = nullptr; + build.reset(); + Game::entityManager->_removeEntity(player->GetObjectID()); + player->SetCharacter(nullptr); + player.reset(); + character.reset(); + TearDownDependencies(); + } + + std::unique_ptr player; + std::unique_ptr character; + std::unique_ptr build; + QuickBuildComponent* quickBuild{}; +}; + +TEST_F(QuickBuildCompleteTests, SuccessHasNoDuration) { + const auto sent = Build(); + ASSERT_EQ(quickBuild->GetState(), eQuickBuildState::COMPLETED); + const auto enables = SentGameMessages(sent); + ASSERT_EQ(enables.size(), 1u); + EXPECT_TRUE(enables[0].bSuccess); + EXPECT_FALSE(enables[0].bFail); + EXPECT_EQ(enables[0].fDuration, 0.0f); + EXPECT_EQ(enables[0].user, player->GetObjectID()); +} diff --git a/tests/dGameTests/dGameMessagesTests/GameMessageTestUtils.h b/tests/dGameTests/dGameMessagesTests/GameMessageTestUtils.h index d384c17fe..a48a3a0eb 100644 --- a/tests/dGameTests/dGameMessagesTests/GameMessageTestUtils.h +++ b/tests/dGameTests/dGameMessagesTests/GameMessageTestUtils.h @@ -121,6 +121,35 @@ namespace GameMessageTestUtils { return msg; } + // The game message ID of every captured game message, in send order; other packets are skipped. + inline std::vector SentGameMessageIds(const std::vector& packets) { + std::vector ids; + for (const auto& packet : packets) { + RakNet::BitStream bitStream(const_cast(packet.bytes.data()), packet.bytes.size(), false); + LWOOBJID target{}; + MessageType::Game msgId{}; + if (GameMessages::NetGameMsg::ReadPacketHeader(bitStream, target, msgId)) ids.push_back(msgId); + } + return ids; + } + + // Every captured game message of type T, read back with its target, in send order. + template + std::vector SentGameMessages(const std::vector& packets) { + std::vector found; + for (const auto& packet : packets) { + RakNet::BitStream bitStream(const_cast(packet.bytes.data()), packet.bytes.size(), false); + T msg; + LWOOBJID target{}; + MessageType::Game msgId{}; + if (!GameMessages::NetGameMsg::ReadPacketHeader(bitStream, target, msgId) || msgId != msg.msgId) continue; + EXPECT_TRUE(msg.Deserialize(bitStream)); + msg.target = target; + found.push_back(msg); + } + return found; + } + // Checks that every strict prefix of msg's serialized payload fails to deserialize. template void ExpectTruncatedFails(const T& msg) {