QuestDB-v0.0.2
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 staynil. - 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
pathon 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"ornilfor either faction, as a seventh field of the packed link. Flights: the link'sobjectis 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), soTRANSPORT_SIDEin 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 undertools/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, sofrom:tocannot 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 byTravelNodePathType(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, anddata_spec.luanow 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;pathdecodes one walk link's polyline lazily. A node whosecontinentdiffers from the graph's is the far end of a boat or zeppelin, and such a link appears in both continents' lists.GetPathContinents()andlib.PATH_TYPEalongside. 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.luachecks 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)returnstitle,details,objectives,offerReward,requestItems,endTextandobjectiveTextsin the loaded locale, with WoW's$N/$C/$R/$B/$Gmarkup left for the consumer to expand.HasText,GetTextLocalealongside. Rows are packed like the rewards; newlines are written as\ninside the Lua literal so every row stays on one source line. Three Vanilla rows (the Ahn'Qiraj war effort) carry a$<id>wworld-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.lualoads only whenlib:WantsLocale(locale)says so: the client's own locale, orenUSwhen the client's locale ships no file (enGBcounts asenUS).Data/<Version>/_core/Locales.luadeclares the shipped set throughlib:SetShippedLocalesand 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.luapins 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.pygenerates it; the dump'slocales_questtable has 0 rows, so--locale deDEand 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.pynow holds the dump lookup, fingerprint, Lua escaping and header shared by both generators;build-rewards.pyimports it.
Rewards API (MINOR 2 to 4)
GetQuestXPreturnsnilfor a player at or above the level cap (MINOR 4), andGetLevelCap()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 paysmoneyMaxLevelinstead. The column's original name in mangos wasRewXpOrMoney(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::RewardQuestitself was still not read.GetRewMoneyMaxLevel()returns 0 underQUEST_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-zeroRewMoneyMaxLevel, so the generator ships it as a tenth field andGetRewardsreportsmoneyMaxLevel = nilfor them whilexp, 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;nilfor a quest with no rewards or an unknown id.HasRewards,IsReady,GetRewardCountandGetSourcealongside it. Storage is one packed\031-separated string per quest, decoded on read; the reader accepts a bareidas count 1 so a generator that ever drops:1on single items cannot silently lose most rewards (Questbook's finding).Money and reputation are SIGNED and stay signed. 69 Vanilla quests charge money (
RewOrReqMoneydown 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.moneyandmoneyMaxLevelare 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 = niltherefore means the quest pays nothing at turn-in below the cap, which is most quests. The emulator'sPlayer::RewardQuestwas not read; the two columns' meaning rests on the wiki and onQuest::XPValue.GetQuestXP(questId, playerLevel)andxpon the result. The dump has noRewXPIdcolumn (verified against all 131; that is Wrath'sQuestXP.dbcmechanism). The emulator derives XP fromRewMoneyMaxLevelandQuestLevel(Quest::XPValue,src/game/Quests/QuestDef.cpp, read from bothmangos-classicandmangos-tbcon 2026-09-18 and identical): full XP isRewMoneyMaxLevel / 0.6up 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 because102 / 0.6is170.00000000000003in a double andceilwould 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".questLevelis 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>readsquest_templatefrom the CMaNGOS SQLite world database that LibItemDB already caches (ItemDB/tools/cmangos_cache/, fetched by itsbuild-proc-rates.py) and writesData/<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) becausecmangos/<x>-dbpublishes only a movinglatestasset 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/wowapisubmodule atbb80c1b,.bustedshim,Testsalready in.pkgmetaignore).Tests/libquestdb_spec.luapins 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, noGetLocale), the text codec, and library upgrade (a newer copy keeps an older copy's rows).Tests/data_spec.lualoads 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.luachecks 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 onLibQuestDB-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.pkgmetaand.luacheckrcfrom ItemDB. - Era and TBC TOCs, and the dev sync watcher.
QuestDB.toc(11509) andQuestDB_TBC.toc(20506), matching the flavours ItemDB and ProfessionDB ship for.wow-version-replication.ps1mirrors the source into the_anniversary_install, driven by.pkgmeta's ignore list, and.vscode/tasks.jsonstarts it on folder open. - CurseForge project linked.
.pkgmetacarriescurseforge-project-id: 1700352and both TOCs## X-Curse-Project-ID: 1700352. The project's SLUG isquestdb, recorded as a comment in.pkgmetabecause the packager has no key for it; consumers list that slug underrequired-dependencies. - Workspace Lua analysis: the annotations path is now absolute. Every
pairs,tonumberandselectin the library was flagged as an undefined global. First diagnosis (wrong): the.code-workspacedisables 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 ownAnnotations\Core\Lua\basic.luadefines WoW's versions. The library path that should have loaded those annotations was written with a~prefix in the.code-workspaceonly. Making it absolute there did not clear the Problems panel; adding the absolute path to.luarc.json'sworkspace.librarydid (read back through the editor's Problems panel). LuaLS treats.luarc.jsonas 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

