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!