Files
tbd-station-14/Content.Server/Worldgen/Systems/Debris/DebrisFeaturePlacerSystem.cs
J afb0d7efca Worldgen spring cleaning (#36199)
* Worldgen warnings cleanup

* DebrisFeaturePlacerSystem general cleanup
2025-04-14 13:36:54 +10:00

279 lines
11 KiB
C#

using System.Linq;
using System.Numerics;
using Content.Server.Worldgen.Components;
using Content.Server.Worldgen.Components.Debris;
using Content.Server.Worldgen.Tools;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Worldgen.Systems.Debris;
/// <summary>
/// This handles placing debris within the world evenly with rng, primarily for structures like asteroid fields.
/// </summary>
public sealed class DebrisFeaturePlacerSystem : BaseWorldSystem
{
[Dependency] private readonly NoiseIndexSystem _noiseIndex = default!;
[Dependency] private readonly PoissonDiskSampler _sampler = default!;
[Dependency] private readonly TransformSystem _xformSys = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
private ISawmill _sawmill = default!;
private List<Entity<MapGridComponent>> _mapGrids = new();
/// <inheritdoc />
public override void Initialize()
{
_sawmill = _logManager.GetSawmill("world.debris.feature_placer");
SubscribeLocalEvent<DebrisFeaturePlacerControllerComponent, WorldChunkLoadedEvent>(OnChunkLoaded);
SubscribeLocalEvent<DebrisFeaturePlacerControllerComponent, WorldChunkUnloadedEvent>(OnChunkUnloaded);
SubscribeLocalEvent<OwnedDebrisComponent, ComponentShutdown>(OnDebrisShutdown);
SubscribeLocalEvent<OwnedDebrisComponent, MoveEvent>(OnDebrisMove);
SubscribeLocalEvent<SimpleDebrisSelectorComponent, TryGetPlaceableDebrisFeatureEvent>(
OnTryGetPlacableDebrisEvent);
}
/// <summary>
/// Handles debris moving, and making sure it stays parented to a chunk for loading purposes.
/// </summary>
private void OnDebrisMove(EntityUid uid, OwnedDebrisComponent component, ref MoveEvent args)
{
if (!HasComp<WorldChunkComponent>(component.OwningController))
return; // Redundant logic, prolly needs it's own handler for your custom system.
var placer = Comp<DebrisFeaturePlacerControllerComponent>(component.OwningController);
var xform = args.Component;
var ownerXform = Transform(component.OwningController);
if (xform.MapUid is null || ownerXform.MapUid is null)
return; // not our problem
if (xform.MapUid != ownerXform.MapUid)
{
_sawmill.Error($"Somehow debris {uid} left it's expected map! Unparenting it to avoid issues.");
RemCompDeferred<OwnedDebrisComponent>(uid);
placer.OwnedDebris.Remove(component.LastKey);
return;
}
placer.OwnedDebris.Remove(component.LastKey);
var newChunk = GetOrCreateChunk(GetChunkCoords(uid), xform.MapUid!.Value);
if (newChunk is null || !TryComp<DebrisFeaturePlacerControllerComponent>(newChunk, out var newPlacer))
{
// Whelp.
RemCompDeferred<OwnedDebrisComponent>(uid);
return;
}
newPlacer.OwnedDebris[_xformSys.GetWorldPosition(xform)] = uid; // Change our owner.
component.OwningController = newChunk.Value;
}
/// <summary>
/// Handles debris shutdown/detach.
/// </summary>
private void OnDebrisShutdown(EntityUid uid, OwnedDebrisComponent component, ComponentShutdown args)
{
if (!TryComp<DebrisFeaturePlacerControllerComponent>(component.OwningController, out var placer))
return;
placer.OwnedDebris[component.LastKey] = null;
if (Terminating(uid))
placer.OwnedDebris.Remove(component.LastKey);
}
/// <summary>
/// Queues all debris owned by the placer for garbage collection.
/// </summary>
private void OnChunkUnloaded(EntityUid uid, DebrisFeaturePlacerControllerComponent component,
ref WorldChunkUnloadedEvent args)
{
component.DoSpawns = true;
}
/// <summary>
/// Handles providing a debris type to place for SimpleDebrisSelectorComponent.
/// This randomly picks a debris type from the EntitySpawnCollectionCache.
/// </summary>
private void OnTryGetPlacableDebrisEvent(EntityUid uid, SimpleDebrisSelectorComponent component,
ref TryGetPlaceableDebrisFeatureEvent args)
{
if (args.DebrisProto is not null)
return;
var l = new List<string?>(1);
component.CachedDebrisTable.GetSpawns(_random, ref l);
switch (l.Count)
{
case 0:
return;
case > 1:
_sawmill.Warning($"Got more than one possible debris type from {uid}. List: {string.Join(", ", l)}");
break;
}
args.DebrisProto = l[0];
}
/// <summary>
/// Handles loading in debris. This does the following:
/// - Checks if the debris is currently supposed to do spawns, if it isn't, aborts immediately.
/// - Evaluates the density value to be used for placement, if it's zero, aborts.
/// - Generates the points to generate debris at, if and only if they've not been selected already by a prior load.
/// - Does the following in a loop over all generated points:
/// - Raises an event to check if something else wants to intercept debris placement, if the event is handled,
/// continues to the next point without generating anything.
/// - Raises an event to get the debris type that should be used for generation.
/// - Spawns the given debris at the point, adding it to the placer's index.
/// </summary>
private void OnChunkLoaded(EntityUid uid, DebrisFeaturePlacerControllerComponent component,
ref WorldChunkLoadedEvent args)
{
if (component.DoSpawns == false)
return;
component.DoSpawns = false; // Don't repeat yourself if this crashes.
if (!TryComp<WorldChunkComponent>(args.Chunk, out var chunk))
return;
var chunkMap = chunk.Map;
if (!TryComp<MapComponent>(chunkMap, out var map))
return;
var densityChannel = component.DensityNoiseChannel;
var density = _noiseIndex.Evaluate(uid, densityChannel, chunk.Coordinates + new Vector2(0.5f, 0.5f));
if (density == 0)
return;
List<Vector2>? points = null;
// If we've been loaded before, reuse the same coordinates.
if (component.OwnedDebris.Count != 0)
{
//TODO: Remove LINQ.
points = component.OwnedDebris
.Where(x => !Deleted(x.Value))
.Select(static x => x.Key)
.ToList();
}
points ??= GeneratePointsInChunk(args.Chunk, density, chunk.Coordinates, chunkMap);
var mapId = map.MapId;
var safetyBounds = Box2.UnitCentered.Enlarged(component.SafetyZoneRadius);
var failures = 0; // Avoid severe log spam.
foreach (var point in points)
{
if (component.OwnedDebris.TryGetValue(point, out var existing))
{
DebugTools.Assert(Exists(existing));
continue;
}
var pointDensity = _noiseIndex.Evaluate(uid, densityChannel, WorldGen.WorldToChunkCoords(point));
if (pointDensity == 0 && component.DensityClip || _random.Prob(component.RandomCancellationChance))
continue;
if (HasCollisions(mapId, safetyBounds.Translated(point)))
continue;
var coords = new EntityCoordinates(chunkMap, point);
var preEv = new PrePlaceDebrisFeatureEvent(coords, args.Chunk);
RaiseLocalEvent(uid, ref preEv);
if (uid != args.Chunk)
RaiseLocalEvent(args.Chunk, ref preEv);
if (preEv.Handled)
continue;
var debrisFeatureEv = new TryGetPlaceableDebrisFeatureEvent(coords, args.Chunk);
RaiseLocalEvent(uid, ref debrisFeatureEv);
if (debrisFeatureEv.DebrisProto == null)
{
// Try on the chunk...?
if (uid != args.Chunk)
RaiseLocalEvent(args.Chunk, ref debrisFeatureEv);
if (debrisFeatureEv.DebrisProto == null)
{
// Nope.
failures++;
continue;
}
}
var ent = Spawn(debrisFeatureEv.DebrisProto, coords);
component.OwnedDebris.Add(point, ent);
var owned = EnsureComp<OwnedDebrisComponent>(ent);
owned.OwningController = uid;
owned.LastKey = point;
}
if (failures > 0)
_sawmill.Error($"Failed to place {failures} debris at chunk {args.Chunk}");
}
/// <summary>
/// Checks to see if the potential spawn point is clear
/// </summary>
/// <param name="mapId"></param>
/// <param name="point"></param>
/// <returns></returns>
private bool HasCollisions(MapId mapId, Box2 point)
{
_mapGrids.Clear();
_mapManager.FindGridsIntersecting(mapId, point, ref _mapGrids);
return _mapGrids.Count > 0;
}
/// <summary>
/// Generates the points to put into a chunk using a poisson disk sampler.
/// </summary>
private List<Vector2> GeneratePointsInChunk(EntityUid chunk, float density, Vector2 coords, EntityUid map)
{
var offs = (int) ((WorldGen.ChunkSize - WorldGen.ChunkSize / 8.0f) / 2.0f);
var topLeft = new Vector2(-offs, -offs);
var lowerRight = new Vector2(offs, offs);
var enumerator = _sampler.SampleRectangle(topLeft, lowerRight, density);
var debrisPoints = new List<Vector2>();
var realCenter = WorldGen.ChunkToWorldCoordsCentered(coords.Floored());
while (enumerator.MoveNext(out var debrisPoint))
{
debrisPoints.Add(realCenter + debrisPoint.Value);
}
return debrisPoints;
}
}
/// <summary>
/// Fired directed on the debris feature placer controller and the chunk, ahead of placing a debris piece.
/// </summary>
[ByRefEvent]
[PublicAPI]
public record struct PrePlaceDebrisFeatureEvent(EntityCoordinates Coords, EntityUid Chunk, bool Handled = false);
/// <summary>
/// Fired directed on the debris feature placer controller and the chunk, to select which debris piece to place.
/// </summary>
[ByRefEvent]
[PublicAPI]
public record struct TryGetPlaceableDebrisFeatureEvent(EntityCoordinates Coords, EntityUid Chunk,
string? DebrisProto = null);