]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Dynamic space world generation and debris. (#15120)
authorMoony <moony@hellomouse.net>
Tue, 16 May 2023 11:36:45 +0000 (06:36 -0500)
committerGitHub <noreply@github.com>
Tue, 16 May 2023 11:36:45 +0000 (06:36 -0500)
* World generation (squash)

* Test fixes.

* command

* o

* Access cleanup.

* Documentation touchups.

* Use a prototype serializer for BiomeSelectionComponent

* Struct enumerator in SimpleFloorPlanPopulatorSystem

* Safety margins around PoissonDiskSampler, cookie acquisition methodologies

* Struct enumerating PoissonDiskSampler; internal side

* Struct enumerating PoissonDiskSampler: Finish it

* Update WorldgenConfigSystem.cs

awa

---------

Co-authored-by: moonheart08 <moonheart08@users.noreply.github.com>
Co-authored-by: 20kdc <asdd2808@gmail.com>
54 files changed:
Content.Client/Entry/EntryPoint.cs
Content.IntegrationTests/PoolManager.cs
Content.IntegrationTests/Tests/CargoTest.cs
Content.IntegrationTests/Tests/EntityTest.cs
Content.IntegrationTests/Tests/PrototypeSaveTest.cs
Content.Server/IoC/ServerContentIoC.cs
Content.Server/Worldgen/Components/BiomeSelectionComponent.cs [new file with mode: 0644]
Content.Server/Worldgen/Components/Carvers/NoiseRangeCarverComponent.cs [new file with mode: 0644]
Content.Server/Worldgen/Components/Debris/BlobFloorPlanBuilderComponent.cs [new file with mode: 0644]
Content.Server/Worldgen/Components/Debris/DebrisFeaturePlacerControllerComponent.cs [new file with mode: 0644]
Content.Server/Worldgen/Components/Debris/NoiseDrivenDebrisSelectorComponent.cs [new file with mode: 0644]
Content.Server/Worldgen/Components/Debris/OwnedDebrisComponent.cs [new file with mode: 0644]
Content.Server/Worldgen/Components/Debris/SimpleDebrisSelectorComponent.cs [new file with mode: 0644]
Content.Server/Worldgen/Components/Debris/SimpleFloorPlanPopulatorComponent.cs [new file with mode: 0644]
Content.Server/Worldgen/Components/GC/GCAbleObjectComponent.cs [new file with mode: 0644]
Content.Server/Worldgen/Components/LoadedChunkComponent.cs [new file with mode: 0644]
Content.Server/Worldgen/Components/LocalityLoaderComponent.cs [new file with mode: 0644]
Content.Server/Worldgen/Components/NoiseIndexComponent.cs [new file with mode: 0644]
Content.Server/Worldgen/Components/WorldChunkComponent.cs [new file with mode: 0644]
Content.Server/Worldgen/Components/WorldControllerComponent.cs [new file with mode: 0644]
Content.Server/Worldgen/Components/WorldLoaderComponent.cs [new file with mode: 0644]
Content.Server/Worldgen/GridPointsNearEnumerator.cs [new file with mode: 0644]
Content.Server/Worldgen/Prototypes/BiomePrototype.cs [new file with mode: 0644]
Content.Server/Worldgen/Prototypes/GCQueuePrototype.cs [new file with mode: 0644]
Content.Server/Worldgen/Prototypes/NoiseChannelPrototype.cs [new file with mode: 0644]
Content.Server/Worldgen/Prototypes/WorldgenConfigPrototype.cs [new file with mode: 0644]
Content.Server/Worldgen/Systems/BaseWorldSystem.cs [new file with mode: 0644]
Content.Server/Worldgen/Systems/Biomes/BiomeSelectionSystem.cs [new file with mode: 0644]
Content.Server/Worldgen/Systems/Carvers/NoiseRangeCarverSystem.cs [new file with mode: 0644]
Content.Server/Worldgen/Systems/Debris/BlobFloorPlanBuilderSystem.cs [new file with mode: 0644]
Content.Server/Worldgen/Systems/Debris/DebrisFeaturePlacerSystem.cs [new file with mode: 0644]
Content.Server/Worldgen/Systems/Debris/NoiseDrivenDebrisSelectorSystem.cs [new file with mode: 0644]
Content.Server/Worldgen/Systems/Debris/SimpleFloorPlanPopulatorSystem.cs [new file with mode: 0644]
Content.Server/Worldgen/Systems/GC/GCQueueSystem.cs [new file with mode: 0644]
Content.Server/Worldgen/Systems/LocalityLoaderSystem.cs [new file with mode: 0644]
Content.Server/Worldgen/Systems/NoiseIndexSystem.cs [new file with mode: 0644]
Content.Server/Worldgen/Systems/WorldControllerSystem.cs [new file with mode: 0644]
Content.Server/Worldgen/Systems/WorldgenConfigSystem.cs [new file with mode: 0644]
Content.Server/Worldgen/Tools/EntitySpawnCollectionCache.cs [new file with mode: 0644]
Content.Server/Worldgen/Tools/PoissonDiskSampler.cs [new file with mode: 0644]
Content.Server/Worldgen/WorldGen.cs [new file with mode: 0644]
Content.Shared/CCVar/CCVars.cs
Resources/Locale/en-US/shell.ftl
Resources/Locale/en-US/worldgen/applyworldgenconfig.ftl [new file with mode: 0644]
Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
Resources/Prototypes/Entities/World/Debris/asteroids.yml [new file with mode: 0644]
Resources/Prototypes/Entities/World/Debris/base_debris.yml [new file with mode: 0644]
Resources/Prototypes/Entities/World/Debris/wrecks.yml [new file with mode: 0644]
Resources/Prototypes/Entities/World/chunk.yml [new file with mode: 0644]
Resources/Prototypes/GC/world.yml [new file with mode: 0644]
Resources/Prototypes/World/Biomes/basic.yml [new file with mode: 0644]
Resources/Prototypes/World/Biomes/failsafes.yml [new file with mode: 0644]
Resources/Prototypes/World/noise_channels.yml [new file with mode: 0644]
Resources/Prototypes/World/worldgen_default.yml [new file with mode: 0644]

index 3a1f2e1cd3308f60d29ec27c57188a4c147548e4..4b30700524d1507f134129ded1783cd003ae5190 100644 (file)
@@ -111,6 +111,10 @@ namespace Content.Client.Entry
             _prototypeManager.RegisterIgnore("salvageMap");
             _prototypeManager.RegisterIgnore("salvageFaction");
             _prototypeManager.RegisterIgnore("gamePreset");
+            _prototypeManager.RegisterIgnore("noiseChannel");
+            _prototypeManager.RegisterIgnore("spaceBiome");
+            _prototypeManager.RegisterIgnore("worldgenConfig");
+            _prototypeManager.RegisterIgnore("gcQueue");
             _prototypeManager.RegisterIgnore("gameRule");
             _prototypeManager.RegisterIgnore("worldSpell");
             _prototypeManager.RegisterIgnore("entitySpell");
index 7c7f442ef510168fea877958f6adda72d732359a..9c25c1541780065f1e0aca28b81effba9af4eeab 100644 (file)
@@ -57,6 +57,7 @@ public static class PoolManager
         (CCVars.ArrivalsShuttles.Name,        "false"),
         (CCVars.EmergencyShuttleEnabled.Name, "false"),
         (CCVars.ProcgenPreload.Name,          "false"),
+        (CCVars.WorldgenEnabled.Name,         "false"),
         // @formatter:on
     };
 
index 06fc46c3aed81194eafbf493d992c24b6c435ed7..617e89b7d2579baadd21616c6886aa2692548812 100644 (file)
@@ -74,6 +74,7 @@ public sealed class CargoTest
 
             var protoIds = protoManager.EnumeratePrototypes<EntityPrototype>()
                 .Where(p=>!p.Abstract)
+                .Where(p => !p.Components.ContainsKey("MapGrid")) // Grids are not for sale.
                 .Select(p => p.ID)
                 .ToList();
 
index 0c095b26147cdd550385b7ebae4b7272f59df065..27adeeef4bf1d06d421ceaeb81c491f33990761e 100644 (file)
@@ -39,6 +39,7 @@ namespace Content.IntegrationTests.Tests
                 var protoIds = prototypeMan
                     .EnumeratePrototypes<EntityPrototype>()
                     .Where(p=>!p.Abstract)
+                    .Where(p => !p.Components.ContainsKey("MapGrid")) // This will smash stuff otherwise.
                     .Select(p => p.ID)
                     .ToList();
                 foreach (var protoId in protoIds)
@@ -87,6 +88,7 @@ namespace Content.IntegrationTests.Tests
                 var protoIds = prototypeMan
                     .EnumeratePrototypes<EntityPrototype>()
                     .Where(p=>!p.Abstract)
+                    .Where(p => !p.Components.ContainsKey("MapGrid")) // This will smash stuff otherwise.
                     .Select(p => p.ID)
                     .ToList();
                 foreach (var protoId in protoIds)
@@ -133,6 +135,7 @@ namespace Content.IntegrationTests.Tests
             var protoIds = prototypeMan
                 .EnumeratePrototypes<EntityPrototype>()
                 .Where(p => !p.Abstract)
+                .Where(p => !p.Components.ContainsKey("MapGrid")) // This will smash stuff otherwise.
                 .Select(p => p.ID)
                 .ToList();
 
@@ -193,6 +196,10 @@ namespace Content.IntegrationTests.Tests
                 "MapGrid",
                 "StationData", // errors when removed mid-round
                 "Actor", // We aren't testing actor components, those need their player session set.
+                "BlobFloorPlanBuilder", // Implodes if unconfigured.
+                "DebrisFeaturePlacerController", // Above.
+                "LoadedChunk", // Worldgen chunk loading malding.
+                "BiomeSelection", // Whaddya know, requires config.
             };
 
             var testEntity = @"
@@ -289,6 +296,10 @@ namespace Content.IntegrationTests.Tests
                 "MapGrid",
                 "StationData", // errors when deleted mid-round
                 "Actor", // We aren't testing actor components, those need their player session set.
+                "BlobFloorPlanBuilder", // Implodes if unconfigured.
+                "DebrisFeaturePlacerController", // Above.
+                "LoadedChunk", // Worldgen chunk loading malding.
+                "BiomeSelection", // Whaddya know, requires config.
             };
 
             var testEntity = @"
index 7881b624e93ad9603edb4afed038a7994dcbd729..cf87ae84f5432aed95ad99af11d4c5fc71d06e26 100644 (file)
@@ -86,6 +86,10 @@ public sealed class PrototypeSaveTest
             if (prototype.Abstract)
                 continue;
 
+            // Yea this test just doesn't work with this, it parents a grid to another grid and causes game logic to explode.
+            if (prototype.Components.ContainsKey("MapGrid"))
+                continue;
+
             // Currently mobs and such can't be serialized, but they aren't flagged as serializable anyways.
             if (!prototype.MapSavable)
                 continue;
index 675fedc807b96fd6421d4282a67a8adf46e3b8fd..106c995d44f49caf933fef1d0ea780dfa54052ba 100644 (file)
@@ -19,6 +19,7 @@ using Content.Server.Preferences.Managers;
 using Content.Server.ServerInfo;
 using Content.Server.ServerUpdates;
 using Content.Server.Voting.Managers;
+using Content.Server.Worldgen.Tools;
 using Content.Shared.Administration;
 using Content.Shared.Administration.Logs;
 using Content.Shared.Administration.Managers;
@@ -58,6 +59,7 @@ namespace Content.Server.IoC
             IoCManager.Register<PlayTimeTrackingManager>();
             IoCManager.Register<UserDbDataManager>();
             IoCManager.Register<ServerInfoManager>();
+            IoCManager.Register<PoissonDiskSampler>();
         }
     }
 }
diff --git a/Content.Server/Worldgen/Components/BiomeSelectionComponent.cs b/Content.Server/Worldgen/Components/BiomeSelectionComponent.cs
new file mode 100644 (file)
index 0000000..b82b8eb
--- /dev/null
@@ -0,0 +1,21 @@
+using Content.Server.Worldgen.Systems.Biomes;
+using Content.Server.Worldgen.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
+
+namespace Content.Server.Worldgen.Components;
+
+/// <summary>
+///     This is used for selecting the biome(s) to be used during world generation.
+/// </summary>
+[RegisterComponent]
+[Access(typeof(BiomeSelectionSystem))]
+public sealed class BiomeSelectionComponent : Component
+{
+    /// <summary>
+    ///     The list of biomes available to this selector.
+    /// </summary>
+    /// <remarks>This is always sorted by priority after ComponentStartup.</remarks>
+    [DataField("biomes", required: true,
+        customTypeSerializer: typeof(PrototypeIdListSerializer<BiomePrototype>))] public List<string> Biomes = new();
+}
+
diff --git a/Content.Server/Worldgen/Components/Carvers/NoiseRangeCarverComponent.cs b/Content.Server/Worldgen/Components/Carvers/NoiseRangeCarverComponent.cs
new file mode 100644 (file)
index 0000000..164f8ff
--- /dev/null
@@ -0,0 +1,27 @@
+using Content.Server.Worldgen.Prototypes;
+using Content.Server.Worldgen.Systems.Carvers;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Server.Worldgen.Components.Carvers;
+
+/// <summary>
+///     This is used for carving out empty space in the game world, providing byways through the debris field.
+/// </summary>
+[RegisterComponent]
+[Access(typeof(NoiseRangeCarverSystem))]
+public sealed class NoiseRangeCarverComponent : Component
+{
+    /// <summary>
+    ///     The noise channel to use as a density controller.
+    /// </summary>
+    /// <remarks>This noise channel should be mapped to exactly the range [0, 1] unless you want a lot of warnings in the log.</remarks>
+    [DataField("noiseChannel", customTypeSerializer: typeof(PrototypeIdSerializer<NoiseChannelPrototype>))]
+    public string NoiseChannel { get; } = default!;
+
+    /// <summary>
+    ///     The index of ranges in which to cut debris generation.
+    /// </summary>
+    [DataField("ranges", required: true)]
+    public List<Vector2> Ranges { get; } = default!;
+}
+
diff --git a/Content.Server/Worldgen/Components/Debris/BlobFloorPlanBuilderComponent.cs b/Content.Server/Worldgen/Components/Debris/BlobFloorPlanBuilderComponent.cs
new file mode 100644 (file)
index 0000000..70861bf
--- /dev/null
@@ -0,0 +1,37 @@
+using Content.Server.Worldgen.Systems.Debris;
+using Content.Shared.Maps;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
+
+namespace Content.Server.Worldgen.Components.Debris;
+
+/// <summary>
+///     This is used for constructing asteroid debris.
+/// </summary>
+[RegisterComponent]
+[Access(typeof(BlobFloorPlanBuilderSystem))]
+public sealed class BlobFloorPlanBuilderComponent : Component
+{
+    /// <summary>
+    ///     The probability that placing a floor tile will add up to three-four neighboring tiles as well.
+    /// </summary>
+    [DataField("blobDrawProb")] public float BlobDrawProb;
+
+    /// <summary>
+    ///     The maximum radius for the structure.
+    /// </summary>
+    [DataField("radius", required: true)] public float Radius;
+
+    /// <summary>
+    ///     The tiles to be used for the floor plan.
+    /// </summary>
+    [DataField("floorTileset", required: true,
+        customTypeSerializer: typeof(PrototypeIdListSerializer<ContentTileDefinition>))]
+    public List<string> FloorTileset { get; } = default!;
+
+    /// <summary>
+    ///     The number of floor tiles to place when drawing the asteroid layout.
+    /// </summary>
+    [DataField("floorPlacements", required: true)]
+    public int FloorPlacements { get; }
+}
+
diff --git a/Content.Server/Worldgen/Components/Debris/DebrisFeaturePlacerControllerComponent.cs b/Content.Server/Worldgen/Components/Debris/DebrisFeaturePlacerControllerComponent.cs
new file mode 100644 (file)
index 0000000..1d59960
--- /dev/null
@@ -0,0 +1,42 @@
+using Content.Server.Worldgen.Prototypes;
+using Content.Server.Worldgen.Systems.Debris;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Server.Worldgen.Components.Debris;
+
+/// <summary>
+///     This is used for controlling the debris feature placer.
+/// </summary>
+[RegisterComponent]
+[Access(typeof(DebrisFeaturePlacerSystem))]
+public sealed class DebrisFeaturePlacerControllerComponent : Component
+{
+    /// <summary>
+    ///     Whether or not to clip debris that would spawn at a location that has a density of zero.
+    /// </summary>
+    [DataField("densityClip")] public bool DensityClip = true;
+
+    /// <summary>
+    ///     Whether or not entities are already spawned.
+    /// </summary>
+    public bool DoSpawns = true;
+
+    [DataField("ownedDebris")] public Dictionary<Vector2, EntityUid?> OwnedDebris = new();
+
+    /// <summary>
+    ///     The chance spawning a piece of debris will just be cancelled randomly.
+    /// </summary>
+    [DataField("randomCancelChance")] public float RandomCancellationChance = 0.1f;
+
+    /// <summary>
+    ///     Radius in which there should be no objects for debris to spawn.
+    /// </summary>
+    [DataField("safetyZoneRadius")] public float SafetyZoneRadius = 16.0f;
+
+    /// <summary>
+    ///     The noise channel to use as a density controller.
+    /// </summary>
+    [DataField("densityNoiseChannel", customTypeSerializer: typeof(PrototypeIdSerializer<NoiseChannelPrototype>))]
+    public string DensityNoiseChannel { get; } = default!;
+}
+
diff --git a/Content.Server/Worldgen/Components/Debris/NoiseDrivenDebrisSelectorComponent.cs b/Content.Server/Worldgen/Components/Debris/NoiseDrivenDebrisSelectorComponent.cs
new file mode 100644 (file)
index 0000000..9690bc5
--- /dev/null
@@ -0,0 +1,44 @@
+using Content.Server.Worldgen.Prototypes;
+using Content.Server.Worldgen.Systems.Debris;
+using Content.Server.Worldgen.Tools;
+using Content.Shared.Storage;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Server.Worldgen.Components.Debris;
+
+/// <summary>
+///     This is used for selecting debris with a probability determined by a noise channel.
+///     Takes priority over SimpleDebrisSelectorComponent and should likely be used in combination.
+/// </summary>
+[RegisterComponent]
+[Access(typeof(NoiseDrivenDebrisSelectorSystem))]
+public sealed class NoiseDrivenDebrisSelectorComponent : Component
+{
+    private EntitySpawnCollectionCache? _cache;
+
+    /// <summary>
+    ///     The prototype-facing debris table entries.
+    /// </summary>
+    [DataField("debrisTable", required: true)]
+    private List<EntitySpawnEntry> _entries = default!;
+
+    /// <summary>
+    ///     The debris entity spawn collection.
+    /// </summary>
+    public EntitySpawnCollectionCache CachedDebrisTable
+    {
+        get
+        {
+            _cache ??= new EntitySpawnCollectionCache(_entries);
+            return _cache;
+        }
+    }
+
+    /// <summary>
+    ///     The noise channel to use as a density controller.
+    /// </summary>
+    /// <remarks>This noise channel should be mapped to exactly the range [0, 1] unless you want a lot of warnings in the log.</remarks>
+    [DataField("noiseChannel", customTypeSerializer: typeof(PrototypeIdSerializer<NoiseChannelPrototype>))]
+    public string NoiseChannel { get; } = default!;
+}
+
diff --git a/Content.Server/Worldgen/Components/Debris/OwnedDebrisComponent.cs b/Content.Server/Worldgen/Components/Debris/OwnedDebrisComponent.cs
new file mode 100644 (file)
index 0000000..73a6cf8
--- /dev/null
@@ -0,0 +1,23 @@
+using Content.Server.Worldgen.Systems.Debris;
+
+namespace Content.Server.Worldgen.Components.Debris;
+
+/// <summary>
+///     This is used for attaching a piece of debris to it's owning controller.
+///     Mostly just syncs deletion.
+/// </summary>
+[RegisterComponent]
+[Access(typeof(DebrisFeaturePlacerSystem))]
+public sealed class OwnedDebrisComponent : Component
+{
+    /// <summary>
+    ///     The last location in the controller's internal structure for this debris.
+    /// </summary>
+    [DataField("lastKey")] public Vector2 LastKey;
+
+    /// <summary>
+    ///     The DebrisFeaturePlacerController-having entity that owns this.
+    /// </summary>
+    [DataField("owningController")] public EntityUid OwningController;
+}
+
diff --git a/Content.Server/Worldgen/Components/Debris/SimpleDebrisSelectorComponent.cs b/Content.Server/Worldgen/Components/Debris/SimpleDebrisSelectorComponent.cs
new file mode 100644 (file)
index 0000000..16b9f39
--- /dev/null
@@ -0,0 +1,34 @@
+using Content.Server.Worldgen.Systems.Debris;
+using Content.Server.Worldgen.Tools;
+using Content.Shared.Storage;
+
+namespace Content.Server.Worldgen.Components.Debris;
+
+/// <summary>
+///     This is used for a very simple debris selection for simple biomes. Just uses a spawn table.
+/// </summary>
+[RegisterComponent]
+[Access(typeof(DebrisFeaturePlacerSystem))]
+public sealed class SimpleDebrisSelectorComponent : Component
+{
+    private EntitySpawnCollectionCache? _cache;
+
+    /// <summary>
+    ///     The prototype-facing debris table entries.
+    /// </summary>
+    [DataField("debrisTable", required: true)]
+    private List<EntitySpawnEntry> _entries = default!;
+
+    /// <summary>
+    ///     The debris entity spawn collection.
+    /// </summary>
+    public EntitySpawnCollectionCache CachedDebrisTable
+    {
+        get
+        {
+            _cache ??= new EntitySpawnCollectionCache(_entries);
+            return _cache;
+        }
+    }
+}
+
diff --git a/Content.Server/Worldgen/Components/Debris/SimpleFloorPlanPopulatorComponent.cs b/Content.Server/Worldgen/Components/Debris/SimpleFloorPlanPopulatorComponent.cs
new file mode 100644 (file)
index 0000000..eab15c8
--- /dev/null
@@ -0,0 +1,47 @@
+using System.Linq;
+using Content.Server.Worldgen.Systems.Debris;
+using Content.Server.Worldgen.Tools;
+using Content.Shared.Maps;
+using Content.Shared.Storage;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
+
+namespace Content.Server.Worldgen.Components.Debris;
+
+/// <summary>
+///     This is used for populating a grid with random entities automatically.
+/// </summary>
+[RegisterComponent]
+[Access(typeof(SimpleFloorPlanPopulatorSystem))]
+public sealed class SimpleFloorPlanPopulatorComponent : Component
+{
+    private Dictionary<string, EntitySpawnCollectionCache>? _caches;
+
+    /// <summary>
+    ///     The prototype facing floor plan populator entries.
+    /// </summary>
+    [DataField("entries", required: true,
+        customTypeSerializer: typeof(PrototypeIdDictionarySerializer<List<EntitySpawnEntry>, ContentTileDefinition>))]
+    private Dictionary<string, List<EntitySpawnEntry>> _entries = default!;
+
+    /// <summary>
+    ///     The spawn collections used to place entities on different tile types.
+    /// </summary>
+    [ViewVariables]
+    public Dictionary<string, EntitySpawnCollectionCache> Caches
+    {
+        get
+        {
+            if (_caches is null)
+            {
+                _caches = _entries
+                    .Select(x =>
+                        new KeyValuePair<string, EntitySpawnCollectionCache>(x.Key,
+                            new EntitySpawnCollectionCache(x.Value)))
+                    .ToDictionary(x => x.Key, x => x.Value);
+            }
+
+            return _caches;
+        }
+    }
+}
+
diff --git a/Content.Server/Worldgen/Components/GC/GCAbleObjectComponent.cs b/Content.Server/Worldgen/Components/GC/GCAbleObjectComponent.cs
new file mode 100644 (file)
index 0000000..3957c7d
--- /dev/null
@@ -0,0 +1,21 @@
+using Content.Server.Worldgen.Prototypes;
+using Content.Server.Worldgen.Systems.GC;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Server.Worldgen.Components.GC;
+
+/// <summary>
+///     This is used for whether or not a GCable object is "dirty". Firing GCDirtyEvent on the object is the correct way to
+///     set this up.
+/// </summary>
+[RegisterComponent]
+[Access(typeof(GCQueueSystem))]
+public sealed class GCAbleObjectComponent : Component
+{
+    /// <summary>
+    ///     Which queue to insert this object into when GCing
+    /// </summary>
+    [DataField("queue", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<GCQueuePrototype>))]
+    public string Queue = default!;
+}
+
diff --git a/Content.Server/Worldgen/Components/LoadedChunkComponent.cs b/Content.Server/Worldgen/Components/LoadedChunkComponent.cs
new file mode 100644 (file)
index 0000000..cebabae
--- /dev/null
@@ -0,0 +1,17 @@
+using Content.Server.Worldgen.Systems;
+
+namespace Content.Server.Worldgen.Components;
+
+/// <summary>
+///     This is used for marking a chunk as loaded.
+/// </summary>
+[RegisterComponent]
+[Access(typeof(WorldControllerSystem))]
+public sealed class LoadedChunkComponent : Component
+{
+    /// <summary>
+    ///     The current list of entities loading this chunk.
+    /// </summary>
+    [ViewVariables] public List<EntityUid>? Loaders = null;
+}
+
diff --git a/Content.Server/Worldgen/Components/LocalityLoaderComponent.cs b/Content.Server/Worldgen/Components/LocalityLoaderComponent.cs
new file mode 100644 (file)
index 0000000..e0b0eb2
--- /dev/null
@@ -0,0 +1,19 @@
+using Content.Server.Worldgen.Systems;
+
+namespace Content.Server.Worldgen.Components;
+
+/// <summary>
+///     This is used for sending a signal to the entity it's on to load contents whenever a loader gets close enough.
+///     Does not support unloading.
+/// </summary>
+[RegisterComponent]
+[Access(typeof(LocalityLoaderSystem))]
+public sealed class LocalityLoaderComponent : Component
+{
+    /// <summary>
+    ///     The maximum distance an entity can be from the loader for it to not load.
+    ///     Once a loader is closer than this, the event is fired and this component removed.
+    /// </summary>
+    [DataField("loadingDistance")] public int LoadingDistance = 32;
+}
+
diff --git a/Content.Server/Worldgen/Components/NoiseIndexComponent.cs b/Content.Server/Worldgen/Components/NoiseIndexComponent.cs
new file mode 100644 (file)
index 0000000..ed6d44f
--- /dev/null
@@ -0,0 +1,20 @@
+using Content.Server.Worldgen.Prototypes;
+using Content.Server.Worldgen.Systems;
+
+namespace Content.Server.Worldgen.Components;
+
+/// <summary>
+///     This is used for containing configured noise generators.
+/// </summary>
+[RegisterComponent]
+[Access(typeof(NoiseIndexSystem))]
+public sealed class NoiseIndexComponent : Component
+{
+    /// <summary>
+    ///     An index of generators, to avoid having to recreate them every time a noise channel is used.
+    ///     Keyed by noise generator prototype ID.
+    /// </summary>
+    [Access(typeof(NoiseIndexSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.None)]
+    public Dictionary<string, NoiseGenerator> Generators { get; } = new();
+}
+
diff --git a/Content.Server/Worldgen/Components/WorldChunkComponent.cs b/Content.Server/Worldgen/Components/WorldChunkComponent.cs
new file mode 100644 (file)
index 0000000..4b5e294
--- /dev/null
@@ -0,0 +1,22 @@
+using Content.Server.Worldgen.Systems;
+
+namespace Content.Server.Worldgen.Components;
+
+/// <summary>
+///     This is used for marking an entity as being a world chunk.
+/// </summary>
+[RegisterComponent]
+[Access(typeof(WorldControllerSystem))]
+public sealed class WorldChunkComponent : Component
+{
+    /// <summary>
+    ///     The coordinates of the chunk, in chunk space.
+    /// </summary>
+    [DataField("coordinates")] public Vector2i Coordinates;
+
+    /// <summary>
+    ///     The map this chunk belongs to.
+    /// </summary>
+    [DataField("map")] public EntityUid Map;
+}
+
diff --git a/Content.Server/Worldgen/Components/WorldControllerComponent.cs b/Content.Server/Worldgen/Components/WorldControllerComponent.cs
new file mode 100644 (file)
index 0000000..9d1bcc7
--- /dev/null
@@ -0,0 +1,25 @@
+using Content.Server.Worldgen.Systems;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Server.Worldgen.Components;
+
+/// <summary>
+///     This is used for controlling overall world loading, containing an index of all chunks in the map.
+/// </summary>
+[RegisterComponent]
+[Access(typeof(WorldControllerSystem))]
+public sealed class WorldControllerComponent : Component
+{
+    /// <summary>
+    ///     The prototype to use for chunks on this world map.
+    /// </summary>
+    [DataField("chunkProto", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
+    public string ChunkProto = "WorldChunk";
+
+    /// <summary>
+    ///     An index of chunks owned by the controller.
+    /// </summary>
+    [DataField("chunks")] public Dictionary<Vector2i, EntityUid> Chunks = new();
+}
+
diff --git a/Content.Server/Worldgen/Components/WorldLoaderComponent.cs b/Content.Server/Worldgen/Components/WorldLoaderComponent.cs
new file mode 100644 (file)
index 0000000..43c990c
--- /dev/null
@@ -0,0 +1,18 @@
+using Content.Server.Worldgen.Systems;
+
+namespace Content.Server.Worldgen.Components;
+
+/// <summary>
+///     This is used for allowing some objects to load the game world.
+/// </summary>
+[RegisterComponent]
+[Access(typeof(WorldControllerSystem))]
+public sealed class WorldLoaderComponent : Component
+{
+    /// <summary>
+    ///     The radius in which the loader loads the world.
+    /// </summary>
+    [ViewVariables(VVAccess.ReadWrite)] [DataField("radius")]
+    public int Radius = 128;
+}
+
diff --git a/Content.Server/Worldgen/GridPointsNearEnumerator.cs b/Content.Server/Worldgen/GridPointsNearEnumerator.cs
new file mode 100644 (file)
index 0000000..24b7106
--- /dev/null
@@ -0,0 +1,59 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+
+namespace Content.Server.Worldgen;
+
+/// <summary>
+///     A struct enumerator of points on a grid within the given radius.
+/// </summary>
+public struct GridPointsNearEnumerator
+{
+    private readonly int _radius;
+    private readonly Vector2i _center;
+    private int _x;
+    private int _y;
+
+    /// <summary>
+    ///     Initializes a new enumerator with the given center and radius.
+    /// </summary>
+    public GridPointsNearEnumerator(Vector2i center, int radius)
+    {
+        _radius = radius;
+        _center = center;
+        _x = -_radius;
+        _y = -_radius;
+    }
+
+    /// <summary>
+    ///     Gets the next point in the enumeration.
+    /// </summary>
+    /// <param name="chunk">The computed point, if any</param>
+    /// <returns>Success</returns>
+    [Pure]
+    public bool MoveNext([NotNullWhen(true)] out Vector2i? chunk)
+    {
+        while (!(_x * _x + _y * _y <= _radius * _radius))
+        {
+            if (_y > _radius)
+            {
+                chunk = null;
+                return false;
+            }
+
+            if (_x > _radius)
+            {
+                _x = -_radius;
+                _y++;
+            }
+            else
+            {
+                _x++;
+            }
+        }
+
+        chunk = _center + new Vector2i(_x, _y);
+        _x++;
+        return true;
+    }
+}
+
diff --git a/Content.Server/Worldgen/Prototypes/BiomePrototype.cs b/Content.Server/Worldgen/Prototypes/BiomePrototype.cs
new file mode 100644 (file)
index 0000000..b76d454
--- /dev/null
@@ -0,0 +1,61 @@
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.Manager;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
+
+namespace Content.Server.Worldgen.Prototypes;
+
+/// <summary>
+///     This is a prototype for biome selection, allowing the component list of a chunk to be amended based on the output
+///     of noise channels at that location.
+/// </summary>
+[Prototype("spaceBiome")]
+public sealed class BiomePrototype : IPrototype, IInheritingPrototype
+{
+    /// <inheritdoc />
+    [ParentDataField(typeof(AbstractPrototypeIdArraySerializer<EntityPrototype>))]
+    public string[]? Parents { get; }
+
+    /// <inheritdoc />
+    [NeverPushInheritance]
+    [AbstractDataField]
+    public bool Abstract { get; }
+
+    /// <inheritdoc />
+    [IdDataField]
+    public string ID { get; } = default!;
+
+    /// <summary>
+    ///     The valid ranges of noise values under which this biome can be picked.
+    /// </summary>
+    [DataField("noiseRanges", required: true)]
+    public Dictionary<string, List<Vector2>> NoiseRanges = default!;
+
+    /// <summary>
+    ///     Higher priority biomes get picked before lower priority ones.
+    /// </summary>
+    [DataField("priority", required: true)]
+    public int Priority { get; }
+
+    /// <summary>
+    ///     The components that get added to the target map.
+    /// </summary>
+    [DataField("chunkComponents")]
+    [AlwaysPushInheritance]
+    public EntityPrototype.ComponentRegistry ChunkComponents { get; } = new();
+
+    //TODO: Get someone to make this a method on componentregistry that does it Correctly.
+    /// <summary>
+    ///     Applies the worldgen config to the given target (presumably a map.)
+    /// </summary>
+    public void Apply(EntityUid target, ISerializationManager serialization, IEntityManager entityManager)
+    {
+        // Add all components required by the prototype. Engine update for this whenst.
+        foreach (var data in ChunkComponents.Values)
+        {
+            var comp = (Component) serialization.CreateCopy(data.Component, notNullableOverride: true);
+            comp.Owner = target; // look im sorry ok this .owner has to live until engine api exists
+            entityManager.AddComponent(target, comp);
+        }
+    }
+}
+
diff --git a/Content.Server/Worldgen/Prototypes/GCQueuePrototype.cs b/Content.Server/Worldgen/Prototypes/GCQueuePrototype.cs
new file mode 100644 (file)
index 0000000..94e6cf5
--- /dev/null
@@ -0,0 +1,41 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Worldgen.Prototypes;
+
+/// <summary>
+///     This is a prototype for a GC queue.
+/// </summary>
+[Prototype("gcQueue")]
+public sealed class GCQueuePrototype : IPrototype
+{
+    /// <inheritdoc />
+    [IdDataField]
+    public string ID { get; } = default!;
+
+    /// <summary>
+    ///     How deep the GC queue is at most. If this value is ever exceeded entities get processed automatically regardless of
+    ///     tick-time cap.
+    /// </summary>
+    [DataField("depth", required: true)]
+    public int Depth { get; }
+
+    /// <summary>
+    ///     The maximum amount of time that can be spent processing this queue.
+    /// </summary>
+    [DataField("maximumTickTime")]
+    public TimeSpan MaximumTickTime { get; } = TimeSpan.FromMilliseconds(1);
+
+    /// <summary>
+    ///     The minimum depth before entities in the queue actually get processed for deletion.
+    /// </summary>
+    [DataField("minDepthToProcess", required: true)]
+    public int MinDepthToProcess { get; }
+
+    /// <summary>
+    ///     Whether or not the GC should fire an event on the entity to see if it's eligible to skip the queue.
+    ///     Useful for making it so only objects a player has actually interacted with get put in the collection queue.
+    /// </summary>
+    [DataField("trySkipQueue")]
+    public bool TrySkipQueue { get; }
+}
+
diff --git a/Content.Server/Worldgen/Prototypes/NoiseChannelPrototype.cs b/Content.Server/Worldgen/Prototypes/NoiseChannelPrototype.cs
new file mode 100644 (file)
index 0000000..0d1ff77
--- /dev/null
@@ -0,0 +1,169 @@
+using Robust.Shared.Noise;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
+
+namespace Content.Server.Worldgen.Prototypes;
+
+/// <summary>
+///     This is a config for noise channels, used by worldgen.
+/// </summary>
+[Virtual]
+public class NoiseChannelConfig
+{
+    /// <summary>
+    ///     The noise type used by the noise generator.
+    /// </summary>
+    [DataField("noiseType")]
+    public FastNoiseLite.NoiseType NoiseType { get; } = FastNoiseLite.NoiseType.Cellular;
+
+    /// <summary>
+    ///     The fractal type used by the noise generator.
+    /// </summary>
+    [DataField("fractalType")]
+    public FastNoiseLite.FractalType FractalType { get; } = FastNoiseLite.FractalType.FBm;
+
+    /// <summary>
+    ///     Multiplied by pi in code when used.
+    /// </summary>
+    [DataField("fractalLacunarityByPi")]
+    public float FractalLacunarityByPi { get; } = 2.0f / 3.0f;
+
+    /// <summary>
+    ///     Ranges of values that get clamped down to the "clipped" value.
+    /// </summary>
+    [DataField("clippingRanges")]
+    public List<Vector2> ClippingRanges { get; } = new();
+
+    /// <summary>
+    ///     The value clipped chunks are set to.
+    /// </summary>
+    [DataField("clippedValue")]
+    public float ClippedValue { get; }
+
+    /// <summary>
+    ///     A value the output is multiplied by.
+    /// </summary>
+    [DataField("outputMultiplier")]
+    public float OutputMultiplier { get; } = 1.0f;
+
+    /// <summary>
+    ///     A value the input is multiplied by.
+    /// </summary>
+    [DataField("inputMultiplier")]
+    public float InputMultiplier { get; } = 1.0f;
+
+    /// <summary>
+    ///     Remaps the output of the noise function from the range (-1, 1) to (0, 1). This is done before all other output
+    ///     transformations.
+    /// </summary>
+    [DataField("remapTo0Through1")]
+    public bool RemapTo0Through1 { get; }
+
+    /// <summary>
+    ///     For when the transformation you need is too complex to describe in YAML.
+    /// </summary>
+    [DataField("noisePostProcess")]
+    public NoisePostProcess? NoisePostProcess { get; }
+
+    /// <summary>
+    ///     For when you need a complex transformation of the input coordinates.
+    /// </summary>
+    [DataField("noiseCoordinateProcess")]
+    public NoiseCoordinateProcess? NoiseCoordinateProcess { get; }
+
+    /// <summary>
+    ///     The "center" of the range of values. Or the minimum if mapped 0 through 1.
+    /// </summary>
+    [DataField("minimum")]
+    public float Minimum { get; }
+}
+
+[Prototype("noiseChannel")]
+public sealed class NoiseChannelPrototype : NoiseChannelConfig, IPrototype, IInheritingPrototype
+{
+    /// <inheritdoc />
+    [ParentDataField(typeof(AbstractPrototypeIdArraySerializer<EntityPrototype>))]
+    public string[]? Parents { get; }
+
+    /// <inheritdoc />
+    [NeverPushInheritance]
+    [AbstractDataField]
+    public bool Abstract { get; }
+
+    /// <inheritdoc />
+    [IdDataField]
+    public string ID { get; } = default!;
+}
+
+/// <summary>
+///     A wrapper around FastNoise's noise generation, using noise channel configs.
+/// </summary>
+public struct NoiseGenerator
+{
+    private readonly NoiseChannelConfig _config;
+    private readonly FastNoiseLite _noise;
+
+    /// <summary>
+    ///     Produces a new noise generator from the given channel config and rng seed.
+    /// </summary>
+    public NoiseGenerator(NoiseChannelConfig config, int seed)
+    {
+        _config = config;
+        _noise = new FastNoiseLite();
+        _noise.SetSeed(seed);
+        _noise.SetNoiseType(_config.NoiseType);
+        _noise.SetFractalType(_config.FractalType);
+        _noise.SetFractalLacunarity(_config.FractalLacunarityByPi * MathF.PI);
+    }
+
+    /// <summary>
+    ///     Evaluates the noise generator at the provided coordinates.
+    /// </summary>
+    /// <param name="coords">Coordinates to use as input</param>
+    /// <returns>Computed noise value</returns>
+    public float Evaluate(Vector2 coords)
+    {
+        var finCoords = coords * _config.InputMultiplier;
+
+        if (_config.NoiseCoordinateProcess is not null)
+            finCoords = _config.NoiseCoordinateProcess.Process(finCoords);
+
+        var value = _noise.GetNoise(finCoords.X, finCoords.Y);
+
+        if (_config.RemapTo0Through1)
+            value = (value + 1.0f) / 2.0f;
+
+        foreach (var range in _config.ClippingRanges)
+        {
+            if (range.X < value && value < range.Y)
+            {
+                value = _config.ClippedValue;
+                break;
+            }
+        }
+
+        if (_config.NoisePostProcess is not null)
+            value = _config.NoisePostProcess.Process(value);
+        value *= _config.OutputMultiplier;
+        return value + _config.Minimum;
+    }
+}
+
+/// <summary>
+///     A processing class that adjusts the input coordinate space to a noise channel.
+/// </summary>
+[ImplicitDataDefinitionForInheritors]
+public abstract class NoiseCoordinateProcess
+{
+    public abstract Vector2 Process(Vector2 inp);
+}
+
+/// <summary>
+///     A processing class that adjusts the final result of the noise channel.
+/// </summary>
+[ImplicitDataDefinitionForInheritors]
+public abstract class NoisePostProcess
+{
+    public abstract float Process(float inp);
+}
+
diff --git a/Content.Server/Worldgen/Prototypes/WorldgenConfigPrototype.cs b/Content.Server/Worldgen/Prototypes/WorldgenConfigPrototype.cs
new file mode 100644 (file)
index 0000000..b2c1230
--- /dev/null
@@ -0,0 +1,38 @@
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.Manager;
+
+namespace Content.Server.Worldgen.Prototypes;
+
+/// <summary>
+///     This is a prototype for controlling overall world generation.
+///     The components included are applied to the map that world generation is configured on.
+/// </summary>
+[Prototype("worldgenConfig")]
+public sealed class WorldgenConfigPrototype : IPrototype
+{
+    /// <inheritdoc />
+    [IdDataField]
+    public string ID { get; } = default!;
+
+    /// <summary>
+    ///     The components that get added to the target map.
+    /// </summary>
+    [DataField("components", required: true)]
+    public EntityPrototype.ComponentRegistry Components { get; } = default!;
+
+    //TODO: Get someone to make this a method on componentregistry that does it Correctly.
+    /// <summary>
+    ///     Applies the worldgen config to the given target (presumably a map.)
+    /// </summary>
+    public void Apply(EntityUid target, ISerializationManager serialization, IEntityManager entityManager)
+    {
+        // Add all components required by the prototype. Engine update for this whenst.
+        foreach (var data in Components.Values)
+        {
+            var comp = (Component) serialization.CreateCopy(data.Component, notNullableOverride: true);
+            comp.Owner = target; // look im sorry ok this .owner has to live until engine api exists
+            entityManager.AddComponent(target, comp);
+        }
+    }
+}
+
diff --git a/Content.Server/Worldgen/Systems/BaseWorldSystem.cs b/Content.Server/Worldgen/Systems/BaseWorldSystem.cs
new file mode 100644 (file)
index 0000000..ea6a5ea
--- /dev/null
@@ -0,0 +1,58 @@
+using Content.Server.Worldgen.Components;
+using JetBrains.Annotations;
+
+namespace Content.Server.Worldgen.Systems;
+
+/// <summary>
+///     This provides some additional functions for world generation systems.
+///     Exists primarily for convenience and to avoid code duplication.
+/// </summary>
+[PublicAPI]
+public abstract class BaseWorldSystem : EntitySystem
+{
+    [Dependency] private readonly WorldControllerSystem _worldController = default!;
+
+    /// <summary>
+    ///     Gets a chunk's coordinates in chunk space as an integer value.
+    /// </summary>
+    /// <param name="ent"></param>
+    /// <param name="xform"></param>
+    /// <returns>Chunk space coordinates</returns>
+    [Pure]
+    public Vector2i GetChunkCoords(EntityUid ent, TransformComponent? xform = null)
+    {
+        if (!Resolve(ent, ref xform))
+            throw new Exception("Failed to resolve transform, somehow.");
+
+        return WorldGen.WorldToChunkCoords(xform.WorldPosition).Floored();
+    }
+
+    /// <summary>
+    ///     Gets a chunk's coordinates in chunk space as a floating point value.
+    /// </summary>
+    /// <param name="ent"></param>
+    /// <param name="xform"></param>
+    /// <returns>Chunk space coordinates</returns>
+    [Pure]
+    public Vector2 GetFloatingChunkCoords(EntityUid ent, TransformComponent? xform = null)
+    {
+        if (!Resolve(ent, ref xform))
+            throw new Exception("Failed to resolve transform, somehow.");
+
+        return WorldGen.WorldToChunkCoords(xform.WorldPosition);
+    }
+
+    /// <summary>
+    ///     Attempts to get a chunk, creating it if it doesn't exist.
+    /// </summary>
+    /// <param name="chunk">Chunk coordinates to get the chunk entity for.</param>
+    /// <param name="map">Map the chunk is in.</param>
+    /// <param name="controller">The controller this chunk belongs to.</param>
+    /// <returns>A chunk, if available.</returns>
+    [Pure]
+    public EntityUid? GetOrCreateChunk(Vector2i chunk, EntityUid map, WorldControllerComponent? controller = null)
+    {
+        return _worldController.GetOrCreateChunk(chunk, map, controller);
+    }
+}
+
diff --git a/Content.Server/Worldgen/Systems/Biomes/BiomeSelectionSystem.cs b/Content.Server/Worldgen/Systems/Biomes/BiomeSelectionSystem.cs
new file mode 100644 (file)
index 0000000..3fff584
--- /dev/null
@@ -0,0 +1,75 @@
+using System.Linq;
+using Content.Server.Worldgen.Components;
+using Content.Server.Worldgen.Prototypes;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.Manager;
+
+namespace Content.Server.Worldgen.Systems.Biomes;
+
+/// <summary>
+///     This handles biome selection, evaluating which biome to apply to a chunk based on noise channels.
+/// </summary>
+public sealed class BiomeSelectionSystem : BaseWorldSystem
+{
+    [Dependency] private readonly NoiseIndexSystem _noiseIdx = default!;
+    [Dependency] private readonly IPrototypeManager _proto = default!;
+    [Dependency] private readonly ISerializationManager _ser = default!;
+
+    /// <inheritdoc />
+    public override void Initialize()
+    {
+        SubscribeLocalEvent<BiomeSelectionComponent, ComponentStartup>(OnBiomeSelectionStartup);
+        SubscribeLocalEvent<BiomeSelectionComponent, WorldChunkAddedEvent>(OnWorldChunkAdded);
+    }
+
+    private void OnWorldChunkAdded(EntityUid uid, BiomeSelectionComponent component, ref WorldChunkAddedEvent args)
+    {
+        var coords = args.Coords;
+        foreach (var biomeId in component.Biomes)
+        {
+            var biome = _proto.Index<BiomePrototype>(biomeId);
+            if (!CheckBiomeValidity(args.Chunk, biome, coords))
+                continue;
+
+            biome.Apply(args.Chunk, _ser, EntityManager);
+            return;
+        }
+
+        Logger.Error($"Biome selection ran out of biomes to select? See biomes list: {component.Biomes}");
+    }
+
+    private void OnBiomeSelectionStartup(EntityUid uid, BiomeSelectionComponent component, ComponentStartup args)
+    {
+        // surely this can't be THAAAAAAAAAAAAAAAT bad right????
+        var sorted = component.Biomes
+            .Select(x => (Id: x, _proto.Index<BiomePrototype>(x).Priority))
+            .OrderByDescending(x => x.Priority)
+            .Select(x => x.Id)
+            .ToList();
+
+        component.Biomes = sorted; // my hopes and dreams rely on this being pre-sorted by priority.
+    }
+
+    private bool CheckBiomeValidity(EntityUid chunk, BiomePrototype biome, Vector2i coords)
+    {
+        foreach (var (noise, ranges) in biome.NoiseRanges)
+        {
+            var value = _noiseIdx.Evaluate(chunk, noise, coords);
+            var anyValid = false;
+            foreach (var range in ranges)
+            {
+                if (range.X < value && value < range.Y)
+                {
+                    anyValid = true;
+                    break;
+                }
+            }
+
+            if (!anyValid)
+                return false;
+        }
+
+        return true;
+    }
+}
+
diff --git a/Content.Server/Worldgen/Systems/Carvers/NoiseRangeCarverSystem.cs b/Content.Server/Worldgen/Systems/Carvers/NoiseRangeCarverSystem.cs
new file mode 100644 (file)
index 0000000..aed32bc
--- /dev/null
@@ -0,0 +1,35 @@
+using Content.Server.Worldgen.Components.Carvers;
+using Content.Server.Worldgen.Systems.Debris;
+
+namespace Content.Server.Worldgen.Systems.Carvers;
+
+/// <summary>
+///     This handles carving out holes in world generation according to a noise channel.
+/// </summary>
+public sealed class NoiseRangeCarverSystem : EntitySystem
+{
+    [Dependency] private readonly NoiseIndexSystem _index = default!;
+
+    /// <inheritdoc />
+    public override void Initialize()
+    {
+        SubscribeLocalEvent<NoiseRangeCarverComponent, PrePlaceDebrisFeatureEvent>(OnPrePlaceDebris);
+    }
+
+    private void OnPrePlaceDebris(EntityUid uid, NoiseRangeCarverComponent component,
+        ref PrePlaceDebrisFeatureEvent args)
+    {
+        var coords = WorldGen.WorldToChunkCoords(args.Coords.ToMapPos(EntityManager));
+        var val = _index.Evaluate(uid, component.NoiseChannel, coords);
+
+        foreach (var (low, high) in component.Ranges)
+        {
+            if (low > val || high < val)
+                continue;
+
+            args.Handled = true;
+            return;
+        }
+    }
+}
+
diff --git a/Content.Server/Worldgen/Systems/Debris/BlobFloorPlanBuilderSystem.cs b/Content.Server/Worldgen/Systems/Debris/BlobFloorPlanBuilderSystem.cs
new file mode 100644 (file)
index 0000000..c69f53d
--- /dev/null
@@ -0,0 +1,85 @@
+using System.Linq;
+using Content.Server.Worldgen.Components.Debris;
+using Content.Shared.Maps;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Random;
+
+namespace Content.Server.Worldgen.Systems.Debris;
+
+/// <summary>
+///     This handles building the floor plans for "blobby" debris.
+/// </summary>
+public sealed class BlobFloorPlanBuilderSystem : BaseWorldSystem
+{
+    [Dependency] private readonly IRobustRandom _random = default!;
+    [Dependency] private readonly ITileDefinitionManager _tileDefinition = default!;
+
+    /// <inheritdoc />
+    public override void Initialize()
+    {
+        SubscribeLocalEvent<BlobFloorPlanBuilderComponent, ComponentStartup>(OnBlobFloorPlanBuilderStartup);
+    }
+
+    private void OnBlobFloorPlanBuilderStartup(EntityUid uid, BlobFloorPlanBuilderComponent component,
+        ComponentStartup args)
+    {
+        PlaceFloorplanTiles(component, Comp<MapGridComponent>(uid));
+    }
+
+    private void PlaceFloorplanTiles(BlobFloorPlanBuilderComponent comp, MapGridComponent grid)
+    {
+        // NO MORE THAN TWO ALLOCATIONS THANK YOU VERY MUCH.
+        var spawnPoints = new HashSet<Vector2i>(comp.FloorPlacements * 6);
+        var taken = new Dictionary<Vector2i, Tile>(comp.FloorPlacements * 5);
+
+        void PlaceTile(Vector2i point)
+        {
+            // Assume we already know that the spawn point is safe.
+            spawnPoints.Remove(point);
+            var north = point.Offset(Direction.North);
+            var south = point.Offset(Direction.South);
+            var east = point.Offset(Direction.East);
+            var west = point.Offset(Direction.West);
+            var radsq = Math.Pow(comp.Radius,
+                2); // I'd put this outside but i'm not 100% certain caching it between calls is a gain.
+
+            // The math done is essentially a fancy way of comparing the distance from 0,0 to the radius,
+            // and skipping the sqrt normally needed for dist.
+            if (!taken.ContainsKey(north) && Math.Pow(north.X, 2) + Math.Pow(north.Y, 2) <= radsq)
+                spawnPoints.Add(north);
+            if (!taken.ContainsKey(south) && Math.Pow(south.X, 2) + Math.Pow(south.Y, 2) <= radsq)
+                spawnPoints.Add(south);
+            if (!taken.ContainsKey(east) && Math.Pow(east.X, 2) + Math.Pow(east.Y, 2) <= radsq)
+                spawnPoints.Add(east);
+            if (!taken.ContainsKey(west) && Math.Pow(west.X, 2) + Math.Pow(west.Y, 2) <= radsq)
+                spawnPoints.Add(west);
+
+            var tileDef = _tileDefinition[_random.Pick(comp.FloorTileset)];
+            taken.Add(point, new Tile(tileDef.TileId, 0, _random.Pick(((ContentTileDefinition)tileDef).PlacementVariants)));
+        }
+
+        PlaceTile(Vector2i.Zero);
+
+        for (var i = 0; i < comp.FloorPlacements; i++)
+        {
+            var point = _random.Pick(spawnPoints);
+            PlaceTile(point);
+
+            if (comp.BlobDrawProb > 0.0f)
+            {
+                if (!taken.ContainsKey(point.Offset(Direction.North)) && _random.Prob(comp.BlobDrawProb))
+                    PlaceTile(point.Offset(Direction.North));
+                if (!taken.ContainsKey(point.Offset(Direction.South)) && _random.Prob(comp.BlobDrawProb))
+                    PlaceTile(point.Offset(Direction.South));
+                if (!taken.ContainsKey(point.Offset(Direction.East)) && _random.Prob(comp.BlobDrawProb))
+                    PlaceTile(point.Offset(Direction.East));
+                if (!taken.ContainsKey(point.Offset(Direction.West)) && _random.Prob(comp.BlobDrawProb))
+                    PlaceTile(point.Offset(Direction.West));
+            }
+        }
+
+        grid.SetTiles(taken.Select(x => (x.Key, x.Value)).ToList());
+    }
+}
+
diff --git a/Content.Server/Worldgen/Systems/Debris/DebrisFeaturePlacerSystem.cs b/Content.Server/Worldgen/Systems/Debris/DebrisFeaturePlacerSystem.cs
new file mode 100644 (file)
index 0000000..8c8360a
--- /dev/null
@@ -0,0 +1,264 @@
+using System.Linq;
+using Content.Server.Worldgen.Components;
+using Content.Server.Worldgen.Components.Debris;
+using Content.Server.Worldgen.Systems.GC;
+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;
+
+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 GCQueueSystem _gc = default!;
+    [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!;
+
+    /// <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<OwnedDebrisComponent, TryCancelGC>(OnTryCancelGC);
+        SubscribeLocalEvent<SimpleDebrisSelectorComponent, TryGetPlaceableDebrisFeatureEvent>(
+            OnTryGetPlacableDebrisEvent);
+    }
+
+    /// <summary>
+    ///     Handles GC cancellation in case the chunk is still loaded.
+    /// </summary>
+    private void OnTryCancelGC(EntityUid uid, OwnedDebrisComponent component, ref TryCancelGC args)
+    {
+        args.Cancelled |= HasComp<LoadedChunkComponent>(component.OwningController);
+    }
+
+    /// <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 = Transform(uid);
+        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)
+    {
+        foreach (var (_, debris) in component.OwnedDebris)
+        {
+            if (debris is not null)
+                _gc.TryGCEntity(debris.Value); // gonb.
+        }
+
+        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.
+
+        var chunk = Comp<WorldChunkComponent>(args.Chunk);
+        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, chunk.Map);
+
+        var safetyBounds = Box2.UnitCentered.Enlarged(component.SafetyZoneRadius);
+        var failures = 0; // Avoid severe log spam.
+        foreach (var point in points)
+        {
+            var pointDensity = _noiseIndex.Evaluate(uid, densityChannel, WorldGen.WorldToChunkCoords(point));
+            if (pointDensity == 0 && component.DensityClip || _random.Prob(component.RandomCancellationChance))
+                continue;
+
+            var coords = new EntityCoordinates(chunk.Map, point);
+
+            if (_mapManager
+                .FindGridsIntersecting(Comp<MapComponent>(chunk.Map).MapId, safetyBounds.Translated(point)).Any())
+                continue; // Oops, gonna collide.
+
+            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>
+    ///     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 = (-offs, -offs);
+        var lowerRight = (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);
+
diff --git a/Content.Server/Worldgen/Systems/Debris/NoiseDrivenDebrisSelectorSystem.cs b/Content.Server/Worldgen/Systems/Debris/NoiseDrivenDebrisSelectorSystem.cs
new file mode 100644 (file)
index 0000000..8d8ca1b
--- /dev/null
@@ -0,0 +1,59 @@
+using Content.Server.Worldgen.Components.Debris;
+using Robust.Server.GameObjects;
+using Robust.Shared.Random;
+
+namespace Content.Server.Worldgen.Systems.Debris;
+
+/// <summary>
+///     This handles selecting debris with probability decided by a noise channel.
+/// </summary>
+public sealed class NoiseDrivenDebrisSelectorSystem : BaseWorldSystem
+{
+    [Dependency] private readonly NoiseIndexSystem _index = default!;
+    [Dependency] private readonly TransformSystem _xformSys = default!;
+    [Dependency] private readonly ILogManager _logManager = default!;
+    [Dependency] private readonly IRobustRandom _random = default!;
+
+    private ISawmill _sawmill = default!;
+
+    /// <inheritdoc />
+    public override void Initialize()
+    {
+        _sawmill = _logManager.GetSawmill("world.debris.noise_debris_selector");
+        // Event is forcibly ordered to always be handled after the simple selector.
+        SubscribeLocalEvent<NoiseDrivenDebrisSelectorComponent, TryGetPlaceableDebrisFeatureEvent>(OnSelectDebrisKind,
+            after: new[] {typeof(DebrisFeaturePlacerSystem)});
+    }
+
+    private void OnSelectDebrisKind(EntityUid uid, NoiseDrivenDebrisSelectorComponent component,
+        ref TryGetPlaceableDebrisFeatureEvent args)
+    {
+        var coords = WorldGen.WorldToChunkCoords(args.Coords.ToMapPos(EntityManager, _xformSys));
+        var prob = _index.Evaluate(uid, component.NoiseChannel, coords);
+
+        if (prob is < 0 or > 1)
+        {
+            _sawmill.Error(
+                $"Sampled a probability of {prob}, which is outside the [0, 1] range, at {coords} aka {args.Coords}.");
+            return;
+        }
+
+        if (!_random.Prob(prob))
+            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];
+    }
+}
+
diff --git a/Content.Server/Worldgen/Systems/Debris/SimpleFloorPlanPopulatorSystem.cs b/Content.Server/Worldgen/Systems/Debris/SimpleFloorPlanPopulatorSystem.cs
new file mode 100644 (file)
index 0000000..ae1c6b5
--- /dev/null
@@ -0,0 +1,49 @@
+using Content.Server.Worldgen.Components.Debris;
+using Content.Shared.Maps;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Random;
+
+namespace Content.Server.Worldgen.Systems.Debris;
+
+/// <summary>
+///     This handles populating simple structures, simply using a loot table for each tile.
+/// </summary>
+public sealed class SimpleFloorPlanPopulatorSystem : BaseWorldSystem
+{
+    [Dependency] private readonly IRobustRandom _random = default!;
+    [Dependency] private readonly ITileDefinitionManager _tileDefinition = default!;
+
+    /// <inheritdoc />
+    public override void Initialize()
+    {
+        SubscribeLocalEvent<SimpleFloorPlanPopulatorComponent, LocalStructureLoadedEvent>(OnFloorPlanBuilt);
+    }
+
+    private void OnFloorPlanBuilt(EntityUid uid, SimpleFloorPlanPopulatorComponent component,
+        LocalStructureLoadedEvent args)
+    {
+        var placeables = new List<string?>(4);
+        var grid = Comp<MapGridComponent>(uid);
+        var enumerator = grid.GetAllTilesEnumerator();
+        while (enumerator.MoveNext(out var tile))
+        {
+            var coords = grid.GridTileToLocal(tile.Value.GridIndices);
+            var selector = tile.Value.Tile.GetContentTileDefinition(_tileDefinition).ID;
+            if (!component.Caches.TryGetValue(selector, out var cache))
+                continue;
+
+            placeables.Clear();
+            cache.GetSpawns(_random, ref placeables);
+
+            foreach (var proto in placeables)
+            {
+                if (proto is null)
+                    continue;
+
+                Spawn(proto, coords);
+            }
+        }
+    }
+}
+
diff --git a/Content.Server/Worldgen/Systems/GC/GCQueueSystem.cs b/Content.Server/Worldgen/Systems/GC/GCQueueSystem.cs
new file mode 100644 (file)
index 0000000..98d17af
--- /dev/null
@@ -0,0 +1,124 @@
+using System.Linq;
+using Content.Server.Worldgen.Components.GC;
+using Content.Server.Worldgen.Prototypes;
+using Content.Shared.CCVar;
+using JetBrains.Annotations;
+using Robust.Shared.Configuration;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Worldgen.Systems.GC;
+
+/// <summary>
+///     This handles delayed garbage collection of entities, to avoid overloading the tick in particularly expensive cases.
+/// </summary>
+public sealed class GCQueueSystem : EntitySystem
+{
+    [Dependency] private readonly IConfigurationManager _cfg = default!;
+    [Dependency] private readonly IPrototypeManager _proto = default!;
+    [Dependency] private readonly IRobustRandom _random = default!;
+
+    [ViewVariables] private TimeSpan _maximumProcessTime = TimeSpan.Zero;
+
+    [ViewVariables] private readonly Dictionary<string, Queue<EntityUid>> _queues = new();
+
+    /// <inheritdoc />
+    public override void Initialize()
+    {
+        _cfg.OnValueChanged(CCVars.GCMaximumTimeMs, s => _maximumProcessTime = TimeSpan.FromMilliseconds(s),
+            true);
+    }
+
+    /// <inheritdoc />CCVars
+    public override void Update(float frameTime)
+    {
+        var overallWatch = new Stopwatch();
+        var queueWatch = new Stopwatch();
+        var queues = _queues.ToList();
+        _random.Shuffle(queues); // Avert resource starvation by always processing in random order.
+        overallWatch.Start();
+        foreach (var (pId, queue) in queues)
+        {
+            if (overallWatch.Elapsed > _maximumProcessTime)
+                return;
+
+            var proto = _proto.Index<GCQueuePrototype>(pId);
+            if (queue.Count < proto.MinDepthToProcess)
+                continue;
+
+            queueWatch.Restart();
+            while (queueWatch.Elapsed < proto.MaximumTickTime && queue.Count >= proto.MinDepthToProcess &&
+                   overallWatch.Elapsed < _maximumProcessTime)
+            {
+                var e = queue.Dequeue();
+                if (!Deleted(e))
+                {
+                    var ev = new TryCancelGC();
+                    RaiseLocalEvent(e, ref ev);
+
+                    if (!ev.Cancelled)
+                        Del(e);
+                }
+            }
+        }
+    }
+
+    /// <summary>
+    ///     Attempts to GC an entity. This functions as QueueDel if it can't.
+    /// </summary>
+    /// <param name="e">Entity to GC.</param>
+    public void TryGCEntity(EntityUid e)
+    {
+        if (!TryComp<GCAbleObjectComponent>(e, out var comp))
+        {
+            QueueDel(e); // not our problem :)
+            return;
+        }
+
+        if (!_queues.TryGetValue(comp.Queue, out var queue))
+        {
+            queue = new Queue<EntityUid>();
+            _queues[comp.Queue] = queue;
+        }
+
+        var proto = _proto.Index<GCQueuePrototype>(comp.Queue);
+        if (queue.Count > proto.Depth)
+        {
+            QueueDel(e); // whelp, too full.
+            return;
+        }
+
+        if (proto.TrySkipQueue)
+        {
+            var ev = new TryGCImmediately();
+            RaiseLocalEvent(e, ref ev);
+            if (!ev.Cancelled)
+            {
+                QueueDel(e);
+                return;
+            }
+        }
+
+        queue.Enqueue(e);
+    }
+}
+
+/// <summary>
+///     Fired by GCQueueSystem to check if it can simply immediately GC an entity, for example if it was never fully
+///     loaded.
+/// </summary>
+/// <param name="Cancelled">Whether or not the immediate deletion attempt was cancelled.</param>
+[ByRefEvent]
+[PublicAPI]
+public record struct TryGCImmediately(bool Cancelled = false);
+
+/// <summary>
+///     Fired by GCQueueSystem to check if the collection of the given entity should be cancelled, for example it's chunk
+///     being loaded again.
+/// </summary>
+/// <param name="Cancelled">Whether or not the deletion attempt was cancelled.</param>
+[ByRefEvent]
+[PublicAPI]
+public record struct TryCancelGC(bool Cancelled = false);
+
diff --git a/Content.Server/Worldgen/Systems/LocalityLoaderSystem.cs b/Content.Server/Worldgen/Systems/LocalityLoaderSystem.cs
new file mode 100644 (file)
index 0000000..97ed4c5
--- /dev/null
@@ -0,0 +1,59 @@
+using Content.Server.Worldgen.Components;
+using Robust.Server.GameObjects;
+
+namespace Content.Server.Worldgen.Systems;
+
+/// <summary>
+///     This handles loading in objects based on distance from player, using some metadata on chunks.
+/// </summary>
+public sealed class LocalityLoaderSystem : BaseWorldSystem
+{
+    [Dependency] private readonly TransformSystem _xformSys = default!;
+
+    /// <inheritdoc />
+    public override void Update(float frameTime)
+    {
+        var e = EntityQueryEnumerator<LocalityLoaderComponent, TransformComponent>();
+        var loadedQuery = GetEntityQuery<LoadedChunkComponent>();
+        var xformQuery = GetEntityQuery<TransformComponent>();
+        var controllerQuery = GetEntityQuery<WorldControllerComponent>();
+
+        while (e.MoveNext(out var uid, out var loadable, out var xform))
+        {
+            if (!controllerQuery.TryGetComponent(xform.MapUid, out var controller))
+                return;
+
+            var coords = GetChunkCoords(uid, xform);
+            var done = false;
+            for (var i = -1; i < 2 && !done; i++)
+            {
+                for (var j = -1; j < 2 && !done; j++)
+                {
+                    var chunk = GetOrCreateChunk(coords + (i, j), xform.MapUid!.Value, controller);
+                    if (!loadedQuery.TryGetComponent(chunk, out var loaded) || loaded.Loaders is null)
+                        continue;
+
+                    foreach (var loader in loaded.Loaders)
+                    {
+                        if (!xformQuery.TryGetComponent(loader, out var loaderXform))
+                            continue;
+
+                        if ((_xformSys.GetWorldPosition(loaderXform) - _xformSys.GetWorldPosition(xform)).Length > loadable.LoadingDistance)
+                            continue;
+
+                        RaiseLocalEvent(uid, new LocalStructureLoadedEvent());
+                        RemCompDeferred<LocalityLoaderComponent>(uid);
+                        done = true;
+                        break;
+                    }
+                }
+            }
+        }
+    }
+}
+
+/// <summary>
+///     A directed fired on a loadable entity when a local loader enters it's vicinity.
+/// </summary>
+public record struct LocalStructureLoadedEvent;
+
diff --git a/Content.Server/Worldgen/Systems/NoiseIndexSystem.cs b/Content.Server/Worldgen/Systems/NoiseIndexSystem.cs
new file mode 100644 (file)
index 0000000..59de257
--- /dev/null
@@ -0,0 +1,46 @@
+using Content.Server.Worldgen.Components;
+using Content.Server.Worldgen.Prototypes;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.Worldgen.Systems;
+
+/// <summary>
+///     This handles the noise index.
+/// </summary>
+public sealed class NoiseIndexSystem : EntitySystem
+{
+    [Dependency] private readonly IPrototypeManager _prototype = default!;
+    [Dependency] private readonly IRobustRandom _random = default!;
+
+    /// <summary>
+    ///     Gets a particular noise channel from the index on the given entity.
+    /// </summary>
+    /// <param name="holder">The holder of the index</param>
+    /// <param name="protoId">The channel prototype ID</param>
+    /// <returns>An initialized noise generator</returns>
+    public NoiseGenerator Get(EntityUid holder, string protoId)
+    {
+        var idx = EnsureComp<NoiseIndexComponent>(holder);
+        if (idx.Generators.TryGetValue(protoId, out var generator))
+            return generator;
+        var proto = _prototype.Index<NoiseChannelPrototype>(protoId);
+        var gen = new NoiseGenerator(proto, _random.Next());
+        idx.Generators[protoId] = gen;
+        return gen;
+    }
+
+    /// <summary>
+    ///     Attempts to evaluate the given noise channel using the generator on the given entity.
+    /// </summary>
+    /// <param name="holder">The holder of the index</param>
+    /// <param name="protoId">The channel prototype ID</param>
+    /// <param name="coords">The coordinates to evaluate at</param>
+    /// <returns>The result of evaluation</returns>
+    public float Evaluate(EntityUid holder, string protoId, Vector2 coords)
+    {
+        var gen = Get(holder, protoId);
+        return gen.Evaluate(coords);
+    }
+}
+
diff --git a/Content.Server/Worldgen/Systems/WorldControllerSystem.cs b/Content.Server/Worldgen/Systems/WorldControllerSystem.cs
new file mode 100644 (file)
index 0000000..84c12cd
--- /dev/null
@@ -0,0 +1,278 @@
+using System.Linq;
+using Content.Server.Ghost.Components;
+using Content.Server.Mind.Components;
+using Content.Server.Worldgen.Components;
+using JetBrains.Annotations;
+using Robust.Server.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Worldgen.Systems;
+
+/// <summary>
+///     This handles putting together chunk entities and notifying them about important changes.
+/// </summary>
+public sealed class WorldControllerSystem : EntitySystem
+{
+    [Dependency] private readonly TransformSystem _xformSys = default!;
+    [Dependency] private readonly IGameTiming _gameTiming = default!;
+    [Dependency] private readonly ILogManager _logManager = default!;
+
+    private const int PlayerLoadRadius = 2;
+
+    private ISawmill _sawmill = default!;
+
+    /// <inheritdoc />
+    public override void Initialize()
+    {
+        _sawmill = _logManager.GetSawmill("world");
+        SubscribeLocalEvent<LoadedChunkComponent, ComponentStartup>(OnChunkLoadedCore);
+        SubscribeLocalEvent<LoadedChunkComponent, ComponentShutdown>(OnChunkUnloadedCore);
+        SubscribeLocalEvent<WorldChunkComponent, ComponentShutdown>(OnChunkShutdown);
+    }
+
+    /// <summary>
+    ///     Handles deleting chunks properly.
+    /// </summary>
+    private void OnChunkShutdown(EntityUid uid, WorldChunkComponent component, ComponentShutdown args)
+    {
+        if (!TryComp<WorldControllerComponent>(component.Map, out var controller))
+            return;
+
+        if (HasComp<LoadedChunkComponent>(uid))
+        {
+            var ev = new WorldChunkUnloadedEvent(uid, component.Coordinates);
+            RaiseLocalEvent(component.Map, ref ev);
+            RaiseLocalEvent(uid, ref ev, broadcast: true);
+        }
+
+        controller.Chunks.Remove(component.Coordinates);
+    }
+
+    /// <summary>
+    ///     Handles the inner logic of loading a chunk, i.e. events.
+    /// </summary>
+    private void OnChunkLoadedCore(EntityUid uid, LoadedChunkComponent component, ComponentStartup args)
+    {
+        if (!TryComp<WorldChunkComponent>(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}");
+    }
+
+    /// <summary>
+    ///     Handles the inner logic of unloading a chunk, i.e. events.
+    /// </summary>
+    private void OnChunkUnloadedCore(EntityUid uid, LoadedChunkComponent component, ComponentShutdown args)
+    {
+        if (!TryComp<WorldChunkComponent>(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}");
+    }
+
+    /// <inheritdoc />
+    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<EntityUid, Dictionary<Vector2i, List<EntityUid>>>();
+
+        var controllerEnum = EntityQueryEnumerator<WorldControllerComponent>();
+        while (controllerEnum.MoveNext(out var uid, out _))
+        {
+            chunksToLoad[uid] = new Dictionary<Vector2i, List<EntityUid>>();
+        }
+
+        if (chunksToLoad.Count == 0)
+            return; // Just bail early.
+
+        var loaderEnum = EntityQueryEnumerator<WorldLoaderComponent, TransformComponent>();
+
+        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<EntityUid>(4);
+                set[chunk.Value].Add(uid);
+            }
+        }
+
+        var mindEnum = EntityQueryEnumerator<MindComponent, TransformComponent>();
+        var ghostQuery = GetEntityQuery<GhostComponent>();
+
+        // 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<EntityUid>(4);
+                set[chunk.Value].Add(uid);
+            }
+        }
+
+        var loadedEnum = EntityQueryEnumerator<LoadedChunkComponent, WorldChunkComponent>();
+        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<LoadedChunkComponent>(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<LoadedChunkComponent>();
+        var controllerQuery = GetEntityQuery<WorldControllerComponent>();
+        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<LoadedChunkComponent>(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.");
+        }
+    }
+
+    /// <summary>
+    ///     Attempts to get a chunk, creating it if it doesn't exist.
+    /// </summary>
+    /// <param name="chunk">Chunk coordinates to get the chunk entity for.</param>
+    /// <param name="map">Map the chunk is in.</param>
+    /// <param name="controller">The controller this chunk belongs to.</param>
+    /// <returns>A chunk, if available.</returns>
+    [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);
+    }
+
+    /// <summary>
+    ///     Constructs a new chunk entity, attaching it to the map.
+    /// </summary>
+    /// <param name="chunkCoords">The coordinates the new chunk should be initialized for.</param>
+    /// <param name="map"></param>
+    /// <param name="controller"></param>
+    /// <returns></returns>
+    private EntityUid CreateChunkEntity(Vector2i chunkCoords, EntityUid map, WorldControllerComponent controller)
+    {
+        var chunk = Spawn(controller.ChunkProto, MapCoordinates.Nullspace);
+        StartupChunkEntity(chunk, chunkCoords, map, controller);
+        var md = MetaData(chunk);
+        md.EntityName = $"Chunk {chunkCoords.X}/{chunkCoords.Y}";
+        return chunk;
+    }
+
+    private void StartupChunkEntity(EntityUid chunk, Vector2i coords, EntityUid map,
+        WorldControllerComponent controller)
+    {
+        if (!TryComp<WorldChunkComponent>(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);
+    }
+}
+
+/// <summary>
+///     A directed event fired when a chunk is initially set up in the world. The chunk is not loaded at this point.
+/// </summary>
+[ByRefEvent]
+[PublicAPI]
+public readonly record struct WorldChunkAddedEvent(EntityUid Chunk, Vector2i Coords);
+
+/// <summary>
+///     A directed event fired when a chunk is loaded into the world, i.e. a player or other world loader has entered vicinity.
+/// </summary>
+[ByRefEvent]
+[PublicAPI]
+public readonly record struct WorldChunkLoadedEvent(EntityUid Chunk, Vector2i Coords);
+
+/// <summary>
+///     A directed event fired when a chunk is unloaded from the world, i.e. no world loaders remain nearby.
+/// </summary>
+[ByRefEvent]
+[PublicAPI]
+public readonly record struct WorldChunkUnloadedEvent(EntityUid Chunk, Vector2i Coords);
+
diff --git a/Content.Server/Worldgen/Systems/WorldgenConfigSystem.cs b/Content.Server/Worldgen/Systems/WorldgenConfigSystem.cs
new file mode 100644 (file)
index 0000000..70db152
--- /dev/null
@@ -0,0 +1,85 @@
+using Content.Server.Administration;
+using Content.Server.GameTicking;
+using Content.Server.GameTicking.Events;
+using Content.Server.Worldgen.Components;
+using Content.Server.Worldgen.Prototypes;
+using Content.Shared.Administration;
+using Content.Shared.CCVar;
+using Robust.Shared.Configuration;
+using Robust.Shared.Console;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.Manager;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Worldgen.Systems;
+
+/// <summary>
+///     This handles configuring world generation during round start.
+/// </summary>
+public sealed class WorldgenConfigSystem : EntitySystem
+{
+    [Dependency] private readonly GameTicker _gameTicker = default!;
+    [Dependency] private readonly IConfigurationManager _cfg = default!;
+    [Dependency] private readonly IConsoleHost _conHost = default!;
+    [Dependency] private readonly IMapManager _map = default!;
+    [Dependency] private readonly IPrototypeManager _proto = default!;
+    [Dependency] private readonly ISerializationManager _ser = default!;
+
+    private bool _enabled;
+    private string _worldgenConfig = default!;
+
+    /// <inheritdoc />
+    public override void Initialize()
+    {
+        SubscribeLocalEvent<RoundStartingEvent>(OnLoadingMaps);
+        _conHost.RegisterCommand("applyworldgenconfig", Loc.GetString("cmd-applyworldgenconfig-description"), Loc.GetString("cmd-applyworldgenconfig-help"), ApplyWorldgenConfigCommand);
+        _cfg.OnValueChanged(CCVars.WorldgenEnabled, b => _enabled = b, true);
+        _cfg.OnValueChanged(CCVars.WorldgenConfig, s => _worldgenConfig = s, true);
+    }
+
+    [AdminCommand(AdminFlags.Mapping)]
+    private void ApplyWorldgenConfigCommand(IConsoleShell shell, string argstr, string[] args)
+    {
+        if (args.Length != 2)
+        {
+            shell.WriteError(Loc.GetString("shell-wrong-arguments-number-need-specific", ("properAmount", 2), ("currentAmount", args.Length)));
+            return;
+        }
+
+        if (!int.TryParse(args[0], out var mapInt) || !_map.MapExists(new MapId(mapInt)))
+        {
+            shell.WriteError(Loc.GetString("shell-invalid-map-id"));
+            return;
+        }
+
+        var map = _map.GetMapEntityId(new MapId(mapInt));
+
+        if (!_proto.TryIndex<WorldgenConfigPrototype>(args[1], out var proto))
+        {
+            shell.WriteError(Loc.GetString("shell-argument-must-be-prototype", ("index", 2), ("prototypeName", "cmd-applyworldgenconfig-prototype")));
+            return;
+        }
+
+        proto.Apply(map, _ser, EntityManager);
+        shell.WriteLine(Loc.GetString("cmd-applyworldgenconfig-success"));
+    }
+
+    /// <summary>
+    ///     Applies the world config to the default map if enabled.
+    /// </summary>
+    private void OnLoadingMaps(RoundStartingEvent ev)
+    {
+        if (_enabled == false)
+            return;
+
+        var target = _map.GetMapEntityId(_gameTicker.DefaultMap);
+        Logger.Debug($"Trying to configure {_gameTicker.DefaultMap}, aka {ToPrettyString(target)} aka {target}");
+        var cfg = _proto.Index<WorldgenConfigPrototype>(_worldgenConfig);
+
+        cfg.Apply(target, _ser, EntityManager); // Apply the config to the map.
+
+        DebugTools.Assert(HasComp<WorldControllerComponent>(target));
+    }
+}
+
diff --git a/Content.Server/Worldgen/Tools/EntitySpawnCollectionCache.cs b/Content.Server/Worldgen/Tools/EntitySpawnCollectionCache.cs
new file mode 100644 (file)
index 0000000..5480575
--- /dev/null
@@ -0,0 +1,96 @@
+using System.Linq;
+using Content.Shared.Storage;
+using Robust.Shared.Random;
+
+namespace Content.Server.Worldgen.Tools;
+
+/// <summary>
+///     A faster version of EntitySpawnCollection that requires caching to work.
+/// </summary>
+public sealed class EntitySpawnCollectionCache
+{
+    [ViewVariables] private readonly Dictionary<string, OrGroup> _orGroups = new();
+
+    public EntitySpawnCollectionCache(IEnumerable<EntitySpawnEntry> entries)
+    {
+        // collect groups together, create singular items that pass probability
+        foreach (var entry in entries)
+        {
+            if (!_orGroups.TryGetValue(entry.GroupId ?? string.Empty, out var orGroup))
+            {
+                orGroup = new OrGroup();
+                _orGroups.Add(entry.GroupId ?? string.Empty, orGroup);
+            }
+
+            orGroup.Entries.Add(entry);
+            orGroup.CumulativeProbability += entry.SpawnProbability;
+        }
+    }
+
+    /// <summary>
+    ///     Using a collection of entity spawn entries, picks a random list of entity prototypes to spawn from that collection.
+    /// </summary>
+    /// <remarks>
+    ///     This does not spawn the entities. The caller is responsible for doing so, since it may want to do something
+    ///     special to those entities (offset them, insert them into storage, etc)
+    /// </remarks>
+    /// <param name="random">Resolve param.</param>
+    /// <param name="spawned">List that spawned entities are inserted into.</param>
+    /// <returns>A list of entity prototypes that should be spawned.</returns>
+    /// <remarks>This is primarily useful if you're calling it many times over, as it lets you reuse the list repeatedly.</remarks>
+    public void GetSpawns(IRobustRandom random, ref List<string?> spawned)
+    {
+        // handle orgroup spawns
+        foreach (var spawnValue in _orGroups.Values)
+        {
+            //HACK: This doesn't seem to work without this if there's only a single orgroup entry. Not sure how to fix the original math properly, but it works in every other case.
+            if (spawnValue.Entries.Count == 1)
+            {
+                var entry = spawnValue.Entries.First();
+                var amount = entry.Amount;
+
+                if (entry.MaxAmount > amount)
+                    amount = random.Next(amount, entry.MaxAmount);
+
+                for (var index = 0; index < amount; index++)
+                {
+                    spawned.Add(entry.PrototypeId);
+                }
+
+                continue;
+            }
+
+            // For each group use the added cumulative probability to roll a double in that range
+            var diceRoll = random.NextDouble() * spawnValue.CumulativeProbability;
+            // Add the entry's spawn probability to this value, if equals or lower, spawn item, otherwise continue to next item.
+            var cumulative = 0.0;
+            foreach (var entry in spawnValue.Entries)
+            {
+                cumulative += entry.SpawnProbability;
+                if (diceRoll > cumulative)
+                    continue;
+                // Dice roll succeeded, add item and break loop
+
+                var amount = entry.Amount;
+
+                if (entry.MaxAmount > amount)
+                    amount = random.Next(amount, entry.MaxAmount);
+
+                for (var index = 0; index < amount; index++)
+                {
+                    spawned.Add(entry.PrototypeId);
+                }
+
+                break;
+            }
+        }
+    }
+
+    private sealed class OrGroup
+    {
+        [ViewVariables] public List<EntitySpawnEntry> Entries { get; } = new();
+
+        [ViewVariables] public float CumulativeProbability { get; set; }
+    }
+}
+
diff --git a/Content.Server/Worldgen/Tools/PoissonDiskSampler.cs b/Content.Server/Worldgen/Tools/PoissonDiskSampler.cs
new file mode 100644 (file)
index 0000000..e372b4f
--- /dev/null
@@ -0,0 +1,243 @@
+using System.Diagnostics.CodeAnalysis;
+using Robust.Shared.Random;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Worldgen.Tools;
+
+/// <summary>
+///     An implementation of Poisson Disk Sampling, for evenly spreading points across a given area.
+/// </summary>
+public sealed class PoissonDiskSampler
+{
+    public const int DefaultPointsPerIteration = 30;
+    [Dependency] private readonly IRobustRandom _random = default!;
+
+    /// <summary>
+    ///     Samples for points within the given circle.
+    /// </summary>
+    /// <param name="center">Center of the sample</param>
+    /// <param name="radius">Radius of the sample</param>
+    /// <param name="minimumDistance">Minimum distance between points. Must be above 0!</param>
+    /// <param name="pointsPerIteration">The number of points placed per iteration of the algorithm</param>
+    /// <returns>An enumerator of points</returns>
+    public SampleEnumerator SampleCircle(Vector2 center, float radius, float minimumDistance,
+        int pointsPerIteration = DefaultPointsPerIteration)
+    {
+        return Sample(center - new Vector2(radius, radius), center + new Vector2(radius, radius), radius,
+            minimumDistance, pointsPerIteration);
+    }
+
+    /// <summary>
+    ///     Samples for points within the given rectangle.
+    /// </summary>
+    /// <param name="topLeft">The top left of the rectangle</param>
+    /// <param name="lowerRight">The bottom right of the rectangle</param>
+    /// <param name="minimumDistance">Minimum distance between points. Must be above 0!</param>
+    /// <param name="pointsPerIteration">The number of points placed per iteration of the algorithm</param>
+    /// <returns>An enumerator of points</returns>
+    public SampleEnumerator SampleRectangle(Vector2 topLeft, Vector2 lowerRight, float minimumDistance,
+        int pointsPerIteration = DefaultPointsPerIteration)
+    {
+        return Sample(topLeft, lowerRight, null, minimumDistance, pointsPerIteration);
+    }
+
+    /// <summary>
+    ///     Samples for points within the given rectangle, with an optional rejection distance.
+    /// </summary>
+    /// <param name="topLeft">The top left of the rectangle</param>
+    /// <param name="lowerRight">The bottom right of the rectangle</param>
+    /// <param name="rejectionDistance">The distance at which points will be discarded, if any</param>
+    /// <param name="minimumDistance">Minimum distance between points. Must be above 0!</param>
+    /// <param name="pointsPerIteration">The number of points placed per iteration of the algorithm</param>
+    /// <returns>An enumerator of points</returns>
+    public SampleEnumerator Sample(Vector2 topLeft, Vector2 lowerRight, float? rejectionDistance,
+        float minimumDistance, int pointsPerIteration)
+    {
+        // This still doesn't guard against dangerously low but non-zero distances, but this will do for now.
+        DebugTools.Assert(minimumDistance > 0, "Minimum distance must be above 0, or else an infinite number of points would be generated.");
+
+        var settings = new SampleSettings
+        {
+            TopLeft = topLeft, LowerRight = lowerRight,
+            Dimensions = lowerRight - topLeft,
+            Center = (topLeft + lowerRight) / 2,
+            CellSize = minimumDistance / (float) Math.Sqrt(2),
+            MinimumDistance = minimumDistance,
+            RejectionSqDistance = rejectionDistance * rejectionDistance
+        };
+
+        settings.GridWidth = (int) (settings.Dimensions.X / settings.CellSize) + 1;
+        settings.GridHeight = (int) (settings.Dimensions.Y / settings.CellSize) + 1;
+
+        var state = new State
+        {
+            Grid = new Vector2?[settings.GridWidth, settings.GridHeight],
+            ActivePoints = new List<Vector2>()
+        };
+
+        return new SampleEnumerator(this, state, settings, pointsPerIteration);
+    }
+
+    private Vector2 AddFirstPoint(ref SampleSettings settings, ref State state)
+    {
+        while (true)
+        {
+            var d = _random.NextDouble();
+            var xr = settings.TopLeft.X + settings.Dimensions.X * d;
+
+            d = _random.NextDouble();
+            var yr = settings.TopLeft.Y + settings.Dimensions.Y * d;
+
+            var p = new Vector2((float) xr, (float) yr);
+            if (settings.RejectionSqDistance != null &&
+                (settings.Center - p).LengthSquared > settings.RejectionSqDistance)
+                continue;
+
+            var index = Denormalize(p, settings.TopLeft, settings.CellSize);
+
+            state.Grid[(int) index.X, (int) index.Y] = p;
+
+            state.ActivePoints.Add(p);
+            return p;
+        }
+    }
+
+    private Vector2? AddNextPoint(Vector2 point, ref SampleSettings settings, ref State state)
+    {
+        var q = GenerateRandomAround(point, settings.MinimumDistance);
+
+        if (q.X >= settings.TopLeft.X && q.X < settings.LowerRight.X &&
+            q.Y > settings.TopLeft.Y && q.Y < settings.LowerRight.Y &&
+            (settings.RejectionSqDistance == null ||
+             (settings.Center - q).LengthSquared <= settings.RejectionSqDistance))
+        {
+            var qIndex = Denormalize(q, settings.TopLeft, settings.CellSize);
+            var tooClose = false;
+
+            for (var i = (int) Math.Max(0, qIndex.X - 2);
+                 i < Math.Min(settings.GridWidth, qIndex.X + 3) && !tooClose;
+                 i++)
+            for (var j = (int) Math.Max(0, qIndex.Y - 2);
+                 j < Math.Min(settings.GridHeight, qIndex.Y + 3) && !tooClose;
+                 j++)
+            {
+                if (state.Grid[i, j].HasValue && (state.Grid[i, j]!.Value - q).Length < settings.MinimumDistance)
+                    tooClose = true;
+            }
+
+            if (!tooClose)
+            {
+                state.ActivePoints.Add(q);
+                state.Grid[(int) qIndex.X, (int) qIndex.Y] = q;
+                return q;
+            }
+        }
+
+        return null;
+    }
+
+    private Vector2 GenerateRandomAround(Vector2 center, float minimumDistance)
+    {
+        var d = _random.NextDouble();
+        var radius = minimumDistance + minimumDistance * d;
+
+        d = _random.NextDouble();
+        var angle = Math.PI * 2 * d;
+
+        var newX = radius * Math.Sin(angle);
+        var newY = radius * Math.Cos(angle);
+
+        return new Vector2((float) (center.X + newX), (float) (center.Y + newY));
+    }
+
+    private static Vector2 Denormalize(Vector2 point, Vector2 origin, double cellSize)
+    {
+        return new Vector2((int) ((point.X - origin.X) / cellSize), (int) ((point.Y - origin.Y) / cellSize));
+    }
+
+    public struct SampleEnumerator
+    {
+        private PoissonDiskSampler _pds;
+        private State _state;
+        private SampleSettings _settings;
+        // These variables make up the state machine.
+        private bool _returnedFirstPoint;
+        private int _pointsPerIteration;
+        private int _iterationListIndex;
+        private bool _iterationFound;
+        private int _iterationPosition;
+
+        // This has internal access because C# nested type access is being weird.
+        internal SampleEnumerator(PoissonDiskSampler pds, State state, SampleSettings settings, int ppi)
+        {
+            _pds = pds;
+            _state = state;
+            _settings = settings;
+            _pointsPerIteration = ppi;
+        }
+
+        public bool MoveNext([NotNullWhen(true)] out Vector2? point)
+        {
+            // First point is chosen via a very particular method.
+            if (!_returnedFirstPoint)
+            {
+                _returnedFirstPoint = true;
+                point = _pds.AddFirstPoint(ref _settings, ref _state);
+                return true;
+            }
+
+            // Remaining points have to be fed out carefully.
+            // We can be interrupted (by a successful point) mid-stream.
+            while (_state.ActivePoints.Count != 0)
+            {
+                if (_iterationPosition == 0)
+                {
+                    // First point of iteration.
+                    _iterationListIndex = _pds._random.Next(_state.ActivePoints.Count);
+                    _iterationFound = false;
+                }
+
+                var basePoint = _state.ActivePoints[_iterationListIndex];
+
+                point = _pds.AddNextPoint(basePoint, ref _settings, ref _state);
+
+                // Set this now, return later after processing is complete.
+                _iterationFound |= point != null;
+
+                // Iteration loop advance.
+                _iterationPosition++;
+                if (_iterationPosition == _pointsPerIteration)
+                {
+                    // Reached end of this iteration.
+                    _iterationPosition = 0;
+                    if (!_iterationFound)
+                        _state.ActivePoints.RemoveAt(_iterationListIndex);
+                }
+
+                if (point != null)
+                    return true;
+            }
+            point = null;
+            return false;
+        }
+    }
+
+    internal struct State
+    {
+        public Vector2?[,] Grid;
+        public List<Vector2> ActivePoints;
+    }
+
+    internal struct SampleSettings
+    {
+        public Vector2 TopLeft, LowerRight, Center;
+        public Vector2 Dimensions;
+        public float? RejectionSqDistance;
+        public float MinimumDistance;
+        public float CellSize;
+        public int GridWidth, GridHeight;
+    }
+}
+
+
+
diff --git a/Content.Server/Worldgen/WorldGen.cs b/Content.Server/Worldgen/WorldGen.cs
new file mode 100644 (file)
index 0000000..4dc64c5
--- /dev/null
@@ -0,0 +1,71 @@
+using System.Diagnostics.Contracts;
+
+namespace Content.Server.Worldgen;
+
+/// <summary>
+///     Contains a few world-generation related constants and static functions.
+/// </summary>
+public static class WorldGen
+{
+    /// <summary>
+    ///     The size of each chunk (isn't that self-explanatory.)
+    ///     Be careful about how small you make this.
+    /// </summary>
+    public const int ChunkSize = 128;
+
+    /// <summary>
+    ///     Converts world coordinates to chunk coordinates.
+    /// </summary>
+    /// <param name="inp">World coordinates</param>
+    /// <returns>Chunk coordinates</returns>
+    [Pure]
+    public static Vector2i WorldToChunkCoords(Vector2i inp)
+    {
+        return ((Vector2) inp * (1.0f / ChunkSize, 1.0f / ChunkSize)).Floored();
+    }
+
+    /// <summary>
+    ///     Converts world coordinates to chunk coordinates.
+    /// </summary>
+    /// <param name="inp">World coordinates</param>
+    /// <returns>Chunk coordinates</returns>
+    [Pure]
+    public static Vector2 WorldToChunkCoords(Vector2 inp)
+    {
+        return inp * (1.0f / ChunkSize, 1.0f / ChunkSize);
+    }
+
+    /// <summary>
+    ///     Converts chunk coordinates to world coordinates.
+    /// </summary>
+    /// <param name="inp">Chunk coordinates</param>
+    /// <returns>World coordinates</returns>
+    [Pure]
+    public static Vector2 ChunkToWorldCoords(Vector2i inp)
+    {
+        return inp * ChunkSize;
+    }
+
+    /// <summary>
+    ///     Converts chunk coordinates to world coordinates.
+    /// </summary>
+    /// <param name="inp">Chunk coordinates</param>
+    /// <returns>World coordinates</returns>
+    [Pure]
+    public static Vector2 ChunkToWorldCoords(Vector2 inp)
+    {
+        return inp * ChunkSize;
+    }
+
+    /// <summary>
+    ///     Converts chunk coordinates to world coordinates, getting the center of the chunk.
+    /// </summary>
+    /// <param name="inp">Chunk coordinates</param>
+    /// <returns>World coordinates</returns>
+    [Pure]
+    public static Vector2 ChunkToWorldCoordsCentered(Vector2i inp)
+    {
+        return inp * ChunkSize + Vector2i.One * (ChunkSize / 2);
+    }
+}
+
index 1d2e58761e856ed33ff6bc2140709fcd57b6414c..bd7954ee08430492c8979cd458f27fe6ab1415ac 100644 (file)
@@ -1589,5 +1589,26 @@ namespace Content.Shared.CCVar
         /// </summary>
         public static readonly CVarDef<bool> ConfigPresetDebug =
             CVarDef.Create("config.preset_debug", true, CVar.SERVERONLY);
+
+        /*
+         * World Generation
+         */
+        /// <summary>
+        ///     Whether or not world generation is enabled.
+        /// </summary>
+        public static readonly CVarDef<bool> WorldgenEnabled =
+            CVarDef.Create("worldgen.enabled", false, CVar.SERVERONLY);
+
+        /// <summary>
+        ///     The worldgen config to use.
+        /// </summary>
+        public static readonly CVarDef<string> WorldgenConfig =
+            CVarDef.Create("worldgen.worldgen_config", "Default", CVar.SERVERONLY);
+
+        /// <summary>
+        ///     The maximum amount of time the entity GC can process, in ms.
+        /// </summary>
+        public static readonly CVarDef<int> GCMaximumTimeMs =
+            CVarDef.Create("entgc.maximum_time_ms", 5, CVar.SERVERONLY);
     }
 }
index aa76563d54a2a46edfb744230763efc733d0ae38..48120503ac7e82cd1b56aad82a5ef28d13a374ce 100644 (file)
@@ -37,7 +37,7 @@ shell-invalid-color-hex = Invalid color hex!
 shell-target-player-does-not-exist = Target player does not exist!
 shell-target-entity-does-not-have-message = Target entity does not have {INDEFINITE($missing)} {$missing}!
 shell-timespan-minutes-must-be-correct = {$span} is not a valid minutes timespan.
-shell-argument-must-be-prototype = Argument {$index} must be a ${prototypeName}!
+shell-argument-must-be-prototype = Argument {$index} must be a {LOC($prototypeName)}!
 shell-argument-number-must-be-between = Argument {$index} must be a number between {$lower} and {$upper}!
 shell-argument-station-id-invalid = Argument {$index} must be a valid station id!
 shell-argument-map-id-invalid = Argument {$index} must be a valid map id!
diff --git a/Resources/Locale/en-US/worldgen/applyworldgenconfig.ftl b/Resources/Locale/en-US/worldgen/applyworldgenconfig.ftl
new file mode 100644 (file)
index 0000000..a2144d0
--- /dev/null
@@ -0,0 +1,4 @@
+cmd-applyworldgenconfig-description = Applies the given worldgen configuration to a map, setting it up for chunk loading/etc.
+cmd-applyworldgenconfig-help = applyworldgenconfig <mapid> <prototype>
+cmd-applyworldgenconfig-prototype = worldgen config prototype
+cmd-applyworldgenconfig-success = Config applied successfully. Do not rerun this command on this map.
index 6d80d44472d24dea70340cdfb0aa2819e08cf754..b1edc2c24eecdd1d03f94f826541696d0d3c8bef 100644 (file)
@@ -55,6 +55,8 @@
       - key: enum.ShuttleConsoleUiKey.Key
         type: ShuttleConsoleBoundUserInterface
   - type: RadarConsole
+  - type: WorldLoader
+    radius: 256
   - type: PointLight
     radius: 1.5
     energy: 1.6
       - Syndicate
   - type: RadarConsole
     maxRange: 1536
+  - type: WorldLoader
+    radius: 1536
   - type: PointLight
     radius: 1.5
     energy: 1.6
diff --git a/Resources/Prototypes/Entities/World/Debris/asteroids.yml b/Resources/Prototypes/Entities/World/Debris/asteroids.yml
new file mode 100644 (file)
index 0000000..0c55e90
--- /dev/null
@@ -0,0 +1,76 @@
+- type: entity
+  id: BaseAsteroidDebris
+  parent: BaseDebris
+  name: Asteroid Debris
+  abstract: true
+  components:
+    - type: MapGrid
+    - type: BlobFloorPlanBuilder
+      floorTileset:
+      - FloorAsteroidCoarseSand0
+      blobDrawProb: 0.5
+      radius: 6
+      floorPlacements: 16
+    - type: SimpleFloorPlanPopulator
+      entries:
+        FloorAsteroidCoarseSand0:
+          - id: WallRock
+            prob: 0.5
+          - id: WallRockGold
+            prob: 0.01
+          - id: WallRockSilver
+            prob: 0.04
+          - id: WallRockPlasma
+            prob: 0.09
+          - id: WallRockTin
+            prob: 0.2
+          - id: WallRockUranium
+            prob: 0.07
+          - id: WallRockQuartz
+            prob: 0.2
+    - type: GCAbleObject
+      queue: SpaceDebris
+    - type: IFF
+      flags: HideLabel
+      color: "#d67e27"
+
+- type: entity
+  id: AsteroidDebrisSmall
+  parent: BaseAsteroidDebris
+  name: Asteroid Debris Small
+  noSpawn: true
+  components:
+    - type: MapGrid
+    - type: BlobFloorPlanBuilder
+      floorPlacements: 8
+
+- type: entity
+  id: AsteroidDebrisMedium
+  parent: BaseAsteroidDebris
+  name: Asteroid Debris Medium
+  noSpawn: true
+  components:
+    - type: MapGrid
+    - type: BlobFloorPlanBuilder
+      floorPlacements: 16
+
+- type: entity
+  id: AsteroidDebrisLarge
+  parent: BaseAsteroidDebris
+  name: Asteroid Debris Large
+  noSpawn: true
+  components:
+    - type: MapGrid
+    - type: BlobFloorPlanBuilder
+      floorPlacements: 24
+
+- type: entity
+  id: AsteroidDebrisLarger
+  parent: BaseAsteroidDebris
+  name: Asteroid Debris Larger
+  noSpawn: true
+  components:
+    - type: MapGrid
+    - type: BlobFloorPlanBuilder
+      radius: 12
+      floorPlacements: 36
diff --git a/Resources/Prototypes/Entities/World/Debris/base_debris.yml b/Resources/Prototypes/Entities/World/Debris/base_debris.yml
new file mode 100644 (file)
index 0000000..c125d99
--- /dev/null
@@ -0,0 +1,6 @@
+- type: entity
+  id: BaseDebris
+  abstract: true
+  components:
+    - type: OwnedDebris
+    - type: LocalityLoader
diff --git a/Resources/Prototypes/Entities/World/Debris/wrecks.yml b/Resources/Prototypes/Entities/World/Debris/wrecks.yml
new file mode 100644 (file)
index 0000000..a3cfaf9
--- /dev/null
@@ -0,0 +1,82 @@
+- type: entity
+  id: BaseScrapDebris
+  parent: BaseDebris
+  name: Scrap Debris
+  abstract: true
+  components:
+    - type: MapGrid
+    - type: BlobFloorPlanBuilder
+      floorTileset:
+        - Plating
+        - Plating
+        - Plating
+        - FloorSteel
+        - Lattice
+      blobDrawProb: 0.5
+      radius: 6
+      floorPlacements: 16
+    - type: SimpleFloorPlanPopulator
+      entries:
+        Plating:
+          - prob: 3 # Intentional blank.
+          - id: SalvageMaterialCrateSpawner
+            prob: 1
+          - id: SalvageCanisterSpawner
+            prob: 0.2
+          - id: SalvageMobSpawner
+            prob: 0.7
+          - id: WallSolid
+            prob: 1
+          - id: Grille
+            prob: 0.5
+        Lattice:
+          - prob: 2
+          - id: Grille
+            prob: 0.2
+          - id: SalvageMaterialCrateSpawner
+            prob: 0.3
+          - id: SalvageCanisterSpawner
+            prob: 0.2
+        FloorSteel:
+          - prob: 3 # Intentional blank.
+          - id: SalvageMaterialCrateSpawner
+            prob: 1
+          - id: SalvageCanisterSpawner
+            prob: 0.2
+          - id: SalvageMobSpawner
+            prob: 0.7
+    - type: GCAbleObject
+      queue: SpaceDebris
+    - type: IFF
+      flags: HideLabel
+      color: "#88b0d1"
+
+- type: entity
+  id: ScrapDebrisSmall
+  parent: BaseScrapDebris
+  name: Scrap Debris Small
+  noSpawn: true
+  components:
+    - type: MapGrid
+    - type: BlobFloorPlanBuilder
+      floorPlacements: 8
+
+- type: entity
+  id: ScrapDebrisMedium
+  parent: BaseScrapDebris
+  name: Scrap Debris Medium
+  noSpawn: true
+  components:
+    - type: MapGrid
+    - type: BlobFloorPlanBuilder
+      floorPlacements: 16
+
+- type: entity
+  id: ScrapDebrisLarge
+  parent: BaseScrapDebris
+  name: Scrap Debris Large
+  noSpawn: true
+  components:
+    - type: MapGrid
+    - type: BlobFloorPlanBuilder
+      floorPlacements: 24
diff --git a/Resources/Prototypes/Entities/World/chunk.yml b/Resources/Prototypes/Entities/World/chunk.yml
new file mode 100644 (file)
index 0000000..83a4d65
--- /dev/null
@@ -0,0 +1,15 @@
+- type: entity
+  id: WorldChunk
+  parent: MarkerBase
+  name: World Chunk
+  description: |
+    It's rude to stare.
+    It's also a bit odd you're looking at the abstract representation of the grid of reality.
+  noSpawn: true
+  components:
+    - type: WorldChunk
+    - type: Sprite
+      sprite: Markers/cross.rsi
+      netsync: false
+      layers:
+        - state: blue
diff --git a/Resources/Prototypes/GC/world.yml b/Resources/Prototypes/GC/world.yml
new file mode 100644 (file)
index 0000000..b58a681
--- /dev/null
@@ -0,0 +1,4 @@
+- type: gcQueue
+  id: SpaceDebris
+  depth: 512 # So there's a decent bit of time before roids unload.
+  minDepthToProcess: 256
diff --git a/Resources/Prototypes/World/Biomes/basic.yml b/Resources/Prototypes/World/Biomes/basic.yml
new file mode 100644 (file)
index 0000000..5ecd85b
--- /dev/null
@@ -0,0 +1,26 @@
+- type: spaceBiome
+  id: AsteroidsStandard
+  priority: 0 # This probably shouldn't get selected.
+  noiseRanges: {}
+  chunkComponents:
+    - type: DebrisFeaturePlacerController
+      densityNoiseChannel: Density
+    - type: SimpleDebrisSelector
+      debrisTable:
+        - id: AsteroidDebrisSmall
+        - id: AsteroidDebrisMedium
+        - id: AsteroidDebrisLarge
+          prob: 0.7
+        - id: AsteroidDebrisLarger
+          prob: 0.4
+    - type: NoiseDrivenDebrisSelector
+      noiseChannel: Wreck
+      debrisTable:
+        - id: ScrapDebrisSmall
+        - id: ScrapDebrisMedium
+        - id: ScrapDebrisLarge
+          prob: 0.5
+    - type: NoiseRangeCarver
+      ranges:
+        - 0.4, 0.6
+      noiseChannel: Carver
diff --git a/Resources/Prototypes/World/Biomes/failsafes.yml b/Resources/Prototypes/World/Biomes/failsafes.yml
new file mode 100644 (file)
index 0000000..5e3c50b
--- /dev/null
@@ -0,0 +1,21 @@
+- type: spaceBiome
+  id: Failsafe
+  priority: -999999 # This DEFINITELY shouldn't get selected!
+  noiseRanges: {}
+
+- type: spaceBiome
+  id: AsteroidsFallback
+  priority: -999998 # This probably shouldn't get selected.
+  noiseRanges: {}
+  chunkComponents:
+    - type: DebrisFeaturePlacerController
+      densityNoiseChannel: Density
+    - type: SimpleDebrisSelector
+      debrisTable:
+        - id: AsteroidDebrisSmall
+        - id: AsteroidDebrisMedium
+        - id: AsteroidDebrisLarge
+          prob: 0.7
+        - id: AsteroidDebrisLarger
+          prob: 0.4
+
diff --git a/Resources/Prototypes/World/noise_channels.yml b/Resources/Prototypes/World/noise_channels.yml
new file mode 100644 (file)
index 0000000..668b338
--- /dev/null
@@ -0,0 +1,44 @@
+- type: noiseChannel
+  id: Density
+  noiseType: Perlin
+  fractalLacunarityByPi: 0.666666666
+  remapTo0Through1: true
+  clippingRanges:
+    - 0.4, 0.6
+  clippedValue: 1.658 # magic number for chunk size.
+  inputMultiplier: 6 # Makes density hopefully low noise in the local area while still being interesting at scale.
+  outputMultiplier: 50.0 # We scale density up significantly for more human-friendly numbers.
+  minimum: 45.0
+
+- type: noiseChannel
+  id: DensityUnclipped
+  noiseType: Perlin
+  fractalLacunarityByPi: 0.666666666
+  remapTo0Through1: true
+  inputMultiplier: 6  # Makes density hopefully low noise in the local area while still being interesting at scale.
+  outputMultiplier: 50.0 # We scale density up significantly for more human-friendly numbers.
+  minimum: 45.0
+
+- type: noiseChannel
+  id: Carver
+  noiseType: Perlin
+  fractalLacunarityByPi: 0.666666666
+  remapTo0Through1: true
+  inputMultiplier: 6
+
+- type: noiseChannel
+  id: Wreck
+  noiseType: Perlin
+  fractalLacunarityByPi: 0.666666666
+  clippingRanges:
+    - 0.0, 0.4
+  clippedValue: 0
+  remapTo0Through1: true
+  inputMultiplier: 16 # Makes wreck concentration very low noise at scale.
+
+- type: noiseChannel
+  id: Temperature
+  noiseType: Perlin
+  fractalLacunarityByPi: 0.666666666
+  remapTo0Through1: true
+  inputMultiplier: 6 # Makes wreck concentration very low noise at scale.
diff --git a/Resources/Prototypes/World/worldgen_default.yml b/Resources/Prototypes/World/worldgen_default.yml
new file mode 100644 (file)
index 0000000..af52c30
--- /dev/null
@@ -0,0 +1,9 @@
+- type: worldgenConfig
+  id: Default
+  components:
+    - type: WorldController
+    - type: BiomeSelection
+      biomes:
+        - AsteroidsFallback
+        - Failsafe
+        - AsteroidsStandard