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