7bbe4ba71d
- Removed the is_predicting() early-return in weapon_manager._rollback_tick which blocked all weapon actions on the client (player owned by server) - Added client-side weapon loadout initialization in player-spawner._spawn so the player has weapons immediately, before server state sync arrives - Server reconciles via rollback state sync Movement was fixed in v0.2.2 (window focus grab). This addresses the remaining 'can't switch or use weapons' issue.
206 lines
6.7 KiB
GDScript
206 lines
6.7 KiB
GDScript
extends Node
|
|
|
|
@export var player_scene: PackedScene
|
|
|
|
## Whether to spawn an avatar for the host (peer 1) when on_server_start fires.
|
|
## Set to false for dedicated server operation.
|
|
@export var spawn_host_avatar: bool = true
|
|
|
|
## When true, this is a dedicated server. Peer 1 (the server itself) never
|
|
## gets a player avatar. Clients also skip spawning peer 1 locally.
|
|
var _dedicated_server: bool = false
|
|
|
|
## Mark this spawner as running on a dedicated server.
|
|
## The host avatar (peer 1) is skipped entirely — no avatar is spawned for the
|
|
## server process itself.
|
|
func set_dedicated_server() -> void:
|
|
_dedicated_server = true
|
|
print("[PlayerSpawner] Dedicated server mode enabled")
|
|
|
|
var t_spawn_points: Array[Marker3D] = []
|
|
var ct_spawn_points: Array[Marker3D] = []
|
|
var team_manager: TeamManager
|
|
var round_manager: RoundManager
|
|
var economy_manager: EconomyManager
|
|
var bomb: Bomb
|
|
var avatars: Dictionary = {}
|
|
|
|
func _ready():
|
|
team_manager = $"../TeamManager"
|
|
round_manager = $"../RoundManager"
|
|
economy_manager = $"../EconomyManager"
|
|
bomb = get_tree().current_scene.get_node_or_null("Network/Bomb")
|
|
|
|
# Discover spawn points from scene containers
|
|
var scene_root := get_tree().current_scene
|
|
var t_container := scene_root.get_node_or_null("T Spawns")
|
|
if t_container:
|
|
for child in t_container.get_children():
|
|
if child is Marker3D:
|
|
t_spawn_points.append(child)
|
|
var ct_container := scene_root.get_node_or_null("CT Spawns")
|
|
if ct_container:
|
|
for child in ct_container.get_children():
|
|
if child is Marker3D:
|
|
ct_spawn_points.append(child)
|
|
|
|
NetworkEvents.on_client_start.connect(_handle_connected)
|
|
NetworkEvents.on_server_start.connect(_handle_host)
|
|
NetworkEvents.on_peer_join.connect(_handle_new_peer)
|
|
NetworkEvents.on_peer_leave.connect(_handle_leave)
|
|
NetworkEvents.on_client_stop.connect(_handle_stop)
|
|
NetworkEvents.on_server_stop.connect(_handle_stop)
|
|
|
|
if round_manager:
|
|
round_manager.round_started.connect(_handle_round_start)
|
|
|
|
func _handle_connected(id: int):
|
|
_spawn(id)
|
|
|
|
func _handle_host():
|
|
if _dedicated_server:
|
|
print("[PlayerSpawner] Dedicated server — skipping host avatar entirely")
|
|
# Match starts when the first client connects (handled by headless_server)
|
|
return
|
|
if not spawn_host_avatar:
|
|
print("[PlayerSpawner] spawn_host_avatar=false — skipping host avatar")
|
|
if round_manager:
|
|
round_manager.start_match()
|
|
return
|
|
_spawn(1)
|
|
# Auto-start match
|
|
if round_manager:
|
|
round_manager.start_match()
|
|
|
|
func _handle_new_peer(id: int):
|
|
# On a dedicated server, peer 1 (the server process) has no avatar.
|
|
# When a client connects, it receives on_peer_join for peer 1, but
|
|
# should skip spawning because the server doesn't have that avatar.
|
|
# This also applies to listen-server: the server replicates peer 1's
|
|
# avatar to clients; spawning a local copy would duplicate it.
|
|
if id == 1 and not multiplayer.is_server():
|
|
print("[PlayerSpawner] Client skipping spawn for peer 1 (server has no dedicated avatar)")
|
|
return
|
|
if _dedicated_server and id == 1:
|
|
return
|
|
if id == 1 and not spawn_host_avatar:
|
|
print("[PlayerSpawner] Skipping spawn for peer 1 (spawn_host_avatar=false)")
|
|
return
|
|
_spawn(id)
|
|
if multiplayer.is_server():
|
|
team_manager.sync_to_peer(id)
|
|
if economy_manager:
|
|
economy_manager.sync_to_peer(id)
|
|
if bomb:
|
|
bomb.sync_to_peer(id)
|
|
if avatars.size() <= 2:
|
|
# First opponent joined — restart round so both spawn alive
|
|
round_manager.start_round()
|
|
else:
|
|
# 3+ players: sync state, spectate until next round
|
|
round_manager.sync_to_peer(id)
|
|
var avatar = avatars.get(id)
|
|
if avatar and avatar.has_method("set_spectator"):
|
|
avatar.call_deferred("set_spectator")
|
|
|
|
func _handle_leave(id: int):
|
|
if not avatars.has(id):
|
|
return
|
|
|
|
var avatar = avatars[id] as Node
|
|
avatar.queue_free()
|
|
avatars.erase(id)
|
|
if economy_manager:
|
|
economy_manager.remove_player(id)
|
|
|
|
func _handle_stop():
|
|
for avatar in avatars.values():
|
|
avatar.queue_free()
|
|
avatars.clear()
|
|
|
|
func _handle_round_start(_round_number: int):
|
|
if not multiplayer.is_server():
|
|
return
|
|
# Respawn all players at their team spawn points
|
|
for peer_id in avatars:
|
|
var avatar = avatars[peer_id] as CharacterBody3D
|
|
if avatar == null:
|
|
continue
|
|
# Fresh spawns (_has_played_round=false) always get default loadout
|
|
var survived: bool = not avatar.is_dead and avatar._has_played_round
|
|
var spawn_pos := get_team_spawn_point(peer_id)
|
|
avatar.round_respawn(spawn_pos, survived)
|
|
|
|
# Assign bomb to a random T player
|
|
if bomb:
|
|
var t_members := team_manager.get_team_members(TeamManager.Team.T)
|
|
if not t_members.is_empty():
|
|
var idx := hash(_round_number * 31) % t_members.size()
|
|
bomb.assign_to_player(t_members[idx])
|
|
|
|
func _spawn(id: int):
|
|
# Auto-assign team on server
|
|
if multiplayer.is_server():
|
|
team_manager.auto_assign(id)
|
|
|
|
var avatar = player_scene.instantiate() as Node
|
|
avatars[id] = avatar
|
|
avatar.name += " #%d" % id
|
|
add_child(avatar)
|
|
avatar.global_position = get_team_spawn_point(id)
|
|
|
|
# Avatar is always owned by server
|
|
avatar.set_multiplayer_authority(1)
|
|
|
|
# Pass references
|
|
if avatar.has_method("setup_references"):
|
|
avatar.setup_references(team_manager, round_manager)
|
|
|
|
# Init economy
|
|
if multiplayer.is_server() and economy_manager:
|
|
economy_manager.init_player(id)
|
|
|
|
print("Spawned avatar %s at %s" % [avatar.name, multiplayer.get_unique_id()])
|
|
|
|
# Avatar's input object is owned by player
|
|
var input = avatar.find_child("Input")
|
|
if input != null:
|
|
input.set_multiplayer_authority(id)
|
|
print("Set input(%s) ownership to %s" % [input.name, id])
|
|
|
|
# Notify RollbackSynchronizer about ownership changes
|
|
var rollback_sync = avatar.find_child("RollbackSynchronizer")
|
|
if rollback_sync and rollback_sync.has_method("process_settings"):
|
|
rollback_sync.process_settings()
|
|
|
|
# On the client side, initialize weapon loadout locally so the player
|
|
# has weapons immediately. The server will reconcile via rollback state sync.
|
|
if not multiplayer.is_server():
|
|
var weapon_mgr = avatar.find_child("WeaponManager")
|
|
if weapon_mgr and weapon_mgr.has_method("set_default_loadout"):
|
|
# Default to T team — server will correct via state sync
|
|
weapon_mgr.set_default_loadout(1) # TeamManager.Team.T = 1
|
|
print("[Spawner] Client-side weapon loadout initialized for peer %s" % id)
|
|
|
|
func get_team_spawn_point(peer_id: int, spawn_idx: int = 0) -> Vector3:
|
|
var team := team_manager.get_team(peer_id)
|
|
var points: Array[Marker3D]
|
|
|
|
match team:
|
|
TeamManager.Team.T:
|
|
points = t_spawn_points
|
|
TeamManager.Team.CT:
|
|
points = ct_spawn_points
|
|
_:
|
|
# Fallback: use T spawns
|
|
points = t_spawn_points
|
|
|
|
if points.is_empty():
|
|
return Vector3.ZERO
|
|
|
|
var idx := hash(peer_id * 37 + spawn_idx * 19) % points.size()
|
|
return points[idx].global_position
|
|
|
|
func get_next_spawn_point(peer_id: int, spawn_idx: int = 0) -> Vector3:
|
|
return get_team_spawn_point(peer_id, spawn_idx)
|