~ruther/NosSmooth

db9c5956f264c8d2ad0a3ab5550175cb4de7d0bb — Rutherther 3 years ago e8b3d21
feat(game): add handling of packets for maps and entities
37 files changed, 1636 insertions(+), 195 deletions(-)

A Core/NosSmooth.Game/Events/Battle/AoESkillUsedEvent.cs
A Core/NosSmooth.Game/Events/Battle/SkillUsedEvent.cs
A Core/NosSmooth.Game/Events/Characters/CharacterDiedEvent.cs
A Core/NosSmooth.Game/Events/Characters/CharacterStunEvent.cs
M Core/NosSmooth.Game/Events/Characters/ReceivedCharacterDataEvent.cs
M Core/NosSmooth.Game/Events/Characters/SkillsReceivedEvent.cs
A Core/NosSmooth.Game/Events/Entities/EntityDiedEvent.cs
A Core/NosSmooth.Game/Events/Entities/EntityJoinedMapEvent.cs
A Core/NosSmooth.Game/Events/Entities/EntityLeftMapEvent.cs
R Core/NosSmooth.Game/Events/Entities/{MovedEvent.cs => EntityMovedEvent.cs}
A Core/NosSmooth.Game/Events/Entities/EntityRevivedEvent.cs
A Core/NosSmooth.Game/Events/Entities/ItemDroppedEvent.cs
D Core/NosSmooth.Game/Events/Login/LogoutEvent.cs
A Core/NosSmooth.Game/Events/Map/MapChangedEvent.cs
D Core/NosSmooth.Game/Events/Players/SkillUsedEvent.cs
M Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs
M Core/NosSmooth.Game/Extensions/SkillsExtensions.cs
M Core/NosSmooth.Game/Game.cs
A Core/NosSmooth.Game/Helpers/EntityHelpers.cs
A Core/NosSmooth.Game/Helpers/EquipmentHelpers.cs
M Core/NosSmooth.Game/NosSmooth.Game.csproj
M Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs
M Core/NosSmooth.Game/PacketHandlers/Characters/SkillResponder.cs
A Core/NosSmooth.Game/PacketHandlers/Characters/StatPacketResponder.cs
M Core/NosSmooth.Game/PacketHandlers/Characters/WalkResponder.cs
A Core/NosSmooth.Game/PacketHandlers/Entities/AoeSkillUsedResponder.cs
A Core/NosSmooth.Game/PacketHandlers/Entities/CondPacketResponder.cs
A Core/NosSmooth.Game/PacketHandlers/Entities/EqResponder.cs
M Core/NosSmooth.Game/PacketHandlers/Entities/SkillUsedResponder.cs
A Core/NosSmooth.Game/PacketHandlers/Entities/StPacketResponder.cs
A Core/NosSmooth.Game/PacketHandlers/Map/AtResponder.cs
A Core/NosSmooth.Game/PacketHandlers/Map/CMapResponder.cs
A Core/NosSmooth.Game/PacketHandlers/Map/DropResponder.cs
A Core/NosSmooth.Game/PacketHandlers/Map/GpPacketResponder.cs
A Core/NosSmooth.Game/PacketHandlers/Map/InResponder.cs
A Core/NosSmooth.Game/PacketHandlers/Map/MoveResponder.cs
A Core/NosSmooth.Game/PacketHandlers/Map/OutResponder.cs
A Core/NosSmooth.Game/Events/Battle/AoESkillUsedEvent.cs => Core/NosSmooth.Game/Events/Battle/AoESkillUsedEvent.cs +26 -0
@@ 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;

/// <summary>
/// An AoE skill has been used. (bs packet)
/// </summary>
/// <remarks>
/// The damage to various entities will be sent in respective Su packets.
/// TODO find out connections between su and bs packets.
/// </remarks>
public record AoESkillUsedEvent
(
    ILivingEntity Caster,
    Skill Skill,
    Position TargetPosition
) : IGameEvent;
\ No newline at end of file

A Core/NosSmooth.Game/Events/Battle/SkillUsedEvent.cs => Core/NosSmooth.Game/Events/Battle/SkillUsedEvent.cs +32 -0
@@ 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;

/// <summary>
/// A skill has been used.
/// </summary>
/// <param name="Caster">The caster entity of the skill.</param>
/// <param name="Target">The target entity of the skill.</param>
/// <param name="Skill">The skill that has been used with the information about the skill.</param>
/// <param name="SkillVNum">The vnum of the skill.</param>
/// <param name="TargetPosition">The position of the target.</param>
/// <param name="Hit"></param>
/// <param name="Damage"></param>
public record SkillUsedEvent
(
    ILivingEntity Caster,
    ILivingEntity Target,
    Skill Skill,
    Position? TargetPosition,
    HitMode? Hit,
    uint Damage
) : IGameEvent;
\ No newline at end of file

A Core/NosSmooth.Game/Events/Characters/CharacterDiedEvent.cs => Core/NosSmooth.Game/Events/Characters/CharacterDiedEvent.cs +14 -0
@@ 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;

/// <summary>
/// The playing character has died.
/// </summary>
public record CharacterDiedEvent(Skill? KillSkill) : IGameEvent;
\ No newline at end of file

A Core/NosSmooth.Game/Events/Characters/CharacterStunEvent.cs => Core/NosSmooth.Game/Events/Characters/CharacterStunEvent.cs +13 -0
@@ 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;

/// <summary>
/// The character has been stunned or unstunned.
/// </summary>
/// <param name="Stunned">Whether the character is stunned.</param>
public record CharacterStunEvent(bool Stunned);
\ No newline at end of file

M Core/NosSmooth.Game/Events/Characters/ReceivedCharacterDataEvent.cs => Core/NosSmooth.Game/Events/Characters/ReceivedCharacterDataEvent.cs +0 -2
@@ 11,10 11,8 @@ namespace NosSmooth.Game.Events.Characters;
/// <summary>
/// Represents received new updated character data.
/// </summary>
/// <param name="OldCharacter">The old data.</param>
/// <param name="Character">The newly received data.</param>
public record ReceivedCharacterDataEvent
(
    Character? OldCharacter,
    Character Character
) : IGameEvent;
\ No newline at end of file

M Core/NosSmooth.Game/Events/Characters/SkillsReceivedEvent.cs => Core/NosSmooth.Game/Events/Characters/SkillsReceivedEvent.cs +4 -0
@@ 8,4 8,8 @@ using NosSmooth.Game.Data.Characters;

namespace NosSmooth.Game.Events.Characters;

/// <summary>
/// Received skills of the character.
/// </summary>
/// <param name="Skills">The skills.</param>
public record SkillsReceivedEvent(Skills Skills) : IGameEvent;
\ No newline at end of file

A Core/NosSmooth.Game/Events/Entities/EntityDiedEvent.cs => Core/NosSmooth.Game/Events/Entities/EntityDiedEvent.cs +21 -0
@@ 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;

/// <summary>
/// An entity has died.
/// </summary>
/// <remarks>
/// Is not emitted for the character, see <see cref="CharacterDiedEvent"/>.
/// </remarks>
/// <param name="Entity">The entity that has died.</param>
/// <param name="KillSkill">The skill that was used to kill the entity, if known.</param>
public record EntityDiedEvent(ILivingEntity Entity, Skill? KillSkill) : IGameEvent;
\ No newline at end of file

A Core/NosSmooth.Game/Events/Entities/EntityJoinedMapEvent.cs => Core/NosSmooth.Game/Events/Entities/EntityJoinedMapEvent.cs +15 -0
@@ 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;

/// <summary>
/// The given entity has joined the map.
/// </summary>
/// <param name="Entity">The entity.</param>
public record EntityJoinedMapEvent(IEntity Entity) : IGameEvent;
\ No newline at end of file

A Core/NosSmooth.Game/Events/Entities/EntityLeftMapEvent.cs => Core/NosSmooth.Game/Events/Entities/EntityLeftMapEvent.cs +21 -0
@@ 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;

/// <summary>
/// An entity has left the map.
/// </summary>
/// <param name="Entity">The entity that has left.</param>
public record EntityLeftMapEvent
(
    IEntity Entity,
    Portal? Portal,
    bool? Died
) : IGameEvent;
\ No newline at end of file

R Core/NosSmooth.Game/Events/Entities/MovedEvent.cs => Core/NosSmooth.Game/Events/Entities/EntityMovedEvent.cs +8 -3
@@ 1,5 1,5 @@
//
//  MovedEvent.cs
//  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.


@@ 9,10 9,15 @@ using NosSmooth.Game.Data.Info;

namespace NosSmooth.Game.Events.Entities;

public record MovedEvent
/// <summary>
/// An entity has moved.
/// </summary>
/// <param name="Entity">The entity.</param>
/// <param name="OldPosition">The previous position of the entity.</param>
/// <param name="NewPosition">The new position of the entity.</param>
public record EntityMovedEvent
(
    IEntity Entity,
    long EntityId,
    Position? OldPosition,
    Position NewPosition
) : IGameEvent;
\ No newline at end of file

A Core/NosSmooth.Game/Events/Entities/EntityRevivedEvent.cs => Core/NosSmooth.Game/Events/Entities/EntityRevivedEvent.cs +15 -0
@@ 0,0 1,15 @@
//
//  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;

namespace NosSmooth.Game.Events.Entities;

/// <summary>
/// The given entity has been revived.
/// </summary>
/// <param name="Entity"></param>
public record EntityRevivedEvent(ILivingEntity Entity);
\ No newline at end of file

A Core/NosSmooth.Game/Events/Entities/ItemDroppedEvent.cs => Core/NosSmooth.Game/Events/Entities/ItemDroppedEvent.cs +15 -0
@@ 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;

/// <summary>
/// An item has been dropped.
/// </summary>
/// <param name="Item">The item that has been dropped.</param>
public record ItemDroppedEvent(IEntity Item) : IGameEvent;
\ No newline at end of file

D Core/NosSmooth.Game/Events/Login/LogoutEvent.cs => Core/NosSmooth.Game/Events/Login/LogoutEvent.cs +0 -14
@@ 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;

/// <summary>
/// Represents event that is emitted if the user is logged out / changing the server etc.
/// </summary>
public class LogoutEvent : IGameEvent
{
}
\ No newline at end of file

A Core/NosSmooth.Game/Events/Map/MapChangedEvent.cs => Core/NosSmooth.Game/Events/Map/MapChangedEvent.cs +18 -0
@@ 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;

/// <summary>
/// A map has been changed.
/// </summary>
/// <param name="PreviousMap">The previous map.</param>
/// <param name="CurrentMap">The new map.</param>
public record MapChangedEvent
(
    Data.Maps.Map? PreviousMap,
    Data.Maps.Map CurrentMap
) : IGameEvent;
\ No newline at end of file

D Core/NosSmooth.Game/Events/Players/SkillUsedEvent.cs => Core/NosSmooth.Game/Events/Players/SkillUsedEvent.cs +0 -13
@@ 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

M Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs => Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs +14 -1
@@ 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<CharacterInitResponder>()
            .AddPacketResponder<SkillResponder>()
            .AddPacketResponder<WalkResponder>()
            .AddPacketResponder<SkillUsedResponder>();
            .AddPacketResponder<SkillUsedResponder>()
            .AddPacketResponder<AoeSkillUsedResponder>()
            .AddPacketResponder<AtResponder>()
            .AddPacketResponder<CMapResponder>()
            .AddPacketResponder<DropResponder>()
            .AddPacketResponder<GpPacketResponder>()
            .AddPacketResponder<InResponder>()
            .AddPacketResponder<MoveResponder>()
            .AddPacketResponder<OutResponder>()
            .AddPacketResponder<StatPacketResponder>()
            .AddPacketResponder<StPacketResponder>()
            .AddPacketResponder<CondPacketResponder>()
            .AddPacketResponder<EqResponder>();

        serviceCollection
            .AddTransient<NostaleChatPacketApi>()

M Core/NosSmooth.Game/Extensions/SkillsExtensions.cs => Core/NosSmooth.Game/Extensions/SkillsExtensions.cs +30 -1
@@ 18,9 18,38 @@ public static class SkillsExtensions
    /// Tries to get the skill of the specified vnum.
    /// </summary>
    /// <param name="skills">The skills of the player.</param>
    /// <param name="castId">The cast id to search for.</param>
    /// <returns>The skill, if found.</returns>
    public static Result<Skill> 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();
    }

    /// <summary>
    /// Tries to get the skill of the specified vnum.
    /// </summary>
    /// <param name="skills">The skills of the player.</param>
    /// <param name="skillVNum">The vnum to search for.</param>
    /// <returns>The skill, if found.</returns>
    public static Result<Skill> TryGetSkill(this Skills skills, long skillVNum)
    public static Result<Skill> TryGetSkillByVNum(this Skills skills, long skillVNum)
    {
        if (skills.PrimarySkill.SkillVNum == skillVNum)
        {

M Core/NosSmooth.Game/Game.cs => Core/NosSmooth.Game/Game.cs +30 -8
@@ 51,7 51,6 @@ public class Game
        internal set
        {
            _currentMap = value;
            MapChanged?.CancelAfter(TimeSpan.FromSeconds(_options.EntityCacheDuration));
        }
    }



@@ 64,11 63,6 @@ public class Game
    public Raid? CurrentRaid { get; internal set; }

    /// <summary>
    /// Cancellation token for changing the map to use in memory cache.
    /// </summary>
    internal CancellationTokenSource? MapChanged { get; private set; }

    /// <summary>
    /// Creates the character if it is null, or updates the current character.
    /// </summary>
    /// <param name="create">The function for creating the character.</param>


@@ 103,9 97,9 @@ public class Game
    /// <param name="releaseSemaphore">Whether to release the semaphore used for changing the map.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>The updated character.</returns>
    internal async Task<Map> CreateMapAsync
    internal async Task<Map?> CreateMapAsync
    (
        Func<Map> create,
        Func<Map?> create,
        bool releaseSemaphore = true,
        CancellationToken ct = default
    )


@@ 120,6 114,34 @@ public class Game
        );
    }

    /// <summary>
    /// Creates the map if it is null, or updates the current map.
    /// </summary>
    /// <param name="create">The function for creating the map.</param>
    /// <param name="update">The function for updating the map.</param>
    /// <param name="releaseSemaphore">Whether to release the semaphore used for changing the map.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>The updated character.</returns>
    internal async Task<Map?> CreateOrUpdateMapAsync
    (
        Func<Map?> create,
        Func<Map?, Map?> update,
        bool releaseSemaphore = true,
        CancellationToken ct = default
    )
    {
        return await CreateOrUpdateAsync<Map?>
        (
            GameSemaphoreType.Map,
            () => CurrentMap,
            m => CurrentMap = m,
            create,
            update,
            releaseSemaphore,
            ct
        );
    }

    private async Task<T> CreateAsync<T>
    (
        GameSemaphoreType type,

A Core/NosSmooth.Game/Helpers/EntityHelpers.cs => Core/NosSmooth.Game/Helpers/EntityHelpers.cs +51 -0
@@ 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;

/// <summary>
/// Helper methods for various operations with entities.
/// </summary>
public static class EntityHelpers
{
    /// <summary>
    /// Create an entity from the given type and id.
    /// </summary>
    /// <param name="type">The entity type.</param>
    /// <param name="entityId">The entity id.</param>
    /// <returns>The entity.</returns>
    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

A Core/NosSmooth.Game/Helpers/EquipmentHelpers.cs => Core/NosSmooth.Game/Helpers/EquipmentHelpers.cs +97 -0
@@ 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;

/// <summary>
/// Helpers for creating equipment from packets.
/// </summary>
public static class EquipmentHelpers
{
    /// <summary>
    /// Create <see cref="Equipment"/> from the given in equipment subpacket.
    /// </summary>
    /// <param name="infoService">The info service.</param>
    /// <param name="equipmentSubPacket">The subpacket.</param>
    /// <param name="weaponUpgradeRare">The weapon upgrade.</param>
    /// <param name="armorUpgradeRare">The armor upgrade.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>The equipment or an error.</returns>
    public static async Task<Equipment> 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<Item?> 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<UpgradeableItem?> 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

M Core/NosSmooth.Game/NosSmooth.Game.csproj => Core/NosSmooth.Game/NosSmooth.Game.csproj +2 -1
@@ 9,11 9,12 @@

    <ItemGroup>
      <Folder Include="Apis" />
      <Folder Include="Data\Quests" />
      <Folder Include="Events\Players" />
      <Folder Include="PacketHandlers" />
    </ItemGroup>

    <ItemGroup>
      <ProjectReference Include="..\..\Data\NosSmooth.Data.Abstractions\NosSmooth.Data.Abstractions.csproj" />
      <ProjectReference Include="..\NosSmooth.Core\NosSmooth.Core.csproj" />
    </ItemGroup>


M Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs => Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs +69 -68
@@ 43,51 43,51 @@ public class CharacterInitResponder : IPacketResponder<CInfoPacket>, 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<CInfoPacket>, 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<CInfoPacket>, 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();

M Core/NosSmooth.Game/PacketHandlers/Characters/SkillResponder.cs => Core/NosSmooth.Game/PacketHandlers/Characters/SkillResponder.cs +42 -11
@@ 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<SkiPacket>
{
    private readonly Game _game;
    private readonly EventDispatcher _eventDispatcher;
    private readonly IInfoService _infoService;
    private readonly ILogger<SkillResponder> _logger;

    /// <summary>
    /// Initializes a new instance of the <see cref="SkillResponder"/> class.
    /// </summary>
    /// <param name="game">The nostale game.</param>
    /// <param name="eventDispatcher">The event dispatcher.</param>
    public SkillResponder(Game game, EventDispatcher eventDispatcher)
    /// <param name="infoService">The info service.</param>
    /// <param name="logger">The logger.</param>
    public SkillResponder
    (
        Game game,
        EventDispatcher eventDispatcher,
        IInfoService infoService,
        ILogger<SkillResponder> logger
    )
    {
        _game = game;
        _eventDispatcher = eventDispatcher;
        _infoService = infoService;
        _logger = logger;
    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<SkiPacket> 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<long>();
        var skillsFromPacket = packet.SkillSubPackets?.Select(x => x.SkillVNum).ToList() ?? new List<int>();
        var skillsFromCharacter = character?.Skills is null
            ? new List<long>()
            ? new List<int>()
            : 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<SkiPacket>

        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<SkiPacket>

        return Result.FromSuccess();
    }

    private async Task<Skill> 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

A Core/NosSmooth.Game/PacketHandlers/Characters/StatPacketResponder.cs => Core/NosSmooth.Game/PacketHandlers/Characters/StatPacketResponder.cs +70 -0
@@ 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;

/// <summary>
/// Responder to stat packet.
/// </summary>
public class StatPacketResponder : IPacketResponder<StatPacket>
{
    private readonly Game _game;

    /// <summary>
    /// Initializes a new instance of the <see cref="StatPacketResponder"/> class.
    /// </summary>
    /// <param name="game">The game.</param>
    public StatPacketResponder(Game game)
    {
        _game = game;
    }

    /// <inheritdoc />
    public Task<Result> Respond(PacketEventArgs<StatPacket> 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

M Core/NosSmooth.Game/PacketHandlers/Characters/WalkResponder.cs => Core/NosSmooth.Game/PacketHandlers/Characters/WalkResponder.cs +20 -38
@@ 38,46 38,28 @@ public class WalkResponder : IPacketResponder<WalkPacket>
    {
        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();
    }

A Core/NosSmooth.Game/PacketHandlers/Entities/AoeSkillUsedResponder.cs => Core/NosSmooth.Game/PacketHandlers/Entities/AoeSkillUsedResponder.cs +101 -0
@@ 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;

/// <summary>
/// Responder to bs packet.
/// </summary>
public class AoeSkillUsedResponder : IPacketResponder<BsPacket>
{
    private readonly Game _game;
    private readonly EventDispatcher _eventDispatcher;
    private readonly IInfoService _infoService;
    private readonly Logger<AoeSkillUsedResponder> _logger;

    /// <summary>
    /// Initializes a new instance of the <see cref="AoeSkillUsedResponder"/> class.
    /// </summary>
    /// <param name="game">The game.</param>
    /// <param name="eventDispatcher">The event dispatcher.</param>
    /// <param name="infoService">The info service.</param>
    /// <param name="logger">The logger.</param>
    public AoeSkillUsedResponder
    (
        Game game,
        EventDispatcher eventDispatcher,
        IInfoService infoService,
        Logger<AoeSkillUsedResponder> logger
    )
    {
        _game = game;
        _eventDispatcher = eventDispatcher;
        _infoService = infoService;
        _logger = logger;

    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<BsPacket> packetArgs, CancellationToken ct = default)
    {
        var packet = packetArgs.Packet;
        var caster = _game.CurrentMap?.Entities.GetEntity<ILivingEntity>
            (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

A Core/NosSmooth.Game/PacketHandlers/Entities/CondPacketResponder.cs => Core/NosSmooth.Game/PacketHandlers/Entities/CondPacketResponder.cs +53 -0
@@ 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;

/// <summary>
/// Responder to cond packet.
/// </summary>
public class CondPacketResponder : IPacketResponder<CondPacket>
{
    private readonly Game _game;

    /// <summary>
    /// Initializes a new instance of the <see cref="CondPacketResponder"/> class.
    /// </summary>
    /// <param name="game">The game.</param>
    public CondPacketResponder(Game game)
    {
        _game = game;
    }

    /// <inheritdoc />
    public Task<Result> Respond(PacketEventArgs<CondPacket> 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<ILivingEntity>(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

A Core/NosSmooth.Game/PacketHandlers/Entities/EqResponder.cs => Core/NosSmooth.Game/PacketHandlers/Entities/EqResponder.cs +71 -0
@@ 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;

/// <summary>
/// Responder to eq packet.
/// </summary>
public class EqResponder : IPacketResponder<EqPacket>
{
    private readonly Game _game;
    private readonly IInfoService _infoService;

    /// <summary>
    /// Initializes a new instance of the <see cref="EqResponder"/> class.
    /// </summary>
    /// <param name="game">The game.</param>
    /// <param name="infoService">The info service.</param>
    public EqResponder(Game game, IInfoService infoService)
    {
        _game = game;
        _infoService = infoService;

    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<EqPacket> packetArgs, CancellationToken ct = default)
    {
        var map = _game.CurrentMap;
        if (map is null)
        {
            return Result.FromSuccess();
        }

        var packet = packetArgs.Packet;
        var entity = map.Entities.GetEntity<Player>(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

M Core/NosSmooth.Game/PacketHandlers/Entities/SkillUsedResponder.cs => Core/NosSmooth.Game/PacketHandlers/Entities/SkillUsedResponder.cs +92 -35
@@ 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<SuPacket>, IPacketResponder<S
{
    private readonly Game _game;
    private readonly EventDispatcher _eventDispatcher;
    private readonly IInfoService _infoService;
    private readonly ILogger<SkillUsedResponder> _logger;

    /// <summary>
    /// Initializes a new instance of the <see cref="SkillUsedResponder"/> class.
    /// </summary>
    /// <param name="game">The game.</param>
    /// <param name="eventDispatcher">The event dispatcher.</param>
    public SkillUsedResponder(Game game, EventDispatcher eventDispatcher)
    /// <param name="infoService">The info service.</param>
    /// <param name="logger">The logger.</param>
    public SkillUsedResponder
    (
        Game game,
        EventDispatcher eventDispatcher,
        IInfoService infoService,
        ILogger<SkillUsedResponder> logger
    )
    {
        _game = game;
        _eventDispatcher = eventDispatcher;
        _infoService = infoService;
        _logger = logger;
    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<SuPacket> 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<ILivingEntity>
            (packet.CasterEntityId) ?? (ILivingEntity)EntityHelpers.CreateEntity
            (packet.CasterEntityType, packet.CasterEntityId);
        var target = map?.Entities?.GetEntity<ILivingEntity>
            (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;
    }

    /// <inheritdoc />


@@ 95,7 152,7 @@ public class SkillUsedResponder : IPacketResponder<SuPacket>, IPacketResponder<S

        if (character is not null && character.Skills is not null)
        {
            var skillResult = character.Skills.TryGetSkill(packet.SkillId);
            var skillResult = character.Skills.TryGetSkillByCastId(packet.SkillId);

            if (skillResult.IsDefined(out var skillEntity))
            {

A Core/NosSmooth.Game/PacketHandlers/Entities/StPacketResponder.cs => Core/NosSmooth.Game/PacketHandlers/Entities/StPacketResponder.cs +85 -0
@@ 0,0 1,85 @@
//
//  StPacketResponder.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.Info;
using NosSmooth.Packets.Server.Entities;
using Remora.Results;

namespace NosSmooth.Game.PacketHandlers.Entities;

/// <summary>
/// Responds to st packet.
/// </summary>
public class StPacketResponder : IPacketResponder<StPacket>
{
    private readonly Game _game;

    /// <summary>
    /// Initializes a new instance of the <see cref="StPacketResponder"/> class.
    /// </summary>
    /// <param name="game">The game.</param>
    public StPacketResponder(Game game)
    {
        _game = game;
    }

    /// <inheritdoc />
    public Task<Result> Respond(PacketEventArgs<StPacket> 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<ILivingEntity>(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

A Core/NosSmooth.Game/PacketHandlers/Map/AtResponder.cs => Core/NosSmooth.Game/PacketHandlers/Map/AtResponder.cs +49 -0
@@ 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;

/// <summary>
/// Responds to at packet.
/// </summary>
public class AtResponder : IPacketResponder<AtPacket>
{
    private readonly Game _game;

    /// <summary>
    /// Initializes a new instance of the <see cref="AtResponder"/> class.
    /// </summary>
    /// <param name="game">The game.</param>
    public AtResponder(Game game)
    {
        _game = game;

    }

    /// <inheritdoc />
    public Task<Result> Respond(PacketEventArgs<AtPacket> 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

A Core/NosSmooth.Game/PacketHandlers/Map/CMapResponder.cs => Core/NosSmooth.Game/PacketHandlers/Map/CMapResponder.cs +91 -0
@@ 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;

/// <summary>
/// Responds to <see cref="CMapResponder"/> by creating a new map.
/// </summary>
public class CMapResponder : IPacketResponder<CMapPacket>
{
    private readonly Game _game;
    private readonly IInfoService _infoService;
    private readonly ILogger<CMapResponder> _logger;

    /// <summary>
    /// Initializes a new instance of the <see cref="CMapResponder"/> class.
    /// </summary>
    /// <param name="game">The nostale game.</param>
    /// <param name="infoService">The info service.</param>
    /// <param name="logger">The logger.</param>
    public CMapResponder(Game game, IInfoService infoService, ILogger<CMapResponder> logger)
    {
        _game = game;
        _infoService = infoService;
        _logger = logger;
    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<CMapPacket> 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<Portal>(),
                        Array.Empty<MinilandObject>()
                    )
                    : new Data.Maps.Map
                    (
                        packet.Id,
                        packet.Type,
                        mapInfoResult.IsSuccess ? mapInfoResult.Entity : null,
                        new MapEntities(),
                        Array.Empty<Portal>()
                    );

                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

A Core/NosSmooth.Game/PacketHandlers/Map/DropResponder.cs => Core/NosSmooth.Game/PacketHandlers/Map/DropResponder.cs +86 -0
@@ 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;

/// <summary>
/// Responds to drop packet.
/// </summary>
public class DropResponder : IPacketResponder<DropPacket>
{
    private readonly Game _game;
    private readonly EventDispatcher _eventDispatcher;
    private readonly IInfoService _infoService;
    private readonly ILogger<DropResponder> _logger;

    /// <summary>
    /// Initializes a new instance of the <see cref="DropResponder"/> class.
    /// </summary>
    /// <param name="game">The game.</param>
    /// <param name="eventDispatcher">The event dispatcher.</param>
    /// <param name="infoService">The info service.</param>
    /// <param name="logger">The logger.</param>
    public DropResponder
    (
        Game game,
        EventDispatcher eventDispatcher,
        IInfoService infoService,
        ILogger<DropResponder> logger
    )
    {
        _game = game;
        _eventDispatcher = eventDispatcher;
        _infoService = infoService;
        _logger = logger;

    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<DropPacket> 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

A Core/NosSmooth.Game/PacketHandlers/Map/GpPacketResponder.cs => Core/NosSmooth.Game/PacketHandlers/Map/GpPacketResponder.cs +60 -0
@@ 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;

/// <summary>
/// Responder to gp packet.
/// </summary>
public class GpPacketResponder : IPacketResponder<GpPacket>
{
    private readonly Game _game;

    /// <summary>
    /// Initializes a new instance of the <see cref="GpPacketResponder"/> class.
    /// </summary>
    /// <param name="game">The game.</param>
    public GpPacketResponder(Game game)
    {
        _game = game;
    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<GpPacket> 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

A Core/NosSmooth.Game/PacketHandlers/Map/InResponder.cs => Core/NosSmooth.Game/PacketHandlers/Map/InResponder.cs +201 -0
@@ 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;

/// <summary>
/// Responds to in packet.
/// </summary>
public class InResponder : IPacketResponder<InPacket>
{
    private readonly Game _game;
    private readonly EventDispatcher _eventDispatcher;
    private readonly IInfoService _infoService;
    private readonly ILogger<InResponder> _logger;

    /// <summary>
    /// Initializes a new instance of the <see cref="InResponder"/> class.
    /// </summary>
    /// <param name="game">The game.</param>
    /// <param name="eventDispatcher">The event dispatcher.</param>
    /// <param name="infoService">The info service.</param>
    /// <param name="logger">The logger.</param>
    public InResponder
    (
        Game game,
        EventDispatcher eventDispatcher,
        IInfoService infoService,
        ILogger<InResponder> logger
    )
    {
        _game = game;
        _eventDispatcher = eventDispatcher;
        _infoService = infoService;
        _logger = logger;
    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<InPacket> 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<IEntity> 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<GroundItem> 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<Player> 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<Monster> 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

A Core/NosSmooth.Game/PacketHandlers/Map/MoveResponder.cs => Core/NosSmooth.Game/PacketHandlers/Map/MoveResponder.cs +53 -0
@@ 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;

/// <summary>
/// Responds to move packet.
/// </summary>
public class MoveResponder : IPacketResponder<MovePacket>
{
    private readonly Game _game;

    /// <summary>
    /// Initializes a new instance of the <see cref="MoveResponder"/> class.
    /// </summary>
    /// <param name="game">The nostale game.</param>
    public MoveResponder(Game game)
    {
        _game = game;
    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<MovePacket> 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<ILivingEntity>(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

A Core/NosSmooth.Game/PacketHandlers/Map/OutResponder.cs => Core/NosSmooth.Game/PacketHandlers/Map/OutResponder.cs +67 -0
@@ 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;

/// <summary>
/// Responds to out packet.
/// </summary>
public class OutResponder : IPacketResponder<OutPacket>
{
    private readonly Game _game;
    private readonly EventDispatcher _eventDispatcher;

    /// <summary>
    /// Initializes a new instance of the <see cref="OutResponder"/> class.
    /// </summary>
    /// <param name="game">The game.</param>
    /// <param name="eventDispatcher">The event dispatcher.</param>
    public OutResponder(Game game, EventDispatcher eventDispatcher)
    {
        _game = game;
        _eventDispatcher = eventDispatcher;
    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<OutPacket> 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

Do not follow this link