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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -176,9 +176,11 @@ public sealed class SpawnSalvageMissionJob : Job<bool>
dungeonOffset = dungeonRotation.RotateVec(dungeonOffset);
var dungeonMod = _prototypeManager.Index<SalvageDungeonModPrototype>(mission.Dungeon);
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));
var dungeon = dungeons.First();
// Aborty
if (dungeon.Rooms.Count == 0)
{

View File

@@ -1,4 +1,6 @@
using Content.Server.Shuttles.Systems;
using Content.Shared.Dataset;
using Content.Shared.Procedural;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
@@ -14,39 +16,92 @@ public sealed partial class GridSpawnComponent : Component
/// Dictionary of groups where each group will have entries selected.
/// String is just an identifier to make yaml easier.
/// </summary>
[DataField(required: true)] public Dictionary<string, GridSpawnGroup> Groups = new();
[DataField(required: true)] public Dictionary<string, IGridSpawnGroup> Groups = new();
}
[DataRecord]
public record struct GridSpawnGroup
public interface IGridSpawnGroup
{
public List<ResPath> Paths = new();
public int MinCount = 1;
public int MaxCount = 1;
/// <summary>
/// Minimum distance to spawn away from the station.
/// </summary>
public float MinimumDistance { get; }
/// <inheritdoc />
public ProtoId<DatasetPrototype>? NameDataset { get; }
/// <inheritdoc />
int MinCount { get; set; }
/// <inheritdoc />
int MaxCount { get; set; }
/// <summary>
/// Components to be added to any spawned grids.
/// </summary>
public ComponentRegistry AddComponents = new();
public ComponentRegistry AddComponents { get; set; }
/// <summary>
/// Hide the IFF label of the grid.
/// </summary>
public bool Hide = false;
public bool Hide { get; set; }
/// <summary>
/// Should we set the metadata name of a grid. Useful for admin purposes.
/// </summary>
public bool NameGrid = false;
public bool NameGrid { get; set; }
/// <summary>
/// Should we add this to the station's grids (if possible / relevant).
/// </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.Station.Components;
using Content.Server.Station.Events;
using Content.Shared.Cargo.Components;
using Content.Shared.CCVar;
using Content.Shared.Procedural;
using Content.Shared.Salvage;
using Content.Shared.Shuttles.Components;
using Robust.Shared.Collections;
using Robust.Shared.Map;
using Robust.Shared.Random;
using Robust.Shared.Utility;
@@ -80,6 +85,76 @@ public sealed partial class ShuttleSystem
_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)
{
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.
var mapId = _mapManager.CreateMap();
var valid = true;
var paths = new List<ResPath>();
foreach (var group in component.Groups.Values)
{
if (group.Paths.Count == 0)
{
Log.Error($"Found no paths for GridSpawn");
continue;
}
var count = _random.Next(group.MinCount, group.MaxCount);
paths.Clear();
var count = _random.Next(group.MinCount, group.MaxCount + 1);
for (var i = 0; i < count; i++)
{
// Round-robin so we try to avoid dupes where possible.
if (paths.Count == 0)
EntityUid spawned;
switch (group)
{
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.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))
case DungeonSpawnGroup dungeon:
if (!TryDungeonSpawn(targetGrid.Value, uid, mapId, dungeon, out spawned))
continue;
var comp = _factory.GetComponent(compType);
AddComp(ent[0], comp, true);
}
}
else
{
valid = false;
break;
case GridSpawnGroup grid:
if (!TryGridSpawn(targetGrid.Value, uid, mapId, grid, out spawned))
continue;
break;
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.Doors.Systems;
using Content.Server.Parallax;
using Content.Server.Procedural;
using Content.Server.Shuttles.Components;
using Content.Server.Station.Systems;
using Content.Server.Stunnable;
@@ -20,6 +21,7 @@ using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
@@ -28,15 +30,18 @@ namespace Content.Server.Shuttles.Systems;
[UsedImplicitly]
public sealed partial class ShuttleSystem : SharedShuttleSystem
{
[Dependency] private readonly IAdminLogManager _logger = default!;
[Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly BiomeSystem _biomes = default!;
[Dependency] private readonly BodySystem _bobby = default!;
[Dependency] private readonly DockingSystem _dockSystem = default!;
[Dependency] private readonly DungeonSystem _dungeon = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly FixtureSystem _fixtures = 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 ThrusterSystem _thruster = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly IAdminLogManager _logger = default!;
public const float TileMassMultiplier = 0.5f;