| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686 |
- 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.Interaction;
- using Content.Shared.Movement.Components;
- using Content.Shared.NPC;
- using Content.Shared.Physics;
- using Robust.Shared.Map;
- using Robust.Shared.Physics;
- using Robust.Shared.Physics.Components;
- using ClimbingComponent = Content.Shared.Climbing.Components.ClimbingComponent;
- namespace Content.Server.NPC.Systems;
- public sealed partial class NPCSteeringSystem
- {
- private void ApplySeek(Span<float> interest, Vector2 direction, float weight)
- {
- if (weight == 0f || direction == Vector2.Zero)
- return;
- var directionAngle = (float)direction.ToAngle().Theta;
- for (var i = 0; i < InterestDirections; i++)
- {
- var angle = i * InterestRadians;
- var dot = MathF.Cos(directionAngle - angle);
- dot = (dot + 1f) * 0.5f;
- interest[i] = Math.Clamp(interest[i] + dot * weight, 0f, 1f);
- }
- }
- #region Seek
- /// <summary>
- /// Takes into account agent-specific context that may allow it to bypass a node which is not FreeSpace.
- /// </summary>
- 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<ClimbingComponent>(uid, out var climbing) &&
- climbing.IsClimbing)
- {
- return true;
- }
- // TODO: Ideally for "FreeSpace" we check all entities on the tile and build flags dynamically (pathfinder refactor in future).
- var ents = _entSetPool.Get();
- _lookup.GetLocalEntitiesIntersecting(node.GraphUid, node.Box.Enlarged(-0.04f), ents, flags: LookupFlags.Static);
- var result = true;
- if (ents.Count > 0)
- {
- var fixtures = _fixturesQuery.GetComponent(uid);
- var physics = _physicsQuery.GetComponent(uid);
- foreach (var intersecting in ents)
- {
- if (!_physics.IsCurrentlyHardCollidable((uid, fixtures, physics), intersecting))
- {
- continue;
- }
- result = false;
- break;
- }
- }
- _entSetPool.Return(ents);
- return result;
- }
- /// <summary>
- /// Attempts to head to the target destination, either via the next pathfinding node or the final target.
- /// </summary>
- private bool TrySeek(
- EntityUid uid,
- InputMoverComponent mover,
- NPCSteeringComponent steering,
- PhysicsComponent body,
- TransformComponent xform,
- Angle offsetRot,
- float moveSpeed,
- Span<float> interest,
- float frameTime,
- ref bool forceSteer)
- {
- var ourCoordinates = xform.Coordinates;
- var destinationCoordinates = steering.Coordinates;
- var inLos = true;
- // Check if we're in LOS if that's required.
- // TODO: Need something uhh better not sure on the interaction between these.
- if (!steering.ForceMove && steering.ArriveOnLineOfSight)
- {
- // TODO: use vision range
- inLos = _interaction.InRangeUnobstructed(uid, steering.Coordinates, 10f);
- if (inLos)
- {
- steering.LineOfSightTimer += frameTime;
- if (steering.LineOfSightTimer >= steering.LineOfSightTimeRequired)
- {
- steering.Status = SteeringStatus.InRange;
- ResetStuck(steering, ourCoordinates);
- return true;
- }
- }
- else
- {
- steering.LineOfSightTimer = 0f;
- }
- }
- else
- {
- steering.LineOfSightTimer = 0f;
- steering.ForceMove = false;
- }
- // We've arrived, nothing else matters.
- if (xform.Coordinates.TryDistance(EntityManager, destinationCoordinates, out var targetDistance) &&
- inLos &&
- targetDistance <= steering.Range)
- {
- steering.Status = SteeringStatus.InRange;
- ResetStuck(steering, ourCoordinates);
- return true;
- }
- // Grab the target position, either the next path node or our end goal..
- var targetCoordinates = GetTargetCoordinates(steering);
- if (!targetCoordinates.IsValid(EntityManager))
- {
- steering.Status = SteeringStatus.NoPath;
- return false;
- }
- var needsPath = false;
- // If the next node is invalid then get new ones
- if (!targetCoordinates.IsValid(EntityManager))
- {
- if (steering.CurrentPath.TryPeek(out var poly) &&
- (poly.Data.Flags & PathfindingBreadcrumbFlag.Invalid) != 0x0)
- {
- steering.CurrentPath.Dequeue();
- // Try to get the next node temporarily.
- targetCoordinates = GetTargetCoordinates(steering);
- needsPath = true;
- ResetStuck(steering, ourCoordinates);
- }
- }
- // Check if mapids match.
- var targetMap = _transform.ToMapCoordinates(targetCoordinates);
- var ourMap = _transform.ToMapCoordinates(ourCoordinates);
- if (targetMap.MapId != ourMap.MapId)
- {
- steering.Status = SteeringStatus.NoPath;
- return false;
- }
- var direction = targetMap.Position - ourMap.Position;
- // Need to be pretty close if it's just a node to make sure LOS for door bashes or the likes.
- bool arrived;
- if (targetCoordinates.Equals(steering.Coordinates))
- {
- // What's our tolerance for arrival.
- // If it's a pathfinding node it might be different to the destination.
- arrived = direction.Length() <= steering.Range;
- }
- // 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) && IsFreeSpace(uid, steering, node))
- {
- arrived = node.Box.Contains(ourCoordinates.Position);
- }
- // Try getting into blocked range I guess?
- // TODO: Consider melee range or the likes.
- else
- {
- arrived = direction.Length() <= SharedInteractionSystem.InteractionRange - 0.05f;
- }
- // Are we in range
- if (arrived)
- {
- // Node needs some kind of special handling like access or smashing.
- if (steering.CurrentPath.TryPeek(out var node) && !IsFreeSpace(uid, steering, node))
- {
- // 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);
- }
- // TODO: Need to handle re-pathing in case the target moves around.
- switch (status)
- {
- case SteeringObstacleStatus.Completed:
- steering.DoAfterId = null;
- break;
- case SteeringObstacleStatus.Failed:
- steering.DoAfterId = null;
- // TODO: Blacklist the poly for next query
- steering.Status = SteeringStatus.NoPath;
- return false;
- case SteeringObstacleStatus.Continuing:
- CheckPath(uid, steering, xform, needsPath, targetDistance);
- return true;
- default:
- throw new ArgumentOutOfRangeException();
- }
- }
- // Distance should already be handled above.
- // It was just a node, not the target, so grab the next destination (either the target or next node).
- if (steering.CurrentPath.Count > 0)
- {
- forceSteer = true;
- steering.CurrentPath.Dequeue();
- // Alright just adjust slightly and grab the next node so we don't stop moving for a tick.
- // TODO: If it's the last node just grab the target instead.
- targetCoordinates = GetTargetCoordinates(steering);
- if (!targetCoordinates.IsValid(EntityManager))
- {
- SetDirection(uid, mover, steering, Vector2.Zero);
- steering.Status = SteeringStatus.NoPath;
- return false;
- }
- targetMap = _transform.ToMapCoordinates(targetCoordinates);
- // Can't make it again.
- if (ourMap.MapId != targetMap.MapId)
- {
- SetDirection(uid, mover, steering, Vector2.Zero);
- steering.Status = SteeringStatus.NoPath;
- return false;
- }
- // Gonna resume now business as usual
- direction = targetMap.Position - ourMap.Position;
- ResetStuck(steering, ourCoordinates);
- }
- else
- {
- needsPath = true;
- }
- }
- // Stuck detection
- // Check if we have moved further than the movespeed * stuck time.
- else if (AntiStuck &&
- ourCoordinates.TryDistance(EntityManager, steering.LastStuckCoordinates, out var stuckDistance) &&
- stuckDistance < NPCSteeringComponent.StuckDistance)
- {
- var stuckTime = _timing.CurTime - steering.LastStuckTime;
- // Either 1 second or how long it takes to move the stuck distance + buffer if we're REALLY slow.
- var maxStuckTime = Math.Max(1, NPCSteeringComponent.StuckDistance / moveSpeed * 1.2f);
- if (stuckTime.TotalSeconds > maxStuckTime)
- {
- // TODO: Blacklist nodes (pathfinder factor wehn)
- // TODO: This should be a warning but
- // A) NPCs get stuck on non-anchored static bodies still (e.g. closets)
- // B) NPCs still try to move in locked containers (e.g. cow, hamster)
- // and I don't want to spam grafana even harder than it gets spammed rn.
- Log.Debug($"NPC {ToPrettyString(uid)} found stuck at {ourCoordinates}");
- needsPath = true;
- if (stuckTime.TotalSeconds > maxStuckTime * 3)
- {
- steering.Status = SteeringStatus.NoPath;
- return false;
- }
- }
- }
- else
- {
- ResetStuck(steering, ourCoordinates);
- }
- // If not in LOS and no path then get a new one fam.
- if ((!inLos && steering.ArriveOnLineOfSight && steering.CurrentPath.Count == 0) ||
- (!steering.ArriveOnLineOfSight && steering.CurrentPath.Count == 0))
- {
- needsPath = true;
- }
- // TODO: Probably need partial planning support i.e. patch from the last node to where the target moved to.
- CheckPath(uid, steering, xform, needsPath, targetDistance);
- // If we don't have a path yet then do nothing; this is to avoid stutter-stepping if it turns out there's no path
- // available but we assume there was.
- if (steering is { Pathfind: true, CurrentPath.Count: 0 })
- return true;
- if (moveSpeed == 0f || direction == Vector2.Zero)
- {
- steering.Status = SteeringStatus.NoPath;
- return false;
- }
- var input = direction.Normalized();
- var tickMovement = moveSpeed * frameTime;
- // We have the input in world terms but need to convert it back to what movercontroller is doing.
- input = offsetRot.RotateVec(input);
- var norm = input.Normalized();
- var weight = MapValue(direction.Length(), tickMovement * 0.5f, tickMovement * 0.75f);
- ApplySeek(interest, norm, weight);
- // Prefer our current direction
- if (weight > 0f && body.LinearVelocity.LengthSquared() > 0f)
- {
- const float sameDirectionWeight = 0.1f;
- norm = body.LinearVelocity.Normalized();
- ApplySeek(interest, norm, sameDirectionWeight);
- }
- return true;
- }
- private void ResetStuck(NPCSteeringComponent component, EntityCoordinates ourCoordinates)
- {
- component.LastStuckCoordinates = ourCoordinates;
- component.LastStuckTime = _timing.CurTime;
- }
- private void CheckPath(EntityUid uid, NPCSteeringComponent steering, TransformComponent xform, bool needsPath, float targetDistance)
- {
- if (!_pathfinding)
- {
- steering.CurrentPath.Clear();
- steering.PathfindToken?.Cancel();
- steering.PathfindToken = null;
- return;
- }
- if (!needsPath && steering.CurrentPath.Count > 0)
- {
- needsPath = steering.CurrentPath.Count > 0 && (steering.CurrentPath.Peek().Data.Flags & PathfindingBreadcrumbFlag.Invalid) != 0x0;
- // If the target has sufficiently moved.
- var lastNode = GetCoordinates(steering.CurrentPath.Last());
- if (lastNode.TryDistance(EntityManager, steering.Coordinates, out var lastDistance) &&
- lastDistance > steering.RepathRange)
- {
- needsPath = true;
- }
- }
- // Request the new path.
- if (needsPath)
- {
- RequestPath(uid, steering, xform, targetDistance);
- }
- }
- /// <summary>
- /// We may be pathfinding and moving at the same time in which case early nodes may be out of date.
- /// </summary>
- public void PrunePath(EntityUid uid, MapCoordinates mapCoordinates, Vector2 direction, List<PathPoly> nodes)
- {
- if (nodes.Count <= 1)
- return;
- // Work out if we're inside any nodes, then use the next one as the starting point.
- var index = 0;
- var found = false;
- for (var i = 0; i < nodes.Count; i++)
- {
- var node = nodes[i];
- var matrix = _transform.GetWorldMatrix(node.GraphUid);
- // Always want to prune the poly itself so we point to the next poly and don't backtrack.
- if (matrix.TransformBox(node.Box).Contains(mapCoordinates.Position))
- {
- index = i + 1;
- found = true;
- break;
- }
- }
- if (found)
- {
- nodes.RemoveRange(0, index);
- _pathfindingSystem.Simplify(nodes);
- return;
- }
- // Otherwise, take the node after the nearest node.
- // TODO: Really need layer support
- CollisionGroup mask = 0;
- if (TryComp<PhysicsComponent>(uid, out var physics))
- {
- mask = (CollisionGroup)physics.CollisionMask;
- }
- for (var i = 0; i < nodes.Count; i++)
- {
- var node = nodes[i];
- if (!node.Data.IsFreeSpace)
- break;
- var nodeMap = _transform.ToMapCoordinates(node.Coordinates);
- // If any nodes are 'behind us' relative to the target we'll prune them.
- // This isn't perfect but should fix most cases of stutter stepping.
- if (nodeMap.MapId == mapCoordinates.MapId &&
- Vector2.Dot(direction, nodeMap.Position - mapCoordinates.Position) < 0f)
- {
- nodes.RemoveAt(i);
- continue;
- }
- break;
- }
- _pathfindingSystem.Simplify(nodes);
- }
- /// <summary>
- /// Get the coordinates we should be heading towards.
- /// </summary>
- private EntityCoordinates GetTargetCoordinates(NPCSteeringComponent steering)
- {
- // Depending on what's going on we may return the target or a pathfind node.
- // Even if we're at the last node may not be able to head to target in case we get stuck on a corner or the likes.
- if (_pathfinding && steering.CurrentPath.Count >= 1 && steering.CurrentPath.TryPeek(out var nextTarget))
- {
- return GetCoordinates(nextTarget);
- }
- return steering.Coordinates;
- }
- /// <summary>
- /// Gets the fraction this value is between min and max
- /// </summary>
- /// <returns></returns>
- private float MapValue(float value, float minValue, float maxValue)
- {
- if (maxValue > minValue)
- {
- var mapped = (value - minValue) / (maxValue - minValue);
- return Math.Clamp(mapped, 0f, 1f);
- }
- return value >= minValue ? 1f : 0f;
- }
- #endregion
- #region Static Avoidance
- /// <summary>
- /// Tries to avoid static blockers such as walls.
- /// </summary>
- private void CollisionAvoidance(
- EntityUid uid,
- Angle offsetRot,
- Vector2 worldPos,
- float agentRadius,
- int layer,
- int mask,
- TransformComponent xform,
- Span<float> danger)
- {
- var objectRadius = 0.25f;
- var detectionRadius = MathF.Max(0.35f, agentRadius + objectRadius);
- var ents = _entSetPool.Get();
- _lookup.GetEntitiesInRange(uid, detectionRadius, ents, LookupFlags.Dynamic | LookupFlags.Static);
- 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,
- out var pointA, out var pointB, out var distance,
- xform, xformB))
- {
- continue;
- }
- if (distance > detectionRadius)
- continue;
- var weight = 1f;
- var obstacleDirection = pointB - pointA;
- // Inside each other so just use worldPos
- if (distance == 0f)
- {
- obstacleDirection = _transform.GetWorldPosition(xformB) - worldPos;
- }
- else
- {
- weight = (detectionRadius - distance) / detectionRadius;
- }
- if (obstacleDirection == Vector2.Zero)
- continue;
- obstacleDirection = offsetRot.RotateVec(obstacleDirection);
- var norm = obstacleDirection.Normalized();
- for (var i = 0; i < InterestDirections; i++)
- {
- var dot = Vector2.Dot(norm, Directions[i]);
- danger[i] = MathF.Max(dot * weight, danger[i]);
- }
- }
- _entSetPool.Return(ents);
- }
- #endregion
- #region Dynamic Avoidance
- /// <summary>
- /// Tries to avoid mobs of the same faction.
- /// </summary>
- private void Separation(
- EntityUid uid,
- Angle offsetRot,
- Vector2 worldPos,
- float agentRadius,
- int layer,
- int mask,
- PhysicsComponent body,
- TransformComponent xform,
- Span<float> danger)
- {
- var objectRadius = 0.25f;
- var detectionRadius = MathF.Max(0.35f, agentRadius + objectRadius);
- var ourVelocity = body.LinearVelocity;
- _factionQuery.TryGetComponent(uid, out var ourFaction);
- var ents = _entSetPool.Get();
- _lookup.GetEntitiesInRange(uid, detectionRadius, ents, LookupFlags.Dynamic);
- 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 ||
- (mask & otherBody.CollisionLayer) == 0x0 &&
- (layer & otherBody.CollisionMask) == 0x0 ||
- !_factionQuery.TryGetComponent(ent, out var otherFaction) ||
- !_npcFaction.IsEntityFriendly((uid, ourFaction), (ent, otherFaction)) ||
- // Use <= 0 so we ignore stationary friends in case.
- Vector2.Dot(otherBody.LinearVelocity, ourVelocity) <= 0f)
- {
- continue;
- }
- var xformB = _xformQuery.GetComponent(ent);
- if (!_physics.TryGetNearest(uid, ent, out var pointA, out var pointB, out var distance, xform, xformB))
- {
- continue;
- }
- if (distance > detectionRadius)
- continue;
- var weight = 1f;
- var obstacleDirection = pointB - pointA;
- // Inside each other so just use worldPos
- if (distance == 0f)
- {
- obstacleDirection = _transform.GetWorldPosition(xformB) - worldPos;
- // Welp
- if (obstacleDirection == Vector2.Zero)
- {
- obstacleDirection = _random.NextAngle().ToVec();
- }
- }
- else
- {
- weight = distance / detectionRadius;
- }
- obstacleDirection = offsetRot.RotateVec(obstacleDirection);
- var norm = obstacleDirection.Normalized();
- weight *= 0.25f;
- for (var i = 0; i < InterestDirections; i++)
- {
- var dot = Vector2.Dot(norm, Directions[i]);
- danger[i] = MathF.Max(dot * weight, danger[i]);
- }
- }
- _entSetPool.Return(ents);
- }
- #endregion
- // TODO: Alignment
- // TODO: Cohesion
- private void Blend(NPCSteeringComponent steering, float frameTime, Span<float> interest, Span<float> danger)
- {
- /*
- * Future sloth notes:
- * Pathfinder cleanup:
- - Cleanup whatever the fuck is happening in pathfinder
- - Use Flee for melee behavior / actions and get the seek direction from that rather than bulldozing
- - Must always have a path
- - Path should return the full version + the snipped version
- - Pathfinder needs to do diagonals
- - Next node is either <current node + 1> or <nearest node + 1> (on the full path)
- - If greater than <1.5m distance> repath
- */
- // IDK why I didn't do this sooner but blending is a lot better than lastdir for fixing stuttering.
- const float BlendWeight = 10f;
- var blendValue = Math.Min(1f, frameTime * BlendWeight);
- for (var i = 0; i < InterestDirections; i++)
- {
- var currentInterest = interest[i];
- var lastInterest = steering.Interest[i];
- var interestDiff = (currentInterest - lastInterest) * blendValue;
- steering.Interest[i] = lastInterest + interestDiff;
- var currentDanger = danger[i];
- var lastDanger = steering.Danger[i];
- var dangerDiff = (currentDanger - lastDanger) * blendValue;
- steering.Danger[i] = lastDanger + dangerDiff;
- }
- }
- }
|