extends Node ## Transport and RPC surface. This is the only autoload in the project. ## ## It has to be an autoload because Godot routes an RPC by node path: the sender ## and receiver must agree on where the node lives, and `/root/Net` is the one ## path that exists identically on a dedicated server and on every client. ## ## The naming rule here is load-bearing: `c_*` is client -> server, `s_*` is ## server -> client. The `s_*` methods are annotated "authority", so the engine ## itself drops any attempt by a client to fake one. signal state_changed(new_state: State) enum State { OFFLINE, CONNECTING, ONLINE, FAILED } ## Peer id the engine gives the host. On a listen server this is also a real ## player, which is why every send goes through the helpers below. const LOCAL_PEER := 1 var state: State = State.OFFLINE var server: ServerRuntime = null var client: ClientRuntime = null var last_error: String = "" func _set_state(s: State) -> void: state = s state_changed.emit(s) # --- Lifecycle -------------------------------------------------------------- func host(port: int) -> Error: shutdown() var peer := ENetMultiplayerPeer.new() var err := peer.create_server(port, Protocol.MAX_CLIENTS, Protocol.CHANNEL_COUNT) if err != OK: last_error = "could not bind port %d (error %d)" % [port, err] GameLog.error("net", last_error) _set_state(State.FAILED) return err multiplayer.multiplayer_peer = peer multiplayer.peer_connected.connect(_on_peer_connected) multiplayer.peer_disconnected.connect(_on_peer_disconnected) server = ServerRuntime.new() server.name = "Server" add_child(server) _set_state(State.ONLINE) GameLog.info("net", "listening on %d" % port) return OK func join(address: String, port: int) -> Error: shutdown() var peer := ENetMultiplayerPeer.new() var err := peer.create_client(address, port, Protocol.CHANNEL_COUNT) if err != OK: last_error = "could not reach %s:%d (error %d)" % [address, port, err] GameLog.error("net", last_error) _set_state(State.FAILED) return err multiplayer.multiplayer_peer = peer multiplayer.connected_to_server.connect(_on_connected) multiplayer.connection_failed.connect(_on_connect_failed) multiplayer.server_disconnected.connect(_on_server_disconnected) client = ClientRuntime.new() client.name = "Client" add_child(client) # Nothing is simulated until the server says hello back; on_welcome turns # the physics process on. client.set_physics_process(false) _set_state(State.CONNECTING) GameLog.info("net", "connecting to %s:%d" % [address, port]) return OK func shutdown() -> void: if multiplayer.multiplayer_peer != null \ and multiplayer.multiplayer_peer is not OfflineMultiplayerPeer: multiplayer.multiplayer_peer.close() multiplayer.multiplayer_peer = null for sig in [multiplayer.peer_connected, multiplayer.peer_disconnected, multiplayer.connected_to_server, multiplayer.connection_failed, multiplayer.server_disconnected]: for c in sig.get_connections(): sig.disconnect(c["callable"]) if server != null: server.queue_free() server = null if client != null: client.queue_free() client = null _set_state(State.OFFLINE) func kick(peer_id: int) -> void: if multiplayer.multiplayer_peer is ENetMultiplayerPeer: (multiplayer.multiplayer_peer as ENetMultiplayerPeer).disconnect_peer(peer_id) func is_server() -> bool: return server != null # --- Transport signals ------------------------------------------------------ func _on_peer_connected(peer_id: int) -> void: if server != null: server.on_peer_connected(peer_id) func _on_peer_disconnected(peer_id: int) -> void: if server != null: server.on_peer_disconnected(peer_id) func _on_connected() -> void: _set_state(State.ONLINE) c_hello.rpc_id(1, Protocol.VERSION, GameOpts.player_name) func _on_connect_failed() -> void: last_error = "connection refused" GameLog.error("net", last_error) _set_state(State.FAILED) func _on_server_disconnected() -> void: last_error = "server closed the connection" GameLog.warn("net", last_error) shutdown() ## Start a client inside the hosting process. The result is a listen server: ## the host plays through exactly the same code path as a remote player, sending ## input and learning outcomes from snapshots, with the transport short-circuited ## to a function call. No simulation code knows the difference. func start_local_client() -> void: if server == null: GameLog.error("net", "start_local_client called with no server") return client = ClientRuntime.new() client.name = "Client" add_child(client) server.on_hello(LOCAL_PEER, Protocol.VERSION, GameOpts.player_name) func _is_local(peer_id: int) -> bool: return peer_id == LOCAL_PEER and client != null # --- Send helpers ----------------------------------------------------------- # Every server -> client message goes through one of these rather than calling # rpc_id directly, so the listen server's own player is reachable too. func send_welcome(peer_id: int) -> void: if _is_local(peer_id): client.on_welcome(peer_id) else: s_welcome.rpc_id(peer_id, peer_id, Protocol.VERSION) func send_enter_instance(peer_id: int, id: int, kind: int, server_tick: int, boss_id: String, spawn: Vector2) -> void: if _is_local(peer_id): client.on_enter_instance(id, kind, server_tick, boss_id, spawn) else: s_enter_instance.rpc_id(peer_id, id, kind, server_tick, boss_id, spawn) func send_snapshot(peer_id: int, data: PackedByteArray) -> void: if _is_local(peer_id): client.on_snapshot(data) else: s_snapshot.rpc_id(peer_id, data) func send_events(peer_id: int, data: PackedByteArray) -> void: if _is_local(peer_id): client.on_events(data) else: s_events.rpc_id(peer_id, data) func send_reject(peer_id: int, reason: String) -> void: if _is_local(peer_id): GameLog.error("net", "local client rejected: %s" % reason) else: s_reject.rpc_id(peer_id, reason) func send_input(data: PackedByteArray) -> void: if server != null: server.on_input(LOCAL_PEER, data) # listen server: no transport at all elif state == State.ONLINE and client != null: c_input.rpc_id(1, data) # --- Client -> server ------------------------------------------------------- @rpc("any_peer", "call_remote", "reliable", 1) func c_hello(version: int, display_name: String) -> void: if server == null: return server.on_hello(multiplayer.get_remote_sender_id(), version, display_name) @rpc("any_peer", "call_remote", "unreliable_ordered", 4) func c_input(data: PackedByteArray) -> void: if server == null: return server.on_input(multiplayer.get_remote_sender_id(), data) # --- Server -> client ------------------------------------------------------- @rpc("authority", "call_remote", "reliable", 1) func s_welcome(peer_id: int, _version: int) -> void: if client == null: return client.on_welcome(peer_id) client.set_physics_process(true) @rpc("authority", "call_remote", "reliable", 1) func s_enter_instance(id: int, kind: int, server_tick: int, boss_id: String, spawn: Vector2) -> void: if client == null: return client.on_enter_instance(id, kind, server_tick, boss_id, spawn) @rpc("authority", "call_remote", "reliable", 1) func s_reject(reason: String) -> void: last_error = reason GameLog.error("net", "rejected by server: %s" % reason) _set_state(State.FAILED) @rpc("authority", "call_remote", "unreliable", 2) func s_snapshot(data: PackedByteArray) -> void: if client != null: client.on_snapshot(data) @rpc("authority", "call_remote", "reliable", 3) func s_events(data: PackedByteArray) -> void: if client != null: client.on_events(data)