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