extends Node ## Netzwerk: ein Server, viele Räume, Beitritt per Code. ## ## Der Server hält alle Räume in einem einzigen Prozess und lauscht auf einem ## einzigen WebSocket-Port. Damit braucht es weder einen Portbereich noch einen ## Prozess je Partie. Ein Raum ist ein Knoten unter /root/Main/Raeume; sein ## SubViewport bringt einen eigenen Physikraum mit, sonst würden die Würfel ## aller Partien im selben Raum liegen. ## ## Die Knotenpfade müssen auf Server und Client gleich heißen - daran hängt ## das Ziel jedes RPC. signal lobby_changed signal game_started(code: String, cfg: Dictionary) signal room_closed(code: String) signal connection_failed(reason: String) signal server_closed const MAX_HUMANS := 8 const STANDARD_SERVER := "wss://dice.marha.de" ## Ohne I, O, 0 und 1: die verwechselt man beim Vorlesen const ALPHABET := "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" const CODE_LAENGE := 4 const SOLO := "SOLO" # ── Client ─────────────────────────────────────────────────────────────────── var player_name := "Spieler" var lobby: Dictionary = {} var owner_id := 0 var is_owner := false var room_code := "" var in_game := false # ── Server ─────────────────────────────────────────────────────────────────── var dedicated := false ## code -> {lobby: {peer: name}, owner: int, in_game: bool} var raeume: Dictionary = {} ## peer -> code var peer_raum: Dictionary = {} var _wunsch: Dictionary = {} var _rng := RandomNumberGenerator.new() func _ready() -> void: _rng.randomize() multiplayer.peer_disconnected.connect(_on_peer_disconnected) multiplayer.connected_to_server.connect(_on_connected) multiplayer.connection_failed.connect(_on_connect_fail) multiplayer.server_disconnected.connect(_on_server_disconnected) ## Zum Entwickeln auf den lokalen Server zeigen lassen: ## PIRATE_DICE_SERVER=ws://127.0.0.1:8000 godot func server_url() -> String: var eigen := OS.get_environment("PIRATE_DICE_SERVER") return eigen if eigen != "" else STANDARD_SERVER # ── Verbinden ──────────────────────────────────────────────────────────────── func create_room(pname: String) -> void: _verbinden({"art": "erstellen"}, pname) func join_room(code: String, pname: String) -> void: code = code.strip_edges().to_upper() if code == "": connection_failed.emit("Kein Code eingegeben.") return _verbinden({"art": "beitreten", "code": code}, pname) ## Der Wunsch wartet, bis die Verbindung steht - vorher kann er nicht raus. func _verbinden(wunsch: Dictionary, pname: String) -> void: player_name = pname _wunsch = wunsch var peer := WebSocketMultiplayerPeer.new() if peer.create_client(server_url()) != OK: connection_failed.emit("Server nicht erreichbar.") return multiplayer.multiplayer_peer = peer ## Nur die headless Instanz auf dem Server ruft das auf. func serve(port: int) -> bool: var peer := WebSocketMultiplayerPeer.new() if peer.create_server(port) != OK: push_error("Port %d ist belegt." % port) return false multiplayer.multiplayer_peer = peer dedicated = true print("Server lauscht auf Port %d." % port) return true func leave() -> void: if multiplayer.multiplayer_peer != null: multiplayer.multiplayer_peer.close() multiplayer.multiplayer_peer = null lobby.clear() owner_id = 0 is_owner = false room_code = "" in_game = false lobby_changed.emit() func is_online() -> bool: var peer := multiplayer.multiplayer_peer return peer != null \ and peer.get_connection_status() != MultiplayerPeer.CONNECTION_DISCONNECTED func _on_connected() -> void: if _wunsch.get("art", "") == "erstellen": _req_create.rpc_id(1, player_name) else: _req_join.rpc_id(1, String(_wunsch.get("code", "")), player_name) _wunsch = {} func _on_connect_fail() -> void: multiplayer.multiplayer_peer = null connection_failed.emit("Server nicht erreichbar.") func _on_server_disconnected() -> void: multiplayer.multiplayer_peer = null lobby.clear() owner_id = 0 is_owner = false in_game = false server_closed.emit() # ── Räume (nur Server) ─────────────────────────────────────────────────────── func _neuer_code() -> String: while true: var code := "" for i in CODE_LAENGE: code += ALPHABET[_rng.randi_range(0, ALPHABET.length() - 1)] if not raeume.has(code): return code return "" @rpc("any_peer", "reliable") func _req_create(pname: String) -> void: if not multiplayer.is_server(): return var id := multiplayer.get_remote_sender_id() var code := _neuer_code() raeume[code] = {"lobby": {id: pname.substr(0, 16)}, "owner": id, "in_game": false, "bestaetigt": {}} peer_raum[id] = code print("Raum %s aufgemacht (%d offen)." % [code, raeume.size()]) _lobby_senden(code) @rpc("any_peer", "reliable") func _req_join(code: String, pname: String) -> void: if not multiplayer.is_server(): return var id := multiplayer.get_remote_sender_id() code = code.strip_edges().to_upper() var raum: Dictionary = raeume.get(code, {}) if raum.is_empty(): _abgewiesen.rpc_id(id, "Den Code %s gibt es nicht." % code) return if raum.in_game: _abgewiesen.rpc_id(id, "In dem Raum läuft schon eine Partie.") return if raum.lobby.size() >= MAX_HUMANS: _abgewiesen.rpc_id(id, "Der Raum ist voll.") return raum.lobby[id] = pname.substr(0, 16) peer_raum[id] = code _lobby_senden(code) ## Ein Broadcast würde alle Räume treffen, also gezielt an die eigenen Peers. func _lobby_senden(code: String) -> void: var raum: Dictionary = raeume.get(code, {}) if raum.is_empty(): return for peer: int in raum.lobby: _push_lobby.rpc_id(peer, code, raum.lobby, raum.owner) func _on_peer_disconnected(id: int) -> void: if not multiplayer.is_server(): return var code: String = peer_raum.get(id, "") peer_raum.erase(id) var raum: Dictionary = raeume.get(code, {}) if raum.is_empty(): return raum.lobby.erase(id) # Der letzte macht das Licht aus: leerer Raum, weg damit. if raum.lobby.is_empty(): raeume.erase(code) room_closed.emit(code) print("Raum %s zu (%d offen)." % [code, raeume.size()]) return # Geht der Host, rückt der nächste nach - sonst startet niemand mehr if id == raum.owner: raum.owner = int(raum.lobby.keys()[0]) _lobby_senden(code) @rpc("authority", "reliable") func _abgewiesen(grund: String) -> void: connection_failed.emit(grund) leave() @rpc("authority", "reliable") func _push_lobby(code: String, data: Dictionary, besitzer: int) -> void: room_code = code lobby = data owner_id = besitzer is_owner = besitzer == multiplayer.get_unique_id() lobby_changed.emit() # ── Start ──────────────────────────────────────────────────────────────────── ## Einzelspieler: kein Netzwerk, nur der lokale Spieler und Bots. func start_singleplayer(pname: String, ai_count: int) -> void: player_name = pname multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new() room_code = SOLO lobby = {1: pname} owner_id = 1 is_owner = true in_game = true lobby_changed.emit() game_started.emit(SOLO, {"peers": [1], "names": lobby.duplicate(), "ai": ai_count}) ## Der Host bittet den Server um den Start. func request_start(ai_count: int) -> void: _req_start.rpc_id(1, ai_count) @rpc("any_peer", "reliable") func _req_start(ai_count: int) -> void: if not multiplayer.is_server(): return var id := multiplayer.get_remote_sender_id() var code: String = peer_raum.get(id, "") var raum: Dictionary = raeume.get(code, {}) if raum.is_empty() or raum.in_game or id != raum.owner: return var order: Array = raum.lobby.keys() order.sort() if order.size() + ai_count < 2: return raum.in_game = true var cfg := {"peers": order, "names": raum.lobby.duplicate(), "ai": ai_count} _start_zustellen(code, cfg) game_started.emit(code, cfg) ## Ein verlorenes Startpaket lässt den Spieler in der Lobby sitzen, während ## die anderen spielen. Deshalb wird nachgelegt, bis jeder quittiert hat. func _start_zustellen(code: String, cfg: Dictionary) -> void: for versuch in 4: var raum: Dictionary = raeume.get(code, {}) if raum.is_empty(): return var offen: Array = [] for peer: int in raum.lobby: if not raum.bestaetigt.has(peer): offen.append(peer) if offen.is_empty(): return if versuch > 0: print("Start an %s noch einmal (Versuch %d)." % [str(offen), versuch + 1]) for peer: int in offen: _begin.rpc_id(peer, code, cfg) await get_tree().create_timer(1.5).timeout @rpc("any_peer", "reliable") func _start_erhalten() -> void: if not multiplayer.is_server(): return var id := multiplayer.get_remote_sender_id() var raum: Dictionary = raeume.get(peer_raum.get(id, ""), {}) if not raum.is_empty(): raum.bestaetigt[id] = true @rpc("authority", "reliable") func _begin(code: String, cfg: Dictionary) -> void: _start_erhalten.rpc_id(1) if in_game and room_code == code: return # Nachzügler-Paket, die Partie läuft hier schon in_game = true room_code = code game_started.emit(code, cfg)