From 17a4f82f243853c5a0767edc49f09149354481c2 Mon Sep 17 00:00:00 2001 From: k0t9i Date: Sun, 29 Oct 2023 20:55:36 +0400 Subject: [PATCH] feat: add pathfinding --- Client/App.xaml.cs | 24 ++- Client/Application/Components/Map.xaml | 35 +++++ .../ViewModels/CreatureListViewModel.cs | 10 +- .../ViewModels/CreatureMapViewModel.cs | 10 +- .../ViewModels/DropListViewModel.cs | 10 +- .../ViewModels/DropMapViewModel.cs | 10 +- .../Application/ViewModels/MainViewModel.cs | 14 +- Client/Application/ViewModels/MapViewModel.cs | 70 ++++++++- .../ViewModels/PathNodeViewModel.cs | 80 ++++++++++ Client/Domain/DTO/PathSegment.cs | 15 ++ .../Domain/Service/AsyncPathMoverInterface.cs | 18 +++ Client/Domain/Service/PathfinderInterface.cs | 15 ++ Client/Domain/Service/WorldHandler.cs | 20 +-- Client/Domain/ValueObjects/PathNode.cs | 18 +++ Client/Domain/ValueObjects/Vector3.cs | 31 +++- .../Infrastructure/Service/AsyncPathMover.cs | 140 ++++++++++++++++++ .../Service/L2jGeoDataPathfinder.cs | 95 ++++++++++++ Client/Properties/launchSettings.json | 8 + Client/config.json | Bin 144 -> 512 bytes L2BotCore/L2BotCore.vcxproj | 2 + 20 files changed, 577 insertions(+), 48 deletions(-) create mode 100644 Client/Application/ViewModels/PathNodeViewModel.cs create mode 100644 Client/Domain/DTO/PathSegment.cs create mode 100644 Client/Domain/Service/AsyncPathMoverInterface.cs create mode 100644 Client/Domain/Service/PathfinderInterface.cs create mode 100644 Client/Domain/ValueObjects/PathNode.cs create mode 100644 Client/Infrastructure/Service/AsyncPathMover.cs create mode 100644 Client/Infrastructure/Service/L2jGeoDataPathfinder.cs create mode 100644 Client/Properties/launchSettings.json diff --git a/Client/App.xaml.cs b/Client/App.xaml.cs index b366357..0dbfe06 100644 --- a/Client/App.xaml.cs +++ b/Client/App.xaml.cs @@ -19,6 +19,9 @@ using Client.Infrastructure.Helpers; using Client.Domain.Events; using Client.Infrastructure.Events; using System; +using Client.Infrastructure.Service; +using System.Collections.Generic; +using System.Linq; namespace Client { @@ -105,7 +108,26 @@ namespace Client .AddSingleton() .AddSingleton() .AddSingleton() - + + .AddSingleton( + typeof(PathfinderInterface), + x => new L2jGeoDataPathfinder( + config.GetValue("GeoDataDirectory") ?? "", + config.GetValue("MaxPassableHeight") + ) + ) + .AddSingleton( + typeof(AsyncPathMoverInterface), + x => new AsyncPathMover( + x.GetRequiredService(), + x.GetRequiredService(), + config.GetValue("PathNumberOfAttempts"), + config.GetValue("NodeWaitingTime"), + config.GetValue("NodeDistanceTolerance"), + config.GetValue("NextNodeDistanceTolerance") + ) + ) + .AddSingleton(); } } diff --git a/Client/Application/Components/Map.xaml b/Client/Application/Components/Map.xaml index fa114be..4c51570 100644 --- a/Client/Application/Components/Map.xaml +++ b/Client/Application/Components/Map.xaml @@ -220,6 +220,41 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/Client/Application/ViewModels/CreatureListViewModel.cs b/Client/Application/ViewModels/CreatureListViewModel.cs index 469ece6..6913672 100644 --- a/Client/Application/ViewModels/CreatureListViewModel.cs +++ b/Client/Application/ViewModels/CreatureListViewModel.cs @@ -39,12 +39,12 @@ namespace Client.Application.ViewModels worldHandler.RequestAttackOrFollow(Id); } - private void OnMouseRightClick(object? obj) + private async Task OnMouseRightClick(object? obj) { - worldHandler.RequestMoveToEntity(Id); + await pathMover.MoveUntilReachedAsync(creature.Transform.Position); } - public CreatureListViewModel(WorldHandler worldHandler, CreatureInterface creature, Hero hero) + public CreatureListViewModel(WorldHandler worldHandler, AsyncPathMoverInterface pathMover, CreatureInterface creature, Hero hero) { creature.PropertyChanged += Creature_PropertyChanged; creature.Transform.Position.PropertyChanged += Position_PropertyChanged; @@ -52,11 +52,12 @@ namespace Client.Application.ViewModels hero.PropertyChanged += Hero_PropertyChanged; MouseLeftClickCommand = new RelayCommand(OnMouseLeftClick); MouseLeftDoubleClickCommand = new RelayCommand(OnMouseLeftDoubleClick); - MouseRightClickCommand = new RelayCommand(OnMouseRightClick); + MouseRightClickCommand = new RelayCommand(async (o) => await OnMouseRightClick(o)); this.creature = creature; this.hero = hero; this.worldHandler = worldHandler; + this.pathMover = pathMover; } private void HeroPosition_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) @@ -94,5 +95,6 @@ namespace Client.Application.ViewModels private readonly CreatureInterface creature; private readonly Hero hero; private readonly WorldHandler worldHandler; + private readonly AsyncPathMoverInterface pathMover; } } diff --git a/Client/Application/ViewModels/CreatureMapViewModel.cs b/Client/Application/ViewModels/CreatureMapViewModel.cs index 2cb5fb2..c75dd87 100644 --- a/Client/Application/ViewModels/CreatureMapViewModel.cs +++ b/Client/Application/ViewModels/CreatureMapViewModel.cs @@ -77,16 +77,17 @@ namespace Client.Application.ViewModels worldHandler.RequestAttackOrFollow(Id); } - private void OnMouseRightClick(object? obj) + private async Task OnMouseRightClick(object? obj) { - worldHandler.RequestMoveToEntity(Id); + await pathMover.MoveUntilReachedAsync(creature.Transform.Position); } - public CreatureMapViewModel(WorldHandler worldHandler, CreatureInterface creature, Hero hero) + public CreatureMapViewModel(WorldHandler worldHandler, AsyncPathMoverInterface pathMover, CreatureInterface creature, Hero hero) { this.creature = creature; this.hero = hero; this.worldHandler = worldHandler; + this.pathMover = pathMover; creature.PropertyChanged += Creature_PropertyChanged; creature.Transform.PropertyChanged += Transform_PropertyChanged; creature.Transform.Position.PropertyChanged += Position_PropertyChanged; @@ -95,7 +96,7 @@ namespace Client.Application.ViewModels hero.PropertyChanged += Hero_PropertyChanged; MouseLeftClickCommand = new RelayCommand(OnMouseLeftClick); MouseLeftDoubleClickCommand = new RelayCommand(OnMouseLeftDoubleClick); - MouseRightClickCommand = new RelayCommand(OnMouseRightClick); + MouseRightClickCommand = new RelayCommand(async (o) => await OnMouseRightClick(o)); } private void VitalStats_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) @@ -149,6 +150,7 @@ namespace Client.Application.ViewModels private readonly CreatureInterface creature; private readonly Hero hero; private readonly WorldHandler worldHandler; + private readonly AsyncPathMoverInterface pathMover; private float scale = 1; private static readonly float MAX_RADIUS = 10; private static readonly float MIN_RADIUS = 2; diff --git a/Client/Application/ViewModels/DropListViewModel.cs b/Client/Application/ViewModels/DropListViewModel.cs index f934db5..a94183c 100644 --- a/Client/Application/ViewModels/DropListViewModel.cs +++ b/Client/Application/ViewModels/DropListViewModel.cs @@ -74,21 +74,22 @@ namespace Client.Application.ViewModels { worldHandler.RequestPickUp(Id); } - private void OnMouseRightClick(object? obj) + private async Task OnMouseRightClick(object? obj) { - worldHandler.RequestMoveToEntity(Id); + await pathMover.MoveUntilReachedAsync(drop.Transform.Position); } - public DropListViewModel(WorldHandler worldHandler, Drop drop, Hero hero) + public DropListViewModel(WorldHandler worldHandler, AsyncPathMoverInterface pathMover, Drop drop, Hero hero) { this.drop = drop; this.hero = hero; this.worldHandler = worldHandler; + this.pathMover = pathMover; drop.PropertyChanged += Drop_PropertyChanged; drop.Transform.Position.PropertyChanged += DropPosition_PropertyChanged; hero.Transform.Position.PropertyChanged += HeroPosition_PropertyChanged; MouseLeftClickCommand = new RelayCommand(OnMouseLeftClick); - MouseRightClickCommand = new RelayCommand(OnMouseRightClick); + MouseRightClickCommand = new RelayCommand(async (o) => await OnMouseRightClick(o)); } private void HeroPosition_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) @@ -126,5 +127,6 @@ namespace Client.Application.ViewModels private readonly Drop drop; private readonly Hero hero; private readonly WorldHandler worldHandler; + private readonly AsyncPathMoverInterface pathMover; } } diff --git a/Client/Application/ViewModels/DropMapViewModel.cs b/Client/Application/ViewModels/DropMapViewModel.cs index 58e4147..e3e5214 100644 --- a/Client/Application/ViewModels/DropMapViewModel.cs +++ b/Client/Application/ViewModels/DropMapViewModel.cs @@ -56,21 +56,22 @@ namespace Client.Application.ViewModels { worldHandler.RequestPickUp(Id); } - private void OnMouseRightClick(object? obj) + private async Task OnMouseRightClick(object? obj) { - worldHandler.RequestMoveToEntity(Id); + await pathMover.MoveUntilReachedAsync(drop.Transform.Position); } - public DropMapViewModel(WorldHandler worldHandler, Drop drop, Hero hero) + public DropMapViewModel(WorldHandler worldHandler, AsyncPathMoverInterface pathMover, Drop drop, Hero hero) { this.drop = drop; this.hero = hero; this.worldHandler = worldHandler; + this.pathMover = pathMover; drop.PropertyChanged += Creature_PropertyChanged; drop.Transform.Position.PropertyChanged += Position_PropertyChanged; hero.Transform.Position.PropertyChanged += HeroPosition_PropertyChanged; MouseLeftClickCommand = new RelayCommand(OnMouseLeftClick); - MouseRightClickCommand = new RelayCommand(OnMouseRightClick); + MouseRightClickCommand = new RelayCommand(async (o) => await OnMouseRightClick(o)); } private void HeroPosition_PropertyChanged(object? sender, PropertyChangedEventArgs e) @@ -94,6 +95,7 @@ namespace Client.Application.ViewModels private readonly Drop drop; private readonly Hero hero; private readonly WorldHandler worldHandler; + private readonly AsyncPathMoverInterface pathMover; private float scale = 1; private static readonly float MAX_RADIUS = 8; private static readonly float MIN_RADIUS = 2; diff --git a/Client/Application/ViewModels/MainViewModel.cs b/Client/Application/ViewModels/MainViewModel.cs index dcf42df..259c08f 100644 --- a/Client/Application/ViewModels/MainViewModel.cs +++ b/Client/Application/ViewModels/MainViewModel.cs @@ -59,7 +59,7 @@ namespace Client.Application.ViewModels { if (hero != null) { - Creatures.Add(new CreatureListViewModel(worldHandler, @event.Creature, hero)); + Creatures.Add(new CreatureListViewModel(worldHandler, pathMover, @event.Creature, hero)); AddCreature(@event.Creature); } } @@ -74,8 +74,8 @@ namespace Client.Application.ViewModels { if (hero != null) { - Drops.Add(new DropListViewModel(worldHandler, @event.Drop, hero)); - Map.Drops.Add(new DropMapViewModel(worldHandler, @event.Drop, hero)); + Drops.Add(new DropListViewModel(worldHandler, pathMover, @event.Drop, hero)); + Map.Drops.Add(new DropMapViewModel(worldHandler, pathMover, @event.Drop, hero)); } } @@ -136,7 +136,7 @@ namespace Client.Application.ViewModels { if (hero != null) { - Map.Creatures.Add(new CreatureMapViewModel(worldHandler, creature, hero)); + Map.Creatures.Add(new CreatureMapViewModel(worldHandler, pathMover, creature, hero)); } } @@ -145,10 +145,11 @@ namespace Client.Application.ViewModels Map.Creatures.RemoveAll(x => x.Id == id); } - public MainViewModel(WorldHandler worldHandler) + public MainViewModel(WorldHandler worldHandler, AsyncPathMoverInterface pathMover) { this.worldHandler = worldHandler; - Map = new MapViewModel(worldHandler); + this.pathMover = pathMover; + Map = new MapViewModel(pathMover); } public ObservableCollection ChatMessages { get; } = new ObservableCollection(); @@ -162,5 +163,6 @@ namespace Client.Application.ViewModels public MapViewModel Map { get; private set; } public Hero? hero; private readonly WorldHandler worldHandler; + private readonly AsyncPathMoverInterface pathMover; } } diff --git a/Client/Application/ViewModels/MapViewModel.cs b/Client/Application/ViewModels/MapViewModel.cs index fa73387..daae7ef 100644 --- a/Client/Application/ViewModels/MapViewModel.cs +++ b/Client/Application/ViewModels/MapViewModel.cs @@ -14,6 +14,10 @@ using System.Collections.Specialized; using Client.Application.Commands; using System.Reflection.Metadata; using System.Windows; +using Client.Infrastructure.Service; +using System.Windows.Data; +using Client.Domain.DTO; +using System.Windows.Documents; namespace Client.Application.ViewModels { @@ -138,11 +142,17 @@ namespace Client.Application.ViewModels drop.Scale = scale; drop.VieportSize = new Vector3((float)ViewportWidth, (float)ViewportHeight, 0); } + + foreach (var node in Path) + { + node.Scale = scale; + node.VieportSize = new Vector3((float)ViewportWidth, (float)ViewportHeight, 0); + } } } public ICommand MouseLeftClickCommand { get; } - private void OnLeftMouseClick(object? obj) + private async Task OnLeftMouseClick(object? obj) { if (obj == null) { @@ -159,7 +169,8 @@ namespace Client.Application.ViewModels (float)(mousePos.Y - ViewportHeight / 2) * scale + hero.Transform.Position.Y, hero.Transform.Position.Z ); - worldHandler.RequestMoveToLocation(location); + + await pathMover.MoveUntilReachedAsync(location); } public void OnMouseWheel(object sender, MouseWheelEventArgs e) @@ -189,13 +200,58 @@ namespace Client.Application.ViewModels mousePosition.Y = (float)(mousePos.Y - ViewportHeight / 2) * scale + hero.Transform.Position.Y; } - public MapViewModel(WorldHandler worldHandler) + public MapViewModel(AsyncPathMoverInterface pathMover) { Creatures.CollectionChanged += Creatures_CollectionChanged; Drops.CollectionChanged += Drops_CollectionChanged; - this.worldHandler = worldHandler; - MouseLeftClickCommand = new RelayCommand(OnLeftMouseClick); + Path.CollectionChanged += Path_CollectionChanged; + MouseLeftClickCommand = new RelayCommand(async (o) => await OnLeftMouseClick(o)); mousePosition.PropertyChanged += MousePosition_PropertyChanged; + BindingOperations.EnableCollectionSynchronization(Path, pathCollectionLock); + this.pathMover = pathMover; + this.pathMover.Path.CollectionChanged += PathMover_Path_CollectionChanged; + } + + private void PathMover_Path_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + lock(pathCollectionLock) + { + if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems != null) + { + if (hero != null) + { + foreach (var item in e.NewItems) + { + var node = (PathSegment)item; + Path.Add(new PathNodeViewModel(node.From, node.To, hero)); + } + } + } + else if (e.Action == NotifyCollectionChangedAction.Remove && e.OldItems != null) + { + foreach (var item in e.OldItems) + { + Path.RemoveAt(0); + } + } + else if (e.Action == NotifyCollectionChangedAction.Reset) + { + Path.Clear(); + } + } + } + + private void Path_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems != null) + { + foreach (var item in e.NewItems) + { + var node = (PathNodeViewModel)item; + node.Scale = scale; + node.VieportSize = new Vector3((float)ViewportWidth, (float)ViewportHeight, 0); + } + } } private void MousePosition_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) @@ -232,9 +288,11 @@ namespace Client.Application.ViewModels public ObservableCollection Blocks { get; } = new ObservableCollection(); public ObservableCollection Creatures { get; } = new ObservableCollection(); public ObservableCollection Drops { get; } = new ObservableCollection(); + public ObservableCollection Path { get; } = new ObservableCollection(); public readonly static float MIN_SCALE = 1; public readonly static float MAX_SCALE = 64; + private readonly AsyncPathMoverInterface pathMover; private MapImageSelector selector = new MapImageSelector(); private Dictionary blocks = new Dictionary(); private Hero? hero; @@ -242,6 +300,6 @@ namespace Client.Application.ViewModels private double viewportWidth = 0; private double viewportHeight = 0; private Vector3 mousePosition = new Vector3(0, 0, 0); - private readonly WorldHandler worldHandler; + private object pathCollectionLock = new object(); } } diff --git a/Client/Application/ViewModels/PathNodeViewModel.cs b/Client/Application/ViewModels/PathNodeViewModel.cs new file mode 100644 index 0000000..2dfa3d0 --- /dev/null +++ b/Client/Application/ViewModels/PathNodeViewModel.cs @@ -0,0 +1,80 @@ +using Client.Application.Commands; +using Client.Domain.Common; +using Client.Domain.Entities; +using Client.Domain.Service; +using Client.Domain.ValueObjects; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Client.Application.ViewModels +{ + public class PathNodeViewModel : ObservableObject + { + public Vector3 From => new Vector3( + (from.X - hero.Transform.Position.X) / scale + (VieportSize.X / 2), + (from.Y - hero.Transform.Position.Y) / scale + (VieportSize.Y / 2), + 0 + ); + + public Vector3 To => new Vector3( + (to.X - hero.Transform.Position.X) / scale + (VieportSize.X / 2), + (to.Y - hero.Transform.Position.Y) / scale + (VieportSize.Y / 2), + 0 + ); + + public float Radius => MAX_RADIUS - (1 / MapViewModel.MIN_SCALE - 1 / scale) / (1 / MapViewModel.MIN_SCALE - 1 / MapViewModel.MAX_SCALE) * (MAX_RADIUS - MIN_RADIUS); + + public float Scale + { + get => scale; + set + { + if (scale != value) + { + scale = value; + OnPropertyChanged("From"); + OnPropertyChanged("To"); + OnPropertyChanged("Radius"); + } + } + } + public Vector3 VieportSize + { + get => vieportSize; + set + { + if (vieportSize != value) + { + vieportSize = value; + OnPropertyChanged("From"); + OnPropertyChanged("To"); + } + } + } + + public PathNodeViewModel(Vector3 from, Vector3 to, Hero hero) + { + this.from = from; + this.to = to; + this.hero = hero; + hero.Transform.Position.PropertyChanged += HeroPosition_PropertyChanged; + } + + private void HeroPosition_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + { + OnPropertyChanged("From"); + OnPropertyChanged("To"); + } + + private readonly Vector3 from; + private readonly Vector3 to; + private readonly Hero hero; + private float scale = 1; + private static readonly float MAX_RADIUS = 10; + private static readonly float MIN_RADIUS = 2; + private Vector3 vieportSize = new Vector3(0, 0, 0); + } +} diff --git a/Client/Domain/DTO/PathSegment.cs b/Client/Domain/DTO/PathSegment.cs new file mode 100644 index 0000000..d7e6fb3 --- /dev/null +++ b/Client/Domain/DTO/PathSegment.cs @@ -0,0 +1,15 @@ +using Client.Domain.ValueObjects; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Client.Domain.DTO +{ + public class PathSegment + { + public Vector3 From = new Vector3(0, 0, 0); + public Vector3 To = new Vector3(0, 0, 0); + } +} diff --git a/Client/Domain/Service/AsyncPathMoverInterface.cs b/Client/Domain/Service/AsyncPathMoverInterface.cs new file mode 100644 index 0000000..ec04107 --- /dev/null +++ b/Client/Domain/Service/AsyncPathMoverInterface.cs @@ -0,0 +1,18 @@ +using Client.Domain.DTO; +using Client.Domain.ValueObjects; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Client.Domain.Service +{ + public interface AsyncPathMoverInterface + { + public ObservableCollection Path { get; } + public Task MoveAsync(Vector3 location); + public Task MoveUntilReachedAsync(Vector3 location); + } +} diff --git a/Client/Domain/Service/PathfinderInterface.cs b/Client/Domain/Service/PathfinderInterface.cs new file mode 100644 index 0000000..841e8ca --- /dev/null +++ b/Client/Domain/Service/PathfinderInterface.cs @@ -0,0 +1,15 @@ +using Client.Domain.DTO; +using Client.Domain.ValueObjects; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Client.Domain.Service +{ + public interface PathfinderInterface + { + public List FindPath(Vector3 start, Vector3 end); + } +} diff --git a/Client/Domain/Service/WorldHandler.cs b/Client/Domain/Service/WorldHandler.cs index 4355839..78caa90 100644 --- a/Client/Domain/Service/WorldHandler.cs +++ b/Client/Domain/Service/WorldHandler.cs @@ -27,6 +27,8 @@ namespace Client.Domain.Service EventHandlerInterface, EventHandlerInterface { + public Hero? Hero => hero; + public void RequestMoveToLocation(Vector3 location) { if (hero == null) @@ -37,24 +39,6 @@ namespace Client.Domain.Service SendMessage(OutgoingMessageTypeEnum.Move, location); } - public void RequestMoveToEntity(uint id) - { - if (hero == null) - { - return; - } - - if (!creatures.ContainsKey(id) && !drops.ContainsKey(id)) - { - Debug.WriteLine("RequestMoveToEntity: entity " + id + " not found"); - return; - } - - var position = creatures.ContainsKey(id) ? creatures[id].Transform.Position : drops[id].Transform.Position; - - RequestMoveToLocation(position); - } - public void RequestAcquireTarget(uint id) { if (hero == null) diff --git a/Client/Domain/ValueObjects/PathNode.cs b/Client/Domain/ValueObjects/PathNode.cs new file mode 100644 index 0000000..4cf6c89 --- /dev/null +++ b/Client/Domain/ValueObjects/PathNode.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Client.Domain.ValueObjects +{ + public struct PathNode + { + public readonly uint minX; + public readonly uint minY; + public readonly uint maxX; + public readonly uint maxY; + public readonly short height; + } +} diff --git a/Client/Domain/ValueObjects/Vector3.cs b/Client/Domain/ValueObjects/Vector3.cs index 700649e..ee4427b 100644 --- a/Client/Domain/ValueObjects/Vector3.cs +++ b/Client/Domain/ValueObjects/Vector3.cs @@ -11,7 +11,7 @@ namespace Client.Domain.ValueObjects public float X { get => x; set { if (value != x) { x = value; OnPropertyChanged("X"); } } } public float Y { get => y; set { if (value != y) { y = value; OnPropertyChanged("Y"); } } } - public float Z { get => z; set { if (value != z) { z = value; OnPropertyChanged("X"); } } } + public float Z { get => z; set { if (value != z) { z = value; OnPropertyChanged("Z"); } } } public Vector3(float x, float y, float z) { @@ -29,5 +29,34 @@ namespace Client.Domain.ValueObjects { return MathF.Sqrt(HorizontalSqrDistance(other)); } + + public override bool Equals(object? other) + { + if (!(other is Vector3)) + { + return false; + } + + var obj = (Vector3)other; + return MathF.Abs(x - obj.x) < float.Epsilon && MathF.Abs(y - obj.y) < float.Epsilon && MathF.Abs(z - obj.z) < float.Epsilon; + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + + public bool ApproximatelyEquals(Vector3 other, float epsilon, bool withZ = false) + { + var equals = MathF.Abs(x - other.x) < epsilon && MathF.Abs(y - other.y) < epsilon; + if (withZ) + { + equals = equals && MathF.Abs(z - other.z) < epsilon; + } + + return equals; + } + + public static readonly Vector3 Zero = new Vector3(0, 0, 0); } } diff --git a/Client/Infrastructure/Service/AsyncPathMover.cs b/Client/Infrastructure/Service/AsyncPathMover.cs new file mode 100644 index 0000000..c5b34f9 --- /dev/null +++ b/Client/Infrastructure/Service/AsyncPathMover.cs @@ -0,0 +1,140 @@ +using Client.Application.ViewModels; +using Client.Domain.DTO; +using Client.Domain.Entities; +using Client.Domain.Enums; +using Client.Domain.Service; +using Client.Domain.ValueObjects; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace Client.Infrastructure.Service +{ + public class AsyncPathMover : AsyncPathMoverInterface + { + private readonly WorldHandler worldHandler; + private readonly PathfinderInterface pathfinder; + private readonly int pathNumberOfAttempts; + private readonly double nodeWaitingTime; + private readonly int nodeDistanceTolerance; + private readonly int nextNodeDistanceTolerance; + private CancellationTokenSource? cancellationTokenSource; + + public ObservableCollection Path { get; private set; } = new ObservableCollection(); + + public async Task MoveUntilReachedAsync(Vector3 location) + { + var remainingAttempts = pathNumberOfAttempts; + while (!await MoveAsync(location) && remainingAttempts > 0) + { + remainingAttempts--; + } + } + + public async Task MoveAsync(Vector3 location) + { + if (cancellationTokenSource != null) + { + cancellationTokenSource.Cancel(); + cancellationTokenSource.Dispose(); + } + cancellationTokenSource = new CancellationTokenSource(); + var cancellationToken = cancellationTokenSource.Token; + + try + { + return await Task.Run(() => + { + cancellationToken.ThrowIfCancellationRequested(); + + Debug.WriteLine("Find path start"); + FindPath(location); + Debug.WriteLine("Find path finish"); + + + foreach (var node in Path.ToList()) + { + worldHandler.RequestMoveToLocation(node.To); + + if (!WaitForNodeReaching(cancellationToken, node)) + { + return false; + } + Path.Remove(node); + } + + return true; + }, cancellationToken); + } + catch (OperationCanceledException) + { + return true; + } + } + + public AsyncPathMover(WorldHandler worldHandler, PathfinderInterface pathfinder, int pathNumberOfAttempts, double nodeWaitingTime, int nodeDistanceTolerance, int nextNodeDistanceTolerance) + { + this.worldHandler = worldHandler; + this.pathfinder = pathfinder; + this.pathNumberOfAttempts = pathNumberOfAttempts; + this.nodeWaitingTime = nodeWaitingTime; + this.nodeDistanceTolerance = nodeDistanceTolerance; + this.nextNodeDistanceTolerance = nextNodeDistanceTolerance; + } + + private void FindPath(Vector3 location) + { + var hero = worldHandler.Hero; + + Path.Clear(); + if (hero == null) + { + return; + } + + var path = pathfinder.FindPath(hero.Transform.Position, location); + foreach (var segment in path) + { + Path.Add(segment); + } + } + + private bool WaitForNodeReaching(CancellationToken token, PathSegment node) + { + var hero = worldHandler.Hero; + + var start = DateTime.Now; + while (hero != null && !hero.Transform.Position.ApproximatelyEquals(node.To, nodeDistanceTolerance)) + { + if (token.IsCancellationRequested) + { + token.ThrowIfCancellationRequested(); + } + + if (hero.Transform.Velocity.Equals(Vector3.Zero)) + { + var elapsedSeconds = (DateTime.Now - start).TotalSeconds; + if (hero.Transform.Position.ApproximatelyEquals(node.To, nextNodeDistanceTolerance)) + { + break; + } + else if (elapsedSeconds >= nodeWaitingTime) + { + Path.Clear(); + return false; + } + } + Task.Delay(25); + } + + return true; + } + } +} diff --git a/Client/Infrastructure/Service/L2jGeoDataPathfinder.cs b/Client/Infrastructure/Service/L2jGeoDataPathfinder.cs new file mode 100644 index 0000000..2c007e3 --- /dev/null +++ b/Client/Infrastructure/Service/L2jGeoDataPathfinder.cs @@ -0,0 +1,95 @@ +using Client.Domain.DTO; +using Client.Domain.Service; +using Client.Domain.ValueObjects; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Client.Infrastructure.Service +{ + public class L2jGeoDataPathfinder : PathfinderInterface + { + [DllImport("L2JGeoDataPathFinder.dll", CallingConvention = CallingConvention.Cdecl)] + private static extern uint FindPath(out IntPtr arrayPtr, string geoDataDirectory, float startX, float startY, float startZ, float endX, float endY, ushort maxPassableHeight); + [DllImport("L2JGeoDataPathFinder.dll", CallingConvention = CallingConvention.Cdecl)] + private static extern uint ReleasePath(IntPtr arrayPtr); + + public L2jGeoDataPathfinder(string geodataDirectory, ushort maxPassableHeight) + { + this.geodataDirectory = geodataDirectory; + this.maxPassableHeight = maxPassableHeight; + } + + public List FindPath(Vector3 start, Vector3 end) + { + var arrayPtr = IntPtr.Zero; + var size = FindPath(out arrayPtr, GetGeodataFullpath(), start.X, start.Y, start.Z, end.X, end.Y, maxPassableHeight); + var originalArrayPtr = arrayPtr; + + var nodes = new List(); + if (size > 0) + { + var entrySize = Marshal.SizeOf(typeof(PathNode)); + for (var i = 0; i < size; i++) + { + var node = Marshal.PtrToStructure(arrayPtr, typeof(PathNode)); + if (node != null) + { + nodes.Add((PathNode)node); + } + arrayPtr = new IntPtr(arrayPtr.ToInt32() + entrySize); + } + ReleasePath(originalArrayPtr); + } + + return BuildPath(nodes); + } + + private List BuildPath(List nodes) + { + var result = new List(); + + var points = new List(); + foreach (var node in nodes) + { + points.Add(NodeToVector(node)); + } + + for (var i = 0; i < points.Count - 1; i++) + { + var point = points[i]; + var nextPoint = points[i + 1]; + + result.Add(new PathSegment + { + From = point, + To = nextPoint + }); + } + + return result; + } + + private Vector3 NodeToVector(PathNode node) + { + var rnd = new Random(); + + return new Vector3( + rnd.Next((int)node.minX, (int)node.maxX + 1), + rnd.Next((int)node.minY, (int)node.maxY + 1), + node.height + ); + } + + private string GetGeodataFullpath() + { + return System.IO.Directory.GetCurrentDirectory() + "/Assets/" + geodataDirectory + "/"; + } + + private readonly string geodataDirectory; + private readonly ushort maxPassableHeight; + } +} diff --git a/Client/Properties/launchSettings.json b/Client/Properties/launchSettings.json new file mode 100644 index 0000000..8871e5a --- /dev/null +++ b/Client/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Client": { + "commandName": "Project", + "nativeDebugging": true + } + } +} \ No newline at end of file diff --git a/Client/config.json b/Client/config.json index b881d7632261cf2605cefd5cb202fea6a8632633..994f8c92d23efd614ca128ffb15561e1111cc6a5 100644 GIT binary patch literal 512 zcma)(L2JT55QX1a@IQ!Wk*X-gTUF45Mh}AL)x>l`jl>lc>0fXAWn4G}Q4o_oZJ$Wlr7z delta 13 UcmZo*nZP)qi;b6oi=mbQ02lEB$^ZZW diff --git a/L2BotCore/L2BotCore.vcxproj b/L2BotCore/L2BotCore.vcxproj index c22d1eb..ed0e6f0 100644 --- a/L2BotCore/L2BotCore.vcxproj +++ b/L2BotCore/L2BotCore.vcxproj @@ -237,9 +237,11 @@ ..\..\pch.h + ..\..\pch.h ..\..\pch.h + ..\..\pch.h Create