grid.SetTiles(tiles);
tiles.Clear();
- Logger.Error($"Unable to find room variant for {roomDimensions}, leaving empty.");
+ _sawmill.Error($"Unable to find room variant for {roomDimensions}, leaving empty.");
continue;
}
roomRotation = new Angle(Math.PI / 2);
- Logger.Debug($"Using rotated variant for room");
- }
-
- if (roomDimensions.X == roomDimensions.Y)
- {
- // Give it a random rotation
- roomRotation = random.Next(4) * Math.PI / 2;
- }
- else if (random.Next(2) == 1)
- {
- roomRotation += Math.PI;
+ _sawmill.Debug($"Using rotated variant for room");
}
var roomTransform = Matrix3.CreateTransform(roomSize.Center - packCenter, roomRotation);
- var finalRoomRotation = roomRotation + packRotation + dungeonRotation;
Matrix3.Multiply(roomTransform, packTransform, out matty);
Matrix3.Multiply(matty, dungeonTransform, out var dungeonMatty);
+ // The expensive bit yippy.
var room = roomProto[random.Next(roomProto.Count)];
- var roomMap = _dungeon.GetOrCreateTemplate(room);
- var templateMapUid = _mapManager.GetMapEntityId(roomMap);
- var templateGrid = _entManager.GetComponent<MapGridComponent>(templateMapUid);
+ _dungeon.SpawnRoom(gridUid, grid, matty, room, random, rotation: true);
+
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;
Box2i? mapBounds = null;
- // Load tiles
- for (var x = 0; x < room.Size.X; x++)
- {
- for (var y = 0; y < room.Size.Y; y++)
- {
- var indices = new Vector2i(x + room.Offset.X, y + room.Offset.Y);
- var tileRef = templateGrid.GetTileRef(indices);
-
- var tilePos = dungeonMatty.Transform(indices + tileOffset);
- var rounded = tilePos.Floored();
- tiles.Add((rounded, tileRef.Tile));
- roomTiles.Add(rounded);
-
- // If this were a Box2 we'd add tilesize although here I think that's undesirable as
- // for example, a box2i of 0,0,1,1 is assumed to also include the tile at 1,1
- mapBounds = mapBounds?.Union(new Box2i(rounded, rounded)) ?? new Box2i(rounded, rounded);
- }
- }
-
for (var x = -1; x <= room.Size.X; x++)
{
for (var y = -1; y <= room.Size.Y; y++)
}
}
- var bounds = new Box2(room.Offset, room.Offset + room.Size);
var center = Vector2.Zero;
foreach (var tile in roomTiles)
{
- center += (Vector2) tile + grid.TileSizeHalfVector;
+ center += tile + grid.TileSizeHalfVector;
}
center /= roomTiles.Count;
dungeon.Rooms.Add(new DungeonRoom(roomTiles, center, mapBounds!.Value, exterior));
- grid.SetTiles(tiles);
- tiles.Clear();
- var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
- var metaQuery = _entManager.GetEntityQuery<MetaDataComponent>();
-
- // Load entities
- // TODO: I don't think engine supports full entity copying so we do this piece of shit.
-
- foreach (var templateEnt in _lookup.GetEntitiesIntersecting(templateMapUid, bounds, LookupFlags.Uncontained))
- {
- var templateXform = xformQuery.GetComponent(templateEnt);
- var childPos = dungeonMatty.Transform(templateXform.LocalPosition - roomCenter);
- var childRot = templateXform.LocalRotation + finalRoomRotation;
- var protoId = metaQuery.GetComponent(templateEnt).EntityPrototype?.ID;
-
- // TODO: Copy the templated entity as is with serv
- var ent = _entManager.SpawnEntity(protoId,
- new EntityCoordinates(gridUid, childPos));
-
- var childXform = xformQuery.GetComponent(ent);
- var anchored = templateXform.Anchored;
- _transform.SetLocalRotation(ent, childRot, childXform);
-
- // If the templated entity was anchored then anchor us too.
- if (anchored && !childXform.Anchored)
- _transform.AnchorEntity(ent, childXform, grid);
- else if (!anchored && childXform.Anchored)
- _transform.Unanchor(ent, childXform);
- }
-
- // Load decals
- if (_entManager.TryGetComponent<DecalGridComponent>(templateMapUid, out var loadedDecals))
- {
- _entManager.EnsureComponent<DecalGridComponent>(gridUid);
-
- foreach (var (_, decal) in _decals.GetDecalsIntersecting(templateMapUid, bounds, loadedDecals))
- {
- // Offset by 0.5 because decals are offset from bot-left corner
- // So we convert it to center of tile then convert it back again after transform.
- // Do these shenanigans because 32x32 decals assume as they are centered on bottom-left of tiles.
- var position = dungeonMatty.Transform(decal.Coordinates + Vector2Helpers.Half - roomCenter);
- position -= Vector2Helpers.Half;
-
- // Umm uhh I love decals so uhhhh idk what to do about this
- var angle = (decal.Angle + finalRoomRotation).Reduced();
-
- // Adjust because 32x32 so we can't rotate cleanly
- // Yeah idk about the uhh vectors here but it looked visually okay but they may still be off by 1.
- // Also EyeManager.PixelsPerMeter should really be in shared.
- if (angle.Equals(Math.PI))
- {
- position += new Vector2(-1f / 32f, 1f / 32f);
- }
- else if (angle.Equals(-Math.PI / 2f))
- {
- position += new Vector2(-1f / 32f, 0f);
- }
- else if (angle.Equals(Math.PI / 2f))
- {
- position += new Vector2(0f, 1f / 32f);
- }
- else if (angle.Equals(Math.PI * 1.5f))
- {
- // I hate this but decals are bottom-left rather than center position and doing the
- // matrix ops is a PITA hence this workaround for now; I also don't want to add a stupid
- // field for 1 specific op on decals
- if (decal.Id != "DiagonalCheckerAOverlay" &&
- decal.Id != "DiagonalCheckerBOverlay")
- {
- position += new Vector2(-1f / 32f, 0f);
- }
- }
-
- var tilePos = position.Floored();
-
- // Fallback because uhhhhhhhh yeah, a corner tile might look valid on the original
- // but place 1 nanometre off grid and fail the add.
- if (!grid.TryGetTileRef(tilePos, out var tileRef) || tileRef.Tile.IsEmpty)
- {
- grid.SetTile(tilePos, fallbackTile);
- }
-
- var result = _decals.TryAddDecal(
- decal.Id,
- new EntityCoordinates(gridUid, position),
- out _,
- decal.Color,
- angle,
- decal.ZIndex,
- decal.Cleanable);
-
- DebugTools.Assert(result);
- }
- }
await SuspendIfOutOfTime();
ValidateResume();
--- /dev/null
+using System.Numerics;
+using Content.Shared.Decals;
+using Content.Shared.Procedural;
+using Content.Shared.Random.Helpers;
+using Content.Shared.Whitelist;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Procedural;
+
+public sealed partial class DungeonSystem
+{
+ // Temporary caches.
+ private readonly HashSet<EntityUid> _entitySet = new();
+ private readonly List<DungeonRoomPrototype> _availableRooms = new();
+
+ /// <summary>
+ /// Gets a random dungeon room matching the specified area and whitelist.
+ /// </summary>
+ public DungeonRoomPrototype? GetRoomPrototype(Vector2i size, Random random, EntityWhitelist? whitelist = null)
+ {
+ // Can never be true.
+ if (whitelist is { Tags: null })
+ {
+ return null;
+ }
+
+ _availableRooms.Clear();
+
+ foreach (var proto in _prototype.EnumeratePrototypes<DungeonRoomPrototype>())
+ {
+ if (proto.Size != size)
+ continue;
+
+ if (whitelist == null)
+ {
+ _availableRooms.Add(proto);
+ continue;
+ }
+
+ foreach (var tag in whitelist.Tags)
+ {
+ if (!proto.Tags.Contains(tag))
+ continue;
+
+ _availableRooms.Add(proto);
+ break;
+ }
+ }
+
+ if (_availableRooms.Count == 0)
+ return null;
+
+ var room = _availableRooms[random.Next(_availableRooms.Count)];
+
+ return room;
+ }
+
+ public void SpawnRoom(
+ EntityUid gridUid,
+ MapGridComponent grid,
+ Vector2i origin,
+ DungeonRoomPrototype room,
+ Random random,
+ bool clearExisting = false,
+ bool rotation = false)
+ {
+ var originTransform = Matrix3.CreateTranslation(origin);
+ SpawnRoom(gridUid, grid, originTransform, room, random, clearExisting, rotation);
+ }
+
+ public void SpawnRoom(
+ EntityUid gridUid,
+ MapGridComponent grid,
+ Matrix3 transform,
+ DungeonRoomPrototype room,
+ Random random,
+ bool clearExisting = false,
+ bool rotation = false)
+ {
+ // Ensure the underlying template exists.
+ var roomMap = GetOrCreateTemplate(room);
+ var templateMapUid = _mapManager.GetMapEntityId(roomMap);
+ var templateGrid = Comp<MapGridComponent>(templateMapUid);
+ var roomRotation = Angle.Zero;
+ var roomDimensions = room.Size;
+
+ if (rotation)
+ {
+ if (roomDimensions.X == roomDimensions.Y)
+ {
+ // Give it a random rotation
+ roomRotation = random.Next(4) * Math.PI / 2;
+ }
+ else if (random.Next(2) == 1)
+ {
+ roomRotation += Math.PI;
+ }
+ }
+
+ var roomTransform = Matrix3.CreateTransform((Vector2) room.Size / 2f, roomRotation);
+ Matrix3.Multiply(roomTransform, transform, out var finalTransform);
+ var finalRoomRotation = finalTransform.Rotation();
+
+ // go BRRNNTTT on existing stuff
+ if (clearExisting)
+ {
+ var gridBounds = new Box2(transform.Transform(Vector2.Zero), transform.Transform(room.Size));
+ _entitySet.Clear();
+ // Polygon skin moment
+ gridBounds = gridBounds.Enlarged(-0.05f);
+ _lookup.GetLocalEntitiesIntersecting(gridUid, gridBounds, _entitySet, LookupFlags.Uncontained);
+
+ foreach (var templateEnt in _entitySet)
+ {
+ Del(templateEnt);
+ }
+
+ if (TryComp(gridUid, out DecalGridComponent? decalGrid))
+ {
+ foreach (var decal in _decals.GetDecalsIntersecting(gridUid, gridBounds, decalGrid))
+ {
+ _decals.RemoveDecal(gridUid, decal.Index, decalGrid);
+ }
+ }
+ }
+
+ var roomCenter = (room.Offset + room.Size / 2f) * grid.TileSize;
+ var tileOffset = -roomCenter + grid.TileSizeHalfVector;
+ _tiles.Clear();
+
+ // Load tiles
+ for (var x = 0; x < roomDimensions.X; x++)
+ {
+ for (var y = 0; y < roomDimensions.Y; y++)
+ {
+ var indices = new Vector2i(x + room.Offset.X, y + room.Offset.Y);
+ var tileRef = _maps.GetTileRef(templateMapUid, templateGrid, indices);
+
+ var tilePos = finalTransform.Transform(indices + tileOffset);
+ var rounded = tilePos.Floored();
+ _tiles.Add((rounded, tileRef.Tile));
+ }
+ }
+
+ var bounds = new Box2(room.Offset, room.Offset + room.Size);
+
+ _maps.SetTiles(gridUid, grid, _tiles);
+
+ // Load entities
+ // TODO: I don't think engine supports full entity copying so we do this piece of shit.
+
+ foreach (var templateEnt in _lookup.GetEntitiesIntersecting(templateMapUid, bounds, LookupFlags.Uncontained))
+ {
+ var templateXform = _xformQuery.GetComponent(templateEnt);
+ var childPos = finalTransform.Transform(templateXform.LocalPosition - roomCenter);
+ var childRot = templateXform.LocalRotation + finalRoomRotation;
+ var protoId = _metaQuery.GetComponent(templateEnt).EntityPrototype?.ID;
+
+ // TODO: Copy the templated entity as is with serv
+ var ent = Spawn(protoId, new EntityCoordinates(gridUid, childPos));
+
+ var childXform = _xformQuery.GetComponent(ent);
+ var anchored = templateXform.Anchored;
+ _transform.SetLocalRotation(ent, childRot, childXform);
+
+ // If the templated entity was anchored then anchor us too.
+ if (anchored && !childXform.Anchored)
+ _transform.AnchorEntity(ent, childXform, grid);
+ else if (!anchored && childXform.Anchored)
+ _transform.Unanchor(ent, childXform);
+ }
+
+ // Load decals
+ if (TryComp<DecalGridComponent>(templateMapUid, out var loadedDecals))
+ {
+ EnsureComp<DecalGridComponent>(gridUid);
+
+ foreach (var (_, decal) in _decals.GetDecalsIntersecting(templateMapUid, bounds, loadedDecals))
+ {
+ // Offset by 0.5 because decals are offset from bot-left corner
+ // So we convert it to center of tile then convert it back again after transform.
+ // Do these shenanigans because 32x32 decals assume as they are centered on bottom-left of tiles.
+ var position = finalTransform.Transform(decal.Coordinates + Vector2Helpers.Half - roomCenter);
+ position -= Vector2Helpers.Half;
+
+ // Umm uhh I love decals so uhhhh idk what to do about this
+ var angle = (decal.Angle + finalRoomRotation).Reduced();
+
+ // Adjust because 32x32 so we can't rotate cleanly
+ // Yeah idk about the uhh vectors here but it looked visually okay but they may still be off by 1.
+ // Also EyeManager.PixelsPerMeter should really be in shared.
+ if (angle.Equals(Math.PI))
+ {
+ position += new Vector2(-1f / 32f, 1f / 32f);
+ }
+ else if (angle.Equals(-Math.PI / 2f))
+ {
+ position += new Vector2(-1f / 32f, 0f);
+ }
+ else if (angle.Equals(Math.PI / 2f))
+ {
+ position += new Vector2(0f, 1f / 32f);
+ }
+ else if (angle.Equals(Math.PI * 1.5f))
+ {
+ // I hate this but decals are bottom-left rather than center position and doing the
+ // matrix ops is a PITA hence this workaround for now; I also don't want to add a stupid
+ // field for 1 specific op on decals
+ if (decal.Id != "DiagonalCheckerAOverlay" &&
+ decal.Id != "DiagonalCheckerBOverlay")
+ {
+ position += new Vector2(-1f / 32f, 0f);
+ }
+ }
+
+ var tilePos = position.Floored();
+
+ // Fallback because uhhhhhhhh yeah, a corner tile might look valid on the original
+ // but place 1 nanometre off grid and fail the add.
+ if (!_maps.TryGetTileRef(gridUid, grid, tilePos, out var tileRef) || tileRef.Tile.IsEmpty)
+ {
+ _maps.SetTile(gridUid, grid, tilePos, _tileDefManager.GetVariantTile(FallbackTileId, _random));
+ }
+
+ var result = _decals.TryAddDecal(
+ decal.Id,
+ new EntityCoordinates(gridUid, position),
+ out _,
+ decal.Color,
+ angle,
+ decal.ZIndex,
+ decal.Cleanable);
+
+ DebugTools.Assert(result);
+ }
+ }
+ }
+}
using Content.Shared.CCVar;
using Content.Shared.Construction.EntitySystems;
using Content.Shared.GameTicking;
+using Content.Shared.Maps;
using Content.Shared.Physics;
using Content.Shared.Procedural;
using Robust.Server.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
namespace Content.Server.Procedural;
[Dependency] private readonly IConsoleHost _console = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefManager = default!;
[Dependency] private readonly AnchorableSystem _anchorable = default!;
[Dependency] private readonly DecalSystem _decals = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly MapLoaderSystem _loader = default!;
+ [Dependency] private readonly SharedMapSystem _maps = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
- private ISawmill _sawmill = default!;
+ private HashSet<EntityUid> _entSet = new();
+ private readonly List<(Vector2i, Tile)> _tiles = new();
+
+ private EntityQuery<MetaDataComponent> _metaQuery;
+ private EntityQuery<TransformComponent> _xformQuery;
private const double DungeonJobTime = 0.005;
private readonly JobQueue _dungeonJobQueue = new(DungeonJobTime);
private readonly Dictionary<DungeonJob, CancellationTokenSource> _dungeonJobs = new();
+ [ValidatePrototypeId<ContentTileDefinition>]
+ public const string FallbackTileId = "FloorSteel";
+
public override void Initialize()
{
base.Initialize();
- _sawmill = Logger.GetSawmill("dungen");
+
+ _metaQuery = GetEntityQuery<MetaDataComponent>();
+ _xformQuery = GetEntityQuery<TransformComponent>();
_console.RegisterCommand("dungen", Loc.GetString("cmd-dungen-desc"), Loc.GetString("cmd-dungen-help"), GenerateDungeon, CompletionCallback);
_console.RegisterCommand("dungen_preset_vis", Loc.GetString("cmd-dungen_preset_vis-desc"), Loc.GetString("cmd-dungen_preset_vis-help"), DungeonPresetVis, PresetCallback);
_console.RegisterCommand("dungen_pack_vis", Loc.GetString("cmd-dungen_pack_vis-desc"), Loc.GetString("cmd-dungen_pack_vis-help"), DungeonPackVis, PackCallback);
{
var cancelToken = new CancellationTokenSource();
var job = new DungeonJob(
- _sawmill,
+ Log,
DungeonJobTime,
EntityManager,
_mapManager,
{
var cancelToken = new CancellationTokenSource();
var job = new DungeonJob(
- _sawmill,
+ Log,
DungeonJobTime,
EntityManager,
_mapManager,
--- /dev/null
+using Content.Shared.Procedural;
+using Content.Shared.Whitelist;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Procedural;
+
+/// <summary>
+/// Marker that indicates the specified room prototype should occupy this space.
+/// </summary>
+[RegisterComponent]
+public sealed partial class RoomFillComponent : Component
+{
+ /// <summary>
+ /// Are we allowed to rotate room templates?
+ /// If the room is not a square this will only do 180 degree rotations.
+ /// </summary>
+ [DataField]
+ public bool Rotation = true;
+
+ /// <summary>
+ /// Size of the room to fill.
+ /// </summary>
+ [DataField(required: true)]
+ public Vector2i Size;
+
+ /// <summary>
+ /// Rooms allowed for the marker.
+ /// </summary>
+ [DataField]
+ public EntityWhitelist? RoomWhitelist;
+
+ /// <summary>
+ /// Should any existing entities / decals be bulldozed first.
+ /// </summary>
+ [DataField]
+ public bool ClearExisting;
+}
--- /dev/null
+using Robust.Shared.Map.Components;
+
+namespace Content.Server.Procedural;
+
+public sealed class RoomFillSystem : EntitySystem
+{
+ [Dependency] private readonly DungeonSystem _dungeon = default!;
+ [Dependency] private readonly SharedMapSystem _maps = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent<RoomFillComponent, MapInitEvent>(OnRoomFillMapInit);
+ }
+
+ private void OnRoomFillMapInit(EntityUid uid, RoomFillComponent component, MapInitEvent args)
+ {
+ // Just test things.
+ if (component.Size == Vector2i.Zero)
+ return;
+
+ var xform = Transform(uid);
+
+ if (xform.GridUid != null)
+ {
+ var random = new Random();
+ var room = _dungeon.GetRoomPrototype(component.Size, random, component.RoomWhitelist);
+
+ if (room != null)
+ {
+ var mapGrid = Comp<MapGridComponent>(xform.GridUid.Value);
+ _dungeon.SpawnRoom(
+ xform.GridUid.Value,
+ mapGrid,
+ _maps.LocalToTile(xform.GridUid.Value, mapGrid, xform.Coordinates),
+ room,
+ random,
+ clearExisting: component.ClearExisting,
+ rotation: component.Rotation);
+ }
+ else
+ {
+ Log.Error($"Unable to find matching room prototype for {ToPrettyString(uid)}");
+ }
+ }
+
+ // Final cleanup
+ QueueDel(uid);
+ }
+}
--- /dev/null
+- type: entity
+ id: BaseRoomMarker
+ name: Room marker
+ parent: MarkerBase
+ suffix: Weh
+ components:
+ - type: RoomFill
+ size: 5,5
+ - type: Sprite
+ layers:
+ - state: red
+ - sprite: Mobs/Aliens/elemental.rsi
+ state: alive