~ruther/NosSmooth.Local

c4fcedefb7c78eaa71a09806509f9c3bca6f57d4 — Rutherther 2 years ago 303fcf2
feat(samples): add simple pii bot sample
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>

Do not follow this link