EntityMenuUIController.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. using System.Linq;
  2. using System.Numerics;
  3. using Content.Client.CombatMode;
  4. using Content.Client.Examine;
  5. using Content.Client.Gameplay;
  6. using Content.Client.Verbs;
  7. using Content.Client.Verbs.UI;
  8. using Content.Shared.CCVar;
  9. using Content.Shared.Examine;
  10. using Content.Shared.IdentityManagement;
  11. using Content.Shared.Input;
  12. using Content.Shared.Verbs;
  13. using Robust.Client.GameObjects;
  14. using Robust.Client.Graphics;
  15. using Robust.Client.Input;
  16. using Robust.Client.Player;
  17. using Robust.Client.State;
  18. using Robust.Client.UserInterface;
  19. using Robust.Client.UserInterface.Controllers;
  20. using Robust.Shared.Configuration;
  21. using Robust.Shared.Input;
  22. using Robust.Shared.Input.Binding;
  23. using Robust.Shared.Map;
  24. using Robust.Shared.Timing;
  25. namespace Content.Client.ContextMenu.UI
  26. {
  27. /// <summary>
  28. /// This class handles the displaying of the entity context menu.
  29. /// </summary>
  30. /// <remarks>
  31. /// This also provides functions to get
  32. /// a list of entities near the mouse position, add them to the context menu grouped by prototypes, and remove
  33. /// them from the menu as they move out of sight.
  34. /// </remarks>
  35. public sealed partial class EntityMenuUIController : UIController, IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>
  36. {
  37. [Dependency] private readonly IEntitySystemManager _systemManager = default!;
  38. [Dependency] private readonly IEntityManager _entityManager = default!;
  39. [Dependency] private readonly IPlayerManager _playerManager = default!;
  40. [Dependency] private readonly IStateManager _stateManager = default!;
  41. [Dependency] private readonly IInputManager _inputManager = default!;
  42. [Dependency] private readonly IConfigurationManager _cfg = default!;
  43. [Dependency] private readonly IGameTiming _gameTiming = default!;
  44. [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
  45. [Dependency] private readonly IEyeManager _eyeManager = default!;
  46. [Dependency] private readonly ContextMenuUIController _context = default!;
  47. [Dependency] private readonly VerbMenuUIController _verb = default!;
  48. [UISystemDependency] private readonly VerbSystem _verbSystem = default!;
  49. [UISystemDependency] private readonly ExamineSystem _examineSystem = default!;
  50. [UISystemDependency] private readonly TransformSystem _xform = default!;
  51. [UISystemDependency] private readonly CombatModeSystem _combatMode = default!;
  52. private bool _updating;
  53. /// <summary>
  54. /// This maps the currently displayed entities to the actual GUI elements.
  55. /// </summary>
  56. /// <remarks>
  57. /// This is used remove GUI elements when the entities are deleted. or leave the LOS.
  58. /// </remarks>
  59. public Dictionary<EntityUid, EntityMenuElement> Elements = new();
  60. public void OnStateEntered(GameplayState state)
  61. {
  62. _updating = true;
  63. _cfg.OnValueChanged(CCVars.EntityMenuGroupingType, OnGroupingChanged, true);
  64. _context.OnContextKeyEvent += OnKeyBindDown;
  65. CommandBinds.Builder
  66. .Bind(EngineKeyFunctions.UseSecondary, new PointerInputCmdHandler(HandleOpenEntityMenu, outsidePrediction: true))
  67. .Register<EntityMenuUIController>();
  68. }
  69. public void OnStateExited(GameplayState state)
  70. {
  71. _updating = false;
  72. Elements.Clear();
  73. _cfg.UnsubValueChanged(CCVars.EntityMenuGroupingType, OnGroupingChanged);
  74. _context.OnContextKeyEvent -= OnKeyBindDown;
  75. CommandBinds.Unregister<EntityMenuUIController>();
  76. }
  77. /// <summary>
  78. /// Given a list of entities, sort them into groups and them to a new entity menu.
  79. /// </summary>
  80. public void OpenRootMenu(List<EntityUid> entities)
  81. {
  82. // close any old menus first.
  83. if (_context.RootMenu.Visible)
  84. _context.Close();
  85. var entitySpriteStates = GroupEntities(entities);
  86. var orderedStates = entitySpriteStates.ToList();
  87. orderedStates.Sort((x, y) => string.Compare(
  88. Identity.Name(x.First(), _entityManager),
  89. Identity.Name(y.First(), _entityManager),
  90. StringComparison.CurrentCulture));
  91. Elements.Clear();
  92. AddToUI(orderedStates);
  93. var box = UIBox2.FromDimensions(_userInterfaceManager.MousePositionScaled.Position, new Vector2(1, 1));
  94. _context.RootMenu.Open(box);
  95. }
  96. public void OnKeyBindDown(ContextMenuElement element, GUIBoundKeyEventArgs args)
  97. {
  98. if (element is not EntityMenuElement entityElement)
  99. return;
  100. // get an entity associated with this element
  101. var entity = entityElement.Entity;
  102. entity ??= GetFirstEntityOrNull(element.SubMenu);
  103. // Deleted() automatically checks for null & existence.
  104. if (_entityManager.Deleted(entity))
  105. return;
  106. // do examination?
  107. if (args.Function == ContentKeyFunctions.ExamineEntity)
  108. {
  109. _systemManager.GetEntitySystem<ExamineSystem>().DoExamine(entity.Value);
  110. args.Handle();
  111. return;
  112. }
  113. // do some other server-side interaction?
  114. if (args.Function == EngineKeyFunctions.Use ||
  115. args.Function == ContentKeyFunctions.ActivateItemInWorld ||
  116. args.Function == ContentKeyFunctions.AltActivateItemInWorld ||
  117. args.Function == ContentKeyFunctions.Point ||
  118. args.Function == ContentKeyFunctions.TryPullObject ||
  119. args.Function == ContentKeyFunctions.MovePulledObject)
  120. {
  121. var inputSys = _systemManager.GetEntitySystem<InputSystem>();
  122. var func = args.Function;
  123. var funcId = _inputManager.NetworkBindMap.KeyFunctionID(func);
  124. var message = new ClientFullInputCmdMessage(
  125. _gameTiming.CurTick,
  126. _gameTiming.TickFraction,
  127. funcId)
  128. {
  129. State = BoundKeyState.Down,
  130. Coordinates = _entityManager.GetComponent<TransformComponent>(entity.Value).Coordinates,
  131. ScreenCoordinates = args.PointerLocation,
  132. Uid = entity.Value,
  133. };
  134. var session = _playerManager.LocalSession;
  135. if (session != null)
  136. {
  137. inputSys.HandleInputCommand(session, func, message);
  138. }
  139. _context.Close();
  140. args.Handle();
  141. }
  142. }
  143. private bool HandleOpenEntityMenu(in PointerInputCmdHandler.PointerInputCmdArgs args)
  144. {
  145. if (args.State != BoundKeyState.Down)
  146. return false;
  147. if (_stateManager.CurrentState is not GameplayStateBase)
  148. return false;
  149. if (_combatMode.IsInCombatMode(args.Session?.AttachedEntity))
  150. return false;
  151. var coords = _xform.ToMapCoordinates(args.Coordinates);
  152. if (_verbSystem.TryGetEntityMenuEntities(coords, out var entities))
  153. OpenRootMenu(entities);
  154. return true;
  155. }
  156. /// <summary>
  157. /// Check that entities in the context menu are still visible. If not, remove them from the context menu.
  158. /// </summary>
  159. public override void FrameUpdate(FrameEventArgs args)
  160. {
  161. if (!_updating || _context.RootMenu == null)
  162. return;
  163. if (!_context.RootMenu.Visible)
  164. return;
  165. if (_playerManager.LocalEntity is not { } player ||
  166. !player.IsValid())
  167. return;
  168. // Do we need to do in-range unOccluded checks?
  169. var visibility = _verbSystem.Visibility;
  170. if (!_eyeManager.CurrentEye.DrawFov)
  171. {
  172. visibility &= ~MenuVisibility.NoFov;
  173. }
  174. var ev = new MenuVisibilityEvent()
  175. {
  176. Visibility = visibility,
  177. };
  178. _entityManager.EventBus.RaiseLocalEvent(player, ref ev);
  179. visibility = ev.Visibility;
  180. _entityManager.TryGetComponent(player, out ExaminerComponent? examiner);
  181. var xformQuery = _entityManager.GetEntityQuery<TransformComponent>();
  182. foreach (var entity in Elements.Keys.ToList())
  183. {
  184. if (!xformQuery.TryGetComponent(entity, out var xform))
  185. {
  186. // entity was deleted
  187. RemoveEntity(entity);
  188. continue;
  189. }
  190. if ((visibility & MenuVisibility.NoFov) == MenuVisibility.NoFov)
  191. continue;
  192. var pos = new MapCoordinates(_xform.GetWorldPosition(xform, xformQuery), xform.MapID);
  193. if (!_examineSystem.CanExamine(player, pos, e => e == player || e == entity, entity, examiner))
  194. RemoveEntity(entity);
  195. }
  196. }
  197. /// <summary>
  198. /// Add menu elements for a list of grouped entities;
  199. /// </summary>
  200. /// <param name="entityGroups"> A list of entity groups. Entities are grouped together based on prototype.</param>
  201. private void AddToUI(List<List<EntityUid>> entityGroups)
  202. {
  203. // If there is only a single group. We will just directly list individual entities
  204. if (entityGroups.Count == 1)
  205. {
  206. AddGroupToMenu(entityGroups[0], _context.RootMenu);
  207. return;
  208. }
  209. foreach (var group in entityGroups)
  210. {
  211. if (group.Count > 1)
  212. {
  213. AddGroupToUI(group);
  214. }
  215. else
  216. {
  217. // this group only has a single entity, add a simple menu element
  218. AddEntityToMenu(group[0], _context.RootMenu);
  219. }
  220. }
  221. }
  222. /// <summary>
  223. /// Given a group of entities, add a menu element that has a pop-up sub-menu listing group members
  224. /// </summary>
  225. private void AddGroupToUI(List<EntityUid> group)
  226. {
  227. EntityMenuElement element = new();
  228. ContextMenuPopup subMenu = new(_context, element);
  229. AddGroupToMenu(group, subMenu);
  230. UpdateElement(element);
  231. _context.AddElement(_context.RootMenu, element);
  232. }
  233. /// <summary>
  234. /// Add the group of entities to the menu
  235. /// </summary>
  236. private void AddGroupToMenu(List<EntityUid> group, ContextMenuPopup menu)
  237. {
  238. foreach (var entity in group)
  239. {
  240. AddEntityToMenu(entity, menu);
  241. }
  242. }
  243. /// <summary>
  244. /// Add the entity to the menu
  245. /// </summary>
  246. private void AddEntityToMenu(EntityUid entity, ContextMenuPopup menu)
  247. {
  248. var element = new EntityMenuElement(entity);
  249. element.SubMenu = new ContextMenuPopup(_context, element);
  250. element.SubMenu.OnPopupOpen += () => _verb.OpenVerbMenu(entity, popup: element.SubMenu);
  251. element.SubMenu.OnPopupHide += element.SubMenu.MenuBody.DisposeAllChildren;
  252. _context.AddElement(menu, element);
  253. Elements.TryAdd(entity, element);
  254. }
  255. /// <summary>
  256. /// Remove an entity from the entity context menu.
  257. /// </summary>
  258. private void RemoveEntity(EntityUid entity)
  259. {
  260. // find the element associated with this entity
  261. if (!Elements.TryGetValue(entity, out var element))
  262. {
  263. Logger.Error($"Attempted to remove unknown entity from the entity menu: {_entityManager.GetComponent<MetaDataComponent>(entity).EntityName} ({entity})");
  264. return;
  265. }
  266. // remove the element
  267. var parent = element.ParentMenu?.ParentElement;
  268. element.Dispose();
  269. Elements.Remove(entity);
  270. // update any parent elements
  271. if (parent is EntityMenuElement e)
  272. UpdateElement(e);
  273. // If this was the last entity, close the entity menu
  274. if (_context.RootMenu.MenuBody.ChildCount == 0)
  275. _context.Close();
  276. }
  277. /// <summary>
  278. /// Update the information displayed by a menu element.
  279. /// </summary>
  280. /// <remarks>
  281. /// This is called when initializing elements or after an element was removed from a sub-menu.
  282. /// </remarks>
  283. private void UpdateElement(EntityMenuElement element)
  284. {
  285. if (element.SubMenu == null)
  286. return;
  287. // Get the first entity in the sub-menus
  288. var entity = GetFirstEntityOrNull(element.SubMenu);
  289. if (entity == null)
  290. {
  291. // This whole element has no associated entities. We should remove it
  292. element.Dispose();
  293. return;
  294. }
  295. element.UpdateEntity(entity);
  296. element.UpdateCount();
  297. if (element.Count == 1)
  298. {
  299. // There was only one entity in the sub-menu. So we will just remove the sub-menu and point directly to
  300. // that entity.
  301. element.Entity = entity;
  302. element.SubMenu.Dispose();
  303. element.SubMenu = null;
  304. Elements[entity.Value] = element;
  305. }
  306. // update the parent element, so that it's count and entity icon gets updated.
  307. var parent = element.ParentMenu?.ParentElement;
  308. if (parent is EntityMenuElement e)
  309. UpdateElement(e);
  310. }
  311. /// <summary>
  312. /// Recursively look through a sub-menu and return the first entity.
  313. /// </summary>
  314. private EntityUid? GetFirstEntityOrNull(ContextMenuPopup? menu)
  315. {
  316. if (menu == null)
  317. return null;
  318. foreach (var element in menu.MenuBody.Children)
  319. {
  320. if (element is not EntityMenuElement entityElement)
  321. continue;
  322. if (entityElement.Entity != null)
  323. {
  324. if (!_entityManager.Deleted(entityElement.Entity))
  325. return entityElement.Entity;
  326. continue;
  327. }
  328. // if the element has no entity, its a group of entities with another attached sub-menu.
  329. var entity = GetFirstEntityOrNull(entityElement.SubMenu);
  330. if (entity != null)
  331. return entity;
  332. }
  333. return null;
  334. }
  335. }
  336. }