# 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 └── / ← 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`. - 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** — stock custom sets ship `Count=0` (verified: `wut01.set`). **Eraser, Raise/Lower, and fill are built-in toolbar actions driven by those `[GENERAL]` fields — they are not palette entries.** **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 `ml1`, `ml2` (mainlights — floating ambient, area- controllable) and `sl1`, `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 `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 `.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 | |---|---|---| | `.set` | tileset definition | CRLF, references tiles by index | | `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 | | `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 -- `) 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 `.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 `_A01_01` (grid position). Keep a code→resref map. | | `VIS_` objects | Renderable trimesh children of AuroraBase | | `WOK_` objects | The AABB walkmesh; assign walkable surfacemat (Stone/Cobble→Stone) | | `BLOCK_` objects | Non-walkable triangles in the walkmesh (blocker surfacemat) | | `MARK__*` / `TILE_` 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 `/` 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 ` and ship the `.tga`. - **Toolset needs `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._