~ruther/NosSmooth

6e4b542e71df0691dbf4a6644d68c084f6cf7c85 — Rutherther 2 years ago b54039f + 092bded
Merge pull request #45 from Rutherther/feat/mates

Add support for mates
52 files changed, 1515 insertions(+), 55 deletions(-)

M Core/NosSmooth.Game/Data/Entities/GroundItem.cs
A Core/NosSmooth.Game/Data/Items/PartnerEquipment.cs
A Core/NosSmooth.Game/Data/Mates/Mate.cs
A Core/NosSmooth.Game/Data/Mates/Mates.cs
A Core/NosSmooth.Game/Data/Mates/Partner.cs
A Core/NosSmooth.Game/Data/Mates/PartnerSkill.cs
A Core/NosSmooth.Game/Data/Mates/PartnerSp.cs
A Core/NosSmooth.Game/Data/Mates/PartyPartner.cs
A Core/NosSmooth.Game/Data/Mates/PartyPet.cs
A Core/NosSmooth.Game/Data/Mates/Pet.cs
M Core/NosSmooth.Game/Data/Social/Group.cs
M Core/NosSmooth.Game/Data/Social/GroupMember.cs
A Core/NosSmooth.Game/Data/Stats/MateArmorStats.cs
A Core/NosSmooth.Game/Data/Stats/MateAttackStats.cs
A Core/NosSmooth.Game/Data/Stats/Resistance.cs
M Core/NosSmooth.Game/Events/Groups/GroupInitializedEvent.cs
M Core/NosSmooth.Game/Events/Groups/GroupMemberStatEvent.cs
A Core/NosSmooth.Game/Events/Mates/MateStatEvent.cs
A Core/NosSmooth.Game/Events/Mates/MatesPartyInitializedEvent.cs
R Core/NosSmooth.Game/{Data/Entities/Partner => Events/Mates/PartnerInitializedEvent}.cs
A Core/NosSmooth.Game/Events/Mates/PartnerSkillsReceivedEvent.cs
R Core/NosSmooth.Game/{Data/Entities/Pet => Events/Mates/PetInitializedEvent}.cs
A Core/NosSmooth.Game/Events/Mates/PetSkillReceivedEvent.cs
R Core/NosSmooth.Game/{Data/Entities/IPet => Extensions/LivingEntityExtensions}.cs
M Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs
M Core/NosSmooth.Game/Game.cs
M Core/NosSmooth.Game/GameSemaphoreType.cs
M Core/NosSmooth.Game/PacketHandlers/Relations/FriendInitResponder.cs
A Core/NosSmooth.Game/PacketHandlers/Relations/MatesInitResponder.cs
A Core/NosSmooth.Game/PacketHandlers/Skills/MatesSkillResponder.cs
M Core/NosSmooth.Game/PacketHandlers/Skills/PlayerSkillResponder.cs
A Packets/NosSmooth.PacketSerializer.Abstractions/NullableWrapper.cs
A Packets/NosSmooth.PacketSerializer/Converters/Common/NullableWrapperConverter.cs
A Packets/NosSmooth.PacketSerializer/Converters/Common/NullableWrapperConverterFactory.cs
M Packets/NosSmooth.PacketSerializer/Extensions/ServiceCollectionExtensions.cs
M Packets/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/BasicInlineConverterGenerator.cs
M Packets/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/ListInlineConverterGenerator.cs
M Packets/NosSmooth.PacketSerializersGenerator/SourceGenerator.cs
A Packets/NosSmooth.Packets/Client/Battle/UsePetSkillPacket.cs
M Packets/NosSmooth.Packets/Server/Entities/EffectsSubPacket.cs
M Packets/NosSmooth.Packets/Server/Entities/RevivePacket.cs
M Packets/NosSmooth.Packets/Server/Groups/PinitMateSubPacket.cs
M Packets/NosSmooth.Packets/Server/Groups/PinitPlayerSubPacket.cs
M Packets/NosSmooth.Packets/Server/Groups/PstPacket.cs
M Packets/NosSmooth.Packets/Server/Login/CListPacket.cs
M Packets/NosSmooth.Packets/Server/Mates/ScNEquipmentSubPacket.cs
M Packets/NosSmooth.Packets/Server/Mates/ScNPacket.cs
M Packets/NosSmooth.Packets/Server/Mates/ScNSkillSubPacket.cs
M Packets/NosSmooth.Packets/Server/Mates/ScNSpSubPacket.cs
M Packets/NosSmooth.Packets/Server/Relations/FInfoPacket.cs
A Packets/NosSmooth.Packets/Server/Relations/FInfoSubPacket.cs
A Samples/FileClient/Responders/InventoryInitializedResponder.cs
M Core/NosSmooth.Game/Data/Entities/GroundItem.cs => Core/NosSmooth.Game/Data/Entities/GroundItem.cs +1 -1
@@ 18,7 18,7 @@ public class GroundItem : IEntity
    /// <summary>
    /// Gets or sets the id of the owner, if any.
    /// </summary>
    public long? OwnerId { get; set; }
    public long? OwnerId { get; internal set; }

    /// <summary>
    /// Gets or sets the amount of the item on the ground.

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

/// <summary>
/// An equipment of a partner.
/// </summary>
/// <param name="Weapon">The weapon of the partner.</param>
/// <param name="Armor">The armor of the partner.</param>
/// <param name="Gauntlet">The gauntlet of the partner.</param>
/// <param name="Boots">The boots of the partner.</param>
public record PartnerEquipment
(
    UpgradeableItem? Weapon,
    UpgradeableItem? Armor,
    UpgradeableItem? Gauntlet,
    UpgradeableItem? Boots
);
\ No newline at end of file

A Core/NosSmooth.Game/Data/Mates/Mate.cs => Core/NosSmooth.Game/Data/Mates/Mate.cs +48 -0
@@ 0,0 1,48 @@
//
//  Mate.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.Stats;
using NosSmooth.Packets.Enums;
using NosSmooth.PacketSerializer.Abstractions.Common;

namespace NosSmooth.Game.Data.Mates;

/// <summary>
/// Information about player's pet or partner.
/// </summary>
/// <remarks>
/// Used for mates the character owns
/// </remarks>
/// <param name="MateId">The id of the mate.</param>
/// <param name="NpcVNum">The vnum of the mate.</param>
/// <param name="TransportId">Unknown function TODO.</param>
/// <param name="Level">The level of the mate.</param>
/// <param name="Loyalty">The loyalty of the mate.</param>
/// <param name="Attack">The attack statistics of the mate.</param>
/// <param name="Armor">The armor statistics of the mate.</param>
/// <param name="Element">The element of the mate.</param>
/// <param name="Resistance">The resistance of the mate.</param>
/// <param name="Hp">The health of the mate.</param>
/// <param name="Mp">The mana of the mate.</param>
/// <param name="Name">The name of the mate.</param>
/// <param name="IsSummonable">Whether the mate is summonable.</param>
public record Mate
(
    long MateId,
    long NpcVNum,
    long TransportId,
    Level Level,
    short Loyalty,
    MateAttackStats Attack,
    MateArmorStats Armor,
    Element Element,
    Resistance Resistance,
    Health Hp,
    Health Mp,
    string Name,
    bool IsSummonable
);
\ No newline at end of file

A Core/NosSmooth.Game/Data/Mates/Mates.cs => Core/NosSmooth.Game/Data/Mates/Mates.cs +94 -0
@@ 0,0 1,94 @@
//
//  Mates.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;
using System.Collections.Concurrent;
using NosSmooth.Game.Data.Characters;

namespace NosSmooth.Game.Data.Mates;

/// <summary>
/// Game mates state.
/// </summary>
public class Mates : IEnumerable<Mate>
{
    private ConcurrentDictionary<long, Partner> _partners;
    private ConcurrentDictionary<long, Pet> _pets;

    /// <summary>
    /// Initializes a new instance of the <see cref="Mates"/> class.
    /// </summary>
    public Mates()
    {
        _partners = new ConcurrentDictionary<long, Partner>();
        _pets = new ConcurrentDictionary<long, Pet>();
    }

    /// <summary>
    /// Gets all of the partners belonging to the character.
    /// </summary>
    public IEnumerable<Partner> Partners => _partners.Values;

    /// <summary>
    /// Gets all of the pets belonging to the character.
    /// </summary>
    public IEnumerable<Pet> Pets => _pets.Values;

    /// <summary>
    /// Gets the current skill of pet, if there is any.
    /// </summary>
    public Skill? PetSkill { get; internal set; }

    /// <summary>
    /// Get sthe current skills of partner(' sp).
    /// </summary>
    public IReadOnlyList<Skill>? PartnerSkills { get; internal set; }

    /// <summary>
    /// Gets the current pet of the client.
    /// </summary>
    public PartyPet? CurrentPet { get; internal set; }

    /// <summary>
    /// Gets the current partner of the client.
    /// </summary>
    public PartyPartner? CurrentPartner { get; internal set; }

    /// <inheritdoc />
    public IEnumerator<Mate> GetEnumerator()
        => _partners.Values.Cast<Mate>().Concat(_pets.Values.Cast<Mate>()).GetEnumerator();

    /// <inheritdoc/>
    IEnumerator IEnumerable.GetEnumerator()
        => GetEnumerator();

    /// <summary>
    /// Sets pet of the given id.
    /// </summary>
    /// <param name="pet">The pet.</param>
    internal void SetPet(Pet pet)
    {
        _pets[pet.MateId] = pet;
    }

    /// <summary>
    /// Sets partner of the given id.
    /// </summary>
    /// <param name="partner">The partner.</param>
    internal void SetPartner(Partner partner)
    {
        _partners[partner.MateId] = partner;
    }

    /// <summary>
    /// Clears partners and pets.
    /// </summary>
    internal void Clear()
    {
        _partners.Clear();
        _pets.Clear();
    }
}
\ No newline at end of file

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

using NosSmooth.Game.Data.Info;
using NosSmooth.Game.Data.Items;
using NosSmooth.Game.Data.Stats;
using NosSmooth.Packets.Enums;

namespace NosSmooth.Game.Data.Mates;

/// <summary>
/// Information about player's partner
/// </summary>
/// <param name="MateId">The id of the mate.</param>
/// <param name="NpcVNum">The vnum of the mate.</param>
/// <param name="TransportId">Unknown function TODO.</param>
/// <param name="Level">The level of the mate.</param>
/// <param name="Loyalty">The loyalty of the mate.</param>
/// <param name="Attack">The attack statistics of the mate.</param>
/// <param name="Armor">The armor statistics of the mate.</param>
/// <param name="Equipment">The equipment of the partner.</param>
/// <param name="Element">The element of the mate.</param>
/// <param name="Resistance">The resistance of the mate.</param>
/// <param name="Hp">The health of the mate.</param>
/// <param name="Mp">The mana of the mate.</param>
/// <param name="MorphVNum">The morph vnum of the partner.</param>
/// <param name="Name">The name of the mate.</param>
/// <param name="IsSummonable">Whether the mate is summonable.</param>
/// <param name="Sp">The equipped sp of the partner.</param>
public record Partner
(
    long MateId,
    long NpcVNum,
    long TransportId,
    Level Level,
    short Loyalty,
    MateAttackStats Attack,
    MateArmorStats Armor,
    PartnerEquipment Equipment,
    Element Element,
    Resistance Resistance,
    Health Hp,
    Health Mp,
    string Name,
    int? MorphVNum,
    bool IsSummonable,
    PartnerSp? Sp
) : Mate
(
    MateId,
    NpcVNum,
    TransportId,
    Level,
    Loyalty,
    Attack,
    Armor,
    Element,
    Resistance,
    Hp,
    Mp,
    Name,
    IsSummonable
);
\ No newline at end of file

A Core/NosSmooth.Game/Data/Mates/PartnerSkill.cs => Core/NosSmooth.Game/Data/Mates/PartnerSkill.cs +24 -0
@@ 0,0 1,24 @@
//
//  PartnerSkill.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.Characters;
using NosSmooth.Packets.Enums.Mates;

namespace NosSmooth.Game.Data.Mates;

/// <summary>
/// A skill of a partner's sp.
/// </summary>
/// <param name="SkillVNum">The vnum of the skill.</param>
/// <param name="Rank">The partner rank of the skill.</param>
/// <param name="Info">The info of the skill.</param>
public record PartnerSkill
(
    int SkillVNum,
    PartnerSkillRank? Rank,
    ISkillInfo? Info
) : Skill(SkillVNum, null, Info);
\ No newline at end of file

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

/// <summary>
/// An sp of the partner.
/// </summary>
/// <param name="VNum">The vnum of the sp item.</param>
/// <param name="AgilityPercentage">The agility percentage for acquiring skills.</param>
/// <param name="Skill1">Information about the first skill of the partner.</param>
/// <param name="Skill2">Information about the second skill of the partner.</param>
/// <param name="Skill3">Information about the third skill of the partner.</param>
public record PartnerSp
(
    long VNum,
    byte? AgilityPercentage,
    PartnerSkill? Skill1,
    PartnerSkill? Skill2,
    PartnerSkill? Skill3
);
\ No newline at end of file

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

namespace NosSmooth.Game.Data.Mates;

/// <summary>
/// Information about a partner that is currently in character's party.
/// </summary>
/// <param name="Partner">The underlying partner.</param>
public record PartyPartner
(
    Partner Partner
)
{
    /// <summary>
    /// Gets the hp of the partner.
    /// </summary>
    public Health? Hp { get; internal set; }

    /// <summary>
    /// Gets the mp of the partner.
    /// </summary>
    public Health? Mp { get; internal set; }
}
\ No newline at end of file

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

namespace NosSmooth.Game.Data.Mates;

/// <summary>
/// Information about a pet that is currently in character's party.
/// </summary>
/// <param name="Pet">The underlying pet.</param>
public record PartyPet
(
    Pet Pet
)
{
    /// <summary>
    /// Gets the hp of the partner.
    /// </summary>
    public Health? Hp { get; internal set; }

    /// <summary>
    /// Gets the mp of the partner.
    /// </summary>
    public Health? Mp { get; internal set; }
}
\ No newline at end of file

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

using NosSmooth.Game.Data.Info;
using NosSmooth.Game.Data.Items;
using NosSmooth.Game.Data.Stats;
using NosSmooth.Packets.Enums;
using NosSmooth.PacketSerializer.Abstractions.Common;

namespace NosSmooth.Game.Data.Mates;

/// <summary>
/// Information about player's pet or partner.
/// </summary>
/// <remarks>
/// Used for mates the character owns
/// </remarks>
/// <param name="MateId">The id of the mate.</param>
/// <param name="NpcVNum">The vnum of the mate.</param>
/// <param name="TransportId">Unknown function TODO.</param>
/// <param name="Level">The level of the mate.</param>
/// <param name="Loyalty">The loyalty of the mate.</param>
/// <param name="Attack">The attack statistics of the mate.</param>
/// <param name="Armor">The armor statistics of the mate.</param>
/// <param name="Element">The element of the mate.</param>
/// <param name="Resistance">The resistance of the mate.</param>
/// <param name="Hp">The health of the mate.</param>
/// <param name="Mp">The mana of the mate.</param>
/// <param name="Name">The name of the mate.</param>
/// <param name="IsSummonable">Whether the mate is summonable.</param>
/// <param name="CanPickUp">Whether the pet can pick up items.</param>
public record Pet
(
    long MateId,
    long NpcVNum,
    long TransportId,
    Level Level,
    short Loyalty,
    MateAttackStats Attack,
    MateArmorStats Armor,
    Element Element,
    Resistance Resistance,
    Health Hp,
    Health Mp,
    string Name,
    bool IsSummonable,
    bool CanPickUp
) : Mate
(
    MateId,
    NpcVNum,
    TransportId,
    Level,
    Loyalty,
    Attack,
    Armor,
    Element,
    Resistance,
    Hp,
    Mp,
    Name,
    IsSummonable
);
\ No newline at end of file

M Core/NosSmooth.Game/Data/Social/Group.cs => Core/NosSmooth.Game/Data/Social/Group.cs +0 -1
@@ 16,7 16,6 @@ namespace NosSmooth.Game.Data.Social;
/// 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, IReadOnlyList<GroupMember>? Members)
{

M Core/NosSmooth.Game/Data/Social/GroupMember.cs => Core/NosSmooth.Game/Data/Social/GroupMember.cs +5 -1
@@ 9,6 9,10 @@ using NosSmooth.Packets.Enums.Players;

namespace NosSmooth.Game.Data.Social;

/// <summary>
/// A member of a group the character is in.
/// </summary>
/// <param name="PlayerId">The id of the group member player.</param>
public record GroupMember(long PlayerId)
{
    /// <summary>


@@ 54,5 58,5 @@ public record GroupMember(long PlayerId)
    /// <summary>
    /// Gets the effects of the member.
    /// </summary>
    public IReadOnlyList<long>? EffectsVNums { get; internal set; }
    public IReadOnlyList<short>? EffectsVNums { get; internal set; }
}
\ No newline at end of file

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

/// <summary>
/// Stats about mate armor.
/// </summary>
/// <param name="DefenceUpgrade">The upgrade of defence.</param>
/// <param name="MeleeDefence">The melee defence.</param>
/// <param name="MeleeDefenceDodge">The melee dodge rate.</param>
/// <param name="RangedDefence">The ranged defence.</param>
/// <param name="RangedDodgeRate">The ranged dodge rate.</param>
/// <param name="MagicalDefence">The magical defence.</param>
public record MateArmorStats
(
    short DefenceUpgrade,
    int MeleeDefence,
    int MeleeDefenceDodge,
    int RangedDefence,
    int RangedDodgeRate,
    int MagicalDefence
);
\ No newline at end of file

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

/// <summary>
/// Stats about mate attack.
/// </summary>
/// <param name="AttackUpgrade">The upgrade of attack.</param>
/// <param name="MinimumAttack">The minimum attack.</param>
/// <param name="MaximumAttack">The maximum attack.</param>
/// <param name="Precision">The precision or concentration.</param>
/// <param name="CriticalChance">The critical chance.</param>
/// <param name="CriticalRate">The critical rate.</param>
public record MateAttackStats
(
    short AttackUpgrade,
    int MinimumAttack,
    int MaximumAttack,
    int Precision,
    int CriticalChance,
    int CriticalRate
);
\ No newline at end of file

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

/// <summary>
/// Stats about resistance of character or mate.
/// </summary>
/// <param name="FireResistance">The fire resistance percentage.</param>
/// <param name="WaterResistance">The water resistance percentage.</param>
/// <param name="LightResistance">The light resistance percentage.</param>
/// <param name="DarkResistance">The dark resistance percentage.</param>
public record Resistance
(
    short FireResistance,
    short WaterResistance,
    short LightResistance,
    short DarkResistance
);
\ No newline at end of file

M Core/NosSmooth.Game/Events/Groups/GroupInitializedEvent.cs => Core/NosSmooth.Game/Events/Groups/GroupInitializedEvent.cs +7 -0
@@ 8,4 8,11 @@ using NosSmooth.Game.Data.Social;

namespace NosSmooth.Game.Events.Groups;

/// <summary>
/// A group has been initialized.
/// </summary>
/// <remarks>
/// May be sent multiple times even for the same group.
/// </remarks>
/// <param name="Group">The initialized group with members.</param>
public record GroupInitializedEvent(Group Group) : IGameEvent;
\ No newline at end of file

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

namespace NosSmooth.Game.Events.Groups;

/// <summary>
/// A new stats (hp, mp) of a group received.
/// </summary>
/// <param name="Member">The updated group member.</param>
public record GroupMemberStatEvent(GroupMember Member) : IGameEvent;
\ No newline at end of file

A Core/NosSmooth.Game/Events/Mates/MateStatEvent.cs => Core/NosSmooth.Game/Events/Mates/MateStatEvent.cs +19 -0
@@ 0,0 1,19 @@
//
//  MateStatEvent.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.Mates;
using NosSmooth.Game.Data.Social;

namespace NosSmooth.Game.Events.Mates;

/// <summary>
/// A new stats (hp, mp) of a mate received.
/// </summary>
/// <param name="Mate">The mate.</param>
/// <param name="Hp">The current hp of the mate.</param>
/// <param name="Mp">The current mp of the mate.</param>
public record MateStatEvent(Mate Mate, Health Hp, Health Mp) : IGameEvent;
\ No newline at end of file

A Core/NosSmooth.Game/Events/Mates/MatesPartyInitializedEvent.cs => Core/NosSmooth.Game/Events/Mates/MatesPartyInitializedEvent.cs +17 -0
@@ 0,0 1,17 @@
//
//  MatesPartyInitializedEvent.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.Mates;
using NosSmooth.Game.Data.Social;

namespace NosSmooth.Game.Events.Mates;

/// <summary>
/// A party was initialized and got information about currently present party pets.
/// </summary>
/// <param name="Pet">The party pet.</param>
/// <param name="Partner">The party partner.</param>
public record MatesPartyInitializedEvent(Pet? Pet, Partner? Partner) : IGameEvent;
\ No newline at end of file

R Core/NosSmooth.Game/Data/Entities/Partner.cs => Core/NosSmooth.Game/Events/Mates/PartnerInitializedEvent.cs +8 -4
@@ 1,12 1,16 @@
//
//  Partner.cs
//  PartnerInitializedEvent.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;
using NosSmooth.Game.Data.Mates;
using NosSmooth.Game.Data.Social;

namespace NosSmooth.Game.Events.Mates;

/// <summary>
/// Represents Partner of the Character.
/// A partner the character owns was initialized.
/// </summary>
public record Partner() : IPet;
\ No newline at end of file
/// <param name="Partner">The partner.</param>
public record PartnerInitializedEvent(Partner Partner) : IGameEvent;
\ No newline at end of file

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

namespace NosSmooth.Game.Events.Mates;

/// <summary>
/// A partner the character owns skills were received.
/// </summary>
/// <param name="Partner">The partner.</param>
/// <param name="Skills">The skills of the partner.</param>
public record PartnerSkillsReceivedEvent(Partner? Partner, IReadOnlyList<Skill> Skills) : IGameEvent;
\ No newline at end of file

R Core/NosSmooth.Game/Data/Entities/Pet.cs => Core/NosSmooth.Game/Events/Mates/PetInitializedEvent.cs +8 -4
@@ 1,12 1,16 @@
//
//  Pet.cs
//  PetInitializedEvent.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;
using NosSmooth.Game.Data.Mates;
using NosSmooth.Game.Data.Social;

namespace NosSmooth.Game.Events.Mates;

/// <summary>
/// Represents pet of the character.
/// A pet the character owns was initialized.
/// </summary>
public record Pet() : IPet;
\ No newline at end of file
/// <param name="Pet">The pet.</param>
public record PetInitializedEvent(Pet Pet) : IGameEvent;
\ No newline at end of file

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

namespace NosSmooth.Game.Events.Mates;

/// <summary>
/// A pet the character owns skill was received.
/// </summary>
/// <param name="Pet">The pet.</param>
/// <param name="PetSkill">The skill of the pet.</param>
public record PetSkillReceivedEvent(Pet? Pet, Skill? PetSkill) : IGameEvent;
\ No newline at end of file

R Core/NosSmooth.Game/Data/Entities/IPet.cs => Core/NosSmooth.Game/Extensions/LivingEntityExtensions.cs +15 -4
@@ 1,14 1,25 @@
//
//  IPet.cs
//  LivingEntityExtensions.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;
using NosSmooth.Game.Data.Entities;

namespace NosSmooth.Game.Extensions;

/// <summary>
/// Represents base type for a pet or a partner.
/// An extension methods for <see cref="ILivingEntity"/>.
/// </summary>
public interface IPet
public static class LivingEntityExtensions
{
    /// <summary>
    /// Checks whether the entity is alive.
    /// </summary>
    /// <param name="entity">The entity to check.</param>
    /// <returns>Whether the entity is alive.</returns>
    public static bool IsAlive(ILivingEntity entity)
    {
        return entity.Hp is null || entity.Hp.Amount != 0 || entity.Hp.Percentage != 0;
    }
}
\ No newline at end of file

M Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs => Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs +3 -0
@@ 15,6 15,7 @@ using NosSmooth.Game.PacketHandlers.Entities;
using NosSmooth.Game.PacketHandlers.Inventory;
using NosSmooth.Game.PacketHandlers.Map;
using NosSmooth.Game.PacketHandlers.Relations;
using NosSmooth.Game.PacketHandlers.Skills;
using NosSmooth.Game.PacketHandlers.Specialists;

namespace NosSmooth.Game.Extensions;


@@ 40,11 41,13 @@ public static class ServiceCollectionExtensions
        serviceCollection
            .AddPacketResponder<CharacterInitResponder>()
            .AddPacketResponder<PlayerSkillResponder>()
            .AddPacketResponder<MatesSkillResponder>()
            .AddPacketResponder<WalkResponder>()
            .AddPacketResponder<SkillUsedResponder>()
            .AddPacketResponder<FriendInitResponder>()
            .AddPacketResponder<InventoryInitResponder>()
            .AddPacketResponder<GroupInitResponder>()
            .AddPacketResponder<MatesInitResponder>()
            .AddPacketResponder<AoeSkillUsedResponder>()
            .AddPacketResponder<AtResponder>()
            .AddPacketResponder<CMapResponder>()

M Core/NosSmooth.Game/Game.cs => Core/NosSmooth.Game/Game.cs +35 -0
@@ 6,10 6,12 @@

using Microsoft.Extensions.Options;
using NosSmooth.Core.Stateful;
using NosSmooth.Game.Data;
using NosSmooth.Game.Data.Characters;
using NosSmooth.Game.Data.Chat;
using NosSmooth.Game.Data.Inventory;
using NosSmooth.Game.Data.Maps;
using NosSmooth.Game.Data.Mates;
using NosSmooth.Game.Data.Raids;
using NosSmooth.Game.Data.Social;



@@ 44,6 46,11 @@ public class Game : IStatefulEntity
    public Character? Character { get; internal set; }

    /// <summary>
    /// Gets the mates of the current character.
    /// </summary>
    public Mates? Mates { get; internal set; }

    /// <summary>
    /// Gets or sets the inventory of the character.
    /// </summary>
    public Inventory? Inventory { get; internal set; }


@@ 89,6 96,34 @@ public class Game : IStatefulEntity
    public Raid? CurrentRaid { get; internal set; }

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

    /// <summary>
    /// Creates the skills if they are null, or updates the current skills.
    /// </summary>
    /// <param name="create">The function for creating the skills.</param>

M Core/NosSmooth.Game/GameSemaphoreType.cs => Core/NosSmooth.Game/GameSemaphoreType.cs +6 -1
@@ 49,5 49,10 @@ public enum GameSemaphoreType
    /// <summary>
    /// The semaphore for raid.
    /// </summary>
    Raid
    Raid,

    /// <summary>
    /// The semaphore for mates.
    /// </summary>
    Mates
}
\ No newline at end of file

M Core/NosSmooth.Game/PacketHandlers/Relations/FriendInitResponder.cs => Core/NosSmooth.Game/PacketHandlers/Relations/FriendInitResponder.cs +4 -3
@@ 40,10 40,11 @@ public class FriendInitResponder : IPacketResponder<FInfoPacket>, IPacketRespond
                .Select(
                    x =>
                    {
                        if (x.PlayerId == packet.PlayerId)
                        var subPacket = packet.FriendSubPackets.FirstOrDefault(y => x.PlayerId == y.PlayerId);
                        if (subPacket is not null)
                        {
                            x.IsConnected = packet.IsConnected;
                            x.CharacterName = packet.Name;
                            x.IsConnected = subPacket.IsConnected;
                            x.CharacterName = subPacket.Name;
                        }

                        return x;

A Core/NosSmooth.Game/PacketHandlers/Relations/MatesInitResponder.cs => Core/NosSmooth.Game/PacketHandlers/Relations/MatesInitResponder.cs +367 -0
@@ 0,0 1,367 @@
//
//  MatesInitResponder.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.Info;
using NosSmooth.Game.Data.Items;
using NosSmooth.Game.Data.Mates;
using NosSmooth.Game.Data.Social;
using NosSmooth.Game.Data.Stats;
using NosSmooth.Game.Events.Core;
using NosSmooth.Game.Events.Mates;
using NosSmooth.Packets.Enums.Entities;
using NosSmooth.Packets.Enums.Mates;
using NosSmooth.Packets.Server.Groups;
using NosSmooth.Packets.Server.Mates;
using NosSmooth.Packets.Server.Miniland;
using Remora.Results;

namespace NosSmooth.Game.PacketHandlers.Relations;

/// <summary>
/// A mates initialization responder.
/// </summary>
public class MatesInitResponder : IPacketResponder<ScPPacket>, IPacketResponder<ScNPacket>,
    IPacketResponder<PinitPacket>, IPacketResponder<PstPacket>, IPacketResponder<PClearPacket>
{
    private readonly Game _game;
    private readonly IInfoService _infoService;
    private readonly EventDispatcher _eventDispatcher;
    private readonly ILogger<MatesInitResponder> _logger;

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

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<ScPPacket> packetArgs, CancellationToken ct = default)
    {
        var packet = packetArgs.Packet;
        var pet = new Pet
        (
            packet.PetId,
            packet.NpcVNum,
            packet.TransportId,
            new Level(packet.Level, packet.Experience, packet.LevelExperience),
            packet.Loyalty,
            new MateAttackStats
            (
                packet.AttackUpgrade,
                packet.MinimumAttack,
                packet.MaximumAttack,
                packet.Concentrate,
                packet.CriticalChance,
                packet.CriticalRate
            ),
            new MateArmorStats
            (
                packet.DefenceUpgrade,
                packet.MeleeDefence,
                packet.MeleeDefenceDodge,
                packet.RangeDefence,
                packet.RangeDodgeRate,
                packet.MagicalDefence
            ),
            packet.Element,
            new Resistance
            (
                packet.ResistanceSubPacket.FireResistance,
                packet.ResistanceSubPacket.WaterResistance,
                packet.ResistanceSubPacket.LightResistance,
                packet.ResistanceSubPacket.DarkResistance
            ),
            new Health { Amount = packet.Hp, Maximum = packet.HpMax },
            new Health { Amount = packet.Mp, Maximum = packet.MpMax },
            packet.Name,
            packet.IsSummonable,
            packet.CanPickUp
        );

        await _game.CreateOrUpdateMatesAsync
        (
            () =>
            {
                var mates = new Mates();
                mates.SetPet(pet);
                return mates;
            },
            mates =>
            {
                mates.SetPet(pet);
                return mates;
            },
            ct: ct
        );

        return await _eventDispatcher.DispatchEvent
        (
            new PetInitializedEvent(pet),
            ct
        );
    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<ScNPacket> packetArgs, CancellationToken ct = default)
    {
        var packet = packetArgs.Packet;
        var partner = new Partner
        (
            packet.PartnerId,
            packet.NpcVNum,
            packet.TransportId,
            new Level(packet.Level, packet.Experience, packet.LevelExperience),
            packet.Loyalty,
            new MateAttackStats
            (
                packet.AttackUpgrade,
                packet.MinimumAttack,
                packet.MaximumAttack,
                packet.Precision,
                packet.CriticalChance,
                packet.CriticalRate
            ),
            new MateArmorStats
            (
                packet.DefenceUpgrade,
                packet.MeleeDefence,
                packet.MeleeDefenceDodge,
                packet.RangeDefence,
                packet.RangeDodgeRate,
                packet.MagicalDefence
            ),
            new PartnerEquipment
            (
                await CreatePartnerItem(packet.WeaponSubPacket, ct),
                await CreatePartnerItem(packet.ArmorSubPacket, ct),
                await CreatePartnerItem(packet.GauntletSubPacket, ct),
                await CreatePartnerItem(packet.BootsSubPacket, ct)
            ),
            packet.Element,
            new Resistance
            (
                packet.ResistanceSubPacket.FireResistance,
                packet.ResistanceSubPacket.WaterResistance,
                packet.ResistanceSubPacket.LightResistance,
                packet.ResistanceSubPacket.DarkResistance
            ),
            new Health { Amount = packet.Hp, Maximum = packet.HpMax },
            new Health { Amount = packet.Mp, Maximum = packet.MpMax },
            packet.Name,
            packet.MorphVNum,
            packet.IsSummonable,
#pragma warning disable SA1118
            packet.SpSubPacket.Value is not null
                ? new PartnerSp
                (
                    packet.SpSubPacket.Value.ItemVNum,
                    packet.SpSubPacket.Value.AgilityPercentage,
                    await CreateSkill(packet.Skill1SubPacket, ct),
                    await CreateSkill(packet.Skill2SubPacket, ct),
                    await CreateSkill(packet.Skill3SubPacket, ct)
                )
                : null
#pragma warning restore SA1118
        );

        await _game.CreateOrUpdateMatesAsync
        (
            () =>
            {
                var mates = new Mates();
                mates.SetPartner(partner);
                return mates;
            },
            mates =>
            {
                mates.SetPartner(partner);
                return mates;
            },
            ct: ct
        );

        return await _eventDispatcher.DispatchEvent
        (
            new PartnerInitializedEvent(partner),
            ct
        );
    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<PinitPacket> packetArgs, CancellationToken ct = default)
    {
        var packet = packetArgs.Packet;
        var partner = packet.PinitSubPackets?.FirstOrDefault(x => x.MateSubPacket?.MateType == MateType.Partner);
        var pet = packet.PinitSubPackets?.FirstOrDefault(x => x.MateSubPacket?.MateType == MateType.Pet);

        Partner? gamePartner = null;
        Pet? gamePet = null;

        await _game.CreateOrUpdateMatesAsync
        (
            () => null,
            m =>
            {
                if (partner is not null)
                {
                    gamePartner = _game.Mates?.Partners.FirstOrDefault(x => x.MateId == partner.EntityId);
                    if (gamePartner is not null && gamePartner != m.CurrentPartner?.Partner)
                    {
                        m.CurrentPartner = new PartyPartner(gamePartner);
                    }
                }

                if (pet is not null)
                {
                    gamePet = _game.Mates?.Pets.FirstOrDefault(x => x.MateId == pet.EntityId);
                    if (gamePet is not null && gamePet != m.CurrentPet?.Pet)
                    {
                        m.CurrentPet = new PartyPet(gamePet);
                    }
                }

                return m;
            },
            ct: ct
        );

        return await _eventDispatcher.DispatchEvent
        (
            new MatesPartyInitializedEvent(gamePet, gamePartner),
            ct
        );
    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<PstPacket> packetArgs, CancellationToken ct = default)
    {
        var packet = packetArgs.Packet;
        if (packet.EntityType != EntityType.Npc || packet.MateType is null)
        {
            return Result.FromSuccess();
        }

        Mate? mate = null;
        var hp = new Health { Amount = packet.Hp, Percentage = packet.HpPercentage };
        var mp = new Health { Amount = packet.Mp, Percentage = packet.MpPercentage };

        await _game.CreateOrUpdateMatesAsync
        (
            () => null,
            m =>
            {
                if (packet.MateType is MateType.Pet && m.CurrentPet is not null)
                {
                    mate = m.CurrentPet.Pet;
                    m.CurrentPet.Hp = hp;
                    m.CurrentPet.Mp = mp;
                }
                else if (packet.MateType is MateType.Partner && m.CurrentPartner is not null)
                {
                    mate = m.CurrentPartner.Partner;
                    m.CurrentPartner.Hp = hp;
                    m.CurrentPartner.Mp = mp;
                }

                return m;
            },
            ct: ct
        );

        if (mate is not null)
        {
            return await _eventDispatcher.DispatchEvent(new MateStatEvent(mate, hp, mp), ct);
        }

        return Result.FromSuccess();
    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<PClearPacket> packetArgs, CancellationToken ct = default)
    {
        await _game.CreateOrUpdateMatesAsync
        (
            () => null,
            m =>
            {
                m.Clear();
                return m;
            },
            ct: ct
        );

        return Result.FromSuccess();
    }

    private async Task<UpgradeableItem?> CreatePartnerItem(ScNEquipmentSubPacket? packet, CancellationToken ct)
    {
        if (packet is null)
        {
            return null;
        }

        var itemInfoResult = await _infoService.GetItemInfoAsync(packet.ItemVNum, ct);
        if (!itemInfoResult.IsDefined(out var itemInfo))
        {
            _logger.LogWarning
            (
                "Could not obtain an item info for vnum {vnum}: {error}",
                packet.ItemVNum,
                itemInfoResult.ToFullString()
            );
        }

        return new UpgradeableItem
        (
            packet.ItemVNum,
            itemInfo,
            packet.ItemUpgrade,
            packet.ItemRare,
            0
        );
    }

    private async Task<PartnerSkill?> CreateSkill(ScNSkillSubPacket? packet, CancellationToken ct)
    {
        if (packet is null)
        {
            return null;
        }

        var skillInfoResult = await _infoService.GetSkillInfoAsync(packet.SkillVNum, ct);
        if (!skillInfoResult.IsDefined(out var skillInfo))
        {
            _logger.LogWarning
            (
                "Could not obtain a skill info for vnum {vnum}: {error}",
                packet.SkillVNum,
                skillInfoResult.ToFullString()
            );
        }

        return new PartnerSkill(packet.SkillVNum, packet.Rank, skillInfo);
    }
}
\ No newline at end of file

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

namespace NosSmooth.Game.PacketHandlers.Skills;

/// <summary>
/// Responds to petski and pski packets.
/// </summary>
public class MatesSkillResponder : IPacketResponder<PetskiPacket>, IPacketResponder<PSkiPacket>
{
    private readonly Game _game;
    private readonly EventDispatcher _eventDispatcher;
    private readonly IInfoService _infoService;
    private readonly ILogger<MatesSkillResponder> _logger;

    /// <summary>
    /// Initializes a new instance of the <see cref="MatesSkillResponder"/> 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 MatesSkillResponder
    (
        Game game,
        EventDispatcher eventDispatcher,
        IInfoService infoService,
        ILogger<MatesSkillResponder> logger
    )
    {
        _game = game;
        _eventDispatcher = eventDispatcher;
        _infoService = infoService;
        _logger = logger;
    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<PetskiPacket> packetArgs, CancellationToken ct = default)
    {
        var packet = packetArgs.Packet;
        Skill? skill = null;
        if (packet.SkillVNum is not null)
        {
            var skillInfoResult = await _infoService.GetSkillInfoAsync(packet.SkillVNum.Value, ct);
            if (!skillInfoResult.IsDefined(out var skillInfo))
            {
                _logger.LogWarning
                (
                    "Could not obtain skill info for vnum {vnum}: {error}",
                    packet.SkillVNum.Value,
                    skillInfoResult.ToFullString()
                );
            }

            skill = new Skill(packet.SkillVNum.Value, null, skillInfo);
        }

        var mates = await _game.CreateOrUpdateMatesAsync
        (
            () => new Mates
            {
                PetSkill = skill
            },
            m =>
            {
                m.PetSkill = skill;
                return m;
            },
            ct: ct
        );

        return await _eventDispatcher.DispatchEvent
        (
            new PetSkillReceivedEvent(mates?.CurrentPet?.Pet, skill),
            ct
        );
    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<PSkiPacket> packetArgs, CancellationToken ct = default)
    {
        var packet = packetArgs.Packet;
        var skills = new List<Skill>();

        foreach (var skillVNum in packet.SkillVNums)
        {
            if (skillVNum is null)
            {
                continue;
            }

            var skillInfoResult = await _infoService.GetSkillInfoAsync(skillVNum.Value, ct);
            if (!skillInfoResult.IsDefined(out var skillInfo))
            {
                _logger.LogWarning
                (
                    "Could not obtain skill info for vnum {vnum}: {error}",
                    skillVNum,
                    skillInfoResult.ToFullString()
                );
            }

            skills.Add(new Skill(skillVNum.Value, null, skillInfo));
        }

        if (skills.Count == 0)
        {
            skills = null;
        }

        var mates = await _game.CreateOrUpdateMatesAsync
        (
            () => new Mates
            {
                PartnerSkills = skills
            },
            m =>
            {
                m.PartnerSkills = skills;
                return m;
            },
            ct: ct
        );

        return await _eventDispatcher.DispatchEvent
        (
            new PartnerSkillsReceivedEvent(mates?.CurrentPartner?.Partner, skills ?? Array.Empty<Skill>().ToList()),
            ct
        );
    }
}
\ No newline at end of file

M Core/NosSmooth.Game/PacketHandlers/Skills/PlayerSkillResponder.cs => Core/NosSmooth.Game/PacketHandlers/Skills/PlayerSkillResponder.cs +2 -2
@@ 14,7 14,7 @@ using NosSmooth.Game.Events.Core;
using NosSmooth.Packets.Server.Skills;
using Remora.Results;

namespace NosSmooth.Game.PacketHandlers.Characters;
namespace NosSmooth.Game.PacketHandlers.Skills;

/// <summary>
/// Responds to SkiPacket to add skill to the character.


@@ 94,7 94,7 @@ public class PlayerSkillResponder : IPacketResponder<SkiPacket>
            otherSkillsFromCharacter.Add(await CreateSkill(newSkill, default));
        }

        skills = new Skills(primarySkill, secondarySkill, otherSkillsFromCharacter);
        skills = new Data.Characters.Skills(primarySkill, secondarySkill, otherSkillsFromCharacter);

        await _game.CreateOrUpdateSkillsAsync
        (

A Packets/NosSmooth.PacketSerializer.Abstractions/NullableWrapper.cs => Packets/NosSmooth.PacketSerializer.Abstractions/NullableWrapper.cs +31 -0
@@ 0,0 1,31 @@
//
//  NullableWrapper.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.PacketSerializer.Abstractions;

/// <summary>
/// Wraps a compound value that may not be present
/// and there will be "-1" instead in the packet.
/// The converter of underlying type will be called
/// if and only if the value is not null.
/// </summary>
/// <param name="Value">The value.</param>
/// <typeparam name="T">The underlying type.</typeparam>
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", Justification = "Fix this, this should not happen.")]
public record NullableWrapper<T>(T? Value)
{
    /// <summary>
    /// Unwrap the underlying value.
    /// </summary>
    /// <param name="wrapper">The wrapper to unwrap.</param>
    /// <returns>The unwrapped value.</returns>
    public static implicit operator T?(NullableWrapper<T> wrapper)
    {
        return wrapper.Value;
    }
}
\ No newline at end of file

A Packets/NosSmooth.PacketSerializer/Converters/Common/NullableWrapperConverter.cs => Packets/NosSmooth.PacketSerializer/Converters/Common/NullableWrapperConverter.cs +79 -0
@@ 0,0 1,79 @@
//
//  NullableWrapperConverter.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;
using NosSmooth.PacketSerializer.Abstractions;
using Remora.Results;

namespace NosSmooth.PacketSerializer.Converters.Common;

/// <summary>
/// Converter of <see cref="NullableWrapper{T}"/>.
/// </summary>
/// <typeparam name="T">The underlying type.</typeparam>
public class NullableWrapperConverter<T> : BaseStringConverter<NullableWrapper<T>>
{
    private readonly IStringConverterRepository _converterRepository;

    /// <summary>
    /// Initializes a new instance of the <see cref="NullableWrapperConverter{T}"/> class.
    /// </summary>
    /// <param name="converterRepository">The converter repository.</param>
    public NullableWrapperConverter(IStringConverterRepository converterRepository)
    {
        _converterRepository = converterRepository;
    }

    /// <inheritdoc />
    public override Result Serialize(NullableWrapper<T>? obj, PacketStringBuilder builder)
    {
        if (obj is null || obj.Value is null)
        {
            builder.Append("-1");
        }
        else
        {
            var converterResult = _converterRepository.GetTypeConverter<T>();
            if (!converterResult.IsDefined(out var converter))
            {
                return Result.FromError(converterResult);
            }

            return converter.Serialize(obj.Value, builder);
        }

        return Result.FromSuccess();
    }

    /// <inheritdoc />
    public override Result<NullableWrapper<T>?> Deserialize(ref PacketStringEnumerator stringEnumerator)
    {
        var tokenResult = stringEnumerator.GetNextToken(out var packetToken, false);
        if (!tokenResult.IsSuccess)
        {
            return Result<NullableWrapper<T>?>.FromError(tokenResult);
        }

        if (packetToken.Token.Length == 2 && packetToken.Token.StartsWith("-1"))
        {
            return Result<NullableWrapper<T>?>.FromSuccess(new NullableWrapper<T>(default));
        }

        var converterResult = _converterRepository.GetTypeConverter<T>();
        if (!converterResult.IsDefined(out var converter))
        {
            return Result<NullableWrapper<T>?>.FromError(converterResult);
        }

        var deserializationResult = converter.Deserialize(ref stringEnumerator);
        if (!deserializationResult.IsDefined(out var deserialization))
        {
            return Result<NullableWrapper<T>?>.FromError(deserializationResult);
        }

        return new NullableWrapper<T>(deserialization);
    }
}
\ No newline at end of file

A Packets/NosSmooth.PacketSerializer/Converters/Common/NullableWrapperConverterFactory.cs => Packets/NosSmooth.PacketSerializer/Converters/Common/NullableWrapperConverterFactory.cs +57 -0
@@ 0,0 1,57 @@
//
//  NullableWrapperConverterFactory.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;
using Microsoft.Extensions.DependencyInjection;
using NosSmooth.PacketSerializer.Abstractions;
using NosSmooth.PacketSerializer.Converters.Special;
using NosSmooth.PacketSerializer.Converters.Special.Converters;
using NosSmooth.PacketSerializer.Extensions;
using Remora.Results;

namespace NosSmooth.PacketSerializer.Converters.Common;

/// <summary>
/// Converts <see cref="NullableWrapper{T}"/>.
/// </summary>
public class NullableWrapperConverterFactory : IStringConverterFactory
{
    private readonly IServiceProvider _serviceProvider;

    /// <summary>
    /// Initializes a new instance of the <see cref="NullableWrapperConverterFactory"/> class.
    /// </summary>
    /// <param name="serviceProvider">The service provider.</param>
    public NullableWrapperConverterFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    /// <inheritdoc />
    public bool ShouldHandle(Type type)
        => type.GetGenericTypeDefinition() == typeof(NullableWrapper<>);

    /// <inheritdoc />
    public Result<IStringConverter> CreateTypeSerializer(Type type)
    {
        var underlyingType = type.GetGenericArguments()[0];
        var serializerType = typeof(NullableWrapperConverter<>).MakeGenericType(underlyingType);

        try
        {
            return Result<IStringConverter>.FromSuccess
                ((IStringConverter)ActivatorUtilities.CreateInstance(_serviceProvider, serializerType));
        }
        catch (Exception e)
        {
            return e;
        }
    }

    /// <inheritdoc />
    public Result<IStringConverter<T>> CreateTypeSerializer<T>()
        => CreateTypeSerializer(typeof(T)).Cast<IStringConverter<T>, IStringConverter>();
}
\ No newline at end of file

M Packets/NosSmooth.PacketSerializer/Extensions/ServiceCollectionExtensions.cs => Packets/NosSmooth.PacketSerializer/Extensions/ServiceCollectionExtensions.cs +1 -0
@@ 77,6 77,7 @@ public static class ServiceCollectionExtensions
            .AddStringConverterFactory<ListStringConverterFactory>()
            .AddStringConverterFactory<NullableStringConverterFactory>()
            .AddStringConverterFactory<EnumStringConverterFactory>()
            .AddStringConverterFactory<NullableWrapperConverterFactory>()
            .AddStringConverter<IntStringConverter>()
            .AddStringConverter<BoolStringConverter>()
            .AddStringConverter<UIntStringConverter>()

M Packets/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/BasicInlineConverterGenerator.cs => Packets/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/BasicInlineConverterGenerator.cs +1 -1
@@ 51,7 51,7 @@ public class BasicInlineConverterGenerator : IInlineConverterGenerator
    {
        var type = typeSyntax is not null
            ? typeSyntax.ToString().TrimEnd('?')
            : typeSymbol?.ToString();
            : typeSymbol?.ToString().TrimEnd('?');
        if (type is null)
        {
            throw new Exception("TypeSyntax or TypeSymbol has to be non null.");

M Packets/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/ListInlineConverterGenerator.cs => Packets/NosSmooth.PacketSerializersGenerator/InlineConverterGenerators/ListInlineConverterGenerator.cs +1 -1
@@ 96,7 96,7 @@ public class ListInlineConverterGenerator : IInlineConverterGenerator
    private string GetMethodName(ITypeSymbol genericArgumentType)
    {
        return
            $"ParseList{genericArgumentType.ToString().Replace('.', '_')}{((genericArgumentType.IsNullable() ?? false) ? "Nullable" : string.Empty)}";
            $"ParseList{genericArgumentType.ToString().TrimEnd('?').Replace('.', '_').Replace('<', '_').Replace('>', '_')}{((genericArgumentType.IsNullable() ?? false) ? "Nullable" : string.Empty)}";
    }

    /// <inheritdoc />

M Packets/NosSmooth.PacketSerializersGenerator/SourceGenerator.cs => Packets/NosSmooth.PacketSerializersGenerator/SourceGenerator.cs +6 -0
@@ 130,6 130,12 @@ public class SourceGenerator : ISourceGenerator
                    $"{packetRecord.GetPrefix()}.{packetRecord.Identifier.NormalizeWhitespace().ToFullString()}Converter.g.cs",
                    stringWriter.GetStringBuilder().ToString()
                );

                File.WriteAllText
                (
                    Path.Combine(Path.GetTempPath(), $"{packetRecord.GetPrefix()}.{packetRecord.Identifier.NormalizeWhitespace().ToFullString()}Converter.g.cs"),
                    stringWriter.GetStringBuilder().ToString()
                );
            }
        }


A Packets/NosSmooth.Packets/Client/Battle/UsePetSkillPacket.cs => Packets/NosSmooth.Packets/Client/Battle/UsePetSkillPacket.cs +37 -0
@@ 0,0 1,37 @@
//
//  UsePetSkillPacket.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.Entities;
using NosSmooth.PacketSerializer.Abstractions.Attributes;

namespace NosSmooth.Packets.Client.Battle;

/// <summary>
/// Sent to use a pet skill.
/// </summary>
/// <param name="MateTransportId">The pet skill id.</param>
/// <param name="TargetEntityType">The target entity type.</param>
/// <param name="TargetId">The target id.</param>
/// <param name="Unknown">Unknown, seems to always be 1.</param>
/// <param name="Unknown1">Unknown, 6 for Otter.</param>
/// <param name="Unknown2">Unknown, 9 for Otter.</param>
[PacketHeader("u_pet", PacketSource.Client)]
[GenerateSerializer(true)]
public record UsePetSkillPacket
(
    [PacketIndex(0)]
    long MateTransportId,
    [PacketIndex(1)]
    EntityType TargetEntityType,
    [PacketIndex(2)]
    long TargetId,
    [PacketIndex(3)]
    byte Unknown,
    [PacketIndex(4)]
    byte Unknown1,
    [PacketIndex(5)]
    byte Unknown2
) : IPacket;
\ No newline at end of file

M Packets/NosSmooth.Packets/Server/Entities/EffectsSubPacket.cs => Packets/NosSmooth.Packets/Server/Entities/EffectsSubPacket.cs +1 -1
@@ 18,7 18,7 @@ namespace NosSmooth.Packets.Server.Entities;
public record EffectsSubPacket
(
    [PacketIndex(0)]
    long CardId,
    short CardId,
    [PacketIndex(1, IsOptional = true)]
    short? Level
) : IPacket;
\ No newline at end of file

M Packets/NosSmooth.Packets/Server/Entities/RevivePacket.cs => Packets/NosSmooth.Packets/Server/Entities/RevivePacket.cs +7 -0
@@ 9,6 9,13 @@ using NosSmooth.PacketSerializer.Abstractions.Attributes;

namespace NosSmooth.Packets.Server.Entities;

/// <summary>
/// A packet specifying a revival
/// of an entity, usually a player.
/// </summary>
/// <param name="EntityType">The type of the revived entity.</param>
/// <param name="EntityId">The id of the revived entity.</param>
/// <param name="TimeSpaceLives">Unknown function, seems like representing lives in a timespace.</param>
[PacketHeader("revive", PacketSource.Server)]
[GenerateSerializer(true)]
public record RevivePacket

M Packets/NosSmooth.Packets/Server/Groups/PinitMateSubPacket.cs => Packets/NosSmooth.Packets/Server/Groups/PinitMateSubPacket.cs +11 -0
@@ 11,6 11,17 @@ using NosSmooth.PacketSerializer.Abstractions.Common;

namespace NosSmooth.Packets.Server.Groups;

/// <summary>
/// A sub packet of <see cref="PinitPacket"/>
/// representing a mate.
/// </summary>
/// <param name="MateType">The type of the mate.</param>
/// <param name="Level">The level of the mate.</param>
/// <param name="Name">The name of the mate.</param>
/// <param name="Unknown">Unknown TODO.</param>
/// <param name="VNum">The VNum of the mate entity.</param>
[PacketHeader(null, PacketSource.Server)]
[GenerateSerializer(true)]
public record PinitMateSubPacket
(
    [PacketIndex(0)]

M Packets/NosSmooth.Packets/Server/Groups/PinitPlayerSubPacket.cs => Packets/NosSmooth.Packets/Server/Groups/PinitPlayerSubPacket.cs +12 -0
@@ 12,6 12,18 @@ using NosSmooth.PacketSerializer.Abstractions.Common;

namespace NosSmooth.Packets.Server.Groups;

/// <summary>
/// A sub packet of <see cref="PinitPacket"/>
/// representing a player.
/// </summary>
/// <param name="GroupPosition">The position in the group.</param>
/// <param name="Level">The level of the player.</param>
/// <param name="Name">The name of the player.</param>
/// <param name="GroupId">The group id of the group character is in.</param>
/// <param name="Sex">The sex of the player.</param>
/// <param name="Class">The class of the player.</param>
/// <param name="MorphVNum">The morph of the player</param>
/// <param name="HeroLevel">The hero level of the player.</param>
[PacketHeader(null, PacketSource.Server)]
[GenerateSerializer(true)]
public record PinitPlayerSubPacket

M Packets/NosSmooth.Packets/Server/Groups/PstPacket.cs => Packets/NosSmooth.Packets/Server/Groups/PstPacket.cs +3 -1
@@ 16,7 16,9 @@ namespace NosSmooth.Packets.Server.Groups;
/// Party status packet.
/// </summary>
/// <param name="EntityType">The type of the entity.</param>
/// <param name="GroupPosition">The position in the group of the entity</param>
/// <param name="EntityId">The id of the entity.</param>
/// <param name="GroupPosition">The position in the group of a player. Present only for players.</param>
/// <param name="MateType">The type of a mate. Present only for mates.</param>
/// <param name="HpPercentage">The hp percentage of the entity.</param>
/// <param name="MpPercentage">The mp percentage of the entity.</param>
/// <param name="Hp">The hp of the entity.</param>

M Packets/NosSmooth.Packets/Server/Login/CListPacket.cs => Packets/NosSmooth.Packets/Server/Login/CListPacket.cs +2 -1
@@ 6,6 6,7 @@

using NosSmooth.Packets.Enums.Players;
using NosSmooth.Packets.Server.Maps;
using NosSmooth.PacketSerializer.Abstractions;
using NosSmooth.PacketSerializer.Abstractions.Attributes;
using NosSmooth.PacketSerializer.Abstractions.Common;



@@ 73,7 74,7 @@ public record CListPacket
    [PacketIndex(14)]
    byte Unknown3,
    [PacketListIndex(15, ListSeparator = '.', InnerSeparator = '.')]
    IReadOnlyList<CListPetSubPacket> PetsSubPacket,
    IReadOnlyList<NullableWrapper<CListPetSubPacket>> PetsSubPacket,
    [PacketIndex(16)]
    byte HatDesign,
    [PacketIndex(17)]

M Packets/NosSmooth.Packets/Server/Mates/ScNEquipmentSubPacket.cs => Packets/NosSmooth.Packets/Server/Mates/ScNEquipmentSubPacket.cs +5 -5
@@ 21,9 21,9 @@ namespace NosSmooth.Packets.Server.Mates;
public record ScNEquipmentSubPacket
(
    [PacketIndex(0)]
    long? ItemVNum,
    [PacketIndex(1, IsOptional = true)]
    long? ItemRare,
    [PacketIndex(2, IsOptional = true)]
    long? ItemUpgrade
    int ItemVNum,
    [PacketIndex(1)]
    sbyte? ItemRare,
    [PacketIndex(2)]
    byte ItemUpgrade
) : IPacket;
\ No newline at end of file

M Packets/NosSmooth.Packets/Server/Mates/ScNPacket.cs => Packets/NosSmooth.Packets/Server/Mates/ScNPacket.cs +10 -9
@@ 7,6 7,7 @@
using NosSmooth.Packets.Enums;
using NosSmooth.Packets.Server.Character;
using NosSmooth.Packets.Server.Maps;
using NosSmooth.PacketSerializer.Abstractions;
using NosSmooth.PacketSerializer.Abstractions.Attributes;
using NosSmooth.PacketSerializer.Abstractions.Common;



@@ 23,7 24,7 @@ namespace NosSmooth.Packets.Server.Mates;
/// <param name="Loyalty">The loyalty of the partner.</param>
/// <param name="Experience">The experience of the partner.</param>
/// <param name="WeaponSubPacket">Information about partner's weapon.</param>
/// <param name="ArmodSubPacket">Information about partner's armor.</param>
/// <param name="ArmorSubPacket">Information about partner's armor.</param>
/// <param name="GauntletSubPacket">Information about partner's gauntlet.</param>
/// <param name="BootsSubPacket">Information about partner's boots.</param>
/// <param name="Unknown1">Unknown TODO.</param>


@@ 73,13 74,13 @@ public record ScNPacket
    [PacketIndex(5)]
    long Experience,
    [PacketIndex(6, InnerSeparator = '.')]
    ScNEquipmentSubPacket? WeaponSubPacket,
    NullableWrapper<ScNEquipmentSubPacket> WeaponSubPacket,
    [PacketIndex(7, InnerSeparator = '.')]
    ScNEquipmentSubPacket? ArmodSubPacket,
    NullableWrapper<ScNEquipmentSubPacket> ArmorSubPacket,
    [PacketIndex(8, InnerSeparator = '.')]
    ScNEquipmentSubPacket? GauntletSubPacket,
    NullableWrapper<ScNEquipmentSubPacket> GauntletSubPacket,
    [PacketIndex(9, InnerSeparator = '.')]
    ScNEquipmentSubPacket? BootsSubPacket,
    NullableWrapper<ScNEquipmentSubPacket> BootsSubPacket,
    [PacketIndex(10, InnerSeparator = '.')]
    short Unknown1,
    [PacketIndex(11)]


@@ 133,11 134,11 @@ public record ScNPacket
    [PacketIndex(35)]
    bool IsSummonable,
    [PacketIndex(36, InnerSeparator = '.')]
    ScNSpSubPacket? SpSubPacket,
    NullableWrapper<ScNSpSubPacket> SpSubPacket,
    [PacketIndex(37, InnerSeparator = '.')]
    ScNSkillSubPacket? Skill1SubPacket,
    NullableWrapper<ScNSkillSubPacket> Skill1SubPacket,
    [PacketIndex(38, InnerSeparator = '.')]
    ScNSkillSubPacket? Skill2SubPacket,
    NullableWrapper<ScNSkillSubPacket> Skill2SubPacket,
    [PacketIndex(39, InnerSeparator = '.')]
    ScNSkillSubPacket? Skill3SubPacket
    NullableWrapper<ScNSkillSubPacket> Skill3SubPacket
) : IPacket;
\ No newline at end of file

M Packets/NosSmooth.Packets/Server/Mates/ScNSkillSubPacket.cs => Packets/NosSmooth.Packets/Server/Mates/ScNSkillSubPacket.cs +3 -3
@@ 21,7 21,7 @@ namespace NosSmooth.Packets.Server.Mates;
public record ScNSkillSubPacket
(
    [PacketIndex(0)]
    long? SkillVNum,
    [PacketIndex(1, IsOptional = true)]
    PartnerSkillRank? Rank
    int SkillVNum,
    [PacketIndex(1)]
    PartnerSkillRank Rank
) : IPacket;
\ No newline at end of file

M Packets/NosSmooth.Packets/Server/Mates/ScNSpSubPacket.cs => Packets/NosSmooth.Packets/Server/Mates/ScNSpSubPacket.cs +3 -3
@@ 20,7 20,7 @@ namespace NosSmooth.Packets.Server.Mates;
public record ScNSpSubPacket
(
    [PacketIndex(0)]
    long? ItemVNum,
    [PacketIndex(1, IsOptional = true)]
    byte? AgilityPercentage
    long ItemVNum,
    [PacketIndex(1)]
    byte AgilityPercentage
);
\ No newline at end of file

M Packets/NosSmooth.Packets/Server/Relations/FInfoPacket.cs => Packets/NosSmooth.Packets/Server/Relations/FInfoPacket.cs +4 -9
@@ 12,16 12,11 @@ namespace NosSmooth.Packets.Server.Relations;
/// <summary>
/// Information update of friend of a character.
/// </summary>
/// <param name="PlayerId">The id of the friend.</param>
/// <param name="IsConnected">Whether the friend is connected.</param>
/// <param name="Name">The name of the friend.</param>[PacketHeader("finfo", PacketSource.Server)]
/// <param name="FriendSubPackets"></param>
[PacketHeader("finfo", PacketSource.Server)]
[GenerateSerializer(true)]
public record FInfoPacket
(
    [PacketIndex(0)]
    long PlayerId,
    [PacketIndex(1)]
    bool IsConnected,
    [PacketIndex(2)]
    NameString Name
    [PacketListIndex(0, InnerSeparator = '.', ListSeparator = ' ')]
    IReadOnlyList<FInfoSubPacket> FriendSubPackets
) : IPacket;
\ No newline at end of file

A Packets/NosSmooth.Packets/Server/Relations/FInfoSubPacket.cs => Packets/NosSmooth.Packets/Server/Relations/FInfoSubPacket.cs +29 -0
@@ 0,0 1,29 @@
//
//  FInfoSubPacket.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.Relations;

/// <summary>
/// A sub packet of <see cref="FInfoPacket"/>
/// containing information about a friend.
/// </summary>
/// <param name="PlayerId">The id of the player.</param>
/// <param name="IsConnected">Whether the player is connected.</param>
/// <param name="Name">The name of the player.</param>
[PacketHeader(null, PacketSource.Server)]
[GenerateSerializer(true)]
public record FInfoSubPacket
(
    [PacketIndex(0)]
    long PlayerId,
    [PacketIndex(1)]
    bool IsConnected,
    [PacketIndex(2)]
    NameString Name
);
\ No newline at end of file

A Samples/FileClient/Responders/InventoryInitializedResponder.cs => Samples/FileClient/Responders/InventoryInitializedResponder.cs +52 -0
@@ 0,0 1,52 @@
//
//  InventoryInitializedResponder.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.Enums;
using NosSmooth.Data.Abstractions.Language;
using NosSmooth.Game.Data.Items;
using NosSmooth.Game.Events.Core;
using NosSmooth.Game.Events.Inventory;
using NosSmooth.Game.Extensions;
using Remora.Results;

namespace FileClient.Responders;

/// <inheritdoc />
public class InventoryInitializedResponder : IGameResponder<InventoryInitializedEvent>
{
    private readonly ILanguageService _languageService;

    /// <summary>
    /// Initializes a new instance of the <see cref="InventoryInitializedResponder"/> class.
    /// </summary>
    /// <param name="languageService">The langauge service.</param>
    public InventoryInitializedResponder(ILanguageService languageService)
    {
        _languageService = languageService;

    }

    /// <inheritdoc />
    public async Task<Result> Respond(InventoryInitializedEvent gameEvent, CancellationToken ct = default)
    {
        foreach (var bag in gameEvent.Inventory)
        {
            foreach (var slot in bag)
            {
                var item = slot.Item;
                if (item?.Info is not null && bag.Type != item.Info.BagType)
                {
                    var translatedResult = await _languageService.GetTranslationAsync(item.Info.Name, Language.Cz, ct);
                    var entity = translatedResult.Entity;

                    Console.WriteLine(entity + $", {item.ItemVNum} is: {bag.Type}, should be: {item.Info.BagType.Convert()}");
                }
            }
        }

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