using AcDream.Core.Net.Packets; namespace AcDream.Core.Net.Messages; /// /// Retail character-restore request (opcode 0xF7D9) and its response /// (opcode 0xF643). /// /// /// Request — guid-only, an ADAPTATION (register row AD-97). Retail /// really does send more than the guid. The PDB-paired binary at /// CPlayerSystem::RestoreCharacter@0x0055d760 is 26 bytes: /// push 0x008173B4; push 0x008173B4; push guid; /// call Proto_UI::SendAdminRestoreCharacter@0x00546cf0 — two REAL /// constant PStringBase<char>* arguments (Binary Ninja renders /// them as an uninitialized edx local and this; that /// rendering is the artifact, the two push imm32 are not). /// SendAdminRestoreCharacter packs both /// (PStringBase::Pack@0x004fc6f0 emits ≥4 bytes even for an empty /// string), so retail's request is ≥16 bytes where ours is 8. We send /// guid-only because ACE /// (CharacterHandler.CharacterRestore, /// ACE.Server/Network/Handlers/CharacterHandler.cs:331-385) reads /// only ReadUInt32() and ignores any tail, and holtburger /// (holtburger-protocol/src/messages/character/types.rs::CharacterRestoreRequestData, /// sent from a real client command path) ships guid-only against ACE /// successfully. The omitted tail is a recorded retail deviation — /// divergence register AD-97. /// /// /// /// LA7b hazards. (1) ACE's restore handler has a SILENT no-reply /// path: an unknown guid hits /// Characters.SingleOrDefault(...) == null → return; — no 0xF643, /// no 0xF659. Selection state must never await a restore reply /// unconditionally. (2) Routing: ACE sends the response on /// GameMessageGroup.UIQueue; retail transmits the request via /// Proto_UI::SendToControl (the delete request goes via /// SendToLogon) — relevant when LA7b picks the outbound queue. /// /// /// /// u32 opcode (0xF7D9) /// u32 characterGuid /// /// /// /// Response — opcode collision with CharacterCreateResponse. ACE's /// own GameMessageOpcode.cs declares both /// CharacterCreateResponse = 0xF643 and /// CharacterRestoreResponse = 0xF643, // This is a duplicate... — a /// genuine retail opcode reuse, not an ACE bug. GameMessageCharacterRestore /// (ACE.Server/Network/GameMessages/Messages/GameMessageCharacterRestore.cs) /// unconditionally writes a success shape: /// /// /// /// u32 opcode (0xF643) /// u32 verificationFlag (1 = Ok, matching CharacterGenerationVerificationResponse.Ok) /// u32 characterGuid /// String16L characterName /// u32 secondsGreyedOut /// /// /// /// But ACE's CharacterRestore handler can ALSO reply on this same /// opcode via the character-CREATE response path when restore itself fails /// — TWO real branches: NameInUse (the freed name collided) and /// Corrupt (SaveCharacter returned false). Both shapes are /// flag-only, with NO trailing fields /// (GameMessageCharacterCreateResponse.cs: the guid / name / /// trailing u32 are only written if (response == ... .Ok)). /// mirrors that conditionality: the trailing three /// fields are read only when verificationFlag == 1. Because the two /// message families are wire-identical when they collide, a caller cannot /// tell "restore response" from "create response" by opcode or shape /// alone — it must track which outbound request /// ( vs. /// ) /// it is awaiting a reply to. /// /// /// /// Campaign CC CC2 update: character creation now exists /// (), so the /// disambiguation this doc comment used to defer is real work now, done by /// WorldSession's awaiting-request latch (set by /// WorldSession.SendRestoreCharacter / /// WorldSession.SendCharacterCreation, cleared on the matching /// response), which routes each 0xF643 to /// WorldSession.CharacterRestoreReceived or /// WorldSession.CharacterCreateResponseReceived accordingly and drops /// (rather than misattributes) a 0xF643 with no outstanding request. The /// wire parse itself is now shared: delegates to /// , which both families /// consume. This type's own shape and /// signature are UNCHANGED by that refactor — every existing caller and test /// keeps working exactly as before. /// /// public static class CharacterRestore { public const uint RequestOpcode = 0xF7D9u; public const uint ResponseOpcode = 0xF643u; /// /// Restore response body. , , and /// are only populated when /// equals 1 (Ok) — retail omits them /// entirely on the wire otherwise (see the collision note above). /// public readonly record struct Parsed( uint VerificationFlag, uint? Guid, string? Name, uint? SecondsGreyedOut) { /// True when the trailing character fields are present. public bool IsOk => VerificationFlag == 1u; } /// /// Build the body bytes for an outbound CharacterRestore request. /// Layout: opcode(4) + characterGuid(4). Guid-only — an adaptation of /// retail's ≥16-byte shape; see the class doc comment and divergence /// register AD-97. /// public static byte[] BuildRequestBody(uint characterGuid) { var w = new PacketWriter(8); w.WriteUInt32(RequestOpcode); w.WriteUInt32(characterGuid); return w.ToArray(); } /// /// Parse a CharacterRestore response body (opcode 0xF643). /// must start with the 4-byte opcode. Delegates /// to the shared (Campaign /// CC CC2); this type's shape and this method's /// exception behavior are unchanged from before that refactor. /// public static Parsed Parse(ReadOnlySpan body) { CharGenVerificationResponse.Parsed shared = CharGenVerificationResponse.Parse(body); return new Parsed(shared.RawCode, shared.Guid, shared.Name, shared.SecondsGreyedOut); } }