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

@ -48,7 +48,7 @@ Copy this block when adding a new issue:
## #213 — Retail client commands were sent to ACE as chat text
**Status:** IN-PROGRESS — implementation complete 2026-07-13, pending live gate
**Status:** IN-PROGRESS — full named-retail command-family port complete 2026-07-13, pending live gate
**Severity:** MEDIUM
**Component:** retained UI / chat commands / net
@ -63,23 +63,29 @@ as Talk text.
**Resolution:** Added an immutable named-retail client-command catalog and
three explicit routes: typed retail client action, ACE server command, and
ordinary chat. `/lifestone`, `/lif`, and `/ls` now publish
`ExecuteClientCommandCmd`; the App-layer `ClientCommandController` invokes
`WorldSession.SendTeleportToLifestone`, which sends retail game action `0x0063`.
Unknown `/` commands publish `SendServerCommandCmd` in canonical `@` form, so
commands such as `/ci` continue to reach ACE. Both chat backends use the same
router.
ordinary chat. The catalog now owns recall/house/PK travel, age/birth,
framerate/lock/version/location/corpse/die, clear and named/automatic UI
layouts, AFK/consent, emotes, friends, squelch/filter/message types, and
fill-components. `ClientCommandController` keeps the panel layer independent
of App/network services; `WorldSession` sends the exact game-action or control
message for server-backed families. Friends and squelch databases are parsed
into authoritative Core state, confirmation requests use the imported retail
dialog, and unknown `/` commands still publish `SendServerCommandCmd` in
canonical `@` form so ACE administrator commands continue to work. Both chat
backends use the same router.
**Research:**
`docs/research/2026-07-13-retail-client-command-routing-pseudocode.md`;
`docs/research/2026-07-13-retail-client-command-families-pseudocode.md`;
`ClientCommunicationSystem::OnChatCommand @ 0x00581320`;
`ClientCommunicationSystem::DoLifestone @ 0x0056FC70`;
`CM_Character::Event_TeleToLifestone @ 0x006A1B90`.
**Acceptance:** In the connected Release client, `/ls`, `/lif`, and
`/lifestone` begin recall to the bound lifestone without appearing as speech or
an ACE unknown-command error. `/ls now` shows local usage and sends nothing.
An ACE command such as `/ci 629 1` still works.
**Acceptance:** In the connected Release client, representative local and
server-backed commands execute without appearing as speech: `/loc`,
`/framerate`, `/saveui test`, `/loadui test`, `/age`, `/friends`, `/afk on`,
and `/marketplace`. `/ls now` shows local usage and sends nothing. An ACE
command such as `/ci 629 1` still works.
---
@ -5132,18 +5138,20 @@ rendering.
---
## #L.6 — UI layout save/load (saveui / loadui / lockui)
## #L.6 — [DONE 2026-07-13] UI layout save/load (saveui / loadui / lockui)
**Status:** OPEN
**Status:** DONE
**Severity:** LOW
**Filed:** 2026-04-26 (deferred from Phase K)
**Component:** ui
**Description:** Retail had `@saveui <name>`, `@loadui <name>`,
`@lockui` commands for persisting ImGui-style window layouts. ImGui
has built-in `LoadIniSettingsFromMemory` /
`SaveIniSettingsToMemory` — wire these to per-named-layout files,
plus chat-command parsing for the `@` prefixes.
**Resolution:** The retained production UI now implements `@saveui [name]`,
`@loadui [name]`, `@saveautoui`, `@loadautoui`, and `@lockui`. Named profiles
persist all mounted retail windows independently of character/resolution;
automatic profiles continue to use the existing character-and-resolution
layout store. Both `/` and `@` prefixes route through the shared typed retail
command catalog. The JSON storage remains the documented IA-15 modern
adaptation of retail's `UIElement_Position` serialization.
---

View file

@ -54,7 +54,7 @@ accepted-divergence entries (#96, #49, #50).
| IA-12 | UI toolkit mirrors retail behavior from research docs, not a byte-port — keystone.dll is outside decomp coverage; observed constants embedded (drag 3 px, tooltip 1000 ms) | `src/AcDream.App/UI/README.md:3` (window mgr: `UiRoot.cs` RegisterWindow/Show/Hide/Toggle/BringToFront — F12 inventory toggle IS retail-faithful) | keystone.dll has no PDB/decomp; semantics reconstructed from the six `docs/research/retail-ui/` deep-dives, keeping retail's event-type constants so panel switch-cases transplant cleanly | Edge-case input semantics the research under-specified (drag threshold, tooltip timing, focus hand-off, capture corners) differ silently with no oracle to diff against | keystone.dll Device DAT_00837ff4; docs/research/retail-ui/04-input-events.md |
| IA-13 | GameEventType registry deliberately omits event types retail ignores; unknown events fall through unhandled | `src/AcDream.Core.Net/Messages/GameEventType.cs:11` | Retail also ignores them — dropping matches retail by construction | If the "retail ignores X" judgment is wrong for any opcode (or a server mod uses one), the event is silently dropped with no diagnostic pointing at the omission | retail GameEvent dispatch (ignored-event set) |
| IA-14 | Rendering + dat-handling base is WorldBuilder's tested port, not a fresh retail-decomp port (Phase N.4/O design stance) | `docs/architecture/worldbuilder-inventory.md` (code at `src/AcDream.{Core,App}/Rendering/Wb/`) | WB visually verified on the AC world, MIT, same stack; known WB↔retail deltas resolved case-by-case — terrain split kept retail `FSplitNESW` (**#51**, pinned by `SplitFormulaDivergenceTest`), scenery drift accepted (AP-31) | A WB-upstream divergence not yet caught ships silently as "our" behavior; guard = the inventory doc's 🟢/🔴 split + per-formula divergence tests | retail decomp per algorithm; `tests/.../SplitFormulaDivergenceTest.cs` |
| IA-15 | D.2b gameplay UI is our own `UiHost`/`UiRoot` retained tree, not a byte-port of Keystone. `RetailUiRuntime` owns the production import/mount graph; `RetailWindowManager`/typed handles centralize registry, raise, focus/capture cleanup, lifecycle events, reverse-order grouped controller teardown, removable Silk input subscriptions, and schema-v2 per-character/per-resolution layout persistence; `RetailWindowFrame` is the single production/Studio mount contract for imported-chrome and shared-wrapper windows. Production LayoutDesc imports include vitals `0x2100006C`, chat `0x21000006`, toolbar `0x21000016`, character `0x2100002E`, inventory `0x21000023` plus mounted `0x21000024/22/21`, and radar `0x21000074`. | `src/AcDream.App/UI/RetailUiRuntime.cs`; `src/AcDream.App/UI/RetailWindowManager.cs`; `src/AcDream.App/UI/RetainedPanelControllerGroup.cs`; `src/AcDream.App/UI/UiHost.cs`; `src/AcDream.App/UI/RetailWindowLayoutPersistence.cs`; `src/AcDream.App/UI/Layout/RetailWindowFrame.cs`; `src/AcDream.App/UI/Layout/LayoutImporter.cs`; binding supply in `GameWindow.cs` | Keystone has no matching PDB/decomp, so we preserve its observable ElementDesc/state/input behavior from DAT, named client call sites, and live evidence while using modern retained ownership. Real RenderSurfaces and imported element geometry remain the visual oracle. | Persistence is behaviorally reconstructed from `saveui/loadui` semantics rather than Keystone internals; lifecycle edge cases remain constrained by conformance tests instead of a byte-port | Production LayoutDesc objects; `docs/research/2026-06-15-layoutdesc-format.md`; Keystone behavior notes in `docs/research/retail-ui/` |
| IA-15 | D.2b gameplay UI is our own `UiHost`/`UiRoot` retained tree, not a byte-port of Keystone. `RetailUiRuntime` owns the production import/mount graph; `RetailWindowManager`/typed handles centralize registry, raise, focus/capture cleanup, lifecycle events, reverse-order grouped controller teardown, removable Silk input subscriptions, schema-v2 per-character/per-resolution automatic layouts, and portable named `saveui/loadui` profiles; `RetailWindowFrame` is the single production/Studio mount contract for imported-chrome and shared-wrapper windows. Production LayoutDesc imports include vitals `0x2100006C`, chat `0x21000006`, toolbar `0x21000016`, character `0x2100002E`, inventory `0x21000023` plus mounted `0x21000024/22/21`, confirmation dialogs `0x2100003C`, and radar `0x21000074`. | `src/AcDream.App/UI/RetailUiRuntime.cs`; `src/AcDream.App/UI/RetailWindowManager.cs`; `src/AcDream.App/UI/RetainedPanelControllerGroup.cs`; `src/AcDream.App/UI/UiHost.cs`; `src/AcDream.App/UI/RetailWindowLayoutPersistence.cs`; `src/AcDream.App/UI/Layout/RetailWindowFrame.cs`; `src/AcDream.App/UI/Layout/RetailConfirmationDialogService.cs`; `src/AcDream.App/UI/Layout/LayoutImporter.cs`; binding supply in `GameWindow.cs` | Keystone has no matching PDB/decomp, so we preserve its observable ElementDesc/state/input behavior from DAT, named client call sites, and live evidence while using modern retained ownership. Real RenderSurfaces and imported element geometry remain the visual oracle. | Persistence is behaviorally reconstructed from `saveui/loadui` semantics rather than Keystone internals; lifecycle edge cases remain constrained by conformance tests instead of a byte-port | Production LayoutDesc objects; `docs/research/2026-06-15-layoutdesc-format.md`; Keystone behavior notes in `docs/research/retail-ui/` |
| IA-17 | Toolbar chrome is toolkit-supplied through the central `RetailWindowFrame` mount (`UiCollapsibleFrame` 8-piece bevel) because LayoutDesc `0x21000016` carries no baked frame. It also supports a toolkit-defined collapse-to-one-row (bottom-edge resize snapping between a row-1-only and a two-row height, row-2 visibility tied to the stop) — retail's real collapse is keystone.dll (no decomp) and the DAT stacks both rows always. | `src/AcDream.App/UI/Layout/RetailWindowFrame.cs`; `src/AcDream.App/UI/UiCollapsibleFrame.cs`; toolbar policy in `GameWindow.cs`; spec: `docs/superpowers/specs/2026-06-20-d2b-toolbar-collapse-design.md` | The central mount now owns wrapper geometry/registration uniformly; border-over-content prevents the row-2 right cap from poking through | The collapse stops remain a toolkit reconstruction rather than a byte-port of Keystone behavior | gmToolbarUI WM chrome (keystone.dll, no PDB); no bevel ids in LayoutDesc 0x21000016 (toolbar dump) |
| IA-18 | Effect overlay tile (enum 0x10000005) is a `ReplaceColor` SURFACE SOURCE — pure-white pixels in the composited drag icon are replaced PER-PIXEL with the same (x,y) pixel of the effect tile (the SURFACE overload `SurfaceWindow::ReplaceColor` 0x004415b0), preserving the tile's texture/gradient; the tile itself is NOT blitted as an additional layer. This IS faithful retail behavior. **Anti-regression: do NOT re-implement this as a blit layer NOR as a flat-color replace (it is a per-pixel surface copy).** | `src/AcDream.App/UI/IconComposer.cs` (`ReplaceWhiteFromSurface`) | Faithful port of `IconData::RenderIcons` @407614 → the SURFACE overload `ReplaceColor` 0x004415b0 (`dst[x,y]=src[x,y]` where `dst==white`); confirmed via clean Ghidra decompile + named decomp + visual (the Energy Crystal's blue is a gradient, 2026-06-17). | A blit-layer or flat-color re-implementation would show the wrong effect look (no gradient) — the visual-verification regression that retired the mean-color approximation | `IconData::RenderIcons` acclient_2013_pseudo_c.txt:407524; `ReplaceColor` SURFACE overload 0x004415b0:71656; `docs/research/2026-06-17-stateful-icon-RESOLVED.md` |
| IA-19 | Automatic combat acquisition is narrowed to attackable non-player monsters. Retail `AutoTarget` falls back to `SelectNext(SELECTION_TYPE_COMPASS_ITEM)`, whose combat filter can also admit attackable enemy players in compatible PK states. | `src/AcDream.Core/Combat/CombatTargetPolicy.cs`; consumer `src/AcDream.App/Rendering/GameWindow.cs` | Explicit product direction: Auto Target must never select NPCs, players, pets, or other objects; manual player-selection commands remain available | In PK play, Auto Target will not acquire an otherwise valid hostile player as retail would; the player must be selected manually | `ClientCombatSystem::AutoTarget @ 0x0056BC80`; `CPlayerSystem::SelectNext @ 0x0055F9A0`; `ClientCombatSystem::ObjectIsAttackable @ 0x0056A600` |
@ -207,7 +207,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
| ~~AP-99~~ | **RETIRED 2026-07-11 (Wave 3.2)** — Core `ItemInteractionPolicy` ports the complete ordered `DetermineUseResult`/`UseObject`/`AttemptPlaceIn3D` matrix; PWD flags and `CombatUse` survive CreateObject; component-pack membership comes from portal.dat; App owns throttle, UseDone-balanced busy state, target mode, wire/optimistic dispatch, and typed confirmation/auxiliary seams. The two corrupt decompiler operands were pinned from matching x86 as `BF_REQUIRES_PACKSLOT` and `BF_VENDOR`. | `src/AcDream.Core/Items/ItemInteractionPolicy.cs`; `src/AcDream.App/UI/ItemInteractionController.cs` | — | — | `ItemHolder::DetermineUseResult @ 0x00588460`; `UseObject @ 0x00588A80`; `AttemptPlaceIn3D @ 0x00588600` |
| ~~AP-101~~ | **RETIRED 2026-07-11 (Wave 4.4e)** — toolbar control coverage is complete: DAT panel launchers, combat, Use/Examine, selected-object strip, shortcuts, and exact thrown-weapon/separate-ammo count resolution on authored element `0x10000194`. | `src/AcDream.Core/Items/ToolbarAmmoPolicy.cs`; `src/AcDream.App/UI/Layout/ToolbarController.cs` | — | — | `gmToolbarUI::UpdateAmmoID @ 0x004BF210`; `UpdateAmmoNumber @ 0x004BE9E0` |
| AP-104 | Vitals detail element `0x100004A9` and root `HideDetail`/`ShowDetail` transitions are not wired | `src/AcDream.App/UI/Layout/VitalsController.cs` | Compact vitals values/bars are correct | Detail click does nothing and expanded retail state is unreachable | `gmVitalsUI::ListenToElementMessage @ 0x004BFC00`; `PostInit @ 0x004BFCE0` |
| AP-105 | **PARTIAL 2026-07-10 (Wave 2.2)** — inherited scrollbar media/roles now come from DAT (decrement/top `0x06004C69`, increment/bottom `0x06004C6C`). Retained chat still lacks complete tab/filter/unread, social availability, squelch, and focus-opacity behavior; a second ChatVM still loses FPS/position providers. | `src/AcDream.App/UI/Layout/DatWidgetFactory.cs`; `ChatWindowController.cs`; chat mount in `GameWindow.cs` | Shared log/send path, wrapping, scrollbar roles, and outer maximize geometry work; Wave 5 consolidates remaining chat state | Commands degrade, tabs are no-ops, moderation/channel state is wrong, and focus visuals diverge | `gmMainChatUI @ 0x004CCCC0..0x004CE2A0`; `UIElement_Scrollbar::OnSetAttribute @ 0x004714D0`; `ChatInterface` methods |
| AP-105 | **PARTIAL 2026-07-13** — inherited scrollbar media/roles now come from DAT (decrement/top `0x06004C69`, increment/bottom `0x06004C6C`), and both chat backends share typed client-command routing plus one retained `ChatVM` for reply state. Retained chat still lacks complete tab/filter/unread, social availability, incoming squelch enforcement, and focus-opacity behavior. | `src/AcDream.App/UI/Layout/DatWidgetFactory.cs`; `ChatWindowController.cs`; `src/AcDream.App/UI/ClientCommandController.cs`; chat mount in `GameWindow.cs` | Shared log/send path, wrapping, scrollbar roles, command ownership, and outer maximize geometry work; later chat work consolidates the remaining presentation/filter state | Tabs are no-ops, squelched lines can still render, contextual social actions are absent, and focus visuals diverge | `gmMainChatUI @ 0x004CCCC0..0x004CE2A0`; `UIElement_Scrollbar::OnSetAttribute @ 0x004714D0`; `ChatInterface` methods |
| AP-106 | The retained UI has no external/ground-container window lifecycle, while the original owned-side-bag `NoLongerViewingContents` premise was incorrect (#196) | `src/AcDream.App/UI`; window runtime; absent external-container controller | Owned inventory navigation remains usable and must not gain an unproven `0x0195` send; Wave 2/6 adds lifecycle ownership and the separate external surface | External container replacement/close cannot notify the server exactly once; adding the packet to owned bag close would itself diverge from the named retail call graph | `ClientUISystem.groundObject`; `CM_Inventory::Event_NoLongerViewingContents @ 0x006ABC50`; `docs/research/2026-07-10-retail-panel-behavior-pseudocode.md` |
| ~~AP-107~~ | **RETIRED 2026-07-11 (Wave 3.3 / #197)** — typed `OfferPrimaryClick` returns `NotActive`, `ConsumedSuccess`, or `ConsumedRejected`; every retained item surface plus radar/world offers active target mode before local selection/open/use fallback. Rejections are consumed and cannot drift selection. | `src/AcDream.App/UI/ItemInteractionController.cs`; inventory/paperdoll/toolbar/radar/world call sites | — | — | `UIElement_ItemList::HandleTargetedUseLeftClick @ 0x004E24D0` |
| AP-108 | Paperdoll/AutoWield still omit full `AutoWieldIsLegal`/dual-wield rules, double-click examine/drag from the doll, and body-part selection lighting. **Aetheria retired from this row 2026-07-13:** all three sigil slots use exact backgrounds/equip masks and live `PropertyInt.AetheriaBitfield` visibility. | `src/AcDream.App/UI/Layout/PaperdollController.cs`; `src/AcDream.App/UI/AutoWieldController.cs` | Basic equip slots, Aetheria, and live doll work; inventory double-click faithfully sequences the primary weapon, incompatible shield, and mismatched ammo blockers through server-confirmed dequip→wield in both peace and war | Remaining illegal/off-hand cases, doll examine/drag, and selection lighting still differ functionally | `CPlayerSystem::AutoWield @ 0x00560A60`; `ACCWeenieObject::BlocksUseOfShield @ 0x0055D3E0`; `gmPaperDollUI @ 0x004A3590..0x004A5F90` |
@ -249,7 +249,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
| TS-28 | LoginComplete sent on PlayerCreate (0xF746) arrival; retail sends it after the portal-space transition animation finishes (no such animation exists yet) | `src/AcDream.Core.Net/Messages/GameActionLoginComplete.cs:30` | acdream has no portal-space animation; "InWorld" phrasing in the file is slightly stale (trigger is PlayerCreate) | Server flips the character out of the loading state and pushes initial updates while the client may still be streaming — server logic assuming retail's load-screen duration fires against a half-initialized client | retail post-EnterWorld flow (holtburger messages.rs:391-422) |
| TS-29 | Background music (MIDI) + ambient loops not ported: PlayMusic/StopMusic no-op; StartAmbient reserves a handle that never plays | `src/AcDream.App/Audio/OpenAlAudioEngine.cs:331` | Explicitly outside R5 audio-phase scope; a landblock-attached ambient system is planned separately | Silent world where retail has music/atmosphere; code trusting StartAmbient's handle to mean "playing" is already subtly wrong (StopAmbient looks up a never-created source) | retail MIDI + ambient system (r05) |
| TS-30 | Chat DAT elements `0x10000522``0x10000525` render but have no controller semantics; the older claim that they are numbered in-window filter tabs is **unproven** | `src/AcDream.App/UI/Layout/ChatWindowController.cs` | Named retail proves separately filtered main/floaty chat windows, not an in-window numbered-tab model. Wave 5 must live/DAT-confirm these element roles before assigning behavior | The controls may be inert today, but inventing tab switching could be a larger divergence than leaving an unconfirmed role inactive | `gmMainChatUI @ 0x004CCCC0..0x004CE2A0`; correction in `docs/research/2026-07-10-retail-panel-behavior-pseudocode.md` |
| TS-31 | Squelch toggle absent (no `/squelch` slash command, no clickable name-tags to silence); retail's squelch list filters incoming chat lines | `src/AcDream.Core/Chat/ChatLog.cs` | Squelch is a social / moderation feature deferred to post-M1.5; the data structure (`ChatLog`) has no squelch set today | Any player can spam all clients; clickable-name-tag contextual menu (used in retail to squelch, tell, add-to-friends) is absent | `ChatFilter::IsSquelched`; retail right-click player name → Squelch menu |
| TS-31 | **NARROWED 2026-07-13**`/squelch`, `/unsquelch`, `/filter`, `/unfilter`, and `/messagetypes` send the exact modification events and consume the authoritative retail `SquelchDB`; incoming `ChatLog` lines are not yet filtered through that database, and clickable name-tag social actions remain absent | `src/AcDream.Core/Social/SquelchState.cs`; `src/AcDream.Core.Net/Messages/SocialStateMessages.cs`; `src/AcDream.App/UI/ClientCommandController.cs`; `src/AcDream.Core/Chat/ChatLog.cs` | Command/state transport is complete; enforcement belongs at the shared inbound-chat boundary so both backends remain identical | A squelch appears in the list and persists server-side but matching incoming lines can still render; contextual name actions remain unavailable | `SquelchDB::UnPack @ 0x006B1900`; `ChatFilter::IsSquelched`; retail right-click player name → Squelch menu |
| TS-32 | `ClientObjectTable` has no pre-queue for a child `CreateObject` that arrives before its parent (out-of-order PARENTED create); such objects are ingested as root objects and their `ContainerId` links a not-yet-known container. Retail's `null_object_table` + `null_weenie_object_table` hold unresolvable objects until the parent arrives | `src/AcDream.Core/Items/ClientObjectTable.cs` (`Ingest`) | PD↔`CreateObject` ordering is handled (upsert semantics); out-of-order PARENTED creates are observed only at high packet loss or in vendor/corpse multi-object bursts on non-loopback links; deferred to D.5.5+ | A container's child object arriving before the container is ingested as a root item — it won't appear in `GetContents` until the next `RecordMembership` or a move event corrects the parent link | `CObjectMaint::null_object_table` / `null_weenie_object_table` (acclient.h / named-retail pc) |
| TS-33 | Outbound position-send tracker over-stamped after MoveToState: `NotePositionSent` writes last-sent position + cell + contact-plane after BOTH the MTS and AP sends, but retail's `SendMovementEvent` (0x006b4680) stamps ONLY `last_sent_position_time` after an MTS — only `SendPositionEvent` (0x006b4770, the AP path) stamps all three. Broader: acdream gates APs on a plain interval (`HeartbeatDue`) where retail uses `ShouldSendPositionEvent` (0x006b45e0 — interval OR cell-change OR contact-plane-change), AND acdream's frame-changed diff compares POSITION only (`ApproxPositionEqual`) where retail's `Frame::is_equal` compares the full frame incl. ORIENTATION — a stationary heading change (R4-V5: the MoveToManager's `HandleTurnToHeading` arrival snap, `set_heading(send:true)`) never triggers an AP, so the server keeps the stale facing until the player next moves. Masked against ACE (ACE rotates server-side on its own mt-8/9 / close-range-use paths and broadcasts the result) | `src/AcDream.App/Rendering/GameWindow.cs:8331` (MTS `NotePositionSent`); `src/AcDream.App/Input/PlayerMovementController.cs` (`ApproxPositionEqual` + the heartbeat diff); the player MoveToManager `setHeading` seam in `EnterPlayerModeNow` (the `send` flag's would-be consumer) | D5 audit-only in the L.2b wire-parity slice; the full cadence port (`ShouldSendPositionEvent` gate + split MTS/AP stamping + full-frame `Frame::is_equal` diff) is a dedicated follow-up slice (R7 outbound) | AP heartbeat cadence diverges from retail — acdream may suppress or reorder autonomous-position sends differently after movement-state sends, and a stationary server-commanded turn leaves observers with stale facing until the next movement; on a real network this shifts the position-correction rhythm | `CommandInterpreter::SendMovementEvent` 0x006b4680, `SendPositionEvent` 0x006b4770, `ShouldSendPositionEvent` 0x006b45e0, `Frame::is_equal` (pc:700263) |
| TS-40 | Retail's `physics_obj->cell` ("placed in the world") is proxied by the explicit `PhysicsBody.InWorld` flag — set by `SnapToCell` (local player placement) and `RemoteMotion` construction (remotes exist only for world entities); consumed by `CMotionInterp`'s detached-object link-strip guards (`if (cell == 0) RemoveLinkAnimations`, raw @305627). Replaces the UNREGISTERED `CellPosition.ObjCellId == 0` proxy, which only the local player ever seeded (#145 `SnapToCell`), so every REMOTE body read "detached" and every dispatched transition link (door swings, remote walk↔run links) was stripped the same tick it was appended — the 2026-07-03 door-snap bug | `src/AcDream.Core/Physics/PhysicsBody.cs` (`InWorld`); `src/AcDream.Core/Physics/MotionInterpreter.cs` (3 guard sites) | acdream has no per-body CObjCell pointer; a boolean placement flag carries exactly the guard's retail meaning until cell-pointer plumbing exists | A body used without either placement path (a future entity class constructing bodies directly) reads detached and loses transition links until its creation site sets the flag | `CMotionInterp::DoInterpretedMotion` 0x00528360 tail @305627; `CPhysicsObj::RemoveLinkAnimations` |
@ -262,7 +262,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
| TS-46 | Player/remote collision spheres are passed as TWO SCALARS (radius, capsule-top height) and reconstructed by `SpherePath.InitPath` (foot center at `radius`, head center at `height radius`) — retail passes the Setup's SPHERE LIST verbatim (`CPhysicsObj::transition` 0x00512dc0 → `init_sphere(GetNumSphere, GetSphere, m_scale)`, ≤2 spheres, each origin AND radius × m_scale). With the corrected callers (0.48, 1.835 = Setup.Height) the reconstruction sits 5 mm off the dat: foot center 0.480 vs dat 0.475, head center 1.355 vs dat 1.350 (human Setup 0x02000001). **#184 Slice 3 (2026-07-07) NARROWED this: the remote de-overlap sweep now derives its scalars from the creature's OWN Setup (`GetSetupCylinder` = `setup.Radius`/`setup.Height` × ObjScale) — remotes NO LONGER use human dims regardless of Setup/scale.** RESIDUAL: (a) it is still the two-SCALAR reconstruction, not retail's ≤2-sphere LIST (lossy for creatures whose foot/head spheres differ), for both player and remotes; (b) the remote sweep's `stepUpHeight`/`stepDownHeight` stay a hardcoded 0.4 m, where retail derives them from `setup->step_up_height`/`step_down_height` (0x005180d0/0x005180f0, 0.04 m fallback, `radius×0.5` clamp) — an adjacent non-Setup divergence left for a later slice. (The pre-2026-07-06 value 1.2f put the head TOP at 1.2 m — the #137 window climb; fixed same day.) | `src/AcDream.Core/Physics/TransitionTypes.cs` (`InitPath`); `src/AcDream.App/Input/PlayerMovementController.cs` (player, human — correct as-is); `src/AcDream.App/Rendering/GameWindow.cs` (remote sweep now `GetSetupCylinder`-fed with a shapeless→human fallback) | The scalar API predates the Setup ingestion; 5 mm is below the visual/feel threshold; the remote scalars are now the creature's real (radius,height)×ObjScale, consistent with the shadow registration's entScale and the moveto/sticky radii | Marginal rε/r+ε grazes still flip on the 5 mm scalar offset; a creature whose head sphere is wider than its foot de-overlaps by the single (radius) approximation, not the true 2-sphere profile; the 0.4 m step heights are non-Setup for all movers | `CPhysicsObj::transition` 0x00512dc0; `SPHEREPATH::init_sphere` 0x0050c670 (≤2, ×m_scale); `set_description` 0x00514f40 (m_scale from wire ObjScale); retire by plumbing the full Setup sphere list into `InitPath` |
| ~~TS-45~~ | **RETIRED 2026-07-07** — the hand-rolled `SphereCollision` (forced `combinedR+1 cm` radial de-penetration + leaked `SetSlidingNormal` + always-Slid, head-sphere ignored) is REPLACED by the faithful `CSphere::intersects_sphere` family port (branch dispatcher 0x00537A80 + `step_sphere_up`/`slide_sphere`/`land_on_sphere`/`collide_with_point`/`step_sphere_down`), routing the grounded slide through the shared crease `SlideSphere` (0x00537440). Humanoid creatures collide via body Spheres, so this was the player-vs-monster crowd path; the radial de-penetration was the "can't wiggle free in a packed crowd" wedge. `SphereCollisionFamilyTests` (slide-around, block, ethereal) + `docs/research/2026-07-07-csphere-collision-family-pseudocode.md`. Residual `AP-84` (PerfectClip TOI dead in M1.5). | — | — | — | `CSphere::intersects_sphere` 0x00537A80 (pc:321678) |
| TS-43 | Remote teleport has no `teleport_hook` equivalent: retail tears down the position managers on every teleport (`CPhysicsObj::teleport_hook` 0x00514ed0 — `CancelMoveTo(0x3c)` @0x00514edf, `PositionManager::UnStick` @0x00514eee, `StopInterpolating`/`UnConstrain`); acdream's remote teleport is a bare UP hard-snap, so a stuck/chasing remote that the server teleports keeps its stick/moveto for up to the 1 s sticky lease / next UM. The LOCAL player side IS wired (R5-V3: `PlayerMovementController.SetPosition``PositionManager.UnStick`; the moveto cancel was already there via `StopCompletely`; the teleport-arrival site also fires the hook's tail — `EntityPhysicsHost.NotifyTeleported` = `TargetManager::ClearTarget` + `NotifyVoyeurOfEvent(Teleported)` @0x00514f1b-0x00514f28, which is what makes mobs stuck to the player drop their sticks on a recall) | `src/AcDream.App/Rendering/GameWindow.cs` (`OnLivePositionUpdated` — no teleport-flag manager teardown for remotes) | Remote teleports are rare (recalls/summons); the sticky 1 s lease + UP hard-snaps self-correct within a second; wiring it properly wants the UP teleport-stamp plumbing (TS-26's stamp work) | A teleported-away attacker briefly steers toward its pre-teleport target from the new location (≤1 s) before the lease/next-UM corrects it | `CPhysicsObj::teleport_hook` 0x00514ed0; retire with the TS-26 UP-stamp port |
| TS-47 | The typed retail client-command catalog currently ports only `lifestone`/`lif`/`ls`; all other retail-owned command verbs fall through to ACE as server commands until ported one family at a time | `src/AcDream.UI.Abstractions/Panels/Chat/RetailClientCommandCatalog.cs` | The catalog/router/controller architecture is the first vertical slice; each remaining family needs its own named-decomp pseudocode, typed action, and conformance tests rather than a guessed bulk alias list | Retail commands such as `/marketplace`, `/house`, or `/friends` may produce ACE unknown-command output or server-specific behavior instead of the retail client action | `ClientCommunicationSystem` command-table construction around `0x00581A40..0x005850A0`; `docs/research/2026-07-13-retail-client-command-routing-pseudocode.md` |
| TS-47 | **NARROWED 2026-07-13** — typed routing now ports the named-retail recall/house/PK travel, age/birth, local display/location, UI persistence, AFK/consent, emote, friends, squelch/filter, and fill-components families. Retail-owned verbs outside the researched family set still fall through to ACE until individually verified. | `src/AcDream.UI.Abstractions/Panels/Chat/RetailClientCommandCatalog.cs`; `src/AcDream.App/UI/ClientCommandController.cs`; `src/AcDream.Core.Net/Messages/ClientCommandRequests.cs` | The high-use researched families have decomp pseudocode, typed actions, and conformance tests; unresearched registry entries must follow the same evidence-first path | An unported retail-owned verb can still produce ACE unknown-command output or server-specific behavior instead of its client action | `ClientCommunicationSystem` command-table construction around `0x00581A40..0x005850A0`; `docs/research/2026-07-13-retail-client-command-routing-pseudocode.md`; `docs/research/2026-07-13-retail-client-command-families-pseudocode.md` |
---

View file

@ -501,6 +501,7 @@ behavior. Estimated 1726 days focused work, 35 weeks calendar.
- **M2 held-object parenting shipped and live-gated 2026-07-11.** The combat toggle now ports `GetDefaultCombatMode` over ordered equipped contents, so a bow requests Missile instead of the old hardcoded Melee. CreateObject preserves parent/placement/timestamp fields, `0xF749` ParentEvent is handled, and `EquippedChildRenderController` renders the weapon as a separate child composed from the animated hand part + holding frame + child placement frame. Live gate passed: bow selected missile stance, rendered in-hand, followed animation, unequipped cleanly, and melee remained correct. App Release builds with zero warnings; the full 4,765-pass / 5-skip suite is green. AP-111 is retired; research: `docs/research/2026-07-11-combat-default-and-parent-event-pseudocode.md`.
- **M2 local attack receive funnel implemented 2026-07-11; live gate pending.** Retail `ExecuteAttack` only sends the request; ACE chooses the concrete melee/missile action and returns it in a non-autonomous mt-0 `UpdateMotion`. The local branch now runs that state through the same constructor-defaulted `MoveToInterpretedState` funnel and 15-bit action-stamp gate as remotes, then applies sticky/long-jump tails. The old local-only direct `Commands[]` replay is deleted. Shared conversion lives in `InboundInterpretedMotionFactory`; research: `docs/research/2026-07-11-local-combat-motion-pseudocode.md`.
- **M2 basic retail combat bar implemented 2026-07-11; corrective live visual gate pending.** Production mounts authored `gmCombatUI` LayoutDesc `0x21000073`, shows it only for Melee/Missile, and routes mouse plus keyboard through one `CombatAttackController`. Corrections include the authored wider centered dark-red child range (accepted IA-20), full-width left-to-right bright live attack-charge feedback, exact Speed-left/Power-right justification, silent control-only `AttackDone(ActionCancelled)`, target-frame Keep in View with manual orbit, and persistent corpse motion: the AP-80 velocity-only NPC adaptation can now replace only Ready/Walk/Run, never authoritative Dead/actions. `CombatTargetController` ports the selection-cleared AutoTarget consumer, so a selected creature's authoritative Dead motion clears it and selects the nearest eligible creature when enabled. The 2026-07-12 replacement-corpse correction unifies both multi-frame and static/reactive spawns behind retail's CreateObject lifecycle: apply the wire's Dead state while detached, then `MotionTableManager::HandleEnterWorld` strips Ready→Dead links before the first in-world tick; multiple corpses remaining fallen passed the live user gate that day. Follow-up #205 makes the toolbar read the final canonical selection after a reentrant Auto Target notice and restricts automatic candidates to hostile non-player monsters (intentional PK-edge divergence IA-19); that live gate also passed 2026-07-12. Research: `docs/research/2026-07-11-retail-combat-bar-pseudocode.md`, `docs/research/2026-07-11-combat-target-camera-pseudocode.md`, `docs/research/2026-07-12-death-and-auto-target-pseudocode.md`; AP-24/AP-95 retired, AP-80 narrowed, AP-110 narrowed, AP-112 records the remaining attack-start and exact trained-Recklessness seams.
- **Retail client command families implemented 2026-07-13; live gate pending.** One shared typed catalog now separates retail client actions from ACE administrator commands for both chat backends. Named-decomp ports cover recall/house/PK travel; age/birth; framerate, lock, version, location, corpse, and die confirmation; clear plus named/automatic UI layouts; AFK/consent; emotes; friends; squelch/filter/message types; and fill-components. App owns execution, Core owns authoritative friends/squelch state, Core.Net owns exact UIQueue/ControlQueue packets, and confirmation requests mount imported LayoutDesc `0x2100003C`. #L.6 is closed; TS-31/TS-47 are narrowed. Research: `docs/research/2026-07-13-retail-client-command-families-pseudocode.md`.
- **✓ SHIPPED — Character window** (`LayoutDesc 0x2100002E`, `CharacterStatController`, 2026-06-26, same branch). **Visually user-confirmed 2026-06-26 — Attributes tab reads as retail.** Three tabs, header (name/heritage/PK), large-gold level number (dat font, `largeDatFont` 18px), "Total Experience (XP):" + "XP for next level:" captions, 9-row attribute list (icons + right-aligned values + Health/Stamina/Mana vitals), click-to-select (top/bottom selection bars + footer State-B "{Attr}: {value}" / "Experience To Raise: Infinity!" + affordability-gated raise triangles), centered footer. User noted "still needs some polish for later" — deferred to Issue #158.
- **✓ SHIPPED — D.5.4 — Client object/item data model (foundation).** Shipped 2026-06-18 (`b506f53`..`a33e897`, 11 commits). Renamed `ItemRepository``ClientObjectTable` / `ItemInstance``ClientObject`; broadened the table to hold EVERY server object (retail `weenie_object_table` shape). `CreateObject` is now the canonical merge-upsert (`ClientObjectTable.Ingest`, retail `SetWeenieDesc` semantics) via a new Core.Net `ObjectTableWiring` (off GameWindow); `DeleteObject` evicts; `PlayerDescription` is a membership manifest (`RecordMembership`); live container-membership index (`GetContents`, retail `object_inventory_table`). `_liveEntityInfoByGuid` retired (selection/describe resolve from the one table). Root fix: the old enrich-existing-only `EnrichItem` dropped `CreateObject`s for items with no `PlayerDescription` stub — live-Coldeve 4/6 hotbar slots blank; items are now created, not dropped. **Crux resolved:** retail is TWO tables (`object_table` + `weenie_object_table`), NOT one — acdream's `WorldEntity` (3D system) + `ClientObjectTable` (data/UI) split was already architecturally faithful; the fix was the ingestion path, not a table unification. 2671 tests green.
- **Roadmap correction (2026-07-10):** the completion order is now the architecture-first campaign in `docs/superpowers/plans/2026-07-10-retail-ui-fidelity-completion.md`. Retail `gmToolbarUI` is object-only: preserve `ShortCutData.index_`, `objectID_`, and `spellID_`, but do not invent spell glyphs on this bar. `PlayerModule::favorite_spells_[8]` feeds separate spell bars.

View file

@ -484,6 +484,14 @@ include dungeons.
automatic candidates to hostile non-player monsters; live gate passed
2026-07-12. See
`docs/research/2026-07-12-death-and-auto-target-pseudocode.md`.
- **Retail client command families (implemented 2026-07-13; live gate pending)**
both chat surfaces now route a named-decomp command catalog into an App-layer
controller, keeping ACE administrator verbs on the explicit server-command
path. The implemented set covers travel, character/time queries, local UI and
location commands, named/automatic layout persistence, AFK/consent, emotes,
friends, squelch/filter, and fill-components, with exact Core.Net messages and
authoritative social-state parsing. See
`docs/research/2026-07-13-retail-client-command-families-pseudocode.md`.
- **L.1b** — Command router + motion-state cleanup (prereq for L.1c).
**Freeze on landing:**

View file

@ -0,0 +1,343 @@
# Retail client command families
Date: 2026-07-13
## Scope
This note extends the first lifestone vertical slice with the exact command
families approved for porting. The named September 2013 retail client is the
behavioral oracle; ACClientLib is used as an independent wire-name and payload
cross-check.
## Sources
- `ClientCommunicationSystem::DoMarketplace @ 0x0056FCE0`
- `ClientCommunicationSystem::DoLockUI @ 0x005703B0`
- `ClientCommunicationSystem::DoFrameRate @ 0x005707D0`
- `ClientCommunicationSystem::DoPKArena @ 0x005788D0`
- `ClientCommunicationSystem::DoPKLArena @ 0x005789D0`
- `ClientCommunicationSystem::DoAge @ 0x0057C5A0`
- `ClientCommunicationSystem::DoBirth @ 0x0056E5F0`
- `CM_Character::Event_QueryAge @ 0x006A1620`
- `CM_Character::Event_QueryBirth @ 0x006A16F0`
- `CM_Character::Event_TeleToMarketplace @ 0x006A1C20`
- `CM_Character::Event_TeleToPKArena @ 0x006A1CB0`
- `CM_Character::Event_TeleToPKLArena @ 0x006A1D40`
- `CM_House::Event_TeleToHouse_Event @ 0x006AAFA0`
- `CM_House::Event_TeleToMansion_Event @ 0x006AB030`
- `CM_Character::DispatchUI_QueryAgeResponse @ 0x006A2E40`
- `ClientCommunicationSystem::Handle_Character__QueryAgeResponse @ 0x005711D0`
- retail command-table construction at `0x00581900..0x00585100`
Independent protocol cross-check:
- `references/acclientlib/UtilityBelt.Common/MessageTypes.cs`
- `references/acclientlib/UtilityBelt.Common/Enums/Enums.cs`
- `references/acclientlib/UtilityBelt.Scripting/Enums/BusyAction.cs`
- ACE `Source/ACE.Server/Network/GameAction/Actions/` handlers for
`SetAFKMode`, `ModifyCharacterSquelch`, `QueryAge`, `ConfirmationResponse`,
`SetDesiredComponentLevel`, `AddFriend`, and the teleport families
- ACE `GameEventConfirmationRequest`, `GameEventFriendsListUpdate`, and
`Network/Structure/SquelchDB`/`SquelchInfo` writers
ACE's self age/birth handlers describe the trailing four zero bytes as an
empty aligned `String16L`; named retail describes the same self-command bytes
as object id zero. The wire bytes are identical for this path, and the named
retail interpretation remains authoritative.
## Packet pseudocode
All actions use the normal `0xF7B1` game-action envelope and the next UI
sequence number.
```text
send_parameterless_action(opcode):
packet = [0xF7B1, next_sequence(), opcode]
send packet to the weenie server
marketplace(): send_parameterless_action(0x028D)
pk_arena(): send_parameterless_action(0x0027)
pkl_arena(): send_parameterless_action(0x0026)
house_recall(): send_parameterless_action(0x0262)
mansion_recall(): send_parameterless_action(0x0278)
```
```text
send_character_query(opcode, object_id):
packet = [0xF7B1, next_sequence(), opcode, object_id]
send packet to the weenie server
query_age(): send_character_query(0x01C2, selected_player_or_zero)
query_birth(): send_character_query(0x01C4, selected_player_or_zero)
```
For the ordinary self-command, `selected_player_or_zero` is zero. Retail's
privileged PSR path may use the currently selected player; acdream does not
claim that privileged mode.
## Command pseudocode
```text
marketplace(arguments):
if no arguments: send marketplace action
else: display local usage
```
```text
pk_arena(arguments):
if arguments: display local usage
else if local player is not full PK: display retail failure 0x055F
else: send PK-arena action
pkl_arena(arguments):
if arguments: display local usage
else if local player is not PKLite: display retail failure 0x0560
else: send PKLite-arena action
```
Retail derives both eligibility checks from the local player's
`PublicWeenieDesc` bits (`IsPK` / `IsPKLite`), not from a guessed server
capability.
```text
framerate(arguments):
if arguments: display local usage
else:
show_framerate = !show_framerate
notify UI so the display changes immediately
lockui(arguments):
if arguments: display local usage
else:
ui_locked = !ui_locked
broadcast the UI-lock state change
```
```text
query_age_response(name, duration):
if name is empty: display "You have played for {duration}."
else: display "{name} has played for {duration}."
```
The age-response payload is two aligned CP-1252 `String16L` values: target
name (empty for self), then the already-formatted duration. Birth has no
dedicated response event in the retail registry; the server supplies its
result through the existing communication/system-text path.
## Location and corpse pseudocode
Sources:
- `ClientCommunicationSystem::DoLoc @ 0x0057A250`
- `Position::ToString @ 0x005A9330`
- `ClientCommunicationSystem::DoCorpse @ 0x00578220`
- `LandDefs::CellidToCoordinateString @ 0x005A9CB0`
```text
loc(arguments):
if arguments: display local usage
else if player position has no cell: display "Not in valid cell!"
else:
position_text = format(
"0x%08X [%f %f %f] %f %f %f %f",
cell, x, y, z, quaternion_w, quaternion_x,
quaternion_y, quaternion_z)
display "Your location is: {position_text}"
```
```text
corpse(arguments):
// Retail ignores arguments for this command.
last_outside = player quality PositionType.LastOutsideDeath (0x0E)
if absent:
display "We're sorry, but we have no record of your last outside corpse location."
else:
coordinates = cell_id_to_coordinate_string(last_outside.cell)
display "The last time you died outside, your corpse was located at ({coordinates})."
```
The corpse command reads the position already carried by PlayerDescription;
it does not infer the corpse location from a nearby corpse object or the most
recent death chat line.
## Chat-local and UI-layout pseudocode
Sources:
- `ClientCommunicationSystem::DoClear @ 0x0056E600`
- `ClientCommunicationSystem::DoSaveUI @ 0x0056FFF0`
- `ClientCommunicationSystem::DoLoadUI @ 0x00570150`
- `ClientCommunicationSystem::DoSaveAutoUI @ 0x005702B0`
- `ClientCommunicationSystem::DoLoadAutoUI @ 0x00570330`
```text
clear(arguments):
source = current_chat_source
if first argument equals "all", ignoring case: source = all_sources
clear(source)
```
```text
save_ui(arguments):
reject more than one argument with the retail usage message
name = first argument, or the empty/default profile
reject names longer than 16 characters
save every attached window's current placement and visibility to name
load_ui(arguments):
apply the same argument rules as save_ui
restore every attached window from name
save_auto_ui(arguments):
reject arguments
save every attached window under the current character and resolution
load_auto_ui(arguments):
reject arguments
restore every attached window for the current character and resolution
```
The original serializes `UIElement_Position` records. acdream stores the same
observable placement/visibility values in its typed layout store; the storage
format is deliberately modern and remains tracked as divergence IA-15.
## Friends pseudocode
Sources:
- `ClientCommunicationSystem::DoFriends @ 0x0057BC00`
- `ClientCommunicationSystem::DoFriendsAdd @ 0x00578FB0`
- `ClientCommunicationSystem::DoFriendsRemove @ 0x00579080`
- `gmFriendsUI::Request_AddFriend @ 0x0048D240`
- `CM_Login::AddFriend @ 0x006A3020`
- `CM_Login::RemoveFriend @ 0x006A30A0`
- `CM_Login::ClearFriends @ 0x006A3110`
```text
friends(arguments):
no arguments: display all friends
"online": display only online friends
"add <name>": add_friend(name)
"remove <name>": remove_friend(name)
"old": request the legacy server friends listing
otherwise: display "Invalid friends command specified."
add_friend(name):
require a non-empty name
if local list already has 50 entries: display WeenieError 0x0561
else send opcode 0x0018 followed by String16L name
remove_friend(name):
require a non-empty name
if name equals "-all":
send opcode 0x0025 and clear the local list
else if a case-insensitive local match exists:
send opcode 0x0017 followed by the friend's object id
else display WeenieError 0x0563
```
The server's `FriendsListUpdate` event is authoritative. Full updates replace
the list; add/remove/login-state updates mutate it by object id. Display order
is the received retail list order.
## Away, consent, and message-filter pseudocode
Sources:
- `ClientCommunicationSystem::DoAFK @ 0x0057B3F0`
- `ClientCommunicationSystem::DoConsent @ 0x0057DDA0`
- `ClientCommunicationSystem::ProcessSquelchArgs @ 0x00579C30`
- `ClientCommunicationSystem::DoSquelch @ 0x0057BF50`
- `ClientCommunicationSystem::DoUnSquelch @ 0x0057C070`
- `ClientCommunicationSystem::DoFilter @ 0x0057C860`
- `ClientCommunicationSystem::DoUnFilter @ 0x0057C880`
- `ClientCommunicationSystem::PerformGlobalSquelchMod @ 0x0057C2D0`
- `LogTextTypeEnumMapper::IsLegalChannel @ 0x006AFF40`
```text
afk(arguments):
no arguments or "on": send opcode 0x000F with true, unless already away
"off": send opcode 0x000F with false, unless already present
"msg <text>": trim/join text, enforce retail's 191-character limit,
send opcode 0x0010 with String16L text, and echo the new text
otherwise: display AFK usage
```
```text
consent(arguments):
"on" / "off": toggle the local accept-loot-permits option and echo it
"who": send opcode 0x0217
"clear": send opcode 0x0216
"remove <name>": send opcode 0x0218 followed by String16L name
otherwise: display the retail invalid-command text
```
The option is CharacterOption bit `0x00080000`; retail's default mask
`0x50C4A54A` leaves it clear.
```text
parse_squelch(arguments):
default category = All
consume options before the target:
-reply uses the most recent teller
-account chooses account-wide storage
-<message type> chooses a legal LogTextType
remaining joined text is the target name
squelch / unsquelch:
with no arguments: display the authoritative local squelch database
account target: send opcode 0x0059, add/remove flag, String16L name
character target: send opcode 0x0058, add/remove flag, zero guid,
String16L name, category
filter / unfilter:
with no arguments: display global filters
require exactly a legal -<message type> option
send opcode 0x005B, add/remove flag, category
```
Legal retail filter types are Speech, Tell, Combat, Magic, Emote, Appraisal,
Spellcasting, Allegiance, Fellowship, Combat_Enemy, Combat_Self, Recall,
Craft, and Salvaging. The inbound `SetSquelchDB` packet replaces the local
database; command output reads that state rather than inventing client-only
state.
## Emote and spell-component pseudocode
Sources:
- `ClientCommunicationSystem::DoEmote @ 0x00578AD0`
- `CM_Communication::Event_Emote @ 0x006A4120`
- `ClientCommunicationSystem::DoFillComponents @ 0x0056FD50`
- `gmVendorUI::FillComponentList @ 0x004C4540`
- `MagicEnumMapper::SpellComponentCategoryFromString @ 0x0056E290`
```text
emote(arguments):
text = join all arguments
if text is non-empty: send opcode 0x01DF followed by String16L text
emotes(arguments):
if arguments: display usage
else: display the retail standard-emote help list
```
```text
fillcomps(arguments):
reject more than two arguments
if first argument is "clear":
send opcode 0x0224, component id 0, amount 0xFFFFFFFF
clear desired component levels and echo "Component list cleared."
return
parse optional component category and optional positive maximum price
if no vendor is open: display "You need an open vendor."
else for each desired component in the chosen category:
needed = desired level - currently held count
find the component in vendor stock and add min(needed, stock) to buy list
stop before exceeding the maximum price and report the limit
```
Accepted category spellings are Scarab(s), Herb(s), PowderedGem(s) or
Powder(s), AlchemicalSubstance(s) or Potion(s), Talisman(s), Taper(s), and
Pea(s). Component buying remains an operation on the vendor controller; the
command parser does not duplicate vendor inventory or price logic.