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 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-10-01 17:47:30 -05:00
parent 818d584c76
commit b81ffb2d28
5 changed files with 39 additions and 25 deletions

View File

@@ -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<MissionComponent>(-1)->LoadFromSaveData(m_Character->GetXMLDoc());
AddComponent<MissionComponent>(-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<ControllablePhysicsComponent>(controllablePhysicsComponentID);
if (m_Character) controllablePhysics->LoadFromSaveData(m_Character->GetXMLDoc());
AddComponent<ControllablePhysicsComponent>(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<DestroyableComponent>(destroyableComponentID);
if (m_Character) comp->LoadFromSaveData(m_Character->GetXMLDoc());
AddComponent<DestroyableComponent>(destroyableComponentID);
}
break;
}
@@ -431,11 +429,11 @@ void Entity::DoObjectComponentLoad(ObjectLoadState& state) {
AddComponent<PossessorComponent>(-1);
AddComponent<MountControlComponent>(-1);
AddComponent<PetCreatorComponent>(-1);
AddComponent<LevelProgressionComponent>(-1)->LoadFromSaveData(m_Character->GetXMLDoc());
AddComponent<LevelProgressionComponent>(-1);
AddComponent<PlayerForcedMovementComponent>(-1);
auto& systemAddress = m_Character->GetParentUser() ? m_Character->GetParentUser()->GetSystemAddress() : UNASSIGNED_SYSTEM_ADDRESS;
AddComponent<CharacterComponent>(-1, m_Character, systemAddress)->LoadFromSaveData(m_Character->GetXMLDoc());
AddComponent<CharacterComponent>(-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<SkillComponent>(skillID);
if (m_Character) skillComponent->LoadFromSaveData(m_Character->GetXMLDoc());
AddComponent<SkillComponent>(skillID);
}
break;
}
@@ -677,7 +674,7 @@ void Entity::DoObjectComponentLoad(ObjectLoadState& state) {
else AddComponent<SimplePhysicsComponent>(state.modelPhysicsComponentID == -1 ? 4247 : state.modelPhysicsComponentID);
}
if (modelType == 0 && !HasComponent(eReplicaComponentType::PATHFINDING)) AddComponent<PathfindingComponent>(-1);
AddComponent<ModelComponent>(modelID)->LoadBehaviors();
AddComponent<ModelComponent>(modelID);
if (!HasComponent(eReplicaComponentType::DESTROYABLE)) {
AddComponent<DestroyableComponent>(-1, true);
}
@@ -995,7 +992,7 @@ void Entity::DoObjectComponentLoad(ObjectLoadState& state) {
AddComponent<DestroyableComponent>(-1);
}
if (m_Character && HasComponent(eReplicaComponentType::CONTROLLABLE_PHYSICS)) {
AddComponent<GhostComponent>(-1)->LoadFromSaveData(m_Character->GetXMLDoc());
AddComponent<GhostComponent>(-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

View File

@@ -7,6 +7,7 @@
#include <typeinfo>
#include <type_traits>
#include <unordered_map>
#include <algorithm>
#include <vector>
#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<Component*> m_ComponentList;
std::vector<EntityTimer> m_Timers;
std::vector<EntityTimer> m_PendingTimers;
std::vector<EntityCallbackTimer> 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

View File

@@ -274,11 +274,6 @@ void DestroyableComponent::LoadFromSaveData(const tinyxml2::XMLDocument& doc) {
return;
}
auto* buffComponent = m_Parent->GetComponent<BuffComponent>();
if (buffComponent != nullptr) {
buffComponent->LoadFromSaveData(doc);
}
dest->QueryAttribute("hc", &m_iHealth);
dest->QueryAttribute("hm", &m_fMaxHealth);

View File

@@ -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<LWOOBJID>(u"userModelID");
LoadBehaviors();
}
bool ModelComponent::OnResetModelToDefaults(GameMessages::ResetModelToDefaults& reset) {

View File

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