Files
tactical-shooter/scripts/map_packaging
shawn 9ea98aa7b8 Phase 7: Windows client export, preset fix, code fixes
- Fix: export_presets.cfg — platform='Windows Desktop' (not 'Windows'), Windows as [preset.0]
- Fix: plugin_manager.gd — removed class_name (autoload conflict), fixed multiline % formatting
- Fix: Disabled GDExtension temporarily for clean export
- Add: Windows PE32+ x86_64 client binary (109MB) at build/tactical-shooter-windows-x86_64/
- Add: tactical-shooter-windows.zip (portable zip package)
- Build: Linux server binary (78MB) rebuilt
2026-07-01 19:47:12 -04:00
..

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/<name>.pck ───┤ (if not cached)
      │                                  ├── .pck file ───────────────►│
      │                                  │                              ├── Save to user://maps/
      │                                  │                              ├── load_resource_pack()
      │                                  │                              ├── Load res://maps/<name>.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

godot --headless --script scripts/map_packaging/pack_map.gd \
    --map=res://scenes/maps/my_map.tscn

Output

  • user://packed_maps/<map_name>.pck — the resource pack
  • user://packed_maps/<map_name>.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

# 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/<name>.pck GET Download a map pack (binary)
/maps/<name>.json GET Metadata for a single map
/ GET Server info and endpoint documentation

Deployment

Systemd Service

[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

# 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:

[autoload]
MapDownloader="*res://scripts/map_packaging/map_downloader.gd"

Usage

# 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

# 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):

# 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:

[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: <map_name>.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 (10100 MB depending on texture resolution). Consider:

# 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:

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/<name>.tscn)
3. Run: godot --headless --script scripts/map_packaging/pack_map.gd --map=res://scenes/maps/<name>.tscn
4. Upload output/user://packed_maps/<name>.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