Files
tekton/wiki/architecture_server.md
T
god 114748a54f experimental: remove Tekton Doors entirely
- Delete portal_mode_manager.gd, portal_door.gd, portal_door.tscn
- Strip all Tekton Doors logic from main.gd, player.gd, lobby.gd,
  lobby_room.gd, lobby_manager.gd, camera_context_manager.gd,
  music_manager.gd, tekton.gd, enhanced_gridmap.gd,
  playerboard_manager.gd, special_tiles_manager.gd
- Remove TK enum (TEKTON_DOORS=2), mode_config schema, arena area
- Update tests: 3 modes instead of 4
- Strip HowToPlay tab from main.tscn
2026-07-06 00:18:59 +08:00

393 lines
14 KiB
Markdown

# Tekton Armageddon - Server Architecture
<a id="top"></a>
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)
---
## System Overview
```mermaid
flowchart LR
Client[Godot Client<br/>player.tscn] -->|RPC calls| Nakama[Nakama Server<br/>port 7350/7351]
Nakama -->|wallet_update| Wallet[Wallet Engine]
Nakama -->|storage_read/write| Storage[Nakama Storage DB<br/>PostgreSQL]
Nakama -->|leaderboard_*| LB[Native Leaderboard]
Nakama -->|match_*| Match[Match Handler]
Nakama -->|channel_*| Chat[Lobby Chat]
Client -->|nk.match_join| Match
Client -->|Direct RPC| Lua[Lua RPC Handlers<br/>server/nakama/lua/]
```
The Lua backend runs **inside** the Nakama process. Lua modules hook into Nakama's lifecycle (after-authentication, RPC dispatch, match signals). All game transactions (wallet, inventory, gacha, shop) are **server-authoritative** — the Godot client sends RPC requests and the Lua code validates and executes.
[Back to top](#top)
---
## Module Architecture
```mermaid
flowchart TD
main["main.lua<br/>Entry point: loads all modules"] --> utils["utils.lua<br/>Auth guards, channel resolver"]
main --> core["core.lua<br/>Authentication hooks<br/>Wallet init, ban check"]
main --> economy["economy.lua<br/>Shop catalog, currency IAP<br/>Item purchases"]
main --> gacha["gacha.lua<br/>Gacha pulls, pity system<br/>RNG on server"]
main --> leaderboard["leaderboard.lua<br/>Score submission, sync<br/>Global rankings"]
main --> inbox["inbox.lua<br/>Global/personal mail<br/>Reward claiming"]
main --> daily["daily_rewards.lua<br/>Daily login rewards<br/>Monthly schedule"]
main --> user["user.lua<br/>Profile updates, identity<br/>Friend sync"]
main --> admin["admin.lua<br/>Kick/ban, stats, chat mgmt<br/>Role management"]
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)
---
## Authentication Flow
```mermaid
sequenceDiagram
participant C as Godot Client
participant N as Nakama Core
participant A as after_hooks (core.lua)
participant DB as Nakama Storage
C->>N: AuthenticateDevice/Custom/Email
N->>A: after_authenticate(context)
A->>DB: storage_read(profiles/profile)
alt First login (no profile)
A->>DB: storage_write(initial profile)
A->>N: wallet_update(gold=100, star=500)
end
A->>DB: storage_read(profiles/profile)
alt metadata.banned == true
A-->>C: error("Account banned")
end
A->>C: session token returned
```
### Boot sequence per player
1. Godot client calls `AuthenticateDevice` (or Email/Custom/Steam).
2. Nakama core calls `after_authenticate()` hook in `core.lua`.
3. Hook reads `profiles/profile` from storage.
4. **If first login:** initializes default profile and grants starting wallet (`gold: 100, star: 500`).
5. **Ban check:** If `metadata.banned == true`, raises error (player rejected).
6. Client receives session token and proceeds to lobby.
[Back to top](#top)
---
## Wallet & Economy Flow
```mermaid
flowchart LR
subgraph Client Side
ShopUI[Shop Panel] -->|buy_currency RPC| Server
ShopUI -->|purchase_item RPC| Server
GachaUI[Gacha Panel] -->|perform_gacha_pull RPC| Server
MailUI[Inbox] -->|claim_mail_reward RPC| Server
DailyUI[Daily Rewards] -->|claim_daily_reward RPC| Server
end
subgraph Server Side
Server[Lua RPC Handler]
Server -->|nk.wallet_update| Wallet[(Wallet)]
Server -->|nk.storage_write| Inv[(Inventory Storage)]
Server -->|nk.storage_write| Rec[(Receipts Storage)]
Server -->|nk.storage_write| Frag[(Fragments Storage)]
end
subgraph Client Refresh
Wallet -->|Wallet updated| Client
Client -->|get_account RPC| Wallet
Client -->|emit profile_updated| UI[All UI Panels<br/>update labels]
end
```
### Currency types
| Currency | Purpose | Earned by |
|---|---|---|
| `gold` | Shop purchases, star conversion | IAP (real money), admin topup |
| `star` | Gacha pulls | Gold conversion, daily rewards, mail rewards |
### All wallet changesets
| Operation | Changeset |
|---|---|
| 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)
---
## Gacha Flow
```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
```
### Drop rates
| Rarity | Rate | Result |
|---|---|---|
| Common | 60% | Fragment (`frag_common`) |
| Uncommon | 25% | Fragment (`frag_uncommon`) |
| Rare | 14% | Fragment (`frag_rare`) |
| Real Prize | 1% | Skin from catalog |
**Pity:** Guaranteed Real Prize at 90 pulls. Pity counter resets on any Real Prize win.
[Back to top](#top)
---
## Mail/Inbox System
```mermaid
flowchart TD
Admin[Admin RPC] -->|admin_send_mail| Global[Global Mail<br/>config/global_mail<br/>system user]
Admin -->|admin_send_mail<br/>with target_user_id| Personal[Personal Mail<br/>inbox/personal<br/>per user]
Client -->|get_mail| Merge[Merge global + personal]
Merge --> Filter[Filter by: not deleted,<br/>within date range,<br/>not expired]
Filter --> Response[Return to client]
Client -->|claim_mail_reward| CW{Check claimed_ids}
CW -->|Already claimed| Err[error: Reward already claimed]
CW -->|Not claimed| Grant[Grant rewards:<br/>gold/star -> wallet<br/>fragments -> fragment storage<br/>skins -> inventory storage]
Grant --> UpdateState[Update state:<br/>add mailId to claimed_ids]
```[Global Mail<br/>config/global_mail<br/>system user]
Admin -->|admin_send_mail<br/>with target_user_id| Personal[Personal Mail<br/>inbox/personal<br/>per user]
Client -->|get_mail| Merge[Merge global + personal]
Merge --> Filter[Filter by: not deleted,<br/>within date range,<br/>not expired]
Filter --> Response[Return to client]
Client -->|claim_mail_reward| CW[Check claimed_ids]
CW -->|Already claimed| Err[error: Reward already claimed]
CW -->|Not claimed| Grant[Grant rewards:<br/>gold/star -> wallet<br/>fragments -> fragment storage<br/>skins -> inventory storage]
Grant --> UpdateState[Update state:<br/>add mailId to claimed_ids]
```
[Back to top](#top)
---
## Client-Server Data Flow
```mermaid
sequenceDiagram
participant C as Godot Client
participant L as Lua RPC
participant S as Nakama Storage
participant W as Wallet
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
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
```
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)
---
## Admin Hierarchy
| Role | Can | Guarded by |
|---|---|---|
| `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"` |
### Guard functions (utils.lua)
```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
```
[Back to top](#top)
---
## Storage Collections
| Collection | Owner | Key | Public R | Public W | Purpose |
|---|---|---|---|---|---|
| `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)
---
## vs Nakama-Server-API
| Aspect | Architecture-Server (this page) | Nakama-Server-API |
|---|---|---|
| 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)
---
## Quick Reference: All 48 Registered RPCs
| RPC Name | Module | Auth | Purpose |
|---|---|---|---|
| `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)
---
## Deployment Topology
```mermaid
flowchart TD
subgraph VPS [VPS 52.74.133.55]
Gitea[Gitea Server<br/>port 3000]
Nakama[Nakama Server<br/>port 7350/7351]
PG[(PostgreSQL<br/>Nakama DB)]
Act[act_runner<br/>CI/CD Worker]
end
Client[Godot Player] -->|HTTPS| Gitea
Client -->|gRPC/WebSocket| Nakama
Nakama --> PG
GitHub_mirror[GitHub Mirror] -->|Push| Gitea
Gitea -->|Webhook/Manual| Act
Act -->|Build| Binary[Binary Releases]
Act -->|Build| Patch[patch.pck on patches branch]
Client -->|Check version| Gitea
Client -->|Download patch| Gitea
```
[Back to top](#top)