blizzless-diiis/src/DiIiS-NA/D3-GameServer/GSSystem/ActorSystem/Actor.cs
Lucca Faria Ferri e2d3344974 Rifts improvement.
- Finishes the rift automatically if config.ini has NephalemAutoFinish is set to true, if it's true, if there are less than NephalemRiftAutoFinish monsters alive, finishes rift (and goes to boss). Disabled by default.
- Added NephalemRiftOrbsChance (rate at which a orb is spawned).
- SpawnGenerator.cs added hexmaze
- DeathPayload.cs improvement
2023-02-09 22:48:41 -08:00

1383 lines
40 KiB
C#

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();
/// <summary>
/// ActorSNO.
/// </summary>
public SNOHandle ActorSNO { get; private set; }
public ActorSno SNO => (ActorSno)ActorSNO.Id;
public string Name => ActorSNO.Name;
/// <summary>
/// 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.
/// </summary>
public ActorSno NameSNO { get; set; }
public bool Disable = false;
public bool Spawner = false;
/// <summary>
/// The actor type.
/// </summary>
public abstract ActorType ActorType { get; }
public object _payloadLock = new();
/// <summary>
/// Current scene for the actor.
/// </summary>
public virtual Scene CurrentScene
{
get { return World.QuadTree.Query<Scene>(Bounds).FirstOrDefault(); }
}
/// <summary>
/// Returns true if actor is already spawned in the world.
/// </summary>
public bool Spawned { get; private set; }
public int GroupId = 0;
public int NumberInWorld = 0;
/// <summary>
/// Default lenght value for region based queries.
/// </summary>
public const int DefaultQueryProximityLenght = 240;
/// <summary>
/// Default lenght value for range based queries.
/// </summary>
public int DefaultQueryProximityRadius = 100;
public float LastSecondCasts = 0;
/// <summary>
/// PRTransform for the actor.
/// </summary>
public virtual PRTransform Transform
{
get { return new PRTransform { Quaternion = new Quaternion { W = RotationW, Vector3D = RotationAxis }, Vector3D = Position }; }
}
/// <summary>
/// Replaces the actor's rotation with one that rotates along the Z-axis by the specified "facing" angle.
/// </summary>
/// <param name="facingAngle">The angle in radians.</param>
public void SetFacingRotation(float facingAngle)
{
if (!Spawner)
{
Quaternion q = Quaternion.FacingRotation(facingAngle);
RotationW = q.W;
RotationAxis = q.Vector3D;
}
}
/// <summary>
/// Tags read from MPQ's for the actor.
/// </summary>
public TagMap Tags { get; private set; }
/// <summary>
/// Attribute map.
/// </summary>
public GameAttributeMap Attributes { get; }
/// <summary>
/// Affix list.
/// </summary>
public List<Affix> AffixList { get; set; }
/// <summary>
/// GBHandle.
/// </summary>
public GBHandle GBHandle { get; private set; }
/// <summary>
/// Collision flags.
/// </summary>
public int CollFlags { get; set; }
/// <summary>
/// Check for summoned monsters
/// </summary>
public bool HasLoot { get; set; }
public bool Dead = false;
public bool Alive => !Dead;
/// <summary>
/// Gets whether the actor is visible by questrange, privately set on quest progress
/// </summary>
public bool Visible { get; private set; }
/// <summary>
/// The QuestRange specifies the visibility of an actor, depending on quest progress
/// </summary>
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; }
/// <summary>
/// Returns true if actor has world location.
/// TODO: I belive this belongs to WorldObject.cs /raist.
/// </summary>
public virtual bool HasWorldLocation
{
get { return true; }
}
/// <summary>
/// The info set for actor. (erekose)
/// </summary>
public DiIiS_NA.Core.MPQ.FileFormats.ActorData ActorData
=> (DiIiS_NA.Core.MPQ.FileFormats.ActorData)MPQStorage.Data.Assets[SNOGroup.Actor][(int)SNO].Data;
/// <summary>
/// The animation set for actor.
/// </summary>
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;
/// <summary>
/// Creates a new actor.
/// </summary>
/// <param name="world">The world that initially belongs to.</param>
/// <param name="sno">SNOId of the actor.</param>
/// <param name="tags">TagMapEntry dictionary read for the actor from MPQ's..</param>
/// <param name="isMarker">Is Marker</param>
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<Affix>();
// 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();
}
/// <summary>
/// Creates a new actor.
/// </summary>
/// <param name="world">The world that initially belongs to.</param>
/// <param name="sno">SNOId of the actor.</param>
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;
/// <summary>
/// Unregister from quest events when object is destroyed
/// </summary>
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;
}
/// <summary>
/// Returns true if the actor is revealed to player.
/// </summary>
/// <param name="player">The player.</param>
/// <returns><see cref="bool"/></returns>
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
};
}
/// <summary>
/// Reveals an actor to a player.
/// </summary>
/// <returns>true if the actor was revealed or false if the actor was already revealed.</returns>
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<TagMap>(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;
}
}
/// <summary>
/// Unreveals an actor from a player.
/// </summary>
/// <returns>true if the actor was unrevealed or false if the actor wasn't already revealed.</returns>
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<Player> GetPlayersInRange(float? radius = null)
{
radius ??= DefaultQueryProximityRadius;
return GetObjectsInRange<Player>(radius);
}
public List<Item> GetItemsInRange(float? radius = null)
{
radius ??= DefaultQueryProximityRadius;
return GetObjectsInRange<Item>(radius);
}
public List<Monster> GetMonstersInRange(float? radius = null)
{
radius ??= DefaultQueryProximityRadius;
return GetObjectsInRange<Monster>(radius);
}
public List<Actor> GetActorsInRange(float? radius = null)
{
radius ??= DefaultQueryProximityRadius;
if (World == null || Position == null) return new List<Actor>();
return GetObjectsInRange<Actor>(radius);
}
public List<Actor> GetActorsInRange(Vector3D TPosition, float? radius = null)
{
radius ??= DefaultQueryProximityRadius;
return GetObjectsInRange<Actor>(TPosition, radius);
}
public List<T> GetObjectsInRange<T>(Vector3D TPosition, float? radius = null) where T : WorldObject
{
var proximityCircle = new Circle(TPosition.X, TPosition.Y, radius ?? DefaultQueryProximityRadius);
return World.QuadTree.Query<T>(proximityCircle);
}
public List<T> GetActorsInRange<T>(float? radius = null) where T : Actor
{
radius ??= DefaultQueryProximityRadius;
return GetObjectsInRange<T>(radius);
}
public List<Scene> GetScenesInRange(float? radius = null)
{
radius ??= DefaultQueryProximityRadius;
return GetObjectsInRange<Scene>(radius);
}
public List<WorldObject> GetObjectsInRange(float? radius = null)
{
radius ??= DefaultQueryProximityRadius;
return GetObjectsInRange<WorldObject>(radius);
}
public List<T> GetObjectsInRange<T>(float? radius = null, bool includeHierarchy = false) where T : WorldObject
{
if (World == null || Position == null) return new List<T>();
radius ??= DefaultQueryProximityRadius;
var proximityCircle = new Circle(Position.X, Position.Y, radius.Value);
return World.QuadTree.Query<T>(proximityCircle, includeHierarchy);
}
#endregion
#region rectangluar region queries
public List<Player> GetPlayersInRegion(int lenght = DefaultQueryProximityLenght) => GetObjectsInRegion<Player>(lenght);
public List<Item> GetItemsInRegion(int lenght = DefaultQueryProximityLenght) => GetObjectsInRegion<Item>(lenght);
public List<Monster> GetMonstersInRegion(int lenght = DefaultQueryProximityLenght) => GetObjectsInRegion<Monster>(lenght);
public List<Actor> GetActorsInRegion(int lenght = DefaultQueryProximityLenght) => GetObjectsInRegion<Actor>(lenght);
public List<T> GetActorsInRegion<T>(int lenght = DefaultQueryProximityLenght) where T : Actor => GetObjectsInRegion<T>(lenght);
public List<Scene> GetScenesInRegion(int lenght = DefaultQueryProximityLenght) => GetObjectsInRegion<Scene>(lenght);
public List<WorldObject> GetObjectsInRegion(int lenght = DefaultQueryProximityLenght) => GetObjectsInRegion<WorldObject>(lenght);
public List<T> GetObjectsInRegion<T>(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<T>(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()
{
}
/// <summary>
/// Called when a player moves close to the actor
/// </summary>
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
/// <summary>
/// Reads known tags from TagMapEntry and set the proper values.
/// </summary>
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
}
}