fix(network): prevent null multiplayer peer crashes offline/teardown and fix Nakama RPC session validation

This commit is contained in:
2026-07-10 17:36:26 +08:00
parent 2b4c9d9dcb
commit b30709de3d
84 changed files with 5291 additions and 982 deletions
+23 -17
View File
@@ -63,7 +63,7 @@ func _ready():
# Auto-start game if coming from lobby (already connected to match)
# Works for both Nakama mode and LAN direct mode (ENet).
var is_lan_connected = LobbyManager.is_lan_mode and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED
if (NakamaManager.is_connected_to_nakama() or is_lan_connected) and multiplayer.get_unique_id() != 0:
if multiplayer.has_multiplayer_peer() and (NakamaManager.is_connected_to_nakama() or is_lan_connected) and multiplayer.get_unique_id() != 0:
print("Coming from lobby - auto-starting game...")
await get_tree().process_frame
_auto_start_from_lobby()
@@ -221,7 +221,7 @@ func _setup_effect_elevation():
func _on_goal_count_updated(peer_id: int, count: int):
# Only update for local player
if peer_id == multiplayer.get_unique_id():
if multiplayer.has_multiplayer_peer() and peer_id == multiplayer.get_unique_id():
ui_manager.update_goal_count_label(count)
func _init_managers():
@@ -564,15 +564,16 @@ func _process(delta):
func _on_match_joined(match_id: String):
var network_panel = get_node_or_null("PauseMenu/Panel/NetworkPanel")
if network_panel:
network_panel.get_node("NetworkInfo/UniquePeerID").text = str(multiplayer.get_unique_id())
if multiplayer.is_server():
network_panel.get_node("NetworkInfo/UniquePeerID").text = str(multiplayer.get_unique_id() if multiplayer and multiplayer.has_multiplayer_peer() else 0)
if multiplayer and multiplayer.has_multiplayer_peer() and multiplayer.is_server():
network_panel.get_node("NetworkInfo/NetworkSideDisplay").text = "Server (Match: %s)" % match_id
_setup_host_game()
else:
network_panel.get_node("NetworkInfo/NetworkSideDisplay").text = "Client"
_setup_client_game()
else:
if multiplayer.is_server():
# If the UI isn't ready yet, still need to setup logic based on mode
if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
_setup_host_game()
else:
_setup_client_game()
@@ -683,7 +684,9 @@ func _spawn_lobby_client_sync(peer_id: int):
func _setup_client_game():
"""Setup client when transitioning from lobby."""
var my_id = multiplayer.get_unique_id()
var my_id = 0
if multiplayer and multiplayer.has_multiplayer_peer():
my_id = multiplayer.get_unique_id()
print("Client setup - my peer ID: ", my_id)
# INITIALIZE ARENA SIZE for Stop n Go locally to prevent out-of-bounds before sync arrives
@@ -766,8 +769,8 @@ func _auto_start_from_lobby():
# Update NetworkPanel in PauseMenu (if exists)
var network_panel = get_node_or_null("PauseMenu/Panel/NetworkPanel")
if network_panel:
network_panel.get_node("NetworkInfo/UniquePeerID").text = str(multiplayer.get_unique_id())
if multiplayer.is_server():
network_panel.get_node("NetworkInfo/UniquePeerID").text = str(multiplayer.get_unique_id() if multiplayer and multiplayer.has_multiplayer_peer() else 0)
if multiplayer and multiplayer.has_multiplayer_peer() and multiplayer.is_server():
network_panel.get_node("NetworkInfo/NetworkSideDisplay").text = "Host (Match: %s)" % short_id
else:
network_panel.get_node("NetworkInfo/NetworkSideDisplay").text = "Client (Match: %s)" % short_id
@@ -1385,7 +1388,7 @@ func add_player_character(peer_id: int, is_bot: bool = false):
print("[Main] Set player %d name to %s from Lobby data" % [peer_id, p_name])
break
if peer_id == multiplayer.get_unique_id():
if multiplayer.has_multiplayer_peer() and peer_id == multiplayer.get_unique_id():
GameStateManager.local_player_character = player_character
ui_manager.set_local_player(player_character)
if touch_controls:
@@ -1608,7 +1611,7 @@ func sync_playerboard(player_id: int, new_playerboard: Array):
player.playerboard = new_playerboard.duplicate()
# Update UI for local player
if player_id == multiplayer.get_unique_id() and GameStateManager.local_player_character:
if multiplayer.has_multiplayer_peer() and player_id == multiplayer.get_unique_id() and GameStateManager.local_player_character:
ui_manager.update_playerboard_ui()
@rpc("any_peer", "call_local", "reliable")
@@ -1628,7 +1631,7 @@ func sync_playerboard_slot(player_id: int, slot_index: int, item_id: int):
return
# Update UI for local player only
if player_id == multiplayer.get_unique_id() and GameStateManager.local_player_character:
if multiplayer.has_multiplayer_peer() and player_id == multiplayer.get_unique_id() and GameStateManager.local_player_character:
ui_manager.update_playerboard_ui()
@@ -1649,7 +1652,7 @@ func verify_all_connections():
@rpc
func connection_verify(expected_players: Array):
for peer_id in expected_players:
if peer_id != multiplayer.get_unique_id() and not has_node(str(peer_id)):
if multiplayer.has_multiplayer_peer() and peer_id != multiplayer.get_unique_id() and not has_node(str(peer_id)):
rpc_id(1, "request_specific_player_data", peer_id)
@rpc("any_peer")
@@ -1721,9 +1724,9 @@ func create_specific_player(data: Dictionary):
# Force collision update
if player_character.has_node("CollisionShape3D"):
player_character.get_node("CollisionShape3D").disabled = false
# Wait for player managers to initialize (player.gd has 0.1s await in _ready)
# Check if this is the local player (client's own player)
var is_local_player = (peer_id == multiplayer.get_unique_id())
var is_local_player = multiplayer.has_multiplayer_peer() and (peer_id == multiplayer.get_unique_id())
if is_local_player and GameStateManager.local_player_character == null:
GameStateManager.local_player_character = player_character
ui_manager.set_local_player(player_character)
@@ -2326,7 +2329,7 @@ func _show_game_over_panel():
canvas_layer.add_child(panel)
# Populate data
var local_peer_id = multiplayer.get_unique_id()
var local_peer_id = multiplayer.get_unique_id() if multiplayer.has_multiplayer_peer() else 0
panel.setup(all_player_scores, local_peer_id)
# --- REPORT NAKAMA MATCH STATS ---
@@ -2351,7 +2354,8 @@ func _show_game_over_panel():
# Connect signals
panel.back_pressed.connect(_on_back_to_menu_pressed)
panel.rematch_pressed.connect(func():
LobbyManager.request_rematch.rpc(multiplayer.get_unique_id())
if multiplayer.has_multiplayer_peer():
LobbyManager.request_rematch.rpc(multiplayer.get_unique_id())
)
LobbyManager.rematch_votes_updated.connect(func(count, required):
@@ -2479,7 +2483,9 @@ func _render_leaderboard_entries(sorted_player_data: Array):
if not vbox: vbox = leaderboard_panel.get_node_or_null("VBox")
if not vbox: return
var my_id = multiplayer.get_unique_id()
var my_id = 0
if multiplayer and multiplayer.has_multiplayer_peer():
my_id = multiplayer.get_unique_id()
var my_index = -1
for i in range(sorted_player_data.size()):
if sorted_player_data[i].peer_id == my_id:
+20 -16
View File
@@ -33,7 +33,7 @@ var display_name: String:
# Helper to check network status
func can_rpc() -> bool:
if not multiplayer.has_multiplayer_peer(): return false
if not is_inside_tree() or not multiplayer or not multiplayer.has_multiplayer_peer(): return false
if multiplayer.multiplayer_peer.get_class() == "OfflineMultiplayerPeer": return false
return multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED
@@ -81,7 +81,7 @@ var is_charged_strike: bool = false:
_refresh_player_visuals()
# Sync to others if we are the authority
if is_multiplayer_authority() and can_rpc():
if is_inside_tree() and is_multiplayer_authority() and can_rpc() and multiplayer.has_multiplayer_peer():
rpc("sync_charged_strike", is_charged_strike)
@rpc("any_peer", "call_local", "reliable")
@@ -219,8 +219,8 @@ func _ready():
# This prevents "grounding" friction and gravity from fighting our Tweens
motion_mode = CharacterBody3D.MOTION_MODE_FLOATING
print("[Player] initialized: ", name, " Authority: ", get_multiplayer_authority())
# name = str(get_multiplayer_authority()) # CRITICAL FIX: Do NOT overwrite name. Bots have authority 1 but unique names (IDs).
var authority_id = get_multiplayer_authority() if multiplayer and multiplayer.has_multiplayer_peer() else 0
print("[Player] initialized: ", name, " Authority: ", authority_id)
# Look up player's display name from LobbyManager
var my_id = get_multiplayer_authority()
@@ -255,7 +255,7 @@ func _ready():
var mat = ShaderMaterial.new()
mat.shader = pointer_shader
if name == str(multiplayer.get_unique_id()):
if multiplayer and multiplayer.has_multiplayer_peer() and name == str(multiplayer.get_unique_id()):
mat.set_shader_parameter("pointer_color", Color(0.0, 1.0, 0.0, 1.0)) # Green
if has_node("Name"):
$Name.modulate = Color(0.2, 0.8, 0.2, 1.0) # Pleasant Green for Name
@@ -308,7 +308,7 @@ func _ready():
current_position = _find_random_spawn_position()
update_player_position(current_position)
spawn_point_selected = true
if can_rpc():
if can_rpc() and multiplayer.has_multiplayer_peer():
rpc("set_spawn_position", current_position)
rpc("notify_spawn_selected", current_position)
@@ -318,11 +318,11 @@ func _ready():
var char_index = (bot_id_val - 1) % bot_characters.size()
var bot_char_name = bot_characters[char_index]
set_character(bot_char_name)
if is_multiplayer_authority() and can_rpc():
if can_rpc() and multiplayer.has_multiplayer_peer():
rpc("sync_character", bot_char_name)
# Sync bot status to network
if is_multiplayer_authority() and can_rpc():
if can_rpc() and multiplayer.has_multiplayer_peer():
rpc("sync_bot_status", true)
# Continue to manager initialization...
@@ -347,7 +347,7 @@ func _ready():
current_position.y * 1 + 1 * 0.5
)
target_visual_position = global_position
if is_multiplayer_authority() and can_rpc():
if can_rpc() and multiplayer.has_multiplayer_peer():
rpc("sync_position", current_position)
else:
target_visual_position = global_position
@@ -417,7 +417,9 @@ func play_scatter_knock():
func play_playerboard_scatter():
"""Show the one-shot scatter VFX over the playerboard UI, but only on the
local human player's own client (the board UI belongs to them)."""
var is_local = name == str(multiplayer.get_unique_id())
var is_local = false
if multiplayer and multiplayer.has_multiplayer_peer():
is_local = name == str(multiplayer.get_unique_id())
var is_bot_check = is_bot or is_in_group("Bots")
if not is_local or is_bot_check:
return
@@ -672,7 +674,7 @@ func _setup_character() -> void:
return
var character_name = "Masbro" # Default
var player_authority_id = get_multiplayer_authority()
var player_authority_id = get_multiplayer_authority() if multiplayer and multiplayer.has_multiplayer_peer() else name.to_int()
# Look up character from LobbyManager for this player (works for all players)
if LobbyManager:
@@ -896,7 +898,9 @@ func sync_playerboard(new_playerboard: Array):
# Only update UI if this is the LOCAL HUMAN PLAYER
# Bots managed by server (ID 1) might sync here, but we must NOT update UI for them
var is_local = name == str(multiplayer.get_unique_id())
var is_local = false
if multiplayer and multiplayer.has_multiplayer_peer():
is_local = name == str(multiplayer.get_unique_id())
var is_bot_check = is_bot or is_in_group("Bots")
# DEBUG: Trace why UI updates might be happening
@@ -1376,7 +1380,7 @@ func _process(delta):
if LobbyManager.get_randomize_spawn() and spawn_point_selected and not visible:
visible = true
if not multiplayer.has_multiplayer_peer():
if not multiplayer or not multiplayer.has_multiplayer_peer():
return
if is_multiplayer_authority():
@@ -1478,7 +1482,7 @@ var last_sent_position: Vector3
func _physics_process(delta):
# Sync position periodically (Heartbeat / Smoothing)
if not multiplayer.has_multiplayer_peer():
if not multiplayer or not multiplayer.has_multiplayer_peer():
return
if is_multiplayer_authority():
@@ -2018,7 +2022,7 @@ func bot_try_grab_item() -> bool:
#if empty_slot == -1:
#return false
#
#if is_multiplayer_authority():
#if is_multiplayer_authority() and can_rpc() and multiplayer.has_multiplayer_peer():
#rpc("bot_grab_item", grid_position, empty_slot, cell.x, cell.y, cell.z)
#return true
#
@@ -2065,7 +2069,7 @@ func bot_try_grab_item() -> bool:
#return false # no space
#
## Perform the grab and auto-place
#if is_multiplayer_authority():
#if is_multiplayer_authority() and can_rpc() and multiplayer.has_multiplayer_peer():
## Update gridmap: remove item
#rpc("sync_grid_item", cell.x, cell.y, cell.z, -1)
## Update playerboard
+6 -6
View File
@@ -4,12 +4,12 @@
[ext_resource type="Theme" uid="uid://cxab3xxy00" path="res://assets/themes/GUI_Tekton.tres" id="1_pti1t"]
[ext_resource type="Texture2D" uid="uid://jqvv6s55mlsk" path="res://assets/graphics/gui/BG.png" id="3_3vyn0"]
[ext_resource type="FontFile" uid="uid://xnjx058n4tsw" path="res://assets/fonts/Nougat-ExtraBlack.ttf" id="3_font"]
[ext_resource type="PackedScene" uid="uid://ejeamn0pyey4" path="res://assets/characters/Bob.glb" id="4_bob"]
[ext_resource type="PackedScene" uid="uid://d4cul3w3wem5w" path="res://assets/characters/Gatot.glb" id="4_gatot"]
[ext_resource type="PackedScene" uid="uid://1vk0mjnwkngi" path="res://assets/characters/Masbro.glb" id="4_masbro"]
[ext_resource type="PackedScene" uid="uid://bmln7v6v5kvxg" path="res://assets/characters/Oldpop.glb" id="4_oldpop"]
[ext_resource type="PackedScene" uid="uid://5qdk1umx2rjf" path="res://assets/characters/Bob.glb" id="4_bob"]
[ext_resource type="PackedScene" uid="uid://bfujakntxa0v6" path="res://assets/characters/Gatot.glb" id="4_gatot"]
[ext_resource type="PackedScene" uid="uid://cfjx66gthp1c5" path="res://assets/characters/Masbro.glb" id="4_masbro"]
[ext_resource type="PackedScene" uid="uid://cxvbrdybeglt5" path="res://assets/characters/Oldpop.glb" id="4_oldpop"]
[ext_resource type="Texture2D" uid="uid://brhn1dhp1gm13" path="res://assets/graphics/character_selection/sc_characters/sc_copper.png" id="4_pti1t"]
[ext_resource type="AnimationLibrary" uid="uid://c3pyopnwibckj" path="res://assets/characters/animations/animation-pack.res" id="5_animlib"]
[ext_resource type="AnimationLibrary" path="res://assets/characters/animations/animation-pack.res" id="5_animlib"]
[ext_resource type="Texture2D" uid="uid://b5pp08fke7ptd" path="res://assets/graphics/gui/lobby/gold.png" id="tex_gold"]
[ext_resource type="Texture2D" uid="uid://d0ouvm3x8h42c" path="res://assets/graphics/gui/lobby/star.png" id="tex_star"]
@@ -388,7 +388,7 @@ unique_name_in_owner = true
own_world_3d = true
transparent_bg = true
handle_input_locally = false
size = Vector2i(651, 554)
size = Vector2i(651, 558)
render_target_update_mode = 4
[node name="WorldEnvironment" type="WorldEnvironment" parent="MainMargin/MainHBox/CenterCol/ViewportWrapper/ViewportContainer/PreviewViewport" unique_id=1660814495]