diff --git a/Source/Framework/Constants/QuestConst.cs b/Source/Framework/Constants/QuestConst.cs index d7600355a..e775fb3ba 100644 --- a/Source/Framework/Constants/QuestConst.cs +++ b/Source/Framework/Constants/QuestConst.cs @@ -621,4 +621,12 @@ namespace Framework.Constants FirstCompletionBonus = 0x1, RepeatCompletionBonus = 0x2 } + + public enum SpawnTrackingState + { + None = 0, + Active = 1, + Complete = 2, + Max + } } diff --git a/Source/Framework/Database/Databases/CharacterDatabase.cs b/Source/Framework/Database/Databases/CharacterDatabase.cs index 928e3f1f3..e17f92b90 100644 --- a/Source/Framework/Database/Databases/CharacterDatabase.cs +++ b/Source/Framework/Database/Databases/CharacterDatabase.cs @@ -92,6 +92,7 @@ namespace Framework.Database PrepareStatement(CharStatements.SEL_CHARACTER_QUESTSTATUS_OBJECTIVES, "SELECT quest, objective, data FROM character_queststatus_objectives WHERE guid = ?"); PrepareStatement(CharStatements.SEL_CHARACTER_QUESTSTATUS_OBJECTIVES_CRITERIA, "SELECT questObjectiveId FROM character_queststatus_objectives_criteria WHERE guid = ?"); PrepareStatement(CharStatements.SEL_CHARACTER_QUESTSTATUS_OBJECTIVES_CRITERIA_PROGRESS, "SELECT criteriaId, counter, date FROM character_queststatus_objectives_criteria_progress WHERE guid = ?"); + PrepareStatement(CharStatements.SEL_CHARACTER_QUESTSTATUS_OBJECTIVES_SPAWN_TRACKING, "SELECT quest, objective, spawnTrackingId FROM character_queststatus_objectives_spawn_tracking WHERE guid = ?"); PrepareStatement(CharStatements.SEL_CHARACTER_QUESTSTATUS_DAILY, "SELECT quest, time FROM character_queststatus_daily WHERE guid = ?"); PrepareStatement(CharStatements.SEL_CHARACTER_QUESTSTATUS_WEEKLY, "SELECT quest FROM character_queststatus_weekly WHERE guid = ?"); @@ -566,6 +567,7 @@ namespace Framework.Database PrepareStatement(CharStatements.DEL_CHAR_QUESTSTATUS_OBJECTIVES_CRITERIA, "DELETE FROM character_queststatus_objectives_criteria WHERE guid = ?"); PrepareStatement(CharStatements.DEL_CHAR_QUESTSTATUS_OBJECTIVES_CRITERIA_PROGRESS, "DELETE FROM character_queststatus_objectives_criteria_progress WHERE guid = ?"); PrepareStatement(CharStatements.DEL_CHAR_QUESTSTATUS_OBJECTIVES_CRITERIA_PROGRESS_BY_CRITERIA, "DELETE FROM character_queststatus_objectives_criteria_progress WHERE guid = ? AND criteriaId = ?"); + PrepareStatement(CharStatements.DEL_CHAR_QUESTSTATUS_OBJECTIVES_SPAWN_TRACKING, "DELETE FROM character_queststatus_objectives_spawn_tracking WHERE guid = ?"); PrepareStatement(CharStatements.DEL_CHAR_SOCIAL_BY_GUID, "DELETE FROM character_social WHERE guid = ?"); PrepareStatement(CharStatements.DEL_CHAR_SOCIAL_BY_FRIEND, "DELETE FROM character_social WHERE friend = ?"); PrepareStatement(CharStatements.DEL_CHAR_ACHIEVEMENT_BY_ACHIEVEMENT, "DELETE FROM character_achievement WHERE achievement = ? AND guid = ?"); @@ -616,6 +618,8 @@ namespace Framework.Database PrepareStatement(CharStatements.DEL_CHAR_QUESTSTATUS_OBJECTIVES_BY_QUEST, "DELETE FROM character_queststatus_objectives WHERE guid = ? AND quest = ?"); PrepareStatement(CharStatements.INS_CHAR_QUESTSTATUS_OBJECTIVES_CRITERIA, "INSERT INTO character_queststatus_objectives_criteria (guid, questObjectiveId) VALUES (?, ?)"); PrepareStatement(CharStatements.INS_CHAR_QUESTSTATUS_OBJECTIVES_CRITERIA_PROGRESS, "INSERT INTO character_queststatus_objectives_criteria_progress (guid, criteriaId, counter, date) VALUES (?, ?, ?, ?)"); + PrepareStatement(CharStatements.REP_CHAR_QUESTSTATUS_OBJECTIVES_SPAWN_TRACKING, "REPLACE INTO character_queststatus_objectives_spawn_tracking (guid, quest, objective, spawnTrackingId) VALUES (?, ?, ?, ?)"); + PrepareStatement(CharStatements.DEL_CHAR_QUESTSTATUS_OBJECTIVES_SPAWN_TRACKING_BY_QUEST, "DELETE FROM character_queststatus_objectives_spawn_tracking WHERE guid = ? AND quest = ?"); PrepareStatement(CharStatements.INS_CHAR_QUESTSTATUS_REWARDED, "INSERT IGNORE INTO character_queststatus_rewarded (guid, quest, active) VALUES (?, ?, 1)"); PrepareStatement(CharStatements.DEL_CHAR_QUESTSTATUS_REWARDED_BY_QUEST, "DELETE FROM character_queststatus_rewarded WHERE guid = ? AND quest = ?"); PrepareStatement(CharStatements.UPD_CHAR_QUESTSTATUS_REWARDED_FACTION_CHANGE, "UPDATE character_queststatus_rewarded SET quest = ? WHERE quest = ? AND guid = ?"); @@ -813,6 +817,7 @@ namespace Framework.Database SEL_CHARACTER_QUESTSTATUS_OBJECTIVES, SEL_CHARACTER_QUESTSTATUS_OBJECTIVES_CRITERIA, SEL_CHARACTER_QUESTSTATUS_OBJECTIVES_CRITERIA_PROGRESS, + SEL_CHARACTER_QUESTSTATUS_OBJECTIVES_SPAWN_TRACKING, SEL_CHARACTER_QUESTSTATUS_DAILY, SEL_CHARACTER_QUESTSTATUS_WEEKLY, SEL_CHARACTER_QUESTSTATUS_MONTHLY, @@ -1204,6 +1209,7 @@ namespace Framework.Database DEL_CHAR_QUESTSTATUS_OBJECTIVES_CRITERIA, DEL_CHAR_QUESTSTATUS_OBJECTIVES_CRITERIA_PROGRESS, DEL_CHAR_QUESTSTATUS_OBJECTIVES_CRITERIA_PROGRESS_BY_CRITERIA, + DEL_CHAR_QUESTSTATUS_OBJECTIVES_SPAWN_TRACKING, DEL_CHAR_SOCIAL_BY_GUID, DEL_CHAR_SOCIAL_BY_FRIEND, DEL_CHAR_ACHIEVEMENT_BY_ACHIEVEMENT, @@ -1254,6 +1260,8 @@ namespace Framework.Database DEL_CHAR_QUESTSTATUS_OBJECTIVES_BY_QUEST, INS_CHAR_QUESTSTATUS_OBJECTIVES_CRITERIA, INS_CHAR_QUESTSTATUS_OBJECTIVES_CRITERIA_PROGRESS, + REP_CHAR_QUESTSTATUS_OBJECTIVES_SPAWN_TRACKING, + DEL_CHAR_QUESTSTATUS_OBJECTIVES_SPAWN_TRACKING_BY_QUEST, INS_CHAR_QUESTSTATUS_REWARDED, DEL_CHAR_QUESTSTATUS_REWARDED_BY_QUEST, UPD_CHAR_QUESTSTATUS_REWARDED_FACTION_CHANGE, diff --git a/Source/Game/Entities/Creature/Creature.cs b/Source/Game/Entities/Creature/Creature.cs index 922ddf2b1..b0cd6ccee 100644 --- a/Source/Game/Entities/Creature/Creature.cs +++ b/Source/Game/Entities/Creature/Creature.cs @@ -338,7 +338,7 @@ namespace Game.Entities ReplaceAllDynamicFlags(UnitDynFlags.None); - SetUpdateFieldValue(m_values.ModifyValue(m_unitData).ModifyValue(m_unitData.StateAnimID), Global.DB2Mgr.GetEmptyAnimStateID()); + SetUpdateFieldValue(m_values.ModifyValue(m_unitData).ModifyValue(m_unitData.StateWorldEffectsQuestObjectiveID), data != null ? data.spawnTrackingQuestObjectiveId : 0); SetCanDualWield(cInfo.FlagsExtra.HasAnyFlag(CreatureFlagsExtra.UseOffhandAttack)); @@ -3025,6 +3025,24 @@ namespace Game.Entities public string GetStringId(StringIdType type) { return m_stringIds[(int)type]; } + public override SpawnTrackingStateData GetSpawnTrackingStateDataForPlayer(Player player) + { + if (player == null) + return null; + + SpawnMetadata data = Global.ObjectMgr.GetSpawnMetadata(SpawnObjectType.Creature, GetSpawnId()); + if (data != null) + { + if (data.spawnTrackingQuestObjectiveId != 0 && data.spawnTrackingData != null) + { + SpawnTrackingState state = player.GetSpawnTrackingStateByObjective(data.spawnTrackingData.SpawnTrackingId, data.spawnTrackingQuestObjectiveId); + return data.spawnTrackingStates[(int)state]; + } + } + + return null; + } + public VendorItemData GetVendorItems() { return Global.ObjectMgr.GetNpcVendorItemList(GetEntry()); diff --git a/Source/Game/Entities/GameObject/GameObject.cs b/Source/Game/Entities/GameObject/GameObject.cs index df5a2ad28..13a19404d 100644 --- a/Source/Game/Entities/GameObject/GameObject.cs +++ b/Source/Game/Entities/GameObject/GameObject.cs @@ -283,8 +283,6 @@ namespace Game.Entities SetGoState(goState); SetGoArtKit(artKit); - SetUpdateFieldValue(m_values.ModifyValue(m_gameObjectData).ModifyValue(m_gameObjectData.SpawnTrackingStateAnimID), Global.DB2Mgr.GetEmptyAnimStateID()); - switch (goInfo.type) { case GameObjectTypes.FishingHole: @@ -1128,6 +1126,8 @@ namespace Game.Entities PhasingHandler.InitDbPhaseShift(GetPhaseShift(), data.PhaseUseFlags, data.PhaseId, data.PhaseGroup); PhasingHandler.InitDbVisibleMapId(GetPhaseShift(), data.terrainSwapMap); + SetUpdateFieldValue(m_values.ModifyValue(m_gameObjectData).ModifyValue(m_gameObjectData.StateWorldEffectsQuestObjectiveID), data != null ? data.spawnTrackingQuestObjectiveId : 0); + if (data.spawntimesecs >= 0) { m_spawnedByDefault = true; @@ -2730,6 +2730,24 @@ namespace Game.Entities public string GetStringId(StringIdType type) { return m_stringIds[(int)type]; } + public override SpawnTrackingStateData GetSpawnTrackingStateDataForPlayer(Player player) + { + if (player == null) + return null; + + SpawnMetadata data = Global.ObjectMgr.GetSpawnMetadata(SpawnObjectType.GameObject, GetSpawnId()); + if (data != null) + { + if (data.spawnTrackingQuestObjectiveId != 0 && data.spawnTrackingData != null) + { + SpawnTrackingState state = player.GetSpawnTrackingStateByObjective(data.spawnTrackingData.SpawnTrackingId, data.spawnTrackingQuestObjectiveId); + return data.spawnTrackingStates[(int)state]; + } + } + + return null; + } + public override string GetName(Locale locale = Locale.enUS) { if (locale != Locale.enUS) diff --git a/Source/Game/Entities/Object/Update/UpdateFields.cs b/Source/Game/Entities/Object/Update/UpdateFields.cs index c3d5e276d..3ecdb8899 100644 --- a/Source/Game/Entities/Object/Update/UpdateFields.cs +++ b/Source/Game/Entities/Object/Update/UpdateFields.cs @@ -160,6 +160,11 @@ namespace Game.Entities if (!gameObject.MeetsInteractCondition(receiver)) dynFlags |= GameObjectDynamicLowFlags.NoInterract; + + var data = Global.ObjectMgr.GetSpawnMetadata(SpawnObjectType.GameObject, gameObject.GetSpawnId()); + if (data != null && data.spawnTrackingQuestObjectiveId != 0 && data.spawnTrackingData != null) + if (receiver.GetSpawnTrackingStateByObjective(data.spawnTrackingData.SpawnTrackingId, data.spawnTrackingQuestObjectiveId) != SpawnTrackingState.Active) + dynFlags &= ~GameObjectDynamicLowFlags.Activate; } unitDynFlags = (uint)dynFlags; @@ -1278,14 +1283,15 @@ namespace Game.Entities data.WriteUInt32(GetViewerDependentNpcFlags(this, owner, receiver)); data.WriteUInt32(GetViewerDependentNpcFlags2(this, owner, receiver)); - data.WriteUInt32(StateSpellVisualID); - data.WriteUInt32(StateAnimID); - data.WriteUInt32(StateAnimKitID); - data.WriteInt32(StateWorldEffectIDs.GetValue().Count); + data.WriteUInt32(GetViewerDependentStateSpellVisualID(this, owner, receiver)); + data.WriteUInt32(GetViewerDependentStateAnimID(this, owner, receiver)); + data.WriteUInt32(GetViewerDependentStateAnimKitID(this, owner, receiver)); + var stateWorldEffects = GetViewerDependentStateWorldEffectIDs(this, owner, receiver); + data.WriteInt32(stateWorldEffects.Count); data.WriteUInt32(StateWorldEffectsQuestObjectiveID); data.WriteInt32(SpellOverrideNameID); - for (int i = 0; i < StateWorldEffectIDs.GetValue().Count; ++i) - data.WriteUInt32(StateWorldEffectIDs.GetValue()[i]); + for (int i = 0; i < stateWorldEffects.Count; ++i) + data.WriteUInt32(stateWorldEffects[i]); data.WritePackedGuid(Charm); data.WritePackedGuid(Summon); @@ -1514,11 +1520,11 @@ namespace Game.Entities } if (changesMask[2]) { - var list = StateWorldEffectIDs.GetValue(); - data.WriteBits(list.Count, 32); - for (int i = 0; i < list.Count; ++i) + var stateWorldEffects = GetViewerDependentStateWorldEffectIDs(this, owner, receiver); + data.WriteBits(stateWorldEffects.Count, 32); + for (int i = 0; i < stateWorldEffects.Count; ++i) { - data.WriteUInt32(list[i]); + data.WriteUInt32(stateWorldEffects[i]); } } } @@ -1594,15 +1600,15 @@ namespace Game.Entities } if (changesMask[9]) { - data.WriteUInt32(StateSpellVisualID); + data.WriteUInt32(GetViewerDependentStateSpellVisualID(this, owner, receiver)); } if (changesMask[10]) { - data.WriteUInt32(StateAnimID); + data.WriteUInt32(GetViewerDependentStateAnimID(this, owner, receiver)); } if (changesMask[11]) { - data.WriteUInt32(StateAnimKitID); + data.WriteUInt32(GetViewerDependentStateAnimKitID(this, owner, receiver)); } if (changesMask[12]) { @@ -2477,6 +2483,63 @@ namespace Game.Entities } return interactSpellId; } + List GetViewerDependentStateWorldEffectIDs(UnitData unitData, Unit unit, Player receiver) + { + List stateWorldEffects = unitData.StateWorldEffectIDs; + + if (unit.IsCreature()) + { + var spawnTrackingStateData = unit.GetSpawnTrackingStateDataForPlayer(receiver); + if (spawnTrackingStateData != null) + stateWorldEffects = spawnTrackingStateData.StateWorldEffects; + } + + return stateWorldEffects; + } + + + uint GetViewerDependentStateSpellVisualID(UnitData unitData, Unit unit, Player receiver) + { + uint stateSpellVisual = unitData.StateSpellVisualID; + + if (unit.IsCreature()) + { + var spawnTrackingStateData = unit.GetSpawnTrackingStateDataForPlayer(receiver); + if (spawnTrackingStateData != null) + stateSpellVisual = spawnTrackingStateData.StateSpellVisualId.GetValueOrDefault(0); + } + + return stateSpellVisual; + } + + + uint GetViewerDependentStateAnimID(UnitData unitData, Unit unit, Player receiver) + { + uint stateAnimId = Global.DB2Mgr.GetEmptyAnimStateID(); + + if (unit.IsCreature()) + { + var spawnTrackingStateData = unit.GetSpawnTrackingStateDataForPlayer(receiver); + if (spawnTrackingStateData != null) + stateAnimId = spawnTrackingStateData.StateAnimId.GetValueOrDefault((ushort)stateAnimId); + } + + return stateAnimId; + } + + uint GetViewerDependentStateAnimKitID(UnitData unitData, Unit unit, Player receiver) + { + uint stateAnimKitId = unitData.StateAnimKitID; + + if (unit.IsCreature()) + { + var spawnTrackingStateData = unit.GetSpawnTrackingStateDataForPlayer(receiver); + if (spawnTrackingStateData != null) + stateAnimKitId = spawnTrackingStateData.StateAnimKitId.GetValueOrDefault(0); + } + + return stateAnimKitId; + } } public class ChrCustomizationChoice : IComparable @@ -7505,25 +7568,26 @@ namespace Game.Entities { data.WriteUInt32(DisplayID); data.WriteUInt32(SpellVisualID); - data.WriteUInt32(StateSpellVisualID); - data.WriteUInt32(SpawnTrackingStateAnimID); - data.WriteUInt32(SpawnTrackingStateAnimKitID); - data.WriteInt32(((List)StateWorldEffectIDs).Count); + data.WriteUInt32(GetViewerDependentStateSpellVisualID(this, owner, receiver)); + data.WriteUInt32(GetViewerDependentStateAnimID(this, owner, receiver)); + data.WriteUInt32(GetViewerDependentStateAnimKitID(this, owner, receiver)); + var stateWorldEffects = GetViewerDependentStateWorldEffectIDs(this, owner, receiver); + data.WriteInt32(stateWorldEffects.Count); data.WriteUInt32(StateWorldEffectsQuestObjectiveID); - for (int i = 0; i < ((List)StateWorldEffectIDs).Count; ++i) + for (int i = 0; i < stateWorldEffects.Count; ++i) { - data.WriteUInt32(((List)StateWorldEffectIDs)[i]); + data.WriteUInt32(stateWorldEffects[i]); } data.WritePackedGuid(CreatedBy); data.WritePackedGuid(GuildGUID); - data.WriteUInt32(GetViewerGameObjectFlags(this, owner, receiver)); + data.WriteUInt32(GetViewerDependentGameObjectFlags(this, owner, receiver)); Quaternion rotation = ParentRotation; data.WriteFloat(rotation.X); data.WriteFloat(rotation.Y); data.WriteFloat(rotation.Z); data.WriteFloat(rotation.W); data.WriteUInt32(FactionTemplate); - data.WriteInt8(GetViewerGameObjectState(this, owner, receiver)); + data.WriteInt8(GetViewerDependentGameObjectState(this, owner, receiver)); data.WriteInt8(TypeID); data.WriteUInt8(PercentHealth); data.WriteUInt32(ArtKit); @@ -7558,10 +7622,11 @@ namespace Game.Entities { if (changesMask[1]) { - data.WriteBits(((List)StateWorldEffectIDs).Count, 32); - for (int i = 0; i < ((List)StateWorldEffectIDs).Count; ++i) + var stateWorldEffects = GetViewerDependentStateWorldEffectIDs(this, owner, receiver); + data.WriteBits(stateWorldEffects.Count, 32); + for (int i = 0; i < stateWorldEffects.Count; ++i) { - data.WriteUInt32(((List)StateWorldEffectIDs)[i]); + data.WriteUInt32(stateWorldEffects[i]); } } } @@ -7616,15 +7681,15 @@ namespace Game.Entities } if (changesMask[6]) { - data.WriteUInt32(StateSpellVisualID); + data.WriteUInt32(GetViewerDependentStateSpellVisualID(this, owner, receiver)); } if (changesMask[7]) { - data.WriteUInt32(SpawnTrackingStateAnimID); + data.WriteUInt32(GetViewerDependentStateAnimID(this, owner, receiver)); } if (changesMask[8]) { - data.WriteUInt32(SpawnTrackingStateAnimKitID); + data.WriteUInt32(GetViewerDependentStateAnimKitID(this, owner, receiver)); } if (changesMask[9]) { @@ -7640,7 +7705,7 @@ namespace Game.Entities } if (changesMask[12]) { - data.WriteUInt32(GetViewerGameObjectFlags(this, owner, receiver)); + data.WriteUInt32(GetViewerDependentGameObjectFlags(this, owner, receiver)); } if (changesMask[13]) { @@ -7655,7 +7720,7 @@ namespace Game.Entities } if (changesMask[15]) { - data.WriteInt8(GetViewerGameObjectState(this, owner, receiver)); + data.WriteInt8(GetViewerDependentGameObjectState(this, owner, receiver)); } if (changesMask[16]) { @@ -7725,7 +7790,7 @@ namespace Game.Entities _changesMask.ResetAll(); } - uint GetViewerGameObjectFlags(GameObjectFieldData gameObjectData, GameObject gameObject, Player receiver) + uint GetViewerDependentGameObjectFlags(GameObjectFieldData gameObjectData, GameObject gameObject, Player receiver) { uint flags = gameObjectData.Flags; if (gameObject.GetGoType() == GameObjectTypes.Chest) @@ -7735,10 +7800,55 @@ namespace Game.Entities return flags; } - sbyte GetViewerGameObjectState(GameObjectFieldData gameObjectData, GameObject gameObject, Player receiver) + sbyte GetViewerDependentGameObjectState(GameObjectFieldData gameObjectData, GameObject gameObject, Player receiver) { return (sbyte)gameObject.GetGoStateFor(receiver.GetGUID()); } + + List GetViewerDependentStateWorldEffectIDs(GameObjectFieldData gameObjectData, GameObject gameObject, Player receiver) + { + List stateWorldEffects = gameObjectData.StateWorldEffectIDs; + + var spawnTrackingStateData = gameObject.GetSpawnTrackingStateDataForPlayer(receiver); + if (spawnTrackingStateData != null) + stateWorldEffects = spawnTrackingStateData.StateWorldEffects; + + return stateWorldEffects; + } + + uint GetViewerDependentStateSpellVisualID(GameObjectFieldData gameObjectData, GameObject gameObject, Player receiver) + { + uint stateSpellVisual = gameObjectData.StateSpellVisualID; + + var spawnTrackingStateData = gameObject.GetSpawnTrackingStateDataForPlayer(receiver); + if (spawnTrackingStateData != null) + stateSpellVisual = spawnTrackingStateData.StateSpellVisualId.GetValueOrDefault(0); + + return stateSpellVisual; + } + + uint GetViewerDependentStateAnimID(GameObjectFieldData gameObjectData, GameObject gameObject, Player receiver) + { + uint stateAnimId = Global.DB2Mgr.GetEmptyAnimStateID(); + + var spawnTrackingStateData = gameObject.GetSpawnTrackingStateDataForPlayer(receiver); + if (spawnTrackingStateData != null) + stateAnimId = spawnTrackingStateData.StateAnimId.GetValueOrDefault((ushort)stateAnimId); + + return stateAnimId; + } + + uint GetViewerDependentStateAnimKitID(GameObjectFieldData gameObjectData, GameObject gameObject, Player receiver) + { + uint stateAnimKitId = gameObjectData.SpawnTrackingStateAnimKitID; + + var spawnTrackingStateData = gameObject.GetSpawnTrackingStateDataForPlayer(receiver); + if (spawnTrackingStateData != null) + stateAnimKitId = spawnTrackingStateData.StateAnimKitId.GetValueOrDefault(0); + + return stateAnimKitId; + } + } public class DynamicObjectData : HasChangesMask diff --git a/Source/Game/Entities/Object/WorldObject.cs b/Source/Game/Entities/Object/WorldObject.cs index 26a61a585..b1d11b146 100644 --- a/Source/Game/Entities/Object/WorldObject.cs +++ b/Source/Game/Entities/Object/WorldObject.cs @@ -867,6 +867,8 @@ namespace Game.Entities public virtual Loot GetLootForPlayer(Player player) { return null; } + public virtual SpawnTrackingStateData GetSpawnTrackingStateDataForPlayer(Player player) { return null; } + public virtual void BuildValuesCreate(WorldPacket data, UpdateFieldFlag flags, Player target) { } public virtual void BuildValuesUpdate(WorldPacket data, UpdateFieldFlag flags, Player target) { } @@ -1272,6 +1274,15 @@ namespace Game.Entities if (!obj.IsPrivateObject() && !Global.ConditionMgr.IsObjectMeetingVisibilityByObjectIdConditions((uint)obj.GetTypeId(), obj.GetEntry(), this)) return false; + // Spawn tracking + Player player = ToPlayer(); + if (player != null) + { + SpawnTrackingStateData spawnTrackingStateData = obj.GetSpawnTrackingStateDataForPlayer(player); + if (spawnTrackingStateData != null && !spawnTrackingStateData.Visible) + return false; + } + bool corpseVisibility = false; if (distanceCheck) { @@ -1304,7 +1315,6 @@ namespace Game.Entities } WorldObject viewpoint = this; - Player player = ToPlayer(); if (player != null) viewpoint = player.GetViewpoint(); diff --git a/Source/Game/Entities/Player/Player.DB.cs b/Source/Game/Entities/Player/Player.DB.cs index f6b3092a8..5c0ef8675 100644 --- a/Source/Game/Entities/Player/Player.DB.cs +++ b/Source/Game/Entities/Player/Player.DB.cs @@ -821,9 +821,11 @@ namespace Game.Entities uint questID = result.Read(0); Quest quest = Global.ObjectMgr.GetQuestTemplate(questID); + if (quest == null) + continue; var questStatusData = m_QuestStatus.LookupByKey(questID); - if (questStatusData != null && questStatusData.Slot < SharedConst.MaxQuestLogSize && quest != null) + if (questStatusData != null && questStatusData.Slot < SharedConst.MaxQuestLogSize) { byte storageIndex = result.Read(1); @@ -845,6 +847,42 @@ namespace Game.Entities while (result.NextRow()); } } + + void _LoadQuestStatusObjectiveSpawnTrackings(SQLResult result) + { + if (!result.IsEmpty()) + { + do + { + uint questID = result.Read(0); + Quest quest = Global.ObjectMgr.GetQuestTemplate(questID); + if (quest == null) + continue; + + var itr = m_QuestStatus.LookupByKey(questID); + if (itr != null && itr.Slot < SharedConst.MaxQuestLogSize) + { + QuestStatusData questStatusData = itr; + sbyte storageIndex = result.Read(1); + var objectiveItr = quest.Objectives.Find(objective => objective.StorageIndex == storageIndex); + if (objectiveItr != null) + { + uint spawnTrackingId = result.Read(2); + + if (Global.ObjectMgr.IsQuestObjectiveForSpawnTracking(spawnTrackingId, objectiveItr.Id)) + questStatusData.SpawnTrackingList.Add((storageIndex, spawnTrackingId)); + else + Log.outError(LogFilter.Player, $"Player::_LoadQuestStatusObjectiveSpawnTrackings: Player '{GetName()}' ({GetGUID()}) has objective {objectiveItr.Id} (quest {questID}) with unrelated spawn tracking {spawnTrackingId}."); + } + else + Log.outError(LogFilter.Player, $"Player::_LoadQuestStatusObjectiveSpawnTrackings: Player '{GetName()}' ({GetGUID()}) has quest {questID} out of range objective index {storageIndex}."); + } + else + Log.outError(LogFilter.Player, $"Player::_LoadQuestStatusObjectiveSpawnTrackings: Player {GetName()} ({GetGUID()}) does not have quest {questID} but has objective spawn trackings for it."); + } while (result.NextRow()); + } + } + void _LoadQuestStatusRewarded(SQLResult result) { if (!result.IsEmpty()) @@ -2189,6 +2227,22 @@ namespace Game.Entities stmt.AddValue(3, GetQuestSlotObjectiveData(data.Slot, obj)); trans.Append(stmt); } + + // Save spawn trackings + stmt = CharacterDatabase.GetPreparedStatement( CharStatements.DEL_CHAR_QUESTSTATUS_OBJECTIVES_SPAWN_TRACKING_BY_QUEST); + stmt.AddValue(0, GetGUID().GetCounter()); + stmt.AddValue(1, save.Key); + trans.Append(stmt); + + foreach (var (questObjectiveStorageIndex, spawnTrackingId) in data.SpawnTrackingList) + { + stmt = CharacterDatabase.GetPreparedStatement(CharStatements.REP_CHAR_QUESTSTATUS_OBJECTIVES_SPAWN_TRACKING); + stmt.AddValue(0, GetGUID().GetCounter()); + stmt.AddValue(1, save.Key); + stmt.AddValue(2, questObjectiveStorageIndex); + stmt.AddValue(3, spawnTrackingId); + trans.Append(stmt); + } } } else @@ -2203,6 +2257,11 @@ namespace Game.Entities stmt.AddValue(0, GetGUID().GetCounter()); stmt.AddValue(1, save.Key); trans.Append(stmt); + + stmt = CharacterDatabase.GetPreparedStatement(CharStatements.DEL_CHAR_QUESTSTATUS_OBJECTIVES_SPAWN_TRACKING_BY_QUEST); + stmt.AddValue(0, GetGUID().GetCounter()); + stmt.AddValue(1, save.Key); + trans.Append(stmt); } } @@ -3427,6 +3486,7 @@ namespace Game.Entities // after spell load, learn rewarded spell if need also _LoadQuestStatus(holder.GetResult(PlayerLoginQueryLoad.QuestStatus)); _LoadQuestStatusObjectives(holder.GetResult(PlayerLoginQueryLoad.QuestStatusObjectives)); + _LoadQuestStatusObjectiveSpawnTrackings(holder.GetResult(PlayerLoginQueryLoad.QuestStatusSpawnTracking)); _LoadQuestStatusRewarded(holder.GetResult(PlayerLoginQueryLoad.QuestStatusRew)); _LoadDailyQuestStatus(holder.GetResult(PlayerLoginQueryLoad.DailyQuestStatus)); _LoadWeeklyQuestStatus(holder.GetResult(PlayerLoginQueryLoad.WeeklyQuestStatus)); @@ -4407,6 +4467,10 @@ namespace Game.Entities stmt.AddValue(0, guid); trans.Append(stmt); + stmt = CharacterDatabase.GetPreparedStatement(CharStatements.DEL_CHAR_QUESTSTATUS_OBJECTIVES_SPAWN_TRACKING); + stmt.AddValue(0, guid); + trans.Append(stmt); + stmt = CharacterDatabase.GetPreparedStatement(CharStatements.DEL_CHAR_QUESTSTATUS_REWARDED); stmt.AddValue(0, guid); trans.Append(stmt); diff --git a/Source/Game/Entities/Player/Player.Quest.cs b/Source/Game/Entities/Player/Player.Quest.cs index 118d0661b..702533a2b 100644 --- a/Source/Game/Entities/Player/Player.Quest.cs +++ b/Source/Game/Entities/Player/Player.Quest.cs @@ -753,6 +753,8 @@ namespace Game.Entities QuestStatusData questStatusData = m_QuestStatus.LookupByKey(questId); QuestStatus oldStatus = questStatusData.Status; + SendForceSpawnTrackingUpdate(questId); + // check for repeatable quests status reset SetQuestSlot(logSlot, questId); questStatusData.Slot = logSlot; @@ -836,6 +838,8 @@ namespace Game.Entities { if (quest_id != 0) { + SendForceSpawnTrackingUpdate(quest_id); + SetQuestStatus(quest_id, QuestStatus.Complete); QuestStatusData questStatus = m_QuestStatus.LookupByKey(quest_id); @@ -2610,6 +2614,32 @@ namespace Game.Entities bool updatePhaseShift = false; bool updateZoneAuras = false; + bool anyObjectiveChangedSpawnTrackingState = false; + bool isObjectiveTypeForSpawnTracking = false; + + if (objectiveType == QuestObjectiveType.Monster || + objectiveType == QuestObjectiveType.GameObject || + objectiveType == QuestObjectiveType.TalkTo || + objectiveType == QuestObjectiveType.WinPetBattleAgainstNpc) + isObjectiveTypeForSpawnTracking = true; + + SpawnMetadata data = null; + if (isObjectiveTypeForSpawnTracking) + { + if (victimGuid.IsGameObject()) + { + GameObject gameObject = ObjectAccessor.GetGameObject(this, victimGuid); + if (gameObject != null) + data = Global.ObjectMgr.GetSpawnMetadata(SpawnObjectType.GameObject, gameObject.GetSpawnId()); + } + else if (victimGuid.IsCreatureOrVehicle()) + { + Creature creature = ObjectAccessor.GetCreatureOrPetOrVehicle(this, victimGuid); + if (creature != null) + data = Global.ObjectMgr.GetSpawnMetadata(SpawnObjectType.Creature, creature.GetSpawnId()); + } + } + foreach (var objectiveStatusData in m_questObjectiveStatus.LookupByKey((objectiveType, objectId))) { uint questId = objectiveStatusData.QuestStatusPair.QuestID; @@ -2719,6 +2749,41 @@ namespace Game.Entities } } + if (data != null && data.spawnTrackingQuestObjectiveId != 0 && data.spawnTrackingData != null) + { + if (objective.Id == data.spawnTrackingQuestObjectiveId) + { + // Store spawn tracking to return correct state in Player::GetSpawnTrackingStateByObjective + QuestStatusData questStatus = objectiveStatusData.QuestStatusPair.Status; + questStatus.SpawnTrackingList.Add((objective.StorageIndex, data.spawnTrackingData.SpawnTrackingId)); + + // Send QuestPOIUpdateResponse for every spawn linked to same SpawnTrackingId + foreach (var spawnMetaData in Global.ObjectMgr.GetSpawnMetadataForSpawnTracking(data.spawnTrackingData.SpawnTrackingId)) + { + SpawnData spawnData = spawnMetaData.ToSpawnData(); + if (spawnData == null) + continue; + + QuestPOIUpdateResponse response = new(); + + SpawnTrackingResponseInfo responseInfo = new(); + responseInfo.SpawnTrackingID = data.spawnTrackingData.SpawnTrackingId; + responseInfo.ObjectID = (int)spawnData.Id; + responseInfo.PhaseID = (int)spawnData.PhaseId; + responseInfo.PhaseGroupID = (int)spawnData.PhaseGroup; + responseInfo.PhaseUseFlags = (int)spawnData.PhaseUseFlags; + + SpawnTrackingState state = GetSpawnTrackingStateByObjective(data.spawnTrackingData.SpawnTrackingId, objective.Id); + responseInfo.Visible = data.spawnTrackingStates[(int)state].Visible; + + response.SpawnTrackingResponses.Add(responseInfo); + SendPacket(response); + } + + anyObjectiveChangedSpawnTrackingState = true; + } + } + if (objectiveWasComplete != objectiveIsNowComplete) anyObjectiveChangedCompletionState = true; @@ -2752,7 +2817,7 @@ namespace Game.Entities } - if (anyObjectiveChangedCompletionState) + if (anyObjectiveChangedCompletionState || anyObjectiveChangedSpawnTrackingState) UpdateVisibleObjectInteractions(true, false, false, true); if (updatePhaseShift) @@ -3138,6 +3203,8 @@ namespace Game.Entities moneyReward = (uint)(GetQuestMoneyReward(quest) + (int)(quest.GetRewMoneyMaxLevel() * WorldConfig.GetFloatValue(WorldCfg.RateDropMoney))); } + SendForceSpawnTrackingUpdate(questId); + if (quest.HasFlag(QuestFlags.TrackingEvent)) return; @@ -3348,6 +3415,51 @@ namespace Game.Entities return false; } + public void SendForceSpawnTrackingUpdate(uint questId) + { + if (questId != 0) + { + ForceSpawnTrackingUpdate data = new(); + data.QuestID = questId; + SendPacket(data); + } + } + + public QuestObjective GetActiveQuestObjectiveForForSpawnTracking(uint spawnTrackingId) + { + var questObjectiveList = Global.ObjectMgr.GetSpawnTrackingQuestObjectiveList(spawnTrackingId); + if (questObjectiveList != null) + foreach (QuestObjective questObjective in questObjectiveList) + if (FindQuestSlot(questObjective.QuestID) < SharedConst.MaxQuestLogSize) + return questObjective; + + return null; + } + + public SpawnTrackingState GetSpawnTrackingStateByObjective(uint spawnTrackingId, uint questObjectiveId) + { + if (spawnTrackingId != 0 && questObjectiveId != 0 && Global.ObjectMgr.IsQuestObjectiveForSpawnTracking(spawnTrackingId, questObjectiveId)) + { + QuestObjective questObjective = Global.ObjectMgr.GetQuestObjective(questObjectiveId); + if (questObjective != null) + { + if (IsQuestRewarded(questObjective.QuestID) || IsQuestObjectiveComplete(questObjective.QuestID, questObjective.Id)) + return SpawnTrackingState.Complete; + else if (GetQuestStatus(questObjective.QuestID) != QuestStatus.None) + { + var itr = m_QuestStatus.LookupByKey(questObjective.QuestID); + if (itr != null && itr.Slot < SharedConst.MaxQuestLogSize) + if (itr.SpawnTrackingList.Contains((questObjective.StorageIndex, spawnTrackingId))) + return SpawnTrackingState.Complete; + + return SpawnTrackingState.Active; + } + } + } + + return SpawnTrackingState.None; + } + public bool HasQuestForGO(int GOId) { foreach (var objectiveStatusData in m_questObjectiveStatus.LookupByKey((QuestObjectiveType.GameObject, GOId))) @@ -3389,7 +3501,16 @@ namespace Game.Entities ObjectFieldData objMask = new(); GameObjectFieldData goMask = new(); - if (m_questObjectiveStatus.ContainsKey((QuestObjectiveType.GameObject, (int)gameObject.GetEntry())) || gameObject.GetGoInfo().GetConditionID1() != 0) + SpawnMetadata data = Global.ObjectMgr.GetSpawnMetadata(SpawnObjectType.GameObject, gameObject.GetSpawnId()); + if (data != null && data.spawnTrackingQuestObjectiveId != 0 && data.spawnTrackingData != null) + { + objMask.MarkChanged(m_objectData.DynamicFlags); + goMask.MarkChanged(gameObject.m_gameObjectData.StateWorldEffectIDs); + goMask.MarkChanged(gameObject.m_gameObjectData.StateSpellVisualID); + goMask.MarkChanged(gameObject.m_gameObjectData.SpawnTrackingStateAnimID); + goMask.MarkChanged(gameObject.m_gameObjectData.SpawnTrackingStateAnimKitID); + } + else if (m_questObjectiveStatus.ContainsKey((QuestObjectiveType.GameObject, (int)gameObject.GetEntry())) || gameObject.GetGoInfo().GetConditionID1() != 0) objMask.MarkChanged(gameObject.m_objectData.DynamicFlags); else { @@ -3430,6 +3551,16 @@ namespace Game.Entities if (creature.m_unitData.NpcFlags2 != 0) unitMask.MarkChanged(m_unitData.NpcFlags2); + SpawnMetadata data = Global.ObjectMgr.GetSpawnMetadata(SpawnObjectType.Creature, creature.GetSpawnId()); + if (data != null && data.spawnTrackingQuestObjectiveId != 0 && data.spawnTrackingData != null) + { + objMask.MarkChanged(objMask.DynamicFlags); + unitMask.MarkChanged(unitMask.StateWorldEffectIDs); + unitMask.MarkChanged(unitMask.StateSpellVisualID); + unitMask.MarkChanged(unitMask.StateAnimID); + unitMask.MarkChanged(unitMask.StateAnimKitID); + } + if (objMask.GetUpdateMask().IsAnySet() || unitMask.GetUpdateMask().IsAnySet()) creature.BuildValuesUpdateForPlayerWithMask(udata, objMask.GetUpdateMask(), unitMask.GetUpdateMask(), this); diff --git a/Source/Game/Entities/Player/Player.cs b/Source/Game/Entities/Player/Player.cs index ab88bf0fa..e1e9141ae 100644 --- a/Source/Game/Entities/Player/Player.cs +++ b/Source/Game/Entities/Player/Player.cs @@ -5524,7 +5524,6 @@ namespace Game.Entities } SetDisplayId(model.DisplayID, true); - SetUpdateFieldValue(m_values.ModifyValue(m_unitData).ModifyValue(m_unitData.StateAnimID), DB2Mgr.GetEmptyAnimStateID()); } //Creature diff --git a/Source/Game/Entities/Transport.cs b/Source/Game/Entities/Transport.cs index a25bad4e6..396f0d5d3 100644 --- a/Source/Game/Entities/Transport.cs +++ b/Source/Game/Entities/Transport.cs @@ -172,7 +172,6 @@ namespace Game.Entities SetGoState(goinfo.MoTransport.allowstopping == 0 ? GameObjectState.Ready : GameObjectState.Active); SetGoType(GameObjectTypes.MapObjTransport); SetGoAnimProgress(255); - SetUpdateFieldValue(m_values.ModifyValue(m_gameObjectData).ModifyValue(m_gameObjectData.SpawnTrackingStateAnimID), Global.DB2Mgr.GetEmptyAnimStateID()); SetName(goinfo.name); SetLocalRotation(0.0f, 0.0f, 0.0f, 1.0f); SetParentRotation(Quaternion.Identity); diff --git a/Source/Game/Globals/ObjectManager.cs b/Source/Game/Globals/ObjectManager.cs index 58eef212f..20b488721 100644 --- a/Source/Game/Globals/ObjectManager.cs +++ b/Source/Game/Globals/ObjectManager.cs @@ -1514,7 +1514,7 @@ namespace Game foreach (Criteria criteria in criteriaList) if (criteria.Entry.Asset != 0) _eventStorage.Add(criteria.Entry.Asset); - }; + } CriteriaType[] eventCriteriaTypes = { CriteriaType.PlayerTriggerGameEvent, CriteriaType.AnyoneTriggerGameEventScenario }; foreach (CriteriaType criteriaType in eventCriteriaTypes) @@ -5804,6 +5804,26 @@ namespace Game void OnDeleteSpawnData(SpawnData data) { + if (data.spawnTrackingData != null) + { + SpawnTrackingTemplateData spawnTrackingData = GetSpawnTrackingData(data.spawnTrackingData.SpawnTrackingId); + Cypher.Assert(spawnTrackingData != null, $"Creature data for ({data.type},{data.SpawnId}) is being deleted and has invalid spawn tracking id {data.spawnTrackingData.SpawnTrackingId}!"); + + var pair = _spawnTrackingMapStorage.LookupByKey(data.spawnTrackingData.SpawnTrackingId); + bool erased = false; + foreach (var it in pair.ToList()) + { + if (it != data) + continue; + + _spawnTrackingMapStorage.Remove(data.spawnTrackingData.SpawnTrackingId, it); + erased = true; + } + + if (!erased) + Log.outFatal(LogFilter.Misc, $"Spawn data ({data.type},{data.SpawnId}) being removed is member of spawn tracking {data.spawnTrackingData.SpawnTrackingId}, but not actually listed in the lookup table for that spawn tracking!"); + } + var templateIt = _spawnGroupDataStorage.LookupByKey(data.spawnGroupData.groupId); Cypher.Assert(templateIt != null, $"Creature data for ({data.type},{data.SpawnId}) is being deleted and has invalid spawn group index {data.spawnGroupData.groupId}!"); @@ -9094,6 +9114,341 @@ namespace Game Log.outInfo(LogFilter.ServerLoading, "Loaded {0} points_of_interest locale strings in {1} ms", _pointOfInterestLocaleStorage.Count, Time.GetMSTimeDiffToNow(oldMSTime)); } + public void LoadSpawnTrackingTemplates() + { + uint oldMSTime = Time.GetMSTime(); + + // need for reload case + _spawnTrackingDataStorage.Clear(); + + // 0 1 2 3 4 + SQLResult result = DB.World.Query("SELECT SpawnTrackingId, MapId, PhaseId, PhaseGroup, PhaseUseFlags FROM spawn_tracking_template"); + + if (result.IsEmpty()) + { + Log.outInfo(LogFilter.ServerLoading, $"Loaded 0 spawn tracking templates. DB table `spawn_tracking_template` is empty!"); + return; + } + + do + { + uint spawnTrackingId = result.Read(0); + uint mapId = result.Read(1); + + if (!CliDB.MapStorage.HasRecord(mapId)) + { + Log.outError(LogFilter.Sql, $"Table `spawn_tracking_template` references non-existing map {mapId}, skipped"); + continue; + } + + SpawnTrackingTemplateData data = new(); + data.SpawnTrackingId = spawnTrackingId; + data.MapId = mapId; + data.PhaseId = result.Read(2); + data.PhaseGroup = result.Read(3); + data.PhaseUseFlags = result.Read(4); + + if (data.PhaseGroup != 0 && data.PhaseId != 0) + { + Log.outError(LogFilter.Sql, $"Table `spawn_tracking_template` has spawn tracking (Id: {data.SpawnTrackingId}) with `PhaseId` and `PhaseGroup` set, `PhaseGroup` set to 0"); + data.PhaseGroup = 0; + } + + if (data.PhaseId != 0) + { + if (!CliDB.PhaseStorage.HasRecord(data.PhaseId)) + { + Log.outError(LogFilter.Sql, $"Table `spawn_tracking_template` has spawn tracking (Id: {data.SpawnTrackingId}) referencing non-existing `PhaseId` {data.PhaseId}, set to 0"); + data.PhaseId = 0; + } + } + + if (data.PhaseGroup != 0) + { + if (Global.DB2Mgr.GetPhasesForGroup(data.PhaseGroup) == null) + { + Log.outError(LogFilter.Sql, $"Table `spawn_tracking_template` has spawn tracking (Id: {data.SpawnTrackingId}) referencing non-existing `PhaseGroup` {data.PhaseGroup}, set to 0"); + data.PhaseGroup = 0; + } + } + + if ((data.PhaseUseFlags & ~(byte)PhaseUseFlagsValues.All) != 0) + { + Log.outError(LogFilter.Sql, $"Table `spawn_tracking_template` has spawn tracking (Id: {data.SpawnTrackingId}) referencing unknown `PhaseUseFlags`, removed unknown value."); + data.PhaseUseFlags &= (byte)PhaseUseFlagsValues.All; + } + + if ((data.PhaseUseFlags & (byte)PhaseUseFlagsValues.AlwaysVisible) != 0 && (data.PhaseUseFlags & (byte)PhaseUseFlagsValues.Inverse) != 0) + { + Log.outError(LogFilter.Sql, $"Table `spawn_tracking_template` has spawn tracking (Id: {data.SpawnTrackingId}) with `PhaseUseFlags` PHASE_USE_FLAGS_ALWAYS_VISIBLE and PHASE_USE_FLAGS_INVERSE, removing PHASE_USE_FLAGS_INVERSE."); + data.PhaseUseFlags &= (byte)~PhaseUseFlagsValues.Inverse; + } + + _spawnTrackingDataStorage[spawnTrackingId] = data; + + } while (result.NextRow()); + + Log.outInfo(LogFilter.ServerLoading, $"Loaded {_spawnTrackingDataStorage.Count} spawn tracking templates in {Time.GetMSTimeDiffToNow(oldMSTime)} ms"); + } + + public SpawnTrackingTemplateData GetSpawnTrackingData(uint spawnTrackingId) + { + return _spawnTrackingDataStorage.LookupByKey(spawnTrackingId); + } + + public List GetSpawnMetadataForSpawnTracking(uint spawnTrackingId) { return _spawnTrackingMapStorage.LookupByKey(spawnTrackingId); } + + public List GetSpawnTrackingQuestObjectiveList(uint spawnTrackingId) { return _spawnTrackingQuestObjectiveStorage.LookupByKey(spawnTrackingId); } + + public bool IsQuestObjectiveForSpawnTracking(uint spawnTrackingId, uint questObjectiveId) + { + var questObjectiveList = _spawnTrackingQuestObjectiveStorage.LookupByKey(spawnTrackingId); + if (questObjectiveList != null) + if (questObjectiveList.Find(p => p.Id == questObjectiveId) != null) + return true; + + return false; + } + + public void LoadSpawnTrackingQuestObjectives() + { + uint oldMSTime = Time.GetMSTime(); + + // need for reload case + _spawnTrackingQuestObjectiveStorage.Clear(); + + // 0 1 + SQLResult result = DB.World.Query("SELECT SpawnTrackingId, QuestObjectiveId FROM spawn_tracking_quest_objective"); + + if (result.IsEmpty()) + { + Log.outInfo(LogFilter.ServerLoading, $"Loaded 0 spawn tracking quest objectives. DB table `spawn_tracking_quest_objective` is empty!"); + return; + } + + uint count = 0; + + do + { + uint spawnTrackingId = result.Read(0); + uint objectiveId = result.Read(1); + + if (GetSpawnTrackingData(spawnTrackingId) == null) + { + Log.outError(LogFilter.Sql, $"Table `spawn_tracking_quest_objective` has quest objective {objectiveId} assigned to spawn tracking {spawnTrackingId}, but spawn tracking does not exist!"); + continue; + } + + QuestObjective questObjective = GetQuestObjective(objectiveId); + if (questObjective == null) + { + Log.outError(LogFilter.Sql, $"Table `spawn_tracking_quest_objective` has quest objective {objectiveId} assigned to spawn tracking {spawnTrackingId}, but quest objective does not exist!"); + continue; + } + + _spawnTrackingQuestObjectiveStorage.Add(spawnTrackingId, questObjective); + + ++count; + } while (result.NextRow()); + + Log.outInfo(LogFilter.ServerLoading, $"Loaded {count} spawn tracking quest objectives in {Time.GetMSTimeDiffToNow(oldMSTime)} ms"); + } + + public void LoadSpawnTrackings() + { + uint oldMSTime = Time.GetMSTime(); + + // need for reload case + _spawnTrackingMapStorage.Clear(); + + // 0 1 2 3 + SQLResult result = DB.World.Query("SELECT SpawnTrackingId, SpawnType, SpawnId, QuestObjectiveId FROM spawn_tracking"); + + if (result.IsEmpty()) + { + Log.outInfo(LogFilter.ServerLoading, $"Loaded 0 spawn tracking members. DB table `spawn_tracking` is empty!"); + return; + } + + uint count = 0; + + do + { + uint spawnTrackingId = result.Read(0); + SpawnObjectType spawnType = (SpawnObjectType)result.Read(1); + ulong spawnId = result.Read(2); + uint objectiveId = result.Read(3); + + if (!SpawnData.TypeIsValid(spawnType)) + { + Log.outError(LogFilter.Sql, $"Table `spawn_tracking` has spawn data with invalid type {spawnType} listed for spawn tracking {spawnTrackingId}. Skipped."); + continue; + } + else if (spawnType == SpawnObjectType.AreaTrigger) + { + Log.outError(LogFilter.Sql, $"Table `spawn_tracking` has areatrigger spawn ({spawnType}) listed for spawn tracking {spawnTrackingId}. Skipped."); + continue; + } + + SpawnMetadata data = GetSpawnMetadata(spawnType, spawnId); + if (data == null) + { + Log.outError(LogFilter.Sql, $"Table `spawn_tracking` has spawn ({spawnType},{spawnId}) not found, but is listed as a member of spawn tracking {spawnTrackingId}!"); + continue; + } + else if (data.spawnTrackingData != null) + { + Log.outError(LogFilter.Sql, $"Table `spawn_tracking` has spawn ({spawnType},{spawnId}) is listed as a member of spawn tracking {spawnTrackingId}, but is already a member of spawn tracking {data.spawnTrackingData.SpawnTrackingId}. Skipped."); + continue; + } + + SpawnTrackingTemplateData spawnTrackingTemplateData = GetSpawnTrackingData(spawnTrackingId); + if (spawnTrackingTemplateData == null) + { + Log.outError(LogFilter.Sql, $"Table `spawn_tracking` has spawn tracking {spawnTrackingId} assigned to spawn ({spawnType},{spawnId}), but spawn tracking does not exist!"); + continue; + } + + if (!IsQuestObjectiveForSpawnTracking(spawnTrackingId, objectiveId)) + { + Log.outError(LogFilter.Sql, $"Table `spawn_tracking` has spawn tracking {spawnTrackingId} assigned to spawn ({spawnType},{spawnId}), but spawn tracking is not linked to quest objective {objectiveId}. Skipped."); + continue; + } + + if (spawnTrackingTemplateData.MapId != data.MapId) + { + Log.outError(LogFilter.Sql, $"Table `spawn_tracking` has spawn tracking {spawnTrackingId} (map {spawnTrackingTemplateData.MapId}) assigned to spawn ({spawnType},{spawnId}), but spawn has map {data.MapId} - spawn NOT added to spawn tracking!"); + continue; + } + + SpawnData spawnData = data.ToSpawnData(); + if (spawnTrackingTemplateData.PhaseId != spawnData.PhaseId || spawnTrackingTemplateData.PhaseGroup != spawnData.PhaseGroup || spawnTrackingTemplateData.PhaseUseFlags != (byte)spawnData.PhaseUseFlags) + { + Log.outError(LogFilter.Sql, $"Table `spawn_tracking` has spawn tracking {spawnTrackingId} with phase info (PhaseId: {spawnTrackingTemplateData.PhaseId}, PhaseGroup: {spawnTrackingTemplateData.PhaseGroup}, PhaseUseFlags: {spawnTrackingTemplateData.PhaseUseFlags}), ", + $"but spawn ({spawnType},{spawnId}) has different phase info (PhaseId: {spawnData.PhaseId}, PhaseGroup: {spawnData.PhaseGroup}, PhaseUseFlags: {spawnData.PhaseUseFlags}) - spawn NOT added to spawn tracking!"); + continue; + } + + data.spawnTrackingData = spawnTrackingTemplateData; + data.spawnTrackingQuestObjectiveId = objectiveId; + _spawnTrackingMapStorage.Add(spawnTrackingId, data); + + ++count; + } while (result.NextRow()); + + Log.outInfo(LogFilter.ServerLoading, $"Loaded {count} spawn tracking members in {Time.GetMSTimeDiffToNow(oldMSTime)} ms"); + } + + public void LoadSpawnTrackingStates() + { + uint oldMSTime = Time.GetMSTime(); + + // 0 1 2 3 4 5 6 7 + SQLResult result = DB.World.Query("SELECT SpawnType, SpawnId, State, Visible, StateSpellVisualId, StateAnimId, StateAnimKitId, StateWorldEffects FROM spawn_tracking_state"); + + if (result.IsEmpty()) + { + Log.outInfo(LogFilter.ServerLoading, $"Loaded 0 spawn tracking states. DB table `spawn_tracking_state` is empty!"); + return; + } + + uint count = 0; + + do + { + SpawnObjectType spawnType = (SpawnObjectType)result.Read(0); + ulong spawnId = result.Read(1); + SpawnTrackingState state = (SpawnTrackingState)result.Read(2); + + if (!SpawnData.TypeIsValid(spawnType)) + { + Log.outError(LogFilter.Sql, $"Table `spawn_tracking_state` has spawn data with invalid type {spawnType}. Skipped."); + continue; + } + else if (spawnType == SpawnObjectType.AreaTrigger) + { + Log.outError(LogFilter.Sql, $"Table `spawn_tracking_state` has areatrigger spawn ({spawnType}). Skipped."); + continue; + } + + SpawnMetadata data = GetSpawnMetadata(spawnType, spawnId); + if (data == null) + { + Log.outError(LogFilter.Sql, $"Table `spawn_tracking_state` has spawn ({spawnType},{spawnId}) not found!"); + continue; + } + else if (data.spawnTrackingData == null) + { + Log.outError(LogFilter.Sql, $"Table `spawn_tracking_state` has spawn ({spawnType},{spawnId}) with spawn tracking states, but is not part of a spawn tracking. Skipped."); + continue; + } + + if (state >= SpawnTrackingState.Max) + { + Log.outError(LogFilter.Sql, $"Table `spawn_tracking_state` has spawn ({spawnType},{spawnId}) with invalid state type {state}. Skipped."); + continue; + } + + SpawnTrackingStateData spawnTrackingStateData = data.spawnTrackingStates[(int)state]; + spawnTrackingStateData.Visible = result.Read(3); + + if (!result.IsNull(4)) + { + spawnTrackingStateData.StateSpellVisualId = result.Read(4); + if (!CliDB.SpellVisualStorage.HasRecord(spawnTrackingStateData.StateSpellVisualId.Value)) + { + Log.outError(LogFilter.Sql, $"Table `spawn_tracking_state` references invalid StateSpellVisualId {spawnTrackingStateData.StateSpellVisualId} for spawn ({spawnType},{spawnId}), set to none."); + spawnTrackingStateData.StateSpellVisualId = null; + } + } + + if (!result.IsNull(5)) + { + spawnTrackingStateData.StateAnimId = result.Read(5); + if (spawnTrackingStateData.StateAnimId != Global.DB2Mgr.GetEmptyAnimStateID() && !CliDB.AnimationDataStorage.HasRecord(spawnTrackingStateData.StateAnimId.Value)) + { + Log.outError(LogFilter.Sql, $"Table `spawn_tracking_state` references invalid StateAnimId {spawnTrackingStateData.StateAnimId} for spawn ({spawnType},{spawnId}), set to none."); + spawnTrackingStateData.StateAnimId = null; + } + } + + if (!result.IsNull(6)) + { + spawnTrackingStateData.StateAnimKitId = result.Read(6); + if (!CliDB.AnimKitStorage.HasRecord(spawnTrackingStateData.StateAnimKitId.Value)) + { + Log.outError(LogFilter.Sql, $"Table `spawn_tracking_state` references invalid StateAnimKitId {spawnTrackingStateData.StateAnimKitId} for spawn ({spawnType},{spawnId}), set to none."); + spawnTrackingStateData.StateAnimKitId = null; + } + } + + if (!result.IsNull(7)) + { + List worldEffectList = new(); + foreach (var worldEffectsStr in result.Read(7).Split(',')) + { + if (!uint.TryParse(worldEffectsStr, out uint worldEffectId)) + continue; + + if (!CliDB.WorldEffectStorage.HasRecord(worldEffectId)) + { + Log.outError(LogFilter.Sql, $"Table `spawn_tracking_state` references invalid StateAnimKitId {worldEffectId} for spawn ({spawnType},{spawnId}). Skipped."); + continue; + } + + worldEffectList.Add(worldEffectId); + } + + if (!worldEffectList.Empty()) + spawnTrackingStateData.StateWorldEffects = worldEffectList; + } + + ++count; + } while (result.NextRow()); + + Log.outInfo(LogFilter.ServerLoading, $"Loaded {count} spawn tracking states in {Time.GetMSTimeDiffToNow(oldMSTime)} ms"); + } + public CreatureLocale GetCreatureLocale(uint entry) { return _creatureLocaleStorage.LookupByKey(entry); @@ -10237,7 +10592,7 @@ namespace Game SQLResult result = DB.World.Query("SELECT CreatureId, CurrencyId FROM creature_quest_currency ORDER BY CreatureId, CurrencyId ASC"); if (result.IsEmpty()) { - Log.outInfo(LogFilter.ServerLoading, ">> Loaded 0 creature quest currencies. DB table `creature_quest_currency` is empty."); + Log.outInfo(LogFilter.ServerLoading, $"Loaded 0 creature quest currencies. DB table `creature_quest_currency` is empty."); return; } @@ -11242,6 +11597,9 @@ namespace Game Dictionary _questObjectives = new(); Dictionary[] _questGreetingStorage = new Dictionary[2]; Dictionary[] _questGreetingLocaleStorage = new Dictionary[2]; + Dictionary _spawnTrackingDataStorage = new(); + MultiMap _spawnTrackingMapStorage = new(); + MultiMap _spawnTrackingQuestObjectiveStorage = new(); //Scripts ScriptNameContainer _scriptNamesStorage = new(); diff --git a/Source/Game/Handlers/CharacterHandler.cs b/Source/Game/Handlers/CharacterHandler.cs index e1cf69b97..d5acece14 100644 --- a/Source/Game/Handlers/CharacterHandler.cs +++ b/Source/Game/Handlers/CharacterHandler.cs @@ -2732,6 +2732,10 @@ namespace Game stmt.AddValue(0, lowGuid); SetQuery(PlayerLoginQueryLoad.QuestStatusObjectivesCriteriaProgress, stmt); + stmt = CharacterDatabase.GetPreparedStatement(CharStatements.SEL_CHARACTER_QUESTSTATUS_OBJECTIVES_SPAWN_TRACKING); + stmt.AddValue(0, lowGuid); + SetQuery(PlayerLoginQueryLoad.QuestStatusSpawnTracking, stmt); + stmt = CharacterDatabase.GetPreparedStatement(CharStatements.SEL_CHARACTER_QUESTSTATUS_DAILY); stmt.AddValue(0, lowGuid); SetQuery(PlayerLoginQueryLoad.DailyQuestStatus, stmt); @@ -2992,6 +2996,7 @@ namespace Game QuestStatusObjectives, QuestStatusObjectivesCriteria, QuestStatusObjectivesCriteriaProgress, + QuestStatusSpawnTracking, DailyQuestStatus, Reputation, Inventory, diff --git a/Source/Game/Handlers/QuestHandler.cs b/Source/Game/Handlers/QuestHandler.cs index 1d7a0d823..9bd4d87a7 100644 --- a/Source/Game/Handlers/QuestHandler.cs +++ b/Source/Game/Handlers/QuestHandler.cs @@ -9,6 +9,7 @@ using Game.Networking; using Game.Networking.Packets; using Game.DataStorage; using System.Collections.Generic; +using System; namespace Game { @@ -416,6 +417,7 @@ namespace Game } } + GetPlayer().SendForceSpawnTrackingUpdate(questId); GetPlayer().SetQuestSlot(packet.Entry, 0); GetPlayer().TakeQuestSourceItem(questId, true); // remove quest src item from player GetPlayer().AbandonQuest(questId); // remove all quest items player received before abandoning quest. Note, this does not remove normal drop items that happen to be quest requirements. @@ -839,5 +841,68 @@ namespace Game SendPacket(response); } + + [WorldPacketHandler(ClientOpcodes.SpawnTrackingUpdate)] + void HandleSpawnTrackingUpdate(SpawnTrackingUpdate spawnTrackingUpdate) + { + QuestPOIUpdateResponse response = new(); + + bool hasObjectTypeRequested(TypeMask objectTypeMask, SpawnObjectType objectType) + { + if (objectTypeMask.HasAnyFlag(TypeMask.Unit)) + return objectType == SpawnObjectType.Creature; + else if (objectTypeMask.HasAnyFlag(TypeMask.GameObject)) + return objectType == SpawnObjectType.GameObject; + + return false; + } + + foreach (var requestInfo in spawnTrackingUpdate.SpawnTrackingRequests) + { + SpawnTrackingResponseInfo responseInfo = new(); + responseInfo.SpawnTrackingID = requestInfo.SpawnTrackingID; + responseInfo.ObjectID = requestInfo.ObjectID; + + var spawnTrackingTemplateData = Global.ObjectMgr.GetSpawnTrackingData(requestInfo.SpawnTrackingID); + QuestObjective activeQuestObjective = _player.GetActiveQuestObjectiveForForSpawnTracking(requestInfo.SpawnTrackingID); + + // Send phase info if map is the same or spawn tracking related quests are taken or completed + if (spawnTrackingTemplateData != null && (_player.GetMapId() == spawnTrackingTemplateData.MapId || activeQuestObjective != null)) + { + responseInfo.PhaseID = (int)spawnTrackingTemplateData.PhaseId; + responseInfo.PhaseGroupID = (int)spawnTrackingTemplateData.PhaseGroup; + responseInfo.PhaseUseFlags = spawnTrackingTemplateData.PhaseUseFlags; + + // Send spawn visibility data if available + if (requestInfo.ObjectTypeMask != 0 && (requestInfo.ObjectTypeMask & (int)(TypeMask.Unit | TypeMask.GameObject)) != 0) + { + // There should only be one entity + foreach (var data in Global.ObjectMgr.GetSpawnMetadataForSpawnTracking(requestInfo.SpawnTrackingID)) + { + var spawnData = data.ToSpawnData(); + if (spawnData == null) + continue; + + if (spawnData.Id != requestInfo.ObjectID) + continue; + + if (!hasObjectTypeRequested((TypeMask)requestInfo.ObjectTypeMask, data.type)) + continue; + + if (activeQuestObjective != null) + { + SpawnTrackingState state = _player.GetSpawnTrackingStateByObjective(requestInfo.SpawnTrackingID, activeQuestObjective.Id); + responseInfo.Visible = data.spawnTrackingStates[(int)state].Visible; + break; + } + } + } + } + + response.SpawnTrackingResponses.Add(responseInfo); + } + + SendPacket(response); + } } -} +} \ No newline at end of file diff --git a/Source/Game/Maps/SpawnData.cs b/Source/Game/Maps/SpawnData.cs index e7b0e0845..a6fe151e1 100644 --- a/Source/Game/Maps/SpawnData.cs +++ b/Source/Game/Maps/SpawnData.cs @@ -58,7 +58,10 @@ namespace Game.Maps public ulong SpawnId; public uint MapId = 0xFFFFFFFF; public bool dbData = true; - public SpawnGroupTemplateData spawnGroupData = null; + public SpawnGroupTemplateData spawnGroupData; + public SpawnTrackingTemplateData spawnTrackingData; + public uint spawnTrackingQuestObjectiveId; + public SpawnTrackingStateData[] spawnTrackingStates = new SpawnTrackingStateData[(int)SpawnTrackingState.Max]; public static bool TypeInMask(SpawnObjectType type, SpawnObjectTypeMask mask) { return ((1 << (int)type) & (int)mask) != 0; } public static bool TypeHasData(SpawnObjectType type) { return type < SpawnObjectType.NumSpawnTypesWithData; } @@ -71,4 +74,22 @@ namespace Game.Maps public SpawnData ToSpawnData() { return TypeHasData(type) ? (SpawnData)this : null; } } + + public class SpawnTrackingTemplateData + { + public uint SpawnTrackingId; + public uint MapId; + public uint PhaseId; + public uint PhaseGroup; + public byte PhaseUseFlags; + } + + public class SpawnTrackingStateData + { + public bool Visible = true; + public uint? StateSpellVisualId; + public ushort? StateAnimId; + public ushort? StateAnimKitId; + public List StateWorldEffects = new(); + } } diff --git a/Source/Game/Networking/Packets/QuestPackets.cs b/Source/Game/Networking/Packets/QuestPackets.cs index f00b587bb..4b0b4fa28 100644 --- a/Source/Game/Networking/Packets/QuestPackets.cs +++ b/Source/Game/Networking/Packets/QuestPackets.cs @@ -994,6 +994,52 @@ namespace Game.Networking.Packets } } + class SpawnTrackingUpdate : ClientPacket + { + public List SpawnTrackingRequests = new(); + + public SpawnTrackingUpdate(WorldPacket packet) : base(packet) { } + + public override void Read() + { + var spawnTrackingRequestCount = _worldPacket.ReadUInt32(); + for (var i = 0; i < spawnTrackingRequestCount; i++) + { + var spawnTrackingRequestInfo = new SpawnTrackingRequestInfo(); + spawnTrackingRequestInfo.Read(_worldPacket); + SpawnTrackingRequests.Add(spawnTrackingRequestInfo); + } + } + + } + + class QuestPOIUpdateResponse : ServerPacket + { + public List SpawnTrackingResponses = new(); + + public QuestPOIUpdateResponse() : base(ServerOpcodes.QuestPoiUpdateResponse, ConnectionType.Instance) { } + + public override void Write() + { + _worldPacket.WriteInt32(SpawnTrackingResponses.Count); + + foreach (SpawnTrackingResponseInfo spawnTrackingResponseInfo in SpawnTrackingResponses) + spawnTrackingResponseInfo.Write(_worldPacket); + } + } + + class ForceSpawnTrackingUpdate : ServerPacket + { + public uint QuestID; + + public ForceSpawnTrackingUpdate() : base(ServerOpcodes.ForceSpawnTrackingUpdate, ConnectionType.Instance) { } + + public override void Write() + { + _worldPacket.WriteUInt32(QuestID); + } + } + //Structs public class QuestGiverInfo { @@ -1533,4 +1579,40 @@ namespace Game.Networking.Packets MawPower.Value.Write(data); } } + + struct SpawnTrackingRequestInfo + { + public int ObjectTypeMask; + public int ObjectID; + public uint SpawnTrackingID; + + public void Read(WorldPacket data) + { + ObjectTypeMask = data.ReadInt32(); + ObjectID = data.ReadInt32(); + SpawnTrackingID = data.ReadUInt32(); + } + } + + class SpawnTrackingResponseInfo + { + public uint SpawnTrackingID; + public int ObjectID; + public int PhaseID; + public int PhaseGroupID; + public int PhaseUseFlags; + public bool Visible = true; + + public void Write(WorldPacket data) + { + data.WriteUInt32(SpawnTrackingID); + data.WriteInt32(ObjectID); + data.WriteInt32(PhaseID); + data.WriteInt32(PhaseGroupID); + data.WriteInt32(PhaseUseFlags); + + data.WriteBit(Visible); + data.FlushBits(); + } + } } \ No newline at end of file diff --git a/Source/Game/Quest/Quest.cs b/Source/Game/Quest/Quest.cs index 4f798f721..40dcb8aa1 100644 --- a/Source/Game/Quest/Quest.cs +++ b/Source/Game/Quest/Quest.cs @@ -995,6 +995,7 @@ namespace Game public long AcceptTime; public uint Timer; public bool Explored; + public List<(sbyte, uint)> SpawnTrackingList = new(); } public class QuestGreeting diff --git a/Source/Game/World/WorldManager.cs b/Source/Game/World/WorldManager.cs index edf037b3e..f1070c323 100644 --- a/Source/Game/World/WorldManager.cs +++ b/Source/Game/World/WorldManager.cs @@ -803,6 +803,18 @@ namespace Game Log.outInfo(LogFilter.ServerLoading, "Loading Player Choices..."); Global.ObjectMgr.LoadPlayerChoices(); + Log.outInfo(LogFilter.ServerLoading, "Loading Spawn Tracking Templates..."); + Global.ObjectMgr.LoadSpawnTrackingTemplates(); + + Log.outInfo(LogFilter.ServerLoading, "Loading Spawn Tracking Quest Objectives..."); + Global.ObjectMgr.LoadSpawnTrackingQuestObjectives(); + + Log.outInfo(LogFilter.ServerLoading, "Loading Spawn Tracking Spawns..."); + Global.ObjectMgr.LoadSpawnTrackings(); + + Log.outInfo(LogFilter.ServerLoading, "Loading Spawn Tracking Spawn States..."); + Global.ObjectMgr.LoadSpawnTrackingStates(); + if (WorldConfig.GetBoolValue(WorldCfg.LoadLocales)) { Log.outInfo(LogFilter.ServerLoading, "Loading Player Choices Locales...");