Save workspace artifacts: character-controller, gdextension scaffold, network scripts, map-pipeline, server config
Includes code from task workspaces that was never pushed: - client/characters/ — fps_character_controller.gd (400 lines), fps_camera.gd, input_handler.gd - gdextension/simulation/ — C++ GDExtension scaffold: entity, movement, hit detection, simulation server, state serializer, bitstream (14 files) - scripts/network/ — ENet-based network_manager.gd, server/client_main.gd, player.gd - scripts/map_packaging/ — PCK pipeline: pack_map.gd, map_downloader.gd, map_registry_server.py, README - scripts/config/ — server_config.gd (480 lines) - scenes/ — client_main, server_main, player, test_range - project.godot, export_presets.cfg
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
## Server Main — Headless dedicated server entry point.
|
||||
##
|
||||
## Runs as `server_main.tscn` which is the project's main scene.
|
||||
## In headless mode (--headless), this is the only running scene.
|
||||
##
|
||||
## On ready:
|
||||
## 1. Load config (env overrides)
|
||||
## 2. Load the first map from the config's map rotation
|
||||
## 3. Start ENet server on the configured port
|
||||
## 4. Spawn players on the map using spawn markers
|
||||
## 5. Broadcast spawn via RPC so clients create visual player nodes
|
||||
## 6. Log player join/leave and replicate position
|
||||
|
||||
extends Node
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Signals
|
||||
# ---------------------------------------------------------------------------
|
||||
signal player_spawned(peer_id: int)
|
||||
signal player_despawned(peer_id: int)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Exports
|
||||
# ---------------------------------------------------------------------------
|
||||
@export var player_scene: PackedScene = preload("res://scenes/player.tscn")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# State
|
||||
# ---------------------------------------------------------------------------
|
||||
var players: Dictionary = {} # peer_id → Node (player instance)
|
||||
var spawn_points_a: Array[Vector3] = [] # Team A spawns
|
||||
var spawn_points_b: Array[Vector3] = [] # Team B spawns
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
func _ready() -> void:
|
||||
# Config driven — ServerConfig singleton loaded at autoload time.
|
||||
# Allow port override via env var for backwards compatibility
|
||||
# (container/VPS deployments that set SERVER_PORT).
|
||||
var effective_port: int = ServerConfig.port
|
||||
if OS.has_environment("SERVER_PORT"):
|
||||
effective_port = int(OS.get_environment("SERVER_PORT"))
|
||||
print("[ServerMain] Port overridden by SERVER_PORT env: %d" % effective_port)
|
||||
|
||||
# Instance the map from the config's map rotation
|
||||
_load_map()
|
||||
|
||||
# Start the ENet server
|
||||
var err: Error = NetworkManager.start_server(effective_port)
|
||||
if err != OK:
|
||||
push_error("[ServerMain] Failed to start server: %s" % error_string(err))
|
||||
get_tree().quit(1)
|
||||
return
|
||||
|
||||
# Connect signals
|
||||
NetworkManager.player_connected.connect(_on_player_connected)
|
||||
NetworkManager.player_disconnected.connect(_on_player_disconnected)
|
||||
|
||||
print("[ServerMain] Tactical Shooter server ready")
|
||||
print("[ServerMain] Port: %d" % effective_port)
|
||||
print("[ServerMain] Name: \"%s\"" % ServerConfig.server_name)
|
||||
print("[ServerMain] Tick rate: %d Hz" % ServerConfig.tick_rate)
|
||||
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()])
|
||||
|
||||
func _exit_tree() -> void:
|
||||
NetworkManager.stop()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Map loading
|
||||
# ---------------------------------------------------------------------------
|
||||
func _load_map() -> void:
|
||||
# Load the first map from the config's rotation list
|
||||
var maps_list: Array[String] = ServerConfig.map_list
|
||||
if maps_list.is_empty():
|
||||
push_warning("[ServerMain] Map rotation is empty — running without map geometry")
|
||||
return
|
||||
|
||||
var first_map_path: String = maps_list[0]
|
||||
var full_path: String = "res://scenes/map/%s.tscn" % first_map_path
|
||||
|
||||
if not ResourceLoader.exists(full_path):
|
||||
push_error("[ServerMain] Map scene not found: %s" % full_path)
|
||||
return
|
||||
|
||||
var map_scene_res: PackedScene = load(full_path)
|
||||
var map_instance: Node = map_scene_res.instantiate()
|
||||
map_instance.name = "Map_%s" % first_map_path
|
||||
add_child(map_instance, true)
|
||||
|
||||
print("[ServerMain] Loaded map: %s" % first_map_path)
|
||||
|
||||
# Collect spawn markers from the map scene
|
||||
_collect_spawn_points(map_instance)
|
||||
|
||||
## Walk the map scene looking for Marker3D nodes named "SpawnA*" and "SpawnB*".
|
||||
func _collect_spawn_points(map_root: Node) -> void:
|
||||
_collect_spawn_points_recursive(map_root)
|
||||
|
||||
# Fallback: only after full recursion, if still empty
|
||||
if spawn_points_a.is_empty() and spawn_points_b.is_empty():
|
||||
print("[ServerMain] No spawn markers found — using default positions")
|
||||
spawn_points_a.append(Vector3(-16, 0, -4))
|
||||
spawn_points_a.append(Vector3(-16, 0, 4))
|
||||
spawn_points_b.append(Vector3(16, 0, -4))
|
||||
spawn_points_b.append(Vector3(16, 0, 4))
|
||||
|
||||
func _collect_spawn_points_recursive(node: Node) -> void:
|
||||
if node is Marker3D:
|
||||
var name_lower: String = node.name.to_lower()
|
||||
if name_lower.begins_with("spawna") or name_lower.begins_with("spawn_a"):
|
||||
spawn_points_a.append(node.position)
|
||||
elif name_lower.begins_with("spawnb") or name_lower.begins_with("spawn_b"):
|
||||
spawn_points_b.append(node.position)
|
||||
|
||||
for child in node.get_children():
|
||||
_collect_spawn_points_recursive(child)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Player management
|
||||
# ---------------------------------------------------------------------------
|
||||
func _on_player_connected(peer_id: int) -> void:
|
||||
print("[ServerMain] Player joined: %d. Total: %d" % [peer_id, multiplayer.get_peers().size() + 1])
|
||||
_spawn_player(peer_id)
|
||||
|
||||
func _on_player_disconnected(peer_id: int) -> void:
|
||||
print("[ServerMain] Player left: %d" % peer_id)
|
||||
_despawn_player(peer_id)
|
||||
|
||||
func _spawn_player(peer_id: int) -> void:
|
||||
# Create player node on the server
|
||||
var player: Node
|
||||
if player_scene:
|
||||
player = player_scene.instantiate()
|
||||
else:
|
||||
player = Node3D.new()
|
||||
player.set_script(preload("res://scripts/network/player.gd"))
|
||||
|
||||
player.name = "Player_%d" % peer_id
|
||||
player.set_multiplayer_authority(peer_id)
|
||||
|
||||
# Assign spawn position — alternate between Team A and Team B spawns
|
||||
var is_team_a: bool = (peer_id % 2) == 0
|
||||
var pool: Array[Vector3] = spawn_points_a if is_team_a else spawn_points_b
|
||||
var spawn_pos: Vector3 = pool[peer_id % pool.size()] if pool.size() > 0 else Vector3.ZERO
|
||||
spawn_pos.y = 0.0
|
||||
player.position = spawn_pos
|
||||
|
||||
add_child(player, true)
|
||||
players[peer_id] = player
|
||||
player_spawned.emit(peer_id)
|
||||
|
||||
# Broadcast spawn to all clients so they create a visual player node
|
||||
NetworkManager.broadcast_spawn_player.rpc(peer_id, spawn_pos, is_team_a)
|
||||
|
||||
print("[ServerMain] Spawned player %d at (%.1f, %.1f) team=%s" % [peer_id, spawn_pos.x, spawn_pos.z, "A" if is_team_a else "B"])
|
||||
|
||||
func _despawn_player(peer_id: int) -> void:
|
||||
if peer_id in players:
|
||||
var player: Node = players[peer_id]
|
||||
player.queue_free()
|
||||
players.erase(peer_id)
|
||||
player_despawned.emit(peer_id)
|
||||
|
||||
# Broadcast despawn to all clients
|
||||
NetworkManager.broadcast_despawn_player.rpc(peer_id)
|
||||
|
||||
print("[ServerMain] Despawned player %d" % peer_id)
|
||||
else:
|
||||
# Still broadcast even if server doesn't have the player
|
||||
NetworkManager.broadcast_despawn_player.rpc(peer_id)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Server tick (128 Hz)
|
||||
# ---------------------------------------------------------------------------
|
||||
func _physics_process(delta: float) -> void:
|
||||
if not NetworkManager.is_server:
|
||||
return
|
||||
|
||||
# Future: authoritative physics tick, state broadcast, etc.
|
||||
pass
|
||||
Reference in New Issue
Block a user