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.
1326 lines
57 KiB
Markdown
1326 lines
57 KiB
Markdown
# 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 1500–2900. 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.**
|