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 Whitelists = new(); + + /// + /// Applies the specified data to this data. + /// + public void Apply(DungeonData data) + { + // Copy-paste moment. + foreach (var color in data.Colors) + { + Colors[color.Key] = color.Value; + } + + foreach (var color in data.Entities) + { + Entities[color.Key] = color.Value; + } + + foreach (var color in data.SpawnGroups) + { + SpawnGroups[color.Key] = color.Value; + } + + foreach (var color in data.Tiles) + { + Tiles[color.Key] = color.Value; + } + + foreach (var color in data.Whitelists) + { + Whitelists[color.Key] = color.Value; + } + } + + public DungeonData Clone() + { + return new DungeonData + { + // Only shallow clones but won't matter for DungeonJob purposes. + Colors = Colors.ShallowClone(), + Entities = Entities.ShallowClone(), + SpawnGroups = SpawnGroups.ShallowClone(), + Tiles = Tiles.ShallowClone(), + Whitelists = Whitelists.ShallowClone(), + }; + } +} + +public enum DungeonDataKey : byte +{ + // Colors + Decals, + + // Entities + Cabling, + CornerWalls, + Fill, + Junction, + Walls, + + // SpawnGroups + CornerClutter, + Entrance, + EntranceFlank, + WallMounts, + Window, + + // Tiles + FallbackTile, + WidenTile, + + // Whitelists + Rooms, +} diff --git a/Content.Shared/Procedural/DungeonGenerators/ExteriorDunGen.cs b/Content.Shared/Procedural/DungeonGenerators/ExteriorDunGen.cs new file mode 100644 index 00000000000000..e9a5181f8d07ab --- /dev/null +++ b/Content.Shared/Procedural/DungeonGenerators/ExteriorDunGen.cs @@ -0,0 +1,13 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.Procedural.DungeonGenerators; + +/// +/// Generates the specified config on an exterior tile of the attached dungeon. +/// Useful if you're using or otherwise want a dungeon on the outside of a grid. +/// +public sealed partial class ExteriorDunGen : IDunGenLayer +{ + [DataField(required: true)] + public ProtoId Proto; +} diff --git a/Content.Shared/Procedural/DungeonGenerators/FillGridDunGen.cs b/Content.Shared/Procedural/DungeonGenerators/FillGridDunGen.cs new file mode 100644 index 00000000000000..368ec5cc3e4552 --- /dev/null +++ b/Content.Shared/Procedural/DungeonGenerators/FillGridDunGen.cs @@ -0,0 +1,10 @@ +namespace Content.Shared.Procedural.DungeonGenerators; + +/// +/// Fills unreserved tiles with the specified entity prototype. +/// +/// +/// DungeonData keys are: +/// - Fill +/// +public sealed partial class FillGridDunGen : IDunGenLayer; diff --git a/Content.Shared/Procedural/DungeonGenerators/IDunGen.cs b/Content.Shared/Procedural/DungeonGenerators/IDunGen.cs deleted file mode 100644 index 5aa82f1596f7a1..00000000000000 --- a/Content.Shared/Procedural/DungeonGenerators/IDunGen.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Content.Shared.Procedural.DungeonGenerators; - -[ImplicitDataDefinitionForInheritors] -public partial interface IDunGen -{ - -} diff --git a/Content.Shared/Procedural/DungeonGenerators/NoiseDistanceDunGen.cs b/Content.Shared/Procedural/DungeonGenerators/NoiseDistanceDunGen.cs new file mode 100644 index 00000000000000..0dfb3daef8474d --- /dev/null +++ b/Content.Shared/Procedural/DungeonGenerators/NoiseDistanceDunGen.cs @@ -0,0 +1,18 @@ +using Content.Shared.Procedural.Distance; + +namespace Content.Shared.Procedural.DungeonGenerators; + +/// +/// Like except with maximum dimensions +/// +public sealed partial class NoiseDistanceDunGen : IDunGenLayer +{ + [DataField] + public IDunGenDistance? DistanceConfig; + + [DataField] + public Vector2i Size; + + [DataField(required: true)] + public List Layers = new(); +} diff --git a/Content.Shared/Procedural/DungeonGenerators/NoiseDunGen.cs b/Content.Shared/Procedural/DungeonGenerators/NoiseDunGen.cs index 3ea0d989a2a2c0..56d63bec8f599c 100644 --- a/Content.Shared/Procedural/DungeonGenerators/NoiseDunGen.cs +++ b/Content.Shared/Procedural/DungeonGenerators/NoiseDunGen.cs @@ -1,15 +1,12 @@ -using Content.Shared.Maps; +using Content.Shared.Procedural.Distance; using Robust.Shared.Noise; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; namespace Content.Shared.Procedural.DungeonGenerators; /// /// Generates dungeon flooring based on the specified noise. /// -public sealed partial class NoiseDunGen : IDunGen +public sealed partial class NoiseDunGen : IDunGenLayer { /* * Floodfills out from 0 until it finds a valid tile. diff --git a/Content.Shared/Procedural/DungeonGenerators/PrefabDunGen.cs b/Content.Shared/Procedural/DungeonGenerators/PrefabDunGen.cs index ef61fff4b045fa..aeb24d0144820f 100644 --- a/Content.Shared/Procedural/DungeonGenerators/PrefabDunGen.cs +++ b/Content.Shared/Procedural/DungeonGenerators/PrefabDunGen.cs @@ -1,30 +1,20 @@ -using Content.Shared.Maps; -using Content.Shared.Tag; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; +using Robust.Shared.Prototypes; namespace Content.Shared.Procedural.DungeonGenerators; /// /// Places rooms in pre-selected pack layouts. Chooses rooms from the specified whitelist. /// -public sealed partial class PrefabDunGen : IDunGen +/// +/// DungeonData keys are: +/// - FallbackTile +/// - Rooms +/// +public sealed partial class PrefabDunGen : IDunGenLayer { - /// - /// Rooms need to match any of these tags - /// - [DataField("roomWhitelist", customTypeSerializer:typeof(PrototypeIdListSerializer))] - public List RoomWhitelist = new(); - /// /// Room pack presets we can use for this prefab. /// - [DataField("presets", required: true, customTypeSerializer:typeof(PrototypeIdListSerializer))] - public List Presets = new(); - - /// - /// Fallback tile. - /// - [DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string Tile = "FloorSteel"; + [DataField(required: true)] + public List> Presets = new(); } diff --git a/Content.Shared/Procedural/DungeonGenerators/PrototypeDunGen.cs b/Content.Shared/Procedural/DungeonGenerators/PrototypeDunGen.cs new file mode 100644 index 00000000000000..346c60a6cb58dd --- /dev/null +++ b/Content.Shared/Procedural/DungeonGenerators/PrototypeDunGen.cs @@ -0,0 +1,13 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.Procedural.DungeonGenerators; + +/// +/// Runs another . +/// Used for storing data on 1 system. +/// +public sealed partial class PrototypeDunGen : IDunGenLayer +{ + [DataField(required: true)] + public ProtoId Proto; +} diff --git a/Content.Shared/Procedural/DungeonGenerators/ReplaceTileDunGen.cs b/Content.Shared/Procedural/DungeonGenerators/ReplaceTileDunGen.cs new file mode 100644 index 00000000000000..64b76b4cccc7e9 --- /dev/null +++ b/Content.Shared/Procedural/DungeonGenerators/ReplaceTileDunGen.cs @@ -0,0 +1,30 @@ +using Content.Shared.Maps; +using Robust.Shared.Noise; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Procedural.DungeonGenerators; + +/// +/// Replaces existing tiles if they're not empty. +/// +public sealed partial class ReplaceTileDunGen : IDunGenLayer +{ + /// + /// Chance for a non-variant tile to be used, in case they're too noisy. + /// + [DataField] + public float VariantWeight = 0.1f; + + [DataField(required: true)] + public List Layers = new(); +} + +[DataRecord] +public record struct ReplaceTileLayer +{ + public ProtoId Tile; + + public float Threshold; + + public FastNoiseLite Noise; +} diff --git a/Content.Shared/Procedural/DungeonLayers/MobsDunGen.cs b/Content.Shared/Procedural/DungeonLayers/MobsDunGen.cs new file mode 100644 index 00000000000000..30b502efe073e2 --- /dev/null +++ b/Content.Shared/Procedural/DungeonLayers/MobsDunGen.cs @@ -0,0 +1,21 @@ +using Content.Shared.Storage; + +namespace Content.Shared.Procedural.DungeonLayers; + + +/// +/// Spawns mobs inside of the dungeon randomly. +/// +public sealed partial class MobsDunGen : IDunGenLayer +{ + // Counts separate to config to avoid some duplication. + + [DataField] + public int MinCount = 1; + + [DataField] + public int MaxCount = 1; + + [DataField(required: true)] + public List Groups = new(); +} diff --git a/Content.Shared/Procedural/DungeonLayers/OreDunGen.cs b/Content.Shared/Procedural/DungeonLayers/OreDunGen.cs new file mode 100644 index 00000000000000..31bf367d0e45f9 --- /dev/null +++ b/Content.Shared/Procedural/DungeonLayers/OreDunGen.cs @@ -0,0 +1,42 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.Procedural.DungeonLayers; + +/// +/// Generates veins inside of the specified dungeon. +/// +/// +/// Generates on top of existing entities for sanity reasons moreso than performance. +/// +public sealed partial class OreDunGen : IDunGenLayer +{ + /// + /// If the vein generation should occur on top of existing entities what are we replacing. + /// + [DataField] + public EntProtoId? Replacement; + + /// + /// Entity to spawn. + /// + [DataField(required: true)] + public EntProtoId Entity; + + /// + /// Maximum amount of group spawns + /// + [DataField] + public int Count = 10; + + /// + /// Minimum entities to spawn in one group. + /// + [DataField] + public int MinGroupSize = 1; + + /// + /// Maximum entities to spawn in one group. + /// + [DataField] + public int MaxGroupSize = 1; +} diff --git a/Content.Shared/Procedural/DungeonRoom.cs b/Content.Shared/Procedural/DungeonRoom.cs index 4802949d2f38a0..0c6af8f23db072 100644 --- a/Content.Shared/Procedural/DungeonRoom.cs +++ b/Content.Shared/Procedural/DungeonRoom.cs @@ -2,6 +2,7 @@ namespace Content.Shared.Procedural; +// TODO: Cache center and bounds and shit and don't make the caller deal with it. public sealed record DungeonRoom(HashSet Tiles, Vector2 Center, Box2i Bounds, HashSet Exterior) { public readonly List Entrances = new(); diff --git a/Content.Shared/Procedural/IDunGenLayer.cs b/Content.Shared/Procedural/IDunGenLayer.cs new file mode 100644 index 00000000000000..a4e8045af1cdce --- /dev/null +++ b/Content.Shared/Procedural/IDunGenLayer.cs @@ -0,0 +1,7 @@ +namespace Content.Shared.Procedural; + +[ImplicitDataDefinitionForInheritors] +public partial interface IDunGenLayer +{ + +} diff --git a/Content.Shared/Procedural/PostGeneration/AutoCablingDunGen.cs b/Content.Shared/Procedural/PostGeneration/AutoCablingDunGen.cs new file mode 100644 index 00000000000000..5afad7edb18f5f --- /dev/null +++ b/Content.Shared/Procedural/PostGeneration/AutoCablingDunGen.cs @@ -0,0 +1,10 @@ +namespace Content.Shared.Procedural.PostGeneration; + +/// +/// Runs cables throughout the dungeon. +/// +/// +/// DungeonData keys are: +/// - Cabling +/// +public sealed partial class AutoCablingDunGen : IDunGenLayer; diff --git a/Content.Shared/Procedural/PostGeneration/AutoCablingPostGen.cs b/Content.Shared/Procedural/PostGeneration/AutoCablingPostGen.cs deleted file mode 100644 index 8278352b036a14..00000000000000 --- a/Content.Shared/Procedural/PostGeneration/AutoCablingPostGen.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.Procedural.PostGeneration; - -/// -/// Runs cables throughout the dungeon. -/// -public sealed partial class AutoCablingPostGen : IPostDunGen -{ - [DataField] - public EntProtoId Entity = "CableApcExtension"; -} diff --git a/Content.Shared/Procedural/PostGeneration/BiomePostGen.cs b/Content.Shared/Procedural/PostGeneration/BiomeDunGen.cs similarity index 78% rename from Content.Shared/Procedural/PostGeneration/BiomePostGen.cs rename to Content.Shared/Procedural/PostGeneration/BiomeDunGen.cs index d02de241355eee..833cf2dec769a9 100644 --- a/Content.Shared/Procedural/PostGeneration/BiomePostGen.cs +++ b/Content.Shared/Procedural/PostGeneration/BiomeDunGen.cs @@ -1,5 +1,4 @@ using Content.Shared.Parallax.Biomes; -using Content.Shared.Procedural.PostGeneration; using Robust.Shared.Prototypes; namespace Content.Shared.Procedural.PostGeneration; @@ -8,7 +7,7 @@ namespace Content.Shared.Procedural.PostGeneration; /// Generates a biome on top of valid tiles, then removes the biome when done. /// Only works if no existing biome is present. /// -public sealed partial class BiomePostGen : IPostDunGen +public sealed partial class BiomeDunGen : IDunGenLayer { [DataField(required: true)] public ProtoId BiomeTemplate; diff --git a/Content.Shared/Procedural/PostGeneration/BiomeMarkerLayerPostGen.cs b/Content.Shared/Procedural/PostGeneration/BiomeMarkerLayerDunGen.cs similarity index 73% rename from Content.Shared/Procedural/PostGeneration/BiomeMarkerLayerPostGen.cs rename to Content.Shared/Procedural/PostGeneration/BiomeMarkerLayerDunGen.cs index dc64febe7b0549..af5d7c5d8f9af0 100644 --- a/Content.Shared/Procedural/PostGeneration/BiomeMarkerLayerPostGen.cs +++ b/Content.Shared/Procedural/PostGeneration/BiomeMarkerLayerDunGen.cs @@ -1,5 +1,3 @@ -using Content.Shared.Parallax.Biomes.Markers; -using Content.Shared.Procedural.PostGeneration; using Content.Shared.Random; using Robust.Shared.Prototypes; @@ -8,7 +6,7 @@ namespace Content.Shared.Procedural.PostGeneration; /// /// Spawns the specified marker layer on top of the dungeon rooms. /// -public sealed partial class BiomeMarkerLayerPostGen : IPostDunGen +public sealed partial class BiomeMarkerLayerDunGen : IDunGenLayer { /// /// How many times to spawn marker layers; can duplicate. diff --git a/Content.Shared/Procedural/PostGeneration/BoundaryWallDunGen.cs b/Content.Shared/Procedural/PostGeneration/BoundaryWallDunGen.cs new file mode 100644 index 00000000000000..4151527f8a07c6 --- /dev/null +++ b/Content.Shared/Procedural/PostGeneration/BoundaryWallDunGen.cs @@ -0,0 +1,23 @@ +namespace Content.Shared.Procedural.PostGeneration; + +/// +/// Iterates room edges and places the relevant tiles and walls on any free indices. +/// +/// +/// Dungeon data keys are: +/// - CornerWalls (Optional) +/// - FallbackTile +/// - Walls +/// +public sealed partial class BoundaryWallDunGen : IDunGenLayer +{ + [DataField] + public BoundaryWallFlags Flags = BoundaryWallFlags.Corridors | BoundaryWallFlags.Rooms; +} + +[Flags] +public enum BoundaryWallFlags : byte +{ + Rooms = 1 << 0, + Corridors = 1 << 1, +} diff --git a/Content.Shared/Procedural/PostGeneration/BoundaryWallPostGen.cs b/Content.Shared/Procedural/PostGeneration/BoundaryWallPostGen.cs deleted file mode 100644 index 390ff42feea8d1..00000000000000 --- a/Content.Shared/Procedural/PostGeneration/BoundaryWallPostGen.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Content.Shared.Maps; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Shared.Procedural.PostGeneration; - -/// -/// Iterates room edges and places the relevant tiles and walls on any free indices. -/// -public sealed partial class BoundaryWallPostGen : IPostDunGen -{ - [DataField] - public ProtoId Tile = "FloorSteel"; - - [DataField] - public EntProtoId Wall = "WallSolid"; - - /// - /// Walls to use in corners if applicable. - /// - [DataField] - public string? CornerWall; - - [DataField] - public BoundaryWallFlags Flags = BoundaryWallFlags.Corridors | BoundaryWallFlags.Rooms; -} - -[Flags] -public enum BoundaryWallFlags : byte -{ - Rooms = 1 << 0, - Corridors = 1 << 1, -} diff --git a/Content.Shared/Procedural/PostGeneration/CornerClutterDunGen.cs b/Content.Shared/Procedural/PostGeneration/CornerClutterDunGen.cs new file mode 100644 index 00000000000000..2a904281c8036e --- /dev/null +++ b/Content.Shared/Procedural/PostGeneration/CornerClutterDunGen.cs @@ -0,0 +1,14 @@ +namespace Content.Shared.Procedural.PostGeneration; + +/// +/// Spawns entities inside corners. +/// +/// +/// Dungeon data keys are: +/// - CornerClutter +/// +public sealed partial class CornerClutterDunGen : IDunGenLayer +{ + [DataField] + public float Chance = 0.50f; +} diff --git a/Content.Shared/Procedural/PostGeneration/CornerClutterPostGen.cs b/Content.Shared/Procedural/PostGeneration/CornerClutterPostGen.cs deleted file mode 100644 index a16c7f9ab3f356..00000000000000 --- a/Content.Shared/Procedural/PostGeneration/CornerClutterPostGen.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Content.Shared.Storage; - -namespace Content.Shared.Procedural.PostGeneration; - -/// -/// Spawns entities inside corners. -/// -public sealed partial class CornerClutterPostGen : IPostDunGen -{ - [DataField] - public float Chance = 0.50f; - - /// - /// The default starting bulbs - /// - [DataField(required: true)] - public List Contents = new(); -} diff --git a/Content.Shared/Procedural/PostGeneration/CorridorClutterPostGen.cs b/Content.Shared/Procedural/PostGeneration/CorridorClutterDunGen.cs similarity index 85% rename from Content.Shared/Procedural/PostGeneration/CorridorClutterPostGen.cs rename to Content.Shared/Procedural/PostGeneration/CorridorClutterDunGen.cs index a8a74ba6ccb036..5b397b40dfc185 100644 --- a/Content.Shared/Procedural/PostGeneration/CorridorClutterPostGen.cs +++ b/Content.Shared/Procedural/PostGeneration/CorridorClutterDunGen.cs @@ -5,7 +5,7 @@ namespace Content.Shared.Procedural.PostGeneration; /// /// Adds entities randomly to the corridors. /// -public sealed partial class CorridorClutterPostGen : IPostDunGen +public sealed partial class CorridorClutterDunGen : IDunGenLayer { [DataField] public float Chance = 0.05f; diff --git a/Content.Shared/Procedural/PostGeneration/CorridorDecalSkirtingPostGen.cs b/Content.Shared/Procedural/PostGeneration/CorridorDecalSkirtingDunGen.cs similarity index 72% rename from Content.Shared/Procedural/PostGeneration/CorridorDecalSkirtingPostGen.cs rename to Content.Shared/Procedural/PostGeneration/CorridorDecalSkirtingDunGen.cs index 4b139a8be65168..e6090436555561 100644 --- a/Content.Shared/Procedural/PostGeneration/CorridorDecalSkirtingPostGen.cs +++ b/Content.Shared/Procedural/PostGeneration/CorridorDecalSkirtingDunGen.cs @@ -7,29 +7,23 @@ namespace Content.Shared.Procedural.PostGeneration; /// /// Applies decal skirting to corridors. /// -public sealed partial class CorridorDecalSkirtingPostGen : IPostDunGen +public sealed partial class CorridorDecalSkirtingDunGen : IDunGenLayer { - /// - /// Color to apply to decals. - /// - [DataField("color")] - public Color? Color; - /// /// Decal where 1 edge is found. /// - [DataField("cardinalDecals")] + [DataField] public Dictionary CardinalDecals = new(); /// /// Decal where 1 corner edge is found. /// - [DataField("pocketDecals")] + [DataField] public Dictionary PocketDecals = new(); /// /// Decal where 2 or 3 edges are found. /// - [DataField("cornerDecals")] + [DataField] public Dictionary CornerDecals = new(); } diff --git a/Content.Shared/Procedural/PostGeneration/CorridorPostGen.cs b/Content.Shared/Procedural/PostGeneration/CorridorDunGen.cs similarity index 73% rename from Content.Shared/Procedural/PostGeneration/CorridorPostGen.cs rename to Content.Shared/Procedural/PostGeneration/CorridorDunGen.cs index 705ae99dcef450..6d75cd9cb2bda3 100644 --- a/Content.Shared/Procedural/PostGeneration/CorridorPostGen.cs +++ b/Content.Shared/Procedural/PostGeneration/CorridorDunGen.cs @@ -1,12 +1,13 @@ -using Content.Shared.Maps; -using Robust.Shared.Prototypes; - namespace Content.Shared.Procedural.PostGeneration; /// /// Connects room entrances via corridor segments. /// -public sealed partial class CorridorPostGen : IPostDunGen +/// +/// Dungeon data keys are: +/// - FallbackTile +/// +public sealed partial class CorridorDunGen : IDunGenLayer { /// /// How far we're allowed to generate a corridor before calling it. @@ -17,9 +18,6 @@ public sealed partial class CorridorPostGen : IPostDunGen [DataField] public int PathLimit = 2048; - [DataField] - public ProtoId Tile = "FloorSteel"; - /// /// How wide to make the corridor. /// diff --git a/Content.Shared/Procedural/PostGeneration/DungeonEntranceDunGen.cs b/Content.Shared/Procedural/PostGeneration/DungeonEntranceDunGen.cs new file mode 100644 index 00000000000000..40cc95f5fc90ee --- /dev/null +++ b/Content.Shared/Procedural/PostGeneration/DungeonEntranceDunGen.cs @@ -0,0 +1,18 @@ +namespace Content.Shared.Procedural.PostGeneration; + +/// +/// Selects [count] rooms and places external doors to them. +/// +/// +/// Dungeon data keys are: +/// - Entrance +/// - FallbackTile +/// +public sealed partial class DungeonEntranceDunGen : IDunGenLayer +{ + /// + /// How many rooms we place doors on. + /// + [DataField] + public int Count = 1; +} diff --git a/Content.Shared/Procedural/PostGeneration/DungeonEntrancePostGen.cs b/Content.Shared/Procedural/PostGeneration/DungeonEntrancePostGen.cs deleted file mode 100644 index 3398b51317974b..00000000000000 --- a/Content.Shared/Procedural/PostGeneration/DungeonEntrancePostGen.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Content.Shared.Maps; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; - -namespace Content.Shared.Procedural.PostGeneration; - -/// -/// Selects [count] rooms and places external doors to them. -/// -public sealed partial class DungeonEntrancePostGen : IPostDunGen -{ - /// - /// How many rooms we place doors on. - /// - [DataField("count")] - public int Count = 1; - - [DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer))] - public List Entities = new() - { - "CableApcExtension", - "AirlockGlass", - }; - - [DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string Tile = "FloorSteel"; -} diff --git a/Content.Shared/Procedural/PostGeneration/EntranceFlankDunGen.cs b/Content.Shared/Procedural/PostGeneration/EntranceFlankDunGen.cs new file mode 100644 index 00000000000000..27baa48ec628c7 --- /dev/null +++ b/Content.Shared/Procedural/PostGeneration/EntranceFlankDunGen.cs @@ -0,0 +1,11 @@ +namespace Content.Shared.Procedural.PostGeneration; + +/// +/// Spawns entities on either side of an entrance. +/// +/// +/// Dungeon data keys are: +/// - FallbackTile +/// - +/// +public sealed partial class EntranceFlankDunGen : IDunGenLayer; diff --git a/Content.Shared/Procedural/PostGeneration/EntranceFlankPostGen.cs b/Content.Shared/Procedural/PostGeneration/EntranceFlankPostGen.cs deleted file mode 100644 index 96e9bd5d6d114d..00000000000000 --- a/Content.Shared/Procedural/PostGeneration/EntranceFlankPostGen.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Content.Shared.Maps; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Shared.Procedural.PostGeneration; - -/// -/// Spawns entities on either side of an entrance. -/// -public sealed partial class EntranceFlankPostGen : IPostDunGen -{ - [DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string Tile = "FloorSteel"; - - [DataField("entities")] - public List Entities = new(); -} diff --git a/Content.Shared/Procedural/PostGeneration/ExternalWindowDunGen.cs b/Content.Shared/Procedural/PostGeneration/ExternalWindowDunGen.cs new file mode 100644 index 00000000000000..0b29344b90b606 --- /dev/null +++ b/Content.Shared/Procedural/PostGeneration/ExternalWindowDunGen.cs @@ -0,0 +1,11 @@ +namespace Content.Shared.Procedural.PostGeneration; + +/// +/// If external areas are found will try to generate windows. +/// +/// +/// Dungeon data keys are: +/// - EntranceFlank +/// - FallbackTile +/// +public sealed partial class ExternalWindowDunGen : IDunGenLayer; diff --git a/Content.Shared/Procedural/PostGeneration/ExternalWindowPostGen.cs b/Content.Shared/Procedural/PostGeneration/ExternalWindowPostGen.cs deleted file mode 100644 index d5580baeaaab65..00000000000000 --- a/Content.Shared/Procedural/PostGeneration/ExternalWindowPostGen.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Content.Shared.Maps; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; - -namespace Content.Shared.Procedural.PostGeneration; - -/// -/// If external areas are found will try to generate windows. -/// -public sealed partial class ExternalWindowPostGen : IPostDunGen -{ - [DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer))] - public List Entities = new() - { - "Grille", - "Window", - }; - - [DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string Tile = "FloorSteel"; -} diff --git a/Content.Shared/Procedural/PostGeneration/IPostDunGen.cs b/Content.Shared/Procedural/PostGeneration/IPostDunGen.cs deleted file mode 100644 index b55cab8e63e360..00000000000000 --- a/Content.Shared/Procedural/PostGeneration/IPostDunGen.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Content.Shared.Procedural.PostGeneration; - -/// -/// Ran after generating dungeon rooms. Can be used for additional loot, contents, etc. -/// -[ImplicitDataDefinitionForInheritors] -public partial interface IPostDunGen -{ - -} diff --git a/Content.Shared/Procedural/PostGeneration/InternalWindowDunGen.cs b/Content.Shared/Procedural/PostGeneration/InternalWindowDunGen.cs new file mode 100644 index 00000000000000..11b1c6a785a6b4 --- /dev/null +++ b/Content.Shared/Procedural/PostGeneration/InternalWindowDunGen.cs @@ -0,0 +1,11 @@ +namespace Content.Shared.Procedural.PostGeneration; + +/// +/// If internal areas are found will try to generate windows. +/// +/// +/// Dungeon data keys are: +/// - FallbackTile +/// - Window +/// +public sealed partial class InternalWindowDunGen : IDunGenLayer; diff --git a/Content.Shared/Procedural/PostGeneration/InternalWindowPostGen.cs b/Content.Shared/Procedural/PostGeneration/InternalWindowPostGen.cs deleted file mode 100644 index 4c6223eb92a01a..00000000000000 --- a/Content.Shared/Procedural/PostGeneration/InternalWindowPostGen.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Content.Shared.Maps; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; - -namespace Content.Shared.Procedural.PostGeneration; - -/// -/// If internal areas are found will try to generate windows. -/// -public sealed partial class InternalWindowPostGen : IPostDunGen -{ - [DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer))] - public List Entities = new() - { - "Grille", - "Window", - }; - - [DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string Tile = "FloorSteel"; -} diff --git a/Content.Shared/Procedural/PostGeneration/JunctionDunGen.cs b/Content.Shared/Procedural/PostGeneration/JunctionDunGen.cs new file mode 100644 index 00000000000000..899f2716216833 --- /dev/null +++ b/Content.Shared/Procedural/PostGeneration/JunctionDunGen.cs @@ -0,0 +1,18 @@ +namespace Content.Shared.Procedural.PostGeneration; + +/// +/// Places the specified entities at junction areas. +/// +/// +/// Dungeon data keys are: +/// - Entrance +/// - FallbackTile +/// +public sealed partial class JunctionDunGen : IDunGenLayer +{ + /// + /// Width to check for junctions. + /// + [DataField] + public int Width = 3; +} diff --git a/Content.Shared/Procedural/PostGeneration/JunctionPostGen.cs b/Content.Shared/Procedural/PostGeneration/JunctionPostGen.cs deleted file mode 100644 index 5c4cf43b7f0b01..00000000000000 --- a/Content.Shared/Procedural/PostGeneration/JunctionPostGen.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Content.Shared.Maps; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; - -namespace Content.Shared.Procedural.PostGeneration; - -/// -/// Places the specified entities at junction areas. -/// -public sealed partial class JunctionPostGen : IPostDunGen -{ - /// - /// Width to check for junctions. - /// - [DataField("width")] - public int Width = 3; - - [DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string Tile = "FloorSteel"; - - [DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer))] - public List Entities = new() - { - "CableApcExtension", - "AirlockGlass" - }; -} diff --git a/Content.Shared/Procedural/PostGeneration/MiddleConnectionDunGen.cs b/Content.Shared/Procedural/PostGeneration/MiddleConnectionDunGen.cs new file mode 100644 index 00000000000000..a5758c14989368 --- /dev/null +++ b/Content.Shared/Procedural/PostGeneration/MiddleConnectionDunGen.cs @@ -0,0 +1,19 @@ +namespace Content.Shared.Procedural.PostGeneration; + +/// +/// Places the specified entities on the middle connections between rooms +/// +public sealed partial class MiddleConnectionDunGen : IDunGenLayer +{ + /// + /// How much overlap there needs to be between 2 rooms exactly. + /// + [DataField] + public int OverlapCount = -1; + + /// + /// How many connections to spawn between rooms. + /// + [DataField] + public int Count = 1; +} diff --git a/Content.Shared/Procedural/PostGeneration/MiddleConnectionPostGen.cs b/Content.Shared/Procedural/PostGeneration/MiddleConnectionPostGen.cs deleted file mode 100644 index d29a65434c7a30..00000000000000 --- a/Content.Shared/Procedural/PostGeneration/MiddleConnectionPostGen.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Content.Shared.Maps; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; - -namespace Content.Shared.Procedural.PostGeneration; - -/// -/// Places the specified entities on the middle connections between rooms -/// -public sealed partial class MiddleConnectionPostGen : IPostDunGen -{ - /// - /// How much overlap there needs to be between 2 rooms exactly. - /// - [DataField("overlapCount")] - public int OverlapCount = -1; - - /// - /// How many connections to spawn between rooms. - /// - [DataField("count")] - public int Count = 1; - - [DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string Tile = "FloorSteel"; - - [DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer))] - public List Entities = new() - { - "CableApcExtension", - "AirlockGlass" - }; - - /// - /// If overlap > 1 then what should spawn on the edges. - /// - [DataField("edgeEntities")] public List EdgeEntities = new(); -} diff --git a/Content.Shared/Procedural/PostGeneration/RoomEntranceDunGen.cs b/Content.Shared/Procedural/PostGeneration/RoomEntranceDunGen.cs new file mode 100644 index 00000000000000..d3b5672dcb015c --- /dev/null +++ b/Content.Shared/Procedural/PostGeneration/RoomEntranceDunGen.cs @@ -0,0 +1,11 @@ +namespace Content.Shared.Procedural.PostGeneration; + +/// +/// Places tiles / entities onto room entrances. +/// +/// +/// DungeonData keys are: +/// - Entrance +/// - FallbackTile +/// +public sealed partial class RoomEntranceDunGen : IDunGenLayer; diff --git a/Content.Shared/Procedural/PostGeneration/RoomEntrancePostGen.cs b/Content.Shared/Procedural/PostGeneration/RoomEntrancePostGen.cs deleted file mode 100644 index 5fd78b0540aec4..00000000000000 --- a/Content.Shared/Procedural/PostGeneration/RoomEntrancePostGen.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Content.Shared.Maps; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; - -namespace Content.Shared.Procedural.PostGeneration; - -/// -/// Places tiles / entities onto room entrances. -/// -public sealed partial class RoomEntrancePostGen : IPostDunGen -{ - [DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer))] - public List Entities = new() - { - "CableApcExtension", - "AirlockGlass", - }; - - [DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string Tile = "FloorSteel"; -} diff --git a/Content.Shared/Procedural/PostGeneration/SplineDungeonConnectorDunGen.cs b/Content.Shared/Procedural/PostGeneration/SplineDungeonConnectorDunGen.cs new file mode 100644 index 00000000000000..ec8349c671bda0 --- /dev/null +++ b/Content.Shared/Procedural/PostGeneration/SplineDungeonConnectorDunGen.cs @@ -0,0 +1,19 @@ +namespace Content.Shared.Procedural.PostGeneration; + +/// +/// Connects dungeons via points that get subdivided. +/// +public sealed partial class SplineDungeonConnectorDunGen : IDunGenLayer +{ + /// + /// Will divide the distance between the start and end points so that no subdivision is more than these metres away. + /// + [DataField] + public int DivisionDistance = 10; + + /// + /// How much each subdivision can vary from the middle. + /// + [DataField] + public float VarianceMax = 0.35f; +} diff --git a/Content.Shared/Procedural/PostGeneration/WallMountDunGen.cs b/Content.Shared/Procedural/PostGeneration/WallMountDunGen.cs new file mode 100644 index 00000000000000..a5c790cb22fc14 --- /dev/null +++ b/Content.Shared/Procedural/PostGeneration/WallMountDunGen.cs @@ -0,0 +1,13 @@ +namespace Content.Shared.Procedural.PostGeneration; + +/// +/// Spawns on the boundary tiles of rooms. +/// +public sealed partial class WallMountDunGen : IDunGenLayer +{ + /// + /// Chance per free tile to spawn a wallmount. + /// + [DataField] + public double Prob = 0.1; +} diff --git a/Content.Shared/Procedural/PostGeneration/WallMountPostGen.cs b/Content.Shared/Procedural/PostGeneration/WallMountPostGen.cs deleted file mode 100644 index 1fbdedf56103ad..00000000000000 --- a/Content.Shared/Procedural/PostGeneration/WallMountPostGen.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Content.Shared.Maps; -using Content.Shared.Storage; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Shared.Procedural.PostGeneration; - -/// -/// Spawns on the boundary tiles of rooms. -/// -public sealed partial class WallMountPostGen : IPostDunGen -{ - [DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string Tile = "FloorSteel"; - - [DataField("spawns")] - public List Spawns = new(); - - /// - /// Chance per free tile to spawn a wallmount. - /// - [DataField("prob")] - public double Prob = 0.1; -} diff --git a/Content.Shared/Procedural/PostGeneration/WormCorridorPostGen.cs b/Content.Shared/Procedural/PostGeneration/WormCorridorDunGen.cs similarity index 73% rename from Content.Shared/Procedural/PostGeneration/WormCorridorPostGen.cs rename to Content.Shared/Procedural/PostGeneration/WormCorridorDunGen.cs index c57d92ef9568e5..b71e845a733706 100644 --- a/Content.Shared/Procedural/PostGeneration/WormCorridorPostGen.cs +++ b/Content.Shared/Procedural/PostGeneration/WormCorridorDunGen.cs @@ -1,14 +1,10 @@ -using Content.Shared.Maps; -using Content.Shared.Procedural.DungeonGenerators; -using Robust.Shared.Prototypes; - namespace Content.Shared.Procedural.PostGeneration; // Ime a worm /// /// Generates worm corridors. /// -public sealed partial class WormCorridorPostGen : IPostDunGen +public sealed partial class WormCorridorDunGen : IDunGenLayer { [DataField] public int PathLimit = 2048; @@ -31,9 +27,6 @@ public sealed partial class WormCorridorPostGen : IPostDunGen [DataField] public Angle MaxAngleChange = Angle.FromDegrees(45); - [DataField] - public ProtoId Tile = "FloorSteel"; - /// /// How wide to make the corridor. /// diff --git a/Content.Shared/Salvage/SharedSalvageSystem.Magnet.cs b/Content.Shared/Salvage/SharedSalvageSystem.Magnet.cs index 81390e5f65a0ec..62edb36db93ae7 100644 --- a/Content.Shared/Salvage/SharedSalvageSystem.Magnet.cs +++ b/Content.Shared/Salvage/SharedSalvageSystem.Magnet.cs @@ -32,14 +32,14 @@ public ISalvageMagnetOffering GetSalvageOffering(int seed) var layers = new Dictionary(); // If we ever add more random layers will need to Next on these. - foreach (var layer in configProto.PostGeneration) + foreach (var layer in configProto.Layers) { switch (layer) { - case BiomePostGen: + case BiomeDunGen: rand.Next(); break; - case BiomeMarkerLayerPostGen marker: + case BiomeMarkerLayerDunGen marker: for (var i = 0; i < marker.Count; i++) { var proto = _proto.Index(marker.MarkerTemplate).Pick(rand); diff --git a/Content.Shared/Shuttles/Systems/SharedShuttleSystem.cs b/Content.Shared/Shuttles/Systems/SharedShuttleSystem.cs index a382e943ff946e..db2cbaa138b874 100644 --- a/Content.Shared/Shuttles/Systems/SharedShuttleSystem.cs +++ b/Content.Shared/Shuttles/Systems/SharedShuttleSystem.cs @@ -18,7 +18,7 @@ public abstract partial class SharedShuttleSystem : EntitySystem [Dependency] protected readonly SharedTransformSystem XformSystem = default!; [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; - public const float FTLRange = 512f; + public const float FTLRange = 256f; public const float FTLBufferRange = 8f; private EntityQuery _gridQuery; diff --git a/Content.Shared/Storage/EntitySpawnEntry.cs b/Content.Shared/Storage/EntitySpawnEntry.cs index 792459c72f72fc..6e24681c2dbdde 100644 --- a/Content.Shared/Storage/EntitySpawnEntry.cs +++ b/Content.Shared/Storage/EntitySpawnEntry.cs @@ -5,6 +5,19 @@ namespace Content.Shared.Storage; +/// +/// Prototype wrapper around +/// +[Prototype] +public sealed class EntitySpawnEntryPrototype : IPrototype +{ + [IdDataField] + public string ID { get; } = string.Empty; + + [DataField] + public List Entries = new(); +} + /// /// Dictates a list of items that can be spawned. /// diff --git a/Resources/Prototypes/Entities/Stations/base.yml b/Resources/Prototypes/Entities/Stations/base.yml index 8a0d6c40030403..7b58091588345e 100644 --- a/Resources/Prototypes/Entities/Stations/base.yml +++ b/Resources/Prototypes/Entities/Stations/base.yml @@ -46,17 +46,26 @@ path: /Maps/Shuttles/cargo.yml - type: GridSpawn groups: - trade: + vgroid: !type:DungeonSpawnGroup + minimumDistance: 1000 + nameDataset: names_borer + addComponents: + - type: Gravity + enabled: true + inherent: true + protos: + - VGRoid + trade: !type:GridSpawnGroup addComponents: - type: ProtectedGrid - type: TradeStation paths: - /Maps/Shuttles/trading_outpost.yml - mining: + mining: !type:GridSpawnGroup paths: - /Maps/Shuttles/mining.yml # Spawn last - ruins: + ruins: !type:GridSpawnGroup hide: true nameGrid: true minCount: 2 diff --git a/Resources/Prototypes/Entities/Structures/Walls/asteroid.yml b/Resources/Prototypes/Entities/Structures/Walls/asteroid.yml index d131805bf51ada..e14bf26e0dbdd0 100644 --- a/Resources/Prototypes/Entities/Structures/Walls/asteroid.yml +++ b/Resources/Prototypes/Entities/Structures/Walls/asteroid.yml @@ -1,5 +1,7 @@ #TODO: Someone should probably move the ore vein prototypes into their own file, or otherwise split this up in some way. This should not be 1.5k lines long. +# Anyway +# See WallRock variants for the remappings. #Asteroid rock - type: entity @@ -639,21 +641,28 @@ description: An ore vein rich with coal. suffix: Coal components: - - type: OreVein - oreChance: 1.0 - currentOre: OreCoal - - type: Sprite - layers: - - state: rock - - map: [ "enum.EdgeLayer.South" ] - state: rock_south - - map: [ "enum.EdgeLayer.East" ] - state: rock_east - - map: [ "enum.EdgeLayer.North" ] - state: rock_north - - map: [ "enum.EdgeLayer.West" ] - state: rock_west - - state: rock_coal + - type: EntityRemap + mask: + AsteroidRock: AsteroidRockCoal + WallRockBasalt: WallRockBasaltCoal + WallRockChromite: WallRockChromiteCoal + WallRockSand: WallRockSandCoal + WallRockSnow: WallRockSnowCoal + - type: OreVein + oreChance: 1.0 + currentOre: OreCoal + - type: Sprite + layers: + - state: rock + - map: [ "enum.EdgeLayer.South" ] + state: rock_south + - map: [ "enum.EdgeLayer.East" ] + state: rock_east + - map: [ "enum.EdgeLayer.North" ] + state: rock_north + - map: [ "enum.EdgeLayer.West" ] + state: rock_west + - state: rock_coal - type: entity id: WallRockGold @@ -661,21 +670,28 @@ description: An ore vein rich with gold. suffix: Gold components: - - type: OreVein - oreChance: 1.0 - currentOre: OreGold - - type: Sprite - layers: - - state: rock - - map: [ "enum.EdgeLayer.South" ] - state: rock_south - - map: [ "enum.EdgeLayer.East" ] - state: rock_east - - map: [ "enum.EdgeLayer.North" ] - state: rock_north - - map: [ "enum.EdgeLayer.West" ] - state: rock_west - - state: rock_gold + - type: EntityRemap + mask: + AsteroidRock: AsteroidRockGold + WallRockBasalt: WallRockBasaltGold + WallRockChromite: WallRockChromiteGold + WallRockSand: WallRockSandGold + WallRockSnow: WallRockSnowGold + - type: OreVein + oreChance: 1.0 + currentOre: OreGold + - type: Sprite + layers: + - state: rock + - map: [ "enum.EdgeLayer.South" ] + state: rock_south + - map: [ "enum.EdgeLayer.East" ] + state: rock_east + - map: [ "enum.EdgeLayer.North" ] + state: rock_north + - map: [ "enum.EdgeLayer.West" ] + state: rock_west + - state: rock_gold - type: entity id: WallRockPlasma @@ -683,21 +699,28 @@ description: An ore vein rich with plasma. suffix: Plasma components: - - type: OreVein - oreChance: 1.0 - currentOre: OrePlasma - - type: Sprite - layers: - - state: rock - - map: [ "enum.EdgeLayer.South" ] - state: rock_south - - map: [ "enum.EdgeLayer.East" ] - state: rock_east - - map: [ "enum.EdgeLayer.North" ] - state: rock_north - - map: [ "enum.EdgeLayer.West" ] - state: rock_west - - state: rock_phoron + - type: EntityRemap + mask: + AsteroidRock: AsteroidRockPlasma + WallRockBasalt: WallRockBasaltPlasma + WallRockChromite: WallRockChromitePlasma + WallRockSand: WallRockSandPlasma + WallRockSnow: WallRockSnowPlasma + - type: OreVein + oreChance: 1.0 + currentOre: OrePlasma + - type: Sprite + layers: + - state: rock + - map: [ "enum.EdgeLayer.South" ] + state: rock_south + - map: [ "enum.EdgeLayer.East" ] + state: rock_east + - map: [ "enum.EdgeLayer.North" ] + state: rock_north + - map: [ "enum.EdgeLayer.West" ] + state: rock_west + - state: rock_phoron - type: entity id: WallRockQuartz @@ -705,21 +728,28 @@ description: An ore vein rich with quartz. suffix: Quartz components: - - type: OreVein - oreChance: 1.0 - currentOre: OreSpaceQuartz - - type: Sprite - layers: - - state: rock - - map: [ "enum.EdgeLayer.South" ] - state: rock_south - - map: [ "enum.EdgeLayer.East" ] - state: rock_east - - map: [ "enum.EdgeLayer.North" ] - state: rock_north - - map: [ "enum.EdgeLayer.West" ] - state: rock_west - - state: rock_quartz + - type: EntityRemap + mask: + AsteroidRock: AsteroidRockQuartz + WallRockBasalt: WallRockBasaltQuartz + WallRockChromite: WallRockChromiteQuartz + WallRockSand: WallRockSandQuartz + WallRockSnow: WallRockSnowQuartz + - type: OreVein + oreChance: 1.0 + currentOre: OreSpaceQuartz + - type: Sprite + layers: + - state: rock + - map: [ "enum.EdgeLayer.South" ] + state: rock_south + - map: [ "enum.EdgeLayer.East" ] + state: rock_east + - map: [ "enum.EdgeLayer.North" ] + state: rock_north + - map: [ "enum.EdgeLayer.West" ] + state: rock_west + - state: rock_quartz - type: entity id: WallRockSilver @@ -727,21 +757,28 @@ description: An ore vein rich with silver. suffix: Silver components: - - type: OreVein - oreChance: 1.0 - currentOre: OreSilver - - type: Sprite - layers: - - state: rock - - map: [ "enum.EdgeLayer.South" ] - state: rock_south - - map: [ "enum.EdgeLayer.East" ] - state: rock_east - - map: [ "enum.EdgeLayer.North" ] - state: rock_north - - map: [ "enum.EdgeLayer.West" ] - state: rock_west - - state: rock_silver + - type: EntityRemap + mask: + AsteroidRock: AsteroidRockSilver + WallRockBasalt: WallRockBasaltSilver + WallRockChromite: WallRockChromiteSilver + WallRockSand: WallRockSandSilver + WallRockSnow: WallRockSnowSilver + - type: OreVein + oreChance: 1.0 + currentOre: OreSilver + - type: Sprite + layers: + - state: rock + - map: [ "enum.EdgeLayer.South" ] + state: rock_south + - map: [ "enum.EdgeLayer.East" ] + state: rock_east + - map: [ "enum.EdgeLayer.North" ] + state: rock_north + - map: [ "enum.EdgeLayer.West" ] + state: rock_west + - state: rock_silver # Yes I know it drops steel but we may get smelting at some point - type: entity @@ -750,6 +787,13 @@ description: An ore vein rich with iron. suffix: Iron components: + - type: EntityRemap + mask: + AsteroidRock: AsteroidRockTin + WallRockBasalt: WallRockBasaltTin + WallRockChromite: WallRockChromiteTin + WallRockSand: WallRockSandTin + WallRockSnow: WallRockSnowTin - type: OreVein oreChance: 1.0 currentOre: OreSteel @@ -772,21 +816,28 @@ description: An ore vein rich with uranium. suffix: Uranium components: - - type: OreVein - oreChance: 1.0 - currentOre: OreUranium - - type: Sprite - layers: - - state: rock - - map: [ "enum.EdgeLayer.South" ] - state: rock_south - - map: [ "enum.EdgeLayer.East" ] - state: rock_east - - map: [ "enum.EdgeLayer.North" ] - state: rock_north - - map: [ "enum.EdgeLayer.West" ] - state: rock_west - - state: rock_uranium + - type: EntityRemap + mask: + AsteroidRock: AsteroidRockUranium + WallRockBasalt: WallRockBasaltUranium + WallRockChromite: WallRockChromiteUranium + WallRockSand: WallRockSandUranium + WallRockSnow: WallRockSnowUranium + - type: OreVein + oreChance: 1.0 + currentOre: OreUranium + - type: Sprite + layers: + - state: rock + - map: [ "enum.EdgeLayer.South" ] + state: rock_south + - map: [ "enum.EdgeLayer.East" ] + state: rock_east + - map: [ "enum.EdgeLayer.North" ] + state: rock_north + - map: [ "enum.EdgeLayer.West" ] + state: rock_west + - state: rock_uranium - type: entity @@ -795,21 +846,28 @@ description: An ore vein rich with bananium. suffix: Bananium components: - - type: OreVein - oreChance: 1.0 - currentOre: OreBananium - - type: Sprite - layers: - - state: rock - - map: [ "enum.EdgeLayer.South" ] - state: rock_south - - map: [ "enum.EdgeLayer.East" ] - state: rock_east - - map: [ "enum.EdgeLayer.North" ] - state: rock_north - - map: [ "enum.EdgeLayer.West" ] - state: rock_west - - state: rock_bananium + - type: EntityRemap + mask: + AsteroidRock: AsteroidRockBananium + WallRockBasalt: WallRockBasaltBananium + WallRockChromite: WallRockChromiteBananium + WallRockSand: WallRockSandBananium + WallRockSnow: WallRockSnowBananium + - type: OreVein + oreChance: 1.0 + currentOre: OreBananium + - type: Sprite + layers: + - state: rock + - map: [ "enum.EdgeLayer.South" ] + state: rock_south + - map: [ "enum.EdgeLayer.East" ] + state: rock_east + - map: [ "enum.EdgeLayer.North" ] + state: rock_north + - map: [ "enum.EdgeLayer.West" ] + state: rock_west + - state: rock_bananium - type: entity id: WallRockArtifactFragment @@ -817,21 +875,28 @@ description: A rock wall. What's that sticking out of it? suffix: Artifact Fragment components: - - type: OreVein - oreChance: 1.0 - currentOre: OreArtifactFragment - - type: Sprite - layers: - - state: rock - - map: [ "enum.EdgeLayer.South" ] - state: rock_south - - map: [ "enum.EdgeLayer.East" ] - state: rock_east - - map: [ "enum.EdgeLayer.North" ] - state: rock_north - - map: [ "enum.EdgeLayer.West" ] - state: rock_west - - state: rock_artifact_fragment + - type: EntityRemap + mask: + AsteroidRock: AsteroidRockArtifactFragment + WallRockBasalt: WallRockBasaltArtifactFragment + WallRockChromite: WallRockChromiteArtifactFragment + WallRockSand: WallRockSandArtifactFragment + WallRockSnow: WallRockSnowArtifactFragment + - type: OreVein + oreChance: 1.0 + currentOre: OreArtifactFragment + - type: Sprite + layers: + - state: rock + - map: [ "enum.EdgeLayer.South" ] + state: rock_south + - map: [ "enum.EdgeLayer.East" ] + state: rock_east + - map: [ "enum.EdgeLayer.North" ] + state: rock_north + - map: [ "enum.EdgeLayer.West" ] + state: rock_west + - state: rock_artifact_fragment - type: entity id: WallRockSalt @@ -839,21 +904,28 @@ description: An ore vein rich with salt. suffix: Salt components: - - type: OreVein - oreChance: 1.0 - currentOre: OreSalt - - type: Sprite - layers: - - state: rock - - map: [ "enum.EdgeLayer.South" ] - state: rock_south - - map: [ "enum.EdgeLayer.East" ] - state: rock_east - - map: [ "enum.EdgeLayer.North" ] - state: rock_north - - map: [ "enum.EdgeLayer.West" ] - state: rock_west - - state: rock_salt + - type: EntityRemap + mask: + AsteroidRock: AsteroidRockSalt + WallRockBasalt: WallRockBasaltSalt + WallRockChromite: WallRockChromiteSalt + WallRockSand: WallRockSandSalt + WallRockSnow: WallRockSnowSalt + - type: OreVein + oreChance: 1.0 + currentOre: OreSalt + - type: Sprite + layers: + - state: rock + - map: [ "enum.EdgeLayer.South" ] + state: rock_south + - map: [ "enum.EdgeLayer.East" ] + state: rock_east + - map: [ "enum.EdgeLayer.North" ] + state: rock_north + - map: [ "enum.EdgeLayer.West" ] + state: rock_west + - state: rock_salt # Basalt variants - type: entity diff --git a/Resources/Prototypes/Procedural/Magnet/asteroid.yml b/Resources/Prototypes/Procedural/Magnet/asteroid.yml index a21b709afada83..c20b80af55bf97 100644 --- a/Resources/Prototypes/Procedural/Magnet/asteroid.yml +++ b/Resources/Prototypes/Procedural/Magnet/asteroid.yml @@ -15,7 +15,8 @@ - type: dungeonConfig id: BlobAsteroid # Floor generation - generator: !type:NoiseDunGen + layers: + - !type:NoiseDunGen tileCap: 1500 capStd: 32 iterations: 3 @@ -28,22 +29,22 @@ fractalType: FBm octaves: 2 lacunarity: 2 - # Everything else - postGeneration: - # Generate biome - - !type:BiomePostGen - biomeTemplate: Asteroid - # Generate ore veins - - !type:MarkerLayerPostGen - markerTemplate: AsteroidOre + # Generate biome + - !type:BiomeDunGen + biomeTemplate: Asteroid + + # Generate ore veins + - !type:BiomeMarkerLayerDunGen + markerTemplate: AsteroidOre # Multiple smaller asteroids # This is a pain so we generate fewer tiles - type: dungeonConfig id: ClusterAsteroid # Floor generation - generator: !type:NoiseDunGen + layers: + - !type:NoiseDunGen tileCap: 1000 capStd: 32 layers: @@ -55,21 +56,21 @@ fractalType: FBm octaves: 2 lacunarity: 2 - # Everything else - postGeneration: - # Generate biome - - !type:BiomePostGen - biomeTemplate: Asteroid - # Generate ore veins - - !type:MarkerLayerPostGen - markerTemplate: AsteroidOre + # Generate biome + - !type:BiomeDunGen + biomeTemplate: Asteroid + + # Generate ore veins + - !type:BiomeMarkerLayerDunGen + markerTemplate: AsteroidOre # Long and spindly, less smooth than blob - type: dungeonConfig id: SpindlyAsteroid # Floor generation - generator: !type:NoiseDunGen + layers: + - !type:NoiseDunGen tileCap: 1500 capStd: 32 layers: @@ -82,20 +83,21 @@ octaves: 3 lacunarity: 2 cellularDistanceFunction: Euclidean - postGeneration: - # Generate biome - - !type:BiomePostGen - biomeTemplate: Asteroid - # Generate ore veins - - !type:MarkerLayerPostGen - markerTemplate: AsteroidOre + # Generate biome + - !type:BiomeDunGen + biomeTemplate: Asteroid + + # Generate ore veins + - !type:BiomeMarkerLayerDunGen + markerTemplate: AsteroidOre # Lots of holes in it - type: dungeonConfig id: SwissCheeseAsteroid # Floor generation - generator: !type:NoiseDunGen + layers: + - !type:NoiseDunGen tileCap: 1500 capStd: 32 layers: @@ -107,12 +109,11 @@ fractalType: FBm octaves: 2 lacunarity: 2 - # Everything else - postGeneration: - # Generate biome - - !type:BiomePostGen - biomeTemplate: Asteroid - # Generate ore veins - - !type:MarkerLayerPostGen - markerTemplate: AsteroidOre + # Generate biome + - !type:BiomeDunGen + biomeTemplate: Asteroid + + # Generate ore veins + - !type:BiomeMarkerLayerDunGen + markerTemplate: AsteroidOre diff --git a/Resources/Prototypes/Procedural/dungeon_configs.yml b/Resources/Prototypes/Procedural/dungeon_configs.yml index 3614e4e787faa3..b55d5a9e697706 100644 --- a/Resources/Prototypes/Procedural/dungeon_configs.yml +++ b/Resources/Prototypes/Procedural/dungeon_configs.yml @@ -1,361 +1,284 @@ +# Base configs - type: dungeonConfig - id: Experiment - generator: !type:PrefabDunGen - roomWhitelist: - - SalvageExperiment + id: PlanetBase + layers: + - !type:PrefabDunGen presets: - - Bucket - - Wow - - SpaceShip - - Tall - postGeneration: - - !type:CorridorPostGen - width: 3 + - Bucket + - Wow + - SpaceShip + - Tall - - !type:DungeonEntrancePostGen - count: 2 + - !type:CorridorDunGen + width: 3 - - !type:RoomEntrancePostGen - entities: - - CableApcExtension - - AirlockGlass + - !type:DungeonEntranceDunGen + count: 2 - - !type:EntranceFlankPostGen - entities: - - Grille - - Window + - !type:RoomEntranceDunGen - - !type:ExternalWindowPostGen - entities: - - Grille - - Window + - !type:EntranceFlankDunGen - - !type:WallMountPostGen - spawns: - # Posters - - id: RandomPosterLegit - orGroup: content - - id: ExtinguisherCabinetFilled - prob: 0.2 - orGroup: content - - id: RandomPainting - prob: 0.05 - orGroup: content - - id: IntercomCommon - prob: 0.1 - orGroup: content + - !type:ExternalWindowDunGen - - !type:BoundaryWallPostGen - tile: FloorSteel - wall: WallSolid - cornerWall: WallReinforced + - !type:WallMountDunGen - - !type:JunctionPostGen - width: 1 + - !type:BoundaryWallDunGen - - !type:JunctionPostGen + - !type:JunctionDunGen + width: 1 - - !type:AutoCablingPostGen + - !type:JunctionDunGen - - !type:CornerClutterPostGen - contents: - - id: PottedPlantRandom - amount: 1 + - !type:AutoCablingDunGen - - !type:CorridorDecalSkirtingPostGen - color: "#D381C996" - cardinalDecals: - South: BrickTileWhiteLineS - East: BrickTileWhiteLineE - North: BrickTileWhiteLineN - West: BrickTileWhiteLineW - cornerDecals: - SouthEast: BrickTileWhiteCornerSe - SouthWest: BrickTileWhiteCornerSw - NorthEast: BrickTileWhiteCornerNe - NorthWest: BrickTileWhiteCornerNw - pocketDecals: - SouthWest: BrickTileWhiteInnerSw - SouthEast: BrickTileWhiteInnerSe - NorthWest: BrickTileWhiteInnerNw - NorthEast: BrickTileWhiteInnerNe + - !type:CornerClutterDunGen + - !type:CorridorDecalSkirtingDunGen + cardinalDecals: + South: BrickTileWhiteLineS + East: BrickTileWhiteLineE + North: BrickTileWhiteLineN + West: BrickTileWhiteLineW + cornerDecals: + SouthEast: BrickTileWhiteCornerSe + SouthWest: BrickTileWhiteCornerSw + NorthEast: BrickTileWhiteCornerNe + NorthWest: BrickTileWhiteCornerNw + pocketDecals: + SouthWest: BrickTileWhiteInnerSw + SouthEast: BrickTileWhiteInnerSe + NorthWest: BrickTileWhiteInnerNw + NorthEast: BrickTileWhiteInnerNe +# Setups - type: dungeonConfig - id: LavaBrig - generator: !type:PrefabDunGen - roomWhitelist: - - LavaBrig - presets: - - Bucket - - Wow - - SpaceShip - - Tall - postGeneration: - - !type:CorridorPostGen - width: 3 - - - !type:DungeonEntrancePostGen - count: 2 - - - !type:RoomEntrancePostGen - entities: - - CableApcExtension - - AirlockSecurityGlassLocked - - - !type:EntranceFlankPostGen - entities: - - Grille - - Window - - - !type:ExternalWindowPostGen - entities: - - Grille - - Window - - - !type:WallMountPostGen - spawns: - # Posters - - id: RandomPosterLegit - orGroup: content - - id: ExtinguisherCabinetFilled - prob: 0.2 - orGroup: content - - id: RandomPainting - prob: 0.05 - orGroup: content - - id: IntercomCommon - prob: 0.1 - orGroup: content - - - !type:BoundaryWallPostGen - tile: FloorSteel - wall: WallSolid - cornerWall: WallReinforced - - - !type:JunctionPostGen - width: 1 - - - !type:JunctionPostGen - - - !type:AutoCablingPostGen - - - !type:CornerClutterPostGen - contents: - - id: PottedPlantRandom - amount: 1 - - - !type:CorridorDecalSkirtingPostGen - color: "#DE3A3A96" - cardinalDecals: - South: BrickTileWhiteLineS - East: BrickTileWhiteLineE - North: BrickTileWhiteLineN - West: BrickTileWhiteLineW - cornerDecals: - SouthEast: BrickTileWhiteCornerSe - SouthWest: BrickTileWhiteCornerSw - NorthEast: BrickTileWhiteCornerNe - NorthWest: BrickTileWhiteCornerNw - pocketDecals: - SouthWest: BrickTileWhiteInnerSw - SouthEast: BrickTileWhiteInnerSe - NorthWest: BrickTileWhiteInnerNw - NorthEast: BrickTileWhiteInnerNe + id: Experiment + data: + colors: + Decals: "#D381C996" + entities: + Cabling: CableApcExtension + CornerWalls: WallReinforced + Walls: WallSolid + spawnGroups: + CornerClutter: BaseClutter + Entrance: BaseAirlock + EntranceFlank: BaseWindow + Junction: BaseAirlock + WallMounts: ScienceLabsWalls + Window: BaseWindow + tiles: + FallbackTile: FloorSteel + whitelists: + Rooms: + tags: + - SalvageExperiment + layers: + - !type:PrototypeDunGen + proto: PlanetBase - type: dungeonConfig - id: Mineshaft - generator: !type:PrefabDunGen - tile: FloorCaveDrought - roomWhitelist: - - Mineshaft + id: Haunted + data: + entities: + Walls: WallRock + tiles: + FallbackTile: FloorCaveDrought + whitelists: + Rooms: + tags: + - Mineshaft + layers: + - !type:PrefabDunGen presets: - - Bucket - - Wow - - SpaceShip - - Tall - postGeneration: - - - !type:CorridorPostGen - tile: FloorCaveDrought - width: 3 - - - !type:DungeonEntrancePostGen - count: 5 - tile: FloorCaveDrought - entities: - - RandomWoodenWall - - - !type:RoomEntrancePostGen - tile: FloorCaveDrought - entities: - - RandomWoodenWall - - - !type:EntranceFlankPostGen - tile: FloorCaveDrought - entities: - - RandomWoodenWall - - - !type:ExternalWindowPostGen - tile: FloorCaveDrought - entities: - - RandomWoodenWall - - - !type:WallMountPostGen - tile: FloorCaveDrought - spawns: - # Ore - - id: WallRockSalt - prob: 0.6 - orGroup: content - - id: WallRockCoal - prob: 0.6 - orGroup: content - - id: WallRockTin - prob: 0.4 - orGroup: content - - id: WallMining - prob: 0.8 - orGroup: content - - - !type:BoundaryWallPostGen - tile: FloorCaveDrought - wall: WallRock - cornerWall: WallRock + - Bucket + - Wow + - SpaceShip + - Tall - - !type:AutoCablingPostGen - entity: Catwalk + - !type:WormCorridorDunGen + width: 3 - - !type:JunctionPostGen - tile: FloorCaveDrought - width: 3 - entities: - - RandomWoodenSupport + - !type:CorridorClutterDunGen + contents: + - id: FloraStalagmite1 + - id: FloraStalagmite2 + - id: FloraStalagmite3 + - id: FloraStalagmite4 + - id: FloraStalagmite5 + - id: FloraStalagmite6 - - !type:CornerClutterPostGen - contents: - - id: RandomStalagmiteOrCrystal - amount: 1 + - !type:BoundaryWallDunGen - type: dungeonConfig - id: SnowyLabs - generator: !type:PrefabDunGen - roomWhitelist: - - SnowyLabs - presets: - - Bucket - - Wow - - SpaceShip - - Tall - postGeneration: - - !type:CorridorPostGen - width: 3 - - - !type:DungeonEntrancePostGen - count: 2 - - - !type:RoomEntrancePostGen - entities: - - CableApcExtension - - AirlockFreezerHydroponicsLocked - - - !type:EntranceFlankPostGen - entities: - - Grille - - Window - - - !type:ExternalWindowPostGen - entities: - - Grille - - Window - - - !type:WallMountPostGen - spawns: - # Posters - - id: RandomPosterLegit - orGroup: content - - id: ExtinguisherCabinetFilled - prob: 0.2 - orGroup: content - - id: RandomPainting - prob: 0.05 - orGroup: content - - id: IntercomScience - prob: 0.1 - orGroup: content - - - !type:BoundaryWallPostGen - tile: FloorSteel - wall: WallSilver - cornerWall: WallSilver - - - !type:JunctionPostGen - width: 1 - entities: - - AirlockGlass - - - !type:JunctionPostGen - entities: - - AirlockGlass - - - !type:AutoCablingPostGen - - - !type:CornerClutterPostGen - contents: - - id: PottedPlantRandom - amount: 1 - - - !type:CorridorDecalSkirtingPostGen - color: "#4cc7aa96" - cardinalDecals: - South: BrickTileWhiteLineS - East: BrickTileWhiteLineE - North: BrickTileWhiteLineN - West: BrickTileWhiteLineW - cornerDecals: - SouthEast: BrickTileWhiteCornerSe - SouthWest: BrickTileWhiteCornerSw - NorthEast: BrickTileWhiteCornerNe - NorthWest: BrickTileWhiteCornerNw - pocketDecals: - SouthWest: BrickTileWhiteInnerSw - SouthEast: BrickTileWhiteInnerSe - NorthWest: BrickTileWhiteInnerNw - NorthEast: BrickTileWhiteInnerNe - -# todo: Add a biome dungeon generator -# Add corridor first gens that place rooms on top -# Add a worm corridor gen (place subsequent corridors somewhere randomly along the path) -# Place room entrances on ends of corridors touching a tile -# Remove all room tiles from corridors -# Fix paths up and try to reconnect all corridor tiles -# Add a postgen step to spread rooms out, though it shouldn't spread into corridor exteriors + id: LavaBrig + data: + colors: + Decals: "#DE3A3A96" + entities: + Cabling: CableApcExtension + CornerWalls: WallReinforced + Walls: WallSolid + spawnGroups: + CornerClutter: BaseClutter + Entrance: LavaBrigEntrance + EntranceFlank: BaseWindow + Junction: BaseAirlock + WallMounts: ScienceLabsWalls + Window: BaseWindow + whitelists: + Rooms: + tags: + - LavaBrig + layers: + - !type:PrototypeDunGen + proto: PlanetBase - type: dungeonConfig - id: Haunted - generator: !type:PrefabDunGen - tile: FloorCaveDrought - roomWhitelist: - - Mineshaft - presets: - - Bucket - - Wow - - SpaceShip - - Tall - postGeneration: - - !type:WormCorridorPostGen - width: 3 - tile: FloorCaveDrought - - - !type:CorridorClutterPostGen - contents: - - id: FloraStalagmite1 - - id: FloraStalagmite2 - - id: FloraStalagmite3 - - id: FloraStalagmite4 - - id: FloraStalagmite5 - - id: FloraStalagmite6 + id: Mineshaft + data: + entities: + Cabling: Catwalk + spawnGroups: + CornerClutter: MineshaftClutter + Entrance: BaseWoodWall + EntranceFlank: BaseWoodWall + Junction: BaseWoodSupport + Window: BaseWoodWall + tiles: + FallbackTile: FloorCaveDrought + whitelists: + Rooms: + tags: + - Mineshaft + layers: + - !type:PrototypeDunGen + proto: PlanetBase - - !type:BoundaryWallPostGen - tile: FloorCaveDrought - wall: WallRock +- type: dungeonConfig + id: SnowyLabs + data: + colors: + Decals: "#4cc7aa96" + entities: + Cabling: CableApcExtension + CornerWalls: WallSilver + Walls: WallSilver + spawnGroups: + CornerClutter: BaseClutter + Entrance: SnowyLabsEntrance + EntranceFlank: BaseWindow + Junction: BaseAirlock + WallMounts: SnowyLabsWalls + Window: BaseWindow + tiles: + FallbackTile: FloorSteel + whitelists: + Rooms: + tags: + - SnowyLabs + layers: + - !type:PrototypeDunGen + proto: PlanetBase + +# Spawn groups +# Basic +- type: entitySpawnEntry + id: BaseClutter + entries: + - id: PottedPlantRandom + amount: 1 + +- type: entitySpawnEntry + id: BaseAirlock + entries: + - id: CableApcExtension + - id: AirlockGlass + +- type: entitySpawnEntry + id: BaseWindow + entries: + - id: Grille + - id: Window + +# Lava brig +- type: entitySpawnEntry + id: LavaBrigEntrance + entries: + - id: CableApcExtension + - id: AirlockSecurityGlassLocked + +# Mineshaft +- type: entitySpawnEntry + id: BaseWoodWall + entries: + - id: RandomWoodenWall + +- type: entitySpawnEntry + id: BaseWoodSupport + entries: + - id: RandomWoodenSupport + +- type: entitySpawnEntry + id: MineshaftClutter + entries: + - id: RandomStalagmiteOrCrystal + amount: 1 + +- type: entitySpawnEntry + id: MineshaftWalls + entries: + # Ore + - id: WallRockSalt + prob: 0.6 + orGroup: content + - id: WallRockCoal + prob: 0.6 + orGroup: content + - id: WallRockTin + prob: 0.4 + orGroup: content + - id: WallMining + prob: 0.8 + orGroup: content + +# Science lab +- type: entitySpawnEntry + id: ScienceLabsWalls + entries: + # Posters + - id: RandomPosterLegit + orGroup: content + - id: ExtinguisherCabinetFilled + prob: 0.2 + orGroup: content + - id: RandomPainting + prob: 0.05 + orGroup: content + - id: IntercomCommon + prob: 0.1 + orGroup: content + +# Snowy labs +- type: entitySpawnEntry + id: SnowyLabsEntrance + entries: + - id: CableApcExtension + - id: AirlockFreezerHydroponicsLocked + +- type: entitySpawnEntry + id: SnowyLabsWalls + entries: + # Posters + - id: RandomPosterLegit + orGroup: content + - id: ExtinguisherCabinetFilled + prob: 0.2 + orGroup: content + - id: RandomPainting + prob: 0.05 + orGroup: content + - id: IntercomScience + prob: 0.1 + orGroup: content diff --git a/Resources/Prototypes/Procedural/vgroid.yml b/Resources/Prototypes/Procedural/vgroid.yml new file mode 100644 index 00000000000000..49e956e73f5ba0 --- /dev/null +++ b/Resources/Prototypes/Procedural/vgroid.yml @@ -0,0 +1,191 @@ +# Okay so my general thought is this: +# 1. Generate the large mass +# 2. Generate smaller masses offset +# 3. Generate N normal dungeons around the larger mass, preferably near the border +# 4. Generate large paths / small paths around the place +# 5. Spawn ores + fill the rest and the normal stuff + +# If you want mobs they needed to be added at specific steps due to how dungeons work at the moment. + +- type: dungeonConfig + id: VGRoid + layers: + - !type:PrototypeDunGen + proto: VGRoidBlob + - !type:PrototypeDunGen + proto: VGRoidExterior + - !type:PrototypeDunGen + proto: VGRoidSmaller + - !type:PrototypeDunGen + proto: VGRoidSmallPaths + # Fill + - !type:PrototypeDunGen + proto: VGRoidFill + # Ores + - !type:OreDunGen + replacement: IronRock + entity: IronRockIron + count: 50 + minGroupSize: 20 + maxGroupSize: 30 + - !type:OreDunGen + replacement: IronRock + entity: IronRockCoal + count: 50 + minGroupSize: 20 + maxGroupSize: 30 + - !type:OreDunGen + replacement: IronRock + entity: IronRockQuartz + count: 50 + minGroupSize: 20 + maxGroupSize: 30 + - !type:OreDunGen + replacement: IronRock + entity: IronRockSalt + count: 50 + minGroupSize: 20 + maxGroupSize: 30 + - !type:OreDunGen + replacement: IronRock + entity: IronRockGold + count: 50 + minGroupSize: 10 + maxGroupSize: 20 + - !type:OreDunGen + replacement: IronRock + entity: IronRockSilver + count: 50 + minGroupSize: 10 + maxGroupSize: 20 + - !type:OreDunGen + replacement: IronRock + entity: IronRockPlasma + count: 50 + minGroupSize: 10 + maxGroupSize: 20 + - !type:OreDunGen + replacement: IronRock + entity: IronRockUranium + count: 50 + minGroupSize: 10 + maxGroupSize: 20 + - !type:OreDunGen + replacement: IronRock + entity: IronRockBananium + count: 50 + minGroupSize: 10 + maxGroupSize: 20 + - !type:OreDunGen + replacement: IronRock + entity: IronRockArtifactFragment + count: 50 + minGroupSize: 2 + maxGroupSize: 4 + +# Configs +- type: dungeonConfig + id: VGRoidBlob + layers: + - !type:NoiseDistanceDunGen + size: 272, 272 + distanceConfig: !type:DunGenEuclideanSquaredDistance + blendWeight: 0.80 + layers: + - tile: FloorAsteroidSand + threshold: 0.50 + noise: + frequency: 0.010 + noiseType: OpenSimplex2 + fractalType: FBm + octaves: 5 + lacunarity: 2 + gain: 0.5 + +- type: dungeonConfig + id: VGRoidSmaller + minOffset: 40 + maxOffset: 60 + layers: + - !type:NoiseDistanceDunGen + size: 150, 150 + distanceConfig: !type:DunGenEuclideanSquaredDistance + layers: + - tile: FloorAsteroidSand + threshold: 0.50 + noise: + frequency: 0.080 + noiseType: OpenSimplex2 + fractalType: FBm + octaves: 5 + lacunarity: 1.5 + gain: 0.5 + +- type: dungeonConfig + id: VGRoidExterior + reserveTiles: true + data: + tiles: + FallbackTile: PlatingAsteroid + WidenTile: FloorAsteroidSand + layers: + - !type:PrototypeDunGen + proto: VGRoidExteriorDungeons + - !type:SplineDungeonConnectorDunGen + +- type: dungeonConfig + id: VGRoidExteriorDungeons + reserveTiles: true + minCount: 2 + maxCount: 3 + layers: + - !type:ExteriorDunGen + proto: Experiment + - !type:MobsDunGen + minCount: 5 + maxCount: 8 + groups: + - id: MobXeno + amount: 1 + +#- type: dungeonConfig +# id: VGRoidInteriorDungeons +# minCount: 3 +# maxCount: 5 +# # Just randomly spawn these in bounds, doesn't really matter if they go out. + +- type: dungeonConfig + id: VGRoidSmallPaths + reserveTiles: true + layers: + - !type:ReplaceTileDunGen + layers: + - tile: FloorAsteroidSand + threshold: 0.75 + noise: + frequency: 0.040 + noiseType: OpenSimplex2 + fractalType: Ridged + lacunarity: 1.5 + octaves: 2 + gain: 2.0 + # Mobs + # If you want exterior dungeon mobs add them under the prototype. + - !type:MobsDunGen + minCount: 20 + maxCount: 30 + groups: + - id: MobXeno + amount: 1 + +#- type: dungeonConfig +# id: VGRoidOres + +# Fill with rocks. +- type: dungeonConfig + id: VGRoidFill + data: + entities: + Fill: IronRock + layers: + - !type:FillGridDunGen