Task 9 of the B-Wire inventory-wire plan. Adds three outbound C→S GameAction builders to InventoryActions.cs (DropItem 0x001B, GetAndWieldItem 0x001A, NoLongerViewingContents 0x0195) plus matching opcode constants after TeleToPoiOpcode. Adds SendDropItem/SendGetAndWieldItem/SendNoLongerViewingContents wrappers to WorldSession.cs (after SendRemoveShortcut, ~line 1157), following the exact same NextGameActionSequence() + SendGameAction() pattern as the existing Send* family. Three byte-layout tests added to InventoryActionsTests.cs (12 total, all green). Core.Net build clean. Unblocks B-Drag (drop) and container-open (NoLongerViewingContents on close) call sites in the inventory controller. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
174 lines
8.7 KiB
C#
174 lines
8.7 KiB
C#
using System.Buffers.Binary;
|
|
|
|
namespace AcDream.Core.Net.Messages;
|
|
|
|
/// <summary>
|
|
/// Outbound inventory-manipulation GameActions: stack merge/split, give,
|
|
/// drop, shortcuts. All ride in the <c>0xF7B1</c> GameAction envelope.
|
|
///
|
|
/// <para>
|
|
/// References: r08 §3 rows 0x0054-0x0056 / 0x019B-0x019D / 0x00CD.
|
|
/// </para>
|
|
/// </summary>
|
|
public static class InventoryActions
|
|
{
|
|
public const uint GameActionEnvelope = 0xF7B1u;
|
|
|
|
public const uint StackableMergeOpcode = 0x0054u;
|
|
public const uint StackableSplitToContainerOpcode = 0x0055u;
|
|
public const uint StackableSplitTo3DOpcode = 0x0056u;
|
|
public const uint StackableSplitToWieldOpcode = 0x019Bu;
|
|
public const uint GiveObjectRequestOpcode = 0x00CDu;
|
|
public const uint AddShortcutOpcode = 0x019Cu;
|
|
public const uint RemoveShortcutOpcode = 0x019Du;
|
|
public const uint TeleToPoiOpcode = 0x00B1u;
|
|
public const uint GetAndWieldItemOpcode = 0x001Au;
|
|
public const uint DropItemOpcode = 0x001Bu;
|
|
public const uint NoLongerViewingContentsOpcode = 0x0195u;
|
|
|
|
/// <summary>
|
|
/// Merge stack A into stack B of the same item type. Server validates
|
|
/// compatibility; if the merge fails it rolls back via
|
|
/// InventoryServerSaveFailed (0x00A0).
|
|
/// </summary>
|
|
public static byte[] BuildStackableMerge(uint seq, uint mergeFromGuid, uint mergeToGuid, uint amount)
|
|
{
|
|
byte[] body = new byte[24];
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), StackableMergeOpcode);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), mergeFromGuid);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(16), mergeToGuid);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(20), amount);
|
|
return body;
|
|
}
|
|
|
|
/// <summary>Split N items off a stack into a container at placement.</summary>
|
|
public static byte[] BuildStackableSplitToContainer(
|
|
uint seq, uint stackGuid, uint containerGuid, uint placement, uint amount)
|
|
{
|
|
byte[] body = new byte[28];
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), StackableSplitToContainerOpcode);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), stackGuid);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(16), containerGuid);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(20), placement);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(24), amount);
|
|
return body;
|
|
}
|
|
|
|
/// <summary>Split N items off a stack and drop them on the ground.</summary>
|
|
public static byte[] BuildStackableSplitTo3D(uint seq, uint stackGuid, uint amount)
|
|
{
|
|
byte[] body = new byte[20];
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), StackableSplitTo3DOpcode);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), stackGuid);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(16), amount);
|
|
return body;
|
|
}
|
|
|
|
/// <summary>Split N items off a stack into an equip slot.</summary>
|
|
public static byte[] BuildStackableSplitToWield(
|
|
uint seq, uint stackGuid, uint equipLocation, uint amount)
|
|
{
|
|
byte[] body = new byte[24];
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), StackableSplitToWieldOpcode);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), stackGuid);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(16), equipLocation);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(20), amount);
|
|
return body;
|
|
}
|
|
|
|
/// <summary>Give an item (or a stack of N) to a target creature/NPC.</summary>
|
|
public static byte[] BuildGiveObjectRequest(
|
|
uint seq, uint targetGuid, uint itemGuid, uint amount)
|
|
{
|
|
byte[] body = new byte[24];
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), GiveObjectRequestOpcode);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), targetGuid);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(16), itemGuid);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(20), amount);
|
|
return body;
|
|
}
|
|
|
|
/// <summary>Pin an item/spell to a quickbar slot. ShortCutData = Index(u32), ObjectId(u32),
|
|
/// SpellId(u16), Layer(u16) — CONFIRMED across ACE/Chorizite/holtburger (action-bar deep-dive
|
|
/// §131-145). For an ITEM: objectGuid = item guid, spellId = layer = 0.</summary>
|
|
public static byte[] BuildAddShortcut(
|
|
uint seq, uint index, uint objectGuid, ushort spellId, ushort layer)
|
|
{
|
|
byte[] body = new byte[24];
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), AddShortcutOpcode);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), index);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(16), objectGuid);
|
|
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(20), spellId);
|
|
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(22), layer);
|
|
return body;
|
|
}
|
|
|
|
/// <summary>Unpin a quickbar slot.</summary>
|
|
public static byte[] BuildRemoveShortcut(uint seq, uint slotIndex)
|
|
{
|
|
byte[] body = new byte[16];
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), RemoveShortcutOpcode);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), slotIndex);
|
|
return body;
|
|
}
|
|
|
|
/// <summary>Teleport to a Point of Interest (quest-driven recall).</summary>
|
|
public static byte[] BuildTeleToPoi(uint seq, uint poiId)
|
|
{
|
|
byte[] body = new byte[16];
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), TeleToPoiOpcode);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), poiId);
|
|
return body;
|
|
}
|
|
|
|
/// <summary>Drop an item on the ground. ACE GameActionDropItem.Handle reads 1 u32.</summary>
|
|
public static byte[] BuildDropItem(uint seq, uint itemGuid)
|
|
{
|
|
byte[] body = new byte[16];
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), DropItemOpcode);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), itemGuid);
|
|
return body;
|
|
}
|
|
|
|
/// <summary>Equip an item from inventory onto the doll. holtburger actions.rs
|
|
/// GetAndWieldItemActionData = (itemGuid, equipMask).</summary>
|
|
public static byte[] BuildGetAndWieldItem(uint seq, uint itemGuid, uint equipMask)
|
|
{
|
|
byte[] body = new byte[20];
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), GetAndWieldItemOpcode);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), itemGuid);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(16), equipMask);
|
|
return body;
|
|
}
|
|
|
|
/// <summary>Close a side-pack / ground-container view. holtburger actions.rs = (containerGuid).</summary>
|
|
public static byte[] BuildNoLongerViewingContents(uint seq, uint containerGuid)
|
|
{
|
|
byte[] body = new byte[16];
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), NoLongerViewingContentsOpcode);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), containerGuid);
|
|
return body;
|
|
}
|
|
}
|