From fa4872fb51383373c52d64fc7028fef70bba3388 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Boh=C3=A1=C4=8Dek?= Date: Fri, 13 Jan 2023 10:40:15 +0100 Subject: [PATCH] feat(game): split nostale apis into safe, unsafe, add contracts usage --- .../NostaleChatApi.cs} | 10 +- .../NostaleMapApi.cs} | 87 +--- .../Apis/Safe/NostaleSkillsApi.cs | 426 ++++++++++++++++++ .../UnsafeInventoryApi.cs} | 61 ++- .../Apis/Unsafe/UnsafeMapApi.cs | 88 ++++ .../UnsafeMateApi.cs} | 11 +- .../UnsafeMateSkillsApi.cs} | 10 +- .../UnsafeSkillsApi.cs} | 175 ++++++- .../Attributes/UnsafeAttribute.cs | 28 -- .../Contracts/UseSkillErrors.cs | 28 ++ .../Contracts/UseSkillStates.cs | 37 ++ .../Errors/WrongSkillTargetError.cs | 19 + .../Extensions/ServiceCollectionExtensions.cs | 14 +- 13 files changed, 841 insertions(+), 153 deletions(-) rename Core/NosSmooth.Game/Apis/{NostaleChatPacketApi.cs => Safe/NostaleChatApi.cs} (95%) rename Core/NosSmooth.Game/Apis/{NostaleMapPacketApi.cs => Safe/NostaleMapApi.cs} (55%) create mode 100644 Core/NosSmooth.Game/Apis/Safe/NostaleSkillsApi.cs rename Core/NosSmooth.Game/Apis/{NostaleInventoryPacketApi.cs => Unsafe/UnsafeInventoryApi.cs} (62%) create mode 100644 Core/NosSmooth.Game/Apis/Unsafe/UnsafeMapApi.cs rename Core/NosSmooth.Game/Apis/{NostaleMatePacketApi.cs => Unsafe/UnsafeMateApi.cs} (92%) rename Core/NosSmooth.Game/Apis/{NostaleMateSkillsPacketApi.cs => Unsafe/UnsafeMateSkillsApi.cs} (90%) rename Core/NosSmooth.Game/Apis/{NostaleSkillsPacketApi.cs => Unsafe/UnsafeSkillsApi.cs} (63%) delete mode 100644 Core/NosSmooth.Game/Attributes/UnsafeAttribute.cs create mode 100644 Core/NosSmooth.Game/Contracts/UseSkillErrors.cs create mode 100644 Core/NosSmooth.Game/Contracts/UseSkillStates.cs create mode 100644 Core/NosSmooth.Game/Errors/WrongSkillTargetError.cs diff --git a/Core/NosSmooth.Game/Apis/NostaleChatPacketApi.cs b/Core/NosSmooth.Game/Apis/Safe/NostaleChatApi.cs similarity index 95% rename from Core/NosSmooth.Game/Apis/NostaleChatPacketApi.cs rename to Core/NosSmooth.Game/Apis/Safe/NostaleChatApi.cs index a560c26..a8e1a61 100644 --- a/Core/NosSmooth.Game/Apis/NostaleChatPacketApi.cs +++ b/Core/NosSmooth.Game/Apis/Safe/NostaleChatApi.cs @@ -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; /// /// Packet api for sending and receiving messages. /// -public class NostaleChatPacketApi +public class NostaleChatApi { // TODO: check length of the messages private readonly INostaleClient _client; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The nostale client. - public NostaleChatPacketApi(INostaleClient client) + public NostaleChatApi(INostaleClient client) { _client = client; } diff --git a/Core/NosSmooth.Game/Apis/NostaleMapPacketApi.cs b/Core/NosSmooth.Game/Apis/Safe/NostaleMapApi.cs similarity index 55% rename from Core/NosSmooth.Game/Apis/NostaleMapPacketApi.cs rename to Core/NosSmooth.Game/Apis/Safe/NostaleMapApi.cs index ebd5014..df4b2aa 100644 --- a/Core/NosSmooth.Game/Apis/NostaleMapPacketApi.cs +++ b/Core/NosSmooth.Game/Apis/Safe/NostaleMapApi.cs @@ -1,54 +1,40 @@ // -// 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; /// /// Packet api for managing maps in inventory. /// -public class NostaleMapPacketApi +public class NostaleMapApi { + private readonly Game _game; + private readonly UnsafeMapApi _unsafeMapApi; + /// /// The range the player may pick up items in. /// public static short PickUpRange => 5; - private readonly Game _game; - private readonly INostaleClient _client; - /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The game. - /// The client. - public NostaleMapPacketApi(Game game, INostaleClient client) + /// The unsafe map api. + public NostaleMapApi(Game game, UnsafeMapApi unsafeMapApi) { _game = game; - _client = client; + _unsafeMapApi = unsafeMapApi; } - /// - /// Use the given portal. - /// - /// The cancellation token for cancelling the operation. - /// A result that may or may not have succeeded. - [Unsafe("Portal position not checked.")] - public Task UsePortalAsync(CancellationToken ct = default) - => _client.SendPacketAsync(new PreqPacket(), ct); - /// /// Pick up the given item. /// @@ -86,28 +72,7 @@ public class NostaleMapPacketApi return new NotInRangeError("Character", characterPosition.Value, itemPosition.Value, PickUpRange); } - return await CharacterPickUpAsync(item.Id, ct); - } - - /// - /// Pick up the given item by character. - /// - /// - /// Unsafe, does not check anything. - /// - /// The id of the item. - /// The cancellation token used for cancelling the operation. - /// A result that may or may not have succeeded. - [Unsafe("Nor character distance, nor the existence of item is checked.")] - public async Task 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); } /// @@ -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); } - /// - /// Pick up the given item by pet. - /// - /// - /// Unsafe, does not check anything. - /// - /// The id of the item. - /// The cancellation token used for cancelling the operation. - /// A result that may or may not have succeeded. - [Unsafe("Nor pet distance to item nor whether the item exists is checked.")] - public async Task 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 diff --git a/Core/NosSmooth.Game/Apis/Safe/NostaleSkillsApi.cs b/Core/NosSmooth.Game/Apis/Safe/NostaleSkillsApi.cs new file mode 100644 index 0000000..2ad173e --- /dev/null +++ b/Core/NosSmooth.Game/Apis/Safe/NostaleSkillsApi.cs @@ -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; + +/// +/// A safe NosTale api for using character skills. +/// +public class NostaleSkillsApi +{ + private readonly Game _game; + private readonly INostaleClient _client; + private readonly Contractor _contractor; + + /// + /// Initializes a new instance of the class. + /// + /// The game. + /// The NosTale client. + /// The contractor. + public NostaleSkillsApi(Game game, INostaleClient client, Contractor contractor) + { + _game = game; + _client = client; + _contractor = contractor; + } + + /// + /// Use the given (targetable) skill on character himself. + /// + /// The skill to use. + /// The cancellation token for cancelling the operation. + /// A result that may or may not have succeeded. + public Task UseSkillOnCharacter + ( + Skill skill, + CancellationToken ct = default + ) + { + var character = _game.Character; + if (character is null) + { + return Task.FromResult(new NotInitializedError("Game.Character")); + } + + var skills = _game.Skills; + if (skills is null) + { + return Task.FromResult(new NotInitializedError("Game.Skills")); + } + + if (skill.IsOnCooldown) + { + return Task.FromResult(new SkillOnCooldownError(skill)); + } + + if (skill.Info is null) + { + return Task.FromResult(new NotInitializedError("skill info")); + } + + if (skill.Info.TargetType is not(TargetType.Self or TargetType.SelfOrTarget)) + { + return Task.FromResult(new WrongSkillTargetError(skill, character)); + } + + return _client.SendPacketAsync + ( + new UseSkillPacket + ( + skill.Info.CastId, + character.Type, + character.Id, + null, + null + ), + ct + ); + } + + /// + /// Use the given (targetable) skill on specified entity. + /// + /// + /// The skill won't be used if it is on cooldown. + /// For skills that can be used only on self, use of the character. + /// For skills that cannot be targeted on an entity, proceed to UseSkillAt. + /// + /// The skill to use. + /// The entity to use the skill on. + /// The x coordinate on the map. (Used for non targeted dashes etc., says where the dash will be to.) + /// The y coordinate on the map. (Used for non targeted dashes etc., says where the dash will be to.) + /// The cancellation token for cancelling the operation. + /// A result that may or may not have succeeded. + public Task 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(new NotInitializedError("Game.Skills")); + } + + if (skill.IsOnCooldown) + { + return Task.FromResult(new SkillOnCooldownError(skill)); + } + + if (skill.Info is null) + { + return Task.FromResult(new NotInitializedError("skill info")); + } + + if (skill.Info.TargetType is not(TargetType.Target or TargetType.SelfOrTarget)) + { + return Task.FromResult(new WrongSkillTargetError(skill, entity)); + } + + var entityPosition = entity.Position; + if (entityPosition is null) + { + return Task.FromResult(new NotInitializedError("entity position")); + } + + var characterPosition = _game.Character?.Position; + if (characterPosition is null) + { + return Task.FromResult(new NotInitializedError("character position")); + } + + if (!entityPosition.Value.IsInRange(characterPosition.Value, skill.Info.Range)) + { + return Task.FromResult + ( + 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 + ( + new NotInRangeError + ( + "Character", + characterPosition.Value, + mapPosition, + skill.Info.Range + ) + ); + } + } + + return _client.SendPacketAsync + ( + new UseSkillPacket + ( + skill.Info.CastId, + entity.Type, + entity.Id, + mapX, + mapY + ), + ct + ); + } + + /// + /// Use the given (targetable) skill on specified entity. + /// + /// + /// For skills that can be used only on self, use of the character. + /// For skills that cannot be targeted on an entity, proceed to UseSkillAt. + /// + /// The skill to use. + /// The id of the entity to use the skill on. + /// The x coordinate on the map. (Used for non targeted dashes etc., says where the dash will be to.) + /// The y coordinate on the map. (Used for non targeted dashes etc., says where the dash will be to.) + /// The cancellation token for cancelling the operation. + /// A result that may or may not have succeeded. + public Task 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(new NotInitializedError("Game.Map")); + } + + var entity = map.Entities.GetEntity(entityId); + if (entity is null) + { + return Task.FromResult(new NotFoundError($"Entity with id {entityId} was not found on the map.")); + } + + return UseSkillOn + ( + skill, + entity, + mapX, + mapY, + ct + ); + } + + /// + /// Use the given (aoe) skill on the specified place. + /// + /// + /// For skills that can have targets, proceed to UseSkillOn. + /// + /// The skill to use. + /// The x coordinate to use the skill at. + /// The y coordinate to use the skill at. + /// The cancellation token for cancelling the operation. + /// A result that may or may not have succeeded. + public Task UseSkillAt + ( + Skill skill, + short mapX, + short mapY, + CancellationToken ct = default + ) + { + var skills = _game.Skills; + if (skills is null) + { + return Task.FromResult(new NotInitializedError("Game.Skills")); + } + + if (skill.IsOnCooldown) + { + return Task.FromResult(new SkillOnCooldownError(skill)); + } + + if (skill.Info is null) + { + return Task.FromResult(new NotInitializedError("skill info")); + } + + if (skill.Info.TargetType is not TargetType.NoTarget) + { + return Task.FromResult(new WrongSkillTargetError(skill, null)); + } + + var characterPosition = _game.Character?.Position; + if (characterPosition is null) + { + return Task.FromResult(new NotInitializedError("character position")); + } + + var target = new Position(mapX, mapY); + if (!target.IsInRange(characterPosition.Value, skill.Info.Range)) + { + return Task.FromResult + ( + new NotInRangeError + ( + "Character", + characterPosition.Value, + target, + skill.Info.Range + ) + ); + } + + return _client.SendPacketAsync + ( + new UseAOESkillPacket(skill.Info.CastId, mapX, mapY), + ct + ); + } + + /// + /// Creates a contract for using a skill on character himself. + /// + /// The skill to use. + /// The contract or an error. + public Result> ContractUseSkillOnCharacter + ( + Skill skill + ) + { + var characterId = _game?.Character?.Id; + if (characterId is null) + { + return new NotInitializedError("Game.Character"); + } + + return Result>.FromSuccess + ( + UnsafeSkillsApi.CreateUseSkillContract + ( + _contractor, + skill.SkillVNum, + characterId.Value, + ct => UseSkillOnCharacter + ( + skill, + ct + ) + ) + ); + } + + /// + /// Creates a contract for using a skill on the given entity. + /// + /// The skill to use. + /// The entity to use the skill on. + /// The x coordinate to use the skill at. + /// The y coordinate to use the skill at. + /// The contract or an error. + public Result> 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>.FromSuccess + ( + UnsafeSkillsApi.CreateUseSkillContract + ( + _contractor, + skill.SkillVNum, + characterId.Value, + ct => UseSkillOn + ( + skill, + entity, + mapX, + mapY, + ct + ) + ) + ); + } + + /// + /// Creates a contract for using a skill at the given location. + /// + /// The skill to use. + /// The x coordinate to use the skill at. + /// The y coordinate to use the skill at. + /// The contract or an error. + public Result> ContractUseSkillAt + ( + Skill skill, + short mapX, + short mapY + ) + { + var characterId = _game?.Character?.Id; + if (characterId is null) + { + return new NotInitializedError("Game.Character"); + } + + return Result>.FromSuccess + ( + UnsafeSkillsApi.CreateUseSkillContract + ( + _contractor, + skill.SkillVNum, + characterId.Value, + ct => UseSkillAt + ( + skill, + mapX, + mapY, + ct + ) + ) + ); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Game/Apis/NostaleInventoryPacketApi.cs b/Core/NosSmooth.Game/Apis/Unsafe/UnsafeInventoryApi.cs similarity index 62% rename from Core/NosSmooth.Game/Apis/NostaleInventoryPacketApi.cs rename to Core/NosSmooth.Game/Apis/Unsafe/UnsafeInventoryApi.cs index 4bf0a1c..7fedd11 100644 --- a/Core/NosSmooth.Game/Apis/NostaleInventoryPacketApi.cs +++ b/Core/NosSmooth.Game/Apis/Unsafe/UnsafeInventoryApi.cs @@ -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; /// /// Packet api for managing items in inventory. /// -public class NostaleInventoryPacketApi +public class UnsafeInventoryApi { private readonly INostaleClient _client; + private readonly Contractor _contractor; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The nostale client. - public NostaleInventoryPacketApi(INostaleClient client) + /// The contractor. + public UnsafeInventoryApi(INostaleClient client, Contractor contractor) { _client = client; + _contractor = contractor; } /// @@ -44,6 +54,47 @@ public class NostaleInventoryPacketApi ) => _client.SendPacketAsync(new PutPacket(bag, slot, amount), ct); + /// + /// Creates a contract for dropping an item. + /// Returns the ground item that was thrown on the ground. + /// + /// The inventory bag. + /// The inventory slot. + /// The amount to drop. + /// A contract representing the drop operation. + public IContract ContractDropItem + ( + BagType bag, + InventorySlot slot, + short amount + ) + { + // TODO: confirm dialog. + return new ContractBuilder(_contractor, DefaultStates.None) + .SetMoveAction + ( + DefaultStates.None, + async (_, ct) => + { + await DropItemAsync(bag, slot.Slot, amount, ct); + return true; + }, + DefaultStates.Requested + ) + .SetMoveFilter + ( + DefaultStates.Requested, + data => data.Item.Amount == amount && data.Item.VNum == slot.Item?.ItemVNum, + DefaultStates.ResponseObtained + ) + .SetFillData + ( + DefaultStates.Requested, + data => data.Item + ) + .Build(); + } + /// /// Move the given item within one bag. /// diff --git a/Core/NosSmooth.Game/Apis/Unsafe/UnsafeMapApi.cs b/Core/NosSmooth.Game/Apis/Unsafe/UnsafeMapApi.cs new file mode 100644 index 0000000..64a6033 --- /dev/null +++ b/Core/NosSmooth.Game/Apis/Unsafe/UnsafeMapApi.cs @@ -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; + +/// +/// Packet api for managing maps in inventory. +/// +public class UnsafeMapApi +{ + private readonly Game _game; + private readonly INostaleClient _client; + + /// + /// Initializes a new instance of the class. + /// + /// The game. + /// The client. + public UnsafeMapApi(Game game, INostaleClient client) + { + _game = game; + _client = client; + } + + /// + /// Use the given portal. + /// + /// The cancellation token for cancelling the operation. + /// A result that may or may not have succeeded. + public Task UsePortalAsync(CancellationToken ct = default) + => _client.SendPacketAsync(new PreqPacket(), ct); + + /// + /// Pick up the given item by character. + /// + /// + /// Unsafe, does not check anything. + /// + /// The id of the item. + /// The cancellation token used for cancelling the operation. + /// A result that may or may not have succeeded. + public async Task 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); + } + + /// + /// Pick up the given item by pet. + /// + /// + /// Unsafe, does not check anything. + /// + /// The id of the item. + /// The cancellation token used for cancelling the operation. + /// A result that may or may not have succeeded. + public async Task 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 diff --git a/Core/NosSmooth.Game/Apis/NostaleMatePacketApi.cs b/Core/NosSmooth.Game/Apis/Unsafe/UnsafeMateApi.cs similarity index 92% rename from Core/NosSmooth.Game/Apis/NostaleMatePacketApi.cs rename to Core/NosSmooth.Game/Apis/Unsafe/UnsafeMateApi.cs index f2820ae..194705e 100644 --- a/Core/NosSmooth.Game/Apis/NostaleMatePacketApi.cs +++ b/Core/NosSmooth.Game/Apis/Unsafe/UnsafeMateApi.cs @@ -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; /// /// Packet api for managing mates, company, stay, sending them back. /// -public class NostaleMatePacketApi +public class UnsafeMateApi { private readonly INostaleClient _client; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The client. - public NostaleMatePacketApi(INostaleClient client) + public UnsafeMateApi(INostaleClient client) { _client = client; } diff --git a/Core/NosSmooth.Game/Apis/NostaleMateSkillsPacketApi.cs b/Core/NosSmooth.Game/Apis/Unsafe/UnsafeMateSkillsApi.cs similarity index 90% rename from Core/NosSmooth.Game/Apis/NostaleMateSkillsPacketApi.cs rename to Core/NosSmooth.Game/Apis/Unsafe/UnsafeMateSkillsApi.cs index d145799..efbe7f3 100644 --- a/Core/NosSmooth.Game/Apis/NostaleMateSkillsPacketApi.cs +++ b/Core/NosSmooth.Game/Apis/Unsafe/UnsafeMateSkillsApi.cs @@ -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; /// /// Packet api for using mate skills. /// -public class NostaleMateSkillsPacketApi +public class UnsafeMateSkillsApi { private readonly INostaleClient _client; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The client. - public NostaleMateSkillsPacketApi(INostaleClient client) + public UnsafeMateSkillsApi(INostaleClient client) { _client = client; } diff --git a/Core/NosSmooth.Game/Apis/NostaleSkillsPacketApi.cs b/Core/NosSmooth.Game/Apis/Unsafe/UnsafeSkillsApi.cs similarity index 63% rename from Core/NosSmooth.Game/Apis/NostaleSkillsPacketApi.cs rename to Core/NosSmooth.Game/Apis/Unsafe/UnsafeSkillsApi.cs index 83a5a4e..4b649dd 100644 --- a/Core/NosSmooth.Game/Apis/NostaleSkillsPacketApi.cs +++ b/Core/NosSmooth.Game/Apis/Unsafe/UnsafeSkillsApi.cs @@ -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; /// /// Packet api for using character skills. /// -public class NostaleSkillsPacketApi +public class UnsafeSkillsApi { private readonly INostaleClient _client; private readonly Game _game; + private readonly Contractor _contractor; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The nostale client. /// The game. - public NostaleSkillsPacketApi(INostaleClient client, Game game) + /// The contractor. + public UnsafeSkillsApi(INostaleClient client, Game game, Contractor contractor) { _client = client; _game = game; + _contractor = contractor; } /// @@ -150,7 +157,6 @@ public class NostaleSkillsPacketApi /// Use the given (targetable) skill on specified entity. /// /// - /// The skill won't be used if it is on cooldown. /// For skills that can be used only on self, use of the character. /// For skills that cannot be targeted on an entity, proceed to UseSkillAt. /// @@ -169,11 +175,6 @@ public class NostaleSkillsPacketApi CancellationToken ct = default ) { - if (skill.IsOnCooldown) - { - return Task.FromResult(new SkillOnCooldownError(skill)); - } - if (skill.Info is null) { return Task.FromResult(new NotInitializedError("skill info")); @@ -217,12 +218,8 @@ public class NostaleSkillsPacketApi CancellationToken ct = default ) { - if (skill.IsOnCooldown) - { - return Task.FromResult(new SkillOnCooldownError(skill)); - } - - if (skill.Info is null) + var info = skill.Info; + if (info is null) { return Task.FromResult(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 /// /// For skills that can have targets, proceed to UseSkillOn. /// - /// The id of the skill. + /// The id of the skill. /// The x coordinate to use the skill at. /// The y coordinate to use the skill at. /// The cancellation token for cancelling the operation. /// A result that may or may not have succeeded. public Task 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(new SkillOnCooldownError(skill)); + return Task.FromResult(new NotInitializedError("skill info")); } return _client.SendPacketAsync ( - new UseAOESkillPacket(skill.SkillVNum, mapX, mapY), + new UseAOESkillPacket(info.CastId, mapX, mapY), ct ); } + + /// + /// Creates a contract for using a skill on the given entity. + /// + /// The skill to use. + /// The id of the entity to use the skill on. + /// The type of the supplied entity. + /// The x coordinate on the map. (Used for non targeted dashes etc., says where the dash will be to.) + /// The y coordinate on the map. (Used for non targeted dashes etc., says where the dash will be to.) + /// The contract or an error. + public Result> 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>.FromSuccess + ( + CreateUseSkillContract + ( + _contractor, + skill.SkillVNum, + characterId.Value, + ct => UseSkillOn + ( + skill.Info.CastId, + entityId, + entityType, + mapX, + mapY, + ct + ) + ) + ); + } + + /// + /// Creates a contract for using a skill at the given location. + /// + /// The skill to use. + /// The x coordinate to use the skill at. + /// The y coordinate to use the skill at. + /// The contract or an error. + public Result> 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>.FromSuccess + ( + CreateUseSkillContract + ( + _contractor, + skill.SkillVNum, + characterId.Value, + ct => + { + return UseSkillAt(skill.Info.CastId, mapX, mapY, ct); + } + ) + ); + } + + /// + /// Creates a use skill contract, + /// casting the skill using the given action. + /// + /// The contractor to register the contract at. + /// The vnum of the casting skill. + /// The id of the caster, character. + /// The used skill event. + /// A contract for using the given skill. + public static IContract CreateUseSkillContract + ( + Contractor contractor, + int skillVNum, + long characterId, + Func> useSkill + ) + { + return new ContractBuilder(contractor, UseSkillStates.None) + .SetMoveAction + ( + UseSkillStates.None, + async (data, ct) => (await useSkill(ct)).Map(true), + UseSkillStates.SkillUseRequested + ) + .SetMoveFilter + ( + UseSkillStates.SkillUseRequested, + data => data.Skill.SkillVNum == skillVNum && data.Caster.Id == characterId, + UseSkillStates.SkillUsedResponse + ) + .SetFillData + ( + UseSkillStates.SkillUsedResponse, + skillUseEvent => skillUseEvent + ) + .SetError(UseSkillStates.SkillUseRequested, _ => UseSkillErrors.Unknown) + .SetTimeout(UseSkillStates.SkillUsedResponse, TimeSpan.FromSeconds(1), UseSkillStates.CharacterRestored) + .Build(); + } } \ No newline at end of file diff --git a/Core/NosSmooth.Game/Attributes/UnsafeAttribute.cs b/Core/NosSmooth.Game/Attributes/UnsafeAttribute.cs deleted file mode 100644 index da05990..0000000 --- a/Core/NosSmooth.Game/Attributes/UnsafeAttribute.cs +++ /dev/null @@ -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; - -/// -/// The given method does not do some checks. -/// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Struct)] -public class UnsafeAttribute : Attribute -{ - /// - /// Initializes a new instance of the class. - /// - /// The reason. - public UnsafeAttribute(string reason) - { - Reason = reason; - } - - /// - /// Gets the unsafe reason. - /// - public string Reason { get; } -} \ No newline at end of file diff --git a/Core/NosSmooth.Game/Contracts/UseSkillErrors.cs b/Core/NosSmooth.Game/Contracts/UseSkillErrors.cs new file mode 100644 index 0000000..aacb2a9 --- /dev/null +++ b/Core/NosSmooth.Game/Contracts/UseSkillErrors.cs @@ -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; + +/// +/// Errors for using a skill. +/// +public enum UseSkillErrors +{ + /// + /// An unknown error has happened. + /// + Unknown, + + /// + /// The character does not have enough ammo. + /// + NoAmmo, + + /// + /// The character does not have enough mana. + /// + NoMana +} \ No newline at end of file diff --git a/Core/NosSmooth.Game/Contracts/UseSkillStates.cs b/Core/NosSmooth.Game/Contracts/UseSkillStates.cs new file mode 100644 index 0000000..522659f --- /dev/null +++ b/Core/NosSmooth.Game/Contracts/UseSkillStates.cs @@ -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; + +/// +/// States for contract for using a skill. +/// +public enum UseSkillStates +{ + /// + /// Skill use was not executed yet. + /// + None, + + /// + /// A skill use packet (u_s, u_as, etc.) was used, + /// awaiting a response from the server. + /// + SkillUseRequested, + + /// + /// The server has responded with a skill use, + /// (after cast time), the information about + /// the caster, target is filled. + /// + SkillUsedResponse, + + /// + /// Fired 1000 ms after , + /// the character may move after this. + /// + CharacterRestored +} \ No newline at end of file diff --git a/Core/NosSmooth.Game/Errors/WrongSkillTargetError.cs b/Core/NosSmooth.Game/Errors/WrongSkillTargetError.cs new file mode 100644 index 0000000..50e2146 --- /dev/null +++ b/Core/NosSmooth.Game/Errors/WrongSkillTargetError.cs @@ -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; + +/// +/// Skill was used at wrong target. +/// +/// The skill that was used. +/// The target of the skill. +public record WrongSkillTargetError(Skill Skill, ILivingEntity? Target) + : ResultError($"The skill {Skill.SkillVNum} was used at wrong target {Target}"); \ No newline at end of file diff --git a/Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs b/Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs index 09078a6..2ca67d5 100644 --- a/Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs +++ b/Core/NosSmooth.Game/Extensions/ServiceCollectionExtensions.cs @@ -8,6 +8,8 @@ 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; @@ -66,12 +68,12 @@ public static class ServiceCollectionExtensions .AddPacketResponder(); serviceCollection - .AddTransient() - .AddTransient() - .AddTransient() - .AddTransient() - .AddTransient() - .AddTransient(); + .AddTransient() + .AddTransient() + .AddTransient() + .AddTransient() + .AddTransient() + .AddTransient(); serviceCollection .AddScoped(); -- 2.48.1