This commit is contained in:
zelezniciar1 2026-05-10 11:43:49 +00:00 committed by GitHub
commit 257c89b5b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
56 changed files with 1428 additions and 54 deletions

View File

@ -1,11 +1,15 @@
using Content.Client.Hands.Systems;
using Content.Shared.Input; //DeltaV - RPD
using Content.Shared.Interaction;
using Content.Shared.RCD;
using Content.Shared.RCD.Components;
using Robust.Client.Placement;
using Robust.Client.Player;
using Robust.Shared.Enums;
using Robust.Shared.Input; //DeltaV - RPD
using Robust.Shared.Input.Binding; //DeltaV - RPD
using Robust.Shared.Prototypes;
using Content.Client.Atmos; //DeltaV - RPD
namespace Content.Client.RCD;
@ -15,6 +19,7 @@ namespace Content.Client.RCD;
public sealed class RCDConstructionGhostSystem : EntitySystem
{
private const string PlacementMode = nameof(AlignRCDConstruction);
private const string RpdPlacementMode = nameof(AlignRPDAtmosPipeLayers); //DeltaV - RPD
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPlacementManager _placementManager = default!;
@ -22,6 +27,50 @@ public sealed class RCDConstructionGhostSystem : EntitySystem
[Dependency] private readonly HandsSystem _hands = default!;
private Direction _placementDirection = default;
//DeltaV - RPD Begin
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<RCDConstructionGhostSystem>();
}
public override void Shutdown()
{
CommandBinds.Unregister<RCDConstructionGhostSystem>();
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<RCDComponent>(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;
}
//DeltaV - RPD End
public override void Update(float frameTime)
{
@ -55,7 +104,19 @@ public sealed class RCDConstructionGhostSystem : EntitySystem
return;
}
var prototype = _protoManager.Index(rcd.ProtoId);
//DeltaV - RPD Begin
// 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<RCDPrototype>(cachedProto.ID, out var rcdProto)
&& rcdProto.HasLayers;
var desiredMode = isLayered ? RpdPlacementMode : PlacementMode;
//DeltaV - RPD End
// Update the direction the RCD prototype based on the placer direction
if (_placementDirection != _placementManager.Direction)
@ -64,18 +125,22 @@ public sealed class RCDConstructionGhostSystem : EntitySystem
RaiseNetworkEvent(new RCDConstructionGhostRotationEvent(GetNetEntity(heldEntity.Value), _placementDirection));
}
//DeltaV - RPD Begin
// If the placer has not changed, exit
if (heldEntity == placerEntity && prototype.Prototype == placerProto)
if (heldEntity == placerEntity &&
prototype == placerProto &&
_placementManager.CurrentPermission?.PlacementOption == desiredMode)
//DeltaV - RPD End
return;
// Create a new placer
var newObjInfo = new PlacementInformation
{
MobUid = heldEntity.Value,
PlacementOption = PlacementMode,
EntityType = prototype.Prototype,
PlacementOption = desiredMode, //DeltaV - RPD
EntityType = prototype,
Range = (int)Math.Ceiling(SharedInteractionSystem.InteractionRange),
IsTile = (prototype.Mode == RcdMode.ConstructTile),
IsTile = (cachedProto.Mode == RcdMode.ConstructTile), //DeltaV - RPD
UseEditorContext = false,
};

View File

@ -24,8 +24,15 @@ 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"))),
["Piping"] = ("rcd-component-piping", new SpriteSpecifier.Texture(new ResPath("/Textures/_DV/Interface/Radial/RPD/fourway.png"))), //DeltaV - RPD
["AtmosphericUtility"] = ("rcd-component-atmospheric-utility", new SpriteSpecifier.Texture(new ResPath("/Textures/_DV/Interface/Radial/RPD/port.png"))), //DeltaV - RPD
["PumpsValves"] = ("rcd-component-pumps-valves", new SpriteSpecifier.Texture(new ResPath("/Textures/_DV/Interface/Radial/RPD/pump_volume.png"))), //DeltaV - RPD
["Vents"] = ("rcd-component-vents", new SpriteSpecifier.Texture(new ResPath("/Textures/_DV/Interface/Radial/RPD/vent_passive.png"))), //DeltaV - RPD
["SensorsMonitors"] = ("rcd-component-sensors-monitors", new SpriteSpecifier.Texture(new ResPath("/Textures/_DV/Interface/Radial/RPD/alarm.png"))), //DeltaV - RPD
};
private bool IsRpd => EntMan.TryGetComponent<RCDComponent>(Owner, out var rcd) && rcd.IsRpd; //DeltaV - RPD
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly ISharedPlayerManager _playerManager = default!;

View File

@ -0,0 +1,286 @@
using Content.Client.Gameplay;
using Content.Client.Hands.Systems;
using Content.Shared.Atmos.Components;
using Content.Shared.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;
namespace Content.Client.RCD;
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// This placement mode is not on the engine because it is content specific.
/// </remarks>
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 float? _currentEyeRotation = null;
private Color _guideColor = new(0, 0, 0.5785f);
public AlignRPDAtmosPipeLayers(PlacementManager pMan) : base(pMan)
{
IoCManager.InjectDependencies(this);
_mapSystem = _entityManager.System<SharedMapSystem>();
_transformSystem = _entityManager.System<SharedTransformSystem>();
_spriteSystem = _entityManager.System<SpriteSystem>();
_rcdSystem = _entityManager.System<RCDSystem>();
_pipeLayersSystem = _entityManager.System<SharedAtmosPipeLayersSystem>();
_handsSystem = _entityManager.System<HandsSystem>();
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<TransformComponent>(player, out var xform) ||
!_transformSystem.InRange(xform.Coordinates, MouseCoords, SharedInteractionSystem.InteractionRange))
{
return;
}
var gridUid = _transformSystem.GetGrid(MouseCoords);
if (gridUid == null || !_entityManager.TryGetComponent<MapGridComponent>(gridUid, out var grid))
return;
if (!_handsSystem.TryGetActiveItem(player, out var heldEntity) ||
!_entityManager.TryGetComponent<RCDComponent>(heldEntity, out var rcd))
return;
// Draw guide circles for each pipe layer if we are not in line/grid placing mode
if (rcd.CurrentMode == 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<MapGridComponent>(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<RCDComponent>(heldEntity, out var rcd) || !rcd.IsRpd)
return;
if (!_entityManager.TryGetComponent<TransformComponent>(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 RpdMode.Primary:
newLayer = AtmosPipeLayer.Primary;
break;
case RpdMode.Secondary:
newLayer = AtmosPipeLayer.Secondary;
break;
case RpdMode.Tertiary:
newLayer = AtmosPipeLayer.Tertiary;
break;
case 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 == RpdMode.Free)
UpdateEyeRotation(heldEntity.Value, _eyeManager.CurrentEye.Rotation);
UpdatePlacer(_currentLayer);
}
// Since player eye rotation isn't networked and there is a comment warning against doing so,
// we need a way of sending current eye rotation to the rpd for correct layer placement.
// I'm sure there's a better solution for this but I haven't found it
private void UpdateEyeRotation(EntityUid heldEntity, Angle eyeRotation)
{
if (_currentEyeRotation != eyeRotation.Theta)
{
_currentEyeRotation = (float) eyeRotation.Theta;
_entityNetwork.SendSystemNetworkMessage(new RPDEyeRotationEvent(_entityManager.GetNetEntity(heldEntity), _currentEyeRotation));
}
}
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<EntityPrototype>(pManager.CurrentPermission.EntityType, out var currentProto))
return;
if (!currentProto.TryGetComponent<AtmosPipeLayersComponent>(out var atmosPipeLayers, _entityManager.ComponentFactory))
return;
if (!_pipeLayersSystem.TryGetAlternativePrototype(atmosPipeLayers, layer, out var newProtoId))
return;
if (_protoManager.TryIndex<EntityPrototype>(newProtoId, out var newProto))
{
// Update the placed prototype
pManager.CurrentPermission.EntityType = newProtoId;
// Update the appearance of the ghost sprite
if (newProto.TryGetComponent<SpriteComponent>(out var sprite, _entityManager.ComponentFactory))
{
var textures = new List<IDirectionalTextureProvider>();
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<TransformComponent>(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<RCDComponent>(heldEntity, out var rcd))
return false;
var gridUid = _transformSystem.GetGrid(position);
if (!_entityManager.TryGetComponent<MapGridComponent>(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;
}
}

View File

@ -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.RCD.Systems;
public sealed class RPDSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
Subs.ItemStatus<RCDComponent>(OnItemStatus);
}
private Control OnItemStatus(Entity<RCDComponent> 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<RCDComponent> entity)
{
_uid = entity.Owner;
_isRpd = entity.Comp.IsRpd;
_rcdSystem = EntitySystem.Get<RCDSystem>();
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]")));
}
}
}

View File

@ -14,7 +14,7 @@ namespace Content.Server.NodeContainer.Nodes
/// </summary>
[DataDefinition]
[Virtual]
public partial class PipeNode : Node, IGasMixtureHolder, IRotatableNode
public partial class PipeNode : Node, IGasMixtureHolder, IRotatableNode, IPipeNode //DeltaV - RPD
{
/// <summary>
/// The directions in which this pipe can connect to other pipes around it.
@ -236,5 +236,8 @@ namespace Content.Server.NodeContainer.Nodes
}
}
}
PipeDirection IPipeNode.Direction => OriginalPipeDirection; //DeltaV - RPD
AtmosPipeLayer IPipeNode.Layer => CurrentPipeLayer; //DeltaV - RPD
}
}

View File

@ -1,6 +1,6 @@
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos.EntitySystems;
namespace Content.Server.Atmos.Components;
namespace Content.Shared.Atmos.Components;
/// <summary>
/// This is used for restricting anchoring pipes so that they do not overlap.

View File

@ -1,30 +1,33 @@
using System.Linq;
using Content.Server.Atmos.Components;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Popups;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Construction.Components;
using Content.Shared.NodeContainer;
using Content.Shared.Popups;
using Content.Shared.Construction.Components;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Map.Components;
namespace Content.Server.Atmos.EntitySystems;
namespace Content.Shared.Atmos.EntitySystems;
/// <summary>
/// This handles restricting pipe-based entities from overlapping outlets/inlets with other entities.
/// </summary>
public sealed class PipeRestrictOverlapSystem : EntitySystem
{
[Dependency] private readonly MapSystem _map = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly TransformSystem _xform = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedTransformSystem _xform = default!;
private readonly List<EntityUid> _anchoredEntities = new();
private EntityQuery<NodeContainerComponent> _nodeContainerQuery;
public readonly record struct ProposedPipe(
PipeDirection Direction,
AtmosPipeLayer Layer,
Angle Rotation = default);
/// <inheritdoc/>
public override void Initialize()
{
@ -116,10 +119,62 @@ public sealed class PipeRestrictOverlapSystem : EntitySystem
{
foreach (var node in pipe.Comp1.Nodes.Values)
{
// we need to rotate the pipe manually like this because the rotation doesn't update for pipes that are unanchored.
if (node is PipeNode pipeNode)
yield return (pipeNode.OriginalPipeDirection.RotatePipeDirection(pipe.Comp2.LocalRotation), pipeNode.CurrentPipeLayer);
if (node is IPipeNode pipeNode)
yield return (pipeNode.Direction.RotatePipeDirection(pipe.Comp2.LocalRotation), pipeNode.Layer);
}
}
}
/// <summary>
/// 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.
/// </summary>
public EntityUid? CheckIfWouldConflict(EntityUid gridUid,
Vector2i tileIndices,
ProposedPipe proposed,
EntityUid? ignoreEntity = null)
{
if (!TryComp<MapGridComponent>(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<NodeContainerComponent, TransformComponent> 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);
}
}
}

View File

@ -1,4 +1,5 @@
using Content.Shared.Charges.Systems;
using Content.Shared.FixedPoint; //DeltaV - RPD
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;

View File

@ -1,5 +1,6 @@
using Content.Shared.RCD.Systems;
using Robust.Shared.GameStates;
using Content.Shared.FixedPoint; //DeltaV - RPD
namespace Content.Shared.RCD.Components;

View File

@ -3,6 +3,7 @@ using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Physics;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization; //DeltaV - RPD
namespace Content.Shared.RCD.Components;
@ -33,6 +34,29 @@ public sealed partial class RCDComponent : Component
[DataField, AutoNetworkedField]
public ProtoId<RCDPrototype> ProtoId { get; set; } = "Invalid";
//DeltaV - RPD Begin
/// <summary>
/// A cached copy of currently selected RCD prototype
/// </summary>
/// <remarks>
/// If the ProtoId is changed, make sure to update the CachedPrototype as well
/// </remarks>
[ViewVariables(VVAccess.ReadOnly)]
public RCDPrototype CachedPrototype { get; set; } = default!;
/// <summary>
/// Indicates if a mirrored version of the construction prototype should be used (if available)
/// </summary>
[AutoNetworkedField, ViewVariables(VVAccess.ReadOnly)]
public bool UseMirrorPrototype = false;
/// <summary>
/// Indicates whether this is an RCD or an RPD
/// </summary>
[DataField, AutoNetworkedField]
public bool IsRpd { get; set; } = false;
//DeltaV - RPD End
/// <summary>
/// The direction constructed entities will face upon spawning
/// </summary>
@ -57,4 +81,32 @@ public sealed partial class RCDComponent : Component
/// </remarks>
[ViewVariables(VVAccess.ReadOnly)]
public Transform ConstructionTransform { get; private set; }
//DeltaV - RPD Begin
/// <summary>
/// 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
/// </summary>
[DataField, AutoNetworkedField]
public float? LastKnownEyeRotation { get; set; } = null;
/// <summary>
/// Current pipe layer / build mode for RPD
/// </summary>
[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,
}
//DeltaV - RPD End

View File

@ -1,6 +1,7 @@
using Content.Shared.RCD.Systems;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Content.Shared.FixedPoint; //DeltaV - RPD
namespace Content.Shared.RCD.Components;
@ -31,4 +32,12 @@ public sealed partial class RCDDeconstructableComponent : Component
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public bool Deconstructable = true;
//DeltaV - RPD Begin
/// <summary>
/// Toggles whether this entity is deconstructable by the RPD or not
/// </summary>
[DataField("rpd"), ViewVariables(VVAccess.ReadWrite)]
public bool RpdDeconstructable = false;
//DeltaV - RPD End
}

View File

@ -15,7 +15,32 @@ public sealed class RCDConstructionGhostRotationEvent(NetEntity netEntity, Direc
public readonly NetEntity NetEntity = netEntity;
public readonly Direction Direction = direction;
}
//DeltaV - RPD Begin
[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 RPDEyeRotationEvent : EntityEventArgs
{
public readonly NetEntity NetEntity;
public float? EyeRotation;
public RPDEyeRotationEvent(NetEntity netEntity, float? eyeRotation)
{
NetEntity = netEntity;
EyeRotation = eyeRotation;
}
}
//DeltaV - RPD End
[Serializable, NetSerializable]
public enum RcdUiKey : byte
{

View File

@ -2,6 +2,7 @@ using Content.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using Content.Shared.FixedPoint; //DeltaV - RPD
namespace Content.Shared.RCD;
@ -50,6 +51,14 @@ public sealed partial class RCDPrototype : IPrototype
[DataField, ViewVariables(VVAccess.ReadOnly)]
public bool AllowMultiDirection { get; private set; }
// DeltaV - RPD
/// <summary>
/// If the entity can be flipped, this prototype is available as an alternate (mode dependent)
/// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)]
public string? MirrorPrototype { get; private set; } = string.Empty;
// End DeltaV - RPD
/// <summary>
/// Number of charges consumed when the operation is completed
/// </summary>
@ -119,6 +128,14 @@ public sealed partial class RCDPrototype : IPrototype
/// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)]
public RcdRotation Rotation { get; private set; } = RcdRotation.User;
//DeltaV - RPD Begin
/// <summary>
/// Determines whether this prototype uses layered placement (true for traditional placement, false for layered). Only applies to RPD.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)]
public bool HasLayers { get; private set; } = false;
//DeltaV - RPD End
}
public enum RcdMode : byte

View File

@ -1,6 +1,7 @@
using Content.Shared.Charges.Components;
using Content.Shared.Charges.Systems;
using Content.Shared.Examine;
using Content.Shared.FixedPoint; //DeltaV - RPD
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.RCD.Components;

View File

@ -22,6 +22,14 @@ using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using System.Linq;
using Content.Shared.Atmos.EntitySystems; //DeltaV - RPD
using Content.Shared.Atmos.Components; //DeltaV - RPD
using Content.Shared.Hands.Components; //DeltaV - RPD
using System.Numerics; //DeltaV - RPD
using Content.Shared.Verbs; //DeltaV - RPD
using Robust.Shared.Utility; //DeltaV - RPD
using Content.Shared.NodeContainer; //DeltaV - RPD
using Content.Shared.Atmos; //DeltaV - RPD
namespace Content.Shared.RCD.Systems;
@ -44,6 +52,9 @@ public sealed class RCDSystem : EntitySystem
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly TagSystem _tags = default!;
[Dependency] private readonly SharedAtmosPipeLayersSystem _pipeLayersSystem = default!; //DeltaV - RPD
[Dependency] private readonly IEntityManager _entityManager = default!; //DeltaV - RPD
[Dependency] private readonly PipeRestrictOverlapSystem _pipeOverlap = default!; //DeltaV - RPD
private readonly int _instantConstructionDelay = 0;
private readonly EntProtoId _instantConstructionFx = "EffectRCDConstruct0";
@ -52,28 +63,37 @@ public sealed class RCDSystem : EntitySystem
private static readonly ProtoId<TagPrototype> CatwalkTag = "Catwalk";
private HashSet<EntityUid> _intersectingEntities = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RCDComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<RCDComponent, ComponentStartup>(OnStartup); //DeltaV - RPD
SubscribeLocalEvent<RCDComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<RCDComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<RCDComponent, RCDDoAfterEvent>(OnDoAfter);
SubscribeLocalEvent<RCDComponent, DoAfterAttemptEvent<RCDDoAfterEvent>>(OnDoAfterAttempt);
SubscribeLocalEvent<RCDComponent, RCDSystemMessage>(OnRCDSystemMessage);
SubscribeNetworkEvent<RCDConstructionGhostRotationEvent>(OnRCDconstructionGhostRotationEvent);
SubscribeNetworkEvent<RCDConstructionGhostFlipEvent>(OnRCDConstructionGhostFlipEvent); //DeltaV - RPD
SubscribeNetworkEvent<RPDEyeRotationEvent>(OnRPDEyeRotationEvent); //DeltaV - RPD
SubscribeLocalEvent<RCDComponent, GetVerbsEvent<UtilityVerb>>(OnGetUtilityVerb); //DeltaV - RPD
SubscribeLocalEvent<RCDComponent, GetVerbsEvent<AlternativeVerb>>(OnGetAlternativeVerb); //DeltaV - RPD
}
#region Event handling
private void OnMapInit(EntityUid uid, RCDComponent component, MapInitEvent args)
{
// On init, set the RCD to its first available recipe
// On map startup, set the RCD to its first available recipe
if (component.AvailablePrototypes.Count > 0)
{
//DeltaV - RPD Begin
if (component.IsRpd)
component.ProtoId = "PipeStraight";
else
component.ProtoId = component.AvailablePrototypes.ElementAt(0);
//DeltaV - RPD End
Dirty(uid, component);
return;
@ -83,6 +103,16 @@ public sealed class RCDSystem : EntitySystem
QueueDel(uid);
}
//DeltaV - RPD Begin
private void OnStartup(EntityUid uid, RCDComponent component, ComponentStartup args)
{
UpdateCachedPrototype(uid, component);
Dirty(uid, component);
return;
}
//DeltaV - RPD End
private void OnRCDSystemMessage(EntityUid uid, RCDComponent component, RCDSystemMessage args)
{
// Exit if the RCD doesn't actually know the supplied prototype
@ -94,6 +124,7 @@ public sealed class RCDSystem : EntitySystem
// Set the current RCD prototype to the one supplied
component.ProtoId = args.ProtoId;
UpdateCachedPrototype(uid, component); //DeltaV - RPD
_adminLogger.Add(LogType.RCD, LogImpact.Low, $"{args.Actor} set RCD mode to: {prototype.Mode} : {prototype.Prototype}");
@ -105,7 +136,8 @@ public sealed class RCDSystem : EntitySystem
if (!args.IsInDetailsRange)
return;
var prototype = _protoManager.Index(component.ProtoId);
UpdateCachedPrototype(uid, component); //DeltaV - RPD
var prototype = component.CachedPrototype; //DeltaV - RPD
var msg = Loc.GetString("rcd-component-examine-mode-details", ("mode", Loc.GetString(prototype.SetName)));
@ -121,8 +153,70 @@ public sealed class RCDSystem : EntitySystem
}
args.PushMarkup(msg);
//DeltaV - RPD Begin
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 OnRPDEyeRotationEvent(RPDEyeRotationEvent ev, EntitySessionEventArgs session)
{
var uid = GetEntity(ev.NetEntity);
if (session.SenderSession.AttachedEntity is not { } player)
return;
if (_hands.GetActiveItem(player) != uid)
return;
if (!TryComp<RCDComponent>(uid, out var rcd))
return;
// Update the layer if different
if (rcd.LastKnownEyeRotation != ev.EyeRotation)
{
rcd.LastKnownEyeRotation = ev.EyeRotation;
}
}
private void OnGetUtilityVerb(EntityUid uid, RCDComponent component, GetVerbsEvent<UtilityVerb> 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<AlternativeVerb> 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);
}
//DeltaV - RPD End
private void OnAfterInteract(EntityUid uid, RCDComponent component, AfterInteractEvent args)
{
if (args.Handled || !args.CanReach)
@ -130,7 +224,8 @@ public sealed class RCDSystem : EntitySystem
var user = args.User;
var location = args.ClickLocation;
var prototype = _protoManager.Index(component.ProtoId);
var prototype = component.CachedPrototype; //DeltaV - RPD
// Initial validity checks
if (!location.IsValid(EntityManager))
@ -145,6 +240,46 @@ public sealed class RCDSystem : EntitySystem
}
var tile = _mapSystem.GetTileRef(gridUid.Value, mapGrid, location);
var position = _mapSystem.TileIndicesFor(gridUid.Value, mapGrid, location);
//DeltaV - RPD Begin
var layer = AtmosPipeLayer.Primary;
if (component.IsRpd && prototype.HasLayers)
{
var tileSize = mapGrid.TileSize;
var tileCenter = new Vector2(tile.X + tileSize / 2, tile.Y + tileSize / 2);
var mouseCoordsDiff = args.ClickLocation.Position - tileCenter - new Vector2(0.5f, 0.5f);
var mouseDeadzoneRadius = 0.25f;
switch (component.CurrentMode)
{
case RpdMode.Primary:
layer = AtmosPipeLayer.Primary;
break;
case RpdMode.Secondary:
layer = AtmosPipeLayer.Secondary;
break;
case RpdMode.Tertiary:
layer = AtmosPipeLayer.Tertiary;
break;
case RpdMode.Free:
// Only use mouse direction in Free mode
if (mouseCoordsDiff.Length() > mouseDeadzoneRadius && component.LastKnownEyeRotation.HasValue)
{
var gridRotation = _transform.GetWorldRotation(gridUid.Value);
var angle = new Angle(mouseCoordsDiff);
var eyeRotation = new Angle(component.LastKnownEyeRotation.Value);
var direction = (angle + eyeRotation + gridRotation + Math.PI / 2).GetCardinalDir();
layer = (direction == Direction.North || direction == Direction.East)
? AtmosPipeLayer.Secondary
: AtmosPipeLayer.Tertiary;
}
break;
}
}
//DeltaV - RPD End
if (!IsRCDOperationStillValid(uid, component, gridUid.Value, mapGrid, tile, position, component.ConstructionDirection, args.Target, args.User))
return;
@ -209,7 +344,7 @@ public sealed class RCDSystem : EntitySystem
// 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, component.ProtoId, cost, layer, GetNetEntity(effect)); //DeltaV - RPD
var doAfterArgs = new DoAfterArgs(EntityManager, user, delay, ev, uid, target: args.Target, used: uid)
{
@ -288,7 +423,7 @@ public sealed class RCDSystem : EntitySystem
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.Target, args.User, args.Layer);
// Play audio and consume charges
_audio.PlayPredicted(component.SuccessSound, uid, args.User);
@ -313,7 +448,45 @@ public sealed class RCDSystem : EntitySystem
rcd.ConstructionDirection = ev.Direction;
Dirty(uid, rcd);
}
//DeltaV - RPD Begin
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<RCDComponent>(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
{
RpdMode.Primary => RpdMode.Secondary,
RpdMode.Secondary => RpdMode.Tertiary,
RpdMode.Tertiary => RpdMode.Free,
RpdMode.Free => RpdMode.Primary,
_ => RpdMode.Free
};
Dirty(uid, component);
if (user != null)
_audio.PlayPredicted(component.SoundSwitchMode, uid, user.Value);
}
//DeltaV - RPD End
#endregion
#region Entity construction/deconstruction rule checks
@ -325,7 +498,10 @@ public sealed class RCDSystem : EntitySystem
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);
// Update cached prototype if required
UpdateCachedPrototype(uid, component); //DeltaV - RPD
var prototype = component.CachedPrototype; //DeltaV - RPD
// Check that the RCD has enough ammo to get the job done
var charges = _sharedCharges.GetCurrentCharges(uid);
@ -347,30 +523,86 @@ public sealed class RCDSystem : EntitySystem
return false;
}
// Exit if the target / target location is obstructed
var unobstructed = (target == null)
? _interaction.InRangeUnobstructed(user, _mapSystem.GridTileToWorld(gridUid, mapGrid, position), popup: popMsgs)
// DeltaV - RPD Begin
bool unobstructed;
var worldPos = _mapSystem.GridTileToWorld(gridUid, mapGrid, position);
if (component.IsRpd)
{
unobstructed = _interaction.InRangeUnobstructed(user, worldPos,
predicate: (entity) => !HasComp<PipeRestrictOverlapComponent>(entity),
popup: popMsgs);
}
else
{
unobstructed = (target == null)
? _interaction.InRangeUnobstructed(user, worldPos, popup: popMsgs)
: _interaction.InRangeUnobstructed(user, target.Value, popup: popMsgs);
}
// DeltaV - RPD End
if (!unobstructed)
return false;
// DeltaV - RPD Begin
var layer = AtmosPipeLayer.Primary;
if (component.IsRpd)
{
layer = GetLayerFromState(component, gridUid, mapGrid, tile, worldPos.Position);
}
// DeltaV - RPD End
// Return whether the operation location is valid
switch (prototype.Mode)
{
case RcdMode.ConstructTile:
case RcdMode.ConstructObject:
return IsConstructionLocationValid(uid, component, gridUid, mapGrid, tile, position, direction, user, popMsgs);
return IsConstructionLocationValid(uid, component, gridUid, mapGrid, tile, position, direction, user, layer, popMsgs); //DeltaV - RPD
case RcdMode.Deconstruct:
return IsDeconstructionStillValid(uid, tile, target, user, popMsgs);
return IsDeconstructionStillValid(uid, component, tile, target, user, popMsgs); //DeltaV - RPD
}
return false;
}
private bool IsConstructionLocationValid(EntityUid uid, RCDComponent component, EntityUid gridUid, MapGridComponent mapGrid, TileRef tile, Vector2i position, Direction direction, EntityUid user, bool popMsgs = true)
// DeltaV - RPD Begin
private AtmosPipeLayer GetLayerFromState(RCDComponent component, EntityUid gridUid, MapGridComponent mapGrid, TileRef tile, Vector2 clickPos)
{
var prototype = _protoManager.Index(component.ProtoId);
if (!component.IsRpd || component.CachedPrototype == null || !component.CachedPrototype.HasLayers)
return AtmosPipeLayer.Primary;
var tileSize = mapGrid.TileSize;
var tileCenter = new Vector2(tile.X + tileSize / 2f, tile.Y + tileSize / 2f);
var mouseCoordsDiff = clickPos - tileCenter;
var mouseDeadzoneRadius = 0.25f;
switch (component.CurrentMode)
{
case RpdMode.Primary: return AtmosPipeLayer.Primary;
case RpdMode.Secondary: return AtmosPipeLayer.Secondary;
case RpdMode.Tertiary: return AtmosPipeLayer.Tertiary;
case RpdMode.Free:
if (mouseCoordsDiff.Length() > mouseDeadzoneRadius && component.LastKnownEyeRotation.HasValue)
{
var gridRotation = _transform.GetWorldRotation(gridUid);
var angle = new Angle(mouseCoordsDiff);
var eyeRotation = new Angle(component.LastKnownEyeRotation.Value);
var direction = (angle + eyeRotation + gridRotation + Math.PI / 2).GetCardinalDir();
return (direction == Direction.North || direction == Direction.East) ? AtmosPipeLayer.Secondary : AtmosPipeLayer.Tertiary;
}
return AtmosPipeLayer.Primary;
default: return AtmosPipeLayer.Primary;
}
}
// DeltaV - RPD End
private bool IsConstructionLocationValid(EntityUid uid, RCDComponent component, EntityUid gridUid, MapGridComponent mapGrid, TileRef tile, Vector2i position, Direction direction, EntityUid user, AtmosPipeLayer layer, bool popMsgs = true)
{
// Update cached prototype if required
UpdateCachedPrototype(uid, component); //DeltaV - RPD
var prototype = component.CachedPrototype; //DeltaV - RPD
// Check rule: Must build on empty tile
if (prototype.ConstructionRules.Contains(RcdConstructionRule.MustBuildOnEmptyTile) && !tile.Tile.IsEmpty)
@ -455,9 +687,32 @@ public sealed class RCDSystem : EntitySystem
// This is to prevent spamming objects on the same tile (e.g. lights)
if (prototype.Prototype != null && MetaData(ent).EntityPrototype?.ID == prototype.Prototype)
{
// DeltaV - RPD Begin
var isIdentical = true;
if (prototype.AllowMultiDirection)
if (component.IsRpd)
{
// A pipe is only "identical" if it exists on the same layer we are trying to build on.
// We check the nodes of the existing entity to find its layer.
if (TryComp<NodeContainerComponent>(ent, out var nodeContainer))
{
var existingLayerMatch = false;
foreach (var node in nodeContainer.Nodes.Values)
{
if (node is IPipeNode pipeNode && pipeNode.Layer == layer)
{
existingLayerMatch = true;
break;
}
}
// If no nodes match the target layer, it's not identical (we can build here!)
if (!existingLayerMatch)
isIdentical = false;
}
}
else if (prototype.AllowMultiDirection)
{
var entDirection = Transform(ent).LocalRotation.GetCardinalDir();
if (entDirection != direction)
@ -466,11 +721,10 @@ public sealed class RCDSystem : EntitySystem
if (isIdentical)
{
if (popMsgs)
_popup.PopupClient(Loc.GetString("rcd-component-cannot-build-identical-entity"), uid, user);
if (popMsgs) _popup.PopupClient(Loc.GetString("rcd-component-cannot-build-identical-entity"), uid, user);
return false;
}
// DeltaV - RPD End
}
if (isWindow && HasComp<SharedCanBuildWindowOnTopComponent>(ent))
@ -484,6 +738,13 @@ public sealed class RCDSystem : EntitySystem
return false;
}
// DeltaV - RPD Begin
if (component.IsRpd && HasComp<PipeRestrictOverlapComponent>(ent))
{
continue;
}
// DeltaV - RPD End
if (prototype.CollisionMask != CollisionGroup.None && TryComp<FixturesComponent>(ent, out var fixtures))
{
foreach (var fixture in fixtures.Fixtures.Values)
@ -509,11 +770,20 @@ public sealed class RCDSystem : EntitySystem
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) //DeltaV - RPD
{
// Attempt to deconstruct a floor tile
if (target == null)
{
//DeltaV - RPD Begin
if (component.IsRpd)
{
if (popMsgs)
_popup.PopupClient(Loc.GetString("rcd-component-deconstruct-target-not-on-whitelist-message"), uid, user);
return false;
}
//DeltaV - RPD End
// The tile is empty
if (tile.Tile.IsEmpty)
{
@ -547,14 +817,26 @@ public sealed class RCDSystem : EntitySystem
// Attempt to deconstruct an object
else
{
// The object is not in the whitelist
if (!TryComp<RCDDeconstructableComponent>(target, out var deconstructible) || !deconstructible.Deconstructable)
//DeltaV - RPD Begin
// The object is not in the RPD whitelist
if (!TryComp<RCDDeconstructableComponent>(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;
}
//DeltaV - RPD End
// The object is not in the whitelist
if (!deconstructible.Deconstructable) //DeltaV - RPD
{
if (popMsgs)
_popup.PopupClient(Loc.GetString("rcd-component-deconstruct-target-not-on-whitelist-message"), uid, user);
return false;
}
}
return true;
@ -564,12 +846,12 @@ public sealed class RCDSystem : EntitySystem
#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, EntityUid? target, EntityUid user, AtmosPipeLayer layer) // DeltaV - RPD
{
if (!_net.IsServer)
return;
var prototype = _protoManager.Index(component.ProtoId);
var prototype = component.CachedPrototype; //DeltaV - RPD
if (prototype.Prototype == null)
return;
@ -585,8 +867,67 @@ public sealed class RCDSystem : EntitySystem
break;
case RcdMode.ConstructObject:
var ent = Spawn(prototype.Prototype, _mapSystem.GridTileToLocal(gridUid, mapGrid, position));
//DeltaV - RPD Begin
var proto = (component.UseMirrorPrototype && !string.IsNullOrEmpty(prototype.MirrorPrototype))
? prototype.MirrorPrototype
: prototype.Prototype;
if (component.IsRpd && prototype.HasLayers)
{
if (_protoManager.TryIndex<EntityPrototype>(proto, out var entityProto) &&
entityProto.TryGetComponent<AtmosPipeLayersComponent>(out var atmosPipeLayers, _entityManager.ComponentFactory) &&
_pipeLayersSystem.TryGetAlternativePrototype(atmosPipeLayers, layer, out var newProtoId))
{
proto = newProtoId;
}
}
// Calculate rotation before spawn
var rotation = prototype.Rotation switch
{
RcdRotation.Fixed => Angle.Zero,
RcdRotation.Camera => Transform(uid).LocalRotation,
RcdRotation.User => direction.ToAngle(),
_ => Angle.Zero // Fallback
};
// 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<EntityPrototype>(proto, out var pipeProto) &&
pipeProto.TryGetComponent<NodeContainerComponent>(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,
layer,
rotation
);
// If there is a conflict, delete the old pipe first
var conflict = _pipeOverlap.CheckIfWouldConflict(gridUid, position, proposed);
if (Exists(conflict) && HasComp<RCDDeconstructableComponent>(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);
//DeltaV - RPD End
switch (prototype.Rotation)
{
case RcdRotation.Fixed:
@ -633,7 +974,25 @@ public sealed class RCDSystem : EntitySystem
return boundingPolygon.ComputeAABB(boundingTransform, 0).Intersects(fixture.Shape.ComputeAABB(entXform, 0));
}
//DeltaV - RPD Begin
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 RpdMode GetCurrentRpdMode(EntityUid uid, RCDComponent? component = null)
{
if (!Resolve(uid, ref component))
return RpdMode.Free; // default to Free mode
return component.CurrentMode;
}
//DeltaV - RPD End
#endregion
}
@ -655,14 +1014,18 @@ public sealed partial class RCDDoAfterEvent : DoAfterEvent
[DataField("fx")]
public NetEntity? Effect { get; private set; }
[DataField] //DeltaV - RPD
public AtmosPipeLayer Layer { get; private set; } = AtmosPipeLayer.Primary; //DeltaV - RPD
private RCDDoAfterEvent() { }
public RCDDoAfterEvent(NetCoordinates location, Direction direction, ProtoId<RCDPrototype> startingProtoId, int cost, NetEntity? effect = null)
public RCDDoAfterEvent(NetCoordinates location, Direction direction, ProtoId<RCDPrototype> startingProtoId, int cost, AtmosPipeLayer layer, NetEntity? effect = null) //DeltaV - RPD
{
Location = location;
Direction = direction;
StartingProtoId = startingProtoId;
Cost = cost;
Layer = layer; //DeltaV - RPD
Effect = effect;
}

View File

@ -0,0 +1,10 @@
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos;
namespace Content.Shared.Atmos;
public interface IPipeNode
{
PipeDirection Direction { get; }
AtmosPipeLayer Layer { get; }
}

View File

@ -0,0 +1,14 @@
# DeltaV - RPD mode change
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}
rcd-component-piping = Piping
rcd-component-pumps-valves = Pumps & Valves
rcd-component-vents = Vents
rcd-component-atmospheric-utility = Atmospheric Utility
rcd-component-sensors-monitors = Sensors & Monitors

View File

@ -160,6 +160,7 @@
- id: DoorRemoteFirefight # DeltaV - Re-added fire-fighting remote.
- id: RCD
- id: RCDAmmo
- id: RPD # DeltaV - RPD!
- id: LunchboxEngineeringFilledRandom # DeltaV - Lunchboxes!
prob: 0.3
- id: AirGrenade

View File

@ -172,6 +172,7 @@
- id: AirGrenade # DeltaV
- id: HolotapeProjector # DeltaV - engineering tape
- id: BoxFolderCeClipboard # DeltaV - Power Digi-Board
- id: RPD # DeltaV - RPD!
# Hardsuit table, used for suit storage as well
- type: entityTable

View File

@ -191,6 +191,7 @@
- CrateEngineeringTeslaGroundingRod
- CrateEngineeringParticleAccelerator
- CrateRCD
- CrateRPD #DeltaV - RPD
- CrateEngineeringGear
rareChance: 0.2
prototypes:

View File

@ -23,6 +23,11 @@
- type: CollideOnAnchor
- type: Anchorable
- type: Rotatable
- type: RCDDeconstructable #DeltaV - RPD
cost: 1 #DeltaV - RPD
delay: 2 #DeltaV - RPD
fx: EffectRCDDeconstruct2 #DeltaV - RPD
rpd: true #DeltaV - RPD
- type: Destructible
thresholds:
- trigger: # for nukes
@ -410,6 +415,3 @@
navMapBlip: GasPipeManifold
showAbsentConnections: false
- type: AtmosDevice
- type: Tag
tags:
- Unstackable

View File

@ -43,6 +43,11 @@
- type: Damageable
damageContainer: Inorganic
damageModifierSet: Metallic
- type: RCDDeconstructable #DeltaV - RPD
cost: 1 #DeltaV - RPD
delay: 2 #DeltaV - RPD
fx: EffectRCDDeconstruct2 #DeltaV - RPD
rpd: true #DeltaV - RPD
- type: Destructible
thresholds:
- trigger:

View File

@ -69,6 +69,11 @@
- type: ContainerContainer
containers:
board: !type:Container
- type: RCDDeconstructable #DeltaV - RPD
cost: 10 #DeltaV - RPD
delay: 2 #DeltaV - RPD
fx: EffectRCDDeconstruct2 #DeltaV - RPD
rpd: true #DeltaV - RPD
- type: Appearance
- type: WiresVisuals
- type: Sprite

View File

@ -7,3 +7,13 @@
cost: 1100
category: cargoproduct-category-name-atmospherics
group: market
- type: cargoProduct
id: EngineeringRPD
icon:
sprite: _DV/Objects/Tools/rpd.rsi
state: icon
product: CrateRPD
cost: 800
category: cargoproduct-category-name-atmospherics
group: market

View File

@ -0,0 +1,10 @@
- type: entity
parent: CrateEngineeringSecure
id: CrateRPD
name: RPD crate
description: A crate containing a single rapid piping device.
components:
- type: EntityTableContainerFill
containers:
entity_storage:
id: RPD

View File

@ -10,3 +10,43 @@
sprite: _DV/Objects/Tools/RCD_combat.rsi
- type: Clothing
sprite: _DV/Objects/Tools/RCD_combat.rsi
- type: entity
id: RPD
parent: RCD
name: RPD
description: A device used to "rapidly" pipe things, and an atmosian's best friend.
components:
- type: RCD
isRpd: true
availablePrototypes:
- PipeFourway
- PipeStraight
- PipeBend
- PipeTJunction
- PipeHalf
- ManifoldGas
- OutletInjector
- ManualValve
- VolumetricPump
- PressurePump
- VentScrubber
- PressureValve
- PressureRegulator
- DualPortVent
- VentGas
- VentPassive
- MixerGas
- Radiator
- RadiatorBend
- SignalValve
- CanisterPort
- FilterGas
- Alarm
- Sensor
- SensorPipe
- Deconstruct
- type: LimitedCharges
maxCharges: 45
- type: Sprite
sprite: _DV/Objects/Tools/rpd.rsi

View File

@ -0,0 +1,308 @@
- type: rcd
id: PipeFourway
category: Piping
sprite: /Textures/_DV/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/_DV/Interface/Radial/RPD/straight.png
mode: ConstructObject
prototype: GasPipeStraight
cost: 1
delay: 0
rotation: User
hasLayers: true
fx: EffectRCDConstruct0
- type: rcd
id: PipeBend
category: Piping
sprite: /Textures/_DV/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/_DV/Interface/Radial/RPD/tjunction.png
mode: ConstructObject
prototype: GasPipeTJunction
cost: 1
delay: 0
collisionMask: Impassable
rotation: User
hasLayers: true
fx: EffectRCDConstruct0
- type: rcd
id: PipeHalf
category: Piping
sprite: /Textures/_DV/Interface/Radial/RPD/pipe_half.png
mode: ConstructObject
prototype: GasPipeHalf
cost: 1
delay: 0
collisionMask: Impassable
rotation: User
hasLayers: true
fx: EffectRCDConstruct0
- type: rcd
id: ManifoldGas
category: Piping
sprite: /Textures/_DV/Interface/Radial/RPD/manifold.png
mode: ConstructObject
prototype: GasPipeManifold
cost: 2
delay: 0
collisionMask: Impassable
rotation: User
fx: EffectRCDConstruct0
- type: rcd
id: PressurePump
category: PumpsValves
sprite: /Textures/_DV/Interface/Radial/RPD/pump_pressure.png
mode: ConstructObject
prototype: GasPressurePump
cost: 2
delay: 0
collisionMask: Impassable
rotation: User
hasLayers: true
fx: EffectRCDConstruct0
- type: rcd
id: VolumetricPump
category: PumpsValves
sprite: /Textures/_DV/Interface/Radial/RPD/pump_volume.png
mode: ConstructObject
prototype: GasVolumePump
cost: 2
delay: 0
collisionMask: Impassable
rotation: User
hasLayers: true
fx: EffectRCDConstruct0
- type: rcd
id: ManualValve
category: PumpsValves
sprite: /Textures/_DV/Interface/Radial/RPD/pump_manual_valve.png
mode: ConstructObject
prototype: GasValve
cost: 2
delay: 0
collisionMask: Impassable
rotation: User
hasLayers: true
fx: EffectRCDConstruct0
- type: rcd
id: SignalValve
category: PumpsValves
sprite: /Textures/_DV/Interface/Radial/RPD/pump_signal_valve.png
mode: ConstructObject
prototype: SignalControlledValve
cost: 2
delay: 0
collisionMask: Impassable
rotation: User
hasLayers: true
fx: EffectRCDConstruct0
- type: rcd
id: PressureValve
category: PumpsValves
sprite: /Textures/_DV/Interface/Radial/RPD/pneumatic_valve.png
mode: ConstructObject
prototype: PressureControlledValve
cost: 2
delay: 0
collisionMask: Impassable
rotation: User
fx: EffectRCDConstruct0
- type: rcd
id: PressureRegulator
category: PumpsValves
sprite: /Textures/_DV/Interface/Radial/RPD/pump_pressure_regulator.png
mode: ConstructObject
prototype: GasPressureRegulator
cost: 2
delay: 0
collisionMask: Impassable
rotation: User
hasLayers: true
fx: EffectRCDConstruct0
- type: rcd
id: OutletInjector
category: Vents
sprite: /Textures/_DV/Interface/Radial/RPD/injector.png
mode: ConstructObject
prototype: GasOutletInjector
cost: 2
delay: 0
collisionMask: Impassable
rotation: User
fx: EffectRCDConstruct0
- type: rcd
id: VentScrubber
category: Vents
sprite: /Textures/_DV/Interface/Radial/RPD/scrub_off.png
mode: ConstructObject
prototype: GasVentScrubber
cost: 2
delay: 0
collisionMask: Impassable
rotation: User
fx: EffectRCDConstruct0
- type: rcd
id: VentGas
category: Vents
sprite: /Textures/_DV/Interface/Radial/RPD/vent_off.png
mode: ConstructObject
prototype: GasVentPump
cost: 2
delay: 0
collisionMask: Impassable
rotation: User
fx: EffectRCDConstruct0
- type: rcd
id: DualPortVent
category: Vents
sprite: /Textures/_DV/Interface/Radial/RPD/dual_port.png
mode: ConstructObject
prototype: GasDualPortVentPump
cost: 2
delay: 0
collisionMask: Impassable
rotation: User
fx: EffectRCDConstruct0
- type: rcd
id: VentPassive
category: Vents
sprite: /Textures/_DV/Interface/Radial/RPD/vent_passive.png
mode: ConstructObject
prototype: GasPassiveVent
cost: 2
delay: 0
collisionMask: Impassable
rotation: User
fx: EffectRCDConstruct0
- type: rcd
id: Radiator
category: AtmosphericUtility
sprite: /Textures/_DV/Interface/Radial/RPD/radiator.png
mode: ConstructObject
prototype: HeatExchanger
cost: 2
delay: 0
collisionMask: Impassable
rotation: User
fx: EffectRCDConstruct0
- type: rcd
id: RadiatorBend
category: AtmosphericUtility
sprite: /Textures/_DV/Interface/Radial/RPD/he_bend.png
mode: ConstructObject
prototype: HeatExchangerBend
cost: 2
delay: 0
collisionMask: Impassable
rotation: User
fx: EffectRCDConstruct0
- type: rcd
id: MixerGas
category: AtmosphericUtility
sprite: /Textures/_DV/Interface/Radial/RPD/gas_mixer.png
mode: ConstructObject
prototype: GasMixer
mirrorPrototype: GasMixerFlipped
cost: 2
delay: 0
collisionMask: Impassable
rotation: User
fx: EffectRCDConstruct0
- type: rcd
id: FilterGas
category: AtmosphericUtility
sprite: /Textures/_DV/Interface/Radial/RPD/gas_filter.png
mode: ConstructObject
prototype: GasFilter
mirrorPrototype: GasFilterFlipped
cost: 2
delay: 0
collisionMask: Impassable
rotation: User
fx: EffectRCDConstruct0
- type: rcd
id: CanisterPort
category: AtmosphericUtility
sprite: /Textures/_DV/Interface/Radial/RPD/port.png
mode: ConstructObject
prototype: GasPort
cost: 2
delay: 0
collisionMask: Impassable
rotation: User
fx: EffectRCDConstruct0
- type: rcd
id: Alarm
category: SensorsMonitors
sprite: /Textures/_DV/Interface/Radial/RPD/alarm.png
mode: ConstructObject
prototype: AirAlarm
cost: 10
delay: 1
rotation: User
fx: EffectRCDConstruct0
- type: rcd
id: Sensor
category: SensorsMonitors
sprite: /Textures/_DV/Interface/Radial/RPD/sensor.png
mode: ConstructObject
prototype: AirSensor
cost: 2
delay: 0
collisionMask: Impassable
rotation: User
fx: EffectRCDConstruct0
- type: rcd
id: SensorPipe
category: SensorsMonitors
sprite: /Textures/_DV/Interface/Radial/RPD/sensorpipe.png
mode: ConstructObject
prototype: GasPipeSensor
cost: 20
delay: 0
collisionMask: Impassable
rotation: User
fx: EffectRCDConstruct0

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 679 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 691 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 820 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 791 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,22 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Taken from CEV-Eris at https://github.com/discordia-space/CEV-Eris/blob/8009c1d3db5ca5244f14899ffafaae93be0e318d/icons/obj/items.dmi,https://github.com/discordia-space/CEV-Eris/blob/df81fe2d0ba5f541bafffbb73554f3f44289dd76/icons/mob/items/righthand.dmi,https://github.com/discordia-space/CEV-Eris/blob/df81fe2d0ba5f541bafffbb73554f3f44289dd76/icons/mob/items/righthand.dmi",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "icon"
},
{
"name": "inhand-left",
"directions": 4
},
{
"name": "inhand-right",
"directions": 4
}
]
}