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:
@@ -0,0 +1 @@
|
||||
uid://c0b8h1hq57lsu
|
||||
@@ -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
@@ -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 (2–32 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"
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user