Files
sow-module-tilesets/AGENTS.md
T
archvillainetteandClaude Opus 5 bc57394290 docs: adopt workspace-wide agent standards and retire sow-docs
Scaffold the per-repo configuration the engineering skills expect, and put
the three-way split of work into standing law:

- Live work -> wayfinder maps + Gitea issues (closeable, assignable,
  queryable).
- Settled decisions -> ADR files in docs/adr/ (immutable, findable, never
  closed).
- Standing law -> DOCTRINE.md / AGENTS.md / CONTEXT.md.

Adds docs/agents/{issue-tracker,triage-labels,domain}.md and an
"Agent skills" section in AGENTS.md. The tracker doc records Gitea via the
tea CLI, the wayfinder map/ticket/blocking operations, and the rule that
issues follow ownership: file work in the repo that owns it, not the one you
happen to be standing in.

ADR-0001 (markdown is reference, law, or an ADR - nothing else) is adopted
from sow-codebase so the rule is local to every repo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 20:32:33 +02:00

31 KiB
Raw Blame History

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), 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 cornersTopLeft, 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 edgesTop, 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 → ~67 tiles minimum (1 pure A, 1 pure B, 45 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 23 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.
  • "Raised" is a height, NOT a terrain type. Verified vs tcn01: every raised corner is still cobble with …Height=1; only cobble ever carries a non-zero height (water/building stay at 0). Raise/Lower is the toolbar height tool acting on cobble corners — do not add a raised entry to [TERRAIN TYPES]. Making it paintable requires three things together: HasHeightTransition=1, a set of ungrouped transition tiles covering the cobble:0 ↔ cobble:1 corner patterns (ramps / retaining edges), and the [PRIMARY RULES] that let the brush step a corner's height. Any one missing and Raise/Lower is inert.
  • 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 even for multi-terrain paint — verified: stock wut01.set paints 6 terrains with [PRIMARY RULES] Count=0. What actually makes a terrain paintable is having a [TILE#] for every corner pattern the brush can request (all rotations), each ungrouped. Rules just smooth cascading auto-transitions; stock tcn01 (City, 5 terrains + heights) ships 76 for polish, but you can ship Count=0 and add them later. Eraser, Raise/Lower, and fill are built-in toolbar actions driven by those [GENERAL] fields — they are not palette entries.

The random-fill pool (verified vs tcn01, 2026-07-04). For a given corner pattern the brush picks a random tile among the ungrouped tiles that matchmembership in any [GROUP#] removes a tile from that pool. So the ungrouped same-corner tiles are the fill: the cobble the Default terrain spawns a new area with, the variants the brush scatters, the tile the eraser resets to. Consequences that bite:

  • Grouping every tile empties the Default pool → the New Area wizard null-derefs (Access violation … Write of 00000040). Keep ≥1 ungrouped tile per paintable corner pattern.
  • Every terrain/transition tile must stay UNGROUPED to take part in painting. [GROUP#] is only for click-place prefabs (streets, bridges, statues) that the builder should not get at random. A "canal edge" you want the brush to auto-lay is a terrain tile (ungrouped), not a feature.
  • tcn01's 513 tiles collapse to 58 distinct corner-tuples — the surplus is art variety. A geometry-only set needs roughly one tile per corner-tuple, and one model can serve all four rotations of a tuple as four [TILE#] entries differing only in Orientation (0/90/180/270) and the correspondingly-rotated corners/edges (the engine rotates model and walkmesh together).

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 STRREFs 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 <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; 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
<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.x4.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) — GUI .set editor; BioWare GFF Editor — edit .itp/GFF; NWN Explorer Reborn — browse/extract stock BIFs (pull a reference .set/.itp); Basic Interior Tileset Template (Symphony) — 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 -- <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 sourcenix 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. Packnwn_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.
  • 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 <resref≤16> and ship the .tga.
  • Toolset needs <set>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

Verify specifics against the wiki before relying on them; correct this file when you find drift.

Agent skills

Issue tracker

Issues live in Gitea at git.westgate.pw (ShadowsOverWestgate/sow-module-tilesets), managed with the tea CLI. Issues follow ownership — file work in the repo that owns it, not the one you happen to be standing in. See docs/agents/issue-tracker.md.

Triage labels

Default label vocabulary (needs-triage, needs-info, ready-for-agent, ready-for-human, wontfix). See docs/agents/triage-labels.md.

Domain docs

Single-context: CONTEXT.md at the repo root plus docs/adr/. See docs/agents/domain.md.

Where work lives

Markdown here is reference, law, or an ADR — nothing else (sow-codebase ADR-0001). Live work -> wayfinder maps + Gitea issues (closeable, assignable, queryable). Settled decisions -> ADR files in docs/adr/ (immutable, never closed, only superseded). Standing law -> DOCTRINE.md / AGENTS.md / CONTEXT.md. Current-state reference -> docs describing what the code does now. Everything else — plans, specs, concepts, handoffs, trackers — is process: it belongs in a Gitea issue, not a file. Harvest unfinished intent to an issue before deleting a process doc. sow-docs is deprecated and read-only.