| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482 |
- using System.Numerics;
- using System.Threading;
- using System.Threading.Tasks;
- using Content.Server.Administration.Managers;
- using Content.Server.DoAfter;
- using Content.Server.NPC.Components;
- using Content.Server.NPC.Events;
- using Content.Server.NPC.Pathfinding;
- using Content.Shared.CCVar;
- using Content.Shared.Climbing.Systems;
- using Content.Shared.CombatMode;
- using Content.Shared.Interaction;
- using Content.Shared.Movement.Components;
- using Content.Shared.Movement.Events;
- using Content.Shared.Movement.Systems;
- using Content.Shared.NPC;
- using Content.Shared.NPC.Components;
- using Content.Shared.NPC.Systems;
- using Content.Shared.NPC.Events;
- using Content.Shared.Physics;
- using Content.Shared.Weapons.Melee;
- using Robust.Shared.Configuration;
- using Robust.Shared.Map;
- using Robust.Shared.Physics;
- using Robust.Shared.Physics.Components;
- using Robust.Shared.Physics.Systems;
- using Robust.Shared.Player;
- using Robust.Shared.Random;
- using Robust.Shared.Timing;
- using Robust.Shared.Utility;
- using Content.Shared.Prying.Systems;
- using Microsoft.Extensions.ObjectPool;
- namespace Content.Server.NPC.Systems;
- 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 IRobustRandom _random = default!;
- [Dependency] private readonly ClimbSystem _climb = default!;
- [Dependency] private readonly DoAfterSystem _doAfter = default!;
- [Dependency] private readonly EntityLookupSystem _lookup = default!;
- [Dependency] private readonly NpcFactionSystem _npcFaction = default!;
- [Dependency] private readonly PathfindingSystem _pathfindingSystem = default!;
- [Dependency] private readonly PryingSystem _pryingSystem = default!;
- [Dependency] private readonly SharedMapSystem _mapSystem = 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!;
- private EntityQuery<FixturesComponent> _fixturesQuery;
- private EntityQuery<MovementSpeedModifierComponent> _modifierQuery;
- private EntityQuery<NpcFactionMemberComponent> _factionQuery;
- private EntityQuery<PhysicsComponent> _physicsQuery;
- private EntityQuery<TransformComponent> _xformQuery;
- private ObjectPool<HashSet<EntityUid>> _entSetPool =
- new DefaultObjectPool<HashSet<EntityUid>>(new SetPolicy<EntityUid>());
- /// <summary>
- /// Enabled antistuck detection so if an NPC is in the same spot for a while it will re-path.
- /// </summary>
- public bool AntiStuck = true;
- private bool _enabled;
- private bool _pathfinding = true;
- public static readonly Vector2[] Directions = new Vector2[InterestDirections];
- private readonly HashSet<ICommonSession> _subscribedSessions = new();
- private object _obstacles = new();
- public override void Initialize()
- {
- base.Initialize();
- Log.Level = LogLevel.Info;
- _fixturesQuery = GetEntityQuery<FixturesComponent>();
- _modifierQuery = GetEntityQuery<MovementSpeedModifierComponent>();
- _factionQuery = GetEntityQuery<NpcFactionMemberComponent>();
- _physicsQuery = GetEntityQuery<PhysicsComponent>();
- _xformQuery = GetEntityQuery<TransformComponent>();
- for (var i = 0; i < InterestDirections; i++)
- {
- Directions[i] = new Angle(InterestRadians * i).ToVec();
- }
- UpdatesBefore.Add(typeof(SharedPhysicsSystem));
- Subs.CVar(_configManager, CCVars.NPCEnabled, SetNPCEnabled, true);
- Subs.CVar(_configManager, CCVars.NPCPathfinding, SetNPCPathfinding, true);
- SubscribeLocalEvent<NPCSteeringComponent, ComponentShutdown>(OnSteeringShutdown);
- SubscribeNetworkEvent<RequestNPCSteeringDebugEvent>(OnDebugRequest);
- }
- private void SetNPCEnabled(bool obj)
- {
- if (!obj)
- {
- foreach (var (comp, mover) in EntityQuery<NPCSteeringComponent, InputMoverComponent>())
- {
- mover.CurTickSprintMovement = Vector2.Zero;
- comp.PathfindToken?.Cancel();
- comp.PathfindToken = null;
- }
- }
- _enabled = obj;
- }
- private void SetNPCPathfinding(bool value)
- {
- _pathfinding = value;
- if (!_pathfinding)
- {
- foreach (var comp in EntityQuery<NPCSteeringComponent>(true))
- {
- comp.PathfindToken?.Cancel();
- comp.PathfindToken = null;
- }
- }
- }
- private void OnDebugRequest(RequestNPCSteeringDebugEvent msg, EntitySessionEventArgs args)
- {
- if (!_admin.IsAdmin(args.SenderSession))
- return;
- if (msg.Enabled)
- _subscribedSessions.Add(args.SenderSession);
- else
- _subscribedSessions.Remove(args.SenderSession);
- }
- private void OnSteeringShutdown(EntityUid uid, NPCSteeringComponent component, ComponentShutdown args)
- {
- // Cancel any active pathfinding jobs as they're irrelevant.
- component.PathfindToken?.Cancel();
- component.PathfindToken = null;
- }
- /// <summary>
- /// Adds the AI to the steering system to move towards a specific target
- /// </summary>
- public NPCSteeringComponent Register(EntityUid uid, EntityCoordinates coordinates, NPCSteeringComponent? component = null)
- {
- if (Resolve(uid, ref component, false))
- {
- if (component.Coordinates.Equals(coordinates))
- return component;
- component.PathfindToken?.Cancel();
- component.PathfindToken = null;
- component.CurrentPath.Clear();
- }
- else
- {
- component = AddComp<NPCSteeringComponent>(uid);
- component.Flags = _pathfindingSystem.GetFlags(uid);
- }
- ResetStuck(component, Transform(uid).Coordinates);
- component.Coordinates = coordinates;
- return component;
- }
- /// <summary>
- /// Attempts to register the entity. Does nothing if the coordinates already registered.
- /// </summary>
- public bool TryRegister(EntityUid uid, EntityCoordinates coordinates, NPCSteeringComponent? component = null)
- {
- if (Resolve(uid, ref component, false) && component.Coordinates.Equals(coordinates))
- {
- return false;
- }
- Register(uid, coordinates, component);
- return true;
- }
- /// <summary>
- /// Stops the steering behavior for the AI and cleans up.
- /// </summary>
- public void Unregister(EntityUid uid, NPCSteeringComponent? component = null)
- {
- if (!Resolve(uid, ref component, false))
- return;
- if (EntityManager.TryGetComponent(uid, out InputMoverComponent? controller))
- {
- controller.CurTickSprintMovement = Vector2.Zero;
- var ev = new SpriteMoveEvent(false);
- RaiseLocalEvent(uid, ref ev);
- }
- component.PathfindToken?.Cancel();
- component.PathfindToken = null;
- RemComp<NPCSteeringComponent>(uid);
- }
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
- if (!_enabled)
- return;
- // Not every mob has the modifier component so do it as a separate query.
- var npcs = new (EntityUid, NPCSteeringComponent, InputMoverComponent, TransformComponent)[Count<ActiveNPCComponent>()];
- var query = EntityQueryEnumerator<ActiveNPCComponent, NPCSteeringComponent, InputMoverComponent, TransformComponent>();
- var index = 0;
- while (query.MoveNext(out var uid, out _, out var steering, out var mover, out var xform))
- {
- npcs[index] = (uid, steering, mover, xform);
- index++;
- }
- // Dependency issues across threads.
- var options = new ParallelOptions
- {
- MaxDegreeOfParallelism = 1,
- };
- var curTime = _timing.CurTime;
- Parallel.For(0, index, options, i =>
- {
- var (uid, steering, mover, xform) = npcs[i];
- Steer(uid, steering, mover, xform, frameTime, curTime);
- });
- if (_subscribedSessions.Count > 0)
- {
- var data = new List<NPCSteeringDebugData>(index);
- for (var i = 0; i < index; i++)
- {
- var (uid, steering, mover, _) = npcs[i];
- data.Add(new NPCSteeringDebugData(
- GetNetEntity(uid),
- mover.CurTickSprintMovement,
- steering.Interest,
- steering.Danger,
- steering.DangerPoints));
- }
- var filter = Filter.Empty();
- filter.AddPlayers(_subscribedSessions);
- RaiseNetworkEvent(new NPCSteeringDebugEvent(data), filter);
- }
- }
- private void SetDirection(EntityUid uid, InputMoverComponent component, NPCSteeringComponent steering, Vector2 value, bool clear = true)
- {
- if (clear && value.Equals(Vector2.Zero))
- {
- steering.CurrentPath.Clear();
- Array.Clear(steering.Interest);
- Array.Clear(steering.Danger);
- }
- component.CurTickSprintMovement = value;
- component.LastInputTick = _timing.CurTick;
- component.LastInputSubTick = ushort.MaxValue;
- var ev = new SpriteMoveEvent(true);
- RaiseLocalEvent(uid, ref ev);
- }
- /// <summary>
- /// Go through each steerer and combine their vectors
- /// </summary>
- private void Steer(
- EntityUid uid,
- NPCSteeringComponent steering,
- InputMoverComponent mover,
- TransformComponent xform,
- float frameTime,
- TimeSpan curTime)
- {
- if (Deleted(steering.Coordinates.EntityId))
- {
- SetDirection(uid, mover, steering, Vector2.Zero);
- steering.Status = SteeringStatus.NoPath;
- return;
- }
- // No path set from pathfinding or the likes.
- if (steering.Status == SteeringStatus.NoPath)
- {
- SetDirection(uid, mover, steering, Vector2.Zero);
- return;
- }
- // Can't move at all, just noop input.
- if (!mover.CanMove)
- {
- SetDirection(uid, mover, steering, Vector2.Zero);
- steering.Status = SteeringStatus.NoPath;
- return;
- }
- var agentRadius = steering.Radius;
- var worldPos = _transform.GetWorldPosition(xform);
- 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 = _physicsQuery.GetComponent(uid);
- var dangerPoints = steering.DangerPoints;
- dangerPoints.Clear();
- Span<float> interest = stackalloc float[InterestDirections];
- Span<float> danger = stackalloc float[InterestDirections];
- // TODO: This should be fly
- steering.CanSeek = true;
- var ev = new NPCSteeringEvent(steering, xform, worldPos, offsetRot);
- RaiseLocalEvent(uid, ref ev);
- // If seek has arrived at the target node for example then immediately re-steer.
- var forceSteer = true;
- if (steering.CanSeek && !TrySeek(uid, mover, steering, body, xform, offsetRot, moveSpeed, interest, frameTime, ref forceSteer))
- {
- SetDirection(uid, mover, steering, Vector2.Zero);
- return;
- }
- DebugTools.Assert(!float.IsNaN(interest[0]));
- // 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)
- {
- SetDirection(uid, mover, steering, steering.LastSteerDirection, false);
- return;
- }
- // Avoid static objects like walls
- CollisionAvoidance(uid, offsetRot, worldPos, agentRadius, layer, mask, xform, danger);
- DebugTools.Assert(!float.IsNaN(danger[0]));
- Separation(uid, offsetRot, worldPos, agentRadius, layer, mask, body, xform, danger);
- // Blend last and current tick
- Blend(steering, frameTime, interest, danger);
- // Remove the danger map from the interest map.
- var desiredDirection = -1;
- var desiredValue = 0f;
- for (var i = 0; i < InterestDirections; i++)
- {
- var adjustedValue = Math.Clamp(steering.Interest[i] - steering.Danger[i], 0f, 1f);
- if (adjustedValue > desiredValue)
- {
- desiredDirection = i;
- desiredValue = adjustedValue;
- }
- }
- var resultDirection = Vector2.Zero;
- if (desiredDirection != -1)
- {
- resultDirection = new Angle(desiredDirection * InterestRadians).ToVec();
- }
- steering.LastSteerDirection = resultDirection;
- DebugTools.Assert(!float.IsNaN(resultDirection.X));
- SetDirection(uid, mover, steering, resultDirection, false);
- }
- private EntityCoordinates GetCoordinates(PathPoly poly)
- {
- if (!poly.IsValid())
- return EntityCoordinates.Invalid;
- return new EntityCoordinates(poly.GraphUid, poly.Box.Center);
- }
- /// <summary>
- /// Get a new job from the pathfindingsystem
- /// </summary>
- 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<PhysicsComponent>(uid, out var physics) &&
- _interaction.InRangeUnobstructed(uid, steering.Coordinates.EntityId, range: 30f, (CollisionGroup)physics.CollisionMask))
- {
- steering.CurrentPath.Clear();
- steering.CurrentPath.Enqueue(targetPoly);
- return;
- }
- steering.PathfindToken = new CancellationTokenSource();
- var flags = _pathfindingSystem.GetFlags(uid);
- var result = await _pathfindingSystem.GetPathSafe(
- uid,
- xform.Coordinates,
- steering.Coordinates,
- steering.Range,
- steering.PathfindToken.Token,
- flags);
- steering.PathfindToken = null;
- if (result.Result == PathResult.NoPath)
- {
- steering.CurrentPath.Clear();
- steering.FailedPathCount++;
- if (steering.FailedPathCount >= NPCSteeringComponent.FailedPathLimit)
- {
- steering.Status = SteeringStatus.NoPath;
- }
- return;
- }
- var targetPos = steering.Coordinates.ToMap(EntityManager, _transform);
- var ourPos = _transform.GetMapCoordinates(uid, xform: xform);
- PrunePath(uid, ourPos, targetPos.Position - ourPos.Position, result.Path);
- steering.CurrentPath = new Queue<PathPoly>(result.Path);
- }
- // TODO: Move these to movercontroller
- private float GetSprintSpeed(EntityUid uid, MovementSpeedModifierComponent? modifier = null)
- {
- if (!Resolve(uid, ref modifier, false))
- {
- return MovementSpeedModifierComponent.DefaultBaseSprintSpeed;
- }
- return modifier.CurrentSprintSpeed;
- }
- }
|