using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Numerics; using Robust.Shared.EntitySerialization; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Map.Events; using Robust.Shared.Map.Components; using Robust.Shared.Map.Enumerators; using Robust.Shared.Maths; using Robust.Shared.Prototypes; using Robust.Shared.Utility; namespace Robust.Shared.GameStates; /// /// Manages nullspace entities that are treated as members of map/grid PVS chunks. /// This allows you to store data at a chunk-level on maps / grids and have them streamed in/out without /// manually handling it. These chunks will also never be returned by EntityLookupSystem. /// /// /// You will need to handle yourself. /// public abstract partial class ChunkEntitySystem : EntitySystem { public const int ChunkSize = MapGridComponent.DefaultChunkSize; private static readonly EntProtoId ChunkEntityPrototype = "ChunkEntity"; [Dependency] private EntityQuery _chentQuery; [Dependency] private EntityQuery _metaQuery; [Dependency] private EntityQuery _mapQuery; [Dependency] private EntityQuery _gridQuery; [Dependency] private EntityQuery _containerQuery; [Dependency] private MetaDataSystem _metaData = default!; /// /// Temporary list for serialization. /// private readonly List _serializationContainers = new(); private readonly List _tempUids = new(); public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnMapCreated); SubscribeLocalEvent(OnGridInitialize); SubscribeLocalEvent(OnChunkStartup); SubscribeLocalEvent(OnChunkShutdown); SubscribeLocalEvent(OnChunkTerminating); SubscribeLocalEvent(OnChunkHandleState); SubscribeLocalEvent(OnContainerMapInit); SubscribeLocalEvent(OnBeforeSerialization); SubscribeLocalEvent(OnMapRemoved); SubscribeLocalEvent(OnGridRemoved); } /// /// Converts local map/grid coordinates to the chunk index used by chunk entities. /// public static Vector2i GetChunkIndices(Vector2 coordinates) { return new Vector2i( (int) Math.Floor(coordinates.X / ChunkSize), (int) Math.Floor(coordinates.Y / ChunkSize)); } /// /// Will return the relevant chunk for the specified position. Assumes normalized chunk origin i.e. position divided by ChunkSize. /// public Entity GetOrCreateChunk(EntityUid root, Vector2i chunk) { if (TryGetChunk(root, chunk, out var existing)) { return existing.Value; } if (!_mapQuery.HasComp(root) && !_gridQuery.HasComp(root)) throw new ArgumentException($"Chunk entity root {ToPrettyString(root)} is not a map or grid.", nameof(root)); var rootMeta = _metaQuery.GetComponent(root); var uid = EntityManager.PredictedSpawn( ChunkEntityPrototype, doMapInit: rootMeta.EntityLifeStage == EntityLifeStage.MapInitialized); // TODO: compreg / faster addcomp var comp = new ChunkEntityComponent { Root = root, Chunk = chunk, }; AddComp(uid, comp); return (uid, comp); } /// /// Returns a chunk for the specified position if one exists. Assumes normalized chunk origin i.e. position divided by ChunkSize. /// public bool TryGetChunk(EntityUid root, Vector2i chunk, [NotNullWhen(true)] out Entity? entity) { if (_containerQuery.TryGetComponent(root, out var container) && container.Chunks.TryGetValue(chunk, out var existing) && IsAvailable(existing)) { entity = (existing.Owner, existing.Comp); return true; } entity = null; return false; } /// /// Returns all known, attached chunk entities for the specified root. /// public ChunkEntityRootEnumerator GetChunks(EntityUid root) { _containerQuery.TryGetComponent(root, out var container); return new ChunkEntityRootEnumerator(this, container); } /// /// Flags a chunk that it should try to be deleted. Will fail if any other components exist on it. /// /// public bool TryRemoveChunk(Entity chunk) { var meta = chunk.Comp2; if (!_metaQuery.Resolve(chunk.Owner, ref meta)) { return false; } if (Deleted(chunk.Owner, metaData: meta)) return true; // Still has hanging components. if (EntityManager.ComponentCount(chunk.Owner) > 3) return false; Del(chunk.Owner, meta); return true; } // Returns chunk entities in range of the position, assumes non-normalized inputs. public ChunkEntityEnumerator GetChunksInRange(EntityUid root, Vector2 localPosition, float range) { return new ChunkEntityEnumerator(this, root, new ChunkIndicesEnumerator(localPosition, range, ChunkSize)); } // Returns chunk entities intersecting a local bounding box, assumes non-normalized inputs. public ChunkEntityEnumerator GetChunksIntersecting(EntityUid root, Box2 localAabb) { return new ChunkEntityEnumerator(this, root, new ChunkIndicesEnumerator(localAabb, ChunkSize)); } /// /// but with component overload. /// public ChunkEntityComponentEnumerator GetChunksInRange( EntityUid root, Vector2 localPosition, float range, EntityQuery query) where T : IComponent { return new ChunkEntityComponentEnumerator(GetChunksInRange(root, localPosition, range), query); } /// /// but with component overload. /// public ChunkEntityComponentEnumerator GetChunksIntersecting( EntityUid root, Box2 localAabb, EntityQuery query) where T : IComponent { return new ChunkEntityComponentEnumerator(GetChunksIntersecting(root, localAabb), query); } private void OnMapCreated(MapCreatedEvent ev) { EnsureComp(ev.Uid); } private void OnGridInitialize(GridInitializeEvent ev) { EnsureComp(ev.EntityUid); } private void OnChunkStartup(Entity ent, ref ComponentStartup args) { AddChunk(ent); } private void OnChunkShutdown(Entity ent, ref ComponentShutdown args) { RemoveChunk(ent); } private void OnChunkTerminating(Entity ent, ref EntityTerminatingEvent args) { RemoveChunk(ent); } private void OnChunkHandleState(Entity ent, ref AfterAutoHandleStateEvent args) { AddChunk(ent); } private void OnContainerMapInit(Entity ent, ref MapInitEvent args) { _tempUids.Clear(); _tempUids.AddRange(ent.Comp.ChunkEntities); foreach (var chunk in _tempUids) { if (!_chentQuery.TryComp(chunk, out var chunkComp) || !_metaQuery.TryComp(chunk, out var chunkMeta)) { continue; } SyncChunkToRoot((chunk, chunkComp, chunkMeta), ent.Owner); } _tempUids.Clear(); } private void OnMapRemoved(MapRemovedEvent ev) { DeleteRootChunks(ev.Uid); } private void OnGridRemoved(GridRemovalEvent ev) { DeleteRootChunks(ev.EntityUid); } private void OnBeforeSerialization(BeforeSerializationEvent ev) { if (ev.Category != FileCategory.Map && ev.Category != FileCategory.Grid && ev.Category != FileCategory.Save) { return; } // Chunk entities are nullspace entities referenced by the saved root container (i.e. grid or map), // so we need to ensure they are included to serialize properly (e.g. in case grid-only serialization excludes them). // This assumption may change at some point but for now we need to do this. DebugTools.Assert(_serializationContainers.Count == 0); foreach (var root in ev.Entities) { if (_containerQuery.TryGetComponent(root, out var container)) _serializationContainers.Add(container); } foreach (var container in _serializationContainers) { SyncRootChunks(container); AddRootChunks(container, ev.Entities); } _serializationContainers.Clear(); } private void SyncRootChunks(ChunkContainerComponent container) { foreach (var chunk in container.ChunkEntities) { if (!_chentQuery.TryComp(chunk, out var chunkComp) || !_metaQuery.TryComp(chunkComp.Root, out var rootMeta) || !_metaQuery.TryComp(chunk, out var chunkMeta)) { continue; } SyncChunkToRoot((chunk, chunkComp, chunkMeta), rootMeta); if (rootMeta.EntityLifeStage != EntityLifeStage.MapInitialized && chunkMeta.EntityLifeStage == EntityLifeStage.MapInitialized) { EntityManager.SetLifeStage(chunkMeta, EntityLifeStage.Initialized); } } } private void SyncChunkToRoot(Entity chunk, EntityUid root) { if (!_metaQuery.TryComp(root, out var rootMeta)) return; SyncChunkToRoot(chunk, rootMeta); } private void SyncChunkToRoot(Entity chunk, MetaDataComponent rootMeta) { if (rootMeta.EntityLifeStage == EntityLifeStage.MapInitialized && chunk.Comp2.EntityLifeStage == EntityLifeStage.Initialized) { EntityManager.RunMapInit(chunk.Owner, chunk.Comp2); } _metaData.SetEntityPaused(chunk.Owner, rootMeta.EntityPaused, chunk.Comp2); } private void AddRootChunks(ChunkContainerComponent container, HashSet entities) { foreach (var chunk in container.ChunkEntities) { DebugTools.Assert(!Deleted(chunk)); entities.Add(chunk); } } private void AddChunk(Entity ent) { var (uid, comp, meta) = ent; var key = (comp.Root, comp.Chunk); meta ??= MetaData(uid); if (!_mapQuery.HasComp(comp.Root) && !_gridQuery.HasComp(comp.Root)) { Del(uid, meta); return; } if (!_containerQuery.TryGetComponent(comp.Root, out var container)) { DebugTools.Assert($"{nameof(ChunkContainerComponent)} missing on chunk root {ToPrettyString(comp.Root)}."); container = EnsureComp(comp.Root); } if (container.Chunks.TryGetValue(comp.Chunk, out var oldChunk)) { if (oldChunk.Owner == uid) { DebugTools.Assert(container.ChunkEntities.Contains(uid)); meta.Flags |= MetaDataFlags.ChunkEntity; SyncChunkToRoot((uid, comp, meta), comp.Root); return; } DebugTools.Assert($"Duplicate chunk entity for root {ToPrettyString(comp.Root)} and chunk {comp.Chunk}."); } meta.Flags |= MetaDataFlags.ChunkEntity; container.Chunks[comp.Chunk] = (uid, comp); container.ChunkEntities.Add(uid); SyncChunkToRoot((uid, comp, meta), comp.Root); var ev = new ChunkEntityAddedEvent(uid, key.Root, key.Chunk); RaiseLocalEvent(ref ev); } private bool IsAvailable(Entity chunk) { return _metaQuery.TryGetComponent(chunk.Owner, out var meta) && !Deleted(chunk.Owner, meta) && !chunk.Comp.Deleted && (meta.Flags & MetaDataFlags.Detached) == 0; } private void RemoveChunk(Entity ent) { var (uid, comp) = ent; if (_containerQuery.TryGetComponent(comp.Root, out var container) && container.Chunks.TryGetValue(comp.Chunk, out var existing) && existing.Owner == uid) { container.Chunks.Remove(comp.Chunk); } container?.ChunkEntities.Remove(uid); var ev = new ChunkEntityRemovedEvent(uid, comp.Root, comp.Chunk); RaiseLocalEvent(ref ev); if (_metaQuery.TryGetComponent(uid, out var meta)) meta.Flags &= ~MetaDataFlags.ChunkEntity; } private void DeleteRootChunks(EntityUid root) { if (!_containerQuery.TryGetComponent(root, out var container)) return; _tempUids.Clear(); _tempUids.AddRange(container.ChunkEntities); foreach (var uid in _tempUids) { if (!_chentQuery.TryComp(uid, out var chent)) { continue; } container.Chunks.Remove(chent.Chunk); container.ChunkEntities.Remove(uid); if (_metaQuery.TryGetComponent(uid, out var meta) && !Deleted(uid, meta)) { var ev = new ChunkEntityRemovedEvent(uid, root, chent.Chunk); RaiseLocalEvent(ref ev); Del(uid, meta); } } } public struct ChunkEntityEnumerator { private readonly ChunkEntitySystem _system; private readonly EntityUid _root; private ChunkIndicesEnumerator _indices; private Entity _current; internal ChunkEntityEnumerator(ChunkEntitySystem system, EntityUid root, ChunkIndicesEnumerator indices) { _system = system; _root = root; _indices = indices; _current = default; } public readonly ChunkEntityEnumerator GetEnumerator() => this; public readonly Entity Current => _current; public bool MoveNext() { while (_indices.MoveNext(out var chunk)) { if (_system.TryGetChunk(_root, chunk.Value, out var entity)) { _current = entity.Value; return true; } } return false; } public bool MoveNext([NotNullWhen(true)] out Entity? entity) { if (MoveNext()) { entity = _current; return true; } entity = null; return false; } } public struct ChunkEntityRootEnumerator { private readonly ChunkEntitySystem _system; private Dictionary>.Enumerator _enumerator; private readonly bool _valid; private Entity _current; internal ChunkEntityRootEnumerator(ChunkEntitySystem system, ChunkContainerComponent? container) { _system = system; _valid = container != null; _enumerator = container?.Chunks.GetEnumerator() ?? default; _current = default; } public readonly ChunkEntityRootEnumerator GetEnumerator() => this; public readonly Entity Current => _current; public bool MoveNext() { while (_valid && _enumerator.MoveNext()) { var chunk = _enumerator.Current.Value; if (_system.IsAvailable(chunk)) { _current = (chunk.Owner, chunk.Comp); return true; } } return false; } public bool MoveNext([NotNullWhen(true)] out Entity? entity) { if (MoveNext()) { entity = _current; return true; } entity = null; return false; } } public struct ChunkEntityComponentEnumerator where T : IComponent { private ChunkEntityEnumerator _enumerator; private readonly EntityQuery _query; private Entity _current; internal ChunkEntityComponentEnumerator(ChunkEntityEnumerator enumerator, EntityQuery query) { _enumerator = enumerator; _query = query; _current = default; } public readonly ChunkEntityComponentEnumerator GetEnumerator() => this; public readonly Entity Current => _current; public bool MoveNext() { while (_enumerator.MoveNext(out var chunk)) { if (_query.TryComp(chunk.Value.Owner, out var comp)) { _current = (chunk.Value.Owner, chunk.Value.Comp, comp); return true; } } return false; } public bool MoveNext([NotNullWhen(true)] out Entity? entity) { if (MoveNext()) { entity = _current; return true; } entity = null; return false; } } } /// /// Raised when a chunk entity is added. /// [ByRefEvent] public record struct ChunkEntityAddedEvent(EntityUid Entity, EntityUid Root, Vector2i Chunk) { public readonly EntityUid Entity = Entity; public readonly EntityUid Root = Root; public readonly Vector2i Chunk = Chunk; } /// /// Raised when a chunk entity is removed. /// [ByRefEvent] public record struct ChunkEntityRemovedEvent(EntityUid Entity, EntityUid Root, Vector2i Chunk) { public readonly EntityUid Entity = Entity; public readonly EntityUid Root = Root; public readonly Vector2i Chunk = Chunk; }