extends Node const DEFAULT_PORT := 24565 const MAX_CLIENTS := 7 const MULTIPLAYER_SCENE := "res://Scenes/multiplayer.tscn" signal player_joined(peer_id: int) signal player_left(peer_id: int) signal session_started(is_server: bool) signal session_ended() signal connection_failed() # World hooks, registered by main.gd once the scene tree exists. var _world: Node = null var _players_spawner: MultiplayerSpawner = null var _items_spawner: MultiplayerSpawner = null var _content_root: Node = null func _ready() -> void: multiplayer.peer_connected.connect(_on_peer_connected) multiplayer.peer_disconnected.connect(_on_peer_disconnected) multiplayer.connected_to_server.connect(_on_connected_to_server) multiplayer.connection_failed.connect(_on_connection_failed) multiplayer.server_disconnected.connect(_on_server_disconnected) _handle_cmdline() # --- Public API ------------------------------------------------------------ ## Start hosting. The host is peer 1 and also plays (listen server). func host(port: int = DEFAULT_PORT) -> Error: var peer := ENetMultiplayerPeer.new() var err := peer.create_server(port, MAX_CLIENTS) if err != OK: print("NetworkManager HOST failed to create_server on port %d: %s" % [port, error_string(err)]) return err multiplayer.multiplayer_peer = peer print("NetworkManager HOST started on port %d (peer id %d)" % [port, multiplayer.get_unique_id()]) session_started.emit(true) get_tree().change_scene_to_file.call_deferred(MULTIPLAYER_SCENE) return OK ## Join an existing host. func join(address: String = "127.0.0.1", port: int = DEFAULT_PORT) -> Error: var peer := ENetMultiplayerPeer.new() var err := peer.create_client(address, port) if err != OK: print("NetworkManager JOIN failed to create_client %s:%d: %s" % [address, port, error_string(err)]) return err multiplayer.multiplayer_peer = peer print("NetworkManager JOIN connecting to %s:%d ..." % [address, port]) return OK ## Leave the session and tear down transport. func leave() -> void: _go_offline() print("NetworkManager Session ended") session_ended.emit() # Restore Godot's default OfflineMultiplayerPeer (rather than leaving the peer # null), so is_multiplayer_authority()/get_unique_id() keep working while we are # back in single-player / menu state. func _go_offline() -> void: if multiplayer.multiplayer_peer: multiplayer.multiplayer_peer.close() multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new() unregister_world() func is_server() -> bool: return is_online() and multiplayer.is_server() ## True only when a real ENet session is active. Godot installs a default ## OfflineMultiplayerPeer, so a non-null peer alone does not mean "online". func is_online() -> bool: var p := multiplayer.multiplayer_peer return p != null and not (p is OfflineMultiplayerPeer) ## True on the machine that owns authoritative world logic: the server when ## online, or the single player when offline. Station logic and spawning should ## only run where this is true, so state has one source of truth. func owns_world() -> bool: return not is_online() or is_server() # --- Station work-progress seam ------------------------------------------- ## Reusable entry point for a client to contribute work to a station (e.g. a ## future chopping/gesture station). The client detects the gesture locally and ## calls this; the server validates and accumulates. Timer-driven stations like ## the Hob don't need it, but it is the drop-in seam for input-driven ones. @rpc("any_peer", "reliable") func submit_work(station_path: NodePath, amount: float) -> void: if not is_server(): return var station := get_node_or_null(station_path) if station and station.has_method("add_work"): print("NetworkManager submit_work: peer %d contributed %.2f to %s" % [multiplayer.get_remote_sender_id(), amount, station.name]) station.add_work(multiplayer.get_remote_sender_id(), amount) ## Called by the world scene once it's ready, passing its spawners. Must run ## before world_ready()/host()/join() on every peer so the custom spawn ## function is installed before any spawn packet can arrive. func register_world(world: Node, players_spawner: MultiplayerSpawner, items_spawner: MultiplayerSpawner) -> void: _world = world _players_spawner = players_spawner _items_spawner = items_spawner _content_root = items_spawner.get_node(items_spawner.spawn_path) if items_spawner else world print("NetworkManager World registered (players_spawner=%s items_spawner=%s)" % [str(players_spawner != null), str(items_spawner != null)]) ## Called when the world scene goes away (disconnect, leaving the session) so ## the autoload doesn't hold stale/freed references across a scene reload. func unregister_world() -> void: _world = null _content_root = null _players_spawner = null _items_spawner = null # --- Session signal handlers ---------------------------------------------- func _on_peer_connected(peer_id: int) -> void: print("NetworkManager peer_connected: %d" % peer_id) # Only the server reacts by materialising that peer's player. if is_server(): print("NetworkManager player_present: %d" % peer_id) player_joined.emit(peer_id) func _on_peer_disconnected(peer_id: int) -> void: print("NetworkManager peer_disconnected: %d" % peer_id) if is_server(): print("NetworkManager player_absent: %d" % peer_id) player_left.emit(peer_id) func _on_connected_to_server() -> void: print("NetworkManager connected_to_server (my id=%d)" % multiplayer.get_unique_id()) session_started.emit(false) get_tree().change_scene_to_file.call_deferred(MULTIPLAYER_SCENE) func _on_connection_failed() -> void: print("NetworkManager connection_failed") _go_offline() connection_failed.emit() func _on_server_disconnected() -> void: print("NetworkManager server_disconnected") _go_offline() session_ended.emit() # --- Command-line driven test bootstrap ----------------------------------- func _handle_cmdline() -> void: print("NetworkManager _handle_cmdline") var args := OS.get_cmdline_user_args() if args.has("--server"): print("NetworkManager cmdline: --server") host() elif args.has("--join"): var idx := args.find("--join") var addr := "127.0.0.1" if idx + 1 < args.size(): addr = args[idx + 1] print("NetworkManager cmdline: --join %s" % addr) join(addr)