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