M Core/NosSmooth.Core/Contracts/ContractBuilder.cs => Core/NosSmooth.Core/Contracts/ContractBuilder.cs +16 -3
@@ 29,7 29,7 @@ public class ContractBuilder<TData, TState, TError>
private readonly TState _defaultState;
private readonly Dictionary<TState, DefaultContract<TData, TState, TError>.StateActionAsync> _actions;
- private readonly Dictionary<TState, (TimeSpan, TState)> _timeouts;
+ private readonly Dictionary<TState, (TimeSpan, TState?, TError?)> _timeouts;
private TState? _fillAtState;
private DefaultContract<TData, TState, TError>.FillDataAsync? _fillData;
@@ 44,7 44,7 @@ public class ContractBuilder<TData, TState, TError>
_contractor = contractor;
_defaultState = defaultState;
_actions = new Dictionary<TState, DefaultContract<TData, TState, TError>.StateActionAsync>();
- _timeouts = new Dictionary<TState, (TimeSpan, TState)>();
+ _timeouts = new Dictionary<TState, (TimeSpan, TState?, TError?)>();
}
/// <summary>
@@ 56,7 56,20 @@ public class ContractBuilder<TData, TState, TError>
/// <returns>The updated builder.</returns>
public ContractBuilder<TData, TState, TError> SetTimeout(TState state, TimeSpan timeout, TState nextState)
{
- _timeouts[state] = (timeout, nextState);
+ _timeouts[state] = (timeout, nextState, null);
+ return this;
+ }
+
+ /// <summary>
+ /// Sets timeout of the given state.
+ /// </summary>
+ /// <param name="state">The state to set timeout for.</param>
+ /// <param name="timeout">The timeout span.</param>
+ /// <param name="error">The error to set.</param>
+ /// <returns>The updated builder.</returns>
+ public ContractBuilder<TData, TState, TError> SetTimeout(TState state, TimeSpan timeout, TError error)
+ {
+ _timeouts[state] = (timeout, null, error);
return this;
}
M Core/NosSmooth.Core/Contracts/Contractor.cs => Core/NosSmooth.Core/Contracts/Contractor.cs +2 -1
@@ 114,6 114,7 @@ public class Contractor : IEnumerable<IContract>
$"A contract {info.contract} has been registered for too long and was unregistered automatically."
)
);
+ toRemove.Add(info);
continue;
}
@@ 148,7 149,7 @@ public class Contractor : IEnumerable<IContract>
return errors.Count switch
{
0 => Result.FromSuccess(),
- 1 => (Result)errors[0],
+ 1 => Result.FromError((Result<ContractUpdateResponse>)errors[0]),
_ => new AggregateError(errors)
};
}
M Core/NosSmooth.Core/Contracts/DefaultContract.cs => Core/NosSmooth.Core/Contracts/DefaultContract.cs +40 -7
@@ 44,7 44,7 @@ public class DefaultContract<TData, TState, TError> : IContract<TData, TState>
private readonly SemaphoreSlim _semaphore;
- private readonly IDictionary<TState, (TimeSpan, TState)> _timeouts;
+ private readonly IDictionary<TState, (TimeSpan, TState?, TError?)> _timeouts;
private readonly IDictionary<TState, StateActionAsync> _actions;
private readonly Contractor _contractor;
private readonly TState _defaultState;
@@ 75,7 75,7 @@ public class DefaultContract<TData, TState, TError> : IContract<TData, TState>
TState fillAtState,
FillDataAsync fillData,
IDictionary<TState, StateActionAsync> actions,
- IDictionary<TState, (TimeSpan Timeout, TState NextState)> timeouts
+ IDictionary<TState, (TimeSpan Timeout, TState? NextState, TError? Error)> timeouts
)
{
_semaphore = new SemaphoreSlim(1, 1);
@@ 139,8 139,16 @@ public class DefaultContract<TData, TState, TError> : IContract<TData, TState>
if (resultData.Error is not null)
{
_error = resultData.Error;
- _waitCancellationSource?.Cancel();
- return ContractUpdateResponse.Interested;
+ try
+ {
+ _waitCancellationSource?.Cancel();
+ }
+ catch
+ {
+ // ignored
+ }
+
+ return ContractUpdateResponse.InterestedAndUnregister;
}
if (resultData.NextState is null)
@@ 170,6 178,7 @@ public class DefaultContract<TData, TState, TError> : IContract<TData, TState>
{
_error = error;
_waitCancellationSource?.Cancel();
+ return new ContractError<TError>(error.Value);
}
if (state is not null)
@@ 199,6 208,16 @@ public class DefaultContract<TData, TState, TError> : IContract<TData, TState>
);
}
+ if (_error is not null)
+ {
+ return new ContractError<TError>(_error.Value);
+ }
+
+ if (_resultError is not null)
+ {
+ return Result<TData>.FromError(_resultError.Value);
+ }
+
if (CurrentState.CompareTo(state) >= 0)
{ // already reached.
return Data;
@@ 234,6 253,8 @@ public class DefaultContract<TData, TState, TError> : IContract<TData, TState>
{
Unregister();
}
+
+ _waitCancellationSource?.Dispose();
}
if (ct.IsCancellationRequested)
@@ 259,6 280,10 @@ public class DefaultContract<TData, TState, TError> : IContract<TData, TState>
return Data;
}
+ /// <inheritdoc />
+ public bool HasReachedState(TState state)
+ => _error is not null || _resultError is not null || CurrentState.CompareTo(state) >= 0;
+
private async Task<Result<ContractUpdateResponse>> SetupNewState<TAny>(TAny data, CancellationToken ct)
{
if (_fillAtState.CompareTo(CurrentState) == 0)
@@ 304,7 329,7 @@ public class DefaultContract<TData, TState, TError> : IContract<TData, TState>
if (_timeouts.ContainsKey(CurrentState))
{
var currentState = CurrentState;
- var (timeout, state) = _timeouts[CurrentState];
+ var (timeout, state, error) = _timeouts[CurrentState];
Task.Run
(
@@ 314,8 339,16 @@ public class DefaultContract<TData, TState, TError> : IContract<TData, TState>
if (CurrentState.CompareTo(currentState) == 0)
{
- await SetCurrentState(state);
- await SetupNewState<int?>(null!, default);
+ if (state is not null)
+ {
+ await SetCurrentState(state.Value);
+ await SetupNewState<int?>(null!, default);
+ }
+ else if (error is not null)
+ {
+ _error = error;
+ _waitCancellationSource?.Cancel();
+ }
}
}
);
M Core/NosSmooth.Core/Contracts/IContract.cs => Core/NosSmooth.Core/Contracts/IContract.cs +7 -0
@@ 116,4 116,11 @@ public interface IContract<TData, TState> : IContract
/// <returns>The data of the contract or an error.</returns>
/// <exception cref="InvalidOperationError">Thrown in case the given state cannot fill the data.</exception>
public Task<Result<TData>> WaitForAsync(TState state, bool unregisterAfter = true, CancellationToken ct = default);
+
+ /// <summary>
+ /// Gets whether the given state has been reached already.
+ /// </summary>
+ /// <param name="state">The state to check has been reached.</param>
+ /// <returns>True in case the given state has been reached or there was an error.</returns>
+ public bool HasReachedState(TState state);
}=
\ No newline at end of file
M Core/NosSmooth.Game/Apis/Unsafe/UnsafeSkillsApi.cs => Core/NosSmooth.Game/Apis/Unsafe/UnsafeSkillsApi.cs +1 -0
@@ 421,6 421,7 @@ public class UnsafeSkillsApi
skillUseEvent => skillUseEvent
)
.SetError<CancelPacket>(UseSkillStates.SkillUseRequested, _ => UseSkillErrors.Unknown)
+ .SetTimeout(UseSkillStates.SkillUseRequested, TimeSpan.FromSeconds(1.5), UseSkillErrors.NoResponse)
.SetTimeout(UseSkillStates.SkillUsedResponse, TimeSpan.FromSeconds(1), UseSkillStates.CharacterRestored)
.Build();
}
M Core/NosSmooth.Game/Contracts/UseSkillErrors.cs => Core/NosSmooth.Game/Contracts/UseSkillErrors.cs +6 -1
@@ 24,5 24,10 @@ public enum UseSkillErrors
/// <summary>
/// The character does not have enough mana.
/// </summary>
- NoMana
+ NoMana,
+
+ /// <summary>
+ /// There was no response from the server.
+ /// </summary>
+ NoResponse
}=
\ No newline at end of file
M Core/NosSmooth.Game/Data/Characters/Skills.cs => Core/NosSmooth.Game/Data/Characters/Skills.cs +7 -1
@@ 17,4 17,10 @@ public record Skills
Skill PrimarySkill,
Skill SecondarySkill,
IReadOnlyList<Skill> OtherSkills
-);>
\ No newline at end of file
+)
+{
+ /// <summary>
+ /// Gets all skills contained in this collection.
+ /// </summary>
+ public IEnumerable<Skill> AllSkills => new[] { PrimarySkill, SecondarySkill }.Concat(OtherSkills);
+}<
\ No newline at end of file
M Core/NosSmooth.Game/PacketHandlers/Skills/PlayerSkillResponder.cs => Core/NosSmooth.Game/PacketHandlers/Skills/PlayerSkillResponder.cs +1 -0
@@ 94,6 94,7 @@ public class PlayerSkillResponder : IPacketResponder<SkiPacket>
otherSkillsFromCharacter.Add(await CreateSkill(newSkill, default));
}
+ otherSkillsFromCharacter.RemoveAll(x => x.SkillVNum == primarySkill.SkillVNum || x.SkillVNum == secondarySkill.SkillVNum);
skills = new Data.Characters.Skills(primarySkill, secondarySkill, otherSkillsFromCharacter);
await _game.CreateOrUpdateSkillsAsync
M Extensions/NosSmooth.Extensions.Combat/CombatManager.cs => Extensions/NosSmooth.Extensions.Combat/CombatManager.cs +103 -114
@@ 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;
@@ 19,8 20,6 @@ namespace NosSmooth.Extensions.Combat;
/// </summary>
public class CombatManager : IStatefulEntity
{
- private readonly List<CancellationTokenSource> _tokenSource;
- private readonly SemaphoreSlim _semaphore;
private readonly INostaleClient _client;
private readonly Game.Game _game;
private bool _cancelling;
@@ 32,8 31,6 @@ public class CombatManager : IStatefulEntity
/// <param name="game">The game.</param>
public CombatManager(INostaleClient client, Game.Game game)
{
- _semaphore = new SemaphoreSlim(1, 1);
- _tokenSource = new List<CancellationTokenSource>();
_client = client;
_game = game;
}
@@ 50,76 47,37 @@ public class CombatManager : IStatefulEntity
long? currentTarget = null;
long? previousTarget = null;
- while (!combatState.ShouldQuit && !ct.IsCancellationRequested)
+ while (!(combatState.ShouldQuit && combatState.CanQuit) && !ct.IsCancellationRequested)
{
var commandResult = await _client.SendCommandAsync
(
new AttackCommand
(
currentTarget,
- async (c) =>
+ async (ct) =>
{
- while (!combatState.ShouldQuit && currentTarget == previousTarget)
+ while (!(combatState.ShouldQuit && combatState.CanQuit) && currentTarget == previousTarget)
{
- if (!technique.ShouldContinue(combatState))
- {
- combatState.QuitCombat();
- continue;
- }
+ var iterationResult = await HandleAttackIterationAsync(combatState, technique, ct);
- var operation = combatState.NextOperation();
-
- 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)
+ var result = iterationResult.Entity;
+ if (!result.TargetChanged)
{
- return Result.FromError(responseResult);
- }
-
- if (responseResult.Entity == CanBeUsedResponse.WontBeUsable)
- {
- 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,78 96,109 @@ public class CombatManager : IStatefulEntity
return Result.FromSuccess();
}
- /// <summary>
- /// Register the given cancellation token source to be cancelled on skill use/cancel.
- /// </summary>
- /// <param name="tokenSource">The token source to register.</param>
- /// <param name="ct">The cancellation token for cancelling the operation.</param>
- /// <returns>A task.</returns>
- public async Task RegisterSkillCancellationTokenAsync(CancellationTokenSource tokenSource, CancellationToken ct)
+ private async Task<Result<(bool TargetChanged, long? TargetId)>> HandleAttackIterationAsync
+ (CombatState combatState, ICombatTechnique technique, CancellationToken ct)
{
- await _semaphore.WaitAsync(ct);
- try
- {
- _tokenSource.Add(tokenSource);
- }
- finally
+ if (!technique.ShouldContinue(combatState))
{
- _semaphore.Release();
+ combatState.QuitCombat();
}
- }
- /// <summary>
- /// Unregister the given cancellation token registered using RegisterSkillCancellationToken.
- /// </summary>
- /// <param name="tokenSource">The token source to unregister.</param>
- /// <param name="ct">The cancellation token for cancelling the operation.</param>
- /// <returns>A task.</returns>
- public async Task UnregisterSkillCancellationTokenAsync(CancellationTokenSource tokenSource, CancellationToken ct)
- {
- if (_cancelling)
- {
- return;
- }
+ // the operations need time for execution and/or
+ // wait.
+ await Task.Delay(50, ct);
- await _semaphore.WaitAsync(ct);
- try
- {
- _tokenSource.Remove(tokenSource);
- }
- finally
+ 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
{
- _semaphore.Release();
- }
+ 0 => results.FirstOrDefault
+ (x => x.Entity.TargetChanged, Result<(bool TargetChanged, long?)>.FromSuccess((false, null))),
+ 1 => (Result<(bool, long?)>)errors[0],
+ _ => new AggregateError()
+ };
}
- /// <summary>
- /// Cancel all of the skill tokens.
- /// </summary>
- /// <param name="ct">The cancellation token for cancelling the operation.</param>
- /// <returns>A task.</returns>
- internal async Task CancelSkillTokensAsync(CancellationToken ct)
+ private async Task<Result<(bool TargetChanged, long? TargetId)>> HandleTypeIterationAsync
+ (
+ OperationQueueType queueType,
+ CombatState combatState,
+ ICombatTechnique technique,
+ CancellationToken ct
+ )
{
- await _semaphore.WaitAsync(ct);
- _cancelling = true;
- try
+ var currentOperation = combatState.GetCurrentOperation(queueType);
+ if (currentOperation?.IsFinished() ?? false)
{
- foreach (var tokenSource in _tokenSource)
+ var operationResult = await currentOperation.WaitForFinishedAsync(combatState, ct);
+ currentOperation.Dispose();
+
+ if (!operationResult.IsSuccess)
{
- try
- {
- tokenSource.Cancel();
- }
- catch
+ return Result<(bool, long?)>.FromError(operationResult);
+ }
+
+ currentOperation = null;
+ }
+
+ if (currentOperation is null && !combatState.ShouldQuit)
+ { // 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)
{
- // ignored
+ return Result<(bool, long?)>.FromError(stepResult);
}
+
+ return Result<(bool, long?)>.FromSuccess((true, stepResult.Entity));
}
+ }
- _tokenSource.Clear();
+ if (currentOperation is null)
+ { // should quit, do nothing.
+ return (false, null);
}
- finally
- {
- _cancelling = false;
- _semaphore.Release();
+
+ 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);
}
}=
\ No newline at end of file
M Extensions/NosSmooth.Extensions.Combat/CombatState.cs => Extensions/NosSmooth.Extensions.Combat/CombatState.cs +77 -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>
@@ 45,63 45,108 @@ internal class CombatState : ICombatState
/// <inheritdoc/>
public INostaleClient Client { get; }
+ /// <summary>
+ /// Gets whether the manager may currently quit.
+ /// </summary>
+ /// <remarks>
+ /// Used for finishing the current operations.
+ /// </remarks>
+ public bool CanQuit => _currentOperations.Values.All(x => !x.IsExecuting() || x.IsFinished());
+
/// <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)
+ {
+ _operations[queueType].RemoveFirst();
+ _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 +80 -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,123 @@ 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);
+ }
- if (!sendResponse.IsSuccess)
+ /// <inheritdoc />
+ public bool IsExecuting()
+ => _contract is not null && _contract.CurrentState > UseSkillStates.None && !IsFinished();
+
+ /// <inheritdoc />
+ public bool IsFinished()
+ => _contract?.HasReachedState(UseSkillStates.CharacterRestored) ?? false;
+
+ /// <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)
{
+ if (info.AttackType == AttackType.Dash)
+ {
+ return SkillsApi.ContractUseSkillOn(Skill, Target, Target.Position!.Value.X, Target.Position!.Value.Y);
+ }
+
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 +91 -11
@@ 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;
@@ 55,12 74,27 @@ public class SimpleAttackTechnique : ICombatTechnique
return false;
}
- var entity = map.Entities.GetEntity<ILivingEntity>(_targetId);
- return !(entity is null || (entity.Hp is not null && (entity.Hp.Amount <= 0 || entity.Hp.Percentage <= 0)));
+ if (_target is null)
+ {
+ _target = map.Entities.GetEntity<ILivingEntity>(_targetId);
+ }
+
+ return !(_target is null || (_target.Hp is not null && (_target.Hp.Amount <= 0 || _target.Hp.Percentage <= 0)));
+ }
+
+ /// <inheritdoc />
+ public Result HandleWaiting(OperationQueueType queueType, ICombatState state, ICombatOperation operation)
+ { // does not do anything, just wait.
+ return Result.FromSuccess();
}
/// <inheritdoc />
- public Result<long?> HandleCombatStep(ICombatState state)
+ 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,9 128,8 @@ public class SimpleAttackTechnique : ICombatTechnique
}
var characterMp = character.Mp?.Amount ?? 0;
- var usableSkills = new[] { skills.PrimarySkill, skills.SecondarySkill }
- .Concat(skills.OtherSkills)
- .Where(x => x.Info is not null && x.Info.HitType != HitType.AlliesInZone)
+ var usableSkills = skills.AllSkills
+ .Where(x => x.Info is not null && x.Info.HitType != HitType.AlliesInZone && x.Info.SkillType == SkillType.Player)
.Where(x => !x.IsOnCooldown && characterMp >= (x.Info?.MpCost ?? long.MaxValue));
var skillResult = _skillSelector.GetSelectedSkill(usableSkills);
@@ 138,20 171,67 @@ public class SimpleAttackTechnique : ICombatTechnique
if (!character.Position.Value.IsInRange(_target.Position.Value, range))
{
- state.WalkInRange(_walkManager, _target, _currentSkill.Info.Range);
+ state.WalkInRange(_walkManager, _target, range);
}
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
A Tests/NosSmooth.Core.Tests/Contracts/ContractData.cs => Tests/NosSmooth.Core.Tests/Contracts/ContractData.cs +28 -0
@@ 0,0 1,28 @@
+//
+// ContractData.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.Core.Tests.Contracts;
+
+/// <summary>
+/// Data for updating a contract.
+/// </summary>
+/// <typeparam name="T">The type of the data.</typeparam>
+public class ContractData<T>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ContractData{T}"/> class.
+ /// </summary>
+ /// <param name="data">The data to pass.</param>
+ public ContractData(T data)
+ {
+ Data = data;
+ }
+
+ /// <summary>
+ /// Gets the data.
+ /// </summary>
+ public T Data { get; }
+}<
\ No newline at end of file
A Tests/NosSmooth.Core.Tests/Contracts/ContractError.cs => Tests/NosSmooth.Core.Tests/Contracts/ContractError.cs +18 -0
@@ 0,0 1,18 @@
+//
+// ContractError.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.Core.Tests.Contracts;
+
+/// <summary>
+/// Errors for a contract.
+/// </summary>
+public enum ContractError
+{
+ /// <summary>
+ /// An error.
+ /// </summary>
+ Error1
+}<
\ No newline at end of file
A Tests/NosSmooth.Core.Tests/Contracts/ContractMultipleStates.cs => Tests/NosSmooth.Core.Tests/Contracts/ContractMultipleStates.cs +33 -0
@@ 0,0 1,33 @@
+//
+// ContractMultipleStates.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.Core.Tests.Contracts;
+
+/// <summary>
+/// Extends DefaultStates to have more states to test.
+/// </summary>
+public enum ContractMultipleStates
+{
+ /// <summary>
+ /// Initial state.
+ /// </summary>
+ None,
+
+ /// <summary>
+ /// Contract executed, request issued.
+ /// </summary>
+ Requested,
+
+ /// <summary>
+ /// A response was obtained.
+ /// </summary>
+ ResponseObtained,
+
+ /// <summary>
+ /// Something else happening after obtaining first response.
+ /// </summary>
+ AfterResponseObtained,
+}<
\ No newline at end of file
A Tests/NosSmooth.Core.Tests/Contracts/ContractTests.cs => Tests/NosSmooth.Core.Tests/Contracts/ContractTests.cs +360 -0
@@ 0,0 1,360 @@
+//
+// ContractTests.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;
+using System.Threading;
+using System.Threading.Tasks;
+using NosSmooth.Core.Contracts;
+using Remora.Results;
+using Shouldly;
+using Xunit;
+
+namespace NosSmooth.Core.Tests.Contracts;
+
+/// <summary>
+/// Tests basics of contract system.
+/// </summary>
+public class ContractTests
+{
+ /// <summary>
+ /// Tests that the contract is executed.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ [Fact]
+ public async Task Test_GetsExecuted()
+ {
+ var contractor = new Contractor();
+ var mock = new MockClass();
+
+ var contract = new ContractBuilder<long, DefaultStates, NoErrors>(contractor, DefaultStates.None)
+ .SetMoveAction(DefaultStates.None, mock.Setup, DefaultStates.Requested)
+ .SetMoveFilter<ContractData<long>>(DefaultStates.Requested, DefaultStates.ResponseObtained)
+ .SetFillData<ContractData<long>>(DefaultStates.ResponseObtained, d => d.Data)
+ .Build();
+
+ contract.CurrentState.ShouldBe(DefaultStates.None);
+ await contract.OnlyExecuteAsync();
+ contract.CurrentState.ShouldBe(DefaultStates.Requested);
+ mock.Executed.ShouldBeTrue();
+ mock.ExecutedTimes.ShouldBe(1);
+ }
+
+ /// <summary>
+ /// Tests that the contract response is obtained.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ [Fact]
+ public async Task Test_ResponseObtained()
+ {
+ var contractor = new Contractor();
+ var mock = new MockClass();
+
+ var contract = new ContractBuilder<long, DefaultStates, NoErrors>(contractor, DefaultStates.None)
+ .SetMoveAction(DefaultStates.None, mock.Setup, DefaultStates.Requested)
+ .SetMoveFilter<ContractData<long>>(DefaultStates.Requested, DefaultStates.ResponseObtained)
+ .SetFillData<ContractData<long>>(DefaultStates.ResponseObtained, d => d.Data)
+ .Build();
+
+ await contract.OnlyExecuteAsync();
+ contract.Register();
+ await contractor.Update(new ContractData<long>(5));
+ contract.CurrentState.ShouldBe(DefaultStates.ResponseObtained);
+ contract.Data.ShouldBe(5);
+
+ await contractor.Update(new ContractData<long>(10));
+ contract.Data.ShouldBe(5);
+ contract.Unregister();
+
+ mock.ExecutedTimes.ShouldBe(1);
+ }
+
+ /// <summary>
+ /// Tests that the contract response is obtained.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ [Fact]
+ public async Task Test_WaitFor()
+ {
+ var contractor = new Contractor();
+ var mock = new MockClass();
+
+ var contract = new ContractBuilder<long, DefaultStates, NoErrors>(contractor, DefaultStates.None)
+ .SetMoveAction(DefaultStates.None, mock.Setup, DefaultStates.Requested)
+ .SetMoveFilter<ContractData<long>>(DefaultStates.Requested, DefaultStates.ResponseObtained)
+ .SetFillData<ContractData<long>>(DefaultStates.ResponseObtained, d => d.Data)
+ .Build();
+
+ Task.Run
+ (
+ async () =>
+ {
+ await Task.Delay(500);
+ await contractor.Update(new ContractData<long>(15));
+ }
+ );
+
+ var result = await contract.WaitForAsync(DefaultStates.ResponseObtained);
+ result.IsSuccess.ShouldBeTrue();
+ result.Entity.ShouldBe(15);
+ contract.IsRegistered.ShouldBeFalse(); // trust the contract for now.
+ contract.CurrentState.ShouldBe(DefaultStates.ResponseObtained);
+ mock.ExecutedTimes.ShouldBe(1);
+ }
+
+ /// <summary>
+ /// Tests that the contract response is obtained.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ [Fact]
+ public async Task Test_WaitForMoreStates()
+ {
+ var contractor = new Contractor();
+ var mock = new MockClass();
+
+ var contract = new ContractBuilder<long, ContractMultipleStates, NoErrors>
+ (contractor, ContractMultipleStates.None)
+ .SetMoveAction(ContractMultipleStates.None, mock.Setup, ContractMultipleStates.Requested)
+ .SetMoveFilter<ContractData<long>>
+ (ContractMultipleStates.Requested, ContractMultipleStates.ResponseObtained)
+ .SetFillData<ContractData<long>>(ContractMultipleStates.ResponseObtained, d => d.Data)
+ .SetMoveFilter<ContractData<bool>>
+ (ContractMultipleStates.ResponseObtained, c => c.Data, ContractMultipleStates.AfterResponseObtained)
+ .Build();
+
+ Task.Run
+ (
+ async () =>
+ {
+ await Task.Delay(500);
+ await contractor.Update(new ContractData<long>(15));
+ await Task.Delay(200);
+ await contractor.Update(new ContractData<bool>(true));
+ }
+ );
+
+ var result = await contract.WaitForAsync(ContractMultipleStates.AfterResponseObtained);
+ result.IsSuccess.ShouldBeTrue();
+ result.Entity.ShouldBe(15);
+ contract.IsRegistered.ShouldBeFalse(); // trust the contract for now.
+ contract.CurrentState.ShouldBe(ContractMultipleStates.AfterResponseObtained);
+ mock.ExecutedTimes.ShouldBe(1);
+ }
+
+ /// <summary>
+ /// Tests that the contract response is obtained.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ [Fact]
+ public async Task Test_MoreStatesFollowed()
+ {
+ var contractor = new Contractor();
+ var mock = new MockClass();
+
+ var contract = new ContractBuilder<long, ContractMultipleStates, NoErrors>
+ (contractor, ContractMultipleStates.None)
+ .SetMoveAction(ContractMultipleStates.None, mock.Setup, ContractMultipleStates.Requested)
+ .SetMoveFilter<ContractData<long>>
+ (ContractMultipleStates.Requested, ContractMultipleStates.ResponseObtained)
+ .SetFillData<ContractData<long>>(ContractMultipleStates.ResponseObtained, d => d.Data)
+ .SetMoveFilter<ContractData<bool>>
+ (ContractMultipleStates.ResponseObtained, c => c.Data, ContractMultipleStates.AfterResponseObtained)
+ .Build();
+
+ await contract.OnlyExecuteAsync();
+ contract.Register();
+ await contractor.Update(new ContractData<long>(15));
+ await contractor.Update(new ContractData<bool>(true));
+ contract.Unregister();
+ var result = await contract.WaitForAsync(ContractMultipleStates.AfterResponseObtained);
+ result.IsSuccess.ShouldBeTrue();
+ result.Entity.ShouldBe(15);
+ contract.IsRegistered.ShouldBeFalse(); // trust the contract for now.
+ contract.CurrentState.ShouldBe(ContractMultipleStates.AfterResponseObtained);
+ mock.ExecutedTimes.ShouldBe(1);
+ }
+
+ /// <summary>
+ /// Tests that the contract response is obtained.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ [Fact]
+ public async Task Test_WaitForTimeout()
+ {
+ var contractor = new Contractor();
+ var mock = new MockClass();
+
+ var contract = new ContractBuilder<long, ContractMultipleStates, NoErrors>
+ (contractor, ContractMultipleStates.None)
+ .SetMoveAction(ContractMultipleStates.None, mock.Setup, ContractMultipleStates.Requested)
+ .SetMoveFilter<ContractData<long>>
+ (ContractMultipleStates.Requested, ContractMultipleStates.ResponseObtained)
+ .SetFillData<ContractData<long>>(ContractMultipleStates.ResponseObtained, d => d.Data)
+ .SetTimeout
+ (
+ ContractMultipleStates.ResponseObtained,
+ TimeSpan.FromMilliseconds(100),
+ ContractMultipleStates.AfterResponseObtained
+ )
+ .Build();
+
+ Task.Run
+ (
+ async () =>
+ {
+ await Task.Delay(500);
+ await contractor.Update(new ContractData<long>(15));
+ }
+ );
+
+ var result = await contract.WaitForAsync(ContractMultipleStates.AfterResponseObtained);
+ result.IsSuccess.ShouldBeTrue();
+ result.Entity.ShouldBe(15);
+ contract.IsRegistered.ShouldBeFalse(); // trust the contract for now.
+ contract.CurrentState.ShouldBe(ContractMultipleStates.AfterResponseObtained);
+ mock.ExecutedTimes.ShouldBe(1);
+ }
+
+ /// <summary>
+ /// Tests that the contract response is obtained.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ [Fact]
+ public async Task Test_MultipleContracts()
+ {
+ var contractor = new Contractor();
+ var mock = new MockClass();
+
+ var contract1 = new ContractBuilder<long, ContractMultipleStates, NoErrors>
+ (contractor, ContractMultipleStates.None)
+ .SetMoveAction(ContractMultipleStates.None, mock.Setup, ContractMultipleStates.Requested)
+ .SetMoveFilter<ContractData<long>>
+ (
+ ContractMultipleStates.Requested,
+ d => d.Data > 10 && d.Data < 20,
+ ContractMultipleStates.ResponseObtained
+ )
+ .SetFillData<ContractData<long>>(ContractMultipleStates.ResponseObtained, d => d.Data)
+ .Build();
+
+ var contract2 = new ContractBuilder<long, ContractMultipleStates, NoErrors>
+ (contractor, ContractMultipleStates.None)
+ .SetMoveAction(ContractMultipleStates.None, mock.Setup, ContractMultipleStates.Requested)
+ .SetMoveFilter<ContractData<long>>
+ (ContractMultipleStates.Requested, d => d.Data > 20, ContractMultipleStates.ResponseObtained)
+ .SetFillData<ContractData<long>>(ContractMultipleStates.ResponseObtained, d => d.Data)
+ .Build();
+
+ await contract1.OnlyExecuteAsync();
+ await contract2.OnlyExecuteAsync();
+
+ Task.Run
+ (
+ async () =>
+ {
+ await Task.Delay(500);
+ await contractor.Update(new ContractData<long>(15));
+ await Task.Delay(500);
+ await contractor.Update(new ContractData<long>(25));
+ }
+ );
+
+ var results = await Task.WhenAll
+ (
+ contract1.WaitForAsync(ContractMultipleStates.ResponseObtained),
+ contract2.WaitForAsync(ContractMultipleStates.ResponseObtained)
+ );
+ results[0].IsSuccess.ShouldBeTrue();
+ results[0].Entity.ShouldBe(15);
+ results[1].IsSuccess.ShouldBeTrue();
+ results[1].Entity.ShouldBe(25);
+ contract1.CurrentState.ShouldBe(ContractMultipleStates.ResponseObtained);
+ contract2.CurrentState.ShouldBe(ContractMultipleStates.ResponseObtained);
+ mock.ExecutedTimes.ShouldBe(2);
+ }
+
+ /// <summary>
+ /// Tests that the contract response is obtained.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ [Fact]
+ public async Task Test_ErrorsFired()
+ {
+ var contractor = new Contractor();
+ var mock = new MockClass();
+
+ var contract = new ContractBuilder<long, ContractMultipleStates, ContractError>
+ (contractor, ContractMultipleStates.None)
+ .SetMoveAction(ContractMultipleStates.None, mock.Setup, ContractMultipleStates.Requested)
+ .SetMoveFilter<ContractData<long>>
+ (ContractMultipleStates.Requested, ContractMultipleStates.ResponseObtained)
+ .SetFillData<ContractData<long>>(ContractMultipleStates.ResponseObtained, d => d.Data)
+ .SetError<ContractData<long>>
+ (
+ ContractMultipleStates.Requested,
+ d =>
+ {
+ if (d.Data == 15)
+ {
+ return ContractError.Error1;
+ }
+
+ return null;
+ }
+ )
+ .Build();
+
+ Task.Run
+ (
+ async () =>
+ {
+ await Task.Delay(500);
+ await contractor.Update(new ContractData<long>(15));
+ }
+ );
+
+ var result = await contract.WaitForAsync(ContractMultipleStates.AfterResponseObtained);
+ result.IsSuccess.ShouldBeFalse();
+ result.Error.ShouldBeOfType<ContractError<ContractError>>();
+ ((ContractError<ContractError>)result.Error).Error.ShouldBe(ContractError.Error1);
+ contract.CurrentState.ShouldBe(ContractMultipleStates.Requested);
+ contract.IsRegistered.ShouldBeFalse();
+ mock.ExecutedTimes.ShouldBe(1);
+ }
+}
+
+/// <summary>
+/// A class for verifying setup was called.
+/// </summary>
+public class MockClass
+{
+ /// <summary>
+ /// Gets the number of times <see cref="Setup"/> was called.
+ /// </summary>
+ public int ExecutedTimes { get; private set; }
+
+ /// <summary>
+ /// Gets whether <see cref="Executed"/> was executed.
+ /// </summary>
+ public bool Executed { get; private set; }
+
+ /// <summary>
+ /// Sets Executed to true..
+ /// </summary>
+ /// <param name="data">The data. should be null.</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<bool>> Setup(object? data, CancellationToken ct)
+ {
+ if (data is not null)
+ {
+ throw new ArgumentException("Should be null.", nameof(data));
+ }
+
+ Executed = true;
+ ExecutedTimes++;
+ return Task.FromResult(Result<bool>.FromSuccess(true));
+ }
+}<
\ No newline at end of file
M Tests/NosSmooth.Core.Tests/NosSmooth.Core.Tests.csproj => Tests/NosSmooth.Core.Tests/NosSmooth.Core.Tests.csproj +2 -0
@@ 10,6 10,8 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
+ <PackageReference Include="Moq" Version="4.18.4" />
+ <PackageReference Include="Shouldly" Version="4.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>