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

View File

@@ -0,0 +1,123 @@
namespace Content.Server.NPC.Pathfinding;
public sealed partial class PathfindingSystem
{
/*
* Handle BFS searches from Start->End. Doesn't consider NPC pathfinding.
*/
/// <summary>
/// Pathfinding args for a 1-many path.
/// </summary>
public record struct BreadthPathArgs()
{
public Vector2i Start;
public List<Vector2i> Ends;
public bool Diagonals = false;
public Func<Vector2i, float>? TileCost;
public int Limit = 10000;
}
/// <summary>
/// Gets a BFS path from start to any end. Can also supply an optional tile-cost for tiles.
/// </summary>
public SimplePathResult GetBreadthPath(BreadthPathArgs args)
{
var cameFrom = new Dictionary<Vector2i, Vector2i>();
var costSoFar = new Dictionary<Vector2i, float>();
var frontier = new PriorityQueue<Vector2i, float>();
costSoFar[args.Start] = 0f;
frontier.Enqueue(args.Start, 0f);
var count = 0;
while (frontier.TryDequeue(out var node, out _) && count < args.Limit)
{
count++;
if (args.Ends.Contains(node))
{
// Found target
var path = ReconstructPath(node, cameFrom);
return new SimplePathResult()
{
CameFrom = cameFrom,
Path = path,
};
}
var gCost = costSoFar[node];
if (args.Diagonals)
{
for (var x = -1; x <= 1; x++)
{
for (var y = -1; y <= 1; y++)
{
var neighbor = node + new Vector2i(x, y);
var neighborCost = OctileDistance(node, neighbor) * args.TileCost?.Invoke(neighbor) ?? 1f;
if (neighborCost.Equals(0f))
{
continue;
}
// f = g + h
// gScore is distance to the start node
// hScore is distance to the end node
var gScore = gCost + neighborCost;
// Slower to get here so just ignore it.
if (costSoFar.TryGetValue(neighbor, out var nextValue) && gScore >= nextValue)
{
continue;
}
cameFrom[neighbor] = node;
costSoFar[neighbor] = gScore;
// pFactor is tie-breaker where the fscore is otherwise equal.
// See http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html#breaking-ties
// There's other ways to do it but future consideration
// The closer the fScore is to the actual distance then the better the pathfinder will be
// (i.e. somewhere between 1 and infinite)
// Can use hierarchical pathfinder or whatever to improve the heuristic but this is fine for now.
frontier.Enqueue(neighbor, gScore);
}
}
}
else
{
for (var x = -1; x <= 1; x++)
{
for (var y = -1; y <= 1; y++)
{
if (x != 0 && y != 0)
continue;
var neighbor = node + new Vector2i(x, y);
var neighborCost = ManhattanDistance(node, neighbor) * args.TileCost?.Invoke(neighbor) ?? 1f;
if (neighborCost.Equals(0f))
continue;
var gScore = gCost + neighborCost;
if (costSoFar.TryGetValue(neighbor, out var nextValue) && gScore >= nextValue)
continue;
cameFrom[neighbor] = node;
costSoFar[neighbor] = gScore;
frontier.Enqueue(neighbor, gScore);
}
}
}
}
return SimplePathResult.NoPath;
}
}

View File

@@ -0,0 +1,74 @@
namespace Content.Server.NPC.Pathfinding;
public sealed partial class PathfindingSystem
{
public void GridCast(Vector2i start, Vector2i end, Vector2iCallback callback)
{
// https://gist.github.com/Pyr3z/46884d67641094d6cf353358566db566
// declare all locals at the top so it's obvious how big the footprint is
int dx, dy, xinc, yinc, side, i, error;
// starting cell is always returned
if (!callback(start))
return;
xinc = (end.X < start.X) ? -1 : 1;
yinc = (end.Y < start.Y) ? -1 : 1;
dx = xinc * (end.X - start.X);
dy = yinc * (end.Y - start.Y);
var ax = start.X;
var ay = start.Y;
if (dx == dy) // Handle perfect diagonals
{
// I include this "optimization" for more aesthetic reasons, actually.
// While Bresenham's Line can handle perfect diagonals just fine, it adds
// additional cells to the line that make it not a perfect diagonal
// anymore. So, while this branch is ~twice as fast as the next branch,
// the real reason it is here is for style.
// Also, there *is* the reason of performance. If used for cell-based
// raycasts, for example, then perfect diagonals will check half as many
// cells.
while (dx --> 0)
{
ax += xinc;
ay += yinc;
if (!callback(new Vector2i(ax, ay)))
return;
}
return;
}
// Handle all other lines
side = -1 * ((dx == 0 ? yinc : xinc) - 1);
i = dx + dy;
error = dx - dy;
dx *= 2;
dy *= 2;
while (i --> 0)
{
if (error > 0 || error == side)
{
ax += xinc;
error -= dy;
}
else
{
ay += yinc;
error += dx;
}
if (!callback(new Vector2i(ax, ay)))
return;
}
}
public delegate bool Vector2iCallback(Vector2i index);
}

View File

@@ -0,0 +1,154 @@
namespace Content.Server.NPC.Pathfinding;
public sealed partial class PathfindingSystem
{
/// <summary>
/// Pathfinding args for a 1-1 path.
/// </summary>
public record struct SimplePathArgs()
{
public Vector2i Start;
public Vector2i End;
public bool Diagonals = false;
public int Limit = 10000;
/// <summary>
/// Custom tile-costs if applicable.
/// </summary>
public Func<Vector2i, float>? TileCost;
}
public record struct SimplePathResult
{
public static SimplePathResult NoPath = new();
public List<Vector2i> Path;
public Dictionary<Vector2i, Vector2i> CameFrom;
}
/// <summary>
/// Gets simple A* path from start to end. Can also supply an optional tile-cost for tiles.
/// </summary>
public SimplePathResult GetPath(SimplePathArgs args)
{
var cameFrom = new Dictionary<Vector2i, Vector2i>();
var costSoFar = new Dictionary<Vector2i, float>();
var frontier = new PriorityQueue<Vector2i, float>();
costSoFar[args.Start] = 0f;
frontier.Enqueue(args.Start, 0f);
var count = 0;
while (frontier.TryDequeue(out var node, out _) && count < args.Limit)
{
count++;
if (node == args.End)
{
// Found target
var path = ReconstructPath(args.End, cameFrom);
return new SimplePathResult()
{
CameFrom = cameFrom,
Path = path,
};
}
var gCost = costSoFar[node];
if (args.Diagonals)
{
for (var x = -1; x <= 1; x++)
{
for (var y = -1; y <= 1; y++)
{
var neighbor = node + new Vector2i(x, y);
var neighborCost = OctileDistance(node, neighbor) * args.TileCost?.Invoke(neighbor) ?? 1f;
if (neighborCost.Equals(0f))
{
continue;
}
// f = g + h
// gScore is distance to the start node
// hScore is distance to the end node
var gScore = gCost + neighborCost;
// Slower to get here so just ignore it.
if (costSoFar.TryGetValue(neighbor, out var nextValue) && gScore >= nextValue)
{
continue;
}
cameFrom[neighbor] = node;
costSoFar[neighbor] = gScore;
// pFactor is tie-breaker where the fscore is otherwise equal.
// See http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html#breaking-ties
// There's other ways to do it but future consideration
// The closer the fScore is to the actual distance then the better the pathfinder will be
// (i.e. somewhere between 1 and infinite)
// Can use hierarchical pathfinder or whatever to improve the heuristic but this is fine for now.
var hScore = OctileDistance(args.End, neighbor) * (1.0f + 1.0f / 1000.0f);
var fScore = gScore + hScore;
frontier.Enqueue(neighbor, fScore);
}
}
}
else
{
for (var x = -1; x <= 1; x++)
{
for (var y = -1; y <= 1; y++)
{
if (x != 0 && y != 0)
continue;
var neighbor = node + new Vector2i(x, y);
var neighborCost = ManhattanDistance(node, neighbor) * args.TileCost?.Invoke(neighbor) ?? 1f;
if (neighborCost.Equals(0f))
continue;
var gScore = gCost + neighborCost;
if (costSoFar.TryGetValue(neighbor, out var nextValue) && gScore >= nextValue)
continue;
cameFrom[neighbor] = node;
costSoFar[neighbor] = gScore;
// Still use octile even for manhattan distance.
var hScore = OctileDistance(args.End, neighbor) * 1.001f;
var fScore = gScore + hScore;
frontier.Enqueue(neighbor, fScore);
}
}
}
}
return SimplePathResult.NoPath;
}
private List<Vector2i> ReconstructPath(Vector2i end, Dictionary<Vector2i, Vector2i> cameFrom)
{
var path = new List<Vector2i>()
{
end,
};
var node = end;
while (cameFrom.TryGetValue(node, out var source))
{
path.Add(source);
node = source;
}
path.Reverse();
return path;
}
}

View File

@@ -0,0 +1,180 @@
using Robust.Shared.Collections;
using Robust.Shared.Random;
namespace Content.Server.NPC.Pathfinding;
public sealed partial class PathfindingSystem
{
public record struct SimplifyPathArgs
{
public Vector2i Start;
public Vector2i End;
public List<Vector2i> Path;
}
public record struct SplinePathResult()
{
public static SplinePathResult NoPath = new();
public List<Vector2i> Points = new();
public List<Vector2i> Path = new();
public Dictionary<Vector2i, Vector2i> CameFrom;
}
public record struct SplinePathArgs(SimplePathArgs Args)
{
public SimplePathArgs Args = Args;
public float MaxRatio = 0.25f;
/// <summary>
/// Minimum distance between subdivisions.
/// </summary>
public int Distance = 20;
}
/// <summary>
/// Gets a spline path from start to end.
/// </summary>
public SplinePathResult GetSplinePath(SplinePathArgs args, Random random)
{
var start = args.Args.Start;
var end = args.Args.End;
var path = new List<Vector2i>();
var pairs = new ValueList<(Vector2i Start, Vector2i End)> { (start, end) };
var subdivided = true;
// Sub-divide recursively
while (subdivided)
{
// Sometimes we might inadvertantly get 2 nodes too close together so better to just check each one as it comes up instead.
var i = 0;
subdivided = false;
while (i < pairs.Count)
{
var pointA = pairs[i].Start;
var pointB = pairs[i].End;
var vector = pointB - pointA;
var halfway = vector / 2f;
// Finding the point
var adj = halfway.Length();
// Should we even subdivide.
if (adj <= args.Distance)
{
// Just check the next entry no double skip.
i++;
continue;
}
subdivided = true;
var opposite = args.MaxRatio * adj;
var hypotenuse = MathF.Sqrt(MathF.Pow(adj, 2) + MathF.Pow(opposite, 2));
// Okay so essentially we have 2 points and no poly
// We add 2 other points to form a diamond and want some point halfway between randomly offset.
var angle = new Angle(MathF.Atan(opposite / adj));
var pointAPerp = pointA + angle.RotateVec(halfway).Normalized() * hypotenuse;
var pointBPerp = pointA + (-angle).RotateVec(halfway).Normalized() * hypotenuse;
var perpLine = pointBPerp - pointAPerp;
var perpHalfway = perpLine.Length() / 2f;
var splinePoint = (pointAPerp + perpLine.Normalized() * random.NextFloat(-args.MaxRatio, args.MaxRatio) * perpHalfway).Floored();
// We essentially take (A, B) and turn it into (A, C) & (C, B)
pairs[i] = (pointA, splinePoint);
pairs.Insert(i + 1, (splinePoint, pointB));
i+= 2;
}
}
var spline = new ValueList<Vector2i>(pairs.Count - 1)
{
start
};
foreach (var pair in pairs)
{
spline.Add(pair.End);
}
// Now we need to pathfind between each node on the spline.
// TODO: Add rotation version or straight-line version for pathfinder config
// Move the worm pathfinder to here I think.
var cameFrom = new Dictionary<Vector2i, Vector2i>();
// TODO: Need to get rid of the branch bullshit.
var points = new List<Vector2i>();
for (var i = 0; i < spline.Count - 1; i++)
{
var point = spline[i];
var target = spline[i + 1];
points.Add(point);
var aStarArgs = args.Args with { Start = point, End = target };
var aStarResult = GetPath(aStarArgs);
if (aStarResult == SimplePathResult.NoPath)
return SplinePathResult.NoPath;
path.AddRange(aStarResult.Path[0..]);
foreach (var a in aStarResult.CameFrom)
{
cameFrom[a.Key] = a.Value;
}
}
points.Add(spline[^1]);
var simple = SimplifyPath(new SimplifyPathArgs()
{
Start = args.Args.Start,
End = args.Args.End,
Path = path,
});
return new SplinePathResult()
{
Path = simple,
CameFrom = cameFrom,
Points = points,
};
}
/// <summary>
/// Does a simpler pathfinder over the nodes to prune unnecessary branches.
/// </summary>
public List<Vector2i> SimplifyPath(SimplifyPathArgs args)
{
var nodes = new HashSet<Vector2i>(args.Path);
var result = GetBreadthPath(new BreadthPathArgs()
{
Start = args.Start,
Ends = new List<Vector2i>()
{
args.End,
},
TileCost = node =>
{
if (!nodes.Contains(node))
return 0f;
return 1f;
}
});
return result.Path;
}
}

View File

@@ -0,0 +1,89 @@
using System.Numerics;
using Robust.Shared.Random;
namespace Content.Server.NPC.Pathfinding;
public sealed partial class PathfindingSystem
{
/// <summary>
/// Widens the path by the specified amount.
/// </summary>
public HashSet<Vector2i> GetWiden(WidenArgs args, Random random)
{
var tiles = new HashSet<Vector2i>(args.Path.Count * 2);
var variance = (args.MaxWiden - args.MinWiden) / 2f + args.MinWiden;
var counter = 0;
foreach (var tile in args.Path)
{
counter++;
if (counter != args.TileSkip)
continue;
counter = 0;
var center = new Vector2(tile.X + 0.5f, tile.Y + 0.5f);
if (args.Square)
{
for (var x = -variance; x <= variance; x++)
{
for (var y = -variance; y <= variance; y++)
{
var neighbor = center + new Vector2(x, y);
tiles.Add(neighbor.Floored());
}
}
}
else
{
for (var x = -variance; x <= variance; x++)
{
for (var y = -variance; y <= variance; y++)
{
var offset = new Vector2(x, y);
if (offset.Length() > variance)
continue;
var neighbor = center + offset;
tiles.Add(neighbor.Floored());
}
}
}
variance += random.NextFloat(-args.Variance * args.TileSkip, args.Variance * args.TileSkip);
variance = Math.Clamp(variance, args.MinWiden, args.MaxWiden);
}
return tiles;
}
public record struct WidenArgs()
{
public bool Square = false;
/// <summary>
/// How many tiles to skip between iterations., 1-in-n
/// </summary>
public int TileSkip = 3;
/// <summary>
/// Maximum amount to vary per tile.
/// </summary>
public float Variance = 0.25f;
/// <summary>
/// Minimum width.
/// </summary>
public float MinWiden = 2f;
public float MaxWiden = 7f;
public List<Vector2i> Path;
}
}

View File

@@ -142,6 +142,13 @@ public sealed partial class NPCSteeringSystem
// Grab the target position, either the next path node or our end goal.. // Grab the target position, either the next path node or our end goal..
var targetCoordinates = GetTargetCoordinates(steering); var targetCoordinates = GetTargetCoordinates(steering);
if (!targetCoordinates.IsValid(EntityManager))
{
steering.Status = SteeringStatus.NoPath;
return false;
}
var needsPath = false; var needsPath = false;
// If the next node is invalid then get new ones // If the next node is invalid then get new ones
@@ -243,6 +250,14 @@ public sealed partial class NPCSteeringSystem
// Alright just adjust slightly and grab the next node so we don't stop moving for a tick. // Alright just adjust slightly and grab the next node so we don't stop moving for a tick.
// TODO: If it's the last node just grab the target instead. // TODO: If it's the last node just grab the target instead.
targetCoordinates = GetTargetCoordinates(steering); targetCoordinates = GetTargetCoordinates(steering);
if (!targetCoordinates.IsValid(EntityManager))
{
SetDirection(mover, steering, Vector2.Zero);
steering.Status = SteeringStatus.NoPath;
return false;
}
targetMap = targetCoordinates.ToMap(EntityManager, _transform); targetMap = targetCoordinates.ToMap(EntityManager, _transform);
// Can't make it again. // Can't make it again.

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;
using Content.Shared.Procedural.DungeonGenerators; using Content.Shared.Procedural.DungeonGenerators;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Utility; using Robust.Shared.Utility;
namespace Content.Server.Procedural; namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob public sealed partial class DungeonJob
{ {
private async Task<Dungeon> GenerateNoiseDungeon(NoiseDunGen dungen, EntityUid gridUid, MapGridComponent grid, /// <summary>
int seed) /// <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 tiles = new List<(Vector2i, Tile)>();
var matrix = Matrix3Helpers.CreateTranslation(position);
foreach (var layer in dungen.Layers) foreach (var layer in dungen.Layers)
{ {
@@ -30,7 +36,7 @@ public sealed partial class DungeonJob
var frontier = new Queue<Vector2i>(); var frontier = new Queue<Vector2i>();
var rooms = new List<DungeonRoom>(); var rooms = new List<DungeonRoom>();
var tileCount = 0; var tileCount = 0;
var tileCap = rand.NextGaussian(dungen.TileCap, dungen.CapStd); var tileCap = random.NextGaussian(dungen.TileCap, dungen.CapStd);
var visited = new HashSet<Vector2i>(); var visited = new HashSet<Vector2i>();
while (iterations > 0 && tileCount < tileCap) while (iterations > 0 && tileCount < tileCap)
@@ -39,22 +45,22 @@ public sealed partial class DungeonJob
iterations--; iterations--;
// Get a random exterior tile to start floodfilling from. // Get a random exterior tile to start floodfilling from.
var edge = rand.Next(4); var edge = random.Next(4);
Vector2i seedTile; Vector2i seedTile;
switch (edge) switch (edge)
{ {
case 0: 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; break;
case 1: 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; break;
case 2: 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; break;
case 3: 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; break;
default: default:
throw new ArgumentOutOfRangeException(); throw new ArgumentOutOfRangeException();
@@ -80,14 +86,20 @@ public sealed partial class DungeonJob
if (value < layer.Threshold) if (value < layer.Threshold)
continue; continue;
roomArea = roomArea.UnionTile(node);
foundNoise = true; foundNoise = true;
noiseFill = true; noiseFill = true;
var tileDef = _tileDefManager[layer.Tile];
var variant = _tile.PickVariant((ContentTileDefinition) tileDef, rand);
tiles.Add((node, new Tile(tileDef.TileId, variant: variant))); // Still want the tile to gen as normal but can't do anything with it.
roomTiles.Add(node); 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++; tileCount++;
break; break;
} }
@@ -123,7 +135,7 @@ public sealed partial class DungeonJob
foreach (var tile in roomTiles) foreach (var tile in roomTiles)
{ {
center += tile + grid.TileSizeHalfVector; center += tile + _grid.TileSizeHalfVector;
} }
center /= roomTiles.Count; center /= roomTiles.Count;
@@ -132,15 +144,8 @@ public sealed partial class DungeonJob
ValidateResume(); ValidateResume();
} }
grid.SetTiles(tiles); _maps.SetTiles(_gridUid, _grid, tiles);
var dungeon = new Dungeon(rooms); var dungeon = new Dungeon(rooms);
foreach (var tile in tiles)
{
dungeon.RoomTiles.Add(tile.Item1);
}
return dungeon; 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.Numerics;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Shared.Decals;
using Content.Shared.Procedural; using Content.Shared.Procedural;
using Content.Shared.Procedural.DungeonGenerators; using Content.Shared.Procedural.DungeonGenerators;
using Content.Shared.Whitelist;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Utility; using Robust.Shared.Utility;
namespace Content.Server.Procedural; namespace Content.Server.Procedural.DungeonJob;
public sealed partial class 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); if (!data.Tiles.TryGetValue(DungeonDataKey.FallbackTile, out var tileProto) ||
var preset = prefab.Presets[random.Next(prefab.Presets.Count)]; !data.Whitelists.TryGetValue(DungeonDataKey.Rooms, out var roomWhitelist))
var gen = _prototype.Index<DungeonPresetPrototype>(preset); {
LogDataError(typeof(PrefabDunGen));
return Dungeon.Empty;
}
var dungeonRotation = _dungeon.GetDungeonRotation(seed); var preset = prefab.Presets[random.Next(prefab.Presets.Count)];
var dungeonTransform = Matrix3Helpers.CreateTransform(_position, dungeonRotation); var gen = _prototype.Index(preset);
var dungeonRotation = _dungeon.GetDungeonRotation(random.Next());
var dungeonTransform = Matrix3Helpers.CreateTransform(position, dungeonRotation);
var roomPackProtos = new Dictionary<Vector2i, List<DungeonRoomPackPrototype>>(); var roomPackProtos = new Dictionary<Vector2i, List<DungeonRoomPackPrototype>>();
foreach (var pack in _prototype.EnumeratePrototypes<DungeonRoomPackPrototype>()) foreach (var pack in _prototype.EnumeratePrototypes<DungeonRoomPackPrototype>())
@@ -42,12 +50,15 @@ public sealed partial class DungeonJob
{ {
var whitelisted = false; 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; if (proto.Tags.Contains(tag))
break; {
whitelisted = true;
break;
}
} }
} }
@@ -182,12 +193,16 @@ public sealed partial class DungeonJob
{ {
for (var y = roomSize.Bottom; y < roomSize.Top; y++) for (var y = roomSize.Bottom; y < roomSize.Top; y++)
{ {
var index = Vector2.Transform(new Vector2(x, y) + grid.TileSizeHalfVector - packCenter, matty).Floored(); var index = Vector2.Transform(new Vector2(x, y) + _grid.TileSizeHalfVector - packCenter, matty).Floored();
tiles.Add((index, new Tile(_tileDefManager["FloorPlanetGrass"].TileId)));
if (reservedTiles.Contains(index))
continue;
tiles.Add((index, new Tile(_tileDefManager[tileProto].TileId)));
} }
} }
grid.SetTiles(tiles); _maps.SetTiles(_gridUid, _grid, tiles);
tiles.Clear(); tiles.Clear();
_sawmill.Error($"Unable to find room variant for {roomDimensions}, leaving empty."); _sawmill.Error($"Unable to find room variant for {roomDimensions}, leaving empty.");
continue; continue;
@@ -215,12 +230,12 @@ public sealed partial class DungeonJob
var dungeonMatty = Matrix3x2.Multiply(matty, dungeonTransform); var dungeonMatty = Matrix3x2.Multiply(matty, dungeonTransform);
// The expensive bit yippy. // 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 roomTiles = new HashSet<Vector2i>(room.Size.X * room.Size.Y);
var exterior = new HashSet<Vector2i>(room.Size.X * 2 + room.Size.Y * 2); 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; Box2i? mapBounds = null;
for (var x = -1; x <= room.Size.X; x++) for (var x = -1; x <= room.Size.X; x++)
@@ -232,8 +247,12 @@ public sealed partial class DungeonJob
continue; continue;
} }
var tilePos = Vector2.Transform(new Vector2i(x + room.Offset.X, y + room.Offset.Y) + tileOffset, dungeonMatty); var tilePos = Vector2.Transform(new Vector2i(x + room.Offset.X, y + room.Offset.Y) + tileOffset, dungeonMatty).Floored();
exterior.Add(tilePos.Floored());
if (reservedTiles.Contains(tilePos))
continue;
exterior.Add(tilePos);
} }
} }
@@ -249,16 +268,18 @@ public sealed partial class DungeonJob
roomTiles.Add(tileIndex); roomTiles.Add(tileIndex);
mapBounds = mapBounds?.Union(tileIndex) ?? new Box2i(tileIndex, tileIndex); mapBounds = mapBounds?.Union(tileIndex) ?? new Box2i(tileIndex, tileIndex);
center += tilePos + grid.TileSizeHalfVector; center += tilePos + _grid.TileSizeHalfVector;
} }
} }
center /= roomTiles.Count; center /= roomTiles.Count;
dungeon.Rooms.Add(new DungeonRoom(roomTiles, center, mapBounds!.Value, exterior)); dungeon.AddRoom(new DungeonRoom(roomTiles, center, mapBounds!.Value, exterior));
await SuspendIfOutOfTime(); await SuspendDungeon();
ValidateResume();
if (!ValidateResume())
return Dungeon.Empty;
} }
} }
@@ -267,20 +288,16 @@ public sealed partial class DungeonJob
foreach (var room in dungeon.Rooms) foreach (var room in dungeon.Rooms)
{ {
dungeon.RoomTiles.UnionWith(room.Tiles); dungeonCenter += room.Center;
dungeon.RoomExteriorTiles.UnionWith(room.Exterior); SetDungeonEntrance(dungeon, room, reservedTiles, random);
} }
foreach (var room in dungeon.Rooms) dungeon.Rebuild();
{
dungeonCenter += room.Center;
SetDungeonEntrance(dungeon, room, random);
}
return dungeon; 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. // TODO: Move to dungeonsystem.
@@ -323,8 +340,10 @@ public sealed partial class DungeonJob
continue; continue;
} }
if (reservedTiles.Contains(entrancePos))
continue;
room.Entrances.Add(entrancePos); room.Entrances.Add(entrancePos);
dungeon.Entrances.Add(entrancePos);
break; 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;
using Content.Shared.Procedural.PostGeneration; using Content.Shared.Procedural.PostGeneration;
using Content.Shared.Storage; using Content.Shared.Storage;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Components;
using Robust.Shared.Random; using Robust.Shared.Random;
namespace Content.Server.Procedural; namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob public sealed partial class DungeonJob
{ {
private async Task PostGen(CorridorClutterPostGen gen, Dungeon dungeon, EntityUid gridUid, MapGridComponent grid, /// <summary>
Random random) /// <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 physicsQuery = _entManager.GetEntityQuery<PhysicsComponent>();
var count = (int) Math.Ceiling(dungeon.CorridorTiles.Count * gen.Chance); 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.Linq;
using System.Numerics;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Shared.Procedural; using Content.Shared.Procedural;
using Content.Shared.Procedural.PostGeneration; using Content.Shared.Procedural.PostGeneration;
using Robust.Shared.Collections; using Robust.Shared.Collections;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Utility; using Robust.Shared.Utility;
namespace Content.Server.Procedural; namespace Content.Server.Procedural.DungeonJob;
public sealed partial class DungeonJob public sealed partial class DungeonJob
{ {
/// <summary> /// <summary>
/// Tries to connect rooms via worm-like corridors. /// <see cref="WormCorridorDunGen"/>
/// </summary> /// </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)>(); var networks = new List<(Vector2i Start, HashSet<Vector2i> Network)>();
// List of places to start from. // List of places to start from.
@@ -32,7 +36,7 @@ public sealed partial class DungeonJob
networks.Add((entrance, network)); networks.Add((entrance, network));
// Point away from the room to start with. // 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. // Find a random network to worm from.
var startIndex = (i % networks.Count); var startIndex = (i % networks.Count);
var startPos = networks[startIndex].Start; var startPos = networks[startIndex].Start;
var position = startPos + grid.TileSizeHalfVector; var position = startPos + _grid.TileSizeHalfVector;
var remainingLength = gen.Length; var remainingLength = gen.Length;
worm.Clear(); worm.Clear();
@@ -108,7 +112,7 @@ public sealed partial class DungeonJob
costSoFar[startNode] = 0f; costSoFar[startNode] = 0f;
var count = 0; var count = 0;
await SuspendIfOutOfTime(); await SuspendDungeon();
if (!ValidateResume()) if (!ValidateResume())
return; return;
@@ -174,9 +178,9 @@ public sealed partial class DungeonJob
WidenCorridor(dungeon, gen.Width, main.Network); WidenCorridor(dungeon, gen.Width, main.Network);
dungeon.CorridorTiles.UnionWith(main.Network); dungeon.CorridorTiles.UnionWith(main.Network);
BuildCorridorExterior(dungeon); BuildCorridorExterior(dungeon);
dungeon.RefreshAllTiles();
var tiles = new List<(Vector2i Index, Tile Tile)>(); var tiles = new List<(Vector2i Index, Tile Tile)>();
var tileDef = _prototype.Index(gen.Tile);
foreach (var tile in dungeon.CorridorTiles) 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)); dungeonUid = EntityManager.CreateEntityUninitialized(null, new EntityCoordinates(dungeonUid, position));
dungeonGrid = EntityManager.AddComponent<MapGridComponent>(dungeonUid); dungeonGrid = EntityManager.AddComponent<MapGridComponent>(dungeonUid);
EntityManager.InitializeAndStartEntity(dungeonUid, mapId); 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; int seed;

View File

@@ -64,6 +64,7 @@ public sealed partial class DungeonSystem
Vector2i origin, Vector2i origin,
DungeonRoomPrototype room, DungeonRoomPrototype room,
Random random, Random random,
HashSet<Vector2i>? reservedTiles,
bool clearExisting = false, bool clearExisting = false,
bool rotation = false) bool rotation = false)
{ {
@@ -78,7 +79,7 @@ public sealed partial class DungeonSystem
var roomTransform = Matrix3Helpers.CreateTransform((Vector2) room.Size / 2f, roomRotation); var roomTransform = Matrix3Helpers.CreateTransform((Vector2) room.Size / 2f, roomRotation);
var finalTransform = Matrix3x2.Multiply(roomTransform, originTransform); 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) public Angle GetRoomRotation(DungeonRoomPrototype room, Random random)
@@ -103,6 +104,7 @@ public sealed partial class DungeonSystem
MapGridComponent grid, MapGridComponent grid,
Matrix3x2 roomTransform, Matrix3x2 roomTransform,
DungeonRoomPrototype room, DungeonRoomPrototype room,
HashSet<Vector2i>? reservedTiles = null,
bool clearExisting = false) bool clearExisting = false)
{ {
// Ensure the underlying template exists. // Ensure the underlying template exists.
@@ -150,6 +152,10 @@ public sealed partial class DungeonSystem
var tilePos = Vector2.Transform(indices + tileOffset, roomTransform); var tilePos = Vector2.Transform(indices + tileOffset, roomTransform);
var rounded = tilePos.Floored(); var rounded = tilePos.Floored();
if (!clearExisting && reservedTiles?.Contains(rounded) == true)
continue;
_tiles.Add((rounded, tileRef.Tile)); _tiles.Add((rounded, tileRef.Tile));
} }
} }
@@ -165,6 +171,10 @@ public sealed partial class DungeonSystem
{ {
var templateXform = _xformQuery.GetComponent(templateEnt); var templateXform = _xformQuery.GetComponent(templateEnt);
var childPos = Vector2.Transform(templateXform.LocalPosition - roomCenter, roomTransform); var childPos = Vector2.Transform(templateXform.LocalPosition - roomCenter, roomTransform);
if (!clearExisting && reservedTiles?.Contains(childPos.Floored()) == true)
continue;
var childRot = templateXform.LocalRotation + finalRoomRotation; var childRot = templateXform.LocalRotation + finalRoomRotation;
var protoId = _metaQuery.GetComponent(templateEnt).EntityPrototype?.ID; 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 // 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. // 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. // 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); var position = Vector2.Transform(decal.Coordinates + grid.TileSizeHalfVector - roomCenter, roomTransform);
position -= Vector2Helpers.Half; position -= grid.TileSizeHalfVector;
if (!clearExisting && reservedTiles?.Contains(position.Floored()) == true)
continue;
// Umm uhh I love decals so uhhhh idk what to do about this // Umm uhh I love decals so uhhhh idk what to do about this
var angle = (decal.Angle + finalRoomRotation).Reduced(); var angle = (decal.Angle + finalRoomRotation).Reduced();

View File

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

View File

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

View File

@@ -176,9 +176,11 @@ public sealed class SpawnSalvageMissionJob : Job<bool>
dungeonOffset = dungeonRotation.RotateVec(dungeonOffset); dungeonOffset = dungeonRotation.RotateVec(dungeonOffset);
var dungeonMod = _prototypeManager.Index<SalvageDungeonModPrototype>(mission.Dungeon); var dungeonMod = _prototypeManager.Index<SalvageDungeonModPrototype>(mission.Dungeon);
var dungeonConfig = _prototypeManager.Index<DungeonConfigPrototype>(dungeonMod.Proto); var dungeonConfig = _prototypeManager.Index<DungeonConfigPrototype>(dungeonMod.Proto);
var dungeon = await WaitAsyncTask(_dungeon.GenerateDungeonAsync(dungeonConfig, mapUid, grid, (Vector2i) dungeonOffset, var dungeons = await WaitAsyncTask(_dungeon.GenerateDungeonAsync(dungeonConfig, mapUid, grid, (Vector2i) dungeonOffset,
_missionParams.Seed)); _missionParams.Seed));
var dungeon = dungeons.First();
// Aborty // Aborty
if (dungeon.Rooms.Count == 0) if (dungeon.Rooms.Count == 0)
{ {

View File

@@ -1,4 +1,6 @@
using Content.Server.Shuttles.Systems; using Content.Server.Shuttles.Systems;
using Content.Shared.Dataset;
using Content.Shared.Procedural;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Utility; using Robust.Shared.Utility;
@@ -14,39 +16,92 @@ public sealed partial class GridSpawnComponent : Component
/// Dictionary of groups where each group will have entries selected. /// Dictionary of groups where each group will have entries selected.
/// String is just an identifier to make yaml easier. /// String is just an identifier to make yaml easier.
/// </summary> /// </summary>
[DataField(required: true)] public Dictionary<string, GridSpawnGroup> Groups = new(); [DataField(required: true)] public Dictionary<string, IGridSpawnGroup> Groups = new();
} }
[DataRecord] public interface IGridSpawnGroup
public record struct GridSpawnGroup
{ {
public List<ResPath> Paths = new(); /// <summary>
public int MinCount = 1; /// Minimum distance to spawn away from the station.
public int MaxCount = 1; /// </summary>
public float MinimumDistance { get; }
/// <inheritdoc />
public ProtoId<DatasetPrototype>? NameDataset { get; }
/// <inheritdoc />
int MinCount { get; set; }
/// <inheritdoc />
int MaxCount { get; set; }
/// <summary> /// <summary>
/// Components to be added to any spawned grids. /// Components to be added to any spawned grids.
/// </summary> /// </summary>
public ComponentRegistry AddComponents = new(); public ComponentRegistry AddComponents { get; set; }
/// <summary> /// <summary>
/// Hide the IFF label of the grid. /// Hide the IFF label of the grid.
/// </summary> /// </summary>
public bool Hide = false; public bool Hide { get; set; }
/// <summary> /// <summary>
/// Should we set the metadata name of a grid. Useful for admin purposes. /// Should we set the metadata name of a grid. Useful for admin purposes.
/// </summary> /// </summary>
public bool NameGrid = false; public bool NameGrid { get; set; }
/// <summary> /// <summary>
/// Should we add this to the station's grids (if possible / relevant). /// Should we add this to the station's grids (if possible / relevant).
/// </summary> /// </summary>
public bool StationGrid = true; public bool StationGrid { get; set; }
}
public GridSpawnGroup() [DataRecord]
{ public sealed class DungeonSpawnGroup : IGridSpawnGroup
} {
/// <summary>
/// Prototypes we can choose from to spawn.
/// </summary>
public List<ProtoId<DungeonConfigPrototype>> Protos = new();
/// <inheritdoc />
public float MinimumDistance { get; }
/// <inheritdoc />
public ProtoId<DatasetPrototype>? NameDataset { get; }
/// <inheritdoc />
public int MinCount { get; set; } = 1;
/// <inheritdoc />
public int MaxCount { get; set; } = 1;
/// <inheritdoc />
public ComponentRegistry AddComponents { get; set; } = new();
/// <inheritdoc />
public bool Hide { get; set; } = false;
/// <inheritdoc />
public bool NameGrid { get; set; } = false;
/// <inheritdoc />
public bool StationGrid { get; set; } = false;
}
[DataRecord]
public sealed class GridSpawnGroup : IGridSpawnGroup
{
public List<ResPath> Paths = new();
public float MinimumDistance { get; }
public ProtoId<DatasetPrototype>? NameDataset { get; }
public int MinCount { get; set; } = 1;
public int MaxCount { get; set; } = 1;
public ComponentRegistry AddComponents { get; set; } = new();
public bool Hide { get; set; } = false;
public bool NameGrid { get; set; } = true;
public bool StationGrid { get; set; } = true;
} }

View File

@@ -1,9 +1,14 @@
using System.Numerics;
using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Components;
using Content.Server.Station.Components; using Content.Server.Station.Components;
using Content.Server.Station.Events; using Content.Server.Station.Events;
using Content.Shared.Cargo.Components; using Content.Shared.Cargo.Components;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Procedural;
using Content.Shared.Salvage;
using Content.Shared.Shuttles.Components; using Content.Shared.Shuttles.Components;
using Robust.Shared.Collections;
using Robust.Shared.Map;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Utility; using Robust.Shared.Utility;
@@ -80,6 +85,76 @@ public sealed partial class ShuttleSystem
_mapManager.DeleteMap(mapId); _mapManager.DeleteMap(mapId);
} }
private bool TryDungeonSpawn(EntityUid targetGrid, EntityUid stationUid, MapId mapId, DungeonSpawnGroup group, out EntityUid spawned)
{
spawned = EntityUid.Invalid;
var dungeonProtoId = _random.Pick(group.Protos);
if (!_protoManager.TryIndex(dungeonProtoId, out var dungeonProto))
{
return false;
}
var spawnCoords = new EntityCoordinates(targetGrid, Vector2.Zero);
if (group.MinimumDistance > 0f)
{
spawnCoords = spawnCoords.Offset(_random.NextVector2(group.MinimumDistance, group.MinimumDistance * 1.5f));
}
var spawnMapCoords = _transform.ToMapCoordinates(spawnCoords);
var spawnedGrid = _mapManager.CreateGridEntity(mapId);
_transform.SetMapCoordinates(spawnedGrid, spawnMapCoords);
_dungeon.GenerateDungeon(dungeonProto, spawnedGrid.Owner, spawnedGrid.Comp, Vector2i.Zero, _random.Next());
spawned = spawnedGrid.Owner;
return true;
}
private bool TryGridSpawn(EntityUid targetGrid, EntityUid stationUid, MapId mapId, GridSpawnGroup group, out EntityUid spawned)
{
spawned = EntityUid.Invalid;
if (group.Paths.Count == 0)
{
Log.Error($"Found no paths for GridSpawn");
return false;
}
var paths = new ValueList<ResPath>();
// Round-robin so we try to avoid dupes where possible.
if (paths.Count == 0)
{
paths.AddRange(group.Paths);
_random.Shuffle(paths);
}
var path = paths[^1];
paths.RemoveAt(paths.Count - 1);
if (_loader.TryLoad(mapId, path.ToString(), out var ent) && ent.Count == 1)
{
if (TryComp<ShuttleComponent>(ent[0], out var shuttle))
{
TryFTLProximity(ent[0], targetGrid);
}
if (group.NameGrid)
{
var name = path.FilenameWithoutExtension;
_metadata.SetEntityName(ent[0], name);
}
spawned = ent[0];
return true;
}
Log.Error($"Error loading gridspawn for {ToPrettyString(stationUid)} / {path}");
return false;
}
private void GridSpawns(EntityUid uid, GridSpawnComponent component) private void GridSpawns(EntityUid uid, GridSpawnComponent component)
{ {
if (!_cfg.GetCVar(CCVars.GridFill)) if (!_cfg.GetCVar(CCVars.GridFill))
@@ -97,81 +172,49 @@ public sealed partial class ShuttleSystem
// Spawn on a dummy map and try to FTL if possible, otherwise dump it. // Spawn on a dummy map and try to FTL if possible, otherwise dump it.
var mapId = _mapManager.CreateMap(); var mapId = _mapManager.CreateMap();
var valid = true;
var paths = new List<ResPath>();
foreach (var group in component.Groups.Values) foreach (var group in component.Groups.Values)
{ {
if (group.Paths.Count == 0) var count = _random.Next(group.MinCount, group.MaxCount + 1);
{
Log.Error($"Found no paths for GridSpawn");
continue;
}
var count = _random.Next(group.MinCount, group.MaxCount);
paths.Clear();
for (var i = 0; i < count; i++) for (var i = 0; i < count; i++)
{ {
// Round-robin so we try to avoid dupes where possible. EntityUid spawned;
if (paths.Count == 0)
switch (group)
{ {
paths.AddRange(group.Paths); case DungeonSpawnGroup dungeon:
_random.Shuffle(paths); if (!TryDungeonSpawn(targetGrid.Value, uid, mapId, dungeon, out spawned))
}
var path = paths[^1];
paths.RemoveAt(paths.Count - 1);
if (_loader.TryLoad(mapId, path.ToString(), out var ent) && ent.Count == 1)
{
if (TryComp<ShuttleComponent>(ent[0], out var shuttle))
{
TryFTLProximity(ent[0], targetGrid.Value);
}
else
{
valid = false;
}
if (group.Hide)
{
var iffComp = EnsureComp<IFFComponent>(ent[0]);
iffComp.Flags |= IFFFlags.HideLabel;
Dirty(ent[0], iffComp);
}
if (group.StationGrid)
{
_station.AddGridToStation(uid, ent[0]);
}
if (group.NameGrid)
{
var name = path.FilenameWithoutExtension;
_metadata.SetEntityName(ent[0], name);
}
foreach (var compReg in group.AddComponents.Values)
{
var compType = compReg.Component.GetType();
if (HasComp(ent[0], compType))
continue; continue;
var comp = _factory.GetComponent(compType); break;
AddComp(ent[0], comp, true); case GridSpawnGroup grid:
} if (!TryGridSpawn(targetGrid.Value, uid, mapId, grid, out spawned))
} continue;
else
{ break;
valid = false; default:
throw new NotImplementedException();
} }
if (!valid) if (_protoManager.TryIndex(group.NameDataset, out var dataset))
{ {
Log.Error($"Error loading gridspawn for {ToPrettyString(uid)} / {path}"); _metadata.SetEntityName(spawned, SharedSalvageSystem.GetFTLName(dataset, _random.Next()));
} }
if (group.Hide)
{
var iffComp = EnsureComp<IFFComponent>(spawned);
iffComp.Flags |= IFFFlags.HideLabel;
Dirty(spawned, iffComp);
}
if (group.StationGrid)
{
_station.AddGridToStation(uid, spawned);
}
EntityManager.AddComponents(spawned, group.AddComponents);
} }
} }

View File

@@ -2,6 +2,7 @@ using Content.Server.Administration.Logs;
using Content.Server.Body.Systems; using Content.Server.Body.Systems;
using Content.Server.Doors.Systems; using Content.Server.Doors.Systems;
using Content.Server.Parallax; using Content.Server.Parallax;
using Content.Server.Procedural;
using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Components;
using Content.Server.Station.Systems; using Content.Server.Station.Systems;
using Content.Server.Stunnable; using Content.Server.Stunnable;
@@ -20,6 +21,7 @@ using Robust.Shared.Map.Components;
using Robust.Shared.Physics; using Robust.Shared.Physics;
using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems; using Robust.Shared.Physics.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Timing; using Robust.Shared.Timing;
@@ -28,15 +30,18 @@ namespace Content.Server.Shuttles.Systems;
[UsedImplicitly] [UsedImplicitly]
public sealed partial class ShuttleSystem : SharedShuttleSystem public sealed partial class ShuttleSystem : SharedShuttleSystem
{ {
[Dependency] private readonly IAdminLogManager _logger = default!;
[Dependency] private readonly IComponentFactory _factory = default!; [Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefManager = default!; [Dependency] private readonly ITileDefinitionManager _tileDefManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly BiomeSystem _biomes = default!; [Dependency] private readonly BiomeSystem _biomes = default!;
[Dependency] private readonly BodySystem _bobby = default!; [Dependency] private readonly BodySystem _bobby = default!;
[Dependency] private readonly DockingSystem _dockSystem = default!; [Dependency] private readonly DockingSystem _dockSystem = default!;
[Dependency] private readonly DungeonSystem _dungeon = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly FixtureSystem _fixtures = default!; [Dependency] private readonly FixtureSystem _fixtures = default!;
[Dependency] private readonly MapLoaderSystem _loader = default!; [Dependency] private readonly MapLoaderSystem _loader = default!;
@@ -52,7 +57,6 @@ public sealed partial class ShuttleSystem : SharedShuttleSystem
[Dependency] private readonly ThrowingSystem _throwing = default!; [Dependency] private readonly ThrowingSystem _throwing = default!;
[Dependency] private readonly ThrusterSystem _thruster = default!; [Dependency] private readonly ThrusterSystem _thruster = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!; [Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly IAdminLogManager _logger = default!;
public const float TileMassMultiplier = 0.5f; public const float TileMassMultiplier = 0.5f;

View File

@@ -0,0 +1,13 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.Procedural.Components;
/// <summary>
/// Indicates this entity prototype should be re-mapped to another
/// </summary>
[RegisterComponent]
public sealed partial class EntityRemapComponent : Component
{
[DataField(required: true)]
public Dictionary<EntProtoId, EntProtoId> Mask = new();
}

View File

@@ -0,0 +1,10 @@
namespace Content.Shared.Procedural.Distance;
/// <summary>
/// Produces a rounder shape useful for more natural areas.
/// </summary>
public sealed partial class DunGenEuclideanSquaredDistance : IDunGenDistance
{
[DataField]
public float BlendWeight { get; set; } = 0.50f;
}

View File

@@ -0,0 +1,10 @@
namespace Content.Shared.Procedural.Distance;
/// <summary>
/// Produces a squarish-shape that's better for filling in most of the area.
/// </summary>
public sealed partial class DunGenSquareBump : IDunGenDistance
{
[DataField]
public float BlendWeight { get; set; } = 0.50f;
}

View File

@@ -0,0 +1,14 @@
namespace Content.Shared.Procedural.Distance;
/// <summary>
/// Used if you want to limit the distance noise is generated by some arbitrary config
/// </summary>
[ImplicitDataDefinitionForInheritors]
public partial interface IDunGenDistance
{
/// <summary>
/// How much to blend between the original noise value and the adjusted one.
/// </summary>
float BlendWeight { get; }
}

View File

@@ -1,8 +1,16 @@
namespace Content.Shared.Procedural; namespace Content.Shared.Procedural;
/// <summary>
/// Procedurally generated dungeon data.
/// </summary>
public sealed class Dungeon public sealed class Dungeon
{ {
public readonly List<DungeonRoom> Rooms; public static Dungeon Empty = new Dungeon();
private List<DungeonRoom> _rooms;
private HashSet<Vector2i> _allTiles = new();
public IReadOnlyList<DungeonRoom> Rooms => _rooms;
/// <summary> /// <summary>
/// Hashset of the tiles across all rooms. /// Hashset of the tiles across all rooms.
@@ -17,18 +25,64 @@ public sealed class Dungeon
public readonly HashSet<Vector2i> Entrances = new(); public readonly HashSet<Vector2i> Entrances = new();
public Dungeon() public IReadOnlySet<Vector2i> AllTiles => _allTiles;
public Dungeon() : this(new List<DungeonRoom>())
{ {
Rooms = new List<DungeonRoom>();
} }
public Dungeon(List<DungeonRoom> rooms) public Dungeon(List<DungeonRoom> rooms)
{ {
Rooms = rooms; // This reftype is mine now.
_rooms = rooms;
foreach (var room in Rooms) foreach (var room in _rooms)
{ {
Entrances.UnionWith(room.Entrances); InternalAddRoom(room);
} }
RefreshAllTiles();
}
public void RefreshAllTiles()
{
_allTiles.Clear();
_allTiles.UnionWith(RoomTiles);
_allTiles.UnionWith(RoomExteriorTiles);
_allTiles.UnionWith(CorridorTiles);
_allTiles.UnionWith(CorridorExteriorTiles);
_allTiles.UnionWith(Entrances);
}
public void Rebuild()
{
_allTiles.Clear();
RoomTiles.Clear();
RoomExteriorTiles.Clear();
Entrances.Clear();
foreach (var room in _rooms)
{
InternalAddRoom(room, false);
}
RefreshAllTiles();
}
public void AddRoom(DungeonRoom room)
{
_rooms.Add(room);
InternalAddRoom(room);
}
private void InternalAddRoom(DungeonRoom room, bool refreshAll = true)
{
Entrances.UnionWith(room.Entrances);
RoomTiles.UnionWith(room.Tiles);
RoomExteriorTiles.UnionWith(room.Exterior);
if (refreshAll)
RefreshAllTiles();
} }
} }

View File

@@ -1,21 +1,53 @@
using Content.Shared.Procedural.DungeonGenerators;
using Content.Shared.Procedural.PostGeneration; using Content.Shared.Procedural.PostGeneration;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
namespace Content.Shared.Procedural; namespace Content.Shared.Procedural;
[Prototype("dungeonConfig")] [Prototype]
public sealed partial class DungeonConfigPrototype : IPrototype public sealed partial class DungeonConfigPrototype : IPrototype
{ {
[IdDataField] [IdDataField]
public string ID { get; private set; } = default!; public string ID { get; private set; } = default!;
[DataField("generator", required: true)] /// <summary>
public IDunGen Generator = default!; /// <see cref="Data"/>
/// </summary>
[DataField]
public DungeonData Data = DungeonData.Empty;
/// <summary> /// <summary>
/// Ran after the main dungeon is created. /// The secret sauce, procedural generation layers that get run.
/// </summary> /// </summary>
[DataField("postGeneration")] [DataField(required: true)]
public List<IPostDunGen> PostGeneration = new(); public List<IDunGenLayer> Layers = new();
/// <summary>
/// Should we reserve the tiles generated by this config so no other dungeons can spawn on it within the same job?
/// </summary>
[DataField]
public bool ReserveTiles;
/// <summary>
/// Minimum times to run the config.
/// </summary>
[DataField]
public int MinCount = 1;
/// <summary>
/// Maximum times to run the config.
/// </summary>
[DataField]
public int MaxCount = 1;
/// <summary>
/// Minimum amount we can offset the dungeon by.
/// </summary>
[DataField]
public int MinOffset;
/// <summary>
/// Maximum amount we can offset the dungeon by.
/// </summary>
[DataField]
public int MaxOffset;
} }

View File

@@ -0,0 +1,105 @@
using System.Linq;
using Content.Shared.Maps;
using Content.Shared.Storage;
using Content.Shared.Whitelist;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared.Procedural;
/// <summary>
/// Used to set dungeon values for all layers.
/// </summary>
/// <remarks>
/// This lets us share data between different dungeon configs without having to repeat entire configs.
/// </remarks>
[DataRecord]
public sealed class DungeonData
{
// I hate this but it also significantly reduces yaml bloat if we add like 10 variations on the same set of layers
// e.g. science rooms, engi rooms, cargo rooms all under PlanetBase for example.
// without having to do weird nesting. It also means we don't need to copy-paste the same prototype across several layers
// The alternative is doing like,
// 2 layer prototype, 1 layer with the specified data, 3 layer prototype, 2 layers with specified data, etc.
// As long as we just keep the code clean over time it won't be bad to maintain.
public static DungeonData Empty = new();
public Dictionary<DungeonDataKey, Color> Colors = new();
public Dictionary<DungeonDataKey, EntProtoId> Entities = new();
public Dictionary<DungeonDataKey, ProtoId<EntitySpawnEntryPrototype>> SpawnGroups = new();
public Dictionary<DungeonDataKey, ProtoId<ContentTileDefinition>> Tiles = new();
public Dictionary<DungeonDataKey, EntityWhitelist> Whitelists = new();
/// <summary>
/// Applies the specified data to this data.
/// </summary>
public void Apply(DungeonData data)
{
// Copy-paste moment.
foreach (var color in data.Colors)
{
Colors[color.Key] = color.Value;
}
foreach (var color in data.Entities)
{
Entities[color.Key] = color.Value;
}
foreach (var color in data.SpawnGroups)
{
SpawnGroups[color.Key] = color.Value;
}
foreach (var color in data.Tiles)
{
Tiles[color.Key] = color.Value;
}
foreach (var color in data.Whitelists)
{
Whitelists[color.Key] = color.Value;
}
}
public DungeonData Clone()
{
return new DungeonData
{
// Only shallow clones but won't matter for DungeonJob purposes.
Colors = Colors.ShallowClone(),
Entities = Entities.ShallowClone(),
SpawnGroups = SpawnGroups.ShallowClone(),
Tiles = Tiles.ShallowClone(),
Whitelists = Whitelists.ShallowClone(),
};
}
}
public enum DungeonDataKey : byte
{
// Colors
Decals,
// Entities
Cabling,
CornerWalls,
Fill,
Junction,
Walls,
// SpawnGroups
CornerClutter,
Entrance,
EntranceFlank,
WallMounts,
Window,
// Tiles
FallbackTile,
WidenTile,
// Whitelists
Rooms,
}

View File

@@ -0,0 +1,13 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.Procedural.DungeonGenerators;
/// <summary>
/// Generates the specified config on an exterior tile of the attached dungeon.
/// Useful if you're using <see cref="GroupDunGen"/> or otherwise want a dungeon on the outside of a grid.
/// </summary>
public sealed partial class ExteriorDunGen : IDunGenLayer
{
[DataField(required: true)]
public ProtoId<DungeonConfigPrototype> Proto;
}

View File

@@ -0,0 +1,10 @@
namespace Content.Shared.Procedural.DungeonGenerators;
/// <summary>
/// Fills unreserved tiles with the specified entity prototype.
/// </summary>
/// <remarks>
/// DungeonData keys are:
/// - Fill
/// </remarks>
public sealed partial class FillGridDunGen : IDunGenLayer;

View File

@@ -1,7 +0,0 @@
namespace Content.Shared.Procedural.DungeonGenerators;
[ImplicitDataDefinitionForInheritors]
public partial interface IDunGen
{
}

View File

@@ -0,0 +1,18 @@
using Content.Shared.Procedural.Distance;
namespace Content.Shared.Procedural.DungeonGenerators;
/// <summary>
/// Like <see cref="Content.Shared.Procedural.DungeonGenerators.NoiseDunGenLayer"/> except with maximum dimensions
/// </summary>
public sealed partial class NoiseDistanceDunGen : IDunGenLayer
{
[DataField]
public IDunGenDistance? DistanceConfig;
[DataField]
public Vector2i Size;
[DataField(required: true)]
public List<NoiseDunGenLayer> Layers = new();
}

View File

@@ -1,15 +1,12 @@
using Content.Shared.Maps; using Content.Shared.Procedural.Distance;
using Robust.Shared.Noise; using Robust.Shared.Noise;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Shared.Procedural.DungeonGenerators; namespace Content.Shared.Procedural.DungeonGenerators;
/// <summary> /// <summary>
/// Generates dungeon flooring based on the specified noise. /// Generates dungeon flooring based on the specified noise.
/// </summary> /// </summary>
public sealed partial class NoiseDunGen : IDunGen public sealed partial class NoiseDunGen : IDunGenLayer
{ {
/* /*
* Floodfills out from 0 until it finds a valid tile. * Floodfills out from 0 until it finds a valid tile.

View File

@@ -1,30 +1,20 @@
using Content.Shared.Maps; using Robust.Shared.Prototypes;
using Content.Shared.Tag;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Shared.Procedural.DungeonGenerators; namespace Content.Shared.Procedural.DungeonGenerators;
/// <summary> /// <summary>
/// Places rooms in pre-selected pack layouts. Chooses rooms from the specified whitelist. /// Places rooms in pre-selected pack layouts. Chooses rooms from the specified whitelist.
/// </summary> /// </summary>
public sealed partial class PrefabDunGen : IDunGen /// <remarks>
/// DungeonData keys are:
/// - FallbackTile
/// - Rooms
/// </remarks>
public sealed partial class PrefabDunGen : IDunGenLayer
{ {
/// <summary>
/// Rooms need to match any of these tags
/// </summary>
[DataField("roomWhitelist", customTypeSerializer:typeof(PrototypeIdListSerializer<TagPrototype>))]
public List<string> RoomWhitelist = new();
/// <summary> /// <summary>
/// Room pack presets we can use for this prefab. /// Room pack presets we can use for this prefab.
/// </summary> /// </summary>
[DataField("presets", required: true, customTypeSerializer:typeof(PrototypeIdListSerializer<DungeonPresetPrototype>))] [DataField(required: true)]
public List<string> Presets = new(); public List<ProtoId<DungeonPresetPrototype>> Presets = new();
/// <summary>
/// Fallback tile.
/// </summary>
[DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
public string Tile = "FloorSteel";
} }

View File

@@ -0,0 +1,13 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.Procedural.DungeonGenerators;
/// <summary>
/// Runs another <see cref="DungeonConfigPrototype"/>.
/// Used for storing data on 1 system.
/// </summary>
public sealed partial class PrototypeDunGen : IDunGenLayer
{
[DataField(required: true)]
public ProtoId<DungeonConfigPrototype> Proto;
}

View File

@@ -0,0 +1,30 @@
using Content.Shared.Maps;
using Robust.Shared.Noise;
using Robust.Shared.Prototypes;
namespace Content.Shared.Procedural.DungeonGenerators;
/// <summary>
/// Replaces existing tiles if they're not empty.
/// </summary>
public sealed partial class ReplaceTileDunGen : IDunGenLayer
{
/// <summary>
/// Chance for a non-variant tile to be used, in case they're too noisy.
/// </summary>
[DataField]
public float VariantWeight = 0.1f;
[DataField(required: true)]
public List<ReplaceTileLayer> Layers = new();
}
[DataRecord]
public record struct ReplaceTileLayer
{
public ProtoId<ContentTileDefinition> Tile;
public float Threshold;
public FastNoiseLite Noise;
}

View File

@@ -0,0 +1,21 @@
using Content.Shared.Storage;
namespace Content.Shared.Procedural.DungeonLayers;
/// <summary>
/// Spawns mobs inside of the dungeon randomly.
/// </summary>
public sealed partial class MobsDunGen : IDunGenLayer
{
// Counts separate to config to avoid some duplication.
[DataField]
public int MinCount = 1;
[DataField]
public int MaxCount = 1;
[DataField(required: true)]
public List<EntitySpawnEntry> Groups = new();
}

View File

@@ -0,0 +1,42 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.Procedural.DungeonLayers;
/// <summary>
/// Generates veins inside of the specified dungeon.
/// </summary>
/// <remarks>
/// Generates on top of existing entities for sanity reasons moreso than performance.
/// </remarks>
public sealed partial class OreDunGen : IDunGenLayer
{
/// <summary>
/// If the vein generation should occur on top of existing entities what are we replacing.
/// </summary>
[DataField]
public EntProtoId? Replacement;
/// <summary>
/// Entity to spawn.
/// </summary>
[DataField(required: true)]
public EntProtoId Entity;
/// <summary>
/// Maximum amount of group spawns
/// </summary>
[DataField]
public int Count = 10;
/// <summary>
/// Minimum entities to spawn in one group.
/// </summary>
[DataField]
public int MinGroupSize = 1;
/// <summary>
/// Maximum entities to spawn in one group.
/// </summary>
[DataField]
public int MaxGroupSize = 1;
}

View File

@@ -2,6 +2,7 @@ using System.Numerics;
namespace Content.Shared.Procedural; namespace Content.Shared.Procedural;
// TODO: Cache center and bounds and shit and don't make the caller deal with it.
public sealed record DungeonRoom(HashSet<Vector2i> Tiles, Vector2 Center, Box2i Bounds, HashSet<Vector2i> Exterior) public sealed record DungeonRoom(HashSet<Vector2i> Tiles, Vector2 Center, Box2i Bounds, HashSet<Vector2i> Exterior)
{ {
public readonly List<Vector2i> Entrances = new(); public readonly List<Vector2i> Entrances = new();

View File

@@ -0,0 +1,7 @@
namespace Content.Shared.Procedural;
[ImplicitDataDefinitionForInheritors]
public partial interface IDunGenLayer
{
}

View File

@@ -0,0 +1,10 @@
namespace Content.Shared.Procedural.PostGeneration;
/// <summary>
/// Runs cables throughout the dungeon.
/// </summary>
/// <remarks>
/// DungeonData keys are:
/// - Cabling
/// </remarks>
public sealed partial class AutoCablingDunGen : IDunGenLayer;

View File

@@ -1,12 +0,0 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.Procedural.PostGeneration;
/// <summary>
/// Runs cables throughout the dungeon.
/// </summary>
public sealed partial class AutoCablingPostGen : IPostDunGen
{
[DataField]
public EntProtoId Entity = "CableApcExtension";
}

View File

@@ -1,5 +1,4 @@
using Content.Shared.Parallax.Biomes; using Content.Shared.Parallax.Biomes;
using Content.Shared.Procedural.PostGeneration;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
namespace Content.Shared.Procedural.PostGeneration; namespace Content.Shared.Procedural.PostGeneration;
@@ -8,7 +7,7 @@ namespace Content.Shared.Procedural.PostGeneration;
/// Generates a biome on top of valid tiles, then removes the biome when done. /// Generates a biome on top of valid tiles, then removes the biome when done.
/// Only works if no existing biome is present. /// Only works if no existing biome is present.
/// </summary> /// </summary>
public sealed partial class BiomePostGen : IPostDunGen public sealed partial class BiomeDunGen : IDunGenLayer
{ {
[DataField(required: true)] [DataField(required: true)]
public ProtoId<BiomeTemplatePrototype> BiomeTemplate; public ProtoId<BiomeTemplatePrototype> BiomeTemplate;

View File

@@ -1,5 +1,3 @@
using Content.Shared.Parallax.Biomes.Markers;
using Content.Shared.Procedural.PostGeneration;
using Content.Shared.Random; using Content.Shared.Random;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
@@ -8,7 +6,7 @@ namespace Content.Shared.Procedural.PostGeneration;
/// <summary> /// <summary>
/// Spawns the specified marker layer on top of the dungeon rooms. /// Spawns the specified marker layer on top of the dungeon rooms.
/// </summary> /// </summary>
public sealed partial class BiomeMarkerLayerPostGen : IPostDunGen public sealed partial class BiomeMarkerLayerDunGen : IDunGenLayer
{ {
/// <summary> /// <summary>
/// How many times to spawn marker layers; can duplicate. /// How many times to spawn marker layers; can duplicate.

View File

@@ -0,0 +1,23 @@
namespace Content.Shared.Procedural.PostGeneration;
/// <summary>
/// Iterates room edges and places the relevant tiles and walls on any free indices.
/// </summary>
/// <remarks>
/// Dungeon data keys are:
/// - CornerWalls (Optional)
/// - FallbackTile
/// - Walls
/// </remarks>
public sealed partial class BoundaryWallDunGen : IDunGenLayer
{
[DataField]
public BoundaryWallFlags Flags = BoundaryWallFlags.Corridors | BoundaryWallFlags.Rooms;
}
[Flags]
public enum BoundaryWallFlags : byte
{
Rooms = 1 << 0,
Corridors = 1 << 1,
}

View File

@@ -1,33 +0,0 @@
using Content.Shared.Maps;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Procedural.PostGeneration;
/// <summary>
/// Iterates room edges and places the relevant tiles and walls on any free indices.
/// </summary>
public sealed partial class BoundaryWallPostGen : IPostDunGen
{
[DataField]
public ProtoId<ContentTileDefinition> Tile = "FloorSteel";
[DataField]
public EntProtoId Wall = "WallSolid";
/// <summary>
/// Walls to use in corners if applicable.
/// </summary>
[DataField]
public string? CornerWall;
[DataField]
public BoundaryWallFlags Flags = BoundaryWallFlags.Corridors | BoundaryWallFlags.Rooms;
}
[Flags]
public enum BoundaryWallFlags : byte
{
Rooms = 1 << 0,
Corridors = 1 << 1,
}

View File

@@ -0,0 +1,14 @@
namespace Content.Shared.Procedural.PostGeneration;
/// <summary>
/// Spawns entities inside corners.
/// </summary>
/// <remarks>
/// Dungeon data keys are:
/// - CornerClutter
/// </remarks>
public sealed partial class CornerClutterDunGen : IDunGenLayer
{
[DataField]
public float Chance = 0.50f;
}

View File

@@ -1,18 +0,0 @@
using Content.Shared.Storage;
namespace Content.Shared.Procedural.PostGeneration;
/// <summary>
/// Spawns entities inside corners.
/// </summary>
public sealed partial class CornerClutterPostGen : IPostDunGen
{
[DataField]
public float Chance = 0.50f;
/// <summary>
/// The default starting bulbs
/// </summary>
[DataField(required: true)]
public List<EntitySpawnEntry> Contents = new();
}

View File

@@ -5,7 +5,7 @@ namespace Content.Shared.Procedural.PostGeneration;
/// <summary> /// <summary>
/// Adds entities randomly to the corridors. /// Adds entities randomly to the corridors.
/// </summary> /// </summary>
public sealed partial class CorridorClutterPostGen : IPostDunGen public sealed partial class CorridorClutterDunGen : IDunGenLayer
{ {
[DataField] [DataField]
public float Chance = 0.05f; public float Chance = 0.05f;

View File

@@ -7,29 +7,23 @@ namespace Content.Shared.Procedural.PostGeneration;
/// <summary> /// <summary>
/// Applies decal skirting to corridors. /// Applies decal skirting to corridors.
/// </summary> /// </summary>
public sealed partial class CorridorDecalSkirtingPostGen : IPostDunGen public sealed partial class CorridorDecalSkirtingDunGen : IDunGenLayer
{ {
/// <summary>
/// Color to apply to decals.
/// </summary>
[DataField("color")]
public Color? Color;
/// <summary> /// <summary>
/// Decal where 1 edge is found. /// Decal where 1 edge is found.
/// </summary> /// </summary>
[DataField("cardinalDecals")] [DataField]
public Dictionary<DirectionFlag, string> CardinalDecals = new(); public Dictionary<DirectionFlag, string> CardinalDecals = new();
/// <summary> /// <summary>
/// Decal where 1 corner edge is found. /// Decal where 1 corner edge is found.
/// </summary> /// </summary>
[DataField("pocketDecals")] [DataField]
public Dictionary<Direction, string> PocketDecals = new(); public Dictionary<Direction, string> PocketDecals = new();
/// <summary> /// <summary>
/// Decal where 2 or 3 edges are found. /// Decal where 2 or 3 edges are found.
/// </summary> /// </summary>
[DataField("cornerDecals")] [DataField]
public Dictionary<DirectionFlag, string> CornerDecals = new(); public Dictionary<DirectionFlag, string> CornerDecals = new();
} }

View File

@@ -1,12 +1,13 @@
using Content.Shared.Maps;
using Robust.Shared.Prototypes;
namespace Content.Shared.Procedural.PostGeneration; namespace Content.Shared.Procedural.PostGeneration;
/// <summary> /// <summary>
/// Connects room entrances via corridor segments. /// Connects room entrances via corridor segments.
/// </summary> /// </summary>
public sealed partial class CorridorPostGen : IPostDunGen /// <remarks>
/// Dungeon data keys are:
/// - FallbackTile
/// </remarks>
public sealed partial class CorridorDunGen : IDunGenLayer
{ {
/// <summary> /// <summary>
/// How far we're allowed to generate a corridor before calling it. /// How far we're allowed to generate a corridor before calling it.
@@ -17,9 +18,6 @@ public sealed partial class CorridorPostGen : IPostDunGen
[DataField] [DataField]
public int PathLimit = 2048; public int PathLimit = 2048;
[DataField]
public ProtoId<ContentTileDefinition> Tile = "FloorSteel";
/// <summary> /// <summary>
/// How wide to make the corridor. /// How wide to make the corridor.
/// </summary> /// </summary>

View File

@@ -0,0 +1,18 @@
namespace Content.Shared.Procedural.PostGeneration;
/// <summary>
/// Selects [count] rooms and places external doors to them.
/// </summary>
/// <remarks>
/// Dungeon data keys are:
/// - Entrance
/// - FallbackTile
/// </remarks>
public sealed partial class DungeonEntranceDunGen : IDunGenLayer
{
/// <summary>
/// How many rooms we place doors on.
/// </summary>
[DataField]
public int Count = 1;
}

View File

@@ -1,28 +0,0 @@
using Content.Shared.Maps;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Shared.Procedural.PostGeneration;
/// <summary>
/// Selects [count] rooms and places external doors to them.
/// </summary>
public sealed partial class DungeonEntrancePostGen : IPostDunGen
{
/// <summary>
/// How many rooms we place doors on.
/// </summary>
[DataField("count")]
public int Count = 1;
[DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
public List<string?> Entities = new()
{
"CableApcExtension",
"AirlockGlass",
};
[DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
public string Tile = "FloorSteel";
}

View File

@@ -0,0 +1,11 @@
namespace Content.Shared.Procedural.PostGeneration;
/// <summary>
/// Spawns entities on either side of an entrance.
/// </summary>
/// <remarks>
/// Dungeon data keys are:
/// - FallbackTile
/// -
/// </remarks>
public sealed partial class EntranceFlankDunGen : IDunGenLayer;

View File

@@ -1,16 +0,0 @@
using Content.Shared.Maps;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Procedural.PostGeneration;
/// <summary>
/// Spawns entities on either side of an entrance.
/// </summary>
public sealed partial class EntranceFlankPostGen : IPostDunGen
{
[DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
public string Tile = "FloorSteel";
[DataField("entities")]
public List<string?> Entities = new();
}

View File

@@ -0,0 +1,11 @@
namespace Content.Shared.Procedural.PostGeneration;
/// <summary>
/// If external areas are found will try to generate windows.
/// </summary>
/// <remarks>
/// Dungeon data keys are:
/// - EntranceFlank
/// - FallbackTile
/// </remarks>
public sealed partial class ExternalWindowDunGen : IDunGenLayer;

View File

@@ -1,22 +0,0 @@
using Content.Shared.Maps;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Shared.Procedural.PostGeneration;
/// <summary>
/// If external areas are found will try to generate windows.
/// </summary>
public sealed partial class ExternalWindowPostGen : IPostDunGen
{
[DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
public List<string?> Entities = new()
{
"Grille",
"Window",
};
[DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
public string Tile = "FloorSteel";
}

View File

@@ -1,10 +0,0 @@
namespace Content.Shared.Procedural.PostGeneration;
/// <summary>
/// Ran after generating dungeon rooms. Can be used for additional loot, contents, etc.
/// </summary>
[ImplicitDataDefinitionForInheritors]
public partial interface IPostDunGen
{
}

View File

@@ -0,0 +1,11 @@
namespace Content.Shared.Procedural.PostGeneration;
/// <summary>
/// If internal areas are found will try to generate windows.
/// </summary>
/// <remarks>
/// Dungeon data keys are:
/// - FallbackTile
/// - Window
/// </remarks>
public sealed partial class InternalWindowDunGen : IDunGenLayer;

View File

@@ -1,22 +0,0 @@
using Content.Shared.Maps;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Shared.Procedural.PostGeneration;
/// <summary>
/// If internal areas are found will try to generate windows.
/// </summary>
public sealed partial class InternalWindowPostGen : IPostDunGen
{
[DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
public List<string?> Entities = new()
{
"Grille",
"Window",
};
[DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
public string Tile = "FloorSteel";
}

View File

@@ -0,0 +1,18 @@
namespace Content.Shared.Procedural.PostGeneration;
/// <summary>
/// Places the specified entities at junction areas.
/// </summary>
/// <remarks>
/// Dungeon data keys are:
/// - Entrance
/// - FallbackTile
/// </remarks>
public sealed partial class JunctionDunGen : IDunGenLayer
{
/// <summary>
/// Width to check for junctions.
/// </summary>
[DataField]
public int Width = 3;
}

View File

@@ -1,28 +0,0 @@
using Content.Shared.Maps;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Shared.Procedural.PostGeneration;
/// <summary>
/// Places the specified entities at junction areas.
/// </summary>
public sealed partial class JunctionPostGen : IPostDunGen
{
/// <summary>
/// Width to check for junctions.
/// </summary>
[DataField("width")]
public int Width = 3;
[DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
public string Tile = "FloorSteel";
[DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
public List<string?> Entities = new()
{
"CableApcExtension",
"AirlockGlass"
};
}

View File

@@ -0,0 +1,19 @@
namespace Content.Shared.Procedural.PostGeneration;
/// <summary>
/// Places the specified entities on the middle connections between rooms
/// </summary>
public sealed partial class MiddleConnectionDunGen : IDunGenLayer
{
/// <summary>
/// How much overlap there needs to be between 2 rooms exactly.
/// </summary>
[DataField]
public int OverlapCount = -1;
/// <summary>
/// How many connections to spawn between rooms.
/// </summary>
[DataField]
public int Count = 1;
}

View File

@@ -1,39 +0,0 @@
using Content.Shared.Maps;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Shared.Procedural.PostGeneration;
/// <summary>
/// Places the specified entities on the middle connections between rooms
/// </summary>
public sealed partial class MiddleConnectionPostGen : IPostDunGen
{
/// <summary>
/// How much overlap there needs to be between 2 rooms exactly.
/// </summary>
[DataField("overlapCount")]
public int OverlapCount = -1;
/// <summary>
/// How many connections to spawn between rooms.
/// </summary>
[DataField("count")]
public int Count = 1;
[DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
public string Tile = "FloorSteel";
[DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
public List<string?> Entities = new()
{
"CableApcExtension",
"AirlockGlass"
};
/// <summary>
/// If overlap > 1 then what should spawn on the edges.
/// </summary>
[DataField("edgeEntities")] public List<string?> EdgeEntities = new();
}

View File

@@ -0,0 +1,11 @@
namespace Content.Shared.Procedural.PostGeneration;
/// <summary>
/// Places tiles / entities onto room entrances.
/// </summary>
/// <remarks>
/// DungeonData keys are:
/// - Entrance
/// - FallbackTile
/// </remarks>
public sealed partial class RoomEntranceDunGen : IDunGenLayer;

View File

@@ -1,22 +0,0 @@
using Content.Shared.Maps;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Shared.Procedural.PostGeneration;
/// <summary>
/// Places tiles / entities onto room entrances.
/// </summary>
public sealed partial class RoomEntrancePostGen : IPostDunGen
{
[DataField("entities", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
public List<string?> Entities = new()
{
"CableApcExtension",
"AirlockGlass",
};
[DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
public string Tile = "FloorSteel";
}

View File

@@ -0,0 +1,19 @@
namespace Content.Shared.Procedural.PostGeneration;
/// <summary>
/// Connects dungeons via points that get subdivided.
/// </summary>
public sealed partial class SplineDungeonConnectorDunGen : IDunGenLayer
{
/// <summary>
/// Will divide the distance between the start and end points so that no subdivision is more than these metres away.
/// </summary>
[DataField]
public int DivisionDistance = 10;
/// <summary>
/// How much each subdivision can vary from the middle.
/// </summary>
[DataField]
public float VarianceMax = 0.35f;
}

View File

@@ -0,0 +1,13 @@
namespace Content.Shared.Procedural.PostGeneration;
/// <summary>
/// Spawns on the boundary tiles of rooms.
/// </summary>
public sealed partial class WallMountDunGen : IDunGenLayer
{
/// <summary>
/// Chance per free tile to spawn a wallmount.
/// </summary>
[DataField]
public double Prob = 0.1;
}

View File

@@ -1,23 +0,0 @@
using Content.Shared.Maps;
using Content.Shared.Storage;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Procedural.PostGeneration;
/// <summary>
/// Spawns on the boundary tiles of rooms.
/// </summary>
public sealed partial class WallMountPostGen : IPostDunGen
{
[DataField("tile", customTypeSerializer:typeof(PrototypeIdSerializer<ContentTileDefinition>))]
public string Tile = "FloorSteel";
[DataField("spawns")]
public List<EntitySpawnEntry> Spawns = new();
/// <summary>
/// Chance per free tile to spawn a wallmount.
/// </summary>
[DataField("prob")]
public double Prob = 0.1;
}

View File

@@ -1,14 +1,10 @@
using Content.Shared.Maps;
using Content.Shared.Procedural.DungeonGenerators;
using Robust.Shared.Prototypes;
namespace Content.Shared.Procedural.PostGeneration; namespace Content.Shared.Procedural.PostGeneration;
// Ime a worm // Ime a worm
/// <summary> /// <summary>
/// Generates worm corridors. /// Generates worm corridors.
/// </summary> /// </summary>
public sealed partial class WormCorridorPostGen : IPostDunGen public sealed partial class WormCorridorDunGen : IDunGenLayer
{ {
[DataField] [DataField]
public int PathLimit = 2048; public int PathLimit = 2048;
@@ -31,9 +27,6 @@ public sealed partial class WormCorridorPostGen : IPostDunGen
[DataField] [DataField]
public Angle MaxAngleChange = Angle.FromDegrees(45); public Angle MaxAngleChange = Angle.FromDegrees(45);
[DataField]
public ProtoId<ContentTileDefinition> Tile = "FloorSteel";
/// <summary> /// <summary>
/// How wide to make the corridor. /// How wide to make the corridor.
/// </summary> /// </summary>

View File

@@ -32,14 +32,14 @@ public abstract partial class SharedSalvageSystem
var layers = new Dictionary<string, int>(); var layers = new Dictionary<string, int>();
// If we ever add more random layers will need to Next on these. // If we ever add more random layers will need to Next on these.
foreach (var layer in configProto.PostGeneration) foreach (var layer in configProto.Layers)
{ {
switch (layer) switch (layer)
{ {
case BiomePostGen: case BiomeDunGen:
rand.Next(); rand.Next();
break; break;
case BiomeMarkerLayerPostGen marker: case BiomeMarkerLayerDunGen marker:
for (var i = 0; i < marker.Count; i++) for (var i = 0; i < marker.Count; i++)
{ {
var proto = _proto.Index(marker.MarkerTemplate).Pick(rand); var proto = _proto.Index(marker.MarkerTemplate).Pick(rand);

View File

@@ -18,7 +18,7 @@ public abstract partial class SharedShuttleSystem : EntitySystem
[Dependency] protected readonly SharedTransformSystem XformSystem = default!; [Dependency] protected readonly SharedTransformSystem XformSystem = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
public const float FTLRange = 512f; public const float FTLRange = 256f;
public const float FTLBufferRange = 8f; public const float FTLBufferRange = 8f;
private EntityQuery<MapGridComponent> _gridQuery; private EntityQuery<MapGridComponent> _gridQuery;

View File

@@ -5,6 +5,19 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototy
namespace Content.Shared.Storage; namespace Content.Shared.Storage;
/// <summary>
/// Prototype wrapper around <see cref="EntitySpawnEntry"/>
/// </summary>
[Prototype]
public sealed class EntitySpawnEntryPrototype : IPrototype
{
[IdDataField]
public string ID { get; } = string.Empty;
[DataField]
public List<EntitySpawnEntry> Entries = new();
}
/// <summary> /// <summary>
/// Dictates a list of items that can be spawned. /// Dictates a list of items that can be spawned.
/// </summary> /// </summary>

View File

@@ -46,17 +46,26 @@
path: /Maps/Shuttles/cargo.yml path: /Maps/Shuttles/cargo.yml
- type: GridSpawn - type: GridSpawn
groups: groups:
trade: vgroid: !type:DungeonSpawnGroup
minimumDistance: 1000
nameDataset: names_borer
addComponents:
- type: Gravity
enabled: true
inherent: true
protos:
- VGRoid
trade: !type:GridSpawnGroup
addComponents: addComponents:
- type: ProtectedGrid - type: ProtectedGrid
- type: TradeStation - type: TradeStation
paths: paths:
- /Maps/Shuttles/trading_outpost.yml - /Maps/Shuttles/trading_outpost.yml
mining: mining: !type:GridSpawnGroup
paths: paths:
- /Maps/Shuttles/mining.yml - /Maps/Shuttles/mining.yml
# Spawn last # Spawn last
ruins: ruins: !type:GridSpawnGroup
hide: true hide: true
nameGrid: true nameGrid: true
minCount: 2 minCount: 2

View File

@@ -1,5 +1,7 @@
#TODO: Someone should probably move the ore vein prototypes into their own file, or otherwise split this up in some way. This should not be 1.5k lines long. #TODO: Someone should probably move the ore vein prototypes into their own file, or otherwise split this up in some way. This should not be 1.5k lines long.
# Anyway
# See WallRock variants for the remappings.
#Asteroid rock #Asteroid rock
- type: entity - type: entity
@@ -639,21 +641,28 @@
description: An ore vein rich with coal. description: An ore vein rich with coal.
suffix: Coal suffix: Coal
components: components:
- type: OreVein - type: EntityRemap
oreChance: 1.0 mask:
currentOre: OreCoal AsteroidRock: AsteroidRockCoal
- type: Sprite WallRockBasalt: WallRockBasaltCoal
layers: WallRockChromite: WallRockChromiteCoal
- state: rock WallRockSand: WallRockSandCoal
- map: [ "enum.EdgeLayer.South" ] WallRockSnow: WallRockSnowCoal
state: rock_south - type: OreVein
- map: [ "enum.EdgeLayer.East" ] oreChance: 1.0
state: rock_east currentOre: OreCoal
- map: [ "enum.EdgeLayer.North" ] - type: Sprite
state: rock_north layers:
- map: [ "enum.EdgeLayer.West" ] - state: rock
state: rock_west - map: [ "enum.EdgeLayer.South" ]
- state: rock_coal state: rock_south
- map: [ "enum.EdgeLayer.East" ]
state: rock_east
- map: [ "enum.EdgeLayer.North" ]
state: rock_north
- map: [ "enum.EdgeLayer.West" ]
state: rock_west
- state: rock_coal
- type: entity - type: entity
id: WallRockGold id: WallRockGold
@@ -661,21 +670,28 @@
description: An ore vein rich with gold. description: An ore vein rich with gold.
suffix: Gold suffix: Gold
components: components:
- type: OreVein - type: EntityRemap
oreChance: 1.0 mask:
currentOre: OreGold AsteroidRock: AsteroidRockGold
- type: Sprite WallRockBasalt: WallRockBasaltGold
layers: WallRockChromite: WallRockChromiteGold
- state: rock WallRockSand: WallRockSandGold
- map: [ "enum.EdgeLayer.South" ] WallRockSnow: WallRockSnowGold
state: rock_south - type: OreVein
- map: [ "enum.EdgeLayer.East" ] oreChance: 1.0
state: rock_east currentOre: OreGold
- map: [ "enum.EdgeLayer.North" ] - type: Sprite
state: rock_north layers:
- map: [ "enum.EdgeLayer.West" ] - state: rock
state: rock_west - map: [ "enum.EdgeLayer.South" ]
- state: rock_gold state: rock_south
- map: [ "enum.EdgeLayer.East" ]
state: rock_east
- map: [ "enum.EdgeLayer.North" ]
state: rock_north
- map: [ "enum.EdgeLayer.West" ]
state: rock_west
- state: rock_gold
- type: entity - type: entity
id: WallRockPlasma id: WallRockPlasma
@@ -683,21 +699,28 @@
description: An ore vein rich with plasma. description: An ore vein rich with plasma.
suffix: Plasma suffix: Plasma
components: components:
- type: OreVein - type: EntityRemap
oreChance: 1.0 mask:
currentOre: OrePlasma AsteroidRock: AsteroidRockPlasma
- type: Sprite WallRockBasalt: WallRockBasaltPlasma
layers: WallRockChromite: WallRockChromitePlasma
- state: rock WallRockSand: WallRockSandPlasma
- map: [ "enum.EdgeLayer.South" ] WallRockSnow: WallRockSnowPlasma
state: rock_south - type: OreVein
- map: [ "enum.EdgeLayer.East" ] oreChance: 1.0
state: rock_east currentOre: OrePlasma
- map: [ "enum.EdgeLayer.North" ] - type: Sprite
state: rock_north layers:
- map: [ "enum.EdgeLayer.West" ] - state: rock
state: rock_west - map: [ "enum.EdgeLayer.South" ]
- state: rock_phoron state: rock_south
- map: [ "enum.EdgeLayer.East" ]
state: rock_east
- map: [ "enum.EdgeLayer.North" ]
state: rock_north
- map: [ "enum.EdgeLayer.West" ]
state: rock_west
- state: rock_phoron
- type: entity - type: entity
id: WallRockQuartz id: WallRockQuartz
@@ -705,21 +728,28 @@
description: An ore vein rich with quartz. description: An ore vein rich with quartz.
suffix: Quartz suffix: Quartz
components: components:
- type: OreVein - type: EntityRemap
oreChance: 1.0 mask:
currentOre: OreSpaceQuartz AsteroidRock: AsteroidRockQuartz
- type: Sprite WallRockBasalt: WallRockBasaltQuartz
layers: WallRockChromite: WallRockChromiteQuartz
- state: rock WallRockSand: WallRockSandQuartz
- map: [ "enum.EdgeLayer.South" ] WallRockSnow: WallRockSnowQuartz
state: rock_south - type: OreVein
- map: [ "enum.EdgeLayer.East" ] oreChance: 1.0
state: rock_east currentOre: OreSpaceQuartz
- map: [ "enum.EdgeLayer.North" ] - type: Sprite
state: rock_north layers:
- map: [ "enum.EdgeLayer.West" ] - state: rock
state: rock_west - map: [ "enum.EdgeLayer.South" ]
- state: rock_quartz state: rock_south
- map: [ "enum.EdgeLayer.East" ]
state: rock_east
- map: [ "enum.EdgeLayer.North" ]
state: rock_north
- map: [ "enum.EdgeLayer.West" ]
state: rock_west
- state: rock_quartz
- type: entity - type: entity
id: WallRockSilver id: WallRockSilver
@@ -727,21 +757,28 @@
description: An ore vein rich with silver. description: An ore vein rich with silver.
suffix: Silver suffix: Silver
components: components:
- type: OreVein - type: EntityRemap
oreChance: 1.0 mask:
currentOre: OreSilver AsteroidRock: AsteroidRockSilver
- type: Sprite WallRockBasalt: WallRockBasaltSilver
layers: WallRockChromite: WallRockChromiteSilver
- state: rock WallRockSand: WallRockSandSilver
- map: [ "enum.EdgeLayer.South" ] WallRockSnow: WallRockSnowSilver
state: rock_south - type: OreVein
- map: [ "enum.EdgeLayer.East" ] oreChance: 1.0
state: rock_east currentOre: OreSilver
- map: [ "enum.EdgeLayer.North" ] - type: Sprite
state: rock_north layers:
- map: [ "enum.EdgeLayer.West" ] - state: rock
state: rock_west - map: [ "enum.EdgeLayer.South" ]
- state: rock_silver state: rock_south
- map: [ "enum.EdgeLayer.East" ]
state: rock_east
- map: [ "enum.EdgeLayer.North" ]
state: rock_north
- map: [ "enum.EdgeLayer.West" ]
state: rock_west
- state: rock_silver
# Yes I know it drops steel but we may get smelting at some point # Yes I know it drops steel but we may get smelting at some point
- type: entity - type: entity
@@ -750,6 +787,13 @@
description: An ore vein rich with iron. description: An ore vein rich with iron.
suffix: Iron suffix: Iron
components: components:
- type: EntityRemap
mask:
AsteroidRock: AsteroidRockTin
WallRockBasalt: WallRockBasaltTin
WallRockChromite: WallRockChromiteTin
WallRockSand: WallRockSandTin
WallRockSnow: WallRockSnowTin
- type: OreVein - type: OreVein
oreChance: 1.0 oreChance: 1.0
currentOre: OreSteel currentOre: OreSteel
@@ -772,21 +816,28 @@
description: An ore vein rich with uranium. description: An ore vein rich with uranium.
suffix: Uranium suffix: Uranium
components: components:
- type: OreVein - type: EntityRemap
oreChance: 1.0 mask:
currentOre: OreUranium AsteroidRock: AsteroidRockUranium
- type: Sprite WallRockBasalt: WallRockBasaltUranium
layers: WallRockChromite: WallRockChromiteUranium
- state: rock WallRockSand: WallRockSandUranium
- map: [ "enum.EdgeLayer.South" ] WallRockSnow: WallRockSnowUranium
state: rock_south - type: OreVein
- map: [ "enum.EdgeLayer.East" ] oreChance: 1.0
state: rock_east currentOre: OreUranium
- map: [ "enum.EdgeLayer.North" ] - type: Sprite
state: rock_north layers:
- map: [ "enum.EdgeLayer.West" ] - state: rock
state: rock_west - map: [ "enum.EdgeLayer.South" ]
- state: rock_uranium state: rock_south
- map: [ "enum.EdgeLayer.East" ]
state: rock_east
- map: [ "enum.EdgeLayer.North" ]
state: rock_north
- map: [ "enum.EdgeLayer.West" ]
state: rock_west
- state: rock_uranium
- type: entity - type: entity
@@ -795,21 +846,28 @@
description: An ore vein rich with bananium. description: An ore vein rich with bananium.
suffix: Bananium suffix: Bananium
components: components:
- type: OreVein - type: EntityRemap
oreChance: 1.0 mask:
currentOre: OreBananium AsteroidRock: AsteroidRockBananium
- type: Sprite WallRockBasalt: WallRockBasaltBananium
layers: WallRockChromite: WallRockChromiteBananium
- state: rock WallRockSand: WallRockSandBananium
- map: [ "enum.EdgeLayer.South" ] WallRockSnow: WallRockSnowBananium
state: rock_south - type: OreVein
- map: [ "enum.EdgeLayer.East" ] oreChance: 1.0
state: rock_east currentOre: OreBananium
- map: [ "enum.EdgeLayer.North" ] - type: Sprite
state: rock_north layers:
- map: [ "enum.EdgeLayer.West" ] - state: rock
state: rock_west - map: [ "enum.EdgeLayer.South" ]
- state: rock_bananium state: rock_south
- map: [ "enum.EdgeLayer.East" ]
state: rock_east
- map: [ "enum.EdgeLayer.North" ]
state: rock_north
- map: [ "enum.EdgeLayer.West" ]
state: rock_west
- state: rock_bananium
- type: entity - type: entity
id: WallRockArtifactFragment id: WallRockArtifactFragment
@@ -817,21 +875,28 @@
description: A rock wall. What's that sticking out of it? description: A rock wall. What's that sticking out of it?
suffix: Artifact Fragment suffix: Artifact Fragment
components: components:
- type: OreVein - type: EntityRemap
oreChance: 1.0 mask:
currentOre: OreArtifactFragment AsteroidRock: AsteroidRockArtifactFragment
- type: Sprite WallRockBasalt: WallRockBasaltArtifactFragment
layers: WallRockChromite: WallRockChromiteArtifactFragment
- state: rock WallRockSand: WallRockSandArtifactFragment
- map: [ "enum.EdgeLayer.South" ] WallRockSnow: WallRockSnowArtifactFragment
state: rock_south - type: OreVein
- map: [ "enum.EdgeLayer.East" ] oreChance: 1.0
state: rock_east currentOre: OreArtifactFragment
- map: [ "enum.EdgeLayer.North" ] - type: Sprite
state: rock_north layers:
- map: [ "enum.EdgeLayer.West" ] - state: rock
state: rock_west - map: [ "enum.EdgeLayer.South" ]
- state: rock_artifact_fragment state: rock_south
- map: [ "enum.EdgeLayer.East" ]
state: rock_east
- map: [ "enum.EdgeLayer.North" ]
state: rock_north
- map: [ "enum.EdgeLayer.West" ]
state: rock_west
- state: rock_artifact_fragment
- type: entity - type: entity
id: WallRockSalt id: WallRockSalt
@@ -839,21 +904,28 @@
description: An ore vein rich with salt. description: An ore vein rich with salt.
suffix: Salt suffix: Salt
components: components:
- type: OreVein - type: EntityRemap
oreChance: 1.0 mask:
currentOre: OreSalt AsteroidRock: AsteroidRockSalt
- type: Sprite WallRockBasalt: WallRockBasaltSalt
layers: WallRockChromite: WallRockChromiteSalt
- state: rock WallRockSand: WallRockSandSalt
- map: [ "enum.EdgeLayer.South" ] WallRockSnow: WallRockSnowSalt
state: rock_south - type: OreVein
- map: [ "enum.EdgeLayer.East" ] oreChance: 1.0
state: rock_east currentOre: OreSalt
- map: [ "enum.EdgeLayer.North" ] - type: Sprite
state: rock_north layers:
- map: [ "enum.EdgeLayer.West" ] - state: rock
state: rock_west - map: [ "enum.EdgeLayer.South" ]
- state: rock_salt state: rock_south
- map: [ "enum.EdgeLayer.East" ]
state: rock_east
- map: [ "enum.EdgeLayer.North" ]
state: rock_north
- map: [ "enum.EdgeLayer.West" ]
state: rock_west
- state: rock_salt
# Basalt variants # Basalt variants
- type: entity - type: entity

Some files were not shown because too many files have changed in this diff Show More