Forge internals

Contributor9 min readUpdated

In short

The Rust workspace, how forge build runs its stages and decides what to redo, the rules for the code, and its environment variables.

On this page
  1. What Forge is for
  2. The workspace
  3. The binary
  4. forge build: stages, keys, staging
  5. How a port was checked
  6. Building, testing, CI
  7. Rules for the work
  8. Environment variables
  9. Crates we build on

This page is for people changing Forge itself: how the Rust workspace is laid out, how forge build runs its stages and decides what to redo, how a port was checked against the JavaScript it replaced, and the rules the code follows. Polestar Forge is the user's entry point; the other pages explain each part: data, nav, model, routes, writer, judge.

What Forge is for#

Forge makes Polestar's leveling routes and writes them into guides, end to end, from the installed game client and the guide sources:

  1. read the game's data and build the reference data (data/forever),
  2. build each faction's world (quests, givers, camps, areas),
  3. price the walks between the places a route can visit,
  4. improve each start's route, starting from the shipped (v1) guides,
  5. write the improved route into the guides,
  6. judge the result and write a report.

It is one Rust workspace built twice from the same code: native (the build pipeline, all cores) and WebAssembly (the parts a browser tool needs, single-threaded). The addon itself stays Lua.

Two decisions shape everything else:

  • v2 is v1 improved.Many players use the shipped guides and like them; their reports are about missed quests, small things and back-and-forth. Each start's route starts from v1's own order and changes only by local edits that make it faster in the model while v1's deliberate orders and the guards still hold (routes).
  • The objective is the fastest route to 60, measured in the model's hours (model). The guards (take a hub's quests, finish an area once, pick up what you pass, never come back for work that was available, train on the way, grind little) keep the route the kind players want; the judge (judge) checks them on the written guides.

Routes are class-agnostic: each faction has race start routes for levels 1 to about 12, then a route from 12 to 60 through that faction's zones and the contested ones. Class quest chains are separate class guides; group content is not planned into the solo route.

The workspace#

rust/ is a Cargo workspace (rust/Cargo.toml, members crates/*, GPL-3.0-only, edition 2021, Rust 1.87 or newer). Each crate does one job; a crate never reaches into another's files.

CrateHoldsNative-only partsWebAssembly
forgethe forge binary: forge build (the stages, keys, staging root), forge data pack/fetch, forge doctor, and every other command as a subcommand (src/cli/)all of itNo
forge-coreids, positions and map frames, race/class masks, content keys, JavaScript-compatible number formatting and roundingnoneYes
forge-gamedatathe installed client: its storage, BLTE, the client's tables (typed by table definitions read at run time), finding the install, the config fileall of itbuilds, not used
forge-datathe data stage: reference data, the knowledge base, training, travel, trainers, professions, dungeons, codex, gear, the sim inputsall of it (built-in Lua, SQLite)No
forge-worlddata/forever loaders, prerequisites, the solo filter, availability, camps, areas, the quest atlas, the world builderfile readingYes
forge-navthe walking grid reader, legs, the CCH router, the walk store, Leffe's navigation maps reader, the hybrid backend, the world and walks stagesthe router, memory maps, threadsreads walk stores
forge-simthe time model, class profiles, the route search and edits, guards, the shared route, the skip and edit ledgersthreads, SQLitesingle-threaded
forge-guide.guide parsing, when, tokens, compile, lint, the pack emitterfile walkingparse and lint
forge-writera route written into the guides (apply-route)file I/OYes
forge-judgethe headless engine, honest playtest, passed-by, avoidable returns, guide-text metrics, seed tests, the reportSQLite, file I/OYes
forge-releasereleasing the addon: the release zips (a deterministic ZIP writer), the version, the changelog fragments, the release notes, the credits (forge release and changelog)file I/ONo
forge-wasmthe route planner in the browser: guide compile from a bundled copy of the data, the judge's walk with the simulator's levels, the judge summary (wasm-bindgen); the bundle binary builds its data bundle; www-build/build.sh builds the pagethe bundle binaryit is the browser build

The browser never runs the router (its bundles are hundreds of MB per continent): it reads walk stores.

Crate names and paths: rust/crates/forge-<name>, Rust paths forge_<name>::....

The binary#

forge has one entry point (crates/forge/src/main.rs). forge build and forge data pack|fetch live there; forge doctor and every other command is a module in crates/forge/src/cli/ with pub fn main(), reading its arguments through cli::args():

CommandModuleDoes
forge buildmain.rs, lib.rsthe whole pipeline, stage by stage
forge doctorcli/doctor.rswhat Forge finds: client, builds, quest data, data pack, Leffe's maps, grid
forge data ...cli/data.rsthe data builders one at a time (data)
forge gamedata ...cli/gamedata.rslist and export the client's tables
forge guide compile|lintcli/guide.rsthe guide compiler (writer)
forge guide seed|publish|check-generatedcli/guide.rs, generated.rsthe seed, the generated guides in the checkout, the check that they are not edited (Polestar Forge)
forge judge ...cli/judge.rsone guide pack, the report, seed tests (judge)
forge sim ...cli/sim.rsroute cuts, the shared route, guards, why, skips (routes)
forge walks ...cli/walks.rsthe world and walks stages on their own (nav)
forge writer apply-route, apply-routes, gate-diffcli/writer.rs, src/gate.rswrite one route into the guides, every start of a build's routes, the writer gate's comparison (writer)

Every command prints its usage with --help and exits 0. Developer-only tools stay separate binaries in the forge-devtools crate (travel-parity, navcch, landing_diff); build one with cargo build --release -p forge-devtools --bin <name>. sim-hash is forge-sim's own binary, so it also builds for WebAssembly.

forge build: stages, keys, staging#

forge build --out <dir> runs eight stages into --out, then writes the fixes ledger:

StageWritesMade by
gridgrid/ (the walking grid, from the game; skipped when --grid names one)forge-nav
datadata/forever/forge-data
preparethe grid's walk/: road masks, walk graphs, zone mixesforge-nav
worldworld/world-<faction>.jsonforge-nav (grid floors) + forge-world
walkswalks/walks-<faction>.bin, the walk storeforge-nav
routesroutes/<start>.json, .skips.json, .edits.jsonforge-sim (v1's chains by forge-judge)
guidesguides/<start>/forge-writer
reportreport.md, report.jsonforge-judge
(every run)fixes.md, fixes.json, the report's Fixes sectionforge (ledger.rs)
Read the rest of this section396 words

The build reads one checkout's fixes (--checkout): forever-db/corrections in the data stage (its rows of what each correction did go to .forge/corrections.json), data/route-fixes.json in the routes stage. With forge/seed/SEED.json in the checkout the input guides are staged in .forge/input-guides: guides/ with each generated guide replaced by its seed (generated.rs).

Keys. Each stage's key is the BLAKE3 hash of its version string (DATA_VERSION, WORLD_VERSION, ... in crates/forge/src/lib.rs), the bytes of its inputs and the keys of the stages it reads. The key is stored in <out>/.forge/<stage>.key; when the new key equals the stored one and the stage's outputs are in place, the stage is skipped. Large external trees (the walking grid, data/sources) are keyed by a manifest of their files' paths, sizes and modification times instead of their bytes. A key never hashes source text that does not change the output.

The routes stage has two keys. Its stage key also hashes the forge binary itself, so any code change reruns routes and everything after it (the stage writes and checks guides, forge_sim::flow). Its search has its own key (routes_search_key), which hashes the planner's code instead: build.rs hashes the sources (Cargo.toml, build.rs, src/) of forge-sim and the workspace crates it depends on into FORGE_PLANNER_HASH (src/planner_src.rs), beside the search's inputs and each start's v1 goal stream. The finished searches stay in <out>/.forge/routes-search/ under that key, so a writer or judge change reruns only the written check and the stages after it (How a route is made).

Bump a stage's version when your change alters what it writes for the same inputs; otherwise old outputs survive. Nothing else invalidates a stage but the routes stage's code keys above.

Staging root. Every stage reads a repository-shaped folder, <out>/.forge/root: its data/forever is the data stage's output; data/sources, data/local, tools, addon and forever-db (the corrections the seed tests read) link to the checkout's (or the data pack's); guides to the input guides. The sim stages also read <out>/.forge/sim (world-*.json, walks-*.bin, params.json). This is why a stage's library code can take "a checkout" and work the same inside a build.

Where the inputs come from. --root names the checkout (default: the nearest folder above the current one with data/sources). Without one, the build reads the public data pack (forge data fetch) and builds the grid from the game (or takes --grid). The client: --wow, $POLESTAR_WOW_DIR, the remembered folder, then a search (Polestar Forge).

How a port was checked#

Every stage replaced a JavaScript tool that already shipped routes or data, and landed only when it matched it:

  • Byte parity where the output is a file: the data builders, the guide compiler, travel, trainers, professions, dungeons, codex and gear write the same bytes as the JavaScript builder run fresh on the same inputs. The goldens under rust/tests/golden hold the JavaScript output and the tests rerun these comparisons (travel-parity against a live JavaScript travel build).
  • Event parity for the model: every event's level and time equal the JavaScript evaluator's on v1's chains (within the reference file's rounding).
  • Row parity for the knowledge base's SQLite file (typed rows compared table by table).
  • Native equals WebAssemblysim-hash runs the same replays under both and compares hashes.

JavaScript number semantics are kept where outputs depend on them (forge-core::jsnum: toFixed, Math.round, String(number), -0 printed as 0). Comparisons written as !(a < b) stay that way because they differ from a >= b when a value is NaN; the workspace's clippy settings allow these forms (rust/Cargo.toml, [workspace.lints.clippy]).

Building, testing, CI#

shell
cd rust
cargo build --release -p forge          # target/release/forge
cargo test --release --workspace        # parity tests skip with a reason when their inputs are missing
cargo fmt --all --check
cargo clippy --workspace --all-targets --release -- -D warnings
cargo check --target wasm32-unknown-unknown --no-default-features --lib \
  -p forge-core -p forge-guide -p forge-world -p forge-sim -p forge-nav -p forge-judge -p forge-writer -p forge-gamedata \
  -p forge-wasm
(cd crates/forge-wasm && wasm-pack build --release --target web --out-dir www/pkg --no-typescript)

The browser planner's page (crates/forge-wasm/www-build/build.sh <checkout> <sim dir> <local dir> name=<guides> ...) also bundles the JavaScript planner page from tools/planner (build.mjs, esbuild) and its static assets (export-assets.js); those steps stay JavaScript.

The forge workflow (.github/workflows/forge.yml) runs these on every change under rust/, checks every command's --help and every documentation block marked <!-- ci --> (rust/scripts/doc-commands.sh), builds the forge binary for Linux, Windows and macOS, and attaches them to a forge-v* release.

Parity tests that need the game client, a built grid or the JavaScript references skip with a note when those are missing, so CI passes without them; run them locally before changing a stage.

Large builds use a lot of memory (a walks stage, a full test run). On a shared machine cap them, for example systemd-run --user --scope -p MemoryMax=6G -p MemorySwapMax=0 cargo test ..., and use -j 4.

Rules for the work#

  • Every number in the docs and the report is measured on committed code.
  • Tests pin behavior (parity, rules, edge cases), not wording.
  • Nothing is called better than v1 without the judge's numbers and a player's read of the guides.
  • Every Forge result records the data it ran on, so a public-data build is never mistaken for an official one.
  • Dependencies must be GPL-3.0-compatible (MIT, Apache-2.0, BSD, zlib, CC0). Credits and licenses: rust/NOTICE.md (opens in a new tab) and the data pack's manifest.json.
  • Private material never enters the repository or the data pack; forge data pack refuses to build when it finds any (data).

Environment variables#

Every variable Forge reads. Command-line options win over them where both exist.

VariableRead byMeaningDefault
POLESTAR_WOW_DIRevery command that reads the gamethe game folder (the one holding .build.info)the remembered folder, then a search (Polestar Forge)
POLESTAR_FORGE_CONFIGevery command that reads the gamethe config file that remembers the game folderpolestar-forge/config.toml in your config folder
POLESTAR_GRIDforge build, forge doctor, forge nav prepare, forge sim, forge judge, forge writer, forge walksa built walking grid (--grid wins)forge build: builds one into <out>/grid; the other commands: data/local/nav4
POLESTAR_LEFFE_NAVforge build, forge doctor, forge walks walksLeffe's navigation maps, private (the folder with Nav.xml or Nav/Nav.xml; --leffe wins)none: without his maps the hybrid runs on the data pack's saved Leffe walks
POLESTAR_LEFFE_NAV_CACHE_MBforge walks walks with POLESTAR_NAV=leffethe cache for Leffe's decoded map pieces, in MB1,024 (forge build and the hybrid use 2,048)
POLESTAR_NAVforge walks walks onlythe walks backend: grid, leffe or hybrid (forge build takes --nav)hybrid when POLESTAR_LEFFE_NAV names the maps or his saved walks lie beside the store, else grid
POLESTAR_NAV_FPforge build (walks), forge data packthe walk model tag written into walks files and the pack's manifestforge-<walks version>
POLESTAR_DATA_PACKforge data fetch, forge build, forge doctorthe data pack folder (--dest / --pack win)polestar/data-pack in your data folder
POLESTAR_DATA_PACK_URLforge data fetchthe pack's index (--url wins)polestar-data-pack.json of the data-pack release
POLESTAR_KBforge build (routes), forge simthe knowledge base filedata/local/kb/forever-kb.sqlite
POLESTAR_ROUTESforge sim why, forge sim skipsa routes folder (--dir / --out win)none
POLESTAR_ROUTE_FIXESforge build (routes), forge sima route fixes file to use instead of the checkout's (routes)data/route-fixes.json in the checkout
POLESTAR_V1_SEEDSforge sim route, widen, shareda folder of saved seed chains (v1-<faction>-<race>-on.json) used instead of reading the guidesunset: the seed comes from the current guides
POLESTAR_DATA_TIMESforge data buildset: print the time of each stepunset
POLESTAR_TIMEforge guide compileset: print the time of each stepunset
POLESTAR_BUILD_TIMESforge build (routes, guides), forge writerset (not 0): a timing: <scope> <phase> <s> s line per phase (each start's search, post passes, written-check rounds; the writer's load, runs and shared guides; the free search's hours after 1, 2, 4, ... rounds), and <out>/.forge/timings-<stage>.json per stage builtunset
FOREVER_TDBforge data build --kbA server core world database (SQLite), local evidenceforever-tdb/world.sqlite under the local folder (data/local, or --local)
WOW_FOREVER_ADDONSforge data buildthe AddOns folder to look in first for Forever's quest datathe AddOns folders of the installs Forge finds
SOURCE_DATE_EPOCHforge data buildthe build time the data builders record (reproducible builds)now

Crates we build on#

NeedCrate
threadsrayon
content keysblake3
the client's tables in Lua addonsmlua (Lua 5.1, built in)
SQLite (the knowledge base)rusqlite (bundled)
the routercch (our contraction hierarchy)
memory-mapped filesmemmap2
compressionflate2, miniz_oxide
the data packtar, ureq
deterministic maths across targetslibm
benchmarks, property testscriterion, proptest

Adopt a crate where the current change needs it; don't rewrite finished, parity-tested code to fit one.

Was this page helpful?

Start with, or act

  • PlayersInstall and first run
  • PlayersCommunity guides
  • PlayersFAQ
  • Guide authorsWriting a community guide
  • Guide authorsGuide format cheatsheet
  • ContributorsContributing
  • ActionCopy the AddOns folder (Windows)C:\Program Files (x86)\World of Warcraft\_classic_beta_\Interface\AddOns
  • ActionCopy the AddOns folder (macOS)/Applications/World of Warcraft/_classic_beta_/Interface/AddOns
  • ActionFind an optionEvery setting with its default; type its name here to jump to it.
  • ActionEvery slash command