diff --git a/Intersect.Client.Core/Entities/Animation.cs b/Intersect.Client.Core/Entities/Animation.cs index 0824e77350..244f33c41f 100644 --- a/Intersect.Client.Core/Entities/Animation.cs +++ b/Intersect.Client.Core/Entities/Animation.cs @@ -14,9 +14,12 @@ namespace Intersect.Client.Entities; public partial class Animation : IAnimation { + public event Action? Disposed; + public event Action? Finished; + public bool AutoRotate { get; set; } - private bool disposed = false; + private bool _disposed = false; public bool Hidden { get; set; } @@ -243,6 +246,8 @@ public void EndDraw() { Dispose(); } + + Finished?.Invoke(this); } static Point RotatePoint(Point pointToRotate, Point centerPoint, double angleInDegrees) @@ -275,10 +280,7 @@ public void Show() public void Dispose() { - if (disposed) - { - return; - } + ObjectDisposedException.ThrowIf(_disposed, this); lock (Graphics.AnimationLock) { @@ -294,9 +296,11 @@ public void Dispose() } _ = Graphics.LiveAnimations.Remove(this); - disposed = true; + _disposed = true; GC.SuppressFinalize(this); } + + Disposed?.Invoke(this); } public void DisposeNextDraw() @@ -304,7 +308,7 @@ public void DisposeNextDraw() mDisposeNextDraw = true; } - public bool IsDisposed => disposed; + public bool IsDisposed => _disposed; public void SetPosition(float worldX, float worldY, int mapx, int mapy, Guid mapId, Direction dir, int z = 0) { @@ -322,7 +326,7 @@ public void SetPosition(float worldX, float worldY, int mapx, int mapy, Guid map public void Update() { - if (disposed) + if (_disposed) { return; } diff --git a/Intersect.Client.Core/Entities/Resource.cs b/Intersect.Client.Core/Entities/Resource.cs index 571250f84b..fc3b079300 100644 --- a/Intersect.Client.Core/Entities/Resource.cs +++ b/Intersect.Client.Core/Entities/Resource.cs @@ -4,10 +4,12 @@ using Intersect.Client.Framework.GenericClasses; using Intersect.Client.Framework.Maps; using Intersect.Client.General; +using Intersect.Core; using Intersect.Enums; using Intersect.Framework.Core.GameObjects.Resources; using Intersect.GameObjects; using Intersect.Network.Packets.Server; +using Microsoft.Extensions.Logging; namespace Intersect.Client.Entities; @@ -20,7 +22,9 @@ public partial class Resource : Entity, IResource private bool _waitingForTilesets; private bool _isDead; + private Guid _descriptorId; private ResourceDescriptor? _descriptor; + private IAnimation? _activeAnimation; public Resource(Guid id, ResourceEntityPacket packet) : base(id, packet, EntityType.Resource) { @@ -108,14 +112,67 @@ public override void Load(EntityPacket? packet) return; } + var wasDead = IsDead; IsDead = resourceEntityPacket.IsDead; - if (!ResourceDescriptor.TryGet(resourceEntityPacket.ResourceId, out _descriptor)) + var descriptorId = resourceEntityPacket.ResourceId; + _descriptorId = descriptorId; + + var justDied = !wasDead && resourceEntityPacket.IsDead; + if (!ResourceDescriptor.TryGet(descriptorId, out var descriptor)) { + if (justDied) + { + ApplicationContext.CurrentContext.Logger.LogError( + "Unable to play resource exhaustion animation because resource {EntityId} ({EntityName}) is missing the descriptor ({DescriptorId})", + Id, + Name, + descriptorId + ); + } + return; } + _descriptor = descriptor; UpdateFromDescriptor(_descriptor); + + if (!justDied) + { + return; + } + + if (MapInstance is { } mapInstance) + { + var animation = mapInstance.AddTileAnimation(descriptor.AnimationId, X, Y, Direction.Up); + if (animation is { IsDisposed: false }) + { + animation.Finished += OnAnimationDisposedOrFinished; + animation.Disposed += OnAnimationDisposedOrFinished; + } + _activeAnimation = animation; + } + else + { + ApplicationContext.CurrentContext.Logger.LogError( + "Unable to play resource exhaustion animation because resource {EntityId} ({EntityName}) has no reference to the map instance for map {MapId}", + Id, + Name, + MapId + ); + } + } + + private void OnAnimationDisposedOrFinished(IAnimation animation) + { + if (_activeAnimation != animation) + { + return; + } + + _activeAnimation = null; + animation.Disposed -= OnAnimationDisposedOrFinished; + animation.Finished -= OnAnimationDisposedOrFinished; } private void UpdateFromDescriptor(ResourceDescriptor? descriptor) @@ -353,9 +410,17 @@ public override void Draw() return; } - if (Texture != null) + if (Texture == null) { - Graphics.DrawGameTexture(Texture, _renderBoundsSrc, _renderBoundsDest, Intersect.Color.White); + return; } + + // TODO: Add an option to show the exhausted sprite until the exhaustion animation is finished, but this is not necessary if the graphics line up like Blinkuz' sample provided to fix #2572 + if (_activeAnimation != null) + { + return; + } + + Graphics.DrawGameTexture(Texture, _renderBoundsSrc, _renderBoundsDest, Intersect.Color.White); } } diff --git a/Intersect.Client.Core/General/Globals.cs b/Intersect.Client.Core/General/Globals.cs index 24870b12e5..6bd63f5037 100644 --- a/Intersect.Client.Core/General/Globals.cs +++ b/Intersect.Client.Core/General/Globals.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Intersect.Client.Entities; using Intersect.Client.Entities.Events; using Intersect.Client.Framework.Database; @@ -9,10 +10,12 @@ using Intersect.Client.Items; using Intersect.Client.Maps; using Intersect.Client.Plugins.Interfaces; +using Intersect.Core; using Intersect.Enums; using Intersect.Framework.Core.GameObjects.Crafting; using Intersect.GameObjects; using Intersect.Network.Packets.Server; +using Microsoft.Extensions.Logging; namespace Intersect.Client.General; @@ -221,23 +224,37 @@ internal static void OnGameDraw(DrawStates state, IEntity entity, TimeSpan delta ); } - public static Entity GetEntity(Guid id, EntityType type) + public static bool TryGetEntity(EntityType entityType, Guid entityId, [NotNullWhen(true)] out Entity? entity) { - if (Entities.ContainsKey(id)) + if (!Entities.TryGetValue(entityId, out entity)) { - var entity = Entities[id]; - - if (!entity.IsDisposed && entity.Type == type) - { - EntitiesToDispose.Remove(entity.Id); + return false; + } - return entity; - } + if (entity.IsDisposed) + { + Entities.Remove(entityId); + entity = null; + return false; + } + if (entity.Type != entityType) + { + ApplicationContext.CurrentContext.Logger.LogError( + "Found instance of {ActualEntityType} registered to {EntityId} but it was expected to be {ExpectedEntityType}", + entity.Type, + entityId, + entityType + ); + Entities.Remove(entityId); entity.Dispose(); - Entities.Remove(id); + entity = null; + return false; } - return default; + EntitiesToDispose.Remove(entityId); + return true; } + + public static Entity? GetEntity(Guid id, EntityType type) => TryGetEntity(type, id, out var entity) ? entity : null; } \ No newline at end of file diff --git a/Intersect.Client.Core/Maps/MapInstance.cs b/Intersect.Client.Core/Maps/MapInstance.cs index e98f0c73aa..78f67bc133 100644 --- a/Intersect.Client.Core/Maps/MapInstance.cs +++ b/Intersect.Client.Core/Maps/MapInstance.cs @@ -725,8 +725,8 @@ private void ClearAttributeSounds() } //Animations - public void AddTileAnimation( - Guid animId, + public IAnimation? AddTileAnimation( + Guid animationDescriptorId, int tileX, int tileY, Direction dir = Direction.None, @@ -734,22 +734,42 @@ public void AddTileAnimation( AnimationSource source = default ) { - var animBase = AnimationDescriptor.Get(animId); - if (animBase == null) + if (!AnimationDescriptor.TryGet(animationDescriptorId, out var animationDescriptor)) { - return; + return null; } - var anim = new MapAnimation( - animBase, + return AddTileAnimation( + animationDescriptor, + tileX, + tileY, + dir, + owner, + source + ); + } + + public IAnimation? AddTileAnimation( + AnimationDescriptor animationDescriptor, + int tileX, + int tileY, + Direction dir = Direction.None, + IEntity? owner = null, + AnimationSource source = default + ) + { + var animationInstance = new MapAnimation( + animationDescriptor, tileX, tileY, dir, owner as Entity, source: source ); - LocalAnimations.TryAdd(anim.Id, anim); - anim.SetPosition( + + LocalAnimations.TryAdd(animationInstance.Id, animationInstance); + + animationInstance.SetPosition( X + tileX * _tileWidth + _tileHalfWidth, Y + tileY * _tileHeight + _tileHalfHeight, tileX, @@ -757,6 +777,8 @@ public void AddTileAnimation( Id, dir ); + + return animationInstance; } private void HideActiveAnimations() diff --git a/Intersect.Client.Core/Networking/PacketHandler.cs b/Intersect.Client.Core/Networking/PacketHandler.cs index 8c4e2bda05..f12eda40a2 100644 --- a/Intersect.Client.Core/Networking/PacketHandler.cs +++ b/Intersect.Client.Core/Networking/PacketHandler.cs @@ -389,15 +389,22 @@ public void HandlePacket(IPacketSender packetSender, NpcEntityPacket packet) //ResourceEntityPacket public void HandlePacket(IPacketSender packetSender, ResourceEntityPacket packet) { - var en = Globals.GetEntity(packet.EntityId, EntityType.Resource); - if (en != null) + if (Globals.TryGetEntity(EntityType.Resource, packet.EntityId, out var entity)) { - en.Load(packet); + entity.Load(packet); } else { - var entity = new Resource(packet.EntityId, packet); - Globals.Entities.Add(entity.Id, entity); + entity = new Resource(packet.EntityId, packet); + if (!Globals.Entities.TryAdd(entity.Id, entity)) + { + ApplicationContext.CurrentContext.Logger.LogError( + "Failed to register new {EntityType} {EntityId} ({EntityName})", + EntityType.Resource, + packet.EntityId, + packet.Name + ); + } } } diff --git a/Intersect.Client.Framework/Entities/IAnimation.cs b/Intersect.Client.Framework/Entities/IAnimation.cs index b95e57a5c5..edf7572357 100644 --- a/Intersect.Client.Framework/Entities/IAnimation.cs +++ b/Intersect.Client.Framework/Entities/IAnimation.cs @@ -1,4 +1,3 @@ -using Intersect.GameObjects; using Intersect.Enums; using Intersect.Framework.Core.GameObjects.Animations; @@ -6,6 +5,10 @@ namespace Intersect.Client.Framework.Entities; public interface IAnimation : IDisposable { + event Action? Disposed; + event Action? Finished; + + bool IsDisposed { get; } bool AutoRotate { get; set; } bool Hidden { get; set; } bool InfiniteLoop { get; set; } @@ -16,4 +19,4 @@ public interface IAnimation : IDisposable void SetDir(Direction dir); void SetPosition(float worldX, float worldY, int mapx, int mapy, Guid mapId, Direction dir, int z = 0); void Show(); -} +} \ No newline at end of file diff --git a/Intersect.Client.Framework/Maps/IMapInstance.cs b/Intersect.Client.Framework/Maps/IMapInstance.cs index d8b1bdb7d9..7cc4fb6b2c 100644 --- a/Intersect.Client.Framework/Maps/IMapInstance.cs +++ b/Intersect.Client.Framework/Maps/IMapInstance.cs @@ -28,7 +28,7 @@ public interface IMapInstance bool IsDisposed { get; } bool IsLoaded { get; } - void AddTileAnimation( + IAnimation? AddTileAnimation( Guid animId, int tileX, int tileY, @@ -37,6 +37,15 @@ void AddTileAnimation( AnimationSource source = default ); + IAnimation? AddTileAnimation( + AnimationDescriptor animationDescriptor, + int tileX, + int tileY, + Direction dir = Direction.None, + IEntity? owner = null, + AnimationSource source = default + ); + void CompareEffects(IMapInstance oldMap); bool InView(); void Load(string json); diff --git a/Intersect.Server.Core/Entities/Resource.cs b/Intersect.Server.Core/Entities/Resource.cs index 45f1871650..22839d0567 100644 --- a/Intersect.Server.Core/Entities/Resource.cs +++ b/Intersect.Server.Core/Entities/Resource.cs @@ -60,19 +60,6 @@ public override void Die(bool dropItems = true, Entity killer = null) if (dropItems) { SpawnResourceItems(killer); - if (Descriptor.AnimationId != Guid.Empty) - { - PacketSender.SendAnimationToProximity( - Descriptor.AnimationId, - -1, - Guid.Empty, - MapId, - X, - Y, - Direction.Up, - MapInstanceId - ); - } } PacketSender.SendEntityDataToProximity(this);