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("salvageMap");
|
||||||
_prototypeManager.RegisterIgnore("salvageFaction");
|
_prototypeManager.RegisterIgnore("salvageFaction");
|
||||||
_prototypeManager.RegisterIgnore("gamePreset");
|
_prototypeManager.RegisterIgnore("gamePreset");
|
||||||
|
_prototypeManager.RegisterIgnore("noiseChannel");
|
||||||
|
_prototypeManager.RegisterIgnore("spaceBiome");
|
||||||
|
_prototypeManager.RegisterIgnore("worldgenConfig");
|
||||||
|
_prototypeManager.RegisterIgnore("gcQueue");
|
||||||
_prototypeManager.RegisterIgnore("gameRule");
|
_prototypeManager.RegisterIgnore("gameRule");
|
||||||
_prototypeManager.RegisterIgnore("worldSpell");
|
_prototypeManager.RegisterIgnore("worldSpell");
|
||||||
_prototypeManager.RegisterIgnore("entitySpell");
|
_prototypeManager.RegisterIgnore("entitySpell");
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ public static class PoolManager
|
|||||||
(CCVars.ArrivalsShuttles.Name, "false"),
|
(CCVars.ArrivalsShuttles.Name, "false"),
|
||||||
(CCVars.EmergencyShuttleEnabled.Name, "false"),
|
(CCVars.EmergencyShuttleEnabled.Name, "false"),
|
||||||
(CCVars.ProcgenPreload.Name, "false"),
|
(CCVars.ProcgenPreload.Name, "false"),
|
||||||
|
(CCVars.WorldgenEnabled.Name, "false"),
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ public sealed class CargoTest
|
|||||||
|
|
||||||
var protoIds = protoManager.EnumeratePrototypes<EntityPrototype>()
|
var protoIds = protoManager.EnumeratePrototypes<EntityPrototype>()
|
||||||
.Where(p=>!p.Abstract)
|
.Where(p=>!p.Abstract)
|
||||||
|
.Where(p => !p.Components.ContainsKey("MapGrid")) // Grids are not for sale.
|
||||||
.Select(p => p.ID)
|
.Select(p => p.ID)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ namespace Content.IntegrationTests.Tests
|
|||||||
var protoIds = prototypeMan
|
var protoIds = prototypeMan
|
||||||
.EnumeratePrototypes<EntityPrototype>()
|
.EnumeratePrototypes<EntityPrototype>()
|
||||||
.Where(p=>!p.Abstract)
|
.Where(p=>!p.Abstract)
|
||||||
|
.Where(p => !p.Components.ContainsKey("MapGrid")) // This will smash stuff otherwise.
|
||||||
.Select(p => p.ID)
|
.Select(p => p.ID)
|
||||||
.ToList();
|
.ToList();
|
||||||
foreach (var protoId in protoIds)
|
foreach (var protoId in protoIds)
|
||||||
@@ -87,6 +88,7 @@ namespace Content.IntegrationTests.Tests
|
|||||||
var protoIds = prototypeMan
|
var protoIds = prototypeMan
|
||||||
.EnumeratePrototypes<EntityPrototype>()
|
.EnumeratePrototypes<EntityPrototype>()
|
||||||
.Where(p=>!p.Abstract)
|
.Where(p=>!p.Abstract)
|
||||||
|
.Where(p => !p.Components.ContainsKey("MapGrid")) // This will smash stuff otherwise.
|
||||||
.Select(p => p.ID)
|
.Select(p => p.ID)
|
||||||
.ToList();
|
.ToList();
|
||||||
foreach (var protoId in protoIds)
|
foreach (var protoId in protoIds)
|
||||||
@@ -133,6 +135,7 @@ namespace Content.IntegrationTests.Tests
|
|||||||
var protoIds = prototypeMan
|
var protoIds = prototypeMan
|
||||||
.EnumeratePrototypes<EntityPrototype>()
|
.EnumeratePrototypes<EntityPrototype>()
|
||||||
.Where(p => !p.Abstract)
|
.Where(p => !p.Abstract)
|
||||||
|
.Where(p => !p.Components.ContainsKey("MapGrid")) // This will smash stuff otherwise.
|
||||||
.Select(p => p.ID)
|
.Select(p => p.ID)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
@@ -193,6 +196,10 @@ namespace Content.IntegrationTests.Tests
|
|||||||
"MapGrid",
|
"MapGrid",
|
||||||
"StationData", // errors when removed mid-round
|
"StationData", // errors when removed mid-round
|
||||||
"Actor", // We aren't testing actor components, those need their player session set.
|
"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 = @"
|
var testEntity = @"
|
||||||
@@ -289,6 +296,10 @@ namespace Content.IntegrationTests.Tests
|
|||||||
"MapGrid",
|
"MapGrid",
|
||||||
"StationData", // errors when deleted mid-round
|
"StationData", // errors when deleted mid-round
|
||||||
"Actor", // We aren't testing actor components, those need their player session set.
|
"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 = @"
|
var testEntity = @"
|
||||||
|
|||||||
@@ -86,6 +86,10 @@ public sealed class PrototypeSaveTest
|
|||||||
if (prototype.Abstract)
|
if (prototype.Abstract)
|
||||||
continue;
|
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.
|
// Currently mobs and such can't be serialized, but they aren't flagged as serializable anyways.
|
||||||
if (!prototype.MapSavable)
|
if (!prototype.MapSavable)
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ using Content.Server.Preferences.Managers;
|
|||||||
using Content.Server.ServerInfo;
|
using Content.Server.ServerInfo;
|
||||||
using Content.Server.ServerUpdates;
|
using Content.Server.ServerUpdates;
|
||||||
using Content.Server.Voting.Managers;
|
using Content.Server.Voting.Managers;
|
||||||
|
using Content.Server.Worldgen.Tools;
|
||||||
using Content.Shared.Administration;
|
using Content.Shared.Administration;
|
||||||
using Content.Shared.Administration.Logs;
|
using Content.Shared.Administration.Logs;
|
||||||
using Content.Shared.Administration.Managers;
|
using Content.Shared.Administration.Managers;
|
||||||
@@ -58,6 +59,7 @@ namespace Content.Server.IoC
|
|||||||
IoCManager.Register<PlayTimeTrackingManager>();
|
IoCManager.Register<PlayTimeTrackingManager>();
|
||||||
IoCManager.Register<UserDbDataManager>();
|
IoCManager.Register<UserDbDataManager>();
|
||||||
IoCManager.Register<ServerInfoManager>();
|
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>
|
/// </summary>
|
||||||
public static readonly CVarDef<bool> ConfigPresetDebug =
|
public static readonly CVarDef<bool> ConfigPresetDebug =
|
||||||
CVarDef.Create("config.preset_debug", true, CVar.SERVERONLY);
|
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-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-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-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-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-station-id-invalid = Argument {$index} must be a valid station id!
|
||||||
shell-argument-map-id-invalid = Argument {$index} must be a valid map 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
|
- key: enum.ShuttleConsoleUiKey.Key
|
||||||
type: ShuttleConsoleBoundUserInterface
|
type: ShuttleConsoleBoundUserInterface
|
||||||
- type: RadarConsole
|
- type: RadarConsole
|
||||||
|
- type: WorldLoader
|
||||||
|
radius: 256
|
||||||
- type: PointLight
|
- type: PointLight
|
||||||
radius: 1.5
|
radius: 1.5
|
||||||
energy: 1.6
|
energy: 1.6
|
||||||
@@ -102,6 +104,8 @@
|
|||||||
- Syndicate
|
- Syndicate
|
||||||
- type: RadarConsole
|
- type: RadarConsole
|
||||||
maxRange: 1536
|
maxRange: 1536
|
||||||
|
- type: WorldLoader
|
||||||
|
radius: 1536
|
||||||
- type: PointLight
|
- type: PointLight
|
||||||
radius: 1.5
|
radius: 1.5
|
||||||
energy: 1.6
|
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