feat(net): D.2b-B B-Wire — DropItem/GetAndWieldItem/NoLongerViewingContents builders + Send wrappers

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>
This commit is contained in:
Erik 2026-06-21 18:58:30 +02:00
parent 7013651a90
commit de8baa1aa1
3 changed files with 93 additions and 0 deletions

View file

@ -22,6 +22,9 @@ public static class InventoryActions
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
@ -133,4 +136,39 @@ public static class InventoryActions
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;
}
}

View file

@ -1156,6 +1156,27 @@ public sealed class WorldSession : IDisposable
SendGameAction(InventoryActions.BuildRemoveShortcut(seq, index));
}
/// <summary>Send DropItem (0x001B) — drop an item on the ground.</summary>
public void SendDropItem(uint itemGuid)
{
uint seq = NextGameActionSequence();
SendGameAction(InventoryActions.BuildDropItem(seq, itemGuid));
}
/// <summary>Send GetAndWieldItem (0x001A) — equip an item to an equip slot.</summary>
public void SendGetAndWieldItem(uint itemGuid, uint equipMask)
{
uint seq = NextGameActionSequence();
SendGameAction(InventoryActions.BuildGetAndWieldItem(seq, itemGuid, equipMask));
}
/// <summary>Send NoLongerViewingContents (0x0195) — close a container view.</summary>
public void SendNoLongerViewingContents(uint containerGuid)
{
uint seq = NextGameActionSequence();
SendGameAction(InventoryActions.BuildNoLongerViewingContents(seq, containerGuid));
}
/// <summary>Send retail QueryHealth (0x01BF). Server replies UpdateHealth (0x01C0).</summary>
/// <remarks>
/// Retail anchor: <c>CM_Combat::Event_QueryHealth</c> / <c>gmToolbarUI::HandleSelectionChanged:198635</c>

View file

@ -113,4 +113,38 @@ public sealed class InventoryActionsTests
Assert.Equal(42u,
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12)));
}
[Fact]
public void BuildDropItem_layout()
{
byte[] b = InventoryActions.BuildDropItem(seq: 7, itemGuid: 0x50000A01u);
Assert.Equal(16, b.Length);
Assert.Equal(0xF7B1u, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(0)));
Assert.Equal(7u, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(4)));
Assert.Equal(0x001Bu, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(8)));
Assert.Equal(0x50000A01u, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(12)));
}
[Fact]
public void BuildGetAndWieldItem_layout()
{
byte[] b = InventoryActions.BuildGetAndWieldItem(seq: 7, itemGuid: 0x50000A01u, equipMask: 0x02u);
Assert.Equal(20, b.Length);
Assert.Equal(0xF7B1u, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(0)));
Assert.Equal(7u, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(4)));
Assert.Equal(0x001Au, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(8)));
Assert.Equal(0x50000A01u, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(12)));
Assert.Equal(0x02u, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(16)));
}
[Fact]
public void BuildNoLongerViewingContents_layout()
{
byte[] b = InventoryActions.BuildNoLongerViewingContents(seq: 7, containerGuid: 0x500000C9u);
Assert.Equal(16, b.Length);
Assert.Equal(0xF7B1u, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(0)));
Assert.Equal(7u, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(4)));
Assert.Equal(0x0195u, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(8)));
Assert.Equal(0x500000C9u, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(12)));
}
}