~ruther/NosSmooth

213fe53ae3a31d69b20543002b447347cf7893c0 — Rutherther 2 years ago 3b11a9d + 36bc263
Merge pull request #81 from Rutherther/feat/pathfinding-mates

Add mates to pathfinding
R Core/NosSmooth.Core/Commands/Walking/PetWalkCommand.cs => Core/NosSmooth.Core/Commands/Walking/MateWalkCommand.cs +4 -4
@@ 1,5 1,5 @@
//
//  PetWalkCommand.cs
//  MateWalkCommand.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.


@@ 11,16 11,16 @@ namespace NosSmooth.Core.Commands.Walking;
/// <summary>
/// Walk the given pet to the specified position.
/// </summary>
/// <param name="PetSelector">Index of the pet to move.</param>
/// <param name="MateId">The id of the mate.</param>
/// <param name="TargetX">The target x coordinate.</param>
/// <param name="TargetY">The target y coordinate.</param>
/// <param name="ReturnDistanceTolerance">The distance tolerance to the target when to return successful result.</param>
/// <param name="CanBeCancelledByAnother">Whether the command may be cancelled by another task within the same group.</param>
/// <param name="WaitForCancellation">Whether to wait for finish of the previous task</param>
/// <param name="AllowUserCancel">Whether to allow the user to cancel by taking any walk/focus/unfollow action</param>
public record PetWalkCommand
public record MateWalkCommand
(
    int PetSelector,
    long MateId,
    short TargetX,
    short TargetY,
    ushort ReturnDistanceTolerance,

M Core/NosSmooth.Core/Commands/Walking/WalkCommand.cs => Core/NosSmooth.Core/Commands/Walking/WalkCommand.cs +1 -1
@@ 23,8 23,8 @@ public record WalkCommand
(
    short TargetX,
    short TargetY,
    IReadOnlyList<(int PetSelector, short TargetX, short TargetY)>? Pets,
    ushort ReturnDistanceTolerance,
    IReadOnlyList<(long MateId, short TargetX, short TargetY)>? Pets = default,
    bool CanBeCancelledByAnother = true,
    bool WaitForCancellation = true,
    bool AllowUserCancel = true

M Core/NosSmooth.Core/Commands/Walking/WalkCommandHandler.cs => Core/NosSmooth.Core/Commands/Walking/WalkCommandHandler.cs +3 -3
@@ 36,7 36,7 @@ internal class WalkCommandHandler : ICommandHandler<WalkCommand>
    public async Task<Result> HandleCommand(WalkCommand command, CancellationToken ct = default)
    {
        var tasks = new List<Task<Result>>();
        foreach (var pet in command.Pets ?? Array.Empty<(int, short, short)>())
        foreach (var pet in command.Pets ?? Array.Empty<(long, short, short)>())
        {
            int x = pet.TargetX;
            int y = pet.TargetY;


@@ 45,9 45,9 @@ internal class WalkCommandHandler : ICommandHandler<WalkCommand>
            (
                _nostaleClient.SendCommandAsync
                (
                    new PetWalkCommand
                    new MateWalkCommand
                    (
                        pet.PetSelector,
                        pet.MateId,
                        (short)x,
                        (short)y,
                        command.ReturnDistanceTolerance,

M Extensions/NosSmooth.Extensions.Combat/Operations/WalkInRangeOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/WalkInRangeOperation.cs +1 -1
@@ 150,7 150,7 @@ public record WalkInRangeOperation
            }

            using var goToCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(ct);
            var walkResultTask = WalkManager.GoToAsync
            var walkResultTask = WalkManager.PlayerGoToAsync
                (closePosition.X, closePosition.Y, true, goToCancellationTokenSource.Token);

            while (!walkResultTask.IsCompleted)

M Extensions/NosSmooth.Extensions.Combat/Operations/WalkOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/WalkOperation.cs +1 -1
@@ 106,7 106,7 @@ public record WalkOperation(WalkManager WalkManager, short X, short Y) : ICombat
    }

    private Task<Result> UseAsync(ICombatState combatState, CancellationToken ct = default)
        => WalkManager.GoToAsync(X, Y, true, ct);
        => WalkManager.PlayerGoToAsync(X, Y, true, ct);

    /// <inheritdoc />
    public void Dispose()

A Extensions/NosSmooth.Extensions.Pathfinding/EntityState.cs => Extensions/NosSmooth.Extensions.Pathfinding/EntityState.cs +28 -0
@@ 0,0 1,28 @@
//
//  EntityState.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.Extensions.Pathfinding;

/// <summary>
/// A state, position of an entity.
/// </summary>
internal class EntityState
{
    /// <summary>
    /// Gets id of the entity.
    /// </summary>
    internal long Id { get; init; }

    /// <summary>
    /// Gets or sets the x coordinate of the entity.
    /// </summary>
    internal short X { get; set; }

    /// <summary>
    /// Gets or sets the y coordinate of the entity.
    /// </summary>
    internal short Y { get; set; }
}
\ No newline at end of file

A Extensions/NosSmooth.Extensions.Pathfinding/Errors/EntityStateNotFoundError.cs => Extensions/NosSmooth.Extensions.Pathfinding/Errors/EntityStateNotFoundError.cs +11 -0
@@ 0,0 1,11 @@
//
//  EntityStateNotFoundError.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Remora.Results;

namespace NosSmooth.Extensions.Pathfinding.Errors;

public record EntityStateNotFoundError(long EntityId) : ResultError($"Could not find pathfinder state of entity {EntityId}");
\ No newline at end of file

M Extensions/NosSmooth.Extensions.Pathfinding/Extensions/ServiceCollectionExtensions.cs => Extensions/NosSmooth.Extensions.Pathfinding/Extensions/ServiceCollectionExtensions.cs +2 -0
@@ 34,6 34,8 @@ public static class ServiceCollectionExtensions
            .AddPacketResponder<SuPacketResponder>()
            .AddPacketResponder<TpPacketResponder>()
            .AddPacketResponder<CInfoPacketResponder>()
            .AddPacketResponder<InResponder>()
            .AddPacketResponder<PtctlResponder>()
            .AddSingleton<WalkManager>()
            .AddSingleton<Pathfinder>()
            .AddSingleton<PathfinderState>();

M Extensions/NosSmooth.Extensions.Pathfinding/Pathfinder.cs => Extensions/NosSmooth.Extensions.Pathfinding/Pathfinder.cs +28 -2
@@ 29,7 29,7 @@ public class Pathfinder
    }

    /// <summary>
    /// Attempts to find a path between the current position and the target.
    /// Attempts to find a path between the current character position and the target.
    /// </summary>
    /// <param name="targetX">The target x coordinate.</param>
    /// <param name="targetY">The target y coordinate.</param>


@@ 39,7 39,33 @@ public class Pathfinder
        short targetX,
        short targetY
    )
        => FindPathFrom(_state.X, _state.Y, targetX, targetY);
        => FindPathFrom(_state.Character.X, _state.Character.Y, targetX, targetY);

    /// <summary>
    /// Attempts to find a path between the current position of the entity and the target.
    /// </summary>
    /// <remarks>
    /// Works only for entities that are stored in state,
    /// that means only the character and mates owned by the character.
    /// </remarks>
    /// <param name="entityId">The id of the entity to find path from.</param>
    /// <param name="targetX">The target x coordinate.</param>
    /// <param name="targetY">The target y coordinate.</param>
    /// <returns>A path or an error.</returns>
    public Result<Path> FindPathFromEntity
    (
        long entityId,
        short targetX,
        short targetY
    )
    {
        if (!_state.Entities.TryGetValue(entityId, out var entityState))
        {
            return new EntityStateNotFoundError(entityId);
        }

        return FindPathFrom(entityState.X, entityState.Y, targetX, targetY);
    }

    /// <summary>
    /// Attempts to find a path between the given positions on the current map.

M Extensions/NosSmooth.Extensions.Pathfinding/PathfinderState.cs => Extensions/NosSmooth.Extensions.Pathfinding/PathfinderState.cs +75 -6
@@ 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 System.Collections.Concurrent;
using NosSmooth.Core.Client;
using NosSmooth.Core.Stateful;
using NosSmooth.Data.Abstractions.Infos;


@@ 15,6 16,27 @@ namespace NosSmooth.Extensions.Pathfinding;
/// </summary>
public class PathfinderState : IStatefulEntity
{
    private ConcurrentDictionary<long, EntityState> _entities;

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

    /// <summary>
    /// Gets the entities.
    /// </summary>
    internal IReadOnlyDictionary<long, EntityState> Entities => _entities;

    /// <summary>
    /// Gets the character entity state.
    /// </summary>
    internal EntityState Character { get; private set; }

    /// <summary>
    /// Gets or sets the current map id.
    /// </summary>


@@ 26,17 48,64 @@ public class PathfinderState : IStatefulEntity
    internal IMapInfo? MapInfo { get; set; }

    /// <summary>
    /// Gets or sets the current x.
    /// Sets the id of the character entity.
    /// </summary>
    internal short X { get; set; }
    /// <param name="characterId">The character id.</param>
    internal void SetCharacterId(long characterId)
    {
        EntityState GetCharacter()
        {
            Character = new EntityState
            {
                Id = characterId,
                X = Character.X,
                Y = Character.Y
            };

            return Character;
        }

        _entities.TryRemove(Character.Id, out _);
        _entities.AddOrUpdate
        (
            characterId,
            _ => GetCharacter(),
            (_, _) => GetCharacter()
        );
    }

    /// <summary>
    /// Gets or sets the current y.
    /// Add the given entity to the list.
    /// </summary>
    internal short Y { get; set; }
    /// <param name="entityId">The id of the entity.</param>
    /// <param name="x">The x coordinate of the entity.</param>
    /// <param name="y">The y coordinate of the entity.</param>
    internal void AddEntity
    (
        long entityId,
        short x,
        short y
    )
    {
        EntityState GetEntity()
        {
            return new EntityState
            {
                Id = entityId,
                X = x,
                Y = y
            };
        }

        _entities.AddOrUpdate(entityId, _ => GetEntity(), (_, _) => GetEntity());
    }

    /// <summary>
    /// Gets or sets the id of the charcter.
    /// Remove all entities from the list.
    /// </summary>
    internal long CharacterId { get; set; }
    internal void ClearEntities()
    {
        _entities.Clear();
        SetCharacterId(Character.Id);
    }
}
\ No newline at end of file

M Extensions/NosSmooth.Extensions.Pathfinding/Responders/AtResponder.cs => Extensions/NosSmooth.Extensions.Pathfinding/Responders/AtResponder.cs +2 -2
@@ 30,8 30,8 @@ internal class AtResponder : IPacketResponder<AtPacket>
    {
        var packet = packetArgs.Packet;

        _state.X = packet.X;
        _state.Y = packet.Y;
        _state.Character.X = packet.X;
        _state.Character.Y = packet.Y;

        return Task.FromResult(Result.FromSuccess());
    }

M Extensions/NosSmooth.Extensions.Pathfinding/Responders/CInfoPacketResponder.cs => Extensions/NosSmooth.Extensions.Pathfinding/Responders/CInfoPacketResponder.cs +1 -1
@@ 28,7 28,7 @@ public class CInfoPacketResponder : IPacketResponder<CInfoPacket>
    /// <inheritdoc />
    public Task<Result> Respond(PacketEventArgs<CInfoPacket> packetArgs, CancellationToken ct = default)
    {
        _state.CharacterId = packetArgs.Packet.CharacterId;
        _state.SetCharacterId(packetArgs.Packet.CharacterId);
        return Task.FromResult(Result.FromSuccess());
    }
}
\ No newline at end of file

M Extensions/NosSmooth.Extensions.Pathfinding/Responders/CMapResponder.cs => Extensions/NosSmooth.Extensions.Pathfinding/Responders/CMapResponder.cs +1 -0
@@ 31,6 31,7 @@ internal class CMapResponder : IPacketResponder<CMapPacket>
    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<CMapPacket> packetArgs, CancellationToken ct = default)
    {
        _state.ClearEntities();
        var packet = packetArgs.Packet;

        _state.MapId = packet.Id;

A Extensions/NosSmooth.Extensions.Pathfinding/Responders/InResponder.cs => Extensions/NosSmooth.Extensions.Pathfinding/Responders/InResponder.cs +53 -0
@@ 0,0 1,53 @@
//
//  InResponder.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.Core.Packets;
using NosSmooth.Packets.Client.Battle;
using NosSmooth.Packets.Enums.Entities;
using NosSmooth.Packets.Server.Maps;
using Remora.Results;

namespace NosSmooth.Extensions.Pathfinding.Responders;

/// <inheritdoc />
public class InResponder : IPacketResponder<InPacket>
{
    private readonly PathfinderState _state;

    /// <summary>
    /// Initializes a new instance of the <see cref="InResponder"/> class.
    /// </summary>
    /// <param name="state">The pathfinder state.</param>
    public InResponder(PathfinderState state)
    {
        _state = state;

    }

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

        if (packet.EntityType != EntityType.Npc)
        {
            return Task.FromResult(Result.FromSuccess());
        }

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

        if (packet.NonPlayerSubPacket.OwnerId != _state.Character.Id)
        {
            return Task.FromResult(Result.FromSuccess());
        }

        _state.AddEntity(packet.EntityId, packet.PositionX, packet.PositionY);
        return Task.FromResult(Result.FromSuccess());
    }
}
\ No newline at end of file

A Extensions/NosSmooth.Extensions.Pathfinding/Responders/PtctlResponder.cs => Extensions/NosSmooth.Extensions.Pathfinding/Responders/PtctlResponder.cs +47 -0
@@ 0,0 1,47 @@
//
//  PtctlResponder.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.Security;
using NosSmooth.Core.Packets;
using NosSmooth.Packets.Client.Mates;
using Remora.Results;

namespace NosSmooth.Extensions.Pathfinding.Responders;

/// <inheritdoc />
public class PtctlResponder : IPacketResponder<PtctlPacket>
{
    private readonly PathfinderState _state;

    /// <summary>
    /// Initializes a new instance of the <see cref="PtctlResponder"/> class.
    /// </summary>
    /// <param name="state">The state.</param>
    public PtctlResponder(PathfinderState state)
    {
        _state = state;
    }

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

        foreach (var walkControl in packet.Controls)
        {
            if (!_state.Entities.TryGetValue(walkControl.MateTransportId, out var entityState))
            {
                _state.AddEntity(walkControl.MateTransportId, walkControl.PositionX, walkControl.PositionY);
                continue;
            }

            entityState.X = walkControl.PositionX;
            entityState.Y = walkControl.PositionY;
        }

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

M Extensions/NosSmooth.Extensions.Pathfinding/Responders/SuPacketResponder.cs => Extensions/NosSmooth.Extensions.Pathfinding/Responders/SuPacketResponder.cs +4 -3
@@ 29,7 29,8 @@ public class SuPacketResponder : IPacketResponder<SuPacket>
    public Task<Result> Respond(PacketEventArgs<SuPacket> packetArgs, CancellationToken ct = default)
    {
        var packet = packetArgs.Packet;
        if (packet.CasterEntityId == _state.CharacterId)

        if (_state.Entities.TryGetValue(packet.CasterEntityId, out var entityState))
        {
            if (packet.PositionX is null || packet.PositionY is null)
            {


@@ 41,8 42,8 @@ public class SuPacketResponder : IPacketResponder<SuPacket>
                return Task.FromResult(Result.FromSuccess());
            }

            _state.X = packet.PositionX.Value;
            _state.Y = packet.PositionY.Value;
            entityState.X = packet.PositionX.Value;
            entityState.Y = packet.PositionY.Value;
        }

        return Task.FromResult(Result.FromSuccess());

M Extensions/NosSmooth.Extensions.Pathfinding/Responders/TpPacketResponder.cs => Extensions/NosSmooth.Extensions.Pathfinding/Responders/TpPacketResponder.cs +3 -3
@@ 28,10 28,10 @@ public class TpPacketResponder : IPacketResponder<TpPacket>
    public Task<Result> Respond(PacketEventArgs<TpPacket> packetArgs, CancellationToken ct = default)
    {
        var packet = packetArgs.Packet;
        if (packet.EntityId == _state.CharacterId)
        if (_state.Entities.TryGetValue(packet.EntityId, out var entityState))
        {
            _state.X = packet.PositionX;
            _state.Y = packet.PositionY;
            entityState.X = packet.PositionX;
            entityState.Y = packet.PositionY;
        }

        return Task.FromResult(Result.FromSuccess());

M Extensions/NosSmooth.Extensions.Pathfinding/Responders/WalkResponder.cs => Extensions/NosSmooth.Extensions.Pathfinding/Responders/WalkResponder.cs +2 -2
@@ 29,8 29,8 @@ internal class WalkResponder : IPacketResponder<WalkPacket>
    {
        var packet = packetArgs.Packet;

        _state.X = packet.PositionX;
        _state.Y = packet.PositionY;
        _state.Character.X = packet.PositionX;
        _state.Character.Y = packet.PositionY;

        return Task.FromResult(Result.FromSuccess());
    }

M Extensions/NosSmooth.Extensions.Pathfinding/WalkManager.cs => Extensions/NosSmooth.Extensions.Pathfinding/WalkManager.cs +88 -9
@@ 7,6 7,7 @@
using NosSmooth.Core.Client;
using NosSmooth.Core.Commands.Walking;
using NosSmooth.Core.Errors;
using NosSmooth.Extensions.Pathfinding.Errors;
using Remora.Results;

namespace NosSmooth.Extensions.Pathfinding;


@@ 34,7 35,7 @@ public class WalkManager
    }

    /// <summary>
    /// Go to the given position.
    /// Move character to the given position.
    /// </summary>
    /// <remarks>
    /// Expect <see cref="WalkNotFinishedError"/> if the destination could not be reached.


@@ 44,31 45,109 @@ public class WalkManager
    /// <param name="y">The target y coordinate.</param>
    /// <param name="allowUserActions">Whether to allow user actions during the walk operation.</param>
    /// <param name="ct">The cancellation token used for cancelling the operation.</param>
    /// <param name="pets">The positions to walk pets to.</param>
    /// <returns>A result that may not succeed.</returns>
    public async Task<Result> GoToAsync(short x, short y, bool allowUserActions = true, CancellationToken ct = default, params (int Selector, short TargetX, short TargetY)[] pets)
    public async Task<Result> PlayerGoToAsync
    (
        short x,
        short y,
        bool allowUserActions = true,
        CancellationToken ct = default
    )
    {
        var pathResult = _pathfinder.FindPathFromCurrent(x, y);
        if (!pathResult.IsSuccess)
        if (!pathResult.IsDefined(out var path))
        {
            return Result.FromError(pathResult);
        }

        if (pathResult.Entity.Parts.Count == 0)
        return await TakePath
        (
            path,
            _state.Character,
            (x, y) => _client.SendCommandAsync
            (
                new WalkCommand
                (
                    x,
                    y,
                    2,
                    AllowUserCancel: allowUserActions
                ),
                ct
            )
        );
    }

    /// <summary>
    /// Move pet to the given position.
    /// </summary>
    /// <remarks>
    /// Expect <see cref="WalkNotFinishedError"/> if the destination could not be reached.
    /// Expect <see cref="NotFoundError"/> if the path could not be found.
    /// </remarks>
    /// <param name="mateId">The id of the mate to move.</param>
    /// <param name="x">The target x coordinate.</param>
    /// <param name="y">The target y coordinate.</param>
    /// <param name="allowUserActions">Whether to allow user actions during the walk operation.</param>
    /// <param name="ct">The cancellation token used for cancelling the operation.</param>
    /// <returns>A result that may not succeed.</returns>
    public async Task<Result> MateWalkToAsync
    (
        long mateId,
        short x,
        short y,
        bool allowUserActions = true,
        CancellationToken ct = default
    )
    {
        if (!_state.Entities.TryGetValue(mateId, out var entityState) || entityState == _state.Character)
        {
            return new EntityStateNotFoundError(mateId);
        }

        var pathResult = _pathfinder.FindPathFromEntity(mateId, x, y);

        if (!pathResult.IsDefined(out var path))
        {
            return Result.FromError(pathResult);
        }

        return await TakePath
        (
            path,
            entityState,
            (x, y) => _client.SendCommandAsync
            (
                new MateWalkCommand
                (
                    mateId,
                    x,
                    y,
                    2,
                    AllowUserCancel: allowUserActions
                ),
                ct
            )
        );
    }

    private async Task<Result> TakePath(Path path, EntityState state, Func<short, short, Task<Result>> walkFunc)
    {
        if (path.Parts.Count == 0)
        {
            return Result.FromSuccess();
        }
        var target = path.Parts.Last();

        var path = pathResult.Entity;
        while (!path.ReachedEnd)
        {
            if (path.MapId != _state.MapId)
            {
                return new WalkNotFinishedError(_state.X, _state.Y, WalkUnfinishedReason.MapChanged);
                return new WalkNotFinishedError(state.X, state.Y, WalkUnfinishedReason.MapChanged);
            }

            var next = path.TakeForwardPath();
            var walkResult = await _client.SendCommandAsync(new WalkCommand(next.X, next.Y, pets, 2, AllowUserCancel: allowUserActions), ct);
            var walkResult = await walkFunc(next.X, next.Y);
            if (!walkResult.IsSuccess)
            {
                if (path.ReachedEnd && walkResult.Error is WalkNotFinishedError walkNotFinishedError


@@ 77,7 156,7 @@ public class WalkManager
                    return Result.FromSuccess();
                }

                if (_state.X == x && _state.Y == y)
                if (state.X == target.X && state.Y == target.Y)
                {
                    return Result.FromSuccess();
                }

M Tests/NosSmooth.Core.Tests/Commands/Walking/WalkCommandHandlerTests.cs => Tests/NosSmooth.Core.Tests/Commands/Walking/WalkCommandHandlerTests.cs +14 -14
@@ 34,12 34,12 @@ public class WalkCommandHandlerTests
        (
            0,
            0,
            new (int, short, short)[]
            0,
            new (long, short, short)[]
            {
                (1, 0, 0),
                (2, 0, 0)
            },
            0
            }
        );
        var walkHandler = new WalkCommandHandler
        (


@@ 51,7 51,7 @@ public class WalkCommandHandlerTests
                    {
                        calledPlayerWalk = true;
                    }
                    if (c is PetWalkCommand)
                    if (c is MateWalkCommand)
                    {
                        calledPetWalk = true;
                    }


@@ 76,14 76,14 @@ public class WalkCommandHandlerTests
        (
            0,
            0,
            new (int, short, short)[]
            0,
            new (long, short, short)[]
            {
                (2, 0, 0),
                (5, 0, 0),
                (7, 0, 0),
                (9, 0, 0),
            },
            0,
            true,
            false,
            false


@@ 120,8 120,8 @@ public class WalkCommandHandlerTests
        (
            10,
            15,
            null,
            0,
            null,
            true,
            false,
            false


@@ 158,7 158,8 @@ public class WalkCommandHandlerTests
        (
            10,
            15,
            new (int, short, short)[]
            0,
            new (long, short, short)[]
            {
                (1, 0, 0),
                (2, 0, 0),


@@ 166,7 167,6 @@ public class WalkCommandHandlerTests
                (7, 0, 0),
                (8, 0, 0),
            },
            0,
            true,
            false,
            false


@@ 177,9 177,9 @@ public class WalkCommandHandlerTests
            (
                (c, _) =>
                {
                    if (c is PetWalkCommand petWalkCommand)
                    if (c is MateWalkCommand petWalkCommand)
                    {
                        if (command.Pets?.Select(x => x.PetSelector).Contains(petWalkCommand.PetSelector) ?? false)
                        if (command.Pets?.Select(x => x.MateId).Contains(petWalkCommand.MateId) ?? false)
                        {
                            calledCount++;
                        }


@@ 208,7 208,8 @@ public class WalkCommandHandlerTests
        (
            10,
            15,
            new (int, short, short)[]
            0,
            new (long, short, short)[]
            {
                (1, 0, 1),
                (2, 1, 0),


@@ 216,7 217,6 @@ public class WalkCommandHandlerTests
                (7, 1, 0),
                (8, 0, 1),
            },
            0,
            true,
            false,
            false


@@ 227,7 227,7 @@ public class WalkCommandHandlerTests
            (
                (c, _) =>
                {
                    if (c is PetWalkCommand petWalkCommand)
                    if (c is MateWalkCommand petWalkCommand)
                    {
                        Assert.True
                        (

Do not follow this link