# 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

```bash
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

```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/<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

```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: `<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 (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/<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
```
