# wgt01 — Build Status & Notes

Progress log for the **Westgate Urban Foundation MVP** tileset (prefix `twu01`).
Spec: [KICKOFF.md](KICKOFF.md). How NWN needs things shaped: [../AGENTS.md](../AGENTS.md).

_Last updated: 2026-07-04._

---

## What exists — the generator kit (`generator/`)

One source of truth (`tiles.py`) feeds four tools; the two NWN emitters need no
Blender, so they're directly testable.

| File | Role | Needs Blender? |
|---|---|---|
| `tiles.py` | **Single source of truth**: 40-tile table, 12 materials, the 12×12 test map, and all pure derivations (paint / height / walkmesh quads / `.set` corner+crosser mapping). `PREFIX=twu01`. | no |
| `build.py` | Builds `westgate_urban_foundation_mvp.blend` — 40 tile collections (`VIS_`/`WOK_`/`BLOCK_` + edge custom props), `TILE_EDGE_METADATA` text block, `PREVIEW_tile_library_grid`, `PREVIEW_12x12_test_district`. Editable preview only (quads OK — the exporters triangulate). | yes (4.0.2) |
| `make_mdl.py` | **Shipping model path.** Pure-Python `.mdl` + `.wok` + solid-colour `.tga`: faces triangulated, one `trimesh` node **per texture** (each with `bitmap`+`tverts`), embedded `aabb` walkmesh. | no |
| `make_itp.py` | `twu01palstd.itp` (GFF `ITP ` via `nwn_gff`) — the palette the toolset needs to register the set. | no (needs `nwn_gff`) |
| `make_wok.py` | Pure-Python ASCII `.wok` emitter (verts+faces+surfacemat+AABB tree); shared `aabb_node()` reused by `make_mdl`. | no |
| `make_set.py` | `twu01.set` (INI, **CRLF**) from the tile grammar; field order/values mirror stock. | no |
| `export_mdl.py` | _Alt path, not shipped._ Neverblender export — **flattens** multi-material tiles to one unloadable material, so superseded by `make_mdl.py`. | yes + addon |

`flake.nix` exposes these as apps + a dev shell (pinned **Blender 4.0.2**,
`neverwinter-nim`, `cleanmodels`).

## Pipeline (commands)

```sh
cd wgt01
nix run .#mdl -- mdl/                   # .mdl + .wok + .tga  (shipping models)
nix run .#set -- twu01.set             # twu01.set (CRLF)
nix run .#itp -- twu01palstd.itp       # palette (needs nwn_gff)
nix run .#build -- out.blend           # optional: editable .blend preview
# nix run .#export -- out.blend mdl/   # alt Neverblender path (flattens materials)
```

## Verified (2026-07-04)

- **models (`make_mdl`)**: 40 `.mdl` + 40 `.wok` + 12 `.tga`. Each `.mdl` has
  `newmodel`, `classification TILE`, AuroraBase name == resref, one `trimesh`
  **per texture** with `bitmap`+`tverts` (tris), and the embedded `aabb`
  walkmesh. E.g. `st_s` → 3 trimesh nodes (swlk/curb/wcob); `cp1` → 1 with
  `bitmap twu01_wcob`. Walkmesh surfacemats: 4 (`wok_Stone`, walkable) / 7
  (`wok_Nonwalk`). Matches the stock `trs02` tile structure field-for-field.
- **`.itp` (rebuilt 2026-07-04, pass 4)**: `twu01palstd.itp` round-trips through
  `nwn_gff` with the **real structure** (../AGENTS.md §2.3): a **Features** folder
  (STRREF 63261) with a leaf per **non-ground** tile (37), and a **Terrain** folder
  (STRREF 8282) = paint `cobble` + the magic `eraser` leaf (STRREF 63291). Ground
  tiles (CP1/CP2/CP3) are omitted from Features on purpose — they're the painted
  fill, not click-placed. Every feature leaf is backed by a 1×1 `[GROUP#]` in the
  `.set` (Name==leaf NAME, `Tile0`==that tile). Guarded by
  `generator/test_namesync.py` (passes). **Pending GUI re-test.**
- **`.set` (pass 4)**: CRLF, 40 tiles, 3 terrains (`cobble`/`raised`/`canal`), 2
  crossers (`ramp`/`canal_edge`). `HasHeightTransition=0` (MVP paints flat cobble
  only; matches stock). **37 `[GROUP#]`, one per non-ground tile — CP1/CP2/CP3 stay
  ungrouped so they remain the random terrain-fill pool** (see Root cause below).
  `Version=V1.0`, stock field order. Corners/heights from each tile's edge grammar;
  non-walkable tiles → `PathNode=N`.
- **install path**: `~/Documents/Neverwinter Nights/development` is a symlink to
  `~/.local/share/Neverwinter Nights/development` (the toolset's real dir); one
  `nix run .#dev-install` stages all 94 loose files there.

### Why the models were rebuilt (2026-07-04, pass 2)
The Neverblender export produced tiles the toolset couldn't load — **no `tverts`,
no `bitmap`, a dangling `materialname` (>16 chars, no `.mtr`)** — and it
**flattened** each multi-material tile to a single arbitrary material. NWN needs
UV'd, bitmapped, one-texture-per-node trimeshes. Fixed by generating the `.mdl`
in pure Python (`make_mdl.py`) with full control, plus the required
`twu01palstd.itp`. Symptom that surfaced it: blank tileset name + "could not
create area" in the New Area wizard.

## Testing in the toolset — no `.hak` needed

The toolset reads loose files from **`~/Documents/Neverwinter Nights/development/`**
(confirmed present). One command does build → export → `.set` → stage:

```sh
cd wgt01
nix run .#dev-install          # -> build/ (inspectable) + installs 40 .mdl/.wok + twu01.set into development/
```

It replaces only the `twu01*` files in `development/` (leaves everything else
alone) and keeps the intermediate outputs in `./build/` for inspection. Then in
the toolset: create a small area using the `twu01` tileset, place tiles, walk-test
pathing/heights. (Packing a `.hak` with `nwn_erf` is deferred until the set is
validated.)

## First-pass caveats — tune before shipping

- **PathNode** is a placeholder (`A` walkable / `N` non-walkable) — verify creature
  routing per tile shape in the toolset; real pathnode letters matter.
- **Textures are flat solid-colour `.tga`s** (one per material, KICKOFF "geometry
  not art"). They double as an in-toolset grammar read-out; swap in real art
  later without touching geometry.
- **No minimaps** — `ImageMap2D` refs exist in the `.set` but the `.tga`s don't
  (skipped by decision; Westgate doesn't use them).
- **Substitutions**: `RA_C` = straight-ramp geometry; `STA` = smooth ramp WOK
  proxy over stepped visual; `SW_C/T/X/E` share the straight-sidewalk visual.
- `MAT_void_black` is defined but currently unpainted (11/12 materials realised).
- Corner terrain is a heuristic from edges — hand-tune adjacency in the toolset.

## Git

Branch **`wgt01-tileset-kit`** (`main` still unborn — `git branch -f main
wgt01-tileset-kit` to promote). Commits:
- `60af184` — build guide + 40-tile generator kit
- `1284f15` — real `.mdl`/`.wok` export via Neverblender

## Diagnosis 2026-07-04 — palette names show but nothing paints

Tiles now carry names, but they're **not selectable/paintable**, and the standard
**Terrain** section (paint terrains + eraser, Raise/Lower) is missing. Root cause,
confirmed against the working `wut01palstd.itp` + `wut01.set` (see ../AGENTS.md
§2.3 for the full model):

- The toolset has **two placement systems** and our `.itp` uses neither correctly.
  Ground is laid by **Paint Terrain** (the toolset corner-matches and auto-picks
  the model); the palette only **click-places `[GROUP#]`-backed prefabs**. It never
  places raw tile models.
- Our `.itp` is **one flat folder of 40 raw tile resrefs**, no Terrain folder, no
  `eraser`, and the `.set` has **`[GROUPS] Count=0`**. So: none of the 40 entries is
  a feature/group → unplaceable; and with no Terrain folder there's no paint/eraser
  UI. `wut01` (123 tiles) lists only its **21 groups** in the palette and paints the
  rest as terrain; its `PRIMARY/SECONDARY RULES` are `Count=0`, so rules aren't the
  blocker.

**The real palette (verified):** `MAIN` = 3 category structs `{ID, STRREF folder
label, LIST}`; leaves `{NAME, RESREF}`. Folder 8282 = **Terrain** (leaf RESREF =
terrain/crosser *name*, plus a magic `eraser` leaf STRREF 63291); 63261 =
**Features** (1×1, RESREF = first-tile resref); 63262 = **Groups** (N×M).

## Root cause 2026-07-04 — New Area crash (pass 3 regression, fixed pass 4)

Creating a new area threw `Access violation ... Write of address 00000040` in
`nwtoolset.exe`. **Root cause: an empty terrain-fill pool.** A grouped tile is
excluded from the toolset's random fill; pass 3 put *all 40* tiles in 1×1 groups,
so `Default=cobble` had **no** tile to fill with → null deref. Verified against
stock `tcn01` (City Exterior): of its 320 cobble-cornered tiles, 314 are group
members and only **6** (non-group `a20_*`/`o01`) form the real fill pool.

**Fix (pass 4):** ground tiles `CP1/CP2/CP3` (`tiles.GROUND_CODES`) are left
ungrouped → they are the interchangeable cobble the area spawns with and the
eraser resets to. The other 37 tiles keep their `[GROUP#]` (palette features).
Also set `HasHeightTransition=0` (matches stock; we don't paint height
transitions this pass). `test_namesync.py::test_ground_tiles_stay_in_fill_pool`
is the regression guard. This also fixes the earlier "random different tiles at
different elevations" fill — only CP1/CP2/CP3 now auto-fill.

## Pass 4 result (2026-07-04, confirmed in toolset)

✅ New Area succeeds, spawns plain cobble. ✅ Features click-place (the `[flat]`
prefabs). ✅ Eraser resets to cobble. ❌ **Canal and Raise/Lower are inert** — as
predicted by the ponytail note. Root cause (now fully understood, see AGENTS.md
§2.2/§2.3):
1. Canal & raised tiles are `[GROUP#]` features → excluded from the paint pool, so
   the brush has nothing to auto-lay.
2. Only `cobble` is exposed in the `.itp` Terrain folder → no canal brush.
3. `HasHeightTransition=0` → Raise/Lower greyed out.
4. `[PRIMARY RULES] Count=0` → the brush can't reconcile a 2nd terrain / a height
   step against neighbours.
5. `raised` is modelled as a *terrain* (`TERRAINS`), but in NWN it's a **height on
   cobble** — the model is wrong.

## Pass 5a done (2026-07-04) — CANAL is now paintable

Staged via `nix run .#dev-install`: 51 tile placements from 41 models, 33 feature
groups, matrix complete, `namesync OK`. What changed vs pass 4:
- `tiles.py`: split spec→placement (`placements()`, rotate one model into 4
  orientations); `is_paint`/`is_transition`; added diagonal-pinch tile `CE_D`;
  fixed `CE_I` corners (its geometry is 1 canal corner, edges wrongly read 3);
  `canal_paint_matrix()` completeness helper.
- `make_set.py`: emits rotated `[TILE#]` placements, groups only NON-paint tiles,
  blanks crossers on paint tiles (corner-match only), **hard-fails if the 16-pattern
  cobble/canal matrix is incomplete** (caught the CE_I bug).
- `make_itp.py`: Terrain folder = `cobble` + `canal` + eraser; Features = non-paint.
- `HasHeightTransition` still 0 (raise/lower = pass 5b).

**Re-test:** (a) is there a **canal** brush in Terrain, and does painting it lay
black-water tiles? (b) do canal/land **edges auto-appear** (straight, both corners,
diagonal) without seams or gaps? (c) does the **eraser** still reset to cobble, and
New Area still spawn cobble (no regression)? (d) do the click-place features (streets
etc.) still place? Report back — then 5b adds raise/lower on the same machinery.

## Pass 5b plan — raise/lower (next) + original full plan below

Reference: stock `tcn01` (513 tiles → 58 corner-tuples; 76 primary rules;
`HasHeightTransition=1`). Our geometry-only version needs ~1 tile per tuple.

- **Model change in `tiles.py`:** split *tile spec* (a model) from *placement* (a
  `[TILE#]` = model + `Orientation` + rotated corners/edges). One transition model
  → up to 4 placements (0/90/180/270). Drop `raised` from `TERRAINS` →
  `TERRAINS=["cobble","canal"]`; raised = `cobble` corner at `Height=1`.
- **Ungroup the terrain tiles:** canal fill, canal-edge (straight/outer/inner),
  ramp, retaining-edge (straight/outer/inner) leave `[GROUPS]` and join the paint
  pool. Streets/sidewalks/bridges/drains **stay** grouped features.
- **Corner matrix:** generate the full cobble/canal patterns and the full
  cobble:0/cobble:1 patterns (via orientation) so the brush never requests a missing
  tile. Keep canal and raised in **separate** matrices (a tile is never
  canal-corner + raised-corner) to bound the count.
- **`[PRIMARY RULES]`:** skip (stay `Count=0`). Verified not required — `wut01`
  paints 6 terrains with `Count=0`. They only polish cascading transitions; add
  later if seams need it.
- **`[GENERAL]`:** `HasHeightTransition=1`.
- **`.itp` Terrain folder:** `cobble`, `canal`, + `eraser`.
- **Guard:** a completeness self-check that fails generation if any corner pattern
  the brush can request lacks a tile (so an incomplete matrix can't ship a crash).
2. Walk-test → tune PathNodes, crossers, corner terrains; add a tileset TLK if
   we later want localized palette names (local `NAME` strings suffice for now).
3. **Textures (art pass, separate note).** Source assets live in
   `/home/vicky/Blender/` (e.g. `textures/`, `PBR_Procampur/`, `td_floor/`,
   `new_floors/`, `pd textures main lib/`). Map each `MAT_*` in `tiles.TEXTURES`
   to a chosen texture, replacing the flat solid-colour `.tga`s. **Downscale to
   ≤2k** — NWN handles 4k textures poorly (perf/artifacts). Geometry is
   unaffected; only the `.tga`/material path changes.
4. Pack `.hak` (`nwn_erf`) once validated.
