using System.Linq; using System.Numerics; using Content.Client.Administration.Managers; using Content.Client.Examine; using Content.Client.Hands.Systems; using Content.Client.Strip; using Content.Client.UserInterface.Controls; using Content.Client.UserInterface.Systems.Hands.Controls; using Content.Client.Verbs.UI; using Content.Shared.CCVar; using Content.Shared.Clothing.Components; using Content.Shared.Contraband; using Content.Shared.Cuffs; using Content.Shared.Ensnaring; using Content.Shared.Ensnaring.Components; using Content.Shared.Hands.Components; using Content.Shared.IdentityManagement; using Content.Shared.Input; using Content.Shared.Inventory; using Content.Shared.Inventory.VirtualItem; using Content.Shared.Strip.Components; using JetBrains.Annotations; using Robust.Client.GameObjects; using Robust.Client.Player; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.Configuration; using Robust.Shared.Input; using Robust.Shared.Map; using Robust.Shared.Prototypes; using Robust.Shared.Utility; using static Content.Client.Inventory.ClientInventorySystem; using static Robust.Client.UserInterface.Control; namespace Content.Client.Inventory { [UsedImplicitly] public sealed partial class StrippableBoundUserInterface : BoundUserInterface { [Dependency] private IPlayerManager _player = default!; [Dependency] private IUserInterfaceManager _ui = default!; [Dependency] private IClientAdminManager _admin = default!; [Dependency] private IPrototypeManager _proto = default!; [Dependency] private IConfigurationManager _cvar = default!; private readonly ExamineSystem _examine; private readonly HandsSystem _hands; private readonly InventorySystem _inv; private readonly SharedCuffableSystem _cuffable; private readonly StrippableSystem _strippable; private readonly SharedEnsnareableSystem _snare; private readonly ContrabandSystem _contraband; // Is the BUI in admin view? If in admin view, has custom UI elements to help admins see things // (E.g contraband status icon, is the item chameleon etc...) private bool _isAdminView; #region Admin overlay vars private readonly ResPath _chameleonClothingTexturePath = new("/Textures/Interface/Default/Slots/ContrabandOverlay/camo.png"); private readonly Color _chameleonColor = new(147, 112, 219); private readonly string _hasContrabandTexturePathBeginning = "/Textures/Interface/Default/Slots/ContrabandOverlay/has_contra"; private readonly string _hasContrabandTexturePathEnding = ".png"; private readonly int _hasContrabandMaximumValue = 9; private readonly ResPath _contrabandTexturePath = new("/Textures/Interface/Default/Slots/ContrabandOverlay/contra.png"); #endregion [ViewVariables] private const int ButtonSeparation = 4; [ViewVariables] public const string HiddenPocketEntityId = "StrippingHiddenEntity"; [ViewVariables] private StrippingMenu? _strippingMenu; [ViewVariables] private readonly EntityUid _virtualHiddenEntity; /// /// The current amount of added hand buttons. /// [ViewVariables] private int _handCount; /// /// The current shape of the inventory, needed to calculate the window size. /// [ViewVariables] private Vector2i _inventoryDimensions; public StrippableBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { _examine = EntMan.System(); _hands = EntMan.System(); _inv = EntMan.System(); _cuffable = EntMan.System(); _strippable = EntMan.System(); _contraband = EntMan.System(); _snare = EntMan.System(); _virtualHiddenEntity = EntMan.SpawnEntity(HiddenPocketEntityId, MapCoordinates.Nullspace); _isAdminView = _cvar.GetCVar(CCVars.AdminStripMenuOverlayDefault); } protected override void Open() { base.Open(); _strippingMenu = this.CreateWindowCenteredLeft(); _strippingMenu.OnDirty += UpdateMenu; _strippingMenu.Title = Loc.GetString("strippable-bound-user-interface-stripping-menu-title", ("ownerName", Identity.Name(Owner, EntMan))); } protected override void Dispose(bool disposing) { if (!disposing) return; if (_strippingMenu != null) _strippingMenu.OnDirty -= UpdateMenu; EntMan.DeleteEntity(_virtualHiddenEntity); base.Dispose(disposing); } public void DirtyMenu() { if (_strippingMenu != null) _strippingMenu.Dirty = true; } public void UpdateMenu() { if (_strippingMenu == null) return; _strippingMenu.ClearButtons(); _handCount = 0; _inventoryDimensions = Vector2i.Zero; if (EntMan.TryGetComponent(Owner, out var inv)) { foreach (var slot in inv.Slots) { AddInventoryButton(Owner, slot.Name, inv); } } if (EntMan.TryGetComponent(Owner, out var handsComp) && handsComp.CanBeStripped) { // good ol hands shit code. there is a GuiHands comparer that does the same thing... but these are hands // and not gui hands... which are different... foreach (var (id, hand) in handsComp.Hands) { if (hand.Location != HandLocation.Right) continue; AddHandButton((Owner, handsComp), id, hand); } foreach (var (id, hand) in handsComp.Hands) { if (hand.Location != HandLocation.Middle) continue; AddHandButton((Owner, handsComp), id, hand); } foreach (var (id, hand) in handsComp.Hands) { if (hand.Location != HandLocation.Left) continue; AddHandButton((Owner, handsComp), id, hand); } } // snare-removal button. This is just the old button before the change to item slots. It is pretty out of place. if (EntMan.TryGetComponent(Owner, out var snare) && _snare.IsEnsnared((Owner, snare))) { var button = new Button() { Text = Loc.GetString("strippable-bound-user-interface-stripping-menu-ensnare-button"), }; button.OnPressed += (_) => SendPredictedMessage(new StrippingEnsnareButtonPressed()); _strippingMenu.ButtonContainer.AddChild(button); } if (_admin.IsAdmin()) { var adminButton = new Button() { Text = Loc.GetString("strippable-bound-user-interface-stripping-menu-admin-button"), ToggleMode = true, Pressed = _isAdminView, ToolTip = Loc.GetString("strippable-bound-user-interface-stripping-menu-admin-button-tooltip") }; adminButton.OnToggled += args => { _isAdminView = !_isAdminView; args.Button.Pressed = _isAdminView; UpdateMenu(); }; _strippingMenu.ButtonContainer.AddChild(adminButton); } // TODO fix layout container measuring (its broken atm). // _strippingMenu.InvalidateMeasure(); // _strippingMenu.Contents.Measure(Vector2Helpers.Infinity); // TODO allow windows to resize based on content's desired size // for now: shit-code // calculate the window size manually // +20 horizontally and vertically from the ContentsContainer margin // +16 vertically from the BoxContainer margin // +27 vertically from the window header var horizontalMenuSize = Math.Max(200, Math.Max(_handCount, _inventoryDimensions.X + 1) * (SlotControl.DefaultButtonSize + ButtonSeparation) + 20); var verticalMenuSize = Math.Max(200, (_inventoryDimensions.Y + (_handCount > 0 ? 2 : 1)) * (SlotControl.DefaultButtonSize + ButtonSeparation) + 53); verticalMenuSize += 25 * _strippingMenu.ButtonContainer.Children.Count(); _strippingMenu.SetSize = new Vector2(horizontalMenuSize, verticalMenuSize); } private void AddHandButton(Entity ent, string handId, Hand hand) { var button = new HandButton(handId, hand.Location); button.Pressed += SlotPressed; var heldEntity = _hands.GetHeldItem(ent.AsNullable(), handId); if (EntMan.TryGetComponent(heldEntity, out var virt)) { button.Blocked = true; if (_cuffable.TryGetAllCuffs(Owner, out var cuffs) && cuffs.Contains(virt.BlockingEntity)) button.BlockedRect.MouseFilter = MouseFilterMode.Ignore; } _strippingMenu!.HandsContainer.AddChild(button); UpdateEntityIcon(button, heldEntity); LayoutContainer.SetPosition(button, new Vector2i(_handCount, 0) * (SlotControl.DefaultButtonSize + ButtonSeparation)); _handCount++; } private void SlotPressed(GUIBoundKeyEventArgs ev, SlotControl slot) { // TODO: allow other interactions? Verbs? But they should then generate a pop-up and/or have a delay so the // user that is being stripped can prevent the verbs from being exectuted. // So for now: only stripping & examining if (ev.Function == EngineKeyFunctions.Use) { SendPredictedMessage(new StrippingSlotButtonPressed(slot.SlotName, slot is HandButton)); return; } if (slot.Entity == null) return; if (ev.Function == ContentKeyFunctions.ExamineEntity) { _examine.DoExamine(slot.Entity.Value); ev.Handle(); } else if (ev.Function == EngineKeyFunctions.UseSecondary) { _ui.GetUIController().OpenVerbMenu(slot.Entity.Value); ev.Handle(); } } private void AddInventoryButton(EntityUid invUid, string slotId, InventoryComponent inv) { if (!_inv.TryGetSlotContainer(invUid, slotId, out var container, out var slotDef, inv)) return; var entity = container.ContainedEntity; // If this is a full pocket, obscure the real entity // this does not work for modified clients because they are still sent the real entity if (entity != null && _strippable.IsStripHidden(slotDef, _player.LocalEntity)) entity = _virtualHiddenEntity; var button = new SlotButton(new SlotData(slotDef, container)); button.Pressed += SlotPressed; _strippingMenu!.InventoryContainer.AddChild(button); UpdateEntityIcon(button, entity); LayoutContainer.SetPosition(button, slotDef.StrippingWindowPos * (SlotControl.DefaultButtonSize + ButtonSeparation)); if (slotDef.StrippingWindowPos.X > _inventoryDimensions.X) _inventoryDimensions = new Vector2i(slotDef.StrippingWindowPos.X, _inventoryDimensions.Y); if (slotDef.StrippingWindowPos.Y > _inventoryDimensions.Y) _inventoryDimensions = new Vector2i(_inventoryDimensions.X, slotDef.StrippingWindowPos.Y); } private void UpdateEntityIcon(SlotControl button, EntityUid? entity) { // Hovering, highlighting & storage are features of general hands & inv GUIs. This UI just re-uses these because I'm lazy. button.ClearHover(); button.StorageButton.Visible = false; if (entity == null) { button.SetEntity(null); return; } EntityUid? viewEnt; if (EntMan.TryGetComponent(entity, out var virt)) viewEnt = EntMan.HasComponent(virt.BlockingEntity) ? virt.BlockingEntity : null; else if (EntMan.HasComponent(entity)) viewEnt = entity; else return; button.SetEntity(viewEnt); if (_admin.IsAdmin() && _isAdminView) { // overlay for chameleon clothing if (EntMan.HasComponent(entity)) { button.AddAdminOverlay(_chameleonClothingTexturePath, _chameleonColor); } // if the item has contraband, add a blue dotted outline (under the main contraband overlay if there is one) if (_contraband.ContainerHasContraband(entity.Value, Owner, out var contraList)) { // Get the color of the "most important" contraband using O(n) shenanigans var hasContraColor = contraList .Select(x => _proto.Index(x)) .MaxBy(x => x.Priority)! .Color; var path = new ResPath(_hasContrabandTexturePathBeginning + Math.Min(_hasContrabandMaximumValue, contraList.Count) + _hasContrabandTexturePathEnding); button.AddAdminOverlay(path, hasContraColor); } // overlay for items that are themselves contraband if (_contraband.IsContraband(entity.Value, Owner, out var contraProtoId)) { button.AddAdminOverlay(_contrabandTexturePath, _proto.Index(contraProtoId).Color); } // more overlays can be added if needed... } } } }