From 68efb60b491fcce3ca742b31aa6ad6f3e73ac301 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 18 Apr 2026 17:18:36 +0200 Subject: [PATCH] feat(interact): Phase B.4 Use / UseWithTarget / TeleToLifestone outbound MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Click-to-interact wire layer. Adds the three most common "do a thing to an object" GameActions that the UI triggers on left-click / use-item contexts. Wire layer: - InteractRequests.BuildUse (0x0036): single target guid — click a door, loot a corpse, talk to an NPC, activate a lifestone, step on a portal. - InteractRequests.BuildUseWithTarget (0x0035): source + target — key on locked door, scroll on self, salvage tool on item. - InteractRequests.BuildTeleToLifestone (0x0063): no-arg recall. Fails server-side if not tied; reply comes back as GameEvent WeenieError. Server reply for Use + UseWithTarget is GameEventType.UseDone (0x01C7) carrying a WeenieError code (0 = success). Already parsed; wiring into a "UseDone" event on CombatState-style holder can be a follow-up. Tests (3 new): byte-exact encoding of all three builders. Build green, 616 tests pass (up from 613). Ref: r08 §3 rows 0x0035/0x0036/0x0063. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Messages/InteractRequests.cs | 76 +++++++++++++++++++ .../Messages/InteractRequestsTests.cs | 45 +++++++++++ 2 files changed, 121 insertions(+) create mode 100644 src/AcDream.Core.Net/Messages/InteractRequests.cs create mode 100644 tests/AcDream.Core.Net.Tests/Messages/InteractRequestsTests.cs 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))); + } +}