Files
sow-module-tilesets/twu01/generator/make_itp.py
T
2026-07-05 01:46:27 +02:00

88 lines
3.5 KiB
Python

"""Generate the tileset palette `twu01palstd.itp` from tiles.py.
python make_itp.py [out.itp] # default: twu01palstd.itp
The toolset loads `<set>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()