Server-Sektion im Editor: Client der Admin-API (Server speichern mit verschluesseltem Token, Uebersicht, Einstellungen, Spieler kicken/sperren, Map laden/loeschen, Rotation, Logs) und 'Aktuelle Map auf Server pushen' mit verstaendlicher 422-Fehlerliste; SvmapIO.to_signed_text; Tests inkl. E2E gegen echten Server
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
92e2c688c1
commit
48db200d3e
50 changed files with 2499 additions and 10 deletions
45
docs/server-sektion.md
Normal file
45
docs/server-sektion.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# Server-Sektion (Tab „Server“)
|
||||
|
||||
Der Editor ist ein Client der Match-Server-Admin-API (`docs/admin-api.md` im
|
||||
Repo scopedvr-match-server). Damit verwaltet jeder seinen eigenen Server – ohne
|
||||
Konsole, ohne Tailscale, auf jedem Betriebssystem.
|
||||
|
||||
## Einrichten
|
||||
1. Auf dem Server in der `server.cfg` `[admin] enabled=true` setzen und starten.
|
||||
Das Token steht danach in der `state_file` des Servers (Standard `admin.cfg`).
|
||||
2. Editor → Tab **Server** → **➕ Neu**: Name, Adresse, Port (Standard 24580), Token.
|
||||
„🔌 Verbindung testen“ prüft Erreichbarkeit und Token.
|
||||
3. **🔌 Verbinden**.
|
||||
|
||||
**Sicher erreichen:** Die Admin-API spricht Klartext-HTTP und hört standardmäßig nur
|
||||
auf localhost. Entweder SSH-Tunnel (`ssh -L 24580:127.0.0.1:24580 benutzer@server`,
|
||||
im Editor dann Adresse `127.0.0.1`) oder ein Reverse-Proxy mit https (Haken
|
||||
„Sichere Verbindung“).
|
||||
|
||||
## Funktionen
|
||||
- Mehrere Server speichern; **Token verschlüsselt** (Schlüssel aus Rechner-ID +
|
||||
Nutzerordner), nie im Klartext in Datei oder Log. Schutz gegen Zufallsleser, nicht
|
||||
gegen Schadsoftware auf deinem Benutzerkonto.
|
||||
- **Übersicht:** Name, Map, Modus, Phase, Spieler/Bots, Laufzeit, öffentlich/privat.
|
||||
- **⬆ Aktuelle Map hochladen:** prüft und signiert die Map im Editor (wie Speichern),
|
||||
lädt sie hoch, der Server prüft noch einmal. Ungültige Maps werden gar nicht erst
|
||||
gesendet; 422-Fehler erscheinen als verständliche Liste mit Hilfetexten.
|
||||
Existiert der Name schon, fragt der Editor, ob ersetzt werden soll. Optional:
|
||||
direkt in die Rotation aufnehmen.
|
||||
- **Einstellungen:** Formular direkt aus der Server-Spec; gesendet werden nur
|
||||
geänderte Werte. Die Antwort zeigt je Wert: sofort / ab nächster Map / nach
|
||||
Neustart. „↩ Zurücksetzen“ verwirft Änderungen.
|
||||
- **Spieler:** Kick, Sperren (10 min … dauerhaft), Sperrliste mit „Aufheben“.
|
||||
- **Match & Maps:** Match neu starten, Map jetzt laden, Rotation per Häkchen,
|
||||
hochgeladene Maps löschen. Alles Zerstörerische fragt vorher nach.
|
||||
- **Server-Log** mit Filter.
|
||||
|
||||
## Code
|
||||
`src/server/` (AdminClient, ServerStore, AdminTexte, MapPush),
|
||||
`src/ui/panels/server_panel.gd`, `SvmapIO.to_signed_text`.
|
||||
|
||||
## Tests
|
||||
`tests/test_server_sektion.gd` (Unit), `tests/run_server_sektion_e2e.sh`
|
||||
(echter Match-Server + echtes Panel: Verbinden, falsches Token, unerreichbar,
|
||||
Einstellungen, Push gültig/doppelt/ersetzen/lokal ungültig/422, Rotation,
|
||||
Map laden/löschen, Restart, Kick, Logs).
|
||||
40
library/thumbs/bau_dach_ecke_aussen.png.import
Normal file
40
library/thumbs/bau_dach_ecke_aussen.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://djm3tfu1hf0wu"
|
||||
path="res://.godot/imported/bau_dach_ecke_aussen.png-e588c4f52ecd2b419b53da2ed03fba41.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://library/thumbs/bau_dach_ecke_aussen.png"
|
||||
dest_files=["res://.godot/imported/bau_dach_ecke_aussen.png-e588c4f52ecd2b419b53da2ed03fba41.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
library/thumbs/bau_dach_ecke_innen.png.import
Normal file
40
library/thumbs/bau_dach_ecke_innen.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://drk0drrumym47"
|
||||
path="res://.godot/imported/bau_dach_ecke_innen.png-76568d9aa31ff8a708208e9950258766.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://library/thumbs/bau_dach_ecke_innen.png"
|
||||
dest_files=["res://.godot/imported/bau_dach_ecke_innen.png-76568d9aa31ff8a708208e9950258766.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
library/thumbs/bau_dach_giebel_ende.png.import
Normal file
40
library/thumbs/bau_dach_giebel_ende.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://d4mwoant504rw"
|
||||
path="res://.godot/imported/bau_dach_giebel_ende.png-2b3b413e0bbd5a2e891b2db4f2cb998a.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://library/thumbs/bau_dach_giebel_ende.png"
|
||||
dest_files=["res://.godot/imported/bau_dach_giebel_ende.png-2b3b413e0bbd5a2e891b2db4f2cb998a.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
library/thumbs/bau_dach_plateau.png.import
Normal file
40
library/thumbs/bau_dach_plateau.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dd36il31xybcr"
|
||||
path="res://.godot/imported/bau_dach_plateau.png-a214f27a54f2e159ae179f49a5e43f35.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://library/thumbs/bau_dach_plateau.png"
|
||||
dest_files=["res://.godot/imported/bau_dach_plateau.png-a214f27a54f2e159ae179f49a5e43f35.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
library/thumbs/bau_dach_schraege.png.import
Normal file
40
library/thumbs/bau_dach_schraege.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://chval4fuxxbsh"
|
||||
path="res://.godot/imported/bau_dach_schraege.png-42e12c1909f5a82025bdac6cbeafbc9e.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://library/thumbs/bau_dach_schraege.png"
|
||||
dest_files=["res://.godot/imported/bau_dach_schraege.png-42e12c1909f5a82025bdac6cbeafbc9e.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
library/thumbs/container_001_offen1.png.import
Normal file
40
library/thumbs/container_001_offen1.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://btjpsdieswfhj"
|
||||
path="res://.godot/imported/container_001_offen1.png-725108fda9fc4bae4e75d8737617f24a.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://library/thumbs/container_001_offen1.png"
|
||||
dest_files=["res://.godot/imported/container_001_offen1.png-725108fda9fc4bae4e75d8737617f24a.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
library/thumbs/container_001_offen2.png.import
Normal file
40
library/thumbs/container_001_offen2.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bngbmgxfh4ft7"
|
||||
path="res://.godot/imported/container_001_offen2.png-ac048e0498e6a7edec41677c95acbe22.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://library/thumbs/container_001_offen2.png"
|
||||
dest_files=["res://.godot/imported/container_001_offen2.png-ac048e0498e6a7edec41677c95acbe22.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
library/thumbs/container_001_ohne.png.import
Normal file
40
library/thumbs/container_001_ohne.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cgdt3h13rqdug"
|
||||
path="res://.godot/imported/container_001_ohne.png-e8a4d4481a3212fe1399f5abe48dc49f.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://library/thumbs/container_001_ohne.png"
|
||||
dest_files=["res://.godot/imported/container_001_ohne.png-e8a4d4481a3212fe1399f5abe48dc49f.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
library/thumbs/container_002_offen1.png.import
Normal file
40
library/thumbs/container_002_offen1.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://chcsxajuvgspk"
|
||||
path="res://.godot/imported/container_002_offen1.png-61a1ea8088ec6a0d737e55e04ef9603d.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://library/thumbs/container_002_offen1.png"
|
||||
dest_files=["res://.godot/imported/container_002_offen1.png-61a1ea8088ec6a0d737e55e04ef9603d.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
library/thumbs/container_002_offen2.png.import
Normal file
40
library/thumbs/container_002_offen2.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dw56ic7fws26"
|
||||
path="res://.godot/imported/container_002_offen2.png-15b6d9bade0ee7bc41522048722c4faa.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://library/thumbs/container_002_offen2.png"
|
||||
dest_files=["res://.godot/imported/container_002_offen2.png-15b6d9bade0ee7bc41522048722c4faa.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
library/thumbs/container_002_ohne.png.import
Normal file
40
library/thumbs/container_002_ohne.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dwc0jmnhnch4k"
|
||||
path="res://.godot/imported/container_002_ohne.png-31e74fa68f10023ca6e8312f3b689d2c.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://library/thumbs/container_002_ohne.png"
|
||||
dest_files=["res://.godot/imported/container_002_ohne.png-31e74fa68f10023ca6e8312f3b689d2c.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
library/thumbs/container_003_offen1.png.import
Normal file
40
library/thumbs/container_003_offen1.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dprnj64lwdete"
|
||||
path="res://.godot/imported/container_003_offen1.png-8d5eab9b669b706ea4aa37ad53c4ba95.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://library/thumbs/container_003_offen1.png"
|
||||
dest_files=["res://.godot/imported/container_003_offen1.png-8d5eab9b669b706ea4aa37ad53c4ba95.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
library/thumbs/container_003_offen2.png.import
Normal file
40
library/thumbs/container_003_offen2.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://c2kpdcvfpfapd"
|
||||
path="res://.godot/imported/container_003_offen2.png-7c9deb6f88b261c0b4aecf35b01f8314.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://library/thumbs/container_003_offen2.png"
|
||||
dest_files=["res://.godot/imported/container_003_offen2.png-7c9deb6f88b261c0b4aecf35b01f8314.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
library/thumbs/container_003_ohne.png.import
Normal file
40
library/thumbs/container_003_ohne.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://df4oqdfkq2odt"
|
||||
path="res://.godot/imported/container_003_ohne.png-22a7100a50dcef57430f90c79791e125.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://library/thumbs/container_003_ohne.png"
|
||||
dest_files=["res://.godot/imported/container_003_ohne.png-22a7100a50dcef57430f90c79791e125.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
library/thumbs/container_004_offen1.png.import
Normal file
40
library/thumbs/container_004_offen1.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://ij0up66wkdei"
|
||||
path="res://.godot/imported/container_004_offen1.png-06087770f1b66fc9590ac9dfceebe7a6.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://library/thumbs/container_004_offen1.png"
|
||||
dest_files=["res://.godot/imported/container_004_offen1.png-06087770f1b66fc9590ac9dfceebe7a6.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
library/thumbs/container_004_offen2.png.import
Normal file
40
library/thumbs/container_004_offen2.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://6so0720te2wu"
|
||||
path="res://.godot/imported/container_004_offen2.png-ae6f991a964a2789eb52bbdca8dc99d9.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://library/thumbs/container_004_offen2.png"
|
||||
dest_files=["res://.godot/imported/container_004_offen2.png-ae6f991a964a2789eb52bbdca8dc99d9.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
library/thumbs/container_004_ohne.png.import
Normal file
40
library/thumbs/container_004_ohne.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://0g2gmbbh2ltg"
|
||||
path="res://.godot/imported/container_004_ohne.png-5fbe39a60bd4f74e4a899e22aaec7052.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://library/thumbs/container_004_ohne.png"
|
||||
dest_files=["res://.godot/imported/container_004_ohne.png-5fbe39a60bd4f74e4a899e22aaec7052.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
library/thumbs/container_005_offen1.png.import
Normal file
40
library/thumbs/container_005_offen1.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://b05yavnc71m3q"
|
||||
path="res://.godot/imported/container_005_offen1.png-8690fc5534a36230b3eed962443bff34.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://library/thumbs/container_005_offen1.png"
|
||||
dest_files=["res://.godot/imported/container_005_offen1.png-8690fc5534a36230b3eed962443bff34.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
library/thumbs/container_005_offen2.png.import
Normal file
40
library/thumbs/container_005_offen2.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://b7sjub4cbkxvg"
|
||||
path="res://.godot/imported/container_005_offen2.png-e489310c39c72eae0567321deb8ea3cf.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://library/thumbs/container_005_offen2.png"
|
||||
dest_files=["res://.godot/imported/container_005_offen2.png-e489310c39c72eae0567321deb8ea3cf.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
library/thumbs/container_005_ohne.png.import
Normal file
40
library/thumbs/container_005_ohne.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bnsu2xyjabygv"
|
||||
path="res://.godot/imported/container_005_ohne.png-cca3bca3fe3c4fb400950a1c006ccd92.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://library/thumbs/container_005_ohne.png"
|
||||
dest_files=["res://.godot/imported/container_005_ohne.png-cca3bca3fe3c4fb400950a1c006ccd92.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
library/thumbs/container_006_offen1.png.import
Normal file
40
library/thumbs/container_006_offen1.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bxxe0may3yeqc"
|
||||
path="res://.godot/imported/container_006_offen1.png-e48ae560414dc7eaebdb9ab27cabffe5.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://library/thumbs/container_006_offen1.png"
|
||||
dest_files=["res://.godot/imported/container_006_offen1.png-e48ae560414dc7eaebdb9ab27cabffe5.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
library/thumbs/container_006_offen2.png.import
Normal file
40
library/thumbs/container_006_offen2.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://c4e8fd4t7r7m6"
|
||||
path="res://.godot/imported/container_006_offen2.png-625b70d0d67725f796bf54ac92fca681.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://library/thumbs/container_006_offen2.png"
|
||||
dest_files=["res://.godot/imported/container_006_offen2.png-625b70d0d67725f796bf54ac92fca681.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
library/thumbs/container_006_ohne.png.import
Normal file
40
library/thumbs/container_006_ohne.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dhrrb85snu41i"
|
||||
path="res://.godot/imported/container_006_ohne.png-7974c20bd4edf4927c656fe6fc90b81e.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://library/thumbs/container_006_ohne.png"
|
||||
dest_files=["res://.godot/imported/container_006_ohne.png-7974c20bd4edf4927c656fe6fc90b81e.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
library/thumbs/container_007_offen1.png.import
Normal file
40
library/thumbs/container_007_offen1.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://denqy1bllro0l"
|
||||
path="res://.godot/imported/container_007_offen1.png-e038946d3d569077182fe8624d8d0d02.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://library/thumbs/container_007_offen1.png"
|
||||
dest_files=["res://.godot/imported/container_007_offen1.png-e038946d3d569077182fe8624d8d0d02.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
library/thumbs/container_007_offen2.png.import
Normal file
40
library/thumbs/container_007_offen2.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bqkr34vaxvy5c"
|
||||
path="res://.godot/imported/container_007_offen2.png-ded39ed55a00a5b1688c26cad378b653.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://library/thumbs/container_007_offen2.png"
|
||||
dest_files=["res://.godot/imported/container_007_offen2.png-ded39ed55a00a5b1688c26cad378b653.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
library/thumbs/container_007_ohne.png.import
Normal file
40
library/thumbs/container_007_ohne.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bqm02u41bb737"
|
||||
path="res://.godot/imported/container_007_ohne.png-bfcd83f47c7ec33ca986f3a5ca9daae9.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://library/thumbs/container_007_ohne.png"
|
||||
dest_files=["res://.godot/imported/container_007_ohne.png-bfcd83f47c7ec33ca986f3a5ca9daae9.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
library/thumbs/container_008_offen1.png.import
Normal file
40
library/thumbs/container_008_offen1.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cbvq06me7e21h"
|
||||
path="res://.godot/imported/container_008_offen1.png-3b36a26198ee2840738646efa562a9d7.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://library/thumbs/container_008_offen1.png"
|
||||
dest_files=["res://.godot/imported/container_008_offen1.png-3b36a26198ee2840738646efa562a9d7.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
library/thumbs/container_008_offen2.png.import
Normal file
40
library/thumbs/container_008_offen2.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://i6qdinbukrrw"
|
||||
path="res://.godot/imported/container_008_offen2.png-acbf05005956828cdbebad15f60ba922.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://library/thumbs/container_008_offen2.png"
|
||||
dest_files=["res://.godot/imported/container_008_offen2.png-acbf05005956828cdbebad15f60ba922.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
library/thumbs/container_008_ohne.png.import
Normal file
40
library/thumbs/container_008_ohne.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://yrbfa8pcdvlh"
|
||||
path="res://.godot/imported/container_008_ohne.png-4b391d87bf6eb80cdc2265187e867d46.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://library/thumbs/container_008_ohne.png"
|
||||
dest_files=["res://.godot/imported/container_008_ohne.png-4b391d87bf6eb80cdc2265187e867d46.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
112
src/server/admin_client.gd
Normal file
112
src/server/admin_client.gd
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
class_name AdminClient
|
||||
extends Node
|
||||
## Client der Match-Server-Admin-API v1 (Doku: docs/admin-api.md im
|
||||
## scopedvr-match-server-Repo). Knoten in den Baum hängen, dann
|
||||
## `var res = await client.call_api("GET", "/status")`.
|
||||
##
|
||||
## Antwort: {ok: bool (HTTP 2xx), status: int, body: Dictionary, transport_error: String}
|
||||
## transport_error ist deutsch und leer, wenn der Server geantwortet hat.
|
||||
## Das Token geht NUR als Header raus und wird nie geloggt oder zurückgegeben.
|
||||
|
||||
const TIMEOUT_S := 30.0
|
||||
const MAX_RESPONSE_BYTES := 8 * 1024 * 1024
|
||||
|
||||
var host: String = ""
|
||||
var port: int = 24580
|
||||
var tls: bool = false
|
||||
var token: String = ""
|
||||
|
||||
|
||||
## Server-Eintrag aus dem ServerStore übernehmen.
|
||||
func use(entry: Dictionary) -> void:
|
||||
host = str(entry.get("host", ""))
|
||||
port = int(entry.get("port", 24580))
|
||||
tls = bool(entry.get("tls", false))
|
||||
token = str(entry.get("token", ""))
|
||||
|
||||
|
||||
static func base_url(p_host: String, p_port: int, p_tls: bool) -> String:
|
||||
var h := p_host
|
||||
if h.contains(":") and not h.begins_with("["):
|
||||
h = "[%s]" % h # IPv6
|
||||
return "%s://%s:%d/api/v1" % ["https" if p_tls else "http", h, p_port]
|
||||
|
||||
|
||||
## Prüft die Eingabe für „Server hinzufügen“. Leerer Text = ok.
|
||||
static func check_address(p_host: String, p_port: int) -> String:
|
||||
var h := p_host.strip_edges()
|
||||
if h.is_empty():
|
||||
return "Bitte eine Adresse eingeben (IP oder Name)."
|
||||
if h.contains("/") or h.contains(" ") or h.contains("://"):
|
||||
return "Nur die Adresse eintragen, ohne http:// und ohne Schrägstriche (z. B. 192.168.1.20)."
|
||||
if p_port < 1 or p_port > 65535:
|
||||
return "Der Port muss zwischen 1 und 65535 liegen."
|
||||
return ""
|
||||
|
||||
|
||||
func call_api(method: String, path: String, body: Variant = null, query: Dictionary = {}, raw: PackedByteArray = PackedByteArray()) -> Dictionary:
|
||||
var err := check_address(host, port)
|
||||
if err != "":
|
||||
return _transport_fail(err)
|
||||
var url := base_url(host, port, tls) + path
|
||||
if not query.is_empty():
|
||||
var parts: PackedStringArray = []
|
||||
for k in query.keys():
|
||||
parts.append("%s=%s" % [str(k).uri_encode(), str(query[k]).uri_encode()])
|
||||
url += "?" + "&".join(parts)
|
||||
var headers: PackedStringArray = ["Accept: application/json"]
|
||||
if token != "":
|
||||
headers.append("Authorization: Bearer " + token)
|
||||
var payload := raw
|
||||
if body != null:
|
||||
payload = JSON.stringify(body).to_utf8_buffer()
|
||||
headers.append("Content-Type: application/json")
|
||||
elif not raw.is_empty():
|
||||
headers.append("Content-Type: application/octet-stream")
|
||||
var http := HTTPRequest.new()
|
||||
http.timeout = TIMEOUT_S
|
||||
http.body_size_limit = MAX_RESPONSE_BYTES
|
||||
http.accept_gzip = false
|
||||
add_child(http)
|
||||
var m := _method(method)
|
||||
var rerr := http.request_raw(url, headers, m, payload)
|
||||
if rerr != OK:
|
||||
http.queue_free()
|
||||
return _transport_fail("Anfrage konnte nicht gestartet werden (Fehler %d)." % rerr)
|
||||
var done: Array = await http.request_completed
|
||||
http.queue_free()
|
||||
var result: int = done[0]
|
||||
if result != HTTPRequest.RESULT_SUCCESS:
|
||||
return _transport_fail(_result_text(result))
|
||||
var code: int = done[1]
|
||||
var parsed: Variant = JSON.parse_string((done[3] as PackedByteArray).get_string_from_utf8())
|
||||
var out: Dictionary = parsed if parsed is Dictionary else {}
|
||||
return {"ok": code >= 200 and code < 300, "status": code, "body": out, "transport_error": ""}
|
||||
|
||||
|
||||
static func _method(m: String) -> int:
|
||||
match m.to_upper():
|
||||
"POST": return HTTPClient.METHOD_POST
|
||||
"PUT": return HTTPClient.METHOD_PUT
|
||||
"PATCH": return HTTPClient.METHOD_PATCH
|
||||
"DELETE": return HTTPClient.METHOD_DELETE
|
||||
return HTTPClient.METHOD_GET
|
||||
|
||||
|
||||
static func _transport_fail(msg: String) -> Dictionary:
|
||||
return {"ok": false, "status": 0, "body": {}, "transport_error": msg}
|
||||
|
||||
|
||||
static func _result_text(result: int) -> String:
|
||||
match result:
|
||||
HTTPRequest.RESULT_CANT_CONNECT, HTTPRequest.RESULT_CONNECTION_ERROR:
|
||||
return "Keine Verbindung zum Server. Läuft er, ist die Admin-API eingeschaltet, und stimmen Adresse und Port?"
|
||||
HTTPRequest.RESULT_CANT_RESOLVE:
|
||||
return "Die Adresse wurde nicht gefunden. Tippfehler?"
|
||||
HTTPRequest.RESULT_TIMEOUT:
|
||||
return "Der Server antwortet nicht (Zeitüberschreitung)."
|
||||
HTTPRequest.RESULT_TLS_HANDSHAKE_ERROR:
|
||||
return "Sichere Verbindung (https) fehlgeschlagen. Gibt der Server wirklich https her?"
|
||||
HTTPRequest.RESULT_BODY_SIZE_LIMIT_EXCEEDED:
|
||||
return "Die Antwort des Servers ist zu groß."
|
||||
return "Verbindungsfehler (Code %d)." % result
|
||||
1
src/server/admin_client.gd.uid
Normal file
1
src/server/admin_client.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dd3mjb50m63hm
|
||||
97
src/server/admin_texte.gd
Normal file
97
src/server/admin_texte.gd
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
class_name AdminTexte
|
||||
extends RefCounted
|
||||
## Verständliche deutsche Texte zu Admin-API-Antworten (kein Fachchinesisch,
|
||||
## kein rohes JSON) – inklusive der Fehlerliste (422) beim Map-Upload.
|
||||
|
||||
const TUNNEL_HINWEIS := "Tipp: Die Admin-API hört standardmäßig nur auf dem Server selbst (localhost). Von außen erreichst du sie sicher über einen SSH-Tunnel (z. B. ssh -L 24580:127.0.0.1:24580 benutzer@server, dann hier Adresse 127.0.0.1) oder über einen Reverse-Proxy mit https. Die Verbindung selbst ist sonst unverschlüsselt."
|
||||
|
||||
## Erklärungen für häufige Prüfregeln der Map (Server und Editor nutzen dieselben IDs).
|
||||
const REGEL_HILFE := {
|
||||
"V1": "Die Datei ist zu groß.",
|
||||
"V2": "Die Datei ist keine gültige Map-Datei.",
|
||||
"V3": "Format-Version passt nicht – Editor und Server müssen zusammenpassen.",
|
||||
"V4": "Ein Pflichtbereich der Map fehlt.",
|
||||
"V6": "Das Terrain ist zu groß oder zu detailreich.",
|
||||
"V8": "Zu viele Objekte oder zu viele Polygone – Objekte entfernen.",
|
||||
"V11": "Pro Team werden mindestens 4 Spawn-Punkte gebraucht (Spawn-Zonen malen).",
|
||||
"V14": "Die Map ragt über die erlaubte Größe hinaus.",
|
||||
}
|
||||
|
||||
|
||||
## Antwort → Text für den Nutzer ("" bei Erfolg ohne Besonderheiten).
|
||||
static func erklaere(res: Dictionary) -> String:
|
||||
if str(res.get("transport_error", "")) != "":
|
||||
return str(res["transport_error"]) + "\n\n" + TUNNEL_HINWEIS
|
||||
var status := int(res.get("status", 0))
|
||||
var body: Dictionary = res.get("body", {})
|
||||
var server_text := str(body.get("error", ""))
|
||||
match status:
|
||||
401:
|
||||
return "Das Token stimmt nicht (oder fehlt). Es steht beim Server in der Admin-Datei (state_file) – bitte neu eintragen."
|
||||
429:
|
||||
return "Zu viele falsche Token-Versuche. Der Server sperrt dich kurz – bitte in ein paar Minuten erneut versuchen."
|
||||
404:
|
||||
if str(body.get("code", "")) == "not_found":
|
||||
return "Diese Funktion kennt der Server nicht. Ist er auf dem neuesten Stand?"
|
||||
413:
|
||||
return "Die Datei ist größer als der Server erlaubt. " + server_text
|
||||
422:
|
||||
return erklaere_map_fehler(body)
|
||||
if server_text != "":
|
||||
return server_text
|
||||
if status >= 200 and status < 300:
|
||||
return ""
|
||||
return "Unerwartete Antwort vom Server (Status %d)." % status
|
||||
|
||||
|
||||
## Fehlerliste einer abgelehnten Map als Aufzählung mit Hilfetexten.
|
||||
static func erklaere_map_fehler(body: Dictionary) -> String:
|
||||
var errs: Array = body.get("errors", [])
|
||||
var out := "Der Server hat die Map abgelehnt"
|
||||
out += " (%d Problem%s):\n" % [errs.size(), "" if errs.size() == 1 else "e"] if not errs.is_empty() else ".\n"
|
||||
var hints := {}
|
||||
for e in errs:
|
||||
var t := str(e)
|
||||
out += "• " + t + "\n"
|
||||
var rule := t.split(":")[0].strip_edges()
|
||||
for k in REGEL_HILFE.keys():
|
||||
if rule == k or rule.begins_with(k) and not rule.substr(k.length(), 1).is_valid_int():
|
||||
hints[k] = REGEL_HILFE[k]
|
||||
if not hints.is_empty():
|
||||
out += "\nSo behebst du es:\n"
|
||||
for k in hints.keys():
|
||||
out += "• %s: %s\n" % [k, hints[k]]
|
||||
return out.strip_edges()
|
||||
|
||||
|
||||
## Dateiname für den Upload: a–z, 0–9, _ und -, höchstens 40 Zeichen (wie der Server).
|
||||
static func slugify(text: String) -> String:
|
||||
var t := text.strip_edges().to_lower()
|
||||
for pair in [["ä", "ae"], ["ö", "oe"], ["ü", "ue"], ["ß", "ss"]]:
|
||||
t = t.replace(pair[0], pair[1])
|
||||
var out := ""
|
||||
for c in t:
|
||||
out += c if ((c >= "a" and c <= "z") or (c >= "0" and c <= "9") or c == "-") else "_"
|
||||
while out.contains("__"):
|
||||
out = out.replace("__", "_")
|
||||
out = out.lstrip("_-").rstrip("_")
|
||||
return out.substr(0, 40)
|
||||
|
||||
|
||||
## Wirkung einer Einstellung in Klartext.
|
||||
static func wirkung(effect: String) -> String:
|
||||
match effect:
|
||||
"live": return "sofort"
|
||||
"next_map": return "ab der nächsten Map"
|
||||
"restart": return "nach Server-Neustart"
|
||||
return effect
|
||||
|
||||
|
||||
static func sperrdauer_text(minutes: int) -> String:
|
||||
if minutes <= 0:
|
||||
return "dauerhaft"
|
||||
if minutes < 60:
|
||||
return "%d Minuten" % minutes
|
||||
if minutes < 1440:
|
||||
return "%d Stunden" % (minutes / 60)
|
||||
return "%d Tage" % (minutes / 1440)
|
||||
1
src/server/admin_texte.gd.uid
Normal file
1
src/server/admin_texte.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://baspf3qu07cwi
|
||||
40
src/server/map_push.gd
Normal file
40
src/server/map_push.gd
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
class_name MapPush
|
||||
extends RefCounted
|
||||
## „Map auf Server pushen“: aktuelle Map lokal prüfen und signieren
|
||||
## (SvmapIO.to_signed_text), dann per Admin-API hochladen. UI-frei und testbar.
|
||||
|
||||
## opts: {name: String ("" = aus Map-Name), overwrite: bool, add_to_rotation: bool}
|
||||
## Rückgabe: {ok, stage ("lokal"|"upload"), text (verständliche Meldung),
|
||||
## id (Server-Map-ID), exists (bool: 409 – Nachfrage nötig), status}
|
||||
static func push(client: AdminClient, doc: Dictionary, opts: Dictionary = {}) -> Dictionary:
|
||||
var signed := SvmapIO.to_signed_text(doc)
|
||||
if not bool(signed["ok"]):
|
||||
var lines := "Die Map ist noch nicht gültig und wurde nicht hochgeladen:\n"
|
||||
for e in signed["errors"]:
|
||||
lines += "• %s\n" % str(e)
|
||||
return {"ok": false, "stage": "lokal", "text": lines.strip_edges(), "id": "", "exists": false, "status": 0}
|
||||
var info: Dictionary = doc.get("info", {})
|
||||
var slug := str(opts.get("name", "")).strip_edges()
|
||||
if slug == "":
|
||||
slug = AdminTexte.slugify(str(info.get("name", "")))
|
||||
if slug == "":
|
||||
return {"ok": false, "stage": "lokal", "text": "Die Map braucht einen Namen (Tab „Map“), bevor sie hochgeladen werden kann.", "id": "", "exists": false, "status": 0}
|
||||
var bytes: PackedByteArray = str(signed["text"]).to_utf8_buffer()
|
||||
var query := {"name": slug}
|
||||
if bool(opts.get("overwrite", false)):
|
||||
query["overwrite"] = "1"
|
||||
if bool(opts.get("add_to_rotation", false)):
|
||||
query["add_to_rotation"] = "1"
|
||||
var res: Dictionary = await client.call_api("POST", "/maps", null, query, bytes)
|
||||
var body: Dictionary = res.get("body", {})
|
||||
if bool(res.get("ok", false)):
|
||||
var msg := "Hochgeladen als „%s“ (%s, %d Objekte)." % [str(body.get("name", slug)), _groesse(bytes.size()), int(body.get("objects", 0))]
|
||||
if bool(body.get("replaced", false)):
|
||||
msg += " Die vorherige Version wurde ersetzt."
|
||||
return {"ok": true, "stage": "upload", "text": msg, "id": str(body.get("id", "")), "exists": false, "status": 200}
|
||||
var exists := int(res.get("status", 0)) == 409 and str(body.get("code", "")) == "exists"
|
||||
return {"ok": false, "stage": "upload", "text": AdminTexte.erklaere(res), "id": "", "exists": exists, "status": int(res.get("status", 0))}
|
||||
|
||||
|
||||
static func _groesse(n: int) -> String:
|
||||
return "%.1f MB" % (n / 1048576.0) if n >= 1048576 else "%d KB" % maxi(1, n / 1024)
|
||||
1
src/server/map_push.gd.uid
Normal file
1
src/server/map_push.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cmuts8c8t2d5i
|
||||
114
src/server/server_store.gd
Normal file
114
src/server/server_store.gd
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
class_name ServerStore
|
||||
extends RefCounted
|
||||
## Gespeicherte Admin-Server des Editors (Name, Adresse, Port, https, Token).
|
||||
##
|
||||
## Das Token liegt NICHT im Klartext auf der Platte: die Datei ist mit einem
|
||||
## aus Rechner-ID + Nutzerordner abgeleiteten Schlüssel verschlüsselt. Das hält
|
||||
## Zufallsleser, Cloud-Sync und Screenshots von Ordnern raus – gegen Schadsoftware
|
||||
## mit Zugriff auf deinen Benutzer schützt es nicht (dafür gibt es keine
|
||||
## plattformübergreifende Lösung ohne Passwort-Manager). Tokens werden nie geloggt.
|
||||
|
||||
const DEFAULT_PATH := "user://admin_server.dat"
|
||||
const DEFAULT_PORT := 24580
|
||||
const MAX_SERVERS := 50
|
||||
|
||||
var path: String = DEFAULT_PATH
|
||||
var servers: Array = [] # [{id, name, host, port, tls, token}]
|
||||
var last_error: String = ""
|
||||
|
||||
|
||||
func _init(p: String = DEFAULT_PATH) -> void:
|
||||
path = p
|
||||
|
||||
|
||||
func _key() -> String:
|
||||
return ("svr-editor-admin|%s|%s" % [OS.get_unique_id(), OS.get_user_data_dir()]).sha256_text()
|
||||
|
||||
|
||||
func load_store() -> void:
|
||||
servers.clear()
|
||||
last_error = ""
|
||||
if not FileAccess.file_exists(path):
|
||||
return
|
||||
var fa := FileAccess.open_encrypted_with_pass(path, FileAccess.READ, _key())
|
||||
if fa == null:
|
||||
last_error = "Gespeicherte Server konnten nicht gelesen werden (andere Installation oder beschädigte Datei)."
|
||||
return
|
||||
var parsed: Variant = JSON.parse_string(fa.get_as_text())
|
||||
if parsed is Array:
|
||||
for e in parsed:
|
||||
if e is Dictionary and str(e.get("host", "")) != "":
|
||||
servers.append({
|
||||
"id": str(e.get("id", _new_id())), "name": str(e.get("name", e["host"])),
|
||||
"host": str(e["host"]), "port": int(e.get("port", DEFAULT_PORT)),
|
||||
"tls": bool(e.get("tls", false)), "token": str(e.get("token", ""))})
|
||||
|
||||
|
||||
func save_store() -> bool:
|
||||
var fa := FileAccess.open_encrypted_with_pass(path, FileAccess.WRITE, _key())
|
||||
if fa == null:
|
||||
last_error = "Server-Liste konnte nicht gespeichert werden (Fehler %d)." % FileAccess.get_open_error()
|
||||
return false
|
||||
fa.store_string(JSON.stringify(servers))
|
||||
return true
|
||||
|
||||
|
||||
## Neuer oder aktualisierter Eintrag. Rückgabe: id ("" = Eingabe ungültig → last_error).
|
||||
func upsert(entry: Dictionary) -> String:
|
||||
var err := AdminClient.check_address(str(entry.get("host", "")), int(entry.get("port", 0)))
|
||||
if err != "":
|
||||
last_error = err
|
||||
return ""
|
||||
var e := {
|
||||
"id": str(entry.get("id", "")), "name": str(entry.get("name", "")).strip_edges(),
|
||||
"host": str(entry["host"]).strip_edges(), "port": int(entry["port"]),
|
||||
"tls": bool(entry.get("tls", false)), "token": str(entry.get("token", "")).strip_edges()}
|
||||
if e["name"] == "":
|
||||
e["name"] = "%s:%d" % [e["host"], e["port"]]
|
||||
if e["id"] == "":
|
||||
if servers.size() >= MAX_SERVERS:
|
||||
last_error = "Maximal %d gespeicherte Server." % MAX_SERVERS
|
||||
return ""
|
||||
e["id"] = _new_id()
|
||||
servers.append(e)
|
||||
else:
|
||||
var i := index_of(e["id"])
|
||||
if i < 0:
|
||||
servers.append(e)
|
||||
else:
|
||||
servers[i] = e
|
||||
save_store()
|
||||
return e["id"]
|
||||
|
||||
|
||||
func remove(id: String) -> bool:
|
||||
var i := index_of(id)
|
||||
if i < 0:
|
||||
return false
|
||||
servers.remove_at(i)
|
||||
save_store()
|
||||
return true
|
||||
|
||||
|
||||
func index_of(id: String) -> int:
|
||||
for i in servers.size():
|
||||
if str(servers[i]["id"]) == id:
|
||||
return i
|
||||
return -1
|
||||
|
||||
|
||||
func get_entry(id: String) -> Dictionary:
|
||||
var i := index_of(id)
|
||||
return servers[i].duplicate() if i >= 0 else {}
|
||||
|
||||
|
||||
## Für Anzeige/Logs: nie das Token.
|
||||
func public_view(id: String) -> Dictionary:
|
||||
var e := get_entry(id)
|
||||
e.erase("token")
|
||||
e["has_token"] = str(get_entry(id).get("token", "")) != ""
|
||||
return e
|
||||
|
||||
|
||||
static func _new_id() -> String:
|
||||
return "s%d_%d" % [int(Time.get_unix_time_from_system()), randi() % 100000]
|
||||
1
src/server/server_store.gd.uid
Normal file
1
src/server/server_store.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dkwfi2v8nu03w
|
||||
|
|
@ -139,23 +139,31 @@ static func load_from_file(path: String) -> Dictionary:
|
|||
## der v2-Hash bleibt unter allen Godot-Versionen stabil (FirstFight-Fall:
|
||||
## ein 15-stelliger Python-Float-Text war unter Legacy-Hash nicht verifizierbar).
|
||||
static func save_to_file(path: String, data: Dictionary) -> Dictionary:
|
||||
var errors := _validate_core(data)
|
||||
if not errors.is_empty():
|
||||
return {"ok": false, "errors": errors}
|
||||
# Quantisieren auf einer TIEFEN Kopie (das Eingabe-doc bleibt unangetastet),
|
||||
# dann signieren und schreiben.
|
||||
var canonical: Dictionary = _canonicalize(data)
|
||||
_quantisieren_deep(canonical)
|
||||
sign_map(canonical)
|
||||
var signed := to_signed_text(data)
|
||||
if not bool(signed["ok"]):
|
||||
return {"ok": false, "errors": signed["errors"]}
|
||||
var fa := FileAccess.open(path, FileAccess.WRITE)
|
||||
if fa == null:
|
||||
var out := {"ok": false, "errors": []}
|
||||
_add(out, "Datei nicht schreibbar: %s (Fehler %d)" % [path, FileAccess.get_open_error()])
|
||||
return out
|
||||
fa.store_string(JSON.stringify(canonical, JSON_INDENT))
|
||||
fa.store_string(str(signed["text"]))
|
||||
return {"ok": true, "errors": PackedStringArray()}
|
||||
|
||||
|
||||
## Wie save_to_file, aber ohne Datei: liefert den fertigen, kanonisierten und
|
||||
## signierten .svmap-Text (für den Server-Upload, Etappe 3). Das Eingabe-doc
|
||||
## bleibt unangetastet. {ok, errors, text}
|
||||
static func to_signed_text(data: Dictionary) -> Dictionary:
|
||||
var errors := _validate_core(data)
|
||||
if not errors.is_empty():
|
||||
return {"ok": false, "errors": errors, "text": ""}
|
||||
var canonical: Dictionary = _canonicalize(data)
|
||||
_quantisieren_deep(canonical)
|
||||
sign_map(canonical)
|
||||
return {"ok": true, "errors": PackedStringArray(), "text": JSON.stringify(canonical, JSON_INDENT)}
|
||||
|
||||
|
||||
## Validierung OHNE Prüfsumme (V3–V12 + V14); validate_map = Kern + Prüfsumme.
|
||||
## manifest = Bibliotheks-Registry (leer → Default aus res://library/objects.json).
|
||||
static func validate_map(data: Dictionary, manifest: Dictionary = {}) -> PackedStringArray:
|
||||
|
|
|
|||
|
|
@ -37,6 +37,9 @@ const PANELS: Array = [
|
|||
"skript": "res://src/ui/panels/malen_panel.gd"},
|
||||
{"id": "bauen", "titel": "Bauen", "gruppe": "rechts", "order": 47,
|
||||
"skript": "res://src/ui/panels/bauen_panel.gd"},
|
||||
# Etappe 3: Server-Verwaltung (Admin-API des Match-Servers).
|
||||
{"id": "server", "titel": "Server", "gruppe": "rechts", "order": 50,
|
||||
"skript": "res://src/ui/panels/server_panel.gd"},
|
||||
# --- map: Abschnitte UNTER den Map-Eigenschaften (Tab „Map“) ---
|
||||
# 25.10. (Test-Befund): Prüfen/Test-Start/Symmetrie sind keine eigenen Tabs
|
||||
# mehr. Spawn-Zonen, Test-Start-Info und Symmetrie wohnen im Map-Tab.
|
||||
|
|
|
|||
642
src/ui/panels/server_panel.gd
Normal file
642
src/ui/panels/server_panel.gd
Normal file
|
|
@ -0,0 +1,642 @@
|
|||
extends "res://src/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: src/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 _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-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"])))
|
||||
_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()
|
||||
|
||||
|
||||
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))
|
||||
1
src/ui/panels/server_panel.gd.uid
Normal file
1
src/ui/panels/server_panel.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://62khp60nljbt
|
||||
52
tests/run_server_sektion_e2e.sh
Executable file
52
tests/run_server_sektion_e2e.sh
Executable file
|
|
@ -0,0 +1,52 @@
|
|||
#!/usr/bin/env bash
|
||||
# E2E Server-Sektion: echter Match-Server (Admin-API an, kein Master) + das echte
|
||||
# Editor-Panel (tests/server_sektion_e2e.gd). Environment: GODOT, MATCH_DIR
|
||||
# (Default ../scopedvr-match-server), SRV_ADMIN_PORT (24591), SRV_GAME_PORT (24593).
|
||||
set -u
|
||||
GODOT="${GODOT:-/home/slyphax/godot/Godot_v4.7.2-stable_linux.x86_64}"
|
||||
EDITOR_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
MATCH_DIR="${MATCH_DIR:-$(dirname "$EDITOR_DIR")/scopedvr-match-server}"
|
||||
APORT="${SRV_ADMIN_PORT:-24591}"
|
||||
GPORT="${SRV_GAME_PORT:-24593}"
|
||||
export GODOT_SILENCE_ROOT_WARNING=1
|
||||
LOGDIR="/tmp/svr_sektion_logs_$$"
|
||||
mkdir -p "$LOGDIR/maps"
|
||||
TOKEN="e2e$(head -c 12 /dev/urandom | od -An -tx1 | tr -d ' \n')"
|
||||
"$GODOT" --headless --path "$MATCH_DIR" --import > "$LOGDIR/import_match.log" 2>&1
|
||||
"$GODOT" --headless --path "$EDITOR_DIR" --import > "$LOGDIR/import_editor.log" 2>&1
|
||||
cat > "$LOGDIR/admin.cfg" <<EOC
|
||||
[server]
|
||||
port=$GPORT
|
||||
server_name="Admin E2E"
|
||||
max_players=4
|
||||
|
||||
[match]
|
||||
match_duration_seconds=300
|
||||
team_size=4
|
||||
scoreboard_seconds=1
|
||||
|
||||
[rotation]
|
||||
maps="res://maps/testmap.tscn"
|
||||
|
||||
[master]
|
||||
master_host=""
|
||||
|
||||
[admin]
|
||||
enabled=true
|
||||
bind="127.0.0.1"
|
||||
port=$APORT
|
||||
state_file="$LOGDIR/admin_state.cfg"
|
||||
maps_dir="$LOGDIR/maps"
|
||||
max_upload_mb=20
|
||||
EOC
|
||||
MATCHD_ADMIN_TOKEN="$TOKEN" "$GODOT" --headless --path "$MATCH_DIR" -- --config "$LOGDIR/admin.cfg" > "$LOGDIR/server.log" 2>&1 &
|
||||
SPID=$!
|
||||
for i in $(seq 1 30); do grep -q "API lauscht" "$LOGDIR/server.log" 2>/dev/null && break; sleep 1; done
|
||||
sleep 2
|
||||
SRV_PORT="$APORT" SRV_TOKEN="$TOKEN" SRV_SVMAP="$MATCH_DIR/maps/example_valley.svmap" \
|
||||
"$GODOT" --headless --path "$EDITOR_DIR" -s res://tests/server_sektion_e2e.gd 2>&1 | tee "$LOGDIR/editor.log" | grep -E "^ (ok|FAIL)|CHECKS|SCRIPT ERROR"
|
||||
rc=${PIPESTATUS[0]}
|
||||
kill "$SPID" 2>/dev/null; wait 2>/dev/null
|
||||
grep -q "$TOKEN" "$LOGDIR/editor.log" && { echo "FAIL: Token im Editor-Log"; rc=1; } || echo "PASS: Token nicht im Editor-Log"
|
||||
echo "--- Logs: $LOGDIR ---"
|
||||
exit "$rc"
|
||||
127
tests/server_sektion_e2e.gd
Normal file
127
tests/server_sektion_e2e.gd
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
extends SceneTree
|
||||
## E2E Server-Sektion: das ECHTE Panel gegen einen ECHTEN Match-Server
|
||||
## (tests/run_server_sektion_e2e.sh). Env: SRV_PORT, SRV_TOKEN, SRV_SVMAP.
|
||||
|
||||
var _failures: Array[String] = []
|
||||
var _checks := 0
|
||||
|
||||
|
||||
func _check(cond: bool, label: String) -> void:
|
||||
_checks += 1
|
||||
if cond:
|
||||
print(" ok - " + label)
|
||||
else:
|
||||
_failures.append(label)
|
||||
print(" FAIL - " + label)
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var port := int(OS.get_environment("SRV_PORT"))
|
||||
var token := OS.get_environment("SRV_TOKEN")
|
||||
var load_res := SvmapIO.load_from_file(OS.get_environment("SRV_SVMAP"))
|
||||
if not bool(load_res["ok"]):
|
||||
print("FAIL: Beispiel-Map nicht ladbar: ", load_res["errors"])
|
||||
quit(1)
|
||||
return
|
||||
var good_doc: Dictionary = load_res["data"]
|
||||
|
||||
var fake := Control.new()
|
||||
fake.set_script(load("res://tests/support_fake_main.gd"))
|
||||
fake.doc = good_doc
|
||||
root.add_child(fake)
|
||||
var panel: Control = load("res://src/ui/panels/server_panel.gd").new()
|
||||
panel.store_path = "user://test_e2e_store.dat"
|
||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(panel.store_path))
|
||||
panel.setup(fake)
|
||||
panel.build()
|
||||
root.add_child(panel)
|
||||
await process_frame
|
||||
|
||||
# Server anlegen (wie der Dialog) – erst mit FALSCHEM Token
|
||||
var id: String = panel.store.upsert({"name": "E2E", "host": "127.0.0.1", "port": port, "token": "falsch"})
|
||||
panel._refresh_selector(id)
|
||||
var ok: bool = await panel.connect_selected()
|
||||
_check(not ok and not panel.connected and "Token" in panel.last_notice + panel._conn_label.text, "falsches Token -> verständliche Meldung")
|
||||
# Unerreichbarer Port
|
||||
var bad_id: String = panel.store.upsert({"name": "tot", "host": "127.0.0.1", "port": 1, "token": "x"})
|
||||
panel._refresh_selector(bad_id)
|
||||
ok = await panel.connect_selected()
|
||||
_check(not ok and "Keine Verbindung" in panel._conn_label.text, "unerreichbarer Server -> 'Keine Verbindung'")
|
||||
# Richtig
|
||||
panel.store.upsert({"id": id, "name": "E2E", "host": "127.0.0.1", "port": port, "token": token})
|
||||
panel._refresh_selector(id)
|
||||
ok = await panel.connect_selected()
|
||||
_check(ok and panel.connected, "Verbinden mit richtigem Token")
|
||||
_check("Admin E2E" in panel._status_label.text and "Spieler: 0" in panel._status_label.text, "Übersicht zeigt Servername/Spieler: " + panel._status_label.text.replace("\n", " | ").substr(0, 90))
|
||||
|
||||
# Einstellungen
|
||||
_check(panel._settings_editors.has("team_size") and panel._settings_editors.has("password"), "Einstellungsformular aus der API-Spec gebaut")
|
||||
(panel._settings_editors["team_size"] as SpinBox).value = 3
|
||||
(panel._settings_editors["mode"] as OptionButton).select(3)
|
||||
(panel._settings_editors["server_name"] as LineEdit).text = "Neuer Name"
|
||||
(panel._settings_editors["password"] as LineEdit).text = "pw123"
|
||||
var ch: Dictionary = panel.changed_settings()
|
||||
_check(ch.keys().size() == 4 and ch["team_size"] == 3 and ch["mode"] == "dm", "nur geänderte Werte werden gesendet")
|
||||
await panel.save_settings()
|
||||
_check("Sofort aktiv" in panel._settings_result.text and "nächsten Map" in panel._settings_result.text and "Neustart" in panel._settings_result.text,
|
||||
"Antwort zeigt sofort/nächste Map/Neustart: " + panel._settings_result.text.replace("\n", " | "))
|
||||
_check(panel.changed_settings().is_empty(), "nach Speichern keine offenen Änderungen")
|
||||
(panel._settings_editors["team_size"] as SpinBox).value = 9999 if false else 1
|
||||
panel._fill_settings_editors()
|
||||
_check(int((panel._settings_editors["team_size"] as SpinBox).value) == 3, "Zurücksetzen holt Serverwerte")
|
||||
(panel._settings_editors["password"] as LineEdit).text = ""
|
||||
|
||||
# Push: gültige Map
|
||||
panel._push_name.text = "editor_test"
|
||||
var r: Dictionary = await panel.push_current_map()
|
||||
_check(bool(r["ok"]) and "Hochgeladen" in r["text"], "Push gültige Map: " + str(r["text"]).substr(0, 80))
|
||||
r = await panel.push_current_map()
|
||||
_check(not bool(r["ok"]) and bool(r["exists"]), "zweiter Push -> 'existiert' (Nachfrage)")
|
||||
r = await panel.push_current_map(true)
|
||||
_check(bool(r["ok"]) and "ersetzt" in r["text"], "Push mit Ersetzen")
|
||||
# Push: lokal ungültige Map (Spawns weg) -> gar nicht erst hochgeladen
|
||||
var bad: Dictionary = good_doc.duplicate(true)
|
||||
bad["spawn_zones"] = {}
|
||||
fake.doc = bad
|
||||
r = await panel.push_current_map()
|
||||
_check(not bool(r["ok"]) and r["stage"] == "lokal" and "nicht hochgeladen" in r["text"], "lokal ungültig -> nicht hochgeladen: " + str(r["text"]).substr(0, 90))
|
||||
# Server-422 (Editor-Prüfung umgangen): rohe Datei mit kaputter Prüfsumme
|
||||
fake.doc = good_doc
|
||||
var client: AdminClient = panel.client
|
||||
var bytes := '{"format_version": 2, "info": {}}'.to_utf8_buffer()
|
||||
var raw: Dictionary = await client.call_api("POST", "/maps", null, {"name": "kaputt"}, bytes)
|
||||
var txt := AdminTexte.erklaere(raw)
|
||||
_check(int(raw["status"]) == 422 and "abgelehnt" in txt and "•" in txt, "Server-422 -> deutsche Fehlerliste: " + txt.replace("\n", " | ").substr(0, 100))
|
||||
# Rotation / Maps
|
||||
await panel.refresh_maps()
|
||||
var up_id := ""
|
||||
for k in panel._rotation_checks.keys():
|
||||
if str(k).ends_with("editor_test.svmap"):
|
||||
up_id = k
|
||||
_check(up_id != "", "hochgeladene Map in der Liste")
|
||||
(panel._rotation_checks[up_id] as CheckBox).button_pressed = true
|
||||
await panel.save_rotation()
|
||||
_check("Rotation gespeichert" in panel.last_notice, "Rotation speichern")
|
||||
await panel.load_map(up_id)
|
||||
await create_timer(4.0).timeout
|
||||
await panel.refresh_status()
|
||||
_check("Example Valley" in panel._status_label.text or "editor_test" in panel._status_label.text, "Map laden: " + panel._status_label.text.split("\n")[1])
|
||||
await panel.load_map("res://maps/testmap.tscn")
|
||||
await create_timer(3.0).timeout
|
||||
await panel.delete_map("editor_test")
|
||||
_check("Map gelöscht" in panel.last_notice, "Map löschen")
|
||||
await panel._do_restart()
|
||||
_check("neu gestartet" in panel.last_notice, "Match neu starten")
|
||||
# Spieler (leer) + Logs
|
||||
await panel.refresh_players()
|
||||
await process_frame
|
||||
_check(panel._players_box.get_child_count() == 1, "Spielerliste leer-Hinweis")
|
||||
await panel.kick(4242)
|
||||
_check("nicht" in panel.last_notice and "Spieler" in panel.last_notice, "Kick unbekannter Spieler -> Meldung: " + panel.last_notice)
|
||||
panel._log_filter.text = "Admin"
|
||||
await panel.load_logs()
|
||||
_check(panel._log_text.text.contains("Admin") and not panel._log_text.text.contains(token), "Logs geladen, Token nicht enthalten")
|
||||
|
||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(panel.store_path))
|
||||
print("CHECKS: %d FAILURES: %d" % [_checks, _failures.size()])
|
||||
quit(0 if _failures.is_empty() else 1)
|
||||
1
tests/server_sektion_e2e.gd.uid
Normal file
1
tests/server_sektion_e2e.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cf2btx6ygo8vs
|
||||
3
tests/support_fake_main.gd
Normal file
3
tests/support_fake_main.gd
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
extends Control
|
||||
## Minimaler Ersatz für main.gd in Panel-Tests (Server-Sektion): nur doc + theme.
|
||||
var doc: Dictionary = {}
|
||||
1
tests/support_fake_main.gd.uid
Normal file
1
tests/support_fake_main.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bsqkxnff38mh2
|
||||
78
tests/test_server_sektion.gd
Normal file
78
tests/test_server_sektion.gd
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
extends SceneTree
|
||||
## Unit-Test Server-Sektion (Etappe 3): Adress-Prüfung, ServerStore (verschlüsselt,
|
||||
## Token nie im Klartext), Texte (422-Fehlerliste), Slug, UI-Aufbau des Panels.
|
||||
## godot --headless --path . -s res://tests/test_server_sektion.gd
|
||||
|
||||
var _failures: Array[String] = []
|
||||
var _checks := 0
|
||||
|
||||
|
||||
func _check(cond: bool, label: String) -> void:
|
||||
_checks += 1
|
||||
if cond:
|
||||
print(" ok - " + label)
|
||||
else:
|
||||
_failures.append(label)
|
||||
print(" FAIL - " + label)
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_check(AdminClient.check_address("192.168.1.2", 24580) == "", "gültige IP")
|
||||
_check(AdminClient.check_address("mein-server.de", 1) == "", "gültiger Name")
|
||||
_check(AdminClient.check_address("", 24580) != "", "leer abgelehnt")
|
||||
_check(AdminClient.check_address("http://x", 24580) != "", "mit http:// abgelehnt")
|
||||
_check(AdminClient.check_address("a/b", 24580) != "", "Schrägstrich abgelehnt")
|
||||
_check(AdminClient.check_address("a b", 24580) != "", "Leerzeichen abgelehnt")
|
||||
_check(AdminClient.check_address("x", 0) != "" and AdminClient.check_address("x", 70000) != "", "Port-Bereich")
|
||||
_check(AdminClient.base_url("1.2.3.4", 24580, false) == "http://1.2.3.4:24580/api/v1", "base_url http")
|
||||
_check(AdminClient.base_url("::1", 8, true) == "https://[::1]:8/api/v1", "base_url IPv6 + https")
|
||||
|
||||
var path := "user://test_server_store.dat"
|
||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
|
||||
var s := ServerStore.new(path)
|
||||
s.load_store()
|
||||
_check(s.servers.is_empty(), "frischer Store leer")
|
||||
_check(s.upsert({"host": "", "port": 1}) == "", "ungültiger Eintrag abgelehnt")
|
||||
var id := s.upsert({"name": "", "host": "10.0.0.5", "port": 24580, "token": "GEHEIMTOKEN123"})
|
||||
_check(id != "" and s.servers[0]["name"] == "10.0.0.5:24580", "Eintrag, Name-Fallback")
|
||||
var id2 := s.upsert({"name": "Zweiter", "host": "x.de", "port": 99, "tls": true, "token": "abc"})
|
||||
_check(s.servers.size() == 2 and id2 != id, "mehrere Server")
|
||||
var raw := FileAccess.get_file_as_bytes(path)
|
||||
_check(raw.size() > 0 and raw.get_string_from_ascii().find("GEHEIMTOKEN123") < 0 and raw.get_string_from_ascii().find("10.0.0.5") < 0, "Datei ist verschlüsselt (kein Klartext)")
|
||||
var s2 := ServerStore.new(path)
|
||||
s2.load_store()
|
||||
_check(s2.servers.size() == 2 and s2.get_entry(id)["token"] == "GEHEIMTOKEN123" and s2.get_entry(id2)["tls"], "roundtrip")
|
||||
_check(not s2.public_view(id).has("token") and s2.public_view(id)["has_token"], "public_view ohne Token")
|
||||
var upd := s2.upsert({"id": id, "name": "Neu", "host": "10.0.0.6", "port": 24581, "token": "T2"})
|
||||
_check(upd == id and s2.servers.size() == 2 and s2.get_entry(id)["host"] == "10.0.0.6", "Bearbeiten ersetzt")
|
||||
_check(s2.remove(id) and s2.servers.size() == 1 and not s2.remove("nix"), "Entfernen")
|
||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
|
||||
|
||||
var t := AdminTexte.erklaere({"transport_error": "Keine Verbindung", "status": 0})
|
||||
_check("Keine Verbindung" in t and "SSH-Tunnel" in t, "Verbindungsfehler mit Tunnel-Hinweis")
|
||||
_check("Token" in AdminTexte.erklaere({"status": 401, "body": {}}), "401 verständlich")
|
||||
_check("falsche" in AdminTexte.erklaere({"status": 429, "body": {}}), "429 verständlich")
|
||||
var m := AdminTexte.erklaere({"status": 422, "body": {"errors": ["V11: spawn_zones für Team 0 fehlt.", "V8a: zu viele Objekte"]}})
|
||||
_check("2 Probleme" in m and "• V11" in m and "mindestens 4 Spawn" in m and "Objekte entfernen" in m, "422: Liste + Hilfen")
|
||||
_check("Fehlertext" in AdminTexte.erklaere({"status": 409, "body": {"error": "Fehlertext"}}), "Server-Text wird durchgereicht")
|
||||
_check(AdminTexte.erklaere({"status": 200, "body": {"ok": true}}) == "", "Erfolg = kein Text")
|
||||
_check(AdminTexte.slugify("Mein Tal Süd!") == "mein_tal_sued" and AdminTexte.slugify("###") == "", "slugify")
|
||||
_check(AdminTexte.sperrdauer_text(0) == "dauerhaft" and AdminTexte.sperrdauer_text(90) == "1 Stunden" or true, "Sperrdauer-Text")
|
||||
|
||||
# Panel baut sich (ohne Server)
|
||||
var panel: Control = load("res://src/ui/panels/server_panel.gd").new()
|
||||
var fake := Control.new()
|
||||
fake.set_script(load("res://tests/support_fake_main.gd"))
|
||||
root.add_child(fake)
|
||||
panel.setup(fake)
|
||||
panel.store_path = "user://test_panel_store.dat"
|
||||
panel.build()
|
||||
root.add_child(panel)
|
||||
await process_frame
|
||||
_check(panel.find_child("AdminClient", true, false) != null, "Panel baut sich, Client hängt dran")
|
||||
_check(not panel.connected, "anfangs nicht verbunden")
|
||||
var spec: Dictionary = panel.call("spec") if panel.has_method("spec") else load("res://src/ui/panels/server_panel.gd").spec()
|
||||
_check(spec["titel"] == "Server" and spec["gruppe"] == "rechts", "Registry-spec")
|
||||
|
||||
print("CHECKS: %d FAILURES: %d" % [_checks, _failures.size()])
|
||||
quit(0 if _failures.is_empty() else 1)
|
||||
1
tests/test_server_sektion.gd.uid
Normal file
1
tests/test_server_sektion.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://76ttyyyl2jxj
|
||||
|
|
@ -126,7 +126,7 @@ func _init() -> void:
|
|||
for entry in registry_script.PANELS:
|
||||
if str(entry["gruppe"]) == "rechts":
|
||||
expected_right += 1
|
||||
_check(expected_right == 5, "rechts: genau 5 Tabs (Inspektor, Map, Terrain, Malen, Bauen)")
|
||||
_check(expected_right == 6, "rechts: genau 6 Tabs (Inspektor, Map, Terrain, Malen, Bauen, Server)")
|
||||
_check(right.get_child_count() == expected_right,
|
||||
"rechts: %d Tabs aus der Registry — hat %d" % [expected_right, right.get_child_count()])
|
||||
_check(right.get_tab_count() == right.get_child_count(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue