Salvage dungeons (#14520)
This commit is contained in:
13
Content.Server/Procedural/DungeonAtlasTemplateComponent.cs
Normal file
13
Content.Server/Procedural/DungeonAtlasTemplateComponent.cs
Normal 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;
|
||||
}
|
||||
429
Content.Server/Procedural/DungeonJob.Generator.cs
Normal file
429
Content.Server/Procedural/DungeonJob.Generator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
554
Content.Server/Procedural/DungeonJob.PostGen.cs
Normal file
554
Content.Server/Procedural/DungeonJob.PostGen.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
139
Content.Server/Procedural/DungeonJob.cs
Normal file
139
Content.Server/Procedural/DungeonJob.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
102
Content.Server/Procedural/DungeonSystem.Commands.cs
Normal file
102
Content.Server/Procedural/DungeonSystem.Commands.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
236
Content.Server/Procedural/DungeonSystem.cs
Normal file
236
Content.Server/Procedural/DungeonSystem.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user