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:
parent
400f01eef1
commit
564d39dfea
9 changed files with 330 additions and 7 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
47
src/AcDream.Core/Items/ToolbarAmmoPolicy.cs
Normal file
47
src/AcDream.Core/Items/ToolbarAmmoPolicy.cs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
namespace AcDream.Core.Items;
|
||||
|
||||
/// <summary>
|
||||
/// Pure retail toolbar ammunition selection.
|
||||
/// Retail references: <c>gmToolbarUI::UpdateAmmoID @ 0x004BF210</c> and
|
||||
/// <c>gmToolbarUI::UpdateAmmoNumber @ 0x004BE9E0</c>.
|
||||
/// </summary>
|
||||
public static class ToolbarAmmoPolicy
|
||||
{
|
||||
public readonly record struct Result(uint ObjectId, int DisplayCount)
|
||||
{
|
||||
public bool IsVisible => ObjectId != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve the first equipped missile weapon. If it is stackable, retail
|
||||
/// treats that stack as its own ammunition (thrown weapons). Otherwise it
|
||||
/// resolves the first separately equipped missile-ammo object.
|
||||
/// </summary>
|
||||
public static Result Resolve(IReadOnlyList<ClientObject> inventoryPlacements)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(inventoryPlacements);
|
||||
|
||||
ClientObject? missileWeapon = FindAtLocation(
|
||||
inventoryPlacements,
|
||||
EquipMask.MissileWeapon);
|
||||
ClientObject? ammo = missileWeapon is { StackSizeMax: > 1 }
|
||||
? missileWeapon
|
||||
: FindAtLocation(inventoryPlacements, EquipMask.MissileAmmo);
|
||||
|
||||
return ammo is null
|
||||
? default
|
||||
: new Result(ammo.ObjectId, ammo.StackSize == 0 ? 1 : ammo.StackSize);
|
||||
}
|
||||
|
||||
private static ClientObject? FindAtLocation(
|
||||
IReadOnlyList<ClientObject> inventoryPlacements,
|
||||
EquipMask location)
|
||||
{
|
||||
// ACCWeenieObject::GetObjectAtLocation @ 0x0058CE00 returns the first
|
||||
// inventory placement whose location mask intersects the requested mask.
|
||||
foreach (ClientObject item in inventoryPlacements)
|
||||
if ((item.CurrentlyEquippedLocation & location) != 0)
|
||||
return item;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue