diff --git a/dChatServer/ChatPacketHandler.cpp b/dChatServer/ChatPacketHandler.cpp
index d567eea14..44b936a41 100644
--- a/dChatServer/ChatPacketHandler.cpp
+++ b/dChatServer/ChatPacketHandler.cpp
@@ -418,6 +418,10 @@ void ChatPacketHandler::HandleShowAll(const ChatPackets::ShowAllRequest& request
if (!playerData) continue;
response.players.push_back({ playerData.playerName, playerData.zoneID });
}
+ // there is no terminator: the client reads playerCount entries, and prints garbage on a mismatch
+ if (response.players.size() != response.playerCount) {
+ LOG("Written players does not match actual players %zu != %i", response.players.size(), response.playerCount);
+ }
}
SendRouted(request.requestor, sender.worldServerSysAddr, response);
}
diff --git a/dChatServer/PlayerContainer.cpp b/dChatServer/PlayerContainer.cpp
index 29e05d37f..333d5fc8f 100644
--- a/dChatServer/PlayerContainer.cpp
+++ b/dChatServer/PlayerContainer.cpp
@@ -75,7 +75,7 @@ void PlayerContainer::InsertPlayer(const ChatPackets::LoginSessionNotify& notify
data.worldServerSysAddr = sysAddr;
m_Names[data.playerID] = GeneralUtils::UTF8ToUTF16(data.playerName);
- m_PlayerCount++;
+ if (isLogin) m_PlayerCount++;
LOG("Added user: %s (%llu), zone: %i", data.playerName.c_str(), data.playerID, data.zoneID.GetMapID());
diff --git a/dCommon/NiColor.h b/dCommon/NiColor.h
index 585f0e5e8..e3065036d 100644
--- a/dCommon/NiColor.h
+++ b/dCommon/NiColor.h
@@ -9,26 +9,8 @@ struct NiColor {
constexpr NiColor(float red, float green, float blue) : m_Red(red), m_Green(green), m_Blue(blue) {}
constexpr NiColor() : NiColor(0.0f, 0.0f, 0.0f) {}
- /* reduce RGB files to grayscale, with or without alpha
- * using the equation given in Poynton's ColorFAQ at
- * // dead link
- * Copyright (c) 1998-01-04 Charles Poynton poynton at inforamp.net
- *
- * Y = 0.212671 * R + 0.715160 * G + 0.072169 * B
- *
- * We approximate this with
- *
- * Y = 0.21268 * R + 0.7151 * G + 0.07217 * B
- *
- * which can be expressed with integers as
- *
- * Y = (6969 * R + 23434 * G + 2365 * B)/32768
- *
- * The calculation is to be done in a linear colorspace.
- *
- * Other integer coefficents can be used via png_set_rgb_to_gray().
- */
- float ToXYZ() const { return (m_Red * 0.212671f) + (m_Green * 0.71516f) + (m_Blue * 0.072169f); };
+ // Relative luminance (Rec. 709 weights) of a linear color
+ constexpr float Luminance() const { return (m_Red * 0.212671f) + (m_Green * 0.71516f) + (m_Blue * 0.072169f); }
};
#endif // NICOLOR_H
diff --git a/dCommon/PropertyBehaviorActions.h b/dCommon/PropertyBehaviorActions.h
index 9da07e568..1b415c111 100644
--- a/dCommon/PropertyBehaviorActions.h
+++ b/dCommon/PropertyBehaviorActions.h
@@ -16,9 +16,11 @@ namespace PropertyBehaviorActions {
constexpr std::string_view ON_INTERACT = "OnInteract";
constexpr std::string_view ON_ATTACK = "OnAttack";
constexpr std::string_view ON_CHAT = "OnChat";
- constexpr std::array TRIGGERS{ ON_INTERACT, ON_ATTACK, ON_CHAT };
+ constexpr std::string_view ON_STARTUP = "OnStartup";
+ constexpr std::string_view ON_TIMER = "OnTimer";
+ constexpr std::array TRIGGERS{ ON_INTERACT, ON_ATTACK, ON_CHAT, ON_STARTUP, ON_TIMER };
- // Moves the block's distance along a world axis at the model's speed
+ // Moves the block's distance along one of the model's local axes (right x, up y, forward z) at the strip's speed
struct Move {
std::string_view type;
char axis; // 'x', 'y' or 'z'
@@ -30,6 +32,13 @@ namespace PropertyBehaviorActions {
Move{ "MoveForward", 'z', 1.0f }, Move{ "MoveBackward", 'z', -1.0f },
};
+ // Turns the block's degrees about one of the model's local axes (tilt x, spin y, roll z) at the strip's speed
+ constexpr std::array ROTATIONS{
+ Move{ "Spin", 'y', 1.0f }, Move{ "SpinNegative", 'y', -1.0f },
+ Move{ "Tilt", 'x', 1.0f }, Move{ "TiltNegative", 'x', -1.0f },
+ Move{ "Roll", 'z', 1.0f }, Move{ "RollNegative", 'z', -1.0f },
+ };
+
// Blocks that make an object: spawns make one enemy at the model; drops give every player one powerup per unit of
// the block's value
struct LotBlock {
@@ -63,6 +72,8 @@ namespace PropertyBehaviorActions {
// The rest, each with its own handling
constexpr std::string_view SET_SPEED = "SetSpeed";
+ constexpr std::string_view MOVE_BACK_TO_START = "MoveBackToStart";
+ constexpr std::string_view DO_DAMAGE = "DoDamage";
constexpr std::string_view SMASH = "Smash";
constexpr std::string_view UNSMASH = "UnSmash";
constexpr std::string_view WAIT = "Wait";
@@ -70,9 +81,9 @@ namespace PropertyBehaviorActions {
constexpr std::string_view PRIVATE_MESSAGE = "PrivateMessage";
constexpr std::string_view PLAY_SOUND = "PlaySound";
constexpr std::string_view RESTART = "Restart";
- constexpr std::array OTHERS{ SET_SPEED, SMASH, UNSMASH, WAIT, CHAT, PRIVATE_MESSAGE, PLAY_SOUND, RESTART };
+ constexpr std::array OTHERS{ SET_SPEED, MOVE_BACK_TO_START, DO_DAMAGE, SMASH, UNSMASH, WAIT, CHAT, PRIVATE_MESSAGE, PLAY_SOUND, RESTART };
- // The speed models move at until a Set Speed block (ModelComponent)
+ // The speed a strip's moves and rotations start at until a Set Speed block (Strip)
constexpr float DEFAULT_SPEED = 3.0f;
// The table entry for a block type, or nullptr
diff --git a/dCommon/dEnums/dCommonVars.h b/dCommon/dEnums/dCommonVars.h
index 89946de39..cbbfd7c16 100644
--- a/dCommon/dEnums/dCommonVars.h
+++ b/dCommon/dEnums/dCommonVars.h
@@ -52,7 +52,7 @@ constexpr LWOMAPID LWOMAPID_INVALID = -1; //!< Invalid LWOMAPID
constexpr uint64_t LWOZONEID_INVALID = 0; //!< Invalid LWOZONEID
constexpr uint32_t MAX_MESSAGE_LENGTH = 0x500000; //!< Prevent exceptionally large msgs from being processed. Should always be used to check user provided inputs.
-constexpr float PI = 3.14159f;
+constexpr float PI = 3.14159265358979323846264338327950288f;
//============ STRUCTS ==============
diff --git a/dCommon/dMath.h b/dCommon/dMath.h
new file mode 100644
index 000000000..f62313358
--- /dev/null
+++ b/dCommon/dMath.h
@@ -0,0 +1,23 @@
+// Darkflame Universe
+// Copyright 2025
+
+#ifndef DMATH_H
+#define DMATH_H
+
+#include
+
+namespace Math {
+ constexpr float PI = 3.14159265358979323846264338327950288f;
+ constexpr float RATIO_DEG_TO_RAD = PI / 180.0f;
+ constexpr float RATIO_RAD_TO_DEG = 180.0f / PI;
+
+ inline float DegToRad(float degrees) {
+ return degrees * RATIO_DEG_TO_RAD;
+ }
+
+ inline float RadToDeg(float radians) {
+ return radians * RATIO_RAD_TO_DEG;
+ }
+};
+
+#endif //!DMATH_H
diff --git a/dDashboardServer/routes/BehaviorXml.h b/dDashboardServer/routes/BehaviorXml.h
index fdd6ef1e6..b1f036dad 100644
--- a/dDashboardServer/routes/BehaviorXml.h
+++ b/dDashboardServer/routes/BehaviorXml.h
@@ -28,16 +28,17 @@ namespace BehaviorXml {
/**
* The blocks the server plays and how (PropertyBehaviorActions, which Strip.cpp runs on), for the viewer's behavior
- * player: {triggers: [type], moves: {type: [axis, sign]}, spawns: {type: {lot, name}}, drops: {type: {lot, name}},
+ * player: {triggers: [type], moves: {type: [axis, sign]}, rotations: {type: [axis, sign]}, spawns: {type: {lot, name}}, drops: {type: {lot, name}},
* stateChanges: {type: stateId}, others: [type], states: {id: name}, defaultSpeed}. itemName names a LOT.
*/
inline nlohmann::json Rules(const std::function& itemName) {
using namespace PropertyBehaviorActions;
- nlohmann::json rules{ {"triggers", nlohmann::json::array()}, {"moves", nlohmann::json::object()}, {"spawns", nlohmann::json::object()},
+ nlohmann::json rules{ {"triggers", nlohmann::json::array()}, {"moves", nlohmann::json::object()}, {"rotations", nlohmann::json::object()}, {"spawns", nlohmann::json::object()},
{"drops", nlohmann::json::object()}, {"stateChanges", nlohmann::json::object()}, {"others", nlohmann::json::array()},
{"states", nlohmann::json::object()}, {"defaultSpeed", DEFAULT_SPEED} };
for (const auto type : TRIGGERS) rules["triggers"].push_back(type);
for (const auto& move : MOVES) rules["moves"][std::string(move.type)] = { std::string(1, move.axis), move.sign };
+ for (const auto& rotation : ROTATIONS) rules["rotations"][std::string(rotation.type)] = { std::string(1, rotation.axis), rotation.sign };
for (const auto& spawn : SPAWNS) rules["spawns"][std::string(spawn.type)] = { {"lot", spawn.lot}, {"name", itemName(spawn.lot)} };
for (const auto& drop : DROPS) rules["drops"][std::string(drop.type)] = { {"lot", drop.lot}, {"name", itemName(drop.lot)} };
for (const auto& change : STATE_CHANGES) rules["stateChanges"][std::string(change.type)] = static_cast(change.state);
diff --git a/dGame/dComponents/DestroyableComponent.cpp b/dGame/dComponents/DestroyableComponent.cpp
index 30781567c..4b007954d 100644
--- a/dGame/dComponents/DestroyableComponent.cpp
+++ b/dGame/dComponents/DestroyableComponent.cpp
@@ -702,7 +702,7 @@ void DestroyableComponent::Damage(uint32_t damage, const LWOOBJID source, uint32
// Client does the same check, so we're doing it too
auto* const modelComponent = m_Parent->GetComponent();
if (modelComponent) {
- modelComponent->OnHit();
+ modelComponent->OnHit(source);
// Don't actually deal the damage so the model doesn't die
return;
}
diff --git a/dGame/dComponents/InventoryComponent.h b/dGame/dComponents/InventoryComponent.h
index f99d2c581..358cf9546 100644
--- a/dGame/dComponents/InventoryComponent.h
+++ b/dGame/dComponents/InventoryComponent.h
@@ -476,6 +476,8 @@ public:
void UpdateGroup(const GroupUpdate& groupUpdate);
const std::map>& GetGroups() const { return m_Groups; }
+ const std::map>& GetGroups() const { return m_Groups; }
+
std::unordered_map& GetPetsMut() { return m_Pets; };
void FixInvisibleItems();
diff --git a/dGame/dComponents/ModelComponent.cpp b/dGame/dComponents/ModelComponent.cpp
index bad6106bc..bec7aaf7c 100644
--- a/dGame/dComponents/ModelComponent.cpp
+++ b/dGame/dComponents/ModelComponent.cpp
@@ -1,9 +1,13 @@
#include "ModelComponent.h"
#include "CombatMessages.h"
+
+#include
+
#include "Entity.h"
#include "Game.h"
#include "Logger.h"
+#include "dMath.h"
#include "BehaviorStates.h"
#include "PropertyBehaviorActions.h"
@@ -14,6 +18,7 @@
#include "SimplePhysicsComponent.h"
#include "eMissionTaskType.h"
#include "eObjectBits.h"
+#include "DestroyableComponent.h"
#include "Database.h"
#include "DluAssert.h"
@@ -55,8 +60,13 @@ bool ModelComponent::OnResetModelToDefaults(GameMessages::ResetModelToDefaults&
if (reset.bResetPos) m_Parent->SetPosition(m_OriginalPosition);
if (reset.bResetRot) m_Parent->SetRotation(m_OriginalRotation);
m_Parent->SetVelocity(NiPoint3Constant::ZERO);
+ // Save and increment the interrupt count so other behaviors on this model move to their next action instead of getting stuck
+ const auto moveInterruptCount = m_Move.interruptCount + 1;
+ m_Move = MoveState{};
+ m_Move.interruptCount = moveInterruptCount;
+ SyncLinearVelocity();
+ ResetRotationState(m_Parent->GetRotation());
- m_Speed = PropertyBehaviorActions::DEFAULT_SPEED;
m_NumListeningInteract = 0;
m_NumActiveAttack = 0;
@@ -85,11 +95,21 @@ bool ModelComponent::OnRequestUse(GameMessages::RequestUseEvent& event) {
void ModelComponent::Run(float deltaTime) {
if (m_IsPaused) return;
+ m_DamageCooldown -= deltaTime;
+
+ // Arrived once this frame's movement reached or passed the target
+ if (m_Move.target && (*m_Move.target - m_Parent->GetPosition()).DotProduct(m_Move.targetDirection) <= 0.0f) {
+ m_Parent->SetPosition(*m_Move.target);
+ m_Move.target.reset();
+ }
for (auto& behavior : m_Behaviors) {
behavior.Update(deltaTime, *this);
}
+ // Done after all behaviors so the heading reflects any rotation applied this frame
+ SyncLinearVelocity();
+
if (!m_RestartAtEndOfFrame) return;
GameMessages::ResetModelToDefaults reset{};
@@ -302,41 +322,102 @@ void ModelComponent::RemoveUnSmash() {
m_NumActiveUnSmash--;
}
-bool ModelComponent::TrySetVelocity(const NiPoint3& velocity) const {
- auto currentVelocity = m_Parent->GetVelocity();
+bool ModelComponent::TryStartMove(const int axis, const float direction, const float speed) {
+ if (axis < 0 || axis > 2 || direction == 0.0f || speed <= 0.0f) return false;
+ if (m_Move.velocity[axis] != 0.0f) return false;
- // If we're currently moving on an axis, prevent the move so only 1 behavior can have control over an axis
- if (velocity != NiPoint3Constant::ZERO) {
- const auto [x, y, z] = velocity * m_Speed;
- if (x != 0.0f) {
- if (currentVelocity.x != 0.0f) return false;
- currentVelocity.x = x;
- } else if (y != 0.0f) {
- if (currentVelocity.y != 0.0f) return false;
- currentVelocity.y = y;
- } else if (z != 0.0f) {
- if (currentVelocity.z != 0.0f) return false;
- currentVelocity.z = z;
- }
- } else {
- currentVelocity = velocity;
- }
-
- m_Parent->SetVelocity(currentVelocity);
+ m_Move.target.reset();
+ m_Move.velocity[axis] = std::copysign(speed, direction);
return true;
}
-void ModelComponent::SetVelocity(const NiPoint3& velocity) const {
- m_Parent->SetVelocity(velocity);
+void ModelComponent::StopMove(const int axis) {
+ if (axis < 0 || axis > 2) return;
+ m_Move.velocity[axis] = 0.0f;
}
-void ModelComponent::OnChatMessageReceived(const std::string& sMessage) {
- for (auto& behavior : m_Behaviors) behavior.OnChatMessageReceived(sMessage);
+void ModelComponent::StartMoveTo(const NiPoint3& target, const float speed) {
+ m_Move.velocity = NiPoint3Constant::ZERO;
+ m_Move.interruptCount++;
+ m_Move.target = target;
+ m_Move.targetSpeed = speed;
}
-void ModelComponent::OnHit() {
+void ModelComponent::SyncLinearVelocity() {
+ const auto& rotation = m_Parent->GetRotation();
+ m_Move.basis = { QuatUtils::Right(rotation), QuatUtils::Up(rotation), QuatUtils::Forward(rotation) };
+
+ NiPoint3 velocity = NiPoint3Constant::ZERO;
+ for (int axis = 0; axis < 3; axis++) velocity += m_Move.basis[axis] * m_Move.velocity[axis];
+
+ if (m_Move.target) {
+ const auto toTarget = *m_Move.target - m_Parent->GetPosition();
+ const float distance = toTarget.Length();
+ m_Move.targetDirection = distance > 0.0f ? toTarget / distance : NiPoint3Constant::ZERO;
+ velocity += m_Move.targetDirection * m_Move.targetSpeed;
+ }
+
+ // Leave velocity alone unless a move owns it, e.g. pets are driven elsewhere
+ const bool isMoving = velocity != NiPoint3Constant::ZERO;
+ if (!isMoving && !m_Move.wasMoving) return;
+ m_Move.wasMoving = isMoving;
+
+ // Setting velocity always marks it dirty for serialization
+ if (velocity != m_Parent->GetVelocity()) m_Parent->SetVelocity(velocity);
+}
+
+bool ModelComponent::TryStartRotation(const int axis, const float direction, const float speed) {
+ if (axis < 0 || axis > 2 || direction == 0.0f || speed <= 0.0f) return false;
+ if (m_Rotation.velocity[axis] != 0.0f) return false;
+
+ // Rebase only when nothing is rotating so simultaneous rotations stay relative to the same base
+ if (m_Rotation.velocity == NiPoint3Constant::ZERO) ResetRotationState(m_Parent->GetRotation());
+
+ m_Rotation.degrees[axis] = std::fmod(m_Rotation.degrees[axis], 360.0f);
+ m_Rotation.actionStart[axis] = m_Rotation.degrees[axis];
+ m_Rotation.velocity[axis] = std::copysign(BASE_ANGULAR_SPEED * speed, direction);
+ SyncAngularVelocity();
+ return true;
+}
+
+void ModelComponent::SetRotationProgress(const int axis, const float degrees) {
+ if (axis < 0 || axis > 2) return;
+ m_Rotation.degrees[axis] = m_Rotation.actionStart[axis] + degrees;
+
+ // Whole turns wrap to exactly 0 so e.g. 720 degrees yields exactly the base rotation
+ const NiPoint3 radians(
+ Math::DegToRad(std::fmod(m_Rotation.degrees.x, 360.0f)),
+ Math::DegToRad(std::fmod(m_Rotation.degrees.y, 360.0f)),
+ Math::DegToRad(std::fmod(m_Rotation.degrees.z, 360.0f))
+ );
+ m_Parent->SetRotation(QuatUtils::FromEuler(radians) * m_Rotation.base);
+}
+
+void ModelComponent::StopRotation(const int axis) {
+ if (axis < 0 || axis > 2) return;
+ m_Rotation.velocity[axis] = 0.0f;
+ SyncAngularVelocity();
+}
+
+void ModelComponent::SyncAngularVelocity() const {
+ GameMessages::SetAngularVelocity setAngVel{};
+ setAngVel.target = m_Parent->GetObjectID();
+ setAngVel.angVelocity = m_Rotation.velocity * Math::DegToRad(1.0f);
+ setAngVel.Send();
+}
+
+void ModelComponent::ResetRotationState(const NiQuaternion& newBase) {
+ m_Rotation = RotationState{ .base = newBase };
+ SyncAngularVelocity();
+}
+
+void ModelComponent::OnChatMessageReceived(const std::string& sMessage, const LWOOBJID sender) {
+ for (auto& behavior : m_Behaviors) behavior.OnChatMessageReceived(sMessage, sender);
+}
+
+void ModelComponent::OnHit(const LWOOBJID attacker) {
for (auto& behavior : m_Behaviors) {
- behavior.OnHit();
+ behavior.OnHit(attacker);
}
}
@@ -405,3 +486,14 @@ void ModelComponent::Allocator(Entity& entity, ObjectLoadState& state) {
}
}
}
+
+void ModelComponent::DoDamage(const LWOOBJID target) {
+ // dont do damage if we've done damage recently
+ if (m_DamageCooldown > 0.0f) return;
+ m_DamageCooldown = 1.0f;
+ auto* const initiator = Game::entityManager->GetEntity(target);
+ if (initiator) {
+ auto* const destComp = initiator->GetComponent();
+ if (destComp) destComp->Damage(1, GetParent()->GetObjectID());
+ }
+}
diff --git a/dGame/dComponents/ModelComponent.h b/dGame/dComponents/ModelComponent.h
index 0ef001311..446131fd5 100644
--- a/dGame/dComponents/ModelComponent.h
+++ b/dGame/dComponents/ModelComponent.h
@@ -1,7 +1,9 @@
#pragma once
#include
+#include
#include