Re-organize all projects (#4166)
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Server.GameObjects.EntitySystems
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class CombatModeSystem : SharedCombatModeSystem
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace Content.Server.GameObjects.EntitySystems.JobQueues
|
||||
{
|
||||
public interface IJob
|
||||
{
|
||||
JobStatus Status { get; }
|
||||
void Run();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
namespace Content.Server.GameObjects.EntitySystems.JobQueues.Queues
|
||||
{
|
||||
public sealed class AiActionJobQueue : JobQueue {}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Content.Server.GameObjects.EntitySystems.JobQueues.Queues
|
||||
{
|
||||
public sealed class PathfindingJobQueue : JobQueue
|
||||
{
|
||||
public override double MaxTime => 0.003;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user