Commits the command-registry, interface-text (SpewBox), and side-channels-vs-ACE research docs (R3 color-table landed with CH1). Corrects the CH1 ledger SHA the implementer recorded pre-amend. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
931 lines
58 KiB
Markdown
931 lines
58 KiB
Markdown
# Retail's transient on-screen "interface text" — the SpewBox
|
||
|
||
**Date:** 2026-08-09
|
||
**Status:** RESEARCH ONLY. No production code changed.
|
||
**Oracle:** `docs/research/named-retail/acclient_2013_pseudo_c.txt` (Sept 2013 EoR
|
||
build, PDB-named), `docs/research/named-retail/symbols.json`,
|
||
`docs/research/named-retail/acclient.h`, plus byte-level string recovery from the
|
||
PDB-paired binary `C:\Users\erikn\Downloads\acclient.exe` (v11.4186, CodeView GUID
|
||
`9e847e2f-777c-4bd9-886c-22256bb87f32`). Server-side cross-check against
|
||
`references/ACE/` and `references/holtburger/`.
|
||
|
||
---
|
||
|
||
## TL;DR
|
||
|
||
The system is called the **SpewBox** — `gmSpewBoxUI` @ `0x004D5A30`.
|
||
|
||
The routing rule is one line: **`ClientSystem::AddTextToScroll(text, type, ...)`
|
||
broadcasts one notice to every text sink, and the sinks self-select — the SpewBox
|
||
takes `type == 0x1A` and nothing else, while every `ChatInterface` window is
|
||
constructed with a 64-bit type filter that has bit 26 (`1 << 0x1A`) cleared, so
|
||
`0x1A` is exactly the type the chat window refuses and the SpewBox accepts.**
|
||
|
||
**23 client-raised local refusal sites** were found (11 distinct message strings),
|
||
none of which involve a server round-trip. All of them use type `0x1A`.
|
||
|
||
---
|
||
|
||
## 1. System identification
|
||
|
||
### 1.1 The display element
|
||
|
||
| Symbol | Address | Role |
|
||
|---|---|---|
|
||
| `gmSpewBoxUI::gmSpewBoxUI` | `0x004D5A30` | ctor; derives from `UIElement_Field` + `NoticeHandler` |
|
||
| `gmSpewBoxUI::Create` | `0x004D5C30` | factory (`operator new(0x610)`) |
|
||
| `gmSpewBoxUI::Register` | `0x004D5DD0` | `UIElement::RegisterElementClass(0x10000016, gmSpewBoxUI::Create)` |
|
||
| `gmSpewBoxUI::GetUIElementType` | `0x004D5AA0` | returns `0x10000016` |
|
||
| `gmSpewBoxUI::PostInit` | `0x004D5AB0` | binds the child ListBox, reads max-items, registers for notice `0x186B6` and global message `3` |
|
||
| `gmSpewBoxUI::RecvNotice_DisplayFinalStringInfo` | `0x004D60A0` | **the type filter** — `if (arg2 == 0x1A) m_spewBoxPending.AddToEnd(str)` |
|
||
| `gmSpewBoxUI::Update` | `0x004D5DF0` | drains the pending queue into the ListBox |
|
||
| `gmSpewBoxUI::ListenToGlobalMessage` | `0x004D6090` | `if (msg == 3) Update()` |
|
||
| `gmSpewBoxUI::ListenToElementMessage` | `0x004D57C0` | `if (idElement == 0x1000004A && idMessage == 0x10000003) DeleteItem()` — the expiry hook |
|
||
| `gmSpewBoxUI::~gmSpewBoxUI` | `0x004D5BD0` | unregisters |
|
||
|
||
It is registered alongside the rest of the HUD in the element-class registration
|
||
block at `0x0047A4A6` (`gmClient` init), between `gmSmartBoxUI::Register()` and the
|
||
`gmFloaty*UI` family.
|
||
|
||
### 1.2 The router
|
||
|
||
| Symbol | Address | Role |
|
||
|---|---|---|
|
||
| `ClientSystem::AddTextToScroll(PStringBase<wchar>, uint type, uint8 allowPluginFilter, uint windowId)` | `0x00563C50` | **the single chokepoint** for all player-visible text |
|
||
| `ClientSystem::AddTextToScroll(PStringBase<char>, ...)` | `0x004C2420` | narrow-string overload → widens → above |
|
||
| `ClientSystem::AddTextToScroll(char const*, ...)` | `0x00487FC0` | literal overload → widens → above |
|
||
| `ECM_UI::SendNotice_DisplayFinalStringInfo` | `0x00692550` | broadcast to notice id `0x186B6` |
|
||
| `ECM_UI::SendNotice_DisplayStringInfo` | `0x006925B0` | broadcast to notice id `0x186A5` |
|
||
| `ECM_UI::SendNotice_DisplayWeenieError` | `0x00692600` | broadcast to notice id `0x186B7` |
|
||
| `ClientCommunicationSystem::RecvNotice_DisplayStringInfo` | `0x0056E890` | notice `0x186A5` → `AddTextToScroll` |
|
||
| `ClientCommunicationSystem::RecvNotice_DisplayWeenieError` | `0x0057E700` | notice `0x186B7` → `HandleFailureEvent` |
|
||
| `ClientCommunicationSystem::HandleFailureEvent(uint errorId, PStringBase<wchar> param)` | `0x00571990` | **the error-id → text + destination switch** (339 cases) |
|
||
| `ChatInterface::RecvNotice_DisplayFinalStringInfo` | `0x004F4640` | the chat-window sink |
|
||
| `ChatInterface::TypeIsActive` | `0x004F2F10` | `(m_llTextTypeFilter >> type) & 1` |
|
||
| `ChatInterface::ChatInterface` (ctor) | `0x004F4550` | sets the default filter — see §2.2 |
|
||
| `ChatInterface::BuildChatColorLookupTable` | `0x004F31C0` | per-type chat colors |
|
||
|
||
### 1.3 The wire entry points
|
||
|
||
| Symbol | Address | Opcode | Behaviour |
|
||
|---|---|---|---|
|
||
| `ClientCommunicationSystem::Handle_Communication__TextboxString` | `0x0057D3A0` | **`0xF7E0`** (`ServerMessage`) | squelch check, then `AddTextToScroll(text, wireChatType, 1, 0)` — **the wire type decides the destination** |
|
||
| `ClientCommunicationSystem::Handle_Communication__TransientString` | `0x0057D460` | **`0x02EB`** (GameEvent) | `AddTextToScroll(text, 0x1A, 1, 0)` — **hardcoded to the SpewBox** |
|
||
| `ClientCommunicationSystem::Handle_Communication__PopUpString` | `0x0057FE80` | **`0x0004`** (GameEvent) | builds a `PropertyCollection` and calls `DialogFactory::MakeDialogInCurrentUI` — **a modal dialog, neither chat nor spew** |
|
||
| `ClientCommunicationSystem::RecvNotice_DisplayWeenieError` | `0x0057E700` | **`0x028A` / `0x028B`** | → `HandleFailureEvent` → per-id destination |
|
||
|
||
Dispatch table sites: `0x0055CA1F` (`0xF7E0`), `0x0055C581` (`0x02EB`),
|
||
`0x0055B0BD` (event `0x0004`), all inside the `UIQueueManager` message switch that
|
||
begins at `0x0055B000`.
|
||
|
||
---
|
||
|
||
## 2. The routing model
|
||
|
||
### 2.1 One broadcast, self-selecting sinks
|
||
|
||
`ClientSystem::AddTextToScroll` @ `0x00563C50` does, in order:
|
||
|
||
1. **Plugin veto.** If `allowPluginFilter != 0` and the plugin API is ready, call
|
||
`IACPlugin::OnChatWindowText(bstr, type, &eat)`. If the plugin sets `eat`, the
|
||
message is dropped entirely — it reaches neither chat nor spew.
|
||
(`0x00563C7D`–`0x00563CB6`.)
|
||
2. **Trim** trailing whitespace.
|
||
3. **Censor.** If `PlayerModule::FilterLanguage()`, explode on spaces and replace
|
||
any word failing `TabooTableAdaptor::CheckCensorsW` with `****`.
|
||
4. **Branch on type** (`0x00563DE6`):
|
||
- `type == 0x1A` → **skip the timestamp, skip the chat log file**, jump straight
|
||
to the broadcast.
|
||
- otherwise → prepend `%#H:%M:%S ` if `PlayerModule::DisplayTimeStamps()`, and
|
||
`fprintf` the line to `ClientSystem::s_pLogFile` if a chat log is open.
|
||
5. **Broadcast** `ECM_UI::SendNotice_DisplayFinalStringInfo(type, mainStr,
|
||
prefixStr, windowId)`.
|
||
|
||
That last call goes to *every* registered handler of notice `0x186B6`. There are
|
||
exactly two kinds of subscriber:
|
||
|
||
- **`gmSpewBoxUI`** (`0x004D60A0`): `if (type == 0x1A) enqueue`. Nothing else, and
|
||
it ignores `windowId` entirely.
|
||
- **`ChatInterface`** (`0x004F4640`), one per chat window:
|
||
```
|
||
if (windowId == this->m_eWindowID) -> append
|
||
else if (windowId == 0 && TypeIsActive(type)) -> append
|
||
else -> ignore
|
||
```
|
||
|
||
So the routing rule is a **type filter on the receiver side**, not a switch on the
|
||
sender side. There is no "destination" field anywhere in the data.
|
||
|
||
The fact that type `0x1A` is *skipped* for timestamping and chat-log-file writing
|
||
(step 4) is the client author's own statement that `0x1A` is not chat.
|
||
|
||
### 2.2 Why `0x1A` never appears in the chat window
|
||
|
||
`ChatInterface::ChatInterface` @ `0x004F4550`:
|
||
|
||
```
|
||
0x004F45B8 this->m_llTextTypeFilter = 0xFFFFFFFF; // low dword
|
||
0x004F45BE ((uint32*)&m_llTextTypeFilter)[1] = 0xFFFFFFFF; // high dword
|
||
0x004F45F3 this->m_llTextTypeFilter &= 0xFBFFFFFF; // clear bit 26
|
||
```
|
||
|
||
`0xFBFFFFFF` = `~0x04000000` = `~(1 << 26)` = `~(1 << 0x1A)`.
|
||
|
||
**Every chat window is born with every text type enabled except `0x1A`.** That is
|
||
the whole mechanism. `TypeIsActive` @ `0x004F2F10` is just
|
||
`(m_llTextTypeFilter >> type) & 1`.
|
||
|
||
The filter is subsequently overwritten from a saved UI bitfield property
|
||
(`InqBitfield64` at `0x004F3109` and `0x004F3984`), so in principle a chat window
|
||
could be configured to show `0x1A` — but the shipped chat-options UI does not offer
|
||
it, which is why ACE's `ChatMessageType.cs:255-259` concluded "Client doesn't
|
||
display it" and commented `x1A` out. **That conclusion is wrong and worth
|
||
recording:** the client does display `0x1A`, just not in the chat scroll.
|
||
|
||
### 2.3 The complete destination model
|
||
|
||
| Destination | Owner | Trigger |
|
||
|---|---|---|
|
||
| **Chat scroll** (one or more windows) | `ChatInterface` @ `0x004F4640` | `AddTextToScroll` with `type != 0x1A`, or with `windowId == this window` |
|
||
| **SpewBox** (transient screen text) | `gmSpewBoxUI` @ `0x004D60A0` | `AddTextToScroll` with `type == 0x1A` |
|
||
| **Modal dialog** | `DialogFactory::MakeDialogInCurrentUI` | `Handle_Communication__PopUpString` (event `0x0004`) only |
|
||
| **Chat log file** | `ClientSystem::s_pLogFile` | `AddTextToScroll` with `type != 0x1A` |
|
||
| **Plugin sink / veto** | `IACPlugin::OnChatWindowText` | every `AddTextToScroll` with `allowPluginFilter != 0` |
|
||
| **(dropped)** | — | plugin sets the `eat` out-param |
|
||
|
||
Note that a `type == 0x1A` message with a **non-zero `windowId`** lands in *both*
|
||
the SpewBox and that specific chat window. This is exactly what slash-command
|
||
output does: `ClientCommunicationSystem` emits its command responses as
|
||
`AddTextToScroll(text, 0x1A, 1, this->m_idCurrentCommandSource)` (~40 sites from
|
||
`0x0056EF3B` through `0x005707FB`), so a `/`-command's reply appears on screen
|
||
*and* is echoed into the window you typed it in.
|
||
|
||
### 2.4 Where the strings come from
|
||
|
||
**Not from `client_local_English.dat`.** Every player-visible error string in this
|
||
path is a **wide-char literal compiled into `acclient.exe`**:
|
||
|
||
- `HandleFailureEvent` @ `0x00571990` builds each one inline
|
||
(`PStringBase<unsigned short>::PStringBase<unsigned short>(&var, u"…")`) or via
|
||
`PStringBase<unsigned short>::sprintf(&s, u"The %s cannot be used …")` with the
|
||
`0x028B` string parameter substituted.
|
||
- The 11 movement/jump refusals are process-lifetime globals initialised by static
|
||
ctors at `0x00708F00`–`0x00709180` (see §4).
|
||
|
||
The DAT `StringTable` machinery (`StringInfo::SetTableEnum`,
|
||
`StringInfo::SetStringIDandTableEnum`) exists and is used for **UI chrome** —
|
||
option labels, tooltips, command aliases — but the failure-event text is hardcoded.
|
||
`StringInfo::SetLiteralValue` is what the failure path uses.
|
||
|
||
This matters for the port: **we do not need a DAT string table to reach parity on
|
||
this feature.** A C# table keyed by `WeenieError` id is exactly what retail does.
|
||
|
||
---
|
||
|
||
## 3. Presentation parameters
|
||
|
||
### 3.1 What acclient owns (portable, measured)
|
||
|
||
From `gmSpewBoxUI::PostInit` @ `0x004D5AB0` and `gmSpewBoxUI::Update` @ `0x004D5DF0`:
|
||
|
||
| Behaviour | Evidence | Value |
|
||
|---|---|---|
|
||
| Backing widget | `0x004D5AD7` | a `UIElement_ListBox` found by `GetChildRecursive(0x10000049)` |
|
||
| Mouse | `0x004D5AC4`, `0x004D5AFE` | the SpewBox and its ListBox are both `SetMouseVisible(0)` — **click-through** |
|
||
| Background | `0x004D5ABB` | `SetShouldEraseBackground(1)` |
|
||
| Max concurrent lines | `0x004D5B34` | ListBox property `0x10000028`; **defaults to 1** if the property is absent or unreadable |
|
||
| Per-line widget | `0x004D5E42` | `CreateChildElementByEnum(parent=null, layoutEnum=0x10000012, elementId=0x1000004A)` — a DAT-authored `UIElement_Text` template |
|
||
| Text preprocessing | `0x004D5E9C` | `trim(leading=0, trailing=1, whitespace)` — trailing whitespace stripped |
|
||
| Sizing | `0x004D5EB1`–`0x004D5EE6` | resized to the ListBox's width, then `RecalculateGlyphList`, then resized again to the computed scrollable height (word wrap) |
|
||
| **Dedupe** | `0x004D5EF6`–`0x004D5F91` | if the current item 0 has **byte-identical text**, that older item is deleted first. A repeated message refreshes in place instead of stacking. |
|
||
| Insertion | `0x004D5F9F` | `InsertItem(item, 0)` — **newest at the top** |
|
||
| Overflow | `0x004D5FB6` | if `count > m_maxConcurrentItems`, `DeleteItem(count - 1)` — **oldest drops off** |
|
||
| Scroll | `0x004D601D` | `ScrollToShow(0)` after a batch |
|
||
| Drain cadence | `0x004D5BA6`, `0x0045CFFB` | global message `3`, broadcast once per UI tick from `UIElementManager::UseTime` @ `0x0045CFD0` |
|
||
| Expiry hook | `0x004D57D7` | the SpewBox deletes an item when it receives element message `0x10000003` from element id `0x1000004A` |
|
||
|
||
The queue is a `SmartArray<StringInfo,1> m_spewBoxPending`; `RecvNotice_*` only
|
||
enqueues, `Update` only drains. Enqueue and display are decoupled by one frame.
|
||
|
||
### 3.2 PRESENTATION-UNKNOWN (keystone / DAT-owned)
|
||
|
||
These could **not** be established from acclient and must not be guessed:
|
||
|
||
1. **Line lifetime / fade curve.** acclient never raises element message
|
||
`0x10000003`. I searched every `BroadcastElementMessage` / `ForwardElementMessage`
|
||
call site in the whole 66 MB listing: the only element message id above
|
||
`0x10000000` that acclient itself raises is `0x10000004`
|
||
(`0x004F0EC5`, a stat-type element). `UIElement` / `UIRegion` / `ElementDesc` /
|
||
`LayoutDesc` expose no `Duration` / `Lifetime` / `Fade` / `Expire` member at all.
|
||
**The timeout and any fade are owned by keystone.dll or by the authored
|
||
`ElementDesc` behaviour of layout `0x10000012` element `0x1000004A`.**
|
||
*Resolution path:* dump that LayoutDesc from `client_local_English.dat`, or set a
|
||
cdb breakpoint on `gmSpewBoxUI::ListenToElementMessage` (`0x004D57C0`) in a live
|
||
retail client and time the deltas between a message appearing and its removal.
|
||
2. **Screen position and extent.** Authored in whatever LayoutDesc declares an
|
||
element of class `0x10000016`. The natural host is the main game view
|
||
(`gmSmartBoxUI`, LayoutDesc `0x2100000F`) but **this was not confirmed** — no
|
||
dumped layout in `docs/research/retail-ui/` mentions it.
|
||
*Resolution path:* enumerate LayoutDescs and look for element type `0x10000016`.
|
||
3. **Font, size, justification, colour of the SpewBox line.** All from the same DAT
|
||
template. In particular, the SpewBox does **not** use the chat colour table:
|
||
`BuildChatColorLookupTable` writes to `ChatInterface::m_chatLog`, a different
|
||
element tree entirely.
|
||
4. **Max concurrent items in the shipped layout.** The code reads ListBox property
|
||
`0x10000028`; the authored value is DAT data. The *code default* is 1.
|
||
|
||
### 3.3 The chat-window colour table (adjacent, for completeness)
|
||
|
||
`ChatInterface::BuildChatColorLookupTable` @ `0x004F31C0` assigns
|
||
`RGBAColor` constants to text types. Colour values below are from
|
||
`claude-memory/reference_retail_chat_colors.md` (dumped live via cdb, 2026-06-16).
|
||
|
||
| Type(s) | Colour symbol | Addr | RGB |
|
||
|---|---|---|---|
|
||
| *default (all)* | `colorGreen` | `0x81C578` | 0.500, 1.000, 0.498 |
|
||
| `0x02` | `colorWhite` | `0x81C4B8` | 1, 1, 1 |
|
||
| `0x03 0x0A 0x13 0x1F` | (yellow, unnamed) | `0x81C4C8` | 1, 1, 0.247 |
|
||
| `0x04 0x0B` | (unnamed, not yet read) | `0x81C4D8` | — |
|
||
| `0x05` | `colorBrightPurple` | `0x81C4E8` | 1, 0.498, 1 |
|
||
| `0x06 0x0F 0x15` | `colorDarkRed` | `0x81C4F8` | 1, 0.247, 0.247 |
|
||
| `0x07 0x11` | `colorLightBlue` | `0x81C518` | 0.247, 0.749, 1 |
|
||
| `0x08 0x09` | `colorPink` | `0x81C528` | 1, 0.588, 0.588 |
|
||
| `0x0C` | `colorGrey` | `0x81C558` | 0.824, 0.824, 0.784 |
|
||
| `0x0D` | `colorCyan` | `0x81C538` | 0.247, 0.863, 0.863 |
|
||
| `0x0E 0x1B 0x1C 0x1D 0x1E 0x20` | `colorBlueGrey` | `0x81C548` | 0.706, 0.863, 0.941 |
|
||
| `0x12 0x21` | (orange, unnamed) | `0x81C568` | 0.933, 0.573, 0.118 |
|
||
| `0x16` | `colorLightRed` | `0x81C508` | 0.960, 0.459, 0.447 |
|
||
| `0x1A` | `colorBrightRed` | `0x81C4A8` | 1, 0, 0 |
|
||
|
||
Two things fall out of this table:
|
||
|
||
- The `0x1A` row exists purely for the case where a user manually enables the
|
||
filter bit. **It is not the SpewBox's colour.** Do not port it as such.
|
||
- Types `0x20` and `0x21` are real and coloured. **ACE's `ChatMessageType` stops at
|
||
`0x1F`** — the client's text-type space is wider than the server-side enum.
|
||
|
||
---
|
||
|
||
## 4. Client-raised local errors (no server round-trip)
|
||
|
||
Retail refuses several actions locally and prints the refusal itself. All of them
|
||
land on type `0x1A`.
|
||
|
||
### 4.1 The message globals
|
||
|
||
Static ctors at `0x00708F00`–`0x00709180`. Strings recovered verbatim from the
|
||
binary (the pseudo-C truncates at 33 chars).
|
||
|
||
| Global | Full text | Used? |
|
||
|---|---|---|
|
||
| `cant_jump_position` | `You can't jump from this position` | yes (3 sites) |
|
||
| `cant_jump_in_air` | `You can't jump while in the air` | yes (3 sites) |
|
||
| `cant_jump_load` | `You're too loaded down to jump` | yes (3 sites) |
|
||
| `cant_jump_stamina` | `You're too tired to jump!` | **dead in this build** |
|
||
| `cant_jump_recent` | `You've jumped too recently!` | **dead in this build** |
|
||
| `too_tired` | `You are too tired to move!` | yes (1 site) |
|
||
| `cant_sit_combat` | `You can't sit down while in combat` * | yes (1 site) |
|
||
| `cant_lie_down_combat` | `You can't lie down while in combat` * | yes (1 site) |
|
||
| `cant_crouch_combat` | `You can't crouch while in combat` * | yes (1 site) |
|
||
| `cant_emote_combat` | `You can't use chat emotes in combat` * | yes (1 site) |
|
||
| `cant_emote_position` | `You can't use chat emotes from this position` * | yes (1 site) |
|
||
|
||
\* these five were length-truncated in the listing; the prefixes are exact, the
|
||
tails are the obvious completion and should be re-read from the binary before being
|
||
committed as literals.
|
||
|
||
### 4.2 Raise sites
|
||
|
||
**Jump family** — the source of the codes is `CMotionInterp` and they are
|
||
`WeenieError` ids, the same numbering the server uses.
|
||
|
||
| Function | Addr | Codes it produces |
|
||
|---|---|---|
|
||
| `CMotionInterp::charge_jump` | `0x005281C0` | `0x49` if `CWeenieObject::CanJump(jump_extent)` fails; `0x48` if `forward_command` is a disallowed posture; `0` otherwise |
|
||
| `CMotionInterp::jump_is_allowed` | `0x005282B0` | `0x24` if not on the ground; `0x47` if fully constrained or out of stamina; else defers to `jump_charge_is_allowed` / `motion_allows_jump` |
|
||
|
||
| Consumer | Addr | Sites |
|
||
|---|---|---|
|
||
| `ClientCombatSystem::CommenceJump` | `0x0056AF90` | `0x0056AFE3` → `cant_jump_position` (0x48); `0x0056AFD7` → `cant_jump_load` (0x49); `0x0056AFCB` → `cant_jump_in_air` (fallback) |
|
||
| `ClientCombatSystem::DoJump` | `0x0056B110` | `0x0056B29A` → `cant_jump_in_air` (0x24); `0x0056B27E` → `cant_jump_position` (0x48); `0x0056B262` → `cant_jump_load` (0x49) |
|
||
| `ClientCommunicationSystem::HandleFailureEvent` | `0x00571990` | `0x00571DA1` (0x24), `0x00571D73` (0x48), `0x00571D8A` (0x49) — **the same three globals, reused for the server-sent ids** |
|
||
|
||
That last row is the important one: retail reuses one string table for
|
||
locally-detected and server-reported failures. The client-local path is a *latency
|
||
optimisation over the server's own answer*, not a separate feature.
|
||
|
||
**Movement / posture / emote family** — `CommandInterpreter::MovePlayer` @
|
||
`0x006B3F40`, switching on `CPhysicsObj::DoMotion`'s return:
|
||
|
||
| Code | Addr | Message | Emit |
|
||
|---|---|---|---|
|
||
| `0x3E` | `0x006B43E4` | `too_tired` | `ECM_UI::SendNotice_DisplayStringInfo(0x1A, ...)` |
|
||
| `0x3F` | `0x006B4366` | `cant_crouch_combat` | same |
|
||
| `0x40` | `0x006B43A4` | `cant_sit_combat` | same |
|
||
| `0x41` | `0x006B43C4` | `cant_lie_down_combat` | same |
|
||
| `0x42` | `0x006B43F6` | `cant_emote_combat` | same |
|
||
| `0x44` | `0x006B4419` | `cant_emote_position` | same |
|
||
|
||
These take the `SendNotice_DisplayStringInfo` path (notice `0x186A5`) rather than
|
||
calling `AddTextToScroll` directly, but
|
||
`ClientCommunicationSystem::RecvNotice_DisplayStringInfo` @ `0x0056E890` immediately
|
||
forwards to `AddTextToScroll(str, 0x1A, 1, 0)`, so the outcome is identical.
|
||
|
||
**Vendor family** — `0x004C4575`, `AddTextToScroll("You need an open vendor.", 0x1A, 1, 0)`.
|
||
|
||
**Total: 23 client-raised sites, 6 enclosing functions, 11 distinct strings.**
|
||
|
||
### 4.3 What is *not* client-raised
|
||
|
||
Retail does **not** locally generate "You are too encumbered to carry that!" —
|
||
`0x2A` arrives from the server as `WeenieError` and is turned into text by
|
||
`HandleFailureEvent`. Likewise spell fizzle (`0x0402`) is server-sent. ACE confirms
|
||
this shape: `Player_Inventory.cs` sends the encumbrance message as
|
||
`GameEventCommunicationTransientString` (`0x02EB`) rather than as a `WeenieError`
|
||
at all, and `Player_Magic.cs:918` sends `SendWeenieError(YourSpellFizzled)`.
|
||
|
||
---
|
||
|
||
## 5. What the server sends (ACE cross-check)
|
||
|
||
| Opcode | Class | Payload | Client destination |
|
||
|---|---|---|---|
|
||
| `0xF7E0` `ServerMessage` | `GameMessageSystemChat` | `string16L text`, `u32 chatType` | `AddTextToScroll(text, chatType, 1, 0)` — chat *or* spew depending on the type |
|
||
| GameEvent `0x02EB` `CommunicationTransientString` | `GameEventCommunicationTransientString` | `string16L text` only, **no type field** | hardcoded `0x1A` → **SpewBox** |
|
||
| GameEvent `0x028A` `WeenieError` | `GameEventWeenieError` | `u32 errorId` | `HandleFailureEvent` → per-id (see appendix) |
|
||
| GameEvent `0x028B` `WeenieErrorWithString` | `GameEventWeenieErrorWithString` | `u32 errorId`, `string16L param` | same, with `%s` substitution |
|
||
| GameEvent `0x0004` `PopUpString` | — | `string16L text` | `DialogFactory::MakeDialogInCurrentUI` → modal dialog |
|
||
|
||
ACE never resolves a `WeenieError` id to text — it always writes the bare u32
|
||
(`references/ACE/Source/ACE.Server/Network/GameEvent/Events/GameEventWeenieError.cs`).
|
||
**The client owns every error string.** `0x48 = YouCantJumpFromThisPosition`,
|
||
`0x49 = CantJumpLoadedDown` (ACE marks the latter "client side only", consistent
|
||
with our `charge_jump` finding).
|
||
|
||
`references/holtburger/` is **not** a useful oracle here: it flattens `0x02EB` into
|
||
a plain system chat line
|
||
(`crates/holtburger-core/src/client/messages.rs:268-275`) and has no transient
|
||
destination at all. It *is* a useful oracle for id→text: its hand-written
|
||
`format_weenie_error` table (`crates/holtburger-core/src/errors.rs`) covers ~60
|
||
ids, and its `is_actually_weenie_error()` allowlist (`errors.rs:302-315`) correctly
|
||
notes that several "errors" are success notices.
|
||
|
||
---
|
||
|
||
## 6. acdream gap list
|
||
|
||
Verified against the worktree at `.claude/worktrees/eloquent-hugle-42119e`.
|
||
|
||
### 6.1 Routing
|
||
|
||
| Retail | acdream today | Gap |
|
||
|---|---|---|
|
||
| One `AddTextToScroll(text, type, ...)` chokepoint feeding N self-selecting sinks | `GameEventWiring.cs:223/228` calls `chat.OnWeenieError(...)`; `LiveSessionEventRouter.cs:268` calls `Chat.OnSystemMessage(text, chatType)` | **No chokepoint, no sink model.** Every producer writes directly into `ChatLog`. |
|
||
| Destination decided by text type on the receiver | `ChatLog` is the only destination | **The whole transient destination is missing.** |
|
||
| Text type `0x1A` = SpewBox | wire `chatType` *is* parsed and stored in `ChatEntry.ChannelId` but **never read for display**; colour comes solely from the 9-value `ChatKind` enum (`ChatWindowController.cs:542-555`) | The discriminator we need is on the wire, captured, and then thrown away. |
|
||
| 34-value text-type space (0x00–0x21) | no enum mirroring it — `ChatKind` (9 buckets), `TurbineChat.ChatType` (rooms), `ChatChannelKind` (outbound) are all different axes | **Missing enum.** Raw `0x1Au` literals already appear at `InteractionRetainedUiComposition.cs:348/418/756/771` and `SessionPlayerComposition.cs:1128` with no name. |
|
||
| `0x02EB CommunicationTransientString` → always spew | **not wired at all** | Missing message. |
|
||
| `0x0004 PopUpString` → modal dialog | `GameEventWiring.cs:126` `chat.OnPopup(...)` → `ChatLog` | Wrong destination (retail opens a dialog). Out of scope for this port but worth a register row. |
|
||
| Plugin veto hook `OnChatWindowText(text, type, &eat)` | none | Missing; note it for the plugin API. |
|
||
| Timestamp + chat-log-file suppressed for `0x1A` | n/a | Falls out of the port if the sink split is done right. |
|
||
|
||
### 6.2 Presentation
|
||
|
||
`PortalWaitNoticeController` (`src/AcDream.App/UI/PortalWaitNoticeController.cs`) is
|
||
the closest existing thing: a single centred full-screen `UiText`, `ClickThrough`,
|
||
`ZOrder = int.MaxValue`, ported from `gmSmartBoxUI::UseTime`. It is a **single
|
||
overwrite-only slot with no queue, no timeout, no fade** — structurally the right
|
||
shape but missing every SpewBox behaviour (bounded queue, newest-on-top, dedupe
|
||
against the newest, per-line expiry).
|
||
|
||
`TextRenderer` + `BitmapFont` (`src/AcDream.App/Rendering/`) are a 2D screen-space
|
||
quad batcher and an ASCII atlas — primitives with no message concept.
|
||
`DebugVM.ToastKind`/`AddToast` is a 25-deep ring rendered **inside the ImGui dev
|
||
panel only** (`DebugPanel.cs:88`), explicitly documented as "no on-screen flash".
|
||
|
||
There is **no** spew-box panel, controller, or element id anywhere in `src/` — a
|
||
tree-wide grep for `Spew` returns zero hits.
|
||
|
||
### 6.3 Strings
|
||
|
||
Two unrelated hardcoded maps exist and neither covers the SpewBox set:
|
||
|
||
- `src/AcDream.Core/Chat/WeenieErrorMessages.cs` — ~30 no-param + ~28 with-string
|
||
templates, fallback `"WeenieError 0x{code:X4}"`.
|
||
- `src/AcDream.Core.Net/Messages/WeenieErrorText.cs` — 4 codes, used only by the
|
||
`UseDone` handler.
|
||
|
||
Neither has `0x0048` or `0x0049`. A server-sent `0x48` renders today as the literal
|
||
string `WeenieError 0x0048`. Retail has 339 ids in its switch.
|
||
|
||
### 6.4 Client-raised errors — the sharpest gap
|
||
|
||
`src/AcDream.Core/Physics/MotionInterpreter.cs` **already computes the right codes**:
|
||
`JumpChargeIsAllowed` (`:1762-1773`), `ChargeJump` (`:1827-1851`, an explicit port
|
||
of `CMotionInterp::charge_jump @ 0x005281C0`), `JumpIsAllowedSharedGate`
|
||
(`:2052-2070`). They are unit-tested
|
||
(`tests/AcDream.Core.Tests/Physics/MotionInterpreterJumpFamilyTests.cs`).
|
||
|
||
They are then **discarded**:
|
||
|
||
- `src/AcDream.Runtime/Gameplay/PlayerMovementController.cs:2473` —
|
||
`_motion.ChargeJump();` with the return value not assigned to anything.
|
||
- `PlayerMovementController.cs:2484-2514` — `var jumpResult = _motion.jump(...)`,
|
||
`if (jumpResult == WeenieError.None) { ...launch... }`, **no `else`**. On refusal
|
||
the controller resets `_jumpCharging` / `_jumpExtent` and returns silently.
|
||
|
||
So the player sees the power bar drain and nothing happen. **No code path in acdream
|
||
carries a locally-produced `WeenieError` to any display surface** — the only
|
||
`WeenieError → text` conversions are triggered by inbound wire events.
|
||
|
||
---
|
||
|
||
## 7. Recommended port shape
|
||
|
||
Layered per `docs/architecture/acdream-architecture.md` and the Code Structure Rules.
|
||
|
||
### 7.1 `AcDream.Core` — the type space and the strings
|
||
|
||
1. **`AcDream.Core/Chat/TextMessageType.cs`** — a `uint`-backed enum mirroring the
|
||
client's 0x00–0x21 space, not ACE's truncated 0x00–0x1F. Names from ACE's
|
||
`ChatMessageType` where they exist; `SpewBox = 0x1A` for the one retail leaves
|
||
unnamed; explicit placeholders for `0x20`/`0x21` (coloured in retail, unnamed in
|
||
every server-side oracle). This retires the raw `0x1Au` literals already
|
||
scattered through `InteractionRetainedUiComposition` and
|
||
`SessionPlayerComposition`.
|
||
2. **Extend `WeenieErrorMessages`** into the full retail table: `(id) → (template,
|
||
TextMessageType)`. The `TextMessageType` column *is* the routing decision, taken
|
||
verbatim from `HandleFailureEvent` — the appendix below is the transcription.
|
||
Keep `_`/`{0}` interpolation for the `0x028B` parameter. Fold
|
||
`WeenieErrorText.cs` into it (it is a 4-entry duplicate).
|
||
3. **`ClientTextRefusals`** — the 11 client-local literals from §4.1 as named
|
||
constants, so the jump/posture sites and the `HandleFailureEvent` table share
|
||
one string exactly as retail does.
|
||
|
||
### 7.2 `AcDream.Core` / `AcDream.Runtime` — the chokepoint and the sinks
|
||
|
||
4. **One router**, the direct analogue of `ClientSystem::AddTextToScroll`:
|
||
`AddText(string text, TextMessageType type, uint windowId = 0)`. It owns:
|
||
trim → (future) plugin veto → branch: `type == SpewBox` bypasses timestamp and
|
||
log-file, everything else does not → publish one event.
|
||
Given J4.1, the natural owner is **`RuntimeCommunicationState`**
|
||
(`src/AcDream.Runtime/...`), which already owns the canonical transcript. It
|
||
should expose *two* borrowed views: the existing chat transcript and a new
|
||
**`SpewBoxState`**.
|
||
5. **`SpewBoxState`** in Runtime — pure state, no presentation:
|
||
- pending queue drained once per tick (retail's global message 3),
|
||
- `MaxConcurrentItems` (retail code default 1; the shipped value is DAT data —
|
||
see the open question in §8),
|
||
- insert at index 0,
|
||
- **dedupe against index 0 only** (identical text deletes the older entry first),
|
||
- drop index `count-1` on overflow,
|
||
- per-entry expiry timestamp. Retail's expiry lives in keystone; until it is
|
||
measured, this is a **divergence needing a register row** (see §7.5).
|
||
6. **Rewire the producers**: `GameEventWiring` `0x028A`/`0x028B` →
|
||
look up `(template, type)` → router. New `0x02EB` handler → router with
|
||
`type = SpewBox`. `LiveSessionEventRouter:268` (`0xF7E0`) → router with the wire
|
||
`chatType` instead of `Chat.OnSystemMessage`.
|
||
7. **Wire the local refusals.** `PlayerMovementController.cs:2473` and `:2484-2514`
|
||
currently drop `WeenieError` values on the floor. Give both an `else` that calls
|
||
the router with the matching string at `TextMessageType.SpewBox`. Same for the
|
||
posture/emote family if/when `CommandInterpreter::MovePlayer` is ported.
|
||
|
||
### 7.3 `AcDream.UI.Abstractions` — the contract
|
||
|
||
8. A `SpewBoxVM` snapshot (ordered lines + remaining lifetime) beside the existing
|
||
`ChatVM`, per Code Structure Rule 3. Panels must not reach into Runtime.
|
||
|
||
### 7.4 `AcDream.App` — presentation
|
||
|
||
9. A `SpewBoxController` next to `PortalWaitNoticeController`, using the same proven
|
||
pattern: full-width `UiText` block, `ClickThrough`, high `ZOrder`, newest line at
|
||
the top. `PortalWaitNoticeController` is the template to copy — it is already the
|
||
right kind of object, it just holds one slot instead of a bounded list.
|
||
10. **Do not** reuse the chat colour table for it. The SpewBox colour is DAT-owned;
|
||
until the layout is dumped, pick a placeholder and put a register row on it.
|
||
|
||
### 7.5 Divergence-register rows this port must add
|
||
|
||
Per the mandatory bookkeeping rule, the following are deviations at the moment of
|
||
landing and each needs a row in
|
||
`docs/architecture/retail-divergence-register.md` **in the same commit**:
|
||
|
||
- SpewBox line lifetime / fade curve is invented, not measured (keystone-owned) —
|
||
risk: lines linger or vanish visibly faster/slower than retail.
|
||
- SpewBox screen position / font / colour are invented until the LayoutDesc is
|
||
dumped — risk: text in the wrong place or the wrong colour.
|
||
- `MaxConcurrentItems` uses the code default (1) rather than the authored DAT value
|
||
— risk: bursts of refusals collapse to one visible line where retail shows N.
|
||
- `0x0004 PopUpString` continues to route to chat rather than a modal dialog.
|
||
|
||
---
|
||
|
||
## 8. Open questions / next steps
|
||
|
||
1. **Which LayoutDesc hosts the SpewBox?** Enumerate LayoutDescs in
|
||
`client_local_English.dat` for an element of type `0x10000016`. That yields
|
||
position, extent, and the ListBox's `0x10000028` max-items property.
|
||
2. **What is the line lifetime?** Two options, both cheap:
|
||
(a) dump layout enum `0x10000012` element `0x1000004A` and read the authored
|
||
behaviour; (b) attach cdb to live retail with a breakpoint on
|
||
`gmSpewBoxUI::ListenToElementMessage` @ `0x004D57C0` and on
|
||
`gmSpewBoxUI::RecvNotice_DisplayFinalStringInfo` @ `0x004D60A0`, then spam
|
||
`You can't jump while in the air` and diff the timestamps. Option (b) also
|
||
answers "does it fade or does it pop?" if the item's alpha is sampled.
|
||
3. **What is `0x81C4D8`?** The one chat colour not yet read (types `0x04`/`0x0B`).
|
||
Trivial to grab in the same cdb session (`dd 0x81c4d8 L4`).
|
||
4. **Re-read the five truncated posture/emote literals** from the binary before
|
||
committing them.
|
||
5. Should the plugin `OnChatWindowText` veto hook be part of the acdream plugin
|
||
API? Retail lets a plugin suppress any line before it reaches any sink.
|
||
|
||
---
|
||
|
||
## Appendix A — `HandleFailureEvent` routing table
|
||
|
||
Transcribed from `ClientCommunicationSystem::HandleFailureEvent` @ `0x00571990`
|
||
(339 cases). `Type` is the literal argument passed to
|
||
`ClientSystem::AddTextToScroll`, i.e. the routing decision:
|
||
|
||
- **`0x1A`** → **SpewBox** (transient on-screen), 119 ids
|
||
- **`0x00`** → chat, default/broadcast colour (green), 162 ids
|
||
- **`0x07`** → chat, `Magic` channel (light blue), 58 ids
|
||
|
||
`%s` is the `0x028B` string parameter. Strings are the full binary literals where
|
||
recovery was unambiguous; `[AMBIG n]` marks a truncated prefix that matched *n*
|
||
candidates in the binary (the shortest is shown) and must be re-read before use.
|
||
|
||
| Error id | Type | Text |
|
||
|---|---|---|
|
||
| `0x017` | **0x1A** | You failed to go to non-combat mode. |
|
||
| `0x01D` | **0x1A** | You're too busy! |
|
||
| `0x01E` | **0x1A** | You must control both objects! |
|
||
| `0x020` | **0x1A** | You must control both objects! |
|
||
| `0x023` | **0x1A** | Unable to move to object! |
|
||
| `0x024` | **0x1A** | *(no literal — uses a shared string global; see §4.1)* |
|
||
| `0x026` | **0x1A** | That is not a valid command. |
|
||
| `0x028` | **0x1A** | The item is under someone else's control! |
|
||
| `0x029` | **0x1A** | You cannot pick that up! |
|
||
| `0x02A` | **0x1A** | You are too encumbered to carry that! |
|
||
| `0x02B` | 0x00 | cannot carry anymore.\n |
|
||
| `0x036` | **0x1A** | Action cancelled! |
|
||
| `0x037` | **0x1A** | Unable to move to object! |
|
||
| `0x038` | **0x1A** | Unable to move to object! |
|
||
| `0x039` | **0x1A** | Unable to move to object! |
|
||
| `0x03A` | **0x1A** | You can't do that... you're dead! |
|
||
| `0x03D` | **0x1A** | You charged too far! |
|
||
| `0x03E` | **0x1A** | You are too tired to do that! |
|
||
| `0x048` | **0x1A** | *(no literal — uses a shared string global; see §4.1)* |
|
||
| `0x049` | **0x1A** | *(no literal — uses a shared string global; see §4.1)* |
|
||
| `0x04A` | 0x00 | Ack! You killed yourself!\n |
|
||
| `0x04D` | **0x1A** | Invalid PK status! |
|
||
| `0x04E` | 0x07 | You fail to affect %s because you cannot affect anyone! [AMBIG 4] |
|
||
| `0x050` | 0x07 | You fail to affect %s because beneficial spells do not affect %s! |
|
||
| `0x051` | 0x07 | You fail to affect %s because you cannot affect anyone! [AMBIG 4] |
|
||
| `0x052` | 0x07 | You fail to affect %s because %s is not a player killer! |
|
||
| `0x053` | 0x07 | You fail to affect %s because you cannot affect anyone! [AMBIG 4] |
|
||
| `0x054` | 0x07 | You fail to affect %s because you cannot affect anyone! [AMBIG 4] |
|
||
| `0x3EF` | 0x00 | is not accepting gifts right now. |
|
||
| `0x3F1` | **0x1A** | You failed to go to non-combat mode. |
|
||
| `0x3F7` | **0x1A** | You are too fatigued to attack! |
|
||
| `0x3F8` | **0x1A** | You are out of ammunition! |
|
||
| `0x3F9` | **0x1A** | Your missile attack misfired! |
|
||
| `0x3FA` | **0x1A** | You've attempted an impossible spell path! |
|
||
| `0x3FE` | **0x1A** | You don't know that spell! |
|
||
| `0x3FF` | **0x1A** | Incorrect target type |
|
||
| `0x400` | **0x1A** | You don't have all the components for this spell. |
|
||
| `0x401` | **0x1A** | You don't have enough Mana to cast this spell. |
|
||
| `0x402` | 0x07 | Your spell fizzled.\n |
|
||
| `0x403` | **0x1A** | Your spell's target is missing! |
|
||
| `0x404` | **0x1A** | Your projectile spell mislaunched! |
|
||
| `0x407` | **0x1A** | Your spell cannot be cast outside |
|
||
| `0x40A` | **0x1A** | You are unprepared to cast a spell |
|
||
| `0x40B` | **0x1A** | You've already sworn your Allegiance |
|
||
| `0x40C` | **0x1A** | You don't have enough experience available to swear Allegiance |
|
||
| `0x413` | **0x1A** | %s is already one of your followers |
|
||
| `0x414` | **0x1A** | You are not in an allegiance! |
|
||
| `0x416` | **0x1A** | %s cannot have any more Vassals |
|
||
| `0x41D` | **0x1A** | You must be the leader of a Fellowship |
|
||
| `0x41E` | **0x1A** | Your Fellowship is full |
|
||
| `0x41F` | **0x1A** | That Fellowship name is not permitted |
|
||
| `0x422` | **0x1A** | That channel doesn't exist. |
|
||
| `0x423` | **0x1A** | You can't use that channel. |
|
||
| `0x424` | **0x1A** | You're already on that channel. |
|
||
| `0x425` | **0x1A** | You're not currently on that channel. |
|
||
| `0x427` | **0x1A** | You cannot merge different stacks! |
|
||
| `0x428` | **0x1A** | You cannot merge enchanted items! |
|
||
| `0x429` | **0x1A** | You must control at least one stack! |
|
||
| `0x432` | **0x1A** | Your craft attempt fails. |
|
||
| `0x433` | **0x1A** | Your craft attempt fails. |
|
||
| `0x434` | **0x1A** | Given that number of items, you cannot craft anything. |
|
||
| `0x435` | **0x1A** | Your craft attempt fails. |
|
||
| `0x437` | **0x1A** | Either you or one of the items involved does not pass the requirements for this craft interaction. |
|
||
| `0x438` | **0x1A** | You do not have all the neccessary items. |
|
||
| `0x439` | **0x1A** | Not all the items are avaliable. |
|
||
| `0x43A` | **0x1A** | You must be at rest in peace mode to do trade skills. |
|
||
| `0x43B` | **0x1A** | You are not trained in that trade skill. |
|
||
| `0x43C` | **0x1A** | Your hands must be free. |
|
||
| `0x43D` | 0x07 | You cannot link to that portal!\n |
|
||
| `0x43E` | 0x00 | You have solved this quest too recently! |
|
||
| `0x43F` | 0x00 | You have solved this quest too many times! |
|
||
| `0x445` | 0x00 | This item requires you to complete a specific quest before you can pick it up! |
|
||
| `0x45C` | 0x07 | Player killers may not interact with that portal! |
|
||
| `0x45D` | 0x07 | Non-player killers may not interact with that portal! |
|
||
| `0x45E` | **0x1A** | You do not own a house! |
|
||
| `0x45F` | **0x1A** | You do not own a house! |
|
||
| `0x466` | 0x07 | You must purchase Asheron's Call -- Dark Majesty to use this function. [AMBIG 6] |
|
||
| `0x469` | 0x00 | You have used all the hooks you are allowed to use for this house. |
|
||
| `0x46A` | 0x00 | doesn't know what to do with th… |
|
||
| `0x474` | 0x07 | You must complete a quest to interact with that portal. |
|
||
| `0x47F` | **0x1A** | You must own a house to use this command. |
|
||
| `0x480` | **0x1A** | Your monarch does not own a mansion or a villa! |
|
||
| `0x481` | **0x1A** | Your monarch does not own a mansion or a villa! |
|
||
| `0x482` | **0x1A** | Your monarch has closed the mansion to the Allegiance. |
|
||
| `0x488` | 0x00 | You must be above level %s to purchase this dwelling. |
|
||
| `0x489` | 0x00 | You must be at or below level %s to purchase this dwelling. |
|
||
| `0x48B` | 0x00 | You must be above allegiance rank %s to purchase this dwelling. |
|
||
| `0x48C` | 0x00 | You must be at or below allegiance rank %s to purchase this dwelling. |
|
||
| `0x48E` | **0x1A** | Your offer of Allegiance has been ignored. |
|
||
| `0x48F` | **0x1A** | You are already involved in something! |
|
||
| `0x490` | **0x1A** | You must be a monarch to use this command. |
|
||
| `0x491` | **0x1A** | You must specify a character to boot. [AMBIG 2] |
|
||
| `0x492` | **0x1A** | You can't boot yourself! |
|
||
| `0x493` | **0x1A** | That character does not exist. |
|
||
| `0x494` | **0x1A** | That person is not a member of your Allegiance! |
|
||
| `0x495` | **0x1A** | No patron from which to break! |
|
||
| `0x496` | 0x00 | Your Allegiance has been dissolved! |
|
||
| `0x497` | 0x00 | Your patron's Allegiance to you has been broken! |
|
||
| `0x498` | **0x1A** | You have moved too far! |
|
||
| `0x499` | **0x1A** | That is not a valid destination! |
|
||
| `0x49A` | **0x1A** | You must purchase Asheron's Call -- Dark Majesty to use this function. [AMBIG 6] |
|
||
| `0x49B` | 0x07 | You fail to link with the lifestone! |
|
||
| `0x49C` | 0x07 | You wandered too far to link with the lifestone! |
|
||
| `0x49D` | 0x07 | You successfully link with the lifestone! |
|
||
| `0x49E` | 0x07 | You must have linked with a lifestone in order to recall to it! |
|
||
| `0x49F` | 0x07 | You fail to recall to the lifestone! |
|
||
| `0x4A0` | 0x07 | You fail to link with the portal! |
|
||
| `0x4A1` | 0x07 | You successfully link with the portal! |
|
||
| `0x4A2` | 0x07 | You fail to recall to the portal! |
|
||
| `0x4A3` | 0x07 | You must have linked with a portal in order to summon it! [AMBIG 2] |
|
||
| `0x4A4` | 0x07 | You fail to summon the portal!\n |
|
||
| `0x4A5` | 0x07 | You must have linked with a portal in order to summon it! [AMBIG 2] |
|
||
| `0x4A6` | 0x07 | You fail to teleport!\n |
|
||
| `0x4A7` | 0x07 | You have been teleported too recently! |
|
||
| `0x4A8` | 0x07 | You must be an Advocate to interact with that portal. |
|
||
| `0x4AA` | 0x07 | Players may not interact with that portal. |
|
||
| `0x4AB` | 0x07 | You are not powerful enough to interact with that portal! |
|
||
| `0x4AC` | 0x07 | You are too powerful to interact with that portal! |
|
||
| `0x4AD` | 0x07 | You cannot recall to that portal! |
|
||
| `0x4AE` | 0x07 | You cannot summon that portal!\n |
|
||
| `0x4AF` | **0x1A** | The lock is already unlocked. |
|
||
| `0x4B0` | **0x1A** | You can't lock or unlock that! |
|
||
| `0x4B1` | **0x1A** | You can't lock or unlock what is open! |
|
||
| `0x4B2` | 0x00 | The key doesn't fit this lock.\n |
|
||
| `0x4B3` | **0x1A** | The lock has been used too recently. |
|
||
| `0x4B4` | **0x1A** | You aren't trained in lockpicking! |
|
||
| `0x4B5` | **0x1A** | You must specify a character to boot. [AMBIG 2] |
|
||
| `0x4B6` | **0x1A** | Please use the allegiance panel to view your own information. |
|
||
| `0x4B7` | **0x1A** | You have used that command too recently. |
|
||
| `0x4B8` | 0x00 | You do not own that salvage tool! |
|
||
| `0x4B9` | 0x00 | You do not own that salvage tool! |
|
||
| `0x4BA` | 0x00 | You do not own that salvage tool! |
|
||
| `0x4BD` | 0x00 | You do not own that salvage tool! |
|
||
| `0x4BE` | 0x00 | You do not own that item!\n |
|
||
| `0x4BF` | **0x1A** | The %s was not suitable for salvaging. |
|
||
| `0x4C0` | **0x1A** | The %s contains the wrong material. |
|
||
| `0x4C1` | 0x00 | The material cannot be created.\n |
|
||
| `0x4C2` | 0x00 | The list of items you are attempting to salvage is invalid. |
|
||
| `0x4C3` | 0x00 | You cannot salvage items that you are trading! |
|
||
| `0x4C4` | 0x07 | You must be a guest in this house to interact with that portal. |
|
||
| `0x4C5` | **0x1A** | Your Allegiance Rank is too low to use that item's magic. |
|
||
| `0x4C6` | **0x1A** | You must be %s to use that item's magic. |
|
||
| `0x4C7` | **0x1A** | Your Arcane Lore skill is too low to use that item's magic. |
|
||
| `0x4C8` | **0x1A** | That item doesn't have enough Mana. |
|
||
| `0x4C9` | **0x1A** | Your %s is too low to use that item's magic. |
|
||
| `0x4CA` | **0x1A** | Only %s may use that item's magic. |
|
||
| `0x4CB` | **0x1A** | You must have %s specialized to use that item's magic. |
|
||
| `0x4CC` | 0x07 | You have been involved in a player killer battle too recently to do that! |
|
||
| `0x4CE` | 0x00 | is too busy to accept gifts right now. |
|
||
| `0x4CF` | 0x00 | cannot accept stacked objects. … |
|
||
| `0x4D0` | 0x00 | You have failed to alter your skill. |
|
||
| `0x4D1` | 0x00 | Your %s skill must be trained, not untrained or specialized, in order to be altered in this way! |
|
||
| `0x4D2` | 0x00 | You do not have enough skill credits to specialize your %s skill. |
|
||
| `0x4D3` | 0x00 | You have too many available experience points to be able to absorb the experience points from your %s skill. Please spend some of your experience points and try again. |
|
||
| `0x4D4` | 0x00 | Your %s skill is already untrained! |
|
||
| `0x4D5` | 0x00 | You are currently wielding items which require a certain level of %s. Your %s skill cannot be lowered while you are wielding these items. Please remove these items and try again. [AMBIG 2] |
|
||
| `0x4D6` | 0x00 | You have succeeded in specializing your %s skill! |
|
||
| `0x4D7` | 0x00 | You have succeeded in lowering your %s skill from specialized to trained! |
|
||
| `0x4D8` | 0x00 | You have succeeded in untraining your %s skill! |
|
||
| `0x4D9` | 0x00 | Although you cannot untrain your %s skill, you have succeeded in recovering all the experience you had invested in it. |
|
||
| `0x4DA` | 0x00 | You have too many credits invested in specialized skills already! Before you can specialize your %s skill, you will need to unspecialize some other skill. |
|
||
| `0x4DD` | 0x00 | You have failed to alter your attributes. |
|
||
| `0x4DE` | 0x00 | *(no literal — uses a shared string global; see §4.1)* |
|
||
| `0x4DF` | 0x00 | *(no literal — uses a shared string global; see §4.1)* |
|
||
| `0x4E0` | 0x00 | You are currently wielding items which require a certain level of %s. Your %s skill cannot be lowered while you are wielding these items. Please remove these items and try again. [AMBIG 2] |
|
||
| `0x4E1` | 0x00 | You have succeeded in transferring your attributes! |
|
||
| `0x4E2` | 0x00 | This hook is a duplicated housing object. You may not add items to a duplicated housing object. Please empty the hook and allow it to reset. |
|
||
| `0x4E3` | 0x00 | That item is of the wrong type to be placed on this hook. |
|
||
| `0x4E4` | 0x00 | This chest is a duplicated housing object. You may not add items to a duplicated housing object. Please empty everything -- including backpacks -- out of the chest and allow the chest to reset. |
|
||
| `0x4E5` | 0x00 | This hook was a duplicated housing object. Since it is now empty, it will be deleted momentarily. Once it is gone, it is safe to use the other, non-duplicated hook that is here. |
|
||
| `0x4E6` | 0x00 | This chest was a duplicated housing object. Since it is now empty, it will be deleted momentarily. Once it is gone, it is safe to use the other, non-duplicated chest that is here. |
|
||
| `0x4E7` | 0x00 | You cannot swear allegiance to anyone because you own a monarch-only house. Please abandon your house and try again. |
|
||
| `0x4E9` | 0x00 | The %s cannot be used while on a hook and only the owner may open the hook. [AMBIG 2] |
|
||
| `0x4EA` | 0x00 | The %s can only be used while on a hook. |
|
||
| `0x4EB` | **0x1A** | You can't do that while in the air! |
|
||
| `0x4EC` | 0x00 | You cannot modify your player killer status while you are recovering from a PK death. |
|
||
| `0x4ED` | 0x00 | Advocates may not change their player killer status! |
|
||
| `0x4EE` | 0x00 | Your level is too low to change your player killer status with this object. |
|
||
| `0x4EF` | 0x00 | Your level is too high to change your player killer status with this object. |
|
||
| `0x4F0` | 0x00 | You feel a harsh dissonance, and you sense that an act of killing you have committed recently is interfering with the conversion. |
|
||
| `0x4F1` | 0x00 | Bael'Zharon's power flows through you again. You are once more a player killer. |
|
||
| `0x4F2` | 0x00 | Bael'Zharon has granted you respite after your moment of weakness. You are temporarily no longer a player killer. |
|
||
| `0x4F3` | 0x07 | Lite Player Killers may not interact with that portal! |
|
||
| `0x4F4` | 0x07 | %s fails to affect you because $… |
|
||
| `0x4F5` | 0x07 | %s fails to affect you because y… |
|
||
| `0x4F6` | 0x07 | %s fails to affect you because %… |
|
||
| `0x4F7` | 0x07 | fails to affect you because you… |
|
||
| `0x4F8` | 0x07 | fails to affect you because you… |
|
||
| `0x4F9` | 0x07 | fails to affect you across a ho… |
|
||
| `0x4FA` | 0x07 | is an invalid target.\n |
|
||
| `0x4FB` | 0x07 | You are an invalid target for the spell of %s. |
|
||
| `0x4FC` | **0x1A** | You aren't trained in healing! |
|
||
| `0x4FD` | **0x1A** | You don't own that healing kit! |
|
||
| `0x4FE` | **0x1A** | You can't heal that! |
|
||
| `0x4FF` | **0x1A** | is already at full health! |
|
||
| `0x500` | **0x1A** | You aren't ready to heal! |
|
||
| `0x501` | **0x1A** | You can only use Healing Kits on player characters. |
|
||
| `0x502` | 0x07 | The Lifestone's magic protects you from the attack! |
|
||
| `0x503` | 0x07 | The portal's residual energy protects you from the attack! |
|
||
| `0x504` | 0x00 | You are enveloped in a feeling of warmth as you are brought back into the protection of the Light. You are once again a Non-Player Killer. |
|
||
| `0x505` | **0x1A** | You're too close to your sanctuary! |
|
||
| `0x506` | **0x1A** | You can't do that -- you're trading! |
|
||
| `0x507` | 0x00 | Only Non-Player Killers may enter PK Lite. Please see @help pklite for more details about this command. |
|
||
| `0x508` | 0x00 | A cold wind touches your heart. You are now a Player Killer Lite. |
|
||
| `0x509` | 0x07 | has no appropriate targets equi… |
|
||
| `0x50A` | 0x07 | You have no appropriate targets equipped for %s's spell. |
|
||
| `0x50B` | 0x00 | is now an open fellowship; anyo… |
|
||
| `0x50C` | 0x00 | is now a closed fellowship.\n |
|
||
| `0x50D` | 0x00 | is now the leader of this fello… |
|
||
| `0x50E` | 0x00 | You have passed leadership of the fellowship to %s |
|
||
| `0x50F` | **0x1A** | You do not belong to a Fellowship. |
|
||
| `0x510` | 0x00 | You may not hook any more %s on your house. You already have the maximum number of %s hooked or you are not permitted to hook any on your type of house. |
|
||
| `0x512` | 0x00 | You are now using the maximum number of hooks. You cannot use another hook until you take an item off one of your hooks. |
|
||
| `0x513` | 0x00 | You are no longer using the maximum number of hooks. You may again add items to your hooks. |
|
||
| `0x514` | 0x00 | You now have the maximum number of %s hooked. You cannot hook any additional %s until you remove one or more from your house. |
|
||
| `0x515` | 0x00 | You no longer have the maximum number of %s hooked. You may hook additional %s. |
|
||
| `0x516` | 0x00 | You are not permitted to use that hook. |
|
||
| `0x517` | 0x00 | is not close enough to your lev… |
|
||
| `0x518` | 0x00 | cannot be recruited into the fe… |
|
||
| `0x519` | 0x00 | The fellowship is locked, you were not added to the fellowship. |
|
||
| `0x51A` | **0x1A** | Only the original owner may use that item's magic. |
|
||
| `0x51B` | 0x00 | You have entered the %s channel. |
|
||
| `0x51C` | 0x00 | You have left the %s channel.\n |
|
||
| `0x51E` | 0x00 | will not receive your message, please use urgent assistance to speak with an in-game representative |
|
||
| `0x51F` | **0x1A** | Message Blocked: %s |
|
||
| `0x520` | 0x00 | You cannot add anymore people to the list of players that you can hear. |
|
||
| `0x521` | 0x00 | has been added to the list of p… |
|
||
| `0x522` | 0x00 | has been removed from the list … |
|
||
| `0x523` | 0x00 | You are now deaf to player's screams. |
|
||
| `0x524` | 0x00 | You can hear all players once again. |
|
||
| `0x525` | 0x00 | You fail to remove %s from your loud list. |
|
||
| `0x526` | **0x1A** | You chicken out. |
|
||
| `0x527` | **0x1A** | You cannot posssibly succeed. |
|
||
| `0x528` | 0x00 | The fellowship is locked; you cannot open locked fellowships. |
|
||
| `0x529` | **0x1A** | Trade Complete! |
|
||
| `0x52A` | **0x1A** | That is not a salvaging tool. |
|
||
| `0x52B` | **0x1A** | That person is not available now. |
|
||
| `0x52C` | 0x00 | You are now snooping on %s.\n |
|
||
| `0x52D` | 0x00 | You are no longer snooping on %s. |
|
||
| `0x52E` | 0x00 | You fail to snoop on %s.\n |
|
||
| `0x52F` | 0x00 | %s attempted to snoop on you.\n |
|
||
| `0x530` | 0x00 | %s is already being snooped on, … |
|
||
| `0x531` | 0x00 | %s is in limbo and cannot receive your message. |
|
||
| `0x532` | 0x00 | You must wait 30 days after purchasing a house before you may purchase another with any character on the same account. This applies to all housing except apartments. |
|
||
| `0x533` | 0x00 | You have been booted from your allegiance chat room. Use "@allegiance chat on" to rejoin. (%s). |
|
||
| `0x534` | 0x00 | %s has been booted from the alle… |
|
||
| `0x535` | 0x00 | You do not have the authority within your allegiance to do that. |
|
||
| `0x536` | 0x00 | The account of %s is already banned from the allegiance. |
|
||
| `0x537` | 0x00 | The account of %s is not banned from the allegiance. |
|
||
| `0x538` | 0x00 | The account of %s was not unbanned from the allegiance. |
|
||
| `0x539` | 0x00 | The account of %s has been banned from the allegiance. |
|
||
| `0x53A` | 0x00 | The account of %s is no longer banned from the allegiance. |
|
||
| `0x53B` | 0x00 | Banned Characters: |
|
||
| `0x53E` | 0x00 | %s is banned from the allegiance… |
|
||
| `0x53F` | 0x00 | You are banned from %s's allegiance! |
|
||
| `0x540` | 0x00 | You have the maximum number of accounts banned.! |
|
||
| `0x541` | 0x00 | %s is now an allegiance officer.… |
|
||
| `0x542` | 0x00 | An unspecified error occurred while attempting to set %s as an allegiance officer. [AMBIG 2] |
|
||
| `0x543` | 0x00 | %s is no longer an allegiance of… |
|
||
| `0x544` | 0x00 | An unspecified error occurred while attempting to set %s as an allegiance officer. [AMBIG 2] |
|
||
| `0x545` | 0x00 | You already have the maximum number of allegiance officers. You must remove some before you add any more. |
|
||
| `0x546` | 0x00 | Your allegiance officers have been cleared. |
|
||
| `0x547` | 0x00 | You must wait %s before communicating again! |
|
||
| `0x548` | 0x00 | You cannot join any chat channels while gagged. |
|
||
| `0x549` | 0x00 | Your allegiance officer status has been modified. You now hold the position of: %s. |
|
||
| `0x54A` | 0x00 | You are no longer an allegiance officer. |
|
||
| `0x54B` | 0x00 | %s is already an allegiance offi… |
|
||
| `0x54C` | 0x00 | Your allegiance does not have a hometown. |
|
||
| `0x54D` | **0x1A** | The %s is currently in use.\n |
|
||
| `0x54E` | 0x00 | The hook does not contain a usable item. Use the '@house hooks on'command to make the hook openable. [AMBIG 2] |
|
||
| `0x54F` | 0x00 | The hook does not contain a usable item. Use the '@house hooks on'command to make the hook openable. [AMBIG 2] |
|
||
| `0x550` | **0x1A** | Out of Range! |
|
||
| `0x551` | 0x00 | You are not listening to the %s channel! |
|
||
| `0x552` | **0x1A** | You must purchase Asheron's Call -- Dark Majesty to use this function. [AMBIG 6] |
|
||
| `0x553` | **0x1A** | You must purchase Asheron's Call -- Dark Majesty to use this function. [AMBIG 6] |
|
||
| `0x554` | **0x1A** | You must purchase Asheron's Call -- Dark Majesty to use this function. [AMBIG 6] |
|
||
| `0x555` | **0x1A** | You must purchase Asheron's Call -- Dark Majesty to use this function. [AMBIG 6] |
|
||
| `0x556` | 0x00 | You have failed to complete the augmentation. |
|
||
| `0x557` | 0x00 | You have used this augmentation too many times already. |
|
||
| `0x558` | 0x00 | You have used augmentations of this type too many times already. |
|
||
| `0x559` | 0x00 | You do not have enough unspent experience available to purchase this augmentation. |
|
||
| `0x55A` | 0x00 | *(no literal — uses a shared string global; see §4.1)* |
|
||
| `0x55B` | 0x00 | Congratulations! You have succeeded in acquiring the %s augmentation. |
|
||
| `0x55C` | 0x00 | Although your augmentation will not allow you to untrain your %s skill, you have succeeded in recovering all the experience you had invested in it. |
|
||
| `0x55D` | 0x00 | You must exit the Training Academy before that command will be available to you. |
|
||
| `0x55E` | 0x00 | *(no literal — uses a shared string global; see §4.1)* |
|
||
| `0x55F` | 0x00 | Only Player Killer characters may use this command! |
|
||
| `0x560` | 0x00 | Only Player Killer Lite characters may use this command! |
|
||
| `0x561` | **0x1A** | You may only have a maximum of 50 friends at once. If you wish to add more friends, you must first remove some. |
|
||
| `0x562` | 0x00 | %s is already on your friends li… |
|
||
| `0x563` | 0x00 | That character is not on your friends list! |
|
||
| `0x564` | 0x00 | Only the character who owns the house may use this command. |
|
||
| `0x565` | 0x00 | That allegiance name is invalid because it is empty. Please use the @allegiance name clear command to clear your allegiance name. |
|
||
| `0x566` | 0x00 | That allegiance name is too long. Please choose another name. |
|
||
| `0x567` | 0x00 | That allegiance name contains illegal characters. Please choose another name using only letters, spaces, - and '. |
|
||
| `0x568` | 0x00 | That allegiance name is not appropriate. Please choose another name. |
|
||
| `0x569` | 0x00 | That allegiance name is already in use. Please choose another name. |
|
||
| `0x56A` | 0x00 | You may only change your allegiance name once every 24 hours. You may change your allegiance name again in %s. |
|
||
| `0x56B` | 0x00 | Your allegiance name has been cleared. |
|
||
| `0x56C` | 0x00 | That is already the name of your allegiance! |
|
||
| `0x56D` | 0x00 | %s is the monarch and cannot be … |
|
||
| `0x56E` | 0x00 | That level of allegiance officer is now known as: %s. |
|
||
| `0x56F` | 0x00 | That is an invalid officer level. |
|
||
| `0x570` | 0x00 | That allegiance officer title is not appropriate. |
|
||
| `0x571` | 0x00 | That allegiance name is too long. Please choose another name. |
|
||
| `0x572` | 0x00 | All of your allegiance officer titles have been cleared. |
|
||
| `0x573` | 0x00 | That allegiance title contains illegal characters. Please choose another name using only letters, spaces, - and '. |
|
||
| `0x574` | 0x00 | Your allegiance is currently: %s. |
|
||
| `0x575` | 0x00 | Your allegiance is now: %s.\n |
|
||
| `0x576` | 0x00 | You may not accept the offer of allegiance from %s because your allegiance is locked. |
|
||
| `0x577` | 0x00 | You may not swear allegiance at this time because the allegiance of %s is locked. |
|
||
| `0x578` | 0x00 | You have pre-approved %s to join your allegiance. |
|
||
| `0x579` | 0x00 | You have not pre-approved any vassals to join your allegiance. |
|
||
| `0x57A` | 0x00 | %s is already a member of your a… |
|
||
| `0x57B` | 0x00 | %s has been pre-approved to join… |
|
||
| `0x57C` | 0x00 | You have cleared the pre-approved vassal for your allegiance. |
|
||
| `0x57D` | 0x00 | That character is already gagged! |
|
||
| `0x57E` | 0x00 | That character is not currently gagged! |
|
||
| `0x57F` | 0x00 | Your allegiance chat privileges have been restored. [AMBIG 3] |
|
||
| `0x580` | 0x00 | %s is now temporarily unable to … |
|
||
| `0x581` | 0x00 | Your allegiance chat privileges have been restored. [AMBIG 3] |
|
||
| `0x582` | 0x00 | Your allegiance chat privileges have been restored. [AMBIG 3] |
|
||
| `0x583` | 0x00 | You have restored allegiance chat privileges to %s. |
|
||
| `0x584` | **0x1A** | You cannot pick up more of that item! |
|
||
| `0x585` | **0x1A** | You are restricted to clothes and armor created for your race. |
|
||
| `0x586` | **0x1A** | That item was specifically created for another race. |
|
||
| `0x587` | 0x07 | Olthoi cannot interact with that! |
|
||
| `0x588` | 0x07 | Olthoi cannot use regular lifestones! Asheron would not allow it! |
|
||
| `0x589` | 0x07 | The vendor looks at you in horror! |
|
||
| `0x58A` | 0x00 | %s cowers from you!\n |
|
||
| `0x58B` | 0x07 | As a mindless engine of destruction an Olthoi cannot join a fellowship! |
|
||
| `0x58C` | 0x07 | The Olthoi only have an allegiance to the Olthoi Queen! |
|
||
| `0x58D` | 0x07 | You cannot use that item!\n |
|
||
| `0x58E` | 0x07 | This person will not interact with you! |
|
||
| `0x58F` | 0x07 | Only Olthoi may pass through this portal! |
|
||
| `0x590` | 0x07 | Olthoi may not pass through this portal! |
|
||
| `0x591` | 0x07 | You may not pass through this portal while Vitae weakens you! |
|
||
| `0x592` | 0x07 | This character must be two weeks old or have been created on an account at least two weeks old to use this portal! |
|
||
| `0x593` | 0x07 | Olthoi characters can only use Lifestone and PK Arena recalls! |
|
||
|
||
---
|
||
|
||
## Appendix B — text types seen in this build
|
||
|
||
| Type | ACE `ChatMessageType` | Notes |
|
||
|---|---|---|
|
||
| `0x00` | `Broadcast` | default colour (green) |
|
||
| `0x01` | `AllChannels` | |
|
||
| `0x02` | `Speech` | white |
|
||
| `0x03` | `Tell` | yellow |
|
||
| `0x04` | `OutgoingTell` | |
|
||
| `0x05` | `System` | bright purple |
|
||
| `0x06` | `Combat` | dark red |
|
||
| `0x07` | `Magic` | light blue — the spell/portal failure family |
|
||
| `0x08` `0x09` | `Channel` / `ChannelSend` | pink |
|
||
| `0x0A` `0x0B` | `Social` / `SocialSend` | |
|
||
| `0x0C` | `Emote` | grey |
|
||
| `0x0D` | `Advancement` | cyan |
|
||
| `0x0E` | `Abuse` | |
|
||
| `0x0F` | `Help` | dark red |
|
||
| `0x10` | `Appraisal` | |
|
||
| `0x11` | `Spellcasting` | light blue |
|
||
| `0x12` | `Allegiance` | orange |
|
||
| `0x13` | `Fellowship` | yellow |
|
||
| `0x14` | `WorldBroadcast` | |
|
||
| `0x15` `0x16` | `CombatEnemy` / `CombatSelf` | |
|
||
| `0x17` | `Recall` | |
|
||
| `0x18` `0x19` | `Craft` / `Salvaging` | |
|
||
| **`0x1A`** | *commented out in ACE* | **SpewBox** |
|
||
| `0x1B`–`0x1E` | unnamed in ACE | blue-grey |
|
||
| `0x1F` | `AdminTell` | yellow |
|
||
| `0x20` `0x21` | **absent from ACE** | coloured by the client (blue-grey / orange) |
|