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:
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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user