using System.Linq; using System.Numerics; using Content.Client.CombatMode; using Content.Client.ContextMenu.UI; using Content.Client.Gameplay; using Content.Client.Mapping; using Content.Shared.Input; using Content.Shared.Verbs; using Robust.Client.Player; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controllers; using Robust.Shared.Collections; using Robust.Shared.Input; using Robust.Shared.Utility; namespace Content.Client.Verbs.UI { /// /// This class handles the displaying of the verb menu. /// /// /// In addition to the normal functionality, this also provides functions /// open a verb menu for a given entity, add verbs to it, and add server-verbs when the server response is /// received. /// public sealed class VerbMenuUIController : UIController, IOnStateEntered, IOnStateExited, IOnStateEntered, IOnStateExited { [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly ContextMenuUIController _context = default!; [UISystemDependency] private readonly CombatModeSystem _combatMode = default!; [UISystemDependency] private readonly VerbSystem _verbSystem = default!; public NetEntity CurrentTarget; public SortedSet CurrentVerbs = new(); public List ExtraCategories = new(); /// /// Separate from , since we can open a verb menu as a submenu /// of an entity menu element. If that happens, we need to be aware and close it properly. /// public ContextMenuPopup? OpenMenu = null; public void OnStateEntered(GameplayState state) { _context.OnContextKeyEvent += OnKeyBindDown; _context.OnContextClosed += Close; _verbSystem.OnVerbsResponse += HandleVerbsResponse; } public void OnStateExited(GameplayState state) { _context.OnContextKeyEvent -= OnKeyBindDown; _context.OnContextClosed -= Close; if (_verbSystem != null) _verbSystem.OnVerbsResponse -= HandleVerbsResponse; Close(); } public void OnStateEntered(MappingState state) { _context.OnContextKeyEvent += OnKeyBindDown; _context.OnContextClosed += Close; _verbSystem.OnVerbsResponse += HandleVerbsResponse; } public void OnStateExited(MappingState state) { _context.OnContextKeyEvent -= OnKeyBindDown; _context.OnContextClosed -= Close; if (_verbSystem != null) _verbSystem.OnVerbsResponse -= HandleVerbsResponse; Close(); } /// /// Open a verb menu and fill it with verbs applicable to the given target entity. /// /// Entity to get verbs on. /// Used to force showing all verbs (mostly for admins). /// /// If this is not null, verbs will be placed into the given popup instead. /// public void OpenVerbMenu(EntityUid target, bool force = false, ContextMenuPopup? popup=null) { DebugTools.Assert(target.IsValid()); OpenVerbMenu(EntityManager.GetNetEntity(target), force, popup); } /// /// Open a verb menu and fill it with verbs applicable to the given target entity. /// /// Entity to get verbs on. /// Used to force showing all verbs. Only works on networked entities if the user is an admin. /// /// If this is not null, verbs will be placed into the given popup instead. /// public void OpenVerbMenu(NetEntity target, bool force = false, ContextMenuPopup? popup=null) { DebugTools.Assert(target.IsValid()); if (_playerManager.LocalEntity is not {Valid: true} user) return; if (!force && _combatMode.IsInCombatMode(user)) return; Close(); var menu = popup ?? _context.RootMenu; menu.MenuBody.DisposeAllChildren(); CurrentTarget = target; CurrentVerbs = _verbSystem.GetVerbs(target, user, Verb.VerbTypes, out ExtraCategories, force); OpenMenu = menu; // Fill in client-side verbs. FillVerbPopup(menu); // if popup isn't null (ie we are opening out of an entity menu element), // assume that that is going to handle opening the submenu properly if (popup != null) return; // Show the menu at mouse pos menu.SetPositionLast(); var box = UIBox2.FromDimensions(UIManager.MousePositionScaled.Position, new Vector2(1, 1)); menu.Open(box); } /// /// Fill the verb pop-up using the verbs stored in /// private void FillVerbPopup(ContextMenuPopup popup) { HashSet listedCategories = new(); var extras = new ValueList(ExtraCategories.Count); foreach (var cat in ExtraCategories) { extras.Add(cat.Text); } foreach (var verb in CurrentVerbs) { if (verb.Category == null) { var element = new VerbMenuElement(verb); _context.AddElement(popup, element); } // Add the category if it's not an extra (this is to avoid shuffling if we're filling from server verbs response). else if (!extras.Contains(verb.Category.Text) && listedCategories.Add(verb.Category.Text)) AddVerbCategory(verb.Category, popup); } foreach (var category in ExtraCategories) { if (listedCategories.Add(category.Text)) AddVerbCategory(category, popup); } popup.InvalidateMeasure(); } /// /// Add a verb category button to the pop-up /// public void AddVerbCategory(VerbCategory category, ContextMenuPopup popup) { // Get a list of the verbs in this category List verbsInCategory = new(); var drawIcons = false; foreach (var verb in CurrentVerbs) { if (verb.Category?.Text == category.Text) { verbsInCategory.Add(verb); drawIcons = drawIcons || verb.Icon != null || verb.IconEntity != null; } } if (verbsInCategory.Count == 0 && !ExtraCategories.Contains(category)) return; var style = verbsInCategory.FirstOrDefault()?.TextStyleClass ?? Verb.DefaultTextStyleClass; var element = new VerbMenuElement(category, style); _context.AddElement(popup, element); // Create the pop-up that appears when hovering over this element element.SubMenu = new ContextMenuPopup(_context, element); foreach (var verb in verbsInCategory) { var subElement = new VerbMenuElement(verb) { IconVisible = drawIcons, TextVisible = !category.IconsOnly }; _context.AddElement(element.SubMenu, subElement); } element.SubMenu.MenuBody.Columns = category.Columns; } /// /// Add verbs from the server to and update the verb menu. /// public void AddServerVerbs(List? verbs, ContextMenuPopup popup) { popup.MenuBody.DisposeAllChildren(); // Verbs may be null if the server does not think we can see the target entity. This **should** not happen. if (verbs == null) { // remove "waiting for server..." and inform user that something went wrong. _context.AddElement(popup, new ContextMenuElement(Loc.GetString("verb-system-null-server-response"))); return; } CurrentVerbs.UnionWith(verbs); FillVerbPopup(popup); } public void OnKeyBindDown(ContextMenuElement element, GUIBoundKeyEventArgs args) { if (args.Function != EngineKeyFunctions.Use && args.Function != ContentKeyFunctions.ActivateItemInWorld) return; if (element is not VerbMenuElement verbElement) { if (element is not ConfirmationMenuElement confElement) return; args.Handle(); ExecuteVerb(confElement.Verb); return; } args.Handle(); var verb = verbElement.Verb; if (verb == null) { // The user probably clicked on a verb category. // If there's only one verb in the category, then it will act as if they clicked on that verb. // Otherwise it opens the category menu. if (verbElement.SubMenu == null || verbElement.SubMenu.ChildCount == 0) return; if (verbElement.SubMenu.MenuBody.ChildCount != 1 || verbElement.SubMenu.MenuBody.Children.First() is not VerbMenuElement verbMenuElement) { _context.OpenSubMenu(verbElement); return; } verb = verbMenuElement.Verb; if (verb == null) return; } #if DEBUG // No confirmation pop-ups in debug mode. ExecuteVerb(verb); #else if (!verb.ConfirmationPopup) { ExecuteVerb(verb); return; } if (verbElement.SubMenu == null) { var popupElement = new ConfirmationMenuElement(verb, Loc.GetString("generic-confirm")); verbElement.SubMenu = new ContextMenuPopup(_context, verbElement); _context.AddElement(verbElement.SubMenu, popupElement); } _context.OpenSubMenu(verbElement); #endif } private void Close() { if (OpenMenu == null) return; OpenMenu.Close(); OpenMenu = null; } private void HandleVerbsResponse(VerbsResponseEvent msg) { if (OpenMenu == null || !OpenMenu.Visible || CurrentTarget != msg.Entity) return; AddServerVerbs(msg.Verbs, OpenMenu); } private void ExecuteVerb(Verb verb) { UIManager.ClickSound(); _verbSystem.ExecuteVerb(CurrentTarget, verb); if (verb.CloseMenu ?? verb.CloseMenuDefault) _context.Close(); } } }