1
0

SharedInteractionSystem.cs 62 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506
  1. using System.Diagnostics.CodeAnalysis;
  2. using System.Linq;
  3. using Content.Shared.ActionBlocker;
  4. using Content.Shared.Administration.Logs;
  5. using Content.Shared.CCVar;
  6. using Content.Shared.Chat;
  7. using Content.Shared.CombatMode;
  8. using Content.Shared.Database;
  9. using Content.Shared.Ghost;
  10. using Content.Shared.Hands;
  11. using Content.Shared.Hands.Components;
  12. using Content.Shared.Input;
  13. using Content.Shared.Interaction.Components;
  14. using Content.Shared.Interaction.Events;
  15. using Content.Shared.Inventory;
  16. using Content.Shared.Inventory.Events;
  17. using Content.Shared.Item;
  18. using Content.Shared.Movement.Components;
  19. using Content.Shared.Movement.Pulling.Systems;
  20. using Content.Shared.Physics;
  21. using Content.Shared.Players.RateLimiting;
  22. using Content.Shared.Popups;
  23. using Content.Shared.Storage;
  24. using Content.Shared.Strip;
  25. using Content.Shared.Tag;
  26. using Content.Shared.Timing;
  27. using Content.Shared.UserInterface;
  28. using Content.Shared.Verbs;
  29. using Content.Shared.Wall;
  30. using JetBrains.Annotations;
  31. using Robust.Shared.Containers;
  32. using Robust.Shared.Input;
  33. using Robust.Shared.Input.Binding;
  34. using Robust.Shared.Map;
  35. using Robust.Shared.Network;
  36. using Robust.Shared.Physics;
  37. using Robust.Shared.Physics.Components;
  38. using Robust.Shared.Physics.Systems;
  39. using Robust.Shared.Player;
  40. using Robust.Shared.Serialization;
  41. using Robust.Shared.Timing;
  42. using Robust.Shared.Utility;
  43. namespace Content.Shared.Interaction
  44. {
  45. /// <summary>
  46. /// Governs interactions during clicking on entities
  47. /// </summary>
  48. [UsedImplicitly]
  49. public abstract partial class SharedInteractionSystem : EntitySystem
  50. {
  51. [Dependency] private readonly IGameTiming _gameTiming = default!;
  52. [Dependency] private readonly INetManager _net = default!;
  53. [Dependency] private readonly IMapManager _mapManager = default!;
  54. [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
  55. [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
  56. [Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default!;
  57. [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
  58. [Dependency] private readonly SharedPhysicsSystem _broadphase = default!;
  59. [Dependency] private readonly SharedTransformSystem _transform = default!;
  60. [Dependency] private readonly SharedVerbSystem _verbSystem = default!;
  61. [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
  62. [Dependency] private readonly UseDelaySystem _useDelay = default!;
  63. [Dependency] private readonly PullingSystem _pullSystem = default!;
  64. [Dependency] private readonly InventorySystem _inventory = default!;
  65. [Dependency] private readonly TagSystem _tagSystem = default!;
  66. [Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
  67. [Dependency] private readonly SharedStrippableSystem _strippable = default!;
  68. [Dependency] private readonly SharedPlayerRateLimitManager _rateLimit = default!;
  69. [Dependency] private readonly ISharedChatManager _chat = default!;
  70. private EntityQuery<IgnoreUIRangeComponent> _ignoreUiRangeQuery;
  71. private EntityQuery<FixturesComponent> _fixtureQuery;
  72. private EntityQuery<ItemComponent> _itemQuery;
  73. private EntityQuery<PhysicsComponent> _physicsQuery;
  74. private EntityQuery<HandsComponent> _handsQuery;
  75. private EntityQuery<InteractionRelayComponent> _relayQuery;
  76. private EntityQuery<CombatModeComponent> _combatQuery;
  77. private EntityQuery<WallMountComponent> _wallMountQuery;
  78. private EntityQuery<UseDelayComponent> _delayQuery;
  79. private EntityQuery<ActivatableUIComponent> _uiQuery;
  80. private const CollisionGroup InRangeUnobstructedMask = CollisionGroup.Impassable | CollisionGroup.InteractImpassable;
  81. public const float InteractionRange = 1.5f;
  82. public const float InteractionRangeSquared = InteractionRange * InteractionRange;
  83. public const float MaxRaycastRange = 100f;
  84. public const string RateLimitKey = "Interaction";
  85. public delegate bool Ignored(EntityUid entity);
  86. public override void Initialize()
  87. {
  88. _ignoreUiRangeQuery = GetEntityQuery<IgnoreUIRangeComponent>();
  89. _fixtureQuery = GetEntityQuery<FixturesComponent>();
  90. _itemQuery = GetEntityQuery<ItemComponent>();
  91. _physicsQuery = GetEntityQuery<PhysicsComponent>();
  92. _handsQuery = GetEntityQuery<HandsComponent>();
  93. _relayQuery = GetEntityQuery<InteractionRelayComponent>();
  94. _combatQuery = GetEntityQuery<CombatModeComponent>();
  95. _wallMountQuery = GetEntityQuery<WallMountComponent>();
  96. _delayQuery = GetEntityQuery<UseDelayComponent>();
  97. _uiQuery = GetEntityQuery<ActivatableUIComponent>();
  98. SubscribeLocalEvent<BoundUserInterfaceCheckRangeEvent>(HandleUserInterfaceRangeCheck);
  99. SubscribeLocalEvent<BoundUserInterfaceMessageAttempt>(OnBoundInterfaceInteractAttempt);
  100. SubscribeAllEvent<InteractInventorySlotEvent>(HandleInteractInventorySlotEvent);
  101. SubscribeLocalEvent<UnremoveableComponent, ContainerGettingRemovedAttemptEvent>(OnRemoveAttempt);
  102. SubscribeLocalEvent<UnremoveableComponent, GotUnequippedEvent>(OnUnequip);
  103. SubscribeLocalEvent<UnremoveableComponent, GotUnequippedHandEvent>(OnUnequipHand);
  104. SubscribeLocalEvent<UnremoveableComponent, DroppedEvent>(OnDropped);
  105. CommandBinds.Builder
  106. .Bind(ContentKeyFunctions.AltActivateItemInWorld,
  107. new PointerInputCmdHandler(HandleAltUseInteraction))
  108. .Bind(EngineKeyFunctions.Use,
  109. new PointerInputCmdHandler(HandleUseInteraction))
  110. .Bind(ContentKeyFunctions.ActivateItemInWorld,
  111. new PointerInputCmdHandler(HandleActivateItemInWorld))
  112. .Bind(ContentKeyFunctions.TryPullObject,
  113. new PointerInputCmdHandler(HandleTryPullObject))
  114. .Register<SharedInteractionSystem>();
  115. _rateLimit.Register(RateLimitKey,
  116. new RateLimitRegistration(CCVars.InteractionRateLimitPeriod,
  117. CCVars.InteractionRateLimitCount,
  118. null,
  119. CCVars.InteractionRateLimitAnnounceAdminsDelay,
  120. RateLimitAlertAdmins)
  121. );
  122. InitializeBlocking();
  123. }
  124. private void RateLimitAlertAdmins(ICommonSession session)
  125. {
  126. _chat.SendAdminAlert(Loc.GetString("interaction-rate-limit-admin-announcement", ("player", session.Name)));
  127. }
  128. public override void Shutdown()
  129. {
  130. CommandBinds.Unregister<SharedInteractionSystem>();
  131. base.Shutdown();
  132. }
  133. /// <summary>
  134. /// Check that the user that is interacting with the BUI is capable of interacting and can access the entity.
  135. /// </summary>
  136. private void OnBoundInterfaceInteractAttempt(BoundUserInterfaceMessageAttempt ev)
  137. {
  138. _uiQuery.TryComp(ev.Target, out var uiComp);
  139. if (!_actionBlockerSystem.CanInteract(ev.Actor, ev.Target))
  140. {
  141. // We permit ghosts to open uis unless explicitly blocked
  142. if (ev.Message is not OpenBoundInterfaceMessage || !HasComp<GhostComponent>(ev.Actor) || uiComp?.BlockSpectators == true || _tagSystem.HasTag(ev.Actor, "CantInteract")) // Shitmed Change
  143. {
  144. ev.Cancel();
  145. return;
  146. }
  147. }
  148. var range = _ui.GetUiRange(ev.Target, ev.UiKey);
  149. // As long as range>0, the UI frame updates should have auto-closed the UI if it is out of range.
  150. DebugTools.Assert(range <= 0 || UiRangeCheck(ev.Actor, ev.Target, range));
  151. if (range <= 0 && !IsAccessible(ev.Actor, ev.Target))
  152. {
  153. ev.Cancel();
  154. return;
  155. }
  156. if (uiComp == null)
  157. return;
  158. if (uiComp.SingleUser && uiComp.CurrentSingleUser != null && uiComp.CurrentSingleUser != ev.Actor)
  159. {
  160. ev.Cancel();
  161. return;
  162. }
  163. if (uiComp.RequiresComplex && !_actionBlockerSystem.CanComplexInteract(ev.Actor))
  164. ev.Cancel();
  165. }
  166. private bool UiRangeCheck(Entity<TransformComponent?> user, Entity<TransformComponent?> target, float range)
  167. {
  168. if (!Resolve(target, ref target.Comp))
  169. return false;
  170. if (user.Owner == target.Owner)
  171. return true;
  172. // Fast check: if the user is the parent of the entity (e.g., holding it), we always assume that it is in range
  173. if (target.Comp.ParentUid == user.Owner)
  174. return true;
  175. return InRangeAndAccessible(user, target, range) || _ignoreUiRangeQuery.HasComp(user);
  176. }
  177. /// <summary>
  178. /// Prevents an item with the Unremovable component from being removed from a container by almost any means
  179. /// </summary>
  180. private void OnRemoveAttempt(EntityUid uid, UnremoveableComponent item, ContainerGettingRemovedAttemptEvent args)
  181. {
  182. args.Cancel();
  183. }
  184. /// <summary>
  185. /// If item has DeleteOnDrop true then item will be deleted if removed from inventory, if it is false then item
  186. /// loses Unremoveable when removed from inventory (gibbing).
  187. /// </summary>
  188. private void OnUnequip(EntityUid uid, UnremoveableComponent item, GotUnequippedEvent args)
  189. {
  190. if (!item.DeleteOnDrop)
  191. RemCompDeferred<UnremoveableComponent>(uid);
  192. else if (_net.IsServer)
  193. QueueDel(uid);
  194. }
  195. private void OnUnequipHand(EntityUid uid, UnremoveableComponent item, GotUnequippedHandEvent args)
  196. {
  197. if (!item.DeleteOnDrop)
  198. RemCompDeferred<UnremoveableComponent>(uid);
  199. else if (_net.IsServer)
  200. QueueDel(uid);
  201. }
  202. private void OnDropped(EntityUid uid, UnremoveableComponent item, DroppedEvent args)
  203. {
  204. if (!item.DeleteOnDrop)
  205. RemCompDeferred<UnremoveableComponent>(uid);
  206. else if (_net.IsServer)
  207. QueueDel(uid);
  208. }
  209. private bool HandleTryPullObject(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
  210. {
  211. if (!ValidateClientInput(session, coords, uid, out var userEntity))
  212. {
  213. Log.Info($"TryPullObject input validation failed");
  214. return true;
  215. }
  216. //is this user trying to pull themself?
  217. if (userEntity.Value == uid)
  218. return false;
  219. if (Deleted(uid))
  220. return false;
  221. if (!InRangeUnobstructed(userEntity.Value, uid, popup: true))
  222. return false;
  223. _pullSystem.TogglePull(uid, userEntity.Value);
  224. return false;
  225. }
  226. /// <summary>
  227. /// Handles the event were a client uses an item in their inventory or in their hands, either by
  228. /// alt-clicking it or pressing 'E' while hovering over it.
  229. /// </summary>
  230. private void HandleInteractInventorySlotEvent(InteractInventorySlotEvent msg, EntitySessionEventArgs args)
  231. {
  232. var item = GetEntity(msg.ItemUid);
  233. // client sanitization
  234. if (!TryComp(item, out TransformComponent? itemXform) || !ValidateClientInput(args.SenderSession, itemXform.Coordinates, item, out var user))
  235. {
  236. Log.Info($"Inventory interaction validation failed. Session={args.SenderSession}");
  237. return;
  238. }
  239. // We won't bother to check that the target item is ACTUALLY in an inventory slot. UserInteraction() and
  240. // InteractionActivate() should check that the item is accessible. So.. if a user wants to lie about an
  241. // in-reach item being used in a slot... that should have no impact. This is functionally the same as if
  242. // they had somehow directly clicked on that item.
  243. if (msg.AltInteract)
  244. // Use 'UserInteraction' function - behaves as if the user alt-clicked the item in the world.
  245. UserInteraction(user.Value, itemXform.Coordinates, item, msg.AltInteract);
  246. else
  247. // User used 'E'. We want to activate it, not simulate clicking on the item
  248. InteractionActivate(user.Value, item);
  249. }
  250. public bool HandleAltUseInteraction(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
  251. {
  252. // client sanitization
  253. if (!ValidateClientInput(session, coords, uid, out var user))
  254. {
  255. Log.Info($"Alt-use input validation failed");
  256. return true;
  257. }
  258. UserInteraction(user.Value, coords, uid, altInteract: true, checkAccess: ShouldCheckAccess(user.Value));
  259. return false;
  260. }
  261. public bool HandleUseInteraction(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
  262. {
  263. // client sanitization
  264. if (!ValidateClientInput(session, coords, uid, out var userEntity))
  265. {
  266. Log.Info($"Use input validation failed");
  267. return true;
  268. }
  269. UserInteraction(userEntity.Value, coords, !Deleted(uid) ? uid : null, checkAccess: ShouldCheckAccess(userEntity.Value));
  270. return false;
  271. }
  272. private bool ShouldCheckAccess(EntityUid user)
  273. {
  274. // This is for Admin/mapping convenience. If ever there are other ghosts that can still interact, this check
  275. // might need to be more selective.
  276. return !_tagSystem.HasTag(user, "BypassInteractionRangeChecks");
  277. }
  278. /// <summary>
  279. /// Returns true if the specified entity should hand interact with the target instead of attacking
  280. /// </summary>
  281. /// <param name="user">The user interacting in combat mode</param>
  282. /// <param name="target">The target of the interaction</param>
  283. /// <returns></returns>
  284. public bool CombatModeCanHandInteract(EntityUid user, EntityUid? target)
  285. {
  286. // Always allow attack in these cases
  287. if (target == null || !_handsQuery.TryComp(user, out var hands) || hands.ActiveHand?.HeldEntity is not null)
  288. return false;
  289. // Only eat input if:
  290. // - Target isn't an item
  291. // - Target doesn't cancel should-interact event
  292. // This is intended to allow items to be picked up in combat mode,
  293. // but to also allow items to force attacks anyway (like mobs which are items, e.g. mice)
  294. if (!_itemQuery.HasComp(target))
  295. return false;
  296. var combatEv = new CombatModeShouldHandInteractEvent();
  297. RaiseLocalEvent(target.Value, ref combatEv);
  298. if (combatEv.Cancelled)
  299. return false;
  300. return true;
  301. }
  302. /// <summary>
  303. /// Resolves user interactions with objects.
  304. /// </summary>
  305. /// <remarks>
  306. /// Checks Whether combat mode is enabled and whether the user can actually interact with the given entity.
  307. /// </remarks>
  308. /// <param name="altInteract">Whether to use default or alternative interactions (usually as a result of
  309. /// alt+clicking). If combat mode is enabled, the alternative action is to perform the default non-combat
  310. /// interaction. Having an item in the active hand also disables alternative interactions.</param>
  311. public void UserInteraction(
  312. EntityUid user,
  313. EntityCoordinates coordinates,
  314. EntityUid? target,
  315. bool altInteract = false,
  316. bool checkCanInteract = true,
  317. bool checkAccess = true,
  318. bool checkCanUse = true)
  319. {
  320. if (_relayQuery.TryComp(user, out var relay) && relay.RelayEntity is not null)
  321. {
  322. // TODO this needs to be handled better. This probably bypasses many complex can-interact checks in weird roundabout ways.
  323. if (_actionBlockerSystem.CanInteract(user, target))
  324. {
  325. UserInteraction(relay.RelayEntity.Value,
  326. coordinates,
  327. target,
  328. altInteract,
  329. checkCanInteract,
  330. checkAccess,
  331. checkCanUse);
  332. return;
  333. }
  334. }
  335. if (target != null && Deleted(target.Value))
  336. return;
  337. if (!altInteract && _combatQuery.TryComp(user, out var combatMode) && combatMode.IsInCombatMode)
  338. {
  339. if (!CombatModeCanHandInteract(user, target))
  340. return;
  341. }
  342. if (!ValidateInteractAndFace(user, coordinates))
  343. return;
  344. if (altInteract && target != null)
  345. {
  346. // Perform alternative interactions, using context menu verbs.
  347. // These perform their own range, can-interact, and accessibility checks.
  348. AltInteract(user, target.Value);
  349. return;
  350. }
  351. if (checkCanInteract && !_actionBlockerSystem.CanInteract(user, target))
  352. return;
  353. // Check if interacted entity is in the same container, the direct child, or direct parent of the user.
  354. // Also checks if the item is accessible via some storage UI (e.g., open backpack)
  355. if (checkAccess && target != null && !IsAccessible(user, target.Value))
  356. return;
  357. var inRangeUnobstructed = target == null
  358. ? !checkAccess || InRangeUnobstructed(user, coordinates)
  359. : !checkAccess || InRangeUnobstructed(user, target.Value); // permits interactions with wall mounted entities
  360. // empty-hand interactions
  361. // combat mode hand interactions will always be true here -- since
  362. // they check this earlier before returning in
  363. if (!TryGetUsedEntity(user, out var used, checkCanUse))
  364. {
  365. if (inRangeUnobstructed && target != null)
  366. InteractHand(user, target.Value);
  367. return;
  368. }
  369. if (target == used)
  370. {
  371. UseInHandInteraction(user, target.Value, checkCanUse: false, checkCanInteract: false);
  372. return;
  373. }
  374. if (inRangeUnobstructed && target != null)
  375. {
  376. InteractUsing(
  377. user,
  378. used.Value,
  379. target.Value,
  380. coordinates,
  381. checkCanInteract: false,
  382. checkCanUse: false);
  383. return;
  384. }
  385. InteractUsingRanged(
  386. user,
  387. used.Value,
  388. target,
  389. coordinates,
  390. inRangeUnobstructed);
  391. }
  392. private bool IsDeleted(EntityUid uid)
  393. {
  394. return TerminatingOrDeleted(uid) || EntityManager.IsQueuedForDeletion(uid);
  395. }
  396. private bool IsDeleted(EntityUid? uid)
  397. {
  398. //optional / null entities can pass this validation check. I.e., is-deleted returns false for null uids
  399. return uid != null && IsDeleted(uid.Value);
  400. }
  401. public void InteractHand(EntityUid user, EntityUid target)
  402. {
  403. if (IsDeleted(user) || IsDeleted(target))
  404. return;
  405. var complexInteractions = _actionBlockerSystem.CanComplexInteract(user);
  406. if (!complexInteractions)
  407. {
  408. InteractionActivate(user,
  409. target,
  410. checkCanInteract: false,
  411. checkUseDelay: true,
  412. checkAccess: false,
  413. complexInteractions: complexInteractions,
  414. checkDeletion: false);
  415. return;
  416. }
  417. // allow for special logic before main interaction
  418. var ev = new BeforeInteractHandEvent(target);
  419. RaiseLocalEvent(user, ev);
  420. if (ev.Handled)
  421. {
  422. _adminLogger.Add(LogType.InteractHand, LogImpact.Low, $"{ToPrettyString(user):user} interacted with {ToPrettyString(target):target}, but it was handled by another system");
  423. return;
  424. }
  425. DebugTools.Assert(!IsDeleted(user) && !IsDeleted(target));
  426. // all interactions should only happen when in range / unobstructed, so no range check is needed
  427. var message = new InteractHandEvent(user, target);
  428. RaiseLocalEvent(target, message, true);
  429. _adminLogger.Add(LogType.InteractHand, LogImpact.Low, $"{ToPrettyString(user):user} interacted with {ToPrettyString(target):target}");
  430. DoContactInteraction(user, target, message);
  431. if (message.Handled)
  432. return;
  433. DebugTools.Assert(!IsDeleted(user) && !IsDeleted(target));
  434. // Else we run Activate.
  435. InteractionActivate(user,
  436. target,
  437. checkCanInteract: false,
  438. checkUseDelay: true,
  439. checkAccess: false,
  440. complexInteractions: complexInteractions,
  441. checkDeletion: false);
  442. }
  443. public void InteractUsingRanged(EntityUid user, EntityUid used, EntityUid? target,
  444. EntityCoordinates clickLocation, bool inRangeUnobstructed)
  445. {
  446. if (IsDeleted(user) || IsDeleted(used) || IsDeleted(target))
  447. return;
  448. if (target != null)
  449. {
  450. _adminLogger.Add(
  451. LogType.InteractUsing,
  452. LogImpact.Low,
  453. $"{ToPrettyString(user):user} interacted with {ToPrettyString(target):target} using {ToPrettyString(used):used}");
  454. }
  455. else
  456. {
  457. _adminLogger.Add(
  458. LogType.InteractUsing,
  459. LogImpact.Low,
  460. $"{ToPrettyString(user):user} interacted with *nothing* using {ToPrettyString(used):used}");
  461. }
  462. if (RangedInteractDoBefore(user, used, target, clickLocation, inRangeUnobstructed, checkDeletion: false))
  463. return;
  464. DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used) && !IsDeleted(target));
  465. if (target != null)
  466. {
  467. var rangedMsg = new RangedInteractEvent(user, used, target.Value, clickLocation);
  468. RaiseLocalEvent(target.Value, rangedMsg, true);
  469. // We contact the USED entity, but not the target.
  470. DoContactInteraction(user, used, rangedMsg);
  471. if (rangedMsg.Handled)
  472. return;
  473. }
  474. DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used) && !IsDeleted(target));
  475. InteractDoAfter(user, used, target, clickLocation, inRangeUnobstructed, checkDeletion: false);
  476. }
  477. protected bool ValidateInteractAndFace(EntityUid user, EntityCoordinates coordinates)
  478. {
  479. // Verify user is on the same map as the entity they clicked on
  480. if (_transform.GetMapId(coordinates) != Transform(user).MapID)
  481. return false;
  482. if (!HasComp<NoRotateOnInteractComponent>(user))
  483. _rotateToFaceSystem.TryFaceCoordinates(user, _transform.ToMapCoordinates(coordinates).Position);
  484. return true;
  485. }
  486. /// <summary>
  487. /// Traces a ray from coords to otherCoords and returns the length
  488. /// of the vector between coords and the ray's first hit.
  489. /// </summary>
  490. /// <param name="origin">Set of coordinates to use.</param>
  491. /// <param name="other">Other set of coordinates to use.</param>
  492. /// <param name="collisionMask">the mask to check for collisions</param>
  493. /// <param name="predicate">
  494. /// A predicate to check whether to ignore an entity or not.
  495. /// If it returns true, it will be ignored.
  496. /// </param>
  497. /// <returns>Length of resulting ray.</returns>
  498. public float UnobstructedDistance(
  499. MapCoordinates origin,
  500. MapCoordinates other,
  501. int collisionMask = (int)InRangeUnobstructedMask,
  502. Ignored? predicate = null)
  503. {
  504. var dir = other.Position - origin.Position;
  505. if (dir.LengthSquared().Equals(0f))
  506. return 0f;
  507. predicate ??= _ => false;
  508. var ray = new CollisionRay(origin.Position, dir.Normalized(), collisionMask);
  509. var rayResults = _broadphase.IntersectRayWithPredicate(origin.MapId, ray, dir.Length(), predicate.Invoke, false).ToList();
  510. if (rayResults.Count == 0)
  511. return dir.Length();
  512. return (rayResults[0].HitPos - origin.Position).Length();
  513. }
  514. /// <summary>
  515. /// Checks that these coordinates are within a certain distance without any
  516. /// entity that matches the collision mask obstructing them.
  517. /// If the <paramref name="range"/> is zero or negative,
  518. /// this method will only check if nothing obstructs the two sets
  519. /// of coordinates.
  520. /// </summary>
  521. /// <param name="origin">Set of coordinates to use.</param>
  522. /// <param name="other">Other set of coordinates to use.</param>
  523. /// <param name="range">
  524. /// Maximum distance between the two sets of coordinates.
  525. /// </param>
  526. /// <param name="collisionMask">The mask to check for collisions.</param>
  527. /// <param name="predicate">
  528. /// A predicate to check whether to ignore an entity or not.
  529. /// If it returns true, it will be ignored.
  530. /// </param>
  531. /// <param name="checkAccess">Perform range checks</param>
  532. /// <returns>
  533. /// True if the two points are within a given range without being obstructed.
  534. /// </returns>
  535. public bool InRangeUnobstructed(
  536. MapCoordinates origin,
  537. MapCoordinates other,
  538. float range = InteractionRange,
  539. CollisionGroup collisionMask = InRangeUnobstructedMask,
  540. Ignored? predicate = null,
  541. bool checkAccess = true)
  542. {
  543. // Have to be on same map regardless.
  544. if (other.MapId != origin.MapId)
  545. return false;
  546. if (!checkAccess)
  547. return true;
  548. var dir = other.Position - origin.Position;
  549. var length = dir.Length();
  550. // If range specified also check it
  551. if (range > 0f && length > range)
  552. return false;
  553. if (MathHelper.CloseTo(length, 0))
  554. return true;
  555. predicate ??= _ => false;
  556. if (length > MaxRaycastRange)
  557. {
  558. Log.Warning("InRangeUnobstructed check performed over extreme range. Limiting CollisionRay size.");
  559. length = MaxRaycastRange;
  560. }
  561. var ray = new CollisionRay(origin.Position, dir.Normalized(), (int)collisionMask);
  562. var rayResults = _broadphase.IntersectRayWithPredicate(origin.MapId, ray, length, predicate.Invoke, false).ToList();
  563. return rayResults.Count == 0;
  564. }
  565. public bool InRangeUnobstructed(
  566. Entity<TransformComponent?> origin,
  567. Entity<TransformComponent?> other,
  568. float range = InteractionRange,
  569. CollisionGroup collisionMask = InRangeUnobstructedMask,
  570. Ignored? predicate = null,
  571. bool popup = false,
  572. bool overlapCheck = true)
  573. {
  574. if (!Resolve(other, ref other.Comp))
  575. return false;
  576. var ev = new InRangeOverrideEvent(origin, other);
  577. RaiseLocalEvent(origin, ref ev);
  578. if (ev.Handled)
  579. {
  580. return ev.InRange;
  581. }
  582. return InRangeUnobstructed(origin,
  583. other,
  584. other.Comp.Coordinates,
  585. other.Comp.LocalRotation,
  586. range,
  587. collisionMask,
  588. predicate,
  589. popup,
  590. overlapCheck);
  591. }
  592. /// <summary>
  593. /// Checks that two entities are within a certain distance without any
  594. /// entity that matches the collision mask obstructing them.
  595. /// If the <paramref name="range"/> is zero or negative,
  596. /// this method will only check if nothing obstructs the two entities.
  597. /// This function will also check whether the other entity is a wall-mounted entity. If it is, it will
  598. /// automatically ignore some obstructions.
  599. /// </summary>
  600. /// <param name="origin">The first entity to use.</param>
  601. /// <param name="other">Other entity to use.</param>
  602. /// <param name="otherAngle">The local rotation to use for the other entity.</param>
  603. /// <param name="range">
  604. /// Maximum distance between the two entities.
  605. /// </param>
  606. /// <param name="collisionMask">The mask to check for collisions.</param>
  607. /// <param name="predicate">
  608. /// A predicate to check whether to ignore an entity or not.
  609. /// If it returns true, it will be ignored.
  610. /// </param>
  611. /// <param name="popup">
  612. /// Whether or not to popup a feedback message on the origin entity for
  613. /// it to see.
  614. /// </param>
  615. /// <param name="otherCoordinates">The coordinates to use for the other entity.</param>
  616. /// <returns>
  617. /// True if the two points are within a given range without being obstructed.
  618. /// </returns>
  619. /// <param name="overlapCheck">If true, if the broadphase query returns an overlap (0f distance) this function will early out true with no raycast made.</param>
  620. public bool InRangeUnobstructed(
  621. Entity<TransformComponent?> origin,
  622. Entity<TransformComponent?> other,
  623. EntityCoordinates otherCoordinates,
  624. Angle otherAngle,
  625. float range = InteractionRange,
  626. CollisionGroup collisionMask = InRangeUnobstructedMask,
  627. Ignored? predicate = null,
  628. bool popup = false,
  629. bool overlapCheck = true)
  630. {
  631. Ignored combinedPredicate = e => e == origin.Owner || (predicate?.Invoke(e) ?? false);
  632. var inRange = true;
  633. MapCoordinates originPos = default;
  634. var targetPos = _transform.ToMapCoordinates(otherCoordinates);
  635. Angle targetRot = default;
  636. // So essentially:
  637. // 1. If fixtures available check nearest point. We take in coordinates / angles because we might want to use a lag compensated position
  638. // 2. Fall back to centre of body.
  639. // Alternatively we could check centre distances first though
  640. // that means we wouldn't be able to easily check overlap interactions.
  641. if (range > 0f &&
  642. _fixtureQuery.TryComp(origin, out var fixtureA) &&
  643. // These fixture counts are stuff that has the component but no fixtures for <reasons> (e.g. buttons).
  644. // At least until they get removed.
  645. fixtureA.FixtureCount > 0 &&
  646. _fixtureQuery.TryComp(other, out var fixtureB) &&
  647. fixtureB.FixtureCount > 0 &&
  648. Resolve(origin, ref origin.Comp))
  649. {
  650. var (worldPosA, worldRotA) = _transform.GetWorldPositionRotation(origin.Comp);
  651. var xfA = new Transform(worldPosA, worldRotA);
  652. var parentRotB = _transform.GetWorldRotation(otherCoordinates.EntityId);
  653. var xfB = new Transform(targetPos.Position, parentRotB + otherAngle);
  654. // Different map or the likes.
  655. if (!_broadphase.TryGetNearest(
  656. origin,
  657. other,
  658. out _,
  659. out _,
  660. out var distance,
  661. xfA,
  662. xfB,
  663. fixtureA,
  664. fixtureB))
  665. {
  666. inRange = false;
  667. }
  668. // Overlap, early out and no raycast.
  669. else if (overlapCheck && distance.Equals(0f))
  670. {
  671. return true;
  672. }
  673. // Entity can bypass range checks.
  674. else if (!ShouldCheckAccess(origin))
  675. {
  676. return true;
  677. }
  678. // Out of range so don't raycast.
  679. else if (distance > range)
  680. {
  681. inRange = false;
  682. }
  683. else
  684. {
  685. // We'll still do the raycast from the centres but we'll bump the range as we know they're in range.
  686. originPos = _transform.GetMapCoordinates(origin, xform: origin.Comp);
  687. range = (originPos.Position - targetPos.Position).Length();
  688. }
  689. }
  690. // No fixtures, e.g. wallmounts.
  691. else
  692. {
  693. originPos = _transform.GetMapCoordinates(origin, origin);
  694. var otherParent = (other.Comp ?? Transform(other)).ParentUid;
  695. targetRot = otherParent.IsValid() ? Transform(otherParent).LocalRotation + otherAngle : otherAngle;
  696. }
  697. // Do a raycast to check if relevant
  698. if (inRange)
  699. {
  700. var rayPredicate = GetPredicate(originPos, other, targetPos, targetRot, collisionMask, combinedPredicate);
  701. inRange = InRangeUnobstructed(originPos, targetPos, range, collisionMask, rayPredicate, ShouldCheckAccess(origin));
  702. }
  703. if (!inRange && popup && _gameTiming.IsFirstTimePredicted)
  704. {
  705. var message = Loc.GetString("interaction-system-user-interaction-cannot-reach");
  706. _popupSystem.PopupClient(message, origin, origin);
  707. }
  708. return inRange;
  709. }
  710. public bool InRangeUnobstructed(
  711. MapCoordinates origin,
  712. EntityUid target,
  713. float range = InteractionRange,
  714. CollisionGroup collisionMask = InRangeUnobstructedMask,
  715. Ignored? predicate = null)
  716. {
  717. var transform = Transform(target);
  718. var (position, rotation) = _transform.GetWorldPositionRotation(transform);
  719. var mapPos = new MapCoordinates(position, transform.MapID);
  720. var combinedPredicate = GetPredicate(origin, target, mapPos, rotation, collisionMask, predicate);
  721. return InRangeUnobstructed(origin, mapPos, range, collisionMask, combinedPredicate);
  722. }
  723. /// <summary>
  724. /// Gets the entities to ignore for an unobstructed raycast
  725. /// </summary>
  726. /// <example>
  727. /// if the target entity is a wallmount we ignore all other entities on the tile.
  728. /// </example>
  729. private Ignored GetPredicate(
  730. MapCoordinates origin,
  731. EntityUid target,
  732. MapCoordinates targetCoords,
  733. Angle targetRotation,
  734. CollisionGroup collisionMask,
  735. Ignored? predicate = null)
  736. {
  737. HashSet<EntityUid> ignored = new();
  738. if (_itemQuery.HasComp(target) && _physicsQuery.TryComp(target, out var physics) && physics.CanCollide)
  739. {
  740. // If the target is an item, we ignore any colliding entities. Currently done so that if items get stuck
  741. // inside of walls, users can still pick them up.
  742. ignored.UnionWith(_broadphase.GetEntitiesIntersectingBody(target, (int)collisionMask, false, physics));
  743. }
  744. else if (_wallMountQuery.TryComp(target, out var wallMount))
  745. {
  746. // wall-mount exemptions may be restricted to a specific angle range.da
  747. bool ignoreAnchored;
  748. if (wallMount.Arc >= Math.Tau)
  749. ignoreAnchored = true;
  750. else
  751. {
  752. var angle = Angle.FromWorldVec(origin.Position - targetCoords.Position);
  753. var angleDelta = (wallMount.Direction + targetRotation - angle).Reduced().FlipPositive();
  754. ignoreAnchored = angleDelta < wallMount.Arc / 2 || Math.Tau - angleDelta < wallMount.Arc / 2;
  755. }
  756. if (ignoreAnchored && _mapManager.TryFindGridAt(targetCoords, out _, out var grid))
  757. ignored.UnionWith(grid.GetAnchoredEntities(targetCoords));
  758. }
  759. Ignored combinedPredicate = e => e == target || (predicate?.Invoke(e) ?? false) || ignored.Contains(e);
  760. return combinedPredicate;
  761. }
  762. /// <summary>
  763. /// Checks that an entity and a set of grid coordinates are within a certain
  764. /// distance without any entity that matches the collision mask
  765. /// obstructing them.
  766. /// If the <paramref name="range"/> is zero or negative,
  767. /// this method will only check if nothing obstructs the entity and component.
  768. /// </summary>
  769. /// <param name="origin">The entity to use.</param>
  770. /// <param name="other">The grid coordinates to use.</param>
  771. /// <param name="range">
  772. /// Maximum distance between the two entity and set of grid coordinates.
  773. /// </param>
  774. /// <param name="collisionMask">The mask to check for collisions.</param>
  775. /// <param name="predicate">
  776. /// A predicate to check whether to ignore an entity or not.
  777. /// If it returns true, it will be ignored.
  778. /// </param>
  779. /// <param name="popup">
  780. /// Whether or not to popup a feedback message on the origin entity for
  781. /// it to see.
  782. /// </param>
  783. /// <returns>
  784. /// True if the two points are within a given range without being obstructed.
  785. /// </returns>
  786. public bool InRangeUnobstructed(
  787. EntityUid origin,
  788. EntityCoordinates other,
  789. float range = InteractionRange,
  790. CollisionGroup collisionMask = InRangeUnobstructedMask,
  791. Ignored? predicate = null,
  792. bool popup = false)
  793. {
  794. return InRangeUnobstructed(origin, _transform.ToMapCoordinates(other), range, collisionMask, predicate, popup);
  795. }
  796. /// <summary>
  797. /// Checks that an entity and a set of map coordinates are within a certain
  798. /// distance without any entity that matches the collision mask
  799. /// obstructing them.
  800. /// If the <paramref name="range"/> is zero or negative,
  801. /// this method will only check if nothing obstructs the entity and component.
  802. /// </summary>
  803. /// <param name="origin">The entity to use.</param>
  804. /// <param name="other">The map coordinates to use.</param>
  805. /// <param name="range">
  806. /// Maximum distance between the two entity and set of map coordinates.
  807. /// </param>
  808. /// <param name="collisionMask">The mask to check for collisions.</param>
  809. /// <param name="predicate">
  810. /// A predicate to check whether to ignore an entity or not.
  811. /// If it returns true, it will be ignored.
  812. /// </param>
  813. /// <param name="popup">
  814. /// Whether or not to popup a feedback message on the origin entity for
  815. /// it to see.
  816. /// </param>
  817. /// <returns>
  818. /// True if the two points are within a given range without being obstructed.
  819. /// </returns>
  820. public bool InRangeUnobstructed(
  821. EntityUid origin,
  822. MapCoordinates other,
  823. float range = InteractionRange,
  824. CollisionGroup collisionMask = InRangeUnobstructedMask,
  825. Ignored? predicate = null,
  826. bool popup = false)
  827. {
  828. Ignored combinedPredicate = e => e == origin || (predicate?.Invoke(e) ?? false);
  829. var originPosition = _transform.GetMapCoordinates(origin);
  830. var inRange = InRangeUnobstructed(originPosition, other, range, collisionMask, combinedPredicate, ShouldCheckAccess(origin));
  831. if (!inRange && popup && _gameTiming.IsFirstTimePredicted)
  832. {
  833. var message = Loc.GetString("interaction-system-user-interaction-cannot-reach");
  834. _popupSystem.PopupEntity(message, origin, origin);
  835. }
  836. return inRange;
  837. }
  838. public bool RangedInteractDoBefore(
  839. EntityUid user,
  840. EntityUid used,
  841. EntityUid? target,
  842. EntityCoordinates clickLocation,
  843. bool canReach,
  844. bool checkDeletion = true)
  845. {
  846. if (checkDeletion && (IsDeleted(user) || IsDeleted(used) || IsDeleted(target)))
  847. return false;
  848. var ev = new BeforeRangedInteractEvent(user, used, target, clickLocation, canReach);
  849. RaiseLocalEvent(used, ev);
  850. if (!ev.Handled)
  851. return false;
  852. // We contact the USED entity, but not the target.
  853. DoContactInteraction(user, used, ev);
  854. return ev.Handled;
  855. }
  856. /// <summary>
  857. /// Uses an item/object on an entity
  858. /// Finds components with the InteractUsing interface and calls their function
  859. /// NOTE: Does not have an InRangeUnobstructed check
  860. /// </summary>
  861. /// <param name="user">User doing the interaction.</param>
  862. /// <param name="used">Item being used on the <paramref name="target"/>.</param>
  863. /// <param name="target">Entity getting interacted with by the <paramref name="user"/> using the
  864. /// <paramref name="used"/> entity.</param>
  865. /// <param name="clickLocation">The location that the <paramref name="user"/> clicked.</param>
  866. /// <param name="checkCanInteract">Whether to check that the <paramref name="user"/> can interact with the
  867. /// <paramref name="target"/>.</param>
  868. /// <param name="checkCanUse">Whether to check that the <paramref name="user"/> can use the
  869. /// <paramref name="used"/> entity.</param>
  870. /// <returns>True if the interaction was handled. Otherwise, false.</returns>
  871. public bool InteractUsing(
  872. EntityUid user,
  873. EntityUid used,
  874. EntityUid target,
  875. EntityCoordinates clickLocation,
  876. bool checkCanInteract = true,
  877. bool checkCanUse = true)
  878. {
  879. if (IsDeleted(user) || IsDeleted(used) || IsDeleted(target))
  880. return false;
  881. if (checkCanInteract && !_actionBlockerSystem.CanInteract(user, target))
  882. return false;
  883. if (checkCanUse && !_actionBlockerSystem.CanUseHeldEntity(user, used))
  884. return false;
  885. _adminLogger.Add(
  886. LogType.InteractUsing,
  887. LogImpact.Low,
  888. $"{ToPrettyString(user):user} interacted with {ToPrettyString(target):target} using {ToPrettyString(used):used}");
  889. if (RangedInteractDoBefore(user, used, target, clickLocation, canReach: true, checkDeletion: false))
  890. return true;
  891. DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used) && !IsDeleted(target));
  892. // all interactions should only happen when in range / unobstructed, so no range check is needed
  893. var interactUsingEvent = new InteractUsingEvent(user, used, target, clickLocation);
  894. RaiseLocalEvent(target, interactUsingEvent, true);
  895. DoContactInteraction(user, used, interactUsingEvent);
  896. DoContactInteraction(user, target, interactUsingEvent);
  897. // Contact interactions are currently only used for forensics, so we don't raise used -> target
  898. if (interactUsingEvent.Handled)
  899. return true;
  900. if (InteractDoAfter(user, used, target, clickLocation, canReach: true, checkDeletion: false))
  901. return true;
  902. DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used) && !IsDeleted(target));
  903. return false;
  904. }
  905. /// <summary>
  906. /// Used when clicking on an entity resulted in no other interaction. Used for low-priority interactions.
  907. /// </summary>
  908. /// <param name="user"><inheritdoc cref="InteractUsing"/></param>
  909. /// <param name="used"><inheritdoc cref="InteractUsing"/></param>
  910. /// <param name="target"><inheritdoc cref="InteractUsing"/></param>
  911. /// <param name="clickLocation"><inheritdoc cref="InteractUsing"/></param>
  912. /// <param name="canReach">Whether the <paramref name="user"/> is in range of the <paramref name="target"/>.
  913. /// </param>
  914. /// <returns>True if the interaction was handled. Otherwise, false.</returns>
  915. public bool InteractDoAfter(EntityUid user, EntityUid used, EntityUid? target, EntityCoordinates clickLocation, bool canReach, bool checkDeletion = true)
  916. {
  917. if (target is { Valid: false })
  918. target = null;
  919. if (checkDeletion && (IsDeleted(user) || IsDeleted(used) || IsDeleted(target)))
  920. return false;
  921. var afterInteractEvent = new AfterInteractEvent(user, used, target, clickLocation, canReach);
  922. RaiseLocalEvent(used, afterInteractEvent);
  923. DoContactInteraction(user, used, afterInteractEvent);
  924. if (canReach)
  925. {
  926. DoContactInteraction(user, target, afterInteractEvent);
  927. // Contact interactions are currently only used for forensics, so we don't raise used -> target
  928. }
  929. if (afterInteractEvent.Handled)
  930. return true;
  931. if (target == null)
  932. return false;
  933. DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used) && !IsDeleted(target));
  934. var afterInteractUsingEvent = new AfterInteractUsingEvent(user, used, target, clickLocation, canReach);
  935. RaiseLocalEvent(target.Value, afterInteractUsingEvent);
  936. DoContactInteraction(user, used, afterInteractUsingEvent);
  937. if (canReach)
  938. {
  939. DoContactInteraction(user, target, afterInteractUsingEvent);
  940. // Contact interactions are currently only used for forensics, so we don't raise used -> target
  941. }
  942. return afterInteractUsingEvent.Handled;
  943. }
  944. #region ActivateItemInWorld
  945. private bool HandleActivateItemInWorld(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
  946. {
  947. if (!ValidateClientInput(session, coords, uid, out var user))
  948. {
  949. Log.Info($"ActivateItemInWorld input validation failed");
  950. return false;
  951. }
  952. if (Deleted(uid))
  953. return false;
  954. InteractionActivate(user.Value, uid, checkAccess: ShouldCheckAccess(user.Value));
  955. return false;
  956. }
  957. /// <summary>
  958. /// Raises <see cref="ActivateInWorldEvent"/> events and activates the IActivate behavior of an object.
  959. /// </summary>
  960. /// <remarks>
  961. /// Does not check the can-use action blocker. In activations interacts can target entities outside of the users
  962. /// hands.
  963. /// </remarks>
  964. public bool InteractionActivate(
  965. EntityUid user,
  966. EntityUid used,
  967. bool checkCanInteract = true,
  968. bool checkUseDelay = true,
  969. bool checkAccess = true,
  970. bool? complexInteractions = null,
  971. bool checkDeletion = true)
  972. {
  973. if (checkDeletion && (IsDeleted(user) || IsDeleted(used)))
  974. return false;
  975. DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used));
  976. _delayQuery.TryComp(used, out var delayComponent);
  977. if (checkUseDelay && delayComponent != null && _useDelay.IsDelayed((used, delayComponent)))
  978. return false;
  979. if (checkCanInteract && !_actionBlockerSystem.CanInteract(user, used))
  980. return false;
  981. if (checkAccess && !InRangeUnobstructed(user, used))
  982. return false;
  983. // Check if interacted entity is in the same container, the direct child, or direct parent of the user.
  984. // This is bypassed IF the interaction happened through an item slot (e.g., backpack UI)
  985. if (checkAccess && !IsAccessible(user, used))
  986. return false;
  987. complexInteractions ??= _actionBlockerSystem.CanComplexInteract(user);
  988. var activateMsg = new ActivateInWorldEvent(user, used, complexInteractions.Value);
  989. RaiseLocalEvent(used, activateMsg, true);
  990. if (activateMsg.Handled)
  991. {
  992. DoContactInteraction(user, used);
  993. if (!activateMsg.WasLogged)
  994. _adminLogger.Add(LogType.InteractActivate, LogImpact.Low, $"{ToPrettyString(user):user} activated {ToPrettyString(used):used}");
  995. if (delayComponent != null)
  996. _useDelay.TryResetDelay(used, component: delayComponent);
  997. return true;
  998. }
  999. DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used));
  1000. var userEv = new UserActivateInWorldEvent(user, used, complexInteractions.Value);
  1001. RaiseLocalEvent(user, userEv, true);
  1002. if (!userEv.Handled)
  1003. return false;
  1004. DoContactInteraction(user, used);
  1005. // Still need to call this even without checkUseDelay in case this gets relayed from Activate.
  1006. if (delayComponent != null)
  1007. _useDelay.TryResetDelay(used, component: delayComponent);
  1008. _adminLogger.Add(LogType.InteractActivate, LogImpact.Low, $"{ToPrettyString(user):user} activated {ToPrettyString(used):used}");
  1009. return true;
  1010. }
  1011. #endregion
  1012. #region Hands
  1013. #region Use
  1014. /// <summary>
  1015. /// Raises UseInHandEvents and activates the IUse behaviors of an entity
  1016. /// Does not check accessibility or range, for obvious reasons
  1017. /// </summary>
  1018. /// <returns>True if the interaction was handled. False otherwise</returns>
  1019. public bool UseInHandInteraction(
  1020. EntityUid user,
  1021. EntityUid used,
  1022. bool checkCanUse = true,
  1023. bool checkCanInteract = true,
  1024. bool checkUseDelay = true)
  1025. {
  1026. if (IsDeleted(user) || IsDeleted(used))
  1027. return false;
  1028. _delayQuery.TryComp(used, out var delayComponent);
  1029. if (checkUseDelay && delayComponent != null && _useDelay.IsDelayed((used, delayComponent)))
  1030. return true; // if the item is on cooldown, we consider this handled.
  1031. if (checkCanInteract && !_actionBlockerSystem.CanInteract(user, used))
  1032. return false;
  1033. if (checkCanUse && !_actionBlockerSystem.CanUseHeldEntity(user, used))
  1034. return false;
  1035. var useMsg = new UseInHandEvent(user);
  1036. RaiseLocalEvent(used, useMsg, true);
  1037. if (useMsg.Handled)
  1038. {
  1039. DoContactInteraction(user, used, useMsg);
  1040. if (delayComponent != null && useMsg.ApplyDelay)
  1041. _useDelay.TryResetDelay((used, delayComponent));
  1042. return true;
  1043. }
  1044. DebugTools.Assert(!IsDeleted(user) && !IsDeleted(used));
  1045. // else, default to activating the item
  1046. return InteractionActivate(user, used, false, false, false, checkDeletion: false);
  1047. }
  1048. /// <summary>
  1049. /// Alternative interactions on an entity.
  1050. /// </summary>
  1051. /// <remarks>
  1052. /// Uses the context menu verb list, and acts out the highest priority alternative interaction verb.
  1053. /// </remarks>
  1054. /// <returns>True if the interaction was handled, false otherwise.</returns>
  1055. public bool AltInteract(EntityUid user, EntityUid target)
  1056. {
  1057. // Get list of alt-interact verbs
  1058. var verbs = _verbSystem.GetLocalVerbs(target, user, typeof(AlternativeVerb));
  1059. if (verbs.Count == 0)
  1060. return false;
  1061. _verbSystem.ExecuteVerb(verbs.First(), user, target);
  1062. return true;
  1063. }
  1064. #endregion
  1065. public void DroppedInteraction(EntityUid user, EntityUid item)
  1066. {
  1067. if (IsDeleted(user) || IsDeleted(item))
  1068. return;
  1069. var dropMsg = new DroppedEvent(user);
  1070. RaiseLocalEvent(item, dropMsg, true);
  1071. // If the dropper is rotated then use their targetrelativerotation as the drop rotation
  1072. var rotation = Angle.Zero;
  1073. if (TryComp<InputMoverComponent>(user, out var mover))
  1074. {
  1075. rotation = mover.TargetRelativeRotation;
  1076. }
  1077. Transform(item).LocalRotation = rotation;
  1078. }
  1079. #endregion
  1080. /// <summary>
  1081. /// Check if a user can access a target (stored in the same containers) and is in range without obstructions.
  1082. /// </summary>
  1083. public bool InRangeAndAccessible(
  1084. Entity<TransformComponent?> user,
  1085. Entity<TransformComponent?> target,
  1086. float range = InteractionRange,
  1087. CollisionGroup collisionMask = InRangeUnobstructedMask,
  1088. Ignored? predicate = null)
  1089. {
  1090. if (user == target)
  1091. return true;
  1092. if (!Resolve(user, ref user.Comp))
  1093. return false;
  1094. if (!Resolve(target, ref target.Comp))
  1095. return false;
  1096. return IsAccessible(user, target) && InRangeUnobstructed(user, target, range, collisionMask, predicate);
  1097. }
  1098. /// <summary>
  1099. /// Check if a user can access a target or if they are stored in different containers.
  1100. /// </summary>
  1101. public bool IsAccessible(Entity<TransformComponent?> user, Entity<TransformComponent?> target)
  1102. {
  1103. var ev = new AccessibleOverrideEvent(user, target);
  1104. RaiseLocalEvent(user, ref ev);
  1105. if (ev.Handled)
  1106. return ev.Accessible;
  1107. if (_containerSystem.IsInSameOrParentContainer(user, target, out _, out var container))
  1108. return true;
  1109. return container != null && CanAccessViaStorage(user, target, container);
  1110. }
  1111. /// <summary>
  1112. /// If a target is in range, but not in the same container as the user, it may be inside of a backpack. This
  1113. /// checks if the user can access the item in these situations.
  1114. /// </summary>
  1115. public bool CanAccessViaStorage(EntityUid user, EntityUid target)
  1116. {
  1117. if (!_containerSystem.TryGetContainingContainer((target, null, null), out var container))
  1118. return false;
  1119. return CanAccessViaStorage(user, target, container);
  1120. }
  1121. /// <inheritdoc cref="CanAccessViaStorage(Robust.Shared.GameObjects.EntityUid,Robust.Shared.GameObjects.EntityUid)"/>
  1122. public bool CanAccessViaStorage(EntityUid user, EntityUid target, BaseContainer container)
  1123. {
  1124. if (StorageComponent.ContainerId != container.ID)
  1125. return false;
  1126. // we don't check if the user can access the storage entity itself. This should be handed by the UI system.
  1127. return _ui.IsUiOpen(container.Owner, StorageComponent.StorageUiKey.Key, user);
  1128. }
  1129. /// <summary>
  1130. /// Checks whether an entity currently equipped by another player is accessible to some user. This shouldn't
  1131. /// be used as a general interaction check, as these kinda of interactions should generally trigger a
  1132. /// do-after and a warning for the other player.
  1133. /// </summary>
  1134. public bool CanAccessEquipment(EntityUid user, EntityUid target)
  1135. {
  1136. if (Deleted(target))
  1137. return false;
  1138. if (!_containerSystem.TryGetContainingContainer((target, null, null), out var container))
  1139. return false;
  1140. var wearer = container.Owner;
  1141. if (!_inventory.TryGetSlot(wearer, container.ID, out var slotDef))
  1142. return false;
  1143. if (wearer == user)
  1144. return true;
  1145. if (_strippable.IsStripHidden(slotDef, user))
  1146. return false;
  1147. return InRangeUnobstructed(user, wearer) && _containerSystem.IsInSameOrParentContainer(user, wearer);
  1148. }
  1149. protected bool ValidateClientInput(
  1150. ICommonSession? session,
  1151. EntityCoordinates coords,
  1152. EntityUid uid,
  1153. [NotNullWhen(true)] out EntityUid? userEntity)
  1154. {
  1155. userEntity = null;
  1156. if (!coords.IsValid(EntityManager))
  1157. {
  1158. Log.Info($"Invalid Coordinates: client={session}, coords={coords}");
  1159. return false;
  1160. }
  1161. if (IsClientSide(uid))
  1162. {
  1163. Log.Warning($"Client sent interaction with client-side entity. Session={session}, Uid={uid}");
  1164. return false;
  1165. }
  1166. userEntity = session?.AttachedEntity;
  1167. if (userEntity == null || !userEntity.Value.Valid)
  1168. {
  1169. Log.Warning($"Client sent interaction with no attached entity. Session={session}");
  1170. return false;
  1171. }
  1172. if (!Exists(userEntity))
  1173. {
  1174. Log.Warning($"Client attempted interaction with a non-existent attached entity. Session={session}, entity={userEntity}");
  1175. return false;
  1176. }
  1177. return _rateLimit.CountAction(session!, RateLimitKey) == RateLimitStatus.Allowed;
  1178. }
  1179. /// <summary>
  1180. /// Simple convenience function to raise contact events (disease, forensics, etc).
  1181. /// </summary>
  1182. public void DoContactInteraction(EntityUid uidA, EntityUid? uidB, HandledEntityEventArgs? args = null)
  1183. {
  1184. if (uidB == null || args?.Handled == false)
  1185. return;
  1186. if (uidA == uidB.Value)
  1187. return;
  1188. if (!TryComp(uidA, out MetaDataComponent? metaA) || metaA.EntityPaused)
  1189. return;
  1190. if (!TryComp(uidB, out MetaDataComponent? metaB) || metaB.EntityPaused)
  1191. return;
  1192. // TODO Struct event
  1193. var ev = new ContactInteractionEvent(uidB.Value);
  1194. RaiseLocalEvent(uidA, ev);
  1195. ev.Other = uidA;
  1196. RaiseLocalEvent(uidB.Value, ev);
  1197. }
  1198. private void HandleUserInterfaceRangeCheck(ref BoundUserInterfaceCheckRangeEvent ev)
  1199. {
  1200. if (ev.Result == BoundUserInterfaceRangeResult.Fail)
  1201. return;
  1202. ev.Result = UiRangeCheck(ev.Actor!, ev.Target, ev.Data.InteractionRange)
  1203. ? BoundUserInterfaceRangeResult.Pass
  1204. : BoundUserInterfaceRangeResult.Fail;
  1205. }
  1206. /// <summary>
  1207. /// Gets the entity that is currently being "used" for the interaction.
  1208. /// In most cases, this refers to the entity in the character's active hand.
  1209. /// </summary>
  1210. /// <returns>If there is an entity being used.</returns>
  1211. public bool TryGetUsedEntity(EntityUid user, [NotNullWhen(true)] out EntityUid? used, bool checkCanUse = true)
  1212. {
  1213. var ev = new GetUsedEntityEvent(user);
  1214. RaiseLocalEvent(user, ref ev);
  1215. used = ev.Used;
  1216. if (!ev.Handled)
  1217. return false;
  1218. // Can the user use the held entity?
  1219. if (checkCanUse && !_actionBlockerSystem.CanUseHeldEntity(user, ev.Used!.Value))
  1220. {
  1221. used = null;
  1222. return false;
  1223. }
  1224. return ev.Handled;
  1225. }
  1226. [Obsolete("Use ActionBlockerSystem")]
  1227. public bool SupportsComplexInteractions(EntityUid user)
  1228. {
  1229. return _actionBlockerSystem.CanComplexInteract(user);
  1230. }
  1231. }
  1232. /// <summary>
  1233. /// Raised when a player attempts to activate an item in an inventory slot or hand slot
  1234. /// </summary>
  1235. [Serializable, NetSerializable]
  1236. public sealed class InteractInventorySlotEvent : EntityEventArgs
  1237. {
  1238. /// <summary>
  1239. /// Entity that was interacted with.
  1240. /// </summary>
  1241. public NetEntity ItemUid { get; }
  1242. /// <summary>
  1243. /// Whether the interaction used the alt-modifier to trigger alternative interactions.
  1244. /// </summary>
  1245. public bool AltInteract { get; }
  1246. public InteractInventorySlotEvent(NetEntity itemUid, bool altInteract = false)
  1247. {
  1248. ItemUid = itemUid;
  1249. AltInteract = altInteract;
  1250. }
  1251. }
  1252. /// <summary>
  1253. /// Raised directed by-ref on an entity to determine what item will be used in interactions.
  1254. /// </summary>
  1255. [ByRefEvent]
  1256. public record struct GetUsedEntityEvent(EntityUid User)
  1257. {
  1258. public EntityUid User = User;
  1259. public EntityUid? Used = null;
  1260. public bool Handled => Used != null;
  1261. };
  1262. /// <summary>
  1263. /// Raised directed by-ref on an item to determine if hand interactions should go through.
  1264. /// Defaults to allowing hand interactions to go through. Cancel to force the item to be attacked instead.
  1265. /// </summary>
  1266. /// <param name="Cancelled">Whether the hand interaction should be cancelled.</param>
  1267. [ByRefEvent]
  1268. public record struct CombatModeShouldHandInteractEvent(bool Cancelled = false);
  1269. /// <summary>
  1270. /// Override event raised directed on the user to say the target is accessible.
  1271. /// </summary>
  1272. /// <param name="User"></param>
  1273. /// <param name="Target"></param>
  1274. [ByRefEvent]
  1275. public record struct AccessibleOverrideEvent(EntityUid User, EntityUid Target)
  1276. {
  1277. public readonly EntityUid User = User;
  1278. public readonly EntityUid Target = Target;
  1279. public bool Handled;
  1280. public bool Accessible = false;
  1281. }
  1282. /// <summary>
  1283. /// Override event raised directed on a user to check InRangeUnoccluded AND InRangeUnobstructed to the target if you require custom logic.
  1284. /// </summary>
  1285. [ByRefEvent]
  1286. public record struct InRangeOverrideEvent(EntityUid User, EntityUid Target)
  1287. {
  1288. public readonly EntityUid User = User;
  1289. public readonly EntityUid Target = Target;
  1290. public bool Handled;
  1291. public bool InRange = false;
  1292. }
  1293. }