mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 19:03:43 +00:00
The client reads the moderation and names bit whenever the pet component is dirty, on updates as well as on construction (LWOPetComponent::Deserialize), and live wrote a 0 there on updates. DLU wrote it only on construction, so on an update the client took the next component's first bit (a combat AI's dirty bit) for it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1445 lines
41 KiB
C++
1445 lines
41 KiB
C++
#include "PetComponent.h"
|
|
#include "DashboardNotify.h"
|
|
#include "GameMessages.h"
|
|
#include "PetMessages.h"
|
|
#include "PlayerMessages.h"
|
|
#include "InventoryMessages.h"
|
|
#include "EffectsMessages.h"
|
|
#include "ObjectMessages.h"
|
|
#include "BrickDatabase.h"
|
|
#include "CDClientDatabase.h"
|
|
#include "CDTamingBuildPuzzleTable.h"
|
|
#include "ChatPackets.h"
|
|
#include "EntityManager.h"
|
|
#include "Character.h"
|
|
#include "CharacterComponent.h"
|
|
#include "InventoryComponent.h"
|
|
#include "Item.h"
|
|
#include "MissionComponent.h"
|
|
#include "User.h"
|
|
#include "SwitchComponent.h"
|
|
#include "DestroyableComponent.h"
|
|
#include "dpWorld.h"
|
|
#include "UserManager.h"
|
|
#include "PetDigServer.h"
|
|
#include "ObjectIDManager.h"
|
|
#include "eUnequippableActiveType.h"
|
|
#include "eTerminateType.h"
|
|
#include "ePetTamingNotifyType.h"
|
|
#include "eUseItemResponse.h"
|
|
#include "ePlayerFlag.h"
|
|
|
|
#include "GeneralUtils.h"
|
|
#include "Game.h"
|
|
#include "dConfig.h"
|
|
#include "dChatFilter.h"
|
|
#include "Database.h"
|
|
#include "EntityInfo.h"
|
|
#include "eMissionTaskType.h"
|
|
#include "RenderComponent.h"
|
|
#include "eObjectBits.h"
|
|
#include "eGameMasterLevel.h"
|
|
#include "eMissionState.h"
|
|
#include "dNavMesh.h"
|
|
#include "eGameActivity.h"
|
|
#include "eStateChangeType.h"
|
|
|
|
std::unordered_map<LWOOBJID, LWOOBJID> PetComponent::currentActivities{};
|
|
std::unordered_map<LWOOBJID, LWOOBJID> PetComponent::activePets{};
|
|
|
|
/**
|
|
* Maps all the pet lots to a flag indicating that the player has caught it. All basic pets have been guessed by ObjID
|
|
* while the faction ones could be checked using their respective missions.
|
|
*/
|
|
|
|
PetComponent::PetComponent(Entity* parentEntity, const int32_t componentID) : Component{ parentEntity, componentID } {
|
|
m_PetInfo = CDClientManager::GetTable<CDPetComponentTable>()->GetByID(componentID); // TODO: Make reference when safe
|
|
|
|
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_MovementAI = nullptr;
|
|
m_TreasureTime = 0;
|
|
|
|
std::string checkPreconditions = GeneralUtils::UTF16ToWTF8(parentEntity->GetVar<std::u16string>(u"CheckPrecondition"));
|
|
|
|
if (!checkPreconditions.empty()) {
|
|
SetPreconditions(checkPreconditions);
|
|
}
|
|
}
|
|
|
|
void PetComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) {
|
|
const bool tamed = m_Owner != LWOOBJID_EMPTY;
|
|
|
|
outBitStream.Write1(); // Always serialize as dirty for now
|
|
|
|
outBitStream.Write<uint32_t>(m_Status);
|
|
outBitStream.Write(tamed ? m_Ability : ePetAbilityType::Invalid); // Something with the overhead icon?
|
|
|
|
const bool interacting = m_Interaction != LWOOBJID_EMPTY;
|
|
|
|
outBitStream.Write(interacting);
|
|
if (interacting) {
|
|
outBitStream.Write(m_Interaction);
|
|
}
|
|
|
|
outBitStream.Write(tamed);
|
|
if (tamed) {
|
|
outBitStream.Write(m_Owner);
|
|
}
|
|
|
|
// The client reads this bit on every update while dirty (as live wrote it): the names follow when it is set
|
|
if (!bIsInitialUpdate) {
|
|
outBitStream.Write0();
|
|
} else {
|
|
outBitStream.Write(tamed);
|
|
if (tamed) {
|
|
outBitStream.Write(m_ModerationStatus);
|
|
|
|
const auto nameData = GeneralUtils::UTF8ToUTF16(m_Name);
|
|
const auto ownerNameData = GeneralUtils::UTF8ToUTF16(m_OwnerName);
|
|
|
|
outBitStream.Write<uint8_t>(nameData.size());
|
|
for (const auto c : nameData) {
|
|
outBitStream.Write(c);
|
|
}
|
|
|
|
outBitStream.Write<uint8_t>(ownerNameData.size());
|
|
for (const auto c : ownerNameData) {
|
|
outBitStream.Write(c);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void PetComponent::OnUse(Entity* originator) {
|
|
if (m_Owner != LWOOBJID_EMPTY) {
|
|
// The owner uses their pet with SHIFT or the pet menu's action button: the client sends RequestUse on the pet
|
|
// (LWOPetControlComponent::msgPetCommand, "contextAction")
|
|
if (originator->GetObjectID() == m_Owner) UseAbility();
|
|
return;
|
|
}
|
|
|
|
if (m_Tamer != LWOOBJID_EMPTY) {
|
|
auto* tamer = Game::entityManager->GetEntity(m_Tamer);
|
|
|
|
if (tamer != nullptr) {
|
|
return;
|
|
}
|
|
|
|
m_Tamer = LWOOBJID_EMPTY;
|
|
}
|
|
|
|
auto* const inventoryComponent = originator->GetComponent<InventoryComponent>();
|
|
if (inventoryComponent == nullptr) {
|
|
return;
|
|
}
|
|
|
|
if (m_Preconditions.has_value()) {
|
|
if (!m_Preconditions->Check(originator)) return;
|
|
// Taming uses up the items its preconditions ask for
|
|
for (const auto& cost : m_Preconditions->GetItemCosts(originator)) {
|
|
inventoryComponent->RemoveItem(cost.lot, cost.count, eInventoryType::ALL);
|
|
}
|
|
}
|
|
|
|
auto* const movementAIComponent = m_Parent->GetComponent<MovementAIComponent>();
|
|
if (movementAIComponent != nullptr) {
|
|
movementAIComponent->Stop();
|
|
}
|
|
|
|
inventoryComponent->DespawnPet();
|
|
|
|
const auto* const entry = CDClientManager::GetTable<CDTamingBuildPuzzleTable>()->GetByLOT(m_Parent->GetLOT());
|
|
if (!entry) {
|
|
ChatPackets::SendSystemMessage(originator->GetSystemAddress(), u"Failed to find the puzzle minigame for this pet.");
|
|
return;
|
|
}
|
|
|
|
const auto* const destroyableComponent = originator->GetComponent<DestroyableComponent>();
|
|
if (destroyableComponent == nullptr) {
|
|
return;
|
|
}
|
|
|
|
const auto imagination = destroyableComponent->GetImagination();
|
|
if (imagination < entry->imaginationCost) {
|
|
return;
|
|
}
|
|
|
|
const auto& bricks = BrickDatabase::GetBricks(entry->validPieces);
|
|
if (bricks.empty()) {
|
|
ChatPackets::SendSystemMessage(originator->GetSystemAddress(), u"Failed to load the puzzle minigame for this pet.");
|
|
LOG("Couldn't find %s for minigame!", entry->validPieces.c_str());
|
|
|
|
return;
|
|
}
|
|
|
|
// Where live put the minigame (2014 captures, the same for every try at a pet): the pet goes back to where it
|
|
// spawned, facing the way it spawned, and the player is teleported 12 units in front of it, facing the pet.
|
|
if (m_StartPosition == NiPoint3Constant::ZERO) {
|
|
m_StartPosition = m_Parent->GetPosition();
|
|
m_StartRotation = m_Parent->GetRotation();
|
|
}
|
|
|
|
constexpr float TAMING_PLAYER_DISTANCE = 12.0f;
|
|
|
|
auto petPosition = m_StartPosition;
|
|
auto forward = QuatUtils::Forward(m_StartRotation);
|
|
forward.y = 0;
|
|
forward = forward.SquaredLength() > 0.0f ? forward.Unitize() : NiPoint3Constant::UNIT_Z;
|
|
auto position = petPosition + forward * TAMING_PLAYER_DISTANCE;
|
|
|
|
if (dpWorld::IsLoaded()) {
|
|
petPosition.y = dpWorld::GetNavMesh()->GetHeightAtPoint(petPosition);
|
|
position.y = dpWorld::GetNavMesh()->GetHeightAtPoint(position);
|
|
}
|
|
|
|
const auto rotation = QuatUtils::AxisAngle(NiPoint3Constant::UNIT_Y, glm::pi<float>()) * m_StartRotation;
|
|
|
|
m_Parent->SetPosition(petPosition);
|
|
m_Parent->SetRotation(m_StartRotation);
|
|
|
|
{
|
|
GameMessages::NotifyPetTamingMinigame msg;
|
|
msg.target = originator->GetObjectID();
|
|
msg.PetID = m_Parent->GetObjectID();
|
|
msg.PlayerTamingID = LWOOBJID_EMPTY;
|
|
msg.bForceTeleport = true;
|
|
msg.notifyType = ePetTamingNotifyType::BEGIN;
|
|
msg.petsDestPos = petPosition;
|
|
msg.telePos = position;
|
|
msg.teleRot = rotation;
|
|
msg.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|
|
|
|
{
|
|
GameMessages::NotifyPetTamingMinigame msg;
|
|
msg.target = m_Parent->GetObjectID();
|
|
msg.PetID = LWOOBJID_EMPTY;
|
|
msg.PlayerTamingID = originator->GetObjectID();
|
|
msg.bForceTeleport = false;
|
|
msg.notifyType = ePetTamingNotifyType::BEGIN;
|
|
msg.petsDestPos = NiPoint3Constant::ZERO;
|
|
msg.telePos = NiPoint3Constant::ZERO;
|
|
msg.teleRot = NiQuaternion(0.0f, 0.0f, 0.0f, 0.0f);
|
|
msg.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|
|
|
|
{
|
|
GameMessages::NotifyPetTamingPuzzleSelected msg;
|
|
msg.target = originator->GetObjectID();
|
|
msg.bricks = bricks;
|
|
msg.Send(originator->GetSystemAddress());
|
|
}
|
|
|
|
m_Tamer = originator->GetObjectID();
|
|
SetStatus(5);
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
|
|
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<CharacterComponent>();
|
|
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();
|
|
m_StartRotation = m_Parent->GetRotation();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// Going back to the backpack, removed once the despawn effect has played
|
|
if (m_Despawning) return;
|
|
|
|
auto* owner = GetOwner();
|
|
|
|
if (owner == nullptr) {
|
|
m_Parent->Kill();
|
|
|
|
return;
|
|
}
|
|
|
|
m_MovementAI = m_Parent->GetComponent<MovementAIComponent>();
|
|
|
|
if (m_MovementAI == nullptr) {
|
|
return;
|
|
}
|
|
|
|
if (m_TreasureTime > 0) {
|
|
auto* treasure = Game::entityManager->GetEntity(m_AbilityObject);
|
|
|
|
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_AbilityObject = 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;
|
|
|
|
Entity* abilityObject = nullptr;
|
|
auto objectAbility = ePetAbilityType::Invalid;
|
|
bool atAbilityObject = false;
|
|
|
|
if (closestSwitch != nullptr && !closestSwitch->GetActive()) {
|
|
NiPoint3 switchPosition = closestSwitch->GetParentEntity()->GetPosition();
|
|
float distance = Vector3::DistanceSquared(position, switchPosition);
|
|
if (distance < 20 * 20) {
|
|
abilityObject = closestSwitch->GetParentEntity();
|
|
objectAbility = ePetAbilityType::JumpOnObject;
|
|
atAbilityObject = distance < 3 * 3;
|
|
|
|
haltDistance = 1;
|
|
|
|
destination = switchPosition;
|
|
}
|
|
}
|
|
|
|
auto* missionComponent = owner->GetComponent<MissionComponent>();
|
|
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);
|
|
|
|
// Skeleton Dragon Pat special case for bone digging
|
|
if (closestTreasure != nullptr && digUnlocked && (closestTreasure->GetLOT() != 12192 || m_Parent->GetLOT() == 13067)) {
|
|
NiPoint3 treasurePosition = closestTreasure->GetPosition();
|
|
float distance = Vector3::DistanceSquared(position, treasurePosition);
|
|
if (distance < 10 * 10) {
|
|
abilityObject = closestTreasure;
|
|
objectAbility = ePetAbilityType::DigAtPosition;
|
|
atAbilityObject = distance < 5 * 5;
|
|
|
|
haltDistance = 1;
|
|
|
|
destination = treasurePosition;
|
|
}
|
|
}
|
|
|
|
SetAbilityObject(abilityObject, objectAbility, atAbilityObject);
|
|
|
|
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) {
|
|
m_Tamer = LWOOBJID_EMPTY;
|
|
|
|
return;
|
|
}
|
|
|
|
const auto* const entry = CDClientManager::GetTable<CDTamingBuildPuzzleTable>()->GetByLOT(m_Parent->GetLOT());
|
|
if (!entry) return;
|
|
|
|
auto* destroyableComponent = tamer->GetComponent<DestroyableComponent>();
|
|
|
|
if (destroyableComponent == nullptr) return;
|
|
|
|
auto imagination = destroyableComponent->GetImagination();
|
|
|
|
imagination -= entry->imaginationCost;
|
|
|
|
destroyableComponent->SetImagination(imagination);
|
|
|
|
Game::entityManager->SerializeEntity(tamer);
|
|
|
|
if (clientFailed) {
|
|
if (imagination < entry->imaginationCost) {
|
|
ClientFailTamingMinigame();
|
|
}
|
|
} else {
|
|
m_Timer = 0;
|
|
}
|
|
|
|
if (numBricks == 0) return;
|
|
|
|
GameMessages::PetTamingTryBuildResult msg;
|
|
msg.target = m_Tamer;
|
|
msg.bSuccess = !clientFailed;
|
|
msg.iNumCorrect = numBricks;
|
|
msg.Send(tamer->GetSystemAddress());
|
|
}
|
|
|
|
void PetComponent::NotifyTamingBuildSuccess(NiPoint3 position) {
|
|
if (m_Tamer == LWOOBJID_EMPTY) return;
|
|
|
|
auto* tamer = Game::entityManager->GetEntity(m_Tamer);
|
|
|
|
if (tamer == nullptr) {
|
|
m_Tamer = LWOOBJID_EMPTY;
|
|
|
|
return;
|
|
}
|
|
|
|
const auto* const entry = CDClientManager::GetTable<CDTamingBuildPuzzleTable>()->GetByLOT(m_Parent->GetLOT());
|
|
if (!entry) return;
|
|
|
|
GameMessages::PlayFXEffect(tamer->GetObjectID(), -1, u"petceleb", "").Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
RenderComponent::PlayAnimation(tamer, u"rebuild-celebrate");
|
|
|
|
EntityInfo info{};
|
|
info.lot = entry->puzzleModelLot;
|
|
info.pos = position;
|
|
info.rot = QuatUtils::IDENTITY;
|
|
info.spawnerID = tamer->GetObjectID();
|
|
|
|
auto* modelEntity = Game::entityManager->CreateEntity(info);
|
|
|
|
m_ModelId = modelEntity->GetObjectID();
|
|
|
|
Game::entityManager->ConstructEntity(modelEntity);
|
|
|
|
{
|
|
GameMessages::NotifyTamingModelLoadedOnServer msg;
|
|
msg.target = m_Tamer;
|
|
msg.Send(tamer->GetSystemAddress());
|
|
}
|
|
|
|
{
|
|
GameMessages::PetResponse msg;
|
|
msg.target = m_Tamer;
|
|
msg.ObjIDPet = m_Parent->GetObjectID();
|
|
msg.iPetCommandType = 0;
|
|
msg.iResponse = 10;
|
|
msg.iTypeID = 0;
|
|
msg.Send(tamer->GetSystemAddress());
|
|
}
|
|
|
|
auto* inventoryComponent = tamer->GetComponent<InventoryComponent>();
|
|
|
|
if (inventoryComponent == nullptr) {
|
|
return;
|
|
}
|
|
|
|
LWOOBJID petSubKey = ObjectIDManager::GetPersistentID();
|
|
const uint32_t maxTries = 100;
|
|
uint32_t tries = 0;
|
|
while (Database::Get()->GetPetNameInfo(petSubKey) && tries < maxTries) {
|
|
tries++;
|
|
LOG("Found a duplicate pet %llu, getting a new subKey", petSubKey);
|
|
petSubKey = ObjectIDManager::GetPersistentID();
|
|
}
|
|
|
|
if (tries >= maxTries) {
|
|
LOG("Failed to get a unique pet subKey after %i tries, aborting pet creation for player %i", maxTries, tamer->GetCharacter() ? tamer->GetCharacter()->GetID() : -1);
|
|
return;
|
|
}
|
|
|
|
m_DatabaseId = petSubKey;
|
|
|
|
std::string petName = tamer->GetCharacter()->GetName();
|
|
petName += "'s Pet";
|
|
|
|
{
|
|
GameMessages::AddPetToPlayer msg;
|
|
msg.target = m_Tamer;
|
|
msg.iElementalType = 0;
|
|
msg.name = GeneralUtils::UTF8ToUTF16(petName);
|
|
msg.petDBID = petSubKey;
|
|
msg.petLOT = m_Parent->GetLOT();
|
|
msg.Send(tamer->GetSystemAddress());
|
|
}
|
|
|
|
{
|
|
GameMessages::RegisterPetID msg;
|
|
msg.target = m_Tamer;
|
|
msg.objID = m_Parent->GetObjectID();
|
|
msg.Send(tamer->GetSystemAddress());
|
|
}
|
|
|
|
{
|
|
GameMessages::RegisterPetDBID msg;
|
|
msg.target = m_Tamer;
|
|
msg.petDBID = petSubKey;
|
|
msg.Send(tamer->GetSystemAddress());
|
|
}
|
|
|
|
inventoryComponent->AddItem(m_Parent->GetLOT(), 1, eLootSourceType::INVENTORY, eInventoryType::MODELS, {}, LWOOBJID_EMPTY, true, false, petSubKey);
|
|
auto* item = inventoryComponent->FindItemBySubKey(petSubKey, MODELS);
|
|
|
|
if (item == nullptr) {
|
|
return;
|
|
}
|
|
|
|
DatabasePet databasePet{};
|
|
|
|
databasePet.lot = m_Parent->GetLOT();
|
|
databasePet.moderationState = 1;
|
|
databasePet.name = petName;
|
|
|
|
inventoryComponent->SetDatabasePet(petSubKey, databasePet);
|
|
|
|
Activate(item, false, true);
|
|
|
|
m_Timer = 0;
|
|
|
|
{
|
|
GameMessages::NotifyPetTamingMinigame msg;
|
|
msg.target = m_Tamer;
|
|
msg.PetID = LWOOBJID_EMPTY;
|
|
msg.PlayerTamingID = LWOOBJID_EMPTY;
|
|
msg.bForceTeleport = false;
|
|
msg.notifyType = ePetTamingNotifyType::NAMINGPET;
|
|
msg.petsDestPos = NiPoint3Constant::ZERO;
|
|
msg.telePos = NiPoint3Constant::ZERO;
|
|
msg.teleRot = QuatUtils::IDENTITY;
|
|
msg.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|
|
|
|
// Triggers the catch a pet missions
|
|
constexpr auto PET_FLAG_BASE = 800;
|
|
tamer->GetCharacter()->SetPlayerFlag(PET_FLAG_BASE + m_ComponentID, true);
|
|
|
|
auto* missionComponent = tamer->GetComponent<MissionComponent>();
|
|
|
|
if (missionComponent != nullptr) {
|
|
missionComponent->Progress(eMissionTaskType::PET_TAMING, m_Parent->GetLOT());
|
|
}
|
|
|
|
SetStatus(1);
|
|
|
|
auto* characterComponent = tamer->GetComponent<CharacterComponent>();
|
|
if (characterComponent != nullptr) {
|
|
characterComponent->UpdatePlayerStatistic(PetsTamed);
|
|
}
|
|
}
|
|
|
|
void PetComponent::RequestSetPetName(std::u16string name) {
|
|
const bool autoRejectNames = UserManager::Instance()->GetMuteAutoRejectNames();
|
|
|
|
if (m_Tamer == LWOOBJID_EMPTY) {
|
|
if (m_Owner != LWOOBJID_EMPTY) {
|
|
auto* owner = GetOwner();
|
|
|
|
// If auto reject names is on, and the user is muted, force use of predefined name
|
|
if (autoRejectNames && owner && owner->GetCharacter() && owner->GetCharacter()->GetParentUser()->GetIsMuted()) {
|
|
m_ModerationStatus = 2; // Approved
|
|
std::string forcedName = "Pet";
|
|
Database::Get()->SetPetNameModerationStatus(m_DatabaseId, IPetNames::Info{ forcedName, static_cast<int32_t>(m_ModerationStatus), m_Owner, m_Parent->GetLOT() });
|
|
DashboardNotify::Changed("pet_names", m_DatabaseId);
|
|
{
|
|
GameMessages::SetPetName msg;
|
|
msg.target = m_Owner;
|
|
msg.name = GeneralUtils::UTF8ToUTF16(m_Name);
|
|
msg.petDBID = m_DatabaseId;
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
{
|
|
GameMessages::SetPetNameModerated msg;
|
|
msg.target = m_Owner;
|
|
msg.PetDBID = m_DatabaseId;
|
|
msg.nModerationStatus = m_ModerationStatus;
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
} else {
|
|
m_ModerationStatus = 1; // Pending
|
|
m_Name = "";
|
|
|
|
//Save our pet's new name to the db:
|
|
SetPetNameForModeration(GeneralUtils::UTF16ToWTF8(name), m_Owner);
|
|
|
|
{
|
|
GameMessages::SetPetName msg;
|
|
msg.target = m_Owner;
|
|
msg.name = GeneralUtils::UTF8ToUTF16(m_Name);
|
|
msg.petDBID = m_DatabaseId;
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
{
|
|
GameMessages::SetPetNameModerated msg;
|
|
msg.target = m_Owner;
|
|
msg.PetDBID = m_DatabaseId;
|
|
msg.nModerationStatus = m_ModerationStatus;
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
auto* 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<InventoryComponent>();
|
|
|
|
if (inventoryComponent == nullptr) {
|
|
return;
|
|
}
|
|
|
|
// If auto reject names is on, and the user is muted, force use of predefined name ELSE proceed with normal name check
|
|
if (autoRejectNames && tamer->GetCharacter() && tamer->GetCharacter()->GetParentUser()->GetIsMuted()) {
|
|
m_ModerationStatus = 2; // Approved
|
|
m_Name = "";
|
|
std::string forcedName = "Pet";
|
|
|
|
Database::Get()->SetPetNameModerationStatus(m_DatabaseId, IPetNames::Info{ forcedName, static_cast<int32_t>(m_ModerationStatus), m_Tamer, m_Parent->GetLOT() });
|
|
DashboardNotify::Changed("pet_names", m_DatabaseId);
|
|
LOG("AccountID: %i is muted, forcing use of predefined pet name", tamer->GetCharacter()->GetParentUser()->GetAccountID());
|
|
} else {
|
|
m_ModerationStatus = 1; // Pending
|
|
m_Name = "";
|
|
|
|
//Save our pet's new name to the db:
|
|
SetPetNameForModeration(GeneralUtils::UTF16ToWTF8(name), m_Tamer);
|
|
}
|
|
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
|
|
std::u16string u16name = GeneralUtils::UTF8ToUTF16(m_Name);
|
|
std::u16string u16ownerName = GeneralUtils::UTF8ToUTF16(m_OwnerName);
|
|
{
|
|
GameMessages::SetPetName msg;
|
|
msg.target = m_Tamer;
|
|
msg.name = u16name;
|
|
msg.petDBID = m_DatabaseId;
|
|
msg.Send(tamer->GetSystemAddress());
|
|
}
|
|
{
|
|
GameMessages::SetPetName msg;
|
|
msg.target = m_Tamer;
|
|
msg.name = u16name;
|
|
msg.petDBID = LWOOBJID_EMPTY;
|
|
msg.Send(tamer->GetSystemAddress());
|
|
}
|
|
{
|
|
GameMessages::PetNameChanged msg;
|
|
msg.target = m_Parent->GetObjectID();
|
|
msg.moderationStatus = m_ModerationStatus;
|
|
msg.name = u16name;
|
|
msg.ownerName = u16ownerName;
|
|
msg.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|
|
{
|
|
GameMessages::SetPetNameModerated msg;
|
|
msg.target = m_Tamer;
|
|
msg.PetDBID = m_DatabaseId;
|
|
msg.nModerationStatus = m_ModerationStatus;
|
|
msg.Send(tamer->GetSystemAddress());
|
|
}
|
|
|
|
{
|
|
GameMessages::NotifyPetTamingMinigame msg;
|
|
msg.target = m_Tamer;
|
|
msg.PetID = m_Parent->GetObjectID();
|
|
msg.PlayerTamingID = m_Tamer;
|
|
msg.bForceTeleport = false;
|
|
msg.notifyType = ePetTamingNotifyType::SUCCESS;
|
|
msg.petsDestPos = NiPoint3Constant::ZERO;
|
|
msg.telePos = NiPoint3Constant::ZERO;
|
|
msg.teleRot = QuatUtils::IDENTITY;
|
|
msg.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|
|
|
|
auto* characterComponent = tamer->GetComponent<CharacterComponent>();
|
|
if (characterComponent != nullptr) {
|
|
characterComponent->SetCurrentActivity(eGameActivity::NONE);
|
|
Game::entityManager->SerializeEntity(tamer);
|
|
}
|
|
GameMessages::TerminateInteraction(m_Tamer, eTerminateType::FROM_INTERACTION, m_Parent->GetObjectID()).Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
|
|
auto* 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
|
|
m_Parent->GetScript()->OnNotifyPetTamingMinigame(m_Parent, tamer, ePetTamingNotifyType::SUCCESS);
|
|
}
|
|
|
|
void PetComponent::ClientExitTamingMinigame(bool voluntaryExit) {
|
|
if (m_Tamer == LWOOBJID_EMPTY) return;
|
|
|
|
auto* tamer = Game::entityManager->GetEntity(m_Tamer);
|
|
|
|
if (tamer == nullptr) {
|
|
m_Tamer = LWOOBJID_EMPTY;
|
|
|
|
return;
|
|
}
|
|
|
|
{
|
|
GameMessages::NotifyPetTamingMinigame msg;
|
|
msg.target = m_Tamer;
|
|
msg.PetID = m_Parent->GetObjectID();
|
|
msg.PlayerTamingID = m_Tamer;
|
|
msg.bForceTeleport = false;
|
|
msg.notifyType = ePetTamingNotifyType::QUIT;
|
|
msg.petsDestPos = NiPoint3Constant::ZERO;
|
|
msg.telePos = NiPoint3Constant::ZERO;
|
|
msg.teleRot = QuatUtils::IDENTITY;
|
|
msg.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|
|
|
|
auto* characterComponent = tamer->GetComponent<CharacterComponent>();
|
|
if (characterComponent != nullptr) {
|
|
characterComponent->SetCurrentActivity(eGameActivity::NONE);
|
|
Game::entityManager->SerializeEntity(tamer);
|
|
}
|
|
{
|
|
GameMessages::NotifyTamingModelLoadedOnServer msg;
|
|
msg.target = m_Tamer;
|
|
msg.Send(tamer->GetSystemAddress());
|
|
}
|
|
|
|
GameMessages::TerminateInteraction(m_Tamer, eTerminateType::FROM_INTERACTION, m_Parent->GetObjectID()).Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
|
|
currentActivities.erase(m_Tamer);
|
|
|
|
SetStatus(67108866);
|
|
m_Tamer = LWOOBJID_EMPTY;
|
|
m_Timer = 0;
|
|
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
|
|
// Notify the end of a pet taming minigame
|
|
m_Parent->GetScript()->OnNotifyPetTamingMinigame(m_Parent, tamer, ePetTamingNotifyType::QUIT);
|
|
}
|
|
|
|
void PetComponent::StartTimer() {
|
|
const auto* const entry = CDClientManager::GetTable<CDTamingBuildPuzzleTable>()->GetByLOT(m_Parent->GetLOT());
|
|
if (!entry) return;
|
|
|
|
m_Timer = entry->timeLimit;
|
|
}
|
|
|
|
void PetComponent::ClientFailTamingMinigame() {
|
|
if (m_Tamer == LWOOBJID_EMPTY) return;
|
|
|
|
auto* tamer = Game::entityManager->GetEntity(m_Tamer);
|
|
|
|
if (tamer == nullptr) {
|
|
m_Tamer = LWOOBJID_EMPTY;
|
|
|
|
return;
|
|
}
|
|
|
|
{
|
|
GameMessages::NotifyPetTamingMinigame msg;
|
|
msg.target = m_Tamer;
|
|
msg.PetID = m_Parent->GetObjectID();
|
|
msg.PlayerTamingID = m_Tamer;
|
|
msg.bForceTeleport = false;
|
|
msg.notifyType = ePetTamingNotifyType::FAILED;
|
|
msg.petsDestPos = NiPoint3Constant::ZERO;
|
|
msg.telePos = NiPoint3Constant::ZERO;
|
|
msg.teleRot = QuatUtils::IDENTITY;
|
|
msg.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|
|
|
|
auto* characterComponent = tamer->GetComponent<CharacterComponent>();
|
|
if (characterComponent != nullptr) {
|
|
characterComponent->SetCurrentActivity(eGameActivity::NONE);
|
|
Game::entityManager->SerializeEntity(tamer);
|
|
}
|
|
{
|
|
GameMessages::NotifyTamingModelLoadedOnServer msg;
|
|
msg.target = m_Tamer;
|
|
msg.Send(tamer->GetSystemAddress());
|
|
}
|
|
|
|
GameMessages::TerminateInteraction(m_Tamer, eTerminateType::FROM_INTERACTION, m_Parent->GetObjectID()).Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
|
|
currentActivities.erase(m_Tamer);
|
|
|
|
SetStatus(67108866);
|
|
m_Tamer = LWOOBJID_EMPTY;
|
|
m_Timer = 0;
|
|
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
|
|
// Notify the end of a pet taming minigame
|
|
m_Parent->GetScript()->OnNotifyPetTamingMinigame(m_Parent, tamer, ePetTamingNotifyType::FAILED);
|
|
}
|
|
|
|
void PetComponent::Wander() {
|
|
m_MovementAI = m_Parent->GetComponent<MovementAIComponent>();
|
|
|
|
if (m_MovementAI == nullptr || !m_MovementAI->AtFinalWaypoint()) {
|
|
return;
|
|
}
|
|
|
|
m_MovementAI->SetHaltDistance(0);
|
|
|
|
const auto& info = m_MovementAI->GetInfo();
|
|
|
|
const auto div = static_cast<int>(info.wanderDelayMax);
|
|
m_Timer = (div == 0 ? 0 : GeneralUtils::GenerateRandomNumber<int>(0, div)) + info.wanderDelayMin; //set a random timer to stay put.
|
|
|
|
const float radius = info.wanderRadius * sqrt(static_cast<double>(GeneralUtils::GenerateRandomNumber<float>(0, 1))); //our wander radius + a bit of random range
|
|
const float theta = ((static_cast<double>(GeneralUtils::GenerateRandomNumber<float>(0, 1)) * 2 * PI));
|
|
|
|
const NiPoint3 delta =
|
|
{
|
|
radius * cos(theta),
|
|
0,
|
|
radius * sin(theta)
|
|
};
|
|
|
|
auto destination = m_StartPosition + delta;
|
|
|
|
if (dpWorld::IsLoaded()) {
|
|
destination.y = dpWorld::GetNavMesh()->GetHeightAtPoint(destination);
|
|
}
|
|
|
|
if (Vector3::DistanceSquared(destination, m_MovementAI->GetParent()->GetPosition()) < 2 * 2) {
|
|
m_MovementAI->Stop();
|
|
|
|
return;
|
|
}
|
|
|
|
m_MovementAI->SetMaxSpeed(m_PetInfo.sprintSpeed);
|
|
|
|
m_MovementAI->SetDestination(destination);
|
|
|
|
m_Timer += (m_MovementAI->GetParent()->GetPosition().x - destination.x) / m_PetInfo.sprintSpeed;
|
|
}
|
|
|
|
void PetComponent::Activate(Item* item, bool registerPet, bool fromTaming) {
|
|
m_ItemId = item->GetId();
|
|
m_DatabaseId = item->GetSubKey();
|
|
|
|
auto* inventoryComponent = item->GetInventory()->GetComponent();
|
|
|
|
if (inventoryComponent == nullptr) return;
|
|
|
|
inventoryComponent->DespawnPet();
|
|
|
|
m_Owner = inventoryComponent->GetParent()->GetObjectID();
|
|
AddDrainImaginationTimer(fromTaming);
|
|
|
|
auto* owner = GetOwner();
|
|
|
|
if (owner == nullptr) return;
|
|
|
|
if (registerPet) {
|
|
// Summoned from the backpack. Live constructed the pet in the spawning state, so the client plays its
|
|
// spawn animation, played the pet's "despawn" effect (the circles and stars) and then cleared the state.
|
|
SetStatus(1 | PET_STATE_SPAWNING);
|
|
m_Parent->AddCallbackTimer(RenderComponent::GetAnimationTime(m_Parent, "spawn"), [this]() {
|
|
GameMessages::PlayFXEffect(m_Parent->GetObjectID(), -1, u"despawn", "").Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
SetStatus(m_Status & ~PET_STATE_SPAWNING);
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
});
|
|
} else {
|
|
SetStatus(1);
|
|
}
|
|
|
|
auto databaseData = inventoryComponent->GetDatabasePet(m_DatabaseId);
|
|
|
|
m_ModerationStatus = databaseData.moderationState;
|
|
|
|
bool updatedModerationStatus = false;
|
|
|
|
//Load mod status from db:
|
|
if (m_ModerationStatus != 2) {
|
|
LoadPetNameFromModeration();
|
|
|
|
databaseData.name = m_Name;
|
|
databaseData.moderationState = m_ModerationStatus;
|
|
|
|
inventoryComponent->SetDatabasePet(m_DatabaseId, databaseData);
|
|
|
|
updatedModerationStatus = true;
|
|
} else {
|
|
m_Name = databaseData.name;
|
|
}
|
|
|
|
m_OwnerName = owner->GetCharacter()->GetName();
|
|
|
|
if (updatedModerationStatus) {
|
|
{
|
|
GameMessages::SetPetName msg;
|
|
msg.target = m_Owner;
|
|
msg.name = GeneralUtils::UTF8ToUTF16(m_Name);
|
|
msg.petDBID = m_DatabaseId;
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
{
|
|
GameMessages::SetPetNameModerated msg;
|
|
msg.target = m_Owner;
|
|
msg.PetDBID = m_DatabaseId;
|
|
msg.nModerationStatus = m_ModerationStatus;
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
}
|
|
|
|
GameMessages::MarkInventoryItemAsActive markActive;
|
|
markActive.target = m_Owner;
|
|
markActive.bActive = true;
|
|
markActive.iType = eUnequippableActiveType::PET;
|
|
markActive.itemID = m_ItemId;
|
|
markActive.Send(GetOwner()->GetSystemAddress());
|
|
|
|
activePets[m_Owner] = m_Parent->GetObjectID();
|
|
|
|
m_Timer = 3;
|
|
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
|
|
owner->GetCharacter()->SetPlayerFlag(ePlayerFlag::FIRST_MANUAL_PET_HIBERNATE, true);
|
|
|
|
if (registerPet) {
|
|
{
|
|
GameMessages::AddPetToPlayer msg;
|
|
msg.target = m_Owner;
|
|
msg.iElementalType = 0;
|
|
msg.name = GeneralUtils::UTF8ToUTF16(m_Name);
|
|
msg.petDBID = m_DatabaseId;
|
|
msg.petLOT = m_Parent->GetLOT();
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
|
|
{
|
|
GameMessages::RegisterPetID msg;
|
|
msg.target = m_Owner;
|
|
msg.objID = m_Parent->GetObjectID();
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
|
|
{
|
|
GameMessages::RegisterPetDBID msg;
|
|
msg.target = m_Owner;
|
|
msg.petDBID = m_DatabaseId;
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
}
|
|
}
|
|
|
|
void PetComponent::AddDrainImaginationTimer(bool fromTaming) {
|
|
if (Game::config->GetValue("pets_take_imagination") != "1") return;
|
|
|
|
auto* playerEntity = Game::entityManager->GetEntity(m_Owner);
|
|
if (!playerEntity) {
|
|
LOG("owner was null or didnt exist!");
|
|
return;
|
|
}
|
|
|
|
auto playerDestroyableComponent = playerEntity->GetComponent<DestroyableComponent>();
|
|
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.
|
|
if (!fromTaming) playerDestroyableComponent->Imagine(-1);
|
|
|
|
// Set this to a variable so when this is called back from the player the timer doesn't fire off.
|
|
m_Parent->AddCallbackTimer(m_PetInfo.imaginationDrainRate, [this]() {
|
|
const auto* owner = Game::entityManager->GetEntity(m_Owner);
|
|
if (!owner) {
|
|
LOG("owner was null or didnt exist!");
|
|
return;
|
|
}
|
|
|
|
const auto* playerDestroyableComponent = owner->GetComponent<DestroyableComponent>();
|
|
if (!playerDestroyableComponent) return;
|
|
|
|
// Already going back to the backpack
|
|
if (m_Despawning) return;
|
|
|
|
// If we are out of imagination despawn the pet.
|
|
if (playerDestroyableComponent->GetImagination() == 0) {
|
|
this->Deactivate();
|
|
auto playerEntity = playerDestroyableComponent->GetParent();
|
|
if (!playerEntity) return;
|
|
|
|
GameMessages::UseItemRequirementsResponse requirementsResponse;
|
|
requirementsResponse.target = playerEntity->GetObjectID();
|
|
requirementsResponse.eUseResponse = eUseItemResponse::NoImaginationForPet;
|
|
requirementsResponse.SendToClient(playerEntity->GetSystemAddress());
|
|
// The pet is gone: no more draining
|
|
return;
|
|
}
|
|
|
|
this->AddDrainImaginationTimer();
|
|
});
|
|
}
|
|
|
|
void PetComponent::Deactivate() {
|
|
if (m_Despawning) return;
|
|
m_Despawning = true;
|
|
|
|
GameMessages::PlayFXEffect(m_Parent->GetObjectID(), -1, u"despawn", "").Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
|
|
activePets.erase(m_Owner);
|
|
|
|
auto* const movementAI = m_Parent->GetComponent<MovementAIComponent>();
|
|
if (movementAI) movementAI->Stop();
|
|
|
|
// Live removed the pet only once its despawn effect had played; removing it at once cut the effect short
|
|
m_Parent->AddCallbackTimer(RenderComponent::GetAnimationTime(m_Parent, "despawn"), [this]() {
|
|
m_Parent->Kill();
|
|
});
|
|
|
|
auto* owner = GetOwner();
|
|
|
|
if (owner == nullptr) return;
|
|
|
|
// The client looks the item up by this ID to grey it out again (LWOInventoryComponent_Common::SendMessage), and
|
|
// items get new IDs when they move, so use the ID the pet's item has now
|
|
auto* const inventoryComponent = owner->GetComponent<InventoryComponent>();
|
|
const auto* const item = inventoryComponent ? inventoryComponent->FindItemBySubKey(m_DatabaseId) : nullptr;
|
|
|
|
GameMessages::MarkInventoryItemAsActive markActive;
|
|
markActive.target = m_Owner;
|
|
markActive.bActive = false;
|
|
markActive.iType = eUnequippableActiveType::PET;
|
|
markActive.itemID = item ? item->GetId() : m_ItemId;
|
|
markActive.Send(owner->GetSystemAddress());
|
|
|
|
{
|
|
GameMessages::RegisterPetID msg;
|
|
msg.target = m_Owner;
|
|
msg.objID = LWOOBJID_EMPTY;
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
|
|
{
|
|
GameMessages::RegisterPetDBID msg;
|
|
msg.target = m_Owner;
|
|
msg.petDBID = LWOOBJID_EMPTY;
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
|
|
{
|
|
GameMessages::ShowPetActionButton msg;
|
|
msg.target = m_Owner;
|
|
msg.ButtonLabel = ePetAbilityType::Invalid;
|
|
msg.bShow = false;
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
}
|
|
|
|
void PetComponent::Release() {
|
|
auto* const owner = GetOwner();
|
|
if (!owner) return;
|
|
auto* const inventoryComponent = owner->GetComponent<InventoryComponent>();
|
|
|
|
if (inventoryComponent == nullptr) {
|
|
return;
|
|
}
|
|
|
|
Deactivate();
|
|
|
|
inventoryComponent->RemoveDatabasePet(m_DatabaseId);
|
|
|
|
auto* const item = inventoryComponent->FindItemBySubKey(m_DatabaseId);
|
|
|
|
if (item) 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();
|
|
if (!owner) return;
|
|
|
|
if (commandType == 1) {
|
|
// Emotes
|
|
GameMessages::PlayEmote playEmote;
|
|
playEmote.target = m_Parent->GetObjectID();
|
|
playEmote.emoteID = typeId;
|
|
playEmote.targetID = owner->GetObjectID();
|
|
playEmote.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
GameMessages::EmotePlayed msg;
|
|
msg.target = owner->GetObjectID();
|
|
msg.emoteID = typeId;
|
|
msg.targetID = 0; // Or set to the intended target entity's ID, or 0 if no target
|
|
msg.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
} else if (commandType == 3) {
|
|
// Follow me, ???
|
|
} else if (commandType == 6) {
|
|
// TODO: Go to player
|
|
}
|
|
|
|
if (owner->GetGMLevel() >= eGameMasterLevel::DEVELOPER) {
|
|
ChatPackets::SendSystemMessage(owner->GetSystemAddress(), u"Commmand Type: " + (GeneralUtils::to_u16string(commandType)) + u" - Type Id: " + (GeneralUtils::to_u16string(typeId)));
|
|
}
|
|
}
|
|
|
|
void PetComponent::SetAbilityObject(Entity* object, const ePetAbilityType ability, const bool atObject) {
|
|
const auto objectId = object ? object->GetObjectID() : LWOOBJID_EMPTY;
|
|
const auto status = m_Status & ~(PET_STATE_AT_ABILITY_OBJECT | PET_STATE_USING_ABILITY | PET_STATE_GOING_TO_ABILITY_OBJECT);
|
|
|
|
// What live did (2014 captures): on the way the pet has state 0x500 and the Go To Object ability, the tutorial
|
|
// for the object is shown; at the object it has 0x120 and the object's ability, and the owner gets the button.
|
|
if (objectId != m_AbilityObject) {
|
|
HideAbilityButton();
|
|
m_AbilityObject = objectId;
|
|
|
|
if (!object) {
|
|
m_Ability = ePetAbilityType::Invalid;
|
|
SetStatus(status);
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
return;
|
|
}
|
|
|
|
m_Ability = ePetAbilityType::GoToObject;
|
|
SetStatus(status | PET_STATE_USING_ABILITY | PET_STATE_GOING_TO_ABILITY_OBJECT);
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
|
|
if (ability == ePetAbilityType::DigAtPosition) SendHelp(eHelpType::PR_DIG_TUTORIAL_01);
|
|
else if (ability == ePetAbilityType::JumpOnObject) SendHelp(eHelpType::PR_BOUNCER_TUTORIAL_01);
|
|
}
|
|
|
|
if (!atObject || m_AbilityButtonShown) return;
|
|
|
|
m_Ability = ability;
|
|
SetStatus(status | PET_STATE_USING_ABILITY | PET_STATE_AT_ABILITY_OBJECT);
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
|
|
if (ability == ePetAbilityType::JumpOnObject) {
|
|
// On the pet switch, as live did: the pet gets excited and the switch engages
|
|
RenderComponent::PlayAnimation(m_Parent, u"excited");
|
|
RenderComponent::PlayAnimation(object, u"engaged");
|
|
SendHelp(eHelpType::PR_BOUNCER_TUTORIAL_03);
|
|
}
|
|
|
|
auto* const owner = GetOwner();
|
|
if (!owner) return;
|
|
|
|
GameMessages::ShowPetActionButton msg;
|
|
msg.target = m_Owner;
|
|
msg.ButtonLabel = ability;
|
|
msg.bShow = true;
|
|
msg.Send(owner->GetSystemAddress());
|
|
m_AbilityButtonShown = true;
|
|
}
|
|
|
|
void PetComponent::UseAbility() {
|
|
if (!m_AbilityButtonShown || m_Despawning) return;
|
|
|
|
auto* const object = Game::entityManager->GetEntity(m_AbilityObject);
|
|
auto* const owner = GetOwner();
|
|
if (!object || !owner) return;
|
|
|
|
auto* const destroyableComponent = owner->GetComponent<DestroyableComponent>();
|
|
if (!destroyableComponent) return;
|
|
|
|
// Using the ability costs imagination (PetAbilities: 2 to jump on a pet switch, 1 to dig)
|
|
const auto cost = GetAbilityImaginationCost(m_Ability);
|
|
if (destroyableComponent->GetImagination() < cost) return;
|
|
destroyableComponent->Imagine(-cost);
|
|
|
|
const auto ability = m_Ability;
|
|
HideAbilityButton();
|
|
|
|
if (ability == ePetAbilityType::DigAtPosition) {
|
|
SendHelp(eHelpType::PR_DIG_TUTORIAL_03);
|
|
|
|
// Live dropped the ability states as soon as the owner had the pet dig
|
|
m_Ability = ePetAbilityType::Invalid;
|
|
SetStatus(m_Status & ~(PET_STATE_AT_ABILITY_OBJECT | PET_STATE_USING_ABILITY | PET_STATE_GOING_TO_ABILITY_OBJECT));
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
|
|
Command(NiPoint3Constant::ZERO, LWOOBJID_EMPTY, 1, 202, true);
|
|
|
|
m_TreasureTime = 2;
|
|
} else if (ability == ePetAbilityType::JumpOnObject) {
|
|
// The pet jumps on the switch, which turns its bouncer on; the pet is done with the switch then
|
|
m_Ability = ePetAbilityType::Invalid;
|
|
m_AbilityObject = LWOOBJID_EMPTY;
|
|
SetStatus(m_Status & ~(PET_STATE_AT_ABILITY_OBJECT | PET_STATE_USING_ABILITY | PET_STATE_GOING_TO_ABILITY_OBJECT));
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
|
|
RenderComponent::PlayAnimation(m_Parent, u"jump");
|
|
SendHelp(eHelpType::PR_TOOLTIP_1ST_PET_JUMPED_ON_SWITCH);
|
|
|
|
auto* const switchComponent = object->GetComponent<SwitchComponent>();
|
|
if (switchComponent) switchComponent->OnUse(m_Parent);
|
|
}
|
|
}
|
|
|
|
void PetComponent::HideAbilityButton() {
|
|
if (!m_AbilityButtonShown) return;
|
|
m_AbilityButtonShown = false;
|
|
|
|
auto* const owner = GetOwner();
|
|
if (!owner) return;
|
|
|
|
GameMessages::ShowPetActionButton msg;
|
|
msg.target = m_Owner;
|
|
msg.ButtonLabel = m_Ability;
|
|
msg.bShow = false;
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
|
|
void PetComponent::SendHelp(const eHelpType helpId) const {
|
|
// Sent every time, as live did; the client shows each tooltip only once
|
|
auto* const owner = GetOwner();
|
|
if (!owner) return;
|
|
|
|
GameMessages::Help msg;
|
|
msg.target = m_Owner;
|
|
msg.helpId = helpId;
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
|
|
int32_t PetComponent::GetAbilityImaginationCost(const ePetAbilityType ability) {
|
|
auto query = CDClientDatabase::CreatePreppedStmt("SELECT ImaginationCost FROM PetAbilities WHERE id = ?;");
|
|
query.bind(1, static_cast<int32_t>(ability));
|
|
|
|
auto result = query.execQuery();
|
|
if (result.eof() || result.fieldIsNull("ImaginationCost")) return 0;
|
|
|
|
return result.getIntField("ImaginationCost");
|
|
}
|
|
|
|
LWOOBJID PetComponent::GetOwnerId() const {
|
|
return m_Owner;
|
|
}
|
|
|
|
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) {
|
|
const auto& pair = currentActivities.find(tamer);
|
|
|
|
if (pair == currentActivities.end()) {
|
|
return nullptr;
|
|
}
|
|
|
|
auto* entity = Game::entityManager->GetEntity(pair->second);
|
|
|
|
if (entity == nullptr) {
|
|
currentActivities.erase(tamer);
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
return entity->GetComponent<PetComponent>();
|
|
}
|
|
|
|
PetComponent* PetComponent::GetActivePet(LWOOBJID owner) {
|
|
const auto& pair = activePets.find(owner);
|
|
|
|
if (pair == activePets.end()) {
|
|
return nullptr;
|
|
}
|
|
|
|
auto* entity = Game::entityManager->GetEntity(pair->second);
|
|
|
|
if (entity == nullptr) {
|
|
activePets.erase(owner);
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
return entity->GetComponent<PetComponent>();
|
|
}
|
|
|
|
Entity* PetComponent::GetParentEntity() const {
|
|
return m_Parent;
|
|
}
|
|
|
|
PetComponent::~PetComponent() {
|
|
m_Owner = LWOOBJID_EMPTY;
|
|
}
|
|
|
|
void PetComponent::SetPetNameForModeration(const std::string& petName, const LWOOBJID owner) {
|
|
int approved = 1; //default, in mod
|
|
|
|
//Make sure that the name isn't already auto-approved:
|
|
if (Game::chatFilter->IsSentenceOkay(petName, eGameMasterLevel::CIVILIAN).empty()) {
|
|
approved = 2; //approved
|
|
}
|
|
|
|
//Save to db:
|
|
Database::Get()->SetPetNameModerationStatus(m_DatabaseId, IPetNames::Info{ petName, approved, owner, m_Parent->GetLOT() });
|
|
DashboardNotify::Changed("pet_names", m_DatabaseId);
|
|
}
|
|
|
|
void PetComponent::LoadPetNameFromModeration() {
|
|
auto petNameInfo = Database::Get()->GetPetNameInfo(m_DatabaseId);
|
|
if (petNameInfo) {
|
|
m_ModerationStatus = petNameInfo->approvalStatus;
|
|
if (m_ModerationStatus == 2) {
|
|
m_Name = petNameInfo->petName;
|
|
}
|
|
}
|
|
}
|
|
|
|
void PetComponent::SetPreconditions(const std::string& preconditions) {
|
|
m_Preconditions = std::make_optional<PreconditionExpression>(preconditions);
|
|
}
|
|
|
|
void PetComponent::ApplyNameModeration(const bool approved) {
|
|
auto* owner = GetOwner();
|
|
if (approved) {
|
|
LoadPetNameFromModeration();
|
|
} else {
|
|
// Rejected names are removed; the pet goes back to having no name until a new one is chosen
|
|
m_ModerationStatus = 0;
|
|
m_Name = "";
|
|
}
|
|
if (owner) {
|
|
if (auto* inventoryComponent = owner->GetComponent<InventoryComponent>()) {
|
|
auto data = inventoryComponent->GetDatabasePet(m_DatabaseId);
|
|
data.name = m_Name;
|
|
data.moderationState = m_ModerationStatus;
|
|
inventoryComponent->SetDatabasePet(m_DatabaseId, data);
|
|
}
|
|
{
|
|
GameMessages::SetPetName msg;
|
|
msg.target = m_Owner;
|
|
msg.name = GeneralUtils::UTF8ToUTF16(m_Name);
|
|
msg.petDBID = m_DatabaseId;
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
{
|
|
GameMessages::SetPetNameModerated msg;
|
|
msg.target = m_Owner;
|
|
msg.PetDBID = m_DatabaseId;
|
|
msg.nModerationStatus = m_ModerationStatus;
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
}
|
|
{
|
|
GameMessages::PetNameChanged msg;
|
|
msg.target = m_Parent->GetObjectID();
|
|
msg.moderationStatus = m_ModerationStatus;
|
|
msg.name = GeneralUtils::UTF8ToUTF16(m_Name);
|
|
msg.ownerName = GeneralUtils::UTF8ToUTF16(m_OwnerName);
|
|
msg.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
}
|