Architecture
Azeroth World Editor is an Electron app written in TypeScript, with a React interface.
Layout
Section titled “Layout”src/ core/ Pure logic: no Electron, no React. Quest model, schema, SQL, scripts, map maths. main/ Electron's main process: the IPC API, the project store, database connections, the game client's files. preload/ The bridge that exposes the main process's API to the interface. renderer/ The React interface: the World (world3d/), the Quests dock (views/dock/), views, modules, editors. shared/ Types shared by main and renderer, such as the IPC contract.drizzle/ Migrations for the local project store.tests/ core, main, renderer, integration, e2e and docs tests.site/ This documentation site.src/core never imports Electron or React, so almost everything the app decides can be unit-tested in plain Node.
Inside src/core, each feature has its own folder: scripts (quest scripting scenes), combat (the combat wizard), entities (new NPCs and objects), patrol, map, client (reading the game client’s MPQ archives and BLP images), game (server data such as DBC files, map heights and navmesh), export, sql and more.
Processes
Section titled “Processes”- The main process (
src/main) owns everything with side effects: the MySQL connection to the world database (read-only) and optional dev database, the open project and its undo history, the local store (SQLite through Drizzle), the file system, the game client and server data folders, and theawe-wow://protocol that serves game client files to the 3D view. - The API the renderer calls is
createApiinsrc/main/api/. It has one module per area (connection-api,lookup-api,quests-api,map-api,entities-api,world-layer-api,spawn-groups-api,history-api,export-apiandproject-api), and each implements its interface from the contract insrc/shared/ipc/, where each area has a file with its types and its part ofApi(QuestsApi,MapApiand so on), andrequests.tsvalidates every call as it arrives. The areas are built from shared services, made once inservices.ts: the connection, the project context, the server data files, the export checks, patch building, spawn groups and history travel. A new call is declared in its area’s interface, given a request schema, and written in its area’s module; a new area is a new pair of files, its interface added toApiand its module spread intocreateApi. - The MCP server (
src/main/mcp/) lets an AI assistant work in the open project. It is off until the user turns it on in Settings (the MCP / AI tab), and listens on127.0.0.1behind a bearer token (http.ts). A tool is data (tool.ts): a name, an input schema, whether it writes, and arunthat reaches the app only throughctx.call, which validates arguments with the same schemas the window’s calls use (api/invoke.ts).server.tsturns the tool list (tools/) into MCP tools. Write tools go through the write guard (write-guard.ts): the window is asked to hand over pending edits, the tool’s changes become one history step labelledAI: …, writes run one at a time, and the window is told what changed by theapp:external-changeevent, shaped like an undo result so the renderer applies it with the same code (applyExternalChange). From the flush until that event the window holds its own quest edits back (app:hold-edits,holdEdits), so a copy of the quest older than the write is never sent over it; edits typed meanwhile are put on top of the change. The tools that tell the assistant what already exists (tools/lore.ts) are built onsrc/core/lore/: pure code over the existing database reads (zones and quests, the NPCs, objects, vendor stock and drops around a point with their facing, name and id clashes), called through theLoreApiinsrc/main/api/lore-api.ts. The same module has a small client for warcraft.wiki.gg (core/lore/wiki.ts): it takes afetch, only ever builds requests to that host’sapi.php, sets no headers, spaces and caches its requests, and is guarded by theAllow lookups on warcraft.wiki.ggsetting, off by default. The tools that write scenes, fights, patrols, loot and new NPCs, objects and items (tools/authoring.ts) are built onsrc/core/authoring/: the seven authoring models, each with the editor’s own zod schema, a JSON Schema generated from it, a hand-written guide (a test fails if a model gains a kind the guide does not mention) and examples built from the editor’s presets. Three workflow prompts (src/main/mcp/prompts.ts) are registered bycreateMcpServerand name real tools (a test checks they exist). The world database is never written andapplyToDevis not exposed (tests/main/mcp-tools-surface.test.tsenforces it). - The renderer (
src/renderer) is the interface. It talks to the main process only through the API thatsrc/preloadexposes; state lives in Zustand stores insrc/renderer/state. The app store (app-store.ts) is made of slices understate/app/, one per area (connection, quest, canvas, focus, links, rotations, world, export, project, history, shell), each with its own interface; what they share that is not state (debounce timers, request tokens, the undo hold) is theKitinkit.ts.
The 3D view
Section titled “The 3D view”The app is built around the World: the game world in 3D, drawn from the user’s own game client. It is where the app opens, and the quest chain is in a dock under or beside it (src/renderer/views/dock/: DockLayout, ChainDock and its ChainGraph). The code is in src/renderer/world3d/.
How it is built
Section titled “How it is built”- Files come straight from the client. The main process opens the client’s MPQ archives in the game’s own load order and serves each file at
awe-wow://file/<path>. Nothing is extracted, copied or hosted, and the repo never contains game files. - Drawing is Three.js. Terrain, doodads (trees, fences, carts) and animated models come from wowserhq/scene, copied into
world3d/scene/because the editor changes its insides. Its README lists every change from upstream. Buildings (WMO), liquids, dressed characters and the editing tools were written here or adapted from MIT-licensed projects. - Loaders run in Web Workers; the main thread only builds Three.js objects from what they return.
- World units are the server’s: yards, X north, Y west, Z up. A spawn’s
position_x/y/zis its place in the scene, with no conversion anywhere. Keep it that way. world3d.tsis the scene’s API (theWorld3Dinterface): React drives it through calls such aslookAt,select,setWorldLayerandsetPlacing, and hears back through callbacks. React components never reach into Three.js objects.World3DView.tsxwires the scene to the app;WorldWorkspace.tsxis the workspace around it (the place card, Find, Teleport, the welcome).- NPC movement is a preview, never data. The walkers in
src/core/world/walk/are pure (no Three.js): each drawn NPC’s walk plan comes from its path or wander distance (plan.ts), andwalker.tssteps it at the server’s default speeds. In the scene,scene/spawn/walkers.tsandMovementDriver.tsmove the drawn models while the oneMovementControlplays (the toolbar’s Play, paused on every open); NPCs on a docked vessel walk through its frame, and the vessel never moves. To keep Play cheap, a walker looks for its ground only while its NPC is in view, and then once per half yard walked (tuning.ts); NPCs out of range or with the NPCs layer hidden are not stepped. - Spawns are read per map tile through the
viewSpawnscall (src/core/db/view-spawns.ts): the camera’s tile and the eight round it, capped per tile.
Rules it keeps
Section titled “Rules it keeps”- Every change is a step of the project’s history. The view keeps no undo of its own: a gesture is sent to the main process as one step, and an undo hands the view a new layer to draw. A gesture that changes ten spawns is still one step. An action that cannot be undone is treated as a bug.
- Where an edit goes is decided by what it touches. The open quest’s own spawns are edited through the quest. Everything else goes into the project’s world layer (
src/core/world/layer.ts): moved and placed spawns, routes, movements, respawn times and spawn groups, each keeping what the database had at the first edit, so it can be listed, reverted one by one, flagged when the database has moved since, and exported with a revert patch. New and edited NPCs, objects and items belong to the project, not to a quest. - The database is only ever read. Edits become rows in a patch; nothing in the 3D view writes to the world database.
- One bad file never takes the view down. A model, building, texture or terrain tile that cannot be read is skipped, drawn as a stand-in where it can be, and reported by name (
scene/diagnostics.ts); the rest still draws. Real clients, especially modded ones, are messier than the format specs. - The right-click menu is built from sections, one file each under
world3d/menu/sections/, registered in order. A section says which subject it applies to and gives its items. A new ability is a new section file, not a branch in a big switch. - Frame time is a budget. Spawns are drawn within a set distance of the camera, simplified further out, and animated less often the further they are; copies of a doodad that never moves are batched into one draw call per model. Before this, Goldshire was 18,000 draw calls at one frame a second. A change that adds per-frame work should be measured in a busy place such as Goldshire, not in an empty field.
Testing it
Section titled “Testing it”npm run test:world3d opens the real 3D code in a browser (software WebGL, no graphics card) against a fake game client built by tests/world3d/fake-client.ts. The failures here are usually silent, an empty view, so write each test to fail against the old code before fixing. See Testing.
One interface
Section titled “One interface”The World and the quest chain are one workspace:
- The dock.
views/dock/DockLayout.tsxlays out the World and the Quests dock under or beside it. The dock is closed when the app starts and opens with Quests in the top bar or when a quest is opened; the World keeps one wrapper, so it never remounts. The quest editor is a centred modal over both (views/QuestEditorModal.tsx). - The focus.
state/app/focus.tsholds the one quest, and part of it, that every view agrees on. Opening a quest sets it, andworld3d/useFocusFollow.tstakes the camera to the quest’s nearest NPC or object, unless the author moved the camera after the focus was set. Selecting a giver, ender or objective of the open quest in the World (by a click or with Find) sets the focused part, and the quest’s card marks it; that selection never moves the camera. - Preferences.
preferences/store.tskeeps this computer’s preferences, such as the dock’s side and its size on each side, in local storage underacqc.preferences, never in the project file. The Settings dialog is built from the sections inviews/settings/sections.ts; a new tab is a new entry there. - Linking by drag. Dragging from one quest card’s handle to another’s makes turning in the first unlock the second.
core/links/drag-link.tsdecides whether the link is allowed (no loops, no second prerequisite), andstate/app/links-edit.tswrites it as one undo step without moving the focus. - Placing by drag. The open quest’s card lists its NPCs and objects; each row drags onto the 3D view (
world3d/chain-drop.ts,CHAIN_DRAG_TYPE) to place a spawn there, as one undo step. A row with a spawn already is marked Placed, from onequestSpawnListread of the open quest (views/dock/usePlacedParts.ts); it still drags, since a second spawn is legal. - Quest positions. The open quest’s script and fight positions and its POI are drawn as labelled pins (
world3d/scene/marker/MarkerLayer.ts, built fromworld3d/quest-markers.ts), selected and dragged like spawns.
The 2D quest map and Leaflet were removed when the World took over placing, patrols and quest positions.
From the editor to SQL
Section titled “From the editor to SQL”- Load. The World reads spawns, routes and groups for the area around the camera. Opening an existing quest reads its rows from the world database and turns them into the app’s quest model. A quest that would not read back exactly is marked unsafe to export.
- Edit. The World edits the world layer and the project’s NPCs, objects and items; modules edit the quest model. Scenes, fights, new NPCs and patrols are kept in author terms (for example “when the quest is accepted, say this”), not as rows.
- Validate. Each module reports warnings and errors, shown as badges.
- Compile. Export turns the model into rows: the project patch for the world layer and the project’s NPCs, objects and items, and each quest’s own tables, plus scripts compiled from scenes, fights and patrol actions.
- Write. The rows become an SQL patch file, or are applied to the dev database. The Changes dialog shows the same rows before you commit to either.
See What gets written to the database for the tables involved.
Interface conventions
Section titled “Interface conventions”- Creating and editing happen in centred modals. The side panel is only for previews.
- Copy is written in author terms: quests, givers, NPCs, scenes. Table and column names appear only where an admin needs them, such as the Changes dialog.

