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:
Moony
2023-05-16 06:36:45 -05:00
committed by GitHub
parent cdb46778dc
commit e91fc652a3
54 changed files with 2748 additions and 1 deletions

View 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; }
}
}

View 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;
}
}