extends Node # Standard Nakama Configuration var nakama_server_key = "defaultkey" var nakama_host = "localhost" var nakama_port = 7350 var nakama_scheme = "http" # Core Nakama Variables var client: NakamaClient var session: NakamaSession var socket: NakamaSocket var bridge: NakamaMultiplayerBridge # Signals signal connected_to_nakama signal connection_failed(error_message) signal match_joined(match_id) signal match_join_error(error_message) # Current Match Info var current_match_id: String = "" func _ready(): # Initialize the Nakama Client _init_client() # Ensure we process network events set_process(true) func _init_client(): client = Nakama.create_client(nakama_server_key, nakama_host, nakama_port, nakama_scheme) func set_server(host: String, port: int = 7350): # Clean up the host string var clean_host = host.strip_edges() # Extract protocol if present (override everything else) var forced_scheme = "" if clean_host.begins_with("https://"): forced_scheme = "https" clean_host = clean_host.replace("https://", "") elif clean_host.begins_with("http://"): forced_scheme = "http" clean_host = clean_host.replace("http://", "") # Handle trailing slashes if clean_host.ends_with("/"): clean_host = clean_host.substr(0, clean_host.length() - 1) # Extract port if explicitly provided in host string (e.g. host:port) var explicit_port = -1 if ":" in clean_host: var parts = clean_host.split(":") clean_host = parts[0] explicit_port = parts[1].to_int() # DETECT SETTINGS nakama_host = clean_host if forced_scheme != "": nakama_scheme = forced_scheme nakama_port = explicit_port if explicit_port != -1 else (443 if forced_scheme == "https" else port) elif clean_host.ends_with(".ts.net") and explicit_port == -1: # Tailscale Funnel Case (No port provided, .ts.net domain) nakama_scheme = "https" nakama_port = 443 elif clean_host.begins_with("100."): # Standard Tailscale IP Case nakama_scheme = "http" nakama_port = explicit_port if explicit_port != -1 else port else: # Generic Case (e.g. localhost, public IP) nakama_scheme = "http" nakama_port = explicit_port if explicit_port != -1 else port _init_client() print("[NakamaManager] Server updated to: ", nakama_scheme, "://", nakama_host, ":", nakama_port) func _process(_delta): # If using the standard socket adapter, it needs polling in some versions if socket: pass func connect_to_nakama_async(email: String = "", password: String = "") -> bool: if is_connected_to_nakama(): print("Already connected to Nakama.") emit_signal("connected_to_nakama") return true # 1. Authenticate if email == "": var device_id = OS.get_unique_id() # If running in editor or debug, append a unique suffix to allow multiple # instances on one machine to have separate sessions. if OS.is_debug_build(): device_id += "_" + str(Time.get_ticks_msec()) + "_" + str(randi() % 1000) session = await client.authenticate_device_async(device_id) else: session = await client.authenticate_email_async(email, password) if session.is_exception(): var err = session.get_exception() printerr("[NakamaManager] Auth Error: %s (Code: %s)" % [err.message, err.status_code]) emit_signal("connection_failed", err.message) return false # 2. Connect Socket socket = Nakama.create_socket_from(client) var socket_result = await socket.connect_async(session) if socket_result.is_exception(): var err = socket_result.get_exception() printerr("[NakamaManager] Socket Error: %s (Code: %s)" % [err.message, err.status_code]) emit_signal("connection_failed", err.message) return false # 3. Initialize Multiplayer Bridge # This links Nakama's socket to Godot's High-Level Multiplayer API if bridge: bridge.leave() bridge = null bridge = NakamaMultiplayerBridge.new(socket) # Connect bridge signals bridge.match_joined.connect(_on_bridge_match_joined) bridge.match_join_error.connect(_on_bridge_match_join_error) # CRITICAL: Set Godot's multiplayer peer to the Nakama bridge # This allows @rpc functions to work over Nakama multiplayer.set_multiplayer_peer(bridge.multiplayer_peer) print("Connected to Nakama and Bridge Initialized.") emit_signal("connected_to_nakama") return true func cleanup(): """Properly shutdown the Nakama connection and reset the multiplayer peer.""" print("[NakamaManager] Full cleanup starting...") if bridge: bridge.leave() bridge = null if socket: socket.close() socket = null current_match_id = "" # Reset Godot's multiplayer peer if multiplayer.get_multiplayer_peer(): multiplayer.set_multiplayer_peer(null) print("[NakamaManager] Cleanup complete.") # --- Match Management --- func host_game(): if not bridge: printerr("Cannot host: Bridge not initialized") return print("Hosting match via Nakama Bridge...") var result = await bridge.create_match() if result and result.is_exception(): emit_signal("match_join_error", result.get_exception().message) func join_game(match_id: String): if not bridge: printerr("Cannot join: Bridge not initialized") return # If already connected, leave current match first if bridge.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED: print("Already connected to a match. Leaving current match before joining...") bridge.leave() # Wait a bit for cleanup await get_tree().create_timer(0.2).timeout print("Joining match: ", match_id) var result = await bridge.join_match(match_id) if result and result.is_exception(): emit_signal("match_join_error", result.get_exception().message) # --- Callbacks --- func _on_bridge_match_joined() -> void: current_match_id = bridge.match_id print("Successfully joined match: ", current_match_id) emit_signal("match_joined", current_match_id) func _on_bridge_match_join_error(error) -> void: printerr("Bridge failed to join match: ", error.message) emit_signal("match_join_error", error.message) func is_connected_to_nakama() -> bool: return socket != null and socket.is_connected_to_host() # --- Match Listing --- func list_matches_async() -> Array: """Query available matches from Nakama server.""" if not client: push_error("Cannot list matches: Client not initialized") return [] if not session or session.is_expired(): push_error("Cannot list matches: No valid session") return [] print("Querying matches from Nakama server...") # Query matches - min 0, max 8 players, limit 20, authoritative=false for relayed matches var result = await client.list_matches_async(session, 0, 8, 20, false, "", "") if result.is_exception(): printerr("Failed to list matches: ", result.get_exception().message) return [] var rooms: Array = [] if result.matches: print("Found %d matches" % result.matches.size()) for match_data in result.matches: print(" Match: ", match_data.match_id, " - Size: ", match_data.size) # Use first 8 chars of match ID as room identifier since Nakama doesn't store custom names var short_id = match_data.match_id.substr(0, 8) if match_data.match_id.length() > 8 else match_data.match_id rooms.append({ "match_id": match_data.match_id, "room_name": short_id, "host_name": "Host", "player_count": match_data.size if match_data.size else 1, "max_players": 4 }) else: print("No matches found") return rooms func _exit_tree(): cleanup()