Salvage dungeons (#14520)

This commit is contained in:
metalgearsloth
2023-03-10 16:41:22 +11:00
committed by GitHub
parent 214ca06997
commit 6157dfa3c0
145 changed files with 24649 additions and 396 deletions

View File

@@ -0,0 +1,13 @@
using Robust.Shared.Utility;
namespace Content.Server.Procedural;
/// <summary>
/// Added to pre-loaded maps for dungeon templates.
/// </summary>
[RegisterComponent]
public sealed class DungeonAtlasTemplateComponent : Component
{
[DataField("path", required: true)]
public ResourcePath? Path;
}

View File

@@ -0,0 +1,429 @@
using System.Threading.Tasks;
using Content.Shared.Decals;
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;
public sealed partial class DungeonJob
{
private async Task<Dungeon> GeneratePrefabDungeon(PrefabDunGen prefab, EntityUid gridUid, MapGridComponent grid, int seed)
{
var random = new Random(seed);
var preset = prefab.Presets[random.Next(prefab.Presets.Count)];
var gen = _prototype.Index<DungeonPresetPrototype>(preset);
var dungeonRotation = _dungeon.GetDungeonRotation(seed);
var dungeonTransform = Matrix3.CreateTransform(_position, dungeonRotation);
var roomPackProtos = new Dictionary<Vector2i, List<DungeonRoomPackPrototype>>();
var externalNodes = new Dictionary<DungeonRoomPackPrototype, HashSet<Vector2i>>();
var fallbackTile = new Tile(_tileDefManager[prefab.Tile].TileId);
foreach (var pack in _prototype.EnumeratePrototypes<DungeonRoomPackPrototype>())
{
var size = pack.Size;
var sizePacks = roomPackProtos.GetOrNew(size);
sizePacks.Add(pack);
// Determine external connections; these are only valid when adjacent to a room node.
// We use this later to determine which room packs connect to each other
var nodes = new HashSet<Vector2i>();
externalNodes.Add(pack, nodes);
foreach (var room in pack.Rooms)
{
var rator = new Box2iEdgeEnumerator(room, false);
while (rator.MoveNext(out var index))
{
nodes.Add(index);
}
}
}
// Need to sort to make the RNG deterministic (at least without prototype changes).
foreach (var roomA in roomPackProtos.Values)
{
roomA.Sort((x, y) =>
string.Compare(x.ID, y.ID, StringComparison.Ordinal));
}
var roomProtos = new Dictionary<Vector2i, List<DungeonRoomPrototype>>();
foreach (var proto in _prototype.EnumeratePrototypes<DungeonRoomPrototype>())
{
var whitelisted = false;
foreach (var tag in prefab.RoomWhitelist)
{
if (proto.Tags.Contains(tag))
{
whitelisted = true;
break;
}
}
if (!whitelisted)
continue;
var size = proto.Size;
var sizeRooms = roomProtos.GetOrNew(size);
sizeRooms.Add(proto);
}
foreach (var roomA in roomProtos.Values)
{
roomA.Sort((x, y) =>
string.Compare(x.ID, y.ID, StringComparison.Ordinal));
}
// First we gather all of the edges for each roompack in the preset
// This allows us to determine which ones should connect from being adjacent
var edges = new HashSet<Vector2i>[gen.RoomPacks.Count];
for (var i = 0; i < gen.RoomPacks.Count; i++)
{
var pack = gen.RoomPacks[i];
var nodes = new HashSet<Vector2i>(pack.Width + 2 + pack.Height);
var rator = new Box2iEdgeEnumerator(pack, false);
while (rator.MoveNext(out var index))
{
nodes.Add(index);
}
edges[i] = nodes;
}
// Build up edge groups between each pack.
var connections = new Dictionary<int, Dictionary<int, HashSet<Vector2i>>>();
for (var i = 0; i < edges.Length; i++)
{
var nodes = edges[i];
var nodeConnections = connections.GetOrNew(i);
for (var j = i + 1; j < edges.Length; j++)
{
var otherNodes = edges[j];
var intersect = new HashSet<Vector2i>(nodes);
intersect.IntersectWith(otherNodes);
if (intersect.Count == 0)
continue;
nodeConnections[j] = intersect;
var otherNodeConnections = connections.GetOrNew(j);
otherNodeConnections[i] = intersect;
}
}
var tiles = new List<(Vector2i, Tile)>();
var dungeon = new Dungeon();
var availablePacks = new List<DungeonRoomPackPrototype>();
var chosenPacks = new DungeonRoomPackPrototype?[gen.RoomPacks.Count];
var packTransforms = new Matrix3[gen.RoomPacks.Count];
var packRotations = new Angle[gen.RoomPacks.Count];
var rotatedPackNodes = new HashSet<Vector2i>[gen.RoomPacks.Count];
// Actually pick the room packs and rooms
for (var i = 0; i < gen.RoomPacks.Count; i++)
{
var bounds = gen.RoomPacks[i];
var dimensions = new Vector2i(bounds.Width, bounds.Height);
// Try every pack rotation
if (roomPackProtos.TryGetValue(dimensions, out var roomPacks))
{
availablePacks.AddRange(roomPacks);
}
// Try rotated versions if there are any.
if (dimensions.X != dimensions.Y)
{
var rotatedDimensions = new Vector2i(dimensions.Y, dimensions.X);
if (roomPackProtos.TryGetValue(rotatedDimensions, out roomPacks))
{
availablePacks.AddRange(roomPacks);
}
}
// Iterate every pack
// To be valid it needs its edge nodes to overlap with every edge group
var external = connections[i];
random.Shuffle(availablePacks);
Matrix3 packTransform = default!;
var found = false;
DungeonRoomPackPrototype pack = default!;
foreach (var aPack in availablePacks)
{
var aExternal = externalNodes[aPack];
for (var j = 0; j < 4; j++)
{
var dir = (DirectionFlag) Math.Pow(2, j);
Vector2i aPackDimensions;
if ((dir & (DirectionFlag.East | DirectionFlag.West)) != 0x0)
{
aPackDimensions = new Vector2i(aPack.Size.Y, aPack.Size.X);
}
else
{
aPackDimensions = aPack.Size;
}
// Rotation doesn't match.
if (aPackDimensions != bounds.Size)
continue;
found = true;
var rotatedNodes = new HashSet<Vector2i>(aExternal.Count);
var aRotation = dir.AsDir().ToAngle();
// Get the external nodes in terms of the dungeon layout
// (i.e. rotated if necessary + translated to the room position)
foreach (var node in aExternal)
{
// Get the node in pack terms (offset from center), then rotate it
// Afterwards we offset it by where the pack is supposed to be in world terms.
var rotated = aRotation.RotateVec((Vector2) node + grid.TileSize / 2f - aPack.Size / 2f);
rotatedNodes.Add((rotated + bounds.Center).Floored());
}
foreach (var group in external.Values)
{
if (rotatedNodes.Overlaps(group))
continue;
found = false;
break;
}
if (!found)
{
continue;
}
// Use this pack
packTransform = Matrix3.CreateTransform(bounds.Center, aRotation);
packRotations[i] = aRotation;
rotatedPackNodes[i] = rotatedNodes;
pack = aPack;
break;
}
if (found)
break;
}
availablePacks.Clear();
// Oop
if (!found)
{
continue;
}
// If we're not the first pack then connect to our edges.
chosenPacks[i] = pack;
packTransforms[i] = packTransform;
}
// Then for overlaps choose either 1x1 / 3x1
// Pick a random tile for it and then expand outwards as relevant (weighted towards middle?)
for (var i = 0; i < chosenPacks.Length; i++)
{
var pack = chosenPacks[i]!;
var packTransform = packTransforms[i];
var packRotation = packRotations[i];
// Actual spawn cud here.
// Pickout the room pack template to get the room dimensions we need.
// TODO: Need to be able to load entities on top of other entities but das a lot of effo
var packCenter = (Vector2) pack.Size / 2;
foreach (var roomSize in pack.Rooms)
{
var roomDimensions = new Vector2i(roomSize.Width, roomSize.Height);
Angle roomRotation = Angle.Zero;
Matrix3 matty;
if (!roomProtos.TryGetValue(roomDimensions, out var roomProto))
{
roomDimensions = new Vector2i(roomDimensions.Y, roomDimensions.X);
if (!roomProtos.TryGetValue(roomDimensions, out roomProto))
{
Matrix3.Multiply(packTransform, dungeonTransform, out matty);
for (var x = roomSize.Left; x < roomSize.Right; x++)
{
for (var y = roomSize.Bottom; y < roomSize.Top; y++)
{
var index = matty.Transform(new Vector2(x, y) + grid.TileSize / 2f - packCenter).Floored();
tiles.Add((index, new Tile(_tileDefManager["FloorPlanetGrass"].TileId)));
}
}
grid.SetTiles(tiles);
tiles.Clear();
Logger.Error($"Unable to find room variant for {roomDimensions}, leaving empty.");
continue;
}
roomRotation = new Angle(Math.PI / 2);
Logger.Debug($"Using rotated variant for room");
}
if (roomDimensions.X == roomDimensions.Y)
{
// Give it a random rotation
roomRotation = random.Next(4) * Math.PI / 2;
}
else if (random.Next(2) == 1)
{
roomRotation += Math.PI;
}
var roomTransform = Matrix3.CreateTransform(roomSize.Center - packCenter, roomRotation);
var finalRoomRotation = roomRotation + packRotation + dungeonRotation;
Matrix3.Multiply(roomTransform, packTransform, out matty);
Matrix3.Multiply(matty, dungeonTransform, out var dungeonMatty);
var room = roomProto[random.Next(roomProto.Count)];
var roomMap = _dungeon.GetOrCreateTemplate(room);
var templateMapUid = _mapManager.GetMapEntityId(roomMap);
var templateGrid = _entManager.GetComponent<MapGridComponent>(templateMapUid);
var roomCenter = (room.Offset + room.Size / 2f) * grid.TileSize;
var roomTiles = new HashSet<Vector2i>(room.Size.X * room.Size.Y);
// Load tiles
for (var x = 0; x < room.Size.X; x++)
{
for (var y = 0; y < room.Size.Y; y++)
{
var indices = new Vector2i(x + room.Offset.X, y + room.Offset.Y);
var tileRef = templateGrid.GetTileRef(indices);
var tilePos = dungeonMatty.Transform((Vector2) indices + grid.TileSize / 2f - roomCenter);
var rounded = tilePos.Floored();
tiles.Add((rounded, tileRef.Tile));
roomTiles.Add(rounded);
}
}
var center = Vector2.Zero;
foreach (var tile in roomTiles)
{
center += ((Vector2) tile + grid.TileSize / 2f);
}
center /= roomTiles.Count;
dungeon.Rooms.Add(new DungeonRoom(roomTiles, center));
grid.SetTiles(tiles);
tiles.Clear();
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
var metaQuery = _entManager.GetEntityQuery<MetaDataComponent>();
// Load entities
// TODO: I don't think engine supports full entity copying so we do this piece of shit.
var bounds = new Box2(room.Offset, room.Offset + room.Size);
foreach (var templateEnt in _lookup.GetEntitiesIntersecting(templateMapUid, bounds, LookupFlags.Uncontained))
{
var templateXform = xformQuery.GetComponent(templateEnt);
var childPos = dungeonMatty.Transform(templateXform.LocalPosition - roomCenter);
var childRot = templateXform.LocalRotation + finalRoomRotation;
var protoId = metaQuery.GetComponent(templateEnt).EntityPrototype?.ID;
// TODO: Copy the templated entity as is with serv
var ent = _entManager.SpawnEntity(protoId,
new EntityCoordinates(gridUid, childPos));
var childXform = xformQuery.GetComponent(ent);
var anchored = templateXform.Anchored;
_transform.SetLocalRotation(ent, childRot, childXform);
// If the templated entity was anchored then anchor us too.
if (anchored && !childXform.Anchored)
_transform.AnchorEntity(ent, childXform, grid);
else if (!anchored && childXform.Anchored)
_transform.Unanchor(ent, childXform);
}
// Load decals
if (_entManager.TryGetComponent<DecalGridComponent>(templateMapUid, out var loadedDecals))
{
_entManager.EnsureComponent<DecalGridComponent>(gridUid);
foreach (var (_, decal) in _decals.GetDecalsIntersecting(templateMapUid, bounds, loadedDecals))
{
// 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 = dungeonMatty.Transform(decal.Coordinates + 0.5f - roomCenter);
position -= 0.5f;
// Umm uhh I love decals so uhhhh idk what to do about this
var angle = (decal.Angle + finalRoomRotation).Reduced();
// Adjust because 32x32 so we can't rotate cleanly
// Yeah idk about the uhh vectors here but it looked visually okay but they may still be off by 1.
// Also EyeManager.PixelsPerMeter should really be in shared.
if (angle.Equals(Math.PI))
{
position += new Vector2(-1f / 32f, 1f / 32f);
}
else if (angle.Equals(Math.PI * 1.5))
{
position += new Vector2(-1f / 32f, 0f);
}
else if (angle.Equals(Math.PI / 2f))
{
position += new Vector2(0f, 1f / 32f);
}
var tilePos = position.Floored();
// Fallback because uhhhhhhhh yeah, a corner tile might look valid on the original
// but place 1 nanometre off grid and fail the add.
if (!grid.TryGetTileRef(tilePos, out var tileRef) || tileRef.Tile.IsEmpty)
{
grid.SetTile(tilePos, fallbackTile);
}
var result = _decals.TryAddDecal(
decal.Id,
new EntityCoordinates(gridUid, position),
out _,
decal.Color,
angle,
decal.ZIndex,
decal.Cleanable);
DebugTools.Assert(result);
}
}
await SuspendIfOutOfTime();
ValidateResume();
}
}
return dungeon;
}
}

View File

@@ -0,0 +1,554 @@
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<DungeonRoom>(dungeon.Rooms);
var roomTiles = new List<Vector2i>();
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<Vector2i>();
for (var i = 0; i < 4; i++)
{
var dir = (DirectionFlag) Math.Pow(2, i);
var dirVec = dir.AsDir().ToIntVec();
foreach (var tile in room.Tiles)
{
var tileAngle = ((Vector2) tile + grid.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<Vector2i>();
for (var i = 0; i < 4; i++)
{
var dir = (DirectionFlag) Math.Pow(2, i);
var dirVec = dir.AsDir().ToIntVec();
foreach (var tile in room.Tiles)
{
var tileAngle = ((Vector2) tile + grid.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<DungeonRoom, HashSet<Vector2i>>(dungeon.Rooms.Count);
foreach (var room in dungeon.Rooms)
{
var roomEdges = new HashSet<Vector2i>();
foreach (var index in room.Tiles)
{
for (var x = -1; x <= 1; x++)
{
for (var y = -1; y <= 1; y++)
{
// Cardinals only
if (x != 0 && y != 0 ||
x == 0 && y == 0)
{
continue;
}
var neighbor = new Vector2i(index.X + x, index.Y + y);
if (dungeon.RoomTiles.Contains(neighbor))
continue;
if (!_anchorable.TileFree(grid, neighbor, 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<DungeonRoom, List<DungeonRoom>>();
var frontier = new Queue<DungeonRoom>();
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<Vector2i>(border);
flipp.IntersectWith(otherBorders);
if (flipp.Count == 0 ||
gen.OverlapCount != -1 && flipp.Count != gen.OverlapCount)
continue;
var center = Vector2.Zero;
foreach (var node in flipp)
{
center += (Vector2) node + grid.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();
}
}
}
/// <summary>
/// Removes any unwanted obstacles around a door tile.
/// </summary>
private void ClearDoor(Dungeon dungeon, MapGridComponent grid, Vector2i indices, bool strict = false)
{
var flags = strict
? LookupFlags.Dynamic | LookupFlags.Static | LookupFlags.StaticSundries
: LookupFlags.Dynamic | LookupFlags.Static;
var physicsQuery = _entManager.GetEntityQuery<PhysicsComponent>();
for (var x = -1; x <= 1; x++)
{
for (var y = -1; y <= 1; y++)
{
if (x != 0 && y != 0)
continue;
var neighbor = new Vector2i(indices.X + x, indices.Y + y);
if (!dungeon.RoomTiles.Contains(neighbor))
continue;
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<Vector2i>();
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);
}
}
}
}
}
}
}

View File

@@ -0,0 +1,139 @@
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Construction;
using Content.Server.CPUJob.JobQueues;
using Content.Server.Decals;
using Content.Shared.Procedural;
using Content.Shared.Procedural.DungeonGenerators;
using Content.Shared.Procedural.PostGeneration;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
namespace Content.Server.Procedural;
public sealed partial class DungeonJob : Job<Dungeon>
{
private readonly IEntityManager _entManager;
private readonly IMapManager _mapManager;
private readonly IPrototypeManager _prototype;
private readonly ITileDefinitionManager _tileDefManager;
private readonly AnchorableSystem _anchorable;
private readonly DecalSystem _decals;
private readonly DungeonSystem _dungeon;
private readonly EntityLookupSystem _lookup;
private readonly SharedTransformSystem _transform;
private readonly DungeonConfigPrototype _gen;
private readonly int _seed;
private readonly Vector2 _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,
SharedTransformSystem transform,
DungeonConfigPrototype gen,
MapGridComponent grid,
EntityUid gridUid,
int seed,
Vector2 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;
_transform = transform;
_gen = gen;
_grid = grid;
_gridUid = gridUid;
_seed = seed;
_position = position;
}
protected override async Task<Dungeon?> Process()
{
Dungeon dungeon;
_sawmill.Info($"Generating dungeon {_gen.ID} with seed {_seed} on {_entManager.ToPrettyString(_gridUid)}");
switch (_gen.Generator)
{
case PrefabDunGen prefab:
dungeon = await GeneratePrefabDungeon(prefab, _gridUid, _grid, _seed);
break;
default:
throw new NotImplementedException();
}
foreach (var room in dungeon.Rooms)
{
dungeon.RoomTiles.UnionWith(room.Tiles);
}
// 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 MiddleConnectionPostGen dordor:
await PostGen(dordor, dungeon, _gridUid, _grid, random);
break;
case EntrancePostGen 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 BoundaryWallPostGen boundary:
await PostGen(boundary, dungeon, _gridUid, _grid, random);
break;
case WallMountPostGen wall:
await PostGen(wall, dungeon, _gridUid, _grid, random);
break;
default:
throw new NotImplementedException();
}
await SuspendIfOutOfTime();
ValidateResume();
}
return dungeon;
}
private bool ValidateResume()
{
if (_entManager.Deleted(_gridUid))
return false;
return true;
}
}

View File

@@ -0,0 +1,102 @@
using System.Threading.Tasks;
using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.Procedural;
using Content.Shared.Procedural.DungeonGenerators;
using Robust.Shared.Console;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
namespace Content.Server.Procedural;
public sealed partial class DungeonSystem
{
/// <summary>
/// Generates a dungeon via command.
/// </summary>
[AdminCommand(AdminFlags.Fun)]
private async void GenerateDungeon(IConsoleShell shell, string argstr, string[] args)
{
if (args.Length < 4)
{
shell.WriteError("cmd-dungen-arg-count");
return;
}
if (!int.TryParse(args[0], out var mapInt))
{
shell.WriteError("cmd-dungen-map-parse");
return;
}
var mapId = new MapId(mapInt);
var mapUid = _mapManager.GetMapEntityId(mapId);
if (!TryComp<MapGridComponent>(mapUid, out var mapGrid))
{
shell.WriteError(Loc.GetString("cmd-dungen-mapgrid"));
return;
}
if (!_prototype.TryIndex<DungeonConfigPrototype>(args[1], out var dungeon))
{
shell.WriteError(Loc.GetString("cmd-dungen-config"));
return;
}
if (!int.TryParse(args[2], out var posX) || !int.TryParse(args[3], out var posY))
{
shell.WriteError(Loc.GetString("cmd-dungen-pos"));
return;
}
var position = new Vector2(posX, posY);
int seed;
if (args.Length >= 5)
{
if (!int.TryParse(args[4], out seed))
{
shell.WriteError(Loc.GetString("cmd-dungen-seed"));
return;
}
}
else
{
seed = new Random().Next();
}
shell.WriteLine(Loc.GetString("cmd-dungen-start", ("seed", seed)));
GenerateDungeon(dungeon, mapUid, mapGrid, position, seed);
}
private CompletionResult CompletionCallback(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
return CompletionResult.FromHintOptions(CompletionHelper.MapIds(EntityManager), Loc.GetString("cmd-dungen-hint-map"));
}
if (args.Length == 2)
{
return CompletionResult.FromHintOptions(CompletionHelper.PrototypeIDs<DungeonConfigPrototype>(proto: _prototype), Loc.GetString("cmd-dungen-hint-config"));
}
if (args.Length == 3)
{
return CompletionResult.FromHint(Loc.GetString("cmd-dungen-hint-posx"));
}
if (args.Length == 4)
{
return CompletionResult.FromHint(Loc.GetString("cmd-dungen-hint-posy"));
}
if (args.Length == 5)
{
return CompletionResult.FromHint(Loc.GetString("cmd-dungen-hint-seed"));
}
return CompletionResult.Empty;
}
}

View File

@@ -0,0 +1,236 @@
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Construction;
using Content.Server.CPUJob.JobQueues.Queues;
using Content.Server.Decals;
using Content.Server.GameTicking.Events;
using Content.Shared.CCVar;
using Content.Shared.Procedural;
using Robust.Server.GameObjects;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
namespace Content.Server.Procedural;
public sealed partial class DungeonSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly IConsoleHost _console = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefManager = default!;
[Dependency] private readonly AnchorableSystem _anchorable = default!;
[Dependency] private readonly DecalSystem _decals = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly MapLoaderSystem _loader = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
private ISawmill _sawmill = default!;
private const double DungeonJobTime = 0.005;
private readonly JobQueue _dungeonJobQueue = new(DungeonJobTime);
private readonly Dictionary<DungeonJob, CancellationTokenSource> _dungeonJobs = new();
public override void Initialize()
{
base.Initialize();
_sawmill = Logger.GetSawmill("dungen");
_console.RegisterCommand("dungen", Loc.GetString("cmd-dungen-desc"), Loc.GetString("cmd-dungen-help"), GenerateDungeon, CompletionCallback);
_prototype.PrototypesReloaded += PrototypeReload;
SubscribeLocalEvent<RoundStartingEvent>(OnRoundStart);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
_dungeonJobQueue.Process();
}
private void OnRoundStart(RoundStartingEvent ev)
{
foreach (var token in _dungeonJobs.Values)
{
token.Cancel();
}
_dungeonJobs.Clear();
var query = AllEntityQuery<DungeonAtlasTemplateComponent>();
while (query.MoveNext(out var uid, out _))
{
QueueDel(uid);
}
if (!_configManager.GetCVar(CCVars.ProcgenPreload))
return;
// Force all templates to be setup.
foreach (var room in _prototype.EnumeratePrototypes<DungeonRoomPrototype>())
{
GetOrCreateTemplate(room);
}
}
public override void Shutdown()
{
base.Shutdown();
_prototype.PrototypesReloaded -= PrototypeReload;
foreach (var token in _dungeonJobs.Values)
{
token.Cancel();
}
_dungeonJobs.Clear();
}
private void PrototypeReload(PrototypesReloadedEventArgs obj)
{
if (!obj.ByType.TryGetValue(typeof(DungeonRoomPrototype), out var rooms))
{
return;
}
foreach (var proto in rooms.Modified.Values)
{
var roomProto = (DungeonRoomPrototype) proto;
var query = AllEntityQuery<DungeonAtlasTemplateComponent>();
while (query.MoveNext(out var uid, out var comp))
{
if (!roomProto.AtlasPath.Equals(comp.Path))
continue;
QueueDel(uid);
break;
}
}
if (!_configManager.GetCVar(CCVars.ProcgenPreload))
return;
foreach (var proto in rooms.Modified.Values)
{
var roomProto = (DungeonRoomPrototype) proto;
var query = AllEntityQuery<DungeonAtlasTemplateComponent>();
var found = false;
while (query.MoveNext(out var comp))
{
if (!roomProto.AtlasPath.Equals(comp.Path))
continue;
found = true;
break;
}
if (!found)
{
GetOrCreateTemplate(roomProto);
}
}
}
public MapId GetOrCreateTemplate(DungeonRoomPrototype proto)
{
var query = AllEntityQuery<DungeonAtlasTemplateComponent>();
DungeonAtlasTemplateComponent? comp;
while (query.MoveNext(out var uid, out comp))
{
// Exists
if (comp.Path?.Equals(proto.AtlasPath) == true)
return Transform(uid).MapID;
}
var mapId = _mapManager.CreateMap();
_loader.Load(mapId, proto.AtlasPath.ToString());
var mapUid = _mapManager.GetMapEntityId(mapId);
_mapManager.SetMapPaused(mapId, true);
comp = AddComp<DungeonAtlasTemplateComponent>(mapUid);
comp.Path = proto.AtlasPath;
return mapId;
}
public void GenerateDungeon(DungeonConfigPrototype gen,
EntityUid gridUid,
MapGridComponent grid,
Vector2 position,
int seed)
{
var cancelToken = new CancellationTokenSource();
var job = new DungeonJob(
_sawmill,
DungeonJobTime,
EntityManager,
_mapManager,
_prototype,
_tileDefManager,
_anchorable,
_decals,
this,
_lookup,
_transform,
gen,
grid,
gridUid,
seed,
position,
cancelToken.Token);
_dungeonJobs.Add(job, cancelToken);
_dungeonJobQueue.EnqueueJob(job);
job.Run();
}
public async Task<Dungeon> GenerateDungeonAsync(
DungeonConfigPrototype gen,
EntityUid gridUid,
MapGridComponent grid,
Vector2 position,
int seed)
{
var cancelToken = new CancellationTokenSource();
var job = new DungeonJob(
_sawmill,
DungeonJobTime,
EntityManager,
_mapManager,
_prototype,
_tileDefManager,
_anchorable,
_decals,
this,
_lookup,
_transform,
gen,
grid,
gridUid,
seed,
position,
cancelToken.Token);
_dungeonJobs.Add(job, cancelToken);
_dungeonJobQueue.EnqueueJob(job);
job.Run();
await job.AsTask;
if (job.Exception != null)
{
throw job.Exception;
}
return job.Result!;
}
public Angle GetDungeonRotation(int seed)
{
// Mask 0 | 1 for rotation seed
var dungeonRotationSeed = 3 & seed;
return Math.PI / 2 * dungeonRotationSeed;
}
}