using System.Linq; using System.Threading.Tasks; using Content.Server.Light.Components; using Content.Shared.Physics; 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.Physics.Components; using Robust.Shared.Physics.Systems; using Robust.Shared.Random; using Robust.Shared.Utility; namespace Content.Server.Procedural; public sealed partial class DungeonJob { /* * Run after the main dungeon generation */ private const int CollisionMask = (int) CollisionGroup.Impassable; private const int CollisionLayer = (int) CollisionGroup.Impassable; private async Task PostGen(BoundaryWallPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random) { var tile = new Tile(_tileDefManager[gen.Tile].TileId); var tiles = new List<(Vector2i Index, Tile Tile)>(); // Spawn wall outline // - Tiles first foreach (var room in dungeon.Rooms) { foreach (var index in room.Tiles) { for (var x = -1; x <= 1; x++) { for (var y = -1; y <= 1; y++) { var neighbor = new Vector2i(x + index.X, y + index.Y); if (dungeon.RoomTiles.Contains(neighbor)) continue; if (!_anchorable.TileFree(grid, neighbor, CollisionLayer, CollisionMask)) continue; tiles.Add((neighbor, tile)); } } } } 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, CollisionLayer, 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)) { 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 % 10 == 0) { await SuspendIfOutOfTime(); ValidateResume(); } } } private async Task PostGen(EntrancePostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random) { var rooms = new List(dungeon.Rooms); var roomTiles = new List(); var tileData = new Tile(_tileDefManager[gen.Tile].TileId); var count = gen.Count; while (count > 0 && rooms.Count > 0) { var roomIndex = random.Next(rooms.Count); var room = rooms[roomIndex]; rooms.RemoveAt(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.Tiles); random.Shuffle(roomTiles); foreach (var tile in roomTiles) { // Check the interior node is at least accessible? // Can't do anchored because it might be a locker or something. // TODO: Better collision mask check if (_lookup.GetEntitiesIntersecting(gridUid, tile, LookupFlags.Dynamic | LookupFlags.Static).Any()) continue; var direction = (tile - room.Center).ToAngle().GetCardinalDir().ToAngle().ToVec(); var isValid = true; for (var j = 1; j < 4; j++) { var neighbor = (tile + direction * j).Floored(); // If it's an interior tile or blocked. if (dungeon.RoomTiles.Contains(neighbor) || _lookup.GetEntitiesIntersecting(gridUid, neighbor, LookupFlags.Dynamic | LookupFlags.Static).Any()) { isValid = false; break; } } if (!isValid) continue; var entrancePos = (tile + direction).Floored(); // Entrance wew grid.SetTile(entrancePos, tileData); ClearDoor(dungeon, grid, entrancePos); var gridCoords = grid.GridTileToLocal(entrancePos); // Need to offset the spawn to avoid spawning in the room. _entManager.SpawnEntity(gen.Door, gridCoords); count--; // 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)) continue; grid.SetTile(nearTile.GridIndices, tileData); } break; } roomTiles.Clear(); } } private async Task PostGen(ExternalWindowPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random) { // Iterate every room with N chance to spawn windows on that wall per cardinal dir. var chance = 0.25; var distance = 10; 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.TileSize / 2f - 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 = true; for (var j = 1; j < distance; j++) { var edgeNeighbor = tile + dirVec * j; if (dungeon.RoomTiles.Contains(edgeNeighbor)) { valid = false; break; } } if (!valid) continue; var windowTile = tile + dirVec; if (!_anchorable.TileFree(grid, windowTile, CollisionLayer, CollisionMask)) continue; validTiles.Add(windowTile); } if (validTiles.Count == 0 || random.NextDouble() > chance) continue; validTiles.Sort((x, y) => ((Vector2) x + grid.TileSize / 2f - room.Center).LengthSquared.CompareTo(((Vector2) y + grid.TileSize / 2f - 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, new Tile(_tileDefManager[gen.Tile].TileId)); foreach (var ent in gen.Entities) { _entManager.SpawnEntity(ent, gridPos); } } if (validTiles.Count > 0) { await SuspendIfOutOfTime(); ValidateResume(); } validTiles.Clear(); } } } /* * 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; 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.TileSize / 2f - 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, CollisionLayer, CollisionMask)) continue; validTiles.Add(windowTile); } validTiles.Sort((x, y) => ((Vector2) x + grid.TileSize / 2f - room.Center).LengthSquared.CompareTo(((Vector2) y + grid.TileSize / 2f - 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, new Tile(_tileDefManager[gen.Tile].TileId)); foreach (var ent in gen.Entities) { _entManager.SpawnEntity(ent, gridPos); } } if (validTiles.Count > 0) { await SuspendIfOutOfTime(); ValidateResume(); } validTiles.Clear(); } } } 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, CollisionLayer, 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 tile = new Tile(_tileDefManager[gen.Tile].TileId); 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.TileSize / 2f; } 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.TileSize / 2f - 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, CollisionLayer, CollisionMask)) continue; width--; grid.SetTile(node, tile); if (gen.EdgeEntities != null && nodeDistances.Count - i <= 2) { foreach (var ent in gen.EdgeEntities) { _entManager.SpawnEntity(ent, gridPos); } } else { // Iterate neighbors and check for blockers, if so bulldoze ClearDoor(dungeon, grid, node); foreach (var ent in gen.Entities) { _entManager.SpawnEntity(ent, gridPos); } } if (width == 0) break; } conns.Add(otherRoom); var otherConns = roomConnections.GetOrNew(otherRoom); otherConns.Add(room); await SuspendIfOutOfTime(); ValidateResume(); } } } /// /// 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; foreach (var ent in _lookup.GetEntitiesIntersecting(_gridUid, neighbor, flags)) { if (!physicsQuery.TryGetComponent(ent, out var physics) || (CollisionMask & physics.CollisionLayer) == 0x0 && (CollisionLayer & physics.CollisionMask) == 0x0) { continue; } _entManager.DeleteEntity(ent); } } } } private async Task PostGen(WallMountPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random) { var tileDef = new Tile(_tileDefManager[gen.Tile].TileId); var checkedTiles = new HashSet(); foreach (var room in dungeon.Rooms) { foreach (var tile in room.Tiles) { for (var x = -1; x <= 1; x++) { for (var y = -1; y <= 1; y++) { if (x != 0 && y != 0) { continue; } var neighbor = new Vector2i(tile.X + x, tile.Y + y); // Occupado if (dungeon.RoomTiles.Contains(neighbor) || checkedTiles.Contains(neighbor) || !_anchorable.TileFree(grid, neighbor, CollisionLayer, CollisionMask)) continue; if (!random.Prob(gen.Prob) || !checkedTiles.Add(neighbor)) continue; grid.SetTile(neighbor, tileDef); var gridPos = grid.GridTileToLocal(neighbor); foreach (var ent in EntitySpawnCollection.GetSpawns(gen.Spawns, random)) { _entManager.SpawnEntity(ent, gridPos); } } } } } } }