687 lines
23 KiB
GDScript
687 lines
23 KiB
GDScript
extends "res://core/ui/panels/editor_panel.gd"
|
||
## Tab „Server“ (Etappe 3 „Server-Verwaltung“): Client der Match-Server-Admin-API.
|
||
## Mehrere Server speichern, verbinden, Status, Einstellungen, Spieler, Maps,
|
||
## Rotation, Logs und „Aktuelle Map auf den Server pushen“ – alles per Klick.
|
||
## Logik: core/server/ (AdminClient, ServerStore, AdminTexte, MapPush).
|
||
|
||
var store: ServerStore
|
||
var store_path := ServerStore.DEFAULT_PATH # Tests lenken um
|
||
var client: AdminClient
|
||
var current_id := ""
|
||
var last_notice := "" # letzte Meldung (Tests)
|
||
var connected := false
|
||
var restart_wait_s := 120.0 # so lange wartet „Server neu starten“ (Tests verkürzen)
|
||
var restart_poll_s := 2.0
|
||
|
||
var _sel: OptionButton
|
||
var _conn_label: Label
|
||
var _notice: Label
|
||
var _status_label: Label
|
||
var _live_box: VBoxContainer # alles, was einen verbundenen Server braucht
|
||
var _push_name: LineEdit
|
||
var _push_rot: CheckBox
|
||
var _push_result: Label
|
||
var _settings_box: VBoxContainer
|
||
var _settings_editors: Dictionary = {} # key -> Control
|
||
var _settings_loaded: Dictionary = {}
|
||
var _settings_spec: Dictionary = {}
|
||
var _settings_result: Label
|
||
var _players_box: VBoxContainer
|
||
var _bans_box: VBoxContainer
|
||
var _ban_minutes: OptionButton
|
||
var _maps_box: VBoxContainer
|
||
var _rotation_checks: Dictionary = {} # map id -> CheckBox
|
||
var _log_filter: LineEdit
|
||
var _log_text: TextEdit
|
||
var _confirm: ConfirmationDialog
|
||
var _confirm_action: Callable
|
||
var _edit_dialog: ConfirmationDialog
|
||
var _edit_fields: Dictionary = {}
|
||
var _edit_id := ""
|
||
|
||
const BAN_DAUER := [[10, "10 Minuten"], [60, "1 Stunde"], [1440, "1 Tag"], [10080, "1 Woche"], [0, "Dauerhaft"]]
|
||
|
||
|
||
static func spec() -> Dictionary:
|
||
return {"titel": "Server", "gruppe": "rechts", "order": 50}
|
||
|
||
|
||
func panel_title() -> String:
|
||
return "Server"
|
||
|
||
|
||
func build() -> void:
|
||
store = ServerStore.new(store_path)
|
||
store.load_store()
|
||
client = AdminClient.new()
|
||
client.name = "AdminClient"
|
||
add_child(client)
|
||
|
||
add_child(_header_label("Server verwalten"))
|
||
add_child(_muted_label("Verbinde den Editor mit deinem Match-Server (Admin-API), um Maps hochzuladen und den Server zu steuern."))
|
||
var hint := _muted_label(AdminTexte.TUNNEL_HINWEIS)
|
||
hint.visible = false
|
||
var hint_btn := Button.new()
|
||
hint_btn.text = "❓ Wie erreiche ich meinen Server sicher?"
|
||
hint_btn.flat = true
|
||
hint_btn.pressed.connect(func() -> void: hint.visible = not hint.visible)
|
||
add_child(hint_btn)
|
||
add_child(hint)
|
||
|
||
_sel = OptionButton.new()
|
||
_sel.custom_minimum_size = UIScale.v2(0, 36)
|
||
_sel.item_selected.connect(func(_i: int) -> void: _on_selected())
|
||
add_child(_sel)
|
||
var row := HBoxContainer.new()
|
||
add_child(row)
|
||
row.add_child(_btn("➕ Neu", _on_add))
|
||
row.add_child(_btn("✏ Bearbeiten", _on_edit))
|
||
row.add_child(_btn("🗑 Entfernen", _on_remove))
|
||
var conn_row := HBoxContainer.new()
|
||
add_child(conn_row)
|
||
conn_row.add_child(_btn("🔌 Verbinden", func() -> void: connect_selected()))
|
||
_conn_label = Label.new()
|
||
_conn_label.text = "Nicht verbunden"
|
||
_conn_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
_conn_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
conn_row.add_child(_conn_label)
|
||
_notice = _muted_label("")
|
||
add_child(_notice)
|
||
|
||
_live_box = VBoxContainer.new()
|
||
_live_box.add_theme_constant_override("separation", UIScale.px(8))
|
||
_live_box.visible = false
|
||
add_child(_live_box)
|
||
_build_live()
|
||
_build_dialogs()
|
||
_refresh_selector()
|
||
wrap_in_scroll()
|
||
|
||
|
||
func _btn(text: String, cb: Callable) -> Button:
|
||
var b := Button.new()
|
||
b.text = text
|
||
b.custom_minimum_size = UIScale.v2(0, 36)
|
||
b.pressed.connect(cb)
|
||
return b
|
||
|
||
|
||
func _section(title: String) -> void:
|
||
_live_box.add_child(_separator())
|
||
_live_box.add_child(_header_label(title))
|
||
|
||
|
||
func _build_live() -> void:
|
||
_section("Übersicht")
|
||
_status_label = Label.new()
|
||
_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
_live_box.add_child(_status_label)
|
||
_live_box.add_child(_btn("🔄 Alles aktualisieren", func() -> void: refresh_all()))
|
||
|
||
_section("Map auf Server pushen")
|
||
_live_box.add_child(_muted_label("Prüft die Map im Editor und lädt sie hoch. Der Server prüft sie noch einmal."))
|
||
_push_name = LineEdit.new()
|
||
_push_name.placeholder_text = "Dateiname (leer = Map-Name)"
|
||
_live_box.add_child(_push_name)
|
||
_push_rot = CheckBox.new()
|
||
_push_rot.text = "In die Rotation aufnehmen"
|
||
_live_box.add_child(_push_rot)
|
||
var pb := _btn("⬆ Aktuelle Map hochladen", func() -> void: push_current_map())
|
||
pb.custom_minimum_size = UIScale.v2(0, 44)
|
||
_live_box.add_child(pb)
|
||
_push_result = Label.new()
|
||
_push_result.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
_live_box.add_child(_push_result)
|
||
|
||
_section("Einstellungen")
|
||
_settings_box = VBoxContainer.new()
|
||
_live_box.add_child(_settings_box)
|
||
var srow := HBoxContainer.new()
|
||
_live_box.add_child(srow)
|
||
srow.add_child(_btn("💾 Speichern", func() -> void: save_settings()))
|
||
srow.add_child(_btn("↩ Zurücksetzen", func() -> void: _fill_settings_editors()))
|
||
_settings_result = _muted_label("")
|
||
_live_box.add_child(_settings_result)
|
||
|
||
_section("Spieler")
|
||
_players_box = VBoxContainer.new()
|
||
_live_box.add_child(_players_box)
|
||
var brow := HBoxContainer.new()
|
||
_live_box.add_child(brow)
|
||
brow.add_child(_muted_label("Sperrdauer:"))
|
||
_ban_minutes = OptionButton.new()
|
||
for d in BAN_DAUER:
|
||
_ban_minutes.add_item(d[1])
|
||
_ban_minutes.select(1)
|
||
brow.add_child(_ban_minutes)
|
||
_live_box.add_child(_muted_label("Gesperrt:"))
|
||
_bans_box = VBoxContainer.new()
|
||
_live_box.add_child(_bans_box)
|
||
|
||
_section("Match & Maps")
|
||
_live_box.add_child(_btn("🔁 Match neu starten", func() -> void: _ask("Match auf dem Server neu starten? Alle Spieler beginnen die Runde von vorn.", _do_restart)))
|
||
_maps_box = VBoxContainer.new()
|
||
_live_box.add_child(_maps_box)
|
||
_live_box.add_child(_btn("💾 Rotation speichern", func() -> void: save_rotation()))
|
||
|
||
_section("Server")
|
||
_live_box.add_child(_btn("⏻ Server neu starten", func() -> void: _ask("Server komplett neu starten? Alle Spieler werden getrennt und die laufende Runde endet.", func() -> void: await restart_server())))
|
||
|
||
_section("Server-Log")
|
||
_log_filter = LineEdit.new()
|
||
_log_filter.placeholder_text = "Filter (z. B. Admin, Join, FEHLER)"
|
||
_live_box.add_child(_log_filter)
|
||
_live_box.add_child(_btn("📜 Log laden", func() -> void: load_logs()))
|
||
_log_text = TextEdit.new()
|
||
_log_text.editable = false
|
||
_log_text.custom_minimum_size = UIScale.v2(0, 220)
|
||
_log_text.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY
|
||
_live_box.add_child(_log_text)
|
||
|
||
|
||
func _build_dialogs() -> void:
|
||
_confirm = ConfirmationDialog.new()
|
||
_confirm.ok_button_text = "Ja"
|
||
_confirm.cancel_button_text = "Abbrechen"
|
||
_confirm.confirmed.connect(func() -> void:
|
||
if _confirm_action.is_valid():
|
||
_confirm_action.call())
|
||
add_child(_confirm)
|
||
|
||
_edit_dialog = ConfirmationDialog.new()
|
||
_edit_dialog.title = "Server"
|
||
_edit_dialog.ok_button_text = "Speichern"
|
||
_edit_dialog.cancel_button_text = "Abbrechen"
|
||
var box := VBoxContainer.new()
|
||
_edit_dialog.add_child(box)
|
||
for f in [["name", "Name (frei wählbar)", "Mein Server"], ["host", "Adresse (IP oder Name)", "127.0.0.1"],
|
||
["port", "Port der Admin-API", str(ServerStore.DEFAULT_PORT)], ["token", "Admin-Token", ""]]:
|
||
box.add_child(_field_label(f[1]))
|
||
var le := LineEdit.new()
|
||
le.placeholder_text = f[2]
|
||
le.custom_minimum_size = UIScale.v2(320, 32)
|
||
if f[0] == "token":
|
||
le.secret = true
|
||
box.add_child(le)
|
||
_edit_fields[f[0]] = le
|
||
var tls := CheckBox.new()
|
||
tls.text = "Sichere Verbindung (https) – nur mit Reverse-Proxy"
|
||
box.add_child(tls)
|
||
_edit_fields["tls"] = tls
|
||
var show := CheckBox.new()
|
||
show.text = "Token anzeigen"
|
||
show.toggled.connect(func(on: bool) -> void: (_edit_fields["token"] as LineEdit).secret = not on)
|
||
box.add_child(show)
|
||
var err := _muted_label("")
|
||
box.add_child(err)
|
||
_edit_fields["_err"] = err
|
||
box.add_child(_btn("🔌 Verbindung testen", func() -> void: _test_dialog_entry()))
|
||
_edit_dialog.confirmed.connect(_on_edit_confirmed)
|
||
add_child(_edit_dialog)
|
||
|
||
|
||
# ------------------------------------------------------------------ Liste --
|
||
|
||
func _refresh_selector(select_id: String = "") -> void:
|
||
_sel.clear()
|
||
for e in store.servers:
|
||
_sel.add_item("%s (%s:%d)" % [e["name"], e["host"], e["port"]])
|
||
_sel.set_item_metadata(_sel.item_count - 1, e["id"])
|
||
if _sel.item_count == 0:
|
||
_sel.add_item("– noch kein Server gespeichert –")
|
||
_sel.disabled = true
|
||
current_id = ""
|
||
return
|
||
_sel.disabled = false
|
||
var idx := 0
|
||
for i in _sel.item_count:
|
||
if str(_sel.get_item_metadata(i)) == (select_id if select_id != "" else current_id):
|
||
idx = i
|
||
_sel.select(idx)
|
||
_on_selected()
|
||
|
||
|
||
func _on_selected() -> void:
|
||
if _sel.item_count == 0 or _sel.disabled:
|
||
return
|
||
var id := str(_sel.get_item_metadata(_sel.selected))
|
||
if id != current_id:
|
||
_set_connected(false, "Nicht verbunden")
|
||
current_id = id
|
||
|
||
|
||
func _on_add() -> void:
|
||
_open_edit({})
|
||
|
||
|
||
func _on_edit() -> void:
|
||
if current_id != "":
|
||
_open_edit(store.get_entry(current_id))
|
||
|
||
|
||
func _open_edit(e: Dictionary) -> void:
|
||
_edit_id = str(e.get("id", ""))
|
||
_edit_dialog.title = "Server bearbeiten" if _edit_id != "" else "Server hinzufügen"
|
||
(_edit_fields["name"] as LineEdit).text = str(e.get("name", ""))
|
||
(_edit_fields["host"] as LineEdit).text = str(e.get("host", ""))
|
||
(_edit_fields["port"] as LineEdit).text = str(e.get("port", ServerStore.DEFAULT_PORT))
|
||
(_edit_fields["token"] as LineEdit).text = str(e.get("token", ""))
|
||
(_edit_fields["tls"] as CheckBox).button_pressed = bool(e.get("tls", false))
|
||
(_edit_fields["_err"] as Label).text = ""
|
||
_edit_dialog.popup_centered(UIScale.v2i(380, 420))
|
||
|
||
|
||
func _entry_from_dialog() -> Dictionary:
|
||
return {
|
||
"id": _edit_id, "name": (_edit_fields["name"] as LineEdit).text,
|
||
"host": (_edit_fields["host"] as LineEdit).text.strip_edges(),
|
||
"port": int((_edit_fields["port"] as LineEdit).text),
|
||
"token": (_edit_fields["token"] as LineEdit).text,
|
||
"tls": (_edit_fields["tls"] as CheckBox).button_pressed}
|
||
|
||
|
||
func _on_edit_confirmed() -> void:
|
||
var id := store.upsert(_entry_from_dialog())
|
||
if id == "":
|
||
_notify(store.last_error)
|
||
return
|
||
_refresh_selector(id)
|
||
|
||
|
||
func _test_dialog_entry() -> void:
|
||
var e := _entry_from_dialog()
|
||
var err := AdminClient.check_address(str(e["host"]), int(e["port"]))
|
||
var lbl: Label = _edit_fields["_err"]
|
||
if err != "":
|
||
lbl.text = err
|
||
return
|
||
lbl.text = "Teste …"
|
||
var probe := AdminClient.new()
|
||
add_child(probe)
|
||
probe.use(e)
|
||
var res: Dictionary = await probe.call_api("GET", "/ping")
|
||
if bool(res["ok"]):
|
||
var auth: Dictionary = await probe.call_api("GET", "/status")
|
||
lbl.text = "✅ Server gefunden. " + ("Token passt." if bool(auth["ok"]) else AdminTexte.erklaere(auth))
|
||
else:
|
||
lbl.text = "❌ " + AdminTexte.erklaere(res)
|
||
probe.queue_free()
|
||
|
||
|
||
func _on_remove() -> void:
|
||
if current_id == "":
|
||
return
|
||
var e := store.public_view(current_id)
|
||
_ask("„%s“ aus der Liste entfernen? (Der Server selbst bleibt unverändert.)" % str(e["name"]), func() -> void:
|
||
store.remove(current_id)
|
||
current_id = ""
|
||
_set_connected(false, "Nicht verbunden")
|
||
_refresh_selector())
|
||
|
||
|
||
# ---------------------------------------------------------------- Verbinden --
|
||
|
||
func connect_selected() -> bool:
|
||
if current_id == "":
|
||
_notify("Bitte erst einen Server hinzufügen (➕ Neu).")
|
||
return false
|
||
client.use(store.get_entry(current_id))
|
||
_conn_label.text = "Verbinde …"
|
||
var ping: Dictionary = await client.call_api("GET", "/ping")
|
||
if not bool(ping["ok"]):
|
||
_set_connected(false, "❌ " + AdminTexte.erklaere(ping))
|
||
return false
|
||
var st: Dictionary = await client.call_api("GET", "/status")
|
||
if not bool(st["ok"]):
|
||
_set_connected(false, "❌ " + AdminTexte.erklaere(st))
|
||
return false
|
||
_set_connected(true, "✅ Verbunden")
|
||
await refresh_all()
|
||
return true
|
||
|
||
|
||
func _set_connected(on: bool, text: String) -> void:
|
||
connected = on
|
||
_conn_label.text = text
|
||
_live_box.visible = on
|
||
|
||
|
||
## Ruft die API auf und meldet Fehler verständlich. null = Fehler (schon gemeldet).
|
||
func _api(method: String, path: String, body: Variant = null, query: Dictionary = {}) -> Variant:
|
||
var res: Dictionary = await client.call_api(method, path, body, query)
|
||
if bool(res["ok"]):
|
||
return res["body"]
|
||
var text := AdminTexte.erklaere(res)
|
||
if int(res.get("status", 0)) in [0, 401, 429]:
|
||
_set_connected(false, "❌ " + text.split("\n")[0])
|
||
_notify(text)
|
||
return null
|
||
|
||
|
||
func _notify(text: String) -> void:
|
||
last_notice = text
|
||
_notice.text = text
|
||
|
||
|
||
func refresh_all() -> void:
|
||
await refresh_status()
|
||
await refresh_settings()
|
||
await refresh_players()
|
||
await refresh_maps()
|
||
|
||
|
||
# ------------------------------------------------------------------ Status --
|
||
|
||
func refresh_status() -> void:
|
||
var s: Variant = await _api("GET", "/status")
|
||
if s == null:
|
||
return
|
||
var p: Dictionary = s["players"]
|
||
var h := int(s["uptime_s"]) / 3600
|
||
var m := (int(s["uptime_s"]) % 3600) / 60
|
||
_status_label.text = "%s\nMap: %s · Modus: %s · Phase: %s\nSpieler: %d von %d (+ %d Bots) · Restzeit %d s\nLäuft seit %d h %d min · %s · Passwort: %s · Master: %s" % [
|
||
s["server_name"], s["map"]["name"], str(s["mode"]).to_upper(), s["phase"],
|
||
p["humans"], p["max"], p["bots"], int(s["time_remaining_s"]), h, m,
|
||
"öffentlich" if s["public"] else "privat", "ja" if s["password_set"] else "nein", s["master"]]
|
||
|
||
|
||
# ------------------------------------------------------------ Einstellungen --
|
||
|
||
func refresh_settings() -> void:
|
||
var r: Variant = await _api("GET", "/settings")
|
||
if r == null:
|
||
return
|
||
_settings_spec = r["spec"]
|
||
_settings_loaded = r["settings"]
|
||
for c in _settings_box.get_children():
|
||
c.queue_free()
|
||
_settings_editors.clear()
|
||
for key in _settings_spec.keys():
|
||
var sp: Dictionary = _settings_spec[key]
|
||
var lbl := _field_label("%s (%s)" % [sp["label"], AdminTexte.wirkung(str(sp["effect"]))])
|
||
_settings_box.add_child(lbl)
|
||
var ed: Control
|
||
match str(sp["type"]):
|
||
"bool":
|
||
ed = CheckBox.new()
|
||
"int", "float":
|
||
var sb := SpinBox.new()
|
||
sb.min_value = float(sp["min"])
|
||
sb.max_value = float(sp["max"])
|
||
sb.step = 1.0 if str(sp["type"]) == "int" else 0.5
|
||
ed = sb
|
||
"enum":
|
||
var ob := OptionButton.new()
|
||
for v in sp["values"]:
|
||
ob.add_item(str(v))
|
||
ed = ob
|
||
_:
|
||
var le := LineEdit.new()
|
||
le.max_length = int(sp.get("max_len", 64))
|
||
if key == "password":
|
||
le.secret = true
|
||
le.placeholder_text = "(unverändert)"
|
||
ed = le
|
||
_settings_box.add_child(ed)
|
||
_settings_editors[key] = ed
|
||
_fill_settings_editors()
|
||
|
||
|
||
func _fill_settings_editors() -> void:
|
||
for key in _settings_editors.keys():
|
||
var ed: Control = _settings_editors[key]
|
||
var sp: Dictionary = _settings_spec[key]
|
||
if key == "password":
|
||
(ed as LineEdit).text = ""
|
||
(ed as LineEdit).placeholder_text = "(gesetzt – leer lassen = unverändert)" if bool(_settings_loaded.get("password_set", false)) else "(kein Passwort)"
|
||
continue
|
||
var v: Variant = _settings_loaded.get(key)
|
||
match str(sp["type"]):
|
||
"bool": (ed as CheckBox).button_pressed = bool(v)
|
||
"int", "float": (ed as SpinBox).value = float(v)
|
||
"enum": (ed as OptionButton).select(maxi(0, (sp["values"] as Array).find(v)))
|
||
_: (ed as LineEdit).text = str(v)
|
||
_settings_result.text = ""
|
||
|
||
|
||
## Nur geänderte Werte (Passwort nur, wenn etwas eingetippt wurde).
|
||
func changed_settings() -> Dictionary:
|
||
var out := {}
|
||
for key in _settings_editors.keys():
|
||
var ed: Control = _settings_editors[key]
|
||
var sp: Dictionary = _settings_spec[key]
|
||
var v: Variant
|
||
match str(sp["type"]):
|
||
"bool": v = (ed as CheckBox).button_pressed
|
||
"int": v = int((ed as SpinBox).value)
|
||
"float": v = (ed as SpinBox).value
|
||
"enum": v = (sp["values"] as Array)[(ed as OptionButton).selected]
|
||
_: v = (ed as LineEdit).text
|
||
if key == "password":
|
||
if str(v) != "":
|
||
out[key] = v
|
||
continue
|
||
if v != _settings_loaded.get(key):
|
||
out[key] = v
|
||
return out
|
||
|
||
|
||
func save_settings() -> void:
|
||
var ch := changed_settings()
|
||
if ch.is_empty():
|
||
_settings_result.text = "Nichts geändert."
|
||
return
|
||
var r: Variant = await _api("PATCH", "/settings", ch)
|
||
if r == null:
|
||
return
|
||
var lines: PackedStringArray = []
|
||
if not (r["applied_now"] as Array).is_empty():
|
||
lines.append("✅ Sofort aktiv: " + ", ".join(_labels(r["applied_now"])))
|
||
if not (r["applies_next_map"] as Array).is_empty():
|
||
lines.append("⏭ Ab der nächsten Map: " + ", ".join(_labels(r["applies_next_map"])))
|
||
if not (r["needs_restart"] as Array).is_empty():
|
||
lines.append("🔁 Erst nach Server-Neustart: " + ", ".join(_labels(r["needs_restart"])))
|
||
for w in r.get("warnings", []):
|
||
lines.append("⚠ " + str(w))
|
||
_settings_result.text = "\n".join(lines)
|
||
last_notice = _settings_result.text
|
||
_settings_loaded = r["settings"]
|
||
_fill_settings_editors()
|
||
_settings_result.text = "\n".join(lines)
|
||
|
||
|
||
func _labels(keys: Array) -> Array:
|
||
return keys.map(func(k: Variant) -> String: return str((_settings_spec.get(k, {}) as Dictionary).get("label", k)))
|
||
|
||
|
||
# ----------------------------------------------------------------- Spieler --
|
||
|
||
func refresh_players() -> void:
|
||
var r: Variant = await _api("GET", "/players")
|
||
var b: Variant = await _api("GET", "/bans")
|
||
if r == null or b == null:
|
||
return
|
||
for c in _players_box.get_children():
|
||
c.queue_free()
|
||
if (r["players"] as Array).is_empty():
|
||
_players_box.add_child(_muted_label("Gerade ist niemand auf dem Server."))
|
||
for p in r["players"]:
|
||
var row := HBoxContainer.new()
|
||
var lbl := Label.new()
|
||
lbl.text = "%s (Team %s · %d/%d · %d ms)" % [p["name"], "Blau" if int(p["team"]) == 0 else "Rot", p["kills"], p["deaths"], p["ping_ms"]]
|
||
lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
lbl.clip_text = true
|
||
row.add_child(lbl)
|
||
var pid := int(p["peer_id"])
|
||
var pname := str(p["name"])
|
||
row.add_child(_btn("Kick", func() -> void: _ask("„%s“ vom Server werfen?" % pname, func() -> void: await kick(pid))))
|
||
row.add_child(_btn("Sperren", func() -> void: _ask("„%s“ sperren (%s)?" % [pname, BAN_DAUER[_ban_minutes.selected][1]], func() -> void: await ban(pid))))
|
||
_players_box.add_child(row)
|
||
for c in _bans_box.get_children():
|
||
c.queue_free()
|
||
if (b["bans"] as Array).is_empty():
|
||
_bans_box.add_child(_muted_label("Niemand gesperrt."))
|
||
for ban_e in b["bans"]:
|
||
var row2 := HBoxContainer.new()
|
||
var l2 := Label.new()
|
||
var until := int(ban_e.get("until", 0))
|
||
l2.text = "%s – %s" % [str(ban_e.get("name", "?")), "dauerhaft" if until == 0 else "bis " + Time.get_datetime_string_from_unix_time(until, true)]
|
||
l2.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
l2.clip_text = true
|
||
row2.add_child(l2)
|
||
var bid := str(ban_e["id"])
|
||
row2.add_child(_btn("Aufheben", func() -> void: await unban(bid)))
|
||
_bans_box.add_child(row2)
|
||
|
||
|
||
func kick(peer_id: int) -> void:
|
||
if await _api("POST", "/players/%d/kick" % peer_id, {}) != null:
|
||
_notify("Spieler wurde vom Server geworfen.")
|
||
await refresh_players()
|
||
|
||
|
||
func ban(peer_id: int, minutes: int = -1) -> void:
|
||
if minutes < 0:
|
||
minutes = int(BAN_DAUER[_ban_minutes.selected][0])
|
||
if await _api("POST", "/players/%d/ban" % peer_id, {"minutes": minutes}) != null:
|
||
_notify("Spieler gesperrt (%s)." % AdminTexte.sperrdauer_text(minutes))
|
||
await refresh_players()
|
||
|
||
|
||
func unban(ban_id: String) -> void:
|
||
if await _api("DELETE", "/bans/" + ban_id) != null:
|
||
_notify("Sperre aufgehoben.")
|
||
await refresh_players()
|
||
|
||
|
||
# -------------------------------------------------------------- Match, Maps --
|
||
|
||
func _do_restart() -> void:
|
||
if await _api("POST", "/match/restart", {}) != null:
|
||
_notify("Match wird neu gestartet.")
|
||
await refresh_status()
|
||
|
||
|
||
## Ganzen Server neu starten und warten, bis er wieder antwortet; dann neu verbinden.
|
||
func restart_server() -> bool:
|
||
var id := current_id
|
||
if await _api("POST", "/restart", {}) == null:
|
||
return false
|
||
_set_connected(false, "Server startet neu…")
|
||
_notify("Server startet neu…")
|
||
var res: Dictionary = await _wait_for_new_server(id)
|
||
if current_id != id:
|
||
return false # inzwischen anderen Server gewählt
|
||
if not bool(res["ok"]):
|
||
_set_connected(false, "❌ " + str(res["text"]))
|
||
_notify(str(res["text"]))
|
||
return false
|
||
var ok: bool = await connect_selected()
|
||
if ok:
|
||
_notify("✅ Server wurde neu gestartet.")
|
||
return ok
|
||
|
||
|
||
## Der alte Prozess antwortet noch kurz: Erst eine Laufzeit, die kleiner ist als die
|
||
## Wartezeit bisher, beweist den neuen Prozess.
|
||
func _wait_for_new_server(id: String) -> Dictionary:
|
||
var start := Time.get_ticks_msec()
|
||
while current_id == id:
|
||
var waited := (Time.get_ticks_msec() - start) / 1000.0
|
||
if waited >= restart_wait_s:
|
||
return {"ok": false, "text": AdminTexte.neustart_zeitueberschreitung(int(restart_wait_s))}
|
||
var st: Dictionary = await client.call_api("GET", "/status")
|
||
if bool(st["ok"]):
|
||
if float(st["body"].get("uptime_s", INF)) <= (Time.get_ticks_msec() - start) / 1000.0:
|
||
return {"ok": true, "text": ""}
|
||
elif int(st.get("status", 0)) in [401, 429]:
|
||
return {"ok": false, "text": AdminTexte.erklaere(st)}
|
||
await get_tree().create_timer(restart_poll_s).timeout
|
||
return {"ok": false, "text": ""}
|
||
|
||
|
||
func refresh_maps() -> void:
|
||
var r: Variant = await _api("GET", "/maps")
|
||
if r == null:
|
||
return
|
||
for c in _maps_box.get_children():
|
||
c.queue_free()
|
||
_rotation_checks.clear()
|
||
for m in r["maps"]:
|
||
var row := HBoxContainer.new()
|
||
var cb := CheckBox.new()
|
||
cb.button_pressed = bool(m["in_rotation"])
|
||
cb.tooltip_text = "In der Rotation"
|
||
row.add_child(cb)
|
||
_rotation_checks[str(m["id"])] = cb
|
||
var lbl := Label.new()
|
||
lbl.text = "%s%s%s" % [m.get("name", m["file"]), " ▶ läuft" if m["active"] else "", " (eingebaut)" if m["source"] == "builtin" else ""]
|
||
lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
lbl.clip_text = true
|
||
row.add_child(lbl)
|
||
var mid := str(m["id"])
|
||
var mname := str(m.get("name", m["file"]))
|
||
row.add_child(_btn("Laden", func() -> void: _ask("„%s“ jetzt auf dem Server laden? Die laufende Runde endet." % mname, func() -> void: await load_map(mid))))
|
||
if m["source"] == "upload":
|
||
var slug := mid.get_file().get_basename()
|
||
row.add_child(_btn("🗑", func() -> void: _ask("„%s“ vom Server LÖSCHEN? Das kann nicht rückgängig gemacht werden." % mname, func() -> void: await delete_map(slug))))
|
||
_maps_box.add_child(row)
|
||
|
||
|
||
func load_map(map_id: String) -> void:
|
||
if await _api("POST", "/match/map", {"map": map_id}) != null:
|
||
_notify("Map wird geladen.")
|
||
await refresh_status()
|
||
|
||
|
||
func delete_map(slug: String) -> void:
|
||
if await _api("DELETE", "/maps/" + slug) != null:
|
||
_notify("Map gelöscht.")
|
||
await refresh_maps()
|
||
|
||
|
||
func save_rotation() -> void:
|
||
var ids: Array = []
|
||
for id in _rotation_checks.keys():
|
||
if (_rotation_checks[id] as CheckBox).button_pressed:
|
||
ids.append(id)
|
||
if ids.is_empty():
|
||
_notify("Die Rotation braucht mindestens eine Map.")
|
||
return
|
||
if await _api("PUT", "/rotation", {"maps": ids}) != null:
|
||
_notify("Rotation gespeichert (%d Maps)." % ids.size())
|
||
await refresh_maps()
|
||
|
||
|
||
# -------------------------------------------------------------------- Push --
|
||
|
||
func push_current_map(overwrite: bool = false) -> Dictionary:
|
||
_push_result.text = "Prüfe und lade hoch …"
|
||
var res: Dictionary = await MapPush.push(client, main.doc, {
|
||
"name": _push_name.text, "overwrite": overwrite, "add_to_rotation": _push_rot.button_pressed})
|
||
_push_result.text = ("✅ " if bool(res["ok"]) else "❌ ") + str(res["text"])
|
||
last_notice = _push_result.text
|
||
if bool(res["exists"]):
|
||
_ask("Auf dem Server gibt es schon eine Map mit diesem Namen. Ersetzen?", func() -> void: await push_current_map(true))
|
||
elif bool(res["ok"]):
|
||
await refresh_maps()
|
||
return res
|
||
|
||
|
||
# -------------------------------------------------------------------- Logs --
|
||
|
||
func load_logs() -> void:
|
||
var q := {"lines": 300}
|
||
if _log_filter.text.strip_edges() != "":
|
||
q["filter"] = _log_filter.text.strip_edges()
|
||
var r: Variant = await _api("GET", "/logs", null, q)
|
||
if r == null:
|
||
return
|
||
_log_text.text = "\n".join(r["lines"])
|
||
_log_text.scroll_vertical = _log_text.get_line_count()
|
||
|
||
|
||
func _ask(text: String, action: Callable) -> void:
|
||
_confirm.dialog_text = text
|
||
_confirm_action = action
|
||
_confirm.popup_centered(UIScale.v2i(380, 140))
|