Three more fields acdream already pulls off the wire and then carries as bare numbers. AmmoType and MaterialType ride PublicWeenieDesc through CreateObject and land on ClientObject as ushort/uint; ItemUseable and CombatUse arrive as PropertyInt 16 and 51. Nothing named them, so every site that reasoned about them did it in hex. AmmoType (acclient.h:4221) and CombatUse (acclient.h:6523) are small and unsurprising. ItemUseable (acclient.h:6478) is neither: it is two 16-bit halves, low for where the used object must be and high for where its target must be, and retail names roughly thirty specific combinations rather than expecting callers to compose them. They are transcribed rather than composed because at least one is not the union it looks like - SOURCE_CONTAINED_TARGET_OBJSELF_OR_CONTAINED is 0x880008, where composing ObjSelf|Contained|(Contained shifted 16) gives 0x800088. A test asserts that specific non-equality so the shortcut cannot be reintroduced. ItemAppraisalTextFormatter's ammunition sentence now reads through AmmoType instead of matching 0x08/0x40/0x10/0x80/0x20/0x100 literals. The fold it performs - crystal and chorizite variants collapsing to their base arrow/bolt/atlatl kind - was already exactly right against retail's bit layout; this only gives it vocabulary. No behavior change, and the appraisal tests confirm it. Core tests 3,836 -> 3,894. Full suite 9,759 passed / 5 skipped, no failures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
133 lines
5.1 KiB
C#
133 lines
5.1 KiB
C#
using System;
|
|
using System.Linq;
|
|
using AcDream.Core.Items;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Tests.Properties;
|
|
|
|
/// <summary>
|
|
/// Pins the three item-data enums the 2026-07-29 campaign added — all of them describe
|
|
/// fields acdream already parses off the wire and stored as bare numbers. Tables are
|
|
/// transcribed from <c>docs/research/named-retail/acclient.h</c>.
|
|
/// </summary>
|
|
public sealed class ItemWireEnumConformanceTests
|
|
{
|
|
/// <summary>acclient.h:4221, <c>enum AMMO_TYPE</c>.</summary>
|
|
public static TheoryData<string, uint> RetailAmmoType => new()
|
|
{
|
|
{ "None", 0x0 },
|
|
{ "Arrow", 0x1 },
|
|
{ "Bolt", 0x2 },
|
|
{ "Atlatl", 0x4 },
|
|
{ "ArrowCrystal", 0x8 },
|
|
{ "BoltCrystal", 0x10 },
|
|
{ "AtlatlCrystal", 0x20 },
|
|
{ "ArrowChorizite", 0x40 },
|
|
{ "BoltChorizite", 0x80 },
|
|
{ "AtlatlChorizite", 0x100 },
|
|
};
|
|
|
|
[Theory]
|
|
[MemberData(nameof(RetailAmmoType))]
|
|
public void AmmoTypeMatchesRetail(string name, uint value)
|
|
{
|
|
Assert.True(Enum.IsDefined(typeof(AmmoType), name), $"AmmoType.{name} is missing");
|
|
Assert.Equal(value, (uint)Enum.Parse<AmmoType>(name));
|
|
}
|
|
|
|
[Fact]
|
|
public void AmmoTypeDeclaresNothingRetailDoesNot()
|
|
{
|
|
var expected = RetailAmmoType.Select(r => (string)r[0]).OrderBy(n => n, StringComparer.Ordinal);
|
|
Assert.Equal(expected, Enum.GetNames<AmmoType>().OrderBy(n => n, StringComparer.Ordinal));
|
|
}
|
|
|
|
/// <summary>acclient.h:6523, <c>enum COMBAT_USE</c>.</summary>
|
|
[Theory]
|
|
[InlineData("None", 0u)]
|
|
[InlineData("Melee", 1u)]
|
|
[InlineData("Missile", 2u)]
|
|
[InlineData("Ammo", 3u)]
|
|
[InlineData("Shield", 4u)]
|
|
[InlineData("TwoHanded", 5u)]
|
|
public void CombatUseMatchesRetail(string name, uint value)
|
|
{
|
|
Assert.True(Enum.IsDefined(typeof(CombatUse), name), $"CombatUse.{name} is missing");
|
|
Assert.Equal(value, (uint)Enum.Parse<CombatUse>(name));
|
|
}
|
|
|
|
/// <summary>acclient.h:6478, <c>enum ITEM_USEABLE</c>, including the composites.</summary>
|
|
public static TheoryData<string, uint> RetailItemUseable => new()
|
|
{
|
|
{ "Undef", 0x0 },
|
|
{ "No", 0x1 },
|
|
{ "Self", 0x2 },
|
|
{ "Wielded", 0x4 },
|
|
{ "Contained", 0x8 },
|
|
{ "Viewed", 0x10 },
|
|
{ "ContainedViewed", 0x18 },
|
|
{ "Remote", 0x20 },
|
|
{ "ViewedRemote", 0x30 },
|
|
{ "ContainedViewedRemote", 0x38 },
|
|
{ "NeverWalk", 0x40 },
|
|
{ "RemoteNeverWalk", 0x60 },
|
|
{ "ViewedRemoteNeverWalk", 0x70 },
|
|
{ "ContainedViewedRemoteNeverWalk", 0x78 },
|
|
{ "ObjSelf", 0x80 },
|
|
{ "SourceMask", 0xFFFF },
|
|
{ "SourceWieldedTargetWielded", 0x40004 },
|
|
{ "SourceContainedTargetWielded", 0x40008 },
|
|
{ "SourceViewedTargetWielded", 0x40010 },
|
|
{ "SourceRemoteTargetWielded", 0x40020 },
|
|
{ "SourceWieldedTargetContained", 0x80004 },
|
|
{ "SourceContainedTargetContained", 0x80008 },
|
|
{ "SourceViewedTargetContained", 0x80010 },
|
|
{ "SourceRemoteTargetContained", 0x80020 },
|
|
{ "SourceContainedTargetSelfOrContained", 0xA0008 },
|
|
{ "SourceWieldedTargetViewed", 0x100004 },
|
|
{ "SourceContainedTargetViewed", 0x100008 },
|
|
{ "SourceViewedTargetViewed", 0x100010 },
|
|
{ "SourceRemoteTargetViewed", 0x100020 },
|
|
{ "SourceWieldedTargetRemote", 0x200004 },
|
|
{ "SourceContainedTargetRemote", 0x200008 },
|
|
{ "SourceViewedTargetRemote", 0x200010 },
|
|
{ "SourceRemoteTargetRemote", 0x200020 },
|
|
{ "SourceContainedTargetRemoteOrSelf", 0x220008 },
|
|
{ "SourceWieldedTargetRemoteNeverWalk", 0x600004 },
|
|
{ "SourceContainedTargetRemoteNeverWalk", 0x600008 },
|
|
{ "SourceRemoteTargetRemoteNeverWalk", 0x600020 },
|
|
{ "SourceContainedTargetObjSelfOrContained", 0x880008 },
|
|
{ "TargetMask", 0xFFFF0000 },
|
|
};
|
|
|
|
[Theory]
|
|
[MemberData(nameof(RetailItemUseable))]
|
|
public void ItemUseableMatchesRetail(string name, uint value)
|
|
{
|
|
Assert.True(Enum.IsDefined(typeof(ItemUseable), name), $"ItemUseable.{name} is missing");
|
|
Assert.Equal(value, (uint)Enum.Parse<ItemUseable>(name));
|
|
}
|
|
|
|
[Fact]
|
|
public void ItemUseableDeclaresNothingRetailDoesNot()
|
|
{
|
|
var expected = RetailItemUseable.Select(r => (string)r[0]).OrderBy(n => n, StringComparer.Ordinal);
|
|
Assert.Equal(expected, Enum.GetNames<ItemUseable>().OrderBy(n => n, StringComparer.Ordinal));
|
|
}
|
|
|
|
/// <summary>
|
|
/// The two halves partition the word, and at least one retail composite is
|
|
/// deliberately not the obvious union — which is why they are transcribed rather
|
|
/// than composed.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ItemUseableSourceAndTargetMasksPartitionTheWord()
|
|
{
|
|
Assert.Equal(0u, (uint)ItemUseable.SourceMask & (uint)ItemUseable.TargetMask);
|
|
Assert.Equal(uint.MaxValue, (uint)ItemUseable.SourceMask | (uint)ItemUseable.TargetMask);
|
|
|
|
uint naiveUnion = (uint)ItemUseable.ObjSelf | (uint)ItemUseable.Contained
|
|
| ((uint)ItemUseable.Contained << 16);
|
|
Assert.NotEqual(naiveUnion, (uint)ItemUseable.SourceContainedTargetObjSelfOrContained);
|
|
}
|
|
}
|