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
This commit is contained in:
@@ -0,0 +1,363 @@
|
||||
# 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
|
||||
```
|
||||
Reference in New Issue
Block a user