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>
47 lines
1.7 KiB
C#
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;
|
|
}
|
|
}
|