STATUS.md: what the generator kit produces, the pipeline commands, what's verified (build/export/set), first-pass caveats, git state, and next passes. AGENTS.md §6.3: test tilesets without packing a .hak by dropping loose .set/ .mdl/.wok into ~/Documents/Neverwinter Nights/development/ (toolset reads it). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
402 lines
20 KiB
Markdown
402 lines
20 KiB
Markdown
# 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`.
|
||
- 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`).
|
||
|
||
---
|
||
|
||
## 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**; references tiles **by name** — must match `.set` |
|
||
| `*.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.
|
||
- **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.
|
||
- **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)
|
||
|
||
_Verify specifics against the wiki before relying on them; correct this file when
|
||
you find drift._
|