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 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 03:09:52 -05:00
parent 44c6340294
commit 6a438ac419
6 changed files with 130 additions and 1 deletions

View File

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

View File

@@ -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 <memory>
#include <gtest/gtest.h>
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<Entity>(0x1000000000000001LL, playerInfo);
Game::entityManager->_addEntity(player.get()); // the quickbuild looks its builder up there
character = std::make_unique<Character>(1, nullptr);
player->SetCharacter(character.get());
character->SetEntity(player.get());
player->AddComponent<CharacterComponent>(-1, character.get(), ClientAddress());
auto buildInfo = info;
buildInfo.lot = 4717;
build = std::make_unique<Entity>(0x0102030405060708LL, buildInfo);
quickBuild = build->AddComponent<QuickBuildComponent>(-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<CapturedPacket> 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<Entity> player;
std::unique_ptr<Character> character;
std::unique_ptr<Entity> build;
QuickBuildComponent* quickBuild{};
};
TEST_F(QuickBuildCompleteTests, SuccessHasNoDuration) {
const auto sent = Build();
ASSERT_EQ(quickBuild->GetState(), eQuickBuildState::COMPLETED);
const auto enables = SentGameMessages<GameMessages::EnableRebuild>(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());
}

View File

@@ -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<MessageType::Game> SentGameMessageIds(const std::vector<CapturedPacket>& packets) {
std::vector<MessageType::Game> ids;
for (const auto& packet : packets) {
RakNet::BitStream bitStream(const_cast<unsigned char*>(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<typename T>
std::vector<T> SentGameMessages(const std::vector<CapturedPacket>& packets) {
std::vector<T> found;
for (const auto& packet : packets) {
RakNet::BitStream bitStream(const_cast<unsigned char*>(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<typename T>
void ExpectTruncatedFails(const T& msg) {