Patch fix for the M10 use-at-object emitter. v2.0.0 shipped a wrong
GuideLime tag (
" steps
introduced in M10. GuideLime's parser does not recognise USE and
rejected those lines on guide load with errors like
Parsing guide "Durotar (+250 rep)": code [USE19851] not recognised
in line "6{G82.8,79.1 Ashenvale}Use ...
The correct GuideLime tag for a clickable use-item button is
[UI<itemId>], the built-in Use-Item tag — the same tag
community-maintained guides (e.g. Sage's Darkshore guide) emit
21× to render the in-game quick-use button. v2.0.1 swaps the
emitter to UI and the affected guides now load cleanly.
Routing, ranking, distance numbers, and tour shape are all
unchanged from v2.0.0.
Background — what v2.0.0 actually did vs. what it claimed
changelog/v2.0.0.md documents the M10 enrichment with literal
examples like Use [USE5619] at the Starbreeze Moonwell for [QC929] and a "Lua tag set extended with [USE<itemId>] —
supported by current GuideLime versions" compatibility note. Both
were wrong:
The emitted tag (USE) was never a GuideLime tag. The fix
was a one-character swap (USE → UI).
The 30 bundled guides that contained at least one use-at-object
step (~112 lines total, top-affected: Darnassus 15, Exodar 14,
Silvermoon 11, Thunder Bluff 10) failed to load on v2.0.0
because of these lines. v2.0.0.md is left as-is — it represents
what the v2.0.0 build was intended to do; v2.0.1 is what
actually works.
Read [USE<id>] everywhere in v2.0.0.md as [UI<id>] going
forward.
What changed in v2.0.1
Emitter: [USE<id>] → [UI<id>]
-[G 82.8,79.1 Ashenvale]Use [USE19851] at the Grom's Monument for [QC8150]
+[G 82.8,79.1 Ashenvale]Use [UI19851] at the Grom's Monument for [QC8150]
Single change in _tag_qc (guides_generator/output/emitter.py).
Verified against Sage's Sage_Guide_11-19_Darkshore.lua and
Flymolo's two TBC guides — [UI<itemId>] is the established
community convention.
Living docs updated to match the actual emission
docs/caveats.md (§ use-at-object linker example + the
pre-commit grep recipe)
docs/pipeline.md (the _tag_qc swap callout)
docs/experiments.md (M10 milestone description)
guides_generator/cmangos/fetch.py (slim-data docstring example)
These are living documents, not version-pinned releases, so they
now reflect what v2.0.1 actually emits. changelog/v2.0.0.md
itself is not modified — it is left as the historical record
of what v2.0.0 shipped (bug and all).
Compatibility
Lua bytes change only on use-at-object lines (~112 across the
30-addon corpus). Every other line is byte-identical to v2.0.0.
Routing / tour shape / rep-per-distance ratio identical to
v2.0.0 (rep/dist 14.94, distance 42959y, byte-identical
determinism).
No .toc change — DefaultState: disabled carries forward.
No CLI surface change. No cache schema change. Existing
cache/<expansion>/cmangos_*.json sidecars stay valid.
v2.0.0
Major version bump. The routing pipeline has been rewritten from
scratch; the addon API and .toc shape are unchanged but the Lua
output is materially different. v1.7.6 → v2.0.0 is a one-way
upgrade — no compatibility shim.
Headline
v1.7.6 v2.0.0 Δ
Global Rep/Dist 17.08 14.94 −2.14 honest-distance correction
Total Distance 37582y 42959y +5377y
X-Jumps 417 461 +44
Determinism random byte-identical across runs
The rep/dist drop is not a regression — v1 silently under-counted
two classes of player movement that the v2 pipeline now reflects
honestly:
Multi-source quest walks (Iverron's Antidote, Battle of Hillsbrad, the Manaforge shutdowns, …): v1 emitted one objective
stop at the densest spawn and dropped the rest, hiding ~32000y of
walking globally. v2 emits one stop per game objective.
Use-item-at-object walks (Crown-of-the-Earth Phial Moonwell
chain, Altar steps, Spell-Focus quests): v1 emitted neither the
Moonwell nor the item link, the player had to walk there blindly.
v2 surfaces the GameObject coord and a clickable [USE<item>]
tag.
The headline number is now the honest rep / map-unit ratio.
There is no fake distance left to compare against.
What changed for the player
Determinism
Same input → identical Lua output, every run. v1's randomized K=64
multistart + ILS produced different tours run-over-run depending on
parallel-pool scheduling. v2 enumerates a fixed (anchor × radius)
cross-product, refines deterministically, and breaks ties
lexicographically. Diffs across routing tweaks are now meaningful.
Multi-source quest honesty
A quest with K game objectives (e.g. "kill creature A in zone X
and loot item B in zone Y") now emits K separate [QC<id>,<n>]
stops, each at its own spawn cluster. The turn-in waits for every
cluster.
Use-item-at-object honesty
When the quest gives the player a Phial / ampoule / recipe scroll
and asks them to Use it at a specific Moonwell / Altar / Spell-Focus
GameObject, the guide now surfaces a step like
[G 63.4,58.1 Teldrassil]Use [USE5619] at the Starbreeze Moonwell for [QC929]
The [USE<id>] macro is GuideLime's existing item-link tag — the
player can click it for the in-game item tooltip / use shortcut. Data
for the link comes from the open-source cmangos TBC server DB,
auto-fetched on first build.
Cluster headers removed
The old per-cluster
[OC]At (x, y) in <zone>: 3x pickup, 1x turnin label is gone. It
restated the per-stop coords already shown on the lines below; the
cluster reads as a tight visual block now.
Cross-chain combine refusal
The Turn in [QT-X], pick up [QA-Y] shortcut is suppressed when
combining would imply a chain X → Y that does not exist (e.g.
Timberling Q919 + Q922 where Q922's actual prereq Q918 sat earlier
in the cluster). Canonical "turn in prereq, pick up next" combines
in real chains stay legal.
Quest requirements highlighted
(needs Cooking 20), (needs Honored), etc. now render in orange
with an uppercase NEEDS keyword:
Pick up [QA4161] (|cffff8800NEEDS Cooking 1|r)
so a missed prerequisite is hard to overlook. Mechanism is generic:
any future hard hint can call the same _highlight() helper.
What changed for developers
One ranking metric, end-to-end
rep / map-unit (Global Rep/Dist) is the only number that
matters. Everything else (Total Distance, X-Jumps,
Absorption Rate, soft-score raw values) is reported but
diagnostic. Workflow for any routing change is cp report /tmp/baseline.md → make change → diff (see
docs/caveats.md §13).
Hard rules gate the optimisation
R1 (precedence) + R2 (quest-action-order QA → QC → QT) + R3
(multi-source-honesty) live behind a single is_valid_tour checker.
Every refinement move calls it before the cost function — no invalid
tour ever reaches cost evaluation.
Soft scores actively drive refinement
coord_revisit and level_progression_misfit are two raw
soft-score values used by full_tour_cost. The soft-aware 2-opt /
or-opt / stop-level passes optimise the full cost (distance + jumps
weighted soft scores) directly, not just the multistart selection.
Three external data sources, one job each
Source
Role
Questie
Primary metadata: quests, NPCs, objects, items, plus community-curated corrections.
Nexus-WoW Items
Drop-chance index for (rare drop X%) annotations.
cmangos (new)
Spell-focus link Questie does not encode — the data behind Use [USE<item>] at the <Object> steps. Auto-fetched once per build.
All three are pulled on python3 create.py, cached under
cache/<expansion>/. First build downloads ~65 MB total
(cmangos zip dominant); subsequent builds reuse the cache.
Documentation reorganised
docs/pipeline.md walks the build phase by phase with mermaid
diagrams. docs/caveats.md collects the GuideLime / Lua / data-DB
quirks the pipeline guards against. docs/experiments.md keeps the
M1 → M10 milestone trail with KPI deltas.
Compatibility
.toc files unchanged from v1.7.6 (DefaultState: disabled,
faction-disabled-by-default).
Lua tag set extended with [USE<itemId>] — supported by current
GuideLime versions.
No CLI surface change. python3 create.py [--all | --faction X]
works as before.
Cache layout extended with cmangos_*.json sidecars + the
tbcmangos.sql raw DB. Wiping cache/<expansion>/ re-fetches
everything; the cmangos download takes ~30-60 s on first run.
dist/<expansion>/_quality_report.md schema unchanged (one extra
diagnostic line for the soft scores).
v1.7.6
Flip the per-faction DefaultState to disabled so a fresh install
no longer floods the GuideLime list with 30 faction guides at once.
Users now opt in to the factions they actually want to farm. The
CurseForge project page calls this out explicitly so it is not a
silent surprise.
Routing and Lua output unchanged — Lua bytes are byte-identical to
v1.7.5. Only the per-addon .toc files and dist/<expansion>/
artefacts change.
What changed
DefaultState: Enabled → DefaultState: disabled
## Dependencies: Guidelime
-## DefaultState: Enabled
+## DefaultState: disabled
## LoadOnDemand: 0
WoW only consults DefaultState on first install — once a user has
flipped a faction on or off in the AddOns screen, that choice is
saved in WTF/ and overrides the TOC default. So this affects
new installs only :
Before v1.7.6: a new install enabled all 30 factions, the
GuideLime guide list filled up immediately, and users had to
hunt-and-disable the 25+ factions they did not want.
From v1.7.6 onwards: a new install enables zero faction guides
by default. The user ticks the boxes for the factions they
intend to farm in WoW's AddOns screen.
CurseForge description rewritten around the new default
CURSEFORGE_DESCRIPTION.md now carries a prominent callout below
the project intro telling users that every faction ships disabled
and that they need to enable the ones they want. The "Why one
bundle?" and "After install" sections are reworded to match.
Compatibility
Lua bytes are byte-identical to v1.7.5.
Existing users keep their saved enable / disable preferences from
WTF/. The TOC default only applies when WoW first encounters an
addon folder it has never seen before.
No CLI surface change.
v1.7.5
Metadata polish + CurseForge summary helper. The TOC headers now
declare the license and a few previously-implicit defaults explicitly,
and the bundle run produces a one-line summary file ready to paste
into the CurseForge project's "Summary" form field.
Routing and Lua output unchanged — Lua bytes are byte-identical to
v1.7.4.
What changed
TOC headers gain four explicit directives
## Interface: 20504
## Title: Guidelime ThPi - <Faction> Rep Farm
## Notes: ...
## Author: ThPi
## Version: <version>
## Dependencies: Guidelime
+## DefaultState: Enabled
+## LoadOnDemand: 0
## X-Category: Quests
+## X-License: GPL-3.0-or-later
X-License: GPL-3.0-or-later — declarative license metadata so
CurseForge / WoWInterface / Wago tooling and addon managers can
surface the license without parsing the bundled LICENSE file.
DefaultState: Enabled and LoadOnDemand: 0 — explicit defaults,
matching the convention AtlasLootClassic uses. No behaviour change;
these were already the implicit defaults.
One-line CurseForge summary file
--all now writes dist/<expansion>/CURSEFORGE_SUMMARY.txt next to
the bundle zip and CURSEFORGE_DESCRIPTION.md. CurseForge's project
form requires a one-sentence summary distinct from the description;
this file is paste-ready for that field. Backed by two new functions:
addon.build_curseforge_summary(expansion) — returns the summary string.
addon.write_curseforge_summary(expansion) — writes it to disk.
addon package surface
+ build_curseforge_summary
+ write_curseforge_summary
Compatibility
Lua bytes are byte-identical to v1.7.4. Only the per-addon .toc
files and dist/<expansion>/ artefacts change.
Existing user enable / disable preferences are unaffected:
DefaultState: Enabled is the implicit default WoW already used,
and saved preferences in WTF/ override TOC defaults anyway.
No CLI surface change.
v1.7.4
Bundle-validator fix. CurseForge's upload validator rejected the
v1.7.3 bundle with "This ZIP contains files at the top level. WoW
addons must be packaged so that all files are inside a root folder."
The cause was the bundle-level README.md written to the zip root.
What changed
Bundle zip is now folders-only
zip_addon_bundle no longer accepts (or writes) a top-level
README.md. The umbrella zip now contains exactly the per-faction
addon folders and nothing else — the layout CurseForge requires for
multi-folder addons (the same convention AtlasLootClassic uses; their
.pkgmeta ignores README.md for the same reason).
Install instructions move to the project page
The wrapper-folder gotcha used to live in the in-zip README.md. It
moves into build_curseforge_description instead, so it appears on
the CurseForge project page where users see it before downloading
the zip:
"Recommended: CurseForge desktop app" — the app handles multi-folder
bundles natively.
"Manual install" — drill into the wrapper folder your unzipper
creates, copy the inner addon folders into Interface/AddOns/,
not the wrapper.
A visual tree comparison of correct vs wrong placement.
"After install" — enable only the factions you intend to farm.
addon package surface
- build_bundle_readme
build_bundle_readme is removed. Callers that need a readable
description of the bundle should use build_curseforge_description
instead — it's a strict superset.
zip_addon_bundle loses its bundle_readme parameter. The zip is now
always validator-clean by construction.
Compatibility
No routing or addon-content change. Lua bytes, .toc content, the
per-addon README.md, CHANGELOG.md and LICENSE are
byte-identical to v1.7.3.
The bundle zip on disk is smaller by exactly the bundle README; the
per-faction addon folders inside are unchanged.
Public API: build_bundle_readme is removed; existing code that
imported it should switch to build_curseforge_description.
v1.7.3
Distribution release. The 30 per-addon zips collapse into a single
umbrella bundle so the project ships as one CurseForge upload, and a
paste-ready CurseForge project description is now generated alongside
the zip. Routing and Lua output are unchanged — addon contents are
byte-identical to v1.7.2.
What changed
Single umbrella bundle on --all
--all no longer writes one zip per addon. Instead it writes one
archive at
dist/<expansion>/Guidelime_<AUTHOR>_RepGuides-<expansion>-v<version>.zip,
with every addon directory at the top level of the zip and a
bundle-level README.md at the zip root.
CurseForge moderation rejects multiple similar projects from the same
author under the
Fair Play rule ,
so a 30-faction set has to ship as one umbrella project. The bundle
follows the same multi-folder convention as WeakAuras , Bagnon
and Bartender4 : the CurseForge desktop app installs every
top-level folder as its own addon, and players manually unzipping into
Interface/AddOns/ get the same result by drilling into the wrapper
folder their unzipper creates and copying the inner addon folders.
New addon/curseforge.py module
Two new outputs are auto-generated:
build_bundle_readme(expansion, version) — the README.md that
lands at the zip root. Explains what the bundle is, walks through
install via the CurseForge app vs manual unzipping (with a visual
tree showing the wrapper-folder gotcha), and lists every bundled
faction with its in-game folder name grouped by faction group.
build_curseforge_description(expansion, version) /
write_curseforge_description — paste-ready markdown for the
CurseForge project page itself, dropped next to the bundle zip as
dist/<expansion>/CURSEFORGE_DESCRIPTION.md.
Both are regenerated on every --all, so the project-page text and the
bundle README stay in sync with the actual bundled factions.
Per-addon README gains a bundle notice
Each per-faction README.md now opens with a "Part of the rep-guides
bundle" section that points at the umbrella project. So if a player
copies a single addon folder out of the bundle and shares it
elsewhere, the readme still explains where it came from.
Pipeline integration
pipeline/bulk.py:run_all now collects each addon path during the
loop and, after the global quality report is written, calls
zip_addon_bundle(...) once with the full list. The per-addon
zip_addon function is gone — there is one bundle per run, not one
zip per faction. The run prints both bundle archive: <path> and
curseforge description: <path> at the end so the artefacts are easy
to find.
addon package surface
- zip_addon
+ zip_addon_bundle
+ bundle_zip_path
+ build_bundle_readme
+ build_curseforge_description
+ write_curseforge_description
Repo readme
readme.md is updated to document the new dist/ layout, the
addon/curseforge.py and addon/zipper.py modules, and the new
public-API surface. The "Pipeline" step list now describes the bundle
step instead of per-addon zipping.
Compatibility
Output : addon contents (.toc, .lua, README.md,
CHANGELOG.md, LICENSE) are byte-identical to v1.7.2. Only the
packaging changes — one bundle zip instead of 30, plus the new
bundle README and CurseForge description files in dist/.
CLI : no surface change. --all and --faction keep their
semantics. Only --all produces the bundle archive; --faction
still writes only the addon directory.
Public API : zip_addon is removed. Callers that want to bundle
programmatically should use zip_addon_bundle(addon_paths, expansion, version, bundle_readme=...) from guides_generator.addon.
v1.7.2
Routing release. The multi-anchor start search now widens the
candidate pool past the strict minimum level, and the per-sub-guide
candidate cap is raised from 4 to 6.
What changed
Wider anchor candidate pool
pick_start_candidates previously only considered quests at the
exact minimum level of the bucket. v1.7.2 expands the search to all
quests within LEVEL_TOLERANCE = 2 levels of the minimum, and bumps
MAX_START_CANDIDATES from 4 to 6.
Rationale: a starter zone has a handful of quests at the same
minimum level by definition, but a level+1 or level+2 quest is
sometimes a better spawn anchor — geographically outlying and
visiting it first amortises the trip across the whole cluster. The
strict-min-level filter blocked those candidates entirely; the
widened pool surfaces them as additional anchor options that the
existing "build one tour per candidate, keep cheapest" loop can
discover.
Effect on the test corpus
Global Rep/Dist: 16.90 → 17.08 (+1.1 %). No regressions —
every faction either improved or stayed exactly the same. Largest
faction-level gains:
Faction
v1.7.1
v1.7.2
Δ
Darnassus
12.94
13.38
+3.4 %
Exodar
10.15
10.40
+2.5 %
Lower City
24.30
24.86
+2.3 %
Stormwind
11.33
11.56
+2.0 %
Undercity
15.55
15.76
+1.3 %
Ravenholdt
17.65
17.83
+1.0 %
Silvermoon City
14.45
14.56
+0.8 %
Total normal distance: 37969 → 37582 (-1.0 %). X-jumps: 414 → 417
(+0.7 %, effectively unchanged).
Runtime cost
--all is ~10–12 minutes (unchanged from v1.7.1). The widened pool
adds a few extra multistart cycles to the candidate sub-guides, but
the MAX_START_CANDIDATES = 6 cap and the early-exit on identical
pickup coords keep the total within the same budget.
Tests that did not make it (documented in _experiments_history.md)
Stop-level Held-Karp cap raised 30 → 35 : +0.01 rep/dist
globally, not worth the runtime cost or the additional memory
footprint at the cap.
JUMP_PENALTY lowered 45 → 30 : looks like +7 % in the report
but is a metric artefact — distance shrinks because the solver
takes more cross-zone shortcuts (zero-cost in the report), but
cross-zone jumps rise +18 % and the real-world cost
(dist + 45 × jumps) is actually 1.3 % worse than baseline.
Dropped.
Files changed
guides_generator/routing/start.py — LEVEL_TOLERANCE = 2,
MAX_START_CANDIDATES = 6, candidate filter widened.
_experiments_history.md — adoption note + null-result entries
for the HK-cap-35 and JUMP_PENALTY tuning experiments.
readme.md — MAX_START_CANDIDATES constant table updated.
v1.7.1
Routing release. Natural-tier sub-guides now try every minimum-level
pickup as the spawn anchor and keep the cheapest tour, instead of
deterministically picking the lowest quest ID.
What changed
Multi-anchor start
routing/start.py gains pick_start_candidates(quests, max_k=4),
which returns up to four distinct pickup coords from the
minimum-level quests of a bucket. output/sub_guide.py:_pick_best_start
runs the full multistart-and-refine pipeline once per candidate and
keeps the cheapest tour under compute_tour_stats(tour, start_pos) —
the same metric the report shows.
Previously pick_start_position broke level ties by quest ID, which
typically lands on an interior NPC. The 96-iteration multistart
varies cluster anchors but not start_pos itself, so it could not
recover from a poor spawn pick.
Cleanup-tier sub-guides skip the search; they have no spawn anchor
by design.
Effect on the test corpus
Sub-guide
before
after
Δ
Stormwind / Stormwind City
18.91
21.63
+14.4 %
Stormwind / Dustwallow Marsh
17.20
20.32
+18.1 %
Darnassus / Loch Modan
0.63
0.92
+47.2 %
Stormwind / Westfall
16.80
16.80
0 %
Stormwind / Elwynn Forest
13.32
13.32
0 %
Most sub-guides have 1–2 candidates and land on the same tour, so
the gain is front-loaded into city + outdoor-hub buckets where there
is real spread between candidate spawn anchors.
Global Rep/Dist: 16.63 → 16.90 (+1.6 %).
Quest-sharing experiment (null result, no production change)
Surveyed every faction × sub-guide for QC-stop pairs with overlapping
obj_creatures and distinct objective centroids. Zero candidates: when
two quests share an objective creature, compute_objective_centroid
deterministically resolves them to the same coord, which the cluster
discovery already merges into a single zero-distance entry.
Quest-sharing is therefore already implicitly handled by the centroid
model. Documented in _experiments_history.md.
Runtime cost
--all now takes ~10–12 minutes (up from ~8) on a 12-thread host,
because the candidate sub-guides run the multistart pipeline k
times. Single-faction runs cost a few seconds extra at most.
Files changed
guides_generator/routing/start.py — pick_start_candidates,
MAX_START_CANDIDATES = 4.
guides_generator/output/sub_guide.py — _pick_best_start helper,
natural-tier dispatch.
_experiments_history.md — multi-anchor results and quest-sharing
null result.
readme.md — routing description plus the new
MAX_START_CANDIDATES constant.
v1.7.0
Routing release. The refinement chain now finds the provably optimal
stop ordering on every sub-guide up to 30 stops, and a long-standing
inconsistency between the routing cost and the report's rep/dist
metric is fixed.
What changed
Stop-level Held-Karp DP
routing/held_karp.py gains held_karp_stop_level_pass, the second
variant of Held-Karp's DP. Where the entry-level pass treats clusters
as atomic and is capped at MAX_ENTRIES = 12, the stop-level pass
flattens the tour and runs the same bitmask DP on the stop sequence
itself, with two changes that scale it past 12:
Sparse dict[(mask, last)] over reachable states instead of a
dense 2^N · N table. Most masks are unreachable under precedence,
so the actual state count is typically a few hundred to a few
thousand even at the cap.
Forward expansion : the DP builds the reachable set on the fly,
rather than allocating up front and visiting infeasible states.
MAX_STOPS = 30 is the production cap. Above that, the dict still
fits but runtime starts to dominate the rest of the chain.
refine_tour calls held_karp_stop_level_pass as the final pass,
after stop-level 2-opt and or-opt. When it fires, the resulting tour
is provably the cheapest stop ordering under the cost model. When it
does not fire (>30 stops) the tour is unchanged from the heuristic
chain.
compute_tour_stats start-position fix
output/sub_guide.py previously called compute_tour_stats(tour)
without passing start_pos. Inside compute_tour_stats, a missing
start position causes the loop to skip the first edge — the
spawn-to-first-stop hop never made it into intra_zone_distance, so
every reported rep/dist was inflated by the missing edge.
The fix is one line: pass start_pos to the stats call. This brings
the report into line with the cost the routing actually optimises
(_tour_cost(tour, start_pos)).
Effect on _quality_report.md: the global Rep/Dist number drops
from 16.78 to 16.63. The number is smaller because the previously
missing edges are now counted, not because the tours got worse — the
underlying tours are at least as short, and provably optimal where
the new HK pass fires.
Sub-guide impact
N ≤ 12 stops: usually unchanged (the entry-level Held-Karp already
fired and found the same answer, modulo cluster atomicity).
13 ≤ N ≤ 30: now provably optimal. Empirically a handful of
buckets tighten by 0.3–1.0 % rep/dist; most were already at the
optimum the heuristic chain found.
N > 30: unchanged.
Files changed
guides_generator/routing/held_karp.py — new pass, shared helpers.
guides_generator/routing/tour.py — wire stop-level HK into
refine_tour.
guides_generator/output/sub_guide.py — pass start_pos to
compute_tour_stats.
_experiments_history.md — post-v1.6.x section explaining the
number drop and the new pass.
readme.md — routing description and constants table updated.
v1.6.2
Tooling release. --all now produces ready-to-upload zip archives
alongside the addon directories, so a bulk regen is one step away from
publishing on CurseForge.
What changed
Auto-zip on --all
Each addon directory is bundled into dist/<expansion>/<addon>.zip
right after its QUALITY_REPORT.md is written. The zip contains the
addon directory at the top level, so it extracts straight into
Interface/AddOns/.
QUALITY_REPORT.md is excluded from the zip (maintainer telemetry,
not part of the distributed addon). LICENSE, README.md, CHANGELOG.md,
.toc and .lua are all included.
run_all prints the dist root at the end of the run.
Module additions
guides_generator/addon/zipper.py — new zip_addon(addon_dir, expansion) function. Pure stdlib (zipfile); no extra deps.
guides_generator.constants.DIST_DIR — ./dist, configurable
alongside ADDONS_DIR and CACHE_DIR.
Compatibility
No routing or output change. Lua bytes, .toc content, README,
CHANGELOG and LICENSE are byte-identical to v1.6.1.
--faction runs are unchanged (no zip step). The zipper is wired
into run_all only.
Generated zip directory is gitignored together with addons/ and
cache/ — they all regenerate from source on every run.
v1.6.1
Licensing release. The project (and every distributed addon) is now
explicitly licensed under GPL-3.0-or-later . Routing and output are
unchanged — Lua content is byte-identical to v1.6.0.
What changed
License: GPL-3.0-or-later
New LICENSE file at the repo root carries the full GPL-3.0 text.
The license is GPL-3.0-or-later so the generated sub-addons stay
license-compatible with GuideLime (the parent addon, GPL-2.0-or-later).
readme.md gains a License section explaining the choice.
LICENSE bundled in every addon
write_addon now copies LICENSE into each generated addon directory
alongside the .toc, .lua, README.md and CHANGELOG.md. Each
standalone CurseForge upload therefore ships GPL-§4-compliant on its
own — the recipient gets a copy of the license without needing to
visit the repo.
The per-addon README.md License block is now a short notice that
points at the bundled LICENSE file and at
https://www.gnu.org/licenses/gpl-3.0.html .
CurseForge project descriptions split out
The hand-tuned per-addon CurseForge descriptions used to live as
CURSEFORGE.md inside each addon directory. They have moved to
curseforge/<AddonName>.md at the repo root, so the addon directories
contain only the files that ship with the addon (the CurseForge prose
is upload-page material, not part of the addon zip).
Readme markdown fix
A small rendering bug in readme.md is fixed: the verb-led-step-body
example used unprotected <a> / <b> placeholders, which the
markdown parser tried to interpret as HTML anchor / bold tags and
swallowed everything that followed as link content. The example
strings are now wrapped in backticks (matching the rest of the
document), so GitHub renders the readme correctly past that point.
Compatibility
No routing change, no Lua-output change. Every faction's
Global Rep/Dist, distance, x-jumps and absorption rate are
byte-identical to v1.6.0.
Each addon's .toc ## Version field bumps to 1.6.1 and a
LICENSE file appears next to the existing files. Players who
overwrite their addon directory get the new license metadata
automatically.
No CLI surface change.
v1.6.0
Output-format release. Routing is unchanged — this release rewrites
how each step is rendered so the guide reads as a sequence of player
instructions instead of a quest-ID dump with chain bookkeeping.
What changed
Verb-led step bodies
Every step now starts with an action verb and points at a target.
GuideLime resolves the IDs to quest and NPC names at runtime, so the
italic *Name*: prefix that used to duplicate the resolved name is
gone.
Step type
v1.5.x
v1.6.0
Pickup
*The Balance of Nature*: [QA456] *Chain 1 1/7*
Pick up [QA456]
Kill objective
*The Balance of Nature*: [QC456][TAR2031][TAR1984] *Chain 1 1/7*
Kill [TAR2031], [TAR1984] for [QC456]
Loot/collect objective
*Webwood Venom*: [QC916] *Chain 5 1/9*
Loot for [QC916]
Generic objective
*Foo*: [QC123]
Complete [QC123]
Turn in
*Foo*: [QT456] (+250 rep)
Turn in [QT456] (+250 rep)
Combined turn-in + pickup
*Q1 -> Q2*: [QT456] (+250 rep)[QA457] *Chain 1 1->7/7*
Turn in [QT456] (+250 rep), pick up [QA457]
Annotations stay where they were useful — (repeatable),
(needs Honored), (needs Cooking 50) follow the pickup tag, and
[A Druid] race/class restrictions follow the closing bracket of
the tag they apply to.
In-game the player now reads e.g. "Kill Forest Strider, Webwood
Spider for The Balance of Nature" instead of "The Balance of
Nature: ▢ ▢ Forest Strider Webwood Spider Chain 1 1/7" .
No more chain bookkeeping
The inline *Chain N x/y* marker on every chain step and the
*Chain N a->b/total* marker on combined steps are gone — they
were technical noise the player couldn't act on.
The opening [OC]Quest chains in this zone: overview block (and
its Complex chains: twin in the cross-zone section) is gone too.
Sub-guide headers now go straight from [GA <side>] to the first
step.
Module cleanup
guides_generator/chains.py and guides_generator/output/chain_index.py
are deleted. find_chains, topo_sort, and
disambiguate_duplicate_names were only used to render the now-removed
chain index and per-step markers.
output/sub_guide.py no longer threads display_names and
quest_pos into the emitter — both arguments are gone from
GuideEmitter.emit_tour and the per-step builders.
Quality report intro trimmed
Per-addon QUALITY_REPORT.md no longer carries the "Auto-generated
by create.py --faction <name>. Pathing efficiency..." preamble
under the title. The same context lives in the repo readme; the
table that follows the title is self-explanatory.
Quality report
Routing is untouched — every faction's Global Rep/Dist, distance,
x-jumps, and absorption rate are byte-identical to v1.5.1. This
release only changes how steps are rendered.
The Lua diff per addon: every [QA<id>] / [QC<id>] / [QT<id>]
line is rewritten, the chain-index block at the top of each
sub-guide disappears, and the *Chain N x/y* suffix vanishes from
each step. Stop order and [G x,y zone] waypoints are unchanged.
Compatibility
Player-visible output changes for every faction. Players overwriting
their addon directory get the new readable format automatically.
No CLI surface change. Anyone who programmatically parsed the addon
Lua — looking for *Chain N markers, the *Name*: italic prefix,
or the [OC]Quest chains in this zone: block — will need to adapt;
those forms no longer appear.
v1.5.1
Performance release. The v1.5.0 multistart pipeline now runs every
sub-guide's K candidates in parallel across all available CPU cores,
cutting bulk runtime from ~7 minutes to ~2.5 minutes (2.7× speedup
on a 12-thread host) while improving the rep/dist metric slightly
through a small K bump.
What changed
routing.multistart spawns a multiprocessing.Pool with one
worker per logical CPU (os.cpu_count()). The K candidate builds
per sub-guide are now distributed via Pool.starmap instead of
running serially. The pool is created lazily on first use and
reused across the whole bulk run, so per-task pickling cost
amortises over many sub-guides.
K bumped from 64 to 96. Multiprocessing introduces a tiny
tie-breaking variance (workers can return same-cost candidates in
a different order than the serial sweep would have produced),
which on its own would cost about 0.01 rep/dist. Running K=96
instead of K=64 absorbs that variance — and lands the headline
metric slightly above the serial v1.5.0 ceiling. Extra
candidates run in parallel for free on a multi-core host, so
wallclock barely moves.
Quality report
Bulk run on the TBC corpus, vs the v1.5.0 serial baseline:
KPI
v1.5.0 (serial K=64)
v1.5.1 (parallel K=96)
Δ
Global Rep/Dist
16.78
16.79
+0.01
Total Distance (Normal)
38251
38236
-15
Total X-Jumps (Normal)
410
409
-1
Bulk runtime
~7m02s
~2m35s
2.7× faster
Single-faction runtimes (12-thread host):
Faction
v1.5.0
v1.5.1
Sporeggar (16 quests, 1 sub-guide)
~4s
~4s
Stormwind (280 quests, 22 sub-guides)
~95s
~36s
Tiny single-faction runs see no improvement — pool startup dominates
their wallclock. Bigger factions and --all benefit substantially.
What was tried and dropped
Adaptive K (smaller K for tiny sub-guides): tested with tiers
K=8/32/64/96 keyed on stop count. Reduced runtime modestly but
cost 0.03 rep/dist because the smaller-K sub-guides occasionally
missed the optimum the K=64 pipeline reliably found. Flat K=96
beats it on both quality and consistency.
Determinism
Output is deterministic given the same input — _derive_seed is
unchanged, sub-seeds for workers are pre-generated from the parent
RNG in fixed iteration order. Two --all runs on the same Questie
DB and codebase produce identical addon files.
Compatibility
No CLI surface change. No quest-data change. Player-visible Lua
output shifts slightly because the algorithm finds a marginally
different (and slightly cheaper) tour on a subset of sub-guides.
Players overwriting their addon directory get the new layout
automatically.
v1.5.0
Routing-pipeline release. The optimisations identified by the v1.4.0
experiment trail are now the production default, hard-wired with no
flag to disable them. Bulk runtime grows from ~17s to ~8min as a
result; in exchange every regenerated guide is +4-5% denser
(rep/dist) and -4% shorter on map distance.
What landed
Always-on multistart routing
Every route_subguide call now runs:
Greedy build with cluster discovery + on-the-way absorption.
Multistart — K=64 randomized rebuilds, cost-aligned
acceptance (distance + JUMP_PENALTY × jumps). Diversification
on three axes: random anchor, stochastic top-3 next-stop pick,
cluster-radius jitter on half the iterations. ILS finisher with
mixed segment-reverse + Lin-Kernighan-style double-bridge
perturbations.
Deep refinement on the winner — alternating 2-opt + or-opt
to convergence, 3-opt for tours with ≤50 entries, defrag pass,
Held-Karp DP for tours with ≤12 entries, stop-level 2-opt
finisher, stop-level or-opt finisher.
There is no --method flag. The fast greedy-only pipeline is gone.
Rationale: the rep/dist metric makes the multistart variant strictly
better, and we don't want to ship two paths.
Module additions
New under routing/:
multistart.py — orchestrator + stochastic-greedy build + ILS
perturbation kernels.
held_karp.py — exact DP for tours ≤12 entries.
three_opt.py — three-cut reconnection sweep.
stop_2opt.py — stop-level 2-opt with cost-aligned acceptance.
stop_or_opt.py — stop-level or-opt with cost-aligned acceptance.
routing/tour.py is the orchestrator. refine_tour is the deep
chain (run once on the multistart winner); refine_tour_fast is the
budget chain (run for each of K=64 candidates, drops 3-opt and the
stop-level finishers).
Helper
compare_reports.py is a small dev tool that diffs the snapshot KPIs
between two report files — handy when iterating on routing knobs.
Quality report
Bulk run on the TBC corpus, vs the v1.4.0 main baseline:
KPI
v1.4.0
v1.5.0
Δ
Global Rep/Dist
16.04
16.78
+0.74 (+4.6%)
Total Distance (Normal)
40022
38251
-1771 (-4.4%)
Total X-Jumps (Normal)
419
410
-9
--all runtime
~17 s
~8 min
~28x
Per-faction Lua output changes for every faction; the same quests
sit at different positions in the tour. The visible diff in the
addon: (+<rep> rep) titles unchanged, but the order of [G ...]
steps is rearranged for shorter walking.
What was tried and dropped
The full experiment table including the techniques that did not
make it in lives in _experiments_history.md. Highlights:
Random-insertion construction regresses (-1.6% distance) —
fragments same-coord stops the greedy build clusters for free.
Larger or-opt segments (k=5,6) regress slightly — more
candidates divert the alternating search into worse local optima.
Plain simulated annealing on top of converged 2-opt: zero
change on its own.
Cluster-defrag as a quality signal : cosmetic only under
rep/dist (still kept in the chain because tighter Lua emission
is nice; not because it improves the metric).
The corresponding feature branches (feat/random-insertion,
feat/larger-or-opt, feat/simulated-annealing,
feat/cluster-defrag, etc.) remain on GitHub for archaeology but
are no longer maintained.
Compatibility
Lua addon output changes for every faction. Players who already
have a v1.4.x addon installed need to copy the new addon directory
to overwrite the old one — same procedure as any release.
The CLI surface shrinks: --method and --iterations are gone.
Anyone who scripted around them needs to drop those flags.
v1.4.0
Quality metric overhaul. The composite 0–100 efficiency score is
gone. The headline metric is now plain rep per map unit walked
(rep / dist). Routing output is unchanged — same tours, same
clusters, same Lua. Only what we measure and display changes.
Why
The old score (50% rpd + 25% total rep + 15% absorption + 10% jump penalty) mixed two unrelated things: how good our routing was, and
how valuable the sub-guide is. A 1-quest cleanup with one fixed walk
got punished for having no clusters and little rep — even though the
routing was as good as it could possibly be. Big rep totals inflated
scores regardless of how well the route was actually walked. And the
0–100 framing implied a known optimum that does not exist for an
open-ended optimisation problem.
Plain rep/dist sidesteps both issues. There is no upper target — a
routing change is an improvement if rep/dist goes up, and that's it.
The number is meaningful for trend tracking across versions of the
same input. Total rep is shown separately (in the sub-guide title and
in input-data tables) where it is genuinely useful for the player's
"is this sub-guide worth doing?" decision.
What changed
output.score removed. No more compute_efficiency_score.
Sub-guide title drops (Eff. <score>, ...); now just
(+<rep> rep). The description block also no longer prints a
score line.
Per-addon QUALITY_REPORT.md snapshot leads with Rep/Dist,
not ø Score. Sub-guide table is sorted by Rep/Dist descending.
Score column is gone. Distance, x-jumps, absorption rate stay as
diagnostics.
Global _quality_report.md snapshot leads with
Global Rep/Dist. Faction comparison is sorted by N-Rep/Dist
descending. Top/Bottom-20 sub-guide lists rank by rep/dist.
Global ø Score is gone.
Documentation (readme.md, caveats.md) updated. Section 14's
historical "score deltas" table is annotated to clarify the numbers
predate this metric switch.
Quality report
Same per-addon and slim-global structure as v1.3.2. Numbers move:
KPI
v1.3.2
v1.4.0
Note
Global Rep/Dist
16.04
16.04
unchanged — same tours
Total Distance (Normal)
40022
40022
unchanged
Total X-Jumps (Normal)
419
419
unchanged
Absorption Rate (Normal)
67.5%
67.5%
unchanged
Total Rep (Normal)
—
641838
new line in snapshot
Routing output is bit-equal to v1.3.2. The diff is purely in how the
report is rendered and what the addon titles say.
Migration notes for branches
The seven feature-experiment branches (feat/multistart-routing,
feat/cluster-defrag, feat/three-opt, feat/stop-level-2opt,
feat/held-karp-small, feat/simulated-annealing,
feat/random-insertion) were measured against v1.3.x's composite
score. After v1.4.0 lands they need to be rebased and re-measured
under rep/dist before any merge decision. The directionality of each
result (win / null / loss) is expected to stand, but the magnitudes
do not translate directly.
The feat/combined-best branch is removed: its measurements are
obsolete under the new metric, and a fresh "best-of" combination
should be reassembled from the rebased components.
v1.3.2
Reporting refactor only — addon Lua output is unchanged. Pathing
metrics, sub-guide stats, and the score-formula are identical to
v1.3.1; this release just splits the single _quality_report.md so
single-faction iterations are easier to diff.
What changed
Per-addon QUALITY_REPORT.md
Each addon directory now ships its own QUALITY_REPORT.md next to
the .lua and CHANGELOG.md. It contains the full per-faction
detail that previously lived in section 3 of the bulk report:
glossary, faction-scoped snapshot, per-sub-guide table sorted by
score, input-data overview, and the lost-quest list (if any).
Both pipelines write it:
python3 create.py --faction sporeggar → updates only that addon's
report.
python3 create.py --all → writes one per addon (30 files for TBC).
This makes single-faction tuning loops self-contained: tweak a
constant, regenerate one addon, diff the addon's own report. No need
to wait for a full bulk run to see KPIs.
Slim global _quality_report.md
The root _quality_report.md is now a slim summary, dropping from
~370 lines to ~110. It keeps:
Snapshot — the headline KPIs (ø Score, Efficiency, Distance,
X-Jumps, Absorption, Lost Quests, Sub-Guides Total).
Faction Comparison — one row per faction with the columns that
move with code changes.
Top 20 / Bottom 20 Sub-Guides by Score — global optimisation
candidate list across all factions.
Per-faction detail (sub-guide tables, dropped-quest lists, glossary,
input-data) intentionally moved into each addon's own report — the
maintainer's diff against the root file stays focused on global
movement, not noise from any single faction.
report package API
report.write_quality_report(...) is replaced by two narrower
functions:
report.write_addon_report(stats, addon_dir, fname, fid, version, expansion)
— writes one <addon_dir>/QUALITY_REPORT.md and returns its path.
report.write_global_report(results, addons_root, version, expansion)
— writes the slim _quality_report.md at the repo root. No-ops
silently if results is empty (so single-faction runs that wire it
in for symmetry would not crash).
Internally sections.py is split into render_global_* and
render_addon_* functions; the orchestrator picks a subset for each
file flavour.
Quality report
No content change — KPIs are identical to v1.3.1: Score 71.8,
Efficiency 16.04, Distance 40022, X-Jumps 419, Absorption 67.5%.
This release only relocates and slims the rendering.
v1.3.1
UX patch: sub-guides are now ordered as a clean two-phase tour instead
of mixing on-tier and cleanup work by quest level. Routing output is
unchanged — same stops, same routes, same quality-report numbers; only
the order in which Guidelime.registerGuide(...) blocks appear in the
Lua file is different.
What changed
output/guide.py:_sort_buckets now splits the addon into:
Natural-tier phase — every (zone, natural) bucket, sorted by
the zone's level tier (city hubs and unmapped zones fall in by their
average quest level).
Cleanup phase — every (zone, cleanup) bucket, sorted by
average quest level.
Before, cleanup buckets were sorted by their average quest level
together with the natural-tier ones, so a high-level cleanup like
Darkshore (Cleanup) (level 31) ended up sitting between Hillsbrad
and Desolace instead of with the rest of the cleanup work.
Example — Darnassus addon, before:
[N 10-20 Darkshore] ... [N 20-30 Hillsbrad] ... [N 9-55 Darnassus]
[N 30-40 Desolace] [N 31-31 Darkshore (Cleanup)] [N 35-45 Feralas]
[N 40-50 The Hinterlands] ... [N 47-47 Thousand Needles (Cleanup)]
[N 48-55 Felwood] ...
After:
[N 10-20 Darkshore] ... [N 20-30 Hillsbrad] ... [N 30-40 Desolace]
[N 35-45 Feralas] [N 40-50 The Hinterlands] ... [N 48-55 Felwood]
[N 50-58 Burning Steppes] ... [N 62-65 Terokkar Forest]
[N 31-31 Darkshore (Cleanup)] [N 47-47 Thousand Needles (Cleanup)]
Quality report
No change. All five headline KPIs (Score 71.8, Efficiency 16.04,
Distance 40022, X-Jumps 419, Absorption 67.5%) are identical to
v1.3.0.
What was tried and rejected
Two further pathing ideas were tested against the v1.3.0 baseline and
did not survive — kept here for context, in case someone is tempted
to re-implement them.
Unlocking-power tiebreaker
Cluster discovery and nearest_feasible would tie-break by
successor count (number of in-sub-guide quests that have the
candidate as a direct predecessor). Idea: prefer the candidate that
unlocks more downstream work. Result on the corpus: Score 71.8 → 71.8
(no delta), Distance +0.5% (worse), X-Jumps -2.1% (better). Mixed
trade-off, no clear win.
Multi-restart routing
Build the tour from up to three anchor candidates per natural-tier
sub-guide (lowest-level pickup, highest-rep pickup, unanchored greedy)
and keep the cheapest. Result: Score 71.8 → 71.7, Distance -1.2%,
X-Jumps -1.2%, Runtime +50% (15s → 22s). Distance gain is real but
small; the score regresses by 0.1; the runtime hit is noticeable.
Verdict: not worth the cost.
v1.3.0
Three independent routing/perf wins, measured against the v1.2.0 quality
report (TBC, all 30 factions). The Lua output of every sub-guide now
takes a different shape — same quests, shorter route.
KPI
v1.2.0
v1.3.0
Δ
Global ø Score
71.0
71.8
+0.8
Global Efficiency (rep/u)
14.59
16.04
+9.9%
Total Distance (Normal)
43996
40022
-9.0%
X-Jumps (Normal)
416
419
+3
Absorption Rate
67.5%
67.5%
0
--all runtime
~80s
15s
-81%
Or-opt refinement (new)
A new routing/or_opt.py module implements or-opt : pick up a
contiguous segment of 1..4 TourEntries and re-insert it elsewhere. Where
2-opt reverses segments, or-opt relocates them — the two find
different classes of improvements, and alternating them is cheaper than
running either one harder.
Each candidate is evaluated by an incremental cost computation: an
or-opt move changes at most six boundary edges, so the delta is O(1)
instead of the O(n) a full _tour_cost recompute would cost. Feasibility
is only re-checked when the cost delta is already negative.
Segment-length sweep on the corpus picked k in (1, 2, 3, 4) as the
sweet spot — k=5 starts to regress because longer segments are too rigid
for precedence-bound chains.
Alternating 2-opt + or-opt with early exit
route_subguide now runs the two refinement passes in alternation
(MAX_REFINE_ROUNDS = 8) and bails as soon as one full round leaves
the tour cost unchanged. In practice most sub-guides converge in 2-4
rounds. This is what nets most of the -9% distance: each pass unlocks
moves the other cannot reach in a single iteration.
O(n²) → O(n) spawn clustering
coords/geometry.cluster_spawns was the dominant runtime cost — a
naive O(n²) single-link clustering sweep that called _close 1.7
billion times during a full bulk run (every objective with a large
mob-spawn list, e.g. wolves in Eastern Plaguelands).
Replaced with a spatial grid : cells of size threshold, each new
point checks only the 3×3 neighbourhood. Same single-link semantics
(earliest-created cluster wins ties), so the resulting cluster
membership is byte-identical to the previous output — the grid is
purely a search-space prune, not an algorithmic change.
This single fix turned an 80-second bulk run into 7 seconds of
clustering + ~6 seconds of routing. Without it the new alternating
refinement would have been impractical.
What was tried and rejected
Followup-aware absorption (idea: bigger detour budget if the
candidate's quest has a follow-up stop near the travel target): the
absorption corridor is too narrow under the current cluster radius
for this signal to help; aggressive bonus values regressed Score
by 1.0 because the algorithm absorbed long-detour stops and clusters
fragmented.
JUMP_PENALTY tuning (sweep 30/45/60/75/90): JP=30 won by +0.1
Score and -3.7% distance, but added +10% X-Jumps — semantically more
expensive for the player than the score reflects.
DETOUR_THRESHOLD tuning (4/6/8/10/12): identical KPIs across the
whole range. Cluster-discovery already absorbs anything within
cluster_radius; the absorption corridor is too narrow for tuning
this knob to matter.
Files
guides_generator/routing/or_opt.py — new
guides_generator/routing/tour.py — alternating refinement loop with
convergence-based early exit
guides_generator/coords/geometry.py — grid-based cluster_spawns
v1.2.0
Routing now anchors each natural-tier sub-guide tour at the pickup of the
lowest-level quest in the bucket . This is the racial tutorial spot in
starter zones — the natural place a player arrives — so the route biases
toward stops near the spawn instead of jumping straight to the densest
cluster anywhere in the zone.
How it works
pick_start_position(quests) returns the pickup coord of the quest with
the lowest level (tiebreak by quest ID). Passed as start_pos to
route_subguide, the value is used twice:
Initial cluster discovery centres on the spawn instead of the first
feasible stop in arbitrary input order.
2-opt cost function treats the spawn as the start of the tour, so
reverse-segment moves that bring early stops closer to the spawn are
preferred.
Cleanup buckets stay unanchored. They are off-tier returns — players
arrive with whatever level, drop in, do the leftover quests, leave — so
"where you arrive" is not a meaningful concept and forcing an anchor
distorted the route. Same for complex sections (cross-zone chains): no
single spawn point makes sense for a tour that already spans zones.
Effect on the quality report
Aggregate (TBC, all 30 factions):
KPI
v1.1.0
v1.2.0
Δ
Global ø Score
70.8
71.0
+0.2
Global Efficiency (rep/unit)
14.38
14.59
+1.5%
Total Distance (Normal)
44621
43996
-625 (-1.4%)
Total X-Jumps (Normal)
414
416
+2
Per-faction highlights (natural-tier improvements):
The Consortium : 961 → 847 dist (-12%), score 82 → 84
Undercity : 3051 → 2841 dist (-7%), score 64 → 65
Ironforge : dist -32, score 58 → 59
Cleanup-only sub-guides (e.g. Argent Dawn, score 91 , 43 dist) are
unchanged.
Files
routing/start.py — new module, contains pick_start_position.
routing/tour.py — route_subguide no longer falls back to a default
anchor; the caller decides explicitly.
output/sub_guide.py — passes start_pos only when bucket == 'natural'.
routing/__init__.py — re-exports pick_start_position.
v1.1.0
Internal refactor plus a few player-facing README polish-ups. Generated
Lua output is byte-identical to v1.0.0 — every quality-report number
stays the same. No routing, no scoring, no tag-emission change.
Per-addon README
The README.md written into each addon directory was restructured so
the most useful blocks come first:
New ## Support the project block sits directly under the intro
(was buried near the bottom under ## A note on the guide content).
Wording matches the project-level readme.md exactly.
New ## Addon info block right after Support — collects faction,
game version, addon version, author, and repository link in one
bullet list (previously these lines floated unstructured between
intro and Requirements).
The BUY_ME_A_COFFEE_URL constant is gone from constants/paths.py;
the URL is hardcoded in addon/readme.py (only one consumer, no
reason to abstract). The placeholder/"link coming soon" fallback is
removed too.
Module reorganisation
Eight monolithic top-level modules became nine domain-named subpackages.
Each subpackage owns one concern and re-exports its public API via
__init__.py, so existing call-sites like from .quests import ...
keep working.
was
is now
constants.py (253 lines)
constants/{paths, factions, races_classes, zones, dungeons, databases}.py
parsers.py + fetch.py
questie/{fetch, lua, spawns, quest_db, npc_db, object_db, item_db}.py
quests.py (429 lines)
quests/{builder, bridges, classify, decode}.py
coords.py (229 lines)
coords/{geometry, resolve, objectives}.py
routing.py (436 lines)
routing/{types, distance, feasibility, cluster, two_opt, tour, stats}.py
output.py (858 lines)
output/{sanitize, chain_index, tags, emitter, header, score, sub_guide, guide}.py
addon.py (226 lines)
addon/{names, expansions, toc, readme, changelog, writer}.py
cli.py (716 lines)
cli.py (~75 lines) + pipeline/{loader, single, bulk, console}.py + report/{aggregate, sections, writer}.py
zones.py, chains.py, prompts.py stay flat — they are small and
single-purpose already.
Why split
output.py and cli.py had each grown well past 700 lines and
mixed several unrelated concerns (text sanitisation + emitter state
sub-guide assembly + scoring; CLI parsing + pipeline orchestration
Markdown report rendering).
The quality report alone was 350+ lines living inside cli.py. It is
now its own report/ package with one module per concern (aggregate
pathing, render the seven Markdown sections, write the file).
Routing was a single 436-line file mixing data types, distance
helper, feasibility check, cluster discovery, on-the-way absorption,
2-opt, and tour-stats aggregation. Each is a separate module now.
Verification
The refactor was driven by a baseline _quality_report.md captured
before the first edit. After every of the five staged steps, the
generator was re-run with python3 create.py --all and the SHA-256
of the new report compared against the baseline. All five
intermediate runs and the final run produced an identical hash to
the baseline.
Repository hygiene
_quality_report.md is now tracked instead of ignored, so changes to
routing or scoring show up in PR diffs.
readme.md "Project layout" / "Configuration" sections rewritten
for the new package structure (paths into the relevant subpackage,
routing constants annotated with the file they live in).
v1.0.0
Initial public release. A generator that turns the Questie quest
database into one GuideLime sub-addon per reputation faction
(Guidelime_<Author>_<Faction>RepGuide). Covers the seven Classic
capital-city factions, the Classic neutral factions, and all TBC
factions — 30 addons in total at this release.
Generated guide structure
One sub-guide per zone, split into a natural tier (level fits
the zone) and a cleanup tier (higher-level returns).
A chain index at the top of every sub-guide lists the quest
chains contained in it.
A complex section at the end of a zone's sub-guide collects
cross-zone chains whose entry quest is picked up here.
Self-contained sub-trees of mixed-zone components are kept in
the normal section: only quests whose entire ancestor chain stays
in one zone are emitted there; the rest move to complex.
Solo cross-zone quests (different pickup vs. turnin zone, no
chain partners) move to the complex section so optional cross-zone
work is grouped at the end.
Routing
A single algorithm — greedy nearest-feasible with cluster discovery,
on-the-way absorption, and a 2-opt refinement pass — produces every
tour:
Cluster discovery : collect every feasible stop within the
per-zone cluster radius of the current position; each picked stop
becomes the new centre and the search repeats.
Travel : pick the nearest still-feasible stop further away.
On-the-way absorption : while travelling, absorb stops whose
detour stays under 6 map units. Stops already inside the
destination's cluster are deferred to the next cluster pass.
2-opt post-pass : reverse-segment swaps that reduce total cost
while preserving precedence. Cost is jump-aware:
intra_zone_distance + JUMP_PENALTY * cross_zone_jumps
(JUMP_PENALTY = 45 map units — one zone change costs as much as
45 walking units).
Precedence (pre, preg, next) is enforced centrally via a
predecessor map.
Cluster radius per zone
WoW map coordinates are normalised 0-100 per zone, but real zone
sizes vary hugely (Tanaris is massive, Stormwind tiny). Default
CLUSTER_RADIUS = 12.0, with per-zone overrides in
ZONE_CLUSTER_RADIUS for sparse zones (Searing Gorge 25, Tanaris 18,
Hinterlands 17, Western Plaguelands 19, Ironforge 15, ...).
Chain-bucket coalescing
Chains crossing the natural / cleanup boundary inside one zone are
moved into the predecessor's bucket as a final pass — otherwise
GuideLime hides the cleanup steps because the prerequisite lives in
a different sub-guide.
Output format
[OC]Quest chains in this zone:
[OC] Chain 1: A -> B -> C
[OC] Chain 2: X -> Y
*Honoring a Hero*: [QA8149]
*Balance of Nature*: [QC456][TAR2031][TAR1984]
[OC]At (34.8, 9.3) in Darnassus: 2x pickup, 1x turnin
*Un'Goro Soil*: [QA3764] *Chain 3 1/4*
*The New Frontier*: [QA6761]
*The New Frontier*: [QT6761] (+10 rep)
Sage-style tags : tag bodies carry only the ID
([QA8149] / [QT8149] / [QC8149]); the quest name is the
italic prefix in front of the tag, resolved by GuideLime from its
own DB at runtime.
Combined-range marker *Chain 3 2->3/4* when a turnin and the
next pickup of the same chain merge at one NPC.
Cluster header is only emitted when the cluster has more than
one stop (otherwise it would just repeat the location).
[A <race/class>] runtime race/class filtering for
faction-specific quests.
[TAR<id>] click-to-target macros for objective NPCs.
(Pt. N) disambiguation for chains with duplicate-named
members.
Annotations : (repeatable), (needs Honored),
(needs Cooking 50), etc., emitted next to the QA step.
Output sanitiser
_safe_text replaces multibyte-UTF-8 punctuation with ASCII
equivalents (× -> x, → -> ->, — -> --, smart quotes, NBSP)
and drops anything else outside ASCII except whitelisted German
umlauts (raw rendering bug in some GuideLime builds).
_safe_tag_content additionally maps [ to ( and ] to ),
so test/dev NPCs named [PH] Foo cannot close a TAR tag early.
Applied to every dynamic string that ends up in the Lua output: tag
bodies, the italic quest-name prefix, cluster headers, the chain
index, the file-header description.
Data sources
Quest, NPC, object, and item databases pulled from
Questie (auto-download +
cache).
Coverage : ~97% pickup, ~99% turnin, ~40% objective coordinates.
Item-drop bridges : bridge quests auto-started by an item drop
resolve via the source NPC; if that NPC is a dungeon boss with no
world coords, get_npc_coords falls back to the dungeon entrance
(DUNGEON_ENTRANCES maps ~30 Classic + TBC instances).
Sentinel-coord filter : instance NPCs without world coords are
skipped at spawn-extraction time.
Quality report
A maintainer-only _quality_report.md is written next to the addon
folder on every --all run. Sections:
Snapshot Headline — the seven KPIs that matter for diff
comparisons (global score, efficiency, distance, jumps,
absorption rate, lost quests, sub-guide count).
Faction comparison — diff-relevant columns per faction.
Per-faction detail — sub-guide table sorted by score.
Faction ranking by rep / distance.
Top 20 sub-guides by score.
Bottom 20 sub-guides (optimisation candidates).
Input data — static quest counts (sanity check).
compute_efficiency_score (in output.py) returns a 0-100
composite: 50% rep / distance (logarithmic), 25% total rep (sqrt),
15% absorption rate, 10% cross-zone-jump penalty. Visible in the
sub-guide title: (Eff. <score>, +<rep> rep).
The report sits in the repo root, not in addons/, so a player who
copies addons/* into Interface/AddOns/ does not pick it up by
accident.
CLI
python create.py — interactive faction + expansion selection.
python create.py --faction <id|name> — single faction.
python create.py --all — bulk run across all factions; writes
the quality report.
Versioning
The ## Version field of each .toc is derived from the
changelog/ directory (highest vX.Y.Z filename wins). Every
addon ships a CHANGELOG.md containing the concatenated changelog
entries.