13 KiB
wgt01 — Build Status & Notes
Progress log for the Westgate Urban Foundation MVP tileset (prefix twu01).
Spec: KICKOFF.md. How NWN needs things shaped: ../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)
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.mdlhasnewmodel,classification TILE, AuroraBase name == resref, onetrimeshper texture withbitmap+tverts(tris), and the embeddedaabbwalkmesh. E.g.st_s→ 3 trimesh nodes (swlk/curb/wcob);cp1→ 1 withbitmap twu01_wcob. Walkmesh surfacemats: 4 (wok_Stone, walkable) / 7 (wok_Nonwalk). Matches the stocktrs02tile structure field-for-field. .itp(rebuilt 2026-07-04, pass 4):twu01palstd.itpround-trips throughnwn_gffwith 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) = paintcobble+ the magiceraserleaf (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 bygenerator/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/developmentis a symlink to~/.local/share/Neverwinter Nights/development(the toolset's real dir); onenix run .#dev-installstages 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:
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 (
Awalkable /Nnon-walkable) — verify creature routing per tile shape in the toolset; real pathnode letters matter. - Textures are flat solid-colour
.tgas (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 —
ImageMap2Drefs exist in the.setbut the.tgas 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/Eshare the straight-sidewalk visual. MAT_void_blackis 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 kit1284f15— real.mdl/.wokexport 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
.itpuses 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
.itpis one flat folder of 40 raw tile resrefs, no Terrain folder, noeraser, and the.sethas[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; itsPRIMARY/SECONDARY RULESareCount=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):
- Canal & raised tiles are
[GROUP#]features → excluded from the paint pool, so the brush has nothing to auto-lay. - Only
cobbleis exposed in the.itpTerrain folder → no canal brush. HasHeightTransition=0→ Raise/Lower greyed out.[PRIMARY RULES] Count=0→ the brush can't reconcile a 2nd terrain / a height step against neighbours.raisedis 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 tileCE_D; fixedCE_Icorners (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.HasHeightTransitionstill 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). DropraisedfromTERRAINS→TERRAINS=["cobble","canal"]; raised =cobblecorner atHeight=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 (stayCount=0). Verified not required —wut01paints 6 terrains withCount=0. They only polish cascading transitions; add later if seams need it.[GENERAL]:HasHeightTransition=1..itpTerrain 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).
- Walk-test → tune PathNodes, crossers, corner terrains; add a tileset TLK if
we later want localized palette names (local
NAMEstrings suffice for now). - 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 eachMAT_*intiles.TEXTURESto a chosen texture, replacing the flat solid-colour.tgas. Downscale to ≤2k — NWN handles 4k textures poorly (perf/artifacts). Geometry is unaffected; only the.tga/material path changes. - Pack
.hak(nwn_erf) once validated.