VerbSystem.cs 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. using System.Diagnostics.CodeAnalysis;
  2. using System.Numerics;
  3. using Content.Client.Examine;
  4. using Content.Client.Gameplay;
  5. using Content.Client.Popups;
  6. using Content.Shared.CCVar;
  7. using Content.Shared.Examine;
  8. using Content.Shared.Tag;
  9. using Content.Shared.Verbs;
  10. using JetBrains.Annotations;
  11. using Robust.Client.ComponentTrees;
  12. using Robust.Client.GameObjects;
  13. using Robust.Client.Graphics;
  14. using Robust.Client.Player;
  15. using Robust.Client.State;
  16. using Robust.Shared.Configuration;
  17. using Robust.Shared.Containers;
  18. using Robust.Shared.Map;
  19. using Robust.Shared.Utility;
  20. namespace Content.Client.Verbs
  21. {
  22. [UsedImplicitly]
  23. public sealed class VerbSystem : SharedVerbSystem
  24. {
  25. [Dependency] private readonly PopupSystem _popupSystem = default!;
  26. [Dependency] private readonly ExamineSystem _examine = default!;
  27. [Dependency] private readonly SpriteTreeSystem _tree = default!;
  28. [Dependency] private readonly TagSystem _tagSystem = default!;
  29. [Dependency] private readonly IStateManager _stateManager = default!;
  30. [Dependency] private readonly IEyeManager _eyeManager = default!;
  31. [Dependency] private readonly IPlayerManager _playerManager = default!;
  32. [Dependency] private readonly SharedContainerSystem _containers = default!;
  33. [Dependency] private readonly IConfigurationManager _cfg = default!;
  34. [Dependency] private readonly EntityLookupSystem _lookup = default!;
  35. private float _lookupSize;
  36. /// <summary>
  37. /// These flags determine what entities the user can see on the context menu.
  38. /// </summary>
  39. public MenuVisibility Visibility;
  40. public Action<VerbsResponseEvent>? OnVerbsResponse;
  41. public override void Initialize()
  42. {
  43. base.Initialize();
  44. SubscribeNetworkEvent<VerbsResponseEvent>(HandleVerbResponse);
  45. Subs.CVar(_cfg, CCVars.GameEntityMenuLookup, OnLookupChanged, true);
  46. }
  47. private void OnLookupChanged(float val)
  48. {
  49. _lookupSize = val;
  50. }
  51. /// <summary>
  52. /// Get all of the entities in an area for displaying on the context menu.
  53. /// </summary>
  54. /// <returns>True if any entities were found.</returns>
  55. public bool TryGetEntityMenuEntities(MapCoordinates targetPos, [NotNullWhen(true)] out List<EntityUid>? entities)
  56. {
  57. entities = null;
  58. if (_stateManager.CurrentState is not GameplayStateBase)
  59. return false;
  60. if (_playerManager.LocalEntity is not { } player)
  61. return false;
  62. // If FOV drawing is disabled, we will modify the visibility option to ignore visiblity checks.
  63. var visibility = _eyeManager.CurrentEye.DrawFov ? Visibility : Visibility | MenuVisibility.NoFov;
  64. var ev = new MenuVisibilityEvent
  65. {
  66. TargetPos = targetPos,
  67. Visibility = visibility,
  68. };
  69. RaiseLocalEvent(player, ref ev);
  70. visibility = ev.Visibility;
  71. // Initially, we include all entities returned by a sprite area lookup
  72. var box = Box2.CenteredAround(targetPos.Position, new Vector2(_lookupSize, _lookupSize));
  73. var queryResult = _tree.QueryAabb(targetPos.MapId, box);
  74. entities = new List<EntityUid>(queryResult.Count);
  75. foreach (var ent in queryResult)
  76. {
  77. entities.Add(ent.Uid);
  78. }
  79. // If we're in a container list all other entities in it.
  80. // E.g., allow players in lockers to examine / interact with other entities in the same locker
  81. if (_containers.TryGetContainingContainer((player, null), out var container))
  82. {
  83. // Only include the container contents when clicking near it.
  84. if (entities.Contains(container.Owner)
  85. || _containers.TryGetOuterContainer(container.Owner, Transform(container.Owner), out var outer)
  86. && entities.Contains(outer.Owner))
  87. {
  88. // The container itself might be in some other container, so it might not have been added by the
  89. // sprite tree lookup.
  90. if (!entities.Contains(container.Owner))
  91. entities.Add(container.Owner);
  92. // TODO Context Menu
  93. // This might miss entities in some situations. E.g., one of the contained entities entity in it, that
  94. // itself has another entity attached to it, then we should be able to "see" that entity.
  95. // E.g., if a security guard is on a segway and gets thrown in a locker, this wouldn't let you see the guard.
  96. foreach (var ent in container.ContainedEntities)
  97. {
  98. if (!entities.Contains(ent))
  99. entities.Add(ent);
  100. }
  101. }
  102. }
  103. if ((visibility & MenuVisibility.InContainer) != 0)
  104. {
  105. // This is inefficient, but I'm lazy and CBF implementing my own recursive container method. Note that
  106. // this might actually fail to add the contained children of some entities in the menu. E.g., an entity
  107. // with a large sprite aabb, but small broadphase might appear in the menu, but have its children added
  108. // by this.
  109. var flags = LookupFlags.All & ~LookupFlags.Sensors;
  110. foreach (var e in _lookup.GetEntitiesInRange(targetPos, _lookupSize, flags: flags))
  111. {
  112. if (!entities.Contains(e))
  113. entities.Add(e);
  114. }
  115. }
  116. // Do we have to do FoV checks?
  117. if ((visibility & MenuVisibility.NoFov) == 0)
  118. {
  119. TryComp(player, out ExaminerComponent? examiner);
  120. for (var i = entities.Count - 1; i >= 0; i--)
  121. {
  122. if (!_examine.CanExamine(player, targetPos, e => e == player, entities[i], examiner))
  123. entities.RemoveSwap(i);
  124. }
  125. }
  126. if ((visibility & MenuVisibility.Invisible) != 0)
  127. return entities.Count != 0;
  128. for (var i = entities.Count - 1; i >= 0; i--)
  129. {
  130. if (_tagSystem.HasTag(entities[i], "HideContextMenu"))
  131. entities.RemoveSwap(i);
  132. }
  133. // Unless we added entities in containers, every entity should already have a visible sprite due to
  134. // the fact that we used the sprite tree query.
  135. if (container == null && (visibility & MenuVisibility.InContainer) == 0)
  136. return entities.Count != 0;
  137. var spriteQuery = GetEntityQuery<SpriteComponent>();
  138. for (var i = entities.Count - 1; i >= 0; i--)
  139. {
  140. if (!spriteQuery.TryGetComponent(entities[i], out var spriteComponent) || !spriteComponent.Visible)
  141. entities.RemoveSwap(i);
  142. }
  143. return entities.Count != 0;
  144. }
  145. /// <summary>
  146. /// Ask the server to send back a list of server-side verbs, and for now return an incomplete list of verbs
  147. /// (only those defined locally).
  148. /// </summary>
  149. public SortedSet<Verb> GetVerbs(NetEntity target, EntityUid user, List<Type> verbTypes, out List<VerbCategory> extraCategories, bool force = false)
  150. {
  151. if (!target.IsClientSide())
  152. RaiseNetworkEvent(new RequestServerVerbsEvent(target, verbTypes, adminRequest: force));
  153. // Some admin menu interactions will try get verbs for entities that have not yet been sent to the player.
  154. if (!TryGetEntity(target, out var local))
  155. {
  156. extraCategories = new();
  157. return new();
  158. }
  159. return GetLocalVerbs(local.Value, user, verbTypes, out extraCategories, force);
  160. }
  161. /// <summary>
  162. /// Execute actions associated with the given verb.
  163. /// </summary>
  164. /// <remarks>
  165. /// Unless this is a client-exclusive verb, this will also tell the server to run the same verb.
  166. /// </remarks>
  167. public void ExecuteVerb(EntityUid target, Verb verb)
  168. {
  169. ExecuteVerb(GetNetEntity(target), verb);
  170. }
  171. /// <summary>
  172. /// Execute actions associated with the given verb.
  173. /// </summary>
  174. /// <remarks>
  175. /// Unless this is a client-exclusive verb, this will also tell the server to run the same verb.
  176. /// </remarks>
  177. public void ExecuteVerb(NetEntity target, Verb verb)
  178. {
  179. if ( _playerManager.LocalEntity is not {} user)
  180. return;
  181. // is this verb actually valid?
  182. if (verb.Disabled)
  183. {
  184. // maybe send an informative pop-up message.
  185. if (!string.IsNullOrWhiteSpace(verb.Message))
  186. _popupSystem.PopupEntity(verb.Message, user);
  187. return;
  188. }
  189. if (verb.ClientExclusive || target.IsClientSide())
  190. // is this a client exclusive (gui) verb?
  191. ExecuteVerb(verb, user, GetEntity(target));
  192. else
  193. EntityManager.RaisePredictiveEvent(new ExecuteVerbEvent(target, verb));
  194. }
  195. private void HandleVerbResponse(VerbsResponseEvent msg)
  196. {
  197. OnVerbsResponse?.Invoke(msg);
  198. }
  199. }
  200. }