## 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 ## WeaponServer — handles hit-scan raycasting and lag-compensated fire. var weapon_server: WeaponServer = null ## LagCompensation — records player positions per tick for rewind raycasts. var lag_compensation: LagCompensation = null ## DamageProcessor — applies hit results, tracks health and kills. var damage_processor: DamageProcessor = null ## TeamManager — manages team assignment and scoring. var team_manager: TeamManager = null ## RoundManager — match lifecycle (warmup → prep → live → post). var round_manager: RoundManager = null ## EconomyManager — per-player money tracking and earnings. var economy_manager: EconomyManager = null ## BuyMenuHandler — server-side buy request validation and processing. var buy_menu_handler: BuyMenuHandler = null ## BombObjective — server-authoritative bomb plant/defuse logic. var bomb_objective: BombObjective = null ## Current server tick counter, incremented each time tick() is called. var _current_tick: int = 0 # Map entity_id → peer_id for broadcasting damage events var entity_to_peer: Dictionary = {} # entity_id (int) → peer_id (int) # Map peer_id → entity_id for backwards lookup var peer_to_entity: Dictionary = {} # peer_id (int) → entity_id (int) # --------------------------------------------------------------------------- # Lifecycle # --------------------------------------------------------------------------- func _ready() -> void: # 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 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) # --- Combat subsystems (GDScript lag-compensated hit-scan) --- # Create and wire the WeaponServer with LagCompensation and DamageProcessor. # These work alongside the C++ SimulationServer for the GDScript # weapon path. weapon_server = WeaponServer.new() weapon_server.physics_world = get_viewport().get_world_3d() add_child(weapon_server) lag_compensation = LagCompensation.new() add_child(lag_compensation) damage_processor = DamageProcessor.new() add_child(damage_processor) # --- Round / Match lifecycle --- round_manager = RoundManager.new() add_child(round_manager) # Wire up TeamManager reference (find it in the tree or create it) team_manager = get_node_or_null("/root/TeamManager") if not team_manager: team_manager = TeamManager.new() add_child(team_manager) round_manager.team_manager = team_manager round_manager.damage_processor = damage_processor # --- Economy system --- economy_manager = EconomyManager.new() add_child(economy_manager) buy_menu_handler = BuyMenuHandler.new() add_child(buy_menu_handler) buy_menu_handler.initialise(economy_manager, weapon_server, team_manager) # Wire kill rewards: when DamageProcessor detects a kill, award money to the shooter # Note: shooter_id is an entity_id; we map it to peer_id for economy lookup. damage_processor.player_killed.connect(_on_kill_for_economy) # --- RCON command handling --- var rcon_handler = get_node_or_null("/root/RconServer/RconCommandHandler") if rcon_handler and rcon_handler.has_signal("rcon_command"): rcon_handler.rcon_command.connect(_on_rcon_command) print("[GameServer] Connected to RCON command handler") else: print("[GameServer] RCON handler not available — commands won't be routed") # Wire lag compensation into WeaponServer weapon_server.lag_compensation = lag_compensation # Connect damage processor signals to GameServer signals for upstream relay damage_processor.player_killed.connect(_on_player_killed) # Connect damage processor kills to RoundManager for round tracking and elimination damage_processor.player_killed.connect(_on_kill_for_round) _current_tick = 0 print("[GameServer] Combat subsystems ready: WeaponServer + LagCompensation + DamageProcessor") # Register as singleton so FPSCharacterController can find us Engine.register_singleton("SimulationServer", simulation_server) # --- Bomb / Defuse Objective --- bomb_objective = BombObjective.new() add_child(bomb_objective) bomb_objective.round_manager = round_manager bomb_objective.team_manager = team_manager bomb_objective.damage_processor = damage_processor bomb_objective.entity_to_peer = entity_to_peer bomb_objective.peer_to_entity = peer_to_entity # Register bomb sites from the scene tree if is_inside_tree(): var bomb_sites: Array[Area3D] = [] for n in get_tree().get_nodes_in_group("bomb_sites"): if n is Area3D: bomb_sites.append(n) if bomb_sites.size() > 0: bomb_objective.register_bomb_sites(bomb_sites) print("[GameServer] Registered %d bomb sites with BombObjective" % bomb_sites.size()) else: print("[GameServer] No bomb sites found in scene — bomb can't be planted") else: print("[GameServer] Not in scene tree yet — bomb sites will be registered later") # Wire: bomb explosion/defuse → round end bomb_objective.bomb_exploded.connect(func(_pos): if round_manager: round_manager.end_round(TeamData.Team.TERRORIST, "bomb_exploded") ) bomb_objective.bomb_defused.connect(func(_player_id): if round_manager: round_manager.end_round(TeamData.Team.COUNTER_TERRORIST, "bomb_defused") ) # Wire: round end → reset bomb round_manager.round_ended.connect(func(_winning_team, _reason): bomb_objective.reset() ) print("[GameServer] BombObjective integrated — bomb/defuse ready") 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 peer_to_entity[peer_id] = entity_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 mappings if entity_id in entity_to_peer: var pid: int = entity_to_peer[entity_id] peer_to_entity.erase(pid) 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 ## Get the current server tick counter. func get_current_tick() -> int: return _current_tick ## Configure weapon damage profile. func set_weapon(config: Dictionary) -> void: if simulation_server: simulation_server.set_weapon_config(config) # --------------------------------------------------------------------------- # Combat subsystem — player registration # --------------------------------------------------------------------------- ## Register a player node with LagCompensation and DamageProcessor. ## Call this after spawning a player entity and assigning its entity_id. ## entity_id: the simulation entity ID assigned by spawn_player_entity() ## node: the player's Node3D (CharacterBody3D) in the scene tree ## max_health: starting/maximum health for damage tracking func register_player_node(entity_id: int, node: Node3D, max_health: float = 100.0) -> void: if lag_compensation: lag_compensation.register_player_node(entity_id, node) if damage_processor: damage_processor.register_player(entity_id, max_health) # Register peer ID for economy tracking var peer_id: int = entity_to_peer.get(entity_id, -1) if peer_id >= 0 and economy_manager: economy_manager.register_player(peer_id) print("[GameServer] Registered player peer=%d with economy manager" % peer_id) print("[GameServer] Registered player node entity=%d with lag compensation + damage processor" % entity_id) ## Unregister a player node from LagCompensation and DamageProcessor. func unregister_player_node(entity_id: int) -> void: if lag_compensation: lag_compensation.unregister_player_node(entity_id) if damage_processor: damage_processor.unregister_player(entity_id) # Unregister from economy tracking var peer_id: int = entity_to_peer.get(entity_id, -1) if peer_id >= 0 and economy_manager: economy_manager.unregister_player(peer_id) print("[GameServer] Unregistered player node entity=%d" % entity_id) # --------------------------------------------------------------------------- # Combat subsystem — kill handling # --------------------------------------------------------------------------- func _on_player_killed(victim_id: int, shooter_id: int) -> void: # Relay the kill event upstream with the shooter's peer_id if available var shooter_peer: int = entity_to_peer.get(shooter_id, -1) player_damaged.emit(victim_id, shooter_id, 0.0, true) print("[GameServer] Kill: victim_entity=%d, shooter_entity=%d (peer=%d)" % [victim_id, shooter_id, shooter_peer]) ## Economy kill reward — award money to the shooter when they get a kill. ## Connected from DamageProcessor.player_killed in _ready(). func _on_kill_for_economy(victim_id: int, shooter_id: int) -> void: if economy_manager == null: return # Map entity_id → peer_id for economy lookup var shooter_peer: int = entity_to_peer.get(shooter_id, -1) if shooter_peer < 0: return economy_manager.award_kill_reward(shooter_peer) ## Round-level kill handler — tracks kills for round stats and checks elimination. ## Connected from DamageProcessor.player_killed. func _on_kill_for_round(victim_id: int, shooter_id: int) -> void: # Forward to RoundManager for kill tracking if round_manager: round_manager.on_player_killed(victim_id, shooter_id) # Check for team elimination using our entity→peer→team mapping if not round_manager or not round_manager.team_manager: return # Get teams for victim and shooter var victim_peer: int = entity_to_peer.get(victim_id, -1) var shooter_peer: int = entity_to_peer.get(shooter_id, -1) if victim_peer < 0 or shooter_peer < 0: return var victim_team: int = round_manager.team_manager.get_player_team(victim_peer) var shooter_team: int = round_manager.team_manager.get_player_team(shooter_peer) if victim_team < 0 or shooter_team < 0 or victim_team == shooter_team: return # Check if all players on victim's team are dead var all_dead: bool = true var team_player_ids: Array[int] = round_manager.team_manager.get_team_players(victim_team) for pid in team_player_ids: var eid: int = peer_to_entity.get(pid, -1) if eid < 0: continue var health: float = damage_processor.get_health(eid) if damage_processor else -1.0 if health > 0.0: all_dead = false break if all_dead and team_player_ids.size() > 0: print("[GameServer] Team elimination detected! %s eliminated by %s" % [TeamData.get_team_name(victim_team), TeamData.get_team_name(shooter_team)]) round_manager.end_round(shooter_team, "elimination") # --------------------------------------------------------------------------- # RCON command dispatch # --------------------------------------------------------------------------- ## Handle RCON commands dispatched via rcon_command_handler. func _on_rcon_command(command: String, args: PackedStringArray) -> void: match command: "start_match": if round_manager: round_manager.start_match() print("[GameServer] RCON: match started") "end_round": # end_round [team_name] [reason] var team_name: String = args[0] if args.size() > 0 else "ct" var reason: String = args[1] if args.size() > 1 else "admin" var team: int = TeamData.from_string(team_name) if team == TeamData.Team.SPECTATOR: # Default to CT team = TeamData.Team.COUNTER_TERRORIST if round_manager: round_manager.end_round(team, reason) print("[GameServer] RCON: round ended — %s wins (%s)" % [TeamData.get_team_name(team), reason]) # --------------------------------------------------------------------------- # 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): # Record positions for lag compensation BEFORE processing this tick's inputs. # This captures the state at the start of the tick, so rewind_and_raycast # can restore players to their pre-movement positions. if lag_compensation: lag_compensation.record_tick(_current_tick) 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) _current_tick += 1