feat(chat): port retail client command families

Expand the typed client-command boundary across travel, character queries, local UI and layout controls, AFK and consent, emotes, friends, squelch and filters, and fill-components. Preserve retail packet layouts and queue ownership, import the confirmation dialog, and keep authoritative social state in Core.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-13 14:50:15 +02:00
parent 5a45a7ac7f
commit 9ea579bdd0
47 changed files with 6659 additions and 99 deletions

View file

@ -9,6 +9,7 @@ using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Player;
using AcDream.Core.Spells;
using AcDream.Core.Social;
using Xunit;
namespace AcDream.Core.Net.Tests;
@ -142,6 +143,72 @@ public sealed class GameEventWiringTests
Assert.Equal(new[] { 0x1500u }, wieldConfirmed);
}
[Fact]
public void WireAll_ConfirmationRequest_UsesRetailTwoIntegerHeader()
{
var dispatcher = new GameEventDispatcher();
var items = new ClientObjectTable();
var combat = new CombatState();
var spellbook = new Spellbook();
var chat = new ChatLog();
GameEvents.CharacterConfirmationRequest? received = null;
GameEventWiring.WireAll(
dispatcher,
items,
combat,
spellbook,
chat,
onConfirmationRequest: request => received = request);
byte[] text = MakeString16L("Proceed?");
byte[] payload = new byte[8 + text.Length];
BinaryPrimitives.WriteUInt32LittleEndian(payload, 7u);
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(4), 42u);
text.CopyTo(payload.AsSpan(8));
var env = GameEventEnvelope.TryParse(WrapEnvelope(
GameEventType.CharacterConfirmationRequest,
payload));
dispatcher.Dispatch(env!.Value);
Assert.Equal(new GameEvents.CharacterConfirmationRequest(7u, 42u, "Proceed?"), received);
}
[Fact]
public void WireAll_FriendsUpdate_ReplacesAuthoritativeSocialState()
{
var dispatcher = new GameEventDispatcher();
var friends = new FriendsState();
GameEventWiring.WireAll(
dispatcher,
new ClientObjectTable(),
new CombatState(),
new Spellbook(),
new ChatLog(),
friends: friends);
byte[] name = MakeString16L("Alice");
byte[] payload = new byte[4 + 12 + name.Length + 4 + 4 + 4];
int p = 0;
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(p), 1u); p += 4;
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(p), 0x50000001u); p += 4;
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(p), 1u); p += 4;
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(p), 0u); p += 4;
name.CopyTo(payload.AsSpan(p)); p += name.Length;
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(p), 0u); p += 4;
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(p), 0u); p += 4;
BinaryPrimitives.WriteUInt32LittleEndian(
payload.AsSpan(p), (uint)FriendsUpdateType.Full);
var env = GameEventEnvelope.TryParse(WrapEnvelope(
GameEventType.FriendsListUpdate, payload));
dispatcher.Dispatch(env!.Value);
FriendEntry friend = Assert.Single(friends.Snapshot());
Assert.Equal("Alice", friend.Name);
Assert.True(friend.Online);
}
[Fact]
public void WireAll_PopupString_RoutesToChatLog()
{
@ -155,6 +222,24 @@ public sealed class GameEventWiringTests
Assert.Equal(ChatKind.Popup, chat.Snapshot()[0].Kind);
}
[Theory]
[InlineData("", "2d 4h", "You have played for 2d 4h.")]
[InlineData("Alice", "1y 2mo", "Alice has played for 1y 2mo.")]
public void WireAll_QueryAgeResponse_UsesRetailWording(
string name, string age, string expected)
{
var (d, _, _, _, chat) = MakeAll();
byte[] nameBytes = MakeString16L(name);
byte[] ageBytes = MakeString16L(age);
byte[] payload = [.. nameBytes, .. ageBytes];
var env = GameEventEnvelope.TryParse(
WrapEnvelope(GameEventType.QueryAgeResponse, payload));
d.Dispatch(env!.Value);
Assert.Equal(expected, chat.Snapshot().Single().Text);
}
[Fact]
public void WireAll_PlayerDescription_PopulatesLocalPlayerStateVitals()
{