From 3ebc6709dbdbbc6ae3f49447e6ae89c6053f6777 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Mon, 5 May 2025 00:17:39 -0700 Subject: [PATCH 1/6] feat: Property behaviors partially functional (#1759) * most of gameplay tab works * smash unsmash and wait working * Add pausing of models and behaviors * working basic behaviors * play sound functioning * add resetting * Fix asynchronous actions executing other strips actions * Add comments, remove dead code etc. * Skip Smashes if they coincide with a UnSmash Remove debug logs Comment on return --- dCommon/DluAssert.h | 2 +- dGame/dComponents/ModelComponent.cpp | 78 ++++++++- dGame/dComponents/ModelComponent.h | 33 +++- .../PropertyManagementComponent.cpp | 28 ++++ dGame/dGameMessages/GameMessageHandler.cpp | 6 +- dGame/dGameMessages/GameMessages.cpp | 114 ++++++++------ dGame/dGameMessages/GameMessages.h | 53 ++++++- dGame/dPropertyBehaviors/PropertyBehavior.cpp | 19 +++ dGame/dPropertyBehaviors/PropertyBehavior.h | 6 + dGame/dPropertyBehaviors/State.cpp | 14 ++ dGame/dPropertyBehaviors/State.h | 5 + dGame/dPropertyBehaviors/Strip.cpp | 148 +++++++++++++++++- dGame/dPropertyBehaviors/Strip.h | 23 +++ 13 files changed, 465 insertions(+), 64 deletions(-) diff --git a/dCommon/DluAssert.h b/dCommon/DluAssert.h index c54dd54e..f099443a 100644 --- a/dCommon/DluAssert.h +++ b/dCommon/DluAssert.h @@ -4,7 +4,7 @@ #include #ifdef _DEBUG -# define DluAssert(expression) assert(expression) +# define DluAssert(expression) do { assert(expression) } while(0) #else # define DluAssert(expression) #endif diff --git a/dGame/dComponents/ModelComponent.cpp b/dGame/dComponents/ModelComponent.cpp index 3a273c9b..730aefa2 100644 --- a/dGame/dComponents/ModelComponent.cpp +++ b/dGame/dComponents/ModelComponent.cpp @@ -10,12 +10,50 @@ #include "SimplePhysicsComponent.h" #include "Database.h" +#include "DluAssert.h" ModelComponent::ModelComponent(Entity* parent) : Component(parent) { m_OriginalPosition = m_Parent->GetDefaultPosition(); m_OriginalRotation = m_Parent->GetDefaultRotation(); + m_IsPaused = false; + m_NumListeningInteract = 0; m_userModelID = m_Parent->GetVarAs(u"userModelID"); + RegisterMsg(MessageType::Game::REQUEST_USE, this, &ModelComponent::OnRequestUse); + RegisterMsg(MessageType::Game::RESET_MODEL_TO_DEFAULTS, this, &ModelComponent::OnResetModelToDefaults); +} + +bool ModelComponent::OnResetModelToDefaults(GameMessages::GameMsg& msg) { + auto& reset = static_cast(msg); + for (auto& behavior : m_Behaviors) behavior.HandleMsg(reset); + GameMessages::UnSmash unsmash; + unsmash.target = GetParent()->GetObjectID(); + unsmash.duration = 0.0f; + unsmash.Send(UNASSIGNED_SYSTEM_ADDRESS); + m_NumListeningInteract = 0; + m_NumActiveUnSmash = 0; + m_Dirty = true; + Game::entityManager->SerializeEntity(GetParent()); + return true; +} + +bool ModelComponent::OnRequestUse(GameMessages::GameMsg& msg) { + bool toReturn = false; + if (!m_IsPaused) { + auto& requestUse = static_cast(msg); + for (auto& behavior : m_Behaviors) behavior.HandleMsg(requestUse); + toReturn = true; + } + + return toReturn; +} + +void ModelComponent::Update(float deltaTime) { + if (m_IsPaused) return; + + for (auto& behavior : m_Behaviors) { + behavior.Update(deltaTime, *this); + } } void ModelComponent::LoadBehaviors() { @@ -29,9 +67,9 @@ void ModelComponent::LoadBehaviors() { LOG_DEBUG("Loading behavior %d", behaviorId.value()); auto& inserted = m_Behaviors.emplace_back(); inserted.SetBehaviorId(*behaviorId); - + const auto behaviorStr = Database::Get()->GetBehavior(behaviorId.value()); - + tinyxml2::XMLDocument behaviorXml; auto res = behaviorXml.Parse(behaviorStr.c_str(), behaviorStr.size()); LOG_DEBUG("Behavior %i %d: %s", res, behaviorId.value(), behaviorStr.c_str()); @@ -45,6 +83,11 @@ void ModelComponent::LoadBehaviors() { } } +void ModelComponent::Resume() { + m_Dirty = true; + m_IsPaused = false; +} + void ModelComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) { // ItemComponent Serialization. Pets do not get this serialization. if (!m_Parent->HasComponent(eReplicaComponentType::PET)) { @@ -56,14 +99,14 @@ void ModelComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialU //actual model component: outBitStream.Write1(); // Yes we are writing model info - outBitStream.Write0(); // Is pickable + outBitStream.Write(m_NumListeningInteract > 0); // Is pickable outBitStream.Write(2); // Physics type outBitStream.Write(m_OriginalPosition); // Original position outBitStream.Write(m_OriginalRotation); // Original rotation outBitStream.Write1(); // We are writing behavior info - outBitStream.Write(0); // Number of behaviors - outBitStream.Write1(); // Is this model paused + outBitStream.Write(m_Behaviors.size()); // Number of behaviors + outBitStream.Write(m_IsPaused); // Is this model paused if (bIsInitialUpdate) outBitStream.Write0(); // We are not writing model editing info } @@ -135,3 +178,28 @@ std::array, 5> ModelComponent::GetBehaviorsForSa } return toReturn; } + +void ModelComponent::AddInteract() { + LOG_DEBUG("Adding interact %i", m_NumListeningInteract); + m_Dirty = true; + m_NumListeningInteract++; +} + +void ModelComponent::RemoveInteract() { + DluAssert(m_NumListeningInteract > 0); + LOG_DEBUG("Removing interact %i", m_NumListeningInteract); + m_Dirty = true; + m_NumListeningInteract--; +} + +void ModelComponent::AddUnSmash() { + LOG_DEBUG("Adding UnSmash %i", m_NumActiveUnSmash); + m_NumActiveUnSmash++; +} + +void ModelComponent::RemoveUnSmash() { + // Players can assign an UnSmash without a Smash so an assert would be bad here + if (m_NumActiveUnSmash == 0) return; + LOG_DEBUG("Removing UnSmash %i", m_NumActiveUnSmash); + m_NumActiveUnSmash--; +} diff --git a/dGame/dComponents/ModelComponent.h b/dGame/dComponents/ModelComponent.h index 12ef7744..e63a7f0e 100644 --- a/dGame/dComponents/ModelComponent.h +++ b/dGame/dComponents/ModelComponent.h @@ -30,6 +30,10 @@ public: ModelComponent(Entity* parent); void LoadBehaviors(); + void Update(float deltaTime) override; + + bool OnRequestUse(GameMessages::GameMsg& msg); + bool OnResetModelToDefaults(GameMessages::GameMsg& msg); void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) override; @@ -59,7 +63,7 @@ public: /** * Main gateway for all behavior messages to be passed to their respective behaviors. - * + * * @tparam Msg The message type to pass * @param args the arguments of the message to be deserialized */ @@ -68,7 +72,7 @@ public: static_assert(std::is_base_of_v, "Msg must be a BehaviorMessageBase"); Msg msg{ args }; for (auto&& behavior : m_Behaviors) { - if (behavior.GetBehaviorId() == msg.GetBehaviorId()) { + if (behavior.GetBehaviorId() == msg.GetBehaviorId()) { behavior.HandleMsg(msg); return; } @@ -109,12 +113,35 @@ public: void SendBehaviorListToClient(AMFArrayValue& args) const; void SendBehaviorBlocksToClient(int32_t behaviorToSend, AMFArrayValue& args) const; - + void VerifyBehaviors(); std::array, 5> GetBehaviorsForSave() const; + const std::vector& GetBehaviors() const { return m_Behaviors; }; + + void AddInteract(); + void RemoveInteract(); + + void Pause() { m_Dirty = true; m_IsPaused = true; } + + void AddUnSmash(); + void RemoveUnSmash(); + bool IsUnSmashing() const { return m_NumActiveUnSmash != 0; } + + void Resume(); private: + // Number of Actions that are awaiting an UnSmash to finish. + uint32_t m_NumActiveUnSmash{}; + + // Whether or not this component needs to have its extra data serialized. + bool m_Dirty{}; + + // The number of strips listening for a RequestUse GM to come in. + uint32_t m_NumListeningInteract{}; + + // Whether or not the model is paused and should reject all interactions regarding behaviors. + bool m_IsPaused{}; /** * The behaviors of the model * Note: This is a vector because the order of the behaviors matters when serializing to the client. diff --git a/dGame/dComponents/PropertyManagementComponent.cpp b/dGame/dComponents/PropertyManagementComponent.cpp index 4fae6d50..d866e0ad 100644 --- a/dGame/dComponents/PropertyManagementComponent.cpp +++ b/dGame/dComponents/PropertyManagementComponent.cpp @@ -255,6 +255,18 @@ void PropertyManagementComponent::OnStartBuilding() { // Push equipped items if (inventoryComponent) inventoryComponent->PushEquippedItems(); + + for (auto modelID : models | std::views::keys) { + auto* model = Game::entityManager->GetEntity(modelID); + if (model) { + auto* modelComponent = model->GetComponent(); + if (modelComponent) modelComponent->Pause(); + Game::entityManager->SerializeEntity(model); + GameMessages::ResetModelToDefaults reset; + reset.target = modelID; + model->HandleMsg(reset); + } + } } void PropertyManagementComponent::OnFinishBuilding() { @@ -267,6 +279,18 @@ void PropertyManagementComponent::OnFinishBuilding() { UpdateApprovedStatus(false); Save(); + + for (auto modelID : models | std::views::keys) { + auto* model = Game::entityManager->GetEntity(modelID); + if (model) { + auto* modelComponent = model->GetComponent(); + if (modelComponent) modelComponent->Resume(); + Game::entityManager->SerializeEntity(model); + GameMessages::ResetModelToDefaults reset; + reset.target = modelID; + model->HandleMsg(reset); + } + } } void PropertyManagementComponent::UpdateModelPosition(const LWOOBJID id, const NiPoint3 position, NiQuaternion rotation) { @@ -318,6 +342,8 @@ void PropertyManagementComponent::UpdateModelPosition(const LWOOBJID id, const N Entity* newEntity = Game::entityManager->CreateEntity(info); if (newEntity != nullptr) { Game::entityManager->ConstructEntity(newEntity); + auto* modelComponent = newEntity->GetComponent(); + if (modelComponent) modelComponent->Pause(); // Make sure the propMgmt doesn't delete our model after the server dies // Trying to do this after the entity is constructed. Shouldn't really change anything but @@ -363,6 +389,8 @@ void PropertyManagementComponent::UpdateModelPosition(const LWOOBJID id, const N info.nodes[0]->config.push_back(new LDFData(u"componentWhitelist", 1)); auto* model = spawner->Spawn(); + auto* modelComponent = model->GetComponent(); + if (modelComponent) modelComponent->Pause(); models.insert_or_assign(model->GetObjectID(), spawnerId); diff --git a/dGame/dGameMessages/GameMessageHandler.cpp b/dGame/dGameMessages/GameMessageHandler.cpp index ef890401..b201f999 100644 --- a/dGame/dGameMessages/GameMessageHandler.cpp +++ b/dGame/dGameMessages/GameMessageHandler.cpp @@ -45,6 +45,7 @@ namespace { using namespace GameMessages; using MessageCreator = std::function()>; std::map g_MessageHandlers = { + { REQUEST_USE, []() { return std::make_unique(); }}, { REQUEST_SERVER_OBJECT_INFO, []() { return std::make_unique(); } }, { SHOOTING_GALLERY_FIRE, []() { return std::make_unique(); } }, }; @@ -118,11 +119,6 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System break; } - case MessageType::Game::REQUEST_USE: { - GameMessages::HandleRequestUse(inStream, entity, sysAddr); - break; - } - case MessageType::Game::SET_FLAG: { GameMessages::HandleSetFlag(inStream, entity); break; diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index 6fd7576f..1f60017b 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -4954,54 +4954,6 @@ void GameMessages::HandleQuickBuildCancel(RakNet::BitStream& inStream, Entity* e quickBuildComponent->CancelQuickBuild(Game::entityManager->GetEntity(userID), eQuickBuildFailReason::CANCELED_EARLY); } -void GameMessages::HandleRequestUse(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - bool bIsMultiInteractUse = false; - unsigned int multiInteractID; - int multiInteractType; - bool secondary; - LWOOBJID objectID; - - inStream.Read(bIsMultiInteractUse); - inStream.Read(multiInteractID); - inStream.Read(multiInteractType); - inStream.Read(objectID); - inStream.Read(secondary); - - Entity* interactedObject = Game::entityManager->GetEntity(objectID); - - if (interactedObject == nullptr) { - LOG("Object %llu tried to interact, but doesn't exist!", objectID); - - return; - } - - if (interactedObject->GetLOT() == 9524) { - entity->GetCharacter()->SetBuildMode(true); - } - - if (bIsMultiInteractUse) { - if (multiInteractType == 0) { - auto* missionOfferComponent = static_cast(interactedObject->GetComponent(eReplicaComponentType::MISSION_OFFER)); - - if (missionOfferComponent != nullptr) { - missionOfferComponent->OfferMissions(entity, multiInteractID); - } - } else { - interactedObject->OnUse(entity); - } - } else { - interactedObject->OnUse(entity); - } - - //Perform use task if possible: - auto missionComponent = static_cast(entity->GetComponent(eReplicaComponentType::MISSION)); - - if (missionComponent == nullptr) return; - - missionComponent->Progress(eMissionTaskType::TALK_TO_NPC, interactedObject->GetLOT(), interactedObject->GetObjectID()); - missionComponent->Progress(eMissionTaskType::INTERACT, interactedObject->GetLOT(), interactedObject->GetObjectID()); -} - void GameMessages::HandlePlayEmote(RakNet::BitStream& inStream, Entity* entity) { int emoteID; LWOOBJID targetID; @@ -6443,4 +6395,70 @@ namespace GameMessages { auto* handlingEntity = Game::entityManager->GetEntity(targetForReport); if (handlingEntity) handlingEntity->HandleMsg(*this); } + + bool RequestUse::Deserialize(RakNet::BitStream& stream) { + if (!stream.Read(bIsMultiInteractUse)) return false; + if (!stream.Read(multiInteractID)) return false; + if (!stream.Read(multiInteractType)) return false; + if (!stream.Read(object)) return false; + if (!stream.Read(secondary)) return false; + return true; + } + + void RequestUse::Handle(Entity& entity, const SystemAddress& sysAddr) { + Entity* interactedObject = Game::entityManager->GetEntity(object); + + if (interactedObject == nullptr) { + LOG("Object %llu tried to interact, but doesn't exist!", object); + + return; + } + + if (interactedObject->GetLOT() == 9524) { + entity.GetCharacter()->SetBuildMode(true); + } + + if (bIsMultiInteractUse) { + if (multiInteractType == 0) { + auto* missionOfferComponent = static_cast(interactedObject->GetComponent(eReplicaComponentType::MISSION_OFFER)); + + if (missionOfferComponent != nullptr) { + missionOfferComponent->OfferMissions(&entity, multiInteractID); + } + } else { + interactedObject->OnUse(&entity); + } + } else { + interactedObject->OnUse(&entity); + } + + interactedObject->HandleMsg(*this); + + //Perform use task if possible: + auto missionComponent = entity.GetComponent(); + + if (!missionComponent) return; + + missionComponent->Progress(eMissionTaskType::TALK_TO_NPC, interactedObject->GetLOT(), interactedObject->GetObjectID()); + missionComponent->Progress(eMissionTaskType::INTERACT, interactedObject->GetLOT(), interactedObject->GetObjectID()); + } + + void Smash::Serialize(RakNet::BitStream& stream) const { + stream.Write(bIgnoreObjectVisibility); + stream.Write(force); + stream.Write(ghostCapacity); + stream.Write(killerID); + } + + void UnSmash::Serialize(RakNet::BitStream& stream) const { + stream.Write(builderID != LWOOBJID_EMPTY); + if (builderID != LWOOBJID_EMPTY) stream.Write(builderID); + stream.Write(duration != 3.0f); + if (builderID != 3.0f) stream.Write(duration); + } + + void PlayBehaviorSound::Serialize(RakNet::BitStream& stream) const { + stream.Write(soundID != -1); + if (soundID != -1) stream.Write(soundID); + } } diff --git a/dGame/dGameMessages/GameMessages.h b/dGame/dGameMessages/GameMessages.h index c3889674..a53fa647 100644 --- a/dGame/dGameMessages/GameMessages.h +++ b/dGame/dGameMessages/GameMessages.h @@ -631,7 +631,6 @@ namespace GameMessages { void HandleFireEventServerSide(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); void HandleRequestPlatformResync(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); void HandleQuickBuildCancel(RakNet::BitStream& inStream, Entity* entity); - void HandleRequestUse(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); void HandlePlayEmote(RakNet::BitStream& inStream, Entity* entity); void HandleModularBuildConvertModel(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); void HandleSetFlag(RakNet::BitStream& inStream, Entity* entity); @@ -782,6 +781,58 @@ namespace GameMessages { bool Deserialize(RakNet::BitStream& bitStream) override; void Handle(Entity& entity, const SystemAddress& sysAddr) override; }; + + struct RequestUse : public GameMsg { + RequestUse() : GameMsg(MessageType::Game::REQUEST_USE) {} + + bool Deserialize(RakNet::BitStream& stream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + LWOOBJID object{}; + + bool secondary{ false }; + + // Set to true if this coming from a multi-interaction UI on the client. + bool bIsMultiInteractUse{}; + + // Used only for multi-interaction + unsigned int multiInteractID{}; + + // Used only for multi-interaction, is of the enum type InteractionType + int multiInteractType{}; + }; + + struct Smash : public GameMsg { + Smash() : GameMsg(MessageType::Game::SMASH) {} + + void Serialize(RakNet::BitStream& stream) const; + + bool bIgnoreObjectVisibility{}; + bool force{}; + float ghostCapacity{}; + LWOOBJID killerID{}; + }; + + struct UnSmash : public GameMsg { + UnSmash() : GameMsg(MessageType::Game::UN_SMASH) {} + + void Serialize(RakNet::BitStream& stream) const; + + LWOOBJID builderID{ LWOOBJID_EMPTY }; + float duration{ 3.0f }; + }; + + struct PlayBehaviorSound : public GameMsg { + PlayBehaviorSound() : GameMsg(MessageType::Game::PLAY_BEHAVIOR_SOUND) {} + + void Serialize(RakNet::BitStream& stream) const; + + int32_t soundID{ -1 }; + }; + + struct ResetModelToDefaults : public GameMsg { + ResetModelToDefaults() : GameMsg(MessageType::Game::RESET_MODEL_TO_DEFAULTS) {} + }; }; #endif // GAMEMESSAGES_H diff --git a/dGame/dPropertyBehaviors/PropertyBehavior.cpp b/dGame/dPropertyBehaviors/PropertyBehavior.cpp index 5bdb5827..c4c9d359 100644 --- a/dGame/dPropertyBehaviors/PropertyBehavior.cpp +++ b/dGame/dPropertyBehaviors/PropertyBehavior.cpp @@ -4,9 +4,13 @@ #include "BehaviorStates.h" #include "ControlBehaviorMsgs.h" #include "tinyxml2.h" +#include "ModelComponent.h" + +#include PropertyBehavior::PropertyBehavior() { m_LastEditedState = BehaviorState::HOME_STATE; + m_ActiveState = BehaviorState::HOME_STATE; } template<> @@ -84,6 +88,17 @@ void PropertyBehavior::HandleMsg(AddMessage& msg) { isLoot = m_BehaviorId != 7965; }; +template<> +void PropertyBehavior::HandleMsg(GameMessages::RequestUse& msg) { + m_States[m_ActiveState].HandleMsg(msg); +} + +template<> +void PropertyBehavior::HandleMsg(GameMessages::ResetModelToDefaults& msg) { + m_ActiveState = BehaviorState::HOME_STATE; + for (auto& state : m_States | std::views::values) state.HandleMsg(msg); +} + void PropertyBehavior::SendBehaviorListToClient(AMFArrayValue& args) const { args.Insert("id", std::to_string(m_BehaviorId)); args.Insert("name", m_Name); @@ -153,3 +168,7 @@ void PropertyBehavior::Deserialize(const tinyxml2::XMLElement& behavior) { m_States[static_cast(stateId)].Deserialize(*stateElement); } } + +void PropertyBehavior::Update(float deltaTime, ModelComponent& modelComponent) { + for (auto& state : m_States | std::views::values) state.Update(deltaTime, modelComponent); +} diff --git a/dGame/dPropertyBehaviors/PropertyBehavior.h b/dGame/dPropertyBehaviors/PropertyBehavior.h index 01eb1968..5232d7f0 100644 --- a/dGame/dPropertyBehaviors/PropertyBehavior.h +++ b/dGame/dPropertyBehaviors/PropertyBehavior.h @@ -10,6 +10,7 @@ namespace tinyxml2 { enum class BehaviorState : uint32_t; class AMFArrayValue; +class ModelComponent; /** * Represents the Entity of a Property Behavior and holds data associated with the behavior @@ -31,7 +32,12 @@ public: void Serialize(tinyxml2::XMLElement& behavior) const; void Deserialize(const tinyxml2::XMLElement& behavior); + + void Update(float deltaTime, ModelComponent& modelComponent); + private: + // The current active behavior state. Behaviors can only be in ONE state at a time. + BehaviorState m_ActiveState; // The states this behavior has. std::map m_States; diff --git a/dGame/dPropertyBehaviors/State.cpp b/dGame/dPropertyBehaviors/State.cpp index 1fb072c1..0655c78c 100644 --- a/dGame/dPropertyBehaviors/State.cpp +++ b/dGame/dPropertyBehaviors/State.cpp @@ -117,6 +117,16 @@ void State::HandleMsg(MigrateActionsMessage& msg) { } }; +template<> +void State::HandleMsg(GameMessages::RequestUse& msg) { + for (auto& strip : m_Strips) strip.HandleMsg(msg); +} + +template<> +void State::HandleMsg(GameMessages::ResetModelToDefaults& msg) { + for (auto& strip : m_Strips) strip.HandleMsg(msg); +} + bool State::IsEmpty() const { for (const auto& strip : m_Strips) { if (!strip.IsEmpty()) return false; @@ -152,3 +162,7 @@ void State::Deserialize(const tinyxml2::XMLElement& state) { strip.Deserialize(*stripElement); } } + +void State::Update(float deltaTime, ModelComponent& modelComponent) { + for (auto& strip : m_Strips) strip.Update(deltaTime, modelComponent); +} diff --git a/dGame/dPropertyBehaviors/State.h b/dGame/dPropertyBehaviors/State.h index 3e8c827f..ab6d76a4 100644 --- a/dGame/dPropertyBehaviors/State.h +++ b/dGame/dPropertyBehaviors/State.h @@ -8,6 +8,7 @@ namespace tinyxml2 { } class AMFArrayValue; +class ModelComponent; class State { public: @@ -19,7 +20,11 @@ public: void Serialize(tinyxml2::XMLElement& state) const; void Deserialize(const tinyxml2::XMLElement& state); + + void Update(float deltaTime, ModelComponent& modelComponent); private: + + // The strips contained within this state. std::vector m_Strips; }; diff --git a/dGame/dPropertyBehaviors/Strip.cpp b/dGame/dPropertyBehaviors/Strip.cpp index d6dc050b..b1dd94eb 100644 --- a/dGame/dPropertyBehaviors/Strip.cpp +++ b/dGame/dPropertyBehaviors/Strip.cpp @@ -3,6 +3,11 @@ #include "Amf3.h" #include "ControlBehaviorMsgs.h" #include "tinyxml2.h" +#include "dEntity/EntityInfo.h" +#include "ModelComponent.h" +#include "PlayerManager.h" + +#include "DluAssert.h" template <> void Strip::HandleMsg(AddStripMessage& msg) { @@ -75,7 +80,138 @@ void Strip::HandleMsg(MigrateActionsMessage& msg) { } else { m_Actions.insert(m_Actions.begin() + msg.GetDstActionIndex(), msg.GetMigratedActions().begin(), msg.GetMigratedActions().end()); } -}; +} + +template<> +void Strip::HandleMsg(GameMessages::RequestUse& msg) { + if (m_PausedTime > 0.0f) return; + + if (m_Actions[m_NextActionIndex].GetType() == "OnInteract") { + IncrementAction(); + m_WaitingForAction = false; + } +} + +template<> +void Strip::HandleMsg(GameMessages::ResetModelToDefaults& msg) { + m_WaitingForAction = false; + m_PausedTime = 0.0f; + m_NextActionIndex = 0; +} + +void Strip::IncrementAction() { + if (m_Actions.empty()) return; + m_NextActionIndex++; + m_NextActionIndex %= m_Actions.size(); +} + +void Strip::Spawn(LOT lot, Entity& entity) { + EntityInfo info{}; + info.lot = lot; + info.pos = entity.GetPosition(); + info.rot = NiQuaternionConstant::IDENTITY; + info.spawnerID = entity.GetObjectID(); + Game::entityManager->ConstructEntity(Game::entityManager->CreateEntity(info, nullptr, &entity)); +} + +// Spawns a specific drop for all +void Strip::SpawnDrop(LOT dropLOT, Entity& entity) { + for (auto* const player : PlayerManager::GetAllPlayers()) { + GameMessages::SendDropClientLoot(player, entity.GetObjectID(), dropLOT, 0, entity.GetPosition()); + } +} + +void Strip::ProcNormalAction(float deltaTime, ModelComponent& modelComponent) { + auto& entity = *modelComponent.GetParent(); + auto& nextAction = GetNextAction(); + auto number = nextAction.GetValueParameterDouble(); + auto numberAsInt = static_cast(number); + auto nextActionType = GetNextAction().GetType(); + if (nextActionType == "SpawnStromling") { + Spawn(10495, entity); // Stromling property + } else if (nextActionType == "SpawnPirate") { + Spawn(10497, entity); // Maelstrom Pirate property + } else if (nextActionType == "SpawnRonin") { + Spawn(10498, entity); // Dark Ronin property + } else if (nextActionType == "DropImagination") { + for (; numberAsInt > 0; numberAsInt--) SpawnDrop(935, entity); // 1 Imagination powerup + } else if (nextActionType == "DropHealth") { + for (; numberAsInt > 0; numberAsInt--) SpawnDrop(177, entity); // 1 Life powerup + } else if (nextActionType == "DropArmor") { + for (; numberAsInt > 0; numberAsInt--) SpawnDrop(6431, entity); // 1 Armor powerup + } else if (nextActionType == "Smash") { + if (!modelComponent.IsUnSmashing()) { + GameMessages::Smash smash{}; + smash.target = entity.GetObjectID(); + smash.killerID = entity.GetObjectID(); + smash.Send(UNASSIGNED_SYSTEM_ADDRESS); + } + } else if (nextActionType == "UnSmash") { + GameMessages::UnSmash unsmash{}; + unsmash.target = entity.GetObjectID(); + unsmash.duration = number; + unsmash.builderID = LWOOBJID_EMPTY; + unsmash.Send(UNASSIGNED_SYSTEM_ADDRESS); + modelComponent.AddUnSmash(); + + m_PausedTime = number; + } else if (nextActionType == "Wait") { + m_PausedTime = number; + } else if (nextActionType == "PlaySound") { + GameMessages::PlayBehaviorSound sound; + sound.target = modelComponent.GetParent()->GetObjectID(); + sound.soundID = numberAsInt; + sound.Send(UNASSIGNED_SYSTEM_ADDRESS); + } else { + static std::set g_WarnedActions; + if (!g_WarnedActions.contains(nextActionType.data())) { + LOG("Tried to play action (%s) which is not supported.", nextActionType.data()); + g_WarnedActions.insert(nextActionType.data()); + } + return; + } + + IncrementAction(); +} + +// Decrement references to the previous state if we have progressed to the next one. +void Strip::RemoveStates(ModelComponent& modelComponent) const { + const auto& prevAction = GetPreviousAction(); + const auto prevActionType = prevAction.GetType(); + + if (prevActionType == "OnInteract") { + modelComponent.RemoveInteract(); + Game::entityManager->SerializeEntity(modelComponent.GetParent()); + } else if (prevActionType == "UnSmash") { + modelComponent.RemoveUnSmash(); + } +} + +void Strip::Update(float deltaTime, ModelComponent& modelComponent) { + m_PausedTime -= deltaTime; + if (m_PausedTime > 0.0f) return; + + m_PausedTime = 0.0f; + + if (m_WaitingForAction) return; + + auto& entity = *modelComponent.GetParent(); + auto& nextAction = GetNextAction(); + + RemoveStates(modelComponent); + + // Check for starting blocks and if not a starting block proc this blocks action + if (m_NextActionIndex == 0) { + if (nextAction.GetType() == "OnInteract") { + modelComponent.AddInteract(); + Game::entityManager->SerializeEntity(entity); + m_WaitingForAction = true; + + } + } else { // should be a normal block + ProcNormalAction(deltaTime, modelComponent); + } +} void Strip::SendBehaviorBlocksToClient(AMFArrayValue& args) const { m_Position.SendBehaviorBlocksToClient(args); @@ -106,3 +242,13 @@ void Strip::Deserialize(const tinyxml2::XMLElement& strip) { action.Deserialize(*actionElement); } } + +const Action& Strip::GetNextAction() const { + DluAssert(m_NextActionIndex < m_Actions.size()); return m_Actions[m_NextActionIndex]; +} + +const Action& Strip::GetPreviousAction() const { + DluAssert(m_NextActionIndex < m_Actions.size()); + size_t index = m_NextActionIndex == 0 ? m_Actions.size() - 1 : m_NextActionIndex - 1; + return m_Actions[index]; +} diff --git a/dGame/dPropertyBehaviors/Strip.h b/dGame/dPropertyBehaviors/Strip.h index 8fd7d0fe..3f929a7d 100644 --- a/dGame/dPropertyBehaviors/Strip.h +++ b/dGame/dPropertyBehaviors/Strip.h @@ -11,6 +11,7 @@ namespace tinyxml2 { } class AMFArrayValue; +class ModelComponent; class Strip { public: @@ -22,8 +23,30 @@ public: void Serialize(tinyxml2::XMLElement& strip) const; void Deserialize(const tinyxml2::XMLElement& strip); + + const Action& GetNextAction() const; + const Action& GetPreviousAction() const; + + void IncrementAction(); + void Spawn(LOT object, Entity& entity); + void Update(float deltaTime, ModelComponent& modelComponent); + void SpawnDrop(LOT dropLOT, Entity& entity); + void ProcNormalAction(float deltaTime, ModelComponent& modelComponent); + void RemoveStates(ModelComponent& modelComponent) const; private: + // Indicates this Strip is waiting for an action to be taken upon it to progress to its actions + bool m_WaitingForAction{ false }; + + // The amount of time this strip is paused for. Any interactions with this strip should be bounced if this is greater than 0. + float m_PausedTime{ 0.0f }; + + // 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 }; + + // The list of actions to be executed on this behavior. std::vector m_Actions; + + // The location of this strip on the UGBehaviorEditor UI StripUiPosition m_Position; }; From c77e9ce33aba6bc0822abcdb73cd50682f9b2c46 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Mon, 5 May 2025 07:04:23 -0700 Subject: [PATCH 2/6] chore: some zone maintenance (#1778) --- .../CDClientDatabase/CDClientManager.cpp | 3 +- .../CDClientTables/CDZoneTableTable.cpp | 102 +++++------- .../CDClientTables/CDZoneTableTable.h | 4 +- dGame/Entity.cpp | 3 +- dGame/dBehaviors/BasicAttackBehavior.cpp | 4 +- dGame/dComponents/DestroyableComponent.cpp | 6 +- dGame/dComponents/VendorComponent.cpp | 2 +- dGame/dMission/Mission.cpp | 4 +- dGame/dUtilities/Mail.cpp | 4 +- dMasterServer/InstanceManager.cpp | 36 ++--- dWorldServer/PerformanceManager.cpp | 29 ++-- dZoneManager/Level.cpp | 4 +- dZoneManager/WorldConfig.h | 5 +- dZoneManager/Zone.cpp | 32 ++-- dZoneManager/Zone.h | 10 +- dZoneManager/dZoneManager.cpp | 152 +++++++++--------- dZoneManager/dZoneManager.h | 25 +-- 17 files changed, 204 insertions(+), 221 deletions(-) diff --git a/dDatabase/CDClientDatabase/CDClientManager.cpp b/dDatabase/CDClientDatabase/CDClientManager.cpp index 6ecfb0ad..9aea0711 100644 --- a/dDatabase/CDClientDatabase/CDClientManager.cpp +++ b/dDatabase/CDClientDatabase/CDClientManager.cpp @@ -102,7 +102,6 @@ DEFINE_TABLE_STORAGE(CDScriptComponentTable); DEFINE_TABLE_STORAGE(CDSkillBehaviorTable); DEFINE_TABLE_STORAGE(CDTamingBuildPuzzleTable); DEFINE_TABLE_STORAGE(CDVendorComponentTable); -DEFINE_TABLE_STORAGE(CDZoneTableTable); void CDClientManager::LoadValuesFromDatabase() { if (!CDClientDatabase::isConnected) { @@ -149,7 +148,7 @@ void CDClientManager::LoadValuesFromDatabase() { CDSkillBehaviorTable::Instance().LoadValuesFromDatabase(); CDTamingBuildPuzzleTable::Instance().LoadValuesFromDatabase(); CDVendorComponentTable::Instance().LoadValuesFromDatabase(); - CDZoneTableTable::Instance().LoadValuesFromDatabase(); + CDZoneTableTable::LoadValuesFromDatabase(); } void CDClientManager::LoadValuesFromDefaults() { diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDZoneTableTable.cpp b/dDatabase/CDClientDatabase/CDClientTables/CDZoneTableTable.cpp index 6aaeb854..a8837acb 100644 --- a/dDatabase/CDClientDatabase/CDClientTables/CDZoneTableTable.cpp +++ b/dDatabase/CDClientDatabase/CDClientTables/CDZoneTableTable.cpp @@ -1,67 +1,53 @@ #include "CDZoneTableTable.h" -void CDZoneTableTable::LoadValuesFromDatabase() { +namespace CDZoneTableTable { + Table entries; - // First, get the size of the table - uint32_t size = 0; - auto tableSize = CDClientDatabase::ExecuteQuery("SELECT COUNT(*) FROM ZoneTable"); - while (!tableSize.eof()) { - size = tableSize.getIntField(0, 0); + void LoadValuesFromDatabase() { + // Get the data from the database + auto tableData = CDClientDatabase::ExecuteQuery("SELECT * FROM ZoneTable"); + while (!tableData.eof()) { + CDZoneTable entry; + entry.zoneID = tableData.getIntField("zoneID", -1); + entry.locStatus = tableData.getIntField("locStatus", -1); + entry.zoneName = tableData.getStringField("zoneName", ""); + entry.scriptID = tableData.getIntField("scriptID", -1); + entry.ghostdistance_min = tableData.getFloatField("ghostdistance_min", -1.0f); + entry.ghostdistance = tableData.getFloatField("ghostdistance", -1.0f); + entry.population_soft_cap = tableData.getIntField("population_soft_cap", -1); + entry.population_hard_cap = tableData.getIntField("population_hard_cap", -1); + UNUSED(entry.DisplayDescription = tableData.getStringField("DisplayDescription", "")); + UNUSED(entry.mapFolder = tableData.getStringField("mapFolder", "")); + entry.smashableMinDistance = tableData.getFloatField("smashableMinDistance", -1.0f); + entry.smashableMaxDistance = tableData.getFloatField("smashableMaxDistance", -1.0f); + UNUSED(entry.mixerProgram = tableData.getStringField("mixerProgram", "")); + UNUSED(entry.clientPhysicsFramerate = tableData.getStringField("clientPhysicsFramerate", "")); + entry.serverPhysicsFramerate = tableData.getStringField("serverPhysicsFramerate", ""); + entry.zoneControlTemplate = tableData.getIntField("zoneControlTemplate", -1); + entry.widthInChunks = tableData.getIntField("widthInChunks", -1); + entry.heightInChunks = tableData.getIntField("heightInChunks", -1); + entry.petsAllowed = tableData.getIntField("petsAllowed", -1) == 1 ? true : false; + entry.localize = tableData.getIntField("localize", -1) == 1 ? true : false; + entry.fZoneWeight = tableData.getFloatField("fZoneWeight", -1.0f); + UNUSED(entry.thumbnail = tableData.getStringField("thumbnail", "")); + entry.PlayerLoseCoinsOnDeath = tableData.getIntField("PlayerLoseCoinsOnDeath", -1) == 1 ? true : false; + entry.disableSaveLoc = tableData.getIntField("disableSaveLoc", -1) == 1 ? true : false; + entry.teamRadius = tableData.getFloatField("teamRadius", -1.0f); + UNUSED(entry.gate_version = tableData.getStringField("gate_version", "")); + entry.mountsAllowed = tableData.getIntField("mountsAllowed", -1) == 1 ? true : false; - tableSize.nextRow(); + entries[entry.zoneID] = entry; + tableData.nextRow(); + } } - tableSize.finalize(); + //! Queries the table with a zoneID to find. + const CDZoneTable* Query(uint32_t zoneID) { + const auto& iter = entries.find(zoneID); + if (iter != entries.end()) { + return &iter->second; + } - // Now get the data - auto tableData = CDClientDatabase::ExecuteQuery("SELECT * FROM ZoneTable"); - auto& entries = GetEntriesMutable(); - while (!tableData.eof()) { - CDZoneTable entry; - entry.zoneID = tableData.getIntField("zoneID", -1); - entry.locStatus = tableData.getIntField("locStatus", -1); - entry.zoneName = tableData.getStringField("zoneName", ""); - entry.scriptID = tableData.getIntField("scriptID", -1); - entry.ghostdistance_min = tableData.getFloatField("ghostdistance_min", -1.0f); - entry.ghostdistance = tableData.getFloatField("ghostdistance", -1.0f); - entry.population_soft_cap = tableData.getIntField("population_soft_cap", -1); - entry.population_hard_cap = tableData.getIntField("population_hard_cap", -1); - UNUSED(entry.DisplayDescription = tableData.getStringField("DisplayDescription", "")); - UNUSED(entry.mapFolder = tableData.getStringField("mapFolder", "")); - entry.smashableMinDistance = tableData.getFloatField("smashableMinDistance", -1.0f); - entry.smashableMaxDistance = tableData.getFloatField("smashableMaxDistance", -1.0f); - UNUSED(entry.mixerProgram = tableData.getStringField("mixerProgram", "")); - UNUSED(entry.clientPhysicsFramerate = tableData.getStringField("clientPhysicsFramerate", "")); - entry.serverPhysicsFramerate = tableData.getStringField("serverPhysicsFramerate", ""); - entry.zoneControlTemplate = tableData.getIntField("zoneControlTemplate", -1); - entry.widthInChunks = tableData.getIntField("widthInChunks", -1); - entry.heightInChunks = tableData.getIntField("heightInChunks", -1); - entry.petsAllowed = tableData.getIntField("petsAllowed", -1) == 1 ? true : false; - entry.localize = tableData.getIntField("localize", -1) == 1 ? true : false; - entry.fZoneWeight = tableData.getFloatField("fZoneWeight", -1.0f); - UNUSED(entry.thumbnail = tableData.getStringField("thumbnail", "")); - entry.PlayerLoseCoinsOnDeath = tableData.getIntField("PlayerLoseCoinsOnDeath", -1) == 1 ? true : false; - entry.disableSaveLoc = tableData.getIntField("disableSaveLoc", -1) == 1 ? true : false; - entry.teamRadius = tableData.getFloatField("teamRadius", -1.0f); - UNUSED(entry.gate_version = tableData.getStringField("gate_version", "")); - entry.mountsAllowed = tableData.getIntField("mountsAllowed", -1) == 1 ? true : false; - - entries.insert(std::make_pair(entry.zoneID, entry)); - tableData.nextRow(); + return nullptr; } - - tableData.finalize(); } - -//! Queries the table with a zoneID to find. -const CDZoneTable* CDZoneTableTable::Query(uint32_t zoneID) { - auto& m_Entries = GetEntries(); - const auto& iter = m_Entries.find(zoneID); - - if (iter != m_Entries.end()) { - return &iter->second; - } - - return nullptr; -} - diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDZoneTableTable.h b/dDatabase/CDClientDatabase/CDClientTables/CDZoneTableTable.h index b1e8b1ba..6d91242b 100644 --- a/dDatabase/CDClientDatabase/CDClientTables/CDZoneTableTable.h +++ b/dDatabase/CDClientDatabase/CDClientTables/CDZoneTableTable.h @@ -33,8 +33,8 @@ struct CDZoneTable { bool mountsAllowed; //!< Whether or not mounts are allowed }; -class CDZoneTableTable : public CDTable> { -public: +namespace CDZoneTableTable { + using Table = std::map; void LoadValuesFromDatabase(); // Queries the table with a zoneID to find. diff --git a/dGame/Entity.cpp b/dGame/Entity.cpp index 90bf7e76..35cd10fb 100644 --- a/dGame/Entity.cpp +++ b/dGame/Entity.cpp @@ -545,9 +545,8 @@ void Entity::Initialize() { // ZoneControl script if (m_TemplateID == 2365) { - CDZoneTableTable* zoneTable = CDClientManager::GetTable(); const auto zoneID = Game::zoneManager->GetZoneID(); - const CDZoneTable* zoneData = zoneTable->Query(zoneID.GetMapID()); + const CDZoneTable* zoneData = CDZoneTableTable::Query(zoneID.GetMapID()); if (zoneData != nullptr) { int zoneScriptID = zoneData->scriptID; diff --git a/dGame/dBehaviors/BasicAttackBehavior.cpp b/dGame/dBehaviors/BasicAttackBehavior.cpp index 97ceb846..41b02e7c 100644 --- a/dGame/dBehaviors/BasicAttackBehavior.cpp +++ b/dGame/dBehaviors/BasicAttackBehavior.cpp @@ -20,7 +20,7 @@ void BasicAttackBehavior::Handle(BehaviorContext* context, RakNet::BitStream& bi //Handle player damage cooldown if (entity->IsPlayer() && !this->m_DontApplyImmune) { - const float immunityTime = Game::zoneManager->GetWorldConfig()->globalImmunityTime; + const float immunityTime = Game::zoneManager->GetWorldConfig().globalImmunityTime; destroyableComponent->SetDamageCooldownTimer(immunityTime); } } @@ -214,7 +214,7 @@ void BasicAttackBehavior::DoBehaviorCalculation(BehaviorContext* context, RakNet //Handle player damage cooldown if (isSuccess && targetEntity->IsPlayer() && !this->m_DontApplyImmune) { - destroyableComponent->SetDamageCooldownTimer(Game::zoneManager->GetWorldConfig()->globalImmunityTime); + destroyableComponent->SetDamageCooldownTimer(Game::zoneManager->GetWorldConfig().globalImmunityTime); } eBasicAttackSuccessTypes successState = eBasicAttackSuccessTypes::FAILIMMUNE; diff --git a/dGame/dComponents/DestroyableComponent.cpp b/dGame/dComponents/DestroyableComponent.cpp index cb8afd5a..cc67a9db 100644 --- a/dGame/dComponents/DestroyableComponent.cpp +++ b/dGame/dComponents/DestroyableComponent.cpp @@ -774,10 +774,10 @@ void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType if (Game::zoneManager->GetPlayerLoseCoinOnDeath()) { auto* character = m_Parent->GetCharacter(); uint64_t coinsTotal = character->GetCoins(); - const uint64_t minCoinsToLose = Game::zoneManager->GetWorldConfig()->coinsLostOnDeathMin; + const uint64_t minCoinsToLose = Game::zoneManager->GetWorldConfig().coinsLostOnDeathMin; if (coinsTotal >= minCoinsToLose) { - const uint64_t maxCoinsToLose = Game::zoneManager->GetWorldConfig()->coinsLostOnDeathMax; - const float coinPercentageToLose = Game::zoneManager->GetWorldConfig()->coinsLostOnDeathPercent; + const uint64_t maxCoinsToLose = Game::zoneManager->GetWorldConfig().coinsLostOnDeathMax; + const float coinPercentageToLose = Game::zoneManager->GetWorldConfig().coinsLostOnDeathPercent; uint64_t coinsToLose = std::max(static_cast(coinsTotal * coinPercentageToLose), minCoinsToLose); coinsToLose = std::min(maxCoinsToLose, coinsToLose); diff --git a/dGame/dComponents/VendorComponent.cpp b/dGame/dComponents/VendorComponent.cpp index b9286d25..b4a5f05c 100644 --- a/dGame/dComponents/VendorComponent.cpp +++ b/dGame/dComponents/VendorComponent.cpp @@ -101,7 +101,7 @@ void VendorComponent::SetupConstants() { std::vector vendorComps = vendorComponentTable->Query([=](CDVendorComponent entry) { return (entry.id == componentID); }); if (vendorComps.empty()) return; auto vendorData = vendorComps.at(0); - if (vendorData.buyScalar == 0.0) m_BuyScalar = Game::zoneManager->GetWorldConfig()->vendorBuyMultiplier; + if (vendorData.buyScalar == 0.0) m_BuyScalar = Game::zoneManager->GetWorldConfig().vendorBuyMultiplier; else m_BuyScalar = vendorData.buyScalar; m_SellScalar = vendorData.sellScalar; m_RefreshTimeSeconds = vendorData.refreshTimeSeconds; diff --git a/dGame/dMission/Mission.cpp b/dGame/dMission/Mission.cpp index 559193b7..b1ff4360 100644 --- a/dGame/dMission/Mission.cpp +++ b/dGame/dMission/Mission.cpp @@ -470,9 +470,9 @@ void Mission::YieldRewards() { int32_t coinsToSend = 0; if (info.LegoScore > 0) { eLootSourceType lootSource = info.isMission ? eLootSourceType::MISSION : eLootSourceType::ACHIEVEMENT; - if (levelComponent->GetLevel() >= Game::zoneManager->GetWorldConfig()->levelCap) { + if (levelComponent->GetLevel() >= Game::zoneManager->GetWorldConfig().levelCap) { // Since the character is at the level cap we reward them with coins instead of UScore. - coinsToSend += info.LegoScore * Game::zoneManager->GetWorldConfig()->levelCapCurrencyConversion; + coinsToSend += info.LegoScore * Game::zoneManager->GetWorldConfig().levelCapCurrencyConversion; } else { characterComponent->SetUScore(characterComponent->GetUScore() + info.LegoScore); GameMessages::SendModifyLEGOScore(entity, entity->GetSystemAddress(), info.LegoScore, lootSource); diff --git a/dGame/dUtilities/Mail.cpp b/dGame/dUtilities/Mail.cpp index d3275b31..6791020e 100644 --- a/dGame/dUtilities/Mail.cpp +++ b/dGame/dUtilities/Mail.cpp @@ -81,7 +81,7 @@ namespace Mail { } else if (GeneralUtils::CaseInsensitiveStringCompare(mailInfo.recipient, character->GetName()) || receiverID->id == character->GetID()) { response.status = eSendResponse::CannotMailSelf; } else { - uint32_t mailCost = Game::zoneManager->GetWorldConfig()->mailBaseFee; + uint32_t mailCost = Game::zoneManager->GetWorldConfig().mailBaseFee; uint32_t stackSize = 0; auto inventoryComponent = player->GetComponent(); @@ -92,7 +92,7 @@ namespace Mail { if (hasAttachment) { item = inventoryComponent->FindItemById(mailInfo.itemID); if (item) { - mailCost += (item->GetInfo().baseValue * Game::zoneManager->GetWorldConfig()->mailPercentAttachmentFee); + mailCost += (item->GetInfo().baseValue * Game::zoneManager->GetWorldConfig().mailPercentAttachmentFee); mailInfo.itemLOT = item->GetLot(); } } diff --git a/dMasterServer/InstanceManager.cpp b/dMasterServer/InstanceManager.cpp index 3c40136c..be4283e7 100644 --- a/dMasterServer/InstanceManager.cpp +++ b/dMasterServer/InstanceManager.cpp @@ -37,9 +37,9 @@ Instance* InstanceManager::GetInstance(LWOMAPID mapID, bool isFriendTransfer, LW // If we are shutting down, return a nullptr so a new instance is not created. if (m_IsShuttingDown) { LOG("Tried to create a new instance map/instance/clone %i/%i/%i, but Master is shutting down.", - mapID, - m_LastInstanceID + 1, - cloneID); + mapID, + m_LastInstanceID + 1, + cloneID); return nullptr; } //TODO: Update this so that the IP is read from a configuration file instead @@ -292,9 +292,9 @@ Instance* InstanceManager::CreatePrivateInstance(LWOMAPID mapID, LWOCLONEID clon if (m_IsShuttingDown) { LOG("Tried to create a new private instance map/instance/clone %i/%i/%i, but Master is shutting down.", - mapID, - m_LastInstanceID + 1, - cloneID); + mapID, + m_LastInstanceID + 1, + cloneID); return nullptr; } @@ -333,29 +333,17 @@ Instance* InstanceManager::FindPrivateInstance(const std::string& password) { } int InstanceManager::GetSoftCap(LWOMAPID mapID) { - CDZoneTableTable* zoneTable = CDClientManager::GetTable(); - if (zoneTable) { - const CDZoneTable* zone = zoneTable->Query(mapID); + const CDZoneTable* zone = CDZoneTableTable::Query(mapID); - if (zone != nullptr) { - return zone->population_soft_cap; - } - } - - return 8; + // Default to 8 which is the cap for most worlds. + return zone ? zone->population_soft_cap : 8; } int InstanceManager::GetHardCap(LWOMAPID mapID) { - CDZoneTableTable* zoneTable = CDClientManager::GetTable(); - if (zoneTable) { - const CDZoneTable* zone = zoneTable->Query(mapID); + const CDZoneTable* zone = CDZoneTableTable::Query(mapID); - if (zone != nullptr) { - return zone->population_hard_cap; - } - } - - return 12; + // Default to 12 which is the cap for most worlds. + return zone ? zone->population_hard_cap : 12; } void Instance::SetShutdownComplete(const bool value) { diff --git a/dWorldServer/PerformanceManager.cpp b/dWorldServer/PerformanceManager.cpp index 29026ec7..8b78867b 100644 --- a/dWorldServer/PerformanceManager.cpp +++ b/dWorldServer/PerformanceManager.cpp @@ -69,22 +69,19 @@ namespace { void PerformanceManager::SelectProfile(LWOMAPID mapID) { // Try to get it from zoneTable - CDZoneTableTable* zoneTable = CDClientManager::GetTable(); - if (zoneTable) { - const CDZoneTable* zone = zoneTable->Query(mapID); - if (zone) { - if (zone->serverPhysicsFramerate == "high") { - m_CurrentProfile = highFrameDelta; - return; - } - if (zone->serverPhysicsFramerate == "medium") { - m_CurrentProfile = mediumFrameDelta; - return; - } - if (zone->serverPhysicsFramerate == "low") { - m_CurrentProfile = lowFrameDelta; - return; - } + const CDZoneTable* zone = CDZoneTableTable::Query(mapID); + if (zone) { + if (zone->serverPhysicsFramerate == "high") { + m_CurrentProfile = highFrameDelta; + return; + } + if (zone->serverPhysicsFramerate == "medium") { + m_CurrentProfile = mediumFrameDelta; + return; + } + if (zone->serverPhysicsFramerate == "low") { + m_CurrentProfile = lowFrameDelta; + return; } } diff --git a/dZoneManager/Level.cpp b/dZoneManager/Level.cpp index d2b67b29..13c85c1a 100644 --- a/dZoneManager/Level.cpp +++ b/dZoneManager/Level.cpp @@ -253,8 +253,8 @@ void Level::ReadSceneObjectDataChunk(std::istream& file, Header& header) { //This is a little bit of a bodge, but because the alpha client (HF) doesn't store the //spawn position / rotation like the later versions do, we need to check the LOT for the spawn pos & set it. if (obj.lot == LOT_MARKER_PLAYER_START) { - Game::zoneManager->GetZone()->SetSpawnPos(obj.position); - Game::zoneManager->GetZone()->SetSpawnRot(obj.rotation); + Game::zoneManager->GetZoneMut()->SetSpawnPos(obj.position); + Game::zoneManager->GetZoneMut()->SetSpawnRot(obj.rotation); } std::string sData = GeneralUtils::UTF16ToWTF8(ldfString); diff --git a/dZoneManager/WorldConfig.h b/dZoneManager/WorldConfig.h index a98433a1..98085a48 100644 --- a/dZoneManager/WorldConfig.h +++ b/dZoneManager/WorldConfig.h @@ -5,7 +5,6 @@ #include struct WorldConfig { - int32_t worldConfigID{}; //! Primary key for WorlcConfig table float peGravityValue{}; //! Unknown float peBroadphaseWorldSize{}; //! Unknown float peGameObjScaleFactor{}; //! Unknown @@ -53,8 +52,8 @@ struct WorldConfig { float reputationPerVoteReceived{}; //! Unknown int32_t showcaseTopModelConsiderationBattles{}; //! Unknown float reputationPerBattlePromotion{}; //! Unknown - float coinsLostOnDeathMinTimeout{}; //! Unknown - float coinsLostOnDeathMaxTimeout{}; //! Unknown + float coinsLostOnDeathMinTimeout{}; //! Minimum amount of time coins lost on death will remain on the playfield. + float coinsLostOnDeathMaxTimeout{}; //! Maximum amount of time coins lost on death will remain on the playfield. int32_t mailBaseFee{}; //! The base fee to take when a player sends mail float mailPercentAttachmentFee{}; //! The scalar multiplied by an items base cost to determine how much that item costs to be mailed int32_t propertyReputationDelay{}; //! Unknown diff --git a/dZoneManager/Zone.cpp b/dZoneManager/Zone.cpp index 1d657075..282f4c45 100644 --- a/dZoneManager/Zone.cpp +++ b/dZoneManager/Zone.cpp @@ -2,6 +2,7 @@ #include "Level.h" #include #include +#include #include "Game.h" #include "Logger.h" #include "GeneralUtils.h" @@ -20,8 +21,8 @@ #include "eWaypointCommandType.h" #include "dNavMesh.h" -Zone::Zone(const LWOMAPID& mapID, const LWOINSTANCEID& instanceID, const LWOCLONEID& cloneID) : - m_ZoneID(mapID, instanceID, cloneID) { +Zone::Zone(const LWOZONEID zoneID) : + m_ZoneID(zoneID) { m_NumberOfObjectsLoaded = 0; m_NumberOfSceneTransitionsLoaded = 0; m_CheckSum = 0; @@ -31,9 +32,6 @@ Zone::Zone(const LWOMAPID& mapID, const LWOINSTANCEID& instanceID, const LWOCLON Zone::~Zone() { LOG("Destroying zone %i", m_ZoneID.GetMapID()); - for (std::map::iterator it = m_Scenes.begin(); it != m_Scenes.end(); ++it) { - if (it->second.level != nullptr) delete it->second.level; - } } void Zone::Initalize() { @@ -153,23 +151,25 @@ void Zone::LoadZoneIntoMemory() { m_ZonePath = m_ZoneFilePath.substr(0, m_ZoneFilePath.rfind('/') + 1); } -std::string Zone::GetFilePathForZoneID() { +std::string Zone::GetFilePathForZoneID() const { //We're gonna go ahead and presume we've got the db loaded already: - CDZoneTableTable* zoneTable = CDClientManager::GetTable(); - const CDZoneTable* zone = zoneTable->Query(this->GetZoneID().GetMapID()); + const CDZoneTable* zone = CDZoneTableTable::Query(this->GetZoneID().GetMapID()); + std::string toReturn("ERR"); if (zone != nullptr) { - std::string toReturn = "maps/" + zone->zoneName; + toReturn = "maps/" + zone->zoneName; std::transform(toReturn.begin(), toReturn.end(), toReturn.begin(), ::tolower); + + /* Normalize to one slash type */ std::ranges::replace(toReturn, '\\', '/'); - return toReturn; } - return std::string("ERR"); + return toReturn; } //Based off code from: https://www.liquisearch.com/fletchers_checksum/implementation/optimizations -uint32_t Zone::CalculateChecksum() { - uint32_t sum1 = 0xffff, sum2 = 0xffff; +uint32_t Zone::CalculateChecksum() const { + uint32_t sum1 = 0xffff; + uint32_t sum2 = 0xffff; for (const auto& [scene, sceneRevision] : m_MapRevisions) { uint32_t sceneID = scene.GetSceneID(); @@ -194,7 +194,7 @@ uint32_t Zone::CalculateChecksum() { void Zone::LoadLevelsIntoMemory() { for (auto& [sceneID, scene] : m_Scenes) { if (scene.level) continue; - scene.level = new Level(this, m_ZonePath + scene.filename); + scene.level = std::make_unique(this, m_ZonePath + scene.filename); if (scene.level->m_ChunkHeaders.empty()) continue; @@ -241,7 +241,7 @@ void Zone::LoadScene(std::istream& file) { BinaryIO::BinaryRead(file, scene.color_g); } - m_Scenes.insert(std::make_pair(lwoSceneID, scene)); + m_Scenes[lwoSceneID] = std::move(scene); } void Zone::LoadLUTriggers(std::string triggerFile, SceneRef& scene) { @@ -299,7 +299,7 @@ void Zone::LoadLUTriggers(std::string triggerFile, SceneRef& scene) { } } -LUTriggers::Trigger* Zone::GetTrigger(uint32_t sceneID, uint32_t triggerID) { +LUTriggers::Trigger* Zone::GetTrigger(uint32_t sceneID, uint32_t triggerID) const { auto scene = m_Scenes.find(sceneID); if (scene == m_Scenes.end()) return nullptr; diff --git a/dZoneManager/Zone.h b/dZoneManager/Zone.h index 299d675b..206ad1f1 100644 --- a/dZoneManager/Zone.h +++ b/dZoneManager/Zone.h @@ -31,7 +31,7 @@ struct SceneRef { uint8_t color_r{}; uint8_t color_g{}; uint8_t color_b{}; - Level* level; + std::unique_ptr level; std::map triggers; }; @@ -203,18 +203,18 @@ public: }; public: - Zone(const LWOMAPID& mapID, const LWOINSTANCEID& instanceID, const LWOCLONEID& cloneID); + Zone(const LWOZONEID zoneID); ~Zone(); void Initalize(); void LoadZoneIntoMemory(); - std::string GetFilePathForZoneID(); - uint32_t CalculateChecksum(); + std::string GetFilePathForZoneID() const; + uint32_t CalculateChecksum() const; void LoadLevelsIntoMemory(); void AddRevision(LWOSCENEID sceneID, uint32_t revision); const LWOZONEID& GetZoneID() const { return m_ZoneID; } const uint32_t GetChecksum() const { return m_CheckSum; } - LUTriggers::Trigger* GetTrigger(uint32_t sceneID, uint32_t triggerID); + LUTriggers::Trigger* GetTrigger(uint32_t sceneID, uint32_t triggerID) const; const Path* GetPath(std::string name) const; uint32_t GetWorldID() const { return m_WorldID; } diff --git a/dZoneManager/dZoneManager.cpp b/dZoneManager/dZoneManager.cpp index 09baabed..ea19dc88 100644 --- a/dZoneManager/dZoneManager.cpp +++ b/dZoneManager/dZoneManager.cpp @@ -14,6 +14,7 @@ #include "eObjectBits.h" #include "CDZoneTableTable.h" #include "AssetManager.h" +#include #include "ObjectIDManager.h" @@ -29,21 +30,18 @@ void dZoneManager::Initialize(const LWOZONEID& zoneID) { LOT zoneControlTemplate = 2365; - CDZoneTableTable* zoneTable = CDClientManager::GetTable(); - if (zoneTable != nullptr) { - const CDZoneTable* zone = zoneTable->Query(zoneID.GetMapID()); + const CDZoneTable* zone = CDZoneTableTable::Query(zoneID.GetMapID()); - if (zone != nullptr) { - zoneControlTemplate = zone->zoneControlTemplate != -1 ? zone->zoneControlTemplate : 2365; - const auto min = zone->ghostdistance_min != -1.0f ? zone->ghostdistance_min : 100; - const auto max = zone->ghostdistance != -1.0f ? zone->ghostdistance : 100; - Game::entityManager->SetGhostDistanceMax(max + min); - Game::entityManager->SetGhostDistanceMin(max); - m_PlayerLoseCoinsOnDeath = zone->PlayerLoseCoinsOnDeath; - m_DisableSaveLocation = zone->disableSaveLoc; - m_MountsAllowed = zone->mountsAllowed; - m_PetsAllowed = zone->petsAllowed; - } + if (zone != nullptr) { + zoneControlTemplate = zone->zoneControlTemplate != -1 ? zone->zoneControlTemplate : 2365; + const auto min = zone->ghostdistance_min != -1.0f ? zone->ghostdistance_min : 100; + const auto max = zone->ghostdistance != -1.0f ? zone->ghostdistance : 100; + Game::entityManager->SetGhostDistanceMax(max + min); + Game::entityManager->SetGhostDistanceMin(max); + m_PlayerLoseCoinsOnDeath = zone->PlayerLoseCoinsOnDeath; + m_DisableSaveLocation = zone->disableSaveLoc; + m_MountsAllowed = zone->mountsAllowed; + m_PetsAllowed = zone->petsAllowed; } LOG("Creating zone control object %i", zoneControlTemplate); @@ -56,7 +54,9 @@ void dZoneManager::Initialize(const LWOZONEID& zoneID) { Game::entityManager->Initialize(); EntityInfo info; info.lot = zoneControlTemplate; - info.id = 70368744177662; + + /* Yep its hardcoded like this in the client too, this exact value. */ + info.id = 0x3FFF'FFFFFFFELL; Entity* zoneControl = Game::entityManager->CreateEntity(info, nullptr, nullptr, true); m_ZoneControlObject = zoneControl; @@ -74,16 +74,16 @@ void dZoneManager::Initialize(const LWOZONEID& zoneID) { dZoneManager::~dZoneManager() { if (m_pZone) delete m_pZone; - for (std::pair p : m_Spawners) { - if (p.second) { - delete p.second; - p.second = nullptr; + for (auto* spawner : m_Spawners | std::views::values) { + if (spawner) { + delete spawner; + spawner = nullptr; } } - if (m_WorldConfig) delete m_WorldConfig; } -Zone* dZoneManager::GetZone() { +Zone* dZoneManager::GetZoneMut() const { + DluAssert(m_pZone); return m_pZone; } @@ -91,20 +91,20 @@ void dZoneManager::LoadZone(const LWOZONEID& zoneID) { if (m_pZone) delete m_pZone; m_ZoneID = zoneID; - m_pZone = new Zone(zoneID.GetMapID(), zoneID.GetInstanceID(), zoneID.GetCloneID()); + m_pZone = new Zone(zoneID); } void dZoneManager::AddSpawner(LWOOBJID id, Spawner* spawner) { - m_Spawners.insert_or_assign(id, spawner); + m_Spawners[id] = spawner; } -LWOZONEID dZoneManager::GetZoneID() const { +const LWOZONEID& dZoneManager::GetZoneID() const { return m_ZoneID; } void dZoneManager::Update(float deltaTime) { - for (auto spawner : m_Spawners) { - spawner.second->Update(deltaTime); + for (auto spawner : m_Spawners | std::views::values) { + spawner->Update(deltaTime); } } @@ -136,12 +136,7 @@ LWOOBJID dZoneManager::MakeSpawner(SpawnerInfo info) { Spawner* dZoneManager::GetSpawner(const LWOOBJID id) { const auto& index = m_Spawners.find(id); - - if (index == m_Spawners.end()) { - return nullptr; - } - - return index->second; + return index != m_Spawners.end() ? index->second : nullptr; } void dZoneManager::RemoveSpawner(const LWOOBJID id) { @@ -154,10 +149,11 @@ void dZoneManager::RemoveSpawner(const LWOOBJID id) { auto* entity = Game::entityManager->GetEntity(id); + LOG("Destroying spawner (%llu)", id); + if (entity != nullptr) { entity->Kill(); } else { - LOG("Failed to find spawner entity (%llu)", id); } @@ -165,7 +161,7 @@ void dZoneManager::RemoveSpawner(const LWOOBJID id) { spawner->Deactivate(); - LOG("Destroying spawner (%llu)", id); + LOG("Destroyed spawner (%llu)", id); m_Spawners.erase(id); @@ -173,24 +169,23 @@ void dZoneManager::RemoveSpawner(const LWOOBJID id) { } -std::vector dZoneManager::GetSpawnersByName(std::string spawnerName) { +std::vector dZoneManager::GetSpawnersByName(const std::string& spawnerName) { std::vector spawners; - for (const auto& spawner : m_Spawners) { - if (spawner.second->GetName() == spawnerName) { - spawners.push_back(spawner.second); + for (const auto& spawner : m_Spawners | std::views::values) { + if (spawner->GetName() == spawnerName) { + spawners.push_back(spawner); } } return spawners; } -std::vector dZoneManager::GetSpawnersInGroup(std::string group) { +std::vector dZoneManager::GetSpawnersInGroup(const std::string& group) { std::vector spawnersInGroup; - for (auto spawner : m_Spawners) { - for (std::string entityGroup : spawner.second->m_Info.groups) { - if (entityGroup == group) { - spawnersInGroup.push_back(spawner.second); - } + for (auto spawner : m_Spawners | std::views::values) { + const auto& groups = spawner->m_Info.groups; + if (std::ranges::find(groups, group) != groups.end()) { + spawnersInGroup.push_back(spawner); } } @@ -200,71 +195,83 @@ std::vector dZoneManager::GetSpawnersInGroup(std::string group) { uint32_t dZoneManager::GetUniqueMissionIdStartingValue() { if (m_UniqueMissionIdStart == 0) { auto tableData = CDClientDatabase::ExecuteQuery("SELECT COUNT(*) FROM Missions WHERE isMission = 0 GROUP BY isMission;"); - m_UniqueMissionIdStart = tableData.getIntField(0, -1); - tableData.finalize(); + m_UniqueMissionIdStart = tableData.getIntField(0, 1); } return m_UniqueMissionIdStart; } bool dZoneManager::CheckIfAccessibleZone(LWOMAPID zoneID) { - //We're gonna go ahead and presume we've got the db loaded already: - CDZoneTableTable* zoneTable = CDClientManager::GetTable(); - const CDZoneTable* zone = zoneTable->Query(zoneID); - if (zone != nullptr) { - return Game::assetManager->HasFile(("maps/" + zone->zoneName).c_str()); - } else { - return false; - } + const CDZoneTable* zone = CDZoneTableTable::Query(zoneID); + return zone && Game::assetManager->HasFile("maps/" + zone->zoneName); } void dZoneManager::LoadWorldConfig() { - LOG("Loading WorldConfig into memory"); + // Already loaded + if (m_WorldConfig) return; - auto worldConfig = CDClientDatabase::ExecuteQuery("SELECT * FROM WorldConfig;"); + LOG_DEBUG("Loading WorldConfig into memory"); - if (!m_WorldConfig) m_WorldConfig = new WorldConfig(); + // This table only has 1 row and only should have 1 row. + auto worldConfig = CDClientDatabase::ExecuteQuery("SELECT * FROM WorldConfig LIMIT 1;"); + + m_WorldConfig = WorldConfig(); if (worldConfig.eof()) { - LOG("WorldConfig table is empty. Is this intended?"); + LOG("WorldConfig table is empty. Is this intended?"); return; } // Now read in the giant table - m_WorldConfig->worldConfigID = worldConfig.getIntField("WorldConfigID"); m_WorldConfig->peGravityValue = worldConfig.getFloatField("pegravityvalue"); m_WorldConfig->peBroadphaseWorldSize = worldConfig.getFloatField("pebroadphaseworldsize"); m_WorldConfig->peGameObjScaleFactor = worldConfig.getFloatField("pegameobjscalefactor"); + m_WorldConfig->characterRotationSpeed = worldConfig.getFloatField("character_rotation_speed"); + m_WorldConfig->characterWalkForwardSpeed = worldConfig.getFloatField("character_walk_forward_speed"); m_WorldConfig->characterWalkBackwardSpeed = worldConfig.getFloatField("character_walk_backward_speed"); m_WorldConfig->characterWalkStrafeSpeed = worldConfig.getFloatField("character_walk_strafe_speed"); m_WorldConfig->characterWalkStrafeForwardSpeed = worldConfig.getFloatField("character_walk_strafe_forward_speed"); m_WorldConfig->characterWalkStrafeBackwardSpeed = worldConfig.getFloatField("character_walk_strafe_backward_speed"); + m_WorldConfig->characterRunBackwardSpeed = worldConfig.getFloatField("character_run_backward_speed"); m_WorldConfig->characterRunStrafeSpeed = worldConfig.getFloatField("character_run_strafe_speed"); m_WorldConfig->characterRunStrafeForwardSpeed = worldConfig.getFloatField("character_run_strafe_forward_speed"); m_WorldConfig->characterRunStrafeBackwardSpeed = worldConfig.getFloatField("character_run_strafe_backward_speed"); - m_WorldConfig->globalCooldown = worldConfig.getFloatField("global_cooldown"); + m_WorldConfig->characterGroundedTime = worldConfig.getFloatField("characterGroundedTime"); m_WorldConfig->characterGroundedSpeed = worldConfig.getFloatField("characterGroundedSpeed"); - m_WorldConfig->globalImmunityTime = worldConfig.getFloatField("globalImmunityTime"); + + m_WorldConfig->characterVersion = worldConfig.getIntField("CharacterVersion"); + m_WorldConfig->characterEyeHeight = worldConfig.getFloatField("character_eye_height"); m_WorldConfig->characterMaxSlope = worldConfig.getFloatField("character_max_slope"); + + m_WorldConfig->globalCooldown = worldConfig.getFloatField("global_cooldown"); + m_WorldConfig->globalImmunityTime = worldConfig.getFloatField("globalImmunityTime"); + m_WorldConfig->defaultRespawnTime = worldConfig.getFloatField("defaultrespawntime"); m_WorldConfig->missionTooltipTimeout = worldConfig.getFloatField("mission_tooltip_timeout"); m_WorldConfig->vendorBuyMultiplier = worldConfig.getFloatField("vendor_buy_multiplier", 0.1); m_WorldConfig->petFollowRadius = worldConfig.getFloatField("pet_follow_radius"); - m_WorldConfig->characterEyeHeight = worldConfig.getFloatField("character_eye_height"); + m_WorldConfig->flightVerticalVelocity = worldConfig.getFloatField("flight_vertical_velocity"); m_WorldConfig->flightAirspeed = worldConfig.getFloatField("flight_airspeed"); m_WorldConfig->flightFuelRatio = worldConfig.getFloatField("flight_fuel_ratio"); m_WorldConfig->flightMaxAirspeed = worldConfig.getFloatField("flight_max_airspeed"); - m_WorldConfig->fReputationPerVote = worldConfig.getFloatField("fReputationPerVote"); - m_WorldConfig->propertyCloneLimit = worldConfig.getIntField("nPropertyCloneLimit"); + m_WorldConfig->defaultHomespaceTemplate = worldConfig.getIntField("defaultHomespaceTemplate"); + m_WorldConfig->coinsLostOnDeathPercent = worldConfig.getFloatField("coins_lost_on_death_percent"); m_WorldConfig->coinsLostOnDeathMin = worldConfig.getIntField("coins_lost_on_death_min"); m_WorldConfig->coinsLostOnDeathMax = worldConfig.getIntField("coins_lost_on_death_max"); + m_WorldConfig->coinsLostOnDeathMinTimeout = worldConfig.getFloatField("coins_lost_on_death_min_timeout"); + m_WorldConfig->coinsLostOnDeathMaxTimeout = worldConfig.getFloatField("coins_lost_on_death_max_timeout"); + m_WorldConfig->characterVotesPerDay = worldConfig.getIntField("character_votes_per_day"); + + m_WorldConfig->defaultPropertyMaxHeight = worldConfig.getFloatField("defaultPropertyMaxHeight"); + m_WorldConfig->propertyCloneLimit = worldConfig.getIntField("nPropertyCloneLimit"); + m_WorldConfig->propertyReputationDelay = worldConfig.getIntField("propertyReputationDelay"); m_WorldConfig->propertyModerationRequestApprovalCost = worldConfig.getIntField("property_moderation_request_approval_cost"); m_WorldConfig->propertyModerationRequestReviewCost = worldConfig.getIntField("property_moderation_request_review_cost"); m_WorldConfig->propertyModRequestsAllowedSpike = worldConfig.getIntField("propertyModRequestsAllowedSpike"); @@ -272,21 +279,22 @@ void dZoneManager::LoadWorldConfig() { m_WorldConfig->propertyModRequestsAllowedTotal = worldConfig.getIntField("propertyModRequestsAllowedTotal"); m_WorldConfig->propertyModRequestsSpikeDuration = worldConfig.getIntField("propertyModRequestsSpikeDuration"); m_WorldConfig->propertyModRequestsIntervalDuration = worldConfig.getIntField("propertyModRequestsIntervalDuration"); + m_WorldConfig->modelModerateOnCreate = worldConfig.getIntField("modelModerateOnCreate") != 0; - m_WorldConfig->defaultPropertyMaxHeight = worldConfig.getFloatField("defaultPropertyMaxHeight"); + + m_WorldConfig->fReputationPerVote = worldConfig.getFloatField("fReputationPerVote"); m_WorldConfig->reputationPerVoteCast = worldConfig.getFloatField("reputationPerVoteCast"); m_WorldConfig->reputationPerVoteReceived = worldConfig.getFloatField("reputationPerVoteReceived"); - m_WorldConfig->showcaseTopModelConsiderationBattles = worldConfig.getIntField("showcaseTopModelConsiderationBattles"); m_WorldConfig->reputationPerBattlePromotion = worldConfig.getFloatField("reputationPerBattlePromotion"); - m_WorldConfig->coinsLostOnDeathMinTimeout = worldConfig.getFloatField("coins_lost_on_death_min_timeout"); - m_WorldConfig->coinsLostOnDeathMaxTimeout = worldConfig.getFloatField("coins_lost_on_death_max_timeout"); + + m_WorldConfig->showcaseTopModelConsiderationBattles = worldConfig.getIntField("showcaseTopModelConsiderationBattles"); + m_WorldConfig->mailBaseFee = worldConfig.getIntField("mail_base_fee"); m_WorldConfig->mailPercentAttachmentFee = worldConfig.getFloatField("mail_percent_attachment_fee"); - m_WorldConfig->propertyReputationDelay = worldConfig.getIntField("propertyReputationDelay"); + m_WorldConfig->levelCap = worldConfig.getIntField("LevelCap"); m_WorldConfig->levelUpBehaviorEffect = worldConfig.getStringField("LevelUpBehaviorEffect"); - m_WorldConfig->characterVersion = worldConfig.getIntField("CharacterVersion"); m_WorldConfig->levelCapCurrencyConversion = worldConfig.getIntField("LevelCapCurrencyConversion"); - worldConfig.finalize(); - LOG("Loaded WorldConfig into memory"); + + LOG_DEBUG("Loaded WorldConfig into memory"); } diff --git a/dZoneManager/dZoneManager.h b/dZoneManager/dZoneManager.h index 4e512d22..f9abee8c 100644 --- a/dZoneManager/dZoneManager.h +++ b/dZoneManager/dZoneManager.h @@ -1,11 +1,10 @@ #pragma once #include "dZMCommon.h" +#include "WorldConfig.h" #include "Zone.h" #include "Spawner.h" #include -class WorldConfig; - class dZoneManager { public: enum class dZoneNotifier { @@ -27,28 +26,36 @@ public: void Initialize(const LWOZONEID& zoneID); ~dZoneManager(); - Zone* GetZone(); //Gets a pointer to the currently loaded zone. + /* Gets a pointer to the currently loaded zone. */ + Zone* GetZoneMut() const; + const Zone* GetZone() const { return GetZoneMut(); }; void LoadZone(const LWOZONEID& zoneID); //Discard the current zone (if any) and loads a new zone. + + /* Adds a spawner to the zone with the specified ID. */ void AddSpawner(LWOOBJID id, Spawner* spawner); - LWOZONEID GetZoneID() const; + const LWOZONEID& GetZoneID() const; + + /* Creates a new spawner. Returns the finalized ID for the created spawner since some bits may be set to get it to function. */ LWOOBJID MakeSpawner(SpawnerInfo info); Spawner* GetSpawner(LWOOBJID id); void RemoveSpawner(LWOOBJID id); - std::vector GetSpawnersByName(std::string spawnerName); - std::vector GetSpawnersInGroup(std::string group); + std::vector GetSpawnersByName(const std::string& spawnerName); + std::vector GetSpawnersInGroup(const std::string& group); void Update(float deltaTime); Entity* GetZoneControlObject() { return m_ZoneControlObject; } bool GetPlayerLoseCoinOnDeath() { return m_PlayerLoseCoinsOnDeath; } bool GetDisableSaveLocation() { return m_DisableSaveLocation; } bool GetMountsAllowed() { return m_MountsAllowed; } bool GetPetsAllowed() { return m_PetsAllowed; } + + /* Gets the starting ID for missions in the player UI so they are ordered properly and show in the order accepted by the player. */ uint32_t GetUniqueMissionIdStartingValue(); bool CheckIfAccessibleZone(LWOMAPID zoneID); // The world config should not be modified by a caller. - const WorldConfig* GetWorldConfig() { + const WorldConfig& GetWorldConfig() { if (!m_WorldConfig) LoadWorldConfig(); - return m_WorldConfig; + return m_WorldConfig.value(); }; private: @@ -64,7 +71,7 @@ private: bool m_MountsAllowed = true; bool m_PetsAllowed = true; std::map m_Spawners; - WorldConfig* m_WorldConfig = nullptr; + std::optional m_WorldConfig = std::nullopt; Entity* m_ZoneControlObject = nullptr; }; From 0e551429d389ec47a422b64d6cf8958b390493ad Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Mon, 5 May 2025 07:04:43 -0700 Subject: [PATCH 3/6] chore: move all teams logic to its own namespace to consolidate logic (#1777) * Add invite initial response msg re-do team leave logic to send more accurate messages Players are still able to leave the team with the same results as before, however now the correct messages are sent to team chats (no fixes for local teams). * chore: move team logic to separate container Makes it easier to follow team logic when you're not bouncing between 3 classes in 3 files. Consolidates all team logic to 1 namespace in TeamContainer. No logic changes were done, only renaming and fixing errors from the moving. TeamData should be replaced with unique_ptrs at some point so the Shutdown method can be removed from TeamContainer. --- dChatServer/CMakeLists.txt | 3 +- dChatServer/ChatPacketHandler.cpp | 397 +----------------- dChatServer/ChatPacketHandler.h | 35 +- dChatServer/ChatServer.cpp | 20 +- dChatServer/ChatWebAPI.cpp | 28 +- dChatServer/JSONUtils.cpp | 16 +- dChatServer/JSONUtils.h | 8 +- dChatServer/PlayerContainer.cpp | 252 +---------- dChatServer/PlayerContainer.h | 28 +- dChatServer/TeamContainer.cpp | 669 ++++++++++++++++++++++++++++++ dChatServer/TeamContainer.h | 59 +++ 11 files changed, 787 insertions(+), 728 deletions(-) create mode 100644 dChatServer/TeamContainer.cpp create mode 100644 dChatServer/TeamContainer.h diff --git a/dChatServer/CMakeLists.txt b/dChatServer/CMakeLists.txt index 313df6d5..8554ac19 100644 --- a/dChatServer/CMakeLists.txt +++ b/dChatServer/CMakeLists.txt @@ -1,9 +1,10 @@ set(DCHATSERVER_SOURCES "ChatIgnoreList.cpp" "ChatPacketHandler.cpp" - "PlayerContainer.cpp" "ChatWebAPI.cpp" "JSONUtils.cpp" + "PlayerContainer.cpp" + "TeamContainer.cpp" ) add_executable(ChatServer "ChatServer.cpp") diff --git a/dChatServer/ChatPacketHandler.cpp b/dChatServer/ChatPacketHandler.cpp index 3690e511..5722dfa0 100644 --- a/dChatServer/ChatPacketHandler.cpp +++ b/dChatServer/ChatPacketHandler.cpp @@ -19,6 +19,7 @@ #include "StringifiedEnum.h" #include "eGameMasterLevel.h" #include "ChatPackets.h" +#include "TeamContainer.h" void ChatPacketHandler::HandleFriendlistRequest(Packet* packet) { //Get from the packet which player we want to do something with: @@ -447,7 +448,7 @@ void ChatPacketHandler::HandleChatMessage(Packet* packet) { switch (channel) { case eChatChannel::TEAM: { - auto* team = Game::playerContainer.GetTeam(playerID); + auto* team = TeamContainer::GetTeam(playerID); if (team == nullptr) return; for (const auto memberId : team->memberIDs) { @@ -563,400 +564,6 @@ void ChatPacketHandler::SendPrivateChatMessage(const PlayerData& sender, const P SEND_PACKET; } - -void ChatPacketHandler::HandleTeamInvite(Packet* packet) { - CINSTREAM_SKIP_HEADER; - - LWOOBJID playerID; - LUWString invitedPlayer; - - inStream.Read(playerID); - inStream.IgnoreBytes(4); - inStream.Read(invitedPlayer); - - const auto& player = Game::playerContainer.GetPlayerData(playerID); - - if (!player) return; - - auto* team = Game::playerContainer.GetTeam(playerID); - - if (team == nullptr) { - team = Game::playerContainer.CreateTeam(playerID); - } - - const auto& other = Game::playerContainer.GetPlayerData(invitedPlayer.GetAsString()); - - if (!other) return; - - if (Game::playerContainer.GetTeam(other.playerID) != nullptr) { - return; - } - - if (team->memberIDs.size() > 3) { - // no more teams greater than 4 - - LOG("Someone tried to invite a 5th player to a team"); - return; - } - - SendTeamInvite(other, player); - - LOG("Got team invite: %llu -> %s", playerID, invitedPlayer.GetAsString().c_str()); - - bool failed = false; - for (const auto& ignore : other.ignoredPlayers) { - if (ignore.playerId == player.playerID) { - failed = true; - break; - } - } - - ChatPackets::TeamInviteInitialResponse response{}; - response.inviteFailedToSend = failed; - response.playerName = invitedPlayer.string; - ChatPackets::SendRoutedMsg(response, playerID, player.worldServerSysAddr); -} - -void ChatPacketHandler::HandleTeamInviteResponse(Packet* packet) { - CINSTREAM_SKIP_HEADER; - LWOOBJID playerID = LWOOBJID_EMPTY; - inStream.Read(playerID); - uint32_t size = 0; - inStream.Read(size); - char declined = 0; - inStream.Read(declined); - LWOOBJID leaderID = LWOOBJID_EMPTY; - inStream.Read(leaderID); - - LOG("Invite reponse received: %llu -> %llu (%d)", playerID, leaderID, declined); - - if (declined) { - return; - } - - auto* team = Game::playerContainer.GetTeam(leaderID); - - if (team == nullptr) { - LOG("Failed to find team for leader (%llu)", leaderID); - - team = Game::playerContainer.GetTeam(playerID); - } - - if (team == nullptr) { - LOG("Failed to find team for player (%llu)", playerID); - return; - } - - Game::playerContainer.AddMember(team, playerID); -} - -void ChatPacketHandler::HandleTeamLeave(Packet* packet) { - CINSTREAM_SKIP_HEADER; - LWOOBJID playerID = LWOOBJID_EMPTY; - inStream.Read(playerID); - uint32_t size = 0; - inStream.Read(size); - - auto* team = Game::playerContainer.GetTeam(playerID); - - LOG("(%llu) leaving team", playerID); - - if (team != nullptr) { - Game::playerContainer.RemoveMember(team, playerID, false, false, true); - } -} - -void ChatPacketHandler::HandleTeamKick(Packet* packet) { - CINSTREAM_SKIP_HEADER; - - LWOOBJID playerID = LWOOBJID_EMPTY; - LUWString kickedPlayer; - - inStream.Read(playerID); - inStream.IgnoreBytes(4); - inStream.Read(kickedPlayer); - - - LOG("(%llu) kicking (%s) from team", playerID, kickedPlayer.GetAsString().c_str()); - - const auto& kicked = Game::playerContainer.GetPlayerData(kickedPlayer.GetAsString()); - - LWOOBJID kickedId = LWOOBJID_EMPTY; - - if (kicked) { - kickedId = kicked.playerID; - } else { - kickedId = Game::playerContainer.GetId(kickedPlayer.string); - } - - if (kickedId == LWOOBJID_EMPTY) return; - - auto* team = Game::playerContainer.GetTeam(playerID); - - if (team != nullptr) { - if (team->leaderID != playerID || team->leaderID == kickedId) return; - - Game::playerContainer.RemoveMember(team, kickedId, false, true, false); - } -} - -void ChatPacketHandler::HandleTeamPromote(Packet* packet) { - CINSTREAM_SKIP_HEADER; - - LWOOBJID playerID = LWOOBJID_EMPTY; - LUWString promotedPlayer; - - inStream.Read(playerID); - inStream.IgnoreBytes(4); - inStream.Read(promotedPlayer); - - LOG("(%llu) promoting (%s) to team leader", playerID, promotedPlayer.GetAsString().c_str()); - - const auto& promoted = Game::playerContainer.GetPlayerData(promotedPlayer.GetAsString()); - - if (!promoted) return; - - auto* team = Game::playerContainer.GetTeam(playerID); - - if (team != nullptr) { - if (team->leaderID != playerID) return; - - Game::playerContainer.PromoteMember(team, promoted.playerID); - } -} - -void ChatPacketHandler::HandleTeamLootOption(Packet* packet) { - CINSTREAM_SKIP_HEADER; - LWOOBJID playerID = LWOOBJID_EMPTY; - inStream.Read(playerID); - uint32_t size = 0; - inStream.Read(size); - - char option; - inStream.Read(option); - - auto* team = Game::playerContainer.GetTeam(playerID); - - if (team != nullptr) { - if (team->leaderID != playerID) return; - - team->lootFlag = option; - - Game::playerContainer.TeamStatusUpdate(team); - - Game::playerContainer.UpdateTeamsOnWorld(team, false); - } -} - -void ChatPacketHandler::HandleTeamStatusRequest(Packet* packet) { - CINSTREAM_SKIP_HEADER; - LWOOBJID playerID = LWOOBJID_EMPTY; - inStream.Read(playerID); - - auto* team = Game::playerContainer.GetTeam(playerID); - const auto& data = Game::playerContainer.GetPlayerData(playerID); - - if (team != nullptr && data) { - LOG_DEBUG("Player %llu is requesting team status", playerID); - if (team->local && data.zoneID.GetMapID() != team->zoneId.GetMapID() && data.zoneID.GetCloneID() != team->zoneId.GetCloneID()) { - Game::playerContainer.RemoveMember(team, playerID, false, false, false, true); - - return; - } - - if (team->memberIDs.size() <= 1 && !team->local) { - Game::playerContainer.DisbandTeam(team, LWOOBJID_EMPTY, u""); - - return; - } - - if (!team->local) { - ChatPacketHandler::SendTeamSetLeader(data, team->leaderID); - } else { - ChatPacketHandler::SendTeamSetLeader(data, LWOOBJID_EMPTY); - } - - Game::playerContainer.TeamStatusUpdate(team); - - const auto leaderName = GeneralUtils::UTF8ToUTF16(data.playerName); - - for (const auto memberId : team->memberIDs) { - const auto& otherMember = Game::playerContainer.GetPlayerData(memberId); - - if (memberId == playerID) continue; - - const auto memberName = Game::playerContainer.GetName(memberId); - - if (otherMember) { - ChatPacketHandler::SendTeamSetOffWorldFlag(otherMember, data.playerID, data.zoneID); - } - ChatPacketHandler::SendTeamAddPlayer(data, false, team->local, false, memberId, memberName, otherMember ? otherMember.zoneID : LWOZONEID(0, 0, 0)); - } - - Game::playerContainer.UpdateTeamsOnWorld(team, false); - } -} - -void ChatPacketHandler::SendTeamInvite(const PlayerData& receiver, const PlayerData& sender) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(receiver.playerID); - - //portion that will get routed: - BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, MessageType::Client::TEAM_INVITE); - - bitStream.Write(LUWString(sender.playerName.c_str())); - bitStream.Write(sender.playerID); - - SystemAddress sysAddr = receiver.worldServerSysAddr; - SEND_PACKET; -} - -void ChatPacketHandler::SendTeamInviteConfirm(const PlayerData& receiver, bool bLeaderIsFreeTrial, LWOOBJID i64LeaderID, LWOZONEID i64LeaderZoneID, uint8_t ucLootFlag, uint8_t ucNumOfOtherPlayers, uint8_t ucResponseCode, std::u16string wsLeaderName) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(receiver.playerID); - - //portion that will get routed: - CMSGHEADER; - - bitStream.Write(receiver.playerID); - bitStream.Write(MessageType::Game::TEAM_INVITE_CONFIRM); - - bitStream.Write(bLeaderIsFreeTrial); - bitStream.Write(i64LeaderID); - bitStream.Write(i64LeaderZoneID); - bitStream.Write(0); // BinaryBuffe, no clue what's in here - bitStream.Write(ucLootFlag); - bitStream.Write(ucNumOfOtherPlayers); - bitStream.Write(ucResponseCode); - bitStream.Write(wsLeaderName.size()); - for (const auto character : wsLeaderName) { - bitStream.Write(character); - } - - SystemAddress sysAddr = receiver.worldServerSysAddr; - SEND_PACKET; -} - -void ChatPacketHandler::SendTeamStatus(const PlayerData& receiver, LWOOBJID i64LeaderID, LWOZONEID i64LeaderZoneID, uint8_t ucLootFlag, uint8_t ucNumOfOtherPlayers, std::u16string wsLeaderName) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(receiver.playerID); - - //portion that will get routed: - CMSGHEADER; - - bitStream.Write(receiver.playerID); - bitStream.Write(MessageType::Game::TEAM_GET_STATUS_RESPONSE); - - bitStream.Write(i64LeaderID); - bitStream.Write(i64LeaderZoneID); - bitStream.Write(0); // BinaryBuffe, no clue what's in here - bitStream.Write(ucLootFlag); - bitStream.Write(ucNumOfOtherPlayers); - bitStream.Write(wsLeaderName.size()); - for (const auto character : wsLeaderName) { - bitStream.Write(character); - } - - SystemAddress sysAddr = receiver.worldServerSysAddr; - SEND_PACKET; -} - -void ChatPacketHandler::SendTeamSetLeader(const PlayerData& receiver, LWOOBJID i64PlayerID) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(receiver.playerID); - - //portion that will get routed: - CMSGHEADER; - - bitStream.Write(receiver.playerID); - bitStream.Write(MessageType::Game::TEAM_SET_LEADER); - - bitStream.Write(i64PlayerID); - - SystemAddress sysAddr = receiver.worldServerSysAddr; - SEND_PACKET; -} - -void ChatPacketHandler::SendTeamAddPlayer(const PlayerData& receiver, bool bIsFreeTrial, bool bLocal, bool bNoLootOnDeath, LWOOBJID i64PlayerID, std::u16string wsPlayerName, LWOZONEID zoneID) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(receiver.playerID); - - //portion that will get routed: - CMSGHEADER; - - bitStream.Write(receiver.playerID); - bitStream.Write(MessageType::Game::TEAM_ADD_PLAYER); - - bitStream.Write(bIsFreeTrial); - bitStream.Write(bLocal); - bitStream.Write(bNoLootOnDeath); - bitStream.Write(i64PlayerID); - bitStream.Write(wsPlayerName.size()); - for (const auto character : wsPlayerName) { - bitStream.Write(character); - } - bitStream.Write1(); - if (receiver.zoneID.GetCloneID() == zoneID.GetCloneID()) { - zoneID = LWOZONEID(zoneID.GetMapID(), zoneID.GetInstanceID(), 0); - } - bitStream.Write(zoneID); - - SystemAddress sysAddr = receiver.worldServerSysAddr; - SEND_PACKET; -} - -void ChatPacketHandler::SendTeamRemovePlayer(const PlayerData& receiver, bool bDisband, bool bIsKicked, bool bIsLeaving, bool bLocal, LWOOBJID i64LeaderID, LWOOBJID i64PlayerID, std::u16string wsPlayerName) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(receiver.playerID); - - //portion that will get routed: - CMSGHEADER; - - bitStream.Write(receiver.playerID); - bitStream.Write(MessageType::Game::TEAM_REMOVE_PLAYER); - - bitStream.Write(bDisband); - bitStream.Write(bIsKicked); - bitStream.Write(bIsLeaving); - bitStream.Write(bLocal); - bitStream.Write(i64LeaderID); - bitStream.Write(i64PlayerID); - bitStream.Write(wsPlayerName.size()); - for (const auto character : wsPlayerName) { - bitStream.Write(character); - } - - SystemAddress sysAddr = receiver.worldServerSysAddr; - SEND_PACKET; -} - -void ChatPacketHandler::SendTeamSetOffWorldFlag(const PlayerData& receiver, LWOOBJID i64PlayerID, LWOZONEID zoneID) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); - bitStream.Write(receiver.playerID); - - //portion that will get routed: - CMSGHEADER; - - bitStream.Write(receiver.playerID); - bitStream.Write(MessageType::Game::TEAM_SET_OFF_WORLD_FLAG); - - bitStream.Write(i64PlayerID); - if (receiver.zoneID.GetCloneID() == zoneID.GetCloneID()) { - zoneID = LWOZONEID(zoneID.GetMapID(), zoneID.GetInstanceID(), 0); - } - bitStream.Write(zoneID); - - SystemAddress sysAddr = receiver.worldServerSysAddr; - SEND_PACKET; -} - void ChatPacketHandler::SendFriendUpdate(const PlayerData& friendData, const PlayerData& playerData, uint8_t notifyType, uint8_t isBestFriend) { /*chat notification is displayed if log in / out and friend is updated in friends list [u8] - update type diff --git a/dChatServer/ChatPacketHandler.h b/dChatServer/ChatPacketHandler.h index ca5c3648..29a20587 100644 --- a/dChatServer/ChatPacketHandler.h +++ b/dChatServer/ChatPacketHandler.h @@ -35,13 +35,13 @@ enum class eChatChannel : uint8_t { enum class eChatMessageResponseCode : uint8_t { - SENT = 0, - NOTONLINE, - GENERALERROR, - RECEIVEDNEWWHISPER, - NOTFRIENDS, - SENDERFREETRIAL, - RECEIVERFREETRIAL, + SENT = 0, + NOTONLINE, + GENERALERROR, + RECEIVEDNEWWHISPER, + NOTFRIENDS, + SENDERFREETRIAL, + RECEIVERFREETRIAL, }; namespace ChatPacketHandler { @@ -52,33 +52,14 @@ namespace ChatPacketHandler { void HandleGMLevelUpdate(Packet* packet); void HandleWho(Packet* packet); void HandleShowAll(Packet* packet); - void HandleChatMessage(Packet* packet); void HandlePrivateChatMessage(Packet* packet); - void SendPrivateChatMessage(const PlayerData& sender, const PlayerData& receiver, const PlayerData& routeTo, const LUWString& message, const eChatChannel channel, const eChatMessageResponseCode responseCode); - void HandleTeamInvite(Packet* packet); - void HandleTeamInviteResponse(Packet* packet); - void HandleTeamLeave(Packet* packet); - void HandleTeamKick(Packet* packet); - void HandleTeamPromote(Packet* packet); - void HandleTeamLootOption(Packet* packet); - void HandleTeamStatusRequest(Packet* packet); void OnAchievementNotify(RakNet::BitStream& bitstream, const SystemAddress& sysAddr); - void SendTeamInvite(const PlayerData& receiver, const PlayerData& sender); - void SendTeamInviteConfirm(const PlayerData& receiver, bool bLeaderIsFreeTrial, LWOOBJID i64LeaderID, LWOZONEID i64LeaderZoneID, uint8_t ucLootFlag, uint8_t ucNumOfOtherPlayers, uint8_t ucResponseCode, std::u16string wsLeaderName); - void SendTeamStatus(const PlayerData& receiver, LWOOBJID i64LeaderID, LWOZONEID i64LeaderZoneID, uint8_t ucLootFlag, uint8_t ucNumOfOtherPlayers, std::u16string wsLeaderName); - void SendTeamSetLeader(const PlayerData& receiver, LWOOBJID i64PlayerID); - void SendTeamAddPlayer(const PlayerData& receiver, bool bIsFreeTrial, bool bLocal, bool bNoLootOnDeath, LWOOBJID i64PlayerID, std::u16string wsPlayerName, LWOZONEID zoneID); - - /* Sends a message to the provided `receiver` with information about the updated team. If `i64LeaderID` is not LWOOBJID_EMPTY, the client will update the leader to that new playerID. */ - void SendTeamRemovePlayer(const PlayerData& receiver, bool bDisband, bool bIsKicked, bool bIsLeaving, bool bLocal, LWOOBJID i64LeaderID, LWOOBJID i64PlayerID, std::u16string wsPlayerName); - void SendTeamSetOffWorldFlag(const PlayerData& receiver, LWOOBJID i64PlayerID, LWOZONEID zoneID); - //FriendData is the player we're SENDING this stuff to. Player is the friend that changed state. void SendFriendUpdate(const PlayerData& friendData, const PlayerData& playerData, uint8_t notifyType, uint8_t isBestFriend); - + void SendPrivateChatMessage(const PlayerData& sender, const PlayerData& receiver, const PlayerData& routeTo, const LUWString& message, const eChatChannel channel, const eChatMessageResponseCode responseCode); void SendFriendRequest(const PlayerData& receiver, const PlayerData& sender); void SendFriendResponse(const PlayerData& receiver, const PlayerData& sender, eAddFriendResponseType responseCode, uint8_t isBestFriendsAlready = 0U, uint8_t isBestFriendRequest = 0U); void SendRemoveFriend(const PlayerData& receiver, std::string& personToRemove, bool isSuccessful); diff --git a/dChatServer/ChatServer.cpp b/dChatServer/ChatServer.cpp index d6728a9a..5c673e27 100644 --- a/dChatServer/ChatServer.cpp +++ b/dChatServer/ChatServer.cpp @@ -20,6 +20,7 @@ #include "MessageType/World.h" #include "ChatIgnoreList.h" #include "StringifiedEnum.h" +#include "TeamContainer.h" #include "Game.h" #include "Server.h" @@ -95,7 +96,7 @@ int main(int argc, char** argv) { // seyup the chat api web server bool web_server_enabled = Game::config->GetValue("web_server_enabled") == "1"; ChatWebAPI chatwebapi; - if (web_server_enabled && !chatwebapi.Startup()){ + if (web_server_enabled && !chatwebapi.Startup()) { // if we want the web api and it fails to start, exit LOG("Failed to start web server, shutting down."); Database::Destroy("ChatServer"); @@ -197,6 +198,7 @@ int main(int argc, char** argv) { std::this_thread::sleep_until(t); } Game::playerContainer.Shutdown(); + TeamContainer::Shutdown(); //Delete our objects here: Database::Destroy("ChatServer"); delete Game::server; @@ -234,7 +236,7 @@ void HandlePacket(Packet* packet) { break; case MessageType::Chat::CREATE_TEAM: - Game::playerContainer.CreateTeamServer(packet); + TeamContainer::CreateTeamServer(packet); break; case MessageType::Chat::GET_FRIENDS_LIST: @@ -254,7 +256,7 @@ void HandlePacket(Packet* packet) { break; case MessageType::Chat::TEAM_GET_STATUS: - ChatPacketHandler::HandleTeamStatusRequest(packet); + TeamContainer::HandleTeamStatusRequest(packet); break; case MessageType::Chat::ADD_FRIEND_REQUEST: @@ -284,27 +286,27 @@ void HandlePacket(Packet* packet) { break; case MessageType::Chat::TEAM_INVITE: - ChatPacketHandler::HandleTeamInvite(packet); + TeamContainer::HandleTeamInvite(packet); break; case MessageType::Chat::TEAM_INVITE_RESPONSE: - ChatPacketHandler::HandleTeamInviteResponse(packet); + TeamContainer::HandleTeamInviteResponse(packet); break; case MessageType::Chat::TEAM_LEAVE: - ChatPacketHandler::HandleTeamLeave(packet); + TeamContainer::HandleTeamLeave(packet); break; case MessageType::Chat::TEAM_SET_LEADER: - ChatPacketHandler::HandleTeamPromote(packet); + TeamContainer::HandleTeamPromote(packet); break; case MessageType::Chat::TEAM_KICK: - ChatPacketHandler::HandleTeamKick(packet); + TeamContainer::HandleTeamKick(packet); break; case MessageType::Chat::TEAM_SET_LOOT: - ChatPacketHandler::HandleTeamLootOption(packet); + TeamContainer::HandleTeamLootOption(packet); break; case MessageType::Chat::GMLEVEL_UPDATE: ChatPacketHandler::HandleGMLevelUpdate(packet); diff --git a/dChatServer/ChatWebAPI.cpp b/dChatServer/ChatWebAPI.cpp index 49903ced..c083c183 100644 --- a/dChatServer/ChatWebAPI.cpp +++ b/dChatServer/ChatWebAPI.cpp @@ -28,7 +28,7 @@ typedef struct mg_http_message mg_http_message; namespace { const char* json_content_type = "Content-Type: application/json\r\n"; - std::map, WebAPIHTTPRoute> Routes {}; + std::map, WebAPIHTTPRoute> Routes{}; } bool ValidateAuthentication(const mg_http_message* http_msg) { @@ -54,7 +54,7 @@ void HandlePlayersRequest(HTTPReply& reply, std::string body) { } void HandleTeamsRequest(HTTPReply& reply, std::string body) { - const json data = Game::playerContainer.GetTeamContainer(); + const json data = TeamContainer::GetTeamContainer(); reply.status = data.empty() ? eHTTPStatusCode::NO_CONTENT : eHTTPStatusCode::OK; reply.message = data.empty() ? "{\"error\":\"No Teams Online\"}" : data.dump(); } @@ -87,12 +87,12 @@ void HandleInvalidRoute(HTTPReply& reply) { void HandleHTTPMessage(mg_connection* connection, const mg_http_message* http_msg) { HTTPReply reply; - + if (!http_msg) { reply.status = eHTTPStatusCode::BAD_REQUEST; reply.message = "{\"error\":\"Invalid Request\"}"; } else if (ValidateAuthentication(http_msg)) { - + // convert method from cstring to std string std::string method_string(http_msg->method.buf, http_msg->method.len); // get mehtod from mg to enum @@ -105,8 +105,8 @@ void HandleHTTPMessage(mg_connection* connection, const mg_http_message* http_ms // convert body from cstring to std string std::string body(http_msg->body.buf, http_msg->body.len); - - const auto routeItr = Routes.find({method, uri}); + + const auto routeItr = Routes.find({ method, uri }); if (routeItr != Routes.end()) { const auto& [_, route] = *routeItr; @@ -122,11 +122,11 @@ void HandleHTTPMessage(mg_connection* connection, const mg_http_message* http_ms void HandleRequests(mg_connection* connection, int request, void* request_data) { switch (request) { - case MG_EV_HTTP_MSG: - HandleHTTPMessage(connection, static_cast(request_data)); - break; - default: - break; + case MG_EV_HTTP_MSG: + HandleHTTPMessage(connection, static_cast(request_data)); + break; + default: + break; } } @@ -172,19 +172,19 @@ bool ChatWebAPI::Startup() { .path = v1_route + "players", .method = eHTTPMethod::GET, .handle = HandlePlayersRequest - }); + }); RegisterHTTPRoutes({ .path = v1_route + "teams", .method = eHTTPMethod::GET, .handle = HandleTeamsRequest - }); + }); RegisterHTTPRoutes({ .path = v1_route + "announce", .method = eHTTPMethod::POST, .handle = HandleAnnounceRequest - }); + }); return true; } diff --git a/dChatServer/JSONUtils.cpp b/dChatServer/JSONUtils.cpp index 1c32409c..116961fb 100644 --- a/dChatServer/JSONUtils.cpp +++ b/dChatServer/JSONUtils.cpp @@ -18,19 +18,12 @@ void to_json(json& data, const PlayerData& playerData) { void to_json(json& data, const PlayerContainer& playerContainer) { data = json::array(); - for(auto& playerData : playerContainer.GetAllPlayers()) { + for (auto& playerData : playerContainer.GetAllPlayers()) { if (playerData.first == LWOOBJID_EMPTY) continue; data.push_back(playerData.second); } } -void to_json(json& data, const TeamContainer& teamContainer) { - for (auto& teamData : Game::playerContainer.GetTeams()) { - if (!teamData) continue; - data.push_back(*teamData); - } -} - void to_json(json& data, const TeamData& teamData) { data["id"] = teamData.teamID; data["loot_flag"] = teamData.lootFlag; @@ -48,6 +41,13 @@ void to_json(json& data, const TeamData& teamData) { } } +void TeamContainer::to_json(json& data, const TeamContainer::Data& teamContainer) { + for (auto& teamData : TeamContainer::GetTeams()) { + if (!teamData) continue; + data.push_back(*teamData); + } +} + std::string JSONUtils::CheckRequiredData(const json& data, const std::vector& requiredData) { json check; check["error"] = json::array(); diff --git a/dChatServer/JSONUtils.h b/dChatServer/JSONUtils.h index a46a1667..ccdd5359 100644 --- a/dChatServer/JSONUtils.h +++ b/dChatServer/JSONUtils.h @@ -3,12 +3,18 @@ #include "json_fwd.hpp" #include "PlayerContainer.h" +#include "TeamContainer.h" + +/* Remember, to_json needs to be in the same namespace as the class its located in */ void to_json(nlohmann::json& data, const PlayerData& playerData); void to_json(nlohmann::json& data, const PlayerContainer& playerContainer); -void to_json(nlohmann::json& data, const TeamContainer& teamData); void to_json(nlohmann::json& data, const TeamData& teamData); +namespace TeamContainer { + void to_json(nlohmann::json& data, const TeamContainer::Data& teamData); +}; + namespace JSONUtils { // check required data for reqeust std::string CheckRequiredData(const nlohmann::json& data, const std::vector& requiredData); diff --git a/dChatServer/PlayerContainer.cpp b/dChatServer/PlayerContainer.cpp index b3323f81..ec53fa32 100644 --- a/dChatServer/PlayerContainer.cpp +++ b/dChatServer/PlayerContainer.cpp @@ -12,6 +12,7 @@ #include "ChatPackets.h" #include "dConfig.h" #include "MessageType/Chat.h" +#include "TeamContainer.h" void PlayerContainer::Initialize() { m_MaxNumberOfBestFriends = @@ -99,7 +100,7 @@ void PlayerContainer::RemovePlayer(const LWOOBJID playerID) { if (fd) ChatPacketHandler::SendFriendUpdate(fd, player, 0, fr.isBestFriend); } - auto* team = GetTeam(playerID); + auto* team = TeamContainer::GetTeam(playerID); if (team != nullptr) { const auto memberName = GeneralUtils::UTF8ToUTF16(player.playerName); @@ -109,7 +110,7 @@ void PlayerContainer::RemovePlayer(const LWOOBJID playerID) { if (!otherMember) continue; - ChatPacketHandler::SendTeamSetOffWorldFlag(otherMember, playerID, { 0, 0, 0 }); + TeamContainer::SendTeamSetOffWorldFlag(otherMember, playerID, { 0, 0, 0 }); } } @@ -140,40 +141,6 @@ void PlayerContainer::MuteUpdate(Packet* packet) { BroadcastMuteUpdate(playerID, expire); } -void PlayerContainer::CreateTeamServer(Packet* packet) { - CINSTREAM_SKIP_HEADER; - LWOOBJID playerID; - inStream.Read(playerID); - size_t membersSize = 0; - inStream.Read(membersSize); - - if (membersSize >= 4) { - LOG("Tried to create a team with more than 4 players"); - return; - } - - std::vector members; - - members.reserve(membersSize); - - for (size_t i = 0; i < membersSize; i++) { - LWOOBJID member; - inStream.Read(member); - members.push_back(member); - } - - LWOZONEID zoneId; - - inStream.Read(zoneId); - - auto* team = CreateLocalTeam(members); - - if (team != nullptr) { - team->zoneId = zoneId; - UpdateTeamsOnWorld(team, false); - } -} - void PlayerContainer::BroadcastMuteUpdate(LWOOBJID player, time_t time) { CBITSTREAM; BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::GM_MUTE); @@ -184,218 +151,6 @@ void PlayerContainer::BroadcastMuteUpdate(LWOOBJID player, time_t time) { Game::server->Send(bitStream, UNASSIGNED_SYSTEM_ADDRESS, true); } -TeamData* PlayerContainer::CreateLocalTeam(std::vector members) { - if (members.empty()) { - return nullptr; - } - - TeamData* newTeam = nullptr; - - for (const auto member : members) { - auto* team = GetTeam(member); - - if (team != nullptr) { - RemoveMember(team, member, false, false, true); - } - - if (newTeam == nullptr) { - newTeam = CreateTeam(member, true); - } else { - AddMember(newTeam, member); - } - } - - newTeam->lootFlag = 1; - - TeamStatusUpdate(newTeam); - - return newTeam; -} - -TeamData* PlayerContainer::CreateTeam(LWOOBJID leader, bool local) { - auto* team = new TeamData(); - - team->teamID = ++m_TeamIDCounter; - team->leaderID = leader; - team->local = local; - - GetTeamsMut().push_back(team); - - AddMember(team, leader); - - return team; -} - -TeamData* PlayerContainer::GetTeam(LWOOBJID playerID) { - for (auto* team : GetTeams()) { - if (std::find(team->memberIDs.begin(), team->memberIDs.end(), playerID) == team->memberIDs.end()) continue; - - return team; - } - - return nullptr; -} - -void PlayerContainer::AddMember(TeamData* team, LWOOBJID playerID) { - if (team->memberIDs.size() >= 4) { - LOG("Tried to add player to team that already had 4 players"); - const auto& player = GetPlayerData(playerID); - if (!player) return; - ChatPackets::SendSystemMessage(player.worldServerSysAddr, u"The teams is full! You have not been added to a team!"); - return; - } - - const auto index = std::find(team->memberIDs.begin(), team->memberIDs.end(), playerID); - - if (index != team->memberIDs.end()) return; - - team->memberIDs.push_back(playerID); - - const auto& leader = GetPlayerData(team->leaderID); - const auto& member = GetPlayerData(playerID); - - if (!leader || !member) return; - - const auto leaderName = GeneralUtils::UTF8ToUTF16(leader.playerName); - const auto memberName = GeneralUtils::UTF8ToUTF16(member.playerName); - - ChatPacketHandler::SendTeamInviteConfirm(member, false, leader.playerID, leader.zoneID, team->lootFlag, 0, 0, leaderName); - - if (!team->local) { - ChatPacketHandler::SendTeamSetLeader(member, leader.playerID); - } else { - ChatPacketHandler::SendTeamSetLeader(member, LWOOBJID_EMPTY); - } - - UpdateTeamsOnWorld(team, false); - - for (const auto memberId : team->memberIDs) { - const auto& otherMember = GetPlayerData(memberId); - - if (otherMember == member) continue; - - const auto otherMemberName = GetName(memberId); - - ChatPacketHandler::SendTeamAddPlayer(member, false, team->local, false, memberId, otherMemberName, otherMember ? otherMember.zoneID : LWOZONEID(0, 0, 0)); - - if (otherMember) { - ChatPacketHandler::SendTeamAddPlayer(otherMember, false, team->local, false, member.playerID, memberName, member.zoneID); - } - } -} - -void PlayerContainer::RemoveMember(TeamData* team, LWOOBJID causingPlayerID, bool disband, bool kicked, bool leaving, bool silent) { - LOG_DEBUG("Player %llu is leaving team %i", causingPlayerID, team->teamID); - const auto index = std::ranges::find(team->memberIDs, causingPlayerID); - - if (index == team->memberIDs.end()) return; - - team->memberIDs.erase(index); - - const auto& member = GetPlayerData(causingPlayerID); - - const auto causingMemberName = GetName(causingPlayerID); - - if (member && !silent) { - ChatPacketHandler::SendTeamRemovePlayer(member, disband, kicked, leaving, team->local, LWOOBJID_EMPTY, causingPlayerID, causingMemberName); - } - - if (team->memberIDs.size() <= 1) { - DisbandTeam(team, causingPlayerID, causingMemberName); - } else /* team has enough members to be a team still */ { - team->leaderID = (causingPlayerID == team->leaderID) ? team->memberIDs[0] : team->leaderID; - for (const auto memberId : team->memberIDs) { - if (silent && memberId == causingPlayerID) { - continue; - } - - const auto& otherMember = GetPlayerData(memberId); - - if (!otherMember) continue; - - ChatPacketHandler::SendTeamRemovePlayer(otherMember, disband, kicked, leaving, team->local, team->leaderID, causingPlayerID, causingMemberName); - } - - UpdateTeamsOnWorld(team, false); - } -} - -void PlayerContainer::PromoteMember(TeamData* team, LWOOBJID newLeader) { - team->leaderID = newLeader; - - for (const auto memberId : team->memberIDs) { - const auto& otherMember = GetPlayerData(memberId); - - if (!otherMember) continue; - - ChatPacketHandler::SendTeamSetLeader(otherMember, newLeader); - } -} - -void PlayerContainer::DisbandTeam(TeamData* team, const LWOOBJID causingPlayerID, const std::u16string& causingPlayerName) { - const auto index = std::ranges::find(GetTeams(), team); - - if (index == GetTeams().end()) return; - LOG_DEBUG("Disbanding team %i", (*index)->teamID); - - for (const auto memberId : team->memberIDs) { - const auto& otherMember = GetPlayerData(memberId); - - if (!otherMember) continue; - - ChatPacketHandler::SendTeamSetLeader(otherMember, LWOOBJID_EMPTY); - ChatPacketHandler::SendTeamRemovePlayer(otherMember, true, false, false, team->local, team->leaderID, causingPlayerID, causingPlayerName); - } - - UpdateTeamsOnWorld(team, true); - - GetTeamsMut().erase(index); - - delete team; -} - -void PlayerContainer::TeamStatusUpdate(TeamData* team) { - const auto index = std::find(GetTeams().begin(), GetTeams().end(), team); - - if (index == GetTeams().end()) return; - - const auto& leader = GetPlayerData(team->leaderID); - - if (!leader) return; - - const auto leaderName = GeneralUtils::UTF8ToUTF16(leader.playerName); - - for (const auto memberId : team->memberIDs) { - const auto& otherMember = GetPlayerData(memberId); - - if (!otherMember) continue; - - if (!team->local) { - ChatPacketHandler::SendTeamStatus(otherMember, team->leaderID, leader.zoneID, team->lootFlag, 0, leaderName); - } - } - - UpdateTeamsOnWorld(team, false); -} - -void PlayerContainer::UpdateTeamsOnWorld(TeamData* team, bool deleteTeam) { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::TEAM_GET_STATUS); - - bitStream.Write(team->teamID); - bitStream.Write(deleteTeam); - - if (!deleteTeam) { - bitStream.Write(team->lootFlag); - bitStream.Write(team->memberIDs.size()); - for (const auto memberID : team->memberIDs) { - bitStream.Write(memberID); - } - } - - Game::server->Send(bitStream, UNASSIGNED_SYSTEM_ADDRESS, true); -} - std::u16string PlayerContainer::GetName(LWOOBJID playerID) { const auto iter = m_Names.find(playerID); @@ -444,5 +199,4 @@ void PlayerContainer::Shutdown() { Database::Get()->UpdateActivityLog(id, eActivityType::PlayerLoggedOut, playerData.zoneID.GetMapID()); m_Players.erase(m_Players.begin()); } - for (auto* team : GetTeams()) if (team) delete team; } diff --git a/dChatServer/PlayerContainer.h b/dChatServer/PlayerContainer.h index f4cffa3d..20785747 100644 --- a/dChatServer/PlayerContainer.h +++ b/dChatServer/PlayerContainer.h @@ -11,10 +11,6 @@ enum class eGameMasterLevel : uint8_t; struct TeamData; -struct TeamContainer { - std::vector mTeams; -}; - struct IgnoreData { IgnoreData(const std::string& name, const LWOOBJID& id) : playerName{ name }, playerId{ id } {} inline bool operator==(const std::string& other) const noexcept { @@ -73,7 +69,6 @@ public: void ScheduleRemovePlayer(Packet* packet); void RemovePlayer(const LWOOBJID playerID); void MuteUpdate(Packet* packet); - void CreateTeamServer(Packet* packet); void BroadcastMuteUpdate(LWOOBJID player, time_t time); void Shutdown(); @@ -81,34 +76,19 @@ public: const PlayerData& GetPlayerData(const std::string& playerName); PlayerData& GetPlayerDataMutable(const LWOOBJID& playerID); PlayerData& GetPlayerDataMutable(const std::string& playerName); + std::u16string GetName(LWOOBJID playerID); + LWOOBJID GetId(const std::u16string& playerName); + void Update(const float deltaTime); + uint32_t GetPlayerCount() { return m_PlayerCount; }; uint32_t GetSimCount() { return m_SimCount; }; const std::map& GetAllPlayers() const { return m_Players; }; - - TeamData* CreateLocalTeam(std::vector members); - TeamData* CreateTeam(LWOOBJID leader, bool local = false); - TeamData* GetTeam(LWOOBJID playerID); - void AddMember(TeamData* team, LWOOBJID playerID); - void RemoveMember(TeamData* team, LWOOBJID playerID, bool disband, bool kicked, bool leaving, bool silent = false); - void PromoteMember(TeamData* team, LWOOBJID newLeader); - void DisbandTeam(TeamData* team, const LWOOBJID causingPlayerID, const std::u16string& causingPlayerName); - void TeamStatusUpdate(TeamData* team); - void UpdateTeamsOnWorld(TeamData* team, bool deleteTeam); - std::u16string GetName(LWOOBJID playerID); - LWOOBJID GetId(const std::u16string& playerName); uint32_t GetMaxNumberOfBestFriends() { return m_MaxNumberOfBestFriends; } uint32_t GetMaxNumberOfFriends() { return m_MaxNumberOfFriends; } - const TeamContainer& GetTeamContainer() { return m_TeamContainer; } - std::vector& GetTeamsMut() { return m_TeamContainer.mTeams; }; - const std::vector& GetTeams() { return GetTeamsMut(); }; - - void Update(const float deltaTime); bool PlayerBeingRemoved(const LWOOBJID playerID) { return m_PlayersToRemove.contains(playerID); } private: - LWOOBJID m_TeamIDCounter = 0; std::map m_Players; - TeamContainer m_TeamContainer{}; std::unordered_map m_Names; std::map m_PlayersToRemove; uint32_t m_MaxNumberOfBestFriends = 5; diff --git a/dChatServer/TeamContainer.cpp b/dChatServer/TeamContainer.cpp new file mode 100644 index 00000000..024a0464 --- /dev/null +++ b/dChatServer/TeamContainer.cpp @@ -0,0 +1,669 @@ +#include "TeamContainer.h" + +#include "ChatPackets.h" + +#include "MessageType/Chat.h" +#include "MessageType/Game.h" + +#include "ChatPacketHandler.h" +#include "PlayerContainer.h" + +namespace { + TeamContainer::Data g_TeamContainer{}; + LWOOBJID g_TeamIDCounter = 0; +} + +const TeamContainer::Data& TeamContainer::GetTeamContainer() { + return g_TeamContainer; +} + +std::vector& TeamContainer::GetTeamsMut() { + return g_TeamContainer.mTeams; +} + +const std::vector& TeamContainer::GetTeams() { + return GetTeamsMut(); +} + +void TeamContainer::Shutdown() { + for (auto* team : g_TeamContainer.mTeams) if (team) delete team; +} + +void TeamContainer::HandleTeamInvite(Packet* packet) { + CINSTREAM_SKIP_HEADER; + + LWOOBJID playerID; + LUWString invitedPlayer; + + inStream.Read(playerID); + inStream.IgnoreBytes(4); + inStream.Read(invitedPlayer); + + const auto& player = Game::playerContainer.GetPlayerData(playerID); + + if (!player) return; + + auto* team = GetTeam(playerID); + + if (team == nullptr) { + team = CreateTeam(playerID); + } + + const auto& other = Game::playerContainer.GetPlayerData(invitedPlayer.GetAsString()); + + if (!other) return; + + if (GetTeam(other.playerID) != nullptr) { + return; + } + + if (team->memberIDs.size() > 3) { + // no more teams greater than 4 + + LOG("Someone tried to invite a 5th player to a team"); + return; + } + + SendTeamInvite(other, player); + + LOG("Got team invite: %llu -> %s", playerID, invitedPlayer.GetAsString().c_str()); + + bool failed = false; + for (const auto& ignore : other.ignoredPlayers) { + if (ignore.playerId == player.playerID) { + failed = true; + break; + } + } + + ChatPackets::TeamInviteInitialResponse response{}; + response.inviteFailedToSend = failed; + response.playerName = invitedPlayer.string; + ChatPackets::SendRoutedMsg(response, playerID, player.worldServerSysAddr); +} + +void TeamContainer::HandleTeamInviteResponse(Packet* packet) { + CINSTREAM_SKIP_HEADER; + LWOOBJID playerID = LWOOBJID_EMPTY; + inStream.Read(playerID); + uint32_t size = 0; + inStream.Read(size); + char declined = 0; + inStream.Read(declined); + LWOOBJID leaderID = LWOOBJID_EMPTY; + inStream.Read(leaderID); + + LOG("Invite reponse received: %llu -> %llu (%d)", playerID, leaderID, declined); + + if (declined) { + return; + } + + auto* team = GetTeam(leaderID); + + if (team == nullptr) { + LOG("Failed to find team for leader (%llu)", leaderID); + + team = GetTeam(playerID); + } + + if (team == nullptr) { + LOG("Failed to find team for player (%llu)", playerID); + return; + } + + AddMember(team, playerID); +} + +void TeamContainer::HandleTeamLeave(Packet* packet) { + CINSTREAM_SKIP_HEADER; + LWOOBJID playerID = LWOOBJID_EMPTY; + inStream.Read(playerID); + uint32_t size = 0; + inStream.Read(size); + + auto* team = GetTeam(playerID); + + LOG("(%llu) leaving team", playerID); + + if (team != nullptr) { + RemoveMember(team, playerID, false, false, true); + } +} + +void TeamContainer::HandleTeamKick(Packet* packet) { + CINSTREAM_SKIP_HEADER; + + LWOOBJID playerID = LWOOBJID_EMPTY; + LUWString kickedPlayer; + + inStream.Read(playerID); + inStream.IgnoreBytes(4); + inStream.Read(kickedPlayer); + + + LOG("(%llu) kicking (%s) from team", playerID, kickedPlayer.GetAsString().c_str()); + + const auto& kicked = Game::playerContainer.GetPlayerData(kickedPlayer.GetAsString()); + + LWOOBJID kickedId = LWOOBJID_EMPTY; + + if (kicked) { + kickedId = kicked.playerID; + } else { + kickedId = Game::playerContainer.GetId(kickedPlayer.string); + } + + if (kickedId == LWOOBJID_EMPTY) return; + + auto* team = GetTeam(playerID); + + if (team != nullptr) { + if (team->leaderID != playerID || team->leaderID == kickedId) return; + + RemoveMember(team, kickedId, false, true, false); + } +} + +void TeamContainer::HandleTeamPromote(Packet* packet) { + CINSTREAM_SKIP_HEADER; + + LWOOBJID playerID = LWOOBJID_EMPTY; + LUWString promotedPlayer; + + inStream.Read(playerID); + inStream.IgnoreBytes(4); + inStream.Read(promotedPlayer); + + LOG("(%llu) promoting (%s) to team leader", playerID, promotedPlayer.GetAsString().c_str()); + + const auto& promoted = Game::playerContainer.GetPlayerData(promotedPlayer.GetAsString()); + + if (!promoted) return; + + auto* team = GetTeam(playerID); + + if (team != nullptr) { + if (team->leaderID != playerID) return; + + PromoteMember(team, promoted.playerID); + } +} + +void TeamContainer::HandleTeamLootOption(Packet* packet) { + CINSTREAM_SKIP_HEADER; + LWOOBJID playerID = LWOOBJID_EMPTY; + inStream.Read(playerID); + uint32_t size = 0; + inStream.Read(size); + + char option; + inStream.Read(option); + + auto* team = GetTeam(playerID); + + if (team != nullptr) { + if (team->leaderID != playerID) return; + + team->lootFlag = option; + + TeamStatusUpdate(team); + + UpdateTeamsOnWorld(team, false); + } +} + +void TeamContainer::HandleTeamStatusRequest(Packet* packet) { + CINSTREAM_SKIP_HEADER; + LWOOBJID playerID = LWOOBJID_EMPTY; + inStream.Read(playerID); + + auto* team = GetTeam(playerID); + const auto& data = Game::playerContainer.GetPlayerData(playerID); + + if (team != nullptr && data) { + LOG_DEBUG("Player %llu is requesting team status", playerID); + if (team->local && data.zoneID.GetMapID() != team->zoneId.GetMapID() && data.zoneID.GetCloneID() != team->zoneId.GetCloneID()) { + RemoveMember(team, playerID, false, false, false, true); + + return; + } + + if (team->memberIDs.size() <= 1 && !team->local) { + DisbandTeam(team, LWOOBJID_EMPTY, u""); + + return; + } + + if (!team->local) { + SendTeamSetLeader(data, team->leaderID); + } else { + SendTeamSetLeader(data, LWOOBJID_EMPTY); + } + + TeamStatusUpdate(team); + + const auto leaderName = GeneralUtils::UTF8ToUTF16(data.playerName); + + for (const auto memberId : team->memberIDs) { + const auto& otherMember = Game::playerContainer.GetPlayerData(memberId); + + if (memberId == playerID) continue; + + const auto memberName = Game::playerContainer.GetName(memberId); + + if (otherMember) { + SendTeamSetOffWorldFlag(otherMember, data.playerID, data.zoneID); + } + SendTeamAddPlayer(data, false, team->local, false, memberId, memberName, otherMember ? otherMember.zoneID : LWOZONEID(0, 0, 0)); + } + + UpdateTeamsOnWorld(team, false); + } +} + +void TeamContainer::SendTeamInvite(const PlayerData& receiver, const PlayerData& sender) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(receiver.playerID); + + //portion that will get routed: + BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, MessageType::Client::TEAM_INVITE); + + bitStream.Write(LUWString(sender.playerName.c_str())); + bitStream.Write(sender.playerID); + + SystemAddress sysAddr = receiver.worldServerSysAddr; + SEND_PACKET; +} + +void TeamContainer::SendTeamInviteConfirm(const PlayerData& receiver, bool bLeaderIsFreeTrial, LWOOBJID i64LeaderID, LWOZONEID i64LeaderZoneID, uint8_t ucLootFlag, uint8_t ucNumOfOtherPlayers, uint8_t ucResponseCode, std::u16string wsLeaderName) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(receiver.playerID); + + //portion that will get routed: + CMSGHEADER; + + bitStream.Write(receiver.playerID); + bitStream.Write(MessageType::Game::TEAM_INVITE_CONFIRM); + + bitStream.Write(bLeaderIsFreeTrial); + bitStream.Write(i64LeaderID); + bitStream.Write(i64LeaderZoneID); + bitStream.Write(0); // BinaryBuffe, no clue what's in here + bitStream.Write(ucLootFlag); + bitStream.Write(ucNumOfOtherPlayers); + bitStream.Write(ucResponseCode); + bitStream.Write(wsLeaderName.size()); + for (const auto character : wsLeaderName) { + bitStream.Write(character); + } + + SystemAddress sysAddr = receiver.worldServerSysAddr; + SEND_PACKET; +} + +void TeamContainer::SendTeamStatus(const PlayerData& receiver, LWOOBJID i64LeaderID, LWOZONEID i64LeaderZoneID, uint8_t ucLootFlag, uint8_t ucNumOfOtherPlayers, std::u16string wsLeaderName) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(receiver.playerID); + + //portion that will get routed: + CMSGHEADER; + + bitStream.Write(receiver.playerID); + bitStream.Write(MessageType::Game::TEAM_GET_STATUS_RESPONSE); + + bitStream.Write(i64LeaderID); + bitStream.Write(i64LeaderZoneID); + bitStream.Write(0); // BinaryBuffe, no clue what's in here + bitStream.Write(ucLootFlag); + bitStream.Write(ucNumOfOtherPlayers); + bitStream.Write(wsLeaderName.size()); + for (const auto character : wsLeaderName) { + bitStream.Write(character); + } + + SystemAddress sysAddr = receiver.worldServerSysAddr; + SEND_PACKET; +} + +void TeamContainer::SendTeamSetLeader(const PlayerData& receiver, LWOOBJID i64PlayerID) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(receiver.playerID); + + //portion that will get routed: + CMSGHEADER; + + bitStream.Write(receiver.playerID); + bitStream.Write(MessageType::Game::TEAM_SET_LEADER); + + bitStream.Write(i64PlayerID); + + SystemAddress sysAddr = receiver.worldServerSysAddr; + SEND_PACKET; +} + +void TeamContainer::SendTeamAddPlayer(const PlayerData& receiver, bool bIsFreeTrial, bool bLocal, bool bNoLootOnDeath, LWOOBJID i64PlayerID, std::u16string wsPlayerName, LWOZONEID zoneID) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(receiver.playerID); + + //portion that will get routed: + CMSGHEADER; + + bitStream.Write(receiver.playerID); + bitStream.Write(MessageType::Game::TEAM_ADD_PLAYER); + + bitStream.Write(bIsFreeTrial); + bitStream.Write(bLocal); + bitStream.Write(bNoLootOnDeath); + bitStream.Write(i64PlayerID); + bitStream.Write(wsPlayerName.size()); + for (const auto character : wsPlayerName) { + bitStream.Write(character); + } + bitStream.Write1(); + if (receiver.zoneID.GetCloneID() == zoneID.GetCloneID()) { + zoneID = LWOZONEID(zoneID.GetMapID(), zoneID.GetInstanceID(), 0); + } + bitStream.Write(zoneID); + + SystemAddress sysAddr = receiver.worldServerSysAddr; + SEND_PACKET; +} + +void TeamContainer::SendTeamRemovePlayer(const PlayerData& receiver, bool bDisband, bool bIsKicked, bool bIsLeaving, bool bLocal, LWOOBJID i64LeaderID, LWOOBJID i64PlayerID, std::u16string wsPlayerName) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(receiver.playerID); + + //portion that will get routed: + CMSGHEADER; + + bitStream.Write(receiver.playerID); + bitStream.Write(MessageType::Game::TEAM_REMOVE_PLAYER); + + bitStream.Write(bDisband); + bitStream.Write(bIsKicked); + bitStream.Write(bIsLeaving); + bitStream.Write(bLocal); + bitStream.Write(i64LeaderID); + bitStream.Write(i64PlayerID); + bitStream.Write(wsPlayerName.size()); + for (const auto character : wsPlayerName) { + bitStream.Write(character); + } + + SystemAddress sysAddr = receiver.worldServerSysAddr; + SEND_PACKET; +} + +void TeamContainer::SendTeamSetOffWorldFlag(const PlayerData& receiver, LWOOBJID i64PlayerID, LWOZONEID zoneID) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET); + bitStream.Write(receiver.playerID); + + //portion that will get routed: + CMSGHEADER; + + bitStream.Write(receiver.playerID); + bitStream.Write(MessageType::Game::TEAM_SET_OFF_WORLD_FLAG); + + bitStream.Write(i64PlayerID); + if (receiver.zoneID.GetCloneID() == zoneID.GetCloneID()) { + zoneID = LWOZONEID(zoneID.GetMapID(), zoneID.GetInstanceID(), 0); + } + bitStream.Write(zoneID); + + SystemAddress sysAddr = receiver.worldServerSysAddr; + SEND_PACKET; +} + +void TeamContainer::CreateTeamServer(Packet* packet) { + CINSTREAM_SKIP_HEADER; + LWOOBJID playerID; + inStream.Read(playerID); + size_t membersSize = 0; + inStream.Read(membersSize); + + if (membersSize >= 4) { + LOG("Tried to create a team with more than 4 players"); + return; + } + + std::vector members; + + members.reserve(membersSize); + + for (size_t i = 0; i < membersSize; i++) { + LWOOBJID member; + inStream.Read(member); + members.push_back(member); + } + + LWOZONEID zoneId; + + inStream.Read(zoneId); + + auto* team = CreateLocalTeam(members); + + if (team != nullptr) { + team->zoneId = zoneId; + UpdateTeamsOnWorld(team, false); + } +} + +TeamData* TeamContainer::CreateLocalTeam(std::vector members) { + if (members.empty()) { + return nullptr; + } + + TeamData* newTeam = nullptr; + + for (const auto member : members) { + auto* team = GetTeam(member); + + if (team != nullptr) { + RemoveMember(team, member, false, false, true); + } + + if (newTeam == nullptr) { + newTeam = CreateTeam(member, true); + } else { + AddMember(newTeam, member); + } + } + + newTeam->lootFlag = 1; + + TeamStatusUpdate(newTeam); + + return newTeam; +} + +TeamData* TeamContainer::CreateTeam(LWOOBJID leader, bool local) { + auto* team = new TeamData(); + + team->teamID = ++g_TeamIDCounter; + team->leaderID = leader; + team->local = local; + + GetTeamsMut().push_back(team); + + AddMember(team, leader); + + return team; +} + +TeamData* TeamContainer::GetTeam(LWOOBJID playerID) { + for (auto* team : GetTeams()) { + if (std::find(team->memberIDs.begin(), team->memberIDs.end(), playerID) == team->memberIDs.end()) continue; + + return team; + } + + return nullptr; +} + +void TeamContainer::AddMember(TeamData* team, LWOOBJID playerID) { + if (team->memberIDs.size() >= 4) { + LOG("Tried to add player to team that already had 4 players"); + const auto& player = Game::playerContainer.GetPlayerData(playerID); + if (!player) return; + ChatPackets::SendSystemMessage(player.worldServerSysAddr, u"The teams is full! You have not been added to a team!"); + return; + } + + const auto index = std::find(team->memberIDs.begin(), team->memberIDs.end(), playerID); + + if (index != team->memberIDs.end()) return; + + team->memberIDs.push_back(playerID); + + const auto& leader = Game::playerContainer.GetPlayerData(team->leaderID); + const auto& member = Game::playerContainer.GetPlayerData(playerID); + + if (!leader || !member) return; + + const auto leaderName = GeneralUtils::UTF8ToUTF16(leader.playerName); + const auto memberName = GeneralUtils::UTF8ToUTF16(member.playerName); + + SendTeamInviteConfirm(member, false, leader.playerID, leader.zoneID, team->lootFlag, 0, 0, leaderName); + + if (!team->local) { + SendTeamSetLeader(member, leader.playerID); + } else { + SendTeamSetLeader(member, LWOOBJID_EMPTY); + } + + UpdateTeamsOnWorld(team, false); + + for (const auto memberId : team->memberIDs) { + const auto& otherMember = Game::playerContainer.GetPlayerData(memberId); + + if (otherMember == member) continue; + + const auto otherMemberName = Game::playerContainer.GetName(memberId); + + SendTeamAddPlayer(member, false, team->local, false, memberId, otherMemberName, otherMember ? otherMember.zoneID : LWOZONEID(0, 0, 0)); + + if (otherMember) { + SendTeamAddPlayer(otherMember, false, team->local, false, member.playerID, memberName, member.zoneID); + } + } +} + +void TeamContainer::RemoveMember(TeamData* team, LWOOBJID causingPlayerID, bool disband, bool kicked, bool leaving, bool silent) { + LOG_DEBUG("Player %llu is leaving team %i", causingPlayerID, team->teamID); + const auto index = std::ranges::find(team->memberIDs, causingPlayerID); + + if (index == team->memberIDs.end()) return; + + team->memberIDs.erase(index); + + const auto& member = Game::playerContainer.GetPlayerData(causingPlayerID); + + const auto causingMemberName = Game::playerContainer.GetName(causingPlayerID); + + if (member && !silent) { + SendTeamRemovePlayer(member, disband, kicked, leaving, team->local, LWOOBJID_EMPTY, causingPlayerID, causingMemberName); + } + + if (team->memberIDs.size() <= 1) { + DisbandTeam(team, causingPlayerID, causingMemberName); + } else /* team has enough members to be a team still */ { + team->leaderID = (causingPlayerID == team->leaderID) ? team->memberIDs[0] : team->leaderID; + for (const auto memberId : team->memberIDs) { + if (silent && memberId == causingPlayerID) { + continue; + } + + const auto& otherMember = Game::playerContainer.GetPlayerData(memberId); + + if (!otherMember) continue; + + SendTeamRemovePlayer(otherMember, disband, kicked, leaving, team->local, team->leaderID, causingPlayerID, causingMemberName); + } + + UpdateTeamsOnWorld(team, false); + } +} + +void TeamContainer::PromoteMember(TeamData* team, LWOOBJID newLeader) { + team->leaderID = newLeader; + + for (const auto memberId : team->memberIDs) { + const auto& otherMember = Game::playerContainer.GetPlayerData(memberId); + + if (!otherMember) continue; + + SendTeamSetLeader(otherMember, newLeader); + } +} + +void TeamContainer::DisbandTeam(TeamData* team, const LWOOBJID causingPlayerID, const std::u16string& causingPlayerName) { + const auto index = std::ranges::find(GetTeams(), team); + + if (index == GetTeams().end()) return; + LOG_DEBUG("Disbanding team %i", (*index)->teamID); + + for (const auto memberId : team->memberIDs) { + const auto& otherMember = Game::playerContainer.GetPlayerData(memberId); + + if (!otherMember) continue; + + SendTeamSetLeader(otherMember, LWOOBJID_EMPTY); + SendTeamRemovePlayer(otherMember, true, false, false, team->local, team->leaderID, causingPlayerID, causingPlayerName); + } + + UpdateTeamsOnWorld(team, true); + + GetTeamsMut().erase(index); + + delete team; +} + +void TeamContainer::TeamStatusUpdate(TeamData* team) { + const auto index = std::find(GetTeams().begin(), GetTeams().end(), team); + + if (index == GetTeams().end()) return; + + const auto& leader = Game::playerContainer.GetPlayerData(team->leaderID); + + if (!leader) return; + + const auto leaderName = GeneralUtils::UTF8ToUTF16(leader.playerName); + + for (const auto memberId : team->memberIDs) { + const auto& otherMember = Game::playerContainer.GetPlayerData(memberId); + + if (!otherMember) continue; + + if (!team->local) { + SendTeamStatus(otherMember, team->leaderID, leader.zoneID, team->lootFlag, 0, leaderName); + } + } + + UpdateTeamsOnWorld(team, false); +} + +void TeamContainer::UpdateTeamsOnWorld(TeamData* team, bool deleteTeam) { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::TEAM_GET_STATUS); + + bitStream.Write(team->teamID); + bitStream.Write(deleteTeam); + + if (!deleteTeam) { + bitStream.Write(team->lootFlag); + bitStream.Write(team->memberIDs.size()); + for (const auto memberID : team->memberIDs) { + bitStream.Write(memberID); + } + } + + Game::server->Send(bitStream, UNASSIGNED_SYSTEM_ADDRESS, true); +} diff --git a/dChatServer/TeamContainer.h b/dChatServer/TeamContainer.h new file mode 100644 index 00000000..a316cf71 --- /dev/null +++ b/dChatServer/TeamContainer.h @@ -0,0 +1,59 @@ +// Darkflame Universe +// Copyright 2025 + +#ifndef TEAMCONTAINER_H +#define TEAMCONTAINER_H + +#include +#include +#include + +#include "dCommonVars.h" + +struct Packet; +struct PlayerData; +struct TeamData; + +namespace TeamContainer { + struct Data { + std::vector mTeams; + }; + + void Shutdown(); + + void HandleTeamInvite(Packet* packet); + void HandleTeamInviteResponse(Packet* packet); + void HandleTeamLeave(Packet* packet); + void HandleTeamKick(Packet* packet); + void HandleTeamPromote(Packet* packet); + void HandleTeamLootOption(Packet* packet); + void HandleTeamStatusRequest(Packet* packet); + + void SendTeamInvite(const PlayerData& receiver, const PlayerData& sender); + void SendTeamInviteConfirm(const PlayerData& receiver, bool bLeaderIsFreeTrial, LWOOBJID i64LeaderID, LWOZONEID i64LeaderZoneID, uint8_t ucLootFlag, uint8_t ucNumOfOtherPlayers, uint8_t ucResponseCode, std::u16string wsLeaderName); + void SendTeamStatus(const PlayerData& receiver, LWOOBJID i64LeaderID, LWOZONEID i64LeaderZoneID, uint8_t ucLootFlag, uint8_t ucNumOfOtherPlayers, std::u16string wsLeaderName); + void SendTeamSetLeader(const PlayerData& receiver, LWOOBJID i64PlayerID); + void SendTeamAddPlayer(const PlayerData& receiver, bool bIsFreeTrial, bool bLocal, bool bNoLootOnDeath, LWOOBJID i64PlayerID, std::u16string wsPlayerName, LWOZONEID zoneID); + + /* Sends a message to the provided `receiver` with information about the updated team. If `i64LeaderID` is not LWOOBJID_EMPTY, the client will update the leader to that new playerID. */ + void SendTeamRemovePlayer(const PlayerData& receiver, bool bDisband, bool bIsKicked, bool bIsLeaving, bool bLocal, LWOOBJID i64LeaderID, LWOOBJID i64PlayerID, std::u16string wsPlayerName); + void SendTeamSetOffWorldFlag(const PlayerData& receiver, LWOOBJID i64PlayerID, LWOZONEID zoneID); + + void CreateTeamServer(Packet* packet); + + TeamData* CreateLocalTeam(std::vector members); + TeamData* CreateTeam(LWOOBJID leader, bool local = false); + TeamData* GetTeam(LWOOBJID playerID); + void AddMember(TeamData* team, LWOOBJID playerID); + void RemoveMember(TeamData* team, LWOOBJID playerID, bool disband, bool kicked, bool leaving, bool silent = false); + void PromoteMember(TeamData* team, LWOOBJID newLeader); + void DisbandTeam(TeamData* team, const LWOOBJID causingPlayerID, const std::u16string& causingPlayerName); + void TeamStatusUpdate(TeamData* team); + void UpdateTeamsOnWorld(TeamData* team, bool deleteTeam); + + const TeamContainer::Data& GetTeamContainer(); + std::vector& GetTeamsMut(); + const std::vector& GetTeams(); +}; + +#endif //!TEAMCONTAINER_H From 522299c9ec24c3ddd884964757f1a2302e0964ca Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Mon, 5 May 2025 07:05:12 -0700 Subject: [PATCH 4/6] feat: normalize brick model positions (#1761) * Add utilities for formats * Normalize model positions when placing in the world Have tested that placing a small and very large model both place and are located at the correct position. * add migration * Update Logger.cpp * add some notes and remove some logs * change arguments and add eof check Revert "fix: buff station cycling and dying too soon" This reverts commit 1c6cb2921e10eb2000ac40007d0c2636ba2ac151. fix: buff station cycling and dying too soon Tested that the buff station now only cycles after it has been built and has been alive for 25 seconds. --- dCommon/BrickByBrickFix.cpp | 7 +- dCommon/CMakeLists.txt | 3 + dCommon/Logger.h | 4 +- dCommon/Lxfml.cpp | 115 ++++++++++++++ dCommon/Lxfml.h | 23 +++ dCommon/Sd0.cpp | 150 ++++++++++++++++++ dCommon/Sd0.h | 42 +++++ dCommon/TinyXmlUtils.cpp | 37 +++++ dCommon/TinyXmlUtils.h | 66 ++++++++ dCommon/ZCompression.h | 6 - dCommon/dClient/Pack.cpp | 3 +- dDatabase/CMakeLists.txt | 2 +- .../GameDatabase/ITables/IPropertyContents.h | 12 +- dDatabase/GameDatabase/ITables/IUgc.h | 3 +- dDatabase/GameDatabase/MySQL/MySQLDatabase.h | 7 +- .../MySQL/Tables/PropertyContents.cpp | 29 +++- dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp | 14 +- .../GameDatabase/SQLite/SQLiteDatabase.h | 7 +- .../SQLite/Tables/PropertyContents.cpp | 31 +++- dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp | 14 +- .../GameDatabase/TestSQL/TestSQLDatabase.cpp | 6 +- .../GameDatabase/TestSQL/TestSQLDatabase.h | 7 +- dDatabase/MigrationRunner.cpp | 12 +- dDatabase/ModelNormalizeMigration.cpp | 30 ++++ dDatabase/ModelNormalizeMigration.h | 11 ++ dGame/EntityManager.cpp | 2 +- dGame/dGameMessages/GameMessages.cpp | 38 ++--- dWorldServer/WorldServer.cpp | 3 +- .../mysql/19_normalize_model_positions.sql | 1 + .../sqlite/2_normalize_model_positions.sql | 1 + 30 files changed, 620 insertions(+), 66 deletions(-) create mode 100644 dCommon/Lxfml.cpp create mode 100644 dCommon/Lxfml.h create mode 100644 dCommon/Sd0.cpp create mode 100644 dCommon/Sd0.h create mode 100644 dCommon/TinyXmlUtils.cpp create mode 100644 dCommon/TinyXmlUtils.h create mode 100644 dDatabase/ModelNormalizeMigration.cpp create mode 100644 dDatabase/ModelNormalizeMigration.h create mode 100644 migrations/dlu/mysql/19_normalize_model_positions.sql create mode 100644 migrations/dlu/sqlite/2_normalize_model_positions.sql diff --git a/dCommon/BrickByBrickFix.cpp b/dCommon/BrickByBrickFix.cpp index 5aef091a..b5a2df9b 100644 --- a/dCommon/BrickByBrickFix.cpp +++ b/dCommon/BrickByBrickFix.cpp @@ -8,6 +8,7 @@ #include "Database.h" #include "Game.h" +#include "Sd0.h" #include "ZCompression.h" #include "Logger.h" @@ -44,10 +45,10 @@ uint32_t BrickByBrickFix::TruncateBrokenBrickByBrickXml() { } // Ignore the valgrind warning about uninitialized values. These are discarded later when we know the actual uncompressed size. - std::unique_ptr uncompressedChunk(new uint8_t[ZCompression::MAX_SD0_CHUNK_SIZE]); + std::unique_ptr uncompressedChunk(new uint8_t[Sd0::MAX_UNCOMPRESSED_CHUNK_SIZE]); int32_t err{}; int32_t actualUncompressedSize = ZCompression::Decompress( - compressedChunk.get(), chunkSize, uncompressedChunk.get(), ZCompression::MAX_SD0_CHUNK_SIZE, err); + compressedChunk.get(), chunkSize, uncompressedChunk.get(), Sd0::MAX_UNCOMPRESSED_CHUNK_SIZE, err); if (actualUncompressedSize != -1) { uint32_t previousSize = completeUncompressedModel.size(); @@ -117,7 +118,7 @@ uint32_t BrickByBrickFix::UpdateBrickByBrickModelsToSd0() { } std::string outputString(sd0ConvertedModel.get(), oldLxfmlSizeWithHeader); - std::istringstream outputStringStream(outputString); + std::stringstream outputStringStream(outputString); try { Database::Get()->UpdateUgcModelData(model.id, outputStringStream); diff --git a/dCommon/CMakeLists.txt b/dCommon/CMakeLists.txt index 18fda0ed..74432e0f 100644 --- a/dCommon/CMakeLists.txt +++ b/dCommon/CMakeLists.txt @@ -16,6 +16,9 @@ set(DCOMMON_SOURCES "BrickByBrickFix.cpp" "BinaryPathFinder.cpp" "FdbToSqlite.cpp" + "TinyXmlUtils.cpp" + "Sd0.cpp" + "Lxfml.cpp" ) # Workaround for compiler bug where the optimized code could result in a memcpy of 0 bytes, even though that isnt possible. diff --git a/dCommon/Logger.h b/dCommon/Logger.h index 5754d9ac..3a1771e6 100644 --- a/dCommon/Logger.h +++ b/dCommon/Logger.h @@ -29,8 +29,8 @@ constexpr const char* GetFileNameFromAbsolutePath(const char* path) { // they will not be valid constexpr and will be evaluated at runtime instead of compile time! // The full string is still stored in the binary, however the offset of the filename in the absolute paths // is used in the instruction instead of the start of the absolute path. -#define LOG(message, ...) do { auto str = FILENAME_AND_LINE; Game::logger->Log(str, message, ##__VA_ARGS__); } while(0) -#define LOG_DEBUG(message, ...) do { auto str = FILENAME_AND_LINE; Game::logger->LogDebug(str, message, ##__VA_ARGS__); } while(0) +#define LOG(message, ...) do { auto str_ = FILENAME_AND_LINE; Game::logger->Log(str_, message, ##__VA_ARGS__); } while(0) +#define LOG_DEBUG(message, ...) do { auto str_ = FILENAME_AND_LINE; Game::logger->LogDebug(str_, message, ##__VA_ARGS__); } while(0) // Writer class for writing data to files. class Writer { diff --git a/dCommon/Lxfml.cpp b/dCommon/Lxfml.cpp new file mode 100644 index 00000000..f713bcab --- /dev/null +++ b/dCommon/Lxfml.cpp @@ -0,0 +1,115 @@ +#include "Lxfml.h" + +#include "GeneralUtils.h" +#include "StringifiedEnum.h" +#include "TinyXmlUtils.h" + +#include + +Lxfml::Result Lxfml::NormalizePosition(const std::string_view data) { + Result toReturn; + tinyxml2::XMLDocument doc; + const auto err = doc.Parse(data.data()); + if (err != tinyxml2::XML_SUCCESS) { + LOG("Failed to parse xml %s.", StringifiedEnum::ToString(err).data()); + return toReturn; + } + + TinyXmlUtils::DocumentReader reader(doc); + std::map transformations; + + auto lxfml = reader["LXFML"]; + if (!lxfml) { + LOG("Failed to find LXFML element."); + return toReturn; + } + + // First get all the positions of bricks + for (const auto& brick : lxfml["Bricks"]) { + const auto* part = brick.FirstChildElement("Part"); + if (part) { + const auto* bone = part->FirstChildElement("Bone"); + if (bone) { + auto* transformation = bone->Attribute("transformation"); + if (transformation) { + auto* refID = bone->Attribute("refID"); + if (refID) transformations[refID] = transformation; + } + } + } + } + + // These points are well out of bounds for an actual player + NiPoint3 lowest{ 10'000.0f, 10'000.0f, 10'000.0f }; + NiPoint3 highest{ -10'000.0f, -10'000.0f, -10'000.0f }; + + // Calculate the lowest and highest points on the entire model + for (const auto& transformation : transformations | std::views::values) { + auto split = GeneralUtils::SplitString(transformation, ','); + if (split.size() < 12) { + LOG("Not enough in the split?"); + continue; + } + + auto x = GeneralUtils::TryParse(split[9]).value(); + auto y = GeneralUtils::TryParse(split[10]).value(); + auto z = GeneralUtils::TryParse(split[11]).value(); + if (x < lowest.x) lowest.x = x; + if (y < lowest.y) lowest.y = y; + if (z < lowest.z) lowest.z = z; + + if (highest.x < x) highest.x = x; + if (highest.y < y) highest.y = y; + if (highest.z < z) highest.z = z; + } + + auto delta = (highest - lowest) / 2.0f; + auto newRootPos = lowest + delta; + + // Clamp the Y to the lowest point on the model + newRootPos.y = lowest.y; + + // Adjust all positions to account for the new origin + for (auto& transformation : transformations | std::views::values) { + auto split = GeneralUtils::SplitString(transformation, ','); + if (split.size() < 12) { + LOG("Not enough in the split?"); + continue; + } + + auto x = GeneralUtils::TryParse(split[9]).value() - newRootPos.x; + auto y = GeneralUtils::TryParse(split[10]).value() - newRootPos.y; + auto z = GeneralUtils::TryParse(split[11]).value() - newRootPos.z; + std::stringstream stream; + for (int i = 0; i < 9; i++) { + stream << split[i]; + stream << ','; + } + stream << x << ',' << y << ',' << z; + transformation = stream.str(); + } + + // Finally write the new transformation back into the lxfml + for (auto& brick : lxfml["Bricks"]) { + auto* part = brick.FirstChildElement("Part"); + if (part) { + auto* bone = part->FirstChildElement("Bone"); + if (bone) { + auto* transformation = bone->Attribute("transformation"); + if (transformation) { + auto* refID = bone->Attribute("refID"); + if (refID) { + bone->SetAttribute("transformation", transformations[refID].c_str()); + } + } + } + } + } + + tinyxml2::XMLPrinter printer; + doc.Print(&printer); + + toReturn.lxfml = printer.CStr(); + toReturn.center = newRootPos; + return toReturn; +} diff --git a/dCommon/Lxfml.h b/dCommon/Lxfml.h new file mode 100644 index 00000000..3f5f4d4a --- /dev/null +++ b/dCommon/Lxfml.h @@ -0,0 +1,23 @@ +// Darkflame Universe +// Copyright 2025 + +#ifndef LXFML_H +#define LXFML_H + +#include +#include + +#include "NiPoint3.h" + +namespace Lxfml { + struct Result { + std::string lxfml; + NiPoint3 center; + }; + + // Normalizes a LXFML model to be positioned relative to its local 0, 0, 0 rather than a game worlds 0, 0, 0. + // Returns a struct of its new center and the updated LXFML containing these edits. + [[nodiscard]] Result NormalizePosition(const std::string_view data); +}; + +#endif //!LXFML_H diff --git a/dCommon/Sd0.cpp b/dCommon/Sd0.cpp new file mode 100644 index 00000000..5b97cb75 --- /dev/null +++ b/dCommon/Sd0.cpp @@ -0,0 +1,150 @@ +#include "Sd0.h" + +#include +#include + +#include "BinaryIO.h" + +#include "Game.h" +#include "Logger.h" + +#include "ZCompression.h" + +// Insert header if on first buffer +void WriteHeader(Sd0::BinaryBuffer& chunk) { + chunk.push_back(Sd0::SD0_HEADER[0]); + chunk.push_back(Sd0::SD0_HEADER[1]); + chunk.push_back(Sd0::SD0_HEADER[2]); + chunk.push_back(Sd0::SD0_HEADER[3]); + chunk.push_back(Sd0::SD0_HEADER[4]); +} + +// Write the size of the buffer to a chunk +void WriteSize(Sd0::BinaryBuffer& chunk, uint32_t chunkSize) { + for (int i = 0; i < 4; i++) { + char toPush = chunkSize & 0xff; + chunkSize = chunkSize >> 8; + chunk.push_back(toPush); + } +} + +int32_t GetDataOffset(bool firstBuffer) { + return firstBuffer ? 9 : 4; +} + +Sd0::Sd0(std::istream& buffer) { + char header[5]{}; + + // Check if this is an sd0 buffer. It's possible we may be handed a zlib buffer directly due to old code so check for that too. + if (!BinaryIO::BinaryRead(buffer, header) || memcmp(header, SD0_HEADER, sizeof(header)) != 0) { + LOG("Failed to read SD0 header %i %i %i %i %i %i %i", buffer.good(), buffer.tellg(), header[0], header[1], header[2], header[3], header[4]); + LOG_DEBUG("This may be a zlib buffer directly? Trying again assuming its a zlib buffer."); + auto& firstChunk = m_Chunks.emplace_back(); + WriteHeader(firstChunk); + buffer.seekg(0, std::ios::end); + uint32_t bufferSize = buffer.tellg(); + buffer.seekg(0, std::ios::beg); + WriteSize(firstChunk, bufferSize); + firstChunk.resize(firstChunk.size() + bufferSize); + auto* dataStart = reinterpret_cast(firstChunk.data() + GetDataOffset(true)); + if (!buffer.read(dataStart, bufferSize)) { + m_Chunks.pop_back(); + LOG("Failed to read %u bytes from chunk %i", bufferSize, m_Chunks.size() - 1); + } + return; + } + + while (buffer && buffer.peek() != std::istream::traits_type::eof()) { + uint32_t chunkSize{}; + if (!BinaryIO::BinaryRead(buffer, chunkSize)) { + LOG("Failed to read chunk size from stream %lld %zu", buffer.tellg(), m_Chunks.size()); + break; + } + auto& chunk = m_Chunks.emplace_back(); + bool firstBuffer = m_Chunks.size() == 1; + auto dataOffset = GetDataOffset(firstBuffer); + + // Insert header if on first buffer + if (firstBuffer) { + WriteHeader(chunk); + } + + WriteSize(chunk, chunkSize); + + chunk.resize(chunkSize + dataOffset); + auto* dataStart = reinterpret_cast(chunk.data() + dataOffset); + if (!buffer.read(dataStart, chunkSize)) { + m_Chunks.pop_back(); + LOG("Failed to read %u bytes from chunk %i", chunkSize, m_Chunks.size() - 1); + break; + } + } +} + +void Sd0::FromData(const uint8_t* data, size_t bufferSize) { + const auto originalBufferSize = bufferSize; + if (bufferSize == 0) return; + + m_Chunks.clear(); + while (bufferSize > 0) { + const auto numToCopy = std::min(MAX_UNCOMPRESSED_CHUNK_SIZE, bufferSize); + const auto* startOffset = data + originalBufferSize - bufferSize; + bufferSize -= numToCopy; + std::array compressedChunk; + const auto compressedSize = ZCompression::Compress( + startOffset, numToCopy, + compressedChunk.data(), compressedChunk.size()); + + auto& chunk = m_Chunks.emplace_back(); + bool firstBuffer = m_Chunks.size() == 1; + auto dataOffset = GetDataOffset(firstBuffer); + + if (firstBuffer) { + WriteHeader(chunk); + } + + WriteSize(chunk, compressedSize); + + chunk.resize(compressedSize + dataOffset); + memcpy(chunk.data() + dataOffset, compressedChunk.data(), compressedSize); + } + +} + +std::string Sd0::GetAsStringUncompressed() const { + std::string toReturn; + bool first = true; + uint32_t totalSize{}; + for (const auto& chunk : m_Chunks) { + auto dataOffset = GetDataOffset(first); + first = false; + const auto chunkSize = chunk.size(); + + auto oldSize = toReturn.size(); + toReturn.resize(oldSize + MAX_UNCOMPRESSED_CHUNK_SIZE); + int32_t error{}; + const auto uncompressedSize = ZCompression::Decompress( + chunk.data() + dataOffset, chunkSize - dataOffset, + reinterpret_cast(toReturn.data()) + oldSize, MAX_UNCOMPRESSED_CHUNK_SIZE, + error); + + totalSize += uncompressedSize; + } + + toReturn.resize(totalSize); + return toReturn; +} + +std::stringstream Sd0::GetAsStream() const { + std::stringstream toReturn; + + for (const auto& chunk : m_Chunks) { + toReturn.write(reinterpret_cast(chunk.data()), chunk.size()); + } + + return toReturn; +} + +const std::vector& Sd0::GetAsVector() const { + return m_Chunks; +} diff --git a/dCommon/Sd0.h b/dCommon/Sd0.h new file mode 100644 index 00000000..40bf9930 --- /dev/null +++ b/dCommon/Sd0.h @@ -0,0 +1,42 @@ +// Darkflame Universe +// Copyright 2025 + +#ifndef SD0_H +#define SD0_H + +#include +#include + +// Sd0 is comprised of multiple zlib compressed buffers stored in a row. +// The format starts with a SD0 header (see SD0_HEADER) followed by the size of a zlib buffer, and then the zlib buffer itself. +// This repeats until end of file +class Sd0 { +public: + using BinaryBuffer = std::vector; + + static inline const char* SD0_HEADER = "sd0\x01\xff"; + + /** + * @brief Max size of an inflated sd0 zlib chunk + */ + static constexpr inline size_t MAX_UNCOMPRESSED_CHUNK_SIZE = 1024 * 256; + + // Read the input buffer into an internal chunk stream to be used later + Sd0(std::istream& buffer); + + // Uncompresses the entire Sd0 buffer and returns it as a string + [[nodiscard]] std::string GetAsStringUncompressed() const; + + // Gets the Sd0 buffer as a stream in its raw compressed form + [[nodiscard]] std::stringstream GetAsStream() const; + + // Gets the Sd0 buffer as a vector in its raw compressed form + [[nodiscard]] const std::vector& GetAsVector() const; + + // Compress data into a Sd0 buffer + void FromData(const uint8_t* data, size_t bufferSize); +private: + std::vector m_Chunks{}; +}; + +#endif //!SD0_H diff --git a/dCommon/TinyXmlUtils.cpp b/dCommon/TinyXmlUtils.cpp new file mode 100644 index 00000000..9fe88eb7 --- /dev/null +++ b/dCommon/TinyXmlUtils.cpp @@ -0,0 +1,37 @@ +#include "TinyXmlUtils.h" + +#include + +using namespace TinyXmlUtils; + +Element DocumentReader::operator[](const std::string_view elem) const { + return Element(m_Doc.FirstChildElement(elem.empty() ? nullptr : elem.data()), elem); +} + +Element::Element(tinyxml2::XMLElement* xmlElem, const std::string_view elem) : + m_IteratedName{ elem }, + m_Elem{ xmlElem } { +} + +Element Element::operator[](const std::string_view elem) const { + const auto* usedElem = elem.empty() ? nullptr : elem.data(); + auto* toReturn = m_Elem ? m_Elem->FirstChildElement(usedElem) : nullptr; + return Element(toReturn, m_IteratedName); +} + +ElementIterator Element::begin() { + return ElementIterator(m_Elem ? m_Elem->FirstChildElement() : nullptr); +} + +ElementIterator Element::end() { + return ElementIterator(nullptr); +} + +ElementIterator::ElementIterator(tinyxml2::XMLElement* elem) : + m_CurElem{ elem } { +} + +ElementIterator& ElementIterator::operator++() { + if (m_CurElem) m_CurElem = m_CurElem->NextSiblingElement(); + return *this; +} diff --git a/dCommon/TinyXmlUtils.h b/dCommon/TinyXmlUtils.h new file mode 100644 index 00000000..e7740f04 --- /dev/null +++ b/dCommon/TinyXmlUtils.h @@ -0,0 +1,66 @@ +// Darkflame Universe +// Copyright 2025 + +#ifndef TINYXMLUTILS_H +#define TINYXMLUTILS_H + +#include + +#include "DluAssert.h" + +#include + +namespace TinyXmlUtils { + // See cstdlib for iterator technicalities + struct ElementIterator { + ElementIterator(tinyxml2::XMLElement* elem); + + ElementIterator& operator++(); + [[nodiscard]] tinyxml2::XMLElement* operator->() { DluAssert(m_CurElem); return m_CurElem; } + [[nodiscard]] tinyxml2::XMLElement& operator*() { DluAssert(m_CurElem); return *m_CurElem; } + + bool operator==(const ElementIterator& other) const { return other.m_CurElem == m_CurElem; } + + private: + tinyxml2::XMLElement* m_CurElem{ nullptr }; + }; + + // Wrapper class to act as an iterator over xml elements. + // All the normal rules that apply to Iterators in the std library apply here. + class Element { + public: + Element(tinyxml2::XMLElement* xmlElem, const std::string_view elem); + + // The first child element of this element. + [[nodiscard]] ElementIterator begin(); + + // Always returns an ElementIterator which points to nullptr. + // TinyXml2 return NULL when you've reached the last child element so + // you can't do any funny one past end logic here. + [[nodiscard]] ElementIterator end(); + + // Get a child element + [[nodiscard]] Element operator[](const std::string_view elem) const; + [[nodiscard]] Element operator[](const char* elem) const { return operator[](std::string_view(elem)); }; + + // Whether or not data exists for this element + operator bool() const { return m_Elem != nullptr; } + + [[nodiscard]] const tinyxml2::XMLElement* operator->() const { return m_Elem; } + private: + const char* GetElementName() const { return m_IteratedName.empty() ? nullptr : m_IteratedName.c_str(); } + const std::string m_IteratedName; + tinyxml2::XMLElement* m_Elem; + }; + + class DocumentReader { + public: + DocumentReader(tinyxml2::XMLDocument& doc) : m_Doc{ doc } {} + + [[nodiscard]] Element operator[](const std::string_view elem) const; + private: + tinyxml2::XMLDocument& m_Doc; + }; +}; + +#endif //!TINYXMLUTILS_H diff --git a/dCommon/ZCompression.h b/dCommon/ZCompression.h index 84e8a9b4..22a5ff86 100644 --- a/dCommon/ZCompression.h +++ b/dCommon/ZCompression.h @@ -8,11 +8,5 @@ namespace ZCompression { int32_t Compress(const uint8_t* abSrc, int32_t nLenSrc, uint8_t* abDst, int32_t nLenDst); int32_t Decompress(const uint8_t* abSrc, int32_t nLenSrc, uint8_t* abDst, int32_t nLenDst, int32_t& nErr); - - /** - * @brief Max size of an inflated sd0 zlib chunk - * - */ - constexpr uint32_t MAX_SD0_CHUNK_SIZE = 1024 * 256; } diff --git a/dCommon/dClient/Pack.cpp b/dCommon/dClient/Pack.cpp index 04d07f12..800cfa19 100644 --- a/dCommon/dClient/Pack.cpp +++ b/dCommon/dClient/Pack.cpp @@ -1,6 +1,7 @@ #include "Pack.h" #include "BinaryIO.h" +#include "Sd0.h" #include "ZCompression.h" Pack::Pack(const std::filesystem::path& filePath) { @@ -106,7 +107,7 @@ bool Pack::ReadFileFromPack(const uint32_t crc, char** data, uint32_t* len) cons pos += size; // Move pointer position the amount of bytes read to the right int32_t err; - currentReadPos += ZCompression::Decompress(reinterpret_cast(chunk), size, reinterpret_cast(decompressedData + currentReadPos), ZCompression::MAX_SD0_CHUNK_SIZE, err); + currentReadPos += ZCompression::Decompress(reinterpret_cast(chunk), size, reinterpret_cast(decompressedData + currentReadPos), Sd0::MAX_UNCOMPRESSED_CHUNK_SIZE, err); free(chunk); } diff --git a/dDatabase/CMakeLists.txt b/dDatabase/CMakeLists.txt index 42bdb983..56ed4df7 100644 --- a/dDatabase/CMakeLists.txt +++ b/dDatabase/CMakeLists.txt @@ -1,7 +1,7 @@ add_subdirectory(CDClientDatabase) add_subdirectory(GameDatabase) -add_library(dDatabase STATIC "MigrationRunner.cpp") +add_library(dDatabase STATIC "MigrationRunner.cpp" "ModelNormalizeMigration.cpp") add_custom_target(conncpp_dylib ${CMAKE_COMMAND} -E copy $ ${PROJECT_BINARY_DIR}) diff --git a/dDatabase/GameDatabase/ITables/IPropertyContents.h b/dDatabase/GameDatabase/ITables/IPropertyContents.h index 0d8d8b5c..e965e05c 100644 --- a/dDatabase/GameDatabase/ITables/IPropertyContents.h +++ b/dDatabase/GameDatabase/ITables/IPropertyContents.h @@ -22,7 +22,7 @@ public: // Inserts a new UGC model into the database. virtual void InsertNewUgcModel( - std::istringstream& sd0Data, + std::stringstream& sd0Data, const uint32_t blueprintId, const uint32_t accountId, const uint32_t characterId) = 0; @@ -34,9 +34,17 @@ public: virtual void InsertNewPropertyModel(const LWOOBJID& propertyId, const IPropertyContents::Model& model, const std::string_view name) = 0; // Update the model position and rotation for the given property id. - virtual void UpdateModel(const LWOOBJID& propertyId, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) = 0; + virtual void UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) = 0; + virtual void UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array behaviorIDs) { + std::array, 5> behaviors; + for (int32_t i = 0; i < behaviors.size(); i++) behaviors[i].first = behaviorIDs[i]; + UpdateModel(modelID, position, rotation, behaviors); + } // Remove the model for the given property id. virtual void RemoveModel(const LWOOBJID& modelId) = 0; + + // Gets a model by ID + virtual Model GetModel(const LWOOBJID modelID) = 0; }; #endif //!__IPROPERTIESCONTENTS__H__ diff --git a/dDatabase/GameDatabase/ITables/IUgc.h b/dDatabase/GameDatabase/ITables/IUgc.h index 024636ac..cbc770b8 100644 --- a/dDatabase/GameDatabase/ITables/IUgc.h +++ b/dDatabase/GameDatabase/ITables/IUgc.h @@ -12,6 +12,7 @@ public: struct Model { std::stringstream lxfmlData; LWOOBJID id{}; + LWOOBJID modelID{}; }; // Gets all UGC models for the given property id. @@ -27,6 +28,6 @@ public: virtual void DeleteUgcModelData(const LWOOBJID& modelId) = 0; // Inserts a new UGC model into the database. - virtual void UpdateUgcModelData(const LWOOBJID& modelId, std::istringstream& lxfml) = 0; + virtual void UpdateUgcModelData(const LWOOBJID& modelId, std::stringstream& lxfml) = 0; }; #endif //!__IUGC__H__ diff --git a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h index e998f488..9b0b38a6 100644 --- a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h +++ b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h @@ -48,7 +48,7 @@ public: void RemoveFriend(const uint32_t playerAccountId, const uint32_t friendAccountId) override; void UpdateActivityLog(const uint32_t characterId, const eActivityType activityType, const LWOMAPID mapId) override; void DeleteUgcModelData(const LWOOBJID& modelId) override; - void UpdateUgcModelData(const LWOOBJID& modelId, std::istringstream& lxfml) override; + void UpdateUgcModelData(const LWOOBJID& modelId, std::stringstream& lxfml) override; std::vector GetAllUgcModels() override; void CreateMigrationHistoryTable() override; bool IsMigrationRun(const std::string_view str) override; @@ -75,14 +75,14 @@ public: std::vector GetPropertyModels(const LWOOBJID& propertyId) override; void RemoveUnreferencedUgcModels() override; void InsertNewPropertyModel(const LWOOBJID& propertyId, const IPropertyContents::Model& model, const std::string_view name) override; - void UpdateModel(const LWOOBJID& propertyId, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) override; + void UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) override; void RemoveModel(const LWOOBJID& modelId) override; void UpdatePerformanceCost(const LWOZONEID& zoneId, const float performanceCost) override; void InsertNewBugReport(const IBugReports::Info& info) override; void InsertCheatDetection(const IPlayerCheatDetections::Info& info) override; void InsertNewMail(const MailInfo& mail) override; void InsertNewUgcModel( - std::istringstream& sd0Data, + std::stringstream& sd0Data, const uint32_t blueprintId, const uint32_t accountId, const uint32_t characterId) override; @@ -127,6 +127,7 @@ public: void DeleteUgcBuild(const LWOOBJID bigId) override; uint32_t GetAccountCount() override; bool IsNameInUse(const std::string_view name) override; + IPropertyContents::Model GetModel(const LWOOBJID modelID) override; sql::PreparedStatement* CreatePreppedStmt(const std::string& query); private: diff --git a/dDatabase/GameDatabase/MySQL/Tables/PropertyContents.cpp b/dDatabase/GameDatabase/MySQL/Tables/PropertyContents.cpp index 05998785..fe63fc49 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/PropertyContents.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/PropertyContents.cpp @@ -52,14 +52,39 @@ void MySQLDatabase::InsertNewPropertyModel(const LWOOBJID& propertyId, const IPr } } -void MySQLDatabase::UpdateModel(const LWOOBJID& propertyId, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) { +void MySQLDatabase::UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) { ExecuteUpdate( "UPDATE properties_contents SET x = ?, y = ?, z = ?, rx = ?, ry = ?, rz = ?, rw = ?, " "behavior_1 = ?, behavior_2 = ?, behavior_3 = ?, behavior_4 = ?, behavior_5 = ? WHERE id = ?;", position.x, position.y, position.z, rotation.x, rotation.y, rotation.z, rotation.w, - behaviors[0].first, behaviors[1].first, behaviors[2].first, behaviors[3].first, behaviors[4].first, propertyId); + behaviors[0].first, behaviors[1].first, behaviors[2].first, behaviors[3].first, behaviors[4].first, modelID); } void MySQLDatabase::RemoveModel(const LWOOBJID& modelId) { ExecuteDelete("DELETE FROM properties_contents WHERE id = ?;", modelId); } + +IPropertyContents::Model MySQLDatabase::GetModel(const LWOOBJID modelID) { + auto result = ExecuteSelect("SELECT * FROM properties_contents WHERE id = ?", modelID); + + IPropertyContents::Model model{}; + while (result->next()) { + model.id = result->getUInt64("id"); + model.lot = static_cast(result->getUInt("lot")); + model.position.x = result->getFloat("x"); + model.position.y = result->getFloat("y"); + model.position.z = result->getFloat("z"); + model.rotation.w = result->getFloat("rw"); + model.rotation.x = result->getFloat("rx"); + model.rotation.y = result->getFloat("ry"); + model.rotation.z = result->getFloat("rz"); + model.ugcId = result->getUInt64("ugc_id"); + model.behaviors[0] = result->getInt("behavior_1"); + model.behaviors[1] = result->getInt("behavior_2"); + model.behaviors[2] = result->getInt("behavior_3"); + model.behaviors[3] = result->getInt("behavior_4"); + model.behaviors[4] = result->getInt("behavior_5"); + } + + return model; +} diff --git a/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp b/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp index 3b62a51b..2d2655f4 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/Ugc.cpp @@ -2,7 +2,7 @@ std::vector MySQLDatabase::GetUgcModels(const LWOOBJID& propertyId) { auto result = ExecuteSelect( - "SELECT lxfml, u.id FROM ugc AS u JOIN properties_contents AS pc ON u.id = pc.ugc_id WHERE lot = 14 AND property_id = ? AND pc.ugc_id IS NOT NULL;", + "SELECT lxfml, u.id as ugcID, pc.id as modelID FROM ugc AS u JOIN properties_contents AS pc ON u.id = pc.ugc_id WHERE lot = 14 AND property_id = ? AND pc.ugc_id IS NOT NULL;", propertyId); std::vector toReturn; @@ -13,7 +13,8 @@ std::vector MySQLDatabase::GetUgcModels(const LWOOBJID& propertyId) // blob is owned by the query, so we need to do a deep copy :/ std::unique_ptr blob(result->getBlob("lxfml")); model.lxfmlData << blob->rdbuf(); - model.id = result->getUInt64("id"); + model.id = result->getUInt64("ugcID"); + model.modelID = result->getUInt64("modelID"); toReturn.push_back(std::move(model)); } @@ -21,13 +22,14 @@ std::vector MySQLDatabase::GetUgcModels(const LWOOBJID& propertyId) } std::vector MySQLDatabase::GetAllUgcModels() { - auto result = ExecuteSelect("SELECT id, lxfml FROM ugc;"); + auto result = ExecuteSelect("SELECT u.id AS ugcID, lxfml, pc.id AS modelID FROM ugc AS u JOIN properties_contents AS pc ON pc.ugc_id = u.id WHERE pc.lot = 14 AND pc.ugc_id IS NOT NULL;"); std::vector models; models.reserve(result->rowsCount()); while (result->next()) { IUgc::Model model; - model.id = result->getInt64("id"); + model.id = result->getInt64("ugcID"); + model.modelID = result->getUInt64("modelID"); // blob is owned by the query, so we need to do a deep copy :/ std::unique_ptr blob(result->getBlob("lxfml")); @@ -43,7 +45,7 @@ void MySQLDatabase::RemoveUnreferencedUgcModels() { } void MySQLDatabase::InsertNewUgcModel( - std::istringstream& sd0Data, // cant be const sad + std:: stringstream& sd0Data, // cant be const sad const uint32_t blueprintId, const uint32_t accountId, const uint32_t characterId) { @@ -65,7 +67,7 @@ void MySQLDatabase::DeleteUgcModelData(const LWOOBJID& modelId) { ExecuteDelete("DELETE FROM properties_contents WHERE ugc_id = ?;", modelId); } -void MySQLDatabase::UpdateUgcModelData(const LWOOBJID& modelId, std::istringstream& lxfml) { +void MySQLDatabase::UpdateUgcModelData(const LWOOBJID& modelId, std::stringstream& lxfml) { const std::istream stream(lxfml.rdbuf()); ExecuteUpdate("UPDATE ugc SET lxfml = ? WHERE id = ?;", &stream, modelId); } diff --git a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h index 15e0176f..f6c545af 100644 --- a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h +++ b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h @@ -46,7 +46,7 @@ public: void RemoveFriend(const uint32_t playerAccountId, const uint32_t friendAccountId) override; void UpdateActivityLog(const uint32_t characterId, const eActivityType activityType, const LWOMAPID mapId) override; void DeleteUgcModelData(const LWOOBJID& modelId) override; - void UpdateUgcModelData(const LWOOBJID& modelId, std::istringstream& lxfml) override; + void UpdateUgcModelData(const LWOOBJID& modelId, std::stringstream& lxfml) override; std::vector GetAllUgcModels() override; void CreateMigrationHistoryTable() override; bool IsMigrationRun(const std::string_view str) override; @@ -73,14 +73,14 @@ public: std::vector GetPropertyModels(const LWOOBJID& propertyId) override; void RemoveUnreferencedUgcModels() override; void InsertNewPropertyModel(const LWOOBJID& propertyId, const IPropertyContents::Model& model, const std::string_view name) override; - void UpdateModel(const LWOOBJID& propertyId, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) override; + void UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) override; void RemoveModel(const LWOOBJID& modelId) override; void UpdatePerformanceCost(const LWOZONEID& zoneId, const float performanceCost) override; void InsertNewBugReport(const IBugReports::Info& info) override; void InsertCheatDetection(const IPlayerCheatDetections::Info& info) override; void InsertNewMail(const MailInfo& mail) override; void InsertNewUgcModel( - std::istringstream& sd0Data, + std::stringstream& sd0Data, const uint32_t blueprintId, const uint32_t accountId, const uint32_t characterId) override; @@ -125,6 +125,7 @@ public: void DeleteUgcBuild(const LWOOBJID bigId) override; uint32_t GetAccountCount() override; bool IsNameInUse(const std::string_view name) override; + IPropertyContents::Model GetModel(const LWOOBJID modelID) override; private: CppSQLite3Statement CreatePreppedStmt(const std::string& query); diff --git a/dDatabase/GameDatabase/SQLite/Tables/PropertyContents.cpp b/dDatabase/GameDatabase/SQLite/Tables/PropertyContents.cpp index 6a8d7028..960e1113 100644 --- a/dDatabase/GameDatabase/SQLite/Tables/PropertyContents.cpp +++ b/dDatabase/GameDatabase/SQLite/Tables/PropertyContents.cpp @@ -52,14 +52,41 @@ void SQLiteDatabase::InsertNewPropertyModel(const LWOOBJID& propertyId, const IP } } -void SQLiteDatabase::UpdateModel(const LWOOBJID& propertyId, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) { +void SQLiteDatabase::UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) { ExecuteUpdate( "UPDATE properties_contents SET x = ?, y = ?, z = ?, rx = ?, ry = ?, rz = ?, rw = ?, " "behavior_1 = ?, behavior_2 = ?, behavior_3 = ?, behavior_4 = ?, behavior_5 = ? WHERE id = ?;", position.x, position.y, position.z, rotation.x, rotation.y, rotation.z, rotation.w, - behaviors[0].first, behaviors[1].first, behaviors[2].first, behaviors[3].first, behaviors[4].first, propertyId); + behaviors[0].first, behaviors[1].first, behaviors[2].first, behaviors[3].first, behaviors[4].first, modelID); } void SQLiteDatabase::RemoveModel(const LWOOBJID& modelId) { ExecuteDelete("DELETE FROM properties_contents WHERE id = ?;", modelId); } + +IPropertyContents::Model SQLiteDatabase::GetModel(const LWOOBJID modelID) { + auto [_, result] = ExecuteSelect("SELECT * FROM properties_contents WHERE id = ?", modelID); + + IPropertyContents::Model model{}; + if (!result.eof()) { + do { + model.id = result.getInt64Field("id"); + model.lot = static_cast(result.getIntField("lot")); + model.position.x = result.getFloatField("x"); + model.position.y = result.getFloatField("y"); + model.position.z = result.getFloatField("z"); + model.rotation.w = result.getFloatField("rw"); + model.rotation.x = result.getFloatField("rx"); + model.rotation.y = result.getFloatField("ry"); + model.rotation.z = result.getFloatField("rz"); + model.ugcId = result.getInt64Field("ugc_id"); + model.behaviors[0] = result.getIntField("behavior_1"); + model.behaviors[1] = result.getIntField("behavior_2"); + model.behaviors[2] = result.getIntField("behavior_3"); + model.behaviors[3] = result.getIntField("behavior_4"); + model.behaviors[4] = result.getIntField("behavior_5"); + } while (result.nextRow()); + } + + return model; +} diff --git a/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp b/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp index 048b53ab..c6410a42 100644 --- a/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp +++ b/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp @@ -2,7 +2,7 @@ std::vector SQLiteDatabase::GetUgcModels(const LWOOBJID& propertyId) { auto [_, result] = ExecuteSelect( - "SELECT lxfml, u.id FROM ugc AS u JOIN properties_contents AS pc ON u.id = pc.ugc_id WHERE lot = 14 AND property_id = ? AND pc.ugc_id IS NOT NULL;", + "SELECT lxfml, u.id AS ugcID, pc.id AS modelID FROM ugc AS u JOIN properties_contents AS pc ON u.id = pc.ugc_id WHERE lot = 14 AND property_id = ? AND pc.ugc_id IS NOT NULL;", propertyId); std::vector toReturn; @@ -13,7 +13,8 @@ std::vector SQLiteDatabase::GetUgcModels(const LWOOBJID& propertyId int blobSize{}; const auto* blob = result.getBlobField("lxfml", blobSize); model.lxfmlData << std::string(reinterpret_cast(blob), blobSize); - model.id = result.getInt64Field("id"); + model.id = result.getInt64Field("ugcID"); + model.modelID = result.getInt64Field("modelID"); toReturn.push_back(std::move(model)); result.nextRow(); } @@ -22,12 +23,13 @@ std::vector SQLiteDatabase::GetUgcModels(const LWOOBJID& propertyId } std::vector SQLiteDatabase::GetAllUgcModels() { - auto [_, result] = ExecuteSelect("SELECT id, lxfml FROM ugc;"); + auto [_, result] = ExecuteSelect("SELECT u.id AS ugcID, pc.id AS modelID, lxfml FROM ugc AS u JOIN properties_contents AS pc ON pc.id = u.id;"); std::vector models; while (!result.eof()) { IUgc::Model model; - model.id = result.getInt64Field("id"); + model.id = result.getInt64Field("ugcID"); + model.modelID = result.getInt64Field("modelID"); int blobSize{}; const auto* blob = result.getBlobField("lxfml", blobSize); @@ -44,7 +46,7 @@ void SQLiteDatabase::RemoveUnreferencedUgcModels() { } void SQLiteDatabase::InsertNewUgcModel( - std::istringstream& sd0Data, // cant be const sad + std::stringstream& sd0Data, // cant be const sad const uint32_t blueprintId, const uint32_t accountId, const uint32_t characterId) { @@ -66,7 +68,7 @@ void SQLiteDatabase::DeleteUgcModelData(const LWOOBJID& modelId) { ExecuteDelete("DELETE FROM properties_contents WHERE ugc_id = ?;", modelId); } -void SQLiteDatabase::UpdateUgcModelData(const LWOOBJID& modelId, std::istringstream& lxfml) { +void SQLiteDatabase::UpdateUgcModelData(const LWOOBJID& modelId, std::stringstream& lxfml) { const std::istream stream(lxfml.rdbuf()); ExecuteUpdate("UPDATE ugc SET lxfml = ? WHERE id = ?;", &stream, modelId); } diff --git a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.cpp b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.cpp index 733f281b..01b3961f 100644 --- a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.cpp +++ b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.cpp @@ -60,7 +60,7 @@ void TestSQLDatabase::DeleteUgcModelData(const LWOOBJID& modelId) { } -void TestSQLDatabase::UpdateUgcModelData(const LWOOBJID& modelId, std::istringstream& lxfml) { +void TestSQLDatabase::UpdateUgcModelData(const LWOOBJID& modelId, std::stringstream& lxfml) { } @@ -168,7 +168,7 @@ void TestSQLDatabase::InsertNewPropertyModel(const LWOOBJID& propertyId, const I } -void TestSQLDatabase::UpdateModel(const LWOOBJID& propertyId, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) { +void TestSQLDatabase::UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) { } @@ -192,7 +192,7 @@ void TestSQLDatabase::InsertNewMail(const MailInfo& mail) { } -void TestSQLDatabase::InsertNewUgcModel(std::istringstream& sd0Data, const uint32_t blueprintId, const uint32_t accountId, const uint32_t characterId) { +void TestSQLDatabase::InsertNewUgcModel(std::stringstream& sd0Data, const uint32_t blueprintId, const uint32_t accountId, const uint32_t characterId) { } diff --git a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h index e9cf8acb..8016f333 100644 --- a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h +++ b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h @@ -25,7 +25,7 @@ class TestSQLDatabase : public GameDatabase { void RemoveFriend(const uint32_t playerAccountId, const uint32_t friendAccountId) override; void UpdateActivityLog(const uint32_t characterId, const eActivityType activityType, const LWOMAPID mapId) override; void DeleteUgcModelData(const LWOOBJID& modelId) override; - void UpdateUgcModelData(const LWOOBJID& modelId, std::istringstream& lxfml) override; + void UpdateUgcModelData(const LWOOBJID& modelId, std::stringstream& lxfml) override; std::vector GetAllUgcModels() override; void CreateMigrationHistoryTable() override; bool IsMigrationRun(const std::string_view str) override; @@ -52,14 +52,14 @@ class TestSQLDatabase : public GameDatabase { std::vector GetPropertyModels(const LWOOBJID& propertyId) override; void RemoveUnreferencedUgcModels() override; void InsertNewPropertyModel(const LWOOBJID& propertyId, const IPropertyContents::Model& model, const std::string_view name) override; - void UpdateModel(const LWOOBJID& propertyId, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) override; + void UpdateModel(const LWOOBJID& modelID, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) override; void RemoveModel(const LWOOBJID& modelId) override; void UpdatePerformanceCost(const LWOZONEID& zoneId, const float performanceCost) override; void InsertNewBugReport(const IBugReports::Info& info) override; void InsertCheatDetection(const IPlayerCheatDetections::Info& info) override; void InsertNewMail(const MailInfo& mail) override; void InsertNewUgcModel( - std::istringstream& sd0Data, + std::stringstream& sd0Data, const uint32_t blueprintId, const uint32_t accountId, const uint32_t characterId) override; @@ -105,6 +105,7 @@ class TestSQLDatabase : public GameDatabase { uint32_t GetAccountCount() override { return 0; }; bool IsNameInUse(const std::string_view name) override { return false; }; + IPropertyContents::Model GetModel(const LWOOBJID modelID) override { return {}; } }; #endif //!TESTSQLDATABASE_H diff --git a/dDatabase/MigrationRunner.cpp b/dDatabase/MigrationRunner.cpp index e6dfb042..8cdd17ae 100644 --- a/dDatabase/MigrationRunner.cpp +++ b/dDatabase/MigrationRunner.cpp @@ -7,6 +7,7 @@ #include "GeneralUtils.h" #include "Logger.h" #include "BinaryPathFinder.h" +#include "ModelNormalizeMigration.h" #include @@ -35,7 +36,7 @@ void MigrationRunner::RunMigrations() { Database::Get()->CreateMigrationHistoryTable(); // has to be here because when moving the files to the new folder, the migration_history table is not updated so it will run them all again. - + const auto migrationFolder = Database::GetMigrationFolder(); if (!Database::Get()->IsMigrationRun("17_migration_for_migrations.sql") && migrationFolder == "mysql") { LOG("Running migration: 17_migration_for_migrations.sql"); @@ -45,6 +46,7 @@ void MigrationRunner::RunMigrations() { std::string finalSQL = ""; bool runSd0Migrations = false; + bool runNormalizeMigrations = false; for (const auto& entry : GeneralUtils::GetSqlFileNamesFromFolder((BinaryPathFinder::GetBinaryDir() / "./migrations/dlu/" / migrationFolder).string())) { auto migration = LoadMigration("dlu/" + migrationFolder + "/", entry); @@ -57,6 +59,8 @@ void MigrationRunner::RunMigrations() { LOG("Running migration: %s", migration.name.c_str()); if (migration.name == "5_brick_model_sd0.sql") { runSd0Migrations = true; + } else if (migration.name.ends_with("_normalize_model_positions.sql")) { + runNormalizeMigrations = true; } else { finalSQL.append(migration.data.c_str()); } @@ -64,7 +68,7 @@ void MigrationRunner::RunMigrations() { Database::Get()->InsertMigration(migration.name); } - if (finalSQL.empty() && !runSd0Migrations) { + if (finalSQL.empty() && !runSd0Migrations && !runNormalizeMigrations) { LOG("Server database is up to date."); return; } @@ -88,6 +92,10 @@ void MigrationRunner::RunMigrations() { uint32_t numberOfTruncatedModels = BrickByBrickFix::TruncateBrokenBrickByBrickXml(); LOG("%i models were truncated from the database.", numberOfTruncatedModels); } + + if (runNormalizeMigrations) { + ModelNormalizeMigration::Run(); + } } void MigrationRunner::RunSQLiteMigrations() { diff --git a/dDatabase/ModelNormalizeMigration.cpp b/dDatabase/ModelNormalizeMigration.cpp new file mode 100644 index 00000000..b8215733 --- /dev/null +++ b/dDatabase/ModelNormalizeMigration.cpp @@ -0,0 +1,30 @@ +#include "ModelNormalizeMigration.h" + +#include "Database.h" +#include "Lxfml.h" +#include "Sd0.h" + +void ModelNormalizeMigration::Run() { + const auto oldCommit = Database::Get()->GetAutoCommit(); + Database::Get()->SetAutoCommit(false); + for (auto& [lxfmlData, id, modelID] : Database::Get()->GetAllUgcModels()) { + const auto model = Database::Get()->GetModel(modelID); + // only BBB models (lot 14) and models with a position of NiPoint3::ZERO need to have their position fixed. + if (model.position != NiPoint3Constant::ZERO || model.lot != 14) continue; + + Sd0 sd0(lxfmlData); + const auto asStr = sd0.GetAsStringUncompressed(); + const auto [newLxfml, newCenter] = Lxfml::NormalizePosition(asStr); + if (newCenter == NiPoint3Constant::ZERO) { + LOG("Failed to update model %llu due to failure reading xml."); + continue; + } + + LOG("Updated model %llu to have a center of %f %f %f", modelID, newCenter.x, newCenter.y, newCenter.z); + sd0.FromData(reinterpret_cast(newLxfml.data()), newLxfml.size()); + auto asStream = sd0.GetAsStream(); + Database::Get()->UpdateModel(model.id, newCenter, model.rotation, model.behaviors); + Database::Get()->UpdateUgcModelData(id, asStream); + } + Database::Get()->SetAutoCommit(oldCommit); +} diff --git a/dDatabase/ModelNormalizeMigration.h b/dDatabase/ModelNormalizeMigration.h new file mode 100644 index 00000000..000781cd --- /dev/null +++ b/dDatabase/ModelNormalizeMigration.h @@ -0,0 +1,11 @@ +// Darkflame Universe +// Copyright 2025 + +#ifndef MODELNORMALIZEMIGRATION_H +#define MODELNORMALIZEMIGRATION_H + +namespace ModelNormalizeMigration { + void Run(); +}; + +#endif //!MODELNORMALIZEMIGRATION_H diff --git a/dGame/EntityManager.cpp b/dGame/EntityManager.cpp index c95af3d7..ad0c0b1c 100644 --- a/dGame/EntityManager.cpp +++ b/dGame/EntityManager.cpp @@ -99,7 +99,7 @@ Entity* EntityManager::CreateEntity(EntityInfo info, User* user, Entity* parentE } // Exclude the zone control object from any flags - if (!controller && info.lot != 14) { + if (!controller) { // The client flags means the client should render the entity GeneralUtils::SetBit(id, eObjectBits::CLIENT); diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index 1f60017b..d1f2c587 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -102,6 +102,8 @@ #include "CDComponentsRegistryTable.h" #include "CDObjectsTable.h" #include "eItemType.h" +#include "Lxfml.h" +#include "Sd0.h" void GameMessages::SendFireEventClientSide(const LWOOBJID& objectID, const SystemAddress& sysAddr, std::u16string args, const LWOOBJID& object, int64_t param1, int param2, const LWOOBJID& sender) { CBITSTREAM; @@ -2574,18 +2576,6 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent TODO Apparently the bricks are supposed to be taken via MoveInventoryBatch? */ - ////Decompress the SD0 from the client so we can process the lxfml properly - //uint8_t* outData = new uint8_t[327680]; - //int32_t error; - //int32_t size = ZCompression::Decompress(inData, lxfmlSize, outData, 327680, error); - - //if (size == -1) { - // LOG("Failed to decompress LXFML: (%i)", error); - // return; - //} - // - //std::string lxfml(reinterpret_cast(outData), size); //std::string version of the decompressed data! - //Now, the cave of dragons: //We runs this in async because the http library here is blocking, meaning it'll halt the thread. @@ -2613,16 +2603,25 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent LWOOBJID propertyId = LWOOBJID_EMPTY; if (propertyInfo) propertyId = propertyInfo->id; - //Insert into ugc: + // Save the binary data to the Sd0 buffer std::string str(sd0Data.get(), sd0Size); std::istringstream sd0DataStream(str); - Database::Get()->InsertNewUgcModel(sd0DataStream, blueprintIDSmall, entity->GetCharacter()->GetParentUser()->GetAccountID(), entity->GetCharacter()->GetID()); + Sd0 sd0(sd0DataStream); + + // Uncompress the data and normalize the position + const auto asStr = sd0.GetAsStringUncompressed(); + const auto [newLxfml, newCenter] = Lxfml::NormalizePosition(asStr); + + // Recompress the data and save to the database + sd0.FromData(reinterpret_cast(newLxfml.data()), newLxfml.size()); + auto sd0AsStream = sd0.GetAsStream(); + Database::Get()->InsertNewUgcModel(sd0AsStream, blueprintIDSmall, entity->GetCharacter()->GetParentUser()->GetAccountID(), entity->GetCharacter()->GetID()); //Insert into the db as a BBB model: IPropertyContents::Model model; model.id = newIDL; model.ugcId = blueprintIDSmall; - model.position = NiPoint3Constant::ZERO; + model.position = newCenter; model.rotation = NiQuaternion(0.0f, 0.0f, 0.0f, 0.0f); model.lot = 14; Database::Get()->InsertNewPropertyModel(propertyId, model, "Objects_14_name"); @@ -2648,6 +2647,9 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent //} //Tell the client their model is saved: (this causes us to actually pop out of our current state): + const auto& newSd0 = sd0.GetAsVector(); + uint32_t sd0Size{}; + for (const auto& chunk : newSd0) sd0Size += chunk.size(); CBITSTREAM; BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, MessageType::Client::BLUEPRINT_SAVE_RESPONSE); bitStream.Write(localId); @@ -2655,9 +2657,9 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent bitStream.Write(1); bitStream.Write(blueprintID); - bitStream.Write(sd0Size); + bitStream.Write(sd0Size); - bitStream.WriteAlignedBytes(reinterpret_cast(sd0Data.get()), sd0Size); + for (const auto& chunk : newSd0) bitStream.WriteAlignedBytes(reinterpret_cast(chunk.data()), chunk.size()); SEND_PACKET; @@ -2665,7 +2667,7 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent EntityInfo info; info.lot = 14; - info.pos = {}; + info.pos = newCenter; info.rot = {}; info.spawner = nullptr; info.spawnerID = entity->GetObjectID(); diff --git a/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index d88d33ce..8d189864 100644 --- a/dWorldServer/WorldServer.cpp +++ b/dWorldServer/WorldServer.cpp @@ -1153,6 +1153,8 @@ void HandlePacket(Packet* packet) { GeneralUtils::SetBit(blueprintID, eObjectBits::CHARACTER); GeneralUtils::SetBit(blueprintID, eObjectBits::PERSISTENT); + // Workaround for not having a UGC server to get model LXFML onto the client so it + // can generate the physics and nif for the object. CBITSTREAM; BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, MessageType::Client::BLUEPRINT_SAVE_RESPONSE); bitStream.Write(LWOOBJID_EMPTY); //always zero so that a check on the client passes @@ -1452,7 +1454,6 @@ void WorldShutdownProcess(uint32_t zoneId) { if (PropertyManagementComponent::Instance() != nullptr) { LOG("Saving ALL property data for zone %i clone %i!", zoneId, PropertyManagementComponent::Instance()->GetCloneId()); PropertyManagementComponent::Instance()->Save(); - Database::Get()->RemoveUnreferencedUgcModels(); LOG("ALL property data saved for zone %i clone %i!", zoneId, PropertyManagementComponent::Instance()->GetCloneId()); } diff --git a/migrations/dlu/mysql/19_normalize_model_positions.sql b/migrations/dlu/mysql/19_normalize_model_positions.sql new file mode 100644 index 00000000..833fbe9b --- /dev/null +++ b/migrations/dlu/mysql/19_normalize_model_positions.sql @@ -0,0 +1 @@ +/* See ModelNormalizeMigration.cpp for details */ diff --git a/migrations/dlu/sqlite/2_normalize_model_positions.sql b/migrations/dlu/sqlite/2_normalize_model_positions.sql new file mode 100644 index 00000000..833fbe9b --- /dev/null +++ b/migrations/dlu/sqlite/2_normalize_model_positions.sql @@ -0,0 +1 @@ +/* See ModelNormalizeMigration.cpp for details */ From b6f7b4c09252065327d5a6c96fcdf01059c67d24 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Mon, 5 May 2025 16:57:05 -0700 Subject: [PATCH 5/6] fix: add null check and character version update (#1793) * fix: add null check * Add version update as well --- dWorldServer/WorldServer.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index 8d189864..d2691e1b 100644 --- a/dWorldServer/WorldServer.cpp +++ b/dWorldServer/WorldServer.cpp @@ -1104,12 +1104,13 @@ void HandlePacket(Packet* packet) { bool complete = true; for (auto missionID : missions) { auto* mission = missionComponent->GetMission(missionID); - if (!mission->IsComplete()) { + if (!mission || !mission->IsComplete()) { complete = false; } } if (complete) missionComponent->CompleteMission(937 /* Nexus Force explorer */); + levelComponent->SetCharacterVersion(eCharacterVersion::UP_TO_DATE); [[fallthrough]]; } case eCharacterVersion::UP_TO_DATE: From e18c504ee4ef0fc9b7a3f2983232160e87e46e30 Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Wed, 7 May 2025 21:15:10 -0700 Subject: [PATCH 6/6] feat: auto reject empty properties (#1794) Tested that having the config option set to 1 and having an empty property auto-rejected it. Tested that having a model on the property or having the new config option set to 0 auto approved the property (as per live) --- dGame/dComponents/PropertyManagementComponent.cpp | 11 ++++++++--- dGame/dComponents/PropertyManagementComponent.h | 2 +- resources/worldconfig.ini | 3 +++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/dGame/dComponents/PropertyManagementComponent.cpp b/dGame/dComponents/PropertyManagementComponent.cpp index d866e0ad..f20a2886 100644 --- a/dGame/dComponents/PropertyManagementComponent.cpp +++ b/dGame/dComponents/PropertyManagementComponent.cpp @@ -26,6 +26,7 @@ #include #include "CppScripts.h" #include +#include "dConfig.h" PropertyManagementComponent* PropertyManagementComponent::instance = nullptr; @@ -151,7 +152,11 @@ void PropertyManagementComponent::SetPrivacyOption(PropertyPrivacyOption value) info.rejectionReason = rejectionReason; info.modApproved = 0; - Database::Get()->UpdatePropertyModerationInfo(info); + if (models.empty() && Game::config->GetValue("auto_reject_empty_properties") == "1") { + UpdateApprovedStatus(false, "Your property is empty. Please place a model to have a public property."); + } else { + Database::Get()->UpdatePropertyModerationInfo(info); + } } void PropertyManagementComponent::UpdatePropertyDetails(std::string name, std::string description) { @@ -565,14 +570,14 @@ void PropertyManagementComponent::DeleteModel(const LWOOBJID id, const int delet } } -void PropertyManagementComponent::UpdateApprovedStatus(const bool value) { +void PropertyManagementComponent::UpdateApprovedStatus(const bool value, const std::string& rejectionReason) { if (owner == LWOOBJID_EMPTY) return; IProperty::Info info; info.id = propertyId; info.modApproved = value; info.privacyOption = static_cast(privacyOption); - info.rejectionReason = ""; + info.rejectionReason = rejectionReason; Database::Get()->UpdatePropertyModerationInfo(info); } diff --git a/dGame/dComponents/PropertyManagementComponent.h b/dGame/dComponents/PropertyManagementComponent.h index 6a9ed09d..4dd482ef 100644 --- a/dGame/dComponents/PropertyManagementComponent.h +++ b/dGame/dComponents/PropertyManagementComponent.h @@ -135,7 +135,7 @@ public: * Updates whether or not this property is approved by a moderator * @param value true if the property should be approved, false otherwise */ - void UpdateApprovedStatus(bool value); + void UpdateApprovedStatus(bool value, const std::string& rejectionReason = ""); /** * Loads all the models on this property from the database diff --git a/resources/worldconfig.ini b/resources/worldconfig.ini index 91028ffe..1e73ceea 100644 --- a/resources/worldconfig.ini +++ b/resources/worldconfig.ini @@ -77,3 +77,6 @@ allow_players_to_skip_cinematics=0 # Customizable message for what to say when there is a cdclient fdb mismatch cdclient_mismatch_title=Version out of date cdclient_mismatch_message=We detected that your client is out of date. Please update your client to the latest version. + +# Auto reject properties which contain no models | must be 1 in order to auto reject. +auto_reject_empty_properties=0