From f97d741c919955e404c86c8712d9b7773e00a4c3 Mon Sep 17 00:00:00 2001 From: team3 Date: Mon, 7 Sep 2026 22:43:02 +0200 Subject: [PATCH] Spielstart zuverlaessig machen, Client wieder scharf rendern Der Client steckte die Partie in einen SubViewport. Der rendert mit der Basisaufloesung 1280x720 und wurde danach hochskaliert - das Bild wurde matschig und die Kantenglaettung fiel weg. Den eigenen Physikraum braucht aber nur der Server, weil dort viele Partien nebeneinander laufen. Also bekommt nur er SubViewports; der Client haengt die Partie direkt in die Szene. Die Knotennamen bleiben gleich, daran haengt das RPC-Ziel. Der Healthcheck oeffnete alle 30 Sekunden eine nackte TCP-Verbindung auf den WebSocket-Port. Der Server lehnte sie ab ("Missing or invalid header 'upgrade'"), und dabei ging reproduzierbar ein Paket an einen echten Spieler verloren. Traefik braucht den Check nicht, also raus damit. Blieb ein seltener Verlust des Startpakets: der Spieler sass in der Lobby, waehrend die anderen spielten. Der Server legt jetzt nach, bis jeder den Start quittiert hat. Ein Verbindungsabbruch im Menue lud ausserdem die Szene neu und warf die Meldung dazu gleich wieder weg. Co-Authored-By: Claude Opus 5 (1M context) --- docker-compose.yml | 13 ++++--------- scripts/main.gd | 31 ++++++++++++++++++++++++------- scripts/net.gd | 39 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 64 insertions(+), 19 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 22bbec6..1dcffd8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,15 +4,10 @@ services: context: . container_name: pirate-dice restart: unless-stopped - healthcheck: - # Der Server spricht WebSocket, kein HTTP - also nur schauen, ob der - # Port jemanden zu Hause hat. - test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/8000"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 20s - start_interval: 2s + # Kein Healthcheck: der Server spricht ausschliesslich WebSocket. Eine + # nackte TCP-Verbindung darauf lehnt er ab ("Missing or invalid header + # 'upgrade'") - und dabei ging reproduzierbar ein Paket an einen echten + # Spieler verloren. Traefik braucht den Check nicht. networks: - web # Keine Portfreigabe nötig: alles läuft als WebSocket über Traefik auf 443. diff --git a/scripts/main.gd b/scripts/main.gd index b42e592..ded9d7f 100644 --- a/scripts/main.gd +++ b/scripts/main.gd @@ -51,15 +51,28 @@ func _raum_bauen(code: String, cfg: Dictionary) -> void: raum.name = code raeume.add_child(raum) - var behaelter := SubViewportContainer.new() + var behaelter: Node + var sicht: Node + if Net.dedicated: + # Auf dem Server laufen viele Partien nebeneinander. Jede braucht ihren + # eigenen Physikraum, sonst lägen alle Würfel im selben. + var c := SubViewportContainer.new() + c.stretch = true + c.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + var v := SubViewport.new() + v.own_world_3d = true + behaelter = c + sicht = v + else: + # Der Client hat nur eine Partie und rendert direkt ins Fenster. Ein + # SubViewport würde hier nur Auflösung und Kantenglättung kosten. + # Die Knotennamen müssen trotzdem gleich heißen - daran hängt das + # Ziel jedes RPC. + behaelter = Node.new() + sicht = Node.new() behaelter.name = "Container" - behaelter.stretch = true - behaelter.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) - raum.add_child(behaelter) - - var sicht := SubViewport.new() sicht.name = "Viewport" - sicht.own_world_3d = true + raum.add_child(behaelter) behaelter.add_child(sicht) var game := Node3D.new() @@ -75,5 +88,9 @@ func _raum_abbauen(code: String) -> void: raum.queue_free() +## Nur wenn wirklich eine Partie lief. Sonst würde ein Verbindungsabbruch im +## Menü die Szene neu laden und die Meldung dazu gleich wieder wegwerfen. func _back_to_menu() -> void: + if raeume.get_child_count() == 0: + return get_tree().change_scene_to_file("res://scenes/main.tscn") diff --git a/scripts/net.gd b/scripts/net.gd index 2303f13..dee0b95 100644 --- a/scripts/net.gd +++ b/scripts/net.gd @@ -152,7 +152,8 @@ func _req_create(pname: String) -> void: 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} + 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) @@ -259,13 +260,45 @@ func _req_start(ai_count: int) -> void: return raum.in_game = true var cfg := {"peers": order, "names": raum.lobby.duplicate(), "ai": ai_count} - for peer: int in order: - _begin.rpc_id(peer, code, cfg) + _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)