1
0

VerbMenuUIController.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. using System.Linq;
  2. using System.Numerics;
  3. using Content.Client.CombatMode;
  4. using Content.Client.ContextMenu.UI;
  5. using Content.Client.Gameplay;
  6. using Content.Client.Mapping;
  7. using Content.Shared.Input;
  8. using Content.Shared.Verbs;
  9. using Robust.Client.Player;
  10. using Robust.Client.UserInterface;
  11. using Robust.Client.UserInterface.Controllers;
  12. using Robust.Shared.Collections;
  13. using Robust.Shared.Input;
  14. using Robust.Shared.Utility;
  15. namespace Content.Client.Verbs.UI
  16. {
  17. /// <summary>
  18. /// This class handles the displaying of the verb menu.
  19. /// </summary>
  20. /// <remarks>
  21. /// In addition to the normal <see cref="ContextMenuUIController"/> functionality, this also provides functions
  22. /// open a verb menu for a given entity, add verbs to it, and add server-verbs when the server response is
  23. /// received.
  24. /// </remarks>
  25. public sealed class VerbMenuUIController : UIController,
  26. IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>,
  27. IOnStateEntered<MappingState>, IOnStateExited<MappingState>
  28. {
  29. [Dependency] private readonly IPlayerManager _playerManager = default!;
  30. [Dependency] private readonly ContextMenuUIController _context = default!;
  31. [UISystemDependency] private readonly CombatModeSystem _combatMode = default!;
  32. [UISystemDependency] private readonly VerbSystem _verbSystem = default!;
  33. public NetEntity CurrentTarget;
  34. public SortedSet<Verb> CurrentVerbs = new();
  35. public List<VerbCategory> ExtraCategories = new();
  36. /// <summary>
  37. /// Separate from <see cref="ContextMenuUIController.RootMenu"/>, since we can open a verb menu as a submenu
  38. /// of an entity menu element. If that happens, we need to be aware and close it properly.
  39. /// </summary>
  40. public ContextMenuPopup? OpenMenu = null;
  41. public void OnStateEntered(GameplayState state)
  42. {
  43. _context.OnContextKeyEvent += OnKeyBindDown;
  44. _context.OnContextClosed += Close;
  45. _verbSystem.OnVerbsResponse += HandleVerbsResponse;
  46. }
  47. public void OnStateExited(GameplayState state)
  48. {
  49. _context.OnContextKeyEvent -= OnKeyBindDown;
  50. _context.OnContextClosed -= Close;
  51. if (_verbSystem != null)
  52. _verbSystem.OnVerbsResponse -= HandleVerbsResponse;
  53. Close();
  54. }
  55. public void OnStateEntered(MappingState state)
  56. {
  57. _context.OnContextKeyEvent += OnKeyBindDown;
  58. _context.OnContextClosed += Close;
  59. _verbSystem.OnVerbsResponse += HandleVerbsResponse;
  60. }
  61. public void OnStateExited(MappingState state)
  62. {
  63. _context.OnContextKeyEvent -= OnKeyBindDown;
  64. _context.OnContextClosed -= Close;
  65. if (_verbSystem != null)
  66. _verbSystem.OnVerbsResponse -= HandleVerbsResponse;
  67. Close();
  68. }
  69. /// <summary>
  70. /// Open a verb menu and fill it with verbs applicable to the given target entity.
  71. /// </summary>
  72. /// <param name="target">Entity to get verbs on.</param>
  73. /// <param name="force">Used to force showing all verbs (mostly for admins).</param>
  74. /// <param name="popup">
  75. /// If this is not null, verbs will be placed into the given popup instead.
  76. /// </param>
  77. public void OpenVerbMenu(EntityUid target, bool force = false, ContextMenuPopup? popup=null)
  78. {
  79. DebugTools.Assert(target.IsValid());
  80. OpenVerbMenu(EntityManager.GetNetEntity(target), force, popup);
  81. }
  82. /// <summary>
  83. /// Open a verb menu and fill it with verbs applicable to the given target entity.
  84. /// </summary>
  85. /// <param name="target">Entity to get verbs on.</param>
  86. /// <param name="force">Used to force showing all verbs. Only works on networked entities if the user is an admin.</param>
  87. /// <param name="popup">
  88. /// If this is not null, verbs will be placed into the given popup instead.
  89. /// </param>
  90. public void OpenVerbMenu(NetEntity target, bool force = false, ContextMenuPopup? popup=null)
  91. {
  92. DebugTools.Assert(target.IsValid());
  93. if (_playerManager.LocalEntity is not {Valid: true} user)
  94. return;
  95. if (!force && _combatMode.IsInCombatMode(user))
  96. return;
  97. Close();
  98. var menu = popup ?? _context.RootMenu;
  99. menu.MenuBody.DisposeAllChildren();
  100. CurrentTarget = target;
  101. CurrentVerbs = _verbSystem.GetVerbs(target, user, Verb.VerbTypes, out ExtraCategories, force);
  102. OpenMenu = menu;
  103. // Fill in client-side verbs.
  104. FillVerbPopup(menu);
  105. // if popup isn't null (ie we are opening out of an entity menu element),
  106. // assume that that is going to handle opening the submenu properly
  107. if (popup != null)
  108. return;
  109. // Show the menu at mouse pos
  110. menu.SetPositionLast();
  111. var box = UIBox2.FromDimensions(UIManager.MousePositionScaled.Position, new Vector2(1, 1));
  112. menu.Open(box);
  113. }
  114. /// <summary>
  115. /// Fill the verb pop-up using the verbs stored in <see cref="CurrentVerbs"/>
  116. /// </summary>
  117. private void FillVerbPopup(ContextMenuPopup popup)
  118. {
  119. HashSet<string> listedCategories = new();
  120. var extras = new ValueList<string>(ExtraCategories.Count);
  121. foreach (var cat in ExtraCategories)
  122. {
  123. extras.Add(cat.Text);
  124. }
  125. foreach (var verb in CurrentVerbs)
  126. {
  127. if (verb.Category == null)
  128. {
  129. var element = new VerbMenuElement(verb);
  130. _context.AddElement(popup, element);
  131. }
  132. // Add the category if it's not an extra (this is to avoid shuffling if we're filling from server verbs response).
  133. else if (!extras.Contains(verb.Category.Text) && listedCategories.Add(verb.Category.Text))
  134. AddVerbCategory(verb.Category, popup);
  135. }
  136. foreach (var category in ExtraCategories)
  137. {
  138. if (listedCategories.Add(category.Text))
  139. AddVerbCategory(category, popup);
  140. }
  141. popup.InvalidateMeasure();
  142. }
  143. /// <summary>
  144. /// Add a verb category button to the pop-up
  145. /// </summary>
  146. public void AddVerbCategory(VerbCategory category, ContextMenuPopup popup)
  147. {
  148. // Get a list of the verbs in this category
  149. List<Verb> verbsInCategory = new();
  150. var drawIcons = false;
  151. foreach (var verb in CurrentVerbs)
  152. {
  153. if (verb.Category?.Text == category.Text)
  154. {
  155. verbsInCategory.Add(verb);
  156. drawIcons = drawIcons || verb.Icon != null || verb.IconEntity != null;
  157. }
  158. }
  159. if (verbsInCategory.Count == 0 && !ExtraCategories.Contains(category))
  160. return;
  161. var style = verbsInCategory.FirstOrDefault()?.TextStyleClass ?? Verb.DefaultTextStyleClass;
  162. var element = new VerbMenuElement(category, style);
  163. _context.AddElement(popup, element);
  164. // Create the pop-up that appears when hovering over this element
  165. element.SubMenu = new ContextMenuPopup(_context, element);
  166. foreach (var verb in verbsInCategory)
  167. {
  168. var subElement = new VerbMenuElement(verb)
  169. {
  170. IconVisible = drawIcons,
  171. TextVisible = !category.IconsOnly
  172. };
  173. _context.AddElement(element.SubMenu, subElement);
  174. }
  175. element.SubMenu.MenuBody.Columns = category.Columns;
  176. }
  177. /// <summary>
  178. /// Add verbs from the server to <see cref="CurrentVerbs"/> and update the verb menu.
  179. /// </summary>
  180. public void AddServerVerbs(List<Verb>? verbs, ContextMenuPopup popup)
  181. {
  182. popup.MenuBody.DisposeAllChildren();
  183. // Verbs may be null if the server does not think we can see the target entity. This **should** not happen.
  184. if (verbs == null)
  185. {
  186. // remove "waiting for server..." and inform user that something went wrong.
  187. _context.AddElement(popup, new ContextMenuElement(Loc.GetString("verb-system-null-server-response")));
  188. return;
  189. }
  190. CurrentVerbs.UnionWith(verbs);
  191. FillVerbPopup(popup);
  192. }
  193. public void OnKeyBindDown(ContextMenuElement element, GUIBoundKeyEventArgs args)
  194. {
  195. if (args.Function != EngineKeyFunctions.Use && args.Function != ContentKeyFunctions.ActivateItemInWorld)
  196. return;
  197. if (element is not VerbMenuElement verbElement)
  198. {
  199. if (element is not ConfirmationMenuElement confElement)
  200. return;
  201. args.Handle();
  202. ExecuteVerb(confElement.Verb);
  203. return;
  204. }
  205. args.Handle();
  206. var verb = verbElement.Verb;
  207. if (verb == null)
  208. {
  209. // The user probably clicked on a verb category.
  210. // If there's only one verb in the category, then it will act as if they clicked on that verb.
  211. // Otherwise it opens the category menu.
  212. if (verbElement.SubMenu == null || verbElement.SubMenu.ChildCount == 0)
  213. return;
  214. if (verbElement.SubMenu.MenuBody.ChildCount != 1
  215. || verbElement.SubMenu.MenuBody.Children.First() is not VerbMenuElement verbMenuElement)
  216. {
  217. _context.OpenSubMenu(verbElement);
  218. return;
  219. }
  220. verb = verbMenuElement.Verb;
  221. if (verb == null)
  222. return;
  223. }
  224. #if DEBUG
  225. // No confirmation pop-ups in debug mode.
  226. ExecuteVerb(verb);
  227. #else
  228. if (!verb.ConfirmationPopup)
  229. {
  230. ExecuteVerb(verb);
  231. return;
  232. }
  233. if (verbElement.SubMenu == null)
  234. {
  235. var popupElement = new ConfirmationMenuElement(verb, "Confirm");
  236. verbElement.SubMenu = new ContextMenuPopup(_context, verbElement);
  237. _context.AddElement(verbElement.SubMenu, popupElement);
  238. }
  239. _context.OpenSubMenu(verbElement);
  240. #endif
  241. }
  242. private void Close()
  243. {
  244. if (OpenMenu == null)
  245. return;
  246. OpenMenu.Close();
  247. OpenMenu = null;
  248. }
  249. private void HandleVerbsResponse(VerbsResponseEvent msg)
  250. {
  251. if (OpenMenu == null || !OpenMenu.Visible || CurrentTarget != msg.Entity)
  252. return;
  253. AddServerVerbs(msg.Verbs, OpenMenu);
  254. }
  255. private void ExecuteVerb(Verb verb)
  256. {
  257. UIManager.ClickSound();
  258. _verbSystem.ExecuteVerb(CurrentTarget, verb);
  259. if (verb.CloseMenu ?? verb.CloseMenuDefault)
  260. _context.Close();
  261. }
  262. }
  263. }