1
0

ActionUIController.cs 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993
  1. using System.Linq;
  2. using System.Numerics;
  3. using Content.Client.Actions;
  4. using Content.Client.Construction;
  5. using Content.Client.Gameplay;
  6. using Content.Client.Hands;
  7. using Content.Client.Interaction;
  8. using Content.Client.Outline;
  9. using Content.Client.UserInterface.Controls;
  10. using Content.Client.UserInterface.Systems.Actions.Controls;
  11. using Content.Client.UserInterface.Systems.Actions.Widgets;
  12. using Content.Client.UserInterface.Systems.Actions.Windows;
  13. using Content.Client.UserInterface.Systems.Gameplay;
  14. using Content.Shared.Actions;
  15. using Content.Shared.Input;
  16. using Robust.Client.GameObjects;
  17. using Robust.Client.Graphics;
  18. using Robust.Client.Input;
  19. using Robust.Client.Player;
  20. using Robust.Client.UserInterface;
  21. using Robust.Client.UserInterface.Controllers;
  22. using Robust.Client.UserInterface.Controls;
  23. using Robust.Shared.Graphics.RSI;
  24. using Robust.Shared.Input;
  25. using Robust.Shared.Input.Binding;
  26. using Robust.Shared.Timing;
  27. using Robust.Shared.Utility;
  28. using static Content.Client.Actions.ActionsSystem;
  29. using static Content.Client.UserInterface.Systems.Actions.Windows.ActionsWindow;
  30. using static Robust.Client.UserInterface.Control;
  31. using static Robust.Client.UserInterface.Controls.BaseButton;
  32. using static Robust.Client.UserInterface.Controls.LineEdit;
  33. using static Robust.Client.UserInterface.Controls.MultiselectOptionButton<
  34. Content.Client.UserInterface.Systems.Actions.Windows.ActionsWindow.Filters>;
  35. using static Robust.Client.UserInterface.Controls.TextureRect;
  36. using static Robust.Shared.Input.Binding.PointerInputCmdHandler;
  37. namespace Content.Client.UserInterface.Systems.Actions;
  38. public sealed class ActionUIController : UIController, IOnStateChanged<GameplayState>, IOnSystemChanged<ActionsSystem>
  39. {
  40. [Dependency] private readonly IOverlayManager _overlays = default!;
  41. [Dependency] private readonly IGameTiming _timing = default!;
  42. [Dependency] private readonly IPlayerManager _playerManager = default!;
  43. [Dependency] private readonly IEntityManager _entMan = default!;
  44. [Dependency] private readonly IInputManager _input = default!;
  45. [UISystemDependency] private readonly ActionsSystem? _actionsSystem = default;
  46. [UISystemDependency] private readonly InteractionOutlineSystem? _interactionOutline = default;
  47. [UISystemDependency] private readonly TargetOutlineSystem? _targetOutline = default;
  48. [UISystemDependency] private readonly SpriteSystem _spriteSystem = default!;
  49. private ActionButtonContainer? _container;
  50. private readonly List<EntityUid?> _actions = new();
  51. private readonly DragDropHelper<ActionButton> _menuDragHelper;
  52. private readonly TextureRect _dragShadow;
  53. private ActionsWindow? _window;
  54. private ActionsBar? ActionsBar => UIManager.GetActiveUIWidgetOrNull<ActionsBar>();
  55. private MenuButton? ActionButton => UIManager.GetActiveUIWidgetOrNull<MenuBar.Widgets.GameTopMenuBar>()?.ActionButton;
  56. public bool IsDragging => _menuDragHelper.IsDragging;
  57. /// <summary>
  58. /// Action slot we are currently selecting a target for.
  59. /// </summary>
  60. public EntityUid? SelectingTargetFor { get; private set; }
  61. public ActionUIController()
  62. {
  63. _menuDragHelper = new DragDropHelper<ActionButton>(OnMenuBeginDrag, OnMenuContinueDrag, OnMenuEndDrag);
  64. _dragShadow = new TextureRect
  65. {
  66. MinSize = new Vector2(64, 64),
  67. Stretch = StretchMode.Scale,
  68. Visible = false,
  69. SetSize = new Vector2(64, 64),
  70. MouseFilter = MouseFilterMode.Ignore
  71. };
  72. }
  73. public override void Initialize()
  74. {
  75. base.Initialize();
  76. var gameplayStateLoad = UIManager.GetUIController<GameplayStateLoadController>();
  77. gameplayStateLoad.OnScreenLoad += OnScreenLoad;
  78. gameplayStateLoad.OnScreenUnload += OnScreenUnload;
  79. }
  80. private void OnScreenLoad()
  81. {
  82. LoadGui();
  83. }
  84. private void OnScreenUnload()
  85. {
  86. UnloadGui();
  87. }
  88. public void OnStateEntered(GameplayState state)
  89. {
  90. if (_actionsSystem != null)
  91. {
  92. _actionsSystem.OnActionAdded += OnActionAdded;
  93. _actionsSystem.OnActionRemoved += OnActionRemoved;
  94. _actionsSystem.ActionsUpdated += OnActionsUpdated;
  95. }
  96. UpdateFilterLabel();
  97. QueueWindowUpdate();
  98. _dragShadow.Orphan();
  99. UIManager.PopupRoot.AddChild(_dragShadow);
  100. var builder = CommandBinds.Builder;
  101. var hotbarKeys = ContentKeyFunctions.GetHotbarBoundKeys();
  102. for (var i = 0; i < hotbarKeys.Length; i++)
  103. {
  104. var boundId = i; // This is needed, because the lambda captures it.
  105. var boundKey = hotbarKeys[i];
  106. builder = builder.Bind(boundKey, new PointerInputCmdHandler((in PointerInputCmdArgs args) =>
  107. {
  108. if (args.State != BoundKeyState.Down)
  109. return false;
  110. TriggerAction(boundId);
  111. return true;
  112. }, false, true));
  113. }
  114. builder
  115. .Bind(ContentKeyFunctions.OpenActionsMenu,
  116. InputCmdHandler.FromDelegate(_ => ToggleWindow()))
  117. .BindBefore(EngineKeyFunctions.Use, new PointerInputCmdHandler(TargetingOnUse, outsidePrediction: true),
  118. typeof(ConstructionSystem), typeof(DragDropSystem))
  119. .BindBefore(EngineKeyFunctions.UIRightClick, new PointerInputCmdHandler(TargetingCancel, outsidePrediction: true))
  120. .Register<ActionUIController>();
  121. }
  122. private bool TargetingCancel(in PointerInputCmdArgs args)
  123. {
  124. if (!_timing.IsFirstTimePredicted)
  125. return false;
  126. // only do something for actual target-based actions
  127. if (SelectingTargetFor == null)
  128. return false;
  129. StopTargeting();
  130. return true;
  131. }
  132. /// <summary>
  133. /// If the user clicked somewhere, and they are currently targeting an action, try and perform it.
  134. /// </summary>
  135. private bool TargetingOnUse(in PointerInputCmdArgs args)
  136. {
  137. if (!_timing.IsFirstTimePredicted || _actionsSystem == null || SelectingTargetFor is not { } actionId)
  138. return false;
  139. if (_playerManager.LocalEntity is not { } user)
  140. return false;
  141. if (!EntityManager.TryGetComponent(user, out ActionsComponent? comp))
  142. return false;
  143. if (!_actionsSystem.TryGetActionData(actionId, out var baseAction) ||
  144. baseAction is not BaseTargetActionComponent action)
  145. {
  146. return false;
  147. }
  148. // Is the action currently valid?
  149. if (!action.Enabled
  150. || action is { Charges: 0, RenewCharges: false }
  151. || action.Cooldown.HasValue && action.Cooldown.Value.End > _timing.CurTime)
  152. {
  153. // The user is targeting with this action, but it is not valid. Maybe mark this click as
  154. // handled and prevent further interactions.
  155. return !action.InteractOnMiss;
  156. }
  157. switch (action)
  158. {
  159. case WorldTargetActionComponent mapTarget:
  160. return TryTargetWorld(args, actionId, mapTarget, user, comp) || !mapTarget.InteractOnMiss;
  161. case EntityTargetActionComponent entTarget:
  162. return TryTargetEntity(args, actionId, entTarget, user, comp) || !entTarget.InteractOnMiss;
  163. case EntityWorldTargetActionComponent entMapTarget:
  164. return TryTargetEntityWorld(args, actionId, entMapTarget, user, comp) || !entMapTarget.InteractOnMiss;
  165. default:
  166. Logger.Error($"Unknown targeting action: {actionId.GetType()}");
  167. return false;
  168. }
  169. }
  170. private bool TryTargetWorld(in PointerInputCmdArgs args, EntityUid actionId, WorldTargetActionComponent action, EntityUid user, ActionsComponent actionComp)
  171. {
  172. if (_actionsSystem == null)
  173. return false;
  174. var coords = args.Coordinates;
  175. if (!_actionsSystem.ValidateWorldTarget(user, coords, (actionId, action)))
  176. {
  177. // Invalid target.
  178. if (action.DeselectOnMiss)
  179. StopTargeting();
  180. return false;
  181. }
  182. if (action.ClientExclusive)
  183. {
  184. if (action.Event != null)
  185. {
  186. action.Event.Target = coords;
  187. }
  188. _actionsSystem.PerformAction(user, actionComp, actionId, action, action.Event, _timing.CurTime);
  189. }
  190. else
  191. EntityManager.RaisePredictiveEvent(new RequestPerformActionEvent(EntityManager.GetNetEntity(actionId), EntityManager.GetNetCoordinates(coords)));
  192. if (!action.Repeat)
  193. StopTargeting();
  194. return true;
  195. }
  196. private bool TryTargetEntity(in PointerInputCmdArgs args, EntityUid actionId, EntityTargetActionComponent action, EntityUid user, ActionsComponent actionComp)
  197. {
  198. if (_actionsSystem == null)
  199. return false;
  200. var entity = args.EntityUid;
  201. if (!_actionsSystem.ValidateEntityTarget(user, entity, (actionId, action)))
  202. {
  203. if (action.DeselectOnMiss)
  204. StopTargeting();
  205. return false;
  206. }
  207. if (action.ClientExclusive)
  208. {
  209. if (action.Event != null)
  210. {
  211. action.Event.Target = entity;
  212. }
  213. _actionsSystem.PerformAction(user, actionComp, actionId, action, action.Event, _timing.CurTime);
  214. }
  215. else
  216. EntityManager.RaisePredictiveEvent(new RequestPerformActionEvent(EntityManager.GetNetEntity(actionId), EntityManager.GetNetEntity(args.EntityUid)));
  217. if (!action.Repeat)
  218. StopTargeting();
  219. return true;
  220. }
  221. private bool TryTargetEntityWorld(in PointerInputCmdArgs args,
  222. EntityUid actionId,
  223. EntityWorldTargetActionComponent action,
  224. EntityUid user,
  225. ActionsComponent actionComp)
  226. {
  227. if (_actionsSystem == null)
  228. return false;
  229. var entity = args.EntityUid;
  230. var coords = args.Coordinates;
  231. if (!_actionsSystem.ValidateEntityWorldTarget(user, entity, coords, (actionId, action)))
  232. {
  233. if (action.DeselectOnMiss)
  234. StopTargeting();
  235. return false;
  236. }
  237. if (action.ClientExclusive)
  238. {
  239. if (action.Event != null)
  240. {
  241. action.Event.Entity = entity;
  242. action.Event.Coords = coords;
  243. }
  244. _actionsSystem.PerformAction(user, actionComp, actionId, action, action.Event, _timing.CurTime);
  245. }
  246. else
  247. EntityManager.RaisePredictiveEvent(new RequestPerformActionEvent(EntityManager.GetNetEntity(actionId), EntityManager.GetNetEntity(args.EntityUid), EntityManager.GetNetCoordinates(coords)));
  248. if (!action.Repeat)
  249. StopTargeting();
  250. return true;
  251. }
  252. public void UnloadButton()
  253. {
  254. if (ActionButton == null)
  255. {
  256. return;
  257. }
  258. ActionButton.OnPressed -= ActionButtonPressed;
  259. }
  260. public void LoadButton()
  261. {
  262. if (ActionButton == null)
  263. {
  264. return;
  265. }
  266. ActionButton.OnPressed += ActionButtonPressed;
  267. }
  268. private void OnWindowOpened()
  269. {
  270. if (ActionButton != null)
  271. ActionButton.SetClickPressed(true);
  272. SearchAndDisplay();
  273. }
  274. private void OnWindowClosed()
  275. {
  276. if (ActionButton != null)
  277. ActionButton.SetClickPressed(false);
  278. }
  279. public void OnStateExited(GameplayState state)
  280. {
  281. if (_actionsSystem != null)
  282. {
  283. _actionsSystem.OnActionAdded -= OnActionAdded;
  284. _actionsSystem.OnActionRemoved -= OnActionRemoved;
  285. _actionsSystem.ActionsUpdated -= OnActionsUpdated;
  286. }
  287. CommandBinds.Unregister<ActionUIController>();
  288. }
  289. private void TriggerAction(int index)
  290. {
  291. if (_actionsSystem == null ||
  292. !_actions.TryGetValue(index, out var actionId) ||
  293. !_actionsSystem.TryGetActionData(actionId, out var baseAction))
  294. {
  295. return;
  296. }
  297. if (baseAction is BaseTargetActionComponent action)
  298. ToggleTargeting(actionId.Value, action);
  299. else
  300. _actionsSystem?.TriggerAction(actionId.Value, baseAction);
  301. }
  302. private void OnActionAdded(EntityUid actionId)
  303. {
  304. if (_actionsSystem == null ||
  305. !_actionsSystem.TryGetActionData(actionId, out var action))
  306. {
  307. return;
  308. }
  309. // if the action is toggled when we add it, start targeting
  310. if (action is BaseTargetActionComponent targetAction && action.Toggled)
  311. StartTargeting(actionId, targetAction);
  312. if (_actions.Contains(actionId))
  313. return;
  314. _actions.Add(actionId);
  315. }
  316. private void OnActionRemoved(EntityUid actionId)
  317. {
  318. if (_container == null)
  319. return;
  320. if (actionId == SelectingTargetFor)
  321. StopTargeting();
  322. _actions.RemoveAll(x => x == actionId);
  323. }
  324. private void OnActionsUpdated()
  325. {
  326. QueueWindowUpdate();
  327. if (_actionsSystem != null)
  328. _container?.SetActionData(_actionsSystem, _actions.ToArray());
  329. }
  330. private void ActionButtonPressed(ButtonEventArgs args)
  331. {
  332. ToggleWindow();
  333. }
  334. private void ToggleWindow()
  335. {
  336. if (_window == null)
  337. return;
  338. if (_window.IsOpen)
  339. {
  340. _window.Close();
  341. return;
  342. }
  343. _window.Open();
  344. }
  345. private void UpdateFilterLabel()
  346. {
  347. if (_window == null)
  348. return;
  349. if (_window.FilterButton.SelectedKeys.Count == 0)
  350. {
  351. _window.FilterLabel.Visible = false;
  352. }
  353. else
  354. {
  355. _window.FilterLabel.Visible = true;
  356. _window.FilterLabel.Text = Loc.GetString("ui-actionmenu-filter-label",
  357. ("selectedLabels", string.Join(", ", _window.FilterButton.SelectedLabels)));
  358. }
  359. }
  360. private bool MatchesFilter(BaseActionComponent action, Filters filter)
  361. {
  362. return filter switch
  363. {
  364. Filters.Enabled => action.Enabled,
  365. Filters.Item => action.Container != null && action.Container != _playerManager.LocalEntity,
  366. Filters.Innate => action.Container == null || action.Container == _playerManager.LocalEntity,
  367. Filters.Instant => action is InstantActionComponent,
  368. Filters.Targeted => action is BaseTargetActionComponent,
  369. _ => throw new ArgumentOutOfRangeException(nameof(filter), filter, null)
  370. };
  371. }
  372. private void ClearList()
  373. {
  374. if (_window?.Disposed == false)
  375. _window.ResultsGrid.RemoveAllChildren();
  376. }
  377. private void PopulateActions(IEnumerable<(EntityUid Id, BaseActionComponent Comp)> actions)
  378. {
  379. if (_window is not { Disposed: false, IsOpen: true })
  380. return;
  381. if (_actionsSystem == null)
  382. return;
  383. _window.UpdateNeeded = false;
  384. List<ActionButton> existing = new(_window.ResultsGrid.ChildCount);
  385. foreach (var child in _window.ResultsGrid.Children)
  386. {
  387. if (child is ActionButton button)
  388. existing.Add(button);
  389. }
  390. int i = 0;
  391. foreach (var action in actions)
  392. {
  393. if (i < existing.Count)
  394. {
  395. existing[i++].UpdateData(action.Id, _actionsSystem);
  396. continue;
  397. }
  398. var button = new ActionButton(_entMan, _spriteSystem, this) {Locked = true};
  399. button.ActionPressed += OnWindowActionPressed;
  400. button.ActionUnpressed += OnWindowActionUnPressed;
  401. button.ActionFocusExited += OnWindowActionFocusExisted;
  402. button.UpdateData(action.Id, _actionsSystem);
  403. _window.ResultsGrid.AddChild(button);
  404. }
  405. for (; i < existing.Count; i++)
  406. {
  407. existing[i].Dispose();
  408. }
  409. }
  410. public void QueueWindowUpdate()
  411. {
  412. if (_window != null)
  413. _window.UpdateNeeded = true;
  414. }
  415. private void SearchAndDisplay()
  416. {
  417. if (_window is not { Disposed: false, IsOpen: true })
  418. return;
  419. if (_actionsSystem == null)
  420. return;
  421. if (_playerManager.LocalEntity is not { } player)
  422. return;
  423. var search = _window.SearchBar.Text;
  424. var filters = _window.FilterButton.SelectedKeys;
  425. var actions = _actionsSystem.GetClientActions();
  426. if (filters.Count == 0 && string.IsNullOrWhiteSpace(search))
  427. {
  428. PopulateActions(actions);
  429. return;
  430. }
  431. actions = actions.Where(action =>
  432. {
  433. if (filters.Count > 0 && filters.Any(filter => !MatchesFilter(action.Comp, filter)))
  434. return false;
  435. if (action.Comp.Keywords.Any(keyword => search.Contains(keyword, StringComparison.OrdinalIgnoreCase)))
  436. return true;
  437. var name = EntityManager.GetComponent<MetaDataComponent>(action.Id).EntityName;
  438. if (name.Contains(search, StringComparison.OrdinalIgnoreCase))
  439. return true;
  440. if (action.Comp.Container == null || action.Comp.Container == player)
  441. return false;
  442. var providerName = EntityManager.GetComponent<MetaDataComponent>(action.Comp.Container.Value).EntityName;
  443. return providerName.Contains(search, StringComparison.OrdinalIgnoreCase);
  444. });
  445. PopulateActions(actions);
  446. }
  447. private void SetAction(ActionButton button, EntityUid? actionId, bool updateSlots = true)
  448. {
  449. if (_actionsSystem == null)
  450. return;
  451. int position;
  452. if (actionId == null)
  453. {
  454. button.ClearData();
  455. if (_container?.TryGetButtonIndex(button, out position) ?? false)
  456. {
  457. if (_actions.Count > position && position >= 0)
  458. _actions.RemoveAt(position);
  459. }
  460. }
  461. else if (button.TryReplaceWith(actionId.Value, _actionsSystem) &&
  462. _container != null &&
  463. _container.TryGetButtonIndex(button, out position))
  464. {
  465. if (position >= _actions.Count)
  466. {
  467. _actions.Add(actionId);
  468. }
  469. else
  470. {
  471. _actions[position] = actionId;
  472. }
  473. }
  474. if (updateSlots)
  475. _container?.SetActionData(_actionsSystem, _actions.ToArray());
  476. }
  477. private void DragAction()
  478. {
  479. if (_menuDragHelper.Dragged is not {ActionId: {} action} dragged)
  480. {
  481. _menuDragHelper.EndDrag();
  482. return;
  483. }
  484. EntityUid? swapAction = null;
  485. var currentlyHovered = UIManager.MouseGetControl(_input.MouseScreenPosition);
  486. if (currentlyHovered is ActionButton button)
  487. {
  488. swapAction = button.ActionId;
  489. SetAction(button, action, false);
  490. }
  491. if (dragged.Parent is ActionButtonContainer)
  492. SetAction(dragged, swapAction, false);
  493. if (_actionsSystem != null)
  494. _container?.SetActionData(_actionsSystem, _actions.ToArray());
  495. _menuDragHelper.EndDrag();
  496. }
  497. private void OnClearPressed(ButtonEventArgs args)
  498. {
  499. if (_window == null)
  500. return;
  501. _window.SearchBar.Clear();
  502. _window.FilterButton.DeselectAll();
  503. UpdateFilterLabel();
  504. QueueWindowUpdate();
  505. }
  506. private void OnSearchChanged(LineEditEventArgs args)
  507. {
  508. QueueWindowUpdate();
  509. }
  510. private void OnFilterSelected(ItemPressedEventArgs args)
  511. {
  512. UpdateFilterLabel();
  513. QueueWindowUpdate();
  514. }
  515. private void OnWindowActionPressed(GUIBoundKeyEventArgs args, ActionButton action)
  516. {
  517. if (args.Function != EngineKeyFunctions.UIClick && args.Function != EngineKeyFunctions.Use)
  518. return;
  519. _menuDragHelper.MouseDown(action);
  520. args.Handle();
  521. }
  522. private void OnWindowActionUnPressed(GUIBoundKeyEventArgs args, ActionButton dragged)
  523. {
  524. if (args.Function != EngineKeyFunctions.UIClick && args.Function != EngineKeyFunctions.Use)
  525. return;
  526. DragAction();
  527. args.Handle();
  528. }
  529. private void OnWindowActionFocusExisted(ActionButton button)
  530. {
  531. _menuDragHelper.EndDrag();
  532. }
  533. private void OnActionPressed(GUIBoundKeyEventArgs args, ActionButton button)
  534. {
  535. if (args.Function == EngineKeyFunctions.UIRightClick)
  536. {
  537. SetAction(button, null);
  538. args.Handle();
  539. return;
  540. }
  541. if (args.Function != EngineKeyFunctions.UIClick)
  542. return;
  543. args.Handle();
  544. if (button.ActionId != null)
  545. {
  546. _menuDragHelper.MouseDown(button);
  547. return;
  548. }
  549. var ev = new FillActionSlotEvent();
  550. EntityManager.EventBus.RaiseEvent(EventSource.Local, ev);
  551. if (ev.Action != null)
  552. SetAction(button, ev.Action);
  553. }
  554. private void OnActionUnpressed(GUIBoundKeyEventArgs args, ActionButton button)
  555. {
  556. if (args.Function != EngineKeyFunctions.UIClick || _actionsSystem == null)
  557. return;
  558. args.Handle();
  559. if (_menuDragHelper.IsDragging)
  560. {
  561. DragAction();
  562. return;
  563. }
  564. _menuDragHelper.EndDrag();
  565. if (!_actionsSystem.TryGetActionData(button.ActionId, out var baseAction))
  566. return;
  567. if (baseAction is not BaseTargetActionComponent action)
  568. {
  569. _actionsSystem?.TriggerAction(button.ActionId.Value, baseAction);
  570. return;
  571. }
  572. // for target actions, we go into "select target" mode, we don't
  573. // message the server until we actually pick our target.
  574. // if we're clicking the same thing we're already targeting for, then we simply cancel
  575. // targeting
  576. ToggleTargeting(button.ActionId.Value, action);
  577. }
  578. private bool OnMenuBeginDrag()
  579. {
  580. // TODO ACTIONS
  581. // The dragging icon shuld be based on the entity's icon style. I.e. if the action has a large icon texture,
  582. // and a small item/provider sprite, then the dragged icon should be the big texture, not the provider.
  583. if (_actionsSystem != null && _actionsSystem.TryGetActionData(_menuDragHelper.Dragged?.ActionId, out var action))
  584. {
  585. if (EntityManager.TryGetComponent(action.EntityIcon, out SpriteComponent? sprite)
  586. && sprite.Icon?.GetFrame(RsiDirection.South, 0) is {} frame)
  587. {
  588. _dragShadow.Texture = frame;
  589. }
  590. else if (action.Icon != null)
  591. {
  592. _dragShadow.Texture = _spriteSystem.Frame0(action.Icon);
  593. }
  594. else
  595. {
  596. _dragShadow.Texture = null;
  597. }
  598. }
  599. LayoutContainer.SetPosition(_dragShadow, UIManager.MousePositionScaled.Position - new Vector2(32, 32));
  600. return true;
  601. }
  602. private bool OnMenuContinueDrag(float frameTime)
  603. {
  604. LayoutContainer.SetPosition(_dragShadow, UIManager.MousePositionScaled.Position - new Vector2(32, 32));
  605. _dragShadow.Visible = true;
  606. return true;
  607. }
  608. private void OnMenuEndDrag()
  609. {
  610. _dragShadow.Texture = null;
  611. _dragShadow.Visible = false;
  612. }
  613. private void UnloadGui()
  614. {
  615. _actionsSystem?.UnlinkAllActions();
  616. if (ActionsBar == null)
  617. {
  618. return;
  619. }
  620. if (_window != null)
  621. {
  622. _window.OnOpen -= OnWindowOpened;
  623. _window.OnClose -= OnWindowClosed;
  624. _window.ClearButton.OnPressed -= OnClearPressed;
  625. _window.SearchBar.OnTextChanged -= OnSearchChanged;
  626. _window.FilterButton.OnItemSelected -= OnFilterSelected;
  627. _window.Dispose();
  628. _window = null;
  629. }
  630. }
  631. private void LoadGui()
  632. {
  633. UnloadGui();
  634. _window = UIManager.CreateWindow<ActionsWindow>();
  635. LayoutContainer.SetAnchorPreset(_window, LayoutContainer.LayoutPreset.CenterTop);
  636. _window.OnOpen += OnWindowOpened;
  637. _window.OnClose += OnWindowClosed;
  638. _window.ClearButton.OnPressed += OnClearPressed;
  639. _window.SearchBar.OnTextChanged += OnSearchChanged;
  640. _window.FilterButton.OnItemSelected += OnFilterSelected;
  641. if (ActionsBar == null)
  642. {
  643. return;
  644. }
  645. RegisterActionContainer(ActionsBar.ActionsContainer);
  646. _actionsSystem?.LinkAllActions();
  647. }
  648. public void RegisterActionContainer(ActionButtonContainer container)
  649. {
  650. if (_container != null)
  651. {
  652. _container.ActionPressed -= OnActionPressed;
  653. _container.ActionUnpressed -= OnActionUnpressed;
  654. }
  655. _container = container;
  656. _container.ActionPressed += OnActionPressed;
  657. _container.ActionUnpressed += OnActionUnpressed;
  658. }
  659. private void ClearActions()
  660. {
  661. _container?.ClearActionData();
  662. }
  663. private void AssignSlots(List<SlotAssignment> assignments)
  664. {
  665. if (_actionsSystem == null)
  666. return;
  667. _actions.Clear();
  668. foreach (var assign in assignments)
  669. {
  670. _actions.Add(assign.ActionId);
  671. }
  672. _container?.SetActionData(_actionsSystem, _actions.ToArray());
  673. }
  674. public void RemoveActionContainer()
  675. {
  676. _container = null;
  677. }
  678. public void OnSystemLoaded(ActionsSystem system)
  679. {
  680. system.LinkActions += OnComponentLinked;
  681. system.UnlinkActions += OnComponentUnlinked;
  682. system.ClearAssignments += ClearActions;
  683. system.AssignSlot += AssignSlots;
  684. }
  685. public void OnSystemUnloaded(ActionsSystem system)
  686. {
  687. system.LinkActions -= OnComponentLinked;
  688. system.UnlinkActions -= OnComponentUnlinked;
  689. system.ClearAssignments -= ClearActions;
  690. system.AssignSlot -= AssignSlots;
  691. }
  692. public override void FrameUpdate(FrameEventArgs args)
  693. {
  694. _menuDragHelper.Update(args.DeltaSeconds);
  695. if (_window is {UpdateNeeded: true})
  696. SearchAndDisplay();
  697. }
  698. private void OnComponentLinked(ActionsComponent component)
  699. {
  700. if (_actionsSystem == null)
  701. return;
  702. LoadDefaultActions();
  703. _container?.SetActionData(_actionsSystem, _actions.ToArray());
  704. QueueWindowUpdate();
  705. }
  706. private void OnComponentUnlinked()
  707. {
  708. _container?.ClearActionData();
  709. QueueWindowUpdate();
  710. StopTargeting();
  711. }
  712. private void LoadDefaultActions()
  713. {
  714. if (_actionsSystem == null)
  715. return;
  716. var actions = _actionsSystem.GetClientActions().Where(action => action.Comp.AutoPopulate).ToList();
  717. actions.Sort(ActionComparer);
  718. _actions.Clear();
  719. foreach (var (action, _) in actions)
  720. {
  721. if (!_actions.Contains(action))
  722. _actions.Add(action);
  723. }
  724. }
  725. /// <summary>
  726. /// If currently targeting with this slot, stops targeting.
  727. /// If currently targeting with no slot or a different slot, switches to
  728. /// targeting with the specified slot.
  729. /// </summary>
  730. private void ToggleTargeting(EntityUid actionId, BaseTargetActionComponent action)
  731. {
  732. if (SelectingTargetFor == actionId)
  733. {
  734. StopTargeting();
  735. return;
  736. }
  737. StartTargeting(actionId, action);
  738. }
  739. /// <summary>
  740. /// Puts us in targeting mode, where we need to pick either a target point or entity
  741. /// </summary>
  742. private void StartTargeting(EntityUid actionId, BaseTargetActionComponent action)
  743. {
  744. // If we were targeting something else we should stop
  745. StopTargeting();
  746. SelectingTargetFor = actionId;
  747. // TODO inform the server
  748. action.Toggled = true;
  749. // override "held-item" overlay
  750. var provider = action.Container;
  751. if (action.TargetingIndicator && _overlays.TryGetOverlay<ShowHandItemOverlay>(out var handOverlay))
  752. {
  753. if (action.ItemIconStyle == ItemActionIconStyle.BigItem && action.Container != null)
  754. {
  755. handOverlay.EntityOverride = provider;
  756. }
  757. else if (action.Toggled && action.IconOn != null)
  758. handOverlay.IconOverride = _spriteSystem.Frame0(action.IconOn);
  759. else if (action.Icon != null)
  760. handOverlay.IconOverride = _spriteSystem.Frame0(action.Icon);
  761. }
  762. if (_container != null)
  763. {
  764. foreach (var button in _container.GetButtons())
  765. {
  766. if (button.ActionId == actionId)
  767. button.UpdateIcons();
  768. }
  769. }
  770. // TODO: allow world-targets to check valid positions. E.g., maybe:
  771. // - Draw a red/green ghost entity
  772. // - Add a yes/no checkmark where the HandItemOverlay usually is
  773. // Highlight valid entity targets
  774. if (action is not EntityTargetActionComponent entityAction)
  775. return;
  776. Func<EntityUid, bool>? predicate = null;
  777. var attachedEnt = entityAction.AttachedEntity;
  778. if (!entityAction.CanTargetSelf)
  779. predicate = e => e != attachedEnt;
  780. var range = entityAction.CheckCanAccess ? action.Range : -1;
  781. _interactionOutline?.SetEnabled(false);
  782. _targetOutline?.Enable(range, entityAction.CheckCanAccess, predicate, entityAction.Whitelist, entityAction.Blacklist, null);
  783. }
  784. /// <summary>
  785. /// Switch out of targeting mode if currently selecting target for an action
  786. /// </summary>
  787. private void StopTargeting()
  788. {
  789. if (SelectingTargetFor == null)
  790. return;
  791. var oldAction = SelectingTargetFor;
  792. if (_actionsSystem != null && _actionsSystem.TryGetActionData(oldAction, out var action))
  793. {
  794. // TODO inform the server
  795. action.Toggled = false;
  796. }
  797. SelectingTargetFor = null;
  798. _targetOutline?.Disable();
  799. _interactionOutline?.SetEnabled(true);
  800. if (_container != null)
  801. {
  802. foreach (var button in _container.GetButtons())
  803. {
  804. if (button.ActionId == oldAction)
  805. button.UpdateIcons();
  806. }
  807. }
  808. if (!_overlays.TryGetOverlay<ShowHandItemOverlay>(out var handOverlay))
  809. return;
  810. handOverlay.IconOverride = null;
  811. handOverlay.EntityOverride = null;
  812. }
  813. }