feat: 2.3.2

This commit is contained in:
2026-05-19 17:30:29 +08:00
parent 7ca11c6534
commit 8430d1054e
39 changed files with 6581 additions and 738 deletions
+355
View File
@@ -0,0 +1,355 @@
local nk = require("nakama")
local utils = require("lua.utils")
local admin = {}
function admin.rpc_admin_kick_player(context, payload)
local request = nk.json_decode(payload)
utils.require_admin_or_host(context, request.match_id)
if request.user_id == context.user_id then
error("Cannot kick yourself")
end
local status, err = pcall(nk.match_signal, request.match_id, nk.json_encode({
action = "kick",
user_id = request.user_id,
reason = request.reason or "Kicked by admin"
}))
if not status then
nk.logger_error("Failed to kick player: " .. tostring(err))
error("Failed to kick player")
end
nk.logger_info("Player " .. request.user_id .. " kicked from match " .. request.match_id .. " by " .. context.user_id)
return nk.json_encode({ success = true })
end
function admin.rpc_admin_ban_player(context, payload)
local request = nk.json_decode(payload)
utils.require_admin(context)
if request.user_id == context.user_id then
error("Cannot ban yourself")
end
local status, targetAccount = pcall(nk.account_get_id, request.user_id)
if not status then error("Target account not found") end
local metadata = {}
if targetAccount.user.metadata then
status, metadata = pcall(nk.json_decode, targetAccount.user.metadata)
if not status then metadata = {} end
end
if utils.ADMIN_ROLES[metadata.role or ""] then
error("Cannot ban an admin")
end
local banExpires = nil
if request.duration_hours and request.duration_hours > 0 then
-- Unix time in seconds for lua, Nakama might expect ISO string or unix
banExpires = os.time() + (request.duration_hours * 60 * 60)
end
metadata.banned = true
metadata.ban_reason = request.reason or "Banned by admin"
if banExpires then metadata.ban_expires = banExpires end
nk.account_update_id(request.user_id, nil, nil, nil, nil, nil, nil, nk.json_encode(metadata))
if request.match_id then
pcall(nk.match_signal, request.match_id, nk.json_encode({
action = "kick",
user_id = request.user_id,
reason = "Banned: " .. (request.reason or "")
}))
end
local banRecord = {
user_id = request.user_id,
username = targetAccount.user.username,
banned_by = context.user_id,
banned_at = os.time(),
reason = request.reason,
expires = banExpires
}
nk.storage_write({{
collection = "bans",
key = request.user_id,
user_id = "00000000-0000-0000-0000-000000000000",
value = banRecord,
permission_read = 2,
permission_write = 0
}})
nk.logger_warn("Player " .. request.user_id .. " banned by " .. context.user_id)
return nk.json_encode({ success = true, ban = banRecord })
end
function admin.rpc_admin_unban_player(context, payload)
local request = nk.json_decode(payload)
utils.require_admin(context)
local status, targetAccount = pcall(nk.account_get_id, request.user_id)
if not status then error("Target account not found") end
local metadata = {}
if targetAccount.user.metadata then
status, metadata = pcall(nk.json_decode, targetAccount.user.metadata)
if not status then metadata = {} end
end
metadata.banned = nil
metadata.ban_reason = nil
metadata.ban_expires = nil
nk.account_update_id(request.user_id, nil, nil, nil, nil, nil, nil, nk.json_encode(metadata))
nk.storage_delete({{
collection = "bans",
key = request.user_id,
user_id = "00000000-0000-0000-0000-000000000000"
}})
nk.logger_info("Player " .. request.user_id .. " unbanned by " .. context.user_id)
return nk.json_encode({ success = true })
end
function admin.rpc_admin_get_ban_list(context, payload)
utils.require_admin(context)
local status, result = pcall(nk.storage_list, "00000000-0000-0000-0000-000000000000", "bans", 100)
local bans = {}
if status and result and result.objects then
for _, obj in ipairs(result.objects) do
table.insert(bans, obj.value)
end
end
return nk.json_encode({ bans = bans })
end
function admin.rpc_admin_get_server_stats(context, payload)
local request = nk.json_decode(payload or "{}")
if request.match_id then
utils.require_admin_or_host(context, request.match_id)
else
utils.require_admin(context)
end
local matches = nk.match_list(100, true)
local activeMatchCount = matches and #matches or 0
local totalPlayers = 0
if matches then
for _, match in ipairs(matches) do
totalPlayers = totalPlayers + (match.size or 0)
end
end
local stats = {
active_matches = activeMatchCount,
total_players = totalPlayers,
server_time = os.time()
}
if request.match_id then
local status, match = pcall(nk.match_get, request.match_id)
if status and match then
stats.match = {
id = match.match_id,
size = match.size,
tick_rate = match.tick_rate,
authoritative = match.authoritative
}
end
end
return nk.json_encode(stats)
end
function admin.rpc_admin_end_match(context, payload)
local request = nk.json_decode(payload)
utils.require_admin_or_host(context, request.match_id)
nk.match_signal(request.match_id, nk.json_encode({
action = "end_match",
reason = request.reason or "Ended by admin"
}))
nk.logger_info("Match " .. request.match_id .. " ended by " .. context.user_id)
return nk.json_encode({ success = true })
end
function admin.rpc_admin_set_user_role(context, payload)
local request = nk.json_decode(payload)
local callerAccount = nk.account_get_id(context.user_id)
local callerMetadata = nk.json_decode(callerAccount.user.metadata or "{}")
if callerMetadata.role ~= "owner" then
error("Only owners can modify user roles")
end
local validRoles = { player = true, moderator = true, admin = true }
if not validRoles[request.role] then
error("Invalid role")
end
local targetAccount = nk.account_get_id(request.user_id)
local metadata = nk.json_decode(targetAccount.user.metadata or "{}")
metadata.role = request.role
nk.account_update_id(request.user_id, nil, nil, nil, nil, nil, nil, nk.json_encode(metadata))
nk.logger_info("User " .. request.user_id .. " role set to " .. request.role .. " by " .. context.user_id)
return nk.json_encode({ success = true, role = request.role })
end
function admin.rpc_admin_topup_gold(context, payload)
utils.require_admin(context)
nk.wallet_update(context.user_id, { gold = 999999 })
nk.logger_info("Admin gold top-up applied for user " .. context.user_id)
return nk.json_encode({ success = true, gold_added = 999999 })
end
function admin.rpc_admin_clear_global_chat(context, payload)
utils.require_admin(context)
local request = nk.json_decode(payload or "{}")
local channelId = request.channel_id or ""
if channelId == "" then
error("channel_id is required. Pass the channel ID from the client.")
end
local deleted = 0
local cursor = ""
repeat
local status, result = pcall(nk.channel_messages_list, channelId, 100, false, cursor)
if not status then break end
local messages = result.messages or {}
for _, msg in ipairs(messages) do
pcall(nk.channel_message_remove, channelId, msg.message_id)
deleted = deleted + 1
end
cursor = result.next_cursor or ""
until cursor == ""
nk.logger_info("[AdminClearGlobalChat] Deleted " .. deleted .. " messages by " .. context.user_id)
return nk.json_encode({ success = true, deleted = deleted })
end
function admin.rpc_admin_list_users(context, payload)
utils.require_admin(context)
local users = {}
local sql = "SELECT id, username, display_name, metadata, create_time FROM users WHERE id != '00000000-0000-0000-0000-000000000000' ORDER BY create_time DESC LIMIT 500"
local status, rows = pcall(nk.sql_query, sql, {})
if status and rows then
for _, row in ipairs(rows) do
local metadata = {}
if row.metadata then
local s, m = pcall(nk.json_decode, row.metadata)
if s then metadata = m end
end
table.insert(users, {
user_id = row.id,
username = row.username or "",
display_name = row.display_name or row.username or "",
create_time = row.create_time,
role = metadata.role or "player",
banned = metadata.banned or false,
ban_reason = metadata.ban_reason or ""
})
end
end
return nk.json_encode({ users = users, count = #users })
end
function admin.rpc_admin_delete_users(context, payload)
utils.require_admin(context)
local request = nk.json_decode(payload)
local userIds = request.user_ids or {}
if #userIds == 0 then error("No user IDs provided") end
for _, uid in ipairs(userIds) do
if uid == context.user_id then
error("Cannot delete your own account")
end
end
local deleted = {}
local failed = {}
for _, uid in ipairs(userIds) do
local status, err = pcall(function()
local account = nk.account_get_id(uid)
local meta = {}
if account.user.metadata then
local s, m = pcall(nk.json_decode, account.user.metadata)
if s then meta = m end
end
if meta.role == "admin" or meta.role == "moderator" or meta.role == "owner" then
error("Cannot delete admin account")
end
nk.account_delete_id(uid, false)
table.insert(deleted, uid)
nk.logger_warn("User " .. uid .. " deleted by " .. context.user_id)
end)
if not status then
table.insert(failed, { user_id = uid, reason = tostring(err) })
end
end
return nk.json_encode({ success = true, deleted = deleted, failed = failed })
end
function admin.rpc_admin_get_player_list(context, payload)
local request = nk.json_decode(payload)
utils.require_admin_or_host(context, request.match_id)
local status, match = pcall(nk.match_get, request.match_id)
if not status or not match then
error("Match not found")
end
-- Get player details
-- Note: In actual implementation, you'd need to track presences
-- This is a simplified version - adjust based on your match handler
local players = {}
return nk.json_encode({ players = players })
end
-- Register RPCs
nk.register_rpc(admin.rpc_admin_kick_player, "admin_kick_player")
nk.register_rpc(admin.rpc_admin_ban_player, "admin_ban_player")
nk.register_rpc(admin.rpc_admin_unban_player, "admin_unban_player")
nk.register_rpc(admin.rpc_admin_get_ban_list, "admin_get_ban_list")
nk.register_rpc(admin.rpc_admin_get_server_stats, "admin_get_server_stats")
nk.register_rpc(admin.rpc_admin_get_player_list, "admin_get_player_list")
nk.register_rpc(admin.rpc_admin_end_match, "admin_end_match")
nk.register_rpc(admin.rpc_admin_set_user_role, "admin_set_user_role")
nk.register_rpc(admin.rpc_admin_topup_gold, "admin_topup_gold")
nk.register_rpc(admin.rpc_admin_clear_global_chat, "admin_clear_global_chat")
nk.register_rpc(admin.rpc_admin_list_users, "admin_list_users")
nk.register_rpc(admin.rpc_admin_delete_users, "admin_delete_users")
nk.logger_info("LUA TEST: admin module loaded successfully")
return admin
+42
View File
@@ -0,0 +1,42 @@
local nk = require("nakama")
-- =============================================================================
-- Steam Auth Hook
-- =============================================================================
-- On first Steam login: set display_name from Steam username, default role to "player"
local function after_authenticate_steam(context, output, input)
if not context.user_id then return end
local status, account = pcall(nk.account_get_id, context.user_id)
if not status or not account then return end
-- On first login (no display name set), use Steam username
if not account.user.display_name or account.user.display_name == "" then
local steamName = input.username or "SteamPlayer"
pcall(nk.account_update_id, context.user_id, nil, steamName, nil, nil, nil, nil, nil)
nk.logger_info("Steam user " .. context.user_id .. " display name set to: " .. steamName)
end
-- Set default role if not set
local metadata = {}
if type(account.user.metadata) == "string" then
local s, m = pcall(nk.json_decode, account.user.metadata or "{}")
if s then metadata = m end
else
metadata = account.user.metadata or {}
end
if not metadata.role or metadata.role == "" then
metadata.role = "player"
pcall(nk.account_update_id, context.user_id, nil, nil, nil, nil, nil, nil, nk.json_encode(metadata))
end
end
-- Register the Steam auth after-hook
nk.register_req_after(after_authenticate_steam, "AuthenticateSteam")
-- Create default native leaderboard on startup
pcall(nk.leaderboard_create, "global_high_score", true, "desc", "best", nil, {})
nk.logger_info("LUA TEST: core module loaded successfully")
+205
View File
@@ -0,0 +1,205 @@
local nk = require("nakama")
local utils = require("lua.utils")
local daily_rewards = {}
function daily_rewards.rpc_claim_daily_reward(context, payload)
if not context.user_id then error("Not authenticated") end
local now = os.date("!*t")
local currentMonth = string.format("%02d", now.month)
local todayStr = string.format("%04d-%02d-%02d", now.year, now.month, now.day)
local todayIndex = now.day - 1 -- 0 to 30
local stateObjs = nk.storage_read({{ collection = "daily_rewards", key = "state", user_id = context.user_id }})
local state = { claimed_days = {}, last_claim_date = "", month = "" }
if stateObjs and #stateObjs > 0 then
local val = stateObjs[1].value
state.last_claim_date = val.last_claim_date or ""
state.month = val.month or ""
if type(val.claimed_days) == "number" then
for i = 0, val.claimed_days - 1 do
table.insert(state.claimed_days, i)
end
elseif type(val.claimed_days) == "table" then
state.claimed_days = val.claimed_days
end
end
if state.month ~= currentMonth then
state.claimed_days = {}
state.month = currentMonth
end
if state.last_claim_date == todayStr then
error("Already claimed today")
end
local configObjs = nk.storage_read({{ collection = "config", key = "daily_rewards", user_id = "00000000-0000-0000-0000-000000000000" }})
local config = {}
if configObjs and #configObjs > 0 then
config = configObjs[1].value
end
local monthRewards = config[currentMonth]
if not monthRewards or #monthRewards == 0 then
monthRewards = {}
for i = 0, 30 do
table.insert(monthRewards, { type = "star", amount = math.min(10 + i * 5, 100) })
end
end
local dayIndex = todayIndex
-- In lua array size is #monthRewards
if dayIndex >= #monthRewards then
error("Already claimed all rewards for this month")
end
local hasClaimed = false
for _, claimed_day in ipairs(state.claimed_days) do
if claimed_day == dayIndex then
hasClaimed = true
break
end
end
if hasClaimed then
error("Already claimed today's reward")
end
-- Lua arrays are 1-indexed!
local rewardData = monthRewards[dayIndex + 1]
if type(rewardData) == "number" then
rewardData = { type = "star", amount = rewardData }
end
local rewardType = rewardData.type or "star"
local rewardAmount = rewardData.amount or 0
if rewardType == "star" or rewardType == "gold" then
local changes = {}
changes[rewardType] = rewardAmount
nk.wallet_update(context.user_id, changes, {}, true)
elseif string.sub(rewardType, 1, 5) == "frag_" then
local invObjs = nk.storage_read({{ collection = "inventory", key = "fragments", user_id = context.user_id }})
local frags = {}
if invObjs and #invObjs > 0 then
frags = invObjs[1].value
end
frags[rewardType] = (frags[rewardType] or 0) + rewardAmount
nk.storage_write({{
collection = "inventory",
key = "fragments",
user_id = context.user_id,
value = frags,
permission_read = 1,
permission_write = 0
}})
end
table.insert(state.claimed_days, dayIndex)
state.last_claim_date = todayStr
nk.storage_write({{
collection = "daily_rewards",
key = "state",
user_id = context.user_id,
value = state,
permission_read = 1,
permission_write = 0
}})
return nk.json_encode({ success = true, reward_type = rewardType, reward_amount = rewardAmount, day = dayIndex + 1 })
end
function daily_rewards.rpc_get_daily_reward_state(context, payload)
if not context.user_id then error("Not authenticated") end
local now = os.date("!*t")
local currentMonth = string.format("%02d", now.month)
local todayStr = string.format("%04d-%02d-%02d", now.year, now.month, now.day)
local todayIndex = now.day - 1
local stateObjs = nk.storage_read({{ collection = "daily_rewards", key = "state", user_id = context.user_id }})
local state = { claimed_days = {}, last_claim_date = "", month = "" }
if stateObjs and #stateObjs > 0 then
local val = stateObjs[1].value
state.last_claim_date = val.last_claim_date or ""
state.month = val.month or ""
if type(val.claimed_days) == "number" then
for i = 0, val.claimed_days - 1 do
table.insert(state.claimed_days, i)
end
elseif type(val.claimed_days) == "table" then
state.claimed_days = val.claimed_days
end
end
if state.month ~= currentMonth then
state.claimed_days = {}
state.month = currentMonth
end
local configObjs = nk.storage_read({{ collection = "config", key = "daily_rewards", user_id = "00000000-0000-0000-0000-000000000000" }})
local config = {}
if configObjs and #configObjs > 0 then
config = configObjs[1].value
end
local monthRewards = config[currentMonth]
if not monthRewards or #monthRewards == 0 then
monthRewards = {}
for i = 0, 30 do
table.insert(monthRewards, { type = "star", amount = math.min(10 + i * 5, 100) })
end
end
local hasClaimedToday = false
for _, claimed_day in ipairs(state.claimed_days) do
if claimed_day == todayIndex then hasClaimedToday = true break end
end
local canClaimToday = (state.last_claim_date ~= todayStr) and (not hasClaimedToday) and (todayIndex < #monthRewards)
return nk.json_encode({
state = state,
month_rewards = monthRewards,
can_claim_today = canClaimToday,
today_date = todayStr,
today_index = todayIndex,
server_month = now.month
})
end
function daily_rewards.rpc_set_daily_reward_config(context, payload)
utils.require_admin(context)
local request = nk.json_decode(payload or "{}")
nk.storage_write({{
collection = "config",
key = "daily_rewards",
user_id = "00000000-0000-0000-0000-000000000000",
value = request.config,
permission_read = 2,
permission_write = 0
}})
return nk.json_encode({ success = true })
end
function daily_rewards.rpc_get_daily_reward_config_admin(context, payload)
utils.require_admin(context)
local configObjs = nk.storage_read({{ collection = "config", key = "daily_rewards", user_id = "00000000-0000-0000-0000-000000000000" }})
local config = {}
if configObjs and #configObjs > 0 then
config = configObjs[1].value
end
return nk.json_encode({ config = config })
end
nk.register_rpc(daily_rewards.rpc_claim_daily_reward, "claim_daily_reward")
nk.register_rpc(daily_rewards.rpc_get_daily_reward_state, "get_daily_reward_state")
nk.register_rpc(daily_rewards.rpc_set_daily_reward_config, "set_daily_reward_config")
nk.register_rpc(daily_rewards.rpc_get_daily_reward_config_admin, "get_daily_reward_config_admin")
nk.logger_info("LUA TEST: daily rewards module loaded")
return daily_rewards
+244
View File
@@ -0,0 +1,244 @@
local nk = require("nakama")
local utils = require("lua.utils")
local economy = {}
local SHOP_CATALOG_DEFS = {
{ id = "oldpop-blue-hat", name = "Oldpop Blue Hat", category = "head", gold = 100, star = 0, rarity = "Common", character = "Oldpop" },
{ id = "oldpop-green-hat", name = "Oldpop Green Hat", category = "head", gold = 100, star = 0, rarity = "Common", character = "Oldpop" },
{ id = "oldpop-red-hat", name = "Oldpop Red Hat", category = "head", gold = 100, star = 0, rarity = "Common", character = "Oldpop" },
{ id = "oldpop-yellow-hat", name = "Oldpop Yellow Hat", category = "head", gold = 100, star = 0, rarity = "Common", character = "Oldpop" },
{ id = "oldpop-og-pant", name = "Copper OG Pant", category = "costume", gold = 0, star = 0, rarity = "Common", character = "Oldpop" },
{ id = "oldpop-grey-pant", name = "Copper Grey Pant", category = "costume", gold = 150, star = 0, rarity = "Common", character = "Oldpop" },
{ id = "oldpop-red-pant", name = "Copper Red Pant", category = "costume", gold = 150, star = 0, rarity = "Common", character = "Oldpop" },
{ id = "oldpop-yellow-pant", name = "Copper Yellow Pant", category = "costume", gold = 150, star = 0, rarity = "Common", character = "Oldpop" },
{ id = "oldpop-blue-gloves", name = "Oldpop Blue Gloves", category = "glove", gold = 75, star = 0, rarity = "Common", character = "Oldpop" },
{ id = "oldpop-green-gloves", name = "Oldpop Green Gloves", category = "glove", gold = 75, star = 0, rarity = "Common", character = "Oldpop" },
{ id = "oldpop-red-gloves", name = "Oldpop Red Gloves", category = "glove", gold = 75, star = 0, rarity = "Common", character = "Oldpop" },
{ id = "oldpop-yellow-gloves", name = "Oldpop Yellow Gloves", category = "glove", gold = 75, star = 0, rarity = "Common", character = "Oldpop" }
}
local function build_shop_catalog()
local catalog = {}
for _, def in ipairs(SHOP_CATALOG_DEFS) do
local cat = def.category
if not catalog[cat] then catalog[cat] = {} end
local entry = {
id = def.id,
name = def.name,
gold = def.gold or 0,
star = def.star or 0,
rarity = def.rarity or "Common",
character = def.character
}
table.insert(catalog[cat], entry)
end
return catalog
end
function economy.rpc_get_shop_catalog(context, payload)
if not context.user_id then error("Not authenticated") end
local result = { catalog = build_shop_catalog(), featured_banners = {} }
local status, objs = pcall(nk.storage_read, {{ collection = "shop_config", key = "featured_banners", user_id = "00000000-0000-0000-0000-000000000000" }})
if status and objs and #objs > 0 then
local val = objs[1].value
if val.banners then result.featured_banners = val.banners end
end
return nk.json_encode(result)
end
function economy.rpc_buy_currency(context, payload)
if not context.user_id then error("Not authenticated") end
local request = nk.json_decode(payload)
local packageId = request.package_id
local receipt = request.receipt
local idempotencyKey = request.idempotency_key
if not packageId or packageId == "" then error("Package ID required") end
if not idempotencyKey or idempotencyKey == "" then error("Idempotency key required") end
local status, existing = pcall(nk.storage_read, {{ collection = "receipts", key = idempotencyKey, user_id = context.user_id }})
if status and existing and #existing > 0 then
return nk.json_encode({ success = true, package_id = packageId, duplicate = true, status = existing[1].value.status })
end
local changeset = { gold = 0, star = 0 }
local requiresVerification = false
if packageId == "gold_100" then changeset.gold = 100; requiresVerification = true
elseif packageId == "gold_500" then changeset.gold = 550; requiresVerification = true
elseif packageId == "gold_1000" then changeset.gold = 1150; requiresVerification = true
elseif packageId == "gold_2000" then changeset.gold = 2400; requiresVerification = true
elseif packageId == "gold_5000" then changeset.gold = 6250; requiresVerification = true
elseif packageId == "gold_10000" then changeset.gold = 13000; requiresVerification = true
elseif packageId == "star_100" then changeset.star = 100; changeset.gold = -500
elseif packageId == "star_250" then changeset.star = 250; changeset.gold = -1100
elseif packageId == "star_600" then changeset.star = 600; changeset.gold = -2500
else error("Invalid package ID") end
if requiresVerification and not receipt then
nk.storage_write({{
collection = "receipts",
key = idempotencyKey,
user_id = context.user_id,
value = { type = "currency", package_id = packageId, status = "pending", created_at = os.date("!%Y-%m-%dT%H:%M:%S.000Z") },
permission_read = 1,
permission_write = 0
}})
return nk.json_encode({ success = true, status = "pending", package_id = packageId })
end
local s, err = pcall(function()
if changeset.gold ~= 0 or changeset.star ~= 0 then
nk.wallet_update(context.user_id, changeset, {}, true)
end
nk.storage_write({{
collection = "receipts",
key = idempotencyKey,
user_id = context.user_id,
value = { type = "currency", package_id = packageId, changeset = changeset, receipt = receipt or nk.json_null(), status = "verified", processed_at = os.date("!%Y-%m-%dT%H:%M:%S.000Z") },
permission_read = 1,
permission_write = 0
}})
end)
if not s then
nk.logger_error("Currency purchase failed: " .. tostring(err))
error("NotEnoughFunds")
end
nk.logger_info("User " .. context.user_id .. " bought currency package " .. packageId)
return nk.json_encode({ success = true, status = "verified", package_id = packageId })
end
function economy.rpc_purchase_item(context, payload)
if not context.user_id then error("Not authenticated") end
local request = nk.json_decode(payload)
local itemId = request.item_id
local quantity = request.quantity or 1
local idempotencyKey = request.idempotency_key
if not itemId or itemId == "" then error("Item ID required") end
if quantity < 1 then error("Invalid quantity") end
if not idempotencyKey or idempotencyKey == "" then error("Idempotency key required") end
local status, existing = pcall(nk.storage_read, {{ collection = "receipts", key = idempotencyKey, user_id = context.user_id }})
if status and existing and #existing > 0 then
return nk.json_encode({ success = true, item = itemId, duplicate = true })
end
local itemDef = nil
for _, def in ipairs(SHOP_CATALOG_DEFS) do
if def.id == itemId then
itemDef = def
break
end
end
if not itemDef then error("ItemNotFound") end
local priceGold = (itemDef.gold or 0) * quantity
local priceStar = (itemDef.star or 0) * quantity
local category = itemDef.category or "accessory"
local s, err = pcall(function()
local changeset = {}
if priceGold > 0 then changeset.gold = -priceGold end
if priceStar > 0 then changeset.star = -priceStar end
if priceGold > 0 or priceStar > 0 then
nk.wallet_update(context.user_id, changeset, {}, true)
end
end)
if not s then
nk.logger_error("Wallet update failed: " .. tostring(err))
error("NotEnoughFunds")
end
local s2, err2 = pcall(function()
local writes = {
{
collection = "inventory",
key = itemId,
user_id = context.user_id,
value = { category = category, purchased_at = os.date("!%Y-%m-%dT%H:%M:%S.000Z"), quantity = quantity },
permission_read = 1,
permission_write = 0
},
{
collection = "receipts",
key = idempotencyKey,
user_id = context.user_id,
value = { type = "item", item_id = itemId, quantity = quantity, cost = { gold = priceGold, star = priceStar }, processed_at = os.date("!%Y-%m-%dT%H:%M:%S.000Z") },
permission_read = 1,
permission_write = 0
}
}
nk.storage_write(writes)
end)
if not s2 then
nk.logger_error("Purchase failed: " .. tostring(err2))
error("PurchaseFailed")
end
nk.logger_info("User " .. context.user_id .. " purchased " .. itemId)
return nk.json_encode({ success = true, item = itemId })
end
function economy.rpc_admin_set_featured_banners(context, payload)
utils.require_admin(context)
local req = nk.json_decode(payload or "{}")
local banners = req.banners or {}
local finalBanners = {}
for i = 1, math.min(#banners, 3) do
table.insert(finalBanners, banners[i])
end
for _, b in ipairs(finalBanners) do
local itemId = b.item_id or ""
if itemId ~= "" then
local found = false
for _, def in ipairs(SHOP_CATALOG_DEFS) do
if def.id == itemId then found = true; break end
end
if not found then error("Item not found in catalog: " .. itemId) end
end
end
nk.storage_write({{
collection = "shop_config",
key = "featured_banners",
user_id = "00000000-0000-0000-0000-000000000000",
value = { banners = finalBanners },
permission_read = 2,
permission_write = 0
}})
nk.logger_info("Featured banners updated by admin " .. context.user_id)
return nk.json_encode({ success = true, banners = finalBanners })
end
function economy.rpc_admin_get_featured_banners(context, payload)
utils.require_admin(context)
local status, objs = pcall(nk.storage_read, {{ collection = "shop_config", key = "featured_banners", user_id = "00000000-0000-0000-0000-000000000000" }})
if status and objs and #objs > 0 then
return nk.json_encode({ banners = objs[1].value.banners or {} })
end
return nk.json_encode({ banners = {} })
end
nk.register_rpc(economy.rpc_get_shop_catalog, "get_shop_catalog")
nk.register_rpc(economy.rpc_buy_currency, "buy_currency")
nk.register_rpc(economy.rpc_purchase_item, "purchase_item")
nk.register_rpc(economy.rpc_admin_set_featured_banners, "admin_set_featured_banners")
nk.register_rpc(economy.rpc_admin_get_featured_banners, "admin_get_featured_banners")
nk.logger_info("LUA TEST: economy module loaded successfully")
return economy
+535
View File
@@ -0,0 +1,535 @@
local nk = require("nakama")
local utils = require("lua.utils")
local inbox = {}
function inbox.rpc_admin_send_mail(context, payload)
utils.require_admin(context)
local request = nk.json_decode(payload or "{}")
local nowStr = os.date("!%Y-%m-%dT%H:%M:%S.000Z") -- approximate ISO8601
local startDate = request.start_date or nowStr
local endDate = request.end_date or ""
-- 30 days from now in seconds for expiry_date fallback if not specified
local expiryDate = os.date("!%Y-%m-%dT%H:%M:%S.000Z", os.time() + 30 * 24 * 60 * 60)
local mailObj = {
id = nk.uuid_v4(),
title = request.title or "Announcement",
content = request.content or "",
sender = "TEKTON DEV TEAM",
date = startDate,
start_date = startDate,
end_date = endDate,
expiry_date = expiryDate,
rewards = request.rewards or {}
}
if request.target_user_id and request.target_user_id ~= "" then
mailObj.type = "personal"
local invObjs = nk.storage_read({{ collection = "inbox", key = "personal", user_id = request.target_user_id }})
local personalMails = {}
if #invObjs > 0 then
personalMails = invObjs[1].value.mails or {}
end
table.insert(personalMails, mailObj)
nk.storage_write({{
collection = "inbox",
key = "personal",
user_id = request.target_user_id,
value = { mails = personalMails },
permission_read = 1,
permission_write = 0
}})
nk.logger_info("Personal mail sent to " .. request.target_user_id)
else
mailObj.type = "global"
local globalObjs = nk.storage_read({{ collection = "config", key = "global_mail", user_id = "00000000-0000-0000-0000-000000000000" }})
local globalMails = {}
if #globalObjs > 0 then
globalMails = globalObjs[1].value.mails or {}
end
table.insert(globalMails, mailObj)
nk.storage_write({{
collection = "config",
key = "global_mail",
user_id = "00000000-0000-0000-0000-000000000000",
value = { mails = globalMails },
permission_read = 2,
permission_write = 0
}})
nk.logger_info("Global mail sent")
end
return nk.json_encode({ success = true, mail = mailObj })
end
function inbox.rpc_get_mail(context, payload)
if not context.user_id then error("Not authenticated") end
local personalObjs = nk.storage_read({{ collection = "inbox", key = "personal", user_id = context.user_id }})
local globalObjs = nk.storage_read({{ collection = "config", key = "global_mail", user_id = "00000000-0000-0000-0000-000000000000" }})
local stateObjs = nk.storage_read({{ collection = "inbox", key = "state", user_id = context.user_id }})
local personalMails = (#personalObjs > 0) and (personalObjs[1].value.mails or {}) or {}
local globalMails = (#globalObjs > 0) and (globalObjs[1].value.mails or {}) or {}
local state = { claimed_ids = {}, deleted_ids = {}, read_ids = {} }
if #stateObjs > 0 then
local val = stateObjs[1].value
state.claimed_ids = val.claimed_ids or {}
state.deleted_ids = val.deleted_ids or {}
state.read_ids = val.read_ids or {}
end
local function array_contains(arr, val)
for _, v in ipairs(arr) do
if v == val then return true end
end
return false
end
local allMails = {}
for _, m in ipairs(personalMails) do table.insert(allMails, m) end
for _, m in ipairs(globalMails) do table.insert(allMails, m) end
local filteredMails = {}
local nowStr = os.date("!%Y-%m-%dT%H:%M:%S.000Z")
for _, mail in ipairs(allMails) do
if not array_contains(state.deleted_ids, mail.id) then
local skip = false
if mail.expiry_date and mail.expiry_date ~= "" and nowStr > mail.expiry_date then
skip = true
end
if not skip and mail.start_date and mail.start_date ~= "" and nowStr < mail.start_date then
skip = true
end
if not skip and mail.type == "global" and mail.end_date and mail.end_date ~= "" and nowStr > mail.end_date then
skip = true
end
if not skip then
table.insert(filteredMails, mail)
end
end
end
return nk.json_encode({ mails = filteredMails, state = state })
end
function inbox.rpc_claim_mail_reward(context, payload)
if not context.user_id then error("Not authenticated") end
local request = nk.json_decode(payload or "{}")
local mailId = request.mail_id
if not mailId then error("mail_id required") end
local personalObjs = nk.storage_read({{ collection = "inbox", key = "personal", user_id = context.user_id }})
local globalObjs = nk.storage_read({{ collection = "config", key = "global_mail", user_id = "00000000-0000-0000-0000-000000000000" }})
local stateObjs = nk.storage_read({{ collection = "inbox", key = "state", user_id = context.user_id }})
local state = { claimed_ids = {}, deleted_ids = {}, read_ids = {} }
if #stateObjs > 0 then
local val = stateObjs[1].value
state.claimed_ids = val.claimed_ids or {}
state.deleted_ids = val.deleted_ids or {}
state.read_ids = val.read_ids or {}
end
local function array_contains(arr, val)
for _, v in ipairs(arr) do
if v == val then return true end
end
return false
end
if array_contains(state.claimed_ids, mailId) then
error("Reward already claimed")
end
local personalMails = (#personalObjs > 0) and (personalObjs[1].value.mails or {}) or {}
local globalMails = (#globalObjs > 0) and (globalObjs[1].value.mails or {}) or {}
local allMails = {}
for _, m in ipairs(personalMails) do table.insert(allMails, m) end
for _, m in ipairs(globalMails) do table.insert(allMails, m) end
local targetMail = nil
for _, mail in ipairs(allMails) do
if mail.id == mailId then
targetMail = mail
break
end
end
if not targetMail then error("Mail not found") end
local rewards = targetMail.rewards or {}
local starTotal = 0
local goldTotal = 0
local fragsToUpdate = {}
local skinsToAdd = {}
if type(rewards) == "table" and not rewards[1] and (rewards.star or rewards.gold) then
-- Handle legacy dictionary format
starTotal = rewards.star or 0
goldTotal = rewards.gold or 0
rewards = {}
end
for _, r in ipairs(rewards) do
local rType = r.type or "star"
local amount = r.amount or 0
if rType == "star" then
starTotal = starTotal + amount
elseif rType == "gold" then
goldTotal = goldTotal + amount
elseif string.sub(rType, 1, 5) == "frag_" or rType == "item" then
local fragId = r.id or rType
fragsToUpdate[fragId] = (fragsToUpdate[fragId] or 0) + amount
elseif rType == "skin" then
if r.id then table.insert(skinsToAdd, r.id) end
end
end
if starTotal > 0 or goldTotal > 0 then
local changes = {}
if starTotal > 0 then changes.star = starTotal end
if goldTotal > 0 then changes.gold = goldTotal end
nk.wallet_update(context.user_id, changes, {}, true)
end
local fragKeysCount = 0
for _ in pairs(fragsToUpdate) do fragKeysCount = fragKeysCount + 1 end
if fragKeysCount > 0 then
local invObjs = nk.storage_read({{ collection = "inventory", key = "fragments", user_id = context.user_id }})
local frags = (#invObjs > 0) and invObjs[1].value or {}
for fId, count in pairs(fragsToUpdate) do
frags[fId] = (frags[fId] or 0) + count
end
nk.storage_write({{
collection = "inventory",
key = "fragments",
user_id = context.user_id,
value = frags,
permission_read = 1,
permission_write = 0
}})
end
if #skinsToAdd > 0 then
local skinWrites = {}
for _, sId in ipairs(skinsToAdd) do
table.insert(skinWrites, {
collection = "inventory",
key = sId,
user_id = context.user_id,
value = { acquired_via = "mail", purchased_at = os.date("!%Y-%m-%dT%H:%M:%S.000Z") },
permission_read = 1,
permission_write = 0
})
end
nk.storage_write(skinWrites)
end
table.insert(state.claimed_ids, mailId)
if not array_contains(state.read_ids, mailId) then
table.insert(state.read_ids, mailId)
end
nk.storage_write({{
collection = "inbox",
key = "state",
user_id = context.user_id,
value = state,
permission_read = 1,
permission_write = 0
}})
return nk.json_encode({ success = true, claimed_ids = state.claimed_ids })
end
function inbox.rpc_delete_mail(context, payload)
if not context.user_id then error("Not authenticated") end
local request = nk.json_decode(payload or "{}")
local mailId = request.mail_id
if not mailId then error("mail_id required") end
local stateObjs = nk.storage_read({{ collection = "inbox", key = "state", user_id = context.user_id }})
local state = { claimed_ids = {}, deleted_ids = {}, read_ids = {} }
if #stateObjs > 0 then
local val = stateObjs[1].value
state.claimed_ids = val.claimed_ids or {}
state.deleted_ids = val.deleted_ids or {}
state.read_ids = val.read_ids or {}
end
local function array_contains(arr, val)
for _, v in ipairs(arr) do if v == val then return true end end
return false
end
if not array_contains(state.deleted_ids, mailId) then table.insert(state.deleted_ids, mailId) end
if not array_contains(state.read_ids, mailId) then table.insert(state.read_ids, mailId) end
nk.storage_write({{
collection = "inbox",
key = "state",
user_id = context.user_id,
value = state,
permission_read = 1,
permission_write = 0
}})
return nk.json_encode({ success = true, deleted_ids = state.deleted_ids })
end
function inbox.rpc_save_mail_state(context, payload)
if not context.user_id then error("Not authenticated") end
local request = nk.json_decode(payload or "{}")
local stateObjs = nk.storage_read({{ collection = "inbox", key = "state", user_id = context.user_id }})
local state = { claimed_ids = {}, deleted_ids = {}, read_ids = {} }
if #stateObjs > 0 then
local val = stateObjs[1].value
state.claimed_ids = val.claimed_ids or {}
state.deleted_ids = val.deleted_ids or {}
state.read_ids = val.read_ids or {}
end
local function array_contains(arr, val)
for _, v in ipairs(arr) do if v == val then return true end end
return false
end
local newReadIds = request.read_ids or {}
for _, rid in ipairs(newReadIds) do
if not array_contains(state.read_ids, rid) then
table.insert(state.read_ids, rid)
end
end
nk.storage_write({{
collection = "inbox",
key = "state",
user_id = context.user_id,
value = state,
permission_read = 1,
permission_write = 0
}})
return nk.json_encode({ success = true })
end
function inbox.rpc_admin_list_mail(context, payload)
utils.require_admin(context)
local globalObjs = nk.storage_read({{ collection = "config", key = "global_mail", user_id = "00000000-0000-0000-0000-000000000000" }})
local globalMails = (#globalObjs > 0) and (globalObjs[1].value.mails or {}) or {}
for _, m in ipairs(globalMails) do m.type = "global" end
local personalMails = {}
local cursor = nil
repeat
local status, listResult = pcall(nk.storage_list, "", "inbox", 100, cursor)
if status and listResult then
local objects = listResult.objects or {}
for _, obj in ipairs(objects) do
if obj.key == "personal" then
local ownerUserId = obj.user_id
local mails = obj.value.mails or {}
for _, m in ipairs(mails) do
m.type = "personal"
m.target_user_id = ownerUserId
table.insert(personalMails, m)
end
end
end
cursor = listResult.cursor
else
cursor = nil
end
until not cursor or cursor == ""
local allMails = {}
for _, m in ipairs(globalMails) do table.insert(allMails, m) end
for _, m in ipairs(personalMails) do table.insert(allMails, m) end
table.sort(allMails, function(a, b)
local d1 = a.date or ""
local d2 = b.date or ""
return d1 > d2
end)
return nk.json_encode({ mails = allMails })
end
function inbox.rpc_admin_update_mail(context, payload)
utils.require_admin(context)
local request = nk.json_decode(payload or "{}")
local mailId = request.mail_id
if not mailId then error("mail_id required") end
local isGlobal = request.type ~= "personal"
local targetUserId = request.target_user_id or ""
local newTargetUserId = request.new_target_user_id
local hasNewTarget = (newTargetUserId ~= nil)
local mailObj = nil
if isGlobal then
local globalObjs = nk.storage_read({{ collection = "config", key = "global_mail", user_id = "00000000-0000-0000-0000-000000000000" }})
local globalMails = (#globalObjs > 0) and (globalObjs[1].value.mails or {}) or {}
for i, m in ipairs(globalMails) do
if m.id == mailId then
mailObj = table.remove(globalMails, i)
break
end
end
if not mailObj then error("Mail not found in global") end
nk.storage_write({{
collection = "config",
key = "global_mail",
user_id = "00000000-0000-0000-0000-000000000000",
value = { mails = globalMails },
permission_read = 2,
permission_write = 0
}})
else
if targetUserId == "" then error("target_user_id required for personal mail") end
local pObjs = nk.storage_read({{ collection = "inbox", key = "personal", user_id = targetUserId }})
local personalMails = (#pObjs > 0) and (pObjs[1].value.mails or {}) or {}
for i, m in ipairs(personalMails) do
if m.id == mailId then
mailObj = table.remove(personalMails, i)
break
end
end
if not mailObj then error("Mail not found in personal inbox") end
nk.storage_write({{
collection = "inbox",
key = "personal",
user_id = targetUserId,
value = { mails = personalMails },
permission_read = 1,
permission_write = 0
}})
end
if request.title ~= nil then mailObj.title = request.title end
if request.content ~= nil then mailObj.content = request.content end
if request.end_date ~= nil then mailObj.end_date = request.end_date end
if request.expiry_date ~= nil then mailObj.expiry_date = request.expiry_date end
local destUserId = ""
if hasNewTarget then destUserId = newTargetUserId else
if not isGlobal then destUserId = targetUserId end
end
if destUserId == "" then
mailObj.type = "global"
local gObjs = nk.storage_read({{ collection = "config", key = "global_mail", user_id = "00000000-0000-0000-0000-000000000000" }})
local gMails = (#gObjs > 0) and (gObjs[1].value.mails or {}) or {}
table.insert(gMails, mailObj)
nk.storage_write({{
collection = "config",
key = "global_mail",
user_id = "00000000-0000-0000-0000-000000000000",
value = { mails = gMails },
permission_read = 2,
permission_write = 0
}})
else
mailObj.type = "personal"
local dObjs = nk.storage_read({{ collection = "inbox", key = "personal", user_id = destUserId }})
local dMails = (#dObjs > 0) and (dObjs[1].value.mails or {}) or {}
table.insert(dMails, mailObj)
nk.storage_write({{
collection = "inbox",
key = "personal",
user_id = destUserId,
value = { mails = dMails },
permission_read = 1,
permission_write = 0
}})
end
nk.logger_info("Admin updated mail " .. mailId .. " by " .. context.user_id)
return nk.json_encode({ success = true })
end
function inbox.rpc_admin_delete_mail_server(context, payload)
utils.require_admin(context)
local request = nk.json_decode(payload or "{}")
local mailId = request.mail_id
if not mailId then error("mail_id required") end
local isGlobal = request.type ~= "personal"
local targetUserId = request.target_user_id or ""
if isGlobal then
local globalObjs = nk.storage_read({{ collection = "config", key = "global_mail", user_id = "00000000-0000-0000-0000-000000000000" }})
local globalMails = (#globalObjs > 0) and (globalObjs[1].value.mails or {}) or {}
local before = #globalMails
local filtered = {}
for _, m in ipairs(globalMails) do
if m.id ~= mailId then table.insert(filtered, m) end
end
if #filtered == before then error("Mail not found") end
nk.storage_write({{
collection = "config",
key = "global_mail",
user_id = "00000000-0000-0000-0000-000000000000",
value = { mails = filtered },
permission_read = 2,
permission_write = 0
}})
else
if targetUserId == "" then error("target_user_id required for personal mail") end
local pObjs = nk.storage_read({{ collection = "inbox", key = "personal", user_id = targetUserId }})
local personalMails = (#pObjs > 0) and (pObjs[1].value.mails or {}) or {}
local before = #personalMails
local filtered = {}
for _, m in ipairs(personalMails) do
if m.id ~= mailId then table.insert(filtered, m) end
end
if #filtered == before then error("Mail not found") end
nk.storage_write({{
collection = "inbox",
key = "personal",
user_id = targetUserId,
value = { mails = filtered },
permission_read = 1,
permission_write = 0
}})
end
nk.logger_info("Admin deleted mail " .. mailId .. " from server by " .. context.user_id)
return nk.json_encode({ success = true })
end
nk.register_rpc(inbox.rpc_admin_send_mail, "admin_send_mail")
nk.register_rpc(inbox.rpc_get_mail, "get_mail")
nk.register_rpc(inbox.rpc_claim_mail_reward, "claim_mail_reward")
nk.register_rpc(inbox.rpc_delete_mail, "delete_mail")
nk.register_rpc(inbox.rpc_save_mail_state, "save_mail_state")
nk.register_rpc(inbox.rpc_admin_list_mail, "admin_list_mail")
nk.register_rpc(inbox.rpc_admin_update_mail, "admin_update_mail")
nk.register_rpc(inbox.rpc_admin_delete_mail_server, "admin_delete_mail_server")
nk.logger_info("LUA TEST: inbox module loaded")
return inbox
+249
View File
@@ -0,0 +1,249 @@
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
+260
View File
@@ -0,0 +1,260 @@
local nk = require("nakama")
local user = {}
function user.rpc_get_user_profile(context, payload)
local request = nk.json_decode(payload or "{}")
local targetUserId = request.user_id or context.user_id
local status, account = pcall(nk.account_get_id, targetUserId)
if not status then error("Account not found") end
local metadata = {}
if account.user.metadata then
status, metadata = pcall(nk.json_decode, account.user.metadata)
if not status then metadata = {} end
end
if metadata.banned and targetUserId == context.user_id then
if metadata.ban_expires then
-- Note: ban_expires stored as Unix time in Lua (seconds) or ISO string depending on how it was stored
-- Let's check against current os.time() assuming Unix time
local expiresAt = tonumber(metadata.ban_expires)
if not expiresAt and type(metadata.ban_expires) == "string" then
-- basic check if we stored iso string
-- We assume it's valid ISO string and lua os.time might not parse it easily without custom function
-- As a fallback, we'll keep the ban if we can't parse it
error("Account banned until " .. metadata.ban_expires .. ". Reason: " .. (metadata.ban_reason or ""))
end
if expiresAt and expiresAt <= os.time() then
metadata.banned = nil
metadata.ban_reason = nil
metadata.ban_expires = nil
nk.account_update_id(targetUserId, nil, nil, nil, nil, nil, nil, nk.json_encode(metadata))
else
error("Account banned until " .. tostring(metadata.ban_expires) .. ". Reason: " .. (metadata.ban_reason or ""))
end
else
error("Account permanently banned. Reason: " .. (metadata.ban_reason or ""))
end
end
return nk.json_encode({
user_id = account.user.id,
username = account.user.username,
display_name = account.user.display_name,
avatar_url = account.user.avatar_url,
create_time = account.user.create_time,
role = metadata.role or "player"
})
end
function user.rpc_update_user_profile(context, payload)
if not context.user_id then error("Not authenticated") end
local request = nk.json_decode(payload)
local status, err = pcall(nk.account_update_id,
context.user_id,
nil,
request.display_name or nil,
nil,
nil,
nil,
request.avatar_url or nil,
nil
)
if not status then
nk.logger_error("Failed to update profile: " .. tostring(err))
error("Failed to update profile")
end
return nk.json_encode({ success = true })
end
function user.rpc_search_users(context, payload)
if not context.user_id then error("Not authenticated") end
local request = nk.json_decode(payload or "{}")
local query = request.query or ""
local users = {}
local sql = ""
local params = {}
if query == "" then
sql = "SELECT id, username, display_name, metadata FROM users WHERE id != '00000000-0000-0000-0000-000000000000' ORDER BY create_time DESC LIMIT 100"
else
sql = "SELECT id, username, display_name, metadata FROM users WHERE (username ILIKE $1 OR display_name ILIKE $1) AND id != '00000000-0000-0000-0000-000000000000' ORDER BY create_time DESC LIMIT 100"
params = {"%" .. query .. "%"}
end
local status, rows = pcall(nk.sql_query, sql, params)
if status and rows then
for _, row in ipairs(rows) do
local metadata = {}
if row.metadata then
local s, m = pcall(nk.json_decode, row.metadata)
if s then metadata = m end
end
table.insert(users, {
user_id = row.id,
username = row.username or "",
display_name = row.display_name or row.username or "",
avatar_url = metadata.avatar_url or ""
})
end
end
return nk.json_encode({ users = users })
end
function user.rpc_change_credentials(context, payload)
if not context.user_id then error("Not authenticated") end
local req = nk.json_decode(payload or "{}")
local account = nk.account_get_id(context.user_id)
if account.email then
if not req.current_password then error("Current password required") end
local status = pcall(nk.authenticate_email, account.email, req.current_password, false)
if not status then error("Incorrect current password.") end
nk.unlink_email(context.user_id, account.email, req.current_password)
end
local status, err = pcall(nk.link_email, context.user_id, req.new_email, req.new_password)
if not status then
if account.email then pcall(nk.link_email, context.user_id, account.email, req.current_password) end
error("Failed to set new credentials: " .. tostring(err))
end
return nk.json_encode({ success = true })
end
function user.rpc_send_lobby_invite(context, payload)
if not context.user_id then error("Not authenticated") end
local req = nk.json_decode(payload or "{}")
if not req.to_user_id or not req.match_id then error("Missing to_user_id or match_id") end
local sender = nk.account_get_id(context.user_id)
local senderName = sender.user.display_name or sender.user.username or "Someone"
nk.notification_send(
req.to_user_id,
senderName .. " invited you to their lobby",
nk.json_encode({ match_id = req.match_id, from_name = senderName }),
1001,
context.user_id,
true
)
nk.logger_info("Lobby invite sent from " .. context.user_id .. " to " .. req.to_user_id .. " for match " .. req.match_id)
return nk.json_encode({ success = true })
end
function user.rpc_send_friend_request(context, payload)
if not context.user_id then error("Not authenticated") end
local request = nk.json_decode(payload or "{}")
local targetUserId = request.user_id or ""
if targetUserId == "" then error("user_id is required") end
if targetUserId == context.user_id then error("Cannot add yourself") end
local senderAccount = nk.account_get_id(context.user_id)
local senderName = senderAccount.user.display_name or senderAccount.user.username or "Someone"
nk.notification_send(
targetUserId,
"Friend Request",
nk.json_encode({ from_user_id = context.user_id, from_name = senderName }),
1002,
context.user_id,
true
)
nk.logger_info("Friend request notification sent from " .. context.user_id .. " to " .. targetUserId)
return nk.json_encode({ success = true })
end
function user.after_authenticate(context, out, payload)
if not context.user_id then return end
-- We store the last 10 logins in user metadata or a dedicated collection
local login_entry = {
time = os.time(),
ip = context.client_ip or "unknown"
}
local status, result = pcall(nk.storage_read, {{collection = "history", key = "logins", user_id = context.user_id}})
local logins = {}
if status and result and #result > 0 then
logins = result[1].value.logins or {}
end
table.insert(logins, 1, login_entry)
-- Keep only last 20 logins to save space
while #logins > 20 do table.remove(logins) end
pcall(nk.storage_write, {{
collection = "history",
key = "logins",
user_id = context.user_id,
value = { logins = logins },
permission_read = 0,
permission_write = 0
}})
end
function user.rpc_admin_get_user_history(context, payload)
local utils = require("lua.utils")
utils.require_admin(context)
local request = nk.json_decode(payload or "{}")
local targetUserId = request.user_id
if not targetUserId then error("user_id is required") end
local history = {
wallet_ledger = {},
logins = {},
matches = {}
}
-- 1. Fetch Wallet Ledger (Economy History)
local status_wallet, wallet_result = pcall(nk.wallet_ledger_list, targetUserId, 50)
if status_wallet and wallet_result then
history.wallet_ledger = wallet_result.items or {}
end
-- 2. Fetch Login History
local status_logins, login_result = pcall(nk.storage_read, {{collection = "history", key = "logins", user_id = targetUserId}})
if status_logins and login_result and #login_result > 0 then
history.logins = login_result[1].value.logins or {}
end
-- 3. Fetch Match History (If stored in collection 'matches')
local status_matches, match_result = pcall(nk.storage_list, targetUserId, "matches", 50, "")
if status_matches and match_result then
for _, obj in ipairs(match_result.objects or {}) do
table.insert(history.matches, obj.value)
end
end
return nk.json_encode({ history = history })
end
nk.register_rpc(user.rpc_get_user_profile, "get_user_profile")
nk.register_rpc(user.rpc_update_user_profile, "update_user_profile")
nk.register_rpc(user.rpc_search_users, "search_users")
nk.register_rpc(user.rpc_change_credentials, "change_credentials")
nk.register_rpc(user.rpc_send_lobby_invite, "send_lobby_invite")
nk.register_rpc(user.rpc_send_friend_request, "send_friend_request")
nk.register_rpc(user.rpc_admin_get_user_history, "admin_get_user_history")
nk.register_req_after(user.after_authenticate, "AuthenticateDevice")
nk.register_req_after(user.after_authenticate, "AuthenticateEmail")
nk.register_req_after(user.after_authenticate, "AuthenticateCustom")
nk.logger_info("LUA TEST: user module loaded")
return user
+52
View File
@@ -0,0 +1,52 @@
local nk = require("nakama")
local utils = {}
utils.ADMIN_ROLES = { ["admin"] = true, ["moderator"] = true, ["owner"] = true }
function utils.is_admin(context)
if not context.user_id then return false end
local status, account = pcall(nk.account_get_id, context.user_id)
if not status or not account then return false end
local metadata = {}
if type(account.user.metadata) == "string" then
status, metadata = pcall(nk.json_decode, account.user.metadata)
if not status then metadata = {} end
else
metadata = account.user.metadata or {}
end
local role = metadata.role or ""
return utils.ADMIN_ROLES[role] == true
end
function utils.is_match_host(context, match_id)
if not context.user_id or not match_id then return false end
local status, match = pcall(nk.match_get, match_id)
if not status or not match then return false end
-- Needs to decode match.state if you're using authoritative matches
-- Simplified for lua translation:
local state = {}
if match.state then
status, state = pcall(nk.json_decode, match.state)
if not status then state = {} end
end
return state.hostUserId == context.user_id
end
function utils.require_admin(context)
if not utils.is_admin(context) then
error("Admin privileges required")
end
end
function utils.require_admin_or_host(context, match_id)
if not utils.is_admin(context) and not utils.is_match_host(context, match_id) then
error("Admin or host privileges required")
end
end
return utils