|
|
|
@@ -0,0 +1,169 @@
|
|
|
|
|
extends Node
|
|
|
|
|
|
|
|
|
|
const DEFAULT_PORT := 24565
|
|
|
|
|
const MAX_CLIENTS := 7
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
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)
|