TriggerOnUserInteractHand and TriggerOnUserInteractUsing (#41843)

* init

* handle check

* oops

* cleanup

* fix resolve

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
This commit is contained in:
ScarKy0 2025-12-14 03:11:58 +01:00 committed by BarryNorfolk
parent 9ce1ce5be5
commit f11978b28d
7 changed files with 240 additions and 79 deletions

View File

@ -1,4 +1,5 @@
using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos.Components;
using Content.Shared.Trigger; using Content.Shared.Trigger;
using Content.Shared.Trigger.Components.Effects; using Content.Shared.Trigger.Components.Effects;
@ -31,7 +32,10 @@ public sealed class FireStackOnTriggerSystem : EntitySystem
if (target == null) if (target == null)
return; return;
_flame.AdjustFireStacks(target.Value, ent.Comp.FireStacks, ignite: ent.Comp.DoIgnite); if (!TryComp<FlammableComponent>(target.Value, out var flammable))
return;
_flame.AdjustFireStacks(target.Value, ent.Comp.FireStacks, ignite: ent.Comp.DoIgnite, flammable: flammable);
args.Handled = true; args.Handled = true;
} }
@ -46,7 +50,10 @@ public sealed class FireStackOnTriggerSystem : EntitySystem
if (target == null) if (target == null)
return; return;
_flame.Extinguish(target.Value); if (!TryComp<FlammableComponent>(target.Value, out var flammable))
return;
_flame.Extinguish(target.Value, flammable: flammable);
args.Handled = true; args.Handled = true;
} }

View File

@ -1,10 +1,9 @@
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.Map;
namespace Content.Shared.Interaction namespace Content.Shared.Interaction;
public sealed class InteractHandEventArgs : EventArgs, ITargetedInteractEventArgs
{ {
public sealed class InteractHandEventArgs : EventArgs, ITargetedInteractEventArgs
{
public InteractHandEventArgs(EntityUid user, EntityUid target) public InteractHandEventArgs(EntityUid user, EntityUid target)
{ {
User = user; User = user;
@ -13,14 +12,14 @@ namespace Content.Shared.Interaction
public EntityUid User { get; } public EntityUid User { get; }
public EntityUid Target { get; } public EntityUid Target { get; }
} }
/// <summary> /// <summary>
/// Raised directed on a target entity when it is interacted with by a user with an empty hand. /// Raised directed on a target entity when it is interacted with by a user with an empty hand.
/// </summary> /// </summary>
[PublicAPI] [PublicAPI]
public sealed class InteractHandEvent : HandledEntityEventArgs, ITargetedInteractEventArgs public sealed class InteractHandEvent : HandledEntityEventArgs, ITargetedInteractEventArgs
{ {
/// <summary> /// <summary>
/// Entity that triggered the interaction. /// Entity that triggered the interaction.
/// </summary> /// </summary>
@ -36,19 +35,41 @@ namespace Content.Shared.Interaction
User = user; User = user;
Target = target; Target = target;
} }
} }
/// <summary>
/// Raised directed on the user when they interact with an entity with an empty hand.
/// </summary>
[PublicAPI]
public sealed class UserInteractHandEvent : HandledEntityEventArgs, ITargetedInteractEventArgs
{
/// <summary>
/// Entity that triggered the interaction.
/// </summary>
public EntityUid User { get; }
/// <summary> /// <summary>
/// Raised on the user before interacting on an entity with bare hand. /// Entity that was interacted on.
/// Interaction is cancelled if this event is handled, so set it to true if you do custom interaction logic.
/// </summary> /// </summary>
public sealed class BeforeInteractHandEvent : HandledEntityEventArgs public EntityUid Target { get; }
public UserInteractHandEvent(EntityUid user, EntityUid target)
{ {
User = user;
Target = target;
}
}
/// <summary>
/// Raised on the user before interacting on an entity with bare hand.
/// Interaction is cancelled if this event is handled, so set it to true if you do custom interaction logic.
/// </summary>
public sealed class BeforeInteractHandEvent : HandledEntityEventArgs
{
public EntityUid Target { get; } public EntityUid Target { get; }
public BeforeInteractHandEvent(EntityUid target) public BeforeInteractHandEvent(EntityUid target)
{ {
Target = target; Target = target;
} }
}
} }

View File

@ -2,14 +2,14 @@ using JetBrains.Annotations;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Utility; using Robust.Shared.Utility;
namespace Content.Shared.Interaction namespace Content.Shared.Interaction;
/// <summary>
/// Raised when a target entity is interacted with by a user while holding an object in their hand.
/// </summary>
[PublicAPI]
public sealed class InteractUsingEvent : HandledEntityEventArgs
{ {
/// <summary>
/// Raised when a target entity is interacted with by a user while holding an object in their hand.
/// </summary>
[PublicAPI]
public sealed class InteractUsingEvent : HandledEntityEventArgs
{
/// <summary> /// <summary>
/// Entity that triggered the interaction. /// Entity that triggered the interaction.
/// </summary> /// </summary>
@ -42,5 +42,45 @@ namespace Content.Shared.Interaction
Target = target; Target = target;
ClickLocation = clickLocation; ClickLocation = clickLocation;
} }
}
/// <summary>
/// Raised when a user entity interacts with a target while holding an object in their hand.
/// </summary>
[PublicAPI]
public sealed class UserInteractUsingEvent : HandledEntityEventArgs
{
/// <summary>
/// Entity that triggered the interaction.
/// </summary>
public EntityUid User { get; }
/// <summary>
/// Entity that the user used to interact.
/// </summary>
public EntityUid Used { get; }
/// <summary>
/// Entity that was interacted on.
/// </summary>
public EntityUid Target { get; }
/// <summary>
/// The original location that was clicked by the user.
/// </summary>
public EntityCoordinates ClickLocation { get; }
public UserInteractUsingEvent(EntityUid user, EntityUid used, EntityUid target, EntityCoordinates clickLocation)
{
// Interact using should not have the same used and target.
// That should be a use-in-hand event instead.
// If this is not the case, can lead to bugs (e.g., attempting to merge a item stack into itself).
DebugTools.Assert(used != target);
User = user;
Used = used;
Target = target;
ClickLocation = clickLocation;
} }
} }

View File

@ -525,12 +525,16 @@ namespace Content.Shared.Interaction
} }
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(target)); DebugTools.Assert(!IsDeleted(user) && !IsDeleted(target));
// all interactions should only happen when in range / unobstructed, so no range check is needed // all interactions should only happen when in range / unobstructed, so no range check is needed
var message = new InteractHandEvent(user, target); var message = new InteractHandEvent(user, target);
RaiseLocalEvent(target, message, true); RaiseLocalEvent(target, message, true);
var userMessage = new UserInteractHandEvent(user, target);
RaiseLocalEvent(user, userMessage, true);
_adminLogger.Add(LogType.InteractHand, LogImpact.Low, $"{user} interacted with {target}"); _adminLogger.Add(LogType.InteractHand, LogImpact.Low, $"{user} interacted with {target}");
DoContactInteraction(user, target, message); DoContactInteraction(user, target, message);
if (message.Handled) if (message.Handled || userMessage.Handled)
return true; return true;
DebugTools.Assert(!IsDeleted(user) && !IsDeleted(target)); DebugTools.Assert(!IsDeleted(user) && !IsDeleted(target));
@ -1063,10 +1067,14 @@ namespace Content.Shared.Interaction
// all interactions should only happen when in range / unobstructed, so no range check is needed // all interactions should only happen when in range / unobstructed, so no range check is needed
var interactUsingEvent = new InteractUsingEvent(user, used, target, clickLocation); var interactUsingEvent = new InteractUsingEvent(user, used, target, clickLocation);
RaiseLocalEvent(target, interactUsingEvent, true); RaiseLocalEvent(target, interactUsingEvent, true);
var userInteractUsingEvent = new UserInteractUsingEvent(user, used, target, clickLocation);
RaiseLocalEvent(user, userInteractUsingEvent, true);
DoContactInteraction(user, used, interactUsingEvent); DoContactInteraction(user, used, interactUsingEvent);
DoContactInteraction(user, target, interactUsingEvent); DoContactInteraction(user, target, interactUsingEvent);
// Contact interactions are currently only used for forensics, so we don't raise used -> target // Contact interactions are currently only used for forensics, so we don't raise used -> target
if (interactUsingEvent.Handled) if (interactUsingEvent.Handled || userInteractUsingEvent.Handled)
return true; return true;
if (InteractDoAfter(user, used, target, clickLocation, canReach: true, checkDeletion: false)) if (InteractDoAfter(user, used, target, clickLocation, canReach: true, checkDeletion: false))

View File

@ -0,0 +1,18 @@
using Content.Shared.Interaction;
using Robust.Shared.GameStates;
namespace Content.Shared.Trigger.Components.Triggers;
/// <summary>
/// Trigger on <see cref="UserInteractHandEvent"/>, aka when owner clicks on an entity with an empty hand.
/// The trigger user is the entity that got interacted with.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class TriggerOnUserInteractHandComponent : BaseTriggerOnXComponent
{
/// <summary>
/// Whether the interaction should be marked as handled after it happens.
/// </summary>
[DataField, AutoNetworkedField]
public bool Handle = true;
}

View File

@ -0,0 +1,40 @@
using Content.Shared.Interaction;
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
namespace Content.Shared.Trigger.Components.Triggers;
/// <summary>
/// Triggers when the owner uses another entity to interact with another entity (<see cref="UserInteractUsingEvent"/>).
/// The trigger user is the interacted entity or the item used, depending on the TargetUsed datafield.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class TriggerOnUserInteractUsingComponent : BaseTriggerOnXComponent
{
/// <summary>
/// Whitelist of entities that can be used to trigger this component.
/// </summary>
/// <remarks>No whitelist check when null.</remarks>
[DataField, AutoNetworkedField]
public EntityWhitelist? Whitelist;
/// <summary>
/// Blacklist of entities that cannot be used to trigger this component.
/// </summary>
/// <remarks>No blacklist check when null.</remarks>
[DataField, AutoNetworkedField]
public EntityWhitelist? Blacklist;
/// <summary>
/// If false, the trigger user will be the entity that got interacted with.
/// If true, the trigger user will the entity that was used to interact.
/// </summary>
[DataField, AutoNetworkedField]
public bool TargetUsed = false;
/// <summary>
/// Whether the interaction should be marked as handled after it happens.
/// </summary>
[DataField, AutoNetworkedField]
public bool Handle = true;
}

View File

@ -16,7 +16,9 @@ public sealed partial class TriggerSystem
SubscribeLocalEvent<TriggerOnActivateComponent, ActivateInWorldEvent>(OnActivate); SubscribeLocalEvent<TriggerOnActivateComponent, ActivateInWorldEvent>(OnActivate);
SubscribeLocalEvent<TriggerOnUseComponent, UseInHandEvent>(OnUse); SubscribeLocalEvent<TriggerOnUseComponent, UseInHandEvent>(OnUse);
SubscribeLocalEvent<TriggerOnInteractHandComponent, InteractHandEvent>(OnInteractHand); SubscribeLocalEvent<TriggerOnInteractHandComponent, InteractHandEvent>(OnInteractHand);
SubscribeLocalEvent<TriggerOnUserInteractHandComponent, UserInteractHandEvent>(OnUserInteractHand);
SubscribeLocalEvent<TriggerOnInteractUsingComponent, InteractUsingEvent>(OnInteractUsing); SubscribeLocalEvent<TriggerOnInteractUsingComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<TriggerOnUserInteractUsingComponent, UserInteractUsingEvent>(OnUserInteractUsing);
SubscribeLocalEvent<TriggerOnThrowComponent, ThrowEvent>(OnThrow); SubscribeLocalEvent<TriggerOnThrowComponent, ThrowEvent>(OnThrow);
SubscribeLocalEvent<TriggerOnThrownComponent, ThrownEvent>(OnThrown); SubscribeLocalEvent<TriggerOnThrownComponent, ThrownEvent>(OnThrown);
@ -64,6 +66,17 @@ public sealed partial class TriggerSystem
args.Handled = true; args.Handled = true;
} }
private void OnUserInteractHand(Entity<TriggerOnUserInteractHandComponent> ent, ref UserInteractHandEvent args)
{
if (args.Handled)
return;
Trigger(ent.Owner, args.Target, ent.Comp.KeyOut);
if (ent.Comp.Handle)
args.Handled = true;
}
private void OnInteractUsing(Entity<TriggerOnInteractUsingComponent> ent, ref InteractUsingEvent args) private void OnInteractUsing(Entity<TriggerOnInteractUsingComponent> ent, ref InteractUsingEvent args)
{ {
if (args.Handled) if (args.Handled)
@ -76,6 +89,20 @@ public sealed partial class TriggerSystem
args.Handled = true; args.Handled = true;
} }
private void OnUserInteractUsing(Entity<TriggerOnUserInteractUsingComponent> ent, ref UserInteractUsingEvent args)
{
if (args.Handled)
return;
if (!_whitelist.CheckBoth(args.Used, ent.Comp.Blacklist, ent.Comp.Whitelist))
return;
Trigger(ent.Owner, ent.Comp.TargetUsed ? args.Used : args.Target, ent.Comp.KeyOut);
if (ent.Comp.Handle)
args.Handled = true;
}
private void OnThrow(Entity<TriggerOnThrowComponent> ent, ref ThrowEvent args) private void OnThrow(Entity<TriggerOnThrowComponent> ent, ref ThrowEvent args)
{ {
Trigger(ent.Owner, args.Thrown, ent.Comp.KeyOut); Trigger(ent.Owner, args.Thrown, ent.Comp.KeyOut);