A Extensions/NosSmooth.Extensions.Combat/CombatManager.cs => Extensions/NosSmooth.Extensions.Combat/CombatManager.cs +149 -0
@@ 0,0 1,149 @@
+//
+// CombatManager.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.Client;
+using NosSmooth.Core.Stateful;
+using NosSmooth.Extensions.Combat.Errors;
+using NosSmooth.Extensions.Combat.Operations;
+using NosSmooth.Extensions.Combat.Techniques;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat;
+
+/// <summary>
+/// The combat manager that uses techniques to attack enemies.
+/// </summary>
+public class CombatManager : IStatefulEntity
+{
+ private readonly List<CancellationTokenSource> _tokenSource;
+ private readonly Semaphore _semaphore;
+ private readonly INostaleClient _client;
+ private readonly Game.Game _game;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CombatManager"/> class.
+ /// </summary>
+ /// <param name="client">The NosTale client.</param>
+ /// <param name="game">The game.</param>
+ public CombatManager(INostaleClient client, Game.Game game)
+ {
+ _semaphore = new Semaphore(1, 1);
+ _tokenSource = new List<CancellationTokenSource>();
+ _client = client;
+ _game = game;
+ }
+
+ /// <summary>
+ /// Enter into a combat state using the given technique.
+ /// </summary>
+ /// <param name="technique">The technique to use.</param>
+ /// <returns>A result that may or may not succeed.</returns>
+ public async Task<Result> EnterCombat(ICombatTechnique technique)
+ {
+ var combatState = new CombatState(_client, _game, this);
+
+ while (!combatState.ShouldQuit)
+ {
+ if (!technique.ShouldContinue(combatState))
+ {
+ combatState.QuitCombat();
+ continue;
+ }
+
+ 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 stepResult;
+ }
+
+ 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);
+ 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);
+ }
+
+ if (!responseResult.IsSuccess)
+ {
+ return Result.FromError(responseResult);
+ }
+
+ if (responseResult.Entity == CanBeUsedResponse.WontBeUsable)
+ {
+ return new UnusableOperationError(operation);
+ }
+
+ var usageResult = await operation.UseAsync(combatState);
+ if (!usageResult.IsSuccess)
+ {
+ var errorHandleResult = technique.HandleError(combatState, operation, usageResult);
+ if (!errorHandleResult.IsSuccess)
+ {
+ return errorHandleResult;
+ }
+ }
+ }
+
+ 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>
+ public void RegisterSkillCancellationToken(CancellationTokenSource tokenSource)
+ {
+ _semaphore.WaitOne();
+ _tokenSource.Add(tokenSource);
+ _semaphore.Release();
+ }
+
+ /// <summary>
+ /// Unregister the given cancellation token registered using <see cref="RegisterSkillCancellationToken"/>.
+ /// </summary>
+ /// <param name="tokenSource">The token source to unregister.</param>
+ public void UnregisterSkillCancellationToken(CancellationTokenSource tokenSource)
+ {
+ _semaphore.WaitOne();
+ _tokenSource.Remove(tokenSource);
+ _semaphore.Release();
+ }
+
+ /// <summary>
+ /// Cancel all of the skill tokens.
+ /// </summary>
+ internal void CancelSkillTokens()
+ {
+ _semaphore.WaitOne();
+ foreach (var tokenSource in _tokenSource)
+ {
+ try
+ {
+ tokenSource.Cancel();
+ }
+ catch
+ {
+ // ignored
+ }
+ }
+
+ _tokenSource.Clear();
+ _semaphore.Release();
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/CombatState.cs => Extensions/NosSmooth.Extensions.Combat/CombatState.cs +107 -0
@@ 0,0 1,107 @@
+//
+// CombatState.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.Xml;
+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;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CombatState"/> class.
+ /// </summary>
+ /// <param name="client">The NosTale client.</param>
+ /// <param name="game">The game.</param>
+ /// <param name="combatManager">The combat manager.</param>
+ public CombatState(INostaleClient client, Game.Game game, CombatManager combatManager)
+ {
+ Client = client;
+ Game = game;
+ CombatManager = combatManager;
+ _operations = new LinkedList<ICombatOperation>();
+ }
+
+ /// <summary>
+ /// Gets whether the combat state should be quit.
+ /// </summary>
+ public bool ShouldQuit { get; private set; }
+
+ /// <inheritdoc/>
+ public CombatManager CombatManager { get; }
+
+ /// <inheritdoc/>
+ public Game.Game Game { get; }
+
+ /// <inheritdoc/>
+ public INostaleClient Client { get; }
+
+ /// <inheritdoc/>
+ public void QuitCombat()
+ {
+ ShouldQuit = true;
+ }
+
+ /// <summary>
+ /// Make a step in the queue.
+ /// </summary>
+ /// <returns>The current operation, if any.</returns>
+ public ICombatOperation? NextOperation()
+ {
+ var operation = _currentOperation = _operations.First?.Value;
+ if (operation is not null)
+ {
+ _operations.RemoveFirst();
+ }
+
+ return operation;
+ }
+
+ /// <inheritdoc/>
+ public void SetCurrentOperation
+ (ICombatOperation operation, bool emptyQueue = false, bool prependCurrentOperationToQueue = false)
+ {
+ var current = _currentOperation;
+ _currentOperation = operation;
+
+ if (emptyQueue)
+ {
+ _operations.Clear();
+ }
+
+ if (prependCurrentOperationToQueue && current is not null)
+ {
+ _operations.AddFirst(current);
+ }
+ }
+
+ /// <inheritdoc/>
+ public void EnqueueOperation(ICombatOperation operation)
+ {
+ _operations.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;
+ }
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Extensions/ServiceCollectionExtensions.cs => Extensions/NosSmooth.Extensions.Combat/Extensions/ServiceCollectionExtensions.cs +30 -0
@@ 0,0 1,30 @@
+//
+// ServiceCollectionExtensions.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Extensions.DependencyInjection;
+using NosSmooth.Core.Extensions;
+using NosSmooth.Extensions.Combat.Responders;
+
+namespace NosSmooth.Extensions.Combat.Extensions;
+
+/// <summary>
+/// Extension methods for <see cref="IServiceCollection"/>.
+/// </summary>
+public static class ServiceCollectionExtensions
+{
+ /// <summary>
+ /// Adds a NosTale combat extension. <see cref="CombatManager"/>.
+ /// </summary>
+ /// <param name="serviceCollection">The service collection.</param>
+ /// <returns>The collection.</returns>
+ public static IServiceCollection AddNostaleCombat(this IServiceCollection serviceCollection)
+ {
+ return serviceCollection
+ .AddPacketResponder<CancelResponder>()
+ .AddPacketResponder<SuResponder>()
+ .AddSingleton<CombatManager>();
+ }
+}<
\ No newline at end of file