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

+98
View File
@@ -4,6 +4,10 @@ var DIFFICULTY_COUNT = 3;
var VALID_MODES = ["mission", "endless", "coop_ai", "coop_online"];
var MATCH_MODULE = "void_bastion_coop";
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 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;
@@ -33,6 +37,96 @@ function leaderboardId(mapIndex, difficulty, mode) { return mode + "_map" + mapI
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"});
}
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) {
if (!ctx.userId) throw new Error("authentication required");
var run;
@@ -79,6 +173,8 @@ function rpcUpdateDisplayName(ctx, logger, nk, payload) {
}
function rpcDeleteAccount(ctx, logger, nk, payload) {
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);
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) {
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("record_activity",rpcRecordActivity);
initializer.registerRpc("private_stats",rpcPrivateStats);
initializer.registerRpc("submit_run",rpcSubmitRun);
initializer.registerRpc("update_display_name",rpcUpdateDisplayName);
initializer.registerRpc("delete_account",rpcDeleteAccount);