chore: clean stale docs, add lua server reference, update steamworks guide
This commit is contained in:
@@ -0,0 +1,1639 @@
|
||||
# Tekton Nakama Lua Server API Reference
|
||||
|
||||
> Auto-generated from source code. For AI agents — call these RPCs without reading Lua.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Authentication & Core (core.lua)](#1-authentication--core-coreluau)
|
||||
2. [Utilities (utils.lua)](#2-utilities-utilsluau)
|
||||
3. [User Module (user.lua)](#3-user-module-userluau)
|
||||
4. [Economy Module (economy.lua)](#4-economy-module-economyluau)
|
||||
5. [Gacha Module (gacha.lua)](#5-gacha-module-gachaluau)
|
||||
6. [Leaderboard Module (leaderboard.lua)](#6-leaderboard-module-leaderboardluau)
|
||||
7. [Inbox/Mail Module (inbox.lua)](#7-inboxmail-module-inboxluau)
|
||||
8. [Daily Rewards Module (daily_rewards.lua)](#8-daily-rewards-module-daily_rewardsluau)
|
||||
9. [Admin Module (admin.lua)](#9-admin-module-adminluau)
|
||||
10. [Storage Collections Reference](#10-storage-collections-reference)
|
||||
11. [Leaderboard Config](#11-leaderboard-config)
|
||||
12. [Error Strings Reference](#12-error-strings-reference)
|
||||
13. [Permission Levels](#13-permission-levels)
|
||||
|
||||
---
|
||||
|
||||
## 1. Authentication & Core (`core.lua`)
|
||||
|
||||
### After-Hook: `after_authenticate_steam`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Trigger** | `AuthenticateSteam` |
|
||||
| **Purpose** | On first Steam login: sets display_name from Steam username, default role to `"player"` |
|
||||
| **Auth** | None (internal hook) |
|
||||
|
||||
**Behavior:**
|
||||
- If user has no `display_name`, sets it from `input.username` (fallback: `"SteamPlayer"`)
|
||||
- If user metadata has no `role`, sets `metadata.role = "player"`
|
||||
|
||||
### Startup (not an RPC)
|
||||
|
||||
```
|
||||
nk.leaderboard_create("global_high_score", true, "desc", "best", nil, {})
|
||||
```
|
||||
|
||||
Creates native leaderboard `"global_high_score"` on module load (also created in `leaderboard.lua` — pcall wraps both so duplicate is safe).
|
||||
|
||||
---
|
||||
|
||||
## 2. Utilities (`utils.lua`)
|
||||
|
||||
Shared helpers used by other modules.
|
||||
|
||||
### Constants
|
||||
|
||||
| Constant | Value | Purpose |
|
||||
|---|---|---|
|
||||
| `ADMIN_ROLES` | `{admin=true, moderator=true, owner=true}` | Roles with admin privileges |
|
||||
| `SYSTEM_USER_ID` | `"00000000-0000-0000-0000-000000000000"` | System/nil user for global storage |
|
||||
| `CHANNEL_TYPE_ROOM` | `1` | Nakama channel type for rooms |
|
||||
| `CHANNEL_TYPE_DIRECT` | `2` | Nakama channel type for DMs |
|
||||
| `CHANNEL_TYPE_GROUP` | `3` | Nakama channel type for groups |
|
||||
|
||||
### `utils.is_admin(context)`
|
||||
|
||||
Checks if caller has admin/moderator/owner role.
|
||||
|
||||
| Param | Type | Description |
|
||||
|---|---|---|
|
||||
| `context` | table | Nakama RPC context |
|
||||
|
||||
**Returns:** `boolean`
|
||||
|
||||
### `utils.is_match_host(context, match_id)`
|
||||
|
||||
Checks if caller is the host of a match.
|
||||
|
||||
| Param | Type | Description |
|
||||
|---|---|---|
|
||||
| `context` | table | Nakama RPC context |
|
||||
| `match_id` | string | Match ID to check |
|
||||
|
||||
**Returns:** `boolean`
|
||||
**Note:** Reads `match.state` and checks `state.hostUserId` against `context.user_id`.
|
||||
|
||||
### `utils.require_admin(context)`
|
||||
|
||||
Errors with `"Admin privileges required"` if caller is not admin/moderator/owner.
|
||||
|
||||
### `utils.require_admin_or_host(context, match_id)`
|
||||
|
||||
Errors with `"Admin or host privileges required"` if caller is neither admin nor match host.
|
||||
|
||||
### `utils.resolve_channel_id(channel_id)`
|
||||
|
||||
Resolves a friendly room name (e.g. `"social_global"`) to a hashed Nakama channel ID.
|
||||
|
||||
| Param | Type | Description |
|
||||
|---|---|---|
|
||||
| `channel_id` | string | Raw channel ID or friendly room name |
|
||||
|
||||
**Returns:** `string` — resolved channel ID (passes through if already hashed, i.e. contains `"."`)
|
||||
**Logic:** If no `"."` in value, calls `nk.channel_id_build("", name, CHANNEL_TYPE_ROOM)`.
|
||||
|
||||
---
|
||||
|
||||
## 3. User Module (`user.lua`)
|
||||
|
||||
### RPC: `get_user_profile`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `user.rpc_get_user_profile` |
|
||||
| **Auth** | None required (can view any profile) |
|
||||
| **Purpose** | Get a user's public profile |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|---|---|---|---|---|
|
||||
| `user_id` | string | No | `context.user_id` | Target user ID |
|
||||
|
||||
**Returns (JSON):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `user_id` | string | User ID |
|
||||
| `username` | string | Username |
|
||||
| `display_name` | string | Display name |
|
||||
| `avatar_url` | string | Avatar URL |
|
||||
| `create_time` | string | Account creation time |
|
||||
| `role` | string | Role from metadata (`"player"` default) |
|
||||
|
||||
**Errors:**
|
||||
- `"Account not found"` — user doesn't exist
|
||||
- `"Account banned until <time>. Reason: <reason>"` — temporary ban still active
|
||||
- `"Account permanently banned. Reason: <reason>"` — permanent ban
|
||||
|
||||
**Ban logic:** Reads `metadata.banned`, `metadata.ban_expires` (Unix timestamp), `metadata.ban_reason`. If `ban_expires <= os.time()`, auto-clears the ban on read.
|
||||
|
||||
### RPC: `update_user_profile`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `user.rpc_update_user_profile` |
|
||||
| **Auth** | Required |
|
||||
| **Purpose** | Update own display name and/or avatar |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `display_name` | string | No | New display name |
|
||||
| `avatar_url` | string | No | New avatar URL |
|
||||
|
||||
**Returns (JSON):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `success` | bool | Always true |
|
||||
|
||||
**Errors:**
|
||||
- `"Not authenticated"`
|
||||
- `"Failed to update profile"`
|
||||
|
||||
### RPC: `search_users`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `user.rpc_search_users` |
|
||||
| **Auth** | Required |
|
||||
| **Purpose** | Search users by username or display name |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|---|---|---|---|---|
|
||||
| `query` | string | No | `""` | Search term (ILike match on username/display_name). Empty = list all (limit 100). |
|
||||
|
||||
**Returns (JSON):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `users` | array | Array of user objects |
|
||||
|
||||
**User object:**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `user_id` | string | User ID |
|
||||
| `username` | string | Username |
|
||||
| `display_name` | string | Display name |
|
||||
| `avatar_url` | string | Avatar URL from metadata |
|
||||
|
||||
**SQL:** Queries `users` table with `ILIKE`. Excludes system user `00000000-0000-0000-0000-000000000000`. Limit 100.
|
||||
|
||||
### RPC: `change_credentials`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `user.rpc_change_credentials` |
|
||||
| **Auth** | Required |
|
||||
| **Purpose** | Change email + password credentials |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `new_email` | string | No | New email |
|
||||
| `new_password` | string | No | New password |
|
||||
| `current_password` | string | If email set | Current password (required to unlink old email) |
|
||||
|
||||
**Returns (JSON):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `success` | bool | Always true |
|
||||
|
||||
**Errors:**
|
||||
- `"Not authenticated"`
|
||||
- `"Current password required"` — account has email but no `current_password` provided
|
||||
- `"Incorrect current password."` — email auth re-verify failed
|
||||
- `"Failed to set new credentials: <err>"` — link_email failed (rolls back old link)
|
||||
|
||||
**Logic:** Verifies current password via `nk.authenticate_email`, unlinks old email, links new email+password. On failure, re-links old credentials.
|
||||
|
||||
### RPC: `send_lobby_invite`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `user.rpc_send_lobby_invite` |
|
||||
| **Auth** | Required |
|
||||
| **Purpose** | Send a lobby invitation notification |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `to_user_id` | string | Yes | Recipient user ID |
|
||||
| `match_id` | string | Yes | Match/lobby ID to invite to |
|
||||
|
||||
**Returns (JSON):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `success` | bool | Always true |
|
||||
|
||||
**Errors:**
|
||||
- `"Not authenticated"`
|
||||
- `"Missing to_user_id or match_id"`
|
||||
|
||||
**Notification:** Code `1001`, persistent (`true`). Content: `{match_id, from_name}`.
|
||||
|
||||
### RPC: `send_friend_request`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `user.rpc_send_friend_request` |
|
||||
| **Auth** | Required |
|
||||
| **Purpose** | Send a friend request notification |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `user_id` | string | Yes | Target user ID |
|
||||
|
||||
**Returns (JSON):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `success` | bool | Always true |
|
||||
|
||||
**Errors:**
|
||||
- `"Not authenticated"`
|
||||
- `"user_id is required"`
|
||||
- `"Cannot add yourself"`
|
||||
|
||||
**Notification:** Code `1002`, persistent (`true`). Content: `{from_user_id, from_name}`.
|
||||
|
||||
### RPC: `admin_get_user_history`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `user.rpc_admin_get_user_history` |
|
||||
| **Auth** | Admin required |
|
||||
| **Purpose** | Get wallet ledger, login history, and match history for a user |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `user_id` | string | Yes | Target user ID |
|
||||
|
||||
**Returns (JSON):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `history.wallet_ledger` | array | Last 50 wallet ledger entries |
|
||||
| `history.logins` | array | Login history (up to 20) |
|
||||
| `history.matches` | array | Match history from storage collection `"matches"` |
|
||||
|
||||
**Errors:**
|
||||
- `"Admin privileges required"` (via `utils.require_admin`)
|
||||
- `"user_id is required"`
|
||||
|
||||
### After-Hook: `after_authenticate`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Trigger** | `AuthenticateDevice`, `AuthenticateEmail`, `AuthenticateCustom` |
|
||||
| **Purpose** | Record login timestamp + IP to user storage |
|
||||
|
||||
**Storage write:** Collection `"history"`, key `"logins"`, permission_read=`0`, permission_write=`0`. Keeps last 20 entries. Each entry: `{time, ip}`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Economy Module (`economy.lua`)
|
||||
|
||||
### Shop Catalog
|
||||
|
||||
Hardcoded items available for purchase:
|
||||
|
||||
| ID | Name | Category | Gold | Star | Rarity | Character |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `oldpop-blue-hat` | Oldpop Blue Hat | head | 100 | 0 | Common | Oldpop |
|
||||
| `oldpop-green-hat` | Oldpop Green Hat | head | 100 | 0 | Common | Oldpop |
|
||||
| `oldpop-red-hat` | Oldpop Red Hat | head | 100 | 0 | Common | Oldpop |
|
||||
| `oldpop-yellow-hat` | Oldpop Yellow Hat | head | 100 | 0 | Common | Oldpop |
|
||||
| `oldpop-og-pant` | Copper OG Pant | costume | 0 | 0 | Common | Oldpop |
|
||||
| `oldpop-grey-pant` | Copper Grey Pant | costume | 150 | 0 | Common | Oldpop |
|
||||
| `oldpop-red-pant` | Copper Red Pant | costume | 150 | 0 | Common | Oldpop |
|
||||
| `oldpop-yellow-pant` | Copper Yellow Pant | costume | 150 | 0 | Common | Oldpop |
|
||||
| `oldpop-blue-gloves` | Oldpop Blue Gloves | glove | 75 | 0 | Common | Oldpop |
|
||||
| `oldpop-green-gloves` | Oldpop Green Gloves | glove | 75 | 0 | Common | Oldpop |
|
||||
| `oldpop-red-gloves` | Oldpop Red Gloves | glove | 75 | 0 | Common | Oldpop |
|
||||
| `oldpop-yellow-gloves` | Oldpop Yellow Gloves | glove | 75 | 0 | Common | Oldpop |
|
||||
|
||||
### RPC: `get_shop_catalog`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `economy.rpc_get_shop_catalog` |
|
||||
| **Auth** | Required |
|
||||
| **Purpose** | Get the shop catalog grouped by category, plus featured banners |
|
||||
|
||||
**Params:** None (ignore payload)
|
||||
|
||||
**Returns (JSON):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `catalog` | object | Grouped by category (`head`, `costume`, `glove`). Each entry: `{id, name, gold, star, rarity, character}` |
|
||||
| `featured_banners` | array | Banners from `shop_config`/`featured_banners` (max 3 items, each: `{item_id, ...}`) |
|
||||
|
||||
**Errors:** `"Not authenticated"`
|
||||
|
||||
### RPC: `buy_currency`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `economy.rpc_buy_currency` |
|
||||
| **Auth** | Required |
|
||||
| **Purpose** | Purchase a currency package (IAP) |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `package_id` | string | Yes | One of: `gold_100`, `gold_500`, `gold_1000`, `gold_2000`, `gold_5000`, `gold_10000`, `star_100`, `star_250`, `star_600` |
|
||||
| `receipt` | string | For gold packages | Store receipt for validation |
|
||||
| `idempotency_key` | string | Yes | Idempotency key (prevents double-claim) |
|
||||
| `store_type` | string | No | `"google"`, `"apple"`, or `"test"` (default: `"test"`) |
|
||||
|
||||
**Package table:**
|
||||
|
||||
| Package ID | Gold Change | Star Change | 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 |
|
||||
|
||||
**Returns (JSON) — success:**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `success` | bool | `true` |
|
||||
| `status` | string | `"verified"` (receipt validated) or `"pending"` (no receipt, awaiting verification) |
|
||||
| `package_id` | string | Package ID |
|
||||
| `duplicate` | bool | (optional) `true` if idempotency key already used |
|
||||
|
||||
**Errors:**
|
||||
- `"Not authenticated"`
|
||||
- `"Package ID required"`
|
||||
- `"Idempotency key required"`
|
||||
- `"Invalid package ID"`
|
||||
- `"InvalidReceipt"` — IAP validation failed
|
||||
- `"NotEnoughFunds"` — wallet update failed (e.g. insufficient gold for star packages)
|
||||
|
||||
**Idempotency:** Checks `receipts` collection with idempotency_key. Returns existing result on duplicate.
|
||||
|
||||
**Receipt validation:** For `store_type="test"`, accepts `receipt == "mock_receipt_for_now"`.
|
||||
|
||||
### RPC: `purchase_item`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `economy.rpc_purchase_item` |
|
||||
| **Auth** | Required |
|
||||
| **Purpose** | Purchase an item from the shop catalog |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|---|---|---|---|---|
|
||||
| `item_id` | string | Yes | — | Item ID from catalog |
|
||||
| `quantity` | number | No | 1 | Quantity (min 1) |
|
||||
| `idempotency_key` | string | Yes | — | Idempotency key |
|
||||
|
||||
**Returns (JSON):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `success` | bool | `true` |
|
||||
| `item` | string | Item ID purchased |
|
||||
| `duplicate` | bool | (optional) `true` if idempotency key already used |
|
||||
|
||||
**Errors:**
|
||||
- `"Not authenticated"`
|
||||
- `"Item ID required"`
|
||||
- `"Invalid quantity"`
|
||||
- `"Idempotency key required"`
|
||||
- `"ItemNotFound"`
|
||||
- `"NotEnoughFunds"` — insufficient gold/star balance
|
||||
- `"PurchaseFailed"` — storage write failed
|
||||
|
||||
**Storage writes:**
|
||||
- Collection `"inventory"`, key `item_id`: `{category, purchased_at, quantity}`
|
||||
- Collection `"receipts"`, key `idempotencyKey`: `{type="item", item_id, quantity, cost, processed_at}`
|
||||
|
||||
### RPC: `admin_set_featured_banners`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `economy.rpc_admin_set_featured_banners` |
|
||||
| **Auth** | Admin required |
|
||||
| **Purpose** | Set up to 3 featured banner items for the shop |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|---|---|---|---|---|
|
||||
| `banners` | array | No | `[]` | Array of banner objects, each with `{item_id, ...}` (max 3) |
|
||||
|
||||
**Returns (JSON):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `success` | bool | `true` |
|
||||
| `banners` | array | Final banners array (max 3) |
|
||||
|
||||
**Errors:**
|
||||
- `"Admin privileges required"`
|
||||
- `"Item not found in catalog: <itemId>"` — banner references a non-catalog item
|
||||
|
||||
**Storage:** Collection `"shop_config"`, key `"featured_banners"`, user `SYSTEM_USER_ID`, permission_read=`2`, permission_write=`0`.
|
||||
|
||||
### RPC: `admin_get_featured_banners`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `economy.rpc_admin_get_featured_banners` |
|
||||
| **Auth** | Admin required |
|
||||
| **Purpose** | Get current featured banners config |
|
||||
|
||||
**Params:** None (ignore payload)
|
||||
|
||||
**Returns (JSON):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `banners` | array | Current banners (may be empty) |
|
||||
|
||||
---
|
||||
|
||||
## 5. Gacha Module (`gacha.lua`)
|
||||
|
||||
### Gacha Banners
|
||||
|
||||
| Banner ID | Name | Currency | Pull 1 Cost | Pull 10 Cost | Pity At |
|
||||
|---|---|---|---|---|---|
|
||||
| `star` | Star Banner | star | 160 | 1440 | 90 |
|
||||
| `gold` | Gold Banner | gold | 50 | 450 | 90 |
|
||||
|
||||
### Drop Rates
|
||||
|
||||
| Rarity | Probability | Pool Items |
|
||||
|---|---|---|
|
||||
| `real_prize` | 1.0% | `skin_gacha_rainbow_suit`, `skin_gacha_dragon_hat`, `skin_gacha_phantom_gloves`, `skin_gacha_neon_acc` |
|
||||
| `rare` | 14.0% | `frag_rare` |
|
||||
| `uncommon` | 25.0% | `frag_uncommon` |
|
||||
| `common` | 60.0% | `frag_common` |
|
||||
|
||||
### Real Prize Catalog
|
||||
|
||||
| Item 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 |
|
||||
|
||||
### Fragment Items
|
||||
|
||||
| ID | Name | Rarity |
|
||||
|---|---|---|
|
||||
| `frag_common` | Common Fragment | common |
|
||||
| `frag_uncommon` | Uncommon Fragment | uncommon |
|
||||
| `frag_rare` | Rare Fragment | rare |
|
||||
|
||||
### RPC: `perform_gacha_pull`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `gacha.rpc_perform_gacha_pull` |
|
||||
| **Auth** | Required |
|
||||
| **Purpose** | Perform 1 or 10 gacha pulls on a banner |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|---|---|---|---|---|
|
||||
| `banner_id` | string | Yes | — | `"star"` or `"gold"` |
|
||||
| `count` | number | No | 1 | Number of pulls (1 or 10) |
|
||||
|
||||
**Returns (JSON):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `success` | bool | Always true |
|
||||
| `results` | array | Array of pull results: `{id, rarity, name}` |
|
||||
| `new_pity` | number | Updated pity counter for this banner |
|
||||
|
||||
**Errors:**
|
||||
- `"Not authenticated"`
|
||||
- `"Banner ID required"`
|
||||
- `"Invalid count"`
|
||||
- `"Unknown banner: <banner_id>"`
|
||||
- `"Could not read account"`
|
||||
- `"Insufficient currency"`
|
||||
- `"Failed to update wallet"`
|
||||
- `"Failed to write storage: <err>"`
|
||||
|
||||
**Pity system:** After `pity_at` (90) pulls without a `real_prize`, the next pull is guaranteed `real_prize`. Pity counter resets on `real_prize` drop. Pity counter persists per banner in storage.
|
||||
|
||||
**Storage reads/writes:**
|
||||
- Collection `"profiles"`, key `"pity_counters"`: per-banner pity counts
|
||||
- Collection `"profiles"`, key `"fragments"`: fragment inventory
|
||||
- Collection `"inventory"`, key = item_id: real prize items
|
||||
|
||||
**Wallet:** Deducts currency cost. For `count=10`, uses `pull_10_cost` if available, otherwise `pull_1_cost * count`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Leaderboard Module (`leaderboard.lua`)
|
||||
|
||||
### Leaderboard Config
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| **ID** | `"global_high_score"` |
|
||||
| **Authoritative** | `true` |
|
||||
| **Sort Order** | `"desc"` (highest first) |
|
||||
| **Operator** | `"best"` (best score kept) |
|
||||
| **Reset Schedule** | `nil` (never resets) |
|
||||
|
||||
### RPC: `get_leaderboard_stats`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `leaderboard.rpc_get_leaderboard_stats` |
|
||||
| **Auth** | None |
|
||||
| **Purpose** | Get top 50 leaderboard entries |
|
||||
|
||||
**Params:** None (ignore payload)
|
||||
|
||||
**Returns (JSON):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `leaderboard` | array | Top 50 leaderboard entries |
|
||||
|
||||
**Leaderboard entry object:**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `user_id` | string | Owner user ID |
|
||||
| `username` | string | Username |
|
||||
| `display_name` | string | Username (fallback — actually `record.username`) |
|
||||
| `avatar_url` | string | Avatar URL from metadata |
|
||||
| `loadout_character` | string | Character from metadata (default `"Copper"`) |
|
||||
| `high_score` | number | Score |
|
||||
| `games_played` | number | From metadata |
|
||||
| `games_won` | number | From metadata |
|
||||
|
||||
### RPC: `submit_score`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `leaderboard.rpc_submit_score` |
|
||||
| **Auth** | Required |
|
||||
| **Purpose** | Submit/update own score on the leaderboard |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|---|---|---|---|---|
|
||||
| `score` | number | No | 0 | Score to submit |
|
||||
| `games_played` | number | No | 0 | Games played count |
|
||||
| `games_won` | number | No | 0 | Games won count |
|
||||
| `avatar_url` | string | No | From account | Avatar URL |
|
||||
| `loadout_character` | string | No | `"Copper"` | Character selection |
|
||||
|
||||
**Returns (JSON):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `success` | bool | `true` |
|
||||
|
||||
**Errors:**
|
||||
- `"Not authenticated"`
|
||||
- `"Failed to submit score"`
|
||||
|
||||
### RPC: `sync_leaderboard`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `leaderboard.rpc_sync_leaderboard` |
|
||||
| **Auth** | Required |
|
||||
| **Purpose** | Bulk-sync all users' stats from `"stats"` and `"profiles"` collections to the native leaderboard |
|
||||
|
||||
**Params:** None (ignore payload)
|
||||
|
||||
**Returns (JSON):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `success` | bool | Always true |
|
||||
| `synced` | number | Number of records written |
|
||||
| `objects_found` | number | Stats objects found |
|
||||
| `debug` | array | Error messages per user |
|
||||
|
||||
**Errors:**
|
||||
- `"Not authenticated"`
|
||||
- `"Sync failed: <err>"`
|
||||
|
||||
**Storage reads:** Collection `"stats"` (all users, limit 100), Collection `"profiles"` (all users, limit 100), key `"profile"`.
|
||||
|
||||
### RPC: `reset_stats`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `leaderboard.rpc_reset_stats` |
|
||||
| **Auth** | Required |
|
||||
| **Purpose** | Delete own leaderboard record and reset stats to zero |
|
||||
|
||||
**Params:** None (ignore payload)
|
||||
|
||||
**Returns (JSON):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `success` | bool | `true` |
|
||||
|
||||
**Storage write:** Collection `"stats"`, key `"game_stats"` — zeros out `games_played`, `games_won`, `high_score`, `total_kills`, `total_deaths`. permission_read=`2`, permission_write=`1`.
|
||||
|
||||
### RPC: `admin_update_stats`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `leaderboard.rpc_admin_update_stats` |
|
||||
| **Auth** | Admin required |
|
||||
| **Purpose** | Force-overwrite a user's stats and leaderboard record |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `user_id` | string | Yes | Target user ID |
|
||||
| `stats` | object | Yes | Stats object: `{high_score, games_played, games_won, loadout_character, ...}` |
|
||||
|
||||
**Returns (JSON):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `success` | bool | `true` |
|
||||
|
||||
**Errors:**
|
||||
- `"Admin privileges required"`
|
||||
- `"User ID and stats are required"`
|
||||
|
||||
**Storage write:** Collection `"stats"`, key `"game_stats"`, permission_read=`1`, permission_write=`0`.
|
||||
|
||||
### RPC: `admin_delete_stats`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `leaderboard.rpc_admin_delete_stats` |
|
||||
| **Auth** | Admin required |
|
||||
| **Purpose** | Delete a user's stats storage and leaderboard record |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `user_id` | string | Yes | Target user ID |
|
||||
|
||||
**Storage deletes:** `{collection="stats", key="stats"}`, `{collection="stats", key="game_stats"}`, plus leaderboard record.
|
||||
|
||||
### RPC: `admin_sync_leaderboard`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `leaderboard.rpc_admin_sync_leaderboard` |
|
||||
| **Auth** | Admin required |
|
||||
| **Purpose** | Same as `sync_leaderboard` but requires admin |
|
||||
|
||||
**Delegates to:** `leaderboard.rpc_sync_leaderboard` (same params, same returns).
|
||||
|
||||
---
|
||||
|
||||
## 7. Inbox/Mail Module (`inbox.lua`)
|
||||
|
||||
### RPC: `admin_send_mail`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `inbox.rpc_admin_send_mail` |
|
||||
| **Auth** | Admin required |
|
||||
| **Purpose** | Send a personal (targeted) or global mail |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|---|---|---|---|---|
|
||||
| `title` | string | No | `"Announcement"` | Mail title |
|
||||
| `content` | string | No | `""` | Mail body |
|
||||
| `start_date` | string | No | Current UTC | ISO8601 start date |
|
||||
| `end_date` | string | No | `""` | ISO8601 end date (global only) |
|
||||
| `rewards` | array/object | No | `{}` | Attached rewards (see claim_mail_reward for format) |
|
||||
| `target_user_id` | string | No | `""` | If set → personal mail to this user. Omit → global mail |
|
||||
|
||||
**Returns (JSON):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `success` | bool | `true` |
|
||||
| `mail` | object | The created mail object |
|
||||
|
||||
**Mail object fields:** `id` (UUIDv4), `title`, `content`, `sender` (`"TEKTON DEV TEAM"`), `date`, `start_date`, `end_date`, `expiry_date` (30 days from now), `rewards`, `type` (`"personal"` or `"global"`).
|
||||
|
||||
**Personal storage:** Collection `"inbox"`, key `"personal"`, permission_read=`1`, permission_write=`0`.
|
||||
**Global storage:** Collection `"config"`, key `"global_mail"`, user `SYSTEM_USER_ID`, permission_read=`2`, permission_write=`0`.
|
||||
|
||||
### RPC: `get_mail`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `inbox.rpc_get_mail` |
|
||||
| **Auth** | Required |
|
||||
| **Purpose** | Get all non-deleted, non-expired personal + global mail for the user |
|
||||
|
||||
**Params:** None (ignore payload)
|
||||
|
||||
**Returns (JSON):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `mails` | array | Filtered mail objects (excludes deleted, expired, not-yet-started, ended global) |
|
||||
| `state` | object | `{claimed_ids, deleted_ids, read_ids}` |
|
||||
|
||||
**Filtering logic:** Excludes mail where:
|
||||
- ID is in `state.deleted_ids`
|
||||
- `expiry_date` is in the past
|
||||
- `start_date` is in the future
|
||||
- Global mail with `end_date` in the past
|
||||
|
||||
**Storage reads:**
|
||||
- Collection `"inbox"`, key `"personal"`, user = caller
|
||||
- Collection `"config"`, key `"global_mail"`, user = `SYSTEM_USER_ID`
|
||||
- Collection `"inbox"`, key `"state"`, user = caller
|
||||
|
||||
### RPC: `claim_mail_reward`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `inbox.rpc_claim_mail_reward` |
|
||||
| **Auth** | Required |
|
||||
| **Purpose** | Claim rewards attached to a mail |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `mail_id` | string | Yes | Mail ID to claim rewards from |
|
||||
|
||||
**Returns (JSON):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `success` | bool | `true` |
|
||||
| `claimed_ids` | array | Updated list of claimed mail IDs |
|
||||
|
||||
**Reward format (in the mail object):**
|
||||
|
||||
Legacy dict format: `{star=100, gold=50}`
|
||||
Array format: `[{type="star", amount=100}, {type="gold", amount=50}, {type="frag_rare", id="frag_rare", amount=1}, {type="skin", id="skin_gacha_rainbow_suit"}]`
|
||||
|
||||
| Reward `type` | Behavior |
|
||||
|---|---|
|
||||
| `"star"` | Added to wallet |
|
||||
| `"gold"` | Added to wallet |
|
||||
| `"frag_"` prefix | Added to inventory fragments (key: `"fragments"`) |
|
||||
| `"item"` | Added to inventory fragments |
|
||||
| `"skin"` | Added to inventory (key = skin item ID) |
|
||||
|
||||
**Errors:**
|
||||
- `"Not authenticated"`
|
||||
- `"mail_id required"`
|
||||
- `"Reward already claimed"`
|
||||
- `"Mail not found"`
|
||||
|
||||
**Storage writes:** Wallet update, then collection `"inventory"` (fragments or skins), then `"inbox"`/`"state"`.
|
||||
|
||||
### RPC: `delete_mail`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `inbox.rpc_delete_mail` |
|
||||
| **Auth** | Required |
|
||||
| **Purpose** | Soft-delete a mail (adds ID to state.deleted_ids) |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `mail_id` | string | Yes | Mail ID to delete |
|
||||
|
||||
### RPC: `save_mail_state`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `inbox.rpc_save_mail_state` |
|
||||
| **Auth** | Required |
|
||||
| **Purpose** | Mark mails as read |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `read_ids` | array | No | Array of mail IDs to mark as read |
|
||||
|
||||
### RPC: `admin_list_mail`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `inbox.rpc_admin_list_mail` |
|
||||
| **Auth** | Admin required |
|
||||
| **Purpose** | List all mail (global + personal) across all users |
|
||||
|
||||
**Params:** None (ignore payload)
|
||||
|
||||
**Returns (JSON):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `mails` | array | All mails sorted by date descending. Personal mails have extra fields: `type="personal"`, `target_user_id` |
|
||||
|
||||
### RPC: `admin_update_mail`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `inbox.rpc_admin_update_mail` |
|
||||
| **Auth** | Admin required |
|
||||
| **Purpose** | Edit or move a mail between users |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `mail_id` | string | Yes | Mail ID to update |
|
||||
| `type` | string | No | `"global"` or `"personal"` |
|
||||
| `target_user_id` | string | If personal | Current owner (for lookup) |
|
||||
| `new_target_user_id` | string | No | Transfer to a different user |
|
||||
| `title` | string | No | New title |
|
||||
| `content` | string | No | New content |
|
||||
| `end_date` | string | No | New end date |
|
||||
| `expiry_date` | string | No | New expiry date |
|
||||
|
||||
### RPC: `admin_delete_mail_server`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `inbox.rpc_admin_delete_mail_server` |
|
||||
| **Auth** | Admin required |
|
||||
| **Purpose** | Permanently delete mail from server storage |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `mail_id` | string | Yes | Mail ID to delete |
|
||||
| `type` | string | No | `"global"` or `"personal"` |
|
||||
| `target_user_id` | string | If personal | Owner user ID |
|
||||
|
||||
---
|
||||
|
||||
## 8. Daily Rewards Module (`daily_rewards.lua`)
|
||||
|
||||
### Default Reward Schedule
|
||||
|
||||
Days 1–31, each day gives `star` currency: `min(10 + (day-1)*5, 100)`.
|
||||
|
||||
| Day | Reward |
|
||||
|---|---|
|
||||
| 1 | 10 star |
|
||||
| 2 | 15 star |
|
||||
| ... | ... |
|
||||
| 19 | 100 star (capped) |
|
||||
| 20–31 | 100 star each |
|
||||
|
||||
### RPC: `claim_daily_reward`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `daily_rewards.rpc_claim_daily_reward` |
|
||||
| **Auth** | Required |
|
||||
| **Purpose** | Claim today's daily reward |
|
||||
|
||||
**Params:** None (ignore payload)
|
||||
|
||||
**Returns (JSON):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `success` | bool | `true` |
|
||||
| `reward_type` | string | `"star"`, `"gold"`, or a fragment ID (starts with `"frag_"`) |
|
||||
| `reward_amount` | number | Amount rewarded |
|
||||
| `day` | number | Day number (1-indexed) |
|
||||
|
||||
**Errors:**
|
||||
- `"Not authenticated"`
|
||||
- `"Already claimed today"` — `last_claim_date == today`
|
||||
- `"Already claimed all rewards for this month"` — day index >= rewards array length
|
||||
- `"Already claimed today's reward"` — day index already in `claimed_days`
|
||||
|
||||
**Storage reads/writes:**
|
||||
- Read: Collection `"daily_rewards"`, key `"state"` (per-user)
|
||||
- Read: Collection `"config"`, key `"daily_rewards"`, user `SYSTEM_USER_ID` (config)
|
||||
- Write: Collection `"daily_rewards"`, key `"state"` (updates claimed days)
|
||||
- Write (if fragment): Collection `"inventory"`, key `"fragments"`
|
||||
- Wallet update for star/gold rewards
|
||||
|
||||
**Reward types:** `"star"` or `"gold"` → wallet update. `"frag_"` prefix → inventory fragments.
|
||||
|
||||
### RPC: `get_daily_reward_state`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `daily_rewards.rpc_get_daily_reward_state` |
|
||||
| **Auth** | Required |
|
||||
| **Purpose** | Get current daily reward state and month config |
|
||||
|
||||
**Params:** None (ignore payload)
|
||||
|
||||
**Returns (JSON):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `state` | object | `{claimed_days, last_claim_date, month}` |
|
||||
| `month_rewards` | array | Reward config for current month (array of `{type, amount}`) |
|
||||
| `can_claim_today` | bool | Whether user can claim today |
|
||||
| `today_date` | string | Today's UTC date (YYYY-MM-DD) |
|
||||
| `today_index` | number | Today's 0-based index |
|
||||
| `server_month` | number | Current month number |
|
||||
|
||||
### RPC: `set_daily_reward_config` [Admin]
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `daily_rewards.rpc_set_daily_reward_config` |
|
||||
| **Auth** | Admin required |
|
||||
| **Purpose** | Set monthly daily reward configuration |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `config` | object | Yes | Map of month → array of `{type, amount}` rewards. E.g. `{"01": [{type="star", amount=50}, ...]}` |
|
||||
|
||||
### RPC: `get_daily_reward_config_admin` [Admin]
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `daily_rewards.rpc_get_daily_reward_config_admin` |
|
||||
| **Auth** | Admin required |
|
||||
| **Purpose** | Get the current daily reward config |
|
||||
|
||||
---
|
||||
|
||||
## 9. Admin Module (`admin.lua`)
|
||||
|
||||
### RPC: `admin_kick_player`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `admin.rpc_admin_kick_player` |
|
||||
| **Auth** | Admin **or** match host |
|
||||
| **Purpose** | Kick a player from a match |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `match_id` | string | Yes | Match ID |
|
||||
| `user_id` | string | Yes | Player to kick |
|
||||
| `reason` | string | No | `"Kicked by admin"` |
|
||||
|
||||
**Errors:**
|
||||
- `"Admin or host privileges required"`
|
||||
- `"Cannot kick yourself"`
|
||||
- `"Failed to kick player"`
|
||||
|
||||
**Mechanism:** Sends `nk.match_signal` with `{action="kick", user_id, reason}`.
|
||||
|
||||
### RPC: `admin_ban_player`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `admin.rpc_admin_ban_player` |
|
||||
| **Auth** | Admin required |
|
||||
| **Purpose** | Ban a player (sets metadata.banned, kicks from match) |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `user_id` | string | Yes | User to ban |
|
||||
| `reason` | string | No | `"Banned by admin"` |
|
||||
| `duration_hours` | number | No | Hours until ban expires (omitted = permanent) |
|
||||
| `match_id` | string | No | Also kick from this match |
|
||||
|
||||
**Errors:**
|
||||
- `"Admin privileges required"`
|
||||
- `"Cannot ban yourself"`
|
||||
- `"Target account not found"`
|
||||
- `"Cannot ban an admin"`
|
||||
|
||||
**Storage:** Writes ban record to collection `"bans"`, key = `user_id`, user = `SYSTEM_USER_ID`, permission_read=`2`, permission_write=`0`.
|
||||
|
||||
### RPC: `admin_unban_player`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `admin.rpc_admin_unban_player` |
|
||||
| **Auth** | Admin required |
|
||||
| **Purpose** | Remove ban from a player |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required |
|
||||
|---|---|---|
|
||||
| `user_id` | string | Yes |
|
||||
|
||||
### RPC: `admin_get_ban_list`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `admin.rpc_admin_get_ban_list` |
|
||||
| **Auth** | Admin required |
|
||||
| **Purpose** | List all ban records |
|
||||
|
||||
### RPC: `admin_get_server_stats`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `admin.rpc_admin_get_server_stats` |
|
||||
| **Auth** | Admin (or host if `match_id` provided) |
|
||||
| **Purpose** | Get server stats (active matches, players) |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `match_id` | string | No | If set, includes match-specific stats |
|
||||
|
||||
**Returns (JSON):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `active_matches` | number | Count of active matches |
|
||||
| `total_players` | number | Total players in matches |
|
||||
| `server_time` | number | `os.time()` |
|
||||
| `match` | object | (optional) `{id, size, tick_rate, authoritative}` |
|
||||
|
||||
### RPC: `admin_end_match`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `admin.rpc_admin_end_match` |
|
||||
| **Auth** | Admin **or** match host |
|
||||
| **Purpose** | Signal a match to end |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `match_id` | string | Yes | Match ID |
|
||||
| `reason` | string | No | `"Ended by admin"` |
|
||||
|
||||
### RPC: `admin_set_user_role`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `admin.rpc_admin_set_user_role` |
|
||||
| **Auth** | **Owner only** (role must be `"owner"`) |
|
||||
| **Purpose** | Set a user's role |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Valid Values |
|
||||
|---|---|---|---|
|
||||
| `user_id` | string | Yes | Target user |
|
||||
| `role` | string | Yes | `"player"`, `"moderator"`, `"admin"` |
|
||||
|
||||
**Errors:**
|
||||
- `"Only owners can modify user roles"`
|
||||
- `"Invalid role"`
|
||||
|
||||
### RPC: `admin_topup_gold`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `admin.rpc_admin_topup_gold` |
|
||||
| **Auth** | Admin required |
|
||||
| **Purpose** | Add 999,999 gold to own wallet (dev quick-fill) |
|
||||
|
||||
**Wallet update:** `{gold = 999999}`
|
||||
|
||||
### RPC: `admin_clear_global_chat`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `admin.rpc_admin_clear_global_chat` |
|
||||
| **Auth** | Admin required |
|
||||
| **Purpose** | Delete all messages from a chat channel |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `channel_id` | string | Yes | Channel ID or room name |
|
||||
|
||||
**Returns (JSON):** `{success, deleted}`
|
||||
|
||||
### RPC: `admin_list_users`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `admin.rpc_admin_list_users` |
|
||||
| **Auth** | Admin required |
|
||||
| **Purpose** | List all users (limit 500) with role and ban info |
|
||||
|
||||
**Returns (JSON):** `{users: [{user_id, username, display_name, create_time, role, banned, ban_reason}], count}`
|
||||
|
||||
### RPC: `admin_delete_users`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `admin.rpc_admin_delete_users` |
|
||||
| **Auth** | Admin required |
|
||||
| **Purpose** | Permanently delete user accounts |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `user_ids` | array | Yes | Array of user IDs to delete |
|
||||
|
||||
**Errors:**
|
||||
- `"Cannot delete your own account"`
|
||||
- `"Cannot delete admin account"` (prevents deleting admin/moderator/owner)
|
||||
|
||||
**Returns (JSON):** `{success, deleted: [user_ids], failed: [{user_id, reason}]}`
|
||||
|
||||
### RPC: `admin_get_user_detail`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `admin.rpc_admin_get_user_detail` |
|
||||
| **Auth** | Admin required |
|
||||
| **Purpose** | Get comprehensive user details (profile, friends, purchases, storage, wallet ledger) |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|---|---|---|---|---|
|
||||
| `user_id` | string | Yes | — | Target user ID |
|
||||
| `collections` | array | No | `["profiles","stats","inventory","receipts","history","matches","inbox"]` | Storage collections to include |
|
||||
|
||||
**Returns (JSON):** Rich object with `{user, friends, purchases, wallet_ledger, storage, subscription}`.
|
||||
|
||||
### RPC: `admin_update_user_identity`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `admin.rpc_admin_update_user_identity` |
|
||||
| **Auth** | Admin required |
|
||||
| **Purpose** | Update a user's identity fields (username, display_name, timezone, location, lang_tag, avatar_url, metadata) |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required |
|
||||
|---|---|---|
|
||||
| `user_id` | string | Yes |
|
||||
| `username` | string | No |
|
||||
| `display_name` | string | No |
|
||||
| `timezone` | string | No |
|
||||
| `location` | string | No |
|
||||
| `lang_tag` | string | No |
|
||||
| `avatar_url` | string | No |
|
||||
| `metadata` | object | No (merged into existing metadata) |
|
||||
|
||||
### RPC: `admin_set_user_password`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `admin.rpc_admin_set_user_password` |
|
||||
| **Auth** | Admin required |
|
||||
| **Purpose** | Force-set a user's password (user must have email credential) |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required |
|
||||
|---|---|---|
|
||||
| `user_id` | string | Yes |
|
||||
| `password` | string | Yes |
|
||||
|
||||
**Errors:**
|
||||
- `"User has no email credential"`
|
||||
- `"user_id and password are required"`
|
||||
|
||||
### RPC: `admin_get_player_list`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `admin.rpc_admin_get_player_list` |
|
||||
| **Auth** | Admin **or** match host |
|
||||
| **Purpose** | Get player list for a match (stub — returns empty) |
|
||||
|
||||
### RPC: `admin_get_chat_config`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `admin.rpc_admin_get_chat_config` |
|
||||
| **Auth** | Admin required |
|
||||
| **Purpose** | Get lobby chat configuration |
|
||||
|
||||
**Returns (JSON):** `{config: {prefix, max_messages, max_age_days}}`
|
||||
|
||||
### RPC: `admin_set_chat_config`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `admin.rpc_admin_set_chat_config` |
|
||||
| **Auth** | Admin required |
|
||||
| **Purpose** | Set lobby chat configuration |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|---|---|---|---|---|
|
||||
| `prefix` | string | No | `""` | Chat prefix |
|
||||
| `max_messages` | number | No | 50 | Max messages |
|
||||
| `max_age_days` | number | No | 0 | Max message age |
|
||||
|
||||
**Storage:** Collection `"config"`, key `"lobby_chat"`, user `SYSTEM_USER_ID`.
|
||||
|
||||
### RPC: `admin_purge_old_messages`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `admin.rpc_admin_purge_old_messages` |
|
||||
| **Auth** | Admin required |
|
||||
| **Purpose** | Delete chat messages older than specified days |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `channel_id` | string | Yes | Channel ID or room name |
|
||||
| `max_age_days` | number | Yes | Delete messages older than this |
|
||||
|
||||
### RPC: `admin_list_channel_messages`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `admin.rpc_admin_list_channel_messages` |
|
||||
| **Auth** | Admin required |
|
||||
| **Purpose** | List messages in a chat channel |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|---|---|---|---|---|
|
||||
| `channel_id` | string | Yes | — | Channel ID or room name |
|
||||
| `limit` | number | No | 50 | Messages per page |
|
||||
| `cursor` | string | No | `""` | Pagination cursor |
|
||||
| `forward` | bool | No | `true` | Direction |
|
||||
|
||||
### RPC: `admin_delete_channel_message`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Function** | `admin.rpc_admin_delete_channel_message` |
|
||||
| **Auth** | Admin required |
|
||||
| **Purpose** | Delete a specific chat message |
|
||||
|
||||
**Params (JSON):**
|
||||
|
||||
| Field | Type | Required |
|
||||
|---|---|---|
|
||||
| `channel_id` | string | Yes |
|
||||
| `message_id` | string | Yes |
|
||||
|
||||
---
|
||||
|
||||
## 10. Storage Collections Reference
|
||||
|
||||
### Collection: `"shop_config"`
|
||||
|
||||
| Key | User | Permission Read | Permission Write | Schema |
|
||||
|---|---|---|---|---|
|
||||
| `featured_banners` | SYSTEM | 2 (public) | 0 (owner) | `{banners: [{item_id, ...}]}` |
|
||||
|
||||
### Collection: `"receipts"`
|
||||
|
||||
| Key | User | Permission Read | Permission Write | Schema |
|
||||
|---|---|---|---|---|
|
||||
| `<idempotency_key>` | Owner | 1 (owner-read) | 0 (owner) | `{type, package_id/ item_id, status, changeset, receipt, processed_at, ...}` |
|
||||
|
||||
### Collection: `"inventory"`
|
||||
|
||||
| Key | User | Permission Read | Permission Write | Schema |
|
||||
|---|---|---|---|---|
|
||||
| `<item_id>` | Owner | 1 (owner-read) | 0 (owner) | `{category, purchased_at, quantity}` (or `{acquired_via, purchased_at}` for skins) |
|
||||
| `"fragments"` | Owner | 1 (owner-read) | 0 (owner) | `{frag_common: N, frag_uncommon: N, frag_rare: N, ...}` |
|
||||
|
||||
### Collection: `"profiles"`
|
||||
|
||||
| Key | User | Permission Read | Permission Write | Schema |
|
||||
|---|---|---|---|---|
|
||||
| `"pity_counters"` | Owner | 1 (owner-read) | 0 (owner) | `{star: N, gold: N}` (per-banner pity) |
|
||||
| `"fragments"` | Owner | 1 (owner-read) | 0 (owner) | Fragment counts (same as inventory fragments) |
|
||||
| `"profile"` | Owner | 2 (public) | 0 (owner) | Profile data (avatar_url, loadout_character, etc.) |
|
||||
|
||||
### Collection: `"history"`
|
||||
|
||||
| Key | User | Permission Read | Permission Write | Schema |
|
||||
|---|---|---|---|---|
|
||||
| `"logins"` | Owner | 0 (no-read) | 0 (owner) | `{logins: [{time, ip}, ...]}` (max 20) |
|
||||
|
||||
### Collection: `"stats"`
|
||||
|
||||
| Key | User | Permission Read | Permission Write | Schema |
|
||||
|---|---|---|---|---|
|
||||
| `"game_stats"` | Owner | 1 (owner-read) or 2 (public) | 1 (owner-write) or 0 (owner) | `{games_played, games_won, high_score, total_kills, total_deaths, avatar_url, loadout_character}` |
|
||||
| `"stats"` | Owner | Varies | 0 (owner) | Legacy/user-level stats |
|
||||
|
||||
### Collection: `"inbox"`
|
||||
|
||||
| Key | User | Permission Read | Permission Write | Schema |
|
||||
|---|---|---|---|---|
|
||||
| `"personal"` | Owner | 1 (owner-read) | 0 (owner) | `{mails: [{id, title, content, sender, date, start_date, end_date, expiry_date, rewards, type}]}` |
|
||||
| `"state"` | Owner | 1 (owner-read) | 0 (owner) | `{claimed_ids: [string], deleted_ids: [string], read_ids: [string]}` |
|
||||
|
||||
### Collection: `"daily_rewards"`
|
||||
|
||||
| Key | User | Permission Read | Permission Write | Schema |
|
||||
|---|---|---|---|---|
|
||||
| `"state"` | Owner | 1 (owner-read) | 0 (owner) | `{claimed_days: [number], last_claim_date: "YYYY-MM-DD", month: "MM"}` |
|
||||
|
||||
### Collection: `"config"` (system-level)
|
||||
|
||||
| Key | User | Permission Read | Permission Write | Schema |
|
||||
|---|---|---|---|---|
|
||||
| `"daily_rewards"` | SYSTEM | 2 (public) | 0 (owner) | `{"MM": [{type, amount}, ...]}` (month→rewards map) |
|
||||
| `"global_mail"` | SYSTEM | 2 (public) | 0 (owner) | `{mails: [{id, title, content, ...}]}` |
|
||||
| `"lobby_chat"` | SYSTEM | 2 (public) | 0 (owner) | `{prefix, max_messages, max_age_days}` |
|
||||
|
||||
### Collection: `"bans"`
|
||||
|
||||
| Key | User | Permission Read | Permission Write | Schema |
|
||||
|---|---|---|---|---|
|
||||
| `<user_id>` | SYSTEM | 2 (public) | 0 (owner) | `{user_id, username, banned_by, banned_at, reason, expires}` |
|
||||
|
||||
### Collection: `"matches"` (per-user)
|
||||
|
||||
| Key | User | Permission | Schema |
|
||||
|---|---|---|---|
|
||||
| `<match_id>` | Owner | 0 (owner) | Match result data (keyed by match ID, varies) |
|
||||
|
||||
---
|
||||
|
||||
## 11. Leaderboard Config
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| **ID** | `"global_high_score"` |
|
||||
| **Authoritative** | `true` (can only be written server-side) |
|
||||
| **Sort Order** | `"desc"` (highest → lowest) |
|
||||
| **Operator** | `"best"` (keep best score per user) |
|
||||
| **Reset** | `nil` (never auto-resets) |
|
||||
| **Created By** | Both `core.lua` and `leaderboard.lua` on module load |
|
||||
|
||||
---
|
||||
|
||||
## 12. Error Strings Reference
|
||||
|
||||
### Auth errors (raised by utils)
|
||||
|
||||
| Error String | Trigger |
|
||||
|---|---|
|
||||
| `"Admin privileges required"` | Non-admin calls admin RPC |
|
||||
| `"Admin or host privileges required"` | Non-admin, non-host calls admin/host RPC |
|
||||
| `"Not authenticated"` | `context.user_id` is nil |
|
||||
|
||||
### Economy errors
|
||||
|
||||
| Error String | Trigger |
|
||||
|---|---|
|
||||
| `"Package ID required"` | `buy_currency` called without `package_id` |
|
||||
| `"Idempotency key required"` | Missing `idempotency_key` |
|
||||
| `"Invalid package ID"` | Unknown `package_id` |
|
||||
| `"InvalidReceipt"` | IAP receipt validation failed |
|
||||
| `"NotEnoughFunds"` | Insufficient wallet balance for purchase |
|
||||
| `"ItemNotFound"` | Item ID not in shop catalog |
|
||||
| `"Invalid quantity"` | Quantity < 1 |
|
||||
| `"PurchaseFailed"` | Storage write failed during item purchase |
|
||||
| `"Item not found in catalog: <id>"` | Admin tried to banner a non-catalog item |
|
||||
|
||||
### User errors
|
||||
|
||||
| Error String | Trigger |
|
||||
|---|---|
|
||||
| `"Account not found"` | User ID doesn't exist |
|
||||
| `"Account banned until <time>. Reason: <reason>"` | Temporary ban |
|
||||
| `"Account permanently banned. Reason: <reason>"` | Permanent ban |
|
||||
| `"Failed to update profile"` | `nk.account_update_id` failed |
|
||||
| `"Current password required"` | Email change without current password |
|
||||
| `"Incorrect current password."` | Email re-auth failed |
|
||||
| `"Failed to set new credentials: <err>"` | Link email failed |
|
||||
| `"Missing to_user_id or match_id"` | Lobby invite missing fields |
|
||||
| `"Cannot add yourself"` | Friend request to self |
|
||||
| `"user_id is required"` | Missing target in friend request |
|
||||
|
||||
### Gacha errors
|
||||
|
||||
| Error String | Trigger |
|
||||
|---|---|
|
||||
| `"Banner ID required"` | Missing `banner_id` |
|
||||
| `"Invalid count"` | Count < 1 |
|
||||
| `"Unknown banner: <id>"` | Invalid banner |
|
||||
| `"Could not read account"` | Account lookup failed |
|
||||
| `"Insufficient currency"` | Not enough star/gold |
|
||||
| `"Failed to update wallet"` | Wallet update failed |
|
||||
| `"Failed to write storage: <err>"` | Storage write failed |
|
||||
|
||||
### Leaderboard errors
|
||||
|
||||
| Error String | Trigger |
|
||||
|---|---|
|
||||
| `"Failed to submit score"` | Leaderboard write failed |
|
||||
| `"Sync failed: <err>"` | Storage list failed |
|
||||
| `"User ID and stats are required"` | Missing params in admin_update_stats |
|
||||
|
||||
### Inbox errors
|
||||
|
||||
| Error String | Trigger |
|
||||
|---|---|
|
||||
| `"mail_id required"` | Missing mail ID |
|
||||
| `"Reward already claimed"` | Mail already in claimed_ids |
|
||||
| `"Mail not found"` | Mail ID not in any mailbox |
|
||||
| `"Mail not found in global"` | Update/delete: mail not in global |
|
||||
| `"Mail not found in personal inbox"` | Update/delete: mail not in personal |
|
||||
| `"target_user_id required for personal mail"` | Missing owner for personal mail operations |
|
||||
|
||||
### Daily reward errors
|
||||
|
||||
| Error String | Trigger |
|
||||
|---|---|
|
||||
| `"Already claimed today"` | `last_claim_date == today` |
|
||||
| `"Already claimed all rewards for this month"` | Day index >= reward array length |
|
||||
| `"Already claimed today's reward"` | Day index already in `claimed_days` |
|
||||
|
||||
### Admin errors
|
||||
|
||||
| Error String | Trigger |
|
||||
|---|---|
|
||||
| `"Cannot kick yourself"` | Self-kick attempt |
|
||||
| `"Failed to kick player"` | Match signal failed |
|
||||
| `"Cannot ban yourself"` | Self-ban attempt |
|
||||
| `"Target account not found"` | Account lookup failed |
|
||||
| `"Cannot ban an admin"` | Target is admin/moderator/owner |
|
||||
| `"Only owners can modify user roles"` | Non-owner tries role change |
|
||||
| `"Invalid role"` | Role not in `{player, moderator, admin}` |
|
||||
| `"channel_id is required"` | Missing channel ID for chat operations |
|
||||
| `"channel_id and message_id are required"` | Missing chat message params |
|
||||
| `"Failed to delete message: <err>"` | Channel message remove failed |
|
||||
| `"Failed to list messages: <err>"` | Channel list failed |
|
||||
| `"max_age_days must be > 0"` | Invalid purge param |
|
||||
| `"Match not found"` | Match lookup failed |
|
||||
| `"User has no email credential"` | Cannot set password for non-email user |
|
||||
| `"Cannot delete your own account"` | Self-deletion attempt |
|
||||
| `"Cannot delete admin account"` | Deleting admin/moderator/owner |
|
||||
| `"No user IDs provided"` | Empty `user_ids` array |
|
||||
|
||||
---
|
||||
|
||||
## 13. Permission Levels
|
||||
|
||||
### Role Hierarchy
|
||||
|
||||
| Role | Can Call Admin RPCs | Can Modify Roles | Notes |
|
||||
|---|---|---|---|
|
||||
| `"player"` | No | No | Default role |
|
||||
| `"moderator"` | Yes | No | Same as admin for auth checks |
|
||||
| `"admin"` | Yes | No | Standard admin |
|
||||
| `"owner"` | Yes | Yes (only owner) | Can set `player`, `moderator`, `admin` roles |
|
||||
|
||||
### Auth Check Functions Summary
|
||||
|
||||
| Check | What It Allows |
|
||||
|---|---|
|
||||
| `utils.require_admin` | Any RPC gated by this. Roles: admin, moderator, owner |
|
||||
| `utils.require_admin_or_host` | Admin/moderator/owner, OR match host (state.hostUserId) |
|
||||
| `metadata.role == "owner"` | Only `admin_set_user_role` |
|
||||
|
||||
### Storage Permission Bits
|
||||
|
||||
| Value | Meaning |
|
||||
|---|---|
|
||||
| `0` | Owner only (no one can read/write except server) |
|
||||
| `1` | Owner read, server write |
|
||||
| `2` | Public read, server write |
|
||||
|
||||
### System User ID
|
||||
|
||||
```
|
||||
00000000-0000-0000-0000-000000000000
|
||||
```
|
||||
|
||||
Used for global/shared storage collections (bans, config, global_mail, shop_config).
|
||||
|
||||
---
|
||||
|
||||
## RPC Summary Table
|
||||
|
||||
| RPC Name | Module | Auth | Admin? |
|
||||
|---|---|---|---|
|
||||
| `get_user_profile` | user | None | No |
|
||||
| `update_user_profile` | user | Required | No |
|
||||
| `search_users` | user | Required | No |
|
||||
| `change_credentials` | user | Required | No |
|
||||
| `send_lobby_invite` | user | Required | No |
|
||||
| `send_friend_request` | user | Required | No |
|
||||
| `admin_get_user_history` | user | Required | Yes |
|
||||
| `get_shop_catalog` | economy | Required | No |
|
||||
| `buy_currency` | economy | Required | No |
|
||||
| `purchase_item` | economy | Required | No |
|
||||
| `admin_set_featured_banners` | economy | Required | Yes |
|
||||
| `admin_get_featured_banners` | economy | Required | Yes |
|
||||
| `perform_gacha_pull` | gacha | Required | No |
|
||||
| `get_leaderboard_stats` | leaderboard | None | No |
|
||||
| `submit_score` | leaderboard | Required | No |
|
||||
| `sync_leaderboard` | leaderboard | Required | No |
|
||||
| `reset_stats` | leaderboard | Required | No |
|
||||
| `admin_update_stats` | leaderboard | Required | Yes |
|
||||
| `admin_delete_stats` | leaderboard | Required | Yes |
|
||||
| `admin_sync_leaderboard` | leaderboard | Required | Yes |
|
||||
| `admin_send_mail` | inbox | Required | Yes |
|
||||
| `get_mail` | inbox | Required | No |
|
||||
| `claim_mail_reward` | inbox | Required | No |
|
||||
| `delete_mail` | inbox | Required | No |
|
||||
| `save_mail_state` | inbox | Required | No |
|
||||
| `admin_list_mail` | inbox | Required | Yes |
|
||||
| `admin_update_mail` | inbox | Required | Yes |
|
||||
| `admin_delete_mail_server` | inbox | Required | Yes |
|
||||
| `claim_daily_reward` | daily_rewards | Required | No |
|
||||
| `get_daily_reward_state` | daily_rewards | Required | No |
|
||||
| `set_daily_reward_config` | daily_rewards | Required | Yes |
|
||||
| `get_daily_reward_config_admin` | daily_rewards | Required | Yes |
|
||||
| `admin_kick_player` | admin | Required | Admin or host |
|
||||
| `admin_ban_player` | admin | Required | Yes |
|
||||
| `admin_unban_player` | admin | Required | Yes |
|
||||
| `admin_get_ban_list` | admin | Required | Yes |
|
||||
| `admin_get_server_stats` | admin | Required | Admin or host |
|
||||
| `admin_end_match` | admin | Required | Admin or host |
|
||||
| `admin_set_user_role` | admin | Required | Owner only |
|
||||
| `admin_topup_gold` | admin | Required | Yes |
|
||||
| `admin_clear_global_chat` | admin | Required | Yes |
|
||||
| `admin_list_users` | admin | Required | Yes |
|
||||
| `admin_delete_users` | admin | Required | Yes |
|
||||
| `admin_get_user_detail` | admin | Required | Yes |
|
||||
| `admin_update_user_identity` | admin | Required | Yes |
|
||||
| `admin_set_user_password` | admin | Required | Yes |
|
||||
| `admin_get_player_list` | admin | Required | Admin or host |
|
||||
| `admin_get_chat_config` | admin | Required | Yes |
|
||||
| `admin_set_chat_config` | admin | Required | Yes |
|
||||
| `admin_purge_old_messages` | admin | Required | Yes |
|
||||
| `admin_list_channel_messages` | admin | Required | Yes |
|
||||
| `admin_delete_channel_message` | admin | Required | Yes |
|
||||
|
||||
## Wallet Update Summary
|
||||
|
||||
| Operation | Changeset | Context |
|
||||
|---|---|---|
|
||||
| IAP: gold packages (verified) | `{gold: +N}` | `buy_currency` |
|
||||
| IAP: star packages | `{star: +N, gold: -N}` | `buy_currency` (costs gold) |
|
||||
| Purchase item | `{gold: -N}` or `{star: -N}` | `purchase_item` |
|
||||
| Gacha pull | `{<currency>: -cost}` | `perform_gacha_pull` |
|
||||
| Claim mail rewards | `{star: +N}` and/or `{gold: +N}` | `claim_mail_reward` |
|
||||
| Daily reward | `{star: +N}` or `{gold: +N}` | `claim_daily_reward` |
|
||||
| Admin gold top-up | `{gold: 999999}` | `admin_topup_gold` |
|
||||
|
||||
All wallet updates use `nk.wallet_update(user_id, changeset, {}, true)` (third arg `true` = update only, no metadata).
|
||||
Reference in New Issue
Block a user