diff --git a/client/scripts/convert_csg_to_mesh.gd.uid b/client/scripts/convert_csg_to_mesh.gd.uid new file mode 100644 index 0000000..4acb47e --- /dev/null +++ b/client/scripts/convert_csg_to_mesh.gd.uid @@ -0,0 +1 @@ +uid://c0b8h1hq57lsu diff --git a/client/scripts/generate_occluders.gd.uid b/client/scripts/generate_occluders.gd.uid new file mode 100644 index 0000000..9aa7ead --- /dev/null +++ b/client/scripts/generate_occluders.gd.uid @@ -0,0 +1 @@ +uid://pauclwpn265a diff --git a/client/scripts/profiling/profile_scene.gd.uid b/client/scripts/profiling/profile_scene.gd.uid new file mode 100644 index 0000000..f4da54e --- /dev/null +++ b/client/scripts/profiling/profile_scene.gd.uid @@ -0,0 +1 @@ +uid://byjqslju7qnt6 diff --git a/client/scripts/profiling/profiler_node.gd.uid b/client/scripts/profiling/profiler_node.gd.uid new file mode 100644 index 0000000..693a5f2 --- /dev/null +++ b/client/scripts/profiling/profiler_node.gd.uid @@ -0,0 +1 @@ +uid://jih7knhnl55a diff --git a/client/scripts/profiling/test_process.gd.uid b/client/scripts/profiling/test_process.gd.uid new file mode 100644 index 0000000..f2abb35 --- /dev/null +++ b/client/scripts/profiling/test_process.gd.uid @@ -0,0 +1 @@ +uid://dc8x14rs2jyu8 diff --git a/client/scripts/profiling/test_simple.gd.uid b/client/scripts/profiling/test_simple.gd.uid new file mode 100644 index 0000000..a934569 --- /dev/null +++ b/client/scripts/profiling/test_simple.gd.uid @@ -0,0 +1 @@ +uid://b8jj7n3bi6ljh diff --git a/docs/playtest-guide.md b/docs/playtest-guide.md index c02378b..8d18f5b 100644 --- a/docs/playtest-guide.md +++ b/docs/playtest-guide.md @@ -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 +rcon commands +rcon changelevel test_range +rcon kick ``` -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=` 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. diff --git a/gdextension/simulation/.sconsign.dblite b/gdextension/simulation/.sconsign.dblite index 9ccbbfc..a830caf 100644 Binary files a/gdextension/simulation/.sconsign.dblite and b/gdextension/simulation/.sconsign.dblite differ diff --git a/gdextension/simulation/gdextension/simulation.gdextension b/gdextension/simulation/gdextension/simulation.gdextension deleted file mode 100644 index 1606bb1..0000000 --- a/gdextension/simulation/gdextension/simulation.gdextension +++ /dev/null @@ -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" diff --git a/scripts/network/server_main.gd b/scripts/network/server_main.gd index 4f92d3f..49b8dc8 100644 --- a/scripts/network/server_main.gd +++ b/scripts/network/server_main.gd @@ -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() diff --git a/server/scripts/game_server.gd b/server/scripts/game_server.gd index 07a5ac6..25a520b 100644 --- a/server/scripts/game_server.gd +++ b/server/scripts/game_server.gd @@ -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 diff --git a/server/scripts/simulation_server_stub.gd b/server/scripts/simulation_server_stub.gd new file mode 100644 index 0000000..aaebcf2 --- /dev/null +++ b/server/scripts/simulation_server_stub.gd @@ -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() diff --git a/server/scripts/simulation_server_stub.gd.uid b/server/scripts/simulation_server_stub.gd.uid new file mode 100644 index 0000000..be21be9 --- /dev/null +++ b/server/scripts/simulation_server_stub.gd.uid @@ -0,0 +1 @@ +uid://b1lw5s1djwsqk