diff --git a/Source/Framework/Constants/SharedConst.cs b/Source/Framework/Constants/SharedConst.cs index 3bfd7013b..4f89a7c1d 100644 --- a/Source/Framework/Constants/SharedConst.cs +++ b/Source/Framework/Constants/SharedConst.cs @@ -3032,6 +3032,26 @@ namespace Framework.Constants IgnoreTransport = 0x20 } + [Flags] + public enum VignetteFlags + { + InfiniteAOI = 0x000001, + ShowOnMap = 0x000002, + PingMinimap = 0x000004, + TestVisibilityRules = 0x000008, + VerticalRangeIsAbsolute = 0x000010, + Unique = 0x000020, + ZoneInfiniteAOI = 0x000040, + PersistsThroughDeath = 0x000080, + + DontShowOnMinimap = 0x000200, + HasTooltip = 0x000400, + + AdditionalHeightReq = 0x008000, // Must be within 10 yards of vignette Z coord (hardcoded in client) + HideOnContinentMaps = 0x010000, + NoPaddingAboveUiWidgets = 0x020000 + } + public enum WorldMapTransformsFlags { Dungeon = 0x04 diff --git a/Source/Framework/Database/Databases/HotfixDatabase.cs b/Source/Framework/Database/Databases/HotfixDatabase.cs index 416cacd84..96304ac98 100644 --- a/Source/Framework/Database/Databases/HotfixDatabase.cs +++ b/Source/Framework/Database/Databases/HotfixDatabase.cs @@ -1566,6 +1566,11 @@ namespace Framework.Database "ExitAnimKitID, VehicleEnterAnimKitID, VehicleRideAnimKitID, VehicleExitAnimKitID, CameraModeID FROM vehicle_seat" + " WHERE (`VerifiedBuild` > 0) = ?"); + // Vignette.db2 + PrepareStatement(HotfixStatements.SEL_VIGNETTE, "SELECT ID, Name, PlayerConditionID, VisibleTrackingQuestID, QuestFeedbackEffectID, Flags, MaxHeight, " + + "MinHeight, VignetteType, RewardQuestID, UiWidgetSetID FROM vignette WHERE (`VerifiedBuild` > 0) = ?"); + PrepareStatement(HotfixStatements.SEL_VIGNETTE_LOCALE, "SELECT ID, Name_lang FROM vignette_locale WHERE (`VerifiedBuild` > 0) = ? AND locale = ?"); + // WmoAreaTable.db2 PrepareStatement(HotfixStatements.SEL_WMO_AREA_TABLE, "SELECT AreaName, ID, WmoID, NameSetID, WmoGroupID, SoundProviderPref, SoundProviderPrefUnderwater, " + "AmbienceID, UwAmbience, ZoneMusic, UwZoneMusic, IntroSound, UwIntroSound, AreaTableID, Flags FROM wmo_area_table" + @@ -2340,6 +2345,9 @@ namespace Framework.Database SEL_VEHICLE_SEAT, + SEL_VIGNETTE, + SEL_VIGNETTE_LOCALE, + SEL_WMO_AREA_TABLE, SEL_WMO_AREA_TABLE_LOCALE, diff --git a/Source/Game/DataStorage/CliDB.cs b/Source/Game/DataStorage/CliDB.cs index 4bb0770ee..88785a717 100644 --- a/Source/Game/DataStorage/CliDB.cs +++ b/Source/Game/DataStorage/CliDB.cs @@ -377,6 +377,7 @@ namespace Game.DataStorage UnitPowerBarStorage = ReadDB2("UnitPowerBar.db2", HotfixStatements.SEL_UNIT_POWER_BAR, HotfixStatements.SEL_UNIT_POWER_BAR_LOCALE); VehicleStorage = ReadDB2("Vehicle.db2", HotfixStatements.SEL_VEHICLE); VehicleSeatStorage = ReadDB2("VehicleSeat.db2", HotfixStatements.SEL_VEHICLE_SEAT); + VignetteStorage = ReadDB2("Vignette.db2", HotfixStatements.SEL_VIGNETTE, HotfixStatements.SEL_VIGNETTE_LOCALE); WMOAreaTableStorage = ReadDB2("WMOAreaTable.db2", HotfixStatements.SEL_WMO_AREA_TABLE, HotfixStatements.SEL_WMO_AREA_TABLE_LOCALE); WorldEffectStorage = ReadDB2("WorldEffect.db2", HotfixStatements.SEL_WORLD_EFFECT); WorldMapOverlayStorage = ReadDB2("WorldMapOverlay.db2", HotfixStatements.SEL_WORLD_MAP_OVERLAY); @@ -813,6 +814,7 @@ namespace Game.DataStorage public static DB6Storage UnitPowerBarStorage; public static DB6Storage VehicleStorage; public static DB6Storage VehicleSeatStorage; + public static DB6Storage VignetteStorage; public static DB6Storage WMOAreaTableStorage; public static DB6Storage WorldEffectStorage; public static DB6Storage WorldMapOverlayStorage; diff --git a/Source/Game/DataStorage/Structs/V_Records.cs b/Source/Game/DataStorage/Structs/V_Records.cs index 2b7ae1bd0..eeb932d23 100644 --- a/Source/Game/DataStorage/Structs/V_Records.cs +++ b/Source/Game/DataStorage/Structs/V_Records.cs @@ -120,4 +120,25 @@ namespace Game.DataStorage } public bool IsEjectable() { return HasFlag(VehicleSeatFlagsB.Ejectable); } } + + public sealed class VignetteRecord + { + public uint ID; + public LocalizedString Name; + public uint PlayerConditionID; + public uint VisibleTrackingQuestID; + public uint QuestFeedbackEffectID; + public int Flags; + public float MaxHeight; + public float MinHeight; + public sbyte VignetteType; + public int RewardQuestID; + public int UiWidgetSetID; + + public VignetteFlags GetFlags() { return (VignetteFlags)Flags; } + public bool IsInfiniteAOI() + { + return GetFlags().HasFlag(VignetteFlags.InfiniteAOI | VignetteFlags.ZoneInfiniteAOI); + } + } } diff --git a/Source/Game/Entities/Creature/Creature.cs b/Source/Game/Entities/Creature/Creature.cs index 4e2245080..273d5a416 100644 --- a/Source/Game/Entities/Creature/Creature.cs +++ b/Source/Game/Entities/Creature/Creature.cs @@ -1724,6 +1724,13 @@ namespace Game.Entities if (CreateVehicleKit(vehId, entry, true)) UpdateDisplayPower(); + if (!IsPet()) + { + uint vignetteId = GetCreatureTemplate().VignetteID; + if (vignetteId != 0) + SetVignette(vignetteId); + } + return true; } @@ -2052,6 +2059,10 @@ namespace Game.Entities RemoveUnitFlag(UnitFlags.InCombat); SetMeleeDamageSchool((SpellSchools)cInfo.DmgSchool); + + uint vignetteId = cInfo.VignetteID; + if (vignetteId != 0) + SetVignette(vignetteId); } InitializeMovementAI(); diff --git a/Source/Game/Entities/Creature/CreatureData.cs b/Source/Game/Entities/Creature/CreatureData.cs index 4a8234089..ab2a2b537 100644 --- a/Source/Game/Entities/Creature/CreatureData.cs +++ b/Source/Game/Entities/Creature/CreatureData.cs @@ -24,7 +24,7 @@ namespace Game.Entities public List GossipMenuIds = new(); public Dictionary difficultyStorage = new(); public uint RequiredExpansion; - public uint VignetteID; // @todo Read Vignette.db2 + public uint VignetteID; public uint Faction; public ulong Npcflag; public float SpeedWalk; diff --git a/Source/Game/Entities/GameObject/GameObject.cs b/Source/Game/Entities/GameObject/GameObject.cs index 93a08f193..f43118099 100644 --- a/Source/Game/Entities/GameObject/GameObject.cs +++ b/Source/Game/Entities/GameObject/GameObject.cs @@ -72,6 +72,8 @@ namespace Game.Entities public override void CleanupsBeforeDelete(bool finalCleanup) { + SetVignette(0); + base.CleanupsBeforeDelete(finalCleanup); RemoveFromOwner(); @@ -371,6 +373,10 @@ namespace Game.Entities _animKitId = (ushort)gameObjectAddon.AIAnimKitID; } + uint vignetteId = GetGoInfo().GetSpawnVignette(); + if (vignetteId != 0) + SetVignette(vignetteId); + LastUsedScriptID = GetGoInfo().ScriptId; m_stringIds[0] = goInfo.StringId; @@ -2591,6 +2597,25 @@ namespace Game.Entities break; } + if (m_vignette != null) + { + Player player = user.ToPlayer(); + if (player != null) + { + Quest reward = Global.ObjectMgr.GetQuestTemplate((uint)m_vignette.Data.RewardQuestID); + if (reward != null && !player.GetQuestRewardStatus((uint)m_vignette.Data.RewardQuestID)) + player.RewardQuest(reward, LootItemType.Item, 0, this, false); + + if (m_vignette.Data.VisibleTrackingQuestID != 0) + player.SetRewardedQuest(m_vignette.Data.VisibleTrackingQuestID); + } + + // only unregister it from visibility (need to keep vignette for other gameobject users in case its usable by multiple players + // to flag their quest completion + if (GetGoInfo().ClearObjectVignetteonOpening()) + Vignettes.Remove(m_vignette, this); + } + if (spellId == 0) return; @@ -3348,6 +3373,10 @@ namespace Game.Entities if (m_goTypeImpl != null) m_goTypeImpl.OnRelocated(); + // TODO: on heartbeat + if (m_vignette != null) + Vignettes.Update(m_vignette, this); + UpdateObjectVisibility(false); } @@ -3423,6 +3452,24 @@ namespace Game.Entities SendMessageToSet(activateAnimKit, true); } + public override VignetteData GetVignette() { return m_vignette; } + + public void SetVignette(uint vignetteId) + { + if (m_vignette != null) + { + if (m_vignette.Data.ID == vignetteId) + return; + + Vignettes.Remove(m_vignette, this); + m_vignette = null; + } + + VignetteRecord vignette = CliDB.VignetteStorage.LookupByKey(vignetteId); + if (vignette != null) + m_vignette = Vignettes.Create(vignette, this); + } + public void SetSpellVisualId(uint spellVisualId, ObjectGuid activatorGuid = default) { SetUpdateFieldValue(m_values.ModifyValue(m_gameObjectData).ModifyValue(m_gameObjectData.SpellVisualID), spellVisualId); @@ -3900,6 +3947,8 @@ namespace Game.Entities ushort _animKitId; uint _worldEffectID; + VignetteData m_vignette; + Dictionary m_perPlayerState; GameObjectState m_prevGoState; // What state to set whenever resetting diff --git a/Source/Game/Entities/GameObject/GameObjectData.cs b/Source/Game/Entities/GameObject/GameObjectData.cs index 2f6fe2cc7..c2eed0929 100644 --- a/Source/Game/Entities/GameObject/GameObjectData.cs +++ b/Source/Game/Entities/GameObject/GameObjectData.cs @@ -744,7 +744,25 @@ namespace Game.Entities GameObjectTypes.AuraGenerator => AuraGenerator.serverOnly, _ => 0, }; - + + public uint GetSpawnVignette() => type switch + { + + GameObjectTypes.Chest => Chest.SpawnVignette, + GameObjectTypes.Goober => Goober.SpawnVignette, + GameObjectTypes.NewFlag => NewFlag.SpawnVignette, + GameObjectTypes.NewFlagDrop => NewFlagDrop.SpawnVignette, + GameObjectTypes.CapturePoint => CapturePoint.SpawnVignette, + GameObjectTypes.GatheringNode => GatheringNode.SpawnVignette, + _ => 0 + }; + + public bool ClearObjectVignetteonOpening() => type switch + { + GameObjectTypes.GatheringNode => GatheringNode.ClearObjectVignetteonOpening != 0, + _ => false + }; + public uint GetSpellFocusType() { switch (type) @@ -776,7 +794,7 @@ namespace Game.Entities GameObjectTypes.SpellFocus or GameObjectTypes.Multi or GameObjectTypes.SiegeableMulti => false, _ => true }; - + public void InitializeQueryData() { QueryData = new QueryGameObjectResponse(); diff --git a/Source/Game/Entities/Object/WorldObject.cs b/Source/Game/Entities/Object/WorldObject.cs index 10a4d71f6..c4b7a341e 100644 --- a/Source/Game/Entities/Object/WorldObject.cs +++ b/Source/Game/Entities/Object/WorldObject.cs @@ -1553,6 +1553,8 @@ namespace Game.Entities return null; } + public virtual VignetteData GetVignette() { return null; } + public TempSummon SummonCreature(uint entry, float x, float y, float z, float o = 0, TempSummonType despawnType = TempSummonType.ManualDespawn, TimeSpan despawnTime = default, ObjectGuid privateObjectOwner = default) { if (x == 0.0f && y == 0.0f && z == 0.0f) diff --git a/Source/Game/Entities/Player/Player.Map.cs b/Source/Game/Entities/Player/Player.Map.cs index 9b0a21a18..ea7ff8533 100644 --- a/Source/Game/Entities/Player/Player.Map.cs +++ b/Source/Game/Entities/Player/Player.Map.cs @@ -230,6 +230,22 @@ namespace Game.Entities UpdateCriteria(CriteriaType.EnterTopLevelArea, newZone); UpdateCriteria(CriteriaType.LeaveTopLevelArea, oldZone); + + VignetteUpdate vignetteUpdate = new(); + + foreach (var vignette in GetMap().GetInfiniteAOIVignettes()) + { + if (!vignette.Data.GetFlags().HasFlag(VignetteFlags.ZoneInfiniteAOI)) + continue; + + if (vignette.ZoneID == newZone && Vignettes.CanSee(this, vignette)) + vignette.FillPacket(vignetteUpdate.Added); + else if (vignette.ZoneID == oldZone) + vignetteUpdate.Removed.Add(vignette.Guid); + } + + if (!vignetteUpdate.Added.IDs.Empty() || !vignetteUpdate.Removed.Empty()) + SendPacket(vignetteUpdate); } } diff --git a/Source/Game/Entities/Player/Player.cs b/Source/Game/Entities/Player/Player.cs index ba2665884..45f26aff6 100644 --- a/Source/Game/Entities/Player/Player.cs +++ b/Source/Game/Entities/Player/Player.cs @@ -5644,6 +5644,17 @@ namespace Game.Entities { UpdateVisibilityForPlayer(); + // Send map wide vignettes before UpdateZone, that will send zone wide vignettes + // But first send on new map will wipe all vignettes on client + VignetteUpdate vignetteUpdate = new(); + vignetteUpdate.ForceUpdate = true; + + foreach (VignetteData vignette in GetMap().GetInfiniteAOIVignettes()) + if (!vignette.Data.GetFlags().HasFlag(VignetteFlags.ZoneInfiniteAOI) && Vignettes.CanSee(this, vignette)) + vignette.FillPacket(vignetteUpdate.Added); + + SendPacket(vignetteUpdate); + // update zone uint newzone, newarea; GetZoneAndAreaId(out newzone, out newarea); @@ -6034,13 +6045,23 @@ namespace Game.Entities } #region Sends / Updates - void BeforeVisibilityDestroy(WorldObject obj, Player p) + void BeforeVisibilityDestroy(WorldObject t, Player p) { - if (!obj.IsTypeId(TypeId.Unit)) - return; + var creature = t.ToCreature(); + if (creature != null) + if (p.GetPetGUID() == creature.GetGUID() && creature.IsPet()) + creature.ToPet().Remove(PetSaveMode.NotInSlot, true); - if (p.GetPetGUID() == obj.GetGUID() && obj.ToCreature().IsPet()) - ((Pet)obj).Remove(PetSaveMode.NotInSlot, true); + VignetteData vignette = t.GetVignette(); + if (vignette != null) + { + if (!vignette.Data.IsInfiniteAOI()) + { + VignetteUpdate vignetteUpdate = new(); + vignetteUpdate.Removed.Add(vignette.Guid); + p.SendPacket(vignetteUpdate); + } + } } public void UpdateVisibilityOf(ICollection targets) @@ -6103,8 +6124,7 @@ namespace Game.Entities { if (!CanSeeOrDetect(target, false, true)) { - if (target.IsTypeId(TypeId.Unit)) - BeforeVisibilityDestroy(target.ToCreature(), this); + BeforeVisibilityDestroy(target, this); if (!target.IsDestroyedObject()) target.SendOutOfRangeForPlayer(this); @@ -6158,6 +6178,16 @@ namespace Game.Entities public void SendInitialVisiblePackets(WorldObject target) { + var sendVignette = (VignetteData vignette, Player where) => + { + if (!vignette.Data.IsInfiniteAOI() && Vignettes.CanSee(where, vignette)) + { + VignetteUpdate vignetteUpdate = new(); + vignette.FillPacket(vignetteUpdate.Added); + where.SendPacket(vignetteUpdate); + } + }; + Unit targetUnit = target.ToUnit(); if (targetUnit != null) { @@ -6167,6 +6197,20 @@ namespace Game.Entities if (targetUnit.HasUnitState(UnitState.MeleeAttacking) && targetUnit.GetVictim() != null) targetUnit.SendMeleeAttackStart(targetUnit.GetVictim()); } + + VignetteData vignette = targetUnit.GetVignette(); + if (vignette != null) + sendVignette(vignette, this); + } + else + { + GameObject targetGo = target.ToGameObject(); + if (targetGo != null) + { + VignetteData vignette = targetGo.GetVignette(); + if (vignette != null) + sendVignette(vignette, this); + } } } diff --git a/Source/Game/Entities/Unit/Unit.Combat.cs b/Source/Game/Entities/Unit/Unit.Combat.cs index 8887deec3..49e8d2309 100644 --- a/Source/Game/Entities/Unit/Unit.Combat.cs +++ b/Source/Game/Entities/Unit/Unit.Combat.cs @@ -928,6 +928,20 @@ namespace Game.Entities } } + VignetteData vignette = victim.GetVignette(); + if (vignette != null) + { + foreach (Player tapper in tappers) + { + Quest reward = Global.ObjectMgr.GetQuestTemplate((uint)vignette.Data.RewardQuestID); + if (reward != null) + tapper.RewardQuest(reward, LootItemType.Item, 0, victim, false); + + if (vignette.Data.VisibleTrackingQuestID != 0) + tapper.SetRewardedQuest(vignette.Data.VisibleTrackingQuestID); + } + } + new KillRewarder(tappers.ToArray(), victim, false).Reward(); } diff --git a/Source/Game/Entities/Unit/Unit.Fields.cs b/Source/Game/Entities/Unit/Unit.Fields.cs index 077bd2952..785214922 100644 --- a/Source/Game/Entities/Unit/Unit.Fields.cs +++ b/Source/Game/Entities/Unit/Unit.Fields.cs @@ -110,6 +110,7 @@ namespace Game.Entities public UnitTypeMask UnitTypeMask { get; set; } UnitState m_state; protected LiquidTypeRecord _lastLiquid; + VignetteData m_vignette; protected DeathState m_deathState; public Vehicle m_vehicle { get; set; } public Vehicle VehicleKit { get; set; } diff --git a/Source/Game/Entities/Unit/Unit.Movement.cs b/Source/Game/Entities/Unit/Unit.Movement.cs index e33de30f8..0a41e7bd6 100644 --- a/Source/Game/Entities/Unit/Unit.Movement.cs +++ b/Source/Game/Entities/Unit/Unit.Movement.cs @@ -684,6 +684,10 @@ namespace Game.Entities if (isInWater) RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags2.Swimming); + // TODO: on heartbeat + if (m_vignette != null) + Vignettes.Update(m_vignette, this); + return (relocated || turn); } diff --git a/Source/Game/Entities/Unit/Unit.cs b/Source/Game/Entities/Unit/Unit.cs index 38ef3d0fe..ae39f0dbb 100644 --- a/Source/Game/Entities/Unit/Unit.cs +++ b/Source/Game/Entities/Unit/Unit.cs @@ -538,6 +538,8 @@ namespace Game.Entities // This needs to be before RemoveFromWorld to make GetCaster() return a valid for aura removal InterruptNonMeleeSpells(true); + SetVignette(0); + if (IsInWorld) RemoveFromWorld(); else @@ -939,6 +941,24 @@ namespace Game.Entities return collisionHeight1 == 0.0f ? MapConst.DefaultCollesionHeight : collisionHeight1; } + public override VignetteData GetVignette() { return m_vignette; } + + public void SetVignette(uint vignetteId) + { + if (m_vignette != null) + { + if (m_vignette.Data.ID == vignetteId) + return; + + Vignettes.Remove(m_vignette, this); + m_vignette = null; + } + + var vignette = CliDB.VignetteStorage.LookupByKey(vignetteId); + if (vignette != null) + m_vignette = Vignettes.Create(vignette, this); + } + public override string GetDebugInfo() { string str = $"{base.GetDebugInfo()}\nIsAIEnabled: {IsAIEnabled()} DeathState: {GetDeathState()} UnitMovementFlags: {GetUnitMovementFlags()} UnitMovementFlags2: {GetUnitMovementFlags2()} Class: {GetClass()}\n" + @@ -1574,6 +1594,9 @@ namespace Game.Entities SetEmoteState(Emote.OneshotNone); SetStandState(UnitStandStateType.Stand); + if (m_vignette != null && !m_vignette.Data.GetFlags().HasFlag(VignetteFlags.PersistsThroughDeath)) + SetVignette(0); + // players in instance don't have ZoneScript, but they have InstanceScript ZoneScript zoneScript = GetZoneScript() != null ? GetZoneScript() : GetInstanceScript(); if (zoneScript != null) diff --git a/Source/Game/Entities/Unit/Vignette.cs b/Source/Game/Entities/Unit/Vignette.cs new file mode 100644 index 000000000..6d9c68194 --- /dev/null +++ b/Source/Game/Entities/Unit/Vignette.cs @@ -0,0 +1,147 @@ +// Copyright (c) CypherCore All rights reserved. +// Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. + +using Framework.Constants; +using Game.DataStorage; +using Game.Maps; +using Game.Networking.Packets; +using System.Collections.Generic; + +namespace Game.Entities +{ + static class Vignettes + { + public static void UpdatePosition(VignetteData vignette, WorldObject owner) + { + vignette.Position = owner.GetPosition(); + WmoLocation wmoLocation = owner.GetCurrentWmo(); + if (wmoLocation != null) + { + vignette.WMOGroupID = (uint)wmoLocation.GroupId; + vignette.WMODoodadPlacementID = wmoLocation.UniqueId; + } + } + + public static void SendVignetteUpdate(VignetteData vignette, WorldObject owner) + { + if (!owner.IsInWorld) + return; + + VignetteUpdate vignetteUpdate = new(); + vignette.FillPacket(vignetteUpdate.Updated); + vignetteUpdate.Write(); + + var sender = (Player receiver) => + { + if (CanSee(receiver, vignette)) + receiver.SendPacket(vignetteUpdate); + }; + + MessageDistDeliverer notifier = new(owner, sender, owner.GetVisibilityRange()); + Cell.VisitWorldObjects(owner, notifier, owner.GetVisibilityRange()); + } + + public static void SendVignetteAdded(VignetteData vignette, WorldObject owner) + { + if (!owner.IsInWorld) + return; + + VignetteUpdate vignetteUpdate = new(); + vignette.FillPacket(vignetteUpdate.Added); + vignetteUpdate.Write(); + + var sender = (Player receiver) => + { + if (CanSee(receiver, vignette)) + receiver.SendPacket(vignetteUpdate); + }; + + MessageDistDeliverer notifier = new(owner, sender, owner.GetVisibilityRange()); + Cell.VisitWorldObjects(owner, notifier, owner.GetVisibilityRange()); + } + + public static VignetteData Create(VignetteRecord vignetteData, WorldObject owner) + { + VignetteData vignette = new(); + vignette.Guid = ObjectGuid.Create(HighGuid.Vignette, owner.GetMapId(), vignetteData.ID, owner.GetMap().GenerateLowGuid(HighGuid.Vignette)); + vignette.Object = owner.GetGUID(); + vignette.Position = owner.GetPosition(); + vignette.Data = vignetteData; + vignette.ZoneID = owner.GetZoneId(); // not updateable + UpdatePosition(vignette, owner); + + if (vignetteData.IsInfiniteAOI()) + owner.GetMap().AddInfiniteAOIVignette(vignette); + else + SendVignetteAdded(vignette, owner); + + return vignette; + } + + public static void Update(VignetteData vignette, WorldObject owner) + { + UpdatePosition(vignette, owner); + + if (vignette.Data.IsInfiniteAOI()) + vignette.NeedUpdate = true; + else + SendVignetteUpdate(vignette, owner); + } + + public static void Remove(VignetteData vignette, WorldObject owner) + { + if (vignette.Data.IsInfiniteAOI()) + owner.GetMap().RemoveInfiniteAOIVignette(vignette); + else + { + VignetteUpdate vignetteUpdate = new(); + vignetteUpdate.Removed.Add(vignette.Guid); + owner.SendMessageToSet(vignetteUpdate, true); + } + } + + public static bool CanSee(Player player, VignetteData vignette) + { + if (vignette.Data.GetFlags().HasFlag(VignetteFlags.ZoneInfiniteAOI)) + if (vignette.ZoneID != player.GetZoneId()) + return false; + + if (vignette.Data.VisibleTrackingQuestID != 0) + if (player.IsQuestRewarded(vignette.Data.VisibleTrackingQuestID)) + return false; + + var playerCondition = CliDB.PlayerConditionStorage.LookupByKey(vignette.Data.PlayerConditionID); + if (playerCondition != null && !ConditionManager.IsPlayerMeetingCondition(player, playerCondition)) + return false; + + return true; + } + } + + public class VignetteData + { + public ObjectGuid Guid; + public ObjectGuid Object; + public Position Position; + public VignetteRecord Data; + public uint ZoneID; + public uint WMOGroupID; + public uint WMODoodadPlacementID; + public bool NeedUpdate; + + public void FillPacket(VignetteDataSet dataSet) + { + dataSet.IDs.Add(Guid); + + VignetteDataPkt data = new(); + data.ObjGUID = Object; + data.Position = Position; + data.VignetteID = (int)Data.ID; + data.ZoneID = ZoneID; + data.WMOGroupID = WMOGroupID; + data.WMODoodadPlacementID = WMODoodadPlacementID; + + dataSet.Data.Add(data); + } + } +} diff --git a/Source/Game/Globals/ObjectManager.cs b/Source/Game/Globals/ObjectManager.cs index d7b1500f3..b4d192b62 100644 --- a/Source/Game/Globals/ObjectManager.cs +++ b/Source/Game/Globals/ObjectManager.cs @@ -2901,6 +2901,12 @@ namespace Game Log.outInfo(LogFilter.Sql, $"Creature (Entry: {cInfo.Entry}) has assigned gossip menu, but npcflag does not include UNIT_NPC_FLAG_GOSSIP."); else if (cInfo.GossipMenuIds.Empty() && cInfo.Npcflag.HasAnyFlag((uint)NPCFlags.Gossip)) Log.outInfo(LogFilter.Sql, $"Creature (Entry: {cInfo.Entry}) has npcflag UNIT_NPC_FLAG_GOSSIP, but gossip menu is unassigned."); + + if (cInfo.VignetteID != 0 && !CliDB.VignetteStorage.HasRecord(cInfo.VignetteID)) + { + Log.outInfo(LogFilter.Sql, $"Creature (Entry: {cInfo.Entry}) has non-existing Vignette {cInfo.VignetteID}, set to 0."); + cInfo.VignetteID = 0; + } } void CheckCreatureMovement(string table, ulong id, CreatureMovementData creatureMovement) { diff --git a/Source/Game/Maps/Map.cs b/Source/Game/Maps/Map.cs index d8ac60c5f..5fce2398f 100644 --- a/Source/Game/Maps/Map.cs +++ b/Source/Game/Maps/Map.cs @@ -32,6 +32,7 @@ namespace Game.Maps m_VisibilityNotifyPeriod = SharedConst.DefaultVisibilityNotifyPeriod; i_gridExpiry = expiry; m_terrain = Global.TerrainMgr.LoadTerrain(id); + _vignetteUpdateTimer = new(5200, 5200); for (uint x = 0; x < MapConst.MaxGrids; ++x) { @@ -398,6 +399,41 @@ namespace Game.Maps } } + + public void AddInfiniteAOIVignette(VignetteData vignette) + { + _infiniteAOIVignettes.Add(vignette); + + VignetteUpdate vignetteUpdate = new(); + vignette.FillPacket(vignetteUpdate.Added); + vignetteUpdate.Write(); + + foreach (var player in GetPlayers()) + if (Vignettes.CanSee(player, vignette)) + player.SendPacket(vignetteUpdate); + } + + public void RemoveInfiniteAOIVignette(VignetteData vignette) + { + if (!_infiniteAOIVignettes.Remove(vignette)) + return; + + VignetteUpdate vignetteUpdate = new(); + vignetteUpdate.Removed.Add(vignette.Guid); + vignetteUpdate.Write(); + + if (vignette.Data.GetFlags().HasFlag(VignetteFlags.ZoneInfiniteAOI)) + { + foreach (var player in GetPlayers()) + if (player.GetZoneId() == vignette.ZoneID) + player.SendPacket(vignetteUpdate); + } + else + SendToPlayers(vignetteUpdate); + } + + public List GetInfiniteAOIVignettes() { return _infiniteAOIVignettes; } + void InitializeObject(WorldObject obj) { if (!obj.IsTypeId(TypeId.Unit) || !obj.IsTypeId(TypeId.GameObject)) @@ -663,6 +699,24 @@ namespace Game.Maps transport.Update(diff); } + if (_vignetteUpdateTimer.Update((int)diff)) + { + foreach (VignetteData vignette in _infiniteAOIVignettes) + { + if (vignette.NeedUpdate) + { + VignetteUpdate vignetteUpdate = new(); + vignette.FillPacket(vignetteUpdate.Updated); + vignetteUpdate.Write(); + foreach (var player in GetPlayers()) + if (Vignettes.CanSee(player, vignette)) + player.SendPacket(vignetteUpdate); + + vignette.NeedUpdate = false; + } + } + } + SendObjectUpdates(); // Process necessary scripts @@ -4780,6 +4834,9 @@ namespace Game.Maps MultiPersonalPhaseTracker _multiPersonalPhaseTracker = new(); Dictionary _worldStateValues = new(); + + List _infiniteAOIVignettes = new(); + PeriodicTimer _vignetteUpdateTimer; #endregion } diff --git a/Source/Game/Networking/Packets/VignettePackets.cs b/Source/Game/Networking/Packets/VignettePackets.cs new file mode 100644 index 000000000..3fb9d5a6d --- /dev/null +++ b/Source/Game/Networking/Packets/VignettePackets.cs @@ -0,0 +1,69 @@ +// Copyright (c) CypherCore All rights reserved. +// Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. + +using Framework.Constants; +using Game.Entities; +using System.Collections.Generic; +using System.Numerics; + +namespace Game.Networking.Packets +{ + public class VignetteDataPkt + { + public ObjectGuid ObjGUID; + public Vector3 Position; + public int VignetteID; + public uint ZoneID; + public uint WMOGroupID; + public uint WMODoodadPlacementID; + + public void Write(WorldPacket data) + { + data.WriteVector3(Position); + data.WritePackedGuid(ObjGUID); + data.WriteInt32(VignetteID); + data.WriteUInt32(ZoneID); + data.WriteUInt32(WMOGroupID); + data.WriteUInt32(WMODoodadPlacementID); + } + } + + public class VignetteDataSet + { + public List IDs = new(); + public List Data = new(); + + public void Write(WorldPacket data) + { + data.WriteInt32(IDs.Count); + data.WriteInt32(Data.Count); + foreach (ObjectGuid id in IDs) + data.WritePackedGuid(id); + + foreach (VignetteDataPkt vignetteData in Data) + vignetteData.Write(data); + } + } + + class VignetteUpdate : ServerPacket + { + public VignetteDataSet Added = new(); + public VignetteDataSet Updated = new(); + public List Removed = new(); + public bool ForceUpdate; + public bool InFogOfWar; + + public VignetteUpdate() : base(ServerOpcodes.VignetteUpdate, ConnectionType.Instance) { } + + public override void Write() + { + _worldPacket.WriteBit(ForceUpdate); + _worldPacket.WriteBit(InFogOfWar); + _worldPacket.WriteInt32(Removed.Count); + Added.Write(_worldPacket); + Updated.Write(_worldPacket); + foreach (ObjectGuid removed in Removed) + _worldPacket.WritePackedGuid(removed); + } + } +} diff --git a/Source/Game/Spells/Auras/AuraEffect.cs b/Source/Game/Spells/Auras/AuraEffect.cs index 28ad7a326..7825b50d6 100644 --- a/Source/Game/Spells/Auras/AuraEffect.cs +++ b/Source/Game/Spells/Auras/AuraEffect.cs @@ -4981,6 +4981,15 @@ namespace Game.Spells target.ToPlayer().SendOnCancelExpectedVehicleRideAura(); } + [AuraEffectHandler(AuraType.SetVignette)] + void HandleSetVignette(AuraApplication aurApp, AuraEffectHandleModes mode, bool apply) + { + if (!mode.HasAnyFlag(AuraEffectHandleModes.Real)) + return; + + aurApp.GetTarget().SetVignette((uint)(apply ? GetMiscValue() : 0)); + } + [AuraEffectHandler(AuraType.PreventResurrection)] void HandlePreventResurrection(AuraApplication aurApp, AuraEffectHandleModes mode, bool apply) { diff --git a/sql/updates/hotfixes/master/2024_03_09_00_hotfixes.sql b/sql/updates/hotfixes/master/2024_03_09_00_hotfixes.sql new file mode 100644 index 000000000..c890f0ee9 --- /dev/null +++ b/sql/updates/hotfixes/master/2024_03_09_00_hotfixes.sql @@ -0,0 +1,42 @@ +-- +-- Table structure for table `vignette` +-- +DROP TABLE IF EXISTS `vignette`; +CREATE TABLE `vignette` ( + `ID` int unsigned NOT NULL DEFAULT '0', + `Name` text, + `PlayerConditionID` int unsigned NOT NULL DEFAULT '0', + `VisibleTrackingQuestID` int unsigned NOT NULL DEFAULT '0', + `QuestFeedbackEffectID` int unsigned NOT NULL DEFAULT '0', + `Flags` int NOT NULL DEFAULT '0', + `MaxHeight` float NOT NULL DEFAULT '0', + `MinHeight` float NOT NULL DEFAULT '0', + `VignetteType` tinyint NOT NULL DEFAULT '0', + `RewardQuestID` int NOT NULL DEFAULT '0', + `UiWidgetSetID` int NOT NULL DEFAULT '0', + `VerifiedBuild` int NOT NULL DEFAULT '0', + PRIMARY KEY (`ID`,`VerifiedBuild`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `vignette_locale` +-- +DROP TABLE IF EXISTS `vignette_locale`; +CREATE TABLE `vignette_locale` ( + `ID` int unsigned NOT NULL DEFAULT '0', + `locale` varchar(4) NOT NULL, + `Name_lang` text, + `VerifiedBuild` int NOT NULL DEFAULT '0', + PRIMARY KEY (`ID`,`locale`,`VerifiedBuild`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +/*!50500 PARTITION BY LIST COLUMNS(locale) +(PARTITION deDE VALUES IN ('deDE') ENGINE = InnoDB, + PARTITION esES VALUES IN ('esES') ENGINE = InnoDB, + PARTITION esMX VALUES IN ('esMX') ENGINE = InnoDB, + PARTITION frFR VALUES IN ('frFR') ENGINE = InnoDB, + PARTITION itIT VALUES IN ('itIT') ENGINE = InnoDB, + PARTITION koKR VALUES IN ('koKR') ENGINE = InnoDB, + PARTITION ptBR VALUES IN ('ptBR') ENGINE = InnoDB, + PARTITION ruRU VALUES IN ('ruRU') ENGINE = InnoDB, + PARTITION zhCN VALUES IN ('zhCN') ENGINE = InnoDB, + PARTITION zhTW VALUES IN ('zhTW') ENGINE = InnoDB) */;