using System.Linq;
using Content.Server.Worldgen.Components;
using Content.Shared.Ghost;
using Content.Shared.Mind.Components;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Timing;
namespace Content.Server.Worldgen.Systems;
///
/// This handles putting together chunk entities and notifying them about important changes.
///
public sealed class WorldControllerSystem : EntitySystem
{
[Dependency] private readonly TransformSystem _xformSys = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
private const int PlayerLoadRadius = 2;
private ISawmill _sawmill = default!;
///
public override void Initialize()
{
_sawmill = _logManager.GetSawmill("world");
SubscribeLocalEvent(OnChunkLoadedCore);
SubscribeLocalEvent(OnChunkUnloadedCore);
SubscribeLocalEvent(OnChunkShutdown);
}
///
/// Handles deleting chunks properly.
///
private void OnChunkShutdown(EntityUid uid, WorldChunkComponent component, ComponentShutdown args)
{
if (!TryComp(component.Map, out var controller))
return;
if (HasComp(uid))
{
var ev = new WorldChunkUnloadedEvent(uid, component.Coordinates);
RaiseLocalEvent(component.Map, ref ev);
RaiseLocalEvent(uid, ref ev, broadcast: true);
}
controller.Chunks.Remove(component.Coordinates);
}
///
/// Handles the inner logic of loading a chunk, i.e. events.
///
private void OnChunkLoadedCore(EntityUid uid, LoadedChunkComponent component, ComponentStartup args)
{
if (!TryComp(uid, out var chunk))
return;
var ev = new WorldChunkLoadedEvent(uid, chunk.Coordinates);
RaiseLocalEvent(chunk.Map, ref ev);
RaiseLocalEvent(uid, ref ev, broadcast: true);
//_sawmill.Debug($"Loaded chunk {ToPrettyString(uid)} at {chunk.Coordinates}");
}
///
/// Handles the inner logic of unloading a chunk, i.e. events.
///
private void OnChunkUnloadedCore(EntityUid uid, LoadedChunkComponent component, ComponentShutdown args)
{
if (!TryComp(uid, out var chunk))
return;
if (Terminating(uid))
return; // SAFETY: This is in case a loaded chunk gets deleted, to avoid double unload.
var ev = new WorldChunkUnloadedEvent(uid, chunk.Coordinates);
RaiseLocalEvent(chunk.Map, ref ev);
RaiseLocalEvent(uid, ref ev);
//_sawmill.Debug($"Unloaded chunk {ToPrettyString(uid)} at {coords}");
}
///
public override void Update(float frameTime)
{
//there was a to-do here about every frame alloc but it turns out it's a nothing burger here.
var chunksToLoad = new Dictionary>>();
var controllerEnum = EntityQueryEnumerator();
while (controllerEnum.MoveNext(out var uid, out _))
{
chunksToLoad[uid] = new Dictionary>();
}
if (chunksToLoad.Count == 0)
return; // Just bail early.
var loaderEnum = EntityQueryEnumerator();
while (loaderEnum.MoveNext(out var uid, out var worldLoader, out var xform))
{
var mapOrNull = xform.MapUid;
if (mapOrNull is null)
continue;
var map = mapOrNull.Value;
if (!chunksToLoad.ContainsKey(map))
continue;
var wc = _xformSys.GetWorldPosition(xform);
var coords = WorldGen.WorldToChunkCoords(wc);
var chunks = new GridPointsNearEnumerator(coords.Floored(),
(int) Math.Ceiling(worldLoader.Radius / (float) WorldGen.ChunkSize) + 1);
var set = chunksToLoad[map];
while (chunks.MoveNext(out var chunk))
{
if (!set.TryGetValue(chunk.Value, out _))
set[chunk.Value] = new List(4);
set[chunk.Value].Add(uid);
}
}
var mindEnum = EntityQueryEnumerator();
var ghostQuery = GetEntityQuery();
// Mindful entities get special privilege as they're always a player and we don't want the illusion being broken around them.
while (mindEnum.MoveNext(out var uid, out var mind, out var xform))
{
if (!mind.HasMind)
continue;
if (ghostQuery.HasComponent(uid))
continue;
var mapOrNull = xform.MapUid;
if (mapOrNull is null)
continue;
var map = mapOrNull.Value;
if (!chunksToLoad.ContainsKey(map))
continue;
var wc = _xformSys.GetWorldPosition(xform);
var coords = WorldGen.WorldToChunkCoords(wc);
var chunks = new GridPointsNearEnumerator(coords.Floored(), PlayerLoadRadius);
var set = chunksToLoad[map];
while (chunks.MoveNext(out var chunk))
{
if (!set.TryGetValue(chunk.Value, out _))
set[chunk.Value] = new List(4);
set[chunk.Value].Add(uid);
}
}
var loadedEnum = EntityQueryEnumerator();
var chunksUnloaded = 0;
// Make sure these chunks get unloaded at the end of the tick.
while (loadedEnum.MoveNext(out var uid, out var _, out var chunk))
{
var coords = chunk.Coordinates;
if (!chunksToLoad[chunk.Map].ContainsKey(coords))
{
RemCompDeferred(uid);
chunksUnloaded++;
}
}
if (chunksUnloaded > 0)
_sawmill.Debug($"Queued {chunksUnloaded} chunks for unload.");
if (chunksToLoad.All(x => x.Value.Count == 0))
return;
var startTime = _gameTiming.RealTime;
var count = 0;
var loadedQuery = GetEntityQuery();
var controllerQuery = GetEntityQuery();
foreach (var (map, chunks) in chunksToLoad)
{
var controller = controllerQuery.GetComponent(map);
foreach (var (chunk, loaders) in chunks)
{
var ent = GetOrCreateChunk(chunk, map, controller); // Ensure everything loads.
LoadedChunkComponent? c = null;
if (ent is not null && !loadedQuery.TryGetComponent(ent.Value, out c))
{
c = AddComp(ent.Value);
count += 1;
}
if (c is not null)
c.Loaders = loaders;
}
}
if (count > 0)
{
var timeSpan = _gameTiming.RealTime - startTime;
_sawmill.Debug($"Loaded {count} chunks in {timeSpan.TotalMilliseconds:N2}ms.");
}
}
///
/// Attempts to get a chunk, creating it if it doesn't exist.
///
/// Chunk coordinates to get the chunk entity for.
/// Map the chunk is in.
/// The controller this chunk belongs to.
/// A chunk, if available.
[Pure]
public EntityUid? GetOrCreateChunk(Vector2i chunk, EntityUid map, WorldControllerComponent? controller = null)
{
if (!Resolve(map, ref controller))
throw new Exception($"Tried to use {ToPrettyString(map)} as a world map, without actually being one.");
if (controller.Chunks.TryGetValue(chunk, out var ent))
return ent;
return CreateChunkEntity(chunk, map, controller);
}
///
/// Constructs a new chunk entity, attaching it to the map.
///
/// The coordinates the new chunk should be initialized for.
///
///
///
private EntityUid CreateChunkEntity(Vector2i chunkCoords, EntityUid map, WorldControllerComponent controller)
{
var chunk = Spawn(controller.ChunkProto, MapCoordinates.Nullspace);
StartupChunkEntity(chunk, chunkCoords, map, controller);
_metaData.SetEntityName(chunk, $"Chunk {chunkCoords.X}/{chunkCoords.Y}");
return chunk;
}
private void StartupChunkEntity(EntityUid chunk, Vector2i coords, EntityUid map,
WorldControllerComponent controller)
{
if (!TryComp(chunk, out var chunkComponent))
{
_sawmill.Error($"Chunk {ToPrettyString(chunk)} is missing WorldChunkComponent.");
return;
}
ref var chunks = ref controller.Chunks;
chunks[coords] = chunk; // Add this entity to chunk index.
chunkComponent.Coordinates = coords;
chunkComponent.Map = map;
var ev = new WorldChunkAddedEvent(chunk, coords);
RaiseLocalEvent(map, ref ev, broadcast: true);
}
}
///
/// A directed event fired when a chunk is initially set up in the world. The chunk is not loaded at this point.
///
[ByRefEvent]
[PublicAPI]
public readonly record struct WorldChunkAddedEvent(EntityUid Chunk, Vector2i Coords);
///
/// A directed event fired when a chunk is loaded into the world, i.e. a player or other world loader has entered vicinity.
///
[ByRefEvent]
[PublicAPI]
public readonly record struct WorldChunkLoadedEvent(EntityUid Chunk, Vector2i Coords);
///
/// A directed event fired when a chunk is unloaded from the world, i.e. no world loaders remain nearby.
///
[ByRefEvent]
[PublicAPI]
public readonly record struct WorldChunkUnloadedEvent(EntityUid Chunk, Vector2i Coords);