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 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-10-01 08:52:10 -05:00
parent e1c3c19c9a
commit 029e601e07
3 changed files with 119 additions and 102 deletions

View File

@@ -298,13 +298,7 @@ void Entity::Initialize() {
AddComponent<RenderComponent>(-1);
auto* destroyableComponent = AddComponent<DestroyableComponent>(-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<DestroyableComponent>(-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<bool>(u"markedAsPhantom");
@@ -429,85 +421,9 @@ void Entity::Initialize() {
bool isSmashable = GetVarAs<int32_t>(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<DestroyableComponent>(componentID);
auto* const destCompTable = CDClientManager::GetTable<CDDestructibleComponentTable>();
std::vector<CDDestructibleComponent> 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<CDCurrencyTableTable>();
const std::vector<CDCurrencyTable> 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<DestroyableComponent>(-1);
destroyable->SetMaxHealth(1.0f);
destroyable->SetHealth(1);
destroyable->SetArmor(1);
destroyable->SetFaction(-1, true);
destroyable->SetIsSmashable(true);
AddComponent<DestroyableComponent>(-1, true);
}
CDRebuildComponentTable* const rebCompTable = CDClientManager::GetTable<CDRebuildComponentTable>();
@@ -861,13 +772,7 @@ void Entity::Initialize() {
if (modelID > -1 && !GetComponent<PetComponent>()) {
AddComponent<ModelComponent>(modelID)->LoadBehaviors();
if (!HasComponent(eReplicaComponentType::DESTROYABLE)) {
auto* const destroyableComponent = AddComponent<DestroyableComponent>(-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<DestroyableComponent>(-1, true);
}
}
break;

View File

@@ -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<bool, const Entity*> DestroyableComponent::IsEnemyImplentation;
Implementation<bool, const Entity*> 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<CDDestructibleComponentTable>();
const auto componentID = m_ComponentID;
std::vector<CDDestructibleComponent> 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<CDComponentsRegistryTable>()->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<CDCurrencyTableTable>();
const std::vector<CDCurrencyTable> 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<int32_t>(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<CDComponentsRegistryTable>()->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() {
}

View File

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