VGRoid support (#27659)

* Dungeon spawn support for grid spawns

* Recursive dungeons working

* Mask approach working

* zack

* More work

* Fix recursive dungeons

* Heap of work

* weh

* the cud

* rar

* Job

* weh

* weh

* weh

* Master merges

* orch

* weh

* vgroid most of the work

* Tweaks

* Tweaks

* weh

* do do do do do do

* Basic layout

* Ore spawning working

* Big breaking changes

* Mob gen working

* weh

* Finalising

* emo

* More finalising

* reverty

* Reduce distance
This commit is contained in:
metalgearsloth
2024-07-03 22:23:11 +10:00
committed by GitHub
parent 1faa1b5df6
commit a2f99cc69e
103 changed files with 4928 additions and 2627 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,138 +0,0 @@
using System.Threading.Tasks;
using Content.Server.Parallax;
using Content.Shared.Parallax.Biomes;
using Content.Shared.Parallax.Biomes.Markers;
using Content.Shared.Procedural;
using Content.Shared.Procedural.PostGeneration;
using Content.Shared.Random.Helpers;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Utility;
namespace Content.Server.Procedural;
public sealed partial class DungeonJob
{
/*
* Handles PostGen code for marker layers + biomes.
*/
private async Task PostGen(BiomePostGen postGen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random)
{
if (_entManager.TryGetComponent(gridUid, out BiomeComponent? biomeComp))
return;
biomeComp = _entManager.AddComponent<BiomeComponent>(gridUid);
var biomeSystem = _entManager.System<BiomeSystem>();
biomeSystem.SetTemplate(gridUid, biomeComp, _prototype.Index(postGen.BiomeTemplate));
var seed = random.Next();
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
foreach (var node in dungeon.RoomTiles)
{
// Need to set per-tile to override data.
if (biomeSystem.TryGetTile(node, biomeComp.Layers, seed, grid, out var tile))
{
_maps.SetTile(gridUid, grid, node, tile.Value);
}
if (biomeSystem.TryGetDecals(node, biomeComp.Layers, seed, grid, out var decals))
{
foreach (var decal in decals)
{
_decals.TryAddDecal(decal.ID, new EntityCoordinates(gridUid, decal.Position), out _);
}
}
if (biomeSystem.TryGetEntity(node, biomeComp, grid, out var entityProto))
{
var ent = _entManager.SpawnEntity(entityProto, new EntityCoordinates(gridUid, node + grid.TileSizeHalfVector));
var xform = xformQuery.Get(ent);
if (!xform.Comp.Anchored)
{
_transform.AnchorEntity(ent, xform);
}
// TODO: Engine bug with SpawnAtPosition
DebugTools.Assert(xform.Comp.Anchored);
}
await SuspendIfOutOfTime();
ValidateResume();
}
biomeComp.Enabled = false;
}
private async Task PostGen(BiomeMarkerLayerPostGen postGen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random)
{
if (!_entManager.TryGetComponent(gridUid, out BiomeComponent? biomeComp))
return;
var biomeSystem = _entManager.System<BiomeSystem>();
var weightedRandom = _prototype.Index(postGen.MarkerTemplate);
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
var templates = new Dictionary<string, int>();
for (var i = 0; i < postGen.Count; i++)
{
var template = weightedRandom.Pick(random);
var count = templates.GetOrNew(template);
count++;
templates[template] = count;
}
foreach (var (template, count) in templates)
{
var markerTemplate = _prototype.Index<BiomeMarkerLayerPrototype>(template);
var bounds = new Box2i();
foreach (var tile in dungeon.RoomTiles)
{
bounds = bounds.UnionTile(tile);
}
await SuspendIfOutOfTime();
ValidateResume();
biomeSystem.GetMarkerNodes(gridUid, biomeComp, grid, markerTemplate, true, bounds, count,
random, out var spawnSet, out var existing, false);
await SuspendIfOutOfTime();
ValidateResume();
foreach (var ent in existing)
{
_entManager.DeleteEntity(ent);
}
await SuspendIfOutOfTime();
ValidateResume();
foreach (var (node, mask) in spawnSet)
{
string? proto;
if (mask != null && markerTemplate.EntityMask.TryGetValue(mask, out var maskedProto))
{
proto = maskedProto;
}
else
{
proto = markerTemplate.Prototype;
}
var ent = _entManager.SpawnAtPosition(proto, new EntityCoordinates(gridUid, node + grid.TileSizeHalfVector));
var xform = xformQuery.Get(ent);
if (!xform.Comp.Anchored)
_transform.AnchorEntity(ent, xform);
await SuspendIfOutOfTime();
ValidateResume();
}
}
}
}

View File

@@ -1,192 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Construction;
using Robust.Shared.CPUJob.JobQueues;
using Content.Server.Decals;
using Content.Shared.Construction.EntitySystems;
using Content.Shared.Maps;
using Content.Shared.Procedural;
using Content.Shared.Procedural.DungeonGenerators;
using Content.Shared.Procedural.PostGeneration;
using Content.Shared.Tag;
using Robust.Server.Physics;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Server.Procedural;
public sealed partial class DungeonJob : Job<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 TagSystem _tag;
private readonly TileSystem _tile;
private readonly SharedMapSystem _maps;
private readonly SharedTransformSystem _transform;
private readonly DungeonConfigPrototype _gen;
private readonly int _seed;
private readonly Vector2i _position;
private readonly MapGridComponent _grid;
private readonly EntityUid _gridUid;
private readonly ISawmill _sawmill;
public DungeonJob(
ISawmill sawmill,
double maxTime,
IEntityManager entManager,
IMapManager mapManager,
IPrototypeManager prototype,
ITileDefinitionManager tileDefManager,
AnchorableSystem anchorable,
DecalSystem decals,
DungeonSystem dungeon,
EntityLookupSystem lookup,
TagSystem tag,
TileSystem tile,
SharedTransformSystem transform,
DungeonConfigPrototype gen,
MapGridComponent grid,
EntityUid gridUid,
int seed,
Vector2i position,
CancellationToken cancellation = default) : base(maxTime, cancellation)
{
_sawmill = sawmill;
_entManager = entManager;
_mapManager = mapManager;
_prototype = prototype;
_tileDefManager = tileDefManager;
_anchorable = anchorable;
_decals = decals;
_dungeon = dungeon;
_lookup = lookup;
_tag = tag;
_tile = tile;
_maps = _entManager.System<SharedMapSystem>();
_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)}");
_grid.CanSplit = false;
switch (_gen.Generator)
{
case NoiseDunGen noise:
dungeon = await GenerateNoiseDungeon(noise, _gridUid, _grid, _seed);
break;
case PrefabDunGen prefab:
dungeon = await GeneratePrefabDungeon(prefab, _gridUid, _grid, _seed);
DebugTools.Assert(dungeon.RoomExteriorTiles.Count > 0);
break;
default:
throw new NotImplementedException();
}
DebugTools.Assert(dungeon.RoomTiles.Count > 0);
// To make it slightly more deterministic treat this RNG as separate ig.
var random = new Random(_seed);
foreach (var post in _gen.PostGeneration)
{
_sawmill.Debug($"Doing postgen {post.GetType()} for {_gen.ID} with seed {_seed}");
switch (post)
{
case AutoCablingPostGen cabling:
await PostGen(cabling, dungeon, _gridUid, _grid, random);
break;
case BiomePostGen biome:
await PostGen(biome, dungeon, _gridUid, _grid, random);
break;
case BoundaryWallPostGen boundary:
await PostGen(boundary, dungeon, _gridUid, _grid, random);
break;
case CornerClutterPostGen clutter:
await PostGen(clutter, dungeon, _gridUid, _grid, random);
break;
case CorridorClutterPostGen corClutter:
await PostGen(corClutter, dungeon, _gridUid, _grid, random);
break;
case CorridorPostGen cordor:
await PostGen(cordor, dungeon, _gridUid, _grid, random);
break;
case CorridorDecalSkirtingPostGen decks:
await PostGen(decks, dungeon, _gridUid, _grid, random);
break;
case EntranceFlankPostGen flank:
await PostGen(flank, dungeon, _gridUid, _grid, random);
break;
case JunctionPostGen junc:
await PostGen(junc, dungeon, _gridUid, _grid, random);
break;
case MiddleConnectionPostGen dordor:
await PostGen(dordor, dungeon, _gridUid, _grid, random);
break;
case DungeonEntrancePostGen entrance:
await PostGen(entrance, dungeon, _gridUid, _grid, random);
break;
case ExternalWindowPostGen externalWindow:
await PostGen(externalWindow, dungeon, _gridUid, _grid, random);
break;
case InternalWindowPostGen internalWindow:
await PostGen(internalWindow, dungeon, _gridUid, _grid, random);
break;
case BiomeMarkerLayerPostGen markerPost:
await PostGen(markerPost, dungeon, _gridUid, _grid, random);
break;
case RoomEntrancePostGen rEntrance:
await PostGen(rEntrance, dungeon, _gridUid, _grid, random);
break;
case WallMountPostGen wall:
await PostGen(wall, dungeon, _gridUid, _grid, random);
break;
case WormCorridorPostGen worm:
await PostGen(worm, dungeon, _gridUid, _grid, random);
break;
default:
throw new NotImplementedException();
}
await SuspendIfOutOfTime();
if (!ValidateResume())
break;
}
// Defer splitting so they don't get spammed and so we don't have to worry about tracking the grid along the way.
_grid.CanSplit = true;
_entManager.System<GridFixtureSystem>().CheckSplits(_gridUid);
return dungeon;
}
private bool ValidateResume()
{
if (_entManager.Deleted(_gridUid))
return false;
return true;
}
}

View File

@@ -0,0 +1,58 @@
using System.Threading.Tasks;
using Content.Server.NPC.Pathfinding;
using Content.Shared.Maps;
using Content.Shared.Procedural;
using Content.Shared.Procedural.DungeonGenerators;
using Robust.Shared.Collections;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob
{
/// <summary>
/// <see cref="ExteriorDunGen"/>
/// </summary>
private async Task<List<Dungeon>> GenerateExteriorDungen(Vector2i position, ExteriorDunGen dungen, HashSet<Vector2i> reservedTiles, Random random)
{
DebugTools.Assert(_grid.ChunkCount > 0);
var aabb = new Box2i(_grid.LocalAABB.BottomLeft.Floored(), _grid.LocalAABB.TopRight.Floored());
var angle = random.NextAngle();
var distance = Math.Max(aabb.Width / 2f + 1f, aabb.Height / 2f + 1f);
var startTile = new Vector2i(0, (int) distance).Rotate(angle);
Vector2i? dungeonSpawn = null;
var pathfinder = _entManager.System<PathfindingSystem>();
// Gridcast
pathfinder.GridCast(startTile, position, tile =>
{
if (!_maps.TryGetTileRef(_gridUid, _grid, tile, out var tileRef) ||
tileRef.Tile.IsSpace(_tileDefManager))
{
return true;
}
dungeonSpawn = tile;
return false;
});
if (dungeonSpawn == null)
{
return new List<Dungeon>()
{
Dungeon.Empty
};
}
var config = _prototype.Index(dungen.Proto);
var nextSeed = random.Next();
var dungeons = await GetDungeons(dungeonSpawn.Value, config, config.Data, config.Layers, reservedTiles, nextSeed, new Random(nextSeed));
return dungeons;
}
}

View File

@@ -0,0 +1,50 @@
using System.Numerics;
using System.Threading.Tasks;
using Content.Shared.Procedural;
using Content.Shared.Procedural.DungeonGenerators;
namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob
{
/// <summary>
/// <see cref="FillGridDunGen"/>
/// </summary>
private async Task<Dungeon> GenerateFillDunGen(DungeonData data, HashSet<Vector2i> reservedTiles)
{
if (!data.Entities.TryGetValue(DungeonDataKey.Fill, out var fillEnt))
{
LogDataError(typeof(FillGridDunGen));
return Dungeon.Empty;
}
var roomTiles = new HashSet<Vector2i>();
var tiles = _maps.GetAllTilesEnumerator(_gridUid, _grid);
while (tiles.MoveNext(out var tileRef))
{
var tile = tileRef.Value.GridIndices;
if (reservedTiles.Contains(tile))
continue;
if (!_anchorable.TileFree(_grid, tile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
continue;
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile);
_entManager.SpawnEntity(fillEnt, gridPos);
roomTiles.Add(tile);
await SuspendDungeon();
if (!ValidateResume())
break;
}
var dungeon = new Dungeon();
var room = new DungeonRoom(roomTiles, Vector2.Zero, Box2i.Empty, new HashSet<Vector2i>());
dungeon.AddRoom(room);
return dungeon;
}
}

View File

@@ -4,19 +4,25 @@ using Content.Shared.Maps;
using Content.Shared.Procedural;
using Content.Shared.Procedural.DungeonGenerators;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Procedural;
namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob
{
private async Task<Dungeon> GenerateNoiseDungeon(NoiseDunGen dungen, EntityUid gridUid, MapGridComponent grid,
int seed)
/// <summary>
/// <see cref="NoiseDunGen"/>
/// </summary>
private async Task<Dungeon> GenerateNoiseDunGen(
Vector2i position,
NoiseDunGen dungen,
HashSet<Vector2i> reservedTiles,
int seed,
Random random)
{
var rand = new Random(seed);
var tiles = new List<(Vector2i, Tile)>();
var matrix = Matrix3Helpers.CreateTranslation(position);
foreach (var layer in dungen.Layers)
{
@@ -30,7 +36,7 @@ public sealed partial class DungeonJob
var frontier = new Queue<Vector2i>();
var rooms = new List<DungeonRoom>();
var tileCount = 0;
var tileCap = rand.NextGaussian(dungen.TileCap, dungen.CapStd);
var tileCap = random.NextGaussian(dungen.TileCap, dungen.CapStd);
var visited = new HashSet<Vector2i>();
while (iterations > 0 && tileCount < tileCap)
@@ -39,22 +45,22 @@ public sealed partial class DungeonJob
iterations--;
// Get a random exterior tile to start floodfilling from.
var edge = rand.Next(4);
var edge = random.Next(4);
Vector2i seedTile;
switch (edge)
{
case 0:
seedTile = new Vector2i(rand.Next(area.Left - 2, area.Right + 1), area.Bottom - 2);
seedTile = new Vector2i(random.Next(area.Left - 2, area.Right + 1), area.Bottom - 2);
break;
case 1:
seedTile = new Vector2i(area.Right + 1, rand.Next(area.Bottom - 2, area.Top + 1));
seedTile = new Vector2i(area.Right + 1, random.Next(area.Bottom - 2, area.Top + 1));
break;
case 2:
seedTile = new Vector2i(rand.Next(area.Left - 2, area.Right + 1), area.Top + 1);
seedTile = new Vector2i(random.Next(area.Left - 2, area.Right + 1), area.Top + 1);
break;
case 3:
seedTile = new Vector2i(area.Left - 2, rand.Next(area.Bottom - 2, area.Top + 1));
seedTile = new Vector2i(area.Left - 2, random.Next(area.Bottom - 2, area.Top + 1));
break;
default:
throw new ArgumentOutOfRangeException();
@@ -80,14 +86,20 @@ public sealed partial class DungeonJob
if (value < layer.Threshold)
continue;
roomArea = roomArea.UnionTile(node);
foundNoise = true;
noiseFill = true;
var tileDef = _tileDefManager[layer.Tile];
var variant = _tile.PickVariant((ContentTileDefinition) tileDef, rand);
tiles.Add((node, new Tile(tileDef.TileId, variant: variant)));
roomTiles.Add(node);
// Still want the tile to gen as normal but can't do anything with it.
if (reservedTiles.Contains(node))
break;
roomArea = roomArea.UnionTile(node);
var tileDef = _tileDefManager[layer.Tile];
var variant = _tile.PickVariant((ContentTileDefinition) tileDef, random);
var adjusted = Vector2.Transform(node + _grid.TileSizeHalfVector, matrix).Floored();
tiles.Add((adjusted, new Tile(tileDef.TileId, variant: variant)));
roomTiles.Add(adjusted);
tileCount++;
break;
}
@@ -123,7 +135,7 @@ public sealed partial class DungeonJob
foreach (var tile in roomTiles)
{
center += tile + grid.TileSizeHalfVector;
center += tile + _grid.TileSizeHalfVector;
}
center /= roomTiles.Count;
@@ -132,15 +144,8 @@ public sealed partial class DungeonJob
ValidateResume();
}
grid.SetTiles(tiles);
_maps.SetTiles(_gridUid, _grid, tiles);
var dungeon = new Dungeon(rooms);
foreach (var tile in tiles)
{
dungeon.RoomTiles.Add(tile.Item1);
}
return dungeon;
}
}

View File

@@ -0,0 +1,112 @@
using System.Numerics;
using System.Threading.Tasks;
using Content.Shared.Maps;
using Content.Shared.Procedural;
using Content.Shared.Procedural.Distance;
using Content.Shared.Procedural.DungeonGenerators;
using Robust.Shared.Map;
namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob
{
/*
* See https://www.redblobgames.com/maps/terrain-from-noise/#islands
* Really it's just blending from the original noise (which may occupy the entire area)
* with some other shape to confine it into a bounds more naturally.
* https://old.reddit.com/r/proceduralgeneration/comments/kaen7h/new_video_on_procedural_island_noise_generation/gfjmgen/ also has more variations
*/
/// <summary>
/// <see cref="NoiseDistanceDunGen"/>
/// </summary>
private async Task<Dungeon> GenerateNoiseDistanceDunGen(
Vector2i position,
NoiseDistanceDunGen dungen,
HashSet<Vector2i> reservedTiles,
int seed,
Random random)
{
var tiles = new List<(Vector2i, Tile)>();
var matrix = Matrix3Helpers.CreateTranslation(position);
foreach (var layer in dungen.Layers)
{
layer.Noise.SetSeed(seed);
}
// First we have to find a seed tile, then floodfill from there until we get to noise
// at which point we floodfill the entire noise.
var area = Box2i.FromDimensions(-dungen.Size / 2, dungen.Size);
var roomTiles = new HashSet<Vector2i>();
var width = (float) area.Width;
var height = (float) area.Height;
for (var x = area.Left; x <= area.Right; x++)
{
for (var y = area.Bottom; y <= area.Top; y++)
{
var node = new Vector2i(x, y);
foreach (var layer in dungen.Layers)
{
var value = layer.Noise.GetNoise(node.X, node.Y);
if (dungen.DistanceConfig != null)
{
// Need to get dx - dx in a range from -1 -> 1
var dx = 2 * x / width;
var dy = 2 * y / height;
var distance = GetDistance(dx, dy, dungen.DistanceConfig);
value = MathHelper.Lerp(value, 1f - distance, dungen.DistanceConfig.BlendWeight);
}
if (value < layer.Threshold)
continue;
var tileDef = _tileDefManager[layer.Tile];
var variant = _tile.PickVariant((ContentTileDefinition) tileDef, random);
var adjusted = Vector2.Transform(node + _grid.TileSizeHalfVector, matrix).Floored();
// Do this down here because noise has a much higher chance of failing than reserved tiles.
if (reservedTiles.Contains(adjusted))
{
break;
}
tiles.Add((adjusted, new Tile(tileDef.TileId, variant: variant)));
roomTiles.Add(adjusted);
break;
}
}
await SuspendDungeon();
}
var room = new DungeonRoom(roomTiles, area.Center, area, new HashSet<Vector2i>());
_maps.SetTiles(_gridUid, _grid, tiles);
var dungeon = new Dungeon(new List<DungeonRoom>()
{
room,
});
await SuspendDungeon();
return dungeon;
}
private float GetDistance(float dx, float dy, IDunGenDistance distance)
{
switch (distance)
{
case DunGenEuclideanSquaredDistance:
return MathF.Min(1f, (dx * dx + dy * dy) / MathF.Sqrt(2));
case DunGenSquareBump:
return 1f - (1f - dx * dx) * (1f - dy * dy);
default:
throw new ArgumentOutOfRangeException();
}
}
}

View File

@@ -1,25 +1,33 @@
using System.Numerics;
using System.Threading.Tasks;
using Content.Shared.Decals;
using Content.Shared.Procedural;
using Content.Shared.Procedural.DungeonGenerators;
using Content.Shared.Whitelist;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Procedural;
namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob
{
private async Task<Dungeon> GeneratePrefabDungeon(PrefabDunGen prefab, EntityUid gridUid, MapGridComponent grid, int seed)
/// <summary>
/// <see cref="PrefabDunGen"/>
/// </summary>
private async Task<Dungeon> GeneratePrefabDunGen(Vector2i position, DungeonData data, PrefabDunGen prefab, HashSet<Vector2i> reservedTiles, Random random)
{
var random = new Random(seed);
var preset = prefab.Presets[random.Next(prefab.Presets.Count)];
var gen = _prototype.Index<DungeonPresetPrototype>(preset);
if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto) ||
!data.Whitelists.TryGetValue(DungeonDataKey.Rooms, out var roomWhitelist))
{
LogDataError(typeof(PrefabDunGen));
return Dungeon.Empty;
}
var dungeonRotation = _dungeon.GetDungeonRotation(seed);
var dungeonTransform = Matrix3Helpers.CreateTransform(_position, dungeonRotation);
var preset = prefab.Presets[random.Next(prefab.Presets.Count)];
var gen = _prototype.Index(preset);
var dungeonRotation = _dungeon.GetDungeonRotation(random.Next());
var dungeonTransform = Matrix3Helpers.CreateTransform(position, dungeonRotation);
var roomPackProtos = new Dictionary<Vector2i, List<DungeonRoomPackPrototype>>();
foreach (var pack in _prototype.EnumeratePrototypes<DungeonRoomPackPrototype>())
@@ -42,12 +50,15 @@ public sealed partial class DungeonJob
{
var whitelisted = false;
foreach (var tag in prefab.RoomWhitelist)
if (roomWhitelist?.Tags != null)
{
if (proto.Tags.Contains(tag))
foreach (var tag in roomWhitelist.Tags)
{
whitelisted = true;
break;
if (proto.Tags.Contains(tag))
{
whitelisted = true;
break;
}
}
}
@@ -182,12 +193,16 @@ public sealed partial class DungeonJob
{
for (var y = roomSize.Bottom; y < roomSize.Top; y++)
{
var index = Vector2.Transform(new Vector2(x, y) + grid.TileSizeHalfVector - packCenter, matty).Floored();
tiles.Add((index, new Tile(_tileDefManager["FloorPlanetGrass"].TileId)));
var index = Vector2.Transform(new Vector2(x, y) + _grid.TileSizeHalfVector - packCenter, matty).Floored();
if (reservedTiles.Contains(index))
continue;
tiles.Add((index, new Tile(_tileDefManager[tileProto].TileId)));
}
}
grid.SetTiles(tiles);
_maps.SetTiles(_gridUid, _grid, tiles);
tiles.Clear();
_sawmill.Error($"Unable to find room variant for {roomDimensions}, leaving empty.");
continue;
@@ -215,12 +230,12 @@ public sealed partial class DungeonJob
var dungeonMatty = Matrix3x2.Multiply(matty, dungeonTransform);
// The expensive bit yippy.
_dungeon.SpawnRoom(gridUid, grid, dungeonMatty, room);
_dungeon.SpawnRoom(_gridUid, _grid, dungeonMatty, room, reservedTiles);
var roomCenter = (room.Offset + room.Size / 2f) * grid.TileSize;
var roomCenter = (room.Offset + room.Size / 2f) * _grid.TileSize;
var roomTiles = new HashSet<Vector2i>(room.Size.X * room.Size.Y);
var exterior = new HashSet<Vector2i>(room.Size.X * 2 + room.Size.Y * 2);
var tileOffset = -roomCenter + grid.TileSizeHalfVector;
var tileOffset = -roomCenter + _grid.TileSizeHalfVector;
Box2i? mapBounds = null;
for (var x = -1; x <= room.Size.X; x++)
@@ -232,8 +247,12 @@ public sealed partial class DungeonJob
continue;
}
var tilePos = Vector2.Transform(new Vector2i(x + room.Offset.X, y + room.Offset.Y) + tileOffset, dungeonMatty);
exterior.Add(tilePos.Floored());
var tilePos = Vector2.Transform(new Vector2i(x + room.Offset.X, y + room.Offset.Y) + tileOffset, dungeonMatty).Floored();
if (reservedTiles.Contains(tilePos))
continue;
exterior.Add(tilePos);
}
}
@@ -249,16 +268,18 @@ public sealed partial class DungeonJob
roomTiles.Add(tileIndex);
mapBounds = mapBounds?.Union(tileIndex) ?? new Box2i(tileIndex, tileIndex);
center += tilePos + grid.TileSizeHalfVector;
center += tilePos + _grid.TileSizeHalfVector;
}
}
center /= roomTiles.Count;
dungeon.Rooms.Add(new DungeonRoom(roomTiles, center, mapBounds!.Value, exterior));
dungeon.AddRoom(new DungeonRoom(roomTiles, center, mapBounds!.Value, exterior));
await SuspendIfOutOfTime();
ValidateResume();
await SuspendDungeon();
if (!ValidateResume())
return Dungeon.Empty;
}
}
@@ -267,20 +288,16 @@ public sealed partial class DungeonJob
foreach (var room in dungeon.Rooms)
{
dungeon.RoomTiles.UnionWith(room.Tiles);
dungeon.RoomExteriorTiles.UnionWith(room.Exterior);
dungeonCenter += room.Center;
SetDungeonEntrance(dungeon, room, reservedTiles, random);
}
foreach (var room in dungeon.Rooms)
{
dungeonCenter += room.Center;
SetDungeonEntrance(dungeon, room, random);
}
dungeon.Rebuild();
return dungeon;
}
private void SetDungeonEntrance(Dungeon dungeon, DungeonRoom room, Random random)
private void SetDungeonEntrance(Dungeon dungeon, DungeonRoom room, HashSet<Vector2i> reservedTiles, Random random)
{
// TODO: Move to dungeonsystem.
@@ -323,8 +340,10 @@ public sealed partial class DungeonJob
continue;
}
if (reservedTiles.Contains(entrancePos))
continue;
room.Entrances.Add(entrancePos);
dungeon.Entrances.Add(entrancePos);
break;
}
}

View File

@@ -0,0 +1,60 @@
using System.Threading.Tasks;
using Content.Shared.Procedural;
using Content.Shared.Procedural.DungeonGenerators;
using Content.Shared.Procedural.PostGeneration;
using Robust.Shared.Map;
using Robust.Shared.Random;
namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob
{
/// <summary>
/// <see cref="ReplaceTileDunGen"/>
/// </summary>
private async Task<Dungeon> GenerateTileReplacementDunGen(ReplaceTileDunGen gen, DungeonData data, HashSet<Vector2i> reservedTiles, Random random)
{
var tiles = _maps.GetAllTilesEnumerator(_gridUid, _grid);
var replacements = new List<(Vector2i Index, Tile Tile)>();
var reserved = new HashSet<Vector2i>();
while (tiles.MoveNext(out var tileRef))
{
var node = tileRef.Value.GridIndices;
if (reservedTiles.Contains(node))
continue;
foreach (var layer in gen.Layers)
{
var value = layer.Noise.GetNoise(node.X, node.Y);
if (value < layer.Threshold)
continue;
Tile tile;
if (random.Prob(gen.VariantWeight))
{
tile = _tileDefManager.GetVariantTile(_prototype.Index(layer.Tile), random);
}
else
{
tile = new Tile(_prototype.Index(layer.Tile).TileId);
}
replacements.Add((node, tile));
reserved.Add(node);
break;
}
await SuspendDungeon();
}
_maps.SetTiles(_gridUid, _grid, replacements);
return new Dungeon(new List<DungeonRoom>()
{
new DungeonRoom(reserved, _position, Box2i.Empty, new HashSet<Vector2i>()),
});
}
}

View File

@@ -0,0 +1,58 @@
using System.Threading.Tasks;
using Content.Server.Ghost.Roles.Components;
using Content.Server.NPC.Components;
using Content.Server.NPC.Systems;
using Content.Shared.Physics;
using Content.Shared.Procedural;
using Content.Shared.Procedural.DungeonLayers;
using Content.Shared.Storage;
using Robust.Shared.Collections;
using Robust.Shared.Random;
namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob
{
private async Task PostGen(
MobsDunGen gen,
Dungeon dungeon,
Random random)
{
var availableRooms = new ValueList<DungeonRoom>();
availableRooms.AddRange(dungeon.Rooms);
var availableTiles = new ValueList<Vector2i>(dungeon.AllTiles);
var entities = EntitySpawnCollection.GetSpawns(gen.Groups, random);
var count = random.Next(gen.MinCount, gen.MaxCount + 1);
var npcs = _entManager.System<NPCSystem>();
for (var i = 0; i < count; i++)
{
while (availableTiles.Count > 0)
{
var tile = availableTiles.RemoveSwap(random.Next(availableTiles.Count));
if (!_anchorable.TileFree(_grid, tile, (int) CollisionGroup.MachineLayer,
(int) CollisionGroup.MachineLayer))
{
continue;
}
foreach (var ent in entities)
{
var uid = _entManager.SpawnAtPosition(ent, _maps.GridTileToLocal(_gridUid, _grid, tile));
_entManager.RemoveComponent<GhostRoleComponent>(uid);
_entManager.RemoveComponent<GhostTakeoverAvailableComponent>(uid);
npcs.SleepNPC(uid);
}
break;
}
await SuspendDungeon();
if (!ValidateResume())
return;
}
}
}

View File

@@ -0,0 +1,149 @@
using System.Threading.Tasks;
using Content.Shared.Procedural;
using Content.Shared.Procedural.Components;
using Content.Shared.Procedural.DungeonLayers;
using Robust.Shared.Collections;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob
{
/// <summary>
/// <see cref="OreDunGen"/>
/// </summary>
private async Task PostGen(
OreDunGen gen,
Dungeon dungeon,
Random random)
{
// Doesn't use dungeon data because layers and we don't need top-down support at the moment.
var emptyTiles = false;
var replaceEntities = new Dictionary<Vector2i, EntityUid>();
var availableTiles = new List<Vector2i>();
foreach (var node in dungeon.AllTiles)
{
// Empty tile, skip if relevant.
if (!emptyTiles && (!_maps.TryGetTile(_grid, node, out var tile) || tile.IsEmpty))
continue;
// Check if it's a valid spawn, if so then use it.
var enumerator = _maps.GetAnchoredEntitiesEnumerator(_gridUid, _grid, node);
var found = false;
// We use existing entities as a mark to spawn in place
// OR
// We check for any existing entities to see if we can spawn there.
while (enumerator.MoveNext(out var uid))
{
// We can't replace so just stop here.
if (gen.Replacement == null)
break;
var prototype = _entManager.GetComponent<MetaDataComponent>(uid.Value).EntityPrototype;
if (prototype?.ID == gen.Replacement)
{
replaceEntities[node] = uid.Value;
found = true;
break;
}
}
if (!found)
continue;
// Add it to valid nodes.
availableTiles.Add(node);
await SuspendDungeon();
if (!ValidateResume())
return;
}
var remapping = new Dictionary<EntProtoId, EntProtoId>();
// TODO: Move this to engine
if (_prototype.TryIndex(gen.Entity, out var proto) &&
proto.Components.TryGetComponent("EntityRemap", out var comps))
{
var remappingComp = (EntityRemapComponent) comps;
remapping = remappingComp.Mask;
}
var frontier = new ValueList<Vector2i>(32);
// Iterate the group counts and pathfind out each group.
for (var i = 0; i < gen.Count; i++)
{
await SuspendDungeon();
if (!ValidateResume())
return;
var groupSize = random.Next(gen.MinGroupSize, gen.MaxGroupSize + 1);
// While we have remaining tiles keep iterating
while (groupSize >= 0 && availableTiles.Count > 0)
{
var startNode = random.PickAndTake(availableTiles);
frontier.Clear();
frontier.Add(startNode);
// This essentially may lead to a vein being split in multiple areas but the count matters more than position.
while (frontier.Count > 0 && groupSize >= 0)
{
// Need to pick a random index so we don't just get straight lines of ores.
var frontierIndex = random.Next(frontier.Count);
var node = frontier[frontierIndex];
frontier.RemoveSwap(frontierIndex);
availableTiles.Remove(node);
// Add neighbors if they're valid, worst case we add no more and pick another random seed tile.
for (var x = -1; x <= 1; x++)
{
for (var y = -1; y <= 1; y++)
{
if (x != 0 && y != 0)
continue;
var neighbor = new Vector2i(node.X + x, node.Y + y);
if (frontier.Contains(neighbor) || !availableTiles.Contains(neighbor))
continue;
frontier.Add(neighbor);
}
}
var prototype = gen.Entity;
if (replaceEntities.TryGetValue(node, out var existingEnt))
{
var existingProto = _entManager.GetComponent<MetaDataComponent>(existingEnt).EntityPrototype;
_entManager.DeleteEntity(existingEnt);
if (existingProto != null && remapping.TryGetValue(existingProto.ID, out var remapped))
{
prototype = remapped;
}
}
// Tile valid salad so add it.
_entManager.SpawnAtPosition(prototype, _maps.GridTileToLocal(_gridUid, _grid, node));
groupSize--;
}
}
if (groupSize > 0)
{
_sawmill.Warning($"Found remaining group size for ore veins!");
}
}
}
}

View File

@@ -0,0 +1,134 @@
using System.Numerics;
using Content.Shared.Procedural;
using Robust.Shared.Collections;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Components;
namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob
{
/*
* Run after the main dungeon generation
*/
private bool HasWall(Vector2i tile)
{
var anchored = _maps.GetAnchoredEntitiesEnumerator(_gridUid, _grid, tile);
while (anchored.MoveNext(out var uid))
{
if (_tags.HasTag(uid.Value, "Wall"))
return true;
}
return false;
}
private void BuildCorridorExterior(Dungeon dungeon)
{
var exterior = dungeon.CorridorExteriorTiles;
// Just ignore entrances or whatever for now.
foreach (var tile in dungeon.CorridorTiles)
{
for (var x = -1; x <= 1; x++)
{
for (var y = -1; y <= 1; y++)
{
var neighbor = new Vector2i(tile.X + x, tile.Y + y);
if (dungeon.CorridorTiles.Contains(neighbor) ||
dungeon.RoomExteriorTiles.Contains(neighbor) ||
dungeon.RoomTiles.Contains(neighbor) ||
dungeon.Entrances.Contains(neighbor))
{
continue;
}
exterior.Add(neighbor);
}
}
}
}
private void WidenCorridor(Dungeon dungeon, float width, ICollection<Vector2i> corridorTiles)
{
var expansion = width - 2;
// Widen the path
if (expansion >= 1)
{
var toAdd = new ValueList<Vector2i>();
foreach (var node in corridorTiles)
{
// Uhhh not sure on the cleanest way to do this but tl;dr we don't want to hug
// exterior walls and make the path smaller.
for (var x = -expansion; x <= expansion; x++)
{
for (var y = -expansion; y <= expansion; y++)
{
var neighbor = new Vector2(node.X + x, node.Y + y).Floored();
// Diagonals still matter here.
if (dungeon.RoomTiles.Contains(neighbor) ||
dungeon.RoomExteriorTiles.Contains(neighbor))
{
// Try
continue;
}
toAdd.Add(neighbor);
}
}
}
foreach (var node in toAdd)
{
corridorTiles.Add(node);
}
}
}
/// <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;
for (var x = -1; x <= 1; x++)
{
for (var y = -1; y <= 1; y++)
{
if (x != 0 && y != 0)
continue;
var neighbor = new Vector2i(indices.X + x, indices.Y + y);
if (!dungeon.RoomTiles.Contains(neighbor))
continue;
// Shrink by 0.01 to avoid polygon overlap from neighboring tiles.
// TODO: Uhh entityset re-usage.
foreach (var ent in _lookup.GetEntitiesIntersecting(_gridUid, new Box2(neighbor * grid.TileSize, (neighbor + 1) * grid.TileSize).Enlarged(-0.1f), flags))
{
if (!_physicsQuery.TryGetComponent(ent, out var physics) ||
!physics.Hard ||
(DungeonSystem.CollisionMask & physics.CollisionLayer) == 0x0 &&
(DungeonSystem.CollisionLayer & physics.CollisionMask) == 0x0)
{
continue;
}
_entManager.DeleteEntity(ent);
}
}
}
}
}

View File

@@ -0,0 +1,162 @@
using System.Linq;
using System.Threading.Tasks;
using Content.Server.NodeContainer;
using Content.Shared.Procedural;
using Content.Shared.Procedural.PostGeneration;
using Robust.Shared.Random;
namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob
{
/// <summary>
/// <see cref="AutoCablingDunGen"/>
/// </summary>
private async Task PostGen(AutoCablingDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
{
if (!data.Entities.TryGetValue(DungeonDataKey.Cabling, out var ent))
{
LogDataError(typeof(AutoCablingDunGen));
return;
}
// There's a lot of ways you could do this.
// For now we'll just connect every LV cable in the dungeon.
var cableTiles = new HashSet<Vector2i>();
var allTiles = new HashSet<Vector2i>(dungeon.CorridorTiles);
allTiles.UnionWith(dungeon.RoomTiles);
allTiles.UnionWith(dungeon.RoomExteriorTiles);
allTiles.UnionWith(dungeon.CorridorExteriorTiles);
var nodeQuery = _entManager.GetEntityQuery<NodeContainerComponent>();
// Gather existing nodes
foreach (var tile in allTiles)
{
var anchored = _maps.GetAnchoredEntitiesEnumerator(_gridUid, _grid, tile);
while (anchored.MoveNext(out var anc))
{
if (!nodeQuery.TryGetComponent(anc, out var nodeContainer) ||
!nodeContainer.Nodes.ContainsKey("power"))
{
continue;
}
cableTiles.Add(tile);
break;
}
}
// Iterating them all might be expensive.
await SuspendDungeon();
if (!ValidateResume())
return;
var startNodes = new List<Vector2i>(cableTiles);
random.Shuffle(startNodes);
var start = startNodes[0];
var remaining = new HashSet<Vector2i>(startNodes);
var frontier = new PriorityQueue<Vector2i, float>();
frontier.Enqueue(start, 0f);
var cameFrom = new Dictionary<Vector2i, Vector2i>();
var costSoFar = new Dictionary<Vector2i, float>();
var lastDirection = new Dictionary<Vector2i, Direction>();
costSoFar[start] = 0f;
lastDirection[start] = Direction.Invalid;
while (remaining.Count > 0)
{
if (frontier.Count == 0)
{
var newStart = remaining.First();
frontier.Enqueue(newStart, 0f);
lastDirection[newStart] = Direction.Invalid;
}
var node = frontier.Dequeue();
if (remaining.Remove(node))
{
var weh = node;
while (cameFrom.TryGetValue(weh, out var receiver))
{
cableTiles.Add(weh);
weh = receiver;
if (weh == start)
break;
}
}
if (!_maps.TryGetTileRef(_gridUid, _grid, node, out var tileRef) || tileRef.Tile.IsEmpty)
{
continue;
}
for (var i = 0; i < 4; i++)
{
var dir = (Direction) (i * 2);
var neighbor = node + dir.ToIntVec();
var tileCost = 1f;
// Prefer straight lines.
if (lastDirection[node] != dir)
{
tileCost *= 1.1f;
}
if (cableTiles.Contains(neighbor))
{
tileCost *= 0.1f;
}
// Prefer tiles without walls on them
if (HasWall(neighbor))
{
tileCost *= 20f;
}
var gScore = costSoFar[node] + tileCost;
if (costSoFar.TryGetValue(neighbor, out var nextValue) && gScore >= nextValue)
{
continue;
}
cameFrom[neighbor] = node;
costSoFar[neighbor] = gScore;
lastDirection[neighbor] = dir;
frontier.Enqueue(neighbor, gScore);
}
}
foreach (var tile in cableTiles)
{
if (reservedTiles.Contains(tile))
continue;
var anchored = _maps.GetAnchoredEntitiesEnumerator(_gridUid, _grid, tile);
var found = false;
while (anchored.MoveNext(out var anc))
{
if (!nodeQuery.TryGetComponent(anc, out var nodeContainer) ||
!nodeContainer.Nodes.ContainsKey("power"))
{
continue;
}
found = true;
break;
}
if (found)
continue;
_entManager.SpawnEntity(ent, _maps.GridTileToLocal(_gridUid, _grid, tile));
}
}
}

View File

@@ -0,0 +1,67 @@
using System.Threading.Tasks;
using Content.Server.Parallax;
using Content.Shared.Parallax.Biomes;
using Content.Shared.Procedural;
using Content.Shared.Procedural.PostGeneration;
using Robust.Shared.Map;
using Robust.Shared.Utility;
namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob
{
/// <summary>
/// <see cref="BiomeDunGen"/>
/// </summary>
private async Task PostGen(BiomeDunGen dunGen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
{
if (_entManager.TryGetComponent(_gridUid, out BiomeComponent? biomeComp))
return;
biomeComp = _entManager.AddComponent<BiomeComponent>(_gridUid);
var biomeSystem = _entManager.System<BiomeSystem>();
biomeSystem.SetTemplate(_gridUid, biomeComp, _prototype.Index(dunGen.BiomeTemplate));
var seed = random.Next();
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
foreach (var node in dungeon.RoomTiles)
{
if (reservedTiles.Contains(node))
continue;
// Need to set per-tile to override data.
if (biomeSystem.TryGetTile(node, biomeComp.Layers, seed, _grid, out var tile))
{
_maps.SetTile(_gridUid, _grid, node, tile.Value);
}
if (biomeSystem.TryGetDecals(node, biomeComp.Layers, seed, _grid, out var decals))
{
foreach (var decal in decals)
{
_decals.TryAddDecal(decal.ID, new EntityCoordinates(_gridUid, decal.Position), out _);
}
}
if (biomeSystem.TryGetEntity(node, biomeComp, _grid, out var entityProto))
{
var ent = _entManager.SpawnEntity(entityProto, new EntityCoordinates(_gridUid, node + _grid.TileSizeHalfVector));
var xform = xformQuery.Get(ent);
if (!xform.Comp.Anchored)
{
_transform.AnchorEntity(ent, xform);
}
// TODO: Engine bug with SpawnAtPosition
DebugTools.Assert(xform.Comp.Anchored);
}
await SuspendDungeon();
if (!ValidateResume())
return;
}
biomeComp.Enabled = false;
}
}

View File

@@ -0,0 +1,105 @@
using System.Threading.Tasks;
using Content.Server.Parallax;
using Content.Shared.Parallax.Biomes;
using Content.Shared.Parallax.Biomes.Markers;
using Content.Shared.Procedural;
using Content.Shared.Procedural.PostGeneration;
using Content.Shared.Random.Helpers;
using Robust.Shared.Map;
using Robust.Shared.Utility;
namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob
{
/// <summary>
/// <see cref="BiomeMarkerLayerDunGen"/>
/// </summary>
private async Task PostGen(BiomeMarkerLayerDunGen dunGen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
{
// If we're adding biome then disable it and just use for markers.
if (_entManager.EnsureComponent(_gridUid, out BiomeComponent biomeComp))
{
biomeComp.Enabled = false;
}
var biomeSystem = _entManager.System<BiomeSystem>();
var weightedRandom = _prototype.Index(dunGen.MarkerTemplate);
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
var templates = new Dictionary<string, int>();
for (var i = 0; i < dunGen.Count; i++)
{
var template = weightedRandom.Pick(random);
var count = templates.GetOrNew(template);
count++;
templates[template] = count;
}
foreach (var (template, count) in templates)
{
var markerTemplate = _prototype.Index<BiomeMarkerLayerPrototype>(template);
var bounds = new Box2i();
foreach (var tile in dungeon.RoomTiles)
{
bounds = bounds.UnionTile(tile);
}
await SuspendDungeon();
if (!ValidateResume())
return;
biomeSystem.GetMarkerNodes(_gridUid, biomeComp, _grid, markerTemplate, true, bounds, count,
random, out var spawnSet, out var existing, false);
await SuspendDungeon();
if (!ValidateResume())
return;
var checkTile = reservedTiles.Count > 0;
foreach (var ent in existing)
{
if (checkTile && reservedTiles.Contains(_maps.LocalToTile(_gridUid, _grid, _xformQuery.GetComponent(ent).Coordinates)))
{
continue;
}
_entManager.DeleteEntity(ent);
await SuspendDungeon();
if (!ValidateResume())
return;
}
foreach (var (node, mask) in spawnSet)
{
if (reservedTiles.Contains(node))
continue;
string? proto;
if (mask != null && markerTemplate.EntityMask.TryGetValue(mask, out var maskedProto))
{
proto = maskedProto;
}
else
{
proto = markerTemplate.Prototype;
}
var ent = _entManager.SpawnAtPosition(proto, new EntityCoordinates(_gridUid, node + _grid.TileSizeHalfVector));
var xform = xformQuery.Get(ent);
if (!xform.Comp.Anchored)
_transform.AnchorEntity(ent, xform);
await SuspendDungeon();
if (!ValidateResume())
return;
}
}
}
}

View File

@@ -0,0 +1,113 @@
using System.Threading.Tasks;
using Content.Shared.Maps;
using Content.Shared.Procedural;
using Content.Shared.Procedural.PostGeneration;
using Robust.Shared.Map;
using Robust.Shared.Utility;
namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob
{
/// <summary>
/// <see cref="BoundaryWallDunGen"/>
/// </summary>
private async Task PostGen(BoundaryWallDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
{
if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var protoTileDef) ||
!data.Entities.TryGetValue(DungeonDataKey.Walls, out var wall))
{
_sawmill.Error($"Error finding dungeon data for {nameof(gen)}");
return;
}
var tileDef = _tileDefManager[protoTileDef];
var tiles = new List<(Vector2i Index, Tile Tile)>(dungeon.RoomExteriorTiles.Count);
if (!data.Entities.TryGetValue(DungeonDataKey.CornerWalls, out var cornerWall))
{
cornerWall = wall;
}
if (cornerWall == default)
{
cornerWall = wall;
}
// Spawn wall outline
// - Tiles first
foreach (var neighbor in dungeon.RoomExteriorTiles)
{
DebugTools.Assert(!dungeon.RoomTiles.Contains(neighbor));
if (dungeon.Entrances.Contains(neighbor))
continue;
if (!_anchorable.TileFree(_grid, neighbor, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
continue;
tiles.Add((neighbor, _tile.GetVariantTile((ContentTileDefinition) tileDef, random)));
}
foreach (var index in dungeon.CorridorExteriorTiles)
{
if (dungeon.RoomTiles.Contains(index))
continue;
if (!_anchorable.TileFree(_grid, index, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
continue;
tiles.Add((index, _tile.GetVariantTile((ContentTileDefinition)tileDef, random)));
}
_maps.SetTiles(_gridUid, _grid, tiles);
// Double iteration coz we bulk set tiles for speed.
for (var i = 0; i < tiles.Count; i++)
{
var index = tiles[i];
if (!_anchorable.TileFree(_grid, index.Index, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
continue;
// If no cardinal neighbors in dungeon then we're a corner.
var isCorner = true;
for (var x = -1; x <= 1; x++)
{
for (var y = -1; y <= 1; y++)
{
if (x != 0 && y != 0)
{
continue;
}
var neighbor = new Vector2i(index.Index.X + x, index.Index.Y + y);
if (dungeon.RoomTiles.Contains(neighbor) || dungeon.CorridorTiles.Contains(neighbor))
{
isCorner = false;
break;
}
}
if (!isCorner)
break;
}
if (isCorner)
_entManager.SpawnEntity(cornerWall, _maps.GridTileToLocal(_gridUid, _grid, index.Index));
if (!isCorner)
_entManager.SpawnEntity(wall, _maps.GridTileToLocal(_gridUid, _grid, index.Index));
if (i % 20 == 0)
{
await SuspendDungeon();
if (!ValidateResume())
return;
}
}
}
}

View File

@@ -0,0 +1,56 @@
using System.Threading.Tasks;
using Content.Shared.Procedural;
using Content.Shared.Procedural.PostGeneration;
using Content.Shared.Storage;
using Robust.Shared.Physics.Components;
using Robust.Shared.Random;
namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob
{
/// <summary>
/// <see cref="CornerClutterDunGen"/>
/// </summary>
private async Task PostGen(CornerClutterDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
{
if (!data.SpawnGroups.TryGetValue(DungeonDataKey.CornerClutter, out var corner))
{
_sawmill.Error(Environment.StackTrace);
return;
}
foreach (var tile in dungeon.CorridorTiles)
{
var blocked = _anchorable.TileFree(_grid, tile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask);
if (blocked)
continue;
// If at least 2 adjacent tiles are blocked consider it a corner
for (var i = 0; i < 4; i++)
{
var dir = (Direction) (i * 2);
blocked = HasWall(tile + dir.ToIntVec());
if (!blocked)
continue;
var nextDir = (Direction) ((i + 1) * 2 % 8);
blocked = HasWall(tile + nextDir.ToIntVec());
if (!blocked)
continue;
if (random.Prob(gen.Chance))
{
var coords = _maps.GridTileToLocal(_gridUid, _grid, tile);
var protos = EntitySpawnCollection.GetSpawns(_prototype.Index(corner).Entries, random);
_entManager.SpawnEntities(coords, protos);
}
break;
}
}
}
}

View File

@@ -0,0 +1,116 @@
using System.Numerics;
using System.Threading.Tasks;
using Content.Shared.Maps;
using Content.Shared.Procedural;
using Content.Shared.Procedural.PostGeneration;
using Robust.Shared.Map;
namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob
{
/// <summary>
/// <see cref="CorridorDunGen"/>
/// </summary>
private async Task PostGen(CorridorDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
{
if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto))
{
LogDataError(typeof(CorridorDunGen));
return;
}
var entrances = new List<Vector2i>(dungeon.Rooms.Count);
// Grab entrances
foreach (var room in dungeon.Rooms)
{
entrances.AddRange(room.Entrances);
}
var edges = _dungeon.MinimumSpanningTree(entrances, random);
await SuspendDungeon();
if (!ValidateResume())
return;
// TODO: Add in say 1/3 of edges back in to add some cyclic to it.
var expansion = gen.Width - 2;
// Okay so tl;dr is that we don't want to cut close to rooms as it might go from 3 width to 2 width suddenly
// So we will add a buffer range around each room to deter pathfinding there unless necessary
var deterredTiles = new HashSet<Vector2i>();
if (expansion >= 1)
{
foreach (var tile in dungeon.RoomExteriorTiles)
{
for (var x = -expansion; x <= expansion; x++)
{
for (var y = -expansion; y <= expansion; y++)
{
var neighbor = new Vector2(tile.X + x, tile.Y + y).Floored();
if (dungeon.RoomTiles.Contains(neighbor) ||
dungeon.RoomExteriorTiles.Contains(neighbor) ||
entrances.Contains(neighbor))
{
continue;
}
deterredTiles.Add(neighbor);
}
}
}
}
foreach (var room in dungeon.Rooms)
{
foreach (var entrance in room.Entrances)
{
// Just so we can still actually get in to the entrance we won't deter from a tile away from it.
var normal = (entrance + _grid.TileSizeHalfVector - room.Center).ToWorldAngle().GetCardinalDir().ToIntVec();
deterredTiles.Remove(entrance + normal);
}
}
var excludedTiles = new HashSet<Vector2i>(dungeon.RoomExteriorTiles);
excludedTiles.UnionWith(dungeon.RoomTiles);
var corridorTiles = new HashSet<Vector2i>();
_dungeon.GetCorridorNodes(corridorTiles, edges, gen.PathLimit, excludedTiles, tile =>
{
var mod = 1f;
if (corridorTiles.Contains(tile))
{
mod *= 0.1f;
}
if (deterredTiles.Contains(tile))
{
mod *= 2f;
}
return mod;
});
WidenCorridor(dungeon, gen.Width, corridorTiles);
var setTiles = new List<(Vector2i, Tile)>();
var tileDef = (ContentTileDefinition) _tileDefManager[tileProto];
foreach (var tile in corridorTiles)
{
if (reservedTiles.Contains(tile))
continue;
setTiles.Add((tile, _tile.GetVariantTile(tileDef, random)));
}
_maps.SetTiles(_gridUid, _grid, setTiles);
dungeon.CorridorTiles.UnionWith(corridorTiles);
dungeon.RefreshAllTiles();
BuildCorridorExterior(dungeon);
}
}

View File

@@ -2,16 +2,17 @@ using System.Threading.Tasks;
using Content.Shared.Procedural;
using Content.Shared.Procedural.PostGeneration;
using Content.Shared.Storage;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Components;
using Robust.Shared.Random;
namespace Content.Server.Procedural;
namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob
{
private async Task PostGen(CorridorClutterPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid,
Random random)
/// <summary>
/// <see cref="CorridorClutterDunGen"/>
/// </summary>
private async Task PostGen(CorridorClutterDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
{
var physicsQuery = _entManager.GetEntityQuery<PhysicsComponent>();
var count = (int) Math.Ceiling(dungeon.CorridorTiles.Count * gen.Chance);

View File

@@ -0,0 +1,124 @@
using System.Threading.Tasks;
using Content.Shared.Doors.Components;
using Content.Shared.Procedural;
using Content.Shared.Procedural.PostGeneration;
using Robust.Shared.Collections;
using Robust.Shared.Physics.Components;
using Robust.Shared.Utility;
namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob
{
/// <summary>
/// <see cref="CorridorDecalSkirtingDunGen"/>
/// </summary>
private async Task PostGen(CorridorDecalSkirtingDunGen decks, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
{
if (!data.Colors.TryGetValue(DungeonDataKey.Decals, out var color))
{
_sawmill.Error(Environment.StackTrace);
}
var directions = new ValueList<DirectionFlag>(4);
var pocketDirections = new ValueList<Direction>(4);
var doorQuery = _entManager.GetEntityQuery<DoorComponent>();
var physicsQuery = _entManager.GetEntityQuery<PhysicsComponent>();
var offset = -_grid.TileSizeHalfVector;
foreach (var tile in dungeon.CorridorTiles)
{
DebugTools.Assert(!dungeon.RoomTiles.Contains(tile));
directions.Clear();
// Do cardinals 1 step
// Do corners the other step
for (var i = 0; i < 4; i++)
{
var dir = (DirectionFlag) Math.Pow(2, i);
var neighbor = tile + dir.AsDir().ToIntVec();
var anc = _maps.GetAnchoredEntitiesEnumerator(_gridUid, _grid, neighbor);
while (anc.MoveNext(out var ent))
{
if (!physicsQuery.TryGetComponent(ent, out var physics) ||
!physics.CanCollide ||
!physics.Hard ||
doorQuery.HasComponent(ent.Value))
{
continue;
}
directions.Add(dir);
break;
}
}
// Pockets
if (directions.Count == 0)
{
pocketDirections.Clear();
for (var i = 1; i < 5; i++)
{
var dir = (Direction) (i * 2 - 1);
var neighbor = tile + dir.ToIntVec();
var anc = _maps.GetAnchoredEntitiesEnumerator(_gridUid, _grid, neighbor);
while (anc.MoveNext(out var ent))
{
if (!physicsQuery.TryGetComponent(ent, out var physics) ||
!physics.CanCollide ||
!physics.Hard ||
doorQuery.HasComponent(ent.Value))
{
continue;
}
pocketDirections.Add(dir);
break;
}
}
if (pocketDirections.Count == 1)
{
if (decks.PocketDecals.TryGetValue(pocketDirections[0], out var cDir))
{
// Decals not being centered biting my ass again
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile).Offset(offset);
_decals.TryAddDecal(cDir, gridPos, out _, color: color);
}
}
continue;
}
if (directions.Count == 1)
{
if (decks.CardinalDecals.TryGetValue(directions[0], out var cDir))
{
// Decals not being centered biting my ass again
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile).Offset(offset);
_decals.TryAddDecal(cDir, gridPos, out _, color: color);
}
continue;
}
// Corners
if (directions.Count == 2)
{
// Auehghegueugegegeheh help me
var dirFlag = directions[0] | directions[1];
if (decks.CornerDecals.TryGetValue(dirFlag, out var cDir))
{
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile).Offset(offset);
_decals.TryAddDecal(cDir, gridPos, out _, color: color);
}
}
}
}
}

View File

@@ -0,0 +1,6 @@
namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob
{
}

View File

@@ -0,0 +1,114 @@
using System.Threading.Tasks;
using Content.Shared.Maps;
using Content.Shared.Procedural;
using Content.Shared.Procedural.PostGeneration;
using Content.Shared.Storage;
using Robust.Shared.Random;
namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob
{
/// <summary>
/// <see cref="DungeonEntranceDunGen"/>
/// </summary>
private async Task PostGen(DungeonEntranceDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
{
if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto) ||
!data.SpawnGroups.TryGetValue(DungeonDataKey.Entrance, out var entrance))
{
LogDataError(typeof(DungeonEntranceDunGen));
return;
}
var rooms = new List<DungeonRoom>(dungeon.Rooms);
var roomTiles = new List<Vector2i>();
var tileDef = (ContentTileDefinition) _tileDefManager[tileProto];
for (var i = 0; i < gen.Count; i++)
{
var roomIndex = random.Next(rooms.Count);
var room = rooms[roomIndex];
// Move out 3 tiles in a direction away from center of the room
// If none of those intersect another tile it's probably external
// TODO: Maybe need to take top half of furthest rooms in case there's interior exits?
roomTiles.AddRange(room.Exterior);
random.Shuffle(roomTiles);
foreach (var tile in roomTiles)
{
var isValid = false;
// Check if one side is dungeon and the other side is nothing.
for (var j = 0; j < 4; j++)
{
var dir = (Direction) (j * 2);
var oppositeDir = dir.GetOpposite();
var dirVec = tile + dir.ToIntVec();
var oppositeDirVec = tile + oppositeDir.ToIntVec();
if (!dungeon.RoomTiles.Contains(dirVec))
{
continue;
}
if (dungeon.RoomTiles.Contains(oppositeDirVec) ||
dungeon.RoomExteriorTiles.Contains(oppositeDirVec) ||
dungeon.CorridorExteriorTiles.Contains(oppositeDirVec) ||
dungeon.CorridorTiles.Contains(oppositeDirVec))
{
continue;
}
// Check if exterior spot free.
if (!_anchorable.TileFree(_grid, tile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
{
continue;
}
// Check if interior spot free (no guarantees on exterior but ClearDoor should handle it)
if (!_anchorable.TileFree(_grid, dirVec, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
{
continue;
}
// Valid pick!
isValid = true;
// Entrance wew
_maps.SetTile(_gridUid, _grid, tile, _tile.GetVariantTile(tileDef, random));
ClearDoor(dungeon, _grid, tile);
var gridCoords = _maps.GridTileToLocal(_gridUid, _grid, tile);
// Need to offset the spawn to avoid spawning in the room.
foreach (var ent in EntitySpawnCollection.GetSpawns(_prototype.Index(entrance).Entries, random))
{
_entManager.SpawnAtPosition(ent, gridCoords);
}
// Clear out any biome tiles nearby to avoid blocking it
foreach (var nearTile in _maps.GetLocalTilesIntersecting(_gridUid, _grid, new Circle(gridCoords.Position, 1.5f), false))
{
if (dungeon.RoomTiles.Contains(nearTile.GridIndices) ||
dungeon.RoomExteriorTiles.Contains(nearTile.GridIndices) ||
dungeon.CorridorTiles.Contains(nearTile.GridIndices) ||
dungeon.CorridorExteriorTiles.Contains(nearTile.GridIndices))
{
continue;
}
_maps.SetTile(_gridUid, _grid, nearTile.GridIndices, _tile.GetVariantTile(tileDef, random));
}
break;
}
if (isValid)
break;
}
roomTiles.Clear();
}
}
}

View File

@@ -0,0 +1,58 @@
using System.Threading.Tasks;
using Content.Shared.Maps;
using Content.Shared.Procedural;
using Content.Shared.Procedural.PostGeneration;
using Content.Shared.Storage;
using Robust.Shared.Collections;
using Robust.Shared.Map;
namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob
{
/// <summary>
/// <see cref="EntranceFlankDunGen"/>
/// </summary>
private async Task PostGen(EntranceFlankDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
{
if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto) ||
!data.SpawnGroups.TryGetValue(DungeonDataKey.EntranceFlank, out var flankProto))
{
_sawmill.Error($"Unable to get dungeon data for {nameof(gen)}");
return;
}
var tiles = new List<(Vector2i Index, Tile)>();
var tileDef = _tileDefManager[tileProto];
var spawnPositions = new ValueList<Vector2i>(dungeon.Rooms.Count);
foreach (var room in dungeon.Rooms)
{
foreach (var entrance in room.Entrances)
{
for (var i = 0; i < 8; i++)
{
var dir = (Direction) i;
var neighbor = entrance + dir.ToIntVec();
if (!dungeon.RoomExteriorTiles.Contains(neighbor))
continue;
if (reservedTiles.Contains(neighbor))
continue;
tiles.Add((neighbor, _tile.GetVariantTile((ContentTileDefinition) tileDef, random)));
spawnPositions.Add(neighbor);
}
}
}
_maps.SetTiles(_gridUid, _grid, tiles);
var entGroup = _prototype.Index(flankProto);
foreach (var entrance in spawnPositions)
{
_entManager.SpawnEntities(_maps.GridTileToLocal(_gridUid, _grid, entrance), EntitySpawnCollection.GetSpawns(entGroup.Entries, random));
}
}
}

View File

@@ -0,0 +1,138 @@
using System.Linq;
using System.Threading.Tasks;
using Content.Shared.Maps;
using Content.Shared.Procedural;
using Content.Shared.Procedural.PostGeneration;
using Content.Shared.Storage;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Random;
namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob
{
// (Comment refers to internal & external).
/*
* You may be wondering why these are different.
* It's because for internals we want to force it as it looks nicer and not leave it up to chance.
*/
// TODO: Can probably combine these a bit, their differences are in really annoying to pull out spots.
/// <summary>
/// <see cref="ExternalWindowDunGen"/>
/// </summary>
private async Task PostGen(ExternalWindowDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
{
if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto) ||
!data.SpawnGroups.TryGetValue(DungeonDataKey.Window, out var windowGroup))
{
_sawmill.Error($"Unable to get dungeon data for {nameof(gen)}");
return;
}
// Iterate every tile with N chance to spawn windows on that wall per cardinal dir.
var chance = 0.25 / 3f;
var allExterior = new HashSet<Vector2i>(dungeon.CorridorExteriorTiles);
allExterior.UnionWith(dungeon.RoomExteriorTiles);
var validTiles = allExterior.ToList();
random.Shuffle(validTiles);
var tiles = new List<(Vector2i, Tile)>();
var tileDef = _tileDefManager[tileProto];
var count = Math.Floor(validTiles.Count * chance);
var index = 0;
var takenTiles = new HashSet<Vector2i>();
// There's a bunch of shit here but tl;dr
// - don't spawn over cap
// - Check if we have 3 tiles in a row that aren't corners and aren't obstructed
foreach (var tile in validTiles)
{
if (index > count)
break;
// Room tile / already used.
if (!_anchorable.TileFree(_grid, tile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask) ||
takenTiles.Contains(tile))
{
continue;
}
// Check we're not on a corner
for (var i = 0; i < 2; i++)
{
var dir = (Direction) (i * 2);
var dirVec = dir.ToIntVec();
var isValid = true;
// Check 1 beyond either side to ensure it's not a corner.
for (var j = -1; j < 4; j++)
{
var neighbor = tile + dirVec * j;
if (!allExterior.Contains(neighbor) ||
takenTiles.Contains(neighbor) ||
!_anchorable.TileFree(_grid, neighbor, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
{
isValid = false;
break;
}
// Also check perpendicular that it is free
foreach (var k in new [] {2, 6})
{
var perp = (Direction) ((i * 2 + k) % 8);
var perpVec = perp.ToIntVec();
var perpTile = tile + perpVec;
if (allExterior.Contains(perpTile) ||
takenTiles.Contains(neighbor) ||
!_anchorable.TileFree(_grid, perpTile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
{
isValid = false;
break;
}
}
if (!isValid)
break;
}
if (!isValid)
continue;
for (var j = 0; j < 3; j++)
{
var neighbor = tile + dirVec * j;
if (reservedTiles.Contains(neighbor))
continue;
tiles.Add((neighbor, _tile.GetVariantTile((ContentTileDefinition) tileDef, random)));
index++;
takenTiles.Add(neighbor);
}
}
}
_maps.SetTiles(_gridUid, _grid, tiles);
index = 0;
var spawnEntry = _prototype.Index(windowGroup);
foreach (var tile in tiles)
{
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile.Item1);
index += spawnEntry.Entries.Count;
_entManager.SpawnEntities(gridPos, EntitySpawnCollection.GetSpawns(spawnEntry.Entries, random));
await SuspendDungeon();
if (!ValidateResume())
return;
}
}
}

View File

@@ -0,0 +1,108 @@
using System.Numerics;
using System.Threading.Tasks;
using Content.Shared.Maps;
using Content.Shared.Procedural;
using Content.Shared.Procedural.PostGeneration;
using Content.Shared.Storage;
namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob
{
/// <summary>
/// <see cref="InternalWindowDunGen"/>
/// </summary>
private async Task PostGen(InternalWindowDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
{
if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto) ||
!data.SpawnGroups.TryGetValue(DungeonDataKey.Window, out var windowGroup))
{
_sawmill.Error($"Unable to find dungeon data keys for {nameof(gen)}");
return;
}
// Iterate every room and check if there's a gap beyond it that leads to another room within N tiles
// If so then consider windows
var minDistance = 4;
var maxDistance = 6;
var tileDef = _tileDefManager[tileProto];
var window = _prototype.Index(windowGroup);
foreach (var room in dungeon.Rooms)
{
var validTiles = new List<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 = (tile + _grid.TileSizeHalfVector - room.Center).ToAngle();
var roundedAngle = Math.Round(tileAngle.Theta / (Math.PI / 2)) * (Math.PI / 2);
var tileVec = (Vector2i) new Angle(roundedAngle).ToVec().Rounded();
if (!tileVec.Equals(dirVec))
continue;
var valid = false;
for (var j = 1; j < maxDistance; j++)
{
var edgeNeighbor = tile + dirVec * j;
if (dungeon.RoomTiles.Contains(edgeNeighbor))
{
if (j < minDistance)
{
valid = false;
}
else
{
valid = true;
}
break;
}
}
if (!valid)
continue;
var windowTile = tile + dirVec;
if (reservedTiles.Contains(windowTile))
continue;
if (!_anchorable.TileFree(_grid, windowTile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
continue;
validTiles.Add(windowTile);
}
validTiles.Sort((x, y) => (x + _grid.TileSizeHalfVector - room.Center).LengthSquared().CompareTo((y + _grid.TileSizeHalfVector - room.Center).LengthSquared()));
for (var j = 0; j < Math.Min(validTiles.Count, 3); j++)
{
var tile = validTiles[j];
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, tile);
_maps.SetTile(_gridUid, _grid, tile, _tile.GetVariantTile((ContentTileDefinition) tileDef, random));
_entManager.SpawnEntities(gridPos, EntitySpawnCollection.GetSpawns(window.Entries, random));
}
if (validTiles.Count > 0)
{
await SuspendDungeon();
if (!ValidateResume())
return;
}
validTiles.Clear();
}
}
}
}

View File

@@ -0,0 +1,144 @@
using System.Threading.Tasks;
using Content.Shared.Maps;
using Content.Shared.Procedural;
using Content.Shared.Procedural.PostGeneration;
using Content.Shared.Storage;
using Robust.Shared.Map.Components;
namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob
{
/// <summary>
/// <see cref="JunctionDunGen"/>
/// </summary>
private async Task PostGen(JunctionDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
{
if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto) ||
!data.SpawnGroups.TryGetValue(DungeonDataKey.Junction, out var junctionProto))
{
_sawmill.Error($"Dungeon data keys are missing for {nameof(gen)}");
return;
}
var tileDef = _tileDefManager[tileProto];
var entranceGroup = _prototype.Index(junctionProto);
// N-wide junctions
foreach (var tile in dungeon.CorridorTiles)
{
if (!_anchorable.TileFree(_grid, tile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
continue;
// Check each direction:
// - Check if immediate neighbors are free
// - Check if the neighbors beyond that are not free
// - Then check either side if they're slightly more free
var exteriorWidth = (int) Math.Floor(gen.Width / 2f);
var width = (int) Math.Ceiling(gen.Width / 2f);
for (var i = 0; i < 2; i++)
{
var isValid = true;
var neighborDir = (Direction) (i * 2);
var neighborVec = neighborDir.ToIntVec();
for (var j = -width; j <= width; j++)
{
if (j == 0)
continue;
var neighbor = tile + neighborVec * j;
// If it's an end tile then check it's occupied.
if (j == -width ||
j == width)
{
if (!HasWall(neighbor))
{
isValid = false;
break;
}
continue;
}
// If we're not at the end tile then check it + perpendicular are free.
if (!_anchorable.TileFree(_grid, neighbor, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
{
isValid = false;
break;
}
var perp1 = tile + neighborVec * j + ((Direction) ((i * 2 + 2) % 8)).ToIntVec();
var perp2 = tile + neighborVec * j + ((Direction) ((i * 2 + 6) % 8)).ToIntVec();
if (!_anchorable.TileFree(_grid, perp1, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
{
isValid = false;
break;
}
if (!_anchorable.TileFree(_grid, perp2, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
{
isValid = false;
break;
}
}
if (!isValid)
continue;
// Check corners to see if either side opens up (if it's just a 1x wide corridor do nothing, needs to be a funnel.
foreach (var j in new [] {-exteriorWidth, exteriorWidth})
{
var freeCount = 0;
// Need at least 3 of 4 free
for (var k = 0; k < 4; k++)
{
var cornerDir = (Direction) (k * 2 + 1);
var cornerVec = cornerDir.ToIntVec();
var cornerNeighbor = tile + neighborVec * j + cornerVec;
if (_anchorable.TileFree(_grid, cornerNeighbor, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
{
freeCount++;
}
}
if (freeCount < gen.Width)
continue;
// Valid!
isValid = true;
for (var x = -width + 1; x < width; x++)
{
var weh = tile + neighborDir.ToIntVec() * x;
if (reservedTiles.Contains(weh))
continue;
_maps.SetTile(_gridUid, _grid, weh, _tile.GetVariantTile((ContentTileDefinition) tileDef, random));
var coords = _maps.GridTileToLocal(_gridUid, _grid, weh);
_entManager.SpawnEntities(coords, EntitySpawnCollection.GetSpawns(entranceGroup.Entries, random));
}
break;
}
if (isValid)
{
await SuspendDungeon();
if (!ValidateResume())
return;
}
break;
}
}
}
}

View File

@@ -0,0 +1,147 @@
using System.Numerics;
using System.Threading.Tasks;
using Content.Shared.Maps;
using Content.Shared.Procedural;
using Content.Shared.Procedural.PostGeneration;
using Content.Shared.Storage;
using Robust.Shared.Utility;
namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob
{
/// <summary>
/// <see cref="MiddleConnectionDunGen"/>
/// </summary>
private async Task PostGen(MiddleConnectionDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
{
if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto) ||
!data.SpawnGroups.TryGetValue(DungeonDataKey.Entrance, out var entranceProto) ||
!_prototype.TryIndex(entranceProto, out var entrance))
{
_sawmill.Error($"Tried to run {nameof(MiddleConnectionDunGen)} without any dungeon data set which is unsupported");
return;
}
data.SpawnGroups.TryGetValue(DungeonDataKey.EntranceFlank, out var flankProto);
_prototype.TryIndex(flankProto, out var flank);
// Grab all of the room bounds
// Then, work out connections between them
var roomBorders = new Dictionary<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, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
continue;
roomEdges.Add(neighbor);
}
}
}
roomBorders.Add(room, roomEdges);
}
// Do pathfind from first room to work out graph.
// TODO: Optional loops
var roomConnections = new Dictionary<DungeonRoom, List<DungeonRoom>>();
var tileDef = _tileDefManager[tileProto];
foreach (var (room, border) in roomBorders)
{
var conns = roomConnections.GetOrNew(room);
foreach (var (otherRoom, otherBorders) in roomBorders)
{
if (room.Equals(otherRoom) ||
conns.Contains(otherRoom))
{
continue;
}
var flipp = new HashSet<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 += node + _grid.TileSizeHalfVector;
}
center /= flipp.Count;
// Weight airlocks towards center more.
var nodeDistances = new List<(Vector2i Node, float Distance)>(flipp.Count);
foreach (var node in flipp)
{
nodeDistances.Add((node, (node + _grid.TileSizeHalfVector - center).LengthSquared()));
}
nodeDistances.Sort((x, y) => x.Distance.CompareTo(y.Distance));
var width = gen.Count;
for (var i = 0; i < nodeDistances.Count; i++)
{
var node = nodeDistances[i].Node;
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, node);
if (!_anchorable.TileFree(_grid, node, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
continue;
width--;
_maps.SetTile(_gridUid, _grid, node, _tile.GetVariantTile((ContentTileDefinition) tileDef, random));
if (flank != null && nodeDistances.Count - i <= 2)
{
_entManager.SpawnEntities(gridPos, EntitySpawnCollection.GetSpawns(flank.Entries, random));
}
else
{
// Iterate neighbors and check for blockers, if so bulldoze
ClearDoor(dungeon, _grid, node);
_entManager.SpawnEntities(gridPos, EntitySpawnCollection.GetSpawns(entrance.Entries, random));
}
if (width == 0)
break;
}
conns.Add(otherRoom);
var otherConns = roomConnections.GetOrNew(otherRoom);
otherConns.Add(room);
await SuspendDungeon();
if (!ValidateResume())
return;
}
}
}
}

View File

@@ -0,0 +1,48 @@
using System.Threading.Tasks;
using Content.Shared.Maps;
using Content.Shared.Procedural;
using Content.Shared.Procedural.PostGeneration;
using Content.Shared.Storage;
using Robust.Shared.Map;
namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob
{
/// <summary>
/// <see cref="RoomEntranceDunGen"/>
/// </summary>
private async Task PostGen(RoomEntranceDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
{
if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto) ||
!data.SpawnGroups.TryGetValue(DungeonDataKey.Entrance, out var entranceProtos) ||
!_prototype.TryIndex(entranceProtos, out var entranceIn))
{
LogDataError(typeof(RoomEntranceDunGen));
return;
}
var setTiles = new List<(Vector2i, Tile)>();
var tileDef = _tileDefManager[tileProto];
foreach (var room in dungeon.Rooms)
{
foreach (var entrance in room.Entrances)
{
setTiles.Add((entrance, _tile.GetVariantTile((ContentTileDefinition) tileDef, random)));
}
}
_maps.SetTiles(_gridUid, _grid, setTiles);
foreach (var room in dungeon.Rooms)
{
foreach (var entrance in room.Entrances)
{
_entManager.SpawnEntities(
_maps.GridTileToLocal(_gridUid, _grid, entrance),
EntitySpawnCollection.GetSpawns(entranceIn.Entries, random));
}
}
}
}

View File

@@ -0,0 +1,147 @@
using System.Numerics;
using System.Threading.Tasks;
using Content.Server.NPC.Pathfinding;
using Content.Shared.Procedural;
using Content.Shared.Procedural.PostGeneration;
using Robust.Shared.Map;
using Robust.Shared.Random;
namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob
{
/// <summary>
/// <see cref="SplineDungeonConnectorDunGen"/>
/// </summary>
private async Task<Dungeon> PostGen(
SplineDungeonConnectorDunGen gen,
DungeonData data,
List<Dungeon> dungeons,
HashSet<Vector2i> reservedTiles,
Random random)
{
// TODO: The path itself use the tile
// Widen it randomly (probably for each tile offset it by some changing amount).
// NOOP
if (dungeons.Count <= 1)
return Dungeon.Empty;
if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var fallback) ||
!data.Tiles.TryGetValue(DungeonDataKey.WidenTile, out var widen))
{
LogDataError(typeof(SplineDungeonConnectorDunGen));
return Dungeon.Empty;
}
var nodes = new List<Vector2i>();
foreach (var dungeon in dungeons)
{
foreach (var room in dungeon.Rooms)
{
if (room.Entrances.Count == 0)
continue;
nodes.Add(room.Entrances[0]);
break;
}
}
var tree = _dungeon.MinimumSpanningTree(nodes, random);
await SuspendDungeon();
if (!ValidateResume())
return Dungeon.Empty;
var tiles = new List<(Vector2i Index, Tile Tile)>();
var pathfinding = _entManager.System<PathfindingSystem>();
var allTiles = new HashSet<Vector2i>();
var fallbackTile = new Tile(_prototype.Index(fallback).TileId);
foreach (var pair in tree)
{
var path = pathfinding.GetSplinePath(new PathfindingSystem.SplinePathArgs()
{
Distance = gen.DivisionDistance,
MaxRatio = gen.VarianceMax,
Args = new PathfindingSystem.SimplePathArgs()
{
Start = pair.Start,
End = pair.End,
TileCost = node =>
{
// We want these to get prioritised internally and into space if it's a space dungeon.
if (_maps.TryGetTile(_grid, node, out var tile) && !tile.IsEmpty)
return 1f;
return 5f;
}
},
},
random);
// Welp
if (path.Path.Count == 0)
{
_sawmill.Error($"Unable to connect spline dungeon path for {_entManager.ToPrettyString(_gridUid)} between {pair.Start} and {pair.End}");
continue;
}
await SuspendDungeon();
if (!ValidateResume())
return Dungeon.Empty;
var wide = pathfinding.GetWiden(new PathfindingSystem.WidenArgs()
{
Path = path.Path,
},
random);
tiles.Clear();
allTiles.EnsureCapacity(allTiles.Count + wide.Count);
foreach (var node in wide)
{
if (reservedTiles.Contains(node))
continue;
allTiles.Add(node);
Tile tile;
if (random.Prob(0.9f))
{
tile = new Tile(_prototype.Index(widen).TileId);
}
else
{
tile = _tileDefManager.GetVariantTile(widen, random);
}
tiles.Add((node, tile));
}
_maps.SetTiles(_gridUid, _grid, tiles);
tiles.Clear();
allTiles.EnsureCapacity(allTiles.Count + path.Path.Count);
foreach (var node in path.Path)
{
if (reservedTiles.Contains(node))
continue;
allTiles.Add(node);
tiles.Add((node, fallbackTile));
}
_maps.SetTiles(_gridUid, _grid, tiles);
}
var dungy = new Dungeon();
var dungyRoom = new DungeonRoom(allTiles, Vector2.Zero, Box2i.Empty, new HashSet<Vector2i>());
dungy.AddRoom(dungyRoom);
return dungy;
}
}

View File

@@ -0,0 +1,56 @@
using System.Threading.Tasks;
using Content.Shared.Procedural;
using Content.Shared.Procedural.PostGeneration;
using Content.Shared.Storage;
using Robust.Shared.Random;
namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob
{
/// <summary>
/// <see cref="WallMountDunGen"/>
/// </summary>
private async Task PostGen(WallMountDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
{
if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto))
{
_sawmill.Error($"Tried to run {nameof(WallMountDunGen)} without any dungeon data set which is unsupported");
return;
}
var tileDef = _prototype.Index(tileProto);
data.SpawnGroups.TryGetValue(DungeonDataKey.WallMounts, out var spawnProto);
var checkedTiles = new HashSet<Vector2i>();
var allExterior = new HashSet<Vector2i>(dungeon.CorridorExteriorTiles);
allExterior.UnionWith(dungeon.RoomExteriorTiles);
var count = 0;
foreach (var neighbor in allExterior)
{
// Occupado
if (dungeon.RoomTiles.Contains(neighbor) || checkedTiles.Contains(neighbor) || !_anchorable.TileFree(_grid, neighbor, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
continue;
if (!random.Prob(gen.Prob) || !checkedTiles.Add(neighbor))
continue;
_maps.SetTile(_gridUid, _grid, neighbor, _tile.GetVariantTile(tileDef, random));
var gridPos = _maps.GridTileToLocal(_gridUid, _grid, neighbor);
var protoNames = EntitySpawnCollection.GetSpawns(_prototype.Index(spawnProto).Entries, random);
_entManager.SpawnEntities(gridPos, protoNames);
count += protoNames.Count;
if (count > 20)
{
count -= 20;
await SuspendDungeon();
if (!ValidateResume())
return;
}
}
}
}

View File

@@ -1,23 +1,27 @@
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using Content.Shared.Procedural;
using Content.Shared.Procedural.PostGeneration;
using Robust.Shared.Collections;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Procedural;
namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob
{
/// <summary>
/// Tries to connect rooms via worm-like corridors.
/// <see cref="WormCorridorDunGen"/>
/// </summary>
private async Task PostGen(WormCorridorPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, Random random)
private async Task PostGen(WormCorridorDunGen gen, DungeonData data, Dungeon dungeon, HashSet<Vector2i> reservedTiles, Random random)
{
if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto) || !_prototype.TryIndex(tileProto, out var tileDef))
{
_sawmill.Error($"Tried to run {nameof(WormCorridorDunGen)} without any dungeon data set which is unsupported");
return;
}
var networks = new List<(Vector2i Start, HashSet<Vector2i> Network)>();
// List of places to start from.
@@ -32,7 +36,7 @@ public sealed partial class DungeonJob
networks.Add((entrance, network));
// Point away from the room to start with.
startAngles.Add(entrance, (entrance + grid.TileSizeHalfVector - room.Center).ToAngle());
startAngles.Add(entrance, (entrance + _grid.TileSizeHalfVector - room.Center).ToAngle());
}
}
@@ -46,7 +50,7 @@ public sealed partial class DungeonJob
// Find a random network to worm from.
var startIndex = (i % networks.Count);
var startPos = networks[startIndex].Start;
var position = startPos + grid.TileSizeHalfVector;
var position = startPos + _grid.TileSizeHalfVector;
var remainingLength = gen.Length;
worm.Clear();
@@ -108,7 +112,7 @@ public sealed partial class DungeonJob
costSoFar[startNode] = 0f;
var count = 0;
await SuspendIfOutOfTime();
await SuspendDungeon();
if (!ValidateResume())
return;
@@ -174,9 +178,9 @@ public sealed partial class DungeonJob
WidenCorridor(dungeon, gen.Width, main.Network);
dungeon.CorridorTiles.UnionWith(main.Network);
BuildCorridorExterior(dungeon);
dungeon.RefreshAllTiles();
var tiles = new List<(Vector2i Index, Tile Tile)>();
var tileDef = _prototype.Index(gen.Tile);
foreach (var tile in dungeon.CorridorTiles)
{

View File

@@ -0,0 +1,309 @@
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Decals;
using Content.Server.NPC.Components;
using Content.Server.NPC.HTN;
using Content.Server.NPC.Systems;
using Content.Shared.Construction.EntitySystems;
using Content.Shared.Maps;
using Content.Shared.Procedural;
using Content.Shared.Procedural.DungeonGenerators;
using Content.Shared.Procedural.DungeonLayers;
using Content.Shared.Procedural.PostGeneration;
using Content.Shared.Tag;
using JetBrains.Annotations;
using Robust.Server.Physics;
using Robust.Shared.CPUJob.JobQueues;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
using IDunGenLayer = Content.Shared.Procedural.IDunGenLayer;
namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob : Job<List<Dungeon>>
{
public bool TimeSlice = true;
private readonly IEntityManager _entManager;
private readonly IPrototypeManager _prototype;
private readonly ITileDefinitionManager _tileDefManager;
private readonly AnchorableSystem _anchorable;
private readonly DecalSystem _decals;
private readonly DungeonSystem _dungeon;
private readonly EntityLookupSystem _lookup;
private readonly TagSystem _tags;
private readonly TileSystem _tile;
private readonly SharedMapSystem _maps;
private readonly SharedTransformSystem _transform;
private EntityQuery<PhysicsComponent> _physicsQuery;
private EntityQuery<TransformComponent> _xformQuery;
private readonly DungeonConfigPrototype _gen;
private readonly int _seed;
private readonly Vector2i _position;
private readonly EntityUid _gridUid;
private readonly MapGridComponent _grid;
private readonly ISawmill _sawmill;
public DungeonJob(
ISawmill sawmill,
double maxTime,
IEntityManager entManager,
IPrototypeManager prototype,
ITileDefinitionManager tileDefManager,
AnchorableSystem anchorable,
DecalSystem decals,
DungeonSystem dungeon,
EntityLookupSystem lookup,
TileSystem tile,
SharedTransformSystem transform,
DungeonConfigPrototype gen,
MapGridComponent grid,
EntityUid gridUid,
int seed,
Vector2i position,
CancellationToken cancellation = default) : base(maxTime, cancellation)
{
_sawmill = sawmill;
_entManager = entManager;
_prototype = prototype;
_tileDefManager = tileDefManager;
_anchorable = anchorable;
_decals = decals;
_dungeon = dungeon;
_lookup = lookup;
_tile = tile;
_tags = _entManager.System<TagSystem>();
_maps = _entManager.System<SharedMapSystem>();
_transform = transform;
_physicsQuery = _entManager.GetEntityQuery<PhysicsComponent>();
_xformQuery = _entManager.GetEntityQuery<TransformComponent>();
_gen = gen;
_grid = grid;
_gridUid = gridUid;
_seed = seed;
_position = position;
}
/// <summary>
/// Gets the relevant dungeon, running recursively as relevant.
/// </summary>
/// <param name="reserve">Should we reserve tiles even if the config doesn't specify.</param>
private async Task<List<Dungeon>> GetDungeons(
Vector2i position,
DungeonConfigPrototype config,
DungeonData data,
List<IDunGenLayer> layers,
HashSet<Vector2i> reservedTiles,
int seed,
Random random)
{
var dungeons = new List<Dungeon>();
var count = random.Next(config.MinCount, config.MaxCount + 1);
for (var i = 0; i < count; i++)
{
position += random.NextPolarVector2(config.MinOffset, config.MaxOffset).Floored();
foreach (var layer in layers)
{
await RunLayer(dungeons, data, position, layer, reservedTiles, seed, random);
if (config.ReserveTiles)
{
foreach (var dungeon in dungeons)
{
reservedTiles.UnionWith(dungeon.AllTiles);
}
}
await SuspendDungeon();
if (!ValidateResume())
return new List<Dungeon>();
}
}
return dungeons;
}
protected override async Task<List<Dungeon>?> Process()
{
_sawmill.Info($"Generating dungeon {_gen.ID} with seed {_seed} on {_entManager.ToPrettyString(_gridUid)}");
_grid.CanSplit = false;
var random = new Random(_seed);
var position = (_position + random.NextPolarVector2(_gen.MinOffset, _gen.MaxOffset)).Floored();
// Tiles we can no longer generate on due to being reserved elsewhere.
var reservedTiles = new HashSet<Vector2i>();
var dungeons = await GetDungeons(position, _gen, _gen.Data, _gen.Layers, reservedTiles, _seed, random);
// To make it slightly more deterministic treat this RNG as separate ig.
// Post-processing after finishing loading.
// Defer splitting so they don't get spammed and so we don't have to worry about tracking the grid along the way.
_grid.CanSplit = true;
_entManager.System<GridFixtureSystem>().CheckSplits(_gridUid);
var npcSystem = _entManager.System<NPCSystem>();
var npcs = new HashSet<Entity<HTNComponent>>();
_lookup.GetChildEntities(_gridUid, npcs);
foreach (var npc in npcs)
{
npcSystem.WakeNPC(npc.Owner, npc.Comp);
}
return dungeons;
}
private async Task RunLayer(
List<Dungeon> dungeons,
DungeonData data,
Vector2i position,
IDunGenLayer layer,
HashSet<Vector2i> reservedTiles,
int seed,
Random random)
{
_sawmill.Debug($"Doing postgen {layer.GetType()} for {_gen.ID} with seed {_seed}");
// If there's a way to just call the methods directly for the love of god tell me.
// Some of these don't care about reservedtiles because they only operate on dungeon tiles (which should
// never be reserved)
// Some may or may not return dungeons.
// It's clamplicated but yeah procgen layering moment I'll take constructive feedback.
switch (layer)
{
case AutoCablingDunGen cabling:
await PostGen(cabling, data, dungeons[^1], reservedTiles, random);
break;
case BiomeMarkerLayerDunGen markerPost:
await PostGen(markerPost, data, dungeons[^1], reservedTiles, random);
break;
case BiomeDunGen biome:
await PostGen(biome, data, dungeons[^1], reservedTiles, random);
break;
case BoundaryWallDunGen boundary:
await PostGen(boundary, data, dungeons[^1], reservedTiles, random);
break;
case CornerClutterDunGen clutter:
await PostGen(clutter, data, dungeons[^1], reservedTiles, random);
break;
case CorridorClutterDunGen corClutter:
await PostGen(corClutter, data, dungeons[^1], reservedTiles, random);
break;
case CorridorDunGen cordor:
await PostGen(cordor, data, dungeons[^1], reservedTiles, random);
break;
case CorridorDecalSkirtingDunGen decks:
await PostGen(decks, data, dungeons[^1], reservedTiles, random);
break;
case EntranceFlankDunGen flank:
await PostGen(flank, data, dungeons[^1], reservedTiles, random);
break;
case ExteriorDunGen exterior:
dungeons.AddRange(await GenerateExteriorDungen(position, exterior, reservedTiles, random));
break;
case FillGridDunGen fill:
dungeons.Add(await GenerateFillDunGen(data, reservedTiles));
break;
case JunctionDunGen junc:
await PostGen(junc, data, dungeons[^1], reservedTiles, random);
break;
case MiddleConnectionDunGen dordor:
await PostGen(dordor, data, dungeons[^1], reservedTiles, random);
break;
case DungeonEntranceDunGen entrance:
await PostGen(entrance, data, dungeons[^1], reservedTiles, random);
break;
case ExternalWindowDunGen externalWindow:
await PostGen(externalWindow, data, dungeons[^1], reservedTiles, random);
break;
case InternalWindowDunGen internalWindow:
await PostGen(internalWindow, data, dungeons[^1], reservedTiles, random);
break;
case MobsDunGen mob:
await PostGen(mob, dungeons[^1], random);
break;
case NoiseDistanceDunGen distance:
dungeons.Add(await GenerateNoiseDistanceDunGen(position, distance, reservedTiles, seed, random));
break;
case NoiseDunGen noise:
dungeons.Add(await GenerateNoiseDunGen(position, noise, reservedTiles, seed, random));
break;
case OreDunGen ore:
await PostGen(ore, dungeons[^1], random);
break;
case PrefabDunGen prefab:
dungeons.Add(await GeneratePrefabDunGen(position, data, prefab, reservedTiles, random));
break;
case PrototypeDunGen prototypo:
var groupConfig = _prototype.Index(prototypo.Proto);
position = (position + random.NextPolarVector2(groupConfig.MinOffset, groupConfig.MaxOffset)).Floored();
var dataCopy = groupConfig.Data.Clone();
dataCopy.Apply(data);
dungeons.AddRange(await GetDungeons(position, groupConfig, dataCopy, groupConfig.Layers, reservedTiles, seed, random));
break;
case ReplaceTileDunGen replace:
dungeons.Add(await GenerateTileReplacementDunGen(replace, data, reservedTiles, random));
break;
case RoomEntranceDunGen rEntrance:
await PostGen(rEntrance, data, dungeons[^1], reservedTiles, random);
break;
case SplineDungeonConnectorDunGen spline:
dungeons.Add(await PostGen(spline, data, dungeons, reservedTiles, random));
break;
case WallMountDunGen wall:
await PostGen(wall, data, dungeons[^1], reservedTiles, random);
break;
case WormCorridorDunGen worm:
await PostGen(worm, data, dungeons[^1], reservedTiles, random);
break;
default:
throw new NotImplementedException();
}
}
private void LogDataError(Type type)
{
_sawmill.Error($"Unable to find dungeon data keys for {type}");
}
[Pure]
private bool ValidateResume()
{
if (_entManager.Deleted(_gridUid))
{
return false;
}
return true;
}
/// <summary>
/// Wrapper around <see cref="Job{T}.SuspendIfOutOfTime"/>
/// </summary>
private async Task SuspendDungeon()
{
if (!TimeSlice)
return;
await SuspendIfOutOfTime();
}
}

View File

@@ -51,6 +51,8 @@ public sealed partial class DungeonSystem
dungeonUid = EntityManager.CreateEntityUninitialized(null, new EntityCoordinates(dungeonUid, position));
dungeonGrid = EntityManager.AddComponent<MapGridComponent>(dungeonUid);
EntityManager.InitializeAndStartEntity(dungeonUid, mapId);
// If we created a grid (e.g. space dungen) then offset it so we don't double-apply positions
position = Vector2i.Zero;
}
int seed;

View File

@@ -64,6 +64,7 @@ public sealed partial class DungeonSystem
Vector2i origin,
DungeonRoomPrototype room,
Random random,
HashSet<Vector2i>? reservedTiles,
bool clearExisting = false,
bool rotation = false)
{
@@ -78,7 +79,7 @@ public sealed partial class DungeonSystem
var roomTransform = Matrix3Helpers.CreateTransform((Vector2) room.Size / 2f, roomRotation);
var finalTransform = Matrix3x2.Multiply(roomTransform, originTransform);
SpawnRoom(gridUid, grid, finalTransform, room, clearExisting);
SpawnRoom(gridUid, grid, finalTransform, room, reservedTiles, clearExisting);
}
public Angle GetRoomRotation(DungeonRoomPrototype room, Random random)
@@ -103,6 +104,7 @@ public sealed partial class DungeonSystem
MapGridComponent grid,
Matrix3x2 roomTransform,
DungeonRoomPrototype room,
HashSet<Vector2i>? reservedTiles = null,
bool clearExisting = false)
{
// Ensure the underlying template exists.
@@ -150,6 +152,10 @@ public sealed partial class DungeonSystem
var tilePos = Vector2.Transform(indices + tileOffset, roomTransform);
var rounded = tilePos.Floored();
if (!clearExisting && reservedTiles?.Contains(rounded) == true)
continue;
_tiles.Add((rounded, tileRef.Tile));
}
}
@@ -165,6 +171,10 @@ public sealed partial class DungeonSystem
{
var templateXform = _xformQuery.GetComponent(templateEnt);
var childPos = Vector2.Transform(templateXform.LocalPosition - roomCenter, roomTransform);
if (!clearExisting && reservedTiles?.Contains(childPos.Floored()) == true)
continue;
var childRot = templateXform.LocalRotation + finalRoomRotation;
var protoId = _metaQuery.GetComponent(templateEnt).EntityPrototype?.ID;
@@ -192,8 +202,11 @@ public sealed partial class DungeonSystem
// Offset by 0.5 because decals are offset from bot-left corner
// So we convert it to center of tile then convert it back again after transform.
// Do these shenanigans because 32x32 decals assume as they are centered on bottom-left of tiles.
var position = Vector2.Transform(decal.Coordinates + Vector2Helpers.Half - roomCenter, roomTransform);
position -= Vector2Helpers.Half;
var position = Vector2.Transform(decal.Coordinates + grid.TileSizeHalfVector - roomCenter, roomTransform);
position -= grid.TileSizeHalfVector;
if (!clearExisting && reservedTiles?.Contains(position.Floored()) == true)
continue;
// Umm uhh I love decals so uhhhh idk what to do about this
var angle = (decal.Angle + finalRoomRotation).Reduced();

View File

@@ -12,6 +12,7 @@ using Content.Shared.Physics;
using Content.Shared.Procedural;
using Content.Shared.Tag;
using Robust.Server.GameObjects;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Map;
@@ -49,7 +50,7 @@ public sealed partial class DungeonSystem : SharedDungeonSystem
public const int CollisionLayer = (int) CollisionGroup.Impassable;
private readonly JobQueue _dungeonJobQueue = new(DungeonJobTime);
private readonly Dictionary<DungeonJob, CancellationTokenSource> _dungeonJobs = new();
private readonly Dictionary<DungeonJob.DungeonJob, CancellationTokenSource> _dungeonJobs = new();
[ValidatePrototypeId<ContentTileDefinition>]
public const string FallbackTileId = "FloorSteel";
@@ -190,18 +191,16 @@ public sealed partial class DungeonSystem : SharedDungeonSystem
int seed)
{
var cancelToken = new CancellationTokenSource();
var job = new DungeonJob(
var job = new DungeonJob.DungeonJob(
Log,
DungeonJobTime,
EntityManager,
_mapManager,
_prototype,
_tileDefManager,
_anchorable,
_decals,
this,
_lookup,
_tag,
_tile,
_transform,
gen,
@@ -215,7 +214,7 @@ public sealed partial class DungeonSystem : SharedDungeonSystem
_dungeonJobQueue.EnqueueJob(job);
}
public async Task<Dungeon> GenerateDungeonAsync(
public async Task<List<Dungeon>> GenerateDungeonAsync(
DungeonConfigPrototype gen,
EntityUid gridUid,
MapGridComponent grid,
@@ -223,18 +222,16 @@ public sealed partial class DungeonSystem : SharedDungeonSystem
int seed)
{
var cancelToken = new CancellationTokenSource();
var job = new DungeonJob(
var job = new DungeonJob.DungeonJob(
Log,
DungeonJobTime,
EntityManager,
_mapManager,
_prototype,
_tileDefManager,
_anchorable,
_decals,
this,
_lookup,
_tag,
_tile,
_transform,
gen,

View File

@@ -35,6 +35,7 @@ public sealed class RoomFillSystem : EntitySystem
_maps.LocalToTile(xform.GridUid.Value, mapGrid, xform.Coordinates),
room,
random,
null,
clearExisting: component.ClearExisting,
rotation: component.Rotation);
}