From 1284f15595dce72112a9f47c753624bad772d3b2 Mon Sep 17 00:00:00 2001 From: vickydotbat Date: Sat, 4 Jul 2026 17:04:41 +0200 Subject: [PATCH] wgt01: real .mdl/.wok export via Neverblender Neverblender is installed + enabled in this machine's Blender 4.0.2 config and confirmed working headless via the flake. Add generator/export_mdl.py: assembles each tile as a Neverblender-native model (AuroraBase dummy/tile, trimesh, single aabb walkmesh) in its own scene and calls scene.nvb_mdlexport to emit real ASCII .mdl + .wok for all 40 tiles. Walkmesh surfacemats via create_wok_materials() slot order (walkable Stone=4, Nonwalk=7) -> verified correct per tile (CP1 all 4, CN1 all 7, BR_1/CE_S mixed). flake.nix: add `export` app (nix run .#export -- [CODE...]). AGENTS.md: record the Neverblender node/surfacemat facts and the kit's tools; note the slot-index (not material-name) surfacemat gotcha. Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 35 +++++++++--- wgt01/flake.nix | 18 ++++++ wgt01/generator/export_mdl.py | 103 ++++++++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 8 deletions(-) create mode 100644 wgt01/generator/export_mdl.py diff --git a/AGENTS.md b/AGENTS.md index a378cc0..c6ccb3e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -273,17 +273,31 @@ tracks Blender 3.x–4.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). Install a NeverBlender - release compatible with **Blender 4.0.x** into this Blender - (`Edit ▸ Preferences ▸ Add-ons ▸ Install`). It is **not** bundled in the stock - binary — an agent cannot assume it is present; check and tell the user if a - real MDL export is requested. + 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. +- **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 -- `) + 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` source** — `nix run .#build -- out.blend` (or drive via the @@ -353,9 +367,14 @@ tracks Blender 3.x–4.x, **not** nixpkgs' Blender 5.x. Do not "upgrade" it. - **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 not installed** in this Blender → `export_for_nwn` gives you - OBJ/FBX + an `mdl.json` config, **not** a real `.mdl`. Install the addon for - true MDL/WOK export; say so rather than claiming MDL output. +- **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. - **Don't upgrade Blender past 4.x** here — breaks NeverBlender/cleanmodels. --- diff --git a/wgt01/flake.nix b/wgt01/flake.nix index e73f6d4..033085d 100644 --- a/wgt01/flake.nix +++ b/wgt01/flake.nix @@ -44,6 +44,20 @@ runtimeInputs = [ py ]; text = ''exec python ${gen}/make_wok.py "$@"''; }; + + # Real .mdl + .wok export via the Neverblender addon (must be enabled in + # this Blender's user config — confirmed working headless). + mkmdl = pkgs.writeShellApplication { + name = "wgt-export"; + runtimeInputs = [ blender ]; + text = '' + blend="''${1:?usage: wgt-export [CODE...]}" + outdir="''${2:-mdl}" + shift 2 || true + exec blender-4.0.2 --background "$blend" --python ${gen}/export_mdl.py \ + --python-exit-code 1 -- "$outdir" "$@" + ''; + }; in { packages.${system}.default = build; @@ -65,6 +79,10 @@ type = "app"; program = "${mkwok}/bin/wgt-wok"; }; + export = { + type = "app"; + program = "${mkmdl}/bin/wgt-export"; + }; }; devShells.${system}.default = pkgs.mkShell { diff --git a/wgt01/generator/export_mdl.py b/wgt01/generator/export_mdl.py new file mode 100644 index 0000000..6019fd5 --- /dev/null +++ b/wgt01/generator/export_mdl.py @@ -0,0 +1,103 @@ +"""Export every tile to real NWN ASCII .mdl + .wok via Neverblender (Blender 4.0.2). + + blender-4.0.2 --background --python export_mdl.py -- [CODE ...] + +Requires the Neverblender addon enabled in this Blender (Import-Export: +Neverblender). Confirmed working headless against ~/.config/blender/4.0. For each +tile it assembles a Neverblender-native model in its own scene — an AuroraBase +(Empty, classification TILE, name == resref), a trimesh child (the VIS geometry), +and one aabb walkmesh child (from tiles.wok_quads) — then calls +scene.nvb_mdlexport(export_walkmesh=True) to emit .mdl and .wok. + +Walkmesh surface materials use Neverblender's name->surfacemat mapping +(nvb_def.wok_materials): walkable faces = wok_Stone (idx 4), blockers = wok_Nonwalk +(idx 7). Refine per tile later (wok_Water/wok_StoneBridge/etc.) if desired. +""" + +import os +import sys + +import bpy +import addon_utils + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import tiles # noqa: E402 + +# Neverblender writes each walkmesh face's material_index (slot) verbatim as the +# surfacemat id (nvb_node.py), and create_wok_materials() fills slots 0..22 in +# nvb_def.wok_materials order. So the slot index IS the surfacemat index: +WALK_SURF = 4 # wok_Stone +BLOCK_SURF = 7 # wok_Nonwalk + + +def build_wok(spec, name): + from neverblender import nvb_utils + quads = tiles.wok_quads(spec) + verts, faces, surf = [], [], [] + for quad, walk in quads: + b = len(verts) + verts += quad + faces.append((b, b + 1, b + 2, b + 3)) + surf.append(WALK_SURF if walk else BLOCK_SURF) + me = bpy.data.meshes.new(name) + me.from_pydata(verts, [], faces) + nvb_utils.create_wok_materials(me) # slots 0..22 == wok_materials order + me.update() + for poly, s in zip(me.polygons, surf): + poly.material_index = s # slot == surfacemat id on export + ob = bpy.data.objects.new(name, me) + ob.nvb.meshtype = "aabb" + return ob + + +def export_tile(spec, outdir): + ref = tiles.resref(spec) + vis = bpy.data.objects.get("VIS_" + spec["code"]) + if vis is None: + return {"code": spec["code"], "ok": False, "error": "missing VIS object"} + + scn = bpy.data.scenes.new("exp_" + spec["code"]) + bpy.context.window.scene = scn + + root = bpy.data.objects.new(ref, None) # AuroraBase; name == filename + root.nvb.emptytype = "dummy" + root.nvb.classification = "tile" + scn.collection.objects.link(root) + + v = vis.copy() + v.data = vis.data.copy() + v.name = ref + "_01" + v.nvb.meshtype = "trimesh" + scn.collection.objects.link(v) + v.parent = root + + w = build_wok(spec, ref + "_wg") # single aabb walkmesh (rule: 1 per tile) + scn.collection.objects.link(w) + w.parent = root + + mdl = os.path.join(outdir, ref + ".mdl") + bpy.ops.scene.nvb_mdlexport(filepath=mdl, export_walkmesh=True, + check_existing=False, batch_mode="OFF") + wok = os.path.join(outdir, ref + ".wok") + return {"code": spec["code"], "ref": ref, "ok": os.path.isfile(mdl), + "mdl": os.path.isfile(mdl), "wok": os.path.isfile(wok)} + + +def main(): + argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] + outdir = os.path.abspath(argv[0]) if argv else os.path.abspath("mdl") + only = set(argv[1:]) + os.makedirs(outdir, exist_ok=True) + + addon_utils.enable("neverblender", default_set=False) + + results = [export_tile(s, outdir) for s in tiles.TILES if not only or s["code"] in only] + ok = sum(1 for r in results if r["ok"]) + bad = [r for r in results if not r["ok"]] + print(f"EXPORTED {ok}/{len(results)} tiles -> {outdir}") + if bad: + print("FAILED:", bad) + + +if __name__ == "__main__": + main()