mirror of
https://github.com/WWhiteDreamProject/wwdpublic.git
synced 2026-05-01 12:37:23 +03:00
## Mirror of PR #22521: [Partial atmos refactor](https://github.com/space-wizards/space-station-14/pull/22521) from <img src="https://avatars.githubusercontent.com/u/10567778?v=4" alt="space-wizards" width="22"/> [space-wizards](https://github.com/space-wizards)/[space-station-14](https://github.com/space-wizards/space-station-14) ###### `18a35e7e83b2b71ee84b054d44d9ed5e595dd618` PR opened by <img src="https://avatars.githubusercontent.com/u/60421075?v=4" width="16"/><a href="https://github.com/ElectroJr"> ElectroJr</a> at 2023-12-15 03:45:42 UTC --- PR changed 43 files with 891 additions and 635 deletions. The PR had the following labels: - Status: Needs Review --- <details open="true"><summary><h1>Original Body</h1></summary> > This PR reworks how some parts of atmos code work. Originally it was just meant to be a performance and bugfix PR, but it has ballooned in scope. I'm not sure about some of my changes largely because I'm not sure if some things were an oversight or an intentional decision for some reason. > > List of changes: > - The `MolesArchived float[]` field is now read-only > - It simply gets zeroed whenever the `GasMixture` is set to null instead of constantly reallocating > - Airtight query information is now cached in `TileAtmosphere` > - This means that it should only iterate over anchored entities once per update. > - Previously an invalidated atmos tile would cause `ProcessRevalidate()` to query airtight entities on the same tile six times by calling a combination of `GridIsTileAirBlocked()`, `NeedsVacuumFixing()`, and `GridIsTileAirBlocked()`. So this should help significantly reduce component lookups & entity enumeration. > - This does change some behaviour. In particular blocked directions are now only updated if the tile was invalidated prior to the current atmos-update, and will only ever be updated once per atmos-update. > - AFAIK this only has an effect if the invalid tile processing is deferred over multiple ticks, and I don't think it should cause any issues? > - Fixes a potential bug, where tiles might not dispose of their excited group if their direction flags changed. > - `MapAtmosphereComponent.Mixture` is now always immutable and no longer nullable > - I'm not sure why the mixture was nullable before? AFAICT the component is meaningless if its null? > - Space "gas" was always immutable, but there was nothing that required planet atmospheres to be immutable. Requiring that it be immutable gets rid of the constant gas mixture cloning. > - I don't know if there was a reason for why they weren't immutable to begin with. > - Fixes lungs removing too much air from a gas mixture, resulting in negative moles. > - `GasMixture.Moles` is now `[Access]` restricted to the atmosphere system. > - This is to prevent people from improperly modifying the gas mixtures (e.g., lungs), or accidentally modifying immutable mixtures. > - Fixes an issue where non-grid atmosphere tiles would fail to update their adjacent tiles, resulting in null reference exception spam > - Fixes #21732 > - Fixes #21210 (probably) > - Disconnected atmosphere tiles, i.e., tiles that aren't on or adjacent to a grid tile, will now get removed from the tile set. Previously the tile set would just always increase, with tiles never getting removed. > - Removes various redundant component and tile-definition queries. > - Removes some method events in favour of just using methods. > - Map-exposded tiles now get updated when a map's atmosphere changes (or the grid moves across maps). > - Adds a `setmapatmos` command for adding map-wide atmospheres. > - Fixed (non-planet) map atmospheres rendering over grids. > > ## Media > > This PR also includes changes to the atmos debug overlay, though I've also split that off into a separate PR to make reviewing easier (#22520). > > Below is a video showing that atmos still seems to work, and that trimming of disconnected tiles works: > > https://github.com/space-wizards/space-station-14/assets/60421075/4da46992-19e6-4354-8ecd-3cd67be4d0ed > > For comparison, here is a video showing how current master works (disconnected tiles never get removed): > > https://github.com/space-wizards/space-station-14/assets/60421075/54590777-e11c-41dc-b49d-fd7e53bfeed7 > > 🆑 > - fix: Fixed a bug where partially airtight entities (e.g., thin windows or doors) could let air leak out into space. > </details> Co-authored-by: SimpleStation14 <Unknown>
343 lines
13 KiB
C#
343 lines
13 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 IMapManager _mapManager = default!;
|
|
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
|
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
|
[Dependency] private readonly SharedMapSystem _map = 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;
|
|
|
|
[ValidatePrototypeId<TagPrototype>]
|
|
private const string 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 tagQuery = GetEntityQuery<TagComponent>();
|
|
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 ||
|
|
tagQuery.TryGetComponent(ent, out var tags) && tags.Tags.Contains(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 direction = (Direction) (i * 2);
|
|
var atmosDir = direction.ToAtmosDirection();
|
|
var neighborPos = SharedMapSystem.GetDirection(tile, direction);
|
|
neighborTiles.Add((comp.GridUid.Value, grid, neighborPos, atmosDir, atmosDir.GetOpposite()));
|
|
}
|
|
|
|
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 ||
|
|
tagQuery.TryGetComponent(ent, out var tags) && tags.Tags.Contains(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);
|
|
}
|
|
}
|
|
}
|
|
}
|