"""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()