using DiIiS_NA.Core.Logging; using DiIiS_NA.D3_GameServer.Core.Types.SNO; using DiIiS_NA.GameServer.Core.Types.Math; using DiIiS_NA.GameServer.Core.Types.SNO; using DiIiS_NA.GameServer.Core.Types.TagMap; using DiIiS_NA.GameServer.GSSystem.ActorSystem.Implementations; using DiIiS_NA.GameServer.GSSystem.ActorSystem.Implementations.Hirelings; using DiIiS_NA.GameServer.GSSystem.GameSystem; using DiIiS_NA.GameServer.GSSystem.GeneratorsSystem; using DiIiS_NA.GameServer.GSSystem.ItemsSystem; using DiIiS_NA.GameServer.GSSystem.ObjectsSystem; using DiIiS_NA.GameServer.GSSystem.PlayerSystem; using DiIiS_NA.GameServer.GSSystem.PowerSystem; using DiIiS_NA.GameServer.MessageSystem; using DiIiS_NA.GameServer.MessageSystem.Message.Definitions.ACD; using DiIiS_NA.GameServer.MessageSystem.Message.Definitions.Animation; using DiIiS_NA.GameServer.MessageSystem.Message.Definitions.Base; using DiIiS_NA.GameServer.MessageSystem.Message.Definitions.Effect; using DiIiS_NA.GameServer.MessageSystem.Message.Definitions.Misc; using DiIiS_NA.GameServer.MessageSystem.Message.Definitions.World; using DiIiS_NA.GameServer.MessageSystem.Message.Fields; using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using DiIiS_NA.Core.MPQ; using DiIiS_NA.Core.MPQ.FileFormats; using DiIiS_NA.D3_GameServer.GSSystem.GameSystem; using DiIiS_NA.LoginServer.Battle; using Circle = DiIiS_NA.GameServer.Core.Types.Misc.Circle; using Player = DiIiS_NA.GameServer.GSSystem.PlayerSystem.Player; using Scene = DiIiS_NA.GameServer.GSSystem.MapSystem.Scene; using World = DiIiS_NA.GameServer.GSSystem.MapSystem.World; namespace DiIiS_NA.GameServer.GSSystem.ActorSystem { public abstract class Actor : WorldObject { private static readonly Logger Logger = LogManager.CreateLogger(); /// /// ActorSNO. /// public SNOHandle ActorSNO { get; private set; } public ActorSno SNO => (ActorSno)ActorSNO.Id; public string Name => ActorSNO.Name; /// /// Gets or sets the sno of the actor used to identify the actor to the player /// This is usually the same as actorSNO except for actors that have a GBHandle /// There are few exceptions though like the Inn_Zombies that have both. /// Used by ACDEnterKnown to name the actor. /// public ActorSno NameSNO { get; set; } public bool Disable = false; public bool Spawner = false; /// /// The actor type. /// public abstract ActorType ActorType { get; } public object _payloadLock = new(); /// /// Current scene for the actor. /// public virtual Scene CurrentScene { get { return World.QuadTree.Query(Bounds).FirstOrDefault(); } } /// /// Returns true if actor is already spawned in the world. /// public bool Spawned { get; private set; } public int GroupId = 0; public int NumberInWorld = 0; /// /// Default lenght value for region based queries. /// public const int DefaultQueryProximityLenght = 240; /// /// Default lenght value for range based queries. /// public int DefaultQueryProximityRadius = 100; public float LastSecondCasts = 0; /// /// PRTransform for the actor. /// public virtual PRTransform Transform { get { return new PRTransform { Quaternion = new Quaternion { W = RotationW, Vector3D = RotationAxis }, Vector3D = Position }; } } /// /// Replaces the actor's rotation with one that rotates along the Z-axis by the specified "facing" angle. /// /// The angle in radians. public void SetFacingRotation(float facingAngle) { if (!Spawner) { Quaternion q = Quaternion.FacingRotation(facingAngle); RotationW = q.W; RotationAxis = q.Vector3D; } } /// /// Tags read from MPQ's for the actor. /// public TagMap Tags { get; private set; } /// /// Attribute map. /// public GameAttributeMap Attributes { get; } /// /// Affix list. /// public List AffixList { get; set; } /// /// GBHandle. /// public GBHandle GBHandle { get; private set; } /// /// Collision flags. /// public int CollFlags { get; set; } /// /// Check for summoned monsters /// public bool HasLoot { get; set; } public bool Dead = false; public bool Alive => !Dead; /// /// Gets whether the actor is visible by questrange, privately set on quest progress /// public bool Visible { get; private set; } /// /// The QuestRange specifies the visibility of an actor, depending on quest progress /// public DiIiS_NA.Core.MPQ.FileFormats.QuestRange _questRange; protected DiIiS_NA.Core.MPQ.FileFormats.ConversationList ConversationList; public Vector3D CheckPointPosition { get; set; } public Vector3D CurrentDestination { get; set; } /// /// Returns true if actor has world location. /// TODO: I belive this belongs to WorldObject.cs /raist. /// public virtual bool HasWorldLocation { get { return true; } } /// /// The info set for actor. (erekose) /// public DiIiS_NA.Core.MPQ.FileFormats.ActorData ActorData => (DiIiS_NA.Core.MPQ.FileFormats.ActorData)MPQStorage.Data.Assets[SNOGroup.Actor][(int)SNO].Data; /// /// The animation set for actor. /// public DiIiS_NA.Core.MPQ.FileFormats.AnimSet AnimationSet { get { if (ActorData.AnimSetSNO != -1) return (DiIiS_NA.Core.MPQ.FileFormats.AnimSet)MPQStorage.Data.Assets[SNOGroup.AnimSet][ActorData.AnimSetSNO].Data; else return null; } } public float WalkSpeed = 0.108f; public int Field2 = 0x00000000; public int Field7 = -1; public virtual int Quality { get; set; } public byte Field10 = 0x00; public int? Field11 = null; public int? MarkerSetSNO { get; private set; } public bool Hidden = false; // TODO: check if the following is correct: @iamdroppy // { // get => Attributes[GameAttribute.Hidden]; // set => Attributes[GameAttribute.Hidden] = value; // } public bool AdjustPosition = true; public int OriginalLevelArea = -1; public int? MarkerSetIndex { get; private set; } private int _snoTriggeredConversation = -1; /// /// Creates a new actor. /// /// The world that initially belongs to. /// SNOId of the actor. /// TagMapEntry dictionary read for the actor from MPQ's.. /// Is Marker protected Actor(World world, ActorSno sno, TagMap tags, bool isMarker = false) : base(world, world.IsPvP ? World.NewActorPvPID : world.Game.NewActorGameId) { Tags = tags; Attributes = new GameAttributeMap(this); if (isMarker) return; AffixList = new List(); // if (tags != null && tags.ContainsKey(MarkerKeys.OnActorSpawnedScript) && tags[MarkerKeys.OnActorSpawnedScript].Id == 178440) // AnimationSet = (AnimSet)MPQStorage.Data.Assets[SNOGroup.AnimSet][11849].Data; //OminNPC_Male (Wounded) //else // if (this.ActorData.AnimSetSNO != -1) // this.AnimationSet = (Mooege.Common.MPQ.FileFormats.AnimSet)Mooege.Common.MPQ.MPQStorage.Data.Assets[SNOGroup.AnimSet][this.ActorData.AnimSetSNO].Data; ActorSNO = new SNOHandle(SNOGroup.Actor, (int)sno); NameSNO = sno; //Logger.Info("Loaded actor {0}, id {1}, type {2}", this.ActorSNO.Name, this.DynamicID, this.ActorData.Type); //Quality = 0; - removed, 0 is default and you can't change the quality HasLoot = true; if (ActorData.TagMap.ContainsKey(ActorKeys.TeamID)) { Attributes[GameAttribute.TeamID] = ActorData.TagMap[ActorKeys.TeamID]; //Logger.Debug("Actor {0} has TeamID {1}", this.ActorSNO.Name, this.Attributes[GameAttribute.TeamID]); } Spawned = false; Size = new Size(1, 1); GBHandle = new GBHandle { Type = -1, GBID = -1 }; // Seems to be the default. /komiga CollFlags = ActorData.ActorCollisionData.CollFlags.I3; ReadTags(); // Listen for quest progress if the actor has a QuestRange attached to it //foreach (var quest in World.Game.QuestManager.Quests) if (_questRange != null) World.Game.QuestManager.OnQuestProgress += quest_OnQuestProgress; UpdateQuestRangeVisibility(); } /// /// Creates a new actor. /// /// The world that initially belongs to. /// SNOId of the actor. protected Actor(World world, ActorSno sno) : this(world, sno, null) { } protected virtual void quest_OnQuestProgress() // erekose changed from protected to public { //Logger.Debug(" (quest_onQuestProgress) has been called for actor {0} -> lauching UpdateQuestRangeVisibility", NameSNOId); try { UpdateQuestRangeVisibility(); } catch (Exception e) { Logger.WarnException(e, "quest_OnQuestProgress exception: "); } } private bool _isDestroyed = false; /// /// Unregister from quest events when object is destroyed /// public override void Destroy() { if (_isDestroyed) return; if (SNO == ActorSno._p6_necro_corpse_flesh) if (World != null) foreach (var plr in World.Game.Players.Values) if (plr.SkillSet.HasPassive(208594) && DiIiS_NA.Core.Helpers.Math.RandomHelper.Next(0,100) > 45) World.SpawnHealthGlobe(this, plr, Position); if (_questRange != null) if (World == null) Logger.Debug("World is null? {0}", GetType()); else if (World.Game == null) Logger.Debug("Game is null? {0}", GetType()); else if (World.Game.QuestManager != null) //foreach (var quest in World.Game.QuestManager) World.Game.QuestManager.OnQuestProgress -= quest_OnQuestProgress; base.Destroy(); } #region enter-world, change-world, teleport helpers public virtual void EnterWorld(Vector3D position) { // var quest = MPQStorage.Data.Assets[SNOGroup.Quest][74128]; if (World != null) { int count = World.GetActorsBySNO(SNO).Count; if (count > 0) NumberInWorld = count; } if (Spawned) return; Position = position; CheckPointPosition = position; CurrentDestination = position; if (World != null) // if actor got into a new world. { World.Enter(this); // let him enter first. if ((this is Monster && AdjustPosition) || this is Item) if (!World.CheckLocationForFlag(position, DiIiS_NA.Core.MPQ.FileFormats.Scene.NavCellFlags.AllowWalk)) //if actor has spawned in unwalkable zone Unstuck(); } } public virtual void BeforeChangeWorld() {} public virtual void AfterChangeWorld() {} public void ChangeWorld(World world, Vector3D position) { if (World == world) return; var prevWorld = World; //uint prevWorldId = prevWorld.GlobalID; BeforeChangeWorld(); World?.Leave(this); // make him leave it first. World = world; Position = position; if (world.IsPvP) { //this.GlobalIDOverride = World.NewActorPvPID; Attributes[GameAttribute.Team_Override] = 10; } else { //this.GlobalIDOverride = 0; Attributes[GameAttribute.Team_Override] = -1; } World?.Enter(this); // let him enter first. CheckPointPosition = position; if (this is Player) world.BroadcastIfRevealed(ACDWorldPositionMessage, this); AfterChangeWorld(); if (this is Player plr) { Hireling hireling = plr.ActiveHireling; if (hireling != null) { hireling.Brain.DeActivate(); hireling.ChangeWorld(world, position); hireling.Brain.Activate(); plr.ActiveHireling = hireling; } Hireling questHireling = plr.QuestHireling; if (questHireling != null) { questHireling.Brain.DeActivate(); questHireling.ChangeWorld(world, position); questHireling.Brain.Activate(); plr.QuestHireling = questHireling; } foreach (var fol in plr.Followers.Keys.ToList()) { var minion = prevWorld.GetActorByGlobalId(fol); if (minion is Minion m) { m.Brain.DeActivate(); plr.Followers.Remove(fol); minion.ChangeWorld(world, position); plr.Followers.Add(minion.GlobalID, minion.SNO); m.Brain.Activate(); } } //(this as Player).InGameClient.SendMessage(new WorldDeletedMessage() { WorldID = prevWorld.GlobalID }); } } public void ChangeWorld(World world, StartingPoint startingPoint) { if (startingPoint != null) { RotationAxis = startingPoint.RotationAxis; RotationW = startingPoint.RotationW; ChangeWorld(world, startingPoint.Position); } } public void Teleport(Vector3D position) { Position = position; if (this is Player player) { player.BetweenWorlds = true; player.InGameClient.SendMessage(new ACDTranslateSyncMessage() { ActorId = DynamicID(this as Player), Position = Position }); } else { World.BroadcastIfRevealed(plr => new ACDTranslateSyncMessage() { ActorId = DynamicID(plr), Position = Position }, this); } OnTeleport(); World.BroadcastIfRevealed(ACDWorldPositionMessage, this); if (this is Player plr) { var hireling = plr.ActiveHireling; if (hireling != null) { (hireling as Hireling).Brain.DeActivate(); hireling.Position = position; (hireling as Hireling).Brain.Activate(); } var questHireling = plr.QuestHireling; if (questHireling != null) { questHireling.Brain.DeActivate(); questHireling.Position = position; questHireling.Brain.Activate(); } foreach (var fol in plr.Followers) { if (World.GetActorByGlobalId(fol.Key) is Minion minion) { minion.Brain.DeActivate(); World.GetActorByGlobalId(fol.Key).Position = position; minion.Brain.Activate(); } } plr.RevealActorsToPlayer(); plr.ReRevealPlayersToPlayer(); Attributes[GameAttribute.Looping_Animation_Start_Time] = -1; Attributes[GameAttribute.Looping_Animation_End_Time] = -1; Attributes.BroadcastChangedIfRevealed(); //Refresh Inventory plr.Inventory.RefreshInventoryToClient(); } } #endregion #region Movement/Translation public void TranslateFacing(Vector3D target, bool immediately = false) { float facingAngle = Movement.MovementHelpers.GetFacingAngle(this, target); SetFacingRotation(facingAngle); if (World == null) return; if (!Spawner) World.BroadcastIfRevealed(plr => new ACDTranslateFacingMessage { ActorId = DynamicID(plr), Angle = facingAngle, TurnImmediately = immediately }, this); } public void Unstuck() { if (World == null) return; for (int i = 1; i <= 8; i++) { int radius = (int)Math.Pow(2, i); for (int a = 0; a < 8; a++) { float angle = (float)(0.125f * a * (Math.PI * 2)); Vector3D correctPosition = Position + new Vector3D((float)Math.Cos(angle) * radius, (float)Math.Sin(angle) * radius, 0); if (World.CheckLocationForFlag(correctPosition, DiIiS_NA.Core.MPQ.FileFormats.Scene.NavCellFlags.AllowWalk)) { Position = correctPosition; World.BroadcastIfRevealed(ACDWorldPositionMessage, this); return; } } } } #endregion #region Effects public void PlayEffectGroup(int effectGroupSNO) { #if DEBUG if (Dicts.DictSNOEffectGroup.ContainsValue(effectGroupSNO)) { var effectGroupKey = Dicts.DictSNOEffectGroup.FirstOrDefault(x => x.Value == effectGroupSNO).Key; Logger.MethodTrace($"{effectGroupSNO} on {GetType().Name}. Type: $[green]${effectGroupKey}$[/]$"); } else { Logger.MethodTrace($"{effectGroupSNO} on {GetType().Name}. Type: $[red]$Unknown$[/]$"); } #endif PlayEffect(Effect.PlayEffectGroup, effectGroupSNO); } public void PlayEffectGroup(int effectGroupSNO, Actor target) { if (target == null || World == null) return; World.BroadcastIfRevealed(plr => new EffectGroupACDToACDMessage { ActorID = DynamicID(plr), TargetID = target.DynamicID(plr), EffectSNOId = effectGroupSNO }, this); } public void PlayHitEffect(int hitEffect, Actor hitDealer) { if (hitDealer.World == null || World == null) return; World.BroadcastIfRevealed(plr => new PlayHitEffectMessage { ActorID = DynamicID(plr), HitDealer = hitDealer.IsRevealedToPlayer(plr) ? hitDealer.DynamicID(plr) : DynamicID(plr), DamageType = hitEffect, CriticalDamage = false }, this); } public void PlayEffect(Effect effect, int? param = null, bool broadcast = true) { if (World == null) return; if (broadcast) World.BroadcastIfRevealed(plr => new PlayEffectMessage { ActorId = DynamicID(plr), Effect = effect, OptionalParameter = param, PlayerId = (this as Player)?.PlayerIndex }, this); else { (this as Player)?.InGameClient.SendMessage(new PlayEffectMessage { ActorId = DynamicID(this as Player), Effect = effect, OptionalParameter = param, PlayerId = (this as Player)?.PlayerIndex }); } } public void AddRopeEffect(int ropeSNO, Actor target) { if (target?.World == null || World == null) return; World.BroadcastIfRevealed(plr => new RopeEffectMessageACDToACD { RopeSNO = ropeSNO, StartSourceActorId = (int)DynamicID(plr), Field2 = 4, DestinationActorId = (int)(target.IsRevealedToPlayer(plr) ? target.DynamicID(plr) : DynamicID(plr)), Field4 = 5, Field5 = true }, this); } public void AddRopeEffect(int ropeSNO, Vector3D target) { World?.BroadcastIfRevealed(plr => new RopeEffectMessageACDToPlace { RopeSNO = ropeSNO, StartSourceActorId = (int)DynamicID(plr), Field2 = 4, EndPosition = new WorldPlace { Position = target, WorldID = World.GlobalID }, Field4 = true }, this); } public void AddComplexEffect(int effectGroupSNO, Actor target) { if (target == null || target.World == null || World == null) return; World.BroadcastIfRevealed(plr => new ComplexEffectAddMessage { EffectId = World.LastCEId++, //last ids Type = 1, EffectSNO = effectGroupSNO, SourceActorId = (int)DynamicID(plr), TargetActorId = (int)(target.IsRevealedToPlayer(plr) ? target.DynamicID(plr) : DynamicID(plr)), Param1 = 0, Param2 = 0, IgroneOwnerAlpha = true }, target); } public void SetIdleAnimation(AnimationSno animationSNO) { if (this.World == null || animationSNO == AnimationSno._NONE) return; World.BroadcastIfRevealed(plr => new SetIdleAnimationMessage { ActorID = this.DynamicID(plr), AnimationSNO = (int)animationSNO }, this); } public void PlayAnimationAsSpawn(AnimationSno animationSNO) { if (this is Monster) { // unused //var Anim = (DiIiS_NA.Core.MPQ.FileFormats.Anim)DiIiS_NA.Core.MPQ.MPQStorage.Data.Assets[SNOGroup.Anim][animationSNO].Data; World.BroadcastIfRevealed(plr => new PlayAnimationMessage { ActorID = DynamicID(plr), AnimReason = 9, UnitAniimStartTime = 0, tAnim = new PlayAnimationMessageSpec[] { new PlayAnimationMessageSpec { Duration = -2, AnimationSNO = (int)animationSNO, PermutationIndex = 0x0, AnimationTag = 0, Speed = 1.0f, } } }, this); } } public void PlayAnimation(int animationType, AnimationSno animationSNO, float speed = 1.0f, int? ticksToPlay = null, int type2 = 0) { if (animationSNO == AnimationSno._NONE) { Logger.Warn($"PlayAnimation: {(int)animationSNO} is not a valid animation"); return; } if (this.World == null) return; World.BroadcastIfRevealed(plr => new PlayAnimationMessage { ActorID = DynamicID(plr), AnimReason = animationType, UnitAniimStartTime = type2, tAnim = new PlayAnimationMessageSpec[] { new() { Duration = ticksToPlay ?? -2, // -2 = play animation once through AnimationSNO = (int)animationSNO, PermutationIndex = 0x0, // TODO: implement variations? AnimationTag = 0, Speed = speed, } } }, this); } public void PlayActionAnimation(AnimationSno animationSNO, float speed = 1.0f, int? ticksToPlay = null) { PlayAnimation(3, animationSNO, speed, ticksToPlay); } public void NotifyConversation(int status) { //0 - turn off, 1 - yellow "!", 2 - yellow "?", 3 - "*", 4 - bubble, 5 - silver "!", 6 - none (spams errors) Attributes[GameAttribute.Conversation_Icon, 0] = status + 1; //this.Attributes[GameAttribute.MinimapIconOverride] = (status > 0) ? 120356 : -1; Attributes.BroadcastChangedIfRevealed(); } public void AddPercentHP(int percentage, bool GuidingLight = false) { float quantity = percentage * Attributes[GameAttribute.Hitpoints_Max_Total] / 100; AddHP(quantity, GuidingLight); } public virtual void AddHP(float quantity, bool guidingLight = false) { if (quantity > 0) { if (Attributes[GameAttribute.Hitpoints_Cur] < Attributes[GameAttribute.Hitpoints_Max_Total]) { Attributes[GameAttribute.Hitpoints_Cur] = Math.Min( Attributes[GameAttribute.Hitpoints_Cur] + quantity, Attributes[GameAttribute.Hitpoints_Max_Total]); Attributes.BroadcastChangedIfRevealed(); } } else { Attributes[GameAttribute.Hitpoints_Cur] = Math.Max( Attributes[GameAttribute.Hitpoints_Cur] + quantity, 0); Attributes.BroadcastChangedIfRevealed(); } } #endregion #region reveal & unreveal handling public void UpdateQuestRangeVisibility() { if (World != null) if (!Hidden) { if (_questRange != null) Visible = (World.Game.CurrentAct == 3000 && !(this is Monster)) || World.Game.QuestManager.IsInQuestRange(_questRange); else Visible = true; } else { Visible = false; foreach (var plr in GetPlayersInRange(100f)) Unreveal(plr); } else Visible = false; } public void SetUsable(bool activated) { Attributes[GameAttribute.Team_Override] = activated ? -1 : 2; Attributes[GameAttribute.Untargetable] = !activated; Attributes[GameAttribute.NPC_Is_Operatable] = activated; Attributes[GameAttribute.Operatable] = activated; Attributes[GameAttribute.Operatable_Story_Gizmo] = activated; Attributes[GameAttribute.Disabled] = !activated; Attributes[GameAttribute.Immunity] = !activated; Attributes.BroadcastChangedIfRevealed(); } public void SetVisible(bool visibility) { Visible = visibility; } /// /// Returns true if the actor is revealed to player. /// /// The player. /// public bool IsRevealedToPlayer(Player player) { return player.RevealedObjects.ContainsKey(GlobalID); } public ACDEnterKnownMessage ACDEnterKnown(Player plr) { return new ACDEnterKnownMessage { ActorID = DynamicID(plr), ActorSNOId = (int)SNO, Flags = Field2, LocationType = HasWorldLocation ? 0 : 1, WorldLocation = HasWorldLocation ? WorldLocationMessage() : null, InventoryLocation = HasWorldLocation ? null : InventoryLocationMessage(plr), GBHandle = GBHandle, snoGroup = Field7, snoHandle = (int)NameSNO, Quality = Quality, LookLinkIndex = Field10, snoAmbientOcclusionOverrideTex = null, MarkerSetSNO = null, MarkerSetIndex = null, EnterKnownLookOverrides = null }; } /// /// Reveals an actor to a player. /// /// true if the actor was revealed or false if the actor was already revealed. public override bool Reveal(Player player) { lock (player.RevealedObjects) { if (Hidden || Dead || !Visible || World == null) return false; var mysticHiddenWorlds = new[] { WorldSno.trdun_crypt_falsepassage_01, WorldSno.trdun_crypt_falsepassage_02, WorldSno.trdun_crypt_fields_flooded_memories_level01, WorldSno.trdun_crypt_fields_flooded_memories_level02, WorldSno.trdun_crypt_skeletonkingcrown_00, WorldSno.trdun_crypt_skeletonkingcrown_01, WorldSno.trdun_crypt_skeletonkingcrown_02, }; //Leave Miriam in Crypt if (SNO == ActorSno._pt_mystic_novendor_nonglobalfollower && mysticHiddenWorlds.Contains(World.SNO)) return false; //Destroy Bonewall and Jondar if Exit_S on Second Level of Cathedral if (World.SNO == WorldSno.a1trdun_level04 && SNO is ActorSno._trdun_cath_bonewall_a_door or ActorSno._adventurer_d_templarintrounique) return false; if (SNO.IsUberWorldActor() && !World.SNO.IsUberWorld()) return false; if (SNO.IsAdventureModeActor() && World.Game.CurrentAct != 3000) return false; if (SNO == ActorSno._x1_adria_boss_scriptedsequenceonly) return false; if (player.RevealedObjects.ContainsKey(GlobalID)) return false; // already revealed if (player.World == null) return false; if (SNO == ActorSno._zombieskinny_custom_a && World.SNO == WorldSno.trout_town && CurrentScene.SceneSNO.Id == 33348 && Position.X < 2896) return false; if (!(this is Item) && player.World.GlobalID != World.GlobalID) return false; if (!(this is Item) && GetScenesInRange().Count > 0 && !GetScenesInRange().OrderBy(scene => PowerMath.Distance2D(scene.Position, Position)).First().IsRevealedToPlayer(player)) return false; uint objId = player.NewDynamicID(GlobalID, this is Player thisPlayer && (!thisPlayer.IsInPvPWorld || this == player) ? thisPlayer.PlayerIndex : -1); player.RevealedObjects.Add(GlobalID, objId); var gbIdBank = new int[AffixList.Count]; int i = 0; foreach (var affix in AffixList) { gbIdBank[i] = affix.AffixGbid; i++; } /* player.InGameClient.SendMessage(new PreloadACDDataMessage(Opcodes.PreloadAddACDMessage) { ActorID = this.DynamicID(player), SNOActor = this.ActorSNO.Id, eWeaponClass = 0, gbidMonsterAffixes = new int[0] //gbidbank }); //*/ var msg = ACDEnterKnown(player); // normaly when we send acdenterknown for players own actor it's set to 0x09. But while sending the acdenterknown for another player's actor we should set it to 0x01. /raist if (this is Player) { msg.Flags = this == player ? 0x09 : 0x01; } player.InGameClient.SendMessage(msg); // Collision Flags if (this is not Projectile && this is not Item) { player.InGameClient.SendMessage(new ACDCollFlagsMessage { ActorID = objId, CollFlags = CollFlags }); } // Send Attributes Attributes.SendMessage(player.InGameClient); if (this is Monster) { Attributes[GameAttribute.Hitpoints_Cur] += 0.001f; Attributes.BroadcastChangedIfRevealed(); } // This is always sent even though it doesn't identify the actor. /komiga player.InGameClient.SendMessage(new PrefetchMessage { Name = ActorSNO }); // Reveal actor (creates actor and makes it visible to the player) if (this is Player || this is NPC || this is Goblin) player.InGameClient.SendMessage(new ACDCreateActorMessage(objId)); TrickleMessage trickle = new TrickleMessage() { ActorId = DynamicID(player), ActorSNO = (int)SNO, WorldLocation = new WorldPlace() { WorldID = World.GlobalID, Position = Position }, HealthPercent = 1f, }; if (this is Player playerTrickle) trickle.PlayerIndex = playerTrickle.PlayerIndex; player.InGameClient.SendMessage(trickle); // Actor group player.InGameClient.SendMessage(new ACDGroupMessage { ActorID = objId, Group1Hash = 0, Group2Hash = 0, }); #region Special cases switch (World.SNO) { // set idle animation for zombies in tristram - ZHRAAT case WorldSno.trout_town: { if (Tags != null) if (Tags.ContainsKey(MarkerKeys.Group1Hash)) if (Tags[MarkerKeys.Group1Hash] == -1248096796) PlayActionAnimation(AnimationSno.zombie_male_skinny_eating); break; } // set idle animation for workers case WorldSno.trout_tristram_inn when SNO == ActorSno._omninpc_tristram_male_a: PlayActionAnimation(AnimationSno.omninpc_male_hth_injured); break; default: { if (SNO == ActorSno._leah) player.InGameClient.SendMessage(new MessageSystem.Message.Definitions.Inventory.VisualInventoryMessage() { ActorID = DynamicID(player), EquipmentList = new VisualEquipment() { Equipment = new VisualItem[] { new() { GbId = -1, DyeType = 0, ItemEffectType = 0, EffectLevel = -1, }, new() { GbId = -1, DyeType = 0, ItemEffectType = 0, EffectLevel = -1, }, new() { GbId = -1, DyeType = 0, ItemEffectType = 0, EffectLevel = -1, }, new() { GbId = -1, DyeType = 0, ItemEffectType = 0, EffectLevel = -1, }, new() { GbId = unchecked((int)-2091504072), DyeType = 0, ItemEffectType = 0, EffectLevel = -1, }, new() { GbId = -1,//0x6C3B0389, DyeType = 0, ItemEffectType = 0, EffectLevel = -1, }, new() { GbId = -1, DyeType = 0, ItemEffectType = 0, EffectLevel = -1, }, new() { GbId = -1, DyeType = 0, ItemEffectType = 0, EffectLevel = -1, }, } } }); break; } } #endregion // if (this is NPC || this is InteractiveNPC) // { // //.Contains(AnimationSetKeys.Idle) // //if (this.AnimationSet.Animations.ContainsKey(AnimationSetKeys.Idle.ID)) // // this.SetIdleAnimation(this.AnimationSet.TagMapAnimDefault[AnimationSetKeys.Idle]); // //this.PlayAnimation(0, this.AnimationSet.TagMapAnimDefault[AnimationSetKeys.Idle]); // } //Logger.Trace("Reveal actor [{2}]{0} as {1}", this.GlobalID, objId, this.ActorSNO.Name); return true; } } /// /// Unreveals an actor from a player. /// /// true if the actor was unrevealed or false if the actor wasn't already revealed. public override bool Unreveal(Player player) { lock (player.RevealedObjects) { if (!player.RevealedObjects.ContainsKey(GlobalID)) return false; // not revealed yet if (!(this is Item) && player.World.GlobalID != World.GlobalID) return false; //PreloadRemoveACDMessage var gbidbank = new int[AffixList.Count]; int i = 0; foreach(var affix in AffixList) { gbidbank[i] = affix.AffixGbid; i++; } if (this is Player) player.InGameClient.SendMessage(new ANNDataMessage(Opcodes.InventoryCreateMessage) { ActorID = DynamicID(player), }); if (this is Minion) { uint DynID = 0; player.RevealedObjects.TryGetValue(GlobalID, out DynID); if (DynID != 0) { player.InGameClient.SendMessage(new MessageSystem.Message.Definitions.Pet.PetDetachMessage() { PetId = DynID, }); } } /* if (this is Monster) player.InGameClient.SendMessage(new RemoveRagdollMessage() { Field0 = this.DynamicID(player), Field1 = (this as Monster).Monster.Id, }); //*/ player.InGameClient.SendMessage(new ACDDestroyActorMessage(DynamicID(player))); //Logger.Trace("Unreveal actor {0} as {1}", this.GlobalID, this.DynamicID(player)); player.RevealedObjects.Remove(GlobalID); //if (!(this is Item) && this.Dead && this.World.Players.Values.Where(p => this.IsRevealedToPlayer(p)).Count() == 0) //this.Destroy(); return true; } } #endregion #region proximity-based query helpers #region circurlar region queries public List GetPlayersInRange(float? radius = null) { radius ??= DefaultQueryProximityRadius; return GetObjectsInRange(radius); } public List GetItemsInRange(float? radius = null) { radius ??= DefaultQueryProximityRadius; return GetObjectsInRange(radius); } public List GetMonstersInRange(float? radius = null) { radius ??= DefaultQueryProximityRadius; return GetObjectsInRange(radius); } public List GetActorsInRange(float? radius = null) { radius ??= DefaultQueryProximityRadius; if (World == null || Position == null) return new List(); return GetObjectsInRange(radius); } public List GetActorsInRange(Vector3D TPosition, float? radius = null) { radius ??= DefaultQueryProximityRadius; return GetObjectsInRange(TPosition, radius); } public List GetObjectsInRange(Vector3D TPosition, float? radius = null) where T : WorldObject { var proximityCircle = new Circle(TPosition.X, TPosition.Y, radius ?? DefaultQueryProximityRadius); return World.QuadTree.Query(proximityCircle); } public List GetActorsInRange(float? radius = null) where T : Actor { radius ??= DefaultQueryProximityRadius; return GetObjectsInRange(radius); } public List GetScenesInRange(float? radius = null) { radius ??= DefaultQueryProximityRadius; return GetObjectsInRange(radius); } public List GetObjectsInRange(float? radius = null) { radius ??= DefaultQueryProximityRadius; return GetObjectsInRange(radius); } public List GetObjectsInRange(float? radius = null, bool includeHierarchy = false) where T : WorldObject { if (World == null || Position == null) return new List(); radius ??= DefaultQueryProximityRadius; var proximityCircle = new Circle(Position.X, Position.Y, radius.Value); return World.QuadTree.Query(proximityCircle, includeHierarchy); } #endregion #region rectangluar region queries public List GetPlayersInRegion(int lenght = DefaultQueryProximityLenght) => GetObjectsInRegion(lenght); public List GetItemsInRegion(int lenght = DefaultQueryProximityLenght) => GetObjectsInRegion(lenght); public List GetMonstersInRegion(int lenght = DefaultQueryProximityLenght) => GetObjectsInRegion(lenght); public List GetActorsInRegion(int lenght = DefaultQueryProximityLenght) => GetObjectsInRegion(lenght); public List GetActorsInRegion(int lenght = DefaultQueryProximityLenght) where T : Actor => GetObjectsInRegion(lenght); public List GetScenesInRegion(int lenght = DefaultQueryProximityLenght) => GetObjectsInRegion(lenght); public List GetObjectsInRegion(int lenght = DefaultQueryProximityLenght) => GetObjectsInRegion(lenght); public List GetObjectsInRegion(int lenght = DefaultQueryProximityLenght) where T : WorldObject { // ReSharper disable PossibleLossOfFraction var proximityRectangle = new RectangleF(Position.X - lenght / 2, Position.Y - lenght / 2, lenght, lenght); // ReSharper enable PossibleLossOfFraction return World.QuadTree.Query(proximityRectangle); } #endregion #endregion #region events public virtual void OnEnter(World world) { } public virtual void OnLeave(World world) { } public void OnActorMove(Actor actor, Vector3D prevPosition) { // TODO: Unreveal from players that are now outside the actor's range. /komiga } public virtual void OnTargeted(Player player, TargetMessage message) { } public virtual void OnTeleport() { } /// /// Called when a player moves close to the actor /// public virtual void OnPlayerApproaching(Player player) { } #endregion #region cooked messages public virtual InventoryLocationMessageData InventoryLocationMessage(Player plr) { // Only used in Item; stubbed here to prevent an overrun in some cases. /komiga return new InventoryLocationMessageData { OwnerID = 0, EquipmentSlot = 0, InventoryLocation = new Vector2D() }; } public virtual ACDWorldPositionMessage ACDWorldPositionMessage(Player plr) { return new ACDWorldPositionMessage { ActorID = DynamicID(plr), WorldLocation = WorldLocationMessage() }; } public virtual ACDInventoryPositionMessage ACDInventoryPositionMessage(Player plr) { return new ACDInventoryPositionMessage() { ItemId = DynamicID(plr), InventoryLocation = InventoryLocationMessage(plr), LocType = 1 // TODO: find out what this is and why it must be 1...is it an enum? }; } public virtual WorldLocationMessageData WorldLocationMessage() { return new WorldLocationMessageData { Scale = Scale, Transform = Transform, WorldID = World.GlobalID }; } #endregion #region tag-readers /// /// Reads known tags from TagMapEntry and set the proper values. /// protected virtual void ReadTags() { if (Tags == null) return; // load scale from actor data and override it with marker tags if one is set Scale = ActorData.TagMap.ContainsKey(ActorKeys.Scale) ? ActorData.TagMap[ActorKeys.Scale] : 1; Scale = Tags.ContainsKey(MarkerKeys.Scale) ? Tags[MarkerKeys.Scale] : Scale; if (Tags.ContainsKey(MarkerKeys.QuestRange)) { int snoQuestRange = Tags[MarkerKeys.QuestRange].Id; if (MPQStorage.Data.Assets[SNOGroup.QuestRange].ContainsKey(snoQuestRange)) _questRange = MPQStorage.Data.Assets[SNOGroup.QuestRange][snoQuestRange].Data as DiIiS_NA.Core.MPQ.FileFormats.QuestRange; else Logger.Debug("Actor {0} GlobalID {1} is tagged with unknown QuestRange {2}", NameSNO, GlobalID, snoQuestRange); } if (Tags.ContainsKey(MarkerKeys.ConversationList) && WorldGenerator.DefaultConversationLists.ContainsKey((int)SNO)) { int snoConversationList = WorldGenerator.DefaultConversationLists[(int)SNO];//Tags[MarkerKeys.ConversationList].Id; Logger.Debug(" (ReadTags) actor {0} GlobalID {2} has a conversation list {1}", NameSNO, snoConversationList, GlobalID); if (MPQStorage.Data.Assets[SNOGroup.ConversationList].ContainsKey(snoConversationList)) ConversationList = MPQStorage.Data.Assets[SNOGroup.ConversationList][snoConversationList].Data as DiIiS_NA.Core.MPQ.FileFormats.ConversationList; else if (snoConversationList != -1) Logger.Warn("Actor {0} - Conversation list {1} not found!", NameSNO, snoConversationList); } if (Tags.ContainsKey(MarkerKeys.TriggeredConversation)) _snoTriggeredConversation = Tags[MarkerKeys.TriggeredConversation].Id; } #endregion #region movement public void Move(Vector3D point, float facingAngle) { CurrentDestination = point; if (point == Position) return; SetFacingRotation(facingAngle); // find suitable movement animation int aniTag; if (AnimationSet == null) aniTag = -1; else if (AnimationSet.TagExists(DiIiS_NA.Core.MPQ.FileFormats.AnimationTags.Walk) && !(this is Minion) && !(this is Hireling)) aniTag = AnimationSet.GetAnimationTag(DiIiS_NA.Core.MPQ.FileFormats.AnimationTags.Walk); else if (AnimationSet.TagExists(DiIiS_NA.Core.MPQ.FileFormats.AnimationTags.Run)) aniTag = AnimationSet.GetAnimationTag(DiIiS_NA.Core.MPQ.FileFormats.AnimationTags.Run); else aniTag = -1; World?.BroadcastIfRevealed(plr => new ACDTranslateNormalMessage { ActorId = DynamicID(plr), Position = point, Angle = facingAngle, SnapFacing = false, MovementSpeed = WalkSpeed, MoveFlags = 0, AnimationTag = aniTag }, this); } public void MoveSnapped(Vector3D point, float facingAngle) { Position = point; SetFacingRotation(facingAngle); World.BroadcastIfRevealed(plr => new ACDTranslateSnappedMessage { ActorId = (int)DynamicID(plr), Position = point, Angle = facingAngle, Field3 = false, Field4 = 0x900 // TODO: figure out when to use this field }, this); } #endregion public override string ToString() => $"[Actor] [Type: {ActorType}] SNOId:{SNO} GlobalId: {GlobalID} Position: {Position} Name: {Name}"; } // This should probably be the same as GBHandleType (probably merge them once all actor classes are created) public enum ActorType : int { Invalid = 0, Monster = 1, Gizmo = 2, ClientEffect = 3, ServerProp = 4, Environment = 5, Critter = 6, Player = 7, Item = 8, AxeSymbol = 9, Projectile = 10, CustomBrain = 11 } }