scopedvr-map-core/ui/panels/server_panel.gd
2026-11-01 19:31:33 +01:00

687 lines
23 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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))