using System.Buffers.Binary;
namespace AcDream.Core.Net.Messages;
///
/// Inbound CharacterError GameMessage (opcode 0xF659) — the
/// server's catch-all failure notice during the pre-world character-select
/// stage (logon conflicts, delete/restore failures, enter-world rejections,
/// subscription state). Today acdream cannot surface ANY character-stage
/// server error to the user; this is the first parser for the family.
///
///
/// Wire layout confirmed directly from retail's inbound dispatcher,
/// UIQueueManager::ProcessNetBlobData at 0x0055b000, which
/// reads a u32 immediately after the opcode and passes it to
/// CPlayerSystem::Handle_CharacterError at 0x0055d5d0 typed
/// as enum charError (enum charError eax_86 = *(uint32_t*)((char*)ecx + 4);):
///
///
///
/// u32 opcode (0xF659)
/// u32 errorCode (enum charError)
///
///
///
/// ACE agrees: GameMessageCharacterError
/// (ACE.Server/Network/GameMessages/Messages/GameMessageCharacterError.cs)
/// writes exactly opcode + (uint)error, and every
/// session.SendCharacterError(...) call site in
/// CharacterHandler.cs (the two this slice's
/// / handlers can raise —
/// CharacterError.Delete, CharacterError.LogonServerFull,
/// CharacterError.EnterGameCouldntPlaceCharacter,
/// CharacterError.EnterGameCharacterNotOwned — plus every other
/// value the wider character-stage flow can raise) goes through this same
/// shape.
///
///
///
/// is a verbatim port of retail's enum charError
/// (docs/research/named-retail/acclient.h:4038-4067) — the header's
/// own numeric ground truth, not a subset filtered through ACE's C# port.
/// It is a strict superset of ACE's ACE.Server.Network.Enum.CharacterError
/// (references/ACE/Source/ACE.Server/Network/Enum/CharacterError.cs):
/// retail additionally names 0x2 (LoggedOn), 0x7 (NoPremade),
/// and 0x16 (CharacterIsBooted) — three values ACE omits entirely,
/// none of which ACE's server ever sends but all of which retail's client
/// can receive from a genuine retail server. At 0x8 the port additionally
/// CORRECTS an ACE misnaming: ACE defines 0x8 as ServerCrash2 with a
/// doc comment duplicating 0x4's ID_CHAR_ERROR_SERVER_CRASH text,
/// but retail's header names 0x8 CHAR_ERROR_ACCOUNT_IN_USE — the
/// header wins. Per the project's property-enum-divergence lesson, we port
/// the complete oracle, not just what today's one server implementation
/// emits. ACE's per-value doc
/// comments (themselves sourced from the client's ID_CHAR_ERROR_*
/// string table) are folded in below where they exist. One retail member,
/// FORCE_charError_32_BIT = 0x7FFFFFFF, is a compiler
/// storage-width pragma (MSVC's "force this enum to 32-bit backing store"
/// idiom) and not a real wire value — it is deliberately NOT ported.
///
///
///
/// Unknown values are never rejected:
/// always carries the wire value verbatim, and casting it to
/// (see ) can never throw in
/// C# even for a value retail itself never defined — future server
/// revisions or private servers may add codes we haven't named yet.
///
///
public static class CharacterError
{
public const uint Opcode = 0xF659u;
///
/// Verbatim port of retail's enum charError
/// (acclient.h:4038-4067), excluding the 32-bit storage-width
/// sentinel FORCE_charError_32_BIT.
///
public enum Code : uint
{
/// 0x00 — CHAR_ERROR_UNDEF.
Undefined = 0x00,
///
/// 0x01 — CHAR_ERROR_LOGON. ACE: "Cannot have two accounts logged
/// on at the same time."
///
Logon = 0x01,
/// 0x02 — CHAR_ERROR_LOGGED_ON. Retail-only; no ACE member.
LoggedOn = 0x02,
///
/// 0x03 — CHAR_ERROR_ACCOUNT_LOGON. ACE: "Server could not access
/// your account information. Please try again in a few minutes."
///
AccountLogon = 0x03,
///
/// 0x04 — CHAR_ERROR_SERVER_CRASH. ACE: "The server has
/// disconnected. Please try again in a few minutes."
///
ServerCrash = 0x04,
/// 0x05 — CHAR_ERROR_LOGOFF. ACE: "Server could not log off your character."
Logoff = 0x05,
///
/// 0x06 — CHAR_ERROR_DELETE. ACE: "Server could not delete your
/// character." Sent by 's
/// server-side handler on every rejection path.
///
Delete = 0x06,
/// 0x07 — CHAR_ERROR_NO_PREMADE. Retail-only; no ACE member.
NoPremade = 0x07,
///
/// 0x08 — CHAR_ERROR_ACCOUNT_IN_USE. ACE misnames this value
/// ServerCrash2 (its doc comment duplicates 0x04's text);
/// retail's header is the authority. See the class doc comment.
///
AccountInUse = 0x08,
///
/// 0x09 — CHAR_ERROR_ACCOUNT_INVALID. ACE: "The account name you
/// specified was not valid."
///
AccountInvalid = 0x09,
///
/// 0x0A — CHAR_ERROR_ACCOUNT_DOESNT_EXIST. ACE: "The account you
/// specified doesn't exist."
///
AccountDoesntExist = 0x0A,
///
/// 0x0B — CHAR_ERROR_ENTER_GAME_GENERIC. ACE: forces the player
/// back to character-select if in 3D mode; otherwise a no-op OK
/// popup.
///
EnterGameGeneric = 0x0B,
///
/// 0x0C — CHAR_ERROR_ENTER_GAME_STRESS_ACCOUNT. ACE: "You cannot
/// enter the game with a stress creating character."
///
EnterGameStressAccount = 0x0C,
///
/// 0x0D — CHAR_ERROR_ENTER_GAME_CHARACTER_IN_WORLD. ACE: "One of
/// your characters is still in the world. Please try again in a
/// few minutes."
///
EnterGameCharacterInWorld = 0x0D,
///
/// 0x0E — CHAR_ERROR_ENTER_GAME_PLAYER_ACCOUNT_MISSING. ACE:
/// "Server unable to find player account. Please try again
/// later."
///
EnterGamePlayerAccountMissing = 0x0E,
///
/// 0x0F — CHAR_ERROR_ENTER_GAME_CHARACTER_NOT_OWNED. ACE: "You do
/// not own this character." Sent by
/// 's
/// server-side handler when the delete grace window has expired.
///
EnterGameCharacterNotOwned = 0x0F,
///
/// 0x10 — CHAR_ERROR_ENTER_GAME_CHARACTER_IN_WORLD_SERVER. ACE:
/// "One of your characters is currently in the world. Please try
/// again later. This is likely an internal server error."
///
EnterGameCharacterInWorldServer = 0x10,
///
/// 0x11 — CHAR_ERROR_ENTER_GAME_OLD_CHARACTER. ACE: forces the
/// player back to character-select if in 3D mode; no-op
/// otherwise.
///
EnterGameOldCharacter = 0x11,
///
/// 0x12 — CHAR_ERROR_ENTER_GAME_CORRUPT_CHARACTER. ACE: "This
/// character's data has been corrupted. Please delete it and
/// create a new character."
///
EnterGameCorruptCharacter = 0x12,
///
/// 0x13 — CHAR_ERROR_ENTER_GAME_START_SERVER_DOWN. ACE: "This
/// character's starting server is experiencing difficulties.
/// Please try again in a few minutes."
///
EnterGameStartServerDown = 0x13,
///
/// 0x14 — CHAR_ERROR_ENTER_GAME_COULDNT_PLACE_CHARACTER. ACE:
/// "This character couldn't be placed in the world right now.
/// Please try again in a few minutes." Sent by
/// 's
/// server-side handler during a shutdown-in-progress race.
///
EnterGameCouldntPlaceCharacter = 0x14,
///
/// 0x15 — CHAR_ERROR_LOGON_SERVER_FULL. ACE: "Sorry, but the
/// Asheron's Call server is full currently. Please try again
/// later." Sent by both
/// and
/// 's
/// server-side handlers when the world is closed to non-advocates.
///
LogonServerFull = 0x15,
/// 0x16 — CHAR_ERROR_CHARACTER_IS_BOOTED. Retail-only; no ACE member.
CharacterIsBooted = 0x16,
///
/// 0x17 — CHAR_ERROR_ENTER_GAME_CHARACTER_LOCKED. ACE: "A save of
/// this character is still in progress. Please try again later."
///
EnterGameCharacterLocked = 0x17,
///
/// 0x18 — CHAR_ERROR_SUBSCRIPTION_EXPIRED. ACE: "Your
/// subscription to this game has expired."
///
SubscriptionExpired = 0x18,
///
/// 0x19 — CHAR_ERROR_NUM_ERRORS. Retail's own count-of-errors
/// sentinel (the array-bound idiom, one past the last real code) —
/// never sent on the wire as an actual error. Kept for verbatim
/// completeness of the enum range; do not treat a received 0x19
/// as meaningful, and LA7b's error-to-string mapping must not
/// render it as a user-facing message.
///
NumErrors = 0x19,
}
public readonly record struct Parsed(uint RawErrorCode)
{
///
/// Best-effort named view of . A plain
/// enum cast never throws in C#, so this is safe even for values
/// retail never defined — always trust
/// as the source of truth.
///
public Code AsCode => (Code)RawErrorCode;
}
///
/// Parse a CharacterError body. must start
/// with the 4-byte opcode (0xF659).
///
public static Parsed Parse(ReadOnlySpan body)
{
int pos = 0;
uint opcode = ReadU32(body, ref pos);
if (opcode != Opcode)
throw new FormatException($"expected CharacterError opcode 0x{Opcode:X4}, got 0x{opcode:X8}");
uint errorCode = ReadU32(body, ref pos);
return new Parsed(errorCode);
}
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;
}
}