# Map PCK Packaging Pipeline ## Overview The PCK packaging pipeline converts map scenes (`.tscn`) into standalone resource packs (`.pck`) that can be downloaded and loaded at runtime. This is Godot's built-in DLC/addon pattern — no engine modifications required. ``` Map Creator Registry Server Game Client ────────────────── ────────────────── ────────────────── │ │ │ ├── Build map in template project │ │ ├── Run pack_map.gd ──────────────►│ (upload .pck) │ │ │ │ │ ├── GET /maps ───────────────►│ │ │◄── [map list JSON] ────────┤ │ │ ├── Check user://maps/ │ │ │ │ │◄── GET /maps/.pck ───┤ (if not cached) │ ├── .pck file ───────────────►│ │ │ ├── Save to user://maps/ │ │ ├── load_resource_pack() │ │ ├── Load res://maps/.tscn │ │ │ ``` ## Components | File | Role | |------|------| | `pack_map.gd` | Godot editor tool — packages a `.tscn` → `.pck` | | `map_downloader.gd` | Godot autoload — client-side download + cache + load | | `map_registry_server.py` | Python HTTP server — serves `.pck` files + metadata | | `config-ext-map-registry.cfg` | Server config extension for registry integration | | `README.md` | This file | --- ## 1. Packaging Maps (`pack_map.gd`) ### Prerequisites - Godot 4.2+ - The map scene and all its dependencies must be local (`res://` paths) ### In the Editor 1. Open the map template project (`client/map_template/`) 2. Build your map in `scenes/maps/` (e.g., `scenes/maps/my_map.tscn`) 3. Open the map scene in the editor 4. Run **Project > Tools > Pack Current Map** Output goes to `user://packed_maps/my_map.pck`. ### Headless / CLI ```bash godot --headless --script scripts/map_packaging/pack_map.gd \ --map=res://scenes/maps/my_map.tscn ``` ### Output - `user://packed_maps/.pck` — the resource pack - `user://packed_maps/.json` — metadata sidecar ### What's in the .pck? The pack contains only the map scene and its direct resource dependencies (meshes, textures, materials). No game logic, no scripts, no config files. This keeps packs small and safe — a map can't inject code. --- ## 2. Map Registry Server (`map_registry_server.py`) A lightweight Python HTTP server that serves `.pck` files and map metadata. ### Quick Start ```bash # Install — no dependencies, pure stdlib python3 scripts/map_packaging/map_registry_server.py # With custom port and maps directory python3 scripts/map_packaging/map_registry_server.py \ --port 8090 \ --maps-dir /data/maps # Via environment variables export MAP_REGISTRY_PORT=8080 export MAP_REGISTRY_MAPS=/data/maps python3 scripts/map_packaging/map_registry_server.py ``` ### API Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/maps` | GET | JSON list of all available maps with metadata | | `/maps/.pck` | GET | Download a map pack (binary) | | `/maps/.json` | GET | Metadata for a single map | | `/` | GET | Server info and endpoint documentation | ### Deployment #### Systemd Service ```ini [Unit] Description=Tactical Shooter Map Registry After=network.target [Service] Type=simple User=tactical-shooter WorkingDirectory=/opt/tactical-shooter ExecStart=/usr/bin/python3 /opt/tactical-shooter/scripts/map_packaging/map_registry_server.py \ --port 8090 \ --maps-dir /var/lib/tactical-shooter/maps Restart=always RestartSec=5 [Install] WantedBy=multi-user.target ``` #### NPMplus Reverse Proxy ```nginx # NPMplus custom location for map registry location /maps/ { proxy_pass http://127.0.0.1:8090; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_buffering off; } ``` --- ## 3. Client Map Downloader (`map_downloader.gd`) ### Installation Add `map_downloader.gd` as an autoload singleton in `project.godot`: ```ini [autoload] MapDownloader="*res://scripts/map_packaging/map_downloader.gd" ``` ### Usage ```gdscript # Set the registry URL (default: http://127.0.0.1:8090) MapDownloader.registry_url = "http://maps.example.com:8090" # Fetch the map list from the registry (auto-downloads missing maps) MapDownloader.fetch_map_list() # Or download specific maps MapDownloader.download_map("de_dust2") MapDownloader.download_maps(["de_dust2", "de_inferno"]) # Check cache if MapDownloader.is_map_cached("de_dust2"): print("Map is ready") MapDownloader.load_map("de_dust2") # Now res://maps/de_dust2.tscn is available # Get cached maps var cached = MapDownloader.get_cached_maps() # Remove a map MapDownloader.remove_map("de_dust2") # Clear all caches MapDownloader.clear_cache() ``` ### Signals ```gdscript # Progress during download MapDownloader.map_download_progress.connect( func(map_name, received, total): var pct = float(received) / total * 100 print("%s: %.1f%%" % [map_name, pct]) ) # Download completed MapDownloader.map_download_complete.connect( func(map_name, success): if success: print("%s ready to play!" % map_name) ) # Map loaded into resource system MapDownloader.map_loaded.connect( func(map_name): print("%s is now available at res://maps/%s.tscn" % [map_name, map_name]) ) ``` ### Environment Overrides | Variable | Description | |----------|-------------| | `MAP_REGISTRY_URL` | Override the registry server URL | --- ## 4. Integration with Server Main To wire map downloads into the existing server flow (`server_main.gd`): ```gdscript # In server_main.gd or a dedicated map manager: func _on_server_started() -> void: # Start map registry if configured if ServerConfig.has_property("map_registry_enabled") and ServerConfig.map_registry_enabled: var registry_url: String = ServerConfig.get("map_registry_url", "") if not registry_url.is_empty(): MapDownloader.registry_url = registry_url # Fetch map list — MapDownloader auto-downloads missing maps MapDownloader.fetch_map_list() func _on_map_needed(map_name: String) -> void: if not MapDownloader.is_map_cached(map_name): # Wait for download var waiter := func(name, success): if success: _start_map(name) MapDownloader.map_download_complete.connect(waiter.bind(map_name), CONNECT_ONE_SHOT) MapDownloader.download_map(map_name) else: _start_map(map_name) func _start_map(map_name: String) -> void: if MapDownloader.load_map(map_name): # Now load the scene var map_scene: PackedScene = load("res://scenes/maps/%s.tscn" % map_name) var instance := map_scene.instantiate() add_child(instance) ``` ### Server Config Extension Add this section to your `server_config.cfg` to enable map registry: ```ini [map_registry] ; Enable automatic map download on server start enabled=false ; URL of the map registry server url="http://127.0.0.1:8090" ; Auto-download all maps from the registry, not just advertised ones auto_download_all=false ``` --- ## 5. Cache and Storage ### Client Cache - Location: `user://maps/` - Files: `.pck` + `manifest.json` - Manifest is validated on startup — stale entries (missing .pck files) are pruned - The cache is persistent across restarts ### Cache Size Management Maps can be large (10–100 MB depending on texture resolution). Consider: ```gdscript # Check cache size var total_size := 0 for name in MapDownloader.get_cached_maps(): var info = MapDownloader.get_cached_map_info(name) total_size += info.get("size", 0) print("Cache size: %.1f MB" % (total_size / 1048576.0)) # Remove least-recently-downloaded maps if over budget const MAX_CACHE_MB := 500 if total_size > MAX_CACHE_MB * 1048576: var cached = MapDownloader.get_cached_maps() for name in cached: MapDownloader.remove_map(name) break # remove one at a time ``` --- ## 6. Security Considerations - **.pck files cannot execute scripts** that weren't already in the game binary. Resource packs only add data (scenes, textures, meshes). Game logic lives in the binary and cannot be injected via a .pck. - **Checksum verification**: The server provides SHA-256 checksums. The client can verify integrity before loading: ```gdscript func verify_map(map_name: String) -> bool: var pck_path = "user://maps/%s.pck" % map_name var global_path = ProjectSettings.globalize_path(pck_path) var f = FileAccess.open(global_path, FileAccess.READ) if not f: return false var ctx = HashingContext.new() ctx.start(HashingContext.HASH_SHA256) while f.get_position() < f.get_length(): ctx.update(f.get_buffer(65536)) var checksum = ctx.finish().hex_encode() f.close() # Compare with server checksum var meta = MapDownloader.get_cached_map_info(map_name) var expected = meta.get("checksum_sha256", "") return expected.is_empty() or checksum == expected ``` - **HTTPS**: Deploy the registry server behind an NPMplus/nginx proxy with SSL termination. The client supports HTTPS natively via HTTPRequest. --- ## 7. Workflow Summary ### For Map Creators ``` 1. Open map_template/ in Godot 4 2. Build map using CSG prefabs (scenes/maps/.tscn) 3. Run: godot --headless --script scripts/map_packaging/pack_map.gd --map=res://scenes/maps/.tscn 4. Upload output/user://packed_maps/.pck to registry server's maps directory ``` ### For Server Operators ``` 1. Deploy map_registry_server.py on your backend (or alongside the game server) 2. Copy .pck files into the maps directory 3. SIGHUP the server to rescan (or it auto-scans every 5 seconds) 4. Configure [map_registry] in server_config.cfg 5. Game clients auto-download on connect ``` ### For Developers ``` 1. Add MapDownloader as project autoload 2. On server start, call MapDownloader.fetch_map_list() 3. Before loading a map scene, call MapDownloader.load_map() if it's a community map 4. Handle download signals for progress UI ```