Files
DarkflameServer/dGame/dComponents/ModelComponent.h
Aaron Kimbrell b466c08ee2 Merge origin/main into experimental: behaviors, raw terrain scene lookups, property model fixes, player count
Main's model behaviors (every rotation, DoDamage, OnTimer, OnStartup, MoveBackToStart, moves along the model's
local axes), raw terrain reading with scene lookups and scene tools, property model and brick by brick fixes and the
chat server's player count fix, merged onto this branch's structures:

- game messages stay structs; the angular velocity messages join them and the handler sets a message's target
- strips own their speed again and the block names, moves and rotations stay in the shared action table
  (PropertyBehaviorActions, which the dashboard's behavior player reads; it now lists rotations too)
- the zone keeps reading its file through ZoneFile and takes the boundaries from it, then loads the raw terrain as main
  does; the old dNavigation terrain reader is gone
- the show all response stays a struct and logs a player count mismatch as main does
- the property management component tells models who sent a chat message

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 10:57:45 -06:00

300 lines
10 KiB
C++

#pragma once
#include <array>
#include <cmath>
#include <map>
#include <optional>
#include "dCommonVars.h"
#include "RakNetTypes.h"
#include "NiPoint3.h"
#include "NiQuaternion.h"
#include "Component.h"
#include "eReplicaComponentType.h"
#include "Action.h"
#include "PropertyBehavior.h"
#include "StripUiPosition.h"
#include "ObjectMessages.h"
struct ObjectLoadState;
class AddMessage;
class AMFArrayValue;
class BehaviorMessageBase;
class Entity;
class MoveToInventoryMessage;
/**
* Component that represents entities that are a model, e.g. collectible models and BBB models.
*/
class ModelComponent final : public Component {
public:
static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::MODEL;
// The client's allocator for this type (ObjectLoader2::DoObjectComponentLoad calls it in load order)
static void Allocator(Entity& entity, ObjectLoadState& state);
ModelComponent(Entity* parent, const int32_t componentID);
void LoadConfigData() override;
void LoadBehaviors();
void Run(float deltaTime) override;
bool OnRequestUse(GameMessages::RequestUseEvent& event);
bool OnResetModelToDefaults(GameMessages::ResetModelToDefaults& resetModelToDefaults);
bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo);
void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) override;
/**
* Returns the original position of the model
* @return the original position of the model
*/
const NiPoint3& GetOriginalPosition() { return m_OriginalPosition; }
/**
* Sets the original position of the model
* @param pos the original position to set
*/
void SetPosition(const NiPoint3& pos) { m_OriginalPosition = pos; }
/**
* Returns the original rotation of the model
* @return the original rotation of the model
*/
const NiQuaternion& GetOriginalRotation() { return m_OriginalRotation; }
/**
* Sets the original rotation of the model
* @param rot the original rotation to set
*/
void SetRotation(const NiQuaternion& rot) { m_OriginalRotation = rot; }
/**
* Main gateway for all behavior messages to be passed to their respective behaviors.
*
* @tparam Msg The message type to pass
* @param args the arguments of the message to be deserialized
*
* @return returns true if a new behaviorID is needed.
*/
template<typename Msg>
bool HandleControlBehaviorsMsg(const AMFArrayValue& args) {
static_assert(std::is_base_of_v<BehaviorMessageBase, Msg>, "Msg must be a BehaviorMessageBase");
Msg msg{ args };
for (auto&& behavior : m_Behaviors) {
if (behavior.GetBehaviorId() == msg.GetBehaviorId()) {
behavior.CheckModifyState(msg);
behavior.HandleMsg(msg);
return msg.GetNeedsNewBehaviorID();
}
}
// If we somehow added more than 5 behaviors, resize to 5.
if (m_Behaviors.size() > 5) m_Behaviors.resize(5);
// Do not allow more than 5 to be added. The client UI will break if you do!
if (m_Behaviors.size() == 5) return false;
auto newBehavior = m_Behaviors.insert(m_Behaviors.begin(), PropertyBehavior());
// Generally if we are inserting a new behavior, it is because the client is creating a new behavior.
// However if we are testing behaviors the behavior will not exist on the initial pass, so we set the ID here to that of the msg.
// This will either set the ID to -1 (no change in the current default) or set the ID to the ID of the behavior we are testing.
newBehavior->SetBehaviorId(msg.GetBehaviorId());
newBehavior->CheckModifyState(msg);
newBehavior->HandleMsg(msg);
return msg.GetNeedsNewBehaviorID();
};
void ProgressAddBehaviorMission(Entity& playerEntity);
void AddBehavior(AddMessage& msg);
void RemoveBehavior(MoveToInventoryMessage& msg, const bool keepItem);
// Updates the pending behavior ID to the new ID.
void UpdatePendingBehaviorId(const LWOOBJID newId, const LWOOBJID oldId);
// Sends the behavior list to the client.
/**
* The behaviors AMFArray will have up to 5 elements in the dense portion.
* Each element in the dense portion will be made up of another AMFArray
* with the following information mapped in the associative portion
* "id": Behavior ID cast to an AMFString
* "isLocked": AMFTrue or AMFFalse of whether or not the behavior is locked
* "isLoot": AMFTrue or AMFFalse of whether or not the behavior is a custom behavior (true if custom)
* "name": The name of the behavior formatted as an AMFString
*/
void SendBehaviorListToClient(AMFArrayValue& args) const;
void SendBehaviorBlocksToClient(const LWOOBJID behaviorToSend, AMFArrayValue& args) const;
void VerifyBehaviors();
std::array<std::pair<LWOOBJID, std::string>, 5> GetBehaviorsForSave() const;
const std::vector<PropertyBehavior>& GetBehaviors() const { return m_Behaviors; };
void AddInteract();
void RemoveInteract();
void Pause() { m_Dirty = true; m_IsPaused = true; }
void AddUnSmash();
void RemoveUnSmash();
bool IsUnSmashing() const { return m_NumActiveUnSmash != 0; }
void Resume();
// Attempts to claim a local axis (0 = right, 1 = up, 2 = forward) for movement in direction (+1 or -1) at speed units per second.
// Returns false if the axis is already controlled by a behavior. Cancels any active StartMoveTo.
bool TryStartMove(const int axis, const float direction, const float speed);
// Releases the local axis so another behavior can move along it.
void StopMove(const int axis);
// World space direction of the local axis used for the most recently applied velocity.
const NiPoint3& GetMoveAxis(const int axis) const { return m_Move.basis[axis]; }
// Interrupts all active moves and moves in a straight line to target at speed units per second.
void StartMoveTo(const NiPoint3& target, const float speed);
// Whether a StartMoveTo move has yet to arrive or be overridden.
bool IsMovingToTarget() const noexcept { return m_Move.target.has_value(); }
// Changes whenever active moves are interrupted, so strips can tell their move was cancelled.
uint32_t GetMoveInterruptCount() const noexcept { return m_Move.interruptCount; }
// Attempts to claim a world axis (0 = x, 1 = y, 2 = z) for rotation in direction (+1 or -1) at the given behavior speed.
// Returns false if the axis is already controlled by a behavior.
bool TryStartRotation(const int axis, const float direction, const float speed);
// Sets how many signed degrees the active rotation on axis has progressed and updates the entity.
void SetRotationProgress(const int axis, const float degrees);
// Releases the axis so another behavior can rotate it.
void StopRotation(const int axis);
// Degrees per second of the active rotation on axis.
float GetAngularSpeed(const int axis) const noexcept { return std::abs(m_Rotation.velocity[axis]); }
void OnChatMessageReceived(const std::string& sMessage, const LWOOBJID sender);
void OnHit(const LWOOBJID attacker);
// Whether or not to restart at the end of the frame
void RestartAtEndOfFrame() { m_RestartAtEndOfFrame = true; }
// Increments the number of strips listening for an attack.
// If this is the first strip adding an attack, it will set the factions to the correct values.
void AddAttack();
// Decrements the number of strips listening for an attack.
// If this is the last strip removing an attack, it will reset the factions to the default of -1.
void RemoveAttack();
void DoDamage(const LWOOBJID target);
private:
// Degrees per second per unit of behavior speed
static constexpr float BASE_ANGULAR_SPEED = 15.0f;
struct RotationState {
// The rotation that degrees is applied on top of
NiQuaternion base = QuatUtils::IDENTITY;
// Accumulated signed degrees per world axis since base was set
NiPoint3 degrees{};
// degrees at the moment the current rotation on each axis started
NiPoint3 actionStart{};
// Signed degrees per second per world axis. Non-zero means a behavior owns that axis.
NiPoint3 velocity{};
};
struct MoveState {
// Signed units per second along local right, up and forward. Non-zero means a behavior owns that axis.
NiPoint3 velocity{};
// Right, up and forward in world space as of the last velocity update
std::array<NiPoint3, 3> basis{ NiPoint3Constant::UNIT_X, NiPoint3Constant::UNIT_Y, NiPoint3Constant::UNIT_Z };
// Whether the last velocity update came from an active move
bool wasMoving{ false };
// World position of an active StartMoveTo move
std::optional<NiPoint3> target;
// Units per second of the move to target
float targetSpeed{};
// Direction to target as of the last velocity update, zero once there
NiPoint3 targetDirection{};
// Incremented each time StartMoveTo interrupts the active moves
uint32_t interruptCount{};
};
// Sends the client-side angular velocity for the currently active rotation axes.
void SyncAngularVelocity() const;
// Recomputes the linear velocity from the active move axes and the current rotation.
void SyncLinearVelocity();
// Clears all rotation state and makes rotation relative to newBase.
void ResetRotationState(const NiQuaternion& newBase);
// Loads a behavior from the database.
void LoadBehavior(const LWOOBJID behaviorID, const size_t index, const bool isIndexed);
// Writes a behavior to a string so it can be saved.
std::string SaveBehavior(const PropertyBehavior& behavior) const;
// Number of Actions that are awaiting an UnSmash to finish.
uint32_t m_NumActiveUnSmash{};
// Whether or not this component needs to have its extra data serialized.
bool m_Dirty{};
// The number of strips listening for a RequestUse GM to come in.
uint32_t m_NumListeningInteract{};
// The number of strips listening for an attack.
uint32_t m_NumActiveAttack{};
// Whether or not the model is paused and should reject all interactions regarding behaviors.
bool m_IsPaused{};
/**
* The behaviors of the model
* Note: This is a vector because the order of the behaviors matters when serializing to the client.
* Note: No two PropertyBehaviors should have the same behavior ID.
*/
std::vector<PropertyBehavior> m_Behaviors;
/**
* The original position of the model
*/
NiPoint3 m_OriginalPosition;
/**
* The rotation original of the model
*/
NiQuaternion m_OriginalRotation = QuatUtils::IDENTITY;
/**
* The ID of the user that made the model
*/
LWOOBJID m_userModelID;
// Whether or not to restart at the end of the frame.
bool m_RestartAtEndOfFrame{ false };
RotationState m_Rotation;
MoveState m_Move;
float m_DamageCooldown { 0.0f };
};