M NosSmooth.Local.sln => NosSmooth.Local.sln +10 -0
@@ 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
A src/Samples/HighLevel/SimplePiiBot/Bot.cs => src/Samples/HighLevel/SimplePiiBot/Bot.cs +200 -0
@@ 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;
+
+/// <summary>
+/// The pii bot.
+/// </summary>
+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<Bot> _logger;
+ private CancellationTokenSource? _startCt;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Bot"/> class.
+ /// </summary>
+ /// <param name="chatPacketApi">The chat packet api.</param>
+ /// <param name="combatManager">The combat manager.</param>
+ /// <param name="game">The game.</param>
+ /// <param name="walkManager">The walk manager.</param>
+ /// <param name="logger">The logger.</param>
+ public Bot
+ (
+ NostaleChatPacketApi chatPacketApi,
+ CombatManager combatManager,
+ Game game,
+ WalkManager walkManager,
+ ILogger<Bot> logger
+ )
+ {
+ _chatPacketApi = chatPacketApi;
+ _combatManager = combatManager;
+ _game = game;
+ _walkManager = walkManager;
+ _logger = logger;
+ }
+
+ /// <summary>
+ /// Start the bot.
+ /// </summary>
+ /// <param name="ct">The cancellation token used for cancelling the operation.</param>
+ /// <returns>A result that may or may not succeed.</returns>
+ public async Task<Result> 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<Monster>()
+ .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<Monster>()
+ .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)
+ );
+ }
+
+ /// <summary>
+ /// Stop the bot.
+ /// </summary>
+ /// <param name="ct">The cancellation token used for cancelling the operation.</param>
+ /// <returns>A result that may or may not succeed.</returns>
+ public async Task<Result> 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
A src/Samples/HighLevel/SimplePiiBot/Commands/ControlCommands.cs => src/Samples/HighLevel/SimplePiiBot/Commands/ControlCommands.cs +45 -0
@@ 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;
+
+/// <summary>
+/// Commands for controlling the bot.
+/// </summary>
+public class ControlCommands : CommandGroup
+{
+ private readonly Bot _bot;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ControlCommands"/> class.
+ /// </summary>
+ /// <param name="bot">The bot.</param>
+ public ControlCommands(Bot bot)
+ {
+ _bot = bot;
+ }
+
+ /// <summary>
+ /// Handle the start command.
+ /// </summary>
+ /// <returns>A result that may or may not succeed.</returns>
+ [Command("start")]
+ public async Task<Result> HandleStartAsync()
+ => await _bot.StartAsync(CancellationToken);
+
+ /// <summary>
+ /// Handle the stop command.
+ /// </summary>
+ /// <returns>A result that may or may not succeed.</returns>
+ [Command("stop")]
+ public async Task<Result> HandleStopAsync()
+ => await _bot.StopAsync(CancellationToken);
+}<
\ No newline at end of file
A src/Samples/HighLevel/SimplePiiBot/DllMain.cs => src/Samples/HighLevel/SimplePiiBot/DllMain.cs +96 -0
@@ 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;
+
+/// <summary>
+/// The entrypoint class.
+/// </summary>
+public class DllMain
+{
+ /// <summary>
+ /// Allocate console.
+ /// </summary>
+ /// <returns>Whether the operation was successful.</returns>
+ [DllImport("kernel32")]
+ public static extern bool AllocConsole();
+
+ /// <summary>
+ /// Represents the dll entrypoint method.
+ /// </summary>
+ [UnmanagedCallersOnly(EntryPoint = "Main")]
+ public static void Main()
+ {
+ AllocConsole();
+ new Thread
+ (
+ () =>
+ {
+ try
+ {
+ MainEntry().GetAwaiter().GetResult();
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e.ToString());
+ }
+ }
+ ).Start();
+ }
+
+ /// <summary>
+ /// The entrypoint method.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ 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<Bot>()
+ .AddNostaleChatCommands()
+ .AddGameResponder<EntityJoinedResponder>()
+ .AddCommandTree()
+ .WithCommandGroup<ControlCommands>();
+ s.AddHostedService<HostedService>();
+ }
+ ).Build();
+ await host.RunAsync();
+ }
+}<
\ No newline at end of file
A src/Samples/HighLevel/SimplePiiBot/HostedService.cs => src/Samples/HighLevel/SimplePiiBot/HostedService.cs +89 -0
@@ 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;
+
+/// <summary>
+/// The simple pii bot hosted service to start the client.
+/// </summary>
+public class HostedService : BackgroundService
+{
+ private readonly IServiceProvider _services;
+ private readonly IPacketTypesRepository _packetRepository;
+ private readonly NostaleDataFilesManager _filesManager;
+ private readonly NosBindingManager _bindingManager;
+ private readonly ILogger<HostedService> _logger;
+ private readonly IHostLifetime _lifetime;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HostedService"/> class.
+ /// </summary>
+ /// <param name="services">The service provider.</param>
+ /// <param name="packetRepository">The packet repository.</param>
+ /// <param name="filesManager">The file manager.</param>
+ /// <param name="bindingManager">The binding manager.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="lifetime">The lifetime.</param>
+ public HostedService
+ (
+ IServiceProvider services,
+ IPacketTypesRepository packetRepository,
+ NostaleDataFilesManager filesManager,
+ NosBindingManager bindingManager,
+ ILogger<HostedService> logger,
+ IHostLifetime lifetime
+ )
+ {
+ _services = services;
+ _packetRepository = packetRepository;
+ _filesManager = filesManager;
+ _bindingManager = bindingManager;
+ _logger = logger;
+ _lifetime = lifetime;
+ }
+
+ /// <inheritdoc />
+ 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<INostaleClient>().RunAsync(stoppingToken);
+ if (!runResult.IsSuccess)
+ {
+ _logger.LogResultError(runResult);
+ await _lifetime.StopAsync(default);
+ }
+ }
+}<
\ No newline at end of file
A src/Samples/HighLevel/SimplePiiBot/Responders/EntityJoinedResponder.cs => src/Samples/HighLevel/SimplePiiBot/Responders/EntityJoinedResponder.cs +51 -0
@@ 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;
+
+/// <summary>
+/// Responds to entity joined map event.
+/// </summary>
+public class EntityJoinedResponder : IGameResponder<EntityJoinedMapEvent>
+{
+ private readonly Bot _bot;
+ private readonly NostaleChatPacketApi _chatApi;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="EntityJoinedResponder"/> class.
+ /// </summary>
+ /// <param name="bot">The bot.</param>
+ /// <param name="chatApi">The chat packet api.</param>
+ public EntityJoinedResponder(Bot bot, NostaleChatPacketApi chatApi)
+ {
+ _bot = bot;
+ _chatApi = chatApi;
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> 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
A src/Samples/HighLevel/SimplePiiBot/SimplePiiBot.csproj => src/Samples/HighLevel/SimplePiiBot/SimplePiiBot.csproj +35 -0
@@ 0,0 1,35 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net7.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
+ <EnableDynamicLoading>true</EnableDynamicLoading>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
+ <PackageReference Include="NosSmooth.Core" Version="3.0.0" />
+ <PackageReference Include="NosSmooth.Data.NOSFiles" Version="2.0.2" />
+ <PackageReference Include="NosSmooth.Extensions.Combat" Version="0.0.1" />
+ <PackageReference Include="Remora.Commands" Version="10.0.3" />
+ <PackageReference Include="Remora.Results" Version="7.2.3" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection">
+ <Version>7.0.0</Version>
+ </PackageReference>
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions">
+ <Version>7.0.0</Version>
+ </PackageReference>
+ <PackageReference Include="Microsoft.Extensions.Logging.Console">
+ <Version>7.0.0</Version>
+ </PackageReference>
+ <PackageReference Include="System.Text.Encoding.CodePages" Version="7.0.0" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\Extensions\NosSmooth.ChatCommands\NosSmooth.ChatCommands.csproj" />
+ </ItemGroup>
+
+</Project>