_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");
(CCVars.ArrivalsShuttles.Name, "false"),
(CCVars.EmergencyShuttleEnabled.Name, "false"),
(CCVars.ProcgenPreload.Name, "false"),
+ (CCVars.WorldgenEnabled.Name, "false"),
// @formatter:on
};
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();
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)
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)
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();
"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 = @"
"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 = @"
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;
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;
IoCManager.Register<PlayTimeTrackingManager>();
IoCManager.Register<UserDbDataManager>();
IoCManager.Register<ServerInfoManager>();
+ IoCManager.Register<PoissonDiskSampler>();
}
}
}
--- /dev/null
+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();
+}
+
--- /dev/null
+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!;
+}
+
--- /dev/null
+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; }
+}
+
--- /dev/null
+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!;
+}
+
--- /dev/null
+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!;
+}
+
--- /dev/null
+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;
+}
+
--- /dev/null
+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;
+ }
+ }
+}
+
--- /dev/null
+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;
+ }
+ }
+}
+
--- /dev/null
+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!;
+}
+
--- /dev/null
+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;
+}
+
--- /dev/null
+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;
+}
+
--- /dev/null
+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();
+}
+
--- /dev/null
+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;
+}
+
--- /dev/null
+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();
+}
+
--- /dev/null
+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;
+}
+
--- /dev/null
+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;
+ }
+}
+
--- /dev/null
+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);
+ }
+ }
+}
+
--- /dev/null
+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; }
+}
+
--- /dev/null
+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);
+}
+
--- /dev/null
+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);
+ }
+ }
+}
+
--- /dev/null
+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);
+ }
+}
+
--- /dev/null
+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;
+ }
+}
+
--- /dev/null
+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;
+ }
+ }
+}
+
--- /dev/null
+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());
+ }
+}
+
--- /dev/null
+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);
+
--- /dev/null
+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];
+ }
+}
+
--- /dev/null
+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);
+ }
+ }
+ }
+}
+
--- /dev/null
+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);
+
--- /dev/null
+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;
+
--- /dev/null
+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);
+ }
+}
+
--- /dev/null
+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);
+
--- /dev/null
+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));
+ }
+}
+
--- /dev/null
+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; }
+ }
+}
+
--- /dev/null
+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;
+ }
+}
+
+
+
--- /dev/null
+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);
+ }
+}
+
/// </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);
}
}
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!
--- /dev/null
+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.
- 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
--- /dev/null
+- 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
--- /dev/null
+- type: entity
+ id: BaseDebris
+ abstract: true
+ components:
+ - type: OwnedDebris
+ - type: LocalityLoader
--- /dev/null
+- 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
--- /dev/null
+- 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
--- /dev/null
+- type: gcQueue
+ id: SpaceDebris
+ depth: 512 # So there's a decent bit of time before roids unload.
+ minDepthToProcess: 256
--- /dev/null
+- 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
--- /dev/null
+- 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
+
--- /dev/null
+- 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.
--- /dev/null
+- type: worldgenConfig
+ id: Default
+ components:
+ - type: WorldController
+ - type: BiomeSelection
+ biomes:
+ - AsteroidsFallback
+ - Failsafe
+ - AsteroidsStandard