Re-organize all projects (#4166)

This commit is contained in:
DrSmugleaf
2021-06-09 22:19:39 +02:00
committed by GitHub
parent 9f50e4061b
commit ff1a2d97ea
1773 changed files with 5258 additions and 5508 deletions

View File

@@ -1,119 +0,0 @@
#nullable enable
using System;
using System.Collections.Generic;
using Content.Server.GameObjects.Components.AI;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Prototypes;
namespace Content.Server.GameObjects.EntitySystems.AI
{
/// <summary>
/// Outlines faction relationships with each other for AI.
/// </summary>
public sealed class AiFactionTagSystem : EntitySystem
{
/*
* Currently factions are implicitly friendly if they are not hostile.
* This may change where specified friendly factions are listed. (e.g. to get number of friendlies in area).
*/
private readonly Dictionary<Faction, Faction> _hostileFactions = new();
public override void Initialize()
{
base.Initialize();
var protoManager = IoCManager.Resolve<IPrototypeManager>();
foreach (var faction in protoManager.EnumeratePrototypes<AiFactionPrototype>())
{
if (Enum.TryParse(faction.ID, out Faction @enum))
{
var parsedFaction = Faction.None;
foreach (var hostile in faction.Hostile)
{
if (Enum.TryParse(hostile, out Faction parsedHostile))
{
parsedFaction |= parsedHostile;
}
else
{
Logger.Error($"Unable to parse hostile faction {hostile} for {faction.ID}");
}
}
_hostileFactions[@enum] = parsedFaction;
}
else
{
Logger.Error($"Unable to parse AI faction {faction.ID}");
}
}
}
public Faction GetHostileFactions(Faction faction) => _hostileFactions.TryGetValue(faction, out var hostiles) ? hostiles : Faction.None;
public Faction GetFactions(IEntity entity) =>
entity.TryGetComponent(out AiFactionTagComponent? factionTags)
? factionTags.Factions
: Faction.None;
public IEnumerable<IEntity> GetNearbyHostiles(IEntity entity, float range)
{
var ourFaction = GetFactions(entity);
var hostile = GetHostileFactions(ourFaction);
if (ourFaction == Faction.None || hostile == Faction.None)
{
yield break;
}
foreach (var component in ComponentManager.EntityQuery<AiFactionTagComponent>(true))
{
if ((component.Factions & hostile) == 0)
continue;
if (component.Owner.Transform.MapID != entity.Transform.MapID)
continue;
if (!component.Owner.Transform.MapPosition.InRange(entity.Transform.MapPosition, range))
continue;
yield return component.Owner;
}
}
public void MakeFriendly(Faction source, Faction target)
{
if (!_hostileFactions.TryGetValue(source, out var hostileFactions))
{
return;
}
hostileFactions &= ~target;
_hostileFactions[source] = hostileFactions;
}
public void MakeHostile(Faction source, Faction target)
{
if (!_hostileFactions.TryGetValue(source, out var hostileFactions))
{
_hostileFactions[source] = target;
return;
}
hostileFactions |= target;
_hostileFactions[source] = hostileFactions;
}
}
[Flags]
public enum Faction
{
None = 0,
NanoTrasen = 1 << 0,
SimpleHostile = 1 << 1,
SimpleNeutral = 1 << 2,
Syndicate = 1 << 3,
Xeno = 1 << 4,
}
}

View File

@@ -1,143 +0,0 @@
#nullable enable
using System;
using System.Collections.Generic;
using Content.Server.AI.Utility.Actions;
using Content.Server.AI.Utility.AiLogic;
using Content.Server.GameObjects.Components.Movement;
using Content.Shared.GameObjects.Components.Mobs.State;
using Content.Shared;
using JetBrains.Annotations;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Utility;
namespace Content.Server.GameObjects.EntitySystems.AI
{
/// <summary>
/// Handles NPCs running every tick.
/// </summary>
[UsedImplicitly]
internal class AiSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
/// <summary>
/// To avoid iterating over dead AI continuously they can wake and sleep themselves when necessary.
/// </summary>
private readonly HashSet<AiControllerComponent> _awakeAi = new();
// To avoid modifying awakeAi while iterating over it.
private readonly List<SleepAiMessage> _queuedSleepMessages = new();
private readonly List<MobStateChangedMessage> _queuedMobStateMessages = new();
public bool IsAwake(AiControllerComponent npc) => _awakeAi.Contains(npc);
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SleepAiMessage>(HandleAiSleep);
SubscribeLocalEvent<MobStateChangedMessage>(MobStateChanged);
}
public override void Shutdown()
{
base.Shutdown();
UnsubscribeLocalEvent<SleepAiMessage>();
UnsubscribeLocalEvent<MobStateChangedMessage>();
}
/// <inheritdoc />
public override void Update(float frameTime)
{
var cvarMaxUpdates = _configurationManager.GetCVar(CCVars.AIMaxUpdates);
if (cvarMaxUpdates <= 0)
return;
foreach (var message in _queuedMobStateMessages)
{
// TODO: Need to generecise this but that will be part of a larger cleanup later anyway.
if (message.Entity.Deleted ||
!message.Entity.TryGetComponent(out UtilityAi? controller))
{
continue;
}
controller.MobStateChanged(message);
}
_queuedMobStateMessages.Clear();
foreach (var message in _queuedSleepMessages)
{
switch (message.Sleep)
{
case true:
if (_awakeAi.Count == cvarMaxUpdates && _awakeAi.Contains(message.Component))
{
Logger.Warning($"Under AI limit again: {_awakeAi.Count - 1} / {cvarMaxUpdates}");
}
_awakeAi.Remove(message.Component);
break;
case false:
_awakeAi.Add(message.Component);
if (_awakeAi.Count > cvarMaxUpdates)
{
Logger.Warning($"AI limit exceeded: {_awakeAi.Count} / {cvarMaxUpdates}");
}
break;
}
}
_queuedSleepMessages.Clear();
var toRemove = new List<AiControllerComponent>();
var maxUpdates = Math.Min(_awakeAi.Count, cvarMaxUpdates);
var count = 0;
foreach (var npc in _awakeAi)
{
if (npc.Paused) continue;
if (npc.Deleted)
{
toRemove.Add(npc);
continue;
}
if (count >= maxUpdates)
{
break;
}
npc.Update(frameTime);
count++;
}
foreach (var processor in toRemove)
{
_awakeAi.Remove(processor);
}
}
private void HandleAiSleep(SleepAiMessage message)
{
_queuedSleepMessages.Add(message);
}
private void MobStateChanged(MobStateChangedMessage message)
{
if (!message.Entity.HasComponent<AiControllerComponent>())
{
return;
}
_queuedMobStateMessages.Add(message);
}
}
}

View File

@@ -1,21 +0,0 @@
using System.Collections.Generic;
using Content.Server.AI.Utility.Actions;
using Content.Server.AI.WorldState;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems.AI.LoadBalancer
{
public class AiActionRequest
{
public EntityUid EntityUid { get; }
public Blackboard? Context { get; }
public IEnumerable<IAiUtility>? Actions { get; }
public AiActionRequest(EntityUid uid, Blackboard context, IEnumerable<IAiUtility> actions)
{
EntityUid = uid;
Context = context;
Actions = actions;
}
}
}

View File

@@ -1,138 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.AI.Utility.Actions;
using Content.Server.AI.Utility.ExpandableActions;
using Content.Server.AI.WorldState.States;
using Content.Server.AI.WorldState.States.Utility;
using Content.Server.GameObjects.Components.Movement;
using Content.Server.GameObjects.EntitySystems.JobQueues;
using Content.Shared.AI;
using Robust.Shared.Utility;
namespace Content.Server.GameObjects.EntitySystems.AI.LoadBalancer
{
public class AiActionRequestJob : Job<UtilityAction>
{
#if DEBUG
public static event Action<SharedAiDebug.UtilityAiDebugMessage>? FoundAction;
#endif
private readonly AiActionRequest _request;
public AiActionRequestJob(
double maxTime,
AiActionRequest request,
CancellationToken cancellationToken = default) : base(maxTime, cancellationToken)
{
_request = request;
}
protected override async Task<UtilityAction?> Process()
{
if (_request.Context == null)
{
return null;
}
var entity = _request.Context.GetState<SelfState>().GetValue();
if (entity == null || !entity.HasComponent<AiControllerComponent>())
{
return null;
}
if (_request.Actions == null || _request.Context == null)
{
return null;
}
var consideredTaskCount = 0;
// Actions are pre-sorted
var actions = new Stack<IAiUtility>(_request.Actions);
// So essentially we go through and once we have a valid score that score becomes the cutoff;
// once the bonus of new tasks is below the cutoff we can stop evaluating.
// Use last action as the basis for the cutoff
var cutoff = _request.Context.GetState<LastUtilityScoreState>().GetValue();
UtilityAction? foundAction = null;
// To see what I was trying to do watch these 2 videos about Infinite Axis Utility System (IAUS):
// Architecture Tricks: Managing Behaviors in Time, Space, and Depth
// Building a Better Centaur
// We'll want to cap the considered entities at some point, e.g. if 500 guns are in a stack cap it at 256 or whatever
while (actions.Count > 0)
{
if (consideredTaskCount > 0 && consideredTaskCount % 5 == 0)
{
await SuspendIfOutOfTime();
// If this happens then that means something changed when we resumed so ABORT
if (actions.Count == 0 || _request.Context == null)
{
return null;
}
}
var action = actions.Pop();
switch (action)
{
case ExpandableUtilityAction expandableUtilityAction:
if (!expandableUtilityAction.IsValid(_request.Context))
{
break;
}
foreach (var expanded in expandableUtilityAction.GetActions(_request.Context))
{
actions.Push(expanded);
}
break;
case UtilityAction utilityAction:
consideredTaskCount++;
var bonus = utilityAction.Bonus;
if (bonus < cutoff)
{
// We know none of the other actions can beat this as they're pre-sorted
actions.Clear();
break;
}
var score = utilityAction.GetScore(_request.Context, cutoff);
if (score > cutoff)
{
foundAction = utilityAction;
cutoff = score;
}
break;
default:
throw new ArgumentOutOfRangeException();
}
}
_request.Context.GetState<LastUtilityScoreState>().SetValue(cutoff);
#if DEBUG
if (foundAction != null)
{
var selfState = _request.Context.GetState<SelfState>().GetValue();
DebugTools.AssertNotNull(selfState);
FoundAction?.Invoke(new SharedAiDebug.UtilityAiDebugMessage(
selfState!.Uid,
DebugTime,
cutoff,
foundAction.GetType().Name,
consideredTaskCount));
}
#endif
_request.Context.ResetPlanning();
return foundAction;
}
}
}

View File

@@ -1,28 +0,0 @@
using System.Threading;
using Content.Server.GameObjects.EntitySystems.JobQueues.Queues;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems.AI.LoadBalancer
{
/// <summary>
/// This will queue up an AI's request for an action and give it one when possible
/// </summary>
public class AiActionSystem : EntitySystem
{
private readonly AiActionJobQueue _aiRequestQueue = new();
public AiActionRequestJob RequestAction(AiActionRequest request, CancellationTokenSource cancellationToken)
{
var job = new AiActionRequestJob(0.002, request, cancellationToken.Token);
// AI should already know if it shouldn't request again
_aiRequestQueue.EnqueueJob(job);
return job;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
_aiRequestQueue.Process();
}
}
}

View File

@@ -1,792 +0,0 @@
using System;
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Access;
using Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders;
using Content.Shared.AI;
using Content.Shared.GameTicking;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible
{
/// <summary>
/// Determines whether an AI has access to a specific pathfinding node.
/// </summary>
/// Long-term can be used to do hierarchical pathfinding
[UsedImplicitly]
public sealed class AiReachableSystem : EntitySystem, IResettingEntitySystem
{
/*
* The purpose of this is to provide a higher-level / hierarchical abstraction of the actual pathfinding graph
* The goal is so that we can more quickly discern if a specific node is reachable or not rather than
* Pathfinding the entire graph.
*
* There's a lot of different implementations of hierarchical or some variation of it: HPA*, PRA, HAA*, etc.
* (HPA* technically caches the edge nodes of each chunk), e.g. Rimworld, Factorio, etc.
* so we'll just write one with SS14's requirements in mind.
*
* There's probably a better data structure to use though you'd need to benchmark multiple ones to compare,
* at the very least on the memory side it could definitely be better.
*/
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
private PathfindingSystem _pathfindingSystem = default!;
/// <summary>
/// Queued region updates
/// </summary>
private readonly HashSet<PathfindingChunk> _queuedUpdates = new();
// Oh god the nesting. Shouldn't need to go beyond this
/// <summary>
/// The corresponding regions for each PathfindingChunk.
/// Regions are groups of nodes with the same profile (for pathfinding purposes)
/// i.e. same collision, not-space, same access, etc.
/// </summary>
private readonly Dictionary<GridId, Dictionary<PathfindingChunk, HashSet<PathfindingRegion>>> _regions =
new();
/// <summary>
/// Minimum time for the cached reachable regions to be stored
/// </summary>
private const float MinCacheTime = 1.0f;
// Cache what regions are accessible from this region. Cached per ReachableArgs
// so multiple entities in the same region with the same args should all be able to share their reachable lookup
// Also need to store when we cached it to know if it's stale if the chunks have updated
// TODO: There's probably a more memory-efficient way to cache this
// Then again, there's likely also a more memory-efficient way to implement regions.
// Also, didn't use a dictionary because there didn't seem to be a clean way to do the lookup
// Plus this way we can check if everything is equal except for vision so an entity with a lower vision radius can use an entity with a higher vision radius' cached result
private readonly Dictionary<ReachableArgs, Dictionary<PathfindingRegion, (TimeSpan CacheTime, HashSet<PathfindingRegion> Regions)>> _cachedAccessible =
new();
private readonly List<PathfindingRegion> _queuedCacheDeletions = new();
#if DEBUG
private HashSet<IPlayerSession> _subscribedSessions = new();
private int _runningCacheIdx = 0;
#endif
public override void Initialize()
{
_pathfindingSystem = Get<PathfindingSystem>();
SubscribeLocalEvent<PathfindingChunkUpdateMessage>(RecalculateNodeRegions);
#if DEBUG
SubscribeNetworkEvent<SharedAiDebug.SubscribeReachableMessage>(HandleSubscription);
SubscribeNetworkEvent<SharedAiDebug.UnsubscribeReachableMessage>(HandleUnsubscription);
#endif
_mapManager.OnGridRemoved += GridRemoved;
}
public override void Shutdown()
{
base.Shutdown();
_queuedUpdates.Clear();
_regions.Clear();
_cachedAccessible.Clear();
_queuedCacheDeletions.Clear();
_mapManager.OnGridRemoved -= GridRemoved;
UnsubscribeLocalEvent<PathfindingChunkUpdateMessage>();
UnsubscribeNetworkEvent<SharedAiDebug.SubscribeReachableMessage>();
UnsubscribeNetworkEvent<SharedAiDebug.UnsubscribeReachableMessage>();
}
private void GridRemoved(MapId mapId, GridId gridId)
{
_regions.Remove(gridId);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var chunk in _queuedUpdates)
{
GenerateRegions(chunk);
}
// TODO: Only send diffs instead
#if DEBUG
if (_subscribedSessions.Count > 0 && _queuedUpdates.Count > 0)
{
foreach (var (gridId, regs) in _regions)
{
if (regs.Count > 0)
{
SendRegionsDebugMessage(gridId);
}
}
}
#endif
_queuedUpdates.Clear();
foreach (var region in _queuedCacheDeletions)
{
ClearCache(region);
}
_queuedCacheDeletions.Clear();
}
#if DEBUG
private void HandleSubscription(SharedAiDebug.SubscribeReachableMessage message, EntitySessionEventArgs eventArgs)
{
_subscribedSessions.Add((IPlayerSession) eventArgs.SenderSession);
foreach (var (gridId, _) in _regions)
{
SendRegionsDebugMessage(gridId);
}
}
private void HandleUnsubscription(SharedAiDebug.UnsubscribeReachableMessage message, EntitySessionEventArgs eventArgs)
{
_subscribedSessions.Remove((IPlayerSession) eventArgs.SenderSession);
}
#endif
private void RecalculateNodeRegions(PathfindingChunkUpdateMessage message)
{
// TODO: Only need to do changed nodes ideally
// For now this is fine but it's a low-hanging fruit optimisation
_queuedUpdates.Add(message.Chunk);
}
/// <summary>
/// Can the entity reach the target?
/// </summary>
/// First it does a quick check to see if there are any traversable nodes in range.
/// Then it will go through the regions to try and see if there's a region connection between the target and itself
/// Will used a cached region if available
/// <param name="entity"></param>
/// <param name="target"></param>
/// <param name="range"></param>
/// <returns></returns>
public bool CanAccess(IEntity entity, IEntity target, float range = 0.0f)
{
var targetTile = _mapManager.GetGrid(target.Transform.GridID).GetTileRef(target.Transform.Coordinates);
var targetNode = _pathfindingSystem.GetNode(targetTile);
var collisionMask = 0;
if (entity.TryGetComponent(out IPhysBody? physics))
{
collisionMask = physics.CollisionMask;
}
var access = AccessReader.FindAccessTags(entity);
// We'll do a quick traversable check before going through regions
// If we can't access it we'll try to get a valid node in range (this is essentially an early-out)
if (!PathfindingHelpers.Traversable(collisionMask, access, targetNode))
{
// ReSharper disable once CompareOfFloatsByEqualityOperator
if (range == 0.0f)
{
return false;
}
var pathfindingArgs = new PathfindingArgs(entity.Uid, access, collisionMask, default, targetTile, range);
foreach (var node in BFSPathfinder.GetNodesInRange(pathfindingArgs, false))
{
targetNode = node;
}
}
return CanAccess(entity, targetNode);
}
public bool CanAccess(IEntity entity, PathfindingNode targetNode)
{
if (entity.Transform.GridID != targetNode.TileRef.GridIndex)
{
return false;
}
var entityTile = _mapManager.GetGrid(entity.Transform.GridID).GetTileRef(entity.Transform.Coordinates);
var entityNode = _pathfindingSystem.GetNode(entityTile);
var entityRegion = GetRegion(entityNode);
var targetRegion = GetRegion(targetNode);
// TODO: Regional pathfind from target to entity
// Early out
if (entityRegion == targetRegion)
{
return true;
}
// We'll go from target's position to us because most of the time it's probably in a locked room rather than vice versa
var reachableArgs = ReachableArgs.GetArgs(entity);
var reachableRegions = GetReachableRegions(reachableArgs, targetRegion);
return entityRegion != null && reachableRegions.Contains(entityRegion);
}
/// <summary>
/// Retrieve the reachable regions
/// </summary>
/// <param name="reachableArgs"></param>
/// <param name="region"></param>
/// <returns></returns>
public HashSet<PathfindingRegion> GetReachableRegions(ReachableArgs reachableArgs, PathfindingRegion? region)
{
// if we're on a node that's not tracked at all atm then region will be null
if (region == null)
{
return new HashSet<PathfindingRegion>();
}
var cachedArgs = GetCachedArgs(reachableArgs);
(TimeSpan CacheTime, HashSet<PathfindingRegion> Regions) cached;
if (!IsCacheValid(cachedArgs, region))
{
cached = GetVisionReachable(cachedArgs, region);
_cachedAccessible[cachedArgs][region] = cached;
#if DEBUG
SendRegionCacheMessage(region.ParentChunk.GridId, cached.Regions, false);
#endif
}
else
{
cached = _cachedAccessible[cachedArgs][region];
#if DEBUG
SendRegionCacheMessage(region.ParentChunk.GridId, cached.Regions, true);
#endif
}
return cached.Regions;
}
/// <summary>
/// Get any adequate cached args if possible, otherwise just use ours
/// </summary>
/// Essentially any args that have the same access AND >= our vision radius can be used
/// <param name="accessibleArgs"></param>
/// <returns></returns>
private ReachableArgs GetCachedArgs(ReachableArgs accessibleArgs)
{
ReachableArgs? foundArgs = null;
foreach (var (cachedAccessible, _) in _cachedAccessible)
{
if (Equals(cachedAccessible.Access, accessibleArgs.Access) &&
cachedAccessible.CollisionMask == accessibleArgs.CollisionMask &&
cachedAccessible.VisionRadius <= accessibleArgs.VisionRadius)
{
foundArgs = cachedAccessible;
break;
}
}
return foundArgs ?? accessibleArgs;
}
/// <summary>
/// Checks whether there's a valid cache for our accessibility args.
/// Most regular mobs can share their cached accessibility with each other
/// </summary>
/// Will also remove it from the cache if it is invalid
/// <param name="accessibleArgs"></param>
/// <param name="region"></param>
/// <returns></returns>
private bool IsCacheValid(ReachableArgs accessibleArgs, PathfindingRegion region)
{
if (!_cachedAccessible.TryGetValue(accessibleArgs, out var cachedArgs))
{
_cachedAccessible.Add(accessibleArgs, new Dictionary<PathfindingRegion, (TimeSpan, HashSet<PathfindingRegion>)>());
return false;
}
if (!cachedArgs.TryGetValue(region, out var regionCache))
{
return false;
}
// Just so we don't invalidate the cache every tick we'll store it for a minimum amount of time
var currentTime = _gameTiming.CurTime;
if ((currentTime - regionCache.CacheTime).TotalSeconds < MinCacheTime)
{
return true;
}
var checkedAccess = new HashSet<PathfindingRegion>();
// Check if cache is stale
foreach (var accessibleRegion in regionCache.Regions)
{
if (checkedAccess.Contains(accessibleRegion)) continue;
// Any applicable chunk has been invalidated OR one of our neighbors has been invalidated (i.e. new connections)
// TODO: Could look at storing the TimeSpan directly on the region so our neighbor can tell us straight-up
if (accessibleRegion.ParentChunk.LastUpdate > regionCache.CacheTime)
{
// Remove the stale cache, to be updated later
_cachedAccessible[accessibleArgs].Remove(region);
return false;
}
foreach (var neighbor in accessibleRegion.Neighbors)
{
if (checkedAccess.Contains(neighbor)) continue;
if (neighbor.ParentChunk.LastUpdate > regionCache.CacheTime)
{
_cachedAccessible[accessibleArgs].Remove(region);
return false;
}
checkedAccess.Add(neighbor);
}
checkedAccess.Add(accessibleRegion);
}
return true;
}
/// <summary>
/// Caches the entity's nearby accessible regions in vision radius
/// </summary>
/// Longer-term TODO: Hierarchical pathfinding in which case this function would probably get bulldozed, BRRRTT
/// <param name="reachableArgs"></param>
/// <param name="entityRegion"></param>
private (TimeSpan, HashSet<PathfindingRegion>) GetVisionReachable(ReachableArgs reachableArgs, PathfindingRegion entityRegion)
{
var openSet = new Queue<PathfindingRegion>();
openSet.Enqueue(entityRegion);
var closedSet = new HashSet<PathfindingRegion>();
var accessible = new HashSet<PathfindingRegion> {entityRegion};
while (openSet.Count > 0)
{
var region = openSet.Dequeue();
closedSet.Add(region);
foreach (var neighbor in region.Neighbors)
{
if (closedSet.Contains(neighbor))
{
continue;
}
// Distance is an approximation here so we'll be generous with it
// TODO: Could do better; the fewer nodes the better it is.
if (!neighbor.RegionTraversable(reachableArgs) ||
neighbor.Distance(entityRegion) > reachableArgs.VisionRadius + 1)
{
closedSet.Add(neighbor);
continue;
}
openSet.Enqueue(neighbor);
accessible.Add(neighbor);
}
}
return (_gameTiming.CurTime, accessible);
}
/// <summary>
/// Grab the related cardinal nodes and if they're in different regions then add to our edge and their edge
/// </summary>
/// Implicitly they would've already been merged if possible
/// <param name="region"></param>
/// <param name="node"></param>
private void UpdateRegionEdge(PathfindingRegion region, PathfindingNode node)
{
DebugTools.Assert(region.Nodes.Contains(node));
// Originally I tried just doing bottom and left but that doesn't work as the chunk update order is not guaranteed
var checkDirections = new[] {Direction.East, Direction.South, Direction.West, Direction.North};
foreach (var direction in checkDirections)
{
var directionNode = node.GetNeighbor(direction);
if (directionNode == null) continue;
var directionRegion = GetRegion(directionNode);
if (directionRegion == null || directionRegion == region) continue;
region.Neighbors.Add(directionRegion);
directionRegion.Neighbors.Add(region);
}
}
/// <summary>
/// Get the current region for this entity
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
public PathfindingRegion? GetRegion(IEntity entity)
{
if (!entity.Transform.GridID.IsValid())
{
return null;
}
var entityTile = _mapManager.GetGrid(entity.Transform.GridID).GetTileRef(entity.Transform.Coordinates);
var entityNode = _pathfindingSystem.GetNode(entityTile);
return GetRegion(entityNode);
}
/// <summary>
/// Get the current region for this node
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
public PathfindingRegion? GetRegion(PathfindingNode node)
{
// Not sure on the best way to optimise this
// On the one hand, just storing each node's region is faster buuutttt muh memory
// On the other hand, you might need O(n) lookups on regions for each chunk, though it's probably not too bad with smaller chunk sizes?
// Someone smarter than me will know better
var parentChunk = node.ParentChunk;
// No guarantee the node even has a region yet (if we're doing neighbor lookups)
if (!_regions[parentChunk.GridId].TryGetValue(parentChunk, out var regions))
{
return null;
}
foreach (var region in regions)
{
if (region.Nodes.Contains(node))
{
return region;
}
}
// Longer term this will probably be guaranteed a region but for now space etc. are no region
return null;
}
/// <summary>
/// Add this node to the relevant region.
/// </summary>
/// <param name="node"></param>
/// <param name="existingRegions">The cached region for each node</param>
/// <param name="chunkRegions">The existing regions in the chunk</param>
/// <param name="x">This is already calculated in advance so may as well re-use it</param>
/// <param name="y">This is already calculated in advance so may as well re-use it</param>
/// <returns></returns>
private PathfindingRegion? CalculateNode(
PathfindingNode node,
Dictionary<PathfindingNode, PathfindingRegion> existingRegions,
HashSet<PathfindingRegion> chunkRegions,
int x, int y)
{
DebugTools.Assert(_regions.ContainsKey(node.ParentChunk.GridId));
DebugTools.Assert(_regions[node.ParentChunk.GridId].ContainsKey(node.ParentChunk));
// TODO For now we don't have these regions but longer-term yeah sure
if (node.BlockedCollisionMask != 0x0 || node.TileRef.Tile.IsEmpty)
{
return null;
}
var parentChunk = node.ParentChunk;
// Doors will be their own separate region
// We won't store them in existingRegions so they don't show up and can't be connected to (at least for now)
if (node.AccessReaders.Count > 0)
{
var region = new PathfindingRegion(node, new HashSet<PathfindingNode>(1) {node}, true);
_regions[parentChunk.GridId][parentChunk].Add(region);
UpdateRegionEdge(region, node);
return region;
}
// Relative x and y of the chunk
// If one of our bottom / left neighbors are in a region try to join them
// Otherwise, make our own region.
var leftNeighbor = x > 0 ? parentChunk.Nodes[x - 1, y] : null;
var bottomNeighbor = y > 0 ? parentChunk.Nodes[x, y - 1] : null;
PathfindingRegion? leftRegion;
PathfindingRegion? bottomRegion;
// We'll check if our left or down neighbors are already in a region and join them
// Is left node valid to connect to
if (leftNeighbor != null &&
existingRegions.TryGetValue(leftNeighbor, out leftRegion) &&
!leftRegion.IsDoor)
{
// We'll try and connect the left node's region to the bottom region if they're separate (yay merge)
if (bottomNeighbor != null &&
existingRegions.TryGetValue(bottomNeighbor, out bottomRegion) &&
bottomRegion != leftRegion &&
!bottomRegion.IsDoor)
{
bottomRegion.Add(node);
existingRegions.Add(node, bottomRegion);
MergeInto(leftRegion, bottomRegion, existingRegions);
// Cleanup leftRegion
// MergeInto will remove it from the overall region chunk cache while we need to remove it from
// our short-term ones (chunkRegions and existingRegions)
chunkRegions.Remove(leftRegion);
foreach (var leftNode in leftRegion.Nodes)
{
existingRegions[leftNode] = bottomRegion;
}
return bottomRegion;
}
leftRegion.Add(node);
existingRegions.Add(node, leftRegion);
UpdateRegionEdge(leftRegion, node);
return leftRegion;
}
//Is bottom node valid to connect to
if (bottomNeighbor != null &&
existingRegions.TryGetValue(bottomNeighbor, out bottomRegion) &&
!bottomRegion.IsDoor)
{
bottomRegion.Add(node);
existingRegions.Add(node, bottomRegion);
UpdateRegionEdge(bottomRegion, node);
return bottomRegion;
}
// If we can't join an existing region then we'll make our own
var newRegion = new PathfindingRegion(node, new HashSet<PathfindingNode> {node}, node.AccessReaders.Count > 0);
_regions[parentChunk.GridId][parentChunk].Add(newRegion);
existingRegions.Add(node, newRegion);
UpdateRegionEdge(newRegion, node);
return newRegion;
}
/// <summary>
/// Combines the two regions into one bigger region
/// </summary>
/// <param name="source"></param>
/// <param name="target"></param>
private void MergeInto(PathfindingRegion source, PathfindingRegion target, Dictionary<PathfindingNode, PathfindingRegion>? existingRegions = null)
{
DebugTools.AssertNotNull(source);
DebugTools.AssertNotNull(target);
DebugTools.Assert(source != target);
foreach (var node in source.Nodes)
{
target.Add(node);
}
if (existingRegions != null)
{
foreach (var node in source.Nodes)
{
existingRegions[node] = target;
}
}
source.Shutdown();
// This doesn't check the cachedaccessible to see if it's reachable but maybe it should?
// Although currently merge gets spammed so maybe when some other stuff is improved
// MergeInto is also only called by GenerateRegions currently so nothing should hold onto the original region
_regions[source.ParentChunk.GridId][source.ParentChunk].Remove(source);
foreach (var node in target.Nodes)
{
UpdateRegionEdge(target, node);
}
}
/// <summary>
/// Remove the cached accessibility lookup for this region
/// </summary>
/// <param name="region"></param>
private void ClearCache(PathfindingRegion region)
{
DebugTools.Assert(region.Deleted);
// Need to forcibly clear cache for ourself and anything that includes us
foreach (var (_, cachedRegions) in _cachedAccessible)
{
if (cachedRegions.ContainsKey(region))
{
cachedRegions.Remove(region);
}
// Seemed like the safest way to remove this
// We could just have GetVisionAccessible remove us if it can tell we're deleted but that
// seems like it could be unreliable
var regionsToClear = new List<PathfindingRegion>();
foreach (var (otherRegion, cache) in cachedRegions)
{
if (cache.Regions.Contains(region))
{
regionsToClear.Add(otherRegion);
}
}
foreach (var otherRegion in regionsToClear)
{
cachedRegions.Remove(otherRegion);
}
}
#if DEBUG
if (_regions.TryGetValue(region.ParentChunk.GridId, out var chunks) &&
chunks.TryGetValue(region.ParentChunk, out var regions))
{
DebugTools.Assert(!regions.Contains(region));
}
#endif
}
/// <summary>
/// Generate all of the regions within a chunk
/// </summary>
/// These can't across over into another chunk and doors are their own region
/// <param name="chunk"></param>
private void GenerateRegions(PathfindingChunk chunk)
{
// Grid deleted while update queued, or invalid grid.
if (!_mapManager.TryGetGrid(chunk.GridId, out _))
{
return;
}
if (!_regions.ContainsKey(chunk.GridId))
{
_regions.Add(chunk.GridId, new Dictionary<PathfindingChunk, HashSet<PathfindingRegion>>());
}
if (_regions[chunk.GridId].TryGetValue(chunk, out var regions))
{
foreach (var region in regions)
{
_queuedCacheDeletions.Add(region);
region.Shutdown();
}
_regions[chunk.GridId].Remove(chunk);
}
// Temporarily store the corresponding region for each node
// Makes merging regions or adding nodes to existing regions neater.
var nodeRegions = new Dictionary<PathfindingNode, PathfindingRegion>();
var chunkRegions = new HashSet<PathfindingRegion>();
_regions[chunk.GridId].Add(chunk, chunkRegions);
for (var y = 0; y < PathfindingChunk.ChunkSize; y++)
{
for (var x = 0; x < PathfindingChunk.ChunkSize; x++)
{
var node = chunk.Nodes[x, y];
var region = CalculateNode(node, nodeRegions, chunkRegions, x, y);
// Currently we won't store a separate region for each mask / space / whatever because muh effort
// Long-term you'll want to account for it probably
if (region == null)
{
continue;
}
chunkRegions.Add(region);
}
}
#if DEBUG
foreach (var region in chunkRegions)
{
DebugTools.Assert(!region.Deleted);
}
DebugTools.Assert(chunkRegions.Count < Math.Pow(PathfindingChunk.ChunkSize, 2));
SendRegionsDebugMessage(chunk.GridId);
#endif
}
public void Reset()
{
_queuedUpdates.Clear();
_regions.Clear();
_cachedAccessible.Clear();
_queuedCacheDeletions.Clear();
}
#if DEBUG
private void SendRegionsDebugMessage(GridId gridId)
{
if (_subscribedSessions.Count == 0) return;
var grid = _mapManager.GetGrid(gridId);
// Chunk / Regions / Nodes
var debugResult = new Dictionary<int, Dictionary<int, List<Vector2>>>();
var chunkIdx = 0;
var regionIdx = 0;
if (!_regions.TryGetValue(gridId, out var dict))
{
return;
}
foreach (var (_, regions) in dict)
{
var debugRegions = new Dictionary<int, List<Vector2>>();
debugResult.Add(chunkIdx, debugRegions);
foreach (var region in regions)
{
var debugRegionNodes = new List<Vector2>(region.Nodes.Count);
debugResult[chunkIdx].Add(regionIdx, debugRegionNodes);
foreach (var node in region.Nodes)
{
var nodeVector = grid.GridTileToLocal(node.TileRef.GridIndices).ToMapPos(EntityManager);
debugRegionNodes.Add(nodeVector);
}
regionIdx++;
}
chunkIdx++;
}
foreach (var session in _subscribedSessions)
{
RaiseNetworkEvent(new SharedAiDebug.ReachableChunkRegionsDebugMessage(gridId, debugResult), session.ConnectedClient);
}
}
/// <summary>
/// Sent whenever the reachable cache for a particular mob is built or retrieved
/// </summary>
/// <param name="gridId"></param>
/// <param name="regions"></param>
/// <param name="cached"></param>
private void SendRegionCacheMessage(GridId gridId, IEnumerable<PathfindingRegion> regions, bool cached)
{
if (_subscribedSessions.Count == 0) return;
var grid = _mapManager.GetGrid(gridId);
var debugResult = new Dictionary<int, List<Vector2>>();
foreach (var region in regions)
{
debugResult.Add(_runningCacheIdx, new List<Vector2>());
foreach (var node in region.Nodes)
{
var nodeVector = grid.GridTileToLocal(node.TileRef.GridIndices).ToMapPos(EntityManager);
debugResult[_runningCacheIdx].Add(nodeVector);
}
_runningCacheIdx++;
}
foreach (var session in _subscribedSessions)
{
RaiseNetworkEvent(new SharedAiDebug.ReachableCacheDebugMessage(gridId, debugResult, cached), session.ConnectedClient);
}
}
#endif
}
}

View File

@@ -1,69 +0,0 @@
using System.Collections.Generic;
using Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible
{
/// <summary>
/// The simplest pathfinder
/// </summary>
public sealed class BFSPathfinder
{
/// <summary>
/// Gets all of the tiles in range that can we access
/// </summary>
/// If you want Dikstra then add distances.
/// Doesn't use the JobQueue as it will generally be encapsulated by other jobs
/// <param name="pathfindingArgs"></param>
/// <param name="range"></param>
/// <param name="fromStart">Whether we traverse from the starting tile or the end tile</param>
/// <returns></returns>
public static IEnumerable<PathfindingNode> GetNodesInRange(PathfindingArgs pathfindingArgs, bool fromStart = true)
{
var pathfindingSystem = EntitySystem.Get<PathfindingSystem>();
// Don't need a priority queue given not looking for shortest path
var openTiles = new Queue<PathfindingNode>();
var closedTiles = new HashSet<TileRef>();
PathfindingNode startNode;
if (fromStart)
{
startNode = pathfindingSystem.GetNode(pathfindingArgs.Start);
}
else
{
startNode = pathfindingSystem.GetNode(pathfindingArgs.End);
}
PathfindingNode currentNode;
openTiles.Enqueue(startNode);
while (openTiles.Count > 0)
{
currentNode = openTiles.Dequeue();
foreach (var neighbor in currentNode.GetNeighbors())
{
// No distances stored so can just check closed tiles here
if (closedTiles.Contains(neighbor.TileRef)) continue;
closedTiles.Add(currentNode.TileRef);
// So currently tileCost gets the octile distance between the 2 so we'll also use that for our range check
var tileCost = PathfindingHelpers.GetTileCost(pathfindingArgs, startNode, neighbor);
var direction = PathfindingHelpers.RelativeDirection(neighbor, currentNode);
if (tileCost == null ||
tileCost > pathfindingArgs.Proximity ||
!PathfindingHelpers.DirectionTraversable(pathfindingArgs.CollisionMask, pathfindingArgs.Access, currentNode, direction))
{
continue;
}
openTiles.Enqueue(neighbor);
yield return neighbor;
}
}
}
}
}

View File

@@ -1,141 +0,0 @@
using System;
using System.Collections.Generic;
namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible
{
/// <summary>
/// A group of homogenous PathfindingNodes inside a single chunk
/// </summary>
/// Makes the graph smaller and quicker to traverse
public class PathfindingRegion : IEquatable<PathfindingRegion>
{
/// <summary>
/// Bottom-left reference node of the region
/// </summary>
public PathfindingNode OriginNode { get; }
// The shape may be anything within the bounds of a chunk, this is just a quick way to do a bounds-check
/// <summary>
/// Maximum width of the nodes
/// </summary>
public int Height { get; private set; } = 1;
/// <summary>
/// Maximum width of the nodes
/// </summary>
public int Width { get; private set; } = 1;
public PathfindingChunk ParentChunk => OriginNode.ParentChunk;
public HashSet<PathfindingRegion> Neighbors { get; } = new();
public bool IsDoor { get; }
public HashSet<PathfindingNode> Nodes => _nodes;
private readonly HashSet<PathfindingNode> _nodes;
public bool Deleted { get; private set; }
public PathfindingRegion(PathfindingNode originNode, HashSet<PathfindingNode> nodes, bool isDoor = false)
{
OriginNode = originNode;
_nodes = nodes;
IsDoor = isDoor;
}
public void Shutdown()
{
// Tell our neighbors we no longer exist ;-/
var neighbors = new List<PathfindingRegion>(Neighbors);
for (var i = 0; i < neighbors.Count; i++)
{
var neighbor = neighbors[i];
neighbor.Neighbors.Remove(this);
}
_nodes.Clear();
Neighbors.Clear();
Deleted = true;
}
/// <summary>
/// Roughly how far away another region is by nearest node
/// </summary>
/// <param name="otherRegion"></param>
/// <returns></returns>
public float Distance(PathfindingRegion otherRegion)
{
// JANK
var xDistance = otherRegion.OriginNode.TileRef.X - OriginNode.TileRef.X;
var yDistance = otherRegion.OriginNode.TileRef.Y - OriginNode.TileRef.Y;
if (xDistance > 0)
{
xDistance -= Width;
}
else if (xDistance < 0)
{
xDistance = Math.Abs(xDistance + otherRegion.Width);
}
if (yDistance > 0)
{
yDistance -= Height;
}
else if (yDistance < 0)
{
yDistance = Math.Abs(yDistance + otherRegion.Height);
}
return PathfindingHelpers.OctileDistance(xDistance, yDistance);
}
/// <summary>
/// Can the given args can traverse this region?
/// </summary>
/// <param name="reachableArgs"></param>
/// <returns></returns>
public bool RegionTraversable(ReachableArgs reachableArgs)
{
// The assumption is that all nodes in a region have the same pathfinding traits
// As such we can just use the origin node for checking.
return PathfindingHelpers.Traversable(reachableArgs.CollisionMask, reachableArgs.Access,
OriginNode);
}
public void Add(PathfindingNode node)
{
var xWidth = Math.Abs(node.TileRef.X - OriginNode.TileRef.X);
var yHeight = Math.Abs(node.TileRef.Y - OriginNode.TileRef.Y);
if (xWidth > Width)
{
Width = xWidth;
}
if (yHeight > Height)
{
Height = yHeight;
}
_nodes.Add(node);
}
// HashSet wasn't working correctly so uhh we got this.
public bool Equals(PathfindingRegion? other)
{
if (other == null) return false;
if (ReferenceEquals(this, other)) return true;
if (_nodes.Count != other.Nodes.Count) return false;
if (Deleted != other.Deleted) return false;
if (OriginNode != other.OriginNode) return false;
return true;
}
public override int GetHashCode()
{
return OriginNode.GetHashCode();
}
}
}

View File

@@ -1,42 +0,0 @@
#nullable enable
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Access;
using Content.Server.GameObjects.Components.Movement;
using Robust.Shared.GameObjects;
using Robust.Shared.Physics;
namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible
{
public sealed class ReachableArgs
{
public float VisionRadius { get; set; }
public ICollection<string> Access { get; }
public int CollisionMask { get; }
public ReachableArgs(float visionRadius, ICollection<string> access, int collisionMask)
{
VisionRadius = visionRadius;
Access = access;
CollisionMask = collisionMask;
}
/// <summary>
/// Get appropriate args for a particular entity
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
public static ReachableArgs GetArgs(IEntity entity)
{
var collisionMask = 0;
if (entity.TryGetComponent(out IPhysBody? physics))
{
collisionMask = physics.CollisionMask;
}
var access = AccessReader.FindAccessTags(entity);
var visionRadius = entity.GetComponent<AiControllerComponent>().VisionRadius;
return new ReachableArgs(visionRadius, access, collisionMask);
}
}
}

View File

@@ -1,163 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.GameObjects.EntitySystems.JobQueues;
using Content.Shared.AI;
using Robust.Shared.Map;
using Robust.Shared.Utility;
namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders
{
public class AStarPathfindingJob : Job<Queue<TileRef>>
{
#if DEBUG
public static event Action<SharedAiDebug.AStarRouteDebug>? DebugRoute;
#endif
private readonly PathfindingNode? _startNode;
private PathfindingNode? _endNode;
private readonly PathfindingArgs _pathfindingArgs;
public AStarPathfindingJob(
double maxTime,
PathfindingNode startNode,
PathfindingNode endNode,
PathfindingArgs pathfindingArgs,
CancellationToken cancellationToken) : base(maxTime, cancellationToken)
{
_startNode = startNode;
_endNode = endNode;
_pathfindingArgs = pathfindingArgs;
}
protected override async Task<Queue<TileRef>?> Process()
{
if (_startNode == null ||
_endNode == null ||
Status == JobStatus.Finished)
{
return null;
}
// If we couldn't get a nearby node that's good enough
if (!PathfindingHelpers.TryEndNode(ref _endNode, _pathfindingArgs))
{
return null;
}
var frontier = new PriorityQueue<ValueTuple<float, PathfindingNode>>(new PathfindingComparer());
var costSoFar = new Dictionary<PathfindingNode, float>();
var cameFrom = new Dictionary<PathfindingNode, PathfindingNode>();
PathfindingNode? currentNode = null;
frontier.Add((0.0f, _startNode));
costSoFar[_startNode] = 0.0f;
var routeFound = false;
var count = 0;
while (frontier.Count > 0)
{
// Handle whether we need to pause if we've taken too long
count++;
if (count % 20 == 0 && count > 0)
{
await SuspendIfOutOfTime();
if (_startNode == null || _endNode == null)
{
return null;
}
}
// Actual pathfinding here
(_, currentNode) = frontier.Take();
if (currentNode.Equals(_endNode))
{
routeFound = true;
break;
}
foreach (var nextNode in currentNode.GetNeighbors())
{
// If tile is untraversable it'll be null
var tileCost = PathfindingHelpers.GetTileCost(_pathfindingArgs, currentNode, nextNode);
if (tileCost == null)
{
continue;
}
// So if we're going NE then that means either N or E needs to be free to actually get there
var direction = PathfindingHelpers.RelativeDirection(nextNode, currentNode);
if (!PathfindingHelpers.DirectionTraversable(_pathfindingArgs.CollisionMask, _pathfindingArgs.Access, currentNode, direction))
{
continue;
}
// f = g + h
// gScore is distance to the start node
// hScore is distance to the end node
var gScore = costSoFar[currentNode] + tileCost.Value;
if (costSoFar.TryGetValue(nextNode, out var nextValue) && gScore >= nextValue)
{
continue;
}
cameFrom[nextNode] = currentNode;
costSoFar[nextNode] = gScore;
// pFactor is tie-breaker where the fscore is otherwise equal.
// See http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html#breaking-ties
// There's other ways to do it but future consideration
// The closer the fScore is to the actual distance then the better the pathfinder will be
// (i.e. somewhere between 1 and infinite)
// Can use hierarchical pathfinder or whatever to improve the heuristic but this is fine for now.
var fScore = gScore + PathfindingHelpers.OctileDistance(_endNode, nextNode) * (1.0f + 1.0f / 1000.0f);
frontier.Add((fScore, nextNode));
}
}
if (!routeFound)
{
return null;
}
DebugTools.AssertNotNull(currentNode);
var route = PathfindingHelpers.ReconstructPath(cameFrom, currentNode!);
if (route.Count == 1)
{
return null;
}
#if DEBUG
// Need to get data into an easier format to send to the relevant clients
if (DebugRoute != null && route.Count > 0)
{
var debugCameFrom = new Dictionary<TileRef, TileRef>(cameFrom.Count);
var debugGScores = new Dictionary<TileRef, float>(costSoFar.Count);
foreach (var (node, parent) in cameFrom)
{
debugCameFrom.Add(node.TileRef, parent.TileRef);
}
foreach (var (node, score) in costSoFar)
{
debugGScores.Add(node.TileRef, score);
}
var debugRoute = new SharedAiDebug.AStarRouteDebug(
_pathfindingArgs.Uid,
route,
debugCameFrom,
debugGScores,
DebugTime);
DebugRoute.Invoke(debugRoute);
}
#endif
return route;
}
}
}

View File

@@ -1,516 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.GameObjects.EntitySystems.JobQueues;
using Content.Shared.AI;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders
{
public class JpsPathfindingJob : Job<Queue<TileRef>>
{
// Some of this is probably fugly due to other structural changes in pathfinding so it could do with optimisation
// Realistically it's probably not getting used given it doesn't support tile costs which can be very useful
#if DEBUG
public static event Action<SharedAiDebug.JpsRouteDebug>? DebugRoute;
#endif
private readonly PathfindingNode? _startNode;
private PathfindingNode? _endNode;
private readonly PathfindingArgs _pathfindingArgs;
public JpsPathfindingJob(double maxTime,
PathfindingNode startNode,
PathfindingNode endNode,
PathfindingArgs pathfindingArgs,
CancellationToken cancellationToken) : base(maxTime, cancellationToken)
{
_startNode = startNode;
_endNode = endNode;
_pathfindingArgs = pathfindingArgs;
}
protected override async Task<Queue<TileRef>?> Process()
{
// VERY similar to A*; main difference is with the neighbor tiles you look for jump nodes instead
if (_startNode == null ||
_endNode == null)
{
return null;
}
// If we couldn't get a nearby node that's good enough
if (!PathfindingHelpers.TryEndNode(ref _endNode, _pathfindingArgs))
{
return null;
}
var openTiles = new PriorityQueue<ValueTuple<float, PathfindingNode>>(new PathfindingComparer());
var gScores = new Dictionary<PathfindingNode, float>();
var cameFrom = new Dictionary<PathfindingNode, PathfindingNode>();
var closedTiles = new HashSet<PathfindingNode>();
#if DEBUG
var jumpNodes = new HashSet<PathfindingNode>();
#endif
PathfindingNode? currentNode = null;
openTiles.Add((0, _startNode));
gScores[_startNode] = 0.0f;
var routeFound = false;
var count = 0;
while (openTiles.Count > 0)
{
count++;
// JPS probably getting a lot fewer nodes than A* is
if (count % 5 == 0 && count > 0)
{
await SuspendIfOutOfTime();
}
(_, currentNode) = openTiles.Take();
if (currentNode.Equals(_endNode))
{
routeFound = true;
break;
}
foreach (var node in currentNode.GetNeighbors())
{
var direction = PathfindingHelpers.RelativeDirection(node, currentNode);
var jumpNode = GetJumpPoint(currentNode, direction, _endNode);
if (jumpNode != null && !closedTiles.Contains(jumpNode))
{
closedTiles.Add(jumpNode);
#if DEBUG
jumpNodes.Add(jumpNode);
#endif
// GetJumpPoint should already check if we can traverse to the node
var tileCost = PathfindingHelpers.GetTileCost(_pathfindingArgs, currentNode, jumpNode);
if (tileCost == null)
{
throw new InvalidOperationException();
}
var gScore = gScores[currentNode] + tileCost.Value;
if (gScores.TryGetValue(jumpNode, out var nextValue) && gScore >= nextValue)
{
continue;
}
cameFrom[jumpNode] = currentNode;
gScores[jumpNode] = gScore;
// pFactor is tie-breaker where the fscore is otherwise equal.
// See http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html#breaking-ties
// There's other ways to do it but future consideration
var fScore = gScores[jumpNode] + PathfindingHelpers.OctileDistance(_endNode, jumpNode) * (1.0f + 1.0f / 1000.0f);
openTiles.Add((fScore, jumpNode));
}
}
}
if (!routeFound)
{
return null;
}
DebugTools.AssertNotNull(currentNode);
var route = PathfindingHelpers.ReconstructJumpPath(cameFrom, currentNode!);
if (route.Count == 1)
{
return null;
}
#if DEBUG
// Need to get data into an easier format to send to the relevant clients
if (DebugRoute != null && route.Count > 0)
{
var debugJumpNodes = new HashSet<TileRef>(jumpNodes.Count);
foreach (var node in jumpNodes)
{
debugJumpNodes.Add(node.TileRef);
}
var debugRoute = new SharedAiDebug.JpsRouteDebug(
_pathfindingArgs.Uid,
route,
debugJumpNodes,
DebugTime);
DebugRoute.Invoke(debugRoute);
}
#endif
return route;
}
private PathfindingNode? GetJumpPoint(PathfindingNode currentNode, Direction direction, PathfindingNode endNode)
{
var count = 0;
while (count < 1000)
{
count++;
PathfindingNode? nextNode = null;
foreach (var node in currentNode.GetNeighbors())
{
if (PathfindingHelpers.RelativeDirection(node, currentNode) == direction)
{
nextNode = node;
break;
}
}
// We'll do opposite DirectionTraversable just because of how the method's setup
// Nodes should be 2-way anyway.
if (nextNode == null ||
PathfindingHelpers.GetTileCost(_pathfindingArgs, currentNode, nextNode) == null)
{
return null;
}
if (nextNode == endNode)
{
return endNode;
}
// Horizontal and vertical are treated the same i.e.
// They only check in their specific direction
// (So Going North means you check NorthWest and NorthEast to see if we're a jump point)
// Diagonals also check the cardinal directions at the same time at the same time
// See https://harablog.wordpress.com/2011/09/07/jump-point-search/ for original description
switch (direction)
{
case Direction.East:
if (IsCardinalJumpPoint(direction, nextNode))
{
return nextNode;
}
break;
case Direction.NorthEast:
if (IsDiagonalJumpPoint(direction, nextNode))
{
return nextNode;
}
if (GetJumpPoint(nextNode, Direction.North, endNode) != null || GetJumpPoint(nextNode, Direction.East, endNode) != null)
{
return nextNode;
}
break;
case Direction.North:
if (IsCardinalJumpPoint(direction, nextNode))
{
return nextNode;
}
break;
case Direction.NorthWest:
if (IsDiagonalJumpPoint(direction, nextNode))
{
return nextNode;
}
if (GetJumpPoint(nextNode, Direction.North, endNode) != null || GetJumpPoint(nextNode, Direction.West, endNode) != null)
{
return nextNode;
}
break;
case Direction.West:
if (IsCardinalJumpPoint(direction, nextNode))
{
return nextNode;
}
break;
case Direction.SouthWest:
if (IsDiagonalJumpPoint(direction, nextNode))
{
return nextNode;
}
if (GetJumpPoint(nextNode, Direction.South, endNode) != null || GetJumpPoint(nextNode, Direction.West, endNode) != null)
{
return nextNode;
}
break;
case Direction.South:
if (IsCardinalJumpPoint(direction, nextNode))
{
return nextNode;
}
break;
case Direction.SouthEast:
if (IsDiagonalJumpPoint(direction, nextNode))
{
return nextNode;
}
if (GetJumpPoint(nextNode, Direction.South, endNode) != null || GetJumpPoint(nextNode, Direction.East, endNode) != null)
{
return nextNode;
}
break;
default:
throw new ArgumentOutOfRangeException(nameof(direction), direction, null);
}
currentNode = nextNode;
}
Logger.WarningS("pathfinding", "Recursion found in JPS pathfinder");
return null;
}
private bool IsDiagonalJumpPoint(Direction direction, PathfindingNode currentNode)
{
// If we're going diagonally need to check all cardinals.
// I tried just casting direction ints and offsets to make it smaller but brain no worky.
// From NorthEast we check (Closed / Open) S - SE, W - NW
PathfindingNode? openNeighborOne = null;
PathfindingNode? closedNeighborOne = null;
PathfindingNode? openNeighborTwo = null;
PathfindingNode? closedNeighborTwo = null;
switch (direction)
{
case Direction.NorthEast:
foreach (var neighbor in currentNode.GetNeighbors())
{
var neighborDirection = PathfindingHelpers.RelativeDirection(neighbor, currentNode);
switch (neighborDirection)
{
case Direction.SouthEast:
openNeighborOne = neighbor;
break;
case Direction.South:
closedNeighborOne = neighbor;
break;
case Direction.NorthWest:
openNeighborTwo = neighbor;
break;
case Direction.West:
closedNeighborTwo = neighbor;
break;
}
}
break;
case Direction.SouthEast:
foreach (var neighbor in currentNode.GetNeighbors())
{
var neighborDirection = PathfindingHelpers.RelativeDirection(neighbor, currentNode);
switch (neighborDirection)
{
case Direction.NorthEast:
openNeighborOne = neighbor;
break;
case Direction.North:
closedNeighborOne = neighbor;
break;
case Direction.SouthWest:
openNeighborTwo = neighbor;
break;
case Direction.West:
closedNeighborTwo = neighbor;
break;
}
}
break;
case Direction.SouthWest:
foreach (var neighbor in currentNode.GetNeighbors())
{
var neighborDirection = PathfindingHelpers.RelativeDirection(neighbor, currentNode);
switch (neighborDirection)
{
case Direction.NorthWest:
openNeighborOne = neighbor;
break;
case Direction.North:
closedNeighborOne = neighbor;
break;
case Direction.SouthEast:
openNeighborTwo = neighbor;
break;
case Direction.East:
closedNeighborTwo = neighbor;
break;
}
}
break;
case Direction.NorthWest:
foreach (var neighbor in currentNode.GetNeighbors())
{
var neighborDirection = PathfindingHelpers.RelativeDirection(neighbor, currentNode);
switch (neighborDirection)
{
case Direction.SouthWest:
openNeighborOne = neighbor;
break;
case Direction.South:
closedNeighborOne = neighbor;
break;
case Direction.NorthEast:
openNeighborTwo = neighbor;
break;
case Direction.East:
closedNeighborTwo = neighbor;
break;
}
}
break;
default:
throw new ArgumentOutOfRangeException();
}
if ((closedNeighborOne == null || PathfindingHelpers.GetTileCost(_pathfindingArgs, currentNode, closedNeighborOne) == null)
&& openNeighborOne != null && PathfindingHelpers.GetTileCost(_pathfindingArgs, currentNode, openNeighborOne) != null)
{
return true;
}
if ((closedNeighborTwo == null || PathfindingHelpers.GetTileCost(_pathfindingArgs, currentNode, closedNeighborTwo) == null)
&& openNeighborTwo != null && PathfindingHelpers.GetTileCost(_pathfindingArgs, currentNode, openNeighborTwo) != null)
{
return true;
}
return false;
}
/// <summary>
/// Check to see if the node is a jump point (only works for cardinal directions)
/// </summary>
private bool IsCardinalJumpPoint(Direction direction, PathfindingNode currentNode)
{
PathfindingNode? openNeighborOne = null;
PathfindingNode? closedNeighborOne = null;
PathfindingNode? openNeighborTwo = null;
PathfindingNode? closedNeighborTwo = null;
switch (direction)
{
case Direction.North:
foreach (var neighbor in currentNode.GetNeighbors())
{
var neighborDirection = PathfindingHelpers.RelativeDirection(neighbor, currentNode);
switch (neighborDirection)
{
case Direction.NorthEast:
openNeighborOne = neighbor;
break;
case Direction.East:
closedNeighborOne = neighbor;
break;
case Direction.NorthWest:
openNeighborTwo = neighbor;
break;
case Direction.West:
closedNeighborTwo = neighbor;
break;
}
}
break;
case Direction.East:
foreach (var neighbor in currentNode.GetNeighbors())
{
var neighborDirection = PathfindingHelpers.RelativeDirection(neighbor, currentNode);
switch (neighborDirection)
{
case Direction.NorthEast:
openNeighborOne = neighbor;
break;
case Direction.North:
closedNeighborOne = neighbor;
break;
case Direction.SouthEast:
openNeighborTwo = neighbor;
break;
case Direction.South:
closedNeighborTwo = neighbor;
break;
}
}
break;
case Direction.South:
foreach (var neighbor in currentNode.GetNeighbors())
{
var neighborDirection = PathfindingHelpers.RelativeDirection(neighbor, currentNode);
switch (neighborDirection)
{
case Direction.SouthEast:
openNeighborOne = neighbor;
break;
case Direction.East:
closedNeighborOne = neighbor;
break;
case Direction.SouthWest:
openNeighborTwo = neighbor;
break;
case Direction.West:
closedNeighborTwo = neighbor;
break;
}
}
break;
case Direction.West:
foreach (var neighbor in currentNode.GetNeighbors())
{
var neighborDirection = PathfindingHelpers.RelativeDirection(neighbor, currentNode);
switch (neighborDirection)
{
case Direction.NorthWest:
openNeighborOne = neighbor;
break;
case Direction.North:
closedNeighborOne = neighbor;
break;
case Direction.SouthWest:
openNeighborTwo = neighbor;
break;
case Direction.South:
closedNeighborTwo = neighbor;
break;
}
}
break;
default:
throw new ArgumentOutOfRangeException();
}
if ((closedNeighborOne == null || !PathfindingHelpers.Traversable(_pathfindingArgs.CollisionMask, _pathfindingArgs.Access, closedNeighborOne)) &&
openNeighborOne != null && PathfindingHelpers.Traversable(_pathfindingArgs.CollisionMask, _pathfindingArgs.Access, openNeighborOne))
{
return true;
}
if ((closedNeighborTwo == null || !PathfindingHelpers.Traversable(_pathfindingArgs.CollisionMask, _pathfindingArgs.Access, closedNeighborTwo)) &&
openNeighborTwo != null && PathfindingHelpers.Traversable(_pathfindingArgs.CollisionMask, _pathfindingArgs.Access, openNeighborTwo))
{
return true;
}
return false;
}
}
}

View File

@@ -1,45 +0,0 @@
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders
{
public struct PathfindingArgs
{
public EntityUid Uid { get; }
public ICollection<string> Access { get; }
public int CollisionMask { get; }
public TileRef Start { get; }
public TileRef End { get; }
// How close we need to get to the endpoint to be 'done'
public float Proximity { get; }
// Whether we use cardinal only or not
public bool AllowDiagonals { get; }
// Can we go through walls
public bool NoClip { get; }
// Can we traverse space tiles
public bool AllowSpace { get; }
public PathfindingArgs(
EntityUid entityUid,
ICollection<string> access,
int collisionMask,
TileRef start,
TileRef end,
float proximity = 0.0f,
bool allowDiagonals = true,
bool noClip = false,
bool allowSpace = false)
{
Uid = entityUid;
Access = access;
CollisionMask = collisionMask;
Start = start;
End = end;
Proximity = proximity;
AllowDiagonals = allowDiagonals;
NoClip = noClip;
AllowSpace = allowSpace;
}
}
}

View File

@@ -1,13 +0,0 @@
using System;
using System.Collections.Generic;
namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders
{
public class PathfindingComparer : IComparer<ValueTuple<float, PathfindingNode>>
{
public int Compare((float, PathfindingNode) x, (float, PathfindingNode) y)
{
return y.Item1.CompareTo(x.Item1);
}
}
}

View File

@@ -1,190 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding
{
public class PathfindingChunkUpdateMessage : EntityEventArgs
{
public PathfindingChunk Chunk { get; }
public PathfindingChunkUpdateMessage(PathfindingChunk chunk)
{
Chunk = chunk;
}
}
public class PathfindingChunk
{
public TimeSpan LastUpdate { get; private set; }
public GridId GridId { get; }
public Vector2i Indices => _indices;
private readonly Vector2i _indices;
// Nodes per chunk row
public static int ChunkSize => 8;
public PathfindingNode[,] Nodes => _nodes;
private readonly PathfindingNode[,] _nodes = new PathfindingNode[ChunkSize,ChunkSize];
public PathfindingChunk(GridId gridId, Vector2i indices)
{
GridId = gridId;
_indices = indices;
}
public void Initialize(IMapGrid mapGrid)
{
for (var x = 0; x < ChunkSize; x++)
{
for (var y = 0; y < ChunkSize; y++)
{
var tileRef = mapGrid.GetTileRef(new Vector2i(x + _indices.X, y + _indices.Y));
CreateNode(tileRef);
}
}
Dirty();
}
/// <summary>
/// Only called when blockers change (i.e. un-anchored physics objects don't trigger)
/// </summary>
public void Dirty()
{
LastUpdate = IoCManager.Resolve<IGameTiming>().CurTime;
IoCManager.Resolve<IEntityManager>().EventBus
.RaiseEvent(EventSource.Local, new PathfindingChunkUpdateMessage(this));
}
public IEnumerable<PathfindingChunk> GetNeighbors()
{
var pathfindingSystem = EntitySystem.Get<PathfindingSystem>();
var chunkGrid = pathfindingSystem.Graph[GridId];
for (var x = -1; x <= 1; x++)
{
for (var y = -1; y <= 1; y++)
{
if (x == 0 && y == 0) continue;
var (neighborX, neighborY) = (_indices.X + ChunkSize * x, _indices.Y + ChunkSize * y);
if (chunkGrid.TryGetValue(new Vector2i(neighborX, neighborY), out var neighbor))
{
yield return neighbor;
}
}
}
}
public bool InBounds(Vector2i Vector2i)
{
if (Vector2i.X < _indices.X || Vector2i.Y < _indices.Y) return false;
if (Vector2i.X >= _indices.X + ChunkSize || Vector2i.Y >= _indices.Y + ChunkSize) return false;
return true;
}
/// <summary>
/// Returns true if the tile is on the outer edge
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
public bool OnEdge(PathfindingNode node)
{
if (node.TileRef.X == _indices.X) return true;
if (node.TileRef.Y == _indices.Y) return true;
if (node.TileRef.X == _indices.X + ChunkSize - 1) return true;
if (node.TileRef.Y == _indices.Y + ChunkSize - 1) return true;
return false;
}
/// <summary>
/// Gets our neighbors that are relevant for the node to retrieve its own neighbors
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
public IEnumerable<PathfindingChunk> RelevantChunks(PathfindingNode node)
{
var relevantDirections = GetEdges(node).ToList();
foreach (var chunk in GetNeighbors())
{
var chunkDirection = PathfindingHelpers.RelativeDirection(chunk, this);
if (relevantDirections.Contains(chunkDirection))
{
yield return chunk;
}
}
}
private IEnumerable<Direction> GetEdges(PathfindingNode node)
{
// West Edge
if (node.TileRef.X == _indices.X)
{
yield return Direction.West;
if (node.TileRef.Y == _indices.Y)
{
yield return Direction.SouthWest;
yield return Direction.South;
} else if (node.TileRef.Y == _indices.Y + ChunkSize - 1)
{
yield return Direction.NorthWest;
yield return Direction.North;
}
yield break;
}
// East edge
if (node.TileRef.X == _indices.X + ChunkSize - 1)
{
yield return Direction.East;
if (node.TileRef.Y == _indices.Y)
{
yield return Direction.SouthEast;
yield return Direction.South;
} else if (node.TileRef.Y == _indices.Y + ChunkSize - 1)
{
yield return Direction.NorthEast;
yield return Direction.North;
}
yield break;
}
// South edge
if (node.TileRef.Y == _indices.Y)
{
yield return Direction.South;
// Given we already checked south-west and south-east above shouldn't need any more
}
// North edge
if (node.TileRef.Y == _indices.Y + ChunkSize - 1)
{
yield return Direction.North;
}
}
public PathfindingNode GetNode(TileRef tile)
{
var chunkX = tile.X - _indices.X;
var chunkY = tile.Y - _indices.Y;
return _nodes[chunkX, chunkY];
}
private void CreateNode(TileRef tile, PathfindingChunk? parent = null)
{
parent ??= this;
var node = new PathfindingNode(parent, tile);
var offsetX = tile.X - Indices.X;
var offsetY = tile.Y - Indices.Y;
_nodes[offsetX, offsetY] = node;
}
}
}

View File

@@ -1,353 +0,0 @@
using System;
using System.Collections.Generic;
using Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible;
using Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding
{
public static class PathfindingHelpers
{
public static bool TryEndNode(ref PathfindingNode endNode, PathfindingArgs pathfindingArgs)
{
if (!Traversable(pathfindingArgs.CollisionMask, pathfindingArgs.Access, endNode))
{
if (pathfindingArgs.Proximity > 0.0f)
{
foreach (var node in BFSPathfinder.GetNodesInRange(pathfindingArgs, false))
{
endNode = node;
return true;
}
}
return false;
}
return true;
}
public static bool DirectionTraversable(int collisionMask, ICollection<string> access, PathfindingNode currentNode, Direction direction)
{
// If it's a diagonal we need to check NSEW to see if we can get to it and stop corner cutting, NE needs N and E etc.
// Given there's different collision layers stored for each node in the graph it's probably not worth it to cache this
// Also this will help with corner-cutting
PathfindingNode? northNeighbor = null;
PathfindingNode? southNeighbor = null;
PathfindingNode? eastNeighbor = null;
PathfindingNode? westNeighbor = null;
foreach (var neighbor in currentNode.GetNeighbors())
{
if (neighbor.TileRef.X == currentNode.TileRef.X &&
neighbor.TileRef.Y == currentNode.TileRef.Y + 1)
{
northNeighbor = neighbor;
continue;
}
if (neighbor.TileRef.X == currentNode.TileRef.X + 1 &&
neighbor.TileRef.Y == currentNode.TileRef.Y)
{
eastNeighbor = neighbor;
continue;
}
if (neighbor.TileRef.X == currentNode.TileRef.X &&
neighbor.TileRef.Y == currentNode.TileRef.Y - 1)
{
southNeighbor = neighbor;
continue;
}
if (neighbor.TileRef.X == currentNode.TileRef.X - 1 &&
neighbor.TileRef.Y == currentNode.TileRef.Y)
{
westNeighbor = neighbor;
continue;
}
}
switch (direction)
{
case Direction.NorthEast:
if (northNeighbor == null || eastNeighbor == null) return false;
if (!Traversable(collisionMask, access, northNeighbor) ||
!Traversable(collisionMask, access, eastNeighbor))
{
return false;
}
break;
case Direction.NorthWest:
if (northNeighbor == null || westNeighbor == null) return false;
if (!Traversable(collisionMask, access, northNeighbor) ||
!Traversable(collisionMask, access, westNeighbor))
{
return false;
}
break;
case Direction.SouthWest:
if (southNeighbor == null || westNeighbor == null) return false;
if (!Traversable(collisionMask, access, southNeighbor) ||
!Traversable(collisionMask, access, westNeighbor))
{
return false;
}
break;
case Direction.SouthEast:
if (southNeighbor == null || eastNeighbor == null) return false;
if (!Traversable(collisionMask, access, southNeighbor) ||
!Traversable(collisionMask, access, eastNeighbor))
{
return false;
}
break;
}
return true;
}
public static bool Traversable(int collisionMask, ICollection<string> access, PathfindingNode node)
{
if ((collisionMask & node.BlockedCollisionMask) != 0)
{
return false;
}
foreach (var reader in node.AccessReaders)
{
if (!reader.IsAllowed(access))
{
return false;
}
}
return true;
}
public static Queue<TileRef> ReconstructPath(Dictionary<PathfindingNode, PathfindingNode> cameFrom, PathfindingNode current)
{
var running = new Stack<TileRef>();
running.Push(current.TileRef);
while (cameFrom.ContainsKey(current))
{
var previousCurrent = current;
current = cameFrom[current];
cameFrom.Remove(previousCurrent);
running.Push(current.TileRef);
}
var result = new Queue<TileRef>(running);
return result;
}
/// <summary>
/// This will reconstruct the path and fill in the tile holes as well
/// </summary>
/// <param name="cameFrom"></param>
/// <param name="current"></param>
/// <returns></returns>
public static Queue<TileRef> ReconstructJumpPath(Dictionary<PathfindingNode, PathfindingNode> cameFrom, PathfindingNode current)
{
var running = new Stack<TileRef>();
running.Push(current.TileRef);
while (cameFrom.ContainsKey(current))
{
var previousCurrent = current;
current = cameFrom[current];
var intermediate = previousCurrent;
cameFrom.Remove(previousCurrent);
var pathfindingSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<PathfindingSystem>();
var mapManager = IoCManager.Resolve<IMapManager>();
var grid = mapManager.GetGrid(current.TileRef.GridIndex);
// Get all the intermediate nodes
while (true)
{
var xOffset = 0;
var yOffset = 0;
if (intermediate.TileRef.X < current.TileRef.X)
{
xOffset += 1;
}
else if (intermediate.TileRef.X > current.TileRef.X)
{
xOffset -= 1;
}
else
{
xOffset = 0;
}
if (intermediate.TileRef.Y < current.TileRef.Y)
{
yOffset += 1;
}
else if (intermediate.TileRef.Y > current.TileRef.Y)
{
yOffset -= 1;
}
else
{
yOffset = 0;
}
intermediate = pathfindingSystem.GetNode(grid.GetTileRef(
new Vector2i(intermediate.TileRef.X + xOffset, intermediate.TileRef.Y + yOffset)));
if (intermediate.TileRef != current.TileRef)
{
// Hacky corner cut fix
running.Push(intermediate.TileRef);
continue;
}
break;
}
running.Push(current.TileRef);
}
var result = new Queue<TileRef>(running);
return result;
}
public static float OctileDistance(int dstX, int dstY)
{
if (dstX > dstY)
{
return 1.4f * dstY + (dstX - dstY);
}
return 1.4f * dstX + (dstY - dstX);
}
public static float OctileDistance(PathfindingNode endNode, PathfindingNode currentNode)
{
// "Fast Euclidean" / octile.
// This implementation is written down in a few sources; it just saves doing sqrt.
int dstX = Math.Abs(currentNode.TileRef.X - endNode.TileRef.X);
int dstY = Math.Abs(currentNode.TileRef.Y - endNode.TileRef.Y);
if (dstX > dstY)
{
return 1.4f * dstY + (dstX - dstY);
}
return 1.4f * dstX + (dstY - dstX);
}
public static float OctileDistance(TileRef endTile, TileRef startTile)
{
// "Fast Euclidean" / octile.
// This implementation is written down in a few sources; it just saves doing sqrt.
int dstX = Math.Abs(startTile.X - endTile.X);
int dstY = Math.Abs(startTile.Y - endTile.Y);
if (dstX > dstY)
{
return 1.4f * dstY + (dstX - dstY);
}
return 1.4f * dstX + (dstY - dstX);
}
public static float ManhattanDistance(PathfindingNode endNode, PathfindingNode currentNode)
{
return Math.Abs(currentNode.TileRef.X - endNode.TileRef.X) + Math.Abs(currentNode.TileRef.Y - endNode.TileRef.Y);
}
public static float? GetTileCost(PathfindingArgs pathfindingArgs, PathfindingNode start, PathfindingNode end)
{
if (!pathfindingArgs.NoClip && !Traversable(pathfindingArgs.CollisionMask, pathfindingArgs.Access, end))
{
return null;
}
if (!pathfindingArgs.AllowSpace && end.TileRef.Tile.IsEmpty)
{
return null;
}
var cost = 1.0f;
switch (pathfindingArgs.AllowDiagonals)
{
case true:
cost *= OctileDistance(end, start);
break;
// Manhattan distance
case false:
cost *= ManhattanDistance(end, start);
break;
}
return cost;
}
public static Direction RelativeDirection(PathfindingChunk endChunk, PathfindingChunk startChunk)
{
var xDiff = (endChunk.Indices.X - startChunk.Indices.X) / PathfindingChunk.ChunkSize;
var yDiff = (endChunk.Indices.Y - startChunk.Indices.Y) / PathfindingChunk.ChunkSize;
return RelativeDirection(xDiff, yDiff);
}
public static Direction RelativeDirection(PathfindingNode endNode, PathfindingNode startNode)
{
var xDiff = endNode.TileRef.X - startNode.TileRef.X;
var yDiff = endNode.TileRef.Y - startNode.TileRef.Y;
return RelativeDirection(xDiff, yDiff);
}
public static Direction RelativeDirection(int x, int y)
{
switch (x)
{
case -1:
switch (y)
{
case -1:
return Direction.SouthWest;
case 0:
return Direction.West;
case 1:
return Direction.NorthWest;
default:
throw new InvalidOperationException();
}
case 0:
switch (y)
{
case -1:
return Direction.South;
case 0:
throw new InvalidOperationException();
case 1:
return Direction.North;
default:
throw new InvalidOperationException();
}
case 1:
switch (y)
{
case -1:
return Direction.SouthEast;
case 0:
return Direction.East;
case 1:
return Direction.NorthEast;
default:
throw new InvalidOperationException();
}
default:
throw new InvalidOperationException();
}
}
}
}

View File

@@ -1,328 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Server.GameObjects.Components.Access;
using Content.Server.GameObjects.Components.Doors;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Utility;
namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding
{
public class PathfindingNode
{
public PathfindingChunk ParentChunk => _parentChunk;
private readonly PathfindingChunk _parentChunk;
public TileRef TileRef { get; private set; }
/// <summary>
/// Whenever there's a change in the collision layers we update the mask as the graph has more reads than writes
/// </summary>
public int BlockedCollisionMask { get; private set; }
private readonly Dictionary<IEntity, int> _blockedCollidables = new(0);
public IReadOnlyDictionary<IEntity, int> PhysicsLayers => _physicsLayers;
private readonly Dictionary<IEntity, int> _physicsLayers = new(0);
/// <summary>
/// The entities on this tile that require access to traverse
/// </summary>
/// We don't store the ICollection, at least for now, as we'd need to replicate the access code here
public IReadOnlyCollection<AccessReader> AccessReaders => _accessReaders.Values;
private readonly Dictionary<IEntity, AccessReader> _accessReaders = new(0);
public PathfindingNode(PathfindingChunk parent, TileRef tileRef)
{
_parentChunk = parent;
TileRef = tileRef;
GenerateMask();
}
public static bool IsRelevant(IEntity entity, IPhysBody physicsComponent)
{
if (entity.Transform.GridID == GridId.Invalid ||
(PathfindingSystem.TrackedCollisionLayers & physicsComponent.CollisionLayer) == 0)
{
return false;
}
return true;
}
/// <summary>
/// Return our neighboring nodes (even across chunks)
/// </summary>
/// <returns></returns>
public IEnumerable<PathfindingNode> GetNeighbors()
{
List<PathfindingChunk>? neighborChunks = null;
if (ParentChunk.OnEdge(this))
{
neighborChunks = ParentChunk.RelevantChunks(this).ToList();
}
for (var x = -1; x <= 1; x++)
{
for (var y = -1; y <= 1; y++)
{
if (x == 0 && y == 0) continue;
var indices = new Vector2i(TileRef.X + x, TileRef.Y + y);
if (ParentChunk.InBounds(indices))
{
var (relativeX, relativeY) = (indices.X - ParentChunk.Indices.X,
indices.Y - ParentChunk.Indices.Y);
yield return ParentChunk.Nodes[relativeX, relativeY];
}
else
{
DebugTools.AssertNotNull(neighborChunks);
// Get the relevant chunk and then get the node on it
foreach (var neighbor in neighborChunks!)
{
// A lot of edge transitions are going to have a single neighboring chunk
// (given > 1 only affects corners)
// So we can just check the count to see if it's inbound
if (neighborChunks.Count > 0 && !neighbor.InBounds(indices)) continue;
var (relativeX, relativeY) = (indices.X - neighbor.Indices.X,
indices.Y - neighbor.Indices.Y);
yield return neighbor.Nodes[relativeX, relativeY];
break;
}
}
}
}
}
public PathfindingNode? GetNeighbor(Direction direction)
{
var chunkXOffset = TileRef.X - ParentChunk.Indices.X;
var chunkYOffset = TileRef.Y - ParentChunk.Indices.Y;
Vector2i neighborVector2i;
switch (direction)
{
case Direction.East:
if (!ParentChunk.OnEdge(this))
{
return ParentChunk.Nodes[chunkXOffset + 1, chunkYOffset];
}
neighborVector2i = new Vector2i(TileRef.X + 1, TileRef.Y);
foreach (var neighbor in ParentChunk.GetNeighbors())
{
if (neighbor.InBounds(neighborVector2i))
{
return neighbor.Nodes[neighborVector2i.X - neighbor.Indices.X,
neighborVector2i.Y - neighbor.Indices.Y];
}
}
return null;
case Direction.NorthEast:
if (!ParentChunk.OnEdge(this))
{
return ParentChunk.Nodes[chunkXOffset + 1, chunkYOffset + 1];
}
neighborVector2i = new Vector2i(TileRef.X + 1, TileRef.Y + 1);
foreach (var neighbor in ParentChunk.GetNeighbors())
{
if (neighbor.InBounds(neighborVector2i))
{
return neighbor.Nodes[neighborVector2i.X - neighbor.Indices.X,
neighborVector2i.Y - neighbor.Indices.Y];
}
}
return null;
case Direction.North:
if (!ParentChunk.OnEdge(this))
{
return ParentChunk.Nodes[chunkXOffset, chunkYOffset + 1];
}
neighborVector2i = new Vector2i(TileRef.X, TileRef.Y + 1);
foreach (var neighbor in ParentChunk.GetNeighbors())
{
if (neighbor.InBounds(neighborVector2i))
{
return neighbor.Nodes[neighborVector2i.X - neighbor.Indices.X,
neighborVector2i.Y - neighbor.Indices.Y];
}
}
return null;
case Direction.NorthWest:
if (!ParentChunk.OnEdge(this))
{
return ParentChunk.Nodes[chunkXOffset - 1, chunkYOffset + 1];
}
neighborVector2i = new Vector2i(TileRef.X - 1, TileRef.Y + 1);
foreach (var neighbor in ParentChunk.GetNeighbors())
{
if (neighbor.InBounds(neighborVector2i))
{
return neighbor.Nodes[neighborVector2i.X - neighbor.Indices.X,
neighborVector2i.Y - neighbor.Indices.Y];
}
}
return null;
case Direction.West:
if (!ParentChunk.OnEdge(this))
{
return ParentChunk.Nodes[chunkXOffset - 1, chunkYOffset];
}
neighborVector2i = new Vector2i(TileRef.X - 1, TileRef.Y);
foreach (var neighbor in ParentChunk.GetNeighbors())
{
if (neighbor.InBounds(neighborVector2i))
{
return neighbor.Nodes[neighborVector2i.X - neighbor.Indices.X,
neighborVector2i.Y - neighbor.Indices.Y];
}
}
return null;
case Direction.SouthWest:
if (!ParentChunk.OnEdge(this))
{
return ParentChunk.Nodes[chunkXOffset - 1, chunkYOffset - 1];
}
neighborVector2i = new Vector2i(TileRef.X - 1, TileRef.Y - 1);
foreach (var neighbor in ParentChunk.GetNeighbors())
{
if (neighbor.InBounds(neighborVector2i))
{
return neighbor.Nodes[neighborVector2i.X - neighbor.Indices.X,
neighborVector2i.Y - neighbor.Indices.Y];
}
}
return null;
case Direction.South:
if (!ParentChunk.OnEdge(this))
{
return ParentChunk.Nodes[chunkXOffset, chunkYOffset - 1];
}
neighborVector2i = new Vector2i(TileRef.X, TileRef.Y - 1);
foreach (var neighbor in ParentChunk.GetNeighbors())
{
if (neighbor.InBounds(neighborVector2i))
{
return neighbor.Nodes[neighborVector2i.X - neighbor.Indices.X,
neighborVector2i.Y - neighbor.Indices.Y];
}
}
return null;
case Direction.SouthEast:
if (!ParentChunk.OnEdge(this))
{
return ParentChunk.Nodes[chunkXOffset + 1, chunkYOffset - 1];
}
neighborVector2i = new Vector2i(TileRef.X + 1, TileRef.Y - 1);
foreach (var neighbor in ParentChunk.GetNeighbors())
{
if (neighbor.InBounds(neighborVector2i))
{
return neighbor.Nodes[neighborVector2i.X - neighbor.Indices.X,
neighborVector2i.Y - neighbor.Indices.Y];
}
}
return null;
default:
throw new ArgumentOutOfRangeException(nameof(direction), direction, null);
}
}
public void UpdateTile(TileRef newTile)
{
TileRef = newTile;
ParentChunk.Dirty();
}
/// <summary>
/// Call if this entity is relevant for the pathfinder
/// </summary>
/// <param name="entity"></param>
/// TODO: These 2 methods currently don't account for a bunch of changes (e.g. airlock unpowered, wrenching, etc.)
/// TODO: Could probably optimise this slightly more.
public void AddEntity(IEntity entity, IPhysBody physicsComponent)
{
// If we're a door
if (entity.HasComponent<AirlockComponent>() || entity.HasComponent<ServerDoorComponent>())
{
// If we need access to traverse this then add to readers, otherwise no point adding it (except for maybe tile costs in future)
// TODO: Check for powered I think (also need an event for when it's depowered
// AccessReader calls this whenever opening / closing but it can seem to get called multiple times
// Which may or may not be intended?
if (entity.TryGetComponent(out AccessReader? accessReader) && !_accessReaders.ContainsKey(entity))
{
_accessReaders.Add(entity, accessReader);
ParentChunk.Dirty();
}
return;
}
DebugTools.Assert((PathfindingSystem.TrackedCollisionLayers & physicsComponent.CollisionLayer) != 0);
if (physicsComponent.BodyType == BodyType.Static)
{
_physicsLayers.Add(entity, physicsComponent.CollisionLayer);
}
else
{
_blockedCollidables.Add(entity, physicsComponent.CollisionLayer);
GenerateMask();
ParentChunk.Dirty();
}
}
/// <summary>
/// Remove the entity from this node.
/// Will check each category and remove it from the applicable one
/// </summary>
/// <param name="entity"></param>
public void RemoveEntity(IEntity entity)
{
// There's no guarantee that the entity isn't deleted
// 90% of updates are probably entities moving around
// Entity can't be under multiple categories so just checking each once is fine.
if (_physicsLayers.ContainsKey(entity))
{
_physicsLayers.Remove(entity);
}
else if (_accessReaders.ContainsKey(entity))
{
_accessReaders.Remove(entity);
ParentChunk.Dirty();
}
else if (_blockedCollidables.ContainsKey(entity))
{
_blockedCollidables.Remove(entity);
GenerateMask();
ParentChunk.Dirty();
}
}
private void GenerateMask()
{
BlockedCollisionMask = 0x0;
foreach (var layer in _blockedCollidables.Values)
{
BlockedCollisionMask |= layer;
}
}
}
}

View File

@@ -1,400 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Content.Server.GameObjects.Components.Access;
using Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders;
using Content.Server.GameObjects.EntitySystems.JobQueues;
using Content.Server.GameObjects.EntitySystems.JobQueues.Queues;
using Content.Shared.GameTicking;
using Content.Shared.Physics;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Utility;
namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding
{
/*
// TODO: IMO use rectangular symmetry reduction on the nodes with collision at all. (currently planned to be implemented via AiReachableSystem and expanded later).
alternatively store all rooms and have an alternative graph for humanoid mobs (same collision mask, needs access etc). You could also just path from room to room as needed.
// TODO: Longer term -> Handle collision layer changes?
TODO: Handle container entities so they're not tracked.
*/
/// <summary>
/// This system handles pathfinding graph updates as well as dispatches to the pathfinder
/// (90% of what it's doing is graph updates so not much point splitting the 2 roles)
/// </summary>
public class PathfindingSystem : EntitySystem, IResettingEntitySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
public IReadOnlyDictionary<GridId, Dictionary<Vector2i, PathfindingChunk>> Graph => _graph;
private readonly Dictionary<GridId, Dictionary<Vector2i, PathfindingChunk>> _graph = new();
private readonly PathfindingJobQueue _pathfindingQueue = new();
// Queued pathfinding graph updates
private readonly Queue<CollisionChangeMessage> _collidableUpdateQueue = new();
private readonly Queue<MoveEvent> _moveUpdateQueue = new();
private readonly Queue<AccessReaderChangeMessage> _accessReaderUpdateQueue = new();
private readonly Queue<TileRef> _tileUpdateQueue = new();
// Need to store previously known entity positions for collidables for when they move
private readonly Dictionary<IEntity, PathfindingNode> _lastKnownPositions = new();
public const int TrackedCollisionLayers = (int)
(CollisionGroup.Impassable |
CollisionGroup.MobImpassable |
CollisionGroup.SmallImpassable |
CollisionGroup.VaultImpassable);
/// <summary>
/// Ask for the pathfinder to gimme somethin
/// </summary>
/// <param name="pathfindingArgs"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Job<Queue<TileRef>> RequestPath(PathfindingArgs pathfindingArgs, CancellationToken cancellationToken)
{
var startNode = GetNode(pathfindingArgs.Start);
var endNode = GetNode(pathfindingArgs.End);
var job = new AStarPathfindingJob(0.003, startNode, endNode, pathfindingArgs, cancellationToken);
_pathfindingQueue.EnqueueJob(job);
return job;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
// Make sure graph is updated, then get pathfinders
ProcessGraphUpdates();
_pathfindingQueue.Process();
}
private void ProcessGraphUpdates()
{
var totalUpdates = 0;
foreach (var update in _collidableUpdateQueue)
{
if (!EntityManager.TryGetEntity(update.Owner, out var entity)) continue;
if (update.CanCollide)
{
HandleEntityAdd(entity);
}
else
{
HandleEntityRemove(entity);
}
totalUpdates++;
}
_collidableUpdateQueue.Clear();
foreach (var update in _accessReaderUpdateQueue)
{
if (update.Enabled)
{
HandleEntityAdd(update.Sender);
}
else
{
HandleEntityRemove(update.Sender);
}
totalUpdates++;
}
_accessReaderUpdateQueue.Clear();
foreach (var tile in _tileUpdateQueue)
{
HandleTileUpdate(tile);
totalUpdates++;
}
_tileUpdateQueue.Clear();
var moveUpdateCount = Math.Max(50 - totalUpdates, 0);
// Other updates are high priority so for this we'll just defer it if there's a spike (explosion, etc.)
// If the move updates grow too large then we'll just do it
if (_moveUpdateQueue.Count > 100)
{
moveUpdateCount = _moveUpdateQueue.Count - 100;
}
moveUpdateCount = Math.Min(moveUpdateCount, _moveUpdateQueue.Count);
for (var i = 0; i < moveUpdateCount; i++)
{
HandleEntityMove(_moveUpdateQueue.Dequeue());
}
DebugTools.Assert(_moveUpdateQueue.Count < 1000);
}
public PathfindingChunk GetChunk(TileRef tile)
{
var chunkX = (int) (Math.Floor((float) tile.X / PathfindingChunk.ChunkSize) * PathfindingChunk.ChunkSize);
var chunkY = (int) (Math.Floor((float) tile.Y / PathfindingChunk.ChunkSize) * PathfindingChunk.ChunkSize);
var Vector2i = new Vector2i(chunkX, chunkY);
if (_graph.TryGetValue(tile.GridIndex, out var chunks))
{
if (!chunks.ContainsKey(Vector2i))
{
CreateChunk(tile.GridIndex, Vector2i);
}
return chunks[Vector2i];
}
var newChunk = CreateChunk(tile.GridIndex, Vector2i);
return newChunk;
}
private PathfindingChunk CreateChunk(GridId gridId, Vector2i indices)
{
var newChunk = new PathfindingChunk(gridId, indices);
if (!_graph.ContainsKey(gridId))
{
_graph.Add(gridId, new Dictionary<Vector2i, PathfindingChunk>());
}
_graph[gridId].Add(indices, newChunk);
newChunk.Initialize(_mapManager.GetGrid(gridId));
return newChunk;
}
/// <summary>
/// Get the entity's tile position, then get the corresponding node
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
public PathfindingNode GetNode(IEntity entity)
{
var tile = _mapManager.GetGrid(entity.Transform.GridID).GetTileRef(entity.Transform.Coordinates);
return GetNode(tile);
}
/// <summary>
/// Return the corresponding PathfindingNode for this tile
/// </summary>
/// <param name="tile"></param>
/// <returns></returns>
public PathfindingNode GetNode(TileRef tile)
{
var chunk = GetChunk(tile);
var node = chunk.GetNode(tile);
return node;
}
public override void Initialize()
{
SubscribeLocalEvent<CollisionChangeMessage>(QueueCollisionChangeMessage);
SubscribeLocalEvent<MoveEvent>(QueueMoveEvent);
SubscribeLocalEvent<AccessReaderChangeMessage>(QueueAccessChangeMessage);
// Handle all the base grid changes
// Anything that affects traversal (i.e. collision layer) is handled separately.
_mapManager.OnGridRemoved += HandleGridRemoval;
_mapManager.GridChanged += QueueGridChange;
_mapManager.TileChanged += QueueTileChange;
}
public override void Shutdown()
{
base.Shutdown();
UnsubscribeLocalEvent<CollisionChangeMessage>();
UnsubscribeLocalEvent<MoveEvent>();
UnsubscribeLocalEvent<AccessReaderChangeMessage>();
_mapManager.OnGridRemoved -= HandleGridRemoval;
_mapManager.GridChanged -= QueueGridChange;
_mapManager.TileChanged -= QueueTileChange;
}
private void HandleTileUpdate(TileRef tile)
{
var node = GetNode(tile);
node.UpdateTile(tile);
}
private void HandleGridRemoval(MapId mapId, GridId gridId)
{
if (_graph.ContainsKey(gridId))
{
_graph.Remove(gridId);
}
}
private void QueueGridChange(object? sender, GridChangedEventArgs eventArgs)
{
foreach (var (position, _) in eventArgs.Modified)
{
_tileUpdateQueue.Enqueue(eventArgs.Grid.GetTileRef(position));
}
}
private void QueueTileChange(object? sender, TileChangedEventArgs eventArgs)
{
_tileUpdateQueue.Enqueue(eventArgs.NewTile);
}
private void QueueAccessChangeMessage(AccessReaderChangeMessage message)
{
_accessReaderUpdateQueue.Enqueue(message);
}
/// <summary>
/// Tries to add the entity to the relevant pathfinding node
/// </summary>
/// The node will filter it to the correct category (if possible)
/// <param name="entity"></param>
private void HandleEntityAdd(IEntity entity)
{
if (entity.Deleted ||
_lastKnownPositions.ContainsKey(entity) ||
!entity.TryGetComponent(out IPhysBody? physics) ||
!PathfindingNode.IsRelevant(entity, physics))
{
return;
}
var grid = _mapManager.GetGrid(entity.Transform.GridID);
var tileRef = grid.GetTileRef(entity.Transform.Coordinates);
var chunk = GetChunk(tileRef);
var node = chunk.GetNode(tileRef);
node.AddEntity(entity, physics);
_lastKnownPositions.Add(entity, node);
}
private void HandleEntityRemove(IEntity entity)
{
if (!_lastKnownPositions.TryGetValue(entity, out var node))
{
return;
}
node.RemoveEntity(entity);
_lastKnownPositions.Remove(entity);
}
private void QueueMoveEvent(MoveEvent moveEvent)
{
_moveUpdateQueue.Enqueue(moveEvent);
}
/// <summary>
/// When an entity moves around we'll remove it from its old node and add it to its new node (if applicable)
/// </summary>
/// <param name="moveEvent"></param>
private void HandleEntityMove(MoveEvent moveEvent)
{
// If we've moved to space or the likes then remove us.
if (moveEvent.Sender.Deleted ||
!moveEvent.Sender.TryGetComponent(out IPhysBody? physics) ||
!PathfindingNode.IsRelevant(moveEvent.Sender, physics) ||
moveEvent.NewPosition.GetGridId(EntityManager) == GridId.Invalid)
{
HandleEntityRemove(moveEvent.Sender);
return;
}
// Memory leak protection until grid parenting confirmed fix / you REALLY need the performance
var gridBounds = _mapManager.GetGrid(moveEvent.Sender.Transform.GridID).WorldBounds;
if (!gridBounds.Contains(moveEvent.Sender.Transform.WorldPosition))
{
HandleEntityRemove(moveEvent.Sender);
return;
}
// If we move from space to a grid we may need to start tracking it.
if (!_lastKnownPositions.TryGetValue(moveEvent.Sender, out var oldNode))
{
HandleEntityAdd(moveEvent.Sender);
return;
}
var newGridId = moveEvent.NewPosition.GetGridId(_entityManager);
if (newGridId == GridId.Invalid)
{
HandleEntityRemove(moveEvent.Sender);
return;
}
// The pathfinding graph is tile-based so first we'll check if they're on a different tile and if we need to update.
// If you get entities bigger than 1 tile wide you'll need some other system so god help you.
var newTile = _mapManager.GetGrid(newGridId).GetTileRef(moveEvent.NewPosition);
if (oldNode == null || oldNode.TileRef == newTile)
{
return;
}
var newNode = GetNode(newTile);
_lastKnownPositions[moveEvent.Sender] = newNode;
oldNode.RemoveEntity(moveEvent.Sender);
newNode.AddEntity(moveEvent.Sender, physics);
}
private void QueueCollisionChangeMessage(CollisionChangeMessage collisionMessage)
{
_collidableUpdateQueue.Enqueue(collisionMessage);
}
// TODO: Need to rethink the pathfinder utils (traversable etc.). Maybe just chuck them all in PathfindingSystem
// Otherwise you get the steerer using this and the pathfinders using a different traversable.
// Also look at increasing tile cost the more physics entities are on it
public bool CanTraverse(IEntity entity, EntityCoordinates coordinates)
{
var gridId = coordinates.GetGridId(EntityManager);
var tile = _mapManager.GetGrid(gridId).GetTileRef(coordinates);
var node = GetNode(tile);
return CanTraverse(entity, node);
}
public bool CanTraverse(IEntity entity, PathfindingNode node)
{
if (entity.TryGetComponent(out IPhysBody? physics) &&
(physics.CollisionMask & node.BlockedCollisionMask) != 0)
{
return false;
}
var access = AccessReader.FindAccessTags(entity);
foreach (var reader in node.AccessReaders)
{
if (!reader.IsAllowed(access))
{
return false;
}
}
return true;
}
public void Reset()
{
_graph.Clear();
_collidableUpdateQueue.Clear();
_moveUpdateQueue.Clear();
_accessReaderUpdateQueue.Clear();
_tileUpdateQueue.Clear();
_lastKnownPositions.Clear();
}
}
}

View File

@@ -1,128 +0,0 @@
using System.Collections.Generic;
using Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders;
using Content.Shared.AI;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding
{
#if DEBUG
[UsedImplicitly]
public class ServerPathfindingDebugSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
AStarPathfindingJob.DebugRoute += DispatchAStarDebug;
JpsPathfindingJob.DebugRoute += DispatchJpsDebug;
SubscribeNetworkEvent<SharedAiDebug.RequestPathfindingGraphMessage>(DispatchGraph);
}
public override void Shutdown()
{
base.Shutdown();
AStarPathfindingJob.DebugRoute -= DispatchAStarDebug;
JpsPathfindingJob.DebugRoute -= DispatchJpsDebug;
}
private void DispatchAStarDebug(SharedAiDebug.AStarRouteDebug routeDebug)
{
var mapManager = IoCManager.Resolve<IMapManager>();
var route = new List<Vector2>();
foreach (var tile in routeDebug.Route)
{
var tileGrid = mapManager.GetGrid(tile.GridIndex).GridTileToLocal(tile.GridIndices);
route.Add(tileGrid.ToMapPos(EntityManager));
}
var cameFrom = new Dictionary<Vector2, Vector2>();
foreach (var (from, to) in routeDebug.CameFrom)
{
var tileOneGrid = mapManager.GetGrid(from.GridIndex).GridTileToLocal(from.GridIndices);
var tileOneWorld = tileOneGrid.ToMapPos(EntityManager);
var tileTwoGrid = mapManager.GetGrid(to.GridIndex).GridTileToLocal(to.GridIndices);
var tileTwoWorld = tileTwoGrid.ToMapPos(EntityManager);
cameFrom.Add(tileOneWorld, tileTwoWorld);
}
var gScores = new Dictionary<Vector2, float>();
foreach (var (tile, score) in routeDebug.GScores)
{
var tileGrid = mapManager.GetGrid(tile.GridIndex).GridTileToLocal(tile.GridIndices);
gScores.Add(tileGrid.ToMapPos(EntityManager), score);
}
var systemMessage = new SharedAiDebug.AStarRouteMessage(
routeDebug.EntityUid,
route,
cameFrom,
gScores,
routeDebug.TimeTaken
);
RaiseNetworkEvent(systemMessage);
}
private void DispatchJpsDebug(SharedAiDebug.JpsRouteDebug routeDebug)
{
var mapManager = IoCManager.Resolve<IMapManager>();
var route = new List<Vector2>();
foreach (var tile in routeDebug.Route)
{
var tileGrid = mapManager.GetGrid(tile.GridIndex).GridTileToLocal(tile.GridIndices);
route.Add(tileGrid.ToMapPos(EntityManager));
}
var jumpNodes = new List<Vector2>();
foreach (var tile in routeDebug.JumpNodes)
{
var tileGrid = mapManager.GetGrid(tile.GridIndex).GridTileToLocal(tile.GridIndices);
jumpNodes.Add(tileGrid.ToMapPos(EntityManager));
}
var systemMessage = new SharedAiDebug.JpsRouteMessage(
routeDebug.EntityUid,
route,
jumpNodes,
routeDebug.TimeTaken
);
RaiseNetworkEvent(systemMessage);
}
private void DispatchGraph(SharedAiDebug.RequestPathfindingGraphMessage message)
{
var pathfindingSystem = EntitySystemManager.GetEntitySystem<PathfindingSystem>();
var mapManager = IoCManager.Resolve<IMapManager>();
var result = new Dictionary<int, List<Vector2>>();
var idx = 0;
foreach (var (gridId, chunks) in pathfindingSystem.Graph)
{
var gridManager = mapManager.GetGrid(gridId);
foreach (var chunk in chunks.Values)
{
var nodes = new List<Vector2>();
foreach (var node in chunk.Nodes)
{
var worldTile = gridManager.GridTileToWorldPos(node.TileRef.GridIndices);
nodes.Add(worldTile);
}
result.Add(idx, nodes);
idx++;
}
}
var systemMessage = new SharedAiDebug.PathfindingGraphMessage(result);
RaiseNetworkEvent(systemMessage);
}
}
#endif
}

View File

@@ -1,30 +0,0 @@
using Content.Server.GameObjects.EntitySystems.AI.LoadBalancer;
using Content.Shared.AI;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems.AI
{
#if DEBUG
[UsedImplicitly]
public class ServerAiDebugSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
AiActionRequestJob.FoundAction += NotifyActionJob;
}
public override void Shutdown()
{
base.Shutdown();
AiActionRequestJob.FoundAction -= NotifyActionJob;
}
private void NotifyActionJob(SharedAiDebug.UtilityAiDebugMessage message)
{
RaiseNetworkEvent(message);
}
}
#endif
}

View File

@@ -1,24 +0,0 @@
using Content.Server.GameObjects.Components.Movement;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems.AI
{
/// <summary>
/// Indicates whether an AI should be updated by the AiSystem or not.
/// Useful to sleep AI when they die or otherwise should be inactive.
/// </summary>
internal sealed class SleepAiMessage : EntityEventArgs
{
/// <summary>
/// Sleep or awake.
/// </summary>
public bool Sleep { get; }
public AiControllerComponent Component { get; }
public SleepAiMessage(AiControllerComponent component, bool sleep)
{
Component = component;
Sleep = sleep;
}
}
}

View File

@@ -1,717 +0,0 @@
using System;
using System.Collections.Generic;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Access;
using Content.Server.GameObjects.Components.Movement;
using Content.Server.GameObjects.EntitySystems.AI.Pathfinding;
using Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders;
using Content.Server.GameObjects.EntitySystems.JobQueues;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.Utility;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.EntitySystems.AI.Steering
{
public sealed class AiSteeringSystem : EntitySystem
{
// http://www.red3d.com/cwr/papers/1999/gdc99steer.html for a steering overview
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPauseManager _pauseManager = default!;
private PathfindingSystem _pathfindingSystem = default!;
/// <summary>
/// Whether we try to avoid non-blocking physics objects
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool CollisionAvoidanceEnabled { get; set; } = true;
/// <summary>
/// How close we need to get to the center of each tile
/// </summary>
private const float TileTolerance = 0.8f;
/// <summary>
/// How long to wait between checks (if necessary).
/// </summary>
private const float InRangeUnobstructedCooldown = 0.25f;
private Dictionary<IEntity, IAiSteeringRequest> RunningAgents => _agentLists[_listIndex];
// We'll cycle the running list every tick as all we're doing is getting a vector2 for the
// agent's steering. Should help a lot given this is the most expensive operator by far.
// The AI will keep moving, it's just it'll keep moving in its existing direction.
// If we change to 20/30 TPS you might want to change this but for now it's fine
private readonly List<Dictionary<IEntity, IAiSteeringRequest>> _agentLists = new(AgentListCount);
private const int AgentListCount = 2;
private int _listIndex;
// Cache nextGrid
private readonly Dictionary<IEntity, EntityCoordinates> _nextGrid = new();
/// <summary>
/// Current live paths for AI
/// </summary>
private readonly Dictionary<IEntity, Queue<TileRef>> _paths = new();
/// <summary>
/// Pathfinding request jobs we're waiting on
/// </summary>
private readonly Dictionary<IEntity, (CancellationTokenSource CancelToken, Job<Queue<TileRef>> Job)> _pathfindingRequests =
new();
/// <summary>
/// Keep track of how long we've been in 1 position and re-path if it's been too long
/// </summary>
private readonly Dictionary<IEntity, int> _stuckCounter = new();
/// <summary>
/// Get a fixed position for the target entity; if they move then re-path
/// </summary>
private readonly Dictionary<IEntity, EntityCoordinates> _entityTargetPosition = new();
// Anti-Stuck
// Given the collision avoidance can lead to twitching need to store a reference position and check if we've been near this too long
private readonly Dictionary<IEntity, EntityCoordinates> _stuckPositions = new();
public override void Initialize()
{
base.Initialize();
_pathfindingSystem = Get<PathfindingSystem>();
for (var i = 0; i < AgentListCount; i++)
{
_agentLists.Add(new Dictionary<IEntity, IAiSteeringRequest>());
}
}
/// <summary>
/// Adds the AI to the steering system to move towards a specific target
/// </summary>
/// We'll add it to the movement list that has the least number of agents
/// <param name="entity"></param>
/// <param name="steeringRequest"></param>
public void Register(IEntity entity, IAiSteeringRequest steeringRequest)
{
var lowestListCount = 1000;
var lowestListIndex = 0;
for (var i = 0; i < _agentLists.Count; i++)
{
var agentList = _agentLists[i];
// Register shouldn't be called twice; if it is then someone dun fucked up
DebugTools.Assert(!agentList.ContainsKey(entity));
if (agentList.Count < lowestListCount)
{
lowestListCount = agentList.Count;
lowestListIndex = i;
}
}
_agentLists[lowestListIndex].Add(entity, steeringRequest);
}
/// <summary>
/// Stops the steering behavior for the AI and cleans up
/// </summary>
/// <param name="entity"></param>
/// <exception cref="InvalidOperationException"></exception>
public void Unregister(IEntity entity)
{
if (entity.TryGetComponent(out AiControllerComponent? controller))
{
controller.VelocityDir = Vector2.Zero;
}
if (_pathfindingRequests.TryGetValue(entity, out var request))
{
switch (request.Job.Status)
{
case JobStatus.Pending:
case JobStatus.Finished:
break;
case JobStatus.Running:
case JobStatus.Paused:
case JobStatus.Waiting:
request.CancelToken.Cancel();
break;
}
switch (request.Job.Exception)
{
case null:
break;
default:
ExceptionDispatchInfo.Capture(request.Job.Exception).Throw();
throw request.Job.Exception;
}
_pathfindingRequests.Remove(entity);
}
if (_paths.ContainsKey(entity))
{
_paths.Remove(entity);
}
if (_nextGrid.ContainsKey(entity))
{
_nextGrid.Remove(entity);
}
if (_stuckCounter.ContainsKey(entity))
{
_stuckCounter.Remove(entity);
}
if (_entityTargetPosition.ContainsKey(entity))
{
_entityTargetPosition.Remove(entity);
}
foreach (var agentList in _agentLists)
{
if (agentList.ContainsKey(entity))
{
agentList.Remove(entity);
return;
}
}
}
/// <summary>
/// Is the entity currently registered for steering?
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
public bool IsRegistered(IEntity entity)
{
foreach (var agentList in _agentLists)
{
if (agentList.ContainsKey(entity))
{
return true;
}
}
return false;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var (agent, steering) in RunningAgents)
{
// Yeah look it's not true frametime but good enough.
var result = Steer(agent, steering, frameTime * RunningAgents.Count);
steering.Status = result;
switch (result)
{
case SteeringStatus.Pending:
break;
case SteeringStatus.NoPath:
Unregister(agent);
break;
case SteeringStatus.Arrived:
Unregister(agent);
break;
case SteeringStatus.Moving:
break;
default:
throw new ArgumentOutOfRangeException();
}
}
_listIndex = (_listIndex + 1) % _agentLists.Count;
}
/// <summary>
/// Go through each steerer and combine their vectors
/// </summary>
/// <param name="entity"></param>
/// <param name="steeringRequest"></param>
/// <param name="frameTime"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
private SteeringStatus Steer(IEntity entity, IAiSteeringRequest steeringRequest, float frameTime)
{
// Main optimisation to be done below is the redundant calls and adding more variables
if (entity.Deleted ||
!entity.TryGetComponent(out AiControllerComponent? controller) ||
!ActionBlockerSystem.CanMove(entity) ||
!entity.Transform.GridID.IsValid())
{
return SteeringStatus.NoPath;
}
var entitySteering = steeringRequest as EntityTargetSteeringRequest;
if (entitySteering != null && entitySteering.Target.Deleted)
{
controller.VelocityDir = Vector2.Zero;
return SteeringStatus.NoPath;
}
if (_pauseManager.IsGridPaused(entity.Transform.GridID))
{
controller.VelocityDir = Vector2.Zero;
return SteeringStatus.Pending;
}
// Validation
// Check if we can even arrive -> Currently only samegrid movement supported
if (entity.Transform.GridID != steeringRequest.TargetGrid.GetGridId(EntityManager))
{
controller.VelocityDir = Vector2.Zero;
return SteeringStatus.NoPath;
}
// Check if we have arrived
var targetDistance = (entity.Transform.MapPosition.Position - steeringRequest.TargetMap.Position).Length;
steeringRequest.TimeUntilInteractionCheck -= frameTime;
if (targetDistance <= steeringRequest.ArrivalDistance && steeringRequest.TimeUntilInteractionCheck <= 0.0f)
{
if (!steeringRequest.RequiresInRangeUnobstructed ||
entity.InRangeUnobstructed(steeringRequest.TargetMap, steeringRequest.ArrivalDistance, popup: true))
{
// TODO: Need cruder LOS checks for ranged weaps
controller.VelocityDir = Vector2.Zero;
return SteeringStatus.Arrived;
}
steeringRequest.TimeUntilInteractionCheck = InRangeUnobstructedCooldown;
// Welp, we'll keep on moving.
}
// If we're really close don't swiggity swoogity back and forth and just wait for the interaction check maybe?
if (steeringRequest.TimeUntilInteractionCheck > 0.0f && targetDistance <= 0.1f)
{
controller.VelocityDir = Vector2.Zero;
return SteeringStatus.Moving;
}
// Handle pathfinding job
// If we still have an existing path then keep following that until the new path arrives
if (_pathfindingRequests.TryGetValue(entity, out var pathRequest) && pathRequest.Job.Status == JobStatus.Finished)
{
switch (pathRequest.Job.Exception)
{
case null:
break;
// Currently nothing should be cancelling these except external factors
case TaskCanceledException _:
controller.VelocityDir = Vector2.Zero;
return SteeringStatus.NoPath;
default:
throw pathRequest.Job.Exception;
}
// No actual path
var path = _pathfindingRequests[entity].Job.Result;
if (path == null || path.Count == 0)
{
controller.VelocityDir = Vector2.Zero;
return SteeringStatus.NoPath;
}
// If we're closer to next tile then we don't want to walk backwards to our tile's center
UpdatePath(entity, path);
// If we're targeting entity get a fixed tile; if they move from it then re-path (at least til we get a better solution)
if (entitySteering != null)
{
_entityTargetPosition[entity] = entitySteering.TargetGrid;
}
// Move next tick
return SteeringStatus.Pending;
}
// Check if we even have a path to follow
// If the route's empty we could be close and may not need a re-path so we won't check if it is
if (!_paths.ContainsKey(entity) && !_pathfindingRequests.ContainsKey(entity) && targetDistance > 1.5f)
{
controller.VelocityDir = Vector2.Zero;
RequestPath(entity, steeringRequest);
return SteeringStatus.Pending;
}
var ignoredCollision = new List<IEntity>();
// Check if the target entity has moved - If so then re-path
// TODO: Patch the path from the target's position back towards us, stopping if it ever intersects the current path
// Probably need a separate "PatchPath" job
if (entitySteering != null)
{
// Check if target's moved too far
if (_entityTargetPosition.TryGetValue(entity, out var targetGrid) &&
(entitySteering.TargetGrid.Position - targetGrid.Position).Length >= entitySteering.TargetMaxMove)
{
// We'll just repath and keep following the existing one until we get a new one
RequestPath(entity, steeringRequest);
}
ignoredCollision.Add(entitySteering.Target);
}
HandleStuck(entity);
// TODO: Probably need a dedicated queuing solver (doorway congestion FML)
// Get the target grid (either next tile or target itself) and pass it in to the steering behaviors
// If there's nowhere to go then just stop and wait
var nextGrid = NextGrid(entity, steeringRequest);
if (!nextGrid.HasValue)
{
controller.VelocityDir = Vector2.Zero;
return SteeringStatus.NoPath;
}
// Validate that we can even get to the next grid (could probably just check if we can use nextTile if we're not near the target grid)
if (!_pathfindingSystem.CanTraverse(entity, nextGrid.Value))
{
controller.VelocityDir = Vector2.Zero;
return SteeringStatus.NoPath;
}
// Now we can /finally/ move
var movementVector = Vector2.Zero;
// Originally I tried using interface steerers but ehhh each one kind of needs to do its own thing
// Plus there's not much point putting these in a separate class
// Each one just adds onto the final vector
movementVector += Seek(entity, nextGrid.Value);
if (CollisionAvoidanceEnabled)
{
movementVector += CollisionAvoidance(entity, movementVector, ignoredCollision);
}
// Group behaviors would also go here e.g. separation, cohesion, alignment
// Move towards it
DebugTools.Assert(movementVector != new Vector2(float.NaN, float.NaN));
controller.VelocityDir = movementVector.Normalized;
return SteeringStatus.Moving;
}
/// <summary>
/// Get a new job from the pathfindingsystem
/// </summary>
/// <param name="entity"></param>
/// <param name="steeringRequest"></param>
private void RequestPath(IEntity entity, IAiSteeringRequest steeringRequest)
{
if (_pathfindingRequests.ContainsKey(entity))
{
return;
}
var cancelToken = new CancellationTokenSource();
var gridManager = _mapManager.GetGrid(entity.Transform.GridID);
var startTile = gridManager.GetTileRef(entity.Transform.Coordinates);
var endTile = gridManager.GetTileRef(steeringRequest.TargetGrid);
var collisionMask = 0;
if (entity.TryGetComponent(out IPhysBody? physics))
{
collisionMask = physics.CollisionMask;
}
var access = AccessReader.FindAccessTags(entity);
var job = _pathfindingSystem.RequestPath(new PathfindingArgs(
entity.Uid,
access,
collisionMask,
startTile,
endTile,
steeringRequest.PathfindingProximity
), cancelToken.Token);
_pathfindingRequests.Add(entity, (cancelToken, job));
}
/// <summary>
/// Given the pathfinding is timesliced we need to trim the first few(?) tiles so we don't walk backwards
/// </summary>
/// <param name="entity"></param>
/// <param name="path"></param>
private void UpdatePath(IEntity entity, Queue<TileRef> path)
{
_pathfindingRequests.Remove(entity);
var entityTile = _mapManager.GetGrid(entity.Transform.GridID).GetTileRef(entity.Transform.Coordinates);
var tile = path.Dequeue();
var closestDistance = PathfindingHelpers.OctileDistance(entityTile, tile);
for (var i = 0; i < path.Count; i++)
{
tile = path.Peek();
var distance = PathfindingHelpers.OctileDistance(entityTile, tile);
if (distance < closestDistance)
{
path.Dequeue();
}
else
{
break;
}
}
_paths[entity] = path;
}
/// <summary>
/// Get the next tile as EntityCoordinates
/// </summary>
/// <param name="entity"></param>
/// <param name="steeringRequest"></param>
/// <returns></returns>
private EntityCoordinates? NextGrid(IEntity entity, IAiSteeringRequest steeringRequest)
{
// Remove the cached grid
if (!_paths.ContainsKey(entity) && _nextGrid.ContainsKey(entity))
{
_nextGrid.Remove(entity);
}
// If no tiles left just move towards the target (if we're close)
if (!_paths.ContainsKey(entity) || _paths[entity].Count == 0)
{
if ((steeringRequest.TargetGrid.Position - entity.Transform.Coordinates.Position).Length <= 2.0f)
{
return steeringRequest.TargetGrid;
}
// Too far so we need a re-path
return null;
}
if (!_nextGrid.TryGetValue(entity, out var nextGrid) ||
(nextGrid.Position - entity.Transform.Coordinates.Position).Length <= TileTolerance)
{
UpdateGridCache(entity);
nextGrid = _nextGrid[entity];
}
DebugTools.Assert(nextGrid != default);
return nextGrid;
}
/// <summary>
/// Rather than converting TileRef to EntityCoordinates over and over we'll just cache it
/// </summary>
/// <param name="entity"></param>
/// <param name="dequeue"></param>
private void UpdateGridCache(IEntity entity, bool dequeue = true)
{
if (_paths[entity].Count == 0) return;
var nextTile = dequeue ? _paths[entity].Dequeue() : _paths[entity].Peek();
var nextGrid = _mapManager.GetGrid(entity.Transform.GridID).GridTileToLocal(nextTile.GridIndices);
_nextGrid[entity] = nextGrid;
}
/// <summary>
/// Check if we've been near our last EntityCoordinates too long and try to fix it
/// </summary>
/// <param name="entity"></param>
private void HandleStuck(IEntity entity)
{
if (!_stuckPositions.TryGetValue(entity, out var stuckPosition))
{
_stuckPositions[entity] = entity.Transform.Coordinates;
_stuckCounter[entity] = 0;
return;
}
if ((entity.Transform.Coordinates.Position - stuckPosition.Position).Length <= 1.0f)
{
_stuckCounter.TryGetValue(entity, out var stuckCount);
_stuckCounter[entity] = stuckCount + 1;
}
else
{
// No longer stuck
_stuckPositions[entity] = entity.Transform.Coordinates;
_stuckCounter[entity] = 0;
return;
}
// Should probably be time-based
if (_stuckCounter[entity] < 30)
{
return;
}
// Okay now we're stuck
_paths.Remove(entity);
_stuckCounter[entity] = 0;
}
#region Steering
/// <summary>
/// Move straight to target position
/// </summary>
/// <param name="entity"></param>
/// <param name="grid"></param>
/// <returns></returns>
private Vector2 Seek(IEntity entity, EntityCoordinates grid)
{
// is-even much
var entityPos = entity.Transform.Coordinates;
return entityPos == grid
? Vector2.Zero
: (grid.Position - entityPos.Position).Normalized;
}
/// <summary>
/// Like Seek but slows down when within distance
/// </summary>
/// <param name="entity"></param>
/// <param name="grid"></param>
/// <param name="slowingDistance"></param>
/// <returns></returns>
private Vector2 Arrival(IEntity entity, EntityCoordinates grid, float slowingDistance = 1.0f)
{
var entityPos = entity.Transform.Coordinates;
DebugTools.Assert(slowingDistance > 0.0f);
if (entityPos == grid)
{
return Vector2.Zero;
}
var targetDiff = grid.Position - entityPos.Position;
var rampedSpeed = targetDiff.Length / slowingDistance;
return targetDiff.Normalized * MathF.Min(1.0f, rampedSpeed);
}
/// <summary>
/// Like Seek but predicts target's future position
/// </summary>
/// <param name="entity"></param>
/// <param name="target"></param>
/// <returns></returns>
private Vector2 Pursuit(IEntity entity, IEntity target)
{
var entityPos = entity.Transform.Coordinates;
var targetPos = target.Transform.Coordinates;
if (entityPos == targetPos)
{
return Vector2.Zero;
}
if (target.TryGetComponent(out IPhysBody? physics))
{
var targetDistance = (targetPos.Position - entityPos.Position);
targetPos = targetPos.Offset(physics.LinearVelocity * targetDistance);
}
return (targetPos.Position - entityPos.Position).Normalized;
}
/// <summary>
/// Checks for non-anchored physics objects that can block us
/// </summary>
/// <param name="entity"></param>
/// <param name="direction">entity's travel direction</param>
/// <param name="ignoredTargets"></param>
/// <returns></returns>
private Vector2 CollisionAvoidance(IEntity entity, Vector2 direction, ICollection<IEntity> ignoredTargets)
{
if (direction == Vector2.Zero || !entity.TryGetComponent(out IPhysBody? physics))
{
return Vector2.Zero;
}
// We'll check tile-by-tile
// Rewriting this frequently so not many comments as they'll go stale
// I realise this is bad so please rewrite it ;-;
var entityCollisionMask = physics.CollisionMask;
var avoidanceVector = Vector2.Zero;
var checkTiles = new HashSet<TileRef>();
var avoidTiles = new HashSet<TileRef>();
var entityGridCoords = entity.Transform.Coordinates;
var grid = _mapManager.GetGrid(entity.Transform.GridID);
var currentTile = grid.GetTileRef(entityGridCoords);
var halfwayTile = grid.GetTileRef(entityGridCoords.Offset(direction / 2));
var nextTile = grid.GetTileRef(entityGridCoords.Offset(direction));
checkTiles.Add(currentTile);
checkTiles.Add(halfwayTile);
checkTiles.Add(nextTile);
// Handling corners with collision avoidance is a real bitch
// TBH collision avoidance in general that doesn't run like arse is a real bitch
foreach (var tile in checkTiles)
{
var node = _pathfindingSystem.GetNode(tile);
// Assume the immovables have already been checked
foreach (var (physicsEntity, layer) in node.PhysicsLayers)
{
// Ignore myself / my target if applicable / if my mask doesn't collide
if (physicsEntity == entity || ignoredTargets.Contains(physicsEntity) || (entityCollisionMask & layer) == 0) continue;
// God there's so many ways to do this
// err for now we'll just assume the first entity is the center and just add a vector for it
//Pathfinding updates are deferred so this may not be done yet.
if (physicsEntity.Deleted) continue;
// if we're moving in the same direction then ignore
// So if 2 entities are moving towards each other and both detect a collision they'll both move in the same direction
// i.e. towards the right
if (physicsEntity.TryGetComponent(out IPhysBody? otherPhysics) &&
Vector2.Dot(otherPhysics.LinearVelocity, direction) > 0)
{
continue;
}
var centerGrid = physicsEntity.Transform.Coordinates;
// Check how close we are to center of tile and get the inverse; if we're closer this is stronger
var additionalVector = (centerGrid.Position - entityGridCoords.Position);
var distance = additionalVector.Length;
// If we're too far no point, if we're close then cap it at the normalized vector
distance = MathHelper.Clamp(2.5f - distance, 0.0f, 1.0f);
additionalVector = new Angle(90 * distance).RotateVec(additionalVector);
avoidanceVector += additionalVector;
// if we do need to avoid that means we'll have to lookahead for the next tile
avoidTiles.Add(tile);
break;
}
}
// Dis ugly
if (_paths.TryGetValue(entity, out var path))
{
if (path.Count > 0)
{
var checkTile = path.Peek();
for (var i = 0; i < Math.Min(path.Count, avoidTiles.Count); i++)
{
if (avoidTiles.Contains(checkTile))
{
checkTile = path.Dequeue();
}
}
UpdateGridCache(entity, false);
}
}
return avoidanceVector == Vector2.Zero ? avoidanceVector : avoidanceVector.Normalized;
}
#endregion
}
public enum SteeringStatus
{
Pending,
NoPath,
Arrived,
Moving,
}
}

View File

@@ -1,41 +0,0 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
namespace Content.Server.GameObjects.EntitySystems.AI.Steering
{
public sealed class EntityTargetSteeringRequest : IAiSteeringRequest
{
public SteeringStatus Status { get; set; } = SteeringStatus.Pending;
public MapCoordinates TargetMap => _target.Transform.MapPosition;
public EntityCoordinates TargetGrid => _target.Transform.Coordinates;
public IEntity Target => _target;
private readonly IEntity _target;
/// <inheritdoc />
public float ArrivalDistance { get; }
/// <inheritdoc />
public float PathfindingProximity { get; }
/// <summary>
/// How far the target can move before we re-path
/// </summary>
public float TargetMaxMove { get; } = 1.5f;
/// <inheritdoc />
public bool RequiresInRangeUnobstructed { get; }
/// <summary>
/// To avoid spamming InRangeUnobstructed we'll apply a cd to it.
/// </summary>
public float TimeUntilInteractionCheck { get; set; }
public EntityTargetSteeringRequest(IEntity target, float arrivalDistance, float pathfindingProximity = 0.5f, bool requiresInRangeUnobstructed = false)
{
_target = target;
ArrivalDistance = arrivalDistance;
PathfindingProximity = pathfindingProximity;
RequiresInRangeUnobstructed = requiresInRangeUnobstructed;
}
}
}

View File

@@ -1,33 +0,0 @@
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
namespace Content.Server.GameObjects.EntitySystems.AI.Steering
{
public sealed class GridTargetSteeringRequest : IAiSteeringRequest
{
public SteeringStatus Status { get; set; } = SteeringStatus.Pending;
public MapCoordinates TargetMap { get; }
public EntityCoordinates TargetGrid { get; }
/// <inheritdoc />
public float ArrivalDistance { get; }
/// <inheritdoc />
public float PathfindingProximity { get; }
public bool RequiresInRangeUnobstructed { get; }
public float TimeUntilInteractionCheck { get; set; } = 0.0f;
public GridTargetSteeringRequest(EntityCoordinates targetGrid, float arrivalDistance, float pathfindingProximity = 0.5f, bool requiresInRangeUnobstructed = false)
{
// Get it once up front so we the manager doesn't have to continuously get it
var entityManager = IoCManager.Resolve<IEntityManager>();
TargetMap = targetGrid.ToMap(entityManager);
TargetGrid = targetGrid;
ArrivalDistance = arrivalDistance;
PathfindingProximity = pathfindingProximity;
RequiresInRangeUnobstructed = requiresInRangeUnobstructed;
}
}
}

View File

@@ -1,30 +0,0 @@
using Robust.Shared.Map;
namespace Content.Server.GameObjects.EntitySystems.AI.Steering
{
public interface IAiSteeringRequest
{
SteeringStatus Status { get; set; }
MapCoordinates TargetMap { get; }
EntityCoordinates TargetGrid { get; }
/// <summary>
/// How close we have to get before we've arrived
/// </summary>
float ArrivalDistance { get; }
/// <summary>
/// How close the pathfinder needs to get. Typically you want this set lower than ArrivalDistance
/// </summary>
float PathfindingProximity { get; }
/// <summary>
/// If we need LOS on the entity first before interaction
/// </summary>
bool RequiresInRangeUnobstructed { get; }
/// <summary>
/// To avoid spamming InRangeUnobstructed we'll apply a cd to it.
/// </summary>
public float TimeUntilInteractionCheck { get; set; }
}
}

View File

@@ -1,27 +0,0 @@
using Content.Server.GameObjects.Components.Power.AME;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
public class AntimatterEngineSystem : EntitySystem
{
private float _accumulatedFrameTime;
public override void Update(float frameTime)
{
base.Update(frameTime);
_accumulatedFrameTime += frameTime;
if (_accumulatedFrameTime >= 10)
{
foreach (var comp in ComponentManager.EntityQuery<AMEControllerComponent>(true))
{
comp.OnUpdate(frameTime);
}
_accumulatedFrameTime -= 10;
}
}
}
}

View File

@@ -1,46 +0,0 @@
#nullable enable
using Content.Server.GameObjects.Components.NodeContainer.NodeGroups;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using System.Collections.Generic;
using Content.Shared.GameTicking;
using Robust.Shared.Timing;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
internal sealed class ApcNetSystem : EntitySystem, IResettingEntitySystem
{
[Dependency] private readonly IPauseManager _pauseManager = default!;
private HashSet<IApcNet> _apcNets = new();
public override void Update(float frameTime)
{
foreach (var apcNet in _apcNets)
{
var gridId = apcNet.GridId;
if (gridId != null && !_pauseManager.IsGridPaused(gridId.Value))
apcNet.Update(frameTime);
}
}
public void AddApcNet(ApcNetNodeGroup apcNet)
{
_apcNets.Add(apcNet);
}
public void RemoveApcNet(ApcNetNodeGroup apcNet)
{
_apcNets.Remove(apcNet);
}
public void Reset()
{
// NodeGroupSystem does not remake ApcNets affected during restarting until a frame later,
// when their grid is invalid. So, we are clearing them on round restart.
_apcNets.Clear();
}
}
}

View File

@@ -5,7 +5,6 @@ using System.Linq;
using Content.Server.Atmos;
using Content.Server.Atmos.Reactions;
using Content.Server.GameObjects.Components.Atmos;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Shared;
using Content.Shared.Atmos;
using Content.Shared.GameObjects.EntitySystems.Atmos;

View File

@@ -1,85 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Content.Server.GameObjects.Components.Arcade;
using Content.Shared.Arcade;
using Robust.Shared.GameObjects;
using Robust.Shared.Utility;
namespace Content.Server.GameObjects.EntitySystems
{
// ReSharper disable once ClassNeverInstantiated.Global
public class BlockGameSystem : EntitySystem
{
private readonly List<BlockGameMessages.HighScoreEntry> _roundHighscores = new();
private readonly List<BlockGameMessages.HighScoreEntry> _globalHighscores = new();
public HighScorePlacement RegisterHighScore(string name, int score)
{
var entry = new BlockGameMessages.HighScoreEntry(name, score);
return new HighScorePlacement(TryInsertIntoList(_roundHighscores, entry), TryInsertIntoList(_globalHighscores, entry));
}
public List<BlockGameMessages.HighScoreEntry> GetLocalHighscores() => GetSortedHighscores(_roundHighscores);
public List<BlockGameMessages.HighScoreEntry> GetGlobalHighscores() => GetSortedHighscores(_globalHighscores);
private List<BlockGameMessages.HighScoreEntry> GetSortedHighscores(List<BlockGameMessages.HighScoreEntry> highScoreEntries)
{
var result = highScoreEntries.ShallowClone();
result.Sort((p1, p2) => p2.Score.CompareTo(p1.Score));
return result;
}
private int? TryInsertIntoList(List<BlockGameMessages.HighScoreEntry> highScoreEntries, BlockGameMessages.HighScoreEntry entry)
{
if (highScoreEntries.Count < 5)
{
highScoreEntries.Add(entry);
return GetPlacement(highScoreEntries, entry);
}
if (highScoreEntries.Min(e => e.Score) >= entry.Score) return null;
var lowestHighscore = highScoreEntries.Min();
if (lowestHighscore == null) return null;
highScoreEntries.Remove(lowestHighscore);
highScoreEntries.Add(entry);
return GetPlacement(highScoreEntries, entry);
}
private int? GetPlacement(List<BlockGameMessages.HighScoreEntry> highScoreEntries, BlockGameMessages.HighScoreEntry entry)
{
int? placement = null;
if (highScoreEntries.Contains(entry))
{
highScoreEntries.Sort((p1,p2) => p2.Score.CompareTo(p1.Score));
placement = 1 + highScoreEntries.IndexOf(entry);
}
return placement;
}
public override void Update(float frameTime)
{
foreach (var comp in ComponentManager.EntityQuery<BlockGameArcadeComponent>(true))
{
comp.DoGameTick(frameTime);
}
}
public readonly struct HighScorePlacement
{
public readonly int? GlobalPlacement;
public readonly int? LocalPlacement;
public HighScorePlacement(int? globalPlacement, int? localPlacement)
{
GlobalPlacement = globalPlacement;
LocalPlacement = localPlacement;
}
}
}
}

View File

@@ -1,72 +0,0 @@
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Body.Surgery;
using Content.Server.GameObjects.Components.Body.Surgery.Messages;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.GameTicking;
using Content.Shared.Utility;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems.Body.Surgery
{
[UsedImplicitly]
public class SurgeryToolSystem : EntitySystem, IResettingEntitySystem
{
private readonly HashSet<SurgeryToolComponent> _openSurgeryUIs = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SurgeryWindowOpenMessage>(OnSurgeryWindowOpen);
SubscribeLocalEvent<SurgeryWindowCloseMessage>(OnSurgeryWindowClose);
}
public override void Shutdown()
{
base.Shutdown();
UnsubscribeLocalEvent<SurgeryWindowOpenMessage>();
UnsubscribeLocalEvent<SurgeryWindowCloseMessage>();
}
public void Reset()
{
_openSurgeryUIs.Clear();
}
private void OnSurgeryWindowOpen(SurgeryWindowOpenMessage ev)
{
_openSurgeryUIs.Add(ev.Tool);
}
private void OnSurgeryWindowClose(SurgeryWindowCloseMessage ev)
{
_openSurgeryUIs.Remove(ev.Tool);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var tool in _openSurgeryUIs)
{
if (tool.PerformerCache == null)
{
continue;
}
if (tool.BodyCache == null)
{
continue;
}
if (!ActionBlockerSystem.CanInteract(tool.PerformerCache) ||
!tool.PerformerCache.InRangeUnobstructed(tool.BodyCache))
{
tool.CloseAllSurgeryUIs();
}
}
}
}
}

View File

@@ -1,123 +0,0 @@
#nullable enable
using Content.Server.GameObjects.Components.Buckle;
using Content.Server.GameObjects.Components.Strap;
using Content.Server.GameObjects.EntitySystems.Click;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces.GameObjects.Components;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
internal sealed class BuckleSystem : SharedBuckleSystem
{
public override void Initialize()
{
base.Initialize();
UpdatesAfter.Add(typeof(InteractionSystem));
UpdatesAfter.Add(typeof(InputSystem));
SubscribeLocalEvent<BuckleComponent, MoveEvent>(MoveEvent);
SubscribeLocalEvent<StrapComponent, RotateEvent>(RotateEvent);
SubscribeLocalEvent<BuckleComponent, EntInsertedIntoContainerMessage>(ContainerModifiedBuckle);
SubscribeLocalEvent<StrapComponent, EntInsertedIntoContainerMessage>(ContainerModifiedStrap);
SubscribeLocalEvent<BuckleComponent, EntRemovedFromContainerMessage>(ContainerModifiedBuckle);
SubscribeLocalEvent<StrapComponent, EntRemovedFromContainerMessage>(ContainerModifiedStrap);
SubscribeLocalEvent<BuckleComponent, InteractHandEvent>(HandleInteractHand);
}
private void HandleInteractHand(EntityUid uid, BuckleComponent component, InteractHandEvent args)
{
args.Handled = component.TryUnbuckle(args.User);
}
public override void Update(float frameTime)
{
foreach (var (buckle, physics) in ComponentManager.EntityQuery<BuckleComponent, PhysicsComponent>())
{
buckle.Update(physics);
}
}
private void MoveEvent(EntityUid uid, BuckleComponent buckle, MoveEvent ev)
{
var strap = buckle.BuckledTo;
if (strap == null)
{
return;
}
var strapPosition = strap.Owner.Transform.Coordinates.Offset(buckle.BuckleOffset);
if (ev.NewPosition.InRange(EntityManager, strapPosition, 0.2f))
{
return;
}
buckle.TryUnbuckle(buckle.Owner, true);
}
private void RotateEvent(EntityUid uid, StrapComponent strap, RotateEvent ev)
{
// On rotation of a strap, reattach all buckled entities.
// This fixes buckle offsets and draw depths.
foreach (var buckledEntity in strap.BuckledEntities)
{
if (!buckledEntity.TryGetComponent(out BuckleComponent? buckled))
{
continue;
}
buckled.ReAttach(strap);
buckled.Dirty();
}
}
private void ContainerModifiedBuckle(EntityUid uid, BuckleComponent buckle, ContainerModifiedMessage message)
{
ContainerModifiedReAttach(buckle, buckle.BuckledTo);
}
private void ContainerModifiedStrap(EntityUid uid, StrapComponent strap, ContainerModifiedMessage message)
{
foreach (var buckledEntity in strap.BuckledEntities)
{
if (!buckledEntity.TryGetComponent(out BuckleComponent? buckled))
{
continue;
}
ContainerModifiedReAttach(buckled, strap);
}
}
private void ContainerModifiedReAttach(BuckleComponent buckle, StrapComponent? strap)
{
if (strap == null)
{
return;
}
var contained = buckle.Owner.TryGetContainer(out var ownContainer);
var strapContained = strap.Owner.TryGetContainer(out var strapContainer);
if (contained != strapContained || ownContainer != strapContainer)
{
buckle.TryUnbuckle(buckle.Owner, true);
return;
}
if (!contained)
{
buckle.ReAttach(strap);
}
}
}
}

View File

@@ -1,209 +0,0 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Content.Server.Cargo;
using Content.Server.GameObjects.Components.Cargo;
using Content.Shared.GameTicking;
using Content.Shared.Prototypes.Cargo;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
public class CargoConsoleSystem : EntitySystem, IResettingEntitySystem
{
/// <summary>
/// How much time to wait (in seconds) before increasing bank accounts balance.
/// </summary>
private const float Delay = 10f;
/// <summary>
/// How many points to give to every bank account every <see cref="Delay"/> seconds.
/// </summary>
private const int PointIncrease = 150;
/// <summary>
/// Keeps track of how much time has elapsed since last balance increase.
/// </summary>
private float _timer;
/// <summary>
/// Stores all bank accounts.
/// </summary>
private readonly Dictionary<int, CargoBankAccount> _accountsDict = new();
private readonly Dictionary<int, CargoOrderDatabase> _databasesDict = new();
/// <summary>
/// Used to assign IDs to bank accounts. Incremental counter.
/// </summary>
private int _accountIndex = 0;
/// <summary>
/// Enumeration of all bank accounts.
/// </summary>
public IEnumerable<CargoBankAccount> BankAccounts => _accountsDict.Values;
/// <summary>
/// The station's bank account.
/// </summary>
public CargoBankAccount StationAccount => GetBankAccount(0);
public CargoOrderDatabase StationOrderDatabase => GetOrderDatabase(0);
public override void Initialize()
{
CreateBankAccount("Space Station 14", 1000);
CreateOrderDatabase(0);
}
public override void Update(float frameTime)
{
_timer += frameTime;
if (_timer < Delay)
{
return;
}
_timer -= Delay;
foreach (var account in BankAccounts)
{
account.Balance += PointIncrease;
}
}
public void Reset()
{
_accountsDict.Clear();
_databasesDict.Clear();
_timer = 0;
_accountIndex = 0;
Initialize();
}
/// <summary>
/// Creates a new bank account.
/// </summary>
public void CreateBankAccount(string name, int balance)
{
var account = new CargoBankAccount(_accountIndex, name, balance);
_accountsDict.Add(_accountIndex, account);
_accountIndex += 1;
}
public void CreateOrderDatabase(int id)
{
_databasesDict.Add(id, new CargoOrderDatabase(id));
}
/// <summary>
/// Returns the bank account associated with the given ID.
/// </summary>
public CargoBankAccount GetBankAccount(int id)
{
return _accountsDict[id];
}
public CargoOrderDatabase GetOrderDatabase(int id)
{
return _databasesDict[id];
}
/// <summary>
/// Returns whether the account exists, eventually passing the account in the out parameter.
/// </summary>
public bool TryGetBankAccount(int id, [NotNullWhen(true)] out CargoBankAccount? account)
{
return _accountsDict.TryGetValue(id, out account);
}
public bool TryGetOrderDatabase(int id, [NotNullWhen(true)] out CargoOrderDatabase? database)
{
return _databasesDict.TryGetValue(id, out database);
}
/// <summary>
/// Verifies if there is enough money in the account's balance to pay the amount.
/// Returns false if there's no account associated with the given ID
/// or if the balance would end up being negative.
/// </summary>
public bool CheckBalance(int id, int amount)
{
if (!TryGetBankAccount(id, out var account))
{
return false;
}
if (account.Balance + amount < 0)
{
return false;
}
return true;
}
/// <summary>
/// Attempts to change the given account's balance.
/// Returns false if there's no account associated with the given ID
/// or if the balance would end up being negative.
/// </summary>
public bool ChangeBalance(int id, int amount)
{
if (!TryGetBankAccount(id, out var account))
{
return false;
}
account.Balance += amount;
return true;
}
public bool AddOrder(int id, string requester, string reason, string productId, int amount, int payingAccountId)
{
if (amount < 1 || !TryGetOrderDatabase(id, out var database) || amount > database.MaxOrderSize)
{
return false;
}
database.AddOrder(requester, reason, productId, amount, payingAccountId);
SyncComponentsWithId(id);
return true;
}
public bool RemoveOrder(int id, int orderNumber)
{
if (!TryGetOrderDatabase(id, out var database))
return false;
database.RemoveOrder(orderNumber);
SyncComponentsWithId(id);
return true;
}
public bool ApproveOrder(int id, int orderNumber)
{
if (!TryGetOrderDatabase(id, out var database))
return false;
if (!database.ApproveOrder(orderNumber))
return false;
SyncComponentsWithId(id);
return true;
}
public List<CargoOrderData> RemoveAndGetApprovedOrders(int id)
{
if (!TryGetOrderDatabase(id, out var database))
return new List<CargoOrderData>();
var approvedOrders = database.SpliceApproved();
SyncComponentsWithId(id);
return approvedOrders;
}
public (int CurrentCapacity, int MaxCapacity) GetCapacity(int id)
{
if (!TryGetOrderDatabase(id, out var database))
return (0,0);
return (database.CurrentOrderSize, database.MaxOrderSize);
}
private void SyncComponentsWithId(int id)
{
foreach (var comp in ComponentManager.EntityQuery<CargoOrderDatabaseComponent>(true))
{
if (comp.Database == null || comp.Database.Id != id)
continue;
comp.Dirty();
}
}
}
}

View File

@@ -1,20 +0,0 @@
#nullable enable
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.Player;
namespace Content.Server.GameObjects.EntitySystems.NewFolder
{
public class ChemicalReactionSystem : SharedChemicalReactionSystem
{
protected override void OnReaction(ReactionPrototype reaction, IEntity owner, ReagentUnit unitReactions)
{
base.OnReaction(reaction, owner, unitReactions);
if (reaction.Sound != null)
SoundSystem.Play(Filter.Pvs(owner), reaction.Sound, owner.Transform.Coordinates);
}
}
}

View File

@@ -1,52 +0,0 @@
using Content.Shared.GameObjects.EntitySystemMessages;
using Content.Shared.GameObjects.EntitySystems;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Utility;
namespace Content.Server.GameObjects.EntitySystems.Click
{
[UsedImplicitly]
public class ExamineSystem : ExamineSystemShared
{
private static readonly FormattedMessage _entityNotFoundMessage;
static ExamineSystem()
{
_entityNotFoundMessage = new FormattedMessage();
_entityNotFoundMessage.AddText(Loc.GetString("That entity doesn't exist"));
}
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<ExamineSystemMessages.RequestExamineInfoMessage>(ExamineInfoRequest);
IoCManager.InjectDependencies(this);
}
private void ExamineInfoRequest(ExamineSystemMessages.RequestExamineInfoMessage request, EntitySessionEventArgs eventArgs)
{
var player = (IPlayerSession) eventArgs.SenderSession;
var session = eventArgs.SenderSession;
var playerEnt = session.AttachedEntity;
var channel = player.ConnectedClient;
if (playerEnt == null
|| !EntityManager.TryGetEntity(request.EntityUid, out var entity)
|| !CanExamine(playerEnt, entity))
{
RaiseNetworkEvent(new ExamineSystemMessages.ExamineInfoResponseMessage(
request.EntityUid, _entityNotFoundMessage), channel);
return;
}
var text = GetExamineText(entity, player.AttachedEntity);
RaiseNetworkEvent(new ExamineSystemMessages.ExamineInfoResponseMessage(request.EntityUid, text), channel);
}
}
}

View File

@@ -1,789 +0,0 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Pulling;
using Content.Server.GameObjects.Components.Buckle;
using Content.Server.GameObjects.Components.Timing;
using Content.Server.Interfaces.GameObjects.Components.Items;
using Content.Shared.GameObjects.Components.Inventory;
using Content.Shared.GameObjects.Components.Items;
using Content.Shared.GameObjects.Components.Rotatable;
using Content.Shared.GameObjects.EntitySystemMessages;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.Input;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Utility;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Players;
using Robust.Shared.Random;
using Robust.Shared.Localization;
using Content.Shared.Interfaces;
namespace Content.Server.GameObjects.EntitySystems.Click
{
/// <summary>
/// Governs interactions during clicking on entities
/// </summary>
[UsedImplicitly]
public sealed class InteractionSystem : SharedInteractionSystem
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
public override void Initialize()
{
SubscribeNetworkEvent<DragDropRequestEvent>(HandleDragDropRequestEvent);
CommandBinds.Builder
.Bind(EngineKeyFunctions.Use,
new PointerInputCmdHandler(HandleUseInteraction))
.Bind(ContentKeyFunctions.WideAttack,
new PointerInputCmdHandler(HandleWideAttack))
.Bind(ContentKeyFunctions.ActivateItemInWorld,
new PointerInputCmdHandler(HandleActivateItemInWorld))
.Bind(ContentKeyFunctions.TryPullObject,
new PointerInputCmdHandler(HandleTryPullObject))
.Register<InteractionSystem>();
}
public override void Shutdown()
{
CommandBinds.Unregister<InteractionSystem>();
base.Shutdown();
}
#region Client Input Validation
private bool ValidateClientInput(ICommonSession? session, EntityCoordinates coords, EntityUid uid, [NotNullWhen(true)] out IEntity? userEntity)
{
userEntity = null;
if (!coords.IsValid(_entityManager))
{
Logger.InfoS("system.interaction", $"Invalid Coordinates: client={session}, coords={coords}");
return false;
}
if (uid.IsClientSide())
{
Logger.WarningS("system.interaction",
$"Client sent interaction with client-side entity. Session={session}, Uid={uid}");
return false;
}
userEntity = ((IPlayerSession?) session)?.AttachedEntity;
if (userEntity == null || !userEntity.IsValid())
{
Logger.WarningS("system.interaction",
$"Client sent interaction with no attached entity. Session={session}");
return false;
}
return true;
}
#endregion
#region Drag drop
private void HandleDragDropRequestEvent(DragDropRequestEvent msg, EntitySessionEventArgs args)
{
if (!ValidateClientInput(args.SenderSession, msg.DropLocation, msg.Target, out var userEntity))
{
Logger.InfoS("system.interaction", $"DragDropRequestEvent input validation failed");
return;
}
if (!EntityManager.TryGetEntity(msg.Dropped, out var dropped))
return;
if (!EntityManager.TryGetEntity(msg.Target, out var target))
return;
var interactionArgs = new DragDropEvent(userEntity, msg.DropLocation, dropped, target);
// must be in range of both the target and the object they are drag / dropping
// Client also does this check but ya know we gotta validate it.
if (!interactionArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true))
return;
// trigger dragdrops on the dropped entity
RaiseLocalEvent(dropped.Uid, interactionArgs);
foreach (var dragDrop in dropped.GetAllComponents<IDraggable>())
{
if (dragDrop.CanDrop(interactionArgs) &&
dragDrop.Drop(interactionArgs))
{
return;
}
}
// trigger dragdropons on the targeted entity
RaiseLocalEvent(target.Uid, interactionArgs, false);
foreach (var dragDropOn in target.GetAllComponents<IDragDropOn>())
{
if (dragDropOn.CanDragDropOn(interactionArgs) &&
dragDropOn.DragDropOn(interactionArgs))
{
return;
}
}
}
#endregion
#region ActivateItemInWorld
private bool HandleActivateItemInWorld(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
if (!ValidateClientInput(session, coords, uid, out var user))
{
Logger.InfoS("system.interaction", $"ActivateItemInWorld input validation failed");
return false;
}
if (!EntityManager.TryGetEntity(uid, out var used))
return false;
InteractionActivate(user, used);
return true;
}
/// <summary>
/// Activates the IActivate behavior of an object
/// Verifies that the user is capable of doing the use interaction first
/// </summary>
public void TryInteractionActivate(IEntity? user, IEntity? used)
{
if (user == null || used == null)
return;
InteractionActivate(user, used);
}
private void InteractionActivate(IEntity user, IEntity used)
{
if (!ActionBlockerSystem.CanInteract(user) || ! ActionBlockerSystem.CanUse(user))
return;
// all activates should only fire when in range / unbostructed
if (!InRangeUnobstructed(user, used, ignoreInsideBlocker: true, popup: true))
return;
var activateMsg = new ActivateInWorldEvent(user, used);
RaiseLocalEvent(used.Uid, activateMsg);
if (activateMsg.Handled)
return;
if (!used.TryGetComponent(out IActivate? activateComp))
return;
var activateEventArgs = new ActivateEventArgs(user, used);
activateComp.Activate(activateEventArgs);
}
#endregion
private bool HandleWideAttack(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
// client sanitization
if (!ValidateClientInput(session, coords, uid, out var userEntity))
{
Logger.InfoS("system.interaction", $"WideAttack input validation failed");
return true;
}
if (userEntity.TryGetComponent(out CombatModeComponent? combatMode) && combatMode.IsInCombatMode)
DoAttack(userEntity, coords, true);
return true;
}
/// <summary>
/// Entity will try and use their active hand at the target location.
/// Don't use for players
/// </summary>
/// <param name="entity"></param>
/// <param name="coords"></param>
/// <param name="uid"></param>
internal void AiUseInteraction(IEntity entity, EntityCoordinates coords, EntityUid uid)
{
if (entity.HasComponent<ActorComponent>())
throw new InvalidOperationException();
UserInteraction(entity, coords, uid);
}
public bool HandleUseInteraction(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
// client sanitization
if (!ValidateClientInput(session, coords, uid, out var userEntity))
{
Logger.InfoS("system.interaction", $"Use input validation failed");
return true;
}
UserInteraction(userEntity, coords, uid);
return true;
}
private bool HandleTryPullObject(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
if (!ValidateClientInput(session, coords, uid, out var userEntity))
{
Logger.InfoS("system.interaction", $"TryPullObject input validation failed");
return true;
}
if (userEntity.Uid == uid)
return false;
if (!EntityManager.TryGetEntity(uid, out var pulledObject))
return false;
if (!InRangeUnobstructed(userEntity, pulledObject, popup: true))
return false;
if (!pulledObject.TryGetComponent(out PullableComponent? pull))
return false;
return pull.TogglePull(userEntity);
}
public async void UserInteraction(IEntity user, EntityCoordinates coordinates, EntityUid clickedUid)
{
if (user.TryGetComponent(out CombatModeComponent? combatMode) && combatMode.IsInCombatMode)
{
DoAttack(user, coordinates, false, clickedUid);
return;
}
if (!ValidateInteractAndFace(user, coordinates))
return;
if (!ActionBlockerSystem.CanInteract(user))
return;
// Get entity clicked upon from UID if valid UID, if not assume no entity clicked upon and null
EntityManager.TryGetEntity(clickedUid, out var target);
// Check if interacted entity is in the same container, the direct child, or direct parent of the user.
if (target != null && !user.IsInSameOrParentContainer(target))
{
Logger.WarningS("system.interaction",
$"User entity named {user.Name} clicked on object {target.Name} that isn't the parent, child, or in the same container");
return;
}
// Verify user has a hand, and find what object he is currently holding in his active hand
if (!user.TryGetComponent<IHandsComponent>(out var hands))
return;
var item = hands.GetActiveHand?.Owner;
// TODO: Replace with body interaction range when we get something like arm length or telekinesis or something.
var inRangeUnobstructed = user.InRangeUnobstructed(coordinates, ignoreInsideBlocker: true);
if (target == null || !inRangeUnobstructed)
{
if (item == null)
return;
if (!await InteractUsingRanged(user, item, target, coordinates, inRangeUnobstructed) &&
!inRangeUnobstructed)
{
var message = Loc.GetString("You can't reach there!");
user.PopupMessage(message);
}
return;
}
else
{
// We are close to the nearby object and the object isn't contained in our active hand
// InteractUsing/AfterInteract: We will either use the item on the nearby object
if (item != null)
await InteractUsing(user, item, target, coordinates);
// InteractHand/Activate: Since our hand is empty we will use InteractHand/Activate
else
InteractHand(user, target);
}
}
private bool ValidateInteractAndFace(IEntity user, EntityCoordinates coordinates)
{
// Verify user is on the same map as the entity he clicked on
if (coordinates.GetMapId(_entityManager) != user.Transform.MapID)
{
Logger.WarningS("system.interaction",
$"User entity named {user.Name} clicked on a map he isn't located on");
return false;
}
FaceClickCoordinates(user, coordinates);
return true;
}
private void FaceClickCoordinates(IEntity user, EntityCoordinates coordinates)
{
var diff = coordinates.ToMapPos(EntityManager) - user.Transform.MapPosition.Position;
if (diff.LengthSquared <= 0.01f)
return;
var diffAngle = Angle.FromWorldVec(diff);
if (ActionBlockerSystem.CanChangeDirection(user))
{
user.Transform.WorldRotation = diffAngle;
}
else
{
if (user.TryGetComponent(out BuckleComponent? buckle) && (buckle.BuckledTo != null))
{
// We're buckled to another object. Is that object rotatable?
if (buckle.BuckledTo!.Owner.TryGetComponent(out SharedRotatableComponent? rotatable) && rotatable.RotateWhileAnchored)
{
// Note the assumption that even if unanchored, user can only do spinnychair with an "independent wheel".
// (Since the user being buckled to it holds it down with their weight.)
// This is logically equivalent to RotateWhileAnchored.
// Barstools and office chairs have independent wheels, while regular chairs don't.
rotatable.Owner.Transform.LocalRotation = diffAngle;
}
}
}
}
/// <summary>
/// We didn't click on any entity, try doing an AfterInteract on the click location
/// </summary>
private async Task<bool> InteractDoAfter(IEntity user, IEntity used, IEntity? target, EntityCoordinates clickLocation, bool canReach)
{
var afterInteractEvent = new AfterInteractEvent(user, used, target, clickLocation, canReach);
RaiseLocalEvent(used.Uid, afterInteractEvent, false);
if (afterInteractEvent.Handled)
return true;
var afterInteractEventArgs = new AfterInteractEventArgs(user, clickLocation, target, canReach);
var afterInteracts = used.GetAllComponents<IAfterInteract>().OrderByDescending(x => x.Priority).ToList();
foreach (var afterInteract in afterInteracts)
{
if (await afterInteract.AfterInteract(afterInteractEventArgs))
return true;
}
return false;
}
/// <summary>
/// Uses a item/object on an entity
/// Finds components with the InteractUsing interface and calls their function
/// NOTE: Does not have an InRangeUnobstructed check
/// </summary>
public async Task InteractUsing(IEntity user, IEntity used, IEntity target, EntityCoordinates clickLocation)
{
if (!ActionBlockerSystem.CanInteract(user))
return;
// all interactions should only happen when in range / unobstructed, so no range check is needed
var interactUsingEvent = new InteractUsingEvent(user, used, target, clickLocation);
RaiseLocalEvent(target.Uid, interactUsingEvent);
if (interactUsingEvent.Handled)
return;
var interactUsingEventArgs = new InteractUsingEventArgs(user, clickLocation, used, target);
var interactUsings = target.GetAllComponents<IInteractUsing>().OrderByDescending(x => x.Priority);
foreach (var interactUsing in interactUsings)
{
// If an InteractUsing returns a status completion we finish our interaction
if (await interactUsing.InteractUsing(interactUsingEventArgs))
return;
}
// If we aren't directly interacting with the nearby object, lets see if our item has an after interact we can do
await InteractDoAfter(user, used, target, clickLocation, true);
}
/// <summary>
/// Uses an empty hand on an entity
/// Finds components with the InteractHand interface and calls their function
/// NOTE: Does not have an InRangeUnobstructed check
/// </summary>
public void InteractHand(IEntity user, IEntity target)
{
if (!ActionBlockerSystem.CanInteract(user))
return;
// all interactions should only happen when in range / unobstructed, so no range check is needed
var message = new InteractHandEvent(user, target);
RaiseLocalEvent(target.Uid, message);
if (message.Handled)
return;
var interactHandEventArgs = new InteractHandEventArgs(user, target);
var interactHandComps = target.GetAllComponents<IInteractHand>().ToList();
foreach (var interactHandComp in interactHandComps)
{
// If an InteractHand returns a status completion we finish our interaction
if (interactHandComp.InteractHand(interactHandEventArgs))
return;
}
// Else we run Activate.
InteractionActivate(user, target);
}
#region Hands
#region Use
/// <summary>
/// Activates the IUse behaviors of an entity
/// Verifies that the user is capable of doing the use interaction first
/// </summary>
/// <param name="user"></param>
/// <param name="used"></param>
public void TryUseInteraction(IEntity user, IEntity used)
{
if (user != null && used != null && ActionBlockerSystem.CanUse(user))
{
UseInteraction(user, used);
}
}
/// <summary>
/// Activates the IUse behaviors of an entity without first checking
/// if the user is capable of doing the use interaction.
/// </summary>
public void UseInteraction(IEntity user, IEntity used)
{
if (used.TryGetComponent<UseDelayComponent>(out var delayComponent))
{
if (delayComponent.ActiveDelay)
return;
else
delayComponent.BeginDelay();
}
var useMsg = new UseInHandEvent(user, used);
RaiseLocalEvent(used.Uid, useMsg);
if (useMsg.Handled)
return;
var uses = used.GetAllComponents<IUse>().ToList();
// Try to use item on any components which have the interface
foreach (var use in uses)
{
// If a Use returns a status completion we finish our interaction
if (use.UseEntity(new UseEntityEventArgs(user)))
return;
}
}
#endregion
#region Throw
/// <summary>
/// Activates the Throw behavior of an object
/// Verifies that the user is capable of doing the throw interaction first
/// </summary>
public bool TryThrowInteraction(IEntity user, IEntity item)
{
if (user == null || item == null || !ActionBlockerSystem.CanThrow(user)) return false;
ThrownInteraction(user, item);
return true;
}
/// <summary>
/// Calls Thrown on all components that implement the IThrown interface
/// on an entity that has been thrown.
/// </summary>
public void ThrownInteraction(IEntity user, IEntity thrown)
{
var throwMsg = new ThrownEvent(user, thrown);
RaiseLocalEvent(thrown.Uid, throwMsg);
if (throwMsg.Handled)
return;
var comps = thrown.GetAllComponents<IThrown>().ToList();
var args = new ThrownEventArgs(user);
// Call Thrown on all components that implement the interface
foreach (var comp in comps)
{
comp.Thrown(args);
}
}
#endregion
#region Equip
/// <summary>
/// Calls Equipped on all components that implement the IEquipped interface
/// on an entity that has been equipped.
/// </summary>
public void EquippedInteraction(IEntity user, IEntity equipped, EquipmentSlotDefines.Slots slot)
{
var equipMsg = new EquippedEvent(user, equipped, slot);
RaiseLocalEvent(equipped.Uid, equipMsg);
if (equipMsg.Handled)
return;
var comps = equipped.GetAllComponents<IEquipped>().ToList();
// Call Thrown on all components that implement the interface
foreach (var comp in comps)
{
comp.Equipped(new EquippedEventArgs(user, slot));
}
}
/// <summary>
/// Calls Unequipped on all components that implement the IUnequipped interface
/// on an entity that has been equipped.
/// </summary>
public void UnequippedInteraction(IEntity user, IEntity equipped, EquipmentSlotDefines.Slots slot)
{
var unequipMsg = new UnequippedEvent(user, equipped, slot);
RaiseLocalEvent(equipped.Uid, unequipMsg);
if (unequipMsg.Handled)
return;
var comps = equipped.GetAllComponents<IUnequipped>().ToList();
// Call Thrown on all components that implement the interface
foreach (var comp in comps)
{
comp.Unequipped(new UnequippedEventArgs(user, slot));
}
}
#region Equip Hand
/// <summary>
/// Calls EquippedHand on all components that implement the IEquippedHand interface
/// on an item.
/// </summary>
public void EquippedHandInteraction(IEntity user, IEntity item, SharedHand hand)
{
var equippedHandMessage = new EquippedHandEvent(user, item, hand);
RaiseLocalEvent(item.Uid, equippedHandMessage);
if (equippedHandMessage.Handled)
return;
var comps = item.GetAllComponents<IEquippedHand>().ToList();
foreach (var comp in comps)
{
comp.EquippedHand(new EquippedHandEventArgs(user, hand));
}
}
/// <summary>
/// Calls UnequippedHand on all components that implement the IUnequippedHand interface
/// on an item.
/// </summary>
public void UnequippedHandInteraction(IEntity user, IEntity item, SharedHand hand)
{
var unequippedHandMessage = new UnequippedHandEvent(user, item, hand);
RaiseLocalEvent(item.Uid, unequippedHandMessage);
if (unequippedHandMessage.Handled)
return;
var comps = item.GetAllComponents<IUnequippedHand>().ToList();
foreach (var comp in comps)
{
comp.UnequippedHand(new UnequippedHandEventArgs(user, hand));
}
}
#endregion
#endregion
#region Drop
/// <summary>
/// Activates the Dropped behavior of an object
/// Verifies that the user is capable of doing the drop interaction first
/// </summary>
public bool TryDroppedInteraction(IEntity user, IEntity item, bool intentional)
{
if (user == null || item == null || !ActionBlockerSystem.CanDrop(user)) return false;
DroppedInteraction(user, item, intentional);
return true;
}
/// <summary>
/// Calls Dropped on all components that implement the IDropped interface
/// on an entity that has been dropped.
/// </summary>
public void DroppedInteraction(IEntity user, IEntity item, bool intentional)
{
var dropMsg = new DroppedEvent(user, item, intentional);
RaiseLocalEvent(item.Uid, dropMsg);
if (dropMsg.Handled)
return;
item.Transform.LocalRotation = intentional ? Angle.Zero : (_random.Next(0, 100) / 100f) * MathHelper.TwoPi;
var comps = item.GetAllComponents<IDropped>().ToList();
// Call Land on all components that implement the interface
foreach (var comp in comps)
{
comp.Dropped(new DroppedEventArgs(user, intentional));
}
}
#endregion
#region Hand Selected
/// <summary>
/// Calls HandSelected on all components that implement the IHandSelected interface
/// on an item entity on a hand that has just been selected.
/// </summary>
public void HandSelectedInteraction(IEntity user, IEntity item)
{
var handSelectedMsg = new HandSelectedEvent(user, item);
RaiseLocalEvent(item.Uid, handSelectedMsg);
if (handSelectedMsg.Handled)
return;
var comps = item.GetAllComponents<IHandSelected>().ToList();
// Call Land on all components that implement the interface
foreach (var comp in comps)
{
comp.HandSelected(new HandSelectedEventArgs(user));
}
}
/// <summary>
/// Calls HandDeselected on all components that implement the IHandDeselected interface
/// on an item entity on a hand that has just been deselected.
/// </summary>
public void HandDeselectedInteraction(IEntity user, IEntity item)
{
var handDeselectedMsg = new HandDeselectedEvent(user, item);
RaiseLocalEvent(item.Uid, handDeselectedMsg);
if (handDeselectedMsg.Handled)
return;
var comps = item.GetAllComponents<IHandDeselected>().ToList();
// Call Land on all components that implement the interface
foreach (var comp in comps)
{
comp.HandDeselected(new HandDeselectedEventArgs(user));
}
}
#endregion
#endregion
/// <summary>
/// Will have two behaviors, either "uses" the used entity at range on the target entity if it is capable of accepting that action
/// Or it will use the used entity itself on the position clicked, regardless of what was there
/// </summary>
public async Task<bool> InteractUsingRanged(IEntity user, IEntity used, IEntity? target, EntityCoordinates clickLocation, bool inRangeUnobstructed)
{
if (target != null)
{
var rangedMsg = new RangedInteractEvent(user, used, target, clickLocation);
RaiseLocalEvent(target.Uid, rangedMsg);
if (rangedMsg.Handled)
return true;
var rangedInteractions = target.GetAllComponents<IRangedInteract>().ToList();
var rangedInteractionEventArgs = new RangedInteractEventArgs(user, used, clickLocation);
// See if we have a ranged interaction
foreach (var t in rangedInteractions)
{
// If an InteractUsingRanged returns a status completion we finish our interaction
if (t.RangedInteract(rangedInteractionEventArgs))
return true;
}
}
if (inRangeUnobstructed)
return await InteractDoAfter(user, used, target, clickLocation, false);
else
return await InteractDoAfter(user, used, null, clickLocation, false);
}
public void DoAttack(IEntity user, EntityCoordinates coordinates, bool wideAttack, EntityUid targetUid = default)
{
if (!ValidateInteractAndFace(user, coordinates))
return;
if (!ActionBlockerSystem.CanAttack(user))
return;
IEntity? targetEnt = null;
if (!wideAttack)
{
// Get entity clicked upon from UID if valid UID, if not assume no entity clicked upon and null
EntityManager.TryGetEntity(targetUid, out targetEnt);
// Check if interacted entity is in the same container, the direct child, or direct parent of the user.
if (targetEnt != null && !user.IsInSameOrParentContainer(targetEnt))
{
Logger.WarningS("system.interaction",
$"User entity named {user.Name} clicked on object {targetEnt.Name} that isn't the parent, child, or in the same container");
return;
}
// TODO: Replace with body attack range when we get something like arm length or telekinesis or something.
if (!user.InRangeUnobstructed(coordinates, ignoreInsideBlocker: true))
return;
}
// Verify user has a hand, and find what object he is currently holding in his active hand
if (user.TryGetComponent<IHandsComponent>(out var hands))
{
var item = hands.GetActiveHand?.Owner;
if (item != null)
{
if (wideAttack)
{
var ev = new WideAttackEvent(item, user, coordinates);
RaiseLocalEvent(item.Uid, ev, false);
if(ev.Handled)
return;
}
else
{
var ev = new ClickAttackEvent(item, user, coordinates, targetUid);
RaiseLocalEvent(item.Uid, ev, false);
if(ev.Handled)
return;
}
}
else if (!wideAttack &&
(targetEnt != null || EntityManager.TryGetEntity(targetUid, out targetEnt)) &&
targetEnt.HasComponent<ItemComponent>())
{
// We pick up items if our hand is empty, even if we're in combat mode.
InteractHand(user, targetEnt);
return;
}
}
// TODO: Make this saner?
// Attempt to do unarmed combat. We don't check for handled just because at this point it doesn't matter.
if(wideAttack)
RaiseLocalEvent(user.Uid, new WideAttackEvent(user, user, coordinates), false);
else
RaiseLocalEvent(user.Uid, new ClickAttackEvent(user, user, coordinates, targetUid), false);
}
}
}

View File

@@ -1,39 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Content.Server.GameObjects.Components.Movement;
using Content.Shared.GameObjects.Components.Movement;
using Content.Shared.GameTicking;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
internal sealed class ClimbSystem : EntitySystem, IResettingEntitySystem
{
private readonly HashSet<ClimbingComponent> _activeClimbers = new();
public void AddActiveClimber(ClimbingComponent climbingComponent)
{
_activeClimbers.Add(climbingComponent);
}
public void RemoveActiveClimber(ClimbingComponent climbingComponent)
{
_activeClimbers.Remove(climbingComponent);
}
public override void Update(float frameTime)
{
foreach (var climber in _activeClimbers.ToArray())
{
climber.Update();
}
}
public void Reset()
{
_activeClimbers.Clear();
}
}
}

View File

@@ -1,177 +0,0 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Content.Server.GameObjects.Components.Medical;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Content.Server.Mobs;
using Content.Shared.GameTicking;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Preferences;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using Robust.Shared.IoC;
using static Content.Shared.GameObjects.Components.Medical.SharedCloningPodComponent;
namespace Content.Server.GameObjects.EntitySystems
{
internal sealed class CloningSystem : EntitySystem, IResettingEntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
public readonly Dictionary<Mind, int> MindToId = new();
public readonly Dictionary<int, ClonerDNAEntry> IdToDNA = new();
private int _nextAllocatedMindId = 0;
private float _quickAndDirtyUserUpdatePreventerTimer = 0.0f;
public readonly Dictionary<Mind, EntityUid> ClonesWaitingForMind = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CloningPodComponent, ActivateInWorldEvent>(HandleActivate);
SubscribeLocalEvent<BeingClonedComponent, MindAddedMessage>(HandleMindAdded);
}
public override void Shutdown()
{
base.Shutdown();
UnsubscribeLocalEvent<CloningPodComponent, ActivateInWorldEvent>(HandleActivate);
UnsubscribeLocalEvent<BeingClonedComponent, MindAddedMessage>(HandleMindAdded);
}
internal void TransferMindToClone(Mind mind)
{
if (!ClonesWaitingForMind.TryGetValue(mind, out var entityUid) ||
!EntityManager.TryGetEntity(entityUid, out var entity) ||
!entity.TryGetComponent(out MindComponent? mindComp) ||
mindComp.Mind != null)
return;
mind.TransferTo(entity);
mind.UnVisit();
ClonesWaitingForMind.Remove(mind);
}
private void HandleActivate(EntityUid uid, CloningPodComponent component, ActivateInWorldEvent args)
{
if (!component.Powered ||
!args.User.TryGetComponent(out ActorComponent? actor))
{
return;
}
component.UserInterface?.Open(actor.PlayerSession);
}
private void HandleMindAdded(EntityUid uid, BeingClonedComponent component, MindAddedMessage message)
{
if (component.Parent == EntityUid.Invalid ||
!EntityManager.TryGetEntity(component.Parent, out var parent) ||
!parent.TryGetComponent<CloningPodComponent>(out var cloningPodComponent) ||
component.Owner != cloningPodComponent.BodyContainer?.ContainedEntity)
{
component.Owner.RemoveComponent<BeingClonedComponent>();
return;
}
cloningPodComponent.UpdateStatus(CloningPodStatus.Cloning);
}
public override void Update(float frameTime)
{
foreach (var (cloning, power) in ComponentManager.EntityQuery<CloningPodComponent, PowerReceiverComponent>(true))
{
if (cloning.UiKnownPowerState != power.Powered)
{
// Must be *before* update
cloning.UiKnownPowerState = power.Powered;
UpdateUserInterface(cloning);
}
if (!power.Powered)
return;
if (cloning.BodyContainer.ContainedEntity != null)
{
cloning.CloningProgress += frameTime;
cloning.CloningProgress = MathHelper.Clamp(cloning.CloningProgress, 0f, cloning.CloningTime);
}
if (cloning.CapturedMind?.Session?.AttachedEntity == cloning.BodyContainer.ContainedEntity)
{
cloning.Eject();
}
}
}
public void UpdateUserInterface(CloningPodComponent comp)
{
var idToUser = GetIdToUser();
comp.UserInterface?.SetState(
new CloningPodBoundUserInterfaceState(
idToUser,
// now
_timing.CurTime,
// progress, time, progressing
comp.CloningProgress,
comp.CloningTime,
// this is duplicate w/ the above check that actually updates progress
// better here than on client though
comp.UiKnownPowerState && (comp.BodyContainer.ContainedEntity != null),
comp.Status == CloningPodStatus.Cloning));
}
public void AddToDnaScans(ClonerDNAEntry dna)
{
if (!MindToId.ContainsKey(dna.Mind))
{
int id = _nextAllocatedMindId++;
MindToId.Add(dna.Mind, id);
IdToDNA.Add(id, dna);
}
OnChangeMadeToDnaScans();
}
public void OnChangeMadeToDnaScans()
{
foreach (var cloning in ComponentManager.EntityQuery<CloningPodComponent>(true))
UpdateUserInterface(cloning);
}
public bool HasDnaScan(Mind mind)
{
return MindToId.ContainsKey(mind);
}
public Dictionary<int, string?> GetIdToUser()
{
return IdToDNA.ToDictionary(m => m.Key, m => m.Value.Mind.CharacterName);
}
public void Reset()
{
MindToId.Clear();
IdToDNA.Clear();
ClonesWaitingForMind.Clear();
_nextAllocatedMindId = 0;
// We PROBABLY don't need to send out UI interface updates for the dna scan changes during a reset
}
}
// TODO: This needs to be moved to Content.Server.Mobs and made a global point of reference.
// For example, GameTicker should be using this, and this should be using ICharacterProfile rather than HumanoidCharacterProfile.
// It should carry a reference or copy of itself with the mobs that it affects.
// See TODO in MedicalScannerComponent.
struct ClonerDNAEntry {
public Mind Mind;
public HumanoidCharacterProfile Profile;
public ClonerDNAEntry(Mind m, HumanoidCharacterProfile hcp)
{
Mind = m;
Profile = hcp;
}
}
}

View File

@@ -1,10 +0,0 @@
using Content.Shared.GameObjects.EntitySystems;
using JetBrains.Annotations;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
public sealed class CombatModeSystem : SharedCombatModeSystem
{
}
}

View File

@@ -1,465 +0,0 @@
#nullable enable
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Construction;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Stack;
using Content.Server.GameObjects.EntitySystems.DoAfter;
using Content.Shared.Construction;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.Interfaces;
using Content.Shared.Utility;
using JetBrains.Annotations;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Players;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.GameObjects.EntitySystems
{
/// <summary>
/// The server-side implementation of the construction system, which is used for constructing entities in game.
/// </summary>
[UsedImplicitly]
internal class ConstructionSystem : SharedConstructionSystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
private readonly Dictionary<ICommonSession, HashSet<int>> _beingBuilt = new();
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<TryStartStructureConstructionMessage>(HandleStartStructureConstruction);
SubscribeNetworkEvent<TryStartItemConstructionMessage>(HandleStartItemConstruction);
}
private IEnumerable<IEntity> EnumerateNearby(IEntity user)
{
if (user.TryGetComponent(out HandsComponent? hands))
{
foreach (var itemComponent in hands?.GetAllHeldItems()!)
{
if (itemComponent.Owner.TryGetComponent(out ServerStorageComponent? storage))
{
foreach (var storedEntity in storage.StoredEntities!)
{
yield return storedEntity;
}
}
yield return itemComponent.Owner;
}
}
if (user!.TryGetComponent(out InventoryComponent? inventory))
{
foreach (var held in inventory.GetAllHeldItems())
{
if (held.TryGetComponent(out ServerStorageComponent? storage))
{
foreach (var storedEntity in storage.StoredEntities!)
{
yield return storedEntity;
}
}
yield return held;
}
}
foreach (var near in IoCManager.Resolve<IEntityLookup>().GetEntitiesInRange(user!, 2f, true))
{
yield return near;
}
}
private async Task<IEntity?> Construct(IEntity user, string materialContainer, ConstructionGraphPrototype graph, ConstructionGraphEdge edge, ConstructionGraphNode targetNode)
{
// We need a place to hold our construction items!
var container = ContainerHelpers.EnsureContainer<Container>(user, materialContainer, out var existed);
if (existed)
{
user.PopupMessageCursor(Loc.GetString("You can't start another construction now!"));
return null;
}
var containers = new Dictionary<string, Container>();
var doAfterTime = 0f;
// HOLY SHIT THIS IS SOME HACKY CODE.
// But I'd rather do this shit than risk having collisions with other containers.
Container GetContainer(string name)
{
if (containers!.ContainsKey(name))
return containers[name];
while (true)
{
var random = _robustRandom.Next();
var c = ContainerHelpers.EnsureContainer<Container>(user!, random.ToString(), out var existed);
if (existed) continue;
containers[name] = c;
return c;
}
}
void FailCleanup()
{
foreach (var entity in container!.ContainedEntities.ToArray())
{
container.Remove(entity);
}
foreach (var cont in containers!.Values)
{
foreach (var entity in cont.ContainedEntities.ToArray())
{
cont.Remove(entity);
}
}
// If we don't do this, items are invisible for some fucking reason. Nice.
Timer.Spawn(1, ShutdownContainers);
}
void ShutdownContainers()
{
container!.Shutdown();
foreach (var c in containers!.Values.ToArray())
{
c.Shutdown();
}
}
var failed = false;
var steps = new List<ConstructionGraphStep>();
foreach (var step in edge.Steps)
{
doAfterTime += step.DoAfter;
var handled = false;
switch (step)
{
case MaterialConstructionGraphStep materialStep:
foreach (var entity in EnumerateNearby(user))
{
if (!materialStep.EntityValid(entity, out var stack))
continue;
var splitStack = new StackSplitEvent()
{Amount = materialStep.Amount, SpawnPosition = user.ToCoordinates()};
RaiseLocalEvent(entity.Uid, splitStack);
if (splitStack.Result == null)
continue;
if (string.IsNullOrEmpty(materialStep.Store))
{
if (!container.Insert(splitStack.Result))
continue;
}
else if (!GetContainer(materialStep.Store).Insert(splitStack.Result))
continue;
handled = true;
break;
}
break;
case ArbitraryInsertConstructionGraphStep arbitraryStep:
foreach (var entity in EnumerateNearby(user))
{
if (!arbitraryStep.EntityValid(entity))
continue;
if (string.IsNullOrEmpty(arbitraryStep.Store))
{
if (!container.Insert(entity))
continue;
}
else if (!GetContainer(arbitraryStep.Store).Insert(entity))
continue;
handled = true;
break;
}
break;
}
if (handled == false)
{
failed = true;
break;
}
steps.Add(step);
}
if (failed)
{
user.PopupMessageCursor(Loc.GetString("You don't have the materials to build that!"));
FailCleanup();
return null;
}
var doAfterSystem = Get<DoAfterSystem>();
var doAfterArgs = new DoAfterEventArgs(user, doAfterTime)
{
BreakOnDamage = true,
BreakOnStun = true,
BreakOnTargetMove = false,
BreakOnUserMove = true,
NeedHand = false,
};
if (await doAfterSystem.DoAfter(doAfterArgs) == DoAfterStatus.Cancelled)
{
FailCleanup();
return null;
}
var newEntity = EntityManager.SpawnEntity(graph.Nodes[edge.Target].Entity, user.Transform.Coordinates);
// Yes, this should throw if it's missing the component.
var construction = newEntity.GetComponent<ConstructionComponent>();
// We attempt to set the pathfinding target.
construction.Target = targetNode;
// We preserve the containers...
foreach (var (name, cont) in containers)
{
var newCont = ContainerHelpers.EnsureContainer<Container>(newEntity, name);
foreach (var entity in cont.ContainedEntities.ToArray())
{
cont.ForceRemove(entity);
newCont.Insert(entity);
}
}
// We now get rid of all them.
ShutdownContainers();
// We have step completed steps!
foreach (var step in steps)
{
foreach (var completed in step.Completed)
{
await completed.PerformAction(newEntity, user);
}
}
// And we also have edge completed effects!
foreach (var completed in edge.Completed)
{
await completed.PerformAction(newEntity, user);
}
return newEntity;
}
private async void HandleStartItemConstruction(TryStartItemConstructionMessage ev, EntitySessionEventArgs args)
{
if (!_prototypeManager.TryIndex(ev.PrototypeName, out ConstructionPrototype? constructionPrototype))
{
Logger.Error($"Tried to start construction of invalid recipe '{ev.PrototypeName}'!");
return;
}
if (!_prototypeManager.TryIndex(constructionPrototype.Graph, out ConstructionGraphPrototype? constructionGraph))
{
Logger.Error($"Invalid construction graph '{constructionPrototype.Graph}' in recipe '{ev.PrototypeName}'!");
return;
}
var startNode = constructionGraph.Nodes[constructionPrototype.StartNode];
var targetNode = constructionGraph.Nodes[constructionPrototype.TargetNode];
var pathFind = constructionGraph.Path(startNode.Name, targetNode.Name);
var user = args.SenderSession.AttachedEntity;
if (user == null || !ActionBlockerSystem.CanInteract(user)) return;
if (!user.TryGetComponent(out HandsComponent? hands)) return;
foreach (var condition in constructionPrototype.Conditions)
{
if (!condition.Condition(user, user.ToCoordinates(), Direction.South))
return;
}
if(pathFind == null)
throw new InvalidDataException($"Can't find path from starting node to target node in construction! Recipe: {ev.PrototypeName}");
var edge = startNode.GetEdge(pathFind[0].Name);
if(edge == null)
throw new InvalidDataException($"Can't find edge from starting node to the next node in pathfinding! Recipe: {ev.PrototypeName}");
// No support for conditions here!
foreach (var step in edge.Steps)
{
switch (step)
{
case ToolConstructionGraphStep _:
case NestedConstructionGraphStep _:
throw new InvalidDataException("Invalid first step for construction recipe!");
}
}
var item = await Construct(user, "item_construction", constructionGraph, edge, targetNode);
if(item != null && item.TryGetComponent(out ItemComponent? itemComp))
hands.PutInHandOrDrop(itemComp);
}
private async void HandleStartStructureConstruction(TryStartStructureConstructionMessage ev, EntitySessionEventArgs args)
{
if (!_prototypeManager.TryIndex(ev.PrototypeName, out ConstructionPrototype? constructionPrototype))
{
Logger.Error($"Tried to start construction of invalid recipe '{ev.PrototypeName}'!");
RaiseNetworkEvent(new AckStructureConstructionMessage(ev.Ack));
return;
}
if (!_prototypeManager.TryIndex(constructionPrototype.Graph, out ConstructionGraphPrototype? constructionGraph))
{
Logger.Error($"Invalid construction graph '{constructionPrototype.Graph}' in recipe '{ev.PrototypeName}'!");
RaiseNetworkEvent(new AckStructureConstructionMessage(ev.Ack));
return;
}
var user = args.SenderSession.AttachedEntity;
if (user == null)
{
Logger.Error($"Client sent {nameof(TryStartStructureConstructionMessage)} with no attached entity!");
return;
}
var startNode = constructionGraph.Nodes[constructionPrototype.StartNode];
var targetNode = constructionGraph.Nodes[constructionPrototype.TargetNode];
var pathFind = constructionGraph.Path(startNode.Name, targetNode.Name);
if (_beingBuilt.TryGetValue(args.SenderSession, out var set))
{
if (!set.Add(ev.Ack))
{
user.PopupMessageCursor(Loc.GetString("You are already building that!"));
return;
}
}
else
{
var newSet = new HashSet<int> {ev.Ack};
_beingBuilt[args.SenderSession] = newSet;
}
foreach (var condition in constructionPrototype.Conditions)
{
if (!condition.Condition(user, ev.Location, ev.Angle.GetCardinalDir()))
{
Cleanup();
return;
}
}
void Cleanup()
{
_beingBuilt[args.SenderSession].Remove(ev.Ack);
}
if (user == null
|| !ActionBlockerSystem.CanInteract(user)
|| !user.TryGetComponent(out HandsComponent? hands) || hands.GetActiveHand == null
|| !user.InRangeUnobstructed(ev.Location, ignoreInsideBlocker:constructionPrototype.CanBuildInImpassable))
{
Cleanup();
return;
}
if(pathFind == null)
throw new InvalidDataException($"Can't find path from starting node to target node in construction! Recipe: {ev.PrototypeName}");
var edge = startNode.GetEdge(pathFind[0].Name);
if(edge == null)
throw new InvalidDataException($"Can't find edge from starting node to the next node in pathfinding! Recipe: {ev.PrototypeName}");
var valid = false;
var holding = hands.GetActiveHand?.Owner;
if (holding == null)
{
Cleanup();
return;
}
// No support for conditions here!
foreach (var step in edge.Steps)
{
switch (step)
{
case EntityInsertConstructionGraphStep entityInsert:
if (entityInsert.EntityValid(holding))
valid = true;
break;
case ToolConstructionGraphStep _:
case NestedConstructionGraphStep _:
throw new InvalidDataException("Invalid first step for item recipe!");
}
if (valid)
break;
}
if (!valid)
{
Cleanup();
return;
}
var structure = await Construct(user, (ev.Ack + constructionPrototype.GetHashCode()).ToString(), constructionGraph, edge, targetNode);
if (structure == null)
{
Cleanup();
return;
}
structure.Transform.Coordinates = ev.Location;
structure.Transform.LocalRotation = constructionPrototype.CanRotate ? ev.Angle : Angle.Zero;
RaiseNetworkEvent(new AckStructureConstructionMessage(ev.Ack));
Cleanup();
}
}
}

View File

@@ -1,30 +0,0 @@
using Content.Server.GameObjects.Components.Singularity;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
public sealed class ContainmentFieldGeneratorSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, PhysicsBodyTypeChangedEvent>(BodyTypeChanged);
}
public override void Shutdown()
{
base.Shutdown();
UnsubscribeLocalEvent<ContainmentFieldGeneratorComponent, PhysicsBodyTypeChangedEvent>();
}
private static void BodyTypeChanged(
EntityUid uid,
ContainmentFieldGeneratorComponent component,
PhysicsBodyTypeChangedEvent args)
{
component.OnAnchoredChanged();
}
}
}

View File

@@ -1,51 +0,0 @@
#nullable enable
using Content.Server.GameObjects.Components.ActionBlocking;
using Content.Server.GameObjects.Components.GUI;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
public class CuffableSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
EntityManager.EventBus.SubscribeEvent<HandCountChangedEvent>(EventSource.Local, this, OnHandCountChanged);
}
/// <summary>
/// Check the current amount of hands the owner has, and if there's less hands than active cuffs we remove some cuffs.
/// </summary>
private void OnHandCountChanged(HandCountChangedEvent message)
{
var owner = message.Sender;
if (!owner.TryGetComponent(out CuffableComponent? cuffable) ||
!cuffable.Initialized) return;
var dirty = false;
var handCount = owner.GetComponentOrNull<HandsComponent>()?.Count ?? 0;
while (cuffable.CuffedHandCount > handCount && cuffable.CuffedHandCount > 0)
{
dirty = true;
var container = cuffable.Container;
var entity = container.ContainedEntities[^1];
container.Remove(entity);
entity.Transform.WorldPosition = owner.Transform.WorldPosition;
}
if (dirty)
{
cuffable.CanStillInteract = handCount > cuffable.CuffedHandCount;
cuffable.CuffedStateChanged();
cuffable.Dirty();
}
}
}
}

View File

@@ -1,27 +0,0 @@
using Content.Shared.GameObjects.EntitySystems;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Random;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
public class DestructibleSystem : EntitySystem
{
[Dependency] public readonly IRobustRandom Random = default!;
public AudioSystem AudioSystem { get; private set; } = default!;
public ActSystem ActSystem { get; private set; } = default!;
public override void Initialize()
{
base.Initialize();
AudioSystem = Get<AudioSystem>();
ActSystem = Get<ActSystem>();
}
}
}

View File

@@ -1,19 +0,0 @@
using Content.Server.Interfaces;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
internal sealed class DeviceNetworkSystem : EntitySystem
{
[Dependency] private readonly IDeviceNetwork _network = default!;
public override void Update(float frameTime)
{
base.Update(frameTime);
_network.Update();
}
}
}

View File

@@ -1,68 +0,0 @@
using Content.Server.GameObjects.Components.Engineering;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Stack;
using Content.Server.GameObjects.EntitySystems.DoAfter;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Utility;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using System.Threading;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
public class DisassembleOnActivateSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DisassembleOnActivateComponent, ActivateInWorldEvent>(HandleActivateInWorld);
}
public override void Shutdown()
{
base.Shutdown();
UnsubscribeLocalEvent<DisassembleOnActivateComponent, ActivateInWorldEvent>(HandleActivateInWorld);
}
private async void HandleActivateInWorld(EntityUid uid, DisassembleOnActivateComponent component, ActivateInWorldEvent args)
{
if (string.IsNullOrEmpty(component.Prototype))
return;
if (!args.User.InRangeUnobstructed(args.Target))
return;
if (component.DoAfterTime > 0 && TryGet<DoAfterSystem>(out var doAfterSystem))
{
var doAfterArgs = new DoAfterEventArgs(args.User, component.DoAfterTime, component.TokenSource.Token)
{
BreakOnUserMove = true,
BreakOnStun = true,
};
var result = await doAfterSystem.DoAfter(doAfterArgs);
if (result != DoAfterStatus.Finished)
return;
component.TokenSource.Cancel();
}
if (component.Deleted || component.Owner.Deleted)
return;
var entity = EntityManager.SpawnEntity(component.Prototype, component.Owner.Transform.Coordinates);
if (args.User.TryGetComponent<HandsComponent>(out var hands)
&& entity.TryGetComponent<ItemComponent>(out var item))
{
hands.PutInHandOrDrop(item);
}
component.Owner.Delete();
return;
}
}
}

View File

@@ -1,19 +0,0 @@
using Content.Server.GameObjects.Components.Disposal;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
internal sealed class DisposableSystem : EntitySystem
{
public override void Update(float frameTime)
{
foreach (var comp in ComponentManager.EntityQuery<DisposalHolderComponent>(true))
{
comp.Update(frameTime);
}
}
}
}

View File

@@ -1,30 +0,0 @@
using Content.Server.GameObjects.Components.Disposal;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems.Disposal
{
public sealed class DisposalMailingUnitSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DisposalMailingUnitComponent, PhysicsBodyTypeChangedEvent>(BodyTypeChanged);
}
public override void Shutdown()
{
base.Shutdown();
UnsubscribeLocalEvent<DisposalMailingUnitComponent, PhysicsBodyTypeChangedEvent>();
}
private static void BodyTypeChanged(
EntityUid uid,
DisposalMailingUnitComponent component,
PhysicsBodyTypeChangedEvent args)
{
component.UpdateVisualState();
}
}
}

View File

@@ -1,30 +0,0 @@
using Content.Server.GameObjects.Components.Disposal;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems.Disposal
{
public sealed class DisposalTubeSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DisposalTubeComponent, PhysicsBodyTypeChangedEvent>(BodyTypeChanged);
}
public override void Shutdown()
{
base.Shutdown();
UnsubscribeLocalEvent<DisposalTubeComponent, PhysicsBodyTypeChangedEvent>();
}
private static void BodyTypeChanged(
EntityUid uid,
DisposalTubeComponent component,
PhysicsBodyTypeChangedEvent args)
{
component.AnchoredChanged();
}
}
}

View File

@@ -1,30 +0,0 @@
using Content.Server.GameObjects.Components.Disposal;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems.Disposal
{
public sealed class DisposalUnitSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DisposalUnitComponent, PhysicsBodyTypeChangedEvent>(BodyTypeChanged);
}
public override void Shutdown()
{
base.Shutdown();
UnsubscribeLocalEvent<DisposalUnitComponent, PhysicsBodyTypeChangedEvent>();
}
private static void BodyTypeChanged(
EntityUid uid,
DisposalUnitComponent component,
PhysicsBodyTypeChangedEvent args)
{
component.UpdateVisualState();
}
}
}

View File

@@ -1,188 +0,0 @@
#nullable enable
using System;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Mobs;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Timing;
namespace Content.Server.GameObjects.EntitySystems.DoAfter
{
public sealed class DoAfter
{
public Task<DoAfterStatus> AsTask { get; }
private TaskCompletionSource<DoAfterStatus> Tcs { get; }
public DoAfterEventArgs EventArgs;
public TimeSpan StartTime { get; }
public float Elapsed { get; set; }
public EntityCoordinates UserGrid { get; }
public EntityCoordinates TargetGrid { get; }
public bool TookDamage { get; set; }
public DoAfterStatus Status => AsTask.IsCompletedSuccessfully ? AsTask.Result : DoAfterStatus.Running;
// NeedHand
private readonly string? _activeHand;
private readonly ItemComponent? _activeItem;
public DoAfter(DoAfterEventArgs eventArgs)
{
EventArgs = eventArgs;
StartTime = IoCManager.Resolve<IGameTiming>().CurTime;
if (eventArgs.BreakOnUserMove)
{
UserGrid = eventArgs.User.Transform.Coordinates;
}
if (eventArgs.BreakOnTargetMove)
{
// Target should never be null if the bool is set.
TargetGrid = eventArgs.Target!.Transform.Coordinates;
}
// For this we need to stay on the same hand slot and need the same item in that hand slot
// (or if there is no item there we need to keep it free).
if (eventArgs.NeedHand && eventArgs.User.TryGetComponent(out HandsComponent? handsComponent))
{
_activeHand = handsComponent.ActiveHand;
_activeItem = handsComponent.GetActiveHand;
}
Tcs = new TaskCompletionSource<DoAfterStatus>();
AsTask = Tcs.Task;
}
public void Run(float frameTime)
{
switch (Status)
{
case DoAfterStatus.Running:
break;
case DoAfterStatus.Cancelled:
case DoAfterStatus.Finished:
return;
default:
throw new ArgumentOutOfRangeException();
}
Elapsed += frameTime;
if (IsFinished())
{
// Do the final checks here
if (!TryPostCheck())
{
Tcs.SetResult(DoAfterStatus.Cancelled);
}
else
{
Tcs.SetResult(DoAfterStatus.Finished);
}
return;
}
if (IsCancelled())
{
Tcs.SetResult(DoAfterStatus.Cancelled);
}
}
private bool IsCancelled()
{
if (EventArgs.User.Deleted || EventArgs.Target?.Deleted == true)
{
return true;
}
//https://github.com/tgstation/tgstation/blob/1aa293ea337283a0191140a878eeba319221e5df/code/__HELPERS/mobs.dm
if (EventArgs.CancelToken.IsCancellationRequested)
{
return true;
}
// TODO :Handle inertia in space.
if (EventArgs.BreakOnUserMove && !EventArgs.User.Transform.Coordinates.InRange(
EventArgs.User.EntityManager, UserGrid, EventArgs.MovementThreshold))
{
return true;
}
if (EventArgs.BreakOnTargetMove && !EventArgs.Target!.Transform.Coordinates.InRange(
EventArgs.User.EntityManager, TargetGrid, EventArgs.MovementThreshold))
{
return true;
}
if (EventArgs.BreakOnDamage && TookDamage)
{
return true;
}
if (EventArgs.ExtraCheck != null && !EventArgs.ExtraCheck.Invoke())
{
return true;
}
if (EventArgs.BreakOnStun &&
EventArgs.User.TryGetComponent(out StunnableComponent? stunnableComponent) &&
stunnableComponent.Stunned)
{
return true;
}
if (EventArgs.NeedHand)
{
if (!EventArgs.User.TryGetComponent(out HandsComponent? handsComponent))
{
// If we had a hand but no longer have it that's still a paddlin'
if (_activeHand != null)
{
return true;
}
}
else
{
var currentActiveHand = handsComponent.ActiveHand;
if (_activeHand != currentActiveHand)
{
return true;
}
var currentItem = handsComponent.GetActiveHand;
if (_activeItem != currentItem)
{
return true;
}
}
}
return false;
}
private bool TryPostCheck()
{
return EventArgs.PostCheck?.Invoke() != false;
}
private bool IsFinished()
{
if (Elapsed <= EventArgs.Delay)
{
return false;
}
return true;
}
}
}

View File

@@ -1,102 +0,0 @@
#nullable enable
using System;
using System.Threading;
using Content.Shared.Physics;
using Content.Shared.Utility;
using Robust.Shared.GameObjects;
// ReSharper disable UnassignedReadonlyField
namespace Content.Server.GameObjects.EntitySystems.DoAfter
{
public sealed class DoAfterEventArgs
{
// Premade checks
public Func<bool> GetInRangeUnobstructed(CollisionGroup collisionMask = CollisionGroup.MobMask)
{
if (Target == null)
{
throw new InvalidOperationException("Can't supply a null target to DoAfterEventArgs.GetInRangeUnobstructed");
}
bool Ignored(IEntity entity) => entity == User || entity == Target;
return () => User.InRangeUnobstructed(Target, collisionMask: collisionMask, predicate: Ignored);
}
/// <summary>
/// The entity invoking do_after
/// </summary>
public IEntity User { get; }
/// <summary>
/// How long does the do_after require to complete
/// </summary>
public float Delay { get; }
/// <summary>
/// Applicable target (if relevant)
/// </summary>
public IEntity? Target { get; }
/// <summary>
/// Manually cancel the do_after so it no longer runs
/// </summary>
public CancellationToken CancelToken { get; }
// Break the chains
/// <summary>
/// Whether we need to keep our active hand as is (i.e. can't change hand or change item).
/// This also covers requiring the hand to be free (if applicable).
/// </summary>
public bool NeedHand { get; set; }
/// <summary>
/// If do_after stops when the user moves
/// </summary>
public bool BreakOnUserMove { get; set; }
/// <summary>
/// If do_after stops when the target moves (if there is a target)
/// </summary>
public bool BreakOnTargetMove { get; set; }
/// <summary>
/// Threshold for user and target movement
/// </summary>
public float MovementThreshold { get; set; }
public bool BreakOnDamage { get; set; }
public bool BreakOnStun { get; set; }
/// <summary>
/// Requires a function call once at the end (like InRangeUnobstructed).
/// </summary>
/// <remarks>
/// Anything that needs a pre-check should do it itself so no DoAfterState is ever sent to the client.
/// </remarks>
public Func<bool>? PostCheck { get; set; } = null;
/// <summary>
/// Additional conditions that need to be met. Return false to cancel.
/// </summary>
public Func<bool>? ExtraCheck { get; set; }
public DoAfterEventArgs(
IEntity user,
float delay,
CancellationToken cancelToken = default,
IEntity? target = null)
{
User = user;
Delay = delay;
CancelToken = cancelToken;
Target = target;
MovementThreshold = 0.1f;
if (Target == null)
{
BreakOnTargetMove = false;
}
}
}
}

View File

@@ -1,83 +0,0 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems.DoAfter
{
[UsedImplicitly]
public sealed class DoAfterSystem : EntitySystem
{
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var comp in ComponentManager.EntityQuery<DoAfterComponent>(true))
{
var cancelled = new List<DoAfter>(0);
var finished = new List<DoAfter>(0);
foreach (var doAfter in comp.DoAfters.ToArray())
{
doAfter.Run(frameTime);
switch (doAfter.Status)
{
case DoAfterStatus.Running:
break;
case DoAfterStatus.Cancelled:
cancelled.Add(doAfter);
break;
case DoAfterStatus.Finished:
finished.Add(doAfter);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
foreach (var doAfter in cancelled)
{
comp.Cancelled(doAfter);
}
foreach (var doAfter in finished)
{
comp.Finished(doAfter);
}
finished.Clear();
}
}
/// <summary>
/// Tasks that are delayed until the specified time has passed
/// These can be potentially cancelled by the user moving or when other things happen.
/// </summary>
/// <param name="eventArgs"></param>
/// <returns></returns>
public async Task<DoAfterStatus> DoAfter(DoAfterEventArgs eventArgs)
{
// Setup
var doAfter = new DoAfter(eventArgs);
// Caller's gonna be responsible for this I guess
var doAfterComponent = eventArgs.User.GetComponent<DoAfterComponent>();
doAfterComponent.Add(doAfter);
await doAfter.AsTask;
return doAfter.Status;
}
}
public enum DoAfterStatus
{
Running,
Cancelled,
Finished,
}
}

View File

@@ -1,45 +0,0 @@
#nullable enable
using Content.Shared.GameObjects.EntitySystems;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
/// <summary>
/// Used on the server side to manage global access level overrides.
/// </summary>
internal sealed class DoorSystem : SharedDoorSystem
{
/// <summary>
/// Determines the base access behavior of all doors on the station.
/// </summary>
public AccessTypes AccessType { get; set; }
/// <summary>
/// How door access should be handled.
/// </summary>
public enum AccessTypes
{
/// <summary> ID based door access. </summary>
Id,
/// <summary>
/// Allows everyone to open doors, except external which airlocks are still handled with ID's
/// </summary>
AllowAllIdExternal,
/// <summary>
/// Allows everyone to open doors, except external airlocks which are never allowed, even if the user has
/// ID access.
/// </summary>
AllowAllNoExternal,
/// <summary> Allows everyone to open all doors. </summary>
AllowAll
}
public override void Initialize()
{
base.Initialize();
AccessType = AccessTypes.Id;
}
}
}

View File

@@ -1,45 +0,0 @@
using System;
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerReceiverUsers;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
internal sealed class EmergencyLightSystem : EntitySystem
{
private readonly HashSet<EmergencyLightComponent> _activeLights = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<EmergencyLightMessage>(HandleEmergencyLightMessage);
}
private void HandleEmergencyLightMessage(EmergencyLightMessage message)
{
switch (message.State)
{
case EmergencyLightComponent.EmergencyLightState.On:
case EmergencyLightComponent.EmergencyLightState.Charging:
_activeLights.Add(message.Component);
break;
case EmergencyLightComponent.EmergencyLightState.Full:
case EmergencyLightComponent.EmergencyLightState.Empty:
_activeLights.Remove(message.Component);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
public override void Update(float frameTime)
{
foreach (var activeLight in _activeLights)
{
activeLight.OnUpdate(frameTime);
}
}
}
}

View File

@@ -1,18 +0,0 @@
using Content.Server.GameObjects.Components.Interactable;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
public class ExpendableLightSystem : EntitySystem
{
public override void Update(float frameTime)
{
foreach (var light in ComponentManager.EntityQuery<ExpendableLightComponent>(true))
{
light.Update(frameTime);
}
}
}
}

View File

@@ -1,79 +0,0 @@
using System;
using System.Linq;
using Content.Server.Interfaces.GameTicking;
using Content.Shared.GameObjects.EntitySystemMessages;
using Content.Shared.GameTicking;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Timing;
#nullable enable
namespace Content.Server.GameObjects.EntitySystems.GameMode
{
[UsedImplicitly]
public sealed class SuspicionEndTimerSystem : EntitySystem, IResettingEntitySystem
{
[Dependency] private readonly IPlayerManager _playerManager = null!;
private TimeSpan? _endTime;
public TimeSpan? EndTime
{
get => _endTime;
set
{
_endTime = value;
SendUpdateToAll();
}
}
public override void Initialize()
{
base.Initialize();
_playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged;
}
public override void Shutdown()
{
base.Shutdown();
_playerManager.PlayerStatusChanged -= PlayerManagerOnPlayerStatusChanged;
}
private void PlayerManagerOnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.NewStatus == SessionStatus.InGame)
{
SendUpdateTimerMessage(e.Session);
}
}
private void SendUpdateToAll()
{
foreach (var player in _playerManager.GetAllPlayers().Where(p => p.Status == SessionStatus.InGame))
{
SendUpdateTimerMessage(player);
}
}
private void SendUpdateTimerMessage(IPlayerSession player)
{
var msg = new SuspicionMessages.SetSuspicionEndTimerMessage
{
EndTime = EndTime
};
EntityManager.EntityNetManager?.SendSystemNetworkMessage(msg, player.ConnectedClient);
}
void IResettingEntitySystem.Reset()
{
EndTime = null;
}
}
}

View File

@@ -1,57 +0,0 @@
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Suspicion;
using Content.Shared.GameTicking;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems.GameMode
{
[UsedImplicitly]
public class SuspicionRoleSystem : EntitySystem, IResettingEntitySystem
{
private readonly HashSet<SuspicionRoleComponent> _traitors = new();
public IReadOnlyCollection<SuspicionRoleComponent> Traitors => _traitors;
public void AddTraitor(SuspicionRoleComponent role)
{
if (!_traitors.Add(role))
{
return;
}
foreach (var traitor in _traitors)
{
traitor.AddAlly(role);
}
role.SetAllies(_traitors);
}
public void RemoveTraitor(SuspicionRoleComponent role)
{
if (!_traitors.Remove(role))
{
return;
}
foreach (var traitor in _traitors)
{
traitor.RemoveAlly(role);
}
role.ClearAllies();
}
public override void Shutdown()
{
_traitors.Clear();
base.Shutdown();
}
public void Reset()
{
_traitors.Clear();
}
}
}

View File

@@ -1,173 +0,0 @@
using System.Collections.Generic;
using Content.Server.Administration;
using Content.Server.Eui;
using Content.Server.GameObjects.Components.Observer;
using Content.Server.GameObjects.Components.Observer.GhostRoles;
using Content.Shared.GameObjects.Components.Observer.GhostRoles;
using Content.Shared.GameTicking;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
public class GhostRoleSystem : EntitySystem, IResettingEntitySystem
{
[Dependency] private readonly EuiManager _euiManager = default!;
private uint _nextRoleIdentifier = 0;
private readonly Dictionary<uint, GhostRoleComponent> _ghostRoles = new();
private readonly Dictionary<IPlayerSession, GhostRolesEui> _openUis = new();
private readonly Dictionary<IPlayerSession, MakeGhostRoleEui> _openMakeGhostRoleUis = new();
[ViewVariables]
public IReadOnlyCollection<GhostRoleComponent> GhostRoles => _ghostRoles.Values;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached);
}
public override void Shutdown()
{
base.Shutdown();
UnsubscribeLocalEvent<PlayerAttachedEvent>();
}
private uint GetNextRoleIdentifier()
{
return unchecked(_nextRoleIdentifier++);
}
public void OpenEui(IPlayerSession session)
{
if (session.AttachedEntity == null || !session.AttachedEntity.HasComponent<GhostComponent>())
return;
if(_openUis.ContainsKey(session))
CloseEui(session);
var eui = _openUis[session] = new GhostRolesEui();
_euiManager.OpenEui(eui, session);
eui.StateDirty();
}
public void OpenMakeGhostRoleEui(IPlayerSession session, EntityUid uid)
{
if (session.AttachedEntity == null)
return;
if (_openMakeGhostRoleUis.ContainsKey(session))
CloseEui(session);
var eui = _openMakeGhostRoleUis[session] = new MakeGhostRoleEui(uid);
_euiManager.OpenEui(eui, session);
eui.StateDirty();
}
public void CloseEui(IPlayerSession session)
{
if (!_openUis.ContainsKey(session)) return;
_openUis.Remove(session, out var eui);
eui?.Close();
}
public void CloseMakeGhostRoleEui(IPlayerSession session)
{
if (_openMakeGhostRoleUis.Remove(session, out var eui))
{
eui?.Close();
}
}
public void UpdateAllEui()
{
foreach (var eui in _openUis.Values)
{
eui.StateDirty();
}
}
public void RegisterGhostRole(GhostRoleComponent role)
{
if (_ghostRoles.ContainsValue(role)) return;
_ghostRoles[role.Identifier = GetNextRoleIdentifier()] = role;
UpdateAllEui();
}
public void UnregisterGhostRole(GhostRoleComponent role)
{
if (!_ghostRoles.ContainsKey(role.Identifier) || _ghostRoles[role.Identifier] != role) return;
_ghostRoles.Remove(role.Identifier);
UpdateAllEui();
}
public void Takeover(IPlayerSession player, uint identifier)
{
if (!_ghostRoles.TryGetValue(identifier, out var role)) return;
if (!role.Take(player)) return;
CloseEui(player);
}
public GhostRoleInfo[] GetGhostRolesInfo()
{
var roles = new GhostRoleInfo[_ghostRoles.Count];
var i = 0;
foreach (var (id, role) in _ghostRoles)
{
roles[i] = new GhostRoleInfo(){Identifier = id, Name = role.RoleName, Description = role.RoleDescription};
i++;
}
return roles;
}
private void OnPlayerAttached(PlayerAttachedEvent message)
{
// Close the session of any player that has a ghost roles window open and isn't a ghost anymore.
if (!_openUis.ContainsKey(message.Player)) return;
if (message.Entity.HasComponent<GhostComponent>()) return;
CloseEui(message.Player);
}
public void Reset()
{
foreach (var session in _openUis.Keys)
{
CloseEui(session);
}
_openUis.Clear();
_ghostRoles.Clear();
_nextRoleIdentifier = 0;
}
}
[AnyCommand]
public class GhostRoles : IConsoleCommand
{
public string Command => "ghostroles";
public string Description => "Opens the ghost role request window.";
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if(shell.Player != null)
EntitySystem.Get<GhostRoleSystem>().OpenEui((IPlayerSession)shell.Player);
else
shell.WriteLine("You can only open the ghost roles UI on a client.");
}
}
}

View File

@@ -1,49 +0,0 @@
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Observer;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
public class GhostSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GhostComponent, MindRemovedMessage>(OnMindRemovedMessage);
SubscribeLocalEvent<GhostComponent, MindUnvisitedMessage>(OnMindUnvisitedMessage);
}
public override void Shutdown()
{
base.Shutdown();
UnsubscribeLocalEvent<GhostComponent, MindRemovedMessage>(OnMindRemovedMessage);
UnsubscribeLocalEvent<GhostComponent, MindUnvisitedMessage>(OnMindUnvisitedMessage);
}
private void OnMindRemovedMessage(EntityUid uid, GhostComponent component, MindRemovedMessage args)
{
DeleteEntity(uid);
}
private void OnMindUnvisitedMessage(EntityUid uid, GhostComponent component, MindUnvisitedMessage args)
{
DeleteEntity(uid);
}
private void DeleteEntity(EntityUid uid)
{
if (!EntityManager.TryGetEntity(uid, out var entity)
|| entity.Deleted == true
|| entity.LifeStage == EntityLifeStage.Terminating)
return;
if (entity.TryGetComponent<MindComponent>(out var mind))
mind.GhostOnShutdown = false;
entity.Delete();
}
}
}

View File

@@ -1,99 +0,0 @@
#nullable enable
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Atmos;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameTicking;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
public class GodmodeSystem : EntitySystem, IResettingEntitySystem
{
private readonly Dictionary<IEntity, OldEntityInformation> _entities = new();
public void Reset()
{
_entities.Clear();
}
public bool EnableGodmode(IEntity entity)
{
if (_entities.ContainsKey(entity))
{
return false;
}
_entities[entity] = new OldEntityInformation(entity);
if (entity.TryGetComponent(out MovedByPressureComponent? moved))
{
moved.Enabled = false;
}
if (entity.TryGetComponent(out IDamageableComponent? damageable))
{
damageable.AddFlag(DamageFlag.Invulnerable);
}
return true;
}
public bool HasGodmode(IEntity entity)
{
return _entities.ContainsKey(entity);
}
public bool DisableGodmode(IEntity entity)
{
if (!_entities.Remove(entity, out var old))
{
return false;
}
if (entity.TryGetComponent(out MovedByPressureComponent? moved))
{
moved.Enabled = old.MovedByPressure;
}
if (entity.TryGetComponent(out IDamageableComponent? damageable))
{
damageable.RemoveFlag(DamageFlag.Invulnerable);
}
return true;
}
/// <summary>
/// Toggles godmode for a given entity.
/// </summary>
/// <param name="entity">The entity to toggle godmode for.</param>
/// <returns>true if enabled, false if disabled.</returns>
public bool ToggleGodmode(IEntity entity)
{
if (HasGodmode(entity))
{
DisableGodmode(entity);
return false;
}
else
{
EnableGodmode(entity);
return true;
}
}
public class OldEntityInformation
{
public OldEntityInformation(IEntity entity)
{
Entity = entity;
MovedByPressure = entity.IsMovedByPressure();
}
public IEntity Entity { get; }
public bool MovedByPressure { get; }
}
}
}

View File

@@ -1,143 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Content.Server.GameObjects.Components.Gravity;
using Content.Server.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Gravity;
using Content.Shared.GameObjects.EntitySystemMessages.Gravity;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Player;
using Robust.Shared.Random;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
internal sealed class GravitySystem : EntitySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
private const float GravityKick = 100.0f;
private const uint ShakeTimes = 10;
private Dictionary<GridId, uint> _gridsToShake = new();
private float _internalTimer = 0.0f;
public override void Update(float frameTime)
{
_internalTimer += frameTime;
var gridsWithGravity = new List<GridId>();
foreach (var generator in ComponentManager.EntityQuery<GravityGeneratorComponent>(true))
{
if (generator.NeedsUpdate)
{
generator.UpdateState();
}
if (generator.Status == GravityGeneratorStatus.On)
{
gridsWithGravity.Add(generator.Owner.Transform.GridID);
}
}
foreach (var grid in _mapManager.GetAllGrids())
{
if (grid.HasGravity && !gridsWithGravity.Contains(grid.Index))
{
DisableGravity(grid);
}
else if (!grid.HasGravity && gridsWithGravity.Contains(grid.Index))
{
EnableGravity(grid);
}
}
if (_internalTimer > 0.2f)
{
ShakeGrids();
_internalTimer = 0.0f;
}
}
private void EnableGravity(IMapGrid grid)
{
grid.HasGravity = true;
ScheduleGridToShake(grid.Index, ShakeTimes);
var message = new GravityChangedMessage(grid);
RaiseLocalEvent(message);
}
private void DisableGravity(IMapGrid grid)
{
grid.HasGravity = false;
ScheduleGridToShake(grid.Index, ShakeTimes);
var message = new GravityChangedMessage(grid);
RaiseLocalEvent(message);
}
private void ScheduleGridToShake(GridId gridId, uint shakeTimes)
{
if (!_gridsToShake.Keys.Contains(gridId))
{
_gridsToShake.Add(gridId, shakeTimes);
}
else
{
_gridsToShake[gridId] = shakeTimes;
}
// Play the gravity sound
foreach (var player in _playerManager.GetAllPlayers())
{
if (player.AttachedEntity == null
|| player.AttachedEntity.Transform.GridID != gridId) continue;
SoundSystem.Play(Filter.Pvs(player.AttachedEntity), "/Audio/Effects/alert.ogg", player.AttachedEntity);
}
}
private void ShakeGrids()
{
// I have to copy this because C# doesn't allow changing collections while they're
// getting enumerated.
var gridsToShake = new Dictionary<GridId, uint>(_gridsToShake);
foreach (var gridId in _gridsToShake.Keys)
{
if (_gridsToShake[gridId] == 0)
{
gridsToShake.Remove(gridId);
continue;
}
ShakeGrid(gridId);
gridsToShake[gridId] -= 1;
}
_gridsToShake = gridsToShake;
}
private void ShakeGrid(GridId gridId)
{
foreach (var player in _playerManager.GetAllPlayers())
{
if (player.AttachedEntity == null
|| player.AttachedEntity.Transform.GridID != gridId
|| !player.AttachedEntity.TryGetComponent(out CameraRecoilComponent? recoil))
{
continue;
}
recoil.Kick(new Vector2(_random.NextFloat(), _random.NextFloat()) * GravityKick);
}
}
}
}

View File

@@ -1,50 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Content.Server.GameObjects.Components.Interactable;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
internal sealed class HandHeldLightSystem : EntitySystem
{
// TODO: Ideally you'd be able to subscribe to power stuff to get events at certain percentages.. or something?
// But for now this will be better anyway.
private HashSet<HandheldLightComponent> _activeLights = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ActivateHandheldLightMessage>(HandleActivate);
SubscribeLocalEvent<DeactivateHandheldLightMessage>(HandleDeactivate);
}
public override void Shutdown()
{
base.Shutdown();
_activeLights.Clear();
UnsubscribeLocalEvent<ActivateHandheldLightMessage>();
UnsubscribeLocalEvent<DeactivateHandheldLightMessage>();
}
private void HandleActivate(ActivateHandheldLightMessage message)
{
_activeLights.Add(message.Component);
}
private void HandleDeactivate(DeactivateHandheldLightMessage message)
{
_activeLights.Remove(message.Component);
}
public override void Update(float frameTime)
{
foreach (var handheld in _activeLights.ToArray())
{
if (handheld.Deleted || handheld.Paused) continue;
handheld.OnUpdate(frameTime);
}
}
}
}

View File

@@ -1,268 +0,0 @@
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Stack;
using Content.Server.GameObjects.EntitySystems.Click;
using Content.Server.Interfaces.GameObjects.Components.Items;
using Content.Shared.GameObjects.Components.Movement;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Input;
using Content.Shared.Interfaces;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Input.Binding;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Players;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using static Content.Shared.GameObjects.Components.Inventory.EquipmentSlotDefines;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
internal sealed class HandsSystem : EntitySystem
{
private const float ThrowForce = 1.5f; // Throwing force of mobs in Newtons
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<EntRemovedFromContainerMessage>(HandleContainerModified);
SubscribeLocalEvent<EntInsertedIntoContainerMessage>(HandleContainerModified);
SubscribeLocalEvent<HandsComponent, ExaminedEvent>(HandleExamined);
CommandBinds.Builder
.Bind(ContentKeyFunctions.SwapHands, InputCmdHandler.FromDelegate(HandleSwapHands))
.Bind(ContentKeyFunctions.Drop, new PointerInputCmdHandler(HandleDrop))
.Bind(ContentKeyFunctions.ActivateItemInHand, InputCmdHandler.FromDelegate(HandleActivateItem))
.Bind(ContentKeyFunctions.ThrowItemInHand, new PointerInputCmdHandler(HandleThrowItem))
.Bind(ContentKeyFunctions.SmartEquipBackpack, InputCmdHandler.FromDelegate(HandleSmartEquipBackpack))
.Bind(ContentKeyFunctions.SmartEquipBelt, InputCmdHandler.FromDelegate(HandleSmartEquipBelt))
.Register<HandsSystem>();
}
/// <inheritdoc />
public override void Shutdown()
{
base.Shutdown();
CommandBinds.Unregister<HandsSystem>();
}
private static void HandleContainerModified(ContainerModifiedMessage args)
{
if (args.Container.Owner.TryGetComponent(out IHandsComponent? handsComponent))
{
handsComponent.HandleSlotModifiedMaybe(args);
}
}
//TODO: Actually shows all items/clothing/etc.
private void HandleExamined(EntityUid uid, HandsComponent component, ExaminedEvent args)
{
foreach (var inhand in component.GetAllHeldItems())
{
args.Message.AddText($"\n{Loc.GetString("comp-hands-examine", ("user", component.Owner), ("item", inhand.Owner))}");
}
}
private static bool TryGetAttachedComponent<T>(IPlayerSession? session, [NotNullWhen(true)] out T? component)
where T : Component
{
component = default;
var ent = session?.AttachedEntity;
if (ent == null || !ent.IsValid() || !ent.TryGetComponent(out T? comp))
{
return false;
}
component = comp;
return true;
}
private static void HandleSwapHands(ICommonSession? session)
{
if (!TryGetAttachedComponent(session as IPlayerSession, out HandsComponent? handsComp))
{
return;
}
var interactionSystem = Get<InteractionSystem>();
var oldItem = handsComp.GetActiveHand;
handsComp.SwapHands();
var newItem = handsComp.GetActiveHand;
if (oldItem != null)
{
interactionSystem.HandDeselectedInteraction(handsComp.Owner, oldItem.Owner);
}
if (newItem != null)
{
interactionSystem.HandSelectedInteraction(handsComp.Owner, newItem.Owner);
}
}
private bool HandleDrop(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
var ent = ((IPlayerSession?) session)?.AttachedEntity;
if (ent == null || !ent.IsValid())
return false;
if (!ent.TryGetComponent(out HandsComponent? handsComp))
return false;
if (handsComp.ActiveHand == null || handsComp.GetActiveHand == null)
return false;
// It's important to note that the calculations are done in map coordinates (they're absolute).
// They're translated back to EntityCoordinates at the end.
var entMap = ent.Transform.MapPosition;
var targetPos = coords.ToMapPos(EntityManager);
var dropVector = targetPos - entMap.Position;
var targetVector = Vector2.Zero;
if (dropVector != Vector2.Zero)
{
var targetLength = MathF.Min(dropVector.Length, SharedInteractionSystem.InteractionRange - 0.001f); // InteractionRange is reduced due to InRange not dealing with floating point error
var newCoords = new MapCoordinates(dropVector.Normalized * targetLength + entMap.Position, entMap.MapId);
var rayLength = Get<SharedInteractionSystem>().UnobstructedDistance(entMap, newCoords, ignoredEnt: ent);
targetVector = dropVector.Normalized * rayLength;
}
var resultMapCoordinates = new MapCoordinates(entMap.Position + targetVector, entMap.MapId);
var resultEntCoordinates = EntityCoordinates.FromMap(coords.GetParent(EntityManager), resultMapCoordinates);
handsComp.Drop(handsComp.ActiveHand, resultEntCoordinates);
return true;
}
private static void HandleActivateItem(ICommonSession? session)
{
if (!TryGetAttachedComponent(session as IPlayerSession, out HandsComponent? handsComp))
return;
handsComp.ActivateItem();
}
private bool HandleThrowItem(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
var playerEnt = ((IPlayerSession?) session)?.AttachedEntity;
if (playerEnt == null || !playerEnt.IsValid())
return false;
if (!playerEnt.TryGetComponent(out HandsComponent? handsComp))
return false;
if (handsComp.ActiveHand == null || !handsComp.CanDrop(handsComp.ActiveHand))
return false;
var throwEnt = handsComp.GetItem(handsComp.ActiveHand)?.Owner;
if (throwEnt == null)
return false;
if (!handsComp.ThrowItem())
return false;
// throw the item, split off from a stack if it's meant to be thrown individually
if (!throwEnt.TryGetComponent(out StackComponent? stackComp) || stackComp.Count < 2 || !stackComp.ThrowIndividually)
{
handsComp.Drop(handsComp.ActiveHand);
}
else
{
var splitStack = new StackSplitEvent() { Amount = 1, SpawnPosition = playerEnt.Transform.Coordinates };
RaiseLocalEvent(throwEnt.Uid, splitStack);
if (splitStack.Result == null)
return false;
throwEnt = splitStack.Result;
}
var direction = coords.ToMapPos(EntityManager) - playerEnt.Transform.WorldPosition;
if (direction == Vector2.Zero) return true;
direction = direction.Normalized * MathF.Min(direction.Length, 8.0f);
var yeet = direction * ThrowForce * 15;
// Softer yeet in weightlessness
if (playerEnt.IsWeightless())
{
throwEnt.TryThrow(yeet / 4, playerEnt, 10.0f);
}
else
{
throwEnt.TryThrow(yeet, playerEnt);
}
return true;
}
private void HandleSmartEquipBackpack(ICommonSession? session)
{
HandleSmartEquip(session, Slots.BACKPACK);
}
private void HandleSmartEquipBelt(ICommonSession? session)
{
HandleSmartEquip(session, Slots.BELT);
}
private void HandleSmartEquip(ICommonSession? session, Slots equipmentSlot)
{
var plyEnt = ((IPlayerSession?) session)?.AttachedEntity;
if (plyEnt == null || !plyEnt.IsValid())
return;
if (!plyEnt.TryGetComponent(out HandsComponent? handsComp) ||
!plyEnt.TryGetComponent(out InventoryComponent? inventoryComp))
return;
if (!inventoryComp.TryGetSlotItem(equipmentSlot, out ItemComponent? equipmentItem)
|| !equipmentItem.Owner.TryGetComponent<ServerStorageComponent>(out var storageComponent))
{
plyEnt.PopupMessage(Loc.GetString("You have no {0} to take something out of!",
SlotNames[equipmentSlot].ToLower()));
return;
}
var heldItem = handsComp.GetItem(handsComp.ActiveHand)?.Owner;
if (heldItem != null)
{
storageComponent.PlayerInsertHeldEntity(plyEnt);
}
else if (storageComponent.StoredEntities != null)
{
if (storageComponent.StoredEntities.Count == 0)
{
plyEnt.PopupMessage(Loc.GetString("There's nothing in your {0} to take out!",
SlotNames[equipmentSlot].ToLower()));
}
else
{
var lastStoredEntity = Enumerable.Last(storageComponent.StoredEntities);
if (storageComponent.Remove(lastStoredEntity))
handsComp.PutInHandOrDrop(lastStoredEntity.GetComponent<ItemComponent>());
}
}
}
}
}

View File

@@ -1,26 +0,0 @@
using Content.Server.GameObjects.Components.Nutrition;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
public class HungerSystem : EntitySystem
{
private float _accumulatedFrameTime;
public override void Update(float frameTime)
{
_accumulatedFrameTime += frameTime;
if (_accumulatedFrameTime > 1)
{
foreach (var comp in ComponentManager.EntityQuery<HungerComponent>(true))
{
comp.OnUpdate(_accumulatedFrameTime);
}
_accumulatedFrameTime -= 1;
}
}
}
}

View File

@@ -1,35 +0,0 @@
using Content.Server.GameObjects.Components.Chemistry;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
public class HypospraySystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HyposprayComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<HyposprayComponent, ClickAttackEvent>(OnClickAttack);
}
public void OnAfterInteract(EntityUid uid, HyposprayComponent comp, AfterInteractEvent args)
{
if (!args.CanReach)
return;
var target = args.Target;
var user = args.User;
comp.TryDoInject(target, user);
}
public void OnClickAttack(EntityUid uid, HyposprayComponent comp, ClickAttackEvent args)
{
var target = args.TargetEntity;
var user = args.User;
comp.TryDoInject(target, user);
}
}
}

View File

@@ -1,60 +0,0 @@
using Content.Server.GameObjects.Components.Instruments;
using Content.Shared;
using JetBrains.Annotations;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
internal sealed class InstrumentSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
public override void Initialize()
{
base.Initialize();
_cfg.OnValueChanged(CCVars.MaxMidiEventsPerSecond, OnMaxMidiEventsPerSecondChanged, true);
_cfg.OnValueChanged(CCVars.MaxMidiEventsPerBatch, OnMaxMidiEventsPerBatchChanged, true);
_cfg.OnValueChanged(CCVars.MaxMidiBatchesDropped, OnMaxMidiBatchesDroppedChanged, true);
_cfg.OnValueChanged(CCVars.MaxMidiLaggedBatches, OnMaxMidiLaggedBatchesChanged, true);
}
public int MaxMidiEventsPerSecond { get; private set; }
public int MaxMidiEventsPerBatch { get; private set; }
public int MaxMidiBatchesDropped { get; private set; }
public int MaxMidiLaggedBatches { get; private set; }
private void OnMaxMidiLaggedBatchesChanged(int obj)
{
MaxMidiLaggedBatches = obj;
}
private void OnMaxMidiBatchesDroppedChanged(int obj)
{
MaxMidiBatchesDropped = obj;
}
private void OnMaxMidiEventsPerBatchChanged(int obj)
{
MaxMidiEventsPerBatch = obj;
}
private void OnMaxMidiEventsPerSecondChanged(int obj)
{
MaxMidiEventsPerSecond = obj;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var component in ComponentManager.EntityQuery<InstrumentComponent>(true))
{
component.Update(frameTime);
}
}
}
}

View File

@@ -1,195 +0,0 @@
using Content.Server.GameObjects.Components;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Shared.Audio;
using Content.Shared.GameObjects.Components;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.GameObjects.Verbs;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Microsoft.EntityFrameworkCore;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Player;
namespace Content.Server.GameObjects.EntitySystems
{
public class ItemCabinetSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ItemCabinetComponent, MapInitEvent>(OnMapInitialize);
SubscribeLocalEvent<ItemCabinetComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<ItemCabinetComponent, InteractHandEvent>(OnInteractHand);
SubscribeLocalEvent<ItemCabinetComponent, ActivateInWorldEvent>(OnActivateInWorld);
SubscribeLocalEvent<ItemCabinetComponent, TryEjectItemCabinetEvent>(OnTryEjectItemCabinet);
SubscribeLocalEvent<ItemCabinetComponent, TryInsertItemCabinetEvent>(OnTryInsertItemCabinet);
SubscribeLocalEvent<ItemCabinetComponent, ToggleItemCabinetEvent>(OnToggleItemCabinet);
}
private void OnMapInitialize(EntityUid uid, ItemCabinetComponent comp, MapInitEvent args)
{
var owner = EntityManager.GetEntity(uid);
comp.ItemContainer =
owner.EnsureContainer<ContainerSlot>("item_cabinet", out _);
if(comp.SpawnPrototype != null)
comp.ItemContainer.Insert(EntityManager.SpawnEntity(comp.SpawnPrototype, owner.Transform.Coordinates));
UpdateVisuals(comp);
}
private void OnInteractUsing(EntityUid uid, ItemCabinetComponent comp, InteractUsingEvent args)
{
args.Handled = true;
if (!comp.Opened)
{
RaiseLocalEvent(uid, new ToggleItemCabinetEvent(), false);
}
else
{
RaiseLocalEvent(uid, new TryInsertItemCabinetEvent(args.User, args.Used), false);
}
args.Handled = true;
}
private void OnInteractHand(EntityUid uid, ItemCabinetComponent comp, InteractHandEvent args)
{
args.Handled = true;
if (comp.Opened)
{
if (comp.ItemContainer.ContainedEntity == null)
{
RaiseLocalEvent(uid, new ToggleItemCabinetEvent(), false);
return;
}
RaiseLocalEvent(uid, new TryEjectItemCabinetEvent(args.User), false);
}
else
{
RaiseLocalEvent(uid, new ToggleItemCabinetEvent(), false);
}
}
private void OnActivateInWorld(EntityUid uid, ItemCabinetComponent comp, ActivateInWorldEvent args)
{
args.Handled = true;
RaiseLocalEvent(uid, new ToggleItemCabinetEvent(), false);
}
/// <summary>
/// Toggles the ItemCabinet's state.
/// </summary>
private void OnToggleItemCabinet(EntityUid uid, ItemCabinetComponent comp, ToggleItemCabinetEvent args)
{
comp.Opened = !comp.Opened;
ClickLatchSound(comp);
UpdateVisuals(comp);
}
/// <summary>
/// Tries to insert an entity into the ItemCabinet's slot from the user's hands.
/// </summary>
private static void OnTryInsertItemCabinet(EntityUid uid, ItemCabinetComponent comp, TryInsertItemCabinetEvent args)
{
if (comp.ItemContainer.ContainedEntity != null || args.Cancelled || (comp.Whitelist != null && !comp.Whitelist.IsValid(args.Item)))
{
return;
}
if (!args.User.TryGetComponent<HandsComponent>(out var hands) || !hands.Drop(args.Item, comp.ItemContainer))
{
return;
}
UpdateVisuals(comp);
}
/// <summary>
/// Tries to eject the ItemCabinet's item, either into the user's hands or onto the floor.
/// </summary>
private static void OnTryEjectItemCabinet(EntityUid uid, ItemCabinetComponent comp, TryEjectItemCabinetEvent args)
{
if (comp.ItemContainer.ContainedEntity == null || args.Cancelled)
return;
if (args.User.TryGetComponent(out HandsComponent? hands))
{
if (comp.ItemContainer.ContainedEntity.TryGetComponent<ItemComponent>(out var item))
{
comp.Owner.PopupMessage(args.User,
Loc.GetString("comp-item-cabinet-successfully-taken",
("item", comp.ItemContainer.ContainedEntity),
("cabinet", comp.Owner)));
hands.PutInHandOrDrop(item);
}
}
else if (comp.ItemContainer.Remove(comp.ItemContainer.ContainedEntity))
{
comp.ItemContainer.ContainedEntity.Transform.Coordinates = args.User.Transform.Coordinates;
}
UpdateVisuals(comp);
}
private static void UpdateVisuals(ItemCabinetComponent comp)
{
if (comp.Owner.TryGetComponent(out SharedAppearanceComponent? appearance))
{
appearance.SetData(ItemCabinetVisuals.IsOpen, comp.Opened);
appearance.SetData(ItemCabinetVisuals.ContainsItem, comp.ItemContainer.ContainedEntity != null);
}
}
private static void ClickLatchSound(ItemCabinetComponent comp)
{
if (comp.DoorSound == null) return;
SoundSystem.Play(Filter.Pvs(comp.Owner), comp.DoorSound, comp.Owner, AudioHelpers.WithVariation(0.15f));
}
}
public class ToggleItemCabinetEvent : EntityEventArgs
{
}
public class TryEjectItemCabinetEvent : CancellableEntityEventArgs
{
/// <summary>
/// The user who tried to eject the item.
/// </summary>
public IEntity User;
public TryEjectItemCabinetEvent(IEntity user)
{
User = user;
}
}
public class TryInsertItemCabinetEvent : CancellableEntityEventArgs
{
/// <summary>
/// The user who tried to eject the item.
/// </summary>
public IEntity User;
/// <summary>
/// The item to be inserted.
/// </summary>
public IEntity Item;
public TryInsertItemCabinetEvent(IEntity user, IEntity item)
{
User = user;
Item = item;
}
}
}

View File

@@ -1,34 +0,0 @@
using System;
using Content.Shared.GameObjects.Components.Items;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
public class ItemCooldownSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ItemCooldownComponent, RefreshItemCooldownEvent>(OnItemCooldownRefreshed);
}
public void OnItemCooldownRefreshed(EntityUid uid, ItemCooldownComponent comp, RefreshItemCooldownEvent args)
{
comp.CooldownStart = args.LastAttackTime;
comp.CooldownEnd = args.CooldownEnd;
}
}
public class RefreshItemCooldownEvent : EntityEventArgs
{
public TimeSpan LastAttackTime { get; }
public TimeSpan CooldownEnd { get; }
public RefreshItemCooldownEvent(TimeSpan lastAttackTime, TimeSpan cooldownEnd)
{
LastAttackTime = lastAttackTime;
CooldownEnd = cooldownEnd;
}
}
}

View File

@@ -1,66 +0,0 @@
#nullable enable
using System;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Janitorial;
using Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerReceiverUsers;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.Interfaces.GameObjects.Components;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems.Janitorial
{
[UsedImplicitly]
public class LightReplacerSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<LightReplacerComponent, InteractUsingEvent>(HandleInteract);
SubscribeLocalEvent<LightReplacerComponent, AfterInteractEvent>(HandleAfterInteract);
}
public override void Shutdown()
{
base.Shutdown();
UnsubscribeLocalEvent<LightReplacerComponent, InteractUsingEvent>(HandleInteract);
UnsubscribeLocalEvent<LightReplacerComponent, AfterInteractEvent>(HandleAfterInteract);
}
private void HandleAfterInteract(EntityUid uid, LightReplacerComponent component, AfterInteractEvent eventArgs)
{
// standard interaction checks
if (!ActionBlockerSystem.CanUse(eventArgs.User)) return;
if (!eventArgs.CanReach) return;
// behaviour will depends on target type
if (eventArgs.Target != null)
{
// replace broken light in fixture?
if (eventArgs.Target.TryGetComponent(out PoweredLightComponent? fixture))
component.TryReplaceBulb(fixture, eventArgs.User);
// add new bulb to light replacer container?
else if (eventArgs.Target.TryGetComponent(out LightBulbComponent? bulb))
component.TryInsertBulb(bulb, eventArgs.User, true);
}
}
private void HandleInteract(EntityUid uid, LightReplacerComponent component, InteractUsingEvent eventArgs)
{
// standard interaction checks
if (!ActionBlockerSystem.CanInteract(eventArgs.User)) return;
if (eventArgs.Used != null)
{
// want to insert a new light bulb?
if (eventArgs.Used.TryGetComponent(out LightBulbComponent? bulb))
component.TryInsertBulb(bulb, eventArgs.User, true);
// add bulbs from storage?
else if (eventArgs.Used.TryGetComponent(out ServerStorageComponent? storage))
component.TryInsertBulb(storage, eventArgs.User);
}
}
}
}

View File

@@ -1,8 +0,0 @@
namespace Content.Server.GameObjects.EntitySystems.JobQueues
{
public interface IJob
{
JobStatus Status { get; }
void Run();
}
}

View File

@@ -1,232 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Robust.Shared.Log;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.GameObjects.EntitySystems.JobQueues
{
/// <summary>
/// CPU-intensive job that can be suspended and resumed on the main thread
/// </summary>
/// <remarks>
/// Implementations should overload <see cref="Process"/>.
/// Inside <see cref="Process"/>, implementations should only await on <see cref="SuspendNow"/>,
/// <see cref="SuspendIfOutOfTime"/>, or <see cref="WaitAsyncTask"/>.
/// </remarks>
/// <typeparam name="T">The type of result this job generates</typeparam>
public abstract class Job<T> : IJob
{
public JobStatus Status { get; private set; } = JobStatus.Pending;
/// <summary>
/// Represents the status of this job as a regular task.
/// </summary>
public Task<T?> AsTask { get; }
public T? Result { get; private set; }
public Exception? Exception { get; private set; }
protected CancellationToken Cancellation { get; }
public double DebugTime { get; private set; }
private readonly double _maxTime;
protected readonly IStopwatch StopWatch;
// TCS for the Task property.
private readonly TaskCompletionSource<T?> _taskTcs;
// TCS to call to resume the suspended job.
private TaskCompletionSource<object?>? _resume;
private Task? _workInProgress;
protected Job(double maxTime, CancellationToken cancellation = default)
: this(maxTime, new Stopwatch(), cancellation)
{
}
protected Job(double maxTime, IStopwatch stopwatch, CancellationToken cancellation = default)
{
_maxTime = maxTime;
StopWatch = stopwatch;
Cancellation = cancellation;
_taskTcs = new TaskCompletionSource<T?>();
AsTask = _taskTcs.Task;
}
/// <summary>
/// Suspends the current task immediately, yielding to other running jobs.
/// </summary>
/// <remarks>
/// This does not stop the job queue from un-suspending the current task immediately again,
/// if there is still time left over.
/// </remarks>
protected Task SuspendNow()
{
DebugTools.AssertNull(_resume);
_resume = new TaskCompletionSource<object?>();
Status = JobStatus.Paused;
DebugTime += StopWatch.Elapsed.TotalSeconds;
return _resume.Task;
}
protected ValueTask SuspendIfOutOfTime()
{
DebugTools.AssertNull(_resume);
// ReSharper disable once CompareOfFloatsByEqualityOperator
if (StopWatch.Elapsed.TotalSeconds <= _maxTime || _maxTime == 0.0)
{
return new ValueTask();
}
return new ValueTask(SuspendNow());
}
/// <summary>
/// Wrapper to await on an external task.
/// </summary>
protected async Task<TTask> WaitAsyncTask<TTask>(Task<TTask> task)
{
DebugTools.AssertNull(_resume);
Status = JobStatus.Waiting;
DebugTime += StopWatch.Elapsed.TotalSeconds;
var result = await task;
// Immediately block on resume so that everything stays correct.
Status = JobStatus.Paused;
_resume = new TaskCompletionSource<object?>();
await _resume.Task;
return result;
}
/// <summary>
/// Wrapper to safely await on an external task.
/// </summary>
protected async Task WaitAsyncTask(Task task)
{
DebugTools.AssertNull(_resume);
Status = JobStatus.Waiting;
DebugTime += StopWatch.Elapsed.TotalSeconds;
await task;
// Immediately block on resume so that everything stays correct.
_resume = new TaskCompletionSource<object?>();
Status = JobStatus.Paused;
await _resume.Task;
}
public void Run()
{
StopWatch.Restart();
_workInProgress ??= ProcessWrap();
if (Status == JobStatus.Finished)
{
return;
}
DebugTools.Assert(_resume != null,
"Run() called without resume. Was this called while the job is in Waiting state?");
var resume = _resume;
_resume = null;
Status = JobStatus.Running;
if (Cancellation.IsCancellationRequested)
{
resume?.TrySetCanceled();
}
else
{
resume?.SetResult(null);
}
if (Status != JobStatus.Finished && Status != JobStatus.Waiting)
{
DebugTools.Assert(_resume != null,
"Job suspended without _resume set. Did you await on an external task without using WaitAsyncTask?");
}
}
protected abstract Task<T?> Process();
private async Task ProcessWrap()
{
try
{
Cancellation.ThrowIfCancellationRequested();
// Making sure that the task starts inside the Running block,
// where the stopwatch is correctly set and such.
await SuspendNow();
Result = await Process();
// TODO: not sure if it makes sense to connect Task directly up
// to the return value of this method/Process.
// Maybe?
_taskTcs.TrySetResult(Result);
}
catch (TaskCanceledException)
{
_taskTcs.TrySetCanceled();
}
catch (Exception e)
{
// TODO: Should this be exposed differently?
// I feel that people might forget to check whether the job failed.
Logger.ErrorS("job", "Job failed on exception:\n{0}", e);
Exception = e;
_taskTcs.TrySetException(e);
}
finally
{
if (Status != JobStatus.Waiting)
{
// If we're blocked on waiting and the waiting task goes cancel/exception,
// this timing info would not be correct.
DebugTime += StopWatch.Elapsed.TotalSeconds;
}
Status = JobStatus.Finished;
}
}
}
public enum JobStatus
{
/// <summary>
/// Job has been created and has not been ran yet.
/// </summary>
Pending,
/// <summary>
/// Job is currently (yes, right now!) executing.
/// </summary>
Running,
/// <summary>
/// Job is paused due to CPU limits.
/// </summary>
Paused,
/// <summary>
/// Job is paused because of waiting on external task.
/// </summary>
Waiting,
/// <summary>
/// Job is done.
/// </summary>
// TODO: Maybe have a different status code for cancelled/failed on exception?
Finished,
}
}

View File

@@ -1,4 +0,0 @@
namespace Content.Server.GameObjects.EntitySystems.JobQueues.Queues
{
public sealed class AiActionJobQueue : JobQueue {}
}

View File

@@ -1,73 +0,0 @@
using System.Collections.Generic;
using Robust.Shared.Timing;
namespace Content.Server.GameObjects.EntitySystems.JobQueues.Queues
{
public class JobQueue
{
private readonly IStopwatch _stopwatch;
public JobQueue() : this(new Stopwatch())
{
}
public JobQueue(IStopwatch stopwatch)
{
_stopwatch = stopwatch;
}
/// <summary>
/// How long the job's allowed to run for before suspending
/// </summary>
public virtual double MaxTime => 0.002;
private readonly Queue<IJob> _pendingQueue = new();
private readonly List<IJob> _waitingJobs = new();
public void EnqueueJob(IJob job)
{
_pendingQueue.Enqueue(job);
}
public void Process()
{
// Move all finished waiting jobs back into the regular queue.
foreach (var waitingJob in _waitingJobs)
{
if (waitingJob.Status != JobStatus.Waiting)
{
_pendingQueue.Enqueue(waitingJob);
}
}
_waitingJobs.RemoveAll(p => p.Status != JobStatus.Waiting);
// At one point I tried making the pathfinding queue multi-threaded but ehhh didn't go great
// Could probably try it again at some point
// it just seemed slow af but I was probably doing something dumb with semaphores
_stopwatch.Restart();
// Although the jobs can stop themselves we might be able to squeeze more of them in the allotted time
while (_stopwatch.Elapsed.TotalSeconds < MaxTime && _pendingQueue.TryDequeue(out var job))
{
// Deque and re-enqueue these to cycle them through to avoid starvation if we've got a lot of jobs.
job.Run();
switch (job.Status)
{
case JobStatus.Finished:
continue;
case JobStatus.Waiting:
// If this job goes into waiting we have to move it into a separate list.
// Otherwise we'd just be spinning like mad here for external IO or such.
_waitingJobs.Add(job);
break;
default:
_pendingQueue.Enqueue(job);
break;
}
}
}
}
}

View File

@@ -1,7 +0,0 @@
namespace Content.Server.GameObjects.EntitySystems.JobQueues.Queues
{
public sealed class PathfindingJobQueue : JobQueue
{
public override double MaxTime => 0.003;
}
}

View File

@@ -1,21 +0,0 @@
using Content.Server.GameObjects.Components.Research;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
internal sealed class LatheSystem : EntitySystem
{
public override void Update(float frameTime)
{
foreach (var comp in ComponentManager.EntityQuery<LatheComponent>(true))
{
if (comp.Producing == false && comp.Queue.Count > 0)
{
comp.Produce(comp.Queue.Dequeue());
}
}
}
}
}

View File

@@ -1,22 +0,0 @@
using Content.Server.Interfaces;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
public class ListeningSystem : EntitySystem
{
public void PingListeners(IEntity source, string message)
{
foreach (var listener in ComponentManager.EntityQuery<IListen>(true))
{
// TODO: Map Position distance
if (listener.CanListen(message, source))
{
listener.Listen(message, source);
}
}
}
}
}

View File

@@ -1,19 +0,0 @@
using Content.Server.GameObjects.Components.Medical;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
internal sealed class MedicalScannerSystem : EntitySystem
{
public override void Update(float frameTime)
{
foreach (var comp in ComponentManager.EntityQuery<MedicalScannerComponent>(true))
{
comp.Update(frameTime);
}
}
}
}

View File

@@ -1,20 +0,0 @@
using Content.Server.GameObjects.Components.Metabolism;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
public class MetabolismSystem : EntitySystem
{
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var metabolism in ComponentManager.EntityQuery<MetabolismComponent>(true))
{
metabolism.Update(frameTime);
}
}
}
}

View File

@@ -1,19 +0,0 @@
using Content.Server.GameObjects.Components.Kitchen;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
internal sealed class MicrowaveSystem : EntitySystem
{
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var comp in ComponentManager.EntityQuery<MicrowaveComponent>(true))
{
comp.OnUpdate();
}
}
}
}

View File

@@ -1,27 +0,0 @@
using Content.Server.GameObjects.Components.Morgue;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
public class MorgueSystem : EntitySystem
{
private float _accumulatedFrameTime;
public override void Update(float frameTime)
{
_accumulatedFrameTime += frameTime;
if (_accumulatedFrameTime >= 10)
{
foreach (var morgue in ComponentManager.EntityQuery<MorgueEntityStorageComponent>(true))
{
morgue.Update();
}
_accumulatedFrameTime -= 10;
}
}
}
}

View File

@@ -1,48 +0,0 @@
using System.Linq;
using Content.Server.GameObjects.Components.NodeContainer;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
public class NodeContainerSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NodeContainerComponent, PhysicsBodyTypeChangedEvent>(OnBodyTypeChanged);
SubscribeLocalEvent<NodeContainerComponent, RotateEvent>(OnRotateEvent);
}
public override void Shutdown()
{
base.Shutdown();
UnsubscribeLocalEvent<NodeContainerComponent, PhysicsBodyTypeChangedEvent>(OnBodyTypeChanged);
UnsubscribeLocalEvent<NodeContainerComponent, RotateEvent>(OnRotateEvent);
}
private void OnBodyTypeChanged(EntityUid uid, NodeContainerComponent component, PhysicsBodyTypeChangedEvent args)
{
component.AnchorUpdate();
}
private void OnRotateEvent(EntityUid uid, NodeContainerComponent container, RotateEvent ev)
{
if (ev.NewRotation == ev.OldRotation)
{
return;
}
foreach (var node in container.Nodes.Values)
{
if (node is not IRotatableNode rotatableNode) continue;
rotatableNode.RotateEvent(ev);
}
}
}
}

View File

@@ -1,46 +0,0 @@
using System.Collections.Generic;
using Content.Server.GameObjects.Components.NodeContainer;
using Content.Server.GameObjects.Components.NodeContainer.NodeGroups;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
public class NodeGroupSystem : EntitySystem
{
private readonly HashSet<INodeGroup> _dirtyNodeGroups = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NodeContainerComponent, SnapGridPositionChangedEvent>(OnSnapGridPositionChanged);
}
private void OnSnapGridPositionChanged(EntityUid uid, NodeContainerComponent component, SnapGridPositionChangedEvent args)
{
foreach (var node in component.Nodes.Values)
{
node.OnSnapGridMove();
}
}
public void AddDirtyNodeGroup(INodeGroup nodeGroup)
{
_dirtyNodeGroups.Add(nodeGroup);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var group in _dirtyNodeGroups)
{
group.RemakeGroup();
}
_dirtyNodeGroups.Clear();
}
}
}

View File

@@ -1,42 +0,0 @@
#nullable enable
using Content.Server.GameObjects.Components.PA;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
public class ParticleAcceleratorPartSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
EntityManager.EventBus.SubscribeEvent<RotateEvent>(EventSource.Local, this, RotateEvent);
SubscribeLocalEvent<ParticleAcceleratorPartComponent, PhysicsBodyTypeChangedEvent>(BodyTypeChanged);
}
public override void Shutdown()
{
base.Shutdown();
UnsubscribeLocalEvent<ParticleAcceleratorPartComponent, PhysicsBodyTypeChangedEvent>();
}
private static void BodyTypeChanged(
EntityUid uid,
ParticleAcceleratorPartComponent component,
PhysicsBodyTypeChangedEvent args)
{
component.OnAnchorChanged();
}
private static void RotateEvent(RotateEvent ev)
{
if (ev.Sender.TryGetComponent(out ParticleAcceleratorPartComponent? part))
{
part.Rotated();
}
}
}
}

View File

@@ -1,81 +0,0 @@
using System.Collections.Generic;
using Content.Server.Botany;
using Content.Server.GameObjects.Components.Botany;
using Content.Shared.GameTicking;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
public class PlantSystem : EntitySystem, IResettingEntitySystem
{
[Dependency] private readonly IComponentManager _componentManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private int _nextUid = 0;
private readonly Dictionary<int, Seed> _seeds = new();
private float _timer = 0f;
public IReadOnlyDictionary<int, Seed> Seeds => _seeds;
public override void Initialize()
{
base.Initialize();
PopulateDatabase();
}
private void PopulateDatabase()
{
_nextUid = 0;
_seeds.Clear();
foreach (var seed in _prototypeManager.EnumeratePrototypes<Seed>())
{
AddSeedToDatabase(seed);
}
}
public bool AddSeedToDatabase(Seed seed)
{
// If it's not -1, it's already in the database. Probably.
if (seed.Uid != -1)
return false;
seed.Uid = GetNextSeedUid();
_seeds[seed.Uid] = seed;
return true;
}
private int GetNextSeedUid()
{
return _nextUid++;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
_timer += frameTime;
if (_timer < 3f)
return;
_timer = 0f;
foreach (var plantHolder in _componentManager.EntityQuery<PlantHolderComponent>(true))
{
plantHolder.Update();
}
}
public void Reset()
{
PopulateDatabase();
}
}
}

View File

@@ -1,202 +0,0 @@
#nullable enable
using System;
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Observer;
using Content.Server.GameObjects.Components.Pointing;
using Content.Server.Players;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.Input;
using Content.Shared.Interfaces;
using Content.Shared.Utility;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Input.Binding;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Players;
using Robust.Shared.Timing;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
internal sealed class PointingSystem : EntitySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
private static readonly TimeSpan PointDelay = TimeSpan.FromSeconds(0.5f);
/// <summary>
/// A dictionary of players to the last time that they
/// pointed at something.
/// </summary>
private readonly Dictionary<ICommonSession, TimeSpan> _pointers = new();
private const float PointingRange = 15f;
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.NewStatus != SessionStatus.Disconnected)
{
return;
}
_pointers.Remove(e.Session);
}
// TODO: FOV
private void SendMessage(IEntity source, IList<IPlayerSession> viewers, IEntity? pointed, string selfMessage,
string viewerMessage, string? viewerPointedAtMessage = null)
{
foreach (var viewer in viewers)
{
var viewerEntity = viewer.AttachedEntity;
if (viewerEntity == null)
{
continue;
}
var message = viewerEntity == source
? selfMessage
: viewerEntity == pointed && viewerPointedAtMessage != null
? viewerPointedAtMessage
: viewerMessage;
source.PopupMessage(viewerEntity, message);
}
}
public bool InRange(IEntity pointer, EntityCoordinates coordinates)
{
if (pointer.HasComponent<GhostComponent>()){
return pointer.Transform.Coordinates.InRange(EntityManager, coordinates, 15);
}
else
{
return pointer.InRangeUnOccluded(coordinates, 15, e => e == pointer);
}
}
public bool TryPoint(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
var player = (session as IPlayerSession)?.ContentData()?.Mind?.CurrentEntity;
if (player == null)
{
return false;
}
if (_pointers.TryGetValue(session!, out var lastTime) &&
_gameTiming.CurTime < lastTime + PointDelay)
{
return false;
}
if (EntityManager.TryGetEntity(uid, out var entity) && entity.HasComponent<PointingArrowComponent>())
{
// this is a pointing arrow. no pointing here...
return false;
}
if (!InRange(player, coords))
{
player.PopupMessage(Loc.GetString("You can't reach there!"));
return false;
}
if (ActionBlockerSystem.CanChangeDirection(player))
{
var diff = coords.ToMapPos(EntityManager) - player.Transform.MapPosition.Position;
if (diff.LengthSquared > 0.01f)
{
player.Transform.LocalRotation = new Angle(diff);
}
}
var arrow = EntityManager.SpawnEntity("pointingarrow", coords);
var layer = (int)VisibilityFlags.Normal;
if (player.TryGetComponent(out VisibilityComponent? playerVisibility))
{
var arrowVisibility = arrow.EnsureComponent<VisibilityComponent>();
layer = arrowVisibility.Layer = playerVisibility.Layer;
}
// Get players that are in range and whose visibility layer matches the arrow's.
var viewers = _playerManager.GetPlayersBy((playerSession) =>
{
var ent = playerSession.ContentData()?.Mind?.CurrentEntity;
if (ent is null || (!ent.TryGetComponent<EyeComponent>(out var eyeComp) || (eyeComp.VisibilityMask & layer) != 0))
return false;
return ent.Transform.MapPosition.InRange(player.Transform.MapPosition, PointingRange);
});
string selfMessage;
string viewerMessage;
string? viewerPointedAtMessage = null;
if (EntityManager.TryGetEntity(uid, out var pointed))
{
selfMessage = player == pointed
? Loc.GetString("You point at yourself.")
: Loc.GetString("You point at {0:theName}.", pointed);
viewerMessage = player == pointed
? $"{player.Name} {Loc.GetString("points at {0:themself}.", player)}"
: $"{player.Name} {Loc.GetString("points at {0:theName}.", pointed)}";
viewerPointedAtMessage = $"{player.Name} {Loc.GetString("points at you.")}";
}
else
{
var tileRef = _mapManager.GetGrid(coords.GetGridId(EntityManager)).GetTileRef(coords);
var tileDef = _tileDefinitionManager[tileRef.Tile.TypeId];
selfMessage = Loc.GetString("You point at {0}.", tileDef.DisplayName);
viewerMessage = $"{player.Name} {Loc.GetString("points at {0}.", tileDef.DisplayName)}";
}
_pointers[session!] = _gameTiming.CurTime;
SendMessage(player, viewers, pointed, selfMessage, viewerMessage, viewerPointedAtMessage);
return true;
}
public override void Initialize()
{
base.Initialize();
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
CommandBinds.Builder
.Bind(ContentKeyFunctions.Point, new PointerInputCmdHandler(TryPoint))
.Register<PointingSystem>();
}
public override void Shutdown()
{
base.Shutdown();
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
_pointers.Clear();
}
public override void Update(float frameTime)
{
foreach (var component in ComponentManager.EntityQuery<PointingArrowComponent>(true))
{
component.Update(frameTime);
}
}
}
}

View File

@@ -1,19 +0,0 @@
#nullable enable
using Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerReceiverUsers;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
internal sealed class BaseChargerSystem : EntitySystem
{
public override void Update(float frameTime)
{
foreach (var comp in ComponentManager.EntityQuery<BaseCharger>(true))
{
comp.OnUpdate(frameTime);
}
}
}
}

View File

@@ -1,19 +0,0 @@
#nullable enable
using Content.Server.GameObjects.Components.Power.PowerNetComponents;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
internal sealed class BatteryDischargerSystem : EntitySystem
{
public override void Update(float frameTime)
{
foreach (var comp in ComponentManager.EntityQuery<BatteryDischargerComponent>(false))
{
comp.Update(frameTime);
}
}
}
}

View File

@@ -1,19 +0,0 @@
#nullable enable
using Content.Server.GameObjects.Components.Power.PowerNetComponents;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
internal sealed class BatteryStorageSystem : EntitySystem
{
public override void Update(float frameTime)
{
foreach (var comp in ComponentManager.EntityQuery<BatteryStorageComponent>(false))
{
comp.Update(frameTime);
}
}
}
}

View File

@@ -1,19 +0,0 @@
#nullable enable
using Content.Server.GameObjects.Components.Power;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
public class BatterySystem : EntitySystem
{
public override void Update(float frameTime)
{
foreach (var comp in ComponentManager.EntityQuery<BatteryComponent>(true))
{
comp.OnUpdate(frameTime);
}
}
}
}

View File

@@ -1,19 +0,0 @@
#nullable enable
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
internal sealed class PowerApcSystem : EntitySystem
{
public override void Update(float frameTime)
{
foreach (var apc in ComponentManager.EntityQuery<ApcComponent>(false))
{
apc.Update();
}
}
}
}

View File

@@ -1,20 +0,0 @@
#nullable enable
using Content.Server.GameObjects.Components.Power.PowerNetComponents;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
public class PowerNetSystem : EntitySystem
{
[Dependency] private readonly IPowerNetManager _powerNetManager = default!;
public override void Update(float frameTime)
{
base.Update(frameTime);
_powerNetManager.Update(frameTime);
}
}
}

View File

@@ -1,30 +0,0 @@
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
public sealed class PowerReceiverSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PowerReceiverComponent, PhysicsBodyTypeChangedEvent>(BodyTypeChanged);
}
public override void Shutdown()
{
base.Shutdown();
UnsubscribeLocalEvent<PowerReceiverComponent, PhysicsBodyTypeChangedEvent>();
}
private static void BodyTypeChanged(
EntityUid uid,
PowerReceiverComponent component,
PhysicsBodyTypeChangedEvent args)
{
component.AnchorUpdate();
}
}
}

View File

@@ -1,19 +0,0 @@
#nullable enable
using Content.Server.GameObjects.Components.Power.PowerNetComponents;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
internal class PowerSmesSystem : EntitySystem
{
public override void Update(float frameTime)
{
foreach (var comp in ComponentManager.EntityQuery<SmesComponent>(true))
{
comp.OnUpdate();
}
}
}
}

View File

@@ -1,32 +0,0 @@
#nullable enable
using Content.Server.GameObjects.Components.Power.PowerNetComponents;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
/// <summary>
/// Responsible for updating solar control consoles.
/// </summary>
[UsedImplicitly]
internal sealed class PowerSolarControlConsoleSystem : EntitySystem
{
/// <summary>
/// Timer used to avoid updating the UI state every frame (which would be overkill)
/// </summary>
private float _updateTimer;
public override void Update(float frameTime)
{
_updateTimer += frameTime;
if (_updateTimer >= 1)
{
_updateTimer -= 1;
foreach (var component in ComponentManager.EntityQuery<SolarControlConsoleComponent>(true))
{
component.UpdateUIState();
}
}
}
}
}

View File

@@ -1,150 +0,0 @@
#nullable enable
using System;
using System.Linq;
using Content.Server.GameObjects.Components.Power.PowerNetComponents;
using Content.Shared.Physics;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Broadphase;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.GameObjects.EntitySystems
{
/// <summary>
/// Responsible for maintaining the solar-panel sun angle and updating <see cref='SolarPanelComponent'/> coverage.
/// </summary>
[UsedImplicitly]
internal sealed class PowerSolarSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
/// <summary>
/// The current sun angle.
/// </summary>
public Angle TowardsSun = Angle.Zero;
/// <summary>
/// The current sun angular velocity. (This is changed in Initialize)
/// </summary>
public Angle SunAngularVelocity = Angle.Zero;
/// <summary>
/// The distance before the sun is considered to have been 'visible anyway'.
/// This value, like the occlusion semantics, is borrowed from all the other SS13 stations with solars.
/// </summary>
public float SunOcclusionCheckDistance = 20;
/// <summary>
/// This is the per-second value used to reduce solar panel coverage updates
/// (and the resulting occlusion raycasts)
/// to within sane boundaries.
/// Keep in mind, this is not exact, as the random interval is also applied.
/// </summary>
public TimeSpan SolarCoverageUpdateInterval = TimeSpan.FromSeconds(0.5);
/// <summary>
/// A random interval used to stagger solar coverage updates reliably.
/// </summary>
public TimeSpan SolarCoverageUpdateRandomInterval = TimeSpan.FromSeconds(0.5);
/// <summary>
/// TODO: *Should be moved into the solar tracker when powernet allows for it.*
/// The current target panel rotation.
/// </summary>
public Angle TargetPanelRotation = Angle.Zero;
/// <summary>
/// TODO: *Should be moved into the solar tracker when powernet allows for it.*
/// The current target panel velocity.
/// </summary>
public Angle TargetPanelVelocity = Angle.Zero;
/// <summary>
/// TODO: *Should be moved into the solar tracker when powernet allows for it.*
/// Last update of total panel power.
/// </summary>
public float TotalPanelPower = 0;
public override void Initialize()
{
// Initialize the sun to something random
TowardsSun = MathHelper.TwoPi * _robustRandom.NextDouble();
SunAngularVelocity = Angle.FromDegrees(0.1 + ((_robustRandom.NextDouble() - 0.5) * 0.05));
}
public override void Update(float frameTime)
{
TowardsSun += SunAngularVelocity * frameTime;
TowardsSun = TowardsSun.Reduced();
TargetPanelRotation += TargetPanelVelocity * frameTime;
TargetPanelRotation = TargetPanelRotation.Reduced();
TotalPanelPower = 0;
foreach (var panel in ComponentManager.EntityQuery<SolarPanelComponent>(true))
{
// There's supposed to be rotational logic here, but that implies putting it somewhere.
panel.Owner.Transform.WorldRotation = TargetPanelRotation;
if (panel.TimeOfNextCoverageUpdate < _gameTiming.CurTime)
{
// Setup the next coverage check.
TimeSpan future = SolarCoverageUpdateInterval + (SolarCoverageUpdateRandomInterval * _robustRandom.NextDouble());
panel.TimeOfNextCoverageUpdate = _gameTiming.CurTime + future;
UpdatePanelCoverage(panel);
}
TotalPanelPower += panel.Coverage * panel.MaxSupply;
}
}
private void UpdatePanelCoverage(SolarPanelComponent panel) {
IEntity entity = panel.Owner;
// So apparently, and yes, I *did* only find this out later,
// this is just a really fancy way of saying "Lambert's law of cosines".
// ...I still think this explaination makes more sense.
// In the 'sunRelative' coordinate system:
// the sun is considered to be an infinite distance directly up.
// this is the rotation of the panel relative to that.
// directly upwards (theta = 0) = coverage 1
// left/right 90 degrees (abs(theta) = (pi / 2)) = coverage 0
// directly downwards (abs(theta) = pi) = coverage -1
// as TowardsSun + = CCW,
// panelRelativeToSun should - = CW
var panelRelativeToSun = entity.Transform.WorldRotation - TowardsSun;
// essentially, given cos = X & sin = Y & Y is 'downwards',
// then for the first 90 degrees of rotation in either direction,
// this plots the lower-right quadrant of a circle.
// now basically assume a line going from the negated X/Y to there,
// and that's the hypothetical solar panel.
//
// since, again, the sun is considered to be an infinite distance upwards,
// this essentially means Cos(panelRelativeToSun) is half of the cross-section,
// and since the full cross-section has a max of 2, effectively-halving it is fine.
//
// as for when it goes negative, it only does that when (abs(theta) > pi)
// and that's expected behavior.
float coverage = (float)Math.Max(0, Math.Cos(panelRelativeToSun));
if (coverage > 0)
{
// Determine if the solar panel is occluded, and zero out coverage if so.
// FIXME: The "Opaque" collision group doesn't seem to work right now.
var ray = new CollisionRay(entity.Transform.WorldPosition, TowardsSun.ToVec(), (int) CollisionGroup.Opaque);
var rayCastResults = EntitySystem.Get<SharedBroadPhaseSystem>().IntersectRay(entity.Transform.MapID, ray, SunOcclusionCheckDistance, entity);
if (rayCastResults.Any())
coverage = 0;
}
// Total coverage calculated; apply it to the panel.
panel.Coverage = coverage;
}
}
}

View File

@@ -1,25 +0,0 @@
using Content.Server.GameObjects.Components.Projectiles;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
internal sealed class ProjectileSystem : EntitySystem
{
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var component in ComponentManager.EntityQuery<ProjectileComponent>(true))
{
component.TimeLeft -= frameTime;
if (component.TimeLeft <= 0)
{
component.Owner.Delete();
}
}
}
}
}

View File

@@ -1,49 +0,0 @@
using Content.Server.GameObjects.Components.Fluids;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
internal sealed class PuddleSystem : EntitySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
public override void Initialize()
{
base.Initialize();
_mapManager.TileChanged += HandleTileChanged;
}
public override void Shutdown()
{
base.Shutdown();
_mapManager.TileChanged -= HandleTileChanged;
}
//TODO: Replace all this with an Unanchored event that deletes the puddle
private void HandleTileChanged(object? sender, TileChangedEventArgs eventArgs)
{
// If this gets hammered you could probably queue up all the tile changes every tick but I doubt that would ever happen.
foreach (var puddle in ComponentManager.EntityQuery<PuddleComponent>(true))
{
// If the tile becomes space then delete it (potentially change by design)
var puddleTransform = puddle.Owner.Transform;
if(!puddleTransform.Anchored)
continue;
var grid = _mapManager.GetGrid(puddleTransform.GridID);
if (eventArgs.NewTile.GridIndex == puddle.Owner.Transform.GridID &&
grid.TileIndicesFor(puddleTransform.Coordinates) == eventArgs.NewTile.GridIndices &&
eventArgs.NewTile.Tile.IsEmpty)
{
puddle.Owner.QueueDelete();
break; // Currently it's one puddle per tile, if that changes remove this
}
}
}
}
}

View File

@@ -1,30 +0,0 @@
using Content.Server.GameObjects.Components.Pulling;
using Content.Shared.GameObjects.EntitySystemMessages.Pulling;
using Content.Shared.GameObjects.EntitySystems;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
public class PullingSystem : SharedPullingSystem
{
public override void Initialize()
{
base.Initialize();
UpdatesAfter.Add(typeof(PhysicsSystem));
SubscribeLocalEvent<PullableComponent, PullableMoveMessage>(OnPullableMove);
SubscribeLocalEvent<PullableComponent, PullableStopMovingMessage>(OnPullableStopMove);
}
public override void Shutdown()
{
base.Shutdown();
UnsubscribeLocalEvent<PullableComponent, PullableMoveMessage>(OnPullableMove);
UnsubscribeLocalEvent<PullableComponent, PullableStopMovingMessage>(OnPullableStopMove);
}
}
}

View File

@@ -1,32 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Content.Server.Interfaces;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
public class RadioSystem : EntitySystem
{
private readonly List<string> _messages = new();
public void SpreadMessage(IRadio source, IEntity speaker, string message, int channel)
{
if (_messages.Contains(message)) return;
_messages.Add(message);
foreach (var radio in ComponentManager.EntityQuery<IRadio>(true))
{
if (radio.Channels.Contains(channel))
{
//TODO: once voice identity gets added, pass into receiver via source.GetSpeakerVoice()
radio.Receive(message, channel, speaker);
}
}
_messages.Remove(message);
}
}
}

View File

@@ -1,19 +0,0 @@
using Content.Server.GameObjects.Components.Kitchen;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
internal sealed class ReagentGrinderSystem : EntitySystem
{
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var comp in ComponentManager.EntityQuery<ReagentGrinderComponent>(true))
{
comp.OnUpdate();
}
}
}
}

View File

@@ -1,83 +0,0 @@
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Research;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
public class ResearchSystem : EntitySystem
{
private const float ResearchConsoleUIUpdateTime = 30f;
private float _timer = ResearchConsoleUIUpdateTime;
private readonly List<ResearchServerComponent> _servers = new();
public IReadOnlyList<ResearchServerComponent> Servers => _servers;
public bool RegisterServer(ResearchServerComponent server)
{
if (_servers.Contains(server)) return false;
_servers.Add(server);
return true;
}
public void UnregisterServer(ResearchServerComponent server)
{
_servers.Remove(server);
}
public ResearchServerComponent? GetServerById(int id)
{
foreach (var server in Servers)
{
if (server.Id == id) return server;
}
return null;
}
public string[] GetServerNames()
{
var list = new string[Servers.Count];
for (var i = 0; i < Servers.Count; i++)
{
list[i] = Servers[i].ServerName;
}
return list;
}
public int[] GetServerIds()
{
var list = new int[Servers.Count];
for (var i = 0; i < Servers.Count; i++)
{
list[i] = Servers[i].Id;
}
return list;
}
public override void Update(float frameTime)
{
_timer += frameTime;
foreach (var server in _servers)
{
server.Update(frameTime);
}
if (_timer >= ResearchConsoleUIUpdateTime)
{
foreach (var console in ComponentManager.EntityQuery<ResearchConsoleComponent>())
{
console.UpdateUserInterface();
}
_timer = 0f;
}
}
}
}

View File

@@ -1,18 +0,0 @@
using Content.Server.GameObjects.Components.Pointing;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
internal sealed class RoguePointingSystem : EntitySystem
{
public override void Update(float frameTime)
{
foreach (var component in ComponentManager.EntityQuery<RoguePointingArrowComponent>(true))
{
component.Update(frameTime);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More