Guidelime Reputation Farm Guides - by ThPi

A bundle of reputation farming guides for WoW Classic / TBC Anniversary, delivered as GuideLime sub-addons. Every faction is its own folder inside the download — install the archive once, then enable only the factions you actually farm.

File Details

Guidelime Reputation Farm Guides - by ThPi

  • R
  • May 4, 2026
  • 1.14 MB
  • 5
  • 2.5.5+4
  • Classic TBC

File Name

Guidelime_ThPi_RepGuides-tbc-v1.7.5.zip

Supported Versions

  • 2.5.5
  • 2.5.4
  • 2.5.3
  • 2.5.2
  • 2.5.1

Changelog

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.pyLEVEL_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.mdMAX_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.pypick_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&gt;.

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/&lt;AddonName&gt;.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 &lt;a&gt; / &lt;b&gt; 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 -&gt; Q2*: [QT456] (+250 rep)[QA457] *Chain 1 1-&gt;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-&gt;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 &lt;side&gt;] 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 &lt;name&gt;. 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&lt;id&gt;] / [QC&lt;id&gt;] / [QT&lt;id&gt;] 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:

  1. Greedy build with cluster discovery + on-the-way absorption.
  2. 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.
  3. 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: (+&lt;rep&gt; 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. &lt;score&gt;, ...); now just (+&lt;rep&gt; 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 &lt;addon_dir&gt;/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:

  1. 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).
  2. 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.pyroute_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_&lt;Author&gt;_&lt;Faction&gt;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:

  1. 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.
  2. Travel: pick the nearest still-feasible stop further away.
  3. 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.
  4. 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 -&gt; B -&gt; C
[OC]  Chain 2: X -&gt; 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-&gt;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 &lt;race/class&gt;] runtime race/class filtering for faction-specific quests.
  • [TAR&lt;id&gt;] 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 (× -&gt; x, → -&gt; -&gt;, — -&gt; --, 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:

  1. Snapshot Headline — the seven KPIs that matter for diff comparisons (global score, efficiency, distance, jumps, absorption rate, lost quests, sub-guide count).
  2. Faction comparison — diff-relevant columns per faction.
  3. Per-faction detail — sub-guide table sorted by score.
  4. Faction ranking by rep / distance.
  5. Top 20 sub-guides by score.
  6. Bottom 20 sub-guides (optimisation candidates).
  7. 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. &lt;score&gt;, +&lt;rep&gt; 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 &lt;id|name&gt; — 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.