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

@ -5,6 +5,7 @@ using AcDream.Core.Items;
using AcDream.Core.Net.Messages;
using AcDream.Core.Player;
using AcDream.Core.Spells;
using AcDream.Core.Social;
namespace AcDream.Core.Net;
@ -70,7 +71,11 @@ public static class GameEventWiring
// the player's own PropertyBundle (EncumbranceVal etc.) into the player ClientObject.
Func<uint>? playerGuid = null,
Action<uint /*weenieError*/>? onUseDone = null,
ItemManaState? itemMana = null)
ItemManaState? itemMana = null,
Action<GameEvents.CharacterConfirmationRequest>? onConfirmationRequest = null,
FriendsState? friends = null,
SquelchState? squelch = null,
Action<IReadOnlyList<(uint Id, uint Amount)>>? onDesiredComponents = null)
{
ArgumentNullException.ThrowIfNull(dispatcher);
ArgumentNullException.ThrowIfNull(items);
@ -99,6 +104,42 @@ public static class GameEventWiring
var s = GameEvents.ParsePopupString(e.Payload.Span);
if (s is not null) chat.OnPopup(s);
});
dispatcher.Register(GameEventType.QueryAgeResponse, e =>
{
var p = GameEvents.ParseQueryAgeResponse(e.Payload.Span);
if (p is null) return;
string text = string.IsNullOrEmpty(p.Value.Name)
? $"You have played for {p.Value.Age}."
: $"{p.Value.Name} has played for {p.Value.Age}.";
chat.OnSystemMessage(text, chatType: 0u);
});
if (onConfirmationRequest is not null)
{
dispatcher.Register(GameEventType.CharacterConfirmationRequest, e =>
{
var request = GameEvents.ParseCharacterConfirmationRequest(e.Payload.Span);
if (request is not null)
onConfirmationRequest(request.Value);
});
}
if (friends is not null)
{
dispatcher.Register(GameEventType.FriendsListUpdate, e =>
{
FriendsUpdate? update = SocialStateMessages.ParseFriendsUpdate(e.Payload.Span);
if (update is not null) friends.Apply(update);
});
}
if (squelch is not null)
{
dispatcher.Register(GameEventType.SetSquelchDB, e =>
{
SquelchDatabase? database = SocialStateMessages.ParseSquelchDatabase(e.Payload.Span);
if (database is not null) squelch.Replace(database);
});
}
// ── TurbineChat channel list (0x0295 SetTurbineChatChannels) ─────
// Phase I.6: arrives once at login (and after chat-server reconnect)
@ -251,6 +292,7 @@ public static class GameEventWiring
{
var p = GameEvents.ParseWieldObject(e.Payload.Span);
if (p is null) return;
uint wielderGuid = p.Value.WielderGuid != 0u
? p.Value.WielderGuid
: (playerGuid?.Invoke() ?? 0u);
@ -264,6 +306,7 @@ public static class GameEventWiring
{
var p = GameEvents.ParsePutObjInContainer(e.Payload.Span);
if (p is null) return;
items.MoveItem(
p.Value.ItemGuid,
p.Value.ContainerGuid,
@ -385,6 +428,8 @@ public static class GameEventWiring
Console.WriteLine($"vitals: PlayerDescription body.len={e.Payload.Length} parsed={(p is null ? "NULL" : $"vec={p.Value.VectorFlags} attrs={p.Value.Attributes.Count} spells={p.Value.Spells.Count}")}");
if (p is null) return;
onDesiredComponents?.Invoke(p.Value.DesiredComps);
// B-Wire: deliver the player's OWN properties to the player ClientObject.
// (PD's "membership manifest" rule is about ITEMS, whose data comes from
// CreateObject; the player's own stats legitimately come from PD.) Upsert
@ -412,6 +457,15 @@ public static class GameEventWiring
if (localPlayer is not null)
{
localPlayer.OnProperties(p.Value.Properties);
localPlayer.OnPositions(p.Value.Positions.ToDictionary(
static pair => pair.Key,
static pair => new AcDream.Core.Physics.Position(
pair.Value.LandblockId,
new System.Numerics.Vector3(
pair.Value.X, pair.Value.Y, pair.Value.Z),
new System.Numerics.Quaternion(
pair.Value.Qx, pair.Value.Qy,
pair.Value.Qz, pair.Value.Qw))));
foreach (var attr in p.Value.Attributes)
{