From 545db47a40f8cf698f5c9a76bf87e58b124aea49 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 05:25:40 -0500 Subject: [PATCH] fix(destroyable): take template factions from factionList, keeping -1 The client reads an object's factions only from the DestructibleComponent factionList (LWODestroyableComponent::LoadDataFromTemplate 0x00c9f900: one atol per comma-separated token, -1 kept; the faction column is not read), and live replicated that list: [-1] on 12,829 constructions (vendors, quickbuilds, bouncers). DLU added the faction column and dropped -1, so those objects were sent with no factions, and the four rows with faction 6 but factionList "-1" were sent as [6]. -1 has no Factions row, so it adds no friends or enemies. Test: ReplicaConstructionTest.TemplateFactionMinusOneIsReplicated. Check in game: vendors, quickbuilds, bouncers and NPCs still can't be attacked; enemies still fight players and pets; smashables still smash. Co-Authored-By: Claude Opus 5.5 --- dGame/Entity.cpp | 13 +--------- dGame/dComponents/DestroyableComponent.cpp | 12 ++++++++++ dGame/dComponents/DestroyableComponent.h | 6 +++++ .../ReplicaConstructionTests.cpp | 24 +++++++++++++++++++ 4 files changed, 43 insertions(+), 12 deletions(-) diff --git a/dGame/Entity.cpp b/dGame/Entity.cpp index 0faccd1d6..eabd86caa 100644 --- a/dGame/Entity.cpp +++ b/dGame/Entity.cpp @@ -475,18 +475,7 @@ void Entity::Initialize() { } } - if (!destCompData.empty()) { - comp->AddFaction(destCompData[0].faction); - std::stringstream ss(destCompData[0].factionList); - std::string token; - - while (std::getline(ss, token, ',')) { - const auto tokenInt = GeneralUtils::TryParse(token); - if (tokenInt == destCompData[0].faction) continue; - - if (tokenInt) comp->AddFaction(tokenInt.value()); - } - } + if (!destCompData.empty()) comp->AddTemplateFactions(destCompData[0].factionList); // Level files can replace the factions with set_faction, unless override_faction is 0 const auto levelFactions = DestroyableComponent::GetLevelFactions(*this); diff --git a/dGame/dComponents/DestroyableComponent.cpp b/dGame/dComponents/DestroyableComponent.cpp index 1a1e36d04..7b593b46b 100644 --- a/dGame/dComponents/DestroyableComponent.cpp +++ b/dGame/dComponents/DestroyableComponent.cpp @@ -357,6 +357,18 @@ void DestroyableComponent::SetIsShielded(bool value) { m_IsShielded = value; } +void DestroyableComponent::AddTemplateFactions(const std::string& factionList) { + // The client (LWODestroyableComponent::LoadDataFromTemplate) and live took the factions from the row's factionList, + // -1 included (live replicated it as [-1]); the faction column is not used. + std::stringstream ss(factionList); + std::string token; + + while (std::getline(ss, token, ',')) { + const auto faction = GeneralUtils::TryParse(token); + if (faction) AddFaction(faction.value(), faction.value() == -1); + } +} + void DestroyableComponent::AddFaction(const int32_t factionID, const bool ignoreChecks) { // Ignore factionID -1 if (factionID == -1 && !ignoreChecks) return; diff --git a/dGame/dComponents/DestroyableComponent.h b/dGame/dComponents/DestroyableComponent.h index 1104ce344..7fcd439c0 100644 --- a/dGame/dComponents/DestroyableComponent.h +++ b/dGame/dComponents/DestroyableComponent.h @@ -213,6 +213,12 @@ public: */ bool GetIsShielded() const { return m_IsShielded; } + /** + * Adds the factions in the object's DestructibleComponent factionList (comma separated), -1 included, as the client + * reads them + */ + void AddTemplateFactions(const std::string& factionList); + /** * Adds a faction to the faction list of this entity, potentially making more factions friendly. Fetches the info * from the CDClient. diff --git a/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp b/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp index 46c0d215e..96a650939 100644 --- a/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp +++ b/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp @@ -218,6 +218,30 @@ TEST_F(ReplicaConstructionTest, SimplePhysicsConstructionLikeLive) { info.settings.values.clear(); } +// Live replicated the DestructibleComponent factionList, so a list of -1 as [-1] (12,829 constructions: vendors, +// quickbuilds, bouncers; DLU dropped it and sent []), and a row with no faction but factionList 6 as [6]. +TEST_F(ReplicaConstructionTest, TemplateFactionMinusOneIsReplicated) { + CDClientDatabase::Connect(":memory:"); + CDClientDatabase::ExecuteDML("CREATE TABLE Factions (faction INTEGER, enemyList TEXT);"); + CDClientDatabase::ExecuteDML("INSERT INTO Factions VALUES (4, '1'), (6, '');"); + + Entity vendor(15, info); + auto* const destroyable = vendor.AddComponent(-1); + destroyable->AddTemplateFactions("-1"); + EXPECT_EQ(destroyable->GetFactionIDs(), std::vector{ -1 }); + + Entity enemy(16, info); + auto* const enemyDestroyable = enemy.AddComponent(-1); + enemyDestroyable->AddTemplateFactions("4"); + EXPECT_EQ(enemyDestroyable->GetFactionIDs(), std::vector{ 4 }); + EXPECT_EQ(enemyDestroyable->GetEnemyFactionsIDs(), std::vector{ 1 }); + + Entity smashable(17, info); + auto* const smashableDestroyable = smashable.AddComponent(-1); + smashableDestroyable->AddTemplateFactions("6"); + EXPECT_EQ(smashableDestroyable->GetFactionIDs(), std::vector{ 6 }); +} + class InventoryConstructionTest : public GameDependenciesTest { protected: // The items of the live sample below (CDClient 1.10.64 values)