Add player names and account deletion

This commit is contained in:
Emporex committed 2026-10-04 08:09:52 +02:00
1 parent 32594c7f81
commit 1b5b8dc0a4
10 files changed
+299 -11

No files matched your search

+32
View File
@@ -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
View File
@@ -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.
+5 -2
View File
@@ -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.
+39
View File
@@ -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]
+119 -2
View File
@@ -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,7 +477,30 @@ 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):
@@ -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
View File
@@ -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 []
+32 -2
View File
@@ -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);
+14
View File
@@ -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;
+16 -2
View File
@@ -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)
+7
View File
@@ -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.")