using System;
using System.Buffers.Binary;
using System.Text;
namespace AcDream.Core.Net.Messages;
///
/// Parser + record types for the most-used
/// sub-opcodes inside the 0xF7B0 envelope. Each parser takes the
/// slice (header stripped) and
/// returns a typed record or null on malformed payload.
///
///
/// References: r08 protocol atlas §4 (wire specs) + ACE
/// GameEventChat.cs, GameEventTell.cs,
/// GameEventUpdateHealth.cs, GameEventWeenieError.cs,
/// GameEventCommunicationTransientString.cs.
///
///
public static class GameEvents
{
// ── Chat / communication ─────────────────────────────────────────────────
/// 0x0147 ChannelBroadcast payload.
public readonly record struct ChannelBroadcast(
uint ChannelId,
string SenderName,
string Message);
public static ChannelBroadcast? ParseChannelBroadcast(ReadOnlySpan 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; }
}
/// 0x02BD Tell payload.
public readonly record struct Tell(
string Message,
string SenderName,
uint SenderGuid,
uint TargetGuid,
uint ChatType);
public static Tell? ParseTell(ReadOnlySpan 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; }
}
/// 0x02EB CommunicationTransientString payload.
public readonly record struct TransientMessage(string Message, uint ChatType);
public static TransientMessage? ParseTransient(ReadOnlySpan 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; }
}
/// 0x0004 PopupString — modal dialog text.
public static string? ParsePopupString(ReadOnlySpan payload)
{
int pos = 0;
try { return ReadString16L(payload, ref pos); } catch { return null; }
}
// ── Errors ──────────────────────────────────────────────────────────────
/// 0x028A WeenieError: generic game-logic failure code.
public static uint? ParseWeenieError(ReadOnlySpan payload)
{
if (payload.Length < 4) return null;
return BinaryPrimitives.ReadUInt32LittleEndian(payload);
}
/// 0x028B WeenieErrorWithString.
public readonly record struct WeenieErrorWithString(uint ErrorCode, string Interpolation);
public static WeenieErrorWithString? ParseWeenieErrorWithString(ReadOnlySpan 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 ─────────────────────────────────────────────────────
/// 0x01C0 UpdateHealth: (guid, healthPercent 0..1).
public readonly record struct UpdateHealth(uint TargetGuid, float HealthPercent);
public static UpdateHealth? ParseUpdateHealth(ReadOnlySpan 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 ────────────────────────────────────────────────────────
/// 0x01EA PingResponse: echoes the client ping id.
public static uint? ParsePingResponse(ReadOnlySpan payload)
{
if (payload.Length < 4) return null;
return BinaryPrimitives.ReadUInt32LittleEndian(payload);
}
// ── Spells / magic ──────────────────────────────────────────────────────
/// 0x02C1 MagicUpdateSpell: spell id added to spellbook.
public static uint? ParseMagicUpdateSpell(ReadOnlySpan payload)
{
if (payload.Length < 4) return null;
return BinaryPrimitives.ReadUInt32LittleEndian(payload);
}
// ── Combat notifications ────────────────────────────────────────────────
/// 0x01AC VictimNotification - death message for the victim.
public readonly record struct VictimNotification(string DeathMessage);
public static VictimNotification? ParseVictimNotification(ReadOnlySpan payload)
{
int pos = 0;
try { return new VictimNotification(ReadString16L(payload, ref pos)); }
catch { return null; }
}
/// 0x01AD KillerNotification - death message for the killer.
public readonly record struct KillerNotification(string DeathMessage);
public static KillerNotification? ParseKillerNotification(ReadOnlySpan payload)
{
int pos = 0;
try { return new KillerNotification(ReadString16L(payload, ref pos)); }
catch { return null; }
}
/// 0x01B1 AttackerNotification - "you hit X".
public readonly record struct AttackerNotification(
string DefenderName,
uint DamageType,
double HealthPercent,
uint Damage,
uint Critical,
ulong AttackConditions);
public static AttackerNotification? ParseAttackerNotification(ReadOnlySpan 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; }
}
/// 0x01B2 DefenderNotification - "X hit you".
public readonly record struct DefenderNotification(
string AttackerName,
uint DamageType,
double HealthPercent,
uint Damage,
uint HitQuadrant,
uint Critical,
ulong AttackConditions);
public static DefenderNotification? ParseDefenderNotification(ReadOnlySpan 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; }
}
/// 0x01B3 EvasionAttackerNotification - "X evaded".
public static string? ParseEvasionAttackerNotification(ReadOnlySpan payload)
{
int pos = 0;
try { return ReadString16L(payload, ref pos); } catch { return null; }
}
/// 0x01B4 EvasionDefenderNotification - "you evaded X".
public static string? ParseEvasionDefenderNotification(ReadOnlySpan payload)
{
int pos = 0;
try { return ReadString16L(payload, ref pos); } catch { return null; }
}
/// 0x01B8 CombatCommenceAttack - empty payload.
public static bool ParseCombatCommenceAttack(ReadOnlySpan payload) => payload.Length == 0;
/// 0x01A7 AttackDone - single WeenieError value.
public readonly record struct AttackDone(uint AttackSequence, uint WeenieError);
public static AttackDone? ParseAttackDone(ReadOnlySpan payload)
{
if (payload.Length < 4) return null;
return new AttackDone(0u, BinaryPrimitives.ReadUInt32LittleEndian(payload));
}
// ── Spell enchantments ──────────────────────────────────────────────────
///
/// 0x02C3 MagicRemoveEnchantment — (layerId, spellId).
///
public readonly record struct MagicRemoveEnchantment(uint LayerId, uint SpellId);
public static MagicRemoveEnchantment? ParseMagicRemoveEnchantment(ReadOnlySpan payload)
{
if (payload.Length < 8) return null;
return new MagicRemoveEnchantment(
BinaryPrimitives.ReadUInt32LittleEndian(payload),
BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(4)));
}
/// 0x01A8 MagicRemoveSpell — spell id removed from spellbook.
public static uint? ParseMagicRemoveSpell(ReadOnlySpan payload)
{
if (payload.Length < 4) return null;
return BinaryPrimitives.ReadUInt32LittleEndian(payload);
}
///
/// 0x02C2 MagicUpdateEnchantment — the Enchantment blob. Full layout
/// (ACE Enchantment.Pack) 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.
///
public readonly record struct EnchantmentSummary(
uint SpellId,
uint LayerId,
float Duration,
uint CasterGuid);
public static EnchantmentSummary? ParseMagicUpdateEnchantment(ReadOnlySpan 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)));
}
///
/// 0x02C7 MagicDispelEnchantment — (layerId, spellId).
/// Structure matches MagicRemoveEnchantment.
///
public static MagicRemoveEnchantment? ParseMagicDispelEnchantment(ReadOnlySpan payload)
=> ParseMagicRemoveEnchantment(payload);
// ── Appraise / identify ─────────────────────────────────────────────────
/// 0x00C9 IdentifyObjectResponse header.
public readonly record struct IdentifyResponseHeader(
uint Guid,
uint AppraiseFlags,
bool Success);
///
/// Parse the header of an IdentifyObjectResponse (0x00C9).
/// 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.
///
public static IdentifyResponseHeader? ParseIdentifyResponseHeader(ReadOnlySpan 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);
}
/// 0x0023 WieldObject: server-driven equip.
public readonly record struct WieldObject(
uint ItemGuid,
uint EquipLoc,
uint WielderGuid);
public static WieldObject? ParseWieldObject(ReadOnlySpan payload)
{
if (payload.Length < 12) return null;
return new WieldObject(
BinaryPrimitives.ReadUInt32LittleEndian(payload),
BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(4)),
BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(8)));
}
/// 0x0022 InventoryPutObjInContainer: server puts item into container slot.
public readonly record struct InventoryPutObjInContainer(
uint ItemGuid,
uint ContainerGuid,
uint Placement);
public static InventoryPutObjInContainer? ParsePutObjInContainer(ReadOnlySpan payload)
{
if (payload.Length < 12) return null;
return new InventoryPutObjInContainer(
BinaryPrimitives.ReadUInt32LittleEndian(payload),
BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(4)),
BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(8)));
}
// ── Other small-payload events ──────────────────────────────────────────
/// 0x01C7 UseDone: the Use/UseWithTarget completion signal (WeenieError code).
public static uint? ParseUseDone(ReadOnlySpan payload)
{
if (payload.Length < 4) return null;
return BinaryPrimitives.ReadUInt32LittleEndian(payload);
}
/// 0x019A InventoryPutObjectIn3D: server dropped item to ground.
public static uint? ParsePutObjectIn3D(ReadOnlySpan payload)
{
if (payload.Length < 4) return null;
return BinaryPrimitives.ReadUInt32LittleEndian(payload);
}
/// 0x00A0 InventoryServerSaveFailed: revert a speculative local inventory op.
public static uint? ParseInventoryServerSaveFailed(ReadOnlySpan payload)
{
if (payload.Length < 4) return null;
return BinaryPrimitives.ReadUInt32LittleEndian(payload);
}
/// 0x0052 CloseGroundContainer: server closed a ground container view.
public static uint? ParseCloseGroundContainer(ReadOnlySpan payload)
{
if (payload.Length < 4) return null;
return BinaryPrimitives.ReadUInt32LittleEndian(payload);
}
/// 0x0207 TradeFailure: server trade error code.
public static uint? ParseTradeFailure(ReadOnlySpan payload)
{
if (payload.Length < 4) return null;
return BinaryPrimitives.ReadUInt32LittleEndian(payload);
}
/// 0x0200 AddToTrade: (itemGuid, slotIndex).
public readonly record struct AddToTrade(uint ItemGuid, uint SlotIndex);
public static AddToTrade? ParseAddToTrade(ReadOnlySpan payload)
{
if (payload.Length < 8) return null;
return new AddToTrade(
BinaryPrimitives.ReadUInt32LittleEndian(payload),
BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(4)));
}
/// 0x0202 AcceptTrade: initiator guid.
public static uint? ParseAcceptTrade(ReadOnlySpan payload)
{
if (payload.Length < 4) return null;
return BinaryPrimitives.ReadUInt32LittleEndian(payload);
}
/// 0x0264 QueryItemManaResponse: (itemGuid, manaPercent).
public readonly record struct QueryItemManaResponse(uint ItemGuid, float ManaPercent);
public static QueryItemManaResponse? ParseQueryItemManaResponse(ReadOnlySpan payload)
{
if (payload.Length < 8) return null;
return new QueryItemManaResponse(
BinaryPrimitives.ReadUInt32LittleEndian(payload),
BinaryPrimitives.ReadSingleLittleEndian(payload.Slice(4)));
}
/// 0x0274 CharacterConfirmationRequest — server-driven modal confirm.
public readonly record struct CharacterConfirmationRequest(
uint Type,
uint ContextId,
uint OtherGuid,
string Message);
public static CharacterConfirmationRequest? ParseCharacterConfirmationRequest(ReadOnlySpan 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 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;
}
}