From b81ffb2d287480776d7d4464eebe4a8fe9814112 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Thu, 1 Oct 2026 17:47:30 -0500 Subject: [PATCH] refactor(entity): components load their config and save after all exist, as the client LWOGameObject::MakeReadyForSerialization runs each component's LoadConfigData and then a character's LoadFromSaveData, in the order the components were made, once DoObjectComponentLoad has made them all; then their Startup. Entity::Initialize now does the same with a component list in creation order (the client's componentList); the walk's own save calls (mission, physics, destroyable, level, character, skill, ghost) and the destroyable's call into the buff go. A model's behaviors load in its LoadConfigData; controllable physics places a non-character at construction, as the client's physics takes the spawn position. A component added later still loads at once. Same bytes for every LOT. Co-Authored-By: Claude Opus 5.5 --- dGame/Entity.cpp | 30 +++++++++++-------- dGame/Entity.h | 15 ++++++++-- dGame/dComponents/DestroyableComponent.cpp | 5 ---- dGame/dComponents/ModelComponent.cpp | 1 + .../Physics/ControllablePhysicsComponent.cpp | 13 ++++---- 5 files changed, 39 insertions(+), 25 deletions(-) diff --git a/dGame/Entity.cpp b/dGame/Entity.cpp index e4a627501..1a45b73ee 100644 --- a/dGame/Entity.cpp +++ b/dGame/Entity.cpp @@ -274,7 +274,7 @@ Entity::ObjectLoadState Entity::DoObjectPreLoad() { // The player's own components (HandleLocalPlayer makes them for the local player, not from the registry) if (m_Character && m_Character->GetParentUser()) { - AddComponent(-1)->LoadFromSaveData(m_Character->GetXMLDoc()); + AddComponent(-1); } // markedAsPhantom doesn't count when the registry has phantom physics @@ -353,8 +353,7 @@ void Entity::DoObjectComponentLoad(ObjectLoadState& state) { if (controllablePhysicsComponentID > 0) state.modelPhysicsComponentID = controllablePhysicsComponentID; } else if (controllablePhysicsComponentID > 0) { // The component places it: where the level put an object, where a character's save and the zone put it - auto* controllablePhysics = AddComponent(controllablePhysicsComponentID); - if (m_Character) controllablePhysics->LoadFromSaveData(m_Character->GetXMLDoc()); + AddComponent(controllablePhysicsComponentID); } break; } @@ -413,8 +412,7 @@ void Entity::DoObjectComponentLoad(ObjectLoadState& state) { // A collectible's (right after it) and a smashable object's (after the walk) come later, as the client makes them if (destroyableComponentID > 0) { // The row, the level config and a character's save, in the client's order - DestroyableComponent* comp = AddComponent(destroyableComponentID); - if (m_Character) comp->LoadFromSaveData(m_Character->GetXMLDoc()); + AddComponent(destroyableComponentID); } break; } @@ -431,11 +429,11 @@ void Entity::DoObjectComponentLoad(ObjectLoadState& state) { AddComponent(-1); AddComponent(-1); AddComponent(-1); - AddComponent(-1)->LoadFromSaveData(m_Character->GetXMLDoc()); + AddComponent(-1); AddComponent(-1); auto& systemAddress = m_Character->GetParentUser() ? m_Character->GetParentUser()->GetSystemAddress() : UNASSIGNED_SYSTEM_ADDRESS; - AddComponent(-1, m_Character, systemAddress)->LoadFromSaveData(m_Character->GetXMLDoc()); + AddComponent(-1, m_Character, systemAddress); } break; @@ -510,8 +508,7 @@ void Entity::DoObjectComponentLoad(ObjectLoadState& state) { case eReplicaComponentType::SKILL: { const auto skillID = state.GetComponentID(eReplicaComponentType::SKILL, -1); if (skillID > -1 || m_Character) { - auto* const skillComponent = AddComponent(skillID); - if (m_Character) skillComponent->LoadFromSaveData(m_Character->GetXMLDoc()); + AddComponent(skillID); } break; } @@ -677,7 +674,7 @@ void Entity::DoObjectComponentLoad(ObjectLoadState& state) { else AddComponent(state.modelPhysicsComponentID == -1 ? 4247 : state.modelPhysicsComponentID); } if (modelType == 0 && !HasComponent(eReplicaComponentType::PATHFINDING)) AddComponent(-1); - AddComponent(modelID)->LoadBehaviors(); + AddComponent(modelID); if (!HasComponent(eReplicaComponentType::DESTROYABLE)) { AddComponent(-1, true); } @@ -995,7 +992,7 @@ void Entity::DoObjectComponentLoad(ObjectLoadState& state) { AddComponent(-1); } if (m_Character && HasComponent(eReplicaComponentType::CONTROLLABLE_PHYSICS)) { - AddComponent(-1)->LoadFromSaveData(m_Character->GetXMLDoc()); + AddComponent(-1); } // A mission offer's object gets a proximity monitor after the walk when it has none (the registry's mission offer; // a character's is DLU's own) @@ -1034,11 +1031,20 @@ void Entity::Initialize() { m_ParentEntity->AddChild(this); } + m_LoadingComponents = true; auto loadState = DoObjectPreLoad(); DoObjectComponentLoad(loadState); + m_LoadingComponents = false; + + // LWOGameObject::MakeReadyForSerialization (0x0101c230): every component exists; each loads the object's config, + // then a character's save, in the order they were made + for (auto* const component : m_ComponentList) { + component->LoadConfigData(); + if (m_Character) component->LoadFromSaveData(m_Character->GetXMLDoc()); + } // Every component exists: the client's Startup step - for (auto* const component : m_Components | std::views::values) if (component) component->Startup(); + for (auto* const component : m_ComponentList) component->Startup(); m_ComponentsStarted = true; // Hacky way to trigger these when the object has had a chance to get constructed diff --git a/dGame/Entity.h b/dGame/Entity.h index 6e312bd8e..67bae3a1a 100644 --- a/dGame/Entity.h +++ b/dGame/Entity.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include "NiPoint3.h" @@ -460,6 +461,10 @@ private: // Every component has had its Startup (the end of Initialize) bool m_ComponentsStarted = false; + // Initialize is making the components (DoObjectComponentLoad); their config and save load after + bool m_LoadingComponents = false; + // The components in the order they were made (the client's componentList) + std::vector m_ComponentList; std::vector m_Timers; std::vector m_PendingTimers; std::vector m_CallbackTimers; @@ -651,8 +656,14 @@ inline ComponentType* Entity::AddComponent(VaArgs... args) { // PrepareConfigData); one added after the object started up starts up at once componentToReturn->LoadDataFromTemplate(); componentToReturn->PrepareConfigData(); - componentToReturn->LoadConfigData(); - if (m_ComponentsStarted) componentToReturn->Startup(); + if (m_LoadingComponents) { + // While the object loads, its config, save and startup come after every component exists + // (LWOGameObject::MakeReadyForSerialization), in the order the components were made + if (std::ranges::find(m_ComponentList, componentToReturn) == m_ComponentList.end()) m_ComponentList.push_back(componentToReturn); + } else { + componentToReturn->LoadConfigData(); + if (m_ComponentsStarted) componentToReturn->Startup(); + } // Finally return the created or already existing component. // Because of the assert above, this should always be a ComponentType* but I need a way to guarantee the map cannot be modifed outside this function diff --git a/dGame/dComponents/DestroyableComponent.cpp b/dGame/dComponents/DestroyableComponent.cpp index bd76d8115..05a950595 100644 --- a/dGame/dComponents/DestroyableComponent.cpp +++ b/dGame/dComponents/DestroyableComponent.cpp @@ -274,11 +274,6 @@ void DestroyableComponent::LoadFromSaveData(const tinyxml2::XMLDocument& doc) { return; } - auto* buffComponent = m_Parent->GetComponent(); - - if (buffComponent != nullptr) { - buffComponent->LoadFromSaveData(doc); - } dest->QueryAttribute("hc", &m_iHealth); dest->QueryAttribute("hm", &m_fMaxHealth); diff --git a/dGame/dComponents/ModelComponent.cpp b/dGame/dComponents/ModelComponent.cpp index c39864dff..b92185c87 100644 --- a/dGame/dComponents/ModelComponent.cpp +++ b/dGame/dComponents/ModelComponent.cpp @@ -33,6 +33,7 @@ ModelComponent::ModelComponent(Entity* parent, const int32_t componentID) : Comp // The level config: the user model this is void ModelComponent::LoadConfigData() { m_userModelID = m_Parent->GetVarAs(u"userModelID"); + LoadBehaviors(); } bool ModelComponent::OnResetModelToDefaults(GameMessages::ResetModelToDefaults& reset) { diff --git a/dGame/dComponents/Physics/ControllablePhysicsComponent.cpp b/dGame/dComponents/Physics/ControllablePhysicsComponent.cpp index a3b8010e6..407e3afb0 100644 --- a/dGame/dComponents/Physics/ControllablePhysicsComponent.cpp +++ b/dGame/dComponents/Physics/ControllablePhysicsComponent.cpp @@ -51,18 +51,19 @@ ControllablePhysicsComponent::ControllablePhysicsComponent(Entity* entity, const m_ImmuneToStunMoveCount = 0; m_ImmuneToStunTurnCount = 0; m_ImmuneToStunUseItemCount = 0; + + // An object that isn't a character starts where the level put it (from the object's spawn info, as the client's + // physics does; a character's place comes with its save) + if (!m_Parent->GetCharacter()) { + SetPosition(m_Parent->GetDefaultPosition()); + SetRotation(m_Parent->GetDefaultRotation()); + } } // After the physics config: a character's body, in the physics component's group void ControllablePhysicsComponent::LoadConfigData() { PhysicsComponent::LoadConfigData(); - // An object that isn't a character starts where the level put it (a character's place comes with its save) - if (!m_Parent->GetCharacter()) { - SetPosition(m_Parent->GetDefaultPosition()); - SetRotation(m_Parent->GetDefaultRotation()); - } - if (m_Parent->GetLOT() != 1) // Other physics entities we care about will be added by BaseCombatAI return;