diff --git a/dCommon/Flag.h b/dCommon/Flag.h new file mode 100644 index 000000000..7353d9c9c --- /dev/null +++ b/dCommon/Flag.h @@ -0,0 +1,160 @@ +#pragma once +#include "GeneralUtils.h" + +/** + * A bitset flag class capable of compile-time bounds-checking. + * It should be possible to unify its methods with C++23, but until then + * use the compile-time overloads preferentially. +*/ +template + requires std::is_enum_v +class Flag { +public: + // Type aliases + using type = T; + using underlying_type = std::underlying_type_t; + + // Static constants + static constexpr auto MAX_FLAG_VAL = sizeof(underlying_type) * CHAR_BIT; + + // Constructors + Flag() = default; + + // Run-time constructor + constexpr Flag(const T value) : m_Flags{ ConvertFlag(value) } {} + + /** + * RUNTIME: Sets one or more flags + * @param flag Flag(s) to set + */ + template ... varArg> + constexpr void Set(const varArg... flag) noexcept { + m_Flags |= (ConvertFlag(flag) | ...); + } + + /** + * COMPILETIME: Sets one or more flags + * @param flag Flag(s) to set + */ + template + constexpr void Set() noexcept { + m_Flags |= (ConvertFlag() | ...); + } + + /** + * RUNTIME: Sets ONLY have the specified flag(s), clearing all others + * @param flag Flag(s) to set exclusively + */ + template ... varArg> + constexpr void Reset(const varArg... flag) { + m_Flags = (ConvertFlag(flag) | ...); + } + + /** + * COMPILETIME: Sets ONLY have the specified flag(s), clearing all others + * @param flag Flag(s) to set exclusively + */ + template + constexpr void Reset() noexcept { + m_Flags = (ConvertFlag() | ...); + } + + /** + * RUNTIME: Unsets one or more flags + * @param flag Flag(s) to unset + */ + template ... varArg> + constexpr void Unset(const varArg... flag) { + m_Flags &= ~(ConvertFlag(flag) | ...); + } + + /** + * COMPILETIME: Unsets one or more flags + * @param flag Flag(s) to unset + */ + template + constexpr void Unset() noexcept { + m_Flags &= ~(ConvertFlag() | ...); + } + + /** + * RUNTIME: Returns true all the specified flag(s) are set + * @param flag Flag(s) to check + */ + template ... varArg> + [[nodiscard]] + constexpr bool Has(const varArg... flag) const { + return (m_Flags & (ConvertFlag(flag) | ...)) == (ConvertFlag(flag) | ...); + } + + /** + * COMPILETIME: Returns true if all the specified flag(s) are set + * @param flag Flag(s) to check + */ + template + [[nodiscard]] + constexpr bool Has() const noexcept { + return (m_Flags & (ConvertFlag() | ...)) == (ConvertFlag() | ...); + } + + /** + * RUNTIME: Returns true if ONLY the specified flag(s) are set + * @param flag Flag(s) to check + */ + template ... varArg> + [[nodiscard]] + constexpr bool HasOnly(const varArg... flag) const { + return m_Flags == (ConvertFlag(flag) | ...); + } + + /** + * COMPILETIME: Returns true if ONLY the specified flag(s) are set + * @param flag Flag(s) to check + */ + template + [[nodiscard]] + constexpr bool HasOnly() const noexcept { + return m_Flags == (ConvertFlag() | ...); + } + + /** + * @return the raw value of the flags set + */ + [[nodiscard]] + constexpr T Value() const noexcept { + return static_cast(m_Flags); + } + + /** + * Operator overload to allow for '=' assignment + */ + constexpr Flag& operator=(const T value) { + m_Flags = ConvertFlag(value); + return *this; + } + +private: + template + [[nodiscard]] + static consteval underlying_type ConvertFlag() noexcept { + constexpr auto flag_val = GeneralUtils::ToUnderlying(flag); + static_assert(flag_val <= MAX_FLAG_VAL, "Flag value is too large to set!"); + + return flag_val != 0 ? 1 << flag_val : flag_val; + } + + [[nodiscard]] + static constexpr underlying_type ConvertFlag(const T flag) { + auto flag_val = GeneralUtils::ToUnderlying(flag); + + // This is less-efficient than the compile-time check, but still works + // We can probably unify this and the above functions with C++23 and 'if consteval' + if (flag_val > MAX_FLAG_VAL) { + throw std::runtime_error{ "Flag value is too large to set!" }; + } + + return flag_val != 0 ? 1 << flag_val : flag_val; + } + + underlying_type m_Flags; +}; diff --git a/dCommon/dEnums/eHelpType.h b/dCommon/dEnums/eHelpType.h index d1838cc67..6f6af1727 100644 --- a/dCommon/dEnums/eHelpType.h +++ b/dCommon/dEnums/eHelpType.h @@ -1,9 +1,10 @@ - #ifndef __EHELPTYPE__H__ #define __EHELPTYPE__H__ #include +#include "magic_enum.hpp" + enum class eHelpType : int32_t { NONE = 0, UNLOCK_MINIMAP = 2, @@ -38,4 +39,10 @@ enum class eHelpType : int32_t { UI_INVENTORY_FULL_CANNOT_PICKUP_ITEM = 86 }; +template <> +struct magic_enum::customize::enum_range { + static constexpr int min = 0; + static constexpr int max = 86; +}; + #endif //!__EHELPTYPE__H__ diff --git a/dCommon/dEnums/ePetAbilityType.h b/dCommon/dEnums/ePetAbilityType.h index 0cc6d6bd7..d5d8121d7 100644 --- a/dCommon/dEnums/ePetAbilityType.h +++ b/dCommon/dEnums/ePetAbilityType.h @@ -1,5 +1,5 @@ -#ifndef __EPETABILITYTYPE__H__ -#define __EPETABILITYTYPE__H__ +#ifndef EPETABILITYTYPE_H +#define EPETABILITYTYPE_H #include @@ -10,4 +10,4 @@ enum class ePetAbilityType : uint32_t { DigAtPosition }; -#endif //!__EPETABILITYTYPE__H__ +#endif //!EPETABILITYTYPE_H diff --git a/dDatabase/CDClientDatabase/CDClientManager.cpp b/dDatabase/CDClientDatabase/CDClientManager.cpp index 6ecfb0ad0..ee72e3845 100644 --- a/dDatabase/CDClientDatabase/CDClientManager.cpp +++ b/dDatabase/CDClientDatabase/CDClientManager.cpp @@ -20,6 +20,7 @@ #include "CDMissionsTable.h" #include "CDObjectSkillsTable.h" #include "CDObjectsTable.h" +#include "CDPetAbilitiesTable.h" #include "CDPhysicsComponentTable.h" #include "CDRebuildComponentTable.h" #include "CDScriptComponentTable.h" @@ -89,6 +90,7 @@ DEFINE_TABLE_STORAGE(CDObjectSkillsTable); DEFINE_TABLE_STORAGE(CDObjectsTable); DEFINE_TABLE_STORAGE(CDPhysicsComponentTable); DEFINE_TABLE_STORAGE(CDPackageComponentTable); +DEFINE_TABLE_STORAGE(CDPetAbilitiesTable); DEFINE_TABLE_STORAGE(CDPetComponentTable); DEFINE_TABLE_STORAGE(CDProximityMonitorComponentTable); DEFINE_TABLE_STORAGE(CDPropertyEntranceComponentTable); @@ -136,6 +138,7 @@ void CDClientManager::LoadValuesFromDatabase() { CDCLIENT_DONT_CACHE_TABLE(CDObjectsTable::Instance().LoadValuesFromDatabase()); CDPhysicsComponentTable::Instance().LoadValuesFromDatabase(); CDPackageComponentTable::Instance().LoadValuesFromDatabase(); + CDPetAbilitiesTable::Instance().LoadValuesFromDatabase(); CDPetComponentTable::Instance().LoadValuesFromDatabase(); CDProximityMonitorComponentTable::Instance().LoadValuesFromDatabase(); CDPropertyEntranceComponentTable::Instance().LoadValuesFromDatabase(); @@ -155,5 +158,6 @@ void CDClientManager::LoadValuesFromDatabase() { void CDClientManager::LoadValuesFromDefaults() { LOG("Loading default CDClient tables!"); + CDPetAbilitiesTable::Instance().LoadValuesFromDefaults(); CDPetComponentTable::Instance().LoadValuesFromDefaults(); } diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDPetAbilitiesTable.cpp b/dDatabase/CDClientDatabase/CDClientTables/CDPetAbilitiesTable.cpp new file mode 100644 index 000000000..50f8a7171 --- /dev/null +++ b/dDatabase/CDClientDatabase/CDClientTables/CDPetAbilitiesTable.cpp @@ -0,0 +1,43 @@ +#include "CDPetAbilitiesTable.h" +#include "ePetAbilityType.h" + +namespace { + // Default entries for fallback + CDPetAbilities defaultEntry{ + .id = ePetAbilityType::Invalid, + UNUSED_ENTRY(.AbilityName = "invalid",) + .imaginationCost = 0, + UNUSED_ENTRY(.locStatus = 2,) + }; +} + +void CDPetAbilitiesTable::LoadValuesFromDatabase() { + auto tableData = CDClientDatabase::ExecuteQuery("SELECT * FROM PetAbilities"); + auto& entries = GetEntriesMutable(); + while (!tableData.eof()) { + const auto abilityId = + static_cast(tableData.getIntField("id", static_cast(defaultEntry.id))); + auto& entry = entries[abilityId]; + + entry.id = abilityId; + UNUSED_COLUMN(entry.abilityName = tableData.getStringField("AbilityName", defaultEntry.abilityName)); + entry.imaginationCost = tableData.getIntField("ImaginationCost", defaultEntry.imaginationCost); + UNUSED_COLUMN(entry.locStatus = tableData.getIntField("locStatus", defaultEntry.locStatus)); + + tableData.nextRow(); + } +} + +void CDPetAbilitiesTable::LoadValuesFromDefaults() { + GetEntriesMutable().emplace(defaultEntry.id, defaultEntry); +} + +const CDPetAbilities& CDPetAbilitiesTable::GetByID(const ePetAbilityType id) { + const auto& entries = GetEntries(); + const auto itr = entries.find(id); + if (itr == entries.cend()) { + LOG("Unable to load pet ability (ID %i) values from database! Using default values instead.", id); + return defaultEntry; + } + return itr->second; +} diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDPetAbilitiesTable.h b/dDatabase/CDClientDatabase/CDClientTables/CDPetAbilitiesTable.h new file mode 100644 index 000000000..6a0d9dc50 --- /dev/null +++ b/dDatabase/CDClientDatabase/CDClientTables/CDPetAbilitiesTable.h @@ -0,0 +1,34 @@ +#pragma once +#include "CDTable.h" +#include +#include + +// Forward declarations +enum class ePetAbilityType : uint32_t; + +struct CDPetAbilities { + ePetAbilityType id; + UNUSED_COLUMN(std::string abilityName;) + int32_t imaginationCost; + UNUSED_COLUMN(uint32_t locStatus;) +}; + +class CDPetAbilitiesTable : public CDTable> { +public: + + /** + * Load values from the CD client database + */ + void LoadValuesFromDatabase(); + + /** + * Load the default values into memory instead of attempting to connect to the CD client database + */ + void LoadValuesFromDefaults(); + + /** + * Gets the pet ability table corresponding to the pet ability ID + * @returns A pointer to the corresponding table, or nullptr if one could not be found + */ + const CDPetAbilities& GetByID(const ePetAbilityType id); +}; diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDPetComponentTable.cpp b/dDatabase/CDClientDatabase/CDClientTables/CDPetComponentTable.cpp index 80c101122..96d4550c0 100644 --- a/dDatabase/CDClientDatabase/CDClientTables/CDPetComponentTable.cpp +++ b/dDatabase/CDClientDatabase/CDClientTables/CDPetComponentTable.cpp @@ -53,10 +53,10 @@ void CDPetComponentTable::LoadValuesFromDefaults() { GetEntriesMutable().emplace(defaultEntry.id, defaultEntry); } -CDPetComponent& CDPetComponentTable::GetByID(const uint32_t componentID) { - auto& entries = GetEntriesMutable(); - auto itr = entries.find(componentID); - if (itr == entries.end()) { +const CDPetComponent& CDPetComponentTable::GetByID(const uint32_t componentID) { + const auto& entries = GetEntries(); + const auto itr = entries.find(componentID); + if (itr == entries.cend()) { LOG("Unable to load pet component (ID %i) values from database! Using default values instead.", componentID); return defaultEntry; } diff --git a/dDatabase/CDClientDatabase/CDClientTables/CDPetComponentTable.h b/dDatabase/CDClientDatabase/CDClientTables/CDPetComponentTable.h index 428902534..1af2137aa 100644 --- a/dDatabase/CDClientDatabase/CDClientTables/CDPetComponentTable.h +++ b/dDatabase/CDClientDatabase/CDClientTables/CDPetComponentTable.h @@ -28,7 +28,7 @@ class CDPetComponentTable : public CDTableGetObjectID()); if (current != nullptr) { - current->Deactivate(); + current->Deactivate(eHelpType::PET_DESPAWN_BY_OWNER_HIBERNATE); if (current->GetDatabaseId() == item->GetSubKey()) { return; @@ -1253,8 +1253,8 @@ void InventoryComponent::SpawnPet(Item* item) { Game::entityManager->ConstructEntity(pet); } -void InventoryComponent::SetDatabasePet(LWOOBJID id, const DatabasePet& data) { - m_Pets.insert_or_assign(id, data); +void InventoryComponent::SetDatabasePet(LWOOBJID id, DatabasePet&& data) { + m_Pets.insert_or_assign(id, std::move(data)); } const DatabasePet& InventoryComponent::GetDatabasePet(LWOOBJID id) const { @@ -1580,12 +1580,13 @@ void InventoryComponent::LoadPetXml(const tinyxml2::XMLDocument& document) { petElement->QueryAttribute("m", &moderationStatus); const char* name = petElement->Attribute("n"); - DatabasePet databasePet; - databasePet.lot = lot; - databasePet.moderationState = moderationStatus; - databasePet.name = std::string(name); + auto databasePet = DatabasePet{ + .lot = lot, + .name = std::string(name), + .moderationState = moderationStatus, + }; - SetDatabasePet(id, databasePet); + SetDatabasePet(id, std::move(databasePet)); petElement = petElement->NextSiblingElement(); } diff --git a/dGame/dComponents/InventoryComponent.h b/dGame/dComponents/InventoryComponent.h index 0055fcea7..46ddf7750 100644 --- a/dGame/dComponents/InventoryComponent.h +++ b/dGame/dComponents/InventoryComponent.h @@ -339,7 +339,7 @@ class InventoryComponent final : public Component { * @param id the id of the pet to find * @param data the data to store on the pet */ - void SetDatabasePet(LWOOBJID id, const DatabasePet& data); + void SetDatabasePet(LWOOBJID id, DatabasePet&& data); /** * Returns the database pet information for an object diff --git a/dGame/dComponents/PetComponent.cpp b/dGame/dComponents/PetComponent.cpp index c2783a15f..f74a9aa8e 100644 --- a/dGame/dComponents/PetComponent.cpp +++ b/dGame/dComponents/PetComponent.cpp @@ -2,6 +2,8 @@ #include "GameMessages.h" #include "BrickDatabase.h" #include "CDClientDatabase.h" +#include "CDPetAbilitiesTable.h" +#include "CDPetComponentTable.h" #include "CDTamingBuildPuzzleTable.h" #include "ChatPackets.h" #include "EntityManager.h" @@ -18,12 +20,15 @@ #include "eUnequippableActiveType.h" #include "eTerminateType.h" #include "ePetTamingNotifyType.h" +#include "ePetAbilityType.h" #include "eUseItemResponse.h" #include "ePlayerFlag.h" +#include "eHelpType.h" #include "Game.h" #include "dConfig.h" #include "dChatFilter.h" +#include "dZoneManager.h" #include "Database.h" #include "EntityInfo.h" #include "eMissionTaskType.h" @@ -72,44 +77,40 @@ const std::map PetComponent::petFlags{ { 13067, 838 }, // Skeleton dragon }; -PetComponent::PetComponent(Entity* parentEntity, uint32_t componentId) : Component{ parentEntity } { +PetComponent::PetComponent(Entity* parentEntity, uint32_t componentId) + : Component{ parentEntity } + , m_Flags{ PetFlag::SPAWNING } +{ m_PetInfo = CDClientManager::GetTable()->GetByID(componentId); // TODO: Make reference when safe m_ComponentId = componentId; - - m_Interaction = LWOOBJID_EMPTY; - m_Owner = LWOOBJID_EMPTY; - m_ModerationStatus = 0; - m_Tamer = LWOOBJID_EMPTY; - m_ModelId = LWOOBJID_EMPTY; - m_Timer = 0; - m_TimerAway = 0; - m_DatabaseId = LWOOBJID_EMPTY; - m_Status = 67108866; // Tamable - m_Ability = ePetAbilityType::Invalid; - m_StartPosition = NiPoint3Constant::ZERO; + m_StartPosition = m_Parent->GetPosition(); m_MovementAI = nullptr; - m_TreasureTime = 0; + + m_ReadyToInteract = false; + m_State = PetAiState::spawn; + SetIsHandlingInteraction(false); std::string checkPreconditions = GeneralUtils::UTF16ToWTF8(parentEntity->GetVar(u"CheckPrecondition")); + m_Preconditions = + checkPreconditions.empty() ? std::nullopt : std::make_optional(PreconditionExpression(checkPreconditions)); - if (!checkPreconditions.empty()) { - SetPreconditions(checkPreconditions); - } + m_FollowRadius = 8.0f; //Game::zoneManager->GetPetFollowRadius(); // TODO: FIX THIS TO LOAD DYNAMICALLY } void PetComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) { const bool tamed = m_Owner != LWOOBJID_EMPTY; - outBitStream.Write1(); // Always serialize as dirty for now + constexpr bool isDirty = true; + outBitStream.Write(isDirty); // Always serialize as dirty for now - outBitStream.Write(m_Status); - outBitStream.Write(tamed ? m_Ability : ePetAbilityType::Invalid); // Something with the overhead icon? + outBitStream.Write(m_Flags.Value()); + outBitStream.Write(tamed ? m_Interaction.ability : ePetAbilityType::Invalid); // Something with the overhead icon? - const bool interacting = m_Interaction != LWOOBJID_EMPTY; + const bool interacting = m_Interaction.obj != LWOOBJID_EMPTY; outBitStream.Write(interacting); if (interacting) { - outBitStream.Write(m_Interaction); + outBitStream.Write(m_Interaction.obj); } outBitStream.Write(tamed); @@ -139,31 +140,102 @@ void PetComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpd } void PetComponent::OnUse(Entity* originator) { - if (m_Owner != LWOOBJID_EMPTY) { - return; + LOG_DEBUG("PET USE!"); + + if (IsReadyToInteract()) { + switch (m_Interaction.ability) { + case ePetAbilityType::DigAtPosition: // Treasure dig + StartInteractTreasureDig(); + break; + + case ePetAbilityType::JumpOnObject: // Bouncer + StartInteractBouncer(); + break; + + default: + LOG_DEBUG( + "Unhandled pet interaction - Owner: %ld, ePetAbilityType %d", + m_Owner, + GeneralUtils::ToUnderlying(m_Interaction.ability) + ); + break; + } + } else { + StartTamingMinigame(originator); } +} - if (m_Tamer != LWOOBJID_EMPTY) { - auto* tamer = Game::entityManager->GetEntity(m_Tamer); +void PetComponent::Update(float deltaTime) { + // Update timers + m_TimerBounce -= deltaTime; + + if (m_Timer > 0) { + m_Timer -= deltaTime; + return; + } - if (tamer != nullptr) { + // Remove "left behind" pets and handle failing pet taming minigame + if (m_Owner != LWOOBJID_EMPTY) { + const Entity* const owner = GetOwner(); + if (!owner) { + m_Parent->Kill(); return; } + } else { + ClientFailTamingMinigame(); // TODO: This is not despawning the built model correctly + } + + if (m_Flags.Has()) OnSpawn(); + + // Handle pet AI states + switch (m_State) { + case PetAiState::idle: + Wander(); + break; + + case PetAiState::follow: + OnFollow(deltaTime); + break; + + case PetAiState::goToObj: + if (m_MovementAI->AtFinalWaypoint()) { + LOG_DEBUG("Reached object!"); + m_MovementAI->Stop(); + SetPetAiState(PetAiState::interact); + } else { + m_Timer += 0.5f; + } + break; + + case PetAiState::interact: + OnInteract(); + break; + default: + LOG_DEBUG("Unknown state: %d!", m_Flags); + break; + } +} + +void PetComponent::StartTamingMinigame(Entity* originator) { + // The minigame logic beneath this comment should be rewritten... eventually + if (m_Owner != LWOOBJID_EMPTY) return; + + if (m_Tamer != LWOOBJID_EMPTY) { + const auto* const tamer = Game::entityManager->GetEntity(m_Tamer); + if (tamer != nullptr) return; m_Tamer = LWOOBJID_EMPTY; } auto* const inventoryComponent = originator->GetComponent(); - if (inventoryComponent == nullptr) { - return; - } + if (!inventoryComponent) return; if (m_Preconditions.has_value() && !m_Preconditions->Check(originator, true)) { return; } auto* const movementAIComponent = m_Parent->GetComponent(); - if (movementAIComponent != nullptr) { + if (movementAIComponent) { movementAIComponent->Stop(); } @@ -176,7 +248,7 @@ void PetComponent::OnUse(Entity* originator) { } const auto* const destroyableComponent = originator->GetComponent(); - if (destroyableComponent == nullptr) { + if (!destroyableComponent) { return; } @@ -194,7 +266,6 @@ void PetComponent::OnUse(Entity* originator) { } const auto petPosition = m_Parent->GetPosition(); - const auto originatorPosition = originator->GetPosition(); m_Parent->SetRotation(NiQuaternion::LookAt(petPosition, originatorPosition)); @@ -205,22 +276,17 @@ void PetComponent::OnUse(Entity* originator) { } auto position = originatorPosition; - - NiPoint3 forward = NiQuaternion::LookAt(m_Parent->GetPosition(), originator->GetPosition()).GetForwardVector(); + auto forward = NiQuaternion::LookAt(m_Parent->GetPosition(), originator->GetPosition()).GetForwardVector(); forward.y = 0; if (dpWorld::IsLoaded()) { - NiPoint3 attempt = petPosition + forward * interactionDistance; - - NiPoint3 nearestPoint = dpWorld::GetNavMesh()->NearestPoint(attempt); + auto attempt = petPosition + forward * interactionDistance; + auto nearestPoint = dpWorld::GetNavMesh()->NearestPoint(attempt); while (std::abs(nearestPoint.y - petPosition.y) > 4 && interactionDistance > 10) { - const NiPoint3 forward = m_Parent->GetRotation().GetForwardVector(); - + const auto forward = m_Parent->GetRotation().GetForwardVector(); attempt = originatorPosition + forward * interactionDistance; - nearestPoint = dpWorld::GetNavMesh()->NearestPoint(attempt); - interactionDistance -= 0.5f; } @@ -229,7 +295,7 @@ void PetComponent::OnUse(Entity* originator) { position = petPosition + forward * interactionDistance; } - auto rotation = NiQuaternion::LookAt(position, petPosition); + const auto rotation = NiQuaternion::LookAt(position, petPosition); GameMessages::SendNotifyPetTamingMinigame( originator->GetObjectID(), @@ -258,202 +324,37 @@ void PetComponent::OnUse(Entity* originator) { GameMessages::SendNotifyPetTamingPuzzleSelected(originator->GetObjectID(), bricks, originator->GetSystemAddress()); m_Tamer = originator->GetObjectID(); - SetStatus(5); - Game::entityManager->SerializeEntity(m_Parent); + m_Flags.Set(); currentActivities.insert_or_assign(m_Tamer, m_Parent->GetObjectID()); // Notify the start of a pet taming minigame m_Parent->GetScript()->OnNotifyPetTamingMinigame(m_Parent, originator, ePetTamingNotifyType::BEGIN); - auto* characterComponent = originator->GetComponent(); + auto* const characterComponent = originator->GetComponent(); if (characterComponent != nullptr) { characterComponent->SetCurrentActivity(eGameActivity::PET_TAMING); Game::entityManager->SerializeEntity(originator); } } -void PetComponent::Update(float deltaTime) { - if (m_StartPosition == NiPoint3Constant::ZERO) { - m_StartPosition = m_Parent->GetPosition(); - } - - if (m_Owner == LWOOBJID_EMPTY) { - if (m_Tamer != LWOOBJID_EMPTY) { - if (m_Timer > 0) { - m_Timer -= deltaTime; - - if (m_Timer <= 0) { - m_Timer = 0; - - ClientFailTamingMinigame(); - } - } - } else { - if (m_Timer > 0) { - m_Timer -= deltaTime; - - if (m_Timer <= 0) { - Wander(); - Game::entityManager->SerializeEntity(m_Parent); - } - } else { - m_Timer = 5; - } - } - - return; - } - - auto* owner = GetOwner(); - - if (owner == nullptr) { - m_Parent->Kill(); - - return; - } - - m_MovementAI = m_Parent->GetComponent(); - - if (m_MovementAI == nullptr) { - return; - } - - if (m_TreasureTime > 0) { - auto* treasure = Game::entityManager->GetEntity(m_Interaction); - - if (treasure == nullptr) { - m_TreasureTime = 0; - - return; - } - - m_TreasureTime -= deltaTime; - - m_MovementAI->Stop(); - - if (m_TreasureTime <= 0) { - m_Parent->SetOwnerOverride(m_Owner); - - treasure->Smash(m_Parent->GetObjectID()); - - m_Interaction = LWOOBJID_EMPTY; - - m_TreasureTime = 0; - } - - return; - } - - auto destination = owner->GetPosition(); - NiPoint3 position = m_MovementAI->GetParent()->GetPosition(); - - float distanceToOwner = Vector3::DistanceSquared(position, destination); - - if (distanceToOwner > 50 * 50 || m_TimerAway > 5) { - m_MovementAI->Warp(destination); - - m_Timer = 1; - m_TimerAway = 0; - - return; - } - - if (distanceToOwner > 15 * 15 || std::abs(destination.y - position.y) >= 3) { - m_TimerAway += deltaTime; - } else { - m_TimerAway = 0; - } - - if (m_Timer > 0) { - m_Timer -= deltaTime; - - return; - } - - SwitchComponent* closestSwitch = SwitchComponent::GetClosestSwitch(position); - - float haltDistance = 5; - - if (closestSwitch != nullptr) { - if (!closestSwitch->GetActive()) { - NiPoint3 switchPosition = closestSwitch->GetParentEntity()->GetPosition(); - float distance = Vector3::DistanceSquared(position, switchPosition); - if (distance < 3 * 3) { - m_Interaction = closestSwitch->GetParentEntity()->GetObjectID(); - closestSwitch->OnUse(m_Parent); - } else if (distance < 20 * 20) { - haltDistance = 1; - - destination = switchPosition; - } - } - } - - 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* closestTreasure = PetDigServer::GetClosestTreasure(position); - - if (closestTreasure != nullptr && digUnlocked) { - // Skeleton Dragon Pat special case for bone digging - if (closestTreasure->GetLOT() == 12192 && m_Parent->GetLOT() != 13067) { - goto skipTreasure; - } - - NiPoint3 treasurePosition = closestTreasure->GetPosition(); - float distance = Vector3::DistanceSquared(position, treasurePosition); - if (distance < 5 * 5) { - m_Interaction = closestTreasure->GetObjectID(); - - Command(NiPoint3Constant::ZERO, LWOOBJID_EMPTY, 1, 202, true); - - m_TreasureTime = 2; - } else if (distance < 10 * 10) { - haltDistance = 1; - - destination = treasurePosition; - } - } - -skipTreasure: - - m_MovementAI->SetHaltDistance(haltDistance); - - m_MovementAI->SetMaxSpeed(2.5f); - - m_MovementAI->SetDestination(destination); - - m_Timer = 1; -} - void PetComponent::TryBuild(uint32_t numBricks, bool clientFailed) { if (m_Tamer == LWOOBJID_EMPTY) return; - auto* tamer = Game::entityManager->GetEntity(m_Tamer); - - if (tamer == nullptr) { + auto* const tamer = Game::entityManager->GetEntity(m_Tamer); + if (!tamer) { m_Tamer = LWOOBJID_EMPTY; - return; } + auto* const destroyableComponent = tamer->GetComponent(); + if (!destroyableComponent) return; + const auto* const entry = CDClientManager::GetTable()->GetByLOT(m_Parent->GetLOT()); if (!entry) return; - auto* destroyableComponent = tamer->GetComponent(); - - if (destroyableComponent == nullptr) return; - - auto imagination = destroyableComponent->GetImagination(); - - imagination -= entry->imaginationCost; - + const auto imagination = destroyableComponent->GetImagination() - entry->imaginationCost; destroyableComponent->SetImagination(imagination); - Game::entityManager->SerializeEntity(tamer); if (clientFailed) { @@ -469,14 +370,12 @@ void PetComponent::TryBuild(uint32_t numBricks, bool clientFailed) { GameMessages::SendPetTamingTryBuildResult(m_Tamer, !clientFailed, numBricks, tamer->GetSystemAddress()); } -void PetComponent::NotifyTamingBuildSuccess(NiPoint3 position) { +void PetComponent::NotifyTamingBuildSuccess(const NiPoint3 position) { if (m_Tamer == LWOOBJID_EMPTY) return; - auto* tamer = Game::entityManager->GetEntity(m_Tamer); - - if (tamer == nullptr) { + auto* const tamer = Game::entityManager->GetEntity(m_Tamer); + if (!tamer) { m_Tamer = LWOOBJID_EMPTY; - return; } @@ -486,58 +385,45 @@ void PetComponent::NotifyTamingBuildSuccess(NiPoint3 position) { GameMessages::SendPlayFXEffect(tamer, -1, u"petceleb", "", LWOOBJID_EMPTY, 1, 1, true); RenderComponent::PlayAnimation(tamer, u"rebuild-celebrate"); - EntityInfo info{}; + auto info = EntityInfo{}; info.lot = entry->puzzleModelLot; info.pos = position; info.rot = NiQuaternionConstant::IDENTITY; info.spawnerID = tamer->GetObjectID(); - auto* modelEntity = Game::entityManager->CreateEntity(info); - + auto* const modelEntity = Game::entityManager->CreateEntity(info); m_ModelId = modelEntity->GetObjectID(); - Game::entityManager->ConstructEntity(modelEntity); GameMessages::SendNotifyTamingModelLoadedOnServer(m_Tamer, tamer->GetSystemAddress()); - GameMessages::SendPetResponse(m_Tamer, m_Parent->GetObjectID(), 0, 10, 0, tamer->GetSystemAddress()); - auto* inventoryComponent = tamer->GetComponent(); - - if (inventoryComponent == nullptr) { - return; - } - - LWOOBJID petSubKey = ObjectIDManager::GenerateRandomObjectID(); + auto* const inventoryComponent = tamer->GetComponent(); + if (!inventoryComponent) return; + auto petSubKey = ObjectIDManager::GenerateRandomObjectID(); GeneralUtils::SetBit(petSubKey, eObjectBits::CHARACTER); GeneralUtils::SetBit(petSubKey, eObjectBits::PERSISTENT); - m_DatabaseId = petSubKey; - std::string petName = tamer->GetCharacter()->GetName(); - petName += "'s Pet"; + auto petName = tamer->GetCharacter()->GetName() + "'s Pet"; GameMessages::SendAddPetToPlayer(m_Tamer, 0, GeneralUtils::UTF8ToUTF16(petName), petSubKey, m_Parent->GetLOT(), tamer->GetSystemAddress()); - GameMessages::SendRegisterPetID(m_Tamer, m_Parent->GetObjectID(), tamer->GetSystemAddress()); - GameMessages::SendRegisterPetDBID(m_Tamer, petSubKey, tamer->GetSystemAddress()); inventoryComponent->AddItem(m_Parent->GetLOT(), 1, eLootSourceType::ACTIVITY, eInventoryType::MODELS, {}, LWOOBJID_EMPTY, true, false, petSubKey); - auto* item = inventoryComponent->FindItemBySubKey(petSubKey, MODELS); - - if (item == nullptr) { - return; - } - DatabasePet databasePet{}; + auto* const item = inventoryComponent->FindItemBySubKey(petSubKey, MODELS); + if (!item) return; - databasePet.lot = m_Parent->GetLOT(); - databasePet.moderationState = 1; - databasePet.name = petName; + auto databasePet = DatabasePet{ + .lot = m_Parent->GetLOT(), + .name = std::move(petName), + .moderationState = 1, + }; - inventoryComponent->SetDatabasePet(petSubKey, databasePet); + inventoryComponent->SetDatabasePet(petSubKey, std::move(databasePet)); Activate(item, false, true); @@ -560,21 +446,20 @@ void PetComponent::NotifyTamingBuildSuccess(NiPoint3 position) { tamer->GetCharacter()->SetPlayerFlag(petFlags.at(m_Parent->GetLOT()), true); } - auto* missionComponent = tamer->GetComponent(); - + auto* const missionComponent = tamer->GetComponent(); if (missionComponent != nullptr) { missionComponent->Progress(eMissionTaskType::PET_TAMING, m_Parent->GetLOT()); } - SetStatus(1); + m_Flags.Reset(); - auto* characterComponent = tamer->GetComponent(); + auto* const characterComponent = tamer->GetComponent(); if (characterComponent != nullptr) { characterComponent->UpdatePlayerStatistic(PetsTamed); } } -void PetComponent::RequestSetPetName(std::u16string name) { +void PetComponent::RequestSetPetName(const std::u16string& name) { if (m_Tamer == LWOOBJID_EMPTY) { if (m_Owner != LWOOBJID_EMPTY) { auto* owner = GetOwner(); @@ -592,21 +477,16 @@ void PetComponent::RequestSetPetName(std::u16string name) { return; } - auto* tamer = Game::entityManager->GetEntity(m_Tamer); - + auto* const tamer = Game::entityManager->GetEntity(m_Tamer); if (tamer == nullptr) { m_Tamer = LWOOBJID_EMPTY; - return; } LOG("Got set pet name (%s)", GeneralUtils::UTF16ToWTF8(name).c_str()); - auto* inventoryComponent = tamer->GetComponent(); - - if (inventoryComponent == nullptr) { - return; - } + auto* const inventoryComponent = tamer->GetComponent(); + if (!inventoryComponent) return; m_ModerationStatus = 1; // Pending m_Name = ""; @@ -616,8 +496,8 @@ void PetComponent::RequestSetPetName(std::u16string name) { Game::entityManager->SerializeEntity(m_Parent); - std::u16string u16name = GeneralUtils::UTF8ToUTF16(m_Name); - std::u16string u16ownerName = GeneralUtils::UTF8ToUTF16(m_OwnerName); + const auto u16name = GeneralUtils::UTF8ToUTF16(m_Name); + const auto u16ownerName = GeneralUtils::UTF8ToUTF16(m_OwnerName); GameMessages::SendSetPetName(m_Tamer, u16name, m_DatabaseId, tamer->GetSystemAddress()); GameMessages::SendSetPetName(m_Tamer, u16name, LWOOBJID_EMPTY, tamer->GetSystemAddress()); GameMessages::SendPetNameChanged(m_Parent->GetObjectID(), m_ModerationStatus, u16name, u16ownerName, UNASSIGNED_SYSTEM_ADDRESS); @@ -635,21 +515,19 @@ void PetComponent::RequestSetPetName(std::u16string name) { UNASSIGNED_SYSTEM_ADDRESS ); - auto* characterComponent = tamer->GetComponent(); + auto* const characterComponent = tamer->GetComponent(); if (characterComponent != nullptr) { characterComponent->SetCurrentActivity(eGameActivity::NONE); Game::entityManager->SerializeEntity(tamer); } GameMessages::SendTerminateInteraction(m_Tamer, eTerminateType::FROM_INTERACTION, m_Parent->GetObjectID()); - auto* modelEntity = Game::entityManager->GetEntity(m_ModelId); - + auto* const modelEntity = Game::entityManager->GetEntity(m_ModelId); if (modelEntity != nullptr) { modelEntity->Smash(m_Tamer); } currentActivities.erase(m_Tamer); - m_Tamer = LWOOBJID_EMPTY; // Notify the end of a pet taming minigame @@ -659,11 +537,9 @@ void PetComponent::RequestSetPetName(std::u16string name) { void PetComponent::ClientExitTamingMinigame(bool voluntaryExit) { if (m_Tamer == LWOOBJID_EMPTY) return; - auto* tamer = Game::entityManager->GetEntity(m_Tamer); - - if (tamer == nullptr) { + auto* const tamer = Game::entityManager->GetEntity(m_Tamer); + if (!tamer) { m_Tamer = LWOOBJID_EMPTY; - return; } @@ -685,12 +561,11 @@ void PetComponent::ClientExitTamingMinigame(bool voluntaryExit) { Game::entityManager->SerializeEntity(tamer); } GameMessages::SendNotifyTamingModelLoadedOnServer(m_Tamer, tamer->GetSystemAddress()); - GameMessages::SendTerminateInteraction(m_Tamer, eTerminateType::FROM_INTERACTION, m_Parent->GetObjectID()); currentActivities.erase(m_Tamer); - SetStatus(67108866); + m_Flags.Reset(); m_Tamer = LWOOBJID_EMPTY; m_Timer = 0; @@ -710,11 +585,9 @@ void PetComponent::StartTimer() { void PetComponent::ClientFailTamingMinigame() { if (m_Tamer == LWOOBJID_EMPTY) return; - auto* tamer = Game::entityManager->GetEntity(m_Tamer); - - if (tamer == nullptr) { + auto* const tamer = Game::entityManager->GetEntity(m_Tamer); + if (!tamer) { m_Tamer = LWOOBJID_EMPTY; - return; } @@ -730,18 +603,17 @@ void PetComponent::ClientFailTamingMinigame() { UNASSIGNED_SYSTEM_ADDRESS ); - auto* characterComponent = tamer->GetComponent(); + auto* const characterComponent = tamer->GetComponent(); if (characterComponent != nullptr) { characterComponent->SetCurrentActivity(eGameActivity::NONE); Game::entityManager->SerializeEntity(tamer); } GameMessages::SendNotifyTamingModelLoadedOnServer(m_Tamer, tamer->GetSystemAddress()); - GameMessages::SendTerminateInteraction(m_Tamer, eTerminateType::FROM_INTERACTION, m_Parent->GetObjectID()); currentActivities.erase(m_Tamer); - SetStatus(67108866); + m_Flags.Reset(); m_Tamer = LWOOBJID_EMPTY; m_Timer = 0; @@ -752,11 +624,7 @@ void PetComponent::ClientFailTamingMinigame() { } void PetComponent::Wander() { - m_MovementAI = m_Parent->GetComponent(); - - if (m_MovementAI == nullptr || !m_MovementAI->AtFinalWaypoint()) { - return; - } + if (!m_MovementAI->AtFinalWaypoint()) return; m_MovementAI->SetHaltDistance(0); @@ -794,23 +662,334 @@ void PetComponent::Wander() { m_Timer += (m_MovementAI->GetParent()->GetPosition().x - destination.x) / m_PetInfo.sprintSpeed; } -void PetComponent::Activate(Item* item, bool registerPet, bool fromTaming) { +void PetComponent::OnSpawn() { + m_MovementAI = m_Parent->GetComponent(); + + if (m_StartPosition == NiPoint3Constant::ZERO) { + m_StartPosition = m_Parent->GetPosition(); + } + + if (m_Owner != LWOOBJID_EMPTY) { + m_Parent->SetOwnerOverride(m_Owner); + m_MovementAI->SetMaxSpeed(m_PetInfo.sprintSpeed); + m_MovementAI->SetHaltDistance(m_FollowRadius); + SetPetAiState(PetAiState::follow); + } else { + m_Flags.Set(); + SetPetAiState(PetAiState::idle); + } + + m_Flags.Set(); + m_Flags.Unset(); + Game::entityManager->SerializeEntity(m_Parent); +} + +void PetComponent::OnFollow(const float deltaTime) { + Entity* owner = GetOwner(); + if (!owner) return; + + const NiPoint3 ownerPos = owner->GetPosition(); + + // Find interactions + const auto* const closestSwitch = SwitchComponent::GetClosestSwitch(ownerPos); + if (closestSwitch != nullptr && !closestSwitch->GetActive()) { + const NiPoint3 switchPos = closestSwitch->GetParentEntity()->GetPosition(); + const LWOOBJID switchID = closestSwitch->GetParentEntity()->GetObjectID(); + const float distance = Vector3::DistanceSquared(ownerPos, switchPos); + if (distance < 16 * 16) { + StartInteract(switchPos, PetInteractType::bouncer, switchID); + return; + } + } + + // Determine if the "Lost Tags" mission has been completed and digging has been unlocked + const auto* const missionComponent = owner->GetComponent(); + if (!missionComponent) return; + const bool digUnlocked = missionComponent->GetMissionState(842) == eMissionState::COMPLETE; + + const auto* const closestTreasure = PetDigServer::GetClosestTreasure(ownerPos); + const bool nonDragonForBone = closestTreasure->GetLOT() == 12192 && m_Parent->GetLOT() != 13067; + if (!nonDragonForBone && closestTreasure != nullptr && digUnlocked) { + const NiPoint3 treasurePos = closestTreasure->GetPosition(); + const float distance = Vector3::DistanceSquared(ownerPos, treasurePos); + if (distance < 16 * 16) { + StartInteract(treasurePos, PetInteractType::treasure, m_Owner); + return; + } + } + + // Handle actual following logic + const NiPoint3 currentPos = m_MovementAI->GetParent()->GetPosition(); + const float distanceToOwner = Vector3::DistanceSquared(currentPos, ownerPos); + + // If the player's position is within range, stop moving + if (distanceToOwner <= m_FollowRadius * m_FollowRadius) { + m_MovementAI->Stop(); + } else { // Chase the player's new position + m_MovementAI->SetDestination(ownerPos); + } + + // Teleporting logic + if (distanceToOwner > 50 * 50 || m_TimerAway > 5) { + m_MovementAI->Warp(ownerPos); + + m_Timer = 1; + m_TimerAway = 0; + + return; + } else if (distanceToOwner > 15 * 15 || std::abs(ownerPos.y - currentPos.y) >= 3) { + m_TimerAway += deltaTime; + } + + m_Timer += 0.5f; +} + +void PetComponent::OnInteract() { + Entity* const owner = GetOwner(); + if (!owner) return; + + const NiPoint3 ownerPos = owner->GetPosition(); + const NiPoint3 currentPos = m_MovementAI->GetParent()->GetPosition(); + const float distanceFromOwner = Vector3::DistanceSquared(ownerPos, currentPos); + + if (distanceFromOwner > 25 * 25) { + LOG_DEBUG("Disengaging from object interaction due to player distance."); + StopInteract(); + return; + } + + switch (m_Interaction.type) { + case PetInteractType::bouncer: + if (IsReadyToInteract()) HandleInteractBouncer(); + else SetupInteractBouncer(); + break; + + case PetInteractType::treasure: + if (IsReadyToInteract()) HandleInteractTreasureDig(); + else SetupInteractTreasureDig(); + break; + + default: + LOG_DEBUG("INTERACT = NONE! RETURNING!"); + StopInteract(); + m_Timer += 0.5f; + break; + } +} + +void PetComponent::StartInteract(const NiPoint3 position, const PetInteractType interactionType, const LWOOBJID interactID) { + m_Interaction.obj = interactID; // TODO: Check if this should be serialized for goToObj + m_Interaction.type = interactionType; + m_Interaction.ability = ePetAbilityType::GoToObject; + SetPetAiState(PetAiState::goToObj); + m_MovementAI->SetMaxSpeed(m_PetInfo.runSpeed); + m_MovementAI->SetHaltDistance(0.0f); + m_MovementAI->SetDestination(position); + LOG_DEBUG("Starting interaction!"); + Game::entityManager->SerializeEntity(m_Parent); +} + +void PetComponent::StopInteract(bool bDontSerialize) { + Entity* const owner = GetOwner(); + if (!owner) return; + + constexpr auto petAbility = ePetAbilityType::Invalid; + + m_Interaction.obj = LWOOBJID_EMPTY; + m_Interaction.type = PetInteractType::none; + m_Interaction.ability = petAbility; + SetPetAiState(PetAiState::follow); + m_Flags.Reset(); + SetIsReadyToInteract(false); + SetIsHandlingInteraction(false); // Needed? + m_MovementAI->SetMaxSpeed(m_PetInfo.sprintSpeed); + m_MovementAI->SetHaltDistance(m_FollowRadius); + LOG_DEBUG("Stopping interaction!"); + + if (!bDontSerialize) { + Game::entityManager->SerializeEntity(m_Parent); + } + + GameMessages::SendShowPetActionButton(m_Owner, petAbility, false, owner->GetSystemAddress()); // Needed? +} + +void PetComponent::SetupInteractBouncer() { + const auto* const owner = GetOwner(); + if (!owner) return; + + LOG_DEBUG("Setting up bouncer interaction!"); + SetIsReadyToInteract(true); + constexpr auto petAbility = ePetAbilityType::JumpOnObject; + + m_Interaction.ability = petAbility; + m_Flags.Unset(); + m_Flags.Set(); // TODO: Double-check this is the right flag being set + LOG_DEBUG("m_Flags = %d", m_Flags); + Game::entityManager->SerializeEntity(m_Parent); // TODO: Double-check pet packet captures + + const auto sysAddr = owner->GetSystemAddress(); + GameMessages::SendHelp(m_Owner, eHelpType::PR_BOUNCER_TUTORIAL_03, sysAddr); + GameMessages::SendShowPetActionButton(m_Owner, petAbility, true, sysAddr); + + SwitchComponent* closestSwitch = SwitchComponent::GetClosestSwitch(m_MovementAI->GetDestination()); // TODO: Find a better way to do this + closestSwitch->EntityEnter(m_Parent); + + m_Timer += 0.5f; +} + +void PetComponent::StartInteractBouncer() { + Entity* const user = GetOwner(); + if (IsHandlingInteraction() || !user) return; + + auto* const destroyableComponent = user->GetComponent(); + if (!destroyableComponent) return; + + auto imagination = destroyableComponent->GetImagination(); + const auto imaginationCost = + CDClientManager::GetTable()->GetByID(ePetAbilityType::JumpOnObject).imaginationCost; + + if (imagination < imaginationCost) { + //GameMessages::SendHelp(user->GetObjectID(), eHelpType::PR_NEED_IMAGINATION, user->GetSystemAddress()); // Check if right message! + return; + } + GameMessages::SendHelp(user->GetObjectID(), eHelpType::PR_TOOLTIP_1ST_PET_JUMPED_ON_SWITCH, user->GetSystemAddress()); + + GameMessages::SendShowPetActionButton(m_Owner, ePetAbilityType::Invalid, false, user->GetSystemAddress()); + + imagination -= imaginationCost; + destroyableComponent->SetImagination(imagination); + Game::entityManager->SerializeEntity(user); + + SetIsHandlingInteraction(true); + SwitchComponent* closestSwitch = SwitchComponent::GetClosestSwitch(m_MovementAI->GetDestination()); // TODO: Find a better way to do this + closestSwitch->OnUse(m_Parent); +} + +void PetComponent::HandleInteractBouncer() { + if (IsHandlingInteraction()) { + auto* const petSwitch = SwitchComponent::GetClosestSwitch(m_MovementAI->GetDestination()); // TODO: Find a better way to do this + if (!petSwitch) return; + + auto* const petSwitchEntity = petSwitch->GetParentEntity(); + if (!petSwitchEntity) return; + + m_Parent->AddCallbackTimer(1.0f, [this, petSwitch, petSwitchEntity]() { + auto* const bouncerComp = petSwitch->GetPetBouncer(); + const auto bouncerCompPos = bouncerComp->GetParentEntity()->GetPosition(); + const auto bouncerId = bouncerComp->GetParentEntity()->GetObjectID(); + + bouncerComp->SetPetBouncerEnabled(true); + GameMessages::SendRequestClientBounce(bouncerId, this->GetOwnerId(), NiPoint3Constant::ZERO, NiPoint3Constant::ZERO, bouncerId, true, false, UNASSIGNED_SYSTEM_ADDRESS); //TODO: Check packet captures!! + bouncerComp->SetPetBouncerEnabled(false); + RenderComponent::PlayAnimation(petSwitchEntity, u"up"); + }); + + RenderComponent::PlayAnimation(petSwitchEntity, u"launch"); //u"engaged"); //TODO: Check if the timing on this is right + // TODO: Need to freeze player movement until the bounce begins! + + Command(NiPoint3Constant::ZERO, LWOOBJID_EMPTY, 1, GeneralUtils::ToUnderlying(PetEmote::ActivateSwitch), true); // Plays 'jump on switch' animation + StopInteract(); + } + m_Timer += 0.5f; +} + +void PetComponent::SetupInteractTreasureDig() { + const auto* owner = GetOwner(); + if (!owner) return; + + LOG_DEBUG("Setting up dig interaction!"); + SetIsReadyToInteract(true); + constexpr auto petAbility = ePetAbilityType::DigAtPosition; + + m_Interaction.ability = petAbility; + m_Flags.Unset(); + m_Flags.Set(); // TODO: Double-check this is the right flag being set + LOG_DEBUG("m_Flags = %d", m_Flags); + Game::entityManager->SerializeEntity(m_Parent); // TODO: Double-check pet packet captures + + const auto sysAddr = owner->GetSystemAddress(); + GameMessages::SendHelp(m_Owner, eHelpType::PR_DIG_TUTORIAL_01, sysAddr); + GameMessages::SendShowPetActionButton(m_Owner, petAbility, true, sysAddr); + + m_Timer += 0.5f; +} + +void PetComponent::StartInteractTreasureDig() { + Entity* const user = GetOwner(); + if (IsHandlingInteraction() || !user) return; + + auto* const destroyableComponent = user->GetComponent(); + if (!destroyableComponent) return; + + auto imagination = destroyableComponent->GetImagination(); + const auto imaginationCost = + CDClientManager::GetTable()->GetByID(ePetAbilityType::DigAtPosition).imaginationCost; + + if (imagination < imaginationCost) { + //GameMessages::SendHelp(user->GetObjectID(), eHelpType::PR_NEED_IMAGINATION, user->GetSystemAddress()); // Check if right message! + return; + } + + GameMessages::SendShowPetActionButton(m_Owner, ePetAbilityType::Invalid, false, user->GetSystemAddress()); + + imagination -= imaginationCost; + destroyableComponent->SetImagination(imagination); + Game::entityManager->SerializeEntity(user); + + SetIsHandlingInteraction(true); + m_Flags.Unset(); // TODO: FIND THE CORRECT STATUS TO USE HERE + m_Flags.Set(); + LOG_DEBUG("StartInteractTreasureDig() m_Flags = %d", m_Flags); + Game::entityManager->SerializeEntity(m_Parent); + + Command(NiPoint3Constant::ZERO, LWOOBJID_EMPTY, 1, GeneralUtils::ToUnderlying(PetEmote::DigTreasure), true); // Plays 'dig' animation + m_Timer = 2.0f; +} + +void PetComponent::HandleInteractTreasureDig() { + if (IsHandlingInteraction()) { + auto* const owner = GetOwner(); + if (!owner) return; + + auto* const treasure = PetDigServer::GetClosestTreasure(m_MovementAI->GetDestination()); // TODO: Find a better way to do this + if (!treasure) return; + + treasure->Smash(m_Parent->GetObjectID()); + GameMessages::SendHelp(m_Owner, eHelpType::PR_DIG_TUTORIAL_03, owner->GetSystemAddress()); + + LOG_DEBUG("Pet dig completed!"); + StopInteract(true); // TODO: This may not be totally consistent with live behavior, where the pet seems to stay near the dig and not immediately follow + + return; + } + + if (m_TimerBounce <= 0.0f) { + Command(NiPoint3Constant::ZERO, LWOOBJID_EMPTY, 1, GeneralUtils::ToUnderlying(PetEmote::Bounce), true); // Plays 'bounce' animation + m_TimerBounce = 1.0f; + } + + m_Timer += 0.5f; +} + +void PetComponent::Activate(Item* item, bool registerPet, bool fromTaming) { // TODO: Offset spawn position so it's not on top of player char + AddDrainImaginationTimer(fromTaming); + m_ItemId = item->GetId(); m_DatabaseId = item->GetSubKey(); - auto* inventoryComponent = item->GetInventory()->GetComponent(); + auto* const inventoryComponent = item->GetInventory()->GetComponent(); - if (inventoryComponent == nullptr) return; + if (!inventoryComponent) return; inventoryComponent->DespawnPet(); m_Owner = inventoryComponent->GetParent()->GetObjectID(); AddDrainImaginationTimer(fromTaming); - auto* owner = GetOwner(); + auto* const owner = GetOwner(); - if (owner == nullptr) return; - SetStatus(1); + if (!owner) return; + m_Flags.Set(); auto databaseData = inventoryComponent->GetDatabasePet(m_DatabaseId); @@ -825,7 +1004,7 @@ void PetComponent::Activate(Item* item, bool registerPet, bool fromTaming) { databaseData.name = m_Name; databaseData.moderationState = m_ModerationStatus; - inventoryComponent->SetDatabasePet(m_DatabaseId, databaseData); + inventoryComponent->SetDatabasePet(m_DatabaseId, std::move(databaseData)); updatedModerationStatus = true; } else { @@ -841,9 +1020,7 @@ void PetComponent::Activate(Item* item, bool registerPet, bool fromTaming) { GameMessages::SendMarkInventoryItemAsActive(m_Owner, true, eUnequippableActiveType::PET, m_ItemId, GetOwner()->GetSystemAddress()); - activePets[m_Owner] = m_Parent->GetObjectID(); - - m_Timer = 3; + activePets.emplace(m_Owner, m_Parent->GetObjectID()); Game::entityManager->SerializeEntity(m_Parent); @@ -861,13 +1038,13 @@ void PetComponent::Activate(Item* item, bool registerPet, bool fromTaming) { void PetComponent::AddDrainImaginationTimer(bool fromTaming) { if (Game::config->GetValue("pets_take_imagination") != "1") return; - auto* playerEntity = Game::entityManager->GetEntity(m_Owner); + auto* const playerEntity = Game::entityManager->GetEntity(m_Owner); if (!playerEntity) { LOG("owner was null or didnt exist!"); return; } - auto playerDestroyableComponent = playerEntity->GetComponent(); + auto* const playerDestroyableComponent = playerEntity->GetComponent(); if (!playerDestroyableComponent) return; // Drain by 1 when you summon pet or when this method is called, but not when we have just tamed this pet. @@ -885,65 +1062,56 @@ void PetComponent::AddDrainImaginationTimer(bool fromTaming) { if (!playerDestroyableComponent) return; // If we are out of imagination despawn the pet. - if (playerDestroyableComponent->GetImagination() == 0) { - this->Deactivate(); + if (playerDestroyableComponent->GetImagination() < 1) { + this->Deactivate(eHelpType::PR_NO_IMAGINATION_HIBERNATE); auto playerEntity = playerDestroyableComponent->GetParent(); if (!playerEntity) return; - - GameMessages::SendUseItemRequirementsResponse(playerEntity->GetObjectID(), playerEntity->GetSystemAddress(), eUseItemResponse::NoImaginationForPet); } this->AddDrainImaginationTimer(); }); } -void PetComponent::Deactivate() { - GameMessages::SendPlayFXEffect(m_Parent->GetObjectID(), -1, u"despawn", "", LWOOBJID_EMPTY, 1, 1, true); +void PetComponent::Deactivate(const eHelpType msg) { + if (msg != eHelpType::NONE) { + GameMessages::SendHelp(m_Parent->GetObjectID(), msg, m_Parent->GetSystemAddress()); + } + GameMessages::SendPlayFXEffect(m_Parent->GetObjectID(), -1, u"despawn", "", LWOOBJID_EMPTY, 1, 1, true); activePets.erase(m_Owner); - m_Parent->Kill(); - auto* owner = GetOwner(); - - if (owner == nullptr) return; + const auto* const owner = GetOwner(); + if (!owner) return; GameMessages::SendMarkInventoryItemAsActive(m_Owner, false, eUnequippableActiveType::PET, m_ItemId, owner->GetSystemAddress()); - GameMessages::SendAddPetToPlayer(m_Owner, 0, u"", LWOOBJID_EMPTY, LOT_NULL, owner->GetSystemAddress()); - GameMessages::SendRegisterPetID(m_Owner, LWOOBJID_EMPTY, owner->GetSystemAddress()); - GameMessages::SendRegisterPetDBID(m_Owner, LWOOBJID_EMPTY, owner->GetSystemAddress()); - GameMessages::SendShowPetActionButton(m_Owner, ePetAbilityType::Invalid, false, owner->GetSystemAddress()); } void PetComponent::Release() { - auto* inventoryComponent = GetOwner()->GetComponent(); - - if (inventoryComponent == nullptr) { - return; - } + auto* const inventoryComponent = GetOwner()->GetComponent(); + if (!inventoryComponent) return; Deactivate(); - inventoryComponent->RemoveDatabasePet(m_DatabaseId); - auto* item = inventoryComponent->FindItemBySubKey(m_DatabaseId); - + auto* const item = inventoryComponent->FindItemBySubKey(m_DatabaseId); item->SetCount(0, false, false); } void PetComponent::Command(const NiPoint3& position, const LWOOBJID source, const int32_t commandType, const int32_t typeId, const bool overrideObey) { - auto* owner = GetOwner(); + auto* const owner = GetOwner(); if (!owner) return; if (commandType == 1) { // Emotes GameMessages::SendPlayEmote(m_Parent->GetObjectID(), typeId, owner->GetObjectID(), UNASSIGNED_SYSTEM_ADDRESS); } else if (commandType == 3) { - // Follow me, ??? + StopInteract(); // TODO: Verify this is necessary + SetPetAiState(PetAiState::follow); } else if (commandType == 6) { // TODO: Go to player } @@ -951,58 +1119,27 @@ void PetComponent::Command(const NiPoint3& position, const LWOOBJID source, cons if (owner->GetGMLevel() >= eGameMasterLevel::DEVELOPER) { ChatPackets::SendSystemMessage(owner->GetSystemAddress(), u"Commmand Type: " + (GeneralUtils::to_u16string(commandType)) + u" - Type Id: " + (GeneralUtils::to_u16string(typeId))); } -} -LWOOBJID PetComponent::GetOwnerId() const { - return m_Owner; + // Add movement functionality + if (position != NiPoint3Constant::ZERO) { + m_MovementAI->SetDestination(position); + } } Entity* PetComponent::GetOwner() const { return Game::entityManager->GetEntity(m_Owner); } -LWOOBJID PetComponent::GetDatabaseId() const { - return m_DatabaseId; -} - -LWOOBJID PetComponent::GetInteraction() const { - return m_Interaction; -} - -LWOOBJID PetComponent::GetItemId() const { - return m_ItemId; -} - -uint32_t PetComponent::GetStatus() const { - return m_Status; -} - -ePetAbilityType PetComponent::GetAbility() const { - return m_Ability; -} - -void PetComponent::SetInteraction(LWOOBJID value) { - m_Interaction = value; -} - -void PetComponent::SetStatus(uint32_t value) { - m_Status = value; -} - -void PetComponent::SetAbility(ePetAbilityType value) { - m_Ability = value; -} - -PetComponent* PetComponent::GetTamingPet(LWOOBJID tamer) { +PetComponent* PetComponent::GetTamingPet(const LWOOBJID tamer) { const auto& pair = currentActivities.find(tamer); if (pair == currentActivities.end()) { return nullptr; } - auto* entity = Game::entityManager->GetEntity(pair->second); + auto* const entity = Game::entityManager->GetEntity(pair->second); - if (entity == nullptr) { + if (!entity) { currentActivities.erase(tamer); return nullptr; @@ -1011,16 +1148,16 @@ PetComponent* PetComponent::GetTamingPet(LWOOBJID tamer) { return entity->GetComponent(); } -PetComponent* PetComponent::GetActivePet(LWOOBJID owner) { +PetComponent* PetComponent::GetActivePet(const LWOOBJID owner) { const auto& pair = activePets.find(owner); if (pair == activePets.end()) { return nullptr; } - auto* entity = Game::entityManager->GetEntity(pair->second); + auto* const entity = Game::entityManager->GetEntity(pair->second); - if (entity == nullptr) { + if (!entity) { activePets.erase(owner); return nullptr; @@ -1029,14 +1166,6 @@ PetComponent* PetComponent::GetActivePet(LWOOBJID owner) { return entity->GetComponent(); } -Entity* PetComponent::GetParentEntity() const { - return m_Parent; -} - -PetComponent::~PetComponent() { - m_Owner = LWOOBJID_EMPTY; -} - void PetComponent::SetPetNameForModeration(const std::string& petName) { int approved = 1; //default, in mod @@ -1050,7 +1179,7 @@ void PetComponent::SetPetNameForModeration(const std::string& petName) { } void PetComponent::LoadPetNameFromModeration() { - auto petNameInfo = Database::Get()->GetPetNameInfo(m_DatabaseId); + const auto petNameInfo = Database::Get()->GetPetNameInfo(m_DatabaseId); if (petNameInfo) { m_ModerationStatus = petNameInfo->approvalStatus; if (m_ModerationStatus == 2) { @@ -1058,7 +1187,3 @@ void PetComponent::LoadPetNameFromModeration() { } } } - -void PetComponent::SetPreconditions(const std::string& preconditions) { - m_Preconditions = std::make_optional(preconditions); -} diff --git a/dGame/dComponents/PetComponent.h b/dGame/dComponents/PetComponent.h index fed3f49a3..70773e943 100644 --- a/dGame/dComponents/PetComponent.h +++ b/dGame/dComponents/PetComponent.h @@ -2,26 +2,109 @@ #define PETCOMPONENT_H #include "Entity.h" +#include "Flag.h" #include "MovementAIComponent.h" #include "Component.h" #include "Preconditions.h" -#include "ePetAbilityType.h" +#include "eHelpType.h" #include "eReplicaComponentType.h" +#include "ePetAbilityType.h" #include "CDPetComponentTable.h" +#include "CDClientManager.h" + +#include + +/* +* The current state of the pet AI +*/ +enum class PetAiState : uint8_t { + idle = 0, // Doing nothing + spawn, // Spawning into the world + follow, // Begin following + goToObj, // Go to object + interact, // Interact with an object + despawn // Despawning from world +}; + +/* +* The type of object the pet is interacting with +*/ +enum class PetInteractType : uint8_t { + none, // Not interacting + treasure, // Treasure dig + bouncer // Bouncer switch +}; + +/** + * The flags governing the status of the pet: Governs the icon above their head and the interactions available +*/ +enum class PetFlag : uint32_t { + NONE, + IDLE, //0x01 - Seems to be "idle," which the game doesn't differentiate from "follow" + UNKNOWN2, //0x02, + UNKNOWN4, //0x04 - FOLLOWING(?) + BEING_TAMED, //0x10, + NOT_WAITING, //0x20, + IMMOBILE, //0x40 - Seems to be the "stop moving" flag - called when taming begins and stays active until a name is submitted + SPAWNING, //0x80 + ON_SWITCH, //0x100 + UNKNOWN1024 = 10, //0x400 + TAMEABLE = 26 //0x4000000 +}; + +/** + * The pet emote animation ids that can used in PetComponent::Command() +*/ +enum class PetEmote : int32_t { + ActivateSwitch = 201, + DigTreasure, + Bounce +}; /** * Represents an entity that is a pet. This pet can be tamed and consequently follows the tamer around, allowing it * to dig for treasure and activate pet bouncers. */ -class PetComponent final : public Component -{ +class PetComponent final : public Component { public: static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::PET; + /** + * PetComponent constructor + * @param parentEntity The parent entity + * @param componentId The component id + */ explicit PetComponent(Entity* parentEntity, uint32_t componentId); - ~PetComponent() override; + ~PetComponent() override = default; + + /** + * Serializes the pet + * @param outBitStream The output bitstream + * @param bIsInitialUpdate Boolean value of whether this is the initial update + */ void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) override; + + /** + * Sets the AI state of the pet + * @param newState New pet AI state + */ + void SetPetAiState(const PetAiState newState) noexcept { + m_State = newState; + }; + + /** + * Gets the AI state of the pet + */ + [[nodiscard]] + PetAiState GetPetAiState() const noexcept { + return m_State; + } + + /** + * Governs the pet update loop + * @param deltaTime Time elapsed since last update + */ void Update(float deltaTime) override; /** @@ -37,6 +120,11 @@ class PetComponent final : public Component */ void TryBuild(uint32_t numBricks, bool clientFailed); + /** + * Start the pet taming minigame + */ + void StartTamingMinigame(Entity* originator); + /** * Handles a notification from the client regarding the completion of the pet minigame, adding the pet to their * inventory. @@ -49,7 +137,7 @@ class PetComponent final : public Component * successfully). * @param name the name of the pet to set */ - void RequestSetPetName(std::u16string name); + void RequestSetPetName(const std::u16string& name); /** * Handles a notification of the client that the taming entity is leaving the minigame, either voluntary or because @@ -73,6 +161,32 @@ class PetComponent final : public Component */ void Wander(); + /** + * Called when the pet is first spawned + */ + void OnSpawn(); + + /** + * Continues a step in the follow state, making sure that the entity is around its start position + */ + void OnFollow(const float deltaTime); + + /** + * Continues a step in the interact state, handling the pet's interaction with an entity + */ + void OnInteract(); + + /** + * Start a pet interaction with an object at a given position + */ + void StartInteract(const NiPoint3 position, const PetInteractType interactionType, const LWOOBJID interactID); + + /** + * Stop a pet interaction with an object + * @param bDontSerialize optional parameter, set to true to not serialize afterwards + */ + void StopInteract(bool bDontSerialize = false); + /** * Spawns a pet from an item in the inventory of an owner * @param item the item to create the pet from @@ -83,7 +197,7 @@ class PetComponent final : public Component /** * Despawns the pet */ - void Deactivate(); + void Deactivate(eHelpType msg = eHelpType::NONE); /** * Removes the pet from the inventory @@ -104,74 +218,114 @@ class PetComponent final : public Component * Returns the ID of the owner of this pet (if any) * @return the ID of the owner of this pet */ - LWOOBJID GetOwnerId() const; + [[nodiscard]] + LWOOBJID GetOwnerId() const noexcept { + return m_Owner; + }; /** * Returns the entity that owns this pet (if any) * @return the entity that owns this pet */ + [[nodiscard]] Entity* GetOwner() const; /** * Returns the ID that is stored in the database with regards to this pet, only set for pets that are tamed * @return the ID that is stored in the database with regards to this pet */ - LWOOBJID GetDatabaseId() const; + [[nodiscard]] + LWOOBJID GetDatabaseId() const noexcept { + return m_DatabaseId; + } /** - * Returns the ID of the object that the pet is currently interacting with, could be a treasure chest or a switch - * @return the ID of the object that the pet is currently interacting with + * Returns the ID that this pet was spawned from, only set for tamed pets + * @return the ID that this pet was spawned from */ - LWOOBJID GetInteraction() const; + [[nodiscard]] + LWOOBJID GetItemId() const noexcept { + return m_ItemId; + } /** - * Sets the ID that the pet is interacting with - * @param value the ID that the pet is interacting with + * Sets preconditions for the pet that need to be met before it can be tamed + * @param conditions the preconditions to set */ - void SetInteraction(LWOOBJID value); + void SetPreconditions(const std::string& preconditions) { + m_Preconditions = PreconditionExpression(preconditions); + } /** - * Returns the ID that this pet was spawned from, only set for tamed pets - * @return the ID that this pet was spawned from + * Sets if the pet is ready to interact with an object + * @param isReady whether the pet is ready to interact (true) or not (false) */ - LWOOBJID GetItemId() const; + void SetIsReadyToInteract(const bool isReady) { + m_ReadyToInteract = isReady; + }; /** - * Returns the status of this pet, e.g. tamable or tamed. The values here are still a bit of mystery and likely a - * bit map - * @return the status of this pet + * @return is pet ready to interact with an object */ - uint32_t GetStatus() const; + [[nodiscard]] + bool IsReadyToInteract() const noexcept { + return m_ReadyToInteract; + } /** - * Sets the current status of the pet - * @param value the current status of the pet to set - */ - void SetStatus(uint32_t value); + * Sets if the pet is currently handling an interaction with an object + * @param isHandlingInteraction whether the pet is currently handling an interaction with an object + */ + void SetIsHandlingInteraction(const bool isHandlingInteraction) { + m_IsHandlingInteraction = isHandlingInteraction; + } /** - * Returns an ability the pet may perform, currently unused - * @return an ability the pet may perform - */ - ePetAbilityType GetAbility() const; + * @return is pet currently handling an interaction with an object + */ + [[nodiscard]] + bool IsHandlingInteraction() const noexcept { + return m_IsHandlingInteraction; + }; /** - * Sets the ability of the pet, currently unused - * @param value the ability to set - */ - void SetAbility(ePetAbilityType value); + * Set up the pet bouncer interaction + */ + void SetupInteractBouncer(); /** - * Sets preconditions for the pet that need to be met before it can be tamed - * @param conditions the preconditions to set + * Starts the pet bouncer interaction + */ + void StartInteractBouncer(); + + /** + * Handles the pet bouncer interaction + */ + void HandleInteractBouncer(); + + /** + * Set up the treasure dig interaction */ - void SetPreconditions(const std::string& conditions); + void SetupInteractTreasureDig(); + + /** + * Starts the pet treasure dig interaction + */ + void StartInteractTreasureDig(); + + /** + * Handles the pet treasure dig interaction + */ + void HandleInteractTreasureDig(); /** * Returns the entity that this component belongs to * @return the entity that this component belongs to */ - Entity* GetParentEntity() const; + [[nodiscard]] + Entity* GetParentEntity() const noexcept { + return m_Parent; + } /** * Sets the name of the pet to be moderated @@ -190,6 +344,7 @@ class PetComponent final : public Component * @param tamer the entity that's currently taming * @return the pet component of the entity that's being tamed */ + [[nodiscard]] static PetComponent* GetTamingPet(LWOOBJID tamer); /** @@ -197,6 +352,7 @@ class PetComponent final : public Component * @param owner the owner of the pet that's spawned * @return the pet component of the entity that was spawned by the owner */ + [[nodiscard]] static PetComponent* GetActivePet(LWOOBJID owner); /** @@ -208,12 +364,13 @@ class PetComponent final : public Component void AddDrainImaginationTimer(bool fromTaming = false); private: + // Needed so it can access flags + friend class DamagingPets; /** * Information for the minigame to be completed */ - struct PetPuzzleData - { + struct PuzzleData { /** * The LOT of the object that is to be created */ @@ -227,7 +384,7 @@ class PetComponent final : public Component /** * The time limit to complete the build */ - int32_t timeLimit; + float timeLimit; /** * The imagination cost for the tamer to start the minigame @@ -240,6 +397,28 @@ class PetComponent final : public Component int32_t numValidPieces; }; + struct Interaction { + /** + * The type of object that the pet is currently interacting with (e.g. a treasure chest or switch) + */ + PetInteractType type = PetInteractType::none; + + /** + * The interaction ability + */ + ePetAbilityType ability = ePetAbilityType::Invalid; + + /** + * The ID of the object that the pet is currently interacting with (e.g. a treasure chest or switch) + */ + LWOOBJID obj = LWOOBJID_EMPTY; + }; + + /** + * Pet interaction info + */ + Interaction m_Interaction{}; + /** * Cache of all the pets that are currently spawned, indexed by tamer */ @@ -250,11 +429,21 @@ class PetComponent final : public Component */ static std::unordered_map currentActivities; + /** + * Cache of all the minigames and their information from the database + */ + static std::unordered_map buildCache; + /** * Flags that indicate that a player has tamed a pet, indexed by the LOT of the pet */ static const std::map petFlags; + /** + * The halting radius of the pet while following a player TODO: Move into struct? + */ + float m_FollowRadius; + /** * The ID of the component in the pet component table */ @@ -263,27 +452,22 @@ class PetComponent final : public Component /** * The ID of the model that was built to complete the taming minigame for this pet */ - LWOOBJID m_ModelId; - - /** - * The ID of the object that the pet is currently interacting with (e.g. a treasure chest or switch) - */ - LWOOBJID m_Interaction; + LWOOBJID m_ModelId = LWOOBJID_EMPTY; /** * The ID of the entity that owns this pet */ - LWOOBJID m_Owner; + LWOOBJID m_Owner = LWOOBJID_EMPTY; /** * The ID of the entity that is currently taming this pet */ - LWOOBJID m_Tamer; + LWOOBJID m_Tamer = LWOOBJID_EMPTY; /** * The ID under which this pet is stored in the database (if it's tamed) */ - LWOOBJID m_DatabaseId; + LWOOBJID m_DatabaseId = LWOOBJID_EMPTY; /** * The ID of the item from which this pet was created @@ -293,7 +477,7 @@ class PetComponent final : public Component /** * The moderation status for the name of this pet */ - uint32_t m_ModerationStatus; + uint32_t m_ModerationStatus = 0; /** * The name of this pet @@ -306,30 +490,39 @@ class PetComponent final : public Component std::string m_OwnerName; /** - * The current state of the pet (e.g. tamable, tamed, etc). + * The current flags of the pet (e.g. tamable, tamed, etc). */ - uint32_t m_Status; + Flag m_Flags; /** - * A currently active ability, mostly unused + * The current state of the pet AI */ - ePetAbilityType m_Ability; + PetAiState m_State; /** * The time an entity has left to complete the minigame */ - float m_Timer; + float m_Timer = 0; /** * A timer that tracks how long a tamed pet has been to far away from its owner, triggering a teleport after timeout */ - float m_TimerAway; + float m_TimerAway = 0; /** - * Timer that tracks how long a pet has been digging up some treasure, required to spawn the treasure contents - * on time + * A timer that tracks how long until a tamed pet will bounce again when standing over a treasure dig site + */ + float m_TimerBounce = 0; + + /** + * Boolean that sets if a pet is ready to interact with an object */ - float m_TreasureTime; + bool m_ReadyToInteract; + + /** + * Boolean that sets if a pet is currently handling an interaction with an object + */ + bool m_IsHandlingInteraction; /** * The position that this pet was spawned at diff --git a/dGame/dComponents/SwitchComponent.cpp b/dGame/dComponents/SwitchComponent.cpp index 4f48fb46e..184df9433 100644 --- a/dGame/dComponents/SwitchComponent.cpp +++ b/dGame/dComponents/SwitchComponent.cpp @@ -79,8 +79,6 @@ void SwitchComponent::EntityEnter(Entity* entity) { if (m_PetBouncer != nullptr) { GameMessages::SendPlayFXEffect(m_Parent->GetObjectID(), 2602, u"pettriggeractive", "BounceEffect", LWOOBJID_EMPTY, 1, 1, true); - RenderComponent::PlayAnimation(m_Parent, u"engaged"); - m_PetBouncer->SetPetBouncerEnabled(true); } else { Game::entityManager->SerializeEntity(m_Parent); } diff --git a/dGame/dGameMessages/GameMessageHandler.cpp b/dGame/dGameMessages/GameMessageHandler.cpp index baa3a84e5..15d99de25 100644 --- a/dGame/dGameMessages/GameMessageHandler.cpp +++ b/dGame/dGameMessages/GameMessageHandler.cpp @@ -110,7 +110,7 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System if (var) { entity->SetVar(u"dlu_first_time_load", false); InventoryComponent* inventoryComponent = entity->GetComponent(); - + if (inventoryComponent) inventoryComponent->FixInvisibleItems(); } break; diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index b42e7d010..9e1a004da 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -42,6 +42,8 @@ #include "eStateChangeType.h" #include "eConnectionType.h" #include "ePlayerFlag.h" +#include "eHelpType.h" +#include "ePetAbilityType.h" #include #include @@ -524,6 +526,17 @@ void GameMessages::SendNotifyClientFlagChange(const LWOOBJID& objectID, uint32_t SEND_PACKET; } +void GameMessages::SendHelp(const LWOOBJID objectId, const eHelpType help, const SystemAddress& sysAddr) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(objectId); + bitStream.Write(MessageType::Game::HELP); + bitStream.Write(help); + + SEND_PACKET; +} + void GameMessages::SendChangeObjectWorldState(const LWOOBJID& objectID, eObjectWorldState state, const SystemAddress& sysAddr) { CBITSTREAM; CMSGHEADER; @@ -3547,7 +3560,7 @@ void GameMessages::SendShowPetActionButton(const LWOOBJID objectId, const ePetAb bitStream.Write(petAbility); bitStream.Write(bShow); - if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; + if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; // TODO: Don't think this should ever be broadcasted, need to confirm SEND_PACKET; } @@ -3593,6 +3606,33 @@ void GameMessages::SendBouncerActiveStatus(LWOOBJID objectId, bool bActive, cons SEND_PACKET; } +void GameMessages::SendRequestClientBounce(const LWOOBJID objectId, const LWOOBJID bounceTargetId, const NiPoint3& bounceTargetPos, const NiPoint3& bouncedObjLinVel, const LWOOBJID& requestSourceId, const bool bAllBounced, const bool bAllowClientOverload, const SystemAddress& sysAddr) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(objectId); + LOG_DEBUG("Object id: %llu", objectId); + bitStream.Write(MessageType::Game::REQUEST_CLIENT_BOUNCE); + + bitStream.Write(bounceTargetId); + LOG_DEBUG("Bounce target id: %llu", bounceTargetId); + bitStream.Write(bounceTargetPos.x); + bitStream.Write(bounceTargetPos.y); + bitStream.Write(bounceTargetPos.z); + LOG_DEBUG("Bounce target pos on server: %f %f %f", bounceTargetPos.x, bounceTargetPos.y, bounceTargetPos.z); + bitStream.Write(bouncedObjLinVel.x); + bitStream.Write(bouncedObjLinVel.y); + bitStream.Write(bouncedObjLinVel.z); + LOG_DEBUG("Bounced object linear velocity: %f %f %f", bouncedObjLinVel.x, bouncedObjLinVel.y, bouncedObjLinVel.z); + bitStream.Write(requestSourceId); + LOG_DEBUG("Request source id: %llu", requestSourceId); + + bitStream.Write(bAllBounced); + bitStream.Write(bAllowClientOverload); + + if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; + SEND_PACKET; +} void GameMessages::SendSetPetName(LWOOBJID objectId, std::u16string name, LWOOBJID petDBID, const SystemAddress& sysAddr) { CBITSTREAM; @@ -3683,21 +3723,17 @@ void GameMessages::HandlePetTamingTryBuild(RakNet::BitStream& inStream, Entity* bool clientFailed; inStream.Read(brickCount); - bricks.reserve(brickCount); for (uint32_t i = 0; i < brickCount; i++) { Brick brick; - inStream.Read(brick); - bricks.push_back(brick); } clientFailed = inStream.ReadBit(); - auto* petComponent = PetComponent::GetTamingPet(entity->GetObjectID()); - + auto* const petComponent = PetComponent::GetTamingPet(entity->GetObjectID()); if (petComponent == nullptr) { return; } @@ -3707,11 +3743,9 @@ void GameMessages::HandlePetTamingTryBuild(RakNet::BitStream& inStream, Entity* void GameMessages::HandleNotifyTamingBuildSuccess(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { NiPoint3 position; - inStream.Read(position); - auto* petComponent = PetComponent::GetTamingPet(entity->GetObjectID()); - + auto* const petComponent = PetComponent::GetTamingPet(entity->GetObjectID()); if (petComponent == nullptr) { return; } @@ -3780,7 +3814,7 @@ void GameMessages::HandleDespawnPet(RakNet::BitStream& inStream, Entity* entity, if (bDeletePet) { petComponent->Release(); } else { - petComponent->Deactivate(); + petComponent->Deactivate(eHelpType::PET_DESPAWN_BY_OWNER_HIBERNATE); } } @@ -6338,6 +6372,6 @@ void GameMessages::SendUpdateInventoryUi(LWOOBJID objectId, const SystemAddress& bitStream.Write(objectId); bitStream.Write(MessageType::Game::UPDATE_INVENTORY_UI); - + SEND_PACKET; } diff --git a/dGame/dGameMessages/GameMessages.h b/dGame/dGameMessages/GameMessages.h index 1dbe5d815..be1fe145e 100644 --- a/dGame/dGameMessages/GameMessages.h +++ b/dGame/dGameMessages/GameMessages.h @@ -26,6 +26,7 @@ enum class eAnimationFlags : uint32_t; enum class eUnequippableActiveType; enum eInventoryType : uint32_t; enum class eGameMasterLevel : uint8_t; +enum class eHelpType : int32_t; enum class eMatchUpdate : int32_t; enum class eKillType : uint32_t; enum class eObjectWorldState : uint32_t; @@ -90,6 +91,7 @@ namespace GameMessages { void SendAddItemToInventoryClientSync(Entity* entity, const SystemAddress& sysAddr, Item* item, const LWOOBJID& objectID, bool showFlyingLoot, int itemCount, LWOOBJID subKey = LWOOBJID_EMPTY, eLootSourceType lootSourceType = eLootSourceType::NONE); void SendNotifyClientFlagChange(const LWOOBJID& objectID, uint32_t iFlagID, bool bFlag, const SystemAddress& sysAddr); + void SendHelp(const LWOOBJID objectId, const eHelpType help, const SystemAddress& sysAddr); void SendChangeObjectWorldState(const LWOOBJID& objectID, eObjectWorldState state, const SystemAddress& sysAddr); void SendOfferMission(const LWOOBJID& entity, const SystemAddress& sysAddr, int32_t missionID, const LWOOBJID& offererID); @@ -401,8 +403,26 @@ namespace GameMessages { void SendPlayEmote(LWOOBJID objectId, int32_t emoteID, LWOOBJID target, const SystemAddress& sysAddr); + /** + * Sends the bouncer active status + * @param objectId Object ID of the bouncer + * @param bActive Active state of the bouncer + * @param sysAddr System address of the user + */ void SendBouncerActiveStatus(LWOOBJID objectId, bool bActive, const SystemAddress& sysAddr); + /** + * Sends a request to the client to bounce + * @param objectId Object ID + * @param bounceTargetId The object ID of the bounce target + * @param bounceTargetPos The position of the bounce target + * @param bouncedObjLinVel TODO: UNUSED + * @param requestSourceId The object Id of the entity requesting the bounce + * @param bAllBounced Whether to bounce all entities standing on the bouncer pad + * @param bAllowClientOverload TODO: UNUSED + */ + void SendRequestClientBounce(LWOOBJID objectId, LWOOBJID bounceTargetId, const NiPoint3& bounceTargetPos, const NiPoint3& bouncedObjLinVel, const LWOOBJID& requestSourceId, const bool bAllBounced, const bool bAllowClientOverload, const SystemAddress& sysAddr); + void SendSetPetName(LWOOBJID objectId, std::u16string name, LWOOBJID petDBID, const SystemAddress& sysAddr); void SendSetPetNameModerated(LWOOBJID objectId, LWOOBJID petDBID, int32_t nModerationStatus, const SystemAddress& sysAddr); diff --git a/dGame/dInventory/Item.cpp b/dGame/dInventory/Item.cpp index 326037614..c71a5def3 100644 --- a/dGame/dInventory/Item.cpp +++ b/dGame/dInventory/Item.cpp @@ -340,7 +340,7 @@ void Item::UseNonEquip(Item* item) { if (packageComponentId == 0) return; auto* packCompTable = CDClientManager::GetTable(); - auto packages = packCompTable->Query([=](const CDPackageComponent entry) {return entry.id == static_cast(packageComponentId); }); + auto packages = packCompTable->Query([packageComponentId](const CDPackageComponent entry) {return entry.id == static_cast(packageComponentId); }); auto success = !packages.empty(); if (success) { diff --git a/dScripts/02_server/Map/General/PetDigServer.cpp b/dScripts/02_server/Map/General/PetDigServer.cpp index 77a50e5af..2f4e89b1d 100644 --- a/dScripts/02_server/Map/General/PetDigServer.cpp +++ b/dScripts/02_server/Map/General/PetDigServer.cpp @@ -1,5 +1,6 @@ #include "dZoneManager.h" #include "PetDigServer.h" +#include "DestroyableComponent.h" #include "MissionComponent.h" #include "EntityManager.h" #include "Character.h" @@ -70,7 +71,7 @@ void PetDigServer::OnDie(Entity* self, Entity* killer) { treasures.erase(iterator); } - auto* owner = killer->GetOwner(); + auto* const owner = killer->GetOwner(); const auto digInfoIterator = digInfoMap.find(self->GetLOT()); const auto digInfo = digInfoIterator != digInfoMap.end() ? digInfoIterator->second : defaultDigInfo; @@ -79,7 +80,7 @@ void PetDigServer::OnDie(Entity* self, Entity* killer) { } else if (digInfo.builderOnly) { // Some treasures may only be retrieved by the player that built the diggable - auto builder = self->GetVar(u"builder"); // Set by the pet dig build script + const auto builder = self->GetVar(u"builder"); // Set by the pet dig build script if (builder != owner->GetObjectID()) return; } else if (digInfo.xBuild) { @@ -95,22 +96,33 @@ void PetDigServer::OnDie(Entity* self, Entity* killer) { // TODO: Reset other pets // Handles smashing leftovers (edge case for the AG X) - auto* xObject = Game::entityManager->GetEntity(self->GetVar(u"X")); + auto* const xObject = Game::entityManager->GetEntity(self->GetVar(u"X")); if (xObject != nullptr) { xObject->Smash(xObject->GetObjectID(), eKillType::VIOLENT); } } +void PetDigServer::OnUse(Entity* self, Entity* user) { + LOG_DEBUG("Treasure used! LWOOBJID: %d", self->GetObjectID()); + + auto* const petComponent = PetComponent::GetActivePet(user->GetObjectID()); + if (!petComponent) return; + + if (petComponent->IsReadyToInteract()) { + petComponent->StartInteractTreasureDig(); + } +} + void PetDigServer::HandleXBuildDig(const Entity* self, Entity* owner, Entity* pet) { - auto playerID = self->GetVar(u"builder"); + const auto playerID = self->GetVar(u"builder"); if (playerID == LWOOBJID_EMPTY || playerID != owner->GetObjectID()) return; - auto* playerEntity = Game::entityManager->GetEntity(playerID); + auto* const playerEntity = Game::entityManager->GetEntity(playerID); if (!playerEntity || !playerEntity->GetCharacter()) return; - auto* player = playerEntity->GetCharacter(); + auto* const player = playerEntity->GetCharacter(); const auto groupID = self->GetVar(u"groupID"); int32_t playerFlag = 0; @@ -125,7 +137,7 @@ void PetDigServer::HandleXBuildDig(const Entity* self, Entity* owner, Entity* pe // If the player doesn't have the flag yet if (playerFlag != 0 && !player->GetPlayerFlag(playerFlag)) { - auto* petComponent = pet->GetComponent(); + auto* const petComponent = pet->GetComponent(); if (petComponent != nullptr) { // TODO: Pet state = 9 ?? } @@ -134,22 +146,22 @@ void PetDigServer::HandleXBuildDig(const Entity* self, Entity* owner, Entity* pe player->SetPlayerFlag(playerFlag, true); } - auto* xObject = Game::entityManager->GetEntity(self->GetVar(u"X")); + auto* const xObject = Game::entityManager->GetEntity(self->GetVar(u"X")); if (xObject != nullptr) { xObject->Smash(xObject->GetObjectID(), eKillType::VIOLENT); } } void PetDigServer::HandleBouncerDig(const Entity* self, const Entity* owner) { - auto bounceNumber = GeneralUtils::UTF16ToWTF8(self->GetVar(u"BouncerNumber")); - auto bouncerSpawners = Game::zoneManager->GetSpawnersByName("PetBouncer" + bounceNumber); - auto switchSpawners = Game::zoneManager->GetSpawnersByName("PetBouncerSwitch" + bounceNumber); + const auto bounceNumber = GeneralUtils::UTF16ToWTF8(self->GetVar(u"BouncerNumber")); + const auto bouncerSpawners = Game::zoneManager->GetSpawnersByName("PetBouncer" + bounceNumber); + const auto switchSpawners = Game::zoneManager->GetSpawnersByName("PetBouncerSwitch" + bounceNumber); - for (auto* bouncerSpawner : bouncerSpawners) { + for (auto* const bouncerSpawner : bouncerSpawners) { bouncerSpawner->Activate(); } - for (auto* switchSpawner : switchSpawners) { + for (auto* const switchSpawner : switchSpawners) { switchSpawner->Activate(); } } diff --git a/dScripts/02_server/Map/General/PetDigServer.h b/dScripts/02_server/Map/General/PetDigServer.h index 4091b1658..60805dfaf 100644 --- a/dScripts/02_server/Map/General/PetDigServer.h +++ b/dScripts/02_server/Map/General/PetDigServer.h @@ -17,6 +17,13 @@ class PetDigServer : public CppScripts::Script void OnStartup(Entity* self) override; void OnDie(Entity* self, Entity* killer) override; + /** + * Invoked when a player interacts with treasure. + * @param self the entity the script belongs to + * @param user the entity that used the treasure + */ + void OnUse(Entity* self, Entity* user) override; + static Entity* GetClosestTreasure(NiPoint3 position); private: diff --git a/dScripts/02_server/Pets/DamagingPets.cpp b/dScripts/02_server/Pets/DamagingPets.cpp index 6fd8c560e..ab4e30a4f 100644 --- a/dScripts/02_server/Pets/DamagingPets.cpp +++ b/dScripts/02_server/Pets/DamagingPets.cpp @@ -74,13 +74,13 @@ void DamagingPets::OnTimerDone(Entity* self, std::string message) { } void DamagingPets::MakeUntamable(Entity* self) { - auto* petComponent = self->GetComponent(); + auto* const petComponent = self->GetComponent(); // If the pet is currently not being tamed, make it hostile - if (petComponent != nullptr && petComponent->GetStatus() != 5) { + if (petComponent != nullptr && !petComponent->m_Flags.Has()) { self->SetNetworkVar(u"bIAmTamable", false); self->SetVar(u"IsEvil", true); - petComponent->SetStatus(1); + petComponent->m_Flags.Set(); auto* combatAIComponent = self->GetComponent(); if (combatAIComponent != nullptr) { @@ -110,7 +110,7 @@ void DamagingPets::ClearEffects(Entity* self) { auto* petComponent = self->GetComponent(); if (petComponent != nullptr) { - petComponent->SetStatus(67108866); + petComponent->m_Flags.Set(); } auto* combatAIComponent = self->GetComponent(); diff --git a/dZoneManager/WorldConfig.h b/dZoneManager/WorldConfig.h index a98433a1e..5c78ce1b1 100644 --- a/dZoneManager/WorldConfig.h +++ b/dZoneManager/WorldConfig.h @@ -5,7 +5,7 @@ #include struct WorldConfig { - int32_t worldConfigID{}; //! Primary key for WorlcConfig table + int32_t worldConfigID{}; //! Primary key for WorldConfig table float peGravityValue{}; //! Unknown float peBroadphaseWorldSize{}; //! Unknown float peGameObjScaleFactor{}; //! Unknown diff --git a/dZoneManager/dZoneManager.cpp b/dZoneManager/dZoneManager.cpp index 09baabed9..546ce9a46 100644 --- a/dZoneManager/dZoneManager.cpp +++ b/dZoneManager/dZoneManager.cpp @@ -197,6 +197,10 @@ std::vector dZoneManager::GetSpawnersInGroup(std::string group) { return spawnersInGroup; } +float dZoneManager::GetPetFollowRadius() { + return GetWorldConfig()->petFollowRadius; +} + uint32_t dZoneManager::GetUniqueMissionIdStartingValue() { if (m_UniqueMissionIdStart == 0) { auto tableData = CDClientDatabase::ExecuteQuery("SELECT COUNT(*) FROM Missions WHERE isMission = 0 GROUP BY isMission;"); diff --git a/dZoneManager/dZoneManager.h b/dZoneManager/dZoneManager.h index 4e512d228..ef86dca5c 100644 --- a/dZoneManager/dZoneManager.h +++ b/dZoneManager/dZoneManager.h @@ -42,6 +42,7 @@ class dZoneManager { bool GetDisableSaveLocation() { return m_DisableSaveLocation; } bool GetMountsAllowed() { return m_MountsAllowed; } bool GetPetsAllowed() { return m_PetsAllowed; } + float GetPetFollowRadius(); uint32_t GetUniqueMissionIdStartingValue(); bool CheckIfAccessibleZone(LWOMAPID zoneID); @@ -51,6 +52,9 @@ class dZoneManager { return m_WorldConfig; }; + // Override world config (for debugging purposes) + void SetWorldConfig(WorldConfig* worldConfig) { m_WorldConfig = worldConfig; } + private: /** * The starting unique mission ID. diff --git a/docs/Commands.md b/docs/Commands.md index 62607939f..fb1252d8f 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -79,10 +79,12 @@ These commands are primarily for development and testing. The usage of many of t |getnavmeshheight|`/getnavmeshheight`|Displays the navmesh height at your current position.|8| |giveuscore|`/giveuscore `|Gives uscore.|8| |gmadditem|`/gmadditem (count)`|Adds the given item to your inventory by id.|8| +|helpmsg|`/helpmsg (help id)`|Display help message pop-up for a given id.|8| |inspect|`/inspect (-m \| -a \| -s \| -p \| -f (faction) \| -t)`|Finds the closest entity with the given component or LDF variable (ignoring players and racing cars), printing its ID, distance from the player, and whether it is sleeping, as well as the the IDs of all components the entity has. See [Detailed `/inspect` Usage](#detailed-inspect-usage) below.|8| |list-spawns|`/list-spawns`|Lists all the character spawn points in the zone. Additionally, this command will display the current scene that plays when the character lands in the next zone, if there is one.|8| |locrow|`/locrow`|Prints the your current position and rotation information to the console.|8| |lookup|`/lookup `|Searches through the Objects table in the client SQLite database for items whose display name, name, or description contains the query. Query can be multiple words delimited by spaces.|8| +|petcommand|`/petcommand `|Sends pet command to pet with given command type and type ID.|8| |playanimation|`/playanimation `|Plays animation with given ID. Alias: `/playanim`.|8| |playeffect|`/playeffect `|Plays an effect.|8| |playlvlfx|`/playlvlfx`|Plays the level up animation on your character.|8| @@ -96,6 +98,10 @@ These commands are primarily for development and testing. The usage of many of t |setcontrolscheme|`/setcontrolscheme `|Sets the character control scheme to the specified number.|8| |setcurrency|`/setcurrency `|Sets your coins.|8| |setflag|`/setflag (value) `|Sets the given inventory or health flag to the given value, where value can be one of "on" or "off". If no value is given, by default this adds the flag to your character (equivalent of calling `/setflag on `).|8| +|setpetaccel|`/setpetaccel (value) `|Sets the acceleration of the owner's pet to the given value.|8| +|setpetflag|`/setpetflag (value) `|Sets the pet flag to the given value, where the value is one of the bitshift values specified in the PetFlags enum.|8| +|setpetmaxspeed|`/setpetmaxspeed (value) `|Sets the maximum speed of the owner's pet to the given value.|8| +|setpethalt|`/setpethalt (value) `|Sets the halting distance of the owner's pet to the given value.|8| |setinventorysize|`/setinventorysize (inventory)` or
`/setinvsize (inventory)`|Sets your inventory size to the given size. If `inventory` is provided, the number or string will be used to set that inventory to the requested size. Alias: `/setinvsize`|8| |setuistate|`/setuistate `|Changes UI state.|8| |spawn|`/spawn `|Spawns an object at your location by id.|8| diff --git a/tests/dCommonTests/CMakeLists.txt b/tests/dCommonTests/CMakeLists.txt index 17b31ceda..179167c14 100644 --- a/tests/dCommonTests/CMakeLists.txt +++ b/tests/dCommonTests/CMakeLists.txt @@ -1,6 +1,7 @@ set(DCOMMONTEST_SOURCES "AMFDeserializeTests.cpp" "Amf3Tests.cpp" + "FlagTests.cpp" "ToUnderlyingTests.cpp" "HeaderSkipTest.cpp" "TestCDFeatureGatingTable.cpp" diff --git a/tests/dCommonTests/FlagTests.cpp b/tests/dCommonTests/FlagTests.cpp new file mode 100644 index 000000000..49dca7726 --- /dev/null +++ b/tests/dCommonTests/FlagTests.cpp @@ -0,0 +1,122 @@ +#include +#include "Flag.h" + +enum class TestFlag : uint8_t { + FLAG0, + FLAG1, + FLAG2, + FLAG3, + FLAG4 +}; + +/** + * Test bitset flags (compile-time methods) +*/ +TEST(FlagTests, FlagMethodTestCompileTime) { + using enum TestFlag; + + auto flag = Flag{}; + + // Test setting and reading single flags, exclusively + flag.Reset(); + ASSERT_TRUE(flag.HasOnly()); + flag.Reset(); + ASSERT_TRUE(flag.HasOnly()); + ASSERT_FALSE(flag.HasOnly()); + + // Test setting and reading multiple flags, exclusively + flag.Reset(); + ASSERT_FALSE(flag.Has()); + ASSERT_TRUE(flag.Has()); + ASSERT_TRUE(flag.Has()); + ASSERT_TRUE((flag.Has())); + ASSERT_FALSE(flag.HasOnly()); + ASSERT_FALSE(flag.HasOnly()); + ASSERT_TRUE((flag.HasOnly())); + + // Test flags are being properly reset for next batch of tests + flag.Reset(); + ASSERT_TRUE(flag.Has()); + ASSERT_TRUE(flag.HasOnly()); + ASSERT_FALSE(flag.Has()); + ASSERT_FALSE((flag.Has())); + + // Test setting and reading single flags, non-exclusively + flag.Set(); + ASSERT_TRUE(flag.Has()); + ASSERT_FALSE(flag.Has()); + + // Test setting and reading multiple flags, non-exclusively + flag.Set(); + ASSERT_TRUE((flag.Has())); + ASSERT_TRUE(flag.Has()); + ASSERT_FALSE((flag.Has())); + ASSERT_TRUE((flag.Has())); + ASSERT_FALSE(flag.Has()); + ASSERT_FALSE((flag.Has())); + + // Test unsetting and reading multiple flags, non-exclusively + flag.Unset(); + ASSERT_FALSE((flag.Has())); + ASSERT_FALSE(flag.Has()); + ASSERT_TRUE((flag.Has())); + ASSERT_FALSE((flag.Has())); + ASSERT_FALSE(flag.Has()); + ASSERT_FALSE((flag.Has())); +} + +/** + * Test bitset flags (run-time methods) +*/ +TEST(FlagTests, FlagMethodTestRunTime) { + using enum TestFlag; + + auto flag = Flag{}; + + // Test setting and reading single flags, exclusively + flag.Reset(FLAG0); + ASSERT_TRUE(flag.HasOnly(FLAG0)); + flag.Reset(FLAG2); + ASSERT_TRUE(flag.HasOnly(FLAG2)); + ASSERT_FALSE(flag.HasOnly(FLAG1)); + + // Test setting and reading multiple flags, exclusively + flag.Reset(FLAG3, FLAG1); + ASSERT_FALSE(flag.Has(FLAG2)); + ASSERT_TRUE(flag.Has(FLAG3)); + ASSERT_TRUE(flag.Has(FLAG1)); + ASSERT_TRUE(flag.Has(FLAG3, FLAG1)); + ASSERT_FALSE(flag.HasOnly(FLAG3)); + ASSERT_FALSE(flag.HasOnly(FLAG1)); + ASSERT_TRUE(flag.HasOnly(FLAG3, FLAG1)); + + // Test flags are being properly reset for next batch of tests + flag.Reset(FLAG0); + ASSERT_TRUE(flag.Has(FLAG0)); + ASSERT_TRUE(flag.HasOnly(FLAG0)); + ASSERT_FALSE(flag.Has(FLAG3)); + ASSERT_FALSE(flag.Has(FLAG3, FLAG1, FLAG2)); + + // Test setting and reading single flags, non-exclusively + flag.Set(FLAG3); + ASSERT_TRUE(flag.Has(FLAG3)); + ASSERT_FALSE(flag.Has(FLAG1)); + + // Test setting and reading multiple flags, non-exclusively + flag.Set(FLAG2, FLAG4); + ASSERT_TRUE(flag.Has(FLAG2, FLAG4)); + ASSERT_TRUE(flag.Has(FLAG3)); + ASSERT_FALSE(flag.Has(FLAG3, FLAG1)); + ASSERT_TRUE(flag.Has(FLAG3, FLAG2, FLAG4)); + ASSERT_FALSE(flag.Has(FLAG1)); + ASSERT_FALSE(flag.Has(FLAG1, FLAG3, FLAG2, FLAG4)); + + // Test unsetting and reading multiple flags, non-exclusively + flag.Unset(FLAG3, FLAG1); + ASSERT_FALSE(flag.Has(FLAG3, FLAG1)); + ASSERT_FALSE(flag.Has(FLAG3)); + ASSERT_TRUE(flag.Has(FLAG2, FLAG4)); + ASSERT_FALSE(flag.Has(FLAG3, FLAG2, FLAG4)); + ASSERT_FALSE(flag.Has(FLAG1)); + ASSERT_FALSE(flag.Has(FLAG1, FLAG3, FLAG2, FLAG4)); +} diff --git a/tests/dGameTests/GameDependencies.h b/tests/dGameTests/GameDependencies.h index 9f8dbb2b5..30e5b8e7e 100644 --- a/tests/dGameTests/GameDependencies.h +++ b/tests/dGameTests/GameDependencies.h @@ -7,8 +7,9 @@ #include "CDClientManager.h" #include "EntityInfo.h" #include "EntityManager.h" -#include "dConfig.h" #include "dZoneManager.h" +#include "dConfig.h" +#include "WorldConfig.h" #include "GameDatabase/TestSQL/TestSQLDatabase.h" #include "Database.h" #include @@ -42,6 +43,11 @@ class GameDependenciesTest : public ::testing::Test { Game::zoneManager->LoadZone(LWOZONEID(1, 0, 0)); Database::_setDatabase(new TestSQLDatabase()); // this new is managed by the Database + // Set up world config + auto worldConfig = new WorldConfig(); + worldConfig->petFollowRadius = 10.0f; + Game::zoneManager->SetWorldConfig(worldConfig); + // Create a CDClientManager instance and load from defaults CDClientManager::LoadValuesFromDefaults(); } diff --git a/tests/dGameTests/dComponentsTests/PetComponentTests.cpp b/tests/dGameTests/dComponentsTests/PetComponentTests.cpp index 75ce4ec83..31633303f 100644 --- a/tests/dGameTests/dComponentsTests/PetComponentTests.cpp +++ b/tests/dGameTests/dComponentsTests/PetComponentTests.cpp @@ -32,6 +32,8 @@ class PetTest : public GameDependenciesTest { }; TEST_F(PetTest, PlacementNewAddComponentTest) { + using enum PetFlag; + // Test adding component ASSERT_NE(petComponent, nullptr); baseEntity->AddComponent(1); @@ -39,5 +41,38 @@ TEST_F(PetTest, PlacementNewAddComponentTest) { // Test getting initial status ASSERT_EQ(petComponent->GetParent()->GetObjectID(), 15); - ASSERT_EQ(petComponent->GetAbility(), ePetAbilityType::Invalid); + ASSERT_EQ(petComponent->GetPetAiState(), PetAiState::spawn); +} + + +TEST_F(PetTest, PetAiState) { + const auto initialState = petComponent->GetPetAiState(); + ASSERT_EQ(initialState, PetAiState::spawn); + + petComponent->SetPetAiState(PetAiState::follow); + ASSERT_EQ(PetAiState::follow, petComponent->GetPetAiState()); + + petComponent->SetPetAiState(PetAiState::idle); + ASSERT_EQ(PetAiState::idle, petComponent->GetPetAiState()); + + petComponent->SetPetAiState(PetAiState::interact); + ASSERT_EQ(PetAiState::interact, petComponent->GetPetAiState()); + + petComponent->SetPetAiState(PetAiState::despawn); + ASSERT_EQ(PetAiState::despawn, petComponent->GetPetAiState()); +} + +// Test the pet use logic +TEST_F(PetTest, PetUse) { + ASSERT_FALSE(petComponent->IsReadyToInteract()); + + petComponent->SetIsReadyToInteract(true); + ASSERT_TRUE(petComponent->IsReadyToInteract()); + + // Test bouncer logic + ASSERT_FALSE(petComponent->IsHandlingInteraction()); + petComponent->OnUse(baseEntity); + + // need to add a destroyable component to the test entity and test the imagination drain + }