Track real player sessions and tower builds
This commit is contained in:
1 parent
8777f28595
commit
1294000348
8 files changed
+213
-8
No files matched your search
@@ -14,6 +14,8 @@ var primary_provider := "guest"
|
||||
var linked_identities: Array = []
|
||||
var highscores: Dictionary = {}
|
||||
var pending_submissions: Array = []
|
||||
var pending_player_events: Array = []
|
||||
var next_player_event_sequence := 0
|
||||
var created_at := 0
|
||||
var storage_enabled := true
|
||||
|
||||
@@ -34,6 +36,8 @@ func _load_or_create_profile() -> void:
|
||||
linked_identities = data.get("linked_identities", [])
|
||||
highscores = data.get("highscores", {})
|
||||
pending_submissions = data.get("pending_submissions", [])
|
||||
pending_player_events = data.get("pending_player_events", [])
|
||||
next_player_event_sequence = int(data.get("next_player_event_sequence", 0))
|
||||
created_at = int(data.get("created_at", 0))
|
||||
if player_id.is_empty():
|
||||
_create_guest_profile()
|
||||
@@ -50,6 +54,8 @@ func _create_guest_profile() -> void:
|
||||
linked_identities = []
|
||||
highscores = {}
|
||||
pending_submissions = []
|
||||
pending_player_events = []
|
||||
next_player_event_sequence = 0
|
||||
created_at = int(Time.get_unix_time_from_system())
|
||||
|
||||
|
||||
@@ -66,6 +72,8 @@ func _save_profile() -> void:
|
||||
"linked_identities": linked_identities,
|
||||
"highscores": highscores,
|
||||
"pending_submissions": pending_submissions,
|
||||
"pending_player_events": pending_player_events,
|
||||
"next_player_event_sequence": next_player_event_sequence,
|
||||
"created_at": created_at
|
||||
}))
|
||||
|
||||
@@ -91,6 +99,29 @@ func record_run(map_index: int, difficulty_index: int, endless: bool, reached_wa
|
||||
return true
|
||||
|
||||
|
||||
func queue_player_event(event_name: String, tower_type := -1) -> void:
|
||||
if event_name != "play_start" and event_name != "tower_build":
|
||||
return
|
||||
next_player_event_sequence += 1
|
||||
pending_player_events.append({
|
||||
"event": event_name,
|
||||
"tower_type": tower_type,
|
||||
"source": player_id,
|
||||
"sequence": next_player_event_sequence,
|
||||
"at": int(Time.get_unix_time_from_system() * 1000.0)
|
||||
})
|
||||
_save_profile()
|
||||
|
||||
|
||||
func mark_player_event_complete(sequence: int) -> void:
|
||||
for index in range(pending_player_events.size() - 1, -1, -1):
|
||||
var event = pending_player_events[index]
|
||||
if event is Dictionary and int(event.get("sequence", -1)) == sequence:
|
||||
pending_player_events.remove_at(index)
|
||||
_save_profile()
|
||||
return
|
||||
|
||||
|
||||
func attach_online_identity(online_user_id: String, online_username: String) -> void:
|
||||
if online_user_id.is_empty():
|
||||
return
|
||||
|
||||
@@ -1645,6 +1645,7 @@ func _toggle_coop_side() -> void:
|
||||
|
||||
func _start_coop_ai_game() -> void:
|
||||
online_manager.set_presence_status("playing")
|
||||
online_manager.record_local_play_start()
|
||||
_reset_developer_access()
|
||||
coop_mode = true
|
||||
coop_ai_mode = true
|
||||
@@ -1849,6 +1850,7 @@ func _refresh_leaderboard() -> void:
|
||||
|
||||
func _start_game() -> void:
|
||||
online_manager.set_presence_status("playing")
|
||||
online_manager.record_local_play_start()
|
||||
_reset_developer_access()
|
||||
coop_online_mode = false
|
||||
credits = int(_difficulty().start_credits)
|
||||
@@ -2149,6 +2151,7 @@ func _place_tower(position: Vector2) -> void:
|
||||
if not is_admin_tower:
|
||||
_spend_credits(0, int(data.cost))
|
||||
towers_built_this_run += 1
|
||||
online_manager.record_local_tower_build(selected_tower_type)
|
||||
towers.append({"pos": position, "type": selected_tower_type, "level": 1, "cooldown": 0.15, "angle": 0.0, "owner": 0, "target_priority": "shield" if selected_tower_type == 1 else "first"})
|
||||
selected_tower_index = towers.size() - 1
|
||||
selected_tower_type = -1
|
||||
|
||||
@@ -73,6 +73,8 @@ var pending_snapshot_payloads: Array[String] = []
|
||||
var friends: Array[Dictionary] = []
|
||||
var friend_statuses: Dictionary = {}
|
||||
var presence_status := ""
|
||||
var player_event_flush_busy := false
|
||||
var player_event_flush_again := false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -349,6 +351,7 @@ func _after_authentication() -> void:
|
||||
var parent_game = get_parent()
|
||||
await set_presence_status("playing" if parent_game != null and parent_game.game_mode in ["playing", "paused", "victory", "game_over", "endless_offer", "endless_result"] else "menu")
|
||||
authenticated.emit(session.user_id, session.username)
|
||||
await flush_pending_player_events()
|
||||
await flush_pending_scores()
|
||||
|
||||
|
||||
@@ -362,6 +365,44 @@ func _record_activity() -> void:
|
||||
await client.rpc_async(session, "record_activity", JSON.stringify(payload))
|
||||
|
||||
|
||||
func record_local_play_start() -> void:
|
||||
_queue_player_event("play_start")
|
||||
|
||||
|
||||
func record_local_tower_build(tower_type: int) -> void:
|
||||
if tower_type < 0 or tower_type >= 7:
|
||||
return
|
||||
_queue_player_event("tower_build", tower_type)
|
||||
|
||||
|
||||
func _queue_player_event(event_name: String, tower_type := -1) -> void:
|
||||
if not account_manager:
|
||||
return
|
||||
account_manager.queue_player_event(event_name, tower_type)
|
||||
if is_authenticated():
|
||||
flush_pending_player_events()
|
||||
|
||||
|
||||
func flush_pending_player_events() -> void:
|
||||
if player_event_flush_busy:
|
||||
player_event_flush_again = true
|
||||
return
|
||||
if not account_manager or not is_authenticated():
|
||||
return
|
||||
player_event_flush_busy = true
|
||||
while is_authenticated() and not account_manager.pending_player_events.is_empty():
|
||||
var event: Dictionary = account_manager.pending_player_events[0]
|
||||
var result = await client.rpc_async(session, "record_player_event", JSON.stringify(event))
|
||||
var parsed := _parse_rpc_result(result)
|
||||
if not bool(parsed.get("accepted", false)):
|
||||
break
|
||||
account_manager.mark_player_event_complete(int(event.get("sequence", -1)))
|
||||
player_event_flush_busy = false
|
||||
if player_event_flush_again:
|
||||
player_event_flush_again = false
|
||||
call_deferred("flush_pending_player_events")
|
||||
|
||||
|
||||
func _bind_socket_signals() -> void:
|
||||
socket.received_matchmaker_matched.connect(_on_matchmaker_matched)
|
||||
socket.received_match_state.connect(_on_match_state)
|
||||
|
||||
@@ -6,7 +6,7 @@ RequestExecutionLevel user
|
||||
!include "LogicLib.nsh"
|
||||
|
||||
!define PRODUCT_NAME "Void Bastion"
|
||||
!define PRODUCT_VERSION "0.1.0-alpha.15"
|
||||
!define PRODUCT_VERSION "0.1.0-alpha.16"
|
||||
!define PRODUCT_PUBLISHER "GerGames"
|
||||
!define PRODUCT_WEBSITE "https://void-bastion.gergames.de/"
|
||||
!define PRODUCT_EXE "Void-Bastion.exe"
|
||||
@@ -21,7 +21,7 @@ BrandingText "GerGames"
|
||||
ShowInstDetails show
|
||||
ShowUninstDetails show
|
||||
|
||||
VIProductVersion "0.1.0.15"
|
||||
VIProductVersion "0.1.0.16"
|
||||
VIAddVersionKey /LANG=1031 "ProductName" "${PRODUCT_NAME}"
|
||||
VIAddVersionKey /LANG=1031 "CompanyName" "${PRODUCT_PUBLISHER}"
|
||||
VIAddVersionKey /LANG=1031 "FileDescription" "${PRODUCT_NAME} Setup"
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ compatibility/default_parent_skeleton_in_mesh_instance_3d=true
|
||||
[application]
|
||||
|
||||
config/name="Void Bastion"
|
||||
config/version="0.1.0-alpha.15"
|
||||
config/version="0.1.0-alpha.16"
|
||||
run/main_scene="res://main.tscn"
|
||||
config/features=PackedStringArray("4.7")
|
||||
config/icon="res://assets/app_icon.png"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
var GAME_VERSION = "0.26";
|
||||
var GAME_VERSION = "0.27";
|
||||
var MAP_COUNT = 7;
|
||||
var DIFFICULTY_COUNT = 3;
|
||||
var VALID_MODES = ["mission", "endless", "coop_ai", "coop_online"];
|
||||
@@ -6,6 +6,7 @@ var MATCH_MODULE = "void_bastion_coop";
|
||||
var ROOM_COLLECTION = "private_rooms";
|
||||
var ACTIVITY_COLLECTION = "player_activity";
|
||||
var ACTIVITY_KEY = "summary";
|
||||
var INTERNAL_TEST_USERNAME_PATTERN = "^(godot_[ab]_[0-9]+|social_[ab]_[0-9]+|GODOT-[AB]|SYSTEMTEST)$";
|
||||
var STATS_COLLECTION = "internal_stats";
|
||||
var STATS_KEY = "global";
|
||||
var PROMO_CODE_COLLECTION = "unlock_codes";
|
||||
@@ -233,7 +234,19 @@ function rpcRecordActivity(ctx, logger, nk, payload) {
|
||||
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}]);
|
||||
for (var attempt=0;attempt<3;attempt++) {
|
||||
playerRecord=readStorageRecord(nk,ACTIVITY_COLLECTION,ACTIVITY_KEY,ctx.userId);
|
||||
var activity=playerRecord&&playerRecord.value?JSON.parse(JSON.stringify(playerRecord.value)):{};
|
||||
activity.first_seen=Number(activity.first_seen||now);
|
||||
activity.last_seen=now;
|
||||
activity.last_seen_day=day;
|
||||
activity.platform=platform;
|
||||
activity.version=version;
|
||||
try {
|
||||
nk.storageWrite([{collection:ACTIVITY_COLLECTION,key:ACTIVITY_KEY,userId:ctx.userId,value:activity,permissionRead:0,permissionWrite:0,version:playerRecord&&playerRecord.version?playerRecord.version:"*"}]);
|
||||
return JSON.stringify({accepted:true});
|
||||
} catch (error) { if (attempt===2) throw error; }
|
||||
}
|
||||
return JSON.stringify({accepted:true});
|
||||
}
|
||||
function rpcPrivateStats(ctx, logger, nk, payload) {
|
||||
@@ -242,6 +255,75 @@ function rpcPrivateStats(ctx, logger, nk, payload) {
|
||||
var stats = record && record.value ? record.value : emptyStats(Date.now());
|
||||
return JSON.stringify(stats);
|
||||
}
|
||||
function updatePlayerActivity(nk,userId,event,type,source,sequence,eventAt) {
|
||||
var now=Date.now();
|
||||
for (var attempt=0;attempt<3;attempt++) {
|
||||
var record=readStorageRecord(nk,ACTIVITY_COLLECTION,ACTIVITY_KEY,userId);
|
||||
var activity=record&&record.value?JSON.parse(JSON.stringify(record.value)):{};
|
||||
var sequences=activity.event_sequences&&typeof activity.event_sequences==="object"?activity.event_sequences:{};
|
||||
if (source && sequence<=Number(sequences[source]||0)) return {accepted:true,duplicate:true};
|
||||
activity.first_seen=Number(activity.first_seen||now);
|
||||
activity.last_seen=now;
|
||||
activity.last_seen_day=utcDay(now);
|
||||
activity.last_played=Math.max(Number(activity.last_played||0),Math.min(now,Math.max(0,Number(eventAt||now))));
|
||||
if (event==="tower_build") {
|
||||
var builds=Array.isArray(activity.tower_builds)?activity.tower_builds.slice(0,TOWERS.length):[];
|
||||
while (builds.length<TOWERS.length) builds.push(0);
|
||||
builds[type]=Math.max(0,Math.floor(Number(builds[type]||0)))+1;
|
||||
activity.tower_builds=builds;
|
||||
activity.tower_build_total=Math.max(0,Math.floor(Number(activity.tower_build_total||0)))+1;
|
||||
}
|
||||
if (source) { sequences[source]=sequence; activity.event_sequences=sequences; }
|
||||
try {
|
||||
nk.storageWrite([{collection:ACTIVITY_COLLECTION,key:ACTIVITY_KEY,userId:userId,value:activity,permissionRead:0,permissionWrite:0,version:record&&record.version?record.version:"*"}]);
|
||||
return {accepted:true,duplicate:false};
|
||||
} catch (error) { if (attempt===2) throw error; }
|
||||
}
|
||||
}
|
||||
function rpcRecordPlayerEvent(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 event=String(request.event||""), type=Math.floor(Number(request.tower_type));
|
||||
var source=String(request.source||""),sequence=Math.floor(Number(request.sequence));
|
||||
if (["play_start","tower_build"].indexOf(event)<0 || (event==="tower_build"&&(type<0||type>=TOWERS.length)) ||
|
||||
!/^[A-Za-z0-9_-]{8,64}$/.test(source) || !isFinite(sequence) || sequence<1 || sequence>1000000000)
|
||||
return JSON.stringify({accepted:false,error:"invalid_event"});
|
||||
var eventAt=Number(request.at||Date.now());
|
||||
return JSON.stringify(updatePlayerActivity(nk,ctx.userId,event,type,source,sequence,eventAt));
|
||||
}
|
||||
function isInternalTestUsername(username) { return new RegExp(INTERNAL_TEST_USERNAME_PATTERN,"i").test(String(username||"")); }
|
||||
function playerDirectoryRow(row) {
|
||||
return {user_id:String(row.id||""),name:String(row.display_name||row.username||"PILOT").substring(0,32),
|
||||
registered_at:Number(row.registered_at||0),last_seen:Number(row.last_seen||0),last_played:Number(row.last_played||0),
|
||||
tower_build_total:Number(row.tower_build_total||0)};
|
||||
}
|
||||
function rpcListRegisteredPlayers(ctx,logger,nk,payload) {
|
||||
if (ctx.userId) throw new Error("server-to-server access required");
|
||||
var request;
|
||||
try { request=JSON.parse(payload||"{}"); } catch (error) { request={}; }
|
||||
var page=Math.max(0,Math.min(10000,Math.floor(Number(request.page||0))||0));
|
||||
var countRows=nk.sqlQuery("SELECT count(*) AS total, count(*) FILTER (WHERE a.value->>'last_seen_day'=$1) AS active_today FROM users u LEFT JOIN storage a ON a.user_id=u.id AND a.collection='player_activity' AND a.key='summary' WHERE u.username<>'' AND u.username !~* $2",[utcDay(Date.now()),INTERNAL_TEST_USERNAME_PATTERN]);
|
||||
var rows=nk.sqlQuery("SELECT u.id,u.username,u.display_name,extract(epoch from u.create_time)*1000 AS registered_at,coalesce((a.value->>'last_seen')::bigint,0) AS last_seen,coalesce((a.value->>'last_played')::bigint,0) AS last_played,coalesce((a.value->>'tower_build_total')::bigint,0) AS tower_build_total FROM users u LEFT JOIN storage a ON a.user_id=u.id AND a.collection='player_activity' AND a.key='summary' WHERE u.username<>'' AND u.username !~* $1 ORDER BY last_seen DESC,u.create_time DESC LIMIT 50 OFFSET $2",[INTERNAL_TEST_USERNAME_PATTERN,page*50]);
|
||||
return JSON.stringify({page:page,page_size:50,total:Number(countRows[0]&&countRows[0].total||0),active_today:Number(countRows[0]&&countRows[0].active_today||0),players:rows.map(playerDirectoryRow)});
|
||||
}
|
||||
function rpcGetRegisteredPlayer(ctx,logger,nk,payload) {
|
||||
if (ctx.userId) throw new Error("server-to-server access required");
|
||||
var request;
|
||||
try { request=JSON.parse(payload||"{}"); } catch (error) { request={}; }
|
||||
var userId=String(request.user_id||"");
|
||||
if (!/^[0-9a-f-]{36}$/i.test(userId)) return JSON.stringify({found:false});
|
||||
var rows=nk.sqlQuery("SELECT id,username,display_name,extract(epoch from create_time)*1000 AS registered_at FROM users WHERE id=$1::uuid LIMIT 1",[userId]);
|
||||
if (!rows.length || isInternalTestUsername(rows[0].username) || !rows[0].username) return JSON.stringify({found:false});
|
||||
var activityRecord=readStorageRecord(nk,ACTIVITY_COLLECTION,ACTIVITY_KEY,userId);
|
||||
var activity=activityRecord&&activityRecord.value?activityRecord.value:{};
|
||||
var builds=Array.isArray(activity.tower_builds)?activity.tower_builds.slice(0,TOWERS.length):[];
|
||||
while (builds.length<TOWERS.length) builds.push(0);
|
||||
return JSON.stringify({found:true,player:{user_id:userId,name:String(rows[0].display_name||rows[0].username).substring(0,32),
|
||||
registered_at:Number(rows[0].registered_at||0),last_seen:Number(activity.last_seen||0),last_played:Number(activity.last_played||0),
|
||||
platform:String(activity.platform||""),version:String(activity.version||""),tower_builds:builds,
|
||||
tower_build_total:Number(activity.tower_build_total||0)}});
|
||||
}
|
||||
function rpcCreateUnlockCode(ctx, logger, nk, payload) {
|
||||
if (ctx.userId) throw new Error("server-to-server access required");
|
||||
var request;
|
||||
@@ -738,7 +820,13 @@ function updateWave(state,delta,dispatcher) {
|
||||
function handleCommand(nk,dispatcher,state,message) {
|
||||
var slot=state.slots[message.sender.userId]; if (slot===undefined) return;
|
||||
var payload=parseMessage(nk,message); if (payload===null) return; var changed=false;
|
||||
if (message.opCode===OP_BUILD) changed=handleBuild(state,slot,payload,message.sender.userId);
|
||||
if (message.opCode===OP_BUILD) {
|
||||
changed=handleBuild(state,slot,payload,message.sender.userId);
|
||||
if (changed&&Math.floor(Number(payload.tower_type))<TOWERS.length) {
|
||||
try { updatePlayerActivity(nk,message.sender.userId,"tower_build",Math.floor(Number(payload.tower_type)),"",0,Date.now()); }
|
||||
catch (telemetryError) { /* Telemetry must never reject an accepted tower build. */ }
|
||||
}
|
||||
}
|
||||
else if (message.opCode===OP_UPGRADE) changed=handleUpgrade(state,slot,payload);
|
||||
else if (message.opCode===OP_SELL) changed=handleSell(state,slot,payload);
|
||||
else if (message.opCode===OP_START_WAVE) changed=startWave(state);
|
||||
@@ -749,7 +837,13 @@ function handleCommand(nk,dispatcher,state,message) {
|
||||
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;
|
||||
if (allReady) { state.started=true; state.pausedForReconnect=false; dispatcher.matchLabelUpdate(matchLabel(state)); broadcastEvent(dispatcher,"match_started",{}); }
|
||||
if (allReady) {
|
||||
state.started=true; state.pausedForReconnect=false; dispatcher.matchLabelUpdate(matchLabel(state)); broadcastEvent(dispatcher,"match_started",{});
|
||||
for (var playerIndex=0;playerIndex<ids.length;playerIndex++) {
|
||||
try { updatePlayerActivity(nk,ids[playerIndex],"play_start",-1,"",0,Date.now()); }
|
||||
catch (telemetryError) { /* Telemetry must never stop the match. */ }
|
||||
}
|
||||
}
|
||||
changed=true;
|
||||
}
|
||||
else if (message.opCode===OP_DEV_AUTH&&state.started&&!state.gameOver) {
|
||||
@@ -885,7 +979,10 @@ 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("record_player_event",rpcRecordPlayerEvent);
|
||||
initializer.registerRpc("private_stats",rpcPrivateStats);
|
||||
initializer.registerRpc("list_registered_players",rpcListRegisteredPlayers);
|
||||
initializer.registerRpc("get_registered_player",rpcGetRegisteredPlayer);
|
||||
initializer.registerRpc("get_game_balance",rpcGetGameBalance);
|
||||
initializer.registerRpc("set_game_balance",rpcSetGameBalance);
|
||||
initializer.registerRpc("create_unlock_code",rpcCreateUnlockCode);
|
||||
|
||||
@@ -231,6 +231,36 @@ 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/);
|
||||
const metricSource = "guest_0123456789abcdef0123456789abcdef";
|
||||
assert.deepStrictEqual(JSON.parse(context.rpcRecordPlayerEvent({ userId: "pilot-metric-a" }, logger, metricNk,
|
||||
JSON.stringify({ event: "play_start", source: metricSource, sequence: 1, at: Date.now() }))), { accepted: true, duplicate: false });
|
||||
assert.deepStrictEqual(JSON.parse(context.rpcRecordPlayerEvent({ userId: "pilot-metric-a" }, logger, metricNk,
|
||||
JSON.stringify({ event: "tower_build", tower_type: 1, source: metricSource, sequence: 2, at: Date.now() }))), { accepted: true, duplicate: false });
|
||||
assert.deepStrictEqual(JSON.parse(context.rpcRecordPlayerEvent({ userId: "pilot-metric-a" }, logger, metricNk,
|
||||
JSON.stringify({ event: "tower_build", tower_type: 1, source: metricSource, sequence: 2, at: Date.now() }))), { accepted: true, duplicate: true });
|
||||
assert.strictEqual(JSON.parse(context.rpcRecordPlayerEvent({ userId: "pilot-metric-a" }, logger, metricNk,
|
||||
JSON.stringify({ event: "tower_build", tower_type: 7, source: metricSource, sequence: 3 }))).accepted, false, "Admin towers must not enter real tower statistics.");
|
||||
context.rpcRecordActivity({ userId: "pilot-metric-a" }, logger, metricNk, JSON.stringify({ platform: "Windows", version: "0.1.0-alpha.16" }));
|
||||
const metricActivity = metricStorage.get("player_activity:pilot-metric-a:summary").value;
|
||||
assert.strictEqual(metricActivity.tower_builds[1], 1, "Repeated telemetry must not double-count towers.");
|
||||
assert.ok(metricActivity.last_played > 0, "Game sessions should track when the player last played.");
|
||||
assert.strictEqual(context.isInternalTestUsername("godot_a_2323883"), true);
|
||||
assert.strictEqual(context.isInternalTestUsername("social_b_2289087"), true);
|
||||
assert.strictEqual(context.isInternalTestUsername("Emporex"), false);
|
||||
const directoryNk = {
|
||||
sqlQuery(sql) {
|
||||
if (sql.includes("count(*) AS total")) return [{ total: 7, active_today: 3 }];
|
||||
if (sql.includes("WHERE id=$1::uuid")) return [{ id: "11111111-1111-1111-1111-111111111111", username: "pilot_real", display_name: "Real Pilot", registered_at: 1000 }];
|
||||
return [{ id: "11111111-1111-1111-1111-111111111111", username: "pilot_real", display_name: "Real Pilot", registered_at: 1000, last_seen: 2000, last_played: 3000, tower_build_total: 2 }];
|
||||
},
|
||||
storageRead() { return [{ value: { tower_builds: [0, 2, 0, 0, 0, 0, 0], tower_build_total: 2, last_seen: 2000, last_played: 3000 } }]; }
|
||||
};
|
||||
assert.strictEqual(JSON.parse(context.rpcListRegisteredPlayers({}, logger, directoryNk, "{}")).total, 7);
|
||||
assert.strictEqual(JSON.parse(context.rpcGetRegisteredPlayer({}, logger, directoryNk,
|
||||
JSON.stringify({ user_id: "11111111-1111-1111-1111-111111111111" }))).player.tower_builds[1], 2);
|
||||
assert.throws(() => context.rpcListRegisteredPlayers({ userId: "pilot-a" }, logger, directoryNk, "{}"), /server-to-server/);
|
||||
nk.storageRead = metricNk.storageRead.bind(metricNk);
|
||||
nk.storageWrite = metricNk.storageWrite.bind(metricNk);
|
||||
|
||||
const promoStorage = new Map();
|
||||
let promoVersion = 0;
|
||||
@@ -376,6 +406,8 @@ state = context.matchLoop({}, logger, nk, dispatcher, 5, state, [
|
||||
assert.strictEqual(state.towers.length, 2);
|
||||
assert.strictEqual(state.credits[0], 310);
|
||||
assert.strictEqual(state.credits[1], 250);
|
||||
assert.strictEqual(metricStorage.get("player_activity:pilot-a:summary").value.tower_builds[0], 1, "Accepted online tower builds should be counted on the server.");
|
||||
assert.strictEqual(metricStorage.get("player_activity:pilot-b:summary").value.tower_builds[1], 1);
|
||||
|
||||
state = context.matchLoop({}, logger, nk, dispatcher, 51, state,
|
||||
[message(presenceA, 10, { tower_type: 7, x: 2200, y: 600 }),
|
||||
@@ -402,6 +434,7 @@ assert.strictEqual(state.credits[0], 310, "Authorized test energy removal should
|
||||
state = context.matchLoop({}, logger, nk, dispatcher, 56, state,
|
||||
[message(presenceA, 10, { tower_type: 7, x: 2200, y: 600 })]).state;
|
||||
assert.ok(state.towers.some(tower => tower.type === 7 && tower.owner === 0), "Authorized player may build the admin turret anywhere on the co-op map.");
|
||||
assert.strictEqual(metricStorage.get("player_activity:pilot-a:summary").value.tower_build_total, 1, "Admin towers are excluded from player statistics.");
|
||||
const adminCombatState = {
|
||||
balance: state.balance,
|
||||
towers: [{ id: 99, type: 7, x: 2200, y: 600, owner: 0, level: 1, cooldown: 0, angle: 0 }],
|
||||
|
||||
@@ -32,7 +32,7 @@ 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.25", "Der Server muss die aktuelle Runtime 0.25 geladen haben: %s" % info_data)
|
||||
assert(info_data.get("version") == "0.27", "Der Server muss die aktuelle Runtime 0.27 geladen haben: %s" % info_data)
|
||||
|
||||
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)
|
||||
|
||||
Reference in new issue
Block a user