From 9361b0b4690c72ec9f68c1b4d7931a1a823ec9b2 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Sat, 22 Apr 2023 18:57:19 +1000 Subject: [PATCH] NPC obstacle fixes (#15645) --- .../NPC/Systems/NPCSteeringSystem.Context.cs | 8 +- .../NPC/Systems/NPCSteeringSystem.cs | 713 +++++++++--------- 2 files changed, 363 insertions(+), 358 deletions(-) diff --git a/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs b/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs index e8ef3c0cb3..0897d8ff1e 100644 --- a/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs +++ b/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs @@ -98,7 +98,7 @@ public sealed partial class NPCSteeringSystem // TODO: Consider melee range or the likes. else { - arrivalDistance = SharedInteractionSystem.InteractionRange - 0.65f; + arrivalDistance = SharedInteractionSystem.InteractionRange - 0.05f; } // Check if mapids match. @@ -126,6 +126,12 @@ public sealed partial class NPCSteeringSystem // Breaking behaviours and the likes. lock (_obstacles) { + // We're still coming to a stop so wait for the do_after. + if (body.LinearVelocity.LengthSquared > 0.01f) + { + return true; + } + status = TryHandleFlags(uid, steering, node, bodyQuery); } diff --git a/Content.Server/NPC/Systems/NPCSteeringSystem.cs b/Content.Server/NPC/Systems/NPCSteeringSystem.cs index 7ab9231648..2ccdb7c6f2 100644 --- a/Content.Server/NPC/Systems/NPCSteeringSystem.cs +++ b/Content.Server/NPC/Systems/NPCSteeringSystem.cs @@ -28,453 +28,452 @@ using Robust.Shared.Threading; using Robust.Shared.Timing; using Robust.Shared.Utility; -namespace Content.Server.NPC.Systems +namespace Content.Server.NPC.Systems; + +public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem { - public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem + /* + * We use context steering to determine which way to move. + * This involves creating an array of possible directions and assigning a value for the desireability of each direction. + * + * There's multiple ways to implement this, e.g. you can average all directions, or you can choose the highest direction + * , or you can remove the danger map entirely and only having an interest map (AKA game endeavour). + * See http://www.gameaipro.com/GameAIPro2/GameAIPro2_Chapter18_Context_Steering_Behavior-Driven_Steering_at_the_Macro_Scale.pdf + * (though in their case it was for an F1 game so used context steering across the width of the road). + */ + + [Dependency] private readonly IAdminManager _admin = default!; + [Dependency] private readonly IConfigurationManager _configManager = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IParallelManager _parallel = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly DoAfterSystem _doAfter = default!; + [Dependency] private readonly DoorSystem _doors = default!; + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly FactionSystem _faction = default!; + [Dependency] private readonly PathfindingSystem _pathfindingSystem = default!; + [Dependency] private readonly SharedInteractionSystem _interaction = default!; + [Dependency] private readonly SharedMeleeWeaponSystem _melee = default!; + [Dependency] private readonly SharedMoverController _mover = default!; + [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly SharedCombatModeSystem _combat = default!; + + /// + /// Enabled antistuck detection so if an NPC is in the same spot for a while it will re-path. + /// + public bool AntiStuck = true; + + private bool _enabled; + + private bool _pathfinding = true; + + public static readonly Vector2[] Directions = new Vector2[InterestDirections]; + + private readonly HashSet _subscribedSessions = new(); + + private object _obstacles = new(); + + private ISawmill _sawmill = default!; + + public override void Initialize() { - /* - * We use context steering to determine which way to move. - * This involves creating an array of possible directions and assigning a value for the desireability of each direction. - * - * There's multiple ways to implement this, e.g. you can average all directions, or you can choose the highest direction - * , or you can remove the danger map entirely and only having an interest map (AKA game endeavour). - * See http://www.gameaipro.com/GameAIPro2/GameAIPro2_Chapter18_Context_Steering_Behavior-Driven_Steering_at_the_Macro_Scale.pdf - * (though in their case it was for an F1 game so used context steering across the width of the road). - */ - - [Dependency] private readonly IAdminManager _admin = default!; - [Dependency] private readonly IConfigurationManager _configManager = default!; - [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] private readonly IMapManager _mapManager = default!; - [Dependency] private readonly IParallelManager _parallel = default!; - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly DoAfterSystem _doAfter = default!; - [Dependency] private readonly DoorSystem _doors = default!; - [Dependency] private readonly EntityLookupSystem _lookup = default!; - [Dependency] private readonly FactionSystem _faction = default!; - [Dependency] private readonly PathfindingSystem _pathfindingSystem = default!; - [Dependency] private readonly SharedInteractionSystem _interaction = default!; - [Dependency] private readonly SharedMeleeWeaponSystem _melee = default!; - [Dependency] private readonly SharedMoverController _mover = default!; - [Dependency] private readonly SharedPhysicsSystem _physics = default!; - [Dependency] private readonly SharedTransformSystem _transform = default!; - [Dependency] private readonly SharedCombatModeSystem _combat = default!; - - /// - /// Enabled antistuck detection so if an NPC is in the same spot for a while it will re-path. - /// - public bool AntiStuck = true; - - private bool _enabled; - - private bool _pathfinding = true; - - public static readonly Vector2[] Directions = new Vector2[InterestDirections]; - - private readonly HashSet _subscribedSessions = new(); - - private object _obstacles = new(); - - private ISawmill _sawmill = default!; - - public override void Initialize() - { - base.Initialize(); - _sawmill = Logger.GetSawmill("npc.steering"); + base.Initialize(); + _sawmill = Logger.GetSawmill("npc.steering"); #if DEBUG - _sawmill.Level = LogLevel.Warning; + _sawmill.Level = LogLevel.Warning; #else _sawmill.Level = LogLevel.Debug; #endif - for (var i = 0; i < InterestDirections; i++) - { - Directions[i] = new Angle(InterestRadians * i).ToVec(); - } + for (var i = 0; i < InterestDirections; i++) + { + Directions[i] = new Angle(InterestRadians * i).ToVec(); + } - UpdatesBefore.Add(typeof(SharedPhysicsSystem)); - _configManager.OnValueChanged(CCVars.NPCEnabled, SetNPCEnabled, true); - _configManager.OnValueChanged(CCVars.NPCPathfinding, SetNPCPathfinding, true); + UpdatesBefore.Add(typeof(SharedPhysicsSystem)); + _configManager.OnValueChanged(CCVars.NPCEnabled, SetNPCEnabled, true); + _configManager.OnValueChanged(CCVars.NPCPathfinding, SetNPCPathfinding, true); - SubscribeLocalEvent(OnSteeringShutdown); - SubscribeLocalEvent(OnSteeringUnpaused); - SubscribeNetworkEvent(OnDebugRequest); - } + SubscribeLocalEvent(OnSteeringShutdown); + SubscribeLocalEvent(OnSteeringUnpaused); + SubscribeNetworkEvent(OnDebugRequest); + } - private void SetNPCEnabled(bool obj) + private void SetNPCEnabled(bool obj) + { + if (!obj) { - if (!obj) + foreach (var (comp, mover) in EntityQuery()) { - foreach (var (comp, mover) in EntityQuery()) - { - mover.CurTickSprintMovement = Vector2.Zero; - comp.PathfindToken?.Cancel(); - comp.PathfindToken = null; - } + mover.CurTickSprintMovement = Vector2.Zero; + comp.PathfindToken?.Cancel(); + comp.PathfindToken = null; } - - _enabled = obj; } - private void SetNPCPathfinding(bool value) - { - _pathfinding = value; + _enabled = obj; + } - if (!_pathfinding) + private void SetNPCPathfinding(bool value) + { + _pathfinding = value; + + if (!_pathfinding) + { + foreach (var comp in EntityQuery(true)) { - foreach (var comp in EntityQuery(true)) - { - comp.PathfindToken?.Cancel(); - comp.PathfindToken = null; - } + comp.PathfindToken?.Cancel(); + comp.PathfindToken = null; } } + } - public override void Shutdown() - { - base.Shutdown(); - _configManager.UnsubValueChanged(CCVars.NPCEnabled, SetNPCEnabled); - _configManager.UnsubValueChanged(CCVars.NPCPathfinding, SetNPCPathfinding); - } + public override void Shutdown() + { + base.Shutdown(); + _configManager.UnsubValueChanged(CCVars.NPCEnabled, SetNPCEnabled); + _configManager.UnsubValueChanged(CCVars.NPCPathfinding, SetNPCPathfinding); + } - private void OnDebugRequest(RequestNPCSteeringDebugEvent msg, EntitySessionEventArgs args) - { - if (!_admin.IsAdmin((IPlayerSession) args.SenderSession)) - return; + private void OnDebugRequest(RequestNPCSteeringDebugEvent msg, EntitySessionEventArgs args) + { + if (!_admin.IsAdmin((IPlayerSession) args.SenderSession)) + return; - if (msg.Enabled) - _subscribedSessions.Add(args.SenderSession); - else - _subscribedSessions.Remove(args.SenderSession); - } + if (msg.Enabled) + _subscribedSessions.Add(args.SenderSession); + else + _subscribedSessions.Remove(args.SenderSession); + } - private void OnSteeringShutdown(EntityUid uid, NPCSteeringComponent component, ComponentShutdown args) + private void OnSteeringShutdown(EntityUid uid, NPCSteeringComponent component, ComponentShutdown args) + { + // Cancel any active pathfinding jobs as they're irrelevant. + component.PathfindToken?.Cancel(); + component.PathfindToken = null; + } + + private void OnSteeringUnpaused(EntityUid uid, NPCSteeringComponent component, ref EntityUnpausedEvent args) + { + component.LastStuckTime += args.PausedTime; + component.NextSteer += args.PausedTime; + } + + /// + /// Adds the AI to the steering system to move towards a specific target + /// + public NPCSteeringComponent Register(EntityUid uid, EntityCoordinates coordinates, NPCSteeringComponent? component = null) + { + if (Resolve(uid, ref component, false)) { - // Cancel any active pathfinding jobs as they're irrelevant. component.PathfindToken?.Cancel(); component.PathfindToken = null; + component.CurrentPath.Clear(); } - - private void OnSteeringUnpaused(EntityUid uid, NPCSteeringComponent component, ref EntityUnpausedEvent args) + else { - component.LastStuckTime += args.PausedTime; - component.NextSteer += args.PausedTime; + component = AddComp(uid); + component.Flags = _pathfindingSystem.GetFlags(uid); } - /// - /// Adds the AI to the steering system to move towards a specific target - /// - public NPCSteeringComponent Register(EntityUid uid, EntityCoordinates coordinates, NPCSteeringComponent? component = null) - { - if (Resolve(uid, ref component, false)) - { - component.PathfindToken?.Cancel(); - component.PathfindToken = null; - component.CurrentPath.Clear(); - } - else - { - component = AddComp(uid); - component.Flags = _pathfindingSystem.GetFlags(uid); - } - - ResetStuck(component, Transform(uid).Coordinates); - component.Coordinates = coordinates; - return component; - } + ResetStuck(component, Transform(uid).Coordinates); + component.Coordinates = coordinates; + return component; + } - /// - /// Attempts to register the entity. Does nothing if the coordinates already registered. - /// - public bool TryRegister(EntityUid uid, EntityCoordinates coordinates, NPCSteeringComponent? component = null) + /// + /// Attempts to register the entity. Does nothing if the coordinates already registered. + /// + public bool TryRegister(EntityUid uid, EntityCoordinates coordinates, NPCSteeringComponent? component = null) + { + if (Resolve(uid, ref component, false) && component.Coordinates.Equals(coordinates)) { - if (Resolve(uid, ref component, false) && component.Coordinates.Equals(coordinates)) - { - return false; - } - - Register(uid, coordinates, component); - return true; + return false; } - /// - /// Stops the steering behavior for the AI and cleans up. - /// - public void Unregister(EntityUid uid, NPCSteeringComponent? component = null) - { - if (!Resolve(uid, ref component, false)) - return; + Register(uid, coordinates, component); + return true; + } - if (EntityManager.TryGetComponent(uid, out InputMoverComponent? controller)) - { - controller.CurTickSprintMovement = Vector2.Zero; - } + /// + /// Stops the steering behavior for the AI and cleans up. + /// + public void Unregister(EntityUid uid, NPCSteeringComponent? component = null) + { + if (!Resolve(uid, ref component, false)) + return; - component.PathfindToken?.Cancel(); - component.PathfindToken = null; - RemComp(uid); + if (EntityManager.TryGetComponent(uid, out InputMoverComponent? controller)) + { + controller.CurTickSprintMovement = Vector2.Zero; } - public override void Update(float frameTime) - { - base.Update(frameTime); + component.PathfindToken?.Cancel(); + component.PathfindToken = null; + RemComp(uid); + } - if (!_enabled) - return; + public override void Update(float frameTime) + { + base.Update(frameTime); - // Not every mob has the modifier component so do it as a separate query. - var bodyQuery = GetEntityQuery(); - var modifierQuery = GetEntityQuery(); - var xformQuery = GetEntityQuery(); + if (!_enabled) + return; - var npcs = EntityQuery() - .Select(o => (o.Item1.Owner, o.Item2, o.Item3, o.Item4)).ToArray(); + // Not every mob has the modifier component so do it as a separate query. + var bodyQuery = GetEntityQuery(); + var modifierQuery = GetEntityQuery(); + var xformQuery = GetEntityQuery(); - // Dependency issues across threads. - var options = new ParallelOptions - { - MaxDegreeOfParallelism = 1, - }; - var curTime = _timing.CurTime; + var npcs = EntityQuery() + .Select(o => (o.Item1.Owner, o.Item2, o.Item3, o.Item4)).ToArray(); - Parallel.For(0, npcs.Length, options, i => - { - var (uid, steering, mover, xform) = npcs[i]; - Steer(uid, steering, mover, xform, modifierQuery, bodyQuery, xformQuery, frameTime, curTime); - }); + // Dependency issues across threads. + var options = new ParallelOptions + { + MaxDegreeOfParallelism = 1, + }; + var curTime = _timing.CurTime; + Parallel.For(0, npcs.Length, options, i => + { + var (uid, steering, mover, xform) = npcs[i]; + Steer(uid, steering, mover, xform, modifierQuery, bodyQuery, xformQuery, frameTime, curTime); + }); - if (_subscribedSessions.Count > 0) - { - var data = new List(npcs.Length); - - foreach (var (uid, steering, mover, _) in npcs) - { - data.Add(new NPCSteeringDebugData( - uid, - mover.CurTickSprintMovement, - steering.Interest, - steering.Danger, - steering.DangerPoints)); - } - - var filter = Filter.Empty(); - filter.AddPlayers(_subscribedSessions); - - RaiseNetworkEvent(new NPCSteeringDebugEvent(data), filter); - } - } - private void SetDirection(InputMoverComponent component, NPCSteeringComponent steering, Vector2 value, bool clear = true) + if (_subscribedSessions.Count > 0) { - if (clear && value.Equals(Vector2.Zero)) + var data = new List(npcs.Length); + + foreach (var (uid, steering, mover, _) in npcs) { - steering.CurrentPath.Clear(); + data.Add(new NPCSteeringDebugData( + uid, + mover.CurTickSprintMovement, + steering.Interest, + steering.Danger, + steering.DangerPoints)); } - component.CurTickSprintMovement = value; - component.LastInputTick = _timing.CurTick; - component.LastInputSubTick = ushort.MaxValue; + var filter = Filter.Empty(); + filter.AddPlayers(_subscribedSessions); + + RaiseNetworkEvent(new NPCSteeringDebugEvent(data), filter); } + } - /// - /// Go through each steerer and combine their vectors - /// - private void Steer( - EntityUid uid, - NPCSteeringComponent steering, - InputMoverComponent mover, - TransformComponent xform, - EntityQuery modifierQuery, - EntityQuery bodyQuery, - EntityQuery xformQuery, - float frameTime, - TimeSpan curTime) + private void SetDirection(InputMoverComponent component, NPCSteeringComponent steering, Vector2 value, bool clear = true) + { + if (clear && value.Equals(Vector2.Zero)) { - if (Deleted(steering.Coordinates.EntityId)) - { - SetDirection(mover, steering, Vector2.Zero); - steering.Status = SteeringStatus.NoPath; - return; - } + steering.CurrentPath.Clear(); + } - // No path set from pathfinding or the likes. - if (steering.Status == SteeringStatus.NoPath) - { - SetDirection(mover, steering, Vector2.Zero); - return; - } + component.CurTickSprintMovement = value; + component.LastInputTick = _timing.CurTick; + component.LastInputSubTick = ushort.MaxValue; + } - // Can't move at all, just noop input. - if (!mover.CanMove) - { - SetDirection(mover, steering, Vector2.Zero); - steering.Status = SteeringStatus.NoPath; - return; - } + /// + /// Go through each steerer and combine their vectors + /// + private void Steer( + EntityUid uid, + NPCSteeringComponent steering, + InputMoverComponent mover, + TransformComponent xform, + EntityQuery modifierQuery, + EntityQuery bodyQuery, + EntityQuery xformQuery, + float frameTime, + TimeSpan curTime) + { + if (Deleted(steering.Coordinates.EntityId)) + { + SetDirection(mover, steering, Vector2.Zero); + steering.Status = SteeringStatus.NoPath; + return; + } - var interest = steering.Interest; - var danger = steering.Danger; - var agentRadius = steering.Radius; - var worldPos = _transform.GetWorldPosition(xform, xformQuery); - var (layer, mask) = _physics.GetHardCollision(uid); - - // Use rotation relative to parent to rotate our context vectors by. - var offsetRot = -_mover.GetParentGridAngle(mover); - modifierQuery.TryGetComponent(uid, out var modifier); - var moveSpeed = GetSprintSpeed(uid, modifier); - var body = bodyQuery.GetComponent(uid); - var dangerPoints = steering.DangerPoints; - dangerPoints.Clear(); - - for (var i = 0; i < InterestDirections; i++) - { - steering.Interest[i] = 0f; - steering.Danger[i] = 0f; - } + // No path set from pathfinding or the likes. + if (steering.Status == SteeringStatus.NoPath) + { + SetDirection(mover, steering, Vector2.Zero); + return; + } - var ev = new NPCSteeringEvent(steering, interest, danger, agentRadius, offsetRot, worldPos); - RaiseLocalEvent(uid, ref ev); - // If seek has arrived at the target node for example then immediately re-steer. - var forceSteer = true; + // Can't move at all, just noop input. + if (!mover.CanMove) + { + SetDirection(mover, steering, Vector2.Zero); + steering.Status = SteeringStatus.NoPath; + return; + } - if (steering.CanSeek && !TrySeek(uid, mover, steering, body, xform, offsetRot, moveSpeed, interest, bodyQuery, frameTime, ref forceSteer)) - { - SetDirection(mover, steering, Vector2.Zero); - return; - } - DebugTools.Assert(!float.IsNaN(interest[0])); + var interest = steering.Interest; + var danger = steering.Danger; + var agentRadius = steering.Radius; + var worldPos = _transform.GetWorldPosition(xform, xformQuery); + var (layer, mask) = _physics.GetHardCollision(uid); + + // Use rotation relative to parent to rotate our context vectors by. + var offsetRot = -_mover.GetParentGridAngle(mover); + modifierQuery.TryGetComponent(uid, out var modifier); + var moveSpeed = GetSprintSpeed(uid, modifier); + var body = bodyQuery.GetComponent(uid); + var dangerPoints = steering.DangerPoints; + dangerPoints.Clear(); + + for (var i = 0; i < InterestDirections; i++) + { + steering.Interest[i] = 0f; + steering.Danger[i] = 0f; + } - // Avoid static objects like walls - CollisionAvoidance(uid, offsetRot, worldPos, agentRadius, layer, mask, xform, danger, dangerPoints, bodyQuery, xformQuery); - DebugTools.Assert(!float.IsNaN(danger[0])); + var ev = new NPCSteeringEvent(steering, interest, danger, agentRadius, offsetRot, worldPos); + RaiseLocalEvent(uid, ref ev); + // If seek has arrived at the target node for example then immediately re-steer. + var forceSteer = true; - Separation(uid, offsetRot, worldPos, agentRadius, layer, mask, body, xform, danger, bodyQuery, xformQuery); + if (steering.CanSeek && !TrySeek(uid, mover, steering, body, xform, offsetRot, moveSpeed, interest, bodyQuery, frameTime, ref forceSteer)) + { + SetDirection(mover, steering, Vector2.Zero); + return; + } + DebugTools.Assert(!float.IsNaN(interest[0])); - // Remove the danger map from the interest map. - var desiredDirection = -1; - var desiredValue = 0f; + // Avoid static objects like walls + CollisionAvoidance(uid, offsetRot, worldPos, agentRadius, layer, mask, xform, danger, dangerPoints, bodyQuery, xformQuery); + DebugTools.Assert(!float.IsNaN(danger[0])); - for (var i = 0; i < InterestDirections; i++) - { - var adjustedValue = Math.Clamp(interest[i] - danger[i], 0f, 1f); + Separation(uid, offsetRot, worldPos, agentRadius, layer, mask, body, xform, danger, bodyQuery, xformQuery); - if (adjustedValue > desiredValue) - { - desiredDirection = i; - desiredValue = adjustedValue; - } - } + // Remove the danger map from the interest map. + var desiredDirection = -1; + var desiredValue = 0f; - var resultDirection = Vector2.Zero; + for (var i = 0; i < InterestDirections; i++) + { + var adjustedValue = Math.Clamp(interest[i] - danger[i], 0f, 1f); - if (desiredDirection != -1) + if (adjustedValue > desiredValue) { - resultDirection = new Angle(desiredDirection * InterestRadians).ToVec(); + desiredDirection = i; + desiredValue = adjustedValue; } + } - // Don't steer too frequently to avoid twitchiness. - // This should also implicitly solve tie situations. - // I think doing this after all the ops above is best? - // Originally I had it way above but sometimes mobs would overshoot their tile targets. - - if (!forceSteer && steering.NextSteer > curTime) - { - SetDirection(mover, steering, steering.LastSteerDirection, false); - return; - } + var resultDirection = Vector2.Zero; - steering.NextSteer = curTime + TimeSpan.FromSeconds(1f / NPCSteeringComponent.SteeringFrequency); - steering.LastSteerDirection = resultDirection; - DebugTools.Assert(!float.IsNaN(resultDirection.X)); - SetDirection(mover, steering, resultDirection, false); + if (desiredDirection != -1) + { + resultDirection = new Angle(desiredDirection * InterestRadians).ToVec(); } - private EntityCoordinates GetCoordinates(PathPoly poly) - { - if (!poly.IsValid()) - return EntityCoordinates.Invalid; + // Don't steer too frequently to avoid twitchiness. + // This should also implicitly solve tie situations. + // I think doing this after all the ops above is best? + // Originally I had it way above but sometimes mobs would overshoot their tile targets. - return new EntityCoordinates(poly.GraphUid, poly.Box.Center); + if (!forceSteer && steering.NextSteer > curTime) + { + SetDirection(mover, steering, steering.LastSteerDirection, false); + return; } - /// - /// Get a new job from the pathfindingsystem - /// - private async void RequestPath(EntityUid uid, NPCSteeringComponent steering, TransformComponent xform, float targetDistance) - { - // If we already have a pathfinding request then don't grab another. - // If we're in range then just beeline them; this can avoid stutter stepping and is an easy way to look nicer. - if (steering.Pathfind || targetDistance < steering.RepathRange) - return; - - // Short-circuit with no path. - var targetPoly = _pathfindingSystem.GetPoly(steering.Coordinates); - - // If this still causes issues future sloth adjust the collision mask. - // Thanks past sloth I already realised. - if (targetPoly != null && - steering.Coordinates.Position.Equals(Vector2.Zero) && - TryComp(uid, out var physics) && - _interaction.InRangeUnobstructed(uid, steering.Coordinates.EntityId, range: 30f, (CollisionGroup) physics.CollisionMask)) - { - steering.CurrentPath.Clear(); - // Enqueue our poly as it will be pruned later. - var ourPoly = _pathfindingSystem.GetPoly(xform.Coordinates); + steering.NextSteer = curTime + TimeSpan.FromSeconds(1f / NPCSteeringComponent.SteeringFrequency); + steering.LastSteerDirection = resultDirection; + DebugTools.Assert(!float.IsNaN(resultDirection.X)); + SetDirection(mover, steering, resultDirection, false); + } + + private EntityCoordinates GetCoordinates(PathPoly poly) + { + if (!poly.IsValid()) + return EntityCoordinates.Invalid; - if (ourPoly != null) - { - steering.CurrentPath.Enqueue(ourPoly); - } + return new EntityCoordinates(poly.GraphUid, poly.Box.Center); + } + + /// + /// Get a new job from the pathfindingsystem + /// + private async void RequestPath(EntityUid uid, NPCSteeringComponent steering, TransformComponent xform, float targetDistance) + { + // If we already have a pathfinding request then don't grab another. + // If we're in range then just beeline them; this can avoid stutter stepping and is an easy way to look nicer. + if (steering.Pathfind || targetDistance < steering.RepathRange) + return; + + // Short-circuit with no path. + var targetPoly = _pathfindingSystem.GetPoly(steering.Coordinates); + + // If this still causes issues future sloth adjust the collision mask. + // Thanks past sloth I already realised. + if (targetPoly != null && + steering.Coordinates.Position.Equals(Vector2.Zero) && + TryComp(uid, out var physics) && + _interaction.InRangeUnobstructed(uid, steering.Coordinates.EntityId, range: 30f, (CollisionGroup) physics.CollisionMask)) + { + steering.CurrentPath.Clear(); + // Enqueue our poly as it will be pruned later. + var ourPoly = _pathfindingSystem.GetPoly(xform.Coordinates); - steering.CurrentPath.Enqueue(targetPoly); - return; + if (ourPoly != null) + { + steering.CurrentPath.Enqueue(ourPoly); } - steering.PathfindToken = new CancellationTokenSource(); + steering.CurrentPath.Enqueue(targetPoly); + return; + } - var flags = _pathfindingSystem.GetFlags(uid); + steering.PathfindToken = new CancellationTokenSource(); - var result = await _pathfindingSystem.GetPathSafe( - uid, - xform.Coordinates, - steering.Coordinates, - steering.Range, - steering.PathfindToken.Token, - flags); + var flags = _pathfindingSystem.GetFlags(uid); - steering.PathfindToken = null; + var result = await _pathfindingSystem.GetPathSafe( + uid, + xform.Coordinates, + steering.Coordinates, + steering.Range, + steering.PathfindToken.Token, + flags); - if (result.Result == PathResult.NoPath) - { - steering.CurrentPath.Clear(); - steering.FailedPathCount++; + steering.PathfindToken = null; - if (steering.FailedPathCount >= NPCSteeringComponent.FailedPathLimit) - { - steering.Status = SteeringStatus.NoPath; - } + if (result.Result == PathResult.NoPath) + { + steering.CurrentPath.Clear(); + steering.FailedPathCount++; - return; + if (steering.FailedPathCount >= NPCSteeringComponent.FailedPathLimit) + { + steering.Status = SteeringStatus.NoPath; } - var targetPos = steering.Coordinates.ToMap(EntityManager, _transform); - var ourPos = xform.MapPosition; - - PrunePath(uid, ourPos, targetPos.Position - ourPos.Position, result.Path); - steering.CurrentPath = result.Path; + return; } - // TODO: Move these to movercontroller + var targetPos = steering.Coordinates.ToMap(EntityManager, _transform); + var ourPos = xform.MapPosition; - private float GetSprintSpeed(EntityUid uid, MovementSpeedModifierComponent? modifier = null) - { - if (!Resolve(uid, ref modifier, false)) - { - return MovementSpeedModifierComponent.DefaultBaseSprintSpeed; - } + PrunePath(uid, ourPos, targetPos.Position - ourPos.Position, result.Path); + steering.CurrentPath = result.Path; + } + + // TODO: Move these to movercontroller - return modifier.CurrentSprintSpeed; + private float GetSprintSpeed(EntityUid uid, MovementSpeedModifierComponent? modifier = null) + { + if (!Resolve(uid, ref modifier, false)) + { + return MovementSpeedModifierComponent.DefaultBaseSprintSpeed; } + + return modifier.CurrentSprintSpeed; } } -- 2.51.2