diff --git a/Content.Client/ADT/_RMC14/Attachable/Components/AttachableHolderVisualsComponent.cs b/Content.Client/ADT/_RMC14/Attachable/Components/AttachableHolderVisualsComponent.cs
new file mode 100644
index 00000000000..d1a1351691d
--- /dev/null
+++ b/Content.Client/ADT/_RMC14/Attachable/Components/AttachableHolderVisualsComponent.cs
@@ -0,0 +1,17 @@
+using Content.Client._RMC14.Attachable.Systems;
+using System.Numerics;
+
+namespace Content.Client._RMC14.Attachable.Components;
+
+[RegisterComponent, AutoGenerateComponentState]
+[Access(typeof(AttachableHolderVisualsSystem))]
+public sealed partial class AttachableHolderVisualsComponent : Component
+{
+ ///
+ /// This dictionary contains a list of offsets for every slot that should display the attachable placed into it.
+ /// If a slot is not in this dictionary, the attachable inside will not be displayed.
+ /// The list of valid slot names can be found in AttachableHolderComponent.cs
+ ///
+ [DataField(required: true), AutoNetworkedField]
+ public Dictionary Offsets = new();
+}
diff --git a/Content.Client/ADT/_RMC14/Attachable/Components/AttachableVisualsComponent.cs b/Content.Client/ADT/_RMC14/Attachable/Components/AttachableVisualsComponent.cs
new file mode 100644
index 00000000000..51100e8a74c
--- /dev/null
+++ b/Content.Client/ADT/_RMC14/Attachable/Components/AttachableVisualsComponent.cs
@@ -0,0 +1,66 @@
+using Content.Client._RMC14.Attachable.Systems;
+using System.Numerics;
+using Robust.Shared.Utility;
+
+namespace Content.Client._RMC14.Attachable.Components;
+
+[RegisterComponent, AutoGenerateComponentState]
+[Access(typeof(AttachableHolderVisualsSystem))]
+public sealed partial class AttachableVisualsComponent : Component
+{
+ ///
+ /// Optional, only used if the item's own state should not be used.
+ /// The path to the RSI file that contains all the attached states.
+ ///
+ [DataField, AutoNetworkedField]
+ public ResPath? Rsi;
+
+ ///
+ /// Optional, only used if the item's own state should not be used.
+ /// This prefix is added to the name of the slot the attachable is installed in.
+ /// The prefix must be in kebab-case and end with a dash, like so: example-prefix-
+ /// The RSI must contain a state for every slot the attachable fits into.
+ /// If the attachment only fits into one slot, it should be named as follows: normal-state_suffix.
+ /// The slot names can be found in AttachableHolderComponent.cs
+ ///
+ [DataField, AutoNetworkedField]
+ public string? Prefix;
+
+ ///
+ /// Optional, only used if the item's own state should not be used.
+ /// This suffix is added to the name of the slot the attachable is installed in.
+ /// The RSI must contain a state for every slot the attachable fits into.
+ /// If the attachment only fits into one slot, it should be named as follows: normal-state_suffix.
+ /// The slot names can be found in AttachableHolderComponent.cs
+ ///
+ [DataField, AutoNetworkedField]
+ public string? Suffix = "_a";
+
+ ///
+ /// If true, will include the holder's slot name to find this attachment's state
+ /// in its RSI.
+ /// In this case, there must be a separate state for each slot the attachment fits into.
+ /// The states should be named as follows: prefix-slot-name-suffix.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool IncludeSlotName;
+
+ ///
+ /// If this is toggled on and the item has an AttachableToggleableComponent, then the visualisation system will try to show a different sprite when it's active.
+ /// Each active state must have "-on" appended to the end of its name.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool ShowActive;
+
+ ///
+ /// If this is set to true, the attachment will be redrawn on its holder every time it receives an AppearanceChangeEvent. Useful for things like the UGL.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool RedrawOnAppearanceChange;
+
+ [DataField, AutoNetworkedField]
+ public int Layer;
+
+ [DataField, AutoNetworkedField]
+ public Vector2 Offset;
+}
diff --git a/Content.Client/ADT/_RMC14/Attachable/Systems/AttachableHolderVisualsSystem.cs b/Content.Client/ADT/_RMC14/Attachable/Systems/AttachableHolderVisualsSystem.cs
new file mode 100644
index 00000000000..b083f1ca2b6
--- /dev/null
+++ b/Content.Client/ADT/_RMC14/Attachable/Systems/AttachableHolderVisualsSystem.cs
@@ -0,0 +1,155 @@
+using Content.Client._RMC14.Attachable.Components;
+using Content.Shared._RMC14.Attachable;
+using Content.Shared._RMC14.Attachable.Components;
+using Content.Shared._RMC14.Attachable.Events;
+using Content.Shared._RMC14.Attachable.Systems;
+using Robust.Client.GameObjects;
+using Robust.Shared.Containers;
+
+namespace Content.Client._RMC14.Attachable.Systems;
+
+public sealed class AttachableHolderVisualsSystem : EntitySystem
+{
+ [Dependency] private readonly AttachableHolderSystem _attachableHolderSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnDetached);
+ SubscribeLocalEvent(OnAttachablesAltered);
+
+ SubscribeLocalEvent(OnAttachableAppearanceChange);
+ }
+
+ private void OnDetached(Entity holder, ref EntRemovedFromContainerMessage args)
+ {
+ if (!HasComp(args.Entity) || !_attachableHolderSystem.HasSlot(holder.Owner, args.Container.ID))
+ return;
+
+ var holderEv = new AttachableHolderAttachablesAlteredEvent(args.Entity, args.Container.ID, AttachableAlteredType.Detached);
+ RaiseLocalEvent(holder, ref holderEv);
+ }
+
+ private void OnAttachablesAltered(Entity holder,
+ ref AttachableHolderAttachablesAlteredEvent args)
+ {
+ if (!TryComp(args.Attachable, out AttachableVisualsComponent? attachableComponent))
+ return;
+
+ string suffix = "";
+ if (attachableComponent.ShowActive && TryComp(args.Attachable, out AttachableToggleableComponent? toggleableComponent) && toggleableComponent.Active)
+ suffix = "-on";
+
+ var attachable = new Entity(args.Attachable, attachableComponent);
+ switch (args.Alteration)
+ {
+ case AttachableAlteredType.Attached:
+ SetAttachableOverlay(holder, attachable, args.SlotId, suffix);
+ break;
+
+ case AttachableAlteredType.Detached:
+ RemoveAttachableOverlay(holder, args.SlotId);
+ break;
+
+ case AttachableAlteredType.Activated:
+ if (!attachableComponent.ShowActive)
+ break;
+
+ SetAttachableOverlay(holder, attachable, args.SlotId, suffix);
+ break;
+
+ case AttachableAlteredType.Deactivated:
+ if (!attachableComponent.ShowActive)
+ break;
+
+ SetAttachableOverlay(holder, attachable, args.SlotId, suffix);
+ break;
+
+ case AttachableAlteredType.Interrupted:
+ if (!attachableComponent.ShowActive)
+ break;
+
+ SetAttachableOverlay(holder, attachable, args.SlotId);
+ break;
+
+ case AttachableAlteredType.AppearanceChanged:
+ SetAttachableOverlay(holder, attachable, args.SlotId, suffix);
+ break;
+ }
+ }
+
+ private void RemoveAttachableOverlay(Entity holder, string slotId)
+ {
+ if (!holder.Comp.Offsets.ContainsKey(slotId) || !TryComp(holder, out SpriteComponent? spriteComponent))
+ return;
+
+ if (!spriteComponent.LayerMapTryGet(slotId, out var index))
+ return;
+
+ spriteComponent.LayerMapRemove(slotId);
+ spriteComponent.RemoveLayer(index);
+ }
+
+ private void SetAttachableOverlay(Entity holder,
+ Entity attachable,
+ string slotId,
+ string suffix = "")
+ {
+ if (!holder.Comp.Offsets.ContainsKey(slotId) ||
+ !TryComp(holder, out SpriteComponent? holderSprite))
+ {
+ return;
+ }
+
+ if (!TryComp(attachable, out SpriteComponent? attachableSprite))
+ return;
+
+ var rsi = attachableSprite.LayerGetActualRSI(attachable.Comp.Layer)?.Path;
+ var state = attachableSprite.LayerGetState(attachable.Comp.Layer).ToString();
+ if (attachable.Comp.Rsi is { } rsiPath)
+ {
+ rsi = rsiPath;
+ }
+
+ if (!string.IsNullOrWhiteSpace(attachable.Comp.Prefix))
+ state = attachable.Comp.Prefix;
+
+ if (attachable.Comp.IncludeSlotName)
+ state += slotId;
+
+ if (!string.IsNullOrWhiteSpace(attachable.Comp.Suffix))
+ state += attachable.Comp.Suffix;
+
+ state += suffix;
+
+ var layerData = new PrototypeLayerData()
+ {
+ RsiPath = rsi.ToString(),
+ State = state,
+ Offset = holder.Comp.Offsets[slotId] + attachable.Comp.Offset,
+ Visible = true,
+ };
+
+ if (holderSprite.LayerMapTryGet(slotId, out var index))
+ {
+ holderSprite.LayerSetData(index, layerData);
+ return;
+ }
+
+ holderSprite.LayerMapSet(slotId, holderSprite.AddLayer(layerData));
+ }
+
+ private void OnAttachableAppearanceChange(Entity attachable, ref AppearanceChangeEvent args)
+ {
+ if (!attachable.Comp.RedrawOnAppearanceChange ||
+ !_attachableHolderSystem.TryGetHolder(attachable.Owner, out var holderUid) ||
+ !_attachableHolderSystem.TryGetSlotId(holderUid.Value, attachable.Owner, out var slotId))
+ {
+ return;
+ }
+
+ var holderEvent = new AttachableHolderAttachablesAlteredEvent(attachable.Owner, slotId, AttachableAlteredType.AppearanceChanged);
+ RaiseLocalEvent(holderUid.Value, ref holderEvent);
+ }
+}
diff --git a/Content.Client/ADT/_RMC14/Attachable/Ui/AttachableHolderChooseSlotMenu.xaml b/Content.Client/ADT/_RMC14/Attachable/Ui/AttachableHolderChooseSlotMenu.xaml
new file mode 100644
index 00000000000..c2ff1b5b704
--- /dev/null
+++ b/Content.Client/ADT/_RMC14/Attachable/Ui/AttachableHolderChooseSlotMenu.xaml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/Content.Client/ADT/_RMC14/Attachable/Ui/AttachableHolderChooseSlotMenu.xaml.cs b/Content.Client/ADT/_RMC14/Attachable/Ui/AttachableHolderChooseSlotMenu.xaml.cs
new file mode 100644
index 00000000000..27c30ab2274
--- /dev/null
+++ b/Content.Client/ADT/_RMC14/Attachable/Ui/AttachableHolderChooseSlotMenu.xaml.cs
@@ -0,0 +1,58 @@
+using Content.Client.UserInterface.Controls;
+using Content.Shared._RMC14.Attachable;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.ADT._RMC14.Attachable.Ui;
+
+[GenerateTypedNameReferences]
+public sealed partial class AttachableHolderChooseSlotMenu : FancyWindow
+{
+ private readonly AttachmentChooseSlotBui _boundUI;
+ private readonly Dictionary _attachableSlotControls;
+
+ public AttachableHolderChooseSlotMenu(AttachmentChooseSlotBui boundUI)
+ {
+ RobustXamlLoader.Load(this);
+
+ _boundUI = boundUI;
+ _attachableSlotControls = new Dictionary();
+ OnClose += boundUI.Close;
+ }
+
+ public void UpdateMenu(List attachableSlots)
+ {
+ foreach (var slotId in attachableSlots)
+ {
+ if (!_attachableSlotControls.ContainsKey(slotId))
+ AddSlotControl(slotId);
+ }
+ }
+
+ private void AddSlotControl(string slotId)
+ {
+ var slotControl = new AttachableSlotControl(this, _boundUI, slotId);
+ SlotsContainer.AddChild(slotControl);
+ _attachableSlotControls.Add(slotId, slotControl);
+ }
+
+ private sealed class AttachableSlotControl : Control
+ {
+ public AttachableSlotControl(AttachableHolderChooseSlotMenu slotMenu,
+ AttachmentChooseSlotBui boundUI,
+ string slotId)
+ {
+ var button = new Button { Text = Loc.GetString(slotId) };
+
+ button.OnPressed += _ =>
+ {
+ boundUI.SendMessage(new AttachableHolderAttachToSlotMessage(slotId));
+ slotMenu.Close();
+ };
+
+ AddChild(button);
+ }
+ }
+}
diff --git a/Content.Client/ADT/_RMC14/Attachable/Ui/AttachableHolderStripMenu.xaml b/Content.Client/ADT/_RMC14/Attachable/Ui/AttachableHolderStripMenu.xaml
new file mode 100644
index 00000000000..a21514abeda
--- /dev/null
+++ b/Content.Client/ADT/_RMC14/Attachable/Ui/AttachableHolderStripMenu.xaml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/Content.Client/ADT/_RMC14/Attachable/Ui/AttachableHolderStripMenu.xaml.cs b/Content.Client/ADT/_RMC14/Attachable/Ui/AttachableHolderStripMenu.xaml.cs
new file mode 100644
index 00000000000..0f045d1068f
--- /dev/null
+++ b/Content.Client/ADT/_RMC14/Attachable/Ui/AttachableHolderStripMenu.xaml.cs
@@ -0,0 +1,95 @@
+using System.Numerics;
+using Content.Client.Stylesheets;
+using Content.Client.UserInterface.Controls;
+using Content.Shared._RMC14.Attachable;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using static Robust.Client.UserInterface.Controls.BoxContainer;
+
+namespace Content.Client.ADT._RMC14.Attachable.Ui;
+
+[GenerateTypedNameReferences]
+public sealed partial class AttachableHolderStripMenu : FancyWindow
+{
+ private readonly AttachmentStripBui _boundUI;
+ private readonly Dictionary _attachableSlotControls;
+
+ public AttachableHolderStripMenu(AttachmentStripBui boundUI)
+ {
+ RobustXamlLoader.Load(this);
+
+ _boundUI = boundUI;
+ _attachableSlotControls = new Dictionary();
+ OnClose += boundUI.Close;
+ }
+
+ public void UpdateMenu(Dictionary attachableSlots)
+ {
+ foreach (var slotId in attachableSlots.Keys)
+ {
+ if (!_attachableSlotControls.ContainsKey(slotId))
+ AddSlotControl(slotId);
+
+ _attachableSlotControls[slotId].Update(attachableSlots[slotId].attachableName, attachableSlots[slotId].locked);
+ }
+ }
+
+ private void AddSlotControl(string slotId, string? attachableName = null)
+ {
+ var slotControl = new AttachableSlotControl(_boundUI, slotId);
+ AttachablesContainer.AddChild(slotControl);
+ _attachableSlotControls.Add(slotId, slotControl);
+ }
+
+ private sealed class AttachableSlotControl : Control
+ {
+ private readonly Button AttachableButton;
+
+ public AttachableSlotControl(AttachmentStripBui boundUI, string slotId)
+ {
+ var slotLabel = new Label
+ {
+ Text = Loc.GetString(slotId) + ':',
+ HorizontalAlignment = HAlignment.Left
+ };
+
+ AttachableButton = new Button
+ {
+ Text = Loc.GetString("rmc-attachable-holder-strip-ui-empty-slot"),
+ HorizontalExpand = true,
+ HorizontalAlignment = HAlignment.Right,
+ StyleClasses = { StyleBase.ButtonOpenRight }
+ };
+
+ var hBox = new BoxContainer
+ {
+ Orientation = LayoutOrientation.Horizontal,
+ Children =
+ {
+ new Control { MinSize = new Vector2(5, 0) },
+ slotLabel,
+ new Control { MinSize = new Vector2(5, 0) },
+ AttachableButton,
+ },
+ };
+
+ AttachableButton.OnPressed += _ => boundUI.SendMessage(new AttachableHolderDetachMessage(slotId));
+ AddChild(hBox);
+ }
+
+ public void Update(string? attachableName, bool slotLocked)
+ {
+ if (attachableName == null)
+ {
+ AttachableButton.Text = Loc.GetString("rmc-attachable-holder-strip-ui-empty-slot");
+ AttachableButton.Disabled = true;
+ return;
+ }
+
+ AttachableButton.Text = attachableName;
+ AttachableButton.Disabled = slotLocked;
+ }
+ }
+}
diff --git a/Content.Client/ADT/_RMC14/Attachable/Ui/AttachmentChooseSlotBui.cs b/Content.Client/ADT/_RMC14/Attachable/Ui/AttachmentChooseSlotBui.cs
new file mode 100644
index 00000000000..6d46808bb7b
--- /dev/null
+++ b/Content.Client/ADT/_RMC14/Attachable/Ui/AttachmentChooseSlotBui.cs
@@ -0,0 +1,45 @@
+using Content.Shared._RMC14.Attachable;
+
+namespace Content.Client.ADT._RMC14.Attachable.Ui;
+
+public sealed class AttachmentChooseSlotBui : BoundUserInterface
+{
+ [ViewVariables]
+ private AttachableHolderChooseSlotMenu? _menu;
+
+ public AttachmentChooseSlotBui(EntityUid owner, Enum uiKey) : base(owner, uiKey) { }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _menu = new AttachableHolderChooseSlotMenu(this);
+ var metaQuery = EntMan.GetEntityQuery();
+ if (metaQuery.TryGetComponent(Owner, out var metadata))
+ _menu.Title = metadata.EntityName;
+
+ _menu.OpenCentered();
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (state is not AttachableHolderChooseSlotUserInterfaceState msg)
+ return;
+
+ if (_menu == null)
+ return;
+
+ _menu.UpdateMenu(msg.AttachableSlots);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (!disposing)
+ return;
+
+ _menu?.Dispose();
+ }
+}
diff --git a/Content.Client/ADT/_RMC14/Attachable/Ui/AttachmentStripBui.cs b/Content.Client/ADT/_RMC14/Attachable/Ui/AttachmentStripBui.cs
new file mode 100644
index 00000000000..b4a9a1afcdd
--- /dev/null
+++ b/Content.Client/ADT/_RMC14/Attachable/Ui/AttachmentStripBui.cs
@@ -0,0 +1,45 @@
+using Content.Shared._RMC14.Attachable;
+
+namespace Content.Client.ADT._RMC14.Attachable.Ui;
+
+public sealed class AttachmentStripBui : BoundUserInterface
+{
+ private AttachableHolderStripMenu? _menu;
+
+ public AttachmentStripBui(EntityUid owner, Enum uiKey) : base(owner, uiKey) { }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _menu = new AttachableHolderStripMenu(this);
+
+ var metaQuery = EntMan.GetEntityQuery();
+ if (metaQuery.TryGetComponent(Owner, out var metadata))
+ _menu.Title = metadata.EntityName;
+
+ _menu.OpenCentered();
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (state is not AttachableHolderStripUserInterfaceState msg)
+ return;
+
+ if (_menu == null)
+ return;
+
+ _menu.UpdateMenu(msg.AttachableSlots);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (!disposing)
+ return;
+
+ _menu?.Dispose();
+ }
+}
diff --git a/Content.Client/ADT/_RMC14/Scoping/ScopeSystem.cs b/Content.Client/ADT/_RMC14/Scoping/ScopeSystem.cs
new file mode 100644
index 00000000000..5483c9a9baa
--- /dev/null
+++ b/Content.Client/ADT/_RMC14/Scoping/ScopeSystem.cs
@@ -0,0 +1,5 @@
+using Content.Shared._RMC14.Scoping;
+
+namespace Content.Client._RMC14.Scoping;
+
+public sealed class ScopeSystem : SharedScopeSystem;
diff --git a/Content.Client/ADT/_RMC14/Weapons/Ranged/PumpActionSystem.cs b/Content.Client/ADT/_RMC14/Weapons/Ranged/PumpActionSystem.cs
new file mode 100644
index 00000000000..ffe6ad56286
--- /dev/null
+++ b/Content.Client/ADT/_RMC14/Weapons/Ranged/PumpActionSystem.cs
@@ -0,0 +1,35 @@
+using Content.Shared._RMC14.Input;
+using Content.Shared._RMC14.Weapons.Ranged;
+using Content.Shared.Examine;
+using Content.Shared.Popups;
+using Content.Shared.Weapons.Ranged.Systems;
+using Robust.Client.Input;
+
+namespace Content.Client._RMC14.Weapons.Ranged;
+
+public sealed class PumpActionSystem : SharedPumpActionSystem
+{
+ [Dependency] private readonly IInputManager _input = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+
+ protected override void OnExamined(Entity ent, ref ExaminedEvent args)
+ {
+ if (!_input.TryGetKeyBinding(CMKeyFunctions.CMUniqueAction, out var bind))
+ return;
+
+ args.PushMarkup(Loc.GetString(ent.Comp.Examine), 1);
+ }
+
+ protected override void OnAttemptShoot(Entity ent, ref AttemptShootEvent args)
+ {
+ base.OnAttemptShoot(ent, ref args);
+
+ if (!ent.Comp.Pumped)
+ {
+ var message = _input.TryGetKeyBinding(CMKeyFunctions.CMUniqueAction, out var bind)
+ ? Loc.GetString(ent.Comp.PopupKey, ("key", bind.GetKeyString()))
+ : Loc.GetString(ent.Comp.Popup);
+ _popup.PopupClient(message, args.User, args.User);
+ }
+ }
+}
diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs
index 4ae0ee62efa..3fa68d7e815 100644
--- a/Content.Client/Input/ContentContexts.cs
+++ b/Content.Client/Input/ContentContexts.cs
@@ -1,5 +1,6 @@
using Content.Shared.Input;
using Robust.Shared.Input;
+using Content.Shared._RMC14.Input; // ADT TWEAK
namespace Content.Client.Input
{
@@ -124,6 +125,21 @@ public static void SetupContexts(IInputContextContainer contexts)
common.AddFunction(ContentKeyFunctions.OpenDecalSpawnWindow);
common.AddFunction(ContentKeyFunctions.OpenAdminMenu);
common.AddFunction(ContentKeyFunctions.OpenGuidebook);
+
+ CMFunctions(contexts); // ADT TWEAK
+ }
+
+ // ADT TWEAK START:
+ private static void CMFunctions(IInputContextContainer contexts)
+ {
+ var human = contexts.GetContext("human");
+ human.AddFunction(CMKeyFunctions.RMCActivateAttachableBarrel);
+ human.AddFunction(CMKeyFunctions.RMCActivateAttachableRail);
+ human.AddFunction(CMKeyFunctions.RMCActivateAttachableStock);
+ human.AddFunction(CMKeyFunctions.RMCActivateAttachableUnderbarrel);
+ human.AddFunction(CMKeyFunctions.RMCFieldStripHeldItem);
+ human.AddFunction(CMKeyFunctions.CMUniqueAction);
}
+ // ADT TWEAK END.
}
}
diff --git a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs
index 20b4330e985..f940815634a 100644
--- a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs
+++ b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs
@@ -13,6 +13,7 @@
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using static Robust.Client.UserInterface.Controls.BoxContainer;
+using Content.Shared._RMC14.Input; // ADT TWEAK
namespace Content.Client.Options.UI.Tabs
{
@@ -150,6 +151,15 @@ void AddCheckBox(string checkBoxName, bool currentState, Action _xformQuery;
+
+ private const string MeleeLungeKey = "melee-lunge";
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ _xformQuery = GetEntityQuery();
+ SubscribeNetworkEvent(OnMeleeLunge);
+ UpdatesOutsidePrediction = true;
+ }
+
+ public override void FrameUpdate(float frameTime)
+ {
+ base.FrameUpdate(frameTime);
+ UpdateEffects();
+ }
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ if (!Timing.IsFirstTimePredicted)
+ return;
+
+ var entityNull = _player.LocalEntity;
+
+ if (entityNull == null)
+ return;
+
+ var entity = entityNull.Value;
+
+ if (!TryGetWeapon(entity, out var weaponUid, out var weapon))
+ return;
+
+ if (!CombatMode.IsInCombatMode(entity) || !Blocker.CanAttack(entity, weapon: (weaponUid, weapon)))
+ {
+ weapon.Attacking = false;
+ return;
+ }
+ var useDown = _inputSystem.CmdStates.GetState(EngineKeyFunctions.Use);
+ var altDown = _inputSystem.CmdStates.GetState(EngineKeyFunctions.UseSecondary);
+
+ if (weapon.AutoAttack || useDown != BoundKeyState.Down && altDown != BoundKeyState.Down)
+ {
+ if (weapon.Attacking)
+ {
+ RaisePredictiveEvent(new StopAttackEvent(GetNetEntity(weaponUid)));
+ }
+ }
+
+ if (weapon.Attacking || weapon.NextAttack > Timing.CurTime)
+ {
+ return;
+ }
+
+ // TODO using targeted actions while combat mode is enabled should NOT trigger attacks.
+ var mousePos = _eyeManager.PixelToMap(_inputManager.MouseScreenPosition);
+
+ if (mousePos.MapId == MapId.Nullspace)
+ {
+ return;
+ }
+
+ EntityCoordinates coordinates;
+
+ if (MapManager.TryFindGridAt(mousePos, out var gridUid, out _))
+ {
+ coordinates = TransformSystem.ToCoordinates(gridUid, mousePos);
+ }
+ else
+ {
+ coordinates = TransformSystem.ToCoordinates(_map.GetMap(mousePos.MapId), mousePos);
+ }
+
+ // If the gun has AltFireMeleeComponent, it can be used to attack.
+ if (TryComp(weaponUid, out var gun) && gun.UseKey)
+ {
+ if (!TryComp(weaponUid, out var altFireComponent) || altDown != BoundKeyState.Down)
+ return;
+
+ switch (altFireComponent.AttackType)
+ {
+ case AltFireAttackType.Light:
+ ClientLightAttack(entity, mousePos, coordinates, weaponUid, weapon);
+ break;
+
+ case AltFireAttackType.Heavy:
+ ClientHeavyAttack(entity, coordinates, weaponUid, weapon);
+ break;
+
+ case AltFireAttackType.Disarm:
+ ClientDisarm(entity, mousePos, coordinates);
+ break;
+ }
+
+ return;
+ }
+
+ // Heavy attack.
+ if (altDown == BoundKeyState.Down)
+ {
+ // If it's an unarmed attack then do a disarm
+ if (weapon.AltDisarm && weaponUid == entity)
+ {
+ ClientDisarm(entity, mousePos, coordinates);
+ return;
+ }
+
+ ClientHeavyAttack(entity, coordinates, weaponUid, weapon);
+ return;
+ }
+
+ // Light attack
+ if (useDown == BoundKeyState.Down)
+ ClientLightAttack(entity, mousePos, coordinates, weaponUid, weapon);
+ }
+
+ protected override bool InRange(EntityUid user, EntityUid target, float range, ICommonSession? session)
+ {
+ var xform = Transform(target);
+ var targetCoordinates = xform.Coordinates;
+ var targetLocalAngle = xform.LocalRotation;
+
+ return Interaction.InRangeUnobstructed(user, target, targetCoordinates, targetLocalAngle, range);
+ }
+
+ protected override void DoDamageEffect(List targets, EntityUid? user, TransformComponent targetXform)
+ {
+ // Server never sends the event to us for predictiveeevent.
+ _color.RaiseEffect(Color.Red, targets, Filter.Local());
+ }
+
+ protected override bool DoDisarm(EntityUid user, DisarmAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session)
+ {
+ if (!base.DoDisarm(user, ev, meleeUid, component, session))
+ return false;
+
+ if (!TryComp(user, out var combatMode) ||
+ combatMode.CanDisarm != true)
+ {
+ return false;
+ }
+
+ var target = GetEntity(ev.Target);
+
+ // They need to either have hands...
+ if (!HasComp(target!.Value))
+ {
+ // or just be able to be shoved over.
+ if (TryComp(target, out var status) && status.AllowedEffects.Contains("KnockedDown"))
+ return true;
+
+ if (Timing.IsFirstTimePredicted && HasComp(target.Value))
+ PopupSystem.PopupEntity(Loc.GetString("disarm-action-disarmable", ("targetName", target.Value)), target.Value);
+
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Raises a heavy attack event with the relevant attacked entities.
+ /// This is to avoid lag effecting the client's perspective too much.
+ ///
+ public void ClientHeavyAttack(EntityUid user, EntityCoordinates coordinates, EntityUid meleeUid, MeleeWeaponComponent component)
+ {
+ // Only run on first prediction to avoid the potential raycast entities changing.
+ if (!_xformQuery.TryGetComponent(user, out var userXform) ||
+ !Timing.IsFirstTimePredicted)
+ {
+ return;
+ }
+
+ var targetMap = TransformSystem.ToMapCoordinates(coordinates);
+
+ if (targetMap.MapId != userXform.MapID)
+ return;
+
+ var userPos = TransformSystem.GetWorldPosition(userXform);
+ var direction = targetMap.Position - userPos;
+ var distance = MathF.Min(component.Range, direction.Length());
+
+ // This should really be improved. GetEntitiesInArc uses pos instead of bounding boxes.
+ // Server will validate it with InRangeUnobstructed.
+ var entities = GetNetEntityList(ArcRayCast(userPos, direction.ToWorldAngle(), component.Angle, distance, userXform.MapID, user).ToList());
+ RaisePredictiveEvent(new HeavyAttackEvent(GetNetEntity(meleeUid), entities.GetRange(0, Math.Min(MaxTargets, entities.Count)), GetNetCoordinates(coordinates)));
+ }
+
+ private void ClientDisarm(EntityUid attacker, MapCoordinates mousePos, EntityCoordinates coordinates)
+ {
+ EntityUid? target = null;
+
+ if (_stateManager.CurrentState is GameplayStateBase screen)
+ target = screen.GetClickedEntity(mousePos);
+
+ RaisePredictiveEvent(new DisarmAttackEvent(GetNetEntity(target), GetNetCoordinates(coordinates)));
+ }
+
+ private void ClientLightAttack(EntityUid attacker, MapCoordinates mousePos, EntityCoordinates coordinates, EntityUid weaponUid, MeleeWeaponComponent meleeComponent)
+ {
+ var attackerPos = TransformSystem.GetMapCoordinates(attacker);
+
+ if (mousePos.MapId != attackerPos.MapId || (attackerPos.Position - mousePos.Position).Length() > meleeComponent.Range)
+ return;
+
+ EntityUid? target = null;
+
+ if (_stateManager.CurrentState is GameplayStateBase screen)
+ target = screen.GetClickedEntity(mousePos);
+
+ // Don't light-attack if interaction will be handling this instead
+ if (Interaction.CombatModeCanHandInteract(attacker, target))
+ return;
+
+ RaisePredictiveEvent(new LightAttackEvent(GetNetEntity(target), GetNetEntity(weaponUid), GetNetCoordinates(coordinates)));
+ }
+ // ADT TWEAK END
private void OnMeleeLunge(MeleeLungeEvent ev)
{
var ent = GetEntity(ev.Entity);
diff --git a/Content.Server/ADT/_RMC14/Scoping/ScopeSystem.cs b/Content.Server/ADT/_RMC14/Scoping/ScopeSystem.cs
new file mode 100644
index 00000000000..91a49c34bea
--- /dev/null
+++ b/Content.Server/ADT/_RMC14/Scoping/ScopeSystem.cs
@@ -0,0 +1,52 @@
+using Content.Shared._RMC14.Scoping;
+using Robust.Server.GameObjects;
+using Robust.Shared.Player;
+
+namespace Content.Server._RMC14.Scoping;
+
+public sealed class ScopeSystem : SharedScopeSystem
+{
+ [Dependency] private readonly ViewSubscriberSystem _viewSubscriber = default!;
+
+ protected override Direction? StartScoping(Entity scope, EntityUid user)
+ {
+ if (base.StartScoping(scope, user) is not { } direction)
+ return null;
+
+ scope.Comp.User = user;
+
+ if (TryComp(user, out ActorComponent? actor))
+ {
+ var coords = Transform(user).Coordinates;
+ var offset = GetScopeOffset(scope, direction);
+ scope.Comp.RelayEntity = SpawnAtPosition(null, coords.Offset(offset));
+ _viewSubscriber.AddViewSubscriber(scope.Comp.RelayEntity.Value, actor.PlayerSession);
+ }
+
+ return direction;
+ }
+
+ protected override bool Unscope(Entity scope)
+ {
+ var user = scope.Comp.User;
+ if (!base.Unscope(scope))
+ return false;
+
+ DeleteRelay(scope, user);
+ return true;
+ }
+
+ protected override void DeleteRelay(Entity scope, EntityUid? user)
+ {
+ if (scope.Comp.RelayEntity is not { } relay)
+ return;
+
+ scope.Comp.RelayEntity = null;
+
+ if (TryComp(user, out ActorComponent? actor))
+ _viewSubscriber.RemoveViewSubscriber(relay, actor.PlayerSession);
+
+ if (!TerminatingOrDeleted(relay))
+ QueueDel(relay);
+ }
+}
diff --git a/Content.Server/ADT/_RMC14/Trigger/ActiveTriggerOnThrowEndComponent.cs b/Content.Server/ADT/_RMC14/Trigger/ActiveTriggerOnThrowEndComponent.cs
new file mode 100644
index 00000000000..7d19414b305
--- /dev/null
+++ b/Content.Server/ADT/_RMC14/Trigger/ActiveTriggerOnThrowEndComponent.cs
@@ -0,0 +1,12 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Server._RMC14.Trigger;
+
+[RegisterComponent, AutoGenerateComponentPause]
+[Access(typeof(RMCTriggerSystem))]
+public sealed partial class ActiveTriggerOnThrowEndComponent : Component
+{
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
+ public TimeSpan TriggerAt;
+}
diff --git a/Content.Server/ADT/_RMC14/Trigger/OnShootTriggerAmmoTimerComponent.cs b/Content.Server/ADT/_RMC14/Trigger/OnShootTriggerAmmoTimerComponent.cs
new file mode 100644
index 00000000000..0e9a0e9376b
--- /dev/null
+++ b/Content.Server/ADT/_RMC14/Trigger/OnShootTriggerAmmoTimerComponent.cs
@@ -0,0 +1,20 @@
+using Robust.Shared.Audio;
+
+namespace Content.Server._RMC14.Trigger;
+
+[RegisterComponent]
+[Access(typeof(RMCTriggerSystem))]
+public sealed partial class OnShootTriggerAmmoTimerComponent : Component
+{
+ [DataField]
+ public float Delay;
+
+ [DataField]
+ public float BeepInterval;
+
+ [DataField]
+ public float? InitialBeepDelay;
+
+ [DataField]
+ public SoundSpecifier? BeepSound;
+}
diff --git a/Content.Server/ADT/_RMC14/Trigger/RMCTriggerSystem.cs b/Content.Server/ADT/_RMC14/Trigger/RMCTriggerSystem.cs
new file mode 100644
index 00000000000..d225aa96071
--- /dev/null
+++ b/Content.Server/ADT/_RMC14/Trigger/RMCTriggerSystem.cs
@@ -0,0 +1,48 @@
+using Content.Server.Explosion.EntitySystems;
+using Content.Shared._RMC14.Weapons.Ranged;
+using Content.Shared.Throwing;
+using Content.Shared.Weapons.Ranged.Events;
+using Robust.Shared.Timing;
+
+namespace Content.Server._RMC14.Trigger;
+
+public sealed class RMCTriggerSystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly TriggerSystem _trigger = default!;
+
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnTriggerTimerAmmoShot);
+ SubscribeLocalEvent(OnTriggerOnFixedDistanceStop);
+ }
+
+ private void OnTriggerTimerAmmoShot(Entity ent, ref AmmoShotEvent args)
+ {
+ foreach (var projectile in args.FiredProjectiles)
+ {
+ _trigger.HandleTimerTrigger(projectile, null, ent.Comp.Delay, ent.Comp.BeepInterval, ent.Comp.InitialBeepDelay, ent.Comp.BeepSound);
+ }
+ }
+
+ private void OnTriggerOnFixedDistanceStop(Entity ent, ref ProjectileFixedDistanceStopEvent args)
+ {
+ var active = EnsureComp(ent);
+ active.TriggerAt = _timing.CurTime + ent.Comp.Delay;
+ }
+
+ public override void Update(float frameTime)
+ {
+ var time = _timing.CurTime;
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var active))
+ {
+ if (time < active.TriggerAt)
+ continue;
+
+ _trigger.Trigger(uid);
+ if (!EntityManager.IsQueuedForDeletion(uid) && !TerminatingOrDeleted(uid))
+ QueueDel(uid);
+ }
+ }
+}
diff --git a/Content.Server/ADT/_RMC14/Trigger/TriggerOnFixedDistanceStopComponent.cs b/Content.Server/ADT/_RMC14/Trigger/TriggerOnFixedDistanceStopComponent.cs
new file mode 100644
index 00000000000..f3346372fa2
--- /dev/null
+++ b/Content.Server/ADT/_RMC14/Trigger/TriggerOnFixedDistanceStopComponent.cs
@@ -0,0 +1,11 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Server._RMC14.Trigger;
+
+[RegisterComponent]
+[Access(typeof(RMCTriggerSystem))]
+public sealed partial class TriggerOnFixedDistanceStopComponent : Component
+{
+ [DataField]
+ public TimeSpan Delay;
+}
diff --git a/Content.Server/ADT/_RMC14/Weapons/Melee/RMCMeleeWeaponSystem.cs b/Content.Server/ADT/_RMC14/Weapons/Melee/RMCMeleeWeaponSystem.cs
new file mode 100644
index 00000000000..7df99629895
--- /dev/null
+++ b/Content.Server/ADT/_RMC14/Weapons/Melee/RMCMeleeWeaponSystem.cs
@@ -0,0 +1,5 @@
+using Content.Shared._RMC14.Weapons.Melee;
+
+namespace Content.Server._RMC14.Weapons.Melee;
+
+public sealed class RMCMeleeWeaponSystem : SharedRMCMeleeWeaponSystem;
diff --git a/Content.Server/ADT/_RMC14/Weapons/Ranged/PumpActionSystem.cs b/Content.Server/ADT/_RMC14/Weapons/Ranged/PumpActionSystem.cs
new file mode 100644
index 00000000000..b1b08add8d6
--- /dev/null
+++ b/Content.Server/ADT/_RMC14/Weapons/Ranged/PumpActionSystem.cs
@@ -0,0 +1,5 @@
+using Content.Shared._RMC14.Weapons.Ranged;
+
+namespace Content.Server._RMC14.Weapons.Ranged;
+
+public sealed class PumpActionSystem : SharedPumpActionSystem;
diff --git a/Content.Shared/ADT/Crawling/Components/AntiLyingWarriorComponent.cs b/Content.Shared/ADT/Crawling/Components/AntiLyingWarriorComponent.cs
new file mode 100644
index 00000000000..a983e677250
--- /dev/null
+++ b/Content.Shared/ADT/Crawling/Components/AntiLyingWarriorComponent.cs
@@ -0,0 +1,8 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.ADT.Crawling;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class AntiLyingWarriorComponent : Component
+{
+}
diff --git a/Content.Shared/ADT/_RMC14/Actions/ActionCooldownComponent.cs b/Content.Shared/ADT/_RMC14/Actions/ActionCooldownComponent.cs
new file mode 100644
index 00000000000..0b9637ea10c
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Actions/ActionCooldownComponent.cs
@@ -0,0 +1,11 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._RMC14.Actions;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(RMCActionsSystem))]
+public sealed partial class ActionCooldownComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public TimeSpan Cooldown;
+}
diff --git a/Content.Shared/ADT/_RMC14/Actions/ActionReducedUseDelayComponent.cs b/Content.Shared/ADT/_RMC14/Actions/ActionReducedUseDelayComponent.cs
new file mode 100644
index 00000000000..381b3db3d4b
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Actions/ActionReducedUseDelayComponent.cs
@@ -0,0 +1,18 @@
+using Content.Shared.FixedPoint;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared._RMC14.Actions;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(RMCActionsSystem))]
+public sealed partial class ActionReducedUseDelayComponent : Component
+{
+ // Default cooldown without reductions
+ [DataField, AutoNetworkedField]
+ public TimeSpan? UseDelayBase = default!;
+
+ // Cooldown reduction percentage
+ [DataField, AutoNetworkedField]
+ public FixedPoint2 UseDelayReduction = default!;
+}
diff --git a/Content.Shared/ADT/_RMC14/Actions/ActionReducedUseDelayEvent.cs b/Content.Shared/ADT/_RMC14/Actions/ActionReducedUseDelayEvent.cs
new file mode 100644
index 00000000000..b4a1ceab71f
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Actions/ActionReducedUseDelayEvent.cs
@@ -0,0 +1,7 @@
+using Content.Shared.FixedPoint;
+
+namespace Content.Shared._RMC14.Actions;
+
+// If amount is 0, will reset usedelay to default value
+// If amount is between 0 and 1, will reduce usedelay
+public record struct ActionReducedUseDelayEvent(FixedPoint2 Amount);
diff --git a/Content.Shared/ADT/_RMC14/Actions/ActionSharedCooldownComponent.cs b/Content.Shared/ADT/_RMC14/Actions/ActionSharedCooldownComponent.cs
new file mode 100644
index 00000000000..1db4e0888bc
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Actions/ActionSharedCooldownComponent.cs
@@ -0,0 +1,21 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared._RMC14.Actions;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(RMCActionsSystem))]
+public sealed partial class ActionSharedCooldownComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public EntProtoId? Id;
+
+ [DataField, AutoNetworkedField]
+ public HashSet Ids = new();
+
+ [DataField, AutoNetworkedField]
+ public TimeSpan Cooldown;
+
+ [DataField, AutoNetworkedField]
+ public bool OnPerform = true;
+}
diff --git a/Content.Shared/ADT/_RMC14/Actions/RMCActionUseAttemptEvent.cs b/Content.Shared/ADT/_RMC14/Actions/RMCActionUseAttemptEvent.cs
new file mode 100644
index 00000000000..cea0989c2b4
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Actions/RMCActionUseAttemptEvent.cs
@@ -0,0 +1,4 @@
+namespace Content.Shared._RMC14.Actions;
+
+[ByRefEvent]
+public record struct RMCActionUseAttemptEvent(EntityUid User, bool Cancelled = false);
diff --git a/Content.Shared/ADT/_RMC14/Actions/RMCActionUseEvent.cs b/Content.Shared/ADT/_RMC14/Actions/RMCActionUseEvent.cs
new file mode 100644
index 00000000000..cb3490f8e3c
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Actions/RMCActionUseEvent.cs
@@ -0,0 +1,4 @@
+namespace Content.Shared._RMC14.Actions;
+
+[ByRefEvent]
+public readonly record struct RMCActionUseEvent(EntityUid User);
diff --git a/Content.Shared/ADT/_RMC14/Actions/RMCActionsSystem.cs b/Content.Shared/ADT/_RMC14/Actions/RMCActionsSystem.cs
new file mode 100644
index 00000000000..a5f77834033
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Actions/RMCActionsSystem.cs
@@ -0,0 +1,126 @@
+using Content.Shared.Actions;
+using Content.Shared.Actions.Events;
+using Content.Shared.FixedPoint;
+
+namespace Content.Shared._RMC14.Actions;
+
+public sealed class RMCActionsSystem : EntitySystem
+{
+ [Dependency] private readonly SharedActionsSystem _actions = default!;
+
+ private EntityQuery _actionSharedCooldownQuery;
+
+ public override void Initialize()
+ {
+ _actionSharedCooldownQuery = GetEntityQuery();
+
+ SubscribeLocalEvent(OnSharedCooldownPerformed);
+
+ SubscribeLocalEvent(OnCooldownUse);
+
+ SubscribeLocalEvent(OnReducedUseDelayEvent);
+ SubscribeLocalEvent(OnReducedUseDelayEvent);
+ SubscribeLocalEvent(OnReducedUseDelayEvent);
+ SubscribeLocalEvent(OnReducedUseDelayEvent);
+ }
+
+ private void OnSharedCooldownPerformed(Entity ent, ref ActionPerformedEvent args)
+ {
+ if (ent.Comp.OnPerform)
+ ActivateSharedCooldown((ent, ent), args.Performer);
+ }
+
+ public void ActivateSharedCooldown(Entity action, EntityUid performer)
+ {
+ if (!Resolve(action, ref action.Comp, false))
+ return;
+
+ if (action.Comp.Cooldown == TimeSpan.Zero)
+ return;
+
+ foreach (var (actionId, _) in _actions.GetActions(performer))
+ {
+ if (!_actionSharedCooldownQuery.TryComp(actionId, out var shared))
+ continue;
+
+ // Same ID or primary ID found in subset of other action's ids
+ if ((shared.Id != null && shared.Id == action.Comp.Id) || (action.Comp.Id != null && shared.Ids.Contains(action.Comp.Id.Value)))
+ _actions.SetIfBiggerCooldown(actionId, action.Comp.Cooldown);
+ }
+ }
+
+ private void OnReducedUseDelayEvent(EntityUid uid, T component, ActionReducedUseDelayEvent args) where T : BaseActionComponent
+ {
+ if (!TryComp(uid, out ActionReducedUseDelayComponent? comp))
+ return;
+
+ if (args.Amount < 0 || args.Amount > 1)
+ return;
+
+ comp.UseDelayReduction = args.Amount;
+
+ if (TryComp(uid, out ActionSharedCooldownComponent? shared))
+ {
+ if (comp.UseDelayBase == null)
+ comp.UseDelayBase = shared.Cooldown;
+
+ RefreshSharedUseDelay((uid, comp), shared);
+ return;
+ }
+
+ // Should be fine to only set this once as the base use delay should remain constant
+ if (comp.UseDelayBase == null)
+ comp.UseDelayBase = component.UseDelay;
+
+ RefreshUseDelay((uid, comp));
+ }
+
+ private void RefreshUseDelay(Entity ent)
+ {
+ if (ent.Comp.UseDelayBase is not { } delayBase)
+ return;
+
+ var reduction = ent.Comp.UseDelayReduction.Double();
+ var delayNew = delayBase.Multiply(1 - reduction);
+
+ _actions.SetUseDelay(ent.Owner, delayNew);
+ }
+
+ private void RefreshSharedUseDelay(Entity ent, ActionSharedCooldownComponent shared)
+ {
+ if (ent.Comp.UseDelayBase is not { } delayBase)
+ return;
+
+ var reduction = ent.Comp.UseDelayReduction.Double();
+ var delayNew = delayBase.Multiply(1 - reduction);
+
+ shared.Cooldown = delayNew;
+ }
+
+ private void OnCooldownUse(Entity ent, ref RMCActionUseEvent args)
+ {
+ _actions.SetIfBiggerCooldown(ent, ent.Comp.Cooldown);
+ }
+
+ public bool CanUseActionPopup(EntityUid user, EntityUid action)
+ {
+ var ev = new RMCActionUseAttemptEvent(user);
+ RaiseLocalEvent(action, ref ev);
+ return !ev.Cancelled;
+ }
+
+ public void ActionUsed(EntityUid user, EntityUid action)
+ {
+ var ev = new RMCActionUseEvent(user);
+ RaiseLocalEvent(action, ref ev);
+ }
+
+ public bool TryUseAction(EntityUid user, EntityUid action)
+ {
+ if (!CanUseActionPopup(user, action))
+ return false;
+
+ ActionUsed(user, action);
+ return true;
+ }
+}
diff --git a/Content.Shared/ADT/_RMC14/Armor/Magnetic/RMCMagneticItemComponent.cs b/Content.Shared/ADT/_RMC14/Armor/Magnetic/RMCMagneticItemComponent.cs
new file mode 100644
index 00000000000..dc3bf644f13
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Armor/Magnetic/RMCMagneticItemComponent.cs
@@ -0,0 +1,12 @@
+using Content.Shared.Inventory;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._RMC14.Armor.Magnetic;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(RMCMagneticSystem))]
+public sealed partial class RMCMagneticItemComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public SlotFlags MagnetizeToSlots = SlotFlags.SUITSTORAGE;
+}
diff --git a/Content.Shared/ADT/_RMC14/Armor/Magnetic/RMCMagneticSystem.cs b/Content.Shared/ADT/_RMC14/Armor/Magnetic/RMCMagneticSystem.cs
new file mode 100644
index 00000000000..d70d58ea8c7
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Armor/Magnetic/RMCMagneticSystem.cs
@@ -0,0 +1,78 @@
+using Content.Shared.Hands;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Inventory;
+using Content.Shared.Popups;
+
+namespace Content.Shared._RMC14.Armor.Magnetic;
+
+public sealed class RMCMagneticSystem : EntitySystem
+{
+ [Dependency] private readonly InventorySystem _inventory = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnMagneticItemDropped);
+ SubscribeLocalEvent(OnMagneticItemDropAttempt);
+ }
+
+ private void OnMagneticItemDropped(Entity ent, ref DroppedEvent args)
+ {
+ TryReturn(ent, args.User);
+ }
+
+ private void OnMagneticItemDropAttempt(Entity ent, ref DropAttemptEvent args)
+ {
+ args.Cancel();
+ }
+
+ private bool TryReturn(Entity ent, EntityUid user)
+ {
+ var returnComp = EnsureComp(ent);
+ returnComp.User = user;
+
+ Dirty(ent, returnComp);
+ return true;
+ }
+
+ public void SetMagnetizeToSlots(Entity ent, SlotFlags slots)
+ {
+ ent.Comp.MagnetizeToSlots = slots;
+ Dirty(ent);
+ }
+
+ public override void Update(float frameTime)
+ {
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var comp))
+ {
+ if (comp.Returned)
+ continue;
+
+ var user = comp.User;
+ if (!TerminatingOrDeleted(user))
+ {
+ var slots = _inventory.GetSlotEnumerator(user, SlotFlags.SUITSTORAGE);
+ while (slots.MoveNext(out var slot))
+ {
+ if (_inventory.TryGetSlotEntity(user, "outerClothing", out _))
+ {
+ if (_inventory.TryEquip(user, uid, slot.ID, force: true))
+ {
+ var popup = Loc.GetString("rmc-magnetize-return",
+ ("item", uid),
+ ("user", user));
+ _popup.PopupClient(popup, user, user, PopupType.Medium);
+
+ comp.Returned = true;
+ Dirty(uid, comp);
+ break;
+ }
+ }
+ }
+ }
+
+ RemCompDeferred(uid);
+ }
+ }
+}
diff --git a/Content.Shared/ADT/_RMC14/Armor/Magnetic/RMCReturnToInventoryComponent.cs b/Content.Shared/ADT/_RMC14/Armor/Magnetic/RMCReturnToInventoryComponent.cs
new file mode 100644
index 00000000000..23dbc58ac0d
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Armor/Magnetic/RMCReturnToInventoryComponent.cs
@@ -0,0 +1,14 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._RMC14.Armor.Magnetic;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(RMCMagneticSystem))]
+public sealed partial class RMCReturnToInventoryComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public EntityUid User;
+
+ [DataField, AutoNetworkedField]
+ public bool Returned;
+}
diff --git a/Content.Shared/ADT/_RMC14/Attachable/AttachableData.cs b/Content.Shared/ADT/_RMC14/Attachable/AttachableData.cs
new file mode 100644
index 00000000000..24719659bd6
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/AttachableData.cs
@@ -0,0 +1,79 @@
+using Content.Shared.Damage;
+using Content.Shared.FixedPoint;
+using Content.Shared.Weapons.Ranged.Components;
+using Content.Shared.Whitelist;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared._RMC14.Attachable;
+
+[DataRecord, Serializable, NetSerializable]
+public record struct AttachableSlot(
+ bool Locked,
+ EntityWhitelist Whitelist,
+ ProtoId? StartingAttachable
+);
+
+[DataRecord, Serializable, NetSerializable]
+public record struct AttachableModifierConditions(
+ bool UnwieldedOnly,
+ bool WieldedOnly,
+ bool ActiveOnly,
+ bool InactiveOnly,
+ EntityWhitelist? Whitelist,
+ EntityWhitelist? Blacklist
+);
+
+[DataRecord, Serializable, NetSerializable]
+public record struct AttachableWeaponMeleeModifierSet(
+ AttachableModifierConditions? Conditions,
+ DamageSpecifier? BonusDamage,
+ DamageSpecifier? DecreaseDamage
+);
+
+[DataRecord, Serializable, NetSerializable]
+public record struct AttachableWeaponRangedModifierSet(
+ AttachableModifierConditions? Conditions,
+ FixedPoint2 AccuracyAddMult, // Not implemented yet. Added to have all the values already on our attachments, so whoever implements this doesn't need to dig through CM13. Remove this comment once implemented.
+ FixedPoint2 AccuracyMovementPenaltyAddMult, // As above.
+ FixedPoint2 DamageFalloffAddMult, // This affects the damage falloff of all shots fired by the weapon. Conversion to RMC: damage_falloff_mod
+ double BurstScatterAddMult, // This affects scatter during burst and full-auto fire. Conversion to RMC: burst_scatter_mod
+ int ShotsPerBurstFlat, // Modifies the maximum number of shots in a burst.
+ FixedPoint2 DamageAddMult, // Additive multiplier to damage.
+ float RecoilFlat, // How much the camera shakes when you shoot.
+ double ScatterFlat, // Scatter in degrees. This is how far bullets go from where you aim. Conversion to RMC: CM_SCATTER * 2
+ float FireDelayFlat, // The delay between each shot. Conversion to RMC: CM_FIRE_DELAY / 10
+ float ProjectileSpeedFlat // How fast the projectiles move. Conversion to RMC: CM_PROJECTILE_SPEED * 10
+);
+
+[DataRecord, Serializable, NetSerializable]
+public record struct AttachableWeaponFireModesModifierSet(
+ AttachableModifierConditions? Conditions,
+ SelectiveFire ExtraFireModes,
+ SelectiveFire SetFireMode
+);
+
+// SS13 has move delay instead of speed. Move delay isn't implemented here, and approximating it through maths like fire delay is scuffed because of how the events used to change speed work.
+// So instead we take the default speed values and use them to convert it to a multiplier beforehand.
+// Converting from move delay to additive multiplier: 1 / (1 / SS14_SPEED + SS13_MOVE_DELAY / 10) / SS14_SPEED - 1
+// Speed and move delay are inversely proportional. So 1 divided by speed is move delay and vice versa.
+// We then add the ss13 move delay, and divide 1 by the result to convert it back into speed.
+// Then we divide it by the original speed and subtract 1 from the result to get the additive multiplier.
+[DataRecord, Serializable, NetSerializable]
+public record struct AttachableSpeedModifierSet(
+ AttachableModifierConditions? Conditions,
+ float Walk, // Default human walk speed: 2.5f
+ float Sprint // Default human sprint speed: 4.5f
+);
+
+[DataRecord, Serializable, NetSerializable]
+public record struct AttachableSizeModifierSet(
+ AttachableModifierConditions? Conditions,
+ int Size
+);
+
+[DataRecord, Serializable, NetSerializable]
+public record struct AttachableWieldDelayModifierSet(
+ AttachableModifierConditions? Conditions,
+ TimeSpan Delay
+);
diff --git a/Content.Shared/ADT/_RMC14/Attachable/AttachableUI.cs b/Content.Shared/ADT/_RMC14/Attachable/AttachableUI.cs
new file mode 100644
index 00000000000..4bab6dbb2b8
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/AttachableUI.cs
@@ -0,0 +1,35 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared._RMC14.Attachable;
+
+[Serializable, NetSerializable]
+public sealed class AttachableHolderStripUserInterfaceState(Dictionary attachableSlots)
+ : BoundUserInterfaceState
+{
+ public Dictionary AttachableSlots = attachableSlots;
+}
+
+[Serializable, NetSerializable]
+public sealed class AttachableHolderChooseSlotUserInterfaceState(List attachableSlots) : BoundUserInterfaceState
+{
+ public List AttachableSlots = attachableSlots;
+}
+
+[Serializable, NetSerializable]
+public sealed class AttachableHolderDetachMessage(string slot) : BoundUserInterfaceMessage
+{
+ public readonly string Slot = slot;
+}
+
+[Serializable, NetSerializable]
+public sealed class AttachableHolderAttachToSlotMessage(string slot) : BoundUserInterfaceMessage
+{
+ public readonly string Slot = slot;
+}
+
+[Serializable, NetSerializable]
+public enum AttachmentUI : byte
+{
+ StripKey,
+ ChooseSlotKey,
+}
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableAntiLyingWarriorComponent.cs b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableAntiLyingWarriorComponent.cs
new file mode 100644
index 00000000000..4a4d3d6c963
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableAntiLyingWarriorComponent.cs
@@ -0,0 +1,9 @@
+using Content.Shared._RMC14.Attachable.Systems;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._RMC14.Attachable.Components;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class AttachableAntiLyingWarriorComponent : Component
+{
+}
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableComponent.cs b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableComponent.cs
new file mode 100644
index 00000000000..31cd00678e9
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableComponent.cs
@@ -0,0 +1,19 @@
+using Content.Shared._RMC14.Attachable.Systems;
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._RMC14.Attachable.Components;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(AttachableHolderSystem))]
+public sealed partial class AttachableComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public float AttachDoAfter = 1.5f;
+
+ [DataField, AutoNetworkedField]
+ public SoundSpecifier? AttachSound = new SoundPathSpecifier("/Audio/ADT/Attachments/attachment_add.ogg", AudioParams.Default.WithVolume(-6.5f));
+
+ [DataField, AutoNetworkedField]
+ public SoundSpecifier? DetachSound = new SoundPathSpecifier("/Audio/ADT/Attachments/attachment_remove.ogg", AudioParams.Default.WithVolume(-5.5f));
+}
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableDirectionLockedComponent.cs b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableDirectionLockedComponent.cs
new file mode 100644
index 00000000000..24c59fbc609
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableDirectionLockedComponent.cs
@@ -0,0 +1,16 @@
+using Content.Shared._RMC14.Attachable.Systems;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._RMC14.Attachable.Components;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(AttachableToggleableSystem))]
+public sealed partial class AttachableDirectionLockedComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public List AttachableList = new();
+
+ [DataField, AutoNetworkedField]
+ public Direction? LockedDirection;
+}
+
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableGunPreventShootComponent.cs b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableGunPreventShootComponent.cs
new file mode 100644
index 00000000000..b706b01b7aa
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableGunPreventShootComponent.cs
@@ -0,0 +1,15 @@
+using Content.Shared._RMC14.Attachable.Systems;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._RMC14.Attachable.Components;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(AttachableToggleableSystem))]
+public sealed partial class AttachableGunPreventShootComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public bool PreventShoot;
+
+ [DataField, AutoNetworkedField]
+ public string Message = "";
+}
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableHolderComponent.cs b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableHolderComponent.cs
new file mode 100644
index 00000000000..d57a17f1503
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableHolderComponent.cs
@@ -0,0 +1,32 @@
+using Content.Shared._RMC14.Attachable.Systems;
+using Content.Shared.Whitelist;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._RMC14.Attachable.Components;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(AttachableHolderSystem))]
+public sealed partial class AttachableHolderComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public EntityUid? SupercedingAttachable;
+
+ ///
+ /// The key is one of the slot IDs at the bottom of this file.
+ /// Each key is followed by the description of the slot.
+ ///
+ [DataField, AutoNetworkedField]
+ public Dictionary Slots = new();
+}
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Slot IDs should be named as follows: rmc-aslot-SLOTNAME, for example: rmc-aslot-barrel. *
+ * Each slot ID must have a name attached to it in \Resources\Locale\en-US\_RMC14\attachable\attachable.ftl *
+ * The slot list is below. If you add more, list them here so others can use the comment for reference. *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * GUN SLOTS:
+ * rmc-aslot-barrel
+ * rmc-aslot-rail
+ * rmc-aslot-stock
+ * rmc-aslot-underbarrel
+ */
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableMagneticComponent.cs b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableMagneticComponent.cs
new file mode 100644
index 00000000000..575fa3dbeb8
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableMagneticComponent.cs
@@ -0,0 +1,13 @@
+using Content.Shared._RMC14.Attachable.Systems;
+using Content.Shared.Inventory;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._RMC14.Attachable.Components;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(AttachableMagneticSystem))]
+public sealed partial class AttachableMagneticComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public SlotFlags MagnetizeToSlots = SlotFlags.SUITSTORAGE;
+}
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableMovementLockedComponent.cs b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableMovementLockedComponent.cs
new file mode 100644
index 00000000000..84350a654f2
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableMovementLockedComponent.cs
@@ -0,0 +1,13 @@
+using Content.Shared._RMC14.Attachable.Systems;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._RMC14.Attachable.Components;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(AttachableToggleableSystem))]
+public sealed partial class AttachableMovementLockedComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public List AttachableList = new();
+}
+
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Components/AttachablePreventDropToggleableComponent.cs b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachablePreventDropToggleableComponent.cs
new file mode 100644
index 00000000000..747ba3d7f64
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachablePreventDropToggleableComponent.cs
@@ -0,0 +1,8 @@
+using Content.Shared._RMC14.Attachable.Systems;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._RMC14.Attachable.Components;
+
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(AttachablePreventDropSystem))]
+public sealed partial class AttachablePreventDropToggleableComponent : Component;
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Components/AttachablePryingComponent.cs b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachablePryingComponent.cs
new file mode 100644
index 00000000000..42ce4afedb4
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachablePryingComponent.cs
@@ -0,0 +1,8 @@
+using Content.Shared._RMC14.Attachable.Systems;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._RMC14.Attachable.Components;
+
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(AttachablePryingSystem))]
+public sealed partial class AttachablePryingComponent : Component;
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableSideLockedComponent.cs b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableSideLockedComponent.cs
new file mode 100644
index 00000000000..a2cfcaf2776
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableSideLockedComponent.cs
@@ -0,0 +1,18 @@
+using Content.Shared._RMC14.Attachable.Systems;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._RMC14.Attachable.Components;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(AttachableToggleableSystem))]
+public sealed partial class AttachableSideLockedComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public List AttachableList = new();
+
+ ///
+ /// The cardinal direction the attachments are locked into. In this case, direction is counted as a full 180 degrees, rather than 90.
+ ///
+ [DataField, AutoNetworkedField]
+ public Direction? LockedDirection;
+}
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableSilencerComponent.cs b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableSilencerComponent.cs
new file mode 100644
index 00000000000..58fd203003b
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableSilencerComponent.cs
@@ -0,0 +1,13 @@
+using Content.Shared._RMC14.Attachable.Systems;
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._RMC14.Attachable.Components;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(AttachableSilencerSystem))]
+public sealed partial class AttachableSilencerComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public SoundSpecifier Sound = new SoundCollectionSpecifier("ADTSilencedShoot");
+}
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableSizeModsComponent.cs b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableSizeModsComponent.cs
new file mode 100644
index 00000000000..ad86fe9a910
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableSizeModsComponent.cs
@@ -0,0 +1,12 @@
+using Content.Shared._RMC14.Attachable.Systems;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._RMC14.Attachable.Components;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(AttachableModifiersSystem))]
+public sealed partial class AttachableSizeModsComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public List Modifiers = new();
+}
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableSpeedModsComponent.cs b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableSpeedModsComponent.cs
new file mode 100644
index 00000000000..7af2a4b6cf5
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableSpeedModsComponent.cs
@@ -0,0 +1,13 @@
+using Content.Shared._RMC14.Attachable.Systems;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._RMC14.Attachable.Components;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(AttachableModifiersSystem))]
+public sealed partial class AttachableSpeedModsComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public List Modifiers = new();
+}
+
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableTemporarySpeedModsComponent.cs b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableTemporarySpeedModsComponent.cs
new file mode 100644
index 00000000000..aa898116ac3
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableTemporarySpeedModsComponent.cs
@@ -0,0 +1,19 @@
+using Content.Shared._RMC14.Attachable.Events;
+using Content.Shared._RMC14.Attachable.Systems;
+using Content.Shared._RMC14.Movement;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._RMC14.Attachable.Components;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(AttachableTemporarySpeedModsSystem))]
+public sealed partial class AttachableTemporarySpeedModsComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public AttachableAlteredType Alteration = AttachableAlteredType.Interrupted;
+
+ [DataField, AutoNetworkedField]
+ public List Modifiers = new();
+}
+
+
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableToggleableComponent.cs b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableToggleableComponent.cs
new file mode 100644
index 00000000000..66520bbccea
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableToggleableComponent.cs
@@ -0,0 +1,148 @@
+using Content.Shared._RMC14.Attachable.Systems;
+using Content.Shared.Whitelist;
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Utility;
+
+namespace Content.Shared._RMC14.Attachable.Components;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(AttachableToggleableSystem))]
+public sealed partial class AttachableToggleableComponent : Component
+{
+ ///
+ /// Whether the attachment is currently active.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool Active = false;
+
+ ///
+ /// If set to true, the attachment will deactivate upon switching hands.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool NeedHand = false;
+
+ ///
+ /// If set to true, the attachment will not toggle itself when its action is interrupted. Used in cases where the item toggles itself separately, like scopes.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool DoInterrupt = false;
+
+ ///
+ /// If set to true, the attachment will deactivate upon moving.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool BreakOnMove = false;
+
+ ///
+ /// If set to true, the attachment will deactivate upon rotating to any direction other than the one it was activated in.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool BreakOnRotate = false;
+
+ ///
+ /// If set to true, the attachment will deactivate upon rotating 90 degrees away from the one it was activated in.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool BreakOnFullRotate = false;
+
+ ///
+ /// If set to true, the attachment can only be toggled when the holder is wielded.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool WieldedOnly = false;
+
+ ///
+ /// If set to true, the attachment can only be used when the holder is wielded.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool WieldedUseOnly = false;
+
+ ///
+ /// If set to true, the attachment can only be activated when someone is holding it.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool HeldOnlyActivate = false;
+
+ ///
+ /// Only the person holding or wearing the holder can activate this attachment.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool UserOnly = false;
+
+ [DataField, AutoNetworkedField]
+ public TimeSpan UseDelay = TimeSpan.FromSeconds(0f);
+
+ [DataField, AutoNetworkedField]
+ public float DoAfter;
+
+ [DataField, AutoNetworkedField]
+ public float? DeactivateDoAfter;
+
+ [DataField, AutoNetworkedField]
+ public bool DoAfterNeedHand = true;
+
+ [DataField, AutoNetworkedField]
+ public bool DoAfterBreakOnMove = true;
+
+ [DataField, AutoNetworkedField]
+ public AttachableInstantToggleConditions InstantToggle = AttachableInstantToggleConditions.None;
+
+ ///
+ /// If set to true, this attachment will block some of the holder's functionality when active and perform it instead.
+ /// Used for attached weapons, like the UGL.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool SupercedeHolder = false;
+
+ ///
+ /// If set to true, this attachment's functions only work when it's attached to a holder.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool AttachedOnly = false;
+
+ [DataField, AutoNetworkedField]
+ public SoundSpecifier? ActivateSound = new SoundPathSpecifier("/Audio/ADT/Attachments/attachment_activate.ogg");
+
+ [DataField, AutoNetworkedField]
+ public SoundSpecifier? DeactivateSound = new SoundPathSpecifier("/Audio/ADT/Attachments/attachment_deactivate.ogg");
+
+ [DataField, AutoNetworkedField]
+ public bool ShowTogglePopup = true;
+
+ [DataField, AutoNetworkedField]
+ public LocId ActivatePopupText = new LocId("attachable-popup-activate-generic");
+
+ [DataField, AutoNetworkedField]
+ public LocId DeactivatePopupText = new LocId("attachable-popup-deactivate-generic");
+
+ [DataField, AutoNetworkedField]
+ public EntityUid? Action;
+
+ [DataField, AutoNetworkedField]
+ public string ActionId = "ADTActionToggleAttachable";
+
+ [DataField, AutoNetworkedField]
+ public string ActionName = "Toggle Attachable";
+
+ [DataField, AutoNetworkedField]
+ public string ActionDesc = "Toggle an attachable. If you're seeing this, someone forgot to set the description properly.";
+
+ [DataField, AutoNetworkedField]
+ public EntityWhitelist? ActionsToRelayWhitelist;
+
+ [DataField, AutoNetworkedField]
+ public SpriteSpecifier Icon = new SpriteSpecifier.Rsi(new ResPath("_RMC14/Objects/Weapons/Guns/Attachments/rail.rsi"), "flashlight");
+
+ [DataField, AutoNetworkedField]
+ public SpriteSpecifier? IconActive;
+
+ [DataField, AutoNetworkedField]
+ public bool Attached = false;
+}
+
+public enum AttachableInstantToggleConditions : byte
+{
+ None = 0,
+ Brace = 1 << 0
+}
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableToggleablePreventShootComponent.cs b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableToggleablePreventShootComponent.cs
new file mode 100644
index 00000000000..82dd1ed5c36
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableToggleablePreventShootComponent.cs
@@ -0,0 +1,15 @@
+using Content.Shared._RMC14.Attachable.Systems;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._RMC14.Attachable.Components;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(AttachableToggleableSystem))]
+public sealed partial class AttachableToggleablePreventShootComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public bool ShootWhenActive = true;
+
+ [DataField, AutoNetworkedField]
+ public string Message = "You can't shoot this right now!";
+}
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableToggleableSimpleActivateComponent.cs b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableToggleableSimpleActivateComponent.cs
new file mode 100644
index 00000000000..16b531a14bf
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableToggleableSimpleActivateComponent.cs
@@ -0,0 +1,8 @@
+using Content.Shared._RMC14.Attachable.Systems;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._RMC14.Attachable.Components;
+
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(AttachableToggleableSystem))]
+public sealed partial class AttachableToggleableSimpleActivateComponent : Component;
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableWeaponMeleeModsComponent.cs b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableWeaponMeleeModsComponent.cs
new file mode 100644
index 00000000000..3f20d62f373
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableWeaponMeleeModsComponent.cs
@@ -0,0 +1,12 @@
+using Content.Shared._RMC14.Attachable.Systems;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._RMC14.Attachable.Components;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(AttachableModifiersSystem))]
+public sealed partial class AttachableWeaponMeleeModsComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public List Modifiers = new();
+}
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableWeaponRangedModsComponent.cs b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableWeaponRangedModsComponent.cs
new file mode 100644
index 00000000000..f4ba342fd31
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableWeaponRangedModsComponent.cs
@@ -0,0 +1,15 @@
+using Content.Shared._RMC14.Attachable.Systems;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._RMC14.Attachable.Components;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(AttachableModifiersSystem))]
+public sealed partial class AttachableWeaponRangedModsComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public List Modifiers = new();
+
+ [DataField, AutoNetworkedField]
+ public List? FireModeMods;
+}
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableWieldDelayModsComponent.cs b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableWieldDelayModsComponent.cs
new file mode 100644
index 00000000000..de609b9e813
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Components/AttachableWieldDelayModsComponent.cs
@@ -0,0 +1,13 @@
+using Content.Shared._RMC14.Attachable.Systems;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._RMC14.Attachable.Components;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(AttachableModifiersSystem))]
+public sealed partial class AttachableWieldDelayModsComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public List Modifiers = new();
+}
+
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Events/AttachableAlteredEvent.cs b/Content.Shared/ADT/_RMC14/Attachable/Events/AttachableAlteredEvent.cs
new file mode 100644
index 00000000000..838ca6cc4cf
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Events/AttachableAlteredEvent.cs
@@ -0,0 +1,21 @@
+namespace Content.Shared._RMC14.Attachable.Events;
+
+[ByRefEvent]
+public readonly record struct AttachableAlteredEvent(
+ EntityUid Holder,
+ AttachableAlteredType Alteration,
+ EntityUid? User = null
+);
+
+public enum AttachableAlteredType : byte
+{
+ Attached = 1 << 0,
+ Detached = 1 << 1,
+ Wielded = 1 << 2,
+ Unwielded = 1 << 3,
+ Activated = 1 << 4,
+ Deactivated = 1 << 5,
+ Interrupted = 1 << 6, // This is used when a toggleable attachment is deactivated by something other than its hotkey or action.
+ AppearanceChanged = 1 << 7,
+ DetachedDeactivated = Detached | Deactivated,
+}
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Events/AttachableAttachDoAfterEvent.cs b/Content.Shared/ADT/_RMC14/Attachable/Events/AttachableAttachDoAfterEvent.cs
new file mode 100644
index 00000000000..124ac4ba79d
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Events/AttachableAttachDoAfterEvent.cs
@@ -0,0 +1,15 @@
+using Content.Shared.DoAfter;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared._RMC14.Attachable.Events;
+
+[Serializable, NetSerializable]
+public sealed partial class AttachableAttachDoAfterEvent : SimpleDoAfterEvent
+{
+ public readonly string SlotId;
+
+ public AttachableAttachDoAfterEvent(string slotId)
+ {
+ SlotId = slotId;
+ }
+}
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Events/AttachableDetachDoAfterEvent.cs b/Content.Shared/ADT/_RMC14/Attachable/Events/AttachableDetachDoAfterEvent.cs
new file mode 100644
index 00000000000..bbe430752b4
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Events/AttachableDetachDoAfterEvent.cs
@@ -0,0 +1,7 @@
+using Content.Shared.DoAfter;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared._RMC14.Attachable.Events;
+
+[Serializable, NetSerializable]
+public sealed partial class AttachableDetachDoAfterEvent : SimpleDoAfterEvent;
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Events/AttachableGetExamineDataEvent.cs b/Content.Shared/ADT/_RMC14/Attachable/Events/AttachableGetExamineDataEvent.cs
new file mode 100644
index 00000000000..6becd4e87fc
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Events/AttachableGetExamineDataEvent.cs
@@ -0,0 +1,4 @@
+namespace Content.Shared._RMC14.Attachable.Events;
+
+[ByRefEvent]
+public readonly record struct AttachableGetExamineDataEvent(Dictionary effectStrings)> Data);
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Events/AttachableHolderAttachablesAlteredEvent.cs b/Content.Shared/ADT/_RMC14/Attachable/Events/AttachableHolderAttachablesAlteredEvent.cs
new file mode 100644
index 00000000000..0d60bfd8da9
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Events/AttachableHolderAttachablesAlteredEvent.cs
@@ -0,0 +1,8 @@
+namespace Content.Shared._RMC14.Attachable.Events;
+
+[ByRefEvent]
+public readonly record struct AttachableHolderAttachablesAlteredEvent(
+ EntityUid Attachable,
+ string SlotId,
+ AttachableAlteredType Alteration
+);
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Events/AttachableRelayedEvent.cs b/Content.Shared/ADT/_RMC14/Attachable/Events/AttachableRelayedEvent.cs
new file mode 100644
index 00000000000..d2842104ee4
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Events/AttachableRelayedEvent.cs
@@ -0,0 +1,16 @@
+namespace Content.Shared._RMC14.Attachable.Events;
+
+///
+/// Wrapper for events relayed to attachables by their holder.
+///
+public sealed class AttachableRelayedEvent : EntityEventArgs
+{
+ public TEvent Args;
+ public EntityUid Holder;
+
+ public AttachableRelayedEvent(TEvent args, EntityUid holder)
+ {
+ Args = args;
+ Holder = holder;
+ }
+}
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Events/AttachableToggleDoAfterEvent.cs b/Content.Shared/ADT/_RMC14/Attachable/Events/AttachableToggleDoAfterEvent.cs
new file mode 100644
index 00000000000..1fb9f1e1318
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Events/AttachableToggleDoAfterEvent.cs
@@ -0,0 +1,17 @@
+using Content.Shared.DoAfter;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared._RMC14.Attachable.Events;
+
+[Serializable, NetSerializable]
+public sealed partial class AttachableToggleDoAfterEvent : SimpleDoAfterEvent
+{
+ public readonly string SlotId;
+ public readonly string PopupText;
+
+ public AttachableToggleDoAfterEvent(string slotId, string popupText)
+ {
+ SlotId = slotId;
+ PopupText = popupText;
+ }
+}
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Events/AttachableToggleStartedEvent.cs b/Content.Shared/ADT/_RMC14/Attachable/Events/AttachableToggleStartedEvent.cs
new file mode 100644
index 00000000000..0ec1cba3cd3
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Events/AttachableToggleStartedEvent.cs
@@ -0,0 +1,10 @@
+using Content.Shared._RMC14.Attachable.Components;
+
+namespace Content.Shared._RMC14.Attachable.Events;
+
+[ByRefEvent]
+public readonly record struct AttachableToggleStartedEvent(
+ EntityUid Holder,
+ EntityUid User,
+ string SlotId
+);
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Events/AttachableToggleableInterruptEvent.cs b/Content.Shared/ADT/_RMC14/Attachable/Events/AttachableToggleableInterruptEvent.cs
new file mode 100644
index 00000000000..2f4bbbbcedd
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Events/AttachableToggleableInterruptEvent.cs
@@ -0,0 +1,8 @@
+using Content.Shared._RMC14.Attachable.Components;
+
+namespace Content.Shared._RMC14.Attachable.Events;
+
+[ByRefEvent]
+public readonly record struct AttachableToggleableInterruptEvent(
+ EntityUid User
+);
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Events/GrantAttachableActionsEvent.cs b/Content.Shared/ADT/_RMC14/Attachable/Events/GrantAttachableActionsEvent.cs
new file mode 100644
index 00000000000..2a7357c61c8
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Events/GrantAttachableActionsEvent.cs
@@ -0,0 +1,4 @@
+namespace Content.Shared._RMC14.Attachable.Events;
+
+[ByRefEvent]
+public readonly record struct GrantAttachableActionsEvent(EntityUid User);
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Events/RemoveAttachableActionsEvent.cs b/Content.Shared/ADT/_RMC14/Attachable/Events/RemoveAttachableActionsEvent.cs
new file mode 100644
index 00000000000..231dbfe5bba
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Events/RemoveAttachableActionsEvent.cs
@@ -0,0 +1,4 @@
+namespace Content.Shared._RMC14.Attachable.Events;
+
+[ByRefEvent]
+public readonly record struct RemoveAttachableActionsEvent(EntityUid User);
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachableAntiLyingWarriorSystem.cs b/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachableAntiLyingWarriorSystem.cs
new file mode 100644
index 00000000000..8c554d558e5
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachableAntiLyingWarriorSystem.cs
@@ -0,0 +1,28 @@
+using Content.Shared._RMC14.Attachable.Components;
+using Content.Shared._RMC14.Attachable.Events;
+using Content.Shared.ADT.Crawling;
+
+namespace Content.Shared._RMC14.Attachable.Systems;
+
+public sealed class AttachableAntiLyingWarriorSystem : EntitySystem
+{
+
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnAttachableAltered);
+ }
+
+ private void OnAttachableAltered(Entity attachable, ref AttachableAlteredEvent args)
+ {
+ switch (args.Alteration)
+ {
+ case AttachableAlteredType.Attached:
+ EnsureComp(args.Holder);
+ break;
+
+ case AttachableAlteredType.Detached:
+ RemCompDeferred(args.Holder);
+ break;
+ }
+ }
+}
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachableHolderSystem.cs b/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachableHolderSystem.cs
new file mode 100644
index 00000000000..f3e31709adb
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachableHolderSystem.cs
@@ -0,0 +1,741 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Numerics;
+using Content.Shared._RMC14.Attachable.Components;
+using Content.Shared._RMC14.Attachable.Events;
+using Content.Shared._RMC14.Input;
+using Content.Shared._RMC14.Item;
+using Content.Shared._RMC14.Weapons.Common;
+using Content.Shared._RMC14.Weapons.Ranged;
+using Content.Shared._RMC14.Wieldable.Events;
+using Content.Shared.ActionBlocker;
+using Content.Shared.Containers;
+using Content.Shared.DoAfter;
+using Content.Shared.Hands;
+using Content.Shared.Hands.Components;
+using Content.Shared.Hands.EntitySystems;
+using Content.Shared.Interaction;
+using Content.Shared.Verbs;
+using Content.Shared.Weapons.Melee.Events;
+using Content.Shared.Weapons.Ranged.Components;
+using Content.Shared.Weapons.Ranged.Events;
+using Content.Shared.Weapons.Ranged.Systems;
+using Content.Shared.Whitelist;
+using Content.Shared.Wieldable;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Containers;
+using Robust.Shared.Input.Binding;
+using Robust.Shared.Map;
+
+namespace Content.Shared._RMC14.Attachable.Systems;
+
+public sealed class AttachableHolderSystem : EntitySystem
+{
+ [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
+ [Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedContainerSystem _container = default!;
+ [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+ [Dependency] private readonly SharedGunSystem _gun = default!;
+ [Dependency] private readonly SharedHandsSystem _hands = default!;
+ [Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
+ [Dependency] private readonly SharedVerbSystem _verbSystem = default!;
+
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnAttachDoAfter);
+ SubscribeLocalEvent(OnDetachDoAfter);
+ SubscribeLocalEvent(OnAttachableHolderAttachToSlotMessage);
+ SubscribeLocalEvent(OnAttachableHolderDetachMessage);
+ SubscribeLocalEvent(OnAttachableHolderAttemptShoot);
+ SubscribeLocalEvent(RelayEvent);
+ SubscribeLocalEvent(OnAttachableHolderUiOpened);
+ SubscribeLocalEvent(OnAttached);
+ SubscribeLocalEvent(OnHolderMapInit,
+ after: new[] { typeof(ContainerFillSystem) });
+ SubscribeLocalEvent>(OnAttachableHolderGetVerbs);
+ SubscribeLocalEvent(RelayEvent);
+ SubscribeLocalEvent(RelayEvent);
+ SubscribeLocalEvent(RelayEvent,
+ after: new[] { typeof(WieldableSystem) });
+ SubscribeLocalEvent(OnAttachableHolderInteractUsing);
+ SubscribeLocalEvent(OnAttachableHolderInteractInWorld);
+ SubscribeLocalEvent(OnHolderWielded);
+ SubscribeLocalEvent(OnHolderUnwielded);
+ SubscribeLocalEvent(OnAttachableHolderUniqueAction);
+ SubscribeLocalEvent(RelayEvent);
+ SubscribeLocalEvent(RelayEvent);
+ SubscribeLocalEvent(RelayEvent);
+ SubscribeLocalEvent(RelayEvent);
+ SubscribeLocalEvent(RelayEvent);
+ SubscribeLocalEvent(RelayEvent);
+ SubscribeLocalEvent(RelayEvent);
+ SubscribeLocalEvent(RelayEvent);
+ SubscribeLocalEvent(RelayEvent);
+ SubscribeLocalEvent(RelayEvent);
+ SubscribeLocalEvent(RelayEvent);
+ SubscribeLocalEvent(RelayEvent);
+ SubscribeLocalEvent(RelayEvent);
+
+
+ CommandBinds.Builder
+ .Bind(CMKeyFunctions.RMCActivateAttachableBarrel,
+ InputCmdHandler.FromDelegate(session =>
+ {
+ if (session?.AttachedEntity is { } userUid)
+ ToggleAttachable(userUid, "rmc-aslot-barrel");
+ },
+ handle: false))
+ .Bind(CMKeyFunctions.RMCActivateAttachableRail,
+ InputCmdHandler.FromDelegate(session =>
+ {
+ if (session?.AttachedEntity is { } userUid)
+ ToggleAttachable(userUid, "rmc-aslot-rail");
+ },
+ handle: false))
+ .Bind(CMKeyFunctions.RMCActivateAttachableStock,
+ InputCmdHandler.FromDelegate(session =>
+ {
+ if (session?.AttachedEntity is { } userUid)
+ ToggleAttachable(userUid, "rmc-aslot-stock");
+ },
+ handle: false))
+ .Bind(CMKeyFunctions.RMCActivateAttachableUnderbarrel,
+ InputCmdHandler.FromDelegate(session =>
+ {
+ if (session?.AttachedEntity is { } userUid)
+ ToggleAttachable(userUid, "rmc-aslot-underbarrel");
+ },
+ handle: false))
+ .Bind(CMKeyFunctions.RMCFieldStripHeldItem,
+ InputCmdHandler.FromDelegate(session =>
+ {
+ if (session?.AttachedEntity is { } userUid)
+ FieldStripHeldItem(userUid);
+ },
+ handle: false))
+ .Register();
+ }
+
+ public override void Shutdown()
+ {
+ CommandBinds.Unregister();
+ }
+
+ private void OnHolderMapInit(Entity holder, ref MapInitEvent args)
+ {
+ var xform = Transform(holder.Owner);
+ var coords = new EntityCoordinates(holder.Owner, Vector2.Zero);
+
+ foreach (var slotId in holder.Comp.Slots.Keys)
+ {
+ if (holder.Comp.Slots[slotId].StartingAttachable == null)
+ continue;
+
+ var container = _container.EnsureContainer(holder, slotId);
+ container.OccludesLight = false;
+
+ var attachableUid = Spawn(holder.Comp.Slots[slotId].StartingAttachable, coords);
+ if (!_container.Insert(attachableUid, container, containerXform: xform))
+ continue;
+ }
+
+ Dirty(holder);
+ }
+
+ private void OnAttachableHolderInteractUsing(Entity holder, ref InteractUsingEvent args)
+ {
+ if (CanAttach(holder, args.Used))
+ {
+ StartAttach(holder, args.Used, args.User);
+ args.Handled = true;
+ }
+
+ if (holder.Comp.SupercedingAttachable == null)
+ return;
+
+ var interactUsingEvent = new InteractUsingEvent(args.User,
+ args.Used,
+ holder.Comp.SupercedingAttachable.Value,
+ args.ClickLocation);
+ RaiseLocalEvent(holder.Comp.SupercedingAttachable.Value, interactUsingEvent);
+
+ if (interactUsingEvent.Handled)
+ {
+ args.Handled = true;
+ return;
+ }
+
+ var afterInteractEvent = new AfterInteractEvent(args.User,
+ args.Used,
+ holder.Comp.SupercedingAttachable.Value,
+ args.ClickLocation,
+ true);
+ RaiseLocalEvent(args.Used, afterInteractEvent);
+
+ if (afterInteractEvent.Handled)
+ args.Handled = true;
+ }
+
+ private void OnAttachableHolderInteractInWorld(Entity holder, ref ActivateInWorldEvent args)
+ {
+ if (args.Handled || holder.Comp.SupercedingAttachable == null)
+ return;
+
+ var activateInWorldEvent = new ActivateInWorldEvent(args.User, holder.Comp.SupercedingAttachable.Value, args.Complex);
+ RaiseLocalEvent(holder.Comp.SupercedingAttachable.Value, activateInWorldEvent);
+
+ args.Handled = activateInWorldEvent.Handled;
+ }
+
+ private void OnAttachableHolderAttemptShoot(Entity holder, ref AttemptShootEvent args)
+ {
+ if (args.Cancelled)
+ return;
+
+ if (holder.Comp.SupercedingAttachable == null)
+ return;
+
+ args.Cancelled = true;
+
+ if (!TryComp(holder.Owner, out var holderGunComponent) ||
+ holderGunComponent.ShootCoordinates == null ||
+ !TryComp(holder.Comp.SupercedingAttachable,
+ out var attachableGunComponent))
+ {
+ return;
+ }
+
+ _gun.AttemptShoot(args.User,
+ holder.Comp.SupercedingAttachable.Value,
+ attachableGunComponent,
+ holderGunComponent.ShootCoordinates.Value);
+ }
+
+ private void OnAttachableHolderUniqueAction(Entity holder, ref UniqueActionEvent args)
+ {
+ if (holder.Comp.SupercedingAttachable == null || args.Handled)
+ return;
+
+ RaiseLocalEvent(holder.Comp.SupercedingAttachable.Value, new UniqueActionEvent(args.UserUid));
+ args.Handled = true;
+ }
+
+ private void OnHolderWielded(Entity holder, ref ItemWieldedEvent args)
+ {
+ AlterAllAttachables(holder, AttachableAlteredType.Wielded);
+ }
+
+ private void OnHolderUnwielded(Entity holder, ref ItemUnwieldedEvent args)
+ {
+ AlterAllAttachables(holder, AttachableAlteredType.Unwielded);
+ }
+
+ private void OnAttachableHolderDetachMessage(EntityUid holderUid,
+ AttachableHolderComponent holderComponent,
+ AttachableHolderDetachMessage args)
+ {
+ StartDetach((holderUid, holderComponent), args.Slot, args.Actor);
+ }
+
+ private void OnAttachableHolderGetVerbs(Entity holder, ref GetVerbsEvent args)
+ {
+
+ EnsureSlots(holder);
+ var userUid = args.User;
+
+ foreach (var slotId in holder.Comp.Slots.Keys)
+ {
+ if (_container.TryGetContainer(holder.Owner, slotId, out var container))
+ {
+ foreach (var contained in container.ContainedEntities)
+ {
+ if (!TryComp(contained, out AttachableToggleableComponent? toggleableComponent))
+ continue;
+
+ if (toggleableComponent.UserOnly &&
+ (!TryComp(holder.Owner, out TransformComponent? transformComponent) || !transformComponent.ParentUid.Valid || transformComponent.ParentUid != userUid))
+ {
+ continue;
+ }
+
+ var verb = new EquipmentVerb()
+ {
+ Text = toggleableComponent.ActionName,
+ IconEntity = GetNetEntity(contained),
+ Act = () =>
+ {
+ var ev = new AttachableToggleStartedEvent(holder.Owner, userUid, slotId);
+ RaiseLocalEvent(contained, ref ev);
+ }
+ };
+
+ args.Verbs.Add(verb);
+ }
+ }
+ }
+ }
+
+ private void OnAttachableHolderAttachToSlotMessage(EntityUid holderUid,
+ AttachableHolderComponent holderComponent,
+ AttachableHolderAttachToSlotMessage args)
+ {
+ TryComp(args.Actor, out var handsComponent);
+
+ if (handsComponent == null)
+ return;
+
+ _hands.TryGetActiveItem((args.Actor, handsComponent), out var attachableUid);
+
+ if (attachableUid == null)
+ return;
+
+ StartAttach((holderUid, holderComponent), attachableUid.Value, args.Actor, args.Slot);
+ }
+
+ private void OnAttachableHolderUiOpened(EntityUid holderUid,
+ AttachableHolderComponent holderComponent,
+ BoundUIOpenedEvent args)
+ {
+ UpdateStripUi(holderUid);
+ }
+
+ public void StartAttach(Entity holder,
+ EntityUid attachableUid,
+ EntityUid userUid,
+ string slotId = "")
+ {
+
+ var validSlots = GetValidSlots(holder, attachableUid);
+
+ if (validSlots.Count == 0)
+ return;
+
+ if (string.IsNullOrEmpty(slotId))
+ {
+ if (validSlots.Count > 1)
+ {
+ TryComp(holder.Owner,
+ out var userInterfaceComponent);
+ _ui.OpenUi((holder.Owner, userInterfaceComponent), AttachmentUI.ChooseSlotKey, userUid);
+
+ var state =
+ new AttachableHolderChooseSlotUserInterfaceState(validSlots);
+ _ui.SetUiState(holder.Owner, AttachmentUI.ChooseSlotKey, state);
+ return;
+ }
+
+ slotId = validSlots[0];
+ }
+
+ _doAfter.TryStartDoAfter(new DoAfterArgs(
+ EntityManager,
+ userUid,
+ Comp(attachableUid).AttachDoAfter,
+ new AttachableAttachDoAfterEvent(slotId),
+ holder,
+ target: holder.Owner,
+ used: attachableUid)
+ {
+ NeedHand = true,
+ BreakOnMove = true,
+ });
+ }
+
+ private void OnAttachDoAfter(EntityUid uid, AttachableHolderComponent component, AttachableAttachDoAfterEvent args)
+ {
+ if (args.Cancelled || args.Handled)
+ return;
+
+ if (args.Target is not { } target || args.Used is not { } used)
+ return;
+
+ if (!TryComp(args.Target, out AttachableHolderComponent? holder) ||
+ !HasComp(args.Used))
+ return;
+
+ if (Attach((target, holder), used, args.User, args.SlotId))
+ args.Handled = true;
+ }
+
+ public bool Attach(Entity holder,
+ EntityUid attachableUid,
+ EntityUid userUid,
+ string slotId = "")
+ {
+ if (!CanAttach(holder, attachableUid, ref slotId))
+ return false;
+
+ var container = _container.EnsureContainer(holder, slotId);
+ container.OccludesLight = false;
+
+ if (container.Count > 0 && !Detach(holder, container.ContainedEntities[0], userUid, slotId))
+ return false;
+
+ if (!_container.Insert(attachableUid, container))
+ return false;
+
+ if (_hands.IsHolding(userUid, holder.Owner))
+ {
+ var addEv = new GrantAttachableActionsEvent(userUid);
+ RaiseLocalEvent(attachableUid, ref addEv);
+ }
+
+ Dirty(holder);
+
+ _gun.RefreshModifiers(holder.Owner);
+ _audio.PlayPredicted(Comp(attachableUid).AttachSound,
+ holder,
+ userUid);
+
+ return true;
+ }
+
+ private void OnAttached(Entity holder, ref EntInsertedIntoContainerMessage args)
+ {
+ if (!TryComp(args.Entity, out AttachableComponent? attachableComponent) || !holder.Comp.Slots.ContainsKey(args.Container.ID))
+ return;
+
+ UpdateStripUi(holder.Owner, holder.Comp);
+
+ var ev = new AttachableAlteredEvent(holder.Owner, AttachableAlteredType.Attached);
+ RaiseLocalEvent(args.Entity, ref ev);
+
+ var holderEv = new AttachableHolderAttachablesAlteredEvent(args.Entity, args.Container.ID, AttachableAlteredType.Attached);
+ RaiseLocalEvent(holder, ref holderEv);
+ }
+
+ //Detaching
+ public void StartDetach(Entity holder, string slotId, EntityUid userUid)
+ {
+ if (TryGetAttachable(holder, slotId, out var attachable) && holder.Comp.Slots.ContainsKey(slotId) && !holder.Comp.Slots[slotId].Locked)
+ StartDetach(holder, attachable.Owner, userUid);
+ }
+
+ public void StartDetach(Entity holder, EntityUid attachableUid, EntityUid userUid)
+ {
+
+ var delay = Comp(attachableUid).AttachDoAfter;
+ var args = new DoAfterArgs(
+ EntityManager,
+ userUid,
+ delay,
+ new AttachableDetachDoAfterEvent(),
+ holder,
+ holder.Owner,
+ attachableUid)
+ {
+ NeedHand = true,
+ BreakOnMove = true,
+ };
+
+ _doAfter.TryStartDoAfter(args);
+ }
+
+ private void OnDetachDoAfter(EntityUid uid, AttachableHolderComponent component, AttachableDetachDoAfterEvent args)
+ {
+ if (args.Cancelled || args.Handled || args.Target == null || args.Used == null)
+ return;
+
+ if (!TryComp(args.Target, out AttachableHolderComponent? holderComponent) || !HasComp(args.Used))
+ return;
+
+ if (!Detach((args.Target.Value, holderComponent), args.Used.Value, args.User))
+ return;
+
+ args.Handled = true;
+ }
+
+ public bool Detach(Entity holder,
+ EntityUid attachableUid,
+ EntityUid userUid,
+ string? slotId = null)
+ {
+ if (TerminatingOrDeleted(holder) || !holder.Comp.Running)
+ return false;
+
+ if (string.IsNullOrEmpty(slotId) && !TryGetSlotId(holder.Owner, attachableUid, out slotId))
+ return false;
+
+ if (!_container.TryGetContainer(holder, slotId, out var container) || container.Count <= 0)
+ return false;
+
+ if (!TryGetAttachable(holder, slotId, out var attachable))
+ return false;
+
+ if (!_container.Remove(attachable.Owner, container, force: true))
+ return false;
+
+ UpdateStripUi(holder.Owner, holder.Comp);
+
+ var ev = new AttachableAlteredEvent(holder.Owner, AttachableAlteredType.Detached, userUid);
+ RaiseLocalEvent(attachableUid, ref ev);
+
+ var holderEv = new AttachableHolderAttachablesAlteredEvent(attachableUid, slotId, AttachableAlteredType.Detached);
+ RaiseLocalEvent(holder.Owner, ref holderEv);
+
+ var removeEv = new RemoveAttachableActionsEvent(userUid);
+ RaiseLocalEvent(attachableUid, ref removeEv);
+
+ _audio.PlayPredicted(Comp(attachableUid).DetachSound,
+ holder,
+ userUid);
+
+ Dirty(holder);
+ _gun.RefreshModifiers(holder.Owner);
+ _hands.TryPickupAnyHand(userUid, attachable);
+ return true;
+ }
+
+ private bool CanAttach(Entity holder, EntityUid attachableUid)
+ {
+ var slotId = "";
+ return CanAttach(holder, attachableUid, ref slotId);
+ }
+
+ private bool CanAttach(Entity holder, EntityUid attachableUid, ref string slotId)
+ {
+ if (!HasComp(attachableUid))
+ return false;
+
+ if (!string.IsNullOrWhiteSpace(slotId))
+ return _whitelist.IsWhitelistPass(holder.Comp.Slots[slotId].Whitelist, attachableUid);
+
+ foreach (var key in holder.Comp.Slots.Keys)
+ {
+ if (_whitelist.IsWhitelistPass(holder.Comp.Slots[key].Whitelist, attachableUid))
+ {
+ slotId = key;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private Dictionary GetSlotsForStripUi(Entity holder)
+ {
+ var result = new Dictionary();
+ var metaQuery = GetEntityQuery();
+
+ foreach (var slotId in holder.Comp.Slots.Keys)
+ {
+ if (TryGetAttachable(holder, slotId, out var attachable) &&
+ metaQuery.TryGetComponent(attachable.Owner, out var metadata))
+ {
+ result.Add(slotId, (metadata.EntityName, holder.Comp.Slots[slotId].Locked));
+ }
+ else
+ {
+ result.Add(slotId, (null, holder.Comp.Slots[slotId].Locked));
+ }
+ }
+
+ return result;
+ }
+
+ public bool TryGetAttachable(Entity holder,
+ string slotId,
+ out Entity attachable)
+ {
+ attachable = default;
+
+ if (!_container.TryGetContainer(holder, slotId, out var container) || container.Count <= 0)
+ return false;
+
+ var ent = container.ContainedEntities[0];
+ if (!TryComp(ent, out AttachableComponent? attachableComp))
+ return false;
+
+ attachable = (ent, attachableComp);
+ return true;
+ }
+
+ private void UpdateStripUi(EntityUid holderUid, AttachableHolderComponent? holderComponent = null)
+ {
+ if (!Resolve(holderUid, ref holderComponent))
+ return;
+
+ var state =
+ new AttachableHolderStripUserInterfaceState(GetSlotsForStripUi((holderUid, holderComponent)));
+ _ui.SetUiState(holderUid, AttachmentUI.StripKey, state);
+ }
+
+ private void EnsureSlots(Entity holder)
+ {
+ foreach (var slotId in holder.Comp.Slots.Keys)
+ {
+ var container = _container.EnsureContainer(holder, slotId);
+ container.OccludesLight = false;
+ }
+ }
+
+ private List GetValidSlots(Entity holder, EntityUid attachableUid, bool ignoreLock = false)
+ {
+ var list = new List();
+
+ if (!HasComp(attachableUid))
+ return list;
+
+ foreach (var slotId in holder.Comp.Slots.Keys)
+ {
+ if (_whitelist.IsWhitelistPass(holder.Comp.Slots[slotId].Whitelist, attachableUid) && (!ignoreLock || !holder.Comp.Slots[slotId].Locked))
+ list.Add(slotId);
+ }
+
+ return list;
+ }
+
+ private void ToggleAttachable(EntityUid userUid, string slotId)
+ {
+ if (!TryComp(userUid, out var handsComponent) ||
+ !TryComp(handsComponent.ActiveHandEntity, out var holderComponent))
+ {
+ return;
+ }
+
+ var active = handsComponent.ActiveHandEntity;
+ if (!holderComponent.Running || !_actionBlocker.CanInteract(userUid, active))
+ return;
+
+ if (!_container.TryGetContainer(active.Value,
+ slotId,
+ out var container) || container.Count <= 0)
+ return;
+
+ var attachableUid = container.ContainedEntities[0];
+
+ if (!HasComp(attachableUid))
+ return;
+
+ if (!TryComp(attachableUid, out var toggleableComponent))
+ return;
+
+ var ev = new AttachableToggleStartedEvent(active.Value, userUid, slotId);
+ RaiseLocalEvent(attachableUid, ref ev);
+ }
+
+ private void FieldStripHeldItem(EntityUid userUid)
+ {
+ if (!TryComp(userUid, out var handsComponent) ||
+ !TryComp(handsComponent.ActiveHandEntity, out var holderComponent))
+ {
+ return;
+ }
+
+ EntityUid holderUid = handsComponent.ActiveHandEntity.Value;
+
+ if (!holderComponent.Running || !_actionBlocker.CanInteract(userUid, holderUid))
+ return;
+
+ foreach (var verb in _verbSystem.GetLocalVerbs(holderUid, userUid, typeof(Verb)))
+ {
+ if (!verb.Text.Equals(Loc.GetString("rmc-verb-strip-attachables")))
+ continue;
+
+ _verbSystem.ExecuteVerb(verb, userUid, holderUid);
+ break;
+ }
+ }
+
+ public void SetSupercedingAttachable(Entity holder, EntityUid? supercedingAttachable)
+ {
+ holder.Comp.SupercedingAttachable = supercedingAttachable;
+ Dirty(holder);
+ }
+
+ public bool TryGetSlotId(EntityUid holderUid, EntityUid attachableUid, [NotNullWhen(true)] out string? slotId)
+ {
+ slotId = null;
+
+ if (!TryComp(holderUid, out var holderComponent) ||
+ !TryComp(attachableUid, out _))
+ {
+ return false;
+ }
+
+ foreach (var id in holderComponent.Slots.Keys)
+ {
+ if (!_container.TryGetContainer(holderUid, id, out var container) || container.Count <= 0)
+ continue;
+
+ if (container.ContainedEntities[0] != attachableUid)
+ continue;
+
+ slotId = id;
+ return true;
+ }
+
+ return false;
+ }
+
+ public bool HasSlot(Entity holder, string slotId)
+ {
+ if (holder.Comp == null)
+ {
+ if (!TryComp(holder.Owner, out AttachableHolderComponent? holderComponent))
+ return false;
+
+ holder.Comp = holderComponent;
+ }
+
+ return holder.Comp.Slots.ContainsKey(slotId);
+ }
+
+ public bool TryGetHolder(EntityUid attachable, [NotNullWhen(true)] out EntityUid? holderUid)
+ {
+ if (!TryComp(attachable, out TransformComponent? transformComponent) ||
+ !transformComponent.ParentUid.Valid ||
+ !HasComp(transformComponent.ParentUid))
+ {
+ holderUid = null;
+ return false;
+ }
+
+ holderUid = transformComponent.ParentUid;
+ return true;
+ }
+
+ public bool TryGetUser(EntityUid attachable, [NotNullWhen(true)] out EntityUid? userUid)
+ {
+ userUid = null;
+
+ if (!TryGetHolder(attachable, out var holderUid))
+ return false;
+
+ if (!TryComp(holderUid, out TransformComponent? transformComponent) || !transformComponent.ParentUid.Valid)
+ return false;
+
+ userUid = transformComponent.ParentUid;
+ return true;
+ }
+
+ public void RelayEvent(Entity holder, ref T args) where T : notnull
+ {
+ var ev = new AttachableRelayedEvent(args, holder.Owner);
+
+ foreach (var slot in holder.Comp.Slots.Keys)
+ {
+ if (_container.TryGetContainer(holder, slot, out var container))
+ {
+ foreach (var contained in container.ContainedEntities)
+ {
+ RaiseLocalEvent(contained, ev);
+ }
+ }
+ }
+
+ args = ev.Args;
+ }
+
+ private void AlterAllAttachables(Entity holder, AttachableAlteredType alteration)
+ {
+ foreach (var slotId in holder.Comp.Slots.Keys)
+ {
+ if (!_container.TryGetContainer(holder, slotId, out var container) || container.Count <= 0)
+ continue;
+
+ var ev = new AttachableAlteredEvent(holder.Owner, alteration);
+ RaiseLocalEvent(container.ContainedEntities[0], ref ev);
+ }
+ }
+}
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachableMagneticSystem.cs b/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachableMagneticSystem.cs
new file mode 100644
index 00000000000..4ec3042de9a
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachableMagneticSystem.cs
@@ -0,0 +1,30 @@
+using Content.Shared._RMC14.Armor.Magnetic;
+using Content.Shared._RMC14.Attachable.Components;
+using Content.Shared._RMC14.Attachable.Events;
+
+namespace Content.Shared._RMC14.Attachable.Systems;
+
+public sealed class AttachableMagneticSystem : EntitySystem
+{
+ [Dependency] private readonly RMCMagneticSystem _magneticSystem = default!;
+
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnAttachableAltered);
+ }
+
+ private void OnAttachableAltered(Entity attachable, ref AttachableAlteredEvent args)
+ {
+ switch (args.Alteration)
+ {
+ case AttachableAlteredType.Attached:
+ var comp = EnsureComp(args.Holder);
+ _magneticSystem.SetMagnetizeToSlots((args.Holder, comp), attachable.Comp.MagnetizeToSlots);
+ break;
+
+ case AttachableAlteredType.Detached:
+ RemCompDeferred(args.Holder);
+ break;
+ }
+ }
+}
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachableModifiersSystem.Melee.cs b/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachableModifiersSystem.Melee.cs
new file mode 100644
index 00000000000..38f44989db5
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachableModifiersSystem.Melee.cs
@@ -0,0 +1,102 @@
+using Content.Shared._RMC14.Attachable.Components;
+using Content.Shared._RMC14.Attachable.Events;
+using Content.Shared.FixedPoint;
+using Content.Shared.Weapons.Melee.Events;
+
+namespace Content.Shared._RMC14.Attachable.Systems;
+
+public sealed partial class AttachableModifiersSystem : EntitySystem
+{
+ private readonly Dictionary _damage = new();
+
+ private void InitializeMelee()
+ {
+ SubscribeLocalEvent(OnMeleeModsGetExamineData);
+ SubscribeLocalEvent>(OnMeleeModsHitEvent);
+ }
+
+ private void OnMeleeModsGetExamineData(Entity attachable, ref AttachableGetExamineDataEvent args)
+ {
+ foreach (var modSet in attachable.Comp.Modifiers)
+ {
+ var key = GetExamineKey(modSet.Conditions);
+
+ if (!args.Data.ContainsKey(key))
+ args.Data[key] = new (modSet.Conditions, GetEffectStrings(modSet));
+ else
+ args.Data[key].effectStrings.AddRange(GetEffectStrings(modSet));
+ }
+ }
+
+ private List GetEffectStrings(AttachableWeaponMeleeModifierSet modSet)
+ {
+ var result = new List();
+
+
+ if (modSet.BonusDamage != null)
+ {
+ var bonusDamage = modSet.BonusDamage.GetTotal();
+ if (bonusDamage != 0)
+ result.Add(Loc.GetString("rmc-attachable-examine-melee-damage",
+ ("colour", modifierExamineColour), ("sign", bonusDamage > 0 ? '+' : ""), ("damage", bonusDamage)));
+ }
+
+ return result;
+ }
+
+ private void OnMeleeModsHitEvent(Entity attachable, ref AttachableRelayedEvent args)
+ {
+ foreach(var modSet in attachable.Comp.Modifiers)
+ {
+ ApplyModifierSet(attachable, modSet, ref args.Args);
+ }
+ }
+
+ private void ApplyModifierSet(Entity attachable, AttachableWeaponMeleeModifierSet modSet, ref MeleeHitEvent args)
+ {
+ if (!_attachableHolderSystem.TryGetHolder(attachable, out _) ||
+ !CanApplyModifiers(attachable.Owner, modSet.Conditions))
+ {
+ return;
+ }
+
+ if (modSet.BonusDamage != null)
+ args.BonusDamage += modSet.BonusDamage;
+
+ if (modSet.DecreaseDamage != null)
+ {
+ foreach (var (decreaseId, decreaseDmg) in modSet.DecreaseDamage.DamageDict)
+ {
+ if (decreaseDmg <= FixedPoint2.Zero)
+ continue;
+
+ if (args.BaseDamage.DamageDict.TryGetValue(decreaseId, out var baseDamage))
+ args.BaseDamage.DamageDict[decreaseId] = FixedPoint2.Max(baseDamage - decreaseDmg, FixedPoint2.Zero);
+ }
+ }
+
+ if (args.BonusDamage.GetTotal() < FixedPoint2.Zero)
+ {
+ _damage.Clear();
+ foreach (var (bonusId, bonusDmg) in args.BonusDamage.DamageDict)
+ {
+ if (bonusDmg > FixedPoint2.Zero)
+ continue;
+
+ if (!args.BaseDamage.DamageDict.TryGetValue(bonusId, out var baseDamage))
+ {
+ _damage[bonusId] = -bonusDmg;
+ continue;
+ }
+
+ if (-bonusDmg > baseDamage)
+ _damage[bonusId] = -bonusDmg - baseDamage;
+ }
+
+ foreach (var (bonusId, bonusDmg) in _damage)
+ {
+ args.BonusDamage.DamageDict[bonusId] = -bonusDmg;
+ }
+ }
+ }
+}
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachableModifiersSystem.Ranged.cs b/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachableModifiersSystem.Ranged.cs
new file mode 100644
index 00000000000..4652539d339
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachableModifiersSystem.Ranged.cs
@@ -0,0 +1,165 @@
+using Content.Shared._RMC14.Attachable.Components;
+using Content.Shared._RMC14.Attachable.Events;
+using Content.Shared._RMC14.Weapons.Ranged;
+using Content.Shared.Weapons.Ranged.Components;
+using Content.Shared.Weapons.Ranged.Events;
+
+namespace Content.Shared._RMC14.Attachable.Systems;
+
+public sealed partial class AttachableModifiersSystem : EntitySystem
+{
+ private void InitializeRanged()
+ {
+ SubscribeLocalEvent(OnRangedModsAltered);
+ SubscribeLocalEvent(OnRangedModsGetExamineData);
+ SubscribeLocalEvent>(OnRangedGetFireModes);
+ SubscribeLocalEvent>(OnRangedModsGetFireModeValues);
+ SubscribeLocalEvent>(OnRangedModsGetDamageFalloff);
+ SubscribeLocalEvent>(OnRangedModsGetGunDamage);
+ SubscribeLocalEvent>(OnRangedModsRefreshModifiers);
+ }
+
+ private void OnRangedModsGetExamineData(Entity attachable, ref AttachableGetExamineDataEvent args)
+ {
+ foreach (var modSet in attachable.Comp.Modifiers)
+ {
+ var key = GetExamineKey(modSet.Conditions);
+
+ if (!args.Data.ContainsKey(key))
+ args.Data[key] = new (modSet.Conditions, GetEffectStrings(modSet));
+ else
+ args.Data[key].effectStrings.AddRange(GetEffectStrings(modSet));
+ }
+ }
+
+ private List GetEffectStrings(AttachableWeaponRangedModifierSet modSet)
+ {
+ var result = new List();
+
+ if (modSet.ScatterFlat != 0)
+ result.Add(Loc.GetString("rmc-attachable-examine-ranged-scatter",
+ ("colour", modifierExamineColour), ("sign", modSet.ScatterFlat > 0 ? '+' : ""), ("scatter", modSet.ScatterFlat)));
+
+ if (modSet.BurstScatterAddMult != 0)
+ result.Add(Loc.GetString("rmc-attachable-examine-ranged-burst-scatter",
+ ("colour", modifierExamineColour), ("sign", modSet.BurstScatterAddMult > 0 ? '+' : ""), ("burstScatterMult", modSet.BurstScatterAddMult)));
+
+ if (modSet.ShotsPerBurstFlat != 0)
+ result.Add(Loc.GetString("rmc-attachable-examine-ranged-shots-per-burst",
+ ("colour", modifierExamineColour), ("sign", modSet.ShotsPerBurstFlat > 0 ? '+' : ""), ("shots", modSet.ShotsPerBurstFlat)));
+
+ if (modSet.FireDelayFlat != 0)
+ result.Add(Loc.GetString("rmc-attachable-examine-ranged-fire-delay",
+ ("colour", modifierExamineColour), ("sign", modSet.FireDelayFlat > 0 ? '+' : ""), ("fireDelay", modSet.FireDelayFlat)));
+
+ if (modSet.RecoilFlat != 0)
+ result.Add(Loc.GetString("rmc-attachable-examine-ranged-recoil",
+ ("colour", modifierExamineColour), ("sign", modSet.RecoilFlat > 0 ? '+' : ""), ("recoil", modSet.RecoilFlat)));
+
+ if (modSet.DamageAddMult != 0)
+ result.Add(Loc.GetString("rmc-attachable-examine-ranged-damage",
+ ("colour", modifierExamineColour), ("sign", modSet.DamageAddMult > 0 ? '+' : ""), ("damage", modSet.DamageAddMult)));
+
+ if (modSet.ProjectileSpeedFlat != 0)
+ result.Add(Loc.GetString("rmc-attachable-examine-ranged-projectile-speed",
+ ("colour", modifierExamineColour), ("sign", modSet.ProjectileSpeedFlat > 0 ? '+' : ""), ("projectileSpeed", modSet.ProjectileSpeedFlat)));
+
+ if (modSet.DamageFalloffAddMult != 0)
+ result.Add(Loc.GetString("rmc-attachable-examine-ranged-damage-falloff",
+ ("colour", modifierExamineColour), ("sign", modSet.DamageFalloffAddMult > 0 ? '+' : ""), ("falloff", modSet.DamageFalloffAddMult)));
+
+ return result;
+ }
+
+ private void OnRangedModsAltered(Entity attachable, ref AttachableAlteredEvent args)
+ {
+ switch(args.Alteration)
+ {
+ case AttachableAlteredType.AppearanceChanged:
+ break;
+
+ case AttachableAlteredType.DetachedDeactivated:
+ break;
+
+ default:
+
+ if (attachable.Comp.FireModeMods != null)
+ {
+ _rmcSelectiveFireSystem.RefreshFireModes(args.Holder, true);
+ break;
+ }
+
+ _rmcSelectiveFireSystem.RefreshModifiableFireModeValues(args.Holder);
+ break;
+ }
+ }
+
+ private void OnRangedModsRefreshModifiers(Entity attachable, ref AttachableRelayedEvent args)
+ {
+ foreach(var modSet in attachable.Comp.Modifiers)
+ {
+ if (!CanApplyModifiers(attachable.Owner, modSet.Conditions))
+ continue;
+
+ args.Args.ShotsPerBurst = Math.Max(args.Args.ShotsPerBurst + modSet.ShotsPerBurstFlat, 1);
+ args.Args.CameraRecoilScalar = Math.Max(args.Args.CameraRecoilScalar + modSet.RecoilFlat, 0);
+ args.Args.MinAngle = Angle.FromDegrees(Math.Max(args.Args.MinAngle.Degrees + modSet.ScatterFlat, 0.0));
+ args.Args.MaxAngle = Angle.FromDegrees(Math.Max(args.Args.MaxAngle.Degrees + modSet.ScatterFlat, args.Args.MinAngle));
+ args.Args.ProjectileSpeed += modSet.ProjectileSpeedFlat;
+
+ // Fire delay doesn't work quite like SS14 fire rate, so we're having to do maths:
+ // Fire rate is shots per second. Fire delay is the interval between shots. They are inversely proportionate to each other.
+ // First we divide 1 second by the fire rate to get our current fire delay, then we add the delay modifier, then we divide 1 by the result again to get the modified fire rate.
+ var fireDelayMod = args.Args.Gun.Comp.SelectedMode == SelectiveFire.Burst ? modSet.FireDelayFlat / 2f : modSet.FireDelayFlat;
+ args.Args.FireRate = 1f / (1f / args.Args.FireRate + fireDelayMod);
+ }
+ }
+
+ private void OnRangedGetFireModes(Entity attachable, ref AttachableRelayedEvent args)
+ {
+ if (attachable.Comp.FireModeMods == null)
+ return;
+
+ foreach (var modSet in attachable.Comp.FireModeMods)
+ {
+ if (!CanApplyModifiers(attachable.Owner, modSet.Conditions))
+ continue;
+
+ args.Args.Modes |= modSet.ExtraFireModes;
+ args.Args.Set = modSet.SetFireMode;
+ }
+ }
+
+ private void OnRangedModsGetDamageFalloff(Entity attachable, ref AttachableRelayedEvent args)
+ {
+ foreach (var modSet in attachable.Comp.Modifiers)
+ {
+ if (!CanApplyModifiers(attachable.Owner, modSet.Conditions))
+ continue;
+
+ args.Args.FalloffMultiplier += modSet.DamageFalloffAddMult;
+ }
+ }
+
+ private void OnRangedModsGetGunDamage(Entity attachable, ref AttachableRelayedEvent args)
+ {
+ foreach (var modSet in attachable.Comp.Modifiers)
+ {
+ if (!CanApplyModifiers(attachable.Owner, modSet.Conditions))
+ continue;
+
+ args.Args.Multiplier += modSet.DamageAddMult;
+ }
+ }
+
+ private void OnRangedModsGetFireModeValues(Entity attachable, ref AttachableRelayedEvent args)
+ {
+ foreach (var modSet in attachable.Comp.Modifiers)
+ {
+ if (!CanApplyModifiers(attachable.Owner, modSet.Conditions))
+ continue;
+
+ args.Args.BurstScatterMult += modSet.BurstScatterAddMult;
+ }
+ }
+}
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachableModifiersSystem.Size.cs b/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachableModifiersSystem.Size.cs
new file mode 100644
index 00000000000..a5ddbfdac4b
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachableModifiersSystem.Size.cs
@@ -0,0 +1,71 @@
+using Content.Shared._RMC14.Attachable.Components;
+using Content.Shared._RMC14.Attachable.Events;
+using Content.Shared._RMC14.Item;
+
+namespace Content.Shared._RMC14.Attachable.Systems;
+
+public sealed partial class AttachableModifiersSystem : EntitySystem
+{
+ [Dependency] private readonly ItemSizeChangeSystem _itemSizeChangeSystem = default!;
+
+ private void InitializeSize()
+ {
+ SubscribeLocalEvent(OnSizeModsGetExamineData);
+ SubscribeLocalEvent(OnAttachableAltered);
+ SubscribeLocalEvent>(OnGetItemSizeModifiers);
+ }
+
+ private void OnSizeModsGetExamineData(Entity attachable, ref AttachableGetExamineDataEvent args)
+ {
+ foreach (var modSet in attachable.Comp.Modifiers)
+ {
+ var key = GetExamineKey(modSet.Conditions);
+
+ if (!args.Data.ContainsKey(key))
+ args.Data[key] = new (modSet.Conditions, GetEffectStrings(modSet));
+ else
+ args.Data[key].effectStrings.AddRange(GetEffectStrings(modSet));
+ }
+ }
+
+ private List GetEffectStrings(AttachableSizeModifierSet modSet)
+ {
+ var result = new List();
+
+ if (modSet.Size != 0)
+ result.Add(Loc.GetString("rmc-attachable-examine-size",
+ ("colour", modifierExamineColour), ("sign", modSet.Size > 0 ? '+' : ""), ("size", modSet.Size)));
+
+ return result;
+ }
+
+ private void OnAttachableAltered(Entity attachable, ref AttachableAlteredEvent args)
+ {
+ if (attachable.Comp.Modifiers.Count == 0)
+ return;
+
+ switch (args.Alteration)
+ {
+ case AttachableAlteredType.AppearanceChanged:
+ break;
+
+ case AttachableAlteredType.DetachedDeactivated:
+ break;
+
+ default:
+ _itemSizeChangeSystem.RefreshItemSizeModifiers(args.Holder);
+ break;
+ }
+ }
+
+ private void OnGetItemSizeModifiers(Entity attachable, ref AttachableRelayedEvent args)
+ {
+ foreach(var modSet in attachable.Comp.Modifiers)
+ {
+ if (!CanApplyModifiers(attachable.Owner, modSet.Conditions))
+ return;
+
+ args.Args.Size += modSet.Size;
+ }
+ }
+}
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachableModifiersSystem.Speed.cs b/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachableModifiersSystem.Speed.cs
new file mode 100644
index 00000000000..3bfd981f66c
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachableModifiersSystem.Speed.cs
@@ -0,0 +1,78 @@
+using Content.Shared._RMC14.Attachable.Components;
+using Content.Shared._RMC14.Attachable.Events;
+using Content.Shared._RMC14.Wieldable;
+using Content.Shared._RMC14.Wieldable.Events;
+using Content.Shared.Wieldable.Components;
+
+namespace Content.Shared._RMC14.Attachable.Systems;
+
+public sealed partial class AttachableModifiersSystem : EntitySystem
+{
+ private void InitializeSpeed()
+ {
+ SubscribeLocalEvent(OnSpeedModsGetExamineData);
+ SubscribeLocalEvent(OnAttachableAltered);
+ SubscribeLocalEvent>(OnGetSpeedModifiers);
+ }
+
+ private void OnSpeedModsGetExamineData(Entity attachable, ref AttachableGetExamineDataEvent args)
+ {
+ foreach (var modSet in attachable.Comp.Modifiers)
+ {
+ var key = GetExamineKey(modSet.Conditions);
+
+ if (!args.Data.ContainsKey(key))
+ args.Data[key] = new (modSet.Conditions, GetEffectStrings(modSet));
+ else
+ args.Data[key].effectStrings.AddRange(GetEffectStrings(modSet));
+ }
+ }
+
+ private List GetEffectStrings(AttachableSpeedModifierSet modSet)
+ {
+ var result = new List();
+
+ if (modSet.Walk != 0)
+ result.Add(Loc.GetString("rmc-attachable-examine-speed-walk",
+ ("colour", modifierExamineColour), ("sign", modSet.Walk > 0 ? '+' : ""), ("speed", modSet.Walk)));
+
+ if (modSet.Sprint != 0)
+ result.Add(Loc.GetString("rmc-attachable-examine-speed-sprint",
+ ("colour", modifierExamineColour), ("sign", modSet.Sprint > 0 ? '+' : ""), ("speed", modSet.Sprint)));
+
+ return result;
+ }
+
+ private void OnAttachableAltered(Entity attachable, ref AttachableAlteredEvent args)
+ {
+ switch(args.Alteration)
+ {
+ case AttachableAlteredType.AppearanceChanged:
+ break;
+
+ case AttachableAlteredType.DetachedDeactivated:
+ break;
+
+ default:
+ _wieldableSystem.RefreshSpeedModifiers(args.Holder);
+ break;
+ }
+ }
+
+ private void OnGetSpeedModifiers(Entity attachable, ref AttachableRelayedEvent args)
+ {
+ foreach(var modSet in attachable.Comp.Modifiers)
+ {
+ ApplyModifierSet(attachable, modSet, ref args.Args);
+ }
+ }
+
+ private void ApplyModifierSet(Entity attachable, AttachableSpeedModifierSet modSet, ref GetWieldableSpeedModifiersEvent args)
+ {
+ if (!CanApplyModifiers(attachable.Owner, modSet.Conditions))
+ return;
+
+ args.Walk += modSet.Walk;
+ args.Sprint += modSet.Sprint;
+ }
+}
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachableModifiersSystem.WieldDelay.cs b/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachableModifiersSystem.WieldDelay.cs
new file mode 100644
index 00000000000..bfa3d627e3b
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachableModifiersSystem.WieldDelay.cs
@@ -0,0 +1,77 @@
+using Content.Shared._RMC14.Attachable.Components;
+using Content.Shared._RMC14.Attachable.Events;
+using Content.Shared._RMC14.Wieldable.Events;
+
+namespace Content.Shared._RMC14.Attachable.Systems;
+
+public sealed partial class AttachableModifiersSystem : EntitySystem
+{
+ private void InitializeWieldDelay()
+ {
+ SubscribeLocalEvent(OnWieldDelayModsGetExamineData);
+ SubscribeLocalEvent(OnAttachableAltered);
+ SubscribeLocalEvent>(OnGetWieldDelay);
+ }
+
+ private void OnWieldDelayModsGetExamineData(Entity attachable, ref AttachableGetExamineDataEvent args)
+ {
+ foreach (var modSet in attachable.Comp.Modifiers)
+ {
+ var key = GetExamineKey(modSet.Conditions);
+
+ if (!args.Data.ContainsKey(key))
+ args.Data[key] = new (modSet.Conditions, GetEffectStrings(modSet));
+ else
+ args.Data[key].effectStrings.AddRange(GetEffectStrings(modSet));
+ }
+ }
+
+ private List GetEffectStrings(AttachableWieldDelayModifierSet modSet)
+ {
+ var result = new List();
+
+ if (modSet.Delay != TimeSpan.Zero)
+ result.Add(Loc.GetString("rmc-attachable-examine-wield-delay",
+ ("colour", modifierExamineColour), ("sign", modSet.Delay.TotalSeconds > 0 ? '+' : ""), ("delay", modSet.Delay.TotalSeconds)));
+
+ return result;
+ }
+
+ private void OnAttachableAltered(Entity attachable, ref AttachableAlteredEvent args)
+ {
+ switch(args.Alteration)
+ {
+ case AttachableAlteredType.AppearanceChanged:
+ break;
+
+ case AttachableAlteredType.DetachedDeactivated:
+ break;
+
+ case AttachableAlteredType.Wielded:
+ break;
+
+ case AttachableAlteredType.Unwielded:
+ break;
+
+ default:
+ _wieldableSystem.RefreshWieldDelay(args.Holder);
+ break;
+ }
+ }
+
+ private void OnGetWieldDelay(Entity attachable, ref AttachableRelayedEvent args)
+ {
+ foreach(var modSet in attachable.Comp.Modifiers)
+ {
+ ApplyModifierSet(attachable, modSet, ref args.Args);
+ }
+ }
+
+ private void ApplyModifierSet(Entity attachable, AttachableWieldDelayModifierSet modSet, ref GetWieldDelayEvent args)
+ {
+ if (!CanApplyModifiers(attachable.Owner, modSet.Conditions))
+ return;
+
+ args.Delay += modSet.Delay;
+ }
+}
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachableModifiersSystem.cs b/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachableModifiersSystem.cs
new file mode 100644
index 00000000000..605c146d740
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachableModifiersSystem.cs
@@ -0,0 +1,215 @@
+using Content.Shared._RMC14.Attachable.Components;
+using Content.Shared._RMC14.Attachable.Events;
+using Content.Shared._RMC14.Weapons.Ranged;
+using Content.Shared._RMC14.Wieldable;
+using Content.Shared.Examine;
+using Content.Shared.Verbs;
+using Content.Shared.Weapons.Ranged.Systems;
+using Content.Shared.Whitelist;
+using Content.Shared.Wieldable.Components;
+using Robust.Shared.Utility;
+
+namespace Content.Shared._RMC14.Attachable.Systems;
+
+public sealed partial class AttachableModifiersSystem : EntitySystem
+{
+ [Dependency] private readonly AttachableHolderSystem _attachableHolderSystem = default!;
+ [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
+ [Dependency] private readonly ExamineSystemShared _examineSystem = default!;
+ [Dependency] private readonly RMCSelectiveFireSystem _rmcSelectiveFireSystem = default!;
+ [Dependency] private readonly RMCWieldableSystem _wieldableSystem = default!;
+ [Dependency] private readonly SharedGunSystem _gunSystem = default!;
+
+ private const string modifierExamineColour = "yellow";
+
+ public override void Initialize()
+ {
+ SubscribeLocalEvent>(OnAttachableGetExamineVerbs);
+
+ InitializeMelee();
+ InitializeRanged();
+ InitializeSize();
+ InitializeSpeed();
+ InitializeWieldDelay();
+ }
+
+ private void OnAttachableGetExamineVerbs(Entity attachable, ref GetVerbsEvent args)
+ {
+ if (!args.CanInteract || !args.CanAccess)
+ return;
+
+ var ev = new AttachableGetExamineDataEvent(new Dictionary effectStrings)>());
+ RaiseLocalEvent(attachable.Owner, ref ev);
+
+ var message = new FormattedMessage();
+ foreach (var key in ev.Data.Keys)
+ {
+ message.TryAddMarkup(GetExamineConditionText(attachable, ev.Data[key].conditions), out _);
+ message.PushNewline();
+
+ foreach (var effectText in ev.Data[key].effectStrings)
+ {
+ message.TryAddMarkup(" " + effectText, out _);
+ message.PushNewline();
+ }
+ }
+
+ if (!message.IsEmpty)
+ {
+ _examineSystem.AddDetailedExamineVerb(args, attachable.Comp, message,
+ Loc.GetString("rmc-attachable-examinable-verb-text"),
+ "/Textures/Interface/VerbIcons/information.svg.192dpi.png",
+ Loc.GetString("rmc-attachable-examinable-verb-message")
+ );
+ }
+ }
+
+ private string GetExamineConditionText(Entity attachable, AttachableModifierConditions? conditions)
+ {
+ string conditionText = Loc.GetString("rmc-attachable-examine-condition-always");
+
+ if (conditions == null)
+ return conditionText;
+
+ AttachableModifierConditions cond = conditions.Value;
+
+ bool conditionPlaced = false;
+ conditionText = Loc.GetString("rmc-attachable-examine-condition-when") + ' ';
+
+ ExamineConditionAddEntry(cond.WieldedOnly, Loc.GetString("rmc-attachable-examine-condition-wielded"), ref conditionText, ref conditionPlaced);
+ ExamineConditionAddEntry(cond.UnwieldedOnly, Loc.GetString("rmc-attachable-examine-condition-unwielded"), ref conditionText, ref conditionPlaced);
+
+ ExamineConditionAddEntry(
+ cond.ActiveOnly,
+ Loc.GetString("rmc-attachable-examine-condition-active", ("attachable", attachable.Owner)),
+ ref conditionText,
+ ref conditionPlaced);
+
+ ExamineConditionAddEntry(
+ cond.InactiveOnly,
+ Loc.GetString("rmc-attachable-examine-condition-inactive", ("attachable", attachable.Owner)),
+ ref conditionText,
+ ref conditionPlaced);
+
+ if (cond.Whitelist != null)
+ {
+ EntityWhitelist whitelist = cond.Whitelist;
+
+ if (whitelist.Registrations != null)
+ ExamineConditionAddEntry(
+ cond.Whitelist != null,
+ Loc.GetString("rmc-attachable-examine-condition-whitelist-comps", ("compNumber", whitelist.RequireAll ? "all" : "one"), ("comps", String.Join(", ", whitelist.Registrations))),
+ ref conditionText,
+ ref conditionPlaced);
+
+ if (whitelist.Sizes != null)
+ ExamineConditionAddEntry(
+ cond.Whitelist != null,
+ Loc.GetString("rmc-attachable-examine-condition-whitelist-sizes", ("sizes", String.Join(", ", whitelist.Sizes))),
+ ref conditionText,
+ ref conditionPlaced);
+
+ if (whitelist.Tags != null)
+ ExamineConditionAddEntry(
+ cond.Whitelist != null,
+ Loc.GetString("rmc-attachable-examine-condition-whitelist-tags", ("tagNumber", whitelist.RequireAll ? "all" : "one"), ("tags", String.Join(", ", whitelist.Tags))),
+ ref conditionText,
+ ref conditionPlaced);
+ }
+
+ if (cond.Blacklist != null && cond.Blacklist.Tags != null)
+ {
+ EntityWhitelist blacklist = cond.Blacklist;
+
+ if (blacklist.Registrations != null)
+ ExamineConditionAddEntry(
+ cond.Blacklist != null,
+ Loc.GetString("rmc-attachable-examine-condition-blacklist-comps", ("compNumber", blacklist.RequireAll ? "one" : "all"), ("comps", String.Join(", ", blacklist.Registrations))),
+ ref conditionText,
+ ref conditionPlaced);
+
+ if (blacklist.Sizes != null)
+ ExamineConditionAddEntry(
+ cond.Blacklist != null,
+ Loc.GetString("rmc-attachable-examine-condition-blacklist-sizes", ("sizes", String.Join(", ", blacklist.Sizes))),
+ ref conditionText,
+ ref conditionPlaced);
+
+ if (blacklist.Tags != null)
+ ExamineConditionAddEntry(
+ cond.Blacklist != null,
+ Loc.GetString("rmc-attachable-examine-condition-blacklist-tags", ("tagNumber", blacklist.RequireAll ? "one" : "all"), ("tags", String.Join(", ", blacklist.Tags))),
+ ref conditionText,
+ ref conditionPlaced);
+ }
+
+ conditionText += ':';
+
+ return conditionText;
+ }
+
+ private void ExamineConditionAddEntry(bool condition, string text, ref string conditionText, ref bool conditionPlaced)
+ {
+ if (!condition)
+ return;
+
+ if (conditionPlaced)
+ conditionText += "; ";
+ conditionText += text;
+ conditionPlaced = true;
+ }
+
+ private byte GetExamineKey(AttachableModifierConditions? conditions)
+ {
+ byte key = 0;
+
+ if (conditions == null)
+ return key;
+
+ key |= conditions.Value.WieldedOnly ? (byte)(1 << 0) : (byte)0;
+ key |= conditions.Value.UnwieldedOnly ? (byte)(1 << 1) : (byte)0;
+ key |= conditions.Value.ActiveOnly ? (byte)(1 << 2) : (byte)0;
+ key |= conditions.Value.InactiveOnly ? (byte)(1 << 3) : (byte)0;
+ key |= conditions.Value.Whitelist != null ? (byte)(1 << 4) : (byte)0;
+ key |= conditions.Value.Blacklist != null ? (byte)(1 << 5) : (byte)0;
+
+ return key;
+ }
+
+ private bool CanApplyModifiers(EntityUid attachableUid, AttachableModifierConditions? conditions)
+ {
+ if (conditions == null)
+ return true;
+
+ _attachableHolderSystem.TryGetHolder(attachableUid, out var holderUid);
+
+ if (holderUid != null)
+ {
+ TryComp(holderUid, out WieldableComponent? wieldableComponent);
+
+ if (conditions.Value.UnwieldedOnly && wieldableComponent != null && wieldableComponent.Wielded)
+ return false;
+ else if (conditions.Value.WieldedOnly && (wieldableComponent == null || !wieldableComponent.Wielded))
+ return false;
+ }
+
+ TryComp(attachableUid, out AttachableToggleableComponent? toggleableComponent);
+
+ if (conditions.Value.InactiveOnly && toggleableComponent != null && toggleableComponent.Active)
+ return false;
+ else if (conditions.Value.ActiveOnly && (toggleableComponent == null || !toggleableComponent.Active))
+ return false;
+
+
+ if (holderUid != null)
+ {
+ if (conditions.Value.Whitelist != null && _whitelistSystem.IsWhitelistFail(conditions.Value.Whitelist, holderUid.Value))
+ return false;
+
+ if (conditions.Value.Blacklist != null && _whitelistSystem.IsWhitelistPass(conditions.Value.Blacklist, holderUid.Value))
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachablePreventDropSystem.cs b/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachablePreventDropSystem.cs
new file mode 100644
index 00000000000..488eae5db06
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachablePreventDropSystem.cs
@@ -0,0 +1,33 @@
+using Content.Shared._RMC14.Attachable.Components;
+using Content.Shared._RMC14.Attachable.Events;
+using Content.Shared.Interaction.Components;
+
+namespace Content.Shared._RMC14.Attachable.Systems;
+
+public sealed class AttachablePreventDropSystem : EntitySystem
+{
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnAttachableAltered);
+ }
+
+ private void OnAttachableAltered(Entity attachable, ref AttachableAlteredEvent args)
+ {
+ switch (args.Alteration)
+ {
+ case AttachableAlteredType.Activated:
+ var comp = EnsureComp(args.Holder);
+ comp.DeleteOnDrop = false;
+ Dirty(args.Holder, comp);
+ break;
+
+ case AttachableAlteredType.Deactivated:
+ RemCompDeferred(args.Holder);
+ break;
+
+ case AttachableAlteredType.DetachedDeactivated:
+ RemCompDeferred(args.Holder);
+ break;
+ }
+ }
+}
diff --git a/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachablePryingSystem.cs b/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachablePryingSystem.cs
new file mode 100644
index 00000000000..398e85b2daa
--- /dev/null
+++ b/Content.Shared/ADT/_RMC14/Attachable/Systems/AttachablePryingSystem.cs
@@ -0,0 +1,46 @@
+using Content.Shared._RMC14.Attachable.Components;
+using Content.Shared._RMC14.Attachable.Events;
+using Content.Shared.Prying.Components;
+using Content.Shared.Tools.Components;
+using Robust.Shared.Audio;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+
+namespace Content.Shared._RMC14.Attachable.Systems;
+
+public sealed class AttachablePryingSystem : EntitySystem
+{
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnAttachableAltered);
+ }
+
+ private void OnAttachableAltered(Entity ent, ref AttachableAlteredEvent args)
+ {
+ if (_timing.ApplyingState)
+ return;
+
+ switch (args.Alteration)
+ {
+ case AttachableAlteredType.Attached:
+ var prying = EnsureComp(args.Holder);
+ var tool = EnsureComp(args.Holder);
+#pragma warning disable RA0002
+ prying.SpeedModifier = 0.5f;
+ tool.Qualities.Add("Prying", _prototype);
+ tool.UseSound = new SoundPathSpecifier("/Audio/Items/crowbar.ogg");
+#pragma warning restore RA0002
+
+ Dirty(args.Holder, prying);
+ Dirty(args.Holder, tool);
+ break;
+ case AttachableAlteredType.Detached:
+ RemCompDeferred