local nk = require("nakama") local utils = require("lua.utils") local leaderboard = {} function leaderboard.rpc_get_leaderboard_stats(context, payload) local status, records_or_err = pcall(nk.leaderboard_records_list, "global_high_score", nil, 50, nil) if not status then nk.logger_error("Failed to get native leaderboard stats: " .. tostring(records_or_err)) return nk.json_encode({ leaderboard = {} }) end local leaderboardData = {} local ownerRecords = records_or_err.records or {} for _, record in pairs(ownerRecords) do local metadata = {} if record.metadata then local s, m = pcall(nk.json_decode, record.metadata) if s then metadata = m end end table.insert(leaderboardData, { user_id = record.owner_id, username = record.username, display_name = record.username, -- Native lua leaderboard returns owner_id, username, score, subscore, num_score, max_num_score, metadata, create_time, update_time avatar_url = metadata.avatar_url or "", loadout_character = metadata.loadout_character or "Copper", high_score = record.score or 0, games_played = metadata.games_played or 0, games_won = metadata.games_won or 0 }) end return nk.json_encode({ leaderboard = leaderboardData }) end function leaderboard.rpc_submit_score(context, payload) if not context.user_id then error("Not authenticated") end local request = nk.json_decode(payload or "{}") local score = tonumber(request.score) or 0 local account = nk.account_get_id(context.user_id) local metadata = { games_played = request.games_played or 0, games_won = request.games_won or 0, avatar_url = request.avatar_url or account.user.avatar_url or "", loadout_character = request.loadout_character or "Copper" } local status, err = pcall(nk.leaderboard_record_write, "global_high_score", context.user_id, account.user.username, score, 0, metadata ) if not status then nk.logger_error("Failed to submit score for " .. context.user_id .. ": " .. tostring(err)) error("Failed to submit score") end nk.logger_info("Score submitted for user " .. context.user_id .. ": " .. score) return nk.json_encode({ success = true }) end function leaderboard.rpc_sync_leaderboard(context, payload) if not context.user_id then error("Not authenticated") end local status, result = pcall(nk.storage_list, nil, "stats", 100, "") if not status then error("Sync failed: " .. tostring(result)) end local statsObjects = result.objects or {} local userGroup = {} for _, obj in ipairs(statsObjects) do local userId = obj.user_id local value = obj.value if not userGroup[userId] then userGroup[userId] = { high_score = value.high_score or 0, games_played = value.games_played or 0, games_won = value.games_won or 0, avatar_url = value.avatar_url or "", loadout_character = value.loadout_character or "" } else userGroup[userId].high_score = math.max(userGroup[userId].high_score, value.high_score or 0) userGroup[userId].games_played = math.max(userGroup[userId].games_played, value.games_played or 0) userGroup[userId].games_won = math.max(userGroup[userId].games_won, value.games_won or 0) end if obj.key == "game_stats" or userGroup[userId].avatar_url == "" then if value.avatar_url then userGroup[userId].avatar_url = value.avatar_url end if value.loadout_character then userGroup[userId].loadout_character = value.loadout_character end end end local statusProf, profileResult = pcall(nk.storage_list, nil, "profiles", 100, "") if statusProf and profileResult and profileResult.objects then for _, obj in ipairs(profileResult.objects) do if obj.key == "profile" then local userId = obj.user_id local value = obj.value if not userGroup[userId] then userGroup[userId] = { high_score = 0, games_played = 0, games_won = 0, avatar_url = "", loadout_character = "" } end if value.avatar_url and userGroup[userId].avatar_url == "" then userGroup[userId].avatar_url = value.avatar_url end if value.loadout_character and userGroup[userId].loadout_character == "" then userGroup[userId].loadout_character = value.loadout_character end end end end local count = 0 local debugLogs = {} for uid, stats in pairs(userGroup) do local s, err = pcall(function() local account = nk.account_get_id(uid) local avatar = stats.avatar_url if not avatar or avatar == "" then avatar = account.user.avatar_url end if not avatar or avatar == "" then avatar = "res://assets/graphics/character_selection/sc_characters/sc_copper.png" end local meta = { games_played = stats.games_played or 0, games_won = stats.games_won or 0, avatar_url = avatar, loadout_character = stats.loadout_character or "Copper" } nk.leaderboard_record_write("global_high_score", uid, account.user.username, stats.high_score, 0, meta) count = count + 1 end) if not s then table.insert(debugLogs, "Error user " .. uid .. ": " .. tostring(err)) nk.logger_error("Failed to sync record for " .. uid .. ": " .. tostring(err)) end end nk.logger_info("Synced " .. count .. " records to leaderboard by user " .. context.user_id) return nk.json_encode({ success = true, synced = count, objects_found = #statsObjects, debug = debugLogs }) end function leaderboard.rpc_reset_stats(context, payload) if not context.user_id then error("Not authenticated") end pcall(nk.leaderboard_record_delete, "global_high_score", context.user_id) local zeros = { games_played = 0, games_won = 0, high_score = 0, total_kills = 0, total_deaths = 0 } nk.storage_write({{ collection = "stats", key = "game_stats", user_id = context.user_id, value = zeros, permission_read = 2, permission_write = 1 }}) return nk.json_encode({ success = true }) end function leaderboard.rpc_admin_update_stats(context, payload) utils.require_admin(context) local request = nk.json_decode(payload) local targetUserId = request.user_id local stats = request.stats if not targetUserId or not stats then error("User ID and stats are required") end nk.storage_write({{ collection = "stats", key = "game_stats", user_id = targetUserId, value = stats, permission_read = 1, permission_write = 0 }}) local account = nk.account_get_id(targetUserId) local score = stats.high_score or 0 local metadata = { games_played = stats.games_played or 0, games_won = stats.games_won or 0, avatar_url = account.user.avatar_url or "", loadout_character = stats.loadout_character or "Copper" } nk.leaderboard_record_write("global_high_score", targetUserId, account.user.username, score, 0, metadata) nk.logger_info("Stats updated for user " .. targetUserId .. " by admin " .. context.user_id) return nk.json_encode({ success = true }) end function leaderboard.rpc_admin_delete_stats(context, payload) utils.require_admin(context) local request = nk.json_decode(payload) local targetUserId = request.user_id if not targetUserId then error("User ID is required") end nk.storage_delete({ { collection = "stats", key = "stats", user_id = targetUserId }, { collection = "stats", key = "game_stats", user_id = targetUserId } }) pcall(nk.leaderboard_record_delete, "global_high_score", targetUserId) nk.logger_info("Stats deleted for user " .. targetUserId .. " by admin " .. context.user_id) return nk.json_encode({ success = true }) end function leaderboard.rpc_admin_sync_leaderboard(context, payload) utils.require_admin(context) return leaderboard.rpc_sync_leaderboard(context, payload) end nk.register_rpc(leaderboard.rpc_get_leaderboard_stats, "get_leaderboard_stats") nk.register_rpc(leaderboard.rpc_submit_score, "submit_score") nk.register_rpc(leaderboard.rpc_sync_leaderboard, "sync_leaderboard") nk.register_rpc(leaderboard.rpc_reset_stats, "reset_stats") nk.register_rpc(leaderboard.rpc_admin_update_stats, "admin_update_stats") nk.register_rpc(leaderboard.rpc_admin_delete_stats, "admin_delete_stats") nk.register_rpc(leaderboard.rpc_admin_sync_leaderboard, "admin_sync_leaderboard") -- Create default native leaderboard -- id: "global_high_score", authoritative: true, sort: "desc", operator: "best", reset: None pcall(nk.leaderboard_create, "global_high_score", true, "desc", "best", nil, {}) nk.logger_info("LUA TEST: leaderboard module loaded") return leaderboard