## pack_map.gd — Map PCK Exporter ## ## Editor tool script that exports a map scene (.tscn) as a standalone ## .pck resource pack. The .pck contains only the map scene and its ## direct dependencies (meshes, textures, materials) — no game logic. ## ## Usage: ## 1. Open the map template project (client/map_template/) in Godot. ## 2. Build your map in the scenes/maps/ directory. ## 3. Run this script from the Editor > Tools menu, or via the CLI: ## godot --headless --script scripts/map_packaging/pack_map.gd --map=res://scenes/maps/my_map.tscn ## ## Output: ## user://packed_maps/my_map.pck (ready to upload to the registry server) ## ## The exported .pck can be loaded in-game via: ## ProjectSettings.load_resource_pack("user://maps/my_map.pck") ## ## Requirements: ## - Godot 4.2+ ## - The map scene must use only local resources (relative paths within the project) ## - External dependencies (e.g. shared game assets) should be excluded — the ## .pck is meant to be additive content, not a full replacement. tool extends EditorScript # --------------------------------------------------------------------------- # Constants # --------------------------------------------------------------------------- ## Output directory relative to user:// const OUTPUT_DIR: String = "packed_maps" ## Godot export mode — packs all dependencies recursively. const EXPORT_MODE: int = PackedScene.GEN_FLAG_SAVE_NONE # --------------------------------------------------------------------------- # Entry point # --------------------------------------------------------------------------- func _run() -> void: # Determine the map scene path var scene_path: String = _resolve_scene_path() if scene_path.is_empty(): print("Usage: godot --headless --script pack_map.gd --map=res://scenes/maps/.tscn") print(" Or run from Editor > Tools > Pack Current Map") return # Validate the scene exists if not ResourceLoader.exists(scene_path): push_error("[PackMap] Scene not found: %s" % scene_path) return # Derive map name from file name var map_name: String = scene_path.get_file().trim_suffix(".tscn") var output_path: String = "user://%s/%s.pck" % [OUTPUT_DIR, map_name] # Ensure output directory exists DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path("user://%s" % OUTPUT_DIR)) print("[PackMap] Packaging: %s" % scene_path) print("[PackMap] Output: %s" % output_path) # Load the scene var scene: PackedScene = load(scene_path) if scene == null: push_error("[PackMap] Failed to load scene: %s" % scene_path) return # Generate the .pck via Godot's built-in packer. # We do this by creating a temporary PackedScene that encapsulates the map, # then using ProjectSettings.save_resource_pack to extract only the needed resources. var err: Error = _export_pck(scene, scene_path, output_path) if err != OK: push_error("[PackMap] Export failed: %s" % error_string(err)) return print("[PackMap] Successfully packed: %s (→ %s)" % [map_name, output_path]) print("[PackMap] File size: %d bytes" % FileAccess.get_size(output_path)) # Write a sidecar JSON with metadata _write_metadata(map_name, output_path, scene_path) # --------------------------------------------------------------------------- # Internal # --------------------------------------------------------------------------- ## Resolve the --map CLI argument or use the currently open scene. func _resolve_scene_path() -> String: # CLI argument takes precedence for arg in OS.get_cmdline_args(): if arg.begins_with("--map="): return arg.trim_prefix("--map=") # Fallback: use the currently open scene in the editor var current_scene_path: String = "" if Engine.is_editor_hint(): # In editor mode, try to get the current scene from the editor interface var editor_interface: EditorInterface = get_editor_interface() if editor_interface: current_scene_path = editor_interface.get_current_scene().scene_file_path return current_scene_path ## Export the scene as a .pck file containing only its direct dependencies. func _export_pck(scene: PackedScene, scene_path: String, output_path: String) -> Error: # Strategy: pack the scene + all its dependencies into a .pck using # ResourceSaver. We create a packaging helper that simulates what # Godot's export process does for resource packs. # Create a collection of all dependencies we need to include var deps: Array[String] = _collect_dependencies(scene_path) # Add the scene itself deps.append(scene_path) # We need to strip the "res://" prefix and map to correct paths # Create a PackedScene copy with packed resources var packed: PackedScene = _build_packed_map(scene_path, deps) if packed == null: return ERR_FILE_CORRUPT # Write the .pck using ProjectSettings.save_resource_pack() var files_to_pack: PackedStringArray = PackedStringArray() for dep in deps: var global_path: String = ProjectSettings.globalize_path(dep) if FileAccess.file_exists(global_path): files_to_pack.append(dep) if files_to_pack.is_empty(): return ERR_FILE_NOT_FOUND var err: Error = ProjectSettings.save_resource_pack(output_path, files_to_pack) return err ## Collect all dependencies of a scene recursively. func _collect_dependencies(scene_path: String) -> Array[String]: var deps: Array[String] = [] var visited: Dictionary = {} var pending: Array[String] = [scene_path] while not pending.is_empty(): var current: String = pending.pop_front() if current in visited: continue visited[current] = true # Skip external / built-in resources if current.begins_with("builtin://") or current.begins_with("uid://"): continue # Get dependencies from ResourceLoader var dep_list: PackedStringArray = ResourceLoader.get_dependencies(current) for dep in dep_list: if dep not in visited and dep.begins_with("res://"): deps.append(dep) pending.append(dep) return deps ## Build a PackedScene from the map and its dependencies. func _build_packed_map(scene_path: String, deps: Array[String]) -> PackedScene: # Load the scene and pack it var scene: PackedScene = load(scene_path) if scene == null: return null return scene ## Write a JSON metadata file alongside the .pck. func _write_metadata(map_name: String, pck_path: String, scene_path: String) -> void: var meta := { map_name = map_name, pck_path = pck_path, source_scene = scene_path, godot_version = Engine.get_version_info(), packed_at = Time.get_datetime_string_from_system(), } var meta_path: String = pck_path.trim_suffix(".pck") + ".json" var f := FileAccess.open(meta_path, FileAccess.WRITE) if f: f.store_string(JSON.stringify(meta, "\t", false)) f.close() print("[PackMap] Metadata written: %s" % meta_path)