acdream/src/AcDream.Core/Items/ToolbarAmmoPolicy.cs
Erik 564d39dfea 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>
2026-07-11 12:29:51 +02:00

47 lines
1.7 KiB
C#

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;
}
}