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 b/Content.Client/_Starlight/RCD/AlignRPDAtmosPipeLayers.CS new file mode 100644 index 00000000000..0d4ff710ab8 --- /dev/null +++ b/Content.Client/_Starlight/RCD/AlignRPDAtmosPipeLayers.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..f2f55dd01db 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,40 @@ 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; + + // 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 + /// + [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..1c39154a10d 100644 --- a/Content.Shared/RCD/Systems/RCDSystem.cs +++ b/Content.Shared/RCD/Systems/RCDSystem.cs @@ -22,6 +22,15 @@ 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.Verbs; +using Robust.Shared.Utility; +using Content.Shared.NodeContainer; +using Content.Shared._Starlight.Atmos; +// Starlight End namespace Content.Shared.RCD.Systems; @@ -44,6 +53,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 +78,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 +94,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 +109,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 @@ -94,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}"); @@ -105,7 +142,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 +163,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 +234,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 +254,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 +348,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 +427,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 +453,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 +504,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 +543,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 +551,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) @@ -464,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) @@ -509,11 +698,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 +745,19 @@ 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) || !`` { if (popMsgs) _popup.PopupClient(Loc.GetString("rcd-component-deconstruct-target-not-on-whitelist-message"), uid, user); @@ -564,12 +773,12 @@ 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; - var prototype = _protoManager.Index(component.ProtoId); + var prototype = component.CachedPrototype; // Starlight Edit: _protoManager.Index(component.ProtoId) -> component.CachedPrototype if (prototype.Prototype == null) return; @@ -585,7 +794,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 +897,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 +941,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 +955,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/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 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/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 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/_DEN/Entities/Objects/Misc/admin.yml b/Resources/Prototypes/_DEN/Entities/Objects/Misc/admin.yml index 4a521a1a35e..713bd7cf7b3 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 @@ -70,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/_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/Entities/Objects/Tools/tools.yml b/Resources/Prototypes/_Starlight/Entities/Objects/Tools/tools.yml index ca0f866e93d..f202927e03a 100644 --- a/Resources/Prototypes/_Starlight/Entities/Objects/Tools/tools.yml +++ b/Resources/Prototypes/_Starlight/Entities/Objects/Tools/tools.yml @@ -712,3 +712,87 @@ - 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: 30 # match RCD charges since this also uses RCD ammo + - 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 + 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 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/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 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 00000000000..14929b0d7b7 Binary files /dev/null and b/Resources/Textures/_Starlight/Interface/Radial/RPD/airalarm.png differ 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 00000000000..c1a10f00200 Binary files /dev/null and b/Resources/Textures/_Starlight/Interface/Radial/RPD/airsensor.png differ 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 00000000000..c62cf62220d Binary files /dev/null and b/Resources/Textures/_Starlight/Interface/Radial/RPD/bend.png differ diff --git a/Resources/Textures/_Starlight/Interface/Radial/RPD/dual_port.png b/Resources/Textures/_Starlight/Interface/Radial/RPD/dual_port.png new file mode 100644 index 00000000000..b2e63dd51ed Binary files /dev/null and b/Resources/Textures/_Starlight/Interface/Radial/RPD/dual_port.png differ 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 00000000000..336d930022d Binary files /dev/null and b/Resources/Textures/_Starlight/Interface/Radial/RPD/fourway.png differ 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 00000000000..35096091a82 Binary files /dev/null and b/Resources/Textures/_Starlight/Interface/Radial/RPD/gas_filter.png differ diff --git a/Resources/Textures/_Starlight/Interface/Radial/RPD/gas_mixer.png b/Resources/Textures/_Starlight/Interface/Radial/RPD/gas_mixer.png new file mode 100644 index 00000000000..0ef1bb69a48 Binary files /dev/null and b/Resources/Textures/_Starlight/Interface/Radial/RPD/gas_mixer.png differ diff --git a/Resources/Textures/_Starlight/Interface/Radial/RPD/half.png b/Resources/Textures/_Starlight/Interface/Radial/RPD/half.png new file mode 100644 index 00000000000..79676d66534 Binary files /dev/null and b/Resources/Textures/_Starlight/Interface/Radial/RPD/half.png differ 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 00000000000..b38c170b02c Binary files /dev/null and b/Resources/Textures/_Starlight/Interface/Radial/RPD/injector.png differ diff --git a/Resources/Textures/_Starlight/Interface/Radial/RPD/manifold.png b/Resources/Textures/_Starlight/Interface/Radial/RPD/manifold.png new file mode 100644 index 00000000000..4282408d064 Binary files /dev/null and b/Resources/Textures/_Starlight/Interface/Radial/RPD/manifold.png differ diff --git a/Resources/Textures/_Starlight/Interface/Radial/RPD/passive_gate.png b/Resources/Textures/_Starlight/Interface/Radial/RPD/passive_gate.png new file mode 100644 index 00000000000..71dfd74634a Binary files /dev/null and b/Resources/Textures/_Starlight/Interface/Radial/RPD/passive_gate.png differ diff --git a/Resources/Textures/_Starlight/Interface/Radial/RPD/pneumatic_valve.png b/Resources/Textures/_Starlight/Interface/Radial/RPD/pneumatic_valve.png new file mode 100644 index 00000000000..0ee9ddf3388 Binary files /dev/null and b/Resources/Textures/_Starlight/Interface/Radial/RPD/pneumatic_valve.png differ diff --git a/Resources/Textures/_Starlight/Interface/Radial/RPD/port.png b/Resources/Textures/_Starlight/Interface/Radial/RPD/port.png new file mode 100644 index 00000000000..93a3d5fb6f2 Binary files /dev/null and b/Resources/Textures/_Starlight/Interface/Radial/RPD/port.png differ diff --git a/Resources/Textures/_Starlight/Interface/Radial/RPD/pressure_regulator.png b/Resources/Textures/_Starlight/Interface/Radial/RPD/pressure_regulator.png new file mode 100644 index 00000000000..0cc6cd55c80 Binary files /dev/null and b/Resources/Textures/_Starlight/Interface/Radial/RPD/pressure_regulator.png differ 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 00000000000..c2860045a33 Binary files /dev/null and b/Resources/Textures/_Starlight/Interface/Radial/RPD/pump_manual_valve.png differ 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 00000000000..aee0831c849 Binary files /dev/null and b/Resources/Textures/_Starlight/Interface/Radial/RPD/pump_pressure.png differ diff --git a/Resources/Textures/_Starlight/Interface/Radial/RPD/pump_signal_valve.png b/Resources/Textures/_Starlight/Interface/Radial/RPD/pump_signal_valve.png new file mode 100644 index 00000000000..6f5955990f9 Binary files /dev/null and b/Resources/Textures/_Starlight/Interface/Radial/RPD/pump_signal_valve.png differ 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 00000000000..a3110ec00e3 Binary files /dev/null and b/Resources/Textures/_Starlight/Interface/Radial/RPD/pump_volume.png differ 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 00000000000..420d68bc733 Binary files /dev/null and b/Resources/Textures/_Starlight/Interface/Radial/RPD/radiator.png differ diff --git a/Resources/Textures/_Starlight/Interface/Radial/RPD/radiator_bend.png b/Resources/Textures/_Starlight/Interface/Radial/RPD/radiator_bend.png new file mode 100644 index 00000000000..cbd90b8416c Binary files /dev/null and b/Resources/Textures/_Starlight/Interface/Radial/RPD/radiator_bend.png differ 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 00000000000..e6da8871ac2 Binary files /dev/null and b/Resources/Textures/_Starlight/Interface/Radial/RPD/scrub_off.png differ 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 00000000000..6c38f004fa3 Binary files /dev/null and b/Resources/Textures/_Starlight/Interface/Radial/RPD/sensor.png differ 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 00000000000..46239651339 Binary files /dev/null and b/Resources/Textures/_Starlight/Interface/Radial/RPD/straight.png differ diff --git a/Resources/Textures/_Starlight/Interface/Radial/RPD/tjunction.png b/Resources/Textures/_Starlight/Interface/Radial/RPD/tjunction.png new file mode 100644 index 00000000000..c537c0ab762 Binary files /dev/null and b/Resources/Textures/_Starlight/Interface/Radial/RPD/tjunction.png differ 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 00000000000..c54c3bb519c Binary files /dev/null and b/Resources/Textures/_Starlight/Interface/Radial/RPD/vent_off.png differ diff --git a/Resources/Textures/_Starlight/Interface/Radial/RPD/vent_passive.png b/Resources/Textures/_Starlight/Interface/Radial/RPD/vent_passive.png new file mode 100644 index 00000000000..cc63ae45041 Binary files /dev/null and b/Resources/Textures/_Starlight/Interface/Radial/RPD/vent_passive.png differ diff --git a/Resources/Textures/_Starlight/Objects/Tools/rpd.rsi/icon.png b/Resources/Textures/_Starlight/Objects/Tools/rpd.rsi/icon.png new file mode 100644 index 00000000000..61bc6ccf1d4 Binary files /dev/null and b/Resources/Textures/_Starlight/Objects/Tools/rpd.rsi/icon.png differ diff --git a/Resources/Textures/_Starlight/Objects/Tools/rpd.rsi/inhand-left.png b/Resources/Textures/_Starlight/Objects/Tools/rpd.rsi/inhand-left.png new file mode 100644 index 00000000000..d80f8abf1f1 Binary files /dev/null and b/Resources/Textures/_Starlight/Objects/Tools/rpd.rsi/inhand-left.png differ diff --git a/Resources/Textures/_Starlight/Objects/Tools/rpd.rsi/inhand-right.png b/Resources/Textures/_Starlight/Objects/Tools/rpd.rsi/inhand-right.png new file mode 100644 index 00000000000..951a323c11b Binary files /dev/null and b/Resources/Textures/_Starlight/Objects/Tools/rpd.rsi/inhand-right.png differ diff --git a/Resources/Textures/_Starlight/Objects/Tools/rpd.rsi/meta.json b/Resources/Textures/_Starlight/Objects/Tools/rpd.rsi/meta.json new file mode 100644 index 00000000000..ebd5996c426 --- /dev/null +++ b/Resources/Textures/_Starlight/Objects/Tools/rpd.rsi/meta.json @@ -0,0 +1,25 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "State based Copyright", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon", + "copyright": "Taken from CEV-Eris at https://github.com/discordia-space/CEV-Eris/blob/8009c1d3db5ca5244f14899ffafaae93be0e318d/icons/obj/items.dmi" + }, + { + "name": "inhand-left", + "copyright": "Taken from CEV-Eris at https://github.com/discordia-space/CEV-Eris/blob/df81fe2d0ba5f541bafffbb73554f3f44289dd76/icons/mob/items/righthand.dmi", + "directions": 4 + }, + { + "name": "inhand-right", + "copyright": "Taken from CEV-Eris at https://github.com/discordia-space/CEV-Eris/blob/df81fe2d0ba5f541bafffbb73554f3f44289dd76/icons/mob/items/righthand.dmi", + "directions": 4 + } + ] +} \ No newline at end of file