diff --git a/Content.Server/NPC/Pathfinding/PathfindingSystem.Breadth.cs b/Content.Server/NPC/Pathfinding/PathfindingSystem.Breadth.cs
new file mode 100644
index 00000000000000..ee8eaa9ad1a167
--- /dev/null
+++ b/Content.Server/NPC/Pathfinding/PathfindingSystem.Breadth.cs
@@ -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.
+ */
+
+ ///
+ /// Pathfinding args for a 1-many path.
+ ///
+ public record struct BreadthPathArgs()
+ {
+ public Vector2i Start;
+ public List Ends;
+
+ public bool Diagonals = false;
+
+ public Func? TileCost;
+
+ public int Limit = 10000;
+ }
+
+ ///
+ /// Gets a BFS path from start to any end. Can also supply an optional tile-cost for tiles.
+ ///
+ public SimplePathResult GetBreadthPath(BreadthPathArgs args)
+ {
+ var cameFrom = new Dictionary();
+ var costSoFar = new Dictionary();
+ var frontier = new PriorityQueue();
+
+ 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
index 00000000000000..479d5ad77f6b12
--- /dev/null
+++ b/Content.Server/NPC/Pathfinding/PathfindingSystem.Line.cs
@@ -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
index 00000000000000..7afd3d78df1ff5
--- /dev/null
+++ b/Content.Server/NPC/Pathfinding/PathfindingSystem.Simple.cs
@@ -0,0 +1,154 @@
+namespace Content.Server.NPC.Pathfinding;
+
+public sealed partial class PathfindingSystem
+{
+ ///
+ /// Pathfinding args for a 1-1 path.
+ ///
+ public record struct SimplePathArgs()
+ {
+ public Vector2i Start;
+ public Vector2i End;
+
+ public bool Diagonals = false;
+
+ public int Limit = 10000;
+
+ ///
+ /// Custom tile-costs if applicable.
+ ///
+ public Func? TileCost;
+ }
+
+ public record struct SimplePathResult
+ {
+ public static SimplePathResult NoPath = new();
+
+ public List Path;
+ public Dictionary CameFrom;
+ }
+
+ ///
+ /// Gets simple A* path from start to end. Can also supply an optional tile-cost for tiles.
+ ///
+ public SimplePathResult GetPath(SimplePathArgs args)
+ {
+ var cameFrom = new Dictionary();
+ var costSoFar = new Dictionary();
+ var frontier = new PriorityQueue();
+
+ 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 ReconstructPath(Vector2i end, Dictionary cameFrom)
+ {
+ var path = new List()
+ {
+ 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
index 00000000000000..9979755f995e0d
--- /dev/null
+++ b/Content.Server/NPC/Pathfinding/PathfindingSystem.Splines.cs
@@ -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 Path;
+ }
+
+ public record struct SplinePathResult()
+ {
+ public static SplinePathResult NoPath = new();
+
+ public List Points = new();
+
+ public List Path = new();
+ public Dictionary CameFrom;
+ }
+
+ public record struct SplinePathArgs(SimplePathArgs Args)
+ {
+ public SimplePathArgs Args = Args;
+
+ public float MaxRatio = 0.25f;
+
+ ///
+ /// Minimum distance between subdivisions.
+ ///
+ public int Distance = 20;
+ }
+
+ ///
+ /// Gets a spline path from start to end.
+ ///
+ public SplinePathResult GetSplinePath(SplinePathArgs args, Random random)
+ {
+ var start = args.Args.Start;
+ var end = args.Args.End;
+
+ var path = new List();
+
+ 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(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();
+
+ // TODO: Need to get rid of the branch bullshit.
+ var points = new List();
+
+ 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,
+ };
+ }
+
+ ///
+ /// Does a simpler pathfinder over the nodes to prune unnecessary branches.
+ ///
+ public List SimplifyPath(SimplifyPathArgs args)
+ {
+ var nodes = new HashSet(args.Path);
+
+ var result = GetBreadthPath(new BreadthPathArgs()
+ {
+ Start = args.Start,
+ Ends = new List()
+ {
+ 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
index 00000000000000..f7bcd019f5fbd0
--- /dev/null
+++ b/Content.Server/NPC/Pathfinding/PathfindingSystem.Widen.cs
@@ -0,0 +1,89 @@
+using System.Numerics;
+using Robust.Shared.Random;
+
+namespace Content.Server.NPC.Pathfinding;
+
+public sealed partial class PathfindingSystem
+{
+ ///
+ /// Widens the path by the specified amount.
+ ///
+ public HashSet GetWiden(WidenArgs args, Random random)
+ {
+ var tiles = new HashSet(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;
+
+ ///
+ /// How many tiles to skip between iterations., 1-in-n
+ ///
+ public int TileSkip = 3;
+
+ ///
+ /// Maximum amount to vary per tile.
+ ///
+ public float Variance = 0.25f;
+
+ ///
+ /// Minimum width.
+ ///
+ public float MinWiden = 2f;
+
+
+ public float MaxWiden = 7f;
+
+ public List Path;
+ }
+}
diff --git a/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs b/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs
index 5f871a6ecfa0b3..e0bcb97a112c2c 100644
--- a/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs
+++ b/Content.Server/NPC/Systems/NPCSteeringSystem.Context.cs
@@ -142,6 +142,13 @@ private bool TrySeek(
// 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 @@ private bool TrySeek(
// 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
index cb9e64f04e256c..00000000000000
--- a/Content.Server/Procedural/DungeonJob.PostGen.cs
+++ /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 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();
- var allTiles = new HashSet(dungeon.CorridorTiles);
- allTiles.UnionWith(dungeon.RoomTiles);
- allTiles.UnionWith(dungeon.RoomExteriorTiles);
- allTiles.UnionWith(dungeon.CorridorExteriorTiles);
- var nodeQuery = _entManager.GetEntityQuery();
-
- // 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(cableTiles);
- random.Shuffle(startNodes);
- var start = startNodes[0];
- var remaining = new HashSet(startNodes);
- var frontier = new PriorityQueue();
- frontier.Enqueue(start, 0f);
- var cameFrom = new Dictionary();
- var costSoFar = new Dictionary();
- var lastDirection = new Dictionary();
- 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();
-
- 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(4);
- var pocketDirections = new ValueList(4);
- var doorQuery = _entManager.GetEntityQuery();
- var physicsQuery = _entManager.GetEntityQuery();
- 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(dungeon.Rooms);
- var roomTiles = new List();
- 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(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();
-
- // 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();
-
- 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();
- }
- }
- }
-
- ///
- /// Simply places tiles / entities on the entrances to rooms.
- ///
- 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);
- }
- }
- }
-
- ///
- /// Generates corridor connections between entrances to all the rooms.
- ///
- private async Task PostGen(CorridorPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random)
- {
- var entrances = new List(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();
-
- 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(dungeon.RoomExteriorTiles);
- excludedTiles.UnionWith(dungeon.RoomTiles);
- var corridorTiles = new HashSet();
-
- _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 corridorTiles)
- {
- var expansion = width - 2;
-
- // Widen the path
- if (expansion >= 1)
- {
- var toAdd = new ValueList();
-
- 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(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>(dungeon.Rooms.Count);
-
- foreach (var room in dungeon.Rooms)
- {
- var roomEdges = new HashSet();
-
- 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>();
- var frontier = new Queue();
- 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(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;
- }
- }
- }
-
- ///
- /// Removes any unwanted obstacles around a door tile.
- ///
- 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();
-
- 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();
- var allExterior = new HashSet(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
index 4d3f573f4d41b0..00000000000000
--- a/Content.Server/Procedural/DungeonJob.PostGenBiome.cs
+++ /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(gridUid);
- var biomeSystem = _entManager.System();
- biomeSystem.SetTemplate(gridUid, biomeComp, _prototype.Index(postGen.BiomeTemplate));
- var seed = random.Next();
- var xformQuery = _entManager.GetEntityQuery();
-
- 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();
- var weightedRandom = _prototype.Index(postGen.MarkerTemplate);
- var xformQuery = _entManager.GetEntityQuery();
- var templates = new Dictionary();
-
- 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(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
index bf2822ff423745..00000000000000
--- a/Content.Server/Procedural/DungeonJob.cs
+++ /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
-{
- 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();
- _transform = transform;
-
- _gen = gen;
- _grid = grid;
- _gridUid = gridUid;
- _seed = seed;
- _position = position;
- }
-
- protected override async Task 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().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
index 00000000000000..acffd057fad0db
--- /dev/null
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.DunGenExterior.cs
@@ -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
+{
+ ///
+ ///
+ ///
+ private async Task> GenerateExteriorDungen(Vector2i position, ExteriorDunGen dungen, HashSet 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();
+
+ // 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.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
index 00000000000000..5a0d77c6151feb
--- /dev/null
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.DunGenFill.cs
@@ -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
+{
+ ///
+ ///
+ ///
+ private async Task GenerateFillDunGen(DungeonData data, HashSet reservedTiles)
+ {
+ if (!data.Entities.TryGetValue(DungeonDataKey.Fill, out var fillEnt))
+ {
+ LogDataError(typeof(FillGridDunGen));
+ return Dungeon.Empty;
+ }
+
+ var roomTiles = new HashSet();
+ 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());
+ dungeon.AddRoom(room);
+
+ return dungeon;
+ }
+}
diff --git a/Content.Server/Procedural/DungeonJob.NoiseDunGen.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.DunGenNoise.cs
similarity index 73%
rename from Content.Server/Procedural/DungeonJob.NoiseDunGen.cs
rename to Content.Server/Procedural/DungeonJob/DungeonJob.DunGenNoise.cs
index 73c3386ead548d..b2526ec17d1b53 100644
--- a/Content.Server/Procedural/DungeonJob.NoiseDunGen.cs
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.DunGenNoise.cs
@@ -4,19 +4,25 @@
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 GenerateNoiseDungeon(NoiseDunGen dungen, EntityUid gridUid, MapGridComponent grid,
- int seed)
+ ///
+ ///
+ ///
+ private async Task GenerateNoiseDunGen(
+ Vector2i position,
+ NoiseDunGen dungen,
+ HashSet 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 @@ private async Task GenerateNoiseDungeon(NoiseDunGen dungen, EntityUid g
var frontier = new Queue();
var rooms = new List();
var tileCount = 0;
- var tileCap = rand.NextGaussian(dungen.TileCap, dungen.CapStd);
+ var tileCap = random.NextGaussian(dungen.TileCap, dungen.CapStd);
var visited = new HashSet();
while (iterations > 0 && tileCount < tileCap)
@@ -39,22 +45,22 @@ private async Task GenerateNoiseDungeon(NoiseDunGen dungen, EntityUid g
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 @@ private async Task GenerateNoiseDungeon(NoiseDunGen dungen, EntityUid g
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 @@ private async Task GenerateNoiseDungeon(NoiseDunGen dungen, EntityUid g
foreach (var tile in roomTiles)
{
- center += tile + grid.TileSizeHalfVector;
+ center += tile + _grid.TileSizeHalfVector;
}
center /= roomTiles.Count;
@@ -132,15 +144,8 @@ private async Task GenerateNoiseDungeon(NoiseDunGen dungen, EntityUid g
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
index 00000000000000..f1808ec90cd2c6
--- /dev/null
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.DunGenNoiseDistance.cs
@@ -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
+ */
+
+ ///
+ ///
+ ///
+ private async Task GenerateNoiseDistanceDunGen(
+ Vector2i position,
+ NoiseDistanceDunGen dungen,
+ HashSet 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();
+ 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());
+
+ _maps.SetTiles(_gridUid, _grid, tiles);
+ var dungeon = new Dungeon(new List()
+ {
+ 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();
+ }
+ }
+}
diff --git a/Content.Server/Procedural/DungeonJob.PrefabDunGen.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.DunGenPrefab.cs
similarity index 82%
rename from Content.Server/Procedural/DungeonJob.PrefabDunGen.cs
rename to Content.Server/Procedural/DungeonJob/DungeonJob.DunGenPrefab.cs
index a19f7e4701d9f9..33bbeba4b53c70 100644
--- a/Content.Server/Procedural/DungeonJob.PrefabDunGen.cs
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.DunGenPrefab.cs
@@ -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 GeneratePrefabDungeon(PrefabDunGen prefab, EntityUid gridUid, MapGridComponent grid, int seed)
+ ///
+ ///
+ ///
+ private async Task GeneratePrefabDunGen(Vector2i position, DungeonData data, PrefabDunGen prefab, HashSet 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(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>();
foreach (var pack in _prototype.EnumeratePrototypes())
@@ -42,12 +50,15 @@ private async Task GeneratePrefabDungeon(PrefabDunGen prefab, EntityUid
{
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 @@ private async Task GeneratePrefabDungeon(PrefabDunGen prefab, EntityUid
{
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 @@ private async Task GeneratePrefabDungeon(PrefabDunGen prefab, EntityUid
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(room.Size.X * room.Size.Y);
var exterior = new HashSet(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 @@ private async Task GeneratePrefabDungeon(PrefabDunGen prefab, EntityUid
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 @@ private async Task GeneratePrefabDungeon(PrefabDunGen prefab, EntityUid
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 reservedTiles, Random random)
{
// TODO: Move to dungeonsystem.
@@ -323,8 +340,10 @@ private void SetDungeonEntrance(Dungeon dungeon, DungeonRoom room, Random random
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
index 00000000000000..6b36d101095c4c
--- /dev/null
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.DunGenReplaceTile.cs
@@ -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
+{
+ ///
+ ///
+ ///
+ private async Task GenerateTileReplacementDunGen(ReplaceTileDunGen gen, DungeonData data, HashSet reservedTiles, Random random)
+ {
+ var tiles = _maps.GetAllTilesEnumerator(_gridUid, _grid);
+ var replacements = new List<(Vector2i Index, Tile Tile)>();
+ var reserved = new HashSet();
+
+ 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()
+ {
+ new DungeonRoom(reserved, _position, Box2i.Empty, new HashSet()),
+ });
+ }
+}
diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.MobDunGen.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.MobDunGen.cs
new file mode 100644
index 00000000000000..150849d2c51919
--- /dev/null
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.MobDunGen.cs
@@ -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();
+ availableRooms.AddRange(dungeon.Rooms);
+ var availableTiles = new ValueList(dungeon.AllTiles);
+
+ var entities = EntitySpawnCollection.GetSpawns(gen.Groups, random);
+ var count = random.Next(gen.MinCount, gen.MaxCount + 1);
+ var npcs = _entManager.System();
+
+ 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(uid);
+ _entManager.RemoveComponent(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
index 00000000000000..e89c1d7e470bc7
--- /dev/null
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.OreDunGen.cs
@@ -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
+{
+ ///
+ ///
+ ///
+ 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();
+ var availableTiles = new List();
+
+ 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(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();
+
+ // 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(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(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
index 00000000000000..b1c83346d87280
--- /dev/null
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGen.cs
@@ -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 corridorTiles)
+ {
+ var expansion = width - 2;
+
+ // Widen the path
+ if (expansion >= 1)
+ {
+ var toAdd = new ValueList();
+
+ 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);
+ }
+ }
+ }
+
+ ///
+ /// Removes any unwanted obstacles around a door tile.
+ ///
+ 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
index 00000000000000..aaea23ddd56694
--- /dev/null
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenAutoCabling.cs
@@ -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
+{
+ ///
+ ///
+ ///
+ private async Task PostGen(AutoCablingDunGen gen, DungeonData data, Dungeon dungeon, HashSet 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();
+ var allTiles = new HashSet(dungeon.CorridorTiles);
+ allTiles.UnionWith(dungeon.RoomTiles);
+ allTiles.UnionWith(dungeon.RoomExteriorTiles);
+ allTiles.UnionWith(dungeon.CorridorExteriorTiles);
+ var nodeQuery = _entManager.GetEntityQuery();
+
+ // 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(cableTiles);
+ random.Shuffle(startNodes);
+ var start = startNodes[0];
+ var remaining = new HashSet(startNodes);
+ var frontier = new PriorityQueue();
+ frontier.Enqueue(start, 0f);
+ var cameFrom = new Dictionary();
+ var costSoFar = new Dictionary();
+ var lastDirection = new Dictionary();
+ 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
index 00000000000000..65f6d2d14f9506
--- /dev/null
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenBiome.cs
@@ -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
+{
+ ///
+ ///
+ ///
+ private async Task PostGen(BiomeDunGen dunGen, DungeonData data, Dungeon dungeon, HashSet reservedTiles, Random random)
+ {
+ if (_entManager.TryGetComponent(_gridUid, out BiomeComponent? biomeComp))
+ return;
+
+ biomeComp = _entManager.AddComponent(_gridUid);
+ var biomeSystem = _entManager.System();
+ biomeSystem.SetTemplate(_gridUid, biomeComp, _prototype.Index(dunGen.BiomeTemplate));
+ var seed = random.Next();
+ var xformQuery = _entManager.GetEntityQuery();
+
+ 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
index 00000000000000..fb0eaa01573e51
--- /dev/null
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenBiomeMarkerLayer.cs
@@ -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
+{
+ ///
+ ///
+ ///
+ private async Task PostGen(BiomeMarkerLayerDunGen dunGen, DungeonData data, Dungeon dungeon, HashSet 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();
+ var weightedRandom = _prototype.Index(dunGen.MarkerTemplate);
+ var xformQuery = _entManager.GetEntityQuery();
+ var templates = new Dictionary();
+
+ 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(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
index 00000000000000..84697a56bc7c63
--- /dev/null
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenBoundaryWall.cs
@@ -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
+{
+ ///
+ ///
+ ///
+ private async Task PostGen(BoundaryWallDunGen gen, DungeonData data, Dungeon dungeon, HashSet 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
index 00000000000000..f7858298504ba9
--- /dev/null
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenCornerClutter.cs
@@ -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
+{
+ ///
+ ///
+ ///
+ private async Task PostGen(CornerClutterDunGen gen, DungeonData data, Dungeon dungeon, HashSet 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
index 00000000000000..8ea79ffe54fd6e
--- /dev/null
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenCorridor.cs
@@ -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
+{
+ ///
+ ///
+ ///
+ private async Task PostGen(CorridorDunGen gen, DungeonData data, Dungeon dungeon, HashSet reservedTiles, Random random)
+ {
+ if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto))
+ {
+ LogDataError(typeof(CorridorDunGen));
+ return;
+ }
+
+ var entrances = new List(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();
+
+ 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(dungeon.RoomExteriorTiles);
+ excludedTiles.UnionWith(dungeon.RoomTiles);
+ var corridorTiles = new HashSet();
+
+ _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);
+ }
+}
diff --git a/Content.Server/Procedural/DungeonJob.CorridorClutterPost.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenCorridorClutter.cs
similarity index 83%
rename from Content.Server/Procedural/DungeonJob.CorridorClutterPost.cs
rename to Content.Server/Procedural/DungeonJob/DungeonJob.PostGenCorridorClutter.cs
index 8099157cc5080a..cb7c4b210c86ee 100644
--- a/Content.Server/Procedural/DungeonJob.CorridorClutterPost.cs
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenCorridorClutter.cs
@@ -2,16 +2,17 @@
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)
+ ///
+ ///
+ ///
+ private async Task PostGen(CorridorClutterDunGen gen, DungeonData data, Dungeon dungeon, HashSet reservedTiles, Random random)
{
var physicsQuery = _entManager.GetEntityQuery();
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
index 00000000000000..3b516c3fa8a635
--- /dev/null
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenCorridorDecalSkirting.cs
@@ -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
+{
+ ///
+ ///
+ ///
+ private async Task PostGen(CorridorDecalSkirtingDunGen decks, DungeonData data, Dungeon dungeon, HashSet reservedTiles, Random random)
+ {
+ if (!data.Colors.TryGetValue(DungeonDataKey.Decals, out var color))
+ {
+ _sawmill.Error(Environment.StackTrace);
+ }
+
+ var directions = new ValueList(4);
+ var pocketDirections = new ValueList(4);
+ var doorQuery = _entManager.GetEntityQuery();
+ var physicsQuery = _entManager.GetEntityQuery();
+ 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
index 00000000000000..917b1ffc9caf01
--- /dev/null
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenDungeonConnector.cs
@@ -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
index 00000000000000..abc52f07c6c975
--- /dev/null
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenDungeonEntrance.cs
@@ -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
+{
+ ///
+ ///
+ ///
+ private async Task PostGen(DungeonEntranceDunGen gen, DungeonData data, Dungeon dungeon, HashSet 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(dungeon.Rooms);
+ var roomTiles = new List();
+ 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
index 00000000000000..3a1c7a37793823
--- /dev/null
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenEntranceFlank.cs
@@ -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
+{
+ ///
+ ///
+ ///
+ private async Task PostGen(EntranceFlankDunGen gen, DungeonData data, Dungeon dungeon, HashSet 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(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
index 00000000000000..9a1b44ec91be85
--- /dev/null
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenExternalWindow.cs
@@ -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.
+
+ ///
+ ///
+ ///
+ private async Task PostGen(ExternalWindowDunGen gen, DungeonData data, Dungeon dungeon, HashSet 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(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();
+
+ // 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
index 00000000000000..d3b8c6d2f5dd0c
--- /dev/null
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenInternalWindow.cs
@@ -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
+{
+ ///
+ ///
+ ///
+ private async Task PostGen(InternalWindowDunGen gen, DungeonData data, Dungeon dungeon, HashSet 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();
+
+ 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
index 00000000000000..700406eb894d92
--- /dev/null
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenJunction.cs
@@ -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
+{
+ ///
+ ///
+ ///
+ private async Task PostGen(JunctionDunGen gen, DungeonData data, Dungeon dungeon, HashSet 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
index 00000000000000..15d0f6342327b2
--- /dev/null
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenMiddleConnection.cs
@@ -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
+{
+ ///
+ ///
+ ///
+ private async Task PostGen(MiddleConnectionDunGen gen, DungeonData data, Dungeon dungeon, HashSet 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>(dungeon.Rooms.Count);
+
+ foreach (var room in dungeon.Rooms)
+ {
+ var roomEdges = new HashSet();
+
+ 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>();
+ 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(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
index 00000000000000..09d223e86cf5f2
--- /dev/null
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenRoomEntrance.cs
@@ -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
+{
+ ///
+ ///
+ ///
+ private async Task PostGen(RoomEntranceDunGen gen, DungeonData data, Dungeon dungeon, HashSet 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
index 00000000000000..8fe2f36665f954
--- /dev/null
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenSplineDungeonConnector.cs
@@ -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
+{
+ ///
+ ///
+ ///
+ private async Task PostGen(
+ SplineDungeonConnectorDunGen gen,
+ DungeonData data,
+ List dungeons,
+ HashSet 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();
+
+ 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();
+ var allTiles = new HashSet();
+ 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());
+ 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
index 00000000000000..afc7608d64af81
--- /dev/null
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenWallMount.cs
@@ -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
+{
+ ///
+ ///
+ ///
+ private async Task PostGen(WallMountDunGen gen, DungeonData data, Dungeon dungeon, HashSet 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();
+ var allExterior = new HashSet(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;
+ }
+ }
+ }
+}
diff --git a/Content.Server/Procedural/DungeonJob.WormPost.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenWorm.cs
similarity index 88%
rename from Content.Server/Procedural/DungeonJob.WormPost.cs
rename to Content.Server/Procedural/DungeonJob/DungeonJob.PostGenWorm.cs
index 5d2271cae6599b..6fd00e54824853 100644
--- a/Content.Server/Procedural/DungeonJob.WormPost.cs
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.PostGenWorm.cs
@@ -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
{
///
- /// Tries to connect rooms via worm-like corridors.
+ ///
///
- 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 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 Network)>();
// List of places to start from.
@@ -32,7 +36,7 @@ private async Task PostGen(WormCorridorPostGen gen, Dungeon dungeon, EntityUid g
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 @@ private async Task PostGen(WormCorridorPostGen gen, Dungeon dungeon, EntityUid g
// 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 @@ private async Task PostGen(WormCorridorPostGen gen, Dungeon dungeon, EntityUid g
costSoFar[startNode] = 0f;
var count = 0;
- await SuspendIfOutOfTime();
+ await SuspendDungeon();
if (!ValidateResume())
return;
@@ -174,9 +178,9 @@ private async Task PostGen(WormCorridorPostGen gen, Dungeon dungeon, EntityUid g
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
index 00000000000000..1468a80902cbc7
--- /dev/null
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.cs
@@ -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>
+{
+ 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 _physicsQuery;
+ private EntityQuery _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();
+ _maps = _entManager.System();
+ _transform = transform;
+
+ _physicsQuery = _entManager.GetEntityQuery();
+ _xformQuery = _entManager.GetEntityQuery();
+
+ _gen = gen;
+ _grid = grid;
+ _gridUid = gridUid;
+ _seed = seed;
+ _position = position;
+ }
+
+ ///
+ /// Gets the relevant dungeon, running recursively as relevant.
+ ///
+ /// Should we reserve tiles even if the config doesn't specify.
+ private async Task> GetDungeons(
+ Vector2i position,
+ DungeonConfigPrototype config,
+ DungeonData data,
+ List layers,
+ HashSet reservedTiles,
+ int seed,
+ Random random)
+ {
+ var dungeons = new List();
+ 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();
+ }
+ }
+
+ return dungeons;
+ }
+
+ protected override async Task?> 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();
+
+ 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().CheckSplits(_gridUid);
+ var npcSystem = _entManager.System();
+ var npcs = new HashSet>();
+
+ _lookup.GetChildEntities(_gridUid, npcs);
+
+ foreach (var npc in npcs)
+ {
+ npcSystem.WakeNPC(npc.Owner, npc.Comp);
+ }
+
+ return dungeons;
+ }
+
+ private async Task RunLayer(
+ List dungeons,
+ DungeonData data,
+ Vector2i position,
+ IDunGenLayer layer,
+ HashSet 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;
+ }
+
+ ///
+ /// Wrapper around
+ ///
+ private async Task SuspendDungeon()
+ {
+ if (!TimeSlice)
+ return;
+
+ await SuspendIfOutOfTime();
+ }
+}
diff --git a/Content.Server/Procedural/DungeonSystem.Commands.cs b/Content.Server/Procedural/DungeonSystem.Commands.cs
index d783eb60c63e85..51a6a57bbebe4a 100644
--- a/Content.Server/Procedural/DungeonSystem.Commands.cs
+++ b/Content.Server/Procedural/DungeonSystem.Commands.cs
@@ -51,6 +51,8 @@ private async void GenerateDungeon(IConsoleShell shell, string argstr, string[]
dungeonUid = EntityManager.CreateEntityUninitialized(null, new EntityCoordinates(dungeonUid, position));
dungeonGrid = EntityManager.AddComponent(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;
diff --git a/Content.Server/Procedural/DungeonSystem.Rooms.cs b/Content.Server/Procedural/DungeonSystem.Rooms.cs
index ddd4a4732f8e2d..8a1606c4889139 100644
--- a/Content.Server/Procedural/DungeonSystem.Rooms.cs
+++ b/Content.Server/Procedural/DungeonSystem.Rooms.cs
@@ -64,6 +64,7 @@ public void SpawnRoom(
Vector2i origin,
DungeonRoomPrototype room,
Random random,
+ HashSet? reservedTiles,
bool clearExisting = false,
bool rotation = false)
{
@@ -78,7 +79,7 @@ public void SpawnRoom(
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 void SpawnRoom(
MapGridComponent grid,
Matrix3x2 roomTransform,
DungeonRoomPrototype room,
+ HashSet? reservedTiles = null,
bool clearExisting = false)
{
// Ensure the underlying template exists.
@@ -150,6 +152,10 @@ public void SpawnRoom(
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 void SpawnRoom(
{
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 void SpawnRoom(
// 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();
diff --git a/Content.Server/Procedural/DungeonSystem.cs b/Content.Server/Procedural/DungeonSystem.cs
index 36009896a2cb59..b73e843fffdbde 100644
--- a/Content.Server/Procedural/DungeonSystem.cs
+++ b/Content.Server/Procedural/DungeonSystem.cs
@@ -12,6 +12,7 @@
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 _dungeonJobs = new();
+ private readonly Dictionary _dungeonJobs = new();
[ValidatePrototypeId]
public const string FallbackTileId = "FloorSteel";
@@ -190,18 +191,16 @@ public void GenerateDungeon(DungeonConfigPrototype gen,
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 void GenerateDungeon(DungeonConfigPrototype gen,
_dungeonJobQueue.EnqueueJob(job);
}
- public async Task GenerateDungeonAsync(
+ public async Task> GenerateDungeonAsync(
DungeonConfigPrototype gen,
EntityUid gridUid,
MapGridComponent grid,
@@ -223,18 +222,16 @@ public async Task GenerateDungeonAsync(
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,
diff --git a/Content.Server/Procedural/RoomFillSystem.cs b/Content.Server/Procedural/RoomFillSystem.cs
index 20ffa98586d901..b539cc9780ef21 100644
--- a/Content.Server/Procedural/RoomFillSystem.cs
+++ b/Content.Server/Procedural/RoomFillSystem.cs
@@ -35,6 +35,7 @@ private void OnRoomFillMapInit(EntityUid uid, RoomFillComponent component, MapIn
_maps.LocalToTile(xform.GridUid.Value, mapGrid, xform.Coordinates),
room,
random,
+ null,
clearExisting: component.ClearExisting,
rotation: component.Rotation);
}
diff --git a/Content.Server/Salvage/SpawnSalvageMissionJob.cs b/Content.Server/Salvage/SpawnSalvageMissionJob.cs
index ce844e57a13e97..e9318792b726de 100644
--- a/Content.Server/Salvage/SpawnSalvageMissionJob.cs
+++ b/Content.Server/Salvage/SpawnSalvageMissionJob.cs
@@ -176,9 +176,11 @@ protected override async Task Process()
dungeonOffset = dungeonRotation.RotateVec(dungeonOffset);
var dungeonMod = _prototypeManager.Index(mission.Dungeon);
var dungeonConfig = _prototypeManager.Index(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)
{
diff --git a/Content.Server/Shuttles/Components/GridSpawnComponent.cs b/Content.Server/Shuttles/Components/GridSpawnComponent.cs
index 5f0fa7dd62447e..d8144354b8e7e8 100644
--- a/Content.Server/Shuttles/Components/GridSpawnComponent.cs
+++ b/Content.Server/Shuttles/Components/GridSpawnComponent.cs
@@ -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.
///
- [DataField(required: true)] public Dictionary Groups = new();
+ [DataField(required: true)] public Dictionary Groups = new();
}
-[DataRecord]
-public record struct GridSpawnGroup
+public interface IGridSpawnGroup
{
- public List Paths = new();
- public int MinCount = 1;
- public int MaxCount = 1;
+ ///
+ /// Minimum distance to spawn away from the station.
+ ///
+ public float MinimumDistance { get; }
+
+ ///
+ public ProtoId? NameDataset { get; }
+
+ ///
+ int MinCount { get; set; }
+
+ ///
+ int MaxCount { get; set; }
///
/// Components to be added to any spawned grids.
///
- public ComponentRegistry AddComponents = new();
+ public ComponentRegistry AddComponents { get; set; }
///
/// Hide the IFF label of the grid.
///
- public bool Hide = false;
+ public bool Hide { get; set; }
///
/// Should we set the metadata name of a grid. Useful for admin purposes.
///
- public bool NameGrid = false;
+ public bool NameGrid { get; set; }
///
/// Should we add this to the station's grids (if possible / relevant).
///
- public bool StationGrid = true;
+ public bool StationGrid { get; set; }
+}
+
+[DataRecord]
+public sealed class DungeonSpawnGroup : IGridSpawnGroup
+{
+ ///
+ /// Prototypes we can choose from to spawn.
+ ///
+ public List> Protos = new();
+
+ ///
+ public float MinimumDistance { get; }
+
+ ///
+ public ProtoId? 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; } = false;
+
+ ///
+ public bool StationGrid { get; set; } = false;
+}
+
+[DataRecord]
+public sealed class GridSpawnGroup : IGridSpawnGroup
+{
+ public List Paths = new();
- public GridSpawnGroup()
- {
- }
+ public float MinimumDistance { get; }
+ public ProtoId? 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;
}
diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.GridFill.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.GridFill.cs
index 853548add37c6a..b4fcccd80557c5 100644
--- a/Content.Server/Shuttles/Systems/ShuttleSystem.GridFill.cs
+++ b/Content.Server/Shuttles/Systems/ShuttleSystem.GridFill.cs
@@ -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 @@ private void CargoSpawn(EntityUid uid, StationCargoShuttleComponent component)
_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();
+
+ // 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(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 @@ private void GridSpawns(EntityUid uid, GridSpawnComponent component)
// 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();
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(ent[0], out var shuttle))
- {
- TryFTLProximity(ent[0], targetGrid.Value);
- }
- else
- {
- valid = false;
- }
-
- if (group.Hide)
- {
- var iffComp = EnsureComp(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(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);
}
}
diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.cs
index b8f216db7370a0..85703389e9dec7 100644
--- a/Content.Server/Shuttles/Systems/ShuttleSystem.cs
+++ b/Content.Server/Shuttles/Systems/ShuttleSystem.cs
@@ -2,6 +2,7 @@
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.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
index 00000000000000..3d7199743af7e2
--- /dev/null
+++ b/Content.Shared/Procedural/Components/EntityRemapComponent.cs
@@ -0,0 +1,13 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Procedural.Components;
+
+///
+/// Indicates this entity prototype should be re-mapped to another
+///
+[RegisterComponent]
+public sealed partial class EntityRemapComponent : Component
+{
+ [DataField(required: true)]
+ public Dictionary Mask = new();
+}
diff --git a/Content.Shared/Procedural/Distance/DunGenEuclideanSquaredDistance.cs b/Content.Shared/Procedural/Distance/DunGenEuclideanSquaredDistance.cs
new file mode 100644
index 00000000000000..617304729e21e0
--- /dev/null
+++ b/Content.Shared/Procedural/Distance/DunGenEuclideanSquaredDistance.cs
@@ -0,0 +1,10 @@
+namespace Content.Shared.Procedural.Distance;
+
+///
+/// Produces a rounder shape useful for more natural areas.
+///
+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
index 00000000000000..48b0c4bcb7ee60
--- /dev/null
+++ b/Content.Shared/Procedural/Distance/DunGenSquareBump.cs
@@ -0,0 +1,10 @@
+namespace Content.Shared.Procedural.Distance;
+
+///
+/// Produces a squarish-shape that's better for filling in most of the area.
+///
+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
index 00000000000000..b1071a14e349fe
--- /dev/null
+++ b/Content.Shared/Procedural/Distance/IDunGenDistance.cs
@@ -0,0 +1,14 @@
+namespace Content.Shared.Procedural.Distance;
+
+///
+/// Used if you want to limit the distance noise is generated by some arbitrary config
+///
+[ImplicitDataDefinitionForInheritors]
+public partial interface IDunGenDistance
+{
+ ///
+ /// How much to blend between the original noise value and the adjusted one.
+ ///
+ float BlendWeight { get; }
+}
+
diff --git a/Content.Shared/Procedural/Dungeon.cs b/Content.Shared/Procedural/Dungeon.cs
index aecfef2c782df1..0d290b67905e8b 100644
--- a/Content.Shared/Procedural/Dungeon.cs
+++ b/Content.Shared/Procedural/Dungeon.cs
@@ -1,8 +1,16 @@
namespace Content.Shared.Procedural;
+///
+/// Procedurally generated dungeon data.
+///
public sealed class Dungeon
{
- public readonly List Rooms;
+ public static Dungeon Empty = new Dungeon();
+
+ private List _rooms;
+ private HashSet _allTiles = new();
+
+ public IReadOnlyList Rooms => _rooms;
///
/// Hashset of the tiles across all rooms.
@@ -17,18 +25,64 @@ public sealed class Dungeon
public readonly HashSet Entrances = new();
- public Dungeon()
+ public IReadOnlySet AllTiles => _allTiles;
+
+ public Dungeon() : this(new List())
{
- Rooms = new List();
}
public Dungeon(List 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();
}
}
diff --git a/Content.Shared/Procedural/DungeonConfigPrototype.cs b/Content.Shared/Procedural/DungeonConfigPrototype.cs
index 07a7000d63748b..d0d8e0ff12d776 100644
--- a/Content.Shared/Procedural/DungeonConfigPrototype.cs
+++ b/Content.Shared/Procedural/DungeonConfigPrototype.cs
@@ -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!;
+ ///
+ ///
+ ///
+ [DataField]
+ public DungeonData Data = DungeonData.Empty;
+
+ ///
+ /// The secret sauce, procedural generation layers that get run.
+ ///
+ [DataField(required: true)]
+ public List Layers = new();
+
+ ///
+ /// Should we reserve the tiles generated by this config so no other dungeons can spawn on it within the same job?
+ ///
+ [DataField]
+ public bool ReserveTiles;
+
+ ///
+ /// Minimum times to run the config.
+ ///
+ [DataField]
+ public int MinCount = 1;
+
+ ///
+ /// Maximum times to run the config.
+ ///
+ [DataField]
+ public int MaxCount = 1;
+
+ ///
+ /// Minimum amount we can offset the dungeon by.
+ ///
+ [DataField]
+ public int MinOffset;
///
- /// Ran after the main dungeon is created.
+ /// Maximum amount we can offset the dungeon by.
///
- [DataField("postGeneration")]
- public List PostGeneration = new();
+ [DataField]
+ public int MaxOffset;
}
diff --git a/Content.Shared/Procedural/DungeonData.cs b/Content.Shared/Procedural/DungeonData.cs
new file mode 100644
index 00000000000000..58ec966786146c
--- /dev/null
+++ b/Content.Shared/Procedural/DungeonData.cs
@@ -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;
+
+///
+/// Used to set dungeon values for all layers.
+///
+///
+/// This lets us share data between different dungeon configs without having to repeat entire configs.
+///
+[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 Colors = new();
+ public Dictionary Entities = new();
+ public Dictionary> SpawnGroups = new();
+ public Dictionary> Tiles = new();
+ public Dictionary