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

@ -730,6 +730,10 @@ public sealed class GameWindow : IDisposable
public readonly AcDream.Core.Chat.TurbineChatState TurbineChat = new();
public readonly AcDream.Core.Combat.CombatState Combat = new();
public readonly AcDream.Core.Items.ItemManaState ItemMana = new();
public readonly AcDream.Core.Social.FriendsState Friends = new();
public readonly AcDream.Core.Social.SquelchState Squelch = new();
public IReadOnlyList<(uint Id, uint Amount)> DesiredComponents { get; private set; }
= System.Array.Empty<(uint Id, uint Amount)>();
// Issue #11 — load static spell metadata from data/spells.csv at startup.
// Provides Family for buff stacking (issue #6) + names + icons + tooltips
// for the future Spellbook panel. The CSV is copied to bin/<config>/net10.0/data/
@ -757,6 +761,7 @@ public sealed class GameWindow : IDisposable
// when no selection. Spec: docs/superpowers/specs/2026-05-15-phase-b7-target-indicator-design.md
private AcDream.App.UI.TargetIndicatorPanel? _targetIndicator;
private AcDream.UI.Abstractions.Panels.Vitals.VitalsVM? _vitalsVm;
private AcDream.UI.Abstractions.Panels.Chat.ChatVM? _retailChatVm;
// Phase D.2b — retained host + composition runtime. Null unless ACDREAM_RETAIL_UI=1.
private AcDream.App.UI.UiHost? _uiHost;
private AcDream.App.UI.RetailUiRuntime? _retailUiRuntime;
@ -2114,6 +2119,7 @@ public sealed class GameWindow : IDisposable
coordinatesOnRadar: () => _persistedGameplay.CoordinatesOnRadar,
uiLocked: () => _persistedGameplay.LockUI);
var retailChatVm = new AcDream.UI.Abstractions.Panels.Chat.ChatVM(Chat, displayLimit: 200);
_retailChatVm = retailChatVm;
AcDream.App.UI.RetailUiPersistenceBindings? persistence = _settingsStore is null
? null
: new AcDream.App.UI.RetailUiPersistenceBindings(
@ -2642,7 +2648,17 @@ public sealed class GameWindow : IDisposable
onShortcuts: list => Shortcuts = list,
playerGuid: () => _playerServerGuid,
onUseDone: error => _itemInteractionController?.CompleteUse(error),
itemMana: ItemMana);
itemMana: ItemMana,
onConfirmationRequest: request =>
_retailUiRuntime?.ConfirmationDialogs?.Show(
request.Message,
accepted => session.SendConfirmationResponse(
request.Type,
request.ContextId,
accepted)),
friends: Friends,
squelch: Squelch,
onDesiredComponents: components => DesiredComponents = components);
// Phase I.7: subscribe to CombatState events and emit
// retail-faithful "You hit X for Y damage" chat lines into
@ -2717,7 +2733,62 @@ public sealed class GameWindow : IDisposable
var turbineChat = TurbineChat;
uint playerGuid = _playerServerGuid;
var clientCommandController = new AcDream.App.UI.ClientCommandController(
liveSession.SendTeleportToLifestone);
new AcDream.App.UI.ClientCommandController.Bindings(
TeleportToLifestone: liveSession.SendTeleportToLifestone,
TeleportToMarketplace: liveSession.SendTeleportToMarketplace,
TeleportToPkArena: liveSession.SendTeleportToPkArena,
TeleportToPkLiteArena: liveSession.SendTeleportToPkLiteArena,
TeleportToHouse: liveSession.SendTeleportToHouse,
TeleportToMansion: liveSession.SendTeleportToMansion,
QueryAge: liveSession.SendQueryAge,
QueryBirth: liveSession.SendQueryBirth,
ToggleFrameRate: ToggleRetailFrameRate,
ToggleUiLock: () => SetRetailUiLocked(!_persistedGameplay.LockUI),
ShowSystemMessage: text => chat.OnSystemMessage(text, 0u),
ShowWeenieError: code => chat.OnWeenieError(code, null),
PlayerPublicWeenieBitfield: () =>
Objects.Get(_playerServerGuid)?.PublicWeenieBitfield,
ClientVersion: () =>
typeof(GameWindow).Assembly.GetName().Version?.ToString(3)
?? "unknown",
CurrentPosition: () => _playerController?.CellPosition,
LastOutsideCorpsePosition: () =>
LocalPlayer.GetPosition(0x0Eu),
ShowConfirmation: (message, completed) =>
_retailUiRuntime?.ConfirmationDialogs?.Show(message, completed),
Suicide: liveSession.SendSuicide,
ClearChat: _ => chat.Clear(),
SaveUi: name => _retailUiRuntime?.SaveNamedLayout(name),
LoadUi: name => _retailUiRuntime?.RestoreNamedLayout(name),
SaveAutoUi: () => _retailUiRuntime?.SaveLayout(),
LoadAutoUi: () => _retailUiRuntime?.RestoreLayout(),
IsAway: () =>
Objects.Get(_playerServerGuid)?.Properties.GetBool(0x6Eu) == true,
SetAway: away => SetRetailAway(liveSession, away),
SetAwayMessage: liveSession.SendSetAfkMessage,
AcceptLootPermits: () => _persistedGameplay.AcceptLootPermits,
SetAcceptLootPermits: SetRetailAcceptLootPermits,
DisplayConsent: liveSession.SendDisplayConsent,
ClearConsent: liveSession.SendClearConsent,
RemoveConsent: liveSession.SendRemoveConsent,
SendEmote: liveSession.SendEmote,
Friends: Friends,
AddFriend: liveSession.SendAddFriend,
RemoveFriend: liveSession.SendRemoveFriend,
ClearFriends: liveSession.SendClearFriends,
RequestLegacyFriends: liveSession.SendLegacyFriendsListRequest,
Squelch: Squelch,
ModifyCharacterSquelch: liveSession.SendModifyCharacterSquelch,
ModifyAccountSquelch: liveSession.SendModifyAccountSquelch,
ModifyGlobalSquelch: liveSession.SendModifyGlobalSquelch,
LastTeller: () => _retailChatVm?.LastIncomingTellSender,
ClearDesiredComponents: () =>
{
liveSession.SendClearDesiredComponents();
DesiredComponents = System.Array.Empty<(uint Id, uint Amount)>();
},
HasOpenVendor: () => false,
FillComponentBuyList: (_, _) => { }));
_commandBus.Register<AcDream.UI.Abstractions.ExecuteClientCommandCmd>(
clientCommandController.Execute);
_commandBus.Register<AcDream.UI.Abstractions.SendServerCommandCmd>(cmd =>
@ -11386,6 +11457,46 @@ public sealed class GameWindow : IDisposable
}
}
private void SetRetailAway(AcDream.Core.Net.WorldSession session, bool away)
{
session.SendSetAfkMode(away);
}
private void SetRetailAcceptLootPermits(bool enabled)
{
_persistedGameplay = _persistedGameplay with { AcceptLootPermits = enabled };
_settingsVm?.SetGameplay(
_settingsVm.GameplayDraft with { AcceptLootPermits = enabled });
_settingsStore?.SaveGameplay(_persistedGameplay);
}
/// <summary>
/// Retail <c>ClientCommunicationSystem::DoFrameRate @ 0x005707D0</c>:
/// flip the live display flag and persist it through the same settings
/// path used by the Display panel.
/// </summary>
private void ToggleRetailFrameRate()
{
_persistedDisplay = _persistedDisplay with
{
ShowFps = !_persistedDisplay.ShowFps,
};
if (_settingsVm is not null)
_settingsVm.SetDisplay(_settingsVm.DisplayDraft with
{
ShowFps = _persistedDisplay.ShowFps,
});
try
{
_settingsStore?.SaveDisplay(_persistedDisplay);
}
catch (Exception ex)
{
Console.WriteLine($"settings: framerate display save failed: {ex.Message}");
}
}
private void SetRetailCombatGameplay(
AcDream.UI.Abstractions.Panels.Settings.GameplaySettings gameplay)
{