Files
tbd-station-14/Content.Server/Spreader/SpreaderSystem.cs
Tornado Tech 21bc3bfa22 Cleans up tag system (#28272)
* Updated tag system

* Added params methods

* Fixed tag integration tests

* Fixed params methods recursion

* Revert has All/Any tag one argument realisation

* Updated tag integration tests

* Shit happens

* Added individual List/HashSet methods, docs, tests
2024-06-02 11:11:19 +10:00

339 lines
12 KiB
C#

using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Shuttles.Components;
using Content.Shared.Atmos;
using Content.Shared.Spreader;
using Content.Shared.Tag;
using Robust.Shared.Collections;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Spreader;
/// <summary>
/// Handles generic spreading logic, where one anchored entity spreads to neighboring tiles.
/// </summary>
public sealed class SpreaderSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly TagSystem _tag = default!;
/// <summary>
/// Cached maximum number of updates per spreader prototype. This is applied per-grid.
/// </summary>
private Dictionary<string, int> _prototypeUpdates = default!;
/// <summary>
/// Remaining number of updates per grid & prototype.
/// </summary>
// TODO PERFORMANCE Assign each prototype to an index and convert dictionary to array
private readonly Dictionary<EntityUid, Dictionary<string, int>> _gridUpdates = [];
private EntityQuery<EdgeSpreaderComponent> _query;
public const float SpreadCooldownSeconds = 1;
private static readonly ProtoId<TagPrototype> IgnoredTag = "SpreaderIgnore";
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<AirtightChanged>(OnAirtightChanged);
SubscribeLocalEvent<GridInitializeEvent>(OnGridInit);
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypeReload);
SubscribeLocalEvent<EdgeSpreaderComponent, EntityTerminatingEvent>(OnTerminating);
SetupPrototypes();
_query = GetEntityQuery<EdgeSpreaderComponent>();
}
private void OnPrototypeReload(PrototypesReloadedEventArgs obj)
{
if (obj.WasModified<EdgeSpreaderPrototype>())
SetupPrototypes();
}
private void SetupPrototypes()
{
_prototypeUpdates = [];
foreach (var proto in _prototype.EnumeratePrototypes<EdgeSpreaderPrototype>())
{
_prototypeUpdates.Add(proto.ID, proto.UpdatesPerSecond);
}
}
private void OnAirtightChanged(ref AirtightChanged ev)
{
ActivateSpreadableNeighbors(ev.Entity, ev.Position);
}
private void OnGridInit(GridInitializeEvent ev)
{
EnsureComp<SpreaderGridComponent>(ev.EntityUid);
}
private void OnTerminating(Entity<EdgeSpreaderComponent> entity, ref EntityTerminatingEvent args)
{
ActivateSpreadableNeighbors(entity);
}
/// <inheritdoc/>
public override void Update(float frameTime)
{
// Check which grids are valid for spreading
var spreadGrids = EntityQueryEnumerator<SpreaderGridComponent>();
_gridUpdates.Clear();
while (spreadGrids.MoveNext(out var uid, out var grid))
{
grid.UpdateAccumulator -= frameTime;
if (grid.UpdateAccumulator > 0)
continue;
_gridUpdates[uid] = _prototypeUpdates.ShallowClone();
grid.UpdateAccumulator += SpreadCooldownSeconds;
}
if (_gridUpdates.Count == 0)
return;
var query = EntityQueryEnumerator<ActiveEdgeSpreaderComponent>();
var xforms = GetEntityQuery<TransformComponent>();
var spreaderQuery = GetEntityQuery<EdgeSpreaderComponent>();
var spreaders = new List<(EntityUid Uid, ActiveEdgeSpreaderComponent Comp)>(Count<ActiveEdgeSpreaderComponent>());
// Build a list of all existing Edgespreaders, shuffle them
while (query.MoveNext(out var uid, out var comp))
{
spreaders.Add((uid, comp));
}
_robustRandom.Shuffle(spreaders);
// Remove the EdgeSpreaderComponent from any entity
// that doesn't meet a few trivial prerequisites
foreach (var (uid, comp) in spreaders)
{
// Get xform first, as entity may have been deleted due to interactions triggered by other spreaders.
if (!xforms.TryGetComponent(uid, out var xform))
continue;
if (xform.GridUid == null)
{
RemComp(uid, comp);
continue;
}
if (!_gridUpdates.TryGetValue(xform.GridUid.Value, out var groupUpdates))
continue;
if (!spreaderQuery.TryGetComponent(uid, out var spreader))
{
RemComp(uid, comp);
continue;
}
if (!groupUpdates.TryGetValue(spreader.Id, out var updates) || updates < 1)
continue;
// Edge detection logic is to be handled
// by the subscribing system, see KudzuSystem
// for a simple example
Spread(uid, xform, spreader.Id, ref updates);
if (updates < 1)
groupUpdates.Remove(spreader.Id);
else
groupUpdates[spreader.Id] = updates;
}
}
private void Spread(EntityUid uid, TransformComponent xform, ProtoId<EdgeSpreaderPrototype> prototype, ref int updates)
{
GetNeighbors(uid, xform, prototype, out var freeTiles, out _, out var neighbors);
var ev = new SpreadNeighborsEvent()
{
NeighborFreeTiles = freeTiles,
Neighbors = neighbors,
Updates = updates,
};
RaiseLocalEvent(uid, ref ev);
updates = ev.Updates;
}
/// <summary>
/// Gets the neighboring node data for the specified entity and the specified node group.
/// </summary>
public void GetNeighbors(EntityUid uid, TransformComponent comp, ProtoId<EdgeSpreaderPrototype> prototype, out ValueList<(MapGridComponent, TileRef)> freeTiles, out ValueList<Vector2i> occupiedTiles, out ValueList<EntityUid> neighbors)
{
// TODO remove occupiedTiles -- its currently unused and just slows this method down.
DebugTools.Assert(_prototype.HasIndex(prototype));
freeTiles = [];
occupiedTiles = [];
neighbors = [];
if (!TryComp<MapGridComponent>(comp.GridUid, out var grid))
return;
var tile = _map.TileIndicesFor(comp.GridUid.Value, grid, comp.Coordinates);
var spreaderQuery = GetEntityQuery<EdgeSpreaderComponent>();
var airtightQuery = GetEntityQuery<AirtightComponent>();
var dockQuery = GetEntityQuery<DockingComponent>();
var xformQuery = GetEntityQuery<TransformComponent>();
var blockedAtmosDirs = AtmosDirection.Invalid;
// Due to docking ports they may not necessarily be opposite directions.
var neighborTiles = new ValueList<(EntityUid entity, MapGridComponent grid, Vector2i Indices, AtmosDirection OtherDir, AtmosDirection OurDir)>();
// Check if anything on our own tile blocking that direction.
var ourEnts = _map.GetAnchoredEntitiesEnumerator(comp.GridUid.Value, grid, tile);
while (ourEnts.MoveNext(out var ent))
{
// Spread via docks in a special-case.
if (dockQuery.TryGetComponent(ent, out var dock) &&
dock.Docked &&
xformQuery.TryGetComponent(ent, out var xform) &&
xformQuery.TryGetComponent(dock.DockedWith, out var dockedXform) &&
TryComp<MapGridComponent>(dockedXform.GridUid, out var dockedGrid))
{
neighborTiles.Add((dockedXform.GridUid.Value, dockedGrid, _map.CoordinatesToTile(dockedXform.GridUid.Value, dockedGrid, dockedXform.Coordinates), xform.LocalRotation.ToAtmosDirection(), dockedXform.LocalRotation.ToAtmosDirection()));
}
// If we're on a blocked tile work out which directions we can go.
if (!airtightQuery.TryGetComponent(ent, out var airtight) || !airtight.AirBlocked ||
_tag.HasTag(ent.Value, IgnoredTag))
{
continue;
}
foreach (var value in new[] { AtmosDirection.North, AtmosDirection.East, AtmosDirection.South, AtmosDirection.West })
{
if ((value & airtight.AirBlockedDirection) == 0x0)
continue;
blockedAtmosDirs |= value;
break;
}
break;
}
// Add the normal neighbors.
for (var i = 0; i < 4; i++)
{
var atmosDir = (AtmosDirection) (1 << i);
var neighborPos = tile.Offset(atmosDir);
neighborTiles.Add((comp.GridUid.Value, grid, neighborPos, atmosDir, i.ToOppositeDir()));
}
foreach (var (neighborEnt, neighborGrid, neighborPos, ourAtmosDir, otherAtmosDir) in neighborTiles)
{
// This tile is blocked to that direction.
if ((blockedAtmosDirs & ourAtmosDir) != 0x0)
continue;
if (!_map.TryGetTileRef(neighborEnt, neighborGrid, neighborPos, out var tileRef) || tileRef.Tile.IsEmpty)
continue;
var directionEnumerator = _map.GetAnchoredEntitiesEnumerator(neighborEnt, neighborGrid, neighborPos);
var occupied = false;
while (directionEnumerator.MoveNext(out var ent))
{
if (!airtightQuery.TryGetComponent(ent, out var airtight) || !airtight.AirBlocked || _tag.HasTag(ent.Value, IgnoredTag))
{
continue;
}
if ((airtight.AirBlockedDirection & otherAtmosDir) == 0x0)
continue;
occupied = true;
break;
}
if (occupied)
continue;
var oldCount = occupiedTiles.Count;
directionEnumerator = _map.GetAnchoredEntitiesEnumerator(neighborEnt, neighborGrid, neighborPos);
while (directionEnumerator.MoveNext(out var ent))
{
if (!spreaderQuery.TryGetComponent(ent, out var spreader))
continue;
if (spreader.Id != prototype)
continue;
neighbors.Add(ent.Value);
occupiedTiles.Add(neighborPos);
break;
}
if (oldCount == occupiedTiles.Count)
freeTiles.Add((neighborGrid, tileRef));
}
}
/// <summary>
/// This function activates all spreaders that are adjacent to a given entity. This also activates other spreaders
/// on the same tile as the current entity (for thin airtight entities like windoors).
/// </summary>
public void ActivateSpreadableNeighbors(EntityUid uid, (EntityUid Grid, Vector2i Tile)? position = null)
{
Vector2i tile;
EntityUid ent;
MapGridComponent? grid;
if (position == null)
{
var transform = Transform(uid);
if (!TryComp(transform.GridUid, out grid) || TerminatingOrDeleted(transform.GridUid.Value))
return;
tile = _map.TileIndicesFor(transform.GridUid.Value, grid, transform.Coordinates);
ent = transform.GridUid.Value;
}
else
{
if (!TryComp(position.Value.Grid, out grid))
return;
(ent, tile) = position.Value;
}
var anchored = _map.GetAnchoredEntitiesEnumerator(ent, grid, tile);
while (anchored.MoveNext(out var entity))
{
if (entity == ent)
continue;
DebugTools.Assert(Transform(entity.Value).Anchored);
if (_query.HasComponent(ent) && !TerminatingOrDeleted(entity.Value))
EnsureComp<ActiveEdgeSpreaderComponent>(entity.Value);
}
for (var i = 0; i < Atmospherics.Directions; i++)
{
var direction = (AtmosDirection) (1 << i);
var adjacentTile = SharedMapSystem.GetDirection(tile, direction.ToDirection());
anchored = _map.GetAnchoredEntitiesEnumerator(ent, grid, adjacentTile);
while (anchored.MoveNext(out var entity))
{
DebugTools.Assert(Transform(entity.Value).Anchored);
if (_query.HasComponent(ent) && !TerminatingOrDeleted(entity.Value))
EnsureComp<ActiveEdgeSpreaderComponent>(entity.Value);
}
}
}
}