Files
sow-module-tilesets/twu01/STATUS.md
T
2026-07-05 01:46:27 +02:00

13 KiB
Raw Blame History

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 .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:

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 .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 minimapsImageMap2D refs exist in the .set but 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/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 TERRAINSTERRAINS=["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).
  1. Walk-test → tune PathNodes, crossers, corner terrains; add a tileset TLK if we later want localized palette names (local NAME strings suffice for now).
  2. 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 .tgas. Downscale to ≤2k — NWN handles 4k textures poorly (perf/artifacts). Geometry is unaffected; only the .tga/material path changes.
  3. Pack .hak (nwn_erf) once validated.