/* * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ #include "Conversation.h" #include "ConditionMgr.h" #include "ConversationAI.h" #include "ConversationDataStore.h" #include "Creature.h" #include "CreatureAISelector.h" #include "DB2Stores.h" #include "IteratorPair.h" #include "Log.h" #include "Map.h" #include "MapUtils.h" #include "ObjectAccessor.h" #include "PhasingHandler.h" #include "Player.h" #include "UpdateData.h" #include "WorldSession.h" Conversation::Conversation() : WorldObject(false), _duration(0), _textureKitId(0) { m_objectType |= TYPEMASK_CONVERSATION; m_objectTypeId = TYPEID_CONVERSATION; m_updateFlag.Stationary = true; m_updateFlag.Conversation = true; m_entityFragments.Add(WowCS::EntityFragment::Tag_Conversation, false); _lastLineEndTimes.fill(Milliseconds::zero()); } Conversation::~Conversation() = default; void Conversation::AddToWorld() { ///- Register the Conversation for guid lookup and for caster if (!IsInWorld()) { GetMap()->GetObjectsStore().Insert(this); WorldObject::AddToWorld(); } } void Conversation::RemoveFromWorld() { ///- Remove the Conversation from the accessor and from all lists of objects in world if (IsInWorld()) { _ai->OnRemove(); WorldObject::RemoveFromWorld(); GetMap()->GetObjectsStore().Remove(this); } } void Conversation::Update(uint32 diff) { _ai->OnUpdate(diff); if (GetDuration() > Milliseconds(diff)) { _duration -= Milliseconds(diff); DoWithSuppressingObjectUpdates([&]() { // Only sent in CreateObject ApplyModUpdateFieldValue(m_values.ModifyValue(&Conversation::m_conversationData).ModifyValue(&UF::ConversationData::Progress), int32(diff), true); const_cast(*m_conversationData).ClearChanged(&UF::ConversationData::Progress); }); } else { Remove(); // expired return; } WorldObject::Update(diff); } void Conversation::Remove() { if (IsInWorld()) { AddObjectToRemoveList(); // calls RemoveFromWorld } } Conversation* Conversation::CreateConversation(uint32 conversationEntry, Unit* creator, Position const& pos, ObjectGuid privateObjectOwner, SpellInfo const* spellInfo /*= nullptr*/, bool autoStart /*= true*/) { ConversationTemplate const* conversationTemplate = sConversationDataStore->GetConversationTemplate(conversationEntry); if (!conversationTemplate) return nullptr; ObjectGuid::LowType lowGuid = creator->GetMap()->GenerateLowGuid(); Conversation* conversation = new Conversation(); conversation->Create(lowGuid, conversationEntry, creator->GetMap(), creator, pos, privateObjectOwner, spellInfo); if (autoStart && !conversation->Start()) { delete conversation; return nullptr; } return conversation; } struct ConversationActorFillVisitor { explicit ConversationActorFillVisitor(Conversation* conversation, Unit const* creator, Map const* map, ConversationActorTemplate const& actor) : _conversation(conversation), _creator(creator), _map(map), _actor(actor) { } void operator()(ConversationActorWorldObjectTemplate const& worldObject) const { Creature const* bestFit = nullptr; for (auto const& [_, creature] : Trinity::Containers::MapEqualRange(_map->GetCreatureBySpawnIdStore(), worldObject.SpawnId)) { bestFit = creature; // If creature is in a personal phase then we pick that one specifically if (creature->GetPhaseShift().GetPersonalGuid() == _creator->GetGUID()) break; } if (bestFit) _conversation->AddActor(_actor.Id, _actor.Index, bestFit->GetGUID()); } void operator()(ConversationActorNoObjectTemplate const& noObject) const { _conversation->AddActor(_actor.Id, _actor.Index, ConversationActorType::WorldObject, noObject.CreatureId, noObject.CreatureDisplayInfoId); } void operator()([[maybe_unused]] ConversationActorActivePlayerTemplate const& activePlayer) const { _conversation->AddActor(_actor.Id, _actor.Index, ObjectGuid::Create(0xFFFFFFFFFFFFFFFF)); } void operator()(ConversationActorTalkingHeadTemplate const& talkingHead) const { _conversation->AddActor(_actor.Id, _actor.Index, ConversationActorType::TalkingHead, talkingHead.CreatureId, talkingHead.CreatureDisplayInfoId); } private: Conversation* _conversation; Unit const* _creator; ::Map const* _map; ConversationActorTemplate const& _actor; }; void Conversation::Create(ObjectGuid::LowType lowGuid, uint32 conversationEntry, Map* map, Unit* creator, Position const& pos, ObjectGuid privateObjectOwner, SpellInfo const* /*spellInfo = nullptr*/) { ConversationTemplate const* conversationTemplate = sConversationDataStore->GetConversationTemplate(conversationEntry); ASSERT(conversationTemplate); _creatorGuid = creator->GetGUID(); SetPrivateObjectOwner(privateObjectOwner); SetMap(map); Relocate(pos); RelocateStationaryPosition(pos); Object::_Create(ObjectGuid::Create(GetMapId(), conversationEntry, lowGuid)); PhasingHandler::InheritPhaseShift(this, creator); UpdatePositionData(); SetZoneScript(); SetEntry(conversationEntry); SetObjectScale(1.0f); AI_Initialize(); _textureKitId = conversationTemplate->TextureKitId; for (ConversationActorTemplate const& actor : conversationTemplate->Actors) std::visit(ConversationActorFillVisitor(this, creator, map, actor), actor.Data); std::vector lines; for (ConversationLineTemplate const* line : conversationTemplate->Lines) { if (!sConditionMgr->IsObjectMeetingNotGroupedConditions(CONDITION_SOURCE_TYPE_CONVERSATION_LINE, line->Id, creator)) continue; ConversationLineEntry const* convoLine = sConversationLineStore.LookupEntry(line->Id); // never null for conversationTemplate->Lines UF::ConversationLine& lineField = lines.emplace_back(); lineField.ConversationLineID = line->Id; lineField.BroadcastTextID = convoLine->BroadcastTextID; lineField.UiCameraID = line->UiCameraID; lineField.ActorIndex = line->ActorIdx; lineField.Flags = line->Flags; lineField.ChatType = line->ChatType; std::array& startTimes = _lineStartTimes[line->Id]; for (LocaleConstant locale = LOCALE_enUS; locale < TOTAL_LOCALES; locale = LocaleConstant(locale + 1)) { if (locale == LOCALE_none) continue; startTimes[locale] = _lastLineEndTimes[locale]; if (locale == DEFAULT_LOCALE) lineField.StartTime = _lastLineEndTimes[locale].count(); if (int32 const* broadcastTextDuration = sDB2Manager.GetBroadcastTextDuration(convoLine->BroadcastTextID, locale)) _lastLineEndTimes[locale] += Milliseconds(*broadcastTextDuration); _lastLineEndTimes[locale] += Milliseconds(convoLine->AdditionalDuration); } } _duration = *std::ranges::max_element(_lastLineEndTimes); SetUpdateFieldValue(m_values.ModifyValue(&Conversation::m_conversationData).ModifyValue(&UF::ConversationData::LastLineEndTime), _duration.count()); SetUpdateFieldValue(m_values.ModifyValue(&Conversation::m_conversationData).ModifyValue(&UF::ConversationData::Lines), std::move(lines)); // conversations are despawned 5-20s after LastLineEndTime _duration += 10s; _ai->OnCreate(creator); } bool Conversation::Start() { ConversationTemplate const* conversationTemplate = sConversationDataStore->GetConversationTemplate(GetEntry()); // never null, already checked in ::Create / ::CreateConversation if (!conversationTemplate->Flags.HasFlag(ConversationFlags::AllowWithoutSpawnedActor)) { for (UF::ConversationLine const& line : *m_conversationData->Lines) { UF::ConversationActor const* actor = line.ActorIndex < m_conversationData->Actors.size() ? &m_conversationData->Actors[line.ActorIndex] : nullptr; if (!actor || (!actor->CreatureID && actor->ActorGUID.IsEmpty() && !actor->NoActorObject)) { TC_LOG_ERROR("entities.conversation", "Failed to create conversation (Id: {}) due to missing actor (Idx: {}).", GetEntry(), line.ActorIndex); return false; } } } if (IsInWorld()) { TC_LOG_ERROR("entities.conversation", "Attempted to start conversation (Id: {}) multiple times.", GetEntry()); return true; // returning true to not cause delete in Conversation::CreateConversation if convo is already started in ConversationScript::OnConversationCreate } if (!GetMap()->AddToMap(this)) return false; _ai->OnStart(); return true; } void Conversation::AddActor(int32 actorId, uint32 actorIdx, ObjectGuid const& actorGuid) { auto actorField = m_values.ModifyValue(&Conversation::m_conversationData).ModifyValue(&UF::ConversationData::Actors, actorIdx); SetUpdateFieldValue(actorField.ModifyValue(&UF::ConversationActor::CreatureID), 0); SetUpdateFieldValue(actorField.ModifyValue(&UF::ConversationActor::CreatureDisplayInfoID), 0); SetUpdateFieldValue(actorField.ModifyValue(&UF::ConversationActor::ActorGUID), actorGuid); SetUpdateFieldValue(actorField.ModifyValue(&UF::ConversationActor::Id), actorId); SetUpdateFieldValue(actorField.ModifyValue(&UF::ConversationActor::Type), AsUnderlyingType(ConversationActorType::WorldObject)); SetUpdateFieldValue(actorField.ModifyValue(&UF::ConversationActor::NoActorObject), 0); } void Conversation::AddActor(int32 actorId, uint32 actorIdx, ConversationActorType type, uint32 creatureId, uint32 creatureDisplayInfoId) { auto actorField = m_values.ModifyValue(&Conversation::m_conversationData).ModifyValue(&UF::ConversationData::Actors, actorIdx); SetUpdateFieldValue(actorField.ModifyValue(&UF::ConversationActor::CreatureID), creatureId); SetUpdateFieldValue(actorField.ModifyValue(&UF::ConversationActor::CreatureDisplayInfoID), creatureDisplayInfoId); SetUpdateFieldValue(actorField.ModifyValue(&UF::ConversationActor::ActorGUID), ObjectGuid::Empty); SetUpdateFieldValue(actorField.ModifyValue(&UF::ConversationActor::Id), actorId); SetUpdateFieldValue(actorField.ModifyValue(&UF::ConversationActor::Type), AsUnderlyingType(type)); SetUpdateFieldValue(actorField.ModifyValue(&UF::ConversationActor::NoActorObject), type == ConversationActorType::WorldObject ? 1 : 0); } Milliseconds const* Conversation::GetLineStartTime(LocaleConstant locale, int32 lineId) const { if (std::array const* durations = Trinity::Containers::MapGetValuePtr(_lineStartTimes, lineId)) return &(*durations)[locale]; return nullptr; } Milliseconds Conversation::GetLastLineEndTime(LocaleConstant locale) const { return _lastLineEndTimes[locale]; } int32 Conversation::GetLineDuration(LocaleConstant locale, int32 lineId) { ConversationLineEntry const* convoLine = sConversationLineStore.LookupEntry(lineId); if (!convoLine) { TC_LOG_ERROR("entities.conversation", "Conversation::GetLineDuration: Tried to get duration for invalid ConversationLine id {}.", lineId); return 0; } int32 const* textDuration = sDB2Manager.GetBroadcastTextDuration(convoLine->BroadcastTextID, locale); if (!textDuration) return 0; return *textDuration + convoLine->AdditionalDuration; } Milliseconds Conversation::GetLineEndTime(LocaleConstant locale, int32 lineId) const { Milliseconds const* lineStartTime = GetLineStartTime(locale, lineId); if (!lineStartTime) { TC_LOG_ERROR("entities.conversation", "Conversation::GetLineEndTime: Unable to get line start time for locale {}, lineid {} (Conversation ID: {}).", locale, lineId, GetEntry()); return Milliseconds(0); } return *lineStartTime + Milliseconds(GetLineDuration(locale, lineId)); } LocaleConstant Conversation::GetPrivateObjectOwnerLocale() const { LocaleConstant privateOwnerLocale = LOCALE_enUS; if (Player* owner = ObjectAccessor::GetPlayer(*this, GetPrivateObjectOwner())) privateOwnerLocale = owner->GetSession()->GetSessionDbLocaleIndex(); return privateOwnerLocale; } Unit* Conversation::GetActorUnit(uint32 actorIdx) const { if (m_conversationData->Actors.size() <= actorIdx) { TC_LOG_ERROR("entities.conversation", "Conversation::GetActorUnit: Tried to access invalid actor idx {} (Conversation ID: {}).", actorIdx, GetEntry()); return nullptr; } return ObjectAccessor::GetUnit(*this, m_conversationData->Actors[actorIdx].ActorGUID); } Creature* Conversation::GetActorCreature(uint32 actorIdx) const { Unit* actor = GetActorUnit(actorIdx); if (!actor) return nullptr; return actor->ToCreature(); } void Conversation::AI_Initialize() { AI_Destroy(); _ai.reset(FactorySelector::SelectConversationAI(this)); _ai->OnInitialize(); } void Conversation::AI_Destroy() { _ai.reset(); } uint32 Conversation::GetScriptId() const { return sConversationDataStore->GetConversationTemplate(GetEntry())->ScriptId; } void Conversation::BuildValuesCreate(ByteBuffer* data, UF::UpdateFieldFlag flags, Player const* target) const { m_objectData->WriteCreate(*data, flags, this, target); m_conversationData->WriteCreate(*data, flags, this, target); } void Conversation::BuildValuesUpdate(ByteBuffer* data, UF::UpdateFieldFlag flags, Player const* target) const { *data << uint32(m_values.GetChangedObjectTypeMask()); if (m_values.HasChanged(TYPEID_OBJECT)) m_objectData->WriteUpdate(*data, flags, this, target); if (m_values.HasChanged(TYPEID_CONVERSATION)) m_conversationData->WriteUpdate(*data, flags, this, target); } void Conversation::BuildValuesUpdateForPlayerWithMask(UpdateData* data, UF::ObjectData::Mask const& requestedObjectMask, UF::ConversationData::Mask const& requestedConversationMask, Player const* target) const { UF::UpdateFieldFlag flags = GetUpdateFieldFlagsFor(target); UpdateMask valuesMask; if (requestedObjectMask.IsAnySet()) valuesMask.Set(TYPEID_OBJECT); if (requestedConversationMask.IsAnySet()) valuesMask.Set(TYPEID_CONVERSATION); ByteBuffer& buffer = PrepareValuesUpdateBuffer(data); std::size_t sizePos = buffer.wpos(); buffer << uint32(0); BuildEntityFragmentsForValuesUpdateForPlayerWithMask(&buffer, flags); buffer << uint32(valuesMask.GetBlock(0)); if (valuesMask[TYPEID_OBJECT]) m_objectData->WriteUpdate(buffer, requestedObjectMask, true, this, target); if (valuesMask[TYPEID_CONVERSATION]) m_conversationData->WriteUpdate(buffer, requestedConversationMask, true, this, target); buffer.put(sizePos, buffer.wpos() - sizePos - 4); data->AddUpdateBlock(); } void Conversation::ValuesUpdateForPlayerWithMaskSender::operator()(Player const* player) const { UpdateData udata(Owner->GetMapId()); WorldPacket packet; Owner->BuildValuesUpdateForPlayerWithMask(&udata, ObjectMask.GetChangesMask(), ConversationMask.GetChangesMask(), player); udata.BuildPacket(&packet); player->SendDirectMessage(&packet); } void Conversation::ClearUpdateMask(bool remove) { m_values.ClearChangesMask(&Conversation::m_conversationData); Object::ClearUpdateMask(remove); }