M Core/NosSmooth.Core/NosSmooth.Core.csproj => Core/NosSmooth.Core/NosSmooth.Core.csproj +3 -2
@@ 8,8 8,9 @@
<Description>NosSmooth Core library allowing implementing nostale client, handling packets and commands.</Description>
<RepositoryUrl>https://github.com/Rutherther/NosSmooth/</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
- <PackageVersion>2.0.0</PackageVersion>
- <PackageReleaseNotes>Make internal classes that do not need to be exposed.</PackageReleaseNotes>
+ <PackageVersion>3.0.0</PackageVersion>
+ <PackageReleaseNotes>Change HandleReceivedPacket, HandleSentPacket to HandlePacket.
+Fix bugs.</PackageReleaseNotes>
</PropertyGroup>
<ItemGroup>
M Core/NosSmooth.Game/Data/Entities/Npc.cs => Core/NosSmooth.Game/Data/Entities/Npc.cs +6 -0
@@ 4,6 4,7 @@
// Copyright (c) František Boháček. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+using NosSmooth.Data.Abstractions.Infos;
using NosSmooth.Game.Data.Info;
using NosSmooth.Packets.Enums;
@@ 15,6 16,11 @@ namespace NosSmooth.Game.Data.Entities;
public class Npc : ILivingEntity
{
/// <summary>
+ /// Gets or sets the monster info.
+ /// </summary>
+ public IMonsterInfo? NpcInfo { get; set; }
+
+ /// <summary>
/// Gets the VNum of the npc.
/// </summary>
public int VNum { get; internal set; }
M Core/NosSmooth.Game/NosSmooth.Game.csproj => Core/NosSmooth.Game/NosSmooth.Game.csproj +4 -2
@@ 8,8 8,10 @@
<Description>NosSmooth Game library handling the current game state by responding to packets.</Description>
<RepositoryUrl>https://github.com/Rutherther/NosSmooth/</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
- <PackageVersion>1.2.1</PackageVersion>
- <PackageReleaseNotes>Release semaphore on exception.</PackageReleaseNotes>
+ <PackageVersion>1.3.0</PackageVersion>
+ <PackageReleaseNotes>Fix multiple packet/event handling problems.</PackageReleaseNotes>
+ <AssemblyVersion>1.3.0</AssemblyVersion>
+ <FileVersion>1.3.0</FileVersion>
</PropertyGroup>
<ItemGroup>
M Core/NosSmooth.Game/PacketHandlers/Map/InResponder.cs => Core/NosSmooth.Game/PacketHandlers/Map/InResponder.cs +42 -0
@@ 15,6 15,7 @@ using NosSmooth.Game.Data.Social;
using NosSmooth.Game.Events.Core;
using NosSmooth.Game.Events.Entities;
using NosSmooth.Game.Helpers;
+using NosSmooth.Packets.Enums;
using NosSmooth.Packets.Server.Entities;
using NosSmooth.Packets.Server.Maps;
using Remora.Results;
@@ 83,6 84,11 @@ public class InResponder : IPacketResponder<InPacket>
}
if (packet.NonPlayerSubPacket is not null)
{
+ if (packet.EntityType == EntityType.Npc)
+ {
+ return await CreateNpc(packet, packet.NonPlayerSubPacket, ct);
+ }
+
return await CreateMonster(packet, packet.NonPlayerSubPacket, ct);
}
@@ 163,6 169,42 @@ public class InResponder : IPacketResponder<InPacket>
};
}
+ private async Task<Npc> CreateNpc
+ (InPacket packet, InNonPlayerSubPacket nonPlayerSubPacket, CancellationToken ct)
+ {
+ if (packet.VNum is null)
+ {
+ throw new Exception("The vnum from the in packet cannot be null for monsters.");
+ }
+
+ var monsterInfoResult = await _infoService.GetMonsterInfoAsync(packet.VNum.Value, ct);
+ if (!monsterInfoResult.IsDefined(out var monsterInfo))
+ {
+ _logger.LogWarning
+ (
+ "Could not obtain a monster info for vnum {vnum}: {error}",
+ packet.VNum.Value,
+ monsterInfoResult.ToFullString()
+ );
+ }
+
+ return new Npc
+ {
+ VNum = packet.VNum.Value,
+ NpcInfo = monsterInfo,
+ Id = packet.EntityId,
+ Direction = packet.Direction,
+ Faction = nonPlayerSubPacket.Faction,
+ Hp = new Health { Percentage = nonPlayerSubPacket.HpPercentage },
+ Mp = new Health { Percentage = nonPlayerSubPacket.MpPercentage },
+ Name = nonPlayerSubPacket.Name?.Name,
+ Position = new Position(packet.PositionX, packet.PositionY),
+ IsInvisible = nonPlayerSubPacket.IsInvisible,
+ Level = monsterInfo?.Level ?? null,
+ IsSitting = nonPlayerSubPacket.IsSitting
+ };
+ }
+
private async Task<Monster> CreateMonster
(InPacket packet, InNonPlayerSubPacket nonPlayerSubPacket, CancellationToken ct)
{
M Data/NosSmooth.Data.Abstractions/Infos/ISkillInfo.cs => Data/NosSmooth.Data.Abstractions/Infos/ISkillInfo.cs +2 -2
@@ 30,12 30,12 @@ public interface ISkillInfo : IVNumInfo
short ZoneRange { get; }
/// <summary>
- /// Gets the time it takes to cast this skill. Units UNKNOWN TODO.
+ /// Gets the time it takes to cast this skill. The unit is Tenth of a second. (10 means one second).
/// </summary>
int CastTime { get; }
/// <summary>
- /// Gets the time of the cooldown. Units UNKNOWN TODO.
+ /// Gets the time of the cooldown. The unit is Tenth of a second. (10 means one second).
/// </summary>
int Cooldown { get; }
M Data/NosSmooth.Data.NOSFiles/NosSmooth.Data.NOSFiles.csproj => Data/NosSmooth.Data.NOSFiles/NosSmooth.Data.NOSFiles.csproj +4 -2
@@ 7,9 7,11 @@
<LangVersion>10</LangVersion>
<RepositoryUrl>https://github.com/Rutherther/NosSmooth/</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
- <PackageVersion>2.0.1</PackageVersion>
- <PackageReleaseNotes>Read map file correctly.</PackageReleaseNotes>
+ <PackageVersion>2.0.2</PackageVersion>
+ <PackageReleaseNotes>Correct skill mp cost position</PackageReleaseNotes>
<TargetFramework>net7.0</TargetFramework>
+ <AssemblyVersion>2.0.2</AssemblyVersion>
+ <FileVersion>2.0.2</FileVersion>
</PropertyGroup>
<ItemGroup>
A Extensions/NosSmooth.Extensions.Combat/CombatManager.cs => Extensions/NosSmooth.Extensions.Combat/CombatManager.cs +215 -0
@@ 0,0 1,215 @@
+//
+// CombatManager.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Core.Client;
+using NosSmooth.Core.Commands.Attack;
+using NosSmooth.Core.Stateful;
+using NosSmooth.Extensions.Combat.Errors;
+using NosSmooth.Extensions.Combat.Operations;
+using NosSmooth.Extensions.Combat.Techniques;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat;
+
+/// <summary>
+/// The combat manager that uses techniques to attack enemies.
+/// </summary>
+public class CombatManager : IStatefulEntity
+{
+ private readonly List<CancellationTokenSource> _tokenSource;
+ private readonly SemaphoreSlim _semaphore;
+ private readonly INostaleClient _client;
+ private readonly Game.Game _game;
+ private bool _cancelling;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CombatManager"/> class.
+ /// </summary>
+ /// <param name="client">The NosTale client.</param>
+ /// <param name="game">The game.</param>
+ public CombatManager(INostaleClient client, Game.Game game)
+ {
+ _semaphore = new SemaphoreSlim(1, 1);
+ _tokenSource = new List<CancellationTokenSource>();
+ _client = client;
+ _game = game;
+ }
+
+ /// <summary>
+ /// Enter into a combat state using the given technique.
+ /// </summary>
+ /// <param name="technique">The technique to use.</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A result that may or may not succeed.</returns>
+ public async Task<Result> EnterCombatAsync(ICombatTechnique technique, CancellationToken ct = default)
+ {
+ var combatState = new CombatState(_client, _game, this);
+ long? currentTarget = null;
+ long? previousTarget = null;
+
+ while (!combatState.ShouldQuit && !ct.IsCancellationRequested)
+ {
+ var commandResult = await _client.SendCommandAsync
+ (
+ new AttackCommand
+ (
+ currentTarget,
+ async (c) =>
+ {
+ while (!combatState.ShouldQuit && currentTarget == previousTarget)
+ {
+ if (!technique.ShouldContinue(combatState))
+ {
+ combatState.QuitCombat();
+ continue;
+ }
+
+ var operation = combatState.NextOperation();
+
+ if (operation is null)
+ { // The operation is null and the step has to be obtained from the technique.
+ var stepResult = technique.HandleCombatStep(combatState);
+ if (!stepResult.IsSuccess)
+ {
+ return Result.FromError(stepResult);
+ }
+
+ previousTarget = currentTarget;
+ currentTarget = stepResult.Entity;
+
+ if (previousTarget != currentTarget)
+ {
+ continue;
+ }
+
+ operation = combatState.NextOperation();
+ }
+
+ if (operation is null)
+ { // The operation could be null just because there is currently not a skill to be used etc.
+ await Task.Delay(5, ct);
+ continue;
+ }
+
+ Result<CanBeUsedResponse> responseResult;
+ while ((responseResult = operation.CanBeUsed(combatState)).IsSuccess
+ && responseResult.Entity == CanBeUsedResponse.MustWait)
+ { // TODO: wait for just some amount of time
+ await Task.Delay(5, ct);
+ }
+
+ if (!responseResult.IsSuccess)
+ {
+ return Result.FromError(responseResult);
+ }
+
+ if (responseResult.Entity == CanBeUsedResponse.WontBeUsable)
+ {
+ return new UnusableOperationError(operation);
+ }
+
+ var usageResult = await operation.UseAsync(combatState, ct);
+ if (!usageResult.IsSuccess)
+ {
+ var errorHandleResult = technique.HandleError(combatState, operation, usageResult);
+ if (!errorHandleResult.IsSuccess)
+ {
+ return errorHandleResult;
+ }
+ }
+ }
+
+ return Result.FromSuccess();
+ }
+ ),
+ ct
+ );
+
+ if (!commandResult.IsSuccess)
+ {
+ return commandResult;
+ }
+
+ previousTarget = currentTarget;
+ }
+ return Result.FromSuccess();
+ }
+
+ /// <summary>
+ /// Register the given cancellation token source to be cancelled on skill use/cancel.
+ /// </summary>
+ /// <param name="tokenSource">The token source to register.</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A task.</returns>
+ public async Task RegisterSkillCancellationTokenAsync(CancellationTokenSource tokenSource, CancellationToken ct)
+ {
+ await _semaphore.WaitAsync(ct);
+ try
+ {
+ _tokenSource.Add(tokenSource);
+ }
+ finally
+ {
+ _semaphore.Release();
+ }
+ }
+
+ /// <summary>
+ /// Unregister the given cancellation token registered using <see cref="RegisterSkillCancellationToken"/>.
+ /// </summary>
+ /// <param name="tokenSource">The token source to unregister.</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A task.</returns>
+ public async Task UnregisterSkillCancellationTokenAsync(CancellationTokenSource tokenSource, CancellationToken ct)
+ {
+ if (_cancelling)
+ {
+ return;
+ }
+
+ await _semaphore.WaitAsync(ct);
+ try
+ {
+ _tokenSource.Remove(tokenSource);
+ }
+ finally
+ {
+ _semaphore.Release();
+ }
+ }
+
+ /// <summary>
+ /// Cancel all of the skill tokens.
+ /// </summary>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A task.</returns>
+ internal async Task CancelSkillTokensAsync(CancellationToken ct)
+ {
+ await _semaphore.WaitAsync(ct);
+ _cancelling = true;
+ try
+ {
+ foreach (var tokenSource in _tokenSource)
+ {
+ try
+ {
+ tokenSource.Cancel();
+ }
+ catch
+ {
+ // ignored
+ }
+ }
+
+ _tokenSource.Clear();
+ }
+ finally
+ {
+ _cancelling = false;
+ _semaphore.Release();
+ }
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/CombatState.cs => Extensions/NosSmooth.Extensions.Combat/CombatState.cs +107 -0
@@ 0,0 1,107 @@
+//
+// CombatState.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Xml;
+using NosSmooth.Core.Client;
+using NosSmooth.Extensions.Combat.Operations;
+using NosSmooth.Game.Data.Entities;
+
+namespace NosSmooth.Extensions.Combat;
+
+/// <inheritdoc />
+internal class CombatState : ICombatState
+{
+ private readonly LinkedList<ICombatOperation> _operations;
+ private ICombatOperation? _currentOperation;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CombatState"/> class.
+ /// </summary>
+ /// <param name="client">The NosTale client.</param>
+ /// <param name="game">The game.</param>
+ /// <param name="combatManager">The combat manager.</param>
+ public CombatState(INostaleClient client, Game.Game game, CombatManager combatManager)
+ {
+ Client = client;
+ Game = game;
+ CombatManager = combatManager;
+ _operations = new LinkedList<ICombatOperation>();
+ }
+
+ /// <summary>
+ /// Gets whether the combat state should be quit.
+ /// </summary>
+ public bool ShouldQuit { get; private set; }
+
+ /// <inheritdoc/>
+ public CombatManager CombatManager { get; }
+
+ /// <inheritdoc/>
+ public Game.Game Game { get; }
+
+ /// <inheritdoc/>
+ public INostaleClient Client { get; }
+
+ /// <inheritdoc/>
+ public void QuitCombat()
+ {
+ ShouldQuit = true;
+ }
+
+ /// <summary>
+ /// Make a step in the queue.
+ /// </summary>
+ /// <returns>The current operation, if any.</returns>
+ public ICombatOperation? NextOperation()
+ {
+ var operation = _currentOperation = _operations.First?.Value;
+ if (operation is not null)
+ {
+ _operations.RemoveFirst();
+ }
+
+ return operation;
+ }
+
+ /// <inheritdoc/>
+ public void SetCurrentOperation
+ (ICombatOperation operation, bool emptyQueue = false, bool prependCurrentOperationToQueue = false)
+ {
+ var current = _currentOperation;
+ _currentOperation = operation;
+
+ if (emptyQueue)
+ {
+ _operations.Clear();
+ }
+
+ if (prependCurrentOperationToQueue && current is not null)
+ {
+ _operations.AddFirst(current);
+ }
+ }
+
+ /// <inheritdoc/>
+ public void EnqueueOperation(ICombatOperation operation)
+ {
+ _operations.AddLast(operation);
+ }
+
+ /// <inheritdoc/>
+ public void RemoveOperations(Func<ICombatOperation, bool> filter)
+ {
+ var node = _operations.First;
+ while (node != null)
+ {
+ var next = node.Next;
+ if (filter(node.Value))
+ {
+ _operations.Remove(node);
+ }
+ node = next;
+ }
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Errors/CharacterNotInitializedError.cs => Extensions/NosSmooth.Extensions.Combat/Errors/CharacterNotInitializedError.cs +15 -0
@@ 0,0 1,15 @@
+//
+// CharacterNotInitializedError.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.Extensions.Combat.Errors;
+
+/// <summary>
+/// The character is not initialized.
+/// </summary>
+public record CharacterNotInitializedError(string Field = "")
+ : ResultError($"The character {Field} is not yet initialized.");<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Errors/EntityNotFoundError.cs => Extensions/NosSmooth.Extensions.Combat/Errors/EntityNotFoundError.cs +14 -0
@@ 0,0 1,14 @@
+//
+// EntityNotFoundError.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.Extensions.Combat.Errors;
+
+/// <summary>
+/// Matching entity not found error.
+/// </summary>
+public record EntityNotFoundError() : ResultError("Could not find an entity that matches the conditions.");<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Errors/ItemNotFoundError.cs => Extensions/NosSmooth.Extensions.Combat/Errors/ItemNotFoundError.cs +14 -0
@@ 0,0 1,14 @@
+//
+// ItemNotFoundError.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.Extensions.Combat.Errors;
+
+/// <summary>
+/// Matchin item not found error.
+/// </summary>
+public record ItemNotFoundError() : ResultError("Could not find an item that matches the conditions.");<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Errors/MapNotInitializedError.cs => Extensions/NosSmooth.Extensions.Combat/Errors/MapNotInitializedError.cs +15 -0
@@ 0,0 1,15 @@
+//
+// MapNotInitializedError.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.Extensions.Combat.Errors;
+
+/// <summary>
+/// The map is not initialized.
+/// </summary>
+public record MapNotInitializedError()
+ : ResultError("The map is not yet initialized.");<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Errors/MissingInfoError.cs => Extensions/NosSmooth.Extensions.Combat/Errors/MissingInfoError.cs +16 -0
@@ 0,0 1,16 @@
+//
+// MissingInfoError.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.Extensions.Combat.Errors;
+
+/// <summary>
+/// Missing info of a specified type and id..
+/// </summary>
+/// <param name="Type">The type.</param>
+/// <param name="Id">The id.</param>
+public record MissingInfoError(string Type, long Id) : ResultError($"Cannot find info of {{Type}} with id {Id}.");<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Errors/SkillNotFoundError.cs => Extensions/NosSmooth.Extensions.Combat/Errors/SkillNotFoundError.cs +14 -0
@@ 0,0 1,14 @@
+//
+// SkillNotFoundError.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.Extensions.Combat.Errors;
+
+/// <summary>
+/// Matchin skill not found.
+/// </summary>
+public record SkillNotFoundError() : ResultError("Could not find a skill that matches the conditions.");<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Errors/TargetNotSetError.cs => Extensions/NosSmooth.Extensions.Combat/Errors/TargetNotSetError.cs +11 -0
@@ 0,0 1,11 @@
+//
+// TargetNotSetError.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.Extensions.Combat.Errors;
+
+public record TargetNotSetError() : ResultError("The current target is not set, the operation cannot complete.");<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Errors/UnusableOperationError.cs => Extensions/NosSmooth.Extensions.Combat/Errors/UnusableOperationError.cs +17 -0
@@ 0,0 1,17 @@
+//
+// UnusableOperationError.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Extensions.Combat.Operations;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Errors;
+
+/// <summary>
+/// An error that tells the operation was unusable.
+/// </summary>
+/// <param name="Operation">The operation.</param>
+public record UnusableOperationError(ICombatOperation Operation)
+ : ResultError("A given operation {Operation} responded that it won't be usable ever and thus there is an unrecoverable state.");<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Extensions/CombatStateExtensions.cs => Extensions/NosSmooth.Extensions.Combat/Extensions/CombatStateExtensions.cs +90 -0
@@ 0,0 1,90 @@
+//
+// CombatStateExtensions.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Extensions.Combat.Errors;
+using NosSmooth.Extensions.Combat.Operations;
+using NosSmooth.Extensions.Combat.Policies;
+using NosSmooth.Extensions.Pathfinding;
+using NosSmooth.Game.Data.Characters;
+using NosSmooth.Game.Data.Entities;
+using NosSmooth.Game.Data.Items;
+using OneOf.Types;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Extensions;
+
+/// <summary>
+/// Extension methods for <see cref="ICombatState"/>.
+/// </summary>
+public static class CombatStateExtensions
+{
+ /// <summary>
+ /// Walk in the range of the given entity.
+ /// </summary>
+ /// <param name="state">The combat state.</param>
+ /// <param name="walkManager">The walk manager.</param>
+ /// <param name="entity">The entity.</param>
+ /// <param name="range">The range distance to walk to.</param>
+ public static void WalkInRange
+ (
+ this ICombatState state,
+ WalkManager walkManager,
+ IEntity entity,
+ float range
+ )
+ {
+ state.EnqueueOperation(new WalkInRangeOperation(walkManager, entity, range));
+ }
+
+ /// <summary>
+ /// Walk to the given position.
+ /// </summary>
+ /// <param name="combatState">The combat state.</param>
+ /// <param name="walkManager">The walk manager.</param>
+ /// <param name="x">The target x coordinate.</param>
+ /// <param name="y">The target y coordinate.</param>
+ public static void WalkTo
+ (
+ this ICombatState combatState,
+ WalkManager walkManager,
+ short x,
+ short y
+ )
+ {
+ combatState.EnqueueOperation(new WalkOperation(walkManager, x, y));
+ }
+
+ /// <summary>
+ /// Use the given skill.
+ /// </summary>
+ /// <param name="combatState">The combat state.</param>
+ /// <param name="skill">The skill.</param>
+ /// <param name="target">The target to use skill at.</param>
+ public static void UseSkill(this ICombatState combatState, Skill skill, ILivingEntity target)
+ {
+ combatState.EnqueueOperation(new UseSkillOperation(skill, target));
+ }
+
+ /// <summary>
+ /// Use primary skill.
+ /// </summary>
+ /// <param name="combatState">The combat state.</param>
+ /// <param name="target">The target to use skill at.</param>
+ public static void UsePrimarySkill(this ICombatState combatState, ILivingEntity target)
+ {
+ combatState.EnqueueOperation(new UsePrimarySkillOperation(target));
+ }
+
+ /// <summary>
+ /// Use the given item.
+ /// </summary>
+ /// <param name="combatState">The combat state.</param>
+ /// <param name="item">The item to use.</param>
+ public static void UseItem(this ICombatState combatState, Item item)
+ {
+ combatState.EnqueueOperation(new UseItemOperation(item));
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Extensions/ServiceCollectionExtensions.cs => Extensions/NosSmooth.Extensions.Combat/Extensions/ServiceCollectionExtensions.cs +30 -0
@@ 0,0 1,30 @@
+//
+// ServiceCollectionExtensions.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Extensions.DependencyInjection;
+using NosSmooth.Core.Extensions;
+using NosSmooth.Extensions.Combat.Responders;
+
+namespace NosSmooth.Extensions.Combat.Extensions;
+
+/// <summary>
+/// Extension methods for <see cref="IServiceCollection"/>.
+/// </summary>
+public static class ServiceCollectionExtensions
+{
+ /// <summary>
+ /// Adds a NosTale combat extension. <see cref="CombatManager"/>.
+ /// </summary>
+ /// <param name="serviceCollection">The service collection.</param>
+ /// <returns>The collection.</returns>
+ public static IServiceCollection AddNostaleCombat(this IServiceCollection serviceCollection)
+ {
+ return serviceCollection
+ .AddPacketResponder<CancelResponder>()
+ .AddPacketResponder<SuResponder>()
+ .AddSingleton<CombatManager>();
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/ICombatState.cs => Extensions/NosSmooth.Extensions.Combat/ICombatState.cs +58 -0
@@ 0,0 1,58 @@
+//
+// ICombatState.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.Extensions.Combat.Operations;
+using NosSmooth.Game.Data.Entities;
+
+namespace NosSmooth.Extensions.Combat;
+
+/// <summary>
+/// The combat technique state used for queuing operations and storing information.
+/// </summary>
+public interface ICombatState
+{
+ /// <summary>
+ /// Gets the combat manager.
+ /// </summary>
+ public CombatManager CombatManager { get; }
+
+ /// <summary>
+ /// Gets the game.
+ /// </summary>
+ public Game.Game Game { get; }
+
+ /// <summary>
+ /// Gets the NosTale client.
+ /// </summary>
+ public INostaleClient Client { get; }
+
+ /// <summary>
+ /// Cancel the combat technique, quit the combat state.
+ /// </summary>
+ public void QuitCombat();
+
+ /// <summary>
+ /// Replace the current operation with this one.
+ /// </summary>
+ /// <param name="operation">The operation to use.</param>
+ /// <param name="emptyQueue">Whether to empty the queue of the operations.</param>
+ /// <param name="prependCurrentOperationToQueue">Whether to still use the current operation (true) after this one or discard it (false).</param>
+ public void SetCurrentOperation
+ (ICombatOperation operation, bool emptyQueue = false, bool prependCurrentOperationToQueue = false);
+
+ /// <summary>
+ /// Enqueue the operation at the end of the queue.
+ /// </summary>
+ /// <param name="operation">The operation to enqueue.</param>
+ public void EnqueueOperation(ICombatOperation operation);
+
+ /// <summary>
+ /// Remove the operations by the given filter.
+ /// </summary>
+ /// <param name="filter">Called for each operation, should return true if it should be removed.</param>
+ public void RemoveOperations(Func<ICombatOperation, bool> filter);
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/IsExternalInit.cs => Extensions/NosSmooth.Extensions.Combat/IsExternalInit.cs +15 -0
@@ 0,0 1,15 @@
+//
+// IsExternalInit.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 System.Runtime.CompilerServices
+{
+ /// <summary>
+ /// Dummy.
+ /// </summary>
+ public class IsExternalInit
+ {
+ }
+}<
\ No newline at end of file
M Extensions/NosSmooth.Extensions.Combat/NosSmooth.Extensions.Combat.csproj => Extensions/NosSmooth.Extensions.Combat/NosSmooth.Extensions.Combat.csproj +29 -0
@@ 0,0 1,29 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ <LangVersion>10</LangVersion>
+ <TargetFramework>net7.0</TargetFramework>
+ <Version>0.0.1</Version>
+ <Description>Manages NosSmooth combat state, currently exposing a simple technique to kill one enemy at a time.</Description>
+ <AssemblyVersion>0.0.1</AssemblyVersion>
+ <FileVersion>0.0.1</FileVersion>
+ <RepositoryUrl>https://github.com/Rutherther/NosSmooth/</RepositoryUrl>
+ <PackageLicenseExpression>MIT</PackageLicenseExpression>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Remora.Results" Version="7.2.3" />
+ <PackageReference Include="System.Text.Encoding.CodePages" Version="7.0.0" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\Core\NosSmooth.Core\NosSmooth.Core.csproj" />
+ <ProjectReference Include="..\..\Core\NosSmooth.Game\NosSmooth.Game.csproj" />
+ <ProjectReference Include="..\..\Data\NosSmooth.Data.Abstractions\NosSmooth.Data.Abstractions.csproj" />
+ <ProjectReference Include="..\..\Packets\NosSmooth.Packets\NosSmooth.Packets.csproj" />
+ <ProjectReference Include="..\NosSmooth.Extensions.Pathfinding\NosSmooth.Extensions.Pathfinding.csproj" />
+ </ItemGroup>
+
+</Project>
A Extensions/NosSmooth.Extensions.Combat/Operations/CanBeUsedResponse.cs => Extensions/NosSmooth.Extensions.Combat/Operations/CanBeUsedResponse.cs +28 -0
@@ 0,0 1,28 @@
+//
+// CanBeUsedResponse.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace NosSmooth.Extensions.Combat.Operations;
+
+/// <summary>
+/// A response to <see cref="ICombatOperation"/> CanBeUsed method.
+/// </summary>
+public enum CanBeUsedResponse
+{
+ /// <summary>
+ /// The operation may be used right awayt.
+ /// </summary>
+ CanBeUsed,
+
+ /// <summary>
+ /// The operation will be usable after some amount of time.
+ /// </summary>
+ MustWait,
+
+ /// <summary>
+ /// The operation won't be usable. (ie. missing arrows).
+ /// </summary>
+ WontBeUsable
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Operations/ICombatOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/ICombatOperation.cs +38 -0
@@ 0,0 1,38 @@
+//
+// ICombatOperation.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Extensions.Combat.Techniques;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Operations;
+
+/// <summary>
+/// A combat operation used in <see cref="ICombatTechnique"/> that can be used as one step.
+/// </summary>
+public interface ICombatOperation
+{
+ /// <summary>
+ /// Checks whether the operation can currently be used.
+ /// </summary>
+ /// <remarks>
+ /// Ie. if the operation is to use a skill, it will return true only if the skill is not on a cooldown,
+ /// the character has enough mana and is not stunned.
+ /// </remarks>
+ /// <param name="combatState">The combat state.</param>
+ /// <returns>Whether the operation can be used right away.</returns>
+ public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState);
+
+ /// <summary>
+ /// Use the operation, if possible.
+ /// </summary>
+ /// <remarks>
+ /// Should block until the operation is finished.
+ /// </remarks>
+ /// <param name="combatState">The combat state.</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A result that may or may not succeed.</returns>
+ public Task<Result> UseAsync(ICombatState combatState, CancellationToken ct = default);
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Operations/UseItemOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/UseItemOperation.cs +26 -0
@@ 0,0 1,26 @@
+//
+// UseItemOperation.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.Items;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Operations;
+
+// TODO: first inventory has to be made
+public record UseItemOperation(Item Item) : ICombatOperation
+{
+ /// <inheritdoc />
+ public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
+ {
+ throw new NotImplementedException();
+ }
+
+ /// <inheritdoc />
+ public Task<Result> UseAsync(ICombatState combatState, CancellationToken ct = default)
+ {
+ throw new NotImplementedException();
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Operations/UsePrimarySkillOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/UsePrimarySkillOperation.cs +72 -0
@@ 0,0 1,72 @@
+//
+// UsePrimarySkillOperation.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Extensions.Combat.Errors;
+using NosSmooth.Game.Data.Characters;
+using NosSmooth.Game.Data.Entities;
+using NosSmooth.Packets.Client.Battle;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Operations;
+
+/// <summary>
+/// An operation that uses the primary skill of the character.
+/// </summary>
+public record UsePrimarySkillOperation(ILivingEntity Target) : ICombatOperation
+{
+ private UseSkillOperation? _useSkillOperation;
+
+ /// <inheritdoc />
+ public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
+ {
+ if (_useSkillOperation is null)
+ {
+ var primarySkillResult = GetPrimarySkill(combatState);
+ if (!primarySkillResult.IsDefined(out var primarySkill))
+ {
+ return Result<CanBeUsedResponse>.FromError(primarySkillResult);
+ }
+
+ _useSkillOperation = new UseSkillOperation(primarySkill, Target);
+ }
+
+ return _useSkillOperation.CanBeUsed(combatState);
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> UseAsync(ICombatState combatState, CancellationToken ct)
+ {
+ if (_useSkillOperation is null)
+ {
+ var primarySkillResult = GetPrimarySkill(combatState);
+ if (!primarySkillResult.IsDefined(out var primarySkill))
+ {
+ return Result.FromError(primarySkillResult);
+ }
+
+ _useSkillOperation = new UseSkillOperation(primarySkill, Target);
+ }
+
+ return await _useSkillOperation.UseAsync(combatState, ct);
+ }
+
+ private Result<Skill> GetPrimarySkill(ICombatState combatState)
+ {
+ var character = combatState.Game.Character;
+ if (character is null)
+ {
+ return new CharacterNotInitializedError();
+ }
+
+ var skills = character.Skills;
+ if (skills is null)
+ {
+ return new CharacterNotInitializedError("Skills");
+ }
+
+ return skills.PrimarySkill;
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Operations/UseSkillOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/UseSkillOperation.cs +87 -0
@@ 0,0 1,87 @@
+//
+// UseSkillOperation.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Xml.XPath;
+using NosSmooth.Extensions.Combat.Errors;
+using NosSmooth.Game.Data.Characters;
+using NosSmooth.Game.Data.Entities;
+using NosSmooth.Packets.Client.Battle;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Operations;
+
+/// <summary>
+/// A combat operation to use a skill.
+/// </summary>
+/// <param name="Skill">The skill to use.</param>
+/// <param name="Target">The target entity to use the skill at.</param>
+public record UseSkillOperation(Skill Skill, ILivingEntity Target) : ICombatOperation
+{
+ /// <inheritdoc />
+ public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
+ {
+ if (Skill.Info is null)
+ {
+ return new MissingInfoError("skill", Skill.SkillVNum);
+ }
+
+ var character = combatState.Game.Character;
+ if (character is not null && character.Mp is not null && character.Mp.Amount is not null)
+ {
+ if (character.Mp.Amount < Skill.Info.MpCost)
+ { // The character is in combat, mp won't restore.
+ return CanBeUsedResponse.WontBeUsable;
+ }
+ }
+
+ return Skill.IsOnCooldown ? CanBeUsedResponse.MustWait : CanBeUsedResponse.CanBeUsed;
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> UseAsync(ICombatState combatState, CancellationToken ct = default)
+ {
+ if (Skill.Info is null)
+ {
+ return new MissingInfoError("skill", Skill.SkillVNum);
+ }
+
+ // TODO: support for area skills, support skills that use x, y coordinates (like dashes or teleports)
+ using var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(ct);
+ await combatState.CombatManager.RegisterSkillCancellationTokenAsync(linkedSource, ct);
+ var sendResponse = await combatState.Client.SendPacketAsync
+ (
+ new UseSkillPacket
+ (
+ Skill.Info.CastId,
+ Target.Type,
+ Target.Id,
+ null,
+ null
+ ),
+ ct
+ );
+
+ if (!sendResponse.IsSuccess)
+ {
+ await combatState.CombatManager.UnregisterSkillCancellationTokenAsync(linkedSource, ct);
+ return sendResponse;
+ }
+
+ try
+ {
+ // wait 10 times the cast delay in case su is not received.
+ await Task.Delay(Skill.Info.CastTime * 1000, linkedSource.Token);
+ }
+ catch (TaskCanceledException)
+ {
+ // ignored
+ }
+ await combatState.CombatManager.UnregisterSkillCancellationTokenAsync(linkedSource, ct);
+ await Task.Delay(1000, ct);
+
+ return Result.FromSuccess();
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Operations/WalkInRangeOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/WalkInRangeOperation.cs +131 -0
@@ 0,0 1,131 @@
+//
+// WalkInRangeOperation.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Extensions.Combat.Errors;
+using NosSmooth.Extensions.Pathfinding;
+using NosSmooth.Extensions.Pathfinding.Errors;
+using NosSmooth.Game.Data.Entities;
+using NosSmooth.Game.Data.Info;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Operations;
+
+/// <summary>
+/// A combat operation that walks into a given range of an entity.
+/// </summary>
+/// <param name="WalkManager">The walk manager.</param>
+/// <param name="Entity">The entity to walk to.</param>
+/// <param name="Distance">The maximal distance from the entity.</param>
+public record WalkInRangeOperation
+(
+ WalkManager WalkManager,
+ IEntity Entity,
+ float Distance
+) : ICombatOperation
+{
+ /// <inheritdoc />
+ public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
+ {
+ var character = combatState.Game.Character;
+ if (character is null)
+ {
+ return new CharacterNotInitializedError();
+ }
+
+ return character.CantMove ? CanBeUsedResponse.MustWait : CanBeUsedResponse.CanBeUsed;
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> UseAsync(ICombatState combatState, CancellationToken ct = default)
+ {
+ var character = combatState.Game.Character;
+ if (character is null)
+ {
+ return new CharacterNotInitializedError();
+ }
+
+ var distance = Distance;
+ while (distance >= 0)
+ {
+ var position = Entity.Position;
+ if (position is null)
+ {
+ return new GenericError("Entity's position is not initialized.");
+ }
+
+ var currentPosition = character.Position;
+ if (currentPosition is null)
+ {
+ return new CharacterNotInitializedError("Position");
+ }
+
+ if (Entity.Position?.DistanceSquared(currentPosition.Value) <= Distance * Distance)
+ {
+ return Result.FromSuccess();
+ }
+
+ var closePosition = GetClosePosition(currentPosition.Value, position.Value, distance);
+ if (closePosition == position)
+ {
+ return Result.FromSuccess();
+ }
+
+ using var goToCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(ct);
+ var walkResultTask = WalkManager.GoToAsync(closePosition.X, closePosition.Y, true, goToCancellationTokenSource.Token);
+
+ while (!walkResultTask.IsCompleted)
+ {
+ await Task.Delay(5, ct);
+ if (Entity.Position != position)
+ {
+ goToCancellationTokenSource.Cancel();
+ await walkResultTask;
+ }
+ }
+
+ if (Entity.Position != position)
+ {
+ continue;
+ }
+
+ var walkResult = await walkResultTask;
+ if ((character.Position - Entity.Position)?.DistanceSquared(Position.Zero) <= Distance * Distance)
+ {
+ return Result.FromSuccess();
+ }
+
+ if (!walkResult.IsSuccess && walkResult.Error is PathNotFoundError)
+ {
+ if (distance - 1 > 0)
+ {
+ distance--;
+ }
+ else
+ {
+ distance = 0;
+ }
+
+ continue;
+ }
+
+ return walkResult;
+ }
+
+ return Result.FromSuccess();
+ }
+
+ private Position GetClosePosition(Position start, Position target, double distance)
+ {
+ var diff = start - target;
+ if (diff.DistanceSquared(Position.Zero) < distance * distance)
+ {
+ return start;
+ }
+
+ var diffLength = Math.Sqrt(diff.DistanceSquared(Position.Zero));
+ return target + ((distance / diffLength) * diff);
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Operations/WalkOperation.cs => Extensions/NosSmooth.Extensions.Combat/Operations/WalkOperation.cs +38 -0
@@ 0,0 1,38 @@
+//
+// WalkOperation.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Extensions.Combat.Errors;
+using NosSmooth.Extensions.Pathfinding;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Operations;
+
+/// <summary>
+/// A combat operation that walks to the target.
+/// </summary>
+/// <param name="WalkManager">The walk manager.</param>
+/// <param name="X">The x coordinate to walk to.</param>
+/// <param name="Y">The y coordinate to walk to.</param>
+public record WalkOperation(WalkManager WalkManager, short X, short Y) : ICombatOperation
+{
+ /// <inheritdoc />
+ public Result<CanBeUsedResponse> CanBeUsed(ICombatState combatState)
+ {
+ var character = combatState.Game.Character;
+ if (character is null)
+ {
+ return new CharacterNotInitializedError();
+ }
+
+ return character.CantMove ? CanBeUsedResponse.MustWait : CanBeUsedResponse.CanBeUsed;
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> UseAsync(ICombatState combatState, CancellationToken ct = default)
+ {
+ return await WalkManager.GoToAsync(X, Y, true, ct);
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Policies/EnemyPolicy.cs => Extensions/NosSmooth.Extensions.Combat/Policies/EnemyPolicy.cs +162 -0
@@ 0,0 1,162 @@
+//
+// EnemyPolicy.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.SqlTypes;
+using NosSmooth.Extensions.Combat.Errors;
+using NosSmooth.Extensions.Combat.Selectors;
+using NosSmooth.Game.Data.Characters;
+using NosSmooth.Game.Data.Entities;
+using NosSmooth.Game.Data.Info;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Policies;
+
+/// <summary>
+/// An enemy selector policy that selects the monsters based on their vnums.
+/// </summary>
+/// <param name="SelectPolicy">The policy to select an enemy.</param>
+/// <param name="MonsterVNums">The vnums of the monsters to target.</param>
+/// <param name="CombatArea">The area in which to get enemies from.</param>
+public record EnemyPolicy
+(
+ EnemySelectPolicy SelectPolicy,
+ int[]? MonsterVNums = default,
+ CombatArea? CombatArea = default
+) : IEnemySelector
+{
+ /// <inheritdoc />
+ public Result<ILivingEntity> GetSelectedEntity(ICombatState combatState, ICollection<ILivingEntity> possibleTargets)
+ {
+ var targets = possibleTargets.OfType<Monster>();
+ if (MonsterVNums is not null)
+ {
+ targets = targets.Where(x => MonsterVNums.Contains(x.VNum));
+ }
+
+ if (!targets.Any())
+ {
+ return new EntityNotFoundError();
+ }
+
+ if (combatState.Game.Character is null)
+ {
+ return new CharacterNotInitializedError();
+ }
+
+ var position = combatState.Game.Character.Position;
+ if (position is null)
+ {
+ return new CharacterNotInitializedError();
+ }
+
+ var characterPosition = position.Value;
+ ILivingEntity? target = null;
+ switch (SelectPolicy)
+ {
+ case EnemySelectPolicy.Aggressive:
+ throw new NotImplementedException(); // TODO: implement aggressive policy
+ case EnemySelectPolicy.Closest:
+ target = targets
+ .Where(x => x.Position is not null && (CombatArea?.IsInside(x.Position.Value) ?? true))
+ .MinBy(x => x.Position!.Value.DistanceSquared(characterPosition))!;
+ break;
+ case EnemySelectPolicy.LowestHealth:
+ target = targets.MinBy
+ (
+ x =>
+ {
+ if (x.Hp is null)
+ {
+ return int.MaxValue;
+ }
+
+ if (x.Hp.Amount is not null)
+ {
+ return x.Hp.Amount;
+ }
+
+ if (x.Hp.Percentage is not null && x.Level is not null)
+ {
+ return x.Hp.Percentage * 100 * x.Level;
+ }
+
+ if (x.Hp.Maximum is not null)
+ {
+ return x.Hp.Maximum; // Assume max health, best guess.
+ }
+
+ return int.MaxValue;
+ }
+ );
+ break;
+ }
+
+ if (target is null)
+ {
+ return new EntityNotFoundError();
+ }
+
+ return Result<ILivingEntity>.FromSuccess(target);
+ }
+}
+
+/// <summary>
+/// A policy enemy selector.
+/// </summary>
+public enum EnemySelectPolicy
+{
+ /// <summary>
+ /// Select the enemy with the lowest health.
+ /// </summary>
+ LowestHealth,
+
+ /// <summary>
+ /// Selects the enemy that targets the user.
+ /// </summary>
+ Aggressive,
+
+ /// <summary>
+ /// Selects the enemy that is the closest to the character.
+ /// </summary>
+ Closest
+}
+
+/// <summary>
+/// The combat area around which to find enemies.
+/// </summary>
+/// <param name="CenterX">The area center x coordinate.</param>
+/// <param name="CenterY">The area center y coordinate.</param>
+/// <param name="Range">The maximum range from the center.</param>
+public record CombatArea(short CenterX, short CenterY, short Range)
+{
+ /// <summary>
+ /// Create a combat area around a specified entity.
+ /// </summary>
+ /// <param name="entity">The entity.</param>
+ /// <param name="range">The range.</param>
+ /// <returns>The combat area.</returns>
+ /// <exception cref="ArgumentException">If the entity does not have a position.</exception>
+ public static CombatArea CreateAroundEntity(IEntity entity, short range)
+ {
+ var position = entity.Position;
+ if (position is null)
+ {
+ throw new ArgumentException(nameof(entity));
+ }
+
+ return new CombatArea(position.Value.X, position.Value.Y, range);
+ }
+
+ /// <summary>
+ /// Gets whether the position is inside of the combat area.
+ /// </summary>
+ /// <param name="position">The position.</param>
+ /// <returns>Whether the position is inside.</returns>
+ public bool IsInside(Position position)
+ {
+ return position.DistanceSquared(new Position(CenterX, CenterY)) < Range * Range;
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Policies/UseItemPolicy.cs => Extensions/NosSmooth.Extensions.Combat/Policies/UseItemPolicy.cs +90 -0
@@ 0,0 1,90 @@
+//
+// UseItemPolicy.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Extensions.Combat.Errors;
+using NosSmooth.Extensions.Combat.Selectors;
+using NosSmooth.Game.Data.Characters;
+using NosSmooth.Game.Data.Items;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Policies;
+
+/// <summary>
+/// The policy to use an item.
+/// </summary>
+/// <param name="UseItems">Whether to use items.</param>
+/// <param name="UseBelowHealthPercentage">Use items below the given character's health percentage.</param>
+/// <param name="UseBelowManaPercentage">Use items below the given character's mana percentage.</param>
+/// <param name="UseHealthItemsVNums">The vnums of the items to use as health items.</param>
+/// <param name="UseManaItemsVNums">The vnums of the items to use as mana items.</param>
+public record UseItemPolicy
+(
+ bool UseItems,
+ int UseBelowHealthPercentage,
+ int UseBelowManaPercentage,
+ int[] UseHealthItemsVNums,
+ int[] UseManaItemsVNums
+) : IItemSelector
+{
+ /// <inheritdoc />
+ public Result<Item> GetSelectedItem(ICombatState combatState, ICollection<Item> possibleItems)
+ {
+ var character = combatState.Game.Character;
+ if (character is null)
+ {
+ return new ItemNotFoundError();
+ }
+
+ if (ShouldUseHpItem(character))
+ {
+ var item = possibleItems.FirstOrDefault(x => UseHealthItemsVNums.Contains(x.ItemVNum));
+ if (item is not null)
+ {
+ return item;
+ }
+ }
+
+ if (ShouldUseMpItem(character))
+ {
+ var item = possibleItems.FirstOrDefault(x => UseManaItemsVNums.Contains(x.ItemVNum));
+ if (item is not null)
+ {
+ return item;
+ }
+ }
+
+ return new ItemNotFoundError();
+ }
+
+ /// <inheritdoc />
+ public Result<bool> ShouldUseItem(ICombatState combatState)
+ {
+ if (!UseItems)
+ {
+ return false;
+ }
+
+ var character = combatState.Game.Character;
+ if (character is null)
+ {
+ return false;
+ }
+
+ return ShouldUseHpItem(character) || ShouldUseMpItem(character);
+ }
+
+ private bool ShouldUseHpItem(Character character)
+ {
+ return character.Hp is not null && character.Hp.Percentage is not null
+ && character.Hp.Percentage < UseBelowHealthPercentage;
+ }
+
+ private bool ShouldUseMpItem(Character character)
+ {
+ return character.Mp is not null && character.Mp.Percentage is not null
+ && character.Mp.Percentage < UseBelowManaPercentage;
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Policies/UseSkillPolicy.cs => Extensions/NosSmooth.Extensions.Combat/Policies/UseSkillPolicy.cs +58 -0
@@ 0,0 1,58 @@
+//
+// UseSkillPolicy.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.Data.Abstractions.Enums;
+using NosSmooth.Extensions.Combat.Errors;
+using NosSmooth.Extensions.Combat.Selectors;
+using NosSmooth.Game.Data.Characters;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Policies;
+
+/// <summary>
+/// The policy to use a skill.
+/// </summary>
+/// <param name="PreferTargetedSkills">Whether to prefer targeted skills (true) or area skills (false).</param>
+/// <param name="AllowedSkillVNums">The vnums of the skills that are allowed to be used.</param>
+public record UseSkillPolicy(bool PreferTargetedSkills, int[]? AllowedSkillVNums)
+ : ISkillSelector
+{
+ /// <inheritdoc />
+ public Result<Skill> GetSelectedSkill(IEnumerable<Skill> usableSkills)
+ {
+ var skills = usableSkills.Where(x => CanBeUsed(x))
+ .Reverse();
+
+ if (PreferTargetedSkills)
+ {
+ skills = skills.OrderBy(x => x.Info!.HitType == HitType.EnemiesInZone ? 1 : 0);
+ }
+
+ var skill = skills.FirstOrDefault();
+ if (skill is null)
+ {
+ return new SkillNotFoundError();
+ }
+
+ return skill;
+ }
+
+ private bool CanBeUsed(Skill skill)
+ {
+ if (AllowedSkillVNums is not null && !AllowedSkillVNums.Contains(skill.SkillVNum))
+ {
+ return false;
+ }
+
+ if (skill.Info is null)
+ {
+ return false;
+ }
+
+ return skill.Info.HitType is HitType.EnemiesInZone or HitType.TargetOnly
+ && skill.Info.TargetType is TargetType.Target or TargetType.NoTarget;
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Responders/CancelResponder.cs => Extensions/NosSmooth.Extensions.Combat/Responders/CancelResponder.cs +35 -0
@@ 0,0 1,35 @@
+//
+// CancelResponder.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Core.Packets;
+using NosSmooth.Packets.Server.Skills;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Responders;
+
+/// <summary>
+/// Responds to cancel packet.
+/// </summary>
+public class CancelResponder : IPacketResponder<CancelPacket>
+{
+ private readonly CombatManager _combatManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CancelResponder"/> class.
+ /// </summary>
+ /// <param name="combatManager">The combat manager.</param>
+ public CancelResponder(CombatManager combatManager)
+ {
+ _combatManager = combatManager;
+ }
+
+ /// <inheritdoc />
+ public Task<Result> Respond(PacketEventArgs<CancelPacket> packetArgs, CancellationToken ct = default)
+ {
+ _combatManager.CancelSkillTokensAsync(ct);
+ return Task.FromResult(Result.FromSuccess());
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Responders/SuResponder.cs => Extensions/NosSmooth.Extensions.Combat/Responders/SuResponder.cs +35 -0
@@ 0,0 1,35 @@
+//
+// SuResponder.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Core.Packets;
+using NosSmooth.Packets.Server.Battle;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Responders;
+
+/// <summary>
+/// Responds to su packet.
+/// </summary>
+public class SuResponder : IPacketResponder<SuPacket>
+{
+ private readonly CombatManager _combatManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SuResponder"/> class.
+ /// </summary>
+ /// <param name="combatManager">The combat manager.</param>
+ public SuResponder(CombatManager combatManager)
+ {
+ _combatManager = combatManager;
+ }
+
+ /// <inheritdoc />
+ public Task<Result> Respond(PacketEventArgs<SuPacket> packetArgs, CancellationToken ct = default)
+ {
+ _combatManager.CancelSkillTokensAsync(ct);
+ return Task.FromResult(Result.FromSuccess());
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Selectors/IEnemySelector.cs => Extensions/NosSmooth.Extensions.Combat/Selectors/IEnemySelector.cs +24 -0
@@ 0,0 1,24 @@
+//
+// IEnemySelector.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.Entities;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Selectors;
+
+/// <summary>
+/// Selects an enemy from the possible enemies.
+/// </summary>
+public interface IEnemySelector
+{
+ /// <summary>
+ /// Gets the entity to be currently selected.
+ /// </summary>
+ /// <param name="combatState">The combat state.</param>
+ /// <param name="possibleTargets">The collection of possible targets.</param>
+ /// <returns>The selected entity, or an error.</returns>
+ public Result<ILivingEntity> GetSelectedEntity(ICombatState combatState, ICollection<ILivingEntity> possibleTargets);
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Selectors/IItemSelector.cs => Extensions/NosSmooth.Extensions.Combat/Selectors/IItemSelector.cs +31 -0
@@ 0,0 1,31 @@
+//
+// IItemSelector.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.Items;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Selectors;
+
+/// <summary>
+/// Selects an item to be used.
+/// </summary>
+public interface IItemSelector
+{
+ /// <summary>
+ /// Gets the entity to be currently selected.
+ /// </summary>
+ /// <param name="combatState">The combat state.</param>
+ /// <param name="possibleItems">The items that may be used.</param>
+ /// <returns>The selected item, or an error.</returns>
+ public Result<Item> GetSelectedItem(ICombatState combatState, ICollection<Item> possibleItems);
+
+ /// <summary>
+ /// Gets whether currently an item should be used.
+ /// </summary>
+ /// <param name="combatState">The combat state.</param>
+ /// <returns>Whether to use an item or an error.</returns>
+ public Result<bool> ShouldUseItem(ICombatState combatState);
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Selectors/ISkillSelector.cs => Extensions/NosSmooth.Extensions.Combat/Selectors/ISkillSelector.cs +23 -0
@@ 0,0 1,23 @@
+//
+// ISkillSelector.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 Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Selectors;
+
+/// <summary>
+/// Selects a skill to use from a possible skills.
+/// </summary>
+public interface ISkillSelector
+{
+ /// <summary>
+ /// Gets the skill to use.
+ /// </summary>
+ /// <param name="usableSkills">The skills that may be used. Won't contain skills the user doesn't have mana for and that are on cooldown.</param>
+ /// <returns>The skill to use, or an error.</returns>
+ public Result<Skill> GetSelectedSkill(IEnumerable<Skill> usableSkills);
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Techniques/ICombatTechnique.cs => Extensions/NosSmooth.Extensions.Combat/Techniques/ICombatTechnique.cs +48 -0
@@ 0,0 1,48 @@
+//
+// ICombatTechnique.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Extensions.Combat.Operations;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Techniques;
+
+/// <summary>
+/// A combat technique that allows to handle the whole combat situations using step callbacks.
+/// </summary>
+/// <remarks>
+/// The callback methods decide the next steps, used in <see cref="CombatManager"/>.
+/// </remarks>
+public interface ICombatTechnique
+{
+ /// <summary>
+ /// Should check whether the technique should process more steps or quit the combat.
+ /// </summary>
+ /// <param name="state">The combat state.</param>
+ /// <returns>Whether to continue with steps.</returns>
+ public bool ShouldContinue(ICombatState state);
+
+ /// <summary>
+ /// Handle one step that should enqueue an operation.
+ /// </summary>
+ /// <remarks>
+ /// If error is returned, the combat will be cancelled.
+ /// </remarks>
+ /// <param name="state">The combat state.</param>
+ /// <returns>An id of the current target entity or an error.</returns>
+ public Result<long?> HandleCombatStep(ICombatState state);
+
+ /// <summary>
+ /// Handles an error from <see cref="ICombatOperation.UseAsync"/>.
+ /// </summary>
+ /// <remarks>
+ /// If an error is returned, the combat will be cancelled.
+ /// </remarks>
+ /// <param name="state">The combat state.</param>
+ /// <param name="operation">The combat operation that returned an error.</param>
+ /// <param name="result">The errorful result.</param>
+ /// <returns>A result that may or may not succeed.</returns>
+ public Result HandleError(ICombatState state, ICombatOperation operation, Result result);
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Combat/Techniques/SimpleAttackTechnique.cs => Extensions/NosSmooth.Extensions.Combat/Techniques/SimpleAttackTechnique.cs +149 -0
@@ 0,0 1,149 @@
+//
+// SimpleAttackTechnique.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Extensions.Combat.Errors;
+using NosSmooth.Extensions.Combat.Extensions;
+using NosSmooth.Extensions.Combat.Operations;
+using NosSmooth.Extensions.Combat.Selectors;
+using NosSmooth.Extensions.Pathfinding;
+using NosSmooth.Game.Data.Characters;
+using NosSmooth.Game.Data.Entities;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Combat.Techniques;
+
+/// <summary>
+/// A combat technique that will attack on the specified enemy, walk within range and use skill until the enemy is dead.
+/// </summary>
+public class SimpleAttackTechnique : ICombatTechnique
+{
+ private readonly long _targetId;
+ private readonly WalkManager _walkManager;
+ private readonly ISkillSelector _skillSelector;
+
+ private Skill? _currentSkill;
+ private ILivingEntity? _target;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SimpleAttackTechnique"/> class.
+ /// </summary>
+ /// <param name="targetId">The target entity id.</param>
+ /// <param name="walkManager">The walk manager.</param>
+ /// <param name="skillSelector">The skill selector.</param>
+ public SimpleAttackTechnique
+ (
+ long targetId,
+ WalkManager walkManager,
+ ISkillSelector skillSelector
+ )
+ {
+ _targetId = targetId;
+ _walkManager = walkManager;
+ _skillSelector = skillSelector;
+ }
+
+ /// <inheritdoc />
+ public bool ShouldContinue(ICombatState state)
+ {
+ var map = state.Game.CurrentMap;
+ if (map is null)
+ {
+ return false;
+ }
+
+ var entity = map.Entities.GetEntity<ILivingEntity>(_targetId);
+ return !(entity is null || (entity.Hp is not null && (entity.Hp.Amount <= 0 || entity.Hp.Percentage <= 0)));
+ }
+
+ /// <inheritdoc />
+ public Result<long?> HandleCombatStep(ICombatState state)
+ {
+ var map = state.Game.CurrentMap;
+ if (map is null)
+ {
+ return new MapNotInitializedError();
+ }
+
+ if (_target is null)
+ {
+ var entity = map.Entities.GetEntity<ILivingEntity>(_targetId);
+ if (entity is null)
+ {
+ return new EntityNotFoundError();
+ }
+
+ _target = entity;
+ }
+
+ var character = state.Game.Character;
+ if (character is null)
+ {
+ return new CharacterNotInitializedError();
+ }
+
+ if (_currentSkill is null)
+ {
+ var skills = character.Skills;
+ if (skills is null)
+ {
+ return new CharacterNotInitializedError("Skills");
+ }
+
+ var characterMp = character.Mp?.Amount ?? 0;
+ var usableSkills = new[] { skills.PrimarySkill, skills.SecondarySkill }
+ .Concat(skills.OtherSkills)
+ .Where(x => !x.IsOnCooldown && characterMp >= (x.Info?.MpCost ?? long.MaxValue));
+
+ var skillResult = _skillSelector.GetSelectedSkill(usableSkills);
+ if (!skillResult.IsSuccess)
+ {
+ if (skillResult.Error is SkillNotFoundError)
+ {
+ return _target.Id;
+ }
+
+ return Result<long?>.FromError(skillResult);
+ }
+
+ _currentSkill = skillResult.Entity;
+ }
+
+ if (_currentSkill.Info is null)
+ {
+ var currentSkill = _currentSkill;
+ _currentSkill = null;
+ return new MissingInfoError("skill", currentSkill.SkillVNum);
+ }
+
+ if (character.Position is null)
+ {
+ return new CharacterNotInitializedError("Position");
+ }
+
+ if (_target.Position is null)
+ {
+ return new EntityNotFoundError();
+ }
+
+ if (!character.Position.Value.IsInRange(_target.Position.Value, _currentSkill.Info.Range))
+ {
+ state.WalkInRange(_walkManager, _target, _currentSkill.Info.Range);
+ }
+ else
+ {
+ state.UseSkill(_currentSkill, _target);
+ _currentSkill = null;
+ }
+
+ return _target.Id;
+ }
+
+ /// <inheritdoc />
+ public Result HandleError(ICombatState state, ICombatOperation operation, Result result)
+ {
+ return result;
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Pathfinding/Errors/PathNotFoundError.cs => Extensions/NosSmooth.Extensions.Pathfinding/Errors/PathNotFoundError.cs +16 -0
@@ 0,0 1,16 @@
+//
+// PathNotFoundError.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.Extensions.Pathfinding.Errors;
+
+/// <summary>
+/// Could not find path to the given target.
+/// </summary>
+/// <param name="TargetX">The target x coordinate.</param>
+/// <param name="TargetY">The target y coordinate.</param>
+public record PathNotFoundError(short TargetX, short TargetY) : ResultError($"Path to {TargetX}, {TargetY} not found.");<
\ No newline at end of file
M Extensions/NosSmooth.Extensions.Pathfinding/NosSmooth.Extensions.Pathfinding.csproj => Extensions/NosSmooth.Extensions.Pathfinding/NosSmooth.Extensions.Pathfinding.csproj +4 -0
@@ 8,6 8,10 @@
<RepositoryUrl>https://github.com/Rutherther/NosSmooth/</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Description>NosSmooth extension allowing for finding paths on maps.</Description>
+ <Version>1.1.0</Version>
+ <PackageReleaseNotes>Fix bugs to stop returning non-existing errors.</PackageReleaseNotes>
+ <AssemblyVersion>1.1.0</AssemblyVersion>
+ <FileVersion>1.1.0</FileVersion>
</PropertyGroup>
<ItemGroup>
M Extensions/NosSmooth.Extensions.Pathfinding/Pathfinder.cs => Extensions/NosSmooth.Extensions.Pathfinding/Pathfinder.cs +2 -2
@@ 92,7 92,7 @@ public class Pathfinder
{
if (!mapInfo.IsWalkable(targetX, targetY))
{
- return new NotFoundError("The requested target is not walkable, path cannot be found.");
+ return new PathNotFoundError(targetX, targetY);
}
if (x == targetX && y == targetY)
@@ 159,7 159,7 @@ public class Pathfinder
}
}
- return new NotFoundError("Could not find path to the given position.");
+ return new PathNotFoundError(targetX, targetY);
}
private Path ReconstructPath
M NosSmooth.sln => NosSmooth.sln +15 -0
@@ 48,6 48,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileClient", "Samples\FileC
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{3A6D13E3-4BBA-4B3D-AE99-BAC8B375F7DF}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Extensions.Combat", "Extensions\NosSmooth.Extensions.Combat\NosSmooth.Extensions.Combat.csproj", "{21F7EA0B-5E3C-4016-8ADD-28AF37C00782}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Extensions.Pathfinding", "Extensions\NosSmooth.Extensions.Pathfinding\NosSmooth.Extensions.Pathfinding.csproj", "{564CAD6F-09B1-450B-83ED-9BCDE106B646}"
EndProject
Global
@@ 228,6 230,18 @@ Global
{D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Release|x64.Build.0 = Release|Any CPU
{D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Release|x86.ActiveCfg = Release|Any CPU
{D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Release|x86.Build.0 = Release|Any CPU
+ {21F7EA0B-5E3C-4016-8ADD-28AF37C00782}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {21F7EA0B-5E3C-4016-8ADD-28AF37C00782}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {21F7EA0B-5E3C-4016-8ADD-28AF37C00782}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {21F7EA0B-5E3C-4016-8ADD-28AF37C00782}.Debug|x64.Build.0 = Debug|Any CPU
+ {21F7EA0B-5E3C-4016-8ADD-28AF37C00782}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {21F7EA0B-5E3C-4016-8ADD-28AF37C00782}.Debug|x86.Build.0 = Debug|Any CPU
+ {21F7EA0B-5E3C-4016-8ADD-28AF37C00782}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {21F7EA0B-5E3C-4016-8ADD-28AF37C00782}.Release|Any CPU.Build.0 = Release|Any CPU
+ {21F7EA0B-5E3C-4016-8ADD-28AF37C00782}.Release|x64.ActiveCfg = Release|Any CPU
+ {21F7EA0B-5E3C-4016-8ADD-28AF37C00782}.Release|x64.Build.0 = Release|Any CPU
+ {21F7EA0B-5E3C-4016-8ADD-28AF37C00782}.Release|x86.ActiveCfg = Release|Any CPU
+ {21F7EA0B-5E3C-4016-8ADD-28AF37C00782}.Release|x86.Build.0 = Release|Any CPU
{564CAD6F-09B1-450B-83ED-9BCDE106B646}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{564CAD6F-09B1-450B-83ED-9BCDE106B646}.Debug|Any CPU.Build.0 = Debug|Any CPU
{564CAD6F-09B1-450B-83ED-9BCDE106B646}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ 259,6 273,7 @@ Global
{7C9C7375-6FC0-4704-9332-1F74CDF41D11} = {01B5E872-271F-4D30-A1AA-AD48D81840C5}
{055C66A7-640C-49BB-81A7-28E630F51C37} = {99E72557-BCE9-496A-B49C-79537B0E6063}
{D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B} = {99E72557-BCE9-496A-B49C-79537B0E6063}
+ {21F7EA0B-5E3C-4016-8ADD-28AF37C00782} = {3A6D13E3-4BBA-4B3D-AE99-BAC8B375F7DF}
{564CAD6F-09B1-450B-83ED-9BCDE106B646} = {3A6D13E3-4BBA-4B3D-AE99-BAC8B375F7DF}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
M Packets/NosSmooth.PacketSerializer/Converters/Packets/UpgradeRareSubPacketConverter.cs => Packets/NosSmooth.PacketSerializer/Converters/Packets/UpgradeRareSubPacketConverter.cs +5 -0
@@ 44,6 44,11 @@ public class UpgradeRareSubPacketConverter : BaseStringConverter<UpgradeRareSubP
return new CouldNotConvertError(this, token.ToString(), "The string is not two/three characters long.");
}
+ if (token == "0")
+ {
+ return Result<UpgradeRareSubPacket?>.FromSuccess(new UpgradeRareSubPacket(0, 0));
+ }
+
var upgradeString = token.Slice(0, token.Length - 1);
var rareString = token.Slice(token.Length - 1);
M Packets/NosSmooth.PacketSerializer/NosSmooth.PacketSerializer.csproj => Packets/NosSmooth.PacketSerializer/NosSmooth.PacketSerializer.csproj +3 -1
@@ 9,7 9,9 @@
<RepositoryUrl>https://github.com/Rutherther/NosSmooth/</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageVersion>1.0.2</PackageVersion>
- <PackageReleaseNotes>Seek through the enumerator when null value type encountered.</PackageReleaseNotes>
+ <PackageReleaseNotes>Fix UpgradeRareSubPacket to work with "0".</PackageReleaseNotes>
+ <AssemblyVersion>1.0.3</AssemblyVersion>
+ <FileVersion>1.0.3</FileVersion>
</PropertyGroup>
<ItemGroup>
M Packets/NosSmooth.Packets/NosSmooth.Packets.csproj => Packets/NosSmooth.Packets/NosSmooth.Packets.csproj +2 -2
@@ 7,9 7,9 @@
<Description>Contains default NosTale packets.</Description>
<RepositoryUrl>https://github.com/Rutherther/NosSmooth/</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
- <PackageVersion>2.2.1</PackageVersion>
+ <PackageVersion>2.3.1</PackageVersion>
<TargetFrameworks>net7.0;netstandard2.1</TargetFrameworks>
- <PackageReleaseNotes>Fix cinfo, eq, cancel packets.</PackageReleaseNotes>
+ <PackageReleaseNotes>Update packets to 2023 versions</PackageReleaseNotes>
</PropertyGroup>
<ItemGroup>
M Packets/NosSmooth.Packets/Server/Skills/SkiPacket.cs => Packets/NosSmooth.Packets/Server/Skills/SkiPacket.cs +4 -2
@@ 19,9 19,11 @@ namespace NosSmooth.Packets.Server.Skills;
public record SkiPacket
(
[PacketIndex(0)]
- int PrimarySkillVNum,
+ int? Unknown,
[PacketIndex(1)]
+ int PrimarySkillVNum,
+ [PacketIndex(2)]
int SecondarySkillVNum,
- [PacketListIndex(2, InnerSeparator = '|', ListSeparator = ' ')]
+ [PacketListIndex(3, InnerSeparator = '|', ListSeparator = ' ')]
IReadOnlyList<SkiSubPacket> SkillSubPackets
) : IPacket;=
\ No newline at end of file
M Packets/NosSmooth.Packets/Server/Skills/SkiSubPacket.cs => Packets/NosSmooth.Packets/Server/Skills/SkiSubPacket.cs +2 -2
@@ 19,6 19,6 @@ public record SkiSubPacket
(
[PacketIndex(0)]
int SkillVNum,
- [PacketIndex(1)]
- byte Rank
+ [PacketIndex(1, IsOptional = true)]
+ byte? Rank
) : IPacket;=
\ No newline at end of file
A Tests/NosSmooth.Packets.Tests/Converters/Packets/SkiPacketConverterTests.cs => Tests/NosSmooth.Packets.Tests/Converters/Packets/SkiPacketConverterTests.cs +67 -0
@@ 0,0 1,67 @@
+//
+// SkiPacketConverterTests.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Extensions.DependencyInjection;
+using NosSmooth.Packets.Enums;
+using NosSmooth.Packets.Extensions;
+using NosSmooth.Packets.Packets;
+using NosSmooth.Packets.Server.Groups;
+using NosSmooth.Packets.Server.Skills;
+using NosSmooth.PacketSerializer.Abstractions.Attributes;
+using Xunit;
+
+namespace NosSmooth.Packets.Tests.Converters.Packets;
+
+/// <summary>
+/// Tests ski packet serializer.
+/// </summary>
+public class SkiPacketConverterTests
+{
+ private readonly IPacketSerializer _packetSerializer;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SkiPacketConverterTests"/> class.
+ /// </summary>
+ public SkiPacketConverterTests()
+ {
+ var provider = new ServiceCollection()
+ .AddPacketSerialization()
+ .BuildServiceProvider();
+
+ _packetSerializer = provider.GetRequiredService<IPacketSerializer>();
+ provider.GetRequiredService<IPacketTypesRepository>().AddDefaultPackets();
+ }
+
+ /// <summary>
+ /// Tests that ski packet is deserialized correctly.
+ /// </summary>
+ [Fact]
+ public void Converter_Deserialization_DeserializesCorrectly()
+ {
+ var deserialized = _packetSerializer.Deserialize
+ (
+ "ski 0 220 221 220 221 697|4 706|0 310 311",
+ PacketSource.Server
+ );
+
+ Assert.True(deserialized.IsSuccess);
+ var expected = new SkiPacket
+ (
+ 0,
+ 220,
+ 221,
+ new[]
+ {
+ new SkiSubPacket(220, null), new SkiSubPacket(221, null), new SkiSubPacket(697, 4),
+ new SkiSubPacket(706, 0), new SkiSubPacket(310, null), new SkiSubPacket(311, null)
+ }
+ );
+ var skiPacket = (SkiPacket)deserialized.Entity;
+ Assert.Equal(expected.PrimarySkillVNum, skiPacket.PrimarySkillVNum);
+ Assert.Equal(expected.SecondarySkillVNum, skiPacket.SecondarySkillVNum);
+ Assert.Equal(expected.SkillSubPackets, skiPacket.SkillSubPackets);
+ }
+}<
\ No newline at end of file