using System; using AcDream.Core.Net.Messages; using Xunit; namespace AcDream.Core.Net.Tests.Messages; /// /// Campaign CT slice CT2 (2026-08-24): golden-vector round-trip tests for /// the two character-title S→C parsers added to . /// Fixtures are built with (the ACE-mirror /// writer) so a pass proves agreement with ACE's own /// GameEventCharacterTitle.cs / GameEventUpdateTitle.cs /// writer shapes, not just with itself. /// public sealed class CharacterTitleEventsTests { // ── 0x0029 CharacterTitle ──────────────────────────────────────────── [Fact] public void ParseCharacterTitleTable_RoundTrips_DiscardsLeadingVersionTag() { byte[] wire = new AceWireWriter() .Write(1u) // ACE's literal pack-version tag — must be discarded .Write(13u) // displayTitleId .Write(3u) // count .Write(1u) .Write(5u) .Write(13u) .ToArray(); GameEvents.CharacterTitleTable? table = GameEvents.ParseCharacterTitleTable(wire); Assert.NotNull(table); Assert.Equal(13u, table!.Value.DisplayTitleId); Assert.Equal(new uint[] { 1u, 5u, 13u }, table.Value.TitleIds); } [Fact] public void ParseCharacterTitleTable_IgnoresNonOneLeadingTag() { // Retail's own UnPack never reads the leading dword into any field — // it is advanced past unconditionally. A hostile/odd server value // there must not change the parse outcome. byte[] wire = new AceWireWriter() .Write(0xDEADBEEFu) .Write(7u) .Write(0u) // empty title list .ToArray(); GameEvents.CharacterTitleTable? table = GameEvents.ParseCharacterTitleTable(wire); Assert.NotNull(table); Assert.Equal(7u, table!.Value.DisplayTitleId); Assert.Empty(table.Value.TitleIds); } [Fact] public void ParseCharacterTitleTable_EmptyTitleList_RoundTrips() { byte[] wire = new AceWireWriter() .Write(1u) .Write(0u) // displayTitleId — no display title yet .Write(0u) // count .ToArray(); GameEvents.CharacterTitleTable? table = GameEvents.ParseCharacterTitleTable(wire); Assert.NotNull(table); Assert.Equal(0u, table!.Value.DisplayTitleId); Assert.Empty(table.Value.TitleIds); } [Theory] [InlineData(0)] // empty payload [InlineData(4)] // only the leading tag [InlineData(8)] // leading tag + displayTitleId, missing count public void ParseCharacterTitleTable_TruncatedHeader_ReturnsNull(int length) { byte[] wire = new byte[length]; Assert.Null(GameEvents.ParseCharacterTitleTable(wire)); } [Fact] public void ParseCharacterTitleTable_CountExceedsAvailableTitleIds_ReturnsNull() { byte[] wire = new AceWireWriter() .Write(1u) .Write(1u) .Write(2u) // count says 2 title ids follow .Write(1u) // only 1 is actually present .ToArray(); Assert.Null(GameEvents.ParseCharacterTitleTable(wire)); } // ── 0x002B UpdateTitle ─────────────────────────────────────────────── [Theory] [InlineData(0u, false)] [InlineData(13u, true)] public void ParseUpdateTitle_RoundTrips(uint titleId, bool setAsDisplay) { byte[] wire = new AceWireWriter() .Write(titleId) .Write(setAsDisplay ? 1u : 0u) .ToArray(); GameEvents.UpdateTitle? update = GameEvents.ParseUpdateTitle(wire); Assert.NotNull(update); Assert.Equal(titleId, update!.Value.TitleId); Assert.Equal(setAsDisplay, update.Value.SetAsDisplay); } [Fact] public void ParseUpdateTitle_NonZeroSetAsDisplay_IsTrue() { // ACE writes Convert.ToUInt32(bool) (always 0 or 1), but retail's own // dispatch reads `!= 0`, not `== 1` — a non-1 truthy value must still // resolve true. byte[] wire = new AceWireWriter().Write(5u).Write(0xFFu).ToArray(); GameEvents.UpdateTitle? update = GameEvents.ParseUpdateTitle(wire); Assert.NotNull(update); Assert.True(update!.Value.SetAsDisplay); } [Theory] [InlineData(0)] [InlineData(4)] // only titleId, missing setAsDisplay public void ParseUpdateTitle_Truncated_ReturnsNull(int length) { byte[] wire = new byte[length]; Assert.Null(GameEvents.ParseUpdateTitle(wire)); } }