using Content.Server.Atmos.Components; using Content.Server.Atmos.EntitySystems; using Content.Server.Shuttles.Components; using Content.Shared.Atmos; using Content.Shared.Maps; 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; /// /// Handles generic spreading logic, where one anchored entity spreads to neighboring tiles. /// 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!; /// /// Cached maximum number of updates per spreader prototype. This is applied per-grid. /// private Dictionary _prototypeUpdates = default!; /// /// Remaining number of updates per grid & prototype. /// // TODO PERFORMANCE Assign each prototype to an index and convert dictionary to array private readonly Dictionary> _gridUpdates = []; private EntityQuery _query; public const float SpreadCooldownSeconds = 1; private static readonly ProtoId IgnoredTag = "SpreaderIgnore"; /// public override void Initialize() { SubscribeLocalEvent(OnAirtightChanged); SubscribeLocalEvent(OnGridInit); SubscribeLocalEvent(OnPrototypeReload); SubscribeLocalEvent(OnTerminating); SetupPrototypes(); _query = GetEntityQuery(); } private void OnPrototypeReload(PrototypesReloadedEventArgs obj) { if (obj.WasModified()) SetupPrototypes(); } private void SetupPrototypes() { _prototypeUpdates = []; foreach (var proto in _prototype.EnumeratePrototypes()) { _prototypeUpdates.Add(proto.ID, proto.UpdatesPerSecond); } } private void OnAirtightChanged(ref AirtightChanged ev) { ActivateSpreadableNeighbors(ev.Entity, ev.Position); } private void OnGridInit(GridInitializeEvent ev) { EnsureComp(ev.EntityUid); } private void OnTerminating(Entity entity, ref EntityTerminatingEvent args) { ActivateSpreadableNeighbors(entity); } /// public override void Update(float frameTime) { // Check which grids are valid for spreading var spreadGrids = EntityQueryEnumerator(); _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(); var xforms = GetEntityQuery(); var spreaderQuery = GetEntityQuery(); var spreaders = new List<(EntityUid Uid, ActiveEdgeSpreaderComponent Comp)>(Count()); // 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 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; } /// /// Gets the neighboring node data for the specified entity and the specified node group. /// public void GetNeighbors(EntityUid uid, TransformComponent comp, ProtoId prototype, out ValueList<(MapGridComponent, TileRef)> freeTiles, out ValueList occupiedTiles, out ValueList neighbors) { freeTiles = []; occupiedTiles = []; neighbors = []; // TODO remove occupiedTiles -- its currently unused and just slows this method down. if (!_prototype.TryIndex(prototype, out var spreaderPrototype)) return; if (!TryComp(comp.GridUid, out var grid)) return; var tile = _map.TileIndicesFor(comp.GridUid.Value, grid, comp.Coordinates); var spreaderQuery = GetEntityQuery(); var airtightQuery = GetEntityQuery(); var dockQuery = GetEntityQuery(); var xformQuery = GetEntityQuery(); 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(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; if (spreaderPrototype.PreventSpreadOnSpaced && tileRef.Tile.IsSpace()) 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)); } } /// /// 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). /// 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(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(entity.Value); } } } public bool RequiresFloorToSpread(EntProtoId spreader) { if (!_prototype.Index(spreader).TryGetComponent(out var spreaderComp, EntityManager.ComponentFactory)) return false; return _prototype.Index(spreaderComp.Id).PreventSpreadOnSpaced; } }