Add private player metrics and alpha.2 package

This commit is contained in:
Emporex committed 2026-10-04 20:41:31 +02:00
1 parent b4980f0acd
commit 178a6bbc85
12 files changed
+199 -14

No files matched your search

+4 -4
View File
@@ -4,10 +4,10 @@ Diese Liste wird vor jeder öffentlichen Windows-Alpha vollständig abgearbeitet
## Build und Installation ## Build und Installation
- [ ] **A** `tests/smoke_test.gd` läuft ohne Fehler. - [x] **A** `tests/smoke_test.gd` läuft ohne Fehler.
- [ ] **A** `tests/online_integration_test.gd` läuft gegen `api.void-bastion.gergames.de` ohne Fehler. - [x] **A** `tests/online_integration_test.gd` läuft gegen `api.void-bastion.gergames.de` ohne Fehler.
- [ ] **A** Windows-Export und Setup werden mit `packaging/windows/build.ps1` reproduzierbar erstellt. - [x] **A** Windows-Export und Setup werden mit `packaging/windows/build.ps1` reproduzierbar erstellt.
- [ ] **A** Entwicklerwerkzeuge, Tests, Serverdateien und interne MCP-Konfigurationen sind nicht im Windows-Paket enthalten. - [x] **A** Entwicklerwerkzeuge, Tests, Serverdateien und interne MCP-Konfigurationen sind nicht im Windows-Paket enthalten.
- [ ] **M** Setup installiert ohne Administratorrechte, Startmenü- und Desktop-Verknüpfung funktionieren. - [ ] **M** Setup installiert ohne Administratorrechte, Startmenü- und Desktop-Verknüpfung funktionieren.
- [ ] **M** Deinstallation entfernt Programmdateien und Verknüpfungen vollständig. - [ ] **M** Deinstallation entfernt Programmdateien und Verknüpfungen vollständig.
- [ ] **M** Windows-SmartScreen-Hinweis ist in den Testhinweisen genannt, solange der Installer noch nicht signiert ist. - [ ] **M** Windows-SmartScreen-Hinweis ist in den Testhinweisen genannt, solange der Installer noch nicht signiert ist.
+6
View File
@@ -22,6 +22,12 @@ Die Kennungen folgen diesem Muster: `mission_map0_difficulty0`.
Mögliche Spielarten sind `mission`, `endless`, `coop_ai` und `coop_online`. Mögliche Spielarten sind `mission`, `endless`, `coop_ai` und `coop_online`.
## Interne Nutzungsstatistik
Nach jeder erfolgreichen Anmeldung meldet der Client Plattform und App-Version an die Serverfunktion `record_activity`. Pro Konto wird nur ein pseudonymer Aktivitätsdatensatz mit erstem und letztem Aktivitätstag gespeichert. Daraus führt Nakama zusammengefasste Werte für registrierte und täglich aktive Spieler sowie die Verteilung nach Plattform und Version.
Die Serverfunktion `private_stats` ist ausschließlich über den geheimen Nakama-HTTP-Schlüssel für die geschützte, nicht verlinkte Statistikseite der Homepage erreichbar. Angemeldete Spielclients können diese Werte nicht abrufen. Der geheime Schlüssel bleibt auf Unraid und im privaten Webhosting-Verzeichnis; er wird weder exportiert noch im Repository gespeichert.
Im Hauptmenü ist eine Ranglistenansicht für die aktuell gewählte Karte und Schwierigkeit eingebaut. Sie zeigt Mission und Endlosmodus getrennt, löst aktuelle öffentliche Profilnamen auf und hebt den eigenen Eintrag hervor. Im Hauptmenü ist eine Ranglistenansicht für die aktuell gewählte Karte und Schwierigkeit eingebaut. Sie zeigt Mission und Endlosmodus getrennt, löst aktuelle öffentliche Profilnamen auf und hebt den eigenen Eintrag hervor.
## Multiplayer ## Multiplayer
+19
View File
@@ -0,0 +1,19 @@
# Void Bastion 0.1.0-alpha.2
Diese Testversion erweitert die Online-Grundlage und verbessert die Zuverlässigkeit des Windows-Pakets.
## Neu
- interne, pseudonyme Zählung registrierter und täglich aktiver Spieler
- Plattform- und Versionsverteilung für die geschützte GerGames-Statistik
- Aktivitätswerte sind nicht öffentlich und für normale Spielclients nicht abrufbar
## Verbessert
- Windows-Export enthält keine lokalen Entwicklungswerkzeuge mehr
- automatischer Koop-KI-Test ist unabhängig von den lokal gewählten Einstellungen
- aktualisierte Datenschutzinformation zur internen Nutzungsstatistik
## Hinweis
Void Bastion befindet sich weiterhin in einer frühen Alpha-Phase. Rückmeldungen und Fehlerberichte bitte an support@gergames.de senden.
+2 -2
View File
@@ -24,8 +24,8 @@ application/modify_resources=true
application/icon="res://assets/app_icon.png" application/icon="res://assets/app_icon.png"
application/console_wrapper_icon="" application/console_wrapper_icon=""
application/icon_interpolation=4 application/icon_interpolation=4
application/file_version="0.1.0.0" application/file_version="0.1.0.2"
application/product_version="0.1.0.0" application/product_version="0.1.0.2"
application/company_name="GerGames" application/company_name="GerGames"
application/product_name="Void Bastion" application/product_name="Void Bastion"
application/file_description="Void Bastion – Alpha-Testversion" application/file_description="Void Bastion – Alpha-Testversion"
+11
View File
@@ -85,6 +85,7 @@ func _authenticate_on(host: String, port: int, scheme: String) -> bool:
func _after_authentication() -> void: func _after_authentication() -> void:
await _record_activity()
socket = Nakama.create_socket_from(client) socket = Nakama.create_socket_from(client)
_bind_socket_signals() _bind_socket_signals()
var result = await socket.connect_async(session, true, 5) var result = await socket.connect_async(session, true, 5)
@@ -99,6 +100,16 @@ func _after_authentication() -> void:
await flush_pending_scores() await flush_pending_scores()
func _record_activity() -> void:
if not is_authenticated():
return
var payload := {
"platform": OS.get_name().to_lower(),
"version": str(ProjectSettings.get_setting("application/config/version", "development"))
}
await client.rpc_async(session, "record_activity", JSON.stringify(payload))
func _bind_socket_signals() -> void: func _bind_socket_signals() -> void:
socket.received_matchmaker_matched.connect(_on_matchmaker_matched) socket.received_matchmaker_matched.connect(_on_matchmaker_matched)
socket.received_match_state.connect(_on_match_state) socket.received_match_state.connect(_on_match_state)
+17 -3
View File
@@ -11,6 +11,7 @@ $projectRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
$gameExe = Join-Path $projectRoot "build\windows\Void-Bastion.exe" $gameExe = Join-Path $projectRoot "build\windows\Void-Bastion.exe"
$installer = Join-Path $projectRoot "dist\Void-Bastion-Setup-x64.exe" $installer = Join-Path $projectRoot "dist\Void-Bastion-Setup-x64.exe"
$installerScript = Join-Path $PSScriptRoot "installer.nsi" $installerScript = Join-Path $PSScriptRoot "installer.nsi"
$projectFile = Join-Path $projectRoot "project.godot"
foreach ($requiredFile in @($GodotExe, $MakeNsisExe, $installerScript)) { foreach ($requiredFile in @($GodotExe, $MakeNsisExe, $installerScript)) {
if (-not (Test-Path -LiteralPath $requiredFile -PathType Leaf)) { if (-not (Test-Path -LiteralPath $requiredFile -PathType Leaf)) {
@@ -20,9 +21,22 @@ foreach ($requiredFile in @($GodotExe, $MakeNsisExe, $installerScript)) {
New-Item -ItemType Directory -Force -Path (Split-Path $gameExe), (Split-Path $installer) | Out-Null New-Item -ItemType Directory -Force -Path (Split-Path $gameExe), (Split-Path $installer) | Out-Null
& $GodotExe --headless --path $projectRoot --export-release "Windows x64" $gameExe $projectConfiguration = Get-Content -LiteralPath $projectFile -Raw
if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $gameExe)) { $releaseConfiguration = $projectConfiguration -replace '(?m)^_MCPGameBridge=.*\r?\n?', ''
throw "Der Godot-Windows-Export ist fehlgeschlagen." $releaseConfiguration = $releaseConfiguration -replace '"res://addons/agent_tools/plugin\.cfg",\s*', ''
try {
# Die lokale MCP-Laufzeit ist ein Entwicklungswerkzeug und wird vom
# Exportfilter ausgeschlossen. Deshalb dürfen weder ihr Autoload-Verweis
# noch das Editor-Plugin in der veröffentlichten project.binary verbleiben.
Set-Content -LiteralPath $projectFile -Value $releaseConfiguration -NoNewline
& $GodotExe --headless --path $projectRoot --export-release "Windows x64" $gameExe
if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $gameExe)) {
throw "Der Godot-Windows-Export ist fehlgeschlagen."
}
}
finally {
Set-Content -LiteralPath $projectFile -Value $projectConfiguration -NoNewline
} }
Push-Location $PSScriptRoot Push-Location $PSScriptRoot
+2 -2
View File
@@ -6,7 +6,7 @@ RequestExecutionLevel user
!include "LogicLib.nsh" !include "LogicLib.nsh"
!define PRODUCT_NAME "Void Bastion" !define PRODUCT_NAME "Void Bastion"
!define PRODUCT_VERSION "0.1.0-alpha" !define PRODUCT_VERSION "0.1.0-alpha.2"
!define PRODUCT_PUBLISHER "GerGames" !define PRODUCT_PUBLISHER "GerGames"
!define PRODUCT_WEBSITE "https://void-bastion.gergames.de/" !define PRODUCT_WEBSITE "https://void-bastion.gergames.de/"
!define PRODUCT_EXE "Void-Bastion.exe" !define PRODUCT_EXE "Void-Bastion.exe"
@@ -21,7 +21,7 @@ BrandingText "GerGames"
ShowInstDetails show ShowInstDetails show
ShowUninstDetails show ShowUninstDetails show
VIProductVersion "0.1.0.0" VIProductVersion "0.1.0.2"
VIAddVersionKey /LANG=1031 "ProductName" "${PRODUCT_NAME}" VIAddVersionKey /LANG=1031 "ProductName" "${PRODUCT_NAME}"
VIAddVersionKey /LANG=1031 "CompanyName" "${PRODUCT_PUBLISHER}" VIAddVersionKey /LANG=1031 "CompanyName" "${PRODUCT_PUBLISHER}"
VIAddVersionKey /LANG=1031 "FileDescription" "${PRODUCT_NAME} Setup" VIAddVersionKey /LANG=1031 "FileDescription" "${PRODUCT_NAME} Setup"
+1 -1
View File
@@ -15,7 +15,7 @@ compatibility/default_parent_skeleton_in_mesh_instance_3d=true
[application] [application]
config/name="Void Bastion" config/name="Void Bastion"
config/version="0.1.0-alpha" config/version="0.1.0-alpha.2"
run/main_scene="res://main.tscn" run/main_scene="res://main.tscn"
config/features=PackedStringArray("4.7") config/features=PackedStringArray("4.7")
config/icon="res://assets/app_icon.png" config/icon="res://assets/app_icon.png"
+7
View File
@@ -27,3 +27,10 @@ Für eine öffentliche Testversion wird Port 7350 später über Nginx Proxy Mana
## Betrieb ## Betrieb
Der Stack verwendet fest versioniertes Nakama 3.37.0 und PostgreSQL 16 Alpine. Vor einem Versionswechsel werden Datenbank und Nakama-Appdata gesichert und die offiziellen Migrationshinweise geprüft. Der Stack verwendet fest versioniertes Nakama 3.37.0 und PostgreSQL 16 Alpine. Vor einem Versionswechsel werden Datenbank und Nakama-Appdata gesichert und die offiziellen Migrationshinweise geprüft.
## Private Statistik-RPCs
- `record_activity`: nur für angemeldete Spieler; aktualisiert die pseudonyme Aktivität sowie aggregierte Plattform- und Versionswerte.
- `private_stats`: nur als Server-zu-Server-Aufruf mit `NAKAMA_HTTP_KEY`; liefert die zusammengefassten Werte für die interne Homepage.
Die echte Webhosting-Konfiguration mit dem HTTP-Schlüssel darf nicht in Git abgelegt werden.
+98
View File
@@ -4,6 +4,10 @@ var DIFFICULTY_COUNT = 3;
var VALID_MODES = ["mission", "endless", "coop_ai", "coop_online"]; var VALID_MODES = ["mission", "endless", "coop_ai", "coop_online"];
var MATCH_MODULE = "void_bastion_coop"; var MATCH_MODULE = "void_bastion_coop";
var ROOM_COLLECTION = "private_rooms"; var ROOM_COLLECTION = "private_rooms";
var ACTIVITY_COLLECTION = "player_activity";
var ACTIVITY_KEY = "summary";
var STATS_COLLECTION = "internal_stats";
var STATS_KEY = "global";
var SYSTEM_USER_ID = "00000000-0000-0000-0000-000000000000"; var SYSTEM_USER_ID = "00000000-0000-0000-0000-000000000000";
var OP_SNAPSHOT = 1, OP_EVENT = 2, OP_BUILD = 10, OP_UPGRADE = 11, OP_SELL = 12, OP_START_WAVE = 13, OP_READY = 14; var OP_SNAPSHOT = 1, OP_EVENT = 2, OP_BUILD = 10, OP_UPGRADE = 11, OP_SELL = 12, OP_START_WAVE = 13, OP_READY = 14;
var TICK_RATE = 10, SNAPSHOT_TICKS = 2, GRID = 80, WORLD_WIDTH = 2960, MIRROR_X = 1480; var TICK_RATE = 10, SNAPSHOT_TICKS = 2, GRID = 80, WORLD_WIDTH = 2960, MIRROR_X = 1480;
@@ -33,6 +37,96 @@ function leaderboardId(mapIndex, difficulty, mode) { return mode + "_map" + mapI
function rpcServerInfo(ctx, logger, nk, payload) { function rpcServerInfo(ctx, logger, nk, payload) {
return JSON.stringify({game:"void_bastion",version:GAME_VERSION,leaderboards:MAP_COUNT*DIFFICULTY_COUNT*VALID_MODES.length,authoritative_multiplayer:true,status:"online"}); return JSON.stringify({game:"void_bastion",version:GAME_VERSION,leaderboards:MAP_COUNT*DIFFICULTY_COUNT*VALID_MODES.length,authoritative_multiplayer:true,status:"online"});
} }
function normalizeMetricLabel(value, fallback) {
var normalized = String(value || "").toLowerCase().replace(/[^a-z0-9._-]/g, "").substring(0, 32);
return normalized || fallback;
}
function utcDay(timestamp) {
return new Date(timestamp).toISOString().slice(0, 10);
}
function readStorageRecord(nk, collection, key, userId) {
var records = nk.storageRead([{collection:collection,key:key,userId:userId}]);
return records.length ? records[0] : null;
}
function emptyStats(now) {
return {total_players:0,platforms:{},versions:{},daily_active:{},created_at:now,updated_at:now};
}
function incrementMetric(metrics, key, amount) {
metrics[key] = Math.max(0, Math.floor(Number(metrics[key] || 0)) + amount);
if (metrics[key] === 0) delete metrics[key];
}
function trimDailyActivity(daily, currentDay) {
var minimum = Date.parse(currentDay + "T00:00:00.000Z") - 35 * 86400000;
var days = Object.keys(daily);
for (var i = 0; i < days.length; i++) {
if (Date.parse(days[i] + "T00:00:00.000Z") < minimum) delete daily[days[i]];
}
}
function writeStats(nk, previousRecord, value) {
var write = {collection:STATS_COLLECTION,key:STATS_KEY,userId:SYSTEM_USER_ID,value:value,permissionRead:0,permissionWrite:0};
if (previousRecord && previousRecord.version) write.version = previousRecord.version;
else write.version = "*";
nk.storageWrite([write]);
}
function updatePlayerStats(nk, playerRecord, platform, version, day, now) {
for (var attempt = 0; attempt < 3; attempt++) {
var statsRecord = readStorageRecord(nk, STATS_COLLECTION, STATS_KEY, SYSTEM_USER_ID);
var stats = statsRecord && statsRecord.value ? statsRecord.value : emptyStats(now);
stats.platforms = stats.platforms || {};
stats.versions = stats.versions || {};
stats.daily_active = stats.daily_active || {};
if (!playerRecord) {
stats.total_players = Math.max(0, Math.floor(Number(stats.total_players || 0))) + 1;
incrementMetric(stats.platforms, platform, 1);
incrementMetric(stats.versions, version, 1);
} else {
var oldPlatform = normalizeMetricLabel(playerRecord.value.platform, "unknown");
var oldVersion = normalizeMetricLabel(playerRecord.value.version, "unknown");
if (oldPlatform !== platform) { incrementMetric(stats.platforms, oldPlatform, -1); incrementMetric(stats.platforms, platform, 1); }
if (oldVersion !== version) { incrementMetric(stats.versions, oldVersion, -1); incrementMetric(stats.versions, version, 1); }
}
if (!playerRecord || String(playerRecord.value.last_seen_day || "") !== day) incrementMetric(stats.daily_active, day, 1);
trimDailyActivity(stats.daily_active, day);
stats.updated_at = now;
try { writeStats(nk, statsRecord, stats); return; }
catch (error) { if (attempt === 2) throw error; }
}
}
function removePlayerStats(nk, playerRecord, now) {
if (!playerRecord || !playerRecord.value) return;
for (var attempt = 0; attempt < 3; attempt++) {
var statsRecord = readStorageRecord(nk, STATS_COLLECTION, STATS_KEY, SYSTEM_USER_ID);
if (!statsRecord || !statsRecord.value) return;
var stats = statsRecord.value;
stats.platforms = stats.platforms || {};
stats.versions = stats.versions || {};
stats.total_players = Math.max(0, Math.floor(Number(stats.total_players || 0)) - 1);
incrementMetric(stats.platforms, normalizeMetricLabel(playerRecord.value.platform, "unknown"), -1);
incrementMetric(stats.versions, normalizeMetricLabel(playerRecord.value.version, "unknown"), -1);
stats.updated_at = now;
try { writeStats(nk, statsRecord, stats); return; }
catch (error) { if (attempt === 2) throw error; }
}
}
function rpcRecordActivity(ctx, logger, nk, payload) {
if (!ctx.userId) throw new Error("authentication required");
var request;
try { request = JSON.parse(payload || "{}"); } catch (error) { request = {}; }
var platform = normalizeMetricLabel(request.platform, "unknown");
var version = normalizeMetricLabel(request.version, "unknown");
var now = Date.now(), day = utcDay(now);
var playerRecord = readStorageRecord(nk, ACTIVITY_COLLECTION, ACTIVITY_KEY, ctx.userId);
updatePlayerStats(nk, playerRecord, platform, version, day, now);
nk.storageWrite([{collection:ACTIVITY_COLLECTION,key:ACTIVITY_KEY,userId:ctx.userId,value:{first_seen:playerRecord&&playerRecord.value?Number(playerRecord.value.first_seen||now):now,last_seen:now,last_seen_day:day,platform:platform,version:version},permissionRead:0,permissionWrite:0}]);
return JSON.stringify({accepted:true});
}
function rpcPrivateStats(ctx, logger, nk, payload) {
if (ctx.userId) throw new Error("server-to-server access required");
var record = readStorageRecord(nk, STATS_COLLECTION, STATS_KEY, SYSTEM_USER_ID);
var stats = record && record.value ? record.value : emptyStats(Date.now());
return JSON.stringify(stats);
}
function rpcSubmitRun(ctx, logger, nk, payload) { function rpcSubmitRun(ctx, logger, nk, payload) {
if (!ctx.userId) throw new Error("authentication required"); if (!ctx.userId) throw new Error("authentication required");
var run; var run;
@@ -79,6 +173,8 @@ function rpcUpdateDisplayName(ctx, logger, nk, payload) {
} }
function rpcDeleteAccount(ctx, logger, nk, payload) { function rpcDeleteAccount(ctx, logger, nk, payload) {
if (!ctx.userId) throw new Error("authentication required"); if (!ctx.userId) throw new Error("authentication required");
var playerRecord = readStorageRecord(nk, ACTIVITY_COLLECTION, ACTIVITY_KEY, ctx.userId);
removePlayerStats(nk, playerRecord, Date.now());
nk.accountDeleteId(ctx.userId, false); nk.accountDeleteId(ctx.userId, false);
return JSON.stringify({accepted:true}); return JSON.stringify({accepted:true});
} }
@@ -294,6 +390,8 @@ function matchSignal(ctx,logger,nk,dispatcher,tick,state,data) { return {state:s
function InitModule(ctx,logger,nk,initializer) { 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"}); 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("server_info",rpcServerInfo);
initializer.registerRpc("record_activity",rpcRecordActivity);
initializer.registerRpc("private_stats",rpcPrivateStats);
initializer.registerRpc("submit_run",rpcSubmitRun); initializer.registerRpc("submit_run",rpcSubmitRun);
initializer.registerRpc("update_display_name",rpcUpdateDisplayName); initializer.registerRpc("update_display_name",rpcUpdateDisplayName);
initializer.registerRpc("delete_account",rpcDeleteAccount); initializer.registerRpc("delete_account",rpcDeleteAccount);
+29
View File
@@ -27,6 +27,7 @@ const presenceB = { userId: "pilot-b", sessionId: "b", username: "Pilot B" };
let updatedAccount = null; let updatedAccount = null;
let deletedAccount = null; let deletedAccount = null;
const accountNk = { const accountNk = {
storageRead() { return []; },
accountUpdateId(...args) { updatedAccount = args; }, accountUpdateId(...args) { updatedAccount = args; },
accountDeleteId(...args) { deletedAccount = args; } accountDeleteId(...args) { deletedAccount = args; }
}; };
@@ -38,6 +39,34 @@ assert.strictEqual(JSON.parse(context.rpcUpdateDisplayName({ userId: "pilot-a" }
assert.deepStrictEqual(JSON.parse(context.rpcDeleteAccount({ userId: "pilot-a" }, logger, accountNk, "{}")), { accepted: true }); assert.deepStrictEqual(JSON.parse(context.rpcDeleteAccount({ userId: "pilot-a" }, logger, accountNk, "{}")), { accepted: true });
assert.deepStrictEqual(deletedAccount, ["pilot-a", false]); assert.deepStrictEqual(deletedAccount, ["pilot-a", false]);
const metricStorage = new Map();
let metricVersion = 0;
const metricNk = {
storageRead(ops) {
return ops.map(op => metricStorage.get(`${op.collection}:${op.userId}:${op.key}`)).filter(Boolean);
},
storageWrite(ops) {
for (const op of ops) {
const storageKey = `${op.collection}:${op.userId}:${op.key}`;
const previous = metricStorage.get(storageKey);
if (op.version === "*" && previous) throw new Error("version conflict");
if (op.version && op.version !== "*" && (!previous || previous.version !== op.version)) throw new Error("version conflict");
metricVersion += 1;
metricStorage.set(storageKey, { value: JSON.parse(JSON.stringify(op.value)), version: String(metricVersion) });
}
}
};
assert.deepStrictEqual(JSON.parse(context.rpcRecordActivity({ userId: "pilot-metric-a" }, logger, metricNk, JSON.stringify({ platform: "Windows", version: "0.1.0-alpha.2" }))), { accepted: true });
assert.deepStrictEqual(JSON.parse(context.rpcRecordActivity({ userId: "pilot-metric-a" }, logger, metricNk, JSON.stringify({ platform: "Windows", version: "0.1.0-alpha.2" }))), { accepted: true });
assert.deepStrictEqual(JSON.parse(context.rpcRecordActivity({ userId: "pilot-metric-b" }, logger, metricNk, JSON.stringify({ platform: "Android", version: "0.1.0-alpha.2" }))), { accepted: true });
const privateStats = JSON.parse(context.rpcPrivateStats({}, logger, metricNk, "{}"));
assert.strictEqual(privateStats.total_players, 2);
assert.strictEqual(privateStats.platforms.windows, 1);
assert.strictEqual(privateStats.platforms.android, 1);
assert.strictEqual(privateStats.versions["0.1.0-alpha.2"], 2);
assert.strictEqual(privateStats.daily_active[context.utcDay(Date.now())], 2);
assert.throws(() => context.rpcPrivateStats({ userId: "pilot-a" }, logger, metricNk, "{}"), /server-to-server/);
let leaderboardWrite = null; let leaderboardWrite = null;
const leaderboardNk = { const leaderboardNk = {
accountGetId() { return { user: { displayName: "Kommandant Ägir" } }; }, accountGetId() { return { user: { displayName: "Kommandant Ägir" } }; },
+3 -2
View File
@@ -50,6 +50,7 @@ func _run() -> void:
assert(game.get("game_mode") == "multiplayer_menu", "Die Spielersuche muss über die sichtbare Schaltfläche abgebrochen werden können.") assert(game.get("game_mode") == "multiplayer_menu", "Die Spielersuche muss über die sichtbare Schaltfläche abgebrochen werden können.")
game.call("_handle_pointer", Vector2(140, 60), false) game.call("_handle_pointer", Vector2(140, 60), false)
assert(game.get("game_mode") == "menu", "Der Multiplayerbereich benötigt einen funktionierenden Zurück-Button.") assert(game.get("game_mode") == "menu", "Der Multiplayerbereich benötigt einen funktionierenden Zurück-Button.")
game.set("selected_difficulty", 0)
game.call("_handle_pointer", Vector2(1100, 730), false) game.call("_handle_pointer", Vector2(1100, 730), false)
game.call("_handle_pointer", Vector2(1300, 695), false) game.call("_handle_pointer", Vector2(1300, 695), false)
assert(game.get("game_mode") == "playing" and game.get("coop_mode") and game.get("coop_ai_mode"), "Der KI-Koop-Modus muss direkt spielbar gestartet werden können.") assert(game.get("game_mode") == "playing" and game.get("coop_mode") and game.get("coop_ai_mode"), "Der KI-Koop-Modus muss direkt spielbar gestartet werden können.")
@@ -82,10 +83,10 @@ func _run() -> void:
assert(game.get("game_mode") == "menu", "Die Bestenliste benötigt einen funktionierenden Zurück-Button.") assert(game.get("game_mode") == "menu", "Die Bestenliste benötigt einen funktionierenden Zurück-Button.")
assert(not account_manager.player_id.is_empty(), "Beim ersten Start muss automatisch ein stabiles Gastprofil angelegt werden.") 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.") assert(not account_manager.display_name.is_empty(), "Das Gastprofil benötigt einen sichtbaren Pilotennamen.")
assert(game.call("_app_version") == "0.1.0-alpha", "Profil, Installer und Online-Übertragung müssen dieselbe Alpha-Version verwenden.") assert(game.call("_app_version") == "0.1.0-alpha.2", "Profil, Installer und Online-Übertragung müssen dieselbe Alpha-Version verwenden.")
var support_url: String = game.call("_support_email_url") var support_url: String = game.call("_support_email_url")
assert(support_url.begins_with("mailto:support@gergames.de?"), "Die Feedback-Schaltfläche muss die öffentliche Supportadresse verwenden.") assert(support_url.begins_with("mailto:support@gergames.de?"), "Die Feedback-Schaltfläche muss die öffentliche Supportadresse verwenden.")
assert(support_url.contains("0.1.0-alpha") and support_url.contains(account_manager.short_id()), "Die Support-Mail muss Version und gekürzte Spieler-ID zur Fehlerzuordnung enthalten.") assert(support_url.contains("0.1.0-alpha.2") and support_url.contains(account_manager.short_id()), "Die Support-Mail muss Version und gekürzte Spieler-ID zur Fehlerzuordnung enthalten.")
account_manager.storage_enabled = false account_manager.storage_enabled = false
var original_display_name: String = account_manager.display_name 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(account_manager.validate_display_name("Kommandant Ägir").is_empty(), "Ein normaler internationaler Spielername muss gültig sein.")