using System.Buffers.Binary;
namespace AcDream.Core.Net.Messages;
///
/// Shared parser for opcode 0xF643 — retail's
/// CharacterGenerationVerificationResponse shape, which BOTH
/// (opcode 0xF7D9 request) and
/// (opcode 0xF656 request) receive on
/// the exact same wire opcode — a genuine retail opcode reuse, confirmed by
/// ACE's own GameMessageOpcode.cs declaring both
/// CharacterCreateResponse = 0xF643 and
/// CharacterRestoreResponse = 0xF643, // This is a duplicate....
///
///
/// Campaign CC CC2: this type is the promotion of the parse logic
/// that used to live only in (Campaign
/// LA slice LA7a). Character creation now exists (),
/// so the two message families that collide on this opcode are both real and
/// both need it — keeps its own
/// shape for source compatibility and
/// delegates to this type internally; new code (the create response,
/// WorldSession.CharacterCreateResponseReceived) consumes
/// directly. A caller cannot tell "restore response"
/// from "create response" by opcode or shape alone — WorldSession
/// disambiguates by tracking which outbound request (restore vs. create) it
/// is awaiting a reply to (see WorldSession's awaiting-request latch).
/// That latch is not merely a reasonable design — it is retail's OWN
/// mechanism: Handle_CharGenVerificationResponse@0x0055E8B0 case 1
/// branches on the client's persistent chargen state,
/// GetVerificationState() == PENDING → new CharacterIdentity
/// + AddIdentity (a create it initiated), else → unpack into the
/// existing identity at slot (a restore). Same discriminator, one
/// layer down (CC2 review's fidelity note).
///
///
///
/// Wire layout, verbatim from ACE's GameMessageCharacterCreateResponse.cs
/// / GameMessageCharacterRestore.cs (both write the identical shape)
/// and cross-checked against holtburger's
/// CharacterCreateResponseData::unpack
/// (holtburger-protocol/src/messages/character/types.rs:379-410):
///
///
///
/// u32 opcode (0xF643)
/// u32 code (CharacterGenerationVerificationResponse)
/// -- only when code == Ok --
/// u32 guid
/// String16L name
/// u32 secondsGreyedOut
///
///
///
/// is a verbatim port of ACE's
/// CharacterGenerationVerificationResponse enum
/// (ACE.Server/Network/Enum/CharacterGenerationVerificationResponse.cs),
/// which is itself retail's own dialog dispatch table
/// (Handle_CharGenVerificationResponse@0x0055E8B0): NameInUse →
/// ID_Character_Err_NameReserved, NameBanned →
/// ID_Character_Err_NameBanned, Corrupt/DatabaseDown →
/// ID_Character_Err_NameDBDown, AdminPrivilegeDenied →
/// ID_Character_Err_NameAdminDenied. Pending/Undef
/// retail treats as a silent state reset with no dialog — notably ACE sends
/// Pending for a disabled-Olthoi rejection
/// (CharacterHandler.CharacterCreateEx,
/// olthoi_play_disabled branch), so that specific rejection is
/// invisible to the retail-faithful client too; this is a retail quirk to
/// port as-is, not a bug to fix. Dialog presentation itself is CC5's job
/// (App layer), not this Core.Net type's.
///
///
public static class CharGenVerificationResponse
{
public const uint ResponseOpcode = 0xF643u;
///
/// Verbatim port of ACE's CharacterGenerationVerificationResponse
/// enum, which is retail's own Handle_CharGenVerificationResponse
/// dispatch table.
///
public enum Code : uint
{
Undef = 0,
Ok = 1,
Pending = 2,
NameInUse = 3,
NameBanned = 4,
Corrupt = 5,
DatabaseDown = 6,
AdminPrivilegeDenied = 7,
}
///
/// Parsed 0xF643 body. , , and
/// are only populated when
/// equals — retail omits
/// them entirely on the wire otherwise (both
/// GameMessageCharacterCreateResponse and
/// GameMessageCharacterRestore gate the trailing fields on
/// response == ... .Ok).
///
public readonly record struct Parsed(
uint RawCode,
uint? Guid,
string? Name,
uint? SecondsGreyedOut)
{
///
/// Best-effort named view of . A plain enum
/// cast never throws in C#, so this is safe even for a value retail
/// never defined — always trust as the source
/// of truth.
///
public Code AsCode => (Code)RawCode;
/// True when the trailing identity fields are present.
public bool IsOk => RawCode == (uint)Code.Ok;
}
///
/// Parse a 0xF643 body. must start with
/// the 4-byte opcode.
///
public static Parsed Parse(ReadOnlySpan body)
{
int pos = 0;
uint opcode = ReadU32(body, ref pos);
if (opcode != ResponseOpcode)
throw new FormatException(
$"expected CharacterGenerationVerificationResponse opcode 0x{ResponseOpcode:X4}, got 0x{opcode:X8}");
uint rawCode = ReadU32(body, ref pos);
if (rawCode != (uint)Code.Ok)
return new Parsed(rawCode, null, null, null);
uint guid = ReadU32(body, ref pos);
string name = StringReader.ReadString16L(body, ref pos);
uint secondsGreyedOut = ReadU32(body, ref pos);
return new Parsed(rawCode, guid, name, secondsGreyedOut);
}
private static uint ReadU32(ReadOnlySpan source, ref int pos)
{
if (source.Length - pos < 4) throw new FormatException("truncated u32");
uint value = BinaryPrimitives.ReadUInt32LittleEndian(source.Slice(pos));
pos += 4;
return value;
}
}