From e16bca5b181c4d72070513f675cd0d320f7b04ff Mon Sep 17 00:00:00 2001
From: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Date: Mon, 18 Aug 2025 01:47:26 +0200
Subject: [PATCH] Hand pickup and drop triggers (#39663)
---
.../TriggerOnDidEquipHandComponent.cs | 10 +++
.../TriggerOnDidUnequipHandComponent.cs | 10 +++
.../Triggers/TriggerOnDroppedComponent.cs | 10 +++
.../TriggerOnGotEquippedHandComponent.cs | 10 +++
.../TriggerOnGotUnequippedHandComponent.cs | 10 +++
.../Trigger/Systems/HandTriggerSystem.cs | 64 +++++++++++++++++++
6 files changed, 114 insertions(+)
create mode 100644 Content.Shared/Trigger/Components/Triggers/TriggerOnDidEquipHandComponent.cs
create mode 100644 Content.Shared/Trigger/Components/Triggers/TriggerOnDidUnequipHandComponent.cs
create mode 100644 Content.Shared/Trigger/Components/Triggers/TriggerOnDroppedComponent.cs
create mode 100644 Content.Shared/Trigger/Components/Triggers/TriggerOnGotEquippedHandComponent.cs
create mode 100644 Content.Shared/Trigger/Components/Triggers/TriggerOnGotUnequippedHandComponent.cs
create mode 100644 Content.Shared/Trigger/Systems/HandTriggerSystem.cs
diff --git a/Content.Shared/Trigger/Components/Triggers/TriggerOnDidEquipHandComponent.cs b/Content.Shared/Trigger/Components/Triggers/TriggerOnDidEquipHandComponent.cs
new file mode 100644
index 0000000000..d598db6d93
--- /dev/null
+++ b/Content.Shared/Trigger/Components/Triggers/TriggerOnDidEquipHandComponent.cs
@@ -0,0 +1,10 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Trigger.Components.Triggers;
+
+///
+/// Triggers an entity when it is equips an item into one of its hand slots.
+/// The user is the entity that was equipped.
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class TriggerOnDidEquipHandComponent : BaseTriggerOnXComponent;
diff --git a/Content.Shared/Trigger/Components/Triggers/TriggerOnDidUnequipHandComponent.cs b/Content.Shared/Trigger/Components/Triggers/TriggerOnDidUnequipHandComponent.cs
new file mode 100644
index 0000000000..5c3d2da018
--- /dev/null
+++ b/Content.Shared/Trigger/Components/Triggers/TriggerOnDidUnequipHandComponent.cs
@@ -0,0 +1,10 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Trigger.Components.Triggers;
+
+///
+/// Triggers an entity when it is drops an item from one of its hand slots.
+/// The user is the entity that was dropped.
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class TriggerOnDidUnequipHandComponent : BaseTriggerOnXComponent;
diff --git a/Content.Shared/Trigger/Components/Triggers/TriggerOnDroppedComponent.cs b/Content.Shared/Trigger/Components/Triggers/TriggerOnDroppedComponent.cs
new file mode 100644
index 0000000000..6e06216a2c
--- /dev/null
+++ b/Content.Shared/Trigger/Components/Triggers/TriggerOnDroppedComponent.cs
@@ -0,0 +1,10 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Trigger.Components.Triggers;
+
+///
+/// Triggers an entity when it is dropped from a users hands, or directly removed from a users inventory, but not when moved between hands & inventory.
+/// The user is the player that was holding or wearing the item.
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class TriggerOnDroppedComponent : BaseTriggerOnXComponent;
diff --git a/Content.Shared/Trigger/Components/Triggers/TriggerOnGotEquippedHandComponent.cs b/Content.Shared/Trigger/Components/Triggers/TriggerOnGotEquippedHandComponent.cs
new file mode 100644
index 0000000000..24c2d4231c
--- /dev/null
+++ b/Content.Shared/Trigger/Components/Triggers/TriggerOnGotEquippedHandComponent.cs
@@ -0,0 +1,10 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Trigger.Components.Triggers;
+
+///
+/// Triggers an item when it is equipped into a hand slot.
+/// The user is the entity that picked the item up.
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class TriggerOnGotEquippedHandComponent : BaseTriggerOnXComponent;
diff --git a/Content.Shared/Trigger/Components/Triggers/TriggerOnGotUnequippedHandComponent.cs b/Content.Shared/Trigger/Components/Triggers/TriggerOnGotUnequippedHandComponent.cs
new file mode 100644
index 0000000000..624c1a141b
--- /dev/null
+++ b/Content.Shared/Trigger/Components/Triggers/TriggerOnGotUnequippedHandComponent.cs
@@ -0,0 +1,10 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Trigger.Components.Triggers;
+
+///
+/// Triggers an item when it is dropped from a hand slot.
+/// The user is the entity that dropped the item.
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class TriggerOnGotUnequippedHandComponent : BaseTriggerOnXComponent;
diff --git a/Content.Shared/Trigger/Systems/HandTriggerSystem.cs b/Content.Shared/Trigger/Systems/HandTriggerSystem.cs
new file mode 100644
index 0000000000..8001d5d92f
--- /dev/null
+++ b/Content.Shared/Trigger/Systems/HandTriggerSystem.cs
@@ -0,0 +1,64 @@
+using Content.Shared.Hands;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Trigger.Components.Triggers;
+using Robust.Shared.Timing;
+
+namespace Content.Shared.Trigger.Systems;
+
+public sealed partial class HandTriggerSystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly TriggerSystem _trigger = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnGotEquipped);
+ SubscribeLocalEvent(OnGotUnequipped);
+ SubscribeLocalEvent(OnDidEquip);
+ SubscribeLocalEvent(OnDidUnequip);
+ SubscribeLocalEvent(OnDropped);
+ }
+
+ private void OnGotEquipped(Entity ent, ref GotEquippedHandEvent args)
+ {
+ // If the entity was equipped on the server (without prediction) then the container change is networked to the client
+ // which will raise the same event, but the effect of the trigger is already networked on its own. So this guard statement
+ // prevents triggering twice on the client.
+ if (_timing.ApplyingState)
+ return;
+
+ _trigger.Trigger(ent.Owner, args.User, ent.Comp.KeyOut);
+ }
+
+ private void OnGotUnequipped(Entity ent, ref GotUnequippedHandEvent args)
+ {
+ if (_timing.ApplyingState)
+ return;
+
+ _trigger.Trigger(ent.Owner, args.User, ent.Comp.KeyOut);
+ }
+
+ private void OnDidEquip(Entity ent, ref DidEquipHandEvent args)
+ {
+ if (_timing.ApplyingState)
+ return;
+
+ _trigger.Trigger(ent.Owner, args.Equipped, ent.Comp.KeyOut);
+ }
+
+ private void OnDidUnequip(Entity ent, ref DidUnequipHandEvent args)
+ {
+ if (_timing.ApplyingState)
+ return;
+
+ _trigger.Trigger(ent.Owner, args.Unequipped, ent.Comp.KeyOut);
+ }
+
+ private void OnDropped(Entity ent, ref DroppedEvent args)
+ {
+ // We don't need the guard statement here because this one is not a container event, but raised directly when interacting.
+ _trigger.Trigger(ent.Owner, args.User, ent.Comp.KeyOut);
+ }
+}