Files
tactical-shooter/scripts/map_packaging/pack_map.gd
T
shawn e9dc05983c Save workspace artifacts: character-controller, gdextension scaffold, network scripts, map-pipeline, server config
Includes code from task workspaces that was never pushed:
- client/characters/ — fps_character_controller.gd (400 lines), fps_camera.gd, input_handler.gd
- gdextension/simulation/ — C++ GDExtension scaffold: entity, movement, hit detection, simulation server, state serializer, bitstream (14 files)
- scripts/network/ — ENet-based network_manager.gd, server/client_main.gd, player.gd
- scripts/map_packaging/ — PCK pipeline: pack_map.gd, map_downloader.gd, map_registry_server.py, README
- scripts/config/ — server_config.gd (480 lines)
- scenes/ — client_main, server_main, player, test_range
- project.godot, export_presets.cfg
2026-07-01 18:30:44 -04:00

191 lines
6.6 KiB
GDScript

## 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/<map_name>.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)