Files
tekton/server/nakama/lua/leaderboard.lua
T
2026-05-19 17:30:29 +08:00

250 lines
9.2 KiB
Lua

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 ipairs(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