Core/Conversations: Dropped time fields and calculate them using db2 data (#27151)

This commit is contained in:
ModoX
2021-10-26 22:49:12 +02:00
committed by GitHub
parent d57e798d50
commit 94b14e4f63
17 changed files with 195 additions and 22 deletions

View File

@@ -0,0 +1,15 @@
--
-- Table structure for table `broadcast_text_duration`
--
DROP TABLE IF EXISTS `broadcast_text_duration`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `broadcast_text_duration` (
`ID` int(10) unsigned NOT NULL DEFAULT '0',
`BroadcastTextID` int(11) NOT NULL DEFAULT '0',
`Locale` int(11) NOT NULL DEFAULT '0',
`Duration` int(11) NOT NULL DEFAULT '0',
`VerifiedBuild` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`ID`,`VerifiedBuild`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;

View File

@@ -0,0 +1,3 @@
--
ALTER TABLE `conversation_line_template` DROP `StartTime`;
ALTER TABLE `conversation_template` DROP `LastLineEndTime`;

View File

@@ -41,3 +41,19 @@ LocaleConstant GetLocaleByName(std::string const& name)
return TOTAL_LOCALES;
}
CascLocaleBit WowLocaleToCascLocaleBit[TOTAL_LOCALES] =
{
CascLocaleBit::enUS,
CascLocaleBit::koKR,
CascLocaleBit::frFR,
CascLocaleBit::deDE,
CascLocaleBit::zhCN,
CascLocaleBit::zhTW,
CascLocaleBit::esES,
CascLocaleBit::esMX,
CascLocaleBit::ruRU,
CascLocaleBit::None,
CascLocaleBit::ptBR,
CascLocaleBit::itIT
};

View File

@@ -98,10 +98,33 @@ enum LocaleConstant : uint8
const uint8 OLD_TOTAL_LOCALES = 9; /// @todo convert in simple system
#define DEFAULT_LOCALE LOCALE_enUS
enum class CascLocaleBit : uint8
{
None = 0,
enUS = 1,
koKR = 2,
Reserved = 3,
frFR = 4,
deDE = 5,
zhCN = 6,
esES = 7,
zhTW = 8,
enGB = 9,
enCN = 10,
enTW = 11,
esMX = 12,
ruRU = 13,
ptBR = 14,
itIT = 15,
ptPT = 16
};
TC_COMMON_API extern char const* localeNames[TOTAL_LOCALES];
TC_COMMON_API LocaleConstant GetLocaleByName(std::string const& name);
TC_COMMON_API extern CascLocaleBit WowLocaleToCascLocaleBit[TOTAL_LOCALES];
constexpr inline bool IsValidLocale(LocaleConstant locale)
{
return locale < TOTAL_LOCALES && locale != LOCALE_none;

View File

@@ -276,6 +276,11 @@ void HotfixDatabaseConnection::DoPrepareStatements()
PREPARE_LOCALE_STMT(HOTFIX_SEL_BROADCAST_TEXT, "SELECT ID, Text_lang, Text1_lang FROM broadcast_text_locale WHERE (`VerifiedBuild` > 0) = ?"
" AND locale = ?", CONNECTION_SYNCH);
// BroadcastTextDuration.db2
PrepareStatement(HOTFIX_SEL_BROADCAST_TEXT_DURATION, "SELECT ID, BroadcastTextID, Locale, Duration FROM broadcast_text_duration"
" WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);
PREPARE_MAX_ID_STMT(HOTFIX_SEL_BROADCAST_TEXT_DURATION, "SELECT MAX(ID) + 1 FROM broadcast_text_duration", CONNECTION_SYNCH);
// CfgRegions.db2
PrepareStatement(HOTFIX_SEL_CFG_REGIONS, "SELECT ID, Tag, RegionID, Raidorigin, RegionGroupMask, ChallengeOrigin FROM cfg_regions"
" WHERE (`VerifiedBuild` > 0) = ?", CONNECTION_SYNCH);

View File

@@ -175,6 +175,9 @@ enum HotfixDatabaseStatements : uint32
HOTFIX_SEL_BROADCAST_TEXT_MAX_ID,
HOTFIX_SEL_BROADCAST_TEXT_LOCALE,
HOTFIX_SEL_BROADCAST_TEXT_DURATION,
HOTFIX_SEL_BROADCAST_TEXT_DURATION_MAX_ID,
HOTFIX_SEL_CFG_REGIONS,
HOTFIX_SEL_CFG_REGIONS_MAX_ID,

View File

@@ -890,6 +890,22 @@ struct BroadcastTextLoadInfo
}
};
struct BroadcastTextDurationLoadInfo
{
static DB2LoadInfo const* Instance()
{
static DB2FieldMeta const fields[] =
{
{ false, FT_INT, "ID" },
{ true, FT_INT, "BroadcastTextID" },
{ true, FT_INT, "Locale" },
{ true, FT_INT, "Duration" },
};
static DB2LoadInfo const loadInfo(&fields[0], std::extent<decltype(fields)>::value, BroadcastTextDurationMeta::Instance(), HOTFIX_SEL_BROADCAST_TEXT_DURATION);
return &loadInfo;
}
};
struct CfgRegionsLoadInfo
{
static DB2LoadInfo const* Instance()

View File

@@ -84,6 +84,7 @@ DB2Storage<BattlePetSpeciesEntry> sBattlePetSpeciesStore("BattlePe
DB2Storage<BattlePetSpeciesStateEntry> sBattlePetSpeciesStateStore("BattlePetSpeciesState.db2", BattlePetSpeciesStateLoadInfo::Instance());
DB2Storage<BattlemasterListEntry> sBattlemasterListStore("BattlemasterList.db2", BattlemasterListLoadInfo::Instance());
DB2Storage<BroadcastTextEntry> sBroadcastTextStore("BroadcastText.db2", BroadcastTextLoadInfo::Instance());
DB2Storage<BroadcastTextDurationEntry> sBroadcastTextDurationStore("BroadcastTextDuration.db2", BroadcastTextDurationLoadInfo::Instance());
DB2Storage<Cfg_RegionsEntry> sCfgRegionsStore("Cfg_Regions.db2", CfgRegionsLoadInfo::Instance());
DB2Storage<CharTitlesEntry> sCharTitlesStore("CharTitles.db2", CharTitlesLoadInfo::Instance());
DB2Storage<CharacterLoadoutEntry> sCharacterLoadoutStore("CharacterLoadout.db2", CharacterLoadoutLoadInfo::Instance());
@@ -407,6 +408,7 @@ namespace
std::unordered_map<uint32 /*azeritePowerSetId*/, std::vector<AzeritePowerSetMemberEntry const*>> _azeritePowers;
std::unordered_map<std::pair<uint32 /*azeriteUnlockSetId*/, ItemContext>, std::array<uint8, MAX_AZERITE_EMPOWERED_TIER>> _azeriteTierUnlockLevels;
std::unordered_map<std::pair<uint32 /*itemId*/, ItemContext>, AzeriteUnlockMappingEntry const*> _azeriteUnlockMappings;
std::unordered_map<std::pair<int32 /*broadcastTextId*/, CascLocaleBit /*cascLocaleBit*/>, int32> _broadcastTextDurations;
std::array<ChrClassUIDisplayEntry const*, MAX_CLASSES> _uiDisplayByClass;
std::array<std::array<uint32, MAX_POWERS>, MAX_CLASSES> _powersByClass;
std::unordered_map<uint32 /*chrCustomizationOptionId*/, std::vector<ChrCustomizationChoiceEntry const*>> _chrCustomizationChoicesByOption;
@@ -635,6 +637,7 @@ uint32 DB2Manager::LoadStores(std::string const& dataPath, LocaleConstant defaul
LOAD_DB2(sBattlePetSpeciesStateStore);
LOAD_DB2(sBattlemasterListStore);
LOAD_DB2(sBroadcastTextStore);
LOAD_DB2(sBroadcastTextDurationStore);
LOAD_DB2(sCfgRegionsStore);
LOAD_DB2(sCharTitlesStore);
LOAD_DB2(sCharacterLoadoutStore);
@@ -967,6 +970,9 @@ uint32 DB2Manager::LoadStores(std::string const& dataPath, LocaleConstant defaul
ASSERT(BATTLE_PET_SPECIES_MAX_ID >= sBattlePetSpeciesStore.GetNumRows(),
"BATTLE_PET_SPECIES_MAX_ID (%d) must be equal to or greater than %u", BATTLE_PET_SPECIES_MAX_ID, sBattlePetSpeciesStore.GetNumRows());
for (BroadcastTextDurationEntry const* broadcastTextDuration : sBroadcastTextDurationStore)
_broadcastTextDurations[{ broadcastTextDuration->BroadcastTextID, CascLocaleBit(broadcastTextDuration->Locale) }] = broadcastTextDuration->Duration;
for (ChrClassUIDisplayEntry const* uiDisplay : sChrClassUIDisplayStore)
{
ASSERT(uiDisplay->ChrClassesID < MAX_CLASSES);
@@ -1844,6 +1850,11 @@ char const* DB2Manager::GetBroadcastTextValue(BroadcastTextEntry const* broadcas
return broadcastText->Text[DEFAULT_LOCALE];
}
int32 const* DB2Manager::GetBroadcastTextDuration(int32 broadcastTextId, LocaleConstant locale /*= DEFAULT_LOCALE*/) const
{
return Trinity::Containers::MapGetValuePtr(_broadcastTextDurations, { broadcastTextId, WowLocaleToCascLocaleBit[locale] });
}
ChrClassUIDisplayEntry const* DB2Manager::GetUiDisplayForClass(Classes unitClass) const
{
ASSERT(unitClass < MAX_CLASSES);

View File

@@ -353,6 +353,7 @@ public:
std::vector<AzeritePowerSetMemberEntry const*> const* GetAzeritePowers(uint32 itemId) const;
uint32 GetRequiredAzeriteLevelForAzeritePowerTier(uint32 azeriteUnlockSetId, ItemContext context, uint32 tier) const;
static char const* GetBroadcastTextValue(BroadcastTextEntry const* broadcastText, LocaleConstant locale = DEFAULT_LOCALE, uint8 gender = GENDER_MALE, bool forceGender = false);
int32 const* GetBroadcastTextDuration(int32 broadcastTextId, LocaleConstant locale = DEFAULT_LOCALE) const;
ChrClassUIDisplayEntry const* GetUiDisplayForClass(Classes unitClass) const;
static char const* GetClassName(uint8 class_, LocaleConstant locale = DEFAULT_LOCALE);
uint32 GetPowerIndexByClass(Powers power, uint32 classId) const;

View File

@@ -532,6 +532,14 @@ struct BroadcastTextEntry
uint16 EmoteDelay[MAX_BROADCAST_TEXT_EMOTES];
};
struct BroadcastTextDurationEntry
{
uint32 ID;
int32 BroadcastTextID;
int32 Locale;
int32 Duration;
};
struct Cfg_RegionsEntry
{
uint32 ID;

View File

@@ -17,8 +17,10 @@
#include "Conversation.h"
#include "ConditionMgr.h"
#include "Containers.h"
#include "ConversationDataStore.h"
#include "Creature.h"
#include "DB2Stores.h"
#include "IteratorPair.h"
#include "Log.h"
#include "Map.h"
@@ -34,6 +36,8 @@ Conversation::Conversation() : WorldObject(false), _duration(0), _textureKitId(0
m_updateFlag.Stationary = true;
m_updateFlag.Conversation = true;
_lastLineEndTimes.fill(Milliseconds::zero());
}
Conversation::~Conversation() = default;
@@ -60,9 +64,9 @@ void Conversation::RemoveFromWorld()
void Conversation::Update(uint32 diff)
{
if (GetDuration() > int32(diff))
if (GetDuration() > Milliseconds(diff))
{
_duration -= diff;
_duration -= Milliseconds(diff);
DoWithSuppressingObjectUpdates([&]()
{
// Only sent in CreateObject
@@ -123,8 +127,6 @@ bool Conversation::Create(ObjectGuid::LowType lowGuid, uint32 conversationEntry,
SetEntry(conversationEntry);
SetObjectScale(1.0f);
SetUpdateFieldValue(m_values.ModifyValue(&Conversation::m_conversationData).ModifyValue(&UF::ConversationData::LastLineEndTime), conversationTemplate->LastLineEndTime);
_duration = conversationTemplate->LastLineEndTime + 10 * IN_MILLISECONDS;
_textureKitId = conversationTemplate->TextureKitId;
for (ConversationActor const& actor : conversationTemplate->Actors)
@@ -158,16 +160,38 @@ bool Conversation::Create(ObjectGuid::LowType lowGuid, uint32 conversationEntry,
actorIndices.insert(line->ActorIdx);
lines.emplace_back();
UF::ConversationLine& lineField = lines.back();
lineField.ConversationLineID = line->Id;
lineField.StartTime = line->StartTime;
lineField.UiCameraID = line->UiCameraID;
lineField.ActorIndex = line->ActorIdx;
lineField.Flags = line->Flags;
ConversationLineEntry const* convoLine = sConversationLineStore.LookupEntry(line->Id); // never null for conversationTemplate->Lines
for (LocaleConstant locale = LOCALE_enUS; locale < TOTAL_LOCALES; locale = LocaleConstant(locale + 1))
{
if (locale == LOCALE_none)
continue;
_lineStartTimes[{ locale, line->Id }] = _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 = Milliseconds(*std::max_element(_lastLineEndTimes.begin(), _lastLineEndTimes.end()));
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;
sScriptMgr->OnConversationCreate(this, creator);
// All actors need to be set
@@ -194,6 +218,16 @@ void Conversation::AddActor(ObjectGuid const& actorGuid, uint16 actorIdx)
SetUpdateFieldValue(actorField.ModifyValue(&UF::ConversationActor::Type), AsUnderlyingType(ActorType::WorldObjectActor));
}
Milliseconds const* Conversation::GetLineStartTime(LocaleConstant locale, int32 lineId) const
{
return Trinity::Containers::MapGetValuePtr(_lineStartTimes, { locale, lineId });
}
Milliseconds Conversation::GetLastLineEndTime(LocaleConstant locale) const
{
return _lastLineEndTimes[locale];
}
uint32 Conversation::GetScriptId() const
{
return sConversationDataStore->GetConversationTemplate(GetEntry())->ScriptId;

View File

@@ -19,6 +19,7 @@
#define TRINITYCORE_CONVERSATION_H
#include "Object.h"
#include "Hash.h"
class Unit;
class SpellInfo;
@@ -43,7 +44,7 @@ class TC_GAME_API Conversation : public WorldObject, public GridObject<Conversat
void Update(uint32 diff) override;
void Remove();
int32 GetDuration() const { return _duration; }
Milliseconds GetDuration() const { return _duration; }
uint32 GetTextureKitId() const { return _textureKitId; }
static Conversation* CreateConversation(uint32 conversationEntry, Unit* creator, Position const& pos, ObjectGuid privateObjectOwner, SpellInfo const* spellInfo = nullptr);
@@ -60,6 +61,9 @@ class TC_GAME_API Conversation : public WorldObject, public GridObject<Conversat
float GetStationaryO() const override { return _stationaryPosition.GetOrientation(); }
void RelocateStationaryPosition(Position const& pos) { _stationaryPosition.Relocate(pos); }
Milliseconds const* GetLineStartTime(LocaleConstant locale, int32 lineId) const;
Milliseconds GetLastLineEndTime(LocaleConstant locale) const;
uint32 GetScriptId() const;
UF::UpdateField<UF::ConversationData, 0, TYPEID_CONVERSATION> m_conversationData;
@@ -73,8 +77,11 @@ class TC_GAME_API Conversation : public WorldObject, public GridObject<Conversat
private:
Position _stationaryPosition;
ObjectGuid _creatorGuid;
uint32 _duration;
Milliseconds _duration;
uint32 _textureKitId;
std::unordered_map<std::pair<LocaleConstant /*locale*/, int32 /*lineId*/>, Milliseconds /*startTime*/> _lineStartTimes;
std::array<Milliseconds /*endTime*/, TOTAL_LOCALES> _lastLineEndTimes;
};
#endif // TRINITYCORE_CONVERSATION_H

View File

@@ -4836,7 +4836,7 @@ void SceneObjectData::ClearChangesMask()
void ConversationLine::WriteCreate(ByteBuffer& data, Conversation const* owner, Player const* receiver) const
{
data << int32(ConversationLineID);
data << uint32(StartTime);
data << uint32(ViewerDependentValue<StartTimeTag>::GetValue(this, owner, receiver));
data << int32(UiCameraID);
data << uint8(ActorIndex);
data << uint8(Flags);
@@ -4846,7 +4846,7 @@ void ConversationLine::WriteCreate(ByteBuffer& data, Conversation const* owner,
void ConversationLine::WriteUpdate(ByteBuffer& data, bool ignoreChangesMask, Conversation const* owner, Player const* receiver) const
{
data << int32(ConversationLineID);
data << uint32(StartTime);
data << uint32(ViewerDependentValue<StartTimeTag>::GetValue(this, owner, receiver));
data << int32(UiCameraID);
data << uint8(ActorIndex);
data << uint8(Flags);
@@ -4898,7 +4898,7 @@ bool ConversationActor::operator==(ConversationActor const& right) const
void ConversationData::WriteCreate(ByteBuffer& data, EnumFlag<UpdateFieldFlag> fieldVisibilityFlags, Conversation const* owner, Player const* receiver) const
{
data << uint32(Lines->size());
data << int32(LastLineEndTime);
data << int32(ViewerDependentValue<LastLineEndTimeTag>::GetValue(this, owner, receiver));
data << uint32(Progress);
for (std::size_t i = 0; i < Lines->size(); ++i)
{
@@ -4963,7 +4963,7 @@ void ConversationData::WriteUpdate(ByteBuffer& data, Mask const& changesMask, bo
}
if (changesMask[4])
{
data << int32(LastLineEndTime);
data << int32(ViewerDependentValue<LastLineEndTimeTag>::GetValue(this, owner, receiver));
}
if (changesMask[5])
{

View File

@@ -906,6 +906,7 @@ struct ConversationLine : public IsUpdateFieldStructureTag
{
int32 ConversationLineID;
uint32 StartTime;
struct StartTimeTag : ViewerDependentValueTag<uint32> {};
int32 UiCameraID;
uint8 ActorIndex;
uint8 Flags;
@@ -938,6 +939,7 @@ struct ConversationData : public IsUpdateFieldStructureTag, public HasChangesMas
UpdateField<std::vector<UF::ConversationLine>, 0, 2> Lines;
DynamicUpdateField<UF::ConversationActor, 0, 3> Actors;
UpdateField<int32, 0, 4> LastLineEndTime;
struct LastLineEndTimeTag : ViewerDependentValueTag<int32> {};
UpdateField<uint32, 0, 5> Progress;
void WriteCreate(ByteBuffer& data, EnumFlag<UpdateFieldFlag> fieldVisibilityFlags, Conversation const* owner, Player const* receiver) const;

View File

@@ -18,6 +18,7 @@
#ifndef ViewerDependentValues_h__
#define ViewerDependentValues_h__
#include "Conversation.h"
#include "Creature.h"
#include "GameObject.h"
#include "Map.h"
@@ -26,6 +27,7 @@
#include "SpellInfo.h"
#include "SpellMgr.h"
#include "World.h"
#include "WorldSession.h"
namespace UF
{
@@ -279,6 +281,37 @@ public:
return state;
}
};
template<>
class ViewerDependentValue<UF::ConversationData::LastLineEndTimeTag>
{
public:
using value_type = UF::ConversationData::LastLineEndTimeTag::value_type;
static value_type GetValue(UF::ConversationData const* /*conversationData*/, Conversation const* conversation, Player const* receiver)
{
LocaleConstant locale = receiver->GetSession()->GetSessionDbLocaleIndex();
return conversation->GetLastLineEndTime(locale).count();
}
};
template<>
class ViewerDependentValue<UF::ConversationLine::StartTimeTag>
{
public:
using value_type = UF::ConversationLine::StartTimeTag::value_type;
static value_type GetValue(UF::ConversationLine const* conversationLineData, Conversation const* conversation, Player const* receiver)
{
value_type startTime = conversationLineData->StartTime;
LocaleConstant locale = receiver->GetSession()->GetSessionDbLocaleIndex();
if (Milliseconds const* localizedStartTime = conversation->GetLineStartTime(locale, conversationLineData->ConversationLineID))
startTime = localizedStartTime->count();
return startTime;
}
};
}
#endif // ViewerDependentValues_h__

View File

@@ -37,7 +37,7 @@ void ConversationDataStore::LoadConversationTemplates()
std::unordered_map<uint32, std::vector<ConversationActor>> actorsByConversation;
std::unordered_map<uint32, std::vector<ObjectGuid::LowType>> actorGuidsByConversation;
if (QueryResult lineTemplates = WorldDatabase.Query("SELECT Id, StartTime, UiCameraID, ActorIdx, Flags FROM conversation_line_template"))
if (QueryResult lineTemplates = WorldDatabase.Query("SELECT Id, UiCameraID, ActorIdx, Flags FROM conversation_line_template"))
{
uint32 oldMSTime = getMSTime();
@@ -55,10 +55,9 @@ void ConversationDataStore::LoadConversationTemplates()
ConversationLineTemplate& conversationLine = _conversationLineTemplateStore[id];
conversationLine.Id = id;
conversationLine.StartTime = fields[1].GetUInt32();
conversationLine.UiCameraID = fields[2].GetUInt32();
conversationLine.ActorIdx = fields[3].GetUInt8();
conversationLine.Flags = fields[4].GetUInt8();
conversationLine.UiCameraID = fields[1].GetUInt32();
conversationLine.ActorIdx = fields[2].GetUInt8();
conversationLine.Flags = fields[3].GetUInt8();
}
while (lineTemplates->NextRow());
@@ -132,7 +131,7 @@ void ConversationDataStore::LoadConversationTemplates()
TC_LOG_INFO("server.loading", ">> Loaded 0 Conversation actors. DB table `conversation_actors` is empty.");
}
if (QueryResult templates = WorldDatabase.Query("SELECT Id, FirstLineId, LastLineEndTime, TextureKitId, ScriptName FROM conversation_template"))
if (QueryResult templates = WorldDatabase.Query("SELECT Id, FirstLineId, TextureKitId, ScriptName FROM conversation_template"))
{
uint32 oldMSTime = getMSTime();
@@ -143,9 +142,8 @@ void ConversationDataStore::LoadConversationTemplates()
ConversationTemplate conversationTemplate;
conversationTemplate.Id = fields[0].GetUInt32();
conversationTemplate.FirstLineId = fields[1].GetUInt32();
conversationTemplate.LastLineEndTime = fields[2].GetUInt32();
conversationTemplate.TextureKitId = fields[3].GetUInt32();
conversationTemplate.ScriptId = sObjectMgr->GetScriptId(fields[4].GetString());
conversationTemplate.TextureKitId = fields[2].GetUInt32();
conversationTemplate.ScriptId = sObjectMgr->GetScriptId(fields[3].GetString());
conversationTemplate.Actors = std::move(actorsByConversation[conversationTemplate.Id]);
conversationTemplate.ActorGuids = std::move(actorGuidsByConversation[conversationTemplate.Id]);

View File

@@ -39,7 +39,6 @@ struct ConversationActor
struct ConversationLineTemplate
{
uint32 Id; // Link to ConversationLine.db2
uint32 StartTime; // Time in ms after conversation creation the line is displayed
uint32 UiCameraID; // Link to UiCamera.db2
uint8 ActorIdx; // Index from conversation_actors
uint8 Flags;
@@ -51,7 +50,6 @@ struct ConversationTemplate
{
uint32 Id;
uint32 FirstLineId; // Link to ConversationLine.db2
uint32 LastLineEndTime; // Time in ms after conversation creation the last line fades out
uint32 TextureKitId; // Background texture
std::vector<ConversationActor> Actors;