265 lines
7.2 KiB
GDScript
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)
|