Questbook-v0.1.1
What's new
Questbook Changelog
[v0.1.1] - 2026-09-24 - Other addons can guide with the HUD; quest text in your own language
Other addons can guide the player with Questbook's HUD (2026-09-24)
Questbook:TrackPlace(target), for ItemDB's new Where to get it window (contract LIBREQ-QB-TRACKPLACE). Clicking a mob, vendor or node row there starts the HUD on it, exactly as a Places row click does, and switches a hidden HUD back on because the click is deliberate.targetis the tracker's world-yards shape,{ name, kind, world = { continent, x, y }, location = { zoneName } }. Only those fields are copied, so the caller's table is never held. It answers a clean false and never raises when the target has no numericworld.continent/x/y, or when the addon is not initialised.Questbook:StopTracking()andQuestbook:IsTracking(), for the stop icon on the same window (contract LIBREQ-QB-STOPTRACKING).StopTrackingis the universal stop that the window's own stop icon and/questbook hud offalready use, so it stops whatever is being guided, not only a place ItemDB sent. Both answer false before the addon is initialised.- As with
ShowQuest, a method's presence is the feature flag, and there is no version to check. Pinned byTests/trackplace_spec.lua. Confirmed working in game by the operator, 2026-09-24.
A quest in your log shows its description in your own language (2026-09-24)
- For a quest the character is carrying, the detail pane's Description and Objective text now come from the game client, which gives the server's own words in the player's language. QuestDB's text is English only, so on a German or French client every description used to be English with a "shown in English" label. Quests not in the log keep QuestDB's text, as before.
QuestLog:Text(Modules/QuestLog.lua) reads it the way Blizzard's own log does:GetQuestLogQuestText()answers only for the SELECTED log entry (Vanilla/QuestLogFrame.lua:355-367), so it saves the player's selection, selects the quest, reads, and restores the selection. That is the pattern Blizzard's own WatchFrame_AbandonQuest uses (Wrath/WatchFrame.lua:1184-1197).- Not verified: whether any of these calls is restricted. No restriction flag was found either
way, so the read is
pcalled and the selection is put back even when the read raises. Each quest is read once per session, because every read briefly moves the log selection. - Pinned by nine examples in
Tests/questtext_spec.lua. Confirmed working in game by the operator, 2026-09-24; not yet checked on a non-English client. - Harness pin moved to
2d4b78f, which modelsGetQuestLogQuestText()on the quest log (Questbook's request 63f4ea90). The stand-inTests/env_questbook.luacarried for it for an hour is deleted, and the read-once example spies on the harness's call instead of reading the stand-in's counter. 862 green on the new pin.
[v0.1.0] - 2026-09-23 - Off Questie, onto QuestDB: three game versions, alts, and a router that uses the roads
One TOC per game version (2026-09-23)
- Questbook now ships three manifests instead of one:
Questbook.tocfor Classic Era,Questbook_TBC.tocfor TBC and Anniversary, andQuestbook_Camelot.tocfor WoW Forever. It previously declared all three interface numbers as a comma list in a single file. Operator: "single tocs for addons makes me nervous. fine for libraries, but addons need different things for different game versions". - The reasoning is about what happens later, not about today. Nothing in
Modulesbranches on the client right now, so a comma list works -- but an addon accumulates version-specific needs (a file that only makes sense on one client, a dependency one version lacks, a Blizzard frame that moved), and the comma list is the shape that has to be unpicked the first time one appears. Three manifests absorb that change where one has to be restructured for it. - The cost of three copies of a forty-line file list is specced, not trusted. A spec asserts that
every
Questbook_<flavour>.tocloads the same files in the same order as the base manifest and declares the same dependencies and optional dependencies, and that between them the three cover exactly the versions QuestDB ships data for -- no more. Order is checked as well as membership, because these files run top to bottom at load and later modules read tables earlier ones create. Red-checked by deleting one file line from the TBC manifest: it fails and names the file. - The replication script is unchanged on purpose and now says so: it copies the tree, so all three manifests travel together and each client picks the one matching its own suffix. Filtering them per flavour there would be a second place for the version list to drift from the manifests.
In-game confirmations, and the two faults they found (2026-09-23)
- The Active tab's character picker works in a real client, and so does the whole window on a TBC client -- 3,778 of 6,152 quests listed from QuestDB's TBC data, with the Status column filled. Neither had ever been run outside the offline suite.
- Another character's rows were showing YOUR distances, and a spec was passing on it. The Yards
column filled with the distance from where the logged-in character stands to a place the OTHER
character's quest needs -- a number about two different people, and precisely what this feature
says it never does.
ActiveView:Rowswithheld the distance correctly and there was a spec pinning that;ActiveView:RefreshDistancesthen ran on the tab's own once-a-second update and read the live position unconditionally, so the column refilled itself about a second after the pick. The lesson is the one worth keeping: the constructor and the refresher are two doors into the same state, and the spec guarded one of them. Fixed by taking the position as nil while viewing another character, which clears a stale distance rather than merely declining to set a new one -- an early return would have frozen whatever happened to be on screen at the moment of the switch. The new spec drives the update after picking, and pins that character's quest to one with a known stop so a distance really is computable there and nil means refused rather than unavailable. - Removed: a spec that could only pass if a third-party file was edited. It required every hard
dependency's TOC in this install to declare every interface number Questbook declares, including
WoW Forever's. No correct Ace3 can satisfy that from the Classic Era tree, because each flavour
has its own install tree with its own build of every dependency -- and the only way it was ever
made green was by hand-editing Ace3's TOC, which is third-party code that updates itself. The edit
has been reverted, the comparison is gone, and what remains asserts something true from where it
stands: every hard dependency must be installed beside us with a readable TOC. The interface
question belongs to whichever tree the client is launched from, and the harness's
verify-toc-deps.luatakes that path as an argument, so it can be aimed at the right one. /questbook timingsettled a deferred decision by measurement. A Browse keystroke costs 13.5 ms on TBC's 6,212 quests once the first pass has cached -- under the 16 ms frame at 60 fps, which was the threshold agreed in advance -- so the planned in-log-set optimisation was dropped rather than built. The same run found something bigger: the first status stamp costs 994 ms, about a second of frozen client on the click that opens the window./questbook timingnow prices the Ready rule separately, because the 994 ms had a suspect and no evidence. It prints a second line under the stamp saying how much of that total went into the first evaluation of the availability rule, and over how many uncached quests.- The Browse list opens faster, and the measurement is what chose the fix. On Classic Era the first pass cost 269 ms, of which the Ready rule was 141.7 ms over 2,599 uncached quests -- so the suspect was the largest single cost and still only half of it, and making it free would have left 127 ms. The count was the thing worth attacking, not the rule. Questbook was working out the status of every quest in the game on every filter run, and then throwing most of those answers away with the rows a zone, level or search filter rejected. It now works out the status of the rows that survive the filter -- except when the Status filter is the one doing the filtering, which cannot choose without the answer, so that case still checks everything. The visible behaviour is identical: every row on screen has its Status word, exactly as before.
The Active tab can show another character's quests (2026-09-23)
- Pick a character on the Active tab and see the quests they were carrying (operator: "to be able to switch the active tab between alts, so you can see quests from other toons from the current toon"). The picker sits beside Plan my route and lists every character Questbook has seen.
- It is a RECORDING feature, not a reading one, and the whole design follows from that. No client
call reads another character's quest log --
GetQuestLogTitleanswers for whoever is logged in and nobody else -- so Questbook writes each character's log down while you play them, into a new account-wide save keyed by name and realm. A character not played since this version landed is not listed at all, rather than listed as having no quests. - The guard that matters is the one against ERASING a record.
QUEST_LOG_UPDATEfires during login before the server has sent the log, so the client briefly reports zero quests for a character who has twenty. An empty log is therefore never written over a record that has quests -- without that, logging in would destroy the thing the feature exists to keep. A character genuinely holding none keeps their last record, which is stale rather than wrong. - Another character's rows are deliberately inert: no distance, because that measures from where YOU are standing to a place THEIR quest needs; no active mark, because the active quest is yours; and no arrow, because the client cannot make somebody else's quest the one you are tracking. The row still opens in the panel, which is the point of looking.
- The status line says whose log it is and how old the record is -- in days, because what a player wants to know is whether it is current, and a date makes them do the arithmetic.
- The picker hides itself while you have only one character, which is the state every new install is in: a dropdown with one entry is a control that cannot do anything, and its presence implies a feature that is not there yet. It appears the first time a second character's log is recorded.
- The session's own audit found that this feature re-opened a bug fixed four hours earlier. The panel explains a quest no database describes only for a quest the player is HOLDING -- and every row in an alt's list is by definition one this character does not hold, so each of those clicks went back to doing nothing. Fixed by asking whether the character whose log is ON SCREEN holds it; a spec pins that this did not also broaden to an arbitrary id, which would break the promise Dibs reads. The general shape is worth more than the fix: the scope of a fix is a claim about which callers exist, and it expires silently the moment you add one.
- The empty-log guard is pinned against a driven event SEQUENCE, not a cleared table -- a character with a record, the modelled login-before-update state, the event fired, the record intact; then the log filled, fired again, written. Red-checked: with the guard removed it fails with the record at zero quests, which is the data loss itself rather than a proxy for it.
Player.Key()is now the ONE spelling of "Name - Realm". The Dibs riding-gear loadout was building it inline and the roster needed the same string; two spellings of an identity that files and reads saved data is how a record gets written under one key and looked for under another.
A quest nothing knows now says so, instead of the click doing nothing (2026-09-23)
- Clicking a quest no database describes used to do NOTHING AT ALL.
DetailPane:Showreturned false when there was no view, so the pane went on displaying the previous quest -- and from the player's side a real hole in the data and a broken list look exactly the same. It now says what is missing. - QuestDB built the signal that makes it specific. Asked on thread 201faa89 whether a player
could be told WHY a quest is absent, they shipped
GetCoverageat MINOR 18: "shipped", "uncovered" (this flavour's client lists the id and nothing offline describes it) or "unknown". Questbook reads it through a newCoverageon the provider seam, capability-gated, and an older QuestDB simply gets the general sentence instead of the sharper one. - It is NOT a WoW Forever case, which is the part worth knowing. QuestDB measures 1,275 uncovered quest ids on Classic Era and 372 on TBC as well as 3,065 on Forever, so this fires on the version Questbook has shipped longest.
- Only for a quest the player is HOLDING, and that limit is QuestDB's own. Their signal is sound
about an id you already have -- a quest in your log is real whatever a database says -- and
unsound as a general claim. So an arbitrary unknown id still gets a clean refusal, which also
keeps the public
Questbook:ShowQuestcontract Dibs reads (DIBSREQ-QB-001). The suite caught that the first cut broke it: four specs went red, and re-baselining them would have shipped a broken promise to another addon. - The wording never says "no such quest", deliberately. QuestDB is explicit that "unknown" is not a claim the id is invalid -- the client's own quest table under-lists by hundreds of ids on every flavour -- so the pane says nothing here knows this quest. And no coverage COUNT is ever shown to a player, because those totals are unsound as counts and QuestDB said so.
- Questie answers
nilrather than "unknown": it cannot tell a hole from an id it never carried, and saying otherwise would invent a fact.
Questbook on WoW Forever and TBC, in one TOC rather than three (2026-09-23)
- Questbook now declares Classic Era, WoW Forever and TBC Classic --
## Interface: 11509, 16001, 20506, where 16001 is Forever (the client'scamelotgame type) and 20506 covers the Anniversary Edition as well as TBC. The TOC already carried Era and TBC; Forever is the addition. - Nothing in the addon changed, and that is the finding rather than luck. No module branches on
the client -- there is no
WOW_PROJECT_IDread anywhere inModules/-- because the per-flavour data is QuestDB's problem and the per-flavour client calls are feature-detected. The onboarding is therefore one line plus the specs that hold it. - Every dependency was checked rather than assumed, one flavour at a time: QuestDB ships
QuestDB_TBC.tocandQuestDB_Camelot.tocwith real data behind each (Data\TBC\andData\Forever\), ItemDB ships the matching pair, and VersionCheck-1.0 and LibAceGUIWidgets both declare 16001 and 20506 in their own comma lists. No contract was needed. - ONE TOC, NOT THREE, WHICH IS THE DOCUMENTED PREFERENCE AND NOT A SHORTCUT. Blizzard's guidance
is that comma-delimited interface versions should be preferred over client-specific TOC files
where possible. FastGuildInvite, QuestDB and ItemDB ship per-flavour files because their file
lists genuinely differ -- QuestDB loads
Data\TBCorData\Forever, FGI loads a retail-only library -- and Questbook's does not. The hazard avoided is measured, not theoretical: the harness records a fleet addon shippingVersion.luain one manifest of six, 503 specs green, and the addon's own version reading nil on five flavours. - Two specs hold it, because the whole of this support is one line and nothing else in the suite would notice it changing. One pins the three interface numbers by name, and pins the other direction too -- a flavour declared here must be one QuestDB has data for. The other says that if a per-flavour TOC is ever added, it must carry the same file list and the same dependencies as this one, so the drift above cannot arrive quietly. Red-checked: removing 16001 fails exactly the first spec, naming Forever.
- The replication script is deliberately unchanged. Forever is a separate product with its own
install root, not a flavour directory beside
_classic_era_and_anniversary_-- this install holds only those two plus_classic_and_retail_. A_forever_entry would have matched nothing silently, which is the same failure mode as a malformed.pkgmetaignore, so the script says so instead of guessing a name. - The player-facing docs were already wrong and are now right.
README.txtand the CurseForge description both said Classic Era only, while the TOC had carried TBC for some time. Both now name all three, and both say plainly that Forever's own quests are absent from the database -- they will not appear in Browse and the HUD cannot guide to them, while everything Classic has is there. - Ace3 was the one thing that did not support Forever, and it is one line. Its TOC shipped every
flavour from Vanilla to retail and not 16001, while Questbook, LibAceGUIWidgets and
FastGuildInvite all hard-depend on the standalone Ace3 and all now declare Forever -- so that single
line is load-bearing for the fleet's Forever support, not just this addon's. Added it.
- Whether it was strictly necessary is still not verified, and adding it is what makes that not matter. TOC loading is client behaviour rather than an API listing, so the Blizzard trees do not say whether Forever refuses to load an addon whose declared dependency is flagged out of date; it may turn entirely on the player's "load out of date AddOns" setting. The edit removes the dependence on that answer instead of measuring it.
- It follows an established practice on this machine rather than inventing one: 41 TOCs here already declare 16001 and many are third-party -- TomTom, Bagnon, Auctionator, AllTheThings, Plater and the DBM set among them. Ace3 was the one that had been missed.
- KNOWN COST, stated rather than buried: this is a hand edit to third-party code and an Ace3 update overwrites it silently, taking every dependent addon off Forever with nothing naming Ace3 as the cause. The alternative, if that proves annoying, is embedding AceGUI as 14 addons on this box already do -- at the cost of the fleet's single shared Ace3.
- So the suite now detects the revert rather than a note asking someone to remember it. The
regression would be silent, delayed, and misattributed: what anyone sees weeks later is several
addons missing on Forever with nothing wrong in any of their own files, so the Camelot TOCs get
read first -- and they are correct -- while
Ace3.tocis the last place anyone looks, precisely because nobody edited it deliberately. A spec now reads the installed Ace3 and requires it to carry every interface number Questbook's own TOC declares, deriving the list rather than restating 16001, so adding a flavour above automatically extends the check. Red-checked by taking the number back out: it fails, names 16001, and says which file to fix.
The bottom row stops being ours, and a spec that was reading the wrong branch (2026-09-23)
- The row of icons beside the Close button is LibAceGUIWidgets' now (
W:DressBottomRow/W:UndressBottomRow, MINOR 33 -- shipped in their v0.2.3 and tagged, so this is an adoption and not a working-tree feature-detect). Questbook raised the contract (QBREQ-LAW-001) after its own stop icon shipped at about half the gear's size: four addons in this fleet grew the same row from the same constants by hand, and the library now derives them from AceGUI's own Close button and asserts them in its own spec, so the day AceGUI moves that button one file says so instead of four drifting apart.- The stop icon's tooltip is now read AT HOVER rather than decided in an
OnEnterof ours -- the half of the contract that was asked for by name, so the icon can say "nothing to stop" without the row being rebuilt. - The status bar is put back where it ACTUALLY was, which our own restore could not do: it wrote AceGUI's stock numbers, and a bar some other addon had pinned elsewhere would have been moved by us on the way out. The library reads the real anchors at the first dress.
Chrome:DressByHandstays as the older-library branch. LibStub hands back the highest minor anything in the install has loaded, so a player whose other addons embed a copy older than MINOR 33 still gets the row; deleting it would have traded one fleet's tidiness for their window.
- The stop icon's tooltip is now read AT HOVER rather than decided in an
- The library's icons hid the tooltip without handing the anchor back, and ours did. Caught by
the existing spec for exactly that, which is the only reason it is not in the release: the next
tooltip the client draws without setting an owner of its own would have appeared where Questbook's
icon was. The row re-installs
HideTooltipon the library's icons, so every tooltip in this window still leaves the same way. Reported to LibAceGUIWidgets rather than only worked around. - Harness pin moved to
f67df9c, six adoption entries on from where it sat. None of them needed anything here -- three say adopt nothing, and the other three turn onprotected.enforceChat,setDeprecationFallbacksandframes.ACCESSORS, none of which these specs touch. Pulled anyway rather than resting on that check, and the whole gate re-run against it. - A new spec for the older-library branch was passing while testing nothing, and finding that is what the coverage gate is for. Both branches cache their buttons under the same names on the frame, and the hand-rolled path only installed its handlers when it CREATED a button -- so handed a frame the library had already dressed, it skipped them and left the library's in place. The spec then asserted the hand-rolled stop tooltip and was really reading the library's. The handlers are installed on every dress now, which is idempotent the way the library's own row is. Nothing in the game could reach it -- LibStub's minor cannot change inside a session, so only one branch ever runs -- but a test that passes while testing nothing is worse than no test.
A road that goes round a hill, a fixture that deleted its own subject, and two promises read as one (2026-09-22, later)
- The roads were being ignored on most trips, and one screenshot found it. The operator: "why is
this quest a straight line, not following the paths and through the 'obstacles' like some of the
other ones?" -- a line drawn straight over the ridge between Undercity and Deathknell with the
road right there.
OFF_ROADdecides how much longer the road may be before a straight walk wins, and it was 1.5, with a comment saying in as many words that the figure was a starting point to be tuned after a run.- The first answer was 2.5 and it was still a guess. Peer Review refused it on the grounds that one observation bounds a constant from BELOW and says nothing about how far above -- and that the opposite failure, a route that goes the long way round, looks like a route and is never reported, so waiting for complaints only ever pushes the number one way. They also pointed out the quantity was sitting in the graph: every road link carries its length in yards beside two nodes with coordinates, so this is a division and not an estimate.
- Measured over 7,544 player-to-target trips on both continents, the median road is 1.98x the crow's flight. A road twice as long as the straight line is the ORDINARY shape of a road, so 1.5 was not a hair short on one route -- at 1.5 the straight line won 92% of trips and the road network was effectively switched off. 2.0 gives it half of them, 2.5 a quarter. It is 4.0 now, which answers 4% of trips on Eastern Kingdoms and 8% on Kalimdor with a straight line: the genuinely absurd detour, and the target so close that the nearest road is further away than it is.
- 4.0 is where the curve goes flat, which is a better reason than the percentile and is the one the code now gives first. Changing the rule by half a point moves about 44% of trips at 1.5 and about 1% at 4.0 -- some forty times less sensitive -- so near 4 the exact value barely matters, which is the property to want in a threshold nobody can check in game.
- The first version of that measurement sampled the wrong thing, which is worth recording because of the direction it erred in. It measured node-to-node ratios, omitting the walk from the player to the first node and from the last one to the target -- both of which are on the road side of the comparison. So it understated the ratio, and understating the ratio understates how often the STRAIGHT LINE wins, which is the very failure the constant exists to suppress. The gap turned out to be about one percentage point and the decision survived it; the tool now measures what the rule actually compares.
- The tool's weakest part is now bounded rather than caveated. It stands its player and target at invented offsets from graph nodes, which is the one part of it that models nothing. Swept at 0, 10, 25, 50, 100, 200 and 400 yards with one magnitude held fixed at a time, the ratio CLIMBS rather than stepping, and gently -- a tenth of a point of median across the first two hundred yards -- so a quest giver standing tens of yards off a road sits on rows that are the 0-yard row to two decimal places. The first version of that sweep sampled 0 and then 316 yards and upwards, concluding "how far off does not matter" with the whole effect sitting unsampled in between: flat across the interval you measured is not flat, it is flat across the interval you measured.
- That sweep's p95 also dips by 0.01 where it should rise, which is the only estimate of the tool's resolution there is: differences at or below about 0.01-0.02 in these numbers are noise. It is what makes "a tenth of a point" a measurement rather than an assertion, and it is worth more than the tidiness of a monotone table.
tools/measure-offroad.luaprints that distribution and is declared as a suite, so the next person to move the constant measures instead of guessing. The ratio is still a stand-in for terrain, which the graph knows nothing about; where the line falls is no longer a stand-in.
- A ride's polyline is not promised to reach its node, nor to start at one, and the code believed
both. Peer Review, twice within the hour. QuestDB's
path()said "endpoints included" flatly and has withdrawn it: 2 Vanilla and 4 TBC transport routes do not span their link at all, and 12 more Vanilla and 16 TBC end over twenty yards shy. The ride branch dropped the last point of each half by index on the strength of that sentence -- and, one line up, the first point of the near half as well. Where a line really does arrive nothing changes; where it stops short a real point was thrown away and the ride drew twenty yards wrong at whichever end was short. Both ends now trim by POSITION: a point is skipped when it is the point already placed or the arrival node itself.- The arrival was never at risk -- the node is appended outright rather than taken from the line -- so the HUD's altitude readout and its arrival test were reading the node throughout.
- The comparison is component equality, not distance.
separationdrops the height term when either point lacks one, and a lift is exactly a pair sharing an x and a y and differing only in height -- so a single missing height would have deleted the top of the shaft with nothing to say so. Peer Review raised it as reachable-or-not; comparing the three components makes it neither. - The general shape, which is worth more than either fix: an absolute promise in a document does not merely go unchecked, it gets built into control flow as an OPTIMISATION. Neither of these read the last point as the arrival -- that had already been fixed. They DELETED points the promise said were redundant, so the assumption was invisible as an assumption. Where a document says always or included, the thing to look for is not the read but the shortcut.
The suite was red, and the fixture had deleted the thing it was testing (2026-09-22, later)
Nothing here reaches a player; it is all test-side, and it is written down because the failure shape is one that hides.
- The fixture's boat stopped being taken, so twenty-one specs quietly stopped testing a ride. When per-vessel boarding waits landed, the fixture's zeppelin turned out to be the Grom'Gol -- Orgrimmar one at 368 seconds, the most expensive vessel in the game -- and at the fixture's geometry that loses to the flight even on foot. Every spec that needs a ride leg to exist lost its ride. The world now carries the Orgrimmar -- Undercity zeppelin, the one Horde vessel whose measured wait sits inside the band both halves of the walk-speed demonstration need (about 186 to 340 seconds), and the band is written in the fixture so the next person to change it knows there is one. The same link exists in two graphs, one per continent, and changing only one priced the outbound and the return differently -- the boat out, the flight back.
- Four specs raised a constant that had stopped being read. They set
TRANSPORT_WAITto 100000 to force the flight; once the wait became per-vessel that constant was only the fallback for an unknown entry, so the knob was connected to nothing. They kept passing, because by then the boat was expensive enough to lose unaided -- a forcing step that stopped forcing, hidden by the thing it was meant to force happening anyway. - A spec restored its own fixture before the call it was testing, so the instance-door fallback had no door to find; and its neighbour, which asserts a placed giver beats the door, passed vacuously for the same reason -- with the door already removed there was nothing for the giver to beat. The door case was also aimed at the wrong entity: the fixture's giver stands INSIDE the instance, which reaches the older entrance rule, where the operator's Private Rocknot is placed nowhere at all.
- Spec files now run in reverse and shuffled order too, in a declared suite beside the CurseForge description checker. It found one immediately: the harness advances the clock by exactly what it is asked for but ticks frames in SLICES, and away from zero those sum a float step short -- so the HUD's sampler declined to sample and two speed specs failed, depending on how far the clock had already run. Filed to WoWAPITesting; worked around here by a microsecond, with the cause at both call sites.
- Adopted the harness's
C_Map.GetMapRectOnMapandUnitIsDeadOrGhostand deleted both stand-ins. Their box is positional where ours took named keys, which fails quietly: a named-key table indexes to nil and the caller's guard reads it as "that map is not on this one".
A boat accelerates, a version gate is a promise not a fact, and a key list stopped being a count (2026-09-22)
Two peer reviews landed together. Between them they found one defect that would have reached a player, one class of defect that had not bitten yet, and a ruling on a question this addon had left open with no evidence either way.
- A vessel's speed is now the server's own number, and it ACCELERATES.
BOAT_SPEEDhas been wrong twice: 10 (a unit error -- a vessel's apparent speed divided into world-map distance, which made transports unreachable at every distance) and then 32, borrowed from the flight speed as a category correction. QuestDB read the real figures out of the emulator: a type-15 GameObject'sdata1ismoveSpeedanddata2isaccelRate, and every rideable vessel in both dumps is 30 yd/s at 1 yd/s/s -- both Grom'Gol zeppelins, the Orgrimmar--Undercity zeppelin, the Booty Bay ship, the Menethil boats, the Feathermoon Ferry. The only outlier is Naxxramas, which nobody boards. They checked the units rather than assuming them, because a plausible 30 beside a plausible 32 is how a wrong field index survives.- The acceleration matters more than the speed. At 1 yd/s/s a ship needs 450 yards to reach
cruising speed and 450 more to stop, so a dock-to-dock leg under 900 yards never cruises at
all and the old flat
distance / speedoverstated every short hop. A 2,000-yard crossing is 97 seconds rather than 67 -- an effective 21 yd/s -- and a 400-yard hop averages almost exactly 10, which is why the original wrong constant was not as mad as it looked: it was a cruising speed used where an average was wanted. - Marked DERIVED, not measured. The two inputs are read out of the dump; the algebra is QuestDB's, and nobody has timed a real ride against it. If an in-game observation disagrees, the observation wins and the spec is what gets re-baselined.
- No per-vessel pricing. It was offered and declined on evidence: the vessels do not differ, so a lookup would be machinery expressing a difference that is not in the data.
- The acceleration matters more than the speed. At 1 yd/s/s a ship needs 450 yards to reach
cruising speed and 450 more to stop, so a dock-to-dock leg under 900 yards never cruises at
all and the old flat
- The version gate was a promise where it needed to be a fact. Four sites asked "is QuestDB at this MINOR?"; two also checked that the method was actually there and two did not, which is one concept with two spellings and no way to tell which a new site should copy. On any install where the version and the method set come apart -- a fork, a hand-copied library, a backport, a renamed reader -- the bare sites would raise "attempt to call a nil value" into the player's client from a read that had a perfectly good quiet degradation available. One helper now takes the MINOR and every method the site calls. Peer Review read three of the four gates and named one bare site; reading all four found a second.
- A key list stopped being a positional contract. Questie answers queries positionally, so every
reader was a hand-counted
values[13]whose meaning lived in a list forty lines away -- and a dead key was being fetched on every quest pane open purely because removing it would have shifted ten fields silently. Every read is now by NAME. The dead key is merely unused rather than unremovable, and an eleventh reader added later cannot be one place off. The same shape cost QuestDB a live defect the same day: an unknown key came back nil and erased a good value.- The one loop that still reads positionally does so generically, over three different key lists, and says so. Position as an index into the caller's own list is fine; position as a number written into a read forty lines from the list is what was removed.
- What a reduced install cannot do is now said on the window's status line, not at login. Ruled by Peer Review on a question left open with no evidence either way, and the reasoning is that a login message reaches somebody who has not yet had the problem, about a capability they may never use -- and by the time it is relevant it has scrolled several screens away. The player meets the symptom when they open the pane. The no-database case keeps its login line, because that window cannot be filled at all and a status line inside an empty pane has to be gone looking for.
- The forward/reverse seam: accepted deliberately, and written down as a decision. The search index reads QuestDB's reverse links; the quest pane reads the forward view. QuestDB has ruled the two may disagree -- 96 quests, 106 one way and 89 the other -- and their spec says a consumer needing agreement must intersect them itself. Closing it means a full quest decode per quest added to an index build that has never been timed in game, so it is accepted for now and the reasoning sits at the call site. Peer Review's objection was not the divergence but that a decision had been made on one side of the seam with the consumer unaware; that part is fixed.
- Three comments that had outlived their code, all corrected rather than deleted so the next reader gets the correction instead of re-deriving the original: one claiming a field both adapters carried when neither does any more, one implying the boat and flight speeds are coupled when they are deliberately independent, and one naming an unmeasured mechanism for the blank Azeroth map where only the remedy is established. The map-rect caches were checked for invalidation hooks and have none, deliberately -- recorded, because an unexamined decision and a correct one look alike.
- The boarding wait was invented, and wrong for every vessel in the game.
TRANSPORT_WAITwas 90 seconds, from "a boat takes about three minutes round trip, so you wait about ninety on average" -- no source, and the largest single term in a ride's price. QuestDB computed the real cycles from each vessel's own waypoints and dock delays: Classic Era mean waits run 125 to 381 seconds, so the truth is between 1.4x and 4.2x what this assumed, and worst on the zeppelins players use most.- It is 268 as an INTERIM -- the mean of those eight, a statistic over their measurements rather than a guess, but still one number standing for a range of three. Per-vessel pricing is the right shape and is blocked on one thing: their table is by vessel NAME and this router keys on the GameObject entry. Asked for; not guessed at, because a wait attached to the wrong boat would look right and be wrong.
- The mean wait is not half the cycle. A vessel is boardable for its dock delay out of each
cycle, so a player arriving at random boards at once with probability
delay/period:(period - delay)^2 / (2 * period). Halving the period overcharges by about 15% on every route. - This changed which way a route goes, which is the clearest evidence the old number was doing damage: a mounted player crossing the test sea now flies where they used to board, because a 3,162-yard landing walk costs 226 seconds mounted and saves a four-and-a-half minute dock wait. On foot the same walk is 452 seconds and the boat still wins. Two specs were re-baselined, with the history written into them -- that split existed once before as an artefact of the 10 yd/s boat, and a test that flips twice needs to say which flip was which.
- The operator's original complaint survives it, checked before shipping: Tirisfal to Grom'gol is ~14,000 yards, so the zeppelin costs 865 seconds against 1,000 mounted and 2,000 on foot. It still wins, which it must -- that route walking past the zeppelin is the report that began all of this.
- Three things this session's own audit caught that no spec did, all fixed:
- A caching regression I introduced today. Folding "no library yet" and "library too old" into
one check made
GetTownsfolkcache an empty answer taken before QuestDB's data had loaded -- andPlacesIndex's Invalidate is deliberately forbidden from clearing that field, so the Places tab would have shown no trainers, repairers, banks or mailboxes for the entire session, with the data sitting right there. The two cases are not the same and the original knew it. 100% line coverage did not catch it, because no spec called that function before the data existed; there is one now. - A cross-continent
STEPwould have lost its far half. Making a step build a walk leg was right, but the walk branch reads one graph where the ride branch splits a crossing between two. No such link is known to exist -- a two-yard hop crossing an ocean would be strange -- but strange is not checked and the failure would be a silently truncated leg, so it is guarded. BOAT_ACCEL = 0would have deleted every vessel from every route. The constants are documented as tunable and zero is the obvious way to write "ignore this"; it divided by zero and produced infinite seconds, which Dijkstra reads as an impassable link. Now tested positively, so zero -- or anything that is not a number -- cruises instead.
- A caching regression I introduced today. Folding "no library yet" and "library too old" into
one check made
- Tests. 795 green; 100% line coverage on every touched module; luacheck clean.
A lift is not a boat: the way into Undercity, and a ride's polyline oriented too (2026-09-22)
Both of these came out of QuestDB measuring the shipped path graph against yesterday's two reports, on the peer-review thread. One of them turned a finding I had filed against their data into a fault in my own pricing.
- The route into Undercity went the long way round because this router charged an ELEVATOR a
boat's ninety-second wait. QuestDB's
PATH_TYPEhas a singleTRANSPORTtype covering "boats, zeppelins, elevators", andLinkCostpriced all three alike. Their Dijkstra over the shipped graph printed both ways out of Brill hop by hop: the way a player actually walks -- up through the Ruins of Lordaeron courtyard and down the Undervator -- is 936 yards over 10 hops, four of themTRANSPORTthrough the lift cluster, against a 3,213-yard walk-only loop west and south through the hills. Four imaginary waits is 360 seconds, and that is what made the loop win. The data had the right way in all along.- Operator: "the path to get to the FP in UC is wrong. it should be going into that green arrow leatrix puts on the map", and on what it costs: "if someone doesn't know how to get into UC, and they use the addon to try to find the entrance, they never will." That is the addon at its least useful for exactly the player who opened it.
- The data now says which it is, and that is what is read. QuestDB shipped
transporton a TRANSPORT link the same afternoon (MINOR 15): a vessel you wait for, a lift you step onto, or a step -- and the third is the one this router had not imagined. A step is the two-yard hop between a vehicle node and its entry node, with no GameObject and no travel at all, and QuestDB's note is that most TRANSPORT links are steps: three of the four hops into Undercity are. Charging one of those a lift's ten seconds is the same category error a size smaller, so a step is now priced as what it is -- a couple of yards walked at the player's own speed, which needs no constant and gets quicker on a mount. - A step is now BUILT as a walk leg, not a "board" leg, which is what its pricing already said it was. As a ride it cost the player twice: the HUD announced "BOARD Undervatorentry" for a two-yard shuffle, and because the readout names the first ride ahead, that noise displaced the real lift while the player was still walking towards it. Three of the four transport hops into Undercity are steps -- so the one instruction a player who does not know the city actually needs, take the Undervator, was the one thing hidden behind them.
- The span rule survives as the fallback for a QuestDB before MINOR 15 (
LIFT_SPAN, 200 yards): nothing that crosses an ocean has its ends that close, and no lift is that long. Still measured in three dimensions, because a lift's whole length is vertical -- the Undervator's two nodes share an x and a y and are 97 yards apart in z, so the flat measure called it a zero-length link and charged the full wait anyway.LIFT_WAITis 10 seconds, a rule of thumb like every constant beside it. - KNOWN COST, now confined to old data: on a pre-MINOR-15 file a genuine crossing encoded as a short link is priced as a lift and preferred by 80 seconds. Small against an ocean crossing's alternatives, where the fault removed is 360 seconds and draws a visibly wrong route. On current data the question does not arise -- a short vessel is believed to be a vessel.
- A RIDE's polyline is oriented now too, not only a walk's. The ride branch built its points from
the same unpromised
graph.pathcall, and a ride's points are what the HUD's steer caret follows while the player is aboard -- so a reversed one would not merely draw a spike, it would aim the caret back at the dock the ship had just left. QuestDB measured which of their polylines come back starting at the far end and reported fifteen, then corrected their own accounting the same day: seven genuine reversals, seven far halves of crossings that only look reversed, and one tie. Every one aTRANSPORT; none a walk. The seven were landing in my routes unoriented.- The seven "false" ones are the asymmetry this fix is built around, which is a nice confirmation from the other side: a crossing's far half legitimately starts at the map edge, so their check -- "is the head nearer the from-node or the to-node?" -- called it backwards. That is exactly why the far half is anchored at its ARRIVAL end here.
- The two halves of a continent-crossing ride anchor at opposite ends, which is why this was not the walk case's one-line flip: the far half has no node on its own near end -- it starts at the map edge -- so it is anchored at the node it arrives at and must finish there.
Orientnow decides in three dimensions as well, and that is what makes it useful rather than decorative here. All fifteen reversed links are lifts and dock pairs whose footprint is a single spot; QuestDB named a flat ends test as their own generator's blind spot, so reusing their rule unchanged would have reproduced the bug instead of catching it. A missing height falls back to the flat distance rather than reading a false zero.- The library's decoded polyline is its own cache and documented read-only, so both halves are copied before being reordered -- reversing in place would have reversed the graph itself and returned the sawtooth on every leg after the first.
- A walk leg now ends at the node it is keyed to. QuestDB stated the contract plainly the same day: "a polyline starts at its from node; it does not always reach its to node" -- the first pinned by a spec of theirs over every route on every continent, the second explicitly not given. 12 Vanilla and 16 TBC walk routes stop more than 20 yards short. The drawn line stopped there and the next leg leapt to catch up, and -- the silent half -- the HUD's ALT readout takes the leg's last height, so it reported the height of a point twenty yards shy of the flight master rather than the flight master's. A polyline that already arrives is untouched; a lift's two-point stub, both points the same coordinate at the top of a 130-yard shaft, becomes the straight line down it always should have been.
- The Swamp of Sorrows sawtooth was NOT mine, and it was not the polyline direction either. I
claimed that fix and had to withdraw it: no walk polyline in either continent is reversed, by
QuestDB's measurement using the same ends rule mine uses, so on walk data
Orientis provably a no-op. They then went and found the real cause -- walk polylines that double back WITHIN a single link, which an ends test is designed not to touch. 343 of 4,062 shipped Vanilla walk polylines carry at least one, seven of them in the Swamp of Sorrows, two on the road to the Stonard flight master. Part is a switchback the bot genuinely walked; part is their route-thinning dropping the rounding either side of a hairpin so it ships as a zero-width spike. Deliberately not smoothed here, at their request and for a good reason: a consumer smoothing the line cannot tell a real switchback from an artefact, and they can. They then fixed it at source: their route-thinning now re-thins the stretches either side of a sharp corner at a quarter tolerance, so the rounding that made a hairpin read as a curve survives. The largest out-and-back in the Vanilla data drops from 116 yards to 64, the two over 100 yards are gone, and the 50-100 yard band falls from 42 polylines to 5. Swamp of Sorrows goes from seven spiky polylines to five, worst case 49 yards down to 40. Not verified in game, and the remaining small spikes are expected to be real switchbacks the bots walked. - STILL OPEN, and it is the other half of the Undercity story. The HUD cannot yet tell a player
standing on top of a lift that the way down is the lift, because it cannot tell that they are on
top:
Steerdecides a same-continent ride is finished by horizontal distance to its far end, and a lift's two ends share an x and a y, so that test reads "arrived" the moment the leg begins. It cannot be fixed from position --UnitPosition's third return is a documented placeholder, always 0, so the client gives no player height at all (checked rather than assumed this time). Naming the lift in the walk before it is the half that could be done safely; knowing whether the player has ridden it needs watching a real one. - Three stale claims on the CurseForge page, found by looking for the shape ItemDB described
(thread fe6b6b05: their own page was still advertising an integration removed in the same
unreleased version, because the removal updated the developer-facing tables and missed the
player-facing prose). The same drift had happened here, in the same direction --
README.txtwas updated and the CurseForge prose was not:- "Not in yet: filtering by whether you have done a quest" -- it is in, and shipped in this
very version:
Index.STATUSEScarries both Completed and Not completed. Telling a player a feature is missing when it is there is the worst of the three, and the README's equivalent list never carried the claim. - The route line was still described as drawing on "the continent view and every zone map", which the Azeroth-map work earlier today made wrong. The release note was written; the feature line was not.
- "The quest's own words ... for every quest in the game" -- about two dozen still have no text anywhere to find, which the same page's own fixed-note says. Now it says it in both places.
- "Not in yet: filtering by whether you have done a quest" -- it is in, and shipped in this
very version:
- The suite now names a failure it could not previously attribute. Questbook's specs load the
INSTALLED Ace3, LibQuestDB and LibAceGUIWidgets from the sibling AddOns folders -- live working
trees, re-read from disk on every reset -- which is deliberate, so a real change upstream goes red
here rather than diverging from a stale vendored copy. The cost is that a sibling caught
half-saved is indistinguishable from a regression in this addon. A run today failed thirteen
specs and eight of them named nothing at all: they read an empty list through a
RowListthat had never attached, so they failed Questbook's own assertions about Questbook's own module. Two rounds went to the wrong sibling before the right one was found.- One assertion in the reset converts that into a named line.
RowListis attached by a separate file whose head bails silently when the core has not registered a library to attach to, so the library can be present and usable whileRowListis simply absent with nothing said anywhere. LibAceGUIWidgets' own session identified the shape, proposed the check, and confirmed the harness reloads all four of their files rather than the core alone, so it cannot fire for that reason. - The message deliberately does not decide the cause, and the first draft did. It blamed a
half-saved tree; they corrected it. A genuinely half-written Lua file is usually a syntax
error, which is loud --
loadfileraises and names their file. The quiet state this catches is the core failing to register, whose causes include a partial eviction, a load-order change or a wrong file list, with "truncated but still parses" the narrowest of them. Naming the rarest cause as though it were the reason is the same over-confidence the guard exists to stop, one level up.
- One assertion in the reset converts that into a named line.
- Tests. 791 green; 100% line coverage on Router (372/372); luacheck clean.
A Description on nearly every quest, and two routing faults the operator found in game (2026-09-22)
- Every quest that has any prose now shows a Description.
GetQuestTextfalls back to the turn-in text --offerReward, thenrequestItems-- when a quest has neitherdetailsnorobjectives, rendered in the Description section exactly as details are. QuestDB measured it over all 4,245 Vanilla quests: 566 showed nothing, 542 of those carry turn-in text, so the blank Description drops to 24. Operator, shown one quest reading "The full quest description is not available yet" beside one with a real description: "this is what i would expect to see on every quest", then "just show the text like this".- The old exclusion was reasoned wrongly, and the comment said so out loud. It called those fields "the quest's ending told in advance" -- a spoiler argument that the rest of the pane does not follow: Start names the giver and where he stands, What to do prints a 0.02% drop rate, Turn in to names the finisher before the player has found him, Chain says what it leads to. A line drawn in one place and nowhere else.
- The remaining 24 cannot be filled from any source -- repeatable turn-ins the dump has no
prose for, and Questie has none for them either (measured:
objectivesTextfor zero of the 566). Named in the adapter so nobody files them as a bug.
- A transport could never be chosen, at any distance.
BOAT_SPEEDwas 10 yd/s -- a fair guess at how fast a vessel looks from the dock, divided into the WORLD-MAP distance between docks, which is a different quantity. The result was not a mis-ranking but a domination: against a mounted 14 yd/s walk,TRANSPORT_WAIT + d/10beatsd/14at no distance whatsoever, so no route ever boarded a boat or zeppelin. Operator: "this route could have easily used the zepp to gromgol instead of tirisfal and the trip would be MUCH shorter". It is now the flight speed -- a reuse, not a new invention, since both are "board a vehicle and be carried along a fixed path"; the wait still separates them. Still not measured, and a boat is probably slower than a gryphon; the honest figure is one timed ride.- Two specs that asserted a mounted player FLIES were re-baselined to "board", because the flight lands 3,162 yards from the target and the 7-second margin they pinned was an artefact of the over-priced boat. Their observable is now the trip's seconds, which cannot be an artefact of which leg wins by a hair.
- The sawtooth spikes on a road are a polyline handed back in the wrong direction. A leg's points
must run from its start to its end, and nothing upstream promised that -- reversed, the line leaps
to the far end, walks the road backwards and leaps back, which draws as a tall narrow spike on an
otherwise correct path.
Router:Orientdecides by the ENDS, so a path that legitimately doubles back is untouched, and it is a no-op when the order is already right. - Tests. 781 green; 100% line coverage on Router; luacheck clean.
The route draws on the Azeroth map, where cross-continent travel is actually read (2026-09-22)
Operator, looking at the world map with a route from Tirisfal to Duskwood planned: "why don't our routes show on this level of the world map" ... "this is poor UX for cross continent travel". They were right, and it is the zoom where a long journey is understood.
- A map that spans continents has no world coordinates of its own.
MapRectderives a map's corners fromC_Map.GetWorldPosFromMapPos, which answers for a picture of ONE continent's yards and has nothing to say about Azeroth. So the rect was absent,OnMapreturned nil for every point, and the line silently did not exist at that zoom. The continent check would have rejected the far half anyway: a rect carries one continent id and that map shows two. - The client already knows where each continent sits on it.
C_Map.GetMapRectOnMap(child, top)answers the child's box in the parent's fractions -- declared in Era's own generated docs, and the same call HereBeDragons uses to place a zone on a continent whose instance differs. A point is now placed on its OWN continent's map and that square is mapped into the open one: two affine steps, no new data. - The continents are read from the map tree,
GetMapChildrenInfo(mapId, Enum.UIMapType.Continent), not from hand-written ids -- so this works on any map that contains continents and cannot go stale against the client. - Continents are tried BEFORE the map's own rect, deliberately: on a map that contains them, its own rect (if the client offers one) describes the whole canvas rather than either continent's yards, and would put every point in the wrong place. A continent or zone map has no continent children and falls through unchanged.
- A harness stand-in, and a contract.
GetMapRectOnMapis not modelled by WoWAPITesting -- they have the map tree and per-map world geometry, and this is the one edge between them. Requested asQBREQ-MAPRECTwithinstallMapRectsstaged inTests/env_questbook.luameanwhile, to be deleted and pinned when it lands. - Tests. 775 green; both continents placed into their own squares on one map, a continent that is not on the map refused rather than misplaced, a zero-area box ignored, and a client without the call degrading to no continents.
The Browse list stops reading the whole quest for six fields (2026-09-22)
GetQuestSummary was a bare lib:GetQuest(questId) -- its own doc comment said "only the fields the
LIST shows" and then read everything: givers, finishers, objectives, every chain list, the text row.
Index:Build calls it once per quest in the game, so that was ~4,160 full reads in the single frame
that opens the window, and it is what turned a slow GetQuest into the ten-second freeze rather than
a hitch. The same INDEX_KEYS / SUMMARY_KEYS split has existed on the Questie side since v0.0.1
and was never carried over.
GetQuestHeader(QuestDB MINOR 14) is used for the list -- the seven fields a row needs, read off the packed row without splitting the other twenty-two. QuestDB measured the full 4,257-quest walk at 0.013s against 0.078-0.101s forGetQuest.- ONLY where the library reads its own shipped rows, gated on
GetDataSource(). The header read ignores a registered source -- which is exactly what makes it cheap -- so with Questie answering it can differ from what the detail pane shows. QuestDB measuredrequiredRacesagreeing on 98.4% of quests, andrequiredRacesfeedsForPlayer, which drives the "For me" filter that is ON BY DEFAULT: roughly sixty-eight quests the list could hide while the pane says the character can take them, with no error and nothing to click. The fast path therefore lands on the player WITHOUT Questie -- the one the shipped data exists for -- and accuracy stays where a disagreement would have been invisible. specialFlagsis gone from the summary. Both providers wrote it to every row and no consumer anywhere read it, so it was a dead field carried four thousand times per window open. It is what made the twoGetQuestSummarypaths differ in shape, and the agreement spec caught it -- the fix for a field nobody reads is to stop writing it, not to widen QuestDB's read for it. It stays in the Questie adapter'sSUMMARY_KEYSat position 7, because the positions after it are what every latervalues[N]counts on.- Tests. 770 green; both sides of the gate pinned, including one that drives 400 real quests through both code paths and asserts they agree field for field, and one that asserts the header read is not called at all when Questie is the source.
A failed list build says so, instead of showing an empty window (2026-09-22)
Found in game by the operator: opening Questbook froze the client for ten seconds, then the Browse tab came up EMPTY, and tabbing away and back was the only way to fill it. One root cause, two symptoms, and the empty list is the half that is Questbook's.
Index:Buildassigns its rows only at the END of the pass (Index.lua:78). It walks every quest in the game in a single frame, and when the client's own "script ran too long" watchdog fired part-way through,Index.rowswas never assigned and the error unwound out of the tab'sdraw. The list was left showing nothing under an ordinary status -- indistinguishable from "this addon knows about no quests", which is the opposite of what had happened.RefreshBrowsenow catches it, drops the half-built index so the next open starts clean, and says the list could not be built.BUILD_FAILEDis deliberately a different sentence fromNO_DATA: "there are no quests" and "I could not finish reading them" look identical in an empty list and mean opposite things, and only one of them is worth reopening the window over. A spec makes the build raise and pins the sentence.- The freeze itself was QuestDB's, fixed in their working tree the same hour and measured rather
than reasoned: a cold
GetQuesthad gone to 29.3 seconds because a source-aware reverse-index build re-entered itself through the resolver, walking the whole dataset on every re-entry. After the fix it is 0.152s cold, and a full walk of all 4,257 quests is 0.473s warm. - KNOWN COST, not yet fixed:
GetQuestSummarystill reads the WHOLE quest for a list row that uses six fields, ~4,160 times on the click that opens the window -- so Questbook is what turned a slowGetQuestinto a frozen client rather than a hitch. That amplifier predates QuestDB's change and is Questbook's own: the sameINDEX_KEYS/SUMMARY_KEYSsplit already exists on the Questie side and was never carried over. QuestDB has been asked for a six-fieldGetQuestHeader.
Test harness pin 8db5998 -> c5b29a8: the reputations and skill lines are the harness's now (2026-09-22)
Contract c72cfa80 delivered. Both stand-ins this repo carried are DELETED, which is what the
harness's adoption entry asks of anyone who stood them in; Tests/env_questbook.lua now aliases
wow.factions and wow.skillLines exactly as it already did for worldMap, minimap and taxi.
- The two behaviours the stand-in had taken on faith came back the same way, which is the useful
part: a collapsed header really does hide its lines from
GetNumSkillLinesandGetSkillLineInfo, and index 0 really does mean every header. The harness settled both from the Era tree structurally --SkillFrame_UpdateSkillsdraws againstGetNumSkillLines()and never consults a parent header's state, so the list itself must shrink -- where this repo had only Questie's behaviour and the wiki. - Two of the stand-in's inventions did NOT survive, and are gone rather than reproduced: it
answered a fabricated
barMaxof 3000 and adescriptionbuilt by appending the word to the faction's name. The harness answers 0 and"", because nothing in the client tree states either. Nothing read them, which is why it went unnoticed -- a stub answering a plausible number is the shape of thing that only fails once something starts believing it. skillHeaderCallsstays ours, and is now a WRAPPER rather than an implementation. The harness deliberately does not record the calls: nothing settles whether the client firesSKILL_LINES_CHANGEDon them, so it models the sourced half and stays silent on the rest. Availability counts its own calls to decide how many events to swallow, so the recorder wraps the harness's globals and the behaviour stays theirs.- A spec must no longer rebind
env.skillLines-- the harness keeps one table identity across its reset, so an assignment would leave every reader looking at rows nobody writes to, and a spec setting up a profession would silently see a character with no skills.env.setSkillLinesreplaces the contents in place, and the five call sites inavailability_specuse it. - Tests. 766 green on the new pin, luacheck clean. The reputation and skill examples now drive the harness's real implementation rather than this repo's copy of it, which is the whole point of the contract.
Questbook stands on QuestDB alone: the version gate is graded, and a stale install says so (2026-09-22)
Operator, asked what should ship when the published QuestDB is older than this Questbook expects:
"we need to make it so we can release questbook without a questie dependency". The gate was
all-or-nothing -- below STRUCTURE_MINOR the QuestDB provider was not "less of a provider", it was
not one -- so on any older QuestDB the only thing holding the window up was Questie being installed.
That is a Questie dependency in everything but the TOC, and this removes it.
- The gate is graded.
STRUCTURE_MINORis 9, where quest structure begins;ENUMERATION_MINOR10 andTRAINER_MINOR11 gate the capabilities that need them, beside the calls that use them. A MINOR 9 or 10 QuestDB now fills the window with less function instead of leaving it empty. - What an older install cannot do is NAMED, not silently absent.
Limitations()is the one place those sentences are written, so a grade cannot gain a capability without gaining its sentence; the reduced list is printed once at login. - "Waiting for the quest database to finish loading..." no longer runs forever. That line is honest only while something is coming. When no database will ever answer, the window now names the addon and the version to install -- the difference between "broken" and "not set up yet". A spec pins both cases apart, because the old behaviour told a player with nothing installed that their addon was still loading.
- Two places grading could have broken rather than reduced, both closed.
TRAINER_TYPEis the MINOR 11 enum table and does not exist below it, so indexing it would have been an error, not a missing feature; and below MINOR 11 every object's faction reads nil, which the mailbox filter admits -- so that list carries both sides' boxes, whichLimitations()says out loud. - Questbook follows QuestDB's own data source now (its MINOR 12 prefers Questie's curated rows
when Questie is installed -- QuestDB's notice, carrying the operator: "use questie if it's
available, otherwise use questdb"). Questie is not ready until after login, so every index built
before that was built from the fallback;
RegisterSourceChangedis feature-detected and drops the quest index, the Places index, the cached views and the townsfolk lists when the source moves. Deliberately NOT the login path: that one also resumes the HUD, which mid-session would pull the arrow off whatever the player is being guided to. - QuestDB's MINOR 13 moves the SPAWNS to Questie as well, and Questbook takes that trade as it
stands -- no code change, recorded here because it is a decision rather than an absence of one.
The headline cost is real and QuestDB measured it in both directions: 990 NPC areas and 664 object
areas lost against 700 and 646 gained, with 6,615 of 7,620 shared NPC rows agreeing exactly. What
decided it was a different number, asked for because the first one could not settle it:
PlacesIndex.TownsfolkRowdrops a row outright whenBestSpawngives no x/y, andGetTownsfolkskips any NPC the data places nowhere, so an id losing its LAST area does not degrade -- it vanishes from the Places tab. QuestDB measured entities orphaned as zero on both sides (7,540 placed NPCs, 7,880 placed objects), because their resolver already falls back per id: an absent Questie row means the shipped table answers. Of five NPCs that lose their last usable map point, every one is an elite or boss inside an instance with no NPC flags -- not a vendor, trainer or repair NPC among them. The fallback this workspace was about to ask them to build turned out to be the contract already. - What grading buys, stated without pinning a version number: quest structure begins at LibQuestDB MINOR 9, so a QuestDB older than that cannot fill the window whatever Questbook does -- and the graded gate is what stops the NEXT version gap emptying it, by dropping a capability and naming it in chat instead. Questbook requires MINOR 9 as its floor and reads MINOR 10 and 11 for enumeration and trainers; a newer QuestDB than that is always fine. Deliberately not recorded here: which QuestDB version is on CurseForge on any given day. An earlier draft of this entry did exactly that and was stale within hours of being written, because it is somebody else's release state and nothing here can keep it true. What Questbook can state honestly is the MINOR it needs; what is published is QuestDB's to say.
- A defect in the above, found by this session's own audit before it shipped: the limitations
were STACKED, so a MINOR 9 install was told both grades' sentences and two of them contradicted
each other in the same chat line -- "the Places tab cannot list ... mailboxes" beside "mailboxes
are listed for both factions rather than yours", when at that grade no mailboxes are listed at
all.
Limitations()now reports the LOWEST unmet grade alone, because its list already describes the whole state and the higher one describes a state the install is not in. The MINOR 10 list is worded to cover every townsfolk kind rather than four of them, and a spec asserts the contradicting sentence is absent. - The same table keyed the grades as the literals 10 and 11 beside gates that used the named constants -- the same number spelled twice, where a renumbering would have gone quiet rather than loud: the sentences would simply stop being found and the player would be told nothing is missing. The keys are the constants now.
- Tests. 766 green; 100% line coverage on
Core.lua,QuestData.lua,QuestDBAdapter.luaandWindow.lua; luacheck clean. The grades are driven against the real library withMINORandTRAINER_TYPEmoved to match, not a stub.
Trainers and mailboxes stop guessing: the server's own columns, on LibQuestDB MINOR 11 (2026-09-22)
Asked of QuestDB the same morning the sever landed (contract b2c3b3e8, because a rule that guesses
should have an expiry rather than a comment) and answered within the hour as MINOR 11. The Places
tab's townsfolk now come from the server's own columns where they used to come from a display title.
- Class trainers are
trainerType/trainerClass, not "Hunter Trainer" read off a sub-name -- so a trainer the data titles anything else is no longer silently missing, and the comparison is against the class ID rather than a localised word. QuestDB measured the rule at 218 of Questie's 231 curated Era ids, with every straggler a warlock's demon trainer, which the one remaining title match takes back: a title rule for one category instead of nine. - A defect this session's own spec caught before it shipped: PET does not mean hunter. The dump types 32 Vanilla rows PET -- 15 pet trainers, 11 demon trainers, two Wintersaber trainers, a wolf, a boar and a bear trainer, four of them with no class at all -- and the first rule took every PET row for a hunter, so a hunter was shown the warlock trainers. The demon-trainer title is now decided first and belongs to warlocks alone (5749 Kayla Smithe is typed PET with class 3, so only the sub-name keeps her out), and a pet trainer must carry the class id. KNOWN COST: the two Wintersaber trainers are typed PET class 3, so a hunter sees them among their class trainers -- real trainers a hunter can use, and two rows of noise is a better trade than another title match.
- Profession trainers are
trainerTypePROFESSION, with the title words deciding only WHICH trade (the column says a row trains a profession, not which one). 258 of Questie's 259 Era ids. - Mailboxes are shown to the side that can use them (
friendlyToFactionon an object, the same shape the NPC read already had): a mailbox with no faction serves everyone, and an Alliance one no longer appears for a Horde character. This was a KNOWN COST of the sever a day old. - An NPC the data places nowhere is not a place to go -- QuestDB's own advice, and it is what keeps the dump's never-placed "World Warrior Trainer" templates out of a list of things to walk to.
STRUCTURE_MINORis 11, pinned with QuestDB's data as before: its library refuses an older data file at load, so an older QuestDB is not a provider at all. The release gate below is unchanged except in which MINOR it names. Superseded the same day -- the entry above grades the gate, so 11 becameTRAINER_MINORand the floor moved to 9. The reasoning here was wrong in one place worth keeping: the library's refusal is of a data file that does not match ITS OWN version, which says nothing about an older library-and-data pair, and those are consistent.- Tests. 755 green; 100% on QuestDBAdapter and PlacesIndex. The columns on real rows (CLASS is
zero, which is why every test is
~= nil), a hunter's pet trainers and no demon trainers, a warlock's demon trainers, a mage's class trainers all carrying class 8, the mailbox sides on QuestDB's own pinned rows (140908 no side, 142075 Alliance, 144112 "AH"), and every listed class trainer having somewhere to stand.
The riding-gear swap can run through Dibs' loadouts (2026-09-22)
Operator, 2026-09-21 after testing Questbook's own swap in game (directive #1522): "lets put this
on the todo list for an integration with dibs and it's paperdoll, we should make a riding gear
template there and swap between 2 save Dibs templates, i think that's a better implementation".
Dibs delivered its public loadout API on 2026-09-22 (contract 5012d277: Dibs.API, version 1 --
SaveEquipped, Equip, List, Slots), so the mechanism moves and the feature stays Questbook's.
- Pick a Dibs loadout as your riding set in the settings, under the riding-gear section: the dropdown lists your own Dibs loadouts, and "Questbook's own set" goes back to the set saved above it. The control is hidden entirely without Dibs. What Questbook still owns is the FEATURE -- the mounted poll, the setting, which two loadouts to swap between; what moved to Dibs is the moving of items.
- The second loadout is Questbook's own, written on every mount: what you were wearing, saved under a reserved name. Dibs' loadout names are account-wide and a clash is refused rather than taken over (their design), so the reserved name carries the character -- "Questbook: before mount (Name - Realm)". If Dibs ever refuses that save, nothing is swapped and it says so: better the riding gear stays off than goes on with no way back.
- Combat and death are Dibs' to wait out on this path -- its
Equipqueues itself onPLAYER_REGEN_ENABLED/PLAYER_UNGHOST/PLAYER_ALIVEand keeps only the last name asked for -- so Questbook neither waits nor re-checks, and says "in combat" once in its own words. A piece Dibs could not equip is named the same way as before. - Questbook's own path is untouched and is the fallback: without Dibs, with no loadout chosen, or when the chosen loadout is renamed or deleted in Dibs, the module moves the items itself as it has since v0.1.0's build. Nothing silently equips a different loadout.
- Tests. 754 green; 100% on RidingGear and Options. Seven examples against a stand-in for the
Dibs.APICONTRACT (Dibs' own suite proves Dibs): the two-loadout swap and the reserved name, the deferral, a missing piece, the refused save, the fallback when the loadout or Dibs goes away, a name Dibs does not have, an API absent/too old/half-built, and the settings dropdown.
The sever lands: the four gaps close on LibQuestDB MINOR 10, the parity spec is green, QuestDB is the default and Questie is optional (2026-09-21)
QuestDB answered contract cd6c8baf the same evening, as LibQuestDB MINOR 10 (in its working tree;
the operator holds its commit, and Questbook pins to the SHA when it exists). Every gap the
2026-09-21 gap analysis found is closed on Questbook's side, the parity spec the plan named as the
gate is green, and the switch the directive asked for (#1606, "then you need to open the contracts
with questdb and lets close the gaps") is made. KNOWN COST, stated for the release: the default
needs QuestDB published -- on an older QuestDB the provider is not usable and Questbook falls back to
Questie where it is installed, else says so and shows an empty window. Release this only after
QuestDB is on CurseForge at the MINOR STRUCTURE_MINOR names (11 after the entry above).
QuestData.DEFAULT_SOURCEis QuestDB. Questie is anOptionalDepsin the TOC and anoptional-dependenciesin.pkgmeta(loaded first when present, so/questbook source questiecan pick it); the QuestDB provider is built against MINOR 10 and refuses an older library as "not installed" -- pinned together with the data, since a MINOR 10 library refuses a MINOR 9Quests.luaat load. Core's no-database message names the MINOR it needs.- Gap 1, enumeration and involved-in.
GetQuestNameswalksGetNPCIds/GetObjectIds/GetQuestItemIdsand asksGetQuestsInvolving*-- a table read per id, a header decode only for the involved ones -- so an objective mob is searchable (598 Era NPCs are involved only as an objective, which Questie's reverse fields never gave), and an item ItemDB links is indexed by the name inside the link's brackets, never the link (whose|Hitem:every search for "item" would match). Townsfolk are Questbook's own rule overnpcFlags,subNameand the object type, Questie's Townsfolk.lua as the model: Repair and Auctioneer by flag; Banker and Battlemaster by flag and a title; Weapon Master a trainer titled so; this class's trainers the trainers titled "<Class> Trainer"; profession trainers by title words (PROFESSION_TITLES, read off QuestDB's own NPC titles); a hunter's stable masters by flag; mailboxes every object of type 19. The flag values are verified against QuestDB's rows (Auctioneer Fitch 4096, Rickle Goldgrubber 256, Marda Weller 16389, Thorgas Grimson 19). KNOWN COST against Questie: mailboxes are listed for both sides (QuestDB's object header carries no faction), and a class trainer titled otherwise is missed. - Gap 2, kill credits. QuestDB folds them into the NPC objective as
alsoCounts; such an objective becomes one kill-credit group in Questie's shape and is not also a plain kill. On Era everyalsoCountsis absent -- QuestDB measured the dump's KillCredit column empty on every Vanilla row -- and Questie's one hand-curated Era group (Pyrewood Ambush) is not copied (its repository declares no licence). KNOWN COST: that one quest names one villager where Questie names eight. - Gap 3, explore triggers.
GetAreaTriggerplaces each explore id as one point in the spawn shape; several ids merge by area into the onetriggerthe pane reads, worded by the quest'sendText("Scout through the Fargodeep Mine" -- QuestDB found the per-objective texts empty on every explore quest). The Fargodeep Mine (62) renders its event at one Elwynn point from QuestDB alone. - Gap 4, hidden quests.
GetQuestIdsleaves out QuestDB'sunobtainable(nothing starts it: 85 on Era) and LISTSseasonal(327: the holidays, the AQ war effort, the Scourge Invasion) with the reason on the summary -- a seasonal quest is real while its event runs and nothing offline says whether it is running, so it is listed as Questie lists an in-season one. KNOWN COST: an out-of-season quest is listed (and reads Ready) where Questie's blacklist hid it. - A Questie-provider defect found by QuestDB's reply: Questie's HIDE_ON_MAP marker is the
STRING
"HIDE_ON_MAP"(QuestieQuestBlacklist.lua:8-10); the Questie provider compared against the number 2, which never matched, so CLUCK! (3861) and 37 other real off-map quests were hidden with the dead ones from v0.0.1 on. Fixed and specced against the string. - The parity spec,
Tests/parity_spec.lua. One pane from each provider on a real quest, the lines diffed: Kobold Camp Cleanup and Cutting Teeth render alike -- header, standing, givers, objectives, turn-in, chain, rewards, description. The Questie side is Questie's REAL quest rows (corrections applied) through the fake, with the NPC facts the fake needs copied from QuestDB's rows, so it proves the two SHAPES come out as one pane; data parity is QuestDB's measurement. Two data rows differ and are pinned by name with QuestDB's answers: 7'snextQuestInChain(15 in Questie, none in the dump -- seven quests follow it, so QuestDB's rule names none) and 787 The New Horde as a second breadcrumb into 788 (the dump's own row; Questie blacklists it). - Player-facing text no longer says Questie where it meant the database: the help, the settings
panel's about line and warning description, the Status filter's tooltip, the pane's guide
tooltip, the HUD's no-place hint, the Places tab's waiting line, the window's waiting and empty
lines, the accept warning's one-of lines; README and the CurseForge description list QuestDB as
the quest information, Questie under Optional, and
/questbook sourceandtimingunder the commands. Older release notes are left as written. - A seasonal quest reads Seasonal, not Ready. A new
QuestStatus.SEASONAL, decided ahead of the doable rule for a quest whose summary carrieshidden = "seasonal": the column says Seasonal and the pane says "offered only while its event is running". Nothing offline -- neither QuestDB's data nor the client -- says whether an event is running, so Ready was a word the column could not stand behind; this is whathiddenwas carried for. Done and In log still win over it, and the Status filter's "Ready for me" leaves a seasonal quest out while "Not completed" keeps it. - The skill read's own events are swallowed. Whether the client fires
SKILL_LINES_CHANGEDonExpandSkillHeader/CollapseSkillHeaderis undocumented, so the answer is made not to matter:Availability:Skillscounts the header calls it makes andCore:OnSkillLinesChangedasksOwnsSkillEventfirst, swallowing exactly that many. A refresh from any other cause clears the count, so a real change is never swallowed on their account. - The townsfolk table is built once per session rather than per Places build: the NPC data and
the character cannot change while logged in, so the ~10,000-header pass survives
PlacesIndex: Invalidate(which runs on every ready and source change).PlacesIndex:Rowsreports it under/questbook timingas "Townsfolk rows". - A defect the suite had ratified, and the shape to watch for. The HIDE_ON_MAP bug above lived
since v0.0.1 because
questieadapter_spec.luaencoded the SAME wrong constant as the code -- a test written from the implementation rather than from Questie's source ratifies whatever the implementation assumes, and only a second data source disagreeing found it. A spec written from the code under test is worth reading twice. - Tests. 747 green; 100% on Core, QuestData, both adapters, Availability, QuestStatus, Index,
PlacesIndex, RidingGear. The four gaps each against QuestDB's real rows (the name index with an
ItemDB link, the hidden set's counts, quest 62's trigger, a kill-credit group driven onto Kobold
Camp Cleanup, the townsfolk rows named above, the Places tab built from them), the flip (QuestDB
by default with both installed, Questie by choice, the fallback with only Questie), the TOC and
.pkgmetarelations, the seasonal status, the swallowed skill events, the townsfolk cache, the three profession-name spellings pinned equal, andTests/provider_shape_parity_spec.lua-- named for what it proves (the two providers' SHAPES render alike), since a file calledparity_specreads as a claim about the two databases' data, which is QuestDB's measurement and not this suite's.
The sever's audit follow-ups: the QuestDB ready waits for the world, and one spelling each (2026-09-21)
The self-audit of the sever build (inbox ad5ad408) and Peer Review's reply on it (663f8e01)
named ten things; every one is built here, so nothing waits on the default flip except QuestDB's
four gaps. None of it reaches a player while DEFAULT_SOURCE is Questie, except the two
RidingGear lines and the /questbook timing command.
- The QuestDB provider's ready callback waits for
PLAYER_ENTERING_WORLD. It used to call back synchronously insideCore:OnInitialize-- QuestDB's data loads before this addon, so the ready handler ran atADDON_LOADED, before the client had filled the quest log or answered the completed set, and built the standings, the index and the HUD's resume from nothing. Questie's callback never showed this because its database compiles after login. The event carriesisInitialLogin/isReloadingUi(Era's SystemDocumentation.lua:111), so a/reloadfires it too; a callback registered after it runs at once, and a later entering (an instance) runs nothing.Core:OnQuestLogChangedalso resumes the guided-to quest ONCE, on the first log update, so whichever of the two the client fills the log before brings the HUD back -- and no later loot does.OnQuestieReadyisOnQuestDataReady: the seam has two providers. - The Status column names an in-log pre-quest.
QuestData:MissingPrerequisitesnow reads QuestDB'spreQuestActive(ANY one in the log or complete) between the single and group lists, in Availability's order, so a quest gated on an active pre-quest reads Pre-quest with a clickable name where it read "Not yet" while the pane said "A pre-quest must be in your log". The sign conventions are now written where the rule reads them: the Questie provider's group is signed, the QuestDB provider's is unsigned, andquestdb_provider_specasserts no QuestDB group entry is negative, so the sign branch is a documented dead one on that data rather than an accidental one. - One spelling of "unnamed item".
QuestDBAdapter:ItemNamereturns a third value,known, andGetQuestNamesreads it instead of matching the label's text;RidingGear.NamecallsItemNameinstead of spelling its own lower-case "item #". The label isUNKNOWN_ITEM, in one place, and changing it no longer changes what the search index files under "item". Player-visible: a riding piece the client has not cached reads "Item #2003" rather than "item #2003", and one ItemDB knows reads as its coloured link. - One empty townsfolk table.
QuestData.EmptyTownsfolk()is where its key set is written; the empty provider and both adapters start from it, and a spec pins each provider's keys to it, so a key added to one goes red in the others. - The skill read leaves the skill frame as it found it.
Availability:Skillsused to callExpandSkillHeader(0)on every cache rebuild, as Questie does, which pops a collapsed "Weapon Skills" open. Now nothing is touched while no header is collapsed (the common state); when one is, the headers are expanded, read, and exactly the ones found collapsed are collapsed again (CollapseSkillHeader, SkillFrame.xml:72), highest index first. Whether the client firesSKILL_LINES_CHANGEDon a collapse is not documented (the wiki names learning and unlearning only); if it does, a player who keeps a header collapsed pays a status re-stamp per filter pass -- on the in-game checklist. The stand-in models a collapsed header hiding its lines and records every expand/collapse call. - An honest verdict without the client's race or class id.
Availability:Ofanswers "cannot say" for a race- or class-masked quest whenPlayerBitshas no bit for it, where the filterForPlayerdefaults open. A filter may default open; a verdict a player reads may not. GetFactionInfoByID's tuple, sourced. Era's tree verifies the order only for the by-index form; the by-id order is the wiki's (API_GetFactionInfo documents both forms as one page with identical returns, read 2026-09-21), written at the call. The in-game/dumpstays on the list as the per-flavour confirmation./questbook timingreports the two passes nobody has measured in game: the status stamp of every quest (per filter run after a standing change) and the mob/object/item name index (once, on the first name search), throughdebugprofilestop(FrameScriptDocumentation.lua:445), the one clock that moves inside a frame. Off by default, per session. A stamp over one frame at 60 fps (16 ms) is the figure that would be a problem.- The two QuestDB-data specs say what a red means in their headers: the data is QuestDB's working tree at MINOR 9, so a failure naming a row and no Questbook change is their data having moved.
- Docs. The clause "the grep is in the file's header" is gone from the sever entry below; it pointed at a place that does not hold the thing.
- Tests. 740 green (from 733); 100% on Core, QuestData, both adapters, Availability, QuestStatus, Index, RidingGear. The QuestDB ready before and after the world; the HUD resuming the guided-to quest at the world, not at load, and once from the log; the in-log pre-quest in the column and the pane; the collapsed-header read and its restore; the nil-bits verdict; the townsfolk key set across three providers; no negative group entry in 4,245 quests; the timing command's arithmetic and its silence when off or without the clock.
Severing from Questie: a data-source seam, the QuestDB provider, and Questbook's own Ready rule (2026-09-21)
Operator, after QuestDB reported 90-100% parity on every area Questbook had asked it to measure
(bank #1590): "is there anything we need to keep questie for? or can we sever the connection?" ->
"then you need to open the contracts with questdb and lets close the gaps" (directive #1606).
The gap analysis (every Questie touch in Modules/QuestieAdapter.lua, the only file that reaches
Questie, set against LibQuestDB-1.0.lua's exported surface) found nothing irreplaceable: four
data gaps, filed to QuestDB as contract cd6c8baf (NPC/object/item id enumeration and "involved
in" links, kill-credit groups, event-trigger positions, the hidden-quest list), and one piece of
behaviour -- the Ready rule -- which is Questbook's to write either way. Not shipped in v0.1.0,
which stays on Questie; the default source flips when the four gaps land.
Modules/QuestData.lua, the seam. Every consumer that readns.QuestieAdapternow readsns.QuestData(AcceptWarning, ActiveView, DetailPane, FilterStrip, Index, PlacesIndex, QuestStatus, QuestView, Tracker, Window, Core). It owns one explicit provider interface (a list of delegated method names, so a provider missing one fails at the call), the rules no database owns (ForPlayer,FactionOf,EffectiveLevel,GetCompletedQuests-- the client first, the provider's copy second -- andMissingPrerequisitesover the provider'sGetChain), the shared constants (FACTION_*,TOWNSFOLK_KINDS,SCALING_LEVEL, the Era race masks), and the choice of provider: QuestDB when it carries quest structure and the character chose it, else Questie when installed, else an empty provider that answers nothing and never errors.DEFAULT_SOURCEis Questie until the QuestDB provider covers everything the Questie one does -- the operator plays on this working tree, and a half-built default would cost them the townsfolk, the mob-name search and the kill credits mid-build.- The provider-neutral shapes.
GetQuestnow hands back keyed tables --startedBy = { npcs, objects, items },finishedBy = { npcs, objects },objectives = { npcs, objects, items, reputation, killCredits, spells, trigger }-- andQuestViewreads those. The decoding of Questie's six-slot objectives and three-slot givers moved from QuestView into the Questie provider (objectivesFrom), with every proving line cited there. One reader for two databases. Modules/QuestieAdapter.luais now the Questie PROVIDER:SOURCE,IsInstalled(its loader global),RaceMasks(Questie'sraceKeys, else the seam's),GetChain(the three chain lists, signed group as Questie stores it),CompletedFallback(Questie'sdb.char.complete). Retired when the switch ships.Modules/QuestDBAdapter.luagains the QuestDB PROVIDER over LibQuestDB MINOR 9, every read built on a cited library accessor:GetQuestIds,GetQuestSummary,GetQuest,GetChain,GetNPC(+GetNPCSpawns, Questie's zone-keyed shape),GetObject,GetItem(drop sources fromGetItemDrops, the name from ItemDB -- QuestDB has no item table),GetQuestNames(one pass over every quest, givers, finishers AND objective mobs -- more than Questie's reverse fields gave),GetDropRate,GetUiMapId,GetInstanceEntrances(GetDungeon),GetDungeons,GetZoneName(GetQuestSortNamefor a sort;GetZoneAreaIdthen the client'sC_Map.GetAreaInfofor an area, which QuestDB keys by real AreaTable ids).IsReadyhere means quest structure loaded; the rewards sense isRewardsReady. Marked GAP where a read waits oncd6c8baf:killCreditsempty,triggernil, townsfolk empty, hidden quests not applied. Per-objective caption text is not attached (QuestDB'sobjectiveTextsdrops empty slots, so position is lost -- a follow-up on the same thread).Modules/Availability.lua, the Ready rule. Questie'sIsDoable(QuestieDB.lua:653-918) mirrored clause by clause in Questie's order, each cited: completed; race;preQuestSingle(ANY) and QuestDB'spreQuestActive(ANY in the log or done); class; reputation min/max throughGetFactionInfoByID'sbarValue(Era's tuple, ReputationFrame.lua:40), an undiscovered faction counted as 0 or -36000 for the eight that start below neutral; skill throughGetNumSkillLines/GetSkillLineInfo(SkillFrame.lua:26) by English skill-line name -- on another locale a skill-gated quest is "cannot say", never a wrong word;preQuestGroup(ALL, with the exclusive relief, only without a single list); next in chain done or held; an exclusive done or held; a breadcrumb whose target is taken; a breadcrumb to this quest in the log. Each verdict carries a KIND and Questie's brief wording, so the pane reads the same on either provider. The clauses Questie reads from its correction fields (parentQuest, requiredRanks, requiredSpell, specialization, achievements, disabledByQuest, dailies, availableUntilCompleted, availableStartingWith, invasion/arena) are named in the header as NOT MIRRORED -- none is in QuestDB's row. Client reads are cached with the status cache and dropped by it;UPDATE_FACTIONandSKILL_LINES_CHANGED(both registered by Era's own frames) now drop it too./questbook source questdb | questie | autochooses the database per character, saved, and rebuilds everything the old one filled;sourcealone reports. For comparing the two in game.- Tests.
env_questbookgainsquestdbData = true: QuestDB's real Vanilla Data files are run ONCE per process against a recorder and replayed into each fresh library, so the QuestDB provider is specced against the rows players get.Tests/questdb_provider_spec.lua(the seam's choice, the empty provider, Kobold Camp Cleanup / Kobold Vermin / Marshal McBride / Garrick's Head / the Deadmines entrance from QuestDB alone, the pane rendered from it) andTests/availability_spec.lua(every clause against Durotar's rows for the harness's Orc Hunter -- Cutting Teeth, Sting of the Scorpid, Your Place In The World, Call of Water, Horde Trauma, the Timbermaw quest -- and the clauses no row reaches through a crafted quest; the events; a full stamp of 4,245 quests). Stand-ins forGetFactionInfoByIDand the skill lines inenv_questbook(requested from the harness,c72cfa80). 733 green, 100% on Availability, QuestData, both adapters, QuestView, QuestStatus, Core.
Riding gear names items through C_Item.GetItemInfo (2026-09-21)
The language server flagged GetItemInfo as deprecated, and on Era it is: the bare global is a
deprecation fallback assigned from C_Item.GetItemInfo behind the loadDeprecationFallbacks CVar
(Blizzard_DeprecatedItemScript/Deprecated_ItemScript.lua:42 in the Era tree). RidingGear.Name now
asks the namespaced form first and the bare one only when it is absent; specced both ways. The
CursorCanGoInSlot warning beside it was the annotation library lacking the global, not the code
-- Era's own equipment manager calls it (Wrath/EquipmentManager.lua:111) -- and is silenced at the
one line with that reason.
"Not yet" says why, and the pre-quest is a click away (2026-09-21)
Operator, from the in-game run: "it shows not yet, but the breadcrumb worked, says unavailable pre quest. instead of saying not yet, can it not say something like prequest and then allow clicking on prequest to take you to that prequest?"
- The Status column words a not-yet quest by its cause: Pre-quest when a prerequisite is
not complete, Not for you when this character's class or race is shut out (no prerequisite
would fix that, so it wins), and Not yet only when Questie's other rules are the cause --
reputation, skill, an exclusive taken, the chain's next quest done.
QuestStatus:LabelFor, stamped byIndex:StampStatusbeside the standing itself. - The pane's standing line names the pre-quest and opens it. "Pre-quest: The Mindless Ones" in
place of Questie's generic "Unavailable: Incomplete pre-quest", the name coloured as a link and
the whole line a click to that quest (the same
questIdrow-click a chain line uses, so Back returns). Several missing: "(and 2 more)" for a group that must ALL be done, "(or 1 other)" when ANY one would do. Every other not-yet keeps Questie's own reason text. - The rule is Questie's, read once per not-yet quest and cached (
QuestieAdapter:MissingPrerequisites, oneQueryQuestfor the two lists -- neverIsDoableVerboseper row, which is the whole dozen- read rule again):preQuestSingleis ANY-of and, when present, the group list is not read at all (QuestieDB.lua:698-704, :768-773);preQuestGroupis ALL-of and SIGNED -- a negative entry is a plain requirement on-id, a positive one is also satisfied when any quest it isexclusiveTois complete (:601-632), which costs one more read per unmet entry. The answer lives inQuestStatus.reasons, dropped with the doable cache on accept, turn-in, abandon and Questie-ready. - Specs: the adapter's two rules with the signed and exclusive cases, the column word per cause, the pane's parts and counts, the cache, the row click, and the Browse list with "Only quests I can do" off. 698 green, 100% on the four touched modules.
Docs: routing needs one flight-master visit first (2026-09-21)
Operator, in game: "so it was a straight line because it didn't have FP data / once i have FP data, it works exactly right" -- and "there is no straight line bug" (directive #1526). The README, the setting's description and the CurseForge description all said "until you have opened a flight map on a character, every flight path is allowed", which is not what happens: the HUD draws a straight line to the target until a flight map has been read, and routes by road, boat and flight from the first read on. The three now say that, and to talk to any flight master once on a new character. Docs only; no code changed and none is to be.
Test harness pin e09fc9d -> 8db5998 (2026-09-21)
Tests only. The harness delivered the two stand-ins Questbook had requested the same day
(14eeea29, 5d16583a): GetUnitSpeed(unit) off wow.unitSpeed (four returns; run nil by
default so the "client cannot say" fallback runs), and the paper doll -- wow.equipped[invSlot]
behind GetInventoryItemID and PickupInventoryItem with the swap-into-origin rule,
CursorCanGoInSlot (wow.cursorCanGo), IsInventoryItemLocked (wow.lockedSlots), IsMounted
(wow.mounted), C_Container.GetContainerItemID. Adopted: installUnitSpeed and
installPaperDoll deleted from Tests/env_questbook.lua (only UnitIsDeadOrGhost, which the
harness does not model, stays as a one-line stand-in); ridinggear_spec and router_spec write
the harness's tables directly. 698 green on the new pin, no spec moved.
Test harness pin 6445dac -> e09fc9d (2026-09-21)
Tests only; nothing in the addon or its Questie dependency changed. The harness delivered the two
taxi globals Questbook had requested (NumTaxiNodes, TaxiNodeGetType on wow.taxi.types, plus
Enum.UIMapSystem verified from the Era tree) and, since the previous pin, the completed-quest set
(wow.completedQuests behind GetQuestsCompleted and C_QuestLog.IsQuestFlaggedCompleted).
Adopted: installTaxiMap deleted from Tests/env_questbook.lua; the fake Questie no longer
installs or removes GetQuestsCompleted and fills wow.completedQuests from a fixture's
completed (in game Questie's copy is that set); the offer dialog's done table is
wow.completedQuests itself. One spec moved: "neither source answers" now removes the client's
call by hand, since the harness carries it on every Classic flavour. 692 green on the new pin.
Riding gear, swapped on mount and back on dismount -- an option (2026-09-21)
Operator: "another twist, and item rack type thing to change out riding gear with the gear you were wearing and put it back on when you dismount, as an option..." (directive #1414).
Modules/RidingGear.lua, off by default and per character. Under the General settings: put the riding pieces on and press Save what I am wearing as my riding set (every worn slot is recorded; the panel lists the set by name), then Swap to my riding set when I mount. From then on mounting equips each saved piece that differs from what is worn, remembering what was there (an empty slot as empty); dismounting puts each remembered piece back and forgets. The remembered gear is in the saved variables, so a/reloadwhile mounted still restores it. Forget my riding set clears the set.- Blizzard's own equipment manager's sequence (Blizzard_FrameXML/Wrath/EquipmentManager.lua:102-123
in the Era tree):
ClearCursor,C_Container.PickupContainerItem(Era's bags, ContainerFrame_Shared.lua:1308),CursorHasItem,CursorCanGoInSlot,IsInventoryItemLocked(Vanilla/PaperDollFrame.lua:861),PickupInventoryItem(:804) -- the worn piece lands in the bag slot the riding piece came from. None carries a restriction flag in Era's generated docs. A piece not in the bags is left alone and said in chat; a slot the client refuses or has locked is left alone with nothing on the cursor. - Combat and death wait: armour cannot change in combat, so a swap that falls due then is
announced once and happens on
PLAYER_REGEN_ENABLED; dead or a ghost, the same wait. - Mounted is polled, not evented:
IsMounted()every half second on a hidden frame that is shown only while the setting is on (the installed Era ItemRack polls it the same way and notesUNIT_AURAis unreliable for it, ItemRackEvents.lua:401-417); a flight is not a mount (UnitOnTaxi).IsMountedhas no Blizzard call site on Era -- ItemRack, Guidelime and Leatrix Plus call it bare -- so it and every call above are feature-detected: a client without one never swaps and the setting does nothing. - The harness models bags and the cursor but not the paper doll or the mount;
Tests/env_questbook.luastands inGetInventoryItemID,PickupInventoryItem(with the client's swap-into-origin rule),CursorCanGoInSlot,IsInventoryItemLocked,IsMountedandC_Container.GetContainerItemID-- to request from WoWAPITesting.Tests/ridinggear_spec.lua(10 examples): the set saved, named and forgotten; the swap on and back with the bags checked either side; only the differing pieces and an empty slot restored empty; a missing piece; a refused and a locked slot; combat and death; a flight not a mount, the setting off, no set, a client without the calls; the half-second poll; the settings panel. 692 in all, 100% over RidingGear, Options and Core.
Not verified in game: that PickupInventoryItem on Era puts the displaced piece into the bag
slot the cursor item came from (Blizzard's equipment manager and ItemRack both rely on it -- read,
not run); the half-second poll against a real mount-up; and whether the dismount animation's timing
lets the swap land before the player moves off (ItemRack's approach, same timing).
The HUD aims at the nearest spawn, and the route is priced at your speed (2026-09-21)
The second Carbonite pass, ruled on (directive #1412). Operator, on the nearest spawn: "yeah, it
should be closest, then next closest etc etc"; on the route planner's speed: "perfect, use the
planned speed to calculate routes, thats good"; and the TomTom-shaped waypoint surface declined
-- "i don't want a tomtom surface, that's why we have the HUD" -- so no TomTom table is
installed and Questie's Ctrl+click waypoints stay Questie's.
- Nearest first.
QuestView:BestSpawnpicked the zone with the most spawn points and that list's FIRST point, so the HUD sent a player to an arbitrary one of a mob's forty spawns, and to the busier of an NPC's two zones whichever they stood in. Now every entity in a view carries its spawns,QuestView:NearestSpawnconverts them to world yards once (kept on the entity, and the view is cached for a log quest) and takes the closest on the player's continent, andActiveQuest.Pickchooses the nearest objective among the open counters (it was the first counter), the nearest drop source for a collect objective, and the nearest turn-in or giver among several (PickStart). The pane's text keeps the deterministic pick, so it reads the same every time; its[hud]lines and everything the HUD does take the nearest. Without a player position every choice is "first", as before. - Then the next nearest. A finished counter re-picks on the log update as before; and once the
player has moved
ActiveQuest.REPICK_YARDS(300) from where the pick was made -- or changed continent -- the pick is made again from there, on the Tracker's tick (ActiveQuest:Repick), without touching the HUD when the answer is the same place. A running route plan holds on the same objective rather than the same coordinates (RoutePlan.SameObjective), so a nearer spawn of the same mob re-aims the HUD without re-planning on every loot. - The route at your speed.
Router:Routetakes awalkSpeed(throughSearchandLinkCost), and every walk leg, the approach, the last walk and the straight-line alternative are priced at it; rides are unchanged.Tracker:TravelSpeedsupplies it: the client'sGetUnitSpeed("player")second return -- the run speed the player can move at, mount included, standing still or not (a bare global with no Blizzard call site on Era; installed Era addons read it that way, WeakAuras Prototypes.lua:10805 -- feature-detected, not verified on Era beyond that); else the last speed the HUD measured with the player moving on the ground (movingSpeed, gated on the raw sample so a stop's decay is not remembered; never a taxi's); else the run constant 7. On the fixture world the effect is visible: on foot the Horde boat wins, on a 100% mount the flight does, because the long road from the flight master is cheap. Requested from the harness:GetUnitSpeed(Tests/env_questbook.luastands it in, nil run speed by default). - Specs:
active_spec(the nearest open objective at its nearest spawn, the pure pick unchanged without a player, the re-pick after 300 yards and its three no-ops),router_spec(walk cost at a given speed with rides unchanged; the boat/flight flip at 14 yd/s, nonsense speeds, the straight walk; TravelSpeed's three sources, the stop's decay not remembered, the taxi ignored, the route planned at it);routeplan_spec's skipped-stop case now models a quest leaving the log, since a cached view no longer forgets one. 682 in all, 100% over QuestView, ActiveQuest, RoutePlan, ActiveView, DetailPane, Tracker, Router and Core.
Not verified in game: GetUnitSpeed's second return on Era (the two fallbacks cover its
absence); the feel of a re-pick at 300 yards on a mob with spawns on both sides of a road; and the
cost of NearestSpawn's one-time conversion for a forty-spawn mob (forty GetWorldPosFromMapPos
calls, once per cached view).
Released versions (v0.0.3 and earlier) are in CHANGELOG_ARCHIVE.md, and so are this release's
own sections dated 2026-09-21 -- moved there for size, not because they belong to an older
version. GitHub refuses a release body over 125,000 characters, this one reached 119,787 with every
earlier version already archived, and the oldest part of the release was the only thing left to
give.
This mod has no additional files

