merge(core): the enum verification campaign, onto the post-deletion tree
Brings `github/overnight/enums` (`c19680fd`) forward onto the V11 tree. The branch was cut at `b70b9832`, before the OpenGL deletion, and the two lines of work turned out to be disjoint: the enum campaign lives entirely in `AcDream.Core` and its tests, while V11 emptied `AcDream.App`. The merge is clean — no conflicting file on either side. What it carries: names for AC's seven property tables verified against two oracles, a correction to `DamageType`'s rotated bits and `ItemType`'s shifted craft ladder, the retail members the equipment and physics enums were missing, and names for `AmmoType`, `CombatUse` and `ItemUseable`. Five commits, seventeen files, +3,767 / -27 lines. Verified on the merge result rather than on the branch: Release build 0 errors, no new warning attributable to any file the branch touches, and `AcDream.Core.Tests` at 3,893 passed / 2 skipped / 3,895. The campaign's claimed +597 is exact — the `Properties` namespace alone runs 597 tests, all passing. The campaign's open decision items — whether to adopt `WeenieError` wholesale, whether the `SoundId` subset is the right cut, and the re-clone of the ACE and Chorizite references that `references/` no longer holds — are not settled here. They are carried into the morning report as questions for the user. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
commit
cd2f3feae2
17 changed files with 3767 additions and 27 deletions
|
|
@ -0,0 +1,133 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
1398
tests/AcDream.Core.Tests/Properties/PropertyEnumConformanceTests.cs
Normal file
1398
tests/AcDream.Core.Tests/Properties/PropertyEnumConformanceTests.cs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,285 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Physics;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Properties;
|
||||
|
||||
/// <summary>
|
||||
/// Pins the wire-adjacent enums acdream shares with the retail client to the values
|
||||
/// in the Sept 2013 EoR header, <c>docs/research/named-retail/acclient.h</c>. Each
|
||||
/// table below is transcribed from the cited <c>enum</c> block, so a hand edit that
|
||||
/// drifts from retail fails here.
|
||||
///
|
||||
/// <para>These tables are the reason the 2026-07-29 enum campaign found real bugs:
|
||||
/// <see cref="DamageType"/> had its four drain/restore bits rotated, and
|
||||
/// <see cref="ItemType"/> had a shifted craft ladder plus locally recomputed composite
|
||||
/// masks. Both are corrected and pinned below.</para>
|
||||
/// </summary>
|
||||
public sealed class RetailEnumConformanceTests
|
||||
{
|
||||
/// <summary>acclient.h:3788, <c>enum DAMAGE_TYPE</c>. FORCE_*_32_BIT omitted.</summary>
|
||||
public static TheoryData<string, uint> RetailDamageType => new()
|
||||
{
|
||||
{ "Undef", 0x0 },
|
||||
{ "Slash", 0x1 },
|
||||
{ "Pierce", 0x2 },
|
||||
{ "Bludgeon", 0x4 },
|
||||
{ "Cold", 0x8 },
|
||||
{ "Fire", 0x10 },
|
||||
{ "Acid", 0x20 },
|
||||
{ "Electric", 0x40 },
|
||||
{ "Health", 0x80 },
|
||||
{ "Stamina", 0x100 },
|
||||
{ "Mana", 0x200 },
|
||||
{ "Nether", 0x400 },
|
||||
{ "Base", 0x10000000 },
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(RetailDamageType))]
|
||||
public void DamageTypeMatchesRetail(string name, uint value)
|
||||
{
|
||||
Assert.True(Enum.IsDefined(typeof(DamageType), name),
|
||||
$"DamageType.{name} is missing");
|
||||
Assert.Equal(value, (uint)Enum.Parse<DamageType>(name));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DamageTypeDeclaresNothingRetailDoesNot()
|
||||
{
|
||||
var expected = RetailDamageType.Select(r => (string)r[0]).OrderBy(n => n, StringComparer.Ordinal);
|
||||
var actual = Enum.GetNames<DamageType>().OrderBy(n => n, StringComparer.Ordinal);
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// acclient.h:3300, <c>enum ITEM_TYPE</c>. TYPE_UNDEF and TYPE_SELF both sit on 0
|
||||
/// in retail; acdream spells that single zero <c>None</c>. FORCE_*_32_BIT omitted.
|
||||
/// </summary>
|
||||
public static TheoryData<string, uint> RetailItemType => new()
|
||||
{
|
||||
{ "None", 0x0 },
|
||||
{ "MeleeWeapon", 0x1 },
|
||||
{ "Armor", 0x2 },
|
||||
{ "Vestements", 0x6 },
|
||||
{ "Clothing", 0x4 },
|
||||
{ "Jewelry", 0x8 },
|
||||
{ "Creature", 0x10 },
|
||||
{ "Food", 0x20 },
|
||||
{ "Money", 0x40 },
|
||||
{ "Misc", 0x80 },
|
||||
{ "MissileWeapon", 0x100 },
|
||||
{ "Weapon", 0x101 },
|
||||
{ "Container", 0x200 },
|
||||
{ "LockableMagicTarget", 0x280 },
|
||||
{ "Useless", 0x400 },
|
||||
{ "Gem", 0x800 },
|
||||
{ "SpellComponents", 0x1000 },
|
||||
{ "Writable", 0x2000 },
|
||||
{ "Key", 0x4000 },
|
||||
{ "Caster", 0x8000 },
|
||||
{ "WeaponOrCaster", 0x8101 },
|
||||
{ "RedirectableItemEnchantmentTarget", 0x8107 },
|
||||
{ "Portal", 0x10000 },
|
||||
{ "Lockable", 0x20000 },
|
||||
{ "PromissoryNote", 0x40000 },
|
||||
{ "ManaStone", 0x80000 },
|
||||
{ "ItemEnchantableTarget", 0x88B8F },
|
||||
{ "Service", 0x100000 },
|
||||
{ "MagicWieldable", 0x200000 },
|
||||
{ "Item", 0x2DFBEF },
|
||||
{ "CraftCookingBase", 0x400000 },
|
||||
{ "VendorGrocer", 0x446220 },
|
||||
{ "CraftAlchemyBase", 0x800000 },
|
||||
{ "CraftFletchingBase", 0x1000000 },
|
||||
{ "CraftAlchemyIntermediate", 0x4000000 },
|
||||
{ "CraftFletchingIntermediate", 0x8000000 },
|
||||
{ "LifeStone", 0x10000000 },
|
||||
{ "PortalMagicTarget", 0x10010000 },
|
||||
{ "TinkeringTool", 0x20000000 },
|
||||
{ "TinkeringMaterial", 0x40000000 },
|
||||
{ "VendorShopkeep", 0x480467A7 },
|
||||
{ "Gameboard", 0x80000000 },
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(RetailItemType))]
|
||||
public void ItemTypeMatchesRetail(string name, uint value)
|
||||
{
|
||||
Assert.True(Enum.IsDefined(typeof(ItemType), name),
|
||||
$"ItemType.{name} is missing");
|
||||
Assert.Equal(value, (uint)Enum.Parse<ItemType>(name));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItemTypeDeclaresNothingRetailDoesNot()
|
||||
{
|
||||
var expected = RetailItemType.Select(r => (string)r[0]).OrderBy(n => n, StringComparer.Ordinal);
|
||||
var actual = Enum.GetNames<ItemType>().OrderBy(n => n, StringComparer.Ordinal);
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The craft ladder specifically: retail leaves 0x02000000 unused, and the ACE
|
||||
/// weenie corpus attests 0x04000000 as Craft_Alchemy_Intermediate 235 times. This
|
||||
/// is the pairing acdream had wrong.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CraftLadderMatchesRetailAndLeavesTheUnusedBitUnclaimed()
|
||||
{
|
||||
Assert.Equal(0x00400000u, (uint)ItemType.CraftCookingBase);
|
||||
Assert.Equal(0x00800000u, (uint)ItemType.CraftAlchemyBase);
|
||||
Assert.Equal(0x01000000u, (uint)ItemType.CraftFletchingBase);
|
||||
Assert.Equal(0x04000000u, (uint)ItemType.CraftAlchemyIntermediate);
|
||||
Assert.Equal(0x08000000u, (uint)ItemType.CraftFletchingIntermediate);
|
||||
Assert.DoesNotContain(Enum.GetValues<ItemType>(), t => (uint)t == 0x02000000u);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>Weapon</c> and <c>WeaponOrCaster</c> were aliases before the campaign, because
|
||||
/// <c>Weapon</c> was recomputed locally as melee|missile|caster. Retail keeps them
|
||||
/// distinct and only the latter includes the caster bit.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WeaponExcludesCasterAndWeaponOrCasterIncludesIt()
|
||||
{
|
||||
Assert.NotEqual(ItemType.Weapon, ItemType.WeaponOrCaster);
|
||||
Assert.Equal(ItemType.MeleeWeapon | ItemType.MissileWeapon, ItemType.Weapon);
|
||||
Assert.Equal(ItemType.Weapon | ItemType.Caster, ItemType.WeaponOrCaster);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// acclient.h:3193, <c>enum INVENTORY_LOC</c> — the composite slot groups only. The
|
||||
/// 32 primitive slots were already correct and are pinned by <c>EquipMaskTests</c>.
|
||||
/// </summary>
|
||||
public static TheoryData<string, uint> RetailEquipMaskComposites => new()
|
||||
{
|
||||
{ "Clothing", 0x080001FF },
|
||||
{ "Armor", 0x00007E00 },
|
||||
{ "Jewelry", 0x7C0F8000 },
|
||||
{ "WristWear", 0x00030000 },
|
||||
{ "FingerWear", 0x000C0000 },
|
||||
{ "Sigil", 0x70000000 },
|
||||
{ "ReadySlot", 0x03F00000 },
|
||||
{ "Weapon", 0x02500000 },
|
||||
{ "WeaponReadySlot", 0x03500000 },
|
||||
{ "All", 0x7FFFFFFF },
|
||||
{ "CanGoInReadySlot", 0x7FFFFFFF },
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(RetailEquipMaskComposites))]
|
||||
public void EquipMaskCompositeMatchesRetail(string name, uint value)
|
||||
{
|
||||
Assert.True(Enum.IsDefined(typeof(EquipMask), name), $"EquipMask.{name} is missing");
|
||||
Assert.Equal(value, (uint)Enum.Parse<EquipMask>(name));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail's CLOTHING_LOC is the nine wear slots plus the cloak slot (bit 27) — it is
|
||||
/// NOT the union of the wear slots alone. A type remark here used to claim the extra
|
||||
/// bit was 31 and unnamed; it is 27 and it is <see cref="EquipMask.Cloak"/>.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ClothingCompositeIsTheWearSlotsPlusCloak()
|
||||
{
|
||||
EquipMask wearSlots =
|
||||
EquipMask.HeadWear | EquipMask.ChestWear | EquipMask.AbdomenWear
|
||||
| EquipMask.UpperArmWear | EquipMask.LowerArmWear | EquipMask.HandWear
|
||||
| EquipMask.UpperLegWear | EquipMask.LowerLegWear | EquipMask.FootWear;
|
||||
|
||||
Assert.Equal(0x000001FFu, (uint)wearSlots);
|
||||
Assert.Equal(EquipMask.Clothing, wearSlots | EquipMask.Cloak);
|
||||
Assert.NotEqual(EquipMask.Clothing, wearSlots);
|
||||
Assert.Equal(0u, (uint)EquipMask.Clothing & 0x80000000u);
|
||||
}
|
||||
|
||||
/// <summary>acclient.h:3688, <c>enum TransientState</c>.</summary>
|
||||
public static TheoryData<string, uint> RetailTransientState => new()
|
||||
{
|
||||
{ "Contact", 0x1 },
|
||||
{ "OnWalkable", 0x2 },
|
||||
{ "Sliding", 0x4 },
|
||||
{ "WaterContact", 0x8 },
|
||||
{ "StationaryFall", 0x10 },
|
||||
{ "StationaryStop", 0x20 },
|
||||
{ "StationaryStuck", 0x40 },
|
||||
{ "Active", 0x80 },
|
||||
{ "CheckEthereal", 0x100 },
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(RetailTransientState))]
|
||||
public void TransientStateFlagsMatchesRetail(string name, uint value)
|
||||
{
|
||||
Assert.True(Enum.IsDefined(typeof(TransientStateFlags), name),
|
||||
$"TransientStateFlags.{name} is missing");
|
||||
Assert.Equal(value, (uint)Enum.Parse<TransientStateFlags>(name));
|
||||
}
|
||||
|
||||
/// <summary>acclient.h:2815, <c>enum PhysicsState</c>, including the two bits retail itself reserves.</summary>
|
||||
public static TheoryData<string, uint> RetailPhysicsState => new()
|
||||
{
|
||||
{ "Static", 0x1 },
|
||||
{ "ReservedUnused1", 0x2 },
|
||||
{ "Ethereal", 0x4 },
|
||||
{ "ReportCollisions", 0x8 },
|
||||
{ "IgnoreCollisions", 0x10 },
|
||||
{ "NoDraw", 0x20 },
|
||||
{ "Missile", 0x40 },
|
||||
{ "Pushable", 0x80 },
|
||||
{ "AlignPath", 0x100 },
|
||||
{ "PathClipped", 0x200 },
|
||||
{ "Gravity", 0x400 },
|
||||
{ "Lighting", 0x800 },
|
||||
{ "ParticleEmitter", 0x1000 },
|
||||
{ "ReservedUnused2", 0x2000 },
|
||||
{ "Hidden", 0x4000 },
|
||||
{ "ScriptedCollision", 0x8000 },
|
||||
{ "HasPhysicsBsp", 0x10000 },
|
||||
{ "Inelastic", 0x20000 },
|
||||
{ "HasDefaultAnim", 0x40000 },
|
||||
{ "HasDefaultScript", 0x80000 },
|
||||
{ "Cloaked", 0x100000 },
|
||||
{ "ReportAsEnvironment", 0x200000 },
|
||||
{ "EdgeSlide", 0x400000 },
|
||||
{ "Sledding", 0x800000 },
|
||||
{ "Frozen", 0x1000000 },
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(RetailPhysicsState))]
|
||||
public void PhysicsStateFlagsMatchesRetail(string name, uint value)
|
||||
{
|
||||
Assert.True(Enum.IsDefined(typeof(PhysicsStateFlags), name),
|
||||
$"PhysicsStateFlags.{name} is missing");
|
||||
Assert.Equal(value, (uint)Enum.Parse<PhysicsStateFlags>(name));
|
||||
}
|
||||
|
||||
/// <summary>acclient.h:4371, <c>enum ATTACK_HEIGHT</c>. NUM_ATTACK_HEIGHTS is a count, not a height.</summary>
|
||||
[Theory]
|
||||
[InlineData("Undef", 0)]
|
||||
[InlineData("High", 1)]
|
||||
[InlineData("Medium", 2)]
|
||||
[InlineData("Low", 3)]
|
||||
public void AttackHeightMatchesRetail(string name, int value)
|
||||
{
|
||||
Assert.True(Enum.IsDefined(typeof(AttackHeight), name), $"AttackHeight.{name} is missing");
|
||||
Assert.Equal(value, (int)Enum.Parse<AttackHeight>(name));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// acclient.h:3807, <c>enum AttackType</c>. The two composites are the interesting
|
||||
/// part: retail spells Unarmed 0x19 and MultiStrike 0x79E0, and acdream derives both
|
||||
/// from its primitives. This asserts the derivation lands on retail's literal.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AttackTypeCompositesMatchRetailLiterals()
|
||||
{
|
||||
Assert.Equal(0x19u, (uint)AttackType.Unarmed);
|
||||
Assert.Equal(0x79E0u, (uint)AttackType.MultiStrike);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue