A Core/NosSmooth.Core/Contracts/ContractBuilder.cs => Core/NosSmooth.Core/Contracts/ContractBuilder.cs +241 -0
@@ 0,0 1,241 @@
+//
+// ContractBuilder.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.Collections.Generic;
+using System.ComponentModel;
+using System.Threading;
+using System.Threading.Tasks;
+using Remora.Results;
+
+namespace NosSmooth.Core.Contracts;
+
+/// <summary>
+/// Builds <see cref="IContract"/> with given states
+/// and errors.
+/// </summary>
+/// <typeparam name="TData">The data type.</typeparam>
+/// <typeparam name="TState">The states.</typeparam>
+/// <typeparam name="TError">The errors that may be returned.</typeparam>
+public class ContractBuilder<TData, TState, TError>
+ where TState : struct, IComparable
+ where TError : struct
+ where TData : notnull
+{
+ private readonly Contractor _contractor;
+ private readonly TState _defaultState;
+
+ private readonly Dictionary<TState, DefaultContract<TData, TState, TError>.StateActionAsync> _actions;
+ private readonly Dictionary<TState, (TimeSpan, TState)> _timeouts;
+
+ private TState? _fillAtState;
+ private DefaultContract<TData, TState, TError>.FillDataAsync? _fillData;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ContractBuilder{TData, TState, TError}"/> class.
+ /// </summary>
+ /// <param name="contractor">The contractor.</param>
+ /// <param name="defaultState">The default state of the contract.</param>
+ public ContractBuilder(Contractor contractor, TState defaultState)
+ {
+ _contractor = contractor;
+ _defaultState = defaultState;
+ _actions = new Dictionary<TState, DefaultContract<TData, TState, TError>.StateActionAsync>();
+ _timeouts = new Dictionary<TState, (TimeSpan, TState)>();
+ }
+
+ /// <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="nextState">The state to go to after timeout.</param>
+ /// <returns>The updated builder.</returns>
+ public ContractBuilder<TData, TState, TError> SetTimeout(TState state, TimeSpan timeout, TState nextState)
+ {
+ _timeouts[state] = (timeout, nextState);
+ return this;
+ }
+
+ /// <summary>
+ /// Set up an action filter that works for the given <paramref name="state"/>
+ /// If the filter matches, moves to the given state from <paramref name="nextState"/>.
+ /// </summary>
+ /// <param name="state">The state to apply filter action to.</param>
+ /// <param name="filter">The filter to match.</param>
+ /// <param name="nextState">The state to move to.</param>
+ /// <typeparam name="TAny">The type of the filter data.</typeparam>
+ /// <returns>The updated builder.</returns>
+ public ContractBuilder<TData, TState, TError> SetMoveFilter<TAny>
+ (TState state, Func<TAny, bool> filter, TState nextState)
+ {
+ _actions[state] = (data, ct) =>
+ {
+ if (data is TAny matched)
+ {
+ if (filter(matched))
+ {
+ return Task.FromResult
+ (Result<(TError?, TState?)>.FromSuccess((null, nextState)));
+ }
+ }
+
+ return Task.FromResult
+ (Result<(TError?, TState?)>.FromSuccess((null, null)));
+ };
+ return this;
+ }
+
+ /// <summary>
+ /// Set up an action filter that works for the given <paramref name="state"/>
+ /// If the filter matches, moves to the given state from <paramref name="nextState"/>.
+ /// </summary>
+ /// <param name="state">The state to apply filter action to.</param>
+ /// <param name="nextState">The state to move to.</param>
+ /// <typeparam name="TAny">The type of the filter data.</typeparam>
+ /// <returns>The updated builder.</returns>
+ public ContractBuilder<TData, TState, TError> SetMoveFilter<TAny>(TState state, TState nextState)
+ => SetMoveFilter<TAny>(state, d => true, nextState);
+
+ /// <summary>
+ /// Sets that the given state will fill the data.
+ /// </summary>
+ /// <param name="state">The state that will fill the data.</param>
+ /// <param name="fillData">The function to fill the data.</param>
+ /// <typeparam name="TAny">The type that is expected to fill the data.</typeparam>
+ /// <returns>The updated builder.</returns>
+ public ContractBuilder<TData, TState, TError> SetFillData<TAny>(TState state, Func<TAny, Result<TData>> fillData)
+ {
+ _fillAtState = state;
+ _fillData = (data, ct) =>
+ {
+ if (data is not TAny matched)
+ {
+ return Task.FromResult(Result<TData>.FromError(new GenericError("Fill data not matched.")));
+ }
+
+ return Task.FromResult(fillData(matched));
+ };
+ return this;
+ }
+
+ /// <summary>
+ /// Sets that the given state should error on the given type.
+ /// </summary>
+ /// <param name="state">The state to accept error at.</param>
+ /// <param name="errorFunction">The error function.</param>
+ /// <typeparam name="TAny">The type to match.</typeparam>
+ /// <returns>The updated builder.</returns>
+ public ContractBuilder<TData, TState, TError> SetError<TAny>(TState state, Func<TAny, TError?> errorFunction)
+ {
+ var last = _actions[state];
+ _actions[state] = async (data, ct) =>
+ {
+ if (data is TAny matched)
+ {
+ var error = errorFunction(matched);
+
+ if (error is not null)
+ {
+ return Result<(TError?, TState?)>.FromSuccess((error, null));
+ }
+ }
+
+ return await last(data, ct);
+ };
+ return this;
+ }
+
+ /// <summary>
+ /// Set up an action that works for the given <paramref name="state"/>
+ /// If the given state is reached and data are updated, this function is called.
+ /// </summary>
+ /// <param name="state">The state to apply filter action to.</param>
+ /// <param name="actionFilter">The filter to filter the action.</param>
+ /// <param name="nextState">The state to move to.</param>
+ /// <typeparam name="TAny">The type of the filter data.</typeparam>
+ /// <returns>The updated builder.</returns>
+ public ContractBuilder<TData, TState, TError> SetMoveAction<TAny>
+ (TState state, Func<TAny, Task<Result<bool>>> actionFilter, TState nextState)
+ {
+ _actions[state] = async (data, ct) =>
+ {
+ if (data is TAny matched)
+ {
+ var filterResult = await actionFilter(matched);
+ if (!filterResult.IsDefined(out var filter))
+ {
+ return Result<(TError?, TState?)>.FromError(filterResult);
+ }
+
+ if (filter)
+ {
+ return Result<(TError?, TState?)>.FromSuccess((null, nextState));
+ }
+ }
+
+ return Result<(TError?, TState?)>.FromSuccess((null, null));
+ };
+ return this;
+ }
+
+ /// <summary>
+ /// Set up an action that works for the given <paramref name="state"/>
+ /// If the given state is reached and data are updated, this function is called.
+ /// </summary>
+ /// <param name="state">The state to apply filter action to.</param>
+ /// <param name="actionFilter">The filter to filter the action.</param>
+ /// <param name="nextState">The state to move to.</param>
+ /// <returns>The updated builder.</returns>
+ public ContractBuilder<TData, TState, TError> SetMoveAction
+ (TState state, Func<object?, CancellationToken, Task<Result<bool>>> actionFilter, TState nextState)
+ {
+ _actions[state] = async (data, ct) =>
+ {
+ var filterResult = await actionFilter(data, ct);
+ if (!filterResult.IsDefined(out var filter))
+ {
+ return Result<(TError?, TState?)>.FromError(filterResult);
+ }
+
+ if (filter)
+ {
+ return Result<(TError?, TState?)>.FromSuccess((null, nextState));
+ }
+
+ return Result<(TError?, TState?)>.FromSuccess((null, null));
+ };
+ return this;
+ }
+
+ /// <summary>
+ /// Build the associate contract.
+ /// </summary>
+ /// <returns>The contract.</returns>
+ /// <exception cref="InvalidOperationException">Thrown in case FillAtState or FillData is null.</exception>
+ public IContract<TData, TState> Build()
+ {
+ if (_fillAtState is null)
+ {
+ throw new InvalidOperationException("FillAtState cannot be null.");
+ }
+
+ if (_fillData is null)
+ {
+ throw new InvalidOperationException("FillData cannot be null.");
+ }
+
+ return new DefaultContract<TData, TState, TError>
+ (
+ _contractor,
+ _defaultState,
+ _fillAtState.Value,
+ _fillData,
+ _actions,
+ _timeouts
+ );
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Core/Contracts/ContractError.cs => Core/NosSmooth.Core/Contracts/ContractError.cs +16 -0
@@ 0,0 1,16 @@
+//
+// 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.
+
+using Remora.Results;
+
+namespace NosSmooth.Core.Contracts;
+
+/// <summary>
+/// An error from contract.
+/// </summary>
+/// <param name="Error">The error.</param>
+/// <typeparam name="TError">The error.</typeparam>
+public record ContractError<TError>(TError Error) : ResultError($"Contract has returned an error {Error}.");<
\ No newline at end of file
A Core/NosSmooth.Core/Contracts/ContractUpdateResponse.cs => Core/NosSmooth.Core/Contracts/ContractUpdateResponse.cs +32 -0
@@ 0,0 1,32 @@
+//
+// ContractUpdateResponse.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.Contracts;
+
+/// <summary>
+/// A response to Contract.Update.
+/// </summary>
+public enum ContractUpdateResponse
+{
+ /// <summary>
+ /// The contract is not interested in the given data.
+ /// </summary>
+ NotInterested,
+
+ /// <summary>
+ /// The contract is interested in the given data,
+ /// the data was used to update the contract and
+ /// the contract wants to stay registered.
+ /// </summary>
+ Interested,
+
+ /// <summary>
+ /// The contract is interested in the given data,
+ /// the data was used to update the contract and
+ /// the contract wants to be unregistered from the contractor.
+ /// </summary>
+ InterestedAndUnregister
+}<
\ No newline at end of file
A Core/NosSmooth.Core/Contracts/Contractor.cs => Core/NosSmooth.Core/Contracts/Contractor.cs +150 -0
@@ 0,0 1,150 @@
+//
+// Contractor.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.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using NosSmooth.Core.Contracts.Responders;
+using Remora.Results;
+
+namespace NosSmooth.Core.Contracts;
+
+/// <summary>
+/// A class holding <see cref="IContract"/>s,
+/// updates the contracts.
+/// </summary>
+public class Contractor : IEnumerable<IContract>
+{
+ /// <summary>
+ /// Maximum time a contract may be registered for.
+ /// </summary>
+ public static readonly TimeSpan Timeout = new TimeSpan(0, 5, 0);
+
+ private readonly List<ContractInfo> _contracts;
+ private readonly SemaphoreSlim _semaphore;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Contractor"/> class.
+ /// </summary>
+ public Contractor()
+ {
+ _semaphore = new SemaphoreSlim(1, 1);
+ _contracts = new List<ContractInfo>();
+ }
+
+ /// <summary>
+ /// Register the given contract to receive feedback for it.
+ /// </summary>
+ /// <param name="contract">The contract to register.</param>
+ public void Register(IContract contract)
+ {
+ try
+ {
+ _semaphore.Wait();
+ _contracts.Add(new ContractInfo(contract, DateTime.Now));
+ }
+ finally
+ {
+ _semaphore.Release();
+ }
+ }
+
+ /// <summary>
+ /// Unregister the given contract, no more info will be received.
+ /// </summary>
+ /// <param name="contract">The contract.</param>
+ public void Unregister(IContract contract)
+ {
+ try
+ {
+ _semaphore.Wait();
+ _contracts.RemoveAll(ci => ci.contract == contract);
+ }
+ finally
+ {
+ _semaphore.Release();
+ }
+ }
+
+ /// <summary>
+ /// Update all of the contracts with the given data.
+ /// </summary>
+ /// <remarks>
+ /// Called from <see cref="ContractPacketResponder"/>
+ /// or similar. Used for updating the state.
+ /// The contracts look for actions that trigger updates
+ /// and in case it matches the <paramref name="data"/>,
+ /// the state is switched.
+ /// </remarks>
+ /// <param name="data">The data that were received.</param>
+ /// <param name="ct">The cancellation token used for cancelling the operation.</param>
+ /// <typeparam name="TData">The type of the data.</typeparam>
+ /// <returns>The result that may or may not have succeeded.</returns>
+ public async Task<Result> Update<TData>(TData data, CancellationToken ct = default)
+ where TData : notnull
+ {
+ var errors = new List<IResult>();
+ var toRemove = new List<ContractInfo>();
+ try
+ {
+ await _semaphore.WaitAsync(ct);
+ foreach (var info in _contracts)
+ {
+ if (DateTime.Now - info.addedAt > Timeout)
+ {
+ errors.Add
+ (
+ (Result)new GenericError
+ (
+ $"A contract {info.contract} has been registered for too long and was unregistered automatically."
+ )
+ );
+ continue;
+ }
+
+ var result = await info.contract.Update(data);
+ if (!result.IsDefined(out var response))
+ {
+ errors.Add(result);
+ }
+
+ if (response == ContractUpdateResponse.InterestedAndUnregister)
+ {
+ toRemove.Add(info);
+ }
+ }
+
+ foreach (var contract in toRemove)
+ {
+ _contracts.Remove(contract);
+ }
+
+ return errors.Count switch
+ {
+ 0 => Result.FromSuccess(),
+ 1 => (Result)errors[0],
+ _ => new AggregateError(errors)
+ };
+ }
+ finally
+ {
+ _semaphore.Release();
+ }
+ }
+
+ /// <inheritdoc />
+ public IEnumerator<IContract> GetEnumerator()
+ => _contracts.Select(x => x.contract).GetEnumerator();
+
+ /// <inheritdoc/>
+ IEnumerator IEnumerable.GetEnumerator()
+ => GetEnumerator();
+
+ private record struct ContractInfo(IContract contract, DateTime addedAt);
+}<
\ No newline at end of file
A Core/NosSmooth.Core/Contracts/DefaultContract.cs => Core/NosSmooth.Core/Contracts/DefaultContract.cs +327 -0
@@ 0,0 1,327 @@
+//
+// DefaultContract.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.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Remora.Results;
+
+namespace NosSmooth.Core.Contracts;
+
+/// <summary>
+/// A generic implementation of contract
+/// supporting any data.
+/// </summary>
+/// <typeparam name="TData">The data type.</typeparam>
+/// <typeparam name="TState">The states.</typeparam>
+/// <typeparam name="TError">The errors that may be returned.</typeparam>
+public class DefaultContract<TData, TState, TError> : IContract<TData, TState>
+ where TState : struct, IComparable
+ where TError : struct
+ where TData : notnull
+{
+ /// <summary>
+ /// An action to execute when a state is reached.
+ /// </summary>
+ /// <param name="data">The data that led to the state.</param>
+ /// <param name="ct">The cancellation token used for cancelling the operation.</param>
+ /// <returns>The result that may or may not have succeeded.</returns>
+ public delegate Task<Result<(TError? Error, TState? NextState)>> StateActionAsync
+ (object? data, CancellationToken ct);
+
+ /// <summary>
+ /// An action to execute when a state that may fill the data is reached.
+ /// Returns the data to fill.
+ /// </summary>
+ /// <param name="data">The data that led to the state.</param>
+ /// <param name="ct">The cancellation token used for cancelling the operation.</param>
+ /// <returns>The result that may or may not have succeeded.</returns>
+ public delegate Task<Result<TData>> FillDataAsync(object data, CancellationToken ct);
+
+ private readonly SemaphoreSlim _semaphore;
+
+ private readonly IDictionary<TState, (TimeSpan, TState)> _timeouts;
+ private readonly IDictionary<TState, StateActionAsync> _actions;
+ private readonly Contractor _contractor;
+ private readonly TState _defaultState;
+
+ private readonly TState _fillAtState;
+ private readonly FillDataAsync _fillData;
+
+ private TError? _error;
+ private Result? _resultError;
+
+ private TState? _waitingFor;
+ private bool _unregisterAtWaitingFor;
+ private CancellationTokenSource? _waitCancellationSource;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DefaultContract{TData, TState, TError}"/> class.
+ /// </summary>
+ /// <param name="contractor">The contractor.</param>
+ /// <param name="defaultState">The default state.</param>
+ /// <param name="fillAtState">The state to fill data at.</param>
+ /// <param name="fillData">The function to fill the data.</param>
+ /// <param name="actions">The actions to execute at each state.</param>
+ /// <param name="timeouts">The timeouts.</param>
+ public DefaultContract
+ (
+ Contractor contractor,
+ TState defaultState,
+ TState fillAtState,
+ FillDataAsync fillData,
+ IDictionary<TState, StateActionAsync> actions,
+ IDictionary<TState, (TimeSpan Timeout, TState NextState)> timeouts
+ )
+ {
+ _semaphore = new SemaphoreSlim(1, 1);
+ _timeouts = timeouts;
+ _defaultState = defaultState;
+ _contractor = contractor;
+ CurrentState = defaultState;
+
+ _actions = actions;
+ _fillData = fillData;
+ _fillAtState = fillAtState;
+ }
+
+ /// <inheritdoc />
+ public TState CurrentState { get; private set; }
+
+ /// <inheritdoc />
+ public TData? Data { get; private set; }
+
+ /// <inheritdoc />
+ public bool IsRegistered { get; private set; }
+
+ /// <inheritdoc />
+ public void Register()
+ {
+ if (!IsRegistered)
+ {
+ _contractor.Register(this);
+ IsRegistered = true;
+ }
+ }
+
+ /// <inheritdoc />
+ public void Unregister()
+ {
+ if (IsRegistered)
+ {
+ _contractor.Unregister(this);
+ IsRegistered = false;
+ }
+ }
+
+ /// <inheritdoc />
+ public async Task<Result<ContractUpdateResponse>> Update<TAny>(TAny data, CancellationToken ct = default)
+ where TAny : notnull
+ {
+ if (!_actions.ContainsKey(CurrentState))
+ {
+ throw new Exception(); // ?
+ }
+
+ var result = await _actions[CurrentState](data, ct);
+ if (!result.IsDefined(out var resultData))
+ {
+ _resultError = Result.FromError(result);
+ _waitCancellationSource?.Cancel();
+ return Result<ContractUpdateResponse>.FromError(result);
+ }
+
+ if (resultData.Error is not null)
+ {
+ _error = resultData.Error;
+ _waitCancellationSource?.Cancel();
+ return ContractUpdateResponse.Interested;
+ }
+
+ if (resultData.NextState is null)
+ {
+ return ContractUpdateResponse.NotInterested;
+ }
+
+ await SetCurrentState(resultData.NextState.Value, ct);
+
+ return await SetupNewState(data!, ct);
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> OnlyExecuteAsync(CancellationToken ct = default)
+ {
+ if (_actions.ContainsKey(_defaultState))
+ {
+ var result = await _actions[_defaultState](default, ct);
+ if (!result.IsSuccess)
+ {
+ return Result.FromError(result);
+ }
+
+ var (error, state) = result.Entity;
+
+ if (error is not null)
+ {
+ _error = error;
+ _waitCancellationSource?.Cancel();
+ }
+
+ if (state is not null)
+ {
+ await SetCurrentState(state.Value, ct);
+ var newStateResult = await SetupNewState<int?>(null, ct);
+
+ if (!newStateResult.IsSuccess)
+ {
+ return Result.FromError(newStateResult);
+ }
+ }
+ }
+
+ return Result.FromSuccess();
+ }
+
+ /// <inheritdoc />
+ public async Task<Result<TData>> WaitForAsync
+ (TState state, bool unregisterAfter = true, CancellationToken ct = default)
+ {
+ if (_fillAtState.CompareTo(state) > 0)
+ {
+ throw new InvalidOperationException
+ (
+ $"The requested state {state} does not guarantee data filled. The state that fills data is {_defaultState}"
+ );
+ }
+
+ _waitingFor = state;
+ _unregisterAtWaitingFor = unregisterAfter;
+ _waitCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(ct);
+
+ Register();
+
+ if (CurrentState.CompareTo(_defaultState) == 0)
+ {
+ var result = await OnlyExecuteAsync(ct);
+ if (!result.IsSuccess)
+ {
+ Unregister();
+ return Result<TData>.FromError(result);
+ }
+ }
+
+ try
+ {
+ await Task.Delay(-1, _waitCancellationSource.Token);
+ }
+ catch
+ {
+ // ignored
+ }
+ finally
+ {
+ if (unregisterAfter)
+ {
+ Unregister();
+ }
+ }
+
+ if (ct.IsCancellationRequested)
+ {
+ throw new TaskCanceledException();
+ }
+
+ if (_resultError is not null)
+ {
+ return Result<TData>.FromError(_resultError.Value);
+ }
+
+ if (_error is not null)
+ {
+ return new ContractError<TError>(_error.Value);
+ }
+
+ if (Data is null)
+ {
+ throw new Exception("Data was null, but shouldn't have. There is an error in DefaultContract.");
+ }
+
+ return Data;
+ }
+
+ private async Task<Result<ContractUpdateResponse>> SetupNewState<TAny>(TAny data, CancellationToken ct)
+ {
+ if (_fillAtState.CompareTo(CurrentState) == 0)
+ {
+ if (data is not null)
+ {
+ var filledResult = await _fillData(data, ct);
+
+ if (!filledResult.IsDefined(out var filled))
+ {
+ _resultError = Result.FromError(filledResult);
+ _waitCancellationSource?.Cancel();
+ return Result<ContractUpdateResponse>.FromError(filledResult);
+ }
+
+ Data = filled;
+ }
+ else
+ {
+ throw new InvalidOperationException
+ (
+ $"Got to a state {CurrentState} without data, but the state should fill data. That's not possible."
+ );
+ }
+ }
+ if (_waitingFor is not null && _waitingFor.Value.CompareTo(CurrentState) == 0)
+ {
+ IsRegistered = false; // avoid deadlock. The cancellation will trigger unregister,
+
+ // but we are inside of the lock now.
+ _waitCancellationSource?.Cancel();
+
+ if (_unregisterAtWaitingFor)
+ {
+ return ContractUpdateResponse.InterestedAndUnregister;
+ }
+ }
+
+ SetupTimeout();
+ return ContractUpdateResponse.Interested;
+ }
+
+ private void SetupTimeout()
+ {
+ if (_timeouts.ContainsKey(CurrentState))
+ {
+ var currentState = CurrentState;
+ var (timeout, state) = _timeouts[CurrentState];
+
+ Task.Run
+ (
+ async () =>
+ {
+ await Task.Delay(timeout);
+
+ if (CurrentState.CompareTo(currentState) == 0)
+ {
+ await SetCurrentState(state);
+ await SetupNewState<int?>(null!, default);
+ }
+ }
+ );
+ }
+ }
+
+ private async Task SetCurrentState(TState state, CancellationToken ct = default)
+ {
+ await _semaphore.WaitAsync(ct);
+ CurrentState = state;
+ _semaphore.Release();
+ }
+}<
\ No newline at end of file
A Core/NosSmooth.Core/Contracts/DefaultStates.cs => Core/NosSmooth.Core/Contracts/DefaultStates.cs +28 -0
@@ 0,0 1,28 @@
+//
+// DefaultStates.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.Contracts;
+
+/// <summary>
+/// Default states used for contracts.
+/// </summary>
+public enum DefaultStates
+{
+ /// <summary>
+ /// Contract has not been executed yet.
+ /// </summary>
+ None,
+
+ /// <summary>
+ /// The contract has requested a response, waiting for it.
+ /// </summary>
+ Requested,
+
+ /// <summary>
+ /// The response was obtained.
+ /// </summary>
+ ResponseObtained
+}<
\ No newline at end of file
A Core/NosSmooth.Core/Contracts/IContract.cs => Core/NosSmooth.Core/Contracts/IContract.cs +119 -0
@@ 0,0 1,119 @@
+//
+// IContract.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.Responders;
+using Remora.Results;
+
+namespace NosSmooth.Core.Contracts;
+
+/// <summary>
+/// A contract, used for executing an operation with feedback.
+/// </summary>
+/// <remarks>
+/// Do not use this type directly, use the generic one instead.
+/// </remarks>
+public interface IContract
+{
+ /// <summary>
+ /// Gets whether this contract
+ /// is registered to the contractor.
+ /// </summary>
+ public bool IsRegistered { get; }
+
+ /// <summary>
+ /// Register this contract into contractor.
+ /// </summary>
+ /// <remarks>
+ /// The contract will receive data from the contractor,
+ /// using CheckDataAsync method. This way there may be a
+ /// feedback coming back to the contract.
+ /// </remarks>
+ public void Register();
+
+ /// <summary>
+ /// Unregister this contract from contractor.
+ /// </summary>
+ public void Unregister();
+
+ /// <summary>
+ /// Update the contract with the given received data.
+ /// </summary>
+ /// <remarks>
+ /// Called from <see cref="ContractPacketResponder"/>
+ /// or similar. Used for updating the state.
+ /// The contract looks for actions that trigger updates
+ /// and in case it matches the <paramref name="data"/>,
+ /// the state is switched.
+ /// </remarks>
+ /// <param name="data">The data that were received.</param>
+ /// <typeparam name="TAny">The type of the data.</typeparam>
+ /// <param name="ct">The cancellation token used for cancelling the operation.</param>
+ /// <returns>The result that may or may not have succeeded.</returns>
+ public Task<Result<ContractUpdateResponse>> Update<TAny>(TAny data, CancellationToken ct = default)
+ where TAny : notnull;
+
+ /// <summary>
+ /// Executes the contract without registering it,
+ /// running only the initial operation.
+ /// </summary>
+ /// <remarks>
+ /// For example, to use skill, create a contract for
+ /// using a skill and call this method.
+ /// If you want to wait for response from the server,
+ /// use <see cref="WaitForAsync"/> instead.
+ /// That will register the contract and wait for response.
+ /// </remarks>
+ /// <param name="ct">The cancellation token used for cancelling the operation.</param>
+ /// <returns>The result that may or may not have succeeded.</returns>
+ public Task<Result> OnlyExecuteAsync(CancellationToken ct = default);
+}
+
+/// <summary>
+/// A contract, used for executing an operation with feedback.
+/// </summary>
+/// <remarks>
+/// Could be used for operations that may end successfully or fail
+/// after some time, with response from the server.
+///
+/// Look at <see cref="ContractBuilder"/> for example usage.
+/// </remarks>
+/// <typeparam name="TData">The data returned by the contract in case of success.</typeparam>
+/// <typeparam name="TState">Type containing the states of the contract.</typeparam>
+public interface IContract<TData, TState> : IContract
+ where TData : notnull
+ where TState : IComparable
+{
+ /// <summary>
+ /// Gets the current state of the contract.
+ /// </summary>
+ /// <remarks>
+ /// To wait for any state, see <see cref="WaitForAsync"/>.
+ /// </remarks>
+ public TState CurrentState { get; }
+
+ /// <summary>
+ /// Gets the data of the contract obtained from packets/.
+ /// </summary>
+ /// <remarks>
+ /// This won't be filled in case the contract
+ /// is not registered.
+ /// </remarks>
+ public TData? Data { get; }
+
+ /// <summary>
+ /// Register to contractor and wait for the given state.
+ /// Execute the initial action.
+ /// </summary>
+ /// <param name="state">The state to wait for.</param>
+ /// <param name="unregisterAfter">Whether to unregister the contract from the contractor after the state is reached. The contract won't be updated anymore.</param>
+ /// <param name="ct">The cancellation token used for cancelling the operation.</param>
+ /// <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);
+}<
\ No newline at end of file
A Core/NosSmooth.Core/Contracts/NoErrors.cs => Core/NosSmooth.Core/Contracts/NoErrors.cs +14 -0
@@ 0,0 1,14 @@
+//
+// NoErrors.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.Contracts;
+
+/// <summary>
+/// Empty enum for contracts without errors.
+/// </summary>
+public enum NoErrors
+{
+}<
\ No newline at end of file
A Core/NosSmooth.Core/Contracts/Responders/ContractPacketResponder.cs => Core/NosSmooth.Core/Contracts/Responders/ContractPacketResponder.cs +35 -0
@@ 0,0 1,35 @@
+//
+// ContractPacketResponder.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.Threading;
+using System.Threading.Tasks;
+using NosSmooth.Core.Packets;
+using NosSmooth.Packets;
+using Remora.Results;
+
+namespace NosSmooth.Core.Contracts.Responders;
+
+/// <summary>
+/// A responder that calls Contractor update.
+/// </summary>
+public class ContractPacketResponder : IEveryPacketResponder
+{
+ private readonly Contractor _contractor;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ContractPacketResponder"/> class.
+ /// </summary>
+ /// <param name="contractor">The contractor.</param>
+ public ContractPacketResponder(Contractor contractor)
+ {
+ _contractor = contractor;
+ }
+
+ /// <inheritdoc />
+ public Task<Result> Respond<TPacket>(PacketEventArgs<TPacket> packetArgs, CancellationToken ct = default)
+ where TPacket : IPacket
+ => _contractor.Update(packetArgs.Packet, ct);
+}<
\ No newline at end of file
M Core/NosSmooth.Core/Extensions/ServiceCollectionExtensions.cs => Core/NosSmooth.Core/Extensions/ServiceCollectionExtensions.cs +6 -0
@@ 12,6 12,8 @@ using NosSmooth.Core.Client;
using NosSmooth.Core.Commands;
using NosSmooth.Core.Commands.Control;
using NosSmooth.Core.Commands.Walking;
+using NosSmooth.Core.Contracts;
+using NosSmooth.Core.Contracts.Responders;
using NosSmooth.Core.Packets;
using NosSmooth.Core.Stateful;
using NosSmooth.PacketSerializer.Extensions;
@@ 39,6 41,10 @@ public static class ServiceCollectionExtensions
serviceCollection.AddPacketSerialization();
serviceCollection.AddSingleton<CommandProcessor>();
+ serviceCollection
+ .AddSingleton<Contractor>()
+ .AddPacketResponder<ContractPacketResponder>();
+
return serviceCollection;
}
R Core/NosSmooth.Game/Apis/NostaleChatPacketApi.cs => Core/NosSmooth.Game/Apis/Safe/NostaleChatApi.cs +5 -5
@@ 1,5 1,5 @@
//
-// NostaleChatPacketApi.cs
+// NostaleChatApi.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.
@@ 10,21 10,21 @@ using NosSmooth.Packets.Enums.Entities;
using NosSmooth.Packets.Server.Chat;
using Remora.Results;
-namespace NosSmooth.Game.Apis;
+namespace NosSmooth.Game.Apis.Safe;
/// <summary>
/// Packet api for sending and receiving messages.
/// </summary>
-public class NostaleChatPacketApi
+public class NostaleChatApi
{
// TODO: check length of the messages
private readonly INostaleClient _client;
/// <summary>
- /// Initializes a new instance of the <see cref="NostaleChatPacketApi"/> class.
+ /// Initializes a new instance of the <see cref="NostaleChatApi"/> class.
/// </summary>
/// <param name="client">The nostale client.</param>
- public NostaleChatPacketApi(INostaleClient client)
+ public NostaleChatApi(INostaleClient client)
{
_client = client;
}
R Core/NosSmooth.Game/Apis/NostaleMapPacketApi.cs => Core/NosSmooth.Game/Apis/Safe/NostaleMapApi.cs +13 -74
@@ 1,55 1,41 @@
//
-// NostaleMapPacketApi.cs
+// NostaleMapApi.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.Game.Attributes;
+using NosSmooth.Game.Apis.Unsafe;
using NosSmooth.Game.Data.Entities;
-using NosSmooth.Game.Data.Items;
using NosSmooth.Game.Errors;
-using NosSmooth.Packets.Client.Inventory;
-using NosSmooth.Packets.Client.Movement;
-using NosSmooth.Packets.Enums.Entities;
using Remora.Results;
-namespace NosSmooth.Game.Apis;
+namespace NosSmooth.Game.Apis.Safe;
/// <summary>
/// Packet api for managing maps in inventory.
/// </summary>
-public class NostaleMapPacketApi
+public class NostaleMapApi
{
+ private readonly Game _game;
+ private readonly UnsafeMapApi _unsafeMapApi;
+
/// <summary>
/// The range the player may pick up items in.
/// </summary>
public static short PickUpRange => 5;
- private readonly Game _game;
- private readonly INostaleClient _client;
-
/// <summary>
- /// Initializes a new instance of the <see cref="NostaleMapPacketApi"/> class.
+ /// Initializes a new instance of the <see cref="NostaleMapApi"/> class.
/// </summary>
/// <param name="game">The game.</param>
- /// <param name="client">The client.</param>
- public NostaleMapPacketApi(Game game, INostaleClient client)
+ /// <param name="unsafeMapApi">The unsafe map api.</param>
+ public NostaleMapApi(Game game, UnsafeMapApi unsafeMapApi)
{
_game = game;
- _client = client;
+ _unsafeMapApi = unsafeMapApi;
}
/// <summary>
- /// Use the given portal.
- /// </summary>
- /// <param name="ct">The cancellation token for cancelling the operation.</param>
- /// <returns>A result that may or may not have succeeded.</returns>
- [Unsafe("Portal position not checked.")]
- public Task<Result> UsePortalAsync(CancellationToken ct = default)
- => _client.SendPacketAsync(new PreqPacket(), ct);
-
- /// <summary>
/// Pick up the given item.
/// </summary>
/// <remarks>
@@ 86,28 72,7 @@ public class NostaleMapPacketApi
return new NotInRangeError("Character", characterPosition.Value, itemPosition.Value, PickUpRange);
}
- return await CharacterPickUpAsync(item.Id, ct);
- }
-
- /// <summary>
- /// Pick up the given item by character.
- /// </summary>
- /// <remarks>
- /// Unsafe, does not check anything.
- /// </remarks>
- /// <param name="itemId">The id of the item.</param>
- /// <param name="ct">The cancellation token used for cancelling the operation.</param>
- /// <returns>A result that may or may not have succeeded.</returns>
- [Unsafe("Nor character distance, nor the existence of item is checked.")]
- public async Task<Result> CharacterPickUpAsync(long itemId, CancellationToken ct = default)
- {
- var character = _game.Character;
- if (character is null)
- {
- return new NotInitializedError("Character");
- }
-
- return await _client.SendPacketAsync(new GetPacket(EntityType.Player, character.Id, itemId), ct);
+ return await _unsafeMapApi.CharacterPickUpAsync(item.Id, ct);
}
/// <summary>
@@ 160,33 125,7 @@ public class NostaleMapPacketApi
return new NotInRangeError("Pet", petPosition.Value, itemPosition.Value, PickUpRange);
}
- return await PetPickUpAsync(item.Id, ct);
+ return await _unsafeMapApi.PetPickUpAsync(item.Id, ct);
}
- /// <summary>
- /// Pick up the given item by pet.
- /// </summary>
- /// <remarks>
- /// Unsafe, does not check anything.
- /// </remarks>
- /// <param name="itemId">The id of the item.</param>
- /// <param name="ct">The cancellation token used for cancelling the operation.</param>
- /// <returns>A result that may or may not have succeeded.</returns>
- [Unsafe("Nor pet distance to item nor whether the item exists is checked.")]
- public async Task<Result> PetPickUpAsync(long itemId, CancellationToken ct = default)
- {
- var mates = _game.Mates;
- if (mates is null)
- {
- return new NotInitializedError("Game.Mates");
- }
-
- var pet = mates.CurrentPet;
- if (pet is null)
- {
- return new NotInitializedError("Game.Mates.CurrentPet");
- }
-
- return await _client.SendPacketAsync(new GetPacket(EntityType.Player, pet.Pet.MateId, itemId), ct);
- }
}=
\ No newline at end of file
A Core/NosSmooth.Game/Apis/Safe/NostaleSkillsApi.cs => Core/NosSmooth.Game/Apis/Safe/NostaleSkillsApi.cs +426 -0
@@ 0,0 1,426 @@
+//
+// NostaleSkillsApi.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.Contracts;
+using NosSmooth.Data.Abstractions.Enums;
+using NosSmooth.Game.Apis.Unsafe;
+using NosSmooth.Game.Contracts;
+using NosSmooth.Game.Data.Characters;
+using NosSmooth.Game.Data.Entities;
+using NosSmooth.Game.Data.Info;
+using NosSmooth.Game.Errors;
+using NosSmooth.Game.Events.Battle;
+using NosSmooth.Packets.Client.Battle;
+using Remora.Results;
+
+namespace NosSmooth.Game.Apis.Safe;
+
+/// <summary>
+/// A safe NosTale api for using character skills.
+/// </summary>
+public class NostaleSkillsApi
+{
+ private readonly Game _game;
+ private readonly INostaleClient _client;
+ private readonly Contractor _contractor;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NostaleSkillsApi"/> class.
+ /// </summary>
+ /// <param name="game">The game.</param>
+ /// <param name="client">The NosTale client.</param>
+ /// <param name="contractor">The contractor.</param>
+ public NostaleSkillsApi(Game game, INostaleClient client, Contractor contractor)
+ {
+ _game = game;
+ _client = client;
+ _contractor = contractor;
+ }
+
+ /// <summary>
+ /// Use the given (targetable) skill on character himself.
+ /// </summary>
+ /// <param name="skill">The skill to use.</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Task<Result> UseSkillOnCharacter
+ (
+ Skill skill,
+ CancellationToken ct = default
+ )
+ {
+ var character = _game.Character;
+ if (character is null)
+ {
+ return Task.FromResult<Result>(new NotInitializedError("Game.Character"));
+ }
+
+ var skills = _game.Skills;
+ if (skills is null)
+ {
+ return Task.FromResult<Result>(new NotInitializedError("Game.Skills"));
+ }
+
+ if (skill.IsOnCooldown)
+ {
+ return Task.FromResult<Result>(new SkillOnCooldownError(skill));
+ }
+
+ if (skill.Info is null)
+ {
+ return Task.FromResult<Result>(new NotInitializedError("skill info"));
+ }
+
+ if (skill.Info.TargetType is not(TargetType.Self or TargetType.SelfOrTarget))
+ {
+ return Task.FromResult<Result>(new WrongSkillTargetError(skill, character));
+ }
+
+ return _client.SendPacketAsync
+ (
+ new UseSkillPacket
+ (
+ skill.Info.CastId,
+ character.Type,
+ character.Id,
+ null,
+ null
+ ),
+ ct
+ );
+ }
+
+ /// <summary>
+ /// Use the given (targetable) skill on specified entity.
+ /// </summary>
+ /// <remarks>
+ /// The skill won't be used if it is on cooldown.
+ /// For skills that can be used only on self, use <paramref name="entity"/> of the character.
+ /// For skills that cannot be targeted on an entity, proceed to UseSkillAt.
+ /// </remarks>
+ /// <param name="skill">The skill to use.</param>
+ /// <param name="entity">The entity to use the skill on.</param>
+ /// <param name="mapX">The x coordinate on the map. (Used for non targeted dashes etc., says where the dash will be to.)</param>
+ /// <param name="mapY">The y coordinate on the map. (Used for non targeted dashes etc., says where the dash will be to.)</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Task<Result> UseSkillOn
+ (
+ Skill skill,
+ ILivingEntity entity,
+ short? mapX = default,
+ short? mapY = default,
+ CancellationToken ct = default
+ )
+ {
+ if (entity == _game.Character)
+ {
+ return UseSkillOnCharacter(skill, ct);
+ }
+
+ var skills = _game.Skills;
+ if (skills is null)
+ {
+ return Task.FromResult<Result>(new NotInitializedError("Game.Skills"));
+ }
+
+ if (skill.IsOnCooldown)
+ {
+ return Task.FromResult<Result>(new SkillOnCooldownError(skill));
+ }
+
+ if (skill.Info is null)
+ {
+ return Task.FromResult<Result>(new NotInitializedError("skill info"));
+ }
+
+ if (skill.Info.TargetType is not(TargetType.Target or TargetType.SelfOrTarget))
+ {
+ return Task.FromResult<Result>(new WrongSkillTargetError(skill, entity));
+ }
+
+ var entityPosition = entity.Position;
+ if (entityPosition is null)
+ {
+ return Task.FromResult<Result>(new NotInitializedError("entity position"));
+ }
+
+ var characterPosition = _game.Character?.Position;
+ if (characterPosition is null)
+ {
+ return Task.FromResult<Result>(new NotInitializedError("character position"));
+ }
+
+ if (!entityPosition.Value.IsInRange(characterPosition.Value, skill.Info.Range))
+ {
+ return Task.FromResult<Result>
+ (
+ new NotInRangeError
+ (
+ "Character",
+ characterPosition.Value,
+ entityPosition.Value,
+ skill.Info.Range
+ )
+ );
+ }
+
+ if (mapX != null && mapY != null)
+ {
+ var mapPosition = new Position(mapX.Value, mapY.Value);
+ if (!mapPosition.IsInRange(characterPosition.Value, skill.Info.Range))
+ {
+ return Task.FromResult<Result>
+ (
+ new NotInRangeError
+ (
+ "Character",
+ characterPosition.Value,
+ mapPosition,
+ skill.Info.Range
+ )
+ );
+ }
+ }
+
+ return _client.SendPacketAsync
+ (
+ new UseSkillPacket
+ (
+ skill.Info.CastId,
+ entity.Type,
+ entity.Id,
+ mapX,
+ mapY
+ ),
+ ct
+ );
+ }
+
+ /// <summary>
+ /// Use the given (targetable) skill on specified entity.
+ /// </summary>
+ /// <remarks>
+ /// For skills that can be used only on self, use <paramref name="entityId"/> of the character.
+ /// For skills that cannot be targeted on an entity, proceed to UseSkillAt.
+ /// </remarks>
+ /// <param name="skill">The skill to use.</param>
+ /// <param name="entityId">The id of the entity to use the skill on.</param>
+ /// <param name="mapX">The x coordinate on the map. (Used for non targeted dashes etc., says where the dash will be to.)</param>
+ /// <param name="mapY">The y coordinate on the map. (Used for non targeted dashes etc., says where the dash will be to.)</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Task<Result> UseSkillOn
+ (
+ Skill skill,
+ long entityId,
+ short? mapX = default,
+ short? mapY = default,
+ CancellationToken ct = default
+ )
+ {
+ var map = _game.CurrentMap;
+ if (map is null)
+ {
+ return Task.FromResult<Result>(new NotInitializedError("Game.Map"));
+ }
+
+ var entity = map.Entities.GetEntity<ILivingEntity>(entityId);
+ if (entity is null)
+ {
+ return Task.FromResult<Result>(new NotFoundError($"Entity with id {entityId} was not found on the map."));
+ }
+
+ return UseSkillOn
+ (
+ skill,
+ entity,
+ mapX,
+ mapY,
+ ct
+ );
+ }
+
+ /// <summary>
+ /// Use the given (aoe) skill on the specified place.
+ /// </summary>
+ /// <remarks>
+ /// For skills that can have targets, proceed to UseSkillOn.
+ /// </remarks>
+ /// <param name="skill">The skill to use.</param>
+ /// <param name="mapX">The x coordinate to use the skill at.</param>
+ /// <param name="mapY">The y coordinate to use the skill at.</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Task<Result> UseSkillAt
+ (
+ Skill skill,
+ short mapX,
+ short mapY,
+ CancellationToken ct = default
+ )
+ {
+ var skills = _game.Skills;
+ if (skills is null)
+ {
+ return Task.FromResult<Result>(new NotInitializedError("Game.Skills"));
+ }
+
+ if (skill.IsOnCooldown)
+ {
+ return Task.FromResult<Result>(new SkillOnCooldownError(skill));
+ }
+
+ if (skill.Info is null)
+ {
+ return Task.FromResult<Result>(new NotInitializedError("skill info"));
+ }
+
+ if (skill.Info.TargetType is not TargetType.NoTarget)
+ {
+ return Task.FromResult<Result>(new WrongSkillTargetError(skill, null));
+ }
+
+ var characterPosition = _game.Character?.Position;
+ if (characterPosition is null)
+ {
+ return Task.FromResult<Result>(new NotInitializedError("character position"));
+ }
+
+ var target = new Position(mapX, mapY);
+ if (!target.IsInRange(characterPosition.Value, skill.Info.Range))
+ {
+ return Task.FromResult<Result>
+ (
+ new NotInRangeError
+ (
+ "Character",
+ characterPosition.Value,
+ target,
+ skill.Info.Range
+ )
+ );
+ }
+
+ return _client.SendPacketAsync
+ (
+ new UseAOESkillPacket(skill.Info.CastId, mapX, mapY),
+ ct
+ );
+ }
+
+ /// <summary>
+ /// Creates a contract for using a skill on character himself.
+ /// </summary>
+ /// <param name="skill">The skill to use.</param>
+ /// <returns>The contract or an error.</returns>
+ public Result<IContract<SkillUsedEvent, UseSkillStates>> ContractUseSkillOnCharacter
+ (
+ Skill skill
+ )
+ {
+ var characterId = _game?.Character?.Id;
+ if (characterId is null)
+ {
+ return new NotInitializedError("Game.Character");
+ }
+
+ return Result<IContract<SkillUsedEvent, UseSkillStates>>.FromSuccess
+ (
+ UnsafeSkillsApi.CreateUseSkillContract
+ (
+ _contractor,
+ skill.SkillVNum,
+ characterId.Value,
+ ct => UseSkillOnCharacter
+ (
+ skill,
+ ct
+ )
+ )
+ );
+ }
+
+ /// <summary>
+ /// Creates a contract for using a skill on the given entity.
+ /// </summary>
+ /// <param name="skill">The skill to use.</param>
+ /// <param name="entity">The entity to use the skill on.</param>
+ /// <param name="mapX">The x coordinate to use the skill at.</param>
+ /// <param name="mapY">The y coordinate to use the skill at.</param>
+ /// <returns>The contract or an error.</returns>
+ public Result<IContract<SkillUsedEvent, UseSkillStates>> ContractUseSkillOn
+ (
+ Skill skill,
+ ILivingEntity entity,
+ short? mapX = default,
+ short? mapY = default
+ )
+ {
+ var characterId = _game?.Character?.Id;
+ if (characterId is null)
+ {
+ return new NotInitializedError("Game.Character");
+ }
+
+ return Result<IContract<SkillUsedEvent, UseSkillStates>>.FromSuccess
+ (
+ UnsafeSkillsApi.CreateUseSkillContract
+ (
+ _contractor,
+ skill.SkillVNum,
+ characterId.Value,
+ ct => UseSkillOn
+ (
+ skill,
+ entity,
+ mapX,
+ mapY,
+ ct
+ )
+ )
+ );
+ }
+
+ /// <summary>
+ /// Creates a contract for using a skill at the given location.
+ /// </summary>
+ /// <param name="skill">The skill to use.</param>
+ /// <param name="mapX">The x coordinate to use the skill at.</param>
+ /// <param name="mapY">The y coordinate to use the skill at.</param>
+ /// <returns>The contract or an error.</returns>
+ public Result<IContract<SkillUsedEvent, UseSkillStates>> ContractUseSkillAt
+ (
+ Skill skill,
+ short mapX,
+ short mapY
+ )
+ {
+ var characterId = _game?.Character?.Id;
+ if (characterId is null)
+ {
+ return new NotInitializedError("Game.Character");
+ }
+
+ return Result<IContract<SkillUsedEvent, UseSkillStates>>.FromSuccess
+ (
+ UnsafeSkillsApi.CreateUseSkillContract
+ (
+ _contractor,
+ skill.SkillVNum,
+ characterId.Value,
+ ct => UseSkillAt
+ (
+ skill,
+ mapX,
+ mapY,
+ ct
+ )
+ )
+ );
+ }
+}<
\ No newline at end of file
R Core/NosSmooth.Game/Apis/NostaleInventoryPacketApi.cs => Core/NosSmooth.Game/Apis/Unsafe/UnsafeInventoryApi.cs +56 -5
@@ 1,30 1,40 @@
//
-// NostaleInventoryPacketApi.cs
+// UnsafeInventoryApi.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.Data;
using NosSmooth.Core.Client;
+using NosSmooth.Core.Contracts;
+using NosSmooth.Game.Data.Entities;
+using NosSmooth.Game.Data.Inventory;
+using NosSmooth.Game.Events.Entities;
using NosSmooth.Packets.Client.Inventory;
using NosSmooth.Packets.Enums.Inventory;
+using NosSmooth.Packets.Server.Maps;
+using OneOf.Types;
using Remora.Results;
-namespace NosSmooth.Game.Apis;
+namespace NosSmooth.Game.Apis.Unsafe;
/// <summary>
/// Packet api for managing items in inventory.
/// </summary>
-public class NostaleInventoryPacketApi
+public class UnsafeInventoryApi
{
private readonly INostaleClient _client;
+ private readonly Contractor _contractor;
/// <summary>
- /// Initializes a new instance of the <see cref="NostaleInventoryPacketApi"/> class.
+ /// Initializes a new instance of the <see cref="UnsafeInventoryApi"/> class.
/// </summary>
/// <param name="client">The nostale client.</param>
- public NostaleInventoryPacketApi(INostaleClient client)
+ /// <param name="contractor">The contractor.</param>
+ public UnsafeInventoryApi(INostaleClient client, Contractor contractor)
{
_client = client;
+ _contractor = contractor;
}
/// <summary>
@@ 45,6 55,47 @@ public class NostaleInventoryPacketApi
=> _client.SendPacketAsync(new PutPacket(bag, slot, amount), ct);
/// <summary>
+ /// Creates a contract for dropping an item.
+ /// Returns the ground item that was thrown on the ground.
+ /// </summary>
+ /// <param name="bag">The inventory bag.</param>
+ /// <param name="slot">The inventory slot.</param>
+ /// <param name="amount">The amount to drop.</param>
+ /// <returns>A contract representing the drop operation.</returns>
+ public IContract<GroundItem, DefaultStates> ContractDropItem
+ (
+ BagType bag,
+ InventorySlot slot,
+ short amount
+ )
+ {
+ // TODO: confirm dialog.
+ return new ContractBuilder<GroundItem, DefaultStates, NoErrors>(_contractor, DefaultStates.None)
+ .SetMoveAction
+ (
+ DefaultStates.None,
+ async (_, ct) =>
+ {
+ await DropItemAsync(bag, slot.Slot, amount, ct);
+ return true;
+ },
+ DefaultStates.Requested
+ )
+ .SetMoveFilter<ItemDroppedEvent>
+ (
+ DefaultStates.Requested,
+ data => data.Item.Amount == amount && data.Item.VNum == slot.Item?.ItemVNum,
+ DefaultStates.ResponseObtained
+ )
+ .SetFillData<ItemDroppedEvent>
+ (
+ DefaultStates.Requested,
+ data => data.Item
+ )
+ .Build();
+ }
+
+ /// <summary>
/// Move the given item within one bag.
/// </summary>
/// <param name="bag">The bag the item is in.</param>
A Core/NosSmooth.Game/Apis/Unsafe/UnsafeMapApi.cs => Core/NosSmooth.Game/Apis/Unsafe/UnsafeMapApi.cs +88 -0
@@ 0,0 1,88 @@
+//
+// UnsafeMapApi.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.Game.Errors;
+using NosSmooth.Packets.Client.Inventory;
+using NosSmooth.Packets.Client.Movement;
+using NosSmooth.Packets.Enums.Entities;
+using Remora.Results;
+
+namespace NosSmooth.Game.Apis.Unsafe;
+
+/// <summary>
+/// Packet api for managing maps in inventory.
+/// </summary>
+public class UnsafeMapApi
+{
+ private readonly Game _game;
+ private readonly INostaleClient _client;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UnsafeMapApi"/> class.
+ /// </summary>
+ /// <param name="game">The game.</param>
+ /// <param name="client">The client.</param>
+ public UnsafeMapApi(Game game, INostaleClient client)
+ {
+ _game = game;
+ _client = client;
+ }
+
+ /// <summary>
+ /// Use the given portal.
+ /// </summary>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Task<Result> UsePortalAsync(CancellationToken ct = default)
+ => _client.SendPacketAsync(new PreqPacket(), ct);
+
+ /// <summary>
+ /// Pick up the given item by character.
+ /// </summary>
+ /// <remarks>
+ /// Unsafe, does not check anything.
+ /// </remarks>
+ /// <param name="itemId">The id of the item.</param>
+ /// <param name="ct">The cancellation token used for cancelling the operation.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public async Task<Result> CharacterPickUpAsync(long itemId, CancellationToken ct = default)
+ {
+ var character = _game.Character;
+ if (character is null)
+ {
+ return new NotInitializedError("Character");
+ }
+
+ return await _client.SendPacketAsync(new GetPacket(EntityType.Player, character.Id, itemId), ct);
+ }
+
+ /// <summary>
+ /// Pick up the given item by pet.
+ /// </summary>
+ /// <remarks>
+ /// Unsafe, does not check anything.
+ /// </remarks>
+ /// <param name="itemId">The id of the item.</param>
+ /// <param name="ct">The cancellation token used for cancelling the operation.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public async Task<Result> PetPickUpAsync(long itemId, CancellationToken ct = default)
+ {
+ var mates = _game.Mates;
+ if (mates is null)
+ {
+ return new NotInitializedError("Game.Mates");
+ }
+
+ var pet = mates.CurrentPet;
+ if (pet is null)
+ {
+ return new NotInitializedError("Game.Mates.CurrentPet");
+ }
+
+ return await _client.SendPacketAsync(new GetPacket(EntityType.Player, pet.Pet.MateId, itemId), ct);
+ }
+}<
\ No newline at end of file
R Core/NosSmooth.Game/Apis/NostaleMatePacketApi.cs => Core/NosSmooth.Game/Apis/Unsafe/UnsafeMateApi.cs +5 -6
@@ 1,30 1,29 @@
//
-// NostaleMatePacketApi.cs
+// UnsafeMateApi.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.Game.Attributes;
using NosSmooth.Packets.Client;
using NosSmooth.Packets.Enums.Entities;
using NosSmooth.Packets.Enums.NRun;
using Remora.Results;
-namespace NosSmooth.Game.Apis;
+namespace NosSmooth.Game.Apis.Unsafe;
/// <summary>
/// Packet api for managing mates, company, stay, sending them back.
/// </summary>
-public class NostaleMatePacketApi
+public class UnsafeMateApi
{
private readonly INostaleClient _client;
/// <summary>
- /// Initializes a new instance of the <see cref="NostaleMatePacketApi"/> class.
+ /// Initializes a new instance of the <see cref="UnsafeMateApi"/> class.
/// </summary>
/// <param name="client">The client.</param>
- public NostaleMatePacketApi(INostaleClient client)
+ public UnsafeMateApi(INostaleClient client)
{
_client = client;
}
R Core/NosSmooth.Game/Apis/NostaleMateSkillsPacketApi.cs => Core/NosSmooth.Game/Apis/Unsafe/UnsafeMateSkillsApi.cs +5 -5
@@ 1,5 1,5 @@
//
-// NostaleMateSkillsPacketApi.cs
+// UnsafeMateSkillsApi.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.
@@ 9,20 9,20 @@ using NosSmooth.Packets.Client.Battle;
using NosSmooth.Packets.Enums.Entities;
using Remora.Results;
-namespace NosSmooth.Game.Apis;
+namespace NosSmooth.Game.Apis.Unsafe;
/// <summary>
/// Packet api for using mate skills.
/// </summary>
-public class NostaleMateSkillsPacketApi
+public class UnsafeMateSkillsApi
{
private readonly INostaleClient _client;
/// <summary>
- /// Initializes a new instance of the <see cref="NostaleMateSkillsPacketApi"/> class.
+ /// Initializes a new instance of the <see cref="UnsafeMateSkillsApi"/> class.
/// </summary>
/// <param name="client">The client.</param>
- public NostaleMateSkillsPacketApi(INostaleClient client)
+ public UnsafeMateSkillsApi(INostaleClient client)
{
_client = client;
}
R Core/NosSmooth.Game/Apis/NostaleSkillsPacketApi.cs => Core/NosSmooth.Game/Apis/Unsafe/UnsafeSkillsApi.cs +151 -24
@@ 1,36 1,43 @@
//
-// NostaleSkillsPacketApi.cs
+// UnsafeSkillsApi.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.Contracts;
+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.Client.Battle;
using NosSmooth.Packets.Enums.Entities;
+using NosSmooth.Packets.Server.Skills;
using Remora.Results;
-namespace NosSmooth.Game.Apis;
+namespace NosSmooth.Game.Apis.Unsafe;
/// <summary>
/// Packet api for using character skills.
/// </summary>
-public class NostaleSkillsPacketApi
+public class UnsafeSkillsApi
{
private readonly INostaleClient _client;
private readonly Game _game;
+ private readonly Contractor _contractor;
/// <summary>
- /// Initializes a new instance of the <see cref="NostaleSkillsPacketApi"/> class.
+ /// Initializes a new instance of the <see cref="UnsafeSkillsApi"/> class.
/// </summary>
/// <param name="client">The nostale client.</param>
/// <param name="game">The game.</param>
- public NostaleSkillsPacketApi(INostaleClient client, Game game)
+ /// <param name="contractor">The contractor.</param>
+ public UnsafeSkillsApi(INostaleClient client, Game game, Contractor contractor)
{
_client = client;
_game = game;
+ _contractor = contractor;
}
/// <summary>
@@ 150,7 157,6 @@ public class NostaleSkillsPacketApi
/// Use the given (targetable) skill on specified entity.
/// </summary>
/// <remarks>
- /// The skill won't be used if it is on cooldown.
/// For skills that can be used only on self, use <paramref name="entity"/> of the character.
/// For skills that cannot be targeted on an entity, proceed to UseSkillAt.
/// </remarks>
@@ 169,11 175,6 @@ public class NostaleSkillsPacketApi
CancellationToken ct = default
)
{
- if (skill.IsOnCooldown)
- {
- return Task.FromResult<Result>(new SkillOnCooldownError(skill));
- }
-
if (skill.Info is null)
{
return Task.FromResult<Result>(new NotInitializedError("skill info"));
@@ 217,12 218,8 @@ public class NostaleSkillsPacketApi
CancellationToken ct = default
)
{
- if (skill.IsOnCooldown)
- {
- return Task.FromResult<Result>(new SkillOnCooldownError(skill));
- }
-
- if (skill.Info is null)
+ var info = skill.Info;
+ if (info is null)
{
return Task.FromResult<Result>(new NotInitializedError("skill info"));
}
@@ 231,7 228,7 @@ public class NostaleSkillsPacketApi
(
new UseSkillPacket
(
- skill.Info.CastId,
+ info.CastId,
entityType,
entityId,
mapX,
@@ 247,14 244,14 @@ public class NostaleSkillsPacketApi
/// <remarks>
/// For skills that can have targets, proceed to UseSkillOn.
/// </remarks>
- /// <param name="skillVNum">The id of the skill.</param>
+ /// <param name="castId">The id of the skill.</param>
/// <param name="mapX">The x coordinate to use the skill at.</param>
/// <param name="mapY">The y coordinate to use the skill at.</param>
/// <param name="ct">The cancellation token for cancelling the operation.</param>
/// <returns>A result that may or may not have succeeded.</returns>
public Task<Result> UseSkillAt
(
- long skillVNum,
+ long castId,
short mapX,
short mapY,
CancellationToken ct = default
@@ 262,7 259,7 @@ public class NostaleSkillsPacketApi
{
return _client.SendPacketAsync
(
- new UseAOESkillPacket(skillVNum, mapX, mapY),
+ new UseAOESkillPacket(castId, mapX, mapY),
ct
);
}
@@ 286,15 283,145 @@ public class NostaleSkillsPacketApi
CancellationToken ct = default
)
{
- if (skill.IsOnCooldown)
+ var info = skill.Info;
+ if (info is null)
{
- return Task.FromResult<Result>(new SkillOnCooldownError(skill));
+ return Task.FromResult<Result>(new NotInitializedError("skill info"));
}
return _client.SendPacketAsync
(
- new UseAOESkillPacket(skill.SkillVNum, mapX, mapY),
+ new UseAOESkillPacket(info.CastId, mapX, mapY),
ct
);
}
+
+ /// <summary>
+ /// Creates a contract for using a skill on the given entity.
+ /// </summary>
+ /// <param name="skill">The skill to use.</param>
+ /// <param name="entityId">The id of the entity to use the skill on.</param>
+ /// <param name="entityType">The type of the supplied entity.</param>
+ /// <param name="mapX">The x coordinate on the map. (Used for non targeted dashes etc., says where the dash will be to.)</param>
+ /// <param name="mapY">The y coordinate on the map. (Used for non targeted dashes etc., says where the dash will be to.)</param>
+ /// <returns>The contract or an error.</returns>
+ public Result<IContract<SkillUsedEvent, UseSkillStates>> ContractUseSkillOn
+ (
+ Skill skill,
+ long entityId,
+ EntityType entityType,
+ short? mapX = default,
+ short? mapY = default
+ )
+ {
+ var characterId = _game?.Character?.Id;
+ if (characterId is null)
+ {
+ return new NotInitializedError("Game.Character");
+ }
+
+ if (skill.Info is null)
+ {
+ return new NotInitializedError("skill info");
+ }
+
+ return Result<IContract<SkillUsedEvent, UseSkillStates>>.FromSuccess
+ (
+ CreateUseSkillContract
+ (
+ _contractor,
+ skill.SkillVNum,
+ characterId.Value,
+ ct => UseSkillOn
+ (
+ skill.Info.CastId,
+ entityId,
+ entityType,
+ mapX,
+ mapY,
+ ct
+ )
+ )
+ );
+ }
+
+ /// <summary>
+ /// Creates a contract for using a skill at the given location.
+ /// </summary>
+ /// <param name="skill">The skill to use.</param>
+ /// <param name="mapX">The x coordinate to use the skill at.</param>
+ /// <param name="mapY">The y coordinate to use the skill at.</param>
+ /// <returns>The contract or an error.</returns>
+ public Result<IContract<SkillUsedEvent, UseSkillStates>> ContractUseSkillAt
+ (
+ Skill skill,
+ short mapX,
+ short mapY
+ )
+ {
+ var characterId = _game?.Character?.Id;
+ if (characterId is null)
+ {
+ return new NotInitializedError("Game.Character");
+ }
+
+ if (skill.Info is null)
+ {
+ return new NotInitializedError("skill info");
+ }
+
+ return Result<IContract<SkillUsedEvent, UseSkillStates>>.FromSuccess
+ (
+ CreateUseSkillContract
+ (
+ _contractor,
+ skill.SkillVNum,
+ characterId.Value,
+ ct =>
+ {
+ return UseSkillAt(skill.Info.CastId, mapX, mapY, ct);
+ }
+ )
+ );
+ }
+
+ /// <summary>
+ /// Creates a use skill contract,
+ /// casting the skill using the given action.
+ /// </summary>
+ /// <param name="contractor">The contractor to register the contract at.</param>
+ /// <param name="skillVNum">The vnum of the casting skill.</param>
+ /// <param name="characterId">The id of the caster, character.</param>
+ /// <param name="useSkill">The used skill event.</param>
+ /// <returns>A contract for using the given skill.</returns>
+ public static IContract<SkillUsedEvent, UseSkillStates> CreateUseSkillContract
+ (
+ Contractor contractor,
+ int skillVNum,
+ long characterId,
+ Func<CancellationToken, Task<Result>> useSkill
+ )
+ {
+ return new ContractBuilder<SkillUsedEvent, UseSkillStates, UseSkillErrors>(contractor, UseSkillStates.None)
+ .SetMoveAction
+ (
+ UseSkillStates.None,
+ async (data, ct) => (await useSkill(ct)).Map(true),
+ UseSkillStates.SkillUseRequested
+ )
+ .SetMoveFilter<SkillUsedEvent>
+ (
+ UseSkillStates.SkillUseRequested,
+ data => data.Skill.SkillVNum == skillVNum && data.Caster.Id == characterId,
+ UseSkillStates.SkillUsedResponse
+ )
+ .SetFillData<SkillUsedEvent>
+ (
+ UseSkillStates.SkillUsedResponse,
+ skillUseEvent => skillUseEvent
+ )
+ .SetError<CancelPacket>(UseSkillStates.SkillUseRequested, _ => UseSkillErrors.Unknown)
+ .SetTimeout(UseSkillStates.SkillUsedResponse, TimeSpan.FromSeconds(1), UseSkillStates.CharacterRestored)
+ .Build();
+ }
}=
\ No newline at end of file
D Core/NosSmooth.Game/Attributes/UnsafeAttribute.cs => Core/NosSmooth.Game/Attributes/UnsafeAttribute.cs +0 -28
@@ 1,28 0,0 @@
-//
-// UnsafeAttribute.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.Game.Attributes;
-
-/// <summary>
-/// The given method does not do some checks.
-/// </summary>
-[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Struct)]
-public class UnsafeAttribute : Attribute
-{
- /// <summary>
- /// Initializes a new instance of the <see cref="UnsafeAttribute"/> class.
- /// </summary>
- /// <param name="reason">The reason.</param>
- public UnsafeAttribute(string reason)
- {
- Reason = reason;
- }
-
- /// <summary>
- /// Gets the unsafe reason.
- /// </summary>
- public string Reason { get; }
-}>
\ No newline at end of file
A Core/NosSmooth.Game/Contracts/ContractEventResponder.cs => Core/NosSmooth.Game/Contracts/ContractEventResponder.cs +35 -0
@@ 0,0 1,35 @@
+//
+// ContractEventResponder.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.Contracts;
+using NosSmooth.Game.Events;
+using NosSmooth.Game.Events.Core;
+using Remora.Results;
+
+namespace NosSmooth.Game.Contracts;
+
+/// <summary>
+/// A responder that calls Contractor update.
+/// </summary>
+public class ContractEventResponder : IEveryGameResponder
+{
+ private readonly Contractor _contractor;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ContractEventResponder"/> class.
+ /// </summary>
+ /// <param name="contractor">The contractor.</param>
+ public ContractEventResponder(Contractor contractor)
+ {
+ _contractor = contractor;
+
+ }
+
+ /// <inheritdoc />
+ public Task<Result> Respond<TEvent>(TEvent gameEvent, CancellationToken ct = default)
+ where TEvent : IGameEvent
+ => _contractor.Update(gameEvent, ct);
+}<
\ No newline at end of file
A Core/NosSmooth.Game/Contracts/UseSkillErrors.cs => Core/NosSmooth.Game/Contracts/UseSkillErrors.cs +28 -0
@@ 0,0 1,28 @@
+//
+// UseSkillErrors.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.Game.Contracts;
+
+/// <summary>
+/// Errors for using a skill.
+/// </summary>
+public enum UseSkillErrors
+{
+ /// <summary>
+ /// An unknown error has happened.
+ /// </summary>
+ Unknown,
+
+ /// <summary>
+ /// The character does not have enough ammo.
+ /// </summary>
+ NoAmmo,
+
+ /// <summary>
+ /// The character does not have enough mana.
+ /// </summary>
+ NoMana
+}<
\ No newline at end of file
A Core/NosSmooth.Game/Contracts/UseSkillStates.cs => Core/NosSmooth.Game/Contracts/UseSkillStates.cs +37 -0
@@ 0,0 1,37 @@
+//
+// UseSkillStates.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.Game.Contracts;
+
+/// <summary>
+/// States for contract for using a skill.
+/// </summary>
+public enum UseSkillStates
+{
+ /// <summary>
+ /// Skill use was not executed yet.
+ /// </summary>
+ None,
+
+ /// <summary>
+ /// A skill use packet (u_s, u_as, etc.) was used,
+ /// awaiting a response from the server.
+ /// </summary>
+ SkillUseRequested,
+
+ /// <summary>
+ /// The server has responded with a skill use,
+ /// (after cast time), the information about
+ /// the caster, target is filled.
+ /// </summary>
+ SkillUsedResponse,
+
+ /// <summary>
+ /// Fired 1000 ms after <see cref="SkillUsedResponse"/>,
+ /// the character may move after this.
+ /// </summary>
+ CharacterRestored
+}<
\ No newline at end of file
A Core/NosSmooth.Game/Errors/WrongSkillTargetError.cs => Core/NosSmooth.Game/Errors/WrongSkillTargetError.cs +19 -0
@@ 0,0 1,19 @@
+//
+// WrongSkillTargetError.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.Game.Data.Characters;
+using NosSmooth.Game.Data.Entities;
+using Remora.Results;
+
+namespace NosSmooth.Game.Errors;
+
+/// <summary>
+/// Skill was used at wrong target.
+/// </summary>
+/// <param name="Skill">The skill that was used.</param>
+/// <param name="Target">The target of the skill.</param>
+public record WrongSkillTargetError(Skill Skill, ILivingEntity? Target)
+ : ResultError($"The skill {Skill.SkillVNum} was used at wrong target {Target}");<
\ No newline at end of file
M Core/NosSmooth.Game/Events/Core/EventDispatcher.cs => Core/NosSmooth.Game/Events/Core/EventDispatcher.cs +23 -3
@@ 36,10 36,30 @@ public class EventDispatcher
where TEvent : IGameEvent
{
using var scope = _provider.CreateScope();
- var results = await Task.WhenAll(
+
+ async Task<Result> SafeCall(Func<Task<Result>> result)
+ {
+ try
+ {
+ return await result();
+ }
+ catch (Exception e)
+ {
+ return e;
+ }
+ }
+
+ var results = await Task.WhenAll
+ (
scope.ServiceProvider
.GetServices<IGameResponder<TEvent>>()
- .Select(responder => responder.Respond(@event, ct))
+ .Select(responder => SafeCall(() => responder.Respond(@event, ct)))
+ .Concat
+ (
+ scope.ServiceProvider
+ .GetServices<IEveryGameResponder>()
+ .Select(responder => SafeCall(() => responder.Respond(@event, ct)))
+ )
);
return results.Length switch
@@ 49,4 69,4 @@ public class EventDispatcher
_ => new AggregateError(results.Cast<IResult>().ToArray()),
};
}
-}
+}<
\ No newline at end of file
M Core/NosSmooth.Game/Events/Core/IGameResponder.cs => Core/NosSmooth.Game/Events/Core/IGameResponder.cs +19 -2
@@ 24,10 24,27 @@ public interface IGameResponder<TEvent> : IGameResponder
where TEvent : IGameEvent
{
/// <summary>
- /// Respond to the given packet.
+ /// Respond to the given event.
/// </summary>
- /// <param name="gameEvent">The packet to respond to.</param>
+ /// <param name="gameEvent">The event to respond to.</param>
/// <param name="ct">The cancellation token for cancelling the operation.</param>
/// <returns>A result that may or may not have succeeded.</returns>
public Task<Result> Respond(TEvent gameEvent, CancellationToken ct = default);
}
+
+/// <summary>
+/// Represents interface for classes that respond to every game event.
+/// Responds to any game event.
+/// </summary>
+public interface IEveryGameResponder
+{
+ /// <summary>
+ /// Respond to any event.
+ /// </summary>
+ /// <param name="gameEvent">The event to respond to.</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <typeparam name="TEvent">The current event type.</typeparam>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Task<Result> Respond<TEvent>(TEvent gameEvent, CancellationToken ct = default)
+ where TEvent : IGameEvent;
+}<
\ No newline at end of file
M Core/NosSmooth.Game/Events/Entities/ItemDroppedEvent.cs => Core/NosSmooth.Game/Events/Entities/ItemDroppedEvent.cs +1 -1
@@ 12,4 12,4 @@ namespace NosSmooth.Game.Events.Entities;
/// An item has been dropped.
/// </summary>
/// <param name="Item">The item that has been dropped.</param>
-public record ItemDroppedEvent(IEntity Item) : IGameEvent;>
\ No newline at end of file
+public record ItemDroppedEvent(GroundItem Item) : IGameEvent;<
\ No newline at end of file
M Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs => Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs +12 -6
@@ 8,6 8,9 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using NosSmooth.Core.Extensions;
using NosSmooth.Game.Apis;
+using NosSmooth.Game.Apis.Safe;
+using NosSmooth.Game.Apis.Unsafe;
+using NosSmooth.Game.Contracts;
using NosSmooth.Game.Events.Core;
using NosSmooth.Game.Events.Inventory;
using NosSmooth.Game.PacketHandlers.Act4;
@@ 65,12 68,15 @@ public static class ServiceCollectionExtensions
.AddPacketResponder<EqResponder>();
serviceCollection
- .AddTransient<NostaleMapPacketApi>()
- .AddTransient<NostaleInventoryPacketApi>()
- .AddTransient<NostaleMatePacketApi>()
- .AddTransient<NostaleMateSkillsPacketApi>()
- .AddTransient<NostaleChatPacketApi>()
- .AddTransient<NostaleSkillsPacketApi>();
+ .AddTransient<UnsafeMapApi>()
+ .AddTransient<UnsafeInventoryApi>()
+ .AddTransient<UnsafeMateApi>()
+ .AddTransient<UnsafeMateSkillsApi>()
+ .AddTransient<NostaleChatApi>()
+ .AddTransient<UnsafeSkillsApi>();
+
+ serviceCollection
+ .AddScoped<IEveryGameResponder, ContractEventResponder>();
return serviceCollection;
}