diff --git a/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs b/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs
index 5770af23aa..36fe75fad7 100644
--- a/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs
+++ b/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs
@@ -152,6 +152,10 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
target = screen.GetClickedEntity(mousePos);
}
+ // Don't light-attack if interaction will be handling this instead
+ if (Interaction.CombatModeCanHandInteract(entity, target))
+ return;
+
RaisePredictiveEvent(new LightAttackEvent(GetNetEntity(target), GetNetEntity(weaponUid), GetNetCoordinates(coordinates)));
}
}
diff --git a/Content.Shared/Interaction/SharedInteractionSystem.cs b/Content.Shared/Interaction/SharedInteractionSystem.cs
index 7d55035d5c..00c357814d 100644
--- a/Content.Shared/Interaction/SharedInteractionSystem.cs
+++ b/Content.Shared/Interaction/SharedInteractionSystem.cs
@@ -262,6 +262,35 @@ namespace Content.Shared.Interaction
return !_tagSystem.HasTag(user, "BypassInteractionRangeChecks");
}
+ ///
+ /// Returns true if the specified entity should hand interact with the target instead of attacking
+ ///
+ /// The user interacting in combat mode
+ /// The target of the interaction
+ ///
+ public bool CombatModeCanHandInteract(EntityUid user, EntityUid? target)
+ {
+ // Always allow attack in these cases
+ if (target == null || !TryComp(user, out var hands) || hands.ActiveHand?.HeldEntity is not null)
+ return false;
+
+ // Only eat input if:
+ // - Target isn't an item
+ // - Target doesn't cancel should-interact event
+ // This is intended to allow items to be picked up in combat mode,
+ // but to also allow items to force attacks anyway (like mobs which are items, e.g. mice)
+ if (!HasComp(target))
+ return false;
+
+ var combatEv = new CombatModeShouldHandInteractEvent();
+ RaiseLocalEvent(target.Value, ref combatEv);
+
+ if (combatEv.Cancelled)
+ return false;
+
+ return true;
+ }
+
///
/// Resolves user interactions with objects.
///
@@ -285,7 +314,8 @@ namespace Content.Shared.Interaction
// TODO this needs to be handled better. This probably bypasses many complex can-interact checks in weird roundabout ways.
if (_actionBlockerSystem.CanInteract(user, target))
{
- UserInteraction(relay.RelayEntity.Value, coordinates, target, altInteract, checkCanInteract, checkAccess, checkCanUse);
+ UserInteraction(relay.RelayEntity.Value, coordinates, target, altInteract, checkCanInteract,
+ checkAccess, checkCanUse);
return;
}
}
@@ -293,10 +323,10 @@ namespace Content.Shared.Interaction
if (target != null && Deleted(target.Value))
return;
- if (!altInteract && TryComp(user, out CombatModeComponent? combatMode) && combatMode.IsInCombatMode)
+ if (!altInteract && TryComp(user, out var combatMode) && combatMode.IsInCombatMode)
{
- // Eat the input
- return;
+ if (!CombatModeCanHandInteract(user, target))
+ return;
}
if (!ValidateInteractAndFace(user, coordinates))
@@ -326,7 +356,7 @@ namespace Content.Shared.Interaction
: !checkAccess || InRangeUnobstructed(user, target.Value); // permits interactions with wall mounted entities
// Does the user have hands?
- if (!TryComp(user, out HandsComponent? hands) || hands.ActiveHand == null)
+ if (!TryComp(user, out var hands) || hands.ActiveHand == null)
{
var ev = new InteractNoHandEvent(user, target, coordinates);
RaiseLocalEvent(user, ev);
@@ -341,6 +371,8 @@ namespace Content.Shared.Interaction
}
// empty-hand interactions
+ // combat mode hand interactions will always be true here -- since
+ // they check this earlier before returning in
if (hands.ActiveHandEntity is not { } held)
{
if (inRangeUnobstructed && target != null)
@@ -1164,4 +1196,12 @@ namespace Content.Shared.Interaction
AltInteract = altInteract;
}
}
+
+ ///
+ /// Raised directed by-ref on an item to determine if hand interactions should go through.
+ /// Defaults to allowing hand interactions to go through. Cancel to force the item to be attacked instead.
+ ///
+ /// Whether the hand interaction should be cancelled.
+ [ByRefEvent]
+ public record struct CombatModeShouldHandInteractEvent(bool Cancelled = false);
}
diff --git a/Content.Shared/Item/SharedItemSystem.cs b/Content.Shared/Item/SharedItemSystem.cs
index bda92c500a..5f890af99f 100644
--- a/Content.Shared/Item/SharedItemSystem.cs
+++ b/Content.Shared/Item/SharedItemSystem.cs
@@ -75,7 +75,7 @@ public abstract class SharedItemSystem : EntitySystem
private void OnHandInteract(EntityUid uid, ItemComponent component, InteractHandEvent args)
{
- if (args.Handled || _combatMode.IsInCombatMode(args.User))
+ if (args.Handled)
return;
args.Handled = _handsSystem.TryPickup(args.User, uid, animateUser: false);
diff --git a/Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs b/Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs
index f4537deede..340a1627d6 100644
--- a/Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs
+++ b/Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs
@@ -1,6 +1,7 @@
using Content.Shared.Bed.Sleep;
using Content.Shared.Emoting;
using Content.Shared.Hands;
+using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Inventory.Events;
using Content.Shared.Item;
@@ -36,6 +37,7 @@ public partial class MobStateSystem
SubscribeLocalEvent(CheckAct);
SubscribeLocalEvent(CheckAct);
SubscribeLocalEvent(OnSleepAttempt);
+ SubscribeLocalEvent(OnCombatModeShouldHandInteract);
}
private void OnStateExitSubscribers(EntityUid target, MobStateComponent component, MobState state)
@@ -139,5 +141,13 @@ public partial class MobStateSystem
CheckAct(target, component, args);
}
+ private void OnCombatModeShouldHandInteract(EntityUid uid, MobStateComponent component, ref CombatModeShouldHandInteractEvent args)
+ {
+ // Disallow empty-hand-interacting in combat mode
+ // for non-dead mobs
+ if (!IsDead(uid, component))
+ args.Cancelled = true;
+ }
+
#endregion
}