~ruther/NosSmooth

2d5bddcd58c1ef739e8669e9f268442f13009bcd — Rutherther 2 years ago 550dc63 + e51525b
Merge pull request #53 from Rutherther/feat/raids

Add raid processing
33 files changed, 1447 insertions(+), 14 deletions(-)

M Core/NosSmooth.Game/Data/Raids/Raid.cs
A Core/NosSmooth.Game/Data/Raids/RaidProgress.cs
A Core/NosSmooth.Game/Data/Raids/RaidState.cs
M Core/NosSmooth.Game/Data/Social/GroupMember.cs
A Core/NosSmooth.Game/Events/Raids/RaidFinishedEvent.cs
A Core/NosSmooth.Game/Events/Raids/RaidJoinedEvent.cs
A Core/NosSmooth.Game/Events/Raids/RaidStateChangedEvent.cs
M Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs
M Core/NosSmooth.Game/Game.cs
A Core/NosSmooth.Game/PacketHandlers/Raids/RaidBfResponder.cs
A Core/NosSmooth.Game/PacketHandlers/Raids/RaidMbfResponder.cs
A Core/NosSmooth.Game/PacketHandlers/Raids/RaidResponder.cs
A Core/NosSmooth.Game/PacketHandlers/Raids/RbossResponder.cs
A Core/NosSmooth.Game/PacketHandlers/Raids/RdlstResponder.cs
A Packets/NosSmooth.PacketSerializer.Abstractions/Attributes/PacketConditionalListIndexAttribute.cs
M Packets/NosSmooth.PacketSerializer/Converters/Common/NullableWrapperConverterFactory.cs
M Packets/NosSmooth.PacketSerializer/Converters/Common/OptionalWrapperConverterFactory.cs
A Packets/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketConditionalListIndexAttributeGenerator.cs
M Packets/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketListIndexAttributeGenerator.cs
M Packets/NosSmooth.PacketSerializersGenerator/SourceGenerator.cs
A Packets/NosSmooth.Packets/Enums/Raids/RaidBfPacketType.cs
A Packets/NosSmooth.Packets/Enums/Raids/RaidLeaveType.cs
A Packets/NosSmooth.Packets/Enums/Raids/RaidPacketType.cs
A Packets/NosSmooth.Packets/Enums/Raids/RaidType.cs
M Packets/NosSmooth.Packets/Server/Groups/PstPacket.cs
A Packets/NosSmooth.Packets/Server/Raids/RaidBfPacket.cs
A Packets/NosSmooth.Packets/Server/Raids/RaidMbfPacket.cs
A Packets/NosSmooth.Packets/Server/Raids/RaidPacket.cs
A Packets/NosSmooth.Packets/Server/Raids/RaidPlayerHealthsSubPacket.cs
A Packets/NosSmooth.Packets/Server/Raids/RbossPacket.cs
A Packets/NosSmooth.Packets/Server/Raids/RdlstPacket.cs
A Packets/NosSmooth.Packets/Server/Raids/RdlstSubPacket.cs
A Tests/NosSmooth.Packets.Tests/Converters/Packets/RaidPacketConverterTests.cs
M Core/NosSmooth.Game/Data/Raids/Raid.cs => Core/NosSmooth.Game/Data/Raids/Raid.cs +16 -1
@@ 4,9 4,24 @@
//  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.Social;
using NosSmooth.Packets.Enums.Raids;

namespace NosSmooth.Game.Data.Raids;

/// <summary>
/// Represents nostale raid.
/// </summary>
public record Raid();
\ No newline at end of file
public record Raid
(
    RaidType Type,
    RaidState State,
    short MinimumLevel,
    short MaximumLevel,
    GroupMember? Leader,
    RaidProgress? Progress,
    Monster? Boss,
    IReadOnlyList<Monster>? Bosses,
    IReadOnlyList<GroupMember>? Members
);
\ No newline at end of file

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

public record RaidProgress
(
    short MonsterLockerInitial,
    short MonsterLockerCurrent,
    short ButtonLockerInitial,
    short ButtonLockerCurrent,
    short CurrentLives,
    short InitialLives
);
\ No newline at end of file

A Core/NosSmooth.Game/Data/Raids/RaidState.cs => Core/NosSmooth.Game/Data/Raids/RaidState.cs +52 -0
@@ 0,0 1,52 @@
//
//  RaidState.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>
/// A state of a <see cref="Raid"/>.
/// </summary>
public enum RaidState
{
    /// <summary>
    /// Waiting for a raid start.
    /// </summary>
    Waiting,

    /// <summary>
    /// The raid has started, the current room is not boss room.
    /// </summary>
    Started,

    /// <summary>
    /// The raid has started and the current room is boss room.
    /// </summary>
    BossFight,

    /// <summary>
    /// The raid has ended, successfully.
    /// </summary>
    EndedSuccessfully,

    /// <summary>
    /// The raid has ended unsuccessfully. The whole team has failed.
    /// </summary>
    TeamFailed,

    /// <summary>
    /// The raid has ended unsuccessfully for the character. He ran out of lifes.
    /// </summary>
    MemberFailed,

    /// <summary>
    /// The character has left the raid.
    /// </summary>
    /// <remarks>
    /// The previous state is needed to be able to tell whether
    /// the raid was already started or was in the <see cref="Waiting"/> state.
    /// </remarks>
    Left
}
\ No newline at end of file

M Core/NosSmooth.Game/Data/Social/GroupMember.cs => Core/NosSmooth.Game/Data/Social/GroupMember.cs +1 -1
@@ 43,7 43,7 @@ public record GroupMember(long PlayerId)
    /// <summary>
    /// Gets the morph vnum of the player.
    /// </summary>
    public long MorphVNum { get; internal set; }
    public int? MorphVNum { get; internal set; }

    /// <summary>
    /// Gets the hp of the member.

A Core/NosSmooth.Game/Events/Raids/RaidFinishedEvent.cs => Core/NosSmooth.Game/Events/Raids/RaidFinishedEvent.cs +11 -0
@@ 0,0 1,11 @@
//
//  RaidFinishedEvent.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.Raids;

namespace NosSmooth.Game.Events.Raids;

public record RaidFinishedEvent(Raid Raid) : IGameEvent;
\ No newline at end of file

A Core/NosSmooth.Game/Events/Raids/RaidJoinedEvent.cs => Core/NosSmooth.Game/Events/Raids/RaidJoinedEvent.cs +11 -0
@@ 0,0 1,11 @@
//
//  RaidJoinedEvent.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.Raids;

namespace NosSmooth.Game.Events.Raids;

public record RaidJoinedEvent(Raid Raid) : IGameEvent;
\ No newline at end of file

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

namespace NosSmooth.Game.Events.Raids;

public record RaidStateChangedEvent
(
    RaidState PreviousState,
    RaidState CurrentState,
    Raid Raid
) : IGameEvent;
\ No newline at end of file

M Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs => Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs +40 -6
@@ 18,9 18,11 @@ using NosSmooth.Game.PacketHandlers.Characters;
using NosSmooth.Game.PacketHandlers.Entities;
using NosSmooth.Game.PacketHandlers.Inventory;
using NosSmooth.Game.PacketHandlers.Map;
using NosSmooth.Game.PacketHandlers.Raids;
using NosSmooth.Game.PacketHandlers.Relations;
using NosSmooth.Game.PacketHandlers.Skills;
using NosSmooth.Game.PacketHandlers.Specialists;
using NosSmooth.Packets.Server.Raids;

namespace NosSmooth.Game.Extensions;



@@ 43,17 45,35 @@ public static class ServiceCollectionExtensions
        serviceCollection.TryAddSingleton<Game>();

        serviceCollection

            // act4
            .AddPacketResponder<FcResponder>()

            // character
            .AddPacketResponder<CharacterInitResponder>()
            .AddPacketResponder<WalkResponder>()

            // skills
            .AddPacketResponder<PlayerSkillResponder>()
            .AddPacketResponder<MatesSkillResponder>()
            .AddPacketResponder<WalkResponder>()
            .AddPacketResponder<SkillUsedResponder>()

            // friends
            .AddPacketResponder<FriendInitResponder>()

            // inventory
            .AddPacketResponder<InventoryInitResponder>()

            // groups
            .AddPacketResponder<GroupInitResponder>()

            // mates
            .AddPacketResponder<MatesInitResponder>()

            // skills
            .AddPacketResponder<AoeSkillUsedResponder>()

            // map
            .AddPacketResponder<AtResponder>()
            .AddPacketResponder<CMapResponder>()
            .AddPacketResponder<DropResponder>()


@@ 61,11 81,22 @@ public static class ServiceCollectionExtensions
            .AddPacketResponder<InResponder>()
            .AddPacketResponder<MoveResponder>()
            .AddPacketResponder<OutResponder>()

            // hp, mp
            .AddPacketResponder<StatPacketResponder>()
            .AddPacketResponder<StPacketResponder>()
            .AddPacketResponder<CondPacketResponder>()

            // equip
            .AddPacketResponder<SpResponder>()
            .AddPacketResponder<EqResponder>();
            .AddPacketResponder<EqResponder>()

            // raids
            .AddPacketResponder<RaidBfResponder>()
            .AddPacketResponder<RaidMbfResponder>()
            .AddPacketResponder<RaidResponder>()
            .AddPacketResponder<RbossResponder>()
            .AddPacketResponder<RdlstResponder>();

        serviceCollection
            .AddTransient<DialogHandler>()


@@ 102,13 133,16 @@ public static class ServiceCollectionExtensions
    /// <returns>The collection.</returns>
    public static IServiceCollection AddGameResponder(this IServiceCollection serviceCollection, Type gameResponder)
    {
        if (!gameResponder.GetInterfaces().Any(
        if (!gameResponder.GetInterfaces().Any
            (
                i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IGameResponder<>)
            ))
        {
            throw new ArgumentException(
            throw new ArgumentException
            (
                $"{nameof(gameResponder)} should implement IGameResponder.",
                nameof(gameResponder));
                nameof(gameResponder)
            );
        }

        var handlerTypeInterfaces = gameResponder.GetInterfaces();


@@ 124,4 158,4 @@ public static class ServiceCollectionExtensions

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

M Core/NosSmooth.Game/Game.cs => Core/NosSmooth.Game/Game.cs +54 -0
@@ 359,6 359,60 @@ public class Game : IStatefulEntity
        );
    }

    /// <summary>
    /// Updates the current raid, if it is not null.
    /// </summary>
    /// <param name="update">The function for updating the raid.</param>
    /// <param name="releaseSemaphore">Whether to release the semaphore used for changing the raid.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>The updated raid.</returns>
    internal Task<Raid?> UpdateRaidAsync
    (
        Func<Raid, Raid?> update,
        bool releaseSemaphore = true,
        CancellationToken ct = default
    )
    {
        return CreateOrUpdateAsync
        (
            GameSemaphoreType.Raid,
            () => CurrentRaid,
            m => CurrentRaid = m,
            () => null,
            update,
            releaseSemaphore,
            ct
        );
    }

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

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

A Core/NosSmooth.Game/PacketHandlers/Raids/RaidBfResponder.cs => Core/NosSmooth.Game/PacketHandlers/Raids/RaidBfResponder.cs +69 -0
@@ 0,0 1,69 @@
//
//  RaidBfResponder.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.Data;
using NosSmooth.Core.Packets;
using NosSmooth.Game.Data.Raids;
using NosSmooth.Game.Events.Core;
using NosSmooth.Game.Events.Raids;
using NosSmooth.Packets.Enums.Raids;
using NosSmooth.Packets.Server.Raids;
using Remora.Results;

namespace NosSmooth.Game.PacketHandlers.Raids;

/// <summary>
/// A responder to <see cref="RaidBfPacket"/>.
/// </summary>
public class RaidBfResponder : IPacketResponder<RaidBfPacket>
{
    private readonly Game _game;
    private readonly EventDispatcher _eventDispatcher;

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

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<RaidBfPacket> packetArgs, CancellationToken ct = default)
    {
        var packet = packetArgs.Packet;
        Raid? previousRaid = null;
        var currentRaid = await _game.UpdateRaidAsync
        (
            raid =>
            {
                previousRaid = raid;
                return raid with
                {
                    State = packet.WindowType switch
                    {
                        RaidBfPacketType.MissionStarted => RaidState.Started,
                        RaidBfPacketType.MissionCleared => RaidState.EndedSuccessfully,
                        _ => RaidState
                            .TeamFailed // TODO: figure out whether OutOfLives is sent for both individual member and whole team
                    }
                };
            },
            ct: ct
        );

        if (previousRaid is not null && currentRaid is not null && previousRaid.State != currentRaid.State)
        {
            return await _eventDispatcher.DispatchEvent
                (new RaidStateChangedEvent(previousRaid.State, currentRaid.State, currentRaid), ct);
        }

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

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

namespace NosSmooth.Game.PacketHandlers.Raids;

/// <summary>
/// A responder to <see cref="RaidMbfPacket"/>.
/// </summary>
public class RaidMbfResponder : IPacketResponder<RaidMbfPacket>
{
    private readonly Game _game;

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

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

        await _game.UpdateRaidAsync
        (
            raid => raid with
            {
                Progress = new RaidProgress
                (
                    packet.MonsterLockerInitial,
                    packet.MonsterLockerCurrent,
                    packet.ButtonLockerInitial,
                    packet.ButtonLockerCurrent,
                    packet.CurrentLives,
                    packet.InitialLives
                )
            },
            ct: ct
        );
        return Result.FromSuccess();
    }
}
\ No newline at end of file

A Core/NosSmooth.Game/PacketHandlers/Raids/RaidResponder.cs => Core/NosSmooth.Game/PacketHandlers/Raids/RaidResponder.cs +114 -0
@@ 0,0 1,114 @@
//
//  RaidResponder.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.Net.Http.Headers;
using NosSmooth.Core.Packets;
using NosSmooth.Game.Data.Info;
using NosSmooth.Game.Data.Raids;
using NosSmooth.Game.Data.Social;
using NosSmooth.Game.Events.Core;
using NosSmooth.Game.Events.Raids;
using NosSmooth.Packets.Enums.Raids;
using NosSmooth.Packets.Server.Raids;
using Remora.Results;

namespace NosSmooth.Game.PacketHandlers.Raids;

/// <summary>
/// A responder to <see cref="RaidPacket"/>.
/// </summary>
public class RaidResponder : IPacketResponder<RaidPacket>
{
    private readonly Game _game;
    private readonly EventDispatcher _eventDispatcher;

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

    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<RaidPacket> packetArgs, CancellationToken ct = default)
    {
        var packet = packetArgs.Packet;
        if (packet.Type is not(RaidPacketType.Leader or RaidPacketType.ListMembers or RaidPacketType.PlayerHealths or RaidPacketType.Leave))
        {
            return Result.FromSuccess();
        }

        Raid? prevRaid = null;
        var currentRaid = await _game.UpdateRaidAsync
        (
            raid =>
            {
                prevRaid = raid;
                switch (packet.Type)
                {
                    case RaidPacketType.Leave:
                        if (packet.LeaveType is not null && packet.LeaveType == RaidLeaveType.PlayerLeft)
                        { // the player has left.
                            prevRaid = raid with
                            {
                                State = RaidState.Left
                            };

                            return null;
                        }

                        return raid;
                    case RaidPacketType.Leader:
                        if (packet.LeaderId is null)
                        { // set the raid to null.
                            return null;
                        }

                        return raid with
                        {
                            Leader = raid.Members?.FirstOrDefault(x => x.PlayerId == packet.LeaderId.Value)
                        };
                    case RaidPacketType.ListMembers:
                        return raid with
                        {
                            Members = raid.Members?.Where(x => packet.ListMembersPlayerIds?.Contains(x.PlayerId) ?? true).ToList()
                        };
                    case RaidPacketType.PlayerHealths:
                        // update healths
                        foreach (var member in raid.Members ?? (IReadOnlyList<GroupMember>)Array.Empty<GroupMember>())
                        {
                            var data = packet.PlayerHealths?.FirstOrDefault(x => x.PlayerId == member.PlayerId);

                            if (data is not null)
                            {
                                member.Hp ??= new Health();
                                member.Mp ??= new Health();

                                member.Hp.Percentage = data.HpPercentage;
                                member.Mp.Percentage = data.MpPercentage;
                            }
                        }
                        return raid;
                }

                return raid;
            },
            ct: ct
        );

        if (currentRaid is null && prevRaid is not null)
        {
            return await _eventDispatcher.DispatchEvent(new RaidFinishedEvent(prevRaid), ct);
        }

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

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

namespace NosSmooth.Game.PacketHandlers.Raids;

/// <summary>
/// A responder to <see cref="RbossPacket"/>.
/// </summary>
public class RbossResponder : IPacketResponder<RbossPacket>
{
    private readonly Game _game;
    private readonly EventDispatcher _eventDispatcher;

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

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

        var bossEntity = packet.EntityId is not null ? map.Entities.GetEntity<Monster>(packet.EntityId.Value) : null;

        RaidState? previousState = null;
        var currentRaid = await _game.UpdateRaidAsync
        (
            raid =>
            {
                previousState = raid.State;
                if (bossEntity is not null && (raid.Bosses is null || !raid.Bosses.Contains(bossEntity)))
                {
                    return raid with
                    {
                        Boss = bossEntity,
                        Bosses = (raid.Bosses ?? Array.Empty<Monster>()).Append(bossEntity).ToList(),
                        State = RaidState.BossFight
                    };
                }

                return raid with
                { // this will oscillate between more bosses ...
                    Boss = bossEntity
                };
            },
            ct: ct
        );

        if (currentRaid is not null && previousState is not null && previousState != currentRaid.State)
        {
            return await _eventDispatcher.DispatchEvent
                (new RaidStateChangedEvent(previousState.Value, currentRaid.State, currentRaid), ct);
        }

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

A Core/NosSmooth.Game/PacketHandlers/Raids/RdlstResponder.cs => Core/NosSmooth.Game/PacketHandlers/Raids/RdlstResponder.cs +98 -0
@@ 0,0 1,98 @@
//
//  RdlstResponder.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.Raids;
using NosSmooth.Game.Data.Social;
using NosSmooth.Game.Events.Core;
using NosSmooth.Game.Events.Raids;
using NosSmooth.Packets.Server.Raids;
using Remora.Results;

namespace NosSmooth.Game.PacketHandlers.Raids;

/// <summary>
/// A responder to <see cref="RdlstPacket"/>.
/// </summary>
public class RdlstResponder : IPacketResponder<RdlstPacket>
{
    private readonly Game _game;
    private readonly EventDispatcher _eventDispatcher;

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

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

        IReadOnlyList<GroupMember> UpdateMembers(IReadOnlyList<GroupMember>? currentMembers)
        {
            return packet.Players
                .Select
                (
                    packetMember =>
                    {
                        var newMember = currentMembers?.FirstOrDefault
                            (member => packetMember.Id == member.PlayerId) ?? new GroupMember(packetMember.Id);

                        newMember.Class = packetMember.Class;
                        newMember.Level = packetMember.Level;
                        newMember.HeroLevel = packetMember.HeroLevel;
                        newMember.Sex = packetMember.Sex;
                        newMember.MorphVNum = packetMember.MorphVNum;

                        return newMember;
                    }
                ).ToArray();
        }

        Raid? prevRaid = null;
        var currentRaid = await _game.CreateOrUpdateRaidAsync
        (
            () => new Raid
            (
                packet.RaidType,
                RaidState.Waiting,
                packet.MinimumLevel,
                packet.MaximumLevel,
                null,
                null,
                null,
                null,
                UpdateMembers(null)
            ),
            raid =>
            {
                prevRaid = raid;
                return raid with
                {
                    Type = packet.RaidType,
                    MinimumLevel = packet.MinimumLevel,
                    MaximumLevel = packet.MaximumLevel,
                    Members = UpdateMembers(raid.Members),
                };
            },
            ct: ct
        );

        if (prevRaid is null && currentRaid is not null)
        {
            return await _eventDispatcher.DispatchEvent(new RaidJoinedEvent(currentRaid), ct);
        }

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

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

/// <summary>
/// <see cref="PacketConditionalIndexAttribute"/> + <see cref="PacketListIndexAttribute"/>
/// in one.
/// </summary>
public class PacketConditionalListIndexAttribute : PacketListIndexAttribute
{
    /// <summary>
    /// Initializes a new instance of the <see cref="PacketConditionalListIndexAttribute"/> class.
    /// You can use this attribute multiple times on one parameter.
    /// </summary>
    /// <param name="index">The position in the packet.</param>
    /// <param name="conditionParameter">What parameter to check. (it has to precede this one).</param>
    /// <param name="negate">Whether to negate the match values (not equals).</param>
    /// <param name="matchValues">The values that mean this parameter is present.</param>
    public PacketConditionalListIndexAttribute(ushort index, string conditionParameter, bool negate = false, params object?[] matchValues)
        : base(index)
    {
    }
}
\ No newline at end of file

M Packets/NosSmooth.PacketSerializer/Converters/Common/NullableWrapperConverterFactory.cs => Packets/NosSmooth.PacketSerializer/Converters/Common/NullableWrapperConverterFactory.cs +1 -1
@@ 32,7 32,7 @@ public class NullableWrapperConverterFactory : IStringConverterFactory

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

    /// <inheritdoc />
    public Result<IStringConverter> CreateTypeSerializer(Type type)

M Packets/NosSmooth.PacketSerializer/Converters/Common/OptionalWrapperConverterFactory.cs => Packets/NosSmooth.PacketSerializer/Converters/Common/OptionalWrapperConverterFactory.cs +1 -1
@@ 31,7 31,7 @@ public class OptionalWrapperConverterFactory : IStringConverterFactory

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

    /// <inheritdoc />
    public Result<IStringConverter> CreateTypeSerializer(Type type)

A Packets/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketConditionalListIndexAttributeGenerator.cs => Packets/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketConditionalListIndexAttributeGenerator.cs +242 -0
@@ 0,0 1,242 @@
//
//  PacketConditionalListIndexAttributeGenerator.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.CodeDom.Compiler;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using NosSmooth.PacketSerializersGenerator.Data;
using NosSmooth.PacketSerializersGenerator.Errors;
using NosSmooth.PacketSerializersGenerator.Extensions;

namespace NosSmooth.PacketSerializersGenerator.AttributeGenerators;

/// <inheritdoc />
public class PacketConditionalListIndexAttributeGenerator : IParameterGenerator
{
    private readonly PacketListIndexAttributeGenerator _listIndexGenerator;
    private readonly InlineTypeConverterGenerator _inlineTypeConverterGenerators;

    /// <summary>
    /// Initializes a new instance of the <see cref="PacketConditionalListIndexAttributeGenerator"/> class.
    /// </summary>
    /// <param name="inlineTypeConverterGenerators">The generator for types.</param>
    public PacketConditionalListIndexAttributeGenerator(InlineTypeConverterGenerator inlineTypeConverterGenerators)
    {
        _inlineTypeConverterGenerators = inlineTypeConverterGenerators;
        _listIndexGenerator = new PacketListIndexAttributeGenerator(inlineTypeConverterGenerators)
        {
            PacketListIndexAttributeFullName = PacketConditionalListIndexAttributeFullName
        };
    }

    /// <summary>
    /// Gets the full name of the packet index attribute.
    /// </summary>
    public static string PacketConditionalListIndexAttributeFullName
        => "NosSmooth.PacketSerializer.Abstractions.Attributes.PacketConditionalListIndexAttribute";

    /// <inheritdoc />
    public bool ShouldHandle(ParameterInfo parameter)
        => parameter.Attributes.Any(x => x.FullName == PacketConditionalListIndexAttributeFullName);

    /// <inheritdoc />
    public IError? CheckParameter(PacketInfo packet, ParameterInfo parameter)
    {
        if (!parameter.Nullable)
        {
            return new DiagnosticError
            (
                "SGNull",
                "Conditional parameters must be nullable",
                "The parameter {0} in {1} has to be nullable, because it is conditional.",
                parameter.Parameter.SyntaxTree,
                parameter.Parameter.FullSpan,
                new List<object?>(new[] { parameter.Name, packet.Name })
            );
        }

        if (parameter.Attributes.Any(x => x.FullName != PacketConditionalListIndexAttributeFullName))
        {
            return new DiagnosticError
            (
                "SGAttr",
                "Packet constructor parameter with multiple packet attributes",
                "Found multiple packet attributes of multiple types on parameter {0} in {1}. PacketConditionalIndexAttribute supports multiple attributes of the same type only.",
                parameter.Parameter.SyntaxTree,
                parameter.Parameter.FullSpan,
                new List<object?>
                (
                    new[]
                    {
                        parameter.Name,
                        packet.Name
                    }
                )
            );
        }

        // Check that all attributes have the same data. (where the same data need to be)
        var firstAttribute = parameter.Attributes.First();
        if (parameter.Attributes.Any
            (
                x =>
                {
                    var index = x.GetIndexedValue<int>(0);
                    if (index != parameter.PacketIndex)
                    {
                        return true;
                    }

                    foreach (var keyValue in x.NamedAttributeArguments)
                    {
                        if (!firstAttribute.NamedAttributeArguments.ContainsKey(keyValue.Key))
                        {
                            return true;
                        }

                        if (firstAttribute.NamedAttributeArguments[keyValue.Key] != keyValue.Value)
                        {
                            return true;
                        }
                    }

                    return false;
                }
            ))
        {
            return new DiagnosticError
            (
                "SGAttr",
                "Packet constructor parameter with multiple conflicting attribute data.",
                "Found multiple packet attributes of multiple types on parameter {0} in {1} with conflicting data. Index, IsOptional, InnerSeparator, ListSeparator, AfterSeparator all have to be the same for each attribute.",
                parameter.Parameter.SyntaxTree,
                parameter.Parameter.FullSpan,
                new List<object?>
                (
                    new[]
                    {
                        parameter.Name,
                        packet.Name
                    }
                )
            );
        }

        var mismatchedAttribute = parameter.Attributes.FirstOrDefault
        (
            x => x.IndexedAttributeArguments.Count < 4 ||
                (x.IndexedAttributeArguments[3].IsArray &&
                    (
                        (x.IndexedAttributeArguments[3].Argument.Expression as ArrayCreationExpressionSyntax)
                        ?.Initializer is null ||
                        (x.IndexedAttributeArguments[3].Argument.Expression as ArrayCreationExpressionSyntax)
                        ?.Initializer?.Expressions.Count == 0
                    )
                )
        );
        if (mismatchedAttribute is not null)
        {
            return new DiagnosticError
            (
                "SGAttr",
                "Packet conditional attribute without matching values",
                "Found PacketConditionalIndexAttribute without matchValues parameters set on {0} in {1}. At least one parameter has to be specified.",
                mismatchedAttribute.Attribute.SyntaxTree,
                mismatchedAttribute.Attribute.FullSpan,
                new List<object?>
                (
                    new[]
                    {
                        parameter.Name,
                        packet.Name
                    }
                )
            );
        }

        return ParameterChecker.CheckOptionalIsNullable(packet, parameter);
    }

    private string BuildAttributeIfPart(AttributeInfo attribute, string prefix)
    {
        var conditionParameterName = attribute.GetIndexedValue<string>(1);
        var negate = attribute.GetIndexedValue<bool>(2);
        var values = attribute.GetParamsVisualValues(3);
        if (conditionParameterName is null || values is null)
        {
            throw new ArgumentException();
        }

        var inner = string.Join
            (" || ", values.Select(x => $"{prefix}{conditionParameterName.Trim('"')} == {x?.ToString() ?? "null"}"));
        return (negate ? "!(" : string.Empty) + inner + (negate ? ")" : string.Empty);
    }

    private string BuildParameterIfStatement(ParameterInfo parameter, string prefix)
    {
        var ifInside = string.Empty;
        foreach (var attribute in parameter.Attributes)
        {
            ifInside += BuildAttributeIfPart(attribute, prefix);
        }

        return $"if ({ifInside})";
    }

    /// <inheritdoc />
    public IError? GenerateSerializerPart(IndentedTextWriter textWriter, PacketInfo packetInfo)
    {
        var parameter = packetInfo.Parameters.Current;

        // begin conditional if
        textWriter.WriteLine(BuildParameterIfStatement(parameter, "obj."));
        textWriter.WriteLine("{");
        textWriter.Indent++;

        var error = _listIndexGenerator.GenerateSerializerPart(textWriter, packetInfo);
        if (error is not null)
        {
            return error;
        }

        // end conditional if
        textWriter.Indent--;
        textWriter.WriteLine("}");

        return null;
    }

    /// <inheritdoc />
    public IError? GenerateDeserializerPart(IndentedTextWriter textWriter, PacketInfo packetInfo)
    {
        var generator = new ConverterDeserializationGenerator(textWriter);
        var parameter = packetInfo.Parameters.Current;

        generator.DeclareLocalVariable(parameter);

        // begin conditional if
        textWriter.WriteLine(BuildParameterIfStatement(parameter, string.Empty));
        textWriter.WriteLine("{");
        textWriter.Indent++;

        var error = _listIndexGenerator.GenerateDeserializerPart(textWriter, packetInfo, false);
        if (error is not null)
        {
            return error;
        }

        // end conditional if
        textWriter.Indent--;
        textWriter.WriteLine("}");
        textWriter.WriteLine("else");
        textWriter.WriteLine("{");
        textWriter.Indent++;
        textWriter.WriteLine($"{parameter.GetVariableName()} = null;");
        textWriter.Indent--;
        textWriter.WriteLine("}");

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

M Packets/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketListIndexAttributeGenerator.cs => Packets/NosSmooth.PacketSerializersGenerator/AttributeGenerators/PacketListIndexAttributeGenerator.cs +16 -3
@@ 33,8 33,8 @@ public class PacketListIndexAttributeGenerator : IParameterGenerator
    /// <summary>
    /// Gets the full name of the packet index attribute.
    /// </summary>
    public static string PacketListIndexAttributeFullName
        => "NosSmooth.PacketSerializer.Abstractions.Attributes.PacketListIndexAttribute";
    public string PacketListIndexAttributeFullName { get; set; }
        = "NosSmooth.PacketSerializer.Abstractions.Attributes.PacketListIndexAttribute";

    /// <inheritdoc />
    public bool ShouldHandle(ParameterInfo parameter)


@@ 94,12 94,25 @@ public class PacketListIndexAttributeGenerator : IParameterGenerator

    /// <inheritdoc />
    public IError? GenerateDeserializerPart(IndentedTextWriter textWriter, PacketInfo packetInfo)
        => GenerateDeserializerPart(textWriter, packetInfo, true);

    /// <summary>
    /// Generate part for the Deserializer method to deserialize the given parameter.
    /// </summary>
    /// <param name="textWriter">The text writer to write the code to.</param>
    /// <param name="packetInfo">The packet info to generate for.</param>
    /// <param name="declare">Whether to declare the local variable.</param>
    /// <returns>The generated source code.</returns>
    public IError? GenerateDeserializerPart(IndentedTextWriter textWriter, PacketInfo packetInfo, bool declare)
    {
        var generator = new ConverterDeserializationGenerator(textWriter);
        var parameter = packetInfo.Parameters.Current;
        var attribute = parameter.Attributes.First(x => x.FullName == PacketListIndexAttributeFullName);

        generator.DeclareLocalVariable(parameter);
        if (declare)
        {
            generator.DeclareLocalVariable(parameter);
        }

        // add optional if
        if (parameter.IsOptional())

M Packets/NosSmooth.PacketSerializersGenerator/SourceGenerator.cs => Packets/NosSmooth.PacketSerializersGenerator/SourceGenerator.cs +1 -0
@@ 54,6 54,7 @@ public class SourceGenerator : ISourceGenerator
                new PacketListIndexAttributeGenerator(inlineTypeConverter),
                new PacketContextListAttributeGenerator(inlineTypeConverter),
                new PacketConditionalIndexAttributeGenerator(inlineTypeConverter),
                new PacketConditionalListIndexAttributeGenerator(inlineTypeConverter),
            }
        );
    }

A Packets/NosSmooth.Packets/Enums/Raids/RaidBfPacketType.cs => Packets/NosSmooth.Packets/Enums/Raids/RaidBfPacketType.cs +25 -0
@@ 0,0 1,25 @@
//
//  RaidBfPacketType.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.Packets.Server.Raids;
#pragma warning disable CS1591

namespace NosSmooth.Packets.Enums.Raids;

/// <summary>
/// A type of <see cref="RaidBfPacket"/>.
/// </summary>
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1602:Enumeration items should be documented", Justification = "Self-explanatory.")]
public enum RaidBfPacketType
{
    MissionStarted = 0,
    MissionCleared = 1,
    TimeUp = 2,
    LeaderDied = 3,
    NoLivesLeft = 4,
    MissionFailed = 5
}
\ No newline at end of file

A Packets/NosSmooth.Packets/Enums/Raids/RaidLeaveType.cs => Packets/NosSmooth.Packets/Enums/Raids/RaidLeaveType.cs +26 -0
@@ 0,0 1,26 @@
//
//  RaidLeaveType.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.Server.Raids;

namespace NosSmooth.Packets.Enums.Raids;

/// <summary>
/// A sub type of <see cref="RaidPacket"/>
/// in case the type of the packet is Leave.
/// </summary>
public enum RaidLeaveType
{
    /// <summary>
    /// The player has left the raid by himself.
    /// </summary>
    PlayerLeft = 0,

    /// <summary>
    /// The raid is finished.
    /// </summary>
    RaidFinished = 1
}
\ No newline at end of file

A Packets/NosSmooth.Packets/Enums/Raids/RaidPacketType.cs => Packets/NosSmooth.Packets/Enums/Raids/RaidPacketType.cs +45 -0
@@ 0,0 1,45 @@
//
//  RaidPacketType.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.Server.Raids;

namespace NosSmooth.Packets.Enums.Raids;

/// <summary>
/// A type of <see cref="RaidPacket"/>.
/// </summary>
public enum RaidPacketType
{
    /// <summary>
    /// A list of member ids follows.
    /// </summary>
    ListMembers = 0,

    /// <summary>
    /// Character left or the raid is finished.
    /// </summary>
    Leave = 1,

    /// <summary>
    /// Leader id follows (or -1 in case of leave).
    /// </summary>
    Leader = 2,

    /// <summary>
    /// Hp, mp stats of players follow.
    /// </summary>
    PlayerHealths = 3,

    /// <summary>
    /// Sent after raid start, but before refresh members.
    /// </summary>
    AfterStartBeforeRefreshMembers = 4,

    /// <summary>
    /// Raid has just started.
    /// </summary>
    Start = 5
}
\ No newline at end of file

A Packets/NosSmooth.Packets/Enums/Raids/RaidType.cs => Packets/NosSmooth.Packets/Enums/Raids/RaidType.cs +51 -0
@@ 0,0 1,51 @@
//
//  RaidType.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;
#pragma warning disable CS1591

namespace NosSmooth.Packets.Enums.Raids;

/// <summary>
/// A type of a raid.
/// </summary>
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1602:Enumeration items should be documented", Justification = "Self-explanatory.")]
public enum RaidType
{
    Cuby = 0,
    Ginseng = 1,
    Castra = 2,
    GiantBlackSpider = 3,
    Slade = 4,
    ChickenKing = 5,
    Namaju = 6,
    Grasslin = 7,
    Snowman = 8,
    RobberGang = 9,
    JackOLantern = 10,
    ChickenQueen = 11,
    Pirate = 12,
    Kertos = 13,
    Valakus = 14,
    Grenigas = 15,
    LordDraco = 16,
    Glacerus = 17,
    Foxy = 18,
    Maru = 19,
    Laurena = 20,
    HongbiCheongbi = 21,
    LolaLopears = 22,
    Zenas = 23,
    Erenia = 24,
    Fernon = 25,
    Fafnir = 26,
    Yertirand = 27,
    MadProffesor = 28,
    MadMarchHare = 29,
    Kirollas = 30,
    Carno = 31,
    Belial = 32
}
\ No newline at end of file

M Packets/NosSmooth.Packets/Server/Groups/PstPacket.cs => Packets/NosSmooth.Packets/Server/Groups/PstPacket.cs +1 -1
@@ 52,7 52,7 @@ public record PstPacket
    [PacketIndex(9)]
    SexType? PlayerSex,
    [PacketIndex(10)]
    long? PlayerMorphVNum,
    int? PlayerMorphVNum,
    [PacketIndex(11, IsOptional = true)]
    IReadOnlyList<EffectsSubPacket>? Effects
) : IPacket;
\ No newline at end of file

A Packets/NosSmooth.Packets/Server/Raids/RaidBfPacket.cs => Packets/NosSmooth.Packets/Server/Raids/RaidBfPacket.cs +28 -0
@@ 0,0 1,28 @@
//
//  RaidBfPacket.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.Raids;
using NosSmooth.PacketSerializer.Abstractions.Attributes;

namespace NosSmooth.Packets.Server.Raids;

/// <summary>
/// Raid ui packet. Unknown function.
/// </summary>
/// <param name="Type">Unknown TODO.</param>
/// <param name="WindowType">Unknown TODO.</param>
/// <param name="Unknown">Unknown TODO.</param>
[PacketHeader("raidbf", PacketSource.Server)]
[GenerateSerializer(true)]
public record RaidBfPacket
(
    [PacketIndex(0)]
    byte Type,
    [PacketIndex(1)]
    RaidBfPacketType WindowType,
    [PacketIndex(2)]
    byte Unknown
) : IPacket;
\ No newline at end of file

A Packets/NosSmooth.Packets/Server/Raids/RaidMbfPacket.cs => Packets/NosSmooth.Packets/Server/Raids/RaidMbfPacket.cs +36 -0
@@ 0,0 1,36 @@
//
//  RaidMbfPacket.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.Raids;

/// <summary>
/// Raid state of lives, lockers.
/// </summary>
/// <param name="MonsterLockerInitial"></param>
/// <param name="MonsterLockerCurrent"></param>
/// <param name="ButtonLockerInitial"></param>
/// <param name="ButtonLockerCurrent"></param>
/// <param name="CurrentLives"></param>
/// <param name="InitialLives"></param>
[PacketHeader("raidmbf", PacketSource.Server)]
[GenerateSerializer(true)]
public record RaidMbfPacket
(
    [PacketIndex(0)]
    short MonsterLockerInitial,
    [PacketIndex(1)]
    short MonsterLockerCurrent,
    [PacketIndex(2)]
    short ButtonLockerInitial,
    [PacketIndex(3)]
    short ButtonLockerCurrent,
    [PacketIndex(4)]
    short CurrentLives,
    [PacketIndex(5)]
    short InitialLives
) : IPacket;
\ No newline at end of file

A Packets/NosSmooth.Packets/Server/Raids/RaidPacket.cs => Packets/NosSmooth.Packets/Server/Raids/RaidPacket.cs +39 -0
@@ 0,0 1,39 @@
//
//  RaidPacket.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.Raids;
using NosSmooth.PacketSerializer.Abstractions.Attributes;

namespace NosSmooth.Packets.Server.Raids;

/// <summary>
/// Raid status packet. Sent for various raid operations.
/// </summary>
/// <remarks>
/// For every type the rest of the packet is different.
/// Fields start with the type they are present for.
/// </remarks>
/// <param name="Type">The status type.</param>
/// <param name="LeaderId">The id of the leader, null if leaving. Present only for Leader type.</param>
/// <param name="LeaveType">The type of the leave type. Present only for Leave  type.</param>
/// <param name="ListMembersPlayerIds">The ids of players in the raid. Present only for ListMembers.</param>
/// <param name="PlayerHealths">Health of the players. Present only for PlayerHealths.</param>
[PacketHeader("raid", PacketSource.Server)]
[PacketHeader("raidf", PacketSource.Server)]
[GenerateSerializer(true)]
public record RaidPacket
(
    [PacketIndex(0)]
    RaidPacketType Type,
    [PacketConditionalIndex(1, "Type", false, RaidPacketType.Leader, IsOptional = true)]
    long? LeaderId,
    [PacketConditionalIndex(2, "Type", false, RaidPacketType.Leave, IsOptional = true)]
    RaidLeaveType? LeaveType,
    [PacketConditionalListIndex(3, "Type", false, RaidPacketType.ListMembers, ListSeparator = ' ', IsOptional = true)]
    IReadOnlyList<long>? ListMembersPlayerIds,
    [PacketConditionalListIndex(4, "Type", false, RaidPacketType.PlayerHealths, InnerSeparator = '.', ListSeparator = ' ', IsOptional = true)]
    IReadOnlyList<RaidPlayerHealthsSubPacket>? PlayerHealths
) : IPacket;
\ No newline at end of file

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

/// <summary>
/// A sub packet of <see cref="RaidPacket"/>
/// present for PlayerHealths. Contains
/// information about player healths.
/// </summary>
/// <param name="PlayerId">The id of the player.</param>
/// <param name="HpPercentage">The hp percentage of the player.</param>
/// <param name="MpPercentage">The mp percentage of the player.</param>
[PacketHeader(null, PacketSource.Server)]
[GenerateSerializer(true)]
public record RaidPlayerHealthsSubPacket
(
    [PacketIndex(0)]
    long PlayerId,
    [PacketIndex(1)]
    byte HpPercentage,
    [PacketIndex(2)]
    byte MpPercentage
) : IPacket;
\ No newline at end of file

A Packets/NosSmooth.Packets/Server/Raids/RbossPacket.cs => Packets/NosSmooth.Packets/Server/Raids/RbossPacket.cs +36 -0
@@ 0,0 1,36 @@
//
//  RbossPacket.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.Server.Raids;

/// <summary>
/// Raid boss information.
/// </summary>
/// <remarks>
/// EntityType and EntityId will be null in case of no boss.
/// </remarks>
/// <param name="EntityType">The boss entity type.</param>
/// <param name="EntityId">The boss entity id.</param>
/// <param name="MaxHp">The max hp of the boss.</param>
/// <param name="VNum">The vnum of the boss entity.</param>
[PacketHeader("rboss", PacketSource.Server)]
[GenerateSerializer(true)]
public record RbossPacket
(
    [PacketIndex(0)]
    EntityType? EntityType,
    [PacketIndex(1)]
    long? EntityId,
    [PacketIndex(2)]
    int Hp,
    [PacketIndex(3)]
    int MaxHp,
    [PacketIndex(4)]
    int VNum
) : IPacket;
\ No newline at end of file

A Packets/NosSmooth.Packets/Server/Raids/RdlstPacket.cs => Packets/NosSmooth.Packets/Server/Raids/RdlstPacket.cs +32 -0
@@ 0,0 1,32 @@
//
//  RdlstPacket.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.Raids;
using NosSmooth.PacketSerializer.Abstractions.Attributes;

namespace NosSmooth.Packets.Server.Raids;

/// <summary>
/// A packet containing information about raid members.
/// </summary>
/// <param name="MinimumLevel">The minimum needed level for the raid treasure.</param>
/// <param name="MaximumLevel">The maximum needed level for the raid treasure.</param>
/// <param name="RaidType">Unknown TODO.</param>
/// <param name="Players">Information about members in the raid.</param>
[PacketHeader("rdlst", PacketSource.Server)]
[PacketHeader("rdlstf", PacketSource.Server)]
[GenerateSerializer(true)]
public record RdlstPacket
(
    [PacketIndex(0)]
    byte MinimumLevel,
    [PacketIndex(1)]
    byte MaximumLevel,
    [PacketIndex(2)]
    RaidType RaidType,
    [PacketIndex(3)]
    IReadOnlyList<RdlstSubPacket> Players
) : IPacket;
\ No newline at end of file

A Packets/NosSmooth.Packets/Server/Raids/RdlstSubPacket.cs => Packets/NosSmooth.Packets/Server/Raids/RdlstSubPacket.cs +45 -0
@@ 0,0 1,45 @@
//
//  RdlstSubPacket.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.Players;
using NosSmooth.PacketSerializer.Abstractions.Attributes;
using NosSmooth.PacketSerializer.Abstractions.Common;

namespace NosSmooth.Packets.Server.Raids;

/// <summary>
/// A sub packet of <see cref="RdlstPacket"/>.
/// Information about a member of the raid.
/// </summary>
/// <param name="Level">The level of the player.</param>
/// <param name="MorphVNum">The morph vnum of the player.</param>
/// <param name="Class">The class of the player.</param>
/// <param name="Deaths">The current number of deaths in the raid.</param>
/// <param name="Name">The name of the player.</param>
/// <param name="Sex">The sex of the player.</param>
/// <param name="Id">The id of the player entity.</param>
/// <param name="HeroLevel">The hero level of the player.</param>
[PacketHeader(null, PacketSource.Server)]
[GenerateSerializer(true)]
public record RdlstSubPacket
(
    [PacketIndex(0)]
    byte Level,
    [PacketIndex(1)]
    int? MorphVNum,
    [PacketIndex(2)]
    PlayerClass Class,
    [PacketIndex(3)]
    byte Deaths,
    [PacketIndex(4)]
    NameString Name,
    [PacketIndex(5)]
    SexType Sex,
    [PacketIndex(6)]
    long Id,
    [PacketIndex(7)]
    byte? HeroLevel
);
\ No newline at end of file

A Tests/NosSmooth.Packets.Tests/Converters/Packets/RaidPacketConverterTests.cs => Tests/NosSmooth.Packets.Tests/Converters/Packets/RaidPacketConverterTests.cs +134 -0
@@ 0,0 1,134 @@
//
//  RaidPacketConverterTests.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 NosSmooth.Packets.Enums.Raids;
using NosSmooth.Packets.Server.Raids;
using NosSmooth.PacketSerializer;
using NosSmooth.PacketSerializer.Abstractions.Attributes;
using NosSmooth.PacketSerializer.Extensions;
using NosSmooth.PacketSerializer.Packets;
using Xunit;

namespace NosSmooth.Packets.Tests.Converters.Packets;

/// <summary>
/// Tests RaidPacketConverter.
/// </summary>
public class RaidPacketConverterTests
{
    private readonly IPacketSerializer _packetSerializer;

    /// <summary>
    /// Initializes a new instance of the <see cref="RaidPacketConverterTests"/> class.
    /// </summary>
    public RaidPacketConverterTests()
    {
        var provider = new ServiceCollection()
            .AddPacketSerialization()
            .BuildServiceProvider();

        _packetSerializer = provider.GetRequiredService<IPacketSerializer>();
        provider.GetRequiredService<IPacketTypesRepository>().AddDefaultPackets();
    }

    /// <summary>
    /// Tests that deserialization of raid packet of list members.
    /// </summary>
    [Fact]
    public void Converter_Deserialization_ListMembers()
    {
        var packetResult = _packetSerializer.Deserialize
        (
            "raid 0 1 2 3 4 5",
            PacketSource.Server
        );
        Assert.True(packetResult.IsSuccess);
        var packet = (RaidPacket)packetResult.Entity;
        Assert.Equal(RaidPacketType.ListMembers, packet.Type);
        Assert.NotNull(packet.ListMembersPlayerIds);
        Assert.Null(packet.LeaveType);
        Assert.Null(packet.LeaderId);
        Assert.Null(packet.PlayerHealths);

        Assert.Equal(new long[] { 1, 2, 3, 4, 5 }, packet.ListMembersPlayerIds);
    }

    /// <summary>
    /// Tests that deserialization of raid packet of leave.
    /// </summary>
    [Fact]
    public void Converter_Deserialization_Leave()
    {
        var packetResult = _packetSerializer.Deserialize
        (
            "raid 1 0",
            PacketSource.Server
        );
        Assert.True(packetResult.IsSuccess);
        var packet = (RaidPacket)packetResult.Entity;
        Assert.Equal(RaidPacketType.Leave, packet.Type);
        Assert.Null(packet.ListMembersPlayerIds);
        Assert.NotNull(packet.LeaveType);
        Assert.Null(packet.LeaderId);
        Assert.Null(packet.PlayerHealths);

        Assert.Equal(RaidLeaveType.PlayerLeft, packet.LeaveType);
    }

    /// <summary>
    /// Tests that deserialization of raid packet of leader.
    /// </summary>
    [Fact]
    public void Converter_Deserialization_Leader()
    {
        var packetResult = _packetSerializer.Deserialize
        (
            "raid 2 50",
            PacketSource.Server
        );
        Assert.True(packetResult.IsSuccess);
        var packet = (RaidPacket)packetResult.Entity;
        Assert.Equal(RaidPacketType.Leader, packet.Type);
        Assert.Null(packet.ListMembersPlayerIds);
        Assert.Null(packet.LeaveType);
        Assert.NotNull(packet.LeaderId);
        Assert.Null(packet.PlayerHealths);

        Assert.Equal(50, packet.LeaderId);
    }

    /// <summary>
    /// Tests that deserialization of raid packet of member healths.
    /// </summary>
    [Fact]
    public void Converter_Deserialization_MemberHealths()
    {
        var packetResult = _packetSerializer.Deserialize
        (
            "raid 3 1.100.100 2.90.95 3.95.90",
            PacketSource.Server
        );
        Assert.True(packetResult.IsSuccess);
        var packet = (RaidPacket)packetResult.Entity;
        Assert.Equal(RaidPacketType.PlayerHealths, packet.Type);
        Assert.Null(packet.ListMembersPlayerIds);
        Assert.Null(packet.LeaveType);
        Assert.Null(packet.LeaderId);
        Assert.NotNull(packet.PlayerHealths);

        Assert.Equal
        (
            new[]
            {
                new RaidPlayerHealthsSubPacket(1, 100, 100),
                new RaidPlayerHealthsSubPacket(2, 90, 95),
                new RaidPlayerHealthsSubPacket(3, 95, 90)
            },
            packet.PlayerHealths
        );
    }
}
\ No newline at end of file