acdream/docs/research/deepdives/r10-quest-dialogs.md
Erik 3f913f1999 docs+feat: 13 retail-AC deep-dives (R1-R13) + C# port scaffolds + roadmap E-H
78,000 words of grounded, citation-backed research across 13 major AC
subsystems, produced by 13 parallel Opus-4.7 high-effort agents. Plus
compact C# port scaffolds for the top-5 systems and a phase-E-through-H
roadmap update sequencing the work.

Research (docs/research/deepdives/):
- 00-master-synthesis.md          (navigation hub + dependency graph)
- r01-spell-system.md        5.4K words (fizzle sigmoid, 8 tabs, 0x004A wire)
- r02-combat-system.md       5.9K words (damage formula, crit, body table)
- r03-motion-animation.md    8.2K words (450+ commands, 27 hook types)
- r04-vfx-particles.md       5.8K words (13 ParticleType, PhysicsScript)
- r05-audio-sound.md         5.6K words (DirectSound 8, CPU falloff)
- r06-items-inventory.md     7.4K words (ItemType flags, EquipMask 31 slots)
- r07-character-creation.md  6.3K words (CharGen dat, 13 heritages)
- r08-network-protocol-atlas 9.7K words (63+149+94 opcodes mapped)
- r09-dungeon-portal-space.md 6.3K words (EnvCell, PlayerTeleport flow)
- r10-quest-dialogs.md       7.1K words (emote-script VM, 122 actions)
- r11-allegiance.md          5.4K words (tree + XP passup + 5 channels)
- r12-weather-daynight.md    4.5K words (deterministic client-side)
- r13-dynamic-lighting.md    4.9K words (8-light cap, hard Range cutoff)

Every claim cites a FUN_ address, ACE file path, DatReaderWriter type,
or holtburger/ACViewer reference. The master synthesis ties them into a
dependency graph and phase sequence.

Key architectural finding: of 94 GameEvents in the 0xF7B0 envelope,
ZERO are handled today — that's the largest network-protocol gap and
blocks F.2 (items) + F.5 (panels) + H.1 (chat).

C# scaffolds (src/AcDream.Core/):
- Items/ItemInstance.cs    — ItemType/EquipMask enums, ItemInstance,
                             Container, PropertyBundle, BurdenMath
- Spells/SpellModel.cs      — SpellDatEntry, SpellComponentEntry,
                             SpellCastStateMachine, ActiveBuff,
                             SpellMath (fizzle sigmoid + mana cost)
- Combat/CombatModel.cs     — CombatMode/AttackType/DamageType/BodyPart,
                             DamageEvent record, CombatMath (hit-chance
                             sigmoids, power/accuracy mods, damage formula),
                             ArmorBuild
- Audio/AudioModel.cs       — SoundId enum, SoundEntry, WaveData,
                             IAudioEngine / ISoundCache contracts,
                             AudioFalloff (inverse-square)
- Vfx/VfxModel.cs           — 13 ParticleType integrators, EmitterDesc,
                             PhysicsScript + hooks, Particle struct,
                             ParticleEmitter, IParticleSystem contract

All Core-layer data models; platform-backed engines live in AcDream.App.
Compiles clean; 470 tests still pass.

Roadmap (docs/plans/2026-04-11-roadmap.md):
- Phase E — "Feel alive": motion-hooks + audio + VFX
- Phase F — Fight + cast + gear: GameEvent dispatch, inventory,
            combat, spell, core panels
- Phase G — World systems: sky/weather, dynamic lighting, dungeons
- Phase H — Social + progression: chat, allegiance, quests, char creation
- Phase J — Long-tail (renumbered from old Phase E)

Quick-lookup table updated with 10+ new rows mapping observations to
new phase letters.
2026-04-18 10:32:44 +02:00

1326 lines
57 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# R10 — Quest System, NPC Dialogs, and Emote Scripts
**Status:** Deep-dive research, 2026-04-18. Author: R10 research agent.
**Scope:** Everything that connects a player talking to an NPC with a change
in the player's persistent state — quest flags, quest timers, emote scripts,
item turn-ins, dialog text, and the client-side rendering of all of the
above.
Every AC-specific behavior in this document is anchored to one of:
- `docs/research/decompiled/` — decompiled retail client (ground truth for
client-side behavior)
- `references/ACE/Source/ACE.Server/` — ACEmulator server (authority on
server-side quest / emote evaluation and on the wire protocol the server
sends to the client)
- `references/DatReaderWriter/` + `references/ACE/Source/ACE.DatLoader/`
the dat files the client reads (`ChatEmoteData`, `Contract`,
`ContractTable`)
- `references/holtburger/crates/holtburger-protocol/` — Rust reference
client's packed/unpacked byte layouts (cross-check on the wire format
for `Tell`, `HearSpeech`, `EmoteText`, `SoulEmote`, `PopupString`)
The short summary: AC has no "quest system" in the sense a modern MMO has.
Quests are a *convention on top of emote scripts*. Every "quest" that
exists in retail is expressed as a tree of emote actions hanging off
weenies (NPCs, items, portals), together with entries in a world-wide
`Quest` table (min delta / max solves) and a per-character quest registry
(last completed time, number of completions). The "quest tracker" UI is
not an independent quest system; it is a presentation layer over the
`Contract` dat table, which in turn points at plain quest-flag names that
the emote engine reads and writes. Everything comes back to the emote
engine.
---
## 1. Quest flag system
### 1.1 Per-character storage
A character's quest state is a set of `CharacterPropertiesQuestRegistry`
rows (see
`references/ACE/Source/ACE.Database/Models/Shard/CharacterPropertiesQuestRegistry.cs`):
```csharp
public partial class CharacterPropertiesQuestRegistry
{
public uint CharacterId { get; set; } // character GUID
public string QuestName { get; set; } // e.g. "arwicemayortalk"
public uint LastTimeCompleted { get; set; } // Unix time (seconds)
public int NumTimesCompleted { get; set; } // solve count
}
```
Key observations:
- **One row per (character, questName).** "Quest flag" in retail
terminology means a row in this table. An NPC checks "does the player
have quest X?" by asking "does a row with questName=X exist for this
char?"
- **`NumTimesCompleted` is overloaded.** For a simple stage flag it is
treated as a 0/non-zero presence test. For a kill-task or kill-counter
(e.g. "GolemKillTask@#kt"), it's a literal count. For a "bit-field"
quest flag, the integer is a 32-bit bitmask and emotes manipulate
specific bits via `SetQuestBitsOn` / `SetQuestBitsOff` / `InqQuestBitsOn`
/ `InqQuestBitsOff` (see section 3). That's the "BitField (bool quest
flags)" notion — a single `NumTimesCompleted` row can store up to 32
boolean sub-flags.
- **`LastTimeCompleted`** drives the "quest stamp" — timestamp + cooldown.
Combined with the world-quest-table `MinDelta`, it answers "can the
player solve this quest again yet?"
### 1.2 World quest table (server-only)
The world (not per-character) has a `Quest` table
(`references/ACE/Source/ACE.Database/Models/World/Quest.cs`):
```csharp
public partial class Quest
{
public uint Id { get; set; }
public string Name { get; set; } // e.g. "arwicemayortalk"
public uint MinDelta { get; set; } // seconds between solves
public int MaxSolves { get; set; } // -1 = unlimited, >0 = cap
public string Message { get; set; } // unused in live
}
```
Only the server needs this table. The client is never shown `MinDelta`
or `MaxSolves` directly — it sees them via the emote-engine
substitutions `%tqt` (time-to-solve), `%tqm` (max solves), `%tqc`
(current solves) baked into NPC dialog strings (see section 3.4
"string replacement"), and via the Contract-tracker UI (section 9).
### 1.3 What the client actually stores
**The retail client never stores quest flags.** Quest state lives entirely
server-side. The client only learns about a quest:
- Indirectly, via NPC dialog strings (Tell/Say/TextDirect) whose text has
been server-side-formatted with `%tqc`/`%tqt` etc.
- Directly, via the Contract tracker messages
(`SendClientContractTrackerTable` 0x0314 and
`SendClientContractTracker` 0x0315). These carry a ContractId + stage +
time-when-done / time-when-repeats; they *do not* send the raw quest
flag name or completion count. See section 9.
- As error toasts — e.g. "You have solved this quest too recently!"
(decompiled at `chunk_00570000.c:1629`, game-event case `0x43e`). This
is a generic error string, the client does not know the quest name
that was involved.
So: for acdream, we do not need a client-side QuestRegistry store.
We need: (a) a Contract tracker store (short-lived, server pushes full
state), and (b) a generic chat log that can render Tell/Say/EmoteText
and the embedded `<Tell:IIDString:...>NAME<\Tell>` clickable-name markup.
### 1.4 Encoding in the save blob
"Save blob" here means the shard-database row, not a client-side file.
The client has no save file for quest state. Server-side, see the
QuestSQLWriter at
`references/ACE/Source/ACE.Database/SQLFormatters/World/QuestSQLWriter.cs`
for world quests and plain EF models for per-character quests. acdream's
server (Phase 7+) would follow ACE's shape.
---
## 2. Quest manager — opcodes and wire events
The server never sends a "quest updated" push to the client in the
generic case. Instead, two categories of server → client message carry
quest-related information:
### 2.1 Generic "game event" errors
When a quest-gated action fails, the server sends a `GameEventWeenieError`
(opcode 0x028A) or `GameEventInventoryServerSaveFailed` (opcode 0x00A0)
with a WeenieError code. The client maps the code to a hard-coded wide
string and displays it in chat. The decompiled client's dispatcher is in
`chunk_00570000.c` around line 15002900. Selected cases:
| Code | String | Source |
|--------|------------------------------------------------------------------------------|--------|
| 0x043E | `You have solved this quest too recently!\n` | `chunk_00570000.c:1629` |
| 0x043F | `You have solved this quest too many times!\n` | `chunk_00570000.c:1633` |
| 0x0445 | `This item requires you to complete a specific quest before you can pick it up!\n` | `chunk_00570000.c:1637` |
| 0x0474 | `You must complete a quest to interact with that portal.\n` | `chunk_00570000.c:1673` |
| 0x0555 | `You must purchase Asheron's Call -- Throne of Destiny to access this quest.`| `chunk_00570000.c:2811` |
These are purely display. There is no structured "quest state" in them.
### 2.2 Contract tracker events (the real "quest tracker" UI)
Two opcodes in the `GameEventType` enum
(`references/ACE/Source/ACE.Server/Network/GameEvent/GameEventType.cs`):
- `SendClientContractTrackerTable = 0x0314` — full replacement of the
tracker panel. Body is a packed list of ContractTracker structs.
- `SendClientContractTracker = 0x0315` — single contract update
(added / changed / deleted). Body: one ContractTracker struct +
`DeleteContract` bool + `SetAsDisplayContract` bool.
Structure written by server (`ContractTracker.Write` at
`references/ACE/Source/ACE.Server/Network/Structure/ContractTracker.cs:137`):
```text
uint32 Version // Contract.Version from dat
uint32 ContractId // key into ContractTable
uint32 Stage // ContractStage enum, see below
double TimeWhenDone // seconds until current cooldown ends
double TimeWhenRepeats // seconds until repeat cooldown ends
```
`ContractStage`:
```csharp
Available = 0x1,
InProgress = 0x2,
DoneOrPendingRepeat = 0x3,
ProgressCounter = 0x4, // add N for N progress steps done
```
The ContractTracker is the client's only structured view of any
quest-like state. It does NOT contain the quest flag name itself — the
flag name is in the Contract dat (see section 9), keyed by ContractId.
The client looks up the Contract in `ContractTable`, reads
`QuestflagStarted`, `QuestflagFinished`, etc., and uses those to
decorate the UI. It does not read or write quest flags directly.
### 2.3 Quest flag write notifications
There is no such opcode. Quest flags are written server-side in
`EmoteManager.ExecuteEmote` via `QuestManager.Update` /
`SetQuestCompletions` / `Erase` etc. The only notification the client
receives is:
1. A contract tracker refresh (if the changed quest flag is referenced
by a Contract in the dat).
2. Any Tell / Say / DirectBroadcast / Sound emote action that happened
to be sequenced after the quest write — this is how NPCs "confirm"
a quest step to the player.
This is why the retail client feels so non-MMO: the server tells the
player "you've done it" by scripting the NPC to say "Here is your
reward, friend," not by emitting a structured quest-progress event.
---
## 3. Emote system — the retail mini-language
An "emote" in AC retail terminology is **not** a chat action like `/wave`.
It's a scripted response a weenie (NPC, item, portal, generator) can run
when some trigger fires. An **emote set** is an ordered list of
**emote actions** tagged with a **category** (the trigger) and optional
filters (quest name, vendor type, weenie class ID, motion style, health
threshold).
Player-visible chat-command emotes like `/wave` are a separate thing —
they are handled by `GameActionEmote` (opcode 0x01E0) and
`GameActionSoulEmote`, which simply broadcast `GameMessageEmoteText` /
`GameMessageSoulEmote` to everyone in range. No scripting involved.
### 3.1 EmoteCategory — the trigger types (39 values)
From `references/ACE/Source/ACE.Entity/Enum/EmoteCategory.cs`:
| Value | Name | When it fires |
|-------|-----------------------------|---------------|
| 0 | Invalid | — |
| 1 | Refuse | NPC "examines" an item given to it but doesn't consume it |
| 2 | Vendor | Vendor window lifecycle events (Open, Close, etc.) |
| 3 | Death | The weenie is killed |
| 4 | Portal | Player activates a portal (post travel) |
| 5 | HeartBeat | Periodic tick (ambient speech, homesickness) |
| 6 | Give | Player gives the NPC an item (quest turn-in) |
| 7 | Use | Player right-clicks / uses the weenie |
| 8 | Activation | Chained Activate from a switch/lever |
| 9 | Generation | A generator spawns something |
| 10 | PickUp | Item is picked up |
| 11 | Drop | Item is dropped |
| 12 | QuestSuccess | InqQuest / InqMyQuest / InqFellowQuest → true |
| 13 | QuestFailure | same → false |
| 14 | Taunt | Combat alert chatter |
| 15 | WoundedTaunt | Taunt filtered by health range |
| 16 | KillTaunt | Fires when this creature kills a player |
| 17 | NewEnemy | First acquires a player target |
| 18 | Scream | Panic chatter |
| 19 | Homesick | Displaced from home |
| 20 | ReceiveCritical | Took a crit hit |
| 21 | ResistSpell | Resisted a magic attack |
| 22 | TestSuccess | Inq*Stat passed |
| 23 | TestFailure | Inq*Stat failed |
| 24 | HearChat | Nearby player spoke a keyword |
| 25 | Wield | Item equipped |
| 26 | UnWield | Item unequipped |
| 27 | EventSuccess | InqEvent → started |
| 28 | EventFailure | InqEvent → not started |
| 29 | TestNoQuality | Inq*Stat on a stat the target doesn't have |
| 30 | QuestNoFellow | InqFellowQuest when player has no fellowship |
| 31 | TestNoFellow | same semantics for arbitrary tests |
| 32 | GotoSet | Named sub-routine target for `Goto` |
| 33 | NumFellowsSuccess | InqFellowNum passed |
| 34 | NumFellowsFailure | InqFellowNum failed |
| 35 | NumCharacterTitlesSuccess | InqNumCharacterTitles passed |
| 36 | NumCharacterTitlesFailure | same failed |
| 37 | ReceiveLocalSignal | LocalSignal broadcast received |
| 38 | ReceiveTalkDirect | Someone used /tell keyword on this NPC |
### 3.2 EmoteType — the action types (122 values)
From `references/ACE/Source/ACE.Entity/Enum/EmoteType.cs`. This is the
instruction-set of the emote mini-language. Grouped by purpose:
**Speech & UI output:**
- `Say (8)` — NPC speaks, broadcast to all nearby (HearSpeech / HearRangedSpeech)
- `Tell (10)` — NPC whispers directly to the player (GameEventTell 0x02BD)
- `Act (1)` — "acting" text, broadcast system chat ("Bob waves at you")
- `TextDirect (13)` — direct broadcast to player's chat window (GameMessageSystemChat)
- `DirectBroadcast (18)` — same as TextDirect, reliable
- `LocalBroadcast (17)` — broadcast in area
- `WorldBroadcast (16)` — broadcast to all players on server
- `FellowBroadcast (65)` — broadcast to player's fellowship
- `TellFellow (64)` — Tell delivered to all fellowship members
- `AdminSpam (26)` — log-channel broadcast
- `BLog (25)` — server log (not client-visible)
- `PopUp (68)` — modal popup window (GameEventPopupString 0x0004)
- `Sound (9)` — play a sound effect (GameMessageSound)
- `PhysScript (7)` — particle / visual effect (GameMessagePlayParticleEffect)
**Motion & movement:**
- `Motion (5)` — play animation on this NPC
- `ForceMotion (52)` — play animation on the player target
- `Move (6)` — walk to home-relative offset
- `MoveHome (4)` — walk to home position
- `MoveToPos (87)` — walk to absolute position
- `Turn (11)` — turn to heading
- `TurnToTarget (12)` — face the player
- `ResetHomePosition (57)` — snap home to current
- `SetSanctuaryPosition (63)` — store player's /recall target
**Quest flag ops (the core of quest scripting):**
- `UpdateQuest (20)` / `UpdateMyQuest (79)` — add quest if new, stamp if old, branch on success
- `InqQuest (21)` / `InqMyQuest (80)` / `InqFellowQuest (58)` — branch on "has and is on cooldown"
- `StampQuest (22)` / `StampMyQuest (81)` / `StampFellowQuest (61)` — update LastTimeCompleted (unconditional)
- `IncrementQuest (33)` / `IncrementMyQuest (85)` — NumTimesCompleted += amount
- `DecrementQuest (32)` / `DecrementMyQuest (84)` — NumTimesCompleted -= amount
- `EraseQuest (31)` / `EraseMyQuest (83)` — delete the quest row entirely
- `SetQuestCompletions (70)` / `SetMyQuestCompletions (86)` — overwrite NumTimesCompleted
- `InqQuestSolves (30)` / `InqMyQuestSolves (82)` — branch on N in [min, max]
- `InqQuestBitsOn (102)` / `InqQuestBitsOff (103)` / `InqMyQuestBitsOn (104)` / `InqMyQuestBitsOff (105)` — branch on bitmask
- `SetQuestBitsOn (106)` / `SetQuestBitsOff (107)` / `SetMyQuestBitsOn (108)` / `SetMyQuestBitsOff (109)` — OR / AND-NOT bits
- `InqFellowNum (59)` — branch on fellowship size
- `UpdateFellowQuest (60)` — fellowship-wide UpdateQuest
The `MyQuest` variants target the creature executing the emote (NPC-local
state); the non-`My` variants target the player interacting with it.
This is how a single statue can "remember" that it has been touched by
this specific player without flooding the central quest table.
**Stat inquiries (all branch into TestSuccess / TestFailure / TestNoQuality):**
- `InqIntStat (36)`, `InqInt64Stat (114)`, `InqFloatStat (37)`, `InqBoolStat (35)`, `InqStringStat (38)`
- `InqAttributeStat (39)` / `InqRawAttributeStat (40)`
- `InqSecondaryAttributeStat (41)` / `InqRawSecondaryAttributeStat (42)` (vitals)
- `InqSkillStat (43)` / `InqRawSkillStat (44)` / `InqSkillTrained (45)` / `InqSkillSpecialized (46)`
- `InqNumCharacterTitles (71)`
- `InqPackSpace (89)` — branch on free inventory slots
- `InqOwnsItems (76)` — branch on "player has WCID × N"
- `InqContractsFull (121)`
- `InqEvent (51)` — branch on world event started
- `InqYesNo (75)` — raise a confirmation dialog (section 5)
**Stat writes:**
- `SetBoolStat (69)`, `SetIntStat (53)`, `SetInt64Stat (115)`, `SetFloatStat (118)`
- `IncrementIntStat (54)` / `DecrementIntStat (55)`
**Awards:**
- `AwardXP (2)`, `AwardNoShareXP (62)`, `AwardLevelProportionalXP (49)`
- `AwardSkillXP (28)`, `AwardSkillPoints (29)`, `AwardLevelProportionalSkillXP (50)`
- `AwardLuminance (113)` / `SpendLuminance (112)`
- `AwardTrainingCredits (47)`
- `AddCharacterTitle (34)`
- `TeachSpell (27)` — add a spell to player's spellbook
- `AddContract (119)` / `RemoveContract (120)` — add/remove a quest-tracker entry
**Actions on the weenie itself:**
- `Generate (72)` — trigger a generator to spawn
- `CreateTreasure (56)` — spawn loot into player's pack
- `Give (3)` — give a specific WCID item to the player
- `TakeItems (74)` — consume items from player's pack
- `DeleteSelf (77)` — remove the weenie
- `KillSelf (78)` — smite the weenie
- `OpenMe (116)` / `CloseMe (117)` — container/door
- `Activate (15)` — chain-activate linked object
- `CastSpell (14)` / `CastSpellInstant (19)` / `PetCastSpellOnOwner (73)`
**Teleport / positioning:**
- `Goto (67)` — jump to named sub-set within same emote-set (sub-routine call)
- `StartEvent (23)` / `StopEvent (24)` — world event control
- `LocalSignal (88)` — broadcast signal that other weenies' `ReceiveLocalSignal` emotes can listen for
- `TeleportSelf (100)` — NPC teleports (unused in retail)
- `TeleportTarget (99)` — player teleports
- `InflictVitaePenalty (48)` / `RemoveVitaePenalty (90)`
**Character appearance (barber):**
- `SetEyeTexture (91)` / `SetEyePalette (92)` / `SetNoseTexture (93)` / `SetNosePalette (94)` /
`SetMouthTexture (95)` / `SetMouthPalette (96)` / `SetHeadObject (97)` / `SetHeadPalette (98)`
- `StartBarber (101)` — open the barber UI
- `SetAltRacialSkills (111)` — for race-change flows
**Misc:**
- `LockFellow (66)` — lock fellowship roster
- `UntrainSkill (110)`
- `Invalid (0)` — no-op
- `Enlightenment (9001)` — ACE-custom (not retail)
### 3.3 PropertiesEmote / PropertiesEmoteAction shape
Source:
`references/ACE/Source/ACE.Entity/Models/PropertiesEmote.cs` and
`PropertiesEmoteAction.cs`. Schema:
```csharp
PropertiesEmote {
EmoteCategory Category;
float Probability; // 0..1, roll per set
uint? WeenieClassId; // filter for Give/Refuse/Taunt
MotionStance? Style; // filter for HeartBeat
MotionCommand? Substyle;
string Quest; // filter — only fire if quest flag matches
VendorType? VendorType; // filter for Vendor category
float? MinHealth; // filter for WoundedTaunt
float? MaxHealth;
List<PropertiesEmoteAction> PropertiesEmoteAction; // the script
}
PropertiesEmoteAction {
uint Type; // EmoteType
float Delay; // pre-delay seconds
float Extent; // motion/speech parameter
MotionCommand? Motion;
string Message; // speech text, quest name, or emote category for Goto
string TestString; // for InqStringStat, InqYesNo prompt text
int?/long?/double? Min/Max; // Inq ranges
int? Stat; // PropertyInt/etc. key for Inq/Set
int? Amount; // integer arg
long? Amount64; // 64-bit arg
double? Percent;
int? SpellId;
PlayScript? PScript;
Sound? Sound;
uint? WeenieClassId; // for Give/TakeItems
int? StackSize;
int? Palette; // for Give
float? Shade;
uint? ObjCellId; // for TeleportTarget/MoveToPos/SetSanctuaryPosition
float? OriginX/Y/Z, AnglesX/Y/Z/W;
...
}
```
### 3.4 String replacement — `%n`, `%s`, `%tqt`, etc.
From `EmoteManager.Replace` (line 1777 of EmoteManager.cs):
| Token | Replaced with |
|----------|---------------|
| `%n` / `%mn` | sender name (this NPC) |
| `%s` / `%tn` | target name (the player) |
| `%ml` / `%tl` | sender / target level |
| `%mt` / `%tt` | sender / target Template property |
| `%mh` / `%th` | sender / target heritage name |
| `%tqt` | time-until-next-solve for the active quest (target) |
| `%CDtime` | same (LSD custom) |
| `%fqt` | fellowship time-until-next-solve |
| `%tqm` | target max-solves |
| `%tqc` | target current-solves |
| `%mqt` | source (NPC) quest-cooldown |
These let the content author write NPC speech like `"I said no, %s. Come
back in %tqt."` and have it rendered dynamically per-player. This is the
only structured quest-state information that ever actually reaches the
client — baked into speech text.
### 3.5 Emote execution model
Flow: `ExecuteEmoteSet(category, quest, target)``GetEmoteSet(...)`
picks a random set (filtered by category + quest + vendor + wcid + style,
weighted by Probability) → `Enqueue(set, target, 0)` runs the action list
in order. Each action can return a delay; the next action is scheduled
after that delay + its own pre-delay. Branching emotes (Inq*, Update*,
Goto) recursively call `ExecuteEmoteSet` on a new category
(`TestSuccess`/`TestFailure`/`QuestSuccess`/`QuestFailure`/`GotoSet`),
which is how conditionals and subroutines are expressed.
Busy-state protection: `EmoteManager.IsBusy` prevents a second trigger
from starting while one is running; `Nested` counts recursion depth;
`Nested > 75` + self-referential emote aborts with a log error (infinite
loop detection). The 75-deep cap is why some retail quests bail out
silently when misconfigured.
---
## 4. NPC dialog flow
### 4.1 Click the NPC
On the client side, right-clicking an NPC or selecting it and hitting Use
runs the Use code at `chunk_00580000.c:5890+`. That function eventually
emits a `GameActionUseRequest` (opcode 0x0035 per ACE's
`GameActionType.cs`). On the server, that lands in the creature's
`OnUse` handler, which calls `EmoteManager.OnUse(player)` — which in turn
calls `ExecuteEmoteSet(EmoteCategory.Use, null, player)` (see
`EmoteManager.cs:1916`).
### 4.2 The server evaluates a Use emote set
`GetEmoteSet(EmoteCategory.Use, ...)` picks one of the NPC's Use sets
(possibly filtered by quest flag — e.g. "only show first-meeting speech
to players who don't have `metArwicMayor`"), rolling against
`Probability`. The picked set's action list is enqueued.
Typical first-meeting conversation script:
```
EmoteSet {Category=Use, Quest=null, Probability=1.0}
Act: "%n turns to %s."
TurnToTarget
Motion: Bow
Tell: "Hello, %s. I don't believe I've had the pleasure. I am Mayor Arwic."
UpdateQuest: "metArwicMayor"
```
After this fires, the NPC has "remembered" this player. A second Use set
(filtered `Quest="metArwicMayor"`) holds the follow-up dialog.
### 4.3 Dialog rendering on the client
The server sends each Say/Tell/TextDirect as its own wire message. The
client has no dialog-box widget — it simply appends each line to the chat
window with the right `ChatMessageType` colour. The "NPC dialog window"
you remember from playing AC is in fact a chat pane, filtered on NPC
messages.
Visible NPC speech goes through the `<Tell:IIDString:ID:name>NAME<\Tell>`
clickable-name markup when the sender is a player (GUID range
`0x50000001 0x6FFFFFFF`). For an NPC (GUID outside that range) the
format is plain `%s tells you, "%s"\n` with no clickable link
(`chunk_00570000.c:1190-1195`):
```c
if (sender_guid < 0x50000001 || sender_guid > 0x6FFFFFFF) {
FUN_00402710(&out, "%s tells you, \"%s\"\n", name, message);
} else {
FUN_00402710(&out, "<Tell:IIDString:%d:%s>%s<\\Tell> tells you, \"%s\"\n",
sender_guid, name, name, message);
}
```
This means in acdream we render NPC dialog as normal chat lines with
colour = `ChatMessageType.Tell` (0x03 — yellow) and no clickable name.
For player-to-player tells we emit the clickable span.
### 4.4 Busy-state & client behaviour
While the NPC's EmoteManager is running a set, all further Use and Give
attempts are rejected server-side (`Player_Inventory.cs:3404` for Give,
similar for Use) with `WeenieErrorWithString.AiRefuseItemDuringEmote`.
The client shows "<NPC> is busy right now.". That's the only "dialog
window modal" indication — a soft refusal, not a UI lock.
---
## 5. Use / Appraise triggers
- **Use** (`EmoteCategory.Use`) — fires on `OnUse`. Any weenie with a Use
emote set is "talkable"/"interactable". Items with Use sets are
single-use consumables that can trigger scripts (e.g. "use this book
to start a quest").
- **Appraise** — technically the *client* action; the server responds
with `GameEventIdentifyObjectResponse` (0x00C9). There is no
EmoteCategory.Appraise. If you want an NPC to react to being appraised,
the closest pattern is to key off a `HearChat` emote with the NPC's
own name — not reliable — or to cheat and trigger from an `InqYesNo`.
Retail quests do not depend on appraisal.
- **Activation** (`EmoteCategory.Activation`) — fires when a switch is
flipped, chained via `OnActivate`. Used by quest puzzle switches.
- **Portal** (`EmoteCategory.Portal`) — fires after a portal teleport
succeeds. Used for warning/greeting text post-travel.
- **HearChat** (`EmoteCategory.HearChat`) — fires when a nearby player
speaks a keyword. Matched by literal string match on `Quest` field.
This is how "say the magic word to the door" puzzles work.
- **ReceiveTalkDirect** (`EmoteCategory.ReceiveTalkDirect`) — fires when
a player sends a `/tell` to this NPC containing a keyword. This is the
"text-input quest" pattern.
---
## 6. Quest turn-in: dragging an item onto an NPC
Wire path:
1. Client sends `GameActionGiveObjectRequest`
(`GameActionType.GiveObjectRequest` ≈ 0x00EA). Payload:
`uint32 targetGuid, uint32 objectGuid, int32 amount`.
Source:
`references/ACE/Source/ACE.Server/Network/GameAction/Actions/GameActionGiveObjectRequest.cs`.
2. Server routes to `Player.HandleActionGiveObjectRequest`
(`Player_Inventory.cs:3190`). Validates the player isn't busy / teleporting
/ trading the item, locates both objects, and walks the player to the
target via `CreateMoveToChain`.
3. On arrival, calls `GiveObjectToNPC(target, item, ...)`
(`Player_Inventory.cs:3380`).
4. Target looks for a matching emote set:
```csharp
var refuseItem = EmoteManager.GetEmoteSet(EmoteCategory.Refuse, null, null, item.WeenieClassId);
var giveItem = EmoteManager.GetEmoteSet(EmoteCategory.Give, null, null, item.WeenieClassId);
```
(Note: these sets are filtered by `WeenieClassId` — the emote "matches"
the item's WCID exactly. Generic "I accept anything" is expressed via
the `AiAcceptEverything` bool property on the weenie.)
5. If Give emote matches: remove the item from the player's inventory,
send chat: `"You give <NPC> <item>."`, broadcast a `ReceiveItem`
sound on the NPC, and run the emote set (`ExecuteEmoteSet(emoteResult, this)`).
The emote set typically contains: `StampQuest`, `AwardXP`, `Give`
(return reward), `Tell` ("Many thanks, %s."), etc.
6. If Refuse emote matches: NPC "examines" the item (it is returned),
chat: `"You allow <NPC> to examine your <item>."`, and the Refuse
emote runs — typically just a `Tell` explaining why it's not
accepted. This is how NPCs react to items that are "close but not
quite" quest items.
7. If neither: `WeenieError.TradeAiDoesntWant` — generic "not accepting
gifts right now."
Critical point: **the NPC's Give emote is the quest turn-in.** There is
no separate "quest turn-in" handler. `StampQuest`/`UpdateQuest` inside
the Give emote writes the quest flag; `AwardXP`/`Give` distributes the
reward; a final `Tell` gives narrative closure. The sequence is not
transactional — if the server crashes between steps, quest state can
become inconsistent. Retail accepted this.
---
## 7. Branching dialogs
NPCs with multiple stages of a questline expose this by having multiple
Use emote sets, each filtered by `Quest`. `GetEmoteSet` iterates the
NPC's PropertiesEmote list, filters by category + quest + vendor + wcid
+ style/substyle/health, then applies `Probability`-weighted random
selection.
Typical multi-stage pattern:
```
EmoteSet {Use, Quest=null, Prob=1.0} // first meet
Tell "Hello, traveller. Talk to me about the %s ring."
UpdateQuest "arwicMayorQuest" // phase 1 stamp
EmoteSet {Use, Quest="arwicMayorQuest", Prob=1.0} // post-first-meet
InqQuest "arwicMayorQuestDone" // has player done the task?
↓ QuestSuccess set → Tell "You're back! Good, good."
↓ QuestFailure set → Tell "Have you not found it yet?"
EmoteSet {Give, WCID=<ring>, Prob=1.0} // turn-in
StampQuest "arwicMayorQuestDone"
Tell "Excellent! Here is your reward."
AwardXP 50000
Give <reward WCID>
```
A single NPC can have dozens of Use/Give sets — the emote engine is the
entire dialog tree. In principle content authors could use `Goto` to
build larger branching trees, and `InqYesNo` to add confirmation popups,
but retail kept it simple: Use emotes were almost always straight-line
scripts.
---
## 8. Random / ambient emotes
NPCs play ambient speech via `HeartBeat` (category 5) emote sets. These
tick periodically (`EmoteManager.HeartBeat` at `EmoteManager.cs:1904`)
while the NPC isn't actively engaged. Implementation filters by current
motion stance/substyle so that e.g. a sitting NPC only plays sitting
emotes. Retail shopkeepers have multi-line HeartBeat sets — `Say`
followed by a `PhysScript` gesture — rolled against Probability.
`HeartBeat` does not fire on player characters (early-return in
`HeartBeat()` method). It does not fire on "awake" creatures (those with
an active combat target); `WoundedTaunt`, `Taunt`, etc. take over in
combat.
`ReceiveCritical`, `ResistSpell`, `NewEnemy`, `Scream`, `Homesick` are
similar background triggers, each driven server-side from the relevant
combat/movement event handlers.
---
## 9. Quest Tracker UI panel
The retail client has a **Contract Tracker** panel (not a "Quest
Tracker"). It displays a list of Contracts, each showing: name,
description, progress, and timers. Contracts are entries in the
ContractTable dat file (`DatReaderWriter.Types.Contract`):
Fields:
```
uint32 Version
uint32 ContractId
string ContractName // e.g. "Aerlinthe Recall Ring Quest"
string Description // long-form text
string DescriptionProgress // "You have defeated N of M..."
string NameNPCStart // NPC that offers the quest
string NameNPCEnd // NPC that takes turn-in
string QuestflagStamped // flag set by first-meeting stamp
string QuestflagStarted // flag set by acceptance
string QuestflagFinished // flag set by completion (terminates)
string QuestflagProgress // flag whose NumTimesCompleted is the counter
string QuestflagTimer // flag whose cooldown feeds TimeWhenDone
string QuestflagRepeatTime // flag whose cooldown feeds TimeWhenRepeats
Position LocationNPCStart
Position LocationNPCEnd
Position LocationQuestArea
```
The server-side `ContractTracker` struct
(`references/ACE/Source/ACE.Server/Network/Structure/ContractTracker.cs`)
builds the live state: it looks up each of the Questflag* names in the
player's QuestRegistry, translates that into a Stage + two doubles
(`TimeWhenDone`, `TimeWhenRepeats`), and writes the packed struct into
`SendClientContractTracker` (0x0315) or `SendClientContractTrackerTable`
(0x0314).
On the client, the panel:
- Looks up the Contract by ContractId in the ContractTable dat.
- Renders `ContractName` as the list item title.
- Renders `Description` (or `DescriptionProgress` if `Stage == ProgressCounter + N`) as the body.
- Shows a timer if `TimeWhenDone > 0` or `TimeWhenRepeats > 0`.
- Clicking the NPC-name rows lets the player see NPC location on the in-game map.
The panel is additive/replacement: a single `SendClientContractTracker`
message with `DeleteContract=true` removes an entry; otherwise the
entry's state is updated. Full refresh arrives via
`SendClientContractTrackerTable` (e.g. after login).
The client learns about *new* Contracts via the `AddContract` emote
action (EmoteType 119), which server-side writes a
`CharacterPropertiesContractRegistry` row and pushes a
`SendClientContractTracker` to the client.
---
## 10. Wire messages — byte layouts
All strings are String16-L: `uint16 length` then UTF-8 bytes, then
padded to 4-byte alignment. All integers and floats are little-endian.
Where noted, the "Game Event" wrapper adds a leading `uint32 event_type`
before the payload described below.
### 10.1 Server → client
**GameEventTell (0x02BD)** — NPC tell / player tell:
```
string16L message
string16L senderName
uint32 senderID // GUID
uint32 targetID // GUID
uint32 chatType // ChatMessageType
uint32 reserved // always 0, observed in retail pcaps
```
ACE: `GameEventTell.cs`. holtburger: `TellEventData`
(`crates/holtburger-protocol/src/messages/chat/events.rs:7`).
**GameEventPopupString (0x0004)** — modal popup window:
```
string16L message
```
ACE: `GameEventPopupString.cs`.
**GameMessageHearSpeech (0x02BB)** — NPC says something locally:
```
string16L message
string16L senderName
uint32 senderID
uint32 chatType
```
**GameMessageHearRangedSpeech (0x02BC)** — Say with explicit range:
```
string16L message
string16L senderName
uint32 senderID
float32 range
uint32 chatType
```
**GameMessageEmoteText (0x01E0)** — acting text ("NPC waves"):
```
uint32 senderID
string16L senderName
string16L emoteText
```
**GameMessageSoulEmote (0x01E2)** — soul-emote (out-of-character tone):
```
uint32 senderID
string16L senderName
string16L emoteText
```
**GameMessageSystemChat (server message frame 0xF7E0)** — plain text to chat:
```
string16L message
int32 chatMessageType
```
**GameEventWeenieError (0x028A)** — typed error:
```
uint32 errorCode // WeenieError enum (e.g. 0x43E YouHaveSolvedThisQuestTooRecently)
```
**GameEventInventoryServerSaveFailed (0x00A0)** — item-related error:
```
uint32 itemGuid
uint32 errorCode
```
**GameEventConfirmationRequest (0x0274)** — yes/no dialog:
```
uint32 confirmationType
uint32 context // server-chosen correlation id
string16L prompt
```
**GameEventSendClientContractTracker (0x0315)** — single contract update:
```
uint32 version
uint32 contractId
uint32 stage // ContractStage
double timeWhenDone
double timeWhenRepeats
uint32 deleteContract // bool (0/1)
uint32 setAsDisplayContract
```
**GameEventSendClientContractTrackerTable (0x0314)** — bulk refresh:
```
uint32 numContracts
ContractManager.Write(...) // packed list; see Structure/ContractManager.cs
```
The inner loop writes each ContractTracker without the trailing two
bools (`ContractTracker.Write` variant at ContractTracker.cs:137).
### 10.2 Client → server
**GameActionEmote (0x01E0-ish, see GameActionType.Emote)** — `/emote` chat command:
```
string16L emoteText
```
Source: `GameActionEmote.cs`. The server re-broadcasts via
`GameMessageEmoteText` to everyone in LocalBroadcastRange.
**GameActionSoulEmote (GameActionType.SoulEmote)** — `/soul` chat command:
```
string16L emoteText
```
**GameActionGiveObjectRequest (GameActionType.GiveObjectRequest)** — drag item to NPC:
```
uint32 targetGuid
uint32 objectGuid
int32 amount
```
**GameActionTellRequest (GameActionType.TellRequest)** — player `/tell` to another player or NPC:
```
string16L message
string16L targetName
```
(Server route: TellFromTargetNameHandler.cs.) For NPC tells (quest
keyword dialogs), this is what triggers `EmoteCategory.ReceiveTalkDirect`
emotes on the NPC.
**GameActionConfirmationResponse (GameActionType.ConfirmationResponse)** — yes/no reply:
```
uint32 confirmationType
uint32 context
uint32 response // 0=no, 1=yes
```
### 10.3 Where these fit in the decompiled dispatch table
`chunk_00550000.c` around line 10700 is the client's incoming game-event
switch. Each opcode maps to a concrete handler function:
- 0x01E0 EmoteText → `FUN_006a5a20`
- 0x02BB HearSpeech → handler (not shown; in same table)
- 0x02BC HearRangedSpeech → handler
- 0x02BD Tell → `FUN_006a5920` (chunk_006A0000.c:4991)
- 0x0314 ContractTrackerTable → handler in same switch
- 0x0315 ContractTracker → handler in same switch
Cross-referencing holtburger's `events.rs` gives clean packed layouts
for most of these; ACE's `Source/ACE.Server/Network/GameEvent/Events/`
gives write-side layouts. The two agree on all byte offsets.
---
## 11. Port plan — C# classes for acdream
Target layer: `AcDream.Core` (state + DTOs) + `AcDream.App/UI` (panels) +
`AcDream.Core.Net/Messages` (wire).
### 11.1 Core types
```csharp
// AcDream.Core/Quests/QuestState.cs
// Client-side mirror of what we know about one quest's state from server
// pushes. We only track it if a Contract references it.
public sealed record QuestState(
string QuestName,
uint LastTimeCompleted, // Unix seconds; 0 = never
int NumTimesCompleted,
uint MinDelta, // from world Quest table snapshot if known
int MaxSolves);
// AcDream.Core/Quests/ContractTracker.cs
public enum ContractStage { Available = 1, InProgress = 2, DoneOrPendingRepeat = 3, ProgressCounter = 4 }
public sealed record ContractEntry(
uint ContractId,
uint Version,
ContractStage Stage,
double TimeWhenDone,
double TimeWhenRepeats);
public interface IContractTracker : IReadOnlyCollection<ContractEntry>
{
event Action<ContractEntry> Added;
event Action<ContractEntry> Updated;
event Action<uint> Removed;
event Action BulkReplaced;
}
public sealed class ContractTracker : IContractTracker { /* standard observable dict */ }
```
### 11.2 Emote mini-language types
For acdream, the emote engine runs server-side. The client only needs
to decode the *outputs* (Tell / Say / EmoteText / Popup / Confirmation /
ContractTracker / WeenieError). But because acdream must support plugin
scripting (per `project_plugin_requirement.md`), we port the full
EmoteType + EmoteCategory enums now so plugins can inspect and inject
into the chat stream with the same vocabulary the server uses.
```csharp
// AcDream.Core/Emotes/EmoteCategory.cs — same 39 values as EmoteCategory.cs (section 3.1)
// AcDream.Core/Emotes/EmoteType.cs — same 122 values as EmoteType.cs (section 3.2)
// AcDream.Core/Emotes/EmoteAction.cs
public sealed record EmoteAction(
EmoteType Type,
float Delay,
float Extent,
int? Motion,
string? Message,
string? TestString,
int? Min, int? Max,
long? Min64, long? Max64,
double? MinDbl, double? MaxDbl,
int? Stat,
int? Amount,
long? Amount64,
double? Percent,
int? SpellId,
uint? WeenieClassId,
int? StackSize,
int? Palette,
float? Shade,
uint? ObjCellId,
float? OriginX, float? OriginY, float? OriginZ,
float? AnglesX, float? AnglesY, float? AnglesZ, float? AnglesW);
// AcDream.Core/Emotes/EmoteSet.cs
public sealed record EmoteSet(
EmoteCategory Category,
float Probability,
uint? WeenieClassId,
string? Quest,
int? Style,
int? Substyle,
float? MinHealth, float? MaxHealth,
IReadOnlyList<EmoteAction> Actions);
// AcDream.Core/Emotes/EmoteScript.cs
// Server-driven only on the client side; acdream plugin code can inspect
// but not override. Plugin hooks:
// - IEmoteObserver.OnEmoteReceived(source, EmoteSet) // from server log stream
// - IEmoteObserver.OnChatLine(ChatLine) // rendered form
public interface IEmoteScriptService
{
IObservable<ChatLine> ChatLines { get; } // every rendered line
IObservable<PopupRequest> Popups { get; }
}
```
### 11.3 Trigger / action enums reused as strict enums
`EmoteCategory` (0..38) and `EmoteType` (0..121 + `Enlightenment=9001`)
as plain C# enums. This gives plugins type-safe switches and the Roslyn
analyzer can flag missing-case issues if we use exhaustive switches.
### 11.4 UI panels
```csharp
// AcDream.App/UI/Panels/ChatPanel.cs
// - Appends ChatLine records with colour by ChatMessageType
// - Renders <Tell:IIDString:ID:name>NAME<\Tell> spans as
// clickable "click to /tell" links
// - Filters / tabs (All / Speech / Fellowship / etc.)
// - This is where NPC dialog ends up — not a separate dialog window
// AcDream.App/UI/Panels/PopupDialogPanel.cs
// - Listens to PopupRequest stream
// - Modal single-text popup with OK
// - Used for EmoteType.PopUp (wire: 0x0004)
// AcDream.App/UI/Panels/YesNoDialogPanel.cs
// - Listens to ConfirmationRequest stream (0x0274)
// - Yes / No buttons post back GameActionConfirmationResponse
// - Used for InqYesNo and other confirmation flows
// - The context uint32 must round-trip unchanged
// AcDream.App/UI/Panels/ContractTrackerPanel.cs
// - Bound to IContractTracker
// - Row per ContractEntry
// - Lookup dat Contract by ContractId
// - Title: Contract.ContractName
// - Body: Description or DescriptionProgress
// - Timer: TimeWhenDone (countdown) or TimeWhenRepeats (cooldown)
// - Click NPC name → show Contract.LocationNPCStart/End on in-game map
```
### 11.5 Wire decoders
```csharp
// AcDream.Core.Net/Messages/GameEventTellMessage.cs
internal static class GameEventTellMessage {
public static TellDto Decode(ref DatReader r) => new(
message: r.ReadString16L(),
senderName: r.ReadString16L(),
senderId: r.ReadUInt32(),
targetId: r.ReadUInt32(),
chatType: (ChatMessageType)r.ReadUInt32(),
reserved: r.ReadUInt32());
}
// Repeat shape for: HearSpeech (02BB), HearRangedSpeech (02BC),
// EmoteText (01E0), SoulEmote (01E2), PopupString (0004),
// WeenieError (028A), WeenieErrorWithString (028B),
// ConfirmationRequest (0274), ConfirmationDone (0276),
// ContractTrackerTable (0314), ContractTracker (0315),
// SystemChat via 0xF7E0 frame.
```
Cross-reference holtburger's `messages/chat/events.rs` and
`messages/chat/types.rs` for field order; copy its pack/unpack test
vectors into `AcDream.Core.Net.Tests` as conformance tests — this is the
only reliable way to catch byte-order / padding regressions before they
hit live traffic.
### 11.6 Plugin API surface
From `project_plugin_requirement.md`: the plugin API must expose
"game state through well-defined interfaces." For R10 that means:
```csharp
public interface IQuestState
{
// For quests known to the client through Contracts (NOT a general
// oracle — the client only knows about Contracts).
IContractTracker Contracts { get; }
// Emitted ChatLines with sender/target GUIDs + ChatMessageType —
// lets plugins regex-match dialog for e.g. macro automation.
IObservable<ChatLine> ChatStream { get; }
IObservable<PopupRequest> PopupStream { get; }
// Lets plugins send /tell (including to NPCs, which is how
// HearChat / ReceiveTalkDirect quests are solved).
Task SendTellAsync(string targetName, string message, CancellationToken ct = default);
// Lets plugins drag an item to an NPC (a.k.a. "give").
Task GiveItemAsync(uint targetGuid, uint itemGuid, int amount, CancellationToken ct = default);
// Use / right-click a weenie.
Task UseAsync(uint targetGuid, CancellationToken ct = default);
// Respond to a YesNo popup.
Task RespondConfirmationAsync(uint confirmationType, uint context, bool yes, CancellationToken ct = default);
}
```
This is enough for plugin-author-driven quest automation without
exposing raw packet sends.
### 11.7 Minimum viable acdream deliverables (phase R10)
1. `AcDream.Core/Emotes/EmoteCategory.cs` + `EmoteType.cs` as typed enums.
2. `AcDream.Core/Quests/ContractEntry.cs` + `ContractTracker.cs` with events.
3. `AcDream.Core.Net/Messages/*` decoders for opcodes listed in section
10.1, matched to holtburger test fixtures.
4. `AcDream.App/UI/Panels/ChatPanel.cs` renders all of:
NPC Tell / player Tell / HearSpeech / HearRangedSpeech / EmoteText /
SoulEmote / SystemChat / channel broadcasts, with the
`<Tell:IIDString:...>` markup parsed into clickable spans.
5. `AcDream.App/UI/Panels/PopupDialogPanel.cs` for PopupString
(simple ack dialog).
6. `AcDream.App/UI/Panels/YesNoDialogPanel.cs` for ConfirmationRequest
(context round-tripping).
7. `AcDream.App/UI/Panels/ContractTrackerPanel.cs` renders
ContractTracker + dat-lookup of Contract records.
8. Plugin interfaces from 11.6 exposed via `IGameState`.
That lets plugins drive quest automation and lets players play every
retail quest that exists, because every retail quest IS an emote script
playing through Tell + Give + (optionally) ContractTracker.
---
## 12. Where this will go wrong (and where it won't)
Subtle bugs to watch in the port:
1. **`%tqt` not rendering** — if the server's string-substitution path
doesn't get a live QuestManager handle, `%tqt` stays literal and
players see the placeholder in chat. On the client side this is
purely cosmetic; just escape `%` properly when parsing incoming
strings — do not attempt to format them yourself. The server
delivers pre-formatted text.
2. **ConfirmationRequest context round-trip** — if the client's
ConfirmationResponse drops the server-chosen `context` uint32, the
server will reject it silently and the emote chain will stall. Always
echo exactly what the server sent.
3. **Contract stage = `ProgressCounter + N`** — note this is ADD, not
replace. `Stage` arrives as (4 + progress). Panel decoder must split
`stage % 4 == 0 && stage >= 4` as progress vs 1/2/3 stages.
4. **Tell markup parsing** — the string `<Tell:IIDString:0x%08x:name>`
can appear inside the Say/Tell `message` field, not just in chat
framing. Every string field that comes from an NPC speech emote is
subject to it. Parse it as a rich-text span while rendering, not as
markup in the pipeline.
5. **Sender GUID range `0x50000001 0x6FFFFFFF`** — this is the retail
player-GUID range. Only wrap speech in `<Tell>` markup when the
sender is in this range. NPCs use other GUID ranges (typically
landblock-local). `chunk_00570000.c:1189-1195` is the reference.
6. **Emote category filtering by `Quest`** — some NPCs have fifty Use
emote sets, each with a different `Quest` value. The client never
sees the filtering; only the one winning emote fires. But plugin
observers that snoop on the chat stream will not be able to reverse-
engineer which set fired unless the NPC's sequence is distinctive
enough. Document this limitation in the plugin API.
7. **Infinite-loop protection** — server aborts nested > 75. If a retail
NPC has a looping emote set, the loop terminates silently in
`Enqueue`. Plugins should not rely on seeing the full sequence if
they observe this abort pattern (rare but possible).
8. **HearChat / ReceiveTalkDirect keywords** — the emote set's `Quest`
field is a case-insensitive literal match against the player's chat
message. This is the ONLY way to trigger NPC reactions to arbitrary
player speech. Document clearly for plugin authors.
Places this port is NOT load-bearing on acdream:
- We do NOT implement QuestManager, EmoteManager, EventManager,
ContractManager, or the world Quest table on the client. Those are
server responsibilities.
- We do NOT evaluate emote scripts on the client. We only *observe*
their output.
- We do NOT store CharacterPropertiesQuestRegistry client-side.
Everything we display comes from server pushes.
---
## 13. Cross-references & file pointers
**Decompiled retail client:**
- `docs/research/decompiled/chunk_00550000.c:10700-10880` — incoming
game-event dispatcher switch; shows 0x2BD (Tell), 0x1E0 (EmoteText),
0x2BB/2BC (HearSpeech), 0x314/0x315 (ContractTracker) handler
functions.
- `docs/research/decompiled/chunk_006A0000.c:4991` (`FUN_006a5920`) —
Tell (0x2BD) receive-side dispatch.
- `docs/research/decompiled/chunk_006A0000.c:5036` (`FUN_006a5a20`) —
EmoteText (0x1E0) receive-side dispatch.
- `docs/research/decompiled/chunk_00570000.c:1165-1205` — text
rendering for Tell, including `<Tell:IIDString>` markup and the
player-GUID range test `0x50000001 0x6FFFFFFF`.
- `docs/research/decompiled/chunk_00570000.c:740-810` — same markup
used for allegiance / fellowship / vassal tell variants.
- `docs/research/decompiled/chunk_00570000.c:1500-2900` — game event
error-code → wide-string lookup table. Contains all the quest-related
error strings (0x43E "solved too recently", 0x43F "too many times",
0x445 "requires quest to pick up", 0x474 "must complete quest to use
portal").
- `docs/research/decompiled/chunk_00580000.c:481-505` — client
chat-command handler; routes `:` / `;` prefix input to `@emote`
command.
- `docs/research/decompiled/chunk_00580000.c:2095-2170` — chat command
registration for `emote` / `emotes`.
- `docs/research/decompiled/chunk_00580000.c:5936-5962` — "Select your
target before using the %s" path; client-side Use pipeline.
**ACE server (wire + server-side quest evaluation):**
- `references/ACE/Source/ACE.Entity/Enum/EmoteType.cs` — the 122 values.
- `references/ACE/Source/ACE.Entity/Enum/EmoteCategory.cs` — the 39 values.
- `references/ACE/Source/ACE.Entity/Enum/ChatMessageType.cs` — colour
codes for every chat line.
- `references/ACE/Source/ACE.Entity/Enum/ConfirmationType.cs` —
Yes_No (7), SwearAllegiance (1), AlterSkill (2), etc.
- `references/ACE/Source/ACE.Entity/Models/PropertiesEmote.cs` — set
shape.
- `references/ACE/Source/ACE.Entity/Models/PropertiesEmoteAction.cs` —
action shape.
- `references/ACE/Source/ACE.Server/Managers/QuestManager.cs` —
HasQuest, CanSolve, IsMaxSolves, Increment, Decrement, Stamp, Erase,
EraseAll, HasQuestBits/SetQuestBits, HandleKillTask, HandleSolveError,
HandlePortalQuestError.
- `references/ACE/Source/ACE.Server/WorldObjects/Managers/EmoteManager.cs` —
the mini-language interpreter (1740+ lines).
- `references/ACE/Source/ACE.Server/Network/GameEvent/GameEventType.cs` —
opcode list including 0x0004 PopupString, 0x028A WeenieError,
0x02BD Tell, 0x0274 CharacterConfirmationRequest,
0x0314 SendClientContractTrackerTable, 0x0315 SendClientContractTracker.
- `references/ACE/Source/ACE.Server/Network/GameMessages/GameMessageOpcode.cs` —
0x01E0 EmoteText, 0x01E2 SoulEmote, 0x02BB HearSpeech,
0x02BC HearRangedSpeech.
- `references/ACE/Source/ACE.Server/Network/GameEvent/Events/GameEventTell.cs`
— server-side Tell packer.
- `references/ACE/Source/ACE.Server/Network/GameEvent/Events/GameEventPopupString.cs` —
PopupString packer.
- `references/ACE/Source/ACE.Server/Network/GameEvent/Events/GameEventConfirmationRequest.cs` —
confirmation request packer.
- `references/ACE/Source/ACE.Server/Network/GameEvent/Events/GameEventSendClientContractTracker.cs`
and `.../GameEventSendClientContractTrackerTable.cs` — tracker
packers.
- `references/ACE/Source/ACE.Server/Network/GameMessages/Messages/GameMessageHearSpeech.cs`
/ `GameMessageHearRangedSpeech.cs` / `GameMessageEmoteText.cs` /
`GameMessageSoulEmote.cs` — speech packers.
- `references/ACE/Source/ACE.Server/Network/GameAction/Actions/GameActionEmote.cs`
/ `GameActionSoulEmote.cs` / `GameActionGiveObjectRequest.cs` —
client → server action handlers.
- `references/ACE/Source/ACE.Server/WorldObjects/Player_Inventory.cs:3190-3478` —
`HandleActionGiveObjectRequest` and `GiveObjectToNPC` flow.
- `references/ACE/Source/ACE.Server/Network/Structure/ContractTracker.cs` —
ContractStage enum and packed struct layout.
- `references/ACE/Source/ACE.Database/Models/Shard/CharacterPropertiesQuestRegistry.cs` —
per-character row.
- `references/ACE/Source/ACE.Database/Models/World/Quest.cs` —
world quest table.
**DatReaderWriter (what the dat files contain):**
- `references/DatReaderWriter/DatReaderWriter/Generated/Types/Contract.generated.cs` —
Contract struct (Quest flag name pointers).
- `references/DatReaderWriter/DatReaderWriter/Generated/Types/ChatEmoteData.generated.cs` —
per-chat-emote dat strings ("MyEmote" = what you see, "OtherEmote" =
what others see).
- `references/DatReaderWriter/DatReaderWriter/Generated/DBObjs/ContractTable.generated.cs` —
Contract → ContractTable keyed dict in portal.dat.
- `references/ACE/Source/ACE.DatLoader/Entity/Contract.cs` — ACE's
reader for the same (byte-compatible).
- `references/ACE/Source/ACE.DatLoader/Entity/ChatEmoteData.cs` —
ACE's reader for ChatEmoteData.
**Holtburger (Rust client wire behaviour):**
- `references/holtburger/crates/holtburger-protocol/src/messages/chat/events.rs` —
TellEventData, PopupStringEventData, ChannelBroadcastEventData with
byte-level pack/unpack impls (ground truth for wire format).
- `references/holtburger/crates/holtburger-protocol/src/messages/chat/types.rs` —
HearSpeechData, HearRangedSpeechData, ServerMessageData,
EmoteTextData, SoulEmoteData.
- `references/holtburger/apps/holtburger-cli/src/pages/game/panels/chat.rs` —
rendering of speech/tell/emote in a terminal (demonstrates the Tell
markup parsing).
---
## 14. Appendix — EmoteType numeric index
For cross-referencing against decompiled type-dispatch constants. From
`EmoteType.cs` (section 3.2 names, for quick lookup without flipping
windows):
```
0 Invalid 28 AwardSkillXP 57 ResetHomePosition 86 SetMyQuestCompletions
1 Act 29 AwardSkillPoints 58 InqFellowQuest 87 MoveToPos
2 AwardXP 30 InqQuestSolves 59 InqFellowNum 88 LocalSignal
3 Give 31 EraseQuest 60 UpdateFellowQuest 89 InqPackSpace
4 MoveHome 32 DecrementQuest 61 StampFellowQuest 90 RemoveVitaePenalty
5 Motion 33 IncrementQuest 62 AwardNoShareXP 91 SetEyeTexture
6 Move 34 AddCharacterTitle 63 SetSanctuaryPosition 92 SetEyePalette
7 PhysScript 35 InqBoolStat 64 TellFellow 93 SetNoseTexture
8 Say 36 InqIntStat 65 FellowBroadcast 94 SetNosePalette
9 Sound 37 InqFloatStat 66 LockFellow 95 SetMouthTexture
10 Tell 38 InqStringStat 67 Goto 96 SetMouthPalette
11 Turn 39 InqAttributeStat 68 PopUp 97 SetHeadObject
12 TurnToTarget 40 InqRawAttributeStat 69 SetBoolStat 98 SetHeadPalette
13 TextDirect 41 InqSecondaryAttr 70 SetQuestCompletions 99 TeleportTarget
14 CastSpell 42 InqRawSecondary 71 InqNumCharacterTitles 100 TeleportSelf
15 Activate 43 InqSkillStat 72 Generate 101 StartBarber
16 WorldBroadcast 44 InqRawSkillStat 73 PetCastSpellOnOwner 102 InqQuestBitsOn
17 LocalBroadcast 45 InqSkillTrained 74 TakeItems 103 InqQuestBitsOff
18 DirectBroadcast 46 InqSkillSpecialized 75 InqYesNo 104 InqMyQuestBitsOn
19 CastSpellInstant 47 AwardTrainingCredits 76 InqOwnsItems 105 InqMyQuestBitsOff
20 UpdateQuest 48 InflictVitaePenalty 77 DeleteSelf 106 SetQuestBitsOn
21 InqQuest 49 AwardLevelPropXP 78 KillSelf 107 SetQuestBitsOff
22 StampQuest 50 AwardLevelPropSkill 79 UpdateMyQuest 108 SetMyQuestBitsOn
23 StartEvent 51 InqEvent 80 InqMyQuest 109 SetMyQuestBitsOff
24 StopEvent 52 ForceMotion 81 StampMyQuest 110 UntrainSkill
25 BLog 53 SetIntStat 82 InqMyQuestSolves 111 SetAltRacialSkills
26 AdminSpam 54 IncrementIntStat 83 EraseMyQuest 112 SpendLuminance
27 TeachSpell 55 DecrementIntStat 84 DecrementMyQuest 113 AwardLuminance
56 CreateTreasure 85 IncrementMyQuest 114 InqInt64Stat
115 SetInt64Stat
116 OpenMe
117 CloseMe
118 SetFloatStat
119 AddContract
120 RemoveContract
121 InqContractsFull
9001 Enlightenment (ACE-custom)
```
---
**End of R10 deep-dive. Approximate word count: ~5,500.**