extends Node ## Netzwerk: Räume liegen auf dem Spielserver, Beitritt läuft über einen Code. ## ## Der Server ist eine eigene headless Instanz und sitzt nicht mit am Tisch. ## Er ist Peer 1 und damit die Autorität. Ein Spieler im Raum ist Eigentümer ## und darf die Partie starten. signal lobby_changed signal game_started(config: Dictionary) signal connection_failed(reason: String) signal server_closed signal room_opened(code: String) const PORT := 8910 const MAX_HUMANS := 8 const STANDARD_SERVER := "https://pirate-dice.marha.de" ## Ohne Spieler schliesst sich ein Raum von selbst, sonst stehen Leichen herum const LEERLAUF := 120.0 var player_name := "Spieler" ## peer_id -> Name. Nur Menschen; der Server steht nicht drin. var lobby: Dictionary = {} var owner_id := 0 var is_owner := false var room_code := "" var in_game := false ## true in der headless Instanz auf dem Server var dedicated := false var _leer_seit := 0.0 func _ready() -> void: multiplayer.peer_connected.connect(_on_peer_connected) 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 einen lokalen Matchmaker zeigen lassen: ## PIRATE_DICE_SERVER=http://127.0.0.1:8020 godot func server_url() -> String: var eigen := OS.get_environment("PIRATE_DICE_SERVER") return eigen if eigen != "" else STANDARD_SERVER # ---------------------------------------------------------------- Räume ## Neuen Raum anlegen lassen und gleich betreten. func create_room(pname: String) -> void: var raum := await _api(HTTPClient.METHOD_POST, "/api/raum") if raum.is_empty(): connection_failed.emit("Spielserver nicht erreichbar.") return room_code = String(raum.get("code", "")) room_opened.emit(room_code) join_game(String(raum.get("host", "")), pname, int(raum.get("port", PORT))) func join_room(code: String, pname: String) -> void: code = code.strip_edges().to_upper() if code == "": connection_failed.emit("Kein Code eingegeben.") return var raum := await _api(HTTPClient.METHOD_GET, "/api/raum/" + code) if raum.is_empty(): connection_failed.emit("Den Code %s gibt es nicht." % code) return room_code = code join_game(String(raum.get("host", "")), pname, int(raum.get("port", PORT))) func _api(method: int, pfad: String) -> Dictionary: var req := HTTPRequest.new() add_child(req) req.timeout = 8.0 if req.request(server_url() + pfad, PackedStringArray(), method) != OK: req.queue_free() return {} var antwort: Array = await req.request_completed req.queue_free() if int(antwort[1]) != 200: return {} var daten: Variant = JSON.parse_string( (antwort[3] as PackedByteArray).get_string_from_utf8()) return daten if daten is Dictionary else {} # ---------------------------------------------------------------- Verbindung ## Nur die headless Instanz auf dem Server ruft das auf. func serve(port: int) -> bool: var peer := ENetMultiplayerPeer.new() if peer.create_server(port, MAX_HUMANS) != OK: push_error("Port %d ist belegt." % port) return false multiplayer.multiplayer_peer = peer dedicated = true _leer_seit = _jetzt() print("Raum offen auf Port %d." % port) return true func join_game(host: String, pname: String, port: int = PORT) -> bool: player_name = pname var peer := ENetMultiplayerPeer.new() if peer.create_client(host, port) != OK: connection_failed.emit("Verbindung zu %s:%d fehlgeschlagen." % [host, port]) return false multiplayer.multiplayer_peer = peer 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 is ENetMultiplayerPeer \ and peer.get_connection_status() != MultiplayerPeer.CONNECTION_DISCONNECTED ## Ein leerer Raum kostet sonst dauerhaft Speicher auf dem Server. func _process(_delta: float) -> void: if not dedicated: return if lobby.is_empty(): if _jetzt() - _leer_seit > LEERLAUF: print("Niemand mehr im Raum, wird geschlossen.") get_tree().quit() else: _leer_seit = _jetzt() func _jetzt() -> float: return Time.get_ticks_msec() / 1000.0 func _on_peer_connected(id: int) -> void: if multiplayer.is_server(): _push_lobby.rpc_id(id, lobby, owner_id) func _on_peer_disconnected(id: int) -> void: if not multiplayer.is_server(): return lobby.erase(id) # Geht der Eigentümer, rückt der nächste nach - sonst startet niemand mehr if id == owner_id: owner_id = 0 if lobby.is_empty() else int(lobby.keys()[0]) _push_lobby.rpc(lobby, owner_id) lobby_changed.emit() func _on_connected() -> void: _register.rpc_id(1, player_name) func _on_connect_fail() -> void: multiplayer.multiplayer_peer = null connection_failed.emit("Raum 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() @rpc("any_peer", "reliable") func _register(pname: String) -> void: if not multiplayer.is_server(): return var id := multiplayer.get_remote_sender_id() if in_game: _abweisen(id, "In dem Raum läuft schon eine Partie.") return if lobby.size() >= MAX_HUMANS: _abweisen(id, "Der Raum ist voll.") return lobby[id] = pname.substr(0, 16) if owner_id == 0: owner_id = id _push_lobby.rpc(lobby, owner_id) lobby_changed.emit() ## Erst die Begründung schicken, dann trennen - andersherum kommt sie nie an. func _abweisen(id: int, grund: String) -> void: _abgewiesen.rpc_id(id, grund) await get_tree().create_timer(0.5).timeout var peer := multiplayer.multiplayer_peer if peer is ENetMultiplayerPeer: peer.disconnect_peer(id) @rpc("authority", "reliable") func _abgewiesen(grund: String) -> void: connection_failed.emit(grund) leave() @rpc("authority", "reliable") func _push_lobby(data: Dictionary, besitzer: int) -> void: 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() lobby = {1: pname} owner_id = 1 is_owner = true lobby_changed.emit() _begin({"peers": [1], "names": lobby.duplicate(), "ai": ai_count}) ## Der Eigentümer 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 if multiplayer.get_remote_sender_id() != owner_id: return start_game(ai_count) func start_game(ai_count: int) -> void: if not multiplayer.is_server() or in_game: return var order: Array = lobby.keys() order.sort() if order.size() + ai_count < 2: return _begin.rpc({"peers": order, "names": lobby.duplicate(), "ai": ai_count}) @rpc("authority", "call_local", "reliable") func _begin(cfg: Dictionary) -> void: in_game = true game_started.emit(cfg)