acdream/src/AcDream.Core.Net/Messages/SocialActions.cs
Erik 86c0a7e0ee feat(runtime,net): Campaign OP slice OP1 — full character-option table, dirty model, real 0x01A1 blob builder
The retail Options panel (Campaign OP) needs a Runtime-owned option map
covering all 53 PlayerOption ids and the real batched SetCharacterOptions
(0x01A1) blob before any UI can be built on top of it. Today's surface only
modeled 6 ListenTo*Chat ids and the 0x01A1 builder was a malformed 16-byte
stub (deleted at Campaign CH slice CH3, docs/research/2026-08-09-chat-side-
channels-vs-ace.md).

- CharacterOptionTable.cs: the ONE typed table, PlayerOption id (0x00..0x34)
  -> (Options1/Options2 word, mask, IsAutoSave, ClientDefault), transcribed
  from acclient.h's verbatim CharacterOption/CharacterOptions2/PlayerOption
  enums and byte-verified against IsAutoSaveOption @0x0059A600 (the 21-id
  auto-save table) and GetDefaultOptionValue @0x005D2A30 (the Defaults-
  button table). Reconstructing CharacterOptions1/2 defaults from the
  ClientDefault column independently reproduces 0x50C4A54A / 0x00008700,
  cross-confirming the id-mask mapping. CharacterOptionId (SocialActions.cs)
  widened from 6 to all 53 ids to match.
- RuntimeCharacterOptionsState: SetOptionBit now resolves through the full
  table (was a 6-case switch). New TrySetOption is the ONE shared local-
  write-then-send/dirty seam — mirrors CPlayerModule::OnChanged exactly:
  write the bit locally first, then either send 0x0005 immediately (auto-
  save ids) or MarkDirty for the batched blob, no-op on an unchanged value
  (retail's own early-return) or an unmodeled id. New dirty model (IsDirty/
  FirstDirtiedAt/MarkDirty/TryFlush/TryFlushIfAutoSaveDue) uses an injected
  TimeProvider so it's fully unit-testable without a live clock.
- Both IRuntimeCharacterCommands.SetSingleOption adapters (Direct + Current)
  now route through TrySetOption instead of duplicating the write; this
  fixes the headless local-write gap the OP1 research flagged (the direct
  adapter previously sent the wire message without writing the bit first,
  same class of bug CH4 fixed for the graphical host). Both also reject an
  id outside the table instead of silently accepting it. LiveSessionRuntime
  Factory's SendSingleCharacterOption closure now delegates to the same
  seam instead of duplicating write-then-send inline.
- New IRuntimeCharacterCommands.SaveOptions(generation) — the explicit
  blob-flush verb (retail's SaveToServer(force: 0)) — wired end-to-end in
  both adapters, including a new SaveCharacterOptionsRuntimeCmd on the
  graphical router.
- SocialActions.BuildSetCharacterOptions + WorldSession.SendSetCharacterOptions:
  the real PlayerModule::Pack body per the wire research's field-by-field
  layout — header always 0x460 OR'd with 0x001/0x008 when shortcuts/desired
  comps are non-empty, favorite spells always 8 lists, never sets 0x100 or
  0x200. Echoes last-parsed shortcuts/favorites/desired-comps/spellbook
  filters (via new CharacterOptionsBlobSource) instead of zeroing them.
  Conformance: a hand-computed golden byte vector (not generated by the
  builder under test — the CH3 builder died of tests that pinned a wrong
  shape and looked green) plus a round-trip through PlayerDescriptionParser.

Contract deviation: the 480 s auto-save timer and the flush-before-logout
trigger are implemented as fully-tested pure state-machine logic
(TryFlushIfAutoSaveDue) but are NOT wired into either host's live per-frame
loop or graceful-shutdown sequence in this slice — only the explicit
SaveOptions verb is production-wired. Wiring the timer touches App's
UpdateFrameOrchestrator graph and Headless's tick loop (outside this
slice's Runtime/wire-layer scope); wiring logout risks the already-fragile
graceful-shutdown sequence CLAUDE.md flags. Filed as TS-71 per the plan's
own escape valve ("target: not deferred" with a register row if deferred).
Also filed: AP-193 (the 0x34 HearPKDeathMessages id/mask is ACE-sourced,
unverifiable against the 2013 binary) and AP-194 (GetDefaultOptionValue's
table disagrees with the constructor default for ConfirmVolatileRareUse/
ShowHelm/ShowCloak — retail's own quirk, reproduced not fixed).

Tests: table completeness x53, auto-save/client-default split pinned
id-by-id against the byte-verified tables, unknown/reserved-id rejection
(0x35/0x36 landmines), local-write-then-send on both adapters + the router,
the dirty/flush state machine, SaveOptions, and the wire golden vector +
PlayerDescriptionParser round-trip. Full Release suite: 12,745 passed / 4
skipped / 0 failed (baseline 12,611/4/0 — slice adds 134 passing tests,
zero regressions).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 23:31:34 +02:00

417 lines
18 KiB
C#

using System;
using System.Buffers.Binary;
using System.Text;
using AcDream.Core.Items;
namespace AcDream.Core.Net.Messages;
/// <summary>
/// Outbound social + query GameActions. These share a common pattern:
/// single-target / single-parameter GameActions inside the <c>0xF7B1</c>
/// envelope.
///
/// <para>
/// Wire format:
/// <code>
/// u32 0xF7B1 envelope
/// u32 gameActionSequence
/// u32 subOpcode
/// &lt;payload&gt; per-action
/// </code>
/// </para>
///
/// <para>
/// References: r08 §3 rows for each opcode.
/// </para>
/// </summary>
public static class SocialActions
{
public const uint GameActionEnvelope = 0xF7B1u;
// Queries
public const uint QueryHealthOpcode = 0x01BFu; // u32 targetGuid
public const uint QueryItemManaOpcode = 0x0263u; // u32 itemGuid; zero cancels
public const uint PingRequestOpcode = 0x01E9u; // no payload
// Fellowship
public const uint FellowshipCreateOpcode = 0x00A2u; // string16L name, bool openness, bool shareXP
public const uint FellowshipQuitOpcode = 0x00A3u; // bool disband
public const uint FellowshipDismissOpcode = 0x00A4u; // u32 guid
public const uint FellowshipRecruitOpcode = 0x00A5u; // u32 guid
public const uint FellowshipUpdateOpcode = 0x00A6u; // bool open
// Character options
// CH3 (2026-08-09): the full-blob SetCharacterOptions (0x01A1) builder
// and the string-payload AddChannel/RemoveChannel (0x0145/0x0146)
// builders were deleted here — none had a production caller, and all
// three were malformed against ACE's real reader (research doc
// 2026-08-09-chat-side-channels-vs-ace.md §3.8/§3.7/§5.5). Only
// SetSingleCharacterOption (0x0005) — the message that actually
// changes Turbine room membership — has a caller (WorldSession.
// SendSetSingleCharacterOption), so only it is implemented.
public const uint SetSingleCharacterOptionOpcode = 0x0005u; // u32 optionId, u32 value (0/1)
// OP1 (Campaign OP, 2026-08-10): the real batched-option blob, resurrected
// per docs/research/2026-08-10-set-character-options-wire.md §2.3-§2.7 —
// NOT the malformed 16-byte CH3 builder this opcode used to name (deleted
// 2026-08-09, post-mortem in that doc §6). Body IS
// `PlayerModule::Pack @0x005D45C0`.
public const uint SetCharacterOptionsOpcode = 0x01A1u;
/// <summary>
/// <c>PlayerModulePackHeader</c> bits retail's 2013 client ALWAYS sets
/// (<c>PlayerModule::SetPackHeader @0x005D44A0</c>, BYTE-VERIFIED — wire
/// research §2.2): <c>SpellLists8 (0x400)</c>, <c>SpellbookFilters
/// (0x020)</c>, <c>2ndCharacterOptions/Options2 (0x040)</c>. The other
/// unconditional bits from the same disassembly are OR'd in below when
/// their section is non-empty; <c>SquelchList (0x02)</c>,
/// <c>MultiSpellList (0x04)</c>, <c>ExtendedMultiSpellLists (0x10)</c>,
/// and <c>TimeStampFormat (0x80)</c> are NEVER set by the 2013 client and
/// never appear here; <c>GenericQualitiesData (0x100)</c> is never set by
/// acdream (wire research §2.4d U2 — float sub-table shape disputed
/// between retail and ACE, unreachable if we never set it);
/// <c>GameplayOptions (0x200)</c> is omitted while acdream packs nothing
/// into <c>m_colGameplayOptions</c> (safe per §2.5 — the receiver leaves
/// its collection untouched when the flag is absent).
/// </summary>
private const uint PlayerModulePackHeaderBase =
0x400u // PM_Packed_8_SpellLists
| 0x020u // PM_Packed_SpellbookFilters
| 0x040u; // PM_Packed_2ndCharacterOptions
private const uint PlayerModulePackHeaderShortcuts = 0x001u; // PM_Packed_ShortCutManager
private const uint PlayerModulePackHeaderDesiredComps = 0x008u; // PM_Packed_DesiredComps
/// <summary>Query a target's health — server replies with UpdateHealth (0x01C0).</summary>
public static byte[] BuildQueryHealth(uint seq, uint targetGuid)
{
byte[] body = new byte[16];
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), QueryHealthOpcode);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), targetGuid);
return body;
}
/// <summary>
/// Query an owned item's mana fraction, or cancel the active query with item guid zero.
/// Retail anchor: <c>CM_Item::Event_QueryItemMana @ 0x006A8610</c>.
/// </summary>
public static byte[] BuildQueryItemMana(uint seq, uint itemGuid)
{
byte[] body = new byte[16];
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), QueryItemManaOpcode);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), itemGuid);
return body;
}
/// <summary>
/// Request the empty PingResponse (0x01EA). Retail
/// <c>CM_Character::Event_RequestPing @ 0x006A19A0</c> sends no payload.
/// </summary>
public static byte[] BuildPingRequest(uint seq)
{
byte[] body = new byte[12];
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), PingRequestOpcode);
return body;
}
/// <summary>Create a fellowship with a chosen name + options.</summary>
public static byte[] BuildFellowshipCreate(
uint seq, string fellowshipName, bool openness, bool shareXp)
{
byte[] name = PackString16L(fellowshipName);
// 2 bools consume 2 bytes + alignment pad to 4.
int boolBlock = 2;
int pad = (4 - ((name.Length + boolBlock) & 3)) & 3;
byte[] body = new byte[12 + name.Length + boolBlock + pad];
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), FellowshipCreateOpcode);
Array.Copy(name, 0, body, 12, name.Length);
body[12 + name.Length] = openness ? (byte)1 : (byte)0;
body[12 + name.Length + 1] = shareXp ? (byte)1 : (byte)0;
return body;
}
/// <summary>Quit your current fellowship (optionally disband if leader).</summary>
public static byte[] BuildFellowshipQuit(uint seq, bool disband)
{
byte[] body = new byte[16]; // envelope + 1 byte bool aligned to 4
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), FellowshipQuitOpcode);
body[12] = disband ? (byte)1 : (byte)0;
return body;
}
/// <summary>Dismiss a specific vassal from your fellowship.</summary>
public static byte[] BuildFellowshipDismiss(uint seq, uint targetGuid)
=> SingleGuid(seq, FellowshipDismissOpcode, targetGuid);
/// <summary>Recruit a target into your fellowship.</summary>
public static byte[] BuildFellowshipRecruit(uint seq, uint targetGuid)
=> SingleGuid(seq, FellowshipRecruitOpcode, targetGuid);
/// <summary>Toggle fellowship open / closed recruiting.</summary>
public static byte[] BuildFellowshipUpdate(uint seq, bool open)
{
byte[] body = new byte[16];
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), FellowshipUpdateOpcode);
body[12] = open ? (byte)1 : (byte)0;
return body;
}
/// <summary>
/// Toggle one character option and push it to the server.
/// <c>GameActionSetSingleCharacterOption @ GameActionType 0x0005</c> —
/// the ONLY wire message that changes Turbine room membership: for the
/// <c>ListenTo*Chat</c> option ids, ACE's handler both flips the option
/// AND calls <c>JoinTurbineChatChannel</c>/<c>LeaveTurbineChatChannel</c>
/// (re-pushing <c>SetTurbineChatChannels</c>). Payload
/// <c>u32 option, u32 value</c> confirmed against ACE
/// (<c>GameActionSetSingleCharacterOption.cs:11-12</c>) and holtburger
/// (<c>SetSingleCharacterOptionActionData::pack</c>,
/// <c>messages/player/actions.rs:149-157</c>) — both agree byte-for-byte.
/// </summary>
public static byte[] BuildSetSingleCharacterOption(uint seq, uint optionId, bool value)
{
byte[] body = new byte[20];
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), SetSingleCharacterOptionOpcode);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), optionId);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(16), value ? 1u : 0u);
return body;
}
/// <summary>
/// Flush the batched-option module: <c>SetCharacterOptions (0x01A1)</c>.
/// Body IS <c>PlayerModule::Pack @0x005D45C0</c> — wire research §2.3
/// field-by-field, exactly. The header is always
/// <see cref="PlayerModulePackHeaderBase"/> (<c>0x460</c>) OR'd with the
/// per-section bits below when that section is non-empty; ACE stores
/// <paramref name="options1"/>/<paramref name="options2"/> and discards
/// the four "TODO" sections (shortcuts, spell lists, desired comps,
/// spellbook filters) into their own dedicated GameActions, but retail
/// still packs them, so this builder echoes the caller's last-parsed
/// values instead of zeroing them (§5.3) — <b>never</b> invent zeros for
/// state this session actually has. <see cref="favoriteSpells"/> MUST
/// have exactly 8 entries, matching retail's unconditional
/// <c>favorite_spells_[8]</c> — an empty tab is a lone <c>u32 0</c>.
/// Never sets header bit <c>0x100</c> (GenericQualitiesData, U2 —
/// unresolved float sub-table shape) or <c>0x200</c> (GameplayOptions,
/// unpacked by acdream today — CH6f). Every field is a 4-byte-aligned
/// <c>u32</c>/record, so the trailing pad (§2.7) is always zero bytes in
/// practice, but the computation is still performed for exact fidelity
/// with <c>PlayerModule::Pack</c>'s own unconditional pad step.
/// </summary>
public static byte[] BuildSetCharacterOptions(
uint seq,
uint options1,
uint options2,
IReadOnlyList<ShortcutEntry> shortcuts,
IReadOnlyList<IReadOnlyList<uint>> favoriteSpells,
IReadOnlyDictionary<uint, uint> desiredComponents,
uint spellbookFilters)
{
ArgumentNullException.ThrowIfNull(shortcuts);
ArgumentNullException.ThrowIfNull(favoriteSpells);
ArgumentNullException.ThrowIfNull(desiredComponents);
if (favoriteSpells.Count != 8)
{
throw new ArgumentException(
"Retail PlayerModule::Pack always emits exactly 8 favorite-spell lists (acclient.h:36507 favorite_spells_[8]).",
nameof(favoriteSpells));
}
uint header = PlayerModulePackHeaderBase;
if (shortcuts.Count > 0) header |= PlayerModulePackHeaderShortcuts;
if (desiredComponents.Count > 0) header |= PlayerModulePackHeaderDesiredComps;
int payloadSize =
4 // header
+ 4 // options1
+ (shortcuts.Count > 0 ? 4 + 12 * shortcuts.Count : 0)
+ FavoriteSpellsPackSize(favoriteSpells)
+ (desiredComponents.Count > 0 ? 4 + 8 * desiredComponents.Count : 0)
+ 4 // spellbookFilters
+ 4; // options2
int pad = (4 - (payloadSize & 3)) & 3;
byte[] body = new byte[12 + payloadSize + pad];
int p = 0;
WriteU32(body, ref p, GameActionEnvelope);
WriteU32(body, ref p, seq);
WriteU32(body, ref p, SetCharacterOptionsOpcode);
WriteU32(body, ref p, header);
WriteU32(body, ref p, options1);
if (shortcuts.Count > 0)
{
WriteU32(body, ref p, (uint)shortcuts.Count);
foreach (ShortcutEntry entry in shortcuts)
{
WriteI32(body, ref p, entry.Index);
WriteU32(body, ref p, entry.ObjectId);
WriteU32(body, ref p, entry.SpellId);
}
}
for (int tab = 0; tab < 8; tab++)
{
IReadOnlyList<uint> list = favoriteSpells[tab];
int count = list?.Count ?? 0;
WriteU32(body, ref p, (uint)count);
for (int i = 0; i < count; i++)
WriteU32(body, ref p, list![i]);
}
if (desiredComponents.Count > 0)
{
// PackableHashTable<K,V>::Pack @0x005692B0: sizeInfo = (tableSize
// << 16) | count. ACE (and acdream's own inbound parser) only
// reads the low 16 bits; the advisory high half is left zero.
WriteU32(body, ref p, (uint)desiredComponents.Count);
foreach (KeyValuePair<uint, uint> kvp in desiredComponents)
{
WriteU32(body, ref p, kvp.Key);
WriteU32(body, ref p, kvp.Value);
}
}
WriteU32(body, ref p, spellbookFilters);
WriteU32(body, ref p, options2);
// Tail pad bytes are already zero from `new byte[]`; nothing to write.
return body;
}
private static int FavoriteSpellsPackSize(
IReadOnlyList<IReadOnlyList<uint>> favoriteSpells)
{
int size = 0;
for (int tab = 0; tab < 8; tab++)
size += 4 + 4 * (favoriteSpells[tab]?.Count ?? 0);
return size;
}
private static void WriteU32(byte[] dest, ref int pos, uint value)
{
BinaryPrimitives.WriteUInt32LittleEndian(dest.AsSpan(pos), value);
pos += 4;
}
private static void WriteI32(byte[] dest, ref int pos, int value)
{
BinaryPrimitives.WriteInt32LittleEndian(dest.AsSpan(pos), value);
pos += 4;
}
// ── Helpers ──────────────────────────────────────────────────────────────
private static byte[] SingleGuid(uint seq, uint sub, uint guid)
{
byte[] body = new byte[16];
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), sub);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), guid);
return body;
}
private static byte[] PackString16L(string s)
{
ArgumentNullException.ThrowIfNull(s);
// Windows-1252 matches retail (and holtburger's encoding_rs::WINDOWS_1252).
byte[] data = Encoding.GetEncoding(1252).GetBytes(s);
if (data.Length > ushort.MaxValue)
throw new ArgumentException("String too long for 16-bit length prefix.", nameof(s));
int recordSize = 2 + data.Length;
int padding = (4 - (recordSize & 3)) & 3;
byte[] result = new byte[recordSize + padding];
BinaryPrimitives.WriteUInt16LittleEndian(result, (ushort)data.Length);
Array.Copy(data, 0, result, 2, data.Length);
return result;
}
}
/// <summary>
/// The linear <c>PlayerOption</c> id space (a LINEAR enum, distinct from the
/// <c>CharacterOptions1</c>/<c>CharacterOptions2</c> BITFIELDS) — the first
/// <c>u32</c> of a <c>SetSingleCharacterOption (0x0005)</c> payload, and the
/// key into <see cref="AcDream.Runtime.Gameplay.CharacterOptionTable"/>.
/// Campaign OP slice OP1 (2026-08-10) widened this from the 6
/// <c>ListenTo*Chat</c> ids Campaign CH slice CH3 needed to the complete
/// <c>0x00..0x34</c> set, verbatim from <c>named-retail/acclient.h:4162</c>
/// (<c>enum PlayerOption</c>) — every member below is
/// <c>&lt;Name&gt;_PlayerOption</c> there with its <c>_PlayerOption</c>
/// suffix dropped, EXCEPT the six pre-existing <c>ListenTo*Chat</c> members
/// (retail names them <c>Hear*Chat_PlayerOption</c>; kept as-is rather than
/// renamed, since every existing caller — <c>TurbineChatMembershipGate</c>,
/// its tests, the CH3/CH4 chat wiring — already spells them this way).
/// <c>HearPkDeathMessages</c> (<c>0x34</c>) is ACE-sourced, not present in
/// the 2013 PDB (the id was <c>TotalNumberOfPlayerOptions_PlayerOption</c>
/// there) — register row, wire research §8.1.
/// </summary>
public enum CharacterOptionId : uint
{
AutoRepeatAttack = 0x00,
IgnoreAllegianceRequests = 0x01,
IgnoreFellowshipRequests = 0x02,
IgnoreTradeRequests = 0x03,
DisableMostWeatherEffects = 0x04,
PersistentAtDay = 0x05,
AllowGive = 0x06,
ViewCombatTarget = 0x07,
ShowTooltips = 0x08,
UseDeception = 0x09,
ToggleRun = 0x0A,
StayInChatMode = 0x0B,
AdvancedCombatUI = 0x0C,
AutoTarget = 0x0D,
VividTargetingIndicator = 0x0E,
FellowshipShareXP = 0x0F,
AcceptLootPermits = 0x10,
FellowshipShareLoot = 0x11,
FellowshipAutoAcceptRequests = 0x12,
SideBySideVitals = 0x13,
CoordinatesOnRadar = 0x14,
SpellDuration = 0x15,
DisableHouseRestrictionEffects = 0x16,
DragItemOnPlayerOpensSecureTrade = 0x17,
DisplayAllegianceLogonNotifications = 0x18,
UseChargeAttack = 0x19,
UseCraftSuccessDialog = 0x1A,
ListenToAllegianceChat = 0x1B,
DisplayDateOfBirth = 0x1C,
DisplayAge = 0x1D,
DisplayChessRank = 0x1E,
DisplayFishingSkill = 0x1F,
DisplayNumberDeaths = 0x20,
DisplayTimeStamps = 0x21,
SalvageMultiple = 0x22,
ListenToGeneralChat = 0x23,
ListenToTradeChat = 0x24,
ListenToLFGChat = 0x25,
ListenToRoleplayChat = 0x26,
AppearOffline = 0x27,
DisplayNumberCharacterTitles = 0x28,
MainPackPreferred = 0x29,
LeadMissileTargets = 0x2A,
UseFastMissiles = 0x2B,
FilterLanguage = 0x2C,
ConfirmVolatileRareUse = 0x2D,
ListenToSocietyChat = 0x2E,
ShowHelm = 0x2F,
DisableDistanceFog = 0x30,
UseMouseTurning = 0x31,
ShowCloak = 0x32,
LockUI = 0x33,
HearPkDeathMessages = 0x34,
}