feat: 2.3.2
This commit is contained in:
+6
-16
@@ -4,7 +4,7 @@ This guide explains how to deploy the admin module to your Nakama server.
|
||||
|
||||
## Files
|
||||
|
||||
- `tekton_admin.ts` - TypeScript server runtime module
|
||||
- `core.js` — Admin, user management, leaderboard, daily rewards, inbox, auth, shop catalog, purchases, and economy RPCs
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -23,12 +23,7 @@ This guide explains how to deploy the admin module to your Nakama server.
|
||||
2. **Compile TypeScript to JavaScript:**
|
||||
```bash
|
||||
cd server/nakama
|
||||
tsc tekton_admin.ts --outDir dist --lib ES2020 --types nakama-runtime
|
||||
```
|
||||
|
||||
3. **Copy to Nakama modules directory:**
|
||||
```bash
|
||||
cp dist/tekton_admin.js /path/to/nakama/data/modules/
|
||||
cp core.js /path/to/nakama/data/modules/
|
||||
```
|
||||
|
||||
4. **Restart Nakama server**
|
||||
@@ -43,15 +38,11 @@ In your `nakama.yml` or `nakama-docker.yml`:
|
||||
|
||||
```yaml
|
||||
runtime:
|
||||
js_entrypoint: "tekton_admin.js"
|
||||
path: /nakama/data/modules
|
||||
```
|
||||
|
||||
Or for multiple modules:
|
||||
|
||||
```yaml
|
||||
runtime:
|
||||
js_entrypoint: "index.js"
|
||||
```
|
||||
`core.js` has an `InitModule` function.
|
||||
Nakama auto-discovers and calls it — no `js_entrypoint` needed.
|
||||
|
||||
## Role System
|
||||
|
||||
@@ -109,8 +100,7 @@ curl -X PUT "http://localhost:7350/v2/console/account/{user_id}" \
|
||||
## Troubleshooting
|
||||
|
||||
### RPC Not Found
|
||||
- Check module is loaded: `nakama --help` or check logs
|
||||
- Verify js_entrypoint in config
|
||||
- Check modules are loaded: look for "Tekton core module loaded" in logs
|
||||
|
||||
### Permission Denied
|
||||
- Check user has correct role in metadata
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
*
|
||||
* This module provides secure admin operations via RPC calls.
|
||||
* Deploy this to your Nakama server's runtime directory.
|
||||
*
|
||||
* NOTE: Economy RPCs (shop, currency, purchase, featured banners)
|
||||
* are registered by economy.js — each file has its own InitModule.
|
||||
*/
|
||||
|
||||
// Initialize RPC endpoints
|
||||
@@ -25,12 +28,8 @@ function InitModule(ctx, logger, nk, initializer) {
|
||||
initializer.registerRpc("get_user_profile", rpcGetUserProfile);
|
||||
initializer.registerRpc("update_user_profile", rpcUpdateUserProfile);
|
||||
initializer.registerRpc("search_users", rpcSearchUsers);
|
||||
|
||||
// Store RPCs
|
||||
initializer.registerRpc("purchase_item", rpcPurchaseItem);
|
||||
initializer.registerRpc("get_shop_catalog", rpcGetShopCatalog);
|
||||
initializer.registerRpc("buy_currency", rpcBuyCurrency);
|
||||
|
||||
|
||||
|
||||
// Leaderboard RPCs
|
||||
initializer.registerRpc("get_leaderboard_stats", rpcGetLeaderboardStats);
|
||||
initializer.registerRpc("admin_update_stats", rpcAdminUpdateStats);
|
||||
@@ -60,75 +59,32 @@ function InitModule(ctx, logger, nk, initializer) {
|
||||
initializer.registerRpc("delete_mail", rpcDeleteMail);
|
||||
initializer.registerRpc("save_mail_state", rpcSaveMailState);
|
||||
|
||||
|
||||
|
||||
// Steam auth hooks
|
||||
initializer.registerAfterAuthenticateSteam(afterAuthenticateSteam);
|
||||
|
||||
// Shop and Economy RPCs
|
||||
initializer.registerRpc("purchase_item", rpcPurchaseItem);
|
||||
initializer.registerRpc("get_shop_catalog", rpcGetShopCatalog);
|
||||
initializer.registerRpc("buy_currency", rpcBuyCurrency);
|
||||
initializer.registerRpc("admin_set_featured_banners", rpcAdminSetFeaturedBanners);
|
||||
initializer.registerRpc("admin_get_featured_banners", rpcAdminGetFeaturedBanners);
|
||||
|
||||
// Create default native leaderboard
|
||||
// id: "global_high_score", authoritative: true, sort: "desc", operator: "best", reset: None
|
||||
try { nk.leaderboardCreate("global_high_score", true, "desc", "best", null, {}); } catch(e) {}
|
||||
try { nk.leaderboardCreate("global_high_score", true, "desc", "best", null, {}); } catch (e) { }
|
||||
|
||||
logger.info("Tekton admin module loaded");
|
||||
logger.info("Tekton core module loaded");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Authorization Helpers
|
||||
// =============================================================================
|
||||
|
||||
|
||||
var ADMIN_ROLES = ["admin", "moderator", "owner"];
|
||||
|
||||
// =============================================================================
|
||||
// Shop Catalog Definitions
|
||||
// =============================================================================
|
||||
// To add a new item: append ONE entry to SHOP_CATALOG_DEFS.
|
||||
// Fields:
|
||||
// id (String) — must match item_id in game inventory + SkinManager
|
||||
// name (String) — display name shown in shop
|
||||
// category (String) — "head" | "costume" | "glove" | "accessory"
|
||||
// gold (Number) — gold price (0 = not sold for gold)
|
||||
// star (Number) — star price (0 = not sold for star)
|
||||
// rarity (String) — "Common" | "Rare" | "Epic" | "Legendary"
|
||||
// character (String) — (optional) which character the skin targets, e.g. "Oldpop"
|
||||
|
||||
// [BEGIN_SHOP_CATALOG_DEFS]
|
||||
var SHOP_CATALOG_DEFS = [
|
||||
// ── HEAD ────────────────────────────────────────────────────────────
|
||||
{ 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" },
|
||||
// ── COSTUME ────────────────────────────────────────────────────────────
|
||||
{ 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" },
|
||||
// ── GLOVE ────────────────────────────────────────────────────────────
|
||||
{ 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" },
|
||||
];
|
||||
// [END_SHOP_CATALOG_DEFS]
|
||||
|
||||
/** Groups SHOP_CATALOG_DEFS by category for the shop RPC response. */
|
||||
function buildShopCatalog() {
|
||||
var catalog = {};
|
||||
for (var i = 0; i < SHOP_CATALOG_DEFS.length; i++) {
|
||||
var def = SHOP_CATALOG_DEFS[i];
|
||||
var cat = def.category;
|
||||
if (!catalog[cat]) catalog[cat] = [];
|
||||
var entry = {
|
||||
id: def.id,
|
||||
name: def.name,
|
||||
gold: def.gold || 0,
|
||||
star: def.star || 0,
|
||||
rarity: def.rarity || "Common"
|
||||
};
|
||||
if (def.character) entry.character = def.character;
|
||||
catalog[cat].push(entry);
|
||||
}
|
||||
return catalog;
|
||||
}
|
||||
|
||||
|
||||
function isAdmin(ctx, nk) {
|
||||
if (!ctx.userId) return false;
|
||||
@@ -185,7 +141,7 @@ function rpcSendLobbyInvite(ctx, logger, nk, payload) {
|
||||
if (!ctx.userId) throw new Error("Not authenticated");
|
||||
var req = JSON.parse(payload || "{}");
|
||||
var toUserId = req.to_user_id;
|
||||
var matchId = req.match_id;
|
||||
var matchId = req.match_id;
|
||||
if (!toUserId || !matchId) throw new Error("Missing to_user_id or match_id");
|
||||
|
||||
var sender = nk.accountGetId(ctx.userId);
|
||||
@@ -227,7 +183,7 @@ function afterAuthenticateSteam(ctx, logger, nk, out, request) {
|
||||
metadata = typeof account.user.metadata === "string"
|
||||
? JSON.parse(account.user.metadata || "{}")
|
||||
: (account.user.metadata || {});
|
||||
} catch (e) {}
|
||||
} catch (e) { }
|
||||
|
||||
if (!metadata.role) {
|
||||
metadata.role = "player";
|
||||
@@ -383,7 +339,7 @@ function rpcAdminGetBanList(ctx, logger, nk, payload) {
|
||||
""
|
||||
);
|
||||
|
||||
var bans = result.objects ? result.objects.map(function(obj) { return obj.value; }) : [];
|
||||
var bans = result.objects ? result.objects.map(function (obj) { return obj.value; }) : [];
|
||||
|
||||
return JSON.stringify({ bans: bans });
|
||||
} catch (e) {
|
||||
@@ -522,14 +478,9 @@ function rpcAdminSetUserRole(ctx, logger, nk, payload) {
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Store / Economy RPCs
|
||||
// Admin Wallet RPCs
|
||||
// =============================================================================
|
||||
|
||||
function rpcGetShopCatalog(ctx, logger, nk, payload) {
|
||||
if (!ctx.userId) throw new Error("Not authenticated");
|
||||
return JSON.stringify({ catalog: buildShopCatalog() });
|
||||
}
|
||||
|
||||
function rpcAdminTopupGold(ctx, logger, nk, payload) {
|
||||
requireAdmin(ctx, nk);
|
||||
try {
|
||||
@@ -542,6 +493,7 @@ function rpcAdminTopupGold(ctx, logger, nk, payload) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// Admin Clear Global Chat RPC
|
||||
// =============================================================================
|
||||
@@ -589,73 +541,7 @@ function rpcAdminClearGlobalChat(ctx, logger, nk, payload) {
|
||||
}
|
||||
|
||||
|
||||
function rpcBuyCurrency(ctx, logger, nk, payload) {
|
||||
if (!ctx.userId) throw new Error("Not authenticated");
|
||||
|
||||
var request = JSON.parse(payload);
|
||||
var packageId = request.package_id;
|
||||
|
||||
var changeset = { "gold": 0, "star": 0 };
|
||||
|
||||
if (packageId === "gold_100") changeset["gold"] = 100;
|
||||
else if (packageId === "gold_500") changeset["gold"] = 550;
|
||||
else if (packageId === "gold_1000") changeset["gold"] = 1150;
|
||||
else if (packageId === "gold_2000") changeset["gold"] = 2400;
|
||||
else if (packageId === "gold_5000") changeset["gold"] = 6250;
|
||||
else if (packageId === "gold_10000") changeset["gold"] = 13000;
|
||||
else if (packageId === "star_100") { changeset["star"] = 100; changeset["gold"] = -500; }
|
||||
else if (packageId === "star_250") { changeset["star"] = 250; changeset["gold"] = -1100; }
|
||||
else if (packageId === "star_600") { changeset["star"] = 600; changeset["gold"] = -2500; }
|
||||
else throw new Error("Invalid package ID");
|
||||
|
||||
try {
|
||||
nk.walletUpdate(ctx.userId, changeset, {}, true);
|
||||
logger.info("User " + ctx.userId + " bought currency package " + packageId);
|
||||
return JSON.stringify({ success: true, package_id: packageId });
|
||||
} catch (e) {
|
||||
logger.error("Currency purchase failed: " + e.message);
|
||||
throw new Error("Currency purchase failed: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function rpcPurchaseItem(ctx, logger, nk, payload) {
|
||||
if (!ctx.userId) throw new Error("Not authenticated");
|
||||
|
||||
var request = JSON.parse(payload);
|
||||
var itemId = request.item_id;
|
||||
var priceGold = request.price_gold || 0;
|
||||
var priceStar = request.price_star || 0;
|
||||
var category = request.category || "accessory"; // head, costume, glove, accessory
|
||||
|
||||
if (!itemId) throw new Error("Item ID required");
|
||||
|
||||
try {
|
||||
var changeset = {};
|
||||
if (priceGold > 0) changeset["gold"] = -priceGold;
|
||||
if (priceStar > 0) changeset["star"] = -priceStar;
|
||||
|
||||
// Check wallet and deduct
|
||||
// nk.walletUpdate throws an error if insufficient funds
|
||||
nk.walletUpdate(ctx.userId, changeset, {}, true);
|
||||
|
||||
// Record purchase in inventory
|
||||
var inventoryObj = {
|
||||
collection: "inventory",
|
||||
key: itemId,
|
||||
userId: ctx.userId,
|
||||
value: { category: category, purchased_at: new Date().toISOString() },
|
||||
permissionRead: 1,
|
||||
permissionWrite: 0
|
||||
};
|
||||
nk.storageWrite([inventoryObj]);
|
||||
|
||||
logger.info("User " + ctx.userId + " purchased " + itemId);
|
||||
return JSON.stringify({ success: true, item: itemId });
|
||||
} catch (e) {
|
||||
logger.error("Purchase failed: " + e.message);
|
||||
throw new Error("Purchase failed: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Daily Rewards RPCs
|
||||
@@ -667,7 +553,7 @@ function rpcClaimDailyReward(ctx, logger, nk, payload) {
|
||||
var currentMonth = now.toISOString().substring(5, 7); // e.g. "05"
|
||||
var todayStr = now.toISOString().substring(0, 10);
|
||||
var todayIndex = now.getUTCDate() - 1; // 0 to 30
|
||||
|
||||
|
||||
var stateObjs = nk.storageRead([{ collection: "daily_rewards", key: "state", userId: ctx.userId }]);
|
||||
var state = { claimed_days: [], last_claim_date: "", month: "" };
|
||||
if (stateObjs && stateObjs.length > 0) {
|
||||
@@ -676,7 +562,7 @@ function rpcClaimDailyReward(ctx, logger, nk, payload) {
|
||||
state.month = val.month || "";
|
||||
if (typeof val.claimed_days === 'number') {
|
||||
var arr = [];
|
||||
for (var i=0; i<val.claimed_days; i++) arr.push(i);
|
||||
for (var i = 0; i < val.claimed_days; i++) arr.push(i);
|
||||
state.claimed_days = arr;
|
||||
} else if (Array.isArray(val.claimed_days)) {
|
||||
state.claimed_days = val.claimed_days;
|
||||
@@ -684,46 +570,46 @@ function rpcClaimDailyReward(ctx, logger, nk, payload) {
|
||||
state.claimed_days = [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (state.month !== currentMonth) {
|
||||
state.claimed_days = [];
|
||||
state.month = currentMonth;
|
||||
}
|
||||
|
||||
|
||||
if (state.last_claim_date === todayStr) {
|
||||
throw new Error("Already claimed today");
|
||||
}
|
||||
|
||||
|
||||
var configObjs = nk.storageRead([{ collection: "config", key: "daily_rewards", userId: "00000000-0000-0000-0000-000000000000" }]);
|
||||
var config = {};
|
||||
if (configObjs && configObjs.length > 0) {
|
||||
config = configObjs[0].value;
|
||||
}
|
||||
|
||||
|
||||
var monthRewards = config[currentMonth];
|
||||
if (!monthRewards || monthRewards.length === 0) {
|
||||
monthRewards = [];
|
||||
for (var i = 0; i < 31; i++) {
|
||||
monthRewards.push({ type: "star", amount: Math.min(10 + i * 5, 100) });
|
||||
monthRewards.push({ type: "star", amount: Math.min(10 + i * 5, 100) });
|
||||
}
|
||||
}
|
||||
|
||||
var dayIndex = todayIndex;
|
||||
|
||||
var dayIndex = todayIndex;
|
||||
if (dayIndex >= monthRewards.length) {
|
||||
throw new Error("Already claimed all rewards for this month");
|
||||
}
|
||||
if (state.claimed_days.indexOf(dayIndex) !== -1) {
|
||||
throw new Error("Already claimed today's reward");
|
||||
}
|
||||
|
||||
|
||||
var rewardData = monthRewards[dayIndex];
|
||||
if (typeof rewardData === "number") {
|
||||
rewardData = { type: "star", amount: rewardData };
|
||||
}
|
||||
|
||||
|
||||
var rewardType = rewardData.type || "star";
|
||||
var rewardAmount = rewardData.amount || 0;
|
||||
|
||||
|
||||
if (rewardType === "star" || rewardType === "gold") {
|
||||
var changes = {};
|
||||
changes[rewardType] = rewardAmount;
|
||||
@@ -744,10 +630,10 @@ function rpcClaimDailyReward(ctx, logger, nk, payload) {
|
||||
permissionWrite: 0
|
||||
}]);
|
||||
}
|
||||
|
||||
|
||||
state.claimed_days.push(dayIndex);
|
||||
state.last_claim_date = todayStr;
|
||||
|
||||
|
||||
nk.storageWrite([{
|
||||
collection: "daily_rewards",
|
||||
key: "state",
|
||||
@@ -756,7 +642,7 @@ function rpcClaimDailyReward(ctx, logger, nk, payload) {
|
||||
permissionRead: 1,
|
||||
permissionWrite: 0
|
||||
}]);
|
||||
|
||||
|
||||
return JSON.stringify({ success: true, reward_type: rewardType, reward_amount: rewardAmount, day: dayIndex + 1 });
|
||||
}
|
||||
|
||||
@@ -766,7 +652,7 @@ function rpcGetDailyRewardState(ctx, logger, nk, payload) {
|
||||
var currentMonth = now.toISOString().substring(5, 7); // e.g. "05"
|
||||
var todayStr = now.toISOString().substring(0, 10);
|
||||
var todayIndex = now.getUTCDate() - 1;
|
||||
|
||||
|
||||
var stateObjs = nk.storageRead([{ collection: "daily_rewards", key: "state", userId: ctx.userId }]);
|
||||
var state = { claimed_days: [], last_claim_date: "", month: "" };
|
||||
if (stateObjs && stateObjs.length > 0) {
|
||||
@@ -775,7 +661,7 @@ function rpcGetDailyRewardState(ctx, logger, nk, payload) {
|
||||
state.month = val.month || "";
|
||||
if (typeof val.claimed_days === 'number') {
|
||||
var arr = [];
|
||||
for (var i=0; i<val.claimed_days; i++) arr.push(i);
|
||||
for (var i = 0; i < val.claimed_days; i++) arr.push(i);
|
||||
state.claimed_days = arr;
|
||||
} else if (Array.isArray(val.claimed_days)) {
|
||||
state.claimed_days = val.claimed_days;
|
||||
@@ -787,7 +673,7 @@ function rpcGetDailyRewardState(ctx, logger, nk, payload) {
|
||||
state.claimed_days = [];
|
||||
state.month = currentMonth;
|
||||
}
|
||||
|
||||
|
||||
var configObjs = nk.storageRead([{ collection: "config", key: "daily_rewards", userId: "00000000-0000-0000-0000-000000000000" }]);
|
||||
var config = {};
|
||||
if (configObjs && configObjs.length > 0) {
|
||||
@@ -797,10 +683,10 @@ function rpcGetDailyRewardState(ctx, logger, nk, payload) {
|
||||
if (!monthRewards || monthRewards.length === 0) {
|
||||
monthRewards = [];
|
||||
for (var i = 0; i < 31; i++) {
|
||||
monthRewards.push({ type: "star", amount: Math.min(10 + i * 5, 100) });
|
||||
monthRewards.push({ type: "star", amount: Math.min(10 + i * 5, 100) });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return JSON.stringify({
|
||||
state: state,
|
||||
month_rewards: monthRewards,
|
||||
@@ -913,7 +799,7 @@ function rpcSearchUsers(ctx, logger, nk, payload) {
|
||||
var request = {};
|
||||
try {
|
||||
request = JSON.parse(payload || "{}");
|
||||
} catch (e) {}
|
||||
} catch (e) { }
|
||||
|
||||
var query = request.query || "";
|
||||
|
||||
@@ -921,21 +807,21 @@ function rpcSearchUsers(ctx, logger, nk, payload) {
|
||||
var users = [];
|
||||
var sql = "";
|
||||
var params = [];
|
||||
|
||||
|
||||
if (query === "") {
|
||||
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 + "%"];
|
||||
}
|
||||
|
||||
|
||||
var rows = nk.sqlQuery(sql, params);
|
||||
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var row = rows[i];
|
||||
var metadata = {};
|
||||
try { metadata = JSON.parse(row.metadata || "{}"); } catch(e) {}
|
||||
|
||||
try { metadata = JSON.parse(row.metadata || "{}"); } catch (e) { }
|
||||
|
||||
users.push({
|
||||
user_id: row.id,
|
||||
username: row.username || "",
|
||||
@@ -960,13 +846,13 @@ function rpcGetLeaderboardStats(ctx, logger, nk, payload) {
|
||||
var limit = 50;
|
||||
var records = nk.leaderboardRecordsList("global_high_score", null, limit, "");
|
||||
var leaderboardData = [];
|
||||
|
||||
|
||||
var ownerRecords = records.records || [];
|
||||
for (var i = 0; i < ownerRecords.length; i++) {
|
||||
var record = ownerRecords[i];
|
||||
var metadata = {};
|
||||
try { metadata = JSON.parse(record.metadata || "{}"); } catch (e) {}
|
||||
|
||||
try { metadata = JSON.parse(record.metadata || "{}"); } catch (e) { }
|
||||
|
||||
leaderboardData.push({
|
||||
user_id: record.ownerId,
|
||||
username: record.username,
|
||||
@@ -978,7 +864,7 @@ function rpcGetLeaderboardStats(ctx, logger, nk, payload) {
|
||||
games_won: metadata.games_won || 0
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return JSON.stringify({ leaderboard: leaderboardData });
|
||||
} catch (e) {
|
||||
logger.error("Failed to get native leaderboard stats: " + e);
|
||||
@@ -1023,7 +909,7 @@ function rpcSyncLeaderboard(ctx, logger, nk, payload) {
|
||||
var result = nk.storageList(null, "stats", 100, "");
|
||||
var statsObjects = result.objects || [];
|
||||
var userGroup = {};
|
||||
|
||||
|
||||
for (var i = 0; i < statsObjects.length; i++) {
|
||||
var obj = statsObjects[i];
|
||||
var userId = obj.userId;
|
||||
@@ -1032,7 +918,7 @@ function rpcSyncLeaderboard(ctx, logger, nk, payload) {
|
||||
value = (typeof obj.value === "string") ? JSON.parse(obj.value) : obj.value;
|
||||
} catch (e) { continue; }
|
||||
if (!value) continue;
|
||||
|
||||
|
||||
if (!userGroup[userId]) {
|
||||
userGroup[userId] = {
|
||||
high_score: value.high_score || 0,
|
||||
@@ -1046,14 +932,14 @@ function rpcSyncLeaderboard(ctx, logger, nk, payload) {
|
||||
userGroup[userId].games_played = Math.max(userGroup[userId].games_played, value.games_played || 0);
|
||||
userGroup[userId].games_won = Math.max(userGroup[userId].games_won, value.games_won || 0);
|
||||
}
|
||||
|
||||
|
||||
// Prioritize avatar and character from game_stats or if current is empty
|
||||
if (obj.key === "game_stats" || !userGroup[userId].avatar_url) {
|
||||
if (value.avatar_url) userGroup[userId].avatar_url = value.avatar_url;
|
||||
if (value.loadout_character) userGroup[userId].loadout_character = value.loadout_character;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Phase 2: Read profiles collection to get loadout and avatars!
|
||||
var profileResult = nk.storageList(null, "profiles", 100, "");
|
||||
var profileObjects = profileResult.objects || [];
|
||||
@@ -1065,7 +951,7 @@ function rpcSyncLeaderboard(ctx, logger, nk, payload) {
|
||||
try {
|
||||
value = (typeof obj.value === "string") ? JSON.parse(obj.value) : obj.value;
|
||||
} catch (e) { continue; }
|
||||
|
||||
|
||||
if (!userGroup[userId]) {
|
||||
userGroup[userId] = {
|
||||
high_score: 0, games_played: 0, games_won: 0,
|
||||
@@ -1076,16 +962,16 @@ function rpcSyncLeaderboard(ctx, logger, nk, payload) {
|
||||
if (value.avatar_url && !userGroup[userId].avatar_url) userGroup[userId].avatar_url = value.avatar_url;
|
||||
if (value.loadout_character && !userGroup[userId].loadout_character) userGroup[userId].loadout_character = value.loadout_character;
|
||||
}
|
||||
|
||||
|
||||
var count = 0;
|
||||
var debugLogs = [];
|
||||
for (var uid in userGroup) {
|
||||
try {
|
||||
var stats = userGroup[uid];
|
||||
var account = nk.accountGetId(uid);
|
||||
var meta = {
|
||||
games_played: stats.games_played || 0,
|
||||
games_won: stats.games_won || 0,
|
||||
var meta = {
|
||||
games_played: stats.games_played || 0,
|
||||
games_won: stats.games_won || 0,
|
||||
avatar_url: stats.avatar_url || account.user.avatarUrl || "res://assets/graphics/character_selection/sc_characters/sc_copper.png",
|
||||
loadout_character: stats.loadout_character || "Copper"
|
||||
};
|
||||
@@ -1108,10 +994,10 @@ function rpcSyncLeaderboard(ctx, logger, nk, payload) {
|
||||
function rpcChangeCredentials(ctx, logger, nk, payload) {
|
||||
if (!ctx.userId) throw new Error("Not authenticated");
|
||||
var req = {};
|
||||
try { req = JSON.parse(payload || "{}"); } catch (e) {}
|
||||
|
||||
try { req = JSON.parse(payload || "{}"); } catch (e) { }
|
||||
|
||||
var account = nk.accountGetId(ctx.userId);
|
||||
|
||||
|
||||
// If not a guest (has email), verify current password and unlink
|
||||
if (account.email) {
|
||||
if (!req.current_password) throw new Error("Current password required");
|
||||
@@ -1122,7 +1008,7 @@ function rpcChangeCredentials(ctx, logger, nk, payload) {
|
||||
}
|
||||
nk.unlinkEmail(ctx.userId, account.email, req.current_password);
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
nk.linkEmail(ctx.userId, req.new_email, req.new_password);
|
||||
} catch (e) {
|
||||
@@ -1130,7 +1016,7 @@ function rpcChangeCredentials(ctx, logger, nk, payload) {
|
||||
if (account.email) nk.linkEmail(ctx.userId, account.email, req.current_password);
|
||||
throw new Error("Failed to set new credentials: " + e.message);
|
||||
}
|
||||
|
||||
|
||||
return JSON.stringify({ success: true });
|
||||
}
|
||||
|
||||
@@ -1138,10 +1024,10 @@ function rpcChangeCredentials(ctx, logger, nk, payload) {
|
||||
function rpcResetStats(ctx, logger, nk, payload) {
|
||||
if (!ctx.userId) throw new Error("Not authenticated");
|
||||
var account = nk.accountGetId(ctx.userId);
|
||||
|
||||
|
||||
// Delete native leaderboard rank
|
||||
try { nk.leaderboardRecordDelete("global_high_score", ctx.userId); } catch (e) {}
|
||||
|
||||
try { nk.leaderboardRecordDelete("global_high_score", ctx.userId); } catch (e) { }
|
||||
|
||||
// Wipe storage stats
|
||||
var zeros = { games_played: 0, games_won: 0, high_score: 0, total_kills: 0, total_deaths: 0 };
|
||||
nk.storageWrite([{
|
||||
@@ -1152,7 +1038,7 @@ function rpcResetStats(ctx, logger, nk, payload) {
|
||||
permissionRead: 2,
|
||||
permissionWrite: 1
|
||||
}]);
|
||||
|
||||
|
||||
return JSON.stringify({ success: true });
|
||||
}
|
||||
|
||||
@@ -1166,11 +1052,11 @@ function rpcAdminListUsers(ctx, logger, nk, payload) {
|
||||
try {
|
||||
var users = [];
|
||||
var rows = nk.sqlQuery("SELECT id, username, display_name, metadata, create_time FROM users WHERE id != '00000000-0000-0000-0000-000000000000' ORDER BY create_time DESC LIMIT 500", []);
|
||||
|
||||
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var row = rows[i];
|
||||
var metadata = {};
|
||||
try { metadata = JSON.parse(row.metadata || "{}"); } catch(e) {}
|
||||
try { metadata = JSON.parse(row.metadata || "{}"); } catch (e) { }
|
||||
users.push({
|
||||
user_id: row.id,
|
||||
username: row.username || "",
|
||||
@@ -1215,7 +1101,7 @@ function rpcAdminDeleteUsers(ctx, logger, nk, payload) {
|
||||
// Check if target is admin — don't allow deleting admins
|
||||
var account = nk.accountGetId(uid);
|
||||
var meta = {};
|
||||
try { meta = JSON.parse(account.user.metadata || "{}"); } catch(e) {}
|
||||
try { meta = JSON.parse(account.user.metadata || "{}"); } catch (e) { }
|
||||
if (ADMIN_ROLES.indexOf(meta.role || "") !== -1) {
|
||||
failed.push({ user_id: uid, reason: "Cannot delete admin account" });
|
||||
continue;
|
||||
@@ -1304,26 +1190,26 @@ function rpcAdminDeleteStats(ctx, logger, nk, payload) {
|
||||
|
||||
function rpcAdminSyncLeaderboard(ctx, logger, nk, payload) {
|
||||
requireAdmin(ctx, nk);
|
||||
|
||||
|
||||
try {
|
||||
var result = nk.storageList(null, "stats", 100, "");
|
||||
var statsObjects = result.objects || [];
|
||||
var userGroup = {}; // [userId] = { highScore, gamesPlayed, gamesWon, avatar }
|
||||
|
||||
|
||||
// Phase 1: Group and merge
|
||||
for (var i = 0; i < statsObjects.length; i++) {
|
||||
var obj = statsObjects[i];
|
||||
var userId = obj.userId;
|
||||
var key = obj.key; // "stats" or "game_stats"
|
||||
var value;
|
||||
|
||||
|
||||
try {
|
||||
value = JSON.parse(obj.value || "{}");
|
||||
} catch (jsonErr) {
|
||||
logger.error("Skipping key " + key + " for user " + userId + " due to corrupt JSON");
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if (!userGroup[userId]) {
|
||||
userGroup[userId] = {
|
||||
high_score: value.high_score || 0,
|
||||
@@ -1338,37 +1224,37 @@ function rpcAdminSyncLeaderboard(ctx, logger, nk, payload) {
|
||||
userGroup[userId].games_played += (value.games_played || 0);
|
||||
userGroup[userId].games_won += (value.games_won || 0);
|
||||
}
|
||||
|
||||
|
||||
// Prioritize avatar and character from game_stats
|
||||
if (key === "game_stats" || !userGroup[userId].avatar_url) {
|
||||
try {
|
||||
if (value.avatar_url) userGroup[userId].avatar_url = value.avatar_url;
|
||||
if (value.loadout_character) userGroup[userId].loadout_character = value.loadout_character;
|
||||
} catch (e) {}
|
||||
} catch (e) { }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Phase 2: Write to native leaderboard
|
||||
var count = 0;
|
||||
for (var userId in userGroup) {
|
||||
try {
|
||||
var stats = userGroup[userId];
|
||||
var account = nk.accountGetId(userId);
|
||||
|
||||
|
||||
var metadata = {
|
||||
games_played: stats.games_played,
|
||||
games_won: stats.games_won,
|
||||
avatar_url: stats.avatar_url || account.user.avatarUrl || "",
|
||||
loadout_character: stats.loadout_character || "Copper"
|
||||
};
|
||||
|
||||
|
||||
nk.leaderboardRecordWrite("global_high_score", userId, account.user.username, stats.high_score, 0, metadata);
|
||||
count++;
|
||||
} catch (inner) {
|
||||
logger.error("Failed to sync merged record for " + userId + ": " + inner);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return JSON.stringify({ success: true, synced: count });
|
||||
} catch (e) {
|
||||
logger.error("Leaderboard sync failed: " + e);
|
||||
@@ -1393,7 +1279,7 @@ function rpcSendFriendRequest(ctx, logger, nk, payload) {
|
||||
if (!ctx.userId) throw new Error("Not authenticated");
|
||||
|
||||
var request = {};
|
||||
try { request = JSON.parse(payload || "{}"); } catch (e) {}
|
||||
try { request = JSON.parse(payload || "{}"); } catch (e) { }
|
||||
|
||||
var targetUserId = request.user_id || "";
|
||||
if (!targetUserId) throw new Error("user_id is required");
|
||||
@@ -1420,12 +1306,12 @@ function rpcSendLobbyInvite(ctx, logger, nk, payload) {
|
||||
if (!ctx.userId) throw new Error("Not authenticated");
|
||||
|
||||
var request = {};
|
||||
try { request = JSON.parse(payload || "{}"); } catch (e) {}
|
||||
try { request = JSON.parse(payload || "{}"); } catch (e) { }
|
||||
|
||||
var toUserId = request.to_user_id || "";
|
||||
var matchId = request.match_id || "";
|
||||
var matchId = request.match_id || "";
|
||||
if (!toUserId) throw new Error("to_user_id is required");
|
||||
if (!matchId) throw new Error("match_id is required");
|
||||
if (!matchId) throw new Error("match_id is required");
|
||||
|
||||
var senderAccount = nk.accountGetId(ctx.userId);
|
||||
var senderName = senderAccount.user.displayName || senderAccount.user.username || "Someone";
|
||||
@@ -1450,16 +1336,16 @@ function rpcSendLobbyInvite(ctx, logger, nk, payload) {
|
||||
function rpcAdminSendMail(ctx, logger, nk, payload) {
|
||||
requireAdmin(ctx, nk);
|
||||
var request = JSON.parse(payload || "{}");
|
||||
|
||||
|
||||
var nowStr = new Date().toISOString();
|
||||
var startDate = request.start_date || nowStr;
|
||||
var endDate = request.end_date || "";
|
||||
|
||||
|
||||
// Auto-delete / expire after 30 days from start
|
||||
var startObj = new Date(startDate);
|
||||
startObj.setDate(startObj.getDate() + 30);
|
||||
var expiryDate = startObj.toISOString();
|
||||
|
||||
|
||||
var mailObj = {
|
||||
id: nk.uuidv4(),
|
||||
title: request.title || "Announcement",
|
||||
@@ -1471,7 +1357,7 @@ function rpcAdminSendMail(ctx, logger, nk, payload) {
|
||||
expiry_date: expiryDate,
|
||||
rewards: request.rewards || []
|
||||
};
|
||||
|
||||
|
||||
if (request.target_user_id) {
|
||||
mailObj.type = "personal";
|
||||
var invObjs = nk.storageRead([{ collection: "inbox", key: "personal", userId: request.target_user_id }]);
|
||||
@@ -1507,20 +1393,20 @@ function rpcAdminSendMail(ctx, logger, nk, payload) {
|
||||
}]);
|
||||
logger.info("Global mail sent");
|
||||
}
|
||||
|
||||
|
||||
return JSON.stringify({ success: true, mail: mailObj });
|
||||
}
|
||||
|
||||
function rpcGetMail(ctx, logger, nk, payload) {
|
||||
if (!ctx.userId) throw new Error("Not authenticated");
|
||||
|
||||
|
||||
var personalObjs = nk.storageRead([{ collection: "inbox", key: "personal", userId: ctx.userId }]);
|
||||
var globalObjs = nk.storageRead([{ collection: "config", key: "global_mail", userId: "00000000-0000-0000-0000-000000000000" }]);
|
||||
var stateObjs = nk.storageRead([{ collection: "inbox", key: "state", userId: ctx.userId }]);
|
||||
|
||||
|
||||
var personalMails = (personalObjs && personalObjs.length > 0) ? (personalObjs[0].value.mails || []) : [];
|
||||
var globalMails = (globalObjs && globalObjs.length > 0) ? (globalObjs[0].value.mails || []) : [];
|
||||
|
||||
|
||||
var state = { claimed_ids: [], deleted_ids: [], read_ids: [] };
|
||||
if (stateObjs && stateObjs.length > 0) {
|
||||
var val = stateObjs[0].value;
|
||||
@@ -1528,33 +1414,33 @@ function rpcGetMail(ctx, logger, nk, payload) {
|
||||
state.deleted_ids = val.deleted_ids || [];
|
||||
state.read_ids = val.read_ids || [];
|
||||
}
|
||||
|
||||
|
||||
var allMails = personalMails.concat(globalMails);
|
||||
var filteredMails = [];
|
||||
var nowStr = new Date().toISOString();
|
||||
|
||||
|
||||
for (var i = 0; i < allMails.length; i++) {
|
||||
var mail = allMails[i];
|
||||
if (state.deleted_ids.indexOf(mail.id) !== -1) continue;
|
||||
|
||||
|
||||
// Expiry check
|
||||
if (mail.expiry_date && nowStr > mail.expiry_date) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Scheduled start
|
||||
if (mail.start_date && nowStr < mail.start_date) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Scheduled end
|
||||
if (mail.type === "global" && mail.end_date && nowStr > mail.end_date) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
filteredMails.push(mail);
|
||||
}
|
||||
|
||||
|
||||
return JSON.stringify({ mails: filteredMails, state: state });
|
||||
}
|
||||
|
||||
@@ -1563,12 +1449,12 @@ function rpcClaimMailReward(ctx, logger, nk, payload) {
|
||||
var request = JSON.parse(payload || "{}");
|
||||
var mailId = request.mail_id;
|
||||
if (!mailId) throw new Error("mail_id required");
|
||||
|
||||
|
||||
// fetch all mails to find it
|
||||
var personalObjs = nk.storageRead([{ collection: "inbox", key: "personal", userId: ctx.userId }]);
|
||||
var globalObjs = nk.storageRead([{ collection: "config", key: "global_mail", userId: "00000000-0000-0000-0000-000000000000" }]);
|
||||
var stateObjs = nk.storageRead([{ collection: "inbox", key: "state", userId: ctx.userId }]);
|
||||
|
||||
|
||||
var state = { claimed_ids: [], deleted_ids: [], read_ids: [] };
|
||||
if (stateObjs && stateObjs.length > 0) {
|
||||
var val = stateObjs[0].value;
|
||||
@@ -1576,15 +1462,15 @@ function rpcClaimMailReward(ctx, logger, nk, payload) {
|
||||
state.deleted_ids = val.deleted_ids || [];
|
||||
state.read_ids = val.read_ids || [];
|
||||
}
|
||||
|
||||
|
||||
if (state.claimed_ids.indexOf(mailId) !== -1) {
|
||||
throw new Error("Reward already claimed");
|
||||
}
|
||||
|
||||
|
||||
var personalMails = (personalObjs && personalObjs.length > 0) ? (personalObjs[0].value.mails || []) : [];
|
||||
var globalMails = (globalObjs && globalObjs.length > 0) ? (globalObjs[0].value.mails || []) : [];
|
||||
var allMails = personalMails.concat(globalMails);
|
||||
|
||||
|
||||
var targetMail = null;
|
||||
for (var i = 0; i < allMails.length; i++) {
|
||||
if (allMails[i].id === mailId) {
|
||||
@@ -1592,28 +1478,28 @@ function rpcClaimMailReward(ctx, logger, nk, payload) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!targetMail) throw new Error("Mail not found");
|
||||
|
||||
|
||||
var rewards = targetMail.rewards || [];
|
||||
var starTotal = 0;
|
||||
var goldTotal = 0;
|
||||
|
||||
|
||||
// Support legacy dictionary if it exists
|
||||
if (!Array.isArray(rewards)) {
|
||||
starTotal = rewards.star || 0;
|
||||
goldTotal = rewards.gold || 0;
|
||||
rewards = []; // prevent array loop
|
||||
}
|
||||
|
||||
|
||||
var fragsToUpdate = {};
|
||||
var skinsToAdd = [];
|
||||
|
||||
|
||||
for (var j = 0; j < rewards.length; j++) {
|
||||
var r = rewards[j];
|
||||
var type = r.type || "star";
|
||||
var amount = r.amount || 0;
|
||||
|
||||
|
||||
if (type === "star") starTotal += amount;
|
||||
else if (type === "gold") goldTotal += amount;
|
||||
else if (type.startsWith("frag_") || type === "item") {
|
||||
@@ -1624,14 +1510,14 @@ function rpcClaimMailReward(ctx, logger, nk, payload) {
|
||||
if (r.id) skinsToAdd.push(r.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (starTotal > 0 || goldTotal > 0) {
|
||||
var changes = {};
|
||||
if (starTotal > 0) changes["star"] = starTotal;
|
||||
if (goldTotal > 0) changes["gold"] = goldTotal;
|
||||
nk.walletUpdate(ctx.userId, changes, {}, true);
|
||||
}
|
||||
|
||||
|
||||
if (Object.keys(fragsToUpdate).length > 0) {
|
||||
var invObjs = nk.storageRead([{ collection: "inventory", key: "fragments", userId: ctx.userId }]);
|
||||
var frags = {};
|
||||
@@ -1650,7 +1536,7 @@ function rpcClaimMailReward(ctx, logger, nk, payload) {
|
||||
permissionWrite: 0
|
||||
}]);
|
||||
}
|
||||
|
||||
|
||||
if (skinsToAdd.length > 0) {
|
||||
var skinWrites = [];
|
||||
for (var s = 0; s < skinsToAdd.length; s++) {
|
||||
@@ -1665,12 +1551,12 @@ function rpcClaimMailReward(ctx, logger, nk, payload) {
|
||||
}
|
||||
nk.storageWrite(skinWrites);
|
||||
}
|
||||
|
||||
|
||||
state.claimed_ids.push(mailId);
|
||||
if (state.read_ids.indexOf(mailId) === -1) {
|
||||
state.read_ids.push(mailId);
|
||||
}
|
||||
|
||||
|
||||
nk.storageWrite([{
|
||||
collection: "inbox",
|
||||
key: "state",
|
||||
@@ -1679,7 +1565,7 @@ function rpcClaimMailReward(ctx, logger, nk, payload) {
|
||||
permissionRead: 1,
|
||||
permissionWrite: 0
|
||||
}]);
|
||||
|
||||
|
||||
return JSON.stringify({ success: true, claimed_ids: state.claimed_ids });
|
||||
}
|
||||
|
||||
@@ -1688,7 +1574,7 @@ function rpcDeleteMail(ctx, logger, nk, payload) {
|
||||
var request = JSON.parse(payload || "{}");
|
||||
var mailId = request.mail_id;
|
||||
if (!mailId) throw new Error("mail_id required");
|
||||
|
||||
|
||||
var stateObjs = nk.storageRead([{ collection: "inbox", key: "state", userId: ctx.userId }]);
|
||||
var state = { claimed_ids: [], deleted_ids: [], read_ids: [] };
|
||||
if (stateObjs && stateObjs.length > 0) {
|
||||
@@ -1697,14 +1583,14 @@ function rpcDeleteMail(ctx, logger, nk, payload) {
|
||||
state.deleted_ids = val.deleted_ids || [];
|
||||
state.read_ids = val.read_ids || [];
|
||||
}
|
||||
|
||||
|
||||
if (state.deleted_ids.indexOf(mailId) === -1) {
|
||||
state.deleted_ids.push(mailId);
|
||||
}
|
||||
if (state.read_ids.indexOf(mailId) === -1) {
|
||||
state.read_ids.push(mailId);
|
||||
}
|
||||
|
||||
|
||||
nk.storageWrite([{
|
||||
collection: "inbox",
|
||||
key: "state",
|
||||
@@ -1713,7 +1599,7 @@ function rpcDeleteMail(ctx, logger, nk, payload) {
|
||||
permissionRead: 1,
|
||||
permissionWrite: 0
|
||||
}]);
|
||||
|
||||
|
||||
return JSON.stringify({ success: true, deleted_ids: state.deleted_ids });
|
||||
}
|
||||
|
||||
@@ -1794,7 +1680,7 @@ function rpcAdminListMail(ctx, logger, nk, payload) {
|
||||
|
||||
var allMails = globalMails.concat(personalMails);
|
||||
// Sort newest first
|
||||
allMails.sort(function(a, b) {
|
||||
allMails.sort(function (a, b) {
|
||||
return (b.date || "").localeCompare(a.date || "");
|
||||
});
|
||||
|
||||
@@ -1912,7 +1798,7 @@ function rpcAdminDeleteMailServer(ctx, logger, nk, payload) {
|
||||
var globalObjs = nk.storageRead([{ collection: "config", key: "global_mail", userId: "00000000-0000-0000-0000-000000000000" }]);
|
||||
var globalMails = (globalObjs && globalObjs.length > 0) ? (globalObjs[0].value.mails || []) : [];
|
||||
var before = globalMails.length;
|
||||
globalMails = globalMails.filter(function(m) { return m.id !== mailId; });
|
||||
globalMails = globalMails.filter(function (m) { return m.id !== mailId; });
|
||||
if (globalMails.length === before) throw new Error("Mail not found");
|
||||
nk.storageWrite([{
|
||||
collection: "config",
|
||||
@@ -1927,7 +1813,7 @@ function rpcAdminDeleteMailServer(ctx, logger, nk, payload) {
|
||||
var pObjs = nk.storageRead([{ collection: "inbox", key: "personal", userId: targetUserId }]);
|
||||
var personalMails = (pObjs && pObjs.length > 0) ? (pObjs[0].value.mails || []) : [];
|
||||
var pBefore = personalMails.length;
|
||||
personalMails = personalMails.filter(function(m) { return m.id !== mailId; });
|
||||
personalMails = personalMails.filter(function (m) { return m.id !== mailId; });
|
||||
if (personalMails.length === pBefore) throw new Error("Mail not found");
|
||||
nk.storageWrite([{
|
||||
collection: "inbox",
|
||||
@@ -1942,3 +1828,273 @@ function rpcAdminDeleteMailServer(ctx, logger, nk, payload) {
|
||||
logger.info("Admin deleted mail " + mailId + " from server by " + ctx.userId);
|
||||
return JSON.stringify({ success: true });
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Shop Catalog Definitions
|
||||
// =============================================================================
|
||||
|
||||
// [BEGIN_SHOP_CATALOG_DEFS]
|
||||
var SHOP_CATALOG_DEFS = [
|
||||
// ── HEAD ────────────────────────────────────────────────────────────
|
||||
{ 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" },
|
||||
// ── COSTUME ────────────────────────────────────────────────────────────
|
||||
{ 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" },
|
||||
// ── GLOVE ────────────────────────────────────────────────────────────
|
||||
{ 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" },
|
||||
];
|
||||
// [END_SHOP_CATALOG_DEFS]
|
||||
|
||||
// =============================================================================
|
||||
// Shop RPCs
|
||||
// =============================================================================
|
||||
|
||||
function buildShopCatalog() {
|
||||
var catalog = {};
|
||||
for (var i = 0; i < SHOP_CATALOG_DEFS.length; i++) {
|
||||
var def = SHOP_CATALOG_DEFS[i];
|
||||
var cat = def.category;
|
||||
if (!catalog[cat]) catalog[cat] = [];
|
||||
var entry = {
|
||||
id: def.id,
|
||||
name: def.name,
|
||||
gold: def.gold || 0,
|
||||
star: def.star || 0,
|
||||
rarity: def.rarity || "Common"
|
||||
};
|
||||
if (def.character) entry.character = def.character;
|
||||
catalog[cat].push(entry);
|
||||
}
|
||||
return catalog;
|
||||
}
|
||||
|
||||
function rpcGetShopCatalog(ctx, logger, nk, payload) {
|
||||
if (!ctx.userId) throw new Error("Not authenticated");
|
||||
var result = { catalog: buildShopCatalog(), featured_banners: [] };
|
||||
try {
|
||||
var objs = nk.storageRead([{ collection: "shop_config", key: "featured_banners", userId: "00000000-0000-0000-0000-000000000000" }]);
|
||||
if (objs && objs.length > 0) {
|
||||
var data = JSON.parse(objs[0].value);
|
||||
if (data && data.banners) result.featured_banners = data.banners;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn("No featured banners configured: " + e);
|
||||
}
|
||||
return JSON.stringify(result);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Currency Purchase RPC
|
||||
// =============================================================================
|
||||
|
||||
function rpcBuyCurrency(ctx, logger, nk, payload) {
|
||||
if (!ctx.userId) throw new Error("Not authenticated");
|
||||
|
||||
var request = JSON.parse(payload);
|
||||
var packageId = request.package_id;
|
||||
var receipt = request.receipt;
|
||||
var idempotencyKey = request.idempotency_key;
|
||||
|
||||
if (!packageId) throw new Error("Package ID required");
|
||||
if (!idempotencyKey) throw new Error("Idempotency key required");
|
||||
|
||||
try {
|
||||
var existing = nk.storageRead([{ collection: "receipts", key: idempotencyKey, userId: ctx.userId }]);
|
||||
if (existing && existing.length > 0) {
|
||||
return JSON.stringify({ success: true, package_id: packageId, duplicate: true, status: existing[0].value.status });
|
||||
}
|
||||
} catch (e) { }
|
||||
|
||||
var changeset = { "gold": 0, "star": 0 };
|
||||
var requiresVerification = false;
|
||||
|
||||
if (packageId === "gold_100") { changeset["gold"] = 100; requiresVerification = true; }
|
||||
else if (packageId === "gold_500") { changeset["gold"] = 550; requiresVerification = true; }
|
||||
else if (packageId === "gold_1000") { changeset["gold"] = 1150; requiresVerification = true; }
|
||||
else if (packageId === "gold_2000") { changeset["gold"] = 2400; requiresVerification = true; }
|
||||
else if (packageId === "gold_5000") { changeset["gold"] = 6250; requiresVerification = true; }
|
||||
else if (packageId === "gold_10000") { changeset["gold"] = 13000; requiresVerification = true; }
|
||||
else if (packageId === "star_100") { changeset["star"] = 100; changeset["gold"] = -500; }
|
||||
else if (packageId === "star_250") { changeset["star"] = 250; changeset["gold"] = -1100; }
|
||||
else if (packageId === "star_600") { changeset["star"] = 600; changeset["gold"] = -2500; }
|
||||
else throw new Error("Invalid package ID");
|
||||
|
||||
if (requiresVerification && !receipt) {
|
||||
var pendingObj = {
|
||||
collection: "receipts",
|
||||
key: idempotencyKey,
|
||||
userId: ctx.userId,
|
||||
value: { type: "currency", package_id: packageId, status: "pending", created_at: new Date().toISOString() },
|
||||
permissionRead: 1,
|
||||
permissionWrite: 0
|
||||
};
|
||||
nk.storageWrite([pendingObj]);
|
||||
return JSON.stringify({ success: true, status: "pending", package_id: packageId });
|
||||
}
|
||||
|
||||
try {
|
||||
if (changeset["gold"] !== 0 || changeset["star"] !== 0) {
|
||||
nk.walletUpdate(ctx.userId, changeset, {}, true);
|
||||
}
|
||||
|
||||
var receiptObj = {
|
||||
collection: "receipts",
|
||||
key: idempotencyKey,
|
||||
userId: ctx.userId,
|
||||
value: { type: "currency", package_id: packageId, changeset: changeset, receipt: receipt || null, status: "verified", processed_at: new Date().toISOString() },
|
||||
permissionRead: 1,
|
||||
permissionWrite: 0
|
||||
};
|
||||
nk.storageWrite([receiptObj]);
|
||||
|
||||
logger.info("User " + ctx.userId + " bought currency package " + packageId);
|
||||
return JSON.stringify({ success: true, status: "verified", package_id: packageId });
|
||||
} catch (e) {
|
||||
logger.error("Currency purchase failed: " + e.message);
|
||||
throw new Error("NotEnoughFunds");
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Item Purchase RPC
|
||||
// =============================================================================
|
||||
|
||||
function rpcPurchaseItem(ctx, logger, nk, payload) {
|
||||
if (!ctx.userId) throw new Error("Not authenticated");
|
||||
|
||||
var request = JSON.parse(payload);
|
||||
var itemId = request.item_id;
|
||||
var quantity = request.quantity || 1;
|
||||
var idempotencyKey = request.idempotency_key;
|
||||
|
||||
if (!itemId) throw new Error("Item ID required");
|
||||
if (quantity < 1) throw new Error("Invalid quantity");
|
||||
if (!idempotencyKey) throw new Error("Idempotency key required");
|
||||
|
||||
try {
|
||||
var existing = nk.storageRead([{ collection: "receipts", key: idempotencyKey, userId: ctx.userId }]);
|
||||
if (existing && existing.length > 0) {
|
||||
return JSON.stringify({ success: true, item: itemId, duplicate: true });
|
||||
}
|
||||
} catch (e) { }
|
||||
|
||||
var itemDef = null;
|
||||
for (var i = 0; i < SHOP_CATALOG_DEFS.length; i++) {
|
||||
if (SHOP_CATALOG_DEFS[i].id === itemId) {
|
||||
itemDef = SHOP_CATALOG_DEFS[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!itemDef) throw new Error("ItemNotFound");
|
||||
|
||||
var priceGold = (itemDef.gold || 0) * quantity;
|
||||
var priceStar = (itemDef.star || 0) * quantity;
|
||||
var category = itemDef.category || "accessory";
|
||||
|
||||
try {
|
||||
var changeset = {};
|
||||
if (priceGold > 0) changeset["gold"] = -priceGold;
|
||||
if (priceStar > 0) changeset["star"] = -priceStar;
|
||||
|
||||
if (priceGold > 0 || priceStar > 0) {
|
||||
nk.walletUpdate(ctx.userId, changeset, {}, true);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("Wallet update failed: " + e.message);
|
||||
throw new Error("NotEnoughFunds");
|
||||
}
|
||||
|
||||
try {
|
||||
var writes = [];
|
||||
writes.push({
|
||||
collection: "inventory",
|
||||
key: itemId,
|
||||
userId: ctx.userId,
|
||||
value: { category: category, purchased_at: new Date().toISOString(), quantity: quantity },
|
||||
permissionRead: 1,
|
||||
permissionWrite: 0
|
||||
});
|
||||
|
||||
writes.push({
|
||||
collection: "receipts",
|
||||
key: idempotencyKey,
|
||||
userId: ctx.userId,
|
||||
value: { type: "item", item_id: itemId, quantity: quantity, cost: { gold: priceGold, star: priceStar }, processed_at: new Date().toISOString() },
|
||||
permissionRead: 1,
|
||||
permissionWrite: 0
|
||||
});
|
||||
|
||||
nk.storageWrite(writes);
|
||||
|
||||
logger.info("User " + ctx.userId + " purchased " + itemId);
|
||||
return JSON.stringify({ success: true, item: itemId });
|
||||
} catch (e) {
|
||||
logger.error("Purchase failed: " + e.message);
|
||||
throw new Error("PurchaseFailed");
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Featured Banners (Shop) RPCs
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Admin sets featured banner slots.
|
||||
* Payload: { banners: [ { item_id, label }, ... ] } (max 3 slots)
|
||||
* Stored in system-owned storage: shop_config / featured_banners
|
||||
*/
|
||||
function rpcAdminSetFeaturedBanners(ctx, logger, nk, payload) {
|
||||
requireAdmin(ctx, nk);
|
||||
var req = JSON.parse(payload || "{}");
|
||||
var banners = req.banners || [];
|
||||
if (banners.length > 3) banners = banners.slice(0, 3);
|
||||
|
||||
// Validate each banner references a real catalog item
|
||||
for (var i = 0; i < banners.length; i++) {
|
||||
var itemId = banners[i].item_id || "";
|
||||
if (itemId === "") continue; // empty slot
|
||||
var found = false;
|
||||
for (var j = 0; j < SHOP_CATALOG_DEFS.length; j++) {
|
||||
if (SHOP_CATALOG_DEFS[j].id === itemId) { found = true; break; }
|
||||
}
|
||||
if (!found) throw new Error("Item not found in catalog: " + itemId);
|
||||
}
|
||||
|
||||
nk.storageWrite([{
|
||||
collection: "shop_config",
|
||||
key: "featured_banners",
|
||||
userId: "00000000-0000-0000-0000-000000000000",
|
||||
value: JSON.stringify({ banners: banners }),
|
||||
permissionRead: 2,
|
||||
permissionWrite: 0
|
||||
}]);
|
||||
|
||||
logger.info("Featured banners updated by admin " + ctx.userId);
|
||||
return JSON.stringify({ success: true, banners: banners });
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin reads current featured banner config.
|
||||
*/
|
||||
function rpcAdminGetFeaturedBanners(ctx, logger, nk, payload) {
|
||||
requireAdmin(ctx, nk);
|
||||
try {
|
||||
var objs = nk.storageRead([{ collection: "shop_config", key: "featured_banners", userId: "00000000-0000-0000-0000-000000000000" }]);
|
||||
if (objs && objs.length > 0) {
|
||||
var data = JSON.parse(objs[0].value);
|
||||
return JSON.stringify({ banners: data.banners || [] });
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn("Error reading featured banners: " + e);
|
||||
}
|
||||
return JSON.stringify({ banners: [] });
|
||||
}
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,13 @@
|
||||
local nk = require("nakama")
|
||||
|
||||
-- Require our lua modules from the lua subfolder so they are executed and their RPCs are registered
|
||||
require("lua.utils")
|
||||
require("lua.economy")
|
||||
require("lua.core")
|
||||
require("lua.admin")
|
||||
require("lua.daily_rewards")
|
||||
require("lua.user")
|
||||
require("lua.leaderboard")
|
||||
require("lua.inbox")
|
||||
|
||||
nk.logger_info("LUA TEST: main.lua entrypoint loaded successfully")
|
||||
Reference in New Issue
Block a user