F1 (MEDIUM): the correlation-latch docs claimed replies are never misattributed; in truth an overlapping send OVERWRITES the latch and the first reply routes to the newest request's event. Narrowed all three doc sites to the exact contract (single outstanding request; overlap refusal is CC3's Runtime verification gate, retail's DoFinish UNDEF-state rule) and pinned the overwrite behavior with OverlappingSend_OverwritesTheLatch_ReplyRoutesToNewestRequest. F2 (LOW): filed register AD-100 for the drop-unless-armed deviation — retail's Handle_CharGenVerificationResponse@0x0055E8B0 has no armed gate and processes whatever arrives against its persistent verification state. F3 (LOW): doc note in CharacterCreate.cs — ACE double-sends NameInUse (IsCharacterNameAvailable runs twice; the first callback's return exits only the lambda), so the second reply hitting the drop path during a connected gate is EXPECTED, not a defect. F4 (LOW): creationFailed's enum-member key renamed name -> reason and the ATTEMPTED character name added as name, before any consumer shipped — one status vocabulary must not give the same key two meanings (characterCreated.name is a character name). Contract, writer, tailer, and shape-pinning tests updated in lockstep. F5 (LOW): the thread-id probe-note pointer now cites ProbeNetLogOutbound's doc comment, where the note actually lives. Fidelity fold (reviewer's positive note): the latch is retail's OWN discriminator one layer down — 0x0055E8B0 case 1 branches on GetVerificationState()==PENDING (create) vs not (restore) — now cited in both the latch doc and CharGenVerificationResponse.cs. Core.Net 994, Runtime 1667, Launcher.Core 324, all green Release. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
152 lines
6.3 KiB
C#
152 lines
6.3 KiB
C#
using System.Buffers.Binary;
|
|
|
|
namespace AcDream.Core.Net.Messages;
|
|
|
|
/// <summary>
|
|
/// Shared parser for opcode <c>0xF643</c> — retail's
|
|
/// <c>CharacterGenerationVerificationResponse</c> shape, which BOTH
|
|
/// <see cref="CharacterRestore"/> (opcode <c>0xF7D9</c> request) and
|
|
/// <see cref="CharacterCreate"/> (opcode <c>0xF656</c> request) receive on
|
|
/// the exact same wire opcode — a genuine retail opcode reuse, confirmed by
|
|
/// ACE's own <c>GameMessageOpcode.cs</c> declaring both
|
|
/// <c>CharacterCreateResponse = 0xF643</c> and
|
|
/// <c>CharacterRestoreResponse = 0xF643, // This is a duplicate...</c>.
|
|
///
|
|
/// <para>
|
|
/// <b>Campaign CC CC2:</b> this type is the promotion of the parse logic
|
|
/// that used to live only in <see cref="CharacterRestore.Parse"/> (Campaign
|
|
/// LA slice LA7a). Character creation now exists (<see cref="CharacterCreate"/>),
|
|
/// so the two message families that collide on this opcode are both real and
|
|
/// both need it — <see cref="CharacterRestore"/> keeps its own
|
|
/// <see cref="CharacterRestore.Parsed"/> shape for source compatibility and
|
|
/// delegates to this type internally; new code (the create response,
|
|
/// <c>WorldSession.CharacterCreateResponseReceived</c>) consumes
|
|
/// <see cref="Parsed"/> directly. A caller cannot tell "restore response"
|
|
/// from "create response" by opcode or shape alone — <c>WorldSession</c>
|
|
/// disambiguates by tracking which outbound request (restore vs. create) it
|
|
/// is awaiting a reply to (see <c>WorldSession</c>'s awaiting-request latch).
|
|
/// That latch is not merely a reasonable design — it is retail's OWN
|
|
/// mechanism: <c>Handle_CharGenVerificationResponse@0x0055E8B0</c> case 1
|
|
/// branches on the client's persistent chargen state,
|
|
/// <c>GetVerificationState() == PENDING</c> → new <c>CharacterIdentity</c>
|
|
/// + <c>AddIdentity</c> (a create it initiated), else → unpack into the
|
|
/// existing identity at <c>slot</c> (a restore). Same discriminator, one
|
|
/// layer down (CC2 review's fidelity note).
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// Wire layout, verbatim from ACE's <c>GameMessageCharacterCreateResponse.cs</c>
|
|
/// / <c>GameMessageCharacterRestore.cs</c> (both write the identical shape)
|
|
/// and cross-checked against holtburger's
|
|
/// <c>CharacterCreateResponseData::unpack</c>
|
|
/// (<c>holtburger-protocol/src/messages/character/types.rs:379-410</c>):
|
|
/// </para>
|
|
///
|
|
/// <code>
|
|
/// u32 opcode (0xF643)
|
|
/// u32 code (CharacterGenerationVerificationResponse)
|
|
/// -- only when code == Ok --
|
|
/// u32 guid
|
|
/// String16L name
|
|
/// u32 secondsGreyedOut
|
|
/// </code>
|
|
///
|
|
/// <para>
|
|
/// <see cref="Code"/> is a verbatim port of ACE's
|
|
/// <c>CharacterGenerationVerificationResponse</c> enum
|
|
/// (<c>ACE.Server/Network/Enum/CharacterGenerationVerificationResponse.cs</c>),
|
|
/// which is itself retail's own dialog dispatch table
|
|
/// (<c>Handle_CharGenVerificationResponse@0x0055E8B0</c>): <c>NameInUse</c> →
|
|
/// <c>ID_Character_Err_NameReserved</c>, <c>NameBanned</c> →
|
|
/// <c>ID_Character_Err_NameBanned</c>, <c>Corrupt</c>/<c>DatabaseDown</c> →
|
|
/// <c>ID_Character_Err_NameDBDown</c>, <c>AdminPrivilegeDenied</c> →
|
|
/// <c>ID_Character_Err_NameAdminDenied</c>. <c>Pending</c>/<c>Undef</c>
|
|
/// retail treats as a silent state reset with no dialog — notably ACE sends
|
|
/// <c>Pending</c> for a disabled-Olthoi rejection
|
|
/// (<c>CharacterHandler.CharacterCreateEx</c>,
|
|
/// <c>olthoi_play_disabled</c> 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.
|
|
/// </para>
|
|
/// </summary>
|
|
public static class CharGenVerificationResponse
|
|
{
|
|
public const uint ResponseOpcode = 0xF643u;
|
|
|
|
/// <summary>
|
|
/// Verbatim port of ACE's <c>CharacterGenerationVerificationResponse</c>
|
|
/// enum, which is retail's own <c>Handle_CharGenVerificationResponse</c>
|
|
/// dispatch table.
|
|
/// </summary>
|
|
public enum Code : uint
|
|
{
|
|
Undef = 0,
|
|
Ok = 1,
|
|
Pending = 2,
|
|
NameInUse = 3,
|
|
NameBanned = 4,
|
|
Corrupt = 5,
|
|
DatabaseDown = 6,
|
|
AdminPrivilegeDenied = 7,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parsed <c>0xF643</c> body. <see cref="Guid"/>, <see cref="Name"/>, and
|
|
/// <see cref="SecondsGreyedOut"/> are only populated when
|
|
/// <see cref="RawCode"/> equals <see cref="Code.Ok"/> — retail omits
|
|
/// them entirely on the wire otherwise (both
|
|
/// <c>GameMessageCharacterCreateResponse</c> and
|
|
/// <c>GameMessageCharacterRestore</c> gate the trailing fields on
|
|
/// <c>response == ... .Ok</c>).
|
|
/// </summary>
|
|
public readonly record struct Parsed(
|
|
uint RawCode,
|
|
uint? Guid,
|
|
string? Name,
|
|
uint? SecondsGreyedOut)
|
|
{
|
|
/// <summary>
|
|
/// Best-effort named view of <see cref="RawCode"/>. A plain enum
|
|
/// cast never throws in C#, so this is safe even for a value retail
|
|
/// never defined — always trust <see cref="RawCode"/> as the source
|
|
/// of truth.
|
|
/// </summary>
|
|
public Code AsCode => (Code)RawCode;
|
|
|
|
/// <summary>True when the trailing identity fields are present.</summary>
|
|
public bool IsOk => RawCode == (uint)Code.Ok;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a <c>0xF643</c> body. <paramref name="body"/> must start with
|
|
/// the 4-byte opcode.
|
|
/// </summary>
|
|
public static Parsed Parse(ReadOnlySpan<byte> 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<byte> 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;
|
|
}
|
|
}
|