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 bundledLICENSEfile.DefaultState: EnabledandLoadOnDemand: 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
.tocfiles anddist/<expansion>/artefacts change. - Existing user enable / disable preferences are unaffected:
DefaultState: Enabledis the implicit default WoW already used, and saved preferences inWTF/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,
.toccontent, the per-addonREADME.md,CHANGELOG.mdandLICENSEare 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_readmeis removed; existing code that imported it should switch tobuild_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)— theREADME.mdthat 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 asdist/<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 indist/. - CLI: no surface change.
--alland--factionkeep their semantics. Only--allproduces the bundle archive;--factionstill writes only the addon directory. - Public API:
zip_addonis removed. Callers that want to bundle programmatically should usezip_addon_bundle(addon_paths, expansion, version, bundle_readme=...)fromguides_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_PENALTYlowered 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_CANDIDATESconstant 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_starthelper, natural-tier dispatch._experiments_history.md— multi-anchor results and quest-sharing null result.readme.md— routing description plus the newMAX_START_CANDIDATESconstant.
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 dense2^N · Ntable. 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 intorefine_tour.guides_generator/output/sub_guide.py— passstart_postocompute_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— newzip_addon(addon_dir, expansion)function. Pure stdlib (zipfile); no extra deps.guides_generator.constants.DIST_DIR—./dist, configurable alongsideADDONS_DIRandCACHE_DIR.
Compatibility
- No routing or output change. Lua bytes,
.toccontent, README, CHANGELOG and LICENSE are byte-identical to v1.6.1. --factionruns are unchanged (no zip step). The zipper is wired intorun_allonly.- Generated zip directory is gitignored together with
addons/andcache/— 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
LICENSEfile 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.mdgains 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## Versionfield bumps to1.6.1and aLICENSEfile 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.pyandguides_generator/output/chain_index.pyare deleted.find_chains,topo_sort, anddisambiguate_duplicate_nameswere only used to render the now-removed chain index and per-step markers.output/sub_guide.pyno longer threadsdisplay_namesandquest_posinto the emitter — both arguments are gone fromGuideEmitter.emit_tourand 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.multistartspawns amultiprocessing.Poolwith one worker per logical CPU (os.cpu_count()). The K candidate builds per sub-guide are now distributed viaPool.starmapinstead 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.scoreremoved. No morecompute_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.mdsnapshot leads withRep/Dist, notø Score. Sub-guide table is sorted byRep/Distdescending. Score column is gone. Distance, x-jumps, absorption rate stay as diagnostics. - Global
_quality_report.mdsnapshot leads withGlobal Rep/Dist. Faction comparison is sorted byN-Rep/Distdescending. Top/Bottom-20 sub-guide lists rank by rep/dist.Global ø Scoreis 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.mdand returns its path.report.write_global_report(results, addons_root, version, expansion)— writes the slim_quality_report.mdat the repo root. No-ops silently ifresultsis 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— newguides_generator/routing/tour.py— alternating refinement loop with convergence-based early exitguides_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, containspick_start_position.routing/tour.py—route_subguideno longer falls back to a default anchor; the caller decides explicitly.output/sub_guide.py— passesstart_posonly whenbucket == 'natural'.routing/__init__.py— re-exportspick_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 projectblock sits directly under the intro (was buried near the bottom under## A note on the guide content). Wording matches the project-levelreadme.mdexactly. - New
## Addon infoblock 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_URLconstant is gone fromconstants/paths.py; the URL is hardcoded inaddon/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.pyandcli.pyhad 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 ownreport/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.mdis 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 = 45map 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_textreplaces 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_contentadditionally maps[to(and]to), so test/dev NPCs named[PH] Foocannot 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_coordsfalls back to the dungeon entrance (DUNGEON_ENTRANCESmaps ~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.