~ruther/NosSmooth

d4b8ba8277386498e34e5dff5f81aaadd210707a — František Boháček 3 years ago 5716978 + 89662d9
Merge pull request #8 from Rutherther/game-objects

Game objects
107 files changed, 4983 insertions(+), 157 deletions(-)

M Core/NosSmooth.Core/Packets/IPacketResponder.cs
A Core/NosSmooth.Game/Apis/NostaleChatPacketApi.cs
A Core/NosSmooth.Game/Apis/NostaleSkillsPacketApi.cs
A Core/NosSmooth.Game/Data/Act4/Act4Raid.cs
A Core/NosSmooth.Game/Data/Act4/Act4Status.cs
A Core/NosSmooth.Game/Data/Characters/Character.cs
A Core/NosSmooth.Game/Data/Characters/Skill.cs
A Core/NosSmooth.Game/Data/Characters/Skills.cs
A Core/NosSmooth.Game/Data/Chat/ChatMessage.cs
A Core/NosSmooth.Game/Data/Chat/DirectMessage.cs
A Core/NosSmooth.Game/Data/Chat/Friend.cs
A Core/NosSmooth.Game/Data/Dialogs/Dialog.cs
A Core/NosSmooth.Game/Data/Entities/GroundItem.cs
A Core/NosSmooth.Game/Data/Entities/IEntity.cs
A Core/NosSmooth.Game/Data/Entities/IPet.cs
A Core/NosSmooth.Game/Data/Entities/LivingEntity.cs
A Core/NosSmooth.Game/Data/Entities/Monster.cs
A Core/NosSmooth.Game/Data/Entities/Npc.cs
A Core/NosSmooth.Game/Data/Entities/Partner.cs
A Core/NosSmooth.Game/Data/Entities/Pet.cs
A Core/NosSmooth.Game/Data/Entities/Player.cs
A Core/NosSmooth.Game/Data/Info/Health.cs
A Core/NosSmooth.Game/Data/Info/Level.cs
A Core/NosSmooth.Game/Data/Info/Morph.cs
A Core/NosSmooth.Game/Data/Info/Position.cs
A Core/NosSmooth.Game/Data/Inventory/Bag.cs
A Core/NosSmooth.Game/Data/Inventory/Inventory.cs
A Core/NosSmooth.Game/Data/Inventory/InventoryItem.cs
A Core/NosSmooth.Game/Data/Items/Equipment.cs
A Core/NosSmooth.Game/Data/Items/Fairy.cs
A Core/NosSmooth.Game/Data/Items/Item.cs
A Core/NosSmooth.Game/Data/Items/UpgradeableItem.cs
A Core/NosSmooth.Game/Data/Login/Channel.cs
A Core/NosSmooth.Game/Data/Login/WorldServer.cs
A Core/NosSmooth.Game/Data/Maps/Map.cs
A Core/NosSmooth.Game/Data/Maps/MapEntities.cs
A Core/NosSmooth.Game/Data/Maps/Miniland.cs
A Core/NosSmooth.Game/Data/Maps/MinilandObject.cs
A Core/NosSmooth.Game/Data/Maps/Portal.cs
A Core/NosSmooth.Game/Data/Maps/Timespace.cs
A Core/NosSmooth.Game/Data/Raids/Raid.cs
A Core/NosSmooth.Game/Data/Social/Family.cs
A Core/NosSmooth.Game/Data/Social/Group.cs
A Core/NosSmooth.Game/Errors/SkillOnCooldownError.cs
A Core/NosSmooth.Game/Events/Battle/AoESkillUsedEvent.cs
A Core/NosSmooth.Game/Events/Battle/SkillUsedEvent.cs
A Core/NosSmooth.Game/Events/Characters/ReceivedCharacterDataEvent.cs
A Core/NosSmooth.Game/Events/Characters/SkillReadyEvent.cs
A Core/NosSmooth.Game/Events/Characters/SkillsReceivedEvent.cs
R Core/NosSmooth.Game/Events/{Handlers/EventDispatcher.cs => Core/EventDispatcher.cs}
R Core/NosSmooth.Game/Events/{Handlers/IGameResponder.cs => Core/IGameResponder.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
A Core/NosSmooth.Game/Events/Entities/EntityMovedEvent.cs
A Core/NosSmooth.Game/Events/Entities/EntityRevivedEvent.cs
A Core/NosSmooth.Game/Events/Entities/EntityStunnedEvent.cs
A Core/NosSmooth.Game/Events/Entities/ItemDroppedEvent.cs
A Core/NosSmooth.Game/Events/Map/MapChangedEvent.cs
M Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs
A Core/NosSmooth.Game/Extensions/SkillsExtensions.cs
A Core/NosSmooth.Game/Game.cs
A Core/NosSmooth.Game/GameOptions.cs
A Core/NosSmooth.Game/GameSemaphoreType.cs
A Core/NosSmooth.Game/GameSemaphores.cs
A Core/NosSmooth.Game/Helpers/EntityHelpers.cs
A Core/NosSmooth.Game/Helpers/EquipmentHelpers.cs
A Core/NosSmooth.Game/IsExternalInit.cs
M Core/NosSmooth.Game/NosSmooth.Game.csproj
A Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs
A Core/NosSmooth.Game/PacketHandlers/Characters/SkillResponder.cs
A Core/NosSmooth.Game/PacketHandlers/Characters/StatPacketResponder.cs
A 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
A 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/PacketHandlers/Specialists/SpResponder.cs
M NosSmooth.sln
M Packets/NosSmooth.Packets/Client/Battle/UseAOESkillPacket.cs
M Packets/NosSmooth.Packets/Client/Battle/UseSkillPacket.cs
A Packets/NosSmooth.Packets/Enums/MinilandState.cs
M Packets/NosSmooth.Packets/NosSmooth.Packets.csproj
M Packets/NosSmooth.Packets/Server/Maps/OutPacket.cs
A Packets/NosSmooth.Packets/Server/Miniland/MlInfoBrPacket.cs
A Packets/NosSmooth.Packets/Server/Miniland/MlInfoPacket.cs
A Packets/NosSmooth.Packets/Server/Miniland/MlObjLstPacket.cs
A Packets/NosSmooth.Packets/Server/Miniland/MlObjPacket.cs
A Packets/NosSmooth.Packets/Server/Miniland/MltObjPacket.cs
A Packets/NosSmooth.Packets/Server/Miniland/MltObjSubPacket.cs
M Packets/NosSmooth.Packets/Server/Players/CInfoPacket.cs
M Packets/NosSmooth.Packets/Server/Players/LevPacket.cs
A Packets/NosSmooth.Packets/Server/Specialists/SdPacket.cs
A Packets/NosSmooth.Packets/Server/Specialists/SpPacket.cs
A Samples/FileClient/App.cs
A Samples/FileClient/Client.cs
A Samples/FileClient/FileClient.csproj
A Samples/FileClient/Program.cs
M Tests/NosSmooth.Packets.Tests/Converters/Packets/InPacketConverterTests.cs
M Core/NosSmooth.Core/Packets/IPacketResponder.cs => Core/NosSmooth.Core/Packets/IPacketResponder.cs +4 -4
@@ 29,10 29,10 @@ public interface IPacketResponder<TPacket> : IPacketResponder
    /// <summary>
    /// Respond to the given packet.
    /// </summary>
    /// <param name="packet">The packet to respond to.</param>
    /// <param name="packetArgs">The packet to respond to.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> Respond(PacketEventArgs<TPacket> packet, CancellationToken ct = default);
    public Task<Result> Respond(PacketEventArgs<TPacket> packetArgs, CancellationToken ct = default);
}

/// <summary>


@@ 43,10 43,10 @@ public interface IEveryPacketResponder : IPacketResponder
    /// <summary>
    /// Respond to the given packet.
    /// </summary>
    /// <param name="packet">The packet to respond to.</param>
    /// <param name="packetArgs">The packet to respond to.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <typeparam name="TPacket">The type of the packet.</typeparam>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> Respond<TPacket>(PacketEventArgs<TPacket> packet, CancellationToken ct = default)
    public Task<Result> Respond<TPacket>(PacketEventArgs<TPacket> packetArgs, CancellationToken ct = default)
        where TPacket : IPacket;
}

A Core/NosSmooth.Game/Apis/NostaleChatPacketApi.cs => Core/NosSmooth.Game/Apis/NostaleChatPacketApi.cs +91 -0
@@ 0,0 1,91 @@
//
//  NostaleChatPacketApi.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.Client;
using NosSmooth.Packets.Enums;
using NosSmooth.Packets.Enums.Chat;
using NosSmooth.Packets.Server.Chat;
using Remora.Results;

namespace NosSmooth.Game.Apis;

/// <summary>
/// Packet api for sending and receiving messages.
/// </summary>
public class NostaleChatPacketApi
{
    // TODO: check length of the messages
    private readonly INostaleClient _client;

    /// <summary>
    /// Initializes a new instance of the <see cref="NostaleChatPacketApi"/> class.
    /// </summary>
    /// <param name="client">The nostale client.</param>
    public NostaleChatPacketApi(INostaleClient client)
    {
        _client = client;
    }

    /// <summary>
    /// Receive the given system message on the client.
    /// </summary>
    /// <remarks>
    /// Won't send anything to the server, it's just the client who will see the message.
    /// </remarks>
    /// <param name="content">The content of the message.</param>
    /// <param name="color">The color of the message.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> ReceiveSystemMessageAsync(string content, SayColor color = SayColor.Yellow, CancellationToken ct = default)
        => _client.ReceivePacketAsync(new SayPacket(EntityType.Map, 0, color, content), ct);

    /// <summary>
    /// Sends the given message to the public chat.
    /// </summary>
    /// <param name="content">The content of the message.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> SendMessageAsync(string content, CancellationToken ct = default)
        => _client.SendPacketAsync(new Packets.Client.Chat.SayPacket(content), ct);

    /// <summary>
    /// Sends the given message to the family chat.
    /// </summary>
    /// <remarks>
    /// Should be used only if the user is in a family.
    /// </remarks>
    /// <param name="content">The content of the message.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> SendFamilyMessage(string content, CancellationToken ct = default)
        => _client.SendPacketAsync(":" + content, ct);

    /// <summary>
    /// Sends the given message to the group chat.
    /// </summary>
    /// <remarks>
    /// Should be used only if the user is in a group. (with people, not only pets).
    /// </remarks>
    /// <param name="content">The content of the message.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> SendGroupMessage(string content, CancellationToken ct = default)
        => _client.SendPacketAsync(";" + content, ct);

    /// <summary>
    /// Sends the given message to the target only.
    /// </summary>
    /// <remarks>
    /// Won't return if the whisper has actually came through, event has to be hooked
    /// up to know if the whisper has went through (and you can know only for messages that are sufficiently long).
    /// </remarks>
    /// <param name="targetName">The name of the user you want to whisper to.</param>
    /// <param name="content">The content of the message.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> SendWhisper(string targetName, string content, CancellationToken ct = default)
        => _client.SendPacketAsync($"/{targetName} {content}", ct);
}
\ No newline at end of file

A Core/NosSmooth.Game/Apis/NostaleSkillsPacketApi.cs => Core/NosSmooth.Game/Apis/NostaleSkillsPacketApi.cs +248 -0
@@ 0,0 1,248 @@
//
//  NostaleSkillsPacketApi.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.Client;
using NosSmooth.Game.Data.Characters;
using NosSmooth.Game.Data.Entities;
using NosSmooth.Game.Errors;
using NosSmooth.Packets.Client.Battle;
using NosSmooth.Packets.Enums;
using Remora.Results;

namespace NosSmooth.Game.Apis;

/// <summary>
/// Packet api for using character skills.
/// </summary>
public class NostaleSkillsPacketApi
{
    private readonly INostaleClient _client;

    /// <summary>
    /// Initializes a new instance of the <see cref="NostaleSkillsPacketApi"/> class.
    /// </summary>
    /// <param name="client">The nostale client.</param>
    public NostaleSkillsPacketApi(INostaleClient client)
    {
        _client = client;
    }

    /// <summary>
    /// Use the given (targetable) skill on specified entity.
    /// </summary>
    /// <remarks>
    /// For skills that can be used only on self, use <paramref name="entityId"/> of the character.
    /// For skills that cannot be targeted on an entity, proceed to <see cref="UseSkillAt"/>.
    /// </remarks>
    /// <param name="skillVNum">The id of the skill.</param>
    /// <param name="entityId">The id of the entity to use the skill on.</param>
    /// <param name="entityType">The type of the supplied entity.</param>
    /// <param name="mapX">The x coordinate on the map. (Used for non targeted dashes etc., says where the dash will be to.)</param>
    /// <param name="mapY">The y coordinate on the map. (Used for non targeted dashes etc., says where the dash will be to.)</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> UseSkillOn
    (
        long skillVNum,
        long entityId,
        EntityType entityType,
        short? mapX = default,
        short? mapY = default,
        CancellationToken ct = default
    )
    {
        return _client.SendPacketAsync
        (
            new UseSkillPacket
            (
                skillVNum,
                entityType,
                entityId,
                mapX,
                mapY
            ),
            ct
        );
    }

    /// <summary>
    /// Use the given (targetable) skill on specified entity.
    /// </summary>
    /// <remarks>
    /// For skills that can be used only on self, use <paramref name="entityId"/> of the character.
    /// For skills that cannot be targeted on an entity, proceed to <see cref="UseSkillAt"/>.
    /// </remarks>
    /// <param name="skillVNum">The id of the skill.</param>
    /// <param name="entity">The entity to use the skill on.</param>
    /// <param name="mapX">The x coordinate on the map. (Used for non targeted dashes etc., says where the dash will be to.)</param>
    /// <param name="mapY">The y coordinate on the map. (Used for non targeted dashes etc., says where the dash will be to.)</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> UseSkillOn
    (
        long skillVNum,
        ILivingEntity entity,
        short? mapX = default,
        short? mapY = default,
        CancellationToken ct = default
    )
    {
        return _client.SendPacketAsync
        (
            new UseSkillPacket
            (
                skillVNum,
                entity.Type,
                entity.Id,
                mapX,
                mapY
            ),
            ct
        );
    }

    /// <summary>
    /// Use the given (targetable) skill on specified entity.
    /// </summary>
    /// <remarks>
    /// The skill won't be used if it is on cooldown.
    /// For skills that can be used only on self, use <paramref name="entityId"/> of the character.
    /// For skills that cannot be targeted on an entity, proceed to <see cref="UseSkillAt"/>.
    /// </remarks>
    /// <param name="skill">The skill to use.</param>
    /// <param name="entity">The entity to use the skill on.</param>
    /// <param name="mapX">The x coordinate on the map. (Used for non targeted dashes etc., says where the dash will be to.)</param>
    /// <param name="mapY">The y coordinate on the map. (Used for non targeted dashes etc., says where the dash will be to.)</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> UseSkillOn
    (
        Skill skill,
        ILivingEntity entity,
        short? mapX = default,
        short? mapY = default,
        CancellationToken ct = default
    )
    {
        if (skill.IsOnCooldown)
        {
            return Task.FromResult<Result>(new SkillOnCooldownError(skill));
        }

        return _client.SendPacketAsync
        (
            new UseSkillPacket
            (
                skill.SkillVNum,
                entity.Type,
                entity.Id,
                mapX,
                mapY
            ),
            ct
        );
    }

    /// <summary>
    /// Use the given (targetable) skill on specified entity.
    /// </summary>
    /// <remarks>
    /// For skills that can be used only on self, use <paramref name="entityId"/> of the character.
    /// For skills that cannot be targeted on an entity, proceed to <see cref="UseSkillAt"/>.
    /// </remarks>
    /// <param name="skill">The skill to use.</param>
    /// <param name="entityId">The id of the entity to use the skill on.</param>
    /// <param name="entityType">The type of the supplied entity.</param>
    /// <param name="mapX">The x coordinate on the map. (Used for non targeted dashes etc., says where the dash will be to.)</param>
    /// <param name="mapY">The y coordinate on the map. (Used for non targeted dashes etc., says where the dash will be to.)</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> UseSkillOn
    (
        Skill skill,
        long entityId,
        EntityType entityType,
        short? mapX = default,
        short? mapY = default,
        CancellationToken ct = default
    )
    {
        if (skill.IsOnCooldown)
        {
            return Task.FromResult<Result>(new SkillOnCooldownError(skill));
        }

        return _client.SendPacketAsync
        (
            new UseSkillPacket
            (
                skill.SkillVNum,
                entityType,
                entityId,
                mapX,
                mapY
            ),
            ct
        );
    }

    /// <summary>
    /// Use the given (aoe) skill on the specified place.
    /// </summary>
    /// <remarks>
    /// For skills that can have targets, proceed to <see cref="UseSkillOn"/>.
    /// </remarks>
    /// <param name="skillVNum">The id of the skill.</param>
    /// <param name="mapX">The x coordinate to use the skill at.</param>
    /// <param name="mapY">The y coordinate to use the skill at.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> UseSkillAt
    (
        long skillVNum,
        short mapX,
        short mapY,
        CancellationToken ct = default
    )
    {
        return _client.SendPacketAsync
        (
            new UseAOESkillPacket(skillVNum, mapX, mapY),
            ct
        );
    }

    /// <summary>
    /// Use the given (aoe) skill on the specified place.
    /// </summary>
    /// <remarks>
    /// For skills that can have targets, proceed to <see cref="UseSkillOn"/>.
    /// </remarks>
    /// <param name="skill">The skill to use.</param>
    /// <param name="mapX">The x coordinate to use the skill at.</param>
    /// <param name="mapY">The y coordinate to use the skill at.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> UseSkillAt
    (
        Skill skill,
        short mapX,
        short mapY,
        CancellationToken ct = default
    )
    {
        if (skill.IsOnCooldown)
        {
            return Task.FromResult<Result>(new SkillOnCooldownError(skill));
        }

        return _client.SendPacketAsync
        (
            new UseAOESkillPacket(skill.SkillVNum, mapX, mapY),
            ct
        );
    }
}
\ No newline at end of file

A Core/NosSmooth.Game/Data/Act4/Act4Raid.cs => Core/NosSmooth.Game/Data/Act4/Act4Raid.cs +33 -0
@@ 0,0 1,33 @@
//
//  Act4Raid.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.Data.Act4;

/// <summary>
/// Represents type of raid in act4.
/// </summary>
public enum Act4Raid
{
    /// <summary>
    /// Fire element raid with the boss Morcos.
    /// </summary>
    Morcos,

    /// <summary>
    /// Shadow element raid with the boss Hatus.
    /// </summary>
    Hatus,

    /// <summary>
    /// Water element raid with the boss Calvina.
    /// </summary>
    Calvina,

    /// <summary>
    /// Light element raid with the boss Berios.
    /// </summary>
    Berios
}
\ No newline at end of file

A Core/NosSmooth.Game/Data/Act4/Act4Status.cs => Core/NosSmooth.Game/Data/Act4/Act4Status.cs +26 -0
@@ 0,0 1,26 @@
//
//  Act4Status.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.Packets.Enums;

namespace NosSmooth.Game.Data.Act4;

/// <summary>
/// Status of a faction in act4
/// </summary>
/// <param name="Percentage">The percentage to Mukraju.</param>
/// <param name="Mode">The current mode.</param>
/// <param name="CurrentTime">The current time of the raid.</param>
/// <param name="TotalTime">The total time the raid will be for.</param>
/// <param name="Raid">The type of the raid.</param>
public record Act4FactionStatus
(
    short Percentage,
    Act4Mode Mode,
    long? CurrentTime,
    long? TotalTime,
    Act4Raid Raid
);
\ No newline at end of file

A Core/NosSmooth.Game/Data/Characters/Character.cs => Core/NosSmooth.Game/Data/Characters/Character.cs +92 -0
@@ 0,0 1,92 @@
//
//  Character.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.Chat;
using NosSmooth.Game.Data.Entities;
using NosSmooth.Game.Data.Info;
using NosSmooth.Game.Data.Social;
using NosSmooth.Packets.Enums;
using NosSmooth.Packets.Enums.Players;

namespace NosSmooth.Game.Data.Characters;

/// <summary>
/// Represents the client character.
/// </summary>
public class Character : Player
{
    /// <summary>
    /// Gets or sets the inventory of the character.
    /// </summary>
    public Inventory.Inventory? Inventory { get; set; }

    /// <summary>
    /// Get or sets the friends of the character.
    /// </summary>
    public IReadOnlyList<Friend>? Friends { get; set; }

    /// <summary>
    /// Gets or sets the skills of the player.
    /// </summary>
    public Skills? Skills { get; set; }

    /// <summary>
    /// Gets or sets the group the player is in.
    /// </summary>
    public Group? Group { get; set; }

    /// <summary>
    /// Gets or sets the c skill points.
    /// </summary>
    public int? SkillCp { get; set; }

    /// <summary>
    /// Gets or sets the job level.
    /// </summary>
    public Level? JobLevel { get; set; }

    /// <summary>
    /// Gets or sets the player level.
    /// </summary>
    public Level? PlayerLevel { get; set; }

    /// <summary>
    /// Gets or sets the player level.
    /// </summary>
    public Level? HeroLevelStruct { get; set; }

    /// <inheritdoc/>
    public override short? HeroLevel
    {
        get => HeroLevelStruct?.Lvl;
        set
        {
            if (HeroLevelStruct is not null && value is not null)
            {
                HeroLevelStruct = HeroLevelStruct with
                {
                    Lvl = value.Value
                };
            }
        }
    }

    /// <summary>
    /// Gets or sets the sp points of the player.
    /// </summary>
    /// <remarks>
    /// Resets every day, max 10 000.
    /// </remarks>
    public int SpPoints { get; set; }

    /// <summary>
    /// Gets or sets the additional sp points of the player.
    /// </summary>
    /// <remarks>
    /// Used if <see cref="SpPoints"/> are 0. Max 1 000 000.
    /// </remarks>
    public int AdditionalSpPoints { get; set; }
}
\ No newline at end of file

A Core/NosSmooth.Game/Data/Characters/Skill.cs => Core/NosSmooth.Game/Data/Characters/Skill.cs +31 -0
@@ 0,0 1,31 @@
//
//  Skill.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.Data.Abstractions.Infos;

namespace NosSmooth.Game.Data.Characters;

/// <summary>
/// Represents nostale skill entity.
/// </summary>
/// <param name="SkillVNum">The vnum of the skill.</param>
/// <param name="Level">The level of the skill. Unknown feature.</param>
public record Skill(int SkillVNum, int? Level = default, ISkillInfo? Info = default)
{
    /// <summary>
    /// Gets the last time this skill was used.
    /// </summary>
    public DateTimeOffset LastUseTime { get; internal set; }

    /// <summary>
    /// Gets whether the skill is on cooldown.
    /// </summary>
    /// <remarks>
    /// This is set when the server sends sr packet,
    /// prefer to use this instead of checking the LastUseTime and Cooldown.
    /// </remarks>
    public bool IsOnCooldown { get; internal set; }
}
\ No newline at end of file

A Core/NosSmooth.Game/Data/Characters/Skills.cs => Core/NosSmooth.Game/Data/Characters/Skills.cs +20 -0
@@ 0,0 1,20 @@
//
//  Skills.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.Data.Characters;

/// <summary>
/// Holds skill of a Character.
/// </summary>
/// <param name="PrimarySkill">The VNum of the primary skill. This skill is used with the primary weapon. (Could be different for sp cards.)</param>
/// <param name="SecondarySkill">The VNum of the secondary skill. This skill is used with the secondary weapon. (Could be different for sp cards)</param>
/// <param name="OtherSkills">The VNums of other skills.</param>
public record Skills
(
    Skill PrimarySkill,
    Skill SecondarySkill,
    IReadOnlyList<Skill> OtherSkills
);
\ No newline at end of file

A Core/NosSmooth.Game/Data/Chat/ChatMessage.cs => Core/NosSmooth.Game/Data/Chat/ChatMessage.cs +17 -0
@@ 0,0 1,17 @@
//
//  ChatMessage.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.Data.Chat;

/// <summary>
/// Message received from user in chat.
/// </summary>
/// <param name="CharacterId">The id of the character.</param>
/// <param name="Player">The player </param>
/// <param name="Message">The message sent from the friend.</param>
public record ChatMessage(long CharacterId, Player? Player, string Message);
\ No newline at end of file

A Core/NosSmooth.Game/Data/Chat/DirectMessage.cs => Core/NosSmooth.Game/Data/Chat/DirectMessage.cs +15 -0
@@ 0,0 1,15 @@
//
//  DirectMessage.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.Data.Chat;

/// <summary>
/// Message received from a friend.
/// </summary>
/// <param name="CharacterId">The id of the character.</param>
/// <param name="Friend">The friend from which the message is. May be null if the client did not receive friend packet.</param>
/// <param name="Message">The message sent from the friend.</param>
public record DirectMessage(long CharacterId, Friend? Friend, string? Message);
\ No newline at end of file

A Core/NosSmooth.Game/Data/Chat/Friend.cs => Core/NosSmooth.Game/Data/Chat/Friend.cs +33 -0
@@ 0,0 1,33 @@
//
//  Friend.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.Data.Chat;

/// <summary>
/// Represents character's friend.
/// </summary>
public class Friend
{
    /// <summary>
    /// The id of the character.
    /// </summary>
    public long CharacterId { get; internal set; }

    /// <summary>
    /// The type of the relation.
    /// </summary>
    // public CharacterRelationType RelationType { get; internal set; }

    /// <summary>
    /// The name of the character.
    /// </summary>
    public string? CharacterName { get; internal set; }

    /// <summary>
    /// Whether the friend is connected to the server.
    /// </summary>
    public bool IsOnline { get; internal set; }
}
\ No newline at end of file

A Core/NosSmooth.Game/Data/Dialogs/Dialog.cs => Core/NosSmooth.Game/Data/Dialogs/Dialog.cs +21 -0
@@ 0,0 1,21 @@
//
//  Dialog.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.Data.Dialogs;

/// <summary>
/// Represents dialog sent by the server
/// </summary>
/// <param name="AcceptCommand"></param>
/// <param name="Message"></param>
/// <param name="Parameters"></param>
public record Dialog
(
    string AcceptCommand,

    // OneOf<Game18NConstString, string> Message,
    IReadOnlyList<string> Parameters
);
\ No newline at end of file

A Core/NosSmooth.Game/Data/Entities/GroundItem.cs => Core/NosSmooth.Game/Data/Entities/GroundItem.cs +54 -0
@@ 0,0 1,54 @@
//
//  GroundItem.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.Data.Abstractions.Infos;
using NosSmooth.Game.Data.Info;
using NosSmooth.Packets.Enums;

namespace NosSmooth.Game.Data.Entities;

/// <summary>
/// The item on the ground.
/// </summary>
public class GroundItem : IEntity
{
    /// <summary>
    /// Gets or sets the id of the owner, if any.
    /// </summary>
    public long? OwnerId { get; set; }

    /// <summary>
    /// Gets or sets the amount of the item on the ground.
    /// </summary>
    public int Amount { get; internal set; }

    /// <summary>
    /// Gets or sets whether the item is for a quest.
    /// </summary>
    public bool IsQuestRelated { get; internal set; }

    /// <summary>
    /// Gets or sets the info about the item, if available.
    /// </summary>
    public IItemInfo? ItemInfo { get; internal set; }

    /// <summary>
    /// Gets the VNum of the npc.
    /// </summary>
    public int VNum { get; internal set; }

    /// <inheritdoc/>
    public long Id { get; set; }

    /// <inheritdoc/>
    public Position? Position { get; set; }

    /// <inheritdoc/>
    public EntityType Type
    {
        get => EntityType.Object;
    }
}
\ No newline at end of file

A Core/NosSmooth.Game/Data/Entities/IEntity.cs => Core/NosSmooth.Game/Data/Entities/IEntity.cs +31 -0
@@ 0,0 1,31 @@
//
//  IEntity.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.Info;
using NosSmooth.Packets.Enums;

namespace NosSmooth.Game.Data.Entities;

/// <summary>
/// Base type for entities.
/// </summary>
public interface IEntity
{
    /// <summary>
    /// Gets the id of the entity.
    /// </summary>
    public long Id { get; set; }

    /// <summary>
    /// Gets the position of the entity.
    /// </summary>
    public Position? Position { get; set; }

    /// <summary>
    /// Gets the type of the entity.
    /// </summary>
    public EntityType Type { get; }
}
\ No newline at end of file

A Core/NosSmooth.Game/Data/Entities/IPet.cs => Core/NosSmooth.Game/Data/Entities/IPet.cs +14 -0
@@ 0,0 1,14 @@
//
//  IPet.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.Data.Entities;

/// <summary>
/// Represents base type for a pet or a partner.
/// </summary>
public interface IPet
{
}
\ No newline at end of file

A Core/NosSmooth.Game/Data/Entities/LivingEntity.cs => Core/NosSmooth.Game/Data/Entities/LivingEntity.cs +81 -0
@@ 0,0 1,81 @@
//
//  LivingEntity.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.Info;
using NosSmooth.Packets.Enums;

namespace NosSmooth.Game.Data.Entities;

/// <summary>
/// Represents any nostale living entity such as monster, npc, player.
/// </summary>
public interface ILivingEntity : IEntity
{
    /// <summary>
    /// Gets the speed of the entity. May be null if unknown.
    /// </summary>
    public int? Speed { get; set; }

    /// <summary>
    /// Gets or sets whether the player is invisible.
    /// </summary>
    public bool? IsInvisible { get; set; }

    /// <summary>
    /// Gets the level of the entity. May be null if unknown.
    /// </summary>
    public ushort? Level { get; set; }

    /// <summary>
    /// Gets the direction the entity is looking. May be null if unknown.
    /// </summary>
    public byte? Direction { get; set; }

    /// <summary>
    /// Gets the percentage of the health points of the entity. May be null if unknown.
    /// </summary>
    public Health? Hp { get; set; }

    /// <summary>
    /// Gets the percentage of the mana points of the entity. May be null if unknown.
    /// </summary>
    public Health? Mp { get; set; }

    /// <summary>
    /// Gets the faction of the entity. May be null if unknown.
    /// </summary>
    public FactionType? Faction { get; set; }

    /// <summary>
    /// Gets the size of the entity.
    /// </summary>
    public short Size { get; set; }

    /// <summary>
    /// Gets the VNums of the effects the entity has.
    /// </summary>
    public IReadOnlyList<long>? EffectsVNums { get; set; }

    /// <summary>
    /// Gets the name of the entity. May be null if unknown.
    /// </summary>
    public string? Name { get; set; }

    /// <summary>
    /// Gets or sets whether the entity is sitting.
    /// </summary>
    public bool IsSitting { get; set; }

    /// <summary>
    /// Gets or sets whether the entity cannot move.
    /// </summary>
    public bool CantMove { get; set; }

    /// <summary>
    /// Gets or sets whether the entity cannot attack.
    /// </summary>
    public bool CantAttack { get; set; }
}
\ No newline at end of file

A Core/NosSmooth.Game/Data/Entities/Monster.cs => Core/NosSmooth.Game/Data/Entities/Monster.cs +75 -0
@@ 0,0 1,75 @@
//
//  Monster.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.Data.Abstractions.Infos;
using NosSmooth.Game.Data.Info;
using NosSmooth.Packets.Enums;

namespace NosSmooth.Game.Data.Entities;

/// <summary>
/// Represents nostale monster entity.
/// </summary>
public class Monster : ILivingEntity
{
    /// <summary>
    /// Gets or sets the monster info.
    /// </summary>
    public IMonsterInfo? MonsterInfo { get; set; }

    /// <summary>
    /// Gets the VNum of the monster.
    /// </summary>
    public int VNum { get; set; }

    /// <inheritdoc/>
    public long Id { get; set; }

    /// <inheritdoc/>
    public string? Name { get; set; }

    /// <inheritdoc />
    public bool IsSitting { get; set; }

    /// <inheritdoc />
    public bool CantMove { get; set; }

    /// <inheritdoc />
    public bool CantAttack { get; set; }

    /// <inheritdoc/>
    public Position? Position { get; set; }

    /// <inheritdoc/>
    public EntityType Type => EntityType.Monster;

    /// <inheritdoc/>
    public int? Speed { get; set; }

    /// <inheritdoc />
    public bool? IsInvisible { get; set; }

    /// <inheritdoc/>
    public ushort? Level { get; set; }

    /// <inheritdoc/>
    public byte? Direction { get; set; }

    /// <inheritdoc/>
    public Health? Hp { get; set; }

    /// <inheritdoc/>
    public Health? Mp { get; set; }

    /// <inheritdoc/>
    public FactionType? Faction { get; set; }

    /// <inheritdoc/>
    public short Size { get; set; }

    /// <inheritdoc/>
    public IReadOnlyList<long>? EffectsVNums { get; set; }
}
\ No newline at end of file

A Core/NosSmooth.Game/Data/Entities/Npc.cs => Core/NosSmooth.Game/Data/Entities/Npc.cs +69 -0
@@ 0,0 1,69 @@
//
//  Npc.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.Info;
using NosSmooth.Packets.Enums;

namespace NosSmooth.Game.Data.Entities;

/// <summary>
/// Represents nostale npc entity.
/// </summary>
public class Npc : ILivingEntity
{
    /// <summary>
    /// Gets the VNum of the npc.
    /// </summary>
    public int VNum { get; internal set; }

    /// <inheritdoc/>
    public long Id { get; set; }

    /// <inheritdoc/>
    public string? Name { get; set; }

    /// <inheritdoc />
    public bool IsSitting { get; set; }

    /// <inheritdoc />
    public bool CantMove { get; set; }

    /// <inheritdoc />
    public bool CantAttack { get; set; }

    /// <inheritdoc/>
    public Position? Position { get; set; }

    /// <inheritdoc/>
    public EntityType Type => EntityType.Npc;

    /// <inheritdoc/>
    public int? Speed { get; set; }

    /// <inheritdoc />
    public bool? IsInvisible { get; set; }

    /// <inheritdoc/>
    public ushort? Level { get; set; }

    /// <inheritdoc/>
    public byte? Direction { get; set; }

    /// <inheritdoc/>
    public Health? Hp { get; set; }

    /// <inheritdoc/>
    public Health? Mp { get; set; }

    /// <inheritdoc/>
    public FactionType? Faction { get; set; }

    /// <inheritdoc/>
    public short Size { get; set; }

    /// <inheritdoc/>
    public IReadOnlyList<long>? EffectsVNums { get; set; }
}
\ No newline at end of file

A Core/NosSmooth.Game/Data/Entities/Partner.cs => Core/NosSmooth.Game/Data/Entities/Partner.cs +12 -0
@@ 0,0 1,12 @@
//
//  Partner.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.Data.Entities;

/// <summary>
/// Represents Partner of the Character.
/// </summary>
public record Partner() : IPet;
\ No newline at end of file

A Core/NosSmooth.Game/Data/Entities/Pet.cs => Core/NosSmooth.Game/Data/Entities/Pet.cs +12 -0
@@ 0,0 1,12 @@
//
//  Pet.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.Data.Entities;

/// <summary>
/// Represents pet of the character.
/// </summary>
public record Pet() : IPet;
\ No newline at end of file

A Core/NosSmooth.Game/Data/Entities/Player.cs => Core/NosSmooth.Game/Data/Entities/Player.cs +142 -0
@@ 0,0 1,142 @@
//
//  Player.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.Info;
using NosSmooth.Game.Data.Items;
using NosSmooth.Game.Data.Social;
using NosSmooth.Packets.Enums;
using NosSmooth.Packets.Enums.Players;

namespace NosSmooth.Game.Data.Entities;

/// <summary>
/// Represents nostale player entity.
/// </summary>
public class Player : ILivingEntity
{
    /// <summary>
    /// Gets or sets the authority of the player.
    /// </summary>
    public AuthorityType Authority { get; set; }

    /// <summary>
    /// Gets or sets the sex of the player.
    /// </summary>
    public SexType Sex { get; set; }

    /// <summary>
    /// Gets or sets the hairstyle of the player.
    /// </summary>
    public HairStyle HairStyle { get; set; }

    /// <summary>
    /// Gets or sets the hair color of the player.
    /// </summary>
    public HairColor HairColor { get; set; }

    /// <summary>
    /// Gets or sets the class of the player.
    /// </summary>
    public PlayerClass Class { get; set; }

    /// <summary>
    /// Gets or sets the reputation icon. UNKNOWN TODO.
    /// </summary>
    public byte? Icon { get; set; }

    /// <summary>
    /// UNKNOWN TODO.
    /// </summary>
    public short? Compliment { get; set; }

    /// <summary>
    /// Gets or sets the morph used for sps, vehicles and such.
    /// </summary>
    public Morph? Morph { get; set; }

    /// <summary>
    /// Gets or sets whether the player is a champion arena winner.
    /// </summary>
    public bool ArenaWinner { get; set; }

    /// <summary>
    /// Gets or sets the reputation number of the player.
    /// </summary>
    public long? Reputation { get; set; }

    /// <summary>
    /// Gets or sets the visible title of the player.
    /// </summary>
    public short Title { get; set; }

    /// <summary>
    /// Gets or sets the family.
    /// </summary>
    public Family? Family { get; set; }

    /// <summary>
    /// Gets the VNum of the npc.
    /// </summary>
    public int VNum { get; set; }

    /// <inheritdoc/>
    public long Id { get; set; }

    /// <inheritdoc/>
    public string? Name { get; set; }

    /// <inheritdoc />
    public bool IsSitting { get; set; }

    /// <inheritdoc />
    public bool CantMove { get; set; }

    /// <inheritdoc />
    public bool CantAttack { get; set; }

    /// <inheritdoc/>
    public Position? Position { get; set; }

    /// <inheritdoc/>
    public EntityType Type => EntityType.Player;

    /// <inheritdoc/>
    public int? Speed { get; set; }

    /// <inheritdoc />
    public bool? IsInvisible { get; set; }

    /// <inheritdoc/>
    public ushort? Level { get; set; }

    /// <inheritdoc/>
    public byte? Direction { get; set; }

    /// <inheritdoc/>
    public Health? Hp { get; set; }

    /// <inheritdoc/>
    public Health? Mp { get; set; }

    /// <inheritdoc/>
    public FactionType? Faction { get; set; }

    /// <inheritdoc/>
    public short Size { get; set; }

    /// <inheritdoc/>
    public IReadOnlyList<long>? EffectsVNums { get; set; }

    /// <summary>
    /// Gets or sets the hero level.
    /// </summary>
    public virtual short? HeroLevel { get; set; }

    /// <summary>
    /// Gets or sets the equipment.
    /// </summary>
    public Equipment? Equipment { get; set; }
}
\ No newline at end of file

A Core/NosSmooth.Game/Data/Info/Health.cs => Core/NosSmooth.Game/Data/Info/Health.cs +112 -0
@@ 0,0 1,112 @@
//
//  Health.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.Data.Info;

/// <summary>
/// Represents the health or mana of an entity.
/// </summary>
public class Health
{
    private byte? _percentage;
    private long? _amount;
    private long? _maximum;

    /// <summary>
    /// Gets or sets the percentage of the health.
    /// </summary>
    public byte? Percentage
    {
        get => _percentage;
        set
        {
            _percentage = value;
            if (value is null)
            {
                return;
            }

            var maximum = _maximum;
            if (maximum is not null)
            {
                _amount = (long)((value / 100.0) * maximum);
                return;
            }

            var amount = _amount;
            if (amount is not null)
            {
                _maximum = (long)(amount / (value / 100.0));
            }
        }
    }

    /// <summary>
    /// Gets or sets the health amount.
    /// </summary>
    public long? Amount
    {
        get => _amount;
        set
        {
            _amount = value;
            if (value is null)
            {
                return;
            }

            var maximum = _maximum;
            if (maximum is not null)
            {
                _percentage = (byte)(((double)value / maximum) * 100);
                return;
            }

            var percentage = _percentage;
            if (percentage is not null)
            {
                _maximum = (long)(value / (percentage / 100.0));
            }
        }
    }

    /// <summary>
    /// Gets or sets the maximum health.
    /// </summary>
    public long? Maximum
    {
        get => _maximum;
        set
        {
            _maximum = value;
            if (value is null)
            {
                return;
            }

            var amount = _amount;
            var percentage = _percentage;

            if (amount is not null)
            {
                if (amount > value)
                {
                    amount = _amount = value;
                    _percentage = 100;
                    return;
                }

                _percentage = (byte)((amount / (double)value) * 100);
                return;
            }

            if (percentage is not null)
            { // ? would this be correct?
                _amount = (long)((percentage / 100.0) * value);
            }
        }
    }
}
\ No newline at end of file

A Core/NosSmooth.Game/Data/Info/Level.cs => Core/NosSmooth.Game/Data/Info/Level.cs +15 -0
@@ 0,0 1,15 @@
//
//  Level.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.Data.Info;

/// <summary>
/// Represents a level, such as job level, hero level, character level.
/// </summary>
/// <param name="Lvl">The level.</param>
/// <param name="Xp">Current xp.</param>
/// <param name="XpLoad">Maximum xp of the current level.</param>
public record Level(short Lvl, long Xp, long XpLoad);
\ No newline at end of file

A Core/NosSmooth.Game/Data/Info/Morph.cs => Core/NosSmooth.Game/Data/Info/Morph.cs +28 -0
@@ 0,0 1,28 @@
//
//  Morph.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.Data.Info;

/// <summary>
/// Represents players morph.
/// </summary>
/// <remarks>
/// Morphs are used mainly for special cards.
/// The VNum will contain the vnum of the special card.
/// </remarks>
/// <param name="VNum">The vnum of the morph.</param>
/// <param name="Upgrade">The upgrade to show wings.</param>
/// <param name="Design">The design of the wings.</param>
/// <param name="Bonus">Unknown.</param>
/// <param name="Skin">The skin of the wings.</param>
public record Morph
(
    long VNum,
    byte Upgrade,
    short? Design = default,
    byte? Bonus = default,
    short? Skin = default
);
\ No newline at end of file

A Core/NosSmooth.Game/Data/Info/Position.cs => Core/NosSmooth.Game/Data/Info/Position.cs +26 -0
@@ 0,0 1,26 @@
//
//  Position.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.Diagnostics.CodeAnalysis;

namespace NosSmooth.Game.Data.Info;

/// <summary>
/// Represents nostale position on map.
/// </summary>
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313", MessageId = "Parameter names should begin with lower-case letter", Justification = "Standard.")]
public record struct Position(long X, long Y)
{
    /// <summary>
    /// Get the squared distance to the given position.
    /// </summary>
    /// <param name="position">The position.</param>
    /// <returns>The distance squared.</returns>
    public long DistanceSquared(Position position)
    {
        return ((position.X - X) * (position.X - X)) + ((position.Y - Y) * (position.Y - Y));
    }
}
\ No newline at end of file

A Core/NosSmooth.Game/Data/Inventory/Bag.cs => Core/NosSmooth.Game/Data/Inventory/Bag.cs +12 -0
@@ 0,0 1,12 @@
//
//  Bag.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.Data.Inventory;

/// <summary>
/// Represents one bag in the inventory of the player.
/// </summary>
public record Bag();
\ No newline at end of file

A Core/NosSmooth.Game/Data/Inventory/Inventory.cs => Core/NosSmooth.Game/Data/Inventory/Inventory.cs +12 -0
@@ 0,0 1,12 @@
//
//  Inventory.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.Data.Inventory;

/// <summary>
/// Represents the whole inventory of the character.
/// </summary>
public record Inventory();
\ No newline at end of file

A Core/NosSmooth.Game/Data/Inventory/InventoryItem.cs => Core/NosSmooth.Game/Data/Inventory/InventoryItem.cs +12 -0
@@ 0,0 1,12 @@
//
//  InventoryItem.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.Data.Inventory;

/// <summary>
/// Represents item in bag inventory of the character.
/// </summary>
public record InventoryItem();
\ No newline at end of file

A Core/NosSmooth.Game/Data/Items/Equipment.cs => Core/NosSmooth.Game/Data/Items/Equipment.cs +21 -0
@@ 0,0 1,21 @@
//
//  Equipment.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.Data.Items;

public record Equipment
(
     Item? Hat,
     UpgradeableItem? Armor,
     UpgradeableItem? MainWeapon,
     UpgradeableItem? SecondaryWeapon,
     Item? Mask,
     Item? Fairy,
     Item? CostumeSuit,
     Item? CostumeHat,
     short? WeaponSkin,
     short? WingSkin
);
\ No newline at end of file

A Core/NosSmooth.Game/Data/Items/Fairy.cs => Core/NosSmooth.Game/Data/Items/Fairy.cs +12 -0
@@ 0,0 1,12 @@
//
//  Fairy.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.Data.Abstractions.Infos;
using NosSmooth.Packets.Enums;

namespace NosSmooth.Game.Data.Items;

public record Fairy(int ItemVNum, Element Element, IItemInfo? Info) : Item(ItemVNum, Info);
\ No newline at end of file

A Core/NosSmooth.Game/Data/Items/Item.cs => Core/NosSmooth.Game/Data/Items/Item.cs +16 -0
@@ 0,0 1,16 @@
//
//  Item.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.Data.Abstractions.Infos;

namespace NosSmooth.Game.Data.Items;

/// <summary>
/// A NosTale item.
/// </summary>
/// <param name="ItemVNum"></param>
/// <param name="Info"></param>
public record Item(int ItemVNum, IItemInfo? Info);
\ No newline at end of file

A Core/NosSmooth.Game/Data/Items/UpgradeableItem.cs => Core/NosSmooth.Game/Data/Items/UpgradeableItem.cs +18 -0
@@ 0,0 1,18 @@
//
//  UpgradeableItem.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.Data.Abstractions.Infos;

namespace NosSmooth.Game.Data.Items;

/// <summary>
/// An item that can be upgraded and has rarity, ie. weapon or armor.
/// </summary>
/// <param name="ItemVNum">The vnum of the item.</param>
/// <param name="Info">The information about the item.</param>
/// <param name="Upgrade">The upgrade (0 - 10).</param>
/// <param name="Rare">The rare nubmer (0 - 8).</param>
public record UpgradeableItem(int ItemVNum, IItemInfo? Info, byte? Upgrade, sbyte? Rare) : Item(ItemVNum, Info);
\ No newline at end of file

A Core/NosSmooth.Game/Data/Login/Channel.cs => Core/NosSmooth.Game/Data/Login/Channel.cs +12 -0
@@ 0,0 1,12 @@
//
//  Channel.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.Data.Login;

/// <summary>
/// Channel of a nostale server.
/// </summary>
public record Channel();
\ No newline at end of file

A Core/NosSmooth.Game/Data/Login/WorldServer.cs => Core/NosSmooth.Game/Data/Login/WorldServer.cs +12 -0
@@ 0,0 1,12 @@
//
//  WorldServer.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.Data.Login;

/// <summary>
/// Represents nostale world server.
/// </summary>
public record WorldServer();
\ No newline at end of file

A Core/NosSmooth.Game/Data/Maps/Map.cs => Core/NosSmooth.Game/Data/Maps/Map.cs +46 -0
@@ 0,0 1,46 @@
//
//  Map.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.Diagnostics.CodeAnalysis;
using NosSmooth.Data.Abstractions.Infos;
using NosSmooth.Game.Data.Info;

namespace NosSmooth.Game.Data.Maps;

/// <summary>
/// Represents nostale map.
/// </summary>
public record Map
(
    long Id,
    byte Type,
    IMapInfo? Info,
    MapEntities Entities,
    IReadOnlyList<Portal> Portals
)
{
    /// <summary>
    /// Gets whether the given position lies on a portal.
    /// </summary>
    /// <param name="position">The position.</param>
    /// <param name="portal">The portal the position is on, if any.</param>
    /// <returns>Whether there was a portal at the specified position.</returns>
    public bool IsOnPortal(Position position, [NotNullWhen(true)] out Portal? portal)
    {
        foreach (var p in Portals)
        {
            // TODO: figure out the distance
            if (p.Position.DistanceSquared(position) < 3)
            {
                portal = p;
                return true;
            }
        }

        portal = null;
        return false;
    }
}
\ No newline at end of file

A Core/NosSmooth.Game/Data/Maps/MapEntities.cs => Core/NosSmooth.Game/Data/Maps/MapEntities.cs +100 -0
@@ 0,0 1,100 @@
//
//  MapEntities.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.Game.Data.Entities;
using NosSmooth.Packets.Enums;

namespace NosSmooth.Game.Data.Maps;

/// <summary>
/// Thread-safe store for the entities on the map.
/// </summary>
public class MapEntities
{
    private readonly ConcurrentDictionary<long, IEntity> _entities;

    /// <summary>
    /// Initializes a new instance of the <see cref="MapEntities"/> class.
    /// </summary>
    public MapEntities()
    {
        _entities = new ConcurrentDictionary<long, IEntity>();
    }

    /// <summary>
    /// Gets the given entity by id.
    /// </summary>
    /// <param name="id">The id of the entity.</param>
    /// <returns>The entity, or null, if not found.</returns>
    public IEntity? GetEntity(long id)
        => _entities.GetValueOrDefault(id);

    /// <summary>
    /// Get the given entity by id.
    /// </summary>
    /// <param name="id">The id of the entity.</param>
    /// <typeparam name="TEntity">The type of the entity.</typeparam>
    /// <returns>The entity.</returns>
    /// <exception cref="Exception">If the entity is not of the specified type.</exception>
    public TEntity? GetEntity<TEntity>(long id)
    {
        var entity = GetEntity(id);
        if (entity is null)
        {
            return default;
        }

        if (entity is TEntity tentity)
        {
            return tentity;
        }

        throw new Exception($"Could not find the entity with the given type {typeof(TEntity)}, was {entity.GetType()}");
    }

    /// <summary>
    /// Add the given entity to the entities list.
    /// </summary>
    /// <param name="entity">The entity to add.</param>
    internal void AddEntity(IEntity entity)
    {
        _entities.AddOrUpdate(entity.Id, _ => entity, (i, e) => entity);
    }

    /// <summary>
    /// .
    /// </summary>
    /// <param name="entityId">The id of the entity.</param>
    /// <param name="createAction">The action to execute on create.</param>
    /// <param name="updateAction">The action to execute on update.</param>
    /// <typeparam name="TEntity">The type of the entity.</typeparam>
    internal void AddOrUpdateEntity<TEntity>
        (long entityId, Func<long, TEntity> createAction, Func<long, TEntity, TEntity> updateAction)
        where TEntity : IEntity
    {
        _entities.AddOrUpdate
            (entityId, (key) => createAction(key), (key, entity) => updateAction(key, (TEntity)entity));
    }

    /// <summary>
    /// Remove the given entity.
    /// </summary>
    /// <param name="entity">The entity to remove.</param>
    internal void RemoveEntity(IEntity entity)
    {
        RemoveEntity(entity.Id);
    }

    /// <summary>
    /// Remove the given entity.
    /// </summary>
    /// <param name="entityId">The id of the entity to remove.</param>
    internal void RemoveEntity(long entityId)
    {
        _entities.TryRemove(entityId, out _);
    }
}
\ No newline at end of file

A Core/NosSmooth.Game/Data/Maps/Miniland.cs => Core/NosSmooth.Game/Data/Maps/Miniland.cs +30 -0
@@ 0,0 1,30 @@
//
//  Miniland.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.Data.Abstractions.Infos;

namespace NosSmooth.Game.Data.Maps;

/// <summary>
/// Represents Miniland map that can contain miniland objects.
/// </summary>
/// <param name="Objects">The objects in the miniland.</param>
public record Miniland
(
    long Id,
    byte Type,
    IMapInfo? Info,
    MapEntities Entities,
    IReadOnlyList<Portal> Portals,
    IReadOnlyList<MinilandObject>? Objects
) : Map
(
    Id,
    Type,
    Info,
    Entities,
    Portals
);
\ No newline at end of file

A Core/NosSmooth.Game/Data/Maps/MinilandObject.cs => Core/NosSmooth.Game/Data/Maps/MinilandObject.cs +9 -0
@@ 0,0 1,9 @@
//
//  MinilandObject.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.Data.Maps;

public record MinilandObject();
\ No newline at end of file

A Core/NosSmooth.Game/Data/Maps/Portal.cs => Core/NosSmooth.Game/Data/Maps/Portal.cs +25 -0
@@ 0,0 1,25 @@
//
//  Portal.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.Info;
using NosSmooth.Packets.Enums;

namespace NosSmooth.Game.Data.Maps;

/// <summary>
/// Represents map portal leading to another map.
/// </summary>
/// <param name="PortalId">The portal id.</param>
/// <param name="Position">The position of the portal.</param>
/// <param name="TargetMapId">The id of the target map.</param>
public record Portal
(
    long PortalId,
    Position Position,
    long TargetMapId,
    PortalType? PortalType,
    bool IsDisabled
);
\ No newline at end of file

A Core/NosSmooth.Game/Data/Maps/Timespace.cs => Core/NosSmooth.Game/Data/Maps/Timespace.cs +9 -0
@@ 0,0 1,9 @@
//
//  Timespace.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.Data.Maps;

public record Timespace();
\ No newline at end of file

A Core/NosSmooth.Game/Data/Raids/Raid.cs => Core/NosSmooth.Game/Data/Raids/Raid.cs +12 -0
@@ 0,0 1,12 @@
//
//  Raid.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.Data.Raids;

/// <summary>
/// Represents nostale raid.
/// </summary>
public record Raid();
\ No newline at end of file

A Core/NosSmooth.Game/Data/Social/Family.cs => Core/NosSmooth.Game/Data/Social/Family.cs +22 -0
@@ 0,0 1,22 @@
//
//  Family.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.Data.Social;

/// <summary>
/// Represents nostale family entity.
/// </summary>
/// <param name="Id">The id of the family.</param>
/// <param name="Name">The name of the family.</param>
/// <param name="Level">The level of the entity.</param>
public record Family
(
    string? Id,
    short? Title,
    string? Name,
    byte? Level,
    IReadOnlyList<bool>? Icons
);
\ No newline at end of file

A Core/NosSmooth.Game/Data/Social/Group.cs => Core/NosSmooth.Game/Data/Social/Group.cs +18 -0
@@ 0,0 1,18 @@
//
//  Group.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 OneOf;

namespace NosSmooth.Game.Data.Social;

/// <summary>
/// Represents nostale group of players or pets and partners.
/// </summary>
/// <param name="Id">The id of the group.</param>
/// <param name="Size">The size of the group.</param>
/// <param name="Members">The members of the group. (excluding the character)</param>
public record Group(short? Id, byte? Size, IReadOnlyList<OneOf<Player, IPet>>? Members);
\ No newline at end of file

A Core/NosSmooth.Game/Errors/SkillOnCooldownError.cs => Core/NosSmooth.Game/Errors/SkillOnCooldownError.cs +15 -0
@@ 0,0 1,15 @@
//
//  SkillOnCooldownError.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 Remora.Results;

namespace NosSmooth.Game.Errors;

/// <summary>
/// Acts as an error specifying the skill is on cooldown.
/// </summary>
public record SkillOnCooldownError(Skill skill) : ResultError("The skill is on cooldown.");
\ No newline at end of file

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/ReceivedCharacterDataEvent.cs => Core/NosSmooth.Game/Events/Characters/ReceivedCharacterDataEvent.cs +18 -0
@@ 0,0 1,18 @@
//
//  ReceivedCharacterDataEvent.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>
/// Represents received new updated character data.
/// </summary>
/// <param name="Character">The newly received data.</param>
public record ReceivedCharacterDataEvent
(
    Character Character
) : IGameEvent;
\ No newline at end of file

A Core/NosSmooth.Game/Events/Characters/SkillReadyEvent.cs => Core/NosSmooth.Game/Events/Characters/SkillReadyEvent.cs +14 -0
@@ 0,0 1,14 @@
//
//  SkillReadyEvent.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 skill cooldown has been up.
/// </summary>
public record SkillReadyEvent(Skill? Skill, long SkillVNum) : IGameEvent;
\ No newline at end of file

A Core/NosSmooth.Game/Events/Characters/SkillsReceivedEvent.cs => Core/NosSmooth.Game/Events/Characters/SkillsReceivedEvent.cs +15 -0
@@ 0,0 1,15 @@
//
//  SkillsReceivedEvent.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>
/// Received skills of the character.
/// </summary>
/// <param name="Skills">The skills.</param>
public record SkillsReceivedEvent(Skills Skills) : IGameEvent;
\ No newline at end of file

R Core/NosSmooth.Game/Events/Handlers/EventDispatcher.cs => Core/NosSmooth.Game/Events/Core/EventDispatcher.cs +52 -51
@@ 1,51 1,52 @@
//
//  EventDispatcher.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.DependencyInjection;
using Remora.Results;

namespace NosSmooth.Game.Events.Handlers;

/// <summary>
/// Dispatches <see cref="IGameResponder"/> with <see cref="IGameEvent"/>.
/// </summary>
public class EventDispatcher
{
    private readonly IServiceProvider _provider;

    /// <summary>
    /// Initializes a new instance of the <see cref="EventDispatcher"/> class.
    /// </summary>
    /// <param name="provider">The services provider.</param>
    public EventDispatcher(IServiceProvider provider)
    {
        _provider = provider;
    }

    /// <summary>
    /// Dispatches game responders that are registered in the service collection.
    /// </summary>
    /// <param name="event">The event to dispatch.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <typeparam name="TEvent">The type of the event.</typeparam>
    /// <returns>A result that may or may not have succeeded.</returns>
    public async Task<Result> DispatchEvent<TEvent>(TEvent @event, CancellationToken ct = default)
        where TEvent : IGameEvent
    {
        var results = await Task.WhenAll(
            _provider
                .GetServices<IGameResponder<TEvent>>()
                .Select(responder => responder.Respond(@event, ct))
        );

        return results.Length switch
        {
            0 => Result.FromSuccess(),
            1 => results[0],
            _ => new AggregateError(results.Cast<IResult>().ToArray()),
        };
    }
}
\ No newline at end of file
//
//  EventDispatcher.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.DependencyInjection;
using Remora.Results;

namespace NosSmooth.Game.Events.Core;

/// <summary>
/// Dispatches <see cref="IGameResponder"/> with <see cref="IGameEvent"/>.
/// </summary>
public class EventDispatcher
{
    private readonly IServiceProvider _provider;

    /// <summary>
    /// Initializes a new instance of the <see cref="EventDispatcher"/> class.
    /// </summary>
    /// <param name="provider">The services provider.</param>
    public EventDispatcher(IServiceProvider provider)
    {
        _provider = provider;
    }

    /// <summary>
    /// Dispatches game responders that are registered in the service collection.
    /// </summary>
    /// <param name="event">The event to dispatch.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <typeparam name="TEvent">The type of the event.</typeparam>
    /// <returns>A result that may or may not have succeeded.</returns>
    public async Task<Result> DispatchEvent<TEvent>(TEvent @event, CancellationToken ct = default)
        where TEvent : IGameEvent
    {
        using var scope = _provider.CreateScope();
        var results = await Task.WhenAll(
            scope.ServiceProvider
                .GetServices<IGameResponder<TEvent>>()
                .Select(responder => responder.Respond(@event, ct))
        );

        return results.Length switch
        {
            0 => Result.FromSuccess(),
            1 => results[0],
            _ => new AggregateError(results.Cast<IResult>().ToArray()),
        };
    }
}

R Core/NosSmooth.Game/Events/Handlers/IGameResponder.cs => Core/NosSmooth.Game/Events/Core/IGameResponder.cs +33 -34
@@ 1,34 1,33 @@
//
//  IGameResponder.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 OneOf.Types;
using Remora.Results;

namespace NosSmooth.Game.Events.Handlers;

/// <summary>
/// Represents interface for classes that respond to <see cref="IGameEvent"/>.
/// </summary>
public interface IGameResponder
{
}

/// <summary>
/// Represents interface for classes that respond to game events.
/// Responds to <typeparamref name="TPacket"/>.
/// </summary>
/// <typeparam name="TEvent">The event type this responder responds to.</typeparam>
public interface IGameResponder<TEvent> : IGameResponder
    where TEvent : IGameEvent
{
    /// <summary>
    /// Respond to the given packet.
    /// </summary>
    /// <param name="packet">The packet to respond to.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> Respond(TEvent packet, CancellationToken ct = default);
}
\ No newline at end of file
//
//  IGameResponder.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 Remora.Results;

namespace NosSmooth.Game.Events.Core;

/// <summary>
/// Represents interface for classes that respond to <see cref="IGameEvent"/>.
/// </summary>
public interface IGameResponder
{
}

/// <summary>
/// Represents interface for classes that respond to game events.
/// Responds to <typeparamref name="TEvent"/>.
/// </summary>
/// <typeparam name="TEvent">The event type this responder responds to.</typeparam>
public interface IGameResponder<TEvent> : IGameResponder
    where TEvent : IGameEvent
{
    /// <summary>
    /// Respond to the given packet.
    /// </summary>
    /// <param name="gameEvent">The packet to respond to.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> Respond(TEvent gameEvent, CancellationToken ct = default);
}

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

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

/// <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,
    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/EntityStunnedEvent.cs => Core/NosSmooth.Game/Events/Entities/EntityStunnedEvent.cs +18 -0
@@ 0,0 1,18 @@
//
//  EntityStunnedEvent.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 stunned or unstunned.
/// </summary>
/// <param name="Entity">The entity.</param>
/// <param name="CantMove">Whether the entity cannot move.</param>
/// <param name="CantAttack">Whether the entity cannot attack.</param>
public record EntityStunnedEvent(ILivingEntity Entity, bool CantMove, bool CantAttack)
    : IGameEvent;
\ 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

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

M Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs => Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs +105 -44
@@ 1,44 1,105 @@

    /// <summary>
    /// Adds the given game event responder.
    /// </summary>
    /// <param name="serviceCollection">The service collection.</param>
    /// <typeparam name="TGameResponder">The responder to add.</typeparam>
    /// <returns>The collection.</returns>
    public static IServiceCollection AddGameResponder<TGameResponder>(this IServiceCollection serviceCollection)
        where TGameResponder : IGameResponder
    {
        return serviceCollection.AddGameResponder(typeof(TGameResponder));
    }

    /// <summary>
    /// Adds the given game event responder.
    /// </summary>
    /// <param name="serviceCollection">The service collection.</param>
    /// <param name="gameResponder">The type of the event responder.</param>
    /// <returns>The collection.</returns>
    public static IServiceCollection AddGameResponder(this IServiceCollection serviceCollection, Type gameResponder)
    {
        if (!gameResponder.GetInterfaces().Any(
                i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IGameResponder<>)
            ))
        {
            throw new ArgumentException(
                $"{nameof(gameResponder)} should implement IGameResponder.",
                nameof(gameResponder));
        }

        var handlerTypeInterfaces = gameResponder.GetInterfaces();
        var handlerInterfaces = handlerTypeInterfaces.Where
        (
            r => r.IsGenericType && r.GetGenericTypeDefinition() == typeof(IGameResponder<>)
        );

        foreach (var handlerInterface in handlerInterfaces)
        {
            serviceCollection.AddScoped(handlerInterface, gameResponder);
        }

        return serviceCollection;
    }
}
\ No newline at end of file
//
//  ServiceCollectionExtensions.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.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using NosSmooth.Core.Extensions;
using NosSmooth.Game.Apis;
using NosSmooth.Game.Events.Core;
using NosSmooth.Game.PacketHandlers.Characters;
using NosSmooth.Game.PacketHandlers.Entities;
using NosSmooth.Game.PacketHandlers.Map;
using NosSmooth.Game.PacketHandlers.Specialists;

namespace NosSmooth.Game.Extensions;

/// <summary>
/// Contains extension methods for <see cref="IServiceCollection"/>.
/// </summary>
public static class ServiceCollectionExtensions
{
    /// <summary>
    /// Adds handling of nostale packets, registering <see cref="Game"/> singleton and dispatching of game events.
    /// </summary>
    /// <param name="serviceCollection">The service collection.</param>
    /// <returns>The collection.</returns>
    public static IServiceCollection AddNostaleGame(this IServiceCollection serviceCollection)
    {
        serviceCollection
            .AddNostaleCore()
            .AddMemoryCache()
            .TryAddSingleton<EventDispatcher>();
        serviceCollection.TryAddSingleton<Game>();

        serviceCollection
            .AddPacketResponder<CharacterInitResponder>()
            .AddPacketResponder<SkillResponder>()
            .AddPacketResponder<WalkResponder>()
            .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<SpResponder>()
            .AddPacketResponder<EqResponder>();

        serviceCollection
            .AddTransient<NostaleChatPacketApi>()
            .AddTransient<NostaleSkillsPacketApi>();

        return serviceCollection;
    }

    /// <summary>
    /// Adds the given game event responder.
    /// </summary>
    /// <param name="serviceCollection">The service collection.</param>
    /// <typeparam name="TGameResponder">The responder to add.</typeparam>
    /// <returns>The collection.</returns>
    public static IServiceCollection AddGameResponder<TGameResponder>(this IServiceCollection serviceCollection)
        where TGameResponder : IGameResponder
    {
        return serviceCollection.AddGameResponder(typeof(TGameResponder));
    }

    /// <summary>
    /// Adds the given game event responder.
    /// </summary>
    /// <param name="serviceCollection">The service collection.</param>
    /// <param name="gameResponder">The type of the event responder.</param>
    /// <returns>The collection.</returns>
    public static IServiceCollection AddGameResponder(this IServiceCollection serviceCollection, Type gameResponder)
    {
        if (!gameResponder.GetInterfaces().Any(
                i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IGameResponder<>)
            ))
        {
            throw new ArgumentException(
                $"{nameof(gameResponder)} should implement IGameResponder.",
                nameof(gameResponder));
        }

        var handlerTypeInterfaces = gameResponder.GetInterfaces();
        var handlerInterfaces = handlerTypeInterfaces.Where
        (
            r => r.IsGenericType && r.GetGenericTypeDefinition() == typeof(IGameResponder<>)
        );

        foreach (var handlerInterface in handlerInterfaces)
        {
            serviceCollection.AddScoped(handlerInterface, gameResponder);
        }

        return serviceCollection;
    }
}

A Core/NosSmooth.Game/Extensions/SkillsExtensions.cs => Core/NosSmooth.Game/Extensions/SkillsExtensions.cs +74 -0
@@ 0,0 1,74 @@
//
//  SkillsExtensions.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 Remora.Results;

namespace NosSmooth.Game.Extensions;

/// <summary>
/// Contains extension methods for <see cref="Skills"/>.
/// </summary>
public static class SkillsExtensions
{
    /// <summary>
    /// 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> TryGetSkillByVNum(this Skills skills, long skillVNum)
    {
        if (skills.PrimarySkill.SkillVNum == skillVNum)
        {
            return skills.PrimarySkill;
        }

        if (skills.SecondarySkill.SkillVNum == skillVNum)
        {
            return skills.SecondarySkill;
        }

        foreach (Skill skill in skills.OtherSkills)
        {
            if (skill.SkillVNum == skillVNum)
            {
                return skill;
            }
        }

        return new NotFoundError();
    }
}
\ No newline at end of file

A Core/NosSmooth.Game/Game.cs => Core/NosSmooth.Game/Game.cs +197 -0
@@ 0,0 1,197 @@
//
//  Game.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.Options;
using NosSmooth.Game.Data.Characters;
using NosSmooth.Game.Data.Maps;
using NosSmooth.Game.Data.Raids;

namespace NosSmooth.Game;

/// <summary>
/// Represents base nostale game class with the character, current map, friends and current raid.
/// </summary>
public class Game
{
    private readonly GameOptions _options;
    private Map? _currentMap;

    /// <summary>
    /// Initializes a new instance of the <see cref="Game"/> class.
    /// </summary>
    /// <param name="options">The options for the game.</param>
    public Game(IOptions<GameOptions> options)
    {
        Semaphores = new GameSemaphores();
        _options = options.Value;
    }

    /// <summary>
    /// Gets the game semaphores.
    /// </summary>
    internal GameSemaphores Semaphores { get; }

    /// <summary>
    /// Gets the playing character of the client.
    /// </summary>
    public Character? Character { get; internal set; }

    /// <summary>
    /// Gets the current map of the client.
    /// </summary>
    /// <remarks>
    /// Will be null until current map packet is received.
    /// </remarks>
    public Map? CurrentMap
    {
        get => _currentMap;
        internal set
        {
            _currentMap = value;
        }
    }

    /// <summary>
    /// Gets the active raid the client is currently on.
    /// </summary>
    /// <remarks>
    /// May be null if there is no raid in progress.
    /// </remarks>
    public Raid? CurrentRaid { get; internal 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>
    /// <param name="update">The function for updating the character.</param>
    /// <param name="releaseSemaphore">Whether to release the semaphore used for changing the character.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>The updated character.</returns>
    internal async Task<Character> CreateOrUpdateCharacterAsync
    (
        Func<Character> create,
        Func<Character, Character> update,
        bool releaseSemaphore = true,
        CancellationToken ct = default
    )
    {
        return await CreateOrUpdateAsync
        (
            GameSemaphoreType.Character,
            () => Character,
            c => Character = c,
            create,
            update,
            releaseSemaphore,
            ct
        );
    }

    /// <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="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
    (
        Func<Map?> create,
        bool releaseSemaphore = true,
        CancellationToken ct = default
    )
    {
        return await CreateAsync
        (
            GameSemaphoreType.Map,
            m => CurrentMap = m,
            create,
            releaseSemaphore,
            ct
        );
    }

    /// <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,
        Action<T> set,
        Func<T> create,
        bool releaseSemaphore = true,
        CancellationToken ct = default
    )
    {
        await Semaphores.AcquireSemaphore(type, ct);

        var current = create();
        set(current);
        if (releaseSemaphore)
        {
            Semaphores.ReleaseSemaphore(type);
        }

        return current;
    }

    private async Task<T> CreateOrUpdateAsync<T>
    (
        GameSemaphoreType type,
        Func<T?> get,
        Action<T> set,
        Func<T> create,
        Func<T, T> update,
        bool releaseSemaphore = true,
        CancellationToken ct = default
    )
    {
        await Semaphores.AcquireSemaphore(type, ct);

        var current = get();
        if (current is null)
        {
            current = create();
        }
        else
        {
            current = update(current);
        }

        set(current);
        if (releaseSemaphore)
        {
            Semaphores.ReleaseSemaphore(type);
        }

        return current;
    }
}
\ No newline at end of file

A Core/NosSmooth.Game/GameOptions.cs => Core/NosSmooth.Game/GameOptions.cs +18 -0
@@ 0,0 1,18 @@
//
//  GameOptions.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;

/// <summary>
/// Options for <see cref="Game"/>.
/// </summary>
public class GameOptions
{
    /// <summary>
    /// Duration to cache entities for after changing maps in seconds.
    /// </summary>
    public ulong EntityCacheDuration { get; set; }
}
\ No newline at end of file

A Core/NosSmooth.Game/GameSemaphoreType.cs => Core/NosSmooth.Game/GameSemaphoreType.cs +28 -0
@@ 0,0 1,28 @@
//
//  GameSemaphoreType.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;

/// <summary>
/// Type of game semaphore.
/// </summary>
public enum GameSemaphoreType
{
    /// <summary>
    /// The semaphore for the character.
    /// </summary>
    Character,

    /// <summary>
    /// The semaphore for the map.
    /// </summary>
    Map,

    /// <summary>
    /// The semaphore for the raid.
    /// </summary>
    Raid
}
\ No newline at end of file

A Core/NosSmooth.Game/GameSemaphores.cs => Core/NosSmooth.Game/GameSemaphores.cs +47 -0
@@ 0,0 1,47 @@
//
//  GameSemaphores.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;

/// <summary>
/// Holds information about semaphores for synchornizing the game packet data.
/// </summary>
internal class GameSemaphores
{
    private Dictionary<GameSemaphoreType, SemaphoreSlim> _semaphores;

    /// <summary>
    /// Initializes a new instance of the <see cref="GameSemaphores"/> class.
    /// </summary>
    public GameSemaphores()
    {
        _semaphores = new Dictionary<GameSemaphoreType, SemaphoreSlim>();
        foreach (var type in Enum.GetValues(typeof(GameSemaphoreType)).Cast<GameSemaphoreType>())
        {
            _semaphores[type] = new SemaphoreSlim(1, 1);
        }
    }

    /// <summary>
    /// Acquire the given semaphore.
    /// </summary>
    /// <param name="semaphoreType">The semaphore type.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A task that may or may not have succeeded.</returns>
    public async Task AcquireSemaphore(GameSemaphoreType semaphoreType, CancellationToken ct = default)
    {
        await _semaphores[semaphoreType].WaitAsync(ct);
    }

    /// <summary>
    /// Release the acquired semaphore.
    /// </summary>
    /// <param name="semaphoreType">The semaphore type.</param>
    public void ReleaseSemaphore(GameSemaphoreType semaphoreType)
    {
        _semaphores[semaphoreType].Release();
    }
}
\ No newline at end of file

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

A Core/NosSmooth.Game/IsExternalInit.cs => Core/NosSmooth.Game/IsExternalInit.cs +16 -0
@@ 0,0 1,16 @@
//
//  IsExternalInit.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.

// ReSharper disable once CheckNamespace
namespace System.Runtime.CompilerServices
{
    /// <summary>
    /// Dummy.
    /// </summary>
    public class IsExternalInit
    {
    }
}
\ No newline at end of file

M Core/NosSmooth.Game/NosSmooth.Game.csproj => Core/NosSmooth.Game/NosSmooth.Game.csproj +28 -10
@@ 1,10 1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <LangVersion>10</LangVersion>
        <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
    </PropertyGroup>

</Project>
<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <LangVersion>10</LangVersion>
        <TargetFrameworks>net6.0;netstandard2.1</TargetFrameworks>
        <Description>NosSmooth Game library handling the current game state by responding to packets.</Description>
        <RepositoryUrl>https://github.com/Rutherther/NosSmooth/</RepositoryUrl>
        <PackageLicenseExpression>MIT</PackageLicenseExpression>
    </PropertyGroup>

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

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

    <ItemGroup>
      <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.0" />
      <PackageReference Include="OneOf" Version="3.0.205" />
    </ItemGroup>

</Project>

A Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs => Core/NosSmooth.Game/PacketHandlers/Characters/CharacterInitResponder.cs +170 -0
@@ 0,0 1,170 @@
//
//  CharacterInitResponder.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.Characters;
using NosSmooth.Game.Data.Info;
using NosSmooth.Game.Data.Social;
using NosSmooth.Game.Events.Characters;
using NosSmooth.Game.Events.Core;
using NosSmooth.Packets.Server.Players;
using Remora.Results;

namespace NosSmooth.Game.PacketHandlers.Characters;

/// <summary>
/// Responds to CInfoPacket by creating the character.
/// </summary>
public class CharacterInitResponder : IPacketResponder<CInfoPacket>, IPacketResponder<LevPacket>,
    IPacketResponder<CModePacket>
{
    private readonly Game _game;
    private readonly EventDispatcher _eventDispatcher;

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

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<CInfoPacket> packetArgs, CancellationToken ct = default)
    {
        var oldCharacter = _game.Character;
        var packet = packetArgs.Packet;
        var character = await _game.CreateOrUpdateCharacterAsync
        (
            () => new Character
            {
                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
                {
                    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(character), ct);
        }

        return Result.FromSuccess();
    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<LevPacket> packetArgs, CancellationToken ct = default)
    {
        var packet = packetArgs.Packet;
        var oldCharacter = _game.Character;

        var character = await _game.CreateOrUpdateCharacterAsync
        (
            () => new Character
            {
                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.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(character), ct);
        }

        return Result.FromSuccess();
    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<CModePacket> packetArgs, CancellationToken ct = default)
    {
        var packet = packetArgs.Packet;
        var oldCharacter = _game.Character;

        if (oldCharacter is null || oldCharacter.Id != packetArgs.Packet.EntityId)
        { // Not the current character.
            return Result.FromSuccess();
        }

        var character = await _game.CreateOrUpdateCharacterAsync
        (
            () => throw new NotImplementedException(),
            (character) =>
            {
                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(character), ct);
        }

        return Result.FromSuccess();
    }
}
\ No newline at end of file

A Core/NosSmooth.Game/PacketHandlers/Characters/SkillResponder.cs => Core/NosSmooth.Game/PacketHandlers/Characters/SkillResponder.cs +125 -0
@@ 0,0 1,125 @@
//
//  SkillResponder.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.Events.Characters;
using NosSmooth.Game.Events.Core;
using NosSmooth.Packets.Server.Skills;
using Remora.Results;

namespace NosSmooth.Game.PacketHandlers.Characters;

/// <summary>
/// Responds to SkiPacket to add skill to the character.
/// </summary>
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>
    /// <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.PrimarySkillVNum == character.Skills?.PrimarySkill.SkillVNum)
        {
            primarySkill = character.Skills.PrimarySkill;
        }
        else
        {
            primarySkill = await CreateSkill(packet.PrimarySkillVNum, default);
        }

        if (character is not null && packet.PrimarySkillVNum == packet.SecondarySkillVNum)
        {
            secondarySkill = primarySkill;
        }
        else if (character is not null && packet.SecondarySkillVNum == character.Skills?.SecondarySkill.SkillVNum)
        {
            secondarySkill = character.Skills.SecondarySkill;
        }
        else
        {
            secondarySkill = await CreateSkill(packet.SecondarySkillVNum, default);
        }

        var skillsFromPacket = packet.SkillSubPackets?.Select(x => x.SkillVNum).ToList() ?? new List<int>();
        var skillsFromCharacter = character?.Skills is null
            ? new List<int>()
            : character.Skills.OtherSkills.Select(x => x.SkillVNum).ToList();
        var newSkills = skillsFromPacket.Except(skillsFromCharacter);
        var oldSkills = skillsFromCharacter.Except(skillsFromPacket);

        var otherSkillsFromCharacter = new List<Skill>(character?.Skills?.OtherSkills ?? new Skill[] { });
        otherSkillsFromCharacter.RemoveAll(x => oldSkills.Contains(x.SkillVNum));

        foreach (var newSkill in newSkills)
        {
            otherSkillsFromCharacter.Add(await CreateSkill(newSkill, default));
        }

        var skills = new Skills(primarySkill, secondarySkill, otherSkillsFromCharacter);

        await _game.CreateOrUpdateCharacterAsync
        (
            () => new Character { Skills = skills },
            c =>
            {
                c.Skills = skills;
                return c;
            },
            ct: ct
        );

        await _eventDispatcher.DispatchEvent(new SkillsReceivedEvent(skills), ct);

        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

A Core/NosSmooth.Game/PacketHandlers/Characters/WalkResponder.cs => Core/NosSmooth.Game/PacketHandlers/Characters/WalkResponder.cs +66 -0
@@ 0,0 1,66 @@
//
//  WalkResponder.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.Characters;
using NosSmooth.Game.Data.Info;
using NosSmooth.Game.Events.Core;
using NosSmooth.Game.Events.Entities;
using NosSmooth.Packets.Client.Movement;
using Remora.Results;

namespace NosSmooth.Game.PacketHandlers.Characters;

/// <summary>
/// Responds to walk packet.
/// </summary>
public class WalkResponder : IPacketResponder<WalkPacket>
{
    private readonly Game _game;
    private readonly EventDispatcher _eventDispatcher;

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

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<WalkPacket> packetArgs, CancellationToken ct = default)
    {
        var character = _game.Character;
        var packet = packetArgs.Packet;
        var oldPosition = character?.Position;
        var position = new Position(packet.PositionX, packet.PositionY);

        character = await _game.CreateOrUpdateCharacterAsync
        (
            () => new Character
            {
                Position = position
            },
            (c) =>
            {
                c.Position = position;
                return c;
            },
            ct: ct
        );

        return await _eventDispatcher.DispatchEvent
        (
            new EntityMovedEvent(character, oldPosition, character.Position!.Value),
            ct
        );

        return Result.FromSuccess();
    }
}
\ No newline at end of file

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 +66 -0
@@ 0,0 1,66 @@
//
//  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.Game.Events.Core;
using NosSmooth.Game.Events.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;
    private readonly EventDispatcher _eventDispatcher;

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

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

        var packet = packetArgs.Packet;
        var entity = map.Entities.GetEntity<ILivingEntity>(packet.EntityId);

        if (entity is null)
        {
            return Result.FromSuccess();
        }

        bool cantMove = entity.CantMove;
        bool cantAttack = entity.CantAttack;

        entity.Speed = packet.Speed;
        entity.CantAttack = packet.CantAttack;
        entity.CantMove = packet.CantMove;

        if (cantMove != packet.CantMove || cantAttack != packet.CantAttack)
        {
            return await _eventDispatcher.DispatchEvent(new EntityStunnedEvent(entity, packet.CantMove, packet.CantAttack), ct);
        }

        return 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

A Core/NosSmooth.Game/PacketHandlers/Entities/SkillUsedResponder.cs => Core/NosSmooth.Game/PacketHandlers/Entities/SkillUsedResponder.cs +170 -0
@@ 0,0 1,170 @@
//
//  SkillUsedResponder.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 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.Entities;
using NosSmooth.Game.Extensions;
using NosSmooth.Game.Helpers;
using NosSmooth.Packets.Server.Battle;
using NosSmooth.Packets.Server.Skills;
using Remora.Results;

namespace NosSmooth.Game.PacketHandlers.Entities;

/// <summary>
/// Responds to skill used packet.
/// </summary>
public class SkillUsedResponder : IPacketResponder<SuPacket>, IPacketResponder<SrPacket>
{
    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>
    /// <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 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 (target.Hp is null)
        {
            target.Hp = new Health
            {
                Percentage = packet.HpPercentage
            };
        }
        else
        {
            target.Hp.Percentage = packet.HpPercentage;
        }

        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.IsOnCooldown = true;
            }
            else
            {
                var skillInfoResult = await _infoService.GetSkillInfoAsync(packet.SkillVNum, ct);
                skillEntity = new Skill
                    (packet.SkillVNum, null, skillInfoResult.IsSuccess ? skillInfoResult.Entity : null);
            }
        }
        else
        {
            var skillInfoResult = await _infoService.GetSkillInfoAsync(packet.SkillVNum, ct);
            if (!skillInfoResult.IsSuccess)
            {
                _logger.LogWarning
                (
                    "Could not obtain a skill info for vnum {vnum}: {error}",
                    packet.SkillVNum,
                    skillInfoResult.ToFullString()
                );
            }

            skillEntity = new Skill
                (packet.SkillVNum, null, skillInfoResult.IsSuccess ? skillInfoResult.Entity : null);
        }

        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 />
    public async Task<Result> Respond(PacketEventArgs<SrPacket> packetArgs, CancellationToken ct = default)
    {
        var packet = packetArgs.Packet;
        var character = _game.Character;

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

            if (skillResult.IsDefined(out var skillEntity))
            {
                skillEntity.IsOnCooldown = false;
                await _eventDispatcher.DispatchEvent(new SkillReadyEvent(skillEntity, skillEntity.SkillVNum), ct);
            }
        }
        else
        {
            await _eventDispatcher.DispatchEvent(new SkillReadyEvent(null, packet.SkillId), ct);
        }

        return Result.FromSuccess();
    }
}
\ No newline at end of file

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

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

namespace NosSmooth.Game.PacketHandlers.Specialists;

/// <summary>
/// Responds to sp packet.
/// </summary>
public class SpResponder : IPacketResponder<SpPacket>
{
    private readonly Game _game;

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

    /// <inheritdoc />
    public Task<Result> Respond(PacketEventArgs<SpPacket> packetArgs, CancellationToken ct = default)
    {
        var packet = packetArgs.Packet;
        var character = _game.Character;

        if (character is null)
        {
            return Task.FromResult(Result.FromSuccess());
        }

        character.SpPoints = packet.SpPoints;
        character.AdditionalSpPoints = packet.AdditionalSpPoints;

        return Task.FromResult(Result.FromSuccess());
    }
}
\ No newline at end of file

M NosSmooth.sln => NosSmooth.sln +30 -0
@@ 42,6 42,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{99E7
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataBrowser", "Samples\DataBrowser\DataBrowser.csproj", "{055C66A7-640C-49BB-81A7-28E630F51C37}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Game", "Core\NosSmooth.Game\NosSmooth.Game.csproj", "{7C9C7375-6FC0-4704-9332-1F74CDF41D11}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileClient", "Samples\FileClient\FileClient.csproj", "{D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}"
EndProject
Global
	GlobalSection(SolutionConfigurationPlatforms) = preSolution
		Debug|Any CPU = Debug|Any CPU


@@ 184,6 188,18 @@ Global
		{F1884ADF-6412-4E9B-81FD-357DC5761ADF}.Release|x64.Build.0 = Release|Any CPU
		{F1884ADF-6412-4E9B-81FD-357DC5761ADF}.Release|x86.ActiveCfg = Release|Any CPU
		{F1884ADF-6412-4E9B-81FD-357DC5761ADF}.Release|x86.Build.0 = Release|Any CPU
		{7C9C7375-6FC0-4704-9332-1F74CDF41D11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{7C9C7375-6FC0-4704-9332-1F74CDF41D11}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{7C9C7375-6FC0-4704-9332-1F74CDF41D11}.Debug|x64.ActiveCfg = Debug|Any CPU
		{7C9C7375-6FC0-4704-9332-1F74CDF41D11}.Debug|x64.Build.0 = Debug|Any CPU
		{7C9C7375-6FC0-4704-9332-1F74CDF41D11}.Debug|x86.ActiveCfg = Debug|Any CPU
		{7C9C7375-6FC0-4704-9332-1F74CDF41D11}.Debug|x86.Build.0 = Debug|Any CPU
		{7C9C7375-6FC0-4704-9332-1F74CDF41D11}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{7C9C7375-6FC0-4704-9332-1F74CDF41D11}.Release|Any CPU.Build.0 = Release|Any CPU
		{7C9C7375-6FC0-4704-9332-1F74CDF41D11}.Release|x64.ActiveCfg = Release|Any CPU
		{7C9C7375-6FC0-4704-9332-1F74CDF41D11}.Release|x64.Build.0 = Release|Any CPU
		{7C9C7375-6FC0-4704-9332-1F74CDF41D11}.Release|x86.ActiveCfg = Release|Any CPU
		{7C9C7375-6FC0-4704-9332-1F74CDF41D11}.Release|x86.Build.0 = Release|Any CPU
		{055C66A7-640C-49BB-81A7-28E630F51C37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{055C66A7-640C-49BB-81A7-28E630F51C37}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{055C66A7-640C-49BB-81A7-28E630F51C37}.Debug|x64.ActiveCfg = Debug|Any CPU


@@ 196,6 212,18 @@ Global
		{055C66A7-640C-49BB-81A7-28E630F51C37}.Release|x64.Build.0 = Release|Any CPU
		{055C66A7-640C-49BB-81A7-28E630F51C37}.Release|x86.ActiveCfg = Release|Any CPU
		{055C66A7-640C-49BB-81A7-28E630F51C37}.Release|x86.Build.0 = Release|Any CPU
		{D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Debug|x64.ActiveCfg = Debug|Any CPU
		{D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Debug|x64.Build.0 = Debug|Any CPU
		{D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Debug|x86.ActiveCfg = Debug|Any CPU
		{D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Debug|x86.Build.0 = Debug|Any CPU
		{D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Release|Any CPU.Build.0 = Release|Any CPU
		{D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Release|x64.ActiveCfg = Release|Any CPU
		{D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Release|x64.Build.0 = Release|Any CPU
		{D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Release|x86.ActiveCfg = Release|Any CPU
		{D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Release|x86.Build.0 = Release|Any CPU
	EndGlobalSection
	GlobalSection(SolutionProperties) = preSolution
		HideSolutionNode = FALSE


@@ 212,7 240,9 @@ Global
		{C4114AC1-72E8-46DA-9B4B-A4C942004492} = {1C785A74-19B9-42D2-93B1-F4EC9D6A8CFD}
		{4820B9B7-00E6-4E0C-B93A-83BB98C1EE99} = {1C785A74-19B9-42D2-93B1-F4EC9D6A8CFD}
		{F1884ADF-6412-4E9B-81FD-357DC5761ADF} = {1C785A74-19B9-42D2-93B1-F4EC9D6A8CFD}
		{7C9C7375-6FC0-4704-9332-1F74CDF41D11} = {01B5E872-271F-4D30-A1AA-AD48D81840C5}
		{055C66A7-640C-49BB-81A7-28E630F51C37} = {99E72557-BCE9-496A-B49C-79537B0E6063}
		{D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B} = {99E72557-BCE9-496A-B49C-79537B0E6063}
	EndGlobalSection
	GlobalSection(ExtensibilityGlobals) = postSolution
		SolutionGuid = {C5F46653-4DEC-429B-8580-4ED18ED9B4CA}

M Packets/NosSmooth.Packets/Client/Battle/UseAOESkillPacket.cs => Packets/NosSmooth.Packets/Client/Battle/UseAOESkillPacket.cs +1 -1
@@ 24,4 24,4 @@ public record UseAOESkillPacket
    short PositionX,
    [PacketIndex(2)]
    short PositionY
);
\ No newline at end of file
) : IPacket;
\ No newline at end of file

M Packets/NosSmooth.Packets/Client/Battle/UseSkillPacket.cs => Packets/NosSmooth.Packets/Client/Battle/UseSkillPacket.cs +1 -1
@@ 29,4 29,4 @@ public record UseSkillPacket
    short? MapX,
    [PacketIndex(4, IsOptional = true)]
    short? MapY
);
\ No newline at end of file
) : IPacket;
\ No newline at end of file

A Packets/NosSmooth.Packets/Enums/MinilandState.cs => Packets/NosSmooth.Packets/Enums/MinilandState.cs +28 -0
@@ 0,0 1,28 @@
//
//  MinilandState.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.Packets.Enums;

/// <summary>
/// State of a miniland.
/// </summary>
public enum MinilandState
{
    /// <summary>
    /// The miniland is open for anybody.
    /// </summary>
    Open,

    /// <summary>
    /// The miniland is closed, cannot be accessed by anyone.
    /// </summary>
    Private,

    /// <summary>
    /// The miniland is locked, cannot be accessed and objects can be built.
    /// </summary>
    Lock,
}
\ No newline at end of file

M Packets/NosSmooth.Packets/NosSmooth.Packets.csproj => Packets/NosSmooth.Packets/NosSmooth.Packets.csproj +2 -3
@@ 7,10 7,9 @@
        <Description>Contains default NosTale packets.</Description>
        <RepositoryUrl>https://github.com/Rutherther/NosSmooth/</RepositoryUrl>
        <PackageLicenseExpression>MIT</PackageLicenseExpression>
        <PackageVersion>2.0.0</PackageVersion>
        <PackageVersion>2.1.0</PackageVersion>
        <TargetFrameworks>net6.0;netstandard2.1</TargetFrameworks>
        <PackageReleaseNotes>Add couple of packet types.
Change namespace of some packets.</PackageReleaseNotes>
        <PackageReleaseNotes>Add couple of packet types.</PackageReleaseNotes>
    </PropertyGroup>

    <ItemGroup>

M Packets/NosSmooth.Packets/Server/Maps/OutPacket.cs => Packets/NosSmooth.Packets/Server/Maps/OutPacket.cs +1 -1
@@ 14,7 14,7 @@ namespace NosSmooth.Packets.Server.Maps;
/// </summary>
/// <param name="EntityType">The entity type.</param>
/// <param name="EntityId">The entity id.</param>
[PacketHeader("c_map", PacketSource.Server)]
[PacketHeader("out", PacketSource.Server)]
[GenerateSerializer(true)]
public record OutPacket
(

A Packets/NosSmooth.Packets/Server/Miniland/MlInfoBrPacket.cs => Packets/NosSmooth.Packets/Server/Miniland/MlInfoBrPacket.cs +37 -0
@@ 0,0 1,37 @@
//
//  MlInfoBrPacket.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.PacketSerializer.Abstractions.Attributes;
using NosSmooth.PacketSerializer.Abstractions.Common;

namespace NosSmooth.Packets.Server.Miniland;

/// <summary>
/// Miniland info packet. For minilands not owned by the playing character.
/// </summary>
/// <param name="MinilandMusicId">The id of the music. 3800 by default.</param>
/// <param name="OwnerName">The name of the owner.</param>
/// <param name="DailyVisitCount">The number of daily visits.</param>
/// <param name="VisitCount">The number of total visits.</param>
/// <param name="Unknown">Unknown TODO.</param>
/// <param name="MinilandMessage">The welcome message.</param>
[PacketHeader("mlinfobr", PacketSource.Server)]
[GenerateSerializer(true)]
public record MlInfoBrPacket
(
    [PacketIndex(0)]
    short MinilandMusicId,
    [PacketIndex(1)]
    NameString OwnerName,
    [PacketIndex(2)]
    int DailyVisitCount,
    [PacketIndex(3)]
    int VisitCount,
    [PacketIndex(4)]
    byte Unknown,
    [PacketGreedyIndex(5)]
    string MinilandMessage
) : IPacket;
\ No newline at end of file

A Packets/NosSmooth.Packets/Server/Miniland/MlInfoPacket.cs => Packets/NosSmooth.Packets/Server/Miniland/MlInfoPacket.cs +47 -0
@@ 0,0 1,47 @@
//
//  MlInfoPacket.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.Packets.Enums;
using NosSmooth.PacketSerializer.Abstractions.Attributes;
using NosSmooth.PacketSerializer.Abstractions.Common;

namespace NosSmooth.Packets.Server.Miniland;

/// <summary>
/// Miniland info packet. For miniland owned by the playing character.
/// </summary>
/// <param name="MinilandMusicId">The id of the music. 3800 by default.</param>
/// <param name="MinilandPoints">The points of the miniland.</param>
/// <param name="Unknown">Unknown TODO.</param>
/// <param name="DailyVisitCount">The number of daily visits.</param>
/// <param name="VisitCount">The number of total visits.</param>
/// <param name="Unknown1">Unknown TODO.</param>
/// <param name="MinilandState">The state of the miniland.</param>
/// <param name="MinilandMusicName">The name of the miniland music.</param>
/// <param name="MinilandMessage">The welcome message.</param>
[PacketHeader("mlinfo", PacketSource.Server)]
[GenerateSerializer(true)]
public record MlInfoPacket
(
    [PacketIndex(0)]
    short MinilandMusicId,
    [PacketIndex(1)]
    long MinilandPoints,
    [PacketIndex(2)]
    byte Unknown,
    [PacketIndex(3)]
    int DailyVisitCount,
    [PacketIndex(4)]
    int VisitCount,
    [PacketIndex(5)]
    byte Unknown1,
    [PacketIndex(6)]
    MinilandState MinilandState,
    [PacketIndex(7)]
    NameString MinilandMusicName,
    [PacketGreedyIndex(8)]
    string MinilandMessage
) : IPacket;
\ No newline at end of file

A Packets/NosSmooth.Packets/Server/Miniland/MlObjLstPacket.cs => Packets/NosSmooth.Packets/Server/Miniland/MlObjLstPacket.cs +21 -0
@@ 0,0 1,21 @@
//
//  MlObjLstPacket.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.PacketSerializer.Abstractions.Attributes;

namespace NosSmooth.Packets.Server.Miniland;

/// <summary>
/// Miniland object list packet.
/// </summary>
/// <param name="Objects">The objects in the miniland or inventory.</param>
[PacketHeader("mlobjlst", PacketSource.Server)]
[GenerateSerializer(true)]
public record MlObjLstPacket
(
    [PacketListIndex(0, ListSeparator = ' ', InnerSeparator = '.')]
    IReadOnlyList<MlObjPacket> Objects
) : IPacket;
\ No newline at end of file

A Packets/NosSmooth.Packets/Server/Miniland/MlObjPacket.cs => Packets/NosSmooth.Packets/Server/Miniland/MlObjPacket.cs +48 -0
@@ 0,0 1,48 @@
//
//  MlObjPacket.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.PacketSerializer.Abstractions.Attributes;

namespace NosSmooth.Packets.Server.Miniland;

/// <summary>
/// Miniland object packet.
/// </summary>
/// <param name="Slot">The slot in the inventory.</param>
/// <param name="InUse">Whether the item is placed in the miniland.</param>
/// <param name="X">The x coordinate, if in use.</param>
/// <param name="Y">The y coordinate, if in use.</param>
/// <param name="Width">The width of the object.</param>
/// <param name="Height">The height of the object.</param>
/// <param name="Unknown">Unknown TODO.</param>
/// <param name="DurabilityPoints">The durability points of a minigame.</param>
/// <param name="Unknown1">Unknown TODO.</param>
/// <param name="Unknown2">Unknown TODO.</param>
[PacketHeader("mlobj", PacketSource.Server)]
[GenerateSerializer(true)]
public record MlObjPacket
(
    [PacketIndex(0)]
    short Slot,
    [PacketIndex(1)]
    bool InUse,
    [PacketIndex(2)]
    short X,
    [PacketIndex(3)]
    short Y,
    [PacketIndex(4)]
    byte Width,
    [PacketIndex(5)]
    byte Height,
    [PacketIndex(6)]
    byte Unknown,
    [PacketIndex(7)]
    int DurabilityPoints,
    [PacketIndex(8)]
    bool Unknown1,
    [PacketIndex(9)]
    bool Unknown2
) : IPacket;
\ No newline at end of file

A Packets/NosSmooth.Packets/Server/Miniland/MltObjPacket.cs => Packets/NosSmooth.Packets/Server/Miniland/MltObjPacket.cs +24 -0
@@ 0,0 1,24 @@
//
//  MltObjPacket.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.PacketSerializer.Abstractions.Attributes;

namespace NosSmooth.Packets.Server.Miniland;

/// <summary>
/// Miniland objects list packet.
/// </summary>
/// <remarks>
/// Used for minilands of different owners.
/// </remarks>
/// <param name="Objects">The miniland objects.</param>
[PacketHeader("mltobj", PacketSource.Server)]
[GenerateSerializer(true)]
public record MltObjPacket
(
    [PacketIndex(0)]
    IReadOnlyList<MltObjSubPacket> Objects
) : IPacket;
\ No newline at end of file

A Packets/NosSmooth.Packets/Server/Miniland/MltObjSubPacket.cs => Packets/NosSmooth.Packets/Server/Miniland/MltObjSubPacket.cs +30 -0
@@ 0,0 1,30 @@
//
//  MltObjSubPacket.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.PacketSerializer.Abstractions.Attributes;

namespace NosSmooth.Packets.Server.Miniland;

/// <summary>
/// Sub packet of <see cref="MltObjPacket"/>.
/// </summary>
/// <param name="VNum">The vnum of the item.</param>
/// <param name="Slot">The slot.</param>
/// <param name="X">The x coordinate.</param>
/// <param name="Y">The y coordinate.</param>
[PacketHeader("mltobjsub", PacketSource.Server)]
[GenerateSerializer(true)]
public record MltObjSubPacket
(
    [PacketIndex(0)]
    int VNum,
    [PacketIndex(1)]
    int Slot,
    [PacketIndex(2)]
    short X,
    [PacketIndex(3)]
    short Y
) : IPacket;
\ No newline at end of file

M Packets/NosSmooth.Packets/Server/Players/CInfoPacket.cs => Packets/NosSmooth.Packets/Server/Players/CInfoPacket.cs +67 -4
@@ 4,11 4,74 @@
//  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.Packets.Enums;
using NosSmooth.Packets.Enums.Players;
using NosSmooth.PacketSerializer.Abstractions.Attributes;

namespace NosSmooth.Packets.Server.Players;

/// <summary>
/// Character information.
/// Information about the playing character.
/// </summary>
// [PacketHeader()]
// [GenerateSerializer(true)]
public record CInfoPacket();
\ No newline at end of file
/// <remarks>
/// Sent on login and when changing map.
/// </remarks>
/// <param name="Name">The name of the character.</param>
/// <param name="Unknown">Unknown TODO</param>
/// <param name="GroupId">The id of the group the player is in, if any.</param>
/// <param name="FamilyId">The id of the family the player is in, if any.</param>
/// <param name="FamilyName">The name of the family the player is in, if any.</param>
/// <param name="CharacterId">The id of the character.</param>
/// <param name="Authority">The authority of the character.</param>
/// <param name="Sex">The sex of the character.</param>
/// <param name="HairStyle">The hair style of the character.</param>
/// <param name="HairColor">The hair color of the character.</param>
/// <param name="Class">The class of the character.</param>
/// <param name="Icon">Unknown TODO</param>
/// <param name="Compliment">Unknown TODO</param>
/// <param name="MorphVNum">The vnum of the morph (used for special cards, vehicles and such).</param>
/// <param name="IsInvisible">Whether the character is invisible.</param>
/// <param name="FamilyLevel">The level of the family, if any.</param>
/// <param name="MorphUpgrade">The upgrade of the morph (wings)</param>
/// <param name="ArenaWinner">Whether the character is an arena winner.</param>
[PacketHeader("c_info", PacketSource.Server)]
[GenerateSerializer(true)]
public record CInfoPacket
(
    [PacketIndex(0)]
    string Name,
    [PacketIndex(1)]
    string? Unknown,
    [PacketIndex(2)]
    short? GroupId,
    [PacketIndex(3)]
    string? FamilyId,
    [PacketIndex(4)]
    string? FamilyName,
    [PacketIndex(5)]
    long CharacterId,
    [PacketIndex(6)]
    AuthorityType Authority,
    [PacketIndex(7)]
    SexType Sex,
    [PacketIndex(8)]
    HairStyle HairStyle,
    [PacketIndex(9)]
    HairColor HairColor,
    [PacketIndex(10)]
    PlayerClass Class,
    [PacketIndex(11)]
    byte Icon,
    [PacketIndex(12)]
    short Compliment,
    [PacketIndex(13)]
    short MorphVNum,
    [PacketIndex(14)]
    bool IsInvisible,
    [PacketIndex(15)]
    byte? FamilyLevel,
    [PacketIndex(16)]
    byte MorphUpgrade,
    [PacketIndex(17)]
    bool ArenaWinner
) : IPacket;

M Packets/NosSmooth.Packets/Server/Players/LevPacket.cs => Packets/NosSmooth.Packets/Server/Players/LevPacket.cs +2 -2
@@ 22,7 22,7 @@ namespace NosSmooth.Packets.Server.Players;
/// <param name="JobXpLoad">Unknown TODO</param>
/// <param name="Reputation">The reputation of the player.</param>
/// <param name="SkillCp">The skill cp. (Used for learning skills)</param>
/// <param name="HeroXp">The xp in hero level. TODO</param>
/// <param name="HeroLevelXp">The xp in hero level. TODO</param>
/// <param name="HeroLevel">The hero level. (shown as (+xx))</param>
/// <param name="HeroXpLoad">Unknown TODO</param>
[PacketHeader("lev", PacketSource.Server)]


@@ 46,7 46,7 @@ public record LevPacket
    [PacketIndex(7)]
    int SkillCp,
    [PacketIndex(8)]
    long HeroXp,
    long HeroLevelXp,
    [PacketIndex(9)]
    byte HeroLevel,
    [PacketIndex(10)]

A Packets/NosSmooth.Packets/Server/Specialists/SdPacket.cs => Packets/NosSmooth.Packets/Server/Specialists/SdPacket.cs +24 -0
@@ 0,0 1,24 @@
//
//  SdPacket.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.PacketSerializer.Abstractions.Attributes;

namespace NosSmooth.Packets.Server.Specialists;

/// <summary>
/// Packet for sp cooldown.
/// </summary>
/// <remarks>
/// Doesn't block putting on the sp. Just shows loading on the character icon.
/// </remarks>
/// <param name="Cooldown">The cooldown.</param>
[PacketHeader("sd", PacketSource.Server)]
[GenerateSerializer(true)]
public record SdPacket
(
    [PacketIndex(0)]
    short Cooldown
) : IPacket;
\ No newline at end of file

A Packets/NosSmooth.Packets/Server/Specialists/SpPacket.cs => Packets/NosSmooth.Packets/Server/Specialists/SpPacket.cs +33 -0
@@ 0,0 1,33 @@
//
//  SpPacket.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.PacketSerializer.Abstractions.Attributes;

namespace NosSmooth.Packets.Server.Specialists;

/// <summary>
/// Packet with information about sp points.
/// </summary>
/// <remarks>
/// Sent on login, on sp change, on points change.
/// </remarks>
/// <param name="AdditionalSpPoints">The additional sp points used after sp points are 0.</param>
/// <param name="MaxAdditionalSpPoints">The maximum of additional sp points.</param>
/// <param name="SpPoints">The sp points that decrease upon using sp.</param>
/// <param name="MaxSpPoints">The maximum of sp points.</param>
[PacketHeader("sp", PacketSource.Server)]
[GenerateSerializer(true)]
public record SpPacket
(
    [PacketIndex(0)]
    int AdditionalSpPoints,
    [PacketIndex(1)]
    int MaxAdditionalSpPoints,
    [PacketIndex(2)]
    int SpPoints,
    [PacketIndex(3)]
    int MaxSpPoints
) : IPacket;
\ No newline at end of file

A Samples/FileClient/App.cs => Samples/FileClient/App.cs +77 -0
@@ 0,0 1,77 @@
//
//  App.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.Hosting;
using Microsoft.Extensions.Logging;
using NosSmooth.Core.Client;
using NosSmooth.Core.Extensions;
using NosSmooth.Data.NOSFiles;
using NosSmooth.Packets.Extensions;
using NosSmooth.Packets.Packets;
using Remora.Results;

namespace FileClient;

/// <summary>
/// The application.
/// </summary>
public class App : BackgroundService
{
    private readonly INostaleClient _client;
    private readonly IPacketTypesRepository _packetRepository;
    private readonly NostaleDataFilesManager _filesManager;
    private readonly ILogger<App> _logger;
    private readonly IHostLifetime _lifetime;

    /// <summary>
    /// Initializes a new instance of the <see cref="App"/> class.
    /// </summary>
    /// <param name="client">The client.</param>
    /// <param name="packetRepository">The packet repository.</param>
    /// <param name="filesManager">The file manager.</param>
    /// <param name="logger">The logger.</param>
    /// <param name="lifetime">The lifetime.</param>
    public App
    (
        INostaleClient client,
        IPacketTypesRepository packetRepository,
        NostaleDataFilesManager filesManager,
        ILogger<App> logger,
        IHostLifetime lifetime
    )
    {
        _client = client;
        _packetRepository = packetRepository;
        _filesManager = filesManager;
        _logger = logger;
        _lifetime = lifetime;
    }

    /// <inheritdoc />
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var packetResult = _packetRepository.AddDefaultPackets();
        if (!packetResult.IsSuccess)
        {
            _logger.LogResultError(packetResult);
            return;
        }

        var filesResult = _filesManager.Initialize();
        if (!filesResult.IsSuccess)
        {
            _logger.LogResultError(filesResult);
            return;
        }

        var runResult = await _client.RunAsync(stoppingToken);
        if (!runResult.IsSuccess)
        {
            _logger.LogResultError(runResult);
            await _lifetime.StopAsync(default);
        }
    }
}
\ No newline at end of file

A Samples/FileClient/Client.cs => Samples/FileClient/Client.cs +126 -0
@@ 0,0 1,126 @@
//
//  Client.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.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
using NosSmooth.Core.Client;
using NosSmooth.Core.Commands;
using NosSmooth.Core.Extensions;
using NosSmooth.Core.Packets;
using NosSmooth.Packets;
using NosSmooth.Packets.Errors;
using NosSmooth.PacketSerializer.Abstractions.Attributes;
using Remora.Results;

namespace FileClient;

/// <summary>
/// A NosTale client using stream to read lines.
/// </summary>
public class Client : BaseNostaleClient
{
    private const string LineRegex = ".*\\[(Recv|Send)\\]\t(.*)";
    private readonly IPacketHandler _packetHandler;
    private readonly IPacketSerializer _packetSerializer;
    private readonly ILogger<Client> _logger;
    private readonly Stream _stream;

    /// <summary>
    /// Initializes a new instance of the <see cref="Client"/> class.
    /// </summary>
    /// <param name="stream">The stream with packets.</param>
    /// <param name="packetHandler">The packet handler.</param>
    /// <param name="commandProcessor">The command processor.</param>
    /// <param name="packetSerializer">The packet serializer.</param>
    /// <param name="logger">The logger.</param>
    public Client(
        Stream stream,
        IPacketHandler packetHandler,
        CommandProcessor commandProcessor,
        IPacketSerializer packetSerializer,
        ILogger<Client> logger
    )
        : base(commandProcessor, packetSerializer)
    {
        _stream = stream;
        _packetHandler = packetHandler;
        _packetSerializer = packetSerializer;
        _logger = logger;
    }

    /// <inheritdoc />
    public override async Task<Result> RunAsync(CancellationToken stopRequested = default)
    {
        using var reader = new StreamReader(_stream);
        var regex = new Regex(LineRegex);
        while (!reader.EndOfStream)
        {
            stopRequested.ThrowIfCancellationRequested();
            var line = await reader.ReadLineAsync();
            if (line is null)
            {
                continue;
            }

            var match = regex.Match(line);
            if (!match.Success)
            {
                _logger.LogError("Could not find match on line {Line}", line);
                continue;
            }

            var type = match.Groups[1].Value;
            var packetStr = match.Groups[2].Value;

            var source = type == "Recv" ? PacketSource.Server : PacketSource.Client;
            var packet = CreatePacket(packetStr, source);
            Result result;
            if (source == PacketSource.Client)
            {
                result = await _packetHandler.HandleSentPacketAsync(packet, packetStr, stopRequested);
            }
            else
            {
                result = await _packetHandler.HandleReceivedPacketAsync(packet, packetStr, stopRequested);
            }

            if (!result.IsSuccess)
            {
                _logger.LogResultError(result);
            }
        }

        return Result.FromSuccess();
    }

    /// <inheritdoc/>
    public override async Task<Result> SendPacketAsync(string packetString, CancellationToken ct = default)
    {
        return await _packetHandler.HandleReceivedPacketAsync(CreatePacket(packetString, PacketSource.Client), packetString, ct);
    }

    /// <inheritdoc/>
    public override async Task<Result> ReceivePacketAsync(string packetString, CancellationToken ct = default)
    {
        return await _packetHandler.HandleReceivedPacketAsync(CreatePacket(packetString, PacketSource.Server), packetString, ct);
    }

    private IPacket CreatePacket(string packetStr, PacketSource source)
    {
        var packetResult = _packetSerializer.Deserialize(packetStr, source);
        if (!packetResult.IsSuccess)
        {
            if (packetResult.Error is PacketConverterNotFoundError err)
            {
                return new UnresolvedPacket(err.Header, packetStr);
            }

            return new ParsingFailedPacket(packetResult, packetStr);
        }

        return packetResult.Entity;
    }
}
\ No newline at end of file

A Samples/FileClient/FileClient.csproj => Samples/FileClient/FileClient.csproj +27 -0
@@ 0,0 1,27 @@
<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net6.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
    </PropertyGroup>

    <ItemGroup>
      <Folder Include="Handlers" />
    </ItemGroup>

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

    <ItemGroup>
      <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
      <PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.0" />
      <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
    </ItemGroup>

</Project>

A Samples/FileClient/Program.cs => Samples/FileClient/Program.cs +66 -0
@@ 0,0 1,66 @@
//
//  Program.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.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using NosSmooth.Core.Client;
using NosSmooth.Core.Commands;
using NosSmooth.Core.Extensions;
using NosSmooth.Core.Packets;
using NosSmooth.Data.Abstractions.Language;
using NosSmooth.Data.NOSFiles.Extensions;
using NosSmooth.Data.NOSFiles.Options;
using NosSmooth.Game.Extensions;
using NosSmooth.Packets;

namespace FileClient;

/// <summary>
/// An entrypoint class.
/// </summary>
public static class Program
{
    // TODO: create console hosting.

    /// <summary>
    /// An entrypoint method.
    /// </summary>
    /// <param name="args">The command line arguments.</param>
    /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
    public static async Task Main(string[] args)
    {
        await using FileStream stream = File.OpenRead(string.Join(' ', args));
        await CreateHost(stream).StartAsync();
    }

    private static IHost CreateHost(Stream fileStream)
    {
        return Host.CreateDefaultBuilder()
            .ConfigureServices(coll =>
            {
                coll.AddHostedService<App>();

                coll.AddNostaleCore()
                    .AddNostaleGame()
                    .AddNostaleDataFiles()
                    .Configure<LanguageServiceOptions>(o => o.Language = Language.Cz)
                    .Configure<NostaleDataOptions>(o => o.SupportedLanguages = new[]
                    {
                        Language.Cz
                    });
                coll.AddSingleton<INostaleClient>(p => new Client(
                    fileStream,
                    p.GetRequiredService<IPacketHandler>(),
                    p.GetRequiredService<CommandProcessor>(),
                    p.GetRequiredService<IPacketSerializer>(),
                    p.GetRequiredService<ILogger<Client>>()
                ));
            })
            .UseConsoleLifetime()
            .Build();
    }
}
\ No newline at end of file

M Tests/NosSmooth.Packets.Tests/Converters/Packets/InPacketConverterTests.cs => Tests/NosSmooth.Packets.Tests/Converters/Packets/InPacketConverterTests.cs +3 -2
@@ 11,6 11,7 @@ using NosSmooth.Packets.Enums.Entities;
using NosSmooth.Packets.Enums.Players;
using NosSmooth.Packets.Extensions;
using NosSmooth.Packets.Server.Entities;
using NosSmooth.Packets.Server.Maps;
using NosSmooth.Packets.Server.Players;
using NosSmooth.Packets.Server.Weapons;
using NosSmooth.PacketSerializer.Abstractions.Attributes;


@@ 90,7 91,7 @@ public class InPacketConverterTests
                new UpgradeRareSubPacket(10, 8),
                new FamilySubPacket(null, null),
                null,
                "26",
                26,
                false,
                0,
                0,


@@ 161,7 162,7 @@ public class InPacketConverterTests
                new UpgradeRareSubPacket(10, 8),
                new FamilySubPacket("-1", null),
                null,
                "26",
                26,
                false,
                0,
                0,

Do not follow this link