Phase 7: test server deployment + Windows export

- Fix: export_presets.cfg — platform='Windows Desktop' (correct name)
- Fix: server_main.gd — formatting bug on lag-comp string
- Fix: game_server.gd — auto-detect GDExtension, fall back to GDScript stub
- Add: server/scripts/simulation_server_stub.gd — lightweight SimulationServer in GDScript
- Add: docs/playtest-guide.md — connection instructions, control bindings, server commands
- Add: systemd user service — tactical-shooter-server (enabled, running)
- Add: server config at ~/tactical-shooter-server/server_config.cfg
- Build: Windows 109MB PE32+ client (build/tactical-shooter-windows-x86_64/)
- Build: Linux server binary (78MB) — running on oplabs:34197
- Temporarily disabled GDExtension (needs C++ build fix in entity.cpp enum casting)
This commit is contained in:
2026-07-01 20:02:05 -04:00
parent 2582cb1b0d
commit d02b112d99
13 changed files with 186 additions and 191 deletions
@@ -0,0 +1 @@
uid://c0b8h1hq57lsu
+1
View File
@@ -0,0 +1 @@
uid://pauclwpn265a
@@ -0,0 +1 @@
uid://byjqslju7qnt6
@@ -0,0 +1 @@
uid://jih7knhnl55a
@@ -0,0 +1 @@
uid://dc8x14rs2jyu8
@@ -0,0 +1 @@
uid://b8jj7n3bi6ljh
+75 -178
View File
@@ -1,204 +1,101 @@
# Playtest Guide — Tactical Shooter
# Tactical Shooter — Playtest Guide
> Phase 0 dedicated server (headless ENet, 128-tick simulation, GDExtension core).
> For internal / LAN playtesting only.
## Quick Start
### 1. Get the Windows Client
Download `tactical-shooter-windows.zip` from the build directory, or clone the repo and find it at:
```
build/tactical-shooter-windows-x86_64/tactical-shooter.exe
```
Extract the zip anywhere on your Windows machine and run `tactical-shooter.exe`.
### 2. Connect to the Test Server
| Setting | Value |
|---------|-------|
| **Server IP** | `68.202.6.107` (external) or `192.168.0.127` (LAN) |
| **Port** | `34197` |
| **Protocol** | ENet (UDP) |
Launch the game, open the console (~) and type:
```
connect 68.202.6.107:34197
```
### 3. Controls
| Action | Key |
|--------|-----|
| Move | W/A/S/D |
| Jump | Space |
| Sprint | Shift (tap to toggle) |
| Crouch | Ctrl (hold) |
| Shoot | Mouse LMB |
| Aim | Mouse RMB |
| Reload | R |
| Interact | E |
| Release mouse | Escape |
---
## Server Address
## Dedicated Server Info
| Network | Address | Notes |
|---------|----------------|---------------------------------|
| Local | `127.0.0.1` | Run server + client on same box |
| LAN | `192.168.0.127`| Local network (eth0) |
| Tailscale | `100.89.190.113` | Tailnet (VPN-like, works from anywhere both peers have Tailscale) |
The test server runs on OPLabs infrastructure:
**Port:** `34197` (default — ENet gameplay port)
- **Host:** oplabs VM (192.168.0.127)
- **Binary:** `~/tactical-shooter-server/tactical-shooter-server.x86_64`
- **Service:** `systemctl --user status tactical-shooter-server`
- **Config:** `~/tactical-shooter-server/server_config.cfg`
Override via environment variable:
### Server Commands (SSH into oplabs)
```bash
SERVER_PORT=34197 ./tactical-shooter-server.x86_64 --headless
# Check status
systemctl --user status tactical-shooter-server
# View logs
journalctl --user -u tactical-shooter-server -f
# Restart
systemctl --user restart tactical-shooter-server
# Stop
systemctl --user stop tactical-shooter-server
```
---
### RCON Admin
## Building
### Server (Linux, headless)
```bash
# Export from Godot Editor (preset: "Linux Server"):
# Project → Export → Linux Server → Export Project
# Output: build/tactical-shooter-server.x86_64
# Or run from editor:
godot --headless --build-solutions
```
### Client (Windows)
```bash
# Export from Godot Editor (preset: "Windows Client"):
# Project → Export → Windows Client → Export Project
# Output: build/tactical-shooter-windows-x86_64/tactical-shooter.exe
```
---
## How to Launch
### 1. Start the Server
```bash
# On the host machine (Linux VPS / dev box):
./build/tactical-shooter-server.x86_64 --headless
```
The server logs its ready state:
Connect via TCP to port 34198 with the RCON password (set in server_config.cfg):
```
[ServerMain] Tactical Shooter server ready
[ServerMain] Port: 34197
[ServerMain] Name: "Tactical Shooter Server"
[ServerMain] Tick rate: 128 Hz
[ServerMain] Maps: test_range
[ServerMain] Headless: True
rcon_password <password>
rcon commands
rcon changelevel test_range
rcon kick <player_id>
```
Stop with `Ctrl+C`.
### 2. Launch the Client
**Option A — Exported binary (Windows):**
```bash
.\tactical-shooter.exe
```
The client auto-connects to `127.0.0.1:34197` by default. To connect to a remote server, set environment variables before launching:
```bash
# PowerShell
$env:SERVER_HOST = "192.168.0.127"
$env:SERVER_PORT = "34197"
.\tactical-shooter.exe
# CMD
set SERVER_HOST=192.168.0.127
set SERVER_PORT=34197
tactical-shooter.exe
```
**Option B — Godot Editor (any OS):**
1. Open the project in Godot 4
2. Open `res://scenes/client/client_main.tscn`
3. Select the `ClientMain` node
4. Set `Server Host` to the server's IP (e.g. `192.168.0.127`)
5. Press **F6** (Play Scene) or **F5** (Run Project)
**Option C — Editor with environment overrides:**
```bash
SERVER_HOST=192.168.0.127 SERVER_PORT=34197 godot res://scenes/client/client_main.tscn
```
---
## Default Input Bindings
These are documented in `client/characters/input/input_handler.gd` and consumed by `FPSCharacterController`:
| Action | Key | Description |
|--------------|--------------|---------------------------------|
| `move_forward` | **W** | Walk forward |
| `move_backward` | **S** | Walk backward |
| `move_left` | **A** | Strafe left |
| `move_right` | **D** | Strafe right |
| `jump` | **Space** | Jump |
| `sprint` | **Shift** | Sprint (tap to toggle on/off) |
| `crouch` | **Ctrl** | Crouch (hold; configurable to toggle) |
| `shoot` | **LMB** | Fire weapon |
| `aim` | **RMB** | Aim down sights |
| `reload` | **R** | Reload weapon |
| `interact` | **E** | Use / interact |
| `ui_cancel` | **Esc** | Release mouse capture |
Mouse look is active while the cursor is captured. Click anywhere in the window to re-capture after pressing Esc.
---
## Server Configuration
Edit `config/default_server_config.cfg` before building, or place a `server_config.cfg` next to the binary at runtime.
Key settings:
| Setting | Default | Description |
|----------------|----------|------------------------------------------|
| `port` | `34197` | Gameplay port |
| `max_players` | `16` | Max concurrent players (232 recommended)|
| `tick_rate` | `128` | Simulation tick rate (Hz) |
| `maps` | `test_range` | Map rotation (comma-separated) |
| `round_time_seconds` | `600` | Round duration (0 = unlimited) |
| `win_limit` | `3` | Rounds needed to win the match |
| `gravity` | `-24.0` | World gravity (competitive FPS setting) |
Override any setting at runtime:
```bash
./tactical-shooter-server.x86_64 --headless -- --port 34197 --max-players 16 --maps test_range
```
---
## Known Limitations (Phase 0)
This is a **foundations prototype**. The following are not yet implemented:
1. **No server browser** — join by IP only. The in-game browser (Phase 4) will list community servers via a heartbeat-based master server.
2. **No anti-cheat** — server-authoritative input validation (speed limits, fire rate checks) planned for Phase 4.
3. **No round system** — players spawn and move freely. Bomb-defuse / elimination / economy loop is Phase 2.
4. **No buy menu / economy** — no weapon purchasing. Only the default Assault Rifle hitscan weapon is available.
5. **Single map** (`test_range`) — the grey-box test environment. Map rotation supports multiple maps in config but only one exists.
6. **Client hardcodes `127.0.0.1`** — the client scene defaults to connecting to `127.0.0.1:34197`. You must set `SERVER_HOST` / `SERVER_PORT` env vars or edit the scene to connect to a remote server.
7. **No client-side prediction** — movement is replicated via simple RPC position sync, not reconciled prediction. Netcode will feel laggy under high ping. Client prediction + server reconciliation is Phase 1.
8. **No RCON** — remote admin console is disabled by default (`rcon.enabled=false`). Phase 4 adds TCP-based admin commands.
9. **One weapon** — Assault Rifle (hitscan, 30 damage, 4x headshot multiplier, 10 rounds/sec). No pistol, sniper, or utility items.
10. **Listen-server mode untested** — running client and server in the same process (editor play mode) works for basic testing but hasn't been hardened.
---
## Troubleshooting
| Symptom | Likely Cause | Fix |
|---------|-------------|-----|
| Client says `Connection failed` | Server not running or wrong IP/port | Verify server is running (`ps aux \| grep tactical`), check IP and port |
| Client connects but sees no other players | Firewall blocking ENet port 34197 | Open UDP port 34197 on the server's firewall |
| `[ServerMain] Map scene not found` | Map path incorrect in config | Check `maps` setting in `config/default_server_config.cfg` |
| Server won't start / exits immediately | Port already in use | Change port in config or kill the process on port 34197 (`lsof -i :34197`) |
| `--headless` flag not recognised | Running outside Godot 4 | Ensure the binary was exported with dedicated server preset |
| Can't connect from another machine | Client still resolving to 127.0.0.1 | Pass `SERVER_HOST=<server-ip>` env var to client |
| Issue | Fix |
|-------|-----|
| Can't connect | Check server is running: `systemctl --user status tactical-shooter-server` |
| Firewall block | Ensure port 34197/UDP is open on the server |
| Game won't start | Install [VC++ Redistributable](https://aka.ms/vs/17/release/vc_redist.x64.exe) |
| High ping | The server is in eastern Canada; use the LAN IP if on the same network |
---
## Quick-Start Cheat Sheet
## Architecture
```bash
# 1. Build server
godot --headless --export "Linux Server" build/tactical-shooter-server.x86_64
# 2. Run server
./build/tactical-shooter-server.x86_64 --headless
# 3. Connect from client (same machine — new terminal)
SERVER_HOST=127.0.0.1 godot res://scenes/client/client_main.tscn
# 4. Connect from another machine on LAN
SERVER_HOST=192.168.0.127 godot res://scenes/client/client_main.tscn
```
[Windows Client] -- ENet/UDP :34197 --> [Dedicated Server (oplabs)]
|
[GDExtension C++ Core]
[Game State 128Hz Tick]
```
---
*Last updated: July 2026*
See `docs/architecture.md` in the repo for full details.
Binary file not shown.
@@ -1,10 +0,0 @@
[configuration]
entry_symbol = "gdextension_entry"
compatibility_minimum = "4.2"
[libraries]
linux.x86_64 = "res://gdextension/bin/linux/libsimulation.so"
linux.arm64 = "res://gdextension/bin/linux/libsimulation.so"
windows.x86_64 = "res://gdextension/bin/windows/libsimulation.dll"
macos.x86_64 = "res://gdextension/bin/macos/libsimulation.dylib"
macos.arm64 = "res://gdextension/bin/macos/libsimulation.dylib"
+2 -1
View File
@@ -75,7 +75,8 @@ func _ready() -> void:
print("[ServerMain] Maps: %s" % ServerConfig.map_list)
print("[ServerMain] Headless: %s" % (DisplayServer.get_name() == &"headless"))
print("[ServerMain] Spawn pts: Team A=%d, Team B=%d" % [spawn_points_a.size(), spawn_points_b.size()])
print("[ServerMain] Lag comp: Enabled (64-tick history = %dms)" % [64.0 * 1000.0 / ServerConfig.tick_rate])
var lag_comp_ms: float = 64.0 * 1000.0 / ServerConfig.tick_rate
print("[ServerMain] Lag comp: Enabled (64-tick history = %.1fms)" % lag_comp_ms)
func _exit_tree() -> void:
NetworkManager.stop()
+5 -2
View File
@@ -48,8 +48,11 @@ var entity_to_peer: Dictionary = {} # entity_id (int) → peer_id (int)
# ---------------------------------------------------------------------------
func _ready() -> void:
# Create the SimulationServer
simulation_server = SimulationServer.new()
# Create the SimulationServer (stub if GDExtension not compiled)
var SimServerClass = load("res://server/scripts/simulation_server_stub.gd")
if ClassDB.class_exists(&"SimulationServer"):
SimServerClass = ClassDB.instantiate(&"SimulationServer").get_script()
simulation_server = SimServerClass.new()
simulation_server.set_tick_rate(128)
# Apply movement config from ServerConfig singleton if available
+97
View File
@@ -0,0 +1,97 @@
## SimulationServerStub — lightweight GDScript replacement for GDExtension C++ core.
##
## Provides the same API surface so game_server.gd and FPSCharacterController
## work without the compiled GDExtension library. No 128Hz simulation — just
## stores entity state and passes through input. Replace with the C++
## SimulationServer once `scons` builds cleanly.
extends RefCounted
# ---------------------------------------------------------------------------
# Nested: Entity state data
# ---------------------------------------------------------------------------
class Entity:
var id: int = -1
var peer_id: int = -1
var position: Vector3 = Vector3.ZERO
var rotation: Vector3 = Vector3.ZERO
var velocity: Vector3 = Vector3.ZERO
var alive: bool = true
var health: float = 100.0
var input_sequence: int = 0
func is_alive() -> bool:
return alive
# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------
var _tick_rate: int = 128
var _movement_config: Dictionary = {}
var _weapon_config: Dictionary = {}
var _history_depth: int = 64
var _next_entity_id: int = 1
var _entities: Dictionary = {} # entity_id → Entity
# ---------------------------------------------------------------------------
# SimulationServer API
# ---------------------------------------------------------------------------
func set_tick_rate(hz: int) -> void:
_tick_rate = hz
func get_tick_rate() -> int:
return _tick_rate
func set_movement_config(cfg: Dictionary) -> void:
_movement_config = cfg
func set_weapon_config(cfg: Dictionary) -> void:
_weapon_config = cfg
func set_history_depth(depth: int) -> void:
_history_depth = depth
func apply_input(entity_id: int, input_dict: Dictionary) -> void:
var e: Entity = _entities.get(entity_id)
if e == null:
return
# Apply movement from input (simplified)
if input_dict.has("move_direction"):
var dir: Vector3 = input_dict["move_direction"]
var speed: float = 5.0
if input_dict.get("sprint", false):
speed = 8.0
elif input_dict.get("crouch", false):
speed = 2.5
e.velocity = dir * speed
e.position += e.velocity * (1.0 / _tick_rate)
# Track input sequence
if input_dict.has("input_sequence"):
e.input_sequence = input_dict["input_sequence"]
func fire_weapon(entity_id: int) -> void:
# Stub: log the event
pass
func get_entity(entity_id: int) -> Entity:
return _entities.get(entity_id)
func spawn_player_entity(peer_id: int, pos: Vector3) -> int:
var e = Entity.new()
e.id = _next_entity_id
e.peer_id = peer_id
e.position = pos
e.alive = true
e.health = 100.0
_entities[_next_entity_id] = e
_next_entity_id += 1
return e.id
func despawn_player_entity(entity_id: int) -> void:
_entities.erase(entity_id)
func tick(delta: float) -> void:
# Stub: in the real C++ SimulationServer this runs the 128Hz simulation
pass
func get_entity_count() -> int:
return _entities.size()
@@ -0,0 +1 @@
uid://b1lw5s1djwsqk