diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDZoneTableTable.cpp b/dDatabase/CDClientDatabase/CDClientTables/CDZoneTableTable.cpp index 1030593e1..2793ccb91 100644 --- a/dDatabase/CDClientDatabase/CDClientTables/CDZoneTableTable.cpp +++ b/dDatabase/CDClientDatabase/CDClientTables/CDZoneTableTable.cpp @@ -31,7 +31,7 @@ void CDZoneTableTable::LoadValuesFromDatabase() { entry.smashableMaxDistance = tableData.getFloatField("smashableMaxDistance", -1.0f); UNUSED(entry.mixerProgram = tableData.getStringField("mixerProgram", "")); UNUSED(entry.clientPhysicsFramerate = tableData.getStringField("clientPhysicsFramerate", "")); - UNUSED(entry.serverPhysicsFramerate = tableData.getStringField("serverPhysicsFramerate", "")); + entry.serverPhysicsFramerate = tableData.getStringField("serverPhysicsFramerate", ""); entry.zoneControlTemplate = tableData.getIntField("zoneControlTemplate", -1); entry.widthInChunks = tableData.getIntField("widthInChunks", -1); entry.heightInChunks = tableData.getIntField("heightInChunks", -1); @@ -40,10 +40,10 @@ void CDZoneTableTable::LoadValuesFromDatabase() { entry.fZoneWeight = tableData.getFloatField("fZoneWeight", -1.0f); UNUSED(entry.thumbnail = tableData.getStringField("thumbnail", "")); entry.PlayerLoseCoinsOnDeath = tableData.getIntField("PlayerLoseCoinsOnDeath", -1) == 1 ? true : false; - UNUSED(entry.disableSaveLoc = tableData.getIntField("disableSaveLoc", -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", "")); - UNUSED(entry.mountsAllowed = tableData.getIntField("mountsAllowed", -1) == 1 ? true : false); + entry.mountsAllowed = tableData.getIntField("mountsAllowed", -1) == 1 ? true : false; this->m_Entries.insert(std::make_pair(entry.zoneID, entry)); tableData.nextRow(); diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDZoneTableTable.h b/dDatabase/CDClientDatabase/CDClientTables/CDZoneTableTable.h index fef8096fd..c3f51aa6f 100644 --- a/dDatabase/CDClientDatabase/CDClientTables/CDZoneTableTable.h +++ b/dDatabase/CDClientDatabase/CDClientTables/CDZoneTableTable.h @@ -18,7 +18,7 @@ struct CDZoneTable { float smashableMaxDistance; //!< The maximum smashable distance? UNUSED(std::string mixerProgram); //!< ??? UNUSED(std::string clientPhysicsFramerate); //!< The client physics framerate - UNUSED(std::string serverPhysicsFramerate); //!< The server physics framerate + std::string serverPhysicsFramerate; //!< The server physics framerate unsigned int zoneControlTemplate; //!< The Zone Control template unsigned int widthInChunks; //!< The width of the world in chunks unsigned int heightInChunks; //!< The height of the world in chunks @@ -27,10 +27,10 @@ struct CDZoneTable { float fZoneWeight; //!< ??? UNUSED(std::string thumbnail); //!< The thumbnail of the world bool PlayerLoseCoinsOnDeath; //!< Whether or not the user loses coins on death - UNUSED(bool disableSaveLoc); //!< Disables the saving location? + bool disableSaveLoc; //!< Disables the saving location? float teamRadius; //!< ??? UNUSED(std::string gate_version); //!< The gate version - UNUSED(bool mountsAllowed); //!< Whether or not mounts are allowed + bool mountsAllowed; //!< Whether or not mounts are allowed }; class CDZoneTableTable : public CDTable { diff --git a/dGame/Character.cpp b/dGame/Character.cpp index a0c607b68..ee04710a6 100644 --- a/dGame/Character.cpp +++ b/dGame/Character.cpp @@ -251,7 +251,7 @@ void Character::SaveXMLToDatabase() { auto zoneInfo = Game::zoneManager->GetZone()->GetZoneID(); // lzid garbage, binary concat of zoneID, zoneInstance and zoneClone - if (zoneInfo.GetMapID() != 0 && zoneInfo.GetCloneID() == 0) { + if (zoneInfo.GetMapID() != 0 && zoneInfo.GetCloneID() == 0 && !Game::zoneManager->GetDisableSaveLocation()) { uint64_t lzidConcat = zoneInfo.GetCloneID(); lzidConcat = (lzidConcat << 16) | uint16_t(zoneInfo.GetInstanceID()); lzidConcat = (lzidConcat << 16) | uint16_t(zoneInfo.GetMapID()); diff --git a/dGame/dBehaviors/BasicAttackBehavior.cpp b/dGame/dBehaviors/BasicAttackBehavior.cpp index 2bd6ce412..36615a9f1 100644 --- a/dGame/dBehaviors/BasicAttackBehavior.cpp +++ b/dGame/dBehaviors/BasicAttackBehavior.cpp @@ -3,6 +3,8 @@ #include "Game.h" #include "Logger.h" #include "EntityManager.h" +#include "dZoneManager.h" +#include "WorldConfig.h" #include "DestroyableComponent.h" #include "BehaviorContext.h" #include "eBasicAttackSuccessTypes.h" @@ -13,8 +15,15 @@ void BasicAttackBehavior::Handle(BehaviorContext* context, RakNet::BitStream* bi auto* destroyableComponent = entity->GetComponent(); if (destroyableComponent != nullptr) { - PlayFx(u"onhit", entity->GetObjectID()); + PlayFx(u"onhit", entity->GetObjectID()); //This damage animation doesn't seem to play consistently destroyableComponent->Damage(this->m_MaxDamage, context->originator, context->skillID); + + //Handle player damage cooldown + if (entity->IsPlayer() && !this->m_DontApplyImmune) { + const float immunityTime = Game::zoneManager->GetWorldConfig()->globalImmunityTime; + destroyableComponent->SetDamageCooldownTimer(immunityTime); + LOG_DEBUG("Target targetEntity %llu took damage, setting damage cooldown timer to %f s", branch.target, immunityTime); + } } this->m_OnSuccess->Handle(context, bitStream, branch); @@ -72,6 +81,7 @@ void BasicAttackBehavior::DoHandleBehavior(BehaviorContext* context, RakNet::Bit } if (isImmune) { + LOG_DEBUG("Target targetEntity %llu is immune!", branch.target); this->m_OnFailImmune->Handle(context, bitStream, branch); return; } @@ -178,11 +188,15 @@ void BasicAttackBehavior::DoBehaviorCalculation(BehaviorContext* context, RakNet return; } - const bool isImmune = destroyableComponent->IsImmune(); + const float immunityTime = Game::zoneManager->GetWorldConfig()->globalImmunityTime; + LOG_DEBUG("Damage cooldown timer currently %f s", destroyableComponent->GetDamageCooldownTimer()); + + const bool isImmune = (destroyableComponent->IsImmune()) || (destroyableComponent->IsCooldownImmune()); bitStream->Write(isImmune); if (isImmune) { + LOG_DEBUG("Target targetEntity %llu is immune!", branch.target); this->m_OnFailImmune->Calculate(context, bitStream, branch); return; } @@ -203,6 +217,12 @@ void BasicAttackBehavior::DoBehaviorCalculation(BehaviorContext* context, RakNet bitStream->Write(isSuccess); + //Handle player damage cooldown + if (isSuccess && targetEntity->IsPlayer() && !this->m_DontApplyImmune) { + destroyableComponent->SetDamageCooldownTimer(immunityTime); + LOG_DEBUG("Target targetEntity %llu took damage, setting damage cooldown timer to %f s", branch.target, immunityTime); + } + eBasicAttackSuccessTypes successState = eBasicAttackSuccessTypes::FAILIMMUNE; if (isSuccess) { if (healthDamageDealt >= 1) { @@ -236,6 +256,8 @@ void BasicAttackBehavior::DoBehaviorCalculation(BehaviorContext* context, RakNet } void BasicAttackBehavior::Load() { + this->m_DontApplyImmune = GetBoolean("dont_apply_immune"); + this->m_MinDamage = GetInt("min damage"); if (this->m_MinDamage == 0) this->m_MinDamage = 1; diff --git a/dGame/dBehaviors/BasicAttackBehavior.h b/dGame/dBehaviors/BasicAttackBehavior.h index f6e3fa287..6525c3431 100644 --- a/dGame/dBehaviors/BasicAttackBehavior.h +++ b/dGame/dBehaviors/BasicAttackBehavior.h @@ -10,14 +10,14 @@ class BasicAttackBehavior final : public Behavior /** * @brief Reads a 16bit short from the bitStream and when the actual behavior handling finishes with all of its branches, the bitStream * is then offset to after the allocated bits for this stream. - * + * */ void DoHandleBehavior(BehaviorContext* context, RakNet::BitStream* bitStream, BehaviorBranchContext branch); /** * @brief Handles a client initialized Basic Attack Behavior cast to be deserialized and verified on the server. - * - * @param context The Skill's Behavior context. All behaviors in the same tree share the same context + * + * @param context The Skill's Behavior context. All behaviors in the same tree share the same context * @param bitStream The bitStream to deserialize. BitStreams will always check their bounds before reading in a behavior * and will fail gracefully if an overread is detected. * @param branch The context of this specific branch of the Skill Behavior. Changes based on which branch you are going down. @@ -27,13 +27,13 @@ class BasicAttackBehavior final : public Behavior /** * @brief Writes a 16bit short to the bitStream and when the actual behavior calculation finishes with all of its branches, the number * of bits used is then written to where the 16bit short initially was. - * + * */ void Calculate(BehaviorContext* context, RakNet::BitStream* bitStream, BehaviorBranchContext branch) override; /** * @brief Calculates a server initialized Basic Attack Behavior cast to be serialized to the client - * + * * @param context The Skill's Behavior context. All behaviors in the same tree share the same context * @param bitStream The bitStream to serialize to. * @param branch The context of this specific branch of the Skill Behavior. Changes based on which branch you are going down. @@ -44,10 +44,12 @@ class BasicAttackBehavior final : public Behavior * @brief Loads this Behaviors parameters from the database. For this behavior specifically: * max and min damage will always be the same. If min is less than max, they are both set to max. * If an action is not in the database, then no action is taken for that result. - * + * */ void Load() override; private: + bool m_DontApplyImmune; + uint32_t m_MinDamage; uint32_t m_MaxDamage; diff --git a/dGame/dComponents/ControllablePhysicsComponent.cpp b/dGame/dComponents/ControllablePhysicsComponent.cpp index 08281ed5c..dd981f660 100644 --- a/dGame/dComponents/ControllablePhysicsComponent.cpp +++ b/dGame/dComponents/ControllablePhysicsComponent.cpp @@ -187,7 +187,7 @@ void ControllablePhysicsComponent::UpdateXml(tinyxml2::XMLDocument* doc) { auto zoneInfo = Game::zoneManager->GetZone()->GetZoneID(); - if (zoneInfo.GetMapID() != 0 && zoneInfo.GetCloneID() == 0) { + if (zoneInfo.GetMapID() != 0 && zoneInfo.GetCloneID() == 0 && !Game::zoneManager->GetDisableSaveLocation()) { character->SetAttribute("lzx", m_Position.x); character->SetAttribute("lzy", m_Position.y); character->SetAttribute("lzz", m_Position.z); diff --git a/dGame/dComponents/DestroyableComponent.cpp b/dGame/dComponents/DestroyableComponent.cpp index 1cf1da409..e98bc33ba 100644 --- a/dGame/dComponents/DestroyableComponent.cpp +++ b/dGame/dComponents/DestroyableComponent.cpp @@ -73,6 +73,8 @@ DestroyableComponent::DestroyableComponent(Entity* parent) : Component(parent) { m_ImmuneToQuickbuildInterruptCount = 0; m_ImmuneToPullToPointCount = 0; m_DeathBehavior = -1; + + m_DamageCooldownTimer = 0.0f; } DestroyableComponent::~DestroyableComponent() { @@ -179,6 +181,10 @@ void DestroyableComponent::Serialize(RakNet::BitStream* outBitStream, bool bIsIn } } +void DestroyableComponent::Update(float deltaTime) { + m_DamageCooldownTimer -= deltaTime; +} + void DestroyableComponent::LoadFromXml(tinyxml2::XMLDocument* doc) { tinyxml2::XMLElement* dest = doc->FirstChildElement("obj")->FirstChildElement("dest"); if (!dest) { @@ -409,7 +415,7 @@ void DestroyableComponent::AddFaction(const int32_t factionID, const bool ignore } bool DestroyableComponent::IsEnemy(const Entity* other) const { - if (m_Parent->IsPlayer() && other->IsPlayer()){ + if (m_Parent->IsPlayer() && other->IsPlayer()) { auto* thisCharacterComponent = m_Parent->GetComponent(); if (!thisCharacterComponent) return false; auto* otherCharacterComponent = other->GetComponent(); @@ -464,6 +470,10 @@ bool DestroyableComponent::IsImmune() const { return m_IsGMImmune || m_ImmuneToBasicAttackCount > 0; } +bool DestroyableComponent::IsCooldownImmune() const { + return m_DamageCooldownTimer > 0.0f; +} + bool DestroyableComponent::IsKnockbackImmune() const { auto* characterComponent = m_Parent->GetComponent(); auto* inventoryComponent = m_Parent->GetComponent(); @@ -546,7 +556,8 @@ void DestroyableComponent::Damage(uint32_t damage, const LWOOBJID source, uint32 return; } - if (IsImmune()) { + if (IsImmune() || IsCooldownImmune()) { + LOG_DEBUG("Target targetEntity %llu is immune!", m_Parent->GetObjectID()); //Immune is succesfully proc'd return; } @@ -634,9 +645,9 @@ void DestroyableComponent::Damage(uint32_t damage, const LWOOBJID source, uint32 } //check if hardcore mode is enabled - if (Game::entityManager->GetHardcoreMode()) { + if (Game::entityManager->GetHardcoreMode()) { DoHardcoreModeDrops(source); - } + } Smash(source, eKillType::VIOLENT, u"", skillID); } @@ -796,16 +807,16 @@ void DestroyableComponent::SetFaction(int32_t factionID, bool ignoreChecks) { } void DestroyableComponent::SetStatusImmunity( - const eStateChangeType state, - const bool bImmuneToBasicAttack, - const bool bImmuneToDamageOverTime, - const bool bImmuneToKnockback, - const bool bImmuneToInterrupt, - const bool bImmuneToSpeed, - const bool bImmuneToImaginationGain, - const bool bImmuneToImaginationLoss, - const bool bImmuneToQuickbuildInterrupt, - const bool bImmuneToPullToPoint) { + const eStateChangeType state, + const bool bImmuneToBasicAttack, + const bool bImmuneToDamageOverTime, + const bool bImmuneToKnockback, + const bool bImmuneToInterrupt, + const bool bImmuneToSpeed, + const bool bImmuneToImaginationGain, + const bool bImmuneToImaginationLoss, + const bool bImmuneToQuickbuildInterrupt, + const bool bImmuneToPullToPoint) { if (state == eStateChangeType::POP) { if (bImmuneToBasicAttack && m_ImmuneToBasicAttackCount > 0) m_ImmuneToBasicAttackCount -= 1; @@ -818,7 +829,7 @@ void DestroyableComponent::SetStatusImmunity( if (bImmuneToQuickbuildInterrupt && m_ImmuneToQuickbuildInterruptCount > 0) m_ImmuneToQuickbuildInterruptCount -= 1; if (bImmuneToPullToPoint && m_ImmuneToPullToPointCount > 0) m_ImmuneToPullToPointCount -= 1; - } else if (state == eStateChangeType::PUSH){ + } else if (state == eStateChangeType::PUSH) { if (bImmuneToBasicAttack) m_ImmuneToBasicAttackCount += 1; if (bImmuneToDamageOverTime) m_ImmuneToDamageOverTimeCount += 1; if (bImmuneToKnockback) m_ImmuneToKnockbackCount += 1; @@ -945,7 +956,7 @@ void DestroyableComponent::AddOnHitCallback(const std::function& m_OnHitCallbacks.push_back(callback); } -void DestroyableComponent::DoHardcoreModeDrops(const LWOOBJID source){ +void DestroyableComponent::DoHardcoreModeDrops(const LWOOBJID source) { //check if this is a player: if (m_Parent->IsPlayer()) { //remove hardcore_lose_uscore_on_death_percent from the player's uscore: @@ -963,9 +974,9 @@ void DestroyableComponent::DoHardcoreModeDrops(const LWOOBJID source){ if (inventory) { //get the items inventory: auto items = inventory->GetInventory(eInventoryType::ITEMS); - if (items){ + if (items) { auto itemMap = items->GetItems(); - if (!itemMap.empty()){ + if (!itemMap.empty()) { for (const auto& item : itemMap) { //drop the item: if (!item.second) continue; diff --git a/dGame/dComponents/DestroyableComponent.h b/dGame/dComponents/DestroyableComponent.h index 52d4be5a2..b81ab9f31 100644 --- a/dGame/dComponents/DestroyableComponent.h +++ b/dGame/dComponents/DestroyableComponent.h @@ -24,6 +24,7 @@ class DestroyableComponent : public Component { DestroyableComponent(Entity* parentEntity); ~DestroyableComponent() override; + void Update(float deltaTime) override; void Serialize(RakNet::BitStream* outBitStream, bool bIsInitialUpdate) override; void LoadFromXml(tinyxml2::XMLDocument* doc) override; void UpdateXml(tinyxml2::XMLDocument* doc) override; @@ -166,6 +167,11 @@ class DestroyableComponent : public Component { */ bool IsImmune() const; + /** + * @return whether this entity is currently immune to attacks due to a damage cooldown period + */ + bool IsCooldownImmune() const; + /** * Sets if this entity has GM immunity, making it not killable * @param value the GM immunity of this entity @@ -406,18 +412,23 @@ class DestroyableComponent : public Component { ); // Getters for status immunities - const bool GetImmuneToBasicAttack() {return m_ImmuneToBasicAttackCount > 0;}; - const bool GetImmuneToDamageOverTime() {return m_ImmuneToDamageOverTimeCount > 0;}; - const bool GetImmuneToKnockback() {return m_ImmuneToKnockbackCount > 0;}; - const bool GetImmuneToInterrupt() {return m_ImmuneToInterruptCount > 0;}; - const bool GetImmuneToSpeed() {return m_ImmuneToSpeedCount > 0;}; - const bool GetImmuneToImaginationGain() {return m_ImmuneToImaginationGainCount > 0;}; - const bool GetImmuneToImaginationLoss() {return m_ImmuneToImaginationLossCount > 0;}; - const bool GetImmuneToQuickbuildInterrupt() {return m_ImmuneToQuickbuildInterruptCount > 0;}; - const bool GetImmuneToPullToPoint() {return m_ImmuneToPullToPointCount > 0;}; - - int32_t GetDeathBehavior() const { return m_DeathBehavior; } + const bool GetImmuneToBasicAttack() { return m_ImmuneToBasicAttackCount > 0; }; + const bool GetImmuneToDamageOverTime() { return m_ImmuneToDamageOverTimeCount > 0; }; + const bool GetImmuneToKnockback() { return m_ImmuneToKnockbackCount > 0; }; + const bool GetImmuneToInterrupt() { return m_ImmuneToInterruptCount > 0; }; + const bool GetImmuneToSpeed() { return m_ImmuneToSpeedCount > 0; }; + const bool GetImmuneToImaginationGain() { return m_ImmuneToImaginationGainCount > 0; }; + const bool GetImmuneToImaginationLoss() { return m_ImmuneToImaginationLossCount > 0; }; + const bool GetImmuneToQuickbuildInterrupt() { return m_ImmuneToQuickbuildInterruptCount > 0; }; + const bool GetImmuneToPullToPoint() { return m_ImmuneToPullToPointCount > 0; }; + + // Damage cooldown setters/getters + void SetDamageCooldownTimer(float value) { m_DamageCooldownTimer = value; } + float GetDamageCooldownTimer() { return m_DamageCooldownTimer; } + + // Death behavior setters/getters void SetDeathBehavior(int32_t value) { m_DeathBehavior = value; } + int32_t GetDeathBehavior() const { return m_DeathBehavior; } /** * Utility to reset all stats to the default stats based on items and completed missions @@ -605,6 +616,11 @@ class DestroyableComponent : public Component { * Death behavior type. If 0, the client plays a death animation as opposed to a smash animation. */ int32_t m_DeathBehavior; + + /** + * Damage immunity cooldown timer. Set to a value that then counts down to create a damage cooldown for players + */ + float m_DamageCooldownTimer; }; #endif // DESTROYABLECOMPONENT_H diff --git a/dGame/dComponents/PetComponent.cpp b/dGame/dComponents/PetComponent.cpp index 65b770c4c..1780437ec 100644 --- a/dGame/dComponents/PetComponent.cpp +++ b/dGame/dComponents/PetComponent.cpp @@ -29,6 +29,7 @@ #include "RenderComponent.h" #include "eObjectBits.h" #include "eGameMasterLevel.h" +#include "eMissionState.h" std::unordered_map PetComponent::buildCache{}; std::unordered_map PetComponent::currentActivities{}; @@ -439,9 +440,15 @@ void PetComponent::Update(float deltaTime) { } } + auto* missionComponent = owner->GetComponent(); + if (!missionComponent) return; + + // Determine if the "Lost Tags" mission has been completed and digging has been unlocked + const bool digUnlocked = missionComponent->GetMissionState(842) == eMissionState::COMPLETE; + Entity* closestTresure = PetDigServer::GetClosestTresure(position); - if (closestTresure != nullptr) { + if (closestTresure != nullptr && digUnlocked) { // Skeleton Dragon Pat special case for bone digging if (closestTresure->GetLOT() == 12192 && m_Parent->GetLOT() != 13067) { goto skipTresure; diff --git a/dGame/dInventory/Item.cpp b/dGame/dInventory/Item.cpp index 3b12f8b30..6a0fd82b4 100644 --- a/dGame/dInventory/Item.cpp +++ b/dGame/dInventory/Item.cpp @@ -19,6 +19,8 @@ #include "eObjectBits.h" #include "eReplicaComponentType.h" #include "eUseItemResponse.h" +#include "dZoneManager.h" +#include "ChatPackets.h" #include "CDBrickIDTableTable.h" #include "CDObjectSkillsTable.h" @@ -292,12 +294,19 @@ void Item::UseNonEquip(Item* item) { const auto type = static_cast(info->itemType); if (type == eItemType::MOUNT) { - playerInventoryComponent->HandlePossession(this); - // TODO Check if mounts are allowed to be spawned - } else if (type == eItemType::PET_INVENTORY_ITEM && subKey != LWOOBJID_EMPTY) { - const auto& databasePet = playerInventoryComponent->GetDatabasePet(subKey); - if (databasePet.lot != LOT_NULL) { - playerInventoryComponent->SpawnPet(this); + if (Game::zoneManager->GetMountsAllowed()){ + playerInventoryComponent->HandlePossession(this); + } else { + ChatPackets::SendSystemMessage(playerEntity->GetSystemAddress(), u"Mounts are not allowed in this zone"); + } + } else if (type == eItemType::PET_INVENTORY_ITEM && subKey != LWOOBJID_EMPTY ) { + if (Game::zoneManager->GetPetsAllowed()){ + const auto& databasePet = playerInventoryComponent->GetDatabasePet(subKey); + if (databasePet.lot != LOT_NULL) { + playerInventoryComponent->SpawnPet(this); + } + } else { + ChatPackets::SendSystemMessage(playerEntity->GetSystemAddress(), u"Pets are not allowed in this zone"); } // This precondition response is taken care of in SpawnPet(). } else { @@ -433,7 +442,7 @@ void Item::DisassembleModel() { return; } - auto* doc = new tinyxml2::XMLDocument(); + std::unique_ptr doc(new tinyxml2::XMLDocument()); if (!doc) { return; diff --git a/dGame/dUtilities/SlashCommandHandler.cpp b/dGame/dUtilities/SlashCommandHandler.cpp index e554a28a8..6c47ac80f 100644 --- a/dGame/dUtilities/SlashCommandHandler.cpp +++ b/dGame/dUtilities/SlashCommandHandler.cpp @@ -247,7 +247,7 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit } if (chatCommand == "credits" || chatCommand == "info") { - const auto& customText = chatCommand == "credits" ? VanityUtilities::ParseMarkdown((BinaryPathFinder::GetBinaryDir() / "vanity/CREDITS.md").string()) : VanityUtilities::ParseMarkdown((BinaryPathFinder::GetBinaryDir() / "vanity/INFO.md").string()); + const auto& customText = chatCommand == "credits" ? VanityUtilities::ParseMarkdown((BinaryPathFinder::GetBinaryDir() / "vanity/CREDITS.md").string()) : VanityUtilities::ParseMarkdown((BinaryPathFinder::GetBinaryDir() / "vanity/INFO.md").string()); { AMFArrayValue args; @@ -1447,6 +1447,24 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit return; } + //Testing basic attack immunity + if (chatCommand == "attackimmune" && args.size() >= 1 && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER) { + auto* destroyableComponent = entity->GetComponent(); + + int32_t state = false; + + if (!GeneralUtils::TryParse(args[0], state)) { + ChatPackets::SendSystemMessage(sysAddr, u"Invalid state."); + return; + } + + if (destroyableComponent != nullptr) { + destroyableComponent->SetIsImmune(state); + } + + return; + } + if (chatCommand == "buff" && args.size() >= 2 && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER) { auto* buffComponent = entity->GetComponent(); @@ -1800,7 +1818,7 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit if (chatCommand == "castskill" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER && args.size() >= 1) { auto* skillComponent = entity->GetComponent(); - if (skillComponent){ + if (skillComponent) { uint32_t skillId; if (!GeneralUtils::TryParse(args[0], skillId)) { @@ -1817,7 +1835,7 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit uint32_t skillId; int slot; auto* inventoryComponent = entity->GetComponent(); - if (inventoryComponent){ + if (inventoryComponent) { if (!GeneralUtils::TryParse(args[0], slot)) { ChatPackets::SendSystemMessage(sysAddr, u"Error getting slot."); return; @@ -1826,7 +1844,7 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit ChatPackets::SendSystemMessage(sysAddr, u"Error getting skill."); return; } else { - if(inventoryComponent->SetSkill(slot, skillId)) ChatPackets::SendSystemMessage(sysAddr, u"Set skill to slot successfully"); + if (inventoryComponent->SetSkill(slot, skillId)) ChatPackets::SendSystemMessage(sysAddr, u"Set skill to slot successfully"); else ChatPackets::SendSystemMessage(sysAddr, u"Set skill to slot failed"); } } @@ -1835,7 +1853,7 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit if (chatCommand == "setfaction" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER && args.size() >= 1) { auto* destroyableComponent = entity->GetComponent(); - if (destroyableComponent){ + if (destroyableComponent) { int32_t faction; if (!GeneralUtils::TryParse(args[0], faction)) { @@ -1850,7 +1868,7 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit if (chatCommand == "addfaction" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER && args.size() >= 1) { auto* destroyableComponent = entity->GetComponent(); - if (destroyableComponent){ + if (destroyableComponent) { int32_t faction; if (!GeneralUtils::TryParse(args[0], faction)) { @@ -1865,7 +1883,7 @@ void SlashCommandHandler::HandleChatCommand(const std::u16string& command, Entit if (chatCommand == "getfactions" && entity->GetGMLevel() >= eGameMasterLevel::DEVELOPER) { auto* destroyableComponent = entity->GetComponent(); - if (destroyableComponent){ + if (destroyableComponent) { ChatPackets::SendSystemMessage(sysAddr, u"Friendly factions:"); for (const auto entry : destroyableComponent->GetFactionIDs()) { ChatPackets::SendSystemMessage(sysAddr, (GeneralUtils::to_u16string(entry))); diff --git a/dGame/dUtilities/VanityUtilities.cpp b/dGame/dUtilities/VanityUtilities.cpp index 95436cd30..6c0085242 100644 --- a/dGame/dUtilities/VanityUtilities.cpp +++ b/dGame/dUtilities/VanityUtilities.cpp @@ -15,6 +15,10 @@ #include "Logger.h" #include "BinaryPathFinder.h" #include "EntityInfo.h" +#include "Spawner.h" +#include "dZoneManager.h" +#include "../dWorldServer/ObjectIDManager.h" +#include "Level.h" #include @@ -29,6 +33,21 @@ void VanityUtilities::SpawnVanity() { const uint32_t zoneID = Game::server->GetZoneID(); + for (const auto& npc : m_NPCs) { + if (npc.m_ID == LWOOBJID_EMPTY) continue; + if (npc.m_LOT == 176){ + Game::zoneManager->RemoveSpawner(npc.m_ID); + } else{ + auto* entity = Game::entityManager->GetEntity(npc.m_ID); + if (!entity) continue; + entity->Smash(LWOOBJID_EMPTY, eKillType::VIOLENT); + } + } + + m_NPCs.clear(); + m_Parties.clear(); + m_PartyPhrases.clear(); + ParseXML((BinaryPathFinder::GetBinaryDir() / "vanity/NPC.xml").string()); // Loop through all parties @@ -53,7 +72,7 @@ void VanityUtilities::SpawnVanity() { // Loop through all locations for (const auto& location : party.m_Locations) { - rate = GeneralUtils::GenerateRandomNumber(0, 1); + rate = GeneralUtils::GenerateRandomNumber(0, 1); if (0.75f < rate) { continue; } @@ -66,10 +85,11 @@ void VanityUtilities::SpawnVanity() { } auto& npc = npcList[npcIndex]; - + // Skip spawners + if (npc.m_LOT == 176) continue; + taken.push_back(npcIndex); - // Spawn the NPC LOG("ldf size is %i", npc.ldf.size()); if (npc.ldf.empty()) { npc.ldf = { @@ -79,13 +99,16 @@ void VanityUtilities::SpawnVanity() { } // Spawn the NPC - auto* npcEntity = SpawnNPC(npc.m_LOT, npc.m_Name, location.m_Position, location.m_Rotation, npc.m_Equipment, npc.ldf); - if (!npc.m_Phrases.empty()) { - npcEntity->SetVar>(u"chats", m_PartyPhrases); - SetupNPCTalk(npcEntity); + if (npc.m_LOT == 176){ + npc.m_ID = SpawnSpawner(npc.m_LOT, location.m_Position, location.m_Rotation, npc.ldf); + } else { + auto* npcEntity = SpawnNPC(npc.m_LOT, npc.m_Name, location.m_Position, location.m_Rotation, npc.m_Equipment, npc.ldf); + if (!npc.m_Phrases.empty()) { + npcEntity->SetVar>(u"chats", m_PartyPhrases); + SetupNPCTalk(npcEntity); + } } } - return; } @@ -111,23 +134,28 @@ void VanityUtilities::SpawnVanity() { new LDFData(u"custom_script_client", u"scripts\\ai\\SPEC\\MISSION_MINIGAME_CLIENT.lua") }; } + if (npc.m_LOT == 176){ + npc.m_ID = SpawnSpawner(npc.m_LOT, location.m_Position, location.m_Rotation, npc.ldf); + } else { + // Spawn the NPC + auto* npcEntity = SpawnNPC(npc.m_LOT, npc.m_Name, location.m_Position, location.m_Rotation, npc.m_Equipment, npc.ldf); + if (!npcEntity) continue; + npc.m_ID = npcEntity->GetObjectID(); + if (!npc.m_Phrases.empty()){ + npcEntity->SetVar>(u"chats", npc.m_Phrases); - // Spawn the NPC - auto* npcEntity = SpawnNPC(npc.m_LOT, npc.m_Name, location.m_Position, location.m_Rotation, npc.m_Equipment, npc.ldf); - if (!npc.m_Phrases.empty()){ - npcEntity->SetVar>(u"chats", npc.m_Phrases); - - auto* scriptComponent = npcEntity->GetComponent(); + auto* scriptComponent = npcEntity->GetComponent(); - if (scriptComponent && !npc.m_Script.empty()) { - scriptComponent->SetScript(npc.m_Script); - scriptComponent->SetSerialized(false); + if (scriptComponent && !npc.m_Script.empty()) { + scriptComponent->SetScript(npc.m_Script); + scriptComponent->SetSerialized(false); - for (const auto& npc : npc.m_Flags) { - npcEntity->SetVar(GeneralUtils::ASCIIToUTF16(npc.first), npc.second); + for (const auto& npc : npc.m_Flags) { + npcEntity->SetVar(GeneralUtils::ASCIIToUTF16(npc.first), npc.second); + } } + SetupNPCTalk(npcEntity); } - SetupNPCTalk(npcEntity); } } @@ -149,8 +177,21 @@ void VanityUtilities::SpawnVanity() { } } -Entity* VanityUtilities::SpawnNPC(LOT lot, const std::string& name, const NiPoint3& position, - const NiQuaternion& rotation, const std::vector& inventory, const std::vector& ldf) { +LWOOBJID VanityUtilities::SpawnSpawner(LOT lot, const NiPoint3& position, const NiQuaternion& rotation, const std::vector& ldf){ + SceneObject obj; + obj.lot = lot; + // guratantee we have no collisions + do { + obj.id = ObjectIDManager::Instance()->GenerateObjectID(); + } while(Game::zoneManager->GetSpawner(obj.id)); + obj.position = position; + obj.rotation = rotation; + obj.settings = ldf; + Level::MakeSpawner(obj); + return obj.id; +} + +Entity* VanityUtilities::SpawnNPC(LOT lot, const std::string& name, const NiPoint3& position, const NiQuaternion& rotation, const std::vector& inventory, const std::vector& ldf) { EntityInfo info; info.lot = lot; info.pos = position; diff --git a/dGame/dUtilities/VanityUtilities.h b/dGame/dUtilities/VanityUtilities.h index 0cca0abaf..cff73bce2 100644 --- a/dGame/dUtilities/VanityUtilities.h +++ b/dGame/dUtilities/VanityUtilities.h @@ -13,6 +13,7 @@ struct VanityNPCLocation struct VanityNPC { + LWOOBJID m_ID = LWOOBJID_EMPTY; std::string m_Name; LOT m_LOT; std::vector m_Equipment; @@ -44,6 +45,13 @@ class VanityUtilities const std::vector& ldf ); + static LWOOBJID SpawnSpawner( + LOT lot, + const NiPoint3& position, + const NiQuaternion& rotation, + const std::vector& ldf + ); + static std::string ParseMarkdown( const std::string& file ); diff --git a/dWorldServer/PerformanceManager.cpp b/dWorldServer/PerformanceManager.cpp index 19f38d00a..248be54af 100644 --- a/dWorldServer/PerformanceManager.cpp +++ b/dWorldServer/PerformanceManager.cpp @@ -1,5 +1,6 @@ #include "PerformanceManager.h" - +#include "CDZoneTableTable.h" +#include "CDClientManager.h" #include "UserManager.h" #define SOCIAL { lowFrameDelta } @@ -68,11 +69,30 @@ std::map PerformanceManager::m_Profiles = { }; void PerformanceManager::SelectProfile(LWOMAPID mapID) { - const auto pair = m_Profiles.find(mapID); + // Try to get it from zoneTable + CDZoneTableTable* zoneTable = CDClientManager::Instance().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; + } + } + } + // Fall back to hardcoded list and defaults + const auto pair = m_Profiles.find(mapID); if (pair == m_Profiles.end()) { m_CurrentProfile = m_DefaultProfile; - return; } diff --git a/dZoneManager/Level.cpp b/dZoneManager/Level.cpp index 953b11e22..4c83d494a 100644 --- a/dZoneManager/Level.cpp +++ b/dZoneManager/Level.cpp @@ -38,6 +38,76 @@ Level::~Level() { } } +void Level::MakeSpawner(SceneObject obj){ + SpawnerInfo spawnInfo = SpawnerInfo(); + SpawnerNode* node = new SpawnerNode(); + spawnInfo.templateID = obj.lot; + spawnInfo.spawnerID = obj.id; + spawnInfo.templateScale = obj.scale; + node->position = obj.position; + node->rotation = obj.rotation; + node->config = obj.settings; + spawnInfo.nodes.push_back(node); + for (LDFBaseData* data : obj.settings) { + if (data) { + if (data->GetKey() == u"spawntemplate") { + spawnInfo.templateID = std::stoi(data->GetValueAsString()); + } + + if (data->GetKey() == u"spawner_node_id") { + node->nodeID = std::stoi(data->GetValueAsString()); + } + + if (data->GetKey() == u"spawner_name") { + spawnInfo.name = data->GetValueAsString(); + } + + if (data->GetKey() == u"max_to_spawn") { + spawnInfo.maxToSpawn = std::stoi(data->GetValueAsString()); + } + + if (data->GetKey() == u"spawner_active_on_load") { + spawnInfo.activeOnLoad = std::stoi(data->GetValueAsString()); + } + + if (data->GetKey() == u"active_on_load") { + spawnInfo.activeOnLoad = std::stoi(data->GetValueAsString()); + } + + if (data->GetKey() == u"respawn") { + if (data->GetValueType() == eLDFType::LDF_TYPE_FLOAT) // Floats are in seconds + { + spawnInfo.respawnTime = std::stof(data->GetValueAsString()); + } else if (data->GetValueType() == eLDFType::LDF_TYPE_U32) // Ints are in ms? + { + spawnInfo.respawnTime = std::stoul(data->GetValueAsString()) / 1000; + } + } + if (data->GetKey() == u"spawnsGroupOnSmash") { + spawnInfo.spawnsOnSmash = std::stoi(data->GetValueAsString()); + } + if (data->GetKey() == u"spawnNetNameForSpawnGroupOnSmash") { + spawnInfo.spawnOnSmashGroupName = data->GetValueAsString(); + } + if (data->GetKey() == u"groupID") { // Load object groups + std::string groupStr = data->GetValueAsString(); + spawnInfo.groups = GeneralUtils::SplitString(groupStr, ';'); + spawnInfo.groups.erase(spawnInfo.groups.end() - 1); + } + if (data->GetKey() == u"no_auto_spawn") { + spawnInfo.noAutoSpawn = static_cast*>(data)->GetValue(); + } + if (data->GetKey() == u"no_timed_spawn") { + spawnInfo.noTimedSpawn = static_cast*>(data)->GetValue(); + } + if (data->GetKey() == u"spawnActivator") { + spawnInfo.spawnActivator = static_cast*>(data)->GetValue(); + } + } + } + Game::zoneManager->MakeSpawner(spawnInfo); +} + const void Level::PrintAllObjects() { for (std::map::iterator it = m_ChunkHeaders.begin(); it != m_ChunkHeaders.end(); ++it) { if (it->second.id == Level::ChunkTypeID::SceneObjectData) { @@ -230,74 +300,7 @@ void Level::ReadSceneObjectDataChunk(std::istream& file, Header& header) { } if (obj.lot == 176) { //Spawner - SpawnerInfo spawnInfo = SpawnerInfo(); - SpawnerNode* node = new SpawnerNode(); - spawnInfo.templateID = obj.lot; - spawnInfo.spawnerID = obj.id; - spawnInfo.templateScale = obj.scale; - node->position = obj.position; - node->rotation = obj.rotation; - node->config = obj.settings; - spawnInfo.nodes.push_back(node); - for (LDFBaseData* data : obj.settings) { - if (data) { - if (data->GetKey() == u"spawntemplate") { - spawnInfo.templateID = std::stoi(data->GetValueAsString()); - } - - if (data->GetKey() == u"spawner_node_id") { - node->nodeID = std::stoi(data->GetValueAsString()); - } - - if (data->GetKey() == u"spawner_name") { - spawnInfo.name = data->GetValueAsString(); - } - - if (data->GetKey() == u"max_to_spawn") { - spawnInfo.maxToSpawn = std::stoi(data->GetValueAsString()); - } - - if (data->GetKey() == u"spawner_active_on_load") { - spawnInfo.activeOnLoad = std::stoi(data->GetValueAsString()); - } - - if (data->GetKey() == u"active_on_load") { - spawnInfo.activeOnLoad = std::stoi(data->GetValueAsString()); - } - - if (data->GetKey() == u"respawn") { - if (data->GetValueType() == eLDFType::LDF_TYPE_FLOAT) // Floats are in seconds - { - spawnInfo.respawnTime = std::stof(data->GetValueAsString()); - } else if (data->GetValueType() == eLDFType::LDF_TYPE_U32) // Ints are in ms? - { - spawnInfo.respawnTime = std::stoul(data->GetValueAsString()) / 1000; - } - } - if (data->GetKey() == u"spawnsGroupOnSmash") { - spawnInfo.spawnsOnSmash = std::stoi(data->GetValueAsString()); - } - if (data->GetKey() == u"spawnNetNameForSpawnGroupOnSmash") { - spawnInfo.spawnOnSmashGroupName = data->GetValueAsString(); - } - if (data->GetKey() == u"groupID") { // Load object groups - std::string groupStr = data->GetValueAsString(); - spawnInfo.groups = GeneralUtils::SplitString(groupStr, ';'); - spawnInfo.groups.erase(spawnInfo.groups.end() - 1); - } - if (data->GetKey() == u"no_auto_spawn") { - spawnInfo.noAutoSpawn = static_cast*>(data)->GetValue(); - } - if (data->GetKey() == u"no_timed_spawn") { - spawnInfo.noTimedSpawn = static_cast*>(data)->GetValue(); - } - if (data->GetKey() == u"spawnActivator") { - spawnInfo.spawnActivator = static_cast*>(data)->GetValue(); - } - } - } - Spawner* spawner = new Spawner(spawnInfo); - Game::zoneManager->AddSpawner(obj.id, spawner); + MakeSpawner(obj); } else { //Regular object EntityInfo info; info.spawnerID = 0; diff --git a/dZoneManager/Level.h b/dZoneManager/Level.h index 0f8fa72d6..a27326a21 100644 --- a/dZoneManager/Level.h +++ b/dZoneManager/Level.h @@ -53,6 +53,8 @@ class Level { public: Level(Zone* parentZone, const std::string& filepath); ~Level(); + + static void MakeSpawner(SceneObject obj); const void PrintAllObjects(); diff --git a/dZoneManager/dZoneManager.cpp b/dZoneManager/dZoneManager.cpp index c87619cd5..137a9cab2 100644 --- a/dZoneManager/dZoneManager.cpp +++ b/dZoneManager/dZoneManager.cpp @@ -40,6 +40,9 @@ void dZoneManager::Initialize(const LWOZONEID& zoneID) { 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; } } diff --git a/dZoneManager/dZoneManager.h b/dZoneManager/dZoneManager.h index 1e08b0085..b5db3f6e0 100644 --- a/dZoneManager/dZoneManager.h +++ b/dZoneManager/dZoneManager.h @@ -41,6 +41,9 @@ class dZoneManager { 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; } uint32_t GetUniqueMissionIdStartingValue(); bool CheckIfAccessibleZone(LWOMAPID zoneID); @@ -58,7 +61,10 @@ class dZoneManager { Zone* m_pZone = nullptr; LWOZONEID m_ZoneID; - bool m_PlayerLoseCoinsOnDeath; //Do players drop coins in this zone when smashed + bool m_PlayerLoseCoinsOnDeath = false; + bool m_DisableSaveLocation = false; + bool m_MountsAllowed = true; + bool m_PetsAllowed = true; std::map m_Spawners; WorldConfig* m_WorldConfig = nullptr; diff --git a/docs/Commands.md b/docs/Commands.md index ea81ff569..71a3da432 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -25,6 +25,7 @@ |ban|`/ban `|Bans a user from the server.|4| |approveproperty|`/approveproperty`|Approves the property the player is currently visiting.|5| |mute|`/mute (days) (hours)`|Mute player for the given amount of time. If no time is given, the mute is indefinite.|6| +|attackimmune|`/attackimmune `|Sets the character's immunity to basic attacks state, where value can be one of "1", to make yourself immune to basic attack damage, or "0" to undo.|8| |gmimmune|`/gmimmunve `|Sets the character's GMImmune state, where value can be one of "1", to make yourself immune to damage, or "0" to undo.|8| |gminvis|`/gminvis`|Toggles invisibility for the character, though it's currently a bit buggy. Requires nonzero GM Level for the character, but the account must have a GM level of 8.|8| |setname|`/setname `|Sets a temporary name for your player. The name resets when you log out.|8| diff --git a/tests/dGameTests/dComponentsTests/DestroyableComponentTests.cpp b/tests/dGameTests/dComponentsTests/DestroyableComponentTests.cpp index acb903520..ff37f1548 100644 --- a/tests/dGameTests/dComponentsTests/DestroyableComponentTests.cpp +++ b/tests/dGameTests/dComponentsTests/DestroyableComponentTests.cpp @@ -536,3 +536,22 @@ TEST_F(DestroyableTest, DestroyableComponentImmunityTest) { } +/** + * Test the Damage cooldown timer of DestroyableComponent + */ +TEST_F(DestroyableTest, DestroyableComponentDamageCooldownTest) { + // Test the damage immune timer state (anything above 0.0f) + destroyableComponent->SetDamageCooldownTimer(1.0f); + EXPECT_FLOAT_EQ(destroyableComponent->GetDamageCooldownTimer(), 1.0f); + ASSERT_TRUE(destroyableComponent->IsCooldownImmune()); + + // Test that the Update() function correctly decrements the damage cooldown timer + destroyableComponent->Update(0.5f); + EXPECT_FLOAT_EQ(destroyableComponent->GetDamageCooldownTimer(), 0.5f); + ASSERT_TRUE(destroyableComponent->IsCooldownImmune()); + + // Test the non damage immune timer state (anything below or equal to 0.0f) + destroyableComponent->SetDamageCooldownTimer(0.0f); + EXPECT_FLOAT_EQ(destroyableComponent->GetDamageCooldownTimer(), 0.0f); + ASSERT_FALSE(destroyableComponent->IsCooldownImmune()); +}