Add persistent cross-game player bank (Refs #146)

This commit is contained in:
2026-10-12 20:21:58 -04:00
parent bcd3b99ee4
commit e071e5b574
14 changed files with 866 additions and 1 deletions
+145
View File
@@ -0,0 +1,145 @@
# Player bank
The bank stores one points balance per exact GUID across T5 and T6. Players use
`.b`, `.d <amount|all>` and `.w <amount|all>` with the TWS bank scripts. Deposits
move spendable match points into the bank; withdrawals return bank points to
the current match. Neither changes earned `score_total`. Bank-assisted games
still count toward the existing leaderboards; the ledger records their match
IDs. This release does not introduce separate bank-assisted rankings.
## Setup and rollout
1. Back up the controller database. Deploy Electric Cherry first and restart
the controller. Migration 20 creates accounts, transfer requests and a ledger.
2. Generate a **different** secret for each registered game server, for example
`ec_bank_` followed by `openssl rand -hex 32`. Configure `EC_BANK_TOKENS` as a
JSON object mapping exact server names to these secrets. Docker Compose
forwards this environment variable. An example shape with placeholders is
`{"t5_zombie_theater_1":"ec_bank_<random-secret>"}`.
3. Set `EC_BANK_MAX_BALANCE` (default 250000, range 1–1000000). Invalid configuration
refuses new deposits. A limit reduction does not confiscate existing balances
or invalidate already prepared deposits; withdrawals remain available.
4. Build/install TWS plugins `t5-plugin`/`t6-plugin` 1.1.0 or later and restart
the **whole game-server process**. Install `tws_t5_bank.gsc` or
`tws_t6_bank.gsc` 1.0.0, the game's common module (1.0.0+) and telemetry
reporter (1.0.0+). Restart the map after installing scripts/configuration.
5. In each server's private configuration, set `tws_server_id` to its registered
name and `tws_bank_token` to that server's secret. The optional `tws_bank_url`
defaults to `https://ec.thewrightserver.net/api/bank`; overrides must use
HTTPS. Keep these settings out of shared scripts and public config downloads.
Bank tokens must match `ec_bank_` followed by 32–128 URL-safe letters, digits,
underscores or hyphens. Missing, malformed, duplicate, or telemetry-reused tokens
disable authentication. `EC_TELEMETRY_TOKEN`, host tokens and browser sessions
cannot authorize bank requests. The credential determines the server, and a
different `server` in the body is refused. Rotate a token by updating both the
controller mapping and that server's private configuration, then restart them.
Keep the same server name and bank URL so existing decisions can finish.
## Machine API
All routes below are `POST /api/bank/<action>`, with `Content-Type:
application/json` and `Authorization: Bearer <server-bank-token>`. Responses
are never cached. Bodies are capped at 4096 bytes. GUIDs and request IDs stay
strings; never convert them to numbers.
| Action | Body | Meaning |
| ---------- | ------------------------- | -------------------------------------------------------------------- |
| `balance` | `server`, `guid` | Read the shared balance and currently available points |
| `deposit` | Transfer fields below | Reserve deposit capacity without crediting the account yet |
| `withdraw` | Transfer fields below | Reserve bank funds without debiting the account yet |
| `commit` | Identical transfer fields | Apply a prepared transfer once and append its ledger entry |
| `cancel` | Identical transfer fields | Release a reservation; also prevents a delayed prepare from applying |
Transfer example:
```json
{
"server": "t5_zombie_theater_1",
"guid": "18446744073709551615",
"match": "reporter-match-key",
"request_id": "unique-process-token-17",
"operation": "deposit",
"amount": 2000,
"capacity": 995000
}
```
`match` is the reporter's telemetry match key, **not** Electric Cherry's numeric
match ID. `request_id` is unique for the server and at most 64 characters.
`amount` is a positive integer up to 1000000; withdrawal also accepts `"all"`.
The script resolves deposit `all` to its captured spendable points. `capacity`
is the current room below the game's bank withdrawal cap of 1000000 points.
An `all` withdrawal that exceeds this room is refused, rather than truncated.
Preparation requires the GUID to be present in the server's latest active match,
with the exact telemetry match key. Late joiners may need to wait for the next
30-second heartbeat. Unknown/destroyed servers, absent players and old matches
cannot prepare transfers. Settlement of an existing decision remains possible
after match completion, retention or server deletion.
Successful application responses include `ok`, `state`, `code`, `request_id`,
`amount` and `balance`. States are `balance`, `prepared`, `committed`, `cancelled`
or `rejected`. For balance reads, `amount` is the available balance. A business
rejection uses HTTP 200 with `ok: false`, `state: "rejected"` and a code such as
`insufficient_balance`, `empty`, `bank_limit`, `points_limit`, `not_in_match` or
`pending`. It never changes points. Authentication failures use 401/403,
malformed input 400/413/415, identity conflicts 409, and unavailable service 503.
Repeated requests return the saved result/state without applying a second
change. Settled IDs never reopen; a committed result retains its original
resulting balance even if later transfers change the account. Reusing an ID
with different transfer fields returns 409. A GUID has at most one prepared
transfer across all servers. SQLite transactions serialize checks, account
updates and ledger insertion, so concurrent requests cannot overspend funds or
deposit past the configured maximum.
## Lost responses and recovery
The game saves a cancellation decision **before** sending a prepare request.
Only a validated prepared reply lets it recheck the player's life, current
spendable points and withdrawal room. It then records a commit decision and
updates spendable points without yielding. The sender retries settlement with
the original identity, at delays of 1, 2, 4, 8, 16, then 30 seconds. Invalid,
truncated or uncorrelated replies never authorize points changes.
A failed/uncertain prepare leaves game points unchanged and queues cancellation.
Cancellation can arrive first: its tombstone blocks the delayed prepare. Death,
respawn, disconnect or match end before application also cancels. After a commit
decision, retries only finish the ledger; they never change game points again.
These points now belong to the current match, so an applied withdrawal is not
automatically refunded if that match ends or its server crashes.
Keep `fs_homepath/tws-bank/t5/` or `t6/` on persistent writable storage with a
single process per server/endpoint. Journals contain transfer data, not secrets.
On a map/process restart, saved decisions settle before new bank commands.
Unreadable storage disables the bank; failed writes prevent points changes.
Pending decisions remain until acknowledged, even on credential errors. Bank
holds do not expire automatically, because expiring a saved commit can lose a
deposit or create a free withdrawal. Restore connectivity/configuration and the
original journal to resolve holds; never delete a pending journal to unblock
the bank. If storage is permanently lost, reconcile against the retained
request and ledger records before any manual repair.
The journal provides recovery from map/process restart. It cannot make live
game memory and SQLite one atomic transaction or preserve match points after a
machine/storage failure. Back up persistent storage and verify crash behavior
in the actual Windows/Plutonium deployment before rollout.
## History and validation
Player profiles show the shared balance, pending status and 20 newest ledger
entries, including signed amounts, resulting balances, server, time and match.
Accounts, request IDs and ledger entries have no cascading match/server foreign
keys and are not deleted by match retention or server deletion. Retained IDs
also keep old retries idempotent. Expired match links become plain historical
IDs. Include these tables in normal database backups.
Run `npx vitest run tests/unit/bank.test.ts`, then the usual lint, type checks,
tests and build. TWS supplies `node tools/check_bank.mjs` and
`python3 tools/test_plugin_bank.py`. Live validation must cover both games:
deposit/withdraw amounts and `all`, insufficient funds and caps, joining before
telemetry, `.help`, score HUD updates, death/respawn/disconnect during requests,
lost prepare/commit replies, controller outage, disk failures and map/process
restart with pending work. Confirm earned score and leaderboard behavior, and
verify that another server's credential and the telemetry credential fail.