feat(ui): port retail toolbar ammo count

Resolve stacked thrown weapons versus separately equipped missile ammo exactly like gmToolbarUI, normalize a zero stack to one, and refresh the authored missile indicator from equipment and stack events. Keep the decision in pure Core with retained and real-DAT conformance coverage.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-11 12:29:51 +02:00
parent 400f01eef1
commit 564d39dfea
9 changed files with 330 additions and 7 deletions

View file

@ -48,6 +48,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
private const uint PanelIdAttribute = 0x10000029u;
private const uint UseButtonId = 0x1000019Du;
private const uint ExamineButtonId = 0x100001A5u;
private const uint AmmoIndicatorId = 0x10000194u;
private const uint InventoryButtonId = 0x100001B1u;
private static readonly uint[] PanelButtonIds =
{ 0x10000197u, 0x10000198u, 0x10000199u, 0x1000055Au, 0x1000019Au, 0x1000019Bu, InventoryButtonId };
@ -57,6 +58,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
private readonly UiButton? _inventoryButton;
private readonly UiButton? _useButton;
private readonly UiButton? _examineButton;
private readonly UiButton? _ammoIndicator;
private readonly List<(uint PanelId, UiButton Button)> _panelButtons = new();
private readonly ClientObjectTable _repo;
private readonly CombatState? _combatState;
@ -72,6 +74,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
private readonly Func<uint> _selectedObjectId;
private readonly Func<uint>? _playerGuid;
private readonly Action<uint, uint, int>? _sendPutItemInContainer;
private uint _ammoObjectId;
private bool _disposed;
// Digit sprite DID arrays for slot labels (top row, numbers 1-9).
@ -103,7 +106,8 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
Action<uint>? selectItem = null,
Func<uint>? selectedObjectId = null,
Func<uint>? playerGuid = null,
Action<uint, uint, int>? sendPutItemInContainer = null)
Action<uint, uint, int>? sendPutItemInContainer = null,
UiDatFont? ammoFont = null)
{
_repo = repo;
_combatState = combatState;
@ -149,6 +153,13 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
button.OnClick = toggleCombat;
}
_ammoIndicator = layout.FindElement(AmmoIndicatorId) as UiButton;
if (_ammoIndicator is not null)
{
_ammoIndicator.LabelFont = ammoFont;
_ammoIndicator.LabelColor = System.Numerics.Vector4.One;
}
foreach (uint elementId in PanelButtonIds)
{
if (layout.FindElement(elementId) is UiButton panelButton
@ -189,14 +200,60 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
repo.ObjectAdded += OnRepositoryObjectChanged;
repo.ObjectUpdated += OnRepositoryObjectChanged;
repo.ObjectRemoved += OnRepositoryObjectChanged;
repo.ObjectMoved += OnRepositoryObjectMoved;
RefreshAmmo();
}
private void OnRepositoryObjectChanged(ClientObject obj)
{
if (IsAmmoRelated(obj))
RefreshAmmo();
if (IsShortcutGuid(obj.ObjectId))
Populate();
}
private void OnRepositoryObjectMoved(ClientObject obj, uint oldContainer, uint newContainer)
{
uint player = _playerGuid?.Invoke() ?? 0u;
if (obj.ObjectId == _ammoObjectId
|| (player != 0 && (oldContainer == player || newContainer == player)))
RefreshAmmo();
if (IsShortcutGuid(obj.ObjectId))
Populate();
}
private bool IsAmmoRelated(ClientObject obj)
{
uint player = _playerGuid?.Invoke() ?? 0u;
return obj.ObjectId == _ammoObjectId
|| (player != 0 && obj.ObjectId == player)
|| (player != 0
&& obj.ContainerId == player
&& (obj.CurrentlyEquippedLocation
& (EquipMask.MissileWeapon | EquipMask.MissileAmmo)) != 0);
}
private void RefreshAmmo()
{
uint player = _playerGuid?.Invoke() ?? 0u;
var placements = new List<ClientObject>();
if (player != 0)
{
foreach (uint objectId in _repo.GetContents(player))
if (_repo.Get(objectId) is { } item)
placements.Add(item);
}
ToolbarAmmoPolicy.Result result = ToolbarAmmoPolicy.Resolve(placements);
_ammoObjectId = result.ObjectId;
if (_ammoIndicator is not null)
{
_ammoIndicator.Label = result.IsVisible
? result.DisplayCount.ToString(System.Globalization.CultureInfo.InvariantCulture)
: null;
}
}
/// <summary>
/// Returns true if <paramref name="guid"/> is one of the currently-active shortcut guids
/// (i.e., present in the live <see cref="AcDream.Core.Items.ShortcutStore"/>).
@ -264,12 +321,13 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
Action<uint>? selectItem = null,
Func<uint>? selectedObjectId = null,
Func<uint>? playerGuid = null,
Action<uint, uint, int>? sendPutItemInContainer = null)
Action<uint, uint, int>? sendPutItemInContainer = null,
UiDatFont? ammoFont = null)
{
var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem, combatState,
peaceDigits, warDigits, emptyDigits, itemInteraction,
sendAddShortcut, sendRemoveShortcut, toggleCombat, selectItem,
selectedObjectId, playerGuid, sendPutItemInContainer);
selectedObjectId, playerGuid, sendPutItemInContainer, ammoFont);
c.Populate();
return c;
}
@ -638,5 +696,6 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
_repo.ObjectAdded -= OnRepositoryObjectChanged;
_repo.ObjectUpdated -= OnRepositoryObjectChanged;
_repo.ObjectRemoved -= OnRepositoryObjectChanged;
_repo.ObjectMoved -= OnRepositoryObjectMoved;
}
}

View file

@ -390,7 +390,8 @@ public sealed class RetailUiRuntime : IDisposable
selectItem: guid => b.Selection.Select(guid, SelectionChangeSource.Toolbar),
selectedObjectId: () => b.Selection.SelectedObjectId ?? 0u,
playerGuid: b.PlayerGuid,
sendPutItemInContainer: b.SendPutItemInContainer);
sendPutItemInContainer: b.SendPutItemInContainer,
ammoFont: _bindings.Assets.DefaultFont);
ToolbarInputController = new ToolbarInputController(ToolbarController, b.Selection);
SelectedObjectController = Layout.SelectedObjectController.Bind(
layout,