From b153de7901c930cdc65554776f5d868cd454c5d7 Mon Sep 17 00:00:00 2001 From: Dunrab Date: Mon, 15 Jun 2026 17:13:34 -0400 Subject: [PATCH 01/10] pipe layers llc is in business --- .../RCD/RCDConstructionGhostSystem.cs | 82 ++++- .../RCD/RCDMenuBoundUserInterface.cs | 9 + ...AlignRPDAtmosPipeLayers.cs\342\200\216.cs" | 297 ++++++++++++++++ .../_Starlight/RCD/Systems/RPDSystem.cs | 60 ++++ .../EntitySystems/AtmosPipeLayersSystem.cs | 4 +- .../NodeContainer/Nodes/PipeNode.cs | 7 +- Content.Shared/RCD/Components/RCDComponent.cs | 52 +++ .../Components/RCDDeconstructableComponent.cs | 10 +- Content.Shared/RCD/RCDEvents.cs | 27 ++ Content.Shared/RCD/RCDPrototype.cs | 16 + Content.Shared/RCD/Systems/RCDSystem.cs | 323 ++++++++++++++++- .../PipeRestrictOverlapComponent.cs | 9 + .../PipeRestrictOverlapSystem.cs | 183 ++++++++++ Content.Shared/_Starlight/Atmos/IPipeNode.cs | 10 + .../en-US/_Starlight/rpd/rpd-components.ftl | 12 + Resources/Prototypes/Datasets/ion_storm.yml | 1 + .../Structures/Piping/Atmospherics/pipes.yml | 7 + .../Specific/Atmospherics/sensor.yml | 7 + .../WallmountMachines/air_alarm.yml | 7 + .../Entities/Objects/Tools/tools.yml | 71 ++++ Resources/Prototypes/_Starlight/RPD/rpd.yml | 326 ++++++++++++++++++ .../Interface/Radial/RPD/airalarm.png | Bin 0 -> 1295 bytes .../Interface/Radial/RPD/airsensor.png | Bin 0 -> 166 bytes .../_Starlight/Interface/Radial/RPD/bend.png | Bin 0 -> 330 bytes .../Interface/Radial/RPD/dual_port.png | Bin 0 -> 777 bytes .../Interface/Radial/RPD/fourway.png | Bin 0 -> 679 bytes .../Interface/Radial/RPD/gas_filter.png | Bin 0 -> 369 bytes .../Interface/Radial/RPD/gas_mixer.png | Bin 0 -> 391 bytes .../_Starlight/Interface/Radial/RPD/half.png | Bin 0 -> 522 bytes .../Interface/Radial/RPD/injector.png | Bin 0 -> 368 bytes .../Interface/Radial/RPD/manifold.png | Bin 0 -> 493 bytes .../Interface/Radial/RPD/passive_gate.png | Bin 0 -> 555 bytes .../Interface/Radial/RPD/pneumatic_valve.png | Bin 0 -> 308 bytes .../_Starlight/Interface/Radial/RPD/port.png | Bin 0 -> 288 bytes .../Radial/RPD/pressure_regulator.png | Bin 0 -> 550 bytes .../Radial/RPD/pump_manual_valve.png | Bin 0 -> 334 bytes .../Interface/Radial/RPD/pump_pressure.png | Bin 0 -> 332 bytes .../Radial/RPD/pump_signal_valve.png | Bin 0 -> 283 bytes .../Interface/Radial/RPD/pump_volume.png | Bin 0 -> 274 bytes .../Interface/Radial/RPD/radiator.png | Bin 0 -> 280 bytes .../Interface/Radial/RPD/radiator_bend.png | Bin 0 -> 569 bytes .../Interface/Radial/RPD/scrub_off.png | Bin 0 -> 820 bytes .../Interface/Radial/RPD/sensor.png | Bin 0 -> 657 bytes .../Interface/Radial/RPD/straight.png | Bin 0 -> 337 bytes .../Interface/Radial/RPD/tjunction.png | Bin 0 -> 359 bytes .../Interface/Radial/RPD/vent_off.png | Bin 0 -> 612 bytes .../Interface/Radial/RPD/vent_passive.png | Bin 0 -> 388 bytes .../_Starlight/Objects/Tools/rpd.rsi/icon.png | Bin 0 -> 872 bytes .../Objects/Tools/rpd.rsi/inhand-left.png | Bin 0 -> 1068 bytes .../Objects/Tools/rpd.rsi/inhand-right.png | Bin 0 -> 1100 bytes .../Objects/Tools/rpd.rsi/meta.json | 25 ++ 51 files changed, 1524 insertions(+), 21 deletions(-) create mode 100644 "Content.Client/_Starlight/RCD/AlignRPDAtmosPipeLayers.cs\342\200\216.cs" create mode 100644 Content.Client/_Starlight/RCD/Systems/RPDSystem.cs create mode 100644 Content.Shared/_Starlight/Atmos/Components/PipeRestrictOverlapComponent.cs create mode 100644 Content.Shared/_Starlight/Atmos/EntitiySystems/PipeRestrictOverlapSystem.cs create mode 100644 Content.Shared/_Starlight/Atmos/IPipeNode.cs create mode 100644 Resources/Locale/en-US/_Starlight/rpd/rpd-components.ftl create mode 100644 Resources/Prototypes/_Starlight/RPD/rpd.yml create mode 100644 Resources/Textures/_Starlight/Interface/Radial/RPD/airalarm.png create mode 100644 Resources/Textures/_Starlight/Interface/Radial/RPD/airsensor.png create mode 100644 Resources/Textures/_Starlight/Interface/Radial/RPD/bend.png create mode 100644 Resources/Textures/_Starlight/Interface/Radial/RPD/dual_port.png create mode 100644 Resources/Textures/_Starlight/Interface/Radial/RPD/fourway.png create mode 100644 Resources/Textures/_Starlight/Interface/Radial/RPD/gas_filter.png create mode 100644 Resources/Textures/_Starlight/Interface/Radial/RPD/gas_mixer.png create mode 100644 Resources/Textures/_Starlight/Interface/Radial/RPD/half.png create mode 100644 Resources/Textures/_Starlight/Interface/Radial/RPD/injector.png create mode 100644 Resources/Textures/_Starlight/Interface/Radial/RPD/manifold.png create mode 100644 Resources/Textures/_Starlight/Interface/Radial/RPD/passive_gate.png create mode 100644 Resources/Textures/_Starlight/Interface/Radial/RPD/pneumatic_valve.png create mode 100644 Resources/Textures/_Starlight/Interface/Radial/RPD/port.png create mode 100644 Resources/Textures/_Starlight/Interface/Radial/RPD/pressure_regulator.png create mode 100644 Resources/Textures/_Starlight/Interface/Radial/RPD/pump_manual_valve.png create mode 100644 Resources/Textures/_Starlight/Interface/Radial/RPD/pump_pressure.png create mode 100644 Resources/Textures/_Starlight/Interface/Radial/RPD/pump_signal_valve.png create mode 100644 Resources/Textures/_Starlight/Interface/Radial/RPD/pump_volume.png create mode 100644 Resources/Textures/_Starlight/Interface/Radial/RPD/radiator.png create mode 100644 Resources/Textures/_Starlight/Interface/Radial/RPD/radiator_bend.png create mode 100644 Resources/Textures/_Starlight/Interface/Radial/RPD/scrub_off.png create mode 100644 Resources/Textures/_Starlight/Interface/Radial/RPD/sensor.png create mode 100644 Resources/Textures/_Starlight/Interface/Radial/RPD/straight.png create mode 100644 Resources/Textures/_Starlight/Interface/Radial/RPD/tjunction.png create mode 100644 Resources/Textures/_Starlight/Interface/Radial/RPD/vent_off.png create mode 100644 Resources/Textures/_Starlight/Interface/Radial/RPD/vent_passive.png create mode 100644 Resources/Textures/_Starlight/Objects/Tools/rpd.rsi/icon.png create mode 100644 Resources/Textures/_Starlight/Objects/Tools/rpd.rsi/inhand-left.png create mode 100644 Resources/Textures/_Starlight/Objects/Tools/rpd.rsi/inhand-right.png create mode 100644 Resources/Textures/_Starlight/Objects/Tools/rpd.rsi/meta.json diff --git a/Content.Client/RCD/RCDConstructionGhostSystem.cs b/Content.Client/RCD/RCDConstructionGhostSystem.cs index d0af28da8aa..8854fa85cd7 100644 --- a/Content.Client/RCD/RCDConstructionGhostSystem.cs +++ b/Content.Client/RCD/RCDConstructionGhostSystem.cs @@ -6,6 +6,13 @@ using Robust.Client.Player; using Robust.Shared.Enums; using Robust.Shared.Prototypes; +// Starlight Start +using Robust.Shared.Input; +using Content.Client._Starlight.RCD; +using Robust.Shared.Input.Binding; +using Content.Client.Atmos; +using Content.Shared.Input; +// Starlight End namespace Content.Client.RCD; @@ -15,6 +22,7 @@ namespace Content.Client.RCD; public sealed class RCDConstructionGhostSystem : EntitySystem { private const string PlacementMode = nameof(AlignRCDConstruction); + private const string RpdPlacementMode = nameof(AlignRPDAtmosPipeLayers); // Starlight RPD [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPlacementManager _placementManager = default!; @@ -23,6 +31,51 @@ public sealed class RCDConstructionGhostSystem : EntitySystem private Direction _placementDirection = default; + // Starlight Start: RPD + private bool _useMirrorPrototype = false; + public event EventHandler? FlipConstructionPrototype; + + public override void Initialize() + { + base.Initialize(); + + // bind key + CommandBinds.Builder + .Bind(ContentKeyFunctions.EditorFlipObject, + new PointerInputCmdHandler(HandleFlip, outsidePrediction: true)) + .Register(); + } + + public override void Shutdown() + { + CommandBinds.Unregister(); + base.Shutdown(); + } + + private bool HandleFlip(in PointerInputCmdHandler.PointerInputCmdArgs args) + { + if (args.State == BoundKeyState.Down) + { + if (!_placementManager.IsActive || _placementManager.Eraser) + return false; + + var placerEntity = _placementManager.CurrentPermission?.MobUid; + + if (!TryComp(placerEntity, out var rcd) || + string.IsNullOrEmpty(rcd.CachedPrototype.MirrorPrototype)) + return false; + + _useMirrorPrototype = !rcd.UseMirrorPrototype; + + // tell the server + + RaiseNetworkEvent(new RCDConstructionGhostFlipEvent(GetNetEntity(placerEntity.Value), _useMirrorPrototype)); + } + + return true; + } + // Starlight End: RPD + public override void Update(float frameTime) { base.Update(frameTime); @@ -55,7 +108,20 @@ public override void Update(float frameTime) return; } - var prototype = _protoManager.Index(rcd.ProtoId); + // Starlight edit Start: RPD - use the mirrored prototype if the flip state is toggled on + // var prototype = _protoManager.Index(rcd.ProtoId); + + // Determine if mirrored + var cachedProto = rcd.CachedPrototype; + var wantMirror = _useMirrorPrototype && !string.IsNullOrEmpty(cachedProto.MirrorPrototype); + var prototype = wantMirror ? cachedProto.MirrorPrototype : cachedProto.Prototype; + + bool isLayered = rcd.IsRpd + && _protoManager.TryIndex(cachedProto.ID, out var rcdProto) + && rcdProto.HasLayers; + + var desiredMode = isLayered ? RpdPlacementMode : PlacementMode; + // Starlight edit End: RPD - use the mirrored prototype if the flip state is toggled on // Update the direction the RCD prototype based on the placer direction if (_placementDirection != _placementManager.Direction) @@ -65,17 +131,23 @@ public override void Update(float frameTime) } // If the placer has not changed, exit - if (heldEntity == placerEntity && prototype.Prototype == placerProto) + // Starlight edit Start + if (heldEntity == placerEntity && + prototype == placerProto && + _placementManager.CurrentPermission?.PlacementOption == desiredMode) + // Starlight edit End return; + //if (heldEntity == placerEntity && prototype.Prototype == placerProto) + // return; // Create a new placer var newObjInfo = new PlacementInformation { MobUid = heldEntity.Value, - PlacementOption = PlacementMode, - EntityType = prototype.Prototype, + PlacementOption = desiredMode, // Starlight Edit: PlacementMode -> desiredMode + EntityType = prototype, // Starlight Edit: prototype.Prototype -> prototype Range = (int)Math.Ceiling(SharedInteractionSystem.InteractionRange), - IsTile = (prototype.Mode == RcdMode.ConstructTile), + IsTile = (cachedProto.Mode == RcdMode.ConstructTile), // Starlight Edit: prototype.Mode -> cachedProto.Mode UseEditorContext = false, }; diff --git a/Content.Client/RCD/RCDMenuBoundUserInterface.cs b/Content.Client/RCD/RCDMenuBoundUserInterface.cs index da583d8e010..8ad6bdf3124 100644 --- a/Content.Client/RCD/RCDMenuBoundUserInterface.cs +++ b/Content.Client/RCD/RCDMenuBoundUserInterface.cs @@ -24,8 +24,17 @@ public sealed class RCDMenuBoundUserInterface : BoundUserInterface ["Airlocks"] = ("rcd-component-airlocks", new SpriteSpecifier.Texture(new ResPath("/Textures/_DV/Interface/Radial/RCD/airlocks.png"))), // DeltaV - Path changed to new sprites reflecting Delta-V resprites ["Electrical"] = ("rcd-component-electrical", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RCD/multicoil.png"))), ["Lighting"] = ("rcd-component-lighting", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RCD/lighting.png"))), + // Starlight Start: RPD + ["Piping"] = ("rpd-component-piping", new SpriteSpecifier.Texture(new ResPath("/Textures/_Starlight/Interface/Radial/RPD/fourway.png"))), + ["AtmosphericUtility"] = ("rpd-component-atmosphericutility", new SpriteSpecifier.Texture(new ResPath("/Textures/_Starlight/Interface/Radial/RPD/port.png"))), + ["PumpsValves"] = ("rpd-component-pumps", new SpriteSpecifier.Texture(new ResPath("/Textures/_Starlight/Interface/Radial/RPD/pump_volume.png"))), + ["Vents"] = ("rpd-component-vents", new SpriteSpecifier.Texture(new ResPath("/Textures/_Starlight/Interface/Radial/RPD/vent_passive.png"))), + ["SensorsMonitors"] = ("rpd-component-sensors-monitors", new SpriteSpecifier.Texture(new ResPath("/Textures/_Starlight/Interface/Radial/RPD/airalarm.png"))), + // Starlight End: RPD }; + private bool IsRpd => EntMan.TryGetComponent(Owner, out var rcd) && rcd.IsRpd; // Starlight: RPD + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly ISharedPlayerManager _playerManager = default!; diff --git "a/Content.Client/_Starlight/RCD/AlignRPDAtmosPipeLayers.cs\342\200\216.cs" "b/Content.Client/_Starlight/RCD/AlignRPDAtmosPipeLayers.cs\342\200\216.cs" new file mode 100644 index 00000000000..0d4ff710ab8 --- /dev/null +++ "b/Content.Client/_Starlight/RCD/AlignRPDAtmosPipeLayers.cs\342\200\216.cs" @@ -0,0 +1,297 @@ +using Content.Client.Gameplay; +using Content.Client.Hands.Systems; +using Content.Shared.Atmos.Components; +using Content.Shared._Starlight.Atmos.EntitySystems; +using Content.Shared.Hands.Components; +using Content.Shared.Interaction; +using Content.Shared.RCD; +using Content.Shared.RCD.Components; +using Content.Shared.RCD.Systems; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.Placement; +using Robust.Client.Player; +using Robust.Client.State; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; +using System.Numerics; +using static Robust.Client.Placement.PlacementManager; +using Content.Shared.Atmos.EntitySystems; + +namespace Content.Client._Starlight.RCD; + +/// +/// Funkystation +/// Allows users to place RCD prototypes with atmos pipe layers on different layers depending on how the mouse cursor is positioned within a grid tile. +/// +/// +/// This placement mode is not on the engine because it is content specific. +/// +public sealed class AlignRPDAtmosPipeLayers : PlacementMode +{ + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IPrototypeManager _protoManager = default!; + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IStateManager _stateManager = default!; + [Dependency] private readonly IEyeManager _eyeManager = default!; + [Dependency] private readonly IEntityNetworkManager _entityNetwork = default!; + + private readonly SharedMapSystem _mapSystem; + private readonly SharedTransformSystem _transformSystem; + private readonly SharedAtmosPipeLayersSystem _pipeLayersSystem; + private readonly SpriteSystem _spriteSystem; + private readonly RCDSystem _rcdSystem; + private readonly HandsSystem _handsSystem; + + private const float SearchBoxSize = 2f; + private const float MouseDeadzoneRadius = 0.25f; + private const float PlaceColorBaseAlpha = 0.5f; + private const float GuideRadius = 0.1f; + private const float GuideOffset = 0.21875f; + + private EntityCoordinates _mouseCoordsRaw = default; + private static AtmosPipeLayer _currentLayer = AtmosPipeLayer.Primary; + private static EntityUid? _lastLayerSyncEntity = null; + private static AtmosPipeLayer? _lastLayerSynced = null; + private Color _guideColor = new(0, 0, 0.5785f); + + public AlignRPDAtmosPipeLayers(PlacementManager pMan) : base(pMan) + { + IoCManager.InjectDependencies(this); + _mapSystem = _entityManager.System(); + _transformSystem = _entityManager.System(); + _spriteSystem = _entityManager.System(); + _rcdSystem = _entityManager.System(); + _pipeLayersSystem = _entityManager.System(); + _handsSystem = _entityManager.System(); + ValidPlaceColor = ValidPlaceColor.WithAlpha(PlaceColorBaseAlpha); + } + + public override void Render(in OverlayDrawArgs args) + { + // Early exit if mouse is out of interaction range + if (_playerManager.LocalSession?.AttachedEntity is not { } player || + !_entityManager.TryGetComponent(player, out var xform) || + !_transformSystem.InRange(xform.Coordinates, MouseCoords, SharedInteractionSystem.InteractionRange)) + { + return; + } + + var gridUid = _transformSystem.GetGrid(MouseCoords); + + if (gridUid == null || !_entityManager.TryGetComponent(gridUid, out var grid)) + return; + + if (!_handsSystem.TryGetActiveItem(player, out var heldEntity) || + !_entityManager.TryGetComponent(heldEntity, out var rcd)) + return; + + // Draw guide circles for each pipe layer if we are not in line/grid placing mode + if (rcd.CurrentMode == RCDComponent.RpdMode.Free && pManager.PlacementType == PlacementTypes.None ) + { + var gridRotation = _transformSystem.GetWorldRotation(gridUid.Value); + var worldPosition = _mapSystem.LocalToWorld(gridUid.Value, grid, MouseCoords.Position); + var direction = (_eyeManager.CurrentEye.Rotation + gridRotation + Math.PI / 2).GetCardinalDir(); + var multi = (direction == Direction.North || direction == Direction.South) ? -1f : 1f; + + args.WorldHandle.DrawCircle(worldPosition, GuideRadius, _guideColor); + args.WorldHandle.DrawCircle(worldPosition + gridRotation.RotateVec(new Vector2(multi * GuideOffset, GuideOffset)), GuideRadius, _guideColor); + args.WorldHandle.DrawCircle(worldPosition - gridRotation.RotateVec(new Vector2(multi * GuideOffset, GuideOffset)), GuideRadius, _guideColor); + } + + base.Render(args); + } + + public override void AlignPlacementMode(ScreenCoordinates mouseScreen) + { + _mouseCoordsRaw = ScreenToCursorGrid(mouseScreen); + MouseCoords = _mouseCoordsRaw.AlignWithClosestGridTile(SearchBoxSize, _entityManager, _mapManager); + + var gridId = _transformSystem.GetGrid(MouseCoords); + + if (!_entityManager.TryGetComponent(gridId, out var mapGrid)) + return; + + CurrentTile = _mapSystem.GetTileRef(gridId.Value, mapGrid, MouseCoords); + + float tileSize = mapGrid.TileSize; + GridDistancing = tileSize; + + if (pManager.CurrentPermission!.IsTile) + { + MouseCoords = new EntityCoordinates(MouseCoords.EntityId, new Vector2(CurrentTile.X + tileSize / 2, + CurrentTile.Y + tileSize / 2)); + } + else + { + MouseCoords = new EntityCoordinates(MouseCoords.EntityId, new Vector2(CurrentTile.X + tileSize / 2 + pManager.PlacementOffset.X, + CurrentTile.Y + tileSize / 2 + pManager.PlacementOffset.Y)); + } + + var player = _playerManager.LocalSession?.AttachedEntity; + if (player == null) + return; + + if (!_handsSystem.TryGetActiveItem(player.Value, out var heldEntity)) + return; + + if (!_entityManager.TryGetComponent(heldEntity, out var rcd) || !rcd.IsRpd) + return; + + if (!_entityManager.TryGetComponent(player.Value, out var playerXform)) + return; + + if (!_transformSystem.InRange(playerXform.Coordinates, MouseCoords, SharedInteractionSystem.InteractionRange)) + return; + + var mouseCoordsDiff = _mouseCoordsRaw.Position - MouseCoords.Position; + var newLayer = AtmosPipeLayer.Primary; // fallback + + // Get held RCD to check CurrentMode + switch (rcd.CurrentMode) + { + case RCDComponent.RpdMode.Primary: + newLayer = AtmosPipeLayer.Primary; + break; + + case RCDComponent.RpdMode.Secondary: + newLayer = AtmosPipeLayer.Secondary; + break; + + case RCDComponent.RpdMode.Tertiary: + newLayer = AtmosPipeLayer.Tertiary; + break; + + case RCDComponent.RpdMode.Free: + // Only in Free mode do we use mouse direction + if (mouseCoordsDiff.Length() > MouseDeadzoneRadius) + { + var gridRotation = _transformSystem.GetWorldRotation(gridId.Value); + var rawAngle = new Angle(mouseCoordsDiff); + var eyeRotation = _eyeManager.CurrentEye.Rotation; + var direction = (rawAngle + eyeRotation + gridRotation + Math.PI / 2).GetCardinalDir(); + newLayer = (direction == Direction.North || direction == Direction.East) + ? AtmosPipeLayer.Secondary + : AtmosPipeLayer.Tertiary; + } + break; + } + + // Update layer if changed + if (newLayer != _currentLayer) + _currentLayer = newLayer; + + if (rcd.CurrentMode == RCDComponent.RpdMode.Free) + { + UpdateSelectedLayer(heldEntity.Value, _currentLayer); + } + + UpdatePlacer(_currentLayer); + } + + // Why this replaced UpdateEyeRotation: + // - Free-mode preview computes an explicit layer choice on the client from cursor position. + // - The old approach only synced camera/eye rotation and asked the server to recompute the layer. + // + // What this does instead: + // - Whenever the locally selected free-mode layer changes (or held RPD/RPLD changes), + // sends that exact layer as RPDSelectedLayerEvent. + // - Server stores it on the held RCDComponent (LastSelectedLayer) + // and uses it directly during placement in Free mode. + private void UpdateSelectedLayer(EntityUid heldEntity, AtmosPipeLayer layer) + { + if (_lastLayerSyncEntity != heldEntity || _lastLayerSynced != layer) + { + _lastLayerSyncEntity = heldEntity; + _lastLayerSynced = layer; + _entityNetwork.SendSystemNetworkMessage(new RPDSelectedLayerEvent(_entityManager.GetNetEntity(heldEntity), (byte) layer)); + } + } + + private void UpdatePlacer(AtmosPipeLayer layer) + { + // Try to get alternative prototypes from the entity atmos pipe layer component + if (pManager.CurrentPermission?.EntityType == null) + return; + + if (!_protoManager.TryIndex(pManager.CurrentPermission.EntityType, out var currentProto)) + return; + + if (!currentProto.TryGetComponent(out var atmosPipeLayers, _entityManager.ComponentFactory)) + return; + + if (!_pipeLayersSystem.TryGetAlternativePrototype(atmosPipeLayers, layer, out var newProtoId)) + return; + + if (_protoManager.TryIndex(newProtoId, out var newProto)) + { + // Update the placed prototype + pManager.CurrentPermission.EntityType = newProtoId; + + // Update the appearance of the ghost sprite + if (newProto.TryGetComponent(out var sprite, _entityManager.ComponentFactory)) + { + var textures = new List(); + + foreach (var spriteLayer in sprite.AllLayers) + { + if (spriteLayer.ActualRsi?.Path != null && spriteLayer.RsiState.Name != null) + textures.Add(_spriteSystem.RsiStateLike(new SpriteSpecifier.Rsi(spriteLayer.ActualRsi.Path, spriteLayer.RsiState.Name))); + } + + pManager.CurrentTextures = textures; + } + } + } + + public override bool IsValidPosition(EntityCoordinates position) + { + var player = _playerManager.LocalSession?.AttachedEntity; + + // If the destination is out of interaction range, set the placer alpha to zero + if (!_entityManager.TryGetComponent(player, out var xform)) + return false; + + if (!_transformSystem.InRange(xform.Coordinates, position, SharedInteractionSystem.InteractionRange)) + { + InvalidPlaceColor = InvalidPlaceColor.WithAlpha(0); + return false; + } + + // Otherwise restore the alpha value + else + { + InvalidPlaceColor = InvalidPlaceColor.WithAlpha(PlaceColorBaseAlpha); + } + + // Determine if player is carrying an RCD in their active hand + if (!_handsSystem.TryGetActiveItem(player.Value, out var heldEntity)) + return false; + + if (!_entityManager.TryGetComponent(heldEntity, out var rcd)) + return false; + + var gridUid = _transformSystem.GetGrid(position); + if (!_entityManager.TryGetComponent(gridUid, out var mapGrid)) + return false; + var tile = _mapSystem.GetTileRef(gridUid.Value, mapGrid, position); + var posVector = _mapSystem.TileIndicesFor(gridUid.Value, mapGrid, position); + + // Determine if the user is hovering over a target + var currentState = _stateManager.CurrentState; + + if (currentState is not GameplayStateBase screen) + return false; + + var target = screen.GetClickedEntity(_transformSystem.ToMapCoordinates(_mouseCoordsRaw)); + + // Determine if the RCD operation is valid or not + if (!_rcdSystem.IsRCDOperationStillValid(heldEntity.Value, rcd, gridUid.Value, mapGrid, tile, posVector, target, player.Value, false)) + return false; + + return true; + } +} diff --git a/Content.Client/_Starlight/RCD/Systems/RPDSystem.cs b/Content.Client/_Starlight/RCD/Systems/RPDSystem.cs new file mode 100644 index 00000000000..82959b9d866 --- /dev/null +++ b/Content.Client/_Starlight/RCD/Systems/RPDSystem.cs @@ -0,0 +1,60 @@ +using Content.Client.Items; +using Content.Client.Message; +using Content.Shared.RCD.Components; +using Content.Shared.RCD.Systems; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Client._Starlight.RCD.Systems; + +public sealed class RPDSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + Subs.ItemStatus(OnItemStatus); + } + + private Control OnItemStatus(Entity entity) + { + return new RPDModeStatusControl(entity); + } + + private sealed class RPDModeStatusControl : Control + { + private readonly RichTextLabel _label = new() + { + StyleClasses = { "ItemStatus" } + }; + + private readonly EntityUid _uid; + private readonly bool _isRpd; + private readonly RCDSystem _rcdSystem; + + public RPDModeStatusControl(Entity entity) + { + _uid = entity.Owner; + _isRpd = entity.Comp.IsRpd; + _rcdSystem = EntitySystem.Get(); + AddChild(_label); + } + + protected override void FrameUpdate(FrameEventArgs args) + { + if (!_isRpd) return; + + base.FrameUpdate(args); + + var currentMode = _rcdSystem.GetCurrentRpdMode(_uid); + + var modeKey = $"rcd-rpd-mode-{currentMode.ToString().ToLowerInvariant()}"; + var modeName = Robust.Shared.Localization.Loc.GetString(modeKey); + + _label.SetMarkup(Robust.Shared.Localization.Loc.GetString("rcd-item-status-mode", + ("mode", $"[color=cyan]{modeName}[/color]"))); + } + } +} diff --git a/Content.Server/Atmos/EntitySystems/AtmosPipeLayersSystem.cs b/Content.Server/Atmos/EntitySystems/AtmosPipeLayersSystem.cs index 924a6695fc8..e3536ecc58e 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosPipeLayersSystem.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosPipeLayersSystem.cs @@ -7,6 +7,8 @@ using Content.Shared.Construction.Components; using Content.Shared.NodeContainer; using Content.Shared.Popups; +using Content.Shared._Starlight.Atmos.EntitySystems; +using Content.Shared._Starlight.Atmos.Components; namespace Content.Server.Atmos.EntitySystems; @@ -65,7 +67,7 @@ public override void SetPipeLayer(Entity ent, AtmosPip // Unanchor the pipe if its new layer overlaps with another pipe var xform = Transform(ent); - if (!HasComp(ent) || !_pipeRestrictOverlap.CheckOverlap((ent, nodeContainer, xform))) + if (!HasComp(ent) || !_pipeRestrictOverlap.CheckOverlap((ent, nodeContainer, xform))) return; RaiseLocalEvent(ent, new BeforeUnanchoredEvent(user.Value, used.Value)); diff --git a/Content.Server/NodeContainer/Nodes/PipeNode.cs b/Content.Server/NodeContainer/Nodes/PipeNode.cs index 3a76666a2c0..000ada928bc 100644 --- a/Content.Server/NodeContainer/Nodes/PipeNode.cs +++ b/Content.Server/NodeContainer/Nodes/PipeNode.cs @@ -5,6 +5,7 @@ using Content.Shared.NodeContainer; using Robust.Shared.Map.Components; using Robust.Shared.Utility; +using Content.Shared._Starlight.Atmos; // Starlight namespace Content.Server.NodeContainer.Nodes { @@ -14,7 +15,7 @@ namespace Content.Server.NodeContainer.Nodes /// [DataDefinition] [Virtual] - public partial class PipeNode : Node, IGasMixtureHolder, IRotatableNode + public partial class PipeNode : Node, IGasMixtureHolder, IRotatableNode, IPipeNode // Starlight Edit: Added IPipeNode { /// /// The directions in which this pipe can connect to other pipes around it. @@ -236,5 +237,9 @@ protected IEnumerable PipesInDirection(Vector2i pos, PipeDirection pip } } } + // Starlight Start: RPD + PipeDirection IPipeNode.Direction => OriginalPipeDirection; + AtmosPipeLayer IPipeNode.Layer => CurrentPipeLayer; + // Starlight End: RPD } } diff --git a/Content.Shared/RCD/Components/RCDComponent.cs b/Content.Shared/RCD/Components/RCDComponent.cs index 1ea31665310..6a4d79c0893 100644 --- a/Content.Shared/RCD/Components/RCDComponent.cs +++ b/Content.Shared/RCD/Components/RCDComponent.cs @@ -3,6 +3,8 @@ using Robust.Shared.GameStates; using Robust.Shared.Physics; using Robust.Shared.Prototypes; +using Content.Shared.Atmos.Components; // Starlight-edit: RPD layered placement support +using Robust.Shared.Serialization; // Starlight namespace Content.Shared.RCD.Components; @@ -33,6 +35,29 @@ public sealed partial class RCDComponent : Component [DataField, AutoNetworkedField] public ProtoId ProtoId { get; set; } = "Invalid"; + // Starlight Start + /// + /// A cached copy of currently selected RCD prototype + /// + /// + /// If the ProtoId is changed, make sure to update the CachedPrototype as well + /// + [ViewVariables(VVAccess.ReadOnly)] + public RCDPrototype CachedPrototype { get; set; } = default!; + + /// + /// Indicates if a mirrored version of the construction prototype should be used (if available) + /// + [AutoNetworkedField, ViewVariables(VVAccess.ReadOnly)] + public bool UseMirrorPrototype = false; + + /// + /// Indicates whether this is an RCD or an RPD + /// + [DataField, AutoNetworkedField] + public bool IsRpd { get; set; } = false; + // Starlight End + /// /// The direction constructed entities will face upon spawning /// @@ -57,4 +82,31 @@ public Direction ConstructionDirection /// [ViewVariables(VVAccess.ReadOnly)] public Transform ConstructionTransform { get; private set; } + + // Starlight Start + /// + /// Last free-mode layer selected on the client. + /// Used by the server as the authoritative layer when placing layered pipes in Free mode. + /// + [DataField] + public AtmosPipeLayer? LastSelectedLayer { get; set; } = null; + + /// + /// Current pipe layer / build mode for RPD + /// + [DataField, AutoNetworkedField] + public RpdMode CurrentMode { get; set; } = RpdMode.Free; + + [DataField] + public SoundSpecifier SoundSwitchMode { get; set; } = new SoundPathSpecifier("/Audio/Machines/quickbeep.ogg"); + + [Serializable, NetSerializable] + public enum RpdMode : byte + { + Primary = 0, + Secondary = 1, + Tertiary = 2, + Free = 3, + // Starlight End + } } diff --git a/Content.Shared/RCD/Components/RCDDeconstructableComponent.cs b/Content.Shared/RCD/Components/RCDDeconstructableComponent.cs index 0ddc6897f05..77a49d984b1 100644 --- a/Content.Shared/RCD/Components/RCDDeconstructableComponent.cs +++ b/Content.Shared/RCD/Components/RCDDeconstructableComponent.cs @@ -15,7 +15,7 @@ public sealed partial class RCDDeconstructableComponent : Component public int Cost = 1; /// - /// The length of the deconstruction + /// The length of the deconstruction /// [DataField, ViewVariables(VVAccess.ReadWrite)] public float Delay = 1f; @@ -31,4 +31,12 @@ public sealed partial class RCDDeconstructableComponent : Component /// [DataField, ViewVariables(VVAccess.ReadWrite)] public bool Deconstructable = true; + + // Starlight Start: RPD + /// + /// Toggles whether this entity is deconstructable by the RPD or not + /// + [DataField("rpd"), ViewVariables(VVAccess.ReadWrite)] + public bool RpdDeconstructable = false; + // Starlight End: RPD } diff --git a/Content.Shared/RCD/RCDEvents.cs b/Content.Shared/RCD/RCDEvents.cs index 6871ec178ee..0a287cc7b57 100644 --- a/Content.Shared/RCD/RCDEvents.cs +++ b/Content.Shared/RCD/RCDEvents.cs @@ -16,6 +16,33 @@ public sealed class RCDConstructionGhostRotationEvent(NetEntity netEntity, Direc public readonly Direction Direction = direction; } +// Starlight Start: RPD +[Serializable, NetSerializable] +public sealed class RCDConstructionGhostFlipEvent : EntityEventArgs +{ + public readonly NetEntity NetEntity; + public readonly bool UseMirrorPrototype; + public RCDConstructionGhostFlipEvent(NetEntity netEntity, bool useMirrorPrototype) + { + NetEntity = netEntity; + UseMirrorPrototype = useMirrorPrototype; + } +} + +[Serializable, NetSerializable] +public sealed class RPDSelectedLayerEvent : EntityEventArgs +{ + public readonly NetEntity NetEntity; + public readonly byte Layer; + + public RPDSelectedLayerEvent(NetEntity netEntity, byte layer) + { + NetEntity = netEntity; + Layer = layer; + } +} +// Starlight End: RPD + [Serializable, NetSerializable] public enum RcdUiKey : byte { diff --git a/Content.Shared/RCD/RCDPrototype.cs b/Content.Shared/RCD/RCDPrototype.cs index c4ac7148f7a..0013e698939 100644 --- a/Content.Shared/RCD/RCDPrototype.cs +++ b/Content.Shared/RCD/RCDPrototype.cs @@ -44,6 +44,14 @@ public sealed partial class RCDPrototype : IPrototype [DataField, ViewVariables(VVAccess.ReadOnly)] public string? Prototype { get; private set; } + // Starlight Start: RPD + /// + /// If the entity can be flipped, this prototype is available as an alternate (mode dependent) + /// + [DataField, ViewVariables(VVAccess.ReadOnly)] + public string? MirrorPrototype { get; private set; } = string.Empty; + // Starlight End: RPD + /// /// If true, allows placing the entity once per direction (North, West, South and East) /// @@ -119,6 +127,14 @@ private set /// [DataField, ViewVariables(VVAccess.ReadOnly)] public RcdRotation Rotation { get; private set; } = RcdRotation.User; + + // Starlight Start: RPD + /// + /// Determines whether this prototype uses layered placement (true for traditional placement, false for layered). Only applies to RPD. + /// + [DataField, ViewVariables(VVAccess.ReadOnly)] + public bool HasLayers { get; private set; } = false; + // Starlight End: RPD } public enum RcdMode : byte diff --git a/Content.Shared/RCD/Systems/RCDSystem.cs b/Content.Shared/RCD/Systems/RCDSystem.cs index 2f1f058a1b7..6736bde6f3a 100644 --- a/Content.Shared/RCD/Systems/RCDSystem.cs +++ b/Content.Shared/RCD/Systems/RCDSystem.cs @@ -22,6 +22,18 @@ using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using System.Linq; +// Starlight Start +using Content.Shared.Atmos.EntitySystems; +using Content.Shared.Atmos.Components; +using Content.Shared._Starlight.Atmos.EntitySystems; +using Content.Shared.Hands.Components; +using System.Numerics; +using Content.Shared.Verbs; +using Robust.Shared.Utility; +using Content.Shared.NodeContainer; +using Content.Shared.Atmos; +using Content.Shared._Starlight.Atmos; +// Starlight End namespace Content.Shared.RCD.Systems; @@ -44,6 +56,11 @@ public sealed class RCDSystem : EntitySystem [Dependency] private readonly SharedMapSystem _mapSystem = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly TagSystem _tags = default!; + // Starlight Start + [Dependency] private readonly SharedAtmosPipeLayersSystem _pipeLayersSystem = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly PipeRestrictOverlapSystem _pipeOverlap = default!; + // Starlight End private readonly int _instantConstructionDelay = 0; private readonly EntProtoId _instantConstructionFx = "EffectRCDConstruct0"; @@ -64,6 +81,13 @@ public override void Initialize() SubscribeLocalEvent>(OnDoAfterAttempt); SubscribeLocalEvent(OnRCDSystemMessage); SubscribeNetworkEvent(OnRCDconstructionGhostRotationEvent); + // Starlight Start + SubscribeLocalEvent(OnStartup); + SubscribeNetworkEvent(OnRCDConstructionGhostFlipEvent); + SubscribeNetworkEvent(OnRPDSelectedLayerEvent); + SubscribeLocalEvent>(OnGetUtilityVerb); + SubscribeLocalEvent>(OnGetAlternativeVerb); + // Starlight End } #region Event handling @@ -73,7 +97,12 @@ private void OnMapInit(EntityUid uid, RCDComponent component, MapInitEvent args) // On init, set the RCD to its first available recipe if (component.AvailablePrototypes.Count > 0) { - component.ProtoId = component.AvailablePrototypes.ElementAt(0); + // Starlight edit Start: RPD + if (component.IsRpd) + component.ProtoId = "PipeStraight"; + else + component.ProtoId = component.AvailablePrototypes.ElementAt(0); + // Starlight edit End: RPD Dirty(uid, component); return; @@ -83,6 +112,16 @@ private void OnMapInit(EntityUid uid, RCDComponent component, MapInitEvent args) QueueDel(uid); } + // Starlight Start: RPD + private void OnStartup(EntityUid uid, RCDComponent component, ComponentStartup args) + { + UpdateCachedPrototype(uid, component); + Dirty(uid, component); + + return; + } + // Starlight End: RPD + private void OnRCDSystemMessage(EntityUid uid, RCDComponent component, RCDSystemMessage args) { // Exit if the RCD doesn't actually know the supplied prototype @@ -105,7 +144,12 @@ private void OnExamine(EntityUid uid, RCDComponent component, ExaminedEvent args if (!args.IsInDetailsRange) return; - var prototype = _protoManager.Index(component.ProtoId); + // Starlight edit Start + UpdateCachedPrototype(uid, component); + var prototype = component.CachedPrototype; + // Starlight edit End + + //var prototype = _protoManager.Index(component.ProtoId); var msg = Loc.GetString("rcd-component-examine-mode-details", ("mode", Loc.GetString(prototype.SetName))); @@ -121,6 +165,70 @@ private void OnExamine(EntityUid uid, RCDComponent component, ExaminedEvent args } args.PushMarkup(msg); + + // Starlight Start + if (component.IsRpd) + { + var modeLoc = $"rcd-rpd-mode-{component.CurrentMode.ToString().ToLowerInvariant()}"; + args.PushMarkup(Loc.GetString("rcd-component-examine-rpd-mode", ("mode", Loc.GetString(modeLoc)))); + } + } + + private void OnRPDSelectedLayerEvent(RPDSelectedLayerEvent ev, EntitySessionEventArgs session) + { + var uid = GetEntity(ev.NetEntity); + + if (session.SenderSession.AttachedEntity is not { } player) + return; + + if (_hands.GetActiveItem(player) != uid) + return; + + if (!TryComp(uid, out var rcd)) + return; + + var layerInt = Math.Clamp(ev.Layer, (byte) AtmosPipeLayer.Primary, (byte) AtmosPipeLayer.Tertiary); + var selectedLayer = (AtmosPipeLayer) layerInt; + + + rcd.LastSelectedLayer = selectedLayer; + } + + private void OnGetUtilityVerb(EntityUid uid, RCDComponent component, GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract || !component.IsRpd) + return; + + var verb = new UtilityVerb + { + Act = () => SwitchPipeMode(uid, component, args.User), + Text = Loc.GetString("rcd-verb-switch-mode"), + Icon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/settings.svg.192dpi.png")), + Impact = LogImpact.Low + }; + + args.Verbs.Add(verb); + } + + private void OnGetAlternativeVerb(EntityUid uid, RCDComponent component, GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract || !component.IsRpd || !args.Using.HasValue) + return; + + // Only show when alt-clicking the RPD itself (args.Using is the held item) + if (args.Using.Value != uid) + return; + + var verb = new AlternativeVerb + { + Act = () => SwitchPipeMode(uid, component, args.User), + Text = Loc.GetString("rcd-verb-switch-mode"), + Icon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/settings.svg.192dpi.png")), + Impact = LogImpact.Low + }; + + args.Verbs.Add(verb); + // Starlight End } private void OnAfterInteract(EntityUid uid, RCDComponent component, AfterInteractEvent args) @@ -128,9 +236,11 @@ private void OnAfterInteract(EntityUid uid, RCDComponent component, AfterInterac if (args.Handled || !args.CanReach) return; + UpdateCachedPrototype(uid, component); // Starlight Edit: Refresh cached prototype before any interaction time layer logic. + var user = args.User; var location = args.ClickLocation; - var prototype = _protoManager.Index(component.ProtoId); + var prototype = component.CachedPrototype; // Starlight Edit: _protoManager.Index(component.ProtoId) -> component.CachedPrototype // Initial validity checks if (!location.IsValid(EntityManager)) @@ -146,6 +256,37 @@ private void OnAfterInteract(EntityUid uid, RCDComponent component, AfterInterac var tile = _mapSystem.GetTileRef(gridUid.Value, mapGrid, location); var position = _mapSystem.TileIndicesFor(gridUid.Value, mapGrid, location); + // Starlight Start + var placementLayer = AtmosPipeLayer.Primary; + if (component.IsRpd && prototype.HasLayers) + { + placementLayer = AtmosPipeLayer.Primary; + + switch (component.CurrentMode) + { + case RCDComponent.RpdMode.Primary: + placementLayer = AtmosPipeLayer.Primary; + break; + + case RCDComponent.RpdMode.Secondary: + placementLayer = AtmosPipeLayer.Secondary; + break; + + case RCDComponent.RpdMode.Tertiary: + placementLayer = AtmosPipeLayer.Tertiary; + break; + + case RCDComponent.RpdMode.Free: + // Free mode layer is selected client-side and synced explicitly. + if (component.LastSelectedLayer.HasValue) + { + placementLayer = component.LastSelectedLayer.Value; + } + break; + } + } + // Starlight End + if (!IsRCDOperationStillValid(uid, component, gridUid.Value, mapGrid, tile, position, component.ConstructionDirection, args.Target, args.User)) return; @@ -209,7 +350,7 @@ private void OnAfterInteract(EntityUid uid, RCDComponent component, AfterInterac // Try to start the do after var effect = Spawn(effectPrototype, location); - var ev = new RCDDoAfterEvent(GetNetCoordinates(location), component.ConstructionDirection, component.ProtoId, cost, GetNetEntity(effect)); + var ev = new RCDDoAfterEvent(GetNetCoordinates(location), component.ConstructionDirection, placementLayer, component.ProtoId, cost, GetNetEntity(effect)); // Starlight Edit: Include layer as well in snapshot at start so finalize uses consistent placement state. var doAfterArgs = new DoAfterArgs(EntityManager, user, delay, ev, uid, target: args.Target, used: uid) { @@ -288,7 +429,7 @@ private void OnDoAfter(EntityUid uid, RCDComponent component, RCDDoAfterEvent ar return; // Finalize the operation (this should handle prediction properly) - FinalizeRCDOperation(uid, component, gridUid.Value, mapGrid, tile, position, args.Direction, args.Target, args.User); + FinalizeRCDOperation(uid, component, gridUid.Value, mapGrid, tile, position, args.Direction, args.PipeLayer, args.Target, args.User); // Starlight Edit: Include layer from do-after event to avoid finalize time drift. // Play audio and consume charges _audio.PlayPredicted(component.SuccessSound, uid, args.User); @@ -314,6 +455,46 @@ private void OnRCDconstructionGhostRotationEvent(RCDConstructionGhostRotationEve Dirty(uid, rcd); } + // Starlight Start: RPD + private void OnRCDConstructionGhostFlipEvent(RCDConstructionGhostFlipEvent ev, EntitySessionEventArgs session) + { + var uid = GetEntity(ev.NetEntity); + + if (session.SenderSession.AttachedEntity is not { } player) + return; + + if (_hands.GetActiveItem(player) != uid) + return; + + if (!TryComp(uid, out var rcd)) + return; + + rcd.UseMirrorPrototype = ev.UseMirrorPrototype; + Dirty(uid, rcd); + } + + private void SwitchPipeMode(EntityUid uid, RCDComponent component, EntityUid? user = null) + { + if (!component.IsRpd) + return; + + // Cycle through modes + component.CurrentMode = component.CurrentMode switch + { + RCDComponent.RpdMode.Primary => RCDComponent.RpdMode.Secondary, + RCDComponent.RpdMode.Secondary => RCDComponent.RpdMode.Tertiary, + RCDComponent.RpdMode.Tertiary => RCDComponent.RpdMode.Free, + RCDComponent.RpdMode.Free => RCDComponent.RpdMode.Primary, + _ => RCDComponent.RpdMode.Free + }; + + Dirty(uid, component); + + if (user != null) + _audio.PlayPredicted(component.SoundSwitchMode, uid, user.Value); + // Starlight End: RPD + } + #endregion #region Entity construction/deconstruction rule checks @@ -325,7 +506,9 @@ public bool IsRCDOperationStillValid(EntityUid uid, RCDComponent component, Enti 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); + UpdateCachedPrototype(uid, component); // Starlight + + var prototype = component.CachedPrototype; // Starlight Edit: _protoManager.Index(component.ProtoId) -> component.CachedPrototype // Check that the RCD has enough ammo to get the job done var charges = _sharedCharges.GetCurrentCharges(uid); @@ -362,7 +545,7 @@ public bool IsRCDOperationStillValid(EntityUid uid, RCDComponent component, Enti case RcdMode.ConstructObject: return IsConstructionLocationValid(uid, component, gridUid, mapGrid, tile, position, direction, user, popMsgs); case RcdMode.Deconstruct: - return IsDeconstructionStillValid(uid, tile, target, user, popMsgs); + return IsDeconstructionStillValid(uid, component, tile, target, user, popMsgs); // Starlight Edit: Added ``component`` } return false; @@ -370,7 +553,9 @@ public bool IsRCDOperationStillValid(EntityUid uid, RCDComponent component, Enti 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); + UpdateCachedPrototype(uid, component); // Starlight + + var prototype = component.CachedPrototype; // Starlight Edit: _protoManager.Index(component.ProtoId) -> component.CachedPrototype // Check rule: Must build on empty tile if (prototype.ConstructionRules.Contains(RcdConstructionRule.MustBuildOnEmptyTile) && !tile.Tile.IsEmpty) @@ -509,11 +694,20 @@ private bool IsConstructionLocationValid(EntityUid uid, RCDComponent component, return true; } - private bool IsDeconstructionStillValid(EntityUid uid, TileRef tile, EntityUid? target, EntityUid user, bool popMsgs = true) + private bool IsDeconstructionStillValid(EntityUid uid, RCDComponent component, TileRef tile, EntityUid? target, EntityUid user, bool popMsgs = true) // Starlight Edit: Added ``RCDComponent component`` { // Attempt to deconstruct a floor tile if (target == null) { + // Starlight Start: RPD + if (component.IsRpd) + { + if (popMsgs) + _popup.PopupClient(Loc.GetString("rcd-component-deconstruct-target-not-on-whitelist-message"), uid, user); + + return false; + } + // Starlight End: RPD // The tile is empty if (tile.Tile.IsEmpty) { @@ -547,8 +741,21 @@ private bool IsDeconstructionStillValid(EntityUid uid, TileRef tile, EntityUid? // Attempt to deconstruct an object else { + // Starlight Start: RPD + // The object is not in the RPD whitelist + if (!TryComp(target, out var deconstructible) || !deconstructible.RpdDeconstructable && component.IsRpd) + { + if (popMsgs) + _popup.PopupClient(Loc.GetString("rcd-component-deconstruct-target-not-on-whitelist-message"), uid, user); + + return false; + } + // Starlight End: RPD + // The object is not in the whitelist - if (!TryComp(target, out var deconstructible) || !deconstructible.Deconstructable) + if (!deconstructible.Deconstructable) // Starlight Edit: RPD - Removed ``TryComp(target, out var deconstructible) || !`` + // The object is not in the whitelist + if (!deconstructible.Deconstructable) // Starlight Edit: RPD - Removed ``TryComp(target, out var deconstructible) || !`` { if (popMsgs) _popup.PopupClient(Loc.GetString("rcd-component-deconstruct-target-not-on-whitelist-message"), uid, user); @@ -564,7 +771,7 @@ private bool IsDeconstructionStillValid(EntityUid uid, TileRef tile, EntityUid? #region Entity construction/deconstruction - private void FinalizeRCDOperation(EntityUid uid, RCDComponent component, EntityUid gridUid, MapGridComponent mapGrid, TileRef tile, Vector2i position, Direction direction, EntityUid? target, EntityUid user) + private void FinalizeRCDOperation(EntityUid uid, RCDComponent component, EntityUid gridUid, MapGridComponent mapGrid, TileRef tile, Vector2i position, Direction direction, AtmosPipeLayer pipeLayer, EntityUid? target, EntityUid user) { if (!_net.IsServer) return; @@ -585,7 +792,61 @@ private void FinalizeRCDOperation(EntityUid uid, RCDComponent component, EntityU break; case RcdMode.ConstructObject: - var ent = Spawn(prototype.Prototype, _mapSystem.GridTileToLocal(gridUid, mapGrid, position)); + // Starlight edit Start: RPD + var proto = (component.UseMirrorPrototype && !string.IsNullOrEmpty(prototype.MirrorPrototype)) + ? prototype.MirrorPrototype + : prototype.Prototype; + + if (component.IsRpd && prototype.HasLayers) + { + if (_protoManager.TryIndex(proto, out var entityProto) && + entityProto.TryGetComponent(out var atmosPipeLayers, _entityManager.ComponentFactory) && + _pipeLayersSystem.TryGetAlternativePrototype(atmosPipeLayers, pipeLayer, out var newProtoId)) + { + proto = newProtoId; + } + } + + // Calculate rotation before spawn + var rotation = GetConstructionRotation(uid, prototype, direction); + + // For RPD's, if overlapping existing pipe, replace the pipe + if (component.IsRpd) + { + // We need to know what the pipe *would* look like to check for overlaps + if (_protoManager.TryIndex(proto, out var pipeProto) && + pipeProto.TryGetComponent(out var nodeContainer, _entityManager.ComponentFactory)) + { + // Check every node in the prototype to see if it overlaps something on the grid + foreach (var node in nodeContainer.Nodes.Values) + { + if (node is IPipeNode pipeNode) + { + var proposed = new PipeRestrictOverlapSystem.ProposedPipe( + pipeNode.Direction, + pipeLayer, + rotation + ); + + // If there is a conflict, delete the old pipe first + var conflict = _pipeOverlap.CheckIfWouldConflict(gridUid, position, proposed); + if (Exists(conflict) && HasComp(conflict)) + { + _adminLogger.Add(LogType.RCD, LogImpact.Medium, + $"{ToPrettyString(user):user} RPD replaced {ToPrettyString(conflict.Value)} at {position}"); + Del(conflict.Value); + _audio.PlayPvs(component.SuccessSound, uid); + } + } + } + } + } + + var entityCoords = _mapSystem.GridTileToLocal(gridUid, mapGrid, position); + var mapCoords = new MapCoordinates(entityCoords.ToMapPos(EntityManager, _transform), entityCoords.GetMapId(EntityManager)); + + var ent = Spawn(proto, mapCoords, rotation: rotation); + // Starlight edit End: RPD switch (prototype.Rotation) { @@ -634,6 +895,38 @@ private bool DoesCustomBoundsIntersectWithFixture(PolygonShape boundingPolygon, return boundingPolygon.ComputeAABB(boundingTransform, 0).Intersects(fixture.Shape.ComputeAABB(entXform, 0)); } + // Starlight Start: RPD + // Break out GetConstructionRotation into its own helper method since it's used in multiple places and the logic is a bit more complex with the addition of RPD/RPLD rotation options. + private Angle GetConstructionRotation(EntityUid rcdUid, RCDPrototype prototype, Direction direction) + { + return prototype.Rotation switch + { + RcdRotation.Fixed => Angle.Zero, + RcdRotation.Camera => Transform(rcdUid).LocalRotation, + RcdRotation.User => direction.ToAngle(), + _ => Angle.Zero + }; + } + + public void UpdateCachedPrototype(EntityUid uid, RCDComponent component) + { + if (component.ProtoId.Id != component.CachedPrototype?.Prototype || + (component.CachedPrototype?.MirrorPrototype != null && + component.ProtoId.Id != component.CachedPrototype?.MirrorPrototype)) + { + component.CachedPrototype = _protoManager.Index(component.ProtoId); + } + } + + public RCDComponent.RpdMode GetCurrentRpdMode(EntityUid uid, RCDComponent? component = null) + { + if (!Resolve(uid, ref component)) + return RCDComponent.RpdMode.Free; // default to Free mode + + return component.CurrentMode; + } + // Starlight End: RPD + #endregion } @@ -646,6 +939,9 @@ public sealed partial class RCDDoAfterEvent : DoAfterEvent [DataField] public Direction Direction { get; private set; } + [DataField] + public AtmosPipeLayer PipeLayer { get; private set; } = AtmosPipeLayer.Primary; // Starlight Edit: Layer snapshot captured at doafter start and replayed on finalize. + [DataField] public ProtoId StartingProtoId { get; private set; } @@ -657,10 +953,11 @@ public sealed partial class RCDDoAfterEvent : DoAfterEvent private RCDDoAfterEvent() { } - public RCDDoAfterEvent(NetCoordinates location, Direction direction, ProtoId startingProtoId, int cost, NetEntity? effect = null) + public RCDDoAfterEvent(NetCoordinates location, Direction direction, AtmosPipeLayer pipeLayer, ProtoId startingProtoId, int cost, NetEntity? effect = null) { Location = location; Direction = direction; + PipeLayer = pipeLayer; // Starlight Edit StartingProtoId = startingProtoId; Cost = cost; Effect = effect; diff --git a/Content.Shared/_Starlight/Atmos/Components/PipeRestrictOverlapComponent.cs b/Content.Shared/_Starlight/Atmos/Components/PipeRestrictOverlapComponent.cs new file mode 100644 index 00000000000..c7da49336e5 --- /dev/null +++ b/Content.Shared/_Starlight/Atmos/Components/PipeRestrictOverlapComponent.cs @@ -0,0 +1,9 @@ +using Content.Shared._Starlight.Atmos.EntitySystems; + +namespace Content.Shared._Starlight.Atmos.Components; + +/// +/// This is used for restricting anchoring pipes so that they do not overlap. +/// +[RegisterComponent, Access(typeof(PipeRestrictOverlapSystem))] +public sealed partial class PipeRestrictOverlapRPDComponent : Component; diff --git a/Content.Shared/_Starlight/Atmos/EntitiySystems/PipeRestrictOverlapSystem.cs b/Content.Shared/_Starlight/Atmos/EntitiySystems/PipeRestrictOverlapSystem.cs new file mode 100644 index 00000000000..21f3c3d1edd --- /dev/null +++ b/Content.Shared/_Starlight/Atmos/EntitiySystems/PipeRestrictOverlapSystem.cs @@ -0,0 +1,183 @@ +using System.Linq; +using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; +using Content.Shared._Starlight.Atmos; +using Content.Shared._Starlight.Atmos.Components; +using Content.Shared.NodeContainer; +using Content.Shared.Popups; +using Content.Shared.Construction.Components; +using JetBrains.Annotations; +using Robust.Shared.Map.Components; + +namespace Content.Shared._Starlight.Atmos.EntitySystems; + +/// +/// This handles restricting pipe-based entities from overlapping outlets/inlets with other entities. +/// +public sealed class PipeRestrictOverlapSystem : EntitySystem +{ + [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedTransformSystem _xform = default!; + + private readonly List _anchoredEntities = new(); + private EntityQuery _nodeContainerQuery; + + public readonly record struct ProposedPipe( + + PipeDirection Direction, + + AtmosPipeLayer Layer, + + Angle Rotation = default); + + /// + 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 entDirsAndLayers = GetAllDirectionsAndLayers(ent).ToList(); + var otherDirsAndLayers = GetAllDirectionsAndLayers(other).ToList(); + + foreach (var (dir, layer) in entDirsAndLayers) + { + foreach (var (otherDir, otherLayer) in otherDirsAndLayers) + { + if ((dir & otherDir) != 0 && layer == otherLayer) + return true; + } + } + + return false; + + IEnumerable<(PipeDirection, AtmosPipeLayer)> GetAllDirectionsAndLayers(Entity pipe) + { + foreach (var node in pipe.Comp1.Nodes.Values) + { + if (node is IPipeNode pipeNode) + yield return (pipeNode.Direction.RotatePipeDirection(pipe.Comp2.LocalRotation), pipeNode.Layer); + } + } + } + + /// + /// Checks if placing a new pipe with the given direction and layer on the specified tile would conflict + /// with any existing anchored pipe on the same tile, same layer and overlapping direction. + /// Returns the EntityUid of the first conflicting pipe found, or null if no conflict. + /// + public EntityUid? CheckIfWouldConflict(EntityUid gridUid, + Vector2i tileIndices, + ProposedPipe proposed, + EntityUid? ignoreEntity = null) + { + if (!TryComp(gridUid, out var gridComp)) + return null; + + // Pre-calculate the absolute direction of the proposed pipe + var proposedDirAbs = proposed.Direction.RotatePipeDirection(proposed.Rotation); + + _anchoredEntities.Clear(); + _map.GetAnchoredEntities((gridUid, gridComp), tileIndices, _anchoredEntities); + + foreach (var otherEnt in _anchoredEntities) + { + if (otherEnt == ignoreEntity) + continue; + + if (!_nodeContainerQuery.TryComp(otherEnt, out var otherNodeComp)) + continue; + + var otherXform = Transform(otherEnt); + + // Compare against the existing pipe's actual rotated nodes + foreach (var (existingDir, existingLayer) in GetPipeNodeData((otherEnt, otherNodeComp, otherXform))) + { + // Conflict occurs if they share a layer AND any directional bit + if (proposed.Layer == existingLayer && (proposedDirAbs & existingDir) != 0) + return otherEnt; + } + } + + return null; + } + + private static IEnumerable<(PipeDirection RotatedDirection, AtmosPipeLayer Layer)> GetPipeNodeData( + Entity pipe) + { + var rotation = pipe.Comp2.LocalRotation; + + foreach (var node in pipe.Comp1.Nodes.Values) + { + if (node is IPipeNode pipeNode) + yield return (pipeNode.Direction.RotatePipeDirection(rotation), pipeNode.Layer); + } + } +} diff --git a/Content.Shared/_Starlight/Atmos/IPipeNode.cs b/Content.Shared/_Starlight/Atmos/IPipeNode.cs new file mode 100644 index 00000000000..5e990aa35a4 --- /dev/null +++ b/Content.Shared/_Starlight/Atmos/IPipeNode.cs @@ -0,0 +1,10 @@ +using Content.Shared.Atmos.Components; +using Content.Shared.Atmos; + +namespace Content.Shared._Starlight.Atmos; + +public interface IPipeNode +{ + PipeDirection Direction { get; } + AtmosPipeLayer Layer { get; } +} diff --git a/Resources/Locale/en-US/_Starlight/rpd/rpd-components.ftl b/Resources/Locale/en-US/_Starlight/rpd/rpd-components.ftl new file mode 100644 index 00000000000..f16798772b1 --- /dev/null +++ b/Resources/Locale/en-US/_Starlight/rpd/rpd-components.ftl @@ -0,0 +1,12 @@ +rpd-component-piping = Piping +rpd-component-atmosphericutility = Atmospheric Utility +rpd-component-pumps = Pumps & Valves +rpd-component-vents = Vents +rpd-component-sensors-monitors = Sensors & Monitors +rcd-rpd-mode-primary = Primary +rcd-rpd-mode-secondary = Secondary +rcd-rpd-mode-tertiary = Tertiary +rcd-rpd-mode-free = Free +rcd-component-examine-rpd-mode = Current mode: [color=cyan]{$mode}[/color] +rcd-verb-switch-mode = Switch mode +rcd-item-status-mode = Mode: {$mode} diff --git a/Resources/Prototypes/Datasets/ion_storm.yml b/Resources/Prototypes/Datasets/ion_storm.yml index a305ab47b11..83f0969f9ff 100644 --- a/Resources/Prototypes/Datasets/ion_storm.yml +++ b/Resources/Prototypes/Datasets/ion_storm.yml @@ -716,6 +716,7 @@ - RACKS - RADIOS - RCDS + - RPDS # Starlight: RPD - REFRIGERATORS - REINFORCED WALLS - ROBOTS diff --git a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/pipes.yml b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/pipes.yml index 02795081a79..12099587fa4 100644 --- a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/pipes.yml +++ b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/pipes.yml @@ -68,6 +68,13 @@ bodyType: static - type: StaticPrice price: 30 + # Starlight Start: RPD + - type: RCDDeconstructable + cost: 1 + delay: 2 + fx: EffectRCDDeconstruct2 + rpd: true + # Starlight End - type: entity abstract: true diff --git a/Resources/Prototypes/Entities/Structures/Specific/Atmospherics/sensor.yml b/Resources/Prototypes/Entities/Structures/Specific/Atmospherics/sensor.yml index 9a5cbbce152..4aa28cd869b 100644 --- a/Resources/Prototypes/Entities/Structures/Specific/Atmospherics/sensor.yml +++ b/Resources/Prototypes/Entities/Structures/Specific/Atmospherics/sensor.yml @@ -90,6 +90,13 @@ tags: - AirSensor - ForceFixRotations + # Starlight Start: RPD + - type: RCDDeconstructable + cost: 2 + delay: 2 + fx: EffectRCDDeconstruct2 + rpd: true + # Starlight End: RPD - type: entity parent: BaseItem diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/WallmountMachines/air_alarm.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/WallmountMachines/air_alarm.yml index 533d592641a..036fdd63426 100644 --- a/Resources/Prototypes/Entities/Structures/Wallmounts/WallmountMachines/air_alarm.yml +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/WallmountMachines/air_alarm.yml @@ -109,6 +109,13 @@ guides: - AirAlarms - DeviceMonitoringAndControl + # Starlight Start: RPD + - type: RCDDeconstructable + cost: 3 + delay: 2 + fx: EffectRCDDeconstruct2 + rpd: true + # Starlight End: RPD - type: entity parent: BaseWallmountMetallic diff --git a/Resources/Prototypes/_Starlight/Entities/Objects/Tools/tools.yml b/Resources/Prototypes/_Starlight/Entities/Objects/Tools/tools.yml index ca0f866e93d..645c382e5de 100644 --- a/Resources/Prototypes/_Starlight/Entities/Objects/Tools/tools.yml +++ b/Resources/Prototypes/_Starlight/Entities/Objects/Tools/tools.yml @@ -712,3 +712,74 @@ - PlantSampleTaker - Wirecutter - Screwdriver + +- type: entity + id: RPD + parent: RCD + name: RPD + description: A device used to rapidly pipe things. + components: + - type: RCD + isRpd: true + availablePrototypes: + - PipeFourway + - PipeStraight + - PipeBend + - PipeTJunction + - OutletInjector + - ManualValve + - VolumetricPump + - PressurePump + - VentScrubber + - PressureValve + - DualPortVent + - VentGas + - VentPassive + - MixerGas + - Radiator + - RadiatorBend + - SignalValve + - CanisterPort + - FilterGas + - PipeHalf + - PipeManifold + - PressureRegulator + - PassiveGate + - PipeSensor + - Deconstruct + - AirAlarm + - AirSensor + - type: LimitedCharges + maxCharges: 90 + - type: Sprite + sprite: _Starlight/Objects/Tools/rpd.rsi + +- type: entity + id: RPDEmpty + parent: RPD + suffix: Empty + components: + - type: LimitedCharges + lastCharges: -1 + +- type: entity + id: RPDRecharging + parent: RPD + name: experimental RPD + description: Cyborg-mounted Rapid Piping Device which creates compressed matter on the fly using an internal fabricator. + suffix: AutoRecharge + components: + - type: LimitedCharges + maxCharges: 20 + - type: AutoRecharge + rechargeDuration: 10 + +- type: entity + id: RPDExperimental + parent: RPD + suffix: Admeme + name: experimental RPD + description: A bluespace-enhanced rapid piping device that passively generates its own compressed matter. + components: + - type: AutoRecharge + rechargeDuration: 1 diff --git a/Resources/Prototypes/_Starlight/RPD/rpd.yml b/Resources/Prototypes/_Starlight/RPD/rpd.yml new file mode 100644 index 00000000000..45f27cb50b5 --- /dev/null +++ b/Resources/Prototypes/_Starlight/RPD/rpd.yml @@ -0,0 +1,326 @@ +- type: rcd + id: PipeFourway + category: Piping + sprite: /Textures/_Starlight/Interface/Radial/RPD/fourway.png + mode: ConstructObject + prototype: GasPipeFourway + cost: 1 + delay: 0 + rotation: User + hasLayers: true + fx: EffectRCDConstruct0 + +- type: rcd + id: PipeStraight + category: Piping + sprite: /Textures/_Starlight/Interface/Radial/RPD/straight.png + mode: ConstructObject + prototype: GasPipeStraight + cost: 1 + delay: 0 + rotation: User + hasLayers: true + fx: EffectRCDConstruct0 + +- type: rcd + id: PipeHalf + category: Piping + sprite: /Textures/_Starlight/Interface/Radial/RPD/half.png + mode: ConstructObject + prototype: GasPipeHalf + cost: 1 + delay: 0 + rotation: User + hasLayers: true + fx: EffectRCDConstruct0 + +- type: rcd + id: PipeBend + category: Piping + sprite: /Textures/_Starlight/Interface/Radial/RPD/bend.png + mode: ConstructObject + prototype: GasPipeBend + cost: 1 + delay: 0 + rotation: User + hasLayers: true + fx: EffectRCDConstruct0 + +- type: rcd + id: PipeTJunction + category: Piping + sprite: /Textures/_Starlight/Interface/Radial/RPD/tjunction.png + mode: ConstructObject + prototype: GasPipeTJunction + cost: 1 + delay: 0 + collisionMask: Impassable + rotation: User + hasLayers: true + fx: EffectRCDConstruct0 + +- type: rcd + id: PipeManifold + category: Piping + sprite: /Textures/_Starlight/Interface/Radial/RPD/manifold.png + mode: ConstructObject + prototype: GasPipeManifold + cost: 2 + delay: 0 + rotation: User + fx: EffectRCDConstruct0 + +- type: rcd + id: PressurePump + category: PumpsValves + sprite: /Textures/_Starlight/Interface/Radial/RPD/pump_pressure.png + mode: ConstructObject + prototype: GasPressurePump + cost: 4 + delay: 0 + collisionMask: Impassable + rotation: User + hasLayers: true + fx: EffectRCDConstruct0 + +- type: rcd + id: VolumetricPump + category: PumpsValves + sprite: /Textures/_Starlight/Interface/Radial/RPD/pump_volume.png + mode: ConstructObject + prototype: GasVolumePump + cost: 4 + delay: 0 + collisionMask: Impassable + rotation: User + hasLayers: true + fx: EffectRCDConstruct0 + +- type: rcd + id: ManualValve + category: PumpsValves + sprite: /Textures/_Starlight/Interface/Radial/RPD/pump_manual_valve.png + mode: ConstructObject + prototype: GasValve + cost: 4 + delay: 0 + collisionMask: Impassable + rotation: User + hasLayers: true + fx: EffectRCDConstruct0 + +- type: rcd + id: SignalValve + category: PumpsValves + sprite: /Textures/_Starlight/Interface/Radial/RPD/pump_signal_valve.png + mode: ConstructObject + prototype: SignalControlledValve + cost: 4 + delay: 0 + collisionMask: Impassable + rotation: User + hasLayers: true + fx: EffectRCDConstruct0 + +- type: rcd + id: PressureValve + category: PumpsValves + sprite: /Textures/_Starlight/Interface/Radial/RPD/pneumatic_valve.png + mode: ConstructObject + prototype: PressureControlledValve + cost: 4 + delay: 0 + collisionMask: Impassable + rotation: User + hasLayers: true + fx: EffectRCDConstruct0 + +- type: rcd + id: OutletInjector + category: Vents + sprite: /Textures/_Starlight/Interface/Radial/RPD/injector.png + mode: ConstructObject + prototype: GasOutletInjector + cost: 4 + delay: 0 + collisionMask: Impassable + rotation: User + hasLayers: true + fx: EffectRCDConstruct0 + +- type: rcd + id: VentScrubber + category: Vents + sprite: /Textures/_Starlight/Interface/Radial/RPD/scrub_off.png + mode: ConstructObject + prototype: GasVentScrubber + cost: 4 + delay: 0 + collisionMask: Impassable + rotation: User + hasLayers: true + fx: EffectRCDConstruct0 + +- type: rcd + id: VentGas + category: Vents + sprite: /Textures/_Starlight/Interface/Radial/RPD/vent_off.png + mode: ConstructObject + prototype: GasVentPump + cost: 4 + delay: 0 + collisionMask: Impassable + rotation: User + hasLayers: true + fx: EffectRCDConstruct0 + +- type: rcd + id: DualPortVent + category: Vents + sprite: /Textures/_Starlight/Interface/Radial/RPD/dual_port.png + mode: ConstructObject + prototype: GasDualPortVentPump + cost: 4 + delay: 0 + collisionMask: Impassable + rotation: User + hasLayers: true + fx: EffectRCDConstruct0 + +- type: rcd + id: VentPassive + category: Vents + sprite: /Textures/_Starlight/Interface/Radial/RPD/vent_passive.png + mode: ConstructObject + prototype: GasPassiveVent + cost: 4 + delay: 0 + collisionMask: Impassable + rotation: User + hasLayers: true + fx: EffectRCDConstruct0 + +- type: rcd + id: Radiator + category: AtmosphericUtility + sprite: /Textures/_Starlight/Interface/Radial/RPD/radiator.png + mode: ConstructObject + prototype: HeatExchanger + cost: 3 + delay: 0 + collisionMask: Impassable + rotation: User + fx: EffectRCDConstruct0 + +- type: rcd + id: MixerGas + category: AtmosphericUtility + sprite: /Textures/_Starlight/Interface/Radial/RPD/gas_mixer.png + mode: ConstructObject + prototype: GasMixer + mirrorPrototype: GasMixerFlipped + cost: 4 + delay: 0 + collisionMask: Impassable + rotation: User + hasLayers: true + fx: EffectRCDConstruct0 + +- type: rcd + id: FilterGas + category: AtmosphericUtility + sprite: /Textures/_Starlight/Interface/Radial/RPD/gas_filter.png + mode: ConstructObject + prototype: GasFilter + mirrorPrototype: GasFilterFlipped + cost: 3 + delay: 0 + collisionMask: Impassable + rotation: User + hasLayers: true + fx: EffectRCDConstruct0 + +- type: rcd + id: CanisterPort + category: AtmosphericUtility + sprite: /Textures/_Starlight/Interface/Radial/RPD/port.png + mode: ConstructObject + prototype: GasPort + cost: 3 + delay: 0 + collisionMask: Impassable + rotation: User + hasLayers: true + fx: EffectRCDConstruct0 + +- type: rcd + id: RadiatorBend + category: AtmosphericUtility + sprite: /Textures/_Starlight/Interface/Radial/RPD/radiator_bend.png + mode: ConstructObject + prototype: HeatExchangerBend + cost: 4 + delay: 0 + rotation: User + fx: EffectRCDConstruct0 + +- type: rcd + id: PressureRegulator + category: PumpsValves + sprite: /Textures/_Starlight/Interface/Radial/RPD/pressure_regulator.png + mode: ConstructObject + prototype: GasPressureRegulator + cost: 4 + delay: 0 + collisionMask: Impassable + rotation: User + hasLayers: true + fx: EffectRCDConstruct0 + +- type: rcd + id: PassiveGate + category: PumpsValves + sprite: /Textures/_Starlight/Interface/Radial/RPD/passive_gate.png + mode: ConstructObject + prototype: GasPassiveGate + cost: 4 + delay: 0 + collisionMask: Impassable + rotation: User + hasLayers: true + fx: EffectRCDConstruct0 + +- type: rcd + id: PipeSensor + category: AtmosphericUtility + sprite: /Textures/_Starlight/Interface/Radial/RPD/sensor.png + mode: ConstructObject + prototype: GasPipeSensor + cost: 4 + delay: 0 + rotation: User + hasLayers: true + fx: EffectRCDConstruct0 + +- type: rcd + id: AirSensor + category: SensorsMonitors + sprite: /Textures/_Starlight/Interface/Radial/RPD/airsensor.png + mode: ConstructObject + prototype: AirSensor + cost: 4 + delay: 0 + collisionMask: Impassable + rotation: User + fx: EffectRCDConstruct0 + +- type: rcd + id: AirAlarm + category: SensorsMonitors + sprite: /Textures/_Starlight/Interface/Radial/RPD/airalarm.png + mode: ConstructObject + prototype: AirAlarm + cost: 8 + delay: 1 + rotation: User + fx: EffectRCDConstruct0 diff --git a/Resources/Textures/_Starlight/Interface/Radial/RPD/airalarm.png b/Resources/Textures/_Starlight/Interface/Radial/RPD/airalarm.png new file mode 100644 index 0000000000000000000000000000000000000000..14929b0d7b7a142fcfb0531006bdb3f53ba7be92 GIT binary patch literal 1295 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=hEVFg9m8I|q0=J1Z0v<)>xlq%tsQ zoS!<;*5j~)%+dH@*R@mm$^;5e1k8Q2Xi?u*F4q+TS*>BRK6-zf)ObC+x)(o)D)^AR z@zsMzYgRX}R^X^>u&}n!+~v{v-0@SxN_QO^RA?ZWw+C`1jUVS zhHiW=`j=xNKjWDbXN;byXm<8Jel2IW@1AR=*gtc&D|3!A%=eH)m~C902nKr1s;*b3=DjSK$uZf!>a)p zN9~y*5hW46K32*3xq68y`AMmI6}bgK)eHls47YguJQ{>uF6ifOi{PDbs{}UK z3djZt>nqvW6s4qD1-ZCERRDRmN*N_31y=g{<>lpi<;HsXMd|v6mX?v90`z zGE;%B09k2gXakl<5wp<;IRwdJb`TMuUx6%m$bfEn(Vp_JUFW@A?lvzWz^)XVCf3kdjSs&)e$R8eZ;!Kh8?(N&A{!92J-HII zzH#Ho2gW&k3Tz6$g`^c!&NrN9 z^tvQdu#Hn#dd}JNhk4SC1ztKkWo@u>aAHY(IkSHMgPh#dJPT>r12x@p-&>p*mBr7@ zwVml0I!!oIg7r?_AKBOrl?iiZbx-0pkP(o%-=X7Dx}hx5Hjmwa$HB2h^u@3B>Q6fU zTNwClhzplrZ4-7KDDuC)GFf5qnkKa{-z81Q`PUh*yMFb*e8o?tw%UkdMk9_sy%Z2m z0D=j@SCl&2ULI!OVD;eBF$cE@lbT;Y8XsOh&d&n^T2g8%7Y%B6)SN$X;6OvvCWXdu z^Ki3I+#P0re*W!#`1bX39(nscX+M5{H~0K9|NednySj?A2CuJ)u9;^1z>C?y;jBr6 do*7pH1H-k0$Ii^mIOz+@Bc85)F6*2UngHSq4XywH literal 0 HcmV?d00001 diff --git a/Resources/Textures/_Starlight/Interface/Radial/RPD/airsensor.png b/Resources/Textures/_Starlight/Interface/Radial/RPD/airsensor.png new file mode 100644 index 0000000000000000000000000000000000000000..c1a10f00200518d833b692952e44a806c559d461 GIT binary patch literal 166 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJTu&Frkcv5Pr`+W{V8GFmd%RG< z{pE>u$@0IPr9KHPP2&E3P*KQ-Pg#hOfk7jU^U96c-z&L$X1;d`U3p$v&8A;kR#sw_ z<@~f>i<{>(l=^4*da=i_>2;nkFgs=VVW#AZ|NpKV2kc`%Wv-vO6S(7@jSDBx OVg^rFKbLh*2~7a+$T;Bu literal 0 HcmV?d00001 diff --git a/Resources/Textures/_Starlight/Interface/Radial/RPD/bend.png b/Resources/Textures/_Starlight/Interface/Radial/RPD/bend.png new file mode 100644 index 0000000000000000000000000000000000000000..c62cf62220d0fe145cc17c2568896ca5c10d210c GIT binary patch literal 330 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyu>n3Iu9A|H9v&Wbb#-xZam~%m zyLRmg2?=RyYm=9kmywa#vuDrt?b{t49cRv*Ieq%{9Xobx-MV$lmM#1D@87p?pRBB` zw6wIMqN0?PRAgjiPft&0XXnnHI~y7rl9G~uHW>ZzbOBQGB|(0{|M3Ebvyx>3K&7>w zE{-7Bn<=4&?KX-%|NFORAc-V%K8zu`h=7nMo2B`*)`cm4H5W!WMZ^&j#Z#FVdE z9dOvN@EVuan;5xoZqm0ok5%=reSM)ZKePG(tF6TxwL7T}6)S`f+p?#mu?#}P``<&nXo!>bZ&RFN{7k|_btgo-*cDqR=6722m+49w9#|XgbbTTzHW!JoQfOI+yKp+sHqod<=0ovQ!%f@YO z6pKaM!EFPSN+miwJ1GXYA{-eR(ZyKPG|NuCUav8po11GG!_g>sBoYB2lgS`S zl4Sq@MN!01Zv=57fY8=vns{=pso((7!9Ieau<5*p0GUh%RaGrJ6WZELqpB)h7cSZd z&@>H6lGxr=+1^!A{Fkf#fA!{VUAxKgQ6pnLfvcvqWBLkudt_u;rpqN&71xXB&rSr7 z4)&p{s+ntZfNVBvWI(Ly+7%xzmkWT;8()pN;`#MZ(3tD?cx#8a0|$p$9z7lfU}!i>VyRpMq7Fn2hzxi<=4VAifK)2Q^6CZv1N}Gn z^l_D3F2|Q|-;Fu>Tn?b-`S$KV*oKlMRrLX}`bMhEf+R_;H=@c;IQ{Z<-8qxv)nJvK z&`c)+i2Yuff$67DOdSGMJE5{~Gz9?VBlXwMA9btT3WTC4q*5vS006;I*wU%?w<_)f z4Rz)>B3ttLeEQhfn7+8UsL#yI=zhOnH@B)U+qYte!-1~rXLBpob)9@ZkJIUFDY$I_ zvFcK(WPBr>DgZz@Ts|3hc6K;AIy!xT5({%`afQ6@Ag3_90>00000NkvXX Hu0mjfV7P3Q literal 0 HcmV?d00001 diff --git a/Resources/Textures/_Starlight/Interface/Radial/RPD/fourway.png b/Resources/Textures/_Starlight/Interface/Radial/RPD/fourway.png new file mode 100644 index 0000000000000000000000000000000000000000..336d930022d9d80f5fc958a0b1cbebc480b2e3a2 GIT binary patch literal 679 zcmV;Y0$BZtP)*3W()+_buUSu!N&{C_z)_xulNT$xNpIgX zN~hDhDFBG*21+TpxVQ)<9tvPE7ywW#7Kuiqy9E$Jd;~`#ESF1daczJg2#Cky%;)n^ z;@SX8eKh^55<3Uj1lc)2EEfB6-#NhB+ZzBcFE61144t5CHv6Uai^YQJbgGq0YZ6+P zMYUQbnM~5}_vv=KOs7+wR6kPzfbn>&SAow)vYKd~=l$_wG#bUSEFK;n$mjFewoSQQ z=KlVkUatqBEX!gtnfz1qbAGc_DzP$@VzJ0_x!iibTdfwgT8(FJ4F zF2~{FAppr_a{c_^-~go*hGC$TVm_ZEr6dRfj*pLNG#a?BYnYDXs7j^6&CSi)VB+!k zR)D*^I{@14HYX=19336;`1r{6^)&$fejk8Nr?WNw_4O5iQmM4Ib7OOv5JGG{6Q$ICwqm6e&(F_DDMN{C z1FXI}2!j3e0DKq8VzmX&;Yl$x45or%x1Gt0PpX^0_!#3kre;{ N002ovPDHLkV1nxWK&b!# literal 0 HcmV?d00001 diff --git a/Resources/Textures/_Starlight/Interface/Radial/RPD/gas_filter.png b/Resources/Textures/_Starlight/Interface/Radial/RPD/gas_filter.png new file mode 100644 index 0000000000000000000000000000000000000000..35096091a82e1f99593bcecaf96415061ab9c86b GIT binary patch literal 369 zcmV-%0gnEOP);i%vqwBhPbfur(^nQVMZU zO=z0Nn|YEX$}uH?X`-!63(*1qK$@n?F-j?_s=_b~(($^kP2tS|ra(Wp8}4np=+}YI z^=b9jlNe)*_A!pvgjxqs6vYH_zFhEqy-KsN8GwKQ;ACzMaE#+c+6BbDZJoBjScG91 zPH7gJub46Blm#O|mSq6`_F71!-__g-fO8H?>0~P8!yK@zfqB*YG`9v25c6->;(Hlj zEA1!Y+fPEP+fa40an1n%U#HV{05*5Ji?jefpJgut_;`-l=zJ|qKBF0pfnEPyFS?)*;%eIDI1ak5>%TqEL(?=yqq?rOUVnl@h-;E0#>3_)O;atz z+t`Mxs#Y`ieSb7J3NW=10Eh_8Y+N&S5hDNqNGXl^APCU5tu|Md<<#-h0J?*QH~%`H z5yde+PbYj|F8!J*aHUtPRX1X05D@@q&F|4H00jYHk@+G31+m238i0uKemGdiR|Rlg z7tD+(ju)HhWb6V}XiBQ!qB#B)1LoI)5rCN?r9@elAfnzx72q(oX%`HQ)|M5(ShYW_ z3&!-2F4$VL0B*Zu$)9-oEU=n}y2+Mh8Nx6`p66)W79YpsHUOrX&dex^VwSNLz?#pp ly8))2V{7T@&+=WnegOTEp{kc0t`q!lvI6;>1s;*b3=DjSL74G){)!Z!pk#?_L`iUdT1k0gQ7S_~VrE{6o}X)of~lUN zo}pnEhuArwq8+Ibo@t(*S_~XO4l9EcBP#s)Ce%m7!No)K zU^TzeuQrd4xVpN(pKW~O`7;?9d@i#vSoASpFa20*Sa_iJe#G^u{i{2fzttYMIj?r? fM7Wpc(p7sHG>;fhd48n(GRQZcu6{1-oD!M<%S(%F literal 0 HcmV?d00001 diff --git a/Resources/Textures/_Starlight/Interface/Radial/RPD/injector.png b/Resources/Textures/_Starlight/Interface/Radial/RPD/injector.png new file mode 100644 index 0000000000000000000000000000000000000000..b38c170b02c372e1053d11ee763382d49a45a6a0 GIT binary patch literal 368 zcmV-$0gwKPP)b;z6ox-n9Rouwj^LTeL(bqEFy;iFIY0*n!&RW;O%F+=UPFN7cgKU8ZCfYzGTYV~FRLj`VWn*7mOiV zz~h`7uKkXrkR;%o8`T;KSudPQn<8 zm~i+Df5Urbvsn)?pU+t=7F;eDj>jVa=kqywUi3Ne5h9AB5Sz_Llw~OZkrJLH0FY%F z%jFV){eDlL=P6^-1@5_CufH$QbsgL7HaU2-z;3sru4_)G(`bo_r}7T7j%v_e=sp+# O0000lq2rKPQ{t(PxfzGKIZ*4EazxVV`!XSTJqZQs5fsA^WpU49_N zT@vIM{2vY&y!;tI0fjjWJR*x382Ao@Fyrz36)8YL$r9IylHmNblJdl&REB`W%)Amk zKi3ciQ$0gHL&Gi(v2#F0J5nP&(>y)37&w3&Rt70XRt82O%L|C5p=^+sG#Ht|;!Hrc zAtMumAdrp%;>>myuy_uT4FU;34AKvy(QRd500zqhb_N!p8UrI^1I7gqb3uAo7eLIK z24sT(6VN0ku*x7y3m^-s%h138B+F)crFc_+ku{JR>gnPbV&VUGf+Jsp0SAlt6}=z- z*{fG96A`YyQ1MFUMhj0%LoGv*vT^GvO@@Y!BQIEEula^1-!WIQVr*S9GqSs=_nKF^ ny5!^R#FE4O^%V(RQ@NASGB50w;}F>4x-4FXI+lbFCNgDfq8ET}F+0|SsOo9UI}P5njIKxVb4 zi(`m||I-QHLJbBS&X+GO`tkJN^3q+}F9O2z&gi{VW^UzUk*}!bQ&{@Mt;aR&>YKDG z=ULuiuaxe036aw!CgfgF5NJEwlX#Q$8z29k ujEarU@^>39+J7i?Yglwrg|Fg*e*^a~GpnA31$zxfufW&c(!`F}HH#cv;|7yRDqA}3`0000ST5Ji8Yh>FfLFai=Ki+Be{VFwnmu3UO|mBmz%_ao7n}tE0Bzf{ z!{>Qs_p$-F#u88fxKqVxnnY$f_elV2EkX!z&WVDZa|j{8TB`%NLIr>XSe8X)&bd#H zq4yqL*NGQV-}i_y!h4_WO-_MEKQRTOO(@-Rrvga!`{@9hCs_@U^At;z^(CvS3j4ml m&UIa5+qRD$sV`YkQPC4k)JGmAsq~2e0000!lvI6;>1s;*b3=DjSL74G){)!Z!pk#?_L`iUdT1k0gQ7S_~VrE{6o}X)of~lUN zo}pnEhuArwq8+Ibo@t(*S_~XO4l9EcBP#10FF|INl85iOU&G-J&n)_ zk}%e3;qu&g?G#&uY4+L{wt)2x>AAY=zFX^sJ}qClBL0xSap}30|Ch^Mn9InZaat(% z_M%LwE2Xj`3KiVP7Ww%E$({R|nsai2XaIjlw81ysjzt>3cB!xZ?tbM=TTBPYr=G5U JF6*2UngD@woH_si literal 0 HcmV?d00001 diff --git a/Resources/Textures/_Starlight/Interface/Radial/RPD/pump_manual_valve.png b/Resources/Textures/_Starlight/Interface/Radial/RPD/pump_manual_valve.png new file mode 100644 index 0000000000000000000000000000000000000000..c2860045a333be9065e671c03c3636554aa57992 GIT binary patch literal 334 zcmV-U0kQsxP)2m3p`SIg7kuTfMu~3S%{W<0#Vbf ztB@jZ0>L8lm2dv{&(9m)fMFQMeIRR=X%W?n2E4R5WPgn3;D zc-#;Io&VIqw gH=$t|hH-Vi01gOSE^ug3#sB~S07*qoM6N<$g6E%#djJ3c literal 0 HcmV?d00001 diff --git a/Resources/Textures/_Starlight/Interface/Radial/RPD/pump_pressure.png b/Resources/Textures/_Starlight/Interface/Radial/RPD/pump_pressure.png new file mode 100644 index 0000000000000000000000000000000000000000..aee0831c849248ce336abc1cf45da0b2f9c36c1f GIT binary patch literal 332 zcmV-S0ki&zP)1pw6o!A5&N_f0OP3BAdWG}?#VxH3Hh9P}TIf+c=mcJ!3wSUTWNZ+a8sfPbtRe2E zP8nLd_zhAB`bNS3^zrzG0FTGxaSWq`IOpc?nbw*#YjKEkZo)7G03=C*wr$Zg4T_>5 zNn9@toaltWn-Bnib(T@b&;{06_G<&y1b&^AR^M&$!=g@Hq?^$B`^>wKSl$W@Wj< z_k6^D+~aWgLRs#}e*qcJxp{hgF#ml_RwbNWeld`X^RZf)AP6u`6K<6v&sqEmPB6y6 enBC%gE|VX-$8A7{l5-mX0000O@wT#6+clF0za zJm<8T(F~@9Vd(PvG=rqToOol-+@Dxi7uj8py&I*^UPoK+c*}# hg1`R*ilQir@&FScNU&8|=;Z(a002ovPDHLkV1j@RaB2Vm literal 0 HcmV?d00001 diff --git a/Resources/Textures/_Starlight/Interface/Radial/RPD/pump_volume.png b/Resources/Textures/_Starlight/Interface/Radial/RPD/pump_volume.png new file mode 100644 index 0000000000000000000000000000000000000000..a3110ec00e3540fd4e70c7d578f5cef5a738e276 GIT binary patch literal 274 zcmV+t0qy>YP)0C}F%G!1oKqpBu}#n`|dRYg?+xC>eMFeiaD zO{c`C3p|1-5CvXapeTyT0;WYY3|F?>4eQm4pVNu9ZOsCsf53b1s%p>gaOK>d>ADM5 zwKv}aGa?eczjooj!}L^%*_s2CWjU|FBQOunVYv)RlF;`(pU0!Qj(HC_=Wy=1gCBWM YZtFlq)$I4mc>n+a07*qoM6N<$g6W5LOaK4? literal 0 HcmV?d00001 diff --git a/Resources/Textures/_Starlight/Interface/Radial/RPD/radiator.png b/Resources/Textures/_Starlight/Interface/Radial/RPD/radiator.png new file mode 100644 index 0000000000000000000000000000000000000000..420d68bc7337e8dde7120a141ffc4f9a05aed16d GIT binary patch literal 280 zcmV+z0q6dSP)48LLC9T_W+2<7ziN%q?It`)l6I!{ubn{q3pU&Gi`(s5&FJYy<<89 z=?nAcxiO2W0n%yDMY5%yjaLOoS1pNd`PBjTvIbxRxLj}1;#UWFKJL!qR|nX`8bAYR z0PRO~tS$I&fOl0KQ+2%MYfV)Z!)L`ifm)1f%(Ml0@;O1PIQ&3ka){P!5oFRG7v(Ft ehD#zv?fDDvFM{is>b(~L00008ZuQ0pzeUNHMZ9FalX#Kr9VqgPgCy$P5-|0C2qv&IumIH<7#SNdE`XQ|(!;s{V%9Vu8w8kuCNY6k23cAFSx{Yu z1_mHmHq$G`oBE5afy}L*E{-7<{&y!`%xwo4vs5>KY>#>mLExm*t9FCe%@=pXNOv{|>laRM;t}EkwZ8f{LAs;pKKe}mC{uh{K!->JApUPY5n Uc)#9~E|6zDUHx3vIVCg!05}1f9RL6T literal 0 HcmV?d00001 diff --git a/Resources/Textures/_Starlight/Interface/Radial/RPD/scrub_off.png b/Resources/Textures/_Starlight/Interface/Radial/RPD/scrub_off.png new file mode 100644 index 0000000000000000000000000000000000000000..e6da8871ac2c2e11e3c30d82eca0fb97435cdd36 GIT binary patch literal 820 zcmV-41Izr0P)jx?LUzuSRCbg+qSn7veCvc3}-?Jf*@czol+D9 zS(cIKIYJ05%fd8G48tIbBJw=nSj5^4gkd;4IXTh6Ez2UyGLqYvL83T1aP?j(r4|Xy zHW5);M=pfW+(;s_a6Xw`Qk?$$E8#%QK?j@)oO2s!{OUyJS#mA1Ob!Dge=Q=e%#Px zt~;;$^Wz3ZQRwfp^9xMVL`td8UtV6W%)n9sX__(`edhjtZn)om|ATg`$=Ue@0H$ej zbA6?S)xEh7y1l+O=5+dOZ5dX203gqEZMbf)&v+ysk>6ZjVVb5Uhq*EHJYN@jGXX*f zeWP}(N!{})ib9hw%24-wZOo$s7a_z(0+yD*wrxDG%w%E#@YBz~P!xssK;82-`FJEz zN^y36!RuEIq?A~eMYUQ+Df`<507OxQ@B0AUTwm!F1JpgAo9ip;o{v(BL82HWzv?WR zrb!eR2DVHlaj>~t3p?iUpa-)W{wi$g1@~9gw@O{;O)D5fE>ek(GV?e! literal 0 HcmV?d00001 diff --git a/Resources/Textures/_Starlight/Interface/Radial/RPD/sensor.png b/Resources/Textures/_Starlight/Interface/Radial/RPD/sensor.png new file mode 100644 index 0000000000000000000000000000000000000000..6c38f004fa32c0adf96138b2b431ba655cd92d0a GIT binary patch literal 657 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCil00(?STB_$<2JUr^^>f++!nwy(nAmtPE0&tPG4mmKP99L)oC< z(O_f-i!%Y)hKx)Mfvp_8juYFOhA*Ez$$|*Er2YjE<*zYkSv?&mEukPMb<#(T2B|p5DWj-%enkb z4jc>#JX_xAEVbAy{{R2|oN2ikOf{cw6q~0oEauZV#w0egu(Bn#(QbheL+DO>9V6|4 zaQ7m|EWdPdhHZ=)yH>JY)BO5Zdy>9aj_Aphrk3lR9jq06+y<$FCsUg5d{>#2t8o88 zPSbgAxf$^rq>e_mbbP6OtRIrMncr&bt0%nd^$85`{~fTIwVXez<|o63UWez+EzSIg kHu8(B$L%es{D1vF@BgK#iy79PUJdeur>mdKI;Vst0Fl|wa{vGU literal 0 HcmV?d00001 diff --git a/Resources/Textures/_Starlight/Interface/Radial/RPD/straight.png b/Resources/Textures/_Starlight/Interface/Radial/RPD/straight.png new file mode 100644 index 0000000000000000000000000000000000000000..46239651339046835fad13f56254c8729c0c97c0 GIT binary patch literal 337 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyQ2{<7u9A|H9v&Wbb#-xZam~%m zyLRmg2?=RyYm=9kmywa#vuBT#lvHG7Cc( zp&=N>(;GXwro*URNTLR|Gs_ufX4e@yY3C7Cg&9Q$}j)xg}1S0zH8H2teidP#>O>%OH_~kdd8hI$tEL; zA$Ip&%M0bdk2NXwvvWJKI;^uhEbsKnBX&;B3D2)Q3j`Xt=9e#+E~C#o zIdkTU6)PMa9i^qEw{G1kB_#zk*q3X17LbxD3Gxg6j~g(EZ+dhcsBW33i(`mI@72j? zg_;$3T7+8!1XqdvUcBdjb(rPVOP*D4@6Gs`w)y0SfO{^^i<{cH>-8k!Equ4^`8{1^rG8$$dXRQ^jR{G8v6MU%S@{C8j z_!M`P9eH;_IrQn7;>Q8THm`SOEI9e$u iM=P#gKK*;?KmAaP$Yp_-^;m!|X7F_Nb6Mw<&;$Tq;iYZ> literal 0 HcmV?d00001 diff --git a/Resources/Textures/_Starlight/Interface/Radial/RPD/vent_off.png b/Resources/Textures/_Starlight/Interface/Radial/RPD/vent_off.png new file mode 100644 index 0000000000000000000000000000000000000000..c54c3bb519c607c8a4b647dd250595e3e297b5d8 GIT binary patch literal 612 zcmV-q0-ODbP)6H0@K80(r5j{WSdgeCiLjSqu_&PpDnUi;2k1q56ngh82p+|YAHYkMke0NB zl#19x_mG4rLDNpq<;w0zlLO>) zv!pZG$hc4d$8k_f#ZIQxsMk(Vg?L=6|sMOH_-EP-&K(FfB?Freeq)T z{p*>u7dvev`;(3Wpie=k?INWloyi7VIG-r9u$cF*UDu^iuQ8L7OdClo%c4{&jStXn zp8@D|0q|_z+ANd1eT%hs?|Jy(eqi?nY%epXTLkr z{>KK;4)|AEscy5eUP4Mqxw6~WE8^^bB!FQYpwu4+I{RH7JzfG}VKL9v=1~pkI?y$s z9Wac8;{n*VO?77vfcd!_eEhIOuh--A*Kc0T_g)X6?|!rO500S_BG3o)>cixBK?rgD zM&$2=t1n*ODO9I-eib{+O%mTLI3?<4Y#dcz; zAvt8pmi_sBFL|u0DvYVSQBR)$CIA5UT@#Ek2qBsh(ONri8U#Tz-fp+dRB4?6LWnBM zG5~;-5=7(-005lxdRPHqy#Z>SPe<(bhhB#Rz-qZboTS!uMgS=#lv2){)jFS!P)gz9 z@o6}K)*3{F%T?iWRfAlJW^-$HtWWS7g$TnCagzQCSC_QbjRP(+0+eOx<-qfc9oPs! zL|zUMk>3+=&hh^7IoN^q<_$$r3tH9 i3yt2CP5=|Y=m5WDP{6-3w&L3W0000`pM8`7~YFAK#51)3*4 zJ&LOP``oCjIkO~SSb*H^CWSlqeuCqzq$w_t#Gc1K3CGYZW`2zDby?Xy4wIdgDfIJ} zS3FD9xT-8xHV!cVpk(`o*1=&OIi-KTP;XF!nysC1V-GBrcDUscxZHzWtLtmKv@jki z68ETg%A3FQ%u-MSfSjubr`}6wh~0{$(Lwn7?btFAhrv7iwO;N&?U#u9o1 z>GcaWaPov40zsoK_GOp91T^!2UIDx7w6|kzN+v(>h4ldmjhTRY*T?1)BJEUH>V@v{ zbx@}!RI|5g;%5#ymY}DrVjr?Am;r)F$w*OD#}v6LG*TqCu2$q4cc7aYby)q8`5RG- z*5=^U%-5!aT6TU`OcXs2nF3WtJ4e|=JBOIAeuk=?jkx9ffXsldx>13)1IM_bam4yB z4b5=lU~BG_rIjM%>1Jc6X33xBfXKlzFL)6V#Om@o<*O-AQ&0L{O^9BxDizbRB#e0x yUO3DWXBK@ElCwy9IpyE}LlOmu0z?7+3xJ==w2z@j@En8y0000WyI-hJsD=jcll>o!y;T zf;lh9ap#=*nD3dNv$OMVfabCQKyz6Dpt&pn&|DS(Xf6u?G?xVcn#%$J&1C_A=28M6 zJt>jdVmIPuOMeLdb14Bp$(LM+LPn~M9lhd~3Zc2m#^@?5t`Y!45r>fJ7)9idF{yDc z9@)*3>|q!*ro||09rOeYK?#7DPo1J+)Znr_|3(4z^x}GPiOHSs zS_cDft8@UAG|heLA({qvZ%z6Y$JH^1p*JZk;rM$2Av0kfZTxzt<)OI$miZTVxugSd zkIagOQ{psKC*!ku=~bWOP`yFpl>ka0^GnAt#td<9tk;m%Ug-dKrsax;lc*O5eox?R zfrFhma~P-6(*8LWp0&5L*G+90@Jztc z51okO4X0ism7pI+=xI<=<<<9Db@{nfA<%t#j$k=vDtXC53>r;+Q=Q^}O5U+;tF$+Z zl0}fF0rgtVgK3oIg~4vGL~nd9W?OHsS&!~Vr=N$NC%?Ep#X~soWZfqGl9)V6{uDYOOLlYBi~xE+56vetn>_}@!=osu02;1mff7%`%1JCFKMev=P%$Fv#l^+o zg)xp{k4M=zN@aV2Vb8}9{s-Is47MCXVdK0gr(UINVdFUJu{3#-_xprODc=lHk|(qC zS>c`@o_voO`t;eGB*|t!z{{N>%>aK69LBz&Bu@&;ZqRLXp{8#JL-V1a z<%Nx1NLWwLbs<;^3IH)3_|-cdR(yt+M?`K%D=l_GR8W5paIcY^JIIN9W&I!m&1C_A z=CS}lb6EhOxhw$CTowRmE(-uOmjwWtOWD~Vf}}uCiKPQj-5H1bO!dYYj7 z;~$U;K!z{Cti1jK@*_EGQ<%U00cilp0}zNWz^r8Z2c!WA#1~*zD*OXdr+~k{0JBoz zACLyXA76l3sq_y>9RdFM0?bO4e?a0?S|lRG{})<&y%x+sVhVV1(WY3Xxu~ldW&)73&~ei9&hxV7Ns1}p>YY)HJY9(F z>`m;|TO=a|);u0OO%!Eob<0~#5~2zqtFBkSQQhLQ$ZwFe*p|-5yKOaCXtm*`RD^8b zETzxe6O=%4#LA4Q0x0;hlNpfxc?zBX@z^zjyUH+j`Fr4D4`N^L7^=PF|MNZDtkCHj zi>LyIa}J=_{gfMStlg}XoESi|^8D5%WUiAi(B+2QpB4~A{W(Bqe+a&+Ys^Y|j+0_aL99W2E64=(n`8%SGT2xpTYBkBDtjXv=m>fm)cY3V?>8|A)TdmlnEFMScIiTTkAt-PwTw(jB*f#7?>`jAkB0PBF$%Dpe6?iDL z7GMuhe326DO=EmJWi`(Lo3xRoM5E;epxD=d$yh(FgAav*+O675ePHylTKt|@xUyRmiaIw=0KONX};B*Kj zC_tWpfk*PM7|kCYtIxZ-B+H6&G=+B_(Tcq zRnU8ERRSVw@PV|a%xJ95#AG%2M2YNG&~t3n*MLN7@QD)HtB||rHBZ-cRF@T$j{f?fsP1jMSrYaYwMUWLE&JH{>< S=HG$<0000 Date: Mon, 15 Jun 2026 17:24:31 -0400 Subject: [PATCH 02/10] i am the silly --- .../_Starlight/RCD/AlignRPDAtmosPipeLayers.CS | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename "Content.Client/_Starlight/RCD/AlignRPDAtmosPipeLayers.cs\342\200\216.cs" => Content.Client/_Starlight/RCD/AlignRPDAtmosPipeLayers.CS (100%) diff --git "a/Content.Client/_Starlight/RCD/AlignRPDAtmosPipeLayers.cs\342\200\216.cs" b/Content.Client/_Starlight/RCD/AlignRPDAtmosPipeLayers.CS similarity index 100% rename from "Content.Client/_Starlight/RCD/AlignRPDAtmosPipeLayers.cs\342\200\216.cs" rename to Content.Client/_Starlight/RCD/AlignRPDAtmosPipeLayers.CS From c95776c40281c20174ad5dad59fcf0de91253e0b Mon Sep 17 00:00:00 2001 From: Dunrab Date: Mon, 15 Jun 2026 19:09:29 -0400 Subject: [PATCH 03/10] give borgs the RPD in the RCD module --- .../Entities/Objects/Specific/Robotics/borg_modules.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml b/Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml index 64530974cab..cc93999120a 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml @@ -788,6 +788,7 @@ - type: ItemBorgModule hands: - item: RCDRecharging + - item: RPDRecharging # Floofstation - give borgs RPD - item: BorgFireExtinguisher - item: BorgHandheldGPSBasic - item: GasAnalyzer From b048e2421dca1300e1977b0ebc5595e6136ef4b9 Mon Sep 17 00:00:00 2001 From: Dunrab Date: Mon, 15 Jun 2026 19:27:14 -0400 Subject: [PATCH 04/10] make an experimental RPD variant and an admin specific variant. also add the admin variant to the admin toolbelt --- .../_DEN/Entities/Objects/Misc/admin.yml | 3 ++- .../Entities/Objects/Tools/tools.yml | 19 ++++++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/Resources/Prototypes/_DEN/Entities/Objects/Misc/admin.yml b/Resources/Prototypes/_DEN/Entities/Objects/Misc/admin.yml index 4a521a1a35e..ec7d28aefbc 100644 --- a/Resources/Prototypes/_DEN/Entities/Objects/Misc/admin.yml +++ b/Resources/Prototypes/_DEN/Entities/Objects/Misc/admin.yml @@ -28,9 +28,10 @@ - id: GeigerCounter - id: AccessConfiguratorUniversal - id: AdminRCD + - id: AdminRPD # Floofstation - give admins the admin RPD in their belts - type: Storage grid: - - 0,0,6,3 + - 0,0,7,3 # Floofstation - increase admin belt size to fit the RPD whitelist: null maxItemSize: Normal diff --git a/Resources/Prototypes/_Starlight/Entities/Objects/Tools/tools.yml b/Resources/Prototypes/_Starlight/Entities/Objects/Tools/tools.yml index 645c382e5de..2202f8a2339 100644 --- a/Resources/Prototypes/_Starlight/Entities/Objects/Tools/tools.yml +++ b/Resources/Prototypes/_Starlight/Entities/Objects/Tools/tools.yml @@ -750,7 +750,7 @@ - AirAlarm - AirSensor - type: LimitedCharges - maxCharges: 90 + maxCharges: 30 # match RCD charges since this also uses RCD ammo - type: Sprite sprite: _Starlight/Objects/Tools/rpd.rsi @@ -777,9 +777,22 @@ - type: entity id: RPDExperimental parent: RPD - suffix: Admeme name: experimental RPD description: A bluespace-enhanced rapid piping device that passively generates its own compressed matter. components: + - type: LimitedCharges # match the charge and recharge time of the Experimental RCD + maxCharges: 20 + - type: AutoRecharge + rechargeDuration: 10 + +- type: entity + id: AdminRPD + parent: RPD + suffix: Admeme + name: admin RPD + description: A bluespace-enhanced rapid piping device that passively generates its own compressed matter. + components: + - type: LimitedCharges + maxCharges: 100 - type: AutoRecharge - rechargeDuration: 1 + rechargeDuration: 0 From c3bae107769584e920a6c6d4ecfa461e6780c24f Mon Sep 17 00:00:00 2001 From: Dunrab Date: Mon, 15 Jun 2026 19:36:36 -0400 Subject: [PATCH 05/10] make both the admin rcd and rpd actually re-charge --- Resources/Prototypes/_DEN/Entities/Objects/Misc/admin.yml | 2 +- .../Prototypes/_Starlight/Entities/Objects/Tools/tools.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Resources/Prototypes/_DEN/Entities/Objects/Misc/admin.yml b/Resources/Prototypes/_DEN/Entities/Objects/Misc/admin.yml index ec7d28aefbc..713bd7cf7b3 100644 --- a/Resources/Prototypes/_DEN/Entities/Objects/Misc/admin.yml +++ b/Resources/Prototypes/_DEN/Entities/Objects/Misc/admin.yml @@ -71,4 +71,4 @@ - type: LimitedCharges maxCharges: 100 - type: AutoRecharge - rechargeDuration: 0 + rechargeDuration: 1 # Floofstatoin - change this from zero to one so it can actually recharge diff --git a/Resources/Prototypes/_Starlight/Entities/Objects/Tools/tools.yml b/Resources/Prototypes/_Starlight/Entities/Objects/Tools/tools.yml index 2202f8a2339..f202927e03a 100644 --- a/Resources/Prototypes/_Starlight/Entities/Objects/Tools/tools.yml +++ b/Resources/Prototypes/_Starlight/Entities/Objects/Tools/tools.yml @@ -795,4 +795,4 @@ - type: LimitedCharges maxCharges: 100 - type: AutoRecharge - rechargeDuration: 0 + rechargeDuration: 1 From a1679b49cdc1b42a99687ebf70b1d5d3e9d8095b Mon Sep 17 00:00:00 2001 From: Dunrab Date: Tue, 16 Jun 2026 00:06:29 -0400 Subject: [PATCH 06/10] add rpd to atmos lockers --- Resources/Prototypes/Catalog/Fills/Lockers/engineer.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/engineer.yml b/Resources/Prototypes/Catalog/Fills/Lockers/engineer.yml index 091dc67765f..53ca0665a78 100644 --- a/Resources/Prototypes/Catalog/Fills/Lockers/engineer.yml +++ b/Resources/Prototypes/Catalog/Fills/Lockers/engineer.yml @@ -158,13 +158,14 @@ - id: MedkitOxygenFilled - id: HolofanProjector - id: DoorRemoteFirefight # DeltaV - Re-added fire-fighting remote. - - id: RCD + #- id: RCD # Floofstation - atmos doesn't need the rcd now - id: RCDAmmo - id: LunchboxEngineeringFilledRandom # DeltaV - Lunchboxes! prob: 0.3 - id: AirGrenade - id: ClothingBeltUtilityAtmos # Euphoria - id: AccessConfigurator # EE + - id: RPD # Floofstation - atmos gets the rpd not rcd - type: entityTable id: FillAtmosphericsHardsuit From 3d1b2ed7c6a69659748c6f156777acdb1ed00700 Mon Sep 17 00:00:00 2001 From: Dunrab Date: Tue, 16 Jun 2026 09:35:52 -0400 Subject: [PATCH 07/10] add experimental to research and static empty rpd as a recpie to the engi techfab --- .../Entities/Structures/Machines/lathe.yml | 1 + .../_DV/Recipes/Lathes/Packs/engineering.yml | 1 + .../Prototypes/_DV/Research/industrial.yml | 1 + .../Recipes/Lathes/Packs/engineering.yml | 4 ++++ .../_Starlight/Recipes/Lathes/tools.yml | 20 +++++++++++++++++++ 5 files changed, 27 insertions(+) create mode 100644 Resources/Prototypes/_Starlight/Recipes/Lathes/Packs/engineering.yml create mode 100644 Resources/Prototypes/_Starlight/Recipes/Lathes/tools.yml diff --git a/Resources/Prototypes/_DV/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/_DV/Entities/Structures/Machines/lathe.yml index 35e87795f17..f6953871e21 100644 --- a/Resources/Prototypes/_DV/Entities/Structures/Machines/lathe.yml +++ b/Resources/Prototypes/_DV/Entities/Structures/Machines/lathe.yml @@ -86,6 +86,7 @@ - EVASuit - EngineeringStaticDeltaV - EngineeringStaticFloof # Floofstation + - EngineeringStaticStarlight # Floofstation - RPD empty recpie dynamicPacks: - AdvancedTools - PowerCells diff --git a/Resources/Prototypes/_DV/Recipes/Lathes/Packs/engineering.yml b/Resources/Prototypes/_DV/Recipes/Lathes/Packs/engineering.yml index 8b86cefae7c..b6b90888996 100644 --- a/Resources/Prototypes/_DV/Recipes/Lathes/Packs/engineering.yml +++ b/Resources/Prototypes/_DV/Recipes/Lathes/Packs/engineering.yml @@ -14,6 +14,7 @@ - FireExtinguisherBluespace - RCDAmmo # RE - RCDRecharging + - RPDRecharging # Floofstation re-charging RPD - type: latheRecipePack id: PowerCellsDeltaV diff --git a/Resources/Prototypes/_DV/Research/industrial.yml b/Resources/Prototypes/_DV/Research/industrial.yml index a093141b447..9036865cce2 100644 --- a/Resources/Prototypes/_DV/Research/industrial.yml +++ b/Resources/Prototypes/_DV/Research/industrial.yml @@ -41,6 +41,7 @@ cost: 15000 recipeUnlocks: - RCDRecharging + - RPDRecharging # Floofstation re-charging RPD - type: technology id: AtmosHardsuit diff --git a/Resources/Prototypes/_Starlight/Recipes/Lathes/Packs/engineering.yml b/Resources/Prototypes/_Starlight/Recipes/Lathes/Packs/engineering.yml new file mode 100644 index 00000000000..73b4c99ec40 --- /dev/null +++ b/Resources/Prototypes/_Starlight/Recipes/Lathes/Packs/engineering.yml @@ -0,0 +1,4 @@ +- type: latheRecipePack + id: EngineeringStaticStarlight + recipes: + - RPDEmpty diff --git a/Resources/Prototypes/_Starlight/Recipes/Lathes/tools.yml b/Resources/Prototypes/_Starlight/Recipes/Lathes/tools.yml new file mode 100644 index 00000000000..b96a4104fbd --- /dev/null +++ b/Resources/Prototypes/_Starlight/Recipes/Lathes/tools.yml @@ -0,0 +1,20 @@ +- type: latheRecipe + parent: BaseToolRecipe + id: RPDEmpty + result: RPDEmpty + completetime: 4 + materials: + Steel: 1000 + Plastic: 300 + +- type: latheRecipe + parent: BaseToolRecipe + id: RPDRecharging + result: RPDRecharging + completetime: 8 + materials: + Steel: 1500 + Plastic: 500 + Uranium: 400 + Gold: 300 + Bluespace: 150 From e531a6620774f944ff204edc8053e099cd5a67b7 Mon Sep 17 00:00:00 2001 From: Dunrab Date: Wed, 17 Jun 2026 14:26:23 -0400 Subject: [PATCH 08/10] small fixes --- Content.Shared/RCD/Systems/RCDSystem.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Content.Shared/RCD/Systems/RCDSystem.cs b/Content.Shared/RCD/Systems/RCDSystem.cs index 6736bde6f3a..c88356bf771 100644 --- a/Content.Shared/RCD/Systems/RCDSystem.cs +++ b/Content.Shared/RCD/Systems/RCDSystem.cs @@ -26,12 +26,9 @@ using Content.Shared.Atmos.EntitySystems; using Content.Shared.Atmos.Components; using Content.Shared._Starlight.Atmos.EntitySystems; -using Content.Shared.Hands.Components; -using System.Numerics; using Content.Shared.Verbs; using Robust.Shared.Utility; using Content.Shared.NodeContainer; -using Content.Shared.Atmos; using Content.Shared._Starlight.Atmos; // Starlight End @@ -754,8 +751,6 @@ private bool IsDeconstructionStillValid(EntityUid uid, RCDComponent component, T // The object is not in the whitelist if (!deconstructible.Deconstructable) // Starlight Edit: RPD - Removed ``TryComp(target, out var deconstructible) || !`` - // The object is not in the whitelist - if (!deconstructible.Deconstructable) // Starlight Edit: RPD - Removed ``TryComp(target, out var deconstructible) || !`` { if (popMsgs) _popup.PopupClient(Loc.GetString("rcd-component-deconstruct-target-not-on-whitelist-message"), uid, user); @@ -776,7 +771,7 @@ private void FinalizeRCDOperation(EntityUid uid, RCDComponent component, EntityU if (!_net.IsServer) return; - var prototype = _protoManager.Index(component.ProtoId); + var prototype = component.CachedPrototype; // Starlight Edit: _protoManager.Index(component.ProtoId) -> component.CachedPrototype if (prototype.Prototype == null) return; From 93f10cad455379802e0dd5011fe04b312088d849 Mon Sep 17 00:00:00 2001 From: Dunrab Date: Wed, 17 Jun 2026 14:56:03 -0400 Subject: [PATCH 09/10] few more things i missed --- Content.Shared/RCD/Components/RCDComponent.cs | 9 +++++++++ Content.Shared/RCD/Systems/RCDSystem.cs | 1 + 2 files changed, 10 insertions(+) diff --git a/Content.Shared/RCD/Components/RCDComponent.cs b/Content.Shared/RCD/Components/RCDComponent.cs index 6a4d79c0893..f2f55dd01db 100644 --- a/Content.Shared/RCD/Components/RCDComponent.cs +++ b/Content.Shared/RCD/Components/RCDComponent.cs @@ -91,6 +91,15 @@ public Direction ConstructionDirection [DataField] public AtmosPipeLayer? LastSelectedLayer { get; set; } = null; + // Starlight Start + /// + /// Stores player rotation + /// This is a workaround to the fact eye rotation is not currently networked and required for pipe layering + /// Sent only when needed + /// + [DataField, AutoNetworkedField] + public float? LastKnownEyeRotation { get; set; } = null; + /// /// Current pipe layer / build mode for RPD /// diff --git a/Content.Shared/RCD/Systems/RCDSystem.cs b/Content.Shared/RCD/Systems/RCDSystem.cs index c88356bf771..07a68af6225 100644 --- a/Content.Shared/RCD/Systems/RCDSystem.cs +++ b/Content.Shared/RCD/Systems/RCDSystem.cs @@ -130,6 +130,7 @@ private void OnRCDSystemMessage(EntityUid uid, RCDComponent component, RCDSystem // Set the current RCD prototype to the one supplied component.ProtoId = args.ProtoId; + UpdateCachedPrototype(uid, component); // Starlight: RPD _adminLogger.Add(LogType.RCD, LogImpact.Low, $"{args.Actor} set RCD mode to: {prototype.Mode} : {prototype.Prototype}"); From 25124bf5947190500610bc6fb71ee78c6ea02f12 Mon Sep 17 00:00:00 2001 From: Dunrab Date: Wed, 17 Jun 2026 16:38:22 -0400 Subject: [PATCH 10/10] fix layering bug --- Content.Shared/RCD/Systems/RCDSystem.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Content.Shared/RCD/Systems/RCDSystem.cs b/Content.Shared/RCD/Systems/RCDSystem.cs index 07a68af6225..1c39154a10d 100644 --- a/Content.Shared/RCD/Systems/RCDSystem.cs +++ b/Content.Shared/RCD/Systems/RCDSystem.cs @@ -647,6 +647,12 @@ private bool IsConstructionLocationValid(EntityUid uid, RCDComponent component, isIdentical = false; } + // Floofstation - RPD Fix, if the RPD is used and the prototype its placing has a layer, allow it to be placed + if (component.IsRpd && prototype.HasLayers) + { + isIdentical = false; + } + if (isIdentical) { if (popMsgs)