protected void InteractionActivate(EntityUid user, EntityUid used) { if (EntityManager.TryGetComponent <UseDelayComponent?>(used, out var delayComponent)) { if (delayComponent.ActiveDelay) { return; } delayComponent.BeginDelay(); } if (!_actionBlockerSystem.CanInteract(user) || !_actionBlockerSystem.CanUse(user)) { return; } // all activates should only fire when in range / unobstructed if (!InRangeUnobstructed(user, used, ignoreInsideBlocker: true, popup: true)) { return; } // Check if interacted entity is in the same container, the direct child, or direct parent of the user. // This is bypassed IF the interaction happened through an item slot (e.g., backpack UI) if (!user.IsInSameOrParentContainer(used) && !CanAccessViaStorage(user, used)) { return; } var activateMsg = new ActivateInWorldEvent(user, used); RaiseLocalEvent(used, activateMsg); if (activateMsg.Handled) { _adminLogSystem.Add(LogType.InteractActivate, LogImpact.Low, $"{user} activated {used}"); return; } if (!EntityManager.TryGetComponent(used, out IActivate? activateComp)) { return; } var activateEventArgs = new ActivateEventArgs(user, used); activateComp.Activate(activateEventArgs); _adminLogSystem.Add(LogType.InteractActivate, LogImpact.Low, $"{user} activated {used}"); // No way to check success. }
/// <summary> /// Raises a number of events in order to get all verbs of the given type(s) defined in local systems. This /// does not request verbs from the server. /// </summary> public virtual Dictionary <VerbType, SortedSet <Verb> > GetLocalVerbs(EntityUid target, EntityUid user, VerbType verbTypes, bool force = false) { Dictionary <VerbType, SortedSet <Verb> > verbs = new(); // accessibility checks bool canAccess = false; if (force || target == user) { canAccess = true; } else if (_interactionSystem.InRangeUnobstructed(user, target, ignoreInsideBlocker: true)) { if (user.IsInSameOrParentContainer(target)) { canAccess = true; } else { // the item might be in a backpack that the user has open canAccess = _interactionSystem.CanAccessViaStorage(user, target); } } // A large number of verbs need to check action blockers. Instead of repeatedly having each system individually // call ActionBlocker checks, just cache it for the verb request. var canInteract = force || _actionBlockerSystem.CanInteract(user); EntityUid @using = default; if (EntityManager.TryGetComponent(user, out SharedHandsComponent? hands) && (force || _actionBlockerSystem.CanUse(user))) { hands.TryGetActiveHeldEntity(out @using); // Check whether the "Held" entity is a virtual pull entity. If yes, set that as the entity being "Used". // This allows you to do things like buckle a dragged person onto a surgery table, without click-dragging // their sprite. if (@using != default && EntityManager.TryGetComponent <HandVirtualItemComponent?>(@using, out var pull)) { @using = pull.BlockingEntity; } } if ((verbTypes & VerbType.Interaction) == VerbType.Interaction) { GetInteractionVerbsEvent getVerbEvent = new(user, target, @using, hands, canInteract, canAccess); RaiseLocalEvent(target, getVerbEvent); verbs.Add(VerbType.Interaction, getVerbEvent.Verbs); } if ((verbTypes & VerbType.Activation) == VerbType.Activation) { GetActivationVerbsEvent getVerbEvent = new(user, target, @using, hands, canInteract, canAccess); RaiseLocalEvent(target, getVerbEvent); verbs.Add(VerbType.Activation, getVerbEvent.Verbs); } if ((verbTypes & VerbType.Alternative) == VerbType.Alternative) { GetAlternativeVerbsEvent getVerbEvent = new(user, target, @using, hands, canInteract, canAccess); RaiseLocalEvent(target, getVerbEvent); verbs.Add(VerbType.Alternative, getVerbEvent.Verbs); } if ((verbTypes & VerbType.Other) == VerbType.Other) { GetOtherVerbsEvent getVerbEvent = new(user, target, @using, hands, canInteract, canAccess); RaiseLocalEvent(target, getVerbEvent); verbs.Add(VerbType.Other, getVerbEvent.Verbs); } return(verbs); }
public void DoAttack(EntityUid user, EntityCoordinates coordinates, bool wideAttack, EntityUid targetUid = default) { if (!ValidateInteractAndFace(user, coordinates)) { return; } if (!_actionBlockerSystem.CanAttack(user)) { return; } if (!wideAttack) { // Check if interacted entity is in the same container, the direct child, or direct parent of the user. if (targetUid != default && !user.IsInSameOrParentContainer(targetUid) && !CanAccessViaStorage(user, targetUid)) { Logger.WarningS("system.interaction", $"User entity named {EntityManager.GetComponent<MetaDataComponent>(user).EntityName} clicked on object {EntityManager.GetComponent<MetaDataComponent>(targetUid).EntityName} that isn't the parent, child, or in the same container"); return; } // TODO: Replace with body attack range when we get something like arm length or telekinesis or something. if (!user.InRangeUnobstructed(coordinates, ignoreInsideBlocker: true)) { return; } } // Verify user has a hand, and find what object they are currently holding in their active hand if (EntityManager.TryGetComponent <HandsComponent?>(user, out var hands)) { if (hands.GetActiveHand?.Owner is { Valid : true } item) { if (wideAttack) { var ev = new WideAttackEvent(item, user, coordinates); RaiseLocalEvent(item, ev, false); if (ev.Handled) { _adminLogSystem.Add(LogType.AttackArmedWide, LogImpact.Medium, $"{user} wide attacked with {item} at {coordinates}"); return; } } else { var ev = new ClickAttackEvent(item, user, coordinates, targetUid); RaiseLocalEvent(item, ev, false); if (ev.Handled) { if (targetUid != default) { _adminLogSystem.Add(LogType.AttackArmedClick, LogImpact.Medium, $"{user} attacked {targetUid} with {item} at {coordinates}"); } else { _adminLogSystem.Add(LogType.AttackArmedClick, LogImpact.Medium, $"{user} attacked with {item} at {coordinates}"); } return; } } } else if (!wideAttack && targetUid != default && _entityManager.HasComponent <ItemComponent>(targetUid)) { // We pick up items if our hand is empty, even if we're in combat mode. InteractHand(user, targetUid); return; } }
/// <summary> /// Resolves user interactions with objects. /// </summary> /// <remarks> /// Checks Whether combat mode is enabled and whether the user can actually interact with the given entity. /// </remarks> /// <param name="altInteract">Whether to use default or alternative interactions (usually as a result of /// alt+clicking). If combat mode is enabled, the alternative action is to perform the default non-combat /// interaction. Having an item in the active hand also disables alternative interactions.</param> public async void UserInteraction(EntityUid user, EntityCoordinates coordinates, EntityUid target, bool altInteract = false) { // TODO COMBAT Consider using alt-interact for advanced combat? maybe alt-interact disarms? if (!altInteract && EntityManager.TryGetComponent(user, out CombatModeComponent? combatMode) && combatMode.IsInCombatMode) { DoAttack(user, coordinates, false, target); return; } if (!ValidateInteractAndFace(user, coordinates)) { return; } if (!_actionBlockerSystem.CanInteract(user)) { return; } // Check if interacted entity is in the same container, the direct child, or direct parent of the user. // This is bypassed IF the interaction happened through an item slot (e.g., backpack UI) if (target != default && !user.IsInSameOrParentContainer(target) && !CanAccessViaStorage(user, target)) { Logger.WarningS("system.interaction", $"User entity named {EntityManager.GetComponent<MetaDataComponent>(user).EntityName} clicked on object {EntityManager.GetComponent<MetaDataComponent>(target).EntityName} that isn't the parent, child, or in the same container"); return; } // Verify user has a hand, and find what object they are currently holding in their active hand if (!EntityManager.TryGetComponent <HandsComponent?>(user, out var hands)) { return; } var item = hands.GetActiveHand?.Owner; // TODO: Replace with body interaction range when we get something like arm length or telekinesis or something. var inRangeUnobstructed = user.InRangeUnobstructed(coordinates, ignoreInsideBlocker: true); if (target == default || !inRangeUnobstructed) { if (item == null) { return; } if (!await InteractUsingRanged(user, item.Value, target, coordinates, inRangeUnobstructed) && !inRangeUnobstructed) { var message = Loc.GetString("interaction-system-user-interaction-cannot-reach"); user.PopupMessage(message); } return; } else { // We are close to the nearby object. if (altInteract) { // Perform alternative interactions, using context menu verbs. AltInteract(user, target); } else if (item != null && item != target) { // We are performing a standard interaction with an item, and the target isn't the same as the item // currently in our hand. We will use the item in our hand on the nearby object via InteractUsing await InteractUsing(user, item.Value, target, coordinates); } else if (item == null) { // Since our hand is empty we will use InteractHand/Activate InteractHand(user, target); } } }
public override void DoAttack(EntityUid user, EntityCoordinates coordinates, bool wideAttack, EntityUid?target = null) { // TODO PREDICTION move server-side interaction logic into the shared system for interaction prediction. if (!ValidateInteractAndFace(user, coordinates)) { return; } if (!_actionBlockerSystem.CanAttack(user)) { return; } if (!wideAttack) { // Check if interacted entity is in the same container, the direct child, or direct parent of the user. if (target != null && !Deleted(target.Value) && !user.IsInSameOrParentContainer(target.Value) && !CanAccessViaStorage(user, target.Value)) { Logger.WarningS("system.interaction", $"User entity {ToPrettyString(user):user} clicked on object {ToPrettyString(target.Value):target} that isn't the parent, child, or in the same container"); return; } // TODO: Replace with body attack range when we get something like arm length or telekinesis or something. if (!user.InRangeUnobstructed(coordinates, ignoreInsideBlocker: true)) { return; } } // Verify user has a hand, and find what object they are currently holding in their active hand if (TryComp(user, out HandsComponent? hands)) { var item = hands.GetActiveHand?.Owner; if (item != null && !Deleted(item.Value)) { if (wideAttack) { var ev = new WideAttackEvent(item.Value, user, coordinates); RaiseLocalEvent(item.Value, ev, false); if (ev.Handled) { _adminLogSystem.Add(LogType.AttackArmedWide, LogImpact.Medium, $"{ToPrettyString(user):user} wide attacked with {ToPrettyString(item.Value):used} at {coordinates}"); return; } } else { var ev = new ClickAttackEvent(item.Value, user, coordinates, target); RaiseLocalEvent(item.Value, ev, false); if (ev.Handled) { if (target != null) { _adminLogSystem.Add(LogType.AttackArmedClick, LogImpact.Medium, $"{ToPrettyString(user):user} attacked {ToPrettyString(target.Value):target} with {ToPrettyString(item.Value):used} at {coordinates}"); } else { _adminLogSystem.Add(LogType.AttackArmedClick, LogImpact.Medium, $"{ToPrettyString(user):user} attacked with {ToPrettyString(item.Value):used} at {coordinates}"); } return; } } } else if (!wideAttack && target != null && HasComp <SharedItemComponent>(target.Value)) { // We pick up items if our hand is empty, even if we're in combat mode. InteractHand(user, target.Value); return; } } // TODO: Make this saner? // Attempt to do unarmed combat. We don't check for handled just because at this point it doesn't matter. if (wideAttack) { var ev = new WideAttackEvent(user, user, coordinates); RaiseLocalEvent(user, ev, false); if (ev.Handled) { _adminLogSystem.Add(LogType.AttackUnarmedWide, $"{ToPrettyString(user):user} wide attacked at {coordinates}"); } } else { var ev = new ClickAttackEvent(user, user, coordinates, target); RaiseLocalEvent(user, ev, false); if (ev.Handled) { if (target != null) { _adminLogSystem.Add(LogType.AttackUnarmedClick, LogImpact.Medium, $"{ToPrettyString(user):user} attacked {ToPrettyString(target.Value):target} at {coordinates}"); } else { _adminLogSystem.Add(LogType.AttackUnarmedClick, LogImpact.Medium, $"{ToPrettyString(user):user} attacked at {coordinates}"); } } } }
/// <summary> /// Resolves user interactions with objects. /// </summary> /// <remarks> /// Checks Whether combat mode is enabled and whether the user can actually interact with the given entity. /// </remarks> /// <param name="altInteract">Whether to use default or alternative interactions (usually as a result of /// alt+clicking). If combat mode is enabled, the alternative action is to perform the default non-combat /// interaction. Having an item in the active hand also disables alternative interactions.</param> public async void UserInteraction(EntityUid user, EntityCoordinates coordinates, EntityUid?target, bool altInteract = false) { if (target != null && Deleted(target.Value)) { return; } // TODO COMBAT Consider using alt-interact for advanced combat? maybe alt-interact disarms? if (!altInteract && TryComp(user, out SharedCombatModeComponent? combatMode) && combatMode.IsInCombatMode) { DoAttack(user, coordinates, false, target); return; } if (!ValidateInteractAndFace(user, coordinates)) { return; } if (!_actionBlockerSystem.CanInteract(user)) { return; } // Check if interacted entity is in the same container, the direct child, or direct parent of the user. // This is bypassed IF the interaction happened through an item slot (e.g., backpack UI) if (target != null && !user.IsInSameOrParentContainer(target.Value) && !CanAccessViaStorage(user, target.Value)) { return; } // Verify user has a hand, and find what object they are currently holding in their active hand if (!TryComp(user, out SharedHandsComponent? hands)) { return; } // TODO: Replace with body interaction range when we get something like arm length or telekinesis or something. var inRangeUnobstructed = user.InRangeUnobstructed(coordinates, ignoreInsideBlocker: true); if (target == null || !inRangeUnobstructed) { if (!hands.TryGetActiveHeldEntity(out var heldEntity)) { return; } if (!await InteractUsingRanged(user, heldEntity.Value, target, coordinates, inRangeUnobstructed) && !inRangeUnobstructed) { var message = Loc.GetString("interaction-system-user-interaction-cannot-reach"); user.PopupMessage(message); } return; } // We are close to the nearby object. if (altInteract) { // Perform alternative interactions, using context menu verbs. AltInteract(user, target.Value); } else if (!hands.TryGetActiveHeldEntity(out var heldEntity)) { // Since our hand is empty we will use InteractHand/Activate InteractHand(user, target.Value); } else if (heldEntity != target) { await InteractUsing(user, heldEntity.Value, target.Value, coordinates); } }