# The campaign modder's tools: overview, startpos, crashes and tests

> Where do you start when you want to mod a Total War: Warhammer III campaign map? This page is the overview: each tool, what it is for and its guide; the words of the trade; then what depends on no tool in particular: the start position, crashes, automated test games. Written after four days porting a complete map from Warhammer I to Warhammer III.

Page: https://bretonia.dev/atelier/en/tools/ · Atlas of Bretonnia, modders' workshop · updated on 2026-09-25. Plain-text version, for readers and AIs; the diagrams are described in words. Reviewed on 25 September 2026, update 9.0. Written against its sources, CAIME 1.0.1, RPFM 5.0.6, the Assembly Kit and the game on patch 8.1; what 9.0 changed on our project has been added, the rest has not been rechecked point by point.

## The toolbox at a glance

A campaign goes through eight families of tools. None does everything, each has its own domain, and almost all have a trap. The first four have their own guide.

- **CAIME (Campaign AI Map Editor)**: The Campaign Map Toolkit's editor, for the logical half of the map: regions, ground, climates, roads, towns, and its exports (Map Data, pathfinding, borders). ([The CAIME guide](https://bretonia.dev/atelier/en/caime/))
- **RPFM (Rusted PackFile Manager)**: Open, create and edit packs: tables, texts, scripts, images; add a unit, a faction, a lord; be driven by an AI (MCP server). ([The RPFM guide](https://bretonia.dev/atelier/en/rpfm/))
- **Terry**: The Assembly Kit's 3D terrain editor: relief, textures, vegetation, props, logical zones. ([The Terry guide](https://bretonia.dev/atelier/en/terry/))
- **BOB**: The Assembly Kit's builder: it turns sources into game files and packs, following `rules.bob` files. ([The BOB guide](https://bretonia.dev/atelier/en/bob/))
- **Dave and Tweak**: Dave (CA writes it "DaVE": `DaVE.retail.x64.exe`; the Steam launcher says *Database Editor: Dave*) edits the Assembly Kit's XML tables (the database CAIME and BOB read); Tweak is the editor of which Terry is a mode. (Steam → Library → Tools)
- **A scripting language**: Python for us: everything repetitive, declaring tables, producing layers, checking a pack, running tests.
- **WinDbg (cdb)**: Reading the game's crash dumps. `cdb.exe` lives in the package's `amd64` subfolder, outside the PATH. (`winget install Microsoft.WinDbg --scope user`)
- **The game's logs**: Knowing what the game really did: loading, scripts, rejected data. (See ["When the game crashes"](https://bretonia.dev/atelier/en/tools/#when-the-game-crashes-reading-the-clues) below)

## The words of the trade

| Word | What it means |
|---|---|
| Pack | a mod's archive (`.pack`), placed in the game's `data` folder; to play, you tick it in the launcher |
| Table, key | a table of the game's database; each row has a unique key, and a mod adds fragments (pieces of a table) to it |
| `.loc` | the displayed texts: one key per sentence |
| Startpos, ESF | a campaign's turn-0 save (`startpos.esf`), in Total War's binary format |
| Generation pack | a pack loaded only on the day the startpos is generated: it serves the `start_pos_*` tables to the game |
| `raw_data` / `working_data` | what the Assembly Kit's tools read / what they produce |
| Campaign playable area | the `campaign_map_playable_areas` row that describes a campaign's map |
| Control case | a case known to be good, rerun identically next to yours |
| Dump (`.mdmp`) | a snapshot of the game's memory at the moment of the crash |
| Null pointer | a read at address 0 (or very close to it): often, not always, the game looked for some data and did not find it |
| `%APPDATA%` | your Windows account's settings folder (type it into Explorer) |

Words specific to the map (hex, layer, swatch, lookup…) are in [the CAIME guide](https://bretonia.dev/atelier/en/caime/).

## Who does what, from database to game

The whole chain of a new map, with each step's tool and the files it produces:

*Diagram: The road map: the database, CAIME (paint, validate, export), Terry and BOB for the terrain, RPFM building the pack, the game generating turn 0, RPFM putting it back in, then testing in game, and repeat.*

In the diagram: 1 · the logical half · The database · declare map, regions, campaign · `regions` · `campaigns` · CAIME · paint the layers · `map.hex` · `tile_map.png` · CAIME · validate (11 checks) · saved · Tweak and Terry closed · project in raw_data · CAIME · export (Process) · `map_data.esf` · `pathfinding.ppd` · `borders.pbd` · `<campaign>_lookup.bmp` · 2 · the visual half · Terry · relief, textures, trees, water · `<map>.terry` · `tile_map.png ← CAIME` · BOB · a run: the terrain · `full_height_map.dds` · `tile_list.bin` · new map: BOB runs only 6 of its 15 actions · 3 · together · RPFM · build the pack: exports, terrain, tables · `my_mod.pack` · The game · generate turn 0 (two packs) · `startpos.esf` · `hlp_data.esf` · `spd_data.esf` · RPFM · put turn 0 back in the pack · `my_mod.pack` · In game · test, read the logs · fix and repeat: export again, then a new startpos

The road map. On top, CAIME builds the logical half: paint, validate, then export, once the project is saved, Tweak and Terry are closed and the project sits in raw_data. Below, meanwhile, Terry and BOB build the visual half, from the database and the tile map CAIME exports; for a new map, BOB runs only 6 of its 15 actions. RPFM builds the mod's pack; the game generates turn 0 with it and the start-table pack, then RPFM puts turn 0 back into it. Under each step, the files it produces. Every change means exporting again, then a new startpos.

- **The database**: the Assembly Kit's tables (Dave, or the XML files by script), or a pack's (RPFM).
- **The logical half**: CAIME, then its exports: [the CAIME guide](https://bretonia.dev/atelier/en/caime/).
- **The visual half**, meanwhile: Terry, then BOB: [Terry](https://bretonia.dev/atelier/en/terry/), [BOB](https://bretonia.dev/atelier/en/bob/).
- **The pack**: RPFM gathers everything: [the RPFM guide](https://bretonia.dev/atelier/en/rpfm/).
- **Turn 0**: the game itself generates the startpos from that pack (below); the files it produces go back into the pack, which you rebuild.

## The Assembly Kit in brief

The Assembly Kit is Creative Assembly's official kit (Steam → Library → Tools). Three folders: `binaries` (its programs), `raw_data` (what the tools read: XML tables, terrain, maps) and `working_data` (what they produce). **Back up before writing to `raw_data\db`**: it is the database CAIME and BOB read.

- **Dave** edits the `raw_data\db\*.xml` tables; they can also be written by script, backup first.
- **Tweak** (`binaries\tweak.modder.x64.exe`) is CA's editor. During CAIME's *Map Data* and *Dynamic Resources* exports, Tweak **and Terry** must be closed (Terry is a mode of Tweak). Don't rely on CAIME's warning: it looks for a program named `Tweak.AssemblyKit`, while Warhammer III's kit ships `tweak.modder.x64.exe`.
- **Terry**, a mode of Tweak: CA's page, written for the Warhammer I kit, presents it as the tool for campaign maps and battlefields, and states that the kit only lets you create battle tiles; we see the same in the Warhammer III kit, whose `raw_data\terrain` folder only holds battle data. While open, Terry locks `working_data`: [the Terry guide](https://bretonia.dev/atelier/en/terry/).
- **BOB** turns sources into game files and packs, following `rules.bob` files. BOB opens from the Assembly Kit's Steam launcher (*Exporter: BOB*), from its executable `bob.modder.x64.exe`, or from Terry (*Process with BOB*, Ctrl+P), which hands it the job. On the command line, its only known option is `-no_console`, the one the launcher passes: [the BOB guide](https://bretonia.dev/atelier/en/bob/).

*Diagram: Where each file lives.*

In the diagram: Where each file lives · The game · `<game>\` · `Warhammer3.exe` · started directly for the startpos · `data\` · the packs: the game's, yours · `my_mod.pack` · your mod · `campaigns\<campaign>\startpos.esf` · loose · `campaign_maps\<map>\` · hlp_data.esf , spd_data.esf , loose (for us) · the pack wins over these loose copies, startpos.esf included: put back in the pack, then removed · `script_log_DDMMYY_HHMM.txt` · the script log, when turned on · The Assembly Kit · `<Assembly Kit>\` · from Steam: <game>\assembly_kit\ · `binaries\` · its programs · `bob.modder.x64.exe` · BOB · `tweak.modder.x64.exe` · Tweak, and Terry, one of its modes · `bob*.log` · BOB's logs · `dave\DaVE.retail.x64.exe` · Dave · `raw_data\` · what the tools read · back up before writing to db\ · `EmpireDesignData\campaign_maps\<map>\` · map.hex: your project, the only place Map Data works · `working_data\` · what they produce · locked while Terry is open · `campaign_maps\<map>\` · what Process writes: map_data.esf, pathfinding.ppd… · The game's settings and logs · `%APPDATA%\The Creative Assembly\Warhammer3\` · `scripts\user.script.txt` · read at launch · after a generation, put it back as it was · `crash_report\` · `bad_mods_report.txt` · the rejected data · `*.mdmp` · the crash dumps · `logs\mp_log.txt` · how far loading got · CAIME · its settings, and the official install · `%APPDATA%\CampaignMapToolkit\Caime\` · `preferences.json` · its settings · `%LOCALAPPDATA%\CampaignMapToolkit\` · `Projects\` · the draft of a new project (Ctrl+N) · Map Data: denied here; save the project under raw_data (above) · `Tools\Release\MapDataBuilder.x64.exe` · the Map Data tool: missing from the v1.0.0 installer; depends on the version, check it

Four drawers: the game folder, the Assembly Kit, the game's settings and logs, and CAIME. In gold, your mod; in red, the trap of each place.

> **Never guessed options**: Never start BOB, Terry or Tweak with guessed options. On BOB, `-h`, `--help` or `-help` open an "Illegal option format" box that blocks the program, and a script waiting for it hangs: seen on BOB; don't try it on Terry or Tweak either. To learn a tool's options, look at what its official launcher passes to it.

## RPFM and packs in brief

A Total War mod is a `.pack` file: an archive of tables, texts (`.loc`), Lua scripts, images and models. RPFM opens the game's packs read-only and builds yours; since version 5, its MCP server lets an AI drive it. Everything is in [the RPFM guide](https://bretonia.dev/atelier/en/rpfm/), with three complete procedures: adding a unit, a faction, a legendary lord.

*Diagram: Anatomy of a pack.*

In the diagram: Anatomy of a pack · `atlas_chevaliers.pack` · your mod: an archive, placed in the game's data folder · `db/land_units_tables/` · never rename or move it (RPFM) · `atlas_land_units` · yours, with your prefix; @ or ! in front: only to modify a game row · `data__` · the game table's name: never in your pack · `text/db/` · `atlas.loc` · one key per sentence; applies to every language · `script/campaign/mod/` · `atlas_script.lua` · loaded by the game on its own · `ui/units/icons/` · `<card>.png` · the unit card: 60 × 130 · `ui/flags/<faction>/` · the flags · `ui/portraits/portholes/` · `portrait_settings*.bin` · portrait settings, per art set · `variantmeshes/variantmeshdefinitions/` · `*.variantmeshdefinition` · a new model · `campaigns/<campaign>/` · `startpos.esf` · made by the game; one per campaign · `campaign_maps/<map>/` · `hlp_data.esf · spd_data.esf` · made by the game, for a new map · `settings.rpfm_reserved · notes.rpfm_reserved` · RPFM's settings and notes, ignored by the game · Tables and .loc files: merged by key. · Other files: same path, only one wins.

A pack stores each file at its path. The fragments of one table (a _tables folder), like .loc files, are merged by key; for any other file, two files at the same path do not both load: only one wins. In gold, what the game makes; in grey, what only RPFM reads.

- **Don't overwrite a game table**: give your fragments your own name. When two fragments give the same key, the game keeps the one whose name sorts first; a fragment named like the game's file (`data__`) replaces it entirely.
- **Don't redefine a game text**: a mod's `.loc` files apply to every language.
- **In `data`, a pack wins over loose files**: a regenerated file (a startpos) only counts once it is back in the pack.

## Campaign map terrain in brief

Neither the Warhammer III kit nor the Warhammer I kit ships raw campaign terrain data. Yet Terry can open a campaign-type project: ChaosRobie proved it. For Immortal Empires Expanded, a mod he made with the CAIME team, he rebuilt the Immortal Empires map as a Terry project, and shared that project with the community. The project folder, its raster sizes, conventions and limits are in [the Terry guide](https://bretonia.dev/atelier/en/terry/); the order of BOB's runs in [the BOB guide](https://bretonia.dev/atelier/en/bob/).

*Diagram: Campaign: the order of runs.*

In the diagram: Campaign: the order of runs · for a map the game's database knows · A map unknown to the game's database (the one in CA's packs): BOB only runs 6 actions out of 15, without a message. A control case, then the community. · The relief · the next steps read it from your pack · `full_height_map.dds` · `full_logic_map.compressed_map` · Relief into your pack, the pack into the game's data folder, then close and reopen BOB · The tile map · `tile_list.bin` · `tile_mask.dds` · `patch_mask.dds` · Tile map into your pack, then close and reopen BOB · The global tile map · roads and cliffs, taken from the tile map in your pack · `global_map/tile_list.bin` · The placed objects · BOB reads CAIME's map.hex, which must match the database; redo it for every region added or removed; up to ten minutes, the tutorial says · `global_props.bin` · Generate Camera Height Map: this action crashes BOB; camera_heightmap.png is made separately (the tutorial: approximated from the heightmap, scaled).

The order from tw-modding's campaign map tutorial. BOB reads the packs in the game's data folder: after the relief, then after the tile map, the pack goes there, then you close and reopen BOB; from Terry, each Process with BOB starts a new BOB.

A full run on a campaign terrain has 15 actions (for us, about a minute once the relief is compressed, 25 minutes the first time). For a map that the game's database (the one in CA's packs) does not know, BOB runs only 6 of them, without a message; adding the map to the kit's database or to a mod pack changes nothing. We know of no official way to lift this limit: make a control case, then ask the community. The control case: a project known to be good, copied under another name; if it also stops at 6 actions, your files are not to blame.

## The start position (startpos)

The startpos (`startpos.esf`) is the turn-0 save: factions, regions, characters, armies, diplomacy. Each campaign has a single start file, and a mod cannot add to it: it replaces it entirely ("there can be only one startpos", says the [modding wiki](https://tw-modding.com/wiki/Startpos)). Two mods touching the same campaign's startpos therefore do not combine.

The `start_pos_*` tables are written in the Assembly Kit (Dave), but the game does not read its XML files. For generation, they are served to it in a **separate pack**, loaded that day only; they never go into the pack you play. Only the generated files do.

The game builds the startpos itself, in a special mode driven by the file `%APPDATA%\The Creative Assembly\Warhammer3\scripts\user.script.txt`. Three routes lead there: RPFM's *Build Startpos*, BOB's *Process start pos* action, or a `user.script.txt` written by hand. For us, only writing it by hand worked. RPFM (right-click on the pack's root → *Build Startpos*) writes the generation's `user.script.txt` itself, but puts only one `mod` line in it: the open pack's. Our generation reads two packs (the mod and the start-table pack), so we write the script by hand. An `add_working_directory` line, which we had tried, made the game exit within nine seconds without producing anything.

### Generating it

1. **The start tables**: The `start_pos_*` tables, in the game's format, in a generation pack; next to it, the up-to-date mod pack. (trap: the game does not read the kit's XML files)
2. **The game's script**: `user.script.txt` loads the packs, asks for the generation, then for the game to close. (trap: restore it afterwards)
3. **The game, in a special mode**: It creates the campaign world, saves it, then closes. Steam must be running. (trap: closed does not mean done: check the file's date)
4. **Turn 0**: The startpos and, for a new map, the AI pathfinding data, written as loose files in `data`. (`startpos.esf`, `hlp_data.esf`, `spd_data.esf`)
5. **Into the pack**: The three files join the mod's pack, which you rebuild. (trap: the pack wins over loose files)

Order matters: the mod pack first, then the generation, then the rebuilt pack. Step by step, as we do it:

1. **The mod pack**, built and placed in the game's `data` folder, up to date (scripts included): the generation reads its tables (`campaigns`, the map…) and runs its scripts.
2. **The generation pack**, also placed in `data`: it holds the `start_pos_*` tables (see "The generation pack" below).
3. **The script**: keep a copy of `user.script.txt` if it exists, then write the lines below into it: one `mod` line per pack, the generation, the closing.
4. **The generation**: with Steam open and the game closed, start the game's executable directly (`Warhammer3.exe`; for us, a `steam_appid.txt` file sat next to it, as RPFM does), then wait for it to close by itself, without touching its window.
5. **The produced files**: the game writes `<game>\data\campaigns\<campaign>\startpos.esf`; with `process_campaign_ai_map_data`, for us, `hlp_data.esf` and `spd_data.esf` in `<game>\data\campaign_maps\<map>\`. Check their date and time.
6. **The rebuilt pack**: copy these files into the mod's sources, rebuild the pack, then remove the loose copies from `data`: the pack wins over them.
7. **The clean-up**: restore `user.script.txt`; left as is, `quit_after_campaign_processing;` closes the game on every launch.

```
mod my_mod.pack;
mod my_start_tables.pack;
process_campaign_startpos my_campaign;
process_campaign_ai_map_data;
quit_after_campaign_processing;
```

*Diagram: The startpos: three places, one round trip.*

In the diagram: The startpos: three places, one round trip · The script · `%APPDATA%\The Creative Assembly\Warhammer3\scripts\` · `user.script.txt` · written by hand: one mod line per pack, then the three commands (the code above) · keep a copy before; put it back as it was after, otherwise the game closes on every launch · read at launch · The game · `<game>\` · `Warhammer3.exe` · started directly, Steam open: it loads the two packs, generates turn 0, then closes by itself · `data\` · `my_mod.pack` · the mod: campaigns, the map, the scripts; on the way back, the three files copied into its sources, then the pack rebuilt · `my_start_tables.pack` · the start_pos_* tables only; never in the pack you play · `campaigns\<campaign>\startpos.esf` · `campaign_maps\<map>\` · for us, the game wrote them here · `hlp_data.esf · spd_data.esf` · With process_campaign_ai_map_data (new map). The time varies a lot: 16 s for us (400 × 440 hexes); tw-modding's campaign map tutorial gives under a minute for spd, about five minutes for hlp on a medium map and well over an hour on a large one; RPFM announces 10 to · 30 minutes. · written loose: once back in the pack, remove them from data, the pack wins over them · Check · the three files' date and time: those of the generation. · bad_mods_report.txt : no complaint, in: `%APPDATA%\The Creative Assembly\Warhammer3\crash_report\` · scripted campaign: a new game has its intro and missions (save counter at 1)

Three places: the script in %APPDATA%, the game and its two packs in <game>\, the produced files loose in data. They go back into the mod pack, which is rebuilt; then the loose copies go.

- `process_campaign_startpos` takes **a single argument**, the campaign key.
- With `process_campaign_ai_map_data`, the game also writes the AI pathfinding data (`hlp_data.esf`, `spd_data.esf`), **essential for a new map**: without them, the game crashes while setting up the player. The time varies a lot: 16 s for us (400 × 440 hexes); tw-modding's campaign map tutorial gives under a minute for `spd`, about five minutes for `hlp` on a medium map and well over an hour on a large one; RPFM announces 10 to 30 minutes.
- To our knowledge, these commands are not publicly documented: we checked them on patch 8.1 of the game, then with update 9.0 (September 2026). Check them again after an update.
- **Loading a mod**: to play, you tick the mod in the launcher; to generate a startpos by hand, one `mod <pack>;` line per pack in `user.script.txt`. Started directly, the executable only loads the packs named this way.
- The three files go into the pack: `campaigns/<campaign>/startpos.esf`, `campaign_maps/<map>/hlp_data.esf` and `campaign_maps/<map>/spd_data.esf`.
- With RPFM (right-click on the pack's root → *Build Startpos*): the pack must be **in the `data` folder**, the campaign declared **in the pack** (`campaigns_tables`), and the game closed; everything the generation reads must be in that one pack. Through its MCP server, `build_starpos` answers "Success" in zero seconds: that means "game launched", not "startpos built"; wait for the game to close, then call `build_starpos_post` and `build_starpos_cleanup`. Details: [the RPFM guide](https://bretonia.dev/atelier/en/rpfm/).

### The generation pack

It is an ordinary mod pack, created with RPFM, used only for generation. It holds the `start_pos_*` tables in the game's format: pack tables, not the kit's XML files. For us, the campaign's `campaigns` row and the other tables the generation reads stay in the mod pack, loaded at the same time; with RPFM's *Build Startpos*, which loads a single pack, everything must be in that one.

- **For us**, it was built once from the start tables that BOB's database export writes, in the game's format, for each campaign (`raw_data\EmpireDesignData\campaigns\<campaign>\`), without the XML files or `campaigns` ([the BOB guide](https://bretonia.dev/atelier/en/bob/) describes that export). Then every row added to the kit was copied into it by script, through RPFM's MCP server.
- **Reread what BOB writes**: it had set `default` as the subtype of eight of our characters, a value the game rejects in a mod pack; we put back the kit's values.
- **Our notes do not describe** copying the kit's rows into this pack by hand, in RPFM's window. To create a pack, add a table to it and paste rows: [the RPFM guide](https://bretonia.dev/atelier/en/rpfm/).
- **Check before launching the game**: every value that points to another table (a faction, a subtype, an AI group) must exist there. Otherwise, the game's validator names the faulty record (box below).

### What a startpos requires: the crashes we went through

*Diagram: When it breaks, what is missing?.*

In the diagram: When it breaks, what is missing? · During generation · The script_log stops right after "Loading Mods", with no dump. · → start_pos_regions : rebel_faction_name and · long_description empty, no "PLACEHOLDER". · Generation crashes. · → A map_data.esf in the 0xABCA variant (not · 0xABCB ), like CA's. · When the campaign loads · A crash at the very start of loading. · → A primary slot for every settlement · ( start_pos_region_slot_templates ). · A crash while loading. · → For every faction, an AI group containing a · personality. · A crash while setting up the player (new map). · → hlp_data.esf and spd_data.esf in the pack. · On the selection screen · No lord to pick. · → The start_pos_starting_general_options · rows. · On turn 1 · A character in the map's corner, on cell (1, 1). · → Its start_pos_character_to_settlements link · (character placed at (0, 0)). · No intro, no missions. · → A startpos generated AFTER the scripts (save counter · at 1). · First: bad_mods_report.txt names the rejected row, one at a time, in: `%APPDATA%\The Creative Assembly\Warhammer3\crash_report\`

When it breaks tells you where to look: each symptom we went through, and the row or file that was missing.

- Every settlement has a **`primary`** slot (`start_pos_region_slot_templates`): without it the startpos still generates, and the game crashes on loading.
- Every faction has an **AI group** containing a personality: otherwise, a crash.
- Without **`start_pos_starting_general_options`**, no lord can be picked on the selection screen.
- In `start_pos_regions`, `rebel_faction_name` and `long_description` stay **empty**: any text (the kit's "PLACEHOLDER") crashes world creation, with no dump and no crash report: the log stops right after the mods load.
- A starting character placed at (0, 0) joins, as a garrison, the settlement that `start_pos_character_to_settlements` gives it; without that link, it appears in the corner of the map, on cell (1, 1).
- **A scripted campaign is generated AFTER its scripts**: during generation, the scripts set the save counter to 1 (`cm:is_new_game()`). With a startpos older than the scripts, every new game is taken for a saved game: no intro, no missions.
- For us, CAIME's *Map Data* export (MapDataBuilder, which relies on a kit DLL) wrote a `map_data.esf` in the ESF `0xABCB` variant, while the ones CA ships are `0xABCA`; generation crashed until it was converted. Ask the CAIME team whether your version already writes the right variant.
- Check before playing: compare the new startpos's structure with that of a startpos that loads (same blocks, same children).

> **The best startpos diagnostic tool**: Put the `start_pos_*` tables in a **mod pack**: the game's mod validator wakes up and writes `%APPDATA%\The Creative Assembly\Warhammer3\crash_report\bad_mods_report.txt`, naming the faulty table and record (`first_invalid_database_record`). It names only one at a time, but it names it. Our generation pack being a mod pack, this validator runs on every generation.

## When the game crashes: reading the clues

Five questions, in order; each "yes" tells you which file to open:

*Diagram: When the game crashes: the diagnostic tree.*

In the diagram: When the game crashes · Does the game close within 10 s? · yes → · Is Steam running? Is user.script.txt back to how it was? · no · A recent bad_mods_report.txt? · yes → · the rejected table and row, one at a time. · `%APPDATA%\The Creative Assembly\Warhammer3\crash_report\bad_mods_report.txt` · no · Does mp_log.txt stop before the campaign? · yes → · its last line tells the stage reached: database read, checksum, campaign environment. · `%APPDATA%\The Creative Assembly\Warhammer3\logs\mp_log.txt` · no · An error in the script_log, or a stop? · yes → · the faulty script and its line. Generation stopped right after "Loading Mods": some text in your start tables (PLACEHOLDER). · `<game>\script_log_DDMMYY_HHMM.txt` · when turned on: a non-empty script/enable_console_logging file in a loaded pack · no · A .mdmp dump: a read at address 0 or close by? · yes → · often, not always, some expected data is missing: look for it in your tables. Note the address with the game version. · `%APPDATA%\The Creative Assembly\Warhammer3\crash_report\*.mdmp` · no · Nothing speaks: a control case, then the community, with your logs and the dump (CAIME's Discord, tw-modding).

Five questions, in order; each "yes" tells you what the file teaches, and where it is. If nothing speaks: a control case, then the community.

- **`%APPDATA%\The Creative Assembly\Warhammer3\crash_report\bad_mods_report.txt`**: rejected data. The `.mdmp` dumps are in the same folder; the `*.stack.txt` says almost nothing, the dump does.
- **`%APPDATA%\The Creative Assembly\Warhammer3\logs\mp_log.txt`**: how far loading got (database read, checksum, campaign environment created).
- **The script log**: a non-empty `script/enable_console_logging` file in a loaded pack, and the game writes `script_log_DDMMYY_HHMM.txt` in its folder (an empty file is ignored). Clicks, even simulated ones, are logged there with the component's path.
- **The dump in the debugger**: install WinDbg (`winget install Microsoft.WinDbg --scope user`). `cdb.exe` is not on the PATH: PowerShell gives the package folder with `(Get-AppxPackage Microsoft.WinDbg*).InstallLocation`, and `cdb.exe` is in its `amd64` subfolder. Then, in PowerShell, `$cdb = "<folder>\amd64\cdb.exe"` (`<folder>`: that package folder), and `& $cdb -z <dump.mdmp> -c ".ecxr; k 40; q"`. Without `.ecxr`, you are looking at the crash reporter's thread, not the faulting one.
- **Reading the verdict**: code `0xC0000005` with a read at a null or near-null address is a null pointer: **often, not always, some expected data is missing**. Look first in your tables for what the game did not find.
- **File crashes by test case and address**: the address depends on the campaign and on the game version; note the game version next to each address. After an update, old addresses no longer compare.
- **Steam must be running**: otherwise the game closes within seconds, and it looks like a crash.

*Diagram: Reading a dump in four steps.*

In the diagram: Reading a dump · The dump: a .mdmp file · the game writes it here when it crashes: `%APPDATA%\The Creative Assembly\Warhammer3\crash_report\*.mdmp` · the *.stack.txt next to it says almost nothing; the dump does. · The reader: cdb.exe, from WinDbg · `winget install Microsoft.WinDbg --scope user` · cdb.exe , not on the PATH, is in the amd64 subfolder of the folder PowerShell gives: `Get-AppxPackage Microsoft.WinDbg*` · InstallLocation: the package's <folder> · The command, in PowerShell · `$cdb = "<folder>\amd64\cdb.exe"` · `& $cdb -z <dump.mdmp> -c ".ecxr; k 40; q"` · .ecxr : the faulting thread · k 40 : the stack · q : quit · Without .ecxr , wrong thread: you are reading the crash reporter's. · The output, schematic · `(…): Access violation - code c0000005` · `0:000> .ecxr` · `rax=… rcx=0000000000000000` · `…` · `Warhammer3!…+0x…:` · `mov rax,qword ptr [rcx]` · `0:000> k 40` · `00 Warhammer3!…+0x…` · `01 Warhammer3!…+0x…` · c0000005 : an access violation, on a read or a write. · rcx is 0 and the instruction reads [rcx] : a read at a null or near-null address: a null pointer. · the crash address, to note with the game version. · Null pointer: often, not always, some expected data is missing. Look for it in your tables first.

Four things to know: where the dump is, where cdb is, the command, and what the output says. The output is schematic: the addresses depend on the crash and on the game version.

## Testing without playing: automated games

To know whether a campaign holds for thirty turns, you don't have to play them. Our method:

- A **separate test pack** (created for the test, removed afterwards) holds a Lua script that clicks through the interface like a player: menu, campaign, lord, game start, end turn. Components are addressed by their path in the interface.
- `all_players_ai;` in `user.script.txt` also hands the player's turns to the AI; for us it neither recruited nor built for them: this mode tests stability, not the player's faction. To our knowledge, this command is not publicly documented: checked on patch 8.1 and update 9.0.

*Diagram: Starting an automated game, click by click.*

In the diagram: Starting an automated game · During the test, nobody touches the PC: a human click skews it. · A separate test pack · created for the test, removed afterwards; two scripts: `script\frontend\mod\` · the menu · `script\campaign\mod\` · the game · A new name: a file already loaded is skipped silently. · loaded as for the startpos, one mod line per pack: %APPDATA%\The Creative Assembly\ · Warhammer3\scripts\user.script.txt: `mod my_mod.pack;` · `mod my_test.pack;` · `all_players_ai;` · all_players_ai , optional: the AI also plays for the player, but neither recruits nor builds for them, and no mission is issued. · Checked on patch 8.1 and update 9.0. · Steam open, Warhammer3.exe started directly; user.script.txt kept before, put back after. · The menu, click by click · `main > button_campaign` · `main > button_start_campaign_new` · `campaign_select_new` · `> CcoCampaignMapPlayableAreaRecord…` · `> button_campaign_entry` · if the campaign list shows; "…": your campaign entry's number · `lord_select_list > list_box > *` · `> lord_button` · lord_key property = the character's id in start_pos_characters ; the list fills in 20 to 45 s · `campaign_select_new > button_start_parent` · `> button_start_campaign` · campaign_key property · Not frontend.start_campaign (the menu's Lua launch function): for us, it closed the campaign within seconds.

A separate test pack, loaded through user.script.txt as for the startpos, then the menu, click by click: each component is addressed by its path in the interface.

- At the end: a turn report, the `script_log` read through, the dumps if any. Then `user.script.txt` restored and the test pack removed, even after a forced stop.
- **The limits**: in "AI plays everything" mode, no mission is issued to an AI. This mode tests stability (AI, invasions, dilemmas); a lord's story and mechanics are tested in player mode.
- **An event that never arrives**: on our map, `FactionTurnStart` reached no script, under `all_players_ai` as in player mode, while `FactionBeginTurnPhaseNormal` arrived every turn. The cause: in the released game, the conditions of all of an event's listeners are evaluated in a single loop, without protection; a condition that errors cancels the event for every script, CA's and yours alike, without writing anything to the log.
- **The culprits, for us**: once the listeners' errors were caught and named in the log, three CA listeners showed up (`wh3_campaign_bonus_values.lua`), two for Mother Ostankya and one for Yuan Bo, whose condition failed at every turn start: they expect Kislev and Cathay systems that our campaign does not load. With their errors caught, the event came back (a three-turn test, the AI playing everything). The chosen remedy, removing these listeners by name when the campaign starts (`core:remove_listener`) rather than creating dummy variables, has not been checked in a game yet. And a test only proves a listener through a log line written by that listener.
- For us, `frontend.start_campaign` (the menu's Lua launch function) closed the campaign within seconds: launch through the interface, like a player.
- On turn 1, the end of turn only goes through after closing the notifications one by one.
- **The camera at playing height**, set on the leader from the first turn: a test must reproduce what a player experiences. For us, the rendering crashes whose camera was logged all happened with the camera very high, and none was seen in normal play.
- A test script that must load for sure goes into `script\campaign\mod\`, under a name no other file already loads: a file already loaded is skipped without a message.
- **Nobody touches the PC during the test**: a human click skews it.
- Always a **control case**: a case known to be good, rerun identically (same command, same options), next to yours.
- **A crash that comes and goes**: at least ten runs per variant before concluding; until then, say "lead", not "cause".

*Diagram: During the automated game: loading, the turn loop, the report.*

In the diagram: During the game · The campaign opens · `custom_loading_screen > bottom_parent` · `> button_continue` · Camera set on the leader, at playing height: the test must reproduce what a player experiences. · The turn loop · on turn 1, first close the notifications, one by one: `end_turn_docker > notification_frame` · `> button_skip` · × each notification · `hud_campaign > faction_buttons_docker` · `> button_end_turn` · Player mode: the loop on every turn. all_players_ai mode: turn 1 only, later turns play by themselves. · The report · turns played, script_log errors, dumps; then user.script.txt put back as it was and the test pack removed, even after a forced stop. · Control case: a case known to be good, same command, options included. · A crash that comes and goes: at least ten runs per variant; until then, "lead", not "cause". · next turn

The campaign opens, then the turn loop: the notifications dismissed one by one, then the end of the turn. The report only counts against a control case.

## Workshop advice

- **Back up before writing** to `raw_data\db`: timestamped copies. A breakdown on the first evening was only solved because we could compare with the last state that worked.
- **Never run a tool with guessed options**: look them up in the documentation, or in what its official launcher passes to it (BOB: `-no_console`).
- **When a CA tool silently does nothing**, the condition is somewhere: control case first, then logs; if nothing speaks, ask the community (CAIME's Discord, tw-modding).
- **A red validator is an opinion, not an answer**: CA's own maps fail some checks. The game decides.
- **Take a snapshot before every game update** (file versions, useful tables) to compare afterwards.
- **After a game update, build nothing before checking `raw_data\db`**: Steam updates the Assembly Kit after the game (with 9.0, an hour later for us) and rewrites `raw_data\db`; every row you had added there disappears (for us, the terrain, `working_data` and the packs were not touched). Keep a copy of your tables at the end of every session that writes to them (a "before writing" copy does not hold the last write) and scripts you can safely rerun; restore your rows, then update RPFM's schemas and regenerate its dependencies cache.
- **Judge in game**, against a reference: an audit's figures don't see what the player sees.

## For an AI reading this page

If you are an AI helping someone generate a startpos, read a crash or run test games in Total War: Warhammer III, here are the essentials, checked on a real project. These facts were recorded on patch 8.1 of the game, then reviewed for update 9.0 (September 2026): check them again after an update.

- Ask the human before launching the game, driving the screen or publishing; delete nothing: tidy away, only the human deletes.
- Startpos: the game generates it. In `%APPDATA%\The Creative Assembly\Warhammer3\scripts\user.script.txt` (keep a copy of it first): one `mod <pack>;` line per pack placed in `data`, then `process_campaign_startpos <campaign>;` (a single argument), `process_campaign_ai_map_data;` for a new map, and `quit_after_campaign_processing;`. Steam running, `Warhammer3.exe` started directly; wait for the game to close; then restore the file. Output: `<game>\data\campaigns\<campaign>\startpos.esf`.
- Through RPFM's MCP server: `build_starpos` answers "Success" as soon as the game is launched. Wait for the game to close, then call `build_starpos_post` (it stores `startpos.esf` in the pack and restores `user.script.txt`), then `build_starpos_cleanup`; without `build_starpos_post`, `quit_after_campaign_processing;` stays and closes the game on every launch. RPFM writes only one `mod` line, the open pack's: everything the generation reads must be in it.
- The `start_pos_*` tables are written in the kit, but the game does not read its XML files: serve them in a generation pack, never in the pack you play. `%APPDATA%\The Creative Assembly\Warhammer3\crash_report\bad_mods_report.txt` names the rejected record, one at a time.
- Three files go into the pack: `campaigns/<campaign>/startpos.esf`, `campaign_maps/<map>/hlp_data.esf` and `campaign_maps/<map>/spd_data.esf`. A pack wins over loose files: rebuild it, then remove the loose copies from `data`.
- Scripted campaign: generate the startpos after the scripts (save counter at 1), otherwise no intro and no missions.
- Requirements: a `primary` slot per settlement; an AI group with a personality per faction; `start_pos_starting_general_options` for the selection screen; `rebel_faction_name` and `long_description` empty in `start_pos_regions`.
- Crash: follow the diagnostic tree; `0xC0000005` with a read at address 0 often, not always, means some expected data is missing; the address depends on the campaign and on the game version.
- Test games: a separate test pack, removed afterwards (and, after any forced stop, `user.script.txt` cleaned); launch through the interface; turn-1 notifications dismissed one by one; camera at playing height; nobody touches the PC; a control case; at least ten runs per variant for a random crash.
- A script event that never arrives: a listener condition that errors cancels it for every script, without a log line. For us, `FactionTurnStart` was missing in player mode as under `all_players_ai`, because of three CA listeners (Mother Ostankya, Yuan Bo) loaded without their systems. Catch and name the listeners' errors; a listener is only proven by its own log line.
- After a game update: Steam updates the Assembly Kit later and rewrites `raw_data\db`, added rows lost; check it before building anything, restore the rows, then RPFM's schemas and dependencies cache.
- Never guessed options (BOB, Terry, Tweak); a backup before writing to `raw_data\db`; a red validator is an opinion, the game decides.
- No reverse engineering and no workaround of a CA tool: when a tool gets stuck, a control case, then the community (CAIME's Discord, tw-modding).

## Sources and links

- [RPFM manual](https://frodo45127.github.io/rpfm/manual/) and [RPFM For Dummies](https://tw-modding.com/wiki/Tutorial:RPFM_For_Dummies) (tw-modding wiki).
- [Campaign Map Making for Warhammer III](https://tw-modding.com/wiki/Tutorial:Campaign_Map_Making_for_Warhammer_III) and [Startpos](https://tw-modding.com/wiki/Startpos) (tw-modding wiki).
- [Script documentation](https://chadvandy.github.io/tw_modding_resources/), maintained by Vandy.
- [Terry introduction](https://wiki.totalwar.com/w/TWW_Assembly_Kit_Terry_Intro) (written for the Warhammer I kit) and [Rules.bob](https://wiki.totalwar.com/w/Rules.bob_Documentation.html) (Creative Assembly wiki).
- [CAIME documentation](https://tw-campaign-map-modding-team.github.io/CampaignMapToolkit/).
- Our guides: [CAIME](https://bretonia.dev/atelier/en/caime/), [RPFM](https://bretonia.dev/atelier/en/rpfm/), [Terry](https://bretonia.dev/atelier/en/terry/), [BOB](https://bretonia.dev/atelier/en/bob/), [modding with an AI](https://bretonia.dev/atelier/en/ai/).

## Frequently asked questions

### Do I need the Assembly Kit to mod Warhammer III?

For tables and scripts, RPFM is enough. For a campaign map, yes: CAIME reads its database, Terry and BOB build the terrain, and the startpos tables are written there.

### Which guide should I start with?

For a map: the CAIME guide. For units, factions or lords: the RPFM guide. For 3D terrain: Terry, then BOB. For working with an AI: the AI guide.

### Where does the game write the startpos it has just generated?

In `<game>\data\campaigns\<campaign>\startpos.esf`; for us, `hlp_data.esf` and `spd_data.esf` in `<game>\data\campaign_maps\<map>\`. Copy them into the mod, rebuild the pack, then remove these loose copies from the `data` folder.

### Why does my new startpos change nothing in game?

Because the pack wins over loose files: put the startpos back into the pack, rebuild it and remove the loose copy from the `data` folder.

### The campaign starts without its intro or missions: why?

Often the startpos is older than the scripts: regenerate it with the up-to-date pack (the save counter must be 1). In `all_players_ai` mode, no mission is issued: that is normal.

### My FactionTurnStart listener never fires: why?

Perhaps not because of it: a listener condition that errors, even in a CA script, cancels the event for everyone, without writing anything to the log. For us, three listeners for Mother Ostankya and Yuan Bo, deprived of their systems on our map, cut it off in player mode as under `all_players_ai`. Catch and name the listeners' errors to find the culprit. `FactionBeginTurnPhaseNormal`, for its part, arrived every turn: our turn-start listeners moved to it.

### My Assembly Kit rows disappeared after an update: why?

Steam updates the Assembly Kit after the game (for us, an hour later, with 9.0) and rewrites `raw_data\db`: added rows disappear; the terrain, `working_data` and the packs were not touched. Restore your rows from your copies, or rerun the scripts that write them, before building anything; then update RPFM's schemas and regenerate its dependencies cache.

### No lord shows on the selection screen: why?

The `start_pos_starting_general_options` rows are missing: they link a startpos character to a lord card (`frontend_faction_leaders`). Add them, then regenerate the startpos.

### My startpos generates, but the campaign crashes on loading: what should I check?

A `primary` slot for every settlement, an AI group with a personality for every faction, `hlp_data.esf` and `spd_data.esf` in the pack for a new map, and the `%APPDATA%\The Creative Assembly\Warhammer3\crash_report\bad_mods_report.txt` file.

### Why does the game close by itself on launch?

Often a `user.script.txt` left over from a startpos generation (`quit_after_campaign_processing;`), or Steam not running.

### Why do my texts replace the game's in every language?

The game loads a mod's `.loc` files whatever the language. Only add new keys, and make one pack per language.

### Where are the logs?

`%APPDATA%\The Creative Assembly\Warhammer3\crash_report\bad_mods_report.txt` (with the `.mdmp` dumps next to it), `%APPDATA%\The Creative Assembly\Warhammer3\logs\mp_log.txt`, the script log (`script_log_*.txt`) in the game folder, and BOB's logs in the kit's `binaries` folder.

### How do I turn on Warhammer III's script log?

Put a non-empty `script/enable_console_logging` file in a loaded pack: the game then writes `script_log_*.txt` files in its folder.
