diff --git a/Architecture-Server.md b/Architecture-Server.md index 2e58d2c..027d13c 100644 --- a/Architecture-Server.md +++ b/Architecture-Server.md @@ -1,2826 +1,396 @@ +# Tekton Armageddon - Server Architecture + -# Tekton Armageddon - Server Architecture (Full Function Reference) - -Complete per-function reference for the Nakama Lua backend at `server/nakama/lua/`. Every RPC, hook, and utility is documented with parameters, returns, errors, storage collections, wallet changesets, and dependencies. - -[Back to top](#top) - -## Table of Contents - -[Back to top](#top) - -1. [Module Loading (main.lua)](#1-module-loading-mainluau) -2. [Core / Auth Hooks (core.lua)](#2-core--auth-hooks-coreluau) -3. [Utilities (utils.lua)](#3-utilities-utilsluau) -4. [User Module (user.lua)](#4-user-module-userluau) -5. [Economy Module (economy.lua)](#5-economy-module-economyluau) -6. [Gacha Module (gacha.lua)](#6-gacha-module-gachaluau) -7. [Leaderboard Module (leaderboard.lua)](#7-leaderboard-module-leaderboardluau) -8. [Inbox/Mail Module (inbox.lua)](#8-inboxmail-module-inboxluau) -9. [Daily Rewards Module (daily_rewards.lua)](#9-daily-rewards-module-daily_rewardsluau) -10. [Admin Module (admin.lua)](#10-admin-module-adminluau) -11. [Storage Collections Reference](#11-storage-collections-reference) -12. [Wallet Keys and Currency](#12-wallet-keys-and-currency) -13. [Leaderboard Configuration](#13-leaderboard-configuration) -14. [Error Strings Reference](#14-error-strings-reference) -15. [Permission Levels](#15-permission-levels) - ---- - -[Back to top](#top) - -## 1. Module Loading (`main.lua`) - -[Back to top](#top) - -Entry point for the Nakama Lua runtime. Requires all sub-modules from `lua/` in the following order: - -``` -1. lua.utils -2. lua.economy -3. lua.core -4. lua.admin -5. lua.daily_rewards -6. lua.user -7. lua.leaderboard -8. lua.inbox -9. lua.gacha -``` - -Each sub-module registers its own RPCs and hooks via `nk.register_rpc()` and `nk.register_req_after()` as a side-effect of being required. The module files are cached by Lua's `require()` so repeated requires are no-ops. - -**Startup side-effect (from core.lua and leaderboard.lua):** -- `nk.leaderboard_create("global_high_score", true, "desc", "best", nil, {})` -- both files call this via `pcall`, so the second call is a safe no-op. - -[Back to top](#top) - -### 1.1 Registered RPCs (complete index) - -[Back to top](#top) - -All 48 RPCs registered by the backend: - -| RPC Name | Lua Function | Module | Auth Required | -|---|---|---|---| -| `get_user_profile` | `user.rpc_get_user_profile` | user.lua | No (fallback to caller) | -| `update_user_profile` | `user.rpc_update_user_profile` | user.lua | Yes | -| `search_users` | `user.rpc_search_users` | user.lua | Yes | -| `change_credentials` | `user.rpc_change_credentials` | user.lua | Yes | -| `send_lobby_invite` | `user.rpc_send_lobby_invite` | user.lua | Yes | -| `send_friend_request` | `user.rpc_send_friend_request` | user.lua | Yes | -| `admin_get_user_history` | `user.rpc_admin_get_user_history` | user.lua | Admin | -| `get_shop_catalog` | `economy.rpc_get_shop_catalog` | economy.lua | Yes | -| `buy_currency` | `economy.rpc_buy_currency` | economy.lua | Yes | -| `purchase_item` | `economy.rpc_purchase_item` | economy.lua | Yes | -| `admin_set_featured_banners` | `economy.rpc_admin_set_featured_banners` | economy.lua | Admin | -| `admin_get_featured_banners` | `economy.rpc_admin_get_featured_banners` | economy.lua | Admin | -| `perform_gacha_pull` | `gacha.rpc_perform_gacha_pull` | gacha.lua | Yes | -| `get_leaderboard_stats` | `leaderboard.rpc_get_leaderboard_stats` | leaderboard.lua | No | -| `submit_score` | `leaderboard.rpc_submit_score` | leaderboard.lua | Yes | -| `sync_leaderboard` | `leaderboard.rpc_sync_leaderboard` | leaderboard.lua | Yes | -| `reset_stats` | `leaderboard.rpc_reset_stats` | leaderboard.lua | Yes | -| `admin_update_stats` | `leaderboard.rpc_admin_update_stats` | leaderboard.lua | Admin | -| `admin_delete_stats` | `leaderboard.rpc_admin_delete_stats` | leaderboard.lua | Admin | -| `admin_sync_leaderboard` | `leaderboard.rpc_admin_sync_leaderboard` | leaderboard.lua | Admin | -| `admin_send_mail` | `inbox.rpc_admin_send_mail` | inbox.lua | Admin | -| `get_mail` | `inbox.rpc_get_mail` | inbox.lua | Yes | -| `claim_mail_reward` | `inbox.rpc_claim_mail_reward` | inbox.lua | Yes | -| `delete_mail` | `inbox.rpc_delete_mail` | inbox.lua | Yes | -| `save_mail_state` | `inbox.rpc_save_mail_state` | inbox.lua | Yes | -| `admin_list_mail` | `inbox.rpc_admin_list_mail` | inbox.lua | Admin | -| `admin_update_mail` | `inbox.rpc_admin_update_mail` | inbox.lua | Admin | -| `admin_delete_mail_server` | `inbox.rpc_admin_delete_mail_server` | inbox.lua | Admin | -| `claim_daily_reward` | `daily_rewards.rpc_claim_daily_reward` | daily_rewards.lua | Yes | -| `get_daily_reward_state` | `daily_rewards.rpc_get_daily_reward_state` | daily_rewards.lua | Yes | -| `set_daily_reward_config` | `daily_rewards.rpc_set_daily_reward_config` | daily_rewards.lua | Admin | -| `get_daily_reward_config_admin` | `daily_rewards.rpc_get_daily_reward_config_admin` | daily_rewards.lua | Admin | -| `admin_kick_player` | `admin.rpc_admin_kick_player` | admin.lua | Admin or Host | -| `admin_ban_player` | `admin.rpc_admin_ban_player` | admin.lua | Admin | -| `admin_unban_player` | `admin.rpc_admin_unban_player` | admin.lua | Admin | -| `admin_get_ban_list` | `admin.rpc_admin_get_ban_list` | admin.lua | Admin | -| `admin_get_server_stats` | `admin.rpc_admin_get_server_stats` | admin.lua | Admin (or Host w/ match_id) | -| `admin_get_player_list` | `admin.rpc_admin_get_player_list` | admin.lua | Admin or Host | -| `admin_end_match` | `admin.rpc_admin_end_match` | admin.lua | Admin or Host | -| `admin_set_user_role` | `admin.rpc_admin_set_user_role` | admin.lua | Owner only | -| `admin_topup_gold` | `admin.rpc_admin_topup_gold` | admin.lua | Admin | -| `admin_clear_global_chat` | `admin.rpc_admin_clear_global_chat` | admin.lua | Admin | -| `admin_list_users` | `admin.rpc_admin_list_users` | admin.lua | Admin | -| `admin_delete_users` | `admin.rpc_admin_delete_users` | admin.lua | Admin | -| `admin_get_user_detail` | `admin.rpc_admin_get_user_detail` | admin.lua | Admin | -| `admin_update_user_identity` | `admin.rpc_admin_update_user_identity` | admin.lua | Admin | -| `admin_set_user_password` | `admin.rpc_admin_set_user_password` | admin.lua | Admin | -| `admin_get_chat_config` | `admin.rpc_admin_get_chat_config` | admin.lua | Admin | -| `admin_set_chat_config` | `admin.rpc_admin_set_chat_config` | admin.lua | Admin | -| `admin_purge_old_messages` | `admin.rpc_admin_purge_old_messages` | admin.lua | Admin | -| `admin_list_channel_messages` | `admin.rpc_admin_list_channel_messages` | admin.lua | Admin | -| `admin_delete_channel_message` | `admin.rpc_admin_delete_channel_message` | admin.lua | Admin | - -[Back to top](#top) - -### 1.2 Registered After-Hooks - -[Back to top](#top) - -| Trigger | Lua Function | Module | Purpose | -|---|---|---|---| -| `AuthenticateSteam` | `after_authenticate_steam` | core.lua | Initialize Steam user profile | -| `AuthenticateDevice` | `user.after_authenticate` | user.lua | Record login history | -| `AuthenticateEmail` | `user.after_authenticate` | user.lua | Record login history | -| `AuthenticateCustom` | `user.after_authenticate` | user.lua | Record login history | +High-level architecture of the Nakama Lua backend. For detailed RPC reference (params, returns, errors), see [Nakama-Server-API](../Nakama-Server-API). [Back to top](#top) --- -[Back to top](#top) +## System Overview -## 2. Core / Auth Hooks (`core.lua`) +```mermaid +flowchart LR + Client[Godot Client
player.tscn] -->|RPC calls| Nakama[Nakama Server
port 7350/7351] + Nakama -->|wallet_update| Wallet[Wallet Engine] + Nakama -->|storage_read/write| Storage[Nakama Storage DB
PostgreSQL] + Nakama -->|leaderboard_*| LB[Native Leaderboard] + Nakama -->|match_*| Match[Match Handler] + Nakama -->|channel_*| Chat[Lobby Chat] + + Client -->|nk.match_join| Match + Client -->|Direct RPC| Lua[Lua RPC Handlers
server/nakama/lua/] +``` + +The Lua backend runs **inside** the Nakama process. Lua modules hook into Nakama's lifecycle (after-authentication, RPC dispatch, match signals). All game transactions (wallet, inventory, gacha, shop) are **server-authoritative** — the Godot client sends RPC requests and the Lua code validates and executes. [Back to top](#top) -**Dependencies:** None beyond standard `nk` module. +--- -**Collections read/written:** None (operates on user account metadata only). +## Module Architecture -**Wallet changesets:** None. +```mermaid +flowchart TD + main["main.lua
Entry point: loads all modules"] --> utils["utils.lua
Auth guards, channel resolver"] + main --> core["core.lua
Authentication hooks
Wallet init, ban check"] + main --> economy["economy.lua
Shop catalog, currency IAP
Item purchases"] + main --> gacha["gacha.lua
Gacha pulls, pity system
RNG on server"] + main --> leaderboard["leaderboard.lua
Score submission, sync
Global rankings"] + main --> inbox["inbox.lua
Global/personal mail
Reward claiming"] + main --> daily["daily_rewards.lua
Daily login rewards
Monthly schedule"] + main --> user["user.lua
Profile updates, identity
Friend sync"] + main --> admin["admin.lua
Kick/ban, stats, chat mgmt
Role management"] -### 2.1 `after_authenticate_steam` (After-Hook, registered on `AuthenticateSteam`) + core --> utils + economy --> utils + gacha --> utils + leaderboard --> utils + inbox --> utils + daily --> utils + user --> utils + admin --> utils + + economy -->|Wallet deduction| Wallet[(Wallet Engine)] + gacha -->|Wallet deduction| Wallet + inbox -->|Claim rewards| Wallet + daily -->|Daily claim| Wallet + user -->|Profile load| Wallet +``` + +### Dependency order (loading sequence) + +1. `utils.lua` — no deps (loaded first) +2. `economy.lua` — depends on utils +3. `core.lua` — depends on utils +4. `admin.lua` — depends on utils +5. `daily_rewards.lua` — depends on utils +6. `user.lua` — depends on utils +7. `leaderboard.lua` — depends on utils +8. `inbox.lua` — depends on utils +9. `gacha.lua` — no deps beyond utils [Back to top](#top) -Triggered after a successful Steam authentication. +--- -**Purpose:** Initialize first-time Steam users with a display name and default player role. +## Authentication Flow -**Parameters (from Nakama hook):** -| Param | Type | Description | +```mermaid +sequenceDiagram + participant C as Godot Client + participant N as Nakama Core + participant A as after_hooks (core.lua) + participant DB as Nakama Storage + + C->>N: AuthenticateDevice/Custom/Email + N->>A: after_authenticate(context) + A->>DB: storage_read(profiles/profile) + alt First login (no profile) + A->>DB: storage_write(initial profile) + A->>N: wallet_update(gold=100, star=500) + end + A->>DB: storage_read(profiles/profile) + alt metadata.banned == true + A-->>C: error("Account banned") + end + A->>C: session token returned +``` + +### Boot sequence per player + +1. Godot client calls `AuthenticateDevice` (or Email/Custom/Steam). +2. Nakama core calls `after_authenticate()` hook in `core.lua`. +3. Hook reads `profiles/profile` from storage. +4. **If first login:** initializes default profile and grants starting wallet (`gold: 100, star: 500`). +5. **Ban check:** If `metadata.banned == true`, raises error (player rejected). +6. Client receives session token and proceeds to lobby. + +[Back to top](#top) + +--- + +## Wallet & Economy Flow + +```mermaid +flowchart LR + subgraph Client Side + ShopUI[Shop Panel] -->|buy_currency RPC| Server + ShopUI -->|purchase_item RPC| Server + GachaUI[Gacha Panel] -->|perform_gacha_pull RPC| Server + MailUI[Inbox] -->|claim_mail_reward RPC| Server + DailyUI[Daily Rewards] -->|claim_daily_reward RPC| Server + end + + subgraph Server Side + Server[Lua RPC Handler] + Server -->|nk.wallet_update| Wallet[(Wallet)] + Server -->|nk.storage_write| Inv[(Inventory Storage)] + Server -->|nk.storage_write| Rec[(Receipts Storage)] + Server -->|nk.storage_write| Frag[(Fragments Storage)] + end + + subgraph Client Refresh + Wallet -->|Wallet updated| Client + Client -->|get_account RPC| Wallet + Client -->|emit profile_updated| UI[All UI Panels
update labels] + end +``` + +### Currency types + +| Currency | Purpose | Earned by | |---|---|---| -| `context` | table | Nakama RPC context (`context.user_id` must exist) | -| `output` | table | Authentication output (unused) | -| `input` | table | Authentication input (`input.username` = Steam username) | +| `gold` | Shop purchases, star conversion | IAP (real money), admin topup | +| `star` | Gacha pulls | Gold conversion, daily rewards, mail rewards | -**Auth:** None (internal hook, runs server-side after auth). +### All wallet changesets -**Behavior:** -1. Skip if `context.user_id` is nil. -2. Fetch account via `nk.account_get_id(context.user_id)`. -3. If `account.user.display_name` is empty/nil, set it from `input.username` (fallback: `"SteamPlayer"`) via `nk.account_update_id`. -4. If user metadata has no `role`, set `metadata.role = "player"` and persist via `nk.account_update_id`. - -**Errors:** None surfaced (wrapped in `pcall`). - -**Collections:** User account metadata (via `nk.account_get_id` / `nk.account_update_id`). - -**Wallet changes:** None. - -**Dependencies:** None. - -[Back to top](#top) - -### 2.2 Startup Leaderboard Creation - -[Back to top](#top) - -``` -nk.leaderboard_create("global_high_score", true, "desc", "best", nil, {}) -``` - -| Field | Value | +| Operation | Changeset | |---|---| -| **ID** | `"global_high_score"` | -| **Authoritative** | `true` | -| **Sort** | `"desc"` (highest score first) | -| **Operator** | `"best"` (keep best score) | -| **Reset schedule** | `nil` (never resets) | - -Also created in leaderboard.lua; second call is a no-op via `pcall`. +| First login grant | `{gold: 100, star: 500}` | +| Buy gold IAP | `{gold: +N}` (N=100/550/1150/2400/6250/13000) | +| Buy stars (gold convert) | `{gold: -N, star: +M}` | +| Buy shop item | `{gold: -price}` or `{star: -price}` | +| Gacha pull | `{star: -cost}` or `{gold: -cost}` | +| Claim mail reward | `{gold: +N}` and/or `{star: +N}` | +| Claim daily reward | `{star: +N}` or `{gold: +N}` | +| Admin topup | `{gold: 999999}` | [Back to top](#top) --- -[Back to top](#top) +## Gacha Flow -## 3. Utilities (`utils.lua`) +```mermaid +flowchart TD + A[Client: perform_gacha_pull] --> B{Check banner} + B -->|star/gold| C[Read wallet balance] + C --> D{Sufficient funds?} + D -->|No| E[error: Insufficient currency] + D -->|Yes| F{Check pity} + F -->|pity >= 90| G[Force real_prize rarity] + F -->|pity < 90| H[Roll rarity by drop rates] + + G --> I[Pick from real_prize pool] + H --> J[Pick from rarity pool] + + I --> K[Deduct wallet cost] + J --> K + + K --> L{real_prize?} + L -->|Yes| M[Add to inventory storage] + L -->|No| N[Increment fragment count] + + M --> O[Return results] + N --> O +``` -[Back to top](#top) +### Drop rates -**Dependencies:** None. - -**Collections read/written:** Reads user metadata (via `nk.account_get_id` in `is_admin`). Reads match state (via `nk.match_get` in `is_match_host`). - -### 3.1 Constants - -[Back to top](#top) - -| Constant | Value | Purpose | +| Rarity | Rate | Result | |---|---|---| -| `utils.ADMIN_ROLES` | `{["admin"]=true, ["moderator"]=true, ["owner"]=true}` | Roles with elevated privileges | -| `utils.SYSTEM_USER_ID` | `"00000000-0000-0000-0000-000000000000"` | System/nil user for global storage objects | -| `utils.CHANNEL_TYPE_ROOM` | `1` | Nakama channel type for chat rooms | -| `utils.CHANNEL_TYPE_DIRECT` | `2` | Nakama channel type for direct messages | -| `utils.CHANNEL_TYPE_GROUP` | `3` | Nakama channel type for group chats | +| Common | 60% | Fragment (`frag_common`) | +| Uncommon | 25% | Fragment (`frag_uncommon`) | +| Rare | 14% | Fragment (`frag_rare`) | +| Real Prize | 1% | Skin from catalog | -[Back to top](#top) - -### 3.2 `utils.is_admin(context)` - -[Back to top](#top) - -Check if caller has admin/moderator/owner role. - -**Parameters:** -| Param | Type | Required | Description | -|---|---|---|---| -| `context` | table | Yes | Nakama RPC context (must have `user_id`) | - -**Returns:** `boolean` - -**Logic:** -1. Look up `context.user_id` via `nk.account_get_id`. -2. Decode metadata JSON (handles both string and table formats). -3. Check if `metadata.role` exists in `ADMIN_ROLES`. - -**Error strings:** None (returns `false` on failure). - -**Collections:** User account metadata (read via `nk.account_get_id`). - -**Dependencies:** None (standalone). - -[Back to top](#top) - -### 3.3 `utils.is_match_host(context, match_id)` - -[Back to top](#top) - -Check if caller is the host of an authoritative match. - -**Parameters:** -| Param | Type | Required | Default | Description | -|---|---|---|---|---| -| `context` | table | Yes | -- | Nakama RPC context | -| `match_id` | string | Yes | -- | Nakama match ID to check | - -**Returns:** `boolean` - -**Logic:** -1. Fetch match via `nk.match_get(match_id)`. -2. Decode `match.state` JSON. -3. Return `true` if `state.hostUserId == context.user_id`. - -**Error strings:** None (returns `false` on failure). - -**Collections:** Match state (via `nk.match_get`). - -**Dependencies:** None. - -[Back to top](#top) - -### 3.4 `utils.require_admin(context)` - -[Back to top](#top) - -Guard function that errors if caller is not an admin/moderator/owner. - -**Parameters:** -| Param | Type | Required | Description | -|---|---|---|---| -| `context` | table | Yes | Nakama RPC context | - -**Error strings:** `"Admin privileges required"` - -**Dependencies:** `utils.is_admin(context)` - -[Back to top](#top) - -### 3.5 `utils.require_admin_or_host(context, match_id)` - -[Back to top](#top) - -Guard function that errors if caller is neither admin nor match host. - -**Parameters:** -| Param | Type | Required | Description | -|---|---|---|---| -| `context` | table | Yes | Nakama RPC context | -| `match_id` | string | Yes | Nakama match ID | - -**Error strings:** `"Admin or host privileges required"` - -**Dependencies:** `utils.is_admin(context)`, `utils.is_match_host(context, match_id)` - -[Back to top](#top) - -### 3.6 `utils.resolve_channel_id(channel_id)` - -[Back to top](#top) - -Resolve a human-readable channel name to a hashed Nakama channel ID for chat RPCs. - -**Parameters:** -| Param | Type | Required | Default | Description | -|---|---|---|---|---| -| `channel_id` | string | Yes | -- | Raw channel name or already-hashed ID | - -**Returns:** `string` -- resolved channel ID - -**Logic:** -1. If empty, return `""`. -2. If contains `"."` (hashed format), return as-is. -3. Otherwise, build via `nk.channel_id_build("", channel_id, utils.CHANNEL_TYPE_ROOM)`. -4. If build fails, return original value. - -**Error strings:** None. - -**Dependencies:** None (pure utility). +**Pity:** Guaranteed Real Prize at 90 pulls. Pity counter resets on any Real Prize win. [Back to top](#top) --- +## Mail/Inbox System + +```mermaid +flowchart TD + Admin[Admin RPC] -->|admin_send_mail| Global[Global Mail
config/global_mail
system user] + Admin -->|admin_send_mail
with target_user_id| Personal[Personal Mail
inbox/personal
per user] + + Client -->|get_mail| Merge[Merge global + personal] + Merge --> Filter[Filter by: not deleted,
within date range,
not expired] + Filter --> Response[Return to client] + + Client -->|claim_mail_reward| CW{ [Back to top](#top) +--- -## 4. User Module (`user.lua`) +## Mail/Inbox System -[Back to top](#top) +```mermaid +flowchart TD + Admin[Admin RPC] -->|admin_send_mail| Global[Global Mail
config/global_mail
system user] + Admin -->|admin_send_mail
with target_user_id| Personal[Personal Mail
inbox/personal
per user] -**Dependencies:** `utils` (for `admin_get_user_history`). + Client -->|get_mail| Merge[Merge global + personal] + Merge --> Filter[Filter by: not deleted,
within date range,
not expired] + Filter --> Response[Return to client] -**Wallet changesets:** None (all user RPCs are informational or identity operations). - -### 4.1 RPC: `get_user_profile` - -[Back to top](#top) - -**Function:** `user.rpc_get_user_profile(context, payload)` - -Get user profile information, with ban check for the requesting user. - -**Parameters:** -| Param | Type | Required | Default | Description | -|---|---|---|---|---| -| `payload.user_id` | string | No | caller's `context.user_id` | Target user ID to fetch | - -**Auth:** None (works without auth; if unauthenticated, uses provided `user_id`). - -**Return:** -```json -{ - "user_id": "string", - "username": "string", - "display_name": "string", - "avatar_url": "string", - "create_time": "integer (Unix timestamp)", - "role": "string" -} + Client -->|claim_mail_reward| CW[Check claimed_ids] + CW -->|Already claimed| Err[error: Reward already claimed] + CW -->|Not claimed| Grant[Grant rewards:
gold/star -> wallet
fragments -> fragment storage
skins -> inventory storage] + Grant --> UpdateState[Update state:
add mailId to claimed_ids] ``` -**Error strings:** -- `"Account not found"` -- target user does not exist -- `"Account banned until . Reason: "` -- user is banned (absolute or expiry) -- `"Account permanently banned. Reason: "` -- no expiry set - -**Collections read:** User account metadata via `nk.account_get_id()`. - -**Collections written:** User account metadata (if clearing expired bans). - -**Logic:** -1. If caller is banned (self-check), error with ban details. -2. If ban expired (Unix timestamp in `metadata.ban_expires` <= `os.time()`), clear ban flags and return profile. -3. Return profile fields. - -**Dependencies:** None. - -[Back to top](#top) - -### 4.2 RPC: `update_user_profile` - -[Back to top](#top) - -**Function:** `user.rpc_update_user_profile(context, payload)` - -Update the caller's display name and/or avatar URL. - -**Parameters:** -| Param | Type | Required | Default | Description | -|---|---|---|---|---| -| `payload.display_name` | string | No | unchanged | New display name | -| `payload.avatar_url` | string | No | unchanged | New avatar URL | - -**Auth:** Required (`context.user_id` must exist). - -**Return:** -```json -{ - "success": true -} -``` - -**Error strings:** -- `"Not authenticated"` -- no `context.user_id` -- `"Failed to update profile"` -- `nk.account_update_id` failed - -**Collections:** User account (via `nk.account_update_id`). - -**Dependencies:** None. - -[Back to top](#top) - -### 4.3 RPC: `search_users` - -[Back to top](#top) - -**Function:** `user.rpc_search_users(context, payload)` - -Search users by username or display name. Returns up to 100 results. - -**Parameters:** -| Param | Type | Required | Default | Description | -|---|---|---|---|---| -| `payload.query` | string | No | `""` | Search query (ILIKE match on username and display_name) | - -**Auth:** Required. - -**Return:** -```json -{ - "users": [ - { - "user_id": "string", - "username": "string", - "display_name": "string", - "avatar_url": "string" - } - ] -} -``` - -**Error strings:** `"Not authenticated"` - -**Collections read:** SQL `users` table via `nk.sql_query`. - -**SQL queries:** -- Empty query: `SELECT id, username, display_name, metadata FROM users WHERE id != '00000000-0000-0000-0000-000000000000' ORDER BY create_time DESC LIMIT 100` -- With query: Same but `WHERE (username ILIKE $1 OR display_name ILIKE $1)` with param `"%%"` - -**Dependencies:** None. - -[Back to top](#top) - -### 4.4 RPC: `change_credentials` - -[Back to top](#top) - -**Function:** `user.rpc_change_credentials(context, payload)` - -Change the caller's email and password credentials. - -**Parameters:** -| Param | Type | Required | Default | Description | -|---|---|---|---|---| -| `payload.current_password` | string | Cond. | -- | Required if user has existing email | -| `payload.new_email` | string | Yes | -- | New email address | -| `payload.new_password` | string | Yes | -- | New password | - -**Auth:** Required. - -**Return:** -```json -{ - "success": true -} -``` - -**Error strings:** -- `"Not authenticated"` -- `"Current password required"` -- has existing email but no current_password provided -- `"Incorrect current password."` -- `nk.authenticate_email` failed -- `"Failed to set new credentials: "` -- `nk.link_email` failed - -**Collections:** User authentication links (via `nk.unlink_email`, `nk.link_email`, `nk.authenticate_email`). - -**Note:** If setting new credentials fails after unlinking old email, it tries to re-link the old email as rollback. - -**Dependencies:** None. - -[Back to top](#top) - -### 4.5 RPC: `send_lobby_invite` - -[Back to top](#top) - -**Function:** `user.rpc_send_lobby_invite(context, payload)` - -Send a lobby invite notification to another user. - -**Parameters:** -| Param | Type | Required | Description | -|---|---|---|---| -| `payload.to_user_id` | string | Yes | Target user to invite | -| `payload.match_id` | string | Yes | Match/lobby ID to join | - -**Auth:** Required. - -**Return:** -```json -{ - "success": true -} -``` - -**Error strings:** -- `"Not authenticated"` -- `"Missing to_user_id or match_id"` - -**Notification sent:** -| Field | Value | -|---|---| -| **Recipient** | `req.to_user_id` | -| **Subject** | `" invited you to their lobby"` | -| **Content** | `{"match_id": req.match_id, "from_name": senderName}` | -| **Code** | `1001` | -| **Sender** | `context.user_id` | -| **Persistent** | `true` | - -**Dependencies:** None. - -[Back to top](#top) - -### 4.6 RPC: `send_friend_request` - -[Back to top](#top) - -**Function:** `user.rpc_send_friend_request(context, payload)` - -Send a friend request notification to another user. - -**Parameters:** -| Param | Type | Required | Description | -|---|---|---|---| -| `payload.user_id` | string | Yes | Target user to friend | - -**Auth:** Required. - -**Return:** -```json -{ - "success": true -} -``` - -**Error strings:** -- `"Not authenticated"` -- `"user_id is required"` -- `"Cannot add yourself"` - -**Notification sent:** -| Field | Value | -|---|---| -| **Recipient** | `req.user_id` | -| **Subject** | `"Friend Request"` | -| **Content** | `{"from_user_id": context.user_id, "from_name": senderName}` | -| **Code** | `1002` | -| **Sender** | `context.user_id` | -| **Persistent** | `true` | - -**Dependencies:** None. - -[Back to top](#top) - -### 4.7 After-Hook: `after_authenticate` - -[Back to top](#top) - -**Function:** `user.after_authenticate(context, out, payload)` - -Record login history after Device, Email, or Custom authentication. - -**Trigger:** `AuthenticateDevice`, `AuthenticateEmail`, `AuthenticateCustom`. - -**Parameters:** Standard Nakama after-hook (context, output, input). - -**Auth:** None (internal hook). - -**Behavior:** -1. Skip if no `context.user_id`. -2. Create login entry `{time=os.time(), ip=context.client_ip or "unknown"}`. -3. Read existing logins from storage collection `"history"`, key `"logins"`, user's scope. -4. Prepend new entry, keep last 20 entries. -5. Write back to storage. - -**Collections:** -- **Read:** `"history"` / `"logins"` / user scope -- **Write:** `"history"` / `"logins"` / user scope (permission_read=0, permission_write=0) - -**Wallet changes:** None. - -**Dependencies:** None. - -[Back to top](#top) - -### 4.8 RPC: `admin_get_user_history` - -[Back to top](#top) - -**Function:** `user.rpc_admin_get_user_history(context, payload)` - -Admin-only: get wallet ledger, login history, and match history for a user. - -**Parameters:** -| Param | Type | Required | Description | -|---|---|---|---| -| `payload.user_id` | string | Yes | Target user ID | - -**Auth:** Admin (calls `utils.require_admin(context)`). - -**Return:** -```json -{ - "history": { - "wallet_ledger": [ ... ], - "logins": [ ... ], - "matches": [ ... ] - } -} -``` - -**Error strings:** -- `"Admin privileges required"` (from `utils.require_admin`) -- `"user_id is required"` - -**Collections read:** -- Wallet ledger via `nk.wallet_ledger_list(targetUserId, 50)` -- `"history"` / `"logins"` / user scope -- `"matches"` collection list via `nk.storage_list(targetUserId, "matches", 50, "")` - -**Dependencies:** `utils.require_admin`. - [Back to top](#top) --- -[Back to top](#top) +## Client-Server Data Flow -## 5. Economy Module (`economy.lua`) +```mermaid +sequenceDiagram + participant C as Godot Client + participant L as Lua RPC + participant S as Nakama Storage + participant W as Wallet -[Back to top](#top) + Note over C,W: Purchase Flow + C->>L: purchase_item(item_id, idempotency_key) + L->>L: Look up item in SHOP_CATALOG_DEFS + L->>W: wallet_update(-price) + alt Insufficient funds + W-->>L: error + L-->>C: error("NotEnoughFunds") + else Success + L->>S: storage_write(inventory/item_id) + L->>S: storage_write(receipts/idempotency_key) + L-->>C: {success: true, item: item_id} + end -**Dependencies:** `utils` (for admin RPCs: `require_admin`). - -**Wallet keys used:** `gold`, `star`. - -### 5.1 Shop Catalog (embedded data) - -[Back to top](#top) - -`SHOP_CATALOG_DEFS` -- 12 cosmetic items across 3 categories: - -**Head items (Oldpop hats):** -| Item ID | Name | Category | Gold Price | Rarity | -|---|---|---|---|---| -| `oldpop-blue-hat` | Oldpop Blue Hat | head | 100 | Common | -| `oldpop-green-hat` | Oldpop Green Hat | head | 100 | Common | -| `oldpop-red-hat` | Oldpop Red Hat | head | 100 | Common | -| `oldpop-yellow-hat` | Oldpop Yellow Hat | head | 100 | Common | - -**Costume items (pants):** -| Item ID | Name | Category | Gold Price | Rarity | -|---|---|---|---|---| -| `oldpop-og-pant` | Copper OG Pant | costume | 0 (free) | Common | -| `oldpop-grey-pant` | Copper Grey Pant | costume | 150 | Common | -| `oldpop-red-pant` | Copper Red Pant | costume | 150 | Common | -| `oldpop-yellow-pant` | Copper Yellow Pant | costume | 150 | Common | - -**Glove items:** -| Item ID | Name | Category | Gold Price | Rarity | -|---|---|---|---|---| -| `oldpop-blue-gloves` | Oldpop Blue Gloves | glove | 75 | Common | -| `oldpop-green-gloves` | Oldpop Green Gloves | glove | 75 | Common | -| `oldpop-red-gloves` | Oldpop Red Gloves | glove | 75 | Common | -| `oldpop-yellow-gloves` | Oldpop Yellow Gloves | glove | 75 | Common | - -[Back to top](#top) - -### 5.2 RPC: `get_shop_catalog` - -[Back to top](#top) - -**Function:** `economy.rpc_get_shop_catalog(context, payload)` - -Get the shop catalog with featured banners. - -**Parameters:** None (payload is ignored). - -**Auth:** Required. - -**Return:** -```json -{ - "catalog": { - "head": [ - { "id": "string", "name": "string", "gold": 100, "star": 0, "rarity": "Common", "character": "Oldpop" } - ], - "costume": [ ... ], - "glove": [ ... ] - }, - "featured_banners": [ ... ] -} + Note over C,W: Wallet State Sync + C->>L: get_account (Nakama SDK call) + S->>C: wallet JSON string + C->>C: Parse wallet JSON + C->>C: emit profile_updated signal + C->>C: All UI panels update labels ``` -**Collections read:** `"shop_config"` / `"featured_banners"` / system user scope. - -**Error strings:** `"Not authenticated"` - -**Dependencies:** None (no utils call). +All transactions are **idempotent** via `idempotency_key` — if the same key is used twice, the server returns the previous result instead of re-executing. [Back to top](#top) -### 5.3 RPC: `buy_currency` +--- -[Back to top](#top) +## Admin Hierarchy -**Function:** `economy.rpc_buy_currency(context, payload)` - -Purchase currency packs via IAP. Supports Google, Apple, and test receipts. - -**Parameters:** -| Param | Type | Required | Default | Description | -|---|---|---|---|---| -| `payload.package_id` | string | Yes | -- | One of: `gold_100`, `gold_500`, `gold_1000`, `gold_2000`, `gold_5000`, `gold_10000`, `star_100`, `star_250`, `star_600` | -| `payload.receipt` | string | No | -- | Platform receipt string (required for gold packages) | -| `payload.idempotency_key` | string | Yes | -- | Unique key to prevent duplicate processing | -| `payload.store_type` | string | No | `"test"` | `"google"`, `"apple"`, or `"test"` | - -**Auth:** Required. - -**Package definitions:** -| Package ID | Gold Gain | Star Gain | Requires Verification | -|---|---|---|---| -| `gold_100` | +100 | 0 | Yes | -| `gold_500` | +550 | 0 | Yes | -| `gold_1000` | +1150 | 0 | Yes | -| `gold_2000` | +2400 | 0 | Yes | -| `gold_5000` | +6250 | 0 | Yes | -| `gold_10000` | +13000 | 0 | Yes | -| `star_100` | -500 | +100 | No | -| `star_250` | -1100 | +250 | No | -| `star_600` | -2500 | +600 | No | - -**Return (pending verification):** -```json -{ - "success": true, - "status": "pending", - "package_id": "string" -} -``` - -**Return (verified/complete):** -```json -{ - "success": true, - "status": "verified", - "package_id": "string" -} -``` - -**Return (duplicate idempotency key):** -```json -{ - "success": true, - "package_id": "string", - "duplicate": true, - "status": "string" -} -``` - -**Error strings:** -- `"Not authenticated"` -- `"Package ID required"` -- `"Idempotency key required"` -- `"Invalid package ID"` -- unknown package_id -- `"InvalidReceipt"` -- receipt validation failed -- `"NotEnoughFunds"` -- wallet update failure (star purchases deduct gold) - -**Collections:** -- **Read:** `"receipts"` / `` / user scope (duplicate check) -- **Write (pending):** `"receipts"` / `` / user scope `{type, package_id, status="pending", created_at}` -- **Write (verified):** `"receipts"` / `` / user scope `{type, package_id, changeset, receipt, status="verified", processed_at}` - -**Wallet changesets:** -- Gold packages: `{gold: +N}` (verified receipt) -- Star packages using gold conversion: `{star: +N, gold: -price}` - -**Receipt verification flow:** -1. `store_type="google"`: calls `nk.purchase_validate_google(context.user_id, receipt)` -2. `store_type="apple"`: calls `nk.purchase_validate_apple(context.user_id, receipt, "")` -3. `store_type="test"`: passes if `receipt == "mock_receipt_for_now"` - -**Dependencies:** None (standalone). - -[Back to top](#top) - -### 5.4 RPC: `purchase_item` - -[Back to top](#top) - -**Function:** `economy.rpc_purchase_item(context, payload)` - -Buy cosmetic items from the shop catalog using wallet currency. - -**Parameters:** -| Param | Type | Required | Default | Description | -|---|---|---|---|---| -| `payload.item_id` | string | Yes | -- | Item ID from shop catalog | -| `payload.quantity` | integer | No | `1` | Quantity to purchase | -| `payload.idempotency_key` | string | Yes | -- | Unique key to prevent duplicate purchases | - -**Auth:** Required. - -**Return:** -```json -{ - "success": true, - "item": "string" -} -``` - -**Return (duplicate):** -```json -{ - "success": true, - "item": "string", - "duplicate": true -} -``` - -**Error strings:** -- `"Not authenticated"` -- `"Item ID required"` -- `"Invalid quantity"` -- `"Idempotency key required"` -- `"ItemNotFound"` -- item_id not in catalog -- `"NotEnoughFunds"` -- wallet insufficient -- `"PurchaseFailed"` -- storage write failure - -**Collections:** -- **Read:** `"receipts"` / `` / user scope -- **Write:** `"inventory"` / `` / user scope `{category, purchased_at, quantity}` -- **Write:** `"receipts"` / `` / user scope `{type="item", item_id, quantity, cost, processed_at}` - -**Wallet changesets:** -- Gold items: `{gold: -priceGold}` (if priceGold > 0) -- Star items: `{star: -priceStar}` (if priceStar > 0) - -**Dependencies:** None. - -[Back to top](#top) - -### 5.5 RPC: `admin_set_featured_banners` - -[Back to top](#top) - -**Function:** `economy.rpc_admin_set_featured_banners(context, payload)` - -Admin-only: set up to 3 featured banners for the shop. Validates item IDs against catalog. - -**Parameters:** -| Param | Type | Required | Default | Description | -|---|---|---|---|---| -| `payload.banners` | array | No | `[]` | Array of banner objects (max 3) | - -**Each banner object:** -| Field | Type | Description | +| Role | Can | Guarded by | |---|---|---| -| `item_id` | string | Valid catalog item ID | -| Any other display fields | any | Passed through | +| `player` (default) | Nothing special | — | +| `moderator` | Match-related admin: kick players, get server stats | `utils.require_admin_or_host` (also checks match host) | +| `admin` | All moderation: ban/unban, manage mail, manage chat, view users | `utils.require_admin(context)` | +| `owner` | Everything admin can + set user roles | Inline check: `callerMetadata.role == "owner"` | -**Auth:** Admin. +### Guard functions (utils.lua) -**Return:** -```json -{ - "success": true, - "banners": [ ... ] -} +```lua +utils.require_admin(context) -- errors if role not admin or owner +utils.require_admin_or_host(context, match_id) -- errors if not admin/owner AND not match host +utils.is_banned(metadata) -- returns boolean +utils.resolve_channel_id(channelId) -- channel name → hashed ID ``` -**Error strings:** -- `"Admin privileges required"` -- `"Item not found in catalog: "` - -**Collections written:** `"shop_config"` / `"featured_banners"` / system user scope. - -**Dependencies:** `utils.require_admin`. - -[Back to top](#top) - -### 5.6 RPC: `admin_get_featured_banners` - -[Back to top](#top) - -**Function:** `economy.rpc_admin_get_featured_banners(context, payload)` - -Admin-only: get current featured banners. - -**Parameters:** None. - -**Auth:** Admin. - -**Return:** -```json -{ - "banners": [ ... ] -} -``` - -**Error strings:** `"Admin privileges required"` - -**Collections read:** `"shop_config"` / `"featured_banners"` / system user scope. - -**Dependencies:** `utils.require_admin`. - [Back to top](#top) --- -[Back to top](#top) +## Storage Collections -## 6. Gacha Module (`gacha.lua`) - -[Back to top](#top) - -**Dependencies:** None (standalone, no utils require). - -**Wallet keys used:** Varies by banner (`"star"` or `"gold"`). - -### 6.1 Embedded Gacha Data - -[Back to top](#top) - -**Banners:** - -| Banner ID | Name | Currency | Pull 1 Cost | Pull 10 Cost | Pity At | +| Collection | Owner | Key | Public R | Public W | Purpose | |---|---|---|---|---|---| -| `star` | Star Banner | star | 160 | 1440 | 90 | -| `gold` | Gold Banner | gold | 50 | 450 | 90 | - -**Drop rates (both banners):** -| Rarity | Probability | -|---|---| -| `common` | 60% | -| `uncommon` | 25% | -| `rare` | 14% | -| `real_prize` | 1% | - -**Fragment prizes:** -| ID | Name | Rarity | -|---|---|---| -| `frag_common` | Common Fragment | common | -| `frag_uncommon` | Uncommon Fragment | uncommon | -| `frag_rare` | Rare Fragment | rare | - -**Real prize pool (skins):** -| ID | Name | Category | -|---|---|---| -| `skin_gacha_rainbow_suit` | Rainbow Suit | costume | -| `skin_gacha_dragon_hat` | Dragon Hat | head | -| `skin_gacha_phantom_gloves` | Phantom Gloves | glove | -| `skin_gacha_neon_acc` | Neon Accessory | accessory | - -[Back to top](#top) - -### 6.2 Internal: `roll_rarity(rates)` - -[Back to top](#top) - -Roll RNG to determine rarity tier. - -**Parameters:** -| Param | Type | Description | -|---|---|---| -| `rates` | table | Rate table with keys: `real_prize`, `rare`, `uncommon`, `common` | - -**Returns:** `string` -- one of `"real_prize"`, `"rare"`, `"uncommon"`, `"common"`. - -**Logic:** Uses `math.random()` + cumulative probability. - -[Back to top](#top) - -### 6.3 Internal: `pick_from_pool(rarity)` - -[Back to top](#top) - -Pick a random item from the pool for a given rarity. - -**Parameters:** -| Param | Type | Description | -|---|---|---| -| `rarity` | string | Rarity tier key | - -**Returns:** `string` -- item/fragment ID. - -**Logic:** Random index into `GACHA_DATA.pools[rarity]`. - -[Back to top](#top) - -### 6.4 RPC: `perform_gacha_pull` - -[Back to top](#top) - -**Function:** `gacha.rpc_perform_gacha_pull(context, payload)` - -Perform gacha pulls on a banner. Uses server-authoritative RNG with pity system. - -**Parameters:** -| Param | Type | Required | Default | Description | -|---|---|---|---|---| -| `payload.banner_id` | string | Yes | -- | `"star"` or `"gold"` | -| `payload.count` | integer | No | `1` | Number of pulls (1 or 10 recommended) | - -**Auth:** Required. - -**Return:** -```json -{ - "success": true, - "results": [ - { - "id": "string", - "rarity": "string", - "name": "string" - } - ], - "new_pity": "integer" -} -``` - -**Error strings:** -- `"Not authenticated"` -- `"Banner ID required"` -- `"Invalid count"` -- `"Unknown banner: "` -- `"Could not read account"` -- `"Insufficient currency"` -- `"Failed to update wallet"` -- `"Failed to write storage: "` - -**Collections:** -- **Read:** User wallet via `nk.account_get_id(context.user_id)` -- **Read:** `"profiles"` / `"pity_counters"` / user scope -- **Read:** `"profiles"` / `"fragments"` / user scope -- **Write:** `"profiles"` / `"pity_counters"` / user scope -- **Write:** `"profiles"` / `"fragments"` / user scope -- **Write (on real prize):** `"inventory"` / `` / user scope `{category, purchased_at, quantity: 1}` - -**Wallet changesets:** -- Star banner: `{star: -cost}` -- Gold banner: `{gold: -cost}` - -**Pity system:** -- At 90 pulls without a `real_prize`, the next pull is forced `real_prize`. -- Pity counter resets to 0 on any `real_prize` drop. -- Each banner has its own independent pity counter. - -**Dependencies:** None. +| `profiles` | User | `"profile"` | 1 | 0 | User metadata, role, ban status, loadout | +| `profiles` | User | `"pity_counters"` | 1 | 0 | Per-banner gacha pity counts | +| `profiles` | User | `"fragments"` | 1 | 0 | Accumulated gacha fragments | +| `inventory` | User | Item ID | 1 | 0 | Owned cosmetic items | +| `inventory` | User | `"fragments"` | 1 | 0 | Fragment counts (legacy) | +| `stats` | User | `"game_stats"` | 1 | 0 | Player stats (wins, kills, score) | +| `receipts` | User | Idempotency key | 1 | 0 | Purchase receipts (IAP + shop) | +| `inbox` | User | `"personal"` | 1 | 0 | Personalized mail inbox | +| `inbox` | User | `"state"` | 1 | 0 | claimed_ids, deleted_ids, read_ids | +| `daily_rewards` | User | `"state"` | 1 | 0 | Daily reward claim tracking | +| `config` | SYSTEM | `"global_mail"` | 2 | 0 | Global mail sent to all players | +| `config` | SYSTEM | `"daily_rewards"` | 2 | 0 | Monthly reward schedule | +| `config` | SYSTEM | `"lobby_chat"` | 2 | 0 | Chat prefix/max_messages config | +| `shop_config` | SYSTEM | `"featured_banners"` | 2 | 0 | Featured shop banners (max 3) | +| `bans` | SYSTEM | User ID | 2 | 0 | Ban records (redundant with metadata) | [Back to top](#top) --- -[Back to top](#top) +## vs Nakama-Server-API -## 7. Leaderboard Module (`leaderboard.lua`) - -[Back to top](#top) - -**Dependencies:** `utils` (for admin RPCs: `require_admin`). - -**Leaderboard:** `"global_high_score"` (authoritative, desc, best score, no reset). - -### 7.1 RPC: `get_leaderboard_stats` - -[Back to top](#top) - -**Function:** `leaderboard.rpc_get_leaderboard_stats(context, payload)` - -Get top 50 leaderboard entries. - -**Parameters:** None (payload ignored). - -**Auth:** None (public). - -**Return:** -```json -{ - "leaderboard": [ - { - "user_id": "string", - "username": "string", - "display_name": "string", - "avatar_url": "string", - "loadout_character": "string", - "high_score": "integer", - "games_played": "integer", - "games_won": "integer" - } - ] -} -``` - -**Error strings:** None (returns `{leaderboard: {}}` on failure). - -**Collections read:** `nk.leaderboard_records_list("global_high_score", nil, 50, nil)`. - -**Dependencies:** None. - -[Back to top](#top) - -### 7.2 RPC: `submit_score` - -[Back to top](#top) - -**Function:** `leaderboard.rpc_submit_score(context, payload)` - -Submit a score to the global leaderboard. - -**Parameters:** -| Param | Type | Required | Default | Description | -|---|---|---|---|---| -| `payload.score` | number | No | `0` | Score value | -| `payload.games_played` | integer | No | `0` | Total games played | -| `payload.games_won` | integer | No | `0` | Total games won | -| `payload.avatar_url` | string | No | account avatar | Avatar URL | -| `payload.loadout_character` | string | No | `"Copper"` | Current character | - -**Auth:** Required. - -**Return:** -```json -{ - "success": true -} -``` - -**Error strings:** -- `"Not authenticated"` -- `"Failed to submit score"` -- `nk.leaderboard_record_write` failed - -**Leaderboard write:** `nk.leaderboard_record_write("global_high_score", userId, username, score, 0, metadata)`. - -**Dependencies:** None. - -[Back to top](#top) - -### 7.3 RPC: `sync_leaderboard` - -[Back to top](#top) - -**Function:** `leaderboard.rpc_sync_leaderboard(context, payload)` - -Sync all users' stats from storage to the native leaderboard. Iterates storage and rewrites records. - -**Parameters:** None (payload ignored). - -**Auth:** Required. - -**Return:** -```json -{ - "success": true, - "synced": "integer (count)", - "objects_found": "integer", - "debug": [ "error messages" ] -} -``` - -**Error strings:** -- `"Not authenticated"` -- `"Sync failed: "` -- `nk.storage_list` failed - -**Collections read:** -- `"stats"` collection (all users, up to 100 objects) -- `"profiles"` collection (all users, up to 100 objects) - -**Leaderboard writes:** For each user, writes `nk.leaderboard_record_write("global_high_score", uid, username, high_score, 0, metadata)`. - -**Dependencies:** None. - -[Back to top](#top) - -### 7.4 RPC: `reset_stats` - -[Back to top](#top) - -**Function:** `leaderboard.rpc_reset_stats(context, payload)` - -Reset the caller's leaderboard record and stats to zero. - -**Parameters:** None (payload ignored). - -**Auth:** Required. - -**Return:** -```json -{ - "success": true -} -``` - -**Error strings:** `"Not authenticated"` - -**Collections:** -- **Delete:** Leaderboard record via `nk.leaderboard_record_delete("global_high_score", userId)` -- **Write:** `"stats"` / `"game_stats"` / user scope `{games_played:0, games_won:0, high_score:0, total_kills:0, total_deaths:0}` - -**Dependencies:** None. - -[Back to top](#top) - -### 7.5 RPC: `admin_update_stats` - -[Back to top](#top) - -**Function:** `leaderboard.rpc_admin_update_stats(context, payload)` - -Admin: overwrite any user's stats and update leaderboard record. - -**Parameters:** -| Param | Type | Required | Description | -|---|---|---|---| -| `payload.user_id` | string | Yes | Target user | -| `payload.stats` | table | Yes | Full stats object with `high_score`, `games_played`, `games_won`, etc. | - -**Auth:** Admin. - -**Return:** -```json -{ - "success": true -} -``` - -**Error strings:** -- `"Admin privileges required"` -- `"User ID and stats are required"` - -**Collections:** -- **Write:** `"stats"` / `"game_stats"` / target user scope -- **Leaderboard write:** `nk.leaderboard_record_write("global_high_score", ...)` - -**Dependencies:** `utils.require_admin`. - -[Back to top](#top) - -### 7.6 RPC: `admin_delete_stats` - -[Back to top](#top) - -**Function:** `leaderboard.rpc_admin_delete_stats(context, payload)` - -Admin: delete a user's stats and leaderboard record. - -**Parameters:** -| Param | Type | Required | Description | -|---|---|---|---| -| `payload.user_id` | string | Yes | Target user | - -**Auth:** Admin. - -**Return:** -```json -{ - "success": true -} -``` - -**Error strings:** -- `"Admin privileges required"` -- `"User ID is required"` - -**Collections:** -- **Delete:** `"stats"` / `"stats"` / user scope -- **Delete:** `"stats"` / `"game_stats"` / user scope -- **Delete:** Leaderboard record via `nk.leaderboard_record_delete` - -**Dependencies:** `utils.require_admin`. - -[Back to top](#top) - -### 7.7 RPC: `admin_sync_leaderboard` - -[Back to top](#top) - -**Function:** `leaderboard.rpc_admin_sync_leaderboard(context, payload)` - -Admin: trigger a full sync (delegates to `rpc_sync_leaderboard`). - -**Parameters:** Same as `sync_leaderboard`. - -**Auth:** Admin. - -**Return:** Same as `sync_leaderboard`. - -**Error strings:** Same as `sync_leaderboard` + `"Admin privileges required"`. - -**Dependencies:** `utils.require_admin`. - -[Back to top](#top) - ---- - -[Back to top](#top) - -## 8. Inbox/Mail Module (`inbox.lua`) - -[Back to top](#top) - -**Dependencies:** `utils` (for admin RPCs: `require_admin`). - -**Wallet keys used:** `star`, `gold` (in mail rewards). - -### 8.1 RPC: `admin_send_mail` - -[Back to top](#top) - -**Function:** `inbox.rpc_admin_send_mail(context, payload)` - -Admin: send global or personal mail. - -**Parameters:** -| Param | Type | Required | Default | Description | -|---|---|---|---|---| -| `payload.title` | string | No | `"Announcement"` | Mail title | -| `payload.content` | string | No | `""` | Mail body | -| `payload.rewards` | array/table | No | `{}` | Reward items (star, gold, frag, skin) | -| `payload.target_user_id` | string | No | `""` | If set, send personal mail to this user. If omitted/empty, sends global mail. | -| `payload.start_date` | string | No | now | ISO8601 start date | -| `payload.end_date` | string | No | `""` | ISO8601 global mail expiry | - -**Auth:** Admin. - -**Return:** -```json -{ - "success": true, - "mail": { - "id": "string (UUID v4)", - "title": "string", - "content": "string", - "sender": "TEKTON DEV TEAM", - "date": "ISO8601", - "start_date": "ISO8601", - "end_date": "ISO8601", - "expiry_date": "ISO8601 (+30 days)", - "rewards": [ ... ], - "type": "global|personal" - } -} -``` - -**Error strings:** `"Admin privileges required"` - -**Collections:** -- **Personal:** `"inbox"` / `"personal"` / target user scope -- **Global:** `"config"` / `"global_mail"` / system user scope - -**Dependencies:** `utils.require_admin`. - -[Back to top](#top) - -### 8.2 RPC: `get_mail` - -[Back to top](#top) - -**Function:** `inbox.rpc_get_mail(context, payload)` - -Get the caller's mail (personal + global), filtered by state (deleted/expired/not-yet-started). - -**Parameters:** None (payload ignored). - -**Auth:** Required. - -**Return:** -```json -{ - "mails": [ ... ], - "state": { - "claimed_ids": [ "string" ], - "deleted_ids": [ "string" ], - "read_ids": [ "string" ] - } -} -``` - -**Filtering rules:** -1. Exclude any mail whose ID is in `state.deleted_ids`. -2. Exclude if `expiry_date` is past. -3. Exclude if `start_date` is in the future. -4. Exclude global mail if `end_date` is past. - -**Collections read:** -- `"inbox"` / `"personal"` / user scope -- `"config"` / `"global_mail"` / system scope -- `"inbox"` / `"state"` / user scope - -**Error strings:** `"Not authenticated"` - -**Dependencies:** None. - -[Back to top](#top) - -### 8.3 RPC: `claim_mail_reward` - -[Back to top](#top) - -**Function:** `inbox.rpc_claim_mail_reward(context, payload)` - -Claim rewards attached to a mail message. - -**Parameters:** -| Param | Type | Required | Description | -|---|---|---|---| -| `payload.mail_id` | string | Yes | Mail UUID | - -**Auth:** Required. - -**Return:** -```json -{ - "success": true, - "claimed_ids": [ "string" ] -} -``` - -**Error strings:** -- `"Not authenticated"` -- `"mail_id required"` -- `"Reward already claimed"` -- mail_id in claimed_ids -- `"Mail not found"` -- mail_id not in personal or global mail lists - -**Reward processing:** -| Reward Type | Handling | -|---|---| -| `"star"` | Adds to wallet: `{star: amount}` | -| `"gold"` | Adds to wallet: `{gold: amount}` | -| `"frag_*"` / `"item"` | Increments fragment count in `"inventory"` / `"fragments"` | -| `"skin"` | Writes `"inventory"` / `` `{acquired_via: "mail", purchased_at: ...}` | - -**Collections:** -- **Read:** `"inbox"` / `"personal"` / user scope, `"config"` / `"global_mail"` / system scope, `"inbox"` / `"state"` / user scope, `"inventory"` / `"fragments"` / user scope -- **Write:** `"inbox"` / `"state"` / user scope, `"inventory"` / `"fragments"` (if frags), `"inventory"` / `` (if skins) - -**Wallet changesets:** `{star: +N}`, `{gold: +N}`. - -**Dependencies:** None. - -[Back to top](#top) - -### 8.4 RPC: `delete_mail` - -[Back to top](#top) - -**Function:** `inbox.rpc_delete_mail(context, payload)` - -Soft-delete a mail for the caller (marks in state). - -**Parameters:** -| Param | Type | Required | Description | -|---|---|---|---| -| `payload.mail_id` | string | Yes | Mail UUID | - -**Auth:** Required. - -**Return:** -```json -{ - "success": true, - "deleted_ids": [ "string" ] -} -``` - -**Error strings:** -- `"Not authenticated"` -- `"mail_id required"` - -**Collections:** -- **Read:** `"inbox"` / `"state"` / user scope -- **Write:** `"inbox"` / `"state"` / user scope (adds to `deleted_ids` and `read_ids`) - -**Dependencies:** None. - -[Back to top](#top) - -### 8.5 RPC: `save_mail_state` - -[Back to top](#top) - -**Function:** `inbox.rpc_save_mail_state(context, payload)` - -Mark mail as read by saving read_ids to state. - -**Parameters:** -| Param | Type | Required | Default | Description | -|---|---|---|---|---| -| `payload.read_ids` | array of strings | No | `[]` | Mail IDs to mark as read | - -**Auth:** Required. - -**Return:** -```json -{ - "success": true -} -``` - -**Error strings:** `"Not authenticated"` - -**Collections:** -- **Read:** `"inbox"` / `"state"` / user scope -- **Write:** `"inbox"` / `"state"` / user scope - -**Dependencies:** None. - -[Back to top](#top) - -### 8.6 RPC: `admin_list_mail` - -[Back to top](#top) - -**Function:** `inbox.rpc_admin_list_mail(context, payload)` - -Admin: list all mail (global + personal) across all users. - -**Parameters:** None (payload ignored). - -**Auth:** Admin. - -**Return:** -```json -{ - "mails": [ - { - "id": "string", - "title": "string", - "content": "string", - "type": "global|personal", - "target_user_id": "string (personal only)", - ... - } - ] -} -``` - -**Error strings:** `"Admin privileges required"` - -**Collections read:** -- `"config"` / `"global_mail"` / system scope -- `"inbox"` collection (all users, paginated via cursor until exhausted) - -**Dependencies:** `utils.require_admin`. - -[Back to top](#top) - -### 8.7 RPC: `admin_update_mail` - -[Back to top](#top) - -**Function:** `inbox.rpc_admin_update_mail(context, payload)` - -Admin: update an existing mail (title, content, dates, or reassign between global/personal). - -**Parameters:** -| Param | Type | Required | Description | -|---|---|---|---| -| `payload.mail_id` | string | Yes | Mail UUID to update | -| `payload.type` | string | No | `"global"` or `"personal"` (determines source) | -| `payload.target_user_id` | string | Cond. | Required for personal mail | -| `payload.new_target_user_id` | string | No | Move to different user | -| `payload.title` | string | No | New title | -| `payload.content` | string | No | New content | -| `payload.end_date` | string | No | New end date | -| `payload.expiry_date` | string | No | New expiry date | - -**Auth:** Admin. - -**Return:** -```json -{ - "success": true -} -``` - -**Error strings:** -- `"Admin privileges required"` -- `"mail_id required"` -- `"Mail not found in global"` / `"Mail not found in personal inbox"` -- `"target_user_id required for personal mail"` - -**Collections read/written:** Same as `admin_send_mail` but with removal from old location and insertion into new. - -**Dependencies:** `utils.require_admin`. - -[Back to top](#top) - -### 8.8 RPC: `admin_delete_mail_server` - -[Back to top](#top) - -**Function:** `inbox.rpc_admin_delete_mail_server(context, payload)` - -Admin: permanently delete mail from the server (global or personal). - -**Parameters:** -| Param | Type | Required | Description | -|---|---|---|---| -| `payload.mail_id` | string | Yes | Mail UUID | -| `payload.type` | string | No | `"global"` or `"personal"` (default: global) | -| `payload.target_user_id` | string | Cond. | Required for personal mail | - -**Auth:** Admin. - -**Return:** -```json -{ - "success": true -} -``` - -**Error strings:** -- `"Admin privileges required"` -- `"mail_id required"` -- `"Mail not found"` -- not in global mail list -- `"target_user_id required for personal mail"` -- `"Mail not found in personal inbox"` -- not in personal mail list - -**Collections:** -- **Write:** `"config"` / `"global_mail"` / system scope (filtered) OR `"inbox"` / `"personal"` / user scope (filtered) - -**Dependencies:** `utils.require_admin`. - -[Back to top](#top) - ---- - -[Back to top](#top) - -## 9. Daily Rewards Module (`daily_rewards.lua`) - -[Back to top](#top) - -**Dependencies:** `utils` (for admin RPCs: `require_admin`). - -**Wallet keys used:** `star`, `gold`. - -### 9.1 Reward Schedule (default, per month) - -[Back to top](#top) - -31 days (index 0-30), rewards scale from day 1 to 30: - -| Day | Star Reward | Formula | +| Aspect | Architecture-Server (this page) | Nakama-Server-API | |---|---|---| -| 1 | 10 | `10 + 0 * 5` | -| 2 | 15 | `10 + 1 * 5` | -| ... | ... | ... | -| 19 | 100 (capped) | `10 + 18 * 5 = 100` | -| 20-31 | 100 (capped) | `min(10 + day * 5, 100)` | - -[Back to top](#top) - -### 9.2 Reward Config Format - -[Back to top](#top) - -Admin can override per-month via `set_daily_reward_config`: - -```json -{ - "config": { - "01": [ // January (month number) - { "type": "star", "amount": 50 }, - { "type": "gold", "amount": 100 }, - { "type": "frag_common", "amount": 3 } - ], - "02": [ ... ], // February - ... - } -} -``` - -Reward entries may be a number (treated as star-only) or a `{type, amount}` object. - -Reward types support: `"star"`, `"gold"`, `"frag_*"` (fragment ID, e.g. `frag_common`). - -[Back to top](#top) - -### 9.3 RPC: `claim_daily_reward` - -[Back to top](#top) - -**Function:** `daily_rewards.rpc_claim_daily_reward(context, payload)` - -Claim today's daily reward. - -**Parameters:** None (payload ignored; reward determined by server date). - -**Auth:** Required. - -**Return:** -```json -{ - "success": true, - "reward_type": "string", - "reward_amount": "integer", - "day": "integer (1-31)" -} -``` - -**Error strings:** -- `"Not authenticated"` -- `"Already claimed today"` -- `last_claim_date == today` -- `"Already claimed all rewards for this month"` -- `dayIndex >= #monthRewards` -- `"Already claimed today's reward"` -- dayIndex already in `claimed_days` - -**Collections:** -- **Read:** `"daily_rewards"` / `"state"` / user scope, `"config"` / `"daily_rewards"` / system scope -- **Write:** `"daily_rewards"` / `"state"` / user scope (updates claimed_days, last_claim_date, month) -- **Write (frags):** `"inventory"` / `"fragments"` / user scope (if reward type is frag) - -**Wallet changesets:** -- Star reward: `{star: +amount}` -- Gold reward: `{gold: +amount}` - -**Month tracking:** -- Resets `claimed_days` when month changes. -- Uses `os.date("!*t")` for UTC dates. - -**Dependencies:** None. - -[Back to top](#top) - -### 9.4 RPC: `get_daily_reward_state` - -[Back to top](#top) - -**Function:** `daily_rewards.rpc_get_daily_reward_state(context, payload)` - -Get the caller's daily reward progress and schedule. - -**Parameters:** None (payload ignored). - -**Auth:** Required. - -**Return:** -```json -{ - "state": { - "claimed_days": [ ... ], - "last_claim_date": "YYYY-MM-DD", - "month": "MM" - }, - "month_rewards": [ - { "type": "star", "amount": 10 }, - ... - ], - "can_claim_today": "boolean", - "today_date": "YYYY-MM-DD", - "today_index": "integer (0-30)", - "server_month": "integer (1-12)" -} -``` - -**Error strings:** `"Not authenticated"` - -**Collections read:** Same as `claim_daily_reward` (state + config). - -**Dependencies:** None. - -[Back to top](#top) - -### 9.5 RPC: `set_daily_reward_config` - -[Back to top](#top) - -**Function:** `daily_rewards.rpc_set_daily_reward_config(context, payload)` - -Admin: set the monthly reward schedule. - -**Parameters:** -| Param | Type | Required | Description | -|---|---|---|---| -| `payload.config` | table | Yes | Month-keyed reward config object | - -**Auth:** Admin. - -**Return:** -```json -{ - "success": true -} -``` - -**Error strings:** `"Admin privileges required"` - -**Collections written:** `"config"` / `"daily_rewards"` / system user scope. - -**Dependencies:** `utils.require_admin`. - -[Back to top](#top) - -### 9.6 RPC: `get_daily_reward_config_admin` - -[Back to top](#top) - -**Function:** `daily_rewards.rpc_get_daily_reward_config_admin(context, payload)` - -Admin: get the current daily reward configuration. - -**Parameters:** None. - -**Auth:** Admin. - -**Return:** -```json -{ - "config": { ... } -} -``` - -**Error strings:** `"Admin privileges required"` - -**Collections read:** `"config"` / `"daily_rewards"` / system user scope. - -**Dependencies:** `utils.require_admin`. +| Audience | Architects, new devs | Implementers, AI agents | +| Detail level | High-level flow, diagrams | Per-function: params, returns, errors | +| Diagrams | Mermaid flowcharts | None | +| RPC listing | Summary table with key flows | Full 48 RPC documentation | +| Storage | Conceptual collection overview | Exact schema per collection | +| Best for | Understanding the system | Calling RPCs without reading code | [Back to top](#top) --- -[Back to top](#top) +## Quick Reference: All 48 Registered RPCs -## 10. Admin Module (`admin.lua`) - -[Back to top](#top) - -**Dependencies:** `utils` (many RPCs use `require_admin`, `require_admin_or_host`, `resolve_channel_id`, `SYSTEM_USER_ID`, `ADMIN_ROLES`). - -### 10.1 RPC: `admin_kick_player` - -[Back to top](#top) - -**Function:** `admin.rpc_admin_kick_player(context, payload)` - -Kick a player from a match. - -**Parameters:** -| Param | Type | Required | Default | Description | -|---|---|---|---|---| -| `payload.match_id` | string | Yes | -- | Match ID | -| `payload.user_id` | string | Yes | -- | Player to kick | -| `payload.reason` | string | No | `"Kicked by admin"` | Kick reason | - -**Auth:** Admin or Host. - -**Return:** -```json -{ - "success": true -} -``` - -**Error strings:** -- `"Admin or host privileges required"` -- `"Cannot kick yourself"` -- `"Failed to kick player"` -- `nk.match_signal` failed - -**Collect actions:** Sends signal `{action="kick", user_id=..., reason=...}` to the match via `nk.match_signal`. - -**Dependencies:** `utils.require_admin_or_host`. - -[Back to top](#top) - -### 10.2 RPC: `admin_ban_player` - -[Back to top](#top) - -**Function:** `admin.rpc_admin_ban_player(context, payload)` - -Ban a player account with optional duration. - -**Parameters:** -| Param | Type | Required | Default | Description | -|---|---|---|---|---| -| `payload.user_id` | string | Yes | -- | Player to ban | -| `payload.reason` | string | No | `"Banned by admin"` | Ban reason | -| `payload.duration_hours` | number | No | `nil` (permanent) | Ban duration in hours | -| `payload.match_id` | string | No | `nil` | Optional match to kick from | - -**Auth:** Admin. - -**Return:** -```json -{ - "success": true, - "ban": { - "user_id": "string", - "username": "string", - "banned_by": "string", - "banned_at": "integer (Unix time)", - "reason": "string", - "expires": "integer|null" - } -} -``` - -**Error strings:** -- `"Admin privileges required"` -- `"Cannot ban yourself"` -- `"Target account not found"` -- `"Cannot ban an admin"` -- target has admin/moderator/owner role - -**Collections:** -- **Read:** User account metadata (check role) -- **Write:** User account metadata via `nk.account_update_id` (sets `banned=true`, `ban_reason`, `ban_expires`) -- **Write:** `"bans"` / `` / system scope `{user_id, username, banned_by, banned_at, reason, expires}` -- **Signal (optional):** `{action="kick", user_id=..., reason="Banned: ..."}` via `nk.match_signal` - -**Ban expiry:** Stored as Unix timestamp (`os.time() + duration_hours * 3600`). Checked in `get_user_profile`. - -**Dependencies:** `utils.require_admin`, `utils.ADMIN_ROLES`. - -[Back to top](#top) - -### 10.3 RPC: `admin_unban_player` - -[Back to top](#top) - -**Function:** `admin.rpc_admin_unban_player(context, payload)` - -Unban a previously banned player. - -**Parameters:** -| Param | Type | Required | Description | +| RPC Name | Module | Auth | Purpose | |---|---|---|---| -| `payload.user_id` | string | Yes | Player to unban | - -**Auth:** Admin. - -**Return:** -```json -{ - "success": true -} -``` - -**Error strings:** -- `"Admin privileges required"` -- `"Target account not found"` - -**Collections:** -- **Write:** User account metadata (clears `banned`, `ban_reason`, `ban_expires`) -- **Delete:** `"bans"` / `` / system scope - -**Dependencies:** `utils.require_admin`. - -[Back to top](#top) - -### 10.4 RPC: `admin_get_ban_list` - -[Back to top](#top) - -**Function:** `admin.rpc_admin_get_ban_list(context, payload)` - -Get all active ban records. - -**Parameters:** None (payload ignored). - -**Auth:** Admin. - -**Return:** -```json -{ - "bans": [ ... ] -} -``` - -**Error strings:** `"Admin privileges required"` - -**Collections read:** `nk.storage_list("00000000-0000-0000-0000-000000000000", "bans", 100)`. - -**Dependencies:** `utils.require_admin`. - -[Back to top](#top) - -### 10.5 RPC: `admin_get_server_stats` - -[Back to top](#top) - -**Function:** `admin.rpc_admin_get_server_stats(context, payload)` - -Get server statistics (active matches, players). Optionally get a specific match's details. - -**Parameters:** -| Param | Type | Required | Description | -|---|---|---|---| -| `payload.match_id` | string | No | If provided, includes match detail and uses host-gated auth | - -**Auth:** Admin (or Host if match_id provided). - -**Return:** -```json -{ - "active_matches": "integer", - "total_players": "integer", - "server_time": "integer (Unix timestamp)", - "match": { - "id": "string", - "size": "integer", - "tick_rate": "integer", - "authoritative": "boolean" - } -} -``` - -**Error strings:** `"Admin privileges required"` or `"Admin or host privileges required"`. - -**Nakama API calls:** `nk.match_list(100, true)`, optionally `nk.match_get(match_id)`. - -**Dependencies:** `utils.require_admin`, `utils.require_admin_or_host`. - -[Back to top](#top) - -### 10.6 RPC: `admin_end_match` - -[Back to top](#top) - -**Function:** `admin.rpc_admin_end_match(context, payload)` - -End a match by sending an end_match signal. - -**Parameters:** -| Param | Type | Required | Default | Description | -|---|---|---|---|---| -| `payload.match_id` | string | Yes | -- | Match to end | -| `payload.reason` | string | No | `"Ended by admin"` | Reason | - -**Auth:** Admin or Host. - -**Return:** -```json -{ - "success": true -} -``` - -**Error strings:** `"Admin or host privileges required"` - -**Actions:** Sends `{action="end_match", reason=...}` via `nk.match_signal`. - -**Dependencies:** `utils.require_admin_or_host`. - -[Back to top](#top) - -### 10.7 RPC: `admin_set_user_role` - -[Back to top](#top) - -**Function:** `admin.rpc_admin_set_user_role(context, payload)` - -Set a user's role. Only `owner` role can use this. - -**Parameters:** -| Param | Type | Required | Description | -|---|---|---|---| -| `payload.user_id` | string | Yes | Target user | -| `payload.role` | string | Yes | One of: `"player"`, `"moderator"`, `"admin"` | - -**Auth:** Owner only (`context.user_id` metadata.role must be `"owner"`). - -**Return:** -```json -{ - "success": true, - "role": "string" -} -``` - -**Error strings:** -- `"Only owners can modify user roles"` -- caller is not owner -- `"Invalid role"` -- not one of player/moderator/admin - -**Collections:** User account metadata via `nk.account_update_id` (sets `metadata.role`). - -**Dependencies:** None (uses direct metadata check, not utils). - -[Back to top](#top) - -### 10.8 RPC: `admin_topup_gold` - -[Back to top](#top) - -**Function:** `admin.rpc_admin_topup_gold(context, payload)` - -Add 999,999 gold to the admin's wallet (self-topup). - -**Parameters:** None (payload ignored). - -**Auth:** Admin. - -**Return:** -```json -{ - "success": true, - "gold_added": 999999 -} -``` - -**Error strings:** `"Admin privileges required"` - -**Wallet changesets:** `{gold: 999999}`. - -**Dependencies:** `utils.require_admin`. - -[Back to top](#top) - -### 10.9 RPC: `admin_clear_global_chat` - -[Back to top](#top) - -**Function:** `admin.rpc_admin_clear_global_chat(context, payload)` - -Delete all messages from a chat channel. - -**Parameters:** -| Param | Type | Required | Description | -|---|---|---|---| -| `payload.channel_id` | string | Yes | Channel ID (or room name to resolve) | - -**Auth:** Admin. - -**Return:** -```json -{ - "success": true, - "deleted": "integer" -} -``` - -**Error strings:** -- `"Admin privileges required"` -- `"channel_id is required. Pass the channel ID from the client."` - -**Nakama API calls:** -- `nk.channel_messages_list(channelId, 100, false, cursor)` -- paginated -- `nk.channel_message_remove(channelId, msg.message_id)` -- per message - -**Dependencies:** `utils.require_admin`, `utils.resolve_channel_id`. - -[Back to top](#top) - -### 10.10 RPC: `admin_list_users` - -[Back to top](#top) - -**Function:** `admin.rpc_admin_list_users(context, payload)` - -List up to 500 users with their roles and ban status. - -**Parameters:** None (payload ignored). - -**Auth:** Admin. - -**Return:** -```json -{ - "users": [ - { - "user_id": "string", - "username": "string", - "display_name": "string", - "create_time": "datetime", - "role": "string", - "banned": "boolean", - "ban_reason": "string" - } - ], - "count": "integer" -} -``` - -**Error strings:** `"Admin privileges required"` - -**SQL query:** `SELECT id, username, display_name, metadata, create_time FROM users WHERE id != '00000000-0000-0000-0000-000000000000' ORDER BY create_time DESC LIMIT 500` - -**Dependencies:** `utils.require_admin`. - -[Back to top](#top) - -### 10.11 RPC: `admin_delete_users` - -[Back to top](#top) - -**Function:** `admin.rpc_admin_delete_users(context, payload)` - -Delete user accounts. Cannot delete admins or own account. - -**Parameters:** -| Param | Type | Required | Description | -|---|---|---|---| -| `payload.user_ids` | array of strings | Yes | User IDs to delete | - -**Auth:** Admin. - -**Return:** -```json -{ - "success": true, - "deleted": [ "user_id", ... ], - "failed": [ - { "user_id": "string", "reason": "string" } - ] -} -``` - -**Error strings:** -- `"Admin privileges required"` -- `"No user IDs provided"` -- `"Cannot delete your own account"` -- `"Cannot delete admin account"` -- per-user failure - -**Nakama API calls:** `nk.account_delete_id(uid, false)`. - -**Dependencies:** `utils.require_admin`. - -[Back to top](#top) - -### 10.12 RPC: `admin_get_user_detail` - -[Back to top](#top) - -**Function:** `admin.rpc_admin_get_user_detail(context, payload)` - -Get comprehensive user details including account, friends, purchases, storage, wallet ledger. - -**Parameters:** -| Param | Type | Required | Default | Description | -|---|---|---|---|---| -| `payload.user_id` | string | Yes | -- | Target user | -| `payload.collections` | array of strings | No | `["profiles","stats","inventory","receipts","history","matches","inbox"]` | Storage collections to include | - -**Auth:** Admin. - -**Return:** -```json -{ - "user": { - "user_id": "string", - "username": "string", - "display_name": "string", - "avatar_url": "string", - "lang_tag": "string", - "location": "string", - "timezone": "string", - "create_time": "string", - "update_time": "string", - "metadata": { ... }, - "wallet": { ... }, - "email": "string", - "email_verified": "boolean" - }, - "friends": [ ... ], - "purchases": [ ... ], - "wallet_ledger": [ ... ], - "storage": { - "profiles": [ { "key": "...", "value": ..., "version": "...", "update_time": "..." } ], - ... - }, - "subscription": { ... } -} -``` - -**Error strings:** -- `"Admin privileges required"` -- `"user_id is required"` - -**Collections read:** -- Account via `nk.account_get_id(userId)` -- Friends via `nk.friends_list(userId, nil, 100, "")` -- Receipts via `nk.storage_list(userId, "receipts", 100, "")` -- Each requested collection via `nk.storage_list(userId, collection, 100, "")` -- Wallet ledger via `nk.wallet_ledger_list(userId, 50)` - -**Dependencies:** `utils.require_admin`. - -[Back to top](#top) - -### 10.13 RPC: `admin_update_user_identity` - -[Back to top](#top) - -**Function:** `admin.rpc_admin_update_user_identity(context, payload)` - -Admin: update a user's identity fields (username, display_name, avatar, timezone, location, lang_tag, metadata). - -**Parameters:** -| Param | Type | Required | Description | -|---|---|---|---| -| `payload.user_id` | string | Yes | Target user | -| `payload.username` | string | No | New username | -| `payload.display_name` | string | No | New display name | -| `payload.timezone` | string | No | New timezone | -| `payload.location` | string | No | New location | -| `payload.lang_tag` | string | No | New language tag | -| `payload.avatar_url` | string | No | New avatar URL | -| `payload.metadata` | table | No | Metadata fields to merge | - -**Auth:** Admin. - -**Return:** -```json -{ - "success": true -} -``` - -**Error strings:** `"Admin privileges required"`, `"user_id is required"` - -**Collections:** User account via `nk.account_update_id`. Merges metadata fields. - -**Dependencies:** `utils.require_admin`. - -[Back to top](#top) - -### 10.14 RPC: `admin_set_user_password` - -[Back to top](#top) - -**Function:** `admin.rpc_admin_set_user_password(context, payload)` - -Admin: set a user's email password (requires user to have email credential). - -**Parameters:** -| Param | Type | Required | Description | -|---|---|---|---| -| `payload.user_id` | string | Yes | Target user | -| `payload.password` | string | Yes | New password | - -**Auth:** Admin. - -**Return:** -```json -{ - "success": true -} -``` - -**Error strings:** -- `"Admin privileges required"` -- `"user_id and password are required"` -- `"User has no email credential"` -- account has no email - -**Collections:** User account via `nk.account_update_id(userId, ..., email, password)`. - -**Dependencies:** `utils.require_admin`. - -[Back to top](#top) - -### 10.15 RPC: `admin_get_player_list` - -[Back to top](#top) - -**Function:** `admin.rpc_admin_get_player_list(context, payload)` - -Get players in a match (simplified -- rerturns empty array in current implementation). - -**Parameters:** -| Param | Type | Required | Description | -|---|---|---|---| -| `payload.match_id` | string | Yes | Match ID | - -**Auth:** Admin or Host. - -**Return:** -```json -{ - "players": [] -} -``` - -**Error strings:** -- `"Admin or host privileges required"` -- `"Match not found"` -- `nk.match_get` failed - -**Nakama API calls:** `nk.match_get(match_id)`. - -**Note:** Player presence tracking depends on the match handler implementation; currently returns `{players=[]}`. - -**Dependencies:** `utils.require_admin_or_host`. - -[Back to top](#top) - -### 10.16 RPC: `admin_get_chat_config` - -[Back to top](#top) - -**Function:** `admin.rpc_admin_get_chat_config(context, payload)` - -Get lobby chat configuration. - -**Parameters:** None (payload ignored). - -**Auth:** Admin. - -**Return:** -```json -{ - "config": { - "prefix": "string", - "max_messages": "integer", - "max_age_days": "integer" - } -} -``` - -**Error strings:** `"Admin privileges required"` - -**Collections read:** `"config"` / `"lobby_chat"` / system user scope. - -**Dependencies:** `utils.require_admin`, `utils.SYSTEM_USER_ID`. - -[Back to top](#top) - -### 10.17 RPC: `admin_set_chat_config` - -[Back to top](#top) - -**Function:** `admin.rpc_admin_set_chat_config(context, payload)` - -Set lobby chat configuration. - -**Parameters:** -| Param | Type | Required | Default | Description | -|---|---|---|---|---| -| `payload.prefix` | string | No | `""` | Chat command prefix | -| `payload.max_messages` | integer | No | `50` | Max messages in history | -| `payload.max_age_days` | integer | No | `0` | Max message age in days | - -**Auth:** Admin. - -**Return:** -```json -{ - "success": true -} -``` - -**Error strings:** `"Admin privileges required"` - -**Collections written:** `"config"` / `"lobby_chat"` / system user scope. - -**Dependencies:** `utils.require_admin`, `utils.SYSTEM_USER_ID`. - -[Back to top](#top) - -### 10.18 RPC: `admin_purge_old_messages` - -[Back to top](#top) - -**Function:** `admin.rpc_admin_purge_old_messages(context, payload)` - -Delete chat messages older than a specified age. - -**Parameters:** -| Param | Type | Required | Description | -|---|---|---|---| -| `payload.channel_id` | string | Yes | Channel ID or room name | -| `payload.max_age_days` | integer | Yes | Messages older than this (days) will be deleted | - -**Auth:** Admin. - -**Return:** -```json -{ - "success": true, - "deleted": "integer" -} -``` - -**Error strings:** -- `"Admin privileges required"` -- `"channel_id is required"` -- empty -- `"max_age_days must be > 0"` - -**Logic:** -1. Resolve channel_id via `utils.resolve_channel_id`. -2. Calculate cutoff: `os.time() - maxAgeDays * 86400`. -3. Paginate messages via `nk.channel_messages_list`. -4. Parse each message's `create_time` (ISO or Unix), compare to cutoff. -5. Remove old messages via `nk.channel_message_remove`. - -**Dependencies:** `utils.require_admin`, `utils.resolve_channel_id`. - -[Back to top](#top) - -### 10.19 RPC: `admin_list_channel_messages` - -[Back to top](#top) - -**Function:** `admin.rpc_admin_list_channel_messages(context, payload)` - -List messages in a chat channel with pagination. - -**Parameters:** -| Param | Type | Required | Default | Description | -|---|---|---|---|---| -| `payload.channel_id` | string | Yes | -- | Channel ID or room name | -| `payload.limit` | integer | No | `50` | Max messages to return | -| `payload.cursor` | string | No | `""` | Pagination cursor | -| `payload.forward` | boolean | No | `true` | Direction | - -**Auth:** Admin. - -**Return:** -```json -{ - "messages": [ - { - "message_id": "string", - "sender_id": "string", - "username": "string", - "content": "string", - "create_time": "string", - "update_time": "string", - "channel_id": "string" - } - ], - "channel_id": "string", - "next_cursor": "string", - "cache_cursor": "string" -} -``` - -**Error strings:** -- `"Admin privileges required"` -- `"channel_id is required"` -- `"Failed to list messages: "` - -**Nakama API calls:** `nk.channel_messages_list(channelId, limit, forward, cursor)`. - -**Dependencies:** `utils.require_admin`, `utils.resolve_channel_id`. - -[Back to top](#top) - -### 10.20 RPC: `admin_delete_channel_message` - -[Back to top](#top) - -**Function:** `admin.rpc_admin_delete_channel_message(context, payload)` - -Delete a single chat message. - -**Parameters:** -| Param | Type | Required | Description | -|---|---|---|---| -| `payload.channel_id` | string | Yes | Channel ID or room name | -| `payload.message_id` | string | Yes | Message UUID | - -**Auth:** Admin. - -**Return:** -```json -{ - "success": true -} -``` - -**Error strings:** -- `"Admin privileges required"` -- `"channel_id and message_id are required"` -- `"Failed to delete message: "` - -**Nakama API calls:** `nk.channel_message_remove(channelId, messageId)`. - -**Dependencies:** `utils.require_admin`, `utils.resolve_channel_id`. +| `update_display_name` | user | required | Change display name | +| `update_avatar` | user | required | Change avatar URL | +| `sync_profile` | user | required | Push profile to server | +| `change_identity` | user | required | Link new device/email | +| `set_password` | user | required | Set email password | +| `sync_friends` | user | required | Push friend list | +| `get_shop_catalog` | economy | required | Get catalog + featured | +| `buy_currency` | economy | required | IAP gold/star purchase | +| `purchase_item` | economy | required | Buy cosmetic item | +| `perform_gacha_pull` | gacha | required | Roll gacha | +| `get_leaderboard_stats` | leaderboard | no | Get top 50 scores | +| `submit_score` | leaderboard | required | Record match score | +| `sync_leaderboard` | leaderboard | required | Sync stats → leaderboard | +| `reset_stats` | leaderboard | required | Clear own stats | +| `get_mail` | inbox | required | Get available mail | +| `claim_mail_reward` | inbox | required | Claim mail rewards | +| `delete_mail` | inbox | required | Soft-delete mail | +| `save_mail_state` | inbox | required | Mark as read | +| `claim_daily_reward` | daily_rewards | required | Claim today's reward | +| `get_daily_reward_state` | daily_rewards | required | View monthly schedule | +| `set_daily_reward_config` | daily_rewards | admin | Set reward schedule | +| `get_daily_reward_config_admin` | daily_rewards | admin | Get reward config | +| `admin_*` (18 RPCs) | admin | admin/owner | Moderation tools | + +For full params/returns/errors on any RPC above, see [Nakama-Server-API](../Nakama-Server-API). [Back to top](#top) --- -[Back to top](#top) +## Deployment Topology -## 11. Storage Collections Reference +```mermaid +flowchart TD + subgraph VPS [VPS 52.74.133.55] + Gitea[Gitea Server
port 3000] + Nakama[Nakama Server
port 7350/7351] + PG[(PostgreSQL
Nakama DB)] + Act[act_runner
CI/CD Worker] + end + + Client[Godot Player] -->|HTTPS| Gitea + Client -->|gRPC/WebSocket| Nakama + Nakama --> PG + + GitHub_mirror[GitHub Mirror] -->|Push| Gitea + Gitea -->|Webhook/Manual| Act + Act -->|Build| Binary[Binary Releases] + Act -->|Build| Patch[patch.pck on patches branch] + Client -->|Check version| Gitea + Client -->|Download patch| Gitea +``` [Back to top](#top) - -Complete map of all storage collections, keys, and access scopes used by the server. - -### User-Scoped Collections (per-user storage) - -[Back to top](#top) - -| Collection | Key | Description | Read/Write | Permission | -|---|---|---|---|---| -| `history` | `logins` | Login history (last 20 entries) | user.lua | read=0, write=0 | -| `inbox` | `personal` | Personal mail messages | inbox.lua | read=1, write=0 | -| `inbox` | `state` | Mail state (claimed/deleted/read IDs) | inbox.lua | read=1, write=0 | -| `inventory` | `` | Purchased items/skins | economy.lua, gacha.lua, inbox.lua | read=1, write=0 | -| `inventory` | `fragments` | Fragment counts | inbox.lua, daily_rewards.lua | read=1, write=0 | -| `profiles` | `pity_counters` | Gacha pity per banner | gacha.lua | read=1, write=0 | -| `profiles` | `fragments` | Gacha fragments | gacha.lua | read=1, write=0 | -| `receipts` | `` | Purchase receipts / idempotency | economy.lua | read=1, write=0 | -| `stats` | `game_stats` | User game stats | leaderboard.lua | read=2/1, write=1/0 | -| `matches` | various | Match history | user.lua | -- | -| `daily_rewards` | `state` | Daily reward claim state | daily_rewards.lua | read=1, write=0 | - -### System-Scoped Collections (user_id = SYSTEM_USER_ID `"00000000-0000-0000-0000-000000000000"`) - -[Back to top](#top) - -| Collection | Key | Description | Read/Write | Permission | -|---|---|---|---|---| -| `config` | `global_mail` | Global mail messages | inbox.lua | read=2, write=0 | -| `config` | `daily_rewards` | Daily reward configuration | daily_rewards.lua | read=2, write=0 | -| `config` | `lobby_chat` | Lobby chat configuration | admin.lua | read=2, write=0 | -| `shop_config` | `featured_banners` | Shop featured banners | economy.lua | read=2, write=0 | -| `bans` | `` | Ban records | admin.lua | read=2, write=0 | - -[Back to top](#top) - -## 12. Wallet Keys and Currency - -[Back to top](#top) - -| Key | Type | Description | Operations | -|---|---|---|---| -| `gold` | integer | Gold currency (soft currency, earnable, bought via IAP) | + from buy_currency, purchase_item, mail, daily_rewards, admin_topup; - from gacha, star conversion | -| `star` | integer | Star currency (premium currency, bought with gold) | + from buy_currency, mail, daily_rewards; - from gacha | - -[Back to top](#top) - -## 13. Leaderboard Configuration - -[Back to top](#top) - -| Property | Value | -|---|---| -| **ID** | `"global_high_score"` | -| **Authoritative** | `true` (server-only writes) | -| **Sort order** | `"desc"` (highest score first) | -| **Operator** | `"best"` (keep the best score per user) | -| **Reset schedule** | `nil` (never auto-resets) | -| **Created at** | Module load (core.lua, leaderboard.lua) via `nk.leaderboard_create` | - -[Back to top](#top) - -## 14. Error Strings Reference - -[Back to top](#top) - -Complete list of every error string raised by Lua backend RPCs. - -### Authentication - -[Back to top](#top) - -| Error String | Source | -|---|---| -| `"Not authenticated"` | Multiple RPCs | -| `"Admin privileges required"` | `utils.require_admin` | -| `"Admin or host privileges required"` | `utils.require_admin_or_host` | -| `"Only owners can modify user roles"` | admin:set_user_role | - -### User - -[Back to top](#top) - -| Error String | Source | -|---|---| -| `"Account not found"` | get_user_profile | -| `"Account banned until . Reason: "` | get_user_profile | -| `"Account permanently banned. Reason: "` | get_user_profile | -| `"Failed to update profile"` | update_user_profile | -| `"Current password required"` | change_credentials | -| `"Incorrect current password."` | change_credentials | -| `"Failed to set new credentials: "` | change_credentials | -| `"Missing to_user_id or match_id"` | send_lobby_invite | -| `"user_id is required"` | send_friend_request | -| `"Cannot add yourself"` | send_friend_request | - -### Economy - -[Back to top](#top) - -| Error String | Source | -|---|---| -| `"Package ID required"` | buy_currency | -| `"Idempotency key required"` | buy_currency | -| `"Invalid package ID"` | buy_currency | -| `"InvalidReceipt"` | buy_currency | -| `"NotEnoughFunds"` | buy_currency, purchase_item | -| `"Item ID required"` | purchase_item | -| `"Invalid quantity"` | purchase_item | -| `"ItemNotFound"` | purchase_item | -| `"PurchaseFailed"` | purchase_item | -| `"Item not found in catalog: "` | admin_set_featured_banners | - -### Gacha - -[Back to top](#top) - -| Error String | Source | -|---|---| -| `"Banner ID required"` | perform_gacha_pull | -| `"Invalid count"` | perform_gacha_pull | -| `"Unknown banner: "` | perform_gacha_pull | -| `"Could not read account"` | perform_gacha_pull | -| `"Insufficient currency"` | perform_gacha_pull | -| `"Failed to update wallet"` | perform_gacha_pull | -| `"Failed to write storage: "` | perform_gacha_pull | - -### Leaderboard - -[Back to top](#top) - -| Error String | Source | -|---|---| -| `"Failed to submit score"` | submit_score | -| `"Sync failed: "` | sync_leaderboard | -| `"User ID and stats are required"` | admin_update_stats | -| `"User ID is required"` | admin_delete_stats | - -### Inbox/Mail - -[Back to top](#top) - -| Error String | Source | -|---|---| -| `"mail_id required"` | claim_mail_reward, delete_mail, admin_update_mail, admin_delete_mail_server | -| `"Reward already claimed"` | claim_mail_reward | -| `"Mail not found"` | claim_mail_reward, admin_delete_mail_server | -| `"Mail not found in global"` | admin_update_mail | -| `"Mail not found in personal inbox"` | admin_update_mail, admin_delete_mail_server | -| `"target_user_id required for personal mail"` | admin_update_mail, admin_delete_mail_server | - -### Daily Rewards - -[Back to top](#top) - -| Error String | Source | -|---|---| -| `"Already claimed today"` | claim_daily_reward | -| `"Already claimed all rewards for this month"` | claim_daily_reward | -| `"Already claimed today's reward"` | claim_daily_reward | - -### Admin - -[Back to top](#top) - -| Error String | Source | -|---|---| -| `"Cannot kick yourself"` | admin_kick_player | -| `"Failed to kick player"` | admin_kick_player | -| `"Cannot ban yourself"` | admin_ban_player | -| `"Target account not found"` | admin_ban_player, admin_unban_player | -| `"Cannot ban an admin"` | admin_ban_player | -| `"Invalid role"` | admin_set_user_role | -| `"channel_id is required. Pass the channel ID from the client."` | admin_clear_global_chat | -| `"No user IDs provided"` | admin_delete_users | -| `"Cannot delete your own account"` | admin_delete_users | -| `"Cannot delete admin account"` | admin_delete_users | -| `"user_id and password are required"` | admin_set_user_password | -| `"User has no email credential"` | admin_set_user_password | -| `"Match not found"` | admin_get_player_list | -| `"channel_id is required"` | admin_purge_old_messages, admin_list_channel_messages | -| `"max_age_days must be > 0"` | admin_purge_old_messages | -| `"channel_id and message_id are required"` | admin_delete_channel_message | -| `"Failed to delete message: "` | admin_delete_channel_message | -| `"Failed to list messages: "` | admin_list_channel_messages | - -[Back to top](#top) - -## 15. Permission Levels - -[Back to top](#top) - -| Level | metadata.role | Can Access | -|---|---|---| -| **Player** | `"player"` (default) | All non-admin RPCs | -| **Moderator** | `"moderator"` | All admin RPCs except `set_user_role` | -| **Admin** | `"admin"` | All admin RPCs except `set_user_role` | -| **Owner** | `"owner"` | All RPCs including `admin_set_user_role` | - -**Guard functions:** -- `utils.require_admin(context)` -- requires admin, moderator, or owner -- `utils.require_admin_or_host(context, match_id)` -- requires admin/moderator/owner, OR be the match host -- `admin_set_user_role` -- requires `metadata.role == "owner"` (checked inline, not via utils) - -**Match host check:** -- `utils.is_match_host(context, match_id)` checks if `state.hostUserId == context.user_id` in the match's decoded state. - -[Back to top](#top) - ---- - -*End of function reference. Generated from source code. 48 registered RPCs, 4 after-hooks. Last updated: July 2026.*