1
0

SpreaderSystem.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. using Content.Server.Atmos.Components;
  2. using Content.Server.Atmos.EntitySystems;
  3. using Content.Server.Shuttles.Components;
  4. using Content.Shared.Atmos;
  5. using Content.Shared.Maps;
  6. using Content.Shared.Spreader;
  7. using Content.Shared.Tag;
  8. using Robust.Shared.Collections;
  9. using Robust.Shared.Map;
  10. using Robust.Shared.Map.Components;
  11. using Robust.Shared.Prototypes;
  12. using Robust.Shared.Random;
  13. using Robust.Shared.Utility;
  14. namespace Content.Server.Spreader;
  15. /// <summary>
  16. /// Handles generic spreading logic, where one anchored entity spreads to neighboring tiles.
  17. /// </summary>
  18. public sealed class SpreaderSystem : EntitySystem
  19. {
  20. [Dependency] private readonly IPrototypeManager _prototype = default!;
  21. [Dependency] private readonly IRobustRandom _robustRandom = default!;
  22. [Dependency] private readonly SharedMapSystem _map = default!;
  23. [Dependency] private readonly TagSystem _tag = default!;
  24. /// <summary>
  25. /// Cached maximum number of updates per spreader prototype. This is applied per-grid.
  26. /// </summary>
  27. private Dictionary<string, int> _prototypeUpdates = default!;
  28. /// <summary>
  29. /// Remaining number of updates per grid & prototype.
  30. /// </summary>
  31. // TODO PERFORMANCE Assign each prototype to an index and convert dictionary to array
  32. private readonly Dictionary<EntityUid, Dictionary<string, int>> _gridUpdates = [];
  33. private EntityQuery<EdgeSpreaderComponent> _query;
  34. public const float SpreadCooldownSeconds = 1;
  35. private static readonly ProtoId<TagPrototype> IgnoredTag = "SpreaderIgnore";
  36. /// <inheritdoc/>
  37. public override void Initialize()
  38. {
  39. SubscribeLocalEvent<AirtightChanged>(OnAirtightChanged);
  40. SubscribeLocalEvent<GridInitializeEvent>(OnGridInit);
  41. SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypeReload);
  42. SubscribeLocalEvent<EdgeSpreaderComponent, EntityTerminatingEvent>(OnTerminating);
  43. SetupPrototypes();
  44. _query = GetEntityQuery<EdgeSpreaderComponent>();
  45. }
  46. private void OnPrototypeReload(PrototypesReloadedEventArgs obj)
  47. {
  48. if (obj.WasModified<EdgeSpreaderPrototype>())
  49. SetupPrototypes();
  50. }
  51. private void SetupPrototypes()
  52. {
  53. _prototypeUpdates = [];
  54. foreach (var proto in _prototype.EnumeratePrototypes<EdgeSpreaderPrototype>())
  55. {
  56. _prototypeUpdates.Add(proto.ID, proto.UpdatesPerSecond);
  57. }
  58. }
  59. private void OnAirtightChanged(ref AirtightChanged ev)
  60. {
  61. ActivateSpreadableNeighbors(ev.Entity, ev.Position);
  62. }
  63. private void OnGridInit(GridInitializeEvent ev)
  64. {
  65. EnsureComp<SpreaderGridComponent>(ev.EntityUid);
  66. }
  67. private void OnTerminating(Entity<EdgeSpreaderComponent> entity, ref EntityTerminatingEvent args)
  68. {
  69. ActivateSpreadableNeighbors(entity);
  70. }
  71. /// <inheritdoc/>
  72. public override void Update(float frameTime)
  73. {
  74. // Check which grids are valid for spreading
  75. var spreadGrids = EntityQueryEnumerator<SpreaderGridComponent>();
  76. _gridUpdates.Clear();
  77. while (spreadGrids.MoveNext(out var uid, out var grid))
  78. {
  79. grid.UpdateAccumulator -= frameTime;
  80. if (grid.UpdateAccumulator > 0)
  81. continue;
  82. _gridUpdates[uid] = _prototypeUpdates.ShallowClone();
  83. grid.UpdateAccumulator += SpreadCooldownSeconds;
  84. }
  85. if (_gridUpdates.Count == 0)
  86. return;
  87. var query = EntityQueryEnumerator<ActiveEdgeSpreaderComponent>();
  88. var xforms = GetEntityQuery<TransformComponent>();
  89. var spreaderQuery = GetEntityQuery<EdgeSpreaderComponent>();
  90. var spreaders = new List<(EntityUid Uid, ActiveEdgeSpreaderComponent Comp)>(Count<ActiveEdgeSpreaderComponent>());
  91. // Build a list of all existing Edgespreaders, shuffle them
  92. while (query.MoveNext(out var uid, out var comp))
  93. {
  94. spreaders.Add((uid, comp));
  95. }
  96. _robustRandom.Shuffle(spreaders);
  97. // Remove the EdgeSpreaderComponent from any entity
  98. // that doesn't meet a few trivial prerequisites
  99. foreach (var (uid, comp) in spreaders)
  100. {
  101. // Get xform first, as entity may have been deleted due to interactions triggered by other spreaders.
  102. if (!xforms.TryGetComponent(uid, out var xform))
  103. continue;
  104. if (xform.GridUid == null)
  105. {
  106. RemComp(uid, comp);
  107. continue;
  108. }
  109. if (!_gridUpdates.TryGetValue(xform.GridUid.Value, out var groupUpdates))
  110. continue;
  111. if (!spreaderQuery.TryGetComponent(uid, out var spreader))
  112. {
  113. RemComp(uid, comp);
  114. continue;
  115. }
  116. if (!groupUpdates.TryGetValue(spreader.Id, out var updates) || updates < 1)
  117. continue;
  118. // Edge detection logic is to be handled
  119. // by the subscribing system, see KudzuSystem
  120. // for a simple example
  121. Spread(uid, xform, spreader.Id, ref updates);
  122. if (updates < 1)
  123. groupUpdates.Remove(spreader.Id);
  124. else
  125. groupUpdates[spreader.Id] = updates;
  126. }
  127. }
  128. private void Spread(EntityUid uid, TransformComponent xform, ProtoId<EdgeSpreaderPrototype> prototype, ref int updates)
  129. {
  130. GetNeighbors(uid, xform, prototype, out var freeTiles, out _, out var neighbors);
  131. var ev = new SpreadNeighborsEvent()
  132. {
  133. NeighborFreeTiles = freeTiles,
  134. Neighbors = neighbors,
  135. Updates = updates,
  136. };
  137. RaiseLocalEvent(uid, ref ev);
  138. updates = ev.Updates;
  139. }
  140. /// <summary>
  141. /// Gets the neighboring node data for the specified entity and the specified node group.
  142. /// </summary>
  143. public void GetNeighbors(EntityUid uid, TransformComponent comp, ProtoId<EdgeSpreaderPrototype> prototype, out ValueList<(MapGridComponent, TileRef)> freeTiles, out ValueList<Vector2i> occupiedTiles, out ValueList<EntityUid> neighbors)
  144. {
  145. freeTiles = [];
  146. occupiedTiles = [];
  147. neighbors = [];
  148. // TODO remove occupiedTiles -- its currently unused and just slows this method down.
  149. if (!_prototype.TryIndex(prototype, out var spreaderPrototype))
  150. return;
  151. if (!TryComp<MapGridComponent>(comp.GridUid, out var grid))
  152. return;
  153. var tile = _map.TileIndicesFor(comp.GridUid.Value, grid, comp.Coordinates);
  154. var spreaderQuery = GetEntityQuery<EdgeSpreaderComponent>();
  155. var airtightQuery = GetEntityQuery<AirtightComponent>();
  156. var dockQuery = GetEntityQuery<DockingComponent>();
  157. var xformQuery = GetEntityQuery<TransformComponent>();
  158. var blockedAtmosDirs = AtmosDirection.Invalid;
  159. // Due to docking ports they may not necessarily be opposite directions.
  160. var neighborTiles = new ValueList<(EntityUid entity, MapGridComponent grid, Vector2i Indices, AtmosDirection OtherDir, AtmosDirection OurDir)>();
  161. // Check if anything on our own tile blocking that direction.
  162. var ourEnts = _map.GetAnchoredEntitiesEnumerator(comp.GridUid.Value, grid, tile);
  163. while (ourEnts.MoveNext(out var ent))
  164. {
  165. // Spread via docks in a special-case.
  166. if (dockQuery.TryGetComponent(ent, out var dock) &&
  167. dock.Docked &&
  168. xformQuery.TryGetComponent(ent, out var xform) &&
  169. xformQuery.TryGetComponent(dock.DockedWith, out var dockedXform) &&
  170. TryComp<MapGridComponent>(dockedXform.GridUid, out var dockedGrid))
  171. {
  172. neighborTiles.Add((dockedXform.GridUid.Value, dockedGrid, _map.CoordinatesToTile(dockedXform.GridUid.Value, dockedGrid, dockedXform.Coordinates), xform.LocalRotation.ToAtmosDirection(), dockedXform.LocalRotation.ToAtmosDirection()));
  173. }
  174. // If we're on a blocked tile work out which directions we can go.
  175. if (!airtightQuery.TryGetComponent(ent, out var airtight) || !airtight.AirBlocked ||
  176. _tag.HasTag(ent.Value, IgnoredTag))
  177. {
  178. continue;
  179. }
  180. foreach (var value in new[] { AtmosDirection.North, AtmosDirection.East, AtmosDirection.South, AtmosDirection.West })
  181. {
  182. if ((value & airtight.AirBlockedDirection) == 0x0)
  183. continue;
  184. blockedAtmosDirs |= value;
  185. break;
  186. }
  187. break;
  188. }
  189. // Add the normal neighbors.
  190. for (var i = 0; i < 4; i++)
  191. {
  192. var atmosDir = (AtmosDirection) (1 << i);
  193. var neighborPos = tile.Offset(atmosDir);
  194. neighborTiles.Add((comp.GridUid.Value, grid, neighborPos, atmosDir, i.ToOppositeDir()));
  195. }
  196. foreach (var (neighborEnt, neighborGrid, neighborPos, ourAtmosDir, otherAtmosDir) in neighborTiles)
  197. {
  198. // This tile is blocked to that direction.
  199. if ((blockedAtmosDirs & ourAtmosDir) != 0x0)
  200. continue;
  201. if (!_map.TryGetTileRef(neighborEnt, neighborGrid, neighborPos, out var tileRef) || tileRef.Tile.IsEmpty)
  202. continue;
  203. if (spreaderPrototype.PreventSpreadOnSpaced && tileRef.Tile.IsSpace())
  204. continue;
  205. var directionEnumerator = _map.GetAnchoredEntitiesEnumerator(neighborEnt, neighborGrid, neighborPos);
  206. var occupied = false;
  207. while (directionEnumerator.MoveNext(out var ent))
  208. {
  209. if (!airtightQuery.TryGetComponent(ent, out var airtight) || !airtight.AirBlocked || _tag.HasTag(ent.Value, IgnoredTag))
  210. {
  211. continue;
  212. }
  213. if ((airtight.AirBlockedDirection & otherAtmosDir) == 0x0)
  214. continue;
  215. occupied = true;
  216. break;
  217. }
  218. if (occupied)
  219. continue;
  220. var oldCount = occupiedTiles.Count;
  221. directionEnumerator = _map.GetAnchoredEntitiesEnumerator(neighborEnt, neighborGrid, neighborPos);
  222. while (directionEnumerator.MoveNext(out var ent))
  223. {
  224. if (!spreaderQuery.TryGetComponent(ent, out var spreader))
  225. continue;
  226. if (spreader.Id != prototype)
  227. continue;
  228. neighbors.Add(ent.Value);
  229. occupiedTiles.Add(neighborPos);
  230. break;
  231. }
  232. if (oldCount == occupiedTiles.Count)
  233. freeTiles.Add((neighborGrid, tileRef));
  234. }
  235. }
  236. /// <summary>
  237. /// This function activates all spreaders that are adjacent to a given entity. This also activates other spreaders
  238. /// on the same tile as the current entity (for thin airtight entities like windoors).
  239. /// </summary>
  240. public void ActivateSpreadableNeighbors(EntityUid uid, (EntityUid Grid, Vector2i Tile)? position = null)
  241. {
  242. Vector2i tile;
  243. EntityUid ent;
  244. MapGridComponent? grid;
  245. if (position == null)
  246. {
  247. var transform = Transform(uid);
  248. if (!TryComp(transform.GridUid, out grid) || TerminatingOrDeleted(transform.GridUid.Value))
  249. return;
  250. tile = _map.TileIndicesFor(transform.GridUid.Value, grid, transform.Coordinates);
  251. ent = transform.GridUid.Value;
  252. }
  253. else
  254. {
  255. if (!TryComp(position.Value.Grid, out grid))
  256. return;
  257. (ent, tile) = position.Value;
  258. }
  259. var anchored = _map.GetAnchoredEntitiesEnumerator(ent, grid, tile);
  260. while (anchored.MoveNext(out var entity))
  261. {
  262. if (entity == ent)
  263. continue;
  264. DebugTools.Assert(Transform(entity.Value).Anchored);
  265. if (_query.HasComponent(ent) && !TerminatingOrDeleted(entity.Value))
  266. EnsureComp<ActiveEdgeSpreaderComponent>(entity.Value);
  267. }
  268. for (var i = 0; i < Atmospherics.Directions; i++)
  269. {
  270. var direction = (AtmosDirection) (1 << i);
  271. var adjacentTile = SharedMapSystem.GetDirection(tile, direction.ToDirection());
  272. anchored = _map.GetAnchoredEntitiesEnumerator(ent, grid, adjacentTile);
  273. while (anchored.MoveNext(out var entity))
  274. {
  275. DebugTools.Assert(Transform(entity.Value).Anchored);
  276. if (_query.HasComponent(ent) && !TerminatingOrDeleted(entity.Value))
  277. EnsureComp<ActiveEdgeSpreaderComponent>(entity.Value);
  278. }
  279. }
  280. }
  281. public bool RequiresFloorToSpread(EntProtoId<EdgeSpreaderComponent> spreader)
  282. {
  283. if (!_prototype.Index(spreader).TryGetComponent<EdgeSpreaderComponent>(out var spreaderComp, EntityManager.ComponentFactory))
  284. return false;
  285. return _prototype.Index(spreaderComp.Id).PreventSpreadOnSpaced;
  286. }
  287. }