# AGENTS.md — Building NWN:EE Tilesets

Operational guide for building **Neverwinter Nights: Enhanced Edition** tilesets in
this repo. Read this before touching any tileset directory. It bridges our
Blender-prototype conventions to the hard requirements of the NWN engine and
toolset, so geometry built here converts cleanly to a shippable `.hak`.

> This is a reference, not a tutorial to read top-to-bottom. Jump to the section
> you need. When a fact here disagrees with the linked nwn.wiki pages
> ([Sources](#sources)), the wiki wins — tell the user and fix this file.

---

## 0. Repo layout

Each tileset lives in its own directory. One tileset = one 5-char prefix = one
`.set` + its models/walkmeshes/palette.

```
sow-module-tilesets/
├── AGENTS.md                 ← you are here (applies to all tilesets)
├── wgt01/                    ← Westgate Urban Foundation MVP (streets, canals, ramps)
│   ├── KICKOFF.md            ← design brief / tile list for this tileset (the spec)
│   ├── concept-art-1.png
│   ├── flake.nix             ← Blender 4.0.2 + generator entry points
│   └── generator/            ← bpy scripts that build the .blend source
└── <future-tileset>/         ← same shape
```

- **wgt01** is the *urban foundation* set: ground grammar only (streets,
  sidewalks, plazas, ramps, terraces, canals, bridges). Buildings/props are
  placeables layered on top later — **not** part of any tileset here.
- Other tilesets (interiors, dungeons, wilderness) may follow. They share these
  rules; only their `KICKOFF.md` brief and prefix differ.
- A tileset's `KICKOFF.md` is authoritative for *what tiles to build*. This file
  is authoritative for *how NWN needs them shaped and packaged*.

---

## 1. Hard constraints (memorize these)

| Constraint | Value | Consequence if violated |
|---|---|---|
| Tile footprint | **10 m × 10 m** on XY, centered at origin (X,Y ∈ [−5, +5]) | Misaligned adjacency, gaps |
| Ground level | **Z = 0**; walkable floor cannot go below 0 | — |
| Axes | **Z up, X = West→East, Y = South→North** | — |
| Resref (any model/wok/tga name) | **≤ 16 characters** | Silent truncation / breakage |
| Tileset prefix | **5 chars**, e.g. `TWU01` (Tileset Westgate Urban 01) | — |
| Walkmesh polycount | **~100 faces per tile** ideal | GMax/exporter crash, broken AABB |
| Faces per walkmesh vertex | **≤ 8** | Export failure |
| Lights per tile | **≤ 2 mainlight + ≤ 2 sourcelight** | — |
| Walkable faces | **must face upward, single level, no overlap** | Downward-facing walkable *crashes the exporter* and makes players jerk/hop |
| Walkmesh edges | exactly at **±5 m** from center | Creatures can't cross tile boundary |
| `.set` file | **INI text, DOS/CRLF line endings** | Toolset crashes on area creation |
| Area size | **≤ 32 × 32 tiles** | — |
| Module | **≤ 100 tilesets** loaded | — |

Prototype-phase relief (from `KICKOFF.md`): geometry may be simple, materials
flat, exact NWN export is **not** required for the Blender MVP. But build with
these numbers in mind so conversion is mechanical, not a rework.

---

## 2. NWN tile fundamentals

### 2.1 Corners, edges, terrains, crossers

A tile connects to neighbors by matching **corners** and **edges**:

- **4 corners** — `TopLeft, TopRight, BottomLeft, BottomRight`. Each carries a
  **terrain type** (string, no whitespace) *and* a **height** (integer number of
  transition steps above 0). Two tiles are adjacency-compatible only if the
  shared corners' terrain **and** height match.
- **4 edges** — `Top, Right, Bottom, Left`. Each carries an optional **crosser**
  (linear feature that splits the edge: road, stream, wall, canal). Blank = none.

**Terrain** = the surface a corner sits on (`cobble`, `plaza`, `canal`, `void`).
**Crosser** = a linear feature crossing an edge (`street`, `sidewalk`, `canal_edge`).
Both are referenced **by name** in the `.set` and `.itp` — names are
case-sensitive and cannot contain spaces.

Mixing terrains is expensive: every pair of terrains that can meet needs
transition tiles.
- 2 terrains → ~6–7 tiles minimum (1 pure A, 1 pure B, 4–5 transitions).
- 3 terrains → +9 tiles per added terrain.
- 4 terrains → full matrix, substantially more.
Keep the terrain/crosser vocabulary **small and deliberate** per tileset.

### 2.2 Heights

- `Transition` (in `[GENERAL]`) = meters between height step 0 and step 1.
  Decimals allowed. BioWare original content used **5 m**; custom sets use **2–3 m**.
- **wgt01 uses a raised tier at Z = 1.5 m**, so its `Transition = 1.5` and the
  raised tiles sit at corner height `1`.
- **"Raised" is a height, NOT a terrain type.** Verified vs `tcn01`: every raised
  corner is still `cobble` with `…Height=1`; only cobble ever carries a non-zero
  height (water/building stay at 0). Raise/Lower is the toolbar height tool acting on
  cobble corners — do **not** add a `raised` entry to `[TERRAIN TYPES]`. Making it
  paintable requires three things together: `HasHeightTransition=1`, a set of
  **ungrouped transition tiles** covering the cobble:0 ↔ cobble:1 corner patterns
  (ramps / retaining edges), and the `[PRIMARY RULES]` that let the brush step a
  corner's height. Any one missing and Raise/Lower is inert.
- Walkable ground can only be *raised* in whole steps, never lowered below Z=0.
  Visual water/void may dip (e.g. canal at Z = −0.3) **only under a non-walkable
  walkmesh** — never a playable under/over space (matches `KICKOFF.md`).

### 2.3 How a builder places tiles — TWO systems (this defines "paintable")

The toolset has two **independent** placement systems. Confusing them is why a
palette can list tile names that refuse to place (the wgt01 2026-07-04 symptom).

**System 1 — Paint Terrain / Paint Features (this lays the ground).** The builder
picks a **terrain** (or a **crosser**) and brushes it across the grid. The toolset
then **auto-selects which tile model to drop** by matching the four **corner
terrains + heights** (and edge **crossers**) against the neighbours — the builder
never picks a specific model. This is how streets/plazas/canals actually get laid.
It needs, in the `.set`:
- `[TERRAIN TYPES]` / `[CROSSER TYPES]` — the paintable vocabulary;
- enough `[TILE#]` entries that **every corner combination the brush can produce**
  is covered by at least one tile (else the brush has nothing to place);
- `[GENERAL]`: `Default` (terrain that fills a new area), `Border` (area edge),
  `Floor` (the terrain the **eraser** paints), `HasHeightTransition=1` (turns on
  Raise/Lower).
`[PRIMARY RULES]` only *refine* auto-picking (cascading corner transitions) and are
**optional even for multi-terrain paint** — verified: stock `wut01.set` paints **6
terrains** with `[PRIMARY RULES] Count=0`. What actually makes a terrain paintable is
having a `[TILE#]` for **every corner pattern the brush can request** (all rotations),
each **ungrouped**. Rules just smooth cascading auto-transitions; stock `tcn01` (City,
5 terrains + heights) ships **76** for polish, but you can ship `Count=0` and add them
later. **Eraser,
Raise/Lower, and fill are built-in toolbar actions driven by those `[GENERAL]`
fields — they are not palette entries.**

**The random-fill pool (verified vs `tcn01`, 2026-07-04).** For a given corner
pattern the brush picks a **random tile among the ungrouped tiles that match** —
**membership in any `[GROUP#]` removes a tile from that pool.** So the ungrouped
same-corner tiles *are* the fill: the cobble the `Default` terrain spawns a new area
with, the variants the brush scatters, the tile the eraser resets to. Consequences
that bite:
- Grouping **every** tile empties the `Default` pool → the New Area wizard
  null-derefs (`Access violation … Write of 00000040`). Keep ≥1 ungrouped tile per
  paintable corner pattern.
- **Every terrain/transition tile must stay UNGROUPED** to take part in painting.
  `[GROUP#]` is only for click-place **prefabs** (streets, bridges, statues) that the
  builder should *not* get at random. A "canal edge" you want the brush to auto-lay is
  a terrain tile (ungrouped), not a feature.
- `tcn01`'s 513 tiles collapse to **58 distinct corner-tuples** — the surplus is art
  variety. A geometry-only set needs roughly **one tile per corner-tuple**, and one
  model can serve all four rotations of a tuple as four `[TILE#]` entries differing
  only in `Orientation` (0/90/180/270) and the correspondingly-rotated corners/edges
  (the engine rotates model **and** walkmesh together).

**System 2 — the palette (`.itp`): Features & Groups only.** The right-side
"Standard" tree click-places **prefabs**: a *feature* (1×1) or a *group* (N×M). A
palette entry is placeable **only if it is backed by a `[GROUP#]` in the `.set`** —
the entry's `RESREF` is that group's **first-tile** model name and its `NAME`
matches the group's `Name`; the toolset locates the first tile in `[GROUPS]` and
pulls the rest from there. A palette entry pointing at a **raw tile with no
`[GROUP#]`** shows its name but **cannot be placed**.

**Verified palette shape** (`wut01palstd.itp`, GFF `ITP `): `MAIN` = a few
**category** structs, each `{ID byte, STRREF (dialog.tlk folder label), LIST}`;
leaves are `{NAME cexostring, RESREF}`. `RESREF` means different things per folder:

| Folder (its `STRREF`) | Leaf `RESREF` is… | Holds |
|---|---|---|
| **Terrain** (8282) | the **terrain/crosser _name_** (`Sand`, `Bridge`, …), **plus a magic `eraser` leaf** (STRREF 63291, no `NAME`) | the paintable terrains + crossers + eraser — i.e. the "Terrain" section a builder expects |
| **Features** (63261) | the feature's **first-tile resref** | 1×1 prefabs; `NAME` == its `[GROUP#]` `Name` |
| **Groups** (63262) | the group's **first-tile resref** | multi-tile prefabs (3×3, 4×1, 3×5…) |

So the "Terrain" section (paintable terrains + eraser) **is category-2 of the
`.itp`** — terrains listed by *name* with an `eraser` entry — not a list of models.
The three folder `STRREF`s above are stock dialog.tlk labels; reuse them.

**wgt01's bug (root cause):** its `.itp` was **one flat folder of 40 raw tile
resrefs** — no Terrain folder, no `eraser`, and the `.set` had `[GROUPS] Count=0`.
So none of the 40 entries is a feature/group → names render but nothing places; and
with no Terrain folder there's no paint/eraser UI at all. **Fix:** emit the
3-folder palette (Terrain+eraser / Features / Groups) and add `[GROUP#]` entries
for anything click-placeable, then lay ground via **Paint Terrain**, not by
clicking tiles. See `wgt01/STATUS.md`.

---

## 3. The tile model (`.mdl`)

ASCII MDL. Binary MDLs must be decompiled first. Structure:

### 3.1 AuroraBase (root dummy)
Mandatory root node. **Node name must equal the filename** (e.g. `twu01_a01_01`).
Set its classification/**Type = `Tile`** for any tile that has a `.wok`. Holds:
tile name, animation frames, supermodel link, and all child nodes.

### 3.2 Renderable geometry (trimesh)
The visible tile. Children of AuroraBase. Keep polys low, edges snap-clean at
±5 m, no geometry past bounds except explicitly-marked harmless trim.

### 3.3 Walkmesh (AABB node) — the `.wok`
Invisible collision + pathfinding mesh. **Parented to AuroraBase only, exactly
one AABB node per tile.** Governs walkable/non-walkable triangles, surface
material per triangle, water splashes, dynamic grass, camera collision, blood.

**The 8 walkmesh rules** (violating these crashes exporters or breaks pathing):
1. ~100 faces max.
2. ≤ 8 faces per vertex (non-walkable often exempt, still keep minimal).
3. Single-level walkable — walkable-face vertices must not overlap other
   walkable faces. Non-walkable faces unrestricted.
4. Walkable faces point **up**. Downward-facing walkable = exporter crash +
   player jerking. Downward non-walkable (ceilings) is fine.
5. One AABB node only.
6. No straight vertical edges (breaks no-overlap). Offset height changes ≥ 1 cm
   on XY per axis.
7. Origin at tile center `(0,0,0)`, matching AuroraBase. No scale/rotation
   transforms — including on raised tiles.
8. Walkmesh edges at exactly ±5 m for cross-tile traversal. Sub-cm gaps usually
   still cross; larger gaps block.

**Surface material types** (per triangle; defined globally in `surfacemat.2da`,
sounds in `footstepsounds.2da`):
`Dirt, Grass, Stone, Wood, Water, Carpet, Metal, Puddles, Leaves, Sand, Snow`.
Non-walkable proxies use a non-walkable surfacemat (e.g. the "barebones" row that
blocks walking + camera but allows spell targeting). **Surfacemat #3 (Grass)**
triggers engine dynamic grass (configured in the `.set` `[GRASS]` section).

### 3.4 Lights (dummy nodes, not real lamps)
Named `<tilename>ml1`, `<tilename>ml2` (mainlights — floating ambient, area-
controllable) and `<tilename>sl1`, `<tilename>sl2` (sourcelights — tied to
torches/fires, respond to day/night). Max 2 of each. Colors restricted to
`tilecolor.2da`.

### 3.5 Animations (on AuroraBase)
Standard 50 frames @ 30 FPS:
`animloop1/2/3` (independently toggleable mesh subsets), `Day`, `Night`,
`Day2Night`, `Night2Day` (1-hour transitions), `tiledefault` (optional fallback).
Bind individual mesh parts to an `animloopN` so one tile can serve multiple states.

### 3.6 A-node (transparency layer, optional)
Dummy `<tilename>a` parented to AuroraBase; its mesh children render in the
dynamic phase (after static meshes) for correct transparency ordering (glass,
foliage, water). Note: VFX particles are still occluded by A-node transparent
objects unless their texture's alpha = 0.

---

## 4. The `.set` file (INI, CRLF line endings)

Named `<prefix>.set` (e.g. `twu01.set`). **Strict section order:**
`[GENERAL] → [GRASS] → Terrain Types → Crosser Types → Primary Rules →
Secondary Rules → Tiles → Groups`. Tiles are referenced **by numeric index**
here (the `.itp` references the same tiles **by name** — keep them in sync or
placement silently breaks).

### `[GENERAL]`
`Name` (internal id, referenced by doortypes/loadscreens), `Type=SET`,
`Version`, `Interior` (0=exterior w/ weather+day/night, 1=interior),
`HasHeightTransition` (1 if raise/lower used), `EnvMap` (water env map),
`Transition` (meters/step, decimals ok), `SelectorHeight` (wireframe visual),
`DisplayName` (tlk ref) or `UnlocalizedName`, `Border` (edge terrain),
`Default` (terrain for new areas), `Floor` (eraser terrain).

### `[GRASS]`
`Grass` (0/1), `GrassTextureName`, `Density`, `Height` (m),
`Ambient{Red,Green,Blue}`, `Diffuse{Red,Green,Blue}`.

### Terrain / Crosser types
`[TERRAIN TYPES] Count=N`, then `[TERRAIN0..N-1]` each with `Name` (case-sensitive,
no spaces) and optional `StrRef`. `[CROSSER TYPES]` + `[CROSSER#]` identical shape.

### Primary rules (toolset auto-picking)
`[PRIMARY RULES] Count=N`, then `[PRIMARY RULE#]` with
`Placed, PlacedHeight, Adjacent, AdjacentHeight, Changed, ChangedHeight`.
When a builder paints adjacent matching terrain, the toolset swaps in the
`Changed` tile automatically. `[SECONDARY RULES]` is unused in vanilla (Count=0).

### `[TILE#]` (per-tile entry — the core)
```
[TILE0]
Model=twu01_a01_01          ; .mdl basename, ≤16 chars, == AuroraBase node name
WalkMesh=msb01              ; legacy walkmesh ref (BioWare default "msb01")
ImageMap2D=twu01_a01_01     ; minimap .tga/.dds, ≥16×16 px
TopLeft=cobble              ; corner terrain
TopLeftHeight=0             ; corner height step
TopRight=cobble
TopRightHeight=0
BottomLeft=cobble
BottomLeftHeight=0
BottomRight=cobble
BottomRightHeight=0
Top=street                 ; edge crosser (blank if none)
Right=
Bottom=street
Left=
MainLight1=0               ; tilecolor.2da index
MainLight2=0
SourceLight1=0
SourceLight2=0
AnimLoop1=0
AnimLoop2=0
AnimLoop3=0
Orientation=0              ; rotation in ±90° steps; positive = counter-clockwise
PathNode=A                 ; pathfinding descriptor (A-Z,a-z, case-sensitive)
VisibilityNode=            ; LOS; defaults to PathNode if blank (keep simpler)
Doors=0
Sounds=0
```
- **PathNode is required.** Without it, creatures and click-to-move break.
  Choose the simplest valid pattern that lets a creature cross the tile. NWN:EE
  added no new pathnode types — use the fixed set.
- **VisibilityNode** should usually be simpler than the pathnode (straight
  edge-to-edge). Omit to inherit the pathnode.
- **Padding:** reserve future slots with `Model=padding` and corners `(null)`.

### `[TILE#_DOOR#]` (only if that tile's `Doors>0`)
`Type` (doortypes.2da index; 0 = force generic from genericdoors.2da),
`X/Y/Z` (meters), `Orientation` (facing).

### Groups / Features
`[GROUPS] Count=N`, `[GROUP#]` with `Name`, optional `StrRef`, `Rows`, `Columns`,
and `Tile0..` indices **ordered bottom-left → right → up → top-right**. A
1-tile group is a **feature**. Rules:
- First tile must have a **unique** filename (ITP detects the group by it) and
  **cannot** be `-1` (random) — that crashes.
- Other positions may use `-1` for a randomized part.
- Whole group rotates as a unit; no per-tile orientation.
- Any non-group tile sharing the first tile's filename is blocked from autopicking.

---

## 5. Supporting files (packaged into the `.hak`)

| File | Purpose | Notes |
|---|---|---|
| `<prefix>.set` | tileset definition | CRLF, references tiles by index |
| `<prefix>palstd.itp` | toolset palette tree | **GFF binary**; 3 folders — Terrain+eraser / Features / Groups (§2.3). Feature/Group leaves must be backed by a `[GROUP#]`; Terrain leaves are terrain/crosser **names**. |
| `*.mdl` | tile models (ASCII) | one per tile/variant |
| `*.wok` | tile walkmeshes | baked from the AABB node |
| `*.tga` / `*.dds` | minimap icons (`ImageMap2D`) | ≥16×16 px, name ≤16 chars |
| `<prefix>edge.2da` | edge/border tiles (optional) | boundary tiles, grayed + non-walkable, prefix often `z` |
| `doortypes.2da` | tileset-specific doors | **mandatory if custom doors** — must be merged with all other sets' versions and shipped to clients, or doors render blank |
| `genericdoors.2da` | tileset-agnostic doors | index 0 = player picks |
| `surfacemat.2da` | global walkmesh surface behavior | only edit if adding materials |
| `footstepsounds.2da` | per-surface audio | |
| `tilecolor.2da` | allowed light colors | mainlight/sourcelight picker source |
| `loadscreens.2da` | area load screens (optional) | `TileSet` column binds images |
| `areag.ini` | generic area presets (optional) | ambient sound, music, scripts, PvP, interior flag |

**Name-sync gotcha:** `.set` = tiles by **index**, `.itp` = tiles by **name**.
Any mismatch breaks placement silently. Regenerate both from one source of truth.

---

## 6. Blender → NWN pipeline

This repo standardizes on **Blender 4.0.2** (`blender-402-bin`, exposed by the
NixOS overlay) specifically because the NWN toolchain (NeverBlender, cleanmodels)
tracks Blender 3.x–4.x, **not** nixpkgs' Blender 5.x. Do not "upgrade" it.

### 6.1 Tools (all on `PATH` in the dev shell)
- **NeverBlender** — Blender addon that exports NWN `.mdl` + `.wok` and defines
  node types (trimesh, **AABB walkmesh**, dummy, light). It is **not** bundled in
  the stock binary, but is **installed and enabled in this machine's Blender 4.0.2
  user config** (`~/.config/blender/4.0`) and works headless — verified via the
  flake's `blender-4.0.2 --background`. `wgt01/generator/export_mdl.py` drives it
  (see below). If working on another machine, don't assume it's present; check
  `addon_utils.enable("neverblender")` and tell the user.
  Key facts: root = Empty `nvb.emptytype='dummy'` + `nvb.classification='tile'`
  (name == resref); geometry meshes `nvb.meshtype='trimesh'`; one walkmesh mesh
  `nvb.meshtype='aabb'`. Walkmesh face surfacemat = the mesh's material *slot*
  index after `nvb_utils.create_wok_materials(mesh)` fills slots 0..22 in
  `nvb_def.wok_materials` order (walkable Stone=4, Nonwalk=7). Export:
  `scene.nvb_mdlexport(filepath=..., export_walkmesh=True)` on the active scene.
- **cleanmodels** — cleans/optimizes ASCII MDL, fixes common walkmesh problems.
- **neverwinter-nim** — CLI (`nwn_erf`, `nwn_gff`, …): pack the `.hak`/ERF, and
  read/write GFF (the `.itp` is GFF).
- **blender-mcp-nwn** (see `/nix/nixos/blender-mcp-nwn`) — MCP server exposing
  `run_blender_script`, `read_blend_metadata`, `export_for_nwn` so Claude can
  drive Blender 4.0.2 headless against these `.blend` sources.
- **Community authoring/inspection tools** (not in-repo; handy for reverse-checking
  our generated files against stock sets):
  [SetEditor 0.85 (Jlen)](https://neverwintervault.org/project/nwn1/other/tool/set-editor-beta-085)
  — GUI `.set` editor; [BioWare GFF Editor](https://neverwintervault.org/project/nwn1/other/tool/gffeditor)
  — edit `.itp`/GFF; [NWN Explorer Reborn](https://neverwintervault.org/project/nwn1/other/tool/nwn-explorer-reborn)
  — browse/extract stock BIFs (pull a reference `.set`/`.itp`);
  [Basic Interior Tileset Template (Symphony)](https://neverwintervault.org/project/nwn1/hakpak/tileset/basic-interior-tileset-template)
  — minimal working set to diff against. We can inspect any of these headlessly via
  `nwn_gff` / `nwn_erf` instead of the GUIs.
- **wgt01 generator kit** (`wgt01/generator/`, driven by `wgt01/flake.nix` apps):
  `build.py` (`nix run .#build`) builds the `.blend` tile library from the shared
  `tiles.py` table; `export_mdl.py` (`nix run .#export -- <blend> <outdir>`)
  exports real `.mdl`+`.wok` per tile via NeverBlender; `make_wok.py`
  (`nix run .#wok`) and `make_set.py` (`nix run .#set`) are pure-Python (no
  Blender) emitters — an ASCII `.wok` fallback (format validated against
  NeverBlender's own output) and a first-pass `.set`.

### 6.2 Build → export → pack
1. **Generate `.blend` source** — `nix run .#build -- out.blend` (or drive via the
   MCP server). Produces the tile library scene.
2. **Model in NWN conventions** — one AuroraBase per tile (name == resref, Type
   `Tile`), trimesh children, one AABB walkmesh, ≤2+2 lights, animations on base.
3. **Export** — NeverBlender → ASCII `.mdl` + `.wok` per tile.
4. **Clean** — run through `cleanmodels` (walkmesh sanity, tri limits).
5. **Author `.set`** — terrains, crossers, tiles (corners/edges/heights/pathnodes),
   groups. **CRLF endings.**
6. **Author `.itp`** — palette tree (GFF), names matching the `.set`.
7. **Minimaps** — render/emit `ImageMap2D` `.tga`/`.dds` per tile.
8. **Pack** — `nwn_erf` bundles `.mdl` + `.wok` + `.set` + `.itp` + minimaps +
   any 2das into `<prefix>.hak`. Ship `doortypes.2da` to clients if custom doors.

### 6.3 Testing without a `.hak` (loose files)

For iteration, skip packing: the toolset reads loose resources from
**`~/Documents/Neverwinter Nights/development/`**. Copy the `.set` + all `.mdl` +
`.wok` (and, once authored, the `.itp`/minimaps/2das) there, then create a small
area with the tileset and walk-test. Much faster than pack→reload; pack the
`.hak` only once the set is validated.

---

## 7. Mapping *our* prototype conventions → NWN

`KICKOFF.md` prototypes deliberately mirror NWN so conversion is mechanical:

| Prototype convention | NWN target |
|---|---|
| Tile centered at origin, X/Y ∈ [−5,+5], Z=0 ground, metric | Exactly NWN tile space |
| Raised tier `Z = 1.5` | Height step 1 with `Transition = 1.5` |
| Canal water `Z = −0.3`, void | Visual only, **under a non-walkable walkmesh** |
| Collection `CODE_name` (e.g. `ST_S_straight_street`) | Design ID → export resref `<prefix>_A01_01` (grid position). Keep a code→resref map. |
| `VIS_<code>` objects | Renderable trimesh children of AuroraBase |
| `WOK_<code>` objects | The AABB walkmesh; assign walkable surfacemat (Stone/Cobble→Stone) |
| `BLOCK_<code>` objects | Non-walkable triangles in the walkmesh (blocker surfacemat) |
| `MARK_<code>_*` / `TILE_<code>` empties | Helpers/edge metadata — dropped or become dummies on export |
| `MAT_debug_walkmesh` (green) | Walkable surface material |
| `MAT_debug_blocker` (red) | Non-walkable surface material |
| Edge metadata `N/E/S/W ∈ {FLAT,RAISED,CANAL,RAMP,MIXED}` | `.set` corner terrain+height (FLAT=h0, RAISED=h1) and edge crossers (CANAL=non-walkable edge, RAMP=transition tile). Document each MIXED edge. |
| `TILE_EDGE_METADATA` text block | Source for authoring `.set` corners/edges |

**wgt01 suggested prefix:** `TWU01` (Tileset Westgate Urban 01). Terrains e.g.
`cobble, plaza, foundation, canal, void, raised`; crossers e.g.
`street, sidewalk, curb, gutter, canal_edge, ramp`.

---

## 8. New-tileset checklist

1. Create `<name>/` with a `KICKOFF.md` brief (tiles, edge grammar, height plan).
2. Pick a unique **5-char prefix**; define the **terrain + crosser vocabulary**
   (keep it minimal — every terrain pairing multiplies tile count, §2.1).
3. Choose `Transition` (m) and whether `Interior=0/1`.
4. Map the **pathnode** type for each tile shape *before* modeling.
5. Copy the `flake.nix` + `generator/` pattern from `wgt01`; adjust prefix/tiles.
6. Build `.blend`, verify with `read_blend_metadata` (collection/object counts).
7. Export (NeverBlender) → clean (cleanmodels) → author `.set`/`.itp` → minimaps.
8. Pack `.hak` with `nwn_erf`; test in the toolset (create a small area, place
   every tile, walk it).

---

## 9. Gotchas (the stuff that silently breaks)

- **`.set` must be CRLF.** Unix line endings crash the toolset on area creation.
- **`.set` index vs `.itp` name mismatch** → tiles won't place, no error.
- **Resref > 16 chars** truncates silently — plan short prefixes/positions.
- **AuroraBase name ≠ filename** → tile won't load correctly.
- **Downward-facing walkable face** → exporter crash + player jerking. Keep
  walkable faces up, single-level, non-overlapping.
- **Vertical walkmesh edges / >100 walkmesh faces** → broken AABB.
- **Custom doors not in `doortypes.2da`** (and not merged/shipped) → blank doors.
- **`Transition=0`** disables adjacent placement of groups/features.
- **Group first tile `-1` or non-unique filename** → crash / no autopick.
- **NeverBlender walkmesh surfacemat** comes from the face's material *slot*
  index, **not** the material name — you must call
  `nvb_utils.create_wok_materials(mesh)` first (fills slots 0..22) then set each
  face's `material_index` to the surfacemat id. Naming a lone material `wok_Stone`
  is silently ignored (falls back to slot order). `export_mdl.py` does this right.
- **NeverBlender not installed** (other machines) → `export_for_nwn` gives you
  OBJ/FBX + an `mdl.json` config, **not** a real `.mdl`. Use `make_wok.py` for an
  ASCII `.wok` fallback, or install the addon; say so rather than claiming MDL.
- **NeverBlender flattens multi-material tiles.** A mesh with N materials exports
  as **one** `trimesh` with a single arbitrary `materialname` — the per-face
  grammar is lost. NWN wants one `trimesh` node **per texture**. Either split the
  mesh by material into separate objects before export, or emit the `.mdl`
  directly (wgt01's `make_mdl.py` does the latter, in pure Python).
- **A tile `.mdl` needs `tverts` + `bitmap`.** No UV map → NeverBlender emits no
  `tverts`; a node-based material with no image → `materialname` (a `.mtr`
  lookup) instead of a loadable `bitmap`. A trimesh with faces but no tverts/
  bitmap won't load → **blank tileset name + "could not create area"** in the New
  Area wizard. Give each material a `bitmap <resref≤16>` and ship the `.tga`.
- **Toolset needs `<set>palstd.itp`** to register a tileset. Every stock/custom
  set ships one (GFF `ITP `: `MAIN` list of category structs, each `ID` byte +
  `LIST` of tile entries). Missing → set won't register.
- **`.itp` entries need a local `NAME` cexostring, not just a `STRREF`.** Sets
  without a tileset TLK give each leaf `{NAME (cexostring), RESREF}` (verified
  vs `wut01palstd.itp`) — the `NAME` is the palette label. An entry with only
  `STRREF 0xFFFFFFFF` (or no name field) renders **blank**.
- **A named `.itp` leaf still won't place unless it's a real terrain or a
  `[GROUP#]`-backed prefab (§2.3).** The palette only *paints terrain* or
  *click-places features/groups*; it does not place raw tile models. A flat
  palette of tile resrefs (wgt01's first cut) shows names but nothing is
  selectable/paintable — you need a **Terrain** folder (terrains-by-name +
  `eraser`) and `[GROUPS]` for any click-placeable tile. Ground is laid by **Paint
  Terrain** (corner-matching auto-picks the model), not by picking tiles.
- **`WalkMesh=` in the `.set` is a label, not a file** — 95k stock tiles use
  `msb01`, others use author codes; per-tile walkmesh is matched by model name.
- **Don't upgrade Blender past 4.x** here — breaks NeverBlender/cleanmodels.

---

## Sources

- [Tileset Construction Tutorial — nwn.wiki](https://nwn.wiki/display/NWN1/Tileset+Construction+Tutorial)
- [SET (file format) — nwn.wiki](https://nwn.wiki/display/NWN1/SET)
- [Tilesets — nwn.wiki](https://nwn.wiki/display/NWN1/Tilesets)
- [NeverBlender — nwn.wiki](https://nwn.wiki/display/NWN1/NeverBlender)
- [Walkmesh — NWNWiki](https://nwn.fandom.com/wiki/Walkmesh)
- [Tile — NWNWiki](https://nwn.fandom.com/wiki/Tile)
- [Common Errors and Their Causes — nwn.wiki](https://nwn.wiki/display/NWN1/Common%20Errors%20and%20Their%20Causes)
- [SET (file format) — nwn.wiki](https://nwn.wiki/display/NWN1/SET) — terrain/crosser/rules/groups fields (§2.3, §4)
- [Tile Path Nodes — nwn.wiki](https://nwn.wiki/spaces/NWN1/pages/139689996/Tile+Path+Nodes) — pathnode types A–Z/a–z, exit-to-region, alignment
- [Tutorial: Creating Custom Tiles (Mr X) — Neverwinter Vault](https://neverwintervault.org/article/tutorial/tutorial-creating-custom-tiles) — comprehensive walkthrough (Cloudflare-gated to WebFetch; open in a browser)
- [Resources for custom NWN tileset creation — NWN Workshop](https://www.nwnworkshop.org/forums/topic/resources-for-custom-nwn-tileset-creation/) — tool/tutorial index (SetEditor, GFF Editor, NWN Explorer, templates)

_Verify specifics against the wiki before relying on them; correct this file when
you find drift._
