using System.Buffers.Binary; using AcDream.Core.Net.Messages; namespace AcDream.Core.Net.Tests.Messages; public sealed class CharacterRestoreTests { [Fact] public void BuildRequestBody_ExactByteSequence_OpcodeThenGuidOnly() { byte[] body = CharacterRestore.BuildRequestBody(0x50000001u); byte[] expected = [ 0xD9, 0xF7, 0x00, 0x00, // opcode 0xF7D9 LE 0x01, 0x00, 0x00, 0x50, // guid 0x50000001 LE ]; Assert.Equal(expected, body); Assert.Equal(8, body.Length); } [Fact] public void Parse_SuccessResponse_PopulatesAllTrailingFields() { // Mirrors ACE's GameMessageCharacterRestore: opcode, flag=1 (Ok), // guid, String16L name, secondsGreyedOut. var w = AceWireWriter.GameMessage(CharacterRestore.ResponseOpcode) .Write(1u) .WriteGuid(0x50000002u) .WriteString16L("+Acdream") .Write(0u); CharacterRestore.Parsed parsed = CharacterRestore.Parse(w.ToArray()); Assert.Equal(1u, parsed.VerificationFlag); Assert.True(parsed.IsOk); Assert.Equal(0x50000002u, parsed.Guid); Assert.Equal("+Acdream", parsed.Name); Assert.Equal(0u, parsed.SecondsGreyedOut); } [Fact] public void Parse_SuccessResponse_NonzeroSecondsGreyedOutPreserved() { var w = AceWireWriter.GameMessage(CharacterRestore.ResponseOpcode) .Write(1u) .WriteGuid(0x50000003u) .WriteString16L("Restored") .Write(45u); CharacterRestore.Parsed parsed = CharacterRestore.Parse(w.ToArray()); Assert.Equal(45u, parsed.SecondsGreyedOut); } [Fact] public void Parse_FailureShapedResponse_LeavesTrailingFieldsNull() { // Retail's colliding CharacterCreateResponse shape: a non-Ok flag // (here 3 = NameInUse) has NO trailing guid/name/seconds on the // wire at all — GameMessageCharacterCreateResponse.cs only writes // them "if (response == ... .Ok)". Parse must not try to read past // the flag in this case. var w = AceWireWriter.GameMessage(CharacterRestore.ResponseOpcode) .Write(3u); // CharacterGenerationVerificationResponse.NameInUse CharacterRestore.Parsed parsed = CharacterRestore.Parse(w.ToArray()); Assert.Equal(3u, parsed.VerificationFlag); Assert.False(parsed.IsOk); Assert.Null(parsed.Guid); Assert.Null(parsed.Name); Assert.Null(parsed.SecondsGreyedOut); } [Fact] public void Parse_WrongOpcode_Throws() { byte[] bytes = new byte[4]; BinaryPrimitives.WriteUInt32LittleEndian(bytes, 0xDEADBEEFu); Assert.Throws(() => CharacterRestore.Parse(bytes)); } [Fact] public void Parse_TruncatedAfterFlag_Throws() { // Claims success (flag=1) but the body ends before the guid. var w = AceWireWriter.GameMessage(CharacterRestore.ResponseOpcode).Write(1u); Assert.Throws(() => CharacterRestore.Parse(w.ToArray())); } [Fact] public void Parse_TruncatedBeforeFlag_Throws() { var w = AceWireWriter.GameMessage(CharacterRestore.ResponseOpcode); Assert.Throws(() => CharacterRestore.Parse(w.ToArray())); } [Fact] public void RequestThenResponse_RoundTrips_GuidIdentity() { const uint guid = 0x50000009u; byte[] request = CharacterRestore.BuildRequestBody(guid); // The request itself carries only the guid; re-derive it the same // way a caller would to confirm nothing was lost in the builder. uint requestedGuid = BinaryPrimitives.ReadUInt32LittleEndian(request.AsSpan(4)); Assert.Equal(guid, requestedGuid); var w = AceWireWriter.GameMessage(CharacterRestore.ResponseOpcode) .Write(1u) .WriteGuid(guid) .WriteString16L("RoundTrip") .Write(0u); CharacterRestore.Parsed response = CharacterRestore.Parse(w.ToArray()); Assert.Equal(requestedGuid, response.Guid); } }