Dynamic space world generation and debris. (#15120)
* 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>
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 = @"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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!;
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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!;
|
||||
}
|
||||
|
||||
@@ -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!;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!;
|
||||
}
|
||||
|
||||
17
Content.Server/Worldgen/Components/LoadedChunkComponent.cs
Normal file
17
Content.Server/Worldgen/Components/LoadedChunkComponent.cs
Normal file
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
20
Content.Server/Worldgen/Components/NoiseIndexComponent.cs
Normal file
20
Content.Server/Worldgen/Components/NoiseIndexComponent.cs
Normal file
@@ -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();
|
||||
}
|
||||
|
||||
22
Content.Server/Worldgen/Components/WorldChunkComponent.cs
Normal file
22
Content.Server/Worldgen/Components/WorldChunkComponent.cs
Normal file
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
18
Content.Server/Worldgen/Components/WorldLoaderComponent.cs
Normal file
18
Content.Server/Worldgen/Components/WorldLoaderComponent.cs
Normal file
@@ -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;
|
||||
}
|
||||
|
||||
59
Content.Server/Worldgen/GridPointsNearEnumerator.cs
Normal file
59
Content.Server/Worldgen/GridPointsNearEnumerator.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
61
Content.Server/Worldgen/Prototypes/BiomePrototype.cs
Normal file
61
Content.Server/Worldgen/Prototypes/BiomePrototype.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
41
Content.Server/Worldgen/Prototypes/GCQueuePrototype.cs
Normal file
41
Content.Server/Worldgen/Prototypes/GCQueuePrototype.cs
Normal file
@@ -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; }
|
||||
}
|
||||
|
||||
169
Content.Server/Worldgen/Prototypes/NoiseChannelPrototype.cs
Normal file
169
Content.Server/Worldgen/Prototypes/NoiseChannelPrototype.cs
Normal file
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
58
Content.Server/Worldgen/Systems/BaseWorldSystem.cs
Normal file
58
Content.Server/Worldgen/Systems/BaseWorldSystem.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
124
Content.Server/Worldgen/Systems/GC/GCQueueSystem.cs
Normal file
124
Content.Server/Worldgen/Systems/GC/GCQueueSystem.cs
Normal file
@@ -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);
|
||||
|
||||
59
Content.Server/Worldgen/Systems/LocalityLoaderSystem.cs
Normal file
59
Content.Server/Worldgen/Systems/LocalityLoaderSystem.cs
Normal file
@@ -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;
|
||||
|
||||
46
Content.Server/Worldgen/Systems/NoiseIndexSystem.cs
Normal file
46
Content.Server/Worldgen/Systems/NoiseIndexSystem.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
278
Content.Server/Worldgen/Systems/WorldControllerSystem.cs
Normal file
278
Content.Server/Worldgen/Systems/WorldControllerSystem.cs
Normal file
@@ -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);
|
||||
|
||||
85
Content.Server/Worldgen/Systems/WorldgenConfigSystem.cs
Normal file
85
Content.Server/Worldgen/Systems/WorldgenConfigSystem.cs
Normal file
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
96
Content.Server/Worldgen/Tools/EntitySpawnCollectionCache.cs
Normal file
96
Content.Server/Worldgen/Tools/EntitySpawnCollectionCache.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
243
Content.Server/Worldgen/Tools/PoissonDiskSampler.cs
Normal file
243
Content.Server/Worldgen/Tools/PoissonDiskSampler.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
71
Content.Server/Worldgen/WorldGen.cs
Normal file
71
Content.Server/Worldgen/WorldGen.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!
|
||||
|
||||
4
Resources/Locale/en-US/worldgen/applyworldgenconfig.ftl
Normal file
4
Resources/Locale/en-US/worldgen/applyworldgenconfig.ftl
Normal file
@@ -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.
|
||||
@@ -55,6 +55,8 @@
|
||||
- key: enum.ShuttleConsoleUiKey.Key
|
||||
type: ShuttleConsoleBoundUserInterface
|
||||
- type: RadarConsole
|
||||
- type: WorldLoader
|
||||
radius: 256
|
||||
- type: PointLight
|
||||
radius: 1.5
|
||||
energy: 1.6
|
||||
@@ -102,6 +104,8 @@
|
||||
- Syndicate
|
||||
- type: RadarConsole
|
||||
maxRange: 1536
|
||||
- type: WorldLoader
|
||||
radius: 1536
|
||||
- type: PointLight
|
||||
radius: 1.5
|
||||
energy: 1.6
|
||||
|
||||
76
Resources/Prototypes/Entities/World/Debris/asteroids.yml
Normal file
76
Resources/Prototypes/Entities/World/Debris/asteroids.yml
Normal file
@@ -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
|
||||
@@ -0,0 +1,6 @@
|
||||
- type: entity
|
||||
id: BaseDebris
|
||||
abstract: true
|
||||
components:
|
||||
- type: OwnedDebris
|
||||
- type: LocalityLoader
|
||||
82
Resources/Prototypes/Entities/World/Debris/wrecks.yml
Normal file
82
Resources/Prototypes/Entities/World/Debris/wrecks.yml
Normal file
@@ -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
|
||||
15
Resources/Prototypes/Entities/World/chunk.yml
Normal file
15
Resources/Prototypes/Entities/World/chunk.yml
Normal file
@@ -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
|
||||
4
Resources/Prototypes/GC/world.yml
Normal file
4
Resources/Prototypes/GC/world.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
- type: gcQueue
|
||||
id: SpaceDebris
|
||||
depth: 512 # So there's a decent bit of time before roids unload.
|
||||
minDepthToProcess: 256
|
||||
26
Resources/Prototypes/World/Biomes/basic.yml
Normal file
26
Resources/Prototypes/World/Biomes/basic.yml
Normal file
@@ -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
|
||||
21
Resources/Prototypes/World/Biomes/failsafes.yml
Normal file
21
Resources/Prototypes/World/Biomes/failsafes.yml
Normal file
@@ -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
|
||||
|
||||
44
Resources/Prototypes/World/noise_channels.yml
Normal file
44
Resources/Prototypes/World/noise_channels.yml
Normal file
@@ -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.
|
||||
9
Resources/Prototypes/World/worldgen_default.yml
Normal file
9
Resources/Prototypes/World/worldgen_default.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
- type: worldgenConfig
|
||||
id: Default
|
||||
components:
|
||||
- type: WorldController
|
||||
- type: BiomeSelection
|
||||
biomes:
|
||||
- AsteroidsFallback
|
||||
- Failsafe
|
||||
- AsteroidsStandard
|
||||
Reference in New Issue
Block a user