mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-09-15 14:52:35 +02:00
577 lines
19 KiB
C#
577 lines
19 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// You will need to handle <see cref="PostGridSplitEvent"/> yourself.
|
|
/// </remarks>
|
|
public abstract partial class ChunkEntitySystem : EntitySystem
|
|
{
|
|
public const int ChunkSize = MapGridComponent.DefaultChunkSize;
|
|
private static readonly EntProtoId ChunkEntityPrototype = "ChunkEntity";
|
|
|
|
[Dependency] private EntityQuery<ChunkEntityComponent> _chentQuery;
|
|
[Dependency] private EntityQuery<MetaDataComponent> _metaQuery;
|
|
[Dependency] private EntityQuery<MapComponent> _mapQuery;
|
|
[Dependency] private EntityQuery<MapGridComponent> _gridQuery;
|
|
[Dependency] private EntityQuery<ChunkContainerComponent> _containerQuery;
|
|
[Dependency] private MetaDataSystem _metaData = default!;
|
|
|
|
/// <summary>
|
|
/// Temporary list for serialization.
|
|
/// </summary>
|
|
private readonly List<ChunkContainerComponent> _serializationContainers = new();
|
|
|
|
private readonly List<EntityUid> _tempUids = new();
|
|
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
SubscribeLocalEvent<MapCreatedEvent>(OnMapCreated);
|
|
SubscribeLocalEvent<GridInitializeEvent>(OnGridInitialize);
|
|
SubscribeLocalEvent<ChunkEntityComponent, ComponentStartup>(OnChunkStartup);
|
|
SubscribeLocalEvent<ChunkEntityComponent, ComponentShutdown>(OnChunkShutdown);
|
|
SubscribeLocalEvent<ChunkEntityComponent, EntityTerminatingEvent>(OnChunkTerminating);
|
|
SubscribeLocalEvent<ChunkEntityComponent, AfterAutoHandleStateEvent>(OnChunkHandleState);
|
|
SubscribeLocalEvent<ChunkContainerComponent, MapInitEvent>(OnContainerMapInit);
|
|
SubscribeLocalEvent<BeforeSerializationEvent>(OnBeforeSerialization);
|
|
SubscribeLocalEvent<MapRemovedEvent>(OnMapRemoved);
|
|
SubscribeLocalEvent<GridRemovalEvent>(OnGridRemoved);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts local map/grid coordinates to the chunk index used by chunk entities.
|
|
/// </summary>
|
|
public static Vector2i GetChunkIndices(Vector2 coordinates)
|
|
{
|
|
return new Vector2i(
|
|
(int) Math.Floor(coordinates.X / ChunkSize),
|
|
(int) Math.Floor(coordinates.Y / ChunkSize));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Will return the relevant chunk for the specified position. Assumes normalized chunk origin i.e. position divided by ChunkSize.
|
|
/// </summary>
|
|
public Entity<ChunkEntityComponent> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a chunk for the specified position if one exists. Assumes normalized chunk origin i.e. position divided by ChunkSize.
|
|
/// </summary>
|
|
public bool TryGetChunk(EntityUid root, Vector2i chunk, [NotNullWhen(true)] out Entity<ChunkEntityComponent>? 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns all known, attached chunk entities for the specified root.
|
|
/// </summary>
|
|
public ChunkEntityRootEnumerator GetChunks(EntityUid root)
|
|
{
|
|
_containerQuery.TryGetComponent(root, out var container);
|
|
return new ChunkEntityRootEnumerator(this, container);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Flags a chunk that it should try to be deleted. Will fail if any other components exist on it.
|
|
/// </summary>
|
|
/// <param name="chunk"></param>
|
|
public bool TryRemoveChunk(Entity<ChunkEntityComponent, MetaDataComponent?> 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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// <see cref="GetChunksInRange"/> but with component overload.
|
|
/// </summary>
|
|
public ChunkEntityComponentEnumerator<T> GetChunksInRange<T>(
|
|
EntityUid root,
|
|
Vector2 localPosition,
|
|
float range,
|
|
EntityQuery<T> query)
|
|
where T : IComponent
|
|
{
|
|
return new ChunkEntityComponentEnumerator<T>(GetChunksInRange(root, localPosition, range), query);
|
|
}
|
|
|
|
/// <summary>
|
|
/// <see cref="GetChunksIntersecting"/> but with component overload.
|
|
/// </summary>
|
|
public ChunkEntityComponentEnumerator<T> GetChunksIntersecting<T>(
|
|
EntityUid root,
|
|
Box2 localAabb,
|
|
EntityQuery<T> query)
|
|
where T : IComponent
|
|
{
|
|
return new ChunkEntityComponentEnumerator<T>(GetChunksIntersecting(root, localAabb), query);
|
|
}
|
|
|
|
private void OnMapCreated(MapCreatedEvent ev)
|
|
{
|
|
EnsureComp<ChunkContainerComponent>(ev.Uid);
|
|
}
|
|
|
|
private void OnGridInitialize(GridInitializeEvent ev)
|
|
{
|
|
EnsureComp<ChunkContainerComponent>(ev.EntityUid);
|
|
}
|
|
|
|
private void OnChunkStartup(Entity<ChunkEntityComponent> ent, ref ComponentStartup args)
|
|
{
|
|
AddChunk(ent);
|
|
}
|
|
|
|
private void OnChunkShutdown(Entity<ChunkEntityComponent> ent, ref ComponentShutdown args)
|
|
{
|
|
RemoveChunk(ent);
|
|
}
|
|
|
|
private void OnChunkTerminating(Entity<ChunkEntityComponent> ent, ref EntityTerminatingEvent args)
|
|
{
|
|
RemoveChunk(ent);
|
|
}
|
|
|
|
private void OnChunkHandleState(Entity<ChunkEntityComponent> ent, ref AfterAutoHandleStateEvent args)
|
|
{
|
|
AddChunk(ent);
|
|
}
|
|
|
|
private void OnContainerMapInit(Entity<ChunkContainerComponent> 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<ChunkEntityComponent, MetaDataComponent> chunk, EntityUid root)
|
|
{
|
|
if (!_metaQuery.TryComp(root, out var rootMeta))
|
|
return;
|
|
|
|
SyncChunkToRoot(chunk, rootMeta);
|
|
}
|
|
|
|
private void SyncChunkToRoot(Entity<ChunkEntityComponent, MetaDataComponent> 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<EntityUid> entities)
|
|
{
|
|
foreach (var chunk in container.ChunkEntities)
|
|
{
|
|
DebugTools.Assert(!Deleted(chunk));
|
|
entities.Add(chunk);
|
|
}
|
|
}
|
|
|
|
private void AddChunk(Entity<ChunkEntityComponent, MetaDataComponent?> 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<ChunkContainerComponent>(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<ChunkEntityComponent> 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<ChunkEntityComponent> 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<ChunkEntityComponent> _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<ChunkEntityComponent> 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<ChunkEntityComponent>? entity)
|
|
{
|
|
if (MoveNext())
|
|
{
|
|
entity = _current;
|
|
return true;
|
|
}
|
|
|
|
entity = null;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public struct ChunkEntityRootEnumerator
|
|
{
|
|
private readonly ChunkEntitySystem _system;
|
|
private Dictionary<Vector2i, Entity<ChunkEntityComponent>>.Enumerator _enumerator;
|
|
private readonly bool _valid;
|
|
private Entity<ChunkEntityComponent> _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<ChunkEntityComponent> 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<ChunkEntityComponent>? entity)
|
|
{
|
|
if (MoveNext())
|
|
{
|
|
entity = _current;
|
|
return true;
|
|
}
|
|
|
|
entity = null;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public struct ChunkEntityComponentEnumerator<T> where T : IComponent
|
|
{
|
|
private ChunkEntityEnumerator _enumerator;
|
|
private readonly EntityQuery<T> _query;
|
|
private Entity<ChunkEntityComponent, T> _current;
|
|
|
|
internal ChunkEntityComponentEnumerator(ChunkEntityEnumerator enumerator, EntityQuery<T> query)
|
|
{
|
|
_enumerator = enumerator;
|
|
_query = query;
|
|
_current = default;
|
|
}
|
|
|
|
public readonly ChunkEntityComponentEnumerator<T> GetEnumerator() => this;
|
|
|
|
public readonly Entity<ChunkEntityComponent, T> 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<ChunkEntityComponent, T>? entity)
|
|
{
|
|
if (MoveNext())
|
|
{
|
|
entity = _current;
|
|
return true;
|
|
}
|
|
|
|
entity = null;
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raised when a chunk entity is added.
|
|
/// </summary>
|
|
[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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raised when a chunk entity is removed.
|
|
/// </summary>
|
|
[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;
|
|
}
|