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; /// /// This handles placing debris within the world evenly with rng, primarily for structures like asteroid fields. /// 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> _mapGrids = new(); /// public override void Initialize() { _sawmill = _logManager.GetSawmill("world.debris.feature_placer"); SubscribeLocalEvent(OnChunkLoaded); SubscribeLocalEvent(OnChunkUnloaded); SubscribeLocalEvent(OnDebrisShutdown); SubscribeLocalEvent(OnDebrisMove); SubscribeLocalEvent( OnTryGetPlacableDebrisEvent); } /// /// Handles debris moving, and making sure it stays parented to a chunk for loading purposes. /// private void OnDebrisMove(EntityUid uid, OwnedDebrisComponent component, ref MoveEvent args) { if (!HasComp(component.OwningController)) return; // Redundant logic, prolly needs it's own handler for your custom system. var placer = Comp(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(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(newChunk, out var newPlacer)) { // Whelp. RemCompDeferred(uid); return; } newPlacer.OwnedDebris[_xformSys.GetWorldPosition(xform)] = uid; // Change our owner. component.OwningController = newChunk.Value; } /// /// Handles debris shutdown/detach. /// private void OnDebrisShutdown(EntityUid uid, OwnedDebrisComponent component, ComponentShutdown args) { if (!TryComp(component.OwningController, out var placer)) return; placer.OwnedDebris[component.LastKey] = null; if (Terminating(uid)) placer.OwnedDebris.Remove(component.LastKey); } /// /// Queues all debris owned by the placer for garbage collection. /// private void OnChunkUnloaded(EntityUid uid, DebrisFeaturePlacerControllerComponent component, ref WorldChunkUnloadedEvent args) { component.DoSpawns = true; } /// /// Handles providing a debris type to place for SimpleDebrisSelectorComponent. /// This randomly picks a debris type from the EntitySpawnCollectionCache. /// private void OnTryGetPlacableDebrisEvent(EntityUid uid, SimpleDebrisSelectorComponent component, ref TryGetPlaceableDebrisFeatureEvent args) { if (args.DebrisProto is not null) return; var l = new List(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]; } /// /// 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. /// 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(args.Chunk, out var chunk)) return; var chunkMap = chunk.Map; if (!TryComp(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? 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(ent); owned.OwningController = uid; owned.LastKey = point; } if (failures > 0) _sawmill.Error($"Failed to place {failures} debris at chunk {args.Chunk}"); } /// /// Checks to see if the potential spawn point is clear /// /// /// /// private bool HasCollisions(MapId mapId, Box2 point) { _mapGrids.Clear(); _mapManager.FindGridsIntersecting(mapId, point, ref _mapGrids); return _mapGrids.Count > 0; } /// /// Generates the points to put into a chunk using a poisson disk sampler. /// private List 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(); var realCenter = WorldGen.ChunkToWorldCoordsCentered(coords.Floored()); while (enumerator.MoveNext(out var debrisPoint)) { debrisPoints.Add(realCenter + debrisPoint.Value); } return debrisPoints; } } /// /// Fired directed on the debris feature placer controller and the chunk, ahead of placing a debris piece. /// [ByRefEvent] [PublicAPI] public record struct PrePlaceDebrisFeatureEvent(EntityCoordinates Coords, EntityUid Chunk, bool Handled = false); /// /// Fired directed on the debris feature placer controller and the chunk, to select which debris piece to place. /// [ByRefEvent] [PublicAPI] public record struct TryGetPlaceableDebrisFeatureEvent(EntityCoordinates Coords, EntityUid Chunk, string? DebrisProto = null);