Simplify hands UI code (#42534)

* Simplify hands UI code

* i remembered about SortedHands in the component

* minor cleanup

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
This commit is contained in:
pathetic meowmeow
2026-01-22 16:31:45 -05:00
committed by GitHub
parent 59d8495cc7
commit 6f8242c1fc
9 changed files with 112 additions and 348 deletions

View File

@@ -1,5 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Client.UserInterface.Systems.Inventory.Controls;
using Content.Shared.Hands.Components;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.UserInterface.Systems.Hands.Controls;
@@ -7,10 +9,10 @@ namespace Content.Client.UserInterface.Systems.Hands.Controls;
public sealed class HandsContainer : ItemSlotUIContainer<HandButton>
{
private readonly GridContainer _grid;
public int ColumnLimit { get => _grid.Columns; set => _grid.Columns = value; }
public int MaxButtonCount { get; set; } = 0;
private readonly List<HandButton> _orderedButtons = new();
public HandsComponent? PlayerHandsComponent;
public int MaxButtonsPerRow { get; set; }= 6;
public int ColumnLimit { get; set; } = 6;
/// <summary>
/// Indexer. This is used to reference a HandsContainer from the
@@ -24,57 +26,32 @@ public sealed class HandsContainer : ItemSlotUIContainer<HandButton>
_grid.ExpandBackwards = true;
}
public override HandButton? AddButton(HandButton newButton)
protected override void AddButton(HandButton newButton)
{
if (MaxButtonCount > 0)
{
if (ButtonCount >= MaxButtonCount)
return null;
_orderedButtons.Add(newButton);
_grid.AddChild(newButton);
}
else
_grid.RemoveAllChildren();
var enumerable = PlayerHandsComponent?.SortedHands is { } sortedHands
? _orderedButtons.OrderBy(it => sortedHands.IndexOf(it.SlotName))
: _orderedButtons.OrderBy(it => it.HandLocation);
foreach (var button in enumerable)
{
_grid.AddChild(newButton);
_grid.AddChild(button);
}
_grid.Columns = Math.Min(_grid.ChildCount, MaxButtonsPerRow);
return base.AddButton(newButton);
_grid.Columns = Math.Min(_grid.ChildCount, ColumnLimit);
}
public override void RemoveButton(string handName)
protected override void RemoveButton(HandButton button)
{
var button = GetButton(handName);
if (button == null)
return;
base.RemoveButton(button);
_orderedButtons.Remove(button);
_grid.RemoveChild(button);
}
public bool TryGetLastButton(out HandButton? control)
public override void ClearButtons()
{
if (Buttons.Count == 0)
{
control = null;
return false;
}
control = Buttons.Values.Last();
return true;
}
public bool TryRemoveLastHand(out HandButton? control)
{
var success = TryGetLastButton(out control);
if (control != null)
RemoveButton(control);
return success;
}
public void Clear()
{
ClearButtons();
_grid.RemoveAllChildren();
base.ClearButtons();
_orderedButtons.Clear();
}
public IEnumerable<HandButton> GetButtons()
@@ -85,8 +62,4 @@ public sealed class HandsContainer : ItemSlotUIContainer<HandButton>
yield return hand;
}
}
public bool IsFull => (MaxButtonCount != 0 && ButtonCount >= MaxButtonCount);
public int ButtonCount => _grid.ChildCount;
}

View File

@@ -7,7 +7,6 @@ using Content.Shared.Hands.Components;
using Content.Shared.Input;
using Content.Shared.Inventory.VirtualItem;
using Content.Shared.Timing;
using JetBrains.Annotations;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
@@ -26,9 +25,6 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
[UISystemDependency] private readonly HandsSystem _handsSystem = default!;
[UISystemDependency] private readonly UseDelaySystem _useDelay = default!;
private readonly List<HandsContainer> _handsContainers = new();
private readonly Dictionary<string, int> _handContainerIndices = new();
private readonly Dictionary<string, HandButton> _handLookup = new();
private HandsComponent? _playerHandsComponent;
private HandButton? _activeHand;
@@ -40,8 +36,6 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
private HandButton? _statusHandLeft;
private HandButton? _statusHandRight;
private int _backupSuffix; //this is used when autogenerating container names if they don't have names
private HotbarGui? HandsGui => UIManager.GetActiveUIWidgetOrNull<HotbarGui>();
public void OnSystemLoaded(HandsSystem system)
@@ -119,25 +113,16 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
private void UnloadPlayerHands()
{
if (HandsGui != null)
HandsGui.Visible = false;
_handContainerIndices.Clear();
_handLookup.Clear();
HandsGui?.Visible = false;
HandsGui?.HandContainer.ClearButtons();
_playerHandsComponent = null;
foreach (var container in _handsContainers)
{
container.Clear();
}
}
private void LoadPlayerHands(Entity<HandsComponent> handsComp)
{
DebugTools.Assert(_playerHandsComponent == null);
if (HandsGui != null)
HandsGui.Visible = true;
HandsGui?.Visible = true;
HandsGui?.HandContainer.PlayerHandsComponent = handsComp;
_playerHandsComponent = handsComp;
foreach (var (name, hand) in handsComp.Comp.Hands)
{
@@ -182,29 +167,18 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
private void HandBlocked(string handName)
{
if (!_handLookup.TryGetValue(handName, out var hand))
{
if (HandsGui?.HandContainer.TryGetButton(handName, out var hand) != true)
return;
}
hand.Blocked = true;
hand!.Blocked = true;
}
private void HandUnblocked(string handName)
{
if (!_handLookup.TryGetValue(handName, out var hand))
{
if (HandsGui?.HandContainer.TryGetButton(handName, out var hand) != true)
return;
}
hand.Blocked = false;
}
private int GetHandContainerIndex(string containerName)
{
if (!_handContainerIndices.TryGetValue(containerName, out var result))
return -1;
return result;
hand!.Blocked = false;
}
private void OnItemAdded(string name, EntityUid entity)
@@ -243,7 +217,7 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
_handsSystem.TryGetHand((playerEntity, _playerHandsComponent), name, out var handData))
{
UpdateHandStatus(hand, null, handData);
if (handData?.EmptyRepresentative is { } representative)
if (handData.Value.EmptyRepresentative is { } representative)
{
SetRepresentative(hand, representative);
return;
@@ -253,30 +227,6 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
hand.SetEntity(null);
}
private HandsContainer GetFirstAvailableContainer()
{
if (_handsContainers.Count == 0)
throw new Exception("Could not find an attached hand hud container");
foreach (var container in _handsContainers)
{
if (container.IsFull)
continue;
return container;
}
throw new Exception("All attached hand hud containers were full!");
}
public bool TryGetHandContainer(string containerName, out HandsContainer? container)
{
container = null;
var containerIndex = GetHandContainerIndex(containerName);
if (containerIndex == -1)
return false;
container = _handsContainers[containerIndex];
return true;
}
//propagate hand activation to the hand system.
private void StorageActivate(GUIBoundKeyEventArgs args, SlotControl handControl)
{
@@ -293,24 +243,23 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
return;
}
if (!_handLookup.TryGetValue(handName, out var handControl) || handControl == _activeHand)
if (HandsGui?.HandContainer.TryGetButton(handName, out var handControl) != true || handControl == _activeHand)
return;
if (_activeHand != null)
_activeHand.Highlight = false;
handControl.Highlight = true;
handControl!.Highlight = true;
_activeHand = handControl;
if (HandsGui != null &&
_playerHandsComponent != null &&
if (_playerHandsComponent != null &&
_player.LocalSession?.AttachedEntity is { } playerEntity &&
_handsSystem.TryGetHand((playerEntity, _playerHandsComponent), handName, out var hand))
{
var heldEnt = _handsSystem.GetHeldItem((playerEntity, _playerHandsComponent), handName);
var foldedLocation = hand.Value.Location.GetUILocation();
if (foldedLocation == HandUILocation.Left)
var foldedLocation = hand.Value.Location;
if (foldedLocation == HandLocation.Left)
{
_statusHandLeft = handControl;
HandsGui.UpdatePanelEntityLeft(heldEnt, hand.Value);
@@ -328,8 +277,7 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
private HandButton? GetHand(string handName)
{
_handLookup.TryGetValue(handName, out var handControl);
return handControl;
return HandsGui?.HandContainer.GetButton(handName);
}
private HandButton AddHand(string handName, Hand hand)
@@ -338,17 +286,7 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
button.StoragePressed += StorageActivate;
button.Pressed += HandPressed;
if (!_handLookup.TryAdd(handName, button))
return _handLookup[handName];
if (HandsGui != null)
{
HandsGui.HandContainer.AddButton(button);
}
else
{
GetFirstAvailableContainer().AddButton(button);
}
HandsGui?.HandContainer.TryAddButton(button);
if (hand.EmptyRepresentative is { } representative)
{
@@ -359,7 +297,7 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
// If we don't have a status for this hand type yet, set it.
// This means we have status filled by default in most scenarios,
// otherwise the user'd need to switch hands to "activate" the hands the first time.
if (hand.Location.GetUILocation() == HandUILocation.Left)
if (hand.Location == HandLocation.Left)
_statusHandLeft ??= button;
else
_statusHandRight ??= button;
@@ -378,62 +316,17 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
_handsSystem.ReloadHandButtons();
}
/// <summary>
/// Swap hands from one container to the other.
/// </summary>
/// <param name="other"></param>
/// <param name="source"></param>
public void SwapHands(HandsContainer other, HandsContainer? source = null)
{
if (HandsGui == null && source == null)
{
throw new ArgumentException("Cannot swap hands if no source hand container exists!");
}
source ??= HandsGui!.HandContainer;
var transfer = new List<Control>();
foreach (var child in source.Children)
{
if (child is not HandButton)
{
continue;
}
transfer.Add(child);
}
foreach (var control in transfer)
{
source.RemoveChild(control);
other.AddChild(control);
}
}
private void RemoveHand(string handName)
{
RemoveHand(handName, out _);
}
[PublicAPI]
private bool RemoveHand(string handName, out HandButton? handButton)
{
if (!_handLookup.TryGetValue(handName, out handButton))
return false;
if (handButton.Parent is HandsContainer handContainer)
{
handContainer.RemoveButton(handButton);
}
if (HandsGui?.HandContainer.TryRemoveButton(handName, out var handButton) != true)
return;
if (_statusHandLeft == handButton)
_statusHandLeft = null;
if (_statusHandRight == handButton)
_statusHandRight = null;
_handLookup.Remove(handName);
handButton.Orphan();
UpdateVisibleStatusPanels();
return true;
}
private void UpdateVisibleStatusPanels()
@@ -441,9 +334,12 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
var leftVisible = false;
var rightVisible = false;
foreach (var hand in _handLookup.Values)
if (HandsGui is null)
return;
foreach (var hand in HandsGui.HandContainer.GetButtons())
{
if (hand.HandLocation.GetUILocation() == HandUILocation.Left)
if (hand.HandLocation == HandLocation.Left)
{
leftVisible = true;
}
@@ -453,73 +349,34 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
}
}
HandsGui?.UpdateStatusVisibility(leftVisible, rightVisible);
}
public string RegisterHandContainer(HandsContainer handContainer)
{
var name = "HandContainer_" + _backupSuffix;
if (handContainer.Indexer == null)
{
handContainer.Indexer = name;
_backupSuffix++;
}
else
{
name = handContainer.Indexer;
}
_handContainerIndices.Add(name, _handsContainers.Count);
_handsContainers.Add(handContainer);
return name;
}
public bool RemoveHandContainer(string handContainerName)
{
var index = GetHandContainerIndex(handContainerName);
if (index == -1)
return false;
_handContainerIndices.Remove(handContainerName);
_handsContainers.RemoveAt(index);
return true;
}
public bool RemoveHandContainer(string handContainerName, out HandsContainer? container)
{
var success = _handContainerIndices.TryGetValue(handContainerName, out var index);
container = _handsContainers[index];
_handContainerIndices.Remove(handContainerName);
_handsContainers.RemoveAt(index);
return success;
HandsGui.UpdateStatusVisibility(leftVisible, rightVisible);
}
public void OnStateEntered(GameplayState state)
{
if (HandsGui != null)
HandsGui.Visible = _playerHandsComponent != null;
HandsGui?.Visible = _playerHandsComponent != null;
}
public override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (HandsGui is not { } handsGui)
return;
// TODO this should be event based but 2 systems modify the same component differently for some reason
foreach (var container in _handsContainers)
foreach (var hand in handsGui.HandContainer.GetButtons())
{
foreach (var hand in container.GetButtons())
if (!_entities.TryGetComponent(hand.Entity, out UseDelayComponent? useDelay))
{
if (!_entities.TryGetComponent(hand.Entity, out UseDelayComponent? useDelay))
{
hand.CooldownDisplay.Visible = false;
continue;
}
var delay = _useDelay.GetLastEndingDelay((hand.Entity.Value, useDelay));
hand.CooldownDisplay.Visible = true;
hand.CooldownDisplay.FromTime(delay.StartTime, delay.EndTime);
hand.CooldownDisplay.Visible = false;
continue;
}
var delay = _useDelay.GetLastEndingDelay((hand.Entity.Value, useDelay));
hand.CooldownDisplay.Visible = true;
hand.CooldownDisplay.FromTime(delay.StartTime, delay.EndTime);
}
}

View File

@@ -36,7 +36,6 @@ public sealed class HotbarUIController : UIController
_inventory = UIManager.GetUIController<InventoryUIController>();
_hands = UIManager.GetUIController<HandsUIController>();
_storage = UIManager.GetUIController<StorageUIController>();
_hands.RegisterHandContainer(handsContainer);
}
public void ReloadHotbar()

View File

@@ -11,8 +11,8 @@ public sealed partial class HotbarGui : UIWidget
public HotbarGui()
{
RobustXamlLoader.Load(this);
StatusPanelRight.SetSide(HandUILocation.Right);
StatusPanelLeft.SetSide(HandUILocation.Left);
StatusPanelRight.SetSide(HandLocation.Right);
StatusPanelLeft.SetSide(HandLocation.Left);
var hotbarController = UserInterfaceManager.GetUIController<HotbarUIController>();
hotbarController.Setup(HandContainer);
@@ -29,10 +29,10 @@ public sealed partial class HotbarGui : UIWidget
StatusPanelRight.Update(entity, hand);
}
public void SetHighlightHand(HandUILocation? hand)
public void SetHighlightHand(HandLocation? hand)
{
StatusPanelLeft.UpdateHighlight(hand is HandUILocation.Left);
StatusPanelRight.UpdateHighlight(hand is HandUILocation.Right);
StatusPanelLeft.UpdateHighlight(hand is HandLocation.Left);
StatusPanelRight.UpdateHighlight(hand is HandLocation.Right);
}
public void UpdateStatusVisibility(bool left, bool right)

View File

@@ -14,54 +14,36 @@ public interface IItemslotUIContainer
[Virtual]
public abstract class ItemSlotUIContainer<T> : GridContainer, IItemslotUIContainer where T : SlotControl
{
protected readonly Dictionary<string, T> Buttons = new();
private readonly Dictionary<string, T> _buttons = new();
private int? _maxColumns;
public int? MaxColumns { get; set; }
public int? MaxColumns
public virtual void ClearButtons()
{
get => _maxColumns;
set => _maxColumns = value;
}
public virtual bool TryAddButton(T newButton, out T button)
{
var tempButton = AddButton(newButton);
if (tempButton == null)
foreach (var button in _buttons.Values)
{
button = newButton;
return false;
button.Orphan();
}
button = newButton;
return true;
}
public void ClearButtons()
{
foreach (var button in Buttons.Values)
{
button.Dispose();
}
Buttons.Clear();
_buttons.Clear();
}
public bool TryRegisterButton(SlotControl control, string newSlotName)
{
if (newSlotName == "")
return false;
if (!(control is T slotButton))
if (control is not T slotButton)
return false;
if (Buttons.TryGetValue(newSlotName, out var foundButton))
if (_buttons.TryGetValue(newSlotName, out var foundButton))
{
if (control == foundButton)
return true; //if the slotName is already set do nothing
throw new Exception("Could not update button to slot:" + newSlotName + " slot already assigned!");
}
Buttons.Remove(slotButton.SlotName);
AddButton(slotButton);
_buttons.Remove(slotButton.SlotName);
TryAddButton(slotButton);
return true;
}
@@ -69,69 +51,54 @@ public abstract class ItemSlotUIContainer<T> : GridContainer, IItemslotUIContain
{
if (control is not T newButton)
return false;
return AddButton(newButton) != null;
return TryAddButton(newButton) != null;
}
public virtual T? AddButton(T newButton)
{
if (!Children.Contains(newButton) && newButton.Parent == null && newButton.SlotName != "")
AddChild(newButton);
Columns = _maxColumns ?? ChildCount;
return AddButtonToDict(newButton);
}
protected virtual T? AddButtonToDict(T newButton)
public T? TryAddButton(T newButton)
{
if (newButton.SlotName == "")
{
Logger.Warning("Could not add button " + newButton.Name + "No slotname");
Log.Warning($"{newButton.Name} because it has no slot name");
return null;
}
return !Buttons.TryAdd(newButton.SlotName, newButton) ? null : newButton;
if (Children.Contains(newButton) || newButton.Parent != null)
return null;
if (!_buttons.TryAdd(newButton.SlotName, newButton))
return null;
AddButton(newButton);
return newButton;
}
public virtual void RemoveButton(string slotName)
protected virtual void AddButton(T newButton)
{
if (!Buttons.TryGetValue(slotName, out var button))
return;
AddChild(newButton);
Columns = MaxColumns ?? ChildCount;
}
public bool TryRemoveButton(string slotName, [NotNullWhen(true)] out T? button)
{
if (!_buttons.TryGetValue(slotName, out button))
return false;
_buttons.Remove(button.SlotName);
RemoveButton(button);
return true;
}
public virtual void RemoveButtons(params string[] slotNames)
protected virtual void RemoveButton(T button)
{
foreach (var slotName in slotNames)
{
RemoveButton(slotName);
}
}
public virtual void RemoveButtons(params T?[] buttons)
{
foreach (var button in buttons)
{
if (button != null)
RemoveButton(button);
}
}
protected virtual void RemoveButtonFromDict(T button)
{
Buttons.Remove(button.SlotName);
}
public virtual void RemoveButton(T button)
{
RemoveButtonFromDict(button);
Children.Remove(button);
button.Dispose();
}
public virtual T? GetButton(string slotName)
public T? GetButton(string slotName)
{
return !Buttons.TryGetValue(slotName, out var button) ? null : button;
return _buttons.GetValueOrDefault(slotName);
}
public virtual bool TryGetButton(string slotName, [NotNullWhen(true)] out T? button)
public bool TryGetButton(string slotName, [NotNullWhen(true)] out T? button)
{
return (button = GetButton(slotName)) != null;
}

View File

@@ -20,7 +20,7 @@ public sealed partial class ItemStatusPanel : Control
[ViewVariables] private Hand? _hand;
// Tracked so we can re-run SetSide() if the theme changes.
private HandUILocation _side;
private HandLocation _side;
public ItemStatusPanel()
{
@@ -28,7 +28,7 @@ public sealed partial class ItemStatusPanel : Control
IoCManager.InjectDependencies(this);
}
public void SetSide(HandUILocation location)
public void SetSide(HandLocation location)
{
// AN IMPORTANT REMINDER ABOUT THIS CODE:
// In the UI, the RIGHT hand is on the LEFT on the screen.
@@ -43,14 +43,14 @@ public sealed partial class ItemStatusPanel : Control
switch (location)
{
case HandUILocation.Right:
case HandLocation.Right or HandLocation.Middle:
texture = Theme.ResolveTexture("item_status_right");
textureHighlight = Theme.ResolveTexture("item_status_right_highlight");
cutOut = StyleBox.Margin.Left;
flat = StyleBox.Margin.Right;
contentMargin = MarginFromThemeColor("_itemstatus_content_margin_right");
break;
case HandUILocation.Left:
case HandLocation.Left:
texture = Theme.ResolveTexture("item_status_left");
textureHighlight = Theme.ResolveTexture("item_status_left_highlight");
cutOut = StyleBox.Margin.Right;

View File

@@ -147,7 +147,7 @@ public sealed class InventoryUIController : UIController, IOnStateEntered<Gamepl
if (!container.TryGetButton(data.SlotName, out var button))
{
button = CreateSlotButton(data);
container.AddButton(button);
container.TryAddButton(button);
}
var showStorage = _entities.HasComponent<StorageComponent>(data.HeldEntity);
@@ -373,7 +373,7 @@ public sealed class InventoryUIController : UIController, IOnStateEntered<Gamepl
return;
var button = CreateSlotButton(data);
slotGroup.AddButton(button);
slotGroup.TryAddButton(button);
}
private void RemoveSlot(SlotData data)
@@ -381,7 +381,7 @@ public sealed class InventoryUIController : UIController, IOnStateEntered<Gamepl
if (!_slotGroups.TryGetValue(data.SlotGroup, out var slotGroup))
return;
slotGroup.RemoveButton(data.SlotName);
slotGroup.TryRemoveButton(data.SlotName, out _);
}
public void ReloadSlots()

View File

@@ -169,43 +169,9 @@ public sealed class HandsComponentState : ComponentState
/// <summary>
/// What side of the body this hand is on.
/// </summary>
/// <seealso cref="HandUILocation"/>
/// <seealso cref="HandLocationExt"/>
public enum HandLocation : byte
{
Left,
Right,
Middle,
Right
}
/// <summary>
/// What side of the UI a hand is on.
/// </summary>
/// <seealso cref="HandLocationExt"/>
/// <seealso cref="HandLocation"/>
public enum HandUILocation : byte
{
Left,
Right
}
/// <summary>
/// Helper functions for working with <see cref="HandLocation"/>.
/// </summary>
public static class HandLocationExt
{
/// <summary>
/// Convert a <see cref="HandLocation"/> into the appropriate <see cref="HandUILocation"/>.
/// This maps "middle" hands to <see cref="HandUILocation.Right"/>.
/// </summary>
public static HandUILocation GetUILocation(this HandLocation location)
{
return location switch
{
HandLocation.Left => HandUILocation.Left,
HandLocation.Middle => HandUILocation.Right,
HandLocation.Right => HandUILocation.Right,
_ => throw new ArgumentOutOfRangeException(nameof(location), location, null)
};
}
Left
}

View File

@@ -90,6 +90,8 @@ public abstract partial class SharedHandsSystem
ent.Comp.Hands.Add(handName, hand);
ent.Comp.SortedHands.Add(handName);
// we use LINQ + ToList instead of the list sort because it's a stable sort vs the list sort
ent.Comp.SortedHands = ent.Comp.SortedHands.OrderBy(handId => ent.Comp.Hands[handId].Location).ToList();
Dirty(ent);
OnPlayerAddHand?.Invoke((ent, ent.Comp), handName, hand.Location);