0x00A0 InventoryServerSaveFailed carries (itemGuid, weenieError) per ACE GameEventInventoryServerSaveFailed.cs and holtburger events.rs:147. The old parser returned only the itemGuid as uint?, silently dropping the error code. Replaced with a typed InventoryServerSaveFailed record that reads both u32s (8-byte guard). Parser was unwired (no callers in GameEvents.cs or GameEventWiring.cs) so the signature change is safe. 1 new test in GameEventsInventoryTests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
499 lines
22 KiB
C#
499 lines
22 KiB
C#
using System;
|
||
using System.Buffers.Binary;
|
||
using System.Text;
|
||
|
||
namespace AcDream.Core.Net.Messages;
|
||
|
||
/// <summary>
|
||
/// Parser + record types for the most-used <see cref="GameEventType"/>
|
||
/// sub-opcodes inside the <c>0xF7B0</c> envelope. Each parser takes the
|
||
/// <see cref="GameEventEnvelope.Payload"/> slice (header stripped) and
|
||
/// returns a typed record or null on malformed payload.
|
||
///
|
||
/// <para>
|
||
/// References: r08 protocol atlas §4 (wire specs) + ACE
|
||
/// <c>GameEventChat.cs</c>, <c>GameEventTell.cs</c>,
|
||
/// <c>GameEventUpdateHealth.cs</c>, <c>GameEventWeenieError.cs</c>,
|
||
/// <c>GameEventCommunicationTransientString.cs</c>.
|
||
/// </para>
|
||
/// </summary>
|
||
public static class GameEvents
|
||
{
|
||
// ── Chat / communication ─────────────────────────────────────────────────
|
||
|
||
/// <summary>0x0147 ChannelBroadcast payload.</summary>
|
||
public readonly record struct ChannelBroadcast(
|
||
uint ChannelId,
|
||
string SenderName,
|
||
string Message);
|
||
|
||
public static ChannelBroadcast? ParseChannelBroadcast(ReadOnlySpan<byte> payload)
|
||
{
|
||
int pos = 0;
|
||
if (payload.Length < 4) return null;
|
||
uint channelId = BinaryPrimitives.ReadUInt32LittleEndian(payload);
|
||
pos += 4;
|
||
try
|
||
{
|
||
string sender = ReadString16L(payload, ref pos);
|
||
string message = ReadString16L(payload, ref pos);
|
||
return new ChannelBroadcast(channelId, sender, message);
|
||
}
|
||
catch { return null; }
|
||
}
|
||
|
||
/// <summary>0x02BD Tell payload.</summary>
|
||
public readonly record struct Tell(
|
||
string Message,
|
||
string SenderName,
|
||
uint SenderGuid,
|
||
uint TargetGuid,
|
||
uint ChatType);
|
||
|
||
public static Tell? ParseTell(ReadOnlySpan<byte> payload)
|
||
{
|
||
int pos = 0;
|
||
try
|
||
{
|
||
string message = ReadString16L(payload, ref pos);
|
||
string sender = ReadString16L(payload, ref pos);
|
||
if (payload.Length - pos < 12) return null;
|
||
uint senderGuid = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||
uint targetGuid = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||
uint chatType = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||
return new Tell(message, sender, senderGuid, targetGuid, chatType);
|
||
}
|
||
catch { return null; }
|
||
}
|
||
|
||
/// <summary>0x02EB CommunicationTransientString payload.</summary>
|
||
public readonly record struct TransientMessage(string Message, uint ChatType);
|
||
|
||
public static TransientMessage? ParseTransient(ReadOnlySpan<byte> payload)
|
||
{
|
||
int pos = 0;
|
||
try
|
||
{
|
||
string message = ReadString16L(payload, ref pos);
|
||
if (payload.Length - pos < 4) return null;
|
||
uint chatType = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos));
|
||
return new TransientMessage(message, chatType);
|
||
}
|
||
catch { return null; }
|
||
}
|
||
|
||
/// <summary>0x0004 PopupString — modal dialog text.</summary>
|
||
public static string? ParsePopupString(ReadOnlySpan<byte> payload)
|
||
{
|
||
int pos = 0;
|
||
try { return ReadString16L(payload, ref pos); } catch { return null; }
|
||
}
|
||
|
||
// ── Errors ──────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>0x028A WeenieError: generic game-logic failure code.</summary>
|
||
public static uint? ParseWeenieError(ReadOnlySpan<byte> payload)
|
||
{
|
||
if (payload.Length < 4) return null;
|
||
return BinaryPrimitives.ReadUInt32LittleEndian(payload);
|
||
}
|
||
|
||
/// <summary>0x028B WeenieErrorWithString.</summary>
|
||
public readonly record struct WeenieErrorWithString(uint ErrorCode, string Interpolation);
|
||
|
||
public static WeenieErrorWithString? ParseWeenieErrorWithString(ReadOnlySpan<byte> payload)
|
||
{
|
||
if (payload.Length < 4) return null;
|
||
uint code = BinaryPrimitives.ReadUInt32LittleEndian(payload);
|
||
int pos = 4;
|
||
try
|
||
{
|
||
string interp = ReadString16L(payload, ref pos);
|
||
return new WeenieErrorWithString(code, interp);
|
||
}
|
||
catch { return null; }
|
||
}
|
||
|
||
// ── Vitals / combat ─────────────────────────────────────────────────────
|
||
|
||
/// <summary>0x01C0 UpdateHealth: (guid, healthPercent 0..1).</summary>
|
||
public readonly record struct UpdateHealth(uint TargetGuid, float HealthPercent);
|
||
|
||
public static UpdateHealth? ParseUpdateHealth(ReadOnlySpan<byte> payload)
|
||
{
|
||
if (payload.Length < 8) return null;
|
||
uint guid = BinaryPrimitives.ReadUInt32LittleEndian(payload);
|
||
float pct = BinaryPrimitives.ReadSingleLittleEndian(payload.Slice(4));
|
||
return new UpdateHealth(guid, pct);
|
||
}
|
||
|
||
// ── Pings / misc ────────────────────────────────────────────────────────
|
||
|
||
/// <summary>0x01EA PingResponse: echoes the client ping id.</summary>
|
||
public static uint? ParsePingResponse(ReadOnlySpan<byte> payload)
|
||
{
|
||
if (payload.Length < 4) return null;
|
||
return BinaryPrimitives.ReadUInt32LittleEndian(payload);
|
||
}
|
||
|
||
// ── Spells / magic ──────────────────────────────────────────────────────
|
||
|
||
/// <summary>0x02C1 MagicUpdateSpell: spell id added to spellbook.</summary>
|
||
public static uint? ParseMagicUpdateSpell(ReadOnlySpan<byte> payload)
|
||
{
|
||
if (payload.Length < 4) return null;
|
||
return BinaryPrimitives.ReadUInt32LittleEndian(payload);
|
||
}
|
||
|
||
// ── Combat notifications ────────────────────────────────────────────────
|
||
|
||
/// <summary>0x01AC VictimNotification - death message for the victim.</summary>
|
||
public readonly record struct VictimNotification(string DeathMessage);
|
||
|
||
public static VictimNotification? ParseVictimNotification(ReadOnlySpan<byte> payload)
|
||
{
|
||
int pos = 0;
|
||
try { return new VictimNotification(ReadString16L(payload, ref pos)); }
|
||
catch { return null; }
|
||
}
|
||
|
||
/// <summary>0x01AD KillerNotification - death message for the killer.</summary>
|
||
public readonly record struct KillerNotification(string DeathMessage);
|
||
|
||
public static KillerNotification? ParseKillerNotification(ReadOnlySpan<byte> payload)
|
||
{
|
||
int pos = 0;
|
||
try { return new KillerNotification(ReadString16L(payload, ref pos)); }
|
||
catch { return null; }
|
||
}
|
||
|
||
/// <summary>0x01B1 AttackerNotification - "you hit X".</summary>
|
||
public readonly record struct AttackerNotification(
|
||
string DefenderName,
|
||
uint DamageType,
|
||
double HealthPercent,
|
||
uint Damage,
|
||
uint Critical,
|
||
ulong AttackConditions);
|
||
|
||
public static AttackerNotification? ParseAttackerNotification(ReadOnlySpan<byte> payload)
|
||
{
|
||
int pos = 0;
|
||
try
|
||
{
|
||
string name = ReadString16L(payload, ref pos);
|
||
if (payload.Length - pos < 28) return null;
|
||
uint damageType = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||
double pct = BinaryPrimitives.ReadDoubleLittleEndian(payload.Slice(pos)); pos += 8;
|
||
uint damage = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||
uint crit = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||
ulong cond = BinaryPrimitives.ReadUInt64LittleEndian(payload.Slice(pos)); pos += 8;
|
||
return new AttackerNotification(name, damageType, pct, damage, crit, cond);
|
||
}
|
||
catch { return null; }
|
||
}
|
||
|
||
/// <summary>0x01B2 DefenderNotification - "X hit you".</summary>
|
||
public readonly record struct DefenderNotification(
|
||
string AttackerName,
|
||
uint DamageType,
|
||
double HealthPercent,
|
||
uint Damage,
|
||
uint HitQuadrant,
|
||
uint Critical,
|
||
ulong AttackConditions);
|
||
|
||
public static DefenderNotification? ParseDefenderNotification(ReadOnlySpan<byte> payload)
|
||
{
|
||
int pos = 0;
|
||
try
|
||
{
|
||
string name = ReadString16L(payload, ref pos);
|
||
if (payload.Length - pos < 32) return null;
|
||
uint dtype = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||
double pct = BinaryPrimitives.ReadDoubleLittleEndian(payload.Slice(pos)); pos += 8;
|
||
uint dmg = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||
uint quad = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||
uint crit = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||
ulong cond = BinaryPrimitives.ReadUInt64LittleEndian(payload.Slice(pos)); pos += 8;
|
||
return new DefenderNotification(name, dtype, pct, dmg, quad, crit, cond);
|
||
}
|
||
catch { return null; }
|
||
}
|
||
|
||
/// <summary>0x01B3 EvasionAttackerNotification - "X evaded".</summary>
|
||
public static string? ParseEvasionAttackerNotification(ReadOnlySpan<byte> payload)
|
||
{
|
||
int pos = 0;
|
||
try { return ReadString16L(payload, ref pos); } catch { return null; }
|
||
}
|
||
|
||
/// <summary>0x01B4 EvasionDefenderNotification - "you evaded X".</summary>
|
||
public static string? ParseEvasionDefenderNotification(ReadOnlySpan<byte> payload)
|
||
{
|
||
int pos = 0;
|
||
try { return ReadString16L(payload, ref pos); } catch { return null; }
|
||
}
|
||
|
||
/// <summary>0x01B8 CombatCommenceAttack - empty payload.</summary>
|
||
public static bool ParseCombatCommenceAttack(ReadOnlySpan<byte> payload) => payload.Length == 0;
|
||
|
||
/// <summary>0x01A7 AttackDone - single WeenieError value.</summary>
|
||
public readonly record struct AttackDone(uint AttackSequence, uint WeenieError);
|
||
|
||
public static AttackDone? ParseAttackDone(ReadOnlySpan<byte> payload)
|
||
{
|
||
if (payload.Length < 4) return null;
|
||
return new AttackDone(0u, BinaryPrimitives.ReadUInt32LittleEndian(payload));
|
||
}
|
||
|
||
// ── Spell enchantments ──────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 0x02C3 MagicRemoveEnchantment — (layerId, spellId).
|
||
/// </summary>
|
||
public readonly record struct MagicRemoveEnchantment(uint LayerId, uint SpellId);
|
||
|
||
public static MagicRemoveEnchantment? ParseMagicRemoveEnchantment(ReadOnlySpan<byte> payload)
|
||
{
|
||
if (payload.Length < 8) return null;
|
||
return new MagicRemoveEnchantment(
|
||
BinaryPrimitives.ReadUInt32LittleEndian(payload),
|
||
BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(4)));
|
||
}
|
||
|
||
/// <summary>0x01A8 MagicRemoveSpell — spell id removed from spellbook.</summary>
|
||
public static uint? ParseMagicRemoveSpell(ReadOnlySpan<byte> payload)
|
||
{
|
||
if (payload.Length < 4) return null;
|
||
return BinaryPrimitives.ReadUInt32LittleEndian(payload);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 0x02C2 MagicUpdateEnchantment — the Enchantment blob. Full layout
|
||
/// (ACE <c>Enchantment.Pack</c>) is ~80+ bytes of spell metadata +
|
||
/// stat mods. We expose the first few fields that drive the enchant
|
||
/// bar UI; the rest is available via the raw payload view.
|
||
/// </summary>
|
||
public readonly record struct EnchantmentSummary(
|
||
uint SpellId,
|
||
uint LayerId,
|
||
float Duration,
|
||
uint CasterGuid);
|
||
|
||
public static EnchantmentSummary? ParseMagicUpdateEnchantment(ReadOnlySpan<byte> payload)
|
||
{
|
||
// Layout (ACE Enchantment.Pack):
|
||
// u32 spellId
|
||
// u32 layerId
|
||
// f32 duration
|
||
// u32 casterGuid
|
||
// ... (stat mods, category, power, etc.)
|
||
if (payload.Length < 16) return null;
|
||
return new EnchantmentSummary(
|
||
BinaryPrimitives.ReadUInt32LittleEndian(payload),
|
||
BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(4)),
|
||
BinaryPrimitives.ReadSingleLittleEndian(payload.Slice(8)),
|
||
BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(12)));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 0x02C7 MagicDispelEnchantment — (layerId, spellId).
|
||
/// Structure matches MagicRemoveEnchantment.
|
||
/// </summary>
|
||
public static MagicRemoveEnchantment? ParseMagicDispelEnchantment(ReadOnlySpan<byte> payload)
|
||
=> ParseMagicRemoveEnchantment(payload);
|
||
|
||
// ── Appraise / identify ─────────────────────────────────────────────────
|
||
|
||
/// <summary>0x00C9 IdentifyObjectResponse header.</summary>
|
||
public readonly record struct IdentifyResponseHeader(
|
||
uint Guid,
|
||
uint AppraiseFlags,
|
||
bool Success);
|
||
|
||
/// <summary>
|
||
/// Parse the header of an <c>IdentifyObjectResponse (0x00C9)</c>.
|
||
/// Full property-bundle deserialization (int / bool / float / string
|
||
/// tables per the AppraiseFlags bitfield) is a future pass; this
|
||
/// header alone is enough for the UI to display "Appraise complete
|
||
/// on target X" and to route into the repository.
|
||
/// </summary>
|
||
public static IdentifyResponseHeader? ParseIdentifyResponseHeader(ReadOnlySpan<byte> payload)
|
||
{
|
||
if (payload.Length < 12) return null;
|
||
uint guid = BinaryPrimitives.ReadUInt32LittleEndian(payload);
|
||
uint flags = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(4));
|
||
uint success = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(8));
|
||
return new IdentifyResponseHeader(guid, flags, success != 0);
|
||
}
|
||
|
||
/// <summary>0x0023 WieldObject: server-driven equip.</summary>
|
||
public readonly record struct WieldObject(
|
||
uint ItemGuid,
|
||
uint EquipLoc,
|
||
uint WielderGuid);
|
||
|
||
public static WieldObject? ParseWieldObject(ReadOnlySpan<byte> payload)
|
||
{
|
||
if (payload.Length < 12) return null;
|
||
return new WieldObject(
|
||
BinaryPrimitives.ReadUInt32LittleEndian(payload),
|
||
BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(4)),
|
||
BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(8)));
|
||
}
|
||
|
||
/// <summary>0x0022 InventoryPutObjInContainer: server puts item into container slot.
|
||
/// 4 fields (ACE GameEventItemServerSaysContainId.cs): itemGuid, containerGuid,
|
||
/// placement, containerType. ContainerType (0=item,1=container,2=foci) confirmed
|
||
/// vs holtburger events.rs fixture (slot=3 type=1).</summary>
|
||
public readonly record struct InventoryPutObjInContainer(
|
||
uint ItemGuid,
|
||
uint ContainerGuid,
|
||
uint Placement,
|
||
uint ContainerType);
|
||
|
||
public static InventoryPutObjInContainer? ParsePutObjInContainer(ReadOnlySpan<byte> payload)
|
||
{
|
||
if (payload.Length < 16) return null;
|
||
return new InventoryPutObjInContainer(
|
||
BinaryPrimitives.ReadUInt32LittleEndian(payload),
|
||
BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(4)),
|
||
BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(8)),
|
||
BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(12)));
|
||
}
|
||
|
||
/// <summary>0x0196 ViewContents: full contents list of a container you opened.
|
||
/// Layout (ACE GameEventViewContents.cs): containerGuid, count, [guid, containerType]×count.
|
||
/// Client consumer: ClientUISystem::OnViewContents (PackableList<ContentProfile>).</summary>
|
||
public readonly record struct ViewContentsEntry(uint Guid, uint ContainerType);
|
||
public readonly record struct ViewContents(uint ContainerGuid, System.Collections.Generic.IReadOnlyList<ViewContentsEntry> Items);
|
||
|
||
public static ViewContents? ParseViewContents(ReadOnlySpan<byte> payload)
|
||
{
|
||
if (payload.Length < 8) return null;
|
||
uint containerGuid = BinaryPrimitives.ReadUInt32LittleEndian(payload);
|
||
uint count = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(4));
|
||
int pos = 8;
|
||
if ((long)payload.Length - pos < (long)count * 8) return null;
|
||
var items = new ViewContentsEntry[count];
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
uint guid = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||
uint type = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||
items[i] = new ViewContentsEntry(guid, type);
|
||
}
|
||
return new ViewContents(containerGuid, items);
|
||
}
|
||
|
||
// ── Other small-payload events ──────────────────────────────────────────
|
||
|
||
/// <summary>0x01C7 UseDone: the Use/UseWithTarget completion signal (WeenieError code).</summary>
|
||
public static uint? ParseUseDone(ReadOnlySpan<byte> payload)
|
||
{
|
||
if (payload.Length < 4) return null;
|
||
return BinaryPrimitives.ReadUInt32LittleEndian(payload);
|
||
}
|
||
|
||
/// <summary>0x019A InventoryPutObjectIn3D: server dropped item to ground.</summary>
|
||
public static uint? ParsePutObjectIn3D(ReadOnlySpan<byte> payload)
|
||
{
|
||
if (payload.Length < 4) return null;
|
||
return BinaryPrimitives.ReadUInt32LittleEndian(payload);
|
||
}
|
||
|
||
/// <summary>0x00A0 InventoryServerSaveFailed: revert a speculative local inventory op.
|
||
/// (itemGuid, weenieError) — ACE GameEventInventoryServerSaveFailed.cs; holtburger
|
||
/// events.rs:147 reads both fields.</summary>
|
||
public readonly record struct InventoryServerSaveFailed(uint ItemGuid, uint WeenieError);
|
||
|
||
public static InventoryServerSaveFailed? ParseInventoryServerSaveFailed(ReadOnlySpan<byte> payload)
|
||
{
|
||
if (payload.Length < 8) return null;
|
||
return new InventoryServerSaveFailed(
|
||
BinaryPrimitives.ReadUInt32LittleEndian(payload),
|
||
BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(4)));
|
||
}
|
||
|
||
/// <summary>0x0052 CloseGroundContainer: server closed a ground container view.</summary>
|
||
public static uint? ParseCloseGroundContainer(ReadOnlySpan<byte> payload)
|
||
{
|
||
if (payload.Length < 4) return null;
|
||
return BinaryPrimitives.ReadUInt32LittleEndian(payload);
|
||
}
|
||
|
||
/// <summary>0x0207 TradeFailure: server trade error code.</summary>
|
||
public static uint? ParseTradeFailure(ReadOnlySpan<byte> payload)
|
||
{
|
||
if (payload.Length < 4) return null;
|
||
return BinaryPrimitives.ReadUInt32LittleEndian(payload);
|
||
}
|
||
|
||
/// <summary>0x0200 AddToTrade: (itemGuid, slotIndex).</summary>
|
||
public readonly record struct AddToTrade(uint ItemGuid, uint SlotIndex);
|
||
|
||
public static AddToTrade? ParseAddToTrade(ReadOnlySpan<byte> payload)
|
||
{
|
||
if (payload.Length < 8) return null;
|
||
return new AddToTrade(
|
||
BinaryPrimitives.ReadUInt32LittleEndian(payload),
|
||
BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(4)));
|
||
}
|
||
|
||
/// <summary>0x0202 AcceptTrade: initiator guid.</summary>
|
||
public static uint? ParseAcceptTrade(ReadOnlySpan<byte> payload)
|
||
{
|
||
if (payload.Length < 4) return null;
|
||
return BinaryPrimitives.ReadUInt32LittleEndian(payload);
|
||
}
|
||
|
||
/// <summary>0x0264 QueryItemManaResponse: (itemGuid, manaPercent).</summary>
|
||
public readonly record struct QueryItemManaResponse(uint ItemGuid, float ManaPercent);
|
||
|
||
public static QueryItemManaResponse? ParseQueryItemManaResponse(ReadOnlySpan<byte> payload)
|
||
{
|
||
if (payload.Length < 8) return null;
|
||
return new QueryItemManaResponse(
|
||
BinaryPrimitives.ReadUInt32LittleEndian(payload),
|
||
BinaryPrimitives.ReadSingleLittleEndian(payload.Slice(4)));
|
||
}
|
||
|
||
/// <summary>0x0274 CharacterConfirmationRequest — server-driven modal confirm.</summary>
|
||
public readonly record struct CharacterConfirmationRequest(
|
||
uint Type,
|
||
uint ContextId,
|
||
uint OtherGuid,
|
||
string Message);
|
||
|
||
public static CharacterConfirmationRequest? ParseCharacterConfirmationRequest(ReadOnlySpan<byte> payload)
|
||
{
|
||
if (payload.Length < 12) return null;
|
||
int pos = 0;
|
||
uint type = BinaryPrimitives.ReadUInt32LittleEndian(payload); pos += 4;
|
||
uint contextId = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||
uint otherGuid = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); pos += 4;
|
||
try
|
||
{
|
||
string msg = ReadString16L(payload, ref pos);
|
||
return new CharacterConfirmationRequest(type, contextId, otherGuid, msg);
|
||
}
|
||
catch { return null; }
|
||
}
|
||
|
||
// ── Shared string reader (matches LoginRequest.ReadString16L) ───────────
|
||
|
||
private static string ReadString16L(ReadOnlySpan<byte> source, ref int pos)
|
||
{
|
||
if (source.Length - pos < 2) throw new FormatException("truncated String16L length");
|
||
ushort length = BinaryPrimitives.ReadUInt16LittleEndian(source.Slice(pos));
|
||
pos += 2;
|
||
if (source.Length - pos < length) throw new FormatException("truncated String16L body");
|
||
// Windows-1252 matches retail (and holtburger's encoding_rs::WINDOWS_1252).
|
||
string result = Encoding.GetEncoding(1252).GetString(source.Slice(pos, length));
|
||
pos += length;
|
||
int recordSize = 2 + length;
|
||
int padding = (4 - (recordSize & 3)) & 3;
|
||
pos += padding;
|
||
return result;
|
||
}
|
||
}
|