~ruther/NosSmooth

1c72fa321b5e1dec1e3f9b8bf1d9c85c7f7ec983 — Rutherther 2 years ago 2490c12 + fa4872f
Merge pull request #55 from Rutherther/feat/contracts

Add contract system
27 files changed, 1891 insertions(+), 159 deletions(-)

A Core/NosSmooth.Core/Contracts/ContractBuilder.cs
A Core/NosSmooth.Core/Contracts/ContractError.cs
A Core/NosSmooth.Core/Contracts/ContractUpdateResponse.cs
A Core/NosSmooth.Core/Contracts/Contractor.cs
A Core/NosSmooth.Core/Contracts/DefaultContract.cs
A Core/NosSmooth.Core/Contracts/DefaultStates.cs
A Core/NosSmooth.Core/Contracts/IContract.cs
A Core/NosSmooth.Core/Contracts/NoErrors.cs
A Core/NosSmooth.Core/Contracts/Responders/ContractPacketResponder.cs
M Core/NosSmooth.Core/Extensions/ServiceCollectionExtensions.cs
R Core/NosSmooth.Game/Apis/{NostaleChatPacketApi.cs => Safe/NostaleChatApi.cs}
R Core/NosSmooth.Game/Apis/{NostaleMapPacketApi.cs => Safe/NostaleMapApi.cs}
A Core/NosSmooth.Game/Apis/Safe/NostaleSkillsApi.cs
R Core/NosSmooth.Game/Apis/{NostaleInventoryPacketApi.cs => Unsafe/UnsafeInventoryApi.cs}
A Core/NosSmooth.Game/Apis/Unsafe/UnsafeMapApi.cs
R Core/NosSmooth.Game/Apis/{NostaleMatePacketApi.cs => Unsafe/UnsafeMateApi.cs}
R Core/NosSmooth.Game/Apis/{NostaleMateSkillsPacketApi.cs => Unsafe/UnsafeMateSkillsApi.cs}
R Core/NosSmooth.Game/Apis/{NostaleSkillsPacketApi.cs => Unsafe/UnsafeSkillsApi.cs}
D Core/NosSmooth.Game/Attributes/UnsafeAttribute.cs
A Core/NosSmooth.Game/Contracts/ContractEventResponder.cs
A Core/NosSmooth.Game/Contracts/UseSkillErrors.cs
A Core/NosSmooth.Game/Contracts/UseSkillStates.cs
A Core/NosSmooth.Game/Errors/WrongSkillTargetError.cs
M Core/NosSmooth.Game/Events/Core/EventDispatcher.cs
M Core/NosSmooth.Game/Events/Core/IGameResponder.cs
M Core/NosSmooth.Game/Events/Entities/ItemDroppedEvent.cs
M Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs
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;
    }

Do not follow this link