## GameServer — drives the 128Hz simulation loop. ## ## Owns a SimulationServer instance, drives its tick loop in _physics_process, ## and manages weapon configuration. Acts as the bridge between the GDScript ## world (NetworkManager, ServerMain) and the C++ simulation core. ## ## Architecture: ## GameServer (Node, autoload candidate) ## ├── SimulationServer (GDExtension C++) — game state + hit detection ## │ ├── applies client input via apply_input() ## │ └── process_compensated_fire() with lag compensation rewind ## └── WeaponManager (optional, on the player node for client-side rate limit) ## ## Usage (server_main.gd): ## func _ready(): ## var gs = GameServer.new() ## add_child(gs) ## gs.configure(ServerConfig.tick_rate, ServerConfig.make_movement_dict()) ## ## Standalone / listen-server test: ## var gs = get_node("/root/GameServer") ## if gs: gs.start_simulation() ## extends Node # --------------------------------------------------------------------------- # Signals # --------------------------------------------------------------------------- ## Emitted after each simulation tick with hit results. signal tick_completed(tick: int) ## Emitted when a player is damaged (for scoreboard/UI updates). signal player_damaged(victim_entity_id: int, shooter_entity_id: int, damage: float, killed: bool) # --------------------------------------------------------------------------- # State # --------------------------------------------------------------------------- var simulation_server: Object = null var is_running: bool = false # Map entity_id → peer_id for broadcasting damage events var entity_to_peer: Dictionary = {} # entity_id (int) → peer_id (int) # --------------------------------------------------------------------------- # Lifecycle # --------------------------------------------------------------------------- func _ready() -> void: # Create the SimulationServer simulation_server = SimulationServer.new() simulation_server.set_tick_rate(128) # Apply movement config from ServerConfig singleton if available if ServerConfig and ServerConfig.has_method(&"make_movement_dict"): simulation_server.set_movement_config(ServerConfig.make_movement_dict()) # Configure the default hitscan weapon (Assault Rifle) simulation_server.set_weapon_config({ "base_damage": 30.0, "head_multiplier": 4.0, "body_multiplier": 1.0, "arm_multiplier": 0.75, "leg_multiplier": 0.6, "max_range": 500.0, "spread_degrees": 0.5, "fire_rate_hz": 10.0, }) # Set history depth for lag compensation (64 ticks = ~500ms at 128Hz) simulation_server.set_history_depth(64) # Register as singleton so FPSCharacterController can find us Engine.register_singleton("SimulationServer", simulation_server) print("[GameServer] SimulationServer created. Tick rate: %d Hz" % simulation_server.get_tick_rate()) func _exit_tree() -> void: if simulation_server: simulation_server.stop() simulation_server = null # --------------------------------------------------------------------------- # Public API # --------------------------------------------------------------------------- ## Start the simulation. func start_simulation() -> void: if is_running: return if simulation_server == null: push_error("[GameServer] SimulationServer is null, cannot start") return simulation_server.start() is_running = true print("[GameServer] Simulation started") ## Stop the simulation. func stop_simulation() -> void: if not is_running: return if simulation_server: simulation_server.stop() is_running = false print("[GameServer] Simulation stopped") ## Spawn a player entity and map to peer ID. ## Returns the entity ID assigned by the simulation server. func spawn_player_entity(peer_id: int, spawn_pos: Vector3) -> int: if simulation_server == null: return -1 if not is_running: start_simulation() var entity_id: int = simulation_server.spawn_entity(spawn_pos) if entity_id < 0 or entity_id >= 65535: push_error("[GameServer] Failed to spawn entity for peer %d" % peer_id) return -1 entity_to_peer[entity_id] = peer_id print("[GameServer] Spawned entity %d for peer %d at (%.1f, %.1f, %.1f)" % [entity_id, peer_id, spawn_pos.x, spawn_pos.y, spawn_pos.z]) return entity_id ## Despawn a player entity. func despawn_player_entity(entity_id: int) -> void: if simulation_server == null: return if entity_id >= 0: simulation_server.despawn_entity(entity_id) # Remove from mapping if entity_id in entity_to_peer: entity_to_peer.erase(entity_id) ## Get the peer ID for a given entity ID. func get_peer_for_entity(entity_id: int) -> int: return entity_to_peer.get(entity_id, -1) ## Get the underlying SimulationServer reference. func get_simulation_server() -> Object: return simulation_server ## Configure weapon damage profile. func set_weapon(config: Dictionary) -> void: if simulation_server: simulation_server.set_weapon_config(config) # --------------------------------------------------------------------------- # Main loop (128 Hz) # --------------------------------------------------------------------------- func _physics_process(delta: float) -> void: if not is_running or simulation_server == null: return # Drive the fixed-timestep simulation loop while simulation_server.can_tick(delta): var snapshot: PackedByteArray = simulation_server.tick() # snapshot is the serialized state — send to network layer in Phase 2 # For now, just emit the tick completed signal tick_completed.emit(simulation_server.get_stats().get("tick_count", 0)) # Check for hit results and emit damage events var hit_result: Dictionary = simulation_server.get_last_hit_result() if hit_result.get("hit", false): var victim_id: int = hit_result.get("entity_id", -1) var damage: float = hit_result.get("damage", 0.0) var killed: bool = hit_result.get("killed", false) # For now we don't know the shooter — this will be wired in Phase 2 player_damaged.emit(victim_id, -1, damage, killed)