From 029e601e079b4933ee55946a91e3e9e347b4a0fd Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Thu, 1 Oct 2026 08:52:10 -0500 Subject: [PATCH] refactor(component): the destroyable loads itself in the client's phases The DestructibleComponent row moves from Entity::Initialize into LoadDataFromTemplate and the level keys into LoadConfigData; a character's save loads after both. Like the client (LWODestroyableComponent::LoadConfigData reads them for every destroyable), the destroyables added for models and quickbuilds now also read set_faction, is_smashable and the level loot matrix; their live defaults become their template. Nothing changes for objects without those keys. Co-Authored-By: Claude Opus 5.5 --- dGame/Entity.cpp | 105 +------------------- dGame/dComponents/DestroyableComponent.cpp | 107 ++++++++++++++++++++- dGame/dComponents/DestroyableComponent.h | 9 +- 3 files changed, 119 insertions(+), 102 deletions(-) diff --git a/dGame/Entity.cpp b/dGame/Entity.cpp index 25ec9bdf3..2748e96dc 100644 --- a/dGame/Entity.cpp +++ b/dGame/Entity.cpp @@ -298,13 +298,7 @@ void Entity::Initialize() { AddComponent(-1); - auto* destroyableComponent = AddComponent(-1); - destroyableComponent->SetHealth(1); - destroyableComponent->SetMaxHealth(1.0f); - // Live: armor 1 (max 0), as for every componentID -1 destroyable (LOT 14 and 8069 on a Raven Bluff property) - destroyableComponent->SetArmor(1); - destroyableComponent->SetFaction(-1, true); - destroyableComponent->SetIsSmashable(true); + AddComponent(-1, true); // We have all our components. return; } @@ -324,8 +318,6 @@ void Entity::Initialize() { const auto petComponentID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::PET); const auto collectibleComponentID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::COLLECTIBLE); const auto combatAiID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::BASE_COMBAT_AI); - // A race car's destroyable has 60 imagination - const bool isRacingCar = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::RACING_STATS) > 0; // If an entity is marked a phantom, simple physics is made into phantom phyics. const bool markedAsPhantom = GetVar(u"markedAsPhantom"); @@ -429,85 +421,9 @@ void Entity::Initialize() { bool isSmashable = GetVarAs(u"is_smashable") != 0; if (destroyableComponentID > 0 || collectibleComponentID > 0 || isSmashable) { const int32_t componentID = destroyableComponentID > 0 ? destroyableComponentID : -1; + // The row, the level config and a character's save, in the client's order DestroyableComponent* comp = AddComponent(componentID); - auto* const destCompTable = CDClientManager::GetTable(); - std::vector destCompData = destCompTable->Query([componentID](const CDDestructibleComponent& entry) { return (entry.id == componentID); }); - - if (m_Character) { - comp->LoadFromSaveData(m_Character->GetXMLDoc()); - } else { - // extraInfo overrides. Client ORs the database smashable and the luz smashable. - comp->SetIsSmashable(comp->GetIsSmashable() || isSmashable); - - if (componentID > 0) { - if (!destCompData.empty()) { - if (isRacingCar) { - destCompData[0].imagination = 60; - } - - comp->SetHealth(destCompData[0].life); - comp->SetImagination(destCompData[0].imagination); - comp->SetArmor(destCompData[0].armor); - - comp->SetMaxHealth(destCompData[0].life); - comp->SetMaxImagination(destCompData[0].imagination); - comp->SetMaxArmor(destCompData[0].armor); - comp->SetDeathBehavior(destCompData[0].death_behavior); - - comp->SetIsSmashable(comp->GetIsSmashable() || destCompData[0].isSmashable); - comp->SetIsNPC(destCompData[0].isnpc); - comp->SetAttackPriority(destCompData[0].attack_priority); - - comp->SetLootMatrixID(destCompData[0].LootMatrixIndex); - comp->SetCurrencyIndex(destCompData[0].CurrencyIndex); - Loot::CacheMatrix(destCompData[0].LootMatrixIndex); - - // Now get currency information - const uint32_t npcMinLevel = destCompData[0].level; - const uint32_t currencyIndex = destCompData[0].CurrencyIndex; - - CDCurrencyTableTable* const currencyTable = CDClientManager::GetTable(); - const std::vector currencyValues = currencyTable->Query([currencyIndex, npcMinLevel](const CDCurrencyTable& entry) { return (entry.currencyIndex == currencyIndex && entry.npcminlevel == npcMinLevel); }); - - if (!currencyValues.empty()) { - // Set the coins - comp->SetMinCoins(currencyValues[0].minvalue); - comp->SetMaxCoins(currencyValues[0].maxvalue); - } - } - } else if (isSmashable) { - // A smashable object without a DestructibleComponent row - comp->SetHealth(1); - comp->SetArmor(0); - - comp->SetMaxHealth(1); - comp->SetMaxArmor(0); - - comp->SetIsSmashable(true); - comp->AddFaction(-1); - comp->AddFaction(6); //Smashables - - // A race car has 60 imagination, other entities defaults to 0. - comp->SetImagination(isRacingCar ? 60 : 0); - comp->SetMaxImagination(isRacingCar ? 60 : 0); - } - } - - 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); - if (levelFactions && !levelFactions->empty()) { - comp->SetFaction(levelFactions->front(), true); - for (const auto faction : *levelFactions | std::views::drop(1)) comp->AddFaction(faction, true); - } - - // Level files can give a smashable its own loot matrix in place of the template's - const auto levelLootMatrix = DestroyableComponent::GetLevelLootMatrix(*this); - if (!m_Character && levelLootMatrix) { - comp->SetLootMatrixID(levelLootMatrix.value()); - Loot::CacheMatrix(levelLootMatrix.value()); - } + if (m_Character) comp->LoadFromSaveData(m_Character->GetXMLDoc()); } break; } @@ -709,12 +625,7 @@ void Entity::Initialize() { // A quick build without a destroyable gets one (componentID -1) and is smashable, as the client makes it // (DoObjectComponentLoad); live: health 1 / 1.0, armor 1 / 0, faction -1, smashable (AG LOT 7804) if (!HasComponent(eReplicaComponentType::DESTROYABLE)) { - auto* const destroyable = AddComponent(-1); - destroyable->SetMaxHealth(1.0f); - destroyable->SetHealth(1); - destroyable->SetArmor(1); - destroyable->SetFaction(-1, true); - destroyable->SetIsSmashable(true); + AddComponent(-1, true); } CDRebuildComponentTable* const rebCompTable = CDClientManager::GetTable(); @@ -861,13 +772,7 @@ void Entity::Initialize() { if (modelID > -1 && !GetComponent()) { AddComponent(modelID)->LoadBehaviors(); if (!HasComponent(eReplicaComponentType::DESTROYABLE)) { - auto* const destroyableComponent = AddComponent(-1); - destroyableComponent->SetHealth(1); - destroyableComponent->SetMaxHealth(1.0f); - // Live: armor 1 (max 0), as for every componentID -1 destroyable (LOT 8069 on a Raven Bluff property) - destroyableComponent->SetArmor(1); - destroyableComponent->SetFaction(-1, true); - destroyableComponent->SetIsSmashable(true); + AddComponent(-1, true); } } break; diff --git a/dGame/dComponents/DestroyableComponent.cpp b/dGame/dComponents/DestroyableComponent.cpp index 84f97b19f..bd76d8115 100644 --- a/dGame/dComponents/DestroyableComponent.cpp +++ b/dGame/dComponents/DestroyableComponent.cpp @@ -18,6 +18,7 @@ #include "User.h" #include "CDClientManager.h" #include "CDDestructibleComponentTable.h" +#include "CDCurrencyTableTable.h" #include "EntityManager.h" #include "QuickBuildComponent.h" #include "CppScripts.h" @@ -53,7 +54,7 @@ Implementation DestroyableComponent::IsEnemyImplentation; Implementation DestroyableComponent::IsFriendImplentation; -DestroyableComponent::DestroyableComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) { +DestroyableComponent::DestroyableComponent(Entity* parent, const int32_t componentID, const bool smashableDefaults) : Component(parent, componentID), m_SmashableDefaults{ smashableDefaults } { m_iArmor = 0; m_fMaxArmor = 0.0f; m_iImagination = 0; @@ -96,6 +97,110 @@ DestroyableComponent::DestroyableComponent(Entity* parent, const int32_t compone RegisterMsg(&DestroyableComponent::OnIsDead); } +// The DestructibleComponent row (a character's stats come from its save after this): stats, loot and coins, then the +// row's factionList. A destroyable the loader adds to a model or quickbuild has no row and live's defaults instead. +void DestroyableComponent::LoadDataFromTemplate() { + if (m_SmashableDefaults) { + // Live: health 1 / 1.0, armor 1 / 0, faction -1, smashable (AG LOT 7804, LOTs 14 and 8069 on a property) + SetMaxHealth(1.0f); + SetHealth(1); + SetArmor(1); + SetFaction(-1, true); + SetIsSmashable(true); + return; + } + + if (m_ComponentID <= 0) return; + + auto* const destCompTable = CDClientManager::GetTable(); + const auto componentID = m_ComponentID; + std::vector destCompData = destCompTable->Query([componentID](const CDDestructibleComponent& entry) { return (entry.id == componentID); }); + if (destCompData.empty()) return; + + if (!m_Parent->GetCharacter()) { + // A race car's destroyable has 60 imagination + const bool isRacingCar = CDClientManager::GetTable()->GetByIDAndType(m_Parent->GetLOT(), eReplicaComponentType::RACING_STATS) > 0; + if (isRacingCar) { + destCompData[0].imagination = 60; + } + + SetHealth(destCompData[0].life); + SetImagination(destCompData[0].imagination); + SetArmor(destCompData[0].armor); + + SetMaxHealth(destCompData[0].life); + SetMaxImagination(destCompData[0].imagination); + SetMaxArmor(destCompData[0].armor); + SetDeathBehavior(destCompData[0].death_behavior); + + SetIsSmashable(GetIsSmashable() || destCompData[0].isSmashable); + SetIsNPC(destCompData[0].isnpc); + SetAttackPriority(destCompData[0].attack_priority); + + SetLootMatrixID(destCompData[0].LootMatrixIndex); + SetCurrencyIndex(destCompData[0].CurrencyIndex); + Loot::CacheMatrix(destCompData[0].LootMatrixIndex); + + // Now get currency information + const uint32_t npcMinLevel = destCompData[0].level; + const uint32_t currencyIndex = destCompData[0].CurrencyIndex; + + CDCurrencyTableTable* const currencyTable = CDClientManager::GetTable(); + const std::vector currencyValues = currencyTable->Query([currencyIndex, npcMinLevel](const CDCurrencyTable& entry) { return (entry.currencyIndex == currencyIndex && entry.npcminlevel == npcMinLevel); }); + + if (!currencyValues.empty()) { + // Set the coins + SetMinCoins(currencyValues[0].minvalue); + SetMaxCoins(currencyValues[0].maxvalue); + } + } + + AddTemplateFactions(destCompData[0].factionList); +} + +// The level config (LWODestroyableComponent::LoadConfigData 0x00c44cb0 reads it for every destroyable): is_smashable, +// set_faction / override_faction and the smashable loot matrix +void DestroyableComponent::LoadConfigData() { + const bool isSmashable = m_Parent->GetVarAs(u"is_smashable") != 0; + const bool isCharacter = m_Parent->GetCharacter() != nullptr; + if (!isCharacter) { + // The client ORs the database smashable and the level's + SetIsSmashable(GetIsSmashable() || isSmashable); + + if (!m_SmashableDefaults && m_ComponentID <= 0 && isSmashable) { + // A smashable object without a DestructibleComponent row + SetHealth(1); + SetArmor(0); + + SetMaxHealth(1); + SetMaxArmor(0); + + SetIsSmashable(true); + AddFaction(-1); + AddFaction(6); //Smashables + + // A race car has 60 imagination, other entities defaults to 0. + const bool isRacingCar = CDClientManager::GetTable()->GetByIDAndType(m_Parent->GetLOT(), eReplicaComponentType::RACING_STATS) > 0; + SetImagination(isRacingCar ? 60 : 0); + SetMaxImagination(isRacingCar ? 60 : 0); + } + } + + // Level files can replace the factions with set_faction, unless override_faction is 0 + const auto levelFactions = GetLevelFactions(*m_Parent); + if (levelFactions && !levelFactions->empty()) { + SetFaction(levelFactions->front(), true); + for (const auto faction : *levelFactions | std::views::drop(1)) AddFaction(faction, true); + } + + // Level files can give a smashable its own loot matrix in place of the template's + const auto levelLootMatrix = GetLevelLootMatrix(*m_Parent); + if (!isCharacter && levelLootMatrix) { + SetLootMatrixID(levelLootMatrix.value()); + Loot::CacheMatrix(levelLootMatrix.value()); + } +} + DestroyableComponent::~DestroyableComponent() { } diff --git a/dGame/dComponents/DestroyableComponent.h b/dGame/dComponents/DestroyableComponent.h index 2e112a3c5..69dffb868 100644 --- a/dGame/dComponents/DestroyableComponent.h +++ b/dGame/dComponents/DestroyableComponent.h @@ -44,9 +44,13 @@ class DestroyableComponent final : public Component { public: static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::DESTROYABLE; - DestroyableComponent(Entity* parentEntity, const int32_t componentID); + // smashableDefaults: a destroyable the loader adds to a model or quickbuild, with live's defaults and no row + DestroyableComponent(Entity* parentEntity, const int32_t componentID, const bool smashableDefaults = false); ~DestroyableComponent() override; + void LoadDataFromTemplate() override; + void LoadConfigData() override; + void Run(float deltaTime) override; void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) override; void LoadFromSaveData(const tinyxml2::XMLDocument& doc) override; @@ -700,6 +704,9 @@ private: * Damage immunity cooldown timer. Set to a value that then counts down to create a damage cooldown for players */ float m_DamageCooldownTimer; + + // Made by the loader for a model or quickbuild: live's smashable defaults in place of a row + bool m_SmashableDefaults{ false }; }; #endif // DESTROYABLECOMPONENT_H