Files
pirate-dice/scripts/net.gd
2026-09-07 17:38:17 +02:00

265 lines
7.2 KiB
GDScript

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)