~ruther/NosSmooth

2043a7d79fee86f80765ba9a785c09ef5aefa62c — František Boháček 2 years ago b210c73
docs: add missing documentation
29 files changed, 164 insertions(+), 27 deletions(-)

M Core/NosSmooth.Core/Contracts/IContract.cs
M Core/NosSmooth.Game/Data/Dialogs/Dialog.cs
M Core/NosSmooth.Game/Data/Raids/RaidProgress.cs
M Core/NosSmooth.Game/Errors/WrongSkillPositionError.cs
M Core/NosSmooth.Game/Errors/WrongSkillTypeError.cs
M Core/NosSmooth.Game/Events/Raids/RaidFinishedEvent.cs
M Core/NosSmooth.Game/Events/Raids/RaidJoinedEvent.cs
M Core/NosSmooth.Game/Events/Raids/RaidStateChangedEvent.cs
M Core/NosSmooth.Game/Events/Ui/DialogOpenedEvent.cs
M Extensions/NosSmooth.Extensions.Combat/CombatManager.cs
M Extensions/NosSmooth.Extensions.Combat/Selectors/InventoryItem.cs
M Packets/NosSmooth.Packets/Enums/QnamlType.cs
M Packets/NosSmooth.Packets/Server/Battle/SuPacket.cs
M Packets/NosSmooth.Packets/Server/Character/CInfoPacket.cs
M Packets/NosSmooth.Packets/Server/Chat/Sayi2Packet.cs
M Packets/NosSmooth.Packets/Server/Entities/StPacket.cs
M Packets/NosSmooth.Packets/Server/Mates/ScNPacket.cs
M Packets/NosSmooth.Packets/Server/Raids/RbossPacket.cs
M Packets/NosSmooth.Packets/Server/Raids/RdlstfPacket.cs
M Packets/NosSmooth.Packets/Server/UI/DlgPacket.cs
M Packets/NosSmooth.Packets/Server/UI/DlgiPacket.cs
M Packets/NosSmooth.Packets/Server/UI/QnaPacket.cs
M Packets/NosSmooth.Packets/Server/UI/QnamlPacket.cs
M Packets/NosSmooth.Packets/Server/UI/Qnamli2Packet.cs
M Tests/NosSmooth.Core.Tests/Commands/CommandProcessorTests.cs
M Tests/NosSmooth.Core.Tests/Commands/Walking/WalkCommandHandlerTests.cs
M Tests/NosSmooth.Core.Tests/Contracts/ContractTests.cs
M Tests/NosSmooth.Core.Tests/Fakes/Commands/FakeCommand.cs
M Tests/NosSmooth.Game.Tests/PacketFileClient.cs
M Core/NosSmooth.Core/Contracts/IContract.cs => Core/NosSmooth.Core/Contracts/IContract.cs +2 -2
@@ 66,7 66,7 @@ public interface IContract
    /// For example, to use skill, create a contract for
    /// using a skill and call this method.
    /// If you want to wait for response from the server,
    /// use <see cref="WaitForAsync"/> instead.
    /// use <see cref="IContract{T, U}.WaitForAsync"/> instead.
    /// That will register the contract and wait for response.
    /// </remarks>
    /// <param name="ct">The cancellation token used for cancelling the operation.</param>


@@ 81,7 81,7 @@ public interface IContract
/// Could be used for operations that may end successfully or fail
/// after some time, with response from the server.
///
/// Look at <see cref="ContractBuilder"/> for example usage.
/// Look at <see cref="ContractBuilder{TData,TState,TError}"/> for example usage.
/// </remarks>
/// <typeparam name="TData">The data returned by the contract in case of success.</typeparam>
/// <typeparam name="TState">Type containing the states of the contract.</typeparam>

M Core/NosSmooth.Game/Data/Dialogs/Dialog.cs => Core/NosSmooth.Game/Data/Dialogs/Dialog.cs +12 -2
@@ 4,6 4,7 @@
//  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.Apis;
using NosSmooth.Packets.Enums;
using OneOf;



@@ 12,8 13,17 @@ namespace NosSmooth.Game.Data.Dialogs;
/// <summary>
/// Represents dialog sent by the server
/// </summary>
/// <param name="AcceptCommand">The accept command sent upon accept.</param>
/// <param name="Parameters">The parameters of the dialog.</param>
/// <remarks>
/// To deny a dialog with null <see cref="DenyCommand"/>, just ignore it.
///
/// For answering to a dialog, use <see cref="DialogHandler"/>.
/// It takes care of collisions. In case the same dialog is accepted
/// and denied from elsewhere, an error will be returned.
/// </remarks>
/// <param name="AcceptCommand">The accept command/packet sent upon accept.</param>
/// <param name="DenyCommand">The deny command/packet sent upon denying. This may be null for some dialogs. To deny a dialog with null DenyCommand, just ignore it.</param>
/// <param name="Message">The message of the dialog, may be a constant i18n message, or a string.</param>
/// <param name="Parameters">The parameters of the i18n message. Empty for string messages.</param>
public record Dialog
(
    string AcceptCommand,

M Core/NosSmooth.Game/Data/Raids/RaidProgress.cs => Core/NosSmooth.Game/Data/Raids/RaidProgress.cs +13 -0
@@ 6,6 6,19 @@

namespace NosSmooth.Game.Data.Raids;

/// <summary>
/// A progress of a started <see cref="Raid"/>.
/// </summary>
/// <remarks>
/// Lockers are relevant before <see cref="RaidState.BossFight"/>,
/// when all lockers are unlocked, the boss room is opened.
/// </remarks>
/// <param name="MonsterLockerInitial">The number of monsters to kill.</param>
/// <param name="MonsterLockerCurrent">The number of monsters already killed.</param>
/// <param name="ButtonLockerInitial">The number of levers to pull.</param>
/// <param name="ButtonLockerCurrent">The number of levers already pulled.</param>
/// <param name="CurrentLives">The current number of lives.</param>
/// <param name="InitialLives">The maximum number of lives.</param>
public record RaidProgress
(
    short MonsterLockerInitial,

M Core/NosSmooth.Game/Errors/WrongSkillPositionError.cs => Core/NosSmooth.Game/Errors/WrongSkillPositionError.cs +7 -0
@@ 9,5 9,12 @@ using Remora.Results;

namespace NosSmooth.Game.Errors;

/// <summary>
/// Dash skills have to have a position specified,
/// non-dash skills cannot have position specified.
/// In case this is not respected in the skills api,
/// this error will be returned.
/// </summary>
/// <param name="AttackType">The type of the skill.</param>
public record WrongSkillPositionError(AttackType AttackType)
    : ResultError($"The skill with an attack type {AttackType} has to have a map position specified.");
\ No newline at end of file

M Core/NosSmooth.Game/Errors/WrongSkillTypeError.cs => Core/NosSmooth.Game/Errors/WrongSkillTypeError.cs +10 -0
@@ 5,9 5,19 @@
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.Data.Abstractions.Enums;
using NosSmooth.Game.Apis.Safe;
using Remora.Results;

namespace NosSmooth.Game.Errors;

/// <summary>
/// Used for safe skill api when the skill type does not match the correct one.
/// </summary>
/// <remarks>
/// Use one of the other methods <see cref="o:NostaleSkillsApi.UseSkillOn"/>,
/// <see cref="NostaleSkillsApi.UseSkillAt"/>,  <see cref="NostaleSkillsApi.UseSkillOnCharacter"/>.
/// </remarks>
/// <param name="ExpectedType">The type that was expected.</param>
/// <param name="ActualType">The actual type of the skill.</param>
public record WrongSkillTypeError(SkillType ExpectedType, SkillType ActualType)
    : ResultError($"Cannot use a skill of type {ActualType}, only type {ExpectedType} is allowed.");
\ No newline at end of file

M Core/NosSmooth.Game/Events/Raids/RaidFinishedEvent.cs => Core/NosSmooth.Game/Events/Raids/RaidFinishedEvent.cs +14 -0
@@ 8,4 8,18 @@ using NosSmooth.Game.Data.Raids;

namespace NosSmooth.Game.Events.Raids;

/// <summary>
/// The raid has been finished.
/// </summary>
/// <remarks>
/// There are multiple possibilities:
/// 1. The character has left.
/// 2. The raid was cancelled.
/// 3. The raid has failed. (either the whole team failed or the character)
/// 4. The raid has succeeded.
///
/// To determine which of these is the one that caused the finish
/// of the raid, look at <see cref="Raid"/> state.
/// </remarks>
/// <param name="Raid">The raid that has finished.</param>
public record RaidFinishedEvent(Raid Raid) : IGameEvent;
\ No newline at end of file

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

namespace NosSmooth.Game.Events.Raids;

/// <summary>
/// The character has joined a raid.
/// </summary>
/// <param name="Raid">The joined raid.</param>
public record RaidJoinedEvent(Raid Raid) : IGameEvent;
\ No newline at end of file

M Core/NosSmooth.Game/Events/Raids/RaidStateChangedEvent.cs => Core/NosSmooth.Game/Events/Raids/RaidStateChangedEvent.cs +6 -0
@@ 8,6 8,12 @@ using NosSmooth.Game.Data.Raids;

namespace NosSmooth.Game.Events.Raids;

/// <summary>
/// A raid <see cref="Raid"/> has changed a state from <see cref="PreviousState"/> to <see cref="CurrentState"/>.
/// </summary>
/// <param name="PreviousState">The previous state of the raid.</param>
/// <param name="CurrentState">The current state of the raid.</param>
/// <param name="Raid">The raid that has changed the state. The current raid, or the last one in case the raid was finished.</param>
public record RaidStateChangedEvent
(
    RaidState PreviousState,

M Core/NosSmooth.Game/Events/Ui/DialogOpenedEvent.cs => Core/NosSmooth.Game/Events/Ui/DialogOpenedEvent.cs +10 -0
@@ 4,8 4,18 @@
//  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.Apis;
using NosSmooth.Game.Data.Dialogs;

namespace NosSmooth.Game.Events.Ui;

/// <summary>
/// A dialog has been opened. An answer is expected.
/// </summary>
/// <remarks>
/// For answering to a dialog, use <see cref="DialogHandler"/>.
/// It takes care of collisions. In case the same dialog is accepted
/// and denied from elsewhere, an error will be returned.
/// </remarks>
/// <param name="Dialog">The dialog that has been opened.</param>
public record DialogOpenedEvent(Dialog Dialog) : IGameEvent;
\ No newline at end of file

M Extensions/NosSmooth.Extensions.Combat/CombatManager.cs => Extensions/NosSmooth.Extensions.Combat/CombatManager.cs +0 -1
@@ 22,7 22,6 @@ public class CombatManager : IStatefulEntity
{
    private readonly INostaleClient _client;
    private readonly Game.Game _game;
    private bool _cancelling;

    /// <summary>
    /// Initializes a new instance of the <see cref="CombatManager"/> class.

M Extensions/NosSmooth.Extensions.Combat/Selectors/InventoryItem.cs => Extensions/NosSmooth.Extensions.Combat/Selectors/InventoryItem.cs +5 -0
@@ 10,5 10,10 @@ using NosSmooth.Game.Data.Inventory;

namespace NosSmooth.Extensions.Combat.Selectors;

/// <summary>
/// An item in an inventory, with a bag and slot specified.
/// </summary>
/// <param name="Bag">The bag of the item.</param>
/// <param name="Item">The slot with the item.</param>
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", Justification = "Fix")]
public record struct InventoryItem(BagType Bag, InventorySlot Item);
\ No newline at end of file

M Packets/NosSmooth.Packets/Enums/QnamlType.cs => Packets/NosSmooth.Packets/Enums/QnamlType.cs +1 -0
@@ 6,6 6,7 @@

using System.Diagnostics.CodeAnalysis;
using NosSmooth.Packets.Server.UI;
#pragma warning disable CS1591

namespace NosSmooth.Packets.Enums;


M Packets/NosSmooth.Packets/Server/Battle/SuPacket.cs => Packets/NosSmooth.Packets/Server/Battle/SuPacket.cs +2 -0
@@ 29,6 29,8 @@ namespace NosSmooth.Packets.Server.Battle;
/// <param name="Damage">The damage the entity has taken.</param>
/// <param name="HitMode">The hit mode.</param>
/// <param name="SkillTypeMinusOne">The skill type of the skill.</param>
/// <param name="Hp">Current hp of the target.</param>
/// <param name="MaxHp">Maximum hp of the target.</param>
[PacketHeader("su", PacketSource.Server)]
[GenerateSerializer(true)]
public record SuPacket

M Packets/NosSmooth.Packets/Server/Character/CInfoPacket.cs => Packets/NosSmooth.Packets/Server/Character/CInfoPacket.cs +1 -1
@@ 20,7 20,7 @@ namespace NosSmooth.Packets.Server.Character;
/// <param name="Name">The name of the character.</param>
/// <param name="Unknown">Unknown TODO</param>
/// <param name="GroupId">The id of the group the player is in, if any.</param>
/// <param name="FamilyId">The id of the family the player is in, if any.</param>
/// <param name="FamilySubPacket">Information about family the player is in, if any.</param>
/// <param name="FamilyName">The name of the family the player is in, if any.</param>
/// <param name="CharacterId">The id of the character.</param>
/// <param name="Authority">The authority of the character.</param>

M Packets/NosSmooth.Packets/Server/Chat/Sayi2Packet.cs => Packets/NosSmooth.Packets/Server/Chat/Sayi2Packet.cs +2 -1
@@ 18,7 18,8 @@ namespace NosSmooth.Packets.Server.Chat;
/// <param name="EntityId">The id of the entity that spoke.</param>
/// <param name="Color">The say/message color.</param>
/// <param name="MessageConst">The message constant.</param>
/// <param name="Arguments">The arguments of the message.</param>
/// <param name="ParametersCount">The number of parameters.</param>
/// <param name="Parameters">The arguments of the message.</param>
[PacketHeader("sayi2", PacketSource.Server)]
[GenerateSerializer(true)]
public record Sayi2Packet

M Packets/NosSmooth.Packets/Server/Entities/StPacket.cs => Packets/NosSmooth.Packets/Server/Entities/StPacket.cs +2 -0
@@ 21,6 21,8 @@ namespace NosSmooth.Packets.Server.Entities;
/// <param name="MpPercentage">The current mp percentage.</param>
/// <param name="Hp">The current amount of hp.</param>
/// <param name="Mp">The current amount of mp.</param>
/// <param name="MaxHp">The current amount of hp.</param>
/// <param name="MaxMp">The maximum amount of mp.</param>
/// <param name="BuffVNums">The list of the buffs the entity has.</param>
[PacketHeader("st", PacketSource.Server)]
[GenerateSerializer(true)]

M Packets/NosSmooth.Packets/Server/Mates/ScNPacket.cs => Packets/NosSmooth.Packets/Server/Mates/ScNPacket.cs +2 -2
@@ 29,7 29,7 @@ namespace NosSmooth.Packets.Server.Mates;
/// <param name="BootsSubPacket">Information about partner's boots.</param>
/// <param name="Unknown1">Unknown TODO.</param>
/// <param name="Unknown2">Unknown TODO.</param>
/// <param name="Unknown3">Unknown TODO.</param>
/// <param name="AttackType">Unknown TODO.</param>
/// <param name="AttackUpgrade">The upgrade of attack.</param>
/// <param name="MinimumAttack">The minimum attack amount.</param>
/// <param name="MaximumAttack">The maximum attack amount.</param>


@@ 48,7 48,7 @@ namespace NosSmooth.Packets.Server.Mates;
/// <param name="HpMax">The maximum hp of the partner.</param>
/// <param name="Mp">The current mp of the partner.</param>
/// <param name="MpMax">The maximum mp of the partner.</param>
/// <param name="Unknown4">Unknown TODO.</param>
/// <param name="IsTeamMember">Unknown TODO.</param>
/// <param name="LevelExperience">The maximum experience in current level of the partner.</param>
/// <param name="Name">The name of the partner.</param>
/// <param name="MorphVNum">The morph vnum of the partner, if any.</param>

M Packets/NosSmooth.Packets/Server/Raids/RbossPacket.cs => Packets/NosSmooth.Packets/Server/Raids/RbossPacket.cs +1 -0
@@ 17,6 17,7 @@ namespace NosSmooth.Packets.Server.Raids;
/// </remarks>
/// <param name="EntityType">The boss entity type.</param>
/// <param name="EntityId">The boss entity id.</param>
/// <param name="Hp">The current hp of the boss.</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)]

M Packets/NosSmooth.Packets/Server/Raids/RdlstfPacket.cs => Packets/NosSmooth.Packets/Server/Raids/RdlstfPacket.cs +2 -1
@@ 14,7 14,8 @@ namespace NosSmooth.Packets.Server.Raids;
/// </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="Unknown">Unknown TODO.</param>
/// <param name="RaidType">The raid, <see cref="RaidType"/>.</param>
/// <param name="Players">Information about members in the raid.</param>
[PacketHeader("rdlstf", PacketSource.Server)]
[GenerateSerializer(true)]

M Packets/NosSmooth.Packets/Server/UI/DlgPacket.cs => Packets/NosSmooth.Packets/Server/UI/DlgPacket.cs +9 -0
@@ 8,6 8,15 @@ using NosSmooth.PacketSerializer.Abstractions.Attributes;

namespace NosSmooth.Packets.Server.UI;

/// <summary>
/// A dialog with normal string message.
/// </summary>
/// <remarks>
/// For a dialog with constant message, <see cref="DlgiPacket"/>.
/// </remarks>
/// <param name="AcceptCommand">The command/packet to send to accept the dialog.</param>
/// <param name="DenyCommand">The command/packet to send to deny the dialog.</param>
/// <param name="Message">The message of the dialog.</param>
[PacketHeader("dlg", PacketSource.Server)]
[GenerateSerializer(true)]
public record DlgPacket

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

namespace NosSmooth.Packets.Server.UI;

/// <summary>
/// A dialog with i18n message.
/// </summary>
/// <param name="AcceptCommand">The command/packet to send to accept the dialog.</param>
/// <param name="DenyCommand">The command/packet to send to deny the dialog.</param>
/// <param name="MessageConst">The i18n message.</param>
/// <param name="Parameters">The parameters of the message.</param>
[PacketHeader("dlgi", PacketSource.Server)]
[GenerateSerializer(true)]
public record DlgiPacket

M Packets/NosSmooth.Packets/Server/UI/QnaPacket.cs => Packets/NosSmooth.Packets/Server/UI/QnaPacket.cs +10 -0
@@ 4,10 4,20 @@
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.Packets.Enums;
using NosSmooth.PacketSerializer.Abstractions.Attributes;

namespace NosSmooth.Packets.Server.UI;

/// <summary>
/// Question and answer dialog that uses string message.
/// </summary>
/// <remarks>
/// To deny the dialog, just ignore it.
/// This behaves as <see cref="QnamlPacket"/> with type <see cref="QnamlType.Dialog"/>.
/// </remarks>
/// <param name="AcceptCommand">The command/packet to send to accept the dialog.</param>
/// <param name="Message">The message of the dialog.</param>
[PacketHeader("qna", PacketSource.Server)]
[GenerateSerializer(true)]
public record QnaPacket

M Packets/NosSmooth.Packets/Server/UI/QnamlPacket.cs => Packets/NosSmooth.Packets/Server/UI/QnamlPacket.cs +10 -1
@@ 9,7 9,16 @@ using NosSmooth.PacketSerializer.Abstractions.Attributes;

namespace NosSmooth.Packets.Server.UI;

[PacketHeader("qnaml", PacketSource.Server)]
/// <summary>
/// Question and answer dialog that can have a location in the UI specified using <see cref="Type"/>.
/// Uses string message.
/// </summary>
/// <remarks>
/// To deny the dialog, just ignore it.
/// </remarks>
/// <param name="Type">The ui location.</param>
/// <param name="AcceptCommand">The command/packet to send to accept the dialog.</param>
/// <param name="Message">The message of the dialog.</param>
[GenerateSerializer(true)]
public record QnamlPacket
(

M Packets/NosSmooth.Packets/Server/UI/Qnamli2Packet.cs => Packets/NosSmooth.Packets/Server/UI/Qnamli2Packet.cs +12 -0
@@ 9,6 9,18 @@ using NosSmooth.PacketSerializer.Abstractions.Attributes;

namespace NosSmooth.Packets.Server.UI;

/// <summary>
/// Question and answer dialog that can have a location in the UI specified using <see cref="Type"/>.
/// Uses i18n message.
/// </summary>
/// <remarks>
/// To deny the dialog, just ignore it.
/// </remarks>
/// <param name="Type">The ui location.</param>
/// <param name="AcceptCommand">The command/packet to send to accept the dialog.</param>
/// <param name="MessageConst">The i18n message.</param>
/// <param name="ParametersCount">The count of parameters for the mssage.</param>
/// <param name="Parameters">The parameters for the message.</param>
[PacketHeader("qnamli2", PacketSource.Server)]
[GenerateSerializer(true)]
public record Qnamli2Packet

M Tests/NosSmooth.Core.Tests/Commands/CommandProcessorTests.cs => Tests/NosSmooth.Core.Tests/Commands/CommandProcessorTests.cs +8 -8
@@ 413,9 413,9 @@ public class CommandProcessorTests
        Assert.NotNull(aggregateError);
        if (aggregateError is not null)
        {
            Assert.True(aggregateError.Errors.Any(x => x.Error == error1));
            Assert.Contains(aggregateError.Errors, x => (FakeError)x.Error! == error1);

            Assert.True(aggregateError.Errors.Any(x => x.Error == error2));
            Assert.Contains(aggregateError.Errors, x => (FakeError)x.Error! == error2);
            Assert.Equal(2, aggregateError.Errors.Count);
        }
    }


@@ 425,7 425,7 @@ public class CommandProcessorTests
    /// </summary>
    /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
    [Fact]
    public async Task ProcessCommand_HavingExceptionInHandler_ShouldNotThrow()
    public Task ProcessCommand_HavingExceptionInHandler_ShouldNotThrow()
    {
        var fakeCommand = new FakeCommand("asdf");
        var provider = new ServiceCollection()


@@ 447,7 447,7 @@ public class CommandProcessorTests
            )
            .BuildServiceProvider();

        await provider.GetRequiredService<CommandProcessor>().ProcessCommand
        return provider.GetRequiredService<CommandProcessor>().ProcessCommand
            (new FakeEmptyNostaleClient(), fakeCommand);
    }



@@ 456,7 456,7 @@ public class CommandProcessorTests
    /// </summary>
    /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
    [Fact]
    public async Task ProcessCommand_HavingExceptionInPreEvent_ShouldNotThrow()
    public Task ProcessCommand_HavingExceptionInPreEvent_ShouldNotThrow()
    {
        var fakeCommand = new FakeCommand("asdf");
        var provider = new ServiceCollection()


@@ 478,7 478,7 @@ public class CommandProcessorTests
            )
            .BuildServiceProvider();

        await provider.GetRequiredService<CommandProcessor>().ProcessCommand
        return provider.GetRequiredService<CommandProcessor>().ProcessCommand
            (new FakeEmptyNostaleClient(), fakeCommand);
    }



@@ 487,7 487,7 @@ public class CommandProcessorTests
    /// </summary>
    /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
    [Fact]
    public async Task ProcessCommand_HavingExceptionInPostEvent_ShouldNotThrow()
    public Task ProcessCommand_HavingExceptionInPostEvent_ShouldNotThrow()
    {
        var fakeCommand = new FakeCommand("asdf");
        var provider = new ServiceCollection()


@@ 509,7 509,7 @@ public class CommandProcessorTests
            )
            .BuildServiceProvider();

        await provider.GetRequiredService<CommandProcessor>().ProcessCommand
        return provider.GetRequiredService<CommandProcessor>().ProcessCommand
            (new FakeEmptyNostaleClient(), fakeCommand);
    }
}
\ No newline at end of file

M Tests/NosSmooth.Core.Tests/Commands/Walking/WalkCommandHandlerTests.cs => Tests/NosSmooth.Core.Tests/Commands/Walking/WalkCommandHandlerTests.cs +6 -6
@@ 70,7 70,7 @@ public class WalkCommandHandlerTests
    /// </summary>
    /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
    [Fact]
    public async Task Handle_PreservesTakeHandlerCommandProperties()
    public Task Handle_PreservesTakeHandlerCommandProperties()
    {
        var command = new WalkCommand
        (


@@ 106,7 106,7 @@ public class WalkCommandHandlerTests
            )
        );

        await walkHandler.HandleCommand(command);
        return walkHandler.HandleCommand(command);
    }

    /// <summary>


@@ 114,7 114,7 @@ public class WalkCommandHandlerTests
    /// </summary>
    /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
    [Fact]
    public async Task Handle_PreservesPlayerWalkPosition()
    public Task Handle_PreservesPlayerWalkPosition()
    {
        var command = new WalkCommand
        (


@@ 143,7 143,7 @@ public class WalkCommandHandlerTests
            )
        );

        await walkHandler.HandleCommand(command);
        return walkHandler.HandleCommand(command);
    }

    /// <summary>


@@ 202,7 202,7 @@ public class WalkCommandHandlerTests
    /// </summary>
    /// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
    [Fact]
    public async Task Handle_WithPets_UsesNearbyPositionForPetCommands()
    public Task Handle_WithPets_UsesNearbyPositionForPetCommands()
    {
        var command = new WalkCommand
        (


@@ 241,6 241,6 @@ public class WalkCommandHandlerTests
            )
        );

        await walkHandler.HandleCommand(command);
        return walkHandler.HandleCommand(command);
    }
}
\ No newline at end of file

M Tests/NosSmooth.Core.Tests/Contracts/ContractTests.cs => Tests/NosSmooth.Core.Tests/Contracts/ContractTests.cs +1 -0
@@ 11,6 11,7 @@ using NosSmooth.Core.Contracts;
using Remora.Results;
using Shouldly;
using Xunit;
#pragma warning disable CS4014

namespace NosSmooth.Core.Tests.Contracts;


M Tests/NosSmooth.Core.Tests/Fakes/Commands/FakeCommand.cs => Tests/NosSmooth.Core.Tests/Fakes/Commands/FakeCommand.cs +4 -0
@@ 8,4 8,8 @@ using NosSmooth.Core.Commands;

namespace NosSmooth.Core.Tests.Fakes.Commands;

/// <summary>
/// A command with arbitrary input.
/// </summary>
/// <param name="Input">The input of the command.</param>
public record FakeCommand(string Input) : ICommand;
\ No newline at end of file

M Tests/NosSmooth.Game.Tests/PacketFileClient.cs => Tests/NosSmooth.Game.Tests/PacketFileClient.cs +1 -2
@@ 29,13 29,12 @@ namespace NosSmooth.Game.Tests;
/// <summary>
/// A client used for tests. Supports loading just part of a file with packets.
/// </summary>
// TODO: make this class cleaner
public class PacketFileClient : BaseNostaleClient, IDisposable
{
    private const string LineRegex = ".*\\[(Recv|Send)\\]\t(.*)";
    private const string LabelRegex = "##(.*)";

    // TODO: make this class cleaner

    private readonly FileStream _stream;
    private readonly StreamReader _reader;
    private readonly IPacketSerializer _packetSerializer;

Do not follow this link