Add private player metrics and alpha.2 package
This commit is contained in:
1 parent
b4980f0acd
commit
178a6bbc85
12 files changed
+198
-13
No files matched your search
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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?', ''
|
||||||
|
$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."
|
throw "Der Godot-Windows-Export ist fehlgeschlagen."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Set-Content -LiteralPath $projectFile -Value $projectConfiguration -NoNewline
|
||||||
}
|
}
|
||||||
|
|
||||||
Push-Location $PSScriptRoot
|
Push-Location $PSScriptRoot
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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.")
|
||||||
|
|||||||
Reference in new issue
Block a user