Add player names and account deletion
This commit is contained in:
1 parent
32594c7f81
commit
1b5b8dc0a4
10 files changed
+299
-11
No files matched your search
@@ -0,0 +1,32 @@
|
||||
# Datenschutz- und Rechtscheckliste
|
||||
|
||||
Stand: Prototyp 0.22. Diese Arbeitsunterlage ersetzt keine individuelle Rechtsberatung.
|
||||
|
||||
## Vor Veröffentlichung zwingend ergänzen
|
||||
|
||||
- vollständiger bürgerlicher Name des Diensteanbieters
|
||||
- ladungsfähige Anschrift
|
||||
- erreichbare Kontakt-E-Mail-Adresse
|
||||
- separate Support-Adresse, falls gewünscht
|
||||
- Prüfung, ob weitere Angaben wie Umsatzsteuer-ID oder Registerangaben bestehen
|
||||
|
||||
Die lokalen Webseitenentwürfe `impressum.html`, `privacy.html`, `support.html` und `delete-account.html` enthalten dafür deutlich markierte Platzhalter und dürfen so noch nicht veröffentlicht werden.
|
||||
|
||||
## Bereits technisch vorbereitet
|
||||
|
||||
- Datenschutz-Link direkt im Pilotenprofil
|
||||
- serverseitig geprüfte öffentliche Spielernamen
|
||||
- zweistufige, ausdrückliche Kontolöschung im Spiel
|
||||
- neues Gastprofil nach Löschung der bisherigen Online- und lokalen Profildaten
|
||||
- eigenes öffentliches Webziel für eine spätere Löschanfrage außerhalb der App
|
||||
- Werbung bleibt bis zu Einwilligung, Store-Deklarationen und echten Anzeigenkennungen im Testbetrieb
|
||||
|
||||
## Vor Store-Einreichung prüfen
|
||||
|
||||
- Datenschutzerklärung und Impressum ohne Platzhalter veröffentlichen
|
||||
- tatsächlich verarbeitete Daten in App Store Connect und Google Play deklarieren
|
||||
- Kontolöschung auf echtem iOS- und Android-Gerät testen
|
||||
- externen Löschweg in der Google-Play-Konsole hinterlegen
|
||||
- Apple-/Game-Center- und Google-Play-Games-Verknüpfung samt Wiederherstellung testen
|
||||
- Einwilligungsdialog, Widerruf der Werbeeinwilligung und `app-ads.txt` prüfen
|
||||
- Aufbewahrungs- und Löschfristen mit Server-Backups praktisch verifizieren
|
||||
+4
-2
@@ -1,9 +1,11 @@
|
||||
# Void Bastion Online 0.21
|
||||
# Void Bastion Online 0.22
|
||||
|
||||
## Fertiger Stand
|
||||
|
||||
Das Spiel nutzt den offiziellen Nakama-Godot-Client 3.4.0. Beim normalen Start wird automatisch ein app-eigenes Gastkonto angemeldet. Das lokale Profil bleibt auch offline nutzbar; neue Bestwerte werden gespeichert und bei der nächsten erfolgreichen Verbindung nachgereicht.
|
||||
|
||||
Spielernamen werden lokal und serverseitig auf 3 bis 20 erlaubte Zeichen geprüft. Reservierte Bezeichnungen wie `ADMIN`, `GERGAMES`, `MODERATOR`, `SUPPORT` und `SYSTEM` sind gesperrt. Im Pilotenprofil kann das aktuell angemeldete Online-Konto nach einer zweiten ausdrücklichen Bestätigung gelöscht werden; anschließend verwirft das Spiel die lokale Identität und erstellt ein neues Gastprofil.
|
||||
|
||||
Der Unraid-Stack besteht aus:
|
||||
|
||||
- Nakama 3.37.0 mit eigener JavaScript-Serverlogik
|
||||
@@ -43,4 +45,4 @@ Die öffentliche Freischaltung ist abgeschlossen: Der autoritative DNS-Eintrag `
|
||||
- echter Onlinetest im Heimnetz: `godot --headless --path . --script res://tests/online_integration_test.gd`
|
||||
- Produktionstest: `VOID_BASTION_TEST_HOST=api.void-bastion.gergames.de VOID_BASTION_TEST_SCHEME=https VOID_BASTION_TEST_PORT=443 godot --headless --path . --script res://tests/online_integration_test.gd`
|
||||
|
||||
Der Onlinetest prüft zwei Gastanmeldungen, RPC, Ergebnisannahme, Ranglistenabruf, zwei WebSocket-Verbindungen, schnelles Matchmaking, die Bereit-Lobby, autoritative Spielzustände sowie Erstellen, Verlassen und Wiederbeitreten eines privaten Raums per Code.
|
||||
Der Onlinetest prüft zwei Gastanmeldungen, Änderung und Ablehnung von Spielernamen, die Löschung eines eigens erzeugten Wegwerf-Testkontos, RPC, Ergebnisannahme, Ranglistenabruf, zwei WebSocket-Verbindungen, schnelles Matchmaking, die Bereit-Lobby, autoritative Spielzustände sowie Erstellen, Verlassen und Wiederbeitreten eines privaten Raums per Code.
|
||||
@@ -1,4 +1,4 @@
|
||||
# Void Bastion – spielbarer Prototyp 0.21
|
||||
# Void Bastion – spielbarer Prototyp 0.22
|
||||
|
||||
Ein plattformübergreifender 2D-Tower-Defense-Prototyp für Godot 4. Der aktuelle Stand ist für Windows, Android und später iOS ausgelegt.
|
||||
|
||||
@@ -38,6 +38,9 @@ Ein plattformübergreifender 2D-Tower-Defense-Prototyp für Godot 4. Der aktuell
|
||||
- dauerhaft gespeicherte Kernkristalle als spätere Freischaltwährung
|
||||
- dauerhaftes lokales Gastprofil mit stabiler Spieler-ID als Grundlage für Apple-, Google- und Windows-Anmeldung
|
||||
- eigene Profilansicht mit lokalen Missions- und Endlosbestwerten pro Karte
|
||||
- frei wählbarer Spielername mit identischer Prüfung im Spiel und auf dem Server
|
||||
- zweistufige Kontolöschung direkt im Pilotenprofil; danach wird ein neues lokales Gastprofil erstellt
|
||||
- direkter Datenschutz-Link im Pilotenprofil
|
||||
- wartende, lokal gespeicherte Highscore-Einträge für die spätere sichere Serverübertragung
|
||||
- eigener Multiplayerbereich mit schneller Spielersuche sowie privaten Koop-Räumen mit sechsstelligen Raumcodes
|
||||
- gemeinsame Online-Lobby mit Spielernamen, festen Verteidigungsseiten und manuellem Bereit-Status für beide Piloten
|
||||
@@ -104,7 +107,7 @@ Die öffentliche Spiel-API ist unter `api.void-bastion.gergames.de` erreichbar.
|
||||
|
||||
## Steuerung
|
||||
|
||||
> **Online-Stand 0.21:** Der offizielle Nakama-Godot-Client, automatische Gastanmeldung, 36 autoritative Ranglisten, geprüfte Ergebnisübermittlung, Echtzeit-Sockets, serverautoritativer Zwei-Spieler-Koop, private Räume per Raumcode, eine Bereit-Lobby und Wiederverbindung sind eingebaut. Die öffentliche API ist über `https://api.void-bastion.gergames.de` mit einem gültigen Let’s-Encrypt-Zertifikat freigeschaltet. Details stehen in `ONLINE_SETUP.md`.
|
||||
> **Online-Stand 0.22:** Der offizielle Nakama-Godot-Client, automatische Gastanmeldung, servergeprüfte Spielernamen, Kontolöschung im Spiel, 36 autoritative Ranglisten, geprüfte Ergebnisübermittlung, Echtzeit-Sockets, serverautoritativer Zwei-Spieler-Koop, private Räume per Raumcode, eine Bereit-Lobby und Wiederverbindung sind eingebaut. Die öffentliche API ist über `https://api.void-bastion.gergames.de` mit einem gültigen Let’s-Encrypt-Zertifikat freigeschaltet. Details stehen in `ONLINE_SETUP.md`.
|
||||
|
||||
- Turm unten auswählen und auf eine freie Plattformfläche klicken oder tippen.
|
||||
- Am PC folgt der Reichweitenkreis vor dem Bau dem Mauszeiger. Auf Mobilgeräten markiert der erste Tipp den Bauplatz samt Reichweite; der zweite Tipp auf dasselbe Feld bestätigt den Bau.
|
||||
|
||||
@@ -4,6 +4,9 @@ extends Node
|
||||
signal profile_changed
|
||||
|
||||
const ACCOUNT_PATH := "user://void_bastion_account.json"
|
||||
const MIN_DISPLAY_NAME_LENGTH := 3
|
||||
const MAX_DISPLAY_NAME_LENGTH := 20
|
||||
const RESERVED_DISPLAY_NAMES := ["ADMIN", "ADMINISTRATOR", "GERGAMES", "MODERATOR", "SUPPORT", "SYSTEM"]
|
||||
|
||||
var player_id := ""
|
||||
var display_name := ""
|
||||
@@ -100,6 +103,42 @@ func attach_online_identity(online_user_id: String, online_username: String) ->
|
||||
profile_changed.emit()
|
||||
|
||||
|
||||
func normalize_display_name(value: String) -> String:
|
||||
var normalized := " ".join(value.strip_edges().split(" ", false))
|
||||
return normalized.left(MAX_DISPLAY_NAME_LENGTH)
|
||||
|
||||
|
||||
func validate_display_name(value: String) -> String:
|
||||
var normalized := normalize_display_name(value)
|
||||
if normalized.length() < MIN_DISPLAY_NAME_LENGTH:
|
||||
return "Der Spielername muss mindestens %d Zeichen lang sein." % MIN_DISPLAY_NAME_LENGTH
|
||||
if normalized.length() > MAX_DISPLAY_NAME_LENGTH:
|
||||
return "Der Spielername darf höchstens %d Zeichen lang sein." % MAX_DISPLAY_NAME_LENGTH
|
||||
if normalized.to_upper() in RESERVED_DISPLAY_NAMES:
|
||||
return "Dieser Spielername ist reserviert."
|
||||
var allowed_characters := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 _-ÄÖÜäöüß"
|
||||
for character in normalized:
|
||||
if not allowed_characters.contains(character):
|
||||
return "Erlaubt sind Buchstaben, Zahlen, Leerzeichen, _ und -."
|
||||
return ""
|
||||
|
||||
|
||||
func set_display_name(value: String) -> bool:
|
||||
var normalized := normalize_display_name(value)
|
||||
if not validate_display_name(normalized).is_empty():
|
||||
return false
|
||||
display_name = normalized
|
||||
_save_profile()
|
||||
profile_changed.emit()
|
||||
return true
|
||||
|
||||
|
||||
func reset_after_account_deletion() -> void:
|
||||
_create_guest_profile()
|
||||
_save_profile()
|
||||
profile_changed.emit()
|
||||
|
||||
|
||||
func mark_submission_complete(record: Dictionary) -> void:
|
||||
for index in range(pending_submissions.size() - 1, -1, -1):
|
||||
var queued = pending_submissions[index]
|
||||
|
||||
@@ -142,6 +142,14 @@ var room_code_input_rect := Rect2(510, 340, 580, 90)
|
||||
var join_room_button := Rect2(625, 525, 350, 64)
|
||||
var coop_side_button := Rect2(920, 18, 140, 54)
|
||||
var room_code_input := ""
|
||||
var profile_name_input := ""
|
||||
var profile_message := ""
|
||||
var profile_delete_armed := false
|
||||
var profile_rename_button := Rect2(220, 305, 250, 48)
|
||||
var profile_privacy_button := Rect2(840, 685, 250, 48)
|
||||
var profile_delete_button := Rect2(1110, 685, 250, 48)
|
||||
var profile_name_input_rect := Rect2(460, 330, 680, 82)
|
||||
var profile_name_save_button := Rect2(625, 470, 350, 62)
|
||||
|
||||
@onready var ads_manager = $AdsManager
|
||||
@onready var account_manager = $AccountManager
|
||||
@@ -370,6 +378,21 @@ func _update_effects(delta: float) -> void:
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if game_mode == "profile_name_entry" and event is InputEventKey and event.pressed and not event.echo:
|
||||
if event.keycode == KEY_ESCAPE:
|
||||
_hide_virtual_keyboard()
|
||||
game_mode = "profile"
|
||||
elif event.keycode == KEY_BACKSPACE:
|
||||
profile_name_input = profile_name_input.left(maxi(0, profile_name_input.length() - 1))
|
||||
elif event.keycode == KEY_ENTER:
|
||||
_save_profile_name()
|
||||
elif event.unicode > 0 and profile_name_input.length() < AccountManager.MAX_DISPLAY_NAME_LENGTH:
|
||||
var character := char(event.unicode)
|
||||
var allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 _-ÄÖÜäöüß"
|
||||
if allowed.contains(character):
|
||||
profile_name_input += character
|
||||
queue_redraw()
|
||||
return
|
||||
if game_mode == "room_code_entry" and event is InputEventKey and event.pressed and not event.echo:
|
||||
if event.keycode == KEY_ESCAPE:
|
||||
_hide_virtual_keyboard()
|
||||
@@ -454,9 +477,32 @@ func _handle_pointer(position: Vector2, is_touch := false) -> void:
|
||||
return
|
||||
if game_mode == "profile":
|
||||
if profile_back_button.has_point(position):
|
||||
profile_delete_armed = false
|
||||
game_mode = "menu"
|
||||
elif profile_rename_button.has_point(position):
|
||||
_open_profile_name_entry()
|
||||
elif profile_privacy_button.has_point(position):
|
||||
OS.shell_open("https://void-bastion.gergames.de/privacy.html")
|
||||
elif profile_delete_button.has_point(position):
|
||||
if not online_manager.is_authenticated():
|
||||
profile_message = "Kontolöschung benötigt eine Verbindung zum GerGames-Server."
|
||||
elif not profile_delete_armed:
|
||||
profile_delete_armed = true
|
||||
profile_message = "Noch einmal bestätigen: Online-Konto, Ranglisten und lokale Werte werden gelöscht."
|
||||
else:
|
||||
_delete_profile_account()
|
||||
queue_redraw()
|
||||
return
|
||||
if game_mode == "profile_name_entry":
|
||||
if profile_back_button.has_point(position):
|
||||
_hide_virtual_keyboard()
|
||||
game_mode = "profile"
|
||||
elif profile_name_input_rect.has_point(position):
|
||||
_show_profile_name_keyboard()
|
||||
elif profile_name_save_button.has_point(position):
|
||||
_save_profile_name()
|
||||
queue_redraw()
|
||||
return
|
||||
if back_button.has_point(position):
|
||||
_return_to_menu()
|
||||
return
|
||||
@@ -672,6 +718,56 @@ func _hide_virtual_keyboard() -> void:
|
||||
DisplayServer.virtual_keyboard_hide()
|
||||
|
||||
|
||||
func _open_profile_name_entry() -> void:
|
||||
profile_name_input = account_manager.display_name
|
||||
profile_message = ""
|
||||
profile_delete_armed = false
|
||||
game_mode = "profile_name_entry"
|
||||
_show_profile_name_keyboard()
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _show_profile_name_keyboard() -> void:
|
||||
if DisplayServer.has_feature(DisplayServer.FEATURE_VIRTUAL_KEYBOARD):
|
||||
DisplayServer.virtual_keyboard_show(profile_name_input, profile_name_input_rect, DisplayServer.KEYBOARD_TYPE_DEFAULT, AccountManager.MAX_DISPLAY_NAME_LENGTH, profile_name_input.length(), profile_name_input.length())
|
||||
|
||||
|
||||
func _save_profile_name() -> void:
|
||||
var validation_error: String = account_manager.validate_display_name(profile_name_input)
|
||||
if not validation_error.is_empty():
|
||||
profile_message = validation_error
|
||||
queue_redraw()
|
||||
return
|
||||
profile_message = "Spielername wird gespeichert …"
|
||||
queue_redraw()
|
||||
var result: Dictionary = await online_manager.update_display_name(profile_name_input)
|
||||
if not bool(result.get("accepted", false)):
|
||||
profile_message = str(result.get("error", "Der Spielername konnte nicht gespeichert werden."))
|
||||
queue_redraw()
|
||||
return
|
||||
_hide_virtual_keyboard()
|
||||
profile_message = "Spielername gespeichert%s." % (" und online synchronisiert" if bool(result.get("online", false)) else "")
|
||||
game_mode = "profile"
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _delete_profile_account() -> void:
|
||||
profile_delete_armed = false
|
||||
profile_message = "Konto wird unwiderruflich gelöscht …"
|
||||
queue_redraw()
|
||||
var result: Dictionary = await online_manager.delete_online_account()
|
||||
if not bool(result.get("accepted", false)):
|
||||
profile_message = str(result.get("error", "Das Konto konnte nicht gelöscht werden."))
|
||||
queue_redraw()
|
||||
return
|
||||
best_wave = 0
|
||||
best_endless_wave = 0
|
||||
core_crystals = 0
|
||||
_save_progress()
|
||||
profile_message = "Online-Konto und lokale Profildaten gelöscht. Ein neues Gastprofil wurde erstellt."
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _start_game() -> void:
|
||||
coop_online_mode = false
|
||||
credits = int(_difficulty().start_credits)
|
||||
@@ -1178,13 +1274,15 @@ func _load_progress() -> void:
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if game_mode in ["menu", "profile", "multiplayer_menu", "room_code_entry", "matchmaking"]:
|
||||
if game_mode in ["menu", "profile", "profile_name_entry", "multiplayer_menu", "room_code_entry", "matchmaking"]:
|
||||
draw_texture_rect(MAP_TEXTURES[selected_map], Rect2(Vector2.ZERO, SCREEN), false, Color("f3f6fb"))
|
||||
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.01, 0.025, 0.055, 0.06))
|
||||
if game_mode == "menu":
|
||||
_draw_menu()
|
||||
elif game_mode == "profile":
|
||||
_draw_profile()
|
||||
elif game_mode == "profile_name_entry":
|
||||
_draw_profile_name_entry()
|
||||
elif game_mode == "multiplayer_menu":
|
||||
_draw_multiplayer_menu()
|
||||
elif game_mode == "room_code_entry":
|
||||
@@ -1619,6 +1717,7 @@ func _draw_profile() -> void:
|
||||
draw_string(font, Vector2(220, 220), account_manager.display_name, HORIZONTAL_ALIGNMENT_LEFT, 520, 30, Color("8ff4ff"))
|
||||
draw_string(font, Vector2(220, 258), account_manager.provider_label(), HORIZONTAL_ALIGNMENT_LEFT, 650, 16, Color("ffcf7a"))
|
||||
draw_string(font, Vector2(220, 292), "SPIELER-ID %s" % account_manager.short_id(), HORIZONTAL_ALIGNMENT_LEFT, 650, 14, Color("91a8bc"))
|
||||
_draw_button(profile_rename_button, "SPIELERNAME ÄNDERN", false, Color("24516a"))
|
||||
draw_string(font, Vector2(840, 215), "KONTO & CLOUD", HORIZONTAL_ALIGNMENT_LEFT, 470, 18, Color("8feeff"))
|
||||
var cloud_label := "GERGAMES ONLINE VERBUNDEN" if online_manager.is_authenticated() else "GERGAMES ONLINE OFFLINE"
|
||||
var cloud_color := Color("83ffc8") if online_manager.is_authenticated() else Color("ffcf7a")
|
||||
@@ -1639,7 +1738,25 @@ func _draw_profile() -> void:
|
||||
draw_string(font, Vector2(930, y), "WELLE %02d" % account_manager.get_map_best_wave(index, true), HORIZONTAL_ALIGNMENT_CENTER, 180, 16, Color("ffcf7a"))
|
||||
var score := maxi(account_manager.get_map_best_score(index, false), account_manager.get_map_best_score(index, true))
|
||||
draw_string(font, Vector2(1160, y), "%07d" % score, HORIZONTAL_ALIGNMENT_CENTER, 180, 16, Color("8fffea"))
|
||||
draw_string(font, Vector2(220, 710), "Das Gastprofil funktioniert bereits lokal. Plattform-Anmeldungen und globale Ranglisten werden nach Einrichtung der Store-Konten aktiviert.", HORIZONTAL_ALIGNMENT_CENTER, 1160, 14, Color("86c8d8"))
|
||||
if not profile_message.is_empty():
|
||||
draw_string(font, Vector2(220, 658), profile_message, HORIZONTAL_ALIGNMENT_CENTER, 1160, 14, Color("ffcf7a"))
|
||||
_draw_button(profile_privacy_button, "DATENSCHUTZ", false, Color("24475b"))
|
||||
_draw_button(profile_delete_button, "LÖSCHEN BESTÄTIGEN" if profile_delete_armed else "KONTO LÖSCHEN", profile_delete_armed, Color("6b2d3b"))
|
||||
|
||||
|
||||
func _draw_profile_name_entry() -> void:
|
||||
var font := ThemeDB.fallback_font
|
||||
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.005, 0.015, 0.035, 0.82))
|
||||
_draw_button(profile_back_button, "< ZURÜCK", false, Color("26394a"))
|
||||
draw_string(font, Vector2(0, 190), "SPIELERNAME", HORIZONTAL_ALIGNMENT_CENTER, 1600, 44, Color("c7f5ff"))
|
||||
draw_string(font, Vector2(0, 250), "Dieser Name ist für Mitspieler und in Ranglisten sichtbar.", HORIZONTAL_ALIGNMENT_CENTER, 1600, 18, Color("a9d8e5"))
|
||||
draw_rect(profile_name_input_rect, Color(0.012, 0.035, 0.065, 0.96), true)
|
||||
draw_rect(profile_name_input_rect, Color("65dfff"), false, 3.0)
|
||||
draw_string(font, profile_name_input_rect.position + Vector2(24, 53), profile_name_input, HORIZONTAL_ALIGNMENT_CENTER, profile_name_input_rect.size.x - 48, 26, Color.WHITE)
|
||||
draw_string(font, Vector2(0, 448), "3–20 Zeichen · Buchstaben, Zahlen, Leerzeichen, _ und -", HORIZONTAL_ALIGNMENT_CENTER, 1600, 15, Color("91a8bc"))
|
||||
_draw_button(profile_name_save_button, "SPIELERNAME SPEICHERN", false, Color("185f68"))
|
||||
if not profile_message.is_empty():
|
||||
draw_string(font, Vector2(300, 585), profile_message, HORIZONTAL_ALIGNMENT_CENTER, 1000, 16, Color("ffcf7a"))
|
||||
|
||||
|
||||
func _draw_overlay(title: String, subtitle: String) -> void:
|
||||
|
||||
+31
-1
@@ -129,7 +129,7 @@ func submit_run(record: Dictionary) -> bool:
|
||||
if not is_authenticated():
|
||||
return false
|
||||
var payload := record.duplicate(true)
|
||||
payload["client_version"] = "0.21"
|
||||
payload["client_version"] = "0.22"
|
||||
var result = await client.rpc_async(session, "submit_run", JSON.stringify(payload))
|
||||
var parsed := _parse_rpc_result(result)
|
||||
if parsed.is_empty() or not bool(parsed.get("accepted", false)):
|
||||
@@ -149,6 +149,36 @@ func flush_pending_scores() -> void:
|
||||
await submit_run(record)
|
||||
|
||||
|
||||
func update_display_name(value: String) -> Dictionary:
|
||||
if not account_manager:
|
||||
return {"accepted": false, "error": "Lokales Profil fehlt."}
|
||||
var validation_error := account_manager.validate_display_name(value)
|
||||
if not validation_error.is_empty():
|
||||
return {"accepted": false, "error": validation_error}
|
||||
var normalized := account_manager.normalize_display_name(value)
|
||||
if is_authenticated():
|
||||
var result = await client.rpc_async(session, "update_display_name", JSON.stringify({"display_name": normalized}))
|
||||
var parsed := _parse_rpc_result(result)
|
||||
if parsed.is_empty() or not bool(parsed.get("accepted", false)):
|
||||
return {"accepted": false, "error": str(parsed.get("error", "Der Name konnte online nicht gespeichert werden."))}
|
||||
normalized = str(parsed.get("display_name", normalized))
|
||||
account_manager.set_display_name(normalized)
|
||||
return {"accepted": true, "display_name": normalized, "online": is_authenticated()}
|
||||
|
||||
|
||||
func delete_online_account() -> Dictionary:
|
||||
if not is_authenticated():
|
||||
return {"accepted": false, "error": "Für die Kontolöschung ist eine Serververbindung erforderlich."}
|
||||
var result = await client.rpc_async(session, "delete_account", "{}")
|
||||
var parsed := _parse_rpc_result(result)
|
||||
if parsed.is_empty() or not bool(parsed.get("accepted", false)):
|
||||
return {"accepted": false, "error": str(parsed.get("error", "Das Online-Konto konnte nicht gelöscht werden."))}
|
||||
disconnect_from_server()
|
||||
if account_manager:
|
||||
account_manager.reset_after_account_deletion()
|
||||
return {"accepted": true}
|
||||
|
||||
|
||||
func load_leaderboard(map_index: int, difficulty_index: int, endless: bool, limit := 20) -> Array:
|
||||
if not is_authenticated():
|
||||
return []
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
var GAME_VERSION = "0.21";
|
||||
var GAME_VERSION = "0.22";
|
||||
var MAP_COUNT = 3;
|
||||
var DIFFICULTY_COUNT = 3;
|
||||
var VALID_MODES = ["mission", "endless", "coop_ai", "coop_online"];
|
||||
@@ -48,6 +48,33 @@ function rpcSubmitRun(ctx, logger, nk, payload) {
|
||||
return JSON.stringify({accepted:true,leaderboard_id:id,rank:record.rank,score:Number(record.score),wave:wave});
|
||||
}
|
||||
|
||||
function normalizeDisplayName(value) {
|
||||
return String(value || "").trim().replace(/\s+/g, " ").substring(0, 20);
|
||||
}
|
||||
function validateDisplayName(value) {
|
||||
var name = normalizeDisplayName(value);
|
||||
if (name.length < 3) return "name_too_short";
|
||||
if (name.length > 20) return "name_too_long";
|
||||
if (!/^[A-Za-z0-9 ÄÖÜäöüß_-]+$/.test(name)) return "invalid_characters";
|
||||
var reserved = ["ADMIN","ADMINISTRATOR","GERGAMES","MODERATOR","SUPPORT","SYSTEM"];
|
||||
if (reserved.indexOf(name.toUpperCase()) >= 0) return "reserved_name";
|
||||
return "";
|
||||
}
|
||||
function rpcUpdateDisplayName(ctx, logger, nk, payload) {
|
||||
if (!ctx.userId) throw new Error("authentication required");
|
||||
var request;
|
||||
try { request = JSON.parse(payload || "{}"); } catch (error) { return JSON.stringify({accepted:false,error:"invalid_json"}); }
|
||||
var name = normalizeDisplayName(request.display_name), errorCode = validateDisplayName(name);
|
||||
if (errorCode) return JSON.stringify({accepted:false,error:errorCode});
|
||||
nk.accountUpdateId(ctx.userId, null, name, null, null, null, null, null);
|
||||
return JSON.stringify({accepted:true,display_name:name});
|
||||
}
|
||||
function rpcDeleteAccount(ctx, logger, nk, payload) {
|
||||
if (!ctx.userId) throw new Error("authentication required");
|
||||
nk.accountDeleteId(ctx.userId, false);
|
||||
return JSON.stringify({accepted:true});
|
||||
}
|
||||
|
||||
function normalizeRoomCode(value) {
|
||||
return String(value || "").toUpperCase().replace(/[^A-Z0-9]/g, "").slice(0, 6);
|
||||
}
|
||||
@@ -232,7 +259,8 @@ function handleCommand(nk,dispatcher,state,message) {
|
||||
else if (message.opCode===OP_START_WAVE) changed=startWave(state);
|
||||
else if (message.opCode===OP_READY&&!state.started) {
|
||||
state.ready[message.sender.userId]=payload.ready!==false;
|
||||
var name=String(payload.name||message.sender.username||"PILOT").trim().substring(0,24);
|
||||
var requestedName=normalizeDisplayName(payload.name||message.sender.username||"PILOT");
|
||||
var name=validateDisplayName(requestedName) ? "PILOT" : requestedName;
|
||||
state.players[message.sender.userId]={name:name||"PILOT"};
|
||||
var ids=Object.keys(state.slots),allReady=ids.length===2&&Object.keys(state.presences).length===2;
|
||||
for (var i=0;i<ids.length;i++) if (!state.ready[ids[i]]||state.presences[ids[i]]===undefined) allReady=false;
|
||||
@@ -259,6 +287,8 @@ function InitModule(ctx,logger,nk,initializer) {
|
||||
for (var mode=0;mode<VALID_MODES.length;mode++) for (var map=0;map<MAP_COUNT;map++) for (var difficulty=0;difficulty<DIFFICULTY_COUNT;difficulty++) nk.leaderboardCreate(leaderboardId(map,difficulty,VALID_MODES[mode]),true,"desc","best",null,{game:"void_bastion"});
|
||||
initializer.registerRpc("server_info",rpcServerInfo);
|
||||
initializer.registerRpc("submit_run",rpcSubmitRun);
|
||||
initializer.registerRpc("update_display_name",rpcUpdateDisplayName);
|
||||
initializer.registerRpc("delete_account",rpcDeleteAccount);
|
||||
initializer.registerRpc("create_private_room",rpcCreatePrivateRoom);
|
||||
initializer.registerRpc("join_private_room",rpcJoinPrivateRoom);
|
||||
initializer.registerMatchmakerMatched(matchmakerMatched);
|
||||
|
||||
@@ -24,6 +24,20 @@ const logger = { info() {} };
|
||||
const presenceA = { userId: "pilot-a", sessionId: "a", username: "Pilot A" };
|
||||
const presenceB = { userId: "pilot-b", sessionId: "b", username: "Pilot B" };
|
||||
|
||||
let updatedAccount = null;
|
||||
let deletedAccount = null;
|
||||
const accountNk = {
|
||||
accountUpdateId(...args) { updatedAccount = args; },
|
||||
accountDeleteId(...args) { deletedAccount = args; }
|
||||
};
|
||||
assert.deepStrictEqual(JSON.parse(context.rpcUpdateDisplayName({ userId: "pilot-a" }, logger, accountNk, JSON.stringify({ display_name: " Kommandant Ägir " }))), { accepted: true, display_name: "Kommandant Ägir" });
|
||||
assert.strictEqual(updatedAccount[0], "pilot-a");
|
||||
assert.strictEqual(updatedAccount[2], "Kommandant Ägir");
|
||||
assert.strictEqual(JSON.parse(context.rpcUpdateDisplayName({ userId: "pilot-a" }, logger, accountNk, JSON.stringify({ display_name: "ADMIN" }))).accepted, false);
|
||||
assert.strictEqual(JSON.parse(context.rpcUpdateDisplayName({ userId: "pilot-a" }, logger, accountNk, JSON.stringify({ display_name: "A!" }))).accepted, false);
|
||||
assert.deepStrictEqual(JSON.parse(context.rpcDeleteAccount({ userId: "pilot-a" }, logger, accountNk, "{}")), { accepted: true });
|
||||
assert.deepStrictEqual(deletedAccount, ["pilot-a", false]);
|
||||
|
||||
let state = context.matchInit({}, logger, nk, { difficulty: 1 }).state;
|
||||
state = context.matchJoin({}, logger, nk, dispatcher, 1, state, [presenceA]).state;
|
||||
state = context.matchJoin({}, logger, nk, dispatcher, 2, state, [presenceB]).state;
|
||||
|
||||
@@ -32,9 +32,23 @@ func _run() -> void:
|
||||
assert(not info.is_exception(), "Die Godot-Anbindung muss Server-RPCs erreichen.")
|
||||
var info_data = JSON.parse_string(info.payload)
|
||||
assert(info_data is Dictionary and info_data.get("status") == "online", "Der Server muss seinen Onlinestatus melden.")
|
||||
assert(info_data.get("version") == "0.21", "Der Server muss die aktuelle Runtime 0.21 geladen haben: %s" % info_data)
|
||||
assert(info_data.get("version") == "0.22", "Der Server muss die aktuelle Runtime 0.22 geladen haben: %s" % info_data)
|
||||
|
||||
var run_payload := JSON.stringify({"map": 1, "difficulty": 1, "mode": "mission", "wave": 4, "score": 4700, "client_version": "0.21-integration"})
|
||||
var deletion_client: NakamaClient = nakama.create_client("voidbastion-public-v1", test_host, test_port, test_scheme, 5, NakamaLogger.LOG_LEVEL.ERROR)
|
||||
var deletion_session: NakamaSession = await deletion_client.authenticate_device_async("voidbastion_delete_test_" + suffix, "delete_test_" + suffix, true)
|
||||
assert(not deletion_session.is_exception() and deletion_session.is_valid(), "Ein Wegwerf-Testkonto muss angelegt werden können.")
|
||||
var renamed = await deletion_client.rpc_async(deletion_session, "update_display_name", JSON.stringify({"display_name": "Testpilot Ägir"}))
|
||||
assert(not renamed.is_exception(), "Ein gültiger Spielername muss serverseitig gespeichert werden können.")
|
||||
var renamed_data = JSON.parse_string(renamed.payload)
|
||||
assert(renamed_data is Dictionary and bool(renamed_data.get("accepted", false)), "Der Server muss die Namensänderung bestätigen.")
|
||||
var invalid_name = await deletion_client.rpc_async(deletion_session, "update_display_name", JSON.stringify({"display_name": "ADMIN"}))
|
||||
assert(not invalid_name.is_exception(), "Ein ungültiger Spielername muss kontrolliert abgelehnt werden.")
|
||||
var invalid_name_data = JSON.parse_string(invalid_name.payload)
|
||||
assert(invalid_name_data is Dictionary and not bool(invalid_name_data.get("accepted", true)), "Reservierte Spielernamen dürfen nicht angenommen werden.")
|
||||
var deleted = await deletion_client.rpc_async(deletion_session, "delete_account", "{}")
|
||||
assert(not deleted.is_exception(), "Das Wegwerf-Testkonto muss im Spiel löschbar sein.")
|
||||
|
||||
var run_payload := JSON.stringify({"map": 1, "difficulty": 1, "mode": "mission", "wave": 4, "score": 4700, "client_version": "0.22-integration"})
|
||||
var submitted = await client_a.rpc_async(session_a, "submit_run", run_payload)
|
||||
assert(not submitted.is_exception(), "Ein plausibles Ergebnis muss serverseitig angenommen werden.")
|
||||
var records = await client_a.list_leaderboard_records_async(session_a, "mission_map1_difficulty1", null, null, 10)
|
||||
|
||||
@@ -79,6 +79,13 @@ func _run() -> void:
|
||||
assert(not account_manager.player_id.is_empty(), "Beim ersten Start muss automatisch ein stabiles Gastprofil angelegt werden.")
|
||||
assert(not account_manager.display_name.is_empty(), "Das Gastprofil benötigt einen sichtbaren Pilotennamen.")
|
||||
account_manager.storage_enabled = false
|
||||
var original_display_name: String = account_manager.display_name
|
||||
assert(account_manager.validate_display_name("Kommandant Ägir").is_empty(), "Ein normaler internationaler Spielername muss gültig sein.")
|
||||
assert(not account_manager.validate_display_name("ADMIN").is_empty(), "Reservierte Spielernamen müssen lokal abgewiesen werden.")
|
||||
assert(not account_manager.validate_display_name("A!").is_empty(), "Zu kurze Namen mit Sonderzeichen müssen lokal abgewiesen werden.")
|
||||
assert(account_manager.set_display_name(" Kommandant Ägir "), "Mehrfache Leerzeichen müssen beim Speichern normalisiert werden.")
|
||||
assert(account_manager.display_name == "Kommandant Ägir", "Der sichtbare Spielername muss normalisiert gespeichert werden.")
|
||||
account_manager.set_display_name(original_display_name)
|
||||
var previous_test_score: int = account_manager.get_map_best_score(0, false)
|
||||
assert(account_manager.record_run(0, 0, false, 3, previous_test_score + 1000), "Ein neuer lokaler Highscore muss gespeichert werden.")
|
||||
assert(account_manager.get_map_best_wave(0, false) >= 3, "Die Profilansicht muss den gespeicherten Missionsbestwert lesen können.")
|
||||
|
||||
Reference in new issue
Block a user