QuestDB-v0.0.1
What's new
QuestDB Changelog
[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

