fix(network): prevent null multiplayer peer crashes offline/teardown and fix Nakama RPC session validation
This commit is contained in:
@@ -427,6 +427,9 @@ func link_google(id_token: String) -> bool:
|
||||
func logout() -> void:
|
||||
print("[AuthManager] Logging out...")
|
||||
|
||||
if NakamaManager.client and NakamaManager.session:
|
||||
await NakamaManager.client.session_logout_async(NakamaManager.session)
|
||||
|
||||
# FULL CLEANUP: close socket, leave bridge, clear multiplayer peer
|
||||
NakamaManager.cleanup()
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ func after_action_completed():
|
||||
return
|
||||
player._is_processing_action = true
|
||||
|
||||
if player.is_multiplayer_authority():
|
||||
if player.can_rpc() and multiplayer.has_multiplayer_peer():
|
||||
player.update_finish_availability()
|
||||
|
||||
# Clear the highlights after placing the tiles. (Quickfix for Clientside)
|
||||
@@ -30,12 +30,12 @@ func after_action_completed():
|
||||
|
||||
# Only update UI if this is the LOCAL HUMAN PLAYER
|
||||
# Guard against stale callbacks after peer teardown (host quitting solo match)
|
||||
if not multiplayer.has_multiplayer_peer():
|
||||
if not multiplayer or not multiplayer.has_multiplayer_peer():
|
||||
player._is_processing_action = false
|
||||
return
|
||||
if multiplayer.get_unique_id() == player.get_multiplayer_authority():
|
||||
# Sync playerboard (Bots DO need to sync their board logic, just not update local UI)
|
||||
if player.is_multiplayer_authority() and player.has_method("can_rpc") and player.can_rpc():
|
||||
if player.can_rpc() and multiplayer.has_multiplayer_peer():
|
||||
var main = player.get_tree().get_root().get_node_or_null("Main")
|
||||
if main:
|
||||
main.rpc("sync_playerboard", player.name.to_int(), player.playerboard)
|
||||
|
||||
@@ -99,10 +99,11 @@ func handle_unhandled_input(event):
|
||||
var gm = player.get_node_or_null("/root/Main/CandySurvivalManager")
|
||||
if gm and gm.active:
|
||||
var pid = player.get("peer_id") if "peer_id" in player else player.name.to_int()
|
||||
if multiplayer.is_server():
|
||||
gm.try_activate_ghost(pid)
|
||||
else:
|
||||
gm.rpc_id(1, "try_activate_ghost", pid)
|
||||
if player.can_rpc() and multiplayer.has_multiplayer_peer():
|
||||
if multiplayer.is_server():
|
||||
gm.try_activate_ghost(pid)
|
||||
else:
|
||||
gm.rpc_id(1, "try_activate_ghost", pid)
|
||||
else:
|
||||
player.activate_held_powerup()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
@@ -168,9 +168,10 @@ func simple_move_to(grid_position: Vector2i) -> bool:
|
||||
if gm and gm.active and gm.has_method("try_deliver"):
|
||||
var dist = abs(grid_position.x - 8) + abs(grid_position.y - 8)
|
||||
if dist <= 1:
|
||||
if multiplayer.is_server():
|
||||
var pid = player.get("peer_id") if "peer_id" in player else player.name.to_int()
|
||||
gm.try_deliver(pid)
|
||||
if player.can_rpc() and multiplayer.has_multiplayer_peer():
|
||||
if multiplayer.is_server():
|
||||
var pid = player.get("peer_id") if "peer_id" in player else player.name.to_int()
|
||||
gm.try_deliver(pid)
|
||||
|
||||
rotate_towards_target(grid_position)
|
||||
|
||||
@@ -249,10 +250,11 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool:
|
||||
if gm and gm.active and gm.has_method("try_knock"):
|
||||
var pid = player.get("peer_id") if "peer_id" in player else player.name.to_int()
|
||||
var other_pid = other_player.get("peer_id") if "peer_id" in other_player else other_player.name.to_int()
|
||||
if multiplayer.is_server():
|
||||
gm.try_knock(pid, other_pid)
|
||||
else:
|
||||
gm.rpc_id(1, "try_knock", pid, other_pid)
|
||||
if player.can_rpc() and multiplayer.has_multiplayer_peer():
|
||||
if multiplayer.is_server():
|
||||
gm.try_knock(pid, other_pid)
|
||||
else:
|
||||
gm.rpc_id(1, "try_knock", pid, other_pid)
|
||||
# Consume knock mode after use (toggle off)
|
||||
player.is_charged_strike = false
|
||||
return false
|
||||
@@ -315,7 +317,7 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool:
|
||||
print("[Move] Ghost mode bypassed push-into-sticky at %s" % pushed_to_pos)
|
||||
else:
|
||||
print("[Move] Player pushed into sticky cell at %s — slowed" % pushed_to_pos)
|
||||
if multiplayer.is_server() or other_player.is_multiplayer_authority():
|
||||
if other_player.can_rpc() and multiplayer.has_multiplayer_peer() and (multiplayer.is_server() or other_player.is_multiplayer_authority()):
|
||||
gm_sticky.apply_sticky_slow(other_player)
|
||||
|
||||
# 2. Apply freeze/stun effect
|
||||
@@ -342,10 +344,10 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool:
|
||||
if main_score:
|
||||
var gcm = main_score.get_node_or_null("GoalsCycleManager")
|
||||
if gcm:
|
||||
if multiplayer.is_server():
|
||||
if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
|
||||
# Server/Bot: Directly add score to specific player ID
|
||||
gcm.add_score(player.name.to_int(), 200)
|
||||
else:
|
||||
elif player.can_rpc() and multiplayer.has_multiplayer_peer():
|
||||
# Client: Request score add (sender ID used)
|
||||
gcm.rpc("request_add_score", 200)
|
||||
NotificationManager.send_message(player, "Successful Attack! +200 Pts", NotificationManager.MessageType.GOAL)
|
||||
|
||||
@@ -196,6 +196,7 @@ func host_game(room_meta: Dictionary = {}):
|
||||
if bridge.match_state == NakamaMultiplayerBridge.MatchState.CONNECTED:
|
||||
print("[NakamaManager] Already hosting a match; ignoring duplicate host request.")
|
||||
return
|
||||
|
||||
if bridge.match_state != NakamaMultiplayerBridge.MatchState.DISCONNECTED:
|
||||
# Stranded mid-join (e.g. previous attempt never resolved). Reset so the
|
||||
# bridge is idle before we try again.
|
||||
@@ -203,8 +204,9 @@ func host_game(room_meta: Dictionary = {}):
|
||||
await bridge.leave()
|
||||
|
||||
print("Hosting match via Nakama Bridge...")
|
||||
await bridge.create_match()
|
||||
# Store room metadata in Nakama storage so other players can see it in listings
|
||||
var result = await bridge.create_match()
|
||||
if result and result.is_exception():
|
||||
emit_signal("match_join_error", result.get_exception().message)
|
||||
if session and current_match_id and room_meta.size() > 0:
|
||||
var meta_json = JSON.stringify(room_meta)
|
||||
var write_obj = NakamaWriteStorageObject.new(
|
||||
|
||||
@@ -93,8 +93,14 @@ func api_rpc_async(rpc_id: String, payload: String = "{}") -> Dictionary:
|
||||
|
||||
var retries := 0
|
||||
while retries <= MAX_RETRIES:
|
||||
if not nakama_backend or not nakama_backend.client or not nakama_backend.session:
|
||||
return { "success": false, "error": ErrorCode.UNAUTHORIZED, "message": "Not authenticated" }
|
||||
|
||||
var result = await nakama_backend.client.rpc_async(nakama_backend.session, rpc_id, payload)
|
||||
|
||||
if result == null:
|
||||
return { "success": false, "error": ErrorCode.INTERNAL_ERROR, "message": "RPC call returned null" }
|
||||
|
||||
# NakamaAPI.ApiRpc has is_exception()
|
||||
if result.is_exception():
|
||||
var ex = result.get_exception()
|
||||
|
||||
+9
-4
@@ -311,7 +311,7 @@ func on_thrown_landing(attacker: Node = null, intensity: float = 1.0):
|
||||
|
||||
# Disable movement/interaction logic temporarily
|
||||
var controller = get_node_or_null("TektonController")
|
||||
if controller and controller.get("timer"):
|
||||
if controller and is_instance_valid(controller) and controller.get("timer") and is_instance_valid(controller.timer):
|
||||
controller.timer.stop()
|
||||
|
||||
# Visual Shrink
|
||||
@@ -329,7 +329,7 @@ func on_thrown_landing(attacker: Node = null, intensity: float = 1.0):
|
||||
t.tween_property(mesh, "scale", base_scale * 0.5, 0.2).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK)
|
||||
|
||||
# Spawn tiles (as requested "tekton will spawn a tiles around that floor also")
|
||||
if is_multiplayer_authority():
|
||||
if is_inside_tree() and is_multiplayer_authority():
|
||||
spawn_tiles_around(int(8 * intensity))
|
||||
|
||||
# Floor Freeze (Visual/Instant - Run on all clients locally)
|
||||
@@ -341,7 +341,12 @@ func on_thrown_landing(attacker: Node = null, intensity: float = 1.0):
|
||||
temporarily_change_floor(current_position, 1, 6, 3.0)
|
||||
|
||||
# Wait 3.0 seconds
|
||||
await get_tree().create_timer(3.0).timeout
|
||||
var tree = get_tree() if is_inside_tree() else null
|
||||
if tree:
|
||||
await tree.create_timer(3.0).timeout
|
||||
else:
|
||||
is_recovering = false
|
||||
return
|
||||
|
||||
# Grow back
|
||||
for i in range(mesh_cache.size()):
|
||||
@@ -355,7 +360,7 @@ func on_thrown_landing(attacker: Node = null, intensity: float = 1.0):
|
||||
|
||||
# Resume AI
|
||||
if not is_multiplayer_authority() or not is_carried:
|
||||
if controller and controller.has_method("_start_timer"):
|
||||
if controller and is_instance_valid(controller) and controller.has_method("_start_timer"):
|
||||
controller._start_timer()
|
||||
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ func _ready():
|
||||
_start_timer()
|
||||
|
||||
func _start_timer():
|
||||
if not timer or not is_instance_valid(timer): return
|
||||
var time = rng.randf_range(move_interval_min, move_interval_max)
|
||||
timer.start(time)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user