acdream/src/AcDream.Core.Net/Messages/CharacterDelete.cs
Erik 4338b1c1f3 fix(net): Campaign LA LA7a review fixes — AD-97 register row, corrected restore justification
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>
2026-08-14 16:01:52 +02:00

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&lt;char&gt;::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;
}