From 3b0867b8709f541ecbc0d5f802f174eb02b73e46 Mon Sep 17 00:00:00 2001 From: Fildrance Date: Thu, 23 Jan 2025 15:16:58 +0300 Subject: [PATCH 01/25] Feature/make radial menu great again (#32653) * it works! kinda * so it works now * minor cleanup * central button now is useful too * more cleanup * minor cleanup * more cleanup * refactor: migrated code from toolbox (as it was rejected as too specific) * feat: moved border drawing for radial menu into RadialMenuTextureButton. Radial menu position setting into was moved to OverrideArrange to not being called on every frame * refactor: major reworks! * renamed DrawBagleSector to DrawAnnulusSector * Remove strange indexing * Regularize math * refactor: re-orienting segment elements to be Y-mirrored * refactor: extracted radial menu radius multiplier property, changed color pallet for radial menu button * refactor: removed icon backgrounds on textures used in current radial menu buttons with sectors, RadialContainer Radius renamed and now actually changed control radius. * refactor: in RadialMenuTextureButtonWithSector all sector colors are converted to and from sRGB in property getter-setters * refactor: renamed srgb to include Srgb suffix so devs gonna see that its srgb clearly * fix: enabled any functional keys pressed when pushing radial menu buttons * fix: radial menu sector now scales with UIScale * fix: accept only one event when clicking on radial menu ContextualButton * fix: now radial menu buttons accepts only click/alt-click, now clicks outside menu closes menu always --------- Co-authored-by: pa.pecherskij Co-authored-by: Eoin Mcloughlin --- Content.Client/Chat/UI/EmotesMenu.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Content.Client/Chat/UI/EmotesMenu.xaml b/Content.Client/Chat/UI/EmotesMenu.xaml index 9ed5567ef14..845b6316171 100644 --- a/Content.Client/Chat/UI/EmotesMenu.xaml +++ b/Content.Client/Chat/UI/EmotesMenu.xaml @@ -9,13 +9,13 @@ - + - + From 9ede1286cb801a74c7dfa75b86eef0ee337b6e17 Mon Sep 17 00:00:00 2001 From: Fildrance Date: Mon, 31 Mar 2025 12:57:47 +0300 Subject: [PATCH 02/25] refactor: simple radial menu for easier creation (#34639) * it works! kinda * so it works now * minor cleanup * central button now is useful too * more cleanup * minor cleanup * more cleanup * refactor: migrated code from toolbox (as it was rejected as too specific) * feat: moved border drawing for radial menu into RadialMenuTextureButton. Radial menu position setting into was moved to OverrideArrange to not being called on every frame * refactor: major reworks! * renamed DrawBagleSector to DrawAnnulusSector * Remove strange indexing * Regularize math * refactor: re-orienting segment elements to be Y-mirrored * refactor: extracted radial menu radius multiplier property, changed color pallet for radial menu button * refactor: removed icon backgrounds on textures used in current radial menu buttons with sectors, RadialContainer Radius renamed and now actually changed control radius. * refactor: in RadialMenuTextureButtonWithSector all sector colors are converted to and from sRGB in property getter-setters * refactor: renamed srgb to include Srgb suffix so devs gonna see that its srgb clearly * fix: enabled any functional keys pressed when pushing radial menu buttons * fix: radial menu sector now scales with UIScale * fix: accept only one event when clicking on radial menu ContextualButton * fix: now radial menu buttons accepts only click/alt-click, now clicks outside menu closes menu always * feat: simple radial menu prototype for easier creation * refactor: cleanup, restored emote filtering, button models now have class hierarchy * refactor: remove usage of closure from 'outside code' * refactor: remove non existing type from UiControlTest * refactor: remove unused using * refactor: revert ability to declare radial menu layers in xaml, scale 32px sprites using scale in radial menu * refactor: whitespaces * refactor: subscribe for dispose on existing radial menus * feat: now simple radial menu button models can have custom color for each sector background (and hover background color). Also added OpenOverMouseScreenPosition inside SimpleRadialMenu * fix: AI door menu now can be closed by verb if it gets unpowered * refactor: simplify hiding border, extended xml-doc for simple radial menu settings * refactor: remove linq * fix: fix AI radial action serialization using invalid type * refactor: fix duplicate ShowDeviceNotRespondingPopup for AI by properly checking if it can interact * refactor: whitespaces, changed list to array in simple radial button preparing methods --------- Co-authored-by: pa.pecherskij Co-authored-by: Eoin Mcloughlin --- Content.Client/Chat/UI/EmotesMenu.xaml | 31 -- Content.Client/Chat/UI/EmotesMenu.xaml.cs | 116 -------- Content.Client/RCD/RCDMenu.xaml | 64 ---- Content.Client/RCD/RCDMenu.xaml.cs | 220 -------------- .../RCD/RCDMenuBoundUserInterface.cs | 126 +++++++- .../StationAi/StationAiBoundUserInterface.cs | 42 ++- .../Silicons/StationAi/StationAiMenu.xaml | 13 - .../Silicons/StationAi/StationAiMenu.xaml.cs | 126 -------- .../UserInterface/Controls/RadialMenu.cs | 58 ++-- .../Controls/SimpleRadialMenu.xaml | 8 + .../Controls/SimpleRadialMenu.xaml.cs | 279 ++++++++++++++++++ .../Systems/Emotes/EmotesUIController.cs | 96 +++++- .../Tests/UserInterface/UiControlTest.cs | 2 - .../StationAi/SharedStationAiSystem.Held.cs | 19 +- 14 files changed, 558 insertions(+), 642 deletions(-) delete mode 100644 Content.Client/Chat/UI/EmotesMenu.xaml delete mode 100644 Content.Client/Chat/UI/EmotesMenu.xaml.cs delete mode 100644 Content.Client/RCD/RCDMenu.xaml delete mode 100644 Content.Client/RCD/RCDMenu.xaml.cs delete mode 100644 Content.Client/Silicons/StationAi/StationAiMenu.xaml delete mode 100644 Content.Client/Silicons/StationAi/StationAiMenu.xaml.cs create mode 100644 Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml create mode 100644 Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs diff --git a/Content.Client/Chat/UI/EmotesMenu.xaml b/Content.Client/Chat/UI/EmotesMenu.xaml deleted file mode 100644 index 845b6316171..00000000000 --- a/Content.Client/Chat/UI/EmotesMenu.xaml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Content.Client/Chat/UI/EmotesMenu.xaml.cs b/Content.Client/Chat/UI/EmotesMenu.xaml.cs deleted file mode 100644 index a26d4b616a2..00000000000 --- a/Content.Client/Chat/UI/EmotesMenu.xaml.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System.Numerics; -using Content.Client.UserInterface.Controls; -using Content.Shared.Chat.Prototypes; -using Content.Shared.Speech; -using Content.Shared.Whitelist; -using Robust.Client.AutoGenerated; -using Robust.Client.GameObjects; -using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; -using Robust.Client.UserInterface.XAML; -using Robust.Shared.Player; -using Robust.Shared.Prototypes; -using Robust.Shared.Utility; - -namespace Content.Client.Chat.UI; - -[GenerateTypedNameReferences] -public sealed partial class EmotesMenu : RadialMenu -{ - [Dependency] private readonly EntityManager _entManager = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly ISharedPlayerManager _playerManager = default!; - - public event Action>? OnPlayEmote; - - public EmotesMenu() - { - IoCManager.InjectDependencies(this); - RobustXamlLoader.Load(this); - - var spriteSystem = _entManager.System(); - var whitelistSystem = _entManager.System(); - - var main = FindControl("Main"); - - var emotes = _prototypeManager.EnumeratePrototypes(); - foreach (var emote in emotes) - { - var player = _playerManager.LocalSession?.AttachedEntity; - if (emote.Category == EmoteCategory.Invalid || - emote.ChatTriggers.Count == 0 || - !(player.HasValue && whitelistSystem.IsWhitelistPassOrNull(emote.Whitelist, player.Value)) || - whitelistSystem.IsBlacklistPass(emote.Blacklist, player.Value)) - continue; - - if (!emote.Available && - _entManager.TryGetComponent(player.Value, out var speech) && - !speech.AllowedEmotes.Contains(emote.ID)) - continue; - - var parent = FindControl(emote.Category.ToString()); - - var button = new EmoteMenuButton - { - SetSize = new Vector2(64f, 64f), - ToolTip = Loc.GetString(emote.Name), - ProtoId = emote.ID, - }; - - var tex = new TextureRect - { - VerticalAlignment = VAlignment.Center, - HorizontalAlignment = HAlignment.Center, - Texture = spriteSystem.Frame0(emote.Icon), - TextureScale = new Vector2(2f, 2f), - }; - - button.AddChild(tex); - parent.AddChild(button); - foreach (var child in main.Children) - { - if (child is not RadialMenuTextureButton castChild) - continue; - - if (castChild.TargetLayer == emote.Category.ToString()) - { - castChild.Visible = true; - break; - } - } - } - - - // Set up menu actions - foreach (var child in Children) - { - if (child is not RadialContainer container) - continue; - AddEmoteClickAction(container); - } - - GeneralCategoryTexture.Texture = spriteSystem.Frame0(new SpriteSpecifier.Rsi(new ResPath("/Textures/Clothing/Head/Soft/mimesoft.rsi"), "icon")); - HandsCategoryTexture.Texture = spriteSystem.Frame0(new SpriteSpecifier.Rsi(new ResPath("/Textures/Clothing/Hands/Gloves/latex.rsi"), "icon")); - } - - private void AddEmoteClickAction(RadialContainer container) - { - foreach (var child in container.Children) - { - if (child is not EmoteMenuButton castChild) - continue; - - castChild.OnButtonUp += _ => - { - OnPlayEmote?.Invoke(castChild.ProtoId); - Close(); - }; - } - } -} - - -public sealed class EmoteMenuButton : RadialMenuTextureButtonWithSector -{ - public ProtoId ProtoId { get; set; } -} diff --git a/Content.Client/RCD/RCDMenu.xaml b/Content.Client/RCD/RCDMenu.xaml deleted file mode 100644 index 4684bd36a69..00000000000 --- a/Content.Client/RCD/RCDMenu.xaml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Content.Client/RCD/RCDMenu.xaml.cs b/Content.Client/RCD/RCDMenu.xaml.cs deleted file mode 100644 index 98f98596536..00000000000 --- a/Content.Client/RCD/RCDMenu.xaml.cs +++ /dev/null @@ -1,220 +0,0 @@ -using Content.Client.UserInterface.Controls; -using Content.Shared.Popups; -using Content.Shared.RCD; -using Content.Shared.RCD.Components; -using Robust.Client.AutoGenerated; -using Robust.Client.GameObjects; -using Robust.Client.Player; -using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; -using Robust.Client.UserInterface.XAML; -using Robust.Shared.Prototypes; -using Robust.Shared.Utility; -using System.Numerics; - -namespace Content.Client.RCD; - -[GenerateTypedNameReferences] -public sealed partial class RCDMenu : RadialMenu -{ - [Dependency] private readonly EntityManager _entManager = default!; - [Dependency] private readonly IPrototypeManager _protoManager = default!; - [Dependency] private readonly IPlayerManager _playerManager = default!; - - private SharedPopupSystem _popup; - private SpriteSystem _sprites; - - public event Action>? SendRCDSystemMessageAction; - - private EntityUid _owner; - - public RCDMenu() - { - IoCManager.InjectDependencies(this); - RobustXamlLoader.Load(this); - - _popup = _entManager.System(); - _sprites = _entManager.System(); - - OnChildAdded += AddRCDMenuButtonOnClickActions; - - // Triad: RPD category-button icons are wired here (not XAML) so we can use SpriteSystem.Frame0 - // to extract a single frame from each entity RSI. XAML's TextureRect.TexturePath loads raw PNGs, - // which would render the entire multi-direction sprite sheet for any atmos hardware state. - ApplyRPDCategoryIcons(); - } - - // Triad: TargetLayer -> (RSI path, state) for the 5 RPD category buttons. Sprites mirror the - // entities we construct so a fork-side sprite swap propagates to the picker automatically. - private static readonly (string Layer, string Rsi, string State)[] RPDCategoryIconSpecs = - { - ("Piping", "/Textures/Structures/Piping/Atmospherics/pipe.rsi", "pipeFourway"), - ("AtmosphericUtility", "/Textures/Structures/Piping/Atmospherics/gascanisterport.rsi", "gasCanisterPort"), - ("PumpsValves", "/Textures/Structures/Piping/Atmospherics/pump.rsi", "pumpVolume"), - ("Vents", "/Textures/_NF/Structures/Piping/Atmospherics/vent.rsi", "vent_passive"), - ("SensorsMonitors", "/Textures/Structures/Wallmounts/air_monitors.rsi", "alarm0"), - }; - - private void ApplyRPDCategoryIcons() - { - var main = FindControl("Main"); - - foreach (var child in main.Children) - { - if (child is not RadialMenuTextureButton btn) - continue; - - (string Layer, string Rsi, string State)? match = null; - foreach (var spec in RPDCategoryIconSpecs) - { - if (spec.Layer == btn.TargetLayer) - { - match = spec; - break; - } - } - if (match == null) - continue; - - btn.AddChild(new TextureRect - { - VerticalAlignment = VAlignment.Center, - HorizontalAlignment = HAlignment.Center, - TextureScale = new Vector2(2f, 2f), - Texture = _sprites.Frame0(new SpriteSpecifier.Rsi(new ResPath(match.Value.Rsi), match.Value.State)), - }); - } - } - - public void SetEntity(EntityUid uid) - { - _owner = uid; - Refresh(); - } - - public void Refresh() - { - // Find the main radial container - var main = FindControl("Main"); - - // Populate secondary radial containers - if (!_entManager.TryGetComponent(_owner, out var rcd)) - return; - - foreach (var protoId in rcd.AvailablePrototypes) - { - if (!_protoManager.TryIndex(protoId, out var proto)) - continue; - - if (proto.Mode == RcdMode.Invalid) - continue; - - var parent = FindControl(proto.Category); - var tooltip = Loc.GetString(proto.SetName); - - if ((proto.Mode == RcdMode.ConstructTile || proto.Mode == RcdMode.ConstructObject) && - proto.Prototype != null && _protoManager.TryIndex(proto.Prototype, out var entProto, logError: false)) - { - tooltip = Loc.GetString(entProto.Name); - } - - tooltip = OopsConcat(char.ToUpper(tooltip[0]).ToString(), tooltip.Remove(0, 1)); - - var button = new RCDMenuButton() - { - SetSize = new Vector2(64f, 64f), - ToolTip = tooltip, - ProtoId = protoId, - }; - - if (proto.Sprite != null) - { - var tex = new TextureRect() - { - VerticalAlignment = VAlignment.Center, - HorizontalAlignment = HAlignment.Center, - Texture = _sprites.Frame0(proto.Sprite), - TextureScale = new Vector2(2f, 2f), - }; - - button.AddChild(tex); - } - - parent.AddChild(button); - - // Ensure that the button that transitions the menu to the associated category layer - // is visible in the main radial container (as these all start with Visible = false) - foreach (var child in main.Children) - { - if (child is not RadialMenuTextureButton castChild) - continue; - - if (castChild.TargetLayer == proto.Category) - { - castChild.Visible = true; - break; - } - } - } - - // Set up menu actions - foreach (var child in Children) - { - AddRCDMenuButtonOnClickActions(child); - } - } - - private static string OopsConcat(string a, string b) - { - // This exists to prevent Roslyn being clever and compiling something that fails sandbox checks. - return a + b; - } - - private void AddRCDMenuButtonOnClickActions(Control control) - { - var radialContainer = control as RadialContainer; - - if (radialContainer == null) - return; - - foreach (var child in radialContainer.Children) - { - var castChild = child as RCDMenuButton; - - if (castChild == null) - continue; - - castChild.OnButtonUp += _ => - { - SendRCDSystemMessageAction?.Invoke(castChild.ProtoId); - - if (_playerManager.LocalSession?.AttachedEntity != null && - _protoManager.TryIndex(castChild.ProtoId, out var proto)) - { - var msg = Loc.GetString("rcd-component-change-mode", ("mode", Loc.GetString(proto.SetName))); - - if (proto.Mode == RcdMode.ConstructTile || proto.Mode == RcdMode.ConstructObject) - { - var name = Loc.GetString(proto.SetName); - - if (proto.Prototype != null && - _protoManager.TryIndex(proto.Prototype, out var entProto, logError: false)) - name = entProto.Name; - - msg = Loc.GetString("rcd-component-change-build-mode", ("name", name)); - } - - // Popup message - _popup.PopupClient(msg, _owner, _playerManager.LocalSession.AttachedEntity); - } - - Close(); - }; - } - } -} - -public sealed class RCDMenuButton : RadialMenuTextureButtonWithSector -{ - public ProtoId ProtoId { get; set; } -} diff --git a/Content.Client/RCD/RCDMenuBoundUserInterface.cs b/Content.Client/RCD/RCDMenuBoundUserInterface.cs index 1dd03626ae6..d599c324e17 100644 --- a/Content.Client/RCD/RCDMenuBoundUserInterface.cs +++ b/Content.Client/RCD/RCDMenuBoundUserInterface.cs @@ -1,20 +1,32 @@ +using Content.Client.Popups; +using Content.Client.UserInterface.Controls; using Content.Shared.RCD; using Content.Shared.RCD.Components; using JetBrains.Annotations; -using Robust.Client.Graphics; -using Robust.Client.Input; using Robust.Client.UserInterface; +using Robust.Shared.Player; using Robust.Shared.Prototypes; +using Robust.Shared.Utility; namespace Content.Client.RCD; [UsedImplicitly] public sealed class RCDMenuBoundUserInterface : BoundUserInterface { - [Dependency] private readonly IClyde _displayManager = default!; - [Dependency] private readonly IInputManager _inputManager = default!; + private static readonly Dictionary PrototypesGroupingInfo + = new Dictionary + { + ["WallsAndFlooring"] = ("rcd-component-walls-and-flooring", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RCD/walls_and_flooring.png"))), + ["WindowsAndGrilles"] = ("rcd-component-windows-and-grilles", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RCD/windows_and_grilles.png"))), + ["Airlocks"] = ("rcd-component-airlocks", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RCD/airlocks.png"))), + ["Electrical"] = ("rcd-component-electrical", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RCD/multicoil.png"))), + ["Lighting"] = ("rcd-component-lighting", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RCD/lighting.png"))), + }; - private RCDMenu? _menu; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly ISharedPlayerManager _playerManager = default!; + + private SimpleRadialMenu? _menu; public RCDMenuBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { @@ -25,19 +37,107 @@ protected override void Open() { base.Open(); - _menu = this.CreateWindow(); - _menu.SetEntity(Owner); - _menu.SendRCDSystemMessageAction += SendRCDSystemMessage; + if (!EntMan.TryGetComponent(Owner, out var rcd)) + return; + + _menu = this.CreateWindow(); + _menu.Track(Owner); + var models = ConvertToButtons(rcd.AvailablePrototypes); + _menu.SetButtons(models); - // Open the menu, centered on the mouse - var vpSize = _displayManager.ScreenSize; - _menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize); + _menu.OpenOverMouseScreenPosition(); } - public void SendRCDSystemMessage(ProtoId protoId) + private IEnumerable ConvertToButtons(HashSet> prototypes) + { + Dictionary> buttonsByCategory = new(); + foreach (var protoId in prototypes) + { + var prototype = _prototypeManager.Index(protoId); + if (!PrototypesGroupingInfo.TryGetValue(prototype.Category, out var groupInfo)) + continue; + + if (!buttonsByCategory.TryGetValue(prototype.Category, out var list)) + { + list = new List(); + buttonsByCategory.Add(prototype.Category, list); + } + + var actionOption = new RadialMenuActionOption(HandleMenuOptionClick, prototype) + { + Sprite = prototype.Sprite, + ToolTip = GetTooltip(prototype) + }; + list.Add(actionOption); + } + + var models = new RadialMenuNestedLayerOption[buttonsByCategory.Count]; + var i = 0; + foreach (var (key, list) in buttonsByCategory) + { + var groupInfo = PrototypesGroupingInfo[key]; + models[i] = new RadialMenuNestedLayerOption(list) + { + Sprite = groupInfo.Sprite, + ToolTip = Loc.GetString(groupInfo.Tooltip) + }; + i++; + } + + return models; + } + + private void HandleMenuOptionClick(RCDPrototype proto) { // A predicted message cannot be used here as the RCD UI is closed immediately // after this message is sent, which will stop the server from receiving it - SendMessage(new RCDSystemMessage(protoId)); + SendMessage(new RCDSystemMessage(proto.ID)); + + + if (_playerManager.LocalSession?.AttachedEntity == null) + return; + + var msg = Loc.GetString("rcd-component-change-mode", ("mode", Loc.GetString(proto.SetName))); + + if (proto.Mode is RcdMode.ConstructTile or RcdMode.ConstructObject) + { + var name = Loc.GetString(proto.SetName); + + if (proto.Prototype != null && + _prototypeManager.TryIndex(proto.Prototype, out var entProto, logError: false)) + name = entProto.Name; + + msg = Loc.GetString("rcd-component-change-build-mode", ("name", name)); + } + + // Popup message + var popup = EntMan.System(); + popup.PopupClient(msg, Owner, _playerManager.LocalSession.AttachedEntity); + } + + private string GetTooltip(RCDPrototype proto) + { + string tooltip; + + if (proto.Mode is RcdMode.ConstructTile or RcdMode.ConstructObject + && proto.Prototype != null + && _prototypeManager.TryIndex(proto.Prototype, out var entProto, logError: false)) + { + tooltip = Loc.GetString(entProto.Name); + } + else + { + tooltip = Loc.GetString(proto.SetName); + } + + tooltip = OopsConcat(char.ToUpper(tooltip[0]).ToString(), tooltip.Remove(0, 1)); + + return tooltip; + } + + private static string OopsConcat(string a, string b) + { + // This exists to prevent Roslyn being clever and compiling something that fails sandbox checks. + return a + b; } } diff --git a/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs b/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs index 68318305a0c..77ac13c972f 100644 --- a/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs +++ b/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs @@ -1,28 +1,46 @@ +using Content.Client.UserInterface.Controls; using Content.Shared.Silicons.StationAi; using Robust.Client.UserInterface; namespace Content.Client.Silicons.StationAi; -public sealed class StationAiBoundUserInterface : BoundUserInterface +public sealed class StationAiBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey) { - private StationAiMenu? _menu; - - public StationAiBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) - { - } + private SimpleRadialMenu? _menu; protected override void Open() { base.Open(); - _menu = this.CreateWindow(); + + var ev = new GetStationAiRadialEvent(); + EntMan.EventBus.RaiseLocalEvent(Owner, ref ev); + + _menu = this.CreateWindow(); _menu.Track(Owner); + var buttonModels = ConvertToButtons(ev.Actions); + _menu.SetButtons(buttonModels); + + _menu.Open(); + } - _menu.OnAiRadial += args => + private IEnumerable ConvertToButtons(IReadOnlyList actions) + { + var models = new RadialMenuActionOption[actions.Count]; + for (int i = 0; i < actions.Count; i++) { - SendPredictedMessage(new StationAiRadialMessage() + var action = actions[i]; + models[i] = new RadialMenuActionOption(HandleRadialMenuClick, action.Event) { - Event = args, - }); - }; + Sprite = action.Sprite, + ToolTip = action.Tooltip + }; + } + + return models; + } + + private void HandleRadialMenuClick(BaseStationAiAction p) + { + SendPredictedMessage(new StationAiRadialMessage { Event = p }); } } diff --git a/Content.Client/Silicons/StationAi/StationAiMenu.xaml b/Content.Client/Silicons/StationAi/StationAiMenu.xaml deleted file mode 100644 index cfa0b93234e..00000000000 --- a/Content.Client/Silicons/StationAi/StationAiMenu.xaml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - diff --git a/Content.Client/Silicons/StationAi/StationAiMenu.xaml.cs b/Content.Client/Silicons/StationAi/StationAiMenu.xaml.cs deleted file mode 100644 index a536d911f3c..00000000000 --- a/Content.Client/Silicons/StationAi/StationAiMenu.xaml.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System.Numerics; -using Content.Client.UserInterface.Controls; -using Content.Shared.Silicons.StationAi; -using Robust.Client.AutoGenerated; -using Robust.Client.GameObjects; -using Robust.Client.Graphics; -using Robust.Client.UserInterface.Controls; -using Robust.Client.UserInterface.XAML; -using Robust.Shared.Timing; - -namespace Content.Client.Silicons.StationAi; - -[GenerateTypedNameReferences] -public sealed partial class StationAiMenu : RadialMenu -{ - [Dependency] private readonly IClyde _clyde = default!; - [Dependency] private readonly IEntityManager _entManager = default!; - - public event Action? OnAiRadial; - - private EntityUid _tracked; - - public StationAiMenu() - { - IoCManager.InjectDependencies(this); - RobustXamlLoader.Load(this); - } - - public void Track(EntityUid owner) - { - _tracked = owner; - - if (!_entManager.EntityExists(_tracked)) - { - Close(); - return; - } - - BuildButtons(); - UpdatePosition(); - } - - private void BuildButtons() - { - var ev = new GetStationAiRadialEvent(); - _entManager.EventBus.RaiseLocalEvent(_tracked, ref ev); - - var main = FindControl("Main"); - main.DisposeAllChildren(); - var sprites = _entManager.System(); - - foreach (var action in ev.Actions) - { - // TODO: This radial boilerplate is quite annoying - var button = new StationAiMenuButton(action.Event) - { - SetSize = new Vector2(64f, 64f), - ToolTip = action.Tooltip != null ? Loc.GetString(action.Tooltip) : null, - }; - - if (action.Sprite != null) - { - var texture = sprites.Frame0(action.Sprite); - var scale = Vector2.One; - - if (texture.Width <= 32) - { - scale *= 2; - } - - var tex = new TextureRect - { - VerticalAlignment = VAlignment.Center, - HorizontalAlignment = HAlignment.Center, - Texture = texture, - TextureScale = scale, - }; - - button.AddChild(tex); - } - - button.OnPressed += args => - { - OnAiRadial?.Invoke(action.Event); - Close(); - }; - main.AddChild(button); - } - } - - protected override void FrameUpdate(FrameEventArgs args) - { - base.FrameUpdate(args); - UpdatePosition(); - } - - private void UpdatePosition() - { - if (!_entManager.TryGetComponent(_tracked, out TransformComponent? xform)) - { - Close(); - return; - } - - if (!xform.Coordinates.IsValid(_entManager)) - { - Close(); - return; - } - - var coords = _entManager.System().GetSpriteScreenCoordinates((_tracked, null, xform)); - - if (!coords.IsValid) - { - Close(); - return; - } - - OpenScreenAt(coords.Position, _clyde); - } -} - -public sealed class StationAiMenuButton(BaseStationAiAction action) : RadialMenuTextureButtonWithSector -{ - public BaseStationAiAction Action = action; -} diff --git a/Content.Client/UserInterface/Controls/RadialMenu.cs b/Content.Client/UserInterface/Controls/RadialMenu.cs index 1b7f07aa2cc..9734cf29601 100644 --- a/Content.Client/UserInterface/Controls/RadialMenu.cs +++ b/Content.Client/UserInterface/Controls/RadialMenu.cs @@ -1,10 +1,10 @@ -using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; -using Robust.Client.UserInterface.CustomControls; using System.Linq; using System.Numerics; using Content.Shared.Input; using Robust.Client.Graphics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; using Robust.Shared.Input; namespace Content.Client.UserInterface.Controls; @@ -143,11 +143,8 @@ protected override Vector2 ArrangeOverride(Vector2 finalSize) return children.First(x => x.Visible); } - public bool TryToMoveToNewLayer(string newLayer) + public bool TryToMoveToNewLayer(Control newLayer) { - if (newLayer == string.Empty) - return false; - var currentLayer = GetCurrentActiveLayer(); if (currentLayer == null) @@ -161,7 +158,7 @@ public bool TryToMoveToNewLayer(string newLayer) continue; // Hide layers which are not of interest - if (result == true || child.Name != newLayer) + if (result == true || child != newLayer) { child.Visible = false; } @@ -186,6 +183,19 @@ public bool TryToMoveToNewLayer(string newLayer) return result; } + public bool TryToMoveToNewLayer(string targetLayerControlName) + { + foreach (var child in Children) + { + if (child.Name == targetLayerControlName && child is RadialContainer) + { + return TryToMoveToNewLayer(child); + } + } + + return false; + } + public void ReturnToPreviousLayer() { // Close the menu if the traversal path is empty @@ -296,9 +306,15 @@ protected override bool HasPoint(Vector2 point) public class RadialMenuTextureButton : RadialMenuTextureButtonBase { /// - /// Upon clicking this button the radial menu will be moved to the named layer + /// Upon clicking this button the radial menu will be moved to the layer of this control. /// - public string TargetLayer { get; set; } = string.Empty; + public Control? TargetLayer { get; set; } + + /// + /// Other way to set navigation to other container, as , + /// but using property of target . + /// + public string? TargetLayerControlName { get; set; } /// /// A simple texture button that can move the user to a different layer within a radial menu @@ -311,7 +327,7 @@ public RadialMenuTextureButton() private void OnClicked(ButtonEventArgs args) { - if (TargetLayer == string.Empty) + if (TargetLayer == null && TargetLayerControlName == null) return; var parent = FindParentMultiLayerContainer(this); @@ -319,7 +335,14 @@ private void OnClicked(ButtonEventArgs args) if (parent == null) return; - parent.TryToMoveToNewLayer(TargetLayer); + if (TargetLayer != null) + { + parent.TryToMoveToNewLayer(TargetLayer); + } + else + { + parent.TryToMoveToNewLayer(TargetLayerControlName!); + } } private RadialMenu? FindParentMultiLayerContainer(Control control) @@ -387,7 +410,7 @@ public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadia private Color _hoverBorderColorSrgb = Color.ToSrgb(new Color(87, 91, 127, 128)); /// - /// Marker, that control should render border of segment. Is false by default. + /// Marker, that controls if border of segment should be rendered. Is false by default. /// /// /// By default color of border is same as color of background. Use @@ -400,13 +423,6 @@ public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadia /// public bool DrawBackground { get; set; } = true; - /// - /// Marker, that control should render separator lines. - /// Separator lines are used to visually separate sector of radial menu items. - /// Is true by default - /// - public bool DrawSeparators { get; set; } = true; - /// /// Color of background in non-hovered state. Accepts RGB color, works with sRGB for DrawPrimitive internally. /// @@ -520,7 +536,7 @@ protected override void Draw(DrawingHandleScreen handle) DrawAnnulusSector(handle, containerCenter, _innerRadius * UIScale, _outerRadius * UIScale, angleFrom, angleTo, borderColor, false); } - if (!_isWholeCircle && DrawSeparators) + if (!_isWholeCircle && DrawBorder) { DrawSeparatorLines(handle, containerCenter, _innerRadius * UIScale, _outerRadius * UIScale, angleFrom, angleTo, SeparatorColor); } diff --git a/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml b/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml new file mode 100644 index 00000000000..307064334db --- /dev/null +++ b/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml @@ -0,0 +1,8 @@ + + diff --git a/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs b/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs new file mode 100644 index 00000000000..15c8065a440 --- /dev/null +++ b/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs @@ -0,0 +1,279 @@ +using Robust.Client.UserInterface; +using System.Numerics; +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Shared.Utility; +using Robust.Client.GameObjects; +using Robust.Shared.Timing; +using Robust.Client.UserInterface.XAML; +using Robust.Client.Input; + +namespace Content.Client.UserInterface.Controls; + +[GenerateTypedNameReferences] +public partial class SimpleRadialMenu : RadialMenu +{ + private EntityUid? _attachMenuToEntity; + + [Dependency] private readonly IClyde _clyde = default!; + [Dependency] private readonly IEntityManager _entManager = default!; + [Dependency] private readonly IInputManager _inputManager = default!; + + public SimpleRadialMenu() + { + IoCManager.InjectDependencies(this); + RobustXamlLoader.Load(this); + } + + public void Track(EntityUid owner) + { + _attachMenuToEntity = owner; + } + + public void SetButtons(IEnumerable models, SimpleRadialMenuSettings? settings = null) + { + ClearExistingChildrenRadialButtons(); + + var sprites = _entManager.System(); + Fill(models, sprites, Children, settings ?? new SimpleRadialMenuSettings()); + } + + public void OpenOverMouseScreenPosition() + { + var vpSize = _clyde.ScreenSize; + OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize); + } + + private void Fill( + IEnumerable models, + SpriteSystem sprites, + ICollection rootControlChildren, + SimpleRadialMenuSettings settings + ) + { + var rootContainer = new RadialContainer + { + HorizontalExpand = true, + VerticalExpand = true, + InitialRadius = settings.DefaultContainerRadius, + ReserveSpaceForHiddenChildren = false, + Visible = true + }; + rootControlChildren.Add(rootContainer); + + foreach (var model in models) + { + if (model is RadialMenuNestedLayerOption nestedMenuModel) + { + var linkButton = RecursiveContainerExtraction(sprites, rootControlChildren, nestedMenuModel, settings); + linkButton.Visible = true; + rootContainer.AddChild(linkButton); + } + else + { + var rootButtons = ConvertToButton(model, sprites, settings, false); + rootContainer.AddChild(rootButtons); + } + } + } + + private RadialMenuTextureButton RecursiveContainerExtraction( + SpriteSystem sprites, + ICollection rootControlChildren, + RadialMenuNestedLayerOption model, + SimpleRadialMenuSettings settings + ) + { + var container = new RadialContainer + { + HorizontalExpand = true, + VerticalExpand = true, + InitialRadius = model.ContainerRadius!.Value, + ReserveSpaceForHiddenChildren = false, + Visible = false + }; + foreach (var nested in model.Nested) + { + if (nested is RadialMenuNestedLayerOption nestedMenuModel) + { + var linkButton = RecursiveContainerExtraction(sprites, rootControlChildren, nestedMenuModel, settings); + container.AddChild(linkButton); + } + else + { + var button = ConvertToButton(nested, sprites, settings, false); + container.AddChild(button); + } + } + rootControlChildren.Add(container); + + var thisLayerLinkButton = ConvertToButton(model, sprites, settings, true); + thisLayerLinkButton.TargetLayer = container; + return thisLayerLinkButton; + } + + private RadialMenuTextureButton ConvertToButton( + RadialMenuOption model, + SpriteSystem sprites, + SimpleRadialMenuSettings settings, + bool haveNested + ) + { + var button = settings.UseSectors + ? ConvertToButtonWithSector(model, settings) + : new RadialMenuTextureButton(); + button.SetSize = new Vector2(64f, 64f); + button.ToolTip = model.ToolTip; + if (model.Sprite != null) + { + var scale = Vector2.One; + + var texture = sprites.Frame0(model.Sprite); + if (texture.Width <= 32) + { + scale *= 2; + } + + button.TextureNormal = texture; + button.Scale = scale; + } + + if (model is RadialMenuActionOption actionOption) + { + button.OnPressed += _ => + { + actionOption.OnPressed?.Invoke(); + if(!haveNested) + Close(); + }; + } + + return button; + } + + private static RadialMenuTextureButtonWithSector ConvertToButtonWithSector(RadialMenuOption model, SimpleRadialMenuSettings settings) + { + var button = new RadialMenuTextureButtonWithSector + { + DrawBorder = settings.DisplayBorders, + DrawBackground = !settings.NoBackground + }; + if (model.BackgroundColor.HasValue) + { + button.BackgroundColor = model.BackgroundColor.Value; + } + + if (model.HoverBackgroundColor.HasValue) + { + button.HoverBackgroundColor = model.HoverBackgroundColor.Value; + } + + return button; + } + + private void ClearExistingChildrenRadialButtons() + { + var toRemove = new List(ChildCount); + foreach (var child in Children) + { + if (child != ContextualButton && child != MenuOuterAreaButton) + { + toRemove.Add(child); + } + } + + foreach (var control in toRemove) + { + Children.Remove(control); + } + } + + #region target entity tracking + + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + if (_attachMenuToEntity != null) + { + UpdatePosition(); + } + } + + private void UpdatePosition() + { + if (!_entManager.TryGetComponent(_attachMenuToEntity, out TransformComponent? xform)) + { + Close(); + return; + } + + if (!xform.Coordinates.IsValid(_entManager)) + { + Close(); + return; + } + + var coords = _entManager.System().GetSpriteScreenCoordinates((_attachMenuToEntity.Value, null, xform)); + + if (!coords.IsValid) + { + Close(); + return; + } + + OpenScreenAt(coords.Position, _clyde); + } + + #endregion + +} + + +public abstract class RadialMenuOption +{ + public string? ToolTip { get; init; } + + public SpriteSpecifier? Sprite { get; init; } + public Color? BackgroundColor { get; set; } + public Color? HoverBackgroundColor { get; set; } +} + +public class RadialMenuActionOption(Action onPressed) : RadialMenuOption +{ + public Action OnPressed { get; } = onPressed; +} + +public class RadialMenuActionOption(Action onPressed, T data) + : RadialMenuActionOption(onPressed: () => onPressed(data)); + +public class RadialMenuNestedLayerOption(IReadOnlyCollection nested, float containerRadius = 100) + : RadialMenuOption +{ + public float? ContainerRadius { get; } = containerRadius; + + public IReadOnlyCollection Nested { get; } = nested; +} + +public class SimpleRadialMenuSettings +{ + /// + /// Default container draw radius. Is going to be further affected by per sector increment. + /// + public int DefaultContainerRadius = 100; + + /// + /// Marker, if sector-buttons should be used. + /// + public bool UseSectors = true; + + /// + /// Marker, if border of buttons should be rendered. Can only be used when = true. + /// + public bool DisplayBorders = true; + + /// + /// Marker, if sector background should not be rendered. Can only be used when = true. + /// + public bool NoBackground = false; +} + diff --git a/Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs b/Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs index 7b86859a1a2..7652e39bfd6 100644 --- a/Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs +++ b/Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs @@ -1,16 +1,17 @@ -using Content.Client.Chat.UI; using Content.Client.Gameplay; using Content.Client.UserInterface.Controls; using Content.Shared.Chat; using Content.Shared.Chat.Prototypes; using Content.Shared.Input; +using Content.Shared.Speech; +using Content.Shared.Whitelist; using JetBrains.Annotations; -using Robust.Client.Graphics; -using Robust.Client.Input; +using Robust.Client.Player; using Robust.Client.UserInterface.Controllers; using Robust.Client.UserInterface.Controls; using Robust.Shared.Input.Binding; using Robust.Shared.Prototypes; +using Robust.Shared.Utility; namespace Content.Client.UserInterface.Systems.Emotes; @@ -18,11 +19,19 @@ namespace Content.Client.UserInterface.Systems.Emotes; public sealed class EmotesUIController : UIController, IOnStateChanged { [Dependency] private readonly IEntityManager _entityManager = default!; - [Dependency] private readonly IClyde _displayManager = default!; - [Dependency] private readonly IInputManager _inputManager = default!; - + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + private MenuButton? EmotesButton => UIManager.GetActiveUIWidgetOrNull()?.EmotesButton; - private EmotesMenu? _menu; + private SimpleRadialMenu? _menu; + + private static readonly Dictionary EmoteGroupingInfo + = new Dictionary + { + [EmoteCategory.General] = ("emote-menu-category-general", new SpriteSpecifier.Texture(new ResPath("/Textures/Clothing/Head/Soft/mimesoft.rsi/icon.png"))), + [EmoteCategory.Hands] = ("emote-menu-category-hands", new SpriteSpecifier.Texture(new ResPath("/Textures/Clothing/Hands/Gloves/latex.rsi/icon.png"))), + [EmoteCategory.Vocal] = ("emote-menu-category-vocal", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Emotes/vocal.png"))), + }; public void OnStateEntered(GameplayState state) { @@ -42,10 +51,16 @@ private void ToggleEmotesMenu(bool centered) if (_menu == null) { // setup window - _menu = UIManager.CreateWindow(); + var prototypes = _prototypeManager.EnumeratePrototypes(); + var models = ConvertToButtons(prototypes); + + _menu = new SimpleRadialMenu(); + _menu.SetButtons(models); + + _menu.Open(); + _menu.OnClose += OnWindowClosed; _menu.OnOpen += OnWindowOpen; - _menu.OnPlayEmote += OnPlayEmote; if (EmotesButton != null) EmotesButton.SetClickPressed(true); @@ -56,16 +71,13 @@ private void ToggleEmotesMenu(bool centered) } else { - // Open the menu, centered on the mouse - var vpSize = _displayManager.ScreenSize; - _menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize); + _menu.OpenOverMouseScreenPosition(); } } else { _menu.OnClose -= OnWindowClosed; _menu.OnOpen -= OnWindowOpen; - _menu.OnPlayEmote -= OnPlayEmote; if (EmotesButton != null) EmotesButton.SetClickPressed(false); @@ -118,8 +130,62 @@ private void CloseMenu() _menu = null; } - private void OnPlayEmote(ProtoId protoId) + private IEnumerable ConvertToButtons(IEnumerable emotePrototypes) + { + var whitelistSystem = EntitySystemManager.GetEntitySystem(); + var player = _playerManager.LocalSession?.AttachedEntity; + + Dictionary> emotesByCategory = new(); + foreach (var emote in emotePrototypes) + { + if(emote.Category == EmoteCategory.Invalid) + continue; + + // only valid emotes that have ways to be triggered by chat and player have access / no restriction on + if (emote.Category == EmoteCategory.Invalid + || emote.ChatTriggers.Count == 0 + || !(player.HasValue && whitelistSystem.IsWhitelistPassOrNull(emote.Whitelist, player.Value)) + || whitelistSystem.IsBlacklistPass(emote.Blacklist, player.Value)) + continue; + + if (!emote.Available + && EntityManager.TryGetComponent(player.Value, out var speech) + && !speech.AllowedEmotes.Contains(emote.ID)) + continue; + + if (!emotesByCategory.TryGetValue(emote.Category, out var list)) + { + list = new List(); + emotesByCategory.Add(emote.Category, list); + } + + var actionOption = new RadialMenuActionOption(HandleRadialButtonClick, emote) + { + Sprite = emote.Icon, + ToolTip = Loc.GetString(emote.Name) + }; + list.Add(actionOption); + } + + var models = new RadialMenuOption[emotesByCategory.Count]; + var i = 0; + foreach (var (key, list) in emotesByCategory) + { + var tuple = EmoteGroupingInfo[key]; + + models[i] = new RadialMenuNestedLayerOption(list) + { + Sprite = tuple.Sprite, + ToolTip = Loc.GetString(tuple.Tooltip) + }; + i++; + } + + return models; + } + + private void HandleRadialButtonClick(EmotePrototype prototype) { - _entityManager.RaisePredictiveEvent(new PlayEmoteMessage(protoId)); + _entityManager.RaisePredictiveEvent(new PlayEmoteMessage(prototype.ID)); } } diff --git a/Content.IntegrationTests/Tests/UserInterface/UiControlTest.cs b/Content.IntegrationTests/Tests/UserInterface/UiControlTest.cs index 9990adfae68..05f68043ba0 100644 --- a/Content.IntegrationTests/Tests/UserInterface/UiControlTest.cs +++ b/Content.IntegrationTests/Tests/UserInterface/UiControlTest.cs @@ -1,5 +1,4 @@ using System.Linq; -using Content.Client.Chat.UI; using Content.Client.LateJoin; using Robust.Client.UserInterface.CustomControls; using Robust.Shared.ContentPack; @@ -15,7 +14,6 @@ public sealed class UiControlTest // You should not be adding to this. private Type[] _ignored = new Type[] { - typeof(EmotesMenu), typeof(LateJoinGui), typeof(CryosleepWakeupWindow), // Frontier: FIXME - refactor this window into EUI(?) pattern, this thing subscribes to events }; diff --git a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Held.cs b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Held.cs index 519ee144cd4..dae9772b80c 100644 --- a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Held.cs +++ b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Held.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using Content.Shared.Actions.Events; using Content.Shared.IdentityManagement; using Content.Shared.Interaction.Events; @@ -131,6 +130,14 @@ private void OnMessageAttempt(BoundUserInterfaceMessageAttempt ev) if (ev.Actor == ev.Target) return; + // no need to show menu if device is not powered. + if (!PowerReceiver.IsPowered(ev.Target)) + { + ShowDeviceNotRespondingPopup(ev.Actor); + ev.Cancel(); + return; + } + if (TryComp(ev.Actor, out StationAiHeldComponent? aiComp) && (!TryComp(ev.Target, out StationAiWhitelistComponent? whitelistComponent) || !ValidateAi((ev.Actor, aiComp)))) @@ -172,7 +179,8 @@ private void OnHeldInteraction(Entity ent, ref Interacti private void OnTargetVerbs(Entity ent, ref GetVerbsEvent args) { if (!args.CanComplexInteract - || !HasComp(args.User)) + || !HasComp(args.User) + || !args.CanInteract) { return; } @@ -194,13 +202,6 @@ private void OnTargetVerbs(Entity ent, ref GetVerbs Text = isOpen ? Loc.GetString("ai-close") : Loc.GetString("ai-open"), Act = () => { - // no need to show menu if device is not powered. - if (!PowerReceiver.IsPowered(ent.Owner)) - { - ShowDeviceNotRespondingPopup(user); - return; - } - if (isOpen) { _uiSystem.CloseUi(ent.Owner, AiUi.Key, user); From b5f0afdfd7682a02af6f8f0de60620fb2e0f8e0e Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Fri, 18 Apr 2025 12:28:24 -0400 Subject: [PATCH 03/25] Cleanup warnings in SimpleRadialMenu (#36690) --- .../Controls/SimpleRadialMenu.xaml.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs b/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs index 15c8065a440..31d7eab3400 100644 --- a/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs +++ b/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs @@ -11,7 +11,7 @@ namespace Content.Client.UserInterface.Controls; [GenerateTypedNameReferences] -public partial class SimpleRadialMenu : RadialMenu +public sealed partial class SimpleRadialMenu : RadialMenu { private EntityUid? _attachMenuToEntity; @@ -147,7 +147,7 @@ bool haveNested Close(); }; } - + return button; } @@ -232,21 +232,21 @@ private void UpdatePosition() public abstract class RadialMenuOption { public string? ToolTip { get; init; } - + public SpriteSpecifier? Sprite { get; init; } public Color? BackgroundColor { get; set; } public Color? HoverBackgroundColor { get; set; } } -public class RadialMenuActionOption(Action onPressed) : RadialMenuOption +public abstract class RadialMenuActionOption(Action onPressed) : RadialMenuOption { public Action OnPressed { get; } = onPressed; } -public class RadialMenuActionOption(Action onPressed, T data) +public sealed class RadialMenuActionOption(Action onPressed, T data) : RadialMenuActionOption(onPressed: () => onPressed(data)); -public class RadialMenuNestedLayerOption(IReadOnlyCollection nested, float containerRadius = 100) +public sealed class RadialMenuNestedLayerOption(IReadOnlyCollection nested, float containerRadius = 100) : RadialMenuOption { public float? ContainerRadius { get; } = containerRadius; @@ -254,7 +254,7 @@ public class RadialMenuNestedLayerOption(IReadOnlyCollection n public IReadOnlyCollection Nested { get; } = nested; } -public class SimpleRadialMenuSettings +public sealed class SimpleRadialMenuSettings { /// /// Default container draw radius. Is going to be further affected by per sector increment. From 4b871b86c84b38459e02d3d47283323283a1b008 Mon Sep 17 00:00:00 2001 From: Fildrance Date: Fri, 18 Apr 2025 07:50:15 +0300 Subject: [PATCH 04/25] fix: return deconstruct to the top level option of radial menu (#36486) feat: now RCD can have recepies that are placed on the top level of radial menu - 'Main' category name is reserved for this Co-authored-by: pa.pecherskij --- .../RCD/RCDMenuBoundUserInterface.cs | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/Content.Client/RCD/RCDMenuBoundUserInterface.cs b/Content.Client/RCD/RCDMenuBoundUserInterface.cs index d599c324e17..c001b7ec70a 100644 --- a/Content.Client/RCD/RCDMenuBoundUserInterface.cs +++ b/Content.Client/RCD/RCDMenuBoundUserInterface.cs @@ -4,6 +4,7 @@ using Content.Shared.RCD.Components; using JetBrains.Annotations; using Robust.Client.UserInterface; +using Robust.Shared.Collections; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Utility; @@ -13,6 +14,8 @@ namespace Content.Client.RCD; [UsedImplicitly] public sealed class RCDMenuBoundUserInterface : BoundUserInterface { + private const string TopLevelActionCategory = "Main"; + private static readonly Dictionary PrototypesGroupingInfo = new Dictionary { @@ -48,12 +51,24 @@ protected override void Open() _menu.OpenOverMouseScreenPosition(); } - private IEnumerable ConvertToButtons(HashSet> prototypes) + private IEnumerable ConvertToButtons(HashSet> prototypes) { Dictionary> buttonsByCategory = new(); + ValueList topLevelActions = new(); foreach (var protoId in prototypes) { var prototype = _prototypeManager.Index(protoId); + if (prototype.Category == TopLevelActionCategory) + { + var topLevelActionOption = new RadialMenuActionOption(HandleMenuOptionClick, prototype) + { + Sprite = prototype.Sprite, + ToolTip = GetTooltip(prototype) + }; + topLevelActions.Add(topLevelActionOption); + continue; + } + if (!PrototypesGroupingInfo.TryGetValue(prototype.Category, out var groupInfo)) continue; @@ -71,7 +86,7 @@ private IEnumerable ConvertToButtons(HashSet ConvertToButtons(HashSet Date: Wed, 10 Sep 2025 11:11:15 +0300 Subject: [PATCH 05/25] feat: SimpleRadial menu support for sprite-view and more extensibility (#39223) --- .../Ghost/GhostRoleRadioBoundUserInterface.cs | 51 +++++- Content.Client/Ghost/GhostRoleRadioMenu.xaml | 8 - .../Ghost/GhostRoleRadioMenu.xaml.cs | 105 ----------- .../RCD/RCDMenuBoundUserInterface.cs | 16 +- .../StationAi/StationAiBoundUserInterface.cs | 6 +- .../UserInterface/Controls/RadialMenu.cs | 37 ++-- .../Controls/SimpleRadialMenu.xaml.cs | 168 +++++++++++++++--- .../Systems/Emotes/EmotesUIController.cs | 12 +- .../GhostRoleMobSpawnerComponent.cs | 6 +- 9 files changed, 228 insertions(+), 181 deletions(-) delete mode 100644 Content.Client/Ghost/GhostRoleRadioMenu.xaml delete mode 100644 Content.Client/Ghost/GhostRoleRadioMenu.xaml.cs diff --git a/Content.Client/Ghost/GhostRoleRadioBoundUserInterface.cs b/Content.Client/Ghost/GhostRoleRadioBoundUserInterface.cs index 52ea835f4a8..9334c855364 100644 --- a/Content.Client/Ghost/GhostRoleRadioBoundUserInterface.cs +++ b/Content.Client/Ghost/GhostRoleRadioBoundUserInterface.cs @@ -1,25 +1,58 @@ +using Content.Client.UserInterface.Controls; using Content.Shared.Ghost.Roles; +using Content.Shared.Ghost.Roles.Components; using Robust.Client.UserInterface; using Robust.Shared.Prototypes; namespace Content.Client.Ghost; -public sealed class GhostRoleRadioBoundUserInterface : BoundUserInterface +public sealed class GhostRoleRadioBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey) { - private GhostRoleRadioMenu? _ghostRoleRadioMenu; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - public GhostRoleRadioBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) - { - IoCManager.InjectDependencies(this); - } + private SimpleRadialMenu? _ghostRoleRadioMenu; protected override void Open() { base.Open(); - _ghostRoleRadioMenu = this.CreateWindow(); - _ghostRoleRadioMenu.SetEntity(Owner); - _ghostRoleRadioMenu.SendGhostRoleRadioMessageAction += SendGhostRoleRadioMessage; + _ghostRoleRadioMenu = this.CreateWindow(); + + // The purpose of this radial UI is for ghost role radios that allow you to select + // more than one potential option, such as with kobolds/lizards. + // This means that it won't show anything if SelectablePrototypes is empty. + if (!EntMan.TryGetComponent(Owner, out var comp)) + return; + + var list = ConvertToButtons(comp.SelectablePrototypes); + + _ghostRoleRadioMenu.SetButtons(list); + } + + private IEnumerable ConvertToButtons(List> protoIds) + { + var list = new List(); + foreach (var ghostRoleProtoId in protoIds) + { + // For each prototype we find we want to create a button that uses the name of the ghost role + // as the hover tooltip, and the icon is taken from either the ghost role entityprototype + // or the indicated icon entityprototype. + if (!_prototypeManager.Resolve(ghostRoleProtoId, out var ghostRoleProto)) + continue; + + var option = new RadialMenuActionOption>(SendGhostRoleRadioMessage, ghostRoleProtoId) + { + ToolTip = Loc.GetString(ghostRoleProto.Name), + // pick the icon if it exists, otherwise fallback to the ghost role's entity + IconSpecifier = ghostRoleProto.IconPrototype != null + && _prototypeManager.Resolve(ghostRoleProto.IconPrototype, out var iconProto) + ? RadialMenuIconSpecifier.With(iconProto) + : RadialMenuIconSpecifier.With(ghostRoleProto.EntityPrototype) + }; + list.Add(option); + } + + return list; } private void SendGhostRoleRadioMessage(ProtoId protoId) diff --git a/Content.Client/Ghost/GhostRoleRadioMenu.xaml b/Content.Client/Ghost/GhostRoleRadioMenu.xaml deleted file mode 100644 index c35ee128c52..00000000000 --- a/Content.Client/Ghost/GhostRoleRadioMenu.xaml +++ /dev/null @@ -1,8 +0,0 @@ - - - - diff --git a/Content.Client/Ghost/GhostRoleRadioMenu.xaml.cs b/Content.Client/Ghost/GhostRoleRadioMenu.xaml.cs deleted file mode 100644 index 1b65eac6ed9..00000000000 --- a/Content.Client/Ghost/GhostRoleRadioMenu.xaml.cs +++ /dev/null @@ -1,105 +0,0 @@ -using Content.Client.UserInterface.Controls; -using Content.Shared.Ghost.Roles; -using Content.Shared.Ghost.Roles.Components; -using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; -using Robust.Client.UserInterface.XAML; -using Robust.Shared.Prototypes; -using System.Numerics; - -namespace Content.Client.Ghost; - -public sealed partial class GhostRoleRadioMenu : RadialMenu -{ - [Dependency] private readonly EntityManager _entityManager = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - - public event Action>? SendGhostRoleRadioMessageAction; - - public EntityUid Entity { get; set; } - - public GhostRoleRadioMenu() - { - IoCManager.InjectDependencies(this); - RobustXamlLoader.Load(this); - } - - public void SetEntity(EntityUid uid) - { - Entity = uid; - RefreshUI(); - } - - private void RefreshUI() - { - // The main control that will contain all the clickable options - var main = FindControl("Main"); - - // The purpose of this radial UI is for ghost role radios that allow you to select - // more than one potential option, such as with kobolds/lizards. - // This means that it won't show anything if SelectablePrototypes is empty. - if (!_entityManager.TryGetComponent(Entity, out var comp)) - return; - - foreach (var ghostRoleProtoString in comp.SelectablePrototypes) - { - // For each prototype we find we want to create a button that uses the name of the ghost role - // as the hover tooltip, and the icon is taken from either the ghost role entityprototype - // or the indicated icon entityprototype. - if (!_prototypeManager.TryIndex(ghostRoleProtoString, out var ghostRoleProto)) - continue; - - var button = new GhostRoleRadioMenuButton() - { - SetSize = new Vector2(64, 64), - ToolTip = Loc.GetString(ghostRoleProto.Name), - ProtoId = ghostRoleProto.ID, - }; - - var entProtoView = new EntityPrototypeView() - { - SetSize = new Vector2(48, 48), - VerticalAlignment = VAlignment.Center, - HorizontalAlignment = HAlignment.Center, - Stretch = SpriteView.StretchMode.Fill - }; - - // pick the icon if it exists, otherwise fallback to the ghost role's entity - if (_prototypeManager.TryIndex(ghostRoleProto.IconPrototype, out var iconProto)) - entProtoView.SetPrototype(iconProto); - else - entProtoView.SetPrototype(ghostRoleProto.EntityPrototype); - - button.AddChild(entProtoView); - main.AddChild(button); - AddGhostRoleRadioMenuButtonOnClickActions(main); - } - } - - private void AddGhostRoleRadioMenuButtonOnClickActions(Control control) - { - var mainControl = control as RadialContainer; - - if (mainControl == null) - return; - - foreach (var child in mainControl.Children) - { - var castChild = child as GhostRoleRadioMenuButton; - - if (castChild == null) - continue; - - castChild.OnButtonUp += _ => - { - SendGhostRoleRadioMessageAction?.Invoke(castChild.ProtoId); - Close(); - }; - } - } -} - -public sealed class GhostRoleRadioMenuButton : RadialMenuTextureButtonWithSector -{ - public ProtoId ProtoId { get; set; } -} diff --git a/Content.Client/RCD/RCDMenuBoundUserInterface.cs b/Content.Client/RCD/RCDMenuBoundUserInterface.cs index c001b7ec70a..076fa9c0193 100644 --- a/Content.Client/RCD/RCDMenuBoundUserInterface.cs +++ b/Content.Client/RCD/RCDMenuBoundUserInterface.cs @@ -51,10 +51,10 @@ protected override void Open() _menu.OpenOverMouseScreenPosition(); } - private IEnumerable ConvertToButtons(HashSet> prototypes) + private IEnumerable ConvertToButtons(HashSet> prototypes) { - Dictionary> buttonsByCategory = new(); - ValueList topLevelActions = new(); + Dictionary> buttonsByCategory = new(); + ValueList topLevelActions = new(); foreach (var protoId in prototypes) { var prototype = _prototypeManager.Index(protoId); @@ -62,7 +62,7 @@ private IEnumerable ConvertToButtons(HashSet(HandleMenuOptionClick, prototype) { - Sprite = prototype.Sprite, + IconSpecifier = RadialMenuIconSpecifier.With(prototype.Sprite), ToolTip = GetTooltip(prototype) }; topLevelActions.Add(topLevelActionOption); @@ -74,26 +74,26 @@ private IEnumerable ConvertToButtons(HashSet(); + list = new List(); buttonsByCategory.Add(prototype.Category, list); } var actionOption = new RadialMenuActionOption(HandleMenuOptionClick, prototype) { - Sprite = prototype.Sprite, + IconSpecifier = RadialMenuIconSpecifier.With(prototype.Sprite), ToolTip = GetTooltip(prototype) }; list.Add(actionOption); } - var models = new RadialMenuOption[buttonsByCategory.Count + topLevelActions.Count]; + var models = new RadialMenuOptionBase[buttonsByCategory.Count + topLevelActions.Count]; var i = 0; foreach (var (key, list) in buttonsByCategory) { var groupInfo = PrototypesGroupingInfo[key]; models[i] = new RadialMenuNestedLayerOption(list) { - Sprite = groupInfo.Sprite, + IconSpecifier = RadialMenuIconSpecifier.With(groupInfo.Sprite), ToolTip = Loc.GetString(groupInfo.Tooltip) }; i++; diff --git a/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs b/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs index 77ac13c972f..2ada6e4b015 100644 --- a/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs +++ b/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs @@ -23,15 +23,15 @@ protected override void Open() _menu.Open(); } - private IEnumerable ConvertToButtons(IReadOnlyList actions) + private IEnumerable ConvertToButtons(IReadOnlyList actions) { - var models = new RadialMenuActionOption[actions.Count]; + var models = new RadialMenuActionOptionBase[actions.Count]; for (int i = 0; i < actions.Count; i++) { var action = actions[i]; models[i] = new RadialMenuActionOption(HandleRadialMenuClick, action.Event) { - Sprite = action.Sprite, + IconSpecifier = RadialMenuIconSpecifier.With(action.Sprite), ToolTip = action.Tooltip }; } diff --git a/Content.Client/UserInterface/Controls/RadialMenu.cs b/Content.Client/UserInterface/Controls/RadialMenu.cs index 9734cf29601..959a60ef4f8 100644 --- a/Content.Client/UserInterface/Controls/RadialMenu.cs +++ b/Content.Client/UserInterface/Controls/RadialMenu.cs @@ -229,10 +229,10 @@ public void ReturnToPreviousLayer() /// from interactions. /// [Virtual] -public class RadialMenuTextureButtonBase : TextureButton +public abstract class RadialMenuButtonBase : BaseButton { /// - protected RadialMenuTextureButtonBase() + protected RadialMenuButtonBase() { EnableAllKeybinds = true; } @@ -242,7 +242,9 @@ protected override void KeyBindUp(GUIBoundKeyEventArgs args) { if (args.Function == EngineKeyFunctions.UIClick || args.Function == ContentKeyFunctions.AltActivateItemInWorld) + { base.KeyBindUp(args); + } } } @@ -253,8 +255,14 @@ protected override void KeyBindUp(GUIBoundKeyEventArgs args) /// works only if control have parent, and ActiveContainer property is set. /// Also considers all space outside of radial menu buttons as itself for clicking. /// -public sealed class RadialMenuContextualCentralTextureButton : RadialMenuTextureButtonBase +public sealed class RadialMenuContextualCentralTextureButton : TextureButton { + /// + public RadialMenuContextualCentralTextureButton() + { + EnableAllKeybinds = true; + } + public float InnerRadius { get; set; } public Vector2? ParentCenter { get; set; } @@ -271,15 +279,25 @@ protected override bool HasPoint(Vector2 point) var innerRadiusSquared = InnerRadius * InnerRadius; - // comparing to squared values is faster then making sqrt + // comparing to squared values is faster, then making sqrt return distSquared < innerRadiusSquared; } + + /// + protected override void KeyBindUp(GUIBoundKeyEventArgs args) + { + if (args.Function == EngineKeyFunctions.UIClick + || args.Function == ContentKeyFunctions.AltActivateItemInWorld) + { + base.KeyBindUp(args); + } + } } /// /// Menu button for outer area of radial menu (covers everything 'outside'). /// -public sealed class RadialMenuOuterAreaButton : RadialMenuTextureButtonBase +public sealed class RadialMenuOuterAreaButton : RadialMenuButtonBase { public float OuterRadius { get; set; } @@ -303,7 +321,7 @@ protected override bool HasPoint(Vector2 point) } [Virtual] -public class RadialMenuTextureButton : RadialMenuTextureButtonBase +public class RadialMenuButton : RadialMenuButtonBase { /// /// Upon clicking this button the radial menu will be moved to the layer of this control. @@ -319,9 +337,8 @@ public class RadialMenuTextureButton : RadialMenuTextureButtonBase /// /// A simple texture button that can move the user to a different layer within a radial menu /// - public RadialMenuTextureButton() + public RadialMenuButton() { - EnableAllKeybinds = true; OnButtonUp += OnClicked; } @@ -391,7 +408,7 @@ public interface IRadialMenuItemWithSector } [Virtual] -public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadialMenuItemWithSector +public class RadialMenuButtonWithSector : RadialMenuButton, IRadialMenuItemWithSector { private Vector2[]? _sectorPointsForDrawing; @@ -500,7 +517,7 @@ float IRadialMenuItemWithSector.AngleSectorTo /// /// A simple texture button that can move the user to a different layer within a radial menu /// - public RadialMenuTextureButtonWithSector() + public RadialMenuButtonWithSector() { } diff --git a/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs b/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs index 31d7eab3400..ec7dcbbb5af 100644 --- a/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs +++ b/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs @@ -7,6 +7,8 @@ using Robust.Shared.Timing; using Robust.Client.UserInterface.XAML; using Robust.Client.Input; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Prototypes; namespace Content.Client.UserInterface.Controls; @@ -30,7 +32,7 @@ public void Track(EntityUid owner) _attachMenuToEntity = owner; } - public void SetButtons(IEnumerable models, SimpleRadialMenuSettings? settings = null) + public void SetButtons(IEnumerable models, SimpleRadialMenuSettings? settings = null) { ClearExistingChildrenRadialButtons(); @@ -45,7 +47,7 @@ public void OpenOverMouseScreenPosition() } private void Fill( - IEnumerable models, + IEnumerable models, SpriteSystem sprites, ICollection rootControlChildren, SimpleRadialMenuSettings settings @@ -77,7 +79,7 @@ SimpleRadialMenuSettings settings } } - private RadialMenuTextureButton RecursiveContainerExtraction( + private RadialMenuButton RecursiveContainerExtraction( SpriteSystem sprites, ICollection rootControlChildren, RadialMenuNestedLayerOption model, @@ -112,8 +114,8 @@ SimpleRadialMenuSettings settings return thisLayerLinkButton; } - private RadialMenuTextureButton ConvertToButton( - RadialMenuOption model, + private RadialMenuButton ConvertToButton( + RadialMenuOptionBase model, SpriteSystem sprites, SimpleRadialMenuSettings settings, bool haveNested @@ -121,29 +123,26 @@ bool haveNested { var button = settings.UseSectors ? ConvertToButtonWithSector(model, settings) - : new RadialMenuTextureButton(); + : new RadialMenuButton(); button.SetSize = new Vector2(64f, 64f); button.ToolTip = model.ToolTip; - if (model.Sprite != null) + var imageControl = model.IconSpecifier switch { - var scale = Vector2.One; - - var texture = sprites.Frame0(model.Sprite); - if (texture.Width <= 32) - { - scale *= 2; - } + RadialMenuTextureIconSpecifier textureSpecifier => CreateTexture(textureSpecifier.Sprite, sprites), + RadialMenuEntityIconSpecifier entitySpecifier => CreateSpriteView(entitySpecifier.Entity), + RadialMenuEntityPrototypeIconSpecifier entProtoSpecifier => CreateEntityPrototypeView(entProtoSpecifier.ProtoId), + _ => null + }; - button.TextureNormal = texture; - button.Scale = scale; - } + if(imageControl != null) + button.AddChild(imageControl); - if (model is RadialMenuActionOption actionOption) + if (model is RadialMenuActionOptionBase actionOption) { button.OnPressed += _ => { actionOption.OnPressed?.Invoke(); - if(!haveNested) + if (!haveNested) Close(); }; } @@ -151,9 +150,53 @@ bool haveNested return button; } - private static RadialMenuTextureButtonWithSector ConvertToButtonWithSector(RadialMenuOption model, SimpleRadialMenuSettings settings) + private Control CreateEntityPrototypeView(EntProtoId protoId) + { + var entProtoView = new EntityPrototypeView + { + SetSize = new Vector2(48, 48), + VerticalAlignment = VAlignment.Center, + HorizontalAlignment = HAlignment.Center, + Stretch = SpriteView.StretchMode.Fill, + }; + entProtoView.SetPrototype(protoId); + return entProtoView; + } + + private static Control CreateSpriteView(EntityUid entityForSpriteView) + { + var entView = new SpriteView + { + SetSize = new Vector2(48, 48), + VerticalAlignment = VAlignment.Center, + HorizontalAlignment = HAlignment.Center, + Stretch = SpriteView.StretchMode.Fill, + }; + entView.SetEntity(entityForSpriteView); + return entView; + } + + private static Control CreateTexture(SpriteSpecifier spriteSpecifier, SpriteSystem sprites) + { + var scale = Vector2.One; + + var texture = sprites.Frame0(spriteSpecifier); + if (texture.Width <= 32) + { + scale *= 2; + } + + var imageControl = new TextureRect() + { + Texture = texture, + TextureScale = scale + }; + return imageControl; + } + + private static RadialMenuButtonWithSector ConvertToButtonWithSector(RadialMenuOptionBase model, SimpleRadialMenuSettings settings) { - var button = new RadialMenuTextureButtonWithSector + var button = new RadialMenuButtonWithSector { DrawBorder = settings.DisplayBorders, DrawBackground = !settings.NoBackground @@ -228,32 +271,99 @@ private void UpdatePosition() } +/// +/// Abstract representation of a way to specify icon in radial menu. +/// +public abstract record RadialMenuIconSpecifier +{ + /// Use entity prototype viewer. + public static RadialMenuIconSpecifier? With(EntProtoId? protoId) + { + if (protoId is null) + return null; + + return new RadialMenuEntityPrototypeIconSpecifier(protoId.Value); + } + + /// Use simple texture icon. + public static RadialMenuIconSpecifier? With(SpriteSpecifier? sprite) + { + if (sprite == null) + return null; -public abstract class RadialMenuOption + return new RadialMenuTextureIconSpecifier(sprite); + } + + /// Use entity sprite viewer. + public static RadialMenuIconSpecifier? With(EntityUid? entity) + { + if (entity == null) + return null; + + return new RadialMenuEntityIconSpecifier(entity.Value); + } +} + +/// Marker that should be used to display radial menu icon. +public sealed record RadialMenuEntityIconSpecifier(EntityUid Entity) : RadialMenuIconSpecifier; + +/// Marker that should be used to display radial menu icon. +public sealed record RadialMenuTextureIconSpecifier(SpriteSpecifier Sprite) : RadialMenuIconSpecifier; + +/// Marker that should be used to display radial menu icon. +public sealed record RadialMenuEntityPrototypeIconSpecifier(EntProtoId ProtoId) : RadialMenuIconSpecifier; + +/// Container for common options for radial menu button. +public abstract class RadialMenuOptionBase { + /// Tooltip to be displayed when button is hovered. public string? ToolTip { get; init; } - public SpriteSpecifier? Sprite { get; init; } + /// + /// Color for button background. + /// Is used only with sector radial (). + /// public Color? BackgroundColor { get; set; } + /// + /// Color for button background when it is hovered. + /// Is used only with sector radial (). + /// public Color? HoverBackgroundColor { get; set; } + + /// + /// Specifier that describes icon to be used for radial menu button. + /// + public RadialMenuIconSpecifier? IconSpecifier { get; set; } } -public abstract class RadialMenuActionOption(Action onPressed) : RadialMenuOption +/// Base type for model of radial menu button with some action on button pressed. +/// +public abstract class RadialMenuActionOptionBase(Action onPressed) : RadialMenuOptionBase { + /// Action to be executed on button press. public Action OnPressed { get; } = onPressed; } -public sealed class RadialMenuActionOption(Action onPressed, T data) - : RadialMenuActionOption(onPressed: () => onPressed(data)); +/// Strong-typed model for radial menu button with action, stores provided data to be used upon button press. +public sealed class RadialMenuActionOption(Action onPressed, T data) : RadialMenuActionOptionBase(onPressed: () => onPressed(data)); -public sealed class RadialMenuNestedLayerOption(IReadOnlyCollection nested, float containerRadius = 100) - : RadialMenuOption +/// +/// Model for radial menu button that represents reference for next layer of radial buttons. +/// +/// List of button models for next layer of menu. +/// Radius for radial menu buttons of next layer. +public sealed class RadialMenuNestedLayerOption(IReadOnlyCollection nested, float containerRadius = 100) : RadialMenuOptionBase { + /// Radius for radial menu buttons of next layer. public float? ContainerRadius { get; } = containerRadius; - public IReadOnlyCollection Nested { get; } = nested; + /// List of button models for next layer of menu. + public IReadOnlyCollection Nested { get; } = nested; } +/// +/// Additional settings for radial menu render. +/// public sealed class SimpleRadialMenuSettings { /// diff --git a/Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs b/Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs index 7652e39bfd6..adbbb486431 100644 --- a/Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs +++ b/Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs @@ -130,12 +130,12 @@ private void CloseMenu() _menu = null; } - private IEnumerable ConvertToButtons(IEnumerable emotePrototypes) + private IEnumerable ConvertToButtons(IEnumerable emotePrototypes) { var whitelistSystem = EntitySystemManager.GetEntitySystem(); var player = _playerManager.LocalSession?.AttachedEntity; - Dictionary> emotesByCategory = new(); + Dictionary> emotesByCategory = new(); foreach (var emote in emotePrototypes) { if(emote.Category == EmoteCategory.Invalid) @@ -155,19 +155,19 @@ private IEnumerable ConvertToButtons(IEnumerable(); + list = new List(); emotesByCategory.Add(emote.Category, list); } var actionOption = new RadialMenuActionOption(HandleRadialButtonClick, emote) { - Sprite = emote.Icon, + IconSpecifier = RadialMenuIconSpecifier.With(emote.Icon), ToolTip = Loc.GetString(emote.Name) }; list.Add(actionOption); } - var models = new RadialMenuOption[emotesByCategory.Count]; + var models = new RadialMenuOptionBase[emotesByCategory.Count]; var i = 0; foreach (var (key, list) in emotesByCategory) { @@ -175,7 +175,7 @@ private IEnumerable ConvertToButtons(IEnumerable - /// Allows a ghost to take this role, spawning a new entity. + /// Allows a ghost to take this role, spawning a new entity. /// [RegisterComponent, EntityCategory("Spawner")] public sealed partial class GhostRoleMobSpawnerComponent : Component @@ -21,9 +21,9 @@ public sealed partial class GhostRoleMobSpawnerComponent : Component public EntProtoId? Prototype; /// - /// If this ghostrole spawner has multiple selectable ghostrole prototypes. + /// If this ghostrole spawner has multiple selectable ghostrole prototypes. /// [DataField] - public List SelectablePrototypes = []; + public List> SelectablePrototypes = []; } } From 131607647d0f4a5ea5205715fe71993d2e78bac4 Mon Sep 17 00:00:00 2001 From: sleepyyapril Date: Fri, 17 Oct 2025 14:43:30 -0300 Subject: [PATCH 06/25] latest --- Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs b/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs index ec7dcbbb5af..06ea63f0a26 100644 --- a/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs +++ b/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs @@ -386,4 +386,3 @@ public sealed class SimpleRadialMenuSettings /// public bool NoBackground = false; } - From 1916dd10d670037d265ce4df67d7ebe1fe0aea1d Mon Sep 17 00:00:00 2001 From: sleepyyapril Date: Fri, 17 Oct 2025 14:51:19 -0300 Subject: [PATCH 07/25] fix 2 --- .../_EstacaoPirata/Cards/Hand/UI/CardHandMenu.xaml.cs | 4 ++-- .../_Goobstation/AmmoSelector/AmmoSelectorMenu.xaml.cs | 2 +- .../Clothing/ToggleableClothingRadialMenu.xaml.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenu.xaml.cs b/Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenu.xaml.cs index 296ef432c8e..99f7dd67e79 100644 --- a/Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenu.xaml.cs +++ b/Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenu.xaml.cs @@ -42,7 +42,7 @@ public CardHandMenu(EntityUid owner, CardHandMenuBoundUserInterface bui) else cardName = Loc.GetString(cardComp.Name); - var button = new CardMenuButton() + var button = new CardMenuButton { StyleClasses = { "RadialMenuButton" }, SetSize = new Vector2(64f, 64f), @@ -78,7 +78,7 @@ public CardHandMenu(EntityUid owner, CardHandMenuBoundUserInterface bui) } } -public sealed class CardMenuButton : RadialMenuTextureButton +public sealed class CardMenuButton : RadialMenuButton { public CardMenuButton() { diff --git a/Content.Client/_Goobstation/AmmoSelector/AmmoSelectorMenu.xaml.cs b/Content.Client/_Goobstation/AmmoSelector/AmmoSelectorMenu.xaml.cs index 2200d110dbc..2fde631a434 100644 --- a/Content.Client/_Goobstation/AmmoSelector/AmmoSelectorMenu.xaml.cs +++ b/Content.Client/_Goobstation/AmmoSelector/AmmoSelectorMenu.xaml.cs @@ -87,7 +87,7 @@ private void AddAmmoSelectorMenuButtonOnClickActions(RadialContainer control) } } -public sealed class AmmoSelectorMenuButton : RadialMenuTextureButtonWithSector +public sealed class AmmoSelectorMenuButton : RadialMenuButton { public ProtoId ProtoId { get; set; } } diff --git a/Content.Client/_Goobstation/Clothing/ToggleableClothingRadialMenu.xaml.cs b/Content.Client/_Goobstation/Clothing/ToggleableClothingRadialMenu.xaml.cs index 86f36e13f0f..681e3c9b5e0 100644 --- a/Content.Client/_Goobstation/Clothing/ToggleableClothingRadialMenu.xaml.cs +++ b/Content.Client/_Goobstation/Clothing/ToggleableClothingRadialMenu.xaml.cs @@ -97,7 +97,7 @@ private void AddToggleableClothingMenuButtonOnClickAction(Control control) } } -public sealed class ToggleableClothingRadialMenuButton : RadialMenuTextureButton +public sealed class ToggleableClothingRadialMenuButton : RadialMenuButton { public EntityUid AttachedClothingId { get; set; } } From 60e0b6011ef909c81dd4cda3f1e3aef492721afc Mon Sep 17 00:00:00 2001 From: sleepyyapril Date: Fri, 17 Oct 2025 14:58:38 -0300 Subject: [PATCH 08/25] fix 3! --- Content.Client/RCD/RCDMenuBoundUserInterface.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Content.Client/RCD/RCDMenuBoundUserInterface.cs b/Content.Client/RCD/RCDMenuBoundUserInterface.cs index 076fa9c0193..3f847c8beb6 100644 --- a/Content.Client/RCD/RCDMenuBoundUserInterface.cs +++ b/Content.Client/RCD/RCDMenuBoundUserInterface.cs @@ -125,8 +125,10 @@ private void HandleMenuOptionClick(RCDPrototype proto) var name = Loc.GetString(proto.SetName); if (proto.Prototype != null && - _prototypeManager.TryIndex(proto.Prototype, out var entProto, logError: false)) + _prototypeManager.TryIndex(proto.Prototype, out var entProto)) // don't use Resolve because this can be a tile + { name = entProto.Name; + } msg = Loc.GetString("rcd-component-change-build-mode", ("name", name)); } @@ -142,7 +144,7 @@ private string GetTooltip(RCDPrototype proto) if (proto.Mode is RcdMode.ConstructTile or RcdMode.ConstructObject && proto.Prototype != null - && _prototypeManager.TryIndex(proto.Prototype, out var entProto, logError: false)) + && _prototypeManager.TryIndex(proto.Prototype, out var entProto)) // don't use Resolve because this can be a tile { tooltip = Loc.GetString(entProto.Name); } From 9aa6fcd9c03986b8f40a5f7215f778bcacc71dd4 Mon Sep 17 00:00:00 2001 From: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Date: Sat, 20 Sep 2025 21:05:57 +0200 Subject: [PATCH 09/25] working! --- Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenu.xaml | 2 +- Content.Client/_Goobstation/AmmoSelector/AmmoSelectorMenu.xaml | 2 +- .../_Goobstation/Clothing/ToggleableClothingRadialMenu.xaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenu.xaml b/Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenu.xaml index 1383c7f5062..5d2760c310b 100644 --- a/Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenu.xaml +++ b/Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenu.xaml @@ -8,5 +8,5 @@ MinSize="450 450"> - + diff --git a/Content.Client/_Goobstation/AmmoSelector/AmmoSelectorMenu.xaml b/Content.Client/_Goobstation/AmmoSelector/AmmoSelectorMenu.xaml index 2cbe120629f..3e4487d0d4b 100644 --- a/Content.Client/_Goobstation/AmmoSelector/AmmoSelectorMenu.xaml +++ b/Content.Client/_Goobstation/AmmoSelector/AmmoSelectorMenu.xaml @@ -6,7 +6,7 @@ HorizontalExpand="True" MinSize="450 450"> - diff --git a/Content.Client/_Goobstation/Clothing/ToggleableClothingRadialMenu.xaml b/Content.Client/_Goobstation/Clothing/ToggleableClothingRadialMenu.xaml index cfa0b93234e..83be4d0723d 100644 --- a/Content.Client/_Goobstation/Clothing/ToggleableClothingRadialMenu.xaml +++ b/Content.Client/_Goobstation/Clothing/ToggleableClothingRadialMenu.xaml @@ -7,7 +7,7 @@ MinSize="450 450"> - + From 52407f0df2bf17f3c85a1a68007286388a1dccbc Mon Sep 17 00:00:00 2001 From: sleepyyapril Date: Fri, 17 Oct 2025 16:45:47 -0300 Subject: [PATCH 10/25] Fix hardlight bow radial --- .../AmmoSelector/AmmoSelectorMenu.xaml | 12 --- .../AmmoSelector/AmmoSelectorMenu.xaml.cs | 93 ------------------- .../AmmoSelectorMenuBoundUserInterface.cs | 41 ++++++-- .../AmmoSelector/SelectableAmmoSystem.cs | 2 +- 4 files changed, 32 insertions(+), 116 deletions(-) delete mode 100644 Content.Client/_Goobstation/AmmoSelector/AmmoSelectorMenu.xaml delete mode 100644 Content.Client/_Goobstation/AmmoSelector/AmmoSelectorMenu.xaml.cs diff --git a/Content.Client/_Goobstation/AmmoSelector/AmmoSelectorMenu.xaml b/Content.Client/_Goobstation/AmmoSelector/AmmoSelectorMenu.xaml deleted file mode 100644 index 3e4487d0d4b..00000000000 --- a/Content.Client/_Goobstation/AmmoSelector/AmmoSelectorMenu.xaml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - diff --git a/Content.Client/_Goobstation/AmmoSelector/AmmoSelectorMenu.xaml.cs b/Content.Client/_Goobstation/AmmoSelector/AmmoSelectorMenu.xaml.cs deleted file mode 100644 index 2fde631a434..00000000000 --- a/Content.Client/_Goobstation/AmmoSelector/AmmoSelectorMenu.xaml.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System.Numerics; -using Content.Client.UserInterface.Controls; -using Content.Shared._Goobstation.Weapons.AmmoSelector; -using Robust.Client.AutoGenerated; -using Robust.Client.GameObjects; -using Robust.Client.Player; -using Robust.Client.UserInterface.Controls; -using Robust.Client.UserInterface.XAML; -using Robust.Shared.Prototypes; - -namespace Content.Client._Goobstation.AmmoSelector; - -[GenerateTypedNameReferences] -public sealed partial class AmmoSelectorMenu : RadialMenu -{ - [Dependency] private readonly EntityManager _entManager = default!; - [Dependency] private readonly IPrototypeManager _protoManager = default!; - [Dependency] private readonly IPlayerManager _playerManager = default!; - - private SpriteSystem _sprites; - - public event Action>? SendAmmoSelectorSystemMessageAction; - - private EntityUid _item; - - public AmmoSelectorMenu() - { - IoCManager.InjectDependencies(this); - RobustXamlLoader.Load(this); - _sprites = _entManager.System(); - } - - public void SetEntity(EntityUid uid) - { - _item = uid; - Refresh(); - } - - public void Refresh() - { - var main = FindControl("Main"); - main.RemoveAllChildren(); - - if (!_entManager.TryGetComponent(_item, out AmmoSelectorComponent? ammoSelector)) - return; - - foreach (var ammo in ammoSelector.Prototypes) - { - if (!_protoManager.TryIndex(ammo, out var prototype)) - continue; - - var button = new AmmoSelectorMenuButton - { - SetSize = new Vector2(64, 64), - ToolTip = Loc.GetString(prototype.Desc), - ProtoId = prototype.ID - }; - - var texture = new TextureRect - { - VerticalAlignment = VAlignment.Center, - HorizontalAlignment = HAlignment.Center, - Texture = _sprites.Frame0(prototype.Icon), - TextureScale = new Vector2(2f, 2f) - }; - - button.AddChild(texture); - main.AddChild(button); - } - - AddAmmoSelectorMenuButtonOnClickActions(main); - } - - private void AddAmmoSelectorMenuButtonOnClickActions(RadialContainer control) - { - foreach (var child in control.Children) - { - if (child is not AmmoSelectorMenuButton castChild) - continue; - - castChild.OnButtonUp += _ => - { - SendAmmoSelectorSystemMessageAction?.Invoke(castChild.ProtoId); - Close(); - }; - } - } -} - -public sealed class AmmoSelectorMenuButton : RadialMenuButton -{ - public ProtoId ProtoId { get; set; } -} diff --git a/Content.Client/_Goobstation/AmmoSelector/AmmoSelectorMenuBoundUserInterface.cs b/Content.Client/_Goobstation/AmmoSelector/AmmoSelectorMenuBoundUserInterface.cs index 400a785ebd2..e74fabd0035 100644 --- a/Content.Client/_Goobstation/AmmoSelector/AmmoSelectorMenuBoundUserInterface.cs +++ b/Content.Client/_Goobstation/AmmoSelector/AmmoSelectorMenuBoundUserInterface.cs @@ -1,3 +1,4 @@ +using Content.Client.UserInterface.Controls; using Content.Shared._Goobstation.Weapons.AmmoSelector; using JetBrains.Annotations; using Robust.Client.Graphics; @@ -10,10 +11,9 @@ namespace Content.Client._Goobstation.AmmoSelector; [UsedImplicitly] public sealed class AmmoSelectorMenuBoundUserInterface : BoundUserInterface { - [Dependency] private readonly IClyde _displayManager = default!; - [Dependency] private readonly IInputManager _inputManager = default!; + [Dependency] private readonly IPrototypeManager _protoMan = default!; - private AmmoSelectorMenu? _menu; + private SimpleRadialMenu? _menu; public AmmoSelectorMenuBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { @@ -24,16 +24,37 @@ protected override void Open() { base.Open(); - _menu = this.CreateWindow(); - _menu.SetEntity(Owner); - _menu.SendAmmoSelectorSystemMessageAction += SendAmmoSelectorSystemMessage; + if (!EntMan.TryGetComponent(Owner, out var ammoSelector)) + return; - var vpSize = _displayManager.ScreenSize; - _menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize); + var actions = GetAmmoSelectorActions(ammoSelector.Prototypes); + + _menu = this.CreateWindow(); + _menu.Track(Owner); + _menu.SetButtons(actions); + _menu.OpenOverMouseScreenPosition(); } - public void SendAmmoSelectorSystemMessage(ProtoId protoId) + private IEnumerable>> GetAmmoSelectorActions(HashSet> protoIds) { - SendPredictedMessage(new AmmoSelectedMessage(protoId)); + var actions = new List>>(); + + foreach (var selectableAmmoId in protoIds) + { + if (!_protoMan.TryIndex(selectableAmmoId, out var selectableAmmo)) + continue; + + var action = new RadialMenuActionOption>(OnAmmoSelected, selectableAmmoId) + { + ToolTip = selectableAmmo.Desc, + IconSpecifier = RadialMenuIconSpecifier.With(selectableAmmo.Icon) + }; + + actions.Add(action); + } + + return actions; } + + private void OnAmmoSelected(ProtoId protoId) => SendPredictedMessage(new AmmoSelectedMessage(protoId)); } diff --git a/Content.Shared/_Goobstation/Weapons/AmmoSelector/SelectableAmmoSystem.cs b/Content.Shared/_Goobstation/Weapons/AmmoSelector/SelectableAmmoSystem.cs index 121b05a6107..18483e447ff 100644 --- a/Content.Shared/_Goobstation/Weapons/AmmoSelector/SelectableAmmoSystem.cs +++ b/Content.Shared/_Goobstation/Weapons/AmmoSelector/SelectableAmmoSystem.cs @@ -55,7 +55,7 @@ private void OnMessage(Entity ent, ref AmmoSelectedMessag var name = GetProviderProtoName(ent); if (name != null) - _popup.PopupClient(Loc.GetString("mode-selected", ("mode", name)), ent, args.Actor); + _popup.PopupClient(Loc.GetString("ammo-selector-mode-selected", ("mode", name)), ent, args.Actor); _audio.PlayPredicted(ent.Comp.SoundSelect, ent, args.Actor); } From b8471dc7d07843ce9bd8f3689f23d4878ff54987 Mon Sep 17 00:00:00 2001 From: sleepyyapril Date: Fri, 17 Oct 2025 17:47:46 -0300 Subject: [PATCH 11/25] card stack fix --- .../Cards/Hand/UI/CardHandMenu.xaml | 12 --- .../Cards/Hand/UI/CardHandMenu.xaml.cs | 87 ------------------- .../Hand/UI/CardHandMenuBoundUserInterface.cs | 64 +++++++++++--- 3 files changed, 50 insertions(+), 113 deletions(-) delete mode 100644 Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenu.xaml delete mode 100644 Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenu.xaml.cs diff --git a/Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenu.xaml b/Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenu.xaml deleted file mode 100644 index 5d2760c310b..00000000000 --- a/Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenu.xaml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - diff --git a/Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenu.xaml.cs b/Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenu.xaml.cs deleted file mode 100644 index 99f7dd67e79..00000000000 --- a/Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenu.xaml.cs +++ /dev/null @@ -1,87 +0,0 @@ -using Content.Client.UserInterface.Controls; -using Content.Shared.Popups; -using Robust.Client.AutoGenerated; -using Robust.Client.GameObjects; -using Robust.Client.Player; -using Robust.Client.UserInterface.Controls; -using Robust.Client.UserInterface.XAML; -using System.Numerics; -using Content.Shared._EstacaoPirata.Cards.Card; -using Content.Shared._EstacaoPirata.Cards.Stack; - -namespace Content.Client._EstacaoPirata.Cards.Hand.UI; - -[GenerateTypedNameReferences] -public sealed partial class CardHandMenu : RadialMenu -{ - [Dependency] private readonly EntityManager _entManager = default!; - [Dependency] private readonly IPlayerManager _playerManager = default!; - - public event Action? CardHandDrawMessageAction; - - public CardHandMenu(EntityUid owner, CardHandMenuBoundUserInterface bui) - { - IoCManager.InjectDependencies(this); - RobustXamlLoader.Load(this); - - // Find the main radial container - var main = FindControl("Main"); - - if (!_entManager.TryGetComponent(owner, out var stack)) - return; - - foreach (var card in stack.Cards) - { - if (_playerManager.LocalSession == null - || !_entManager.TryGetComponent(card, out var cardComp)) - return; - - string cardName; - if (cardComp.Flipped && _entManager.TryGetComponent(card, out var metadata)) - cardName = metadata.EntityName; - else - cardName = Loc.GetString(cardComp.Name); - - var button = new CardMenuButton - { - StyleClasses = { "RadialMenuButton" }, - SetSize = new Vector2(64f, 64f), - ToolTip = cardName, - }; - - if (_entManager.TryGetComponent(card, out var sprite)) - { - if (sprite.Icon == null) - continue; - - var tex = new TextureRect() - { - VerticalAlignment = VAlignment.Center, - HorizontalAlignment = HAlignment.Center, - Texture = sprite.Icon?.Default, - TextureScale = new Vector2(2f, 2f), - }; - - button.AddChild(tex); - } - - main.AddChild(button); - - button.OnButtonUp += _ => - { - CardHandDrawMessageAction?.Invoke(_entManager.GetNetEntity(card)); - Close(); - }; - } - - CardHandDrawMessageAction += bui.SendCardHandDrawMessage; - } -} - -public sealed class CardMenuButton : RadialMenuButton -{ - public CardMenuButton() - { - - } -} diff --git a/Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenuBoundUserInterface.cs b/Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenuBoundUserInterface.cs index 5c8e3022e1f..b463d74a298 100644 --- a/Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenuBoundUserInterface.cs +++ b/Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenuBoundUserInterface.cs @@ -1,17 +1,24 @@ using Content.Shared._EstacaoPirata.Cards.Hand; +using Content.Client.UserInterface.Controls; +using Content.Shared._EstacaoPirata.Cards.Card; +using Content.Shared._EstacaoPirata.Cards.Hand; +using Content.Shared._EstacaoPirata.Cards.Stack; using JetBrains.Annotations; +using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.Input; +using Robust.Client.Player; +using Robust.Client.UserInterface; +using Robust.Shared.Prototypes; namespace Content.Client._EstacaoPirata.Cards.Hand.UI; [UsedImplicitly] public sealed class CardHandMenuBoundUserInterface : BoundUserInterface { - [Dependency] private readonly IClyde _displayManager = default!; - [Dependency] private readonly IInputManager _inputManager = default!; + [Dependency] private readonly IPlayerManager _playerMan = default!; - private CardHandMenu? _menu; + private SimpleRadialMenu? _menu; public CardHandMenuBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { @@ -22,21 +29,50 @@ protected override void Open() { base.Open(); - _menu = new(Owner, this); - _menu.OnClose += Close; + if (!EntMan.TryGetComponent(Owner, out var stack)) + return; - // Open the menu, centered on the mouse - var vpSize = _displayManager.ScreenSize; - _menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize); - } + var actions = GetCardStackActions(stack); - public void SendCardHandDrawMessage(NetEntity e) => SendMessage(new CardHandDrawMessage(e)); + _menu = this.CreateWindow(); + _menu.Track(Owner); + _menu.SetButtons(actions); + _menu.OpenOverMouseScreenPosition(); + } - protected override void Dispose(bool disposing) + private IEnumerable> GetCardStackActions(CardStackComponent stack) { - base.Dispose(disposing); - if (!disposing) return; + List> actions = new(); + + foreach (var card in stack.Cards) + { + if (_playerMan.LocalSession == null + || !EntMan.TryGetComponent(card, out var cardComp)) + continue; + + var networkedCard = EntMan.GetNetEntity(card); + string cardName; - _menu?.Dispose(); + if (cardComp.Flipped && EntMan.TryGetComponent(card, out var metadata)) + cardName = metadata.EntityName; + else + cardName = Loc.GetString(cardComp.Name); + + if (!EntMan.TryGetComponent(card, out var sprite) || sprite.Icon == null) + continue; + + var iconSpecifier = RadialMenuIconSpecifier.With(card); + var action = new RadialMenuActionOption(SendCardHandDrawMessage, networkedCard) + { + IconSpecifier = iconSpecifier, + ToolTip = cardName + }; + + actions.Add(action); + } + + return actions; } + + public void SendCardHandDrawMessage(NetEntity e) => SendMessage(new CardHandDrawMessage(e)); } From a1be6ff83c5cd2e0025b24c65e3d76585a9b3597 Mon Sep 17 00:00:00 2001 From: sleepyyapril Date: Fri, 17 Oct 2025 18:23:34 -0300 Subject: [PATCH 12/25] Modsuit fix --- .../ToggleableClothingBoundUserInterface.cs | 56 ++++++++-- .../ToggleableClothingRadialMenu.xaml | 13 --- .../ToggleableClothingRadialMenu.xaml.cs | 103 ------------------ 3 files changed, 44 insertions(+), 128 deletions(-) delete mode 100644 Content.Client/_Goobstation/Clothing/ToggleableClothingRadialMenu.xaml delete mode 100644 Content.Client/_Goobstation/Clothing/ToggleableClothingRadialMenu.xaml.cs diff --git a/Content.Client/_Goobstation/Clothing/ToggleableClothingBoundUserInterface.cs b/Content.Client/_Goobstation/Clothing/ToggleableClothingBoundUserInterface.cs index 55407f78347..1d6595fce68 100644 --- a/Content.Client/_Goobstation/Clothing/ToggleableClothingBoundUserInterface.cs +++ b/Content.Client/_Goobstation/Clothing/ToggleableClothingBoundUserInterface.cs @@ -1,39 +1,71 @@ +using Content.Client.UserInterface.Controls; using Content.Shared.Clothing.Components; using Robust.Client.Graphics; using Robust.Client.Input; using Robust.Client.UserInterface; +using Robust.Shared.Containers; +using Robust.Shared.Prototypes; + namespace Content.Client._Goobstation.Clothing; public sealed class ToggleableClothingBoundUserInterface : BoundUserInterface { - [Dependency] private readonly IClyde _displayManager = default!; - [Dependency] private readonly IInputManager _inputManager = default!; + [Dependency] private readonly IPrototypeManager _protoMan = default!; - private IEntityManager _entityManager; - private ToggleableClothingRadialMenu? _menu; + private SimpleRadialMenu? _menu; public ToggleableClothingBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { IoCManager.InjectDependencies(this); - _entityManager = IoCManager.Resolve(); } protected override void Open() { base.Open(); - _menu = this.CreateWindow(); - _menu.SetEntity(Owner); - _menu.SendToggleClothingMessageAction += SendToggleableClothingMessage; + if (!EntMan.TryGetComponent(Owner, out var clothing) + || clothing.Container is not { } clothingContainer) + return; + + var actions = GetToggleableClothingActions(clothing, clothingContainer); + + _menu = this.CreateWindow(); + _menu.Track(Owner); + _menu.SetButtons(actions); + _menu.OpenOverMouseScreenPosition(); + } + + private IEnumerable> GetToggleableClothingActions( + ToggleableClothingComponent clothing, + Container clothingContainer) + { + var actions = new List>(); + + foreach (var pair in clothing.ClothingUids) + { + // Change tooltip text if attached clothing is toggle/untoggled + var tooltipText = Loc.GetString(clothing.UnattachTooltip); + + if (clothingContainer.Contains(pair.Key)) + tooltipText = Loc.GetString(clothing.AttachTooltip); + + var netEntity = EntMan.GetNetEntity(Owner); + var action = new RadialMenuActionOption(SendToggleableClothingMessage, netEntity) + { + ToolTip = tooltipText, + IconSpecifier = RadialMenuIconSpecifier.With(pair.Key) + }; + + actions.Add(action); + } - var vpSize = _displayManager.ScreenSize; - _menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize); + return actions; } - private void SendToggleableClothingMessage(EntityUid uid) + private void SendToggleableClothingMessage(NetEntity uid) { - var message = new ToggleableClothingUiMessage(_entityManager.GetNetEntity(uid)); + var message = new ToggleableClothingUiMessage(uid); SendPredictedMessage(message); } } diff --git a/Content.Client/_Goobstation/Clothing/ToggleableClothingRadialMenu.xaml b/Content.Client/_Goobstation/Clothing/ToggleableClothingRadialMenu.xaml deleted file mode 100644 index 83be4d0723d..00000000000 --- a/Content.Client/_Goobstation/Clothing/ToggleableClothingRadialMenu.xaml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - diff --git a/Content.Client/_Goobstation/Clothing/ToggleableClothingRadialMenu.xaml.cs b/Content.Client/_Goobstation/Clothing/ToggleableClothingRadialMenu.xaml.cs deleted file mode 100644 index 681e3c9b5e0..00000000000 --- a/Content.Client/_Goobstation/Clothing/ToggleableClothingRadialMenu.xaml.cs +++ /dev/null @@ -1,103 +0,0 @@ -using Content.Client.UserInterface.Controls; -using Content.Shared.Clothing.Components; -using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; -using Robust.Client.UserInterface.XAML; -using Robust.Shared.Prototypes; -using System.Numerics; - -namespace Content.Client._Goobstation.Clothing; - -public sealed partial class ToggleableClothingRadialMenu : RadialMenu -{ - [Dependency] private readonly EntityManager _entityManager = default!; - - public event Action? SendToggleClothingMessageAction; - - public EntityUid Entity { get; set; } - - public ToggleableClothingRadialMenu() - { - IoCManager.InjectDependencies(this); - RobustXamlLoader.Load(this); - } - - public void SetEntity(EntityUid uid) - { - Entity = uid; - RefreshUI(); - } - - public void RefreshUI() - { - var main = FindControl("Main"); - - if (!_entityManager.TryGetComponent(Entity, out var clothing)) - return; - - var clothingContainer = clothing.Container; - - if (clothingContainer == null) - return; - - foreach (var attached in clothing.ClothingUids) - { - // Change tooltip text if attached clothing is toggle/untoggled - var tooltipText = Loc.GetString("toggleable-clothing-unattach-tooltip"); - - if (clothingContainer.Contains(attached.Key)) - tooltipText = Loc.GetString("toggleable-clothing-attach-tooltip"); - - var button = new ToggleableClothingRadialMenuButton() - { - StyleClasses = { "RadialMenuButton" }, - SetSize = new Vector2(64, 64), - ToolTip = tooltipText, - AttachedClothingId = attached.Key - }; - - var spriteView = new SpriteView() - { - SetSize = new Vector2(48, 48), - VerticalAlignment = VAlignment.Center, - HorizontalAlignment = HAlignment.Center, - Stretch = SpriteView.StretchMode.Fill - }; - - spriteView.SetEntity(attached.Key); - - button.AddChild(spriteView); - main.AddChild(button); - } - - AddToggleableClothingMenuButtonOnClickAction(main); - } - - private void AddToggleableClothingMenuButtonOnClickAction(Control control) - { - var mainControl = control as RadialContainer; - - if (mainControl == null) - return; - - foreach (var child in mainControl.Children) - { - var castChild = child as ToggleableClothingRadialMenuButton; - - if (castChild == null) - return; - - castChild.OnButtonDown += _ => - { - SendToggleClothingMessageAction?.Invoke(castChild.AttachedClothingId); - mainControl.DisposeAllChildren(); - RefreshUI(); - }; - } - } -} - -public sealed class ToggleableClothingRadialMenuButton : RadialMenuButton -{ - public EntityUid AttachedClothingId { get; set; } -} From 7ca4a0ffd440132bae668e8b27de02413f60891d Mon Sep 17 00:00:00 2001 From: sleepyyapril Date: Fri, 17 Oct 2025 18:23:39 -0300 Subject: [PATCH 13/25] RPD fix --- Content.Client/RCD/RCDMenuBoundUserInterface.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Content.Client/RCD/RCDMenuBoundUserInterface.cs b/Content.Client/RCD/RCDMenuBoundUserInterface.cs index 3f847c8beb6..47afa313d76 100644 --- a/Content.Client/RCD/RCDMenuBoundUserInterface.cs +++ b/Content.Client/RCD/RCDMenuBoundUserInterface.cs @@ -17,13 +17,17 @@ public sealed class RCDMenuBoundUserInterface : BoundUserInterface private const string TopLevelActionCategory = "Main"; private static readonly Dictionary PrototypesGroupingInfo - = new Dictionary + = new() { ["WallsAndFlooring"] = ("rcd-component-walls-and-flooring", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RCD/walls_and_flooring.png"))), ["WindowsAndGrilles"] = ("rcd-component-windows-and-grilles", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RCD/windows_and_grilles.png"))), ["Airlocks"] = ("rcd-component-airlocks", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RCD/airlocks.png"))), ["Electrical"] = ("rcd-component-electrical", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RCD/multicoil.png"))), ["Lighting"] = ("rcd-component-lighting", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RCD/lighting.png"))), + ["Piping"] = ("rcd-component-piping", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RPD/tjunction.png"))), + ["AtmosphericUtility"] = ("rcd-component-atmosphericutility", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RPD/radiator.png"))), + ["PumpsValves"] = ("rcd-component-pumps", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RPD/pump_pressure.png"))), + ["Vents"] = ("rcd-component-vents", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RPD/vent_passive.png"))) }; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; @@ -43,9 +47,10 @@ protected override void Open() if (!EntMan.TryGetComponent(Owner, out var rcd)) return; + var models = ConvertToButtons(rcd.AvailablePrototypes); + _menu = this.CreateWindow(); _menu.Track(Owner); - var models = ConvertToButtons(rcd.AvailablePrototypes); _menu.SetButtons(models); _menu.OpenOverMouseScreenPosition(); From 5477d62259900ec82cedc18bace01ab77248ba4b Mon Sep 17 00:00:00 2001 From: sleepyyapril Date: Fri, 17 Oct 2025 18:52:43 -0300 Subject: [PATCH 14/25] use proper tool name (it's not always an RCD) --- Content.Client/RCD/RCDMenuBoundUserInterface.cs | 5 ++--- Content.Shared/RCD/Systems/RCDSystem.cs | 4 ++-- Resources/Locale/en-US/rcd/components/rcd-component.ftl | 8 ++++---- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Content.Client/RCD/RCDMenuBoundUserInterface.cs b/Content.Client/RCD/RCDMenuBoundUserInterface.cs index 47afa313d76..f80706544bd 100644 --- a/Content.Client/RCD/RCDMenuBoundUserInterface.cs +++ b/Content.Client/RCD/RCDMenuBoundUserInterface.cs @@ -119,11 +119,10 @@ private void HandleMenuOptionClick(RCDPrototype proto) // after this message is sent, which will stop the server from receiving it SendMessage(new RCDSystemMessage(proto.ID)); - if (_playerManager.LocalSession?.AttachedEntity == null) return; - var msg = Loc.GetString("rcd-component-change-mode", ("mode", Loc.GetString(proto.SetName))); + var msg = Loc.GetString("rcd-component-change-mode", ("tool", Owner), ("mode", Loc.GetString(proto.SetName))); if (proto.Mode is RcdMode.ConstructTile or RcdMode.ConstructObject) { @@ -135,7 +134,7 @@ private void HandleMenuOptionClick(RCDPrototype proto) name = entProto.Name; } - msg = Loc.GetString("rcd-component-change-build-mode", ("name", name)); + msg = Loc.GetString("rcd-component-change-build-mode", ("tool", Owner), ("name", name)); } // Popup message diff --git a/Content.Shared/RCD/Systems/RCDSystem.cs b/Content.Shared/RCD/Systems/RCDSystem.cs index 3cd51c16262..0845f67459b 100644 --- a/Content.Shared/RCD/Systems/RCDSystem.cs +++ b/Content.Shared/RCD/Systems/RCDSystem.cs @@ -396,7 +396,7 @@ public bool IsRCDOperationStillValid(EntityUid uid, RCDComponent component, MapG if (charges == 0) { if (popMsgs) - _popup.PopupClient(Loc.GetString("rcd-component-no-ammo-message"), uid, user); + _popup.PopupClient(Loc.GetString("rcd-component-no-ammo-message", ("tool", uid)), uid, user); return false; } @@ -404,7 +404,7 @@ public bool IsRCDOperationStillValid(EntityUid uid, RCDComponent component, MapG if (prototype.Cost > charges) { if (popMsgs) - _popup.PopupClient(Loc.GetString("rcd-component-insufficient-ammo-message"), uid, user); + _popup.PopupClient(Loc.GetString("rcd-component-insufficient-ammo-message", ("tool", uid)), uid, user); return false; } diff --git a/Resources/Locale/en-US/rcd/components/rcd-component.ftl b/Resources/Locale/en-US/rcd/components/rcd-component.ftl index 6e8682eed75..f0714ce7ea5 100644 --- a/Resources/Locale/en-US/rcd/components/rcd-component.ftl +++ b/Resources/Locale/en-US/rcd/components/rcd-component.ftl @@ -8,12 +8,12 @@ rcd-component-examine-build-details = It's currently set to build {MAKEPLURAL($n ### Interaction Messages # Mode change -rcd-component-change-mode = The RCD is now set to '{$mode}' mode. -rcd-component-change-build-mode = The RCD is now set to build {MAKEPLURAL($name)}. +rcd-component-change-mode = {CAPITALIZE(THE($tool))} is now set to '{$mode}' mode. +rcd-component-change-build-mode = {CAPITALIZE(THE($tool))} is now set to build {MAKEPLURAL($name)}. # Ammo count -rcd-component-no-ammo-message = The RCD has run out of charges! -rcd-component-insufficient-ammo-message = The RCD doesn't have enough charges left! +rcd-component-no-ammo-message = {CAPITALIZE(THE($tool))} has run out of charges! +rcd-component-insufficient-ammo-message = {CAPITALIZE(THE($tool))} doesn't have enough charges left! # Deconstruction rcd-component-tile-indestructible-message = That tile can't be destructed! From fff29381d6e9734ef515551e1351842f28fa6628 Mon Sep 17 00:00:00 2001 From: sleepyyapril Date: Fri, 17 Oct 2025 18:57:41 -0300 Subject: [PATCH 15/25] cleanup + fix modsuit equip --- .../Clothing/ToggleableClothingBoundUserInterface.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Content.Client/_Goobstation/Clothing/ToggleableClothingBoundUserInterface.cs b/Content.Client/_Goobstation/Clothing/ToggleableClothingBoundUserInterface.cs index 1d6595fce68..9e5cb3a9915 100644 --- a/Content.Client/_Goobstation/Clothing/ToggleableClothingBoundUserInterface.cs +++ b/Content.Client/_Goobstation/Clothing/ToggleableClothingBoundUserInterface.cs @@ -45,12 +45,9 @@ private IEnumerable> GetToggleableClothingActi foreach (var pair in clothing.ClothingUids) { // Change tooltip text if attached clothing is toggle/untoggled - var tooltipText = Loc.GetString(clothing.UnattachTooltip); - - if (clothingContainer.Contains(pair.Key)) - tooltipText = Loc.GetString(clothing.AttachTooltip); - - var netEntity = EntMan.GetNetEntity(Owner); + var attached = clothingContainer.Contains(pair.Key) ? clothing.AttachTooltip : clothing.UnattachTooltip; + var tooltipText = Loc.GetString(attached); + var netEntity = EntMan.GetNetEntity(pair.Key); var action = new RadialMenuActionOption(SendToggleableClothingMessage, netEntity) { ToolTip = tooltipText, From ccbef5f48d87dae0e006b68e7ccaea581f8a6101 Mon Sep 17 00:00:00 2001 From: sleepyyapril Date: Fri, 17 Oct 2025 19:12:26 -0300 Subject: [PATCH 16/25] use prototype to avoid disappearing on entity change --- .../Clothing/ToggleableClothingBoundUserInterface.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Content.Client/_Goobstation/Clothing/ToggleableClothingBoundUserInterface.cs b/Content.Client/_Goobstation/Clothing/ToggleableClothingBoundUserInterface.cs index 9e5cb3a9915..d920e986e47 100644 --- a/Content.Client/_Goobstation/Clothing/ToggleableClothingBoundUserInterface.cs +++ b/Content.Client/_Goobstation/Clothing/ToggleableClothingBoundUserInterface.cs @@ -44,6 +44,9 @@ private IEnumerable> GetToggleableClothingActi foreach (var pair in clothing.ClothingUids) { + if (!EntMan.TryGetComponent(pair.Key, out MetaDataComponent? metaData) || metaData.EntityPrototype == null) + continue; + // Change tooltip text if attached clothing is toggle/untoggled var attached = clothingContainer.Contains(pair.Key) ? clothing.AttachTooltip : clothing.UnattachTooltip; var tooltipText = Loc.GetString(attached); @@ -51,7 +54,7 @@ private IEnumerable> GetToggleableClothingActi var action = new RadialMenuActionOption(SendToggleableClothingMessage, netEntity) { ToolTip = tooltipText, - IconSpecifier = RadialMenuIconSpecifier.With(pair.Key) + IconSpecifier = RadialMenuIconSpecifier.With(metaData.EntityPrototype) }; actions.Add(action); From 0af54e621fcc7d2c5231c4201fa5463d8a2b7b16 Mon Sep 17 00:00:00 2001 From: SquishyEmmaaaa <235807504+SquishyEmmaaaa@users.noreply.github.com> Date: Sat, 11 Jul 2026 06:37:55 -0700 Subject: [PATCH 17/25] Getting rid of clothing interface errors. I think Den has the renameable in character select clothes. --- .../Clothing/ToggleableClothingBoundUserInterface.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Content.Client/_Goobstation/Clothing/ToggleableClothingBoundUserInterface.cs b/Content.Client/_Goobstation/Clothing/ToggleableClothingBoundUserInterface.cs index d920e986e47..cd13b0ea60f 100644 --- a/Content.Client/_Goobstation/Clothing/ToggleableClothingBoundUserInterface.cs +++ b/Content.Client/_Goobstation/Clothing/ToggleableClothingBoundUserInterface.cs @@ -47,13 +47,9 @@ private IEnumerable> GetToggleableClothingActi if (!EntMan.TryGetComponent(pair.Key, out MetaDataComponent? metaData) || metaData.EntityPrototype == null) continue; - // Change tooltip text if attached clothing is toggle/untoggled - var attached = clothingContainer.Contains(pair.Key) ? clothing.AttachTooltip : clothing.UnattachTooltip; - var tooltipText = Loc.GetString(attached); var netEntity = EntMan.GetNetEntity(pair.Key); var action = new RadialMenuActionOption(SendToggleableClothingMessage, netEntity) { - ToolTip = tooltipText, IconSpecifier = RadialMenuIconSpecifier.With(metaData.EntityPrototype) }; From 1a26425f57a30e26b4440f7bce83c972fa6c6052 Mon Sep 17 00:00:00 2001 From: SquishyEmmaaaa <235807504+SquishyEmmaaaa@users.noreply.github.com> Date: Sat, 11 Jul 2026 06:38:18 -0700 Subject: [PATCH 18/25] Remove duplicate using namespace line --- .../Cards/Hand/UI/CardHandMenuBoundUserInterface.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenuBoundUserInterface.cs b/Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenuBoundUserInterface.cs index b463d74a298..66da057cacc 100644 --- a/Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenuBoundUserInterface.cs +++ b/Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenuBoundUserInterface.cs @@ -1,4 +1,3 @@ -using Content.Shared._EstacaoPirata.Cards.Hand; using Content.Client.UserInterface.Controls; using Content.Shared._EstacaoPirata.Cards.Card; using Content.Shared._EstacaoPirata.Cards.Hand; From 9030ba48430fbe6286917594a7754823b1356ff6 Mon Sep 17 00:00:00 2001 From: SquishyEmmaaaa <235807504+SquishyEmmaaaa@users.noreply.github.com> Date: Sat, 11 Jul 2026 06:45:08 -0700 Subject: [PATCH 19/25] Thingies? --- Content.Client/RPD/RPDMenu.xaml.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Content.Client/RPD/RPDMenu.xaml.cs b/Content.Client/RPD/RPDMenu.xaml.cs index 966915e5a6b..4d7103a09ac 100644 --- a/Content.Client/RPD/RPDMenu.xaml.cs +++ b/Content.Client/RPD/RPDMenu.xaml.cs @@ -1,9 +1,3 @@ -// SPDX-FileCopyrightText: 2025 Steve -// SPDX-FileCopyrightText: 2025 marc-pelletier <113944176+marc-pelletier@users.noreply.github.com> -// SPDX-FileCopyrightText: 2026 Triad Sector -// -// SPDX-License-Identifier: AGPL-3.0-or-later - using System.Linq; using System.Numerics; using Content.Client.RCD; From 7dc39728b37831f6c29ef08449c442c30930e071 Mon Sep 17 00:00:00 2001 From: SquishyEmmaaaa <235807504+SquishyEmmaaaa@users.noreply.github.com> Date: Sat, 11 Jul 2026 06:56:57 -0700 Subject: [PATCH 20/25] Changes two names of RCDMenu to RPDMenu to fix errors - Using the letter P for the pipe device is important it seems! Removes the virtual tag from the Radial Menu Button Base, to match parity with Wizden, and fix a virtual classes cannot be abstract error. --- Content.Client/RPD/RPDMenu.xaml.cs | 4 ++-- Content.Client/UserInterface/Controls/RadialMenu.cs | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Content.Client/RPD/RPDMenu.xaml.cs b/Content.Client/RPD/RPDMenu.xaml.cs index 4d7103a09ac..4eacbb2c07a 100644 --- a/Content.Client/RPD/RPDMenu.xaml.cs +++ b/Content.Client/RPD/RPDMenu.xaml.cs @@ -23,7 +23,7 @@ public sealed partial class RPDMenu : RadialMenu { [Dependency] private readonly ILocalizationManager _locManager = default!; - private readonly RCDMenu _rcdMenu; + private readonly RPDMenu _rcdMenu; private string? _selectedColor; public event Action? ColorSelected; @@ -38,7 +38,7 @@ public RPDMenu() { IoCManager.InjectDependencies(this); RobustXamlLoader.Load(this); - _rcdMenu = new RCDMenu(); + _rcdMenu = new RPDMenu(); var rcdHolder = FindControl("RCDMenuHolder"); rcdHolder.AddChild(_rcdMenu); _rcdMenu.OnClose += Close; diff --git a/Content.Client/UserInterface/Controls/RadialMenu.cs b/Content.Client/UserInterface/Controls/RadialMenu.cs index 959a60ef4f8..0cc207dd89d 100644 --- a/Content.Client/UserInterface/Controls/RadialMenu.cs +++ b/Content.Client/UserInterface/Controls/RadialMenu.cs @@ -228,7 +228,6 @@ public void ReturnToPreviousLayer() /// Base class for radial menu buttons. Excludes all actions except clicks and alt-clicks /// from interactions. /// -[Virtual] public abstract class RadialMenuButtonBase : BaseButton { /// From 54a5fe28d09df7957f6019c8c6df08aecf2bc1f8 Mon Sep 17 00:00:00 2001 From: SquishyEmmaaaa <235807504+SquishyEmmaaaa@users.noreply.github.com> Date: Sat, 11 Jul 2026 07:25:23 -0700 Subject: [PATCH 21/25] Huh. How was that missing? Adds the keybind down to both fields. Maybe this will fix the stack overflow? We don't have the IsClickOrAltClick function. --- .../UserInterface/Controls/RadialMenu.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Content.Client/UserInterface/Controls/RadialMenu.cs b/Content.Client/UserInterface/Controls/RadialMenu.cs index 0cc207dd89d..25861e0a048 100644 --- a/Content.Client/UserInterface/Controls/RadialMenu.cs +++ b/Content.Client/UserInterface/Controls/RadialMenu.cs @@ -245,6 +245,16 @@ protected override void KeyBindUp(GUIBoundKeyEventArgs args) base.KeyBindUp(args); } } + + /// + protected override void KeyBindDown(GUIBoundKeyEventArgs args) + { + if (args.Function == EngineKeyFunctions.UIClick + || args.Function == ContentKeyFunctions.AltActivateItemInWorld) + { + base.KeyBindDown(args); + } + } } /// @@ -291,6 +301,16 @@ protected override void KeyBindUp(GUIBoundKeyEventArgs args) base.KeyBindUp(args); } } + + /// + protected override void KeyBindDown(GUIBoundKeyEventArgs args) + { + if (args.Function == EngineKeyFunctions.UIClick + || args.Function == ContentKeyFunctions.AltActivateItemInWorld) + { + base.KeyBindDown(args); + } + } } /// From 065e6ac8f1550f1ba2b52941720abfdfb4f3f0e8 Mon Sep 17 00:00:00 2001 From: SquishyEmmaaaa <235807504+SquishyEmmaaaa@users.noreply.github.com> Date: Wed, 29 Jul 2026 16:32:42 -0700 Subject: [PATCH 22/25] Revert "RPD fix" This reverts commit 7ca4a0ffd440132bae668e8b27de02413f60891d. --- Content.Client/RCD/RCDMenuBoundUserInterface.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Content.Client/RCD/RCDMenuBoundUserInterface.cs b/Content.Client/RCD/RCDMenuBoundUserInterface.cs index f80706544bd..1445ab3b17b 100644 --- a/Content.Client/RCD/RCDMenuBoundUserInterface.cs +++ b/Content.Client/RCD/RCDMenuBoundUserInterface.cs @@ -17,17 +17,13 @@ public sealed class RCDMenuBoundUserInterface : BoundUserInterface private const string TopLevelActionCategory = "Main"; private static readonly Dictionary PrototypesGroupingInfo - = new() + = new Dictionary { ["WallsAndFlooring"] = ("rcd-component-walls-and-flooring", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RCD/walls_and_flooring.png"))), ["WindowsAndGrilles"] = ("rcd-component-windows-and-grilles", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RCD/windows_and_grilles.png"))), ["Airlocks"] = ("rcd-component-airlocks", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RCD/airlocks.png"))), ["Electrical"] = ("rcd-component-electrical", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RCD/multicoil.png"))), ["Lighting"] = ("rcd-component-lighting", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RCD/lighting.png"))), - ["Piping"] = ("rcd-component-piping", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RPD/tjunction.png"))), - ["AtmosphericUtility"] = ("rcd-component-atmosphericutility", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RPD/radiator.png"))), - ["PumpsValves"] = ("rcd-component-pumps", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RPD/pump_pressure.png"))), - ["Vents"] = ("rcd-component-vents", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RPD/vent_passive.png"))) }; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; @@ -47,10 +43,9 @@ protected override void Open() if (!EntMan.TryGetComponent(Owner, out var rcd)) return; - var models = ConvertToButtons(rcd.AvailablePrototypes); - _menu = this.CreateWindow(); _menu.Track(Owner); + var models = ConvertToButtons(rcd.AvailablePrototypes); _menu.SetButtons(models); _menu.OpenOverMouseScreenPosition(); From b32b35e647f23c3694df1bec7fe3db6b2170f255 Mon Sep 17 00:00:00 2001 From: SquishyEmmaaaa <235807504+SquishyEmmaaaa@users.noreply.github.com> Date: Wed, 29 Jul 2026 16:34:56 -0700 Subject: [PATCH 23/25] Revert "Huh. How was that missing?" This reverts commit 54a5fe28d09df7957f6019c8c6df08aecf2bc1f8. Apparently we are bootstrapping mirroring off the KeyBindDown, so that makes more sense now. --- .../UserInterface/Controls/RadialMenu.cs | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/Content.Client/UserInterface/Controls/RadialMenu.cs b/Content.Client/UserInterface/Controls/RadialMenu.cs index 25861e0a048..0cc207dd89d 100644 --- a/Content.Client/UserInterface/Controls/RadialMenu.cs +++ b/Content.Client/UserInterface/Controls/RadialMenu.cs @@ -245,16 +245,6 @@ protected override void KeyBindUp(GUIBoundKeyEventArgs args) base.KeyBindUp(args); } } - - /// - protected override void KeyBindDown(GUIBoundKeyEventArgs args) - { - if (args.Function == EngineKeyFunctions.UIClick - || args.Function == ContentKeyFunctions.AltActivateItemInWorld) - { - base.KeyBindDown(args); - } - } } /// @@ -301,16 +291,6 @@ protected override void KeyBindUp(GUIBoundKeyEventArgs args) base.KeyBindUp(args); } } - - /// - protected override void KeyBindDown(GUIBoundKeyEventArgs args) - { - if (args.Function == EngineKeyFunctions.UIClick - || args.Function == ContentKeyFunctions.AltActivateItemInWorld) - { - base.KeyBindDown(args); - } - } } /// From af6fc8c2a0f2e3179c30b578addb82f8a8356a62 Mon Sep 17 00:00:00 2001 From: rebaserHEAD <38984539+rebaserHEAD@users.noreply.github.com> Date: Wed, 29 Jul 2026 20:51:29 -0400 Subject: [PATCH 24/25] Rebuild RPD menu on SimpleRadialMenu The radial refactor deleted RCDMenu, which the RPD picker embedded for its category radial. The interim rename made RPDMenu construct itself recursively and stack-overflow on open. The RPD BUI now builds the same button models as RCDMenuBoundUserInterface with the RPD atmos categories and feeds them to an embedded SimpleRadialMenu; the color strip is unchanged. Also restores the SPDX header dropped from RPDMenu.xaml.cs. --- Content.Client/RPD/RPDMenu.xaml.cs | 37 ++-- .../RPD/RPDMenuBoundUserInterface.cs | 160 ++++++++++++++++-- 2 files changed, 162 insertions(+), 35 deletions(-) diff --git a/Content.Client/RPD/RPDMenu.xaml.cs b/Content.Client/RPD/RPDMenu.xaml.cs index 4eacbb2c07a..38c125388e2 100644 --- a/Content.Client/RPD/RPDMenu.xaml.cs +++ b/Content.Client/RPD/RPDMenu.xaml.cs @@ -1,52 +1,53 @@ +// SPDX-FileCopyrightText: 2025 Steve +// SPDX-FileCopyrightText: 2025 marc-pelletier <113944176+marc-pelletier@users.noreply.github.com> +// SPDX-FileCopyrightText: 2026 Triad Sector +// +// SPDX-License-Identifier: AGPL-3.0-or-later + using System.Linq; using System.Numerics; -using Content.Client.RCD; using Content.Client.UserInterface.Controls; -using Content.Shared.RCD; using Content.Shared.RPD; using Robust.Client.AutoGenerated; using Robust.Client.Graphics; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; -using Robust.Shared.Prototypes; namespace Content.Client.RPD; /// -/// Wraps the standard RCDMenu radial with a color-picker strip below it. Operator can build pipes in a chosen -/// palette color via PipeColorVisualsComponent. Swatches render in two centered rows: general pipe-loop -/// labels on top, alphabetized named gases below. +/// Wraps an embedded build picker with a color-picker strip below it. Operator can +/// build pipes in a chosen palette color via PipeColorVisualsComponent. Swatches render in two centered rows: +/// general pipe-loop labels on top, alphabetized named gases below. /// [GenerateTypedNameReferences] public sealed partial class RPDMenu : RadialMenu { [Dependency] private readonly ILocalizationManager _locManager = default!; - private readonly RPDMenu _rcdMenu; + private readonly SimpleRadialMenu _radialMenu; private string? _selectedColor; public event Action? ColorSelected; - public event Action>? SendRCDSystemMessageAction - { - add => _rcdMenu.SendRCDSystemMessageAction += value; - remove => _rcdMenu.SendRCDSystemMessageAction -= value; - } - public RPDMenu() { IoCManager.InjectDependencies(this); RobustXamlLoader.Load(this); - _rcdMenu = new RPDMenu(); + _radialMenu = new SimpleRadialMenu(); var rcdHolder = FindControl("RCDMenuHolder"); - rcdHolder.AddChild(_rcdMenu); - _rcdMenu.OnClose += Close; + rcdHolder.AddChild(_radialMenu); + _radialMenu.OnClose += Close; } - public void SetEntity(EntityUid uid) + /// + /// Sets the build options on the embedded radial. Models are built by + /// , which owns the category grouping and click actions. + /// + public void SetRadialButtons(IEnumerable models) { - _rcdMenu.SetEntity(uid); + _radialMenu.SetButtons(models); } public void Populate(IReadOnlyDictionary palette, string selectedColor = "default") diff --git a/Content.Client/RPD/RPDMenuBoundUserInterface.cs b/Content.Client/RPD/RPDMenuBoundUserInterface.cs index 0e59d635fb9..44639420cec 100644 --- a/Content.Client/RPD/RPDMenuBoundUserInterface.cs +++ b/Content.Client/RPD/RPDMenuBoundUserInterface.cs @@ -3,25 +3,47 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later +using Content.Client.Popups; +using Content.Client.UserInterface.Controls; using Content.Shared.RCD; +using Content.Shared.RCD.Components; using Content.Shared.RPD; using Content.Shared.RPD.Components; using Robust.Client.Graphics; using Robust.Client.Input; using Robust.Client.UserInterface; +using Robust.Shared.Collections; +using Robust.Shared.Player; using Robust.Shared.Prototypes; +using Robust.Shared.Utility; namespace Content.Client.RPD; /// -/// Opens an populated with the shared . Color selection is -/// forwarded to the server via . +/// Opens an populated with the shared and the RPD's build options. +/// Build selection is forwarded to the server via , color selection via +/// . Mirrors the button-model conversion in +/// with the RPD's atmos categories. /// public sealed class RPDMenuBoundUserInterface : BoundUserInterface { + private const string TopLevelActionCategory = "Main"; + + // Triad: category icons mirror the entities we construct so a fork-side sprite swap propagates to the picker. + private static readonly Dictionary PrototypesGroupingInfo + = new Dictionary + { + ["Piping"] = ("rcd-component-piping", new SpriteSpecifier.Rsi(new ResPath("/Textures/Structures/Piping/Atmospherics/pipe.rsi"), "pipeFourway")), + ["AtmosphericUtility"] = ("rcd-component-atmosphericutility", new SpriteSpecifier.Rsi(new ResPath("/Textures/Structures/Piping/Atmospherics/gascanisterport.rsi"), "gasCanisterPort")), + ["PumpsValves"] = ("rcd-component-pumps", new SpriteSpecifier.Rsi(new ResPath("/Textures/Structures/Piping/Atmospherics/pump.rsi"), "pumpVolume")), + ["Vents"] = ("rcd-component-vents", new SpriteSpecifier.Rsi(new ResPath("/Textures/_NF/Structures/Piping/Atmospherics/vent.rsi"), "vent_passive")), + ["SensorsMonitors"] = ("rcd-component-sensorsmonitors", new SpriteSpecifier.Rsi(new ResPath("/Textures/Structures/Wallmounts/air_monitors.rsi"), "alarm0")), + }; + [Dependency] private readonly IClyde _displayManager = default!; [Dependency] private readonly IInputManager _inputManager = default!; - [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly ISharedPlayerManager _playerManager = default!; private RPDMenu? _menu; @@ -34,17 +56,16 @@ protected override void Open() { base.Open(); - if (!_entityManager.HasComponent(Owner)) + if (!EntMan.TryGetComponent(Owner, out var rcd) + || !EntMan.TryGetComponent(Owner, out var rpd)) return; _menu = this.CreateWindow(); - _menu.SetEntity(Owner); + _menu.SetRadialButtons(ConvertToButtons(rcd.AvailablePrototypes)); _menu.ColorSelected += OnColorSelected; - _menu.SendRCDSystemMessageAction += OnRCDSystemMessage; - var selectedColor = _entityManager.TryGetComponent(Owner, out var comp) - && RPDPalette.IsValid(comp.PipeColor) - ? comp.PipeColor + var selectedColor = RPDPalette.IsValid(rpd.PipeColor) + ? rpd.PipeColor : RPDPalette.DefaultKey; _menu.Populate(RPDPalette.Colors, selectedColor); @@ -52,26 +73,131 @@ protected override void Open() _menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize); } - private void OnColorSelected(string colorKey) + private IEnumerable ConvertToButtons(HashSet> prototypes) { - if (!RPDPalette.IsValid(colorKey)) + Dictionary> buttonsByCategory = new(); + ValueList topLevelActions = new(); + foreach (var protoId in prototypes) + { + var prototype = _prototypeManager.Index(protoId); + if (prototype.Category == TopLevelActionCategory) + { + var topLevelActionOption = new RadialMenuActionOption(HandleMenuOptionClick, prototype) + { + IconSpecifier = RadialMenuIconSpecifier.With(prototype.Sprite), + ToolTip = GetTooltip(prototype) + }; + topLevelActions.Add(topLevelActionOption); + continue; + } + + if (!PrototypesGroupingInfo.TryGetValue(prototype.Category, out var groupInfo)) + continue; + + if (!buttonsByCategory.TryGetValue(prototype.Category, out var list)) + { + list = new List(); + buttonsByCategory.Add(prototype.Category, list); + } + + var actionOption = new RadialMenuActionOption(HandleMenuOptionClick, prototype) + { + IconSpecifier = RadialMenuIconSpecifier.With(prototype.Sprite), + ToolTip = GetTooltip(prototype) + }; + list.Add(actionOption); + } + + var models = new RadialMenuOptionBase[buttonsByCategory.Count + topLevelActions.Count]; + var i = 0; + foreach (var (key, list) in buttonsByCategory) + { + var groupInfo = PrototypesGroupingInfo[key]; + models[i] = new RadialMenuNestedLayerOption(list) + { + IconSpecifier = RadialMenuIconSpecifier.With(groupInfo.Sprite), + ToolTip = Loc.GetString(groupInfo.Tooltip) + }; + i++; + } + + foreach (var action in topLevelActions) + { + models[i] = action; + i++; + } + + return models; + } + + private void HandleMenuOptionClick(RCDPrototype proto) + { + // A predicted message cannot be used here as the RPD UI is closed immediately + // after this message is sent, which will stop the server from receiving it + SendMessage(new RCDSystemMessage(proto.ID)); + + if (_playerManager.LocalSession?.AttachedEntity == null) return; - SendMessage(new RPDColorChangeMessage(_entityManager.GetNetEntity(Owner), colorKey)); + var msg = Loc.GetString("rcd-component-change-mode", ("tool", Owner), ("mode", Loc.GetString(proto.SetName))); + + if (proto.Mode is RcdMode.ConstructTile or RcdMode.ConstructObject) + { + var name = Loc.GetString(proto.SetName); + + if (proto.Prototype != null && + _prototypeManager.TryIndex(proto.Prototype, out var entProto)) // don't use Resolve because this can be a tile + { + name = entProto.Name; + } + + msg = Loc.GetString("rcd-component-change-build-mode", ("tool", Owner), ("name", name)); + } + + // Popup message + var popup = EntMan.System(); + popup.PopupClient(msg, Owner, _playerManager.LocalSession.AttachedEntity); } - private void OnRCDSystemMessage(ProtoId protoId) + private string GetTooltip(RCDPrototype proto) { - SendMessage(new RCDSystemMessage(protoId)); + string tooltip; + + if (proto.Mode is RcdMode.ConstructTile or RcdMode.ConstructObject + && proto.Prototype != null + && _prototypeManager.TryIndex(proto.Prototype, out var entProto)) // don't use Resolve because this can be a tile + { + tooltip = Loc.GetString(entProto.Name); + } + else + { + tooltip = Loc.GetString(proto.SetName); + } + + tooltip = OopsConcat(char.ToUpper(tooltip[0]).ToString(), tooltip.Remove(0, 1)); + + return tooltip; + } + + private static string OopsConcat(string a, string b) + { + // This exists to prevent Roslyn being clever and compiling something that fails sandbox checks. + return a + b; + } + + private void OnColorSelected(string colorKey) + { + if (!RPDPalette.IsValid(colorKey)) + return; + + SendMessage(new RPDColorChangeMessage(EntMan.GetNetEntity(Owner), colorKey)); } protected override void Dispose(bool disposing) { if (disposing && _menu != null) - { _menu.ColorSelected -= OnColorSelected; - _menu.SendRCDSystemMessageAction -= OnRCDSystemMessage; - } + base.Dispose(disposing); } } From 7e40fdd50989b6014d9c093b0e6991ca35da950f Mon Sep 17 00:00:00 2001 From: rebaserHEAD <38984539+rebaserHEAD@users.noreply.github.com> Date: Wed, 29 Jul 2026 20:51:39 -0400 Subject: [PATCH 25/25] Radial port fixups: ammo selector loc key, emote menu cleanup The ammo selector port renamed the C# loc key to ammo-selector-mode-selected without renaming the ftl entry, so the popup printed the raw key. Also drops a redundant Open() and duplicate Invalid check in the emotes controller and trailing whitespace. --- .../Silicons/StationAi/StationAiBoundUserInterface.cs | 2 +- .../UserInterface/Systems/Emotes/EmotesUIController.cs | 7 +------ Resources/Locale/en-US/_Goobstation/weapons/gun.ftl | 2 +- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs b/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs index 2ada6e4b015..e6a6e746256 100644 --- a/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs +++ b/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs @@ -19,7 +19,7 @@ protected override void Open() _menu.Track(Owner); var buttonModels = ConvertToButtons(ev.Actions); _menu.SetButtons(buttonModels); - + _menu.Open(); } diff --git a/Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs b/Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs index adbbb486431..b3cd2842eae 100644 --- a/Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs +++ b/Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs @@ -21,7 +21,7 @@ public sealed class EmotesUIController : UIController, IOnStateChanged UIManager.GetActiveUIWidgetOrNull()?.EmotesButton; private SimpleRadialMenu? _menu; @@ -57,8 +57,6 @@ private void ToggleEmotesMenu(bool centered) _menu = new SimpleRadialMenu(); _menu.SetButtons(models); - _menu.Open(); - _menu.OnClose += OnWindowClosed; _menu.OnOpen += OnWindowOpen; @@ -138,9 +136,6 @@ private IEnumerable ConvertToButtons(IEnumerable> emotesByCategory = new(); foreach (var emote in emotePrototypes) { - if(emote.Category == EmoteCategory.Invalid) - continue; - // only valid emotes that have ways to be triggered by chat and player have access / no restriction on if (emote.Category == EmoteCategory.Invalid || emote.ChatTriggers.Count == 0 diff --git a/Resources/Locale/en-US/_Goobstation/weapons/gun.ftl b/Resources/Locale/en-US/_Goobstation/weapons/gun.ftl index aacc62a8e8a..319524f3d74 100644 --- a/Resources/Locale/en-US/_Goobstation/weapons/gun.ftl +++ b/Resources/Locale/en-US/_Goobstation/weapons/gun.ftl @@ -1,7 +1,7 @@ # Hardlight Bow ammo-selector-examine-mode = Current mode: {$mode} mode-select-verb-text = Select firing mode -mode-selected = Selected {$mode} +ammo-selector-mode-selected = Selected {$mode} # RequiresDualWieldComponent dual-wield-component-requires = That doesn't feel cool enough, you need to dual wield.