diff --git a/Content.Client/NPC/PathfindingSystem.cs b/Content.Client/NPC/PathfindingSystem.cs
index b6f726e4d8..716360f256 100644
--- a/Content.Client/NPC/PathfindingSystem.cs
+++ b/Content.Client/NPC/PathfindingSystem.cs
@@ -551,6 +551,9 @@ namespace Content.Client.NPC
foreach (var route in _system.Routes)
{
+ if (route.Message.Costs.Values.Count == 0)
+ continue;
+
var highestGScore = route.Message.Costs.Values.Max();
foreach (var (node, cost) in route.Message.Costs)
diff --git a/Content.Server/NPC/Pathfinding/PathfindingSystem.Common.cs b/Content.Server/NPC/Pathfinding/PathfindingSystem.Common.cs
index 5d31f47e26..f3e7123a78 100644
--- a/Content.Server/NPC/Pathfinding/PathfindingSystem.Common.cs
+++ b/Content.Server/NPC/Pathfinding/PathfindingSystem.Common.cs
@@ -1,8 +1,4 @@
-using Content.Shared.Gravity;
-using Content.Shared.Maps;
using Content.Shared.NPC;
-using Robust.Shared.Map.Components;
-using Robust.Shared.Spawners;
namespace Content.Server.NPC.Pathfinding;
@@ -61,18 +57,20 @@ public sealed partial class PathfindingSystem
// TODO: Handling power + door prying
// Door we should be able to open
- if (isDoor && !isAccess && (request.Flags & PathFlags.Interact) != 0x0)
+ if (isDoor)
{
- modifier += 0.5f;
- }
- // Door we can force open one way or another
- else if (isDoor && isAccess && (request.Flags & PathFlags.Prying) != 0x0)
- {
- modifier += 10f;
+ if (!isAccess && (request.Flags & PathFlags.Interact) != 0x0)
+ modifier += 0.5f;
+ else if (isAccess && (request.Flags & PathFlags.Prying) != 0x0)
+ modifier += 10f;
+ else
+ // Last ditch—try to bump the door if it's the only feasible option.
+ modifier += 20f;
}
else if ((request.Flags & PathFlags.Smashing) != 0x0 && end.Data.Damage > 0f)
{
- modifier += 10f + end.Data.Damage / 100f;
+ // Breaking stuff should be usually last resort, especially because we WILL try to punch walls.
+ modifier += 10f + end.Data.Damage / 10f;
}
else if (isClimb && (request.Flags & PathFlags.Climbing) != 0x0)
{
diff --git a/Content.Server/NPC/Systems/NPCCombatSystem.Melee.cs b/Content.Server/NPC/Systems/NPCCombatSystem.Melee.cs
index f6cf49c14d..c5480ba160 100644
--- a/Content.Server/NPC/Systems/NPCCombatSystem.Melee.cs
+++ b/Content.Server/NPC/Systems/NPCCombatSystem.Melee.cs
@@ -97,6 +97,12 @@ public sealed partial class NPCCombatSystem
return;
}
+ if (!_interaction.InRangeUnobstructed(uid, component.Target, distance + 0.1f))
+ {
+ component.Status = CombatStatus.TargetUnreachable;
+ return;
+ }
+
if (weapon.NextAttack > curTime || !Enabled)
return;
diff --git a/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs b/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs
index 40a3b43ea6..7d20b5b1b8 100644
--- a/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs
+++ b/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs
@@ -1,9 +1,11 @@
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
-using Content.Server.Examine;
using Content.Server.NPC.Components;
using Content.Server.NPC.Pathfinding;
-using Content.Shared.Climbing;
+using Content.Shared.Climbing.Components;
+using Content.Shared.CombatMode;
+using Content.Shared.Doors.Components;
using Content.Shared.Interaction;
using Content.Shared.Movement.Components;
using Content.Shared.NPC;
@@ -208,19 +210,9 @@ public sealed partial class NPCSteeringSystem
{
// Ignore stuck while handling obstacles.
ResetStuck(steering, ourCoordinates);
- SteeringObstacleStatus status;
// 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);
- }
+ var status = TryHandleFlags(uid, steering, node);
// TODO: Need to handle re-pathing in case the target moves around.
switch (status)
@@ -497,6 +489,7 @@ public sealed partial class NPCSteeringSystem
///
private void CollisionAvoidance(
EntityUid uid,
+ NPCSteeringComponent steering,
Angle offsetRot,
Vector2 worldPos,
float agentRadius,
@@ -509,20 +502,10 @@ public sealed partial class NPCSteeringSystem
var detectionRadius = MathF.Max(0.35f, agentRadius + objectRadius);
var ents = _entSetPool.Get();
_lookup.GetEntitiesInRange(uid, detectionRadius, ents, LookupFlags.Dynamic | LookupFlags.Static | LookupFlags.Approximate);
-
+ // Filter out stuff that we can pry/vault/bash through
+ FilterObstacleEntities((uid, steering), mask, layer, ents, false);
foreach (var ent in ents)
{
- // TODO: If we can access the door or smth.
- if (!_physicsQuery.TryGetComponent(ent, out var otherBody) ||
- !otherBody.Hard ||
- !otherBody.CanCollide ||
- otherBody.BodyType == BodyType.KinematicController ||
- (mask & otherBody.CollisionLayer) == 0x0 &&
- (layer & otherBody.CollisionMask) == 0x0)
- {
- continue;
- }
-
var xformB = _xformQuery.GetComponent(ent);
if (!_physics.TryGetNearest(uid, ent,
@@ -564,6 +547,107 @@ public sealed partial class NPCSteeringSystem
_entSetPool.Return(ents);
}
+ ///
+ /// Filter down a list of entities into what can be considered obstacles.
+ ///
+ /// If allObstacles is set to false, don't filter out obstacles that can
+ /// be avoided by prying/vaulting/punching.
+ ///
+ private void FilterObstacleEntities(
+ Entity ent,
+ int mask,
+ int layer,
+ HashSet nearbyEntities,
+ bool allObstacles = true)
+ {
+ if (!ent.Comp.CurrentPath.TryPeek(out var poly))
+ return;
+
+ var climbing = CompOrNull(ent.Owner);
+ var combatMode = CompOrNull(ent.Owner);
+
+ var checkDoors = (poly.Data.Flags & PathfindingBreadcrumbFlag.Door) != 0x0;
+ var checkClimbs = (poly.Data.Flags & PathfindingBreadcrumbFlag.Climb) != 0x0 &&
+ (ent.Comp.Flags & PathFlags.Climbing) != 0x0 &&
+ climbing != null;
+ var checkSmash = (ent.Comp.Flags & PathFlags.Smashing) != 0x0 &&
+ combatMode != null &&
+ _melee.TryGetWeapon(ent, out _, out var weapon) &&
+ weapon.NextAttack <= _timing.CurTime;
+
+
+ nearbyEntities.RemoveWhere(nearbyEnt =>
+ {
+ // Get rid of stuff we can phase through.
+ if (!_physicsQuery.TryGetComponent(nearbyEnt, out var otherBody) ||
+ !otherBody.Hard ||
+ !otherBody.CanCollide ||
+ otherBody.BodyType == BodyType.KinematicController ||
+ (mask & otherBody.CollisionLayer) == 0x0 &&
+ (layer & otherBody.CollisionMask) == 0x0)
+ {
+ return true;
+ }
+
+ // If we just care about physical obstacles then this entity's checks are done.
+ if (allObstacles)
+ return false;
+
+ // If we're walking into a door we can handle...
+ if (checkDoors &&
+ CanHandleDoor(ent, poly.Data.Flags, nearbyEnt))
+ return true;
+
+ // Then check climbability.
+ if (checkClimbs &&
+ CanHandleClimb((ent, climbing!), nearbyEnt, out _))
+ return true;
+
+ // Check if we can smash. Should also check if we can even damage the entity at some point.
+ return checkSmash && _destructibleQuery.HasComponent(nearbyEnt);
+ });
+ }
+
+ private bool CanHandleDoor(Entity ent,
+ PathfindingBreadcrumbFlag flags,
+ EntityUid doorUid,
+ bool allowPrying = true)
+ {
+ if (!_doorQuery.TryComp(doorUid, out var door))
+ return false;
+
+ if (door.State == DoorState.Opening)
+ return true;
+
+ var isAccessRequired = (flags & PathfindingBreadcrumbFlag.Access) != 0x0 &&
+ !_access.IsAllowed(ent, doorUid);
+ var canInteract = (ent.Comp.Flags & PathFlags.Interact) != 0x0;
+
+ // If not access locked we're fine if it can be bumped open or we can interact.
+ if (!isAccessRequired
+ && (door.BumpOpen || canInteract))
+ return true;
+
+ // Last possibility is that we can pry it open.
+ return allowPrying && (ent.Comp.Flags & PathFlags.Prying) != 0x0;
+ }
+
+ private bool CanHandleClimb(
+ Entity ent,
+ EntityUid climbableUid,
+ [NotNullWhen(true)] out ClimbableComponent? climbable)
+ {
+ if (!_climbableQuery.TryComp(climbableUid, out climbable))
+ return false;
+
+ // We're already climbing something, we're fine.
+ if (ent.Comp.IsClimbing || ent.Comp.NextTransition != null)
+ return true;
+
+ // Actually check if we can climb this
+ return _climb.CanVault(climbable, ent, climbableUid, out _);
+ }
+
#endregion
#region Dynamic Avoidance
diff --git a/Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs b/Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs
index b84f2cdaee..eb583b8a56 100644
--- a/Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs
+++ b/Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs
@@ -58,160 +58,129 @@ public sealed partial class NPCSteeringSystem
// TODO: Should cache the fact we're doing this somewhere.
// See https://github.com/space-wizards/space-station-14/issues/11475
- if ((poly.Data.CollisionLayer & mask) != 0x0 ||
- (poly.Data.CollisionMask & layer) != 0x0)
+ if ((poly.Data.CollisionLayer & mask) == 0x0 &&
+ (poly.Data.CollisionMask & layer) == 0x0)
+ return SteeringObstacleStatus.Completed;
+
+ var id = component.DoAfterId;
+
+ // Still doing what we were doing before.
+ var doAfterStatus = _doAfter.GetStatus(id);
+
+ switch (doAfterStatus)
{
- var id = component.DoAfterId;
-
- // Still doing what we were doing before.
- var doAfterStatus = _doAfter.GetStatus(id);
-
- switch (doAfterStatus)
- {
- case DoAfterStatus.Running:
- return SteeringObstacleStatus.Continuing;
- case DoAfterStatus.Cancelled:
- return SteeringObstacleStatus.Failed;
- }
-
- var obstacleEnts = new List();
-
- GetObstacleEntities(poly, mask, layer, obstacleEnts);
- var isDoor = (poly.Data.Flags & PathfindingBreadcrumbFlag.Door) != 0x0;
- var isAccessRequired = (poly.Data.Flags & PathfindingBreadcrumbFlag.Access) != 0x0;
- var isClimbable = (poly.Data.Flags & PathfindingBreadcrumbFlag.Climb) != 0x0;
-
- // Just walk into it stupid
- if (isDoor && !isAccessRequired)
- {
- // ... At least if it's not a bump open.
- foreach (var ent in obstacleEnts)
- {
- if (!_doorQuery.TryGetComponent(ent, out var door))
- continue;
-
- if (!door.BumpOpen && (component.Flags & PathFlags.Interact) != 0x0)
- {
- if (door.State != DoorState.Opening)
- {
- _interaction.InteractionActivate(uid, ent);
- return SteeringObstacleStatus.Continuing;
- }
- }
- }
-
- // If we get to here then didn't succeed for reasons.
- }
-
- if ((component.Flags & PathFlags.Prying) != 0x0 && isDoor)
- {
- // Get the relevant obstacle
- foreach (var ent in obstacleEnts)
- {
- if (_doorQuery.TryGetComponent(ent, out var door) && door.State != DoorState.Open)
- {
- // TODO: Use the verb.
-
- if (door.State != DoorState.Opening)
- _pryingSystem.TryPry(ent, uid, out id, uid);
-
- component.DoAfterId = id;
- return SteeringObstacleStatus.Continuing;
- }
- }
-
- if (obstacleEnts.Count == 0)
- return SteeringObstacleStatus.Completed;
- }
- // Try climbing obstacles
- else if ((component.Flags & PathFlags.Climbing) != 0x0 && isClimbable)
- {
- if (TryComp(uid, out var climbing))
- {
- if (climbing.IsClimbing)
- {
- return SteeringObstacleStatus.Completed;
- }
- else if (climbing.NextTransition != null)
- {
- return SteeringObstacleStatus.Continuing;
- }
-
- // Get the relevant obstacle
- foreach (var ent in obstacleEnts)
- {
- if (_climbableQuery.TryGetComponent(ent, out var table) &&
- _climb.CanVault(table, uid, uid, out _) &&
- _climb.TryClimb(uid, uid, ent, out id, table, climbing))
- {
- component.DoAfterId = id;
- return SteeringObstacleStatus.Continuing;
- }
- }
- }
-
- if (obstacleEnts.Count == 0)
- return SteeringObstacleStatus.Completed;
- }
- // Try smashing obstacles.
- else if ((component.Flags & PathFlags.Smashing) != 0x0)
- {
- if (_melee.TryGetWeapon(uid, out _, out var meleeWeapon) && meleeWeapon.NextAttack <= _timing.CurTime && TryComp(uid, out var combatMode))
- {
- _combat.SetInCombatMode(uid, true, combatMode);
- // TODO: This is a hack around grilles and windows.
- _random.Shuffle(obstacleEnts);
- var attackResult = false;
-
- foreach (var ent in obstacleEnts)
- {
- // TODO: Validate we can damage it
- if (_destructibleQuery.HasComponent(ent))
- {
- attackResult = _melee.AttemptLightAttack(uid, uid, meleeWeapon, ent);
- break;
- }
- }
-
- _combat.SetInCombatMode(uid, false, combatMode);
-
- // Blocked or the likes?
- if (!attackResult)
- return SteeringObstacleStatus.Failed;
-
- if (obstacleEnts.Count == 0)
- return SteeringObstacleStatus.Completed;
-
- return SteeringObstacleStatus.Continuing;
- }
- }
-
- return SteeringObstacleStatus.Failed;
+ case DoAfterStatus.Running:
+ return SteeringObstacleStatus.Continuing;
+ case DoAfterStatus.Cancelled:
+ return SteeringObstacleStatus.Failed;
}
- return SteeringObstacleStatus.Completed;
- }
-
- private void GetObstacleEntities(PathPoly poly, int mask, int layer, List ents)
- {
- // TODO: Can probably re-use this from pathfinding or something
if (!TryComp(poly.GraphUid, out var grid))
- {
- return;
- }
+ return SteeringObstacleStatus.Completed;
- foreach (var ent in _mapSystem.GetLocalAnchoredEntities(poly.GraphUid, grid, poly.Box))
+ var obstacleEnts = new HashSet(_mapSystem.GetLocalAnchoredEntities(poly.GraphUid, grid, poly.Box));
+ FilterObstacleEntities((uid, component), mask, layer, obstacleEnts);
+
+ // Nothing actually near us.
+ if (obstacleEnts.Count == 0)
+ return SteeringObstacleStatus.Completed;
+
+ var isDoor = (poly.Data.Flags & PathfindingBreadcrumbFlag.Door) != 0x0;
+ var isClimbable = (poly.Data.Flags & PathfindingBreadcrumbFlag.Climb) != 0x0;
+
+ // Just walk into it stupid.
+ if (isDoor)
{
- if (!_physicsQuery.TryGetComponent(ent, out var body) ||
- !body.Hard ||
- !body.CanCollide ||
- (body.CollisionMask & layer) == 0x0 && (body.CollisionLayer & mask) == 0x0)
+ foreach (var ent in obstacleEnts)
{
- continue;
+ // Includes checking if we have access:
+ if (!CanHandleDoor((uid, component), poly.Data.Flags, ent, false))
+ continue;
+
+ // Interacts are bit nicer than bumps, so try interacting regardless.
+ _interaction.InteractionActivate(uid, ent);
+
+ return SteeringObstacleStatus.Continuing;
}
- ents.Add(ent);
+ // Couldn't normal-open the door, and can't pry. Give up.
+ if ((component.Flags & PathFlags.Prying) == 0x0)
+ return SteeringObstacleStatus.Failed;
+
+ foreach (var ent in obstacleEnts)
+ {
+ if (!CanHandleDoor((uid, component), poly.Data.Flags, ent))
+ continue;
+
+ // Should be able to pry from CanHandleDoor:
+ // TODO: Use the verb.
+ _pryingSystem.TryPry(ent, uid, out id, uid);
+
+ if (id == null)
+ return SteeringObstacleStatus.Failed;
+
+ component.DoAfterId = id;
+ return SteeringObstacleStatus.Continuing;
+ }
}
+ // Try climbing obstacles
+ else if ((component.Flags & PathFlags.Climbing) != 0x0 && isClimbable)
+ {
+ if (!TryComp(uid, out var climbing))
+ return SteeringObstacleStatus.Failed;
+
+ if (climbing.IsClimbing)
+ return SteeringObstacleStatus.Completed;
+
+ if (climbing.NextTransition != null)
+ return SteeringObstacleStatus.Continuing;
+
+ // Get the relevant obstacle.
+ foreach (var ent in obstacleEnts)
+ {
+ if (CanHandleClimb((uid, climbing), ent, out var climbable) &&
+ _climb.TryClimb(uid, uid, ent, out id, climbable, climbing))
+ {
+ component.DoAfterId = id;
+ return SteeringObstacleStatus.Continuing;
+ }
+ }
+ }
+ // Try smashing obstacles.
+ else if ((component.Flags & PathFlags.Smashing) != 0x0)
+ {
+ // Check we have a weapon, can (probably) swing it, and have combat mode.
+ if (!_melee.TryGetWeapon(uid, out var weaponUid, out var weaponComp) ||
+ weaponComp.NextAttack > _timing.CurTime ||
+ !TryComp(uid, out var combatMode))
+ return SteeringObstacleStatus.Failed;
+
+ _combat.SetInCombatMode(uid, true, combatMode);
+
+ var attackResult = false;
+ foreach (var ent in obstacleEnts)
+ {
+ if (!_destructibleQuery.HasComponent(ent) ||
+ !_interaction.InRangeUnobstructed(uid, ent, weaponComp.Range))
+ continue;
+
+ if (_melee.AttemptLightAttack(uid, weaponUid, weaponComp, ent))
+ {
+ attackResult = true;
+ break;
+ }
+ }
+
+ _combat.SetInCombatMode(uid, false, combatMode);
+
+ // Blocked or the likes?
+ if (!attackResult)
+ return SteeringObstacleStatus.Failed;
+
+ return SteeringObstacleStatus.Continuing;
+ }
+
+ return SteeringObstacleStatus.Failed;
}
private enum SteeringObstacleStatus : byte
diff --git a/Content.Server/NPC/Systems/NPCSteeringSystem.cs b/Content.Server/NPC/Systems/NPCSteeringSystem.cs
index 3487548c27..36c46fe206 100644
--- a/Content.Server/NPC/Systems/NPCSteeringSystem.cs
+++ b/Content.Server/NPC/Systems/NPCSteeringSystem.cs
@@ -6,6 +6,7 @@ using Content.Server.DoAfter;
using Content.Server.NPC.Components;
using Content.Server.NPC.Events;
using Content.Server.NPC.Pathfinding;
+using Content.Shared.Access.Systems;
using Content.Shared.CCVar;
using Content.Shared.Climbing.Systems;
using Content.Shared.CombatMode;
@@ -54,6 +55,7 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
[Dependency] private IConfigurationManager _configManager = default!;
[Dependency] private IGameTiming _timing = default!;
[Dependency] private IRobustRandom _random = default!;
+ [Dependency] private AccessReaderSystem _access = default!;
[Dependency] private ClimbSystem _climb = default!;
[Dependency] private DoAfterSystem _doAfter = default!;
[Dependency] private EntityLookupSystem _lookup = default!;
@@ -90,8 +92,6 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
private readonly HashSet _subscribedSessions = new();
- private object _obstacles = new();
-
private int _activeSteeringCount;
public override void Initialize()
@@ -373,7 +373,7 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
}
// Avoid static objects like walls
- CollisionAvoidance(uid, offsetRot, worldPos, agentRadius, layer, mask, xform, danger);
+ CollisionAvoidance(uid, steering, offsetRot, worldPos, agentRadius, layer, mask, xform, danger);
DebugTools.Assert(!float.IsNaN(danger[0]));
Separation(uid, offsetRot, worldPos, agentRadius, layer, mask, body, xform, danger);