Prepare alpha 11 with online diagnostics
This commit is contained in:
1 parent
d53467baf2
commit
4d9e6d9185
14 files changed
+550
-77
No files matched your search
@@ -12,6 +12,9 @@ var PROMO_CODE_COLLECTION = "unlock_codes";
|
||||
var PROMO_UNLOCK_COLLECTION = "player_unlocks";
|
||||
var PROMO_UNLOCK_KEY = "website_codes";
|
||||
var PROMO_ATTEMPT_COLLECTION = "unlock_attempts";
|
||||
var DIAG_PARTICIPANT_COLLECTION = "online_diag_participants";
|
||||
var DIAG_REPORT_COLLECTION = "online_diag_reports";
|
||||
var DIAG_RETENTION_MS = 14 * 86400000;
|
||||
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 = 1, GRID = 80, WORLD_WIDTH = 2960, MIRROR_X = 1480;
|
||||
@@ -320,6 +323,18 @@ function rpcUpdateDisplayName(ctx, logger, nk, payload) {
|
||||
nk.accountUpdateId(ctx.userId, null, name, null, null, null, null, null);
|
||||
return JSON.stringify({accepted:true,display_name:name});
|
||||
}
|
||||
function rpcFindPlayerByName(ctx, logger, nk, payload) {
|
||||
if (!ctx.userId) throw new Error("authentication required");
|
||||
var request;
|
||||
try { request = JSON.parse(payload || "{}"); } catch (error) { return JSON.stringify({found:false,error:"invalid_json"}); }
|
||||
var name = normalizeDisplayName(request.display_name);
|
||||
if (validateDisplayName(name)) return JSON.stringify({found:false,error:"invalid_name"});
|
||||
var rows = nk.sqlQuery("SELECT id, display_name FROM users WHERE lower(display_name) = lower($1) LIMIT 3", [name]);
|
||||
var candidates = rows.filter(function(row) { return String(row.id) !== ctx.userId; });
|
||||
if (!candidates.length) return JSON.stringify({found:false,error:"not_found"});
|
||||
if (candidates.length !== 1 || rows.length > 2) return JSON.stringify({found:false,error:"ambiguous"});
|
||||
return JSON.stringify({found:true,user_id:String(candidates[0].id),display_name:String(candidates[0].display_name)});
|
||||
}
|
||||
function rpcDeleteAccount(ctx, logger, nk, payload) {
|
||||
if (!ctx.userId) throw new Error("authentication required");
|
||||
var playerRecord = readStorageRecord(nk, ACTIVITY_COLLECTION, ACTIVITY_KEY, ctx.userId);
|
||||
@@ -449,6 +464,16 @@ function matchJoin(ctx,logger,nk,dispatcher,tick,state,presences) {
|
||||
}
|
||||
if (state.started&&Object.keys(state.presences).length===2) { state.pausedForReconnect=false; state.reconnectDeadlineTick=0; if (rejoined) broadcastEvent(dispatcher,"partner_rejoined",{}); }
|
||||
state.emptyTicks=0;
|
||||
if (ctx.matchId) {
|
||||
var roundId = nk.sha256Hash(ctx.matchId).slice(0, 16);
|
||||
var participantSlots = {}, participantIds = Object.keys(state.slots);
|
||||
for (var participantIndex = 0; participantIndex < participantIds.length; participantIndex++) {
|
||||
var participantId = participantIds[participantIndex];
|
||||
participantSlots[nk.sha256Hash(ctx.matchId + ":" + participantId)] = state.slots[participantId];
|
||||
}
|
||||
nk.storageWrite([{collection:DIAG_PARTICIPANT_COLLECTION,key:roundId,userId:SYSTEM_USER_ID,
|
||||
value:{participants:participantSlots,created_at:Date.now()},permissionRead:0,permissionWrite:0}]);
|
||||
}
|
||||
dispatcher.matchLabelUpdate(matchLabel(state));
|
||||
broadcastEvent(dispatcher,"players",{count:Object.keys(state.presences).length,started:state.started,slots:state.slots});
|
||||
broadcastSnapshot(dispatcher,state,tick);
|
||||
@@ -535,6 +560,7 @@ function spawnEnemy(state) {
|
||||
var routeId=state.nextSpawnRoute; state.nextSpawnRoute=(state.nextSpawnRoute+1)%2; var route=PATHS[routeId];
|
||||
var hasShield=type===4||(state.wave>=4&&Math.random()<Math.min(0.42,0.10+state.wave*0.018+state.difficulty*0.06));
|
||||
var maxShieldHp=hasShield?base.hp*scale*0.55:0;
|
||||
if (boss && state.wave === 5) maxShieldHp *= 0.5;
|
||||
state.enemies.push({id:state.nextEnemyId++,type:type,x:route[0][0],y:route[0][1],path_index:1,route_id:routeId,hp:base.hp*scale,max_hp:base.hp*scale,speed:base.speed*(1+state.wave*0.012)*(boss?0.82:1)*d.speed,reward:Math.floor((base.reward+state.wave)*(boss?4:1)*d.income),angle:0,boss:boss,shield:hasShield,shield_hp:maxShieldHp,max_shield_hp:maxShieldHp,shield_resistance:type===4?0.18:0.28});
|
||||
}
|
||||
function applyDamageToEnemy(enemy,damage,towerType) {
|
||||
@@ -642,6 +668,95 @@ function matchLoop(ctx,logger,nk,dispatcher,tick,state,messages) {
|
||||
function matchTerminate(ctx,logger,nk,dispatcher,tick,state,graceSeconds) { deletePrivateRoomRecord(nk,state.roomCode); broadcastEvent(dispatcher,"server_shutdown",{grace:graceSeconds}); return {state:state}; }
|
||||
function matchSignal(ctx,logger,nk,dispatcher,tick,state,data) { return {state:state,data:JSON.stringify(snapshot(state,tick))}; }
|
||||
|
||||
function sanitizeDiagnosticRecord(input) {
|
||||
if (!input || typeof input !== "object") return null;
|
||||
var allowedEvents = ["window","snapshot_gap","tick_jump","frame_stall","socket_closed","socket_reconnected","socket_reconnect_failed","side","end"];
|
||||
var event = String(input.event || "");
|
||||
if (allowedEvents.indexOf(event) < 0) return null;
|
||||
var result = {event:event};
|
||||
var utc = String(input.utc || "");
|
||||
if (/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d/.test(utc)) result.utc = utc.slice(0, 25);
|
||||
var fields = ["seconds","snapshots","snapshots_per_second","gap_p95_ms","gap_max_ms","gaps_over_500_ms","last_server_tick","tick_jump_max","tick_jumps_over_1","bytes_avg","bytes_max","parse_max_us","apply_max_us","frame_max_ms","frame_stalls_over_200_ms","coalesced","queue_dropped","queue_max","enemies","towers","duration_ms","from_tick","to_tick","steps","attempt"];
|
||||
for (var i = 0; i < fields.length; i++) {
|
||||
var value = Number(input[fields[i]]);
|
||||
if (input[fields[i]] !== undefined && isFinite(value) && value >= 0 && value <= 1000000000) result[fields[i]] = value;
|
||||
}
|
||||
if (event === "end") result.reason = ["left_match","cancelled_matchmaking","game_over","victory","new_match"].indexOf(String(input.reason)) >= 0 ? String(input.reason) : "other";
|
||||
if (event === "side") result.viewing_right = input.viewing_right === true;
|
||||
return result;
|
||||
}
|
||||
function rpcSubmitOnlineDiagnostics(ctx, logger, nk, payload) {
|
||||
if (!ctx.userId) throw new Error("authentication required");
|
||||
if (!payload || payload.length > 65000) return JSON.stringify({accepted:false,error:"too_large"});
|
||||
var request;
|
||||
try { request = JSON.parse(payload); } catch (error) { return JSON.stringify({accepted:false,error:"invalid_json"}); }
|
||||
var matchId = String(request.match_id || "");
|
||||
if (!matchId || matchId.length > 128 || !Array.isArray(request.records) || request.records.length > 160)
|
||||
return JSON.stringify({accepted:false,error:"invalid_payload"});
|
||||
var roundId = nk.sha256Hash(matchId).slice(0, 16);
|
||||
var participant = readStorageRecord(nk, DIAG_PARTICIPANT_COLLECTION, roundId, SYSTEM_USER_ID);
|
||||
var slots = participant && participant.value ? participant.value.participants || {} : {};
|
||||
var participantKey = nk.sha256Hash(matchId + ":" + ctx.userId);
|
||||
if (slots[participantKey] === undefined || Date.now() - Number(participant.value.created_at || 0) > DIAG_RETENTION_MS)
|
||||
return JSON.stringify({accepted:false,error:"not_participant"});
|
||||
var slot = Number(slots[participantKey]);
|
||||
if (slot !== 0 && slot !== 1) return JSON.stringify({accepted:false,error:"invalid_slot"});
|
||||
var records = request.records.map(sanitizeDiagnosticRecord).filter(function(record) { return record !== null; });
|
||||
if (!records.length) return JSON.stringify({accepted:false,error:"no_records"});
|
||||
var key = roundId + "-" + slot;
|
||||
nk.storageWrite([{collection:DIAG_REPORT_COLLECTION,key:key,userId:SYSTEM_USER_ID,
|
||||
value:{round_id:roundId,slot:slot,created_at:Date.now(),version:normalizeMetricLabel(request.version,"unknown"),records:records},
|
||||
permissionRead:0,permissionWrite:0}]);
|
||||
try { pruneOnlineDiagnostics(nk); } catch (error) { if (logger.warn) logger.warn("ONLINE DIAG cleanup failed: %s", error); }
|
||||
if (logger.info) logger.info("ONLINE DIAG received round=%s slot=%d records=%d", roundId, slot, records.length);
|
||||
return JSON.stringify({accepted:true,round_id:roundId,slot:slot});
|
||||
}
|
||||
function pruneOnlineDiagnostics(nk) {
|
||||
var collections = [DIAG_REPORT_COLLECTION, DIAG_PARTICIPANT_COLLECTION];
|
||||
var cutoff = Date.now() - DIAG_RETENTION_MS;
|
||||
for (var c = 0; c < collections.length; c++) {
|
||||
var cursor = "", pages = 0, stale = [];
|
||||
do {
|
||||
var page = nk.storageList(SYSTEM_USER_ID, collections[c], 100, cursor);
|
||||
var objects = Array.isArray(page) ? page : (page && Array.isArray(page.objects) ? page.objects : []);
|
||||
for (var i = 0; i < objects.length; i++) {
|
||||
if (Number(objects[i].value && objects[i].value.created_at || 0) < cutoff && objects[i].key)
|
||||
stale.push({collection:collections[c],key:objects[i].key,userId:SYSTEM_USER_ID});
|
||||
}
|
||||
cursor = page && !Array.isArray(page) ? String(page.cursor || "") : "";
|
||||
pages++;
|
||||
} while (cursor && pages < 10);
|
||||
for (var deleteIndex = 0; deleteIndex < stale.length; deleteIndex += 100)
|
||||
nk.storageDelete(stale.slice(deleteIndex, deleteIndex + 100));
|
||||
}
|
||||
}
|
||||
function rpcListOnlineDiagnostics(ctx, logger, nk, payload) {
|
||||
if (ctx.userId) throw new Error("server-to-server access required");
|
||||
try { pruneOnlineDiagnostics(nk); } catch (error) { if (logger.warn) logger.warn("ONLINE DIAG cleanup failed: %s", error); }
|
||||
var request;
|
||||
try { request = JSON.parse(payload || "{}"); } catch (error) { request = {}; }
|
||||
var requestedRound = String(request.round_id || "");
|
||||
if (requestedRound) {
|
||||
if (!/^[a-f0-9]{16}$/.test(requestedRound)) return JSON.stringify({reports:[],error:"invalid_round_id"});
|
||||
var pair = nk.storageRead([0,1].map(function(slot) { return {collection:DIAG_REPORT_COLLECTION,key:requestedRound+"-"+slot,userId:SYSTEM_USER_ID}; }));
|
||||
return JSON.stringify({reports:pair.map(function(record) { return record.value; })});
|
||||
}
|
||||
var reports = [], cursor = "", pages = 0, cutoff = Date.now() - DIAG_RETENTION_MS;
|
||||
do {
|
||||
var page = nk.storageList(SYSTEM_USER_ID, DIAG_REPORT_COLLECTION, 100, cursor);
|
||||
var objects = Array.isArray(page) ? page : (page && Array.isArray(page.objects) ? page.objects : []);
|
||||
for (var i = 0; i < objects.length; i++) {
|
||||
var object = objects[i], value = object && object.value ? object.value : {};
|
||||
if (Number(value.created_at || 0) >= cutoff) reports.push({round_id:value.round_id,slot:value.slot,
|
||||
created_at:value.created_at,version:value.version,record_count:Array.isArray(value.records)?value.records.length:0});
|
||||
}
|
||||
cursor = page && !Array.isArray(page) ? String(page.cursor || "") : "";
|
||||
pages++;
|
||||
} while (cursor && pages < 10);
|
||||
reports.sort(function(a,b) { return b.created_at - a.created_at; });
|
||||
return JSON.stringify({reports:reports.slice(0,100),truncated:!!cursor||reports.length>100});
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -654,6 +769,9 @@ function InitModule(ctx,logger,nk,initializer) {
|
||||
initializer.registerRpc("get_promo_unlocks",rpcGetPromoUnlocks);
|
||||
initializer.registerRpc("submit_run",rpcSubmitRun);
|
||||
initializer.registerRpc("update_display_name",rpcUpdateDisplayName);
|
||||
initializer.registerRpc("find_player_by_name",rpcFindPlayerByName);
|
||||
initializer.registerRpc("submit_online_diagnostics",rpcSubmitOnlineDiagnostics);
|
||||
initializer.registerRpc("list_online_diagnostics",rpcListOnlineDiagnostics);
|
||||
initializer.registerRpc("delete_account",rpcDeleteAccount);
|
||||
initializer.registerRpc("create_private_room",rpcCreatePrivateRoom);
|
||||
initializer.registerRpc("join_private_room",rpcJoinPrivateRoom);
|
||||
|
||||
Reference in new issue
Block a user