docs: rewrite arch server as architecture diagrams + flow, not RPC listing
+304
-2734
@@ -1,2826 +1,396 @@
|
||||
# Tekton Armageddon - Server Architecture
|
||||
|
||||
<a id="top"></a>
|
||||
|
||||
# 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<br/>player.tscn] -->|RPC calls| Nakama[Nakama Server<br/>port 7350/7351]
|
||||
Nakama -->|wallet_update| Wallet[Wallet Engine]
|
||||
Nakama -->|storage_read/write| Storage[Nakama Storage DB<br/>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<br/>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<br/>Entry point: loads all modules"] --> utils["utils.lua<br/>Auth guards, channel resolver"]
|
||||
main --> core["core.lua<br/>Authentication hooks<br/>Wallet init, ban check"]
|
||||
main --> economy["economy.lua<br/>Shop catalog, currency IAP<br/>Item purchases"]
|
||||
main --> gacha["gacha.lua<br/>Gacha pulls, pity system<br/>RNG on server"]
|
||||
main --> leaderboard["leaderboard.lua<br/>Score submission, sync<br/>Global rankings"]
|
||||
main --> inbox["inbox.lua<br/>Global/personal mail<br/>Reward claiming"]
|
||||
main --> daily["daily_rewards.lua<br/>Daily login rewards<br/>Monthly schedule"]
|
||||
main --> user["user.lua<br/>Profile updates, identity<br/>Friend sync"]
|
||||
main --> admin["admin.lua<br/>Kick/ban, stats, chat mgmt<br/>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<br/>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<br/>config/global_mail<br/>system user]
|
||||
Admin -->|admin_send_mail<br/>with target_user_id| Personal[Personal Mail<br/>inbox/personal<br/>per user]
|
||||
|
||||
Client -->|get_mail| Merge[Merge global + personal]
|
||||
Merge --> Filter[Filter by: not deleted,<br/>within date range,<br/>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<br/>config/global_mail<br/>system user]
|
||||
Admin -->|admin_send_mail<br/>with target_user_id| Personal[Personal Mail<br/>inbox/personal<br/>per user]
|
||||
|
||||
**Dependencies:** `utils` (for `admin_get_user_history`).
|
||||
Client -->|get_mail| Merge[Merge global + personal]
|
||||
Merge --> Filter[Filter by: not deleted,<br/>within date range,<br/>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:<br/>gold/star -> wallet<br/>fragments -> fragment storage<br/>skins -> inventory storage]
|
||||
Grant --> UpdateState[Update state:<br/>add mailId to claimed_ids]
|
||||
```
|
||||
|
||||
**Error strings:**
|
||||
- `"Account not found"` -- target user does not exist
|
||||
- `"Account banned until <date>. Reason: <reason>"` -- user is banned (absolute or expiry)
|
||||
- `"Account permanently banned. Reason: <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 `"%<query>%"`
|
||||
|
||||
**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: <error>"` -- `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** | `"<senderName> 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"` / `<idempotencyKey>` / user scope (duplicate check)
|
||||
- **Write (pending):** `"receipts"` / `<idempotencyKey>` / user scope `{type, package_id, status="pending", created_at}`
|
||||
- **Write (verified):** `"receipts"` / `<idempotencyKey>` / 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"` / `<idempotencyKey>` / user scope
|
||||
- **Write:** `"inventory"` / `<itemId>` / user scope `{category, purchased_at, quantity}`
|
||||
- **Write:** `"receipts"` / `<idempotencyKey>` / 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: <itemId>"`
|
||||
|
||||
**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: <id>"`
|
||||
- `"Could not read account"`
|
||||
- `"Insufficient currency"`
|
||||
- `"Failed to update wallet"`
|
||||
- `"Failed to write storage: <error>"`
|
||||
|
||||
**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"` / `<item_id>` / 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: <error>"` -- `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"` / `<skin_id>` `{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"` / `<skin_id>` (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"` / `<user_id>` / 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"` / `<user_id>` / 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: <error>"`
|
||||
|
||||
**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: <error>"`
|
||||
|
||||
**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<br/>port 3000]
|
||||
Nakama[Nakama Server<br/>port 7350/7351]
|
||||
PG[(PostgreSQL<br/>Nakama DB)]
|
||||
Act[act_runner<br/>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` | `<item_id>` | 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` | `<idempotency_key>` | 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` | `<user_id>` | 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 <date>. Reason: <reason>"` | get_user_profile |
|
||||
| `"Account permanently banned. Reason: <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: <error>"` | 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: <item_id>"` | 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: <id>"` | 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: <error>"` | perform_gacha_pull |
|
||||
|
||||
### Leaderboard
|
||||
|
||||
[Back to top](#top)
|
||||
|
||||
| Error String | Source |
|
||||
|---|---|
|
||||
| `"Failed to submit score"` | submit_score |
|
||||
| `"Sync failed: <error>"` | 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: <error>"` | admin_delete_channel_message |
|
||||
| `"Failed to list messages: <error>"` | 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.*
|
||||
|
||||
Reference in New Issue
Block a user