Files
DarkflameServer/dGame/Entity.h
Aaron Kimbrell 477dd9f3e3 refactor(entity): load an object in the client's two steps
Entity::Initialize now runs DoObjectPreLoad (what the components will be:
the player's own components, markedAsPhantom, model behaviors, the
component whitelist) and DoObjectComponentLoad (the walk in the client's
order and what the client adds after it), as ObjectLoader2 does, with an
ObjectLoadState that answers the registry through the whitelist. Same
components and bytes for every LOT.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 16:52:15 -05:00

670 lines
22 KiB
C++

#pragma once
#include <chrono>
#include <map>
#include <functional>
#include <tuple>
#include <typeinfo>
#include <type_traits>
#include <unordered_map>
#include <vector>
#include "NiPoint3.h"
#include "NiQuaternion.h"
#include "LDFFormat.h"
#include "eKillType.h"
#include "Observable.h"
#include "Profiler.h"
namespace GameMessages {
struct GameMsg;
template<typename Msg> struct NetGameMsgEvent;
struct ActivityNotify;
struct ShootingGalleryFire;
struct ChildLoaded;
struct PlayerResurrectionFinished;
struct RequestServerObjectInfo;
struct DropClientLoot;
struct GetFlag;
struct GetFactionTokenType;
struct PickupItem;
struct RequestUse;
struct ChildRemoved;
};
namespace MessageType {
enum class Game : uint16_t;
}
namespace Loot {
class Info;
};
namespace tinyxml2 {
class XMLDocument;
};
class Player;
class User;
class Spawner;
class ScriptComponent;
class dpEntity;
class EntityTimer;
class Component;
class Item;
class Character;
class EntityCallbackTimer;
class PositionUpdate;
struct EntityInfo;
enum class eTriggerEventType;
enum class eGameMasterLevel : uint8_t;
enum class eReplicaComponentType : uint32_t;
enum class eReplicaPacketType : uint8_t;
enum class eCinematicEvent : uint32_t;
namespace CppScripts {
class Script;
};
/**
* An entity in the world. Has multiple components.
*/
class Entity {
public:
Entity(const LWOOBJID& objectID, const EntityInfo& info, User* parentUser = nullptr, Entity* parentEntity = nullptr);
~Entity();
void Initialize();
bool operator==(const Entity& other) const;
bool operator!=(const Entity& other) const;
/**
* Getters
*/
const LWOOBJID& GetObjectID() const { return m_ObjectID; }
const LOT GetLOT() const { return m_TemplateID; }
Character* GetCharacter() const { return m_Character; }
eGameMasterLevel GetGMLevel() const { return m_GMLevel; }
uint8_t GetCollectibleID() const;
Entity* GetParentEntity() const { return m_ParentEntity; }
std::vector<std::string>& GetGroups() { return m_Groups; };
Spawner* GetSpawner() const { return m_Spawner; }
LWOOBJID GetSpawnerID() const { return m_SpawnerID; }
const LwoNameValue& GetSettings() const { return m_Settings; }
const LwoNameValue& GetNetworkSettings() const { return m_NetworkSettings; }
bool GetIsDead() const;
bool GetPlayerReadyForUpdates() const { return m_PlayerIsReadyForUpdates; }
bool GetIsGhostingCandidate() const;
void SetIsGhostingCandidate(bool value) { m_IsGhostingCandidate = value; };
int8_t GetObservers() const;
uint16_t GetNetworkId() const;
// Cannot return nullptr.
Entity* GetOwner() const;
const NiPoint3& GetDefaultPosition() const;
const NiQuaternion& GetDefaultRotation() const;
float GetDefaultScale() const;
NiPoint3 GetPosition() const;
const NiQuaternion& GetRotation() const;
const SystemAddress& GetSystemAddress() const;
// Returns the collision group for this entity.
// Because the collision group is stored on a base component, this will look for a physics component
// then return the collision group from that.
int32_t GetCollisionGroup() const;
const NiPoint3& GetVelocity() const;
/**
* Setters
*/
void SetCharacter(Character* value) { m_Character = value; }
void SetGMLevel(eGameMasterLevel value);
void SetOwnerOverride(LWOOBJID value);
void SetPlayerReadyForUpdates() { m_PlayerIsReadyForUpdates = true; }
void SetObservers(int8_t value);
void SetNetworkId(uint16_t id);
void SetPosition(const NiPoint3& position);
void SetRotation(const NiQuaternion& rotation);
void SetRespawnPos(const NiPoint3& position) const;
void SetRespawnRot(const NiQuaternion& rotation) const;
void SetVelocity(const NiPoint3& velocity);
/**
* Component management
*/
Component* GetComponent(eReplicaComponentType componentID) const;
template<typename T>
T* GetComponent() const;
template<typename... T>
auto GetComponents() const;
template<typename... T>
auto GetComponentsMut() const;
template<typename T>
bool TryGetComponent(eReplicaComponentType componentId, T*& component) const;
bool HasComponent(eReplicaComponentType componentId) const;
void AddComponent(eReplicaComponentType componentId, Component* component);
bool MsgRequestServerObjectInfo(GameMessages::NetGameMsgEvent<GameMessages::RequestServerObjectInfo>& event);
bool MsgDropClientLoot(GameMessages::NetGameMsgEvent<GameMessages::DropClientLoot>& event);
bool MsgGetFlag(GameMessages::GetFlag& msg);
bool MsgGetFactionTokenType(GameMessages::GetFactionTokenType& msg);
bool MsgPickupItem(GameMessages::NetGameMsgEvent<GameMessages::PickupItem>& event);
bool MsgChildRemoved(GameMessages::ChildRemoved& msg);
// This is expceted to never return nullptr, an assert checks this.
CppScripts::Script* const GetScript() const;
void Subscribe(LWOOBJID scriptObjId, CppScripts::Script* scriptToAdd, const std::string& notificationName);
void Unsubscribe(LWOOBJID scriptObjId, const std::string& notificationName);
void SetProximityRadius(float proxRadius, std::string name, uint32_t collisionGroup = 0);
void SetProximityRadius(dpEntity* entity, std::string name);
void AddChild(Entity* child);
void RemoveChild(Entity* child);
void RemoveParent();
// Adds a timer to start next frame with the given name and time.
void AddTimer(const std::string& name, float time);
void AddCallbackTimer(float time, const std::function<void()> callback);
bool HasTimer(const std::string& name);
void CancelCallbackTimers();
void CancelAllTimers();
void CancelTimer(const std::string& name);
void AddToGroup(const std::string& group);
void RemoveFromGroup(const std::string& group);
bool IsPlayer() const;
std::unordered_map<eReplicaComponentType, Component*>& GetComponents() { return m_Components; } // TODO: Remove
void WriteBaseReplicaData(RakNet::BitStream& outBitStream, eReplicaPacketType packetType);
// Milliseconds since this object was created on this server (the construction's time_since_created_on_server).
uint32_t GetTimeSinceCreatedMs() const {
return static_cast<uint32_t>(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - m_CreationTime).count());
}
void WriteComponents(RakNet::BitStream& outBitStream, eReplicaPacketType packetType) const;
void UpdateXMLDoc(tinyxml2::XMLDocument& doc);
void Update(float deltaTime);
// A player starts or stops seeing this object: its components' OnEnterScope / OnLeaveScope
void OnEnterScope(Entity& player);
void OnLeaveScope(Entity& player);
// Events
void OnCollisionProximity(LWOOBJID otherEntity, const std::string& proxName, const std::string& status);
void OnCollisionPhantom(LWOOBJID otherEntity);
void OnCollisionLeavePhantom(LWOOBJID otherEntity);
void OnFireEventServerSide(Entity* sender, std::string args, int32_t param1 = -1, int32_t param2 = -1, int32_t param3 = -1);
void OnActivityStateChangeRequest(const LWOOBJID senderID, const int32_t value1, const int32_t value2,
const std::u16string& stringValue);
void OnCinematicUpdate(Entity* self, Entity* sender, eCinematicEvent event, const std::u16string& pathName,
float_t pathTime, float_t totalTime, int32_t waypoint);
void NotifyObject(Entity* sender, const std::string& name, int32_t param1 = 0, int32_t param2 = 0);
void OnEmoteReceived(int32_t emote, Entity* target);
// The client's RequestUse of this object by user: the INTERACT trigger and the script's OnUse (not for the mission
// choice of a multi-interact use), then the components' RequestUse handlers.
void RequestUse(const GameMessages::RequestUse& msg, Entity& user);
// originator uses this object as if their client had sent RequestUse
void OnUse(Entity* originator);
void OnHitOrHealResult(Entity* attacker, int32_t damage);
void OnHit(Entity* attacker);
void OnZonePropertyEditBegin();
void OnZonePropertyEditEnd();
void OnZonePropertyModelEquipped();
void OnZonePropertyModelPlaced(Entity* player);
void OnZonePropertyModelPickedUp(Entity* player);
void OnZonePropertyModelRemoved(Entity* player);
void OnZonePropertyModelRemovedWhileEquipped(Entity* player);
void OnZonePropertyModelRotated(Entity* player);
void OnActivityNotify(GameMessages::ActivityNotify& notify);
void OnShootingGalleryFire(GameMessages::ShootingGalleryFire& notify);
void OnChildLoaded(GameMessages::ChildLoaded& childLoaded);
void NotifyPlayerResurrectionFinished(GameMessages::PlayerResurrectionFinished& msg);
void OnMessageBoxResponse(Entity* sender, int32_t button, const std::u16string& identifier, const std::u16string& userData);
void OnChoiceBoxResponse(Entity* sender, int32_t button, const std::u16string& buttonIdentifier, const std::u16string& identifier);
void RequestActivityExit(Entity* sender, LWOOBJID player, bool canceled);
void Smash(const LWOOBJID source = LWOOBJID_EMPTY, const eKillType killType = eKillType::VIOLENT, const std::u16string& deathType = u"");
void Kill(Entity* murderer = nullptr, const eKillType killType = eKillType::SILENT);
void AddQuickBuildCompleteCallback(const std::function<void(Entity* user)>& callback) const;
void AddCollisionPhantomCallback(const std::function<void(Entity* target)>& callback);
void AddDieCallback(const std::function<void()>& callback);
void Resurrect();
void AddLootItem(const Loot::Info& info) const;
void PickupItem(const LWOOBJID& objectID) const;
bool PickupCoins(uint64_t count) const;
void RegisterCoinDrop(uint64_t count) const;
void ScheduleKillAfterUpdate(Entity* murderer = nullptr);
void TriggerEvent(eTriggerEventType event, Entity* optionalTarget = nullptr) const;
void ScheduleDestructionAfterUpdate() { m_ShouldDestroyAfterUpdate = true; }
const NiPoint3& GetRespawnPosition() const;
const NiQuaternion& GetRespawnRotation() const;
void Sleep() const;
void Wake() const;
bool IsSleeping() const;
/*
* Utility
*/
/**
* Retroactively corrects the model vault size due to incorrect initialization in a previous patch.
*
*/
void RetroactiveVaultSize() const;
bool GetBoolean(const std::u16string& name) const;
int32_t GetI32(const std::u16string& name) const;
int64_t GetI64(const std::u16string& name) const;
void SetBoolean(const std::u16string& name, bool value);
void SetI32(const std::u16string& name, int32_t value);
void SetI64(const std::u16string& name, int64_t value);
bool HasVar(const std::u16string& name) const;
template<typename T>
const T& GetVar(const std::u16string& name) const;
template<typename T>
void SetVar(const std::u16string& name, T value);
void SendNetworkVar(const std::string& data, const SystemAddress& sysAddr);
template<typename T>
void SetNetworkVar(const std::u16string& name, T value, const SystemAddress& sysAddr = UNASSIGNED_SYSTEM_ADDRESS);
template<typename T>
void SetNetworkVar(const std::u16string& name, std::vector<T> value, const SystemAddress& sysAddr = UNASSIGNED_SYSTEM_ADDRESS);
template<typename T>
void SetNetworkVar(const std::string& name, T value, const SystemAddress& sysAddr = UNASSIGNED_SYSTEM_ADDRESS);
template<typename T>
LwoNameValue::ValueType::iterator InsertNetworkVar(const std::u16string& name, T value);
template<typename T>
T GetNetworkVar(const std::u16string& name);
/**
* Get the LDF value and cast it as T.
*/
template<typename T>
T GetVarAs(const std::u16string& name) const;
template<typename ComponentType, typename... VaArgs>
ComponentType* AddComponent(VaArgs... args);
/**
* Get the LDF value and convert it to a string.
*/
std::string GetVarAsString(const std::u16string& name) const;
// Whether the construction sends this level config key to the client (a model's settings, or syncLDF)
bool IsVarSentToClient(const std::u16string& name) const;
/*
* Collision
*/
std::vector<LWOOBJID> GetTargetsInPhantom();
Entity* GetScheduledKiller() { return m_ScheduleKiller; }
void ProcessPositionUpdate(PositionUpdate& update);
// Scale will only be communicated to the client when the construction packet is sent
void SetScale(const float scale) { m_Scale = scale; };
// The zone scene the object was placed in (EntityInfo::scene), -1 when it came from none
int32_t GetScene() const { return m_Scene; }
void RegisterMsg(const MessageType::Game msgId, std::function<bool(GameMessages::GameMsg&)> handler);
bool HandleMsg(GameMessages::GameMsg& msg) const;
// Provided a function that has a derived GameMessage as its only argument and returns a boolean,
// this will register it as a handler for that message type. Casting is done automatically to the type
// of the message in the first argument. This object is expected to exist as long as the handler can be called.
template<typename DerivedGameMsg>
inline void RegisterMsg(bool (Entity::* handler)(DerivedGameMsg&)) {
static_assert(std::is_base_of_v<GameMessages::GameMsg, DerivedGameMsg>, "DerivedGameMsg must inherit from GameMsg");
const auto boundFunction = std::bind(handler, this, std::placeholders::_1);
// This is the actual function that will be registered, which casts the base GameMsg to the derived type
const auto castWrapper = [boundFunction](GameMessages::GameMsg& msg) {
return boundFunction(static_cast<DerivedGameMsg&>(msg));
};
DerivedGameMsg msg;
RegisterMsg(msg.msgId, castWrapper);
}
/**
* @brief The observable for player entity position updates.
*/
static Observable<Entity*, const PositionUpdate&> OnPlayerPositionUpdate;
private:
/**
* The client's ObjectLoadState (ObjectLoader2): the object's components as its registry gives them, after the
* pre-load rules and the component whitelist, and what the component walk has found so far
*/
struct ObjectLoadState {
LOT lot{};
// The config's componentWhitelist (ApplyComponentWhitelist): only these registry components, when set
const std::initializer_list<eReplicaComponentType>* whitelist = nullptr;
bool hasModelBehaviors = false;
bool markedAsPhantom = false;
// A model with behaviors' physics id, which the physics cases leave to the model case
int32_t modelPhysicsComponentID = -1;
// The registry's id for type, defaultValue when it has none or the whitelist drops it
int32_t GetComponentID(eReplicaComponentType type, int32_t defaultValue = 0) const;
};
// ObjectLoader2::DoObjectPreLoad: what the components will be, before any is made
ObjectLoadState DoObjectPreLoad();
// ObjectLoader2::DoObjectComponentLoad: the components in the client's order, then the ones it adds after
void DoObjectComponentLoad(ObjectLoadState& state);
/**
* Get the LDF data.
*/
const LDFBaseData* const GetVarData(const std::u16string& name) const;
template<typename T>
LwoNameValue::ValueType::iterator InsertLnvData(LwoNameValue& lnv, const std::u16string& key, T value);
void WriteLDFData(const LwoNameValue& ldf, RakNet::BitStream& outBitStream) const;
LWOOBJID m_ObjectID;
LOT m_TemplateID;
LwoNameValue m_Settings;
LwoNameValue m_NetworkSettings;
NiPoint3 m_DefaultPosition;
NiQuaternion m_DefaultRotation = QuatUtils::IDENTITY;
float m_Scale;
int32_t m_Scene = -1;
Spawner* m_Spawner;
LWOOBJID m_SpawnerID;
bool m_HasSpawnerNodeID;
uint32_t m_SpawnerNodeID;
Character* m_Character;
Entity* m_ParentEntity; //For spawners and the like
std::vector<Entity*> m_ChildEntities;
eGameMasterLevel m_GMLevel;
std::vector<std::string> m_Groups;
uint16_t m_NetworkID;
std::vector<std::function<void()>> m_DieCallbacks;
std::vector<std::function<void(Entity* target)>> m_PhantomCollisionCallbacks;
std::unordered_map<eReplicaComponentType, Component*> m_Components;
// Every component has had its Startup (the end of Initialize)
bool m_ComponentsStarted = false;
std::vector<EntityTimer> m_Timers;
std::vector<EntityTimer> m_PendingTimers;
std::vector<EntityCallbackTimer> m_CallbackTimers;
std::vector<EntityCallbackTimer> m_PendingCallbackTimers;
bool m_ShouldDestroyAfterUpdate = false;
LWOOBJID m_OwnerOverride;
Entity* m_ScheduleKiller;
bool m_PlayerIsReadyForUpdates = false;
bool m_IsGhostingCandidate = false;
int8_t m_Observers = 0;
bool m_IsParentChildDirty = true;
// When this object was created on this server. Construction sends its age from this, as live did.
std::chrono::steady_clock::time_point m_CreationTime = std::chrono::steady_clock::now();
bool m_IsSleeping = false;
/*
* Collision
*/
std::vector<LWOOBJID> m_TargetsInPhantom;
// objectID of receiver and map of notification name to script
std::map<LWOOBJID, std::map<std::string, CppScripts::Script*>> m_Subscriptions;
std::unordered_multimap<MessageType::Game, std::function<bool(GameMessages::GameMsg&)>> m_MsgHandlers;
};
/**
* Template definitions.
*/
template<typename T>
bool Entity::TryGetComponent(const eReplicaComponentType componentId, T*& component) const {
const auto& index = m_Components.find(componentId);
if (index == m_Components.end()) {
component = nullptr;
return false;
}
component = dynamic_cast<T*>(index->second);
return true;
}
template <typename T>
T* Entity::GetComponent() const {
return dynamic_cast<T*>(GetComponent(T::ComponentType));
}
template<typename T>
const T& Entity::GetVar(const std::u16string& name) const {
const auto* const data = GetVarData(name);
if (data == nullptr) {
return LDFData<T>::Default;
}
auto* typed = dynamic_cast<const LDFData<T>* const>(data);
if (typed == nullptr) {
return LDFData<T>::Default;
}
return typed->GetValue();
}
template<typename T>
T Entity::GetVarAs(const std::u16string& name) const {
const auto data = GetVarAsString(name);
return GeneralUtils::TryParse<T>(data).value_or(LDFData<T>::Default);
}
template<typename T>
void Entity::SetVar(const std::u16string& name, T value) {
InsertLnvData<T>(m_Settings, name, value);
}
template<typename T>
LwoNameValue::ValueType::iterator Entity::InsertLnvData(LwoNameValue& lnv, const std::u16string& key, T value) {
auto itr = lnv.values.find(key);
if (itr != lnv.values.end()) {
auto* lnvCast = dynamic_cast<LDFData<T>*>(itr->second.get());
if (!lnvCast) {
// Is of different type
itr->second = std::make_unique<LDFData<T>>(key, value);
} else {
// Is the same type and exists
lnvCast->SetValue(value);
}
} else {
// Doesn't exist
itr = lnv.values.insert_or_assign(key, std::make_unique<LDFData<T>>(key, value)).first;
}
return itr;
}
template<typename T>
LwoNameValue::ValueType::iterator Entity::InsertNetworkVar(const std::u16string& name, T value) {
return InsertLnvData<T>(m_NetworkSettings, name, value);
}
template<typename T>
void Entity::SetNetworkVar(const std::u16string& name, T value, const SystemAddress& sysAddr) {
const auto itr = InsertNetworkVar<T>(name, value);
SendNetworkVar(itr->second->GetString(), sysAddr);
}
template<typename T>
void Entity::SetNetworkVar(const std::string& name, T value, const SystemAddress& sysAddr) {
SetNetworkVar(GeneralUtils::UTF8ToUTF16(name), value, sysAddr);
}
template<typename T>
void Entity::SetNetworkVar(const std::u16string& name, std::vector<T> values, const SystemAddress& sysAddr) {
std::stringstream updates;
auto index = 1;
for (const auto& value : values) {
const auto& indexedName = name + u"." + GeneralUtils::to_u16string(index);
const auto itr = InsertNetworkVar<T>(indexedName, value);
updates << itr->second->GetString();
if (index != values.size()) {
updates << "\n";
}
index++;
}
SendNetworkVar(updates.str(), sysAddr);
}
template<typename T>
T Entity::GetNetworkVar(const std::u16string& name) {
T toReturn = LDFData<T>::Default;
const auto itr = m_NetworkSettings.values.find(name);
if (itr != m_NetworkSettings.values.cend()) {
auto* cast = dynamic_cast<LDFData<T>*>(itr->second.get());
if (cast) toReturn = cast->GetValue();
}
return toReturn;
}
/**
* @brief Adds a component of type ComponentType to this entity and forwards the arguments to the constructor.
*
* @tparam ComponentType The component class type to add. Must derive from Component.
* @tparam VaArgs The argument types to forward to the constructor.
* @param args The arguments to forward to the constructor. The first argument passed to the ComponentType constructor will be this entity.
* @return ComponentType* The added component. Will never return null.
*/
template<typename ComponentType, typename... VaArgs>
inline ComponentType* Entity::AddComponent(VaArgs... args) {
static_assert(std::is_base_of_v<Component, ComponentType>, "ComponentType must be a Component");
// Frame timing: each component type's construction (loading a character's XML is in some of them)
Profiler::Scope profile(Profiler::COMPONENT, static_cast<uint64_t>(ComponentType::ComponentType));
// Get the component if it already exists, or default construct a nullptr
auto*& componentToReturn = m_Components[ComponentType::ComponentType];
// If it doesn't exist, create it and forward the arguments to the constructor
if (!componentToReturn) {
componentToReturn = new ComponentType(this, std::forward<VaArgs>(args)...);
} else {
// In this case the block is already allocated and ready for use
// so we use a placement new to construct the component again as was requested by the caller.
// Placement new means we already have memory allocated for the object, so this just calls its constructor again.
// This is useful for when we want to create a new object in the same memory location as an old one.
componentToReturn->~Component();
new(componentToReturn) ComponentType(this, std::forward<VaArgs>(args)...);
}
// The client's load steps for a new component (DoObjectComponentLoad: the allocator reads the CDClient row, then
// PrepareConfigData); one added after the object started up starts up at once
componentToReturn->LoadDataFromTemplate();
componentToReturn->PrepareConfigData();
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
// To allow a static cast here instead of a dynamic one.
return dynamic_cast<ComponentType*>(componentToReturn);
}
template<typename... T>
auto Entity::GetComponents() const {
return GetComponentsMut<const T...>();
}
template<typename... T>
auto Entity::GetComponentsMut() const {
return std::tuple{ GetComponent<T>()... };
}