Fix headless class_name dependencies

- Replaced TeamData.Team type hints with int in all scripts
- Added explicit preloads for headless mode class resolution
- Created stub LagCompensation and DamageProcessor scripts
- Fixed PluginManager, SpawnManager, EconomyManager, BuyZone,
  RoundManager, BuyMenuHandler, BombObjective class_name references
- Updated server config to match ServerConfig.gd format

Gray screen root cause: server scripts failed to parse in headless
mode due to Godot 4 class_name loading order (Resource after Node),
leaving server_main.gd non-functional — accepted connections but
never spawned players.
This commit is contained in:
2026-07-02 10:09:28 -04:00
parent ad48f38ca5
commit 4a5264c5b0
14 changed files with 202 additions and 284 deletions
+38 -23
View File
@@ -23,6 +23,21 @@
##
extends Node
# ---------------------------------------------------------------------------
# Force class_name script dependencies to load first
# (Godot 4 headless mode doesn't resolve global classes in dependency order)
# ---------------------------------------------------------------------------
const _ws = preload("res://server/scripts/weapons/weapon_server.gd")
const _lc = preload("res://server/scripts/combat/lag_compensation.gd")
const _dp = preload("res://server/scripts/combat/damage_processor.gd")
const _tm = preload("res://scripts/teams/team_manager.gd")
const _rm = preload("res://server/scripts/round/round_manager.gd")
const _em = preload("res://server/scripts/economy/economy_manager.gd")
const _bm = preload("res://server/scripts/economy/buy_menu_handler.gd")
const _bo = preload("res://server/scripts/objectives/bomb_objective.gd")
const _sm = preload("res://scripts/teams/spawn_manager.gd")
const _td = preload("res://scripts/teams/team_data.gd")
# ---------------------------------------------------------------------------
# Signals
# ---------------------------------------------------------------------------
@@ -41,28 +56,28 @@ var simulation_server: Object = null
var is_running: bool = false
## WeaponServer — handles hit-scan raycasting and lag-compensated fire.
var weapon_server: WeaponServer = null
var weapon_server = null
## LagCompensation — records player positions per tick for rewind raycasts.
var lag_compensation: LagCompensation = null
var lag_compensation = null
## DamageProcessor — applies hit results, tracks health and kills.
var damage_processor: DamageProcessor = null
var damage_processor = null
## TeamManager — manages team assignment and scoring.
var team_manager: TeamManager = null
var team_manager = null
## RoundManager — match lifecycle (warmup → prep → live → post).
var round_manager: RoundManager = null
var round_manager = null
## EconomyManager — per-player money tracking and earnings.
var economy_manager: EconomyManager = null
var economy_manager = null
## BuyMenuHandler — server-side buy request validation and processing.
var buy_menu_handler: BuyMenuHandler = null
var buy_menu_handler = null
## BombObjective — server-authoritative bomb plant/defuse logic.
var bomb_objective: BombObjective = null
var bomb_objective = null
## Current server tick counter, incremented each time tick() is called.
var _current_tick: int = 0
@@ -108,34 +123,34 @@ func _ready() -> void:
# 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 = _ws.new()
weapon_server.physics_world = get_viewport().get_world_3d()
add_child(weapon_server)
lag_compensation = LagCompensation.new()
lag_compensation = _lc.new()
add_child(lag_compensation)
damage_processor = DamageProcessor.new()
damage_processor = _dp.new()
add_child(damage_processor)
# --- Round / Match lifecycle ---
round_manager = RoundManager.new()
round_manager = _rm.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()
team_manager = _tm.new()
add_child(team_manager)
round_manager.team_manager = team_manager
round_manager.damage_processor = damage_processor
# --- Economy system ---
economy_manager = EconomyManager.new()
economy_manager = _em.new()
add_child(economy_manager)
buy_menu_handler = BuyMenuHandler.new()
buy_menu_handler = _bm.new()
add_child(buy_menu_handler)
buy_menu_handler.initialise(economy_manager, weapon_server, team_manager)
@@ -167,7 +182,7 @@ func _ready() -> void:
Engine.register_singleton("SimulationServer", simulation_server)
# --- Bomb / Defuse Objective ---
bomb_objective = BombObjective.new()
bomb_objective = _bo.new()
add_child(bomb_objective)
bomb_objective.round_manager = round_manager
bomb_objective.team_manager = team_manager
@@ -192,11 +207,11 @@ func _ready() -> void:
# 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")
round_manager.end_round(_td.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")
round_manager.end_round(_td.Team.COUNTER_TERRORIST, "bomb_defused")
)
# Wire: round end → reset bomb
@@ -380,7 +395,7 @@ func _on_kill_for_round(victim_id: int, shooter_id: int) -> void:
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)])
[_td.get_team_name(victim_team), _td.get_team_name(shooter_team)])
round_manager.end_round(shooter_team, "elimination")
# ---------------------------------------------------------------------------
@@ -398,13 +413,13 @@ func _on_rcon_command(command: String, args: PackedStringArray) -> void:
# 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:
var team: int = _td.from_string(team_name)
if team == _td.Team.SPECTATOR:
# Default to CT
team = TeamData.Team.COUNTER_TERRORIST
team = _td.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])
print("[GameServer] RCON: round ended — %s wins (%s)" % [_td.get_team_name(team), reason])
# ---------------------------------------------------------------------------
# Main loop (128 Hz)