promotional bannermobile promotional banner

LibQuestDB

A standalone, offline quest database for World of Warcraft Classic. It answers questions about a quest without the quest being in your log or the game's cache; the quest counterpart to LibItemDB and LibProfessionDB.
Back to Files

QuestDB-v0.0.2

File nameQuestDB-QuestDB-v0.0.2.zip
Uploader
PmptastyPmptasty
Uploaded
Sep 20, 2026
Downloads
10
Size
3.2 MB
Flavors
Classic TBCClassic
File ID
8932819
Type
R
Release
Supported game versions
  • 2.5.6
  • 1.15.9

What's new

QuestDB Changelog

[v0.0.2] (2026-09-20) - The path graph: routes for a HUD, with boats, flights and sides

Adds a path graph per continent for route-following consumers (Questbook's HUD): named places, directed links typed walk, transport, flight and portal, the polyline the emulator's bots walk for every walk link, the gryphon's route for every flight and the ship's route for every boat and zeppelin, with a crossing split by continent and a faction side on every flight and transport. Library MINOR is 8. Vanilla's graph is 843 KB, TBC's 1.27 MB. At release: 96 specs, 100% line coverage on LibQuestDB-1.0.lua, luacheck / ruff / markdownlint clean, replication dry run ships the new file.

Ship routes, split crossings and link sides (MINOR 8)

  • Two directives from the operator on 2026-09-19, both relayed through Questbook. Seeing the flight curves: "the flight path is better, can you ensure the zepplin and boat paths are incliuded". Routed as Horde onto the Menethil boat: "the path isn't wrong, but it's telling me as horder to take the alliance boat...".
  • Every transport link carries the ship's route under the same path(from, to). The source table stores the ships' routes backwards relative to their link (Feathermoon Ferry 622 to 623 starts at node 623), so the generator orients every route by which end lies nearer which node before thinning; 16 Vanilla routes were reversed that way. 21 Vanilla dock steps have no route and stay nil.
  • A route that changes continent ships in two halves. Each continent's polylines table holds only the points on its own map, from its node to the map edge or from the edge to its node, because the two continents' yards are different frames. The link is already listed in both graphs, so path on each answers with its half. This also gives the nine TBC map-changing flights a route where MINOR 7 shipped none.
  • Every flight and transport link carries side, "A", "H" or nil for either faction, as a seventh field of the packed link. Flights: the link's object is the TaxiPath id (all 266 Vanilla and 443 TBC matched wago's table), and a flight's side is the faction allowed at both of its taxi nodes. Vanilla: 130 Alliance, 136 Horde, none open to both; TBC has 6 open to both. Transports: no table carries it (the ships are faction 35), so TRANSPORT_SIDE in the generator records dock ownership by GameObject entry: zeppelins and the Undercity and Thunder Bluff lifts Horde, the Menethil, Auberdine, Feathermoon and Azuremyst boats Alliance, the Booty Bay goblin ship and unidentified lifts neither. An entry missing from the table is printed per run. The wago CSVs are cached under tools/wago_cache/.
  • Sizes: Vanilla 843 KB, TBC 1.27 MB.

Peer Review's findings on the sides (2026-09-20, still MINOR 8)

  • The flight side now comes from TaxiNodes.Flags, not the mount columns. Peer Review found the flags column (bit 1 Alliance, bit 2 Horde) pairs with the mount columns on every row of both builds, and that two TBC nodes, Zabra'jin and Orebor Harborage, carry one faction's mount in both slots, so the mount rule read them as neutral and shipped Zabra'jin's flights with no side: exactly the operator's original complaint, on a flight. The flags are a bitmask: the Shattered Sun Staging Area carries 11, and an exact lookup stripped the side from its five flights before the low two bits were masked. The generator now asserts the mount columns agree with the flags, so a reordered wago column fails the build instead of inverting every side. Result: Vanilla unchanged; TBC 214 Alliance, 223 Horde, 6 either, the two Zabra'jin flights having gained Horde.
  • The Great Lift was marked Horde. Objects 11898 and 11899 serve both the Freewind Post lifts and the Great Lift between the Barrens and Thousand Needles, 3,900 yards apart, so a side keyed by object could not tell them apart, and an Alliance router lost its only land route into Thousand Needles. Lifts whose object stands in more than one place are now sided by where the link's from-node stands: Freewind Post stays Horde, the Great Lift is open to both. Peer Review's reading of the node positions also identified the three unnamed lifts: the Gnomeregan entrance lift, the Searing Gorge mining scaffold, and two small lifts in Dun Morogh and Azshara, all neutral.
  • The generator refuses a route that changes map more than once, where it silently overwrote the first half before. No such route exists in either dump.
  • Specs pin the identity of the sides, not only their counts: every flight out of Aerie Peak is Alliance and every flight out of Orgrimmar is Horde, so a swap in either direction goes red. The data spec also asserts every packed link decoded, so a malformed entry cannot ship as a silently missing link, and checks the Great Lift and Freewind Post sides by position.

Flight routes on the path graph (MINOR 7)

  • Every flight link now carries the gryphon's own route as its polyline, under the same path(from, to) as a walk, so a route drawn on the map follows the flight instead of a chord between the two flight masters. Asked by the operator while flying Orgrimmar to Crossroads (through Questbook, 2026-09-18): "it's a straight line between FP, not the actual flight path, do you have coords for the path? it looks funny when i'm flying". The travel graph's path table already held the route for all 266 outdoor Vanilla flights (33 points on average, climbing to 430 yards over the Badlands); they are thinned at 30 yards to 5,233 points. Nine TBC flights into the Blood Elf zones change map mid-route and ship no polyline, because one line cannot be drawn across two coordinate frames. Measured before choosing the key: no node pair carries two link types in either dump, so from:to cannot collide. Vanilla is now 830 KB, TBC 1.25 MB.

Path graph (MINOR 6)

  • Decided by the operator on 2026-09-18, answering Questbook's request for a walkable route graph for its HUD steer caret: "if it's talking about the directions for the HUD, it should use the FP/Boats/Zepplins as sometimes those will be REQUIRED dependant on where you currently are." So flights, boats and zeppelins ship as routable links; nobody prunes them.
  • The source is the playerbot module's travel graph inside the CMaNGOS database (ai_playerbot_travelnode, _link, _path): named places, directed links typed by TravelNodePathType (1 walk, 2 areaTrigger, 3 transport, 4 flightPath, 6 staticPortal), and the polyline the emulator's bots actually walk for every walk link, so routes go round cliffs and water by construction. Coordinates are server world yards, x north / y west, on the world map id, which is the "continent" (0 Eastern Kingdoms, 1 Kalimdor, 530 Outland). Nodes inside instances are not shipped, which drops instance doors (Questbook's request: the HUD has no position inside an instance). The first cut dropped every areaTrigger link by type, which would have lost the Dark Portal and the Eastern Plaguelands to Thalassian Pass portal on TBC; those join two outdoor continents and are kept. Only boats and zeppelins cross continents on Vanilla; on TBC so do those portals, the Shattrath portals and a few flights, because the Blood Elf zones sit on map 530, and data_spec.lua now asserts only that a walk never crosses.
  • GetPathGraph(continent) returns { continent, nodes = { [id] = {x, y, z, name, continent} }, links = { {from, to, type, object, distance, swim} }, path(from, to) }, decoded on first call and cached; path decodes one walk link's polyline lazily. A node whose continent differs from the graph's is the far end of a boat or zeppelin, and such a link appears in both continents' lists. GetPathContinents() and lib.PATH_TYPE alongside. Transport and flight links carry distance 0: the data has no travel time, so a consumer weights them itself, and nothing in the data knows which flight paths a character has learned.
  • tools/build-paths.py <Version> thins each polyline with Douglas-Peucker at 8 yards (every kept point is an original point). Vanilla: 1,245 nodes, 4,489 links, 36,382 of 344,220 walk points kept. TBC: 1,972 nodes, 7,191 links, 54,447 of 531,530. Loaded at login as packed strings; WoW cannot load a file on demand short of a separate LoadOnDemand addon, and this is the cost of a route mid-run.
  • Tests/data_spec.lua checks every link joins two nodes present on its continent, that foreign nodes are exactly the far ends of cross-continent links and none of those is a walk, that every walk link's polyline starts and ends within 60 yards of its nodes and no other link has one, and that Aerie Peak's flight master is a node with flights out of it.

[v0.0.1] (2026-09-18) - Initial release: quest rewards, derived XP and English quest text

The first release of LibQuestDB-1.0, an offline quest database for Classic Era and TBC that fills the gap Questie leaves: what a quest pays (items, choices, money, reputation, spells, the item handed over on accept), the XP it gives at any player level, and the quest's full text. Everything is static data generated from the CMaNGOS world database and read through LibStub; the library declares no dependency. Library MINOR is 5.

Quest text and the locale framework (MINOR 5)

  • Decided by the operator on 2026-09-18: "we need to create a locale and wire everything into an english locale, we can add other languages later after we're up and running, but we need the locale framework in place as mvp so we can just add new stuff later."
  • GetQuestText(questId) returns title, details, objectives, offerReward, requestItems, endText and objectiveTexts in the loaded locale, with WoW's $N / $C / $R / $B / $G markup left for the consumer to expand. HasText, GetTextLocale alongside. Rows are packed like the rewards; newlines are written as \n inside the Lua literal so every row stays on one source line. Three Vanilla rows (the Ahn'Qiraj war effort) carry a $<id>w world-state counter only the live server can fill (Questbook's finding); it is kept and named in the header, and a consumer renders it as a quantity word.
  • One file per locale, self-selecting. Data/<Version>/<locale>/Text.lua loads only when lib:WantsLocale(locale) says so: the client's own locale, or enUS when the client's locale ships no file (enGB counts as enUS). Data/<Version>/_core/Locales.lua declares the shipped set through lib:SetShippedLocales and loads before every text file, because the fallback reads it. Adding a language is one generated file, a regenerated manifest and one TOC line; toc_spec.lua pins the order and the gate.
  • English ships now: Vanilla 4,245 quests (3.29 MB), TBC 6,599 (4.96 MB). Every quest has at least a title. tools/build-text.py generates it; the dump's locales_quest table has 0 rows, so --locale deDE and friends exit with a message until a translation source exists. Known cost, accepted with the directive: a non-English client reads English text until then.
  • tools/questdb_common.py now holds the dump lookup, fingerprint, Lua escaping and header shared by both generators; build-rewards.py imports it.

Rewards API (MINOR 2 to 4)

  • GetQuestXP returns nil for a player at or above the level cap (MINOR 4), and GetLevelCap() reports it (60 Vanilla, 70 TBC, from the loaded data's expansion). Questbook's finding on 2026-09-18: the scaler only looked at the level DIFFERENCE, so a level-60 player on a level-58 quest was told the full XP when the server pays moneyMaxLevel instead. The column's original name in mangos was RewXpOrMoney (sql/archive/0.10/4875_mangos_quest_template.sql), and the quest-details packet comments it "used in XP calculation at client" (QuestHandler.cpp), which is the code-level basis for the XP-or-money reading; Player::RewardQuest itself was still not read. GetRewMoneyMaxLevel() returns 0 under QUEST_FLAGS_NO_MONEY_FROM_XP (0x100), a flag the enum comment marks "Not used currently" but the getter honours. Counted in the dumps: 139 Vanilla and 27 TBC quests carry it with a non-zero RewMoneyMaxLevel, so the generator ships it as a tenth field and GetRewards reports moneyMaxLevel = nil for them while xp, which the emulator reads from the raw column, is unchanged.

  • GetRewards(questId) returns the quest's rewards as a fresh table: items, choices, money, moneyMaxLevel, xp, questLevel, rep, spell, spellCast, srcItem; nil for a quest with no rewards or an unknown id. HasRewards, IsReady, GetRewardCount and GetSource alongside it. Storage is one packed \031-separated string per quest, decoded on read; the reader accepts a bare id as count 1 so a generator that ever drops :1 on single items cannot silently lose most rewards (Questbook's finding).

  • Money and reputation are SIGNED and stay signed. 69 Vanilla quests charge money (RewOrReqMoney down to -3,000,000 copper) and 69 cost reputation with an opposing faction. abs() anywhere on the path would render "you receive 300 gold" for a quest that costs it.

  • money and moneyMaxLevel are two things, not one field spelled twice. RewOrReqMoney (845 of 4,245 Vanilla quests) is paid or charged at turn-in; RewMoneyMaxLevel (3,493) is the money a character at the cap receives in place of XP, per the CMaNGOS wiki, and the base of the XP formula. money = nil therefore means the quest pays nothing at turn-in below the cap, which is most quests. The emulator's Player::RewardQuest was not read; the two columns' meaning rests on the wiki and on Quest::XPValue.

  • GetQuestXP(questId, playerLevel) and xp on the result. The dump has no RewXPId column (verified against all 131; that is Wrath's QuestXP.dbc mechanism). The emulator derives XP from RewMoneyMaxLevel and QuestLevel (Quest::XPValue, src/game/Quests/QuestDef.cpp, read from both mangos-classic and mangos-tbc on 2026-09-18 and identical): full XP is RewMoneyMaxLevel / 0.6 up to quest level 60 (1.2, 2.4, 3.6, 4.8, 6.0 for 61 to 65+), then scaled to 0.8 / 0.6 / 0.4 / 0.2 / 0.1 for a player 6 / 7 / 8 / 9 / 10+ levels over the quest, rounded up. Computed in integers because 102 / 0.6 is 170.00000000000003 in a double and ceil would pay 171 where the server's float32 pays 170. This is the emulator's arithmetic, not a measurement of the live client; consumers label it "up to N XP".

  • questLevel is shipped because it is half of that formula, not as quest metadata. Questie remains the source for levels, zones and chains; a quest whose only field would be its level is not emitted at all.

Data

  • tools/build-rewards.py <Version> reads quest_template from the CMaNGOS SQLite world database that LibItemDB already caches (ItemDB/tools/cmangos_cache/, fetched by its build-proc-rates.py) and writes Data/<Version>/_core/Rewards.lua. It never downloads; one copy of a 117 MB dump on the box is enough. It introspects the table and refuses to run against a dump missing any column it reads, rather than guessing.
  • Vanilla: 4,140 of 4,245 quests (248 KB). TBC: 6,270 of 6,599 (371 KB). Both are locale-independent: every value is an id or an integer. Quest TEXT is per-locale and ships separately (see above).
  • Every generated header records the dump's content fingerprint (classicmangos.sqlite sha256:0c908c740286, tbcmangos.sqlite sha256:f76a4e173ce5) because cmangos/<x>-db publishes only a moving latest asset with no version to record.
  • Licence: the database repos are GPL-3.0 (the server core is GPL-2.0 and is not read). Facts are extracted and re-emitted in this library's own schema, no CMaNGOS file is redistributed, and the source is credited in each header, the README and the CurseForge description, on the same footing as LibItemDB.

Tests

  • Offline suite on the WoWAPITesting harness (Tests/wowapi submodule at bb80c1b, .busted shim, Tests already in .pkgmeta ignore). Tests/libquestdb_spec.lua pins the codec and every read against a synthetic fixture, including the signed values, trimmed rows, bare ids, the XP table for quest levels 60 to 70, integer rounding, the level cap, the locale fallback (own locale, English stand-in, enGB, no GetLocale), the text codec, and library upgrade (a newer copy keeps an older copy's rows). Tests/data_spec.lua loads the SHIPPED tables for both flavours and checks the header counts, the fingerprint, that every reward and text row decodes, that signed money, signed rep and the $ markup are present in the data, and three known Vanilla rows. Tests/toc_spec.lua checks both manifests: files exist, load order, each flavour loads its own expansion's data, the locale manifest precedes every text file and lists it, each text file gates on its own locale, no dependency, identical directives. At release: 71 specs, 100% line coverage on LibQuestDB-1.0.lua, luacheck / ruff / markdownlint clean.

Project

  • Repository created. Folder under the Classic Era AddOns tree, git directory kept outside the WoW install at C:\Users\ianpl\wow-addon-gitdirs\_classic_era_\QuestDB, single-root VS Code workspace. Packaging, release workflow, lint and ignore files are modelled on TOGBankClassic, with the library-shaped .pkgmeta and .luacheckrc from ItemDB.
  • Era and TBC TOCs, and the dev sync watcher. QuestDB.toc (11509) and QuestDB_TBC.toc (20506), matching the flavours ItemDB and ProfessionDB ship for. wow-version-replication.ps1 mirrors the source into the _anniversary_ install, driven by .pkgmeta's ignore list, and .vscode/tasks.json starts it on folder open.
  • CurseForge project linked. .pkgmeta carries curseforge-project-id: 1700352 and both TOCs ## X-Curse-Project-ID: 1700352. The project's SLUG is questdb, recorded as a comment in .pkgmeta because the packager has no key for it; consumers list that slug under required-dependencies.
  • Workspace Lua analysis: the annotations path is now absolute. Every pairs, tonumber and select in the library was flagged as an undefined global. First diagnosis (wrong): the .code-workspace disables LuaLS's stock builtins, so re-enable them. The ketho.wow-api extension rewrote that block back within minutes; it manages it, and disables the stock builtins because its own Annotations\Core\Lua\basic.lua defines WoW's versions. The library path that should have loaded those annotations was written with a ~ prefix in the .code-workspace only. Making it absolute there did not clear the Problems panel; adding the absolute path to .luarc.json's workspace.library did (read back through the editor's Problems panel). LuaLS treats .luarc.json as authoritative, so the annotations path now lives there, and the workspace file's copy is kept in step with a comment saying the extension owns the builtin block.

This mod has no additional files