From 49f3f07e30262386d462a48549d51f715260267a Mon Sep 17 00:00:00 2001 From: Vordenburg <114301317+Vordenburg@users.noreply.github.com> Date: Fri, 28 Jul 2023 02:37:29 -0400 Subject: [PATCH] Add pathfinding support for NPCs climbing tables (#17415) --- .../Tests/Climbing/ClimbingTest.cs | 4 +-- Content.Server/Climbing/ClimbSystem.cs | 20 ++++++----- Content.Server/NPC/NPCBlackboard.cs | 5 +++ Content.Server/NPC/Pathfinding/PathFlags.cs | 7 +++- .../Pathfinding/PathfindingSystem.Common.cs | 5 +++ .../NPC/Pathfinding/PathfindingSystem.Grid.cs | 12 +++++-- .../NPC/Pathfinding/PathfindingSystem.cs | 6 ++++ .../NPC/Systems/NPCSteeringSystem.Context.cs | 29 ++++++++++++++-- .../Systems/NPCSteeringSystem.Obstacles.cs | 34 +++++++++++++++++++ .../NPC/Systems/NPCSteeringSystem.cs | 2 ++ Content.Shared/NPC/PathfindingBreadcrumb.cs | 5 +++ 11 files changed, 114 insertions(+), 15 deletions(-) diff --git a/Content.IntegrationTests/Tests/Climbing/ClimbingTest.cs b/Content.IntegrationTests/Tests/Climbing/ClimbingTest.cs index 3212e51e3e..e909775793 100644 --- a/Content.IntegrationTests/Tests/Climbing/ClimbingTest.cs +++ b/Content.IntegrationTests/Tests/Climbing/ClimbingTest.cs @@ -29,7 +29,7 @@ public sealed class ClimbingTest : MovementTest // Try to start climbing var sys = SEntMan.System(); - await Server.WaitPost(() => sys.TryClimb(Player, Player, Target.Value)); + await Server.WaitPost(() => sys.TryClimb(Player, Player, Target.Value, out _)); await AwaitDoAfters(); // Player should now be climbing @@ -56,7 +56,7 @@ public sealed class ClimbingTest : MovementTest Assert.That(Delta(), Is.LessThan(0)); // Start climbing - await Server.WaitPost(() => sys.TryClimb(Player, Player, Target.Value)); + await Server.WaitPost(() => sys.TryClimb(Player, Player, Target.Value, out _)); await AwaitDoAfters(); Assert.Multiple(() => diff --git a/Content.Server/Climbing/ClimbSystem.cs b/Content.Server/Climbing/ClimbSystem.cs index 72802831e2..af634976cb 100644 --- a/Content.Server/Climbing/ClimbSystem.cs +++ b/Content.Server/Climbing/ClimbSystem.cs @@ -95,7 +95,7 @@ public sealed class ClimbSystem : SharedClimbSystem // TODO VERBS ICON add a climbing icon? args.Verbs.Add(new AlternativeVerb { - Act = () => TryClimb(args.User, args.User, args.Target, component), + Act = () => TryClimb(args.User, args.User, args.Target, out _, component), Text = Loc.GetString("comp-climbable-verb-climb") }); } @@ -106,22 +106,25 @@ public sealed class ClimbSystem : SharedClimbSystem // but don't have computer access and i have to do this without syntax if (args.Handled || args.User != args.Dragged && !HasComp(args.User)) return; - TryClimb(args.User, args.Dragged, uid, component); + TryClimb(args.User, args.Dragged, uid, out _, component); } - public void TryClimb(EntityUid user, + public bool TryClimb(EntityUid user, EntityUid entityToMove, EntityUid climbable, + out DoAfterId? id, ClimbableComponent? comp = null, ClimbingComponent? climbing = null) { + id = null; + if (!Resolve(climbable, ref comp) || !Resolve(entityToMove, ref climbing)) - return; + return false; // Note, IsClimbing does not mean a DoAfter is active, it means the target has already finished a DoAfter and // is currently on top of something.. if (climbing.IsClimbing) - return; + return true; var args = new DoAfterArgs(user, comp.ClimbDelay, new ClimbDoAfterEvent(), entityToMove, target: climbable, used: entityToMove) { @@ -130,7 +133,8 @@ public sealed class ClimbSystem : SharedClimbSystem BreakOnDamage = true }; - _doAfterSystem.TryStartDoAfter(args); + _doAfterSystem.TryStartDoAfter(args, out id); + return true; } private void OnDoAfter(EntityUid uid, ClimbingComponent component, ClimbDoAfterEvent args) @@ -279,7 +283,7 @@ public sealed class ClimbSystem : SharedClimbSystem /// The object that is being vaulted /// The reason why it cant be dropped /// - private bool CanVault(ClimbableComponent component, EntityUid user, EntityUid target, out string reason) + public bool CanVault(ClimbableComponent component, EntityUid user, EntityUid target, out string reason) { if (!_actionBlockerSystem.CanInteract(user, target)) { @@ -315,7 +319,7 @@ public sealed class ClimbSystem : SharedClimbSystem /// The object that is being vaulted onto /// The reason why it cant be dropped /// - private bool CanVault(ClimbableComponent component, EntityUid user, EntityUid dragged, EntityUid target, + public bool CanVault(ClimbableComponent component, EntityUid user, EntityUid dragged, EntityUid target, out string reason) { if (!_actionBlockerSystem.CanInteract(user, dragged) || !_actionBlockerSystem.CanInteract(user, target)) diff --git a/Content.Server/NPC/NPCBlackboard.cs b/Content.Server/NPC/NPCBlackboard.cs index f386fba21e..596b89f71c 100644 --- a/Content.Server/NPC/NPCBlackboard.cs +++ b/Content.Server/NPC/NPCBlackboard.cs @@ -225,6 +225,11 @@ public sealed class NPCBlackboard : IEnumerable> /// public const string NavSmash = "NavSmash"; + /// + /// Can the NPC climb obstacles for steering. + /// + public const string NavClimb = "NavClimb"; + /// /// Default key storage for a movement pathfind. /// diff --git a/Content.Server/NPC/Pathfinding/PathFlags.cs b/Content.Server/NPC/Pathfinding/PathFlags.cs index 1b60e187b8..656dc679de 100644 --- a/Content.Server/NPC/Pathfinding/PathFlags.cs +++ b/Content.Server/NPC/Pathfinding/PathFlags.cs @@ -20,8 +20,13 @@ public enum PathFlags : byte /// Smashing = 1 << 2, + /// + /// Can we climb it like a table or railing. + /// + Climbing = 1 << 3, + /// /// Can we open stuff that requires interaction (e.g. click-open doors). /// - Interact = 1 << 3, + Interact = 1 << 4, } diff --git a/Content.Server/NPC/Pathfinding/PathfindingSystem.Common.cs b/Content.Server/NPC/Pathfinding/PathfindingSystem.Common.cs index 82a9467ce2..2dbb4260a8 100644 --- a/Content.Server/NPC/Pathfinding/PathfindingSystem.Common.cs +++ b/Content.Server/NPC/Pathfinding/PathfindingSystem.Common.cs @@ -55,6 +55,7 @@ public sealed partial class PathfindingSystem { var isDoor = (end.Data.Flags & PathfindingBreadcrumbFlag.Door) != 0x0; var isAccess = (end.Data.Flags & PathfindingBreadcrumbFlag.Access) != 0x0; + var isClimb = (end.Data.Flags & PathfindingBreadcrumbFlag.Climb) != 0x0; // TODO: Handling power + door prying // Door we should be able to open @@ -71,6 +72,10 @@ public sealed partial class PathfindingSystem { modifier += 10f + end.Data.Damage / 100f; } + else if (isClimb && (request.Flags & PathFlags.Climbing) != 0x0) + { + modifier += 0.5f; + } else { return 0f; diff --git a/Content.Server/NPC/Pathfinding/PathfindingSystem.Grid.cs b/Content.Server/NPC/Pathfinding/PathfindingSystem.Grid.cs index 13c4aa92b8..db48ad2c7a 100644 --- a/Content.Server/NPC/Pathfinding/PathfindingSystem.Grid.cs +++ b/Content.Server/NPC/Pathfinding/PathfindingSystem.Grid.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Content.Server.Destructible; using Content.Shared.Access.Components; +using Content.Shared.Climbing; using Content.Shared.Doors.Components; using Content.Shared.NPC; using Content.Shared.Physics; @@ -154,11 +155,12 @@ public sealed partial class PathfindingSystem var accessQuery = GetEntityQuery(); var destructibleQuery = GetEntityQuery(); var doorQuery = GetEntityQuery(); + var climbableQuery = GetEntityQuery(); var fixturesQuery = GetEntityQuery(); var physicsQuery = GetEntityQuery(); var xformQuery = GetEntityQuery(); - BuildBreadcrumbs(dirt[i], mapGridComp, accessQuery, destructibleQuery, doorQuery, fixturesQuery, - physicsQuery, xformQuery); + BuildBreadcrumbs(dirt[i], mapGridComp, accessQuery, destructibleQuery, doorQuery, climbableQuery, + fixturesQuery, physicsQuery, xformQuery); }); const int Division = 4; @@ -423,6 +425,7 @@ public sealed partial class PathfindingSystem EntityQuery accessQuery, EntityQuery destructibleQuery, EntityQuery doorQuery, + EntityQuery climbableQuery, EntityQuery fixturesQuery, EntityQuery physicsQuery, EntityQuery xformQuery) @@ -540,6 +543,11 @@ public sealed partial class PathfindingSystem flags |= PathfindingBreadcrumbFlag.Door; } + if (climbableQuery.HasComponent(ent)) + { + flags |= PathfindingBreadcrumbFlag.Climb; + } + if (destructibleQuery.TryGetComponent(ent, out var damageable)) { damage += _destructible.DestroyedAt(ent, damageable).Float(); diff --git a/Content.Server/NPC/Pathfinding/PathfindingSystem.cs b/Content.Server/NPC/Pathfinding/PathfindingSystem.cs index 4f74b01c4e..fc9dc87c31 100644 --- a/Content.Server/NPC/Pathfinding/PathfindingSystem.cs +++ b/Content.Server/NPC/Pathfinding/PathfindingSystem.cs @@ -7,6 +7,7 @@ using Content.Server.Administration.Managers; using Content.Server.Destructible; using Content.Server.NPC.Components; using Content.Shared.Administration; +using Content.Shared.Climbing; using Content.Shared.Interaction; using Content.Shared.NPC; using Robust.Server.Player; @@ -436,6 +437,11 @@ namespace Content.Server.NPC.Pathfinding flags |= PathFlags.Smashing; } + if (blackboard.TryGetValue(NPCBlackboard.NavClimb, out var climb, EntityManager) && climb) + { + flags |= PathFlags.Climbing; + } + if (blackboard.TryGetValue(NPCBlackboard.NavInteract, out var interact, EntityManager) && interact) { flags |= PathFlags.Interact; diff --git a/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs b/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs index 0f57965ae1..797030bd5f 100644 --- a/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs +++ b/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs @@ -3,6 +3,7 @@ using System.Numerics; using Content.Server.Examine; using Content.Server.NPC.Components; using Content.Server.NPC.Pathfinding; +using Content.Shared.Climbing; using Content.Shared.Interaction; using Content.Shared.Movement.Components; using Content.Shared.NPC; @@ -35,6 +36,30 @@ public sealed partial class NPCSteeringSystem #region Seek + /// + /// Takes into account agent-specific context that may allow it to bypass a node which is not FreeSpace. + /// + private bool IsFreeSpace( + EntityUid uid, + NPCSteeringComponent steering, + PathPoly node) + { + if (node.Data.IsFreeSpace) + { + return true; + } + // Handle the case where the node is a climb, we can climb, and we are climbing. + else if ((node.Data.Flags & PathfindingBreadcrumbFlag.Climb) != 0x0 && + (steering.Flags & PathFlags.Climbing) != 0x0 && + TryComp(uid, out var climbing) && + climbing.IsClimbing) + { + return true; + } + + return false; + } + /// /// Attempts to head to the target destination, either via the next pathfinding node or the final target. /// @@ -90,7 +115,7 @@ public sealed partial class NPCSteeringSystem } // If next node is a free tile then get within its bounds. // This is to avoid popping it too early - else if (steering.CurrentPath.TryPeek(out var node) && node.Data.IsFreeSpace) + else if (steering.CurrentPath.TryPeek(out var node) && IsFreeSpace(uid, steering, node)) { arrivalDistance = MathF.Min(node.Box.Width / 2f, node.Box.Height / 2f) - 0.01f; } @@ -117,7 +142,7 @@ public sealed partial class NPCSteeringSystem if (direction.Length() <= arrivalDistance) { // Node needs some kind of special handling like access or smashing. - if (steering.CurrentPath.TryPeek(out var node) && !node.Data.IsFreeSpace) + if (steering.CurrentPath.TryPeek(out var node) && !IsFreeSpace(uid, steering, node)) { // Ignore stuck while handling obstacles. ResetStuck(steering, ourCoordinates); diff --git a/Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs b/Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs index a8a396ba42..1d9c19de02 100644 --- a/Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs +++ b/Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs @@ -1,6 +1,7 @@ using Content.Server.Destructible; using Content.Server.NPC.Components; using Content.Server.NPC.Pathfinding; +using Content.Shared.Climbing; using Content.Shared.CombatMode; using Content.Shared.DoAfter; using Content.Shared.Doors.Components; @@ -74,6 +75,7 @@ public sealed partial class NPCSteeringSystem 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) @@ -121,6 +123,38 @@ public sealed partial class NPCSteeringSystem 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.OwnerIsTransitioning) + { + return SteeringObstacleStatus.Continuing; + } + + var climbableQuery = GetEntityQuery(); + + // 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) { diff --git a/Content.Server/NPC/Systems/NPCSteeringSystem.cs b/Content.Server/NPC/Systems/NPCSteeringSystem.cs index 42e4fa5d29..9fe9f5ace3 100644 --- a/Content.Server/NPC/Systems/NPCSteeringSystem.cs +++ b/Content.Server/NPC/Systems/NPCSteeringSystem.cs @@ -3,6 +3,7 @@ using System.Numerics; using System.Threading; using System.Threading.Tasks; using Content.Server.Administration.Managers; +using Content.Server.Climbing; using Content.Server.DoAfter; using Content.Server.Doors.Systems; using Content.Server.NPC.Components; @@ -50,6 +51,7 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IParallelManager _parallel = default!; [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly ClimbSystem _climb = default!; [Dependency] private readonly DoAfterSystem _doAfter = default!; [Dependency] private readonly DoorSystem _doors = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; diff --git a/Content.Shared/NPC/PathfindingBreadcrumb.cs b/Content.Shared/NPC/PathfindingBreadcrumb.cs index 90a765da99..f0d3f26665 100644 --- a/Content.Shared/NPC/PathfindingBreadcrumb.cs +++ b/Content.Shared/NPC/PathfindingBreadcrumb.cs @@ -115,4 +115,9 @@ public enum PathfindingBreadcrumbFlag : ushort /// Is there access required /// Access = 1 << 3, + + /// + /// Is there climbing involved + /// + Climb = 1 << 4, }