diff --git a/Content.Client/Atmos/UI/GasFilterBoundUserInterface.cs b/Content.Client/Atmos/UI/GasFilterBoundUserInterface.cs
index a0daf9c654..6c16881224 100644
--- a/Content.Client/Atmos/UI/GasFilterBoundUserInterface.cs
+++ b/Content.Client/Atmos/UI/GasFilterBoundUserInterface.cs
@@ -14,7 +14,6 @@ namespace Content.Client.Atmos.UI
{
private GasFilterWindow? _window;
- private const float MaxTransferRate = Atmospherics.MaxTransferRate;
public GasFilterBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
@@ -49,7 +48,6 @@ namespace Content.Client.Atmos.UI
private void OnFilterTransferRatePressed(string value)
{
float rate = float.TryParse(value, out var parsed) ? parsed : 0f;
- if (rate > MaxTransferRate) rate = MaxTransferRate;
SendMessage(new GasFilterChangeRateMessage(rate));
}
diff --git a/Content.Client/Atmos/UI/GasFilterWindow.xaml b/Content.Client/Atmos/UI/GasFilterWindow.xaml
index 30333b07bd..6963a71d3d 100644
--- a/Content.Client/Atmos/UI/GasFilterWindow.xaml
+++ b/Content.Client/Atmos/UI/GasFilterWindow.xaml
@@ -9,7 +9,7 @@
-
+
diff --git a/Content.Client/Construction/ConstructionSystem.cs b/Content.Client/Construction/ConstructionSystem.cs
index 8a7e0be06d..02ba8282c6 100644
--- a/Content.Client/Construction/ConstructionSystem.cs
+++ b/Content.Client/Construction/ConstructionSystem.cs
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
+using Content.Client.Popups;
using Content.Shared.Construction;
using Content.Shared.Construction.Prototypes;
using Content.Shared.Examine;
@@ -24,6 +25,7 @@ namespace Content.Client.Construction
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
private readonly Dictionary _ghosts = new();
private readonly Dictionary _guideCache = new();
@@ -188,11 +190,8 @@ namespace Content.Client.Construction
if (!_interactionSystem.InRangeUnobstructed(user, loc, 20f, predicate: predicate))
return false;
- foreach (var condition in prototype.Conditions)
- {
- if (!condition.Condition(user, loc, dir))
- return false;
- }
+ if (!CheckConstructionConditions(prototype, loc, dir, user, showPopup: true))
+ return false;
ghost = EntityManager.SpawnEntity("constructionghost", loc);
var comp = EntityManager.GetComponent(ghost.Value);
@@ -217,6 +216,30 @@ namespace Content.Client.Construction
return true;
}
+ private bool CheckConstructionConditions(ConstructionPrototype prototype, EntityCoordinates loc, Direction dir,
+ EntityUid user, bool showPopup = false)
+ {
+ foreach (var condition in prototype.Conditions)
+ {
+ if (!condition.Condition(user, loc, dir))
+ {
+ if (showPopup)
+ {
+ var message = condition.GenerateGuideEntry()?.Localization;
+ if (message != null)
+ {
+ // Show the reason to the user:
+ _popupSystem.PopupCoordinates(Loc.GetString(message), loc);
+ }
+ }
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+
///
/// Checks if any construction ghosts are present at the given position
///
diff --git a/Content.Server/Construction/AnchorableSystem.cs b/Content.Server/Construction/AnchorableSystem.cs
index a6583f9fc4..309ed9ebe6 100644
--- a/Content.Server/Construction/AnchorableSystem.cs
+++ b/Content.Server/Construction/AnchorableSystem.cs
@@ -1,13 +1,16 @@
using Content.Server.Administration.Logs;
+using Content.Server.Construction.Components;
using Content.Server.Coordinates.Helpers;
using Content.Server.Popups;
using Content.Server.Pulling;
using Content.Shared.Construction.Components;
+using Content.Shared.Construction.Conditions;
using Content.Shared.Construction.EntitySystems;
using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.Pulling.Components;
+using Content.Shared.Tag;
using Content.Shared.Tools;
using Content.Shared.Tools.Components;
using Robust.Shared.Map;
@@ -85,7 +88,15 @@ namespace Content.Server.Construction
// TODO: Anchoring snaps rn anyway!
if (component.Snap)
{
- _transform.SetCoordinates(uid, xform.Coordinates.SnapToGrid(EntityManager, _mapManager));
+ var coordinates = xform.Coordinates.SnapToGrid(EntityManager, _mapManager);
+
+ if (AnyUnstackable(uid, coordinates))
+ {
+ _popup.PopupEntity(Loc.GetString("construction-step-condition-no-unstackable-in-tile"), uid, args.User);
+ return;
+ }
+
+ _transform.SetCoordinates(uid, coordinates);
}
RaiseLocalEvent(uid, new BeforeAnchoredEvent(args.User, used));
@@ -195,6 +206,12 @@ namespace Content.Server.Construction
return;
}
+ if (AnyUnstackable(uid, transform.Coordinates))
+ {
+ _popup.PopupEntity(Loc.GetString("construction-step-condition-no-unstackable-in-tile"), uid, userUid);
+ return;
+ }
+
_tool.UseTool(usingUid, userUid, uid, anchorable.Delay, usingTool.Qualities, new TryAnchorCompletedEvent());
}
diff --git a/Content.Shared/Construction/Conditions/NoUnstackableInTile.cs b/Content.Shared/Construction/Conditions/NoUnstackableInTile.cs
new file mode 100644
index 0000000000..84e048db9a
--- /dev/null
+++ b/Content.Shared/Construction/Conditions/NoUnstackableInTile.cs
@@ -0,0 +1,31 @@
+using Content.Shared.Construction.EntitySystems;
+using JetBrains.Annotations;
+using Robust.Shared.Map;
+
+namespace Content.Shared.Construction.Conditions;
+
+///
+/// Check for "Unstackable" condition commonly used by atmos devices and others which otherwise don't check on
+/// collisions with other items.
+///
+[UsedImplicitly]
+[DataDefinition]
+public sealed class NoUnstackableInTile : IConstructionCondition
+{
+ public const string GuidebookString = "construction-step-condition-no-unstackable-in-tile";
+ public bool Condition(EntityUid user, EntityCoordinates location, Direction direction)
+ {
+ var sysMan = IoCManager.Resolve();
+ var anchorable = sysMan.GetEntitySystem();
+
+ return !anchorable.AnyUnstackablesAnchoredAt(location);
+ }
+
+ public ConstructionGuideEntry GenerateGuideEntry()
+ {
+ return new ConstructionGuideEntry
+ {
+ Localization = GuidebookString
+ };
+ }
+}
diff --git a/Content.Shared/Construction/Conditions/NoWindowsInTile.cs b/Content.Shared/Construction/Conditions/NoWindowsInTile.cs
index 4a409a998e..3488c8927d 100644
--- a/Content.Shared/Construction/Conditions/NoWindowsInTile.cs
+++ b/Content.Shared/Construction/Conditions/NoWindowsInTile.cs
@@ -2,6 +2,7 @@
using Content.Shared.Tag;
using JetBrains.Annotations;
using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
namespace Content.Shared.Construction.Conditions
{
@@ -11,11 +12,20 @@ namespace Content.Shared.Construction.Conditions
{
public bool Condition(EntityUid user, EntityCoordinates location, Direction direction)
{
- var tagSystem = IoCManager.Resolve().GetEntitySystem();
+ var entManager = IoCManager.Resolve();
+ var gridUid = location.GetGridUid(entManager);
- foreach (var entity in location.GetEntitiesInTile(LookupFlags.Approximate | LookupFlags.Static))
+ if (!entManager.TryGetComponent(gridUid, out var grid))
+ return true;
+
+ var tagQuery = entManager.GetEntityQuery();
+ var sysMan = entManager.EntitySysManager;
+ var tagSystem = sysMan.GetEntitySystem();
+ var lookup = sysMan.GetEntitySystem();
+
+ foreach (var entity in lookup.GetEntitiesIntersecting(gridUid.Value, grid.LocalToTile(location)))
{
- if (tagSystem.HasTag(entity, "Window"))
+ if (tagSystem.HasTag(entity, "Window", tagQuery))
return false;
}
diff --git a/Content.Shared/Construction/EntitySystems/SharedAnchorableSystem.cs b/Content.Shared/Construction/EntitySystems/SharedAnchorableSystem.cs
index 93e9504b12..df15b9b872 100644
--- a/Content.Shared/Construction/EntitySystems/SharedAnchorableSystem.cs
+++ b/Content.Shared/Construction/EntitySystems/SharedAnchorableSystem.cs
@@ -3,17 +3,28 @@ using Content.Shared.Containers.ItemSlots;
using Content.Shared.DoAfter;
using Content.Shared.Interaction;
using Content.Shared.Pulling.Components;
+using Content.Shared.Tag;
using Content.Shared.Tools.Components;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
using Robust.Shared.Serialization;
namespace Content.Shared.Construction.EntitySystems;
public abstract class SharedAnchorableSystem : EntitySystem
{
+ [Dependency] private readonly TagSystem _tagSystem = default!;
+
+ protected EntityQuery TagQuery;
+
+ public const string Unstackable = "Unstackable";
+
public override void Initialize()
{
base.Initialize();
+ TagQuery = GetEntityQuery();
+
SubscribeLocalEvent(OnInteractUsing,
before: new[] { typeof(ItemSlotsSystem) }, after: new[] { typeof(SharedConstructionSystem) });
}
@@ -42,6 +53,38 @@ public abstract class SharedAnchorableSystem : EntitySystem
// TODO tool system is fixed now, make this actually shared.
}
+ public bool AnyUnstackablesAnchoredAt(EntityCoordinates location)
+ {
+ var gridUid = location.GetGridUid(EntityManager);
+
+ if (!TryComp(gridUid, out var grid))
+ return false;
+
+ var enumerator = grid.GetAnchoredEntitiesEnumerator(grid.LocalToTile(location));
+
+ while (enumerator.MoveNext(out var entity))
+ {
+ // If we find another unstackable here, return true.
+ if (_tagSystem.HasTag(entity.Value, Unstackable, TagQuery))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public bool AnyUnstackable(EntityUid uid, EntityCoordinates location)
+ {
+ // If we are unstackable, iterate through any other entities anchored on the current square
+ if (_tagSystem.HasTag(uid, Unstackable, TagQuery))
+ {
+ return AnyUnstackablesAnchoredAt(location);
+ }
+
+ return false;
+ }
+
[Serializable, NetSerializable]
protected sealed class TryUnanchorCompletedEvent : SimpleDoAfterEvent
{
diff --git a/Resources/Locale/en-US/construction/conditions/no-unstackable-in-tile.ftl b/Resources/Locale/en-US/construction/conditions/no-unstackable-in-tile.ftl
new file mode 100644
index 0000000000..37ce0de9e8
--- /dev/null
+++ b/Resources/Locale/en-US/construction/conditions/no-unstackable-in-tile.ftl
@@ -0,0 +1 @@
+construction-step-condition-no-unstackable-in-tile = You cannot make a stack of similar devices.
diff --git a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/binary.yml b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/binary.yml
index 698b9c224d..758f4dbc00 100644
--- a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/binary.yml
+++ b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/binary.yml
@@ -6,6 +6,9 @@
mode: SnapgridCenter
components:
- type: AtmosDevice
+ - type: Tag
+ tags:
+ - Unstackable
- type: SubFloorHide
blockInteractions: false
blockAmbience: false
diff --git a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/trinary.yml b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/trinary.yml
index 0b435f426f..a6f831e1a5 100644
--- a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/trinary.yml
+++ b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/trinary.yml
@@ -6,6 +6,9 @@
mode: SnapgridCenter
components:
- type: AtmosDevice
+ - type: Tag
+ tags:
+ - Unstackable
- type: SubFloorHide
blockInteractions: false
blockAmbience: false
@@ -54,11 +57,16 @@
type: GasFilterBoundUserInterface
- type: GasFilter
enabled: false
+ transferRate: 1000
+ maxTransferRate: 1000
- type: Flippable
mirrorEntity: GasFilterFlipped
- type: Construction
graph: GasTrinary
node: filter
+ conditions:
+ - !type:TileNotBlocked
+ - !type:NoUnstackableInTile
- type: AmbientSound
enabled: false
volume: -9
@@ -201,6 +209,9 @@
mode: SnapgridCenter
components:
- type: AtmosDevice
+ - type: Tag
+ tags:
+ - Unstackable
- type: SubFloorHide
blockInteractions: false
blockAmbience: false
diff --git a/Resources/Prototypes/Recipes/Construction/utilities.yml b/Resources/Prototypes/Recipes/Construction/utilities.yml
index f39b11b477..6eba83b82f 100644
--- a/Resources/Prototypes/Recipes/Construction/utilities.yml
+++ b/Resources/Prototypes/Recipes/Construction/utilities.yml
@@ -530,6 +530,7 @@
state: pumpPressure
conditions:
- !type:TileNotBlocked {}
+ - !type:NoUnstackableInTile
- type: construction
name: volumetric gas pump
@@ -551,6 +552,7 @@
state: pumpVolume
conditions:
- !type:TileNotBlocked {}
+ - !type:NoUnstackableInTile
- type: construction
id: GasPassiveGate
@@ -572,6 +574,7 @@
state: pumpPassiveGate
conditions:
- !type:TileNotBlocked {}
+ - !type:NoUnstackableInTile
- type: construction
id: GasValve
@@ -593,6 +596,7 @@
state: pumpManualValve
conditions:
- !type:TileNotBlocked {}
+ - !type:NoUnstackableInTile
- type: construction
id: SignalControlledValve
@@ -614,6 +618,7 @@
state: pumpSignalValve
conditions:
- !type:TileNotBlocked {}
+ - !type:NoUnstackableInTile
- type: construction
id: GasPort
@@ -635,6 +640,7 @@
state: gasCanisterPort
conditions:
- !type:TileNotBlocked {}
+ - !type:NoUnstackableInTile
- type: construction
id: GasDualPortVentPump
@@ -674,6 +680,7 @@
mirror: GasFilterFlipped
conditions:
- !type:TileNotBlocked {}
+ - !type:NoUnstackableInTile
- type: construction
id: GasFilterFlipped
@@ -692,6 +699,7 @@
mirror: GasFilter
conditions:
- !type:TileNotBlocked {}
+ - !type:NoUnstackableInTile
- type: construction
id: GasMixer
@@ -709,6 +717,7 @@
mirror: GasMixerFlipped
conditions:
- !type:TileNotBlocked {}
+ - !type:NoUnstackableInTile
- type: construction
id: GasMixerFlipped
@@ -726,7 +735,8 @@
state: gasMixerF
mirror: GasMixer
conditions:
- - !type:TileNotBlocked {}
+ - !type:TileNotBlocked
+ - !type:NoUnstackableInTile
- type: construction
id: PressureControlledValve
@@ -742,7 +752,8 @@
sprite: Structures/Piping/Atmospherics/pneumaticvalve.rsi
state: off
conditions:
- - !type:TileNotBlocked {}
+ - !type:TileNotBlocked
+ - !type:NoUnstackableInTile
# INTERCOM
- type: construction
diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml
index 5b2d7841ac..991ecbb5a6 100644
--- a/Resources/Prototypes/tags.yml
+++ b/Resources/Prototypes/tags.yml
@@ -796,6 +796,9 @@
- type: Tag
id: TrashBag
+
+- type: Tag
+ id: Unstackable # To prevent things like atmos devices (filters etc) being stacked on one tile. See NoUnstackableInTile
- type: Tag
id: UraniumGlassShard