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