]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
VGRoid support (#27659)
authormetalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Wed, 3 Jul 2024 12:23:11 +0000 (22:23 +1000)
committerGitHub <noreply@github.com>
Wed, 3 Jul 2024 12:23:11 +0000 (22:23 +1000)
* Dungeon spawn support for grid spawns

* Recursive dungeons working

* Mask approach working

* zack

* More work

* Fix recursive dungeons

* Heap of work

* weh

* the cud

* rar

* Job

* weh

* weh

* weh

* Master merges

* orch

* weh

* vgroid most of the work

* Tweaks

* Tweaks

* weh

* do do do do do do

* Basic layout

* Ore spawning working

* Big breaking changes

* Mob gen working

* weh

* Finalising

* emo

* More finalising

* reverty

* Reduce distance

103 files changed:
Content.Server/NPC/Pathfinding/PathfindingSystem.Breadth.cs [new file with mode: 0644]
Content.Server/NPC/Pathfinding/PathfindingSystem.Line.cs [new file with mode: 0644]
Content.Server/NPC/Pathfinding/PathfindingSystem.Simple.cs [new file with mode: 0644]
Content.Server/NPC/Pathfinding/PathfindingSystem.Splines.cs [new file with mode: 0644]
Content.Server/NPC/Pathfinding/PathfindingSystem.Widen.cs [new file with mode: 0644]
Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs
Content.Server/Procedural/DungeonJob.PostGen.cs [deleted file]
Content.Server/Procedural/DungeonJob.PostGenBiome.cs [deleted file]
Content.Server/Procedural/DungeonJob.cs [deleted file]
Content.Server/Procedural/DungeonJob/DungeonJob.DunGenExterior.cs [new file with mode: 0644]
Content.Server/Procedural/DungeonJob/DungeonJob.DunGenFill.cs [new file with mode: 0644]
Content.Server/Procedural/DungeonJob/DungeonJob.DunGenNoise.cs [moved from Content.Server/Procedural/DungeonJob.NoiseDunGen.cs with 73% similarity]
Content.Server/Procedural/DungeonJob/DungeonJob.DunGenNoiseDistance.cs [new file with mode: 0644]
Content.Server/Procedural/DungeonJob/DungeonJob.DunGenPrefab.cs [moved from Content.Server/Procedural/DungeonJob.PrefabDunGen.cs with 82% similarity]
Content.Server/Procedural/DungeonJob/DungeonJob.DunGenReplaceTile.cs [new file with mode: 0644]
Content.Server/Procedural/DungeonJob/DungeonJob.MobDunGen.cs [new file with mode: 0644]
Content.Server/Procedural/DungeonJob/DungeonJob.OreDunGen.cs [new file with mode: 0644]
Content.Server/Procedural/DungeonJob/DungeonJob.PostGen.cs [new file with mode: 0644]
Content.Server/Procedural/DungeonJob/DungeonJob.PostGenAutoCabling.cs [new file with mode: 0644]
Content.Server/Procedural/DungeonJob/DungeonJob.PostGenBiome.cs [new file with mode: 0644]
Content.Server/Procedural/DungeonJob/DungeonJob.PostGenBiomeMarkerLayer.cs [new file with mode: 0644]
Content.Server/Procedural/DungeonJob/DungeonJob.PostGenBoundaryWall.cs [new file with mode: 0644]
Content.Server/Procedural/DungeonJob/DungeonJob.PostGenCornerClutter.cs [new file with mode: 0644]
Content.Server/Procedural/DungeonJob/DungeonJob.PostGenCorridor.cs [new file with mode: 0644]
Content.Server/Procedural/DungeonJob/DungeonJob.PostGenCorridorClutter.cs [moved from Content.Server/Procedural/DungeonJob.CorridorClutterPost.cs with 83% similarity]
Content.Server/Procedural/DungeonJob/DungeonJob.PostGenCorridorDecalSkirting.cs [new file with mode: 0644]
Content.Server/Procedural/DungeonJob/DungeonJob.PostGenDungeonConnector.cs [new file with mode: 0644]
Content.Server/Procedural/DungeonJob/DungeonJob.PostGenDungeonEntrance.cs [new file with mode: 0644]
Content.Server/Procedural/DungeonJob/DungeonJob.PostGenEntranceFlank.cs [new file with mode: 0644]
Content.Server/Procedural/DungeonJob/DungeonJob.PostGenExternalWindow.cs [new file with mode: 0644]
Content.Server/Procedural/DungeonJob/DungeonJob.PostGenInternalWindow.cs [new file with mode: 0644]
Content.Server/Procedural/DungeonJob/DungeonJob.PostGenJunction.cs [new file with mode: 0644]
Content.Server/Procedural/DungeonJob/DungeonJob.PostGenMiddleConnection.cs [new file with mode: 0644]
Content.Server/Procedural/DungeonJob/DungeonJob.PostGenRoomEntrance.cs [new file with mode: 0644]
Content.Server/Procedural/DungeonJob/DungeonJob.PostGenSplineDungeonConnector.cs [new file with mode: 0644]
Content.Server/Procedural/DungeonJob/DungeonJob.PostGenWallMount.cs [new file with mode: 0644]
Content.Server/Procedural/DungeonJob/DungeonJob.PostGenWorm.cs [moved from Content.Server/Procedural/DungeonJob.WormPost.cs with 88% similarity]
Content.Server/Procedural/DungeonJob/DungeonJob.cs [new file with mode: 0644]
Content.Server/Procedural/DungeonSystem.Commands.cs
Content.Server/Procedural/DungeonSystem.Rooms.cs
Content.Server/Procedural/DungeonSystem.cs
Content.Server/Procedural/RoomFillSystem.cs
Content.Server/Salvage/SpawnSalvageMissionJob.cs
Content.Server/Shuttles/Components/GridSpawnComponent.cs
Content.Server/Shuttles/Systems/ShuttleSystem.GridFill.cs
Content.Server/Shuttles/Systems/ShuttleSystem.cs
Content.Shared/Procedural/Components/EntityRemapComponent.cs [new file with mode: 0644]
Content.Shared/Procedural/Distance/DunGenEuclideanSquaredDistance.cs [new file with mode: 0644]
Content.Shared/Procedural/Distance/DunGenSquareBump.cs [new file with mode: 0644]
Content.Shared/Procedural/Distance/IDunGenDistance.cs [new file with mode: 0644]
Content.Shared/Procedural/Dungeon.cs
Content.Shared/Procedural/DungeonConfigPrototype.cs
Content.Shared/Procedural/DungeonData.cs [new file with mode: 0644]
Content.Shared/Procedural/DungeonGenerators/ExteriorDunGen.cs [new file with mode: 0644]
Content.Shared/Procedural/DungeonGenerators/FillGridDunGen.cs [new file with mode: 0644]
Content.Shared/Procedural/DungeonGenerators/IDunGen.cs [deleted file]
Content.Shared/Procedural/DungeonGenerators/NoiseDistanceDunGen.cs [new file with mode: 0644]
Content.Shared/Procedural/DungeonGenerators/NoiseDunGen.cs
Content.Shared/Procedural/DungeonGenerators/PrefabDunGen.cs
Content.Shared/Procedural/DungeonGenerators/PrototypeDunGen.cs [new file with mode: 0644]
Content.Shared/Procedural/DungeonGenerators/ReplaceTileDunGen.cs [new file with mode: 0644]
Content.Shared/Procedural/DungeonLayers/MobsDunGen.cs [new file with mode: 0644]
Content.Shared/Procedural/DungeonLayers/OreDunGen.cs [new file with mode: 0644]
Content.Shared/Procedural/DungeonRoom.cs
Content.Shared/Procedural/IDunGenLayer.cs [new file with mode: 0644]
Content.Shared/Procedural/PostGeneration/AutoCablingDunGen.cs [new file with mode: 0644]
Content.Shared/Procedural/PostGeneration/AutoCablingPostGen.cs [deleted file]
Content.Shared/Procedural/PostGeneration/BiomeDunGen.cs [moved from Content.Shared/Procedural/PostGeneration/BiomePostGen.cs with 78% similarity]
Content.Shared/Procedural/PostGeneration/BiomeMarkerLayerDunGen.cs [moved from Content.Shared/Procedural/PostGeneration/BiomeMarkerLayerPostGen.cs with 73% similarity]
Content.Shared/Procedural/PostGeneration/BoundaryWallDunGen.cs [new file with mode: 0644]
Content.Shared/Procedural/PostGeneration/BoundaryWallPostGen.cs [deleted file]
Content.Shared/Procedural/PostGeneration/CornerClutterDunGen.cs [new file with mode: 0644]
Content.Shared/Procedural/PostGeneration/CornerClutterPostGen.cs [deleted file]
Content.Shared/Procedural/PostGeneration/CorridorClutterDunGen.cs [moved from Content.Shared/Procedural/PostGeneration/CorridorClutterPostGen.cs with 85% similarity]
Content.Shared/Procedural/PostGeneration/CorridorDecalSkirtingDunGen.cs [moved from Content.Shared/Procedural/PostGeneration/CorridorDecalSkirtingPostGen.cs with 72% similarity]
Content.Shared/Procedural/PostGeneration/CorridorDunGen.cs [moved from Content.Shared/Procedural/PostGeneration/CorridorPostGen.cs with 73% similarity]
Content.Shared/Procedural/PostGeneration/DungeonEntranceDunGen.cs [new file with mode: 0644]
Content.Shared/Procedural/PostGeneration/DungeonEntrancePostGen.cs [deleted file]
Content.Shared/Procedural/PostGeneration/EntranceFlankDunGen.cs [new file with mode: 0644]
Content.Shared/Procedural/PostGeneration/EntranceFlankPostGen.cs [deleted file]
Content.Shared/Procedural/PostGeneration/ExternalWindowDunGen.cs [new file with mode: 0644]
Content.Shared/Procedural/PostGeneration/ExternalWindowPostGen.cs [deleted file]
Content.Shared/Procedural/PostGeneration/IPostDunGen.cs [deleted file]
Content.Shared/Procedural/PostGeneration/InternalWindowDunGen.cs [new file with mode: 0644]
Content.Shared/Procedural/PostGeneration/InternalWindowPostGen.cs [deleted file]
Content.Shared/Procedural/PostGeneration/JunctionDunGen.cs [new file with mode: 0644]
Content.Shared/Procedural/PostGeneration/JunctionPostGen.cs [deleted file]
Content.Shared/Procedural/PostGeneration/MiddleConnectionDunGen.cs [new file with mode: 0644]
Content.Shared/Procedural/PostGeneration/MiddleConnectionPostGen.cs [deleted file]
Content.Shared/Procedural/PostGeneration/RoomEntranceDunGen.cs [new file with mode: 0644]
Content.Shared/Procedural/PostGeneration/RoomEntrancePostGen.cs [deleted file]
Content.Shared/Procedural/PostGeneration/SplineDungeonConnectorDunGen.cs [new file with mode: 0644]
Content.Shared/Procedural/PostGeneration/WallMountDunGen.cs [new file with mode: 0644]
Content.Shared/Procedural/PostGeneration/WallMountPostGen.cs [deleted file]
Content.Shared/Procedural/PostGeneration/WormCorridorDunGen.cs [moved from Content.Shared/Procedural/PostGeneration/WormCorridorPostGen.cs with 73% similarity]
Content.Shared/Salvage/SharedSalvageSystem.Magnet.cs
Content.Shared/Shuttles/Systems/SharedShuttleSystem.cs
Content.Shared/Storage/EntitySpawnEntry.cs
Resources/Prototypes/Entities/Stations/base.yml
Resources/Prototypes/Entities/Structures/Walls/asteroid.yml
Resources/Prototypes/Procedural/Magnet/asteroid.yml
Resources/Prototypes/Procedural/dungeon_configs.yml
Resources/Prototypes/Procedural/vgroid.yml [new file with mode: 0644]

diff --git a/Content.Server/NPC/Pathfinding/PathfindingSystem.Breadth.cs b/Content.Server/NPC/Pathfinding/PathfindingSystem.Breadth.cs
new file mode 100644 (file)
index 0000000..ee8eaa9
--- /dev/null
@@ -0,0 +1,123 @@
+namespace Content.Server.NPC.Pathfinding;
+
+public sealed partial class PathfindingSystem
+{
+    /*
+     * Handle BFS searches from Start->End. Doesn't consider NPC pathfinding.
+     */
+
+    /// <summary>
+    /// Pathfinding args for a 1-many path.
+    /// </summary>
+    public record struct BreadthPathArgs()
+    {
+        public Vector2i Start;
+        public List<Vector2i> Ends;
+
+        public bool Diagonals = false;
+
+        public Func<Vector2i, float>? TileCost;
+
+        public int Limit = 10000;
+    }
+
+    /// <summary>
+    /// Gets a BFS path from start to any end. Can also supply an optional tile-cost for tiles.
+    /// </summary>
+    public SimplePathResult GetBreadthPath(BreadthPathArgs args)
+    {
+        var cameFrom = new Dictionary<Vector2i, Vector2i>();
+        var costSoFar = new Dictionary<Vector2i, float>();
+        var frontier = new PriorityQueue<Vector2i, float>();
+
+        costSoFar[args.Start] = 0f;
+        frontier.Enqueue(args.Start, 0f);
+        var count = 0;
+
+        while (frontier.TryDequeue(out var node, out _) && count < args.Limit)
+        {
+            count++;
+
+            if (args.Ends.Contains(node))
+            {
+                // Found target
+                var path = ReconstructPath(node, cameFrom);
+
+                return new SimplePathResult()
+                {
+                    CameFrom = cameFrom,
+                    Path = path,
+                };
+            }
+
+            var gCost = costSoFar[node];
+
+            if (args.Diagonals)
+            {
+                for (var x = -1; x <= 1; x++)
+                {
+                    for (var y = -1; y <= 1; y++)
+                    {
+                        var neighbor = node + new Vector2i(x, y);
+                        var neighborCost = OctileDistance(node, neighbor) * args.TileCost?.Invoke(neighbor) ?? 1f;
+
+                        if (neighborCost.Equals(0f))
+                        {
+                            continue;
+                        }
+
+                        // f = g + h
+                        // gScore is distance to the start node
+                        // hScore is distance to the end node
+                        var gScore = gCost + neighborCost;
+
+                        // Slower to get here so just ignore it.
+                        if (costSoFar.TryGetValue(neighbor, out var nextValue) && gScore >= nextValue)
+                        {
+                            continue;
+                        }
+
+                        cameFrom[neighbor] = node;
+                        costSoFar[neighbor] = gScore;
+                        // pFactor is tie-breaker where the fscore is otherwise equal.
+                        // See http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html#breaking-ties
+                        // There's other ways to do it but future consideration
+                        // The closer the fScore is to the actual distance then the better the pathfinder will be
+                        // (i.e. somewhere between 1 and infinite)
+                        // Can use hierarchical pathfinder or whatever to improve the heuristic but this is fine for now.
+                        frontier.Enqueue(neighbor, gScore);
+                    }
+                }
+            }
+            else
+            {
+                for (var x = -1; x <= 1; x++)
+                {
+                    for (var y = -1; y <= 1; y++)
+                    {
+                        if (x != 0 && y != 0)
+                            continue;
+
+                        var neighbor = node + new Vector2i(x, y);
+                        var neighborCost = ManhattanDistance(node, neighbor) * args.TileCost?.Invoke(neighbor) ?? 1f;
+
+                        if (neighborCost.Equals(0f))
+                            continue;
+
+                        var gScore = gCost + neighborCost;
+
+                        if (costSoFar.TryGetValue(neighbor, out var nextValue) && gScore >= nextValue)
+                            continue;
+
+                        cameFrom[neighbor] = node;
+                        costSoFar[neighbor] = gScore;
+
+                        frontier.Enqueue(neighbor, gScore);
+                    }
+                }
+            }
+        }
+
+        return SimplePathResult.NoPath;
+    }
+}
diff --git a/Content.Server/NPC/Pathfinding/PathfindingSystem.Line.cs b/Content.Server/NPC/Pathfinding/PathfindingSystem.Line.cs
new file mode 100644 (file)
index 0000000..479d5ad
--- /dev/null
@@ -0,0 +1,74 @@
+namespace Content.Server.NPC.Pathfinding;
+
+public sealed partial class PathfindingSystem
+{
+    public void GridCast(Vector2i start, Vector2i end, Vector2iCallback callback)
+    {
+        // https://gist.github.com/Pyr3z/46884d67641094d6cf353358566db566
+        // declare all locals at the top so it's obvious how big the footprint is
+        int dx, dy, xinc, yinc, side, i, error;
+
+        // starting cell is always returned
+        if (!callback(start))
+            return;
+
+        xinc  = (end.X < start.X) ? -1 : 1;
+        yinc  = (end.Y < start.Y) ? -1 : 1;
+        dx    = xinc * (end.X - start.X);
+        dy    = yinc * (end.Y - start.Y);
+        var ax = start.X;
+        var ay = start.Y;
+
+        if (dx == dy) // Handle perfect diagonals
+        {
+            // I include this "optimization" for more aesthetic reasons, actually.
+            // While Bresenham's Line can handle perfect diagonals just fine, it adds
+            // additional cells to the line that make it not a perfect diagonal
+            // anymore. So, while this branch is ~twice as fast as the next branch,
+            // the real reason it is here is for style.
+
+            // Also, there *is* the reason of performance. If used for cell-based
+            // raycasts, for example, then perfect diagonals will check half as many
+            // cells.
+
+            while (dx --> 0)
+            {
+                ax += xinc;
+                ay += yinc;
+                if (!callback(new Vector2i(ax, ay)))
+                    return;
+            }
+
+            return;
+        }
+
+        // Handle all other lines
+
+        side = -1 * ((dx == 0 ? yinc : xinc) - 1);
+
+        i     = dx + dy;
+        error = dx - dy;
+
+        dx *= 2;
+        dy *= 2;
+
+        while (i --> 0)
+        {
+            if (error > 0 || error == side)
+            {
+                ax    += xinc;
+                error -= dy;
+            }
+            else
+            {
+                ay    += yinc;
+                error += dx;
+            }
+
+            if (!callback(new Vector2i(ax, ay)))
+                return;
+        }
+    }
+
+    public delegate bool Vector2iCallback(Vector2i index);
+}
diff --git a/Content.Server/NPC/Pathfinding/PathfindingSystem.Simple.cs b/Content.Server/NPC/Pathfinding/PathfindingSystem.Simple.cs
new file mode 100644 (file)
index 0000000..7afd3d7
--- /dev/null
@@ -0,0 +1,154 @@
+namespace Content.Server.NPC.Pathfinding;
+
+public sealed partial class PathfindingSystem
+{
+    /// <summary>
+    /// Pathfinding args for a 1-1 path.
+    /// </summary>
+    public record struct SimplePathArgs()
+    {
+        public Vector2i Start;
+        public Vector2i End;
+
+        public bool Diagonals = false;
+
+        public int Limit = 10000;
+
+        /// <summary>
+        /// Custom tile-costs if applicable.
+        /// </summary>
+        public Func<Vector2i, float>? TileCost;
+    }
+
+    public record struct SimplePathResult
+    {
+        public static SimplePathResult NoPath = new();
+
+        public List<Vector2i> Path;
+        public Dictionary<Vector2i, Vector2i> CameFrom;
+    }
+
+    /// <summary>
+    /// Gets simple A* path from start to end. Can also supply an optional tile-cost for tiles.
+    /// </summary>
+    public SimplePathResult GetPath(SimplePathArgs args)
+    {
+        var cameFrom = new Dictionary<Vector2i, Vector2i>();
+        var costSoFar = new Dictionary<Vector2i, float>();
+        var frontier = new PriorityQueue<Vector2i, float>();
+
+        costSoFar[args.Start] = 0f;
+        frontier.Enqueue(args.Start, 0f);
+        var count = 0;
+
+        while (frontier.TryDequeue(out var node, out _) && count < args.Limit)
+        {
+            count++;
+
+            if (node == args.End)
+            {
+                // Found target
+                var path = ReconstructPath(args.End, cameFrom);
+
+                return new SimplePathResult()
+                {
+                    CameFrom = cameFrom,
+                    Path = path,
+                };
+            }
+
+            var gCost = costSoFar[node];
+
+            if (args.Diagonals)
+            {
+                for (var x = -1; x <= 1; x++)
+                {
+                    for (var y = -1; y <= 1; y++)
+                    {
+                        var neighbor = node + new Vector2i(x, y);
+                        var neighborCost = OctileDistance(node, neighbor) * args.TileCost?.Invoke(neighbor) ?? 1f;
+
+                        if (neighborCost.Equals(0f))
+                        {
+                            continue;
+                        }
+
+                        // f = g + h
+                        // gScore is distance to the start node
+                        // hScore is distance to the end node
+                        var gScore = gCost + neighborCost;
+
+                        // Slower to get here so just ignore it.
+                        if (costSoFar.TryGetValue(neighbor, out var nextValue) && gScore >= nextValue)
+                        {
+                            continue;
+                        }
+
+                        cameFrom[neighbor] = node;
+                        costSoFar[neighbor] = gScore;
+                        // pFactor is tie-breaker where the fscore is otherwise equal.
+                        // See http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html#breaking-ties
+                        // There's other ways to do it but future consideration
+                        // The closer the fScore is to the actual distance then the better the pathfinder will be
+                        // (i.e. somewhere between 1 and infinite)
+                        // Can use hierarchical pathfinder or whatever to improve the heuristic but this is fine for now.
+                        var hScore = OctileDistance(args.End, neighbor) * (1.0f + 1.0f / 1000.0f);
+                        var fScore = gScore + hScore;
+                        frontier.Enqueue(neighbor, fScore);
+                    }
+                }
+            }
+            else
+            {
+                for (var x = -1; x <= 1; x++)
+                {
+                    for (var y = -1; y <= 1; y++)
+                    {
+                        if (x != 0 && y != 0)
+                            continue;
+
+                        var neighbor = node + new Vector2i(x, y);
+                        var neighborCost = ManhattanDistance(node, neighbor) * args.TileCost?.Invoke(neighbor) ?? 1f;
+
+                        if (neighborCost.Equals(0f))
+                            continue;
+
+                        var gScore = gCost + neighborCost;
+
+                        if (costSoFar.TryGetValue(neighbor, out var nextValue) && gScore >= nextValue)
+                            continue;
+
+                        cameFrom[neighbor] = node;
+                        costSoFar[neighbor] = gScore;
+
+                        // Still use octile even for manhattan distance.
+                        var hScore = OctileDistance(args.End, neighbor) * 1.001f;
+                        var fScore = gScore + hScore;
+                        frontier.Enqueue(neighbor, fScore);
+                    }
+                }
+            }
+        }
+
+        return SimplePathResult.NoPath;
+    }
+
+    private List<Vector2i> ReconstructPath(Vector2i end, Dictionary<Vector2i, Vector2i> cameFrom)
+    {
+        var path = new List<Vector2i>()
+        {
+            end,
+        };
+        var node = end;
+
+        while (cameFrom.TryGetValue(node, out var source))
+        {
+            path.Add(source);
+            node = source;
+        }
+
+        path.Reverse();
+
+        return path;
+    }
+}
diff --git a/Content.Server/NPC/Pathfinding/PathfindingSystem.Splines.cs b/Content.Server/NPC/Pathfinding/PathfindingSystem.Splines.cs
new file mode 100644 (file)
index 0000000..9979755
--- /dev/null
@@ -0,0 +1,180 @@
+using Robust.Shared.Collections;
+using Robust.Shared.Random;
+
+namespace Content.Server.NPC.Pathfinding;
+
+public sealed partial class PathfindingSystem
+{
+    public record struct SimplifyPathArgs
+    {
+        public Vector2i Start;
+        public Vector2i End;
+        public List<Vector2i> Path;
+    }
+
+    public record struct SplinePathResult()
+    {
+        public static SplinePathResult NoPath = new();
+
+        public List<Vector2i> Points = new();
+
+        public List<Vector2i> Path = new();
+        public Dictionary<Vector2i, Vector2i> CameFrom;
+    }
+
+    public record struct SplinePathArgs(SimplePathArgs Args)
+    {
+        public SimplePathArgs Args = Args;
+
+        public float MaxRatio = 0.25f;
+
+        /// <summary>
+        /// Minimum distance between subdivisions.
+        /// </summary>
+        public int Distance = 20;
+    }
+
+    /// <summary>
+    /// Gets a spline path from start to end.
+    /// </summary>
+    public SplinePathResult GetSplinePath(SplinePathArgs args, Random random)
+    {
+        var start = args.Args.Start;
+        var end = args.Args.End;
+
+        var path = new List<Vector2i>();
+
+        var pairs = new ValueList<(Vector2i Start, Vector2i End)> { (start, end) };
+        var subdivided = true;
+
+        // Sub-divide recursively
+        while (subdivided)
+        {
+            // Sometimes we might inadvertantly get 2 nodes too close together so better to just check each one as it comes up instead.
+            var i = 0;
+            subdivided = false;
+
+            while (i < pairs.Count)
+            {
+                var pointA = pairs[i].Start;
+                var pointB = pairs[i].End;
+                var vector = pointB - pointA;
+
+                var halfway = vector / 2f;
+
+                // Finding the point
+                var adj = halfway.Length();
+
+                // Should we even subdivide.
+                if (adj <= args.Distance)
+                {
+                    // Just check the next entry no double skip.
+                    i++;
+                    continue;
+                }
+
+                subdivided = true;
+                var opposite = args.MaxRatio * adj;
+                var hypotenuse = MathF.Sqrt(MathF.Pow(adj, 2) + MathF.Pow(opposite, 2));
+
+                // Okay so essentially we have 2 points and no poly
+                // We add 2 other points to form a diamond and want some point halfway between randomly offset.
+                var angle = new Angle(MathF.Atan(opposite / adj));
+                var pointAPerp = pointA + angle.RotateVec(halfway).Normalized() * hypotenuse;
+                var pointBPerp = pointA + (-angle).RotateVec(halfway).Normalized() * hypotenuse;
+
+                var perpLine = pointBPerp - pointAPerp;
+                var perpHalfway = perpLine.Length() / 2f;
+
+                var splinePoint = (pointAPerp + perpLine.Normalized() * random.NextFloat(-args.MaxRatio, args.MaxRatio) * perpHalfway).Floored();
+
+                // We essentially take (A, B) and turn it into (A, C) & (C, B)
+                pairs[i] = (pointA, splinePoint);
+                pairs.Insert(i + 1, (splinePoint, pointB));
+
+                i+= 2;
+            }
+        }
+
+        var spline = new ValueList<Vector2i>(pairs.Count - 1)
+        {
+            start
+        };
+
+        foreach (var pair in pairs)
+        {
+            spline.Add(pair.End);
+        }
+
+        // Now we need to pathfind between each node on the spline.
+
+        // TODO: Add rotation version or straight-line version for pathfinder config
+        // Move the worm pathfinder to here I think.
+        var cameFrom = new Dictionary<Vector2i, Vector2i>();
+
+        // TODO: Need to get rid of the branch bullshit.
+        var points = new List<Vector2i>();
+
+        for (var i = 0; i < spline.Count - 1; i++)
+        {
+            var point = spline[i];
+            var target = spline[i + 1];
+            points.Add(point);
+            var aStarArgs = args.Args with { Start = point, End = target };
+
+            var aStarResult = GetPath(aStarArgs);
+
+            if (aStarResult == SimplePathResult.NoPath)
+                return SplinePathResult.NoPath;
+
+            path.AddRange(aStarResult.Path[0..]);
+
+            foreach (var a in aStarResult.CameFrom)
+            {
+                cameFrom[a.Key] = a.Value;
+            }
+        }
+
+        points.Add(spline[^1]);
+
+        var simple = SimplifyPath(new SimplifyPathArgs()
+        {
+            Start = args.Args.Start,
+            End = args.Args.End,
+            Path = path,
+        });
+
+        return new SplinePathResult()
+        {
+            Path = simple,
+            CameFrom = cameFrom,
+            Points = points,
+        };
+    }
+
+    /// <summary>
+    /// Does a simpler pathfinder over the nodes to prune unnecessary branches.
+    /// </summary>
+    public List<Vector2i> SimplifyPath(SimplifyPathArgs args)
+    {
+        var nodes = new HashSet<Vector2i>(args.Path);
+
+        var result = GetBreadthPath(new BreadthPathArgs()
+        {
+            Start = args.Start,
+            Ends = new List<Vector2i>()
+            {
+                args.End,
+            },
+            TileCost = node =>
+            {
+                if (!nodes.Contains(node))
+                    return 0f;
+
+                return 1f;
+            }
+        });
+
+        return result.Path;
+    }
+}
diff --git a/Content.Server/NPC/Pathfinding/PathfindingSystem.Widen.cs b/Content.Server/NPC/Pathfinding/PathfindingSystem.Widen.cs
new file mode 100644 (file)
index 0000000..f7bcd01
--- /dev/null
@@ -0,0 +1,89 @@
+using System.Numerics;
+using Robust.Shared.Random;
+
+namespace Content.Server.NPC.Pathfinding;
+
+public sealed partial class PathfindingSystem
+{
+    /// <summary>
+    /// Widens the path by the specified amount.
+    /// </summary>
+    public HashSet<Vector2i> GetWiden(WidenArgs args, Random random)
+    {
+        var tiles = new HashSet<Vector2i>(args.Path.Count * 2);
+        var variance = (args.MaxWiden - args.MinWiden) / 2f + args.MinWiden;
+        var counter = 0;
+
+        foreach (var tile in args.Path)
+        {
+            counter++;
+
+            if (counter != args.TileSkip)
+                continue;
+
+            counter = 0;
+
+            var center = new Vector2(tile.X + 0.5f, tile.Y + 0.5f);
+
+            if (args.Square)
+            {
+                for (var x = -variance; x <= variance; x++)
+                {
+                    for (var y = -variance; y <= variance; y++)
+                    {
+                        var neighbor = center + new Vector2(x, y);
+
+                        tiles.Add(neighbor.Floored());
+                    }
+                }
+            }
+            else
+            {
+                for (var x = -variance; x <= variance; x++)
+                {
+                    for (var y = -variance; y <= variance; y++)
+                    {
+                        var offset = new Vector2(x, y);
+
+                        if (offset.Length() > variance)
+                            continue;
+
+                        var neighbor = center + offset;
+
+                        tiles.Add(neighbor.Floored());
+                    }
+                }
+            }
+
+            variance += random.NextFloat(-args.Variance * args.TileSkip, args.Variance * args.TileSkip);
+            variance = Math.Clamp(variance, args.MinWiden, args.MaxWiden);
+        }
+
+        return tiles;
+    }
+
+    public record struct WidenArgs()
+    {
+        public bool Square = false;
+
+        /// <summary>
+        /// How many tiles to skip between iterations., 1-in-n
+        /// </summary>
+        public int TileSkip = 3;
+
+        /// <summary>
+        /// Maximum amount to vary per tile.
+        /// </summary>
+        public float Variance = 0.25f;
+
+        /// <summary>
+        /// Minimum width.
+        /// </summary>
+        public float MinWiden = 2f;
+
+
+        public float MaxWiden = 7f;
+
+        public List<Vector2i> Path;
+    }
+}
index 5f871a6ecfa0b3d10f90c9c205bcd94ace9ab2b7..e0bcb97a112c2cac01e85c172e60b716d44852ee 100644 (file)
@@ -142,6 +142,13 @@ public sealed partial class NPCSteeringSystem
 
         // Grab the target position, either the next path node or our end goal..
         var targetCoordinates = GetTargetCoordinates(steering);
+
+        if (!targetCoordinates.IsValid(EntityManager))
+        {
+            steering.Status = SteeringStatus.NoPath;
+            return false;
+        }
+
         var needsPath = false;
 
         // If the next node is invalid then get new ones
@@ -243,6 +250,14 @@ public sealed partial class NPCSteeringSystem
                 // Alright just adjust slightly and grab the next node so we don't stop moving for a tick.
                 // TODO: If it's the last node just grab the target instead.
                 targetCoordinates = GetTargetCoordinates(steering);
+
+                if (!targetCoordinates.IsValid(EntityManager))
+                {
+                    SetDirection(mover, steering, Vector2.Zero);
+                    steering.Status = SteeringStatus.NoPath;
+                    return false;
+                }
+
                 targetMap = targetCoordinates.ToMap(EntityManager, _transform);
 
                 // Can't make it again.
diff --git a/Content.Server/Procedural/DungeonJob.PostGen.cs b/Content.Server/Procedural/DungeonJob.PostGen.cs
deleted file mode 100644 (file)
index cb9e64f..0000000
+++ /dev/null
@@ -1,1258 +0,0 @@
-using System.Linq;
-using System.Numerics;
-using System.Threading.Tasks;
-using Content.Server.NodeContainer;
-using Content.Shared.Doors.Components;
-using Content.Shared.Maps;
-using Content.Shared.Physics;
-using Content.Shared.Procedural;
-using Content.Shared.Procedural.PostGeneration;
-using Content.Shared.Storage;
-using Content.Shared.Tag;
-using Robust.Shared.Collections;
-using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
-using Robust.Shared.Physics.Components;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Random;
-using Robust.Shared.Utility;
-
-namespace Content.Server.Procedural;
-
-public sealed partial class DungeonJob
-{
-    /*
-     * Run after the main dungeon generation
-     */
-
-    private static readonly ProtoId<TagPrototype> WallTag = "Wall";
-
-    private bool HasWall(MapGridComponent grid, Vector2i tile)
-    {
-        var anchored = _maps.GetAnchoredEntitiesEnumerator(_gridUid, _grid, tile);
-
-        while (anchored.MoveNext(out var uid))
-        {
-            if (_tag.HasTag(uid.Value, WallTag))
-                return true;
-        }
-
-        return false;
-    }
-
-    private async Task PostGen(AutoCablingPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid,
-        Random random)
-    {
-        // There's a lot of ways you could do this.
-        // For now we'll just connect every LV cable in the dungeon.
-        var cableTiles = new HashSet<Vector2i>();
-        var allTiles = new HashSet<Vector2i>(dungeon.CorridorTiles);
-        allTiles.UnionWith(dungeon.RoomTiles);
-        allTiles.UnionWith(dungeon.RoomExteriorTiles);
-        allTiles.UnionWith(dungeon.CorridorExteriorTiles);
-        var nodeQuery = _entManager.GetEntityQuery<NodeContainerComponent>();
-
-        // Gather existing nodes
-        foreach (var tile in allTiles)
-        {
-            var anchored = _maps.GetAnchoredEntitiesEnumerator(_gridUid, _grid, tile);
-
-            while (anchored.MoveNext(out var anc))
-            {
-                if (!nodeQuery.TryGetComponent(anc, out var nodeContainer) ||
-                   !nodeContainer.Nodes.ContainsKey("power"))
-                {
-                    continue;
-                }
-
-                cableTiles.Add(tile);
-                break;
-            }
-        }
-
-        // Iterating them all might be expensive.
-        await SuspendIfOutOfTime();
-
-        if (!ValidateResume())
-            return;
-
-        var startNodes = new List<Vector2i>(cableTiles);
-        random.Shuffle(startNodes);
-        var start = startNodes[0];
-        var remaining = new HashSet<Vector2i>(startNodes);
-        var frontier = new PriorityQueue<Vector2i, float>();
-        frontier.Enqueue(start, 0f);
-        var cameFrom = new Dictionary<Vector2i, Vector2i>();
-        var costSoFar = new Dictionary<Vector2i, float>();
-        var lastDirection = new Dictionary<Vector2i, Direction>();
-        costSoFar[start] = 0f;
-        lastDirection[start] = Direction.Invalid;
-
-        while (remaining.Count > 0)
-        {
-            if (frontier.Count == 0)
-            {
-                var newStart = remaining.First();
-                frontier.Enqueue(newStart, 0f);
-                lastDirection[newStart] = Direction.Invalid;
-            }
-
-            var node = frontier.Dequeue();
-
-            if (remaining.Remove(node))
-            {
-                var weh = node;
-
-                while (cameFrom.TryGetValue(weh, out var receiver))
-                {
-                    cableTiles.Add(weh);
-                    weh = receiver;
-
-                    if (weh == start)
-                        break;
-                }
-            }
-
-            if (!grid.TryGetTileRef(node, out var tileRef) || tileRef.Tile.IsEmpty)
-            {
-                continue;
-            }
-
-            for (var i = 0; i < 4; i++)
-            {
-                var dir = (Direction) (i * 2);
-
-                var neighbor = node + dir.ToIntVec();
-                var tileCost = 1f;
-
-                // Prefer straight lines.
-                if (lastDirection[node] != dir)
-                {
-                    tileCost *= 1.1f;
-                }
-
-                if (cableTiles.Contains(neighbor))
-                {
-                    tileCost *= 0.1f;
-                }
-
-                // Prefer tiles without walls on them
-                if (HasWall(grid, neighbor))
-                {
-                    tileCost *= 20f;
-                }
-
-                var gScore = costSoFar[node] + tileCost;
-
-                if (costSoFar.TryGetValue(neighbor, out var nextValue) && gScore >= nextValue)
-                {
-                    continue;
-                }
-
-                cameFrom[neighbor] = node;
-                costSoFar[neighbor] = gScore;
-                lastDirection[neighbor] = dir;
-                frontier.Enqueue(neighbor, gScore);
-            }
-        }
-
-        foreach (var tile in cableTiles)
-        {
-            var anchored = grid.GetAnchoredEntitiesEnumerator(tile);
-            var found = false;
-
-            while (anchored.MoveNext(out var anc))
-            {
-                if (!nodeQuery.TryGetComponent(anc, out var nodeContainer) ||
-                    !nodeContainer.Nodes.ContainsKey("power"))
-                {
-                    continue;
-                }
-
-                found = true;
-                break;
-            }
-
-            if (found)
-                continue;
-
-            _entManager.SpawnEntity(gen.Entity, _grid.GridTileToLocal(tile));
-        }
-    }
-
-    private async Task PostGen(BoundaryWallPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random)
-    {
-        var tileDef = _tileDefManager[gen.Tile];
-        var tiles = new List<(Vector2i Index, Tile Tile)>(dungeon.RoomExteriorTiles.Count);
-
-        // Spawn wall outline
-        // - Tiles first
-        foreach (var neighbor in dungeon.RoomExteriorTiles)
-        {
-            DebugTools.Assert(!dungeon.RoomTiles.Contains(neighbor));
-
-            if (dungeon.Entrances.Contains(neighbor))
-                continue;
-
-            if (!_anchorable.TileFree(grid, neighbor, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
-                continue;
-
-            tiles.Add((neighbor, _tile.GetVariantTile((ContentTileDefinition) tileDef, random)));
-        }
-
-        foreach (var index in dungeon.CorridorExteriorTiles)
-        {
-            if (dungeon.RoomTiles.Contains(index))
-                continue;
-
-            if (!_anchorable.TileFree(grid, index, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
-                continue;
-
-            tiles.Add((index, _tile.GetVariantTile((ContentTileDefinition)tileDef, random)));
-        }
-
-        grid.SetTiles(tiles);
-
-        // Double iteration coz we bulk set tiles for speed.
-        for (var i = 0; i < tiles.Count; i++)
-        {
-            var index = tiles[i];
-            if (!_anchorable.TileFree(grid, index.Index, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
-                continue;
-
-            // If no cardinal neighbors in dungeon then we're a corner.
-            var isCorner = false;
-
-            if (gen.CornerWall != null)
-            {
-                isCorner = true;
-
-                for (var x = -1; x <= 1; x++)
-                {
-                    for (var y = -1; y <= 1; y++)
-                    {
-                        if (x != 0 && y != 0)
-                        {
-                            continue;
-                        }
-
-                        var neighbor = new Vector2i(index.Index.X + x, index.Index.Y + y);
-
-                        if (dungeon.RoomTiles.Contains(neighbor) || dungeon.CorridorTiles.Contains(neighbor))
-                        {
-                            isCorner = false;
-                            break;
-                        }
-                    }
-
-                    if (!isCorner)
-                        break;
-                }
-
-                if (isCorner)
-                    _entManager.SpawnEntity(gen.CornerWall, grid.GridTileToLocal(index.Index));
-            }
-
-            if (!isCorner)
-                _entManager.SpawnEntity(gen.Wall, grid.GridTileToLocal(index.Index));
-
-            if (i % 20 == 0)
-            {
-                await SuspendIfOutOfTime();
-
-                if (!ValidateResume())
-                    return;
-            }
-        }
-    }
-
-    private async Task PostGen(CornerClutterPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid,
-        Random random)
-    {
-        var physicsQuery = _entManager.GetEntityQuery<PhysicsComponent>();
-
-        foreach (var tile in dungeon.CorridorTiles)
-        {
-            var enumerator = _grid.GetAnchoredEntitiesEnumerator(tile);
-            var blocked = false;
-
-            while (enumerator.MoveNext(out var ent))
-            {
-                // TODO: TileFree
-                if (!physicsQuery.TryGetComponent(ent, out var physics) ||
-                    !physics.CanCollide ||
-                    !physics.Hard)
-                {
-                    continue;
-                }
-
-                blocked = true;
-                break;
-            }
-
-            if (blocked)
-                continue;
-
-            // If at least 2 adjacent tiles are blocked consider it a corner
-            for (var i = 0; i < 4; i++)
-            {
-                var dir = (Direction) (i * 2);
-                blocked = HasWall(grid, tile + dir.ToIntVec());
-
-                if (!blocked)
-                    continue;
-
-                var nextDir = (Direction) ((i + 1) * 2 % 8);
-                blocked = HasWall(grid, tile + nextDir.ToIntVec());
-
-                if (!blocked)
-                    continue;
-
-                if (random.Prob(gen.Chance))
-                {
-                    var coords = _grid.GridTileToLocal(tile);
-                    var protos = EntitySpawnCollection.GetSpawns(gen.Contents, random);
-                    _entManager.SpawnEntities(coords, protos);
-                }
-
-                break;
-            }
-        }
-    }
-
-    private async Task PostGen(CorridorDecalSkirtingPostGen decks, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random)
-    {
-        var directions = new ValueList<DirectionFlag>(4);
-        var pocketDirections = new ValueList<Direction>(4);
-        var doorQuery = _entManager.GetEntityQuery<DoorComponent>();
-        var physicsQuery = _entManager.GetEntityQuery<PhysicsComponent>();
-        var offset = -_grid.TileSizeHalfVector;
-        var color = decks.Color;
-
-        foreach (var tile in dungeon.CorridorTiles)
-        {
-            DebugTools.Assert(!dungeon.RoomTiles.Contains(tile));
-            directions.Clear();
-
-            // Do cardinals 1 step
-            // Do corners the other step
-            for (var i = 0; i < 4; i++)
-            {
-                var dir = (DirectionFlag) Math.Pow(2, i);
-                var neighbor = tile + dir.AsDir().ToIntVec();
-
-                var anc = _grid.GetAnchoredEntitiesEnumerator(neighbor);
-
-                while (anc.MoveNext(out var ent))
-                {
-                    if (!physicsQuery.TryGetComponent(ent, out var physics) ||
-                        !physics.CanCollide ||
-                        !physics.Hard ||
-                        doorQuery.HasComponent(ent.Value))
-                    {
-                        continue;
-                    }
-
-                    directions.Add(dir);
-                    break;
-                }
-            }
-
-            // Pockets
-            if (directions.Count == 0)
-            {
-                pocketDirections.Clear();
-
-                for (var i = 1; i < 5; i++)
-                {
-                    var dir = (Direction) (i * 2 - 1);
-                    var neighbor = tile + dir.ToIntVec();
-
-                    var anc = _grid.GetAnchoredEntitiesEnumerator(neighbor);
-
-                    while (anc.MoveNext(out var ent))
-                    {
-                        if (!physicsQuery.TryGetComponent(ent, out var physics) ||
-                            !physics.CanCollide ||
-                            !physics.Hard ||
-                            doorQuery.HasComponent(ent.Value))
-                        {
-                            continue;
-                        }
-
-                        pocketDirections.Add(dir);
-                        break;
-                    }
-                }
-
-                if (pocketDirections.Count == 1)
-                {
-                    if (decks.PocketDecals.TryGetValue(pocketDirections[0], out var cDir))
-                    {
-                        // Decals not being centered biting my ass again
-                        var gridPos = _grid.GridTileToLocal(tile).Offset(offset);
-                        _decals.TryAddDecal(cDir, gridPos, out _, color: color);
-                    }
-                }
-
-                continue;
-            }
-
-            if (directions.Count == 1)
-            {
-                if (decks.CardinalDecals.TryGetValue(directions[0], out var cDir))
-                {
-                    // Decals not being centered biting my ass again
-                    var gridPos = _grid.GridTileToLocal(tile).Offset(offset);
-                    _decals.TryAddDecal(cDir, gridPos, out _, color: color);
-                }
-
-                continue;
-            }
-
-            // Corners
-            if (directions.Count == 2)
-            {
-                // Auehghegueugegegeheh help me
-                var dirFlag = directions[0] | directions[1];
-
-                if (decks.CornerDecals.TryGetValue(dirFlag, out var cDir))
-                {
-                    var gridPos = _grid.GridTileToLocal(tile).Offset(offset);
-                    _decals.TryAddDecal(cDir, gridPos, out _, color: color);
-                }
-            }
-        }
-    }
-
-    private async Task PostGen(DungeonEntrancePostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random)
-    {
-        var rooms = new List<DungeonRoom>(dungeon.Rooms);
-        var roomTiles = new List<Vector2i>();
-        var tileDef = _tileDefManager[gen.Tile];
-
-        for (var i = 0; i < gen.Count; i++)
-        {
-            var roomIndex = random.Next(rooms.Count);
-            var room = rooms[roomIndex];
-
-            // Move out 3 tiles in a direction away from center of the room
-            // If none of those intersect another tile it's probably external
-            // TODO: Maybe need to take top half of furthest rooms in case there's interior exits?
-            roomTiles.AddRange(room.Exterior);
-            random.Shuffle(roomTiles);
-
-            foreach (var tile in roomTiles)
-            {
-                var isValid = false;
-
-                // Check if one side is dungeon and the other side is nothing.
-                for (var j = 0; j < 4; j++)
-                {
-                    var dir = (Direction) (j * 2);
-                    var oppositeDir = dir.GetOpposite();
-                    var dirVec = tile + dir.ToIntVec();
-                    var oppositeDirVec = tile + oppositeDir.ToIntVec();
-
-                    if (!dungeon.RoomTiles.Contains(dirVec))
-                    {
-                        continue;
-                    }
-
-                    if (dungeon.RoomTiles.Contains(oppositeDirVec) ||
-                        dungeon.RoomExteriorTiles.Contains(oppositeDirVec) ||
-                        dungeon.CorridorExteriorTiles.Contains(oppositeDirVec) ||
-                        dungeon.CorridorTiles.Contains(oppositeDirVec))
-                    {
-                        continue;
-                    }
-
-                    // Check if exterior spot free.
-                    if (!_anchorable.TileFree(_grid, tile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
-                    {
-                        continue;
-                    }
-
-                    // Check if interior spot free (no guarantees on exterior but ClearDoor should handle it)
-                    if (!_anchorable.TileFree(_grid, dirVec, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
-                    {
-                        continue;
-                    }
-
-                    // Valid pick!
-                    isValid = true;
-
-                    // Entrance wew
-                    grid.SetTile(tile, _tile.GetVariantTile((ContentTileDefinition) tileDef, random));
-                    ClearDoor(dungeon, grid, tile);
-                    var gridCoords = grid.GridTileToLocal(tile);
-                    // Need to offset the spawn to avoid spawning in the room.
-
-                    _entManager.SpawnEntities(gridCoords, gen.Entities);
-
-                    // Clear out any biome tiles nearby to avoid blocking it
-                    foreach (var nearTile in grid.GetTilesIntersecting(new Circle(gridCoords.Position, 1.5f), false))
-                    {
-                        if (dungeon.RoomTiles.Contains(nearTile.GridIndices) ||
-                            dungeon.RoomExteriorTiles.Contains(nearTile.GridIndices) ||
-                            dungeon.CorridorTiles.Contains(nearTile.GridIndices) ||
-                            dungeon.CorridorExteriorTiles.Contains(nearTile.GridIndices))
-                        {
-                            continue;
-                        }
-
-                        grid.SetTile(nearTile.GridIndices, _tile.GetVariantTile((ContentTileDefinition) tileDef, random));;
-                    }
-
-                    break;
-                }
-
-                if (isValid)
-                    break;
-            }
-
-            roomTiles.Clear();
-        }
-    }
-
-    private async Task PostGen(ExternalWindowPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid,
-        Random random)
-    {
-        // Iterate every tile with N chance to spawn windows on that wall per cardinal dir.
-        var chance = 0.25 / 3f;
-
-        var allExterior = new HashSet<Vector2i>(dungeon.CorridorExteriorTiles);
-        allExterior.UnionWith(dungeon.RoomExteriorTiles);
-        var validTiles = allExterior.ToList();
-        random.Shuffle(validTiles);
-
-        var tiles = new List<(Vector2i, Tile)>();
-        var tileDef = _tileDefManager[gen.Tile];
-        var count = Math.Floor(validTiles.Count * chance);
-        var index = 0;
-        var takenTiles = new HashSet<Vector2i>();
-
-        // There's a bunch of shit here but tl;dr
-        // - don't spawn over cap
-        // - Check if we have 3 tiles in a row that aren't corners and aren't obstructed
-        foreach (var tile in validTiles)
-        {
-            if (index > count)
-                break;
-
-            // Room tile / already used.
-            if (!_anchorable.TileFree(_grid, tile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask) ||
-                takenTiles.Contains(tile))
-            {
-                continue;
-            }
-
-            // Check we're not on a corner
-            for (var i = 0; i < 2; i++)
-            {
-                var dir = (Direction) (i * 2);
-                var dirVec = dir.ToIntVec();
-                var isValid = true;
-
-                // Check 1 beyond either side to ensure it's not a corner.
-                for (var j = -1; j < 4; j++)
-                {
-                    var neighbor = tile + dirVec * j;
-
-                    if (!allExterior.Contains(neighbor) ||
-                        takenTiles.Contains(neighbor) ||
-                        !_anchorable.TileFree(grid, neighbor, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
-                    {
-                        isValid = false;
-                        break;
-                    }
-
-                    // Also check perpendicular that it is free
-                    foreach (var k in new [] {2, 6})
-                    {
-                        var perp = (Direction) ((i * 2 + k) % 8);
-                        var perpVec = perp.ToIntVec();
-                        var perpTile = tile + perpVec;
-
-                        if (allExterior.Contains(perpTile) ||
-                            takenTiles.Contains(neighbor) ||
-                            !_anchorable.TileFree(_grid, perpTile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
-                        {
-                            isValid = false;
-                            break;
-                        }
-                    }
-
-                    if (!isValid)
-                        break;
-                }
-
-                if (!isValid)
-                    continue;
-
-                for (var j = 0; j < 3; j++)
-                {
-                    var neighbor = tile + dirVec * j;
-
-                    tiles.Add((neighbor, _tile.GetVariantTile((ContentTileDefinition) tileDef, random)));
-                    index++;
-                    takenTiles.Add(neighbor);
-                }
-            }
-        }
-
-        grid.SetTiles(tiles);
-        index = 0;
-
-        foreach (var tile in tiles)
-        {
-            var gridPos = grid.GridTileToLocal(tile.Item1);
-
-            index += gen.Entities.Count;
-            _entManager.SpawnEntities(gridPos, gen.Entities);
-
-            if (index > 20)
-            {
-                index -= 20;
-                await SuspendIfOutOfTime();
-
-                if (!ValidateResume())
-                    return;
-            }
-        }
-    }
-
-    /*
-     * You may be wondering why these are different.
-     * It's because for internals we want to force it as it looks nicer and not leave it up to chance.
-     */
-
-    // TODO: Can probably combine these a bit, their differences are in really annoying to pull out spots.
-
-    private async Task PostGen(InternalWindowPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid,
-        Random random)
-    {
-        // Iterate every room and check if there's a gap beyond it that leads to another room within N tiles
-        // If so then consider windows
-        var minDistance = 4;
-        var maxDistance = 6;
-        var tileDef = _tileDefManager[gen.Tile];
-
-        foreach (var room in dungeon.Rooms)
-        {
-            var validTiles = new List<Vector2i>();
-
-            for (var i = 0; i < 4; i++)
-            {
-                var dir = (DirectionFlag) Math.Pow(2, i);
-                var dirVec = dir.AsDir().ToIntVec();
-
-                foreach (var tile in room.Tiles)
-                {
-                    var tileAngle = ((Vector2) tile + grid.TileSizeHalfVector - room.Center).ToAngle();
-                    var roundedAngle = Math.Round(tileAngle.Theta / (Math.PI / 2)) * (Math.PI / 2);
-
-                    var tileVec = (Vector2i) new Angle(roundedAngle).ToVec().Rounded();
-
-                    if (!tileVec.Equals(dirVec))
-                        continue;
-
-                    var valid = false;
-
-                    for (var j = 1; j < maxDistance; j++)
-                    {
-                        var edgeNeighbor = tile + dirVec * j;
-
-                        if (dungeon.RoomTiles.Contains(edgeNeighbor))
-                        {
-                            if (j < minDistance)
-                            {
-                                valid = false;
-                            }
-                            else
-                            {
-                                valid = true;
-                            }
-
-                            break;
-                        }
-                    }
-
-                    if (!valid)
-                        continue;
-
-                    var windowTile = tile + dirVec;
-
-                    if (!_anchorable.TileFree(grid, windowTile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
-                        continue;
-
-                    validTiles.Add(windowTile);
-                }
-
-                validTiles.Sort((x, y) => ((Vector2) x + grid.TileSizeHalfVector - room.Center).LengthSquared().CompareTo((y + grid.TileSizeHalfVector - room.Center).LengthSquared));
-
-                for (var j = 0; j < Math.Min(validTiles.Count, 3); j++)
-                {
-                    var tile = validTiles[j];
-                    var gridPos = grid.GridTileToLocal(tile);
-                    grid.SetTile(tile, _tile.GetVariantTile((ContentTileDefinition) tileDef, random));
-
-                    _entManager.SpawnEntities(gridPos, gen.Entities);
-                }
-
-                if (validTiles.Count > 0)
-                {
-                    await SuspendIfOutOfTime();
-
-                    if (!ValidateResume())
-                        return;
-                }
-
-                validTiles.Clear();
-            }
-        }
-    }
-
-    /// <summary>
-    /// Simply places tiles / entities on the entrances to rooms.
-    /// </summary>
-    private async Task PostGen(RoomEntrancePostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid,
-        Random random)
-    {
-        var setTiles = new List<(Vector2i, Tile)>();
-        var tileDef = _tileDefManager[gen.Tile];
-
-        foreach (var room in dungeon.Rooms)
-        {
-            foreach (var entrance in room.Entrances)
-            {
-                setTiles.Add((entrance, _tile.GetVariantTile((ContentTileDefinition) tileDef, random)));
-            }
-        }
-
-        grid.SetTiles(setTiles);
-
-        foreach (var room in dungeon.Rooms)
-        {
-            foreach (var entrance in room.Entrances)
-            {
-                _entManager.SpawnEntities(grid.GridTileToLocal(entrance), gen.Entities);
-            }
-        }
-    }
-
-    /// <summary>
-    /// Generates corridor connections between entrances to all the rooms.
-    /// </summary>
-    private async Task PostGen(CorridorPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random)
-    {
-        var entrances = new List<Vector2i>(dungeon.Rooms.Count);
-
-        // Grab entrances
-        foreach (var room in dungeon.Rooms)
-        {
-            entrances.AddRange(room.Entrances);
-        }
-
-        var edges = _dungeon.MinimumSpanningTree(entrances, random);
-        await SuspendIfOutOfTime();
-
-        if (!ValidateResume())
-            return;
-
-        // TODO: Add in say 1/3 of edges back in to add some cyclic to it.
-
-        var expansion = gen.Width - 2;
-        // Okay so tl;dr is that we don't want to cut close to rooms as it might go from 3 width to 2 width suddenly
-        // So we will add a buffer range around each room to deter pathfinding there unless necessary
-        var deterredTiles = new HashSet<Vector2i>();
-
-        if (expansion >= 1)
-        {
-            foreach (var tile in dungeon.RoomExteriorTiles)
-            {
-                for (var x = -expansion; x <= expansion; x++)
-                {
-                    for (var y = -expansion; y <= expansion; y++)
-                    {
-                        var neighbor = new Vector2(tile.X + x, tile.Y + y).Floored();
-
-                        if (dungeon.RoomTiles.Contains(neighbor) ||
-                            dungeon.RoomExteriorTiles.Contains(neighbor) ||
-                            entrances.Contains(neighbor))
-                        {
-                            continue;
-                        }
-
-                        deterredTiles.Add(neighbor);
-                    }
-                }
-            }
-        }
-
-        foreach (var room in dungeon.Rooms)
-        {
-            foreach (var entrance in room.Entrances)
-            {
-                // Just so we can still actually get in to the entrance we won't deter from a tile away from it.
-                var normal = (entrance + grid.TileSizeHalfVector - room.Center).ToWorldAngle().GetCardinalDir().ToIntVec();
-                deterredTiles.Remove(entrance + normal);
-            }
-        }
-
-        var excludedTiles = new HashSet<Vector2i>(dungeon.RoomExteriorTiles);
-        excludedTiles.UnionWith(dungeon.RoomTiles);
-        var corridorTiles = new HashSet<Vector2i>();
-
-        _dungeon.GetCorridorNodes(corridorTiles, edges, gen.PathLimit, excludedTiles, tile =>
-        {
-            var mod = 1f;
-
-            if (corridorTiles.Contains(tile))
-            {
-                mod *= 0.1f;
-            }
-
-            if (deterredTiles.Contains(tile))
-            {
-                mod *= 2f;
-            }
-
-            return mod;
-        });
-
-        WidenCorridor(dungeon, gen.Width, corridorTiles);
-
-        var setTiles = new List<(Vector2i, Tile)>();
-        var tileDef = _prototype.Index(gen.Tile);
-
-        foreach (var tile in corridorTiles)
-        {
-            setTiles.Add((tile, _tile.GetVariantTile(tileDef, random)));
-        }
-
-        grid.SetTiles(setTiles);
-        dungeon.CorridorTiles.UnionWith(corridorTiles);
-        BuildCorridorExterior(dungeon);
-    }
-
-    private void BuildCorridorExterior(Dungeon dungeon)
-    {
-        var exterior = dungeon.CorridorExteriorTiles;
-
-        // Just ignore entrances or whatever for now.
-        foreach (var tile in dungeon.CorridorTiles)
-        {
-            for (var x = -1; x <= 1; x++)
-            {
-                for (var y = -1; y <= 1; y++)
-                {
-                    var neighbor = new Vector2i(tile.X + x, tile.Y + y);
-
-                    if (dungeon.CorridorTiles.Contains(neighbor) ||
-                        dungeon.RoomExteriorTiles.Contains(neighbor) ||
-                        dungeon.RoomTiles.Contains(neighbor) ||
-                        dungeon.Entrances.Contains(neighbor))
-                    {
-                        continue;
-                    }
-
-                    exterior.Add(neighbor);
-                }
-            }
-        }
-    }
-
-    private void WidenCorridor(Dungeon dungeon, float width, ICollection<Vector2i> corridorTiles)
-    {
-        var expansion = width - 2;
-
-        // Widen the path
-        if (expansion >= 1)
-        {
-            var toAdd = new ValueList<Vector2i>();
-
-            foreach (var node in corridorTiles)
-            {
-                // Uhhh not sure on the cleanest way to do this but tl;dr we don't want to hug
-                // exterior walls and make the path smaller.
-
-                for (var x = -expansion; x <= expansion; x++)
-                {
-                    for (var y = -expansion; y <= expansion; y++)
-                    {
-                        var neighbor = new Vector2(node.X + x, node.Y + y).Floored();
-
-                        // Diagonals still matter here.
-                        if (dungeon.RoomTiles.Contains(neighbor) ||
-                            dungeon.RoomExteriorTiles.Contains(neighbor))
-                        {
-                            // Try
-
-                            continue;
-                        }
-
-                        toAdd.Add(neighbor);
-                    }
-                }
-            }
-
-            foreach (var node in toAdd)
-            {
-                corridorTiles.Add(node);
-            }
-        }
-    }
-
-    private async Task PostGen(EntranceFlankPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid,
-        Random random)
-    {
-        var tiles = new List<(Vector2i Index, Tile)>();
-        var tileDef = _tileDefManager[gen.Tile];
-        var spawnPositions = new ValueList<Vector2i>(dungeon.Rooms.Count);
-
-        foreach (var room in dungeon.Rooms)
-        {
-            foreach (var entrance in room.Entrances)
-            {
-                for (var i = 0; i < 8; i++)
-                {
-                    var dir = (Direction) i;
-                    var neighbor = entrance + dir.ToIntVec();
-
-                    if (!dungeon.RoomExteriorTiles.Contains(neighbor))
-                        continue;
-
-                    tiles.Add((neighbor, _tile.GetVariantTile((ContentTileDefinition) tileDef, random)));
-                    spawnPositions.Add(neighbor);
-                }
-            }
-        }
-
-        grid.SetTiles(tiles);
-
-        foreach (var entrance in spawnPositions)
-        {
-            _entManager.SpawnEntities(_grid.GridTileToLocal(entrance), gen.Entities);
-        }
-    }
-
-    private async Task PostGen(JunctionPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid,
-        Random random)
-    {
-        var tileDef = _tileDefManager[gen.Tile];
-
-        // N-wide junctions
-        foreach (var tile in dungeon.CorridorTiles)
-        {
-            if (!_anchorable.TileFree(_grid, tile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
-                continue;
-
-            // Check each direction:
-            // - Check if immediate neighbors are free
-            // - Check if the neighbors beyond that are not free
-            // - Then check either side if they're slightly more free
-            var exteriorWidth = (int) Math.Floor(gen.Width / 2f);
-            var width = (int) Math.Ceiling(gen.Width / 2f);
-
-            for (var i = 0; i < 2; i++)
-            {
-                var isValid = true;
-                var neighborDir = (Direction) (i * 2);
-                var neighborVec = neighborDir.ToIntVec();
-
-                for (var j = -width; j <= width; j++)
-                {
-                    if (j == 0)
-                        continue;
-
-                    var neighbor = tile + neighborVec * j;
-
-                    // If it's an end tile then check it's occupied.
-                    if (j == -width ||
-                        j == width)
-                    {
-                        if (!HasWall(grid, neighbor))
-                        {
-                            isValid = false;
-                            break;
-                        }
-
-                        continue;
-                    }
-
-                    // If we're not at the end tile then check it + perpendicular are free.
-                    if (!_anchorable.TileFree(_grid, neighbor, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
-                    {
-                        isValid = false;
-                        break;
-                    }
-
-                    var perp1 = tile + neighborVec * j + ((Direction) ((i * 2 + 2) % 8)).ToIntVec();
-                    var perp2 = tile + neighborVec * j + ((Direction) ((i * 2 + 6) % 8)).ToIntVec();
-
-                    if (!_anchorable.TileFree(_grid, perp1, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
-                    {
-                        isValid = false;
-                        break;
-                    }
-
-                    if (!_anchorable.TileFree(_grid, perp2, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
-                    {
-                        isValid = false;
-                        break;
-                    }
-                }
-
-                if (!isValid)
-                    continue;
-
-                // Check corners to see if either side opens up (if it's just a 1x wide corridor do nothing, needs to be a funnel.
-                foreach (var j in new [] {-exteriorWidth, exteriorWidth})
-                {
-                    var freeCount = 0;
-
-                    // Need at least 3 of 4 free
-                    for (var k = 0; k < 4; k++)
-                    {
-                        var cornerDir = (Direction) (k * 2 + 1);
-                        var cornerVec = cornerDir.ToIntVec();
-                        var cornerNeighbor = tile + neighborVec * j + cornerVec;
-
-                        if (_anchorable.TileFree(_grid, cornerNeighbor, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
-                        {
-                            freeCount++;
-                        }
-                    }
-
-                    if (freeCount < gen.Width)
-                        continue;
-
-                    // Valid!
-                    isValid = true;
-
-                    for (var x = -width + 1; x < width; x++)
-                    {
-                        var weh = tile + neighborDir.ToIntVec() * x;
-                        grid.SetTile(weh, _tile.GetVariantTile((ContentTileDefinition) tileDef, random));
-
-                        var coords = grid.GridTileToLocal(weh);
-                        _entManager.SpawnEntities(coords, gen.Entities);
-                    }
-
-                    break;
-                }
-
-                if (isValid)
-                {
-                    await SuspendIfOutOfTime();
-
-                    if (!ValidateResume())
-                        return;
-                }
-
-                break;
-            }
-        }
-    }
-
-    private async Task PostGen(MiddleConnectionPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random)
-    {
-        // TODO: Need a minimal spanning tree version tbh
-
-        // Grab all of the room bounds
-        // Then, work out connections between them
-        var roomBorders = new Dictionary<DungeonRoom, HashSet<Vector2i>>(dungeon.Rooms.Count);
-
-        foreach (var room in dungeon.Rooms)
-        {
-            var roomEdges = new HashSet<Vector2i>();
-
-            foreach (var index in room.Tiles)
-            {
-                for (var x = -1; x <= 1; x++)
-                {
-                    for (var y = -1; y <= 1; y++)
-                    {
-                        // Cardinals only
-                        if (x != 0 && y != 0 ||
-                            x == 0 && y == 0)
-                        {
-                            continue;
-                        }
-
-                        var neighbor = new Vector2i(index.X + x, index.Y + y);
-
-                        if (dungeon.RoomTiles.Contains(neighbor))
-                            continue;
-
-                        if (!_anchorable.TileFree(grid, neighbor, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
-                            continue;
-
-                        roomEdges.Add(neighbor);
-                    }
-                }
-            }
-
-            roomBorders.Add(room, roomEdges);
-        }
-
-        // Do pathfind from first room to work out graph.
-        // TODO: Optional loops
-
-        var roomConnections = new Dictionary<DungeonRoom, List<DungeonRoom>>();
-        var frontier = new Queue<DungeonRoom>();
-        frontier.Enqueue(dungeon.Rooms.First());
-        var tileDef = _tileDefManager[gen.Tile];
-
-        foreach (var (room, border) in roomBorders)
-        {
-            var conns = roomConnections.GetOrNew(room);
-
-            foreach (var (otherRoom, otherBorders) in roomBorders)
-            {
-                if (room.Equals(otherRoom) ||
-                    conns.Contains(otherRoom))
-                {
-                    continue;
-                }
-
-                var flipp = new HashSet<Vector2i>(border);
-                flipp.IntersectWith(otherBorders);
-
-                if (flipp.Count == 0 ||
-                    gen.OverlapCount != -1 && flipp.Count != gen.OverlapCount)
-                    continue;
-
-                var center = Vector2.Zero;
-
-                foreach (var node in flipp)
-                {
-                    center += (Vector2) node + grid.TileSizeHalfVector;
-                }
-
-                center /= flipp.Count;
-                // Weight airlocks towards center more.
-                var nodeDistances = new List<(Vector2i Node, float Distance)>(flipp.Count);
-
-                foreach (var node in flipp)
-                {
-                    nodeDistances.Add((node, ((Vector2) node + grid.TileSizeHalfVector - center).LengthSquared()));
-                }
-
-                nodeDistances.Sort((x, y) => x.Distance.CompareTo(y.Distance));
-
-                var width = gen.Count;
-
-                for (var i = 0; i < nodeDistances.Count; i++)
-                {
-                    var node = nodeDistances[i].Node;
-                    var gridPos = grid.GridTileToLocal(node);
-                    if (!_anchorable.TileFree(grid, node, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
-                        continue;
-
-                    width--;
-                    grid.SetTile(node, _tile.GetVariantTile((ContentTileDefinition) tileDef, random));
-
-                    if (gen.EdgeEntities != null && nodeDistances.Count - i <= 2)
-                    {
-                        _entManager.SpawnEntities(gridPos, gen.EdgeEntities);
-                    }
-                    else
-                    {
-                        // Iterate neighbors and check for blockers, if so bulldoze
-                        ClearDoor(dungeon, grid, node);
-
-                        _entManager.SpawnEntities(gridPos, gen.Entities);
-                    }
-
-                    if (width == 0)
-                        break;
-                }
-
-                conns.Add(otherRoom);
-                var otherConns = roomConnections.GetOrNew(otherRoom);
-                otherConns.Add(room);
-                await SuspendIfOutOfTime();
-
-                if (!ValidateResume())
-                    return;
-            }
-        }
-    }
-
-    /// <summary>
-    /// Removes any unwanted obstacles around a door tile.
-    /// </summary>
-    private void ClearDoor(Dungeon dungeon, MapGridComponent grid, Vector2i indices, bool strict = false)
-    {
-        var flags = strict
-            ? LookupFlags.Dynamic | LookupFlags.Static | LookupFlags.StaticSundries
-            : LookupFlags.Dynamic | LookupFlags.Static;
-        var physicsQuery = _entManager.GetEntityQuery<PhysicsComponent>();
-
-        for (var x = -1; x <= 1; x++)
-        {
-            for (var y = -1; y <= 1; y++)
-            {
-                if (x != 0 && y != 0)
-                    continue;
-
-                var neighbor = new Vector2i(indices.X + x, indices.Y + y);
-
-                if (!dungeon.RoomTiles.Contains(neighbor))
-                    continue;
-
-                // Shrink by 0.01 to avoid polygon overlap from neighboring tiles.
-                foreach (var ent in _lookup.GetEntitiesIntersecting(_gridUid, new Box2(neighbor * grid.TileSize, (neighbor + 1) * grid.TileSize).Enlarged(-0.1f), flags))
-                {
-                    if (!physicsQuery.TryGetComponent(ent, out var physics) ||
-                        !physics.Hard ||
-                        (DungeonSystem.CollisionMask & physics.CollisionLayer) == 0x0 &&
-                        (DungeonSystem.CollisionLayer & physics.CollisionMask) == 0x0)
-                    {
-                        continue;
-                    }
-
-                    _entManager.DeleteEntity(ent);
-                }
-            }
-        }
-    }
-
-    private async Task PostGen(WallMountPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid,
-        Random random)
-    {
-        var tileDef = _tileDefManager[gen.Tile];
-        var checkedTiles = new HashSet<Vector2i>();
-        var allExterior = new HashSet<Vector2i>(dungeon.CorridorExteriorTiles);
-        allExterior.UnionWith(dungeon.RoomExteriorTiles);
-        var count = 0;
-
-        foreach (var neighbor in allExterior)
-        {
-            // Occupado
-            if (dungeon.RoomTiles.Contains(neighbor) || checkedTiles.Contains(neighbor) || !_anchorable.TileFree(grid, neighbor, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
-                continue;
-
-            if (!random.Prob(gen.Prob) || !checkedTiles.Add(neighbor))
-                continue;
-
-            grid.SetTile(neighbor, _tile.GetVariantTile((ContentTileDefinition) tileDef, random));
-            var gridPos = grid.GridTileToLocal(neighbor);
-            var protoNames = EntitySpawnCollection.GetSpawns(gen.Spawns, random);
-
-            _entManager.SpawnEntities(gridPos, protoNames);
-            count += protoNames.Count;
-
-            if (count > 20)
-            {
-                count -= 20;
-                await SuspendIfOutOfTime();
-
-                if (!ValidateResume())
-                    return;
-            }
-        }
-    }
-}
diff --git a/Content.Server/Procedural/DungeonJob.PostGenBiome.cs b/Content.Server/Procedural/DungeonJob.PostGenBiome.cs
deleted file mode 100644 (file)
index 4d3f573..0000000
+++ /dev/null
@@ -1,138 +0,0 @@
-using System.Threading.Tasks;
-using Content.Server.Parallax;
-using Content.Shared.Parallax.Biomes;
-using Content.Shared.Parallax.Biomes.Markers;
-using Content.Shared.Procedural;
-using Content.Shared.Procedural.PostGeneration;
-using Content.Shared.Random.Helpers;
-using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
-using Robust.Shared.Utility;
-
-namespace Content.Server.Procedural;
-
-public sealed partial class DungeonJob
-{
-    /*
-     * Handles PostGen code for marker layers + biomes.
-     */
-
-    private async Task PostGen(BiomePostGen postGen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random)
-    {
-        if (_entManager.TryGetComponent(gridUid, out BiomeComponent? biomeComp))
-            return;
-
-        biomeComp = _entManager.AddComponent<BiomeComponent>(gridUid);
-        var biomeSystem = _entManager.System<BiomeSystem>();
-        biomeSystem.SetTemplate(gridUid, biomeComp, _prototype.Index(postGen.BiomeTemplate));
-        var seed = random.Next();
-        var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
-
-        foreach (var node in dungeon.RoomTiles)
-        {
-            // Need to set per-tile to override data.
-            if (biomeSystem.TryGetTile(node, biomeComp.Layers, seed, grid, out var tile))
-            {
-                _maps.SetTile(gridUid, grid, node, tile.Value);
-            }
-
-            if (biomeSystem.TryGetDecals(node, biomeComp.Layers, seed, grid, out var decals))
-            {
-                foreach (var decal in decals)
-                {
-                    _decals.TryAddDecal(decal.ID, new EntityCoordinates(gridUid, decal.Position), out _);
-                }
-            }
-
-            if (biomeSystem.TryGetEntity(node, biomeComp, grid, out var entityProto))
-            {
-                var ent = _entManager.SpawnEntity(entityProto, new EntityCoordinates(gridUid, node + grid.TileSizeHalfVector));
-                var xform = xformQuery.Get(ent);
-
-                if (!xform.Comp.Anchored)
-                {
-                    _transform.AnchorEntity(ent, xform);
-                }
-
-                // TODO: Engine bug with SpawnAtPosition
-                DebugTools.Assert(xform.Comp.Anchored);
-            }
-
-            await SuspendIfOutOfTime();
-            ValidateResume();
-        }
-
-        biomeComp.Enabled = false;
-    }
-
-    private async Task PostGen(BiomeMarkerLayerPostGen postGen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random)
-    {
-        if (!_entManager.TryGetComponent(gridUid, out BiomeComponent? biomeComp))
-            return;
-
-        var biomeSystem = _entManager.System<BiomeSystem>();
-        var weightedRandom = _prototype.Index(postGen.MarkerTemplate);
-        var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
-        var templates = new Dictionary<string, int>();
-
-        for (var i = 0; i < postGen.Count; i++)
-        {
-            var template = weightedRandom.Pick(random);
-            var count = templates.GetOrNew(template);
-            count++;
-            templates[template] = count;
-        }
-
-        foreach (var (template, count) in templates)
-        {
-            var markerTemplate = _prototype.Index<BiomeMarkerLayerPrototype>(template);
-
-            var bounds = new Box2i();
-
-            foreach (var tile in dungeon.RoomTiles)
-            {
-                bounds = bounds.UnionTile(tile);
-            }
-
-            await SuspendIfOutOfTime();
-            ValidateResume();
-
-            biomeSystem.GetMarkerNodes(gridUid, biomeComp, grid, markerTemplate, true, bounds, count,
-                random, out var spawnSet, out var existing, false);
-
-            await SuspendIfOutOfTime();
-            ValidateResume();
-
-            foreach (var ent in existing)
-            {
-                _entManager.DeleteEntity(ent);
-            }
-
-            await SuspendIfOutOfTime();
-            ValidateResume();
-
-            foreach (var (node, mask) in spawnSet)
-            {
-                string? proto;
-
-                if (mask != null && markerTemplate.EntityMask.TryGetValue(mask, out var maskedProto))
-                {
-                    proto = maskedProto;
-                }
-                else
-                {
-                    proto = markerTemplate.Prototype;
-                }
-
-                var ent = _entManager.SpawnAtPosition(proto, new EntityCoordinates(gridUid, node + grid.TileSizeHalfVector));
-                var xform = xformQuery.Get(ent);
-
-                if (!xform.Comp.Anchored)
-                    _transform.AnchorEntity(ent, xform);
-
-                await SuspendIfOutOfTime();
-                ValidateResume();
-            }
-        }
-    }
-}
diff --git a/Content.Server/Procedural/DungeonJob.cs b/Content.Server/Procedural/DungeonJob.cs
deleted file mode 100644 (file)
index bf2822f..0000000
+++ /dev/null
@@ -1,192 +0,0 @@
-using System.Threading;
-using System.Threading.Tasks;
-using Content.Server.Construction;
-using Robust.Shared.CPUJob.JobQueues;
-using Content.Server.Decals;
-using Content.Shared.Construction.EntitySystems;
-using Content.Shared.Maps;
-using Content.Shared.Procedural;
-using Content.Shared.Procedural.DungeonGenerators;
-using Content.Shared.Procedural.PostGeneration;
-using Content.Shared.Tag;
-using Robust.Server.Physics;
-using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Utility;
-
-namespace Content.Server.Procedural;
-
-public sealed partial class DungeonJob : Job<Dungeon>
-{
-    private readonly IEntityManager _entManager;
-    private readonly IMapManager _mapManager;
-    private readonly IPrototypeManager _prototype;
-    private readonly ITileDefinitionManager _tileDefManager;
-
-    private readonly AnchorableSystem _anchorable;
-    private readonly DecalSystem _decals;
-    private readonly DungeonSystem _dungeon;
-    private readonly EntityLookupSystem _lookup;
-    private readonly TagSystem _tag;
-    private readonly TileSystem _tile;
-    private readonly SharedMapSystem _maps;
-    private readonly SharedTransformSystem _transform;
-
-    private readonly DungeonConfigPrototype _gen;
-    private readonly int _seed;
-    private readonly Vector2i _position;
-
-    private readonly MapGridComponent _grid;
-    private readonly EntityUid _gridUid;
-
-    private readonly ISawmill _sawmill;
-
-    public DungeonJob(
-        ISawmill sawmill,
-        double maxTime,
-        IEntityManager entManager,
-        IMapManager mapManager,
-        IPrototypeManager prototype,
-        ITileDefinitionManager tileDefManager,
-        AnchorableSystem anchorable,
-        DecalSystem decals,
-        DungeonSystem dungeon,
-        EntityLookupSystem lookup,
-        TagSystem tag,
-        TileSystem tile,
-        SharedTransformSystem transform,
-        DungeonConfigPrototype gen,
-        MapGridComponent grid,
-        EntityUid gridUid,
-        int seed,
-        Vector2i position,
-        CancellationToken cancellation = default) : base(maxTime, cancellation)
-    {
-        _sawmill = sawmill;
-        _entManager = entManager;
-        _mapManager = mapManager;
-        _prototype = prototype;
-        _tileDefManager = tileDefManager;
-
-        _anchorable = anchorable;
-        _decals = decals;
-        _dungeon = dungeon;
-        _lookup = lookup;
-        _tag = tag;
-        _tile = tile;
-        _maps = _entManager.System<SharedMapSystem>();
-        _transform = transform;
-
-        _gen = gen;
-        _grid = grid;
-        _gridUid = gridUid;
-        _seed = seed;
-        _position = position;
-    }
-
-    protected override async Task<Dungeon?> Process()
-    {
-        Dungeon dungeon;
-        _sawmill.Info($"Generating dungeon {_gen.ID} with seed {_seed} on {_entManager.ToPrettyString(_gridUid)}");
-        _grid.CanSplit = false;
-
-        switch (_gen.Generator)
-        {
-            case NoiseDunGen noise:
-                dungeon = await GenerateNoiseDungeon(noise, _gridUid, _grid, _seed);
-                break;
-            case PrefabDunGen prefab:
-                dungeon = await GeneratePrefabDungeon(prefab, _gridUid, _grid, _seed);
-                DebugTools.Assert(dungeon.RoomExteriorTiles.Count > 0);
-                break;
-            default:
-                throw new NotImplementedException();
-        }
-
-        DebugTools.Assert(dungeon.RoomTiles.Count > 0);
-
-        // To make it slightly more deterministic treat this RNG as separate ig.
-        var random = new Random(_seed);
-
-        foreach (var post in _gen.PostGeneration)
-        {
-            _sawmill.Debug($"Doing postgen {post.GetType()} for {_gen.ID} with seed {_seed}");
-
-            switch (post)
-            {
-                case AutoCablingPostGen cabling:
-                    await PostGen(cabling, dungeon, _gridUid, _grid, random);
-                    break;
-                case BiomePostGen biome:
-                    await PostGen(biome, dungeon, _gridUid, _grid, random);
-                    break;
-                case BoundaryWallPostGen boundary:
-                    await PostGen(boundary, dungeon, _gridUid, _grid, random);
-                    break;
-                case CornerClutterPostGen clutter:
-                    await PostGen(clutter, dungeon, _gridUid, _grid, random);
-                    break;
-                case CorridorClutterPostGen corClutter:
-                    await PostGen(corClutter, dungeon, _gridUid, _grid, random);
-                    break;
-                case CorridorPostGen cordor:
-                    await PostGen(cordor, dungeon, _gridUid, _grid, random);
-                    break;
-                case CorridorDecalSkirtingPostGen decks:
-                    await PostGen(decks, dungeon, _gridUid, _grid, random);
-                    break;
-                case EntranceFlankPostGen flank:
-                    await PostGen(flank, dungeon, _gridUid, _grid, random);
-                    break;
-                case JunctionPostGen junc:
-                    await PostGen(junc, dungeon, _gridUid, _grid, random);
-                    break;
-                case MiddleConnectionPostGen dordor:
-                    await PostGen(dordor, dungeon, _gridUid, _grid, random);
-                    break;
-                case DungeonEntrancePostGen entrance:
-                    await PostGen(entrance, dungeon, _gridUid, _grid, random);
-                    break;
-                case ExternalWindowPostGen externalWindow:
-                    await PostGen(externalWindow, dungeon, _gridUid, _grid, random);
-                    break;
-                case InternalWindowPostGen internalWindow:
-                    await PostGen(internalWindow, dungeon, _gridUid, _grid, random);
-                    break;
-                case BiomeMarkerLayerPostGen markerPost:
-                    await PostGen(markerPost, dungeon, _gridUid, _grid, random);
-                    break;
-                case RoomEntrancePostGen rEntrance:
-                    await PostGen(rEntrance, dungeon, _gridUid, _grid, random);
-                    break;
-                case WallMountPostGen wall:
-                    await PostGen(wall, dungeon, _gridUid, _grid, random);
-                    break;
-                case WormCorridorPostGen worm:
-                    await PostGen(worm, dungeon, _gridUid, _grid, random);
-                    break;
-                default:
-                    throw new NotImplementedException();
-            }
-
-            await SuspendIfOutOfTime();
-
-            if (!ValidateResume())
-                break;
-        }
-
-        // Defer splitting so they don't get spammed and so we don't have to worry about tracking the grid along the way.
-        _grid.CanSplit = true;
-        _entManager.System<GridFixtureSystem>().CheckSplits(_gridUid);
-        return dungeon;
-    }
-
-    private bool ValidateResume()
-    {
-        if (_entManager.Deleted(_gridUid))
-            return false;
-
-        return true;
-    }
-}
diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.DunGenExterior.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.DunGenExterior.cs
new file mode 100644 (file)
index 0000000..acffd05
--- /dev/null
@@ -0,0 +1,58 @@
+using System.Threading.Tasks;
+using Content.Server.NPC.Pathfinding;
+using Content.Shared.Maps;
+using Content.Shared.Procedural;
+using Content.Shared.Procedural.DungeonGenerators;
+using Robust.Shared.Collections;
+using Robust.Shared.Random;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Procedural.DungeonJob;
+
+public sealed partial class DungeonJob
+{
+    /// <summary>
+    /// <see cref="ExteriorDunGen"/>
+    /// </summary>
+    private async Task<List<Dungeon>> GenerateExteriorDungen(Vector2i position, ExteriorDunGen dungen, HashSet<Vector2i> reservedTiles, Random random)
+    {
+        DebugTools.Assert(_grid.ChunkCount > 0);
+
+        var aabb = new Box2i(_grid.LocalAABB.BottomLeft.Floored(), _grid.LocalAABB.TopRight.Floored());
+        var angle = random.NextAngle();
+
+        var distance = Math.Max(aabb.Width / 2f + 1f, aabb.Height / 2f + 1f);
+
+        var startTile = new Vector2i(0, (int) distance).Rotate(angle);
+
+        Vector2i? dungeonSpawn = null;
+        var pathfinder = _entManager.System<PathfindingSystem>();
+
+        // Gridcast
+        pathfinder.GridCast(startTile, position, tile =>
+        {
+            if (!_maps.TryGetTileRef(_gridUid, _grid, tile, out var tileRef) ||
+                tileRef.Tile.IsSpace(_tileDefManager))
+            {
+                return true;
+            }
+
+            dungeonSpawn = tile;
+            return false;
+        });
+
+        if (dungeonSpawn == null)
+        {
+            return new List<Dungeon>()
+            {
+                Dungeon.Empty
+            };
+        }
+
+        var config = _prototype.Index(dungen.Proto);
+        var nextSeed = random.Next();
+        var dungeons = await GetDungeons(dungeonSpawn.Value, config, config.Data, config.Layers, reservedTiles, nextSeed, new Random(nextSeed));
+
+        return dungeons;
+    }
+}
diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.DunGenFill.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.DunGenFill.cs
new file mode 100644 (file)
index 0000000..5a0d77c
--- /dev/null
@@ -0,0 +1,50 @@
+using System.Numerics;
+using System.Threading.Tasks;
+using Content.Shared.Procedural;
+using Content.Shared.Procedural.DungeonGenerators;
+
+namespace Content.Server.Procedural.DungeonJob;
+
+public sealed partial class DungeonJob
+{
+    /// <summary>
+    /// <see cref="FillGridDunGen"/>
+    /// </summary>
+    private async Task<Dungeon> GenerateFillDunGen(DungeonData data, HashSet<Vector2i> reservedTiles)
+    {
+        if (!data.Entities.TryGetValue(DungeonDataKey.Fill, out var fillEnt))
+        {
+            LogDataError(typeof(FillGridDunGen));
+            return Dungeon.Empty;
+        }
+
+        var roomTiles = new HashSet<Vector2i>();
+        var tiles = _maps.GetAllTilesEnumerator(_gridUid, _grid);
+
+        while (tiles.MoveNext(out var tileRef))
+        {
+            var tile = tileRef.Value.GridIndices;
+
+            if (reservedTiles.Contains(tile))
+                continue;
+
+            if (!_anchorable.TileFree(_grid, tile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
+                continue;
+
+            var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile);
+            _entManager.SpawnEntity(fillEnt, gridPos);
+
+            roomTiles.Add(tile);
+
+            await SuspendDungeon();
+            if (!ValidateResume())
+                break;
+        }
+
+        var dungeon = new Dungeon();
+        var room = new DungeonRoom(roomTiles, Vector2.Zero, Box2i.Empty, new HashSet<Vector2i>());
+        dungeon.AddRoom(room);
+
+        return dungeon;
+    }
+}
similarity index 73%
rename from Content.Server/Procedural/DungeonJob.NoiseDunGen.cs
rename to Content.Server/Procedural/DungeonJob/DungeonJob.DunGenNoise.cs
index 73c3386ead548dd977e4f3a49fe04dacd130013d..b2526ec17d1b53d5ea17950048dd11fcc6c32d62 100644 (file)
@@ -4,19 +4,25 @@ using Content.Shared.Maps;
 using Content.Shared.Procedural;
 using Content.Shared.Procedural.DungeonGenerators;
 using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
 using Robust.Shared.Random;
 using Robust.Shared.Utility;
 
-namespace Content.Server.Procedural;
+namespace Content.Server.Procedural.DungeonJob;
 
 public sealed partial class DungeonJob
 {
-    private async Task<Dungeon> GenerateNoiseDungeon(NoiseDunGen dungen, EntityUid gridUid, MapGridComponent grid,
-        int seed)
+    /// <summary>
+    /// <see cref="NoiseDunGen"/>
+    /// </summary>
+    private async Task<Dungeon> GenerateNoiseDunGen(
+        Vector2i position,
+        NoiseDunGen dungen,
+        HashSet<Vector2i> reservedTiles,
+        int seed,
+        Random random)
     {
-        var rand = new Random(seed);
         var tiles = new List<(Vector2i, Tile)>();
+        var matrix = Matrix3Helpers.CreateTranslation(position);
 
         foreach (var layer in dungen.Layers)
         {
@@ -30,7 +36,7 @@ public sealed partial class DungeonJob
         var frontier = new Queue<Vector2i>();
         var rooms = new List<DungeonRoom>();
         var tileCount = 0;
-        var tileCap = rand.NextGaussian(dungen.TileCap, dungen.CapStd);
+        var tileCap = random.NextGaussian(dungen.TileCap, dungen.CapStd);
         var visited = new HashSet<Vector2i>();
 
         while (iterations > 0 && tileCount < tileCap)
@@ -39,22 +45,22 @@ public sealed partial class DungeonJob
             iterations--;
 
             // Get a random exterior tile to start floodfilling from.
-            var edge = rand.Next(4);
+            var edge = random.Next(4);
             Vector2i seedTile;
 
             switch (edge)
             {
                 case 0:
-                    seedTile = new Vector2i(rand.Next(area.Left - 2, area.Right + 1), area.Bottom - 2);
+                    seedTile = new Vector2i(random.Next(area.Left - 2, area.Right + 1), area.Bottom - 2);
                     break;
                 case 1:
-                    seedTile = new Vector2i(area.Right + 1, rand.Next(area.Bottom - 2, area.Top + 1));
+                    seedTile = new Vector2i(area.Right + 1, random.Next(area.Bottom - 2, area.Top + 1));
                     break;
                 case 2:
-                    seedTile = new Vector2i(rand.Next(area.Left - 2, area.Right + 1), area.Top + 1);
+                    seedTile = new Vector2i(random.Next(area.Left - 2, area.Right + 1), area.Top + 1);
                     break;
                 case 3:
-                    seedTile = new Vector2i(area.Left - 2, rand.Next(area.Bottom - 2, area.Top + 1));
+                    seedTile = new Vector2i(area.Left - 2, random.Next(area.Bottom - 2, area.Top + 1));
                     break;
                 default:
                     throw new ArgumentOutOfRangeException();
@@ -80,14 +86,20 @@ public sealed partial class DungeonJob
                     if (value < layer.Threshold)
                         continue;
 
-                    roomArea = roomArea.UnionTile(node);
                     foundNoise = true;
                     noiseFill = true;
+
+                    // Still want the tile to gen as normal but can't do anything with it.
+                    if (reservedTiles.Contains(node))
+                        break;
+
+                    roomArea = roomArea.UnionTile(node);
                     var tileDef = _tileDefManager[layer.Tile];
-                    var variant = _tile.PickVariant((ContentTileDefinition) tileDef, rand);
+                    var variant = _tile.PickVariant((ContentTileDefinition) tileDef, random);
+                    var adjusted = Vector2.Transform(node + _grid.TileSizeHalfVector, matrix).Floored();
 
-                    tiles.Add((node, new Tile(tileDef.TileId, variant: variant)));
-                    roomTiles.Add(node);
+                    tiles.Add((adjusted, new Tile(tileDef.TileId, variant: variant)));
+                    roomTiles.Add(adjusted);
                     tileCount++;
                     break;
                 }
@@ -123,7 +135,7 @@ public sealed partial class DungeonJob
 
             foreach (var tile in roomTiles)
             {
-                center += tile + grid.TileSizeHalfVector;
+                center += tile + _grid.TileSizeHalfVector;
             }
 
             center /= roomTiles.Count;
@@ -132,15 +144,8 @@ public sealed partial class DungeonJob
             ValidateResume();
         }
 
-        grid.SetTiles(tiles);
-
+        _maps.SetTiles(_gridUid, _grid, tiles);
         var dungeon = new Dungeon(rooms);
-
-        foreach (var tile in tiles)
-        {
-            dungeon.RoomTiles.Add(tile.Item1);
-        }
-
         return dungeon;
     }
 }
diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.DunGenNoiseDistance.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.DunGenNoiseDistance.cs
new file mode 100644 (file)
index 0000000..f1808ec
--- /dev/null
@@ -0,0 +1,112 @@
+using System.Numerics;
+using System.Threading.Tasks;
+using Content.Shared.Maps;
+using Content.Shared.Procedural;
+using Content.Shared.Procedural.Distance;
+using Content.Shared.Procedural.DungeonGenerators;
+using Robust.Shared.Map;
+
+namespace Content.Server.Procedural.DungeonJob;
+
+public sealed partial class DungeonJob
+{
+    /*
+     * See https://www.redblobgames.com/maps/terrain-from-noise/#islands
+     * Really it's just blending from the original noise (which may occupy the entire area)
+     * with some other shape to confine it into a bounds more naturally.
+     * https://old.reddit.com/r/proceduralgeneration/comments/kaen7h/new_video_on_procedural_island_noise_generation/gfjmgen/ also has more variations
+     */
+
+    /// <summary>
+    /// <see cref="NoiseDistanceDunGen"/>
+    /// </summary>
+    private async Task<Dungeon> GenerateNoiseDistanceDunGen(
+        Vector2i position,
+        NoiseDistanceDunGen dungen,
+        HashSet<Vector2i> reservedTiles,
+        int seed,
+        Random random)
+    {
+        var tiles = new List<(Vector2i, Tile)>();
+        var matrix = Matrix3Helpers.CreateTranslation(position);
+
+        foreach (var layer in dungen.Layers)
+        {
+            layer.Noise.SetSeed(seed);
+        }
+
+        // First we have to find a seed tile, then floodfill from there until we get to noise
+        // at which point we floodfill the entire noise.
+        var area = Box2i.FromDimensions(-dungen.Size / 2, dungen.Size);
+        var roomTiles = new HashSet<Vector2i>();
+        var width = (float) area.Width;
+        var height = (float) area.Height;
+
+        for (var x = area.Left; x <= area.Right; x++)
+        {
+            for (var y = area.Bottom; y <= area.Top; y++)
+            {
+                var node = new Vector2i(x, y);
+
+                foreach (var layer in dungen.Layers)
+                {
+                    var value = layer.Noise.GetNoise(node.X, node.Y);
+
+                    if (dungen.DistanceConfig != null)
+                    {
+                        // Need to get dx - dx in a range from -1 -> 1
+                        var dx = 2 * x / width;
+                        var dy = 2 * y / height;
+
+                        var distance = GetDistance(dx, dy, dungen.DistanceConfig);
+
+                        value = MathHelper.Lerp(value, 1f - distance, dungen.DistanceConfig.BlendWeight);
+                    }
+
+                    if (value < layer.Threshold)
+                        continue;
+
+                    var tileDef = _tileDefManager[layer.Tile];
+                    var variant = _tile.PickVariant((ContentTileDefinition) tileDef, random);
+                    var adjusted = Vector2.Transform(node + _grid.TileSizeHalfVector, matrix).Floored();
+
+                    // Do this down here because noise has a much higher chance of failing than reserved tiles.
+                    if (reservedTiles.Contains(adjusted))
+                    {
+                        break;
+                    }
+
+                    tiles.Add((adjusted, new Tile(tileDef.TileId, variant: variant)));
+                    roomTiles.Add(adjusted);
+                    break;
+                }
+            }
+
+            await SuspendDungeon();
+        }
+
+        var room = new DungeonRoom(roomTiles, area.Center, area, new HashSet<Vector2i>());
+
+        _maps.SetTiles(_gridUid, _grid, tiles);
+        var dungeon = new Dungeon(new List<DungeonRoom>()
+        {
+            room,
+        });
+
+        await SuspendDungeon();
+        return dungeon;
+    }
+
+    private float GetDistance(float dx, float dy, IDunGenDistance distance)
+    {
+        switch (distance)
+        {
+            case DunGenEuclideanSquaredDistance:
+                return MathF.Min(1f, (dx * dx + dy * dy) / MathF.Sqrt(2));
+            case DunGenSquareBump:
+                return 1f - (1f - dx * dx) * (1f - dy * dy);
+            default:
+                throw new ArgumentOutOfRangeException();
+        }
+    }
+}
similarity index 82%
rename from Content.Server/Procedural/DungeonJob.PrefabDunGen.cs
rename to Content.Server/Procedural/DungeonJob/DungeonJob.DunGenPrefab.cs
index a19f7e4701d9f9f80312b4b0c99ac7e21e88ff36..33bbeba4b53c700d847acf5b35f554efbd7b4fd3 100644 (file)
@@ -1,25 +1,33 @@
 using System.Numerics;
 using System.Threading.Tasks;
-using Content.Shared.Decals;
 using Content.Shared.Procedural;
 using Content.Shared.Procedural.DungeonGenerators;
+using Content.Shared.Whitelist;
 using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
 using Robust.Shared.Random;
 using Robust.Shared.Utility;
 
-namespace Content.Server.Procedural;
+namespace Content.Server.Procedural.DungeonJob;
 
 public sealed partial class DungeonJob
 {
-    private async Task<Dungeon> GeneratePrefabDungeon(PrefabDunGen prefab, EntityUid gridUid, MapGridComponent grid, int seed)
+    /// <summary>
+    /// <see cref="PrefabDunGen"/>
+    /// </summary>
+    private async Task<Dungeon> GeneratePrefabDunGen(Vector2i position, DungeonData data, PrefabDunGen prefab, HashSet<Vector2i> reservedTiles, Random random)
     {
-        var random = new Random(seed);
+        if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto) ||
+            !data.Whitelists.TryGetValue(DungeonDataKey.Rooms, out var roomWhitelist))
+        {
+            LogDataError(typeof(PrefabDunGen));
+            return Dungeon.Empty;
+        }
+
         var preset = prefab.Presets[random.Next(prefab.Presets.Count)];
-        var gen = _prototype.Index<DungeonPresetPrototype>(preset);
+        var gen = _prototype.Index(preset);
 
-        var dungeonRotation = _dungeon.GetDungeonRotation(seed);
-        var dungeonTransform = Matrix3Helpers.CreateTransform(_position, dungeonRotation);
+        var dungeonRotation = _dungeon.GetDungeonRotation(random.Next());
+        var dungeonTransform = Matrix3Helpers.CreateTransform(position, dungeonRotation);
         var roomPackProtos = new Dictionary<Vector2i, List<DungeonRoomPackPrototype>>();
 
         foreach (var pack in _prototype.EnumeratePrototypes<DungeonRoomPackPrototype>())
@@ -42,12 +50,15 @@ public sealed partial class DungeonJob
         {
             var whitelisted = false;
 
-            foreach (var tag in prefab.RoomWhitelist)
+            if (roomWhitelist?.Tags != null)
             {
-                if (proto.Tags.Contains(tag))
+                foreach (var tag in roomWhitelist.Tags)
                 {
-                    whitelisted = true;
-                    break;
+                    if (proto.Tags.Contains(tag))
+                    {
+                        whitelisted = true;
+                        break;
+                    }
                 }
             }
 
@@ -182,12 +193,16 @@ public sealed partial class DungeonJob
                         {
                             for (var y = roomSize.Bottom; y < roomSize.Top; y++)
                             {
-                                var index = Vector2.Transform(new Vector2(x, y) + grid.TileSizeHalfVector - packCenter, matty).Floored();
-                                tiles.Add((index, new Tile(_tileDefManager["FloorPlanetGrass"].TileId)));
+                                var index = Vector2.Transform(new Vector2(x, y) + _grid.TileSizeHalfVector - packCenter, matty).Floored();
+
+                                if (reservedTiles.Contains(index))
+                                    continue;
+
+                                tiles.Add((index, new Tile(_tileDefManager[tileProto].TileId)));
                             }
                         }
 
-                        grid.SetTiles(tiles);
+                        _maps.SetTiles(_gridUid, _grid, tiles);
                         tiles.Clear();
                         _sawmill.Error($"Unable to find room variant for {roomDimensions}, leaving empty.");
                         continue;
@@ -215,12 +230,12 @@ public sealed partial class DungeonJob
                 var dungeonMatty = Matrix3x2.Multiply(matty, dungeonTransform);
 
                 // The expensive bit yippy.
-                _dungeon.SpawnRoom(gridUid, grid, dungeonMatty, room);
+                _dungeon.SpawnRoom(_gridUid, _grid, dungeonMatty, room, reservedTiles);
 
-                var roomCenter = (room.Offset + room.Size / 2f) * grid.TileSize;
+                var roomCenter = (room.Offset + room.Size / 2f) * _grid.TileSize;
                 var roomTiles = new HashSet<Vector2i>(room.Size.X * room.Size.Y);
                 var exterior = new HashSet<Vector2i>(room.Size.X * 2 + room.Size.Y * 2);
-                var tileOffset = -roomCenter + grid.TileSizeHalfVector;
+                var tileOffset = -roomCenter + _grid.TileSizeHalfVector;
                 Box2i? mapBounds = null;
 
                 for (var x = -1; x <= room.Size.X; x++)
@@ -232,8 +247,12 @@ public sealed partial class DungeonJob
                             continue;
                         }
 
-                        var tilePos = Vector2.Transform(new Vector2i(x + room.Offset.X, y + room.Offset.Y) + tileOffset, dungeonMatty);
-                        exterior.Add(tilePos.Floored());
+                        var tilePos = Vector2.Transform(new Vector2i(x + room.Offset.X, y + room.Offset.Y) + tileOffset, dungeonMatty).Floored();
+
+                        if (reservedTiles.Contains(tilePos))
+                            continue;
+
+                        exterior.Add(tilePos);
                     }
                 }
 
@@ -249,38 +268,36 @@ public sealed partial class DungeonJob
                         roomTiles.Add(tileIndex);
 
                         mapBounds = mapBounds?.Union(tileIndex) ?? new Box2i(tileIndex, tileIndex);
-                        center += tilePos + grid.TileSizeHalfVector;
+                        center += tilePos + _grid.TileSizeHalfVector;
                     }
                 }
 
                 center /= roomTiles.Count;
 
-                dungeon.Rooms.Add(new DungeonRoom(roomTiles, center, mapBounds!.Value, exterior));
+                dungeon.AddRoom(new DungeonRoom(roomTiles, center, mapBounds!.Value, exterior));
+
+                await SuspendDungeon();
 
-                await SuspendIfOutOfTime();
-                ValidateResume();
+                if (!ValidateResume())
+                    return Dungeon.Empty;
             }
         }
 
         // Calculate center and do entrances
         var dungeonCenter = Vector2.Zero;
 
-        foreach (var room in dungeon.Rooms)
-        {
-            dungeon.RoomTiles.UnionWith(room.Tiles);
-            dungeon.RoomExteriorTiles.UnionWith(room.Exterior);
-        }
-
         foreach (var room in dungeon.Rooms)
         {
             dungeonCenter += room.Center;
-            SetDungeonEntrance(dungeon, room, random);
+            SetDungeonEntrance(dungeon, room, reservedTiles, random);
         }
 
+        dungeon.Rebuild();
+
         return dungeon;
     }
 
-    private void SetDungeonEntrance(Dungeon dungeon, DungeonRoom room, Random random)
+    private void SetDungeonEntrance(Dungeon dungeon, DungeonRoom room, HashSet<Vector2i> reservedTiles, Random random)
     {
         // TODO: Move to dungeonsystem.
 
@@ -323,8 +340,10 @@ public sealed partial class DungeonJob
                     continue;
                 }
 
+                if (reservedTiles.Contains(entrancePos))
+                    continue;
+
                 room.Entrances.Add(entrancePos);
-                dungeon.Entrances.Add(entrancePos);
                 break;
             }
         }
diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.DunGenReplaceTile.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.DunGenReplaceTile.cs
new file mode 100644 (file)
index 0000000..6b36d10
--- /dev/null
@@ -0,0 +1,60 @@
+using System.Threading.Tasks;
+using Content.Shared.Procedural;
+using Content.Shared.Procedural.DungeonGenerators;
+using Content.Shared.Procedural.PostGeneration;
+using Robust.Shared.Map;
+using Robust.Shared.Random;
+
+namespace Content.Server.Procedural.DungeonJob;
+
+public sealed partial class DungeonJob
+{
+    /// <summary>
+    /// <see cref="ReplaceTileDunGen"/>
+    /// </summary>
+    private async Task<Dungeon> GenerateTileReplacementDunGen(ReplaceTileDunGen gen, DungeonData data, HashSet<Vector2i> reservedTiles, Random random)
+    {
+        var tiles = _maps.GetAllTilesEnumerator(_gridUid, _grid);
+        var replacements = new List<(Vector2i Index, Tile Tile)>();
+        var reserved = new HashSet<Vector2i>();
+
+        while (tiles.MoveNext(out var tileRef))
+        {
+            var node = tileRef.Value.GridIndices;
+
+            if (reservedTiles.Contains(node))
+                continue;
+
+            foreach (var layer in gen.Layers)
+            {
+                var value = layer.Noise.GetNoise(node.X, node.Y);
+
+                if (value < layer.Threshold)
+                    continue;
+
+                Tile tile;
+
+                if (random.Prob(gen.VariantWeight))
+                {
+                    tile = _tileDefManager.GetVariantTile(_prototype.Index(layer.Tile), random);
+                }
+                else
+                {
+                    tile = new Tile(_prototype.Index(layer.Tile).TileId);
+                }
+
+                replacements.Add((node, tile));
+                reserved.Add(node);
+                break;
+            }
+
+            await SuspendDungeon();
+        }
+
+        _maps.SetTiles(_gridUid, _grid, replacements);
+        return new Dungeon(new List<DungeonRoom>()
+        {
+            new DungeonRoom(reserved, _position, Box2i.Empty, new HashSet<Vector2i>()),
+        });
+    }
+}
diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.MobDunGen.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.MobDunGen.cs
new file mode 100644 (file)
index 0000000..150849d
--- /dev/null
@@ -0,0 +1,58 @@
+using System.Threading.Tasks;
+using Content.Server.Ghost.Roles.Components;
+using Content.Server.NPC.Components;
+using Content.Server.NPC.Systems;
+using Content.Shared.Physics;
+using Content.Shared.Procedural;
+using Content.Shared.Procedural.DungeonLayers;
+using Content.Shared.Storage;
+using Robust.Shared.Collections;
+using Robust.Shared.Random;
+
+namespace Content.Server.Procedural.DungeonJob;
+
+public sealed partial class DungeonJob
+{
+    private async Task PostGen(
+        MobsDunGen gen,
+        Dungeon dungeon,
+        Random random)
+    {
+        var availableRooms = new ValueList<DungeonRoom>();
+        availableRooms.AddRange(dungeon.Rooms);
+        var availableTiles = new ValueList<Vector2i>(dungeon.AllTiles);
+
+        var entities = EntitySpawnCollection.GetSpawns(gen.Groups, random);
+        var count = random.Next(gen.MinCount, gen.MaxCount + 1);
+        var npcs = _entManager.System<NPCSystem>();
+
+        for (var i = 0; i < count; i++)
+        {
+            while (availableTiles.Count > 0)
+            {
+                var tile = availableTiles.RemoveSwap(random.Next(availableTiles.Count));
+
+                if (!_anchorable.TileFree(_grid, tile, (int) CollisionGroup.MachineLayer,
+                        (int) CollisionGroup.MachineLayer))
+                {
+                    continue;
+                }
+
+                foreach (var ent in entities)
+                {
+                    var uid = _entManager.SpawnAtPosition(ent, _maps.GridTileToLocal(_gridUid, _grid, tile));
+                    _entManager.RemoveComponent<GhostRoleComponent>(uid);
+                    _entManager.RemoveComponent<GhostTakeoverAvailableComponent>(uid);
+                    npcs.SleepNPC(uid);
+                }
+
+                break;
+            }
+
+            await SuspendDungeon();
+
+            if (!ValidateResume())
+                return;
+        }
+    }
+}
diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.OreDunGen.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.OreDunGen.cs
new file mode 100644 (file)
index 0000000..e89c1d7
--- /dev/null
@@ -0,0 +1,149 @@
+using System.Threading.Tasks;
+using Content.Shared.Procedural;
+using Content.Shared.Procedural.Components;
+using Content.Shared.Procedural.DungeonLayers;
+using Robust.Shared.Collections;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.Procedural.DungeonJob;
+
+public sealed partial class DungeonJob
+{
+    /// <summary>
+    /// <see cref="OreDunGen"/>
+    /// </summary>
+    private async Task PostGen(
+        OreDunGen gen,
+        Dungeon dungeon,
+        Random random)
+    {
+        // Doesn't use dungeon data because layers and we don't need top-down support at the moment.
+
+        var emptyTiles = false;
+        var replaceEntities = new Dictionary<Vector2i, EntityUid>();
+        var availableTiles = new List<Vector2i>();
+
+        foreach (var node in dungeon.AllTiles)
+        {
+            // Empty tile, skip if relevant.
+            if (!emptyTiles && (!_maps.TryGetTile(_grid, node, out var tile) || tile.IsEmpty))
+                continue;
+
+            // Check if it's a valid spawn, if so then use it.
+            var enumerator = _maps.GetAnchoredEntitiesEnumerator(_gridUid, _grid, node);
+            var found = false;
+
+            // We use existing entities as a mark to spawn in place
+            // OR
+            // We check for any existing entities to see if we can spawn there.
+            while (enumerator.MoveNext(out var uid))
+            {
+                // We can't replace so just stop here.
+                if (gen.Replacement == null)
+                    break;
+
+                var prototype = _entManager.GetComponent<MetaDataComponent>(uid.Value).EntityPrototype;
+
+                if (prototype?.ID == gen.Replacement)
+                {
+                    replaceEntities[node] = uid.Value;
+                    found = true;
+                    break;
+                }
+            }
+
+            if (!found)
+                continue;
+
+            // Add it to valid nodes.
+            availableTiles.Add(node);
+
+            await SuspendDungeon();
+
+            if (!ValidateResume())
+                return;
+        }
+
+        var remapping = new Dictionary<EntProtoId, EntProtoId>();
+
+        // TODO: Move this to engine
+        if (_prototype.TryIndex(gen.Entity, out var proto) &&
+            proto.Components.TryGetComponent("EntityRemap", out var comps))
+        {
+            var remappingComp = (EntityRemapComponent) comps;
+            remapping = remappingComp.Mask;
+        }
+
+        var frontier = new ValueList<Vector2i>(32);
+
+        // Iterate the group counts and pathfind out each group.
+        for (var i = 0; i < gen.Count; i++)
+        {
+            await SuspendDungeon();
+
+            if (!ValidateResume())
+                return;
+
+            var groupSize = random.Next(gen.MinGroupSize, gen.MaxGroupSize + 1);
+
+            // While we have remaining tiles keep iterating
+            while (groupSize >= 0 && availableTiles.Count > 0)
+            {
+                var startNode = random.PickAndTake(availableTiles);
+                frontier.Clear();
+                frontier.Add(startNode);
+
+                // This essentially may lead to a vein being split in multiple areas but the count matters more than position.
+                while (frontier.Count > 0 && groupSize >= 0)
+                {
+                    // Need to pick a random index so we don't just get straight lines of ores.
+                    var frontierIndex = random.Next(frontier.Count);
+                    var node = frontier[frontierIndex];
+                    frontier.RemoveSwap(frontierIndex);
+                    availableTiles.Remove(node);
+
+                    // Add neighbors if they're valid, worst case we add no more and pick another random seed tile.
+                    for (var x = -1; x <= 1; x++)
+                    {
+                        for (var y = -1; y <= 1; y++)
+                        {
+                            if (x != 0 && y != 0)
+                                continue;
+
+                            var neighbor = new Vector2i(node.X + x, node.Y + y);
+
+                            if (frontier.Contains(neighbor) || !availableTiles.Contains(neighbor))
+                                continue;
+
+                            frontier.Add(neighbor);
+                        }
+                    }
+
+                    var prototype = gen.Entity;
+
+                    if (replaceEntities.TryGetValue(node, out var existingEnt))
+                    {
+                        var existingProto = _entManager.GetComponent<MetaDataComponent>(existingEnt).EntityPrototype;
+                        _entManager.DeleteEntity(existingEnt);
+
+                        if (existingProto != null && remapping.TryGetValue(existingProto.ID, out var remapped))
+                        {
+                            prototype = remapped;
+                        }
+                    }
+
+                    // Tile valid salad so add it.
+                    _entManager.SpawnAtPosition(prototype, _maps.GridTileToLocal(_gridUid, _grid, node));
+
+                    groupSize--;
+                }
+            }
+
+            if (groupSize > 0)
+            {
+                _sawmill.Warning($"Found remaining group size for ore veins!");
+            }
+        }
+    }
+}
diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.PostGen.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGen.cs
new file mode 100644 (file)
index 0000000..b1c8334
--- /dev/null
@@ -0,0 +1,134 @@
+using System.Numerics;
+using Content.Shared.Procedural;
+using Robust.Shared.Collections;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Physics.Components;
+
+namespace Content.Server.Procedural.DungeonJob;
+
+public sealed partial class DungeonJob
+{
+    /*
+     * Run after the main dungeon generation
+     */
+
+    private bool HasWall(Vector2i tile)
+    {
+        var anchored = _maps.GetAnchoredEntitiesEnumerator(_gridUid, _grid, tile);
+
+        while (anchored.MoveNext(out var uid))
+        {
+            if (_tags.HasTag(uid.Value, "Wall"))
+                return true;
+        }
+
+        return false;
+    }
+
+    private void BuildCorridorExterior(Dungeon dungeon)
+    {
+        var exterior = dungeon.CorridorExteriorTiles;
+
+        // Just ignore entrances or whatever for now.
+        foreach (var tile in dungeon.CorridorTiles)
+        {
+            for (var x = -1; x <= 1; x++)
+            {
+                for (var y = -1; y <= 1; y++)
+                {
+                    var neighbor = new Vector2i(tile.X + x, tile.Y + y);
+
+                    if (dungeon.CorridorTiles.Contains(neighbor) ||
+                        dungeon.RoomExteriorTiles.Contains(neighbor) ||
+                        dungeon.RoomTiles.Contains(neighbor) ||
+                        dungeon.Entrances.Contains(neighbor))
+                    {
+                        continue;
+                    }
+
+                    exterior.Add(neighbor);
+                }
+            }
+        }
+    }
+
+    private void WidenCorridor(Dungeon dungeon, float width, ICollection<Vector2i> corridorTiles)
+    {
+        var expansion = width - 2;
+
+        // Widen the path
+        if (expansion >= 1)
+        {
+            var toAdd = new ValueList<Vector2i>();
+
+            foreach (var node in corridorTiles)
+            {
+                // Uhhh not sure on the cleanest way to do this but tl;dr we don't want to hug
+                // exterior walls and make the path smaller.
+
+                for (var x = -expansion; x <= expansion; x++)
+                {
+                    for (var y = -expansion; y <= expansion; y++)
+                    {
+                        var neighbor = new Vector2(node.X + x, node.Y + y).Floored();
+
+                        // Diagonals still matter here.
+                        if (dungeon.RoomTiles.Contains(neighbor) ||
+                            dungeon.RoomExteriorTiles.Contains(neighbor))
+                        {
+                            // Try
+
+                            continue;
+                        }
+
+                        toAdd.Add(neighbor);
+                    }
+                }
+            }
+
+            foreach (var node in toAdd)
+            {
+                corridorTiles.Add(node);
+            }
+        }
+    }
+
+    /// <summary>
+    /// Removes any unwanted obstacles around a door tile.
+    /// </summary>
+    private void ClearDoor(Dungeon dungeon, MapGridComponent grid, Vector2i indices, bool strict = false)
+    {
+        var flags = strict
+            ? LookupFlags.Dynamic | LookupFlags.Static | LookupFlags.StaticSundries
+            : LookupFlags.Dynamic | LookupFlags.Static;
+
+        for (var x = -1; x <= 1; x++)
+        {
+            for (var y = -1; y <= 1; y++)
+            {
+                if (x != 0 && y != 0)
+                    continue;
+
+                var neighbor = new Vector2i(indices.X + x, indices.Y + y);
+
+                if (!dungeon.RoomTiles.Contains(neighbor))
+                    continue;
+
+                // Shrink by 0.01 to avoid polygon overlap from neighboring tiles.
+                // TODO: Uhh entityset re-usage.
+                foreach (var ent in _lookup.GetEntitiesIntersecting(_gridUid, new Box2(neighbor * grid.TileSize, (neighbor + 1) * grid.TileSize).Enlarged(-0.1f), flags))
+                {
+                    if (!_physicsQuery.TryGetComponent(ent, out var physics) ||
+                        !physics.Hard ||
+                        (DungeonSystem.CollisionMask & physics.CollisionLayer) == 0x0 &&
+                        (DungeonSystem.CollisionLayer & physics.CollisionMask) == 0x0)
+                    {
+                        continue;
+                    }
+
+                    _entManager.DeleteEntity(ent);
+                }
+            }
+        }
+    }
+}
diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenAutoCabling.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenAutoCabling.cs
new file mode 100644 (file)
index 0000000..aaea23d
--- /dev/null
@@ -0,0 +1,162 @@
+using System.Linq;
+using System.Threading.Tasks;
+using Content.Server.NodeContainer;
+using Content.Shared.Procedural;
+using Content.Shared.Procedural.PostGeneration;
+using Robust.Shared.Random;
+
+namespace Content.Server.Procedural.DungeonJob;
+
+public sealed partial class DungeonJob
+{
+    /// <summary>
+    /// <see cref="AutoCablingDunGen"/>
+    /// </summary>
+    private async Task PostGen(AutoCablingDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
+    {
+        if (!data.Entities.TryGetValue(DungeonDataKey.Cabling, out var ent))
+        {
+            LogDataError(typeof(AutoCablingDunGen));
+            return;
+        }
+
+        // There's a lot of ways you could do this.
+        // For now we'll just connect every LV cable in the dungeon.
+        var cableTiles = new HashSet<Vector2i>();
+        var allTiles = new HashSet<Vector2i>(dungeon.CorridorTiles);
+        allTiles.UnionWith(dungeon.RoomTiles);
+        allTiles.UnionWith(dungeon.RoomExteriorTiles);
+        allTiles.UnionWith(dungeon.CorridorExteriorTiles);
+        var nodeQuery = _entManager.GetEntityQuery<NodeContainerComponent>();
+
+        // Gather existing nodes
+        foreach (var tile in allTiles)
+        {
+            var anchored = _maps.GetAnchoredEntitiesEnumerator(_gridUid, _grid, tile);
+
+            while (anchored.MoveNext(out var anc))
+            {
+                if (!nodeQuery.TryGetComponent(anc, out var nodeContainer) ||
+                   !nodeContainer.Nodes.ContainsKey("power"))
+                {
+                    continue;
+                }
+
+                cableTiles.Add(tile);
+                break;
+            }
+        }
+
+        // Iterating them all might be expensive.
+        await SuspendDungeon();
+
+        if (!ValidateResume())
+            return;
+
+        var startNodes = new List<Vector2i>(cableTiles);
+        random.Shuffle(startNodes);
+        var start = startNodes[0];
+        var remaining = new HashSet<Vector2i>(startNodes);
+        var frontier = new PriorityQueue<Vector2i, float>();
+        frontier.Enqueue(start, 0f);
+        var cameFrom = new Dictionary<Vector2i, Vector2i>();
+        var costSoFar = new Dictionary<Vector2i, float>();
+        var lastDirection = new Dictionary<Vector2i, Direction>();
+        costSoFar[start] = 0f;
+        lastDirection[start] = Direction.Invalid;
+
+        while (remaining.Count > 0)
+        {
+            if (frontier.Count == 0)
+            {
+                var newStart = remaining.First();
+                frontier.Enqueue(newStart, 0f);
+                lastDirection[newStart] = Direction.Invalid;
+            }
+
+            var node = frontier.Dequeue();
+
+            if (remaining.Remove(node))
+            {
+                var weh = node;
+
+                while (cameFrom.TryGetValue(weh, out var receiver))
+                {
+                    cableTiles.Add(weh);
+                    weh = receiver;
+
+                    if (weh == start)
+                        break;
+                }
+            }
+
+            if (!_maps.TryGetTileRef(_gridUid, _grid, node, out var tileRef) || tileRef.Tile.IsEmpty)
+            {
+                continue;
+            }
+
+            for (var i = 0; i < 4; i++)
+            {
+                var dir = (Direction) (i * 2);
+
+                var neighbor = node + dir.ToIntVec();
+                var tileCost = 1f;
+
+                // Prefer straight lines.
+                if (lastDirection[node] != dir)
+                {
+                    tileCost *= 1.1f;
+                }
+
+                if (cableTiles.Contains(neighbor))
+                {
+                    tileCost *= 0.1f;
+                }
+
+                // Prefer tiles without walls on them
+                if (HasWall(neighbor))
+                {
+                    tileCost *= 20f;
+                }
+
+                var gScore = costSoFar[node] + tileCost;
+
+                if (costSoFar.TryGetValue(neighbor, out var nextValue) && gScore >= nextValue)
+                {
+                    continue;
+                }
+
+                cameFrom[neighbor] = node;
+                costSoFar[neighbor] = gScore;
+                lastDirection[neighbor] = dir;
+                frontier.Enqueue(neighbor, gScore);
+            }
+        }
+
+        foreach (var tile in cableTiles)
+        {
+            if (reservedTiles.Contains(tile))
+                continue;
+
+            var anchored = _maps.GetAnchoredEntitiesEnumerator(_gridUid, _grid, tile);
+            var found = false;
+
+            while (anchored.MoveNext(out var anc))
+            {
+                if (!nodeQuery.TryGetComponent(anc, out var nodeContainer) ||
+                    !nodeContainer.Nodes.ContainsKey("power"))
+                {
+                    continue;
+                }
+
+                found = true;
+                break;
+            }
+
+            if (found)
+                continue;
+
+            _entManager.SpawnEntity(ent, _maps.GridTileToLocal(_gridUid, _grid, tile));
+        }
+    }
+}
diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenBiome.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenBiome.cs
new file mode 100644 (file)
index 0000000..65f6d2d
--- /dev/null
@@ -0,0 +1,67 @@
+using System.Threading.Tasks;
+using Content.Server.Parallax;
+using Content.Shared.Parallax.Biomes;
+using Content.Shared.Procedural;
+using Content.Shared.Procedural.PostGeneration;
+using Robust.Shared.Map;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Procedural.DungeonJob;
+
+public sealed partial class DungeonJob
+{
+    /// <summary>
+    /// <see cref="BiomeDunGen"/>
+    /// </summary>
+    private async Task PostGen(BiomeDunGen dunGen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
+    {
+        if (_entManager.TryGetComponent(_gridUid, out BiomeComponent? biomeComp))
+            return;
+
+        biomeComp = _entManager.AddComponent<BiomeComponent>(_gridUid);
+        var biomeSystem = _entManager.System<BiomeSystem>();
+        biomeSystem.SetTemplate(_gridUid, biomeComp, _prototype.Index(dunGen.BiomeTemplate));
+        var seed = random.Next();
+        var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
+
+        foreach (var node in dungeon.RoomTiles)
+        {
+            if (reservedTiles.Contains(node))
+                continue;
+
+            // Need to set per-tile to override data.
+            if (biomeSystem.TryGetTile(node, biomeComp.Layers, seed, _grid, out var tile))
+            {
+                _maps.SetTile(_gridUid, _grid, node, tile.Value);
+            }
+
+            if (biomeSystem.TryGetDecals(node, biomeComp.Layers, seed, _grid, out var decals))
+            {
+                foreach (var decal in decals)
+                {
+                    _decals.TryAddDecal(decal.ID, new EntityCoordinates(_gridUid, decal.Position), out _);
+                }
+            }
+
+            if (biomeSystem.TryGetEntity(node, biomeComp, _grid, out var entityProto))
+            {
+                var ent = _entManager.SpawnEntity(entityProto, new EntityCoordinates(_gridUid, node + _grid.TileSizeHalfVector));
+                var xform = xformQuery.Get(ent);
+
+                if (!xform.Comp.Anchored)
+                {
+                    _transform.AnchorEntity(ent, xform);
+                }
+
+                // TODO: Engine bug with SpawnAtPosition
+                DebugTools.Assert(xform.Comp.Anchored);
+            }
+
+            await SuspendDungeon();
+            if (!ValidateResume())
+                return;
+        }
+
+        biomeComp.Enabled = false;
+    }
+}
diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenBiomeMarkerLayer.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenBiomeMarkerLayer.cs
new file mode 100644 (file)
index 0000000..fb0eaa0
--- /dev/null
@@ -0,0 +1,105 @@
+using System.Threading.Tasks;
+using Content.Server.Parallax;
+using Content.Shared.Parallax.Biomes;
+using Content.Shared.Parallax.Biomes.Markers;
+using Content.Shared.Procedural;
+using Content.Shared.Procedural.PostGeneration;
+using Content.Shared.Random.Helpers;
+using Robust.Shared.Map;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Procedural.DungeonJob;
+
+public sealed partial class DungeonJob
+{
+    /// <summary>
+    /// <see cref="BiomeMarkerLayerDunGen"/>
+    /// </summary>
+    private async Task PostGen(BiomeMarkerLayerDunGen dunGen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
+    {
+        // If we're adding biome then disable it and just use for markers.
+        if (_entManager.EnsureComponent(_gridUid, out BiomeComponent biomeComp))
+        {
+            biomeComp.Enabled = false;
+        }
+
+        var biomeSystem = _entManager.System<BiomeSystem>();
+        var weightedRandom = _prototype.Index(dunGen.MarkerTemplate);
+        var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
+        var templates = new Dictionary<string, int>();
+
+        for (var i = 0; i < dunGen.Count; i++)
+        {
+            var template = weightedRandom.Pick(random);
+            var count = templates.GetOrNew(template);
+            count++;
+            templates[template] = count;
+        }
+
+        foreach (var (template, count) in templates)
+        {
+            var markerTemplate = _prototype.Index<BiomeMarkerLayerPrototype>(template);
+
+            var bounds = new Box2i();
+
+            foreach (var tile in dungeon.RoomTiles)
+            {
+                bounds = bounds.UnionTile(tile);
+            }
+
+            await SuspendDungeon();
+            if (!ValidateResume())
+                return;
+
+            biomeSystem.GetMarkerNodes(_gridUid, biomeComp, _grid, markerTemplate, true, bounds, count,
+                random, out var spawnSet, out var existing, false);
+
+            await SuspendDungeon();
+            if (!ValidateResume())
+                return;
+
+            var checkTile = reservedTiles.Count > 0;
+
+            foreach (var ent in existing)
+            {
+                if (checkTile && reservedTiles.Contains(_maps.LocalToTile(_gridUid, _grid, _xformQuery.GetComponent(ent).Coordinates)))
+                {
+                    continue;
+                }
+
+                _entManager.DeleteEntity(ent);
+
+                await SuspendDungeon();
+                if (!ValidateResume())
+                    return;
+            }
+
+            foreach (var (node, mask) in spawnSet)
+            {
+                if (reservedTiles.Contains(node))
+                    continue;
+
+                string? proto;
+
+                if (mask != null && markerTemplate.EntityMask.TryGetValue(mask, out var maskedProto))
+                {
+                    proto = maskedProto;
+                }
+                else
+                {
+                    proto = markerTemplate.Prototype;
+                }
+
+                var ent = _entManager.SpawnAtPosition(proto, new EntityCoordinates(_gridUid, node + _grid.TileSizeHalfVector));
+                var xform = xformQuery.Get(ent);
+
+                if (!xform.Comp.Anchored)
+                    _transform.AnchorEntity(ent, xform);
+
+                await SuspendDungeon();
+                if (!ValidateResume())
+                    return;
+            }
+        }
+    }
+}
diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenBoundaryWall.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenBoundaryWall.cs
new file mode 100644 (file)
index 0000000..84697a5
--- /dev/null
@@ -0,0 +1,113 @@
+using System.Threading.Tasks;
+using Content.Shared.Maps;
+using Content.Shared.Procedural;
+using Content.Shared.Procedural.PostGeneration;
+using Robust.Shared.Map;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Procedural.DungeonJob;
+
+public sealed partial class DungeonJob
+{
+    /// <summary>
+    /// <see cref="BoundaryWallDunGen"/>
+    /// </summary>
+    private async Task PostGen(BoundaryWallDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
+    {
+        if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var protoTileDef) ||
+            !data.Entities.TryGetValue(DungeonDataKey.Walls, out var wall))
+        {
+            _sawmill.Error($"Error finding dungeon data for {nameof(gen)}");
+            return;
+        }
+
+        var tileDef = _tileDefManager[protoTileDef];
+        var tiles = new List<(Vector2i Index, Tile Tile)>(dungeon.RoomExteriorTiles.Count);
+
+        if (!data.Entities.TryGetValue(DungeonDataKey.CornerWalls, out var cornerWall))
+        {
+            cornerWall = wall;
+        }
+
+        if (cornerWall == default)
+        {
+            cornerWall = wall;
+        }
+
+        // Spawn wall outline
+        // - Tiles first
+        foreach (var neighbor in dungeon.RoomExteriorTiles)
+        {
+            DebugTools.Assert(!dungeon.RoomTiles.Contains(neighbor));
+
+            if (dungeon.Entrances.Contains(neighbor))
+                continue;
+
+            if (!_anchorable.TileFree(_grid, neighbor, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
+                continue;
+
+            tiles.Add((neighbor, _tile.GetVariantTile((ContentTileDefinition) tileDef, random)));
+        }
+
+        foreach (var index in dungeon.CorridorExteriorTiles)
+        {
+            if (dungeon.RoomTiles.Contains(index))
+                continue;
+
+            if (!_anchorable.TileFree(_grid, index, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
+                continue;
+
+            tiles.Add((index, _tile.GetVariantTile((ContentTileDefinition)tileDef, random)));
+        }
+
+        _maps.SetTiles(_gridUid, _grid, tiles);
+
+        // Double iteration coz we bulk set tiles for speed.
+        for (var i = 0; i < tiles.Count; i++)
+        {
+            var index = tiles[i];
+
+            if (!_anchorable.TileFree(_grid, index.Index, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
+                continue;
+
+            // If no cardinal neighbors in dungeon then we're a corner.
+            var isCorner = true;
+
+            for (var x = -1; x <= 1; x++)
+            {
+                for (var y = -1; y <= 1; y++)
+                {
+                    if (x != 0 && y != 0)
+                    {
+                        continue;
+                    }
+
+                    var neighbor = new Vector2i(index.Index.X + x, index.Index.Y + y);
+
+                    if (dungeon.RoomTiles.Contains(neighbor) || dungeon.CorridorTiles.Contains(neighbor))
+                    {
+                        isCorner = false;
+                        break;
+                    }
+                }
+
+                if (!isCorner)
+                    break;
+            }
+
+            if (isCorner)
+                _entManager.SpawnEntity(cornerWall, _maps.GridTileToLocal(_gridUid, _grid, index.Index));
+
+            if (!isCorner)
+                _entManager.SpawnEntity(wall, _maps.GridTileToLocal(_gridUid, _grid, index.Index));
+
+            if (i % 20 == 0)
+            {
+                await SuspendDungeon();
+
+                if (!ValidateResume())
+                    return;
+            }
+        }
+    }
+}
diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenCornerClutter.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenCornerClutter.cs
new file mode 100644 (file)
index 0000000..f785829
--- /dev/null
@@ -0,0 +1,56 @@
+using System.Threading.Tasks;
+using Content.Shared.Procedural;
+using Content.Shared.Procedural.PostGeneration;
+using Content.Shared.Storage;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Random;
+
+namespace Content.Server.Procedural.DungeonJob;
+
+public sealed partial class DungeonJob
+{
+    /// <summary>
+    /// <see cref="CornerClutterDunGen"/>
+    /// </summary>
+    private async Task PostGen(CornerClutterDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
+    {
+        if (!data.SpawnGroups.TryGetValue(DungeonDataKey.CornerClutter, out var corner))
+        {
+            _sawmill.Error(Environment.StackTrace);
+            return;
+        }
+
+        foreach (var tile in dungeon.CorridorTiles)
+        {
+            var blocked = _anchorable.TileFree(_grid, tile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask);
+
+            if (blocked)
+                continue;
+
+            // If at least 2 adjacent tiles are blocked consider it a corner
+            for (var i = 0; i < 4; i++)
+            {
+                var dir = (Direction) (i * 2);
+                blocked = HasWall(tile + dir.ToIntVec());
+
+                if (!blocked)
+                    continue;
+
+                var nextDir = (Direction) ((i + 1) * 2 % 8);
+                blocked = HasWall(tile + nextDir.ToIntVec());
+
+                if (!blocked)
+                    continue;
+
+                if (random.Prob(gen.Chance))
+                {
+                    var coords = _maps.GridTileToLocal(_gridUid, _grid, tile);
+                    var protos = EntitySpawnCollection.GetSpawns(_prototype.Index(corner).Entries, random);
+                    _entManager.SpawnEntities(coords, protos);
+                }
+
+                break;
+            }
+        }
+    }
+}
diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenCorridor.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenCorridor.cs
new file mode 100644 (file)
index 0000000..8ea79ff
--- /dev/null
@@ -0,0 +1,116 @@
+using System.Numerics;
+using System.Threading.Tasks;
+using Content.Shared.Maps;
+using Content.Shared.Procedural;
+using Content.Shared.Procedural.PostGeneration;
+using Robust.Shared.Map;
+
+namespace Content.Server.Procedural.DungeonJob;
+
+public sealed partial class DungeonJob
+{
+    /// <summary>
+    /// <see cref="CorridorDunGen"/>
+    /// </summary>
+    private async Task PostGen(CorridorDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
+    {
+        if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto))
+        {
+            LogDataError(typeof(CorridorDunGen));
+            return;
+        }
+
+        var entrances = new List<Vector2i>(dungeon.Rooms.Count);
+
+        // Grab entrances
+        foreach (var room in dungeon.Rooms)
+        {
+            entrances.AddRange(room.Entrances);
+        }
+
+        var edges = _dungeon.MinimumSpanningTree(entrances, random);
+        await SuspendDungeon();
+
+        if (!ValidateResume())
+            return;
+
+        // TODO: Add in say 1/3 of edges back in to add some cyclic to it.
+
+        var expansion = gen.Width - 2;
+        // Okay so tl;dr is that we don't want to cut close to rooms as it might go from 3 width to 2 width suddenly
+        // So we will add a buffer range around each room to deter pathfinding there unless necessary
+        var deterredTiles = new HashSet<Vector2i>();
+
+        if (expansion >= 1)
+        {
+            foreach (var tile in dungeon.RoomExteriorTiles)
+            {
+                for (var x = -expansion; x <= expansion; x++)
+                {
+                    for (var y = -expansion; y <= expansion; y++)
+                    {
+                        var neighbor = new Vector2(tile.X + x, tile.Y + y).Floored();
+
+                        if (dungeon.RoomTiles.Contains(neighbor) ||
+                            dungeon.RoomExteriorTiles.Contains(neighbor) ||
+                            entrances.Contains(neighbor))
+                        {
+                            continue;
+                        }
+
+                        deterredTiles.Add(neighbor);
+                    }
+                }
+            }
+        }
+
+        foreach (var room in dungeon.Rooms)
+        {
+            foreach (var entrance in room.Entrances)
+            {
+                // Just so we can still actually get in to the entrance we won't deter from a tile away from it.
+                var normal = (entrance + _grid.TileSizeHalfVector - room.Center).ToWorldAngle().GetCardinalDir().ToIntVec();
+                deterredTiles.Remove(entrance + normal);
+            }
+        }
+
+        var excludedTiles = new HashSet<Vector2i>(dungeon.RoomExteriorTiles);
+        excludedTiles.UnionWith(dungeon.RoomTiles);
+        var corridorTiles = new HashSet<Vector2i>();
+
+        _dungeon.GetCorridorNodes(corridorTiles, edges, gen.PathLimit, excludedTiles, tile =>
+        {
+            var mod = 1f;
+
+            if (corridorTiles.Contains(tile))
+            {
+                mod *= 0.1f;
+            }
+
+            if (deterredTiles.Contains(tile))
+            {
+                mod *= 2f;
+            }
+
+            return mod;
+        });
+
+        WidenCorridor(dungeon, gen.Width, corridorTiles);
+
+        var setTiles = new List<(Vector2i, Tile)>();
+        var tileDef = (ContentTileDefinition) _tileDefManager[tileProto];
+
+        foreach (var tile in corridorTiles)
+        {
+            if (reservedTiles.Contains(tile))
+                continue;
+
+            setTiles.Add((tile, _tile.GetVariantTile(tileDef, random)));
+        }
+
+        _maps.SetTiles(_gridUid, _grid, setTiles);
+        dungeon.CorridorTiles.UnionWith(corridorTiles);
+        dungeon.RefreshAllTiles();
+        BuildCorridorExterior(dungeon);
+    }
+}
similarity index 83%
rename from Content.Server/Procedural/DungeonJob.CorridorClutterPost.cs
rename to Content.Server/Procedural/DungeonJob/DungeonJob.PostGenCorridorClutter.cs
index 8099157cc5080ac57343b9ad058ab92c502ed69a..cb7c4b210c86ee4d4c7cc8c50b2e9d7d0cd392bf 100644 (file)
@@ -2,16 +2,17 @@ using System.Threading.Tasks;
 using Content.Shared.Procedural;
 using Content.Shared.Procedural.PostGeneration;
 using Content.Shared.Storage;
-using Robust.Shared.Map.Components;
 using Robust.Shared.Physics.Components;
 using Robust.Shared.Random;
 
-namespace Content.Server.Procedural;
+namespace Content.Server.Procedural.DungeonJob;
 
 public sealed partial class DungeonJob
 {
-    private async Task PostGen(CorridorClutterPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid,
-        Random random)
+    /// <summary>
+    /// <see cref="CorridorClutterDunGen"/>
+    /// </summary>
+    private async Task PostGen(CorridorClutterDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
     {
         var physicsQuery = _entManager.GetEntityQuery<PhysicsComponent>();
         var count = (int) Math.Ceiling(dungeon.CorridorTiles.Count * gen.Chance);
diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenCorridorDecalSkirting.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenCorridorDecalSkirting.cs
new file mode 100644 (file)
index 0000000..3b516c3
--- /dev/null
@@ -0,0 +1,124 @@
+using System.Threading.Tasks;
+using Content.Shared.Doors.Components;
+using Content.Shared.Procedural;
+using Content.Shared.Procedural.PostGeneration;
+using Robust.Shared.Collections;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Procedural.DungeonJob;
+
+public sealed partial class DungeonJob
+{
+    /// <summary>
+    /// <see cref="CorridorDecalSkirtingDunGen"/>
+    /// </summary>
+    private async Task PostGen(CorridorDecalSkirtingDunGen decks, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
+    {
+        if (!data.Colors.TryGetValue(DungeonDataKey.Decals, out var color))
+        {
+            _sawmill.Error(Environment.StackTrace);
+        }
+
+        var directions = new ValueList<DirectionFlag>(4);
+        var pocketDirections = new ValueList<Direction>(4);
+        var doorQuery = _entManager.GetEntityQuery<DoorComponent>();
+        var physicsQuery = _entManager.GetEntityQuery<PhysicsComponent>();
+        var offset = -_grid.TileSizeHalfVector;
+
+        foreach (var tile in dungeon.CorridorTiles)
+        {
+            DebugTools.Assert(!dungeon.RoomTiles.Contains(tile));
+            directions.Clear();
+
+            // Do cardinals 1 step
+            // Do corners the other step
+            for (var i = 0; i < 4; i++)
+            {
+                var dir = (DirectionFlag) Math.Pow(2, i);
+                var neighbor = tile + dir.AsDir().ToIntVec();
+
+                var anc = _maps.GetAnchoredEntitiesEnumerator(_gridUid, _grid, neighbor);
+
+                while (anc.MoveNext(out var ent))
+                {
+                    if (!physicsQuery.TryGetComponent(ent, out var physics) ||
+                        !physics.CanCollide ||
+                        !physics.Hard ||
+                        doorQuery.HasComponent(ent.Value))
+                    {
+                        continue;
+                    }
+
+                    directions.Add(dir);
+                    break;
+                }
+            }
+
+            // Pockets
+            if (directions.Count == 0)
+            {
+                pocketDirections.Clear();
+
+                for (var i = 1; i < 5; i++)
+                {
+                    var dir = (Direction) (i * 2 - 1);
+                    var neighbor = tile + dir.ToIntVec();
+
+                    var anc = _maps.GetAnchoredEntitiesEnumerator(_gridUid, _grid, neighbor);
+
+                    while (anc.MoveNext(out var ent))
+                    {
+                        if (!physicsQuery.TryGetComponent(ent, out var physics) ||
+                            !physics.CanCollide ||
+                            !physics.Hard ||
+                            doorQuery.HasComponent(ent.Value))
+                        {
+                            continue;
+                        }
+
+                        pocketDirections.Add(dir);
+                        break;
+                    }
+                }
+
+                if (pocketDirections.Count == 1)
+                {
+                    if (decks.PocketDecals.TryGetValue(pocketDirections[0], out var cDir))
+                    {
+                        // Decals not being centered biting my ass again
+                        var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile).Offset(offset);
+                        _decals.TryAddDecal(cDir, gridPos, out _, color: color);
+                    }
+                }
+
+                continue;
+            }
+
+            if (directions.Count == 1)
+            {
+                if (decks.CardinalDecals.TryGetValue(directions[0], out var cDir))
+                {
+                    // Decals not being centered biting my ass again
+                    var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile).Offset(offset);
+                    _decals.TryAddDecal(cDir, gridPos, out _, color: color);
+                }
+
+                continue;
+            }
+
+            // Corners
+            if (directions.Count == 2)
+            {
+                // Auehghegueugegegeheh help me
+                var dirFlag = directions[0] | directions[1];
+
+                if (decks.CornerDecals.TryGetValue(dirFlag, out var cDir))
+                {
+                    var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile).Offset(offset);
+                    _decals.TryAddDecal(cDir, gridPos, out _, color: color);
+                }
+            }
+        }
+    }
+}
diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenDungeonConnector.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenDungeonConnector.cs
new file mode 100644 (file)
index 0000000..917b1ff
--- /dev/null
@@ -0,0 +1,6 @@
+namespace Content.Server.Procedural.DungeonJob;
+
+public sealed partial class DungeonJob
+{
+
+}
diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenDungeonEntrance.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenDungeonEntrance.cs
new file mode 100644 (file)
index 0000000..abc52f0
--- /dev/null
@@ -0,0 +1,114 @@
+using System.Threading.Tasks;
+using Content.Shared.Maps;
+using Content.Shared.Procedural;
+using Content.Shared.Procedural.PostGeneration;
+using Content.Shared.Storage;
+using Robust.Shared.Random;
+
+namespace Content.Server.Procedural.DungeonJob;
+
+public sealed partial class DungeonJob
+{
+    /// <summary>
+    /// <see cref="DungeonEntranceDunGen"/>
+    /// </summary>
+    private async Task PostGen(DungeonEntranceDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
+    {
+        if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto) ||
+            !data.SpawnGroups.TryGetValue(DungeonDataKey.Entrance, out var entrance))
+        {
+            LogDataError(typeof(DungeonEntranceDunGen));
+            return;
+        }
+
+        var rooms = new List<DungeonRoom>(dungeon.Rooms);
+        var roomTiles = new List<Vector2i>();
+        var tileDef = (ContentTileDefinition) _tileDefManager[tileProto];
+
+        for (var i = 0; i < gen.Count; i++)
+        {
+            var roomIndex = random.Next(rooms.Count);
+            var room = rooms[roomIndex];
+
+            // Move out 3 tiles in a direction away from center of the room
+            // If none of those intersect another tile it's probably external
+            // TODO: Maybe need to take top half of furthest rooms in case there's interior exits?
+            roomTiles.AddRange(room.Exterior);
+            random.Shuffle(roomTiles);
+
+            foreach (var tile in roomTiles)
+            {
+                var isValid = false;
+
+                // Check if one side is dungeon and the other side is nothing.
+                for (var j = 0; j < 4; j++)
+                {
+                    var dir = (Direction) (j * 2);
+                    var oppositeDir = dir.GetOpposite();
+                    var dirVec = tile + dir.ToIntVec();
+                    var oppositeDirVec = tile + oppositeDir.ToIntVec();
+
+                    if (!dungeon.RoomTiles.Contains(dirVec))
+                    {
+                        continue;
+                    }
+
+                    if (dungeon.RoomTiles.Contains(oppositeDirVec) ||
+                        dungeon.RoomExteriorTiles.Contains(oppositeDirVec) ||
+                        dungeon.CorridorExteriorTiles.Contains(oppositeDirVec) ||
+                        dungeon.CorridorTiles.Contains(oppositeDirVec))
+                    {
+                        continue;
+                    }
+
+                    // Check if exterior spot free.
+                    if (!_anchorable.TileFree(_grid, tile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
+                    {
+                        continue;
+                    }
+
+                    // Check if interior spot free (no guarantees on exterior but ClearDoor should handle it)
+                    if (!_anchorable.TileFree(_grid, dirVec, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
+                    {
+                        continue;
+                    }
+
+                    // Valid pick!
+                    isValid = true;
+
+                    // Entrance wew
+                    _maps.SetTile(_gridUid, _grid, tile, _tile.GetVariantTile(tileDef, random));
+                    ClearDoor(dungeon, _grid, tile);
+                    var gridCoords = _maps.GridTileToLocal(_gridUid, _grid, tile);
+                    // Need to offset the spawn to avoid spawning in the room.
+
+                    foreach (var ent in EntitySpawnCollection.GetSpawns(_prototype.Index(entrance).Entries, random))
+                    {
+                        _entManager.SpawnAtPosition(ent, gridCoords);
+                    }
+
+                    // Clear out any biome tiles nearby to avoid blocking it
+                    foreach (var nearTile in _maps.GetLocalTilesIntersecting(_gridUid, _grid, new Circle(gridCoords.Position, 1.5f), false))
+                    {
+                        if (dungeon.RoomTiles.Contains(nearTile.GridIndices) ||
+                            dungeon.RoomExteriorTiles.Contains(nearTile.GridIndices) ||
+                            dungeon.CorridorTiles.Contains(nearTile.GridIndices) ||
+                            dungeon.CorridorExteriorTiles.Contains(nearTile.GridIndices))
+                        {
+                            continue;
+                        }
+
+                        _maps.SetTile(_gridUid, _grid, nearTile.GridIndices, _tile.GetVariantTile(tileDef, random));
+                    }
+
+                    break;
+                }
+
+                if (isValid)
+                    break;
+            }
+
+            roomTiles.Clear();
+        }
+    }
+}
diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenEntranceFlank.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenEntranceFlank.cs
new file mode 100644 (file)
index 0000000..3a1c7a3
--- /dev/null
@@ -0,0 +1,58 @@
+using System.Threading.Tasks;
+using Content.Shared.Maps;
+using Content.Shared.Procedural;
+using Content.Shared.Procedural.PostGeneration;
+using Content.Shared.Storage;
+using Robust.Shared.Collections;
+using Robust.Shared.Map;
+
+namespace Content.Server.Procedural.DungeonJob;
+
+public sealed partial class DungeonJob
+{
+    /// <summary>
+    /// <see cref="EntranceFlankDunGen"/>
+    /// </summary>
+    private async Task PostGen(EntranceFlankDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
+    {
+        if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto) ||
+            !data.SpawnGroups.TryGetValue(DungeonDataKey.EntranceFlank, out var flankProto))
+        {
+            _sawmill.Error($"Unable to get dungeon data for {nameof(gen)}");
+            return;
+        }
+
+        var tiles = new List<(Vector2i Index, Tile)>();
+        var tileDef = _tileDefManager[tileProto];
+        var spawnPositions = new ValueList<Vector2i>(dungeon.Rooms.Count);
+
+        foreach (var room in dungeon.Rooms)
+        {
+            foreach (var entrance in room.Entrances)
+            {
+                for (var i = 0; i < 8; i++)
+                {
+                    var dir = (Direction) i;
+                    var neighbor = entrance + dir.ToIntVec();
+
+                    if (!dungeon.RoomExteriorTiles.Contains(neighbor))
+                        continue;
+
+                    if (reservedTiles.Contains(neighbor))
+                        continue;
+
+                    tiles.Add((neighbor, _tile.GetVariantTile((ContentTileDefinition) tileDef, random)));
+                    spawnPositions.Add(neighbor);
+                }
+            }
+        }
+
+        _maps.SetTiles(_gridUid, _grid, tiles);
+        var entGroup = _prototype.Index(flankProto);
+
+        foreach (var entrance in spawnPositions)
+        {
+            _entManager.SpawnEntities(_maps.GridTileToLocal(_gridUid, _grid, entrance), EntitySpawnCollection.GetSpawns(entGroup.Entries, random));
+        }
+    }
+}
diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenExternalWindow.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenExternalWindow.cs
new file mode 100644 (file)
index 0000000..9a1b44e
--- /dev/null
@@ -0,0 +1,138 @@
+using System.Linq;
+using System.Threading.Tasks;
+using Content.Shared.Maps;
+using Content.Shared.Procedural;
+using Content.Shared.Procedural.PostGeneration;
+using Content.Shared.Storage;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Random;
+
+namespace Content.Server.Procedural.DungeonJob;
+
+public sealed partial class DungeonJob
+{
+    // (Comment refers to internal & external).
+
+    /*
+     * You may be wondering why these are different.
+     * It's because for internals we want to force it as it looks nicer and not leave it up to chance.
+     */
+
+    // TODO: Can probably combine these a bit, their differences are in really annoying to pull out spots.
+
+    /// <summary>
+    /// <see cref="ExternalWindowDunGen"/>
+    /// </summary>
+    private async Task PostGen(ExternalWindowDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
+    {
+        if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto) ||
+            !data.SpawnGroups.TryGetValue(DungeonDataKey.Window, out var windowGroup))
+        {
+            _sawmill.Error($"Unable to get dungeon data for {nameof(gen)}");
+            return;
+        }
+
+        // Iterate every tile with N chance to spawn windows on that wall per cardinal dir.
+        var chance = 0.25 / 3f;
+
+        var allExterior = new HashSet<Vector2i>(dungeon.CorridorExteriorTiles);
+        allExterior.UnionWith(dungeon.RoomExteriorTiles);
+        var validTiles = allExterior.ToList();
+        random.Shuffle(validTiles);
+
+        var tiles = new List<(Vector2i, Tile)>();
+        var tileDef = _tileDefManager[tileProto];
+        var count = Math.Floor(validTiles.Count * chance);
+        var index = 0;
+        var takenTiles = new HashSet<Vector2i>();
+
+        // There's a bunch of shit here but tl;dr
+        // - don't spawn over cap
+        // - Check if we have 3 tiles in a row that aren't corners and aren't obstructed
+        foreach (var tile in validTiles)
+        {
+            if (index > count)
+                break;
+
+            // Room tile / already used.
+            if (!_anchorable.TileFree(_grid, tile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask) ||
+                takenTiles.Contains(tile))
+            {
+                continue;
+            }
+
+            // Check we're not on a corner
+            for (var i = 0; i < 2; i++)
+            {
+                var dir = (Direction) (i * 2);
+                var dirVec = dir.ToIntVec();
+                var isValid = true;
+
+                // Check 1 beyond either side to ensure it's not a corner.
+                for (var j = -1; j < 4; j++)
+                {
+                    var neighbor = tile + dirVec * j;
+
+                    if (!allExterior.Contains(neighbor) ||
+                        takenTiles.Contains(neighbor) ||
+                        !_anchorable.TileFree(_grid, neighbor, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
+                    {
+                        isValid = false;
+                        break;
+                    }
+
+                    // Also check perpendicular that it is free
+                    foreach (var k in new [] {2, 6})
+                    {
+                        var perp = (Direction) ((i * 2 + k) % 8);
+                        var perpVec = perp.ToIntVec();
+                        var perpTile = tile + perpVec;
+
+                        if (allExterior.Contains(perpTile) ||
+                            takenTiles.Contains(neighbor) ||
+                            !_anchorable.TileFree(_grid, perpTile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
+                        {
+                            isValid = false;
+                            break;
+                        }
+                    }
+
+                    if (!isValid)
+                        break;
+                }
+
+                if (!isValid)
+                    continue;
+
+                for (var j = 0; j < 3; j++)
+                {
+                    var neighbor = tile + dirVec * j;
+
+                    if (reservedTiles.Contains(neighbor))
+                        continue;
+
+                    tiles.Add((neighbor, _tile.GetVariantTile((ContentTileDefinition) tileDef, random)));
+                    index++;
+                    takenTiles.Add(neighbor);
+                }
+            }
+        }
+
+        _maps.SetTiles(_gridUid, _grid, tiles);
+        index = 0;
+        var spawnEntry = _prototype.Index(windowGroup);
+
+        foreach (var tile in tiles)
+        {
+            var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile.Item1);
+
+            index += spawnEntry.Entries.Count;
+            _entManager.SpawnEntities(gridPos, EntitySpawnCollection.GetSpawns(spawnEntry.Entries, random));
+            await SuspendDungeon();
+
+            if (!ValidateResume())
+                return;
+        }
+    }
+}
diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenInternalWindow.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenInternalWindow.cs
new file mode 100644 (file)
index 0000000..d3b8c6d
--- /dev/null
@@ -0,0 +1,108 @@
+using System.Numerics;
+using System.Threading.Tasks;
+using Content.Shared.Maps;
+using Content.Shared.Procedural;
+using Content.Shared.Procedural.PostGeneration;
+using Content.Shared.Storage;
+
+namespace Content.Server.Procedural.DungeonJob;
+
+public sealed partial class DungeonJob
+{
+    /// <summary>
+    /// <see cref="InternalWindowDunGen"/>
+    /// </summary>
+    private async Task PostGen(InternalWindowDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
+    {
+        if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto) ||
+            !data.SpawnGroups.TryGetValue(DungeonDataKey.Window, out var windowGroup))
+        {
+            _sawmill.Error($"Unable to find dungeon data keys for {nameof(gen)}");
+            return;
+        }
+
+        // Iterate every room and check if there's a gap beyond it that leads to another room within N tiles
+        // If so then consider windows
+        var minDistance = 4;
+        var maxDistance = 6;
+        var tileDef = _tileDefManager[tileProto];
+        var window = _prototype.Index(windowGroup);
+
+        foreach (var room in dungeon.Rooms)
+        {
+            var validTiles = new List<Vector2i>();
+
+            for (var i = 0; i < 4; i++)
+            {
+                var dir = (DirectionFlag) Math.Pow(2, i);
+                var dirVec = dir.AsDir().ToIntVec();
+
+                foreach (var tile in room.Tiles)
+                {
+                    var tileAngle = (tile + _grid.TileSizeHalfVector - room.Center).ToAngle();
+                    var roundedAngle = Math.Round(tileAngle.Theta / (Math.PI / 2)) * (Math.PI / 2);
+
+                    var tileVec = (Vector2i) new Angle(roundedAngle).ToVec().Rounded();
+
+                    if (!tileVec.Equals(dirVec))
+                        continue;
+
+                    var valid = false;
+
+                    for (var j = 1; j < maxDistance; j++)
+                    {
+                        var edgeNeighbor = tile + dirVec * j;
+
+                        if (dungeon.RoomTiles.Contains(edgeNeighbor))
+                        {
+                            if (j < minDistance)
+                            {
+                                valid = false;
+                            }
+                            else
+                            {
+                                valid = true;
+                            }
+
+                            break;
+                        }
+                    }
+
+                    if (!valid)
+                        continue;
+
+                    var windowTile = tile + dirVec;
+
+                    if (reservedTiles.Contains(windowTile))
+                        continue;
+
+                    if (!_anchorable.TileFree(_grid, windowTile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
+                        continue;
+
+                    validTiles.Add(windowTile);
+                }
+
+                validTiles.Sort((x, y) => (x + _grid.TileSizeHalfVector - room.Center).LengthSquared().CompareTo((y + _grid.TileSizeHalfVector - room.Center).LengthSquared()));
+
+                for (var j = 0; j < Math.Min(validTiles.Count, 3); j++)
+                {
+                    var tile = validTiles[j];
+                    var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile);
+                    _maps.SetTile(_gridUid, _grid, tile, _tile.GetVariantTile((ContentTileDefinition) tileDef, random));
+
+                    _entManager.SpawnEntities(gridPos, EntitySpawnCollection.GetSpawns(window.Entries, random));
+                }
+
+                if (validTiles.Count > 0)
+                {
+                    await SuspendDungeon();
+
+                    if (!ValidateResume())
+                        return;
+                }
+
+                validTiles.Clear();
+            }
+        }
+    }
+}
diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenJunction.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenJunction.cs
new file mode 100644 (file)
index 0000000..700406e
--- /dev/null
@@ -0,0 +1,144 @@
+using System.Threading.Tasks;
+using Content.Shared.Maps;
+using Content.Shared.Procedural;
+using Content.Shared.Procedural.PostGeneration;
+using Content.Shared.Storage;
+using Robust.Shared.Map.Components;
+
+namespace Content.Server.Procedural.DungeonJob;
+
+public sealed partial class DungeonJob
+{
+    /// <summary>
+    /// <see cref="JunctionDunGen"/>
+    /// </summary>
+    private async Task PostGen(JunctionDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
+    {
+        if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto) ||
+            !data.SpawnGroups.TryGetValue(DungeonDataKey.Junction, out var junctionProto))
+        {
+            _sawmill.Error($"Dungeon data keys are missing for {nameof(gen)}");
+            return;
+        }
+
+        var tileDef = _tileDefManager[tileProto];
+        var entranceGroup = _prototype.Index(junctionProto);
+
+        // N-wide junctions
+        foreach (var tile in dungeon.CorridorTiles)
+        {
+            if (!_anchorable.TileFree(_grid, tile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
+                continue;
+
+            // Check each direction:
+            // - Check if immediate neighbors are free
+            // - Check if the neighbors beyond that are not free
+            // - Then check either side if they're slightly more free
+            var exteriorWidth = (int) Math.Floor(gen.Width / 2f);
+            var width = (int) Math.Ceiling(gen.Width / 2f);
+
+            for (var i = 0; i < 2; i++)
+            {
+                var isValid = true;
+                var neighborDir = (Direction) (i * 2);
+                var neighborVec = neighborDir.ToIntVec();
+
+                for (var j = -width; j <= width; j++)
+                {
+                    if (j == 0)
+                        continue;
+
+                    var neighbor = tile + neighborVec * j;
+
+                    // If it's an end tile then check it's occupied.
+                    if (j == -width ||
+                        j == width)
+                    {
+                        if (!HasWall(neighbor))
+                        {
+                            isValid = false;
+                            break;
+                        }
+
+                        continue;
+                    }
+
+                    // If we're not at the end tile then check it + perpendicular are free.
+                    if (!_anchorable.TileFree(_grid, neighbor, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
+                    {
+                        isValid = false;
+                        break;
+                    }
+
+                    var perp1 = tile + neighborVec * j + ((Direction) ((i * 2 + 2) % 8)).ToIntVec();
+                    var perp2 = tile + neighborVec * j + ((Direction) ((i * 2 + 6) % 8)).ToIntVec();
+
+                    if (!_anchorable.TileFree(_grid, perp1, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
+                    {
+                        isValid = false;
+                        break;
+                    }
+
+                    if (!_anchorable.TileFree(_grid, perp2, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
+                    {
+                        isValid = false;
+                        break;
+                    }
+                }
+
+                if (!isValid)
+                    continue;
+
+                // Check corners to see if either side opens up (if it's just a 1x wide corridor do nothing, needs to be a funnel.
+                foreach (var j in new [] {-exteriorWidth, exteriorWidth})
+                {
+                    var freeCount = 0;
+
+                    // Need at least 3 of 4 free
+                    for (var k = 0; k < 4; k++)
+                    {
+                        var cornerDir = (Direction) (k * 2 + 1);
+                        var cornerVec = cornerDir.ToIntVec();
+                        var cornerNeighbor = tile + neighborVec * j + cornerVec;
+
+                        if (_anchorable.TileFree(_grid, cornerNeighbor, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
+                        {
+                            freeCount++;
+                        }
+                    }
+
+                    if (freeCount < gen.Width)
+                        continue;
+
+                    // Valid!
+                    isValid = true;
+
+                    for (var x = -width + 1; x < width; x++)
+                    {
+                        var weh = tile + neighborDir.ToIntVec() * x;
+
+                        if (reservedTiles.Contains(weh))
+                            continue;
+
+                        _maps.SetTile(_gridUid, _grid, weh, _tile.GetVariantTile((ContentTileDefinition) tileDef, random));
+
+                        var coords = _maps.GridTileToLocal(_gridUid, _grid, weh);
+                        _entManager.SpawnEntities(coords, EntitySpawnCollection.GetSpawns(entranceGroup.Entries, random));
+                    }
+
+                    break;
+                }
+
+                if (isValid)
+                {
+                    await SuspendDungeon();
+
+                    if (!ValidateResume())
+                        return;
+                }
+
+                break;
+            }
+        }
+    }
+}
diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenMiddleConnection.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenMiddleConnection.cs
new file mode 100644 (file)
index 0000000..15d0f63
--- /dev/null
@@ -0,0 +1,147 @@
+using System.Numerics;
+using System.Threading.Tasks;
+using Content.Shared.Maps;
+using Content.Shared.Procedural;
+using Content.Shared.Procedural.PostGeneration;
+using Content.Shared.Storage;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Procedural.DungeonJob;
+
+public sealed partial class DungeonJob
+{
+    /// <summary>
+    /// <see cref="MiddleConnectionDunGen"/>
+    /// </summary>
+    private async Task PostGen(MiddleConnectionDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
+    {
+        if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto) ||
+            !data.SpawnGroups.TryGetValue(DungeonDataKey.Entrance, out var entranceProto) ||
+            !_prototype.TryIndex(entranceProto, out var entrance))
+        {
+            _sawmill.Error($"Tried to run {nameof(MiddleConnectionDunGen)} without any dungeon data set which is unsupported");
+            return;
+        }
+
+        data.SpawnGroups.TryGetValue(DungeonDataKey.EntranceFlank, out var flankProto);
+        _prototype.TryIndex(flankProto, out var flank);
+
+        // Grab all of the room bounds
+        // Then, work out connections between them
+        var roomBorders = new Dictionary<DungeonRoom, HashSet<Vector2i>>(dungeon.Rooms.Count);
+
+        foreach (var room in dungeon.Rooms)
+        {
+            var roomEdges = new HashSet<Vector2i>();
+
+            foreach (var index in room.Tiles)
+            {
+                for (var x = -1; x <= 1; x++)
+                {
+                    for (var y = -1; y <= 1; y++)
+                    {
+                        // Cardinals only
+                        if (x != 0 && y != 0 ||
+                            x == 0 && y == 0)
+                        {
+                            continue;
+                        }
+
+                        var neighbor = new Vector2i(index.X + x, index.Y + y);
+
+                        if (dungeon.RoomTiles.Contains(neighbor))
+                            continue;
+
+                        if (!_anchorable.TileFree(_grid, neighbor, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
+                            continue;
+
+                        roomEdges.Add(neighbor);
+                    }
+                }
+            }
+
+            roomBorders.Add(room, roomEdges);
+        }
+
+        // Do pathfind from first room to work out graph.
+        // TODO: Optional loops
+
+        var roomConnections = new Dictionary<DungeonRoom, List<DungeonRoom>>();
+        var tileDef = _tileDefManager[tileProto];
+
+        foreach (var (room, border) in roomBorders)
+        {
+            var conns = roomConnections.GetOrNew(room);
+
+            foreach (var (otherRoom, otherBorders) in roomBorders)
+            {
+                if (room.Equals(otherRoom) ||
+                    conns.Contains(otherRoom))
+                {
+                    continue;
+                }
+
+                var flipp = new HashSet<Vector2i>(border);
+                flipp.IntersectWith(otherBorders);
+
+                if (flipp.Count == 0 ||
+                    gen.OverlapCount != -1 && flipp.Count != gen.OverlapCount)
+                    continue;
+
+                var center = Vector2.Zero;
+
+                foreach (var node in flipp)
+                {
+                    center += node + _grid.TileSizeHalfVector;
+                }
+
+                center /= flipp.Count;
+                // Weight airlocks towards center more.
+                var nodeDistances = new List<(Vector2i Node, float Distance)>(flipp.Count);
+
+                foreach (var node in flipp)
+                {
+                    nodeDistances.Add((node, (node + _grid.TileSizeHalfVector - center).LengthSquared()));
+                }
+
+                nodeDistances.Sort((x, y) => x.Distance.CompareTo(y.Distance));
+
+                var width = gen.Count;
+
+                for (var i = 0; i < nodeDistances.Count; i++)
+                {
+                    var node = nodeDistances[i].Node;
+                    var gridPos = _maps.GridTileToLocal(_gridUid, _grid, node);
+                    if (!_anchorable.TileFree(_grid, node, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
+                        continue;
+
+                    width--;
+                    _maps.SetTile(_gridUid, _grid, node, _tile.GetVariantTile((ContentTileDefinition) tileDef, random));
+
+                    if (flank != null && nodeDistances.Count - i <= 2)
+                    {
+                        _entManager.SpawnEntities(gridPos, EntitySpawnCollection.GetSpawns(flank.Entries, random));
+                    }
+                    else
+                    {
+                        // Iterate neighbors and check for blockers, if so bulldoze
+                        ClearDoor(dungeon, _grid, node);
+
+                        _entManager.SpawnEntities(gridPos, EntitySpawnCollection.GetSpawns(entrance.Entries, random));
+                    }
+
+                    if (width == 0)
+                        break;
+                }
+
+                conns.Add(otherRoom);
+                var otherConns = roomConnections.GetOrNew(otherRoom);
+                otherConns.Add(room);
+                await SuspendDungeon();
+
+                if (!ValidateResume())
+                    return;
+            }
+        }
+    }
+}
diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenRoomEntrance.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenRoomEntrance.cs
new file mode 100644 (file)
index 0000000..09d223e
--- /dev/null
@@ -0,0 +1,48 @@
+using System.Threading.Tasks;
+using Content.Shared.Maps;
+using Content.Shared.Procedural;
+using Content.Shared.Procedural.PostGeneration;
+using Content.Shared.Storage;
+using Robust.Shared.Map;
+
+namespace Content.Server.Procedural.DungeonJob;
+
+public sealed partial class DungeonJob
+{
+    /// <summary>
+    /// <see cref="RoomEntranceDunGen"/>
+    /// </summary>
+    private async Task PostGen(RoomEntranceDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
+    {
+        if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto) ||
+            !data.SpawnGroups.TryGetValue(DungeonDataKey.Entrance, out var entranceProtos) ||
+            !_prototype.TryIndex(entranceProtos, out var entranceIn))
+        {
+            LogDataError(typeof(RoomEntranceDunGen));
+            return;
+        }
+
+        var setTiles = new List<(Vector2i, Tile)>();
+        var tileDef = _tileDefManager[tileProto];
+
+        foreach (var room in dungeon.Rooms)
+        {
+            foreach (var entrance in room.Entrances)
+            {
+                setTiles.Add((entrance, _tile.GetVariantTile((ContentTileDefinition) tileDef, random)));
+            }
+        }
+
+        _maps.SetTiles(_gridUid, _grid, setTiles);
+
+        foreach (var room in dungeon.Rooms)
+        {
+            foreach (var entrance in room.Entrances)
+            {
+                _entManager.SpawnEntities(
+                    _maps.GridTileToLocal(_gridUid, _grid, entrance),
+                    EntitySpawnCollection.GetSpawns(entranceIn.Entries, random));
+            }
+        }
+    }
+}
diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenSplineDungeonConnector.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenSplineDungeonConnector.cs
new file mode 100644 (file)
index 0000000..8fe2f36
--- /dev/null
@@ -0,0 +1,147 @@
+using System.Numerics;
+using System.Threading.Tasks;
+using Content.Server.NPC.Pathfinding;
+using Content.Shared.Procedural;
+using Content.Shared.Procedural.PostGeneration;
+using Robust.Shared.Map;
+using Robust.Shared.Random;
+
+namespace Content.Server.Procedural.DungeonJob;
+
+public sealed partial class DungeonJob
+{
+    /// <summary>
+    /// <see cref="SplineDungeonConnectorDunGen"/>
+    /// </summary>
+    private async Task<Dungeon> PostGen(
+        SplineDungeonConnectorDunGen gen,
+        DungeonData data,
+        List<Dungeon> dungeons,
+        HashSet<Vector2i> reservedTiles,
+        Random random)
+    {
+        // TODO: The path itself use the tile
+        // Widen it randomly (probably for each tile offset it by some changing amount).
+
+        // NOOP
+        if (dungeons.Count <= 1)
+            return Dungeon.Empty;
+
+        if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var fallback) ||
+            !data.Tiles.TryGetValue(DungeonDataKey.WidenTile, out var widen))
+        {
+            LogDataError(typeof(SplineDungeonConnectorDunGen));
+            return Dungeon.Empty;
+        }
+
+        var nodes = new List<Vector2i>();
+
+        foreach (var dungeon in dungeons)
+        {
+            foreach (var room in dungeon.Rooms)
+            {
+                if (room.Entrances.Count == 0)
+                    continue;
+
+                nodes.Add(room.Entrances[0]);
+                break;
+            }
+        }
+
+        var tree = _dungeon.MinimumSpanningTree(nodes, random);
+        await SuspendDungeon();
+
+        if (!ValidateResume())
+            return Dungeon.Empty;
+
+        var tiles = new List<(Vector2i Index, Tile Tile)>();
+        var pathfinding = _entManager.System<PathfindingSystem>();
+        var allTiles = new HashSet<Vector2i>();
+        var fallbackTile = new Tile(_prototype.Index(fallback).TileId);
+
+        foreach (var pair in tree)
+        {
+            var path = pathfinding.GetSplinePath(new PathfindingSystem.SplinePathArgs()
+            {
+                Distance = gen.DivisionDistance,
+                MaxRatio = gen.VarianceMax,
+                Args = new PathfindingSystem.SimplePathArgs()
+                {
+                    Start = pair.Start,
+                    End = pair.End,
+                    TileCost = node =>
+                    {
+                        // We want these to get prioritised internally and into space if it's a space dungeon.
+                        if (_maps.TryGetTile(_grid, node, out var tile) && !tile.IsEmpty)
+                            return 1f;
+
+                        return 5f;
+                    }
+                },
+            },
+            random);
+
+            // Welp
+            if (path.Path.Count == 0)
+            {
+                _sawmill.Error($"Unable to connect spline dungeon path for {_entManager.ToPrettyString(_gridUid)} between {pair.Start} and {pair.End}");
+                continue;
+            }
+
+            await SuspendDungeon();
+
+            if (!ValidateResume())
+                return Dungeon.Empty;
+
+            var wide = pathfinding.GetWiden(new PathfindingSystem.WidenArgs()
+            {
+                Path = path.Path,
+            },
+            random);
+
+            tiles.Clear();
+            allTiles.EnsureCapacity(allTiles.Count + wide.Count);
+
+            foreach (var node in wide)
+            {
+                if (reservedTiles.Contains(node))
+                    continue;
+
+                allTiles.Add(node);
+                Tile tile;
+
+                if (random.Prob(0.9f))
+                {
+                    tile = new Tile(_prototype.Index(widen).TileId);
+                }
+                else
+                {
+                    tile = _tileDefManager.GetVariantTile(widen, random);
+                }
+
+                tiles.Add((node, tile));
+            }
+
+            _maps.SetTiles(_gridUid, _grid, tiles);
+            tiles.Clear();
+            allTiles.EnsureCapacity(allTiles.Count + path.Path.Count);
+
+            foreach (var node in path.Path)
+            {
+                if (reservedTiles.Contains(node))
+                    continue;
+
+                allTiles.Add(node);
+                tiles.Add((node, fallbackTile));
+            }
+
+            _maps.SetTiles(_gridUid, _grid, tiles);
+        }
+
+        var dungy = new Dungeon();
+        var dungyRoom = new DungeonRoom(allTiles, Vector2.Zero, Box2i.Empty, new HashSet<Vector2i>());
+        dungy.AddRoom(dungyRoom);
+
+        return dungy;
+    }
+}
diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenWallMount.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenWallMount.cs
new file mode 100644 (file)
index 0000000..afc7608
--- /dev/null
@@ -0,0 +1,56 @@
+using System.Threading.Tasks;
+using Content.Shared.Procedural;
+using Content.Shared.Procedural.PostGeneration;
+using Content.Shared.Storage;
+using Robust.Shared.Random;
+
+namespace Content.Server.Procedural.DungeonJob;
+
+public sealed partial class DungeonJob
+{
+    /// <summary>
+    /// <see cref="WallMountDunGen"/>
+    /// </summary>
+    private async Task PostGen(WallMountDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
+    {
+        if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto))
+        {
+            _sawmill.Error($"Tried to run {nameof(WallMountDunGen)} without any dungeon data set which is unsupported");
+            return;
+        }
+
+        var tileDef = _prototype.Index(tileProto);
+        data.SpawnGroups.TryGetValue(DungeonDataKey.WallMounts, out var spawnProto);
+
+        var checkedTiles = new HashSet<Vector2i>();
+        var allExterior = new HashSet<Vector2i>(dungeon.CorridorExteriorTiles);
+        allExterior.UnionWith(dungeon.RoomExteriorTiles);
+        var count = 0;
+
+        foreach (var neighbor in allExterior)
+        {
+            // Occupado
+            if (dungeon.RoomTiles.Contains(neighbor) || checkedTiles.Contains(neighbor) || !_anchorable.TileFree(_grid, neighbor, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
+                continue;
+
+            if (!random.Prob(gen.Prob) || !checkedTiles.Add(neighbor))
+                continue;
+
+            _maps.SetTile(_gridUid, _grid, neighbor, _tile.GetVariantTile(tileDef, random));
+            var gridPos = _maps.GridTileToLocal(_gridUid, _grid, neighbor);
+            var protoNames = EntitySpawnCollection.GetSpawns(_prototype.Index(spawnProto).Entries, random);
+
+            _entManager.SpawnEntities(gridPos, protoNames);
+            count += protoNames.Count;
+
+            if (count > 20)
+            {
+                count -= 20;
+                await SuspendDungeon();
+
+                if (!ValidateResume())
+                    return;
+            }
+        }
+    }
+}
similarity index 88%
rename from Content.Server/Procedural/DungeonJob.WormPost.cs
rename to Content.Server/Procedural/DungeonJob/DungeonJob.PostGenWorm.cs
index 5d2271cae6599b8c1f70f12cca400716337569a5..6fd00e5482485330b0af9b26672b6b0e7bc2c805 100644 (file)
@@ -1,23 +1,27 @@
 using System.Linq;
-using System.Numerics;
 using System.Threading.Tasks;
 using Content.Shared.Procedural;
 using Content.Shared.Procedural.PostGeneration;
 using Robust.Shared.Collections;
 using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
 using Robust.Shared.Random;
 using Robust.Shared.Utility;
 
-namespace Content.Server.Procedural;
+namespace Content.Server.Procedural.DungeonJob;
 
 public sealed partial class DungeonJob
 {
     /// <summary>
-    /// Tries to connect rooms via worm-like corridors.
+    /// <see cref="WormCorridorDunGen"/>
     /// </summary>
-    private async Task PostGen(WormCorridorPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random)
+    private async Task PostGen(WormCorridorDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
     {
+        if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto) || !_prototype.TryIndex(tileProto, out var tileDef))
+        {
+            _sawmill.Error($"Tried to run {nameof(WormCorridorDunGen)} without any dungeon data set which is unsupported");
+            return;
+        }
+
         var networks = new List<(Vector2i Start, HashSet<Vector2i> Network)>();
 
         // List of places to start from.
@@ -32,7 +36,7 @@ public sealed partial class DungeonJob
                 networks.Add((entrance, network));
 
                 // Point away from the room to start with.
-                startAngles.Add(entrance, (entrance + grid.TileSizeHalfVector - room.Center).ToAngle());
+                startAngles.Add(entrance, (entrance + _grid.TileSizeHalfVector - room.Center).ToAngle());
             }
         }
 
@@ -46,7 +50,7 @@ public sealed partial class DungeonJob
             // Find a random network to worm from.
             var startIndex = (i % networks.Count);
             var startPos = networks[startIndex].Start;
-            var position = startPos + grid.TileSizeHalfVector;
+            var position = startPos + _grid.TileSizeHalfVector;
 
             var remainingLength = gen.Length;
             worm.Clear();
@@ -108,7 +112,7 @@ public sealed partial class DungeonJob
                 costSoFar[startNode] = 0f;
                 var count = 0;
 
-                await SuspendIfOutOfTime();
+                await SuspendDungeon();
                 if (!ValidateResume())
                     return;
 
@@ -174,9 +178,9 @@ public sealed partial class DungeonJob
         WidenCorridor(dungeon, gen.Width, main.Network);
         dungeon.CorridorTiles.UnionWith(main.Network);
         BuildCorridorExterior(dungeon);
+        dungeon.RefreshAllTiles();
 
         var tiles = new List<(Vector2i Index, Tile Tile)>();
-        var tileDef = _prototype.Index(gen.Tile);
 
         foreach (var tile in dungeon.CorridorTiles)
         {
diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.cs
new file mode 100644 (file)
index 0000000..1468a80
--- /dev/null
@@ -0,0 +1,309 @@
+using System.Threading;
+using System.Threading.Tasks;
+using Content.Server.Decals;
+using Content.Server.NPC.Components;
+using Content.Server.NPC.HTN;
+using Content.Server.NPC.Systems;
+using Content.Shared.Construction.EntitySystems;
+using Content.Shared.Maps;
+using Content.Shared.Procedural;
+using Content.Shared.Procedural.DungeonGenerators;
+using Content.Shared.Procedural.DungeonLayers;
+using Content.Shared.Procedural.PostGeneration;
+using Content.Shared.Tag;
+using JetBrains.Annotations;
+using Robust.Server.Physics;
+using Robust.Shared.CPUJob.JobQueues;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Robust.Shared.Utility;
+using IDunGenLayer = Content.Shared.Procedural.IDunGenLayer;
+
+namespace Content.Server.Procedural.DungeonJob;
+
+public sealed partial class DungeonJob : Job<List<Dungeon>>
+{
+    public bool TimeSlice = true;
+
+    private readonly IEntityManager _entManager;
+    private readonly IPrototypeManager _prototype;
+    private readonly ITileDefinitionManager _tileDefManager;
+
+    private readonly AnchorableSystem _anchorable;
+    private readonly DecalSystem _decals;
+    private readonly DungeonSystem _dungeon;
+    private readonly EntityLookupSystem _lookup;
+    private readonly TagSystem _tags;
+    private readonly TileSystem _tile;
+    private readonly SharedMapSystem _maps;
+    private readonly SharedTransformSystem _transform;
+
+    private EntityQuery<PhysicsComponent> _physicsQuery;
+    private EntityQuery<TransformComponent> _xformQuery;
+
+    private readonly DungeonConfigPrototype _gen;
+    private readonly int _seed;
+    private readonly Vector2i _position;
+
+    private readonly EntityUid _gridUid;
+    private readonly MapGridComponent _grid;
+
+    private readonly ISawmill _sawmill;
+
+    public DungeonJob(
+        ISawmill sawmill,
+        double maxTime,
+        IEntityManager entManager,
+        IPrototypeManager prototype,
+        ITileDefinitionManager tileDefManager,
+        AnchorableSystem anchorable,
+        DecalSystem decals,
+        DungeonSystem dungeon,
+        EntityLookupSystem lookup,
+        TileSystem tile,
+        SharedTransformSystem transform,
+        DungeonConfigPrototype gen,
+        MapGridComponent grid,
+        EntityUid gridUid,
+        int seed,
+        Vector2i position,
+        CancellationToken cancellation = default) : base(maxTime, cancellation)
+    {
+        _sawmill = sawmill;
+        _entManager = entManager;
+        _prototype = prototype;
+        _tileDefManager = tileDefManager;
+
+        _anchorable = anchorable;
+        _decals = decals;
+        _dungeon = dungeon;
+        _lookup = lookup;
+        _tile = tile;
+        _tags = _entManager.System<TagSystem>();
+        _maps = _entManager.System<SharedMapSystem>();
+        _transform = transform;
+
+        _physicsQuery = _entManager.GetEntityQuery<PhysicsComponent>();
+        _xformQuery = _entManager.GetEntityQuery<TransformComponent>();
+
+        _gen = gen;
+        _grid = grid;
+        _gridUid = gridUid;
+        _seed = seed;
+        _position = position;
+    }
+
+    /// <summary>
+    /// Gets the relevant dungeon, running recursively as relevant.
+    /// </summary>
+    /// <param name="reserve">Should we reserve tiles even if the config doesn't specify.</param>
+    private async Task<List<Dungeon>> GetDungeons(
+        Vector2i position,
+        DungeonConfigPrototype config,
+        DungeonData data,
+        List<IDunGenLayer> layers,
+        HashSet<Vector2i> reservedTiles,
+        int seed,
+        Random random)
+    {
+        var dungeons = new List<Dungeon>();
+        var count = random.Next(config.MinCount, config.MaxCount + 1);
+
+        for (var i = 0; i < count; i++)
+        {
+            position += random.NextPolarVector2(config.MinOffset, config.MaxOffset).Floored();
+
+            foreach (var layer in layers)
+            {
+                await RunLayer(dungeons, data, position, layer, reservedTiles, seed, random);
+
+                if (config.ReserveTiles)
+                {
+                    foreach (var dungeon in dungeons)
+                    {
+                        reservedTiles.UnionWith(dungeon.AllTiles);
+                    }
+                }
+
+                await SuspendDungeon();
+                if (!ValidateResume())
+                    return new List<Dungeon>();
+            }
+        }
+
+        return dungeons;
+    }
+
+    protected override async Task<List<Dungeon>?> Process()
+    {
+        _sawmill.Info($"Generating dungeon {_gen.ID} with seed {_seed} on {_entManager.ToPrettyString(_gridUid)}");
+        _grid.CanSplit = false;
+        var random = new Random(_seed);
+        var position = (_position + random.NextPolarVector2(_gen.MinOffset, _gen.MaxOffset)).Floored();
+
+        // Tiles we can no longer generate on due to being reserved elsewhere.
+        var reservedTiles = new HashSet<Vector2i>();
+
+        var dungeons = await GetDungeons(position, _gen, _gen.Data, _gen.Layers, reservedTiles, _seed, random);
+        // To make it slightly more deterministic treat this RNG as separate ig.
+
+        // Post-processing after finishing loading.
+
+        // Defer splitting so they don't get spammed and so we don't have to worry about tracking the grid along the way.
+        _grid.CanSplit = true;
+        _entManager.System<GridFixtureSystem>().CheckSplits(_gridUid);
+        var npcSystem = _entManager.System<NPCSystem>();
+        var npcs = new HashSet<Entity<HTNComponent>>();
+
+        _lookup.GetChildEntities(_gridUid, npcs);
+
+        foreach (var npc in npcs)
+        {
+            npcSystem.WakeNPC(npc.Owner, npc.Comp);
+        }
+
+        return dungeons;
+    }
+
+    private async Task RunLayer(
+        List<Dungeon> dungeons,
+        DungeonData data,
+        Vector2i position,
+        IDunGenLayer layer,
+        HashSet<Vector2i> reservedTiles,
+        int seed,
+        Random random)
+    {
+        _sawmill.Debug($"Doing postgen {layer.GetType()} for {_gen.ID} with seed {_seed}");
+
+        // If there's a way to just call the methods directly for the love of god tell me.
+        // Some of these don't care about reservedtiles because they only operate on dungeon tiles (which should
+        // never be reserved)
+
+        // Some may or may not return dungeons.
+        // It's clamplicated but yeah procgen layering moment I'll take constructive feedback.
+
+        switch (layer)
+        {
+            case AutoCablingDunGen cabling:
+                await PostGen(cabling, data, dungeons[^1], reservedTiles, random);
+                break;
+            case BiomeMarkerLayerDunGen markerPost:
+                await PostGen(markerPost, data, dungeons[^1], reservedTiles, random);
+                break;
+            case BiomeDunGen biome:
+                await PostGen(biome, data, dungeons[^1], reservedTiles, random);
+                break;
+            case BoundaryWallDunGen boundary:
+                await PostGen(boundary, data, dungeons[^1], reservedTiles, random);
+                break;
+            case CornerClutterDunGen clutter:
+                await PostGen(clutter, data, dungeons[^1], reservedTiles, random);
+                break;
+            case CorridorClutterDunGen corClutter:
+                await PostGen(corClutter, data, dungeons[^1], reservedTiles, random);
+                break;
+            case CorridorDunGen cordor:
+                await PostGen(cordor, data, dungeons[^1], reservedTiles, random);
+                break;
+            case CorridorDecalSkirtingDunGen decks:
+                await PostGen(decks, data, dungeons[^1], reservedTiles, random);
+                break;
+            case EntranceFlankDunGen flank:
+                await PostGen(flank, data, dungeons[^1], reservedTiles, random);
+                break;
+            case ExteriorDunGen exterior:
+                dungeons.AddRange(await GenerateExteriorDungen(position, exterior, reservedTiles, random));
+                break;
+            case FillGridDunGen fill:
+                dungeons.Add(await GenerateFillDunGen(data, reservedTiles));
+                break;
+            case JunctionDunGen junc:
+                await PostGen(junc, data, dungeons[^1], reservedTiles, random);
+                break;
+            case MiddleConnectionDunGen dordor:
+                await PostGen(dordor, data, dungeons[^1], reservedTiles, random);
+                break;
+            case DungeonEntranceDunGen entrance:
+                await PostGen(entrance, data, dungeons[^1], reservedTiles, random);
+                break;
+            case ExternalWindowDunGen externalWindow:
+                await PostGen(externalWindow, data, dungeons[^1], reservedTiles, random);
+                break;
+            case InternalWindowDunGen internalWindow:
+                await PostGen(internalWindow, data, dungeons[^1], reservedTiles, random);
+                break;
+            case MobsDunGen mob:
+                await PostGen(mob, dungeons[^1], random);
+                break;
+            case NoiseDistanceDunGen distance:
+                dungeons.Add(await GenerateNoiseDistanceDunGen(position, distance, reservedTiles, seed, random));
+                break;
+            case NoiseDunGen noise:
+                dungeons.Add(await GenerateNoiseDunGen(position, noise, reservedTiles, seed, random));
+                break;
+            case OreDunGen ore:
+                await PostGen(ore, dungeons[^1], random);
+                break;
+            case PrefabDunGen prefab:
+                dungeons.Add(await GeneratePrefabDunGen(position, data, prefab, reservedTiles, random));
+                break;
+            case PrototypeDunGen prototypo:
+                var groupConfig = _prototype.Index(prototypo.Proto);
+                position = (position + random.NextPolarVector2(groupConfig.MinOffset, groupConfig.MaxOffset)).Floored();
+
+                var dataCopy = groupConfig.Data.Clone();
+                dataCopy.Apply(data);
+
+                dungeons.AddRange(await GetDungeons(position, groupConfig, dataCopy, groupConfig.Layers, reservedTiles, seed, random));
+                break;
+            case ReplaceTileDunGen replace:
+                dungeons.Add(await GenerateTileReplacementDunGen(replace, data, reservedTiles, random));
+                break;
+            case RoomEntranceDunGen rEntrance:
+                await PostGen(rEntrance, data, dungeons[^1], reservedTiles, random);
+                break;
+            case SplineDungeonConnectorDunGen spline:
+                dungeons.Add(await PostGen(spline, data, dungeons, reservedTiles, random));
+                break;
+            case WallMountDunGen wall:
+                await PostGen(wall, data, dungeons[^1], reservedTiles, random);
+                break;
+            case WormCorridorDunGen worm:
+                await PostGen(worm, data, dungeons[^1], reservedTiles, random);
+                break;
+            default:
+                throw new NotImplementedException();
+        }
+    }
+
+    private void LogDataError(Type type)
+    {
+        _sawmill.Error($"Unable to find dungeon data keys for {type}");
+    }
+
+    [Pure]
+    private bool ValidateResume()
+    {
+        if (_entManager.Deleted(_gridUid))
+        {
+            return false;
+        }
+
+        return true;
+    }
+
+    /// <summary>
+    /// Wrapper around <see cref="Job{T}.SuspendIfOutOfTime"/>
+    /// </summary>
+    private async Task SuspendDungeon()
+    {
+        if (!TimeSlice)
+            return;
+
+        await SuspendIfOutOfTime();
+    }
+}
index d783eb60c63e85c4120bfca2d83defdf1be7d768..51a6a57bbebe4ac1ab18c66de5d49e7af39c2d23 100644 (file)
@@ -51,6 +51,8 @@ public sealed partial class DungeonSystem
             dungeonUid = EntityManager.CreateEntityUninitialized(null, new EntityCoordinates(dungeonUid, position));
             dungeonGrid = EntityManager.AddComponent<MapGridComponent>(dungeonUid);
             EntityManager.InitializeAndStartEntity(dungeonUid, mapId);
+            // If we created a grid (e.g. space dungen) then offset it so we don't double-apply positions
+            position = Vector2i.Zero;
         }
 
         int seed;
index ddd4a4732f8e2d466db1ea62b101860683cce446..8a1606c48891392c6b65c9819f91a0cab23eb6bf 100644 (file)
@@ -64,6 +64,7 @@ public sealed partial class DungeonSystem
         Vector2i origin,
         DungeonRoomPrototype room,
         Random random,
+        HashSet<Vector2i>? reservedTiles,
         bool clearExisting = false,
         bool rotation = false)
     {
@@ -78,7 +79,7 @@ public sealed partial class DungeonSystem
         var roomTransform = Matrix3Helpers.CreateTransform((Vector2) room.Size / 2f, roomRotation);
         var finalTransform = Matrix3x2.Multiply(roomTransform, originTransform);
 
-        SpawnRoom(gridUid, grid, finalTransform, room, clearExisting);
+        SpawnRoom(gridUid, grid, finalTransform, room, reservedTiles, clearExisting);
     }
 
     public Angle GetRoomRotation(DungeonRoomPrototype room, Random random)
@@ -103,6 +104,7 @@ public sealed partial class DungeonSystem
         MapGridComponent grid,
         Matrix3x2 roomTransform,
         DungeonRoomPrototype room,
+        HashSet<Vector2i>? reservedTiles = null,
         bool clearExisting = false)
     {
         // Ensure the underlying template exists.
@@ -150,6 +152,10 @@ public sealed partial class DungeonSystem
 
                 var tilePos = Vector2.Transform(indices + tileOffset, roomTransform);
                 var rounded = tilePos.Floored();
+
+                if (!clearExisting && reservedTiles?.Contains(rounded) == true)
+                    continue;
+
                 _tiles.Add((rounded, tileRef.Tile));
             }
         }
@@ -165,6 +171,10 @@ public sealed partial class DungeonSystem
         {
             var templateXform = _xformQuery.GetComponent(templateEnt);
             var childPos = Vector2.Transform(templateXform.LocalPosition - roomCenter, roomTransform);
+
+            if (!clearExisting && reservedTiles?.Contains(childPos.Floored()) == true)
+                continue;
+
             var childRot = templateXform.LocalRotation + finalRoomRotation;
             var protoId = _metaQuery.GetComponent(templateEnt).EntityPrototype?.ID;
 
@@ -192,8 +202,11 @@ public sealed partial class DungeonSystem
                 // Offset by 0.5 because decals are offset from bot-left corner
                 // So we convert it to center of tile then convert it back again after transform.
                 // Do these shenanigans because 32x32 decals assume as they are centered on bottom-left of tiles.
-                var position = Vector2.Transform(decal.Coordinates + Vector2Helpers.Half - roomCenter, roomTransform);
-                position -= Vector2Helpers.Half;
+                var position = Vector2.Transform(decal.Coordinates + grid.TileSizeHalfVector - roomCenter, roomTransform);
+                position -= grid.TileSizeHalfVector;
+
+                if (!clearExisting && reservedTiles?.Contains(position.Floored()) == true)
+                    continue;
 
                 // Umm uhh I love decals so uhhhh idk what to do about this
                 var angle = (decal.Angle + finalRoomRotation).Reduced();
index 36009896a2cb599081c9e54d37746812551220aa..b73e843fffdbde733f125b39398a65cd0606e32b 100644 (file)
@@ -12,6 +12,7 @@ using Content.Shared.Physics;
 using Content.Shared.Procedural;
 using Content.Shared.Tag;
 using Robust.Server.GameObjects;
+using Robust.Shared.Collections;
 using Robust.Shared.Configuration;
 using Robust.Shared.Console;
 using Robust.Shared.Map;
@@ -49,7 +50,7 @@ public sealed partial class DungeonSystem : SharedDungeonSystem
     public const int CollisionLayer = (int) CollisionGroup.Impassable;
 
     private readonly JobQueue _dungeonJobQueue = new(DungeonJobTime);
-    private readonly Dictionary<DungeonJob, CancellationTokenSource> _dungeonJobs = new();
+    private readonly Dictionary<DungeonJob.DungeonJob, CancellationTokenSource> _dungeonJobs = new();
 
     [ValidatePrototypeId<ContentTileDefinition>]
     public const string FallbackTileId = "FloorSteel";
@@ -190,18 +191,16 @@ public sealed partial class DungeonSystem : SharedDungeonSystem
         int seed)
     {
         var cancelToken = new CancellationTokenSource();
-        var job = new DungeonJob(
+        var job = new DungeonJob.DungeonJob(
             Log,
             DungeonJobTime,
             EntityManager,
-            _mapManager,
             _prototype,
             _tileDefManager,
             _anchorable,
             _decals,
             this,
             _lookup,
-            _tag,
             _tile,
             _transform,
             gen,
@@ -215,7 +214,7 @@ public sealed partial class DungeonSystem : SharedDungeonSystem
         _dungeonJobQueue.EnqueueJob(job);
     }
 
-    public async Task<Dungeon> GenerateDungeonAsync(
+    public async Task<List<Dungeon>> GenerateDungeonAsync(
         DungeonConfigPrototype gen,
         EntityUid gridUid,
         MapGridComponent grid,
@@ -223,18 +222,16 @@ public sealed partial class DungeonSystem : SharedDungeonSystem
         int seed)
     {
         var cancelToken = new CancellationTokenSource();
-        var job = new DungeonJob(
+        var job = new DungeonJob.DungeonJob(
             Log,
             DungeonJobTime,
             EntityManager,
-            _mapManager,
             _prototype,
             _tileDefManager,
             _anchorable,
             _decals,
             this,
             _lookup,
-            _tag,
             _tile,
             _transform,
             gen,
index 20ffa98586d901af1c59b05fe0342924714a09d5..b539cc9780ef21e7adbb8028147044d4dd0cb07c 100644 (file)
@@ -35,6 +35,7 @@ public sealed class RoomFillSystem : EntitySystem
                     _maps.LocalToTile(xform.GridUid.Value, mapGrid, xform.Coordinates),
                     room,
                     random,
+                    null,
                     clearExisting: component.ClearExisting,
                     rotation: component.Rotation);
             }
index ce844e57a13e9750e016294107ebf0b5ac3da1d0..e9318792b726de476a8bacee1b770c02c71cf9cf 100644 (file)
@@ -176,9 +176,11 @@ public sealed class SpawnSalvageMissionJob : Job<bool>
         dungeonOffset = dungeonRotation.RotateVec(dungeonOffset);
         var dungeonMod = _prototypeManager.Index<SalvageDungeonModPrototype>(mission.Dungeon);
         var dungeonConfig = _prototypeManager.Index<DungeonConfigPrototype>(dungeonMod.Proto);
-        var dungeon = await WaitAsyncTask(_dungeon.GenerateDungeonAsync(dungeonConfig, mapUid, grid, (Vector2i) dungeonOffset,
+        var dungeons = await WaitAsyncTask(_dungeon.GenerateDungeonAsync(dungeonConfig, mapUid, grid, (Vector2i) dungeonOffset,
             _missionParams.Seed));
 
+        var dungeon = dungeons.First();
+
         // Aborty
         if (dungeon.Rooms.Count == 0)
         {
index 5f0fa7dd62447eb6afdf1e8291b364735b956d4f..d8144354b8e7e88be321d135ed6a272e77c4f45e 100644 (file)
@@ -1,4 +1,6 @@
 using Content.Server.Shuttles.Systems;
+using Content.Shared.Dataset;
+using Content.Shared.Procedural;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Utility;
 
@@ -14,39 +16,92 @@ public sealed partial class GridSpawnComponent : Component
     /// Dictionary of groups where each group will have entries selected.
     /// String is just an identifier to make yaml easier.
     /// </summary>
-    [DataField(required: true)] public Dictionary<string, GridSpawnGroup> Groups = new();
+    [DataField(required: true)] public Dictionary<string, IGridSpawnGroup> Groups = new();
 }
 
-[DataRecord]
-public record struct GridSpawnGroup
+public interface IGridSpawnGroup
 {
-    public List<ResPath> Paths = new();
-    public int MinCount = 1;
-    public int MaxCount = 1;
+    /// <summary>
+    /// Minimum distance to spawn away from the station.
+    /// </summary>
+    public float MinimumDistance { get; }
+
+    /// <inheritdoc />
+    public ProtoId<DatasetPrototype>? NameDataset { get; }
+
+    /// <inheritdoc />
+    int MinCount { get; set; }
+
+    /// <inheritdoc />
+    int MaxCount { get; set; }
 
     /// <summary>
     /// Components to be added to any spawned grids.
     /// </summary>
-    public ComponentRegistry AddComponents = new();
+    public ComponentRegistry AddComponents { get; set; }
 
     /// <summary>
     /// Hide the IFF label of the grid.
     /// </summary>
-    public bool Hide = false;
+    public bool Hide { get; set; }
 
     /// <summary>
     /// Should we set the metadata name of a grid. Useful for admin purposes.
     /// </summary>
-    public bool NameGrid = false;
+    public bool NameGrid { get; set; }
 
     /// <summary>
     /// Should we add this to the station's grids (if possible / relevant).
     /// </summary>
-    public bool StationGrid = true;
+    public bool StationGrid { get; set; }
+}
+
+[DataRecord]
+public sealed class DungeonSpawnGroup : IGridSpawnGroup
+{
+    /// <summary>
+    /// Prototypes we can choose from to spawn.
+    /// </summary>
+    public List<ProtoId<DungeonConfigPrototype>> Protos = new();
+
+    /// <inheritdoc />
+    public float MinimumDistance { get; }
+
+    /// <inheritdoc />
+    public ProtoId<DatasetPrototype>? NameDataset { get; }
+
+    /// <inheritdoc />
+    public int MinCount { get; set; } = 1;
+
+    /// <inheritdoc />
+    public int MaxCount { get; set; } = 1;
+
+    /// <inheritdoc />
+    public ComponentRegistry AddComponents { get; set; } = new();
+
+    /// <inheritdoc />
+    public bool Hide { get; set; } = false;
+
+    /// <inheritdoc />
+    public bool NameGrid { get; set; } = false;
+
+    /// <inheritdoc />
+    public bool StationGrid { get; set; } = false;
+}
+
+[DataRecord]
+public sealed class GridSpawnGroup : IGridSpawnGroup
+{
+    public List<ResPath> Paths = new();
 
-    public GridSpawnGroup()
-    {
-    }
+    public float MinimumDistance { get; }
+    public ProtoId<DatasetPrototype>? NameDataset { get; }
+    public int MinCount { get; set; } = 1;
+    public int MaxCount { get; set; } = 1;
+    public ComponentRegistry AddComponents { get; set; } = new();
+    public bool Hide { get; set; } = false;
+    public bool NameGrid { get; set; } = true;
+    public bool StationGrid { get; set; } = true;
 }
 
 
index 853548add37c6a248e0f7a280c91b6470b640517..b4fcccd80557c5059c64ed321d85f5f715d1e444 100644 (file)
@@ -1,9 +1,14 @@
+using System.Numerics;
 using Content.Server.Shuttles.Components;
 using Content.Server.Station.Components;
 using Content.Server.Station.Events;
 using Content.Shared.Cargo.Components;
 using Content.Shared.CCVar;
+using Content.Shared.Procedural;
+using Content.Shared.Salvage;
 using Content.Shared.Shuttles.Components;
+using Robust.Shared.Collections;
+using Robust.Shared.Map;
 using Robust.Shared.Random;
 using Robust.Shared.Utility;
 
@@ -80,6 +85,76 @@ public sealed partial class ShuttleSystem
         _mapManager.DeleteMap(mapId);
     }
 
+    private bool TryDungeonSpawn(EntityUid targetGrid, EntityUid stationUid, MapId mapId, DungeonSpawnGroup group, out EntityUid spawned)
+    {
+        spawned = EntityUid.Invalid;
+        var dungeonProtoId = _random.Pick(group.Protos);
+
+        if (!_protoManager.TryIndex(dungeonProtoId, out var dungeonProto))
+        {
+            return false;
+        }
+
+        var spawnCoords = new EntityCoordinates(targetGrid, Vector2.Zero);
+
+        if (group.MinimumDistance > 0f)
+        {
+            spawnCoords = spawnCoords.Offset(_random.NextVector2(group.MinimumDistance, group.MinimumDistance * 1.5f));
+        }
+
+        var spawnMapCoords = _transform.ToMapCoordinates(spawnCoords);
+        var spawnedGrid = _mapManager.CreateGridEntity(mapId);
+
+        _transform.SetMapCoordinates(spawnedGrid, spawnMapCoords);
+        _dungeon.GenerateDungeon(dungeonProto, spawnedGrid.Owner, spawnedGrid.Comp, Vector2i.Zero, _random.Next());
+
+        spawned = spawnedGrid.Owner;
+        return true;
+    }
+
+    private bool TryGridSpawn(EntityUid targetGrid, EntityUid stationUid, MapId mapId, GridSpawnGroup group, out EntityUid spawned)
+    {
+        spawned = EntityUid.Invalid;
+
+        if (group.Paths.Count == 0)
+        {
+            Log.Error($"Found no paths for GridSpawn");
+            return false;
+        }
+
+        var paths = new ValueList<ResPath>();
+
+        // Round-robin so we try to avoid dupes where possible.
+        if (paths.Count == 0)
+        {
+            paths.AddRange(group.Paths);
+            _random.Shuffle(paths);
+        }
+
+        var path = paths[^1];
+        paths.RemoveAt(paths.Count - 1);
+
+        if (_loader.TryLoad(mapId, path.ToString(), out var ent) && ent.Count == 1)
+        {
+            if (TryComp<ShuttleComponent>(ent[0], out var shuttle))
+            {
+                TryFTLProximity(ent[0], targetGrid);
+            }
+
+            if (group.NameGrid)
+            {
+                var name = path.FilenameWithoutExtension;
+                _metadata.SetEntityName(ent[0], name);
+            }
+
+            spawned = ent[0];
+            return true;
+        }
+
+        Log.Error($"Error loading gridspawn for {ToPrettyString(stationUid)} / {path}");
+        return false;
+    }
+
     private void GridSpawns(EntityUid uid, GridSpawnComponent component)
     {
         if (!_cfg.GetCVar(CCVars.GridFill))
@@ -97,81 +172,49 @@ public sealed partial class ShuttleSystem
 
         // Spawn on a dummy map and try to FTL if possible, otherwise dump it.
         var mapId = _mapManager.CreateMap();
-        var valid = true;
-        var paths = new List<ResPath>();
 
         foreach (var group in component.Groups.Values)
         {
-            if (group.Paths.Count == 0)
-            {
-                Log.Error($"Found no paths for GridSpawn");
-                continue;
-            }
-
-            var count = _random.Next(group.MinCount, group.MaxCount);
-            paths.Clear();
+            var count = _random.Next(group.MinCount, group.MaxCount + 1);
 
             for (var i = 0; i < count; i++)
             {
-                // Round-robin so we try to avoid dupes where possible.
-                if (paths.Count == 0)
-                {
-                    paths.AddRange(group.Paths);
-                    _random.Shuffle(paths);
-                }
-
-                var path = paths[^1];
-                paths.RemoveAt(paths.Count - 1);
+                EntityUid spawned;
 
-                if (_loader.TryLoad(mapId, path.ToString(), out var ent) && ent.Count == 1)
+                switch (group)
                 {
-                    if (TryComp<ShuttleComponent>(ent[0], out var shuttle))
-                    {
-                        TryFTLProximity(ent[0], targetGrid.Value);
-                    }
-                    else
-                    {
-                        valid = false;
-                    }
-
-                    if (group.Hide)
-                    {
-                        var iffComp = EnsureComp<IFFComponent>(ent[0]);
-                        iffComp.Flags |= IFFFlags.HideLabel;
-                        Dirty(ent[0], iffComp);
-                    }
-
-                    if (group.StationGrid)
-                    {
-                        _station.AddGridToStation(uid, ent[0]);
-                    }
-
-                    if (group.NameGrid)
-                    {
-                        var name = path.FilenameWithoutExtension;
-                        _metadata.SetEntityName(ent[0], name);
-                    }
-
-                    foreach (var compReg in group.AddComponents.Values)
-                    {
-                        var compType = compReg.Component.GetType();
+                    case DungeonSpawnGroup dungeon:
+                        if (!TryDungeonSpawn(targetGrid.Value, uid, mapId, dungeon, out spawned))
+                            continue;
 
-                        if (HasComp(ent[0], compType))
+                        break;
+                    case GridSpawnGroup grid:
+                        if (!TryGridSpawn(targetGrid.Value, uid, mapId, grid, out spawned))
                             continue;
 
-                        var comp = _factory.GetComponent(compType);
-                        AddComp(ent[0], comp, true);
-                    }
+                        break;
+                    default:
+                        throw new NotImplementedException();
+                }
+
+                if (_protoManager.TryIndex(group.NameDataset, out var dataset))
+                {
+                    _metadata.SetEntityName(spawned, SharedSalvageSystem.GetFTLName(dataset, _random.Next()));
                 }
-                else
+
+                if (group.Hide)
                 {
-                    valid = false;
+                    var iffComp = EnsureComp<IFFComponent>(spawned);
+                    iffComp.Flags |= IFFFlags.HideLabel;
+                    Dirty(spawned, iffComp);
                 }
 
-                if (!valid)
+                if (group.StationGrid)
                 {
-                    Log.Error($"Error loading gridspawn for {ToPrettyString(uid)} / {path}");
+                    _station.AddGridToStation(uid, spawned);
                 }
+
+                EntityManager.AddComponents(spawned, group.AddComponents);
             }
         }
 
index b8f216db7370a0747c0d92cc6202aadb940c28f6..85703389e9dec72e9e9a6f3c5c475d317ba98fac 100644 (file)
@@ -2,6 +2,7 @@ using Content.Server.Administration.Logs;
 using Content.Server.Body.Systems;
 using Content.Server.Doors.Systems;
 using Content.Server.Parallax;
+using Content.Server.Procedural;
 using Content.Server.Shuttles.Components;
 using Content.Server.Station.Systems;
 using Content.Server.Stunnable;
@@ -20,6 +21,7 @@ using Robust.Shared.Map.Components;
 using Robust.Shared.Physics;
 using Robust.Shared.Physics.Components;
 using Robust.Shared.Physics.Systems;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Random;
 using Robust.Shared.Timing;
 
@@ -28,15 +30,18 @@ namespace Content.Server.Shuttles.Systems;
 [UsedImplicitly]
 public sealed partial class ShuttleSystem : SharedShuttleSystem
 {
+    [Dependency] private readonly IAdminLogManager _logger = default!;
     [Dependency] private readonly IComponentFactory _factory = default!;
     [Dependency] private readonly IConfigurationManager _cfg = default!;
+    [Dependency] private readonly IGameTiming _gameTiming = default!;
     [Dependency] private readonly IMapManager _mapManager = default!;
+    [Dependency] private readonly IPrototypeManager _protoManager = default!;
     [Dependency] private readonly IRobustRandom _random = default!;
     [Dependency] private readonly ITileDefinitionManager _tileDefManager = default!;
-    [Dependency] private readonly IGameTiming _gameTiming = default!;
     [Dependency] private readonly BiomeSystem _biomes = default!;
     [Dependency] private readonly BodySystem _bobby = default!;
     [Dependency] private readonly DockingSystem _dockSystem = default!;
+    [Dependency] private readonly DungeonSystem _dungeon = default!;
     [Dependency] private readonly EntityLookupSystem _lookup = default!;
     [Dependency] private readonly FixtureSystem _fixtures = default!;
     [Dependency] private readonly MapLoaderSystem _loader = default!;
@@ -52,7 +57,6 @@ public sealed partial class ShuttleSystem : SharedShuttleSystem
     [Dependency] private readonly ThrowingSystem _throwing = default!;
     [Dependency] private readonly ThrusterSystem _thruster = default!;
     [Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
-    [Dependency] private readonly IAdminLogManager _logger = default!;
 
     public const float TileMassMultiplier = 0.5f;
 
diff --git a/Content.Shared/Procedural/Components/EntityRemapComponent.cs b/Content.Shared/Procedural/Components/EntityRemapComponent.cs
new file mode 100644 (file)
index 0000000..3d71997
--- /dev/null
@@ -0,0 +1,13 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Procedural.Components;
+
+/// <summary>
+/// Indicates this entity prototype should be re-mapped to another
+/// </summary>
+[RegisterComponent]
+public sealed partial class EntityRemapComponent : Component
+{
+    [DataField(required: true)]
+    public Dictionary<EntProtoId, EntProtoId> Mask = new();
+}
diff --git a/Content.Shared/Procedural/Distance/DunGenEuclideanSquaredDistance.cs b/Content.Shared/Procedural/Distance/DunGenEuclideanSquaredDistance.cs
new file mode 100644 (file)
index 0000000..6173047
--- /dev/null
@@ -0,0 +1,10 @@
+namespace Content.Shared.Procedural.Distance;
+
+/// <summary>
+/// Produces a rounder shape useful for more natural areas.
+/// </summary>
+public sealed partial class DunGenEuclideanSquaredDistance : IDunGenDistance
+{
+    [DataField]
+    public float BlendWeight { get; set; } = 0.50f;
+}
diff --git a/Content.Shared/Procedural/Distance/DunGenSquareBump.cs b/Content.Shared/Procedural/Distance/DunGenSquareBump.cs
new file mode 100644 (file)
index 0000000..48b0c4b
--- /dev/null
@@ -0,0 +1,10 @@
+namespace Content.Shared.Procedural.Distance;
+
+/// <summary>
+/// Produces a squarish-shape that's better for filling in most of the area.
+/// </summary>
+public sealed partial class DunGenSquareBump : IDunGenDistance
+{
+    [DataField]
+    public float BlendWeight { get; set; } = 0.50f;
+}
diff --git a/Content.Shared/Procedural/Distance/IDunGenDistance.cs b/Content.Shared/Procedural/Distance/IDunGenDistance.cs
new file mode 100644 (file)
index 0000000..b1071a1
--- /dev/null
@@ -0,0 +1,14 @@
+namespace Content.Shared.Procedural.Distance;
+
+/// <summary>
+/// Used if you want to limit the distance noise is generated by some arbitrary config
+/// </summary>
+[ImplicitDataDefinitionForInheritors]
+public partial interface IDunGenDistance
+{
+    /// <summary>
+    /// How much to blend between the original noise value and the adjusted one.
+    /// </summary>
+    float BlendWeight { get; }
+}
+
index aecfef2c782df17fa1b991a44fba8f0a972b7358..0d290b67905e8ba55836587dd0370616a88ccabe 100644 (file)
@@ -1,8 +1,16 @@
 namespace Content.Shared.Procedural;
 
+/// <summary>
+/// Procedurally generated dungeon data.
+/// </summary>
 public sealed class Dungeon
 {
-    public readonly List<DungeonRoom> Rooms;
+    public static Dungeon Empty = new Dungeon();
+
+    private List<DungeonRoom> _rooms;
+    private HashSet<Vector2i> _allTiles = new();
+
+    public IReadOnlyList<DungeonRoom> Rooms => _rooms;
 
     /// <summary>
     /// Hashset of the tiles across all rooms.
@@ -17,18 +25,64 @@ public sealed class Dungeon
 
     public readonly HashSet<Vector2i> Entrances = new();
 
-    public Dungeon()
+    public IReadOnlySet<Vector2i> AllTiles => _allTiles;
+
+    public Dungeon() : this(new List<DungeonRoom>())
     {
-        Rooms = new List<DungeonRoom>();
     }
 
     public Dungeon(List<DungeonRoom> rooms)
     {
-        Rooms = rooms;
+        // This reftype is mine now.
+        _rooms = rooms;
+
+        foreach (var room in _rooms)
+        {
+            InternalAddRoom(room);
+        }
+
+        RefreshAllTiles();
+    }
+
+    public void RefreshAllTiles()
+    {
+        _allTiles.Clear();
+        _allTiles.UnionWith(RoomTiles);
+        _allTiles.UnionWith(RoomExteriorTiles);
+        _allTiles.UnionWith(CorridorTiles);
+        _allTiles.UnionWith(CorridorExteriorTiles);
+        _allTiles.UnionWith(Entrances);
+    }
+
+    public void Rebuild()
+    {
+        _allTiles.Clear();
+
+        RoomTiles.Clear();
+        RoomExteriorTiles.Clear();
+        Entrances.Clear();
 
-        foreach (var room in Rooms)
+        foreach (var room in _rooms)
         {
-            Entrances.UnionWith(room.Entrances);
+            InternalAddRoom(room, false);
         }
+
+        RefreshAllTiles();
+    }
+
+    public void AddRoom(DungeonRoom room)
+    {
+        _rooms.Add(room);
+        InternalAddRoom(room);
+    }
+
+    private void InternalAddRoom(DungeonRoom room, bool refreshAll = true)
+    {
+        Entrances.UnionWith(room.Entrances);
+        RoomTiles.UnionWith(room.Tiles);
+        RoomExteriorTiles.UnionWith(room.Exterior);
+
+        if (refreshAll)
+            RefreshAllTiles();
     }
 }
index 07a7000d63748b8acd381bc241a4070735b0ec81..d0d8e0ff12d7760cfcc3408fb94a516751fef5f6 100644 (file)
@@ -1,21 +1,53 @@
-using Content.Shared.Procedural.DungeonGenerators;
 using Content.Shared.Procedural.PostGeneration;
 using Robust.Shared.Prototypes;
 
 namespace Content.Shared.Procedural;
 
-[Prototype("dungeonConfig")]
+[Prototype]
 public sealed partial class DungeonConfigPrototype : IPrototype
 {
     [IdDataField]
     public string ID { get; private set; } = default!;
 
-    [DataField("generator", required: true)]
-    public IDunGen Generator = default!;
+    /// <summary>
+    /// <see cref="Data"/>
+    /// </summary>
+    [DataField]
+    public DungeonData Data = DungeonData.Empty;
+
+    /// <summary>
+    /// The secret sauce, procedural generation layers that get run.
+    /// </summary>
+    [DataField(required: true)]
+    public List<IDunGenLayer> Layers = new();
+
+    /// <summary>
+    /// Should we reserve the tiles generated by this config so no other dungeons can spawn on it within the same job?
+    /// </summary>
+    [DataField]
+    public bool ReserveTiles;
+
+    /// <summary>
+    /// Minimum times to run the config.
+    /// </summary>
+    [DataField]
+    public int MinCount = 1;
+
+    /// <summary>
+    /// Maximum times to run the config.
+    /// </summary>
+    [DataField]
+    public int MaxCount = 1;
+
+    /// <summary>
+    /// Minimum amount we can offset the dungeon by.
+    /// </summary>
+    [DataField]
+    public int MinOffset;
 
     /// <summary>
-    /// Ran after the main dungeon is created.
+    /// Maximum amount we can offset the dungeon by.
     /// </summary>
-    [DataField("postGeneration")]
-    public List<IPostDunGen> PostGeneration = new();
+    [DataField]
+    public int MaxOffset;
 }
diff --git a/Content.Shared/Procedural/DungeonData.cs b/Content.Shared/Procedural/DungeonData.cs
new file mode 100644 (file)
index 0000000..58ec966
--- /dev/null
@@ -0,0 +1,105 @@
+using System.Linq;
+using Content.Shared.Maps;
+using Content.Shared.Storage;
+using Content.Shared.Whitelist;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Procedural;
+
+/// <summary>
+/// Used to set dungeon values for all layers.
+/// </summary>
+/// <remarks>
+/// This lets us share data between different dungeon configs without having to repeat entire configs.
+/// </remarks>
+[DataRecord]
+public sealed class DungeonData
+{
+    // I hate this but it also significantly reduces yaml bloat if we add like 10 variations on the same set of layers
+    // e.g. science rooms, engi rooms, cargo rooms all under PlanetBase for example.
+    // without having to do weird nesting. It also means we don't need to copy-paste the same prototype across several layers
+    // The alternative is doing like,
+    // 2 layer prototype, 1 layer with the specified data, 3 layer prototype, 2 layers with specified data, etc.
+    // As long as we just keep the code clean over time it won't be bad to maintain.
+
+    public static DungeonData Empty = new();
+
+    public Dictionary<DungeonDataKey, Color> Colors = new();
+    public Dictionary<DungeonDataKey, EntProtoId> Entities = new();
+    public Dictionary<DungeonDataKey, ProtoId<EntitySpawnEntryPrototype>> SpawnGroups = new();
+    public Dictionary<DungeonDataKey, ProtoId<ContentTileDefinition>> Tiles = new();
+    public Dictionary<DungeonDataKey, EntityWhitelist> Whitelists = new();
+
+    /// <summary>
+    /// Applies the specified data to this data.
+    /// </summary>
+    public void Apply(DungeonData data)
+    {
+        // Copy-paste moment.
+        foreach (var color in data.Colors)
+        {
+            Colors[color.Key] = color.Value;
+        }
+
+        foreach (var color in data.Entities)
+        {
+            Entities[color.Key] = color.Value;
+        }
+
+        foreach (var color in data.SpawnGroups)
+        {
+            SpawnGroups[color.Key] = color.Value;
+        }
+
+        foreach (var color in data.Tiles)
+        {
+            Tiles[color.Key] = color.Value;
+        }
+
+        foreach (var color in data.Whitelists)
+        {
+            Whitelists[color.Key] = color.Value;
+        }
+    }
+
+    public DungeonData Clone()
+    {
+        return new DungeonData
+        {
+            // Only shallow clones but won't matter for DungeonJob purposes.
+            Colors = Colors.ShallowClone(),
+            Entities = Entities.ShallowClone(),
+            SpawnGroups = SpawnGroups.ShallowClone(),
+            Tiles = Tiles.ShallowClone(),
+            Whitelists = Whitelists.ShallowClone(),
+        };
+    }
+}
+
+public enum DungeonDataKey : byte
+{
+    // Colors
+    Decals,
+
+    // Entities
+    Cabling,
+    CornerWalls,
+    Fill,
+    Junction,
+    Walls,
+
+    // SpawnGroups
+    CornerClutter,
+    Entrance,
+    EntranceFlank,
+    WallMounts,
+    Window,
+
+    // Tiles
+    FallbackTile,
+    WidenTile,
+
+    // Whitelists
+    Rooms,
+}
diff --git a/Content.Shared/Procedural/DungeonGenerators/ExteriorDunGen.cs b/Content.Shared/Procedural/DungeonGenerators/ExteriorDunGen.cs
new file mode 100644 (file)
index 0000000..e9a5181
--- /dev/null
@@ -0,0 +1,13 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Procedural.DungeonGenerators;
+
+/// <summary>
+/// Generates the specified config on an exterior tile of the attached dungeon.
+/// Useful if you're using <see cref="GroupDunGen"/> or otherwise want a dungeon on the outside of a grid.
+/// </summary>
+public sealed partial class ExteriorDunGen : IDunGenLayer
+{
+    [DataField(required: true)]
+    public ProtoId<DungeonConfigPrototype> Proto;
+}
diff --git a/Content.Shared/Procedural/DungeonGenerators/FillGridDunGen.cs b/Content.Shared/Procedural/DungeonGenerators/FillGridDunGen.cs
new file mode 100644 (file)
index 0000000..368ec5c
--- /dev/null
@@ -0,0 +1,10 @@
+namespace Content.Shared.Procedural.DungeonGenerators;
+
+/// <summary>
+/// Fills unreserved tiles with the specified entity prototype.
+/// </summary>
+/// <remarks>
+/// DungeonData keys are:
+/// - Fill
+/// </remarks>
+public sealed partial class FillGridDunGen : IDunGenLayer;
diff --git a/Content.Shared/Procedural/DungeonGenerators/IDunGen.cs b/Content.Shared/Procedural/DungeonGenerators/IDunGen.cs
deleted file mode 100644 (file)
index 5aa82f1..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Content.Shared.Procedural.DungeonGenerators;
-
-[ImplicitDataDefinitionForInheritors]
-public partial interface IDunGen
-{
-
-}
diff --git a/Content.Shared/Procedural/DungeonGenerators/NoiseDistanceDunGen.cs b/Content.Shared/Procedural/DungeonGenerators/NoiseDistanceDunGen.cs
new file mode 100644 (file)
index 0000000..0dfb3da
--- /dev/null
@@ -0,0 +1,18 @@
+using Content.Shared.Procedural.Distance;
+
+namespace Content.Shared.Procedural.DungeonGenerators;
+
+/// <summary>
+/// Like <see cref="Content.Shared.Procedural.DungeonGenerators.NoiseDunGenLayer"/> except with maximum dimensions
+/// </summary>
+public sealed partial class NoiseDistanceDunGen : IDunGenLayer
+{
+    [DataField]
+    public IDunGenDistance? DistanceConfig;
+
+    [DataField]
+    public Vector2i Size;
+
+    [DataField(required: true)]
+    public List<NoiseDunGenLayer> Layers = new();
+}
index 3ea0d989a2a2c03a212c32ba891d014af7152604..56d63bec8f599cb4361196cd0628889a327e748c 100644 (file)
@@ -1,15 +1,12 @@
-using Content.Shared.Maps;
+using Content.Shared.Procedural.Distance;
 using Robust.Shared.Noise;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
 
 namespace Content.Shared.Procedural.DungeonGenerators;
 
 /// <summary>
 /// Generates dungeon flooring based on the specified noise.
 /// </summary>
-public sealed partial class NoiseDunGen : IDunGen
+public sealed partial class NoiseDunGen : IDunGenLayer
 {
     /*
      * Floodfills out from 0 until it finds a valid tile.
index ef61fff4b045fa6d46668eccb646f1ac3baf8a6d..aeb24d0144820ff5e5e919f3f7d0e869e8ca95db 100644 (file)
@@ -1,30 +1,20 @@
-using Content.Shared.Maps;
-using Content.Shared.Tag;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
+using Robust.Shared.Prototypes;
 
 namespace Content.Shared.Procedural.DungeonGenerators;
 
 /// <summary>
 /// Places rooms in pre-selected pack layouts. Chooses rooms from the specified whitelist.
 /// </summary>
-public sealed partial class PrefabDunGen : IDunGen
+/// <remarks>
+/// DungeonData keys are:
+/// - FallbackTile
+/// - Rooms
+/// </remarks>
+public sealed partial class PrefabDunGen : IDunGenLayer
 {
-    /// <summary>
-    /// Rooms need to match any of these tags
-    /// </summary>
-    [DataField("roomWhitelist", customTypeSerializer:typeof(PrototypeIdListSerializer<TagPrototype>))]
-    public List<string> RoomWhitelist = new();
-
     /// <summary>
     /// Room pack presets we can use for this prefab.
     /// </summary>
-    [DataField("presets", required: true, customTypeSerializer:typeof(PrototypeIdListSerializer<DungeonPresetPrototype>))]
-    public List<string> Presets = new();
-
-    /// <summary>
-    /// Fallback tile.
-    /// </summary>
-    [DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
-    public string Tile = "FloorSteel";
+    [DataField(required: true)]
+    public List<ProtoId<DungeonPresetPrototype>> Presets = new();
 }
diff --git a/Content.Shared/Procedural/DungeonGenerators/PrototypeDunGen.cs b/Content.Shared/Procedural/DungeonGenerators/PrototypeDunGen.cs
new file mode 100644 (file)
index 0000000..346c60a
--- /dev/null
@@ -0,0 +1,13 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Procedural.DungeonGenerators;
+
+/// <summary>
+/// Runs another <see cref="DungeonConfigPrototype"/>.
+/// Used for storing data on 1 system.
+/// </summary>
+public sealed partial class PrototypeDunGen : IDunGenLayer
+{
+    [DataField(required: true)]
+    public ProtoId<DungeonConfigPrototype> Proto;
+}
diff --git a/Content.Shared/Procedural/DungeonGenerators/ReplaceTileDunGen.cs b/Content.Shared/Procedural/DungeonGenerators/ReplaceTileDunGen.cs
new file mode 100644 (file)
index 0000000..64b76b4
--- /dev/null
@@ -0,0 +1,30 @@
+using Content.Shared.Maps;
+using Robust.Shared.Noise;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Procedural.DungeonGenerators;
+
+/// <summary>
+/// Replaces existing tiles if they're not empty.
+/// </summary>
+public sealed partial class ReplaceTileDunGen : IDunGenLayer
+{
+    /// <summary>
+    /// Chance for a non-variant tile to be used, in case they're too noisy.
+    /// </summary>
+    [DataField]
+    public float VariantWeight = 0.1f;
+
+    [DataField(required: true)]
+    public List<ReplaceTileLayer> Layers = new();
+}
+
+[DataRecord]
+public record struct ReplaceTileLayer
+{
+    public ProtoId<ContentTileDefinition> Tile;
+
+    public float Threshold;
+
+    public FastNoiseLite Noise;
+}
diff --git a/Content.Shared/Procedural/DungeonLayers/MobsDunGen.cs b/Content.Shared/Procedural/DungeonLayers/MobsDunGen.cs
new file mode 100644 (file)
index 0000000..30b502e
--- /dev/null
@@ -0,0 +1,21 @@
+using Content.Shared.Storage;
+
+namespace Content.Shared.Procedural.DungeonLayers;
+
+
+/// <summary>
+/// Spawns mobs inside of the dungeon randomly.
+/// </summary>
+public sealed partial class MobsDunGen : IDunGenLayer
+{
+    // Counts separate to config to avoid some duplication.
+
+    [DataField]
+    public int MinCount = 1;
+
+    [DataField]
+    public int MaxCount = 1;
+
+    [DataField(required: true)]
+    public List<EntitySpawnEntry> Groups = new();
+}
diff --git a/Content.Shared/Procedural/DungeonLayers/OreDunGen.cs b/Content.Shared/Procedural/DungeonLayers/OreDunGen.cs
new file mode 100644 (file)
index 0000000..31bf367
--- /dev/null
@@ -0,0 +1,42 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Procedural.DungeonLayers;
+
+/// <summary>
+/// Generates veins inside of the specified dungeon.
+/// </summary>
+/// <remarks>
+/// Generates on top of existing entities for sanity reasons moreso than performance.
+/// </remarks>
+public sealed partial class OreDunGen : IDunGenLayer
+{
+    /// <summary>
+    /// If the vein generation should occur on top of existing entities what are we replacing.
+    /// </summary>
+    [DataField]
+    public EntProtoId? Replacement;
+
+    /// <summary>
+    /// Entity to spawn.
+    /// </summary>
+    [DataField(required: true)]
+    public EntProtoId Entity;
+
+    /// <summary>
+    /// Maximum amount of group spawns
+    /// </summary>
+    [DataField]
+    public int Count = 10;
+
+    /// <summary>
+    /// Minimum entities to spawn in one group.
+    /// </summary>
+    [DataField]
+    public int MinGroupSize = 1;
+
+    /// <summary>
+    /// Maximum entities to spawn in one group.
+    /// </summary>
+    [DataField]
+    public int MaxGroupSize = 1;
+}
index 4802949d2f38a09c5f33c69450d8d32abec72d78..0c6af8f23db0722822ac408960ca7b2ba8989d80 100644 (file)
@@ -2,6 +2,7 @@ using System.Numerics;
 
 namespace Content.Shared.Procedural;
 
+// TODO: Cache center and bounds and shit and don't make the caller deal with it.
 public sealed record DungeonRoom(HashSet<Vector2i> Tiles, Vector2 Center, Box2i Bounds, HashSet<Vector2i> Exterior)
 {
     public readonly List<Vector2i> Entrances = new();
diff --git a/Content.Shared/Procedural/IDunGenLayer.cs b/Content.Shared/Procedural/IDunGenLayer.cs
new file mode 100644 (file)
index 0000000..a4e8045
--- /dev/null
@@ -0,0 +1,7 @@
+namespace Content.Shared.Procedural;
+
+[ImplicitDataDefinitionForInheritors]
+public partial interface IDunGenLayer
+{
+
+}
diff --git a/Content.Shared/Procedural/PostGeneration/AutoCablingDunGen.cs b/Content.Shared/Procedural/PostGeneration/AutoCablingDunGen.cs
new file mode 100644 (file)
index 0000000..5afad7e
--- /dev/null
@@ -0,0 +1,10 @@
+namespace Content.Shared.Procedural.PostGeneration;
+
+/// <summary>
+/// Runs cables throughout the dungeon.
+/// </summary>
+/// <remarks>
+/// DungeonData keys are:
+/// - Cabling
+/// </remarks>
+public sealed partial class AutoCablingDunGen : IDunGenLayer;
diff --git a/Content.Shared/Procedural/PostGeneration/AutoCablingPostGen.cs b/Content.Shared/Procedural/PostGeneration/AutoCablingPostGen.cs
deleted file mode 100644 (file)
index 8278352..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-using Robust.Shared.Prototypes;
-
-namespace Content.Shared.Procedural.PostGeneration;
-
-/// <summary>
-/// Runs cables throughout the dungeon.
-/// </summary>
-public sealed partial class AutoCablingPostGen : IPostDunGen
-{
-    [DataField]
-    public EntProtoId Entity = "CableApcExtension";
-}
similarity index 78%
rename from Content.Shared/Procedural/PostGeneration/BiomePostGen.cs
rename to Content.Shared/Procedural/PostGeneration/BiomeDunGen.cs
index d02de241355eee6e262f8b019c8b4fe4fd126e50..833cf2dec769a9894097e0334bc8c223eae465cd 100644 (file)
@@ -1,5 +1,4 @@
 using Content.Shared.Parallax.Biomes;
-using Content.Shared.Procedural.PostGeneration;
 using Robust.Shared.Prototypes;
 
 namespace Content.Shared.Procedural.PostGeneration;
@@ -8,7 +7,7 @@ namespace Content.Shared.Procedural.PostGeneration;
 /// Generates a biome on top of valid tiles, then removes the biome when done.
 /// Only works if no existing biome is present.
 /// </summary>
-public sealed partial class BiomePostGen : IPostDunGen
+public sealed partial class BiomeDunGen : IDunGenLayer
 {
     [DataField(required: true)]
     public ProtoId<BiomeTemplatePrototype> BiomeTemplate;
similarity index 73%
rename from Content.Shared/Procedural/PostGeneration/BiomeMarkerLayerPostGen.cs
rename to Content.Shared/Procedural/PostGeneration/BiomeMarkerLayerDunGen.cs
index dc64febe7b054922263049cc41bd8d8b64071c63..af5d7c5d8f9af0c7fb42b76be717b9c9764a56ba 100644 (file)
@@ -1,5 +1,3 @@
-using Content.Shared.Parallax.Biomes.Markers;
-using Content.Shared.Procedural.PostGeneration;
 using Content.Shared.Random;
 using Robust.Shared.Prototypes;
 
@@ -8,7 +6,7 @@ namespace Content.Shared.Procedural.PostGeneration;
 /// <summary>
 /// Spawns the specified marker layer on top of the dungeon rooms.
 /// </summary>
-public sealed partial class BiomeMarkerLayerPostGen : IPostDunGen
+public sealed partial class BiomeMarkerLayerDunGen : IDunGenLayer
 {
     /// <summary>
     /// How many times to spawn marker layers; can duplicate.
diff --git a/Content.Shared/Procedural/PostGeneration/BoundaryWallDunGen.cs b/Content.Shared/Procedural/PostGeneration/BoundaryWallDunGen.cs
new file mode 100644 (file)
index 0000000..4151527
--- /dev/null
@@ -0,0 +1,23 @@
+namespace Content.Shared.Procedural.PostGeneration;
+
+/// <summary>
+/// Iterates room edges and places the relevant tiles and walls on any free indices.
+/// </summary>
+/// <remarks>
+/// Dungeon data keys are:
+/// - CornerWalls (Optional)
+/// - FallbackTile
+/// - Walls
+/// </remarks>
+public sealed partial class BoundaryWallDunGen : IDunGenLayer
+{
+    [DataField]
+    public BoundaryWallFlags Flags = BoundaryWallFlags.Corridors | BoundaryWallFlags.Rooms;
+}
+
+[Flags]
+public enum BoundaryWallFlags : byte
+{
+    Rooms = 1 << 0,
+    Corridors = 1 << 1,
+}
diff --git a/Content.Shared/Procedural/PostGeneration/BoundaryWallPostGen.cs b/Content.Shared/Procedural/PostGeneration/BoundaryWallPostGen.cs
deleted file mode 100644 (file)
index 390ff42..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-using Content.Shared.Maps;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Shared.Procedural.PostGeneration;
-
-/// <summary>
-/// Iterates room edges and places the relevant tiles and walls on any free indices.
-/// </summary>
-public sealed partial class BoundaryWallPostGen : IPostDunGen
-{
-    [DataField]
-    public ProtoId<ContentTileDefinition> Tile = "FloorSteel";
-
-    [DataField]
-    public EntProtoId Wall = "WallSolid";
-
-    /// <summary>
-    /// Walls to use in corners if applicable.
-    /// </summary>
-    [DataField]
-    public string? CornerWall;
-
-    [DataField]
-    public BoundaryWallFlags Flags = BoundaryWallFlags.Corridors | BoundaryWallFlags.Rooms;
-}
-
-[Flags]
-public enum BoundaryWallFlags : byte
-{
-    Rooms = 1 << 0,
-    Corridors = 1 << 1,
-}
diff --git a/Content.Shared/Procedural/PostGeneration/CornerClutterDunGen.cs b/Content.Shared/Procedural/PostGeneration/CornerClutterDunGen.cs
new file mode 100644 (file)
index 0000000..2a90428
--- /dev/null
@@ -0,0 +1,14 @@
+namespace Content.Shared.Procedural.PostGeneration;
+
+/// <summary>
+/// Spawns entities inside corners.
+/// </summary>
+/// <remarks>
+/// Dungeon data keys are:
+/// - CornerClutter
+/// </remarks>
+public sealed partial class CornerClutterDunGen : IDunGenLayer
+{
+    [DataField]
+    public float Chance = 0.50f;
+}
diff --git a/Content.Shared/Procedural/PostGeneration/CornerClutterPostGen.cs b/Content.Shared/Procedural/PostGeneration/CornerClutterPostGen.cs
deleted file mode 100644 (file)
index a16c7f9..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-using Content.Shared.Storage;
-
-namespace Content.Shared.Procedural.PostGeneration;
-
-/// <summary>
-/// Spawns entities inside corners.
-/// </summary>
-public sealed partial class CornerClutterPostGen : IPostDunGen
-{
-    [DataField]
-    public float Chance = 0.50f;
-
-    /// <summary>
-    /// The default starting bulbs
-    /// </summary>
-    [DataField(required: true)]
-    public List<EntitySpawnEntry> Contents = new();
-}
similarity index 85%
rename from Content.Shared/Procedural/PostGeneration/CorridorClutterPostGen.cs
rename to Content.Shared/Procedural/PostGeneration/CorridorClutterDunGen.cs
index a8a74ba6ccb0367885e04d0f22f76604298315e2..5b397b40dfc1852539b0c380ad683b708f4f90d0 100644 (file)
@@ -5,7 +5,7 @@ namespace Content.Shared.Procedural.PostGeneration;
 /// <summary>
 /// Adds entities randomly to the corridors.
 /// </summary>
-public sealed partial class CorridorClutterPostGen : IPostDunGen
+public sealed partial class CorridorClutterDunGen : IDunGenLayer
 {
     [DataField]
     public float Chance = 0.05f;
similarity index 72%
rename from Content.Shared/Procedural/PostGeneration/CorridorDecalSkirtingPostGen.cs
rename to Content.Shared/Procedural/PostGeneration/CorridorDecalSkirtingDunGen.cs
index 4b139a8be65168472c79179d87c78f1e83b5efe3..e6090436555561d114c884fb9387daf5e0704c18 100644 (file)
@@ -7,29 +7,23 @@ namespace Content.Shared.Procedural.PostGeneration;
 /// <summary>
 /// Applies decal skirting to corridors.
 /// </summary>
-public sealed partial class CorridorDecalSkirtingPostGen : IPostDunGen
+public sealed partial class CorridorDecalSkirtingDunGen : IDunGenLayer
 {
-    /// <summary>
-    /// Color to apply to decals.
-    /// </summary>
-    [DataField("color")]
-    public Color? Color;
-
     /// <summary>
     /// Decal where 1 edge is found.
     /// </summary>
-    [DataField("cardinalDecals")]
+    [DataField]
     public Dictionary<DirectionFlag, string> CardinalDecals = new();
 
     /// <summary>
     /// Decal where 1 corner edge is found.
     /// </summary>
-    [DataField("pocketDecals")]
+    [DataField]
     public Dictionary<Direction, string> PocketDecals = new();
 
     /// <summary>
     /// Decal where 2 or 3 edges are found.
     /// </summary>
-    [DataField("cornerDecals")]
+    [DataField]
     public Dictionary<DirectionFlag, string> CornerDecals = new();
 }
similarity index 73%
rename from Content.Shared/Procedural/PostGeneration/CorridorPostGen.cs
rename to Content.Shared/Procedural/PostGeneration/CorridorDunGen.cs
index 705ae99dcef45090096985efc264122e042f0ae6..6d75cd9cb2bda3781d20f7a979cc799e454bf2e9 100644 (file)
@@ -1,12 +1,13 @@
-using Content.Shared.Maps;
-using Robust.Shared.Prototypes;
-
 namespace Content.Shared.Procedural.PostGeneration;
 
 /// <summary>
 /// Connects room entrances via corridor segments.
 /// </summary>
-public sealed partial class CorridorPostGen : IPostDunGen
+/// <remarks>
+/// Dungeon data keys are:
+/// - FallbackTile
+/// </remarks>
+public sealed partial class CorridorDunGen : IDunGenLayer
 {
     /// <summary>
     /// How far we're allowed to generate a corridor before calling it.
@@ -17,9 +18,6 @@ public sealed partial class CorridorPostGen : IPostDunGen
     [DataField]
     public int PathLimit = 2048;
 
-    [DataField]
-    public ProtoId<ContentTileDefinition> Tile = "FloorSteel";
-
     /// <summary>
     /// How wide to make the corridor.
     /// </summary>
diff --git a/Content.Shared/Procedural/PostGeneration/DungeonEntranceDunGen.cs b/Content.Shared/Procedural/PostGeneration/DungeonEntranceDunGen.cs
new file mode 100644 (file)
index 0000000..40cc95f
--- /dev/null
@@ -0,0 +1,18 @@
+namespace Content.Shared.Procedural.PostGeneration;
+
+/// <summary>
+/// Selects [count] rooms and places external doors to them.
+/// </summary>
+/// <remarks>
+/// Dungeon data keys are:
+/// - Entrance
+/// - FallbackTile
+/// </remarks>
+public sealed partial class DungeonEntranceDunGen : IDunGenLayer
+{
+    /// <summary>
+    /// How many rooms we place doors on.
+    /// </summary>
+    [DataField]
+    public int Count = 1;
+}
diff --git a/Content.Shared/Procedural/PostGeneration/DungeonEntrancePostGen.cs b/Content.Shared/Procedural/PostGeneration/DungeonEntrancePostGen.cs
deleted file mode 100644 (file)
index 3398b51..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-using Content.Shared.Maps;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
-
-namespace Content.Shared.Procedural.PostGeneration;
-
-/// <summary>
-/// Selects [count] rooms and places external doors to them.
-/// </summary>
-public sealed partial class DungeonEntrancePostGen : IPostDunGen
-{
-    /// <summary>
-    /// How many rooms we place doors on.
-    /// </summary>
-    [DataField("count")]
-    public int Count = 1;
-
-    [DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
-    public List<string?> Entities = new()
-    {
-        "CableApcExtension",
-        "AirlockGlass",
-    };
-
-    [DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
-    public string Tile = "FloorSteel";
-}
diff --git a/Content.Shared/Procedural/PostGeneration/EntranceFlankDunGen.cs b/Content.Shared/Procedural/PostGeneration/EntranceFlankDunGen.cs
new file mode 100644 (file)
index 0000000..27baa48
--- /dev/null
@@ -0,0 +1,11 @@
+namespace Content.Shared.Procedural.PostGeneration;
+
+/// <summary>
+/// Spawns entities on either side of an entrance.
+/// </summary>
+/// <remarks>
+/// Dungeon data keys are:
+/// - FallbackTile
+/// -
+/// </remarks>
+public sealed partial class EntranceFlankDunGen : IDunGenLayer;
diff --git a/Content.Shared/Procedural/PostGeneration/EntranceFlankPostGen.cs b/Content.Shared/Procedural/PostGeneration/EntranceFlankPostGen.cs
deleted file mode 100644 (file)
index 96e9bd5..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-using Content.Shared.Maps;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Shared.Procedural.PostGeneration;
-
-/// <summary>
-/// Spawns entities on either side of an entrance.
-/// </summary>
-public sealed partial class EntranceFlankPostGen : IPostDunGen
-{
-    [DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
-    public string Tile = "FloorSteel";
-
-    [DataField("entities")]
-    public List<string?> Entities = new();
-}
diff --git a/Content.Shared/Procedural/PostGeneration/ExternalWindowDunGen.cs b/Content.Shared/Procedural/PostGeneration/ExternalWindowDunGen.cs
new file mode 100644 (file)
index 0000000..0b29344
--- /dev/null
@@ -0,0 +1,11 @@
+namespace Content.Shared.Procedural.PostGeneration;
+
+/// <summary>
+/// If external areas are found will try to generate windows.
+/// </summary>
+/// <remarks>
+/// Dungeon data keys are:
+/// - EntranceFlank
+/// - FallbackTile
+/// </remarks>
+public sealed partial class ExternalWindowDunGen : IDunGenLayer;
diff --git a/Content.Shared/Procedural/PostGeneration/ExternalWindowPostGen.cs b/Content.Shared/Procedural/PostGeneration/ExternalWindowPostGen.cs
deleted file mode 100644 (file)
index d5580ba..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-using Content.Shared.Maps;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
-
-namespace Content.Shared.Procedural.PostGeneration;
-
-/// <summary>
-/// If external areas are found will try to generate windows.
-/// </summary>
-public sealed partial class ExternalWindowPostGen : IPostDunGen
-{
-    [DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
-    public List<string?> Entities = new()
-    {
-        "Grille",
-        "Window",
-    };
-
-    [DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
-    public string Tile = "FloorSteel";
-}
diff --git a/Content.Shared/Procedural/PostGeneration/IPostDunGen.cs b/Content.Shared/Procedural/PostGeneration/IPostDunGen.cs
deleted file mode 100644 (file)
index b55cab8..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace Content.Shared.Procedural.PostGeneration;
-
-/// <summary>
-/// Ran after generating dungeon rooms. Can be used for additional loot, contents, etc.
-/// </summary>
-[ImplicitDataDefinitionForInheritors]
-public partial interface IPostDunGen
-{
-
-}
diff --git a/Content.Shared/Procedural/PostGeneration/InternalWindowDunGen.cs b/Content.Shared/Procedural/PostGeneration/InternalWindowDunGen.cs
new file mode 100644 (file)
index 0000000..11b1c6a
--- /dev/null
@@ -0,0 +1,11 @@
+namespace Content.Shared.Procedural.PostGeneration;
+
+/// <summary>
+/// If internal areas are found will try to generate windows.
+/// </summary>
+/// <remarks>
+/// Dungeon data keys are:
+/// - FallbackTile
+/// - Window
+/// </remarks>
+public sealed partial class InternalWindowDunGen : IDunGenLayer;
diff --git a/Content.Shared/Procedural/PostGeneration/InternalWindowPostGen.cs b/Content.Shared/Procedural/PostGeneration/InternalWindowPostGen.cs
deleted file mode 100644 (file)
index 4c6223e..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-using Content.Shared.Maps;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
-
-namespace Content.Shared.Procedural.PostGeneration;
-
-/// <summary>
-/// If internal areas are found will try to generate windows.
-/// </summary>
-public sealed partial class InternalWindowPostGen : IPostDunGen
-{
-    [DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
-    public List<string?> Entities = new()
-    {
-        "Grille",
-        "Window",
-    };
-
-    [DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
-    public string Tile = "FloorSteel";
-}
diff --git a/Content.Shared/Procedural/PostGeneration/JunctionDunGen.cs b/Content.Shared/Procedural/PostGeneration/JunctionDunGen.cs
new file mode 100644 (file)
index 0000000..899f271
--- /dev/null
@@ -0,0 +1,18 @@
+namespace Content.Shared.Procedural.PostGeneration;
+
+/// <summary>
+/// Places the specified entities at junction areas.
+/// </summary>
+/// <remarks>
+/// Dungeon data keys are:
+/// - Entrance
+/// - FallbackTile
+/// </remarks>
+public sealed partial class JunctionDunGen : IDunGenLayer
+{
+    /// <summary>
+    /// Width to check for junctions.
+    /// </summary>
+    [DataField]
+    public int Width = 3;
+}
diff --git a/Content.Shared/Procedural/PostGeneration/JunctionPostGen.cs b/Content.Shared/Procedural/PostGeneration/JunctionPostGen.cs
deleted file mode 100644 (file)
index 5c4cf43..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-using Content.Shared.Maps;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
-
-namespace Content.Shared.Procedural.PostGeneration;
-
-/// <summary>
-/// Places the specified entities at junction areas.
-/// </summary>
-public sealed partial class JunctionPostGen : IPostDunGen
-{
-    /// <summary>
-    /// Width to check for junctions.
-    /// </summary>
-    [DataField("width")]
-    public int Width = 3;
-
-    [DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
-    public string Tile = "FloorSteel";
-
-    [DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
-    public List<string?> Entities = new()
-    {
-        "CableApcExtension",
-        "AirlockGlass"
-    };
-}
diff --git a/Content.Shared/Procedural/PostGeneration/MiddleConnectionDunGen.cs b/Content.Shared/Procedural/PostGeneration/MiddleConnectionDunGen.cs
new file mode 100644 (file)
index 0000000..a5758c1
--- /dev/null
@@ -0,0 +1,19 @@
+namespace Content.Shared.Procedural.PostGeneration;
+
+/// <summary>
+/// Places the specified entities on the middle connections between rooms
+/// </summary>
+public sealed partial class MiddleConnectionDunGen : IDunGenLayer
+{
+    /// <summary>
+    /// How much overlap there needs to be between 2 rooms exactly.
+    /// </summary>
+    [DataField]
+    public int OverlapCount = -1;
+
+    /// <summary>
+    /// How many connections to spawn between rooms.
+    /// </summary>
+    [DataField]
+    public int Count = 1;
+}
diff --git a/Content.Shared/Procedural/PostGeneration/MiddleConnectionPostGen.cs b/Content.Shared/Procedural/PostGeneration/MiddleConnectionPostGen.cs
deleted file mode 100644 (file)
index d29a654..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-using Content.Shared.Maps;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
-
-namespace Content.Shared.Procedural.PostGeneration;
-
-/// <summary>
-/// Places the specified entities on the middle connections between rooms
-/// </summary>
-public sealed partial class MiddleConnectionPostGen : IPostDunGen
-{
-    /// <summary>
-    /// How much overlap there needs to be between 2 rooms exactly.
-    /// </summary>
-    [DataField("overlapCount")]
-    public int OverlapCount = -1;
-
-    /// <summary>
-    /// How many connections to spawn between rooms.
-    /// </summary>
-    [DataField("count")]
-    public int Count = 1;
-
-    [DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
-    public string Tile = "FloorSteel";
-
-    [DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
-    public List<string?> Entities = new()
-    {
-        "CableApcExtension",
-        "AirlockGlass"
-    };
-
-    /// <summary>
-    /// If overlap > 1 then what should spawn on the edges.
-    /// </summary>
-    [DataField("edgeEntities")] public List<string?> EdgeEntities = new();
-}
diff --git a/Content.Shared/Procedural/PostGeneration/RoomEntranceDunGen.cs b/Content.Shared/Procedural/PostGeneration/RoomEntranceDunGen.cs
new file mode 100644 (file)
index 0000000..d3b5672
--- /dev/null
@@ -0,0 +1,11 @@
+namespace Content.Shared.Procedural.PostGeneration;
+
+/// <summary>
+/// Places tiles / entities onto room entrances.
+/// </summary>
+/// <remarks>
+/// DungeonData keys are:
+/// - Entrance
+/// - FallbackTile
+/// </remarks>
+public sealed partial class RoomEntranceDunGen : IDunGenLayer;
diff --git a/Content.Shared/Procedural/PostGeneration/RoomEntrancePostGen.cs b/Content.Shared/Procedural/PostGeneration/RoomEntrancePostGen.cs
deleted file mode 100644 (file)
index 5fd78b0..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-using Content.Shared.Maps;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
-
-namespace Content.Shared.Procedural.PostGeneration;
-
-/// <summary>
-/// Places tiles / entities onto room entrances.
-/// </summary>
-public sealed partial class RoomEntrancePostGen : IPostDunGen
-{
-    [DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
-    public List<string?> Entities = new()
-    {
-        "CableApcExtension",
-        "AirlockGlass",
-    };
-
-    [DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
-    public string Tile = "FloorSteel";
-}
diff --git a/Content.Shared/Procedural/PostGeneration/SplineDungeonConnectorDunGen.cs b/Content.Shared/Procedural/PostGeneration/SplineDungeonConnectorDunGen.cs
new file mode 100644 (file)
index 0000000..ec8349c
--- /dev/null
@@ -0,0 +1,19 @@
+namespace Content.Shared.Procedural.PostGeneration;
+
+/// <summary>
+/// Connects dungeons via points that get subdivided.
+/// </summary>
+public sealed partial class SplineDungeonConnectorDunGen : IDunGenLayer
+{
+    /// <summary>
+    /// Will divide the distance between the start and end points so that no subdivision is more than these metres away.
+    /// </summary>
+    [DataField]
+    public int DivisionDistance = 10;
+
+    /// <summary>
+    /// How much each subdivision can vary from the middle.
+    /// </summary>
+    [DataField]
+    public float VarianceMax = 0.35f;
+}
diff --git a/Content.Shared/Procedural/PostGeneration/WallMountDunGen.cs b/Content.Shared/Procedural/PostGeneration/WallMountDunGen.cs
new file mode 100644 (file)
index 0000000..a5c790c
--- /dev/null
@@ -0,0 +1,13 @@
+namespace Content.Shared.Procedural.PostGeneration;
+
+/// <summary>
+/// Spawns on the boundary tiles of rooms.
+/// </summary>
+public sealed partial class WallMountDunGen : IDunGenLayer
+{
+    /// <summary>
+    /// Chance per free tile to spawn a wallmount.
+    /// </summary>
+    [DataField]
+    public double Prob = 0.1;
+}
diff --git a/Content.Shared/Procedural/PostGeneration/WallMountPostGen.cs b/Content.Shared/Procedural/PostGeneration/WallMountPostGen.cs
deleted file mode 100644 (file)
index 1fbdedf..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-using Content.Shared.Maps;
-using Content.Shared.Storage;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Shared.Procedural.PostGeneration;
-
-/// <summary>
-/// Spawns on the boundary tiles of rooms.
-/// </summary>
-public sealed partial class WallMountPostGen : IPostDunGen
-{
-    [DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
-    public string Tile = "FloorSteel";
-
-    [DataField("spawns")]
-    public List<EntitySpawnEntry> Spawns = new();
-
-    /// <summary>
-    /// Chance per free tile to spawn a wallmount.
-    /// </summary>
-    [DataField("prob")]
-    public double Prob = 0.1;
-}
similarity index 73%
rename from Content.Shared/Procedural/PostGeneration/WormCorridorPostGen.cs
rename to Content.Shared/Procedural/PostGeneration/WormCorridorDunGen.cs
index c57d92ef9568e505cc31d48ea5c528403b3b5abc..b71e845a733706f45b99313d2081445b6a5555e4 100644 (file)
@@ -1,14 +1,10 @@
-using Content.Shared.Maps;
-using Content.Shared.Procedural.DungeonGenerators;
-using Robust.Shared.Prototypes;
-
 namespace Content.Shared.Procedural.PostGeneration;
 
 // Ime a worm
 /// <summary>
 /// Generates worm corridors.
 /// </summary>
-public sealed partial class WormCorridorPostGen : IPostDunGen
+public sealed partial class WormCorridorDunGen : IDunGenLayer
 {
     [DataField]
     public int PathLimit = 2048;
@@ -31,9 +27,6 @@ public sealed partial class WormCorridorPostGen : IPostDunGen
     [DataField]
     public Angle MaxAngleChange = Angle.FromDegrees(45);
 
-    [DataField]
-    public ProtoId<ContentTileDefinition> Tile = "FloorSteel";
-
     /// <summary>
     /// How wide to make the corridor.
     /// </summary>
index 81390e5f65a0ece58a1c30fbdd21378f2dcfd4be..62edb36db93ae7b01155c8c5579bdb28b7142d50 100644 (file)
@@ -32,14 +32,14 @@ public abstract partial class SharedSalvageSystem
             var layers = new Dictionary<string, int>();
 
             // If we ever add more random layers will need to Next on these.
-            foreach (var layer in configProto.PostGeneration)
+            foreach (var layer in configProto.Layers)
             {
                 switch (layer)
                 {
-                    case BiomePostGen:
+                    case BiomeDunGen:
                         rand.Next();
                         break;
-                    case BiomeMarkerLayerPostGen marker:
+                    case BiomeMarkerLayerDunGen marker:
                         for (var i = 0; i < marker.Count; i++)
                         {
                             var proto = _proto.Index(marker.MarkerTemplate).Pick(rand);
index a382e943ff946e8b3b4ce1fd5aec128c22c26539..db2cbaa138b8747fbc3d6e79dde526dc5bd2a4f7 100644 (file)
@@ -18,7 +18,7 @@ public abstract partial class SharedShuttleSystem : EntitySystem
     [Dependency] protected readonly SharedTransformSystem XformSystem = default!;
     [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
 
-    public const float FTLRange = 512f;
+    public const float FTLRange = 256f;
     public const float FTLBufferRange = 8f;
 
     private EntityQuery<MapGridComponent> _gridQuery;
index 792459c72f72fce7dabcae88702dc655e91bbec1..6e24681c2dbdde3bdf5a2afa27515c63d5731e11 100644 (file)
@@ -5,6 +5,19 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototy
 
 namespace Content.Shared.Storage;
 
+/// <summary>
+/// Prototype wrapper around <see cref="EntitySpawnEntry"/>
+/// </summary>
+[Prototype]
+public sealed class EntitySpawnEntryPrototype : IPrototype
+{
+    [IdDataField]
+    public string ID { get; } = string.Empty;
+
+    [DataField]
+    public List<EntitySpawnEntry> Entries = new();
+}
+
 /// <summary>
 ///     Dictates a list of items that can be spawned.
 /// </summary>
index 8a0d6c40030403d76b6cbc101e161eda4aafac14..7b58091588345ea653997ad54ab346b44466b683 100644 (file)
       path: /Maps/Shuttles/cargo.yml
     - type: GridSpawn
       groups:
-        trade:
+        vgroid: !type:DungeonSpawnGroup
+          minimumDistance: 1000
+          nameDataset: names_borer
+          addComponents:
+          - type: Gravity
+            enabled: true
+            inherent: true
+          protos:
+          - VGRoid
+        trade: !type:GridSpawnGroup
           addComponents:
             - type: ProtectedGrid
             - type: TradeStation
           paths:
             - /Maps/Shuttles/trading_outpost.yml
-        mining:
+        mining: !type:GridSpawnGroup
           paths:
           - /Maps/Shuttles/mining.yml
         # Spawn last
-        ruins:
+        ruins: !type:GridSpawnGroup
           hide: true
           nameGrid: true
           minCount: 2
index d131805bf51adaff934fe342ed3f872b2975d754..e14bf26e0dbdd06d78345b7477bb5a63edb8a04e 100644 (file)
@@ -1,5 +1,7 @@
 #TODO: Someone should probably move the ore vein prototypes into their own file, or otherwise split this up in some way. This should not be 1.5k lines long.
 
+# Anyway
+# See WallRock variants for the remappings.
 
 #Asteroid rock
 - type: entity
   description: An ore vein rich with coal.
   suffix: Coal
   components:
-    - type: OreVein
-      oreChance: 1.0
-      currentOre: OreCoal
-    - type: Sprite
-      layers:
-        - state: rock
-        - map: [ "enum.EdgeLayer.South" ]
-          state: rock_south
-        - map: [ "enum.EdgeLayer.East" ]
-          state: rock_east
-        - map: [ "enum.EdgeLayer.North" ]
-          state: rock_north
-        - map: [ "enum.EdgeLayer.West" ]
-          state: rock_west
-        - state: rock_coal
+  - type: EntityRemap
+    mask:
+      AsteroidRock: AsteroidRockCoal
+      WallRockBasalt: WallRockBasaltCoal
+      WallRockChromite: WallRockChromiteCoal
+      WallRockSand: WallRockSandCoal
+      WallRockSnow: WallRockSnowCoal
+  - type: OreVein
+    oreChance: 1.0
+    currentOre: OreCoal
+  - type: Sprite
+    layers:
+      - state: rock
+      - map: [ "enum.EdgeLayer.South" ]
+        state: rock_south
+      - map: [ "enum.EdgeLayer.East" ]
+        state: rock_east
+      - map: [ "enum.EdgeLayer.North" ]
+        state: rock_north
+      - map: [ "enum.EdgeLayer.West" ]
+        state: rock_west
+      - state: rock_coal
 
 - type: entity
   id: WallRockGold
   description: An ore vein rich with gold.
   suffix: Gold
   components:
-    - type: OreVein
-      oreChance: 1.0
-      currentOre: OreGold
-    - type: Sprite
-      layers:
-        - state: rock
-        - map: [ "enum.EdgeLayer.South" ]
-          state: rock_south
-        - map: [ "enum.EdgeLayer.East" ]
-          state: rock_east
-        - map: [ "enum.EdgeLayer.North" ]
-          state: rock_north
-        - map: [ "enum.EdgeLayer.West" ]
-          state: rock_west
-        - state: rock_gold
+  - type: EntityRemap
+    mask:
+      AsteroidRock: AsteroidRockGold
+      WallRockBasalt: WallRockBasaltGold
+      WallRockChromite: WallRockChromiteGold
+      WallRockSand: WallRockSandGold
+      WallRockSnow: WallRockSnowGold
+  - type: OreVein
+    oreChance: 1.0
+    currentOre: OreGold
+  - type: Sprite
+    layers:
+      - state: rock
+      - map: [ "enum.EdgeLayer.South" ]
+        state: rock_south
+      - map: [ "enum.EdgeLayer.East" ]
+        state: rock_east
+      - map: [ "enum.EdgeLayer.North" ]
+        state: rock_north
+      - map: [ "enum.EdgeLayer.West" ]
+        state: rock_west
+      - state: rock_gold
 
 - type: entity
   id: WallRockPlasma
   description: An ore vein rich with plasma.
   suffix: Plasma
   components:
-    - type: OreVein
-      oreChance: 1.0
-      currentOre: OrePlasma
-    - type: Sprite
-      layers:
-        - state: rock
-        - map: [ "enum.EdgeLayer.South" ]
-          state: rock_south
-        - map: [ "enum.EdgeLayer.East" ]
-          state: rock_east
-        - map: [ "enum.EdgeLayer.North" ]
-          state: rock_north
-        - map: [ "enum.EdgeLayer.West" ]
-          state: rock_west
-        - state: rock_phoron
+  - type: EntityRemap
+    mask:
+      AsteroidRock: AsteroidRockPlasma
+      WallRockBasalt: WallRockBasaltPlasma
+      WallRockChromite: WallRockChromitePlasma
+      WallRockSand: WallRockSandPlasma
+      WallRockSnow: WallRockSnowPlasma
+  - type: OreVein
+    oreChance: 1.0
+    currentOre: OrePlasma
+  - type: Sprite
+    layers:
+      - state: rock
+      - map: [ "enum.EdgeLayer.South" ]
+        state: rock_south
+      - map: [ "enum.EdgeLayer.East" ]
+        state: rock_east
+      - map: [ "enum.EdgeLayer.North" ]
+        state: rock_north
+      - map: [ "enum.EdgeLayer.West" ]
+        state: rock_west
+      - state: rock_phoron
 
 - type: entity
   id: WallRockQuartz
   description: An ore vein rich with quartz.
   suffix: Quartz
   components:
-    - type: OreVein
-      oreChance: 1.0
-      currentOre: OreSpaceQuartz
-    - type: Sprite
-      layers:
-        - state: rock
-        - map: [ "enum.EdgeLayer.South" ]
-          state: rock_south
-        - map: [ "enum.EdgeLayer.East" ]
-          state: rock_east
-        - map: [ "enum.EdgeLayer.North" ]
-          state: rock_north
-        - map: [ "enum.EdgeLayer.West" ]
-          state: rock_west
-        - state: rock_quartz
+  - type: EntityRemap
+    mask:
+      AsteroidRock: AsteroidRockQuartz
+      WallRockBasalt: WallRockBasaltQuartz
+      WallRockChromite: WallRockChromiteQuartz
+      WallRockSand: WallRockSandQuartz
+      WallRockSnow: WallRockSnowQuartz
+  - type: OreVein
+    oreChance: 1.0
+    currentOre: OreSpaceQuartz
+  - type: Sprite
+    layers:
+      - state: rock
+      - map: [ "enum.EdgeLayer.South" ]
+        state: rock_south
+      - map: [ "enum.EdgeLayer.East" ]
+        state: rock_east
+      - map: [ "enum.EdgeLayer.North" ]
+        state: rock_north
+      - map: [ "enum.EdgeLayer.West" ]
+        state: rock_west
+      - state: rock_quartz
 
 - type: entity
   id: WallRockSilver
   description: An ore vein rich with silver.
   suffix: Silver
   components:
-    - type: OreVein
-      oreChance: 1.0
-      currentOre: OreSilver
-    - type: Sprite
-      layers:
-        - state: rock
-        - map: [ "enum.EdgeLayer.South" ]
-          state: rock_south
-        - map: [ "enum.EdgeLayer.East" ]
-          state: rock_east
-        - map: [ "enum.EdgeLayer.North" ]
-          state: rock_north
-        - map: [ "enum.EdgeLayer.West" ]
-          state: rock_west
-        - state: rock_silver
+  - type: EntityRemap
+    mask:
+      AsteroidRock: AsteroidRockSilver
+      WallRockBasalt: WallRockBasaltSilver
+      WallRockChromite: WallRockChromiteSilver
+      WallRockSand: WallRockSandSilver
+      WallRockSnow: WallRockSnowSilver
+  - type: OreVein
+    oreChance: 1.0
+    currentOre: OreSilver
+  - type: Sprite
+    layers:
+      - state: rock
+      - map: [ "enum.EdgeLayer.South" ]
+        state: rock_south
+      - map: [ "enum.EdgeLayer.East" ]
+        state: rock_east
+      - map: [ "enum.EdgeLayer.North" ]
+        state: rock_north
+      - map: [ "enum.EdgeLayer.West" ]
+        state: rock_west
+      - state: rock_silver
 
 # Yes I know it drops steel but we may get smelting at some point
 - type: entity
   description: An ore vein rich with iron.
   suffix: Iron
   components:
+    - type: EntityRemap
+      mask:
+        AsteroidRock: AsteroidRockTin
+        WallRockBasalt: WallRockBasaltTin
+        WallRockChromite: WallRockChromiteTin
+        WallRockSand: WallRockSandTin
+        WallRockSnow: WallRockSnowTin
     - type: OreVein
       oreChance: 1.0
       currentOre: OreSteel
   description: An ore vein rich with uranium.
   suffix: Uranium
   components:
-    - type: OreVein
-      oreChance: 1.0
-      currentOre: OreUranium
-    - type: Sprite
-      layers:
-        - state: rock
-        - map: [ "enum.EdgeLayer.South" ]
-          state: rock_south
-        - map: [ "enum.EdgeLayer.East" ]
-          state: rock_east
-        - map: [ "enum.EdgeLayer.North" ]
-          state: rock_north
-        - map: [ "enum.EdgeLayer.West" ]
-          state: rock_west
-        - state: rock_uranium
+  - type: EntityRemap
+    mask:
+      AsteroidRock: AsteroidRockUranium
+      WallRockBasalt: WallRockBasaltUranium
+      WallRockChromite: WallRockChromiteUranium
+      WallRockSand: WallRockSandUranium
+      WallRockSnow: WallRockSnowUranium
+  - type: OreVein
+    oreChance: 1.0
+    currentOre: OreUranium
+  - type: Sprite
+    layers:
+      - state: rock
+      - map: [ "enum.EdgeLayer.South" ]
+        state: rock_south
+      - map: [ "enum.EdgeLayer.East" ]
+        state: rock_east
+      - map: [ "enum.EdgeLayer.North" ]
+        state: rock_north
+      - map: [ "enum.EdgeLayer.West" ]
+        state: rock_west
+      - state: rock_uranium
 
 
 - type: entity
   description: An ore vein rich with bananium.
   suffix: Bananium
   components:
-    - type: OreVein
-      oreChance: 1.0
-      currentOre: OreBananium
-    - type: Sprite
-      layers:
-        - state: rock
-        - map: [ "enum.EdgeLayer.South" ]
-          state: rock_south
-        - map: [ "enum.EdgeLayer.East" ]
-          state: rock_east
-        - map: [ "enum.EdgeLayer.North" ]
-          state: rock_north
-        - map: [ "enum.EdgeLayer.West" ]
-          state: rock_west
-        - state: rock_bananium
+  - type: EntityRemap
+    mask:
+      AsteroidRock: AsteroidRockBananium
+      WallRockBasalt: WallRockBasaltBananium
+      WallRockChromite: WallRockChromiteBananium
+      WallRockSand: WallRockSandBananium
+      WallRockSnow: WallRockSnowBananium
+  - type: OreVein
+    oreChance: 1.0
+    currentOre: OreBananium
+  - type: Sprite
+    layers:
+      - state: rock
+      - map: [ "enum.EdgeLayer.South" ]
+        state: rock_south
+      - map: [ "enum.EdgeLayer.East" ]
+        state: rock_east
+      - map: [ "enum.EdgeLayer.North" ]
+        state: rock_north
+      - map: [ "enum.EdgeLayer.West" ]
+        state: rock_west
+      - state: rock_bananium
 
 - type: entity
   id: WallRockArtifactFragment
   description: A rock wall. What's that sticking out of it?
   suffix: Artifact Fragment
   components:
-    - type: OreVein
-      oreChance: 1.0
-      currentOre: OreArtifactFragment
-    - type: Sprite
-      layers:
-        - state: rock
-        - map: [ "enum.EdgeLayer.South" ]
-          state: rock_south
-        - map: [ "enum.EdgeLayer.East" ]
-          state: rock_east
-        - map: [ "enum.EdgeLayer.North" ]
-          state: rock_north
-        - map: [ "enum.EdgeLayer.West" ]
-          state: rock_west
-        - state: rock_artifact_fragment
+  - type: EntityRemap
+    mask:
+      AsteroidRock: AsteroidRockArtifactFragment
+      WallRockBasalt: WallRockBasaltArtifactFragment
+      WallRockChromite: WallRockChromiteArtifactFragment
+      WallRockSand: WallRockSandArtifactFragment
+      WallRockSnow: WallRockSnowArtifactFragment
+  - type: OreVein
+    oreChance: 1.0
+    currentOre: OreArtifactFragment
+  - type: Sprite
+    layers:
+      - state: rock
+      - map: [ "enum.EdgeLayer.South" ]
+        state: rock_south
+      - map: [ "enum.EdgeLayer.East" ]
+        state: rock_east
+      - map: [ "enum.EdgeLayer.North" ]
+        state: rock_north
+      - map: [ "enum.EdgeLayer.West" ]
+        state: rock_west
+      - state: rock_artifact_fragment
 
 - type: entity
   id: WallRockSalt
   description: An ore vein rich with salt.
   suffix: Salt
   components:
-    - type: OreVein
-      oreChance: 1.0
-      currentOre: OreSalt
-    - type: Sprite
-      layers:
-        - state: rock
-        - map: [ "enum.EdgeLayer.South" ]
-          state: rock_south
-        - map: [ "enum.EdgeLayer.East" ]
-          state: rock_east
-        - map: [ "enum.EdgeLayer.North" ]
-          state: rock_north
-        - map: [ "enum.EdgeLayer.West" ]
-          state: rock_west
-        - state: rock_salt
+  - type: EntityRemap
+    mask:
+      AsteroidRock: AsteroidRockSalt
+      WallRockBasalt: WallRockBasaltSalt
+      WallRockChromite: WallRockChromiteSalt
+      WallRockSand: WallRockSandSalt
+      WallRockSnow: WallRockSnowSalt
+  - type: OreVein
+    oreChance: 1.0
+    currentOre: OreSalt
+  - type: Sprite
+    layers:
+      - state: rock
+      - map: [ "enum.EdgeLayer.South" ]
+        state: rock_south
+      - map: [ "enum.EdgeLayer.East" ]
+        state: rock_east
+      - map: [ "enum.EdgeLayer.North" ]
+        state: rock_north
+      - map: [ "enum.EdgeLayer.West" ]
+        state: rock_west
+      - state: rock_salt
 
 # Basalt variants
 - type: entity
index a21b709afada83a942f49f36b2536cfd27c92a50..c20b80af55bf979c08bc555eb669aaff43140620 100644 (file)
@@ -15,7 +15,8 @@
 - type: dungeonConfig
   id: BlobAsteroid
   # Floor generation
-  generator: !type:NoiseDunGen
+  layers:
+  - !type:NoiseDunGen
     tileCap: 1500
     capStd: 32
     iterations: 3
           fractalType: FBm
           octaves: 2
           lacunarity: 2
-  # Everything else
-  postGeneration:
-    # Generate biome
-    - !type:BiomePostGen
-      biomeTemplate: Asteroid
 
-    # Generate ore veins
-    - !type:MarkerLayerPostGen
-      markerTemplate: AsteroidOre
+  # Generate biome
+  - !type:BiomeDunGen
+    biomeTemplate: Asteroid
+
+  # Generate ore veins
+  - !type:BiomeMarkerLayerDunGen
+    markerTemplate: AsteroidOre
 
 # Multiple smaller asteroids
 # This is a pain so we generate fewer tiles
 - type: dungeonConfig
   id: ClusterAsteroid
   # Floor generation
-  generator: !type:NoiseDunGen
+  layers:
+  - !type:NoiseDunGen
     tileCap: 1000
     capStd: 32
     layers:
           fractalType: FBm
           octaves: 2
           lacunarity: 2
-  # Everything else
-  postGeneration:
-    # Generate biome
-    - !type:BiomePostGen
-      biomeTemplate: Asteroid
 
-    # Generate ore veins
-    - !type:MarkerLayerPostGen
-      markerTemplate: AsteroidOre
+  # Generate biome
+  - !type:BiomeDunGen
+    biomeTemplate: Asteroid
+
+  # Generate ore veins
+  - !type:BiomeMarkerLayerDunGen
+    markerTemplate: AsteroidOre
 
 # Long and spindly, less smooth than blob
 - type: dungeonConfig
   id: SpindlyAsteroid
   # Floor generation
-  generator: !type:NoiseDunGen
+  layers:
+  - !type:NoiseDunGen
     tileCap: 1500
     capStd: 32
     layers:
           octaves: 3
           lacunarity: 2
           cellularDistanceFunction: Euclidean
-  postGeneration:
-    # Generate biome
-    - !type:BiomePostGen
-      biomeTemplate: Asteroid
 
-    # Generate ore veins
-    - !type:MarkerLayerPostGen
-      markerTemplate: AsteroidOre
+  # Generate biome
+  - !type:BiomeDunGen
+    biomeTemplate: Asteroid
+
+  # Generate ore veins
+  - !type:BiomeMarkerLayerDunGen
+    markerTemplate: AsteroidOre
 
 # Lots of holes in it
 - type: dungeonConfig
   id: SwissCheeseAsteroid
   # Floor generation
-  generator: !type:NoiseDunGen
+  layers:
+  - !type:NoiseDunGen
     tileCap: 1500
     capStd: 32
     layers:
           fractalType: FBm
           octaves: 2
           lacunarity: 2
-  # Everything else
-  postGeneration:
-    # Generate biome
-    - !type:BiomePostGen
-      biomeTemplate: Asteroid
 
-    # Generate ore veins
-    - !type:MarkerLayerPostGen
-      markerTemplate: AsteroidOre
+  # Generate biome
+  - !type:BiomeDunGen
+    biomeTemplate: Asteroid
+
+  # Generate ore veins
+  - !type:BiomeMarkerLayerDunGen
+    markerTemplate: AsteroidOre
index 3614e4e787faa35c633efcd58af04a8fc8c359aa..b55d5a9e697706d9c3c775229ac7ff5af20067fd 100644 (file)
+# Base configs
 - type: dungeonConfig
-  id: Experiment
-  generator: !type:PrefabDunGen
-    roomWhitelist:
-      - SalvageExperiment
+  id: PlanetBase
+  layers:
+  - !type:PrefabDunGen
     presets:
-      - Bucket
-      - Wow
-      - SpaceShip
-      - Tall
-  postGeneration:
-    - !type:CorridorPostGen
-      width: 3
+    - Bucket
+    - Wow
+    - SpaceShip
+    - Tall
 
-    - !type:DungeonEntrancePostGen
-      count: 2
+  - !type:CorridorDunGen
+    width: 3
 
-    - !type:RoomEntrancePostGen
-      entities:
-        - CableApcExtension
-        - AirlockGlass
+  - !type:DungeonEntranceDunGen
+    count: 2
 
-    - !type:EntranceFlankPostGen
-      entities:
-        - Grille
-        - Window
+  - !type:RoomEntranceDunGen
 
-    - !type:ExternalWindowPostGen
-      entities:
-        - Grille
-        - Window
+  - !type:EntranceFlankDunGen
 
-    - !type:WallMountPostGen
-      spawns:
-        # Posters
-        - id: RandomPosterLegit
-          orGroup: content
-        - id: ExtinguisherCabinetFilled
-          prob: 0.2
-          orGroup: content
-        - id: RandomPainting
-          prob: 0.05
-          orGroup: content
-        - id: IntercomCommon
-          prob: 0.1
-          orGroup: content
+  - !type:ExternalWindowDunGen
 
-    - !type:BoundaryWallPostGen
-      tile: FloorSteel
-      wall: WallSolid
-      cornerWall: WallReinforced
+  - !type:WallMountDunGen
 
-    - !type:JunctionPostGen
-      width: 1
+  - !type:BoundaryWallDunGen
 
-    - !type:JunctionPostGen
+  - !type:JunctionDunGen
+    width: 1
 
-    - !type:AutoCablingPostGen
+  - !type:JunctionDunGen
 
-    - !type:CornerClutterPostGen
-      contents:
-        - id: PottedPlantRandom
-          amount: 1
+  - !type:AutoCablingDunGen
 
-    - !type:CorridorDecalSkirtingPostGen
-      color: "#D381C996"
-      cardinalDecals:
-        South: BrickTileWhiteLineS
-        East: BrickTileWhiteLineE
-        North: BrickTileWhiteLineN
-        West: BrickTileWhiteLineW
-      cornerDecals:
-        SouthEast: BrickTileWhiteCornerSe
-        SouthWest: BrickTileWhiteCornerSw
-        NorthEast: BrickTileWhiteCornerNe
-        NorthWest: BrickTileWhiteCornerNw
-      pocketDecals:
-        SouthWest: BrickTileWhiteInnerSw
-        SouthEast: BrickTileWhiteInnerSe
-        NorthWest: BrickTileWhiteInnerNw
-        NorthEast: BrickTileWhiteInnerNe
+  - !type:CornerClutterDunGen
 
+  - !type:CorridorDecalSkirtingDunGen
+    cardinalDecals:
+      South: BrickTileWhiteLineS
+      East: BrickTileWhiteLineE
+      North: BrickTileWhiteLineN
+      West: BrickTileWhiteLineW
+    cornerDecals:
+      SouthEast: BrickTileWhiteCornerSe
+      SouthWest: BrickTileWhiteCornerSw
+      NorthEast: BrickTileWhiteCornerNe
+      NorthWest: BrickTileWhiteCornerNw
+    pocketDecals:
+      SouthWest: BrickTileWhiteInnerSw
+      SouthEast: BrickTileWhiteInnerSe
+      NorthWest: BrickTileWhiteInnerNw
+      NorthEast: BrickTileWhiteInnerNe
 
+# Setups
 - type: dungeonConfig
-  id: LavaBrig
-  generator: !type:PrefabDunGen
-    roomWhitelist:
-      - LavaBrig
-    presets:
-      - Bucket
-      - Wow
-      - SpaceShip
-      - Tall
-  postGeneration:
-    - !type:CorridorPostGen
-      width: 3
-
-    - !type:DungeonEntrancePostGen
-      count: 2
-
-    - !type:RoomEntrancePostGen
-      entities:
-        - CableApcExtension
-        - AirlockSecurityGlassLocked
-
-    - !type:EntranceFlankPostGen
-      entities:
-        - Grille
-        - Window
-
-    - !type:ExternalWindowPostGen
-      entities:
-        - Grille
-        - Window
-
-    - !type:WallMountPostGen
-      spawns:
-        # Posters
-        - id: RandomPosterLegit
-          orGroup: content
-        - id: ExtinguisherCabinetFilled
-          prob: 0.2
-          orGroup: content
-        - id: RandomPainting
-          prob: 0.05
-          orGroup: content
-        - id: IntercomCommon
-          prob: 0.1
-          orGroup: content
-
-    - !type:BoundaryWallPostGen
-      tile: FloorSteel
-      wall: WallSolid
-      cornerWall: WallReinforced
-
-    - !type:JunctionPostGen
-      width: 1
-
-    - !type:JunctionPostGen
-
-    - !type:AutoCablingPostGen
-
-    - !type:CornerClutterPostGen
-      contents:
-        - id: PottedPlantRandom
-          amount: 1
-
-    - !type:CorridorDecalSkirtingPostGen
-      color: "#DE3A3A96"
-      cardinalDecals:
-        South: BrickTileWhiteLineS
-        East: BrickTileWhiteLineE
-        North: BrickTileWhiteLineN
-        West: BrickTileWhiteLineW
-      cornerDecals:
-        SouthEast: BrickTileWhiteCornerSe
-        SouthWest: BrickTileWhiteCornerSw
-        NorthEast: BrickTileWhiteCornerNe
-        NorthWest: BrickTileWhiteCornerNw
-      pocketDecals:
-        SouthWest: BrickTileWhiteInnerSw
-        SouthEast: BrickTileWhiteInnerSe
-        NorthWest: BrickTileWhiteInnerNw
-        NorthEast: BrickTileWhiteInnerNe
+  id: Experiment
+  data:
+    colors:
+      Decals: "#D381C996"
+    entities:
+      Cabling: CableApcExtension
+      CornerWalls: WallReinforced
+      Walls: WallSolid
+    spawnGroups:
+      CornerClutter: BaseClutter
+      Entrance: BaseAirlock
+      EntranceFlank: BaseWindow
+      Junction: BaseAirlock
+      WallMounts: ScienceLabsWalls
+      Window: BaseWindow
+    tiles:
+      FallbackTile: FloorSteel
+    whitelists:
+      Rooms:
+        tags:
+        - SalvageExperiment
+  layers:
+  - !type:PrototypeDunGen
+    proto: PlanetBase
 
 - type: dungeonConfig
-  id: Mineshaft
-  generator: !type:PrefabDunGen
-    tile: FloorCaveDrought
-    roomWhitelist:
-      - Mineshaft
+  id: Haunted
+  data:
+    entities:
+      Walls: WallRock
+    tiles:
+      FallbackTile: FloorCaveDrought
+    whitelists:
+      Rooms:
+        tags:
+        - Mineshaft
+  layers:
+  - !type:PrefabDunGen
     presets:
-      - Bucket
-      - Wow
-      - SpaceShip
-      - Tall
-  postGeneration:
-
-  - !type:CorridorPostGen
-      tile: FloorCaveDrought
-      width: 3
-
-  - !type:DungeonEntrancePostGen
-      count: 5
-      tile: FloorCaveDrought
-      entities:
-        - RandomWoodenWall
-
-  - !type:RoomEntrancePostGen
-      tile: FloorCaveDrought
-      entities:
-        - RandomWoodenWall
-
-  - !type:EntranceFlankPostGen
-      tile: FloorCaveDrought
-      entities:
-        - RandomWoodenWall
-
-  - !type:ExternalWindowPostGen
-      tile: FloorCaveDrought
-      entities:
-        - RandomWoodenWall
-
-  - !type:WallMountPostGen
-      tile: FloorCaveDrought
-      spawns:
-        # Ore
-        - id: WallRockSalt
-          prob: 0.6
-          orGroup: content
-        - id: WallRockCoal
-          prob: 0.6
-          orGroup: content
-        - id: WallRockTin
-          prob: 0.4
-          orGroup: content
-        - id: WallMining
-          prob: 0.8
-          orGroup: content
-
-  - !type:BoundaryWallPostGen
-      tile: FloorCaveDrought
-      wall: WallRock
-      cornerWall: WallRock
+    - Bucket
+    - Wow
+    - SpaceShip
+    - Tall
 
-  - !type:AutoCablingPostGen
-      entity: Catwalk
+  - !type:WormCorridorDunGen
+    width: 3
 
-  - !type:JunctionPostGen
-      tile: FloorCaveDrought
-      width: 3
-      entities:
-        - RandomWoodenSupport
+  - !type:CorridorClutterDunGen
+    contents:
+    - id: FloraStalagmite1
+    - id: FloraStalagmite2
+    - id: FloraStalagmite3
+    - id: FloraStalagmite4
+    - id: FloraStalagmite5
+    - id: FloraStalagmite6
 
-  - !type:CornerClutterPostGen
-      contents:
-        - id: RandomStalagmiteOrCrystal
-          amount: 1
+  - !type:BoundaryWallDunGen
 
 - type: dungeonConfig
-  id: SnowyLabs
-  generator: !type:PrefabDunGen
-    roomWhitelist:
-      - SnowyLabs
-    presets:
-      - Bucket
-      - Wow
-      - SpaceShip
-      - Tall
-  postGeneration:
-    - !type:CorridorPostGen
-      width: 3
-
-    - !type:DungeonEntrancePostGen
-      count: 2
-
-    - !type:RoomEntrancePostGen
-      entities:
-        - CableApcExtension
-        - AirlockFreezerHydroponicsLocked
-
-    - !type:EntranceFlankPostGen
-      entities:
-        - Grille
-        - Window
-
-    - !type:ExternalWindowPostGen
-      entities:
-        - Grille
-        - Window
-
-    - !type:WallMountPostGen
-      spawns:
-        # Posters
-        - id: RandomPosterLegit
-          orGroup: content
-        - id: ExtinguisherCabinetFilled
-          prob: 0.2
-          orGroup: content
-        - id: RandomPainting
-          prob: 0.05
-          orGroup: content
-        - id: IntercomScience
-          prob: 0.1
-          orGroup: content
-
-    - !type:BoundaryWallPostGen
-        tile: FloorSteel
-        wall: WallSilver
-        cornerWall: WallSilver
-
-    - !type:JunctionPostGen
-      width: 1
-      entities:
-        - AirlockGlass
-
-    - !type:JunctionPostGen
-      entities:
-        - AirlockGlass
-
-    - !type:AutoCablingPostGen
-
-    - !type:CornerClutterPostGen
-      contents:
-        - id: PottedPlantRandom
-          amount: 1
-
-    - !type:CorridorDecalSkirtingPostGen
-      color: "#4cc7aa96"
-      cardinalDecals:
-        South: BrickTileWhiteLineS
-        East: BrickTileWhiteLineE
-        North: BrickTileWhiteLineN
-        West: BrickTileWhiteLineW
-      cornerDecals:
-        SouthEast: BrickTileWhiteCornerSe
-        SouthWest: BrickTileWhiteCornerSw
-        NorthEast: BrickTileWhiteCornerNe
-        NorthWest: BrickTileWhiteCornerNw
-      pocketDecals:
-        SouthWest: BrickTileWhiteInnerSw
-        SouthEast: BrickTileWhiteInnerSe
-        NorthWest: BrickTileWhiteInnerNw
-        NorthEast: BrickTileWhiteInnerNe
-
-# todo: Add a biome dungeon generator
-# Add corridor first gens that place rooms on top
-# Add a worm corridor gen (place subsequent corridors somewhere randomly along the path)
-# Place room entrances on ends of corridors touching a tile
-# Remove all room tiles from corridors
-# Fix paths up and try to reconnect all corridor tiles
-# Add a postgen step to spread rooms out, though it shouldn't spread into corridor exteriors
+  id: LavaBrig
+  data:
+    colors:
+      Decals: "#DE3A3A96"
+    entities:
+      Cabling: CableApcExtension
+      CornerWalls: WallReinforced
+      Walls: WallSolid
+    spawnGroups:
+      CornerClutter: BaseClutter
+      Entrance: LavaBrigEntrance
+      EntranceFlank: BaseWindow
+      Junction: BaseAirlock
+      WallMounts: ScienceLabsWalls
+      Window: BaseWindow
+    whitelists:
+      Rooms:
+        tags:
+        - LavaBrig
+  layers:
+  - !type:PrototypeDunGen
+    proto: PlanetBase
 
 - type: dungeonConfig
-  id: Haunted
-  generator: !type:PrefabDunGen
-    tile: FloorCaveDrought
-    roomWhitelist:
-    - Mineshaft
-    presets:
-    - Bucket
-    - Wow
-    - SpaceShip
-    - Tall
-  postGeneration:
-    - !type:WormCorridorPostGen
-      width: 3
-      tile: FloorCaveDrought
-
-    - !type:CorridorClutterPostGen
-      contents:
-      - id: FloraStalagmite1
-      - id: FloraStalagmite2
-      - id: FloraStalagmite3
-      - id: FloraStalagmite4
-      - id: FloraStalagmite5
-      - id: FloraStalagmite6
+  id: Mineshaft
+  data:
+    entities:
+      Cabling: Catwalk
+    spawnGroups:
+      CornerClutter: MineshaftClutter
+      Entrance: BaseWoodWall
+      EntranceFlank: BaseWoodWall
+      Junction: BaseWoodSupport
+      Window: BaseWoodWall
+    tiles:
+      FallbackTile: FloorCaveDrought
+    whitelists:
+      Rooms:
+        tags:
+        - Mineshaft
+  layers:
+  - !type:PrototypeDunGen
+    proto: PlanetBase
 
-    - !type:BoundaryWallPostGen
-      tile: FloorCaveDrought
-      wall: WallRock
+- type: dungeonConfig
+  id: SnowyLabs
+  data:
+    colors:
+      Decals: "#4cc7aa96"
+    entities:
+      Cabling: CableApcExtension
+      CornerWalls: WallSilver
+      Walls: WallSilver
+    spawnGroups:
+      CornerClutter: BaseClutter
+      Entrance: SnowyLabsEntrance
+      EntranceFlank: BaseWindow
+      Junction: BaseAirlock
+      WallMounts: SnowyLabsWalls
+      Window: BaseWindow
+    tiles:
+      FallbackTile: FloorSteel
+    whitelists:
+      Rooms:
+        tags:
+        - SnowyLabs
+  layers:
+  - !type:PrototypeDunGen
+    proto: PlanetBase
+
+# Spawn groups
+# Basic
+- type: entitySpawnEntry
+  id: BaseClutter
+  entries:
+  - id: PottedPlantRandom
+    amount: 1
+
+- type: entitySpawnEntry
+  id: BaseAirlock
+  entries:
+  - id: CableApcExtension
+  - id: AirlockGlass
+
+- type: entitySpawnEntry
+  id: BaseWindow
+  entries:
+  - id: Grille
+  - id: Window
+
+# Lava brig
+- type: entitySpawnEntry
+  id: LavaBrigEntrance
+  entries:
+  - id: CableApcExtension
+  - id: AirlockSecurityGlassLocked
+
+# Mineshaft
+- type: entitySpawnEntry
+  id: BaseWoodWall
+  entries:
+  - id: RandomWoodenWall
+
+- type: entitySpawnEntry
+  id: BaseWoodSupport
+  entries:
+  - id: RandomWoodenSupport
+
+- type: entitySpawnEntry
+  id: MineshaftClutter
+  entries:
+  - id: RandomStalagmiteOrCrystal
+    amount: 1
+
+- type: entitySpawnEntry
+  id: MineshaftWalls
+  entries:
+  # Ore
+  - id: WallRockSalt
+    prob: 0.6
+    orGroup: content
+  - id: WallRockCoal
+    prob: 0.6
+    orGroup: content
+  - id: WallRockTin
+    prob: 0.4
+    orGroup: content
+  - id: WallMining
+    prob: 0.8
+    orGroup: content
+
+# Science lab
+- type: entitySpawnEntry
+  id: ScienceLabsWalls
+  entries:
+  # Posters
+  - id: RandomPosterLegit
+    orGroup: content
+  - id: ExtinguisherCabinetFilled
+    prob: 0.2
+    orGroup: content
+  - id: RandomPainting
+    prob: 0.05
+    orGroup: content
+  - id: IntercomCommon
+    prob: 0.1
+    orGroup: content
+
+# Snowy labs
+- type: entitySpawnEntry
+  id: SnowyLabsEntrance
+  entries:
+  - id: CableApcExtension
+  - id: AirlockFreezerHydroponicsLocked
+
+- type: entitySpawnEntry
+  id: SnowyLabsWalls
+  entries:
+  # Posters
+  - id: RandomPosterLegit
+    orGroup: content
+  - id: ExtinguisherCabinetFilled
+    prob: 0.2
+    orGroup: content
+  - id: RandomPainting
+    prob: 0.05
+    orGroup: content
+  - id: IntercomScience
+    prob: 0.1
+    orGroup: content
diff --git a/Resources/Prototypes/Procedural/vgroid.yml b/Resources/Prototypes/Procedural/vgroid.yml
new file mode 100644 (file)
index 0000000..49e956e
--- /dev/null
@@ -0,0 +1,191 @@
+# Okay so my general thought is this:
+# 1. Generate the large mass
+# 2. Generate smaller masses offset
+# 3. Generate N normal dungeons around the larger mass, preferably near the border
+# 4. Generate large paths / small paths around the place
+# 5. Spawn ores + fill the rest and the normal stuff
+
+# If you want mobs they needed to be added at specific steps due to how dungeons work at the moment.
+
+- type: dungeonConfig
+  id: VGRoid
+  layers:
+    - !type:PrototypeDunGen
+      proto: VGRoidBlob
+    - !type:PrototypeDunGen
+      proto: VGRoidExterior
+    - !type:PrototypeDunGen
+      proto: VGRoidSmaller
+    - !type:PrototypeDunGen
+      proto: VGRoidSmallPaths
+    # Fill
+    - !type:PrototypeDunGen
+      proto: VGRoidFill
+    # Ores
+    - !type:OreDunGen
+      replacement: IronRock
+      entity: IronRockIron
+      count: 50
+      minGroupSize: 20
+      maxGroupSize: 30
+    - !type:OreDunGen
+      replacement: IronRock
+      entity: IronRockCoal
+      count: 50
+      minGroupSize: 20
+      maxGroupSize: 30
+    - !type:OreDunGen
+      replacement: IronRock
+      entity: IronRockQuartz
+      count: 50
+      minGroupSize: 20
+      maxGroupSize: 30
+    - !type:OreDunGen
+      replacement: IronRock
+      entity: IronRockSalt
+      count: 50
+      minGroupSize: 20
+      maxGroupSize: 30
+    - !type:OreDunGen
+      replacement: IronRock
+      entity: IronRockGold
+      count: 50
+      minGroupSize: 10
+      maxGroupSize: 20
+    - !type:OreDunGen
+      replacement: IronRock
+      entity: IronRockSilver
+      count: 50
+      minGroupSize: 10
+      maxGroupSize: 20
+    - !type:OreDunGen
+      replacement: IronRock
+      entity: IronRockPlasma
+      count: 50
+      minGroupSize: 10
+      maxGroupSize: 20
+    - !type:OreDunGen
+      replacement: IronRock
+      entity: IronRockUranium
+      count: 50
+      minGroupSize: 10
+      maxGroupSize: 20
+    - !type:OreDunGen
+      replacement: IronRock
+      entity: IronRockBananium
+      count: 50
+      minGroupSize: 10
+      maxGroupSize: 20
+    - !type:OreDunGen
+      replacement: IronRock
+      entity: IronRockArtifactFragment
+      count: 50
+      minGroupSize: 2
+      maxGroupSize: 4
+
+# Configs
+- type: dungeonConfig
+  id: VGRoidBlob
+  layers:
+  - !type:NoiseDistanceDunGen
+    size: 272, 272
+    distanceConfig: !type:DunGenEuclideanSquaredDistance
+      blendWeight: 0.80
+    layers:
+    - tile: FloorAsteroidSand
+      threshold: 0.50
+      noise:
+        frequency: 0.010
+        noiseType: OpenSimplex2
+        fractalType: FBm
+        octaves: 5
+        lacunarity: 2
+        gain: 0.5
+
+- type: dungeonConfig
+  id: VGRoidSmaller
+  minOffset: 40
+  maxOffset: 60
+  layers:
+  - !type:NoiseDistanceDunGen
+    size: 150, 150
+    distanceConfig: !type:DunGenEuclideanSquaredDistance
+    layers:
+    - tile: FloorAsteroidSand
+      threshold: 0.50
+      noise:
+        frequency: 0.080
+        noiseType: OpenSimplex2
+        fractalType: FBm
+        octaves: 5
+        lacunarity: 1.5
+        gain: 0.5
+
+- type: dungeonConfig
+  id: VGRoidExterior
+  reserveTiles: true
+  data:
+    tiles:
+      FallbackTile: PlatingAsteroid
+      WidenTile: FloorAsteroidSand
+  layers:
+  - !type:PrototypeDunGen
+    proto: VGRoidExteriorDungeons
+  - !type:SplineDungeonConnectorDunGen
+
+- type: dungeonConfig
+  id: VGRoidExteriorDungeons
+  reserveTiles: true
+  minCount: 2
+  maxCount: 3
+  layers:
+  - !type:ExteriorDunGen
+    proto: Experiment
+  - !type:MobsDunGen
+    minCount: 5
+    maxCount: 8
+    groups:
+    - id: MobXeno
+      amount: 1
+
+#- type: dungeonConfig
+#  id: VGRoidInteriorDungeons
+#  minCount: 3
+#  maxCount: 5
+#  # Just randomly spawn these in bounds, doesn't really matter if they go out.
+
+- type: dungeonConfig
+  id: VGRoidSmallPaths
+  reserveTiles: true
+  layers:
+  - !type:ReplaceTileDunGen
+    layers:
+    - tile: FloorAsteroidSand
+      threshold: 0.75
+      noise:
+        frequency: 0.040
+        noiseType: OpenSimplex2
+        fractalType: Ridged
+        lacunarity: 1.5
+        octaves: 2
+        gain: 2.0
+  # Mobs
+  # If you want exterior dungeon mobs add them under the prototype.
+  - !type:MobsDunGen
+    minCount: 20
+    maxCount: 30
+    groups:
+    - id: MobXeno
+      amount: 1
+
+#- type: dungeonConfig
+#  id: VGRoidOres
+
+# Fill with rocks.
+- type: dungeonConfig
+  id: VGRoidFill
+  data:
+    entities:
+      Fill: IronRock
+  layers:
+  - !type:FillGridDunGen