The Opus retail-lens review decoded the PDB-paired binary at CPlayerSystem::RestoreCharacter@0x0055d760 and refuted the uninitialized-edx justification: the two extra arguments are real push imm32 of a constant PStringBase (BN mis-renders them, but they pack to >=4 bytes each), so retail 0xF7D9 is >=16 bytes where ours is 8. The guid-only CODE stands (ACE reads only the guid; holtburger consensus) but it is an adaptation, not a corrected decompile — filed as divergence register AD-97 and the doc comment now states the true mechanism. Also from the review: the 0xF643 conditional-parse doc now names BOTH ACE flag-only failure branches (NameInUse + Corrupt); CharacterError 0x08 doc corrected (ACE misnames it ServerCrash2 — the port corrects an ACE misnaming; ACE omits three values, not four); LA7b hazard notes added (ACE silent no-reply on unknown restore guid; retail SendToLogon vs SendToControl routing; NumErrors never rendered); two review-nit tests (flag=0 Undef flag-only, non-Ok body with trailing bytes ignored). Core.Net suite: 953 passed / 0 failed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
89 lines
3.6 KiB
C#
89 lines
3.6 KiB
C#
using System.Buffers.Binary;
|
|
using AcDream.Core.Net.Packets;
|
|
|
|
namespace AcDream.Core.Net.Messages;
|
|
|
|
/// <summary>
|
|
/// Retail character-delete request and server acknowledgement, both riding
|
|
/// opcode <c>0xF655</c>.
|
|
///
|
|
/// <para>
|
|
/// Wire layout ported from retail <c>Proto_UI::SendDeleteCharacter</c> at
|
|
/// <c>0x00546b30</c>: the opcode, then <c>AC1Legacy::PStringBase<char>::Pack</c>
|
|
/// of the account id as a String16L, then a trailing u32 written directly
|
|
/// after the packed string (<c>*(uint32_t*)var_4 = arg2</c>):
|
|
/// </para>
|
|
///
|
|
/// <code>
|
|
/// u32 opcode (0xF655)
|
|
/// String16L accountName
|
|
/// u32 characterSlot (NOT the character guid)
|
|
/// </code>
|
|
///
|
|
/// <para>
|
|
/// The caller, <c>CPlayerSystem::DeleteCharacter</c> at <c>0x0055f830</c>,
|
|
/// resolves that trailing u32 from the target character's guid via
|
|
/// <c>CharacterSet::GetSlot(persistentData + 4, guid)</c> before sending —
|
|
/// retail deletes by **account + SLOT INDEX**, never the character guid.
|
|
/// This builder takes the already-resolved slot; resolving a selected
|
|
/// character to its slot is Runtime selection-state work (Campaign LA
|
|
/// slice LA7b), not this file's job.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// The server's acknowledgement reuses the same opcode with no trailing
|
|
/// payload — ACE's <c>GameMessageCharacterDelete</c> constructs a bare
|
|
/// 4-byte body
|
|
/// (<c>ACE.Server/Network/GameMessages/Messages/GameMessageCharacterDelete.cs</c>,
|
|
/// base constructor called with <c>bodyLength: 4</c> and no further
|
|
/// <c>Writer.Write</c> calls). holtburger's inbound dispatcher
|
|
/// (<c>holtburger-protocol/src/messages/game_message/unpack.rs:50-58</c>)
|
|
/// disambiguates request vs. ack the identical way we do here — a request
|
|
/// has bytes remaining after the opcode, the ack does not.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// Routing note for LA7b: retail transmits this request via
|
|
/// <c>Proto_UI::SendToLogon</c> (the restore request rides
|
|
/// <c>SendToControl</c>); ACE sends its acknowledgement and the follow-up
|
|
/// refreshed CharacterList on <c>GameMessageGroup.UIQueue</c>.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// After the ack, ACE immediately follows with a fresh <see cref="CharacterList"/>
|
|
/// so the roster reflects the character's new pending-delete state
|
|
/// (<c>CharacterHandler.CharacterDelete</c>,
|
|
/// <c>ACE.Server/Network/Handlers/CharacterHandler.cs:322</c>, inside the
|
|
/// <c>SaveCharacter</c> success callback). Requesting and re-rendering that
|
|
/// refreshed roster belongs to LA7b's Runtime selection state — this file
|
|
/// only builds the request and recognizes the ack.
|
|
/// </para>
|
|
/// </summary>
|
|
public static class CharacterDelete
|
|
{
|
|
public const uint Opcode = 0xF655u;
|
|
|
|
/// <summary>
|
|
/// Build the body bytes for an outbound <c>CharacterDelete</c> request.
|
|
/// Layout: opcode(4) + String16L(accountName) + characterSlot(4).
|
|
/// </summary>
|
|
public static byte[] BuildRequestBody(string accountName, uint characterSlot)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(accountName);
|
|
var w = new PacketWriter(32);
|
|
w.WriteUInt32(Opcode);
|
|
w.WriteString16L(accountName);
|
|
w.WriteUInt32(characterSlot);
|
|
return w.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns whether a complete game-message body is the server's
|
|
/// delete acknowledgement — the canonical four-byte opcode-only form
|
|
/// ACE emits. A fresh <see cref="CharacterList"/> follows separately
|
|
/// and is not this method's concern.
|
|
/// </summary>
|
|
public static bool IsAcknowledgement(ReadOnlySpan<byte> body) =>
|
|
body.Length == sizeof(uint) &&
|
|
BinaryPrimitives.ReadUInt32LittleEndian(body) == Opcode;
|
|
}
|