From c4fcedefb7c78eaa71a09806509f9c3bca6f57d4 Mon Sep 17 00:00:00 2001 From: Rutherther Date: Sat, 31 Dec 2022 17:20:15 +0100 Subject: [PATCH] feat(samples): add simple pii bot sample --- NosSmooth.Local.sln | 10 + src/Samples/HighLevel/SimplePiiBot/Bot.cs | 200 ++++++++++++++++++ .../SimplePiiBot/Commands/ControlCommands.cs | 45 ++++ src/Samples/HighLevel/SimplePiiBot/DllMain.cs | 96 +++++++++ .../HighLevel/SimplePiiBot/HostedService.cs | 89 ++++++++ .../Responders/EntityJoinedResponder.cs | 51 +++++ .../SimplePiiBot/SimplePiiBot.csproj | 35 +++ 7 files changed, 526 insertions(+) create mode 100644 src/Samples/HighLevel/SimplePiiBot/Bot.cs create mode 100644 src/Samples/HighLevel/SimplePiiBot/Commands/ControlCommands.cs create mode 100644 src/Samples/HighLevel/SimplePiiBot/DllMain.cs create mode 100644 src/Samples/HighLevel/SimplePiiBot/HostedService.cs create mode 100644 src/Samples/HighLevel/SimplePiiBot/Responders/EntityJoinedResponder.cs create mode 100644 src/Samples/HighLevel/SimplePiiBot/SimplePiiBot.csproj diff --git a/NosSmooth.Local.sln b/NosSmooth.Local.sln index 96e76d237e7bfa120d8e8f7f962535ded5b1b2cf..8047b6079e8ec8c06b43fd67e0c9bacd49343d9d 100644 --- a/NosSmooth.Local.sln +++ b/NosSmooth.Local.sln @@ -39,6 +39,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".metadata", ".metadata", "{ Directory.Build.props = Directory.Build.props EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HighLevel", "HighLevel", "{8B99B1BD-0738-40C1-9961-4DBC7AF3E8CE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimplePiiBot", "src\Samples\HighLevel\SimplePiiBot\SimplePiiBot.csproj", "{A93B5A9F-68FC-4A17-AFAC-DAB22B617A50}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -57,6 +61,8 @@ Global {CADFC50F-8CEC-4254-99FF-E766DA0B9194} = {3FEDC05A-980C-4C96-923D-B2CD7D96CD7B} {CEC274A0-7FB7-4C21-B52E-DD79AB93DDB4} = {3FEDC05A-980C-4C96-923D-B2CD7D96CD7B} {7190312B-CFEE-49D3-8DAB-542C31071E87} = {D19F45EC-8E59-4F7C-A4C4-D0BD11C8C32B} + {8B99B1BD-0738-40C1-9961-4DBC7AF3E8CE} = {004AF2D8-8D02-4BDF-BD18-752C54D65F1C} + {A93B5A9F-68FC-4A17-AFAC-DAB22B617A50} = {8B99B1BD-0738-40C1-9961-4DBC7AF3E8CE} EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {CA2873D8-BD0B-4583-818D-B94A3C2ABBA3}.Debug|Any CPU.ActiveCfg = Debug|Win32 @@ -99,5 +105,9 @@ Global {7190312B-CFEE-49D3-8DAB-542C31071E87}.Debug|Any CPU.Build.0 = Debug|Any CPU {7190312B-CFEE-49D3-8DAB-542C31071E87}.Release|Any CPU.ActiveCfg = Release|Any CPU {7190312B-CFEE-49D3-8DAB-542C31071E87}.Release|Any CPU.Build.0 = Release|Any CPU + {A93B5A9F-68FC-4A17-AFAC-DAB22B617A50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A93B5A9F-68FC-4A17-AFAC-DAB22B617A50}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A93B5A9F-68FC-4A17-AFAC-DAB22B617A50}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A93B5A9F-68FC-4A17-AFAC-DAB22B617A50}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/src/Samples/HighLevel/SimplePiiBot/Bot.cs b/src/Samples/HighLevel/SimplePiiBot/Bot.cs new file mode 100644 index 0000000000000000000000000000000000000000..1a624d9af88cd23f27aa33bb291476415b307bad --- /dev/null +++ b/src/Samples/HighLevel/SimplePiiBot/Bot.cs @@ -0,0 +1,200 @@ +// +// Bot.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.Logging; +using NosSmooth.Core.Extensions; +using NosSmooth.Core.Stateful; +using NosSmooth.Extensions.Combat; +using NosSmooth.Extensions.Combat.Policies; +using NosSmooth.Extensions.Combat.Techniques; +using NosSmooth.Extensions.Pathfinding; +using NosSmooth.Game; +using NosSmooth.Game.Apis; +using NosSmooth.Game.Data.Characters; +using NosSmooth.Game.Data.Entities; +using NosSmooth.Game.Data.Info; +using NosSmooth.Game.Data.Maps; +using Remora.Results; + +namespace SimplePiiBot; + +/// +/// The pii bot. +/// +public class Bot : IStatefulEntity +{ + private static readonly long[] PiiPods = { 45, 46, 47, 48, 49, 50, 51, 52, 53 }; + private static readonly long[] Piis = { 36, 37, 38, 39, 40, 41, 42, 43, 44 }; + private static readonly long RangeSquared = 15 * 15; + private static readonly long MaxPiiCount = 15; + + private readonly NostaleChatPacketApi _chatPacketApi; + private readonly CombatManager _combatManager; + private readonly Game _game; + private readonly WalkManager _walkManager; + private readonly ILogger _logger; + private CancellationTokenSource? _startCt; + + /// + /// Initializes a new instance of the class. + /// + /// The chat packet api. + /// The combat manager. + /// The game. + /// The walk manager. + /// The logger. + public Bot + ( + NostaleChatPacketApi chatPacketApi, + CombatManager combatManager, + Game game, + WalkManager walkManager, + ILogger logger + ) + { + _chatPacketApi = chatPacketApi; + _combatManager = combatManager; + _game = game; + _walkManager = walkManager; + _logger = logger; + } + + /// + /// Start the bot. + /// + /// The cancellation token used for cancelling the operation. + /// A result that may or may not succeed. + public async Task StartAsync(CancellationToken ct = default) + { + if (_startCt is not null) + { + return new GenericError("The bot is already running."); + } + + Task.Run + ( + async () => + { + try + { + await Run(ct); + } + catch (Exception e) + { + _logger.LogError(e, "The bot threw an exception"); + } + } + ); + + return Result.FromSuccess(); + } + + private async Task Run(CancellationToken ct) + { + await _chatPacketApi.ReceiveSystemMessageAsync("Starting the bot.", ct: ct); + _startCt = CancellationTokenSource.CreateLinkedTokenSource(ct); + ct = _startCt.Token; + while (!ct.IsCancellationRequested) + { + var map = _game.CurrentMap; + if (map is null) + { + await _chatPacketApi.ReceiveSystemMessageAsync("The map is null, quitting. Change the map.", ct: ct); + await StopAsync(); + return; + } + + var character = _game.Character; + if (character is null || character.Position is null) + { + await _chatPacketApi.ReceiveSystemMessageAsync + ("The character is null, quitting. Change the map.", ct: ct); + await StopAsync(); + return; + } + + var entity = ChooseNextEntity(map, character, character.Position.Value); + if (entity is null) + { + await _chatPacketApi.ReceiveSystemMessageAsync + ("There are no piis in range.", ct: ct); + await StopAsync(); + return; + } + + var combatResult = await _combatManager.EnterCombatAsync + ( + new SimpleAttackTechnique + ( + entity.Id, + _walkManager, + new UseSkillPolicy(true, null) + ), + ct + ); + + if (!combatResult.IsSuccess) + { + _logger.LogResultError(combatResult); + await StopAsync(); + return; + } + } + } + + private ILivingEntity? ChooseNextEntity(Map map, Character character, Position characterPosition) + { + var piisCount = map.Entities + .GetEntities() + .Where(x => x.Position?.DistanceSquared(characterPosition) <= RangeSquared) + .OfType() + .Count(x => Piis.Contains(x.VNum) && x.Hp?.Percentage > 0); + + var choosingList = PiiPods; + if (piisCount >= MaxPiiCount) + { // max count of piis reached, choose pii instead of a pad + choosingList = Piis; + } + + return map.Entities.GetEntities() + .OfType() + .Where(x => x.Hp?.Percentage > 0) + .Where(x => x.Position?.DistanceSquared(characterPosition) <= RangeSquared) + .Where(x => choosingList.Contains(x.VNum)) + .MinBy + ( + x => x.Position is null + ? long.MaxValue + : characterPosition.DistanceSquared(x.Position.Value) + ); + } + + /// + /// Stop the bot. + /// + /// The cancellation token used for cancelling the operation. + /// A result that may or may not succeed. + public async Task StopAsync(CancellationToken ct = default) + { + var startCt = _startCt; + var messageResult = await _chatPacketApi.ReceiveSystemMessageAsync("Stopping the bot.", ct: ct); + if (startCt is not null) + { + try + { + startCt.Cancel(); + } + catch + { + // ignored + } + startCt.Dispose(); + } + _startCt = null; + + return messageResult; + } +} \ No newline at end of file diff --git a/src/Samples/HighLevel/SimplePiiBot/Commands/ControlCommands.cs b/src/Samples/HighLevel/SimplePiiBot/Commands/ControlCommands.cs new file mode 100644 index 0000000000000000000000000000000000000000..2a4ca096cfe09ce7fcd8297e4679452d5ab15457 --- /dev/null +++ b/src/Samples/HighLevel/SimplePiiBot/Commands/ControlCommands.cs @@ -0,0 +1,45 @@ +// +// ControlCommands.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 OneOf.Types; +using Remora.Commands.Attributes; +using Remora.Commands.Groups; +using Remora.Results; + +namespace SimplePiiBot.Commands; + +/// +/// Commands for controlling the bot. +/// +public class ControlCommands : CommandGroup +{ + private readonly Bot _bot; + + /// + /// Initializes a new instance of the class. + /// + /// The bot. + public ControlCommands(Bot bot) + { + _bot = bot; + } + + /// + /// Handle the start command. + /// + /// A result that may or may not succeed. + [Command("start")] + public async Task HandleStartAsync() + => await _bot.StartAsync(CancellationToken); + + /// + /// Handle the stop command. + /// + /// A result that may or may not succeed. + [Command("stop")] + public async Task HandleStopAsync() + => await _bot.StopAsync(CancellationToken); +} \ No newline at end of file diff --git a/src/Samples/HighLevel/SimplePiiBot/DllMain.cs b/src/Samples/HighLevel/SimplePiiBot/DllMain.cs new file mode 100644 index 0000000000000000000000000000000000000000..a94daf6b1b54dc93829cfcf9b852927fa8c641b8 --- /dev/null +++ b/src/Samples/HighLevel/SimplePiiBot/DllMain.cs @@ -0,0 +1,96 @@ +// +// DllMain.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.Runtime.InteropServices; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using NosSmooth.ChatCommands; +using NosSmooth.Core.Extensions; +using NosSmooth.Data.NOSFiles.Extensions; +using NosSmooth.Extensions.Combat.Extensions; +using NosSmooth.Extensions.Pathfinding.Extensions; +using NosSmooth.Game.Extensions; +using NosSmooth.LocalClient.Extensions; +using Remora.Commands.Extensions; +using SimplePiiBot.Commands; +using SimplePiiBot.Responders; + +namespace SimplePiiBot; + +/// +/// The entrypoint class. +/// +public class DllMain +{ + /// + /// Allocate console. + /// + /// Whether the operation was successful. + [DllImport("kernel32")] + public static extern bool AllocConsole(); + + /// + /// Represents the dll entrypoint method. + /// + [UnmanagedCallersOnly(EntryPoint = "Main")] + public static void Main() + { + AllocConsole(); + new Thread + ( + () => + { + try + { + MainEntry().GetAwaiter().GetResult(); + } + catch (Exception e) + { + Console.WriteLine(e.ToString()); + } + } + ).Start(); + } + + /// + /// The entrypoint method. + /// + /// A representing the asynchronous operation. + private static async Task MainEntry() + { + var host = Host.CreateDefaultBuilder() + .UseConsoleLifetime() + .ConfigureLogging + ( + b => + { + b + .ClearProviders() + .AddConsole(); + } + ) + .ConfigureServices + ( + s => + { + s.AddNostaleCore() + .AddNostaleGame() + .AddLocalClient() + .AddNostaleDataFiles() + .AddNostaleCombat() + .AddNostalePathfinding() + .AddSingleton() + .AddNostaleChatCommands() + .AddGameResponder() + .AddCommandTree() + .WithCommandGroup(); + s.AddHostedService(); + } + ).Build(); + await host.RunAsync(); + } +} \ No newline at end of file diff --git a/src/Samples/HighLevel/SimplePiiBot/HostedService.cs b/src/Samples/HighLevel/SimplePiiBot/HostedService.cs new file mode 100644 index 0000000000000000000000000000000000000000..f5246f3ee931a86cfa33de13299ac1b1ba164552 --- /dev/null +++ b/src/Samples/HighLevel/SimplePiiBot/HostedService.cs @@ -0,0 +1,89 @@ +// +// HostedService.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 Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using NosSmooth.Core.Client; +using NosSmooth.Core.Extensions; +using NosSmooth.Data.NOSFiles; +using NosSmooth.LocalBinding; +using NosSmooth.Packets.Extensions; +using NosSmooth.Packets.Packets; + +namespace SimplePiiBot; + +/// +/// The simple pii bot hosted service to start the client. +/// +public class HostedService : BackgroundService +{ + private readonly IServiceProvider _services; + private readonly IPacketTypesRepository _packetRepository; + private readonly NostaleDataFilesManager _filesManager; + private readonly NosBindingManager _bindingManager; + private readonly ILogger _logger; + private readonly IHostLifetime _lifetime; + + /// + /// Initializes a new instance of the class. + /// + /// The service provider. + /// The packet repository. + /// The file manager. + /// The binding manager. + /// The logger. + /// The lifetime. + public HostedService + ( + IServiceProvider services, + IPacketTypesRepository packetRepository, + NostaleDataFilesManager filesManager, + NosBindingManager bindingManager, + ILogger logger, + IHostLifetime lifetime + ) + { + _services = services; + _packetRepository = packetRepository; + _filesManager = filesManager; + _bindingManager = bindingManager; + _logger = logger; + _lifetime = lifetime; + } + + /// + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var packetResult = _packetRepository.AddDefaultPackets(); + if (!packetResult.IsSuccess) + { + _logger.LogResultError(packetResult); + return; + } + + var filesResult = _filesManager.Initialize(); + if (!filesResult.IsSuccess) + { + _logger.LogResultError(filesResult); + return; + } + + var bindingResult = _bindingManager.Initialize(); + if (!bindingResult.IsSuccess) + { + _logger.LogResultError(bindingResult); + return; + } + + var runResult = await _services.GetRequiredService().RunAsync(stoppingToken); + if (!runResult.IsSuccess) + { + _logger.LogResultError(runResult); + await _lifetime.StopAsync(default); + } + } +} \ No newline at end of file diff --git a/src/Samples/HighLevel/SimplePiiBot/Responders/EntityJoinedResponder.cs b/src/Samples/HighLevel/SimplePiiBot/Responders/EntityJoinedResponder.cs new file mode 100644 index 0000000000000000000000000000000000000000..37309332d9d73ba2d2e385273d988ad62b7fa90c --- /dev/null +++ b/src/Samples/HighLevel/SimplePiiBot/Responders/EntityJoinedResponder.cs @@ -0,0 +1,51 @@ +// +// EntityJoinedResponder.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.Apis; +using NosSmooth.Game.Data.Entities; +using NosSmooth.Game.Events.Core; +using NosSmooth.Game.Events.Entities; +using NosSmooth.Packets.Enums; +using Remora.Results; + +namespace SimplePiiBot.Responders; + +/// +/// Responds to entity joined map event. +/// +public class EntityJoinedResponder : IGameResponder +{ + private readonly Bot _bot; + private readonly NostaleChatPacketApi _chatApi; + + /// + /// Initializes a new instance of the class. + /// + /// The bot. + /// The chat packet api. + public EntityJoinedResponder(Bot bot, NostaleChatPacketApi chatApi) + { + _bot = bot; + _chatApi = chatApi; + } + + /// + public async Task Respond(EntityJoinedMapEvent gameEvent, CancellationToken ct = default) + { + if (gameEvent.Entity is Player player) + { + if (player.Authority > AuthorityType.User) + { + var result = await _bot.StopAsync(ct); + await _chatApi.ReceiveSystemMessageAsync("A GM has joined the map, stopping the bot.", ct: ct); + + return result; + } + } + + return Result.FromSuccess(); + } +} \ No newline at end of file diff --git a/src/Samples/HighLevel/SimplePiiBot/SimplePiiBot.csproj b/src/Samples/HighLevel/SimplePiiBot/SimplePiiBot.csproj new file mode 100644 index 0000000000000000000000000000000000000000..52307bf7080ea5c39f35479142f789615f029cb6 --- /dev/null +++ b/src/Samples/HighLevel/SimplePiiBot/SimplePiiBot.csproj @@ -0,0 +1,35 @@ + + + + net7.0 + enable + enable + true + true + + + + + + + + + + + + 7.0.0 + + + 7.0.0 + + + 7.0.0 + + + + + + + + +