using AcDream.Core.Net.Messages; using AcDream.Runtime.Gameplay; namespace AcDream.Runtime.Tests.Gameplay; /// /// Campaign OP slice OP1 (2026-08-10) conformance for /// — table completeness across the /// complete 0x00..0x34 id space, the auto-save split pinned /// id-by-id against /// docs/research/2026-08-10-set-character-options-wire.md §3.2's /// byte-verified table, the client-Defaults split against §8.2's, and /// unknown-id rejection (ACE throws on an unmodeled id — one must never /// reach the wire, wire doc §5.4.2). /// public sealed class CharacterOptionTableTests { // wire research §3.2 — CPlayerModule::IsAutoSaveOption @0x0059A600, // byte-verified 0x34-byte table at VA 0x0059A62C. 21 ids. private static readonly CharacterOptionId[] AutoSaveIds = [ CharacterOptionId.AutoRepeatAttack, CharacterOptionId.IgnoreAllegianceRequests, CharacterOptionId.IgnoreFellowshipRequests, CharacterOptionId.FellowshipShareXP, CharacterOptionId.AcceptLootPermits, CharacterOptionId.FellowshipShareLoot, CharacterOptionId.FellowshipAutoAcceptRequests, CharacterOptionId.UseChargeAttack, CharacterOptionId.ListenToAllegianceChat, CharacterOptionId.ListenToGeneralChat, CharacterOptionId.ListenToTradeChat, CharacterOptionId.ListenToLFGChat, CharacterOptionId.ListenToRoleplayChat, CharacterOptionId.AppearOffline, CharacterOptionId.LeadMissileTargets, CharacterOptionId.UseFastMissiles, CharacterOptionId.ListenToSocietyChat, CharacterOptionId.ShowHelm, CharacterOptionId.UseMouseTurning, CharacterOptionId.ShowCloak, CharacterOptionId.LockUI, ]; // wire research §8.2 — PlayerModule::GetDefaultOptionValue @0x005D2A30, // byte-verified 0x2B-byte table at VA 0x005D2A5C. 16 default-ON ids // (every id past 0x2A falls off the end of that table and defaults to // false, even though 3 of them are ON in the raw constructor word — // see the divergence register row, D3/OP1). private static readonly CharacterOptionId[] ClientDefaultOnIds = [ CharacterOptionId.AutoRepeatAttack, CharacterOptionId.IgnoreFellowshipRequests, CharacterOptionId.AllowGive, CharacterOptionId.ShowTooltips, CharacterOptionId.ToggleRun, CharacterOptionId.AutoTarget, CharacterOptionId.VividTargetingIndicator, CharacterOptionId.FellowshipShareXP, CharacterOptionId.CoordinatesOnRadar, CharacterOptionId.SpellDuration, CharacterOptionId.UseChargeAttack, CharacterOptionId.ListenToAllegianceChat, CharacterOptionId.ListenToGeneralChat, CharacterOptionId.ListenToTradeChat, CharacterOptionId.ListenToLFGChat, CharacterOptionId.LeadMissileTargets, ]; [Fact] public void All_HasExactly53Entries_Ids0x00Through0x34Contiguous() { CharacterOptionTableEntry[] all = [.. CharacterOptionTable.All]; Assert.Equal(53, all.Length); for (uint id = 0x00; id <= 0x34; id++) { Assert.True( CharacterOptionTable.TryGet(id, out CharacterOptionTableEntry entry), $"id 0x{id:X2} missing from CharacterOptionTable"); Assert.Equal(id, (uint)entry.Id); } } [Theory] [MemberData(nameof(AllModeledIds))] public void IsAutoSave_MatchesByteVerifiedSplit(CharacterOptionId id) { bool expected = Array.IndexOf(AutoSaveIds, id) >= 0; Assert.True(CharacterOptionTable.TryGet(id, out CharacterOptionTableEntry entry)); Assert.Equal(expected, entry.IsAutoSave); } [Theory] [MemberData(nameof(AllModeledIds))] public void ClientDefault_MatchesByteVerifiedSplit(CharacterOptionId id) { bool expected = Array.IndexOf(ClientDefaultOnIds, id) >= 0; Assert.True(CharacterOptionTable.TryGet(id, out CharacterOptionTableEntry entry)); Assert.Equal(expected, entry.ClientDefault); } [Fact] public void AutoSaveIds_CountIs21() { Assert.Equal(21, AutoSaveIds.Length); Assert.Equal(21, CharacterOptionTable.All.Count(static e => e.IsAutoSave)); } [Fact] public void ClientDefaultOnIds_CountIs16() { Assert.Equal(16, ClientDefaultOnIds.Length); Assert.Equal(16, CharacterOptionTable.All.Count(static e => e.ClientDefault)); } [Fact] public void ReconstructedClientDefaultWords_MatchIndependentlyConfirmedConstants() { // wire research §1.4/§2.5: OR'ing every ClientDefault=true row's mask // into its own word reconstructs EXACTLY CharacterOptions1.Default // (0x50C4A54A, also the retail constructor literal) for Options1, // and 0x00008700 for Options2 — the client Defaults-button value, // deliberately NOT the same as the raw constructor default // (0x00948700, RuntimeCharacterOptionsState.DefaultOptions2) because // GetDefaultOptionValue's table stops at id 0x2A. uint options1 = 0u; uint options2 = 0u; foreach (CharacterOptionTableEntry entry in CharacterOptionTable.All) { if (!entry.ClientDefault) continue; if (entry.IsOptions1) options1 |= entry.Mask; else options2 |= entry.Mask; } Assert.Equal(0x50C4A54Au, options1); Assert.Equal(0x00008700u, options2); } [Theory] [InlineData(0x35u)] // CharacterOptions1Default — the WHOLE default mask, not a real option [InlineData(0x36u)] // CharacterOptions2Default — same landmine, other word [InlineData(0xFFFFu)] [InlineData(0xFFFFFFFFu)] public void TryGet_RejectsUnknownAndReservedIds(uint optionId) { Assert.False(CharacterOptionTable.TryGet(optionId, out _)); } [Fact] public void SpotCheck_WordAndMaskAgainstVerbatimAcclientEnums() { // acclient.h:3404-3436 `enum CharacterOption` / :3451-3481 // `enum CharacterOptions2`. Assert.True(CharacterOptionTable.TryGet( CharacterOptionId.AutoRepeatAttack, out CharacterOptionTableEntry autoRepeat)); Assert.True(autoRepeat.IsOptions1); Assert.Equal(0x00000002u, autoRepeat.Mask); // PersistentAtDay (id 0x05) lives in Options2 despite its low id — // acclient.h:3454 `PersistentAtDay_CharacterOptions2 = 0x1`. Assert.True(CharacterOptionTable.TryGet( CharacterOptionId.PersistentAtDay, out CharacterOptionTableEntry persistentAtDay)); Assert.False(persistentAtDay.IsOptions1); Assert.Equal(0x00000001u, persistentAtDay.Mask); Assert.True(CharacterOptionTable.TryGet( CharacterOptionId.ListenToAllegianceChat, out CharacterOptionTableEntry allegiance)); Assert.True(allegiance.IsOptions1); Assert.Equal(0x40000000u, allegiance.Mask); // HearPkDeathMessages (0x34) — ACE-sourced, unverifiable against the // 2013 binary; register row. Assert.True(CharacterOptionTable.TryGet( CharacterOptionId.HearPkDeathMessages, out CharacterOptionTableEntry pkDeath)); Assert.False(pkDeath.IsOptions1); Assert.Equal(0x02000000u, pkDeath.Mask); Assert.False(pkDeath.IsAutoSave); } public static IEnumerable AllModeledIds() { for (uint id = 0x00; id <= 0x34; id++) yield return [(CharacterOptionId)id]; } }