331 lines
10 KiB
GDScript
331 lines
10 KiB
GDScript
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)
|
|
## Nur Server: jemand will in eine laufende Partie zurück. Der Raum prüft,
|
|
## ob es einen verwaisten Platz mit dem Namen gibt.
|
|
signal rejoin_requested(code: String, peer: int, pname: 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
|
|
## Wunsch für eine neue Solopartie; das frisch geladene Menü löst ihn ein.
|
|
var rematch: Dictionary = {}
|
|
|
|
# ── 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:
|
|
# Kein Neuling, aber vielleicht ein Rückkehrer: dessen Platz spielt
|
|
# gerade ein Bot weiter. Der Raum entscheidet.
|
|
rejoin_requested.emit(code, id, pname.substr(0, 16))
|
|
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)
|
|
|
|
|
|
## Rückkehrer aufnehmen: in die Lobby eintragen und mit der laufenden
|
|
## Aufstellung starten lassen. `cfg` beschreibt alle Plätze, nicht nur Peers.
|
|
func rejoin_annehmen(code: String, peer: int, pname: String, cfg: Dictionary) -> void:
|
|
var raum: Dictionary = raeume.get(code, {})
|
|
if raum.is_empty():
|
|
return
|
|
raum.lobby[peer] = pname
|
|
peer_raum[peer] = code
|
|
raum.bestaetigt.erase(peer)
|
|
_begin.rpc_id(peer, code, cfg)
|
|
print("%s kehrt in Raum %s zurück." % [pname, code])
|
|
|
|
|
|
func abweisen(peer: int, grund: String) -> void:
|
|
_abgewiesen.rpc_id(peer, grund)
|
|
|
|
|
|
## 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, stufe := 1) -> 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,
|
|
"stufe": stufe})
|
|
|
|
|
|
## Der Host bittet den Server um den Start.
|
|
func request_start(ai_count: int, stufe := 1) -> void:
|
|
_req_start.rpc_id(1, ai_count, stufe)
|
|
|
|
|
|
@rpc("any_peer", "reliable")
|
|
func _req_start(ai_count: int, stufe: 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,
|
|
"stufe": clampi(stufe, 0, 2)}
|
|
_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)
|