~ruther/NosSmooth

972cf4714e11a5760eeb8c316be34625a2d240f7 — Rutherther 2 years ago 452ad53
feat(combat): update operations to be non-blocking, add support for handling waiting
M Extensions/NosSmooth.Extensions.Combat/CombatManager.cs => Extensions/NosSmooth.Extensions.Combat/CombatManager.cs +115 -51
@@ 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.Diagnostics;
using NosSmooth.Core.Client;
using NosSmooth.Core.Commands.Attack;
using NosSmooth.Core.Stateful;


@@ 61,65 62,26 @@ public class CombatManager : IStatefulEntity
                    {
                        while (!combatState.ShouldQuit && currentTarget == previousTarget)
                        {
                            if (!technique.ShouldContinue(combatState))
                            {
                                combatState.QuitCombat();
                                continue;
                            }

                            var operation = combatState.NextOperation();
                            var iterationResult = await HandleAttackIterationAsync(combatState, technique, ct);

                            if (operation is null)
                            { // The operation is null and the step has to be obtained from the technique.
                                var stepResult = technique.HandleCombatStep(combatState);
                                if (!stepResult.IsSuccess)
                                {
                                    return Result.FromError(stepResult);
                                }

                                previousTarget = currentTarget;
                                currentTarget = stepResult.Entity;
                            if (!iterationResult.IsSuccess)
                            {
                                var errorResult = technique.HandleError(combatState, Result.FromError(iterationResult));

                                if (previousTarget != currentTarget)
                                {
                                    continue;
                                if (!errorResult.IsSuccess)
                                { // end the attack.
                                    return errorResult;
                                }

                                operation = combatState.NextOperation();
                            }

                            if (operation is null)
                            { // The operation could be null just because there is currently not a skill to be used etc.
                                await Task.Delay(5, ct);
                                continue;
                            }

                            Result<CanBeUsedResponse> responseResult;
                            while ((responseResult = operation.CanBeUsed(combatState)).IsSuccess
                                && responseResult.Entity == CanBeUsedResponse.MustWait)
                            { // TODO: wait for just some amount of time
                                await Task.Delay(5, ct);
                            }

                            if (!responseResult.IsSuccess)
                            {
                                return Result.FromError(responseResult);
                            }

                            if (responseResult.Entity == CanBeUsedResponse.WontBeUsable)
                            var result = iterationResult.Entity;
                            if (!result.TargetChanged)
                            {
                                return new UnusableOperationError(operation);
                                continue;
                            }

                            var usageResult = await operation.UseAsync(combatState, ct);
                            if (!usageResult.IsSuccess)
                            {
                                var errorHandleResult = technique.HandleError(combatState, operation, usageResult);
                                if (!errorHandleResult.IsSuccess)
                                {
                                    return errorHandleResult;
                                }
                            }
                            previousTarget = currentTarget;
                            currentTarget = result.TargetId;
                        }

                        return Result.FromSuccess();


@@ 138,6 100,108 @@ public class CombatManager : IStatefulEntity
        return Result.FromSuccess();
    }

    private async Task<Result<(bool TargetChanged, long? TargetId)>> HandleAttackIterationAsync
        (CombatState combatState, ICombatTechnique technique, CancellationToken ct)
    {
        if (!technique.ShouldContinue(combatState))
        {
            combatState.QuitCombat();
            return Result<(bool, long?)>.FromSuccess((false, null));
        }

        // the operations need time for execution and/or
        // wait.
        await Task.Delay(50, ct);

        var tasks = technique.HandlingQueueTypes
            .Select(x => HandleTypeIterationAsync(x, combatState, technique, ct))
            .ToArray();

        var results = await Task.WhenAll(tasks);
        var errors = results.Where(x => !x.IsSuccess).Cast<IResult>().ToArray();

        return errors.Length switch
        {
            0 => results.FirstOrDefault
                (x => x.Entity.TargetChanged, Result<(bool TargetChanged, long?)>.FromSuccess((false, null))),
            1 => (Result<(bool, long?)>)errors[0],
            _ => new AggregateError()
        };
    }

    private async Task<Result<(bool TargetChanged, long? TargetId)>> HandleTypeIterationAsync
    (
        OperationQueueType queueType,
        CombatState combatState,
        ICombatTechnique technique,
        CancellationToken ct
    )
    {
        var currentOperation = combatState.GetCurrentOperation(queueType);
        if (currentOperation?.IsFinished() ?? false)
        {
            var operationResult = await currentOperation.WaitForFinishedAsync(combatState, ct);
            currentOperation.Dispose();

            if (!operationResult.IsSuccess)
            {
                return Result<(bool, long?)>.FromError(operationResult);
            }

            currentOperation = null;
        }

        if (currentOperation is null)
        { // waiting for an operation.
            currentOperation = combatState.NextOperation(queueType);

            if (currentOperation is null)
            { // The operation is null and the step has to be obtained from the technique.
                var stepResult = technique.HandleNextCombatStep(queueType, combatState);
                if (!stepResult.IsSuccess)
                {
                    return Result<(bool, long?)>.FromError(stepResult);
                }

                return Result<(bool, long?)>.FromSuccess((true, stepResult.Entity));
            }
        }

        if (!currentOperation.IsExecuting())
        { // not executing, check can be used, execute if can.
            var canBeUsedResult = currentOperation.CanBeUsed(combatState);
            if (!canBeUsedResult.IsDefined(out var canBeUsed))
            {
                return Result<(bool, long?)>.FromError(canBeUsedResult);
            }

            switch (canBeUsed)
            {
                case CanBeUsedResponse.WontBeUsable:
                    return new UnusableOperationError(currentOperation);
                case CanBeUsedResponse.MustWait:
                    var waitingResult = technique.HandleWaiting(queueType, combatState, currentOperation);

                    if (!waitingResult.IsSuccess)
                    {
                        return Result<(bool, long?)>.FromError(waitingResult);
                    }

                    return Result<(bool, long?)>.FromSuccess((false, null));
                case CanBeUsedResponse.CanBeUsed:
                    var executingResult = await currentOperation.BeginExecution(combatState, ct);

                    if (!executingResult.IsSuccess)
                    {
                        return Result<(bool, long?)>.FromError(executingResult);
                    }
                    break;
            }
        }

        return (false, null);
    }

    /// <summary>
    /// Register the given cancellation token source to be cancelled on skill use/cancel.
    /// </summary>

M Extensions/NosSmooth.Extensions.Combat/CombatState.cs => Extensions/NosSmooth.Extensions.Combat/CombatState.cs +68 -32
@@ 4,18 4,17 @@
//  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.Xml;
using System.Diagnostics.CodeAnalysis;
using NosSmooth.Core.Client;
using NosSmooth.Extensions.Combat.Operations;
using NosSmooth.Game.Data.Entities;

namespace NosSmooth.Extensions.Combat;

/// <inheritdoc />
internal class CombatState : ICombatState
{
    private readonly LinkedList<ICombatOperation> _operations;
    private ICombatOperation? _currentOperation;
    private readonly Dictionary<OperationQueueType, LinkedList<ICombatOperation>> _operations;
    private readonly Dictionary<OperationQueueType, ICombatOperation> _currentOperations;

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


@@ 28,7 27,8 @@ internal class CombatState : ICombatState
        Client = client;
        Game = game;
        CombatManager = combatManager;
        _operations = new LinkedList<ICombatOperation>();
        _operations = new Dictionary<OperationQueueType, LinkedList<ICombatOperation>>();
        _currentOperations = new Dictionary<OperationQueueType, ICombatOperation>();
    }

    /// <summary>


@@ 46,62 46,98 @@ internal class CombatState : ICombatState
    public INostaleClient Client { get; }

    /// <inheritdoc/>
    public void QuitCombat()
    {
        ShouldQuit = true;
    }
    public bool IsWaitingOnOperation => _currentOperations.Any(x => !x.Value.IsExecuting());

    /// <summary>
    /// Make a step in the queue.
    /// Move to next operation, if available.
    /// </summary>
    /// <returns>The current operation, if any.</returns>
    public ICombatOperation? NextOperation()
    /// <param name="queueType">The queue type to move to next operation in.</param>
    /// <returns>Next operation, if any.</returns>
    public ICombatOperation? NextOperation(OperationQueueType queueType)
    {
        var operation = _currentOperation = _operations.First?.Value;
        if (operation is not null)
        if (_operations.ContainsKey(queueType))
        {
            _operations.RemoveFirst();
            var nextOperation = _operations[queueType].FirstOrDefault();

            if (nextOperation is not null)
            {
                _currentOperations[queueType] = nextOperation;
                return nextOperation;
            }
        }

        return operation;
        return null;
    }

    /// <inheritdoc/>
    public IReadOnlyList<ICombatOperation> GetWaitingForOperations()
    {
        return _currentOperations.Values.Where(x => !x.IsExecuting()).ToList();
    }

    /// <inheritdoc/>
    public ICombatOperation? GetCurrentOperation(OperationQueueType queueType)
        => _currentOperations.GetValueOrDefault(queueType);

    /// <inheritdoc/>
    public bool IsExecutingOperation(OperationQueueType queueType, [NotNullWhen(true)] out ICombatOperation? operation)
    {
        operation = GetCurrentOperation(queueType);
        return operation is not null && operation.IsExecuting();
    }

    /// <inheritdoc/>
    public void QuitCombat()
    {
        ShouldQuit = true;
    }

    /// <inheritdoc/>
    public void SetCurrentOperation
        (ICombatOperation operation, bool emptyQueue = false, bool prependCurrentOperationToQueue = false)
    {
        var current = _currentOperation;
        _currentOperation = operation;
        var type = operation.QueueType;

        if (!_operations.ContainsKey(type))
        {
            _operations[type] = new LinkedList<ICombatOperation>();
        }

        if (emptyQueue)
        {
            _operations.Clear();
            _operations[type].Clear();
        }

        if (prependCurrentOperationToQueue)
        {
            _operations[type].AddFirst(operation);
            return;
        }

        if (prependCurrentOperationToQueue && current is not null)
        if (_currentOperations.ContainsKey(type))
        {
            _operations.AddFirst(current);
            _currentOperations[type].Dispose();
        }

        _currentOperations[type] = operation;
    }

    /// <inheritdoc/>
    public void EnqueueOperation(ICombatOperation operation)
    {
        _operations.AddLast(operation);
        var type = operation.QueueType;

        if (!_operations.ContainsKey(type))
        {
            _operations[type] = new LinkedList<ICombatOperation>();
        }

        _operations[type].AddLast(operation);
    }

    /// <inheritdoc/>
    public void RemoveOperations(Func<ICombatOperation, bool> filter)
    {
        var node = _operations.First;
        while (node != null)
        {
            var next = node.Next;
            if (filter(node.Value))
            {
                _operations.Remove(node);
            }
            node = next;
        }
        throw new NotImplementedException();
    }
}
\ No newline at end of file

M Extensions/NosSmooth.Extensions.Combat/Extensions/CombatStateExtensions.cs => Extensions/NosSmooth.Extensions.Combat/Extensions/CombatStateExtensions.cs +6 -13
@@ 7,7 7,9 @@
using NosSmooth.Extensions.Combat.Errors;
using NosSmooth.Extensions.Combat.Operations;
using NosSmooth.Extensions.Combat.Policies;
using NosSmooth.Extensions.Combat.Selectors;
using NosSmooth.Extensions.Pathfinding;
using NosSmooth.Game.Apis.Safe;
using NosSmooth.Game.Data.Characters;
using NosSmooth.Game.Data.Entities;
using NosSmooth.Game.Data.Items;


@@ 61,26 63,17 @@ public static class CombatStateExtensions
    /// Use the given skill.
    /// </summary>
    /// <param name="combatState">The combat state.</param>
    /// <param name="skillsApi">The skills api for using a skill.</param>
    /// <param name="skill">The skill.</param>
    /// <param name="target">The target to use skill at.</param>
    public static void UseSkill(this ICombatState combatState, Skill skill, ILivingEntity target)
    public static void UseSkill(this ICombatState combatState, NostaleSkillsApi skillsApi, Skill skill, ILivingEntity target)
    {
        if (combatState.Game.Character is null)
        {
            throw new InvalidOperationException("The character is not initialized.");
        }

        combatState.EnqueueOperation(new UseSkillOperation(skill, combatState.Game.Character, target));
    }

    /// <summary>
    /// Use primary skill.
    /// </summary>
    /// <param name="combatState">The combat state.</param>
    /// <param name="target">The target to use skill at.</param>
    public static void UsePrimarySkill(this ICombatState combatState, ILivingEntity target)
    {
        combatState.EnqueueOperation(new UsePrimarySkillOperation(target));
        combatState.EnqueueOperation(new UseSkillOperation(skillsApi, skill, combatState.Game.Character, target));
    }

    /// <summary>


@@ 88,7 81,7 @@ public static class CombatStateExtensions
    /// </summary>
    /// <param name="combatState">The combat state.</param>
    /// <param name="item">The item to use.</param>
    public static void UseItem(this ICombatState combatState, Item item)
    public static void UseItem(this ICombatState combatState, InventoryItem item)
    {
        combatState.EnqueueOperation(new UseItemOperation(item));
    }

M Extensions/NosSmooth.Extensions.Combat/Extensions/ServiceCollectionExtensions.cs => Extensions/NosSmooth.Extensions.Combat/Extensions/ServiceCollectionExtensions.cs +0 -4
@@ 5,8 5,6 @@
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.Extensions.DependencyInjection;
using NosSmooth.Core.Extensions;
using NosSmooth.Extensions.Combat.Responders;

namespace NosSmooth.Extensions.Combat.Extensions;



@@ 23,8 21,6 @@ public static class ServiceCollectionExtensions
    public static IServiceCollection AddNostaleCombat(this IServiceCollection serviceCollection)
    {
        return serviceCollection
            .AddPacketResponder<CancelResponder>()
            .AddPacketResponder<SuResponder>()
            .AddSingleton<CombatManager>();
    }
}
\ No newline at end of file

M Extensions/NosSmooth.Extensions.Combat/ICombatState.cs => Extensions/NosSmooth.Extensions.Combat/ICombatState.cs +31 -0
@@ 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.Diagnostics.CodeAnalysis;
using NosSmooth.Core.Client;
using NosSmooth.Extensions.Combat.Operations;
using NosSmooth.Game.Data.Entities;


@@ 31,6 32,36 @@ public interface ICombatState
    public INostaleClient Client { get; }

    /// <summary>
    /// Gets whether there is an operation that cannot be used
    /// and we must wait for it to be usable.
    /// </summary>
    public bool IsWaitingOnOperation { get; }

    /// <summary>
    /// Get the operations the state is waiting for to to be usable.
    /// </summary>
    /// <returns>The operations needed to wait for.</returns>
    public IReadOnlyList<ICombatOperation> GetWaitingForOperations();

    /// <summary>
    /// Gets the current operation of the given queue type.
    /// </summary>
    /// <param name="queueType">The queue type to get the current operation of.</param>
    /// <returns>The operation of the given queue, if any.</returns>
    public ICombatOperation? GetCurrentOperation(OperationQueueType queueType);

    /// <summary>
    /// Checks whether an operation is being executed in the given queue.
    /// </summary>
    /// <remarks>
    /// If not, either waiting for the operation or there is no operation enqueued.
    /// </remarks>
    /// <param name="queueType">The type of queue to look at.</param>
    /// <param name="operation">The operation currently being executed.</param>
    /// <returns>Whether an operation is being executed.</returns>
    public bool IsExecutingOperation(OperationQueueType queueType, [NotNullWhen(true)] out ICombatOperation? operation);

    /// <summary>
    /// Cancel the combat technique, quit the combat state.
    /// </summary>
    public void QuitCombat();

A Extensions/NosSmooth.Extensions.Combat/OperationQueueType.cs => Extensions/NosSmooth.Extensions.Combat/OperationQueueType.cs +73 -0
@@ 0,0 1,73 @@
//
//  OperationQueueType.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.Combat;

/// <summary>
/// A type classifying operations into multiple sequential queues.
/// </summary>
public enum OperationQueueType
{
    /// <summary>
    /// A total control of controls is needed (walking, sitting - recovering, using a skill).
    /// </summary>
    TotalControl,

    /// <summary>
    /// An item is being used.
    /// </summary>
    Item,

    /// <summary>
    /// Any operation that should be executed sequentially with other <see cref="Reserved1"/> operations.
    /// </summary>
    Reserved1,

    /// <summary>
    /// Any operation that should be executed sequentially with other <see cref="Reserved2"/> operations.
    /// </summary>
    Reserved2,

    /// <summary>
    /// Any operation that should be executed sequentially with other <see cref="Reserved3"/> operations.
    /// </summary>
    Reserved3,

    /// <summary>
    /// Any operation that should be executed sequentially with other <see cref="Reserved4"/> operations.
    /// </summary>
    Reserved4,

    /// <summary>
    /// Any operation that should be executed sequentially with other <see cref="Reserved5"/> operations.
    /// </summary>
    Reserved5,

    /// <summary>
    /// Any operation that should be executed sequentially with other <see cref="Reserved6"/> operations.
    /// </summary>
    Reserved6,

    /// <summary>
    /// Any operation that should be executed sequentially with other <see cref="Reserved7"/> operations.
    /// </summary>
    Reserved7,

    /// <summary>
    /// Any operation that should be executed sequentially with other <see cref="Reserved8"/> operations.
    /// </summary>
    Reserved8,

    /// <summary>
    /// Any operation that should be executed sequentially with other <see cref="Reserved9"/> operations.
    /// </summary>
    Reserved9,

    /// <summary>
    /// Any operation that should be executed sequentially with other <see cref="Reserved10"/> operations.
    /// </summary>
    Reserved10,
}
\ No newline at end of file

M Extensions/NosSmooth.Extensions.Combat/Operations/ICombatOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/ICombatOperation.cs +47 -11
@@ 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.Core.Contracts;
using NosSmooth.Extensions.Combat.Techniques;
using Remora.Results;



@@ 12,27 13,62 @@ namespace NosSmooth.Extensions.Combat.Operations;
/// <summary>
/// A combat operation used in <see cref="ICombatTechnique"/> that can be used as one step.
/// </summary>
public interface ICombatOperation
public interface ICombatOperation : IDisposable
{
    // 1. wait for CanBeUsed
    // 2. use OnlyExecute
    // 3. periodically check IsFinished
    // 4. Finished
    // 5. Call Dispose
    // 6. Go to next operation in queue
    // go to step 1

    /// <summary>
    /// Checks whether the operation can currently be used.
    /// Gets the queue type the operation belongs to.
    /// </summary>
    /// <remarks>
    /// Ie. if the operation is to use a skill, it will return true only if the skill is not on a cooldown,
    /// the character has enough mana and is not stunned.
    /// Used for distinguishing what operations may run simultaneously.
    /// For example items may be used simultaneous to attacking. Attacking
    /// may not be simultaneous to walking.
    /// </remarks>
    public OperationQueueType QueueType { get; }

    /// <summary>
    /// Begin the execution without waiting for the finished state.
    /// </summary>
    /// <param name="combatState">The combat state.</param>
    /// <returns>Whether the operation can be used right away.</returns>
    public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState);
    /// <param name="ct">The cancellation token used for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> BeginExecution(ICombatState combatState, CancellationToken ct = default);

    /// <summary>
    /// Asynchronously wait for finished state.
    /// </summary>
    /// <param name="combatState">The combat state.</param>
    /// <param name="ct">The cancellation token used for cancelling the operation.</param>
    /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
    public Task<Result> WaitForFinishedAsync(ICombatState combatState, CancellationToken ct = default);

    /// <summary>
    /// Checks whether the operation is currently being executed.
    /// </summary>
    /// <returns>Whether the operation is being executed.</returns>
    public bool IsExecuting();

    /// <summary>
    /// Checks whether the operation is done.
    /// </summary>
    /// <returns>Whether the operation is finished.</returns>
    public bool IsFinished();

    /// <summary>
    /// Use the operation, if possible.
    /// Checks whether the operation can currently be used.
    /// </summary>
    /// <remarks>
    /// Should block until the operation is finished.
    /// Ie. if the operation is to use a skill, it will return true only if the skill is not on a cooldown,
    /// the character has enough mana and is not stunned.
    /// </remarks>
    /// <param name="combatState">The combat state.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not succeed.</returns>
    public Task<Result> UseAsync(ICombatState combatState, CancellationToken ct = default);
    /// <returns>Whether the operation can be used right away.</returns>
    public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState);
}
\ No newline at end of file

M Extensions/NosSmooth.Extensions.Combat/Operations/UseItemOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/UseItemOperation.cs +54 -6
@@ 4,7 4,12 @@
//  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.Items;
using System.Diagnostics;
using NosSmooth.Data.Abstractions.Enums;
using NosSmooth.Extensions.Combat.Selectors;
using NosSmooth.Game.Data.Inventory;
using NosSmooth.Game.Extensions;
using NosSmooth.Packets.Client.Inventory;
using Remora.Results;

namespace NosSmooth.Extensions.Combat.Operations;


@@ 13,17 18,60 @@ namespace NosSmooth.Extensions.Combat.Operations;
/// A combat operation to use an item.
/// </summary>
/// <param name="Item">The item to use.</param>
public record UseItemOperation(Item Item) : ICombatOperation
public record UseItemOperation(InventoryItem Item) : ICombatOperation
{
    private Task<Result>? _useItemOperation;

    /// <inheritdoc />
    public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
    public OperationQueueType QueueType => OperationQueueType.Item;

    /// <inheritdoc />
    public Task<Result> BeginExecution(ICombatState combatState, CancellationToken ct = default)
    {
        if (_useItemOperation is not null)
        {
            return Task.FromResult(Result.FromSuccess());
        }

        _useItemOperation = Task.Run(
            () => combatState.Client.SendPacketAsync(new UseItemPacket(Item.Bag.Convert(), Item.Item.Slot), ct),
            ct
        );
        return Task.FromResult(Result.FromSuccess());
    }

    /// <inheritdoc />
    public Task<Result> WaitForFinishedAsync(ICombatState combatState, CancellationToken ct = default)
    {
        throw new NotImplementedException();
        if (IsFinished())
        {
            return Task.FromResult(Result.FromSuccess());
        }

        BeginExecution(combatState, ct);
        if (_useItemOperation is null)
        {
            throw new UnreachableException();
        }

        return _useItemOperation;
    }

    /// <inheritdoc />
    public Task<Result> UseAsync(ICombatState combatState, CancellationToken ct = default)
    public bool IsExecuting()
        => _useItemOperation is not null && !IsFinished();

    /// <inheritdoc />
    public bool IsFinished()
        => _useItemOperation?.IsCompleted ?? false;

    /// <inheritdoc />
    public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
        => CanBeUsedResponse.CanBeUsed;

    /// <inheritdoc />
    public void Dispose()
    {
        throw new NotImplementedException();
        _useItemOperation?.Dispose();
    }
}
\ No newline at end of file

D Extensions/NosSmooth.Extensions.Combat/Operations/UsePrimarySkillOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/UsePrimarySkillOperation.cs +0 -77
@@ 1,77 0,0 @@
//
//  UsePrimarySkillOperation.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.Extensions.Combat.Errors;
using NosSmooth.Game.Data.Characters;
using NosSmooth.Game.Data.Entities;
using NosSmooth.Packets.Client.Battle;
using Remora.Results;

namespace NosSmooth.Extensions.Combat.Operations;

/// <summary>
/// An operation that uses the primary skill of the character.
/// </summary>
public record UsePrimarySkillOperation(ILivingEntity Target) : ICombatOperation
{
    private UseSkillOperation? _useSkillOperation;

    /// <inheritdoc />
    public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
    {
        if (_useSkillOperation is null)
        {
            var primarySkillResult = GetPrimarySkill(combatState);
            if (!primarySkillResult.IsDefined(out var primarySkill))
            {
                return Result<CanBeUsedResponse>.FromError(primarySkillResult);
            }

            if (combatState.Game.Character is null)
            {
                return new CharacterNotInitializedError();
            }

            _useSkillOperation = new UseSkillOperation(primarySkill, combatState.Game.Character, Target);
        }

        return _useSkillOperation.CanBeUsed(combatState);
    }

    /// <inheritdoc />
    public async Task<Result> UseAsync(ICombatState combatState, CancellationToken ct)
    {
        if (_useSkillOperation is null)
        {
            var primarySkillResult = GetPrimarySkill(combatState);
            if (!primarySkillResult.IsDefined(out var primarySkill))
            {
                return Result.FromError(primarySkillResult);
            }

            if (combatState.Game.Character is null)
            {
                return new CharacterNotInitializedError();
            }

            _useSkillOperation = new UseSkillOperation(primarySkill, combatState.Game.Character, Target);
        }

        return await _useSkillOperation.UseAsync(combatState, ct);
    }

    private Result<Skill> GetPrimarySkill(ICombatState combatState)
    {
        var skills = combatState.Game.Skills;

        if (skills is null)
        {
            return new CharacterNotInitializedError("Skills");
        }

        return skills.PrimarySkill;
    }
}
\ No newline at end of file

M Extensions/NosSmooth.Extensions.Combat/Operations/UseSkillOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/UseSkillOperation.cs +75 -61
@@ 6,11 6,16 @@

using System.Diagnostics;
using System.Xml.XPath;
using NosSmooth.Core.Contracts;
using NosSmooth.Data.Abstractions.Enums;
using NosSmooth.Data.Abstractions.Infos;
using NosSmooth.Extensions.Combat.Errors;
using NosSmooth.Game.Apis.Safe;
using NosSmooth.Game.Contracts;
using NosSmooth.Game.Data.Characters;
using NosSmooth.Game.Data.Entities;
using NosSmooth.Game.Errors;
using NosSmooth.Game.Events.Battle;
using NosSmooth.Packets;
using NosSmooth.Packets.Client.Battle;
using Remora.Results;


@@ 20,109 25,118 @@ namespace NosSmooth.Extensions.Combat.Operations;
/// <summary>
/// A combat operation to use a skill.
/// </summary>
/// <param name="SkillsApi">The skills api used for executing the skills.</param>
/// <param name="Skill">The skill to use.</param>
/// <param name="Caster">The caster entity that is using the skill.</param>
/// <param name="Target">The target entity to use the skill at.</param>
public record UseSkillOperation(Skill Skill, ILivingEntity Caster, ILivingEntity Target) : ICombatOperation
public record UseSkillOperation(NostaleSkillsApi SkillsApi, Skill Skill, ILivingEntity Caster, ILivingEntity Target) : ICombatOperation
{
    private IContract<SkillUsedEvent, UseSkillStates>? _contract;

    /// <inheritdoc />
    public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
    public OperationQueueType QueueType => OperationQueueType.TotalControl;

    /// <inheritdoc />
    public async Task<Result> BeginExecution(ICombatState combatState, CancellationToken ct = default)
    {
        if (_contract is not null)
        {
            return Result.FromSuccess();
        }

        if (Skill.Info is null)
        {
            return new MissingInfoError("skill", Skill.SkillVNum);
        }

        var character = combatState.Game.Character;
        if (character is not null && character.Mp is not null && character.Mp.Amount is not null)
        if (Target.Position is null)
        {
            if (character.Mp.Amount < Skill.Info.MpCost)
            { // The character is in combat, mp won't restore.
                return CanBeUsedResponse.WontBeUsable;
            }
            return new NotInitializedError("target's position");
        }

        return Skill.IsOnCooldown ? CanBeUsedResponse.MustWait : CanBeUsedResponse.CanBeUsed;
        var contractResult = ContractSkill(Skill.Info);
        if (!contractResult.IsDefined(out var contract))
        {
            return Result.FromError(contractResult);
        }

        _contract = contract;
        var executed = await _contract.OnlyExecuteAsync(ct);
        _contract.Register();

        return executed;
    }

    /// <inheritdoc />
    public async Task<Result> UseAsync(ICombatState combatState, CancellationToken ct = default)
    public async Task<Result> WaitForFinishedAsync(ICombatState combatState, CancellationToken ct = default)
    {
        if (Skill.Info is null)
        var result = await BeginExecution(combatState, ct);
        if (!result.IsSuccess)
        {
            return new MissingInfoError("skill", Skill.SkillVNum);
            return result;
        }

        if (_contract is null)
        {
            throw new UnreachableException();
        }

        using var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(ct);
        await combatState.CombatManager.RegisterSkillCancellationTokenAsync(linkedSource, ct);
        var sendResponse = await combatState.Client.SendPacketAsync
        (
            CreateSkillUsePacket(Skill.Info),
            ct
        );
        var waitResult = await _contract.WaitForAsync(UseSkillStates.CharacterRestored, ct: ct);
        return waitResult.IsSuccess ? Result.FromSuccess() : Result.FromError(waitResult);
    }

    /// <inheritdoc />
    public bool IsExecuting()
        => _contract is not null && _contract.CurrentState > UseSkillStates.None && !IsFinished();

    /// <inheritdoc />
    public bool IsFinished()
        => _contract?.CurrentState == UseSkillStates.CharacterRestored;

        if (!sendResponse.IsSuccess)
    /// <inheritdoc />
    public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
    {
        if (Skill.Info is null)
        {
            await combatState.CombatManager.UnregisterSkillCancellationTokenAsync(linkedSource, ct);
            return sendResponse;
            return new MissingInfoError("skill", Skill.SkillVNum);
        }

        try
        var character = combatState.Game.Character;
        if (Target.Hp is not null && Target.Hp.Amount is not null && Target.Hp.Amount == 0)
        {
            // wait 10 times the cast delay in case su is not received.
            await Task.Delay(Skill.Info.CastTime * 1000, linkedSource.Token);
            return CanBeUsedResponse.WontBeUsable;
        }
        catch (TaskCanceledException)

        if (character is not null && character.Mp is not null && character.Mp.Amount is not null)
        {
            // ignored
            if (character.Mp.Amount < Skill.Info.MpCost)
            { // The character is in combat, mp won't restore.
                return CanBeUsedResponse.WontBeUsable;
            }
        }
        await combatState.CombatManager.UnregisterSkillCancellationTokenAsync(linkedSource, ct);
        await Task.Delay(1000, ct);

        return Result.FromSuccess();
        return Skill.IsOnCooldown ? CanBeUsedResponse.MustWait : CanBeUsedResponse.CanBeUsed;
    }

    private IPacket CreateSkillUsePacket(ISkillInfo info)
    private Result<IContract<SkillUsedEvent, UseSkillStates>> ContractSkill(ISkillInfo info)
    {
        switch (info.TargetType)
        {
            case TargetType.SelfOrTarget: // a buff?
            case TargetType.Self:
                return CreateSelfTargetedSkillPacket(info);
                return SkillsApi.ContractUseSkillOnCharacter(Skill);
            case TargetType.NoTarget: // area skill?
                return CreateAreaSkillPacket(info);
                return SkillsApi.ContractUseSkillAt(Skill, Target.Position!.Value.X, Target.Position.Value.Y);
            case TargetType.Target:
                return CreateTargetedSkillPacket(info);
                return SkillsApi.ContractUseSkillOn(Skill, Target);
        }

        throw new UnreachableException();
    }

    private IPacket CreateAreaSkillPacket(ISkillInfo info)
        => new UseAOESkillPacket
        (
            info.CastId,
            Target.Position!.Value.X,
            Target.Position.Value.Y
        );

    private IPacket CreateTargetedSkillPacket(ISkillInfo info)
        => new UseSkillPacket
        (
            info.CastId,
            Target.Type,
            Target.Id,
            null,
            null
        );

    private IPacket CreateSelfTargetedSkillPacket(ISkillInfo info)
        => new UseSkillPacket
        (
            info.CastId,
            Caster.Type,
            Caster.Id,
            null,
            null
        );
    /// <inheritdoc />
    public void Dispose()
    {
        _contract?.Unregister();
    }
}
\ No newline at end of file

M Extensions/NosSmooth.Extensions.Combat/Operations/WalkInRangeOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/WalkInRangeOperation.cs +57 -3
@@ 4,12 4,14 @@
//  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;
using NosSmooth.Extensions.Combat.Errors;
using NosSmooth.Extensions.Pathfinding;
using NosSmooth.Extensions.Pathfinding.Errors;
using NosSmooth.Game.Data.Entities;
using NosSmooth.Game.Data.Info;
using NosSmooth.Game.Errors;
using NosSmooth.Packets.Client.Inventory;
using Remora.Results;

namespace NosSmooth.Extensions.Combat.Operations;


@@ 27,6 29,52 @@ public record WalkInRangeOperation
    float Distance
) : ICombatOperation
{
    private Task<Result>? _walkInRangeOperation;

    /// <inheritdoc />
    public OperationQueueType QueueType => OperationQueueType.TotalControl;

    /// <inheritdoc />
    public Task<Result> BeginExecution(ICombatState combatState, CancellationToken ct = default)
    {
        if (_walkInRangeOperation is not null)
        {
            return Task.FromResult(Result.FromSuccess());
        }

        _walkInRangeOperation = Task.Run
        (
            () => UseAsync(combatState, ct),
            ct
        );
        return Task.FromResult(Result.FromSuccess());
    }

    /// <inheritdoc />
    public async Task<Result> WaitForFinishedAsync(ICombatState combatState, CancellationToken ct = default)
    {
        if (IsFinished())
        {
            return Result.FromSuccess();
        }

        await BeginExecution(combatState, ct);
        if (_walkInRangeOperation is null)
        {
            throw new UnreachableException();
        }

        return await _walkInRangeOperation;
    }

    /// <inheritdoc />
    public bool IsExecuting()
        => _walkInRangeOperation is not null && !IsFinished();

    /// <inheritdoc />
    public bool IsFinished()
        => _walkInRangeOperation?.IsCompleted ?? false;

    /// <inheritdoc />
    public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
    {


@@ 39,8 87,7 @@ public record WalkInRangeOperation
        return character.CantMove ? CanBeUsedResponse.MustWait : CanBeUsedResponse.CanBeUsed;
    }

    /// <inheritdoc />
    public async Task<Result> UseAsync(ICombatState combatState, CancellationToken ct = default)
    private async Task<Result> UseAsync(ICombatState combatState, CancellationToken ct = default)
    {
        var character = combatState.Game.Character;
        if (character is null)


@@ 75,7 122,8 @@ public record WalkInRangeOperation
            }

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

            while (!walkResultTask.IsCompleted)
            {


@@ 129,4 177,10 @@ public record WalkInRangeOperation
        var diffLength = Math.Sqrt(diff.DistanceSquared(Position.Zero));
        return target + ((distance / diffLength) * diff);
    }

    /// <inheritdoc />
    public void Dispose()
    {
        _walkInRangeOperation?.Dispose();
    }
}
\ No newline at end of file

M Extensions/NosSmooth.Extensions.Combat/Operations/WalkOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/WalkOperation.cs +54 -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 System.Diagnostics;
using NosSmooth.Extensions.Combat.Errors;
using NosSmooth.Extensions.Pathfinding;
using Remora.Results;


@@ 18,6 19,52 @@ namespace NosSmooth.Extensions.Combat.Operations;
/// <param name="Y">The y coordinate to walk to.</param>
public record WalkOperation(WalkManager WalkManager, short X, short Y) : ICombatOperation
{
    private Task<Result>? _walkOperation;

    /// <inheritdoc />
    public OperationQueueType QueueType => OperationQueueType.TotalControl;

    /// <inheritdoc />
    public Task<Result> BeginExecution(ICombatState combatState, CancellationToken ct = default)
    {
        if (_walkOperation is not null)
        {
            return Task.FromResult(Result.FromSuccess());
        }

        _walkOperation = Task.Run
        (
            () => UseAsync(combatState, ct),
            ct
        );
        return Task.FromResult(Result.FromSuccess());
    }

    /// <inheritdoc />
    public async Task<Result> WaitForFinishedAsync(ICombatState combatState, CancellationToken ct = default)
    {
        if (IsFinished())
        {
            return Result.FromSuccess();
        }

        await BeginExecution(combatState, ct);
        if (_walkOperation is null)
        {
            throw new UnreachableException();
        }

        return await _walkOperation;
    }

    /// <inheritdoc />
    public bool IsExecuting()
        => _walkOperation is not null && !IsFinished();

    /// <inheritdoc />
    public bool IsFinished()
        => _walkOperation?.IsCompleted ?? false;

    /// <inheritdoc />
    public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
    {


@@ 30,7 77,12 @@ public record WalkOperation(WalkManager WalkManager, short X, short Y) : ICombat
        return character.CantMove ? CanBeUsedResponse.MustWait : CanBeUsedResponse.CanBeUsed;
    }

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

    /// <inheritdoc />
    public void Dispose()
    {
        _walkOperation?.Dispose();
    }
}
\ No newline at end of file

M Extensions/NosSmooth.Extensions.Combat/Policies/EnemyPolicy.cs => Extensions/NosSmooth.Extensions.Combat/Policies/EnemyPolicy.cs +1 -0
@@ 35,6 35,7 @@ public record EnemyPolicy
        {
            targets = targets.Where(x => MonsterVNums.Contains(x.VNum));
        }
        targets = targets.ToArray();

        if (!targets.Any())
        {

M Extensions/NosSmooth.Extensions.Combat/Policies/UseItemPolicy.cs => Extensions/NosSmooth.Extensions.Combat/Policies/UseItemPolicy.cs +7 -5
@@ 30,7 30,7 @@ public record UseItemPolicy
) : IItemSelector
{
    /// <inheritdoc />
    public Result<Item> GetSelectedItem(ICombatState combatState, ICollection<Item> possibleItems)
    public Result<InventoryItem> GetSelectedItem(ICombatState combatState, ICollection<InventoryItem> possibleItems)
    {
        var character = combatState.Game.Character;
        if (character is null)


@@ 40,19 40,21 @@ public record UseItemPolicy

        if (ShouldUseHpItem(character))
        {
            var item = possibleItems.FirstOrDefault(x => UseHealthItemsVNums.Contains(x.ItemVNum));
            var item = possibleItems.Cast<InventoryItem?>().FirstOrDefault
                (x => UseHealthItemsVNums.Contains(x?.Item.Item?.ItemVNum ?? -1));
            if (item is not null)
            {
                return item;
                return item.Value;
            }
        }

        if (ShouldUseMpItem(character))
        {
            var item = possibleItems.FirstOrDefault(x => UseManaItemsVNums.Contains(x.ItemVNum));
            var item = possibleItems.Cast<InventoryItem?>().FirstOrDefault
                (x => UseManaItemsVNums.Contains(x?.Item.Item?.ItemVNum ?? -1));
            if (item is not null)
            {
                return item;
                return item.Value;
            }
        }


D Extensions/NosSmooth.Extensions.Combat/Responders/CancelResponder.cs => Extensions/NosSmooth.Extensions.Combat/Responders/CancelResponder.cs +0 -35
@@ 1,35 0,0 @@
//
//  CancelResponder.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.Server.Skills;
using Remora.Results;

namespace NosSmooth.Extensions.Combat.Responders;

/// <summary>
/// Responds to cancel packet.
/// </summary>
public class CancelResponder : IPacketResponder<CancelPacket>
{
    private readonly CombatManager _combatManager;

    /// <summary>
    /// Initializes a new instance of the <see cref="CancelResponder"/> class.
    /// </summary>
    /// <param name="combatManager">The combat manager.</param>
    public CancelResponder(CombatManager combatManager)
    {
        _combatManager = combatManager;
    }

    /// <inheritdoc />
    public Task<Result> Respond(PacketEventArgs<CancelPacket> packetArgs, CancellationToken ct = default)
    {
        Task.Run(() => _combatManager.CancelSkillTokensAsync(default));
        return Task.FromResult(Result.FromSuccess());
    }
}
\ No newline at end of file

D Extensions/NosSmooth.Extensions.Combat/Responders/SkillUseResponder.cs => Extensions/NosSmooth.Extensions.Combat/Responders/SkillUseResponder.cs +0 -52
@@ 1,52 0,0 @@
//
//  SkillUseResponder.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;
using NosSmooth.Packets.Server.Battle;
using Remora.Results;

namespace NosSmooth.Extensions.Combat.Responders;

/// <summary>
/// Responds to su packet.
/// </summary>
public class SuResponder : IPacketResponder<SuPacket>, IPacketResponder<BsPacket>
{
    private readonly CombatManager _combatManager;
    private readonly Game.Game _game;

    /// <summary>
    /// Initializes a new instance of the <see cref="SuResponder"/> class.
    /// </summary>
    /// <param name="combatManager">The combat manager.</param>
    /// <param name="game">The game.</param>
    public SuResponder(CombatManager combatManager, Game.Game game)
    {
        _combatManager = combatManager;
        _game = game;
    }

    /// <inheritdoc />
    public Task<Result> Respond(PacketEventArgs<SuPacket> packetArgs, CancellationToken ct = default)
    {
        if (packetArgs.Packet.CasterEntityId == _game.Character?.Id)
        {
            Task.Run(() => _combatManager.CancelSkillTokensAsync(default));
        }
        return Task.FromResult(Result.FromSuccess());
    }

    /// <inheritdoc />
    public Task<Result> Respond(PacketEventArgs<BsPacket> packetArgs, CancellationToken ct = default)
    {
        if (packetArgs.Packet.CasterEntityId == _game.Character?.Id)
        {
            Task.Run(() => _combatManager.CancelSkillTokensAsync(default));
        }
        return Task.FromResult(Result.FromSuccess());
    }
}
\ No newline at end of file

M Extensions/NosSmooth.Extensions.Combat/Selectors/IItemSelector.cs => Extensions/NosSmooth.Extensions.Combat/Selectors/IItemSelector.cs +1 -1
@@ 20,7 20,7 @@ public interface IItemSelector
    /// <param name="combatState">The combat state.</param>
    /// <param name="possibleItems">The items that may be used.</param>
    /// <returns>The selected item, or an error.</returns>
    public Result<Item> GetSelectedItem(ICombatState combatState, ICollection<Item> possibleItems);
    public Result<InventoryItem> GetSelectedItem(ICombatState combatState, ICollection<InventoryItem> possibleItems);

    /// <summary>
    /// Gets whether currently an item should be used.

A Extensions/NosSmooth.Extensions.Combat/Selectors/InventoryItem.cs => Extensions/NosSmooth.Extensions.Combat/Selectors/InventoryItem.cs +14 -0
@@ 0,0 1,14 @@
//
//  InventoryItem.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.Data.Abstractions.Enums;
using NosSmooth.Game.Data.Inventory;

namespace NosSmooth.Extensions.Combat.Selectors;

[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 Extensions/NosSmooth.Extensions.Combat/Techniques/ICombatTechnique.cs => Extensions/NosSmooth.Extensions.Combat/Techniques/ICombatTechnique.cs +23 -4
@@ 18,6 18,15 @@ namespace NosSmooth.Extensions.Combat.Techniques;
public interface ICombatTechnique
{
    /// <summary>
    /// Gets the types this technique may handle.
    /// </summary>
    /// <remarks>
    /// <see cref="HandleNextCombatStep"/> will be called only for queue types
    /// from this collection.
    /// </remarks>
    public IReadOnlyList<OperationQueueType> HandlingQueueTypes { get; }

    /// <summary>
    /// Should check whether the technique should process more steps or quit the combat.
    /// </summary>
    /// <param name="state">The combat state.</param>


@@ 26,23 35,33 @@ public interface ICombatTechnique

    /// <summary>
    /// Handle one step that should enqueue an operation.
    /// Enqueue only operation of the given queue type.
    /// </summary>
    /// <remarks>
    /// If error is returned, the combat will be cancelled.
    /// </remarks>
    /// <param name="queueType">The type of the operation to enqueue.</param>
    /// <param name="state">The combat state.</param>
    /// <returns>An id of the current target entity or an error.</returns>
    public Result<long?> HandleCombatStep(ICombatState state);
    public Result<long?> HandleNextCombatStep(OperationQueueType queueType, ICombatState state);

    /// <summary>
    /// Handle waiting for an operation.
    /// </summary>
    /// <param name="queueType">The type of the operation.</param>
    /// <param name="state">The combat state.</param>
    /// <param name="operation">The operation that needs waiting.</param>
    /// <returns>A result that may or may not have succeeded. In case of an error, <see cref="HandleError"/> will be called with the error.</returns>
    public Result HandleWaiting(OperationQueueType queueType, ICombatState state, ICombatOperation operation);

    /// <summary>
    /// Handles an error from <see cref="ICombatOperation.UseAsync"/>.
    /// Handles an arbitrary error.
    /// </summary>
    /// <remarks>
    /// If an error is returned, the combat will be cancelled.
    /// </remarks>
    /// <param name="state">The combat state.</param>
    /// <param name="operation">The combat operation that returned an error.</param>
    /// <param name="result">The errorful result.</param>
    /// <returns>A result that may or may not succeed.</returns>
    public Result HandleError(ICombatState state, ICombatOperation operation, Result result);
    public Result HandleError(ICombatState state, Result result);
}
\ No newline at end of file

M Extensions/NosSmooth.Extensions.Combat/Techniques/SimpleAttackTechnique.cs => Extensions/NosSmooth.Extensions.Combat/Techniques/SimpleAttackTechnique.cs +83 -7
@@ 4,14 4,17 @@
//  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;
using NosSmooth.Data.Abstractions.Enums;
using NosSmooth.Extensions.Combat.Errors;
using NosSmooth.Extensions.Combat.Extensions;
using NosSmooth.Extensions.Combat.Operations;
using NosSmooth.Extensions.Combat.Selectors;
using NosSmooth.Extensions.Pathfinding;
using NosSmooth.Game.Apis.Safe;
using NosSmooth.Game.Data.Characters;
using NosSmooth.Game.Data.Entities;
using NosSmooth.Game.Data.Inventory;
using Remora.Results;

namespace NosSmooth.Extensions.Combat.Techniques;


@@ 21,9 24,16 @@ namespace NosSmooth.Extensions.Combat.Techniques;
/// </summary>
public class SimpleAttackTechnique : ICombatTechnique
{
    private static OperationQueueType[] _handlingTypes = new[]
    {
        OperationQueueType.Item, OperationQueueType.TotalControl
    };

    private readonly long _targetId;
    private readonly NostaleSkillsApi _skillsApi;
    private readonly WalkManager _walkManager;
    private readonly ISkillSelector _skillSelector;
    private readonly IItemSelector _itemSelector;

    private Skill? _currentSkill;
    private ILivingEntity? _target;


@@ 32,21 42,30 @@ public class SimpleAttackTechnique : ICombatTechnique
    /// Initializes a new instance of the <see cref="SimpleAttackTechnique"/> class.
    /// </summary>
    /// <param name="targetId">The target entity id.</param>
    /// <param name="skillsApi">The skills api.</param>
    /// <param name="walkManager">The walk manager.</param>
    /// <param name="skillSelector">The skill selector.</param>
    /// <param name="itemSelector">The item selector.</param>
    public SimpleAttackTechnique
    (
        long targetId,
        NostaleSkillsApi skillsApi,
        WalkManager walkManager,
        ISkillSelector skillSelector
        ISkillSelector skillSelector,
        IItemSelector itemSelector
    )
    {
        _targetId = targetId;
        _skillsApi = skillsApi;
        _walkManager = walkManager;
        _skillSelector = skillSelector;
        _itemSelector = itemSelector;
    }

    /// <inheritdoc />
    public IReadOnlyList<OperationQueueType> HandlingQueueTypes => _handlingTypes;

    /// <inheritdoc />
    public bool ShouldContinue(ICombatState state)
    {
        var map = state.Game.CurrentMap;


@@ 60,7 79,18 @@ public class SimpleAttackTechnique : ICombatTechnique
    }

    /// <inheritdoc />
    public Result<long?> HandleCombatStep(ICombatState state)
    public Result HandleWaiting(OperationQueueType queueType, ICombatState state, ICombatOperation operation)
    { // does not do anything, just wait.
        return Result.FromSuccess();
    }

    /// <inheritdoc />
    public Result HandleError(ICombatState state, Result result)
    { // no handling of errors is done
        return result;
    }

    private Result<long?> HandleTotalControl(ICombatState state)
    {
        var map = state.Game.CurrentMap;
        if (map is null)


@@ 94,8 124,7 @@ public class SimpleAttackTechnique : ICombatTechnique
            }

            var characterMp = character.Mp?.Amount ?? 0;
            var usableSkills = new[] { skills.PrimarySkill, skills.SecondarySkill }
                .Concat(skills.OtherSkills)
            var usableSkills = skills.OtherSkills
                .Where(x => x.Info is not null && x.Info.HitType != HitType.AlliesInZone)
                .Where(x => !x.IsOnCooldown && characterMp >= (x.Info?.MpCost ?? long.MaxValue));



@@ 142,16 171,63 @@ public class SimpleAttackTechnique : ICombatTechnique
        }
        else
        {
            state.UseSkill(_currentSkill, _target);
            state.UseSkill(_skillsApi, _currentSkill, _target);
            _currentSkill = null;
        }

        return _target.Id;
    }

    private Result<long?> HandleItem(ICombatState state)
    {
        var shouldUseItemResult = _itemSelector.ShouldUseItem(state);
        if (!shouldUseItemResult.IsDefined(out var shouldUseItem))
        {
            return Result<long?>.FromError(shouldUseItemResult);
        }

        if (!shouldUseItem)
        {
            return _targetId;
        }

        var inventory = state.Game.Inventory;
        if (inventory is null)
        {
            return _targetId;
        }

        var main = inventory.GetBag(BagType.Main)
            .Where(x => x is { Amount: > 0, Item.Info.Type: ItemType.Potion })
            .Select(x => new InventoryItem(BagType.Main, x));
        var etc = inventory.GetBag(BagType.Etc)
            .Where(x => x is { Amount: > 0, Item.Info.Type: ItemType.Food or ItemType.Snack })
            .Select(x => new InventoryItem(BagType.Etc, x));

        var possibleItems = main.Concat(etc).ToList();

        var itemResult = _itemSelector.GetSelectedItem(state, possibleItems);

        if (!itemResult.IsDefined(out var item))
        {
            return Result<long?>.FromError(itemResult);
        }

        state.UseItem(item);
        return _targetId;
    }

    /// <inheritdoc />
    public Result HandleError(ICombatState state, ICombatOperation operation, Result result)
    public Result<long?> HandleNextCombatStep(OperationQueueType queueType, ICombatState state)
    {
        return result;
        switch (queueType)
        {
            case OperationQueueType.Item:
                return HandleItem(state);
            case OperationQueueType.TotalControl:
                return HandleTotalControl(state);
        }

        throw new InvalidOperationException("SimpleAttackTechnique supports only Item and TotalControl queue types.");
    }
}
\ No newline at end of file

Do not follow this link