WorldControllerSystem.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. using System.Linq;
  2. using Content.Server.Worldgen.Components;
  3. using Content.Shared.Ghost;
  4. using Content.Shared.Mind.Components;
  5. using JetBrains.Annotations;
  6. using Robust.Server.GameObjects;
  7. using Robust.Shared.Map;
  8. using Robust.Shared.Timing;
  9. namespace Content.Server.Worldgen.Systems;
  10. /// <summary>
  11. /// This handles putting together chunk entities and notifying them about important changes.
  12. /// </summary>
  13. public sealed class WorldControllerSystem : EntitySystem
  14. {
  15. [Dependency] private readonly TransformSystem _xformSys = default!;
  16. [Dependency] private readonly IGameTiming _gameTiming = default!;
  17. [Dependency] private readonly ILogManager _logManager = default!;
  18. [Dependency] private readonly MetaDataSystem _metaData = default!;
  19. private const int PlayerLoadRadius = 2;
  20. private ISawmill _sawmill = default!;
  21. /// <inheritdoc />
  22. public override void Initialize()
  23. {
  24. _sawmill = _logManager.GetSawmill("world");
  25. SubscribeLocalEvent<LoadedChunkComponent, ComponentStartup>(OnChunkLoadedCore);
  26. SubscribeLocalEvent<LoadedChunkComponent, ComponentShutdown>(OnChunkUnloadedCore);
  27. SubscribeLocalEvent<WorldChunkComponent, ComponentShutdown>(OnChunkShutdown);
  28. }
  29. /// <summary>
  30. /// Handles deleting chunks properly.
  31. /// </summary>
  32. private void OnChunkShutdown(EntityUid uid, WorldChunkComponent component, ComponentShutdown args)
  33. {
  34. if (!TryComp<WorldControllerComponent>(component.Map, out var controller))
  35. return;
  36. if (HasComp<LoadedChunkComponent>(uid))
  37. {
  38. var ev = new WorldChunkUnloadedEvent(uid, component.Coordinates);
  39. RaiseLocalEvent(component.Map, ref ev);
  40. RaiseLocalEvent(uid, ref ev, broadcast: true);
  41. }
  42. controller.Chunks.Remove(component.Coordinates);
  43. }
  44. /// <summary>
  45. /// Handles the inner logic of loading a chunk, i.e. events.
  46. /// </summary>
  47. private void OnChunkLoadedCore(EntityUid uid, LoadedChunkComponent component, ComponentStartup args)
  48. {
  49. if (!TryComp<WorldChunkComponent>(uid, out var chunk))
  50. return;
  51. var ev = new WorldChunkLoadedEvent(uid, chunk.Coordinates);
  52. RaiseLocalEvent(chunk.Map, ref ev);
  53. RaiseLocalEvent(uid, ref ev, broadcast: true);
  54. //_sawmill.Debug($"Loaded chunk {ToPrettyString(uid)} at {chunk.Coordinates}");
  55. }
  56. /// <summary>
  57. /// Handles the inner logic of unloading a chunk, i.e. events.
  58. /// </summary>
  59. private void OnChunkUnloadedCore(EntityUid uid, LoadedChunkComponent component, ComponentShutdown args)
  60. {
  61. if (!TryComp<WorldChunkComponent>(uid, out var chunk))
  62. return;
  63. if (Terminating(uid))
  64. return; // SAFETY: This is in case a loaded chunk gets deleted, to avoid double unload.
  65. var ev = new WorldChunkUnloadedEvent(uid, chunk.Coordinates);
  66. RaiseLocalEvent(chunk.Map, ref ev);
  67. RaiseLocalEvent(uid, ref ev);
  68. //_sawmill.Debug($"Unloaded chunk {ToPrettyString(uid)} at {coords}");
  69. }
  70. /// <inheritdoc />
  71. public override void Update(float frameTime)
  72. {
  73. //there was a to-do here about every frame alloc but it turns out it's a nothing burger here.
  74. var chunksToLoad = new Dictionary<EntityUid, Dictionary<Vector2i, List<EntityUid>>>();
  75. var controllerEnum = EntityQueryEnumerator<WorldControllerComponent>();
  76. while (controllerEnum.MoveNext(out var uid, out _))
  77. {
  78. chunksToLoad[uid] = new Dictionary<Vector2i, List<EntityUid>>();
  79. }
  80. if (chunksToLoad.Count == 0)
  81. return; // Just bail early.
  82. var loaderEnum = EntityQueryEnumerator<WorldLoaderComponent, TransformComponent>();
  83. while (loaderEnum.MoveNext(out var uid, out var worldLoader, out var xform))
  84. {
  85. var mapOrNull = xform.MapUid;
  86. if (mapOrNull is null)
  87. continue;
  88. var map = mapOrNull.Value;
  89. if (!chunksToLoad.ContainsKey(map))
  90. continue;
  91. var wc = _xformSys.GetWorldPosition(xform);
  92. var coords = WorldGen.WorldToChunkCoords(wc);
  93. var chunks = new GridPointsNearEnumerator(coords.Floored(),
  94. (int) Math.Ceiling(worldLoader.Radius / (float) WorldGen.ChunkSize) + 1);
  95. var set = chunksToLoad[map];
  96. while (chunks.MoveNext(out var chunk))
  97. {
  98. if (!set.TryGetValue(chunk.Value, out _))
  99. set[chunk.Value] = new List<EntityUid>(4);
  100. set[chunk.Value].Add(uid);
  101. }
  102. }
  103. var mindEnum = EntityQueryEnumerator<MindContainerComponent, TransformComponent>();
  104. var ghostQuery = GetEntityQuery<GhostComponent>();
  105. // Mindful entities get special privilege as they're always a player and we don't want the illusion being broken around them.
  106. while (mindEnum.MoveNext(out var uid, out var mind, out var xform))
  107. {
  108. if (!mind.HasMind)
  109. continue;
  110. if (ghostQuery.HasComponent(uid))
  111. continue;
  112. var mapOrNull = xform.MapUid;
  113. if (mapOrNull is null)
  114. continue;
  115. var map = mapOrNull.Value;
  116. if (!chunksToLoad.ContainsKey(map))
  117. continue;
  118. var wc = _xformSys.GetWorldPosition(xform);
  119. var coords = WorldGen.WorldToChunkCoords(wc);
  120. var chunks = new GridPointsNearEnumerator(coords.Floored(), PlayerLoadRadius);
  121. var set = chunksToLoad[map];
  122. while (chunks.MoveNext(out var chunk))
  123. {
  124. if (!set.TryGetValue(chunk.Value, out _))
  125. set[chunk.Value] = new List<EntityUid>(4);
  126. set[chunk.Value].Add(uid);
  127. }
  128. }
  129. var loadedEnum = EntityQueryEnumerator<LoadedChunkComponent, WorldChunkComponent>();
  130. var chunksUnloaded = 0;
  131. // Make sure these chunks get unloaded at the end of the tick.
  132. while (loadedEnum.MoveNext(out var uid, out var _, out var chunk))
  133. {
  134. var coords = chunk.Coordinates;
  135. if (!chunksToLoad[chunk.Map].ContainsKey(coords))
  136. {
  137. RemCompDeferred<LoadedChunkComponent>(uid);
  138. chunksUnloaded++;
  139. }
  140. }
  141. if (chunksUnloaded > 0)
  142. _sawmill.Debug($"Queued {chunksUnloaded} chunks for unload.");
  143. if (chunksToLoad.All(x => x.Value.Count == 0))
  144. return;
  145. var startTime = _gameTiming.RealTime;
  146. var count = 0;
  147. var loadedQuery = GetEntityQuery<LoadedChunkComponent>();
  148. var controllerQuery = GetEntityQuery<WorldControllerComponent>();
  149. foreach (var (map, chunks) in chunksToLoad)
  150. {
  151. var controller = controllerQuery.GetComponent(map);
  152. foreach (var (chunk, loaders) in chunks)
  153. {
  154. var ent = GetOrCreateChunk(chunk, map, controller); // Ensure everything loads.
  155. LoadedChunkComponent? c = null;
  156. if (ent is not null && !loadedQuery.TryGetComponent(ent.Value, out c))
  157. {
  158. c = AddComp<LoadedChunkComponent>(ent.Value);
  159. count += 1;
  160. }
  161. if (c is not null)
  162. c.Loaders = loaders;
  163. }
  164. }
  165. if (count > 0)
  166. {
  167. var timeSpan = _gameTiming.RealTime - startTime;
  168. _sawmill.Debug($"Loaded {count} chunks in {timeSpan.TotalMilliseconds:N2}ms.");
  169. }
  170. }
  171. /// <summary>
  172. /// Attempts to get a chunk, creating it if it doesn't exist.
  173. /// </summary>
  174. /// <param name="chunk">Chunk coordinates to get the chunk entity for.</param>
  175. /// <param name="map">Map the chunk is in.</param>
  176. /// <param name="controller">The controller this chunk belongs to.</param>
  177. /// <returns>A chunk, if available.</returns>
  178. [Pure]
  179. public EntityUid? GetOrCreateChunk(Vector2i chunk, EntityUid map, WorldControllerComponent? controller = null)
  180. {
  181. if (!Resolve(map, ref controller))
  182. throw new Exception($"Tried to use {ToPrettyString(map)} as a world map, without actually being one.");
  183. if (controller.Chunks.TryGetValue(chunk, out var ent))
  184. return ent;
  185. return CreateChunkEntity(chunk, map, controller);
  186. }
  187. /// <summary>
  188. /// Constructs a new chunk entity, attaching it to the map.
  189. /// </summary>
  190. /// <param name="chunkCoords">The coordinates the new chunk should be initialized for.</param>
  191. /// <param name="map"></param>
  192. /// <param name="controller"></param>
  193. /// <returns></returns>
  194. private EntityUid CreateChunkEntity(Vector2i chunkCoords, EntityUid map, WorldControllerComponent controller)
  195. {
  196. var chunk = Spawn(controller.ChunkProto, MapCoordinates.Nullspace);
  197. StartupChunkEntity(chunk, chunkCoords, map, controller);
  198. _metaData.SetEntityName(chunk, $"Chunk {chunkCoords.X}/{chunkCoords.Y}");
  199. return chunk;
  200. }
  201. private void StartupChunkEntity(EntityUid chunk, Vector2i coords, EntityUid map,
  202. WorldControllerComponent controller)
  203. {
  204. if (!TryComp<WorldChunkComponent>(chunk, out var chunkComponent))
  205. {
  206. _sawmill.Error($"Chunk {ToPrettyString(chunk)} is missing WorldChunkComponent.");
  207. return;
  208. }
  209. ref var chunks = ref controller.Chunks;
  210. chunks[coords] = chunk; // Add this entity to chunk index.
  211. chunkComponent.Coordinates = coords;
  212. chunkComponent.Map = map;
  213. var ev = new WorldChunkAddedEvent(chunk, coords);
  214. RaiseLocalEvent(map, ref ev, broadcast: true);
  215. }
  216. }
  217. /// <summary>
  218. /// A directed event fired when a chunk is initially set up in the world. The chunk is not loaded at this point.
  219. /// </summary>
  220. [ByRefEvent]
  221. [PublicAPI]
  222. public readonly record struct WorldChunkAddedEvent(EntityUid Chunk, Vector2i Coords);
  223. /// <summary>
  224. /// A directed event fired when a chunk is loaded into the world, i.e. a player or other world loader has entered vicinity.
  225. /// </summary>
  226. [ByRefEvent]
  227. [PublicAPI]
  228. public readonly record struct WorldChunkLoadedEvent(EntityUid Chunk, Vector2i Coords);
  229. /// <summary>
  230. /// A directed event fired when a chunk is unloaded from the world, i.e. no world loaders remain nearby.
  231. /// </summary>
  232. [ByRefEvent]
  233. [PublicAPI]
  234. public readonly record struct WorldChunkUnloadedEvent(EntityUid Chunk, Vector2i Coords);