Files
tekton/scripts/nakama_manager.gd
T

225 lines
6.6 KiB
GDScript

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):
nakama_host = host
# Auto-detect secure tunnels (Tailscale, ngrok, playit, cloudflare)
if host.ends_with(".ts.net") or host.ends_with(".gg") or host.begins_with("https://"):
if host.begins_with("https://"):
nakama_host = host.replace("https://", "")
if host.begins_with("http://"):
nakama_host = host.replace("http://", "")
if nakama_host.ends_with("/"):
nakama_host = nakama_host.substr(0, nakama_host.length() - 1)
nakama_port = 443
nakama_scheme = "https"
else:
# Extract port if they typed something like 192.168.1.1:7350
if ":" in host and not host.begins_with("http"):
var parts = host.split(":")
nakama_host = parts[0]
nakama_port = parts[1].to_int()
else:
nakama_port = port
nakama_scheme = "http"
_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()