## Bakes terrain, M2 doodads and WMO objects in one pass. ## ## Usage: ## godot --headless --path --script res://tools/bake_all.gd -- \ ## --map Azeroth --force ## ## Optional args: ## --extracted res://extracted ## --terrain-output res://cache/baked_terrain_v2 ## --m2-output res://cache/m2_glb ## --wmo-output res://cache/wmo_tscn ## --full-texture-size 2048 ## --coarse-texture-size 512 ## --skip-terrain --skip-m2 --skip-wmo extends SceneTree const ADT_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/adt_builder.gd") const BAKED_TILE_SCRIPT := preload("res://resources/baked_adt_tile.gd") const WMO_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/wmo_builder.gd") const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd") var _image_cache: Dictionary = {} func _initialize() -> void: var args := OS.get_cmdline_user_args() if not ClassDB.class_exists("ADTLoader"): push_error("ADTLoader not found. Rebuild GDExtension first.") quit(1); return if not ClassDB.class_exists("WMOLoader"): push_error("WMOLoader not found. Rebuild GDExtension first.") quit(1); return if not ClassDB.class_exists("M2Loader"): push_error("M2Loader not found. Rebuild GDExtension first.") quit(1); return var map_name := _arg(args, "--map", "Azeroth") var extracted := _res(_arg(args, "--extracted", "res://extracted")) var terrain_out := _res(_arg(args, "--terrain-output","res://cache/baked_terrain_v2")) var m2_out := _res(_arg(args, "--m2-output", "res://cache/m2_glb")) var wmo_out := _res(_arg(args, "--wmo-output", "res://cache/wmo_tscn")) var full_tex_size := maxi(256, _arg(args, "--full-texture-size", "2048").to_int()) var coarse_size := maxi(128, _arg(args, "--coarse-texture-size", "512").to_int()) var force := args.has("--force") var skip_terrain := args.has("--skip-terrain") var skip_m2 := args.has("--skip-m2") var skip_wmo := args.has("--skip-wmo") 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 directory: %s" % map_dir) quit(1); return var total_start := Time.get_ticks_msec() # ── Phase 1: Terrain ───────────────────────────────────────────────────── if not skip_terrain: print("\n=== Phase 1/3: Terrain ===") _bake_terrain(map_name, map_dir, extracted, terrain_out, full_tex_size, coarse_size, force) else: print("\n=== Phase 1/3: Terrain [SKIPPED] ===") # ── Phase 2: M2 ────────────────────────────────────────────────────────── if not skip_m2: print("\n=== Phase 2/3: M2 Doodads ===") var unique_m2 := _collect_m2(map_name, map_dir) _bake_m2(unique_m2, extracted, m2_out, force) else: print("\n=== Phase 2/3: M2 Doodads [SKIPPED] ===") # ── Phase 3: WMO ───────────────────────────────────────────────────────── if not skip_wmo: print("\n=== Phase 3/3: WMO Objects ===") var unique_wmo := _collect_wmo(map_name, map_dir) _bake_wmo(unique_wmo, extracted, wmo_out, force) else: print("\n=== Phase 3/3: WMO Objects [SKIPPED] ===") var elapsed := float(Time.get_ticks_msec() - total_start) / 1000.0 print("\nAll done in %.1fs." % elapsed) quit(0) # ── Terrain ─────────────────────────────────────────────────────────────────── func _bake_terrain(map_name: String, map_dir: String, extracted: String, output_dir: String, full_tex: int, coarse_tex: int, force: bool) -> void: DirAccess.make_dir_recursive_absolute( ProjectSettings.globalize_path(output_dir.path_join(map_name))) var builder = ADT_BUILDER_SCRIPT.new() var loader = ClassDB.instantiate("ADTLoader") var dir = DirAccess.open(map_dir) var baked := 0; var skipped := 0; var failed := 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 if not parts[1].is_valid_int() or not parts[2].is_valid_int(): continue var tx := parts[1].to_int() var ty := parts[2].to_int() var out_path := output_dir.path_join(map_name).path_join( "%s_%d_%d.res" % [map_name, tx, ty]) if not force and FileAccess.file_exists(ProjectSettings.globalize_path(out_path)): skipped += 1; continue var data: Dictionary = loader.call("load_adt", ProjectSettings.globalize_path(map_dir.path_join(fname))) if data.is_empty() or not data.has("chunks"): failed += 1; continue var full_payload := builder.build_baked_tile_render_payload( data, _image_cache, ProjectSettings.globalize_path(extracted), 0, full_tex) var coarse_payload := builder.build_baked_tile_render_payload( data, _image_cache, ProjectSettings.globalize_path(extracted), 3, coarse_tex) if full_payload.is_empty(): failed += 1; continue var tile: Resource = BAKED_TILE_SCRIPT.new() tile.set("format_version", BAKED_TILE_SCRIPT.FORMAT_VERSION) tile.set("map_name", map_name) tile.set("tile_x", tx) tile.set("tile_y", ty) tile.set("tile_origin", builder.get_tile_origin_for_data(data)) tile.set("full_texture_size", full_tex) tile.set("coarse_texture_size", coarse_tex) tile.set("wmo_names", data.get("wmo_names", PackedStringArray())) tile.set("wmo_placements", data.get("wmo_placements", [])) tile.set("m2_names", data.get("m2_names", PackedStringArray())) tile.set("m2_placements", data.get("m2_placements", [])) tile.set("full_mesh", full_payload.get("mesh", null)) tile.set("coarse_mesh", coarse_payload.get("mesh", null)) if ResourceSaver.save(tile, out_path) != OK: failed += 1; continue baked += 1 if baked % 25 == 0: print(" terrain: baked=%d skipped=%d failed=%d" % [baked, skipped, failed]) print(" terrain done. baked=%d skipped=%d failed=%d" % [baked, skipped, failed]) # ── M2 ──────────────────────────────────────────────────────────────────────── func _collect_m2(map_name: String, map_dir: String) -> Dictionary: var loader = ClassDB.instantiate("ADTLoader") var dir = DirAccess.open(map_dir) var unique: Dictionary = {} 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 = loader.call("load_adt", ProjectSettings.globalize_path(map_dir.path_join(fname))) 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(): unique[norm] = true print(" found %d unique M2 models." % unique.size()) return unique func _bake_m2(unique: Dictionary, extracted: String, output_dir: String, force: bool) -> void: DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(output_dir)) var loader = ClassDB.instantiate("M2Loader") var baked := 0; var skipped := 0; var failed := 0 var total := unique.size(); var i := 0 for rel_path in unique.keys(): i += 1 var out_path := output_dir.path_join(rel_path.get_basename() + ".tscn") if not force and ResourceLoader.exists(out_path): skipped += 1; continue var abs_m2 := ProjectSettings.globalize_path(extracted.path_join(rel_path)) if not FileAccess.file_exists(abs_m2): failed += 1; continue var data: Dictionary = 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 var scene := PackedScene.new() if scene.pack(node) != OK: node.free(); failed += 1; continue node.free() DirAccess.make_dir_recursive_absolute( ProjectSettings.globalize_path(out_path.get_base_dir())) if ResourceSaver.save(scene, out_path) != OK: failed += 1; continue baked += 1 if i % 50 == 0 or i == total: print(" m2 [%d/%d] baked=%d skipped=%d failed=%d" % [i, total, baked, skipped, failed]) print(" m2 done. baked=%d skipped=%d failed=%d" % [baked, skipped, failed]) # ── WMO ─────────────────────────────────────────────────────────────────────── func _collect_wmo(map_name: String, map_dir: String) -> Dictionary: var loader = ClassDB.instantiate("ADTLoader") var dir = DirAccess.open(map_dir) var unique: Dictionary = {} 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 = loader.call("load_adt", ProjectSettings.globalize_path(map_dir.path_join(fname))) for rel in data.get("wmo_names", PackedStringArray()): var norm := str(rel).replace("\\", "/").to_lower() if not norm.is_empty(): unique[norm] = true print(" found %d unique WMO models." % unique.size()) return unique func _bake_wmo(unique: Dictionary, extracted: String, output_dir: String, force: bool) -> void: DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(output_dir)) var loader = ClassDB.instantiate("WMOLoader") var baked := 0; var skipped := 0; var failed := 0 var total := unique.size(); var i := 0 for rel_path in unique.keys(): i += 1 var out_path := output_dir.path_join(rel_path.get_basename() + ".tscn") if not force and ResourceLoader.exists(out_path): skipped += 1; continue var abs_wmo := ProjectSettings.globalize_path(extracted.path_join(rel_path)) if not FileAccess.file_exists(abs_wmo): failed += 1; continue var data: Dictionary = loader.call("load_wmo", abs_wmo) if data.is_empty(): failed += 1; continue var node: Node3D = WMO_BUILDER_SCRIPT.build(data, extracted) if node == null: failed += 1; continue var scene := PackedScene.new() if scene.pack(node) != OK: node.free(); failed += 1; continue node.free() DirAccess.make_dir_recursive_absolute( ProjectSettings.globalize_path(out_path.get_base_dir())) if ResourceSaver.save(scene, out_path) != OK: failed += 1; continue baked += 1 if i % 20 == 0 or i == total: print(" wmo [%d/%d] baked=%d skipped=%d failed=%d" % [i, total, baked, skipped, failed]) print(" wmo done. baked=%d skipped=%d failed=%d" % [baked, skipped, failed]) # ── Helpers ─────────────────────────────────────────────────────────────────── 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 _res(path: String) -> String: if path.begins_with("res://") or path.begins_with("user://"): return path return "res://" + path.trim_prefix("./").trim_prefix("/")