diff --git a/Content.Server/Atmos/Components/PipeRestrictOverlapComponent.cs b/Content.Server/Atmos/Components/PipeRestrictOverlapComponent.cs new file mode 100644 index 0000000000..49e1a8c94d --- /dev/null +++ b/Content.Server/Atmos/Components/PipeRestrictOverlapComponent.cs @@ -0,0 +1,9 @@ +using Content.Server.Atmos.EntitySystems; + +namespace Content.Server.Atmos.Components; + +/// +/// This is used for restricting anchoring pipes so that they do not overlap. +/// +[RegisterComponent, Access(typeof(PipeRestrictOverlapSystem))] +public sealed partial class PipeRestrictOverlapComponent : Component; diff --git a/Content.Server/Atmos/EntitySystems/PipeRestrictOverlapSystem.cs b/Content.Server/Atmos/EntitySystems/PipeRestrictOverlapSystem.cs new file mode 100644 index 0000000000..c2ff87ca79 --- /dev/null +++ b/Content.Server/Atmos/EntitySystems/PipeRestrictOverlapSystem.cs @@ -0,0 +1,123 @@ +using System.Linq; +using Content.Server.Atmos.Components; +using Content.Server.NodeContainer; +using Content.Server.NodeContainer.Nodes; +using Content.Server.Popups; +using Content.Shared.Atmos; +using Content.Shared.Construction.Components; +using JetBrains.Annotations; +using Robust.Server.GameObjects; +using Robust.Shared.Map.Components; + +namespace Content.Server.Atmos.EntitySystems; + +/// +/// This handles restricting pipe-based entities from overlapping outlets/inlets with other entities. +/// +public sealed class PipeRestrictOverlapSystem : EntitySystem +{ + [Dependency] private readonly MapSystem _map = default!; + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly TransformSystem _xform = default!; + + private readonly List _anchoredEntities = new(); + private EntityQuery _nodeContainerQuery; + + /// + public override void Initialize() + { + SubscribeLocalEvent(OnAnchorStateChanged); + SubscribeLocalEvent(OnAnchorAttempt); + + _nodeContainerQuery = GetEntityQuery(); + } + + private void OnAnchorStateChanged(Entity ent, ref AnchorStateChangedEvent args) + { + if (!args.Anchored) + return; + + if (HasComp(ent) && CheckOverlap(ent)) + { + _popup.PopupEntity(Loc.GetString("pipe-restrict-overlap-popup-blocked", ("pipe", ent.Owner)), ent); + _xform.Unanchor(ent, Transform(ent)); + } + } + + private void OnAnchorAttempt(Entity ent, ref AnchorAttemptEvent args) + { + if (args.Cancelled) + return; + + if (!_nodeContainerQuery.TryComp(ent, out var node)) + return; + + var xform = Transform(ent); + if (CheckOverlap((ent, node, xform))) + { + _popup.PopupEntity(Loc.GetString("pipe-restrict-overlap-popup-blocked", ("pipe", ent.Owner)), ent, args.User); + args.Cancel(); + } + } + + [PublicAPI] + public bool CheckOverlap(EntityUid uid) + { + if (!_nodeContainerQuery.TryComp(uid, out var node)) + return false; + + return CheckOverlap((uid, node, Transform(uid))); + } + + public bool CheckOverlap(Entity ent) + { + if (ent.Comp2.GridUid is not { } grid || !TryComp(grid, out var gridComp)) + return false; + + var indices = _map.TileIndicesFor(grid, gridComp, ent.Comp2.Coordinates); + _anchoredEntities.Clear(); + _map.GetAnchoredEntities((grid, gridComp), indices, _anchoredEntities); + + foreach (var otherEnt in _anchoredEntities) + { + // this should never actually happen but just for safety + if (otherEnt == ent.Owner) + continue; + + if (!_nodeContainerQuery.TryComp(otherEnt, out var otherComp)) + continue; + + if (PipeNodesOverlap(ent, (otherEnt, otherComp, Transform(otherEnt)))) + return true; + } + + return false; + } + + public bool PipeNodesOverlap(Entity ent, Entity other) + { + var entDirs = GetAllDirections(ent).ToList(); + var otherDirs = GetAllDirections(other).ToList(); + + foreach (var dir in entDirs) + { + foreach (var otherDir in otherDirs) + { + if ((dir & otherDir) != 0) + return true; + } + } + + return false; + + IEnumerable GetAllDirections(Entity pipe) + { + foreach (var node in pipe.Comp1.Nodes.Values) + { + // we need to rotate the pipe manually like this because the rotation doesn't update for pipes that are unanchored. + if (node is PipeNode pipeNode) + yield return pipeNode.OriginalPipeDirection.RotatePipeDirection(pipe.Comp2.LocalRotation); + } + } + } +} diff --git a/Content.Server/Construction/ConstructionSystem.Initial.cs b/Content.Server/Construction/ConstructionSystem.Initial.cs index 17ed5c90f4..04d3722c66 100644 --- a/Content.Server/Construction/ConstructionSystem.Initial.cs +++ b/Content.Server/Construction/ConstructionSystem.Initial.cs @@ -15,6 +15,7 @@ using Content.Shared.Interaction; using Content.Shared.Inventory; using Content.Shared.Storage; using Robust.Shared.Containers; +using Robust.Shared.Map; using Robust.Shared.Player; using Robust.Shared.Timing; @@ -91,7 +92,14 @@ namespace Content.Server.Construction } // LEGACY CODE. See warning at the top of the file! - private async Task Construct(EntityUid user, string materialContainer, ConstructionGraphPrototype graph, ConstructionGraphEdge edge, ConstructionGraphNode targetNode) + private async Task Construct( + EntityUid user, + string materialContainer, + ConstructionGraphPrototype graph, + ConstructionGraphEdge edge, + ConstructionGraphNode targetNode, + EntityCoordinates coords, + Angle angle = default) { // We need a place to hold our construction items! var container = _container.EnsureContainer(user, materialContainer, out var existed); @@ -261,7 +269,7 @@ namespace Content.Server.Construction } var newEntityProto = graph.Nodes[edge.Target].Entity.GetId(null, user, new(EntityManager)); - var newEntity = EntityManager.SpawnEntity(newEntityProto, EntityManager.GetComponent(user).Coordinates); + var newEntity = Spawn(newEntityProto, _transformSystem.ToMapCoordinates(coords), rotation: angle); if (!TryComp(newEntity, out ConstructionComponent? construction)) { @@ -376,7 +384,13 @@ namespace Content.Server.Construction } } - if (await Construct(user, "item_construction", constructionGraph, edge, targetNode) is not { Valid: true } item) + if (await Construct( + user, + "item_construction", + constructionGraph, + edge, + targetNode, + Transform(user).Coordinates) is not { Valid: true } item) return false; // Just in case this is a stack, attempt to merge it. If it isn't a stack, this will just normally pick up @@ -511,23 +525,18 @@ namespace Content.Server.Construction return; } - if (await Construct(user, (ev.Ack + constructionPrototype.GetHashCode()).ToString(), constructionGraph, - edge, targetNode) is not {Valid: true} structure) + if (await Construct(user, + (ev.Ack + constructionPrototype.GetHashCode()).ToString(), + constructionGraph, + edge, + targetNode, + GetCoordinates(ev.Location), + constructionPrototype.CanRotate ? ev.Angle : Angle.Zero) is not {Valid: true} structure) { Cleanup(); return; } - // We do this to be able to move the construction to its proper position in case it's anchored... - // Oh wow transform anchoring is amazing wow I love it!!!! - // ikr - var xform = Transform(structure); - var wasAnchored = xform.Anchored; - xform.Anchored = false; - xform.Coordinates = GetCoordinates(ev.Location); - xform.LocalRotation = constructionPrototype.CanRotate ? ev.Angle : Angle.Zero; - xform.Anchored = wasAnchored; - RaiseNetworkEvent(new AckStructureConstructionMessage(ev.Ack, GetNetEntity(structure))); _adminLogger.Add(LogType.Construction, LogImpact.Low, $"{ToPrettyString(user):player} has turned a {ev.PrototypeName} construction ghost into {ToPrettyString(structure)} at {Transform(structure).Coordinates}"); Cleanup(); diff --git a/Content.Server/NodeContainer/Nodes/PipeNode.cs b/Content.Server/NodeContainer/Nodes/PipeNode.cs index 861f3eea98..31ee571249 100644 --- a/Content.Server/NodeContainer/Nodes/PipeNode.cs +++ b/Content.Server/NodeContainer/Nodes/PipeNode.cs @@ -20,7 +20,7 @@ namespace Content.Server.NodeContainer.Nodes /// The directions in which this pipe can connect to other pipes around it. /// [DataField("pipeDirection")] - private PipeDirection _originalPipeDirection; + public PipeDirection OriginalPipeDirection; /// /// The *current* pipe directions (accounting for rotation) @@ -110,26 +110,26 @@ namespace Content.Server.NodeContainer.Nodes return; var xform = entMan.GetComponent(owner); - CurrentPipeDirection = _originalPipeDirection.RotatePipeDirection(xform.LocalRotation); + CurrentPipeDirection = OriginalPipeDirection.RotatePipeDirection(xform.LocalRotation); } bool IRotatableNode.RotateNode(in MoveEvent ev) { - if (_originalPipeDirection == PipeDirection.Fourway) + if (OriginalPipeDirection == PipeDirection.Fourway) return false; // update valid pipe direction if (!RotationsEnabled) { - if (CurrentPipeDirection == _originalPipeDirection) + if (CurrentPipeDirection == OriginalPipeDirection) return false; - CurrentPipeDirection = _originalPipeDirection; + CurrentPipeDirection = OriginalPipeDirection; return true; } var oldDirection = CurrentPipeDirection; - CurrentPipeDirection = _originalPipeDirection.RotatePipeDirection(ev.NewRotation); + CurrentPipeDirection = OriginalPipeDirection.RotatePipeDirection(ev.NewRotation); return oldDirection != CurrentPipeDirection; } @@ -142,12 +142,12 @@ namespace Content.Server.NodeContainer.Nodes if (!RotationsEnabled) { - CurrentPipeDirection = _originalPipeDirection; + CurrentPipeDirection = OriginalPipeDirection; return; } var xform = entityManager.GetComponent(Owner); - CurrentPipeDirection = _originalPipeDirection.RotatePipeDirection(xform.LocalRotation); + CurrentPipeDirection = OriginalPipeDirection.RotatePipeDirection(xform.LocalRotation); } public override IEnumerable GetReachableNodes(TransformComponent xform, 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 index 37ce0de9e8..715825e801 100644 --- a/Resources/Locale/en-US/construction/conditions/no-unstackable-in-tile.ftl +++ b/Resources/Locale/en-US/construction/conditions/no-unstackable-in-tile.ftl @@ -1 +1,2 @@ construction-step-condition-no-unstackable-in-tile = You cannot make a stack of similar devices. +pipe-restrict-overlap-popup-blocked = { CAPITALIZE(THE($pipe))} doesn't fit over the other pipes! diff --git a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/binary.yml b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/binary.yml index fa5804c645..213b0b893d 100644 --- a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/binary.yml +++ b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/binary.yml @@ -358,6 +358,7 @@ - type: PipeColorVisuals - type: Rotatable - type: GasRecycler + - type: PipeRestrictOverlap - type: NodeContainer nodes: inlet: diff --git a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/pipes.yml b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/pipes.yml index 0025fc5ae1..c2fc4e0565 100644 --- a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/pipes.yml +++ b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/pipes.yml @@ -51,6 +51,7 @@ - type: Appearance - type: PipeColorVisuals - type: NodeContainer + - type: PipeRestrictOverlap - type: AtmosUnsafeUnanchor - type: AtmosPipeColor - type: Tag diff --git a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/unary.yml b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/unary.yml index c0664602b4..d301f43c78 100644 --- a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/unary.yml +++ b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/unary.yml @@ -246,6 +246,7 @@ key: enum.ThermomachineUiKey.Key - type: WiresPanel - type: WiresVisuals + - type: PipeRestrictOverlap - type: NodeContainer nodes: pipe: @@ -420,6 +421,7 @@ - type: GasCondenser - type: AtmosPipeColor - type: AtmosDevice + - type: PipeRestrictOverlap - type: ApcPowerReceiver powerLoad: 10000 - type: Machine diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/teg.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/teg.yml index 9a378c26a4..78d979ab8e 100644 --- a/Resources/Prototypes/Entities/Structures/Power/Generation/teg.yml +++ b/Resources/Prototypes/Entities/Structures/Power/Generation/teg.yml @@ -176,6 +176,7 @@ nodeGroupID: Teg - type: AtmosUnsafeUnanchor + - type: PipeRestrictOverlap - type: TegCirculator - type: StealTarget stealGroup: Teg