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