//
// Golden conformance table for SoundId. See
// docs/research/2026-07-29-enum-verification-campaign.md ยง8.2.
//
using System;
using System.Linq;
using AcDream.Core.Audio;
using Xunit;
namespace AcDream.Core.Tests.Audio;
///
/// Pins every member of to its retail value. The table is
/// transcribed from acclient.h:4569 enum SoundType and independently agrees
/// with ACE Sound and DatReaderWriter Sound on every name and value.
/// Retail's trailing NUM_SOUND_TYPES and FORCE_SoundType_32_BIT are a
/// count and a width pin, not sounds, and are deliberately absent.
///
public sealed class SoundIdConformanceTests
{
private static readonly (string Name, uint Value)[] Golden =
[
("Invalid", 0x00),
("Speak1", 0x01),
("Random", 0x02),
("Attack1", 0x03),
("Attack2", 0x04),
("Attack3", 0x05),
("SpecialAttack1", 0x06),
("SpecialAttack2", 0x07),
("SpecialAttack3", 0x08),
("Damage1", 0x09),
("Damage2", 0x0A),
("Damage3", 0x0B),
("Wound1", 0x0C),
("Wound2", 0x0D),
("Wound3", 0x0E),
("Death1", 0x0F),
("Death2", 0x10),
("Death3", 0x11),
("Grunt1", 0x12),
("Grunt2", 0x13),
("Grunt3", 0x14),
("Oh1", 0x15),
("Oh2", 0x16),
("Oh3", 0x17),
("Heave1", 0x18),
("Heave2", 0x19),
("Heave3", 0x1A),
("Knockdown1", 0x1B),
("Knockdown2", 0x1C),
("Knockdown3", 0x1D),
("Swoosh1", 0x1E),
("Swoosh2", 0x1F),
("Swoosh3", 0x20),
("Thump1", 0x21),
("Smash1", 0x22),
("Scratch1", 0x23),
("Spear", 0x24),
("Sling", 0x25),
("Dagger", 0x26),
("ArrowWhiz1", 0x27),
("ArrowWhiz2", 0x28),
("CrossbowPull", 0x29),
("CrossbowRelease", 0x2A),
("BowPull", 0x2B),
("BowRelease", 0x2C),
("ThrownWeaponRelease1", 0x2D),
("ArrowLand", 0x2E),
("Collision", 0x2F),
("HitFlesh1", 0x30),
("HitLeather1", 0x31),
("HitChain1", 0x32),
("HitPlate1", 0x33),
("HitMissile1", 0x34),
("HitMissile2", 0x35),
("HitMissile3", 0x36),
("Footstep1", 0x37),
("Footstep2", 0x38),
("Walk1", 0x39),
("Dance1", 0x3A),
("Dance2", 0x3B),
("Dance3", 0x3C),
("Hidden1", 0x3D),
("Hidden2", 0x3E),
("Hidden3", 0x3F),
("Eat1", 0x40),
("Drink1", 0x41),
("Open", 0x42),
("Close", 0x43),
("OpenSlam", 0x44),
("CloseSlam", 0x45),
("Ambient1", 0x46),
("Ambient2", 0x47),
("Ambient3", 0x48),
("Ambient4", 0x49),
("Ambient5", 0x4A),
("Ambient6", 0x4B),
("Ambient7", 0x4C),
("Ambient8", 0x4D),
("Waterfall", 0x4E),
("LogOut", 0x4F),
("LogIn", 0x50),
("LifestoneOn", 0x51),
("AttribUp", 0x52),
("AttribDown", 0x53),
("SkillUp", 0x54),
("SkillDown", 0x55),
("HealthUp", 0x56),
("HealthDown", 0x57),
("ShieldUp", 0x58),
("ShieldDown", 0x59),
("EnchantUp", 0x5A),
("EnchantDown", 0x5B),
("VisionUp", 0x5C),
("VisionDown", 0x5D),
("Fizzle", 0x5E),
("Launch", 0x5F),
("Explode", 0x60),
("TransUp", 0x61),
("TransDown", 0x62),
("BreatheFlaem", 0x63),
("BreatheAcid", 0x64),
("BreatheFrost", 0x65),
("BreatheLightning", 0x66),
("Create", 0x67),
("Destroy", 0x68),
("Lockpicking", 0x69),
("UI_EnterPortal", 0x6A),
("UI_ExitPortal", 0x6B),
("UI_GeneralQuery", 0x6C),
("UI_GeneralError", 0x6D),
("UI_TransientMessage", 0x6E),
("UI_IconPickUp", 0x6F),
("UI_IconSuccessfulDrop", 0x70),
("UI_IconInvalid_Drop", 0x71),
("UI_ButtonPress", 0x72),
("UI_GrabSlider", 0x73),
("UI_ReleaseSlider", 0x74),
("UI_NewTargetSelected", 0x75),
("UI_Roar", 0x76),
("UI_Bell", 0x77),
("UI_Chant1", 0x78),
("UI_Chant2", 0x79),
("UI_DarkWhispers1", 0x7A),
("UI_DarkWhispers2", 0x7B),
("UI_DarkLaugh", 0x7C),
("UI_DarkWind", 0x7D),
("UI_DarkSpeech", 0x7E),
("UI_Drums", 0x7F),
("UI_GhostSpeak", 0x80),
("UI_Breathing", 0x81),
("UI_Howl", 0x82),
("UI_LostSouls", 0x83),
("UI_Squeal", 0x84),
("UI_Thunder1", 0x85),
("UI_Thunder2", 0x86),
("UI_Thunder3", 0x87),
("UI_Thunder4", 0x88),
("UI_Thunder5", 0x89),
("UI_Thunder6", 0x8A),
("RaiseTrait", 0x8B),
("WieldObject", 0x8C),
("UnwieldObject", 0x8D),
("ReceiveItem", 0x8E),
("PickUpItem", 0x8F),
("DropItem", 0x90),
("ResistSpell", 0x91),
("PicklockFail", 0x92),
("LockSuccess", 0x93),
("OpenFailDueToLock", 0x94),
("TriggerActivated", 0x95),
("SpellExpire", 0x96),
("ItemManaDepleted", 0x97),
("TriggerActivated1", 0x98),
("TriggerActivated2", 0x99),
("TriggerActivated3", 0x9A),
("TriggerActivated4", 0x9B),
("TriggerActivated5", 0x9C),
("TriggerActivated6", 0x9D),
("TriggerActivated7", 0x9E),
("TriggerActivated8", 0x9F),
("TriggerActivated9", 0xA0),
("TriggerActivated10", 0xA1),
("TriggerActivated11", 0xA2),
("TriggerActivated12", 0xA3),
("TriggerActivated13", 0xA4),
("TriggerActivated14", 0xA5),
("TriggerActivated15", 0xA6),
("TriggerActivated16", 0xA7),
("TriggerActivated17", 0xA8),
("TriggerActivated18", 0xA9),
("TriggerActivated19", 0xAA),
("TriggerActivated20", 0xAB),
("TriggerActivated21", 0xAC),
("TriggerActivated22", 0xAD),
("TriggerActivated23", 0xAE),
("TriggerActivated24", 0xAF),
("TriggerActivated25", 0xB0),
("TriggerActivated26", 0xB1),
("TriggerActivated27", 0xB2),
("TriggerActivated28", 0xB3),
("TriggerActivated29", 0xB4),
("TriggerActivated30", 0xB5),
("TriggerActivated31", 0xB6),
("TriggerActivated32", 0xB7),
("TriggerActivated33", 0xB8),
("TriggerActivated34", 0xB9),
("TriggerActivated35", 0xBA),
("TriggerActivated36", 0xBB),
("TriggerActivated37", 0xBC),
("TriggerActivated38", 0xBD),
("TriggerActivated39", 0xBE),
("TriggerActivated40", 0xBF),
("TriggerActivated41", 0xC0),
("TriggerActivated42", 0xC1),
("TriggerActivated43", 0xC2),
("TriggerActivated44", 0xC3),
("TriggerActivated45", 0xC4),
("TriggerActivated46", 0xC5),
("TriggerActivated47", 0xC6),
("TriggerActivated48", 0xC7),
("TriggerActivated49", 0xC8),
("TriggerActivated50", 0xC9),
("HealthDownVoid", 0xCA),
("RegenDownVoid", 0xCB),
("SkillDownVoid", 0xCC),
];
[Fact]
public void EveryRetailSoundHasItsExactValue()
{
foreach (var (name, value) in Golden)
{
Assert.True(Enum.IsDefined(typeof(SoundId), name), $"SoundId.{name} is missing");
Assert.Equal(value, (uint)Enum.Parse(name));
}
}
[Fact]
public void DeclaresNothingRetailDoesNot()
{
var expected = Golden.Select(g => g.Name).OrderBy(n => n, StringComparer.Ordinal);
var actual = Enum.GetNames().OrderBy(n => n, StringComparer.Ordinal);
Assert.Equal(expected, actual);
}
/// Retail numbers the sounds densely from 0 to 0xCC, and
/// NUM_SOUND_TYPES is 0xCD. A gap here means a member was dropped.
[Fact]
public void CoversTheDenseRetailRangeWithNoGaps()
{
Assert.Equal(205, Enum.GetNames().Length);
var values = Enum.GetValues().Cast().OrderBy(v => v).ToArray();
Assert.Equal(Enumerable.Range(0, 205).Select(i => (uint)i), values);
Assert.Equal(0xCCu, values[^1]);
}
/// The dat-side hook path resolves sounds through DatReaderWriter's
/// own Sound enum. The two must not drift apart, or a SoundTable lookup
/// named through would fetch a different slot.
[Fact]
public void AgreesWithTheDatReaderEnumUsedAtRuntime()
{
foreach (var (name, value) in Golden)
{
Assert.True(Enum.IsDefined(typeof(DatReaderWriter.Enums.Sound), name),
$"DatReaderWriter.Enums.Sound.{name} is missing");
Assert.Equal(value, (uint)Enum.Parse(name));
}
}
}