From 7d58e42ade391a61183145d271cb4e76b683bc22 Mon Sep 17 00:00:00 2001
From: Velken <8467292+Velken@users.noreply.github.com>
Date: Thu, 15 Jan 2026 17:22:54 -0300
Subject: [PATCH] Fix RCD light spam, bypass of indestructible tiles and some
plating fixes (#42432)
* No more light spam, and some plating fixes
* fixed test
---
.../Tests/Construction/RCDTest.cs | 8 +--
.../Interaction/InteractionTest.Constants.cs | 1 +
Content.Shared/RCD/RCDPrototype.cs | 6 ++
Content.Shared/RCD/Systems/RCDSystem.cs | 60 ++++++++++++++++---
.../en-US/rcd/components/rcd-component.ftl | 1 +
Resources/Prototypes/RCD/rcd.yml | 6 +-
Resources/Prototypes/Tiles/floors.yml | 13 +---
Resources/Prototypes/Tiles/plating.yml | 28 +++++++++
8 files changed, 100 insertions(+), 23 deletions(-)
diff --git a/Content.IntegrationTests/Tests/Construction/RCDTest.cs b/Content.IntegrationTests/Tests/Construction/RCDTest.cs
index f20a0cb434..770f004517 100644
--- a/Content.IntegrationTests/Tests/Construction/RCDTest.cs
+++ b/Content.IntegrationTests/Tests/Construction/RCDTest.cs
@@ -38,9 +38,9 @@ public sealed class RCDTest : InteractionTest
pEast = Transform.WithEntityId(pEast, MapData.Grid);
pWest = Transform.WithEntityId(pWest, MapData.Grid);
- await SetTile(Plating, SEntMan.GetNetCoordinates(pNorth), MapData.Grid);
- await SetTile(Plating, SEntMan.GetNetCoordinates(pSouth), MapData.Grid);
- await SetTile(Plating, SEntMan.GetNetCoordinates(pEast), MapData.Grid);
+ await SetTile(PlatingRCD, SEntMan.GetNetCoordinates(pNorth), MapData.Grid);
+ await SetTile(PlatingRCD, SEntMan.GetNetCoordinates(pSouth), MapData.Grid);
+ await SetTile(PlatingRCD, SEntMan.GetNetCoordinates(pEast), MapData.Grid);
await SetTile(Lattice, SEntMan.GetNetCoordinates(pWest), MapData.Grid);
Assert.That(ProtoMan.TryIndex(RCDSettingWall, out var settingWall), $"RCDPrototype not found: {RCDSettingWall}.");
@@ -194,7 +194,7 @@ public sealed class RCDTest : InteractionTest
// Deconstruct the steel tile.
await Interact(null, pEast);
await RunSeconds(settingDeconstructTile.Delay + 1); // wait for the deconstruction to finish
- await AssertTile(Plating, FromServer(pEast));
+ await AssertTile(PlatingRCD, FromServer(pEast));
// Check that the cost of the deconstruction was subtracted from the current charges.
newCharges = sCharges.GetCurrentCharges(ToServer(rcd));
diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs
index 3cfb5a5dba..1aac18f3a4 100644
--- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs
+++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs
@@ -11,6 +11,7 @@ public abstract partial class InteractionTest
protected const string Floor = "FloorSteel";
protected const string FloorItem = "FloorTileItemSteel";
protected const string Plating = "Plating";
+ protected const string PlatingRCD = "PlatingRCD";
protected const string Lattice = "Lattice";
protected const string PlatingBrass = "PlatingBrass";
diff --git a/Content.Shared/RCD/RCDPrototype.cs b/Content.Shared/RCD/RCDPrototype.cs
index 2be5e1c776..c4ac7148f7 100644
--- a/Content.Shared/RCD/RCDPrototype.cs
+++ b/Content.Shared/RCD/RCDPrototype.cs
@@ -44,6 +44,12 @@ public sealed partial class RCDPrototype : IPrototype
[DataField, ViewVariables(VVAccess.ReadOnly)]
public string? Prototype { get; private set; }
+ ///
+ /// If true, allows placing the entity once per direction (North, West, South and East)
+ ///
+ [DataField, ViewVariables(VVAccess.ReadOnly)]
+ public bool AllowMultiDirection { get; private set; }
+
///
/// Number of charges consumed when the operation is completed
///
diff --git a/Content.Shared/RCD/Systems/RCDSystem.cs b/Content.Shared/RCD/Systems/RCDSystem.cs
index 8b3ae16a1f..2f1f058a1b 100644
--- a/Content.Shared/RCD/Systems/RCDSystem.cs
+++ b/Content.Shared/RCD/Systems/RCDSystem.cs
@@ -146,7 +146,7 @@ public sealed class RCDSystem : EntitySystem
var tile = _mapSystem.GetTileRef(gridUid.Value, mapGrid, location);
var position = _mapSystem.TileIndicesFor(gridUid.Value, mapGrid, location);
- if (!IsRCDOperationStillValid(uid, component, gridUid.Value, mapGrid, tile, position, args.Target, args.User))
+ if (!IsRCDOperationStillValid(uid, component, gridUid.Value, mapGrid, tile, position, component.ConstructionDirection, args.Target, args.User))
return;
if (!_net.IsServer)
@@ -254,7 +254,7 @@ public sealed class RCDSystem : EntitySystem
var tile = _mapSystem.GetTileRef(gridUid.Value, mapGrid, location);
var position = _mapSystem.TileIndicesFor(gridUid.Value, mapGrid, location);
- if (!IsRCDOperationStillValid(uid, component, gridUid.Value, mapGrid, tile, position, args.Event.Target, args.Event.User))
+ if (!IsRCDOperationStillValid(uid, component, gridUid.Value, mapGrid, tile, position, args.Event.Direction, args.Event.Target, args.Event.User))
args.Cancel();
}
@@ -284,7 +284,7 @@ public sealed class RCDSystem : EntitySystem
var position = _mapSystem.TileIndicesFor(gridUid.Value, mapGrid, location);
// Ensure the RCD operation is still valid
- if (!IsRCDOperationStillValid(uid, component, gridUid.Value, mapGrid, tile, position, args.Target, args.User))
+ if (!IsRCDOperationStillValid(uid, component, gridUid.Value, mapGrid, tile, position, args.Direction, args.Target, args.User))
return;
// Finalize the operation (this should handle prediction properly)
@@ -319,6 +319,11 @@ public sealed class RCDSystem : EntitySystem
#region Entity construction/deconstruction rule checks
public bool IsRCDOperationStillValid(EntityUid uid, RCDComponent component, EntityUid gridUid, MapGridComponent mapGrid, TileRef tile, Vector2i position, EntityUid? target, EntityUid user, bool popMsgs = true)
+ {
+ return IsRCDOperationStillValid(uid, component, gridUid, mapGrid, tile, position, component.ConstructionDirection, target, user, popMsgs);
+ }
+
+ public bool IsRCDOperationStillValid(EntityUid uid, RCDComponent component, EntityUid gridUid, MapGridComponent mapGrid, TileRef tile, Vector2i position, Direction direction, EntityUid? target, EntityUid user, bool popMsgs = true)
{
var prototype = _protoManager.Index(component.ProtoId);
@@ -355,7 +360,7 @@ public sealed class RCDSystem : EntitySystem
{
case RcdMode.ConstructTile:
case RcdMode.ConstructObject:
- return IsConstructionLocationValid(uid, component, gridUid, mapGrid, tile, position, user, popMsgs);
+ return IsConstructionLocationValid(uid, component, gridUid, mapGrid, tile, position, direction, user, popMsgs);
case RcdMode.Deconstruct:
return IsDeconstructionStillValid(uid, tile, target, user, popMsgs);
}
@@ -363,7 +368,7 @@ public sealed class RCDSystem : EntitySystem
return false;
}
- private bool IsConstructionLocationValid(EntityUid uid, RCDComponent component, EntityUid gridUid, MapGridComponent mapGrid, TileRef tile, Vector2i position, EntityUid user, bool popMsgs = true)
+ private bool IsConstructionLocationValid(EntityUid uid, RCDComponent component, EntityUid gridUid, MapGridComponent mapGrid, TileRef tile, Vector2i position, Direction direction, EntityUid user, bool popMsgs = true)
{
var prototype = _protoManager.Index(component.ProtoId);
@@ -406,8 +411,24 @@ public sealed class RCDSystem : EntitySystem
return false;
}
+ var tileDef = _turf.GetContentTileDefinition(tile);
+
+ // Check rule: Respect baseTurf and baseWhitelist
+ if (prototype.Prototype != null && _tileDefMan.TryGetDefinition(prototype.Prototype, out var replacementDef))
+ {
+ var replacementContentDef = (ContentTileDefinition) replacementDef;
+
+ if (replacementContentDef.BaseTurf != tileDef.ID && !replacementContentDef.BaseWhitelist.Contains(tileDef.ID))
+ {
+ if (popMsgs)
+ _popup.PopupClient(Loc.GetString("rcd-component-cannot-build-on-empty-tile-message"), uid, user);
+
+ return false;
+ }
+ }
+
// Check rule: Tiles can't be identical
- if (_turf.GetContentTileDefinition(tile).ID == prototype.Prototype)
+ if (tileDef.ID == prototype.Prototype)
{
if (popMsgs)
_popup.PopupClient(Loc.GetString("rcd-component-cannot-build-identical-tile"), uid, user);
@@ -430,6 +451,28 @@ public sealed class RCDSystem : EntitySystem
foreach (var ent in _intersectingEntities)
{
+ // If the entity is the exact same prototype as what we are trying to build, then block it.
+ // This is to prevent spamming objects on the same tile (e.g. lights)
+ if (prototype.Prototype != null && MetaData(ent).EntityPrototype?.ID == prototype.Prototype)
+ {
+ var isIdentical = true;
+
+ if (prototype.AllowMultiDirection)
+ {
+ var entDirection = Transform(ent).LocalRotation.GetCardinalDir();
+ if (entDirection != direction)
+ isIdentical = false;
+ }
+
+ if (isIdentical)
+ {
+ if (popMsgs)
+ _popup.PopupClient(Loc.GetString("rcd-component-cannot-build-identical-entity"), uid, user);
+
+ return false;
+ }
+ }
+
if (isWindow && HasComp(ent))
continue;
@@ -534,7 +577,10 @@ public sealed class RCDSystem : EntitySystem
switch (prototype.Mode)
{
case RcdMode.ConstructTile:
- _mapSystem.SetTile(gridUid, mapGrid, position, new Tile(_tileDefMan[prototype.Prototype].TileId));
+ if (!_tileDefMan.TryGetDefinition(prototype.Prototype, out var tileDef))
+ return;
+
+ _tile.ReplaceTile(tile, (ContentTileDefinition) tileDef, gridUid, mapGrid);
_adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(user):user} used RCD to set grid: {gridUid} {position} to {prototype.Prototype}");
break;
diff --git a/Resources/Locale/en-US/rcd/components/rcd-component.ftl b/Resources/Locale/en-US/rcd/components/rcd-component.ftl
index 9741bde388..17fda9111e 100644
--- a/Resources/Locale/en-US/rcd/components/rcd-component.ftl
+++ b/Resources/Locale/en-US/rcd/components/rcd-component.ftl
@@ -29,6 +29,7 @@ rcd-component-must-build-on-subfloor-message = You can only build that on expose
rcd-component-cannot-build-on-subfloor-message = You can't build that on exposed subfloor!
rcd-component-cannot-build-on-occupied-tile-message = You can't build here, the space is already occupied!
rcd-component-cannot-build-identical-tile = That tile already exists there!
+rcd-component-cannot-build-identical-entity = That already exists there!
### Category names
diff --git a/Resources/Prototypes/RCD/rcd.yml b/Resources/Prototypes/RCD/rcd.yml
index 5fb5356f91..b173fa4157 100644
--- a/Resources/Prototypes/RCD/rcd.yml
+++ b/Resources/Prototypes/RCD/rcd.yml
@@ -37,7 +37,7 @@
category: WallsAndFlooring
sprite: /Textures/Interface/Radial/RCD/plating.png
mode: ConstructTile
- prototype: Plating
+ prototype: PlatingRCD
cost: 1
delay: 1
collisionMask: InteractImpassable
@@ -128,6 +128,7 @@
- IsWindow
rotation: User
fx: EffectRCDConstruct1
+ allowMultiDirection: true
- type: rcd
id: ReinforcedWindow
@@ -157,6 +158,7 @@
- IsWindow
rotation: User
fx: EffectRCDConstruct2
+ allowMultiDirection: true
# Airlocks
- type: rcd
@@ -208,6 +210,7 @@
collisionBounds: "-0.23,-0.49,0.23,-0.36"
rotation: User
fx: EffectRCDConstruct1
+ allowMultiDirection: true
- type: rcd
id: BulbLight
@@ -221,6 +224,7 @@
collisionBounds: "-0.23,-0.49,0.23,-0.36"
rotation: User
fx: EffectRCDConstruct1
+ allowMultiDirection: true
# Electrical
- type: rcd
diff --git a/Resources/Prototypes/Tiles/floors.yml b/Resources/Prototypes/Tiles/floors.yml
index 52657990d1..2d2a9ff3ea 100644
--- a/Resources/Prototypes/Tiles/floors.yml
+++ b/Resources/Prototypes/Tiles/floors.yml
@@ -22,6 +22,8 @@
- FloorPlanetGrass
- FloorSnow
- FloorDirt
+ - PlatingRCD
+ - FloorHullReinforced
- type: tile
id: FloorSteel
@@ -1607,17 +1609,6 @@
collection: FootstepHull
itemDrop: FloorTileItemSteel #probably should not be normally obtainable, but the game shits itself and dies when you try to put null here
-- type: tile
- id: FloorHullReinforced
- parent: BaseStationTile
- name: tiles-hull-reinforced
- sprite: /Textures/Tiles/hull_reinforced.png
- footstepSounds:
- collection: FootstepHull
- itemDrop: FloorTileItemSteel
- heatCapacity: 100000 #/tg/ has this set as "INFINITY." I don't know if that exists here so I've just added an extra 0
- indestructible: true
-
- type: tile
id: FloorReinforcedHardened
parent: BaseStationTile
diff --git a/Resources/Prototypes/Tiles/plating.yml b/Resources/Prototypes/Tiles/plating.yml
index 910f941bee..a6f150959d 100644
--- a/Resources/Prototypes/Tiles/plating.yml
+++ b/Resources/Prototypes/Tiles/plating.yml
@@ -16,6 +16,34 @@
name: tiles-plating
sprite: /Textures/Tiles/plating.png
+- type: tile
+ id: PlatingRCD
+ parent: Plating
+ baseWhitelist:
+ - TrainLattice
+ - FloorPlanetDirt
+ - FloorDesert
+ - FloorLowDesert
+ - FloorPlanetGrass
+ - FloorSnow
+ - FloorDirt
+ - FloorAsteroidIronsand
+ - FloorAsteroidSand
+ - FloorAsteroidSandBorderless
+ - FloorAsteroidIronsandBorderless
+ - FloorAsteroidSandRedBorderless
+
+- type: tile
+ id: FloorHullReinforced
+ parent: BasePlating
+ name: tiles-hull-reinforced
+ sprite: /Textures/Tiles/hull_reinforced.png
+ footstepSounds:
+ collection: FootstepHull
+ itemDrop: FloorTileItemSteel
+ heatCapacity: 100000 #/tg/ has this set as "INFINITY." I don't know if that exists here so I've just added an extra 0
+ indestructible: true
+
- type: tile
id: PlatingDamaged
parent: BasePlating
--
2.52.0