diff --git a/Content.Client/Chat/UI/EmotesMenu.xaml b/Content.Client/Chat/UI/EmotesMenu.xaml deleted file mode 100644 index 9ed5567ef14..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/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/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..1445ab3b17b 100644 --- a/Content.Client/RCD/RCDMenuBoundUserInterface.cs +++ b/Content.Client/RCD/RCDMenuBoundUserInterface.cs @@ -1,20 +1,35 @@ +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.Collections; +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 const string TopLevelActionCategory = "Main"; - private RCDMenu? _menu; + 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"))), + }; + + [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 +40,126 @@ 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); + + _menu.OpenOverMouseScreenPosition(); + } + + 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) + { + 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++; + } - // Open the menu, centered on the mouse - var vpSize = _displayManager.ScreenSize; - _menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize); + foreach (var action in topLevelActions) + { + models[i] = action; + i++; + } + + return models; } - public void SendRCDSystemMessage(ProtoId protoId) + 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", ("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 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)) // 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; } } diff --git a/Content.Client/RPD/RPDMenu.xaml.cs b/Content.Client/RPD/RPDMenu.xaml.cs index 966915e5a6b..38c125388e2 100644 --- a/Content.Client/RPD/RPDMenu.xaml.cs +++ b/Content.Client/RPD/RPDMenu.xaml.cs @@ -6,53 +6,48 @@ 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 RCDMenu _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 RCDMenu(); + _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); } } diff --git a/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs b/Content.Client/Silicons/StationAi/StationAiBoundUserInterface.cs index 68318305a0c..e6a6e746256 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 RadialMenuActionOptionBase[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, - }); - }; + IconSpecifier = RadialMenuIconSpecifier.With(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..0cc207dd89d 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 @@ -218,11 +228,10 @@ public void ReturnToPreviousLayer() /// Base class for radial menu buttons. Excludes all actions except clicks and alt-clicks /// from interactions. /// -[Virtual] -public class RadialMenuTextureButtonBase : TextureButton +public abstract class RadialMenuButtonBase : BaseButton { /// - protected RadialMenuTextureButtonBase() + protected RadialMenuButtonBase() { EnableAllKeybinds = true; } @@ -232,7 +241,9 @@ protected override void KeyBindUp(GUIBoundKeyEventArgs args) { if (args.Function == EngineKeyFunctions.UIClick || args.Function == ContentKeyFunctions.AltActivateItemInWorld) + { base.KeyBindUp(args); + } } } @@ -243,8 +254,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; } @@ -261,15 +278,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; } @@ -293,25 +320,30 @@ 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 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 /// - public RadialMenuTextureButton() + public RadialMenuButton() { - EnableAllKeybinds = true; OnButtonUp += OnClicked; } private void OnClicked(ButtonEventArgs args) { - if (TargetLayer == string.Empty) + if (TargetLayer == null && TargetLayerControlName == null) return; var parent = FindParentMultiLayerContainer(this); @@ -319,7 +351,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) @@ -368,7 +407,7 @@ public interface IRadialMenuItemWithSector } [Virtual] -public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadialMenuItemWithSector +public class RadialMenuButtonWithSector : RadialMenuButton, IRadialMenuItemWithSector { private Vector2[]? _sectorPointsForDrawing; @@ -387,7 +426,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 +439,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. /// @@ -484,7 +516,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() { } @@ -520,7 +552,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..06ea63f0a26 --- /dev/null +++ b/Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs @@ -0,0 +1,388 @@ +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; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Prototypes; + +namespace Content.Client.UserInterface.Controls; + +[GenerateTypedNameReferences] +public sealed 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 RadialMenuButton 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 RadialMenuButton ConvertToButton( + RadialMenuOptionBase model, + SpriteSystem sprites, + SimpleRadialMenuSettings settings, + bool haveNested + ) + { + var button = settings.UseSectors + ? ConvertToButtonWithSector(model, settings) + : new RadialMenuButton(); + button.SetSize = new Vector2(64f, 64f); + button.ToolTip = model.ToolTip; + var imageControl = model.IconSpecifier switch + { + RadialMenuTextureIconSpecifier textureSpecifier => CreateTexture(textureSpecifier.Sprite, sprites), + RadialMenuEntityIconSpecifier entitySpecifier => CreateSpriteView(entitySpecifier.Entity), + RadialMenuEntityPrototypeIconSpecifier entProtoSpecifier => CreateEntityPrototypeView(entProtoSpecifier.ProtoId), + _ => null + }; + + if(imageControl != null) + button.AddChild(imageControl); + + if (model is RadialMenuActionOptionBase actionOption) + { + button.OnPressed += _ => + { + actionOption.OnPressed?.Invoke(); + if (!haveNested) + Close(); + }; + } + + return button; + } + + 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 RadialMenuButtonWithSector + { + 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 + +} + +/// +/// 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; + + 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; } + + /// + /// 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; } +} + +/// 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; +} + +/// 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)); + +/// +/// 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; + + /// List of button models for next layer of menu. + public IReadOnlyCollection Nested { get; } = nested; +} + +/// +/// Additional settings for radial menu render. +/// +public sealed 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..b3cd2842eae 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,14 @@ 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.OnClose += OnWindowClosed; _menu.OnOpen += OnWindowOpen; - _menu.OnPlayEmote += OnPlayEmote; if (EmotesButton != null) EmotesButton.SetClickPressed(true); @@ -56,16 +69,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 +128,59 @@ 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) + { + // 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) + { + IconSpecifier = RadialMenuIconSpecifier.With(emote.Icon), + ToolTip = Loc.GetString(emote.Name) + }; + list.Add(actionOption); + } + + var models = new RadialMenuOptionBase[emotesByCategory.Count]; + var i = 0; + foreach (var (key, list) in emotesByCategory) + { + var tuple = EmoteGroupingInfo[key]; + + models[i] = new RadialMenuNestedLayerOption(list) + { + IconSpecifier = RadialMenuIconSpecifier.With(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.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenu.xaml b/Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenu.xaml deleted file mode 100644 index 1383c7f5062..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 296ef432c8e..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 : RadialMenuTextureButton -{ - public CardMenuButton() - { - - } -} diff --git a/Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenuBoundUserInterface.cs b/Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenuBoundUserInterface.cs index 5c8e3022e1f..66da057cacc 100644 --- a/Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenuBoundUserInterface.cs +++ b/Content.Client/_EstacaoPirata/Cards/Hand/UI/CardHandMenuBoundUserInterface.cs @@ -1,17 +1,23 @@ +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 +28,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; + + if (cardComp.Flipped && EntMan.TryGetComponent(card, out var metadata)) + cardName = metadata.EntityName; + else + cardName = Loc.GetString(cardComp.Name); - _menu?.Dispose(); + 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)); } diff --git a/Content.Client/_Goobstation/AmmoSelector/AmmoSelectorMenu.xaml b/Content.Client/_Goobstation/AmmoSelector/AmmoSelectorMenu.xaml deleted file mode 100644 index 2cbe120629f..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 2200d110dbc..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 : RadialMenuTextureButtonWithSector -{ - 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.Client/_Goobstation/Clothing/ToggleableClothingBoundUserInterface.cs b/Content.Client/_Goobstation/Clothing/ToggleableClothingBoundUserInterface.cs index 55407f78347..cd13b0ea60f 100644 --- a/Content.Client/_Goobstation/Clothing/ToggleableClothingBoundUserInterface.cs +++ b/Content.Client/_Goobstation/Clothing/ToggleableClothingBoundUserInterface.cs @@ -1,39 +1,67 @@ +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) + { + if (!EntMan.TryGetComponent(pair.Key, out MetaDataComponent? metaData) || metaData.EntityPrototype == null) + continue; + + var netEntity = EntMan.GetNetEntity(pair.Key); + var action = new RadialMenuActionOption(SendToggleableClothingMessage, netEntity) + { + IconSpecifier = RadialMenuIconSpecifier.With(metaData.EntityPrototype) + }; + + 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 cfa0b93234e..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 86f36e13f0f..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 : RadialMenuTextureButton -{ - public EntityUid AttachedClothingId { get; set; } -} 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/Ghost/Roles/Components/GhostRoleMobSpawnerComponent.cs b/Content.Shared/Ghost/Roles/Components/GhostRoleMobSpawnerComponent.cs index 2e44effad96..6984be91f96 100644 --- a/Content.Shared/Ghost/Roles/Components/GhostRoleMobSpawnerComponent.cs +++ b/Content.Shared/Ghost/Roles/Components/GhostRoleMobSpawnerComponent.cs @@ -3,7 +3,7 @@ namespace Content.Shared.Ghost.Roles.Components { /// - /// 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 = []; } } 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/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); 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); } 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. 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!