From 47e02cb47fbff169a78c55b0f0ec1a017e8eb8b6 Mon Sep 17 00:00:00 2001 From: Xian55 <367101+Xian55@users.noreply.github.com> Date: Sat, 11 May 2024 14:14:03 +0200 Subject: [PATCH] Core: RemoteV3 new branch of AmeisenNavigation Core: Added PathVisualizer. Currently PPather only works with Wotlk maps for viz. Not suitable for Cata maps. While using RemoteV3 and RemoteV1 is available as well, the generated paths and player positions can be visualized. Core: RemoteV3 uses new build of AmeisenNavigation which is more performant during startup and supports multiple mmaps versions. --- Core/BotController.cs | 50 +++++++++++----- Core/DependencyInjection.cs | 28 +++++++++ Core/PPather/IPathVizualizer.cs | 18 ++++++ Core/PPather/NoPathVisualizer.cs | 21 +++++++ Core/PPather/RemotePathingAPI.cs | 7 ++- Core/PPather/RemotePathingAPIV3.cs | 93 ++++++++++++++++++++---------- Core/Path/RouteInfo.cs | 10 ++-- README.md | 18 ++++-- 8 files changed, 185 insertions(+), 60 deletions(-) create mode 100644 Core/PPather/IPathVizualizer.cs create mode 100644 Core/PPather/NoPathVisualizer.cs diff --git a/Core/BotController.cs b/Core/BotController.cs index 40c09518..b0fbf4df 100644 --- a/Core/BotController.cs +++ b/Core/BotController.cs @@ -25,6 +25,7 @@ public sealed partial class BotController : IBotController, IDisposable private readonly IServiceProvider serviceProvider; private readonly ILogger logger; private readonly IPPather pather; + private readonly IPathVizualizer pathViz; private readonly MinimapNodeFinder minimapNodeFinder; private readonly DataConfig dataConfig; private readonly CancellationTokenSource cts; @@ -64,7 +65,9 @@ public sealed partial class BotController : IBotController, IDisposable public BotController( ILogger logger, CancellationTokenSource cts, - IPPather pather, DataConfig dataConfig, + IPPather pather, + IPathVizualizer pathViz, + DataConfig dataConfig, WowProcess process, IWowScreen screen, NpcNameFinder npcNameFinder, @@ -81,6 +84,7 @@ public BotController( this.logger = logger; this.pather = pather; + this.pathViz = pathViz; this.dataConfig = dataConfig; this.screen = screen; @@ -121,7 +125,7 @@ public BotController( screenshotThread = new(ScreenshotThread); screenshotThread.Start(); - if (pather is RemotePathingAPI) + if (pathViz is not NoPathVisualizer) { remotePathing = new(RemotePathingThread); remotePathing.Start(); @@ -239,35 +243,44 @@ static double Average(ReadOnlySpan span) private void RemotePathingThread() { - bool newLoaded = false; + bool routeChanged = false; + RouteInfo? routeInfo = null; + ProfileLoaded += OnProfileLoaded; - void OnProfileLoaded() => newLoaded = true; + void OnProfileLoaded() + { + routeChanged = true; + routeInfo = sessionScope!.ServiceProvider.GetRequiredService(); + } Vector3 oldPos = Vector3.Zero; + Vector3[] mapRoute = Array.Empty(); while (!cts.IsCancellationRequested) { cts.Token.WaitHandle.WaitOne(remotePathingTickMs); - if (sessionScope == null) + if (sessionScope == null || routeInfo == null) continue; - if (newLoaded) + if (routeChanged) { - Vector3[] mapRoute = sessionScope - .ServiceProvider.GetRequiredService(); - - pather.DrawLines(new() + mapRoute = routeInfo.Route; + if (mapRoute.Length == 0) { - new LineArgs("grindpath", - mapRoute, 2, playerReader.UIMapId.Value) - }).AsTask().Wait(cts.Token); + continue; + } + + pather.DrawLines( + [ + new LineArgs("grindpath", mapRoute, 2, playerReader.UIMapId.Value), + ]).AsTask().Wait(cts.Token); oldPos = Vector3.Zero; - newLoaded = false; + routeChanged = false; } - if (playerReader.MapPos != oldPos) + if (!routeChanged && playerReader.MapPos != oldPos) { oldPos = playerReader.MapPos; @@ -276,6 +289,13 @@ private void RemotePathingThread() bits.Combat() ? 1 : bits.Target() ? 6 : 2, playerReader.UIMapId.Value)) .AsTask().Wait(cts.Token); + + _ = routeInfo.NextPoint(); + + if (!routeInfo.Route.SequenceEqual(mapRoute)) + { + routeChanged = true; + } } } diff --git a/Core/DependencyInjection.cs b/Core/DependencyInjection.cs index 36fa7354..071e618a 100644 --- a/Core/DependencyInjection.cs +++ b/Core/DependencyInjection.cs @@ -166,6 +166,9 @@ public static IServiceCollection AddCoreNormal( s.AddSingleton(x => GetScreenCapture(x.GetRequiredService(), log)); + s.AddSingleton(x => + GetPathVizualizer(x.GetRequiredService(), log)); + s.AddSingleton(x => GetPather(x.GetRequiredService(), log)); @@ -306,12 +309,14 @@ private static IPPather GetPather(IServiceProvider sp, ILogger logger) var scp = sp.GetRequiredService>().Value; var dataConfig = sp.GetRequiredService(); var worldMapAreaDB = sp.GetRequiredService(); + var pathViz = sp.GetRequiredService(); bool failed = false; if (scp.Type == StartupConfigPathing.Types.RemoteV3) { var remoteLogger = loggerFactory.CreateLogger(); RemotePathingAPIV3 api = new( + pathViz, remoteLogger, scp.hostv3, scp.portv3, worldMapAreaDB); if (api.PingServer()) @@ -360,4 +365,27 @@ private static IPPather GetPather(IServiceProvider sp, ILogger logger) return localApi; } + + private static IPathVizualizer GetPathVizualizer(IServiceProvider sp, ILogger logger) + { + var loggerFactory = sp.GetRequiredService(); + var remoteLogger = loggerFactory.CreateLogger(); + + var scp = sp.GetRequiredService>().Value; + RemotePathingAPI? api = new(remoteLogger, scp.hostv1, scp.portv1); + + if (!api.PingServer()) + { + api.Dispose(); + api = null; + } + else + { + logger.LogInformation( + $"Found PathViz {StartupConfigPathing.Types.RemoteV1}({api.GetType().Name}) " + + $"{scp.hostv1}:{scp.portv1}"); + } + + return api ?? (IPathVizualizer)new NoPathVisualizer(); + } } diff --git a/Core/PPather/IPathVizualizer.cs b/Core/PPather/IPathVizualizer.cs new file mode 100644 index 00000000..0dea6ecc --- /dev/null +++ b/Core/PPather/IPathVizualizer.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; + +using PPather.Data; + +namespace Core; + +public interface IPathVizualizer : IDisposable +{ + HttpClient Client { get; } + JsonSerializerOptions Options { get; } + + ValueTask DrawLines(List lineArgs); + ValueTask DrawSphere(SphereArgs args); +} \ No newline at end of file diff --git a/Core/PPather/NoPathVisualizer.cs b/Core/PPather/NoPathVisualizer.cs new file mode 100644 index 00000000..e335d923 --- /dev/null +++ b/Core/PPather/NoPathVisualizer.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; + +using PPather.Data; + +namespace Core; + +internal sealed class NoPathVisualizer : IPathVizualizer +{ + public HttpClient Client => throw new System.NotImplementedException(); + + public JsonSerializerOptions Options => throw new System.NotImplementedException(); + + public void Dispose() { } + + public ValueTask DrawLines(List lineArgs) => ValueTask.CompletedTask; + + public ValueTask DrawSphere(SphereArgs args) => ValueTask.CompletedTask; +} diff --git a/Core/PPather/RemotePathingAPI.cs b/Core/PPather/RemotePathingAPI.cs index e9c31862..139f41e2 100644 --- a/Core/PPather/RemotePathingAPI.cs +++ b/Core/PPather/RemotePathingAPI.cs @@ -10,11 +10,10 @@ using System.Numerics; using SharedLib.Converters; using System.Net.Sockets; -using System.Diagnostics; namespace Core; -public sealed class RemotePathingAPI : IPPather, IDisposable +public sealed class RemotePathingAPI : IPPather, IPathVizualizer, IDisposable { private readonly ILogger logger; @@ -22,9 +21,11 @@ public sealed class RemotePathingAPI : IPPather, IDisposable private readonly int port = 5001; private readonly JsonSerializerOptions options; - private readonly HttpClient client; + public HttpClient Client => client; + public JsonSerializerOptions Options => options; + public RemotePathingAPI(ILogger logger, string host, int port) { diff --git a/Core/PPather/RemotePathingAPIV3.cs b/Core/PPather/RemotePathingAPIV3.cs index 10178325..960c93fa 100644 --- a/Core/PPather/RemotePathingAPIV3.cs +++ b/Core/PPather/RemotePathingAPIV3.cs @@ -7,6 +7,10 @@ using AnTCP.Client; using SharedLib; using System.Numerics; +using System.Net.Http; +using System.Text.Json; +using System.Text; +using PPather; #pragma warning disable 162 @@ -17,22 +21,29 @@ public sealed class RemotePathingAPIV3 : IPPather, IDisposable private const bool debug = false; private const int watchdogPollMs = 500; - public enum EMessageType + private const EMessageType TYPE = EMessageType.PATH; + private const PathRequestFlags FLAGS = PathRequestFlags.SMOOTH_CATMULLROM | PathRequestFlags.VALIDATE_CPOP; + + private enum EMessageType { - PATH, - MOVE_ALONG_SURFACE, - RANDOM_POINT, - RANDOM_POINT_AROUND, - CAST_RAY, - RANDOM_PATH + PATH, // Generate a simple straight path + MOVE_ALONG_SURFACE, // Move an entity by small deltas using pathfinding (usefull to prevent falling off edges...) + RANDOM_POINT, // Get a random point on the mesh + RANDOM_POINT_AROUND, // Get a random point on the mesh in a circle + CAST_RAY, // Cast a movement ray to test for obstacles + RANDOM_PATH, // Generate a straight path where the nodes get offsetted by a random value + EXPLORE_POLY, // Generate a route to explore the polygon (W.I.P) + CONFIGURE_FILTER, // Cpnfigure the clients dtQueryFilter area costs } - public enum PathRequestFlags + private enum PathRequestFlags { NONE = 0, - CHAIKIN = 1, - CATMULLROM = 2, - FIND_LOCATION = 4 + SMOOTH_CHAIKIN = 1 << 0, // Smooth path using Chaikin Curve + SMOOTH_CATMULLROM = 1 << 1, // Smooth path using Catmull-Rom Spline + SMOOTH_BEZIERCURVE = 1 << 2, // Smooth path using Bezier Curve + VALIDATE_CPOP = 1 << 3, // Validate smoothed path using closestPointOnPoly + VALIDATE_MAS = 1 << 4, // Validate smoothed path using moveAlongSurface }; private readonly ILogger logger; @@ -42,11 +53,19 @@ public enum PathRequestFlags private readonly Thread connectionWatchdog; private readonly CancellationTokenSource cts; - public RemotePathingAPIV3(ILogger logger, + private readonly IPathVizualizer pathViz; + + private int uiMap; + private Vector3[] result = Array.Empty(); + + public RemotePathingAPIV3( + IPathVizualizer pathViz, + ILogger logger, string ip, int port, WorldMapAreaDB areaDB) { this.logger = logger; this.areaDB = areaDB; + this.pathViz = pathViz; cts = new(); @@ -60,23 +79,33 @@ public void Dispose() RequestDisconnect(); } - #region old - public ValueTask DrawLines(List lineArgs) { - return ValueTask.CompletedTask; + if (pathViz is NoPathVisualizer || result == Array.Empty()) + return ValueTask.CompletedTask; + + StringContent content = + new(JsonSerializer.Serialize(new DrawMapPathRequest(uiMap, result), pathViz.Options), + Encoding.UTF8, "application/json"); + + pathViz.DrawLines(lineArgs).AsTask().Wait(); + + return new(pathViz.Client.PostAsync("DrawMapPath", content)); } public ValueTask DrawSphere(SphereArgs args) { - return ValueTask.CompletedTask; + if (pathViz is NoPathVisualizer) + return ValueTask.CompletedTask; + + return pathViz.DrawSphere(args); } public Vector3[] FindMapRoute(int uiMap, Vector3 mapFrom, Vector3 mapTo) { if (!client.IsConnected || !areaDB.TryGet(uiMap, out WorldMapArea area)) - return Array.Empty(); + return result = Array.Empty(); try { @@ -94,12 +123,13 @@ public Vector3[] FindMapRoute(int uiMap, Vector3 mapFrom, Vector3 mapTo) if (debug) logger.LogDebug($"Finding map route from {mapFrom}({worldFrom}) map {uiMap} to {mapTo}({worldTo}) map {uiMap}..."); - Vector3[] path = client.Send((byte)EMessageType.PATH, - (area.MapID, PathRequestFlags.FIND_LOCATION | PathRequestFlags.CATMULLROM, + Vector3[] path = client.Send( + (byte)TYPE, + (area.MapID, FLAGS, worldFrom.X, worldFrom.Y, worldFrom.Z, worldTo.X, worldTo.Y, worldTo.Z)).AsArray(); if (path.Length == 1 && path[0] == Vector3.Zero) - return Array.Empty(); + return result = Array.Empty(); for (int i = 0; i < path.Length; i++) { @@ -109,22 +139,24 @@ public Vector3[] FindMapRoute(int uiMap, Vector3 mapFrom, Vector3 mapTo) path[i] = areaDB.ToMap_FlipXY(path[i], area.MapID, uiMap); } - return path; + return result = path; } catch (Exception ex) { logger.LogError(ex, $"Finding map route from {mapFrom} to {mapTo}"); - return Array.Empty(); + return result = Array.Empty(); } } public Vector3[] FindWorldRoute(int uiMap, Vector3 worldFrom, Vector3 worldTo) { if (!client.IsConnected) - return Array.Empty(); + return result = Array.Empty(); if (!areaDB.TryGet(uiMap, out WorldMapArea area)) - return Array.Empty(); + return result = Array.Empty(); + + this.uiMap = uiMap; try { @@ -139,19 +171,20 @@ public Vector3[] FindWorldRoute(int uiMap, Vector3 worldFrom, Vector3 worldTo) if (debug) logger.LogDebug($"Finding world route from {worldFrom}({worldFrom}) map {uiMap} to {worldTo}({worldTo}) map {uiMap}..."); - Vector3[] path = client.Send((byte)EMessageType.PATH, - (area.MapID, PathRequestFlags.FIND_LOCATION | PathRequestFlags.CATMULLROM, + Vector3[] path = client.Send( + (byte)TYPE, + (area.MapID, FLAGS, worldFrom.X, worldFrom.Y, worldFrom.Z, worldTo.X, worldTo.Y, worldTo.Z)).AsArray(); if (path.Length == 1 && path[0] == Vector3.Zero) - return Array.Empty(); + return result = Array.Empty(); - return path; + return result = path; } catch (Exception ex) { logger.LogError(ex, $"Finding world route from {worldFrom} to {worldTo}"); - return Array.Empty(); + return result = Array.Empty(); } } @@ -182,8 +215,6 @@ private void RequestDisconnect() } } - #endregion old - private void ObserveConnection() { while (!cts.IsCancellationRequested) diff --git a/Core/Path/RouteInfo.cs b/Core/Path/RouteInfo.cs index f999ca89..0ef4f97a 100644 --- a/Core/Path/RouteInfo.cs +++ b/Core/Path/RouteInfo.cs @@ -308,20 +308,20 @@ public string RenderPathPoints(ReadOnlySpan path) public Vector3 NextPoint() { - var route = pathedRoutes + IRouteProvider? mostRecent = pathedRoutes .OrderByDescending(MostRecent) .FirstOrDefault(); - if (route == null || !route.HasNext()) + if (mostRecent == null || !mostRecent.HasNext()) return Vector3.Zero; // dynamically update the path based on source - if (route.MapRoute() != Array.Empty()) + if (mostRecent.MapRoute() != Array.Empty()) { - RouteSrc = Route = route.MapRoute(); + RouteSrc = Route = mostRecent.MapRoute(); } - return route.NextMapPoint(); + return mostRecent.NextMapPoint(); } public string RenderNextPoint() diff --git a/README.md b/README.md index 3cc9a0ec..ab54b0e1 100644 --- a/README.md +++ b/README.md @@ -21,12 +21,18 @@ Further detail about the architecture can be found in [Blog post](http://www.cod # Pathfinders * World map - Outdoor there are multiple solutions - *by default the app attempts to discover the available services in the following order*: - * **V3 Remote**: Out of process [AmeisenNavigation](https://github.com/Xian55/AmeisenNavigation/tree/feature/guess-z-coord-after-rewrite) + * **V3 Remote**: Out of process [AmeisenNavigation](https://github.com/Xian55/AmeisenNavigation/tree/feature/multi-version-guess-z-coord) * **V1 Remote**: Out of process [PathingAPI](https://github.com/Xian55/WowClassicGrindBot/tree/dev/PathingAPI) more info [here](#v1-remote-pathing---pathingapi) * **V1 Local**: In process [PPather](https://github.com/Xian55/WowClassicGrindBot/tree/dev/PPather) * World map - Indoors pathfinder only works properly if `PathFilename` is exists. * Dungeons / instances **not** supported! +# Supporting Cataclysm Classic limitations + +With Cataclysm, the navigation will be limited. Only V3 Remote will be support for now. + +V1 Local and V1 Remote does not have the capability as of this moment to read the CASC files only works with MPQs. + # Features ## General Features @@ -134,19 +140,19 @@ Technical details about **V1:** - Download the navmesh files. -**Vanilla + TBC:** [**Vanilla + TBC**](https://mega.nz/file/7HgkHIyA#c_gzUeTadecWY0JDY3KT39ktfPGLs2vzt_90bMvhszk) -**Vanilla + TBC + Wrath:** [**Vanilla + TBC + Wrath**](https://mega.nz/file/zWQ2XIKI#9EKWOPyyTMfY1LACkcP_wioZ0poVIuaGh2xcRh4V9dw) +[**Vanilla + TBC + Wrath + Cataclysm** - work in progress](https://mega.nz/file/7Og32TDA#5HpxZ8Sh1XvDNCmWbI8H-cOFEJzDmh97Z6FGrO2p3X4) + 1. Extract and copy anywhere you want, like `C:\mmaps` -2. Create a [build](https://github.com/Xian55/WowClassicGrindBot/issues/449) of [AmeisenNavigation](https://github.com/Xian55/AmeisenNavigation/tree/feature/guess-z-coord-after-rewrite) -3. Navigate to the build location and find `config.cfg` +2. Create a [build](https://github.com/Xian55/WowClassicGrindBot/issues/449) of [AmeisenNavigation](https://github.com/Xian55/AmeisenNavigation/tree/feature/multi-version-guess-z-coord) +3. Navigate to the build location of `AmeisenNavigation.Server` and find `config.cfg` 4. Edit the last line of the file to look like `sMmapsPath=C:\mmaps` Technical details about **V3:** -- Uses another project called [AmeisenNavigation](https://github.com/Xian55/AmeisenNavigation/tree/feature/guess-z-coord-after-rewrite) +- Uses another project called [AmeisenNavigation](https://github.com/Xian55/AmeisenNavigation/tree/feature/multi-version-guess-z-coord) - Under the hood uses [Recast and Detour](https://github.com/recastnavigation/recastnavigation) - Source code is written in **C++** - Uses `*.mmap` files as source