DebrisFeaturePlacerSystem.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. using System.Linq;
  2. using System.Numerics;
  3. using Content.Server.Worldgen.Components;
  4. using Content.Server.Worldgen.Components.Debris;
  5. using Content.Server.Worldgen.Tools;
  6. using JetBrains.Annotations;
  7. using Robust.Server.GameObjects;
  8. using Robust.Shared.Map;
  9. using Robust.Shared.Map.Components;
  10. using Robust.Shared.Random;
  11. using Robust.Shared.Utility;
  12. namespace Content.Server.Worldgen.Systems.Debris;
  13. /// <summary>
  14. /// This handles placing debris within the world evenly with rng, primarily for structures like asteroid fields.
  15. /// </summary>
  16. public sealed class DebrisFeaturePlacerSystem : BaseWorldSystem
  17. {
  18. [Dependency] private readonly NoiseIndexSystem _noiseIndex = default!;
  19. [Dependency] private readonly PoissonDiskSampler _sampler = default!;
  20. [Dependency] private readonly TransformSystem _xformSys = default!;
  21. [Dependency] private readonly ILogManager _logManager = default!;
  22. [Dependency] private readonly IMapManager _mapManager = default!;
  23. [Dependency] private readonly IRobustRandom _random = default!;
  24. private ISawmill _sawmill = default!;
  25. /// <inheritdoc />
  26. public override void Initialize()
  27. {
  28. _sawmill = _logManager.GetSawmill("world.debris.feature_placer");
  29. SubscribeLocalEvent<DebrisFeaturePlacerControllerComponent, WorldChunkLoadedEvent>(OnChunkLoaded);
  30. SubscribeLocalEvent<DebrisFeaturePlacerControllerComponent, WorldChunkUnloadedEvent>(OnChunkUnloaded);
  31. SubscribeLocalEvent<OwnedDebrisComponent, ComponentShutdown>(OnDebrisShutdown);
  32. SubscribeLocalEvent<OwnedDebrisComponent, MoveEvent>(OnDebrisMove);
  33. SubscribeLocalEvent<SimpleDebrisSelectorComponent, TryGetPlaceableDebrisFeatureEvent>(
  34. OnTryGetPlacableDebrisEvent);
  35. }
  36. /// <summary>
  37. /// Handles debris moving, and making sure it stays parented to a chunk for loading purposes.
  38. /// </summary>
  39. private void OnDebrisMove(EntityUid uid, OwnedDebrisComponent component, ref MoveEvent args)
  40. {
  41. if (!HasComp<WorldChunkComponent>(component.OwningController))
  42. return; // Redundant logic, prolly needs it's own handler for your custom system.
  43. var placer = Comp<DebrisFeaturePlacerControllerComponent>(component.OwningController);
  44. var xform = args.Component;
  45. var ownerXform = Transform(component.OwningController);
  46. if (xform.MapUid is null || ownerXform.MapUid is null)
  47. return; // not our problem
  48. if (xform.MapUid != ownerXform.MapUid)
  49. {
  50. _sawmill.Error($"Somehow debris {uid} left it's expected map! Unparenting it to avoid issues.");
  51. RemCompDeferred<OwnedDebrisComponent>(uid);
  52. placer.OwnedDebris.Remove(component.LastKey);
  53. return;
  54. }
  55. placer.OwnedDebris.Remove(component.LastKey);
  56. var newChunk = GetOrCreateChunk(GetChunkCoords(uid), xform.MapUid!.Value);
  57. if (newChunk is null || !TryComp<DebrisFeaturePlacerControllerComponent>(newChunk, out var newPlacer))
  58. {
  59. // Whelp.
  60. RemCompDeferred<OwnedDebrisComponent>(uid);
  61. return;
  62. }
  63. newPlacer.OwnedDebris[_xformSys.GetWorldPosition(xform)] = uid; // Change our owner.
  64. component.OwningController = newChunk.Value;
  65. }
  66. /// <summary>
  67. /// Handles debris shutdown/detach.
  68. /// </summary>
  69. private void OnDebrisShutdown(EntityUid uid, OwnedDebrisComponent component, ComponentShutdown args)
  70. {
  71. if (!TryComp<DebrisFeaturePlacerControllerComponent>(component.OwningController, out var placer))
  72. return;
  73. placer.OwnedDebris[component.LastKey] = null;
  74. if (Terminating(uid))
  75. placer.OwnedDebris.Remove(component.LastKey);
  76. }
  77. /// <summary>
  78. /// Queues all debris owned by the placer for garbage collection.
  79. /// </summary>
  80. private void OnChunkUnloaded(EntityUid uid, DebrisFeaturePlacerControllerComponent component,
  81. ref WorldChunkUnloadedEvent args)
  82. {
  83. component.DoSpawns = true;
  84. }
  85. /// <summary>
  86. /// Handles providing a debris type to place for SimpleDebrisSelectorComponent.
  87. /// This randomly picks a debris type from the EntitySpawnCollectionCache.
  88. /// </summary>
  89. private void OnTryGetPlacableDebrisEvent(EntityUid uid, SimpleDebrisSelectorComponent component,
  90. ref TryGetPlaceableDebrisFeatureEvent args)
  91. {
  92. if (args.DebrisProto is not null)
  93. return;
  94. var l = new List<string?>(1);
  95. component.CachedDebrisTable.GetSpawns(_random, ref l);
  96. switch (l.Count)
  97. {
  98. case 0:
  99. return;
  100. case > 1:
  101. _sawmill.Warning($"Got more than one possible debris type from {uid}. List: {string.Join(", ", l)}");
  102. break;
  103. }
  104. args.DebrisProto = l[0];
  105. }
  106. /// <summary>
  107. /// Handles loading in debris. This does the following:
  108. /// - Checks if the debris is currently supposed to do spawns, if it isn't, aborts immediately.
  109. /// - Evaluates the density value to be used for placement, if it's zero, aborts.
  110. /// - Generates the points to generate debris at, if and only if they've not been selected already by a prior load.
  111. /// - Does the following in a loop over all generated points:
  112. /// - Raises an event to check if something else wants to intercept debris placement, if the event is handled,
  113. /// continues to the next point without generating anything.
  114. /// - Raises an event to get the debris type that should be used for generation.
  115. /// - Spawns the given debris at the point, adding it to the placer's index.
  116. /// </summary>
  117. private void OnChunkLoaded(EntityUid uid, DebrisFeaturePlacerControllerComponent component,
  118. ref WorldChunkLoadedEvent args)
  119. {
  120. if (component.DoSpawns == false)
  121. return;
  122. component.DoSpawns = false; // Don't repeat yourself if this crashes.
  123. var chunk = Comp<WorldChunkComponent>(args.Chunk);
  124. var densityChannel = component.DensityNoiseChannel;
  125. var density = _noiseIndex.Evaluate(uid, densityChannel, chunk.Coordinates + new Vector2(0.5f, 0.5f));
  126. if (density == 0)
  127. return;
  128. List<Vector2>? points = null;
  129. // If we've been loaded before, reuse the same coordinates.
  130. if (component.OwnedDebris.Count != 0)
  131. {
  132. //TODO: Remove LINQ.
  133. points = component.OwnedDebris
  134. .Where(x => !Deleted(x.Value))
  135. .Select(static x => x.Key)
  136. .ToList();
  137. }
  138. points ??= GeneratePointsInChunk(args.Chunk, density, chunk.Coordinates, chunk.Map);
  139. var safetyBounds = Box2.UnitCentered.Enlarged(component.SafetyZoneRadius);
  140. var failures = 0; // Avoid severe log spam.
  141. foreach (var point in points)
  142. {
  143. if (component.OwnedDebris.TryGetValue(point, out var existing))
  144. {
  145. DebugTools.Assert(Exists(existing));
  146. continue;
  147. }
  148. var pointDensity = _noiseIndex.Evaluate(uid, densityChannel, WorldGen.WorldToChunkCoords(point));
  149. if (pointDensity == 0 && component.DensityClip || _random.Prob(component.RandomCancellationChance))
  150. continue;
  151. var coords = new EntityCoordinates(chunk.Map, point);
  152. if (_mapManager
  153. .FindGridsIntersecting(Comp<MapComponent>(chunk.Map).MapId, safetyBounds.Translated(point)).Any())
  154. continue; // Oops, gonna collide.
  155. var preEv = new PrePlaceDebrisFeatureEvent(coords, args.Chunk);
  156. RaiseLocalEvent(uid, ref preEv);
  157. if (uid != args.Chunk)
  158. RaiseLocalEvent(args.Chunk, ref preEv);
  159. if (preEv.Handled)
  160. continue;
  161. var debrisFeatureEv = new TryGetPlaceableDebrisFeatureEvent(coords, args.Chunk);
  162. RaiseLocalEvent(uid, ref debrisFeatureEv);
  163. if (debrisFeatureEv.DebrisProto == null)
  164. {
  165. // Try on the chunk...?
  166. if (uid != args.Chunk)
  167. RaiseLocalEvent(args.Chunk, ref debrisFeatureEv);
  168. if (debrisFeatureEv.DebrisProto == null)
  169. {
  170. // Nope.
  171. failures++;
  172. continue;
  173. }
  174. }
  175. var ent = Spawn(debrisFeatureEv.DebrisProto, coords);
  176. component.OwnedDebris.Add(point, ent);
  177. var owned = EnsureComp<OwnedDebrisComponent>(ent);
  178. owned.OwningController = uid;
  179. owned.LastKey = point;
  180. }
  181. if (failures > 0)
  182. _sawmill.Error($"Failed to place {failures} debris at chunk {args.Chunk}");
  183. }
  184. /// <summary>
  185. /// Generates the points to put into a chunk using a poisson disk sampler.
  186. /// </summary>
  187. private List<Vector2> GeneratePointsInChunk(EntityUid chunk, float density, Vector2 coords, EntityUid map)
  188. {
  189. var offs = (int) ((WorldGen.ChunkSize - WorldGen.ChunkSize / 8.0f) / 2.0f);
  190. var topLeft = new Vector2(-offs, -offs);
  191. var lowerRight = new Vector2(offs, offs);
  192. var enumerator = _sampler.SampleRectangle(topLeft, lowerRight, density);
  193. var debrisPoints = new List<Vector2>();
  194. var realCenter = WorldGen.ChunkToWorldCoordsCentered(coords.Floored());
  195. while (enumerator.MoveNext(out var debrisPoint))
  196. {
  197. debrisPoints.Add(realCenter + debrisPoint.Value);
  198. }
  199. return debrisPoints;
  200. }
  201. }
  202. /// <summary>
  203. /// Fired directed on the debris feature placer controller and the chunk, ahead of placing a debris piece.
  204. /// </summary>
  205. [ByRefEvent]
  206. [PublicAPI]
  207. public record struct PrePlaceDebrisFeatureEvent(EntityCoordinates Coords, EntityUid Chunk, bool Handled = false);
  208. /// <summary>
  209. /// Fired directed on the debris feature placer controller and the chunk, to select which debris piece to place.
  210. /// </summary>
  211. [ByRefEvent]
  212. [PublicAPI]
  213. public record struct TryGetPlaceableDebrisFeatureEvent(EntityCoordinates Coords, EntityUid Chunk,
  214. string? DebrisProto = null);