diff --git a/src/AcDream.Core.Net/Messages/InteractRequests.cs b/src/AcDream.Core.Net/Messages/InteractRequests.cs
new file mode 100644
index 0000000..d9cfe7b
--- /dev/null
+++ b/src/AcDream.Core.Net/Messages/InteractRequests.cs
@@ -0,0 +1,76 @@
+using System.Buffers.Binary;
+
+namespace AcDream.Core.Net.Messages;
+
+///
+/// Outbound interaction GameActions. Each carries a single uint32
+/// target guid (plus a second guid for UseWithTarget) inside the
+/// standard 0xF7B1 GameAction envelope.
+///
+///
+/// Wire layout (r08 §3 rows 0x0035/0x0036):
+///
+/// u32 0xF7B1
+/// u32 gameActionSequence
+/// u32 subOpcode
+/// u32 targetGuid // e.g. door, NPC, lifestone, corpse, item
+/// u32 sourceGuid // only for UseWithTarget (the item you're using)
+///
+///
+///
+///
+/// Server reply is GameEventType.UseDone (0x01C7) carrying a
+/// WeenieError; 0 = success.
+///
+///
+public static class InteractRequests
+{
+ public const uint GameActionEnvelope = 0xF7B1u;
+ public const uint UseOpcode = 0x0036u;
+ public const uint UseWithTargetOpcode = 0x0035u;
+ public const uint TeleToLifestoneOpcode = 0x0063u;
+
+ ///
+ /// Use an object: click a door, loot a corpse, talk to an NPC,
+ /// activate a lifestone, step onto a portal.
+ ///
+ public static byte[] BuildUse(uint gameActionSequence, uint targetGuid)
+ {
+ byte[] body = new byte[16];
+ BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
+ BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), gameActionSequence);
+ BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), UseOpcode);
+ BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), targetGuid);
+ return body;
+ }
+
+ ///
+ /// Use source item on target: e.g. key on locked door, key on
+ /// chest, scroll on yourself, salvage tool on an item.
+ ///
+ public static byte[] BuildUseWithTarget(
+ uint gameActionSequence, uint sourceGuid, uint targetGuid)
+ {
+ byte[] body = new byte[20];
+ BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
+ BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), gameActionSequence);
+ BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), UseWithTargetOpcode);
+ BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), sourceGuid);
+ BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(16), targetGuid);
+ return body;
+ }
+
+ ///
+ /// Teleport to your lifestone. No target guid — just tells the
+ /// server "recall me." Fails if you haven't tied to a lifestone
+ /// (server responds with a WeenieError).
+ ///
+ public static byte[] BuildTeleToLifestone(uint gameActionSequence)
+ {
+ byte[] body = new byte[12];
+ BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
+ BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), gameActionSequence);
+ BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), TeleToLifestoneOpcode);
+ return body;
+ }
+}
diff --git a/tests/AcDream.Core.Net.Tests/Messages/InteractRequestsTests.cs b/tests/AcDream.Core.Net.Tests/Messages/InteractRequestsTests.cs
new file mode 100644
index 0000000..5e99b5d
--- /dev/null
+++ b/tests/AcDream.Core.Net.Tests/Messages/InteractRequestsTests.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Buffers.Binary;
+using AcDream.Core.Net.Messages;
+using Xunit;
+
+namespace AcDream.Core.Net.Tests.Messages;
+
+public sealed class InteractRequestsTests
+{
+ [Fact]
+ public void BuildUse_WritesOpcode0x0036AndTarget()
+ {
+ byte[] body = InteractRequests.BuildUse(gameActionSequence: 2, targetGuid: 0xDEAD);
+
+ Assert.Equal(16, body.Length);
+ Assert.Equal(InteractRequests.UseOpcode,
+ BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
+ Assert.Equal(0xDEADu,
+ BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12)));
+ }
+
+ [Fact]
+ public void BuildUseWithTarget_WritesBothGuids()
+ {
+ byte[] body = InteractRequests.BuildUseWithTarget(
+ gameActionSequence: 3, sourceGuid: 0x1000, targetGuid: 0x2000);
+
+ Assert.Equal(20, body.Length);
+ Assert.Equal(InteractRequests.UseWithTargetOpcode,
+ BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
+ Assert.Equal(0x1000u,
+ BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12)));
+ Assert.Equal(0x2000u,
+ BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(16)));
+ }
+
+ [Fact]
+ public void BuildTeleToLifestone_IsEnvelopeOnly()
+ {
+ byte[] body = InteractRequests.BuildTeleToLifestone(gameActionSequence: 7);
+ Assert.Equal(12, body.Length);
+ Assert.Equal(InteractRequests.TeleToLifestoneOpcode,
+ BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
+ }
+}