From f533a1a543af7784f7f9788073a6aae404761022 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Thu, 4 Jan 2024 15:17:04 +1100 Subject: [PATCH] Add RoomFill markers (#22293) * Add RoomFill markers * weh * Also deez * Working * Randomised fills working * Fixes * Fix lack of prototypes * Fix tests * Fix tests? --- .../Procedural/DungeonJob.PrefabDunGen.cs | 137 +--------- .../Procedural/DungeonSystem.Rooms.cs | 240 ++++++++++++++++++ Content.Server/Procedural/DungeonSystem.cs | 21 +- .../Procedural/RoomFillComponent.cs | 37 +++ Content.Server/Procedural/RoomFillSystem.cs | 50 ++++ .../Prototypes/Entities/Markers/rooms.yml | 13 + 6 files changed, 363 insertions(+), 135 deletions(-) create mode 100644 Content.Server/Procedural/DungeonSystem.Rooms.cs create mode 100644 Content.Server/Procedural/RoomFillComponent.cs create mode 100644 Content.Server/Procedural/RoomFillSystem.cs create mode 100644 Resources/Prototypes/Entities/Markers/rooms.yml diff --git a/Content.Server/Procedural/DungeonJob.PrefabDunGen.cs b/Content.Server/Procedural/DungeonJob.PrefabDunGen.cs index 7720110fd2..9314a784e8 100644 --- a/Content.Server/Procedural/DungeonJob.PrefabDunGen.cs +++ b/Content.Server/Procedural/DungeonJob.PrefabDunGen.cs @@ -191,59 +191,29 @@ public sealed partial class DungeonJob 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(templateMapUid); + _dungeon.SpawnRoom(gridUid, grid, matty, room, random, rotation: true); + var roomCenter = (room.Offset + room.Size / 2f) * grid.TileSize; var roomTiles = new HashSet(room.Size.X * room.Size.Y); var exterior = new HashSet(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++) @@ -258,111 +228,16 @@ public sealed partial class DungeonJob } } - 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(); - var metaQuery = _entManager.GetEntityQuery(); - - // 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(templateMapUid, out var loadedDecals)) - { - _entManager.EnsureComponent(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(); diff --git a/Content.Server/Procedural/DungeonSystem.Rooms.cs b/Content.Server/Procedural/DungeonSystem.Rooms.cs new file mode 100644 index 0000000000..c7445e9e71 --- /dev/null +++ b/Content.Server/Procedural/DungeonSystem.Rooms.cs @@ -0,0 +1,240 @@ +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 _entitySet = new(); + private readonly List _availableRooms = new(); + + /// + /// Gets a random dungeon room matching the specified area and whitelist. + /// + 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()) + { + 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(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(templateMapUid, out var loadedDecals)) + { + EnsureComp(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); + } + } + } +} diff --git a/Content.Server/Procedural/DungeonSystem.cs b/Content.Server/Procedural/DungeonSystem.cs index d8377940f8..2352aa5120 100644 --- a/Content.Server/Procedural/DungeonSystem.cs +++ b/Content.Server/Procedural/DungeonSystem.cs @@ -7,6 +7,7 @@ using Content.Server.GameTicking.Events; 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; @@ -15,6 +16,7 @@ using Robust.Shared.Console; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Prototypes; +using Robust.Shared.Random; namespace Content.Server.Procedural; @@ -24,14 +26,20 @@ public sealed partial class DungeonSystem : SharedDungeonSystem [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 _entSet = new(); + private readonly List<(Vector2i, Tile)> _tiles = new(); + + private EntityQuery _metaQuery; + private EntityQuery _xformQuery; private const double DungeonJobTime = 0.005; @@ -41,10 +49,15 @@ public sealed partial class DungeonSystem : SharedDungeonSystem private readonly JobQueue _dungeonJobQueue = new(DungeonJobTime); private readonly Dictionary _dungeonJobs = new(); + [ValidatePrototypeId] + public const string FallbackTileId = "FloorSteel"; + public override void Initialize() { base.Initialize(); - _sawmill = Logger.GetSawmill("dungen"); + + _metaQuery = GetEntityQuery(); + _xformQuery = GetEntityQuery(); _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); @@ -176,7 +189,7 @@ public sealed partial class DungeonSystem : SharedDungeonSystem { var cancelToken = new CancellationTokenSource(); var job = new DungeonJob( - _sawmill, + Log, DungeonJobTime, EntityManager, _mapManager, @@ -207,7 +220,7 @@ public sealed partial class DungeonSystem : SharedDungeonSystem { var cancelToken = new CancellationTokenSource(); var job = new DungeonJob( - _sawmill, + Log, DungeonJobTime, EntityManager, _mapManager, diff --git a/Content.Server/Procedural/RoomFillComponent.cs b/Content.Server/Procedural/RoomFillComponent.cs new file mode 100644 index 0000000000..50d0fa7c0a --- /dev/null +++ b/Content.Server/Procedural/RoomFillComponent.cs @@ -0,0 +1,37 @@ +using Content.Shared.Procedural; +using Content.Shared.Whitelist; +using Robust.Shared.Prototypes; + +namespace Content.Server.Procedural; + +/// +/// Marker that indicates the specified room prototype should occupy this space. +/// +[RegisterComponent] +public sealed partial class RoomFillComponent : Component +{ + /// + /// Are we allowed to rotate room templates? + /// If the room is not a square this will only do 180 degree rotations. + /// + [DataField] + public bool Rotation = true; + + /// + /// Size of the room to fill. + /// + [DataField(required: true)] + public Vector2i Size; + + /// + /// Rooms allowed for the marker. + /// + [DataField] + public EntityWhitelist? RoomWhitelist; + + /// + /// Should any existing entities / decals be bulldozed first. + /// + [DataField] + public bool ClearExisting; +} diff --git a/Content.Server/Procedural/RoomFillSystem.cs b/Content.Server/Procedural/RoomFillSystem.cs new file mode 100644 index 0000000000..20ffa98586 --- /dev/null +++ b/Content.Server/Procedural/RoomFillSystem.cs @@ -0,0 +1,50 @@ +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(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(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); + } +} diff --git a/Resources/Prototypes/Entities/Markers/rooms.yml b/Resources/Prototypes/Entities/Markers/rooms.yml new file mode 100644 index 0000000000..e4f341daf1 --- /dev/null +++ b/Resources/Prototypes/Entities/Markers/rooms.yml @@ -0,0 +1,13 @@ +- 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 -- 2.51.2