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

57 KiB
Raw Blame History

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):

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):

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):

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:

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:

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):

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 " 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:

    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

// 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.

// 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

// 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

// 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:

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-3478HandleActionGiveObjectRequest 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.