"""Generate the tileset palette `twu01palstd.itp` from tiles.py. python make_itp.py [out.itp] # default: twu01palstd.itp The toolset loads `palstd.itp` when it registers a tileset; without it the set fails to register (blank name in the New Area wizard, "could not create area"). It's a GFF `ITP ` file: MAIN = a list of palette *category* folders, each {ID byte, STRREF (dialog.tlk folder label), LIST of leaves}. We emit TWO folders (the verified structure — AGENTS.md §2.3; wut01palstd.itp): * Features (STRREF 63261): one leaf per NON-paint tile, {NAME, RESREF=tile resref}. Each leaf is placeable ONLY because make_set.py backs it with a 1x1 [GROUP#] whose Name == this NAME and Tile0 == that tile; the toolset finds the group by the RESREF (its first tile). Paint tiles (tiles.is_paint) are omitted here on purpose — they're brush-laid terrain, not click-placed features. * Terrain (STRREF 8282): the paintable vocabulary — `cobble` and `canal` (each RESREF = terrain name) plus the magic `eraser` leaf (RESREF `eraser`, STRREF 63291, no NAME). `raised` is NOT here: it's a height on cobble (Raise/Lower), deferred to the next pass with HasHeightTransition=1 + a cobble-height matrix. Multi-tile groups (folder 63262) would go in a third folder; we have none yet. Built as JSON and converted to GFF with neverwinter.nim's nwn_gff (on PATH). """ import json import os import subprocess import sys import tempfile import tiles STRREF_FEATURES = 63261 # dialog.tlk label for the "Features" palette folder STRREF_TERRAIN = 8282 # dialog.tlk label for the "Terrain" palette folder STRREF_ERASER = 63291 # the magic eraser leaf (no NAME) def leaf(name, resref): return {"__struct_id": 0, "NAME": {"type": "cexostring", "value": name}, "RESREF": {"type": "resref", "value": resref}} def eraser_leaf(): return {"__struct_id": 0, "RESREF": {"type": "resref", "value": "eraser"}, "STRREF": {"type": "dword", "value": STRREF_ERASER}} def category(cid, strref, leaves): return {"__struct_id": 0, "ID": {"type": "byte", "value": cid}, "STRREF": {"type": "dword", "value": strref}, "LIST": {"type": "list", "value": leaves}} def build(): features = [leaf(tiles.group_name(s), tiles.resref(s)) for s in tiles.TILES if not tiles.is_paint(s)] # Paintable terrains = every .set terrain EXCEPT 'raised' (which is a height on # cobble, not a terrain — the Raise/Lower tool, deferred to the next pass). paintable = [t for t in tiles.TERRAINS if t != "raised"] # -> cobble, canal terrain = [leaf(t, t) for t in paintable] + [eraser_leaf()] return { "__data_type": "ITP ", "MAIN": {"type": "list", "value": [ category(0, STRREF_FEATURES, features), # ID/STRREF mirror wut01palstd.itp category(2, STRREF_TERRAIN, terrain), ]}, } def main(): out = sys.argv[1] if len(sys.argv) > 1 else f"{tiles.PREFIX}palstd.itp" with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as tf: json.dump(build(), tf) js = tf.name try: subprocess.run(["nwn_gff", "-i", js, "-l", "json", "-k", "gff", "-o", out], check=True) finally: os.unlink(js) nfeat = sum(1 for s in tiles.TILES if not tiles.is_paint(s)) print(f"wrote {out} ({nfeat} features + paint:cobble,canal + eraser)") if __name__ == "__main__": main()