## Pre-bakes M2 doodad models into PackedScene (.tscn) files for fast runtime loading. ## ## Usage: ## godot --headless --path --script res://src/tools/bake_m2_cache.gd -- \ ## --map Azeroth --extracted res://data/extracted --output res://data/cache/m2_glb --force ## Add --only-pattern waterfall to rebuild only matching model paths. ## Add --glb-animations to also emit animated GLB scenes through src/tools/m2_to_gltf.py. extends SceneTree const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd") func _initialize() -> void: var args := OS.get_cmdline_user_args() var map_name := _arg(args, "--map", "Azeroth") var extracted := _res(_arg(args, "--extracted", "res://data/extracted")) var output_dir := _res(_arg(args, "--output", "res://data/cache/m2_glb")) var force := args.has("--force") var glb_animations := args.has("--glb-animations") var only_patterns := _arg_values(args, "--only-pattern") var python_exe := _arg(args, "--python", "python") var converter := _res(_arg(args, "--converter", "res://src/tools/m2_to_gltf.py")) if not ClassDB.class_exists("ADTLoader") or not ClassDB.class_exists("M2Loader"): push_error("GDExtension not loaded. Rebuild first.") quit(1) return DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(output_dir)) # Collect unique M2 paths from all ADT tiles var unique: Dictionary = {} var adt_loader = ClassDB.instantiate("ADTLoader") var map_dir := extracted.path_join("World/Maps/%s" % map_name) var dir := DirAccess.open(map_dir) if dir == null: push_error("Cannot open map dir: %s" % map_dir) quit(1) return print("Scanning ADT tiles...") var scanned := 0 for fname in dir.get_files(): if not fname.ends_with(".adt"): continue var parts := fname.trim_suffix(".adt").split("_") if parts.size() != 3 or parts[0] != map_name: continue var data: Dictionary = adt_loader.call("load_adt", ProjectSettings.globalize_path(map_dir.path_join(fname))) if data.is_empty(): continue for rel in data.get("m2_names", PackedStringArray()): var norm := str(rel).replace("\\", "/").to_lower() if norm.ends_with(".mdx") or norm.ends_with(".mdl"): norm = norm.get_basename() + ".m2" if not norm.is_empty() and _matches_only_patterns(norm, only_patterns): unique[norm] = true scanned += 1 print("Found %d unique M2 models from %d tiles." % [unique.size(), scanned]) var m2_loader = ClassDB.instantiate("M2Loader") var baked := 0 var baked_glb := 0 var skipped := 0 var failed := 0 var failed_glb := 0 var total := unique.size() var i := 0 for rel_path in unique.keys(): i += 1 var stem: String = rel_path.get_basename() var out_path := output_dir.path_join(stem + ".tscn") var out_glb_path := output_dir.path_join(stem + ".glb") var abs_m2 := ProjectSettings.globalize_path(extracted.path_join(rel_path)) if not FileAccess.file_exists(abs_m2): failed += 1 continue if not force and ResourceLoader.exists(out_path): if glb_animations: if _bake_glb_animation_cache(python_exe, converter, abs_m2, output_dir, out_glb_path, false): baked_glb += 1 else: failed_glb += 1 skipped += 1 continue var data: Dictionary = m2_loader.call("load_m2", abs_m2) if data.is_empty() or data.get("vertices", PackedVector3Array()).is_empty(): failed += 1 continue var node: Node3D = M2_BUILDER_SCRIPT.build(data, extracted) if node == null: failed += 1 continue _set_owner_recursive(node, node) var scene := PackedScene.new() var err := scene.pack(node) node.free() if err != OK: push_warning("pack failed for %s: %d" % [rel_path, err]) failed += 1 continue DirAccess.make_dir_recursive_absolute( ProjectSettings.globalize_path(out_path.get_base_dir())) err = ResourceSaver.save(scene, out_path) if err != OK: push_warning("save failed for %s: %d" % [out_path, err]) failed += 1 continue baked += 1 if glb_animations: if _bake_glb_animation_cache(python_exe, converter, abs_m2, output_dir, out_glb_path, force): baked_glb += 1 else: failed_glb += 1 if i % 50 == 0 or i == total: print("[%d/%d] baked=%d glb=%d skipped=%d failed=%d glb_failed=%d" % [ i, total, baked, baked_glb, skipped, failed, failed_glb]) print("Done. baked=%d glb=%d skipped=%d failed=%d glb_failed=%d" % [ baked, baked_glb, skipped, failed, failed_glb]) quit(0) func _arg(args: PackedStringArray, name: String, default: String) -> String: var idx := args.find(name) if idx >= 0 and idx + 1 < args.size(): return args[idx + 1] return default func _arg_values(args: PackedStringArray, name: String) -> PackedStringArray: var result := PackedStringArray() for i in args.size(): if args[i] != name or i + 1 >= args.size(): continue for part in String(args[i + 1]).split(",", false): var value := part.strip_edges().to_lower() if not value.is_empty(): result.append(value) return result func _matches_only_patterns(path: String, patterns: PackedStringArray) -> bool: if patterns.is_empty(): return true var lower := path.to_lower() for pattern in patterns: if lower.contains(pattern): return true return false func _res(path: String) -> String: if path.begins_with("res://") or path.begins_with("user://"): return path return "res://" + path.trim_prefix("./").trim_prefix("/") func _bake_glb_animation_cache( python_exe: String, converter: String, abs_m2: String, output_dir: String, out_glb_path: String, force: bool) -> bool: var abs_out_glb := ProjectSettings.globalize_path(out_glb_path) if not force and FileAccess.file_exists(abs_out_glb): return true var abs_converter := ProjectSettings.globalize_path(converter) var abs_output := ProjectSettings.globalize_path(output_dir) if not FileAccess.file_exists(abs_converter): push_warning("M2 GLB converter not found: %s" % converter) return false var stdout := [] var exit_code := OS.execute( python_exe, [abs_converter, abs_m2, abs_output], stdout, true, false) if exit_code != 0: push_warning("animated GLB bake failed for %s exit=%d\n%s" % [ abs_m2, exit_code, "\n".join(stdout)]) return false return FileAccess.file_exists(abs_out_glb) func _set_owner_recursive(node: Node, owner_root: Node) -> void: for child in node.get_children(): child.owner = owner_root _set_owner_recursive(child, owner_root)