From db9c5956f264c8d2ad0a3ab5550175cb4de7d0bb Mon Sep 17 00:00:00 2001 From: Rutherther Date: Thu, 3 Feb 2022 18:34:37 +0100 Subject: [PATCH] feat(game): add handling of packets for maps and entities --- .../Events/Battle/AoESkillUsedEvent.cs | 26 +++ .../Events/Battle/SkillUsedEvent.cs | 32 +++ .../Events/Characters/CharacterDiedEvent.cs | 14 ++ .../Events/Characters/CharacterStunEvent.cs | 13 ++ .../Characters/ReceivedCharacterDataEvent.cs | 2 - .../Events/Characters/SkillsReceivedEvent.cs | 4 + .../Events/Entities/EntityDiedEvent.cs | 21 ++ .../Events/Entities/EntityJoinedMapEvent.cs | 15 ++ .../Events/Entities/EntityLeftMapEvent.cs | 21 ++ .../Events/Entities/EntityMovedEvent.cs | 23 ++ .../{MovedEvent.cs => EntityRevivedEvent.cs} | 17 +- .../Events/Entities/ItemDroppedEvent.cs | 15 ++ .../Events/Login/LogoutEvent.cs | 14 -- .../Events/Map/MapChangedEvent.cs | 18 ++ .../Events/Players/SkillUsedEvent.cs | 13 -- .../Extensions/ServiceCollectionExtensions.cs | 15 +- .../Extensions/SkillsExtensions.cs | 31 ++- Core/NosSmooth.Game/Game.cs | 38 +++- Core/NosSmooth.Game/Helpers/EntityHelpers.cs | 51 +++++ .../Helpers/EquipmentHelpers.cs | 97 +++++++++ Core/NosSmooth.Game/NosSmooth.Game.csproj | 3 +- .../Characters/CharacterInitResponder.cs | 137 ++++++------ .../Characters/SkillResponder.cs | 53 ++++- .../Characters/StatPacketResponder.cs | 70 ++++++ .../Characters/WalkResponder.cs | 58 ++--- .../Entities/AoeSkillUsedResponder.cs | 101 +++++++++ .../Entities/CondPacketResponder.cs | 53 +++++ .../PacketHandlers/Entities/EqResponder.cs | 71 +++++++ .../Entities/SkillUsedResponder.cs | 127 ++++++++--- .../Entities/StPacketResponder.cs | 85 ++++++++ .../PacketHandlers/Map/AtResponder.cs | 49 +++++ .../PacketHandlers/Map/CMapResponder.cs | 91 ++++++++ .../PacketHandlers/Map/DropResponder.cs | 86 ++++++++ .../PacketHandlers/Map/GpPacketResponder.cs | 60 ++++++ .../PacketHandlers/Map/InResponder.cs | 201 ++++++++++++++++++ .../PacketHandlers/Map/MoveResponder.cs | 53 +++++ .../PacketHandlers/Map/OutResponder.cs | 67 ++++++ 37 files changed, 1643 insertions(+), 202 deletions(-) create mode 100644 Core/NosSmooth.Game/Events/Battle/AoESkillUsedEvent.cs create mode 100644 Core/NosSmooth.Game/Events/Battle/SkillUsedEvent.cs create mode 100644 Core/NosSmooth.Game/Events/Characters/CharacterDiedEvent.cs create mode 100644 Core/NosSmooth.Game/Events/Characters/CharacterStunEvent.cs create mode 100644 Core/NosSmooth.Game/Events/Entities/EntityDiedEvent.cs create mode 100644 Core/NosSmooth.Game/Events/Entities/EntityJoinedMapEvent.cs create mode 100644 Core/NosSmooth.Game/Events/Entities/EntityLeftMapEvent.cs create mode 100644 Core/NosSmooth.Game/Events/Entities/EntityMovedEvent.cs rename Core/NosSmooth.Game/Events/Entities/{MovedEvent.cs => EntityRevivedEvent.cs} (56%) create mode 100644 Core/NosSmooth.Game/Events/Entities/ItemDroppedEvent.cs delete mode 100644 Core/NosSmooth.Game/Events/Login/LogoutEvent.cs create mode 100644 Core/NosSmooth.Game/Events/Map/MapChangedEvent.cs delete mode 100644 Core/NosSmooth.Game/Events/Players/SkillUsedEvent.cs create mode 100644 Core/NosSmooth.Game/Helpers/EntityHelpers.cs create mode 100644 Core/NosSmooth.Game/Helpers/EquipmentHelpers.cs create mode 100644 Core/NosSmooth.Game/PacketHandlers/Characters/StatPacketResponder.cs create mode 100644 Core/NosSmooth.Game/PacketHandlers/Entities/AoeSkillUsedResponder.cs create mode 100644 Core/NosSmooth.Game/PacketHandlers/Entities/CondPacketResponder.cs create mode 100644 Core/NosSmooth.Game/PacketHandlers/Entities/EqResponder.cs create mode 100644 Core/NosSmooth.Game/PacketHandlers/Entities/StPacketResponder.cs create mode 100644 Core/NosSmooth.Game/PacketHandlers/Map/AtResponder.cs create mode 100644 Core/NosSmooth.Game/PacketHandlers/Map/CMapResponder.cs create mode 100644 Core/NosSmooth.Game/PacketHandlers/Map/DropResponder.cs create mode 100644 Core/NosSmooth.Game/PacketHandlers/Map/GpPacketResponder.cs create mode 100644 Core/NosSmooth.Game/PacketHandlers/Map/InResponder.cs create mode 100644 Core/NosSmooth.Game/PacketHandlers/Map/MoveResponder.cs create mode 100644 Core/NosSmooth.Game/PacketHandlers/Map/OutResponder.cs diff --git a/Core/NosSmooth.Game/Events/Battle/AoESkillUsedEvent.cs b/Core/NosSmooth.Game/Events/Battle/AoESkillUsedEvent.cs new file mode 100644 index 0000000..4c3a347 --- /dev/null +++ b/Core/NosSmooth.Game/Events/Battle/AoESkillUsedEvent.cs @@ -0,0 +1,26 @@ +// +// AoESkillUsedEvent.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.Game.Data.Characters; +using NosSmooth.Game.Data.Entities; +using NosSmooth.Game.Data.Info; +using NosSmooth.Packets.Enums.Battle; + +namespace NosSmooth.Game.Events.Battle; + +/// +/// An AoE skill has been used. (bs packet) +/// +/// +/// The damage to various entities will be sent in respective Su packets. +/// TODO find out connections between su and bs packets. +/// +public record AoESkillUsedEvent +( + ILivingEntity Caster, + Skill Skill, + Position TargetPosition +) : IGameEvent; \ No newline at end of file diff --git a/Core/NosSmooth.Game/Events/Battle/SkillUsedEvent.cs b/Core/NosSmooth.Game/Events/Battle/SkillUsedEvent.cs new file mode 100644 index 0000000..580eccc --- /dev/null +++ b/Core/NosSmooth.Game/Events/Battle/SkillUsedEvent.cs @@ -0,0 +1,32 @@ +// +// SkillUsedEvent.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.Game.Data.Characters; +using NosSmooth.Game.Data.Entities; +using NosSmooth.Game.Data.Info; +using NosSmooth.Packets.Enums.Battle; + +namespace NosSmooth.Game.Events.Battle; + +/// +/// A skill has been used. +/// +/// The caster entity of the skill. +/// The target entity of the skill. +/// The skill that has been used with the information about the skill. +/// The vnum of the skill. +/// The position of the target. +/// +/// +public record SkillUsedEvent +( + ILivingEntity Caster, + ILivingEntity Target, + Skill Skill, + Position? TargetPosition, + HitMode? Hit, + uint Damage +) : IGameEvent; \ No newline at end of file diff --git a/Core/NosSmooth.Game/Events/Characters/CharacterDiedEvent.cs b/Core/NosSmooth.Game/Events/Characters/CharacterDiedEvent.cs new file mode 100644 index 0000000..1331661 --- /dev/null +++ b/Core/NosSmooth.Game/Events/Characters/CharacterDiedEvent.cs @@ -0,0 +1,14 @@ +// +// CharacterDiedEvent.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.Game.Data.Characters; + +namespace NosSmooth.Game.Events.Characters; + +/// +/// The playing character has died. +/// +public record CharacterDiedEvent(Skill? KillSkill) : IGameEvent; \ No newline at end of file diff --git a/Core/NosSmooth.Game/Events/Characters/CharacterStunEvent.cs b/Core/NosSmooth.Game/Events/Characters/CharacterStunEvent.cs new file mode 100644 index 0000000..4e72279 --- /dev/null +++ b/Core/NosSmooth.Game/Events/Characters/CharacterStunEvent.cs @@ -0,0 +1,13 @@ +// +// CharacterStunEvent.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace NosSmooth.Game.Events.Characters; + +/// +/// The character has been stunned or unstunned. +/// +/// Whether the character is stunned. +public record CharacterStunEvent(bool Stunned); \ No newline at end of file diff --git a/Core/NosSmooth.Game/Events/Characters/ReceivedCharacterDataEvent.cs b/Core/NosSmooth.Game/Events/Characters/ReceivedCharacterDataEvent.cs index 984f8ae..8322e48 100644 --- a/Core/NosSmooth.Game/Events/Characters/ReceivedCharacterDataEvent.cs +++ b/Core/NosSmooth.Game/Events/Characters/ReceivedCharacterDataEvent.cs @@ -11,10 +11,8 @@ namespace NosSmooth.Game.Events.Characters; /// /// Represents received new updated character data. /// -/// The old data. /// The newly received data. public record ReceivedCharacterDataEvent ( - Character? OldCharacter, Character Character ) : IGameEvent; \ No newline at end of file diff --git a/Core/NosSmooth.Game/Events/Characters/SkillsReceivedEvent.cs b/Core/NosSmooth.Game/Events/Characters/SkillsReceivedEvent.cs index 58e6a89..679bd52 100644 --- a/Core/NosSmooth.Game/Events/Characters/SkillsReceivedEvent.cs +++ b/Core/NosSmooth.Game/Events/Characters/SkillsReceivedEvent.cs @@ -8,4 +8,8 @@ using NosSmooth.Game.Data.Characters; namespace NosSmooth.Game.Events.Characters; +/// +/// Received skills of the character. +/// +/// The skills. public record SkillsReceivedEvent(Skills Skills) : IGameEvent; \ No newline at end of file diff --git a/Core/NosSmooth.Game/Events/Entities/EntityDiedEvent.cs b/Core/NosSmooth.Game/Events/Entities/EntityDiedEvent.cs new file mode 100644 index 0000000..e9270d3 --- /dev/null +++ b/Core/NosSmooth.Game/Events/Entities/EntityDiedEvent.cs @@ -0,0 +1,21 @@ +// +// EntityDiedEvent.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.Game.Data.Characters; +using NosSmooth.Game.Data.Entities; +using NosSmooth.Game.Events.Characters; + +namespace NosSmooth.Game.Events.Entities; + +/// +/// An entity has died. +/// +/// +/// Is not emitted for the character, see . +/// +/// The entity that has died. +/// The skill that was used to kill the entity, if known. +public record EntityDiedEvent(ILivingEntity Entity, Skill? KillSkill) : IGameEvent; \ No newline at end of file diff --git a/Core/NosSmooth.Game/Events/Entities/EntityJoinedMapEvent.cs b/Core/NosSmooth.Game/Events/Entities/EntityJoinedMapEvent.cs new file mode 100644 index 0000000..0ad85e9 --- /dev/null +++ b/Core/NosSmooth.Game/Events/Entities/EntityJoinedMapEvent.cs @@ -0,0 +1,15 @@ +// +// EntityJoinedMapEvent.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.Game.Data.Entities; + +namespace NosSmooth.Game.Events.Entities; + +/// +/// The given entity has joined the map. +/// +/// The entity. +public record EntityJoinedMapEvent(IEntity Entity) : IGameEvent; \ No newline at end of file diff --git a/Core/NosSmooth.Game/Events/Entities/EntityLeftMapEvent.cs b/Core/NosSmooth.Game/Events/Entities/EntityLeftMapEvent.cs new file mode 100644 index 0000000..cb09655 --- /dev/null +++ b/Core/NosSmooth.Game/Events/Entities/EntityLeftMapEvent.cs @@ -0,0 +1,21 @@ +// +// EntityLeftMapEvent.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.Game.Data.Entities; +using NosSmooth.Game.Data.Maps; + +namespace NosSmooth.Game.Events.Entities; + +/// +/// An entity has left the map. +/// +/// The entity that has left. +public record EntityLeftMapEvent +( + IEntity Entity, + Portal? Portal, + bool? Died +) : IGameEvent; \ No newline at end of file diff --git a/Core/NosSmooth.Game/Events/Entities/EntityMovedEvent.cs b/Core/NosSmooth.Game/Events/Entities/EntityMovedEvent.cs new file mode 100644 index 0000000..4b0573f --- /dev/null +++ b/Core/NosSmooth.Game/Events/Entities/EntityMovedEvent.cs @@ -0,0 +1,23 @@ +// +// EntityMovedEvent.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.Game.Data.Entities; +using NosSmooth.Game.Data.Info; + +namespace NosSmooth.Game.Events.Entities; + +/// +/// An entity has moved. +/// +/// The entity. +/// The previous position of the entity. +/// The new position of the entity. +public record EntityMovedEvent +( + IEntity Entity, + Position? OldPosition, + Position NewPosition +) : IGameEvent; \ No newline at end of file diff --git a/Core/NosSmooth.Game/Events/Entities/MovedEvent.cs b/Core/NosSmooth.Game/Events/Entities/EntityRevivedEvent.cs similarity index 56% rename from Core/NosSmooth.Game/Events/Entities/MovedEvent.cs rename to Core/NosSmooth.Game/Events/Entities/EntityRevivedEvent.cs index a89c354..5110480 100644 --- a/Core/NosSmooth.Game/Events/Entities/MovedEvent.cs +++ b/Core/NosSmooth.Game/Events/Entities/EntityRevivedEvent.cs @@ -1,18 +1,15 @@ -// -// MovedEvent.cs +// +// EntityRevivedEvent.cs // // Copyright (c) František Boháček. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using NosSmooth.Game.Data.Entities; -using NosSmooth.Game.Data.Info; namespace NosSmooth.Game.Events.Entities; -public record MovedEvent -( - IEntity Entity, - long EntityId, - Position? OldPosition, - Position NewPosition -) : IGameEvent; \ No newline at end of file +/// +/// The given entity has been revived. +/// +/// +public record EntityRevivedEvent(ILivingEntity Entity); \ No newline at end of file diff --git a/Core/NosSmooth.Game/Events/Entities/ItemDroppedEvent.cs b/Core/NosSmooth.Game/Events/Entities/ItemDroppedEvent.cs new file mode 100644 index 0000000..5226bee --- /dev/null +++ b/Core/NosSmooth.Game/Events/Entities/ItemDroppedEvent.cs @@ -0,0 +1,15 @@ +// +// ItemDroppedEvent.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.Game.Data.Entities; + +namespace NosSmooth.Game.Events.Entities; + +/// +/// An item has been dropped. +/// +/// The item that has been dropped. +public record ItemDroppedEvent(IEntity Item) : IGameEvent; \ No newline at end of file diff --git a/Core/NosSmooth.Game/Events/Login/LogoutEvent.cs b/Core/NosSmooth.Game/Events/Login/LogoutEvent.cs deleted file mode 100644 index f094a04..0000000 --- a/Core/NosSmooth.Game/Events/Login/LogoutEvent.cs +++ /dev/null @@ -1,14 +0,0 @@ -// -// LogoutEvent.cs -// -// Copyright (c) František Boháček. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace NosSmooth.Game.Events.Login; - -/// -/// Represents event that is emitted if the user is logged out / changing the server etc. -/// -public class LogoutEvent : IGameEvent -{ -} \ No newline at end of file diff --git a/Core/NosSmooth.Game/Events/Map/MapChangedEvent.cs b/Core/NosSmooth.Game/Events/Map/MapChangedEvent.cs new file mode 100644 index 0000000..f342035 --- /dev/null +++ b/Core/NosSmooth.Game/Events/Map/MapChangedEvent.cs @@ -0,0 +1,18 @@ +// +// MapChangedEvent.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace NosSmooth.Game.Events.Map; + +/// +/// A map has been changed. +/// +/// The previous map. +/// The new map. +public record MapChangedEvent +( + Data.Maps.Map? PreviousMap, + Data.Maps.Map CurrentMap +) : IGameEvent; \ No newline at end of file diff --git a/Core/NosSmooth.Game/Events/Players/SkillUsedEvent.cs b/Core/NosSmooth.Game/Events/Players/SkillUsedEvent.cs deleted file mode 100644 index 58e2551..0000000 --- a/Core/NosSmooth.Game/Events/Players/SkillUsedEvent.cs +++ /dev/null @@ -1,13 +0,0 @@ -// -// SkillUsedEvent.cs -// -// Copyright (c) František Boháček. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using NosSmooth.Game.Data.Characters; -using NosSmooth.Game.Data.Entities; -using NosSmooth.Game.Data.Info; - -namespace NosSmooth.Game.Events.Players; - -public record SkillUsedEvent(ILivingEntity? Entity, long EntityId, Skill? Skill, long SkillVNum, long TargetId, Position? TargetPosition) : IGameEvent; \ No newline at end of file diff --git a/Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs b/Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs index ac19b42..e381006 100644 --- a/Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs +++ b/Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs @@ -11,6 +11,7 @@ using NosSmooth.Game.Apis; using NosSmooth.Game.Events.Core; using NosSmooth.Game.PacketHandlers.Characters; using NosSmooth.Game.PacketHandlers.Entities; +using NosSmooth.Game.PacketHandlers.Map; namespace NosSmooth.Game.Extensions; @@ -36,7 +37,19 @@ public static class ServiceCollectionExtensions .AddPacketResponder() .AddPacketResponder() .AddPacketResponder() - .AddPacketResponder(); + .AddPacketResponder() + .AddPacketResponder() + .AddPacketResponder() + .AddPacketResponder() + .AddPacketResponder() + .AddPacketResponder() + .AddPacketResponder() + .AddPacketResponder() + .AddPacketResponder() + .AddPacketResponder() + .AddPacketResponder() + .AddPacketResponder() + .AddPacketResponder(); serviceCollection .AddTransient() diff --git a/Core/NosSmooth.Game/Extensions/SkillsExtensions.cs b/Core/NosSmooth.Game/Extensions/SkillsExtensions.cs index b8d10c9..53f7b76 100644 --- a/Core/NosSmooth.Game/Extensions/SkillsExtensions.cs +++ b/Core/NosSmooth.Game/Extensions/SkillsExtensions.cs @@ -14,13 +14,42 @@ namespace NosSmooth.Game.Extensions; /// public static class SkillsExtensions { + /// + /// Tries to get the skill of the specified vnum. + /// + /// The skills of the player. + /// The cast id to search for. + /// The skill, if found. + public static Result TryGetSkillByCastId(this Skills skills, short castId) + { + if (skills.PrimarySkill.Info?.CastId == castId) + { + return skills.PrimarySkill; + } + + if (skills.SecondarySkill.Info?.CastId == castId) + { + return skills.SecondarySkill; + } + + foreach (Skill skill in skills.OtherSkills) + { + if (skill.Info?.CastId == castId) + { + return skill; + } + } + + return new NotFoundError(); + } + /// /// Tries to get the skill of the specified vnum. /// /// The skills of the player. /// The vnum to search for. /// The skill, if found. - public static Result TryGetSkill(this Skills skills, long skillVNum) + public static Result TryGetSkillByVNum(this Skills skills, long skillVNum) { if (skills.PrimarySkill.SkillVNum == skillVNum) { diff --git a/Core/NosSmooth.Game/Game.cs b/Core/NosSmooth.Game/Game.cs index 888a2af..e73d8c6 100644 --- a/Core/NosSmooth.Game/Game.cs +++ b/Core/NosSmooth.Game/Game.cs @@ -51,7 +51,6 @@ public class Game internal set { _currentMap = value; - MapChanged?.CancelAfter(TimeSpan.FromSeconds(_options.EntityCacheDuration)); } } @@ -63,11 +62,6 @@ public class Game /// public Raid? CurrentRaid { get; internal set; } - /// - /// Cancellation token for changing the map to use in memory cache. - /// - internal CancellationTokenSource? MapChanged { get; private set; } - /// /// Creates the character if it is null, or updates the current character. /// @@ -103,9 +97,9 @@ public class Game /// Whether to release the semaphore used for changing the map. /// The cancellation token for cancelling the operation. /// The updated character. - internal async Task CreateMapAsync + internal async Task CreateMapAsync ( - Func create, + Func create, bool releaseSemaphore = true, CancellationToken ct = default ) @@ -120,6 +114,34 @@ public class Game ); } + /// + /// Creates the map if it is null, or updates the current map. + /// + /// The function for creating the map. + /// The function for updating the map. + /// Whether to release the semaphore used for changing the map. + /// The cancellation token for cancelling the operation. + /// The updated character. + internal async Task CreateOrUpdateMapAsync + ( + Func create, + Func update, + bool releaseSemaphore = true, + CancellationToken ct = default + ) + { + return await CreateOrUpdateAsync + ( + GameSemaphoreType.Map, + () => CurrentMap, + m => CurrentMap = m, + create, + update, + releaseSemaphore, + ct + ); + } + private async Task CreateAsync ( GameSemaphoreType type, diff --git a/Core/NosSmooth.Game/Helpers/EntityHelpers.cs b/Core/NosSmooth.Game/Helpers/EntityHelpers.cs new file mode 100644 index 0000000..6554e4a --- /dev/null +++ b/Core/NosSmooth.Game/Helpers/EntityHelpers.cs @@ -0,0 +1,51 @@ +// +// EntityHelpers.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.Game.Data.Entities; +using NosSmooth.Packets.Enums; + +namespace NosSmooth.Game.Helpers; + +/// +/// Helper methods for various operations with entities. +/// +public static class EntityHelpers +{ + /// + /// Create an entity from the given type and id. + /// + /// The entity type. + /// The entity id. + /// The entity. + public static IEntity CreateEntity(EntityType type, long entityId) + { + switch (type) + { + case EntityType.Npc: + return new Npc + { + Id = entityId + }; + case EntityType.Monster: + return new Monster + { + Id = entityId + }; + case EntityType.Player: + return new Player + { + Id = entityId + }; + case EntityType.Object: + return new GroundItem + { + Id = entityId + }; + } + + throw new Exception("Unknown entity type."); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Game/Helpers/EquipmentHelpers.cs b/Core/NosSmooth.Game/Helpers/EquipmentHelpers.cs new file mode 100644 index 0000000..1661ae9 --- /dev/null +++ b/Core/NosSmooth.Game/Helpers/EquipmentHelpers.cs @@ -0,0 +1,97 @@ +// +// EquipmentHelpers.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Reflection; +using NosSmooth.Data.Abstractions; +using NosSmooth.Game.Data.Items; +using NosSmooth.Packets.Server.Maps; +using NosSmooth.Packets.Server.Weapons; +using Remora.Results; + +namespace NosSmooth.Game.Helpers; + +/// +/// Helpers for creating equipment from packets. +/// +public static class EquipmentHelpers +{ + /// + /// Create from the given in equipment subpacket. + /// + /// The info service. + /// The subpacket. + /// The weapon upgrade. + /// The armor upgrade. + /// The cancellation token for cancelling the operation. + /// The equipment or an error. + public static async Task CreateEquipmentFromInSubpacketAsync + ( + IInfoService infoService, + InEquipmentSubPacket equipmentSubPacket, + UpgradeRareSubPacket? weaponUpgradeRare = default, + UpgradeRareSubPacket? armorUpgradeRare = default, + CancellationToken ct = default + ) + { + var fairy = await CreateItemAsync(infoService, equipmentSubPacket.FairyVNum, ct); + var mask = await CreateItemAsync(infoService, equipmentSubPacket.MaskVNum, ct); + var hat = await CreateItemAsync(infoService, equipmentSubPacket.HatVNum, ct); + var costumeSuit = await CreateItemAsync(infoService, equipmentSubPacket.CostumeSuitVNum, ct); + var costumeHat = await CreateItemAsync(infoService, equipmentSubPacket.CostumeHatVNum, ct); + var mainWeapon = await CreateItemAsync(infoService, equipmentSubPacket.MainWeaponVNum, weaponUpgradeRare, ct); + var secondaryWeapon = await CreateItemAsync(infoService, equipmentSubPacket.SecondaryWeaponVNum, null, ct); + var armor = await CreateItemAsync(infoService, equipmentSubPacket.ArmorVNum, armorUpgradeRare, ct); + + return new Equipment + ( + hat, + armor, + mainWeapon, + secondaryWeapon, + mask, + fairy, + costumeSuit, + costumeHat, + equipmentSubPacket.WeaponSkin, + equipmentSubPacket.WingSkin + ); + } + + private static async Task CreateItemAsync(IInfoService infoService, int? itemVNum, CancellationToken ct = default) + { + if (itemVNum is null) + { + return null; + } + + var itemInfo = await infoService.GetItemInfoAsync(itemVNum.Value, ct); + + return new Item + ( + itemVNum.Value, + itemInfo.IsSuccess ? itemInfo.Entity : null + ); + } + + private static async Task CreateItemAsync + (IInfoService infoService, int? itemVNum, UpgradeRareSubPacket? upgradeRareSubPacket, CancellationToken ct = default) + { + if (itemVNum is null) + { + return null; + } + + var itemInfo = await infoService.GetItemInfoAsync(itemVNum.Value, ct); + + return new UpgradeableItem + ( + itemVNum.Value, + itemInfo.IsSuccess ? itemInfo.Entity : null, + upgradeRareSubPacket?.Upgrade, + upgradeRareSubPacket?.Rare + ); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Game/NosSmooth.Game.csproj b/Core/NosSmooth.Game/NosSmooth.Game.csproj index 81e0dc9..c4768d7 100644 --- a/Core/NosSmooth.Game/NosSmooth.Game.csproj +++ b/Core/NosSmooth.Game/NosSmooth.Game.csproj @@ -9,11 +9,12 @@ - + + diff --git a/Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs b/Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs index ac94249..f34ef43 100644 --- a/Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs +++ b/Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs @@ -43,51 +43,51 @@ public class CharacterInitResponder : IPacketResponder, IPacketResp var character = await _game.CreateOrUpdateCharacterAsync ( () => new Character - ( - Family: new Family(packet.FamilyId, packet.FamilyName, packet.FamilyLevel), - Group: new Group(packet.GroupId, default, default), - Id: packet.CharacterId, - Name: packet.Name, - AuthorityType: packet.Authority, - Sex: packet.Sex, - HairStyle: packet.HairStyle, - HairColor: packet.HairColor, - Class: packet.Class, - Icon: packet.Icon, - Compliment: packet.Compliment, - Morph: new Morph(packet.MorphVNum, packet.MorphUpgrade), - Invisible: packet.IsInvisible, - ArenaWinner: packet.ArenaWinner - ), - (character) - => character with + { + Family = new Family(packet.FamilyId, null, packet.FamilyName, packet.FamilyLevel, null), + Group = new Group(packet.GroupId, default, default), + Id = packet.CharacterId, + Name = packet.Name, + Authority = packet.Authority, + Sex = packet.Sex, + HairStyle = packet.HairStyle, + HairColor = packet.HairColor, + Class = packet.Class, + Icon = packet.Icon, + Compliment = packet.Compliment, + Morph = new Morph(packet.MorphVNum, packet.MorphUpgrade), + IsInvisible = packet.IsInvisible, + ArenaWinner = packet.ArenaWinner + }, + (character) => + { + character.Id = packet.CharacterId; + character.Authority = packet.Authority; + character.Sex = packet.Sex; + character.HairStyle = packet.HairStyle; + character.HairColor = packet.HairColor; + character.Class = packet.Class; + character.Icon = packet.Icon; + character.Compliment = packet.Compliment; + character.Group = (character.Group ?? new Group(packet.GroupId, null, null)) with + { + Id = packet.GroupId + }; + character.Morph = (character.Morph ?? new Morph(packet.MorphVNum, packet.MorphUpgrade)) with { - Id = packet.CharacterId, - AuthorityType = packet.Authority, - Sex = packet.Sex, - HairStyle = packet.HairStyle, - HairColor = packet.HairColor, - Class = packet.Class, - Icon = packet.Icon, - Compliment = packet.Compliment, - Group = (character.Group ?? new Group(packet.GroupId, null, null)) with - { - Id = packet.GroupId - }, - Morph = (character.Morph ?? new Morph(packet.MorphVNum, packet.MorphUpgrade)) with - { - VNum = packet.MorphVNum, Upgrade = packet.MorphUpgrade - }, - ArenaWinner = packet.ArenaWinner, - Invisible = packet.IsInvisible, - Family = new Family(packet.FamilyId, packet.FamilyName, packet.FamilyLevel) - }, + VNum = packet.MorphVNum, Upgrade = packet.MorphUpgrade + }; + character.ArenaWinner = packet.ArenaWinner; + character.IsInvisible = packet.IsInvisible; + character.Family = new Family(packet.FamilyId, null, packet.FamilyName, packet.FamilyLevel, null); + return character; + }, ct: ct ); if (character != oldCharacter) { - return await _eventDispatcher.DispatchEvent(new ReceivedCharacterDataEvent(oldCharacter, character), ct); + return await _eventDispatcher.DispatchEvent(new ReceivedCharacterDataEvent(character), ct); } return Result.FromSuccess(); @@ -102,28 +102,28 @@ public class CharacterInitResponder : IPacketResponder, IPacketResp var character = await _game.CreateOrUpdateCharacterAsync ( () => new Character - ( - SkillCp: packet.SkillCp, - Reputation: packet.Reputation, - Level: new Level(packet.Level, packet.LevelXp, packet.XpLoad), - JobLevel: new Level(packet.JobLevel, packet.JobLevelXp, packet.JobXpLoad), - HeroLevel: new Level(packet.HeroLevel, packet.HeroLevelXp, packet.HeroXpLoad) - ), + { + SkillCp = packet.SkillCp, + Reputation = packet.Reputation, + PlayerLevel = new Level(packet.Level, packet.LevelXp, packet.XpLoad), + JobLevel = new Level(packet.JobLevel, packet.JobLevelXp, packet.JobXpLoad), + HeroLevelStruct = new Level(packet.HeroLevel, packet.HeroLevelXp, packet.HeroXpLoad) + }, (character) => - character with - { - SkillCp = packet.SkillCp, - Reputation = packet.Reputation, - Level = new Level(packet.Level, packet.LevelXp, packet.XpLoad), - JobLevel = new Level(packet.JobLevel, packet.JobLevelXp, packet.JobXpLoad), - HeroLevel = new Level(packet.HeroLevel, packet.HeroLevelXp, packet.HeroXpLoad) - }, + { + character.SkillCp = packet.SkillCp; + character.Reputation = packet.Reputation; + character.PlayerLevel = new Level(packet.Level, packet.LevelXp, packet.XpLoad); + character.JobLevel = new Level(packet.JobLevel, packet.JobLevelXp, packet.JobXpLoad); + character.HeroLevelStruct = new Level(packet.HeroLevel, packet.HeroLevelXp, packet.HeroXpLoad); + return character; + }, ct: ct ); if (character != oldCharacter) { - return await _eventDispatcher.DispatchEvent(new ReceivedCharacterDataEvent(oldCharacter, character), ct); + return await _eventDispatcher.DispatchEvent(new ReceivedCharacterDataEvent(character), ct); } return Result.FromSuccess(); @@ -144,24 +144,25 @@ public class CharacterInitResponder : IPacketResponder, IPacketResp ( () => throw new NotImplementedException(), (character) => - character with - { - Morph = new Morph - ( - packet.MorphVNum, - packet.MorphUpgrade, - packet.MorphDesign, - packet.MorphBonus, - packet.MorphSkin - ), - Size = packet.Size - }, + { + character.Morph = new Morph + ( + packet.MorphVNum, + packet.MorphUpgrade, + packet.MorphDesign, + packet.MorphBonus, + packet.MorphSkin + ); + + character.Size = packet.Size; + return character; + }, ct: ct ); if (oldCharacter != character) { - return await _eventDispatcher.DispatchEvent(new ReceivedCharacterDataEvent(oldCharacter, character), ct); + return await _eventDispatcher.DispatchEvent(new ReceivedCharacterDataEvent(character), ct); } return Result.FromSuccess(); diff --git a/Core/NosSmooth.Game/PacketHandlers/Characters/SkillResponder.cs b/Core/NosSmooth.Game/PacketHandlers/Characters/SkillResponder.cs index 05d8658..7718ec3 100644 --- a/Core/NosSmooth.Game/PacketHandlers/Characters/SkillResponder.cs +++ b/Core/NosSmooth.Game/PacketHandlers/Characters/SkillResponder.cs @@ -4,7 +4,10 @@ // Copyright (c) František Boháček. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using Microsoft.Extensions.Logging; +using NosSmooth.Core.Extensions; using NosSmooth.Core.Packets; +using NosSmooth.Data.Abstractions; using NosSmooth.Game.Data.Characters; using NosSmooth.Game.Events.Characters; using NosSmooth.Game.Events.Core; @@ -20,52 +23,65 @@ public class SkillResponder : IPacketResponder { private readonly Game _game; private readonly EventDispatcher _eventDispatcher; + private readonly IInfoService _infoService; + private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// /// The nostale game. /// The event dispatcher. - public SkillResponder(Game game, EventDispatcher eventDispatcher) + /// The info service. + /// The logger. + public SkillResponder + ( + Game game, + EventDispatcher eventDispatcher, + IInfoService infoService, + ILogger logger + ) { _game = game; _eventDispatcher = eventDispatcher; + _infoService = infoService; + _logger = logger; } /// public async Task Respond(PacketEventArgs packetArgs, CancellationToken ct = default) { + // TODO: put all code into CreateOrUpdate to avoid concurrent problems. var packet = packetArgs.Packet; Skill primarySkill, secondarySkill; var character = _game.Character; - if (character is not null && packet.PrimarySkillId == character.Skills?.PrimarySkill.SkillVNum) + if (character is not null && packet.PrimarySkillVNum == character.Skills?.PrimarySkill.SkillVNum) { primarySkill = character.Skills.PrimarySkill; } else { - primarySkill = new Skill(packet.PrimarySkillId); + primarySkill = await CreateSkill(packet.PrimarySkillVNum, default); } - if (character is not null && packet.PrimarySkillId == packet.SecondarySkillId) + if (character is not null && packet.PrimarySkillVNum == packet.SecondarySkillVNum) { secondarySkill = primarySkill; } - else if (character is not null && packet.SecondarySkillId == character.Skills?.SecondarySkill.SkillVNum) + else if (character is not null && packet.SecondarySkillVNum == character.Skills?.SecondarySkill.SkillVNum) { secondarySkill = character.Skills.SecondarySkill; } else { - secondarySkill = new Skill(packet.SecondarySkillId); + secondarySkill = await CreateSkill(packet.SecondarySkillVNum, default); } - var skillsFromPacket = packet.SkillSubPackets?.Select(x => x.SkillId).ToList() ?? new List(); + var skillsFromPacket = packet.SkillSubPackets?.Select(x => x.SkillVNum).ToList() ?? new List(); var skillsFromCharacter = character?.Skills is null - ? new List() + ? new List() : character.Skills.OtherSkills.Select(x => x.SkillVNum).ToList(); var newSkills = skillsFromPacket.Except(skillsFromCharacter); var oldSkills = skillsFromCharacter.Except(skillsFromPacket); @@ -75,15 +91,19 @@ public class SkillResponder : IPacketResponder foreach (var newSkill in newSkills) { - otherSkillsFromCharacter.Add(new Skill(newSkill)); + otherSkillsFromCharacter.Add(await CreateSkill(newSkill, default)); } var skills = new Skills(primarySkill, secondarySkill, otherSkillsFromCharacter); await _game.CreateOrUpdateCharacterAsync ( - () => new Character(Skills: skills), - c => c with { Skills = skills }, + () => new Character { Skills = skills }, + c => + { + c.Skills = skills; + return c; + }, ct: ct ); @@ -91,4 +111,15 @@ public class SkillResponder : IPacketResponder return Result.FromSuccess(); } + + private async Task CreateSkill(int vnum, int? level) + { + var infoResult = await _infoService.GetSkillInfoAsync(vnum); + if (!infoResult.IsSuccess) + { + _logger.LogWarning("Could not obtain a skill info for vnum {vnum}: {error}", vnum, infoResult.ToFullString()); + } + + return new Skill(vnum, level, infoResult.IsSuccess ? infoResult.Entity : null); + } } \ No newline at end of file diff --git a/Core/NosSmooth.Game/PacketHandlers/Characters/StatPacketResponder.cs b/Core/NosSmooth.Game/PacketHandlers/Characters/StatPacketResponder.cs new file mode 100644 index 0000000..2ee9a8a --- /dev/null +++ b/Core/NosSmooth.Game/PacketHandlers/Characters/StatPacketResponder.cs @@ -0,0 +1,70 @@ +// +// StatPacketResponder.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.Core.Packets; +using NosSmooth.Game.Data.Info; +using NosSmooth.Packets.Server.Entities; +using Remora.Results; + +namespace NosSmooth.Game.PacketHandlers.Characters; + +/// +/// Responder to stat packet. +/// +public class StatPacketResponder : IPacketResponder +{ + private readonly Game _game; + + /// + /// Initializes a new instance of the class. + /// + /// The game. + public StatPacketResponder(Game game) + { + _game = game; + } + + /// + public Task Respond(PacketEventArgs packetArgs, CancellationToken ct = default) + { + var character = _game.Character; + if (character is null) + { + return Task.FromResult(Result.FromSuccess()); + } + + var packet = packetArgs.Packet; + if (character.Hp is null) + { + character.Hp = new Health + { + Amount = packet.Hp, + Maximum = packet.HpMaximum + }; + } + else + { + character.Hp.Amount = packet.Hp; + character.Hp.Maximum = packet.HpMaximum; + } + + if (character.Mp is null) + { + character.Mp = new Health + { + Amount = packet.Mp, + Maximum = packet.MpMaximum + }; + } + else + { + character.Mp.Amount = packet.Mp; + character.Mp.Maximum = packet.MpMaximum; + } + + return Task.FromResult(Result.FromSuccess()); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Game/PacketHandlers/Characters/WalkResponder.cs b/Core/NosSmooth.Game/PacketHandlers/Characters/WalkResponder.cs index 5047b05..e0e4c80 100644 --- a/Core/NosSmooth.Game/PacketHandlers/Characters/WalkResponder.cs +++ b/Core/NosSmooth.Game/PacketHandlers/Characters/WalkResponder.cs @@ -38,46 +38,28 @@ public class WalkResponder : IPacketResponder { var character = _game.Character; var packet = packetArgs.Packet; - if (character is not null && character.Position is not null) - { - var oldPosition = new Position - { - X = character.Position.X, - Y = character.Position.Y - }; + var oldPosition = character?.Position; + var position = new Position(packet.PositionX, packet.PositionY); - character.Position.X = packet.PositionX; - character.Position.Y = packet.PositionY; + character = await _game.CreateOrUpdateCharacterAsync + ( + () => new Character + { + Position = position + }, + (c) => + { + c.Position = position; + return c; + }, + ct: ct + ); - return await _eventDispatcher.DispatchEvent - ( - new MovedEvent(character, character.Id, oldPosition, character.Position), - ct - ); - } - else if (character?.Position is null) - { - await _game.CreateOrUpdateCharacterAsync - ( - () => new Character - ( - Position: new Position() - { - X = packet.PositionX, - Y = packet.PositionY - } - ), - (c) => c with - { - Position = new Position() - { - X = packet.PositionX, - Y = packet.PositionY - } - }, - ct: ct - ); - } + return await _eventDispatcher.DispatchEvent + ( + new EntityMovedEvent(character, oldPosition, character.Position!.Value), + ct + ); return Result.FromSuccess(); } diff --git a/Core/NosSmooth.Game/PacketHandlers/Entities/AoeSkillUsedResponder.cs b/Core/NosSmooth.Game/PacketHandlers/Entities/AoeSkillUsedResponder.cs new file mode 100644 index 0000000..1a37280 --- /dev/null +++ b/Core/NosSmooth.Game/PacketHandlers/Entities/AoeSkillUsedResponder.cs @@ -0,0 +1,101 @@ +// +// AoeSkillUsedResponder.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Extensions.Logging; +using NosSmooth.Core.Extensions; +using NosSmooth.Core.Packets; +using NosSmooth.Data.Abstractions; +using NosSmooth.Game.Data.Characters; +using NosSmooth.Game.Data.Entities; +using NosSmooth.Game.Data.Info; +using NosSmooth.Game.Events.Battle; +using NosSmooth.Game.Events.Core; +using NosSmooth.Game.Extensions; +using NosSmooth.Game.Helpers; +using NosSmooth.Packets.Server.Battle; +using Remora.Results; + +namespace NosSmooth.Game.PacketHandlers.Entities; + +/// +/// Responder to bs packet. +/// +public class AoeSkillUsedResponder : IPacketResponder +{ + private readonly Game _game; + private readonly EventDispatcher _eventDispatcher; + private readonly IInfoService _infoService; + private readonly Logger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The game. + /// The event dispatcher. + /// The info service. + /// The logger. + public AoeSkillUsedResponder + ( + Game game, + EventDispatcher eventDispatcher, + IInfoService infoService, + Logger logger + ) + { + _game = game; + _eventDispatcher = eventDispatcher; + _infoService = infoService; + _logger = logger; + + } + + /// + public async Task Respond(PacketEventArgs packetArgs, CancellationToken ct = default) + { + var packet = packetArgs.Packet; + var caster = _game.CurrentMap?.Entities.GetEntity + (packet.CasterEntityId) ?? (ILivingEntity)EntityHelpers.CreateEntity + (packet.CasterEntityType, packet.CasterEntityId); + Skill? skillEntity = null; + + if (caster is Character character) + { + var skillResult = character.Skills?.TryGetSkillByVNum(packet.SkillVNum); + if (skillResult?.IsSuccess ?? false) + { + skillEntity = skillResult.Value.Entity; + } + } + + if (skillEntity is null) + { + var skillInfoResult = await _infoService.GetSkillInfoAsync(packet.SkillVNum, ct); + _logger.LogWarning + ( + "Could not obtain a skill info for vnum {vnum}: {error}", + packet.SkillVNum, + skillInfoResult.ToFullString() + ); + + skillEntity = new Skill(packet.SkillVNum, Info: skillInfoResult.IsSuccess ? skillInfoResult.Entity : null); + } + + return await _eventDispatcher.DispatchEvent + ( + new AoESkillUsedEvent + ( + caster, + skillEntity, + new Position + ( + packet.X, + packet.Y + ) + ), + ct + ); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Game/PacketHandlers/Entities/CondPacketResponder.cs b/Core/NosSmooth.Game/PacketHandlers/Entities/CondPacketResponder.cs new file mode 100644 index 0000000..829a63c --- /dev/null +++ b/Core/NosSmooth.Game/PacketHandlers/Entities/CondPacketResponder.cs @@ -0,0 +1,53 @@ +// +// CondPacketResponder.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.Core.Packets; +using NosSmooth.Game.Data.Entities; +using NosSmooth.Packets.Server.Entities; +using Remora.Results; + +namespace NosSmooth.Game.PacketHandlers.Entities; + +/// +/// Responder to cond packet. +/// +public class CondPacketResponder : IPacketResponder +{ + private readonly Game _game; + + /// + /// Initializes a new instance of the class. + /// + /// The game. + public CondPacketResponder(Game game) + { + _game = game; + } + + /// + public Task Respond(PacketEventArgs packetArgs, CancellationToken ct = default) + { + var map = _game.CurrentMap; + if (map is null) + { + return Task.FromResult(Result.FromSuccess()); + } + + var packet = packetArgs.Packet; + var entity = map.Entities.GetEntity(packet.EntityId); + + if (entity is null) + { + return Task.FromResult(Result.FromSuccess()); + } + + entity.Speed = packet.Speed; + entity.CantAttack = packet.CantAttack; + entity.CantMove = packet.CantMove; + + return Task.FromResult(Result.FromSuccess()); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Game/PacketHandlers/Entities/EqResponder.cs b/Core/NosSmooth.Game/PacketHandlers/Entities/EqResponder.cs new file mode 100644 index 0000000..c302508 --- /dev/null +++ b/Core/NosSmooth.Game/PacketHandlers/Entities/EqResponder.cs @@ -0,0 +1,71 @@ +// +// EqResponder.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.Core.Packets; +using NosSmooth.Data.Abstractions; +using NosSmooth.Game.Data.Entities; +using NosSmooth.Game.Data.Items; +using NosSmooth.Game.Helpers; +using NosSmooth.Packets.Server.Entities; +using Remora.Results; + +namespace NosSmooth.Game.PacketHandlers.Entities; + +/// +/// Responder to eq packet. +/// +public class EqResponder : IPacketResponder +{ + private readonly Game _game; + private readonly IInfoService _infoService; + + /// + /// Initializes a new instance of the class. + /// + /// The game. + /// The info service. + public EqResponder(Game game, IInfoService infoService) + { + _game = game; + _infoService = infoService; + + } + + /// + public async Task Respond(PacketEventArgs packetArgs, CancellationToken ct = default) + { + var map = _game.CurrentMap; + if (map is null) + { + return Result.FromSuccess(); + } + + var packet = packetArgs.Packet; + var entity = map.Entities.GetEntity(packet.CharacterId); + + if (entity is null) + { + return Result.FromSuccess(); + } + + entity.Sex = packet.Sex; + entity.Class = packet.Class; + entity.Size = packet.Size; + entity.Authority = packet.AuthorityType; + entity.HairColor = packet.HairColor; + entity.HairStyle = packet.HairStyle; + entity.Equipment = await EquipmentHelpers.CreateEquipmentFromInSubpacketAsync + ( + _infoService, + packet.EquipmentSubPacket, + packet.WeaponUpgradeRareSubPacket, + packet.ArmorUpgradeRareSubPacket, + ct + ); + + return Result.FromSuccess(); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Game/PacketHandlers/Entities/SkillUsedResponder.cs b/Core/NosSmooth.Game/PacketHandlers/Entities/SkillUsedResponder.cs index 748097a..5d1d21b 100644 --- a/Core/NosSmooth.Game/PacketHandlers/Entities/SkillUsedResponder.cs +++ b/Core/NosSmooth.Game/PacketHandlers/Entities/SkillUsedResponder.cs @@ -4,12 +4,21 @@ // Copyright (c) František Boháček. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using Microsoft.Extensions.Logging; +using Microsoft.VisualBasic; +using NosSmooth.Core.Extensions; using NosSmooth.Core.Packets; +using NosSmooth.Data.Abstractions; +using NosSmooth.Data.Abstractions.Enums; +using NosSmooth.Game.Data.Characters; +using NosSmooth.Game.Data.Entities; using NosSmooth.Game.Data.Info; +using NosSmooth.Game.Events.Battle; using NosSmooth.Game.Events.Characters; using NosSmooth.Game.Events.Core; -using NosSmooth.Game.Events.Players; +using NosSmooth.Game.Events.Entities; using NosSmooth.Game.Extensions; +using NosSmooth.Game.Helpers; using NosSmooth.Packets.Server.Battle; using NosSmooth.Packets.Server.Skills; using Remora.Results; @@ -23,68 +32,116 @@ public class SkillUsedResponder : IPacketResponder, IPacketResponder _logger; /// /// Initializes a new instance of the class. /// /// The game. /// The event dispatcher. - public SkillUsedResponder(Game game, EventDispatcher eventDispatcher) + /// The info service. + /// The logger. + public SkillUsedResponder + ( + Game game, + EventDispatcher eventDispatcher, + IInfoService infoService, + ILogger logger + ) { _game = game; _eventDispatcher = eventDispatcher; + _infoService = infoService; + _logger = logger; } /// public async Task Respond(PacketEventArgs packetArgs, CancellationToken ct = default) { var packet = packetArgs.Packet; - var character = _game.Character; + var map = _game.CurrentMap; + + // TODO: add to map if the entity is created? + var caster = map?.Entities?.GetEntity + (packet.CasterEntityId) ?? (ILivingEntity)EntityHelpers.CreateEntity + (packet.CasterEntityType, packet.CasterEntityId); + var target = map?.Entities?.GetEntity + (packet.TargetEntityId) ?? (ILivingEntity)EntityHelpers.CreateEntity + (packet.TargetEntityType, packet.TargetEntityId); - if (character is not null && character.Id == packet.CasterEntityId && character.Skills is not null) + if (target.Hp is null) { - var skillResult = character.Skills.TryGetSkill(packet.SkillVNum); + target.Hp = new Health + { + Percentage = packet.HpPercentage + }; + } + else + { + target.Hp.Percentage = packet.HpPercentage; + } - if (skillResult.IsDefined(out var skillEntity)) + Skill? skillEntity; + if (caster is Character character && character.Skills is not null) + { + var skillResult = character.Skills.TryGetSkillByVNum(packet.SkillVNum); + + if (skillResult.IsDefined(out skillEntity)) { skillEntity.LastUseTime = DateTimeOffset.Now; - skillEntity.Cooldown = TimeSpan.FromSeconds(packet.SkillCooldown / 10.0); skillEntity.IsOnCooldown = true; } - - await _eventDispatcher.DispatchEvent( - new SkillUsedEvent - ( - character, - character.Id, - skillResult.IsSuccess ? skillEntity : null, - packet.SkillVNum, - packet.TargetEntityId, - new Position { X = packet.PositionX, Y = packet.PositionY } - ), - ct); + else + { + var skillInfoResult = await _infoService.GetSkillInfoAsync(packet.SkillVNum, ct); + skillEntity = new Skill + (packet.SkillVNum, null, skillInfoResult.IsSuccess ? skillInfoResult.Entity : null); + } } else { - // TODO: add entity from the map, if exists. - await _eventDispatcher.DispatchEvent( - new SkillUsedEvent + var skillInfoResult = await _infoService.GetSkillInfoAsync(packet.SkillVNum, ct); + if (!skillInfoResult.IsSuccess) + { + _logger.LogWarning ( - null, - packet.CasterEntityId, - null, + "Could not obtain a skill info for vnum {vnum}: {error}", packet.SkillVNum, - packet.TargetEntityId, - new Position - { - X = packet.PositionX, Y = packet.PositionY - } - ), - ct - ); + skillInfoResult.ToFullString() + ); + } + + skillEntity = new Skill + (packet.SkillVNum, null, skillInfoResult.IsSuccess ? skillInfoResult.Entity : null); } - return Result.FromSuccess(); + var dispatchResult = await _eventDispatcher.DispatchEvent + ( + new SkillUsedEvent + ( + caster, + target, + skillEntity, + new Position(packet.PositionX, packet.PositionY), + packet.HitMode, + packet.Damage + ), + ct + ); + + if (!packet.TargetIsAlive) + { + var diedResult = await _eventDispatcher.DispatchEvent(new EntityDiedEvent(target, skillEntity), ct); + if (!diedResult.IsSuccess) + { + return dispatchResult.IsSuccess + ? diedResult + : new AggregateError(diedResult, dispatchResult); + } + } + + return dispatchResult; } /// @@ -95,7 +152,7 @@ public class SkillUsedResponder : IPacketResponder, IPacketResponder +/// Responds to st packet. +/// +public class StPacketResponder : IPacketResponder +{ + private readonly Game _game; + + /// + /// Initializes a new instance of the class. + /// + /// The game. + public StPacketResponder(Game game) + { + _game = game; + } + + /// + public Task Respond(PacketEventArgs packetArgs, CancellationToken ct = default) + { + var map = _game.CurrentMap; + + if (map is null) + { + return Task.FromResult(Result.FromSuccess()); + } + + var packet = packetArgs.Packet; + var entity = map.Entities.GetEntity(packet.EntityId); + if (entity is null) + { + return Task.FromResult(Result.FromSuccess()); + } + + entity.EffectsVNums = packet.BuffVNums; + entity.Level = packet.Level; + if (entity is Player player) + { + player.HeroLevel = packet.HeroLevel; + } + + if (entity.Hp is null) + { + entity.Hp = new Health + { + Amount = packet.Hp, + Percentage = packet.HpPercentage + }; + } + else + { + entity.Hp.Amount = packet.Hp; + entity.Hp.Percentage = packet.HpPercentage; + } + + if (entity.Mp is null) + { + entity.Mp = new Health + { + Amount = packet.Mp, + Percentage = packet.MpPercentage + }; + } + else + { + entity.Mp.Amount = packet.Mp; + entity.Mp.Percentage = packet.MpPercentage; + } + + return Task.FromResult(Result.FromSuccess()); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Game/PacketHandlers/Map/AtResponder.cs b/Core/NosSmooth.Game/PacketHandlers/Map/AtResponder.cs new file mode 100644 index 0000000..2b21cd7 --- /dev/null +++ b/Core/NosSmooth.Game/PacketHandlers/Map/AtResponder.cs @@ -0,0 +1,49 @@ +// +// AtResponder.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.Core.Packets; +using NosSmooth.Game.Data.Info; +using NosSmooth.Packets.Server.Maps; +using Remora.Results; + +namespace NosSmooth.Game.PacketHandlers.Map; + +/// +/// Responds to at packet. +/// +public class AtResponder : IPacketResponder +{ + private readonly Game _game; + + /// + /// Initializes a new instance of the class. + /// + /// The game. + public AtResponder(Game game) + { + _game = game; + + } + + /// + public Task Respond(PacketEventArgs packetArgs, CancellationToken ct = default) + { + var packet = packetArgs.Packet; + var map = _game.CurrentMap; + if (map is null) + { + return Task.FromResult(Result.FromSuccess()); + } + + var entity = map.Entities.GetEntity(packet.CharacterId); + if (entity is not null) + { + entity.Position = new Position(packet.X, packet.Y); + } + + return Task.FromResult(Result.FromSuccess()); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Game/PacketHandlers/Map/CMapResponder.cs b/Core/NosSmooth.Game/PacketHandlers/Map/CMapResponder.cs new file mode 100644 index 0000000..cc72594 --- /dev/null +++ b/Core/NosSmooth.Game/PacketHandlers/Map/CMapResponder.cs @@ -0,0 +1,91 @@ +// +// CMapResponder.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; +using NosSmooth.Core.Extensions; +using NosSmooth.Core.Packets; +using NosSmooth.Data.Abstractions; +using NosSmooth.Game.Data.Entities; +using NosSmooth.Game.Data.Maps; +using NosSmooth.Packets.Server.Maps; +using Remora.Results; + +namespace NosSmooth.Game.PacketHandlers.Map; + +/// +/// Responds to by creating a new map. +/// +public class CMapResponder : IPacketResponder +{ + private readonly Game _game; + private readonly IInfoService _infoService; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The nostale game. + /// The info service. + /// The logger. + public CMapResponder(Game game, IInfoService infoService, ILogger logger) + { + _game = game; + _infoService = infoService; + _logger = logger; + } + + /// + public async Task Respond(PacketEventArgs packetArgs, CancellationToken ct = default) + { + var packet = packetArgs.Packet; + var mapInfoResult = await _infoService.GetMapInfoAsync(packet.Id, ct); + if (!mapInfoResult.IsSuccess) + { + _logger.LogWarning + ( + "Could not obtain a map info for id {id}: {error}", + packet.Id, + mapInfoResult.ToFullString() + ); + } + + await _game.CreateMapAsync + ( + () => + { + var map = packet.Id == 20001 + ? new Miniland + ( + packet.Id, + packet.Type, + mapInfoResult.IsSuccess ? mapInfoResult.Entity : null, + new MapEntities(), + Array.Empty(), + Array.Empty() + ) + : new Data.Maps.Map + ( + packet.Id, + packet.Type, + mapInfoResult.IsSuccess ? mapInfoResult.Entity : null, + new MapEntities(), + Array.Empty() + ); + + var character = _game.Character; + if (character is not null) + { + map.Entities.AddEntity(character); + } + return map; + }, + ct: ct + ); + + return Result.FromSuccess(); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Game/PacketHandlers/Map/DropResponder.cs b/Core/NosSmooth.Game/PacketHandlers/Map/DropResponder.cs new file mode 100644 index 0000000..a508f9e --- /dev/null +++ b/Core/NosSmooth.Game/PacketHandlers/Map/DropResponder.cs @@ -0,0 +1,86 @@ +// +// DropResponder.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Extensions.Logging; +using NosSmooth.Core.Extensions; +using NosSmooth.Core.Packets; +using NosSmooth.Data.Abstractions; +using NosSmooth.Game.Data.Entities; +using NosSmooth.Game.Data.Info; +using NosSmooth.Game.Events.Core; +using NosSmooth.Game.Events.Entities; +using NosSmooth.Packets.Server.Entities; +using NosSmooth.Packets.Server.Maps; +using Remora.Results; + +namespace NosSmooth.Game.PacketHandlers.Map; + +/// +/// Responds to drop packet. +/// +public class DropResponder : IPacketResponder +{ + private readonly Game _game; + private readonly EventDispatcher _eventDispatcher; + private readonly IInfoService _infoService; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The game. + /// The event dispatcher. + /// The info service. + /// The logger. + public DropResponder + ( + Game game, + EventDispatcher eventDispatcher, + IInfoService infoService, + ILogger logger + ) + { + _game = game; + _eventDispatcher = eventDispatcher; + _infoService = infoService; + _logger = logger; + + } + + /// + public async Task Respond(PacketEventArgs packetArgs, CancellationToken ct = default) + { + var packet = packetArgs.Packet; + var itemInfoResult = await _infoService.GetItemInfoAsync(packet.ItemVNum, ct); + if (!itemInfoResult.IsDefined(out var itemInfo)) + { + _logger.LogWarning("Could not obtain item info for vnum {vnum}: {error}", packet.ItemVNum, itemInfoResult.ToFullString()); + } + + var entity = new GroundItem + { + Amount = packet.Amount, + Id = packet.DropId, + IsQuestRelated = packet.IsQuestRelated, + ItemInfo = itemInfo, + OwnerId = null, + Position = new Position + ( + packet.X, + packet.Y + ), + VNum = packet.ItemVNum + }; + + var map = _game.CurrentMap; + if (map is not null) + { + map.Entities.AddEntity(entity); + } + + return await _eventDispatcher.DispatchEvent(new ItemDroppedEvent(entity), ct); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Game/PacketHandlers/Map/GpPacketResponder.cs b/Core/NosSmooth.Game/PacketHandlers/Map/GpPacketResponder.cs new file mode 100644 index 0000000..b76a473 --- /dev/null +++ b/Core/NosSmooth.Game/PacketHandlers/Map/GpPacketResponder.cs @@ -0,0 +1,60 @@ +// +// GpPacketResponder.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.Core.Packets; +using NosSmooth.Game.Data.Info; +using NosSmooth.Game.Data.Maps; +using NosSmooth.Packets.Server.Portals; +using Remora.Results; + +namespace NosSmooth.Game.PacketHandlers.Map; + +/// +/// Responder to gp packet. +/// +public class GpPacketResponder : IPacketResponder +{ + private readonly Game _game; + + /// + /// Initializes a new instance of the class. + /// + /// The game. + public GpPacketResponder(Game game) + { + _game = game; + } + + /// + public async Task Respond(PacketEventArgs packetArgs, CancellationToken ct = default) + { + var packet = packetArgs.Packet; + await _game.CreateOrUpdateMapAsync + ( + () => null, + (map) => map! with + { + Portals = map.Portals.Concat + ( + new[] + { + new Portal + ( + packet.PortalId, + new Position(packet.X, packet.Y), + packet.TargetMapId, + packet.PortalType, + packet.IsDisabled + ) + } + ).ToArray() + }, + ct: ct + ); + + return Result.FromSuccess(); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Game/PacketHandlers/Map/InResponder.cs b/Core/NosSmooth.Game/PacketHandlers/Map/InResponder.cs new file mode 100644 index 0000000..bb7a0ef --- /dev/null +++ b/Core/NosSmooth.Game/PacketHandlers/Map/InResponder.cs @@ -0,0 +1,201 @@ +// +// InResponder.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Extensions.Logging; +using NosSmooth.Core.Extensions; +using NosSmooth.Core.Packets; +using NosSmooth.Data.Abstractions; +using NosSmooth.Game.Data.Entities; +using NosSmooth.Game.Data.Info; +using NosSmooth.Game.Data.Items; +using NosSmooth.Game.Data.Social; +using NosSmooth.Game.Events.Core; +using NosSmooth.Game.Events.Entities; +using NosSmooth.Game.Helpers; +using NosSmooth.Packets.Server.Entities; +using NosSmooth.Packets.Server.Maps; +using Remora.Results; + +namespace NosSmooth.Game.PacketHandlers.Map; + +/// +/// Responds to in packet. +/// +public class InResponder : IPacketResponder +{ + private readonly Game _game; + private readonly EventDispatcher _eventDispatcher; + private readonly IInfoService _infoService; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The game. + /// The event dispatcher. + /// The info service. + /// The logger. + public InResponder + ( + Game game, + EventDispatcher eventDispatcher, + IInfoService infoService, + ILogger logger + ) + { + _game = game; + _eventDispatcher = eventDispatcher; + _infoService = infoService; + _logger = logger; + } + + /// + public async Task Respond(PacketEventArgs packetArgs, CancellationToken ct = default) + { + var packet = packetArgs.Packet; + var map = _game.CurrentMap; + if (map is null) + { + return Result.FromSuccess(); + } + + var entities = map.Entities; + + // add entity to the map + var entity = await CreateEntityFromInPacket(packet, ct); + entities.AddEntity(entity); + + return await _eventDispatcher.DispatchEvent(new EntityJoinedMapEvent(entity), ct); + } + + private async Task CreateEntityFromInPacket(InPacket packet, CancellationToken ct) + { + if (packet.ItemSubPacket is not null) + { + return await CreateGroundItem(packet, packet.ItemSubPacket, ct); + } + if (packet.PlayerSubPacket is not null) + { + return await CreatePlayer(packet, packet.PlayerSubPacket, ct); + } + if (packet.NonPlayerSubPacket is not null) + { + return await CreateMonster(packet, packet.NonPlayerSubPacket, ct); + } + + throw new Exception("The in packet did not contain any subpacket. Bug?"); + } + + private async Task CreateGroundItem + (InPacket packet, InItemSubPacket itemSubPacket, CancellationToken ct) + { + if (packet.VNum is null) + { + throw new Exception("The vnum from the in packet cannot be null for items."); + } + var itemInfoResult = await _infoService.GetItemInfoAsync(packet.VNum.Value, ct); + if (!itemInfoResult.IsDefined(out var itemInfo)) + { + _logger.LogWarning + ( + "Could not obtain an item info for vnum {vnum}: {error}", + packet.VNum.Value, + itemInfoResult.ToFullString() + ); + } + + return new GroundItem + { + Amount = itemSubPacket.Amount, + Id = packet.EntityId, + OwnerId = itemSubPacket.OwnerId, + IsQuestRelated = itemSubPacket.IsQuestRelative, + ItemInfo = itemInfo, + Position = new Position(packet.PositionX, packet.PositionY), + VNum = packet.VNum.Value, + }; + } + + private async Task CreatePlayer(InPacket packet, InPlayerSubPacket playerSubPacket, CancellationToken ct) + { + return new Player + { + Position = new Position(packet.PositionX, packet.PositionY), + Id = packet.EntityId, + Name = packet.Name?.Name, + ArenaWinner = playerSubPacket.ArenaWinner, + Class = playerSubPacket.Class, + Compliment = playerSubPacket.Compliment, + Direction = packet.Direction, + Equipment = await EquipmentHelpers.CreateEquipmentFromInSubpacketAsync + ( + _infoService, + playerSubPacket.Equipment, + playerSubPacket.WeaponUpgradeRareSubPacket, + playerSubPacket.ArmorUpgradeRareSubPacket, + ct + ), + Faction = playerSubPacket.Faction, + Size = playerSubPacket.Size, + Authority = playerSubPacket.Authority, + Sex = playerSubPacket.Sex, + HairStyle = playerSubPacket.HairStyle, + HairColor = playerSubPacket.HairColor, + Icon = playerSubPacket.ReputationIcon, + IsInvisible = playerSubPacket.IsInvisible, + Title = playerSubPacket.Title, + Level = playerSubPacket.Level, + HeroLevel = playerSubPacket.HeroLevel, + Morph = new Morph(playerSubPacket.MorphVNum, playerSubPacket.MorphUpgrade), + Family = playerSubPacket.FamilySubPacket.FamilyId is null + ? null + : new Family + ( + playerSubPacket.FamilySubPacket.FamilyId, + playerSubPacket.FamilySubPacket.Title, + playerSubPacket.FamilyName, + playerSubPacket.Level, + playerSubPacket.FamilyIcons + ), + }; + } + + private async Task CreateMonster + (InPacket packet, InNonPlayerSubPacket nonPlayerSubPacket, CancellationToken ct) + { + if (packet.VNum is null) + { + throw new Exception("The vnum from the in packet cannot be null for monsters."); + } + + var monsterInfoResult = await _infoService.GetMonsterInfoAsync(packet.VNum.Value, ct); + if (!monsterInfoResult.IsDefined(out var monsterInfo)) + { + _logger.LogWarning + ( + "Could not obtain a monster info for vnum {vnum}: {error}", + packet.VNum.Value, + monsterInfoResult.ToFullString() + ); + } + + return new Monster + { + VNum = packet.VNum.Value, + MonsterInfo = monsterInfo, + Id = packet.EntityId, + Direction = packet.Direction, + Faction = nonPlayerSubPacket.Faction, + Hp = new Health { Percentage = nonPlayerSubPacket.HpPercentage }, + Mp = new Health { Percentage = nonPlayerSubPacket.MpPercentage }, + Name = nonPlayerSubPacket.Name?.Name, + Position = new Position(packet.PositionX, packet.PositionY), + IsInvisible = nonPlayerSubPacket.IsInvisible, + Level = monsterInfo?.Level ?? null, + IsSitting = nonPlayerSubPacket.IsSitting + }; + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Game/PacketHandlers/Map/MoveResponder.cs b/Core/NosSmooth.Game/PacketHandlers/Map/MoveResponder.cs new file mode 100644 index 0000000..67d2fa6 --- /dev/null +++ b/Core/NosSmooth.Game/PacketHandlers/Map/MoveResponder.cs @@ -0,0 +1,53 @@ +// +// MoveResponder.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Concurrent; +using NosSmooth.Core.Packets; +using NosSmooth.Game.Data.Entities; +using NosSmooth.Game.Data.Info; +using NosSmooth.Packets.Server.Entities; +using Remora.Results; + +namespace NosSmooth.Game.PacketHandlers.Map; + +/// +/// Responds to move packet. +/// +public class MoveResponder : IPacketResponder +{ + private readonly Game _game; + + /// + /// Initializes a new instance of the class. + /// + /// The nostale game. + public MoveResponder(Game game) + { + _game = game; + } + + /// + public async Task Respond(PacketEventArgs packetArgs, CancellationToken ct = default) + { + var packet = packetArgs.Packet; + var map = _game.CurrentMap; + + // TODO: store entities somewhere else so we can store them even if the map is still null? + if (map is null) + { + return Result.FromSuccess(); + } + + var entity = map.Entities.GetEntity(packet.EntityId); + if (entity is not null && entity.Position is not null) + { + entity.Position = new Position(packet.MapX, packet.MapY); + entity.Speed = packet.Speed; + } + + return Result.FromSuccess(); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Game/PacketHandlers/Map/OutResponder.cs b/Core/NosSmooth.Game/PacketHandlers/Map/OutResponder.cs new file mode 100644 index 0000000..653b2f9 --- /dev/null +++ b/Core/NosSmooth.Game/PacketHandlers/Map/OutResponder.cs @@ -0,0 +1,67 @@ +// +// OutResponder.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.Core.Packets; +using NosSmooth.Game.Data.Entities; +using NosSmooth.Game.Data.Maps; +using NosSmooth.Game.Events.Core; +using NosSmooth.Game.Events.Entities; +using NosSmooth.Game.Helpers; +using NosSmooth.Packets.Enums; +using NosSmooth.Packets.Server.Maps; +using Remora.Results; + +namespace NosSmooth.Game.PacketHandlers.Map; + +/// +/// Responds to out packet. +/// +public class OutResponder : IPacketResponder +{ + private readonly Game _game; + private readonly EventDispatcher _eventDispatcher; + + /// + /// Initializes a new instance of the class. + /// + /// The game. + /// The event dispatcher. + public OutResponder(Game game, EventDispatcher eventDispatcher) + { + _game = game; + _eventDispatcher = eventDispatcher; + } + + /// + public async Task Respond(PacketEventArgs packetArgs, CancellationToken ct = default) + { + var packet = packetArgs.Packet; + var map = _game.CurrentMap; + if (map is null) + { + return Result.FromSuccess(); + } + map.Entities.RemoveEntity(packet.EntityId); + Portal? portal = null; + + IEntity entity = map.Entities.GetEntity(packet.EntityId) ?? EntityHelpers.CreateEntity + (packet.EntityType, packet.EntityId); + + var position = entity.Position; + if (position is not null) + { + map.IsOnPortal(position.Value, out portal); + } + + bool? died = null; + if (entity is ILivingEntity livingEntity) + { + died = (livingEntity.Hp?.Amount ?? -1) == 0 || (livingEntity.Hp?.Percentage ?? -1) == 0; + } + + return await _eventDispatcher.DispatchEvent(new EntityLeftMapEvent(entity, portal, died), ct); + } +} \ No newline at end of file -- 2.48.1