mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-11-14 07:47:13 +00:00
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>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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
|
||||
* <http://www.inforamp.net/~poynton/> // 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ==============
|
||||
|
||||
|
||||
23
dCommon/dMath.h
Normal file
23
dCommon/dMath.h
Normal file
@@ -0,0 +1,23 @@
|
||||
// Darkflame Universe
|
||||
// Copyright 2025
|
||||
|
||||
#ifndef DMATH_H
|
||||
#define DMATH_H
|
||||
|
||||
#include <cmath>
|
||||
|
||||
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
|
||||
@@ -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<std::string(int32_t)>& 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<int>(change.state);
|
||||
|
||||
@@ -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<ModelComponent>();
|
||||
if (modelComponent) {
|
||||
modelComponent->OnHit();
|
||||
modelComponent->OnHit(source);
|
||||
// Don't actually deal the damage so the model doesn't die
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -476,6 +476,8 @@ public:
|
||||
void UpdateGroup(const GroupUpdate& groupUpdate);
|
||||
const std::map<eInventoryType, std::vector<Group>>& GetGroups() const { return m_Groups; }
|
||||
|
||||
const std::map<eInventoryType, std::vector<Group>>& GetGroups() const { return m_Groups; }
|
||||
|
||||
std::unordered_map<LWOOBJID, DatabasePet>& GetPetsMut() { return m_Pets; };
|
||||
|
||||
void FixInvisibleItems();
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
#include "ModelComponent.h"
|
||||
#include "CombatMessages.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#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<DestroyableComponent>();
|
||||
if (destComp) destComp->Damage(1, GetParent()->GetObjectID());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
|
||||
#include "dCommonVars.h"
|
||||
#include "RakNetTypes.h"
|
||||
@@ -12,7 +14,6 @@
|
||||
|
||||
#include "Action.h"
|
||||
#include "PropertyBehavior.h"
|
||||
#include "PropertyBehaviorActions.h"
|
||||
#include "StripUiPosition.h"
|
||||
#include "ObjectMessages.h"
|
||||
|
||||
@@ -148,20 +149,41 @@ public:
|
||||
|
||||
void Resume();
|
||||
|
||||
// Attempts to set the velocity of an axis for movement.
|
||||
// If the axis currently has a velocity of zero, returns true.
|
||||
// If the axis is currently controlled by a behavior, returns false.
|
||||
bool TrySetVelocity(const NiPoint3& velocity) const;
|
||||
// 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);
|
||||
|
||||
// Force sets the velocity to a value.
|
||||
void SetVelocity(const NiPoint3& velocity) const;
|
||||
// Releases the local axis so another behavior can move along it.
|
||||
void StopMove(const int axis);
|
||||
|
||||
void OnChatMessageReceived(const std::string& sMessage);
|
||||
// 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]; }
|
||||
|
||||
void OnHit();
|
||||
// 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);
|
||||
|
||||
// Sets the speed of the model
|
||||
void SetSpeed(const float newSpeed) { m_Speed = newSpeed; }
|
||||
// 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; }
|
||||
@@ -173,7 +195,56 @@ public:
|
||||
// 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);
|
||||
@@ -217,9 +288,12 @@ private:
|
||||
*/
|
||||
LWOOBJID m_userModelID;
|
||||
|
||||
// The speed at which this model moves
|
||||
float m_Speed{ PropertyBehaviorActions::DEFAULT_SPEED };
|
||||
|
||||
// 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 };
|
||||
};
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
#include "PhantomPhysicsComponent.h"
|
||||
|
||||
SimplePhysicsComponent::SimplePhysicsComponent(Entity* parent, const int32_t componentID) : PhysicsComponent(parent, componentID) {
|
||||
RegisterMsg(&SimplePhysicsComponent::OnGetAngularVelocity);
|
||||
RegisterMsg(&SimplePhysicsComponent::OnSetAngularVelocity);
|
||||
RegisterMsg(&SimplePhysicsComponent::OnGetObjectReportInfo);
|
||||
|
||||
m_Position = m_Parent->GetDefaultPosition();
|
||||
|
||||
@@ -24,6 +24,11 @@ enum class eClimbableType : int32_t {
|
||||
CLIMBABLE_TYPE_WALL_STICK
|
||||
};
|
||||
|
||||
namespace GameMessages {
|
||||
struct SetAngularVelocity;
|
||||
struct GetAngularVelocity;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Component that serializes locations of entities to the client
|
||||
@@ -73,6 +78,9 @@ public:
|
||||
*/
|
||||
void SetAngularVelocity(const NiPoint3& value) { m_AngularVelocity = value; m_DirtyVelocity = true; }
|
||||
|
||||
bool OnSetAngularVelocity(GameMessages::SetAngularVelocity& setAngVel);
|
||||
bool OnGetAngularVelocity(GameMessages::GetAngularVelocity& getAngVel);
|
||||
|
||||
/**
|
||||
* Returns the physics motion state
|
||||
* @return the physics motion state
|
||||
|
||||
@@ -999,14 +999,14 @@ const std::map<LWOOBJID, LWOOBJID>& PropertyManagementComponent::GetModels() con
|
||||
return models;
|
||||
}
|
||||
|
||||
void PropertyManagementComponent::OnChatMessageReceived(const std::string& sMessage) const {
|
||||
void PropertyManagementComponent::OnChatMessageReceived(const std::string& sMessage, const LWOOBJID sender) const {
|
||||
for (const auto& modelID : models | std::views::keys) {
|
||||
auto* const model = Game::entityManager->GetEntity(modelID);
|
||||
if (!model) continue;
|
||||
auto* const modelComponent = model->GetComponent<ModelComponent>();
|
||||
if (!modelComponent) continue;
|
||||
|
||||
modelComponent->OnChatMessageReceived(sMessage);
|
||||
modelComponent->OnChatMessageReceived(sMessage, sender);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ public:
|
||||
uint32_t GetReputation() const noexcept { return reputation; }
|
||||
|
||||
|
||||
void OnChatMessageReceived(const std::string& sMessage) const;
|
||||
void OnChatMessageReceived(const std::string& sMessage, const LWOOBJID sender) const;
|
||||
|
||||
/**
|
||||
* Live updates (docs/LiveUpdate.md): a new instance of this property is about to load it from the database. Takes
|
||||
|
||||
@@ -30,10 +30,12 @@ target_include_directories(dGameMessages PUBLIC "."
|
||||
${DLU_COMPONENT_INCLUDE_DIRS} # direct MissionComponent.h
|
||||
"${PROJECT_SOURCE_DIR}/dGame/dUtilities" # direct SlashCommandHandler.h
|
||||
"${PROJECT_SOURCE_DIR}/dGame/dPropertyBehaviors" # direct ControlBehaviors.h
|
||||
"${PROJECT_SOURCE_DIR}/dGame/dPropertyBehaviors/ControlBehaviorMessages" # direct ControlBehaviors.h
|
||||
"${PROJECT_SOURCE_DIR}/dGame/dMission" # via MissionComponent.h
|
||||
"${PROJECT_SOURCE_DIR}/dGame/dBehaviors" # via InventoryComponent.h
|
||||
"${PROJECT_SOURCE_DIR}/dGame/dInventory" # via InventoryComponent.h
|
||||
"${PROJECT_SOURCE_DIR}/dGame/dEntity" # via dZoneManager/Spawner.h
|
||||
"${PROJECT_SOURCE_DIR}/dZoneManager" # via GameMessages.cpp, GameMessageHandler.cpp
|
||||
"${PROJECT_SOURCE_DIR}/dGame/d"
|
||||
)
|
||||
target_precompile_headers(dGameMessages REUSE_FROM dGameBase)
|
||||
|
||||
@@ -260,6 +260,7 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System
|
||||
}
|
||||
}
|
||||
|
||||
msg->target = objectID;
|
||||
if (!msg->Deserialize(inStream)) {
|
||||
LOG("Dropping malformed GM %4i, %s from (%llu) targeting (%llu)", messageID, StringifiedEnum::ToString(messageID).data(), usr->GetLoggedInChar(), objectID);
|
||||
return;
|
||||
|
||||
@@ -196,6 +196,21 @@ namespace GameMessages {
|
||||
bool bIgnoreChecks{ false };
|
||||
};
|
||||
|
||||
struct GetAngularVelocity : public GameMsg {
|
||||
GetAngularVelocity() : GameMsg(MessageType::Game::GET_ANGULAR_VELOCITY) {}
|
||||
|
||||
NiPoint3 angVelocity{};
|
||||
};
|
||||
|
||||
struct SetAngularVelocity : public GameMsg {
|
||||
SetAngularVelocity() : GameMsg(MessageType::Game::SET_ANGULAR_VELOCITY) {}
|
||||
|
||||
NiPoint3 angVelocity{};
|
||||
|
||||
bool bIgnoreDirtyFlags{};
|
||||
|
||||
bool bForceFlagDirty{};
|
||||
};
|
||||
|
||||
struct GetMissionState : public GameMsg {
|
||||
GetMissionState() : GameMsg(MessageType::Game::GET_MISSION_STATE) {}
|
||||
|
||||
@@ -212,14 +212,14 @@ void PropertyBehavior::Update(float deltaTime, ModelComponent& modelComponent) {
|
||||
}
|
||||
}
|
||||
|
||||
void PropertyBehavior::OnChatMessageReceived(const std::string& sMessage) {
|
||||
void PropertyBehavior::OnChatMessageReceived(const std::string& sMessage, const LWOOBJID sender) {
|
||||
auto& activeState = GetActiveState();
|
||||
activeState.OnChatMessageReceived(sMessage);
|
||||
activeState.OnChatMessageReceived(sMessage, sender);
|
||||
}
|
||||
|
||||
void PropertyBehavior::OnHit() {
|
||||
void PropertyBehavior::OnHit(const LWOOBJID attacker) {
|
||||
auto& activeState = GetActiveState();
|
||||
activeState.OnHit();
|
||||
activeState.OnHit(attacker);
|
||||
}
|
||||
|
||||
State& PropertyBehavior::GetActiveState() {
|
||||
|
||||
@@ -46,8 +46,8 @@ public:
|
||||
void Deserialize(const tinyxml2::XMLElement& behavior);
|
||||
|
||||
void Update(float deltaTime, ModelComponent& modelComponent);
|
||||
void OnChatMessageReceived(const std::string& sMessage);
|
||||
void OnHit();
|
||||
void OnChatMessageReceived(const std::string& sMessage, const LWOOBJID sender);
|
||||
void OnHit(const LWOOBJID attacker);
|
||||
|
||||
private:
|
||||
State& GetActiveState();
|
||||
|
||||
@@ -168,10 +168,10 @@ void State::Update(float deltaTime, ModelComponent& modelComponent, UpdateResult
|
||||
for (auto& strip : m_Strips) strip.Update(deltaTime, modelComponent, updateResult);
|
||||
}
|
||||
|
||||
void State::OnChatMessageReceived(const std::string& sMessage) {
|
||||
for (auto& strip : m_Strips) strip.OnChatMessageReceived(sMessage);
|
||||
void State::OnChatMessageReceived(const std::string& sMessage, const LWOOBJID sender) {
|
||||
for (auto& strip : m_Strips) strip.OnChatMessageReceived(sMessage, sender);
|
||||
}
|
||||
|
||||
void State::OnHit() {
|
||||
for (auto& strip : m_Strips) strip.OnHit();
|
||||
void State::OnHit(const LWOOBJID attacker) {
|
||||
for (auto& strip : m_Strips) strip.OnHit(attacker);
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ public:
|
||||
|
||||
void Update(float deltaTime, ModelComponent& modelComponent, UpdateResult& updateResult);
|
||||
|
||||
void OnChatMessageReceived(const std::string& sMessage);
|
||||
void OnHit();
|
||||
void OnChatMessageReceived(const std::string& sMessage, const LWOOBJID sender);
|
||||
void OnHit(const LWOOBJID attacker);
|
||||
private:
|
||||
|
||||
// The strips contained within this state.
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "Strip.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include "CombatMessages.h"
|
||||
|
||||
#include "Amf3.h"
|
||||
@@ -10,6 +12,7 @@
|
||||
#include "PropertyManagementComponent.h"
|
||||
#include "PlayerManager.h"
|
||||
#include "SimplePhysicsComponent.h"
|
||||
#include "DestroyableComponent.h"
|
||||
|
||||
#include "dChatFilter.h"
|
||||
|
||||
@@ -102,6 +105,7 @@ void Strip::HandleMsg(GameMessages::RequestUse& msg) {
|
||||
if (nextAction.GetType() == PropertyBehaviorActions::ON_INTERACT) {
|
||||
IncrementAction();
|
||||
m_WaitingForAction = false;
|
||||
m_StripInitiatorID = msg.target;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,27 +114,34 @@ void Strip::HandleMsg(GameMessages::ResetModelToDefaults& msg) {
|
||||
m_WaitingForAction = false;
|
||||
m_PausedTime = 0.0f;
|
||||
m_NextActionIndex = 0;
|
||||
m_InActionMove = NiPoint3Constant::ZERO;
|
||||
m_InActionTranslation = NiPoint3Constant::ZERO;
|
||||
m_PreviousFramePosition = NiPoint3Constant::ZERO;
|
||||
m_InActionRotation = NiPoint3Constant::ZERO;
|
||||
m_RotationProgress = 0.0f;
|
||||
m_Speed = DEFAULT_SPEED;
|
||||
m_PausedFromOnTimer = false;
|
||||
m_MovingToStart = false;
|
||||
}
|
||||
|
||||
void Strip::OnChatMessageReceived(const std::string& sMessage) {
|
||||
void Strip::OnChatMessageReceived(const std::string& sMessage, const LWOOBJID sender) {
|
||||
if (m_PausedTime > 0.0f || !HasMinimumActions()) return;
|
||||
|
||||
const auto& nextAction = GetNextAction();
|
||||
if (nextAction.GetType() == PropertyBehaviorActions::ON_CHAT && nextAction.GetValueParameterString() == sMessage) {
|
||||
IncrementAction();
|
||||
m_WaitingForAction = false;
|
||||
m_StripInitiatorID = sender;
|
||||
}
|
||||
}
|
||||
|
||||
void Strip::OnHit() {
|
||||
void Strip::OnHit(const LWOOBJID attacker) {
|
||||
if (m_PausedTime > 0.0f || !HasMinimumActions()) return;
|
||||
|
||||
const auto& nextAction = GetNextAction();
|
||||
if (nextAction.GetType() == PropertyBehaviorActions::ON_ATTACK) {
|
||||
IncrementAction();
|
||||
m_WaitingForAction = false;
|
||||
m_StripInitiatorID = attacker;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,19 +189,37 @@ void Strip::ProcNormalAction(float deltaTime, ModelComponent& modelComponent, Up
|
||||
namespace Actions = PropertyBehaviorActions;
|
||||
/* BEGIN Move */
|
||||
if (const auto* move = Actions::Find(Actions::MOVES, nextActionType)) {
|
||||
const auto& unit = move->axis == 'x' ? NiPoint3Constant::UNIT_X : move->axis == 'y' ? NiPoint3Constant::UNIT_Y : NiPoint3Constant::UNIT_Z;
|
||||
// Default velocity is 3 units per second.
|
||||
if (modelComponent.TrySetVelocity(unit * move->sign)) {
|
||||
// Local right (x), up (y) and forward (z) axes
|
||||
const int axis = move->axis == 'x' ? 0 : move->axis == 'y' ? 1 : 2;
|
||||
if (modelComponent.TryStartMove(axis, move->sign, m_Speed)) {
|
||||
m_PreviousFramePosition = entity.GetPosition();
|
||||
auto& distance = move->axis == 'x' ? m_InActionMove.x : move->axis == 'y' ? m_InActionMove.y : m_InActionMove.z;
|
||||
m_MoveInterruptCount = modelComponent.GetMoveInterruptCount();
|
||||
auto& distance = move->axis == 'x' ? m_InActionTranslation.x : move->axis == 'y' ? m_InActionTranslation.y : m_InActionTranslation.z;
|
||||
distance = move->sign < 0 ? -number : number;
|
||||
}
|
||||
}
|
||||
/* END Move */
|
||||
|
||||
/* BEGIN Rotate */
|
||||
else if (const auto* rotation = Actions::Find(Actions::ROTATIONS, nextActionType)) {
|
||||
// Local tilt (x), spin (y) and roll (z) axes
|
||||
const int axis = rotation->axis == 'x' ? 0 : rotation->axis == 'y' ? 1 : 2;
|
||||
if (number != 0.0 && modelComponent.TryStartRotation(axis, rotation->sign, m_Speed)) {
|
||||
m_MoveInterruptCount = modelComponent.GetMoveInterruptCount();
|
||||
auto& degrees = rotation->axis == 'x' ? m_InActionRotation.x : rotation->axis == 'y' ? m_InActionRotation.y : m_InActionRotation.z;
|
||||
degrees = rotation->sign * number;
|
||||
m_RotationProgress = 0.0f;
|
||||
}
|
||||
}
|
||||
/* END Rotate */
|
||||
|
||||
/* BEGIN Navigation */
|
||||
else if (nextActionType == Actions::SET_SPEED) {
|
||||
modelComponent.SetSpeed(number);
|
||||
// Floored so a move or rotation can never stall forever
|
||||
m_Speed = std::max(static_cast<float>(number), MIN_SPEED);
|
||||
} else if (nextActionType == Actions::MOVE_BACK_TO_START) {
|
||||
modelComponent.StartMoveTo(modelComponent.GetOriginalPosition(), m_Speed);
|
||||
m_MovingToStart = true;
|
||||
}
|
||||
/* END Navigation */
|
||||
|
||||
@@ -226,9 +255,9 @@ void Strip::ProcNormalAction(float deltaTime, ModelComponent& modelComponent, Up
|
||||
chatMessage.message = GeneralUtils::ASCIIToUTF16(valueStr);
|
||||
chatMessage.Broadcast();
|
||||
}
|
||||
PropertyManagementComponent::Instance()->OnChatMessageReceived(valueStr.data());
|
||||
PropertyManagementComponent::Instance()->OnChatMessageReceived(valueStr.data(), m_StripInitiatorID);
|
||||
} else if (nextActionType == Actions::PRIVATE_MESSAGE) {
|
||||
PropertyManagementComponent::Instance()->OnChatMessageReceived(valueStr.data());
|
||||
PropertyManagementComponent::Instance()->OnChatMessageReceived(valueStr.data(), m_StripInitiatorID);
|
||||
} else if (nextActionType == Actions::PLAY_SOUND) {
|
||||
GameMessages::PlayBehaviorSound sound;
|
||||
sound.target = modelComponent.GetParent()->GetObjectID();
|
||||
@@ -241,6 +270,8 @@ void Strip::ProcNormalAction(float deltaTime, ModelComponent& modelComponent, Up
|
||||
/* BEGIN Gameplay */
|
||||
else if (const auto* spawn = Actions::Find(Actions::SPAWNS, nextActionType)) {
|
||||
Spawn(spawn->lot, entity);
|
||||
} else if (nextActionType == Actions::DO_DAMAGE) {
|
||||
modelComponent.DoDamage(m_StripInitiatorID);
|
||||
} else if (const auto* drop = Actions::Find(Actions::DROPS, nextActionType)) {
|
||||
for (; numberAsInt > 0; numberAsInt--) SpawnDrop(drop->lot, entity);
|
||||
}
|
||||
@@ -277,49 +308,66 @@ void Strip::RemoveStates(ModelComponent& modelComponent) const {
|
||||
}
|
||||
|
||||
bool Strip::CheckMovement(float deltaTime, ModelComponent& modelComponent) {
|
||||
if (m_MovingToStart) {
|
||||
if (modelComponent.IsMovingToTarget()) return false;
|
||||
m_MovingToStart = false;
|
||||
}
|
||||
|
||||
// A MoveBackToStart cancelled our move, so skip to the next action
|
||||
if (m_MoveInterruptCount != modelComponent.GetMoveInterruptCount()) m_InActionTranslation = NiPoint3Constant::ZERO;
|
||||
|
||||
auto& entity = *modelComponent.GetParent();
|
||||
const auto& currentPos = entity.GetPosition();
|
||||
const auto diff = currentPos - m_PreviousFramePosition;
|
||||
const auto [moveX, moveY, moveZ] = m_InActionMove;
|
||||
m_PreviousFramePosition = currentPos;
|
||||
|
||||
// Only want to subtract from the move if one is being performed.
|
||||
// Starts at true because we may not be doing a move at all.
|
||||
// If one is being done, then one of the move_ variables will be non-zero
|
||||
bool moveFinished = true;
|
||||
NiPoint3 finalPositionAdjustment = NiPoint3Constant::ZERO;
|
||||
if (moveX != 0.0f) {
|
||||
m_InActionMove.x -= diff.x;
|
||||
// If the sign bit is different between the two numbers, then we have finished our move.
|
||||
moveFinished = std::signbit(m_InActionMove.x) != std::signbit(moveX);
|
||||
finalPositionAdjustment.x = m_InActionMove.x;
|
||||
} else if (moveY != 0.0f) {
|
||||
m_InActionMove.y -= diff.y;
|
||||
// If the sign bit is different between the two numbers, then we have finished our move.
|
||||
moveFinished = std::signbit(m_InActionMove.y) != std::signbit(moveY);
|
||||
finalPositionAdjustment.y = m_InActionMove.y;
|
||||
} else if (moveZ != 0.0f) {
|
||||
m_InActionMove.z -= diff.z;
|
||||
// If the sign bit is different between the two numbers, then we have finished our move.
|
||||
moveFinished = std::signbit(m_InActionMove.z) != std::signbit(moveZ);
|
||||
finalPositionAdjustment.z = m_InActionMove.z;
|
||||
}
|
||||
for (int axis = 0; axis < 3; axis++) {
|
||||
const float target = m_InActionTranslation[axis];
|
||||
if (target == 0.0f) continue;
|
||||
|
||||
// Once done, set the in action move & velocity to zero
|
||||
if (moveFinished && m_InActionMove != NiPoint3Constant::ZERO) {
|
||||
auto entityVelocity = entity.GetVelocity();
|
||||
// Zero out only the velocity that was acted on
|
||||
if (moveX != 0.0f) entityVelocity.x = 0.0f;
|
||||
else if (moveY != 0.0f) entityVelocity.y = 0.0f;
|
||||
else if (moveZ != 0.0f) entityVelocity.z = 0.0f;
|
||||
modelComponent.SetVelocity(entityVelocity);
|
||||
// The local axes are orthonormal so this isolates our axis from any other active moves
|
||||
const auto& axisVector = modelComponent.GetMoveAxis(axis);
|
||||
m_InActionTranslation[axis] -= diff.DotProduct(axisVector);
|
||||
|
||||
// If the sign bit is different between the two numbers, then we have finished our move.
|
||||
if (std::signbit(m_InActionTranslation[axis]) == std::signbit(target)) return false;
|
||||
|
||||
// Do the final adjustment so we will have moved exactly the requested units
|
||||
entity.SetPosition(entity.GetPosition() + finalPositionAdjustment);
|
||||
m_InActionMove = NiPoint3Constant::ZERO;
|
||||
entity.SetPosition(entity.GetPosition() + axisVector * m_InActionTranslation[axis]);
|
||||
modelComponent.StopMove(axis);
|
||||
m_InActionTranslation = NiPoint3Constant::ZERO;
|
||||
}
|
||||
|
||||
return moveFinished;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Strip::CheckRotation(float deltaTime, ModelComponent& modelComponent) {
|
||||
if (m_MoveInterruptCount != modelComponent.GetMoveInterruptCount()) {
|
||||
for (int axis = 0; axis < 3; axis++) {
|
||||
if (m_InActionRotation[axis] != 0.0f) modelComponent.StopRotation(axis);
|
||||
}
|
||||
m_InActionRotation = NiPoint3Constant::ZERO;
|
||||
m_RotationProgress = 0.0f;
|
||||
}
|
||||
|
||||
for (int axis = 0; axis < 3; axis++) {
|
||||
const float target = m_InActionRotation[axis];
|
||||
if (target == 0.0f) continue;
|
||||
|
||||
// Snapping to the target keeps the final angle exact regardless of speed or frame time
|
||||
const float step = modelComponent.GetAngularSpeed(axis) * deltaTime;
|
||||
if (std::abs(target - m_RotationProgress) <= step) m_RotationProgress = target;
|
||||
else m_RotationProgress += std::copysign(step, target);
|
||||
|
||||
modelComponent.SetRotationProgress(axis, m_RotationProgress);
|
||||
if (m_RotationProgress != target) return false;
|
||||
|
||||
modelComponent.StopRotation(axis);
|
||||
m_InActionRotation = NiPoint3Constant::ZERO;
|
||||
m_RotationProgress = 0.0f;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Strip::Update(float deltaTime, ModelComponent& modelComponent, UpdateResult& updateResult) {
|
||||
@@ -327,8 +375,9 @@ void Strip::Update(float deltaTime, ModelComponent& modelComponent, UpdateResult
|
||||
// Strips are also designed to have 2 actions or more to run.
|
||||
if (!HasMinimumActions()) return;
|
||||
|
||||
// Return if this strip has an active movement action
|
||||
// Return if this strip has an active movement or rotation action
|
||||
if (!CheckMovement(deltaTime, modelComponent)) return;
|
||||
if (!CheckRotation(deltaTime, modelComponent)) return;
|
||||
|
||||
// Don't run this strip if we're paused.
|
||||
m_PausedTime -= deltaTime;
|
||||
@@ -346,16 +395,35 @@ void Strip::Update(float deltaTime, ModelComponent& modelComponent, UpdateResult
|
||||
|
||||
// Check for trigger blocks and if not a trigger block proc this blocks action
|
||||
if (m_NextActionIndex == 0) {
|
||||
m_StripInitiatorID = LWOOBJID_EMPTY;
|
||||
LOG("Behavior strip started %s", nextAction.GetType().data());
|
||||
m_Speed = DEFAULT_SPEED;
|
||||
if (nextAction.GetType() == PropertyBehaviorActions::ON_INTERACT) {
|
||||
modelComponent.AddInteract();
|
||||
m_WaitingForAction = true;
|
||||
} else if (nextAction.GetType() == PropertyBehaviorActions::ON_CHAT) {
|
||||
// logic here if needed
|
||||
m_WaitingForAction = true;
|
||||
} else if (nextAction.GetType() == PropertyBehaviorActions::ON_ATTACK) {
|
||||
modelComponent.AddAttack();
|
||||
m_WaitingForAction = true;
|
||||
} else if (nextAction.GetType() == PropertyBehaviorActions::ON_STARTUP) {
|
||||
IncrementAction();
|
||||
} else if (nextAction.GetType() == PropertyBehaviorActions::ON_TIMER) {
|
||||
if (!m_PausedFromOnTimer) {
|
||||
m_PausedTime = nextAction.GetValueParameterDouble();
|
||||
m_PausedFromOnTimer = true;
|
||||
} else {
|
||||
IncrementAction();
|
||||
m_PausedFromOnTimer = false;
|
||||
}
|
||||
} else {
|
||||
// in case we run into an unimplemented action or one that isnt a start node
|
||||
// mark as waiting for action so we dont waste time re-starting the same logic and serializing
|
||||
// every frame
|
||||
m_WaitingForAction = true;
|
||||
}
|
||||
|
||||
Game::entityManager->SerializeEntity(entity);
|
||||
m_WaitingForAction = true;
|
||||
} else { // should be a normal block
|
||||
ProcNormalAction(deltaTime, modelComponent, updateResult);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,10 @@ public:
|
||||
// Checks the movement logic for whether or not to proceed
|
||||
// Returns true if the movement can continue, false if it needs to wait more.
|
||||
bool CheckMovement(float deltaTime, ModelComponent& modelComponent);
|
||||
|
||||
// Checks the rotation logic for whether or not to proceed
|
||||
// Returns true if the rotation can continue, false if it needs to wait more.
|
||||
bool CheckRotation(float deltaTime, ModelComponent& modelComponent);
|
||||
void Update(float deltaTime, ModelComponent& modelComponent, UpdateResult& updateResult);
|
||||
void SpawnDrop(LOT dropLOT, Entity& entity);
|
||||
void ProcNormalAction(float deltaTime, ModelComponent& modelComponent, UpdateResult& updateResult);
|
||||
@@ -42,8 +46,8 @@ public:
|
||||
// 2 actions are required for strips to work
|
||||
bool HasMinimumActions() const { return m_Actions.size() >= 2; }
|
||||
|
||||
void OnChatMessageReceived(const std::string& sMessage);
|
||||
void OnHit();
|
||||
void OnChatMessageReceived(const std::string& sMessage, const LWOOBJID sender);
|
||||
void OnHit(const LWOOBJID attacker);
|
||||
private:
|
||||
// Indicates this Strip is waiting for an action to be taken upon it to progress to its actions
|
||||
bool m_WaitingForAction{ false };
|
||||
@@ -51,6 +55,7 @@ private:
|
||||
// The amount of time this strip is paused for. Any interactions with this strip should be bounced if this is greater than 0.
|
||||
// Actions that do not use time do not use this (ex. positions).
|
||||
float m_PausedTime{ 0.0f };
|
||||
bool m_PausedFromOnTimer{ false };
|
||||
|
||||
// The index of the next action to be played. This should always be within range of [0, m_Actions.size()).
|
||||
size_t m_NextActionIndex{ 0 };
|
||||
@@ -61,14 +66,32 @@ private:
|
||||
// The location of this strip on the UGBehaviorEditor UI
|
||||
StripUiPosition m_Position;
|
||||
|
||||
// The current actions remaining distance to the target
|
||||
// The current actions remaining translation to the target along the model's local right (x), up (y) and forward (z) axes.
|
||||
// Only 1 of these vertexs' will be active at once for any given strip.
|
||||
NiPoint3 m_InActionMove{};
|
||||
NiPoint3 m_InActionTranslation{};
|
||||
|
||||
// The position of the parent model on the previous frame
|
||||
NiPoint3 m_PreviousFramePosition{};
|
||||
|
||||
NiPoint3 m_SavedVelocity{};
|
||||
// The signed target degrees of the current rotation action. Only 1 axis is active at once for any given strip.
|
||||
NiPoint3 m_InActionRotation{};
|
||||
|
||||
// The signed degrees the current rotation action has progressed so far
|
||||
float m_RotationProgress{ 0.0f };
|
||||
|
||||
// Whether this strip is waiting on a MoveBackToStart to arrive
|
||||
bool m_MovingToStart{ false };
|
||||
|
||||
// The model's move interrupt count when this strip's current move or rotation started
|
||||
uint32_t m_MoveInterruptCount{};
|
||||
|
||||
static constexpr float DEFAULT_SPEED = 3.0f;
|
||||
static constexpr float MIN_SPEED = 0.1f;
|
||||
|
||||
// Speed applied to moves and rotations started by this strip
|
||||
float m_Speed{ DEFAULT_SPEED };
|
||||
|
||||
LWOOBJID m_StripInitiatorID{ LWOOBJID_EMPTY };
|
||||
};
|
||||
|
||||
#endif //!__STRIP__H__
|
||||
|
||||
@@ -1062,6 +1062,25 @@ void SlashCommandHandler::Startup() {
|
||||
};
|
||||
RegisterCommand(ExecuteCommand);
|
||||
|
||||
Command GetSceneCommand{
|
||||
.help = "Get the current scene ID and name at your position",
|
||||
.info = "Displays the scene ID and name at the player's current position. Scenes do not care about height.",
|
||||
.aliases = { "getscene", "scene" },
|
||||
.handle = DEVGMCommands::GetScene,
|
||||
.requiredLevel = eGameMasterLevel::DEVELOPER
|
||||
};
|
||||
RegisterCommand(GetSceneCommand);
|
||||
|
||||
Command GetAdjacentScenesCommand{
|
||||
.help = "Get all scenes adjacent to your current scene",
|
||||
.info = "Displays all scenes that are directly connected to the player's current scene via scene transitions.",
|
||||
.aliases = { "getadjacentscenes", "adjacentscenes" },
|
||||
.handle = DEVGMCommands::GetAdjacentScenes,
|
||||
.requiredLevel = eGameMasterLevel::DEVELOPER
|
||||
};
|
||||
RegisterCommand(GetAdjacentScenesCommand);
|
||||
|
||||
|
||||
// Register Greater Than Zero Commands
|
||||
|
||||
Command KickCommand{
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "dServer.h"
|
||||
#include "dpShapeSphere.h"
|
||||
#include "dZoneManager.h"
|
||||
#include <sstream>
|
||||
#include "EntityInfo.h"
|
||||
#include "Metrics.h"
|
||||
#include "PlayerManager.h"
|
||||
@@ -2005,4 +2006,81 @@ namespace DEVGMCommands {
|
||||
ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::UTF8ToUTF16("Making " + std::to_string(queued) + " model(s) again" +
|
||||
(options.empty() ? "" : " with " + options) + ". Everyone on the property is reloaded when they're made."));
|
||||
}
|
||||
|
||||
void GetScene(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
|
||||
const auto position = entity->GetPosition();
|
||||
|
||||
// Get the scene ID from the zone manager
|
||||
const auto sceneID = Game::zoneManager->GetSceneIDFromPosition(position);
|
||||
|
||||
if (sceneID == LWOSCENEID_INVALID) {
|
||||
ChatPackets::SendSystemMessage(sysAddr, u"No scene found at current position.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the scene reference from the zone to get the name
|
||||
const auto* zone = Game::zoneManager->GetZone();
|
||||
if (!zone) {
|
||||
ChatPackets::SendSystemMessage(sysAddr, u"No zone loaded.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Build the feedback message
|
||||
std::ostringstream feedback;
|
||||
feedback << "Scene ID: " << sceneID.GetSceneID();
|
||||
feedback << " (Layer: " << sceneID.GetLayerID() << ")";
|
||||
|
||||
// Get the scene name
|
||||
const auto* sceneRef = zone->GetScene(sceneID);
|
||||
if (sceneRef && !sceneRef->name.empty()) {
|
||||
feedback << " - Name: " << sceneRef->name;
|
||||
}
|
||||
|
||||
ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(feedback.str()));
|
||||
}
|
||||
|
||||
void GetAdjacentScenes(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
|
||||
const auto position = entity->GetPosition();
|
||||
|
||||
// Get the scene ID from the zone manager
|
||||
const auto sceneID = Game::zoneManager->GetSceneIDFromPosition(position);
|
||||
|
||||
if (sceneID == LWOSCENEID_INVALID) {
|
||||
ChatPackets::SendSystemMessage(sysAddr, u"No scene found at current position.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the zone reference
|
||||
const auto* zone = Game::zoneManager->GetZone();
|
||||
if (!zone) {
|
||||
ChatPackets::SendSystemMessage(sysAddr, u"No zone loaded.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current scene info
|
||||
const auto* currentScene = zone->GetScene(sceneID);
|
||||
std::string currentSceneName = currentScene && !currentScene->name.empty() ? currentScene->name : "Unknown";
|
||||
|
||||
// Get adjacent scenes
|
||||
const auto adjacentSceneIDs = Game::zoneManager->GetAdjacentScenes(sceneID);
|
||||
|
||||
std::ostringstream feedback;
|
||||
feedback << "Current Scene: " << sceneID.GetSceneID() << " (" << currentSceneName << ")";
|
||||
if (adjacentSceneIDs.empty()) {
|
||||
feedback << " - No adjacent scenes found.";
|
||||
} else {
|
||||
feedback << "\nAdjacent Scenes (" << adjacentSceneIDs.size() << "):";
|
||||
for (const auto& adjSceneID : adjacentSceneIDs) {
|
||||
feedback << "\n - Scene ID: " << adjSceneID.GetSceneID() << " (Layer: " << adjSceneID.GetLayerID() << ")";
|
||||
|
||||
// Get the scene name if available
|
||||
const auto* sceneRef = zone->GetScene(adjSceneID);
|
||||
if (sceneRef && !sceneRef->name.empty()) {
|
||||
feedback << " - " << sceneRef->name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(feedback.str()));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -78,6 +78,8 @@ namespace DEVGMCommands {
|
||||
void Barfight(Entity* entity, const SystemAddress& sysAddr, const std::string args);
|
||||
void Despawn(Entity* entity, const SystemAddress& sysAddr, const std::string args);
|
||||
void Execute(Entity* entity, const SystemAddress& sysAddr, const std::string args);
|
||||
void GetScene(Entity* entity, const SystemAddress& sysAddr, const std::string args);
|
||||
void GetAdjacentScenes(Entity* entity, const SystemAddress& sysAddr, const std::string args);
|
||||
}
|
||||
|
||||
#endif //!DEVGMCOMMANDS_H
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
set(DNAVIGATION_SOURCES "dNavMesh.cpp")
|
||||
|
||||
add_subdirectory(dTerrain)
|
||||
|
||||
foreach(file ${DNAVIGATIONS_DTERRAIN_SOURCES})
|
||||
set(DNAVIGATION_SOURCES ${DNAVIGATION_SOURCES} "dTerrain/${file}")
|
||||
endforeach()
|
||||
|
||||
add_library(dNavigation OBJECT ${DNAVIGATION_SOURCES})
|
||||
target_include_directories(dNavigation PUBLIC "."
|
||||
PRIVATE
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "dNavMesh.h"
|
||||
|
||||
#include "RawFile.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Logger.h"
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
set(DNAVIGATIONS_DTERRAIN_SOURCES "RawFile.cpp"
|
||||
"RawChunk.cpp"
|
||||
"RawHeightMap.cpp" PARENT_SCOPE)
|
||||
@@ -1,93 +0,0 @@
|
||||
#include "RawChunk.h"
|
||||
|
||||
#include "BinaryIO.h"
|
||||
|
||||
#include "RawMesh.h"
|
||||
#include "RawHeightMap.h"
|
||||
|
||||
RawChunk::RawChunk(std::ifstream& stream) {
|
||||
// Read the chunk index and info
|
||||
|
||||
BinaryIO::BinaryRead(stream, m_ChunkIndex);
|
||||
BinaryIO::BinaryRead(stream, m_Width);
|
||||
BinaryIO::BinaryRead(stream, m_Height);
|
||||
BinaryIO::BinaryRead(stream, m_X);
|
||||
BinaryIO::BinaryRead(stream, m_Z);
|
||||
|
||||
m_HeightMap = new RawHeightMap(stream, m_Height, m_Width);
|
||||
|
||||
// We can just skip the rest of the data so we can read the next chunks, we don't need anymore data
|
||||
|
||||
// Skips are relative and done in 64 bit math so large sizes can't wrap around and seek backwards.
|
||||
uint32_t colorMapSize;
|
||||
BinaryIO::BinaryRead(stream, colorMapSize);
|
||||
stream.seekg(static_cast<std::streamoff>(colorMapSize) * colorMapSize * 4, std::ios::cur);
|
||||
|
||||
uint32_t lightmapSize;
|
||||
BinaryIO::BinaryRead(stream, lightmapSize);
|
||||
stream.seekg(static_cast<std::streamoff>(lightmapSize), std::ios::cur);
|
||||
|
||||
uint32_t colorMapSize2;
|
||||
BinaryIO::BinaryRead(stream, colorMapSize2);
|
||||
stream.seekg(static_cast<std::streamoff>(colorMapSize2) * colorMapSize2 * 4, std::ios::cur);
|
||||
|
||||
uint8_t unknown;
|
||||
BinaryIO::BinaryRead(stream, unknown);
|
||||
|
||||
uint32_t blendmapSize;
|
||||
BinaryIO::BinaryRead(stream, blendmapSize);
|
||||
stream.seekg(static_cast<std::streamoff>(blendmapSize), std::ios::cur);
|
||||
|
||||
uint32_t pointSize;
|
||||
BinaryIO::BinaryRead(stream, pointSize);
|
||||
stream.seekg(static_cast<std::streamoff>(pointSize) * 9 * 4, std::ios::cur);
|
||||
|
||||
stream.seekg(static_cast<std::streamoff>(colorMapSize) * colorMapSize, std::ios::cur);
|
||||
|
||||
uint32_t endCounter;
|
||||
BinaryIO::BinaryRead(stream, endCounter);
|
||||
stream.seekg(static_cast<std::streamoff>(endCounter) * 2, std::ios::cur);
|
||||
|
||||
if (endCounter != 0) {
|
||||
stream.seekg(32, std::ios::cur);
|
||||
|
||||
for (int i = 0; i < 0x10; i++) {
|
||||
uint16_t finalCountdown;
|
||||
BinaryIO::BinaryRead(stream, finalCountdown);
|
||||
stream.seekg(static_cast<std::streamoff>(finalCountdown) * 2, std::ios::cur);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate our mesh/geo data for this chunk
|
||||
|
||||
this->GenerateMesh();
|
||||
}
|
||||
|
||||
RawChunk::~RawChunk() {
|
||||
if (m_Mesh) delete m_Mesh;
|
||||
if (m_HeightMap) delete m_HeightMap;
|
||||
}
|
||||
|
||||
void RawChunk::GenerateMesh() {
|
||||
RawMesh* meshData = new RawMesh();
|
||||
|
||||
for (int i = 0; i < m_Width; ++i) {
|
||||
for (int j = 0; j < m_Height; ++j) {
|
||||
float y = *std::next(m_HeightMap->m_FloatMap.begin(), m_Width * i + j);
|
||||
|
||||
meshData->m_Vertices.push_back(NiPoint3(i, y, j));
|
||||
|
||||
if (i == 0 || j == 0) continue;
|
||||
|
||||
meshData->m_Triangles.push_back(m_Width * i + j);
|
||||
meshData->m_Triangles.push_back(m_Width * i + j - 1);
|
||||
meshData->m_Triangles.push_back(m_Width * (i - 1) + j - 1);
|
||||
|
||||
meshData->m_Triangles.push_back(m_Width * (i - 1) + j - 1);
|
||||
meshData->m_Triangles.push_back(m_Width * (i - 1) + j);
|
||||
meshData->m_Triangles.push_back(m_Width * i + j);
|
||||
}
|
||||
}
|
||||
|
||||
m_Mesh = meshData;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
|
||||
struct RawMesh;
|
||||
class RawHeightMap;
|
||||
|
||||
class RawChunk {
|
||||
public:
|
||||
RawChunk(std::ifstream& stream);
|
||||
~RawChunk();
|
||||
|
||||
void GenerateMesh();
|
||||
|
||||
uint32_t m_ChunkIndex;
|
||||
uint32_t m_Width;
|
||||
uint32_t m_Height;
|
||||
float m_X;
|
||||
float m_Z;
|
||||
|
||||
RawHeightMap* m_HeightMap;
|
||||
RawMesh* m_Mesh;
|
||||
};
|
||||
@@ -1,84 +0,0 @@
|
||||
#include "RawFile.h"
|
||||
|
||||
#include "BinaryIO.h"
|
||||
#include "RawChunk.h"
|
||||
#include "RawMesh.h"
|
||||
#include "RawHeightMap.h"
|
||||
|
||||
RawFile::RawFile(std::string fileName) {
|
||||
if (!BinaryIO::DoesFileExist(fileName)) return;
|
||||
|
||||
std::ifstream file(fileName, std::ios::binary);
|
||||
|
||||
// Read header
|
||||
|
||||
BinaryIO::BinaryRead(file, m_Version);
|
||||
BinaryIO::BinaryRead(file, m_Padding);
|
||||
BinaryIO::BinaryRead(file, m_ChunkCount);
|
||||
BinaryIO::BinaryRead(file, m_Width);
|
||||
BinaryIO::BinaryRead(file, m_Height);
|
||||
|
||||
|
||||
if (m_Version < 0x20) {
|
||||
return; // Version is too old to be supported
|
||||
}
|
||||
|
||||
// Read in chunks
|
||||
|
||||
m_Chunks = {};
|
||||
|
||||
for (uint32_t i = 0; i < m_ChunkCount; i++) {
|
||||
RawChunk* chunk = new RawChunk(file);
|
||||
m_Chunks.push_back(chunk);
|
||||
}
|
||||
|
||||
m_FinalMesh = new RawMesh();
|
||||
|
||||
this->GenerateFinalMeshFromChunks();
|
||||
}
|
||||
|
||||
RawFile::~RawFile() {
|
||||
if (m_FinalMesh) delete m_FinalMesh;
|
||||
for (const auto* item : m_Chunks) {
|
||||
if (item) delete item;
|
||||
}
|
||||
}
|
||||
|
||||
void RawFile::GenerateFinalMeshFromChunks() {
|
||||
uint32_t lenOfLastChunk = 0; // index of last vert set in the last chunk
|
||||
|
||||
for (const auto& chunk : m_Chunks) {
|
||||
for (const auto& vert : chunk->m_Mesh->m_Vertices) {
|
||||
auto tempVert = vert;
|
||||
|
||||
// Scale X and Z by the chunk's position in the world
|
||||
// Scale Y by the chunk's heightmap scale factor
|
||||
tempVert.SetX(tempVert.GetX() + (chunk->m_X / chunk->m_HeightMap->m_ScaleFactor));
|
||||
tempVert.SetY(tempVert.GetY() / chunk->m_HeightMap->m_ScaleFactor);
|
||||
tempVert.SetZ(tempVert.GetZ() + (chunk->m_Z / chunk->m_HeightMap->m_ScaleFactor));
|
||||
|
||||
// Then scale it again for some reason
|
||||
tempVert *= chunk->m_HeightMap->m_ScaleFactor;
|
||||
|
||||
m_FinalMesh->m_Vertices.push_back(tempVert);
|
||||
}
|
||||
|
||||
for (const auto& tri : chunk->m_Mesh->m_Triangles) {
|
||||
m_FinalMesh->m_Triangles.push_back(tri + lenOfLastChunk);
|
||||
}
|
||||
|
||||
lenOfLastChunk += chunk->m_Mesh->m_Vertices.size();
|
||||
}
|
||||
}
|
||||
|
||||
void RawFile::WriteFinalMeshToOBJ(std::string path) {
|
||||
std::ofstream file(path);
|
||||
|
||||
for (const auto& v : m_FinalMesh->m_Vertices) {
|
||||
file << "v " << v.x << ' ' << v.y << ' ' << v.z << '\n';
|
||||
}
|
||||
|
||||
for (int i = 0; i < m_FinalMesh->m_Triangles.size(); i += 3) {
|
||||
file << "f " << *std::next(m_FinalMesh->m_Triangles.begin(), i) + 1 << ' ' << *std::next(m_FinalMesh->m_Triangles.begin(), i + 1) + 1 << ' ' << *std::next(m_FinalMesh->m_Triangles.begin(), i + 2) + 1 << '\n';
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
class RawChunk;
|
||||
struct RawMesh;
|
||||
|
||||
class RawFile {
|
||||
public:
|
||||
RawFile(std::string filePath);
|
||||
~RawFile();
|
||||
|
||||
private:
|
||||
|
||||
void GenerateFinalMeshFromChunks();
|
||||
void WriteFinalMeshToOBJ(std::string path);
|
||||
|
||||
uint8_t m_Version;
|
||||
uint16_t m_Padding;
|
||||
uint32_t m_ChunkCount;
|
||||
uint32_t m_Width;
|
||||
uint32_t m_Height;
|
||||
|
||||
std::vector<RawChunk*> m_Chunks;
|
||||
RawMesh* m_FinalMesh = nullptr;
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
#include "RawHeightMap.h"
|
||||
|
||||
#include "BinaryIO.h"
|
||||
|
||||
RawHeightMap::RawHeightMap() {}
|
||||
|
||||
RawHeightMap::RawHeightMap(std::ifstream& stream, float height, float width) {
|
||||
// Read in height map data header and scale
|
||||
|
||||
BinaryIO::BinaryRead(stream, m_Unknown1);
|
||||
BinaryIO::BinaryRead(stream, m_Unknown2);
|
||||
BinaryIO::BinaryRead(stream, m_Unknown3);
|
||||
BinaryIO::BinaryRead(stream, m_Unknown4);
|
||||
BinaryIO::BinaryRead(stream, m_ScaleFactor);
|
||||
|
||||
// read all vertices in
|
||||
|
||||
for (uint64_t i = 0; i < width * height; i++) {
|
||||
float value;
|
||||
BinaryIO::BinaryRead(stream, value);
|
||||
m_FloatMap.push_back(value);
|
||||
}
|
||||
}
|
||||
|
||||
RawHeightMap::~RawHeightMap() {
|
||||
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <fstream>
|
||||
|
||||
class RawHeightMap {
|
||||
public:
|
||||
RawHeightMap();
|
||||
RawHeightMap(std::ifstream& stream, float height, float width);
|
||||
~RawHeightMap();
|
||||
|
||||
uint32_t m_Unknown1;
|
||||
uint32_t m_Unknown2;
|
||||
uint32_t m_Unknown3;
|
||||
uint32_t m_Unknown4;
|
||||
|
||||
float m_ScaleFactor;
|
||||
|
||||
std::vector<float> m_FloatMap = {};
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "NiPoint3.h"
|
||||
|
||||
struct RawMesh {
|
||||
std::vector<NiPoint3> m_Vertices;
|
||||
std::vector<uint32_t> m_Triangles;
|
||||
};
|
||||
@@ -1727,7 +1727,7 @@ namespace {
|
||||
generalChat.senderID = user->GetLoggedInChar();
|
||||
generalChat.message = message;
|
||||
generalChat.Broadcast();
|
||||
if (PropertyManagementComponent::Instance()) PropertyManagementComponent::Instance()->OnChatMessageReceived(sMessage);
|
||||
if (PropertyManagementComponent::Instance()) PropertyManagementComponent::Instance()->OnChatMessageReceived(sMessage, user->GetLoggedInChar());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
set(DZONEMANAGER_SOURCES "dZoneManager.cpp"
|
||||
"Level.cpp"
|
||||
"Raw.cpp"
|
||||
"Spawner.cpp"
|
||||
"Zone.cpp")
|
||||
|
||||
@@ -15,6 +16,7 @@ target_include_directories(dZoneManager PUBLIC "."
|
||||
"${PROJECT_SOURCE_DIR}/dGame/dEntity" # EntityInfo.h
|
||||
PRIVATE
|
||||
${DLU_COMPONENT_INCLUDE_DIRS} #InventoryComponent.h
|
||||
"${PROJECT_SOURCE_DIR}/dCommon/dClient" # SceneColor.h
|
||||
"${PROJECT_SOURCE_DIR}/dGame/dInventory" #InventoryComponent.h (transitive)
|
||||
"${PROJECT_SOURCE_DIR}/dGame/dBehaviors" #BehaviorSlot.h
|
||||
"${PROJECT_SOURCE_DIR}/dGame/dGameMessages" #GameMessages.h
|
||||
|
||||
573
dZoneManager/Raw.cpp
Normal file
573
dZoneManager/Raw.cpp
Normal file
@@ -0,0 +1,573 @@
|
||||
#include "Raw.h"
|
||||
#include "BinaryIO.h"
|
||||
#include "Logger.h"
|
||||
#include "SceneColor.h"
|
||||
#include <fstream>
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
constexpr uint32_t kMaxResolution = 4096;
|
||||
constexpr size_t kMaxBlobBytes = 64ULL * 1024 * 1024; // 64 MiB
|
||||
constexpr uint32_t kMaxChunks = 1024;
|
||||
} // namespace
|
||||
|
||||
namespace Raw {
|
||||
|
||||
bool Chunk::IsValidForSceneLookup() const {
|
||||
return !sceneMap.empty() && colorMapResolution > 0 && !heightMap.empty()
|
||||
&& scaleFactor > 0.0f && width > 1 && height > 1;
|
||||
}
|
||||
|
||||
uint8_t Chunk::GetSceneIDAtLocal(float localX, float localZ) const {
|
||||
// The nearest scene map cell
|
||||
const auto toCell = [this](float cell) {
|
||||
return static_cast<uint32_t>(std::clamp(static_cast<int32_t>(std::floor(cell + 0.5f)), 0, static_cast<int32_t>(colorMapResolution) - 1));
|
||||
};
|
||||
const uint32_t sceneIndex = toCell(sceneCellsPerUnitX * localX) * colorMapResolution + toCell(sceneCellsPerUnitZ * localZ);
|
||||
if (sceneIndex >= sceneMap.size()) return 0;
|
||||
return sceneMap[sceneIndex];
|
||||
}
|
||||
|
||||
uint8_t Chunk::GetSceneIDAtGrid(uint32_t i, uint32_t j) const {
|
||||
return GetSceneIDAtLocal(static_cast<float>(i) * scaleFactor, static_cast<float>(j) * scaleFactor);
|
||||
}
|
||||
|
||||
NiPoint3 Chunk::GridToWorldPos(uint32_t i, uint32_t j) const {
|
||||
const uint32_t heightIndex = i * height + j;
|
||||
const float y = heightIndex < heightMap.size() ? heightMap[heightIndex] : 0.0f;
|
||||
return NiPoint3(offsetX + static_cast<float>(i) * scaleFactor, y, offsetZ + static_cast<float>(j) * scaleFactor);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Read flair attributes from stream
|
||||
*/
|
||||
static bool ReadFlairAttributes(std::istream& stream, FlairAttributes& flair) {
|
||||
try {
|
||||
BinaryIO::BinaryRead(stream, flair.id);
|
||||
BinaryIO::BinaryRead(stream, flair.scaleFactor);
|
||||
BinaryIO::BinaryRead(stream, flair.position.x);
|
||||
BinaryIO::BinaryRead(stream, flair.position.y);
|
||||
BinaryIO::BinaryRead(stream, flair.position.z);
|
||||
BinaryIO::BinaryRead(stream, flair.rotation.x);
|
||||
BinaryIO::BinaryRead(stream, flair.rotation.y);
|
||||
BinaryIO::BinaryRead(stream, flair.rotation.z);
|
||||
BinaryIO::BinaryRead(stream, flair.colorR);
|
||||
BinaryIO::BinaryRead(stream, flair.colorG);
|
||||
BinaryIO::BinaryRead(stream, flair.colorB);
|
||||
BinaryIO::BinaryRead(stream, flair.padding);
|
||||
return true;
|
||||
} catch (const std::exception&) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Read mesh triangle data from stream
|
||||
*/
|
||||
static bool ReadMeshTri(std::istream& stream, MeshTri& meshTri) {
|
||||
try {
|
||||
BinaryIO::BinaryRead(stream, meshTri.meshTriListSize);
|
||||
meshTri.meshTriList.resize(meshTri.meshTriListSize);
|
||||
for (uint16_t i = 0; i < meshTri.meshTriListSize; ++i) {
|
||||
BinaryIO::BinaryRead(stream, meshTri.meshTriList[i]);
|
||||
}
|
||||
return true;
|
||||
} catch (const std::exception&) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Read a chunk from stream
|
||||
*/
|
||||
static bool ReadChunkData(std::istream& stream, Chunk& chunk, uint16_t version) {
|
||||
try {
|
||||
// Read basic chunk info
|
||||
BinaryIO::BinaryRead(stream, chunk.id);
|
||||
if (stream.fail()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
BinaryIO::BinaryRead(stream, chunk.width);
|
||||
BinaryIO::BinaryRead(stream, chunk.height);
|
||||
BinaryIO::BinaryRead(stream, chunk.offsetX);
|
||||
BinaryIO::BinaryRead(stream, chunk.offsetZ);
|
||||
|
||||
if (stream.fail()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For version < 32, shader ID comes before texture IDs
|
||||
if (version < 32) {
|
||||
BinaryIO::BinaryRead(stream, chunk.shaderId);
|
||||
}
|
||||
|
||||
// Read texture IDs (4 textures)
|
||||
chunk.textureIds.resize(4);
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
BinaryIO::BinaryRead(stream, chunk.textureIds[i]);
|
||||
}
|
||||
|
||||
if (stream.fail()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read scale factor
|
||||
BinaryIO::BinaryRead(stream, chunk.scaleFactor);
|
||||
|
||||
if (stream.fail()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read heightmap: width * height floats, at most kMaxResolution^2 (64 MiB)
|
||||
if (chunk.width == 0 || chunk.height == 0 || chunk.width > kMaxResolution || chunk.height > kMaxResolution) {
|
||||
LOG("Chunk %u has invalid heightmap dimensions: width=%u, height=%u (max %u)", chunk.id, chunk.width, chunk.height, kMaxResolution);
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t heightMapSize = static_cast<size_t>(chunk.width) * chunk.height;
|
||||
chunk.heightMap.resize(heightMapSize);
|
||||
for (size_t i = 0; i < heightMapSize; ++i) {
|
||||
BinaryIO::BinaryRead(stream, chunk.heightMap[i]);
|
||||
}
|
||||
|
||||
if (stream.fail()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ColorMap
|
||||
if (version >= 32) {
|
||||
BinaryIO::BinaryRead(stream, chunk.colorMapResolution);
|
||||
} else {
|
||||
chunk.colorMapResolution = chunk.width - 1;
|
||||
}
|
||||
|
||||
if (chunk.colorMapResolution > kMaxResolution) {
|
||||
LOG("Chunk colorMapResolution %u exceeds maximum %u", chunk.colorMapResolution, kMaxResolution);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (version >= 32) {
|
||||
const size_t colorMapPixelCount = static_cast<size_t>(chunk.colorMapResolution) * chunk.colorMapResolution * 4;
|
||||
if (colorMapPixelCount > kMaxBlobBytes) {
|
||||
LOG("Chunk colorMap size %zu exceeds maximum %zu bytes", colorMapPixelCount, kMaxBlobBytes);
|
||||
return false;
|
||||
}
|
||||
chunk.colorMap.resize(colorMapPixelCount);
|
||||
stream.read(reinterpret_cast<char*>(chunk.colorMap.data()), static_cast<std::streamsize>(colorMapPixelCount));
|
||||
} else {
|
||||
const size_t legacyColorBytes = static_cast<size_t>(chunk.width) * chunk.width * 4;
|
||||
if (legacyColorBytes > kMaxBlobBytes) {
|
||||
LOG("Chunk legacy colorMap size %zu exceeds maximum %zu bytes", legacyColorBytes, kMaxBlobBytes);
|
||||
return false;
|
||||
}
|
||||
chunk.colorMap.resize(legacyColorBytes);
|
||||
stream.read(reinterpret_cast<char*>(chunk.colorMap.data()), static_cast<std::streamsize>(legacyColorBytes));
|
||||
}
|
||||
|
||||
if (stream.fail()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// LightMap/diffusemap.dds (v>=32 only)
|
||||
if (version >= 32) {
|
||||
uint32_t lightMapSize;
|
||||
BinaryIO::BinaryRead(stream, lightMapSize);
|
||||
|
||||
if (lightMapSize > kMaxBlobBytes) {
|
||||
LOG("Chunk lightMap size %u exceeds maximum %zu bytes", lightMapSize, kMaxBlobBytes);
|
||||
return false;
|
||||
}
|
||||
chunk.lightMap.resize(lightMapSize);
|
||||
stream.read(reinterpret_cast<char*>(chunk.lightMap.data()), static_cast<std::streamsize>(lightMapSize));
|
||||
|
||||
if (stream.fail()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Blend/texture map
|
||||
BinaryIO::BinaryRead(stream, chunk.textureMapResolution);
|
||||
|
||||
if (chunk.textureMapResolution > kMaxResolution) {
|
||||
LOG("Chunk textureMapResolution %u exceeds maximum %u", chunk.textureMapResolution, kMaxResolution);
|
||||
return false;
|
||||
}
|
||||
const size_t textureMapPixelCount = static_cast<size_t>(chunk.textureMapResolution) * chunk.textureMapResolution * 4;
|
||||
if (textureMapPixelCount > kMaxBlobBytes) {
|
||||
LOG("Chunk textureMap size %zu exceeds maximum %zu bytes", textureMapPixelCount, kMaxBlobBytes);
|
||||
return false;
|
||||
}
|
||||
chunk.textureMap.resize(textureMapPixelCount);
|
||||
stream.read(reinterpret_cast<char*>(chunk.textureMap.data()), static_cast<std::streamsize>(textureMapPixelCount));
|
||||
|
||||
if (stream.fail()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Texture settings + blend map DDS (v>=32 only)
|
||||
if (version >= 32) {
|
||||
BinaryIO::BinaryRead(stream, chunk.textureSettings);
|
||||
|
||||
uint32_t blendMapDDSSize;
|
||||
BinaryIO::BinaryRead(stream, blendMapDDSSize);
|
||||
|
||||
if (blendMapDDSSize > kMaxBlobBytes) {
|
||||
LOG("Chunk blendMap size %u exceeds maximum %zu bytes", blendMapDDSSize, kMaxBlobBytes);
|
||||
return false;
|
||||
}
|
||||
chunk.blendMap.resize(blendMapDDSSize);
|
||||
stream.read(reinterpret_cast<char*>(chunk.blendMap.data()), static_cast<std::streamsize>(blendMapDDSSize));
|
||||
|
||||
if (stream.fail()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Read flairs
|
||||
uint32_t numFlairs;
|
||||
BinaryIO::BinaryRead(stream, numFlairs);
|
||||
|
||||
if (stream.fail()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t flairBytes = static_cast<size_t>(numFlairs) * sizeof(FlairAttributes);
|
||||
if (flairBytes > kMaxBlobBytes) {
|
||||
LOG("Chunk %u flair count %u exceeds maximum (byte size %zu > %zu)", chunk.id, numFlairs, flairBytes, kMaxBlobBytes);
|
||||
return false;
|
||||
}
|
||||
chunk.flairs.resize(numFlairs);
|
||||
for (uint32_t i = 0; i < numFlairs; ++i) {
|
||||
if (!ReadFlairAttributes(stream, chunk.flairs[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Scene map
|
||||
if (version >= 32) {
|
||||
const size_t sceneMapSize = static_cast<size_t>(chunk.colorMapResolution) * chunk.colorMapResolution;
|
||||
|
||||
if (sceneMapSize > kMaxBlobBytes) {
|
||||
LOG("Chunk sceneMap size %zu exceeds maximum %zu bytes", sceneMapSize, kMaxBlobBytes);
|
||||
return false;
|
||||
}
|
||||
chunk.sceneMap.resize(sceneMapSize);
|
||||
stream.read(reinterpret_cast<char*>(chunk.sceneMap.data()), static_cast<std::streamsize>(sceneMapSize));
|
||||
} else {
|
||||
const size_t sceneMapCells = static_cast<size_t>(chunk.colorMapResolution + 1) * (chunk.colorMapResolution + 1);
|
||||
if (sceneMapCells > kMaxBlobBytes) {
|
||||
LOG("Chunk legacy sceneMap size %zu exceeds maximum %zu bytes", sceneMapCells, kMaxBlobBytes);
|
||||
return false;
|
||||
}
|
||||
chunk.sceneMapLegacy.resize(sceneMapCells);
|
||||
stream.read(reinterpret_cast<char*>(chunk.sceneMapLegacy.data()), static_cast<std::streamsize>(sceneMapCells));
|
||||
// Like the client: version 31 uses the first colorMapResolution rows and columns, older versions are all scene 0
|
||||
chunk.sceneMap.assign(static_cast<size_t>(chunk.colorMapResolution) * chunk.colorMapResolution, 0);
|
||||
if (version == 31) {
|
||||
for (uint32_t row = 0; row < chunk.colorMapResolution; ++row) {
|
||||
for (uint32_t col = 0; col < chunk.colorMapResolution; ++col) {
|
||||
chunk.sceneMap[row * chunk.colorMapResolution + col] = chunk.sceneMapLegacy[row * (chunk.colorMapResolution + 1) + col];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (stream.fail()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mesh data (v>=32 only)
|
||||
if (version < 32) {
|
||||
return true;
|
||||
}
|
||||
|
||||
BinaryIO::BinaryRead(stream, chunk.vertSize);
|
||||
|
||||
if (stream.fail()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (chunk.vertSize == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const size_t vertBytes = static_cast<size_t>(chunk.vertSize) * sizeof(uint16_t);
|
||||
if (vertBytes > kMaxBlobBytes) {
|
||||
LOG("Chunk %u vertSize %u exceeds maximum (byte size %zu > %zu)", chunk.id, chunk.vertSize, vertBytes, kMaxBlobBytes);
|
||||
return false;
|
||||
}
|
||||
chunk.meshVertUsage.resize(chunk.vertSize);
|
||||
for (uint32_t i = 0; i < chunk.vertSize; ++i) {
|
||||
BinaryIO::BinaryRead(stream, chunk.meshVertUsage[i]);
|
||||
}
|
||||
|
||||
if (stream.fail()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
chunk.meshVertSize.resize(16);
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
BinaryIO::BinaryRead(stream, chunk.meshVertSize[i]);
|
||||
}
|
||||
|
||||
if (stream.fail()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
chunk.meshTri.resize(16);
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
if (!ReadMeshTri(stream, chunk.meshTri[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (const std::exception&) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Read a chunk and work out its scene map cell sizes
|
||||
*/
|
||||
static bool ReadChunk(std::istream& stream, Chunk& chunk, uint16_t version) {
|
||||
if (!ReadChunkData(stream, chunk, version)) return false;
|
||||
|
||||
if (chunk.IsValidForSceneLookup()) {
|
||||
chunk.sceneCellSizeX = static_cast<float>(chunk.width - 1) * chunk.scaleFactor / static_cast<float>(chunk.colorMapResolution);
|
||||
chunk.sceneCellSizeZ = static_cast<float>(chunk.height - 1) * chunk.scaleFactor / static_cast<float>(chunk.colorMapResolution);
|
||||
chunk.sceneCellsPerUnitX = 1.0f / chunk.sceneCellSizeX;
|
||||
chunk.sceneCellsPerUnitZ = 1.0f / chunk.sceneCellSizeZ;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Index the chunks by grid cell when they form a regular grid of equal chunks
|
||||
*/
|
||||
static void BuildChunkGrid(Raw& raw) {
|
||||
raw.chunkGrid.clear();
|
||||
const auto& first = raw.chunks.front();
|
||||
const float spanX = first.GetMaxX() - first.offsetX;
|
||||
const float spanZ = first.GetMaxZ() - first.offsetZ;
|
||||
if (spanX <= 0.0f || spanZ <= 0.0f) return;
|
||||
|
||||
const auto countX = static_cast<uint32_t>(std::lround((raw.maxBoundsX - raw.minBoundsX) / spanX));
|
||||
const auto countZ = static_cast<uint32_t>(std::lround((raw.maxBoundsZ - raw.minBoundsZ) / spanZ));
|
||||
if (countX == 0 || countZ == 0 || static_cast<size_t>(countX) * countZ > kMaxChunks) return;
|
||||
|
||||
std::vector<int32_t> grid(static_cast<size_t>(countX) * countZ, -1);
|
||||
for (size_t i = 0; i < raw.chunks.size(); ++i) {
|
||||
const auto& chunk = raw.chunks[i];
|
||||
const float cellX = (chunk.offsetX - raw.minBoundsX) / spanX;
|
||||
const float cellZ = (chunk.offsetZ - raw.minBoundsZ) / spanZ;
|
||||
const long x = std::lround(cellX);
|
||||
const long z = std::lround(cellZ);
|
||||
const bool sameSize = chunk.width == first.width && chunk.height == first.height && chunk.scaleFactor == first.scaleFactor;
|
||||
if (!sameSize || std::abs(cellX - x) > 0.01f || std::abs(cellZ - z) > 0.01f || x < 0 || z < 0
|
||||
|| x >= static_cast<long>(countX) || z >= static_cast<long>(countZ)) {
|
||||
LOG("Raw chunks are not a regular grid (chunk %u); scene lookups check every chunk", chunk.id);
|
||||
return;
|
||||
}
|
||||
auto& cell = grid[static_cast<size_t>(x) * countZ + z];
|
||||
if (cell == -1) cell = static_cast<int32_t>(i); // the client takes the first chunk that matches
|
||||
}
|
||||
|
||||
raw.chunkGrid = std::move(grid);
|
||||
raw.chunkGridCountX = countX;
|
||||
raw.chunkGridCountZ = countZ;
|
||||
raw.chunkSpanX = spanX;
|
||||
raw.chunkSpanZ = spanZ;
|
||||
}
|
||||
|
||||
bool ReadRaw(std::istream& stream, Raw& outRaw) {
|
||||
// Get stream size
|
||||
stream.seekg(0, std::ios::end);
|
||||
auto streamSize = stream.tellg();
|
||||
stream.seekg(0, std::ios::beg);
|
||||
|
||||
if (streamSize <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Read header
|
||||
BinaryIO::BinaryRead(stream, outRaw.version);
|
||||
|
||||
if (stream.fail()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
BinaryIO::BinaryRead(stream, outRaw.dev);
|
||||
|
||||
if (stream.fail()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only read chunks if dev == 0
|
||||
if (outRaw.dev == 0) {
|
||||
BinaryIO::BinaryRead(stream, outRaw.numChunks);
|
||||
BinaryIO::BinaryRead(stream, outRaw.numChunksWidth);
|
||||
BinaryIO::BinaryRead(stream, outRaw.numChunksHeight);
|
||||
|
||||
if (outRaw.numChunks > kMaxChunks) {
|
||||
LOG("Raw numChunks %u exceeds maximum %u", outRaw.numChunks, kMaxChunks);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read all chunks
|
||||
outRaw.chunks.resize(outRaw.numChunks);
|
||||
for (uint32_t i = 0; i < outRaw.numChunks; ++i) {
|
||||
if (!ReadChunk(stream, outRaw.chunks[i], outRaw.version)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate terrain bounds from all chunks
|
||||
if (!outRaw.chunks.empty()) {
|
||||
outRaw.minBoundsX = std::numeric_limits<float>::max();
|
||||
outRaw.minBoundsZ = std::numeric_limits<float>::max();
|
||||
outRaw.maxBoundsX = std::numeric_limits<float>::lowest();
|
||||
outRaw.maxBoundsZ = std::numeric_limits<float>::lowest();
|
||||
|
||||
for (const auto& chunk : outRaw.chunks) {
|
||||
outRaw.minBoundsX = std::min(outRaw.minBoundsX, chunk.offsetX);
|
||||
outRaw.minBoundsZ = std::min(outRaw.minBoundsZ, chunk.offsetZ);
|
||||
outRaw.maxBoundsX = std::max(outRaw.maxBoundsX, chunk.GetMaxX());
|
||||
outRaw.maxBoundsZ = std::max(outRaw.maxBoundsZ, chunk.GetMaxZ());
|
||||
}
|
||||
LOG_DEBUG("Raw terrain bounds: X[%.2f, %.2f], Z[%.2f, %.2f]",
|
||||
outRaw.minBoundsX, outRaw.maxBoundsX, outRaw.minBoundsZ, outRaw.maxBoundsZ);
|
||||
BuildChunkGrid(outRaw);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (const std::exception&) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// The client's TerrainManager::GetSceneAtPos: clamp to the terrain bounds, keep one scene cell inside the far edge,
|
||||
// find the chunk containing the position (edges moved in by 0.001) and take its nearest scene map cell
|
||||
std::optional<uint8_t> Raw::GetSceneIDAt(float x, float z) const {
|
||||
if (chunks.empty()) return std::nullopt;
|
||||
|
||||
constexpr float kEdgeEpsilon = 0.001f;
|
||||
const float posX = std::clamp(x, minBoundsX, maxBoundsX);
|
||||
const float posZ = std::clamp(z, minBoundsZ, maxBoundsZ);
|
||||
|
||||
const auto sceneInChunk = [&](const Chunk& chunk) -> std::optional<uint8_t> {
|
||||
if (!chunk.IsValidForSceneLookup()) return std::nullopt;
|
||||
const float chunkX = std::min(posX, maxBoundsX - chunk.sceneCellSizeX);
|
||||
const float chunkZ = std::min(posZ, maxBoundsZ - chunk.sceneCellSizeZ);
|
||||
if (chunk.offsetX - kEdgeEpsilon <= chunkX && chunkX < chunk.GetMaxX() - kEdgeEpsilon &&
|
||||
chunk.offsetZ - kEdgeEpsilon <= chunkZ && chunkZ < chunk.GetMaxZ() - kEdgeEpsilon) {
|
||||
return chunk.GetSceneIDAtLocal(chunkX - chunk.offsetX, chunkZ - chunk.offsetZ);
|
||||
}
|
||||
return std::nullopt;
|
||||
};
|
||||
|
||||
if (chunkGrid.empty()) {
|
||||
for (const auto& chunk : chunks) {
|
||||
if (const auto sceneID = sceneInChunk(chunk)) return sceneID;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// The grid cell the position falls in, checked with the client's test; for a position on a float edge the
|
||||
// neighbouring cells are checked too
|
||||
const auto toCell = [](float pos, float min, float span, uint32_t count) {
|
||||
const auto cell = static_cast<int32_t>(std::floor((pos + kEdgeEpsilon - min) / span));
|
||||
return std::clamp(cell, 0, static_cast<int32_t>(count) - 1);
|
||||
};
|
||||
const int32_t cellX = toCell(posX, minBoundsX, chunkSpanX, chunkGridCountX);
|
||||
const int32_t cellZ = toCell(posZ, minBoundsZ, chunkSpanZ, chunkGridCountZ);
|
||||
for (const int32_t offsetX : { 0, -1, 1 }) {
|
||||
for (const int32_t offsetZ : { 0, -1, 1 }) {
|
||||
const int32_t x = cellX + offsetX;
|
||||
const int32_t z = cellZ + offsetZ;
|
||||
if (x < 0 || z < 0 || x >= static_cast<int32_t>(chunkGridCountX) || z >= static_cast<int32_t>(chunkGridCountZ)) continue;
|
||||
const int32_t index = chunkGrid[static_cast<size_t>(x) * chunkGridCountZ + z];
|
||||
if (index < 0) continue;
|
||||
if (const auto sceneID = sceneInChunk(chunks[index])) return sceneID;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void GenerateTerrainMesh(const Raw& raw, TerrainMesh& outMesh) {
|
||||
outMesh.vertices.clear();
|
||||
outMesh.triangles.clear();
|
||||
|
||||
if (raw.chunks.empty() || raw.version < 32) {
|
||||
return; // No scene data available
|
||||
}
|
||||
|
||||
LOG_DEBUG("GenerateTerrainMesh: Processing %zu chunks", raw.chunks.size());
|
||||
|
||||
uint32_t vertexOffset = 0;
|
||||
|
||||
for (const auto& chunk : raw.chunks) {
|
||||
if (!chunk.IsValidForSceneLookup()) continue;
|
||||
|
||||
// One vertex per heightmap point, in [i * height + j] order
|
||||
for (uint32_t i = 0; i < chunk.width; ++i) {
|
||||
for (uint32_t j = 0; j < chunk.height; ++j) {
|
||||
outMesh.vertices.emplace_back(chunk.GridToWorldPos(i, j), chunk.GetSceneIDAtGrid(i, j));
|
||||
if (i > 0 && j > 0) {
|
||||
const uint32_t currentVert = vertexOffset + chunk.height * i + j;
|
||||
const uint32_t leftVert = currentVert - 1;
|
||||
const uint32_t bottomLeftVert = vertexOffset + chunk.height * (i - 1) + j - 1;
|
||||
const uint32_t bottomVert = vertexOffset + chunk.height * (i - 1) + j;
|
||||
|
||||
// First triangle
|
||||
outMesh.triangles.push_back(currentVert);
|
||||
outMesh.triangles.push_back(leftVert);
|
||||
outMesh.triangles.push_back(bottomLeftVert);
|
||||
|
||||
// Second triangle
|
||||
outMesh.triangles.push_back(bottomLeftVert);
|
||||
outMesh.triangles.push_back(bottomVert);
|
||||
outMesh.triangles.push_back(currentVert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vertexOffset += chunk.width * chunk.height;
|
||||
}
|
||||
}
|
||||
|
||||
bool WriteTerrainMeshToOBJ(const TerrainMesh& mesh, const std::string& path) {
|
||||
try {
|
||||
std::ofstream file(path);
|
||||
if (!file.is_open()) {
|
||||
LOG("Failed to open OBJ file for writing: %s", path.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& v : mesh.vertices) {
|
||||
const NiColor& color = SceneColor::Get(v.sceneID);
|
||||
file << "v " << v.position.x << ' ' << v.position.y << ' ' << v.position.z
|
||||
<< ' ' << color.m_Red << ' ' << color.m_Green << ' ' << color.m_Blue << '\n';
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < mesh.triangles.size(); i += 3) {
|
||||
file << "f " << (mesh.triangles[i] + 1) << ' '
|
||||
<< (mesh.triangles[i + 1] + 1) << ' '
|
||||
<< (mesh.triangles[i + 2] + 1) << '\n';
|
||||
}
|
||||
|
||||
file.close();
|
||||
LOG("Successfully wrote terrain mesh to OBJ: %s (%zu vertices, %zu triangles)",
|
||||
path.c_str(), mesh.vertices.size(), mesh.triangles.size() / 3);
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
LOG("Exception while writing OBJ file: %s", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Raw
|
||||
194
dZoneManager/Raw.h
Normal file
194
dZoneManager/Raw.h
Normal file
@@ -0,0 +1,194 @@
|
||||
#pragma once
|
||||
|
||||
#ifndef __RAW_H__
|
||||
#define __RAW_H__
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <istream>
|
||||
#include "NiPoint3.h"
|
||||
#include "dCommonVars.h"
|
||||
|
||||
namespace Raw {
|
||||
|
||||
/**
|
||||
* @brief Flair attributes structure
|
||||
* Represents decorative elements on the terrain
|
||||
*/
|
||||
struct FlairAttributes {
|
||||
uint32_t id;
|
||||
float scaleFactor;
|
||||
NiPoint3 position;
|
||||
NiPoint3 rotation;
|
||||
uint8_t colorR;
|
||||
uint8_t colorG;
|
||||
uint8_t colorB;
|
||||
// Padding: never set when a flair is placed, so it holds leftover memory; nothing reads it
|
||||
uint8_t padding;
|
||||
};
|
||||
static_assert(sizeof(FlairAttributes) == 36, "a flair record in the .raw is 36 bytes");
|
||||
|
||||
/**
|
||||
* @brief Mesh triangle structure
|
||||
* Contains triangle indices for terrain mesh
|
||||
*/
|
||||
struct MeshTri {
|
||||
uint16_t meshTriListSize;
|
||||
std::vector<uint16_t> meshTriList;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Vertex with scene ID
|
||||
* Used for the generated terrain mesh to enable fast scene lookups
|
||||
*/
|
||||
struct SceneVertex {
|
||||
NiPoint3 position;
|
||||
uint8_t sceneID;
|
||||
|
||||
SceneVertex() : position(), sceneID(0) {}
|
||||
SceneVertex(const NiPoint3& pos, uint8_t scene) : position(pos), sceneID(scene) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Generated terrain mesh
|
||||
* Contains vertices with scene IDs for fast scene lookups at arbitrary positions
|
||||
*/
|
||||
struct TerrainMesh {
|
||||
std::vector<SceneVertex> vertices;
|
||||
std::vector<uint32_t> triangles; // Indices into vertices array (groups of 3)
|
||||
|
||||
TerrainMesh() = default;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Terrain chunk structure
|
||||
* Represents a single chunk of terrain with heightmap, textures, flair, scene map and meshes
|
||||
*/
|
||||
struct Chunk {
|
||||
uint32_t id;
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
float offsetX;
|
||||
float offsetZ;
|
||||
uint32_t shaderId = 0; // version < 32 only
|
||||
|
||||
// Texture IDs (4 textures per chunk)
|
||||
std::vector<uint32_t> textureIds;
|
||||
|
||||
// Terrain scale factor (world units between two heightmap points)
|
||||
float scaleFactor;
|
||||
|
||||
// Heightmap data (width * height floats)
|
||||
std::vector<float> heightMap;
|
||||
|
||||
// Color map: colorMapResolution^2 RGBA pixels (version < 32: width^2 pixels, resolution width - 1)
|
||||
uint32_t colorMapResolution = 0;
|
||||
std::vector<uint8_t> colorMap;
|
||||
std::vector<uint8_t> lightMap; // DDS, version 32+
|
||||
|
||||
uint32_t textureMapResolution = 0;
|
||||
std::vector<uint8_t> textureMap; // textureMapResolution^2 RGBA pixels
|
||||
uint8_t textureSettings = 0; // version 32+
|
||||
std::vector<uint8_t> blendMap; // DDS, version 32+
|
||||
|
||||
// Flair data
|
||||
std::vector<FlairAttributes> flairs;
|
||||
|
||||
// The scene map is colorMapResolution * colorMapResolution scene IDs, indexed [x cell * resolution + z cell]
|
||||
std::vector<uint8_t> sceneMap;
|
||||
// Version 31 and older: the file's (colorMapResolution + 1)^2 scene map. In version 31 sceneMap is its first
|
||||
// colorMapResolution rows and columns; older versions don't use it and every cell is scene 0
|
||||
std::vector<uint8_t> sceneMapLegacy;
|
||||
|
||||
// Mesh data (version 32+)
|
||||
uint32_t vertSize = 0;
|
||||
std::vector<uint16_t> meshVertUsage;
|
||||
std::vector<uint16_t> meshVertSize;
|
||||
std::vector<MeshTri> meshTri;
|
||||
|
||||
// Scene map cells per world unit, and the size of one cell, along X and Z (set once the chunk is read)
|
||||
float sceneCellsPerUnitX = 0.0f;
|
||||
float sceneCellsPerUnitZ = 0.0f;
|
||||
float sceneCellSizeX = 0.0f;
|
||||
float sceneCellSizeZ = 0.0f;
|
||||
|
||||
bool IsValidForSceneLookup() const;
|
||||
float GetMaxX() const { return offsetX + static_cast<float>(width - 1) * scaleFactor; }
|
||||
float GetMaxZ() const { return offsetZ + static_cast<float>(height - 1) * scaleFactor; }
|
||||
// The scene ID at a position relative to the chunk's offset, as the client looks it up
|
||||
uint8_t GetSceneIDAtLocal(float localX, float localZ) const;
|
||||
uint8_t GetSceneIDAtGrid(uint32_t i, uint32_t j) const;
|
||||
NiPoint3 GridToWorldPos(uint32_t i, uint32_t j) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief RAW terrain file structure
|
||||
* Complete representation of a .raw terrain file
|
||||
*/
|
||||
struct Raw {
|
||||
uint16_t version = 0;
|
||||
uint8_t dev = 0;
|
||||
uint32_t numChunks = 0;
|
||||
uint32_t numChunksWidth = 0;
|
||||
uint32_t numChunksHeight = 0;
|
||||
std::vector<Chunk> chunks;
|
||||
|
||||
// Calculated bounds of the entire terrain
|
||||
float minBoundsX = 0.0f;
|
||||
float minBoundsZ = 0.0f;
|
||||
float maxBoundsX = 0.0f;
|
||||
float maxBoundsZ = 0.0f;
|
||||
|
||||
// Chunk lookup by position: the chunks of every client terrain form a regular grid, written X-major
|
||||
// ([x * chunks along Z + z]). chunkGrid holds the index into chunks per grid cell (-1 for none); it is empty when
|
||||
// the chunks don't form a grid, and lookups then check every chunk.
|
||||
std::vector<int32_t> chunkGrid;
|
||||
uint32_t chunkGridCountX = 0;
|
||||
uint32_t chunkGridCountZ = 0;
|
||||
float chunkSpanX = 0.0f;
|
||||
float chunkSpanZ = 0.0f;
|
||||
|
||||
/**
|
||||
* @brief The scene ID at a world position, looked up as the client's TerrainManager::GetSceneAtPos does
|
||||
* (height is ignored). Constant time on a chunk grid.
|
||||
*
|
||||
* @return The scene ID, or nullopt when there is no terrain with a scene map there
|
||||
*/
|
||||
std::optional<uint8_t> GetSceneIDAt(float x, float z) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Read a RAW terrain file from an input stream
|
||||
*
|
||||
* @param stream Input stream containing RAW file data
|
||||
* @param outRaw Output RAW file structure
|
||||
* @return true if successfully read, false otherwise
|
||||
*/
|
||||
bool ReadRaw(std::istream& stream, Raw& outRaw);
|
||||
|
||||
/**
|
||||
* @brief Generate a terrain mesh from raw chunks
|
||||
* Similar to dTerrain's GenerateFinalMeshFromChunks but creates a mesh with scene IDs
|
||||
* per vertex for fast scene lookups at arbitrary positions.
|
||||
*
|
||||
* @param raw The RAW terrain data to generate mesh from
|
||||
* @param outMesh Output terrain mesh with vertices and scene IDs
|
||||
*/
|
||||
void GenerateTerrainMesh(const Raw& raw, TerrainMesh& outMesh);
|
||||
|
||||
/**
|
||||
* @brief Write terrain mesh to OBJ file for debugging/visualization
|
||||
* Merged from dTerrain's WriteFinalMeshToOBJ functionality
|
||||
* Vertices are colored based on their scene ID using a hash function
|
||||
*
|
||||
* @param mesh The terrain mesh to export
|
||||
* @param path Output path for the OBJ file
|
||||
* @return true if successfully written, false otherwise
|
||||
*/
|
||||
bool WriteTerrainMeshToOBJ(const TerrainMesh& mesh, const std::string& path);
|
||||
|
||||
} // namespace Raw
|
||||
|
||||
#endif // __RAW_H__
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "GeneralUtils.h"
|
||||
#include "BinaryIO.h"
|
||||
#include "LUTriggers.h"
|
||||
#include "dConfig.h"
|
||||
|
||||
#include "AssetManager.h"
|
||||
#include "CDClientManager.h"
|
||||
@@ -22,6 +23,7 @@
|
||||
#include "eWaypointCommandType.h"
|
||||
#include "dNavMesh.h"
|
||||
#include "ZoneFileLog.h"
|
||||
#include "Raw.h"
|
||||
|
||||
Zone::Zone(const LWOZONEID zoneID) :
|
||||
m_ZoneID(zoneID) {
|
||||
@@ -90,9 +92,36 @@ void Zone::LoadZoneIntoMemory() {
|
||||
m_SceneTransitions = std::move(zoneFile.sceneTransitions);
|
||||
m_NumberOfSceneTransitionsLoaded = m_SceneTransitions.size();
|
||||
|
||||
m_PathDataLength = zoneFile.pathDataLength;
|
||||
m_PathChunkVersion = zoneFile.pathChunkVersion;
|
||||
m_Paths = std::move(zoneFile.paths);
|
||||
m_Boundaries = std::move(zoneFile.zoneBoundaries);
|
||||
|
||||
// The terrain gives the scene at a position. Like the client, a missing or unreadable .raw is logged
|
||||
// and the zone loads without it.
|
||||
const auto rawPath = m_ZoneFilePath.substr(0, m_ZoneFilePath.rfind('/') + 1) + m_ZoneRawPath;
|
||||
if (m_FileFormatVersion < Zone::FileFormatVersion::PrePreAlpha) {
|
||||
LOG("Zone %s uses the legacy raw terrain format (version %u), which is not supported; no scene lookups", m_ZoneFilePath.c_str(), static_cast<uint32_t>(m_FileFormatVersion));
|
||||
} else if (!Game::assetManager->HasFile(rawPath)) {
|
||||
LOG("Failed to find %s; no scene lookups", rawPath.c_str());
|
||||
} else {
|
||||
auto rawFile = Game::assetManager->GetFile(rawPath);
|
||||
if (Raw::ReadRaw(rawFile, m_Raw)) {
|
||||
LOG_DEBUG("Successfully parsed %s", rawPath.c_str());
|
||||
} else {
|
||||
LOG("Failed to parse %s; no scene lookups", rawPath.c_str());
|
||||
m_Raw = Raw::Raw{};
|
||||
}
|
||||
}
|
||||
|
||||
// Optionally export terrain mesh to OBJ for debugging/visualization
|
||||
if (Game::config->GetValue("export_terrain_to_obj") == "1") {
|
||||
Raw::TerrainMesh terrainMesh;
|
||||
Raw::GenerateTerrainMesh(m_Raw, terrainMesh);
|
||||
LOG("Generated terrain mesh with %zu vertices and %zu triangles", terrainMesh.vertices.size(), terrainMesh.triangles.size() / 3);
|
||||
|
||||
const std::string objFileName = "terrain_" + std::to_string(m_ZoneID.GetMapID()) + ".obj";
|
||||
if (Raw::WriteTerrainMeshToOBJ(terrainMesh, objFileName)) {
|
||||
LOG("Exported terrain mesh to %s", objFileName.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// We verify the waypoint heights against the navmesh because in many movement paths,
|
||||
// the waypoint is located near 0 height,
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include "Raw.h"
|
||||
|
||||
namespace LUTriggers {
|
||||
struct Trigger;
|
||||
@@ -61,6 +62,12 @@ public:
|
||||
void SetSpawnPos(const NiPoint3& pos) { m_Spawnpoint = pos; }
|
||||
void SetSpawnRot(const NiQuaternion& rot) { m_SpawnpointRotation = rot; }
|
||||
|
||||
const Raw::Raw& GetZoneRaw() const { return m_Raw; }
|
||||
const SceneRef* GetScene(LWOSCENEID sceneID) const;
|
||||
const std::vector<SceneTransition>& GetSceneTransitions() const { return m_SceneTransitions; }
|
||||
const std::vector<ZoneBoundary>& GetBoundaries() const { return m_Boundaries; }
|
||||
const std::map<LWOSCENEID, SceneRef>& GetScenes() const { return m_Scenes; }
|
||||
|
||||
private:
|
||||
LWOZONEID m_ZoneID;
|
||||
std::string m_ZoneFilePath;
|
||||
@@ -77,11 +84,13 @@ private:
|
||||
std::string m_ZoneName; //Name given to the zone by a level designer
|
||||
std::string m_ZoneDesc; //Description of the zone by a level designer
|
||||
std::string m_ZoneRawPath; //Path to the .raw file of this zone.
|
||||
Raw::Raw m_Raw; // The Raw data for this zone
|
||||
|
||||
std::map<LWOSCENEID, SceneRef> m_Scenes;
|
||||
std::vector<SceneTransition> m_SceneTransitions;
|
||||
ZoneScenes::SceneGraph m_SceneGraph;
|
||||
ZoneScenes::SceneMap m_SceneMap;
|
||||
std::vector<ZoneBoundary> m_Boundaries;
|
||||
|
||||
uint32_t m_PathDataLength;
|
||||
uint32_t m_PathChunkVersion;
|
||||
|
||||
@@ -11,9 +11,10 @@
|
||||
#include "VanityUtilities.h"
|
||||
#include "WorldConfig.h"
|
||||
#include "CDZoneTableTable.h"
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <set>
|
||||
#include "eObjectBits.h"
|
||||
#include "CDZoneTableTable.h"
|
||||
#include "AssetManager.h"
|
||||
#include <ranges>
|
||||
|
||||
@@ -63,6 +64,9 @@ void dZoneManager::Initialize(const LWOZONEID& zoneID) {
|
||||
|
||||
m_pZone->Initalize();
|
||||
|
||||
// Build the scene graph after zone is loaded
|
||||
BuildSceneGraph();
|
||||
|
||||
endTime = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
|
||||
LoadWorldConfig();
|
||||
@@ -306,3 +310,83 @@ void dZoneManager::LoadWorldConfig() {
|
||||
|
||||
LOG_DEBUG("Loaded WorldConfig into memory");
|
||||
}
|
||||
|
||||
LWOSCENEID dZoneManager::GetSceneIDFromPosition(const NiPoint3& position) const {
|
||||
if (!m_pZone) return LWOSCENEID_INVALID;
|
||||
|
||||
const auto sceneID = m_pZone->GetZoneRaw().GetSceneIDAt(position.x, position.z);
|
||||
return sceneID ? LWOSCENEID(*sceneID, 0) : LWOSCENEID_INVALID;
|
||||
}
|
||||
|
||||
void dZoneManager::BuildSceneGraph() {
|
||||
if (!m_pZone) return;
|
||||
|
||||
// Clear any existing adjacency list
|
||||
m_SceneAdjacencyList.clear();
|
||||
|
||||
// Initialize adjacency list with all scenes
|
||||
const auto& scenes = m_pZone->GetScenes();
|
||||
for (const auto& [sceneID, sceneRef] : scenes) {
|
||||
if (sceneRef.sceneType != eSceneType::General) continue;
|
||||
m_SceneAdjacencyList.try_emplace(sceneID, std::vector<LWOSCENEID>());
|
||||
}
|
||||
|
||||
// Build adjacency list from scene transitions
|
||||
const auto& transitions = m_pZone->GetSceneTransitions();
|
||||
for (const auto& transition : transitions) {
|
||||
// Each transition has multiple points, each pointing to a scene
|
||||
// We need to determine which scenes this transition connects
|
||||
|
||||
// Group transition points by their scene IDs to find unique connections
|
||||
std::set<LWOSCENEID> connectedScenes;
|
||||
for (const auto& point : transition.points) {
|
||||
if (point.sceneID != LWOSCENEID_INVALID && m_SceneAdjacencyList.contains(point.sceneID)) {
|
||||
connectedScenes.insert(point.sceneID);
|
||||
}
|
||||
}
|
||||
|
||||
// Create bidirectional edges between all scenes in this transition
|
||||
// (transitions typically connect two scenes, but can be more complex)
|
||||
std::vector<LWOSCENEID> sceneList(connectedScenes.begin(), connectedScenes.end());
|
||||
|
||||
for (size_t i = 0; i < sceneList.size(); ++i) {
|
||||
for (size_t j = 0; j < sceneList.size(); ++j) {
|
||||
if (i != j) {
|
||||
LWOSCENEID fromScene = sceneList[i];
|
||||
LWOSCENEID toScene = sceneList[j];
|
||||
|
||||
// Add edge if it doesn't already exist
|
||||
auto& adjacentScenes = m_SceneAdjacencyList[fromScene];
|
||||
if (std::find(adjacentScenes.begin(), adjacentScenes.end(), toScene) == adjacentScenes.end()) {
|
||||
adjacentScenes.push_back(toScene);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scene 0 (global scene) is always loaded and adjacent to all other scenes
|
||||
LWOSCENEID globalScene = LWOSCENEID(0, 0);
|
||||
for (auto& [sceneID, adjacentScenes] : m_SceneAdjacencyList) {
|
||||
if (sceneID != globalScene) {
|
||||
// Add global scene to this scene's adjacency list if not already present
|
||||
if (std::find(adjacentScenes.begin(), adjacentScenes.end(), globalScene) == adjacentScenes.end()) {
|
||||
adjacentScenes.push_back(globalScene);
|
||||
}
|
||||
|
||||
// Add this scene to global scene's adjacency list if not already present
|
||||
auto& globalAdjacent = m_SceneAdjacencyList[globalScene];
|
||||
if (std::find(globalAdjacent.begin(), globalAdjacent.end(), sceneID) == globalAdjacent.end()) {
|
||||
globalAdjacent.push_back(sceneID);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<LWOSCENEID> dZoneManager::GetAdjacentScenes(LWOSCENEID sceneID) const {
|
||||
auto it = m_SceneAdjacencyList.find(sceneID);
|
||||
if (it != m_SceneAdjacencyList.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return std::vector<LWOSCENEID>();
|
||||
}
|
||||
|
||||
@@ -57,6 +57,35 @@ public:
|
||||
uint32_t GetUniqueMissionIdStartingValue();
|
||||
bool CheckIfAccessibleZone(LWOMAPID zoneID);
|
||||
|
||||
/**
|
||||
* @brief Get the scene ID at a given position. Scenes do not care about height (Y coordinate).
|
||||
*
|
||||
* @param position The position to query
|
||||
* @return The scene ID at that position, or LWOSCENEID_INVALID if not found
|
||||
*/
|
||||
LWOSCENEID GetSceneIDFromPosition(const NiPoint3& position) const;
|
||||
|
||||
/**
|
||||
* @brief Get the adjacency list for the scene graph.
|
||||
* The adjacency list maps each scene ID to a list of scene IDs it can transition to.
|
||||
*
|
||||
* @return A reference to the scene adjacency list
|
||||
*/
|
||||
const std::map<LWOSCENEID, std::vector<LWOSCENEID>>& GetSceneAdjacencyList() const { return m_SceneAdjacencyList; }
|
||||
|
||||
/**
|
||||
* @brief Get all scenes adjacent to (connected to) a given scene.
|
||||
*
|
||||
* @param sceneID The scene ID to query
|
||||
* @return A vector of scene IDs that are directly connected to this scene, or empty vector if scene not found
|
||||
*/
|
||||
std::vector<LWOSCENEID> GetAdjacentScenes(LWOSCENEID sceneID) const;
|
||||
|
||||
/**
|
||||
* @brief (Re)builds the scene graph adjacency list from the loaded zone's scene transitions.
|
||||
*/
|
||||
void BuildSceneGraph();
|
||||
|
||||
// The world config should not be modified by a caller.
|
||||
const WorldConfig& GetWorldConfig() {
|
||||
if (!m_WorldConfig) LoadWorldConfig();
|
||||
@@ -79,4 +108,9 @@ private:
|
||||
std::optional<WorldConfig> m_WorldConfig = std::nullopt;
|
||||
|
||||
Entity* m_ZoneControlObject = nullptr;
|
||||
|
||||
/**
|
||||
* Scene graph adjacency list: maps each scene ID to a list of scenes it can transition to
|
||||
*/
|
||||
std::map<LWOSCENEID, std::vector<LWOSCENEID>> m_SceneAdjacencyList;
|
||||
};
|
||||
|
||||
@@ -158,6 +158,8 @@ These commands are primarily for development and testing. The usage of many of t
|
||||
|freecam|`/freecam`|Toggles freecam mode|8|
|
||||
|freemoney|`/freemoney <coins>`|Give yourself coins. Aliases: `/givemoney`, `/money`, `/givecoins`, `/coins`.|8|
|
||||
|getnavmeshheight|`/getnavmeshheight`|Display the navmesh height at your current position|8|
|
||||
|getscene|`/getscene`|Display the scene ID and name at your current position (alias `/scene`)|8|
|
||||
|getadjacentscenes|`/getadjacentscenes`|List the scenes connected to your current scene by scene transitions (alias `/adjacentscenes`)|8|
|
||||
|giveuscore|`/giveuscore <uscore>`|Gives uscore|8|
|
||||
|gmadditem|`/gmadditem <id> (count)`|Adds the given item to your inventory by id. Aliases: `/give`.|8|
|
||||
|inspect|`/inspect <component or ldf variable or player name> (-m <waypoint> | -a <animation> | -s | -p | -f (faction) | -t)`|Finds the closest entity with the given component or LNV variable (ignoring players and racing cars), printing its ID, distance from the player, and whether it is sleeping, as well as the IDs of all components the entity has. Use `localCharacter` or `zoneControl` to inspect your current character or the zone control object.|8|
|
||||
|
||||
@@ -108,6 +108,10 @@ hardcore_disabled_worlds=
|
||||
# Keeps this percentage of a players' coins on death in hardcore
|
||||
hardcore_coin_keep=
|
||||
|
||||
# Export terrain meshes to OBJ files when zones load
|
||||
# OBJ files will be saved as terrain_<zoneID>.obj in the server directory
|
||||
export_terrain_to_obj=0
|
||||
|
||||
# save pre-split lxfmls to disk for debugging
|
||||
save_lxfmls=0
|
||||
|
||||
|
||||
@@ -43,6 +43,8 @@ list(APPEND DGAMETEST_SOURCES ${DGAMEMESSAGES_TESTS})
|
||||
|
||||
add_subdirectory(dNetTests)
|
||||
list(APPEND DGAMETEST_SOURCES ${DNET_TESTS})
|
||||
add_subdirectory(dZoneManagerTests)
|
||||
list(APPEND DGAMETEST_SOURCES ${DZONEMANAGER_TESTS})
|
||||
|
||||
file(COPY ${GAMEMESSAGE_TESTBITSTREAMS} DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
|
||||
file(COPY ${COMPONENT_TEST_DATA} DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
|
||||
|
||||
@@ -8,6 +8,8 @@ set(DCOMPONENTS_TESTS
|
||||
"VendorBuybackTests.cpp"
|
||||
"RacingWrongWayTests.cpp"
|
||||
"DestroyableComponentTests.cpp"
|
||||
"InventoryGroupTests.cpp"
|
||||
"ModelComponentTests.cpp"
|
||||
"PetComponentTests.cpp"
|
||||
"SimplePhysicsComponentTests.cpp"
|
||||
"MovementBlockerScenarioTests.cpp"
|
||||
|
||||
@@ -132,3 +132,11 @@ TEST_F(InventoryGroupTests, TheLockedFlagIsSavedAndLoaded) {
|
||||
EXPECT_TRUE(groups[0].locked);
|
||||
EXPECT_FALSE(groups[1].locked);
|
||||
}
|
||||
|
||||
TEST_F(InventoryGroupTests, OnlyBricksAndModelsHaveGroups) {
|
||||
Group("ADD", "user_group1", u"g", eInventoryType::ITEMS);
|
||||
EXPECT_FALSE(inventory->GetGroups().contains(eInventoryType::ITEMS));
|
||||
|
||||
Group("ADD", "user_group1", u"g", eInventoryType::BRICKS);
|
||||
EXPECT_EQ(inventory->GetGroups().at(eInventoryType::BRICKS).size(), 1u);
|
||||
}
|
||||
|
||||
55
tests/dGameTests/dComponentsTests/ModelComponentTests.cpp
Normal file
55
tests/dGameTests/dComponentsTests/ModelComponentTests.cpp
Normal file
@@ -0,0 +1,55 @@
|
||||
#include "GameDependencies.h"
|
||||
|
||||
#include "BitStream.h"
|
||||
#include "CDClientManager.h"
|
||||
#include "CDComponentsRegistryTable.h"
|
||||
#include "Entity.h"
|
||||
#include "GameMessages.h"
|
||||
#include "ModelComponent.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
class ModelComponentTests : public GameDependenciesTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
SetUpDependencies();
|
||||
CDClientManager::GetEntriesMutable<CDComponentsRegistryTable>().insert_or_assign(static_cast<uint64_t>(info.lot), 0);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
TearDownDependencies();
|
||||
}
|
||||
|
||||
// The bits a model's serialization writes
|
||||
static uint32_t SerializedBits(Entity& entity) {
|
||||
auto* model = entity.AddComponent<ModelComponent>(-1);
|
||||
RakNet::BitStream bitStream;
|
||||
model->Serialize(bitStream, true);
|
||||
return bitStream.GetNumberOfBitsUsed();
|
||||
}
|
||||
};
|
||||
|
||||
// Item block (1 + 64 + 32 + 1), model block (1 + 1 + 32 + 96 + 128)
|
||||
constexpr uint32_t ModelBlockBits = 98 + 258;
|
||||
// Behavior info: flag, count, paused, and on the initial update the editing info flag
|
||||
constexpr uint32_t BehaviorBits = 1 + 32 + 1 + 1;
|
||||
|
||||
// The client reads the behaviors only for property and inventory models (ObjectLoader2::LoadModelBehaviorsComponent)
|
||||
TEST_F(ModelComponentTests, OtherModelsHaveNoBehaviorInfo) {
|
||||
Entity entity(1, info);
|
||||
EXPECT_EQ(SerializedBits(entity), ModelBlockBits);
|
||||
}
|
||||
|
||||
TEST_F(ModelComponentTests, PropertyModelsHaveBehaviorInfo) {
|
||||
EntityInfo propertyInfo = info;
|
||||
propertyInfo.settings.Insert<bool>(u"propertyObjectID", true);
|
||||
Entity entity(1, propertyInfo);
|
||||
EXPECT_EQ(SerializedBits(entity), ModelBlockBits + BehaviorBits);
|
||||
}
|
||||
|
||||
TEST_F(ModelComponentTests, InventoryModelsHaveBehaviorInfo) {
|
||||
EntityInfo inventoryInfo = info;
|
||||
inventoryInfo.settings.Insert<bool>(u"inInventory", true);
|
||||
Entity entity(1, inventoryInfo);
|
||||
EXPECT_EQ(SerializedBits(entity), ModelBlockBits + BehaviorBits);
|
||||
}
|
||||
@@ -90,6 +90,51 @@ TEST_F(GameMessageTests, SendBlueprintLoadItemResponse) {
|
||||
ASSERT_EQ(bitStream->GetNumberOfUnreadBits(), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief PlaceModelResponse is laid out as the client's PlaceModelResponse::Deserialize (0x00dc0170) reads it: every
|
||||
* field optional, the rotation a w, x, y, z quaternion
|
||||
*/
|
||||
TEST_F(GameMessageTests, SendPlaceModelResponse) {
|
||||
const NiQuaternion rotation = NiQuaternion(0.5f, 0.1f, 0.2f, 0.3f); // w, x, y, z
|
||||
GameMessages::SendPlaceModelResponse(77, UNASSIGNED_SYSTEM_ADDRESS, NiPoint3(1.0f, 2.0f, 3.0f), LWOOBJID_EMPTY, 14, rotation);
|
||||
auto* bitStream = static_cast<dServerMock*>(Game::server)->GetMostRecentBitStream();
|
||||
ASSERT_NE(bitStream, nullptr);
|
||||
bitStream->IgnoreBits(64); // packet header
|
||||
|
||||
LWOOBJID objectId{};
|
||||
MessageType::Game messageId{};
|
||||
bitStream->Read(objectId);
|
||||
bitStream->Read(messageId);
|
||||
EXPECT_EQ(objectId, 77);
|
||||
EXPECT_EQ(messageId, MessageType::Game::PLACE_MODEL_RESPONSE);
|
||||
|
||||
bool hasPosition{}, hasPlaque{}, hasResponse{}, hasRotation{};
|
||||
NiPoint3 position{};
|
||||
int32_t response{};
|
||||
float w{}, x{}, y{}, z{};
|
||||
bitStream->Read(hasPosition);
|
||||
bitStream->Read(position);
|
||||
bitStream->Read(hasPlaque);
|
||||
bitStream->Read(hasResponse);
|
||||
bitStream->Read(response);
|
||||
bitStream->Read(hasRotation);
|
||||
bitStream->Read(w);
|
||||
bitStream->Read(x);
|
||||
bitStream->Read(y);
|
||||
bitStream->Read(z);
|
||||
EXPECT_TRUE(hasPosition);
|
||||
EXPECT_EQ(position, NiPoint3(1.0f, 2.0f, 3.0f));
|
||||
EXPECT_FALSE(hasPlaque);
|
||||
EXPECT_TRUE(hasResponse);
|
||||
EXPECT_EQ(response, 14);
|
||||
EXPECT_TRUE(hasRotation);
|
||||
EXPECT_FLOAT_EQ(w, rotation.w);
|
||||
EXPECT_FLOAT_EQ(x, rotation.x);
|
||||
EXPECT_FLOAT_EQ(y, rotation.y);
|
||||
EXPECT_FLOAT_EQ(z, rotation.z);
|
||||
EXPECT_EQ(bitStream->GetNumberOfUnreadBits(), 0);
|
||||
}
|
||||
|
||||
TEST_F(GameMessageTests, ControlBehaviorAddStrip) {
|
||||
auto data = ReadFromFile("addStrip");
|
||||
RakNet::BitStream inStream(reinterpret_cast<unsigned char*>(&data[0]), data.length(), true);
|
||||
|
||||
10
tests/dGameTests/dZoneManagerTests/CMakeLists.txt
Normal file
10
tests/dGameTests/dZoneManagerTests/CMakeLists.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
SET(DZONEMANAGER_TESTS
|
||||
"RawTests.cpp"
|
||||
"ZoneSceneTests.cpp")
|
||||
|
||||
# Get the folder name and prepend it to the files above
|
||||
get_filename_component(thisFolderName ${CMAKE_CURRENT_SOURCE_DIR} NAME)
|
||||
list(TRANSFORM DZONEMANAGER_TESTS PREPEND "${thisFolderName}/")
|
||||
|
||||
# Export to parent scope
|
||||
set(DZONEMANAGER_TESTS ${DZONEMANAGER_TESTS} PARENT_SCOPE)
|
||||
381
tests/dGameTests/dZoneManagerTests/RawTests.cpp
Normal file
381
tests/dGameTests/dZoneManagerTests/RawTests.cpp
Normal file
@@ -0,0 +1,381 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <random>
|
||||
|
||||
#include "Game.h"
|
||||
#include "Logger.h"
|
||||
#include "NiColor.h"
|
||||
#include "Raw.h"
|
||||
#include "SceneColor.h"
|
||||
#include "TerrainTestWriter.h"
|
||||
|
||||
using namespace TerrainTestWriter;
|
||||
|
||||
class RawTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
Game::logger = new Logger("./testing.log", false, false);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
delete Game::logger;
|
||||
Game::logger = nullptr;
|
||||
}
|
||||
|
||||
static bool Read(const std::string& data, Raw::Raw& raw) {
|
||||
std::istringstream stream(data);
|
||||
return Raw::ReadRaw(stream, raw);
|
||||
}
|
||||
|
||||
// A chunksX by chunksZ grid of 5x5 point chunks, scale 2 (8 units across), each with a 4x4 scene map whose cells
|
||||
// are numbered [x * 4 + z] plus 16 * the chunk's index
|
||||
static std::vector<TestChunk> MakeGrid(uint32_t chunksX, uint32_t chunksZ, float originX = -8.0f, float originZ = -8.0f) {
|
||||
std::vector<TestChunk> chunks;
|
||||
for (uint32_t x = 0; x < chunksX; ++x) {
|
||||
for (uint32_t z = 0; z < chunksZ; ++z) {
|
||||
TestChunk chunk;
|
||||
chunk.id = x * chunksZ + z;
|
||||
chunk.width = 5;
|
||||
chunk.height = 5;
|
||||
chunk.scale = 2.0f;
|
||||
chunk.offsetX = originX + 8.0f * x;
|
||||
chunk.offsetZ = originZ + 8.0f * z;
|
||||
chunk.colorMapResolution = 4;
|
||||
for (uint8_t cell = 0; cell < 16; ++cell) chunk.sceneMap.push_back(static_cast<uint8_t>(cell + 16 * chunk.id));
|
||||
chunks.push_back(chunk);
|
||||
}
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(RawTest, ReadsEveryPartOfVersion32) {
|
||||
auto chunks = MakeGrid(2, 3);
|
||||
chunks[1].vertSize = 0; // a chunk without a render mesh
|
||||
chunks[2].flairCount = 0;
|
||||
const auto data = WriteRaw(chunks, 32, 2, 3);
|
||||
|
||||
std::istringstream stream(data);
|
||||
Raw::Raw raw;
|
||||
ASSERT_TRUE(Raw::ReadRaw(stream, raw));
|
||||
EXPECT_EQ(stream.tellg(), static_cast<std::streamoff>(data.size())); // every block was read or skipped
|
||||
|
||||
EXPECT_EQ(raw.version, 32);
|
||||
EXPECT_EQ(raw.dev, 0);
|
||||
EXPECT_EQ(raw.numChunks, 6);
|
||||
EXPECT_EQ(raw.numChunksWidth, 3);
|
||||
EXPECT_EQ(raw.numChunksHeight, 2);
|
||||
ASSERT_EQ(raw.chunks.size(), 6);
|
||||
|
||||
const auto& chunk = raw.chunks[4];
|
||||
EXPECT_EQ(chunk.id, 4);
|
||||
EXPECT_EQ(chunk.width, 5);
|
||||
EXPECT_EQ(chunk.height, 5);
|
||||
EXPECT_FLOAT_EQ(chunk.offsetX, 0.0f);
|
||||
EXPECT_FLOAT_EQ(chunk.offsetZ, 0.0f);
|
||||
EXPECT_FLOAT_EQ(chunk.scaleFactor, 2.0f);
|
||||
ASSERT_EQ(chunk.heightMap.size(), 25);
|
||||
EXPECT_FLOAT_EQ(chunk.heightMap[7], 7.0f);
|
||||
EXPECT_EQ(chunk.colorMapResolution, 4);
|
||||
ASSERT_EQ(chunk.sceneMap.size(), 16);
|
||||
EXPECT_EQ(chunk.sceneMap[5], 5 + 16 * 4);
|
||||
EXPECT_TRUE(chunk.IsValidForSceneLookup());
|
||||
EXPECT_FLOAT_EQ(chunk.sceneCellSizeX, 2.0f);
|
||||
EXPECT_FLOAT_EQ(chunk.sceneCellsPerUnitZ, 0.5f);
|
||||
|
||||
EXPECT_EQ(chunk.textureIds, (std::vector<uint32_t>{ 100, 101, 102, 103 }));
|
||||
EXPECT_EQ(chunk.colorMap.size(), 4 * 4 * 4);
|
||||
EXPECT_EQ(chunk.colorMap[0], 0xAB);
|
||||
EXPECT_EQ(chunk.lightMap.size(), 5);
|
||||
EXPECT_EQ(chunk.textureMapResolution, 2);
|
||||
EXPECT_EQ(chunk.textureMap.size(), 2 * 2 * 4);
|
||||
EXPECT_EQ(chunk.textureSettings, 1);
|
||||
EXPECT_EQ(chunk.blendMap.size(), 7);
|
||||
|
||||
ASSERT_EQ(chunk.flairs.size(), 2);
|
||||
const auto& flair = chunk.flairs[1];
|
||||
EXPECT_EQ(flair.id, 501);
|
||||
EXPECT_FLOAT_EQ(flair.scaleFactor, 1.5f);
|
||||
EXPECT_FLOAT_EQ(flair.position.x, 2.0f);
|
||||
EXPECT_FLOAT_EQ(flair.position.z, 3.0f);
|
||||
EXPECT_FLOAT_EQ(flair.rotation.y, 90.0f);
|
||||
EXPECT_EQ(flair.colorR, 10);
|
||||
EXPECT_EQ(flair.colorG, 20);
|
||||
EXPECT_EQ(flair.colorB, 30);
|
||||
EXPECT_EQ(flair.padding, 40);
|
||||
EXPECT_TRUE(raw.chunks[2].flairs.empty());
|
||||
|
||||
EXPECT_EQ(chunk.vertSize, 4);
|
||||
EXPECT_EQ(chunk.meshVertUsage.size(), 4);
|
||||
EXPECT_EQ(chunk.meshVertSize.size(), 16);
|
||||
ASSERT_EQ(chunk.meshTri.size(), 16);
|
||||
EXPECT_EQ(chunk.meshTri[2].meshTriListSize, 6);
|
||||
EXPECT_EQ(chunk.meshTri[2].meshTriList.size(), 6);
|
||||
EXPECT_EQ(chunk.meshTri[15].meshTriListSize, 3);
|
||||
EXPECT_EQ(raw.chunks[1].vertSize, 0);
|
||||
EXPECT_TRUE(raw.chunks[1].meshTri.empty());
|
||||
}
|
||||
|
||||
TEST_F(RawTest, BoundsAndChunkGridUseTheSpanBetweenFirstAndLastPoint) {
|
||||
Raw::Raw raw;
|
||||
ASSERT_TRUE(Read(WriteRaw(MakeGrid(2, 3), 32, 2, 3), raw));
|
||||
|
||||
// 5 points 2 apart span 8 units, so chunks don't overlap
|
||||
EXPECT_FLOAT_EQ(raw.minBoundsX, -8.0f);
|
||||
EXPECT_FLOAT_EQ(raw.maxBoundsX, 8.0f);
|
||||
EXPECT_FLOAT_EQ(raw.minBoundsZ, -8.0f);
|
||||
EXPECT_FLOAT_EQ(raw.maxBoundsZ, 16.0f);
|
||||
EXPECT_FLOAT_EQ(raw.chunks[0].GetMaxX(), 0.0f);
|
||||
|
||||
EXPECT_EQ(raw.chunkGridCountX, 2);
|
||||
EXPECT_EQ(raw.chunkGridCountZ, 3);
|
||||
EXPECT_FLOAT_EQ(raw.chunkSpanX, 8.0f);
|
||||
EXPECT_FLOAT_EQ(raw.chunkSpanZ, 8.0f);
|
||||
ASSERT_EQ(raw.chunkGrid.size(), 6);
|
||||
for (int32_t i = 0; i < 6; ++i) EXPECT_EQ(raw.chunkGrid[i], i);
|
||||
}
|
||||
|
||||
TEST_F(RawTest, ChunksOutOfOrderAreFoundThroughTheGrid) {
|
||||
auto chunks = MakeGrid(2, 2);
|
||||
std::swap(chunks[0], chunks[3]);
|
||||
Raw::Raw raw;
|
||||
ASSERT_TRUE(Read(WriteRaw(chunks, 32, 2, 2), raw));
|
||||
ASSERT_EQ(raw.chunkGrid.size(), 4);
|
||||
EXPECT_EQ(raw.chunkGrid[0], 3);
|
||||
EXPECT_EQ(raw.chunkGrid[3], 0);
|
||||
EXPECT_EQ(raw.GetSceneIDAt(-7.0f, -7.0f), 1 * 4 + 1); // chunk id 0, local (1, 1)
|
||||
}
|
||||
|
||||
TEST_F(RawTest, ChunksThatAreNotAGridAreSearchedOneByOne) {
|
||||
auto chunks = MakeGrid(2, 1);
|
||||
chunks[1].offsetZ = 3.0f; // shifted off the grid
|
||||
Raw::Raw raw;
|
||||
ASSERT_TRUE(Read(WriteRaw(chunks, 32, 2, 1), raw));
|
||||
EXPECT_TRUE(raw.chunkGrid.empty());
|
||||
EXPECT_EQ(raw.GetSceneIDAt(-7.0f, -7.0f), 1 * 4 + 1);
|
||||
EXPECT_EQ(raw.GetSceneIDAt(1.0f, 4.0f), 16 + 1 * 4 + 1); // chunk 1, local (1, 1)
|
||||
}
|
||||
|
||||
TEST_F(RawTest, ReadsVersion31SceneMapWithoutTheLastRowAndColumn) {
|
||||
TestChunk chunk;
|
||||
chunk.width = 3;
|
||||
chunk.height = 3;
|
||||
// resolution is width - 1 = 2; the file has a 3x3 scene map
|
||||
chunk.sceneMap = { 1, 2, 9, 3, 4, 9, 9, 9, 9 };
|
||||
const auto data = WriteRaw({ chunk }, 31);
|
||||
|
||||
std::istringstream stream(data);
|
||||
Raw::Raw raw;
|
||||
ASSERT_TRUE(Raw::ReadRaw(stream, raw));
|
||||
EXPECT_EQ(stream.tellg(), static_cast<std::streamoff>(data.size()));
|
||||
ASSERT_EQ(raw.chunks.size(), 1);
|
||||
EXPECT_EQ(raw.chunks[0].colorMapResolution, 2);
|
||||
EXPECT_EQ(raw.chunks[0].sceneMap, (std::vector<uint8_t>{ 1, 2, 3, 4 }));
|
||||
EXPECT_EQ(raw.chunks[0].sceneMapLegacy, chunk.sceneMap);
|
||||
EXPECT_EQ(raw.chunks[0].shaderId, 77);
|
||||
EXPECT_EQ(raw.chunks[0].textureIds, (std::vector<uint32_t>{ 100, 101, 102, 103 }));
|
||||
EXPECT_EQ(raw.chunks[0].colorMap.size(), 3 * 3 * 4); // width^2 pixels
|
||||
EXPECT_TRUE(raw.chunks[0].lightMap.empty());
|
||||
EXPECT_EQ(raw.chunks[0].textureMap.size(), 2 * 2 * 4);
|
||||
EXPECT_EQ(raw.chunks[0].flairs.size(), 2);
|
||||
EXPECT_TRUE(raw.chunks[0].meshTri.empty());
|
||||
}
|
||||
|
||||
TEST_F(RawTest, Version30SceneMapIsReadButEveryCellIsSceneZero) {
|
||||
TestChunk chunk;
|
||||
chunk.width = 3;
|
||||
chunk.height = 3;
|
||||
// resolution is width - 1 = 2; the file has a 3x3 scene map the client doesn't use
|
||||
chunk.sceneMap = { 7, 7, 7, 7, 7, 7, 7, 7, 7 };
|
||||
const auto data = WriteRaw({ chunk, chunk }, 30, 1, 2);
|
||||
|
||||
std::istringstream stream(data);
|
||||
Raw::Raw raw;
|
||||
ASSERT_TRUE(Raw::ReadRaw(stream, raw));
|
||||
EXPECT_EQ(stream.tellg(), static_cast<std::streamoff>(data.size()));
|
||||
ASSERT_EQ(raw.chunks.size(), 2);
|
||||
EXPECT_EQ(raw.chunks[1].sceneMapLegacy, chunk.sceneMap);
|
||||
EXPECT_EQ(raw.chunks[1].sceneMap, (std::vector<uint8_t>{ 0, 0, 0, 0 }));
|
||||
EXPECT_TRUE(raw.chunks[1].IsValidForSceneLookup());
|
||||
EXPECT_EQ(raw.GetSceneIDAt(0.5f, 0.5f), 0);
|
||||
}
|
||||
|
||||
TEST_F(RawTest, DevFileHasNoChunks) {
|
||||
Raw::Raw raw;
|
||||
ASSERT_TRUE(Read(WriteRaw({}, 32, 0, 0, 1), raw));
|
||||
EXPECT_EQ(raw.dev, 1);
|
||||
EXPECT_TRUE(raw.chunks.empty());
|
||||
EXPECT_EQ(raw.GetSceneIDAt(0.0f, 0.0f), std::nullopt);
|
||||
}
|
||||
|
||||
TEST_F(RawTest, RejectsBadFiles) {
|
||||
Raw::Raw raw;
|
||||
EXPECT_FALSE(Read("", raw));
|
||||
|
||||
const auto data = WriteRaw(MakeGrid(1, 2), 32, 1, 2);
|
||||
EXPECT_FALSE(Read(data.substr(0, data.size() - 3), raw)); // truncated in the last chunk's mesh
|
||||
|
||||
std::string tooManyChunks = data;
|
||||
const uint32_t chunkCount = 5000;
|
||||
tooManyChunks.replace(3, sizeof(chunkCount), reinterpret_cast<const char*>(&chunkCount), sizeof(chunkCount));
|
||||
EXPECT_FALSE(Read(tooManyChunks, raw));
|
||||
|
||||
// a 3x3 chunk's color map resolution is at 15 (header) + 40 (chunk info) + 36 (heights)
|
||||
std::string hugeColorMap = WriteRaw({ TestChunk{} });
|
||||
const uint32_t resolution = 5000;
|
||||
hugeColorMap.replace(91, sizeof(resolution), reinterpret_cast<const char*>(&resolution), sizeof(resolution));
|
||||
EXPECT_FALSE(Read(hugeColorMap, raw));
|
||||
}
|
||||
|
||||
TEST_F(RawTest, SceneCellIsTheNearestOneLikeTheClient) {
|
||||
Raw::Raw raw;
|
||||
ASSERT_TRUE(Read(WriteRaw(MakeGrid(1, 1, 0.0f, 0.0f)), raw));
|
||||
const auto& chunk = raw.chunks[0];
|
||||
|
||||
// cells are 2 units; floor(position / 2 + 0.5), clamped to [0, 3]; index [x cell * 4 + z cell]
|
||||
EXPECT_EQ(chunk.GetSceneIDAtLocal(0.0f, 0.0f), 0);
|
||||
EXPECT_EQ(chunk.GetSceneIDAtLocal(0.9f, 0.0f), 0);
|
||||
EXPECT_EQ(chunk.GetSceneIDAtLocal(1.0f, 0.0f), 4);
|
||||
EXPECT_EQ(chunk.GetSceneIDAtLocal(0.0f, 3.1f), 2);
|
||||
EXPECT_EQ(chunk.GetSceneIDAtLocal(5.0f, 1.0f), 13);
|
||||
EXPECT_EQ(chunk.GetSceneIDAtLocal(100.0f, 100.0f), 15);
|
||||
EXPECT_EQ(chunk.GetSceneIDAtLocal(-100.0f, -100.0f), 0);
|
||||
|
||||
// grid points are 2 units apart: point (i, j) is at (2i, 2j)
|
||||
EXPECT_EQ(chunk.GetSceneIDAtGrid(1, 2), 1 * 4 + 2);
|
||||
EXPECT_EQ(chunk.GetSceneIDAtGrid(4, 4), 15);
|
||||
}
|
||||
|
||||
TEST_F(RawTest, SceneAtPositionMatchesTheClient) {
|
||||
Raw::Raw raw;
|
||||
ASSERT_TRUE(Read(WriteRaw(MakeGrid(2, 2), 32, 2, 2), raw));
|
||||
|
||||
// inside chunk 0 ([-8, 0] x [-8, 0])
|
||||
EXPECT_EQ(raw.GetSceneIDAt(-8.0f, -8.0f), 0);
|
||||
EXPECT_EQ(raw.GetSceneIDAt(-5.0f, -8.0f), 4 * 2);
|
||||
// chunk 3 ([0, 8] x [0, 8])
|
||||
EXPECT_EQ(raw.GetSceneIDAt(0.5f, 0.5f), 48);
|
||||
// within 0.001 of a chunk's start counts as the next chunk
|
||||
EXPECT_EQ(raw.GetSceneIDAt(-0.0005f, -7.0f), 32 + 1);
|
||||
EXPECT_EQ(raw.GetSceneIDAt(-0.01f, -7.0f), 3 * 4 + 1);
|
||||
// outside the terrain clamps to its edge
|
||||
EXPECT_EQ(raw.GetSceneIDAt(-500.0f, -500.0f), 0);
|
||||
// the far edge is kept one scene cell inside, so it is the last cell of the last chunk
|
||||
EXPECT_EQ(raw.GetSceneIDAt(8.0f, 8.0f), 48 + 15);
|
||||
EXPECT_EQ(raw.GetSceneIDAt(500.0f, 500.0f), 48 + 15);
|
||||
}
|
||||
|
||||
TEST_F(RawTest, GridLookupMatchesCheckingEveryChunk) {
|
||||
auto chunks = MakeGrid(3, 4, -1126.4f, -921.6f);
|
||||
for (auto& chunk : chunks) {
|
||||
chunk.scale = 3.2f;
|
||||
chunk.offsetX = -1126.4f + (chunk.offsetX + 1126.4f) / 8.0f * 12.8f;
|
||||
chunk.offsetZ = -921.6f + (chunk.offsetZ + 921.6f) / 8.0f * 12.8f;
|
||||
}
|
||||
chunks.erase(chunks.begin() + 5); // a hole in the grid
|
||||
Raw::Raw raw;
|
||||
ASSERT_TRUE(Read(WriteRaw(chunks, 32, 3, 4), raw));
|
||||
ASSERT_FALSE(raw.chunkGrid.empty());
|
||||
|
||||
Raw::Raw linear = raw;
|
||||
linear.chunkGrid.clear();
|
||||
|
||||
std::mt19937 random(1);
|
||||
std::uniform_real_distribution<float> randomX(raw.minBoundsX - 20.0f, raw.maxBoundsX + 20.0f);
|
||||
std::uniform_real_distribution<float> randomZ(raw.minBoundsZ - 20.0f, raw.maxBoundsZ + 20.0f);
|
||||
for (int i = 0; i < 20000; ++i) {
|
||||
float x = randomX(random);
|
||||
float z = randomZ(random);
|
||||
// every few positions on a chunk edge
|
||||
if (i % 4 == 0) x = raw.minBoundsX + std::round((x - raw.minBoundsX) / raw.chunkSpanX) * raw.chunkSpanX;
|
||||
if (i % 5 == 0) z = raw.minBoundsZ + std::round((z - raw.minBoundsZ) / raw.chunkSpanZ) * raw.chunkSpanZ;
|
||||
ASSERT_EQ(raw.GetSceneIDAt(x, z), linear.GetSceneIDAt(x, z)) << x << ", " << z;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(RawTest, GridToWorldPosition) {
|
||||
Raw::Raw raw;
|
||||
ASSERT_TRUE(Read(WriteRaw(MakeGrid(1, 1)), raw));
|
||||
const auto position = raw.chunks[0].GridToWorldPos(1, 3);
|
||||
EXPECT_FLOAT_EQ(position.x, -8.0f + 2.0f);
|
||||
EXPECT_FLOAT_EQ(position.y, 1 * 5 + 3);
|
||||
EXPECT_FLOAT_EQ(position.z, -8.0f + 6.0f);
|
||||
}
|
||||
|
||||
TEST_F(RawTest, TerrainMeshForNonSquareChunks) {
|
||||
TestChunk first;
|
||||
first.width = 3;
|
||||
first.height = 4;
|
||||
first.colorMapResolution = 2;
|
||||
first.sceneMap = { 1, 2, 3, 4 };
|
||||
TestChunk second = first;
|
||||
second.offsetX = 2.0f;
|
||||
Raw::Raw raw;
|
||||
ASSERT_TRUE(Read(WriteRaw({ first, second }, 32, 2, 1), raw));
|
||||
|
||||
Raw::TerrainMesh mesh;
|
||||
Raw::GenerateTerrainMesh(raw, mesh);
|
||||
ASSERT_EQ(mesh.vertices.size(), 2 * 12);
|
||||
ASSERT_EQ(mesh.triangles.size(), 2 * (2 * 3) * 2 * 3);
|
||||
for (const auto index : mesh.triangles) EXPECT_LT(index, mesh.vertices.size());
|
||||
|
||||
// vertex [i * height + j]; the first quad of the first chunk
|
||||
EXPECT_EQ(mesh.triangles[0], 1 * 4 + 1);
|
||||
EXPECT_EQ(mesh.triangles[1], 1 * 4 + 0);
|
||||
EXPECT_EQ(mesh.triangles[2], 0);
|
||||
EXPECT_FLOAT_EQ(mesh.vertices[1 * 4 + 3].position.x, 1.0f);
|
||||
EXPECT_FLOAT_EQ(mesh.vertices[1 * 4 + 3].position.z, 3.0f);
|
||||
EXPECT_EQ(mesh.vertices[2 * 4 + 3].sceneID, 4);
|
||||
// the second chunk's vertices come after the first's
|
||||
EXPECT_FLOAT_EQ(mesh.vertices[12].position.x, 2.0f);
|
||||
EXPECT_EQ(mesh.triangles[36], 12 + 1 * 4 + 1);
|
||||
}
|
||||
|
||||
TEST_F(RawTest, NoTerrainMeshWithoutSceneData) {
|
||||
TestChunk chunk;
|
||||
Raw::Raw raw;
|
||||
ASSERT_TRUE(Read(WriteRaw({ chunk }, 30), raw));
|
||||
Raw::TerrainMesh mesh;
|
||||
Raw::GenerateTerrainMesh(raw, mesh);
|
||||
EXPECT_TRUE(mesh.vertices.empty());
|
||||
EXPECT_TRUE(mesh.triangles.empty());
|
||||
}
|
||||
|
||||
TEST_F(RawTest, WritesTheTerrainMeshAsObj) {
|
||||
Raw::TerrainMesh mesh;
|
||||
mesh.vertices = { Raw::SceneVertex(NiPoint3(0.0f, 1.0f, 2.0f), 1), Raw::SceneVertex(NiPoint3(3.0f, 4.0f, 5.0f), 200), Raw::SceneVertex() };
|
||||
mesh.triangles = { 0, 1, 2 };
|
||||
const auto path = (std::filesystem::temp_directory_path() / "dlu_raw_test.obj").string();
|
||||
ASSERT_TRUE(Raw::WriteTerrainMeshToOBJ(mesh, path));
|
||||
|
||||
std::vector<std::string> lines;
|
||||
{
|
||||
std::ifstream file(path);
|
||||
for (std::string line; std::getline(file, line);) lines.push_back(line);
|
||||
} // closed before removing it (Windows can't remove an open file)
|
||||
std::filesystem::remove(path);
|
||||
|
||||
ASSERT_EQ(lines.size(), 4);
|
||||
EXPECT_EQ(lines[0], "v 0 1 2 1 0 0"); // scene 1 is red
|
||||
EXPECT_EQ(lines[1], "v 3 4 5 1 1 1"); // past the table: white
|
||||
EXPECT_EQ(lines[3], "f 1 2 3");
|
||||
EXPECT_FALSE(Raw::WriteTerrainMeshToOBJ(mesh, "/nonexistent-directory/terrain.obj"));
|
||||
}
|
||||
|
||||
TEST(SceneColorTest, TemplateColorsAndFallback) {
|
||||
EXPECT_FLOAT_EQ(SceneColor::Get(0).m_Red, 128.0f / 255.0f);
|
||||
EXPECT_FLOAT_EQ(SceneColor::Get(3).m_Blue, 1.0f);
|
||||
EXPECT_FLOAT_EQ(SceneColor::Get(145).m_Green, SceneColor::TEMPLATE_COLORS.back().m_Green);
|
||||
EXPECT_EQ(&SceneColor::Get(146), &SceneColor::FALLBACK_COLOR);
|
||||
EXPECT_EQ(&SceneColor::Get(255), &SceneColor::FALLBACK_COLOR);
|
||||
}
|
||||
|
||||
TEST(NiColorTest, ConstructionAndLuminance) {
|
||||
constexpr NiColor black;
|
||||
EXPECT_FLOAT_EQ(black.m_Red, 0.0f);
|
||||
EXPECT_FLOAT_EQ(black.Luminance(), 0.0f);
|
||||
EXPECT_NEAR(NiColor(1.0f, 1.0f, 1.0f).Luminance(), 1.0f, 0.0001f);
|
||||
EXPECT_FLOAT_EQ(NiColor(0.0f, 1.0f, 0.0f).Luminance(), 0.71516f);
|
||||
}
|
||||
193
tests/dGameTests/dZoneManagerTests/TerrainTestWriter.h
Normal file
193
tests/dGameTests/dZoneManagerTests/TerrainTestWriter.h
Normal file
@@ -0,0 +1,193 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "NiPoint3.h"
|
||||
|
||||
// Writes .raw terrain and .luz zone files in the client's layout, for the zone manager tests
|
||||
namespace TerrainTestWriter {
|
||||
template<typename T>
|
||||
void Write(std::ostream& stream, const T& value) {
|
||||
stream.write(reinterpret_cast<const char*>(&value), sizeof(T));
|
||||
}
|
||||
|
||||
inline void WriteBytes(std::ostream& stream, size_t count, uint8_t value = 0xAB) {
|
||||
const std::string bytes(count, static_cast<char>(value));
|
||||
stream.write(bytes.data(), bytes.size());
|
||||
}
|
||||
|
||||
inline void WriteString8(std::ostream& stream, const std::string& value) {
|
||||
Write<uint8_t>(stream, static_cast<uint8_t>(value.size()));
|
||||
stream.write(value.data(), value.size());
|
||||
}
|
||||
|
||||
struct TestChunk {
|
||||
uint32_t id = 0;
|
||||
uint32_t width = 3;
|
||||
uint32_t height = 3;
|
||||
float offsetX = 0.0f;
|
||||
float offsetZ = 0.0f;
|
||||
float scale = 1.0f;
|
||||
// Version 32+: the color map resolution; older versions use width - 1
|
||||
uint32_t colorMapResolution = 2;
|
||||
// colorMapResolution^2 scene IDs ((resolution + 1)^2 for version 31 and older); empty = all 0
|
||||
std::vector<uint8_t> sceneMap;
|
||||
// width * height heights; empty = the point index
|
||||
std::vector<float> heights;
|
||||
uint32_t lightMapSize = 5;
|
||||
uint32_t textureMapResolution = 2;
|
||||
uint32_t blendMapSize = 7;
|
||||
uint32_t flairCount = 2;
|
||||
uint32_t vertSize = 4;
|
||||
std::vector<uint16_t> triangleListSizes = { 3, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3 };
|
||||
};
|
||||
|
||||
inline void WriteChunk(std::ostream& stream, const TestChunk& chunk, uint16_t version) {
|
||||
Write(stream, chunk.id);
|
||||
Write(stream, chunk.width);
|
||||
Write(stream, chunk.height);
|
||||
Write(stream, chunk.offsetX);
|
||||
Write(stream, chunk.offsetZ);
|
||||
if (version < 32) Write<uint32_t>(stream, 77); // shader ID
|
||||
for (uint32_t i = 0; i < 4; ++i) Write<uint32_t>(stream, 100 + i); // texture IDs
|
||||
Write(stream, chunk.scale);
|
||||
for (uint32_t i = 0; i < chunk.width * chunk.height; ++i) {
|
||||
Write<float>(stream, chunk.heights.empty() ? static_cast<float>(i) : chunk.heights[i]);
|
||||
}
|
||||
|
||||
const uint32_t resolution = version >= 32 ? chunk.colorMapResolution : chunk.width - 1;
|
||||
if (version >= 32) {
|
||||
Write(stream, resolution);
|
||||
WriteBytes(stream, static_cast<size_t>(resolution) * resolution * 4);
|
||||
Write(stream, chunk.lightMapSize);
|
||||
WriteBytes(stream, chunk.lightMapSize);
|
||||
} else {
|
||||
WriteBytes(stream, static_cast<size_t>(chunk.width) * chunk.width * 4);
|
||||
}
|
||||
|
||||
Write(stream, chunk.textureMapResolution);
|
||||
WriteBytes(stream, static_cast<size_t>(chunk.textureMapResolution) * chunk.textureMapResolution * 4);
|
||||
if (version >= 32) {
|
||||
Write<uint8_t>(stream, 1); // texture settings
|
||||
Write(stream, chunk.blendMapSize);
|
||||
WriteBytes(stream, chunk.blendMapSize);
|
||||
}
|
||||
|
||||
// Flair: id, scale, position, rotation, RGB, padding byte (36 bytes)
|
||||
Write(stream, chunk.flairCount);
|
||||
for (uint32_t i = 0; i < chunk.flairCount; ++i) {
|
||||
Write<uint32_t>(stream, 500 + i);
|
||||
Write<float>(stream, 1.5f);
|
||||
Write(stream, NiPoint3(1.0f + i, 2.0f, 3.0f));
|
||||
Write(stream, NiPoint3(0.0f, 90.0f, 0.0f));
|
||||
for (const uint8_t channel : { 10, 20, 30, 40 }) Write(stream, channel);
|
||||
}
|
||||
|
||||
// version 31 and older: one more row and column
|
||||
const size_t cells = version >= 32 ? static_cast<size_t>(resolution) * resolution : static_cast<size_t>(resolution + 1) * (resolution + 1);
|
||||
for (size_t i = 0; i < cells; ++i) Write<uint8_t>(stream, chunk.sceneMap.empty() ? 0 : chunk.sceneMap[i]);
|
||||
|
||||
if (version < 32) return;
|
||||
Write(stream, chunk.vertSize);
|
||||
if (chunk.vertSize == 0) return;
|
||||
WriteBytes(stream, static_cast<size_t>(chunk.vertSize) * sizeof(uint16_t));
|
||||
WriteBytes(stream, 16 * sizeof(uint16_t));
|
||||
for (uint32_t i = 0; i < 16; ++i) {
|
||||
const uint16_t size = i < chunk.triangleListSizes.size() ? chunk.triangleListSizes[i] : 0;
|
||||
Write(stream, size);
|
||||
WriteBytes(stream, static_cast<size_t>(size) * sizeof(uint16_t));
|
||||
}
|
||||
}
|
||||
|
||||
inline std::string WriteRaw(const std::vector<TestChunk>& chunks, uint16_t version = 32, uint32_t chunksX = 1, uint32_t chunksZ = 1, uint8_t dev = 0) {
|
||||
std::ostringstream stream;
|
||||
Write(stream, version);
|
||||
Write(stream, dev);
|
||||
if (dev == 0) {
|
||||
Write<uint32_t>(stream, static_cast<uint32_t>(chunks.size()));
|
||||
Write(stream, chunksZ); // the header's "width" is the chunk count along Z
|
||||
Write(stream, chunksX);
|
||||
for (const auto& chunk : chunks) WriteChunk(stream, chunk, version);
|
||||
}
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
struct TestScene {
|
||||
std::string filename;
|
||||
uint32_t id = 0;
|
||||
uint32_t layer = 0;
|
||||
std::string name;
|
||||
uint8_t r = 0, g = 0, b = 0;
|
||||
};
|
||||
|
||||
struct TestTransitionPoint {
|
||||
uint32_t sceneID = 0;
|
||||
uint32_t layerID = 0;
|
||||
NiPoint3 position;
|
||||
};
|
||||
|
||||
// A .luz (version 36 to 41) with no paths
|
||||
inline std::string WriteLuz(uint32_t worldID, const std::vector<TestScene>& scenes, uint8_t boundaryCount, const std::string& rawName,
|
||||
const std::vector<std::pair<TestTransitionPoint, TestTransitionPoint>>& transitions, uint32_t version = 41) {
|
||||
std::ostringstream stream;
|
||||
Write(stream, version);
|
||||
Write<uint32_t>(stream, 5); // revision
|
||||
Write(stream, worldID);
|
||||
if (version >= 38) {
|
||||
Write(stream, NiPoint3(1.0f, 2.0f, 3.0f)); // spawn position
|
||||
for (const float value : { 1.0f, 0.0f, 0.0f, 0.0f }) Write(stream, value); // spawn rotation
|
||||
}
|
||||
// the scene count is a byte before version 37
|
||||
if (version >= 37) Write<uint32_t>(stream, static_cast<uint32_t>(scenes.size()));
|
||||
else Write<uint8_t>(stream, static_cast<uint8_t>(scenes.size()));
|
||||
for (const auto& scene : scenes) {
|
||||
WriteString8(stream, scene.filename);
|
||||
Write(stream, scene.id);
|
||||
Write(stream, scene.layer);
|
||||
WriteString8(stream, scene.name);
|
||||
Write(stream, scene.r);
|
||||
Write(stream, scene.b);
|
||||
Write(stream, scene.g);
|
||||
}
|
||||
|
||||
// Zone boundary lines: normal, point, destination zone, destination scene, spawn location (44 bytes)
|
||||
Write(stream, boundaryCount);
|
||||
for (uint8_t i = 0; i < boundaryCount; ++i) {
|
||||
Write(stream, NiPoint3(0.0f, 0.0f, 1.0f));
|
||||
Write(stream, NiPoint3(10.0f, 0.0f, static_cast<float>(i)));
|
||||
Write<uint16_t>(stream, 1100); // map
|
||||
Write<uint16_t>(stream, i + 1); // instance
|
||||
Write<uint32_t>(stream, 2);
|
||||
Write(stream, NiPoint3(5.0f, 6.0f, 7.0f));
|
||||
}
|
||||
|
||||
WriteString8(stream, rawName);
|
||||
WriteString8(stream, "Test Zone");
|
||||
WriteString8(stream, "A zone for tests");
|
||||
|
||||
Write<uint32_t>(stream, static_cast<uint32_t>(transitions.size()));
|
||||
for (const auto& [from, to] : transitions) {
|
||||
// before version 40 a transition has a name and a width
|
||||
if (version < 40) {
|
||||
WriteString8(stream, "transition");
|
||||
Write(stream, 2.5f);
|
||||
}
|
||||
// five points from version 34 to 38 (the last three repeat the second here), two otherwise
|
||||
const size_t pointCount = version >= 34 && version < 39 ? 5 : 2;
|
||||
for (size_t i = 0; i < pointCount; ++i) {
|
||||
const auto& point = i == 0 ? from : to;
|
||||
Write(stream, point.sceneID);
|
||||
Write(stream, point.layerID);
|
||||
Write(stream, point.position);
|
||||
}
|
||||
}
|
||||
|
||||
Write<uint32_t>(stream, 8); // path data length
|
||||
Write<uint32_t>(stream, 1); // path chunk version
|
||||
Write<uint32_t>(stream, 0); // path count
|
||||
return stream.str();
|
||||
}
|
||||
}
|
||||
243
tests/dGameTests/dZoneManagerTests/ZoneSceneTests.cpp
Normal file
243
tests/dGameTests/dZoneManagerTests/ZoneSceneTests.cpp
Normal file
@@ -0,0 +1,243 @@
|
||||
#include "GameDependencies.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
#include "AssetManager.h"
|
||||
#include "CDZoneTableTable.h"
|
||||
#include "DEVGMCommands.h"
|
||||
#include "Entity.h"
|
||||
#include "TerrainTestWriter.h"
|
||||
#include "Zone.h"
|
||||
#include "dZoneManager.h"
|
||||
|
||||
using namespace TerrainTestWriter;
|
||||
|
||||
// The zone table's storage (CDZoneTableTable.cpp), to add the test zone
|
||||
namespace CDZoneTableTable {
|
||||
extern Table entries;
|
||||
}
|
||||
|
||||
// Loads a written test zone (a .luz and its .raw) through the zone manager
|
||||
class ZoneSceneTest : public GameDependenciesTest {
|
||||
protected:
|
||||
static constexpr LWOMAPID TEST_MAP = 9990;
|
||||
std::filesystem::path root;
|
||||
|
||||
void SetUp() override {
|
||||
SetUpDependencies();
|
||||
root = std::filesystem::temp_directory_path() / (std::string("dlu_zone_scene_test_") + ::testing::UnitTest::GetInstance()->current_test_info()->name());
|
||||
std::filesystem::create_directories(root / "res" / "maps" / "testzone");
|
||||
std::ofstream(root / "res" / "CDServer.sqlite").close(); // marks an unpacked client
|
||||
Game::assetManager = new AssetManager(root);
|
||||
|
||||
CDZoneTable zone{};
|
||||
zone.zoneID = TEST_MAP;
|
||||
zone.zoneName = "testzone/test.luz";
|
||||
CDZoneTableTable::entries[TEST_MAP] = zone;
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
CDZoneTableTable::entries.erase(TEST_MAP);
|
||||
delete Game::assetManager;
|
||||
Game::assetManager = nullptr;
|
||||
std::filesystem::remove_all(root);
|
||||
TearDownDependencies();
|
||||
}
|
||||
|
||||
void WriteFile(const std::string& name, const std::string& data) const {
|
||||
std::ofstream file(root / "res" / "maps" / "testzone" / name, std::ios::binary);
|
||||
file.write(data.data(), data.size());
|
||||
}
|
||||
|
||||
// Scenes 0 (global), 1, 2 and 3 (general) and 5 (audio, layer 1); transitions 1 <-> 2 and 2 <-> 3
|
||||
void WriteZone(const std::string& rawName = "test.raw", uint8_t boundaryCount = 2, uint32_t version = 41) const {
|
||||
const std::vector<TestScene> scenes = {
|
||||
{ "global.lvl", 0, 0, "Global", 10, 20, 30 },
|
||||
{ "one.lvl", 1, 0, "Scene One", 255, 0, 0 },
|
||||
{ "two.lvl", 2, 0, "Scene Two", 0, 255, 0 },
|
||||
{ "three.lvl", 3, 0, "Scene Three", 0, 0, 255 },
|
||||
{ "audio.lvl", 5, 1, "Audio", 0, 0, 0 },
|
||||
};
|
||||
WriteFile("test.luz", WriteLuz(TEST_MAP, scenes, boundaryCount, rawName, {
|
||||
{ { 1, 0, NiPoint3(1.0f, 0.0f, 0.0f) }, { 2, 0, NiPoint3(2.0f, 0.0f, 0.0f) } },
|
||||
{ { 2, 0, NiPoint3(3.0f, 0.0f, 0.0f) }, { 3, 0, NiPoint3(4.0f, 0.0f, 0.0f) } },
|
||||
}, version));
|
||||
}
|
||||
|
||||
// One 3x3 point chunk from (0, 0) to (4, 4) whose 2x2 scene map holds scenes 1 2 / 3 1
|
||||
static std::string MakeRaw() {
|
||||
TestChunk chunk;
|
||||
chunk.width = 3;
|
||||
chunk.height = 3;
|
||||
chunk.scale = 2.0f;
|
||||
chunk.colorMapResolution = 2;
|
||||
chunk.sceneMap = { 1, 2, 3, 1 };
|
||||
return WriteRaw({ chunk });
|
||||
}
|
||||
|
||||
Zone* LoadZone() const {
|
||||
Game::zoneManager->LoadZone(LWOZONEID(TEST_MAP, 0, 0));
|
||||
Zone* zone = Game::zoneManager->GetZoneMut();
|
||||
zone->LoadZoneIntoMemory(); // the levels the scenes name aren't written
|
||||
Game::zoneManager->BuildSceneGraph();
|
||||
return zone;
|
||||
}
|
||||
|
||||
static bool Contains(const std::vector<LWOSCENEID>& scenes, LWOSCENEID scene) {
|
||||
return std::ranges::find(scenes, scene) != scenes.end();
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(ZoneSceneTest, ReadsScenesBoundariesAndTransitions) {
|
||||
WriteZone();
|
||||
WriteFile("test.raw", MakeRaw());
|
||||
const Zone* zone = LoadZone();
|
||||
|
||||
EXPECT_EQ(zone->GetWorldID(), TEST_MAP);
|
||||
EXPECT_EQ(zone->GetZoneRawPath(), "test.raw");
|
||||
EXPECT_EQ(zone->GetZoneName(), "Test Zone"); // read after the boundary lines
|
||||
EXPECT_EQ(zone->GetZonePath(), "maps/testzone/");
|
||||
|
||||
const auto& boundaries = zone->GetBoundaries();
|
||||
ASSERT_EQ(boundaries.size(), 2);
|
||||
EXPECT_FLOAT_EQ(boundaries[1].normal.z, 1.0f);
|
||||
EXPECT_FLOAT_EQ(boundaries[1].point.x, 10.0f);
|
||||
EXPECT_FLOAT_EQ(boundaries[1].point.z, 1.0f);
|
||||
EXPECT_EQ(boundaries[1].destinationZone.GetMapID(), 1100);
|
||||
EXPECT_EQ(boundaries[1].destinationZone.GetInstanceID(), 2);
|
||||
EXPECT_EQ(boundaries[1].destinationZone.GetCloneID(), 0);
|
||||
EXPECT_EQ(boundaries[1].destinationSceneID, 2);
|
||||
EXPECT_FLOAT_EQ(boundaries[1].spawnLocation.y, 6.0f);
|
||||
|
||||
ASSERT_EQ(zone->GetScenes().size(), 5);
|
||||
const SceneRef* one = zone->GetScene(LWOSCENEID(1, 0));
|
||||
ASSERT_NE(one, nullptr);
|
||||
EXPECT_EQ(one->name, "Scene One");
|
||||
EXPECT_EQ(one->sceneType, eSceneType::General);
|
||||
EXPECT_FLOAT_EQ(one->color.m_Red, 1.0f);
|
||||
const SceneRef* global = zone->GetScene(LWOSCENEID(0, 0));
|
||||
ASSERT_NE(global, nullptr);
|
||||
// stored in the file as red, blue, green
|
||||
EXPECT_FLOAT_EQ(global->color.m_Red, 10 / 255.0f);
|
||||
EXPECT_FLOAT_EQ(global->color.m_Green, 20 / 255.0f);
|
||||
EXPECT_FLOAT_EQ(global->color.m_Blue, 30 / 255.0f);
|
||||
const SceneRef* audio = zone->GetScene(LWOSCENEID(5, 1));
|
||||
ASSERT_NE(audio, nullptr);
|
||||
EXPECT_EQ(audio->sceneType, eSceneType::Audio);
|
||||
EXPECT_EQ(zone->GetScene(LWOSCENEID(4, 0)), nullptr);
|
||||
|
||||
const auto& transitions = zone->GetSceneTransitions();
|
||||
ASSERT_EQ(transitions.size(), 2);
|
||||
ASSERT_EQ(transitions[1].points.size(), 2);
|
||||
EXPECT_EQ(transitions[1].points[0].sceneID, LWOSCENEID(2, 0));
|
||||
EXPECT_EQ(transitions[1].points[1].sceneID, LWOSCENEID(3, 0));
|
||||
EXPECT_FLOAT_EQ(transitions[1].points[1].position.x, 4.0f);
|
||||
}
|
||||
|
||||
TEST_F(ZoneSceneTest, ZoneWithoutBoundaryLines) {
|
||||
WriteZone("test.raw", 0);
|
||||
WriteFile("test.raw", MakeRaw());
|
||||
const Zone* zone = LoadZone();
|
||||
EXPECT_EQ(zone->GetZoneName(), "Test Zone");
|
||||
EXPECT_TRUE(zone->GetBoundaries().empty());
|
||||
EXPECT_EQ(zone->GetSceneTransitions().size(), 2);
|
||||
}
|
||||
|
||||
// Version 37 has a u32 scene count (a byte before it) and no spawn point; 36 and 37 have named five point transitions
|
||||
TEST_F(ZoneSceneTest, ReadsOlderZoneVersions) {
|
||||
for (const uint32_t version : { 36u, 37u, 38u, 39u, 40u }) {
|
||||
WriteZone("test.raw", 2, version);
|
||||
WriteFile("test.raw", MakeRaw());
|
||||
const Zone* zone = LoadZone();
|
||||
EXPECT_EQ(zone->GetScenes().size(), 5) << version;
|
||||
EXPECT_EQ(zone->GetBoundaries().size(), 2) << version;
|
||||
EXPECT_EQ(zone->GetZoneRawPath(), "test.raw") << version;
|
||||
EXPECT_EQ(zone->GetZoneName(), "Test Zone") << version;
|
||||
ASSERT_EQ(zone->GetSceneTransitions().size(), 2) << version;
|
||||
const auto& transition = zone->GetSceneTransitions()[1];
|
||||
EXPECT_EQ(transition.points.size(), version < 39 ? 5 : 2) << version;
|
||||
EXPECT_EQ(transition.points[1].sceneID, LWOSCENEID(3, 0)) << version;
|
||||
if (version < 40) {
|
||||
EXPECT_EQ(transition.name, "transition") << version;
|
||||
EXPECT_FLOAT_EQ(transition.width, 2.5f) << version;
|
||||
}
|
||||
EXPECT_FALSE(zone->GetZoneRaw().chunks.empty()) << version;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ZoneSceneTest, SceneAtPosition) {
|
||||
WriteZone();
|
||||
WriteFile("test.raw", MakeRaw());
|
||||
LoadZone();
|
||||
|
||||
EXPECT_EQ(Game::zoneManager->GetSceneIDFromPosition(NiPoint3(0.5f, 100.0f, 0.5f)), LWOSCENEID(1, 0));
|
||||
EXPECT_EQ(Game::zoneManager->GetSceneIDFromPosition(NiPoint3(0.5f, -50.0f, 3.5f)), LWOSCENEID(2, 0));
|
||||
EXPECT_EQ(Game::zoneManager->GetSceneIDFromPosition(NiPoint3(3.5f, 0.0f, 0.5f)), LWOSCENEID(3, 0));
|
||||
EXPECT_EQ(Game::zoneManager->GetSceneIDFromPosition(NiPoint3(50.0f, 0.0f, 50.0f)), LWOSCENEID(1, 0));
|
||||
}
|
||||
|
||||
TEST_F(ZoneSceneTest, SceneGraphFromTransitions) {
|
||||
WriteZone();
|
||||
WriteFile("test.raw", MakeRaw());
|
||||
LoadZone();
|
||||
|
||||
const auto& graph = Game::zoneManager->GetSceneAdjacencyList();
|
||||
EXPECT_EQ(graph.size(), 4); // the audio scene is left out
|
||||
EXPECT_FALSE(graph.contains(LWOSCENEID(5, 1)));
|
||||
|
||||
const auto one = Game::zoneManager->GetAdjacentScenes(LWOSCENEID(1, 0));
|
||||
EXPECT_EQ(one.size(), 2);
|
||||
EXPECT_TRUE(Contains(one, LWOSCENEID(2, 0)));
|
||||
EXPECT_TRUE(Contains(one, LWOSCENEID(0, 0)));
|
||||
|
||||
const auto two = Game::zoneManager->GetAdjacentScenes(LWOSCENEID(2, 0));
|
||||
EXPECT_EQ(two.size(), 3);
|
||||
EXPECT_TRUE(Contains(two, LWOSCENEID(1, 0)));
|
||||
EXPECT_TRUE(Contains(two, LWOSCENEID(3, 0)));
|
||||
|
||||
// the global scene is next to every other scene
|
||||
const auto global = Game::zoneManager->GetAdjacentScenes(LWOSCENEID(0, 0));
|
||||
EXPECT_EQ(global.size(), 3);
|
||||
EXPECT_FALSE(Contains(global, LWOSCENEID(0, 0)));
|
||||
|
||||
EXPECT_TRUE(Game::zoneManager->GetAdjacentScenes(LWOSCENEID(9, 0)).empty());
|
||||
|
||||
// building again doesn't add anything twice
|
||||
Game::zoneManager->BuildSceneGraph();
|
||||
EXPECT_EQ(Game::zoneManager->GetAdjacentScenes(LWOSCENEID(2, 0)).size(), 3);
|
||||
}
|
||||
|
||||
TEST_F(ZoneSceneTest, MissingRawStillLoadsTheZone) {
|
||||
WriteZone("missing.raw");
|
||||
const Zone* zone = LoadZone();
|
||||
EXPECT_EQ(zone->GetZoneName(), "Test Zone");
|
||||
EXPECT_EQ(zone->GetSceneTransitions().size(), 2);
|
||||
EXPECT_TRUE(zone->GetZoneRaw().chunks.empty());
|
||||
EXPECT_EQ(Game::zoneManager->GetSceneIDFromPosition(NiPoint3(0.5f, 0.0f, 0.5f)), LWOSCENEID_INVALID);
|
||||
}
|
||||
|
||||
TEST_F(ZoneSceneTest, UnreadableRawStillLoadsTheZone) {
|
||||
WriteZone();
|
||||
const auto raw = MakeRaw();
|
||||
WriteFile("test.raw", raw.substr(0, raw.size() / 2));
|
||||
const Zone* zone = LoadZone();
|
||||
EXPECT_EQ(zone->GetZoneName(), "Test Zone");
|
||||
EXPECT_TRUE(zone->GetZoneRaw().chunks.empty());
|
||||
EXPECT_EQ(Game::zoneManager->GetSceneIDFromPosition(NiPoint3(0.5f, 0.0f, 0.5f)), LWOSCENEID_INVALID);
|
||||
}
|
||||
|
||||
TEST_F(ZoneSceneTest, SceneCommandsReport) {
|
||||
WriteZone();
|
||||
WriteFile("test.raw", MakeRaw());
|
||||
LoadZone();
|
||||
|
||||
EntityInfo playerInfo = info;
|
||||
playerInfo.pos = NiPoint3(0.5f, 0.0f, 0.5f); // scene 1
|
||||
auto player = std::make_unique<Entity>(1, playerInfo);
|
||||
|
||||
// the scene commands only report; they must not fail
|
||||
DEVGMCommands::GetScene(player.get(), UNASSIGNED_SYSTEM_ADDRESS, "");
|
||||
DEVGMCommands::GetAdjacentScenes(player.get(), UNASSIGNED_SYSTEM_ADDRESS, "");
|
||||
}
|
||||
Reference in New Issue
Block a user