Toolchain

Contributor38 min readUpdated

In short

Node ≥ 24, zero npm dependencies

On this page
  1. Packs (pack mode)
  2. Zone art overlays (maps overlays)
  3. Codex (codex build)
  4. Reference data (data/forever/, built by data build, gitignored)
  5. Sources and precedence
  6. Recordings (data/recordings/<session>.json)
  7. Measuring routes
  8. Route planner (planner)
  9. Navigation grid (nav build)

node tools/polestar.js <command> [args]. Each command is a module tools/commands/<name>.js exporting:

js
module.exports = { name, summary, usage, async run(argv) { return 0 /* exit code */ } }

The dispatcher discovers them; adding a command never edits a shared file. Shared helpers: tools/lib/paths.js (the repository's folders), tools/lib/cli.js (argument parsing, diagnostics), tools/lib/game/ (XP rules, races and classes), tools/lib/lua.js (run Lua 5.1, convert Lua data to JSON), tools/lib/tga.js (anti-aliased TGA textures).

Every command, every generated file and the rebuild order: tools/README.md.

Packs (pack mode)#

The guide commands work on a pack: the folder given as their one argument or in --pack <dir> (one of the two; it must hold pack.json), else the nearest folder holding a pack.json (this one or a parent), else our guides/ (tools/lib/pack/locate.js). tools/lib/pack/manifest.js reads and checks pack.json; tools/lib/pack/format.js holds the contract constants (FORMAT, NEEDS, the id rule, reserved ids, the generated-folder marker); tools/lib/pack/build.js builds a pack's files; tools/lib/dsl/project.js loads a pack with our guides as context (their next targets and chains seen, their own diagnostics not reported).

What compile writes into a pack's folder: Core, "Guide data (compiled Lua)".

Read the rest of this section312 words
CommandIn a pack
new [dir]writes a pack from tools/templates/pack/ (the CI workflow and devcontainer only when the clone has an origin)
lint, compilechecks with our guides as context; writes <pack>/dist/<folder>/; --format text|json|github
export <file.guide>lints the file's pack (the nearest pack.json above it) and prints its one guide as an import string (tools/lib/pack/importstring.js; PACK_FORMAT.md, "Import strings"); refuses a file with errors, of several guides, of Polestar's own, over 60 steps or 50,000 characters. play and dev --play give the same string (POST /api/import, the page's Copy import string)
doctorNode, Lua, reference data, AddOns folder, pack.json
findquests, NPCs, objects, items, inns, flight points and our guides as guide lines (--json); --kind inn|flight without a text lists every inn or flight point (from travel.json)
playtestevery guide to the end per class (tools/lib/playtest.js, also behind tests/guides.test.js); a guide starts with the prerequisites of its accepts that the guides chaining into it turn in already done, and those they leave for it only to hand in in the log, as for a player arriving on the route
play, devthe playground with the pack built into a temp folder; dev watches and builds into AddOns
install, packbuilds and installs the pack; writes <pack>/dist/<folder>-<version>.zip
record draft, sim comparedrafts into <pack>/drafts/ under <id>/draft/; scores the pack's chain beside ours

lint, compile, dev, play, install and pack build a pack through buildPack (tools/lib/pack/build.js). When the data folder is missing or holds none of the core files, it reads no guide and returns one error (noReferenceData) with noData set; the command prints that error and exits 1, and dev checks this once at start instead of watching. --no-data (lint, compile, dev, and install and pack on a pack) builds with empty data instead and cannot be combined with --data, --context-data or --sim.

The folder rule (PACK_FORMAT.md, "TOC fields") is packDirRefusal in tools/lib/dsl/project.js. Lint's pack rules mirror the runtime's (tests/packs-parity.test.js); our house rules (elites in Leveling guides) are warnings in other packs.

Zone art overlays (maps overlays)#

node tools/polestar.js maps overlays [--build <ver>] [--offline] writes addon/Polestar/UI/ZoneArtOverlays.lua (tools/lib/maps/overlays.js) from the client's DB2 tables, as the game-table export CSV cached under data/sources/<tables>/<build>/: UiMapXMapArt (every art a map shows, any phase), UiMapArt → UiMapArtStyleLayer (layer 0's tile size), then each WorldMapOverlay of those arts (texture size, offset) with its WorldMapOverlayTile files (layer 0) in row-major order; UiMap names the comments. Keyed by UiMapArtID so the addon can tell whether the client shows the art it was built for. Empty (0 x 0) rows are dropped; a row whose tiles do not fill its grid is dropped with a warning. The default build is the Forever client's (1.60.1.70338, product wow_classic_beta). It lives in the core addon, loaded right after UI/ZoneArt.lua, because the core is what draws it (the menu and the route cards work without any pack), and it registers through ZoneArt.RegisterOverlays the way MapFrames.lua and Maps/ZoneLevels.lua register their generated tables.

Codex (codex build)#

node tools/polestar.js codex build [--addons <dir>] [--build <version>] [--offline] [--out <dir>] [--local-extras] merges data/forever/*.json (the Era quest database, the community quest database facts, recordings, corrections, the web database and web XP facts), the Forever client's tables (the game-table export, cached under data/sources/<tables>/<build>/: the newest 1.60+ wow_classic_beta build the game-table export lists, as data build picks its --forever-build (pickBuild in tools/lib/data/<tables>.js); --build names one; offline or with the game-table export unreachable, the newest cached build holding every table the Codex reads; meta.json records it), the installed the leveling gear lists (MIT) and Polestar's own guides into data/forever/codex/*.json, then emits the load-on-demand pack addon/Polestar_Codex/ (tools/lib/codex/pack.js) with its LICENSE-DATA.txt, which lists every source with its version and license. Both are build products (gitignored); the pack is part of the release: pack puts it in the zip and fails without it, install copies it when built.

  • Release-safe by default. --local-extras also merges three inputs that may not be redistributed: the loot-table addon (GPL-2.0, incompatible with Polestar's GPL-3.0-only), the rares database (All Rights Reserved) and data/forever/dungeons.json (DungeonJournal, no license). That build's meta.json and Meta.lua carry localExtras = true, its LICENSE-DATA.txt says not to share it, and pack refuses it, as it refuses a pack whose sources name any of the three (releaseRefusal in tools/lib/codex/pack.js). It exists for comparison on the developer's machine.

  • Importers, one per installed addon, all reading through tools/lib/consume/lua.js (addonVersion, readAddonLua: the installed addon's files, found with tools/lib/addons.js, run sandboxed in Lua 5.1 with the namespace the client passes them): <leveling-bis>.js (item sources: quest rewards, drops, crafts); with --local-extras also <loot-db>.js (DungeonsAndRaids incl. forever.lua and droprate.lua, Collections, Crafting, Factions, PvP) and <rares-db>.js (rares, their loot, treasures, mount and pet items). Each returns plain JSON with a meta naming its source, version and license.

  • Client tables: Item, ItemSparse (names, quality, levels, prices, the reputation and PvP rank an item requires: rep and pvp sources), ItemEffect, ItemXItemEffect, SpellEffect, SpellName, SkillLine, SkillLineAbility, Mount and BattlePetSpecies (the summon spells that make an item a mount or a companion), and for the dungeons Map, AreaTable, DungeonEncounter, LFGDungeons, MapDifficulty, ContentTuning; the Classic Era Item, ItemSparse, ItemEffect and Map fill items Forever's tables lack and tell Forever's own instances apart.

  • Dungeons and raids (tools/lib/codex/instances.js, release build): every Map instance that Forever's dungeon finder (LFGDungeons) names, or that belongs to the original game (map id below 1000) and has DungeonEncounter rows; Season of Discovery maps the client also carries stay out. forever = absent from the Era client's Map. Levels [entry, min, max]: the MapDifficulty "at least level N" message, the ContentTuning minimum of its dungeon-finder entries, the highest boss level (at most 60). Bosses: the DungeonEncounter rows of the normal difficulty in OrderIndex order, each matched to npcs.json (a recorded encounter's NPCs, else the same name among NPCs placed in the instance, else the same name preferring elites without open-world spawns at the instance's level, else one spawnless NPC whose name starts with it or is within two letters of it, of several the one placed inside (Kirtonos the Herald, not its spell visual); "A and B" encounters name both; an encounter naming no NPC takes the chest placed inside whose name ends with its name, The Seven: Chest of The Seven). A boss's loot (at most 30 items) is its own table first: its items.json drops at 1% or more or at an unknown rate, and its the web database drops outside the world-drop pool at any rate (Thermaplugg's schematics, Dextren Ward's plans); then the web database's commondrop rows (world books, world blues) at 1% or more, so those no longer crowd out a boss's own loot (Onyxia's Sacks of Gems); each part best chance first. The entrance (chooseEntrance): a recorded one (Recorder enter, data/forever/instances.json), else the curated table tools/lib/codex/entrances.json (per instance key: the point on the zone map where one walks in, { key, instance, map, x, y, source, note }, each with its public source, checked against the corpse point and our guides' gotos; every dungeon and raid in the Codex, Forever's own included, from the community quest database's Forever dungeon table, the community wiki and the Forever web database), else a guide's goto x,y "<instance name> ...", else the Map Corpse point (where a ghost comes back: near the way in, not at it; the Deadmines' is the portal deep in the Moonbrook mine, 200 yd from the Defias Hideout one walks in at) on the zone holding it, marked approx: true (packed as a fifth entrance field a, read as Codex.Instance(key).entrance.approx). AreaTrigger rows name no instance and the trigger nearest a Corpse point is as often an inn or city trigger, so they are not used. Every NPC placed in an instance (npcs.json instances, from the community quest database and recordings, or the web database's location naming an AreaTable area of the instance's map) belongs to it: its drops carry the instance. An instance's boss or mob the data ranks rare is not a rare unless the rares database lists it. --local-extras takes the instances from the loot-table addon and dungeons.json instead.

  • The Forever web database per entity (tools/lib/codex/<web-db>.js, release build only, lowest precedence). codex build reads the Forever web database facts cached under data/sources/<web-db>/codex/, and builds without them when there are none. The cache holds the web database's NPC, object and item list rows by id range (lists/<type>.json: id, name, levels, classification, boss flag, location and envChange, how Forever differs from Classic) and each fetched entity's facts (<npc|object|item|spell>/<id>.json). codex build reads the NPC rows for one fact only: the level of an NPC neither our data nor a page gives one (286 NPCs); a row's rank, boss flag and place are no page's, so rares, bosses and parity's verdicts read only pages. From the pages: an NPC's level, rare rank and spawns where ours lack them (and the rares only the web database places), drops and object contents with chance = looted count / looted kills, an NPC's or object's location (the areas its page entry names) and the web database's boss flag, and the reputation ("Requires <faction> - <standing>") and PvP rank ("(Rank N)") an item's Forever tooltip asks for. An instance also lists, after its encounters and by level, the NPCs placed inside that the data or the web database ranks rare or boss (or flags boss), or that alone drop an item of quality 2 or better at 5% or more, each with a role: rare (ranked rare), event (the Era quest database and the community quest database know no static spawn: summoned or scripted, the Ring of Law's fighters, the Tier 0.5 summons), else optional; then the chests placed inside (no gathering node) that hold more than one item (one is a quest object) and alone an item of quality 2 or better (at 5% or more, or an unknown rate), as bosses with an object and no npc (role optional). A boss is { name, npc, items }, plus object and role when set; the pack writes its ref as the npc id, or o<object>, then r/e/x for the role (an old pack's bare npc id still reads), and the Dungeons Bosses tab says Chest and the role beside the level.

  • Attunements and keys (tools/lib/codex/attunements.js, release-safe): the cited table tools/lib/codex/attunements.json names per Codex instance each attunement's goal only (the quests that end it, one of them with any, or the key item), whether everyone entering needs it or one in the group, and our one-line note, each entry with its public sources (the web database Forever, the community wiki). The steps come from the Codex: every prerequisite of a goal (all of pre, the preAny of the entry's side), oldest first, each with after/afterAny; a quest only an item starts gets that item's drops before it (Blackhand's Command from the Scarshield Quartermaster); an item goal adds the item's own sources (a dropped key's drops and chests, best chance first, three at most; Aqual Quintessence's standing and the NPC handing it out). links add a prerequisite our quest data lacks, with its source (Forever's City of Dalaran: Blood in the Streets after Dalaran Patrols and Key to the City, Heart of Disruption after it). inside places an NPC or object of a chain that has no open-world spawn and no Codex instance in the instance it is in, each with its the Forever web database page (Marshal Windsor, the Monument of Franclorn Forgewright and the Core Fragment in Blackrock Depths; the Inconspicuous Documents and Drakkisath's Brand in Blackrock Spire; Doan's Strongbox in Scarlet Monastery); each attunement carries the entries its steps meet as inside: [{ kind, id, instance }]. removed lists what Forever took out, with its evidence (the Seal of Ascension: 4743 no longer rewards it, no Forever ItemSparse row), read only by the local comparison. Covered: Molten Core (Attunement to the Core; Aqual Quintessence), Onyxia's Lair (the Drakefire Amulet, each side's chain), Blackwing Lair (Blackhand's Command), Naxxramas (one of the three Dread Citadel quests), Scholomance (the Skeleton Key, each side), Blackrock Depths (the Shadowforge and Prison Cell keys), Stratholme (Key to the City), Dire Maul (Crescent Key), Gnomeregan (Workshop Key), Scarlet Monastery (The Scarlet Key), the City of Dalaran (the Horde's Dalaran Sewer Key). A goal the Codex lacks drops its entry with a warning (codex build prints it); an instance the build lacks has none. The journal puts each list on its page as attunement and counts them (meta.counts.attunements, the summary's attune = { A, H }). auditReport(journal, attune, codex) writes the local comparison with the attunement data (codex attune, data/local/attunements-audit.md): per the attunement data entry the attunement of ours sharing most quests, the quests only one side lists and why ours has or lacks them (in our chain, a follow-up of our goal, not a prerequisite in our data, not in our Codex), then our verdicts on which source is right, kept in data/local/attunements-verdicts.md so a rerun keeps them.

  • Rares and treasures: NPCs ranked rare (4) or rare elite (2) in npcs.json (the Era quest database, the community quest database, recordings), with their spawns and loot; treasures are objects with the chest role, with their loot. Loot (Codex.Loot, where the Rares view picks a signature drop) is every drop or object candidate naming it, its own first (items no other NPC or object drops or holds: Bayne's Bite, Serrated Shadowclaw), then best chance first, at most 150 (a rare's candidates run to hundreds, median 286; 150 keeps every drop the internal build names that the Forever web database confirms, for about 215 KB of pack). An item keeps its 10 likeliest droppers (6 objects, 10 vendors) plus the drops of its instances' bosses.

  • Precedence for a fact found in several places follows docs/SUITE.md; sources are deduplicated by type and id, keep their from provenance and the best-known drop chance.

  • Output: meta.json (sources, localExtras, counts by kind, coverage = items with at least one source, pack = files, bytes and measured memory), items.json ({ id, name, quality, kind: item|mount|pet|recipe|tabard, spell, reqLevel, repFaction, repStanding, reqSkill, reqSkillRank, status, sources }; a source is { type: vendor|drop|object|quest|craft|rep|pvp| container|world, from, ... }), npcs.json, objects.json, quests.json (with pre = every blocking prerequisite, preAny, rewards from recordings and the leveling gear lists, xp = the full XP reward from data/forever/quests.json where known (what a guide or dungeon is worth in levels: Core/Levels.lua), and <web-db> on each quest the Forever web database's zone, dungeon and raid lists carry: { zone, type, side, choices, items, money, rep }, the zone or instance category and the list's rewards, read by the journal), instances.json, zones.json, factions.json, spells.json (mount and pet aura spell → item, class), frames.json (Era bounds for the pack's maps), journal.json (the Dungeons pages). An item's status is nosource without a source, and retired for a mount or pet whose every source is an Era vendor row (the Era quest database provenance) and, in a release build, whose summon spell's Mount row the Forever client flags hidden until known (Flags 0x40: kept only for those who already have it; the still-sold Black Stallion is not flagged), or, with --local-extras, that the loot-table addon Forever's collection lists do not name: the stock the pre-1.4 epic mounts were retired from. A curated guide's collects keeps an item obtainable, and Forever-only rows are trusted. Companions have no such client flag: a release build retires none.

  • Parity (tools/lib/codex/parity.js): a release build compares itself, entity by entity, with the last --local-extras build in data/local/codex-extras (when there is one), writes data/local/codex-parity.json and .md (per kind: total, matching, missing and different rows; a difference whose value comes from a better-evidenced input says so and counts as a match; a corpse point is never taken for an entrance) and records the match per kind in its meta.parity; without the internal build it claims none. A confirmed missing drop or loot row that the build knows but a cap left out (the merge's evidence.cut, not shipped) says so ("cut by a cap") and is counted on its own line. A chest boss matches the internal row named after its object (Chest of The Seven); a confirmed missing boss that Forever's sources place outside every instance (the web database location or spawns off the instance maps' areas: Techbot, Maraudon's Desolace caves) or rank an ordinary NPC (no the web database boss flag, not rare, no green or better item only it drops at 5% or more) says so, counted as "no boss of it in Forever's sources". codex parity [--ours <dir>] [--against <dir>] reruns the comparison. The report also lists what only play can fill and the Recorder event that fills it (Forever instances and their entrances: enter, encounter; Forever rares: rare, slain + drop; quest givers, enders, rewards: accept, turnin). On 2026-10-15 (Forever client 1.60.1.70374, the same data/forever):

    KindInternalRelease matchingMatch
    instances443784.1%
    instance levels2929100.0%
    instance entrances141392.9%
    bosses37333389.3%
    bosses with loot32632499.4%
    boss loot items2306197785.7%
    drop chances207071753384.7%
    rares41341199.5%
    rare positions38738499.2%
    rare levels406406100.0%
    rare loot516045078798.4%
    treasures656092.3%
    treasure positions3131100.0%
    treasure loot4502449899.9%
    items with a source162051618799.9%
    items: drop8806874399.3%
    items: quest24292429100.0%
    items: vendor22482248100.0%
    items: object32883287100.0%
    items: world41524150100.0%
    items: craft17971797100.0%
    items: container20952095100.0%
    tabards4040100.0%
    mounts136136100.0%
    items: rep98097599.5%
    pets9090100.0%
    items: pvp763763100.0%
    journal quests83382398.8%
    quest givers58256897.6%
    quest enders61860898.4%
    quest XP69868998.7%
    quest chains48147498.5%
    quest rewards58257799.1%

    The gaps: seven Forever instances the loot-table addon lists ahead of release are not in the client (no Map row); the Hall of Thanes' entrance stays different (the community quest database's Ironforge 27.6,47.8, where the Forever web database's comments put the quest givers "near the entrance", against 43.5,52) until a recording settles it; the loot-table addon's extra bosses (the Ring of Law's fighters, rare spawns, chests) are not DungeonEncounter rows; reputation standings that vendors gate in gossip, and battleground rewards the loot-table addon files as PvP, are in no client table (ItemSparse carries no MinFactionID or RequiredPVPRank for them, and the Forever web database's Sells standing is that same MinReputation). A vendor's, giver's or quest's gate comes from the cited table tools/lib/codex/repgates.json (loadRepGateTable, provenance curated: rows { item, name, faction, standing, npcs: [vendors or givers] | quest (+ choice), source, note? }, one per item and faction, each citing the community wiki, else the web database Classic's reputation guides or the community quest database; the Alterac Valley and Warsong Gulch supply officers, Argent Dawn quartermasters, Timbermaw Hold, Duke Hydraxis, Rin'wosho the Trader's barter quests, which the row also makes the item's quest source since the community quest database lists no rewards for them), from a curated guide that collects the item and reaches the standing before its buy (curatedRepGates, provenance guides: the Winterspring Frostsaber, the Stormpike Battle Charger, the Frostwolf Howler), and a quest's from the community quest database (minRep or a reputation objective, the higher: a rep source naming no vendor, and the quest's repFaction/repStanding, which blocks its reward row in Lookup; a table row's standing replaces it, with a warning when they differ). Precedence: client, then the table, then a guide. A rep source at an item's vendor or giver sets its repFaction/repStanding; Lookup gates each vendor or giver row by the rep source standing at that npc, else (a purchase) by the item's own, unless that is the other side's faction and a gate of the npc's side exists (Lookup.PurchaseRep: Alterac Valley wares both sides' supply officers sell). A table row whose item, vendor or quest the Codex lacks is a build warning. What stays missing: the Insignia of the Dawn and of the Crusade (quest rewards and turn-ins, no standing), the Bloodsail Shirt, Sash and Pants (in the Buccaneer's Uniform, whose quest, Dressing the Part, carries the Neutral gate) and the world-drop Warbear patterns 15742 and 15754 (Timbermaw Hold sells 20253 and 20254, gated in ItemSparse). An NPC that casts a spell creating an item (client SpellEffect 24; who casts it, the Forever web database's spell page in the cache) hands it out: a gift { npc, spell } source (Duke Hydraxis's Aqual and Eternal Quintessence). 26 Forever rares no database ranks yet. The 251 confirmed drop chances the release lacks are all cut by an item's 10-dropper cap (the internal build keeps the loot-table addon's instance trash past it; every dropper of every item would be 1.6 million sources against 74 thousand).

Reference data (data/forever/, built by data build, gitignored)#

text
quests.json   { "<id>": { id, title, level, minLevel, races, classes, start: { npc: [], object: [], item: [] },
                          end: { npc: [], object: [] }, pre: [ids], excl: [ids], next, objectives: [
                          { type: "kill"|"item"|"object"|"event", id, count, text } ], objectiveText, zone, xp,
                          rewards?: { items: [ids], choices: [ids], money }, source: [] } }   (rewards: recorded only;
              count: the objective text's, else <era-db>'s, else <server-db> <server-db> quest_template's for the same id or the
              quest's one objective of that kind, else null; tools/lib/data/<quest-db>-merge.js fillCounts)
npcs.json     { "<id>": { id, name, level, levelMax?, rank?, faction, roles?, spawns: { "<uiMapID>": { f: "e"|"l", pts: [[x, y], ...],
                          more?: [[x, y], ...] } }, instances?: [instance map ids], encounter?: DungeonEncounter id, source: [] } }
objects.json  { "<id>": { id, name, roles?, spawns: { ... same ... }, instances?: [instance map ids], source: [] } }
items.json    { "<id>": { id, name, drops: { npc: { "<id>": chance }, object: { "<id>": chance } }, vendors: [ids], source: [],
                          seen?: { npc|object: { "<id>": { n, of } } } } }   (chance in percent; seen: recorded loot counts)
zones.json    { "<uiMapID>": { id, name, areaId, parent, type } }
map-frames.json { era: { "<uiMapID>": { c, left, right, top, bottom } }, forever: { ... same, Forever client bounds ... } }
professions.json { "<skillLineID>": { id, name, kind: "primary"|"gathering"|"secondary", ranks: [ { name, max, minLevel } ],
                  trainers: { Alliance: [npcIds], Horde: [npcIds] }, recipes: [ { spell, name, item, itemCount, reagents: [ { item, count } ],
                  learnedAt, yellow, green, grey, source: "trainer"|"vendor"|"drop"|"quest"|"item", recipeItem, cost } ] } }
factions.json { "<factionID>": { id, name, side: "Alliance"|"Horde"|null, parent, source: [] } }   (Forever Faction table wins by id, Era fills)
races.json    { "<raceFile>": { "<Alliance|Horde>": { id, bit, classes: [classFiles] } } }   (Forever client ChrRaces, ChrClasses,
              CharBaseInfo; written only when those tables are cached; see tools/lib/data/races.js)
spawnz.json   { npc: { "<id>": [[instanceMap, x, y, z], ...] }, object: { ... } }   (<server-db> <server-db> creature and
              gameobject spawns on maps 0 and 1, UnitPosition's frame (x north, y west), 0.1 yd, each id's spawns sorted;
              one id per line; the compiler's goal `z`, tools/lib/dsl/heights.js; left out, with one warning, when
              --offline has no cached dump; tools/lib/travel/<server-db>.js loadSpawnHeights)
meta.json     { builtAt, sources: [ { name, url, revision, license } ], counts: { ... }, liveFrameMaps: [uiMapIDs],
                <quest-db>: { notInStore: [quest ids], changedFields: { field: count } } }
dungeons.json { source: { name, addon, title, version, author, license }, dungeons: [ { key, name, levels: { min, max },
                zone, zoneName, entrance: { map, x, y, f }, abbreviations, factions, bosses: [ { name, npc, level, rare,
                loot: [ { item, name, slot, quality } ] } ], quests: [ quest ], chainQuests: { "<id>": quest } } ] }
                quest = { id, title, faction, level, minLevel, classes, xp, xpSource, money, rewards: { items, choices },
                start, end: { map, x, y, f, source: "data"|"data-npc"|"dj"|"web"|"entrance", kind, id?, item?, name, inDungeon? },
                chain, followUps, startItem, startBoss, startNear, bosses, hidden?, inData }

Spawn points per entity per map: pts holds at most 24 (data build --cap), spread evenly over the entity's spawns (farthest-point sampling); more the distinct points the cap left out, sorted by x then y, only when there are any. Every consumer reads the evenly spread pts (the simulator, zone levels, Codex, the profession generators), except the ones that mark where an entity spawns, which read pts and more, every known spawn: the guide compiler's spawn areas and near-spawn lint, and QuestDB's marker clusters.

Read the rest of this section825 words

dungeons.json is built by dungeons build (tools/lib/data/dungeonjournal.js) from the installed DungeonJournal addon (no license: local only, see THIRD_PARTY.md). Bosses are in kill order; entrance and DungeonJournal positions are live frame (f: "l"). Quest positions take our data first (the quest's giver/ender with spawns, DungeonJournal's map picking the spawn; else a spawning NPC of the named giver), then DungeonJournal's maps, then positions researched by hand (WEB_POSITIONS, with a reference); a giver inside the instance gets the entrance and inDungeon. chain is DungeonJournal's prerequisite list with our data's pre added in doing order; xp is our data's, else DungeonJournal's observed Forever XP. The guide compiler reads nothing from dungeons.json (its facts stay local, THIRD_PARTY.md): a quest only it knows needs its title (and level) written in the guide.

The hub's Dungeons section and the pick-up guide built from it: "Dungeons" above.

professions.json is built by profession build from the Forever client's DB2 (wow_classic_beta 1.60+ via the game-table export: SkillLine, SkillLineAbility, SpellName, SpellEffect, SpellReagents, ItemSparse, Item, ItemEffect, ItemXItemEffect, ...; tools/lib/professions/tables.js). Forever changed recipe thresholds, reagents and names from Classic (issue #109), so Classic Era only fills gaps: a table the game-table export has no Forever export of (SpellTotems, SpellCastingRequirements, SpellFocusObject) comes whole from the newest Era build, and the spells and items the Forever tables lack come from Era's rows. Each profession also carries mainTrainer { Alliance, Horde } and materials { itemId: { name, buy, buyCount, sell, quality, vendors { A, H }, limitedVendors { A, H }, bop, madeBy, gather { herb, mine, fishing }, drops, dropCount } } (bop: the client's ItemSparse binds it when picked up, so the Auction House never has it); ranks carry minSkill, spell, trainers { A, H } and, for secondary skills, book/quest; recipes may carry enchant, learnedAtEstimated, requires (specialisation), reputation { faction, standing }, tools, focus, races, classes, skillUps. Season of Discovery recipes (spell id ≥ 400000 that the Era client's SkillLineAbility lists too; Forever's own recipes, past 1,000,000, it does not) and the ones Forever retired (AcquireMethod 3, such as Alchemy's healing potions, which Forever teaches in First Aid) are excluded. A crafting profession whose last points no trainer or vendor recipe reaches (Forever Tailoring past 295: Spinning Wheel, Loom) is drafted as far as they go, with a last step naming what is left.

races/classes are Blizzard bitmasks (Human 1, Orc 2, Dwarf 4, NightElf 8, Undead 16, Tauren 32, Gnome 64, Troll 128; Warrior 1, Paladin 2, Hunter 4, Rogue 8, Priest 16, Shaman 64, Mage 128, Warlock 256, Druid 1024), null means unrestricted. The Era quest database masks are Era masks: Skyborne has no bit there, so never read an Era-only mask as excluding Skyborne. The community quest database masks add Forever's Skyborne bits 2^32 (High Order, Alliance) and 2^33 (Windshapers, Horde), past 32-bit bit.band: split them arithmetically (races % 2^32, floor(races / 2^32)). Polestar_QuestDB keeps the low 32 bits. Spawn f is the frame of those points ("e" Era, "l" live Forever). A frame's c is the instance map id (0 Eastern instances.json { "<instance>": { id, name, type: "party"|"raid", entrances: [ { map, x, y, n } ], encounters: { "<dungeonencounter>": { name, npcs: [ids], kills } }, source: ["recorder"] } } (recorded only; {} without) Kingdoms, 1 Kalimdor; 2991, 2997 for Forever's new islands): the continentID C_Map.GetWorldPosFromMapPos returns.

Optional extras: quests skill (profession requirement), event (holiday), escort (true or false, only from corrections.json: whether it is an escort where its own text does not say so right; tools/lib/dsl/escort.js), xpSamples (how many recorded observations back xp); event objectives map, f, x, y; npcs levelMax, rank, roles (flightmaster, innkeeper, vendor, trainer, ...), subName; objects roles (herb, mine, chest, ...); items containers (items that contain it), questItem: true (the community quest database item class 12: drops only while on the quest), duration (seconds the item lasts once the player has it: the Forever client's ItemSparse DurationInInventory, when that table is cached; the timed-item lint reads it). A drop chance is null when the community quest database lists the dropper but no source has a rate. From the community quest database, each only when it has a value: quests preAll (all must be done; preQuestGroup), preAny (any one suffices; preQuestSingle), breadcrumbFor (the quest this optional breadcrumb leads to), breadcrumbs (breadcrumbs leading here), parent (must be active; parentQuest), children (childQuests), requiredItems (needed, not objectives), sourceItem (handed over at accept), maxLevel, skillLevel, repeatable, reputation { faction, value } (an objective), minRep { faction, value } (requiredMinRep: the reputation value, 3000 Friendly, a character needs to take it), area (AreaTable id of the quest's zone or dungeon, the community quest database zoneOrSort > 0); kill objectives credit (every npc id that gives the kill credit). pre keeps the Era quest database's meaning (any one suffices, optional breadcrumbs included) for every quest the Era quest database has; Forever-only quests take preAny, else preAll. excl is the exclusive group: the Era quest database close united with the community quest database exclusiveTo.

Sources and precedence#

data build reads, lowest precedence first:

  1. The Era quest database (MIT, git clone cached in data/sources/<era-db>): Classic Era quests, npcs, objects, items, drop chances, Era-frame coordinates.
  2. The community quest database's Forever store (<quest-db>-forever): the <quest-db>_Forever.toc that a community quest addon for Forever installs. --<quest-db> <dir>, else auto-detected in the Forever client the way install finds it (client detection lives in tools/lib/addons.js: detectAddonsDirs, foreverKind). It's read in place (tools/lib/data/<quest-db>.js ports the community quest database's src/meta/codec.lua, src/read/baked.lua and normalize.default). Field layouts come from the installed src/meta/*Meta.lua, and support data from support/Forever/*: QuestXP, DropTables (chance = manual correction, else <server-db>, else the web database, as DropDB.GetItemDroprate), Zones (AreaID → UiMapID). It adds Forever-only quests, npcs, objects and items. On shared ones the community quest database wins on every scalar field it has a value for (titles, levels, masks, objectives, spawns), and Era-database-only fields stay. Source lists are unions: item droppers, drop objects and vendors, npc roles, and quest givers and enders list the community quest database's entries first. Its values win on conflicts (a drop chance), and the Era quest database's extra entries are kept. A missing the community quest database entry is not evidence that Forever removed something. Real removals, or wrong the Era quest database sources, go in corrections.json. Exceptions: npc faction stays the Era quest database's, because the community quest database's friendlyToFaction means "not hostile", so wildlife reads "AH". pre also stays the Era quest database's (see above). The Era quest database copies of a spawn on another map (a city and its zone) are kept when they lie within 30 yards of a community quest database spawn. The community quest database's Static Corrections are folded into the store. Its Dynamic (per-character) Corrections are not applied. The community quest addon and its database ship no license file (CurseForge says GPL-3.0; the maintainers say to treat them as all rights reserved, the community quest database issue #4447). The data stays in data/, and only the importer is in this repository.
  3. The Forever web database's change notes (tools/lib/data/<web-db>.js, --no-<web-db> skips them, as does a build without the community quest database): the quest list of every zone and city page and of the nine class pages (class quests are filed there, not under a zone) on the Forever web database, read from data/sources/<web-db>/zones.json; without that cache this step adds nothing. A quest the web database marks updated with "Experience X instead of Y", "Level X instead of Y" or "Requires level X instead of Y" gets X where its value is still the old Y (for XP, also where it has none), and one marked "Renamed from Y" gets the web database's Forever name where its title is still Y (Chastise, formerly A Lack of Fear); any other value is left alone, and the quest's source gains <web-db>:xp, <web-db>:level, <web-db>:minLevel or <web-db>:title. Only rows whose category is the zone's area count: the web database answers an unknown zone path with a generic list. Its Forever database also lists datamined quests that are not live (<NYI> placeholders, Season of Discovery leftovers), so presence there is not evidence that a quest is in the game.
  4. Polestar corrections (tools/lib/data/corrections.json, committed): facts the community quest database or the Era quest database get wrong or lack, each with its source in a note: quest fields (set), item sources to drop (removeDrops, removeVendors), npc/object fields (set: the Forever elite and rare ranks the community quest database's Forever store lacks, since it gives every Forever npc rank 0, and renamed Era records) and npc/object spawn maps (addSpawns). They are applied after the merge, only in a build that reads the community quest database, and the record's source gains polestar-correction. One cleanup is general rather than per quest: pre, preAll and preAny never contain the quest itself or an id from its excl. The community quest database preQuestSingle sometimes lists the whole exclusive group.
  5. Recordings (data/recordings), which win over all of the above; finds (rares, kills, loot, treasures, instances; "Recordings" below) follow the rules of their own paragraph there.
Read the rest of this section518 words

source on every quest, npc, object and item lists its contributors in that order: <era-db>, <quest-db>-forever, <web-db>:* (change notes), polestar-correction, the web XP tag, recorder, then the recorded XP tag (recorder:xp-*). Blizzard map tables come from the game-table export: Classic Era (wow_classic_era) for the Era frames the Era quest database's coordinates use, and the Forever client (wow_classic_beta builds 1.60+, --forever-build, --no-forever) for Forever zones and map-frames.json forever. Forever zone names win.

Coordinate frames. the community quest database's store holds Forever-frame percentages. For the evidence, compare its points with the Era quest database's Era points for the same entity. On Mulgore, Eastern Plaguelands, Redridge and Stormwind, the community quest database's points match the Era quest database only after inverting the community quest database's own EraToForever transform (src/support/eraToForever.lua). Median distance to the nearest the Era quest database point, in map percent, raw vs inverted:

Mapnpcsobjects
Mulgore2.57 vs 0.615.10 vs 1.28
EPL7.93 vs 0.088.52 vs 2.58
Redridge4.25 vs 0.075.09 vs 1.40
Stormwind14.02 vs 0.0511.14 vs 0.05

Per entity on those four maps, 1244 were clearly closer inverted, 0 closer raw and 114 ambiguous. On unchanged maps (Elwynn, Durotar) raw and inverted agree (0.1-0.2 median for npcs), as they must when the frames are equal. So the community quest database points are tagged "l" on maps whose Forever bounds differ from the Era bounds by more than 1 yard (with Forever tables; otherwise the maps eraToForever lists) and on maps without Era bounds (Forever-only: Zephras Isle 2521, Darkspear Islands 2524, Riverglades 2548, Shen'dralas 2652, Mount Hyjal 2482). Everywhere else they are "e", the same point in both frames. meta.liveFrameMaps lists the changed maps.

Quest xp: recordings first (rewardXpBase wins; otherwise the largest rewardXp or turn-in xpReward observed while the player was at most 5 levels above the quest, since Classic reduces XP beyond that, so an over-levelled observation never overwrites a full one; source names recorder:xp-base, recorder:xp-log or recorder:xp-turnin). Then the community quest database's QuestXP, with the web database's "Experience X instead of Y" notes applied where it still has the old value (above). corrections.json xp replaces both: Forever about doubled the XP of many dungeon quests, and QuestXP still has the Classic value for some (issue #394), so those take the XP shown by in-game evidence dated after the 1 October 2026 change that halved dungeon quests' extra XP (build 70170; a client's quest cache gives difficulty and multiplier, its XP is QuestXP × multiplier, while web databases built from the DB2s give only the Classic base), or an undated capture at the dated post-change rate (XP / base) of a quest in the same dungeon: the extra XP was twice as much before the change, so such a value can't be a pre-change one. Then, for quests still without XP, the Forever web databases (--no-web-xp skips them; results are cached in data/sources/webxp/quest-xp.json): a Forever talents site (Forever beta client cache, tag <talents-site>:xp), else a Forever changes site (the web database's Forever database: the quest's reward, <changes-site>:xp, or its quest-chain list, <changes-site>:xp-chain), else the XP on the web database's zone quest list (<web-db>:xp). meta.counts gives questXpFromWeb, questXpFrom<web-db> and <web-db>Changed ({ xp, level, minLevel, title }).

Recordings (data/recordings/<session>.json)#

Normalised from PolestarRecorderDB by record import:

text
{ schema: 1, build, session, char: { name, realm, race, class, faction }, started, ended,
  events: [ { t, e, ...fields } ], maps: { "<uiMapID>": { name, type, parent, c, left, right, top, bottom } } }
Read the rest of this section392 words

record import --anonymize (a file a player sent in) sets char.name and char.realm to null and adds char.id, 8 hex digits of a SHA-256 of "name-realm", which also replaces the name in the session key; no raw copy is archived.

t = seconds since session start. Event kinds and fields: accept {q, title, qlvl, lvl, xp, map, x, y, npc, npcName, object}, turnin {q, xpReward, money, lvl, map, x, y, npc, npcName}, progress {q, oi, text, cur, max, map, x, y}, complete {q}, abandon {q}, level {lvl, map, x, y}, zone {map, zone, subzone}, pos {map, x, y}, kill {npc, npcName, map, x, y}, loot {item, itemName, count, map, x, y}, gossip {npc, npcName, available: [q], active: [q], map, x, y}, fp {location, npc, npcName, map, x, y}, taxi {from, to}, bind {location, map, x, y}, trainer {npc, npcName, map, x, y}, vendor {npc, npcName, map, x, y}, death {map, x, y}, mark {text, map, x, y}.

Additive fields: progress.type; kill/loot may carry q and oi; accept/turnin carry object/objName for game-object givers; accept.item for item-started quests; accept.rewardXp/rewardXpBase (from the client's GetQuestLogRewardXP, when cached); a reward {q, rewardXp, rewardXpBase, lvl} event when the reward was not cached at accept, or for quests already in the log at login; a session may carry dropped: { kind: n } when caps were hit. Forever restricts COMBAT_LOG_EVENT_UNFILTERED to secure code, so kills come from UNIT_DIED/PARTY_KILL correlated with objective progress.

Character progress: skill {skill, skillLine, rank, max, map, x, y}, rep {faction, factionName, standing, value, delta, map, x, y}, explore {map, x, y, area, areaID, xp}, craft {spell, item, count, skill}, learn {spell, name}, achieve {id, name, already}. Session dumps from /psrec factions|skills|legacy: factions: { "<id>": { name, order, parent, header, headerRep, standing, value, min, max, atWar, canToggleAtWar } }, skills: { "<skillLine>": { name, order, rank, max, category, categoryID } }, legacy: { categories, achievements, majorFactions, trees }, each time-stamped in dumped: { kind: epochSeconds }.

Route telemetry (v = 1), recorded while Polestar runs: step {v, reason, guide, step, title, guideTitle, time, lvl, xp, xpMax, rested, money, combat, map, x, y} with reason = login (session start or /reload), step (STEP_CHANGED), guide (GUIDE_CHANGED: tab switch or new guide) or complete (GUIDE_COMPLETED; no step). Only the active tab is snapshotted. Also, with or without Polestar: combat {on} (PLAYER_REGEN_DISABLED/ENABLED), afk {on}, revive (alive again after a death).

Measuring routes#

Recording is opt-in (PolestarRecorderDB.enabled == true, set by /psrec on or Options > Recorder); while it is off the Recorder writes nothing, not even a session. Testers turn it on, play with Polestar, run record import (or keep record watch running), then record report [--guide <text>] [--json] [--top n]. The report rebuilds step passes from every session and character:

  • Play time between consecutive events counts up to 5 minutes; time flagged AFK counts zero; time between sessions counts nothing. A /reload costs its few seconds and continues the step.

  • Per character and guide, a frontier step: a later step closes its pass and Instances. npcs and objects inside a dungeon or raid get instances (instance map ids). The community quest database gives them no coordinates, only their areas ({ [areaId] = { { -1, -1 } } }): an area's instance is its Forever AreaTable ContinentID when that Map row is InstanceType 1 (party) or 2 (raid); without the Map table (an optional Forever table), an area no UiMap shows on a continent other than 0 and 1. Recorded inst fields add to them.

    opens the next; returning to it (tab switch, reload, new session) continues the pass; an earlier step (Prev, Goto back, a step un-done) is a revisit, reported apart. Completion closes the last pass; passes still open are shown as open and kept out of the statistics.

  • XP from the snapshots, as cumulative XP (recorded xpMax per level, else the 1.12 table), so level-ups inside a step count in full.

Per step: passes, median/p75 time, XP, XP/hr, combat share (the rest is mostly travel), deaths, turn-ins, travel yards (taxis and jumps faster than 40 yd/s excluded), revisits. Per guide: time (completed runs), levels gained, XP/hr. Then three lists: slowest steps by XP/hr; steps at least twice and 2 minutes over the guide's median step time (compiled packs carry no time estimate to compare against); detours, steps with 600+ yd of median travel and no XP or a third of the guide's XP per yard.

Route planner (planner)#

node tools/polestar.js planner [<pack dir>...] [--port 8078] [--nav <dir>] serves a local page (127.0.0.1 only) that plays a guide route on the game's map and edits its .guide sources. Inspired by Nivek_Gaming (@Nivek_Gaming on X), whose idea the planner and the navigation grid both come from.

  • Coordinates (tools/lib/planner/geo.js): world { c, wx, wy } (the client's yards, c the instance map), map percent per frame (f = "e" Era, else live), and the renderer's plane X = -wy, Y = -wx.
  • Map art (planner art, tools/lib/planner/art/): the Forever client's minimap blocks (WDT MAID, 8th field; ADT tile (col, row) = map<col>_<row>.blp = WDT entry row * 64 + col) and zone map art (UiMapArtTile plus every WorldMapOverlay), fetched by fileDataID from the game-table export into data/sources/casc/<product>/<build>/, built into data/local/planner/art/: index.json, a BC1 pyramid of 512 px tiles with 8 mips each (level 0 one ADT block, level 6 the whole grid; missing blocks in the map's sea colour), and one unfogged PNG per zone over its live frame. Local only, never shipped.
  • Playback (tools/lib/planner/timeline.js): plays compiled guides goal by goal for one character with the simulator's priors (walk x detour, kill time by level gap and class, talk and use times, drop rates, flights over travel.json legs at 30 yd/s, boats with their waits, hearth with its cooldown, levelOverhead per level) and tools/lib/game/xp.js. The Timeline holds per step its times, split (travel, combat, interact, wait), levels, XP, goal places and spawn areas, the character's state at its start, plus a position track and events.
  • Edits (tools/lib/planner/source.js, goalfmt.js): the parser records each goal's token spans (goal.spans, non-enumerable, so nothing compiled changes); edit operations rewrite only the tokens they change, regenerate a goal line from its fields when a form changes it, move steps and goals with their comments, and add an explicit zone line wherever a move would change the zone a goal's coordinates are read in. A result that parses worse than its input is refused.
  • Commands (tools/planner/commands.js): one registry served as POST /api/cmd, as MCP tools (planner mcp: stdio JSON-RPC that proxies to a running planner or starts one) and, where the browser has it, WebMCP. Edits name the file's hash and are refused when it changed; they write the file at once, keep an undo history and are pushed to the page over GET /api/events (SSE), as are external changes (fs.watch). A full lint runs in a worker thread 600 ms after changes; pack_compile writes the addon folder as compile does. The server answers only requests naming it in Host, runs only JSON POSTs from no Origin or its own, refuses an icon GET (which may fetch from the game-table export) that the browser marks as another site's, and forbids framing (X-Frame-Options: DENY, frame-ancestors 'none').
  • Navigation (tools/planner/navpool.js; the grid itself: "Navigation grid" below): with a navigation grid (data/local/nav, nav build) every walk of the playback is routed on it with the character's movement profile (tools/lib/nav/move.js profileFor: mount from mountLevel, Ghost Wolf, Travel and Aquatic Form, Cheetah, indoor rules): the legs (to a goal's place, a boat, a flight master, a spawn pool away from the character) and the hops between spawn points inside a kill, collect or interact area (picked nearest first as before; kill and loot time stay the priors'). buildTimeline({ routes }) takes a synchronous route provider: a routed walk takes the route's seconds, its track follows the route's points (a leg with a mode per segment: swim, fall, climb, lift, indoor; a hop in fight mode), and the goal records route { yards, byClass, falls }; a walk the grid cannot route, or one not computed yet, keeps the straight-line estimate (a hop the spacing model's walk) and marks the goal (estimate, routeReason) and its step. Totals count legs, routed, hops, hopsRouted, routeSeconds and estimateSeconds. Flights keep their time and, with flightPaths (the Era client's TaxiPathNode points of each travel.json taxi leg, read from the cached data/sources/<tables>/<build>/TaxiPathNode.csv), their track follows the points the flight takes. The workspace builds a timeline with the cache as provider; the legs it misses (ends quantised to 1 yd, plus the profile's speeds) are computed in a worker_threads pool (one Nav per worker; CPUs - 1, at most one per 4 GiB) while timeline_get answers at once with the straight legs and routing { pending, done } (SSE routing each second); when they are all in, the cached timeline is dropped and SSE changed { timeline: true } makes a page whose timeline is still being routed ask again (as does an SSE reconnect, and, when no routing event for its timeline arrives within 10 s, the page itself). Routes are appended to data/local/nav/routes-<build>.jsonl, build being a hash of the grid's index.json, lifts.json and the route code, so restarts are warm and a rebuilt grid (or edited costs) starts afresh; the timeline cache key holds the build. The choice between walking and a boat or a flight master still compares straight-line times. nav_route routes one walk for an agent; GET /nav/index.json lists the grid's tiles, GET /nav/tile/<c>/<col>_<row>.png draws an ADT tile's Walkable layer (per column: nothing walkable, steep, climb, swim only, indoor only; plain ground clear), and GET /nav/aids.json serves the arrow aids (nav aids, crossings schema) as world points.

node tools/polestar.js nav build [--continents 0,1] [--force] [--offline] builds data/local/nav from the Forever client's own files, fetched by fileDataID from the game-table export into data/sources/casc (local, never shipped):

  • Geometry (tools/lib/nav/geom): per map tile, the root ADT's terrain (heights, low- and high-res holes, area ids) and MH2O liquids, and the obj0 ADT's WMO and M2 placements, each model's collision (WMO faces by the collision and render flags, indoor groups; M2 collision triangles) and WMO liquids, placed with the client's transform (checked against MODF extents).
  • Grid (tools/lib/nav/grid, constants and file layout in format.js): 2.0833 yd columns (256 per tile), walkable surfaces with 1.6 yd clearance, walls (model faces of 80° and steeper) stored per column side; links for steps, slopes (terrain to 35° plain, 35-50° and 50-60° slow, 60-80° climbed upward only; models to 50°), WMO staircases (treads under the upper column's top rising at most 1 yd walk as a slope to 45°; steps whose top rise is up to 1.6 yd over treads rising at most 1 yd, 2.6 yd in all, are a jump: the Ruins of Lordaeron's throne room into the Undercity's lift halls), jumps to 1.6 yd, falls (free under 14.57 yd, then the servers' damage share 0.018 h - 0.2426; at most 30 yd onto ground and 60 yd into water; landing only on stable ground), swims and lifts (elevator objects of data/forever at their car's travel from the client's TransportAnimation); regions; a per-tile graph per movement class. Reviewed openings (geom/openings.json): a WMO door narrower than about three yards can lose to the columns when a jamb's moulding or reveal lies on its column centre lines, so each listed portal (by WMO and MOPT index, in every placement of that WMO) is cut out after rasterising: the columns whose centre lies within its span and depth yards of its plane lose their spans and walls from just above its bottom to its top (Brill's town hall, whose only way in is a 2.9 yd door into its tower room, #366). Fatigue water is never swum: MH2O's deep quads (the server's fatigue; a chunk of ocean without attributes is deep throughout, geom/mh2o.js) give a blocked swim surface (SWIM | BLOCKED), so no route crosses open sea while swims along a coast and across shallow water stay.
  • Routes (tools/lib/nav/path.js, API in its header; move.js the profiles): snap picks the surface for a map point (without a height, a point over an indoor floor takes that floor or one level with it beside it: an innkeeper behind a counter gets the inn's floor, not its roof or the terrain under the city, never the storey below; then indoor floors before terrain, never a ledge one only falls onto), route minimises time for a character (run, swim, mounts, Travel Form, Ghost Wolf, Cheetah, indoor rules, slope, fall and lift costs) with a direct search for near ends and the per-tile graph otherwise, then smooths the path; los tests a straight walkable line. nav path prints one route.
  • Packing for the in-game engine (nav pack, tools/lib/nav/pack.js, rules in its header): the grid as Polestar's walking maps (format 2, grown from Shortest Path Forever's walking-map format: his baker's HPA* and encodings, ported), which the router in Polestar_Nav (started from his engine) routes on. Each SPF cell (128 per tile by default, 4.17 yd) holds the connected pieces of our surfaces in its columns (base plus floors; a wall inside a cell keeps its sides apart); a step between cells is open only where one of our links crosses it, so walls and cliffs close it; falls and lifts stay out (lifts are his transports), and so do climbs (60-80°, one way up). Faces are held back: steep links (50-60°) and terrain links inside a run of triangles over 50° (Forever's client slides off them) rising more than a jump, or on ground steeper than 50° measured 8 yd across. His steps are two-way and cost their flat length, so a face packed as steps was a straight line up it (Fitz's report: the Raptor Horns slope in the Barrens, 61.66,35.46 to 61.00,33.70, where the columns and cells averaged 51-56° triangles to under 50°). A face link is opened again only when it is the only way between two parts of the map (union-find over every other join, then the face links gentlest first), so nothing reachable before is lost and a face with a way round is walked round; closing them all cut off 84 guide goals. A cluster (one tile) border gets his entrances, one in every run of crossings between the same two regions (~38 crossings at 128 cells, a cell crossing straight and both ways diagonally, so about every 54 yd on open ground), and one more on every road crossing it with none of his within 8 yd: every walk across a border goes through an entrance, and where the border crossed Ratchet's north-east road 35 yd from the main road with the one entrance on the main road, walks left the road along the border (Fitz's report, 2026-10-16). A run of dry crossings (ground on both sides, no swim surface) that no entrance crosses dry gets one too: land and the water it wades into are one region, so his entrance can fall in the water beside a beach or a pier's root, and walks from Auberdine to its boats swam 200 yd across the bay to the pier because the beach crossed the tile border x = 6400 with no dry entrance (Fitz's report, 2026-10-17). An intra-cluster edge is pruned only for a two-hop path whose hops are both shorter (gen_nav.py's 2% rule let two entrances a few yards apart at the end of a long walk prune each other's edge, cutting a region in two; it cut 14 guide goals off with faces held back, Thunder Bluff's among them). A route down a face goes round as well. Every surface carries a ground class byte (normal, road, gentle slope, steep slope, indoor, indoor city, plus an Alliance and a Horde hub bit): the steepest terrain triangle under it (grid format 4's tslope) against the slope bands, the indoor flag (indoor city where the chunk's area or the zone map around it is a capital, AreaTable Flags_0 & 0x100), the roads layer and the hubs layer (nav layers, a hub bit on every cell a hub disk touches); Routing/Costs.lua (mirrored in tools/lib/nav/costs.js) prices a step by it, per faction, and every graph edge carries its cost for Alliance, Horde and no profile and its yards. chokepoints.js adds Chokepoints.lua (PolestarNavChokepoints[map]: lift landings, bridges, gates at passages, tunnels, cave and building mouths, passes, each named by its AreaTable area) for the journey's named walk steps.
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