Skip to content

Latest commit

 

History

History
1752 lines (1613 loc) · 133 KB

SPEC.md

File metadata and controls

1752 lines (1613 loc) · 133 KB

Ding — specification

Ding is a free leveling guide addon for World of Warcraft: Forever (client 1.60.x, interface 16001). It shows the current step and an arrow to the next objective, and moves on by itself when the player accepts, completes or turns in a quest.

Project goals:

  1. An engine independent of the guides. Anyone can write a guide, in Ding or in a separate addon, without touching the engine.
  2. Guides optimized for XP per hour, generated by an out-of-game optimizer and then adjusted by hand.
  3. The Claude Design interface (docs/UI-handoff.md, its themes in docs/UI-themes.md), unobtrusive (adjustable opacity) and fully usable with a gamepad, with its own arrow (TomTom is not needed).

1. Architecture

               out of game                                  in game
 ┌──────────────────────────────────────┐   ┌────────────────────────────────────┐
 │ QuestieDB (merged Forever export)    │   │ Ding/DB (built from that export)   │
 │            │                         │   │            │                       │
 │            ▼                         │   │            ▼                       │
 │ tools/ : extraction ─► optimizer ────┼──►│ Ding/Guides/*.lua ─► Ding engine   │
 │            ▲                         │   │                         │          │
 │            └── time measurements ◄───┼───┤ SavedVariables (DingDB.timings)    │
 └──────────────────────────────────────┘   └────────────────────────────────────┘
Component Role
Ding/Core Namespace, API compatibility, data access, registry, step machine, timing
Ding/DB Ding's game database, generated by tools/build_data.lua from a QuestieDB release
Ding/UI Step panel and arrow
Ding/Guides Guides shipped with the addon
tools/ Out-of-game scripts: zone listing, guide validation, then the optimizer

Game data: Ding/DB, from Questie's database

Questie's database (published under the GNU GPL v3, kept up to date in QuestieDB) contains Forever's quests, NPCs, objects and items, including the new ones as they are discovered.

  • In game, Ding reads its own copy, Ding/DB, through Core/Data.lua: no other addon to install. tools/build_data.lua writes it from a QuestieDB release (the Forever export, its zone tables and its names in every language): only the fields Ding reads, spawns already by UiMapID, one file of names per language (loaded by that client only). Questie's credits are kept (Ding/DB/CREDITS.md). The QuestieDB addon is only read when Ding/DB is missing.
  • On each QuestieDB release, Ding/DB is built again from it, and db_parity_test checks that Ding gets the same answers from both; CI uses the release named in Ding/DB/DB.lua.
  • Out of game, the tools read the merged export produced by tools/export-forever.lua in the QuestieDB repository.
  • The guides contain only quest IDs and the order of the steps: that is our work. Names, NPCs and coordinates come from the database at display time.

Getter semantics, the same for both sources (QuestieDB's docs/api.md):

  • a missing numeric field is 0, never nil;
  • startedBy, finishedBy and objectives always return a table;
  • spawns: Ding/DB stores them by UiMapID ({ [map] = { x, y, x, y, ... } }); QuestieDB groups them by AreaID, as percentages 0-100, and Data converts them with its zone table.

Data: notes for working sessions

  • In game: Ding's own database, Ding/DB, only through Core/Data.lua. It is generated by tools/build_data.lua from a QuestieDB release (the Forever export, its zone tables and its names in every language): positional records (Ding.DB.LAYOUT), spawns already by UiMapID ({ [map] = { x, y, x, y, ... } }), the names of each language in Ding/DB/Locales/<locale>.lua (loaded by that client only). Ding.DB.source names the release. The QuestieDB addon is only a fallback when Ding/DB is missing (Data:UseBackend); the getters keep QuestieDB's semantics (docs: https://github.com/Questie/QuestieDB/blob/master/docs/api.md).
  • DingDB is the public name of Ding/DB (docs/dingdb.md, a public contract like docs/guide-packs.md): other addons read it through the global LibDingDB (the global DingDB is Ding's saved settings); corrections and additions go in tools/data/corrections/*.lua (plain data, checked by tools/check_data.lua and tools/dingdb_corrections.lua), applied by build_data.lua on top of Questie's data; Ding.DB.corrected lists the ids they change. The tools that write and check guides (check_guides, write_leveling, draft_guide, list_zone, the route tests) read the export with the same corrections on top (questiedb_export.lua loadCorrected): a fixed prerequisite or elite moves the quest in the routes once they are written again.
  • Updating the data on a new QuestieDB release: export that release (tag), run tools/build_data.lua <combined dir> --source "QuestieDB vX.Y.Z", then the whole suite with that export (db_parity_test compares Ding/DB with it); CI clones the tag named in Ding/DB/DB.lua.
  • Pitfalls: a missing number is 0 (test ~= 0); startedBy, finishedBy, objectives always return a table; other empty tables are nil; QuestieDB's own spawns are by AreaID in percentages 0-100 (Data converts them); race masks go beyond 32 bits (no bit.band, use math.floor(mask / bit) % 2).
  • Project license: GPL-3.0 (LICENSE), with no exception: Yendi's choice, "everything stays free", third-party guide packs included (derivative works). GPL code (Questie) may be reused, citing where it comes from; never code under an incompatible license.
  • TomTom is "all rights reserved": we only call its public API (Ding.TomTom), no file nor texture is reused. Calls always protected (pcall); on failure, back to Ding's arrow. It stays optional and off by default: Ding's arrow must be enough (Yendi's request).
  • Noto Sans (in Ding/Media/Fonts) is under the OFL 1.1 license: shipped with its license.
  • Questie's data: Questie is published under the GNU GPL v3; Ding/DB is built from its database (through QuestieDB) and keeps its credits (Ding/DB/CREDITS.md, the README, the header of each generated file). Guides only hold quest ids, route coordinates and our notes.

2. Guide format

A guide is a Lua file that calls Ding.RegisterGuide: it is a section of a guide pack. Ding ships five: Ding! - Leveling (id ding, the default, the pack of the guides that have no pack: each section is a stretch of the Leveling routes, a race start then shared sections up to 60 over one or a few zones), Ding! - Zones (id ding-zones: one section per zone, the ids of Ding's first guides), Ding! - Items (id ding-items, category items: one section per item worth getting off the leveling route, for both sides: the Cozy Sleeping Bag first) and Ding! - Class quests (id ding-class, category class: one section per class and side, Ding/Guides/Class/<Side>/<Class>.lua, see "Class quests" below) and Ding! - Dungeon quests (id ding-dungeons, category dungeon: one section per dungeon and side, Ding/Guides/Dungeons/<Side>/<NN>_<Name>.lua, see "Dungeon quests" below). A third-party addon provides a pack by declaring ## Dependencies: Ding, then calling Ding.RegisterGuidePack({ id, title, author, category }) before its sections, which carry pack = "<id>"; like Ding, it must be released under GPL v3 (no exception: everything built on Ding stays free). The author documentation is docs/guide-packs.md, with a tested example pack (examples/DingExamplePack) and the validator tools/check_guides.lua --addon <folder>.

Registry rules: a pack id is unique (Ding's included); a section id is unique across all packs (a pack cannot take another pack's id, but may re-register its own section); an unknown pack is rejected; next may lead to a section of another pack (Ding's included). A pack's category (Ding.GUIDE_CATEGORIES: "leveling", the default, "items", "reputations", "professions", "class", "dungeon") groups it in the guide list, under a header that cannot be picked, leveling first, empty categories left out; an unknown category is "leveling" (debug note, warning of the validator). Picking a pack again takes up the section the character last followed in it (Ding.char.packGuide) while it has quest work left at their level, else the suggested one; the section itself resumes at its Ding.char.lastStep: an item guide and back to the leveling route loses nothing.

Ding.RegisterGuide({
    id       = "nightelf-01-shadowglen",   -- unique, stable
    pack     = nil,                        -- pack (Ding.RegisterGuidePack); nil = Ding's own
    title    = "Shadowglen (1-6)",
    faction  = "Alliance",                 -- "Alliance" | "Horde" | nil (both)
    races    = { "NightElf" },             -- English client names (UnitRace, 2nd value); nil = all
    classes  = nil,                        -- e.g. { "HUNTER" }; nil = all
    minLevel = 1, maxLevel = 6,
    next     = "nightelf-02-dolanaar",     -- next guide, chained automatically; or one per
                                           -- faction: { Alliance = "...", Horde = "..." }
    steps    = { ... },
})

Steps

A step groups one or more actions. It is done when all of its actions are done. The actions of a step run in order: turn in, then accept, then complete.

Key Value Done when
turnin id or list of ids the quest is turned in
accept id or list of ids the quest is in the log (or already turned in)
complete id or list of ids the quest's objectives are met (or it is turned in)
go { uiMapId, x, y } (0-100) the player is within radius meters (10 by default); a point at a dungeon's entrance (Data:DungeonAtEntrance, 1.5 % of the map) is also reached once the player is inside an instance (Compat.InInstance), where the game gives no position (Engine marks it done like a point reached)
level number the player reaches that level (Ding shows the experience and kills left, and a monster to farm); level = 12, xp = 4500 (plus that experience into level 12) or level = 12.5 (that fraction of level 12's experience: Grind:Into, Engine:LevelReached)
sethearth inn name the hearthstone is bound (HEARTHSTONE_BOUND event)
hearth true the hearthstone is cast (UNIT_SPELLCAST_SUCCEEDED, spell 8690), or "Next"
deathskip true (+ text) the player died during the step and is alive again, or "Next"
train true / text a class trainer's window (not a profession's) opened during the step is closed, a spell is learned during the step (LEARNED_SPELL_IN_TAB, not at a profession trainer), or "Next"; behind the option "Class trainer steps" (ui.trainerSteps, on by default)
fly true / text a flight map opens or a new flight path is found (ERR_NEWTAXIPATH), or "Next"; behind the option "Flight path steps" (ui.flightSteps, on by default)
vendor, repair true / text a merchant's window opened during the step is closed, or "Next"; behind the option "Vendor and repair steps" (ui.serviceSteps, on by default)
text text manual validation ("Next" button)

Goals without a quest (Core/Goals.lua, Phase 9), combined with the keys above (the step is done when all of its parts are); a pair is { id, count } (a list of pairs; one pair alone or a bare id are read too):

Key Value Done when
kill pairs { npcId, count } killed count times since the step began: CHAT_MSG_COMBAT_XP_GAIN read with the client's COMBATLOG_XPGAIN_FIRSTPERSON (Compat.FormatPattern, open-ended for the rested and group bonuses), the name matched to QuestieDB's; progress in Ding.char.goals["<guide>#<step>"]
collect pairs { itemId, count } GetItemCount reaches the count
buy { itemId, count, vendorNpcId? } list GetItemCount reaches the count; the arrow on the vendor given, QuestieDB's vendors of the item, else the nearest vendor
talk NPC id(s) an NPC window opens (gossip, quest greeting / detail / progress / complete, merchant, trainer, flight map, bank, stable) with that NPC (UnitGUID("npc")'s creature id, else its name)
gossip option index GOSSIP_CLOSED after a GOSSIP_SHOW of the step; while open, a banner names the line (C_GossipInfo.GetOptions, else GetGossipOptions); never selected by Ding
useitem item id(s) its spell cast (UNIT_SPELLCAST_SUCCEEDED = C_Item.GetItemSpell), its count below the step's start, or the objectives of the step's complete quests advanced; offered on the item button
object object id(s) LOOT_OPENED, GOSSIP_SHOW, QUEST_DETAIL or ITEM_TEXT_BEGIN within 20 yards of one of its spawns (or spots)
equip item id(s) worn in any slot (GetInventoryItemID); offered on the item button
destroy, bank item id(s) none left in the bags (never done by Ding); bank: the arrow on the bank the fewest seconds away (Directory:Best("bank"), the travel planner), not while the bank's window is open (BANKFRAME_OPENED / _CLOSED)
unbank item id(s) in the bags; the arrow on the bank like bank
abandon quest id(s) out of the log (never done by Ding)
keep item id(s) reminder only: Ding.char.keep, never sold as a grey item (Core/Merchant.lua)
spots { uiMapId, x, y } list authored places for kill, collect, object (an area) instead of QuestieDB's spawns

Each part is a line of the window (action = "goal", the kill / objective / use glyphs, a counter 2/8); a click (A) aims the arrow at that part; on a useitem or equip line A uses the item button.

Common options:

Key Role
note tip shown under the step
optional the step can be passed without blocking what follows
faction "Alliance" or "Horde": the step only appears for that side (UnitFactionGroup), in a guide for both sides
races, classes the step only appears for these races/classes
radius arrival radius of a go step, in yards (6 by default)
floor "up", "top" or "down": the step's target is one floor up, at the top, one floor down (the arrow says it near the target and on arrival, see "Floors")
use item id or list of item ids to offer in the item button (the first one owned)
sticky quest id or list: persistent objectives, see "Along the way"
dungeon dungeon name: the step's quests are done inside it
elite true: the step's quests are against elite monsters
when conditions, all must hold (Engine:ConditionsHold): done, notdone, log, notlog, ready (quest ids), item (id or { id, count }), noitem, minlevel, maxlevel; else the step does not apply (Ding:StepApplies), re-read on quest log, level and bag changes
noauto quest id(s) AutoQuest never accepts (Engine:NoAuto); turned in as usual
revive on a deathskip step, { uiMapId, x, y }: as a ghost the arrow goes to that spirit healer ("Spirit healer", reached within 30 yards) instead of the corpse; "corpse" by default
patrol { uiMapId, x, y } list: the arrow's only point is the current one ("text · point 2 of 5"), the next once reached within radius (Engine:PatrolPoint, session state Engine.patrol); until the step's quests, goals and level are done, or "Next" when it has none

Steps behind an option (Route tab, Ding:StepApplies): sethearth and hearth need "Manage the hearthstone" (ui.hearth, on by default), deathskip "Death skips" (ui.deathSkip), dungeon the choice for that dungeon (ui.dungeonChoice[name], a row per dungeon the active guide's pack names, Engine:DungeonNames) and by default "Dungeon quests" (ui.dungeons, Ding:DungeonWanted), elite "Elite quests" (ui.elites), all three off by default. A step whose option is off does not apply, like a step for another race: Advance passes it, AutoQuest ignores its quests, "back to a skipped step" never comes back to it. Turning an option on takes effect from the current step; the steps passed before come back with "back to a skipped step". The window adds the step's tag under it ("Dungeon: Gnomeregan", "Elite quest: better with a group.", how a death skip works). The generator puts these keys on steps of their own, and dungeon / elite on every step of the quest.

next keyed by faction ({ Alliance = id, Horde = id }): the Skyborne start is neutral until the character picks a side around level 12-13; the engine chains to the entry of UnitFactionGroup("player") (Engine:NextGuide), and a character with no side yet, or a side missing from the table, finishes the guide.

Examples:

{ accept = { 456, 458 } },
{ complete = 456, note = "Nightsabers and boars around Aldrassil." },
{ turnin = 456, accept = 457 },
{ go = { 1438, 55.6, 59.8 }, text = "Head for Dolanaar" },
{ train = true, text = "Visit your class trainer." },

The engine generates the text itself ("Accept The Woodland Protector — Melithar Staghelm") in the client's language, from QuestieDB. The guide does not need to spell out the names.

Languages

The texts of a guide (title, text, note, the place of a sethearth) are written in English, the default language. Their translation lives in a separate file, Ding/Locales/Guides/<locale>/<same path as the guide>.lua, loaded after the guides in Ding.toc:

Ding.RegisterGuideTranslation("frFR", "nightelf-01-shadowglen", {
    ["Head for Dolanaar"] = "Direction Dolanaar",
})

The key is the exact English text. A client in that language sees the translated text; any other client, or a text with no translation, keeps the English. A sethearth place keeps its English name in the step (the engine knows the inn by it, Ding.char.hearthInn); its translation is what the window shows ("Lie ta pierre de foyer à l'auberge : Sombre-Comté", step.sethearthText). An inn named for itself in English ("Deepwater Tavern") is translated as its town ("Port de Menethil"). tools/translate_guides.lua fails when a place has no translation, and guide_texts_test when one is left in English. If the English text changes, the translation no longer applies: tools/check_guides.lua reports untranslated texts and orphaned translations.

Shipped languages, one file per language: French (frFR), German (deDE), Spanish (esES, also loaded by a Latin American client, esMX), Brazilian Portuguese (ptBR), Korean (koKR), Simplified Chinese (zhCN) and Traditional Chinese (zhTW). These are the languages in which QuestieDB has quest, NPC and item names (no Italian: the Classic client does not exist in that language).

  • Interface: Ding/Locales/enUS.lua (reference) and one file per language, Ding/Locales/<locale>.lua, loaded only by a client in that language, with exactly the same keys and the same %s/%d in the same order.
  • Guides: translations of the generated guides are not written by hand. Each language has its own dictionary, tools/routes/locales/<locale>.lua ([English sentence] = translation: zone names, route introductions, automatic notes, notes of the Shadowglen guide). tools/translate_guides.lua writes each guide's Ding/Locales/Guides/<locale>/... files and their lines in the TOC; a note that chains several sentences is translated sentence by sentence, and a title keeps its level range ("Westfall (11-16)": the dictionary names the zone). tools/draft_guide.lua does the same for the guides it generates.
  • Adding a language: copy Ding/Locales/frFR.lua and tools/routes/locales/frFR.lua under the new code, translate them, add the code to M.LOCALES in tools/guide_texts.lua and the interface file to the TOC, then run tools/translate_guides.lua.
  • Font: Noto Sans covers Latin and Cyrillic. A Korean or Chinese client keeps the game font (UI.GAME_FONT in UI/Widgets.lua).

Guides as text (import and export)

A guide can also come as text, pasted in the game (public page: docs/create-a-guide-with-ai.md): one Lua table literal, either a guide { id?, title, category?, faction?, races?, classes?, minLevel?, maxLevel?, next?, steps } or a pack { pack = { id?, title, author?, category? }, guides = { ... } }, with the keys of section 2 above.

  • Reading (Core/GuideText.lua): a reader written by hand, never load, loadstring, setfenv nor RunScript. It takes tables, texts (quoted or long brackets, .. between texts), numbers, true / false / nil and comments; any other word (a function, a call, a variable) is refused. Limits: 300 KB, tables 10 deep, 100,000 values. A code fence (```lua) around the table, as chat AIs write it, is read through.
  • Checks (Core/Import.lua): only the keys of the format (a whitelist per guide, pack, step and when), each with its kind of value; the registry's rules (lists of ids, go as three numbers); QuestieDB: quests, NPCs and objects exist (an NPC or an object is only a warning when the step has spots or go), items (a warning), the quests taken are open to the step's races (its races, else the guide's, else its faction's), the guide's races are on its side; coordinates within 0-100 on a map the client knows; levels, text lengths (1,000), steps (1,000 per guide), guides (50). Errors block the import, warnings do not; each message is localized and says the line, the guide and the step.
  • Ids: kept unless missing or taken by a pack of an addon or Ding's (or a guide of another pack): then user-<id> (user-<title> when missing), and next follows a renamed guide. Importing the same id again replaces the imported guide.
  • Storage: DingDB.imported[packId] = { pack, guides, at }, account-wide. At login, once every addon has registered its packs and before the engine starts, each entry becomes a pack (pack.imported = true, its category) with its guides; an addon that took the id since moves the import to a user- id. A guide imported alone is a pack of the same id. Deleting it (Ding.UnregisterGuidePack) sends a character on it back to the pack it was on before (Ding.char.prevPack, its section and step), else to Ding's guide.
  • Export (Import:Export(id, compact)): a pack, or a guide (the active one by default), written back with the same keys in a fixed order (a lone id written bare), one step per line; compact: one line, | written \124. A pack of one guide of its own id comes back as that guide. What is exported reads back without error, and exports the same.
  • Window (UI/ImportWindow.lua, /ding import, /ding export [id], the Guide tab's page Guides as text): the text in a scrolling box, the report in a second box (plain text, to copy back to the AI: "Error: Step 3: ..."), buttons. Gamepad: D-pad left / right between the buttons, A, Y selects the text or the report for Ctrl+C, B closes; temporary bindings, closed in combat. Mouse: the buttons, Esc. Opened from the settings, closing it comes back to the page. The window never takes the keyboard from a box that is not Ding's (the game's chat, which still has it while /ding export runs from it, and keeps it with the gamepad interface): the text takes it once the keyboard is free, the status line says so meanwhile.

Class quests (Ding/Guides/Class)

The pack Ding! - Class quests (Ding.CLASS_PACK = ding-class, builtin = 4, category class) holds one section per class and side: class-<side>-<class> (Ding/Guides/Class/<Side>/<Class>.lua, hand-written like the Items guide, English texts translated through tools/routes/locales). Each has faction and classes = { <CLASS> }; its race-specific chains carry races on their steps, and a guide whose every quest is for some races only in DingDB carries them itself (the Alliance paladins: Humans and Dwarves).

  • Content: the class's important chains in level order, the level that opens each one (from the quests' required level): a druid's forms, a hunter's first pet, a warlock's demons and mounts, a warrior's stances and Whirlwind weapon, a paladin's Redemption, Verigan's Fist and mounts, a shaman's totems, a rogue's poisons, the priests' and mages' race and class quests, up to the level 50 chains of the Temple of Atal'Hakkar and the level 60 epic mounts. Forever's own class quests are in: Skyborne starts, the Great Cat Spirit at 20, the Forsaken paladins, the Alliance shamans, the Human, Gnome and Skyborne hunters' pets, Delgren's paladin chain, the Syndicate of Ravenholdt, Rau Cliffrunner's and Veyric Thunderhame's shaman quests. Raid and armor-set quests of level 60 are left out. Where DingDB has no prerequisite (most of Forever's quests), the order follows who gives and who takes each quest.
  • Plain words: the first step of a chain says, under it (why with short), from which level and what it is for ("From level 10: the chain that teaches Bear Form."); the step that teaches something says so ("Turning this in teaches Bear Form."); notes say where the NPC or the objective is. Breadcrumb quests (a trainer sending you to another) are optional.
  • Options: a quest done inside a dungeon has dungeon = "<name>" on all its steps and on the quests that follow it (per-dungeon choice in the Route tab, off by default), elite quests elite = true: with the option off, the guide goes on without them.
  • Picking it: Engine:SetPack(Ding.CLASS_PACK) takes the section of the character's class and side (Ding:SuggestGuide); the Guide tab's section list shows that one only; the setup assistant's last screen names it under the pack. A class guide keeps its grey quests (Engine:KeepsGrey). It never leads to another section: the player goes back to their leveling pack in the Guide tab; the Leveling routes pass a class quest the player already did.
  • Checks: tools/check_guides.lua replays a class guide for each race of its side (and the Skyborne its steps name) with its class only; a quest's own id in its preQuestSingle is how the data writes a group of alternatives (Call of Earth), not a prerequisite. tools/tests/class_quests_test.lua: one section per class and side, the right class only, the section list, the assistant, every quest, item and NPC in DingDB, every text in every language; the smoke test plays each one with the race of its side that has the most steps.

Dungeon quests (Ding/Guides/Dungeons)

The pack Ding! - Dungeon quests (Ding.DUNGEONS_PACK = ding-dungeons, builtin = 5, category dungeon) holds one section per dungeon and side: dungeon-<side>-<key> (Ding/Guides/Dungeons/<Side>/<NN>_<Name>.lua, numbered by level, hand-finished from the drafts of tools/draft_dungeon.lua, English texts translated through tools/routes/locales). Each has faction, a title " (-)" from the levels the game recommends (Forever's own dungeons: the levels of their quests) and no next.

  • Content: every quest of the dungeon the side can take (filed under the dungeon, or with an objective found only inside it), the chains that lead to them from where they start (prerequisites, and breadcrumbs as optional steps), the quests a drop of the dungeon starts, and the follow-ups the quest's ender gives (the chain's end, its reward). Order: the trips before the dungeon form the shortest path found that ends at the entrance (flight paths, boats, mount: tools/route_travel.lua), then a go step to the entrance (DingDB's), the dungeon's quests in the order of its bosses, then the turn-ins from the entrance. A chain that needs a second visit (a capital between Kharan's tale and the emperor) enters again. Classic dungeons from Ragefire Chasm to Stratholme, Lower Blackrock Spire included, and Forever's own (the Hall of Thanes, the Ruins of Lordaeron, Excavation Site: Wetlands, the City of Dalaran). Left out: raids and their attunements, the Upper Spire (a raid of ten), the level 60 armor-set chains, the class books and librams of Dire Maul, profession and repeatable quests, world drops, long class chains (Ding! - Class quests leads them), and quests whose giver or ender DingDB does not know.
  • Plain words: the arrow does not work inside: every objective after the entrance has a note saying where it is and which boss or monster drops what. The first step of each chain says in its why (short) what it brings (an item for which kind of character, a big XP reward, "about" figures), whether it is long or far, an elite or group fight; nothing the data does not say. Very long or far chains are optional on every step and say why.
  • Options: the dungeon pack's steps carry neither dungeon nor elite: picking a dungeon's guide is the choice (the Route options would hide the whole guide). A quest a drop starts has when = { item = id } on its accept, then when = { log } / { done } on what follows (turn-in and next accept on separate steps): nothing blocks when the drop never came.
  • Picking it: Engine:SetPack(Ding.DUNGEONS_PACK) takes the dungeon of the character's level and side (Ding:SuggestGuide); the Guide tab's section list shows the character's side only, by level, the ones above the character's level greyed (they can still be picked); a dungeon guide keeps its grey quests (Engine:KeepsGrey).
  • Checks: tools/check_guides.lua replays each guide for every race of its side (0 error); tools/tests/dungeon_quests_test.lua: the pack, sides and level order, the section list, the assistant, the entrance step passed from inside, a note on every step inside, every quest, item and NPC in DingDB, every text in every language; tools/smoke_test.lua --only dungeon- plays them all.

3. Engine

Progression

  • The engine automatically skips the steps already done: when a character is picked up partway through, the guide realigns itself.
  • Resuming: for 10 s after a guide is loaded (login, guide change, "restart"), a manual step (go, train, fly…) counts as done if a quest further on in the guide is already turned in. Quest data that arrives after login therefore still realigns the guide. A quest that is only accepted further on is not enough (it may have been picked up early), nor is a turn-in past a step with a quest of the log to complete or turn in (the player is not past that step: those quests were done out of the guide's order). After that, a manual step is passed only by "Next" or by progress in the next 5 steps.
  • Guide choice for a character Ding does not know yet, among the compatible sections of Ding's Leveling pack (its Zones pack when Leveling has none for the character), or of the chosen pack (see below), in this order: the first guide started and not finished (unless the character is past its maximum level); otherwise the first guide not started that covers the character's level; otherwise the last guide not started below that level. A guide counts as "started" only if one of its quests was accepted or turned in (a quest out of reach does not count).
  • The player can switch guides at any time with the two drop-down lists of the Guide tab: the guide (Ding's four "built in" packs, Leveling, Zones, Items then Class quests, then the installed packs by title, with their author and the levels they cover for the character; grayed out if it has nothing for them), then the section (only those of the chosen pack, including those for another race or class, grayed out; a pack of category class lists the character's own sections only). Choosing a pack loads its section for the character (same rule as above, otherwise its first section) and resumes it where the player is; choosing the active pack again changes nothing.
  • The pack is remembered per character (Ding.char.pack). At login, if the remembered section is gone (pack updated), a section of that pack is offered; if the pack is gone (addon disabled), one from Ding's pack.
  • It listens to QUEST_ACCEPTED (a single questID argument on Forever), QUEST_TURNED_IN, QUEST_LOG_UPDATE, QUEST_REMOVED, PLAYER_LEVEL_UP, HEARTHSTONE_BOUND.
  • A turned-in quest is remembered immediately, because IsQuestFlaggedCompleted can answer with a slight delay.
  • A quest whose required level is not reached yet is flagged ("level 3 required") without blocking the rest of the guide.
  • If a step must complete or turn in a quest missing from the log (skipped for its level, abandoned…), Ding first shows "Accept …" and the arrow points to the quest giver.
  • A step that turns in a quest of the log whose objectives are not done never says only "Turn in": "Complete …" and its objectives come first (the next steps preview too), and the arrow goes to them, then to the NPC (in game, 13/10/2026: "Ormer's Revenge" to turn in without its raptor's talon, its "complete" step passed by a resume).
  • Catching up on prerequisite quests: a guide quest that cannot be accepted yet because an earlier quest in its chain was never turned in (preQuestSingle: any one of the quests is enough, preQuestGroup: all of them, according to QuestieDB) first shows what is left to do on the first missing link (accept, complete or turn in, "before …"), and the blocked quest in red. The arrow and automatic accepting follow that link. The quests of the step itself and those closed to the player's race or class do not count.
  • Gray quests (setting, off by default): a quest that is gray for the player's level (the client's GetQuestGreenRange, otherwise the Classic formula), missing from the log and not a prerequisite of any quest in the guide, is skipped and never accepted automatically. Only on a leveling route: a guide of a pack of another category (items, reputations, professions, class; Engine:KeepsGrey(guide)) is followed for its reward and keeps them.
  • Skip reason: under the current step, the panel says why the guide just passed steps without the player: "Skipped: … (gray quest)", "For later: … (level N required)"; and every step for the character it passed by itself in play, never shown (Engine.passNotes): "Step passed: “Climb the trail” — you are already there." (a trip whose place was reached), "… — nothing is left to do there", "— you are already further on", "— already done", "— marked as done by hand", "— optional", "— it does not concern you now"; two lines at most, then "and N more", until the guide moves again. Not the step that was on screen (kept 1.5 s in green), unless it had only just come; none in the first seconds after a guide is loaded. In game (13/10/2026), the Wetlands' 30 -> 32 (the trail climbed already) went by without a word and read as a jump.
  • Step numbers: the window ("Step 18 / 74"), the minimized window ("18/74"), "Go to step N" and its chat line, and the NPC quests panel ("step N") number the character's steps only (Engine:StepNumber, Ding:StepForCharacter: its race, class and options, not the when conditions), never the guide file's index, which counted other classes' steps (Yendi read 111 -> 114 as a jump). The import and export reports, for guide authors, keep the file's.
  • An optional step is passed as soon as a later step makes progress, or right away if its quest was never accepted (starting item never dropped).
  • After "Previous", the engine stays on the displayed step until "Next". Browsing is one step at a time both ways: "Previous" shows the step before (the previous one for the character's race, class and options), done or not; "Next" then shows the step right after the one on screen, done or not, marking nothing, until the guide's current step is back (Engine.browseFrom, Engine:IsBrowsing()); only there does "Next" mean "mark done and move on" (in game, 13/10/2026: "Next" after "Previous" ×N marked the step shown and jumped to the next step still to do). The preview under a step browsed back lists the steps "Next" will show. "Go to step" on a step behind in the section shows it the same way (Engine:BrowseTo), the gamepad's LB / RB in focus mode too; while browsing, the saved step stays the guide's current one. The step menu's "Mark as done" (Engine:MarkDone) still marks the step shown and goes on to the next step still to do.
  • Skipping a step (binding, right click on "Next", /ding skip): goes to the next step to do without validating this one, which is not timed. Going back to a skipped step (binding, right click on "Previous", /ding back) brings back the nearest step not done before the current step; once it is done, the guide moves forward again. Like any manual step, a skipped manual step counts as done as soon as a later step makes progress.
  • Resume where I am (settings, guide list, /ding guide, first guide): the guide starts over from the beginning and passes what the game shows done (quests turned in, accepted or ready, level reached) and what the player marked. The quests alone never place the player further: quests done out of the guide's order, in another guide, look like progress (in game, 13/10/2026: Redridge done before Duskwood put a hunter in the middle of Duskwood at the end of Redridge, 70 steps ahead; then the Wetlands done at level 24 put the same hunter, back in Darkshire, at step 73 of the route's Wetlands; each time the guide passed the quests, trainers and trips still to do, and "Next" passed several steps at a time). A section picked again (guide list, another pack and back, an update of the guide) is taken up to the step the character was on in it (Ding.char.lastStep): up to there the guide also passes what the player left behind, so the steps the game cannot check later (a trip, a trainer, the inn, a step behind an option turned on since, a goal whose items are spent) are not asked again, but it stops on any quest from the guide's past that is still in the log and remains to be completed or turned in (a quest ready to turn in counts: Yendi's feedback, "Filthy Paws" skipped); a step whose log quests are only accepted does not stop it; a step that turns in the quest the player has just turned in (30 s) does: its sequel comes a moment later from the same NPC, and passing on would leave the steps that take and complete it behind. Then it carries on to the player's place. The steps left behind remain reachable through "back to a skipped step". "Take this guide up where I am" forgets the marks and keeps that place: it takes the section up from its start to there (a strict resume) and stops on what is really left, a quest of the log with work left or a quest of the route the character can still take (not optional, not behind an option, open to it now); what the player left behind on purpose passes, the quests behind an option turned on since included (in game: "Elite quests" turned on at step 38, the button used to send the player back to step 5, an elite quest never taken). A character saved before v1.4.0 drops its resume in progress once and keeps the step it is on as its place (Ding.MigrateChar). A sethearth step whose bind happened during the step stays done while the hearthstone is bound there (Ding.char.hearthInn: the client's own name of the inn then, so the language does not matter), also after "Take this guide up where I am".
  • Login: the quest log arrives a moment after login. The engine places the player only once the log has been received (first QUEST_LOG_UPDATE, 5 s at most): without the log, a resume would believe every quest is missing and would skip the ones to turn in.
  • Updated guide: a new version of Ding can regenerate a guide and move its steps. Ding.char.guideSig keeps the signature of the active guide (its quest actions and its level milestones, in order); if it changed at login, the guide is resumed (up to the section's saved place, see above) instead of staying on a step number that is now wrong.
  • Trips with nothing left at the end: a step that only takes the player somewhere (go, fly, hearth, nothing else) is passed as soon as the next step for the player is already done by the game's own state (its quests turned in, accepted or ready, its level reached), or is itself such a trip. A manual step next (class trainer, inn) keeps the trip.
  • At the end of a guide, the engine chains to next if it exists.

Automatic accept and turn-in (Core/AutoQuest.lua)

When the player talks to an NPC (gossip, greeting or quest windows), Ding accepts and turns in only the guide's quests: those of the current step and the following ones, 5 steps at most, stopping after a step that changes location (go, fly, hearth). Steps for another race or class are ignored. A quest to complete or turn in that is missing from the log is accepted again from its giver.

  • Along the way (Yendi's feedback, Captain Rugelfuss): a quest the guide accepts further on is accepted if the NPC offers it, it is not above the player's level and the log holds fewer than 15 quests. It is recorded in Ding.char.early: a quest accepted early says nothing about the player's place and never passes a manual step (travel, class trainer), nor does it count for resuming; except a trip whose next step it completes: nothing is left at the end of that trip. Turn-ins stay at their step.

  • Above the player's level: the routes respect "never above the player's level" for a simulated player. In game, a player who has fallen behind (skipped quests, deaths) sees such a quest in orange with its level ("· Level 16", on its line and under its objectives) and carries on: it is accepted like the others. Holding it back until the right level (v1.1.0) forced too much grinding (Yendi's feedback). Only the quests accepted along the way, ahead of the guide, still wait for the player's level.

  • Turn in first, then accept; one quest per conversation (the client closes the window).

  • A quest whose objectives are not met is never selected.

  • The reward is taken only if there is no choice (or a single item); with several, see "Quest rewards" below.

  • Two settings (Guide tab, on by default); holding Shift (LT on the gamepad) suspends everything.

  • A selection that has no effect is retried only after 2 s (no loop on GOSSIP_SHOW).

Quests an NPC offers (UI/Offers.lua)

Yendi's question in game: the NPCs offer quests the guide does not ask for. Next to the game's window (gossip, greeting or quest details), a small Ding window, "Ding! · These quests and your guide", answers "should I take this quest now?" in plain sentences, without color codes (Yendi's feedback: colors are hard to decode). Each quest the NPC offers (its level and its name in the client's language, from DingDB, in the text color) gets one or two sentences:

  • "Take it: it is the guide's current step." / "...: the guide takes it here, at step N." / "...: the guide's next quests need it." (a catch-up) / "...: step N of the guide needs it and you do not have it yet." (no step takes it from here on, a later step completes or turns it in).
  • "The guide does this quest later (step N)." then "You can take it now: it saves you a trip back." when AutoQuest would take it on the way (not above the player's level, fewer than 15 quests in the log), else "It is too high for you for now (level X, you are level Y)." or "Your quest log is almost full: keep it for later."
  • "The guide does this quest when you come back here, in “
    ” (levels a-b)." (a later section of the active pack).
  • "The guide took this quest earlier (step N, already passed): do it if you like." or "..., in “
    ” (levels a-b): ..." (a section before the active one).
  • "The guide does not do this quest: ." with the real reason when Ding knows it: an elite quest (option off), done in a dungeon (option off), for another race or class, an elite quest for a group (an objective with elite monsters only, or the quest before it in its chain: the generator's own rule), or the route's own reason through Offers:WhyLeftOut(q), the one place that asks Ding.Why:Quest(q) (the generator's reasons, when shipped: a clause or { text }). "The guide skips this quest: it is gray, too low for you." ("Skip gray quests").
  • Otherwise, honestly: "The guide does not plan this quest." (plus "It is too high for you for now (level X, you are level Y)." as a fact, never as the reason).

A quest above the player's level adds "It is level X, you are level Y." Shown only when it helps: one quest at least that the steps at the player's place do not take; when the guide takes every quest offered right here, nothing shows. Then every quest of the window has its row, in the window's order; names and sentences wrap, never cut.

Steps are read without their conditions (when can hold later); each section is indexed once. The greeting window only gives titles: they are matched to the quests the NPC starts (Data:NpcQuests), else to the pack's quests by name. Information only: no button, no mouse, nothing for the gamepad to press. The game's windows are only read (GossipFrame, QuestFrame: shown or not, where they stand), never anchored to, changed, shown nor hidden (with the gamepad interface, touching them taints the game's frame controls). Shown 0.2 s after the window opens (the game shows it first; a quest AutoQuest takes closes it meanwhile), hidden with it (GOSSIP_CLOSED, QUEST_FINISHED, or the window seen hidden). Option "Quests an NPC offers" (Automation tab, on by default).

Why the guide makes its choices (Core/Why.lua)

Yendi in game: sent from Duskwood to Westfall and back without knowing why, he doubted the guide. The player must understand the guide's choices in plain words (never a color or a code), without drowning the window, and only with what is true:

  • Under the step, one short line, only for a surprising choice (a step's why with short = true): a detour (a few steps in another zone, or one step far off the way: "Pourquoi ce détour (environ 5 min) : rendre « La légende de Stalvan » ouvre une suite de quêtes qui rapporte beaucoup d'XP (environ 13 000)."), a quest taken now or never (turned in on another continent, the route never back here before). It goes after the guide's note, under the first row still to do; never under the finished step kept on screen. "A lot of XP" from a quarter of the player's current level; figures with "about".
  • On demand, the whole reason: the tooltip of the step's own rows (the mouse over a row; X in focus mode), under "Why?": the detour's cost beyond the direct way, the quest it names and the chain it opens (follow-ups the guide does next), the whole detour's XP; when a carried quest is turned in; the levels of the quests after a level step; then "Figures are the guide's estimates." A step without a reason: no "Why?" at all (Yendi: on nearly every card it said nothing). A row without a quest (a trip, a level, a goal) gets a tooltip of its own (its text, then why).
  • Quests left out: a section's leftOut ({ [quest] = reason }) says why it does not take a quest (too little XP for the detour, with its figures; its giver, objectives or turn-in off the route; the log full there; a prerequisite or another version the route does not do...). Why:Quest(q) reads the active section, then the other sections of its pack for the character, for the panel beside an NPC's window (UI/Offers.lua): a clause that ends its sentence, "The guide does not do this quest: it is worth too little XP for the detour (about 300 XP for about 4 min)."
  • Honesty: the Leveling routes come from older routes; for many choices only the measured effect is known. The reasons are computed offline (tools/route_why.lua, §5) from the route's simulation and QuestieDB's chains, and say nothing else: a detour that also visits a trainer, a vendor, a bank, a flight master or an inn (what the simulation does not value) gets no reason; a reason holds only when every race that plays the step finds it.
  • Option "Guide explanations" (Ding.db.panel.why, Guide tab, on by default): off, no line under the steps and no "Why?" in the tooltips (Why:Quest still answers: the NPC panel has its own option). Texts through Ding.L (WHY_*); a pack's own why.text and leftOut texts are translated like notes (§2, Languages).

Choosing the destination point

Current action Candidate points
turn in NPC or object that ends the quest
accept NPC or object that starts the quest; for a quest started by an item, the monsters or objects that drop it
complete spawns of the monsters, objects and item sources of the objectives not met yet (matched by objective name)
go the given point

The candidates of all the step's unfinished actions are offered together (turn-ins of ready quests, quest givers, objective targets): among them, the arrow points to the one closest to the player (real distance in yards, computed in world coordinates), chosen again every second. Turn-ins still waiting on their objectives come only when nothing else is left, then the step's go point.

Travel (Core/Travel.lua)

Yendi's feedback: the arrow went in a straight line, whatever the distance. The arrow's target (the nearest candidate) goes through a travel planner, the fastest way in seconds:

  • Legs: walking (7 yards per second, 11 from level 40, 1.2 times the straight line), the flight paths the character knows (30 yards per second, 1.25 times the straight line, plus 15 s to take off; between two known flight masters of the same continent open to the player's side), boats, zeppelins and the tram (crossing plus half a round trip of waiting), and the hearthstone from where the player stands to the inn (30 s, plus its cooldown). Each change of transport must save 20 s. Dijkstra over the player, the target, the known flight masters, the docks and the inn.
  • Flight masters never seen (of the player's side): talking to one learns its path, so a flight may start there (never end there), 60 s slower for the doubt. The arrow then says "Try to fly to Lakeshire, Redridge".
  • Measured flights: each flight taken is timed from TakeTaxiNode to the landing (PLAYER_CONTROL_GAINED); between 10 s and 30 minutes, it joins the average of that route (DingDB.flightTimes["<from npc>:<to npc>"] = { seconds, count }, at most 20 counted, account wide), which replaces the estimate from then on.
  • Teleports, hearthstones of their own from where the player stands (30 s plus the spell's cooldown): the mage's cities of the player's side when the spell is known and a Rune of Teleportation is in the bags, the druid's Moonglade. Each lands where a QuestieDB NPC of the city stands. The arrow keeps the target with "Teleport to Stormwind City" and the item button casts the spell (type "spell", out of combat like any attribute).
  • Dangerous zones: a walk is sampled every 400 yards (at most 6 looks, the zone under each cached per 300-yard cell); the share of looks in a zone more than 4 levels above the player (Classic zone levels, Travel.ZONE_LEVELS) makes the walk up to 2.5 times as long, so a flight or a boat around wins sooner.
  • Walking passages (Core/Passages.lua, Yendi's feedback: the arrow pointed through cliffs and walls): lifts, tunnels, bridges, city gates and portals, each joining two sides of the world (Passages.SIDES: lists of zones, a box of a zone, or "*" for anywhere else). A walk from one side of a pair to the other is never straight: both ends of every passage are nodes of the plan, and the way through costs the walk from end to end, or a fixed time for a lift (45 s, its wait and ride) or a portal (10 s). A place on neither side of a pair is not held back by it. The ends are { map, x, y } as /ding where prints them (zone: the zone the place lies in when it is read on another map, a lift's foot on its city's map); faction keeps a passage to one side. Data: Thunder Bluff's two lifts, the Undercity's three, the gates of Ironforge, Stormwind and Orgrimmar (two), the Rut'theran portal, Thandol Span, Dun Algaz and the Great Lift, placed where DingDB's guards and NPCs stand at each of them and where the Leveling routes go through; each carries verified (false until someone checked it in game; the planner takes it either way). A lift whose two ends lie on the same map (Freewind Post) cannot be told apart without the height and is left out.
  • Islands: pairs of sides without a passage (Teldrassil with Darnassus, the Skyborne's Zephras Isle): no walk over the sea, only a boat, a flight, a portal or the hearthstone. Darnassus to Menethil Harbor: the portal down, the boat to Auberdine, the boat to Menethil.
  • /ding passages [n], for testers: the five passages of the continent nearest to the player, each with its hint, its distance, "checked in game" or "not checked yet", and both ends as /ding where prints a point (a = { 1456, 31.9, 66.6 }, b = …); the arrow points at the n-th (the nearest by default; the end on the player's side) until the step changes. A wrong point is reported with /ding where standing on the right spot.
  • Itinerary: when the plan takes anything but a walk, the window writes it in the travel notice (under the step), "Route: walk 1:20, fly 6:30 to Redridge Mountains, walk 0:40 (8:30 in all)" (Travel:Itinerary(Travel.plan)): each leg that is not a walk names the zone it leads to; a passage counts ("lift 0:45 to Thunder Bluff", tunnel, bridge, gates, portal).
  • A continent never seen: no flight map opened there yet (Ding.char.taxiSeen[continent], set on TAXIMAP_OPENED; a flight path known there counts too) and a flight master of the player's side within 400 yards: a row "New continent: talk to … (flight master) to learn its flight paths" under the step (not on a fly step); a click (A in focus mode) leads there.
  • Taking the flight automatically ("Take the flight automatically", Automation tab, Flights block, off by default, Yendi's decision): when the flight map opens and the plan flies the player to a node of that map they can reach, out of combat and without Shift (LT) held, Ding takes that flight (TakeTaxiNode(index) through Compat.TakeTaxi: the only call of the taxi API Ding makes, allowed by name in taint_test; the game's flight map frame is never touched), after Dismount() when mounted, and says "Flying to …" in the chat. Off, or anything else: only the highlight of the destination on the flight map.
  • Data: flight masters from QuestieDB (npcFlags bit 8, friendlyToFaction), zeppelin towers and the Booty Bay - Ratchet boat where their masters stand in QuestieDB, the tram where QuestieDB's dungeon table enters it (Forever coordinates); only the Alliance boats (Menethil - Auberdine, Menethil - Theramore, Auberdine - Rut'theran), with no NPC on their piers, come from Ding's own table.
  • What the character knows, learned in game and kept per character: the flight paths when a flight map opens (TAXIMAP_OPENED: C_TaxiMap.GetAllTaxiNodes, else the legacy taxi functions; each reachable node matched to its flight master within 150 yards; the map's coordinates are trusted only if its current node is the flight master next to the player, which is known in any case), the inn when the hearthstone is bound (HEARTHSTONE_BOUND) or where it lands (loading screen, or a jump of the position); a hearthstone bound elsewhere since (its name changed) is not used.
  • The arrow follows the first leg that is not a walk: it points to the flight master with "Fly to Lakeshire, Redridge", to the dock with "Take the boat to Darkshore" (zeppelin, tram), or keeps the target with "Hearthstone to Goldshire" (the item button then offers the hearthstone), or to a passage's entry with "Take the lift up to Thunder Bluff", "Go through the tunnel to Loch Modan", "Enter Ironforge through its gates"; within 60 yards of a passage's line (on the lift, in the tunnel; not a portal), to its far end, then on to the target: within 25 yards of a far end at once, even on a plan made a moment ago (a lift: once its ride changes the map). The NPC directory's ways ("· by flight, boat") leave passages out, like walks. The window repeats it in a notice under the step. Once at a dock, the arrow waits for the far side ("on board") until the player gets there (5 minutes at most after leaving the dock). On a flight, the plan starts where it lands (TakeTaxiNode, watched with hooksecurefunc). A go step is still validated on its own point.
  • The plan is made again when the target changes, the player moved 150 yards or to another map (a lift goes up without moving on the map), or after 15 s.
  • Flight map: when the plan flies the player, a Ding banner names the destination ("Fly to …") and a ring marks its node, both Ding frames on UIParent placed where the game's node button stands (only read): Ding never anchors to, shows, hides nor changes the game's flight map.

Persistent objectives (sticky)

A step's sticky quests join a per-character set (Ding.char.sticky) once the step is reached; while such a quest is in the log and not ready, its unfinished objectives are listed under every following step (with the along-the-way rows, first, under a header "Another quest of your log, to finish along the way: [29] “quest”", whatever the distance; never twice, never under a step that names the quest, nor under a step seen again with "Previous": in game, 13/10/2026, "The Cursed Crew" listed at 0/13 in red under a step done read as that step's objectives; nor, whatever the distance, while the next step still to do works on that quest: it comes right after, and among its monsters it is an ordinary along-the-way row; not the red of the step's work: Guide window rules 9 and 10), and their targets are low-priority arrow candidates on the steps with quests (not on a trip: a go point must stay the arrow's to be validated): such a point counts as twice its distance plus 300 yards. A quest turned in leaves the set, and so does a quest the active guide only makes sticky further on than the step shown ("Previous", or the guide taken up earlier): never "Still to do" before its step. In game (13/10/2026), "The Night Watch" (57), made sticky by a jump past its step, listed its monsters in red under the finished "The Night Watch" (56). The quests of an earlier section stay.

Section wrap-up

Past the section's maxLevel (and with a next), three rows under the step: "Next section: …" (takes it up where the player is), "Turn in first (N steps), then the next section" (only the turn-in steps of quests still in the log stay, then the guide chains; Ding.char.wrapUp) and "Stay in this section" (remembered, Ding.char.stay). A click or A in focus mode.

Next steps preview

"Next steps shown" (Guide tab, 0 to 3, off by default): under the step's rows, one dimmed line (state "preview") per next step still to do, the ones "Next" goes to one after the other (not the steps a resume in progress passes, Engine:NextSteps): its first line still to do (Engine:Describe(step, true), without the quests the engine passes: level too low to take them, grey). A quest line names the quest with its level and how many other quests the step still deals with: "Turn in [2] “Name” and 1 more quest" (L.QUEST_WITH_LEVEL, UI.More); a step without a quest keeps its own text. A "Next steps" header row sits above them (dimmed rows alone looked like work to do now).

Plain words

Yendi (v1.4.0): the player must understand everything in plain words; no meaning carried only by a color, an icon or developer shorthand. Every code also says itself in words, where it clutters least:

  • A row's card (hover, X in focus mode; Panel:RowMeaning(row), Panel:ShowQuest): first what the row's look means: "Still to do." (red), "Done." (green), "Not possible yet: level 12 required." / "…: take the quest first." / "…: “Name” comes first in its chain." (grey; the row's requires or waitsOn), "Marked done by hand: the game has not confirmed it." (violet), "Skipped for later: still to do." (amber), "A step further on…" (dimmed), "Not this step's work: another quest of your log, whose targets are right around you…" (along the way), "Not this step's work: another quest of your log, which the guide keeps on screen…" (persistent), "Not a step of the guide: a suggestion besides this step." (training, weapon, upgrade, talent, wrap-up, a quest found elsewhere), a quest header's quest: "In progress: its objectives are below." / "Objectives done: ready to turn in." / "Turned in: no longer in your quest log.", a goal's lone number ("4 still in your bags."). Then a quest's level ("Quest level 29", the window's "[29]"; above the player's, the orange: "Quest level 29, above yours (27): harder, but the guide plans it."), "· before “Name”" in a sentence, and the action in the gamepad's words in focus mode ("A: the arrow goes there.", "Right: find this quest in the guide."). A row without a quest gets a card too (its text and these lines), except section titles, notices and controls, whose text says it all. A gear upgrade says how much better in words, no bare "+12": "Clearly better than [worn item] for your class."; its card: what it replaces, which stats change, "the guide weighs each stat for your class".
  • Quest lines done say it in their text, like the objectives: "Accepted / Turned in / Completed [2] “Name”".
  • Minimized window: its card says the row, its state and "Step 3 of 42" (the "3/42").
  • Focus mode (gamepad): a hint bar under the progress says what A does on the focused row (Arrow, Use, Open, Select, Go; nothing when the row carries the A chip), X (Details, when the row has a card) and B (Leave); the directory's button, focused, shows its name.
  • Lists cut short: "and 2 more" (UI.More), never "+2".
  • Elsewhere: the arrow's "distance unknown" (not "?"), its farming loop "name · farming loop: 2 of 10" (the label wraps, never cut); the minimap pins say what they are when hovered; the nameplate mark's tooltip line "Kill for / Loot for “Quest” 3/8 · current step", "Quest to take / to turn in"; the item button's tooltip says why this item ("For “Quest”.", "The fastest way…", "An upgrade…"), its cooldown and a dimmed look; the NPC directory's "8 min away" and "by flight, boat"; the offers panel's "Name (level 24)"; the settings' "levels 10–20" and "from level 20".

Guide window: rules

The guide window is Ding's one interface the player looks at all the time: every rule below holds in every language, from 240 to 420 wide and at every scale (50-150 %), with the mouse and with a gamepad. tools/tests/panel_rules_test.lua (French) and panel_rules_enus_test.lua (English) check each one, the test named after its number, on the scenarios of tools/panel_scenarios.lua (Leveling guides, Yendi's reports in game); tools/panel_snapshot.lua writes the window as plain text (rows, their look in words, counters, sub-lines, notices, cards, the step menu), and the French snapshots of tools/tests/snapshots/ show every change of the window in a review (panel_snapshot_test.lua).

Information apart from the step

  1. Informational messages are clearly separated from the quest steps (Yendi's first rule): the step's own work (its quest headers, objectives, accept / turn-in / complete rows, a trip, a level, the trainer, its goals, with the guide's note, the step's hints and its why line as their sub-lines, and the undo of its mark) comes first, in one block; then a 1-unit line and a "Good to know" title (11 SemiBold, dimmed); then everything else, never with the look of an objective (no to-do / done state, no state background): the way there, a trip of the NPC directory and its "Cancel the trip", steps passed, other quests' objectives, the rows besides the step, bags and quest log almost full. "Next steps" come last, under their own line and title. None of these is ever above or between the step's rows.

Words

  1. Nothing said by a color or an icon alone. Every row but a section header, a notice and a control has a card (hover; X in focus mode) whose first line says what the row's look means (Plain words, above): to do, done, not possible yet and why, marked by hand, skipped, a step further on, another quest's objective and why it is there, a row besides the step; a quest header says where its quest stands (in progress, ready to turn in, turned in, not taken). A section header, a notice and a control say it all in their text. Cards stay short: an objective's card is that objective (its row's text, what its look and its counter mean: "3 of 7 so far."), then its quest in one line ("For [25] « Name »: 1 of 3 objectives done."), never the quest's whole list again; the quest's header and its other rows have the quest's card.
  2. Why lines (Ding.Why): one line under the first row still to do, after the guide's note, only for a step whose reason is marked short; the whole reason at the end of the cards of the step's own rows, under "Why?", and only when the guide has a reason (no "Why?" saying "the guide plans this step here" on every card); neither with "Guide explanations" off; never under the finished step kept on screen.
  3. Texts never cut: a row's text, its sub-lines and the notices wrap whole and the row grows, at every width and scale. Minimized, the one line may end in an ellipsis: hovering it shows the row whole, with its note, what its look means and "Step N of M".
  4. Every quest carries its level: "[28] « Name »" in the headers, the accept, turn-in and complete rows, the next steps and the other quests' headers; a quest above the player's level is orange and its card says why ("Quest level 29, above yours (27): harder, but the guide plans it."). A quest of a chain whose parts share its name says which part, so a turn-in and the next part's accept read apart: "Turn in [28] « The Legend of Stalvan » (part 2 of 13)", "Accept … (part 3 of 13)" (rows, next steps, cards; Data:QuestPart, from the prerequisite links between quests of the same English name).

The step's quests

  1. A quest's objectives sit under a header "[level] « Name »" (no icon, no background, SemiBold), indented by 8, unless a row of the step names the quest already.
  2. Accept, turn-in and complete rows say the action and the quest; done, in the past ("Accepted / Turned in / Completed [28] « Name »").
  3. A quest ready to turn in or turned in: its objective rows are green, in the past tense, with a full counter, whatever the client reports (counts back to 0, a secret "finished"), also when the step is seen again with Previous; its card says it is turned in, never "not in your log yet".

Other quests

  1. Another quest's objectives (persistent sticky, along the way) are listed only when they are relevant: never for a quest a row of the step names, never twice, never under a step seen again with Previous; a persistent quest only from the step that makes it persistent on, and not while the next step still to do works on it (it comes right after). Along the way lists the log's quests whose targets are right around the player, the guide's next quests first. Open (Yendi's decision): whether along the way should also leave out a quest whose own step is further on (Menethil, « The Cursed Crew » among its sailors at step 7); the test keeps that case PENDING.
  2. They never pass for the step's work: under a header that names the other quest and says why it is there ("Another quest to finish on the way: [29] « Name »", "Another quest, its targets are here: …"), indented rows without the red of a row to do (no background, grey icons and counters); their card begins "Not this step's work".

Notices, rows besides the step, next steps

  1. Window-wide messages are notices: the way there, a trip of the NPC directory, steps passed, bags almost full, quest log almost full: boxes of their own (grey box, no icon, 11 Regular), never a sub-line of a row; the guide's notes and the step's own hints stay under the step.
  2. A trip of the NPC directory has a "Cancel the trip" control right under its notice.
  3. Rows besides the step (class training, weapon to learn, gear upgrade, talent, a new continent's flight master, a quest found elsewhere in the guide, the section wrap-up) come under "Good to know" and never look like its objectives: no red nor green, no state background, a blue icon; their card says they are not a step of the guide and what a click (A) does. A gear upgrade says how much better in words, never a bare "+4": "Clearly better than [worn item] for your class."; its card says what it replaces, which stats change and that the guide weighs each stat for the class. "Suggest better gear" (Automation tab, on) hides it, its banner and the item button's offer; "Equip upgrades automatically" stays its own option.
  4. Next steps: under a "Next steps" header, one dimmed row per step, "Turn in [2] « Name » and 1 more quest" (never "+1"), exactly the steps Next goes to, in that order.

Moving through the guide, marks

  1. While browsing (after Previous), Previous and Next move one step at a time, done steps included, and mark nothing; only on the guide's current step does Next mark the step done and move on to the next step to do.
  2. A step just finished stays 1.5 s on screen, green; Next or Skip meanwhile only brings the step the guide moved to.
  3. A step marked done by hand is violet, a skipped one amber; a line under it and the cards say it in words; an "Undo" control under it, and first in the step menu, takes the mark away.
  4. The step menu says what each entry acts on: during a trip, the trip's entries first, then "the guide's step"; an entry that would do what the step's mark already says is left out (no "Mark as done" on a step marked done, no "Skip" on a skipped one); "Find this quest in the guide" on a row with a quest.

Gamepad and mouse

  1. Each action once: with a gamepad, a focused control carries its A chip and focus mode's hint bar lists only what no control on screen does (A on a row, X when the row has a card, B); LB / RB chips in the step bar with a gamepad only; without one, no chip and no hint bar.
  2. Focus mode reaches every row (quest headers, objectives, other quests, notices, controls, rows besides the step) and the directory's button above them; X shows the focused row's card; A does what a click does.
  3. The mouse does everything: a click = A, hovering = X, a right click = the step menu.

Frames and looks

  1. Own frames only: the window never opens, closes nor drives a game window (quest log, map, talents); plain frames, nothing protected, no taint (taint_test).
  2. One look per kind, the same everywhere, and the step's own work stands out: only the step's rows have a state's background (red, green, violet, amber) and their icon; quest headers (12 SemiBold, no icon), section headers ("Good to know", "Next steps": 11 SemiBold, dimmed, no icon, a line above), notices (grey box, 11 Regular), controls (button background, lighter under the mouse), rows besides the step (no background, blue icon), other quests (no background, grey icon), next steps (dimmed). Colors only through UI.C.

Sentences

  1. Sub-lines are plain sentences: no "Button: action · Button: action" shorthand under a row (the upgrade, the talent, the offer to jump, the quest log's notice, the level's experience left): "To equip it, use the item button on screen. To ignore it, press Left in focus mode."
  2. Numbers are written the language's way (Compat.Number, L.NUMBER_GROUP): "13,000" in English, "13 000" in French (a no-break space), "13.000" in German, Spanish and Portuguese; never the client's BreakUpLargeNumbers, which writes the English way on some clients (the why lines, the experience left, distances).

Minimap pins (UI/MinimapPins.lua)

On the minimap, with Ding's own textures: the arrow's target (an accent ring) and up to 5 of the step's nearest spots (dots), or in a farming loop up to 12 of its stops, placed from the world positions with the minimap's zoom (Classic yards per zoom level, indoors and out, IsIndoors) and its rotation (rotateMinimap); the target farther than the minimap shows: an arrow on its rim. Hidden in instances, with the guide window, or with "Pins on the minimap" off (Appearance). No world map pins.

Quest rewards (Core/Rewards.lua, UI/Rewards.lua)

With several rewards to choose from (QUEST_COMPLETE), Ding works out the best one: the best upgrade for the class (each item's stats weighted for the class while leveling: primary stats, stamina, armor, weapon damage per second for the hands or the ranged slot, wands included; against what is equipped in that slot, the weaker ring or trinket, both hands for a two-hander; only items the class can use, at least half a point better), else the item that sells for the most. A banner names it and a ring marks its button in the game's quest window (Ding's own frames, the window only read). With the option "Choose quest rewards" (Automation tab, off by default, with automatic turn-in on), Ding takes it; Shift (LT) held leaves the choice to the player. Items the client has not loaded yet are looked at again when they arrive (GET_ITEM_INFO_RECEIVED, or a second later); nothing is taken meanwhile.

Quest log cleanup

The Guide tab lists, under "Quest log cleanup", the quests of the log that neither the active guide nor the guides after it ever use (prerequisites of their quests included), each with its card ("Details", A or a click). When the log holds 16 quests or more, the window says how many can go, in a notice of its own; A or a click on it opens that list. The player abandons them from the game's quest log: abandoning needs the quest selected in that log (SelectQuestLogEntry / C_QuestLog.SetSelectedQuest), which Ding never calls (taint, taint_test).

Level steps (guided grinding, Core/Grind.lua)

Yendi's feedback: the guide stopped on "reach level X" with nothing else to say. A level step now shows, on its row:

  • the percent of the current level as its counter, and under it the experience left to the step's level and about how many kills that makes. The experience of a kill is measured on the player's own last 30 kills (PLAYER_XP_UPDATE deltas), kept as a share of a monster of the player's level (5 × level + 45, Classic) so that they stay valid from one level to the next; without measures, the formula itself. The rested bonus is left out of the measure (a kill with rest left after it gave twice its experience), and it shortens the count: kills = max(left - rested, left / 2) / experience per kill. Experience within 1.5 s of a turn-in (either order) is the quest's, and a gain of more than three monsters of the player's level (a discovery) is not a kill;
  • a row "Kill · levels 3-4", the monster to farm, and the arrow on its spawns (a hunting area). Candidates: the monsters (killed or looted) of the quests of the log still to complete first (each kill counts twice), then those of the next 15 steps, then of the 15 steps just done; never an elite or a boss, nor a monster of the player's side, nor grey (no experience), nor more than 2 levels above the player. The cost of each is the trip to its nearest spawn plus half the trip on to the guide's next place, plus the kills left (more of them when the monster gives less experience, Classic "zero difference"), riskier above the player's level. It is chosen again when the step, the level or the quest log changes, and every 15 s. With no monster to suggest, the arrow points to the guide's next place (reaching it does not validate the level step);
  • the step passes by itself as soon as the level is reached, like before.

Along the way (Core/Along.lua)

Under the current step, the window lists the unfinished objectives of the other quests of the log whose targets (monsters, objects, item sources) are within 100 yards of the player: they stay listed until the player is 160 yards away (no flicker), the objective is done or the quest leaves the log. Three at most, the guide's next quests first, after the step's own rows, each quest under a header "Another quest of your log, its targets are right here: [26] “quest”", its rows grey (no background, grey icon and counter: not the step's work, Guide window rules 8 and 9). Not under a step seen again with "Previous" (they would read as its objectives). Hovering, clicking (A in focus mode) works as for the step's objectives. The objective the level step already farms is not repeated. Looked at every 2 s while the window is shown, and on every quest log change.

Quest monsters (UI/QuestMobs.lua)

Above the enemy nameplates of the monsters a log quest still needs, a small mark of Ding's (its own frame, parented to the game's plate, which is never changed): a diamond for a monster to kill, a dot for one that drops an item a quest needs (DingDB's item drops), then the objective's counter ("3/8"). The current step's monsters (its quests and their missing links, the monster a level step farms, its kill and collect goals) are in the accent color; those of the log's other quests are quieter (grey glyph, plain border, 80 %). A finished objective, or a quest ready to turn in, loses its mark. On a friendly plate (when the game shows them): the current step's quest giver "!" and turn-in NPC "?". The unit tooltip gets one line per objective (three at most): "Ding! · “Quest name” 3/8", the quest in the client's language; hooked through the client's tooltip processor (TooltipDataProcessor), else its OnTooltipSetUnit script, never replaced.

What counts is computed again with the window's rows (Engine:Refresh, so after every quest log change), per creature id; the objectives are matched to DingDB's targets the way "Along the way" does it (Along:Objectives, Along:StepQuests). A plate's monster is read once, when it appears (UnitGUID): when the client keeps the GUID secret (in combat, maybe), the plate keeps what it had, else its name is matched with the monsters Ding knows; a player is never marked. Nothing is protected: frames are only created, anchored, shown and hidden.

Options (Arrow and maps tab, "Quest monsters"): marks on the plates (on), the tooltip line (on), "Show enemy nameplates farther" (on since v1.7.0): the client's nameplateMaxDistance at its maximum (Ding asks for 60 yards, the client keeps its own limit), the value before put back when the option goes off (Ding.db.farPlatesFrom, outside the settings, so that the tab's defaults and "Restore all defaults" put it back too); never changed in combat (after the fight). Turned on or off in the settings, the chat says the distance now. The marks sit on the enemy nameplates, which the client hides until the player shows them (V): with the marks on, Ding shows them once (nameplateShowEnemies, out of combat), says so in the chat with how to hide them, and never again after (Ding.db.platesShown).

Interface

The design (dimensions, colors, fonts, states, texts) is in UI-handoff.md, the Claude Design handoff, to be reproduced exactly: the Noto Sans font shipped with the addon, rounded shapes from the texture kit. Its look comes from a theme (UI-themes.md, UI/Themes.lua): Forever · Halo by default (no frame, a soft veil under the text, the game's gold, its own font for titles), Forever · Slate (ardoise: a dark panel, gold hairlines, rows tinted by state), Forever · Bands (bandes: a band per line, outlined text, a blue accent) and Ding dark (dingdark: the first handoff's flat dark look, blue accent #5BB4FF). A theme changes only colors, textures and font flags, never a size nor a position.

  • Guide window (300 wide, 240 to 420 with the mouse; background at the chosen opacity, the theme's default until the player moves it: 60 % in Halo, 80 Ardoise, 82 Bandes, 85 Ding dark; scale 50-150%): title bar (menu button that opens the settings, "Ding · guide", minimize, close), step bar (previous / next bindings as gamepad chips, ‹ "Step 3 / 42" ›, zone), the step's lines and a progress bar. One line per action and per quest log objective: icon (check mark, ◆ kill, ● collect, item, ? turn in, ! accept), text wrapped whole with names in blue (no text of a row is ever cut: the row grows, at every width; the minimized window's one line shows its row whole, with its note, on hover), a "2/6" counter on the right, the guide note underneath. States: done (green), to do (red), impossible (gray: level too low, quest missing from the log). A quest is always named with its level first, "Accept [22] “quest”" (L.QUEST_WITH_LEVEL). A quest's objectives come under a header row with its level and name, "[22] “quest”" (state "header", no icon), indented, unless a row of the step already names the quest (a turn-in of the same quest). Hovering over or clicking a line (a header too): its quest's card (name, level, objectives); the click also sends the arrow there (from a header: the nearest of the quest's objectives). Focus mode starts on the first row to do (past the header); the D-pad also stops on headers; the "target" binding (DING_CYCLE) passes over them.

  • Notices (state "notice", action = "notice"): what concerns the whole window rather than one row of the step gets a box of its own (no icon, 11 Regular, wrapped whole), under the "Good to know" title after the step's rows (Guide window rule 1): first the way there (travel: the travel hint, then the itinerary), a trip of the NPC directory (trip: with the way there in its box, and a "Cancel the trip" control under it: A or a click on it ends the trip) and why steps were passed (skipped); after the rows added under the step, the bags almost full (bags) and the quest log almost full (cleanup, with "A: see the list" or "Click: see the list": the settings open on the cleanup page). The guide's notes and the step's own hints (service, "press Next", marked by hand, option steps) stay under the step. A step the player has just finished stays displayed for 1.5 s, in green; "Next" or "Skip" meanwhile only brings the step the guide moved to, without marking it (the player has not seen it yet: one press, one step). The objectives of a quest that is ready to turn in or already turned in are done (in green, full counter), even when the client resets them to 0 (quest turned in, past step reviewed with "Previous"); a full counter also counts as done when the client's flag is secret.

  • Step menu (right click on the window or a line): during a trip of the NPC directory, the trip's own entries first, "Cancel the trip to …" and "Done with …: back to the guide" (both end the trip and leave the guide's step as it is), then under a line the guide's step said so: "Mark the guide's step as done", "Skip the guide's step" (Yendi took "Mark as done" for the NPC and lost a step). Without a trip: "Mark as done" (like "Next") or "Skip, do it later" (like a right click on "Next"). On a step marked done or skipped, its undo first ("Undo: mark as not done", "Undo the skip"), and not the entry the mark already says (no "Mark as done" on a step marked done, no "Skip" on a skipped one); on a line with a quest "Find this quest in the guide". On the gamepad, the "Next step" and "Skip step (do it later)" bindings do the same. A step settled by hand keeps its mark (Engine:StepMark: Ding.char.manual, Ding.char.skipped), visible when you come back to it: whatever the game does not confirm takes the mark's color, purple if marked done, amber if skipped (green stays reserved for what the game confirms), with a line "Marked as done by hand." / "Skipped: still to do.". "Previous" keeps the mark; coming back to do a skipped step clears its mark; "Take this guide up where I am" clears all of the guide's marks, and whatever is not really done goes back to "to do". Undo: under a marked step on screen (browsing back with "Previous"), a control "Undo: mark as not done" / "Undo the skip" (a click, A in focus mode), and the same first in the step menu (Engine:Unmark): the mark goes; the step still to do, the guide goes back to it (like going back to a skipped step); done in the game since, the guide goes on from there.

  • Closing the window (close button) also hides the arrow (and the TomTom waypoint); the minimap button, the binding, /ding or the "Show the guide and its arrow" checkbox (Guide tab) bring them back together. "Go" steps are still validated. With "Hide the guide in dungeons", the arrow is also hidden in instances (Panel:Visible): with no position, it would only show its target's name and "?" there.

  • Minimized version (300 × 28): "3/42", the current objective and its counter, the progress at the bottom; "done" in green for 1.5 s. In combat when the option is on (off by default) or by binding.

  • Arrow (centered, 120 above): blue while on the way, red when the target is behind (more than 120°, "turn around"), a green "You're here" ring on arrival (6 yards from the place or the NPC, a step's radius otherwise), which fades after 3 s; beneath it, the target's name, the distance and the time to get there (speed measured from the player's movement). Size 40-80, not clickable (movable in move mode). Monsters to kill and items to collect form an area: "You're in the area" from within 25 yards of a spawn (about an attack's reach), kept up to 40. In an area, the farming loop (option "Farming loop", Arrow and maps tab, arrow.loop, on by default): the spawns of the same monster or object within 300 yards of the nearest one, one stop per 25 yards, at most 12, in nearest-neighbour order from there (3 stops at least, else the plain area); the arrow leads to the next stop, reached within 15 yards, then the one after, and around ("Young Nightsaber · farming loop: 2 of 6"). The loop stays while the nearest spawn is of the same monster near the loop and on the same step (Arrow.farm). A ghost is first led to its corpse.

  • Floors (Yendi's feedback: "You're here" in the middle of a building whose NPC is one floor up): the game gives no height, neither for the player nor for a target, and the minimap's floor icon cannot be read. A step's floor key says it ("up", "top", "down"): on arrival the distance line reads "The NPC is one floor up" (an NPC: point.npc, not an area) or "It's one floor up" (an object, a place, a monster), "… at the top", "… one floor down", instead of "You're here" / "You're in the area", and the arrow stays (no fade after 3 s: the player still has to find the stairs). Within 30 yards, the detail line adds "One floor up" / "At the top" / "One floor down". Only for the step's own targets (Arrow:Floor: not the corpse, a flight master on the way, a persistent objective or a go point). The leveling generator writes the key from the route's note (tools/guide_floor.lua, an explicit table of English phrases; a note that names two floors gets none), and check_guides warns when a note names a floor that its step's key does not say.

  • Cycle (binding "Arrow: next target of the step", DING_CYCLE): the arrow goes to the target of the next row of the window that has one, in the rows' order (that of focus mode, whose focus follows), then back to the nearest of all (Panel:CycleTarget, Arrow:Unfocus). A click on a row (A in focus mode) still aims at that row.

  • Zone names (Yendi's feedback, a quest giver 1,745 yards away and no zone): when the arrow's target is not in the player's zone, its zone's name follows the distance ("1,745 yd · Duskwood"; "Another continent · Duskwood"), and a line of the window whose target is in another zone ends with it, the NPC first for a quest to take or turn in ("— Madame Eva, Duskwood"): quest givers, turn-ins, objectives, goals, services, trips. The zone is the target's UiMap (C_Map.GetMapInfo), its parent zone for a dungeon, a city's sub-map or a cave (Compat.ZoneOf, Engine:ZoneLabel), the nearest target of the line (Panel:Where, looked up again every 3 s). On a travel leg, the zone of the place the arrow shows (the flight master, the dock).

  • The target on the world map ("Mark the target on the world map", Arrow and maps tab, on): the arrow's target becomes the client's own map waypoint (C_Map.SetUserWaypoint with a UiMapPoint, C_SuperTrack.SetSuperTrackedUserWaypoint when the client has it), moved when the target changes and cleared when there is none, the option is off or the guide window is closed. API calls only, the world map frame is never touched: taint_test allows exactly these calls in Core/Compat.lua. Ding moves or clears only its own waypoint (Ding.char.worldMark, so a reload keeps knowing it): one the player set stays until they clear it. Without that API: a TomTom pin when TomTom is installed (world map only, no TomTom arrow), else nothing (the minimap pin stays). No pin of Ding's own on the world map: it would have to live on the game's map frame.

  • TomTom (optional, ## OptionalDeps: TomTom, off by default: Ding's arrow is enough on its own): if the player ticks "Use TomTom's arrow", Ding passes it a temporary waypoint through its public API and hides its own arrow. If TomTom fails, Ding brings its own arrow back. No TomTom file is reused: its license is "all rights reserved".

  • Ding always measures the distance to validate "go" steps, including when its arrow is hidden (setting or TomTom) and without orientation (in dungeons, the arrow hides).

  • Quest item button (44 × 44, floating, saved position): the item the step needs (use key, otherwise the quest log's item or the one handed over when the quest starts, if it has a "Use" effect), its name above, the glyph of the assigned binding; states ready, pressed, cooldown (seconds left), unusable or out of range. It is Ding's only secure frame: in combat it keeps its item, and updates wait for the end of combat.

  • Target binding ("Target the arrow's NPC or monster", CLICK DingItemButton:Target): the same secure button's second action, on the virtual mouse button "Target" (type-Target = "macro", macrotext-Target = /targetexact <name> lines), so mouse clicks never trigger it. The names are the step's unfinished targets, nearest first (Engine:TargetNames: the NPC to talk to, the objectives' monsters, the monster to farm; at most 3, looked up once a second), the one the arrow shows first; on the way to a flight master or a dock, only that NPC. Each line that finds its unit takes the target, so the nearest name is the macro's last line (QuestieDB's names in the client's language). The macro changes out of combat only (in combat it keeps the last one). Solo, with "Mark the target (solo)" (on by default), the target gets a skull (SetRaidTarget, only if it has no marker). Under the arrow, "Target: name" with the binding's glyph when it is bound. A row in the Keybinds tab, followed by a tip: bind the game's own "Interact with Target" key too (Ding's key targets, the game's talks or attacks).

  • Service steps point to the nearest NPC of their service on the player's side (QuestieDB npcFlags: vendor 4, flight master 8, trainer 16, innkeeper 128, repair 16384; a class trainer is a trainer whose English subtitle names the player's class) and end by themselves (table above); the window says so instead of "press Next".

  • NPC directory (binding "NPC directory", DING_FINDER; the guide window's pin button; /ding find <text>; Core/Directory.lua finds, UI/Finder.lua shows): every NPC of DingDB that offers something, anywhere in the world, small outposts included. Only NPCs of the player's side (or neutral), with a spawn outside a dungeon; vendors of seasonal events (English subtitle with "Smokywood Pastures", "Darkmoon Faire", "Hallow's End", "Festival", "Holiday", "Sweet Treats", "Fishing Extravaganza" or "Fishing Promoter") are left out. The categories, in this order, and what puts an NPC in each:

    • Vendor, repair, innkeeper, flight master, bank, auction house, stable master: its npcFlags (4, 16384, 128, 8, 256, 4096, 8192). Stable master for hunters only.
    • Class trainer: the trainer flag (16) and the player's class in its English subtitle (as for service steps).
    • Weapon masters, by weapon: DingDB's weaponSkills (tools/data/corrections/ 10_weapon_masters.lua, the eight weapon masters). Only the weapons the class can use (the gear table of Core/Gear.lua), that the character surely does not know (known: it has the weapon skill's spell, IsPlayerSpell with the Classic ids, the same in every language, or wears such a weapon, or the client's skill lines name it; Forever has no skill lines and its IsSpellKnown says false for weapon skills; when the client cannot tell, the list shows every weapon the class can use, claiming nothing), from the level the master asks (polearms: 20), and that a weapon master of its side teaches. The category goes when nothing is left.
    • Profession trainers, by profession: the trainer flag and a word of the profession in the English subtitle ("Alchem", "Blacksmith", "Enchant", "Engineer", "Herbalis", "Leatherwork" / "Leathercraft", "Mining", "Skinning", "Tailor", "Cook", "First Aid" / "Physician" / "Medic", "Fishing" / "Fisherman" / "Fisher"); the specialists of a branch (Dragonscale, Elemental, Tribal, Gnome, Goblin, Shadoweave, Mithril Order) are left out.
    • What vendors sell (DingDB's item vendors, class, subClass, requiredLevel, flags): ammunition (hunters, warriors, rogues), by choice: arrows (class 6, subclass 2) and bullets (6, 3); food and drink: consumables (class 0, subclass 0, or 5 for Forever's new food) with no item flag (poisons, lures, bandages and explosives have one) from level 1 (alcohol is level 0); trade supplies: trade goods (class 7).
    • Reagents (mages, priests, paladins, shamans, druids, warlocks): the vendors of the class's reagents, a list of item ids per class in Core/Directory.lua (Arcane Powder, runes, Light Feather; candles; symbols; Ankh, Fish Oil, Shiny Fish Scales; berries, thornroot and seeds; Infernal Stone, Demonic Figurine), each a reagent item in DingDB (class 5, or 15/1).
    • Poisons (rogues): the vendors of the reagents only poisons use (Essence of Pain, Deathweed, Essence of Agony, Dust of Deterioration), of Flash Powder, or of a poison.

    Each kind of NPC is a list built once per session (Directory:Ids(kind)). The NPC to go to is the one the fewest seconds away (Travel:Reach(): one pass of the planner from the player, then any point priced: walking, known and unseen flight paths, boats, zeppelins, the tram, the hearthstone, teleports), else the fewest yards. Priced: every NPC of a kind of 24 or fewer, else the 4 nearest on the player's continent and the 2 nearest to where the hearthstone or a teleport lands. A row says where the NPC stands (the city or town, below, else the zone), the ways besides walking (reach:Route: "· by flight", "· by boat, flight", "· by zeppelin, flight", tram, hearthstone, teleport) and the time ("8 min away", "under 1 min away"; yards without a time).

    The window ("Ding! · Directory", 360 wide) has two tabs, LB / RB or a click:

    • Nearby: one row per category (name, "NPC · place · ways", the time); a category with sub-choices (ammunition, weapon masters, profession trainers) shows the first ones and opens their list, one row per choice ("Guns", "Buliwyf Stonehand · Ironforge · by flight", "8 min away"). A category without any NPC is grayed ("None known nearby").
    • Cities: the capitals of the player's side (Stormwind, Ironforge, Darnassus; Orgrimmar, Thunder Bluff, Undercity: on their own map) and the neutral towns (Booty Bay, Gadgetzan, Ratchet, Everlook: within 260, 200, 200 and 160 yards of their inn), with the time to reach them; a city opens its categories (the NPC fewest seconds away, "+N" others), a category its NPCs with what each offers ("Vendor, Repair, Food and drink"; a weapon master's or a trainer's list by weapon or profession).
    • /ding find <text>: the categories, weapons, professions and services whose name holds the text (in the client's language or in English, without case nor accents; or each word of it starts a word of the name: "arme a feu" finds "Armes à feu"), then up to 20 NPCs by name, nearest first, each with what it offers. The results open over the Nearby tab ("Search: …"; B goes back to it); nothing found: one grayed row and a chat line. Without text, the usage.

    D-pad, A, B and the shoulders while it is open (temporary bindings, out of combat; never opens in combat), or the mouse: a click opens or goes, a right click or Esc goes back or closes, the wheel scrolls (9 rows shown). A row chosen sends the arrow to its NPC like any target of the arrow, through the travel planner: the flight master, the dock or the hearthstone first, the flight map's destination, the boarding (Travel above). The guide window's trip notice says "On the way to … (Guns): the guide resumes once there.", with in its box what to take ("Fly to Ironforge"), the whole way ("Route: walk 0:20, fly 7:56 to Ironforge, walk 0:37 (8:53 in all)"; no separate travel notice then). The trip ends when that NPC's window opens (by its creature id, else its name; when the client tells neither, the window of its kind: merchant, trainer, bank, auction house, stable, flight map), then the guide takes the arrow back. It also ends, the arrow back on the step at once, whenever the player moves the guide: next, mark as done, skip, previous, back to a skipped step, another step ("Go to step N"), another section or "Take this guide up where I am" (Engine → Finder:Cancel(true); the guide's own progress, quests done on the way, does not end it). And on demand (Yendi: "there must be a clear way to cancel"):

    • a "Cancel the trip" control right under the trip's notice (a row with a button's look: bg.control, lighter under the mouse; in focus mode A on it, its chip on the right with a gamepad); the notice itself only informs;
    • "Cancel the trip to …" first in the step menu (right click on the window or a line);
    • the directory's first row during a trip, "Cancel the trip" ("On the way to …"), focused when the window opens (its binding or the pin button): A or a click; B there closes the window and ends the trip too (a right click or Esc only closes it);
    • /ding cancel ("Trip cancelled: the arrow follows the guide again.", or "No trip in progress."). A weapon master's trainer window is not taken for the class trainer's (Training:Record).
  • Report a problem with the guide (binding of that name, DING_REPORT, a chord like the others; the guide window's flag, right beside the directory's pin; /ding bug; Core/Report.lua writes, UI/Report.lua shows): players send precise reports the authors read on GitHub issues. The flag must invite a press (Yendi): a warm fill of its own in every theme (token report, the flag in onReport, both at least 4.5:1, unlike the accent), its tooltip in words, "Problem with this step?" and a line under it ("Tell the people who make the guide: Ding! writes the report for you."); never the word "bug" in what the player reads (the command apart). Icon only: a word on it would cut the guide's title at the window's usual widths.

    • What it holds, read when the window opens: the guide pack, the section (title and id), the step on screen (the held one while it shows) as the window numbers it ("Step 19 / 161") and its index in the guide's file, its summary (Engine:Summary) and its keys on one line (Import:StepText: the step as a guide writes it, without text, note and why); the player's UiMap (id and name), sub-zone, x and y (one decimal), level, race, class, side; the step's quests (accept, complete, turnin) whatever their state, then the quests of the log that the next 3 steps name (Engine:NextSteps), each with its id, English name (Data: QuestEnglishName, and the client's when it differs), level, state (in the log, ready to turn in, turned in, not in the log), where the guide names it, and its objectives as the client gives them (text, have / need, done); what the arrow shows (the point: label, NPC or object id, map, x, y, yards away or unknown; the travel hint, the directory's trip, hidden), the step's number of candidate points; the route options (elites, dungeons, hearth, deathSkip, skipGrey, serviceSteps, trainerSteps, flightSteps), the theme, TomTom on or off (and installed); Ding's version, the client's locale, its build (GetBuildInfo), the date and time in UTC. Never the character's name, the realm nor anything of the account.
    • First page ("Ding! · Report a problem with the guide", a dialog like the directory): one sentence on what it is for and that nothing is sent by itself; "What the report will contain", one plain sentence per part (the step, where you are, your character, the quests with their counters and "and 2 more", the arrow, the settings, the versions, never your name); "What went wrong?", four ready reasons with a radio ring (a dot when chosen: never color alone): "The NPC is not there", "The arrow points to the wrong place", "The quest is not offered", "Something else"; "In your words (optional)", a box with a line in it while empty; "Create the text" (accent). With a gamepad the reasons come first: A picks one and the focus goes on to "Create the text" (a gamepad types little); the box is a stop too, A gives it the keyboard. Without one, the box takes the keyboard at once. A box never takes the keyboard from a box that is not Ding's (the chat's, after /ding bug): it waits, and a line says so.
    • "Create the text", in the order the player goes (Yendi, in game): "1. Open this address on GitHub:" and the GitHub form's address (.github/ISSUE_TEMPLATE/ding-report.yml) whole in a one-line read-only box (10, the window 560 wide: in 11 the game cut it), selected first; "2. Copy this text and paste it into the report:" and the report in a read-only box; a line saying each button selects its box, then Ctrl+C copies it (an addon cannot put text on the clipboard); where the reports are kept for a player without a keyboard (WTF\Account\<account>\SavedVariables\Ding.lua, written when the player logs out or reloads). Buttons in the same order: "Select the link" (focused), "Select the text" (Y).
    • The text: GitHub Markdown, English labels, the player's words as typed (a quote). Its first line is a title ("Ding! report: Step 19/161 of
      : ", the player's first words cut after a word near 60 characters when the reason is "Something else"), then "What went wrong", the words, and sections Guide, Player, Quests, Arrow, Settings, Client. No raw "|" (the box would read an escape).
    • Kept: each text made goes to DingDB.reports ({ at, title, text }), the last 20, the oldest out first; not a setting: "Restore all defaults" keeps them.
  • Weapon to learn (guide window, option "Remind me of weapons to learn" in the Guide tab, on by default): a row "Weapon to learn: Guns — Buliwyf Stonehand, Ironforge" (the weapon master the fewest seconds away) only when the player has such a weapon in hand: in the bags, or among a quest's rewards while its window is open (QUEST_DETAIL / QUEST_COMPLETE to QUEST_FINISHED), whose type the class can use (Core/Gear.lua) and that the player surely has not learned (as in the directory above: never a guess, and a weapon worn is known; when the client cannot tell, no row), from the level its master asks, and that the player did not dismiss. Never only because a weapon master is close by (Yendi: polearms suggested in Stormwind, unexplained). Under it, why in plain words ("In your bags: [Old Blunderbuss]. Your class can use this kind of weapon (Guns), but you have not learned it yet.") and what to do ("Click: go to the master · Right-click: never suggest it again", in focus mode "A: go to the master · Left: never suggest it again"). A click (A) sends the arrow there; a right click (Left in focus mode) dismisses that weapon type for the character (Ding.char.weaponsDismissed[skill line], a chat line says so); the step menu's "mark as done" acts on the guide's step, never on it. Not during a trip.

  • Class training reminder (Core/Training.lua): when a class trainer's window is open, Ding notes what can be learned now and at which level the rest comes, with its cost (GetTrainerServiceInfo, …LevelReq, …Cost; only what the trainer's list shows, its filters are left alone), in Ding.char.training. From those levels, a row "New spells at your class trainer (cost)" under the step (not on a train step), until the next visit; a click (A in focus mode) sends the arrow to the nearest class trainer. Before the first visit (nothing known), the row comes at even levels: "New spells may wait at your class trainer (level N)". Within 80 yards of a class trainer of the player's class while something waits, one chat line ("Your class trainer … is right here: new spells are waiting."), once per level (Ding.char.trainingReminded).

  • Gear upgrades (Core/Gear.lua): each piece of gear in the bags that the class can wear at its level (armor and weapon proficiencies of Classic, GetItemInfoInstant) is scored like the quest rewards against what it would replace; one point of score or more is an upgrade. The best one gets a 6 s banner ("Upgrade in your bags: [item]"), a row "Equip [item]" under "Good to know" saying in words how much better it is ("Slightly / Clearly better than [worn item] for your class.", "You wear nothing there: better than nothing.": a gain of a quarter of what is worn or more is clear, Rewards:Compare) and how to equip it or say no, and the item button offers it (using a piece of gear from the bags wears it), when the step needs no item of its own. "Suggest better gear" (Automation tab, on by default, ui.suggestGear): off, no row, no banner, nothing on the item button (Gear:Suggested); the bags are still looked at for "Equip upgrades automatically", its own option. Declined (right click on the row, Left in focus mode): remembered in Ding.char.gearDeclined. "Equip upgrades automatically" (off by default): out of combat, only an item already bound to the player (C_Item.IsBound), else the game would ask its confirmation. An item a quest of the log needs (objective, item handed out) is never offered, nor replaced.

  • Talent advisor (Core/Talents.lua): talent builds registered per class, Ding.RegisterTalentBuild({ id, class = "WARRIOR", title, picks = { { tab, tier, column }, … } }), one pick per point in the order they are spent (tab in the client's talent frame order, tier and column as GetTalentInfo gives them). The build followed is the one picked in the Automation tab ("Talent build": the class's builds, then "None"; the class's first by default), in Ding.char.talentBuild. With a free point (UnitCharacterPoints, else GetUnspentTalentPoints), a row "Talent: name" with the rank it reaches ("2/3") under the step: the first pick the character does not have yet and can take now (its tier open with 5 points per tier in that tree, its prerequisite at its last rank); a pick out of reach, after points spent elsewhere, waits and the next one comes. A press (A in focus mode, or a click) asks to confirm for 5 s ("A again: learn it"), the second one calls LearnTalent(tab, index), out of combat only, and says so in the chat. No build ships: Yendi adds them at release (talents change during the beta), one file per class, Ding/Talents/<CLASS>.lua listed after Core/Talents.lua in the .toc, each file calling Ding.RegisterTalentBuild for its builds (class = the class token of UnitClass, WARRIOR, MAGE…; id unique, not "off"). With no build for the player's class, nothing of the advisor shows: no row, no setting.

  • Find this quest in the guide (Engine:FindQuest, Panel:FindQuest): from a row with a quest (the step's, along the way, persistent), the step that deals with it: its turn-in when the quest is ready, else where it is worked on, else its turn-in, when it is in the log; where it is taken otherwise; in the active section first, then in the sections of its pack that fit the character. Offered as a row under the step, "Go to step 12 of Westfall" with "Turn in “…” · A: go there · Left: never mind" (mouse: "Click: go there · Right-click: never mind"); going there sets that section and step. Gamepad: Right in focus mode on the row (the focus moves to the offer); mouse: "Find this quest in the guide" in the step menu (right click on the row). Nothing found, or the current step: a chat line. The offer goes when the step changes.

  • Merchant (Core/Merchant.lua): at any merchant, the grey items of the bags are sold ("Sell gray items", on) and the gear repaired if the money is there ("Repair at merchants", on), then one chat line: "Sold 4 gray items for 66c · repaired for 2s 50c · 15 free bag slots". Shift (LT) held: nothing. With 4 free bag slots or fewer, the window says so under the step.

  • Settings window (560 × 440; /ding config, binding, menu button, minimap button, Options > AddOns), by intent (Yendi: "a huge mass of information; make it readable and digestible"), six tabs, each in small titled blocks with one-line descriptions:

    • Guide: the guide and its section, "Take this guide up where I am", show the guide, the next steps preview, "Stay in this section" undone (when chosen), the quest log cleanup (a page), guides as text (a page), "Run the setup assistant again" (Help).
    • Route (what the guide makes you do): hearthstone, death skips; elite quests, dungeon quests (one row, "3 of 12", the list on a page); class trainer, flight path, vendor and repair steps.
    • Automation: Quests (accept, turn in, rewards, skip gray), Merchant (sell gray, repair), Equipment (equip upgrades, talent build), Flights (take the flight).
    • Arrow and maps: arrow, its look (seven cards like the themes': the theme's arrow by default, Halo, Ardoise, Bandes, Ding!, Gold (the arrow of Ding!'s icon) or the game's minimap arrow, with any theme; focusing one shows it on the real arrow, A or a click keeps it, leaving puts the kept one back, Y: the theme's; Ding.db.arrow.skin), size, farming loop, move; the world map mark, the minimap pins; the target's skull; quest monsters (marks on the nameplates, the tooltip line, enemy nameplates farther); "More options" folded: orientation, TomTom, reset the arrow's position.
    • Keybinds: unchanged.
    • Appearance: the theme (four cards: focusing one shows it live on the guide window, the arrow and the settings; A or a click keeps it; leaving the cards puts the kept one back; Y: Halo; UI.Theme.Apply, Ding.db.ui.theme), window (opacity, its default the theme's while the player never moved it: Ding.db.ui.opacityTouched; scale, minimized in combat, lock, hidden in dungeons, move), the settings windows' size (60 % to 120 %, 85 % by default: the settings, the NPC directory, the setup assistant, import / export; UI.Dialog), item button, minimap button and sound; "Advanced" folded: reset the window's position, debug, timings, restore all defaults. A list longer than about 6 rows goes on a page: its row says how much is on, A or a click opens it, B, Esc, a right click or its title go back, LB / RB leave it for a tab; the help bar follows it. Every tab but Keybinds ends with "Defaults for this tab" (confirmed): the saved settings it shows (Config.TAB_SETTINGS) go back to Ding.DEFAULTS. The saved keys never moved: only where they are shown. A footer reminds the player of the useful buttons; everything applies live. Config:Open(key) takes a tab's key (Config.TAB).
  • Minimap button (Ding!'s icon, the gold arrow on a dark disc): left click for settings, right click to show / hide the guide, drag to move around the minimap; can be hidden (Appearance, /ding minimap).

  • Setup assistant (UI/Setup.lua; Yendi, in game: lost on a long Duskwood - Westfall - Duskwood trip, on quests the NPCs offer that the guide leaves out, on a bank step; then, on six pages of text: "if this window pops up I press next, next, next without reading"; then "still too many options"). "Ding! · Setup", four screens, each a title, one simple line saying what it is for, then three choices at most:

    1. Welcome ("To start, tell Ding! how you play."), how the player plays (one choice): "Gamepad, Xbox buttons", "Gamepad, PlayStation buttons" (ui.buttonStyle, at once), "Keyboard and mouse" (the button names stay; the Keybinds tab does not follow and ui.welcomed is set). Preselected and tagged "Recommended": the gamepad in use (Gamepad:Active) with its button names, else keyboard and mouse. Under the choices, one line says how to open Ding! later (Setup.OpenNote): with a gamepad, "To open Ding! later:" and the chip of the button "Open Ding! settings" has (else where to bind one); with the keyboard and the mouse, the button around the minimap, the guide window's menu button, and the key when one is bound.
    2. What Ding! does for you ("To save you time, Ding! can do some things for you."): accept and turn in the guide's quests (autoAccept and autoTurnin together), sell gray items, repair. Choosing the reward and taking the planned flight stay in the Automation tab only: both are off by default and are the player's judgment.
    3. Your route ("Sometimes the guide sends you far or leaves a quest out: it's on purpose, and it tells you why."): hearthstone steps, group quests (elite quests and every dungeon's quests together, as the Route tab's page does for dungeons: the choices per dungeon go), death skips.
    4. What do you want to do first?: one row per guide pack (Ding:GuidePacks: its sections for an item pack, else the levels it covers for the character; its author for a community pack; "Recommended" on Ding's Leveling, a check on the current one), then "Take my guide up where I am" (Engine:Reset). A pick sets the pack like the Guide tab (Engine:SetPack) and ends the assistant; Finish keeps the guide. The sentence says that everything can be changed later in the settings. The help on screen (quest monster marks, the guide's explanations, the panel beside an NPC's window, the weapon to learn) is not asked: on by default, in the settings. A check or a choice writes its setting at once, through the same calls as the settings window (Engine:Update); a screen shows the current values. "Keep the recommended settings" (a button on every screen, Y) ends it: every setting the player did not change in this run goes back to its default (Ding.DEFAULTS); the settings it does not ask and the guide stay. B, Start, Esc and the close cross end it as it is. Shown once per account (Ding.db.ui.setupDone), 2 s after the first login, before the Keybinds tab of the first launch, which follows once it is closed (with a gamepad, while "Open Ding! settings" has no button); an account that already had Ding sees it once too (new in 1.4.0). Right before it, "Open Ding! settings" gets its default chord (Gamepad:BindDefaultConfig, once per account, Ding.db.ui.configChordDone; a later login when no gamepad was in use): RB + Up (the player's choice: neither the game nor Easy Controller uses it, Easy Controller has RB + Down), else LB + Up when RB + Up is another action's chord or Easy Controller's; never over a gamepad button or chord the player gave the action. The LT + Start game binding the first v1.5.0 builds set (it did nothing in play) is replaced by it. Closed by a fight (the temporary bindings must go), the first launch's assistant comes back on its screen a second after the fight, its choices kept; opened by hand, a fight only closes it. Another Ding window opening (settings, directory, import) closes it quietly: done, and the Keybinds tab waits for the next login. Again from the Guide tab (header "Help", "Run the setup assistant again", back to that row once closed) or /ding intro. "Restore all defaults" does not run it again. Window 460 wide (window look, opaque, DIALOG, clamped to the screen), as high as its tallest screen in the client's language: every text wraps whole, nothing is ever cut, the buttons stay in place. Footer: "Keep the recommended settings" across, then Previous (disabled on the first screen), the screen dots (a click opens a screen), Next (accent; "Finish" on the last screen). Gamepad (temporary bindings, out of combat): the D-pad up / down between the choices (around), A ticks or picks, LB / RB the screens, Y keeps the recommended settings, B or Start closes; each action once: A on the focused choice, LB on Previous, RB on Next, Y on Keep, B by the close cross; no hint bar. Mouse: hover focuses a choice, a click ticks or picks; the buttons, the dots, the cross, Esc.

Keyboard and mouse

Player feedback: without a gamepad, nothing could be confirmed (help and messages only showed gamepad buttons). Every gamepad action therefore has an equivalent:

  • Click = A (select, tick, confirm, bind); right click = Y (default value, unbind) or cancels a confirmation; Esc = B (closes the settings, cancels a capture or move mode).
  • Confirmation: the button says "Click again to confirm". Binding conflict: "click to replace, right-click to cancel" (a click elsewhere also cancels). A capture takes a keyboard key just like a gamepad button.
  • Move mode: drag the frame with the mouse; "Save", "Cancel" and "Reset" buttons on the plate, Enter saves, Esc cancels.
  • The help bar is clickable (settings, import window and the move mode plate): a click on "A Open", "Y Reset", "B Cancel" or "Y Select for Ctrl+C" does what the button does. These hints look like buttons (background, border, lighter on hover); the D-pad hint and the mouse help text, which cannot be clicked, stay plain text. Yendi's feedback: with no gamepad plugged in, you could neither confirm nor cancel, since A and B were only pictures.
  • "Gamepad" means enabled and plugged in (Gamepad:Active: C_GamePad.IsEnabled or the GamePadEnable variable, and at least one device in C_GamePad.GetAllDeviceIDs), checked again on every GAME_PAD_CONNECTED / GAME_PAD_DISCONNECTED. Without a gamepad, the help bar and these messages talk mouse and keyboard ("Click: change · Right-click: default · Esc: close"), with no glyph; with a gamepad, nothing changes. mouse_test checks it.
  • Each action once (Yendi's feedback: "the gamepad interface and the mouse interface coexist, so I get duplicate buttons"). With a gamepad, a button on screen carries the chips of the gamepad buttons that press it (A on the focused one, B on the one that closes or goes back, Y on the one that selects), and the help bar lists only what no button does (D-pad, Y to select for Ctrl+C, a row's A when the row has no chip…). In the settings, B is on the close cross (on a page: on the page's title) and LB / RB at the ends of the tabs, so the footer no longer repeats them. Without a gamepad: the buttons alone, no chip, the mouse help. A window open while a gamepad is plugged in or out follows it. single_action_test checks every window, gamepad or not, in every language (buttons and hints fit).

Gamepad

  • Game bindings: next / previous step, show the guide, minimize the guide, open the settings, use the quest item, target the arrow's NPC or monster, browse the guide (focus mode), skip the step, go back to a skipped step, the NPC directory, the arrow on the step's next target, report a problem. They work in combat, except opening the settings, focus mode, the NPC directory and the report window.
  • No window captures a button permanently: the guide window stays displayed without taking anything away from the game.
  • Focus mode of the guide window: the D-pad moves through the lines, A points the arrow at the line (or uses the item on the item's line, equips a gear upgrade), X shows its quest's card, Left ignores the gear upgrade under focus, LB / RB change step, Y minimizes, B exits. On a talent suggestion, A twice learns the talent. Right, on a row with a quest, finds that quest in the guide; Left drops the offer. Up from the first line, the focus goes to the NPC directory's button of the title bar (outer focus ring, A beside it): A opens the directory. Up again (or Left), the "Report a problem" flag beside it (Right goes back to the pin): A opens the report window. The A chip of either sits left of the flag. Only these buttons are taken; the mode stops in combat.
  • Settings window: temporary bindings (SetOverrideBindingClick) set when it opens and removed when it closes. Every Ding window that takes the gamepad (settings, setup assistant, NPC directory, import / export, report a problem, focus mode, move mode) also reads its buttons with a frame of its own first (DingPadInput, OnGamePadButtonDown), before any binding: neither the game's gamepad interface nor another addon's override bindings go before them (reported: in focus mode, Easy Controller on, the D-pad cast the gamepad bar's spells); only the window's keys are kept, any other button goes on to the game. A button already down when the window opens, whose press went to the game (RB of the RB + Up that opened it), stays the game's until released (Gamepad.heldKeys): its release must reach the game, which kept RB held otherwise (reported). A confirms, B closes or cancels, Y resets to default or clears, LB / RB change tab, the D-pad selects and adjusts (repeats when held). The window closes on entering combat.
  • Move mode for the guide window, the arrow and the item button with the D-pad (steps of 5, 1 with LB, 20 with RB, the mouse works too), A saves, B cancels, Y restores the original position.
  • Binding a button from the Keybinds tab, with conflict detection; buttons are shown as Xbox chips (A, LB, LT+Right) or PlayStation chips (symbols). The capture (Gamepad:Capture) reads the buttons themselves, Ding's navigation bindings released until it ends, and lets none through to the game:
    • a button is taken when released alone (a game binding); held, it starts a chord: any held button and a second one ("LB+Up", "LT+A", "RB+X"), taken when the second goes down and saved in Ding.db.chords (per account, kept by "Restore all defaults"), never in the game's bindings (the game binds a button with Shift, Ctrl or Alt only, and its gamepad interface went before such a binding: LT + Start did nothing in play). The quest item's two actions are secure clicks, which a chord cannot run: for them a combination stays the game's, with its modifiers (GamePadEmulateShift / Ctrl / Alt, LT and RT usually), "RB+Up" refused in words;
    • B tapped cancels, B held one second is taken; Escape, a fight (the window closes) or 20 seconds without a press cancel; a key of the keyboard is taken with its modifiers;
    • in the way, said in words, A binds anyway and B keeps things as they were: a saved binding or another action's chord (named, replaced); another addon's override binding or Easy Controller's own chord (its settings' shortcut, read from its saved settings; named: Easy Controller, ConsolePort; theirs is left as it is and may win in play) or a button of the game's gamepad interface (its binding context GamepadModeInGameCore, while it is on; it may win in play). Refusing them left nothing to bind with Easy Controller on (v1.5.0 report);
    • saved like the game's own Keybindings (the action's binding context): a gamepad button or a chord replaces the action's gamepad buttons and chord, a key its keys, the other kind stays; the game refusing it is said, the old one kept. The row shows the kind of the input in use (the gamepad's while one is active), "and 2 more" when an action has several; Y unbinds what it shows. (Before v1.5.0 a new button was added beside the old ones and the row showed the first: "RB" whatever was pressed.)
  • Chords in play (Gamepad:Capture above, "Chords in play" in UI/Gamepad.lua): watched like Easy Controller's own RB + Down, IsKeyDown of both buttons every frame, never bound (binding the second button while the first is held broke the game's own RB combinations in Easy Controller). While a chord's first button is held, out of combat, a frame of Ding's (DingChordInput) reads the gamepad's buttons first and keeps the chord's second button from the game (its gamepad bar would act too); the first button keeps its own use (RB: the game's targeting), any other button goes on. In combat that frame cannot change (it goes before the lockdown): the chord still runs, the second button's own action with it. A chord does nothing while a Ding window holds the gamepad, while a Keybinds row waits, while the chat has the keyboard or a game window the gamepad's focus. Its action runs what the binding of Bindings.xml runs (Gamepad.CHORD_RUN).
  • First launch: "Open Ding settings" gets the chord RB + Up when it is free (else LB + Up), then the setup assistant (Interface, above) says how to open Ding! later; then, unless the player chose the keyboard and the mouse there or the action has a button, the window opens on the bindings so the player can bind "Open Ding settings". /ding help says how to open the settings too.
  • Other gamepad addons (docs/compatibility.md): Ding's windows take their buttons whatever Easy Controller shows (waiting for its windows left Ding unusable: v1.5.0 report); Gamepad:HasWindow() tells them a Ding window holds the gamepad; Ding never changes the game's gamepad settings (GamePadEmulate*) and its default chord never takes theirs.

API compatibility

The Forever client shares Retail's project ID but ships the Classic API, and some old functions exist but fail. Ding goes through Core/Compat.lua, which prefers C_QuestLog.* and C_Map.*, protects calls (pcall) and falls back to the legacy functions when needed.


4. Timing (for the optimizer)

For each step finished during the session, Ding records in DingDB.timings: duration, and level and XP at the start and at the end (positions 1 to 5, then the date: the only fields of the older samples), plus map (UiMapID where the step ended), deaths, kills and killXP (the kills during the step and their experience), levelStep (a level step) and step, the step's signature ("a456,458 c456 t457 l12 g") to find it again once a guide is regenerated and its steps move. Steps caught up automatically at login are not counted, nor steps longer than an hour (four for a level step: the player was away). The last 20 measurements per step are kept.

The file WTF/Account/<account>/SavedVariables/Ding.lua then serves as input to the optimizer, to replace its estimates with real times.


5. Optimizer (out of game)

tools/draft_guide.lua writes the guides of a route (tools/routes/*.lua); the details are in tools/README.md (§6).

Input: the QuestieDB Forever export (quests, NPCs, objects, map geometry, drop rates, quest XP) and the route (zones, level brackets, forced or excluded quests, notes). Output: a guide file in the format above, and its translation.

  1. Quest graph (tools/route_data.lua): accept (giver, or starting item dropped by the monsters of another quest), objectives (densest spawn clusters, each with the level of its own monsters, number of targets read from the quest text, drop rates) and turn-in of each quest, prerequisites, chains, levels, exclusive quests, XP (QuestieDB's QuestXP table).
  2. Time model (tools/route_model.lua, PARAMS table; travel in tools/route_travel.lua): travel (world distance × detours / speed, mount at level 40, flight paths between the faction's flight masters, boats, zeppelins and the tram, hearthstone to the bound inn once an hour, the binding itself chosen by the optimizer), time per objective (number of targets, spawn density, drop rate, respawns), monster XP (Classic formula). Parameters grouped together for recalibration with real measurements (DingDB.timings, §4): to do.
  3. Solving (tools/route_solver.lua): a tour with pickups and deliveries (pickup and delivery), under constraints of prerequisites, exclusive quests, level (quest XP in full, monster XP with a 90% safety margin), gray quests and a limited quest log (18 quests, a margin below 20). A quest at most 2 levels above the player, its monsters at most 1 (Yendi's rule, 12/10/2026; the required level is never above): the quest waits for the level, in the guide or in the next one, behind a level step when the stop pays off (a quest that a later guide needs: an hour of stops per guide at most). Start: the nearest-neighbor tour, then simulated annealing (moves, swaps, 2-opt, Or-opt, quest removal and reinsertion, ruin and recreate), fixed seed. Objective function: XP per hour, expressed as the time needed to reach the guide's maximum level (XP from quests and killed monsters, reduced XP for quests that are too easy, missing XP filled in by grinding): a quest is kept if it pays off faster than grinding, detour included; a grinding stop counts as lost time plus 15 minutes (a wall in the guide). Level windows: tools/fit_windows.lua cuts each guide's window from the level the previous one reaches, so that no guide ends with a level step just to reach the next one's minimum (see tools/README.md §6). Leveling guides (Ding! - Leveling): tools/write_leveling.lua writes them from the Leveling routes (tools/routes/leveling/<faction>.lua: the route's order, shortcuts and race and class steps are kept as written; Forever changes it as little as possible (a quest missing, closed or already done is left out for those concerned, a missing prerequisite on the same map is done first, a quest waits for its prerequisite's turn-in), and Forever's own quests are inserted where the detour is smallest, within the +2 / +1 caps (only they follow them). See tools/README.md §7. Reasons (tools/route_why.lua): on the final simulation, the steps whose choice surprises get a why (a detour costing 4 minutes or more beyond the direct way, with what it brings: its turn-ins, the quests taken there and the chains they open; a quest taken now or never), the others an on-demand one (smaller detours, quests carried an hour or more, level steps followed by higher quests); the free-quests audit's verdicts become the sections' leftOut (§3, "Why the guide makes its choices"). --why lists them. The zone guides' optimizer (draft_guide.lua) also builds guides over several zones (a level bracket over a pool of zones, the order across them optimized), for the Skyborne starts (one per side). Steps behind an option, each on a step of its own: sethearth / hearth (where the optimizer binds and uses the hearthstone), deathskip = true with its text, dungeon = "<dungeon name>" and elite = true on every step of such a quest (accept, objectives, turn-in). Skipped, the guide stays valid: shortcuts are only shortcuts, and no other step needs a dungeon or elite quest (tools/check_guides.lua checks it). A quest done in a dungeon (route_data.lua questDungeon) is never part of the tour.
  4. Export of the guide, then human review (elite quests, contested PvP zones…). --report shows, per guide, distance, estimated time, XP and XP/h before and after optimization, and the reason for each removed quest.

6. Roadmap

Step Content Status
1 Engine, panel, arrow, hand-written Shadowglen guide done, to validate in game
1b Mockup interface: panel, arrow, settings, gamepad navigation done, to validate in game
2 In-game validation on the beta (arrow, tracking, gamepad) to do
3 Tools: zone listing and guide validation done
4 Route optimizer for XP per hour (tools/route_solver.lua), Alliance guides regenerated done, to validate in game
5 Measurement → recalibration loop to do
6 Alliance 1-60 guides generated by tools/draft_guide.lua (per-race tracks up to 20, then a common trunk) done, to validate in game
6b Horde 1-60 guides generated the same way (Orc/Troll, Tauren, Undead tracks up to 20, then a common trunk) done, to validate in game
7 Hand touch-ups of the generated guides, based on tests and time measurements to do

The beta ends on October 21, 2026 (level 30 max); the game launches on November 4.