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);
}
}