Use public endpoint for game bank requests (Refs #13)
This commit is contained in:
@@ -6,6 +6,36 @@ T5/T6 script and each native plugin. Current versions are recorded in
|
|||||||
|
|
||||||
## 2026-10-12
|
## 2026-10-12
|
||||||
|
|
||||||
|
### Public game-bank endpoint — TWS #13
|
||||||
|
|
||||||
|
| Component | Version | Affected source |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `tws_t5_bank.gsc` | `2.0.0` | `t5/scripts/tws_t5_bank.gsc` |
|
||||||
|
| `tws_t6_bank.gsc` | `2.0.0` | `t6/scripts/tws_t6_bank.gsc` |
|
||||||
|
|
||||||
|
- Use `https://ec.thewrightserver.net/api/bank` for all balance, transfer and
|
||||||
|
settlement requests on both engines. Remove the `tws_bank_url` override;
|
||||||
|
stale settings are ignored. This configuration removal requires a major bump.
|
||||||
|
Keep EC's `tws_server_id` and private per-instance `tws_bank_token` authentication.
|
||||||
|
- Both T5 and T6 receive the same behavior with their existing namespaces and
|
||||||
|
startup guards. Native plugins and other scripts need no source changes.
|
||||||
|
- Compatibility: requires companion EC agent `1.9.1+` to recognize public and
|
||||||
|
legacy controller-derived journals during authenticated teardown settlement.
|
||||||
|
Existing plugin `1.1.0`, common `1.0.0+` and telemetry `1.0.0+` remain sufficient;
|
||||||
|
no new native API or third-party dependency requirement.
|
||||||
|
- Deployment: update agents first. When changing endpoints, **rebuild through
|
||||||
|
EC** so the old process stops and its pending decisions settle before a new
|
||||||
|
process starts. Do not switch scripts with just a map restart in that case;
|
||||||
|
journals are endpoint-scoped. If the old URL already matches exactly, copy the
|
||||||
|
scripts and restart the map; no full server-process restart is required.
|
||||||
|
See [endpoint upgrade and recovery](README.md#upgrading-the-bank-endpoint).
|
||||||
|
No deployment was performed; agent release signing, Windows/GSC compilation
|
||||||
|
and live T5/T6 validation remain required.
|
||||||
|
- Checks: portable script fixtures exercise real initialization with stale
|
||||||
|
overrides, every request type, authentication, empty balances and retries.
|
||||||
|
Native fixtures cover zero-balance and insufficient-balance replies alongside
|
||||||
|
existing durable storage and process-restart checks.
|
||||||
|
|
||||||
### Persistent cross-game player bank — TWS #3 / Electric Cherry #146
|
### Persistent cross-game player bank — TWS #3 / Electric Cherry #146
|
||||||
|
|
||||||
| Component | Version | Affected source |
|
| Component | Version | Affected source |
|
||||||
|
|||||||
@@ -190,11 +190,15 @@ requirements; disabling it by dvar does not avoid linking.
|
|||||||
|
|
||||||
Electric Cherry generates a unique bank token when it prepares a native server
|
Electric Cherry generates a unique bank token when it prepares a native server
|
||||||
instance and injects it through that instance's private
|
instance and injects it through that instance's private
|
||||||
`electric_cherry_bank.cfg`. It sets `tws_server_id`, `tws_bank_token` and
|
`electric_cherry_bank.cfg`, with `tws_server_id` supplied by the launch configuration.
|
||||||
`tws_bank_url` automatically. Rebuilds and host moves keep the instance token;
|
Both bank scripts `2.0.0` always use `https://ec.thewrightserver.net/api/bank`;
|
||||||
|
`tws_bank_url` is no longer read, even if an old configuration still sets it.
|
||||||
|
The host agent's enrolled controller address is independent and continues to
|
||||||
|
serve agent traffic. Agent `1.9.1+` writes only the private `tws_bank_token` in
|
||||||
|
the bank configuration. Rebuilds and host moves keep the instance token;
|
||||||
destroying the instance revokes it. Reusing a server name gets a new token.
|
destroying the instance revokes it. Reusing a server name gets a new token.
|
||||||
There is no `EC_BANK_TOKENS` map or per-server secret to maintain. Keep secrets
|
There is no `EC_BANK_TOKENS` map or per-server secret to maintain. Keep secrets
|
||||||
outside shared scripts and promoted configs. The URL must use HTTPS.
|
outside shared scripts and promoted configs.
|
||||||
Set `tws_bank_enabled 0` to disable the module; restart maps after script/config changes.
|
Set `tws_bank_enabled 0` to disable the module; restart maps after script/config changes.
|
||||||
Electric Cherry's `EC_BANK_MAX_BALANCE` defaults to 250000 (range 1–1000000).
|
Electric Cherry's `EC_BANK_MAX_BALANCE` defaults to 250000 (range 1–1000000).
|
||||||
Each transfer and bank-funded spendable balance is capped at 1000000 points.
|
Each transfer and bank-funded spendable balance is capped at 1000000 points.
|
||||||
@@ -209,7 +213,7 @@ have only one prepared transfer across the fleet.
|
|||||||
|
|
||||||
Preserve `tws-bank/t5/` and `tws-bank/t6/`, relative to the game process working
|
Preserve `tws-bank/t5/` and `tws-bank/t6/`, relative to the game process working
|
||||||
directory, across process/container restarts, with one process per server/endpoint
|
directory, across process/container restarts, with one process per server/endpoint
|
||||||
directory. EC host agents **1.9.0+** settle
|
directory. EC host agents **1.9.1+** settle
|
||||||
pending journals before deleting runtime storage on removal, rebuild, migration
|
pending journals before deleting runtime storage on removal, rebuild, migration
|
||||||
or launch cancellation. Update the agents before enabling the bank. An outage
|
or launch cancellation. Update the agents before enabling the bank. An outage
|
||||||
or invalid journal blocks cleanup and preserves the files. Decisions are scoped
|
or invalid journal blocks cleanup and preserves the files. Decisions are scoped
|
||||||
@@ -222,11 +226,41 @@ are not refunded when a match ends or its process crashes. As with any live
|
|||||||
score, the journal cannot make game memory and the remote database atomic
|
score, the journal cannot make game memory and the remote database atomic
|
||||||
across a machine/storage failure.
|
across a machine/storage failure.
|
||||||
|
|
||||||
|
### Upgrading the bank endpoint
|
||||||
|
|
||||||
|
Deploy the Electric Cherry agent `1.9.1+` companion to
|
||||||
|
[TWS #13](https://gitea.thewrightserver.net/josh/tws-zombies-scripts/issues/13)
|
||||||
|
before installing bank scripts `2.0.0`. It accepts journals for the public game
|
||||||
|
endpoint and the legacy bank URL derived from that host's enrolled controller.
|
||||||
|
It settles through the existing authenticated, instance-scoped agent API at the
|
||||||
|
controller; it never follows journal URLs and rejects other endpoint identities.
|
||||||
|
The instance token and server identity must stay the same through the upgrade.
|
||||||
|
|
||||||
|
**If the old `tws_bank_url` differs from the public endpoint, rebuild the server
|
||||||
|
through EC with agent `1.9.1+` and the new scripts.** The agent stops the old
|
||||||
|
process and settles its original commit/cancel decisions before starting the
|
||||||
|
replacement and deleting old storage. A lost reply replays the same request ID;
|
||||||
|
EC deduplicates it. An outage, corrupt journal or failed acknowledgment retains
|
||||||
|
storage and blocks the replacement. Resolve the failure and retry the rebuild.
|
||||||
|
Keep the enrolled controller address unchanged until legacy decisions drain.
|
||||||
|
Do not merely replace scripts and restart the map: native journals are keyed by
|
||||||
|
server and endpoint, so the new scripts cannot see a legacy endpoint's pending
|
||||||
|
decisions. Do not rename, rewrite or delete those journals to force migration.
|
||||||
|
|
||||||
|
If the previous endpoint already was the exact public URL, install the scripts
|
||||||
|
and restart the map; saved decisions replay under the same journal identity.
|
||||||
|
No new plugin build or full process restart is required for that case. Other
|
||||||
|
legacy endpoints are refused by the agent and require recovery with their
|
||||||
|
original configuration before switching. No deployment has been performed here.
|
||||||
|
|
||||||
Run `node tools/check_bank.mjs` and `python3 tools/test_plugin_bank.py`.
|
Run `node tools/check_bank.mjs` and `python3 tools/test_plugin_bank.py`.
|
||||||
Before rollout, build/link Windows DLLs, compile GSC, and verify both games:
|
Before rollout, build/link Windows DLLs, compile GSC, and verify both games:
|
||||||
help, balance and `all` commands, score HUD/persistence, max/overdraw errors,
|
help, balance and `all` commands, score HUD/persistence, max/overdraw errors,
|
||||||
death/respawn/disconnect during HTTP, late telemetry, lost prepare/commit
|
death/respawn/disconnect during HTTP, late telemetry, lost prepare/commit
|
||||||
responses, disk failures and map/process restart with pending decisions.
|
responses, disk failures and map/process restart with pending decisions.
|
||||||
|
For this upgrade also verify stale URL settings are ignored, an empty `.b`
|
||||||
|
shows zero, `.w all` reports insufficient balance, and an EC rebuild/removal
|
||||||
|
settles legacy and public journals without duplicate ledger entries or lost points.
|
||||||
|
|
||||||
## Map record announcements
|
## Map record announcements
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,12 @@ The optional `tws_t5_bank.gsc` adds `.b`, `.d <amount|all>` and
|
|||||||
`.w <amount|all>` and registers them in help. It requires common and telemetry
|
`.w <amount|all>` and registers them in help. It requires common and telemetry
|
||||||
`1.0.0` or newer, the TWS t5 plugin `1.1.0`, Electric Cherry's bank API and
|
`1.0.0` or newer, the TWS t5 plugin `1.1.0`, Electric Cherry's bank API and
|
||||||
a private per-server credential. Follow [bank setup and recovery](../../README.md#player-bank-t5-and-t6),
|
a private per-server credential. Follow [bank setup and recovery](../../README.md#player-bank-t5-and-t6),
|
||||||
including a full process restart for the plugin and a map restart for scripts.
|
including a full process restart for the plugin. Bank `2.0.0` uses only
|
||||||
|
`https://ec.thewrightserver.net/api/bank` and ignores `tws_bank_url`.
|
||||||
|
Update EC agents to `1.9.1+` first. If the old endpoint differs, follow the
|
||||||
|
[endpoint upgrade procedure](../../README.md#upgrading-the-bank-endpoint):
|
||||||
|
an EC rebuild must settle legacy journals before the new scripts start.
|
||||||
|
When the endpoint already matches, only a map restart is required.
|
||||||
|
|
||||||
## Persistent Discord text
|
## Persistent Discord text
|
||||||
|
|
||||||
|
|||||||
@@ -6,17 +6,15 @@ init()
|
|||||||
{
|
{
|
||||||
if (GetDvar(#"zombiemode") != "1" || GetDvar("tws_bank_enabled") == "0")
|
if (GetDvar(#"zombiemode") != "1" || GetDvar("tws_bank_enabled") == "0")
|
||||||
return;
|
return;
|
||||||
level.bank_url = GetDvar("tws_bank_url");
|
// Game containers use the public API, independent of the host controller URL.
|
||||||
if (level.bank_url == "")
|
|
||||||
level.bank_url = "https://ec.thewrightserver.net/api/bank";
|
level.bank_url = "https://ec.thewrightserver.net/api/bank";
|
||||||
level.bank_server = GetDvar("tws_server_id");
|
level.bank_server = GetDvar("tws_server_id");
|
||||||
level.bank_token = GetDvar("tws_bank_token");
|
level.bank_token = GetDvar("tws_bank_token");
|
||||||
// A private per-server cfg supplies this; never embed bank credentials.
|
// A private per-server cfg supplies this; never embed bank credentials.
|
||||||
if (level.bank_server == "" || level.bank_token.size < 40 ||
|
if (level.bank_server == "" || level.bank_token.size < 40 ||
|
||||||
getSubStr(level.bank_token, 0, 8) != "ec_bank_" ||
|
getSubStr(level.bank_token, 0, 8) != "ec_bank_")
|
||||||
getSubStr(level.bank_url, 0, 8) != "https://")
|
|
||||||
{
|
{
|
||||||
print("^1[tws_bank] Disabled: configure HTTPS, tws_server_id and a private tws_bank_token");
|
print("^1[tws_bank] Disabled: configure tws_server_id and a private tws_bank_token");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
level.bank_queue = bankLoad(level.bank_server, level.bank_url);
|
level.bank_queue = bankLoad(level.bank_server, level.bank_url);
|
||||||
|
|||||||
@@ -48,7 +48,12 @@ The optional `tws_t6_bank.gsc` adds `.b`, `.d <amount|all>` and
|
|||||||
`.w <amount|all>` and registers them in help. It requires common and telemetry
|
`.w <amount|all>` and registers them in help. It requires common and telemetry
|
||||||
`1.0.0` or newer, the TWS t6 plugin `1.1.0`, Electric Cherry's bank API and
|
`1.0.0` or newer, the TWS t6 plugin `1.1.0`, Electric Cherry's bank API and
|
||||||
a private per-server credential. Follow [bank setup and recovery](../../README.md#player-bank-t5-and-t6),
|
a private per-server credential. Follow [bank setup and recovery](../../README.md#player-bank-t5-and-t6),
|
||||||
including a full process restart for the plugin and a map restart for scripts.
|
including a full process restart for the plugin. Bank `2.0.0` uses only
|
||||||
|
`https://ec.thewrightserver.net/api/bank` and ignores `tws_bank_url`.
|
||||||
|
Update EC agents to `1.9.1+` first. If the old endpoint differs, follow the
|
||||||
|
[endpoint upgrade procedure](../../README.md#upgrading-the-bank-endpoint):
|
||||||
|
an EC rebuild must settle legacy journals before the new scripts start.
|
||||||
|
When the endpoint already matches, only a map restart is required.
|
||||||
|
|
||||||
## Persistent Discord text
|
## Persistent Discord text
|
||||||
|
|
||||||
|
|||||||
@@ -10,17 +10,15 @@ init()
|
|||||||
{
|
{
|
||||||
if (GetDvar("tws_bank_enabled") == "0")
|
if (GetDvar("tws_bank_enabled") == "0")
|
||||||
return;
|
return;
|
||||||
level.bank_url = GetDvar("tws_bank_url");
|
// Game containers use the public API, independent of the host controller URL.
|
||||||
if (level.bank_url == "")
|
|
||||||
level.bank_url = "https://ec.thewrightserver.net/api/bank";
|
level.bank_url = "https://ec.thewrightserver.net/api/bank";
|
||||||
level.bank_server = GetDvar("tws_server_id");
|
level.bank_server = GetDvar("tws_server_id");
|
||||||
level.bank_token = GetDvar("tws_bank_token");
|
level.bank_token = GetDvar("tws_bank_token");
|
||||||
// A private per-server cfg supplies this; never embed bank credentials.
|
// A private per-server cfg supplies this; never embed bank credentials.
|
||||||
if (level.bank_server == "" || level.bank_token.size < 40 ||
|
if (level.bank_server == "" || level.bank_token.size < 40 ||
|
||||||
getSubStr(level.bank_token, 0, 8) != "ec_bank_" ||
|
getSubStr(level.bank_token, 0, 8) != "ec_bank_")
|
||||||
getSubStr(level.bank_url, 0, 8) != "https://")
|
|
||||||
{
|
{
|
||||||
println("^1[tws_bank] Disabled: configure HTTPS, tws_server_id and a private tws_bank_token");
|
println("^1[tws_bank] Disabled: configure tws_server_id and a private tws_bank_token");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
level.bank_queue = bankLoad(level.bank_server, level.bank_url);
|
level.bank_queue = bankLoad(level.bank_server, level.bank_url);
|
||||||
|
|||||||
+71
-9
@@ -3,6 +3,8 @@ import assert from 'node:assert/strict';
|
|||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import vm from 'node:vm';
|
import vm from 'node:vm';
|
||||||
const root = new URL('../', import.meta.url);
|
const root = new URL('../', import.meta.url);
|
||||||
|
const publicEndpoint = 'https://ec.thewrightserver.net/api/bank';
|
||||||
|
const privateToken = 'ec_bank_' + 'a'.repeat(43);
|
||||||
function functions(source) {
|
function functions(source) {
|
||||||
const literals = [];
|
const literals = [];
|
||||||
const protectedSource = source.replace(/"(?:\\.|[^"\\])*"|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, token => {
|
const protectedSource = source.replace(/"(?:\\.|[^"\\])*"|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, token => {
|
||||||
@@ -11,7 +13,7 @@ function functions(source) {
|
|||||||
return `__literal_${literals.length - 1}__`;
|
return `__literal_${literals.length - 1}__`;
|
||||||
});
|
});
|
||||||
const found = new Map();
|
const found = new Map();
|
||||||
const headers = /^(bank_\w+)\(([^\n]*)\)\s*\n\{/gm;
|
const headers = /^(init|bank_\w+)\(([^\n]*)\)\s*\n\{/gm;
|
||||||
for (let match; (match = headers.exec(protectedSource));) {
|
for (let match; (match = headers.exec(protectedSource));) {
|
||||||
let end = headers.lastIndex, depth = 1;
|
let end = headers.lastIndex, depth = 1;
|
||||||
while (depth && end < protectedSource.length) {
|
while (depth && end < protectedSource.length) {
|
||||||
@@ -24,6 +26,8 @@ function functions(source) {
|
|||||||
const locals = new Set([...body.matchAll(/(?<![\w.])(\w+)\s*(?:[+*/%-]?=(?!=)|\+\+|--)/g)].map(x => x[1]));
|
const locals = new Set([...body.matchAll(/(?<![\w.])(\w+)\s*(?:[+*/%-]?=(?!=)|\+\+|--)/g)].map(x => x[1]));
|
||||||
body = body.replace(/\.size\b/g, '.length').replace(/http::request/g, 'httpPost');
|
body = body.replace(/\.size\b/g, '.length').replace(/http::request/g, 'httpPost');
|
||||||
body = body.replace(/player getGuid\(\)/g, 'getGuid(player)');
|
body = body.replace(/player getGuid\(\)/g, 'getGuid(player)');
|
||||||
|
body = body.replace(/#(?=__literal_)/g, '');
|
||||||
|
body = body.replace(/scripts\\(?:sp|zm)\\tws_t[56]_common::register_help_command/g, 'registerHelpCommand');
|
||||||
body = body.replace(/level thread \w+\(\);/g, '').replace(/level notify\(/g, 'notify(');
|
body = body.replace(/level thread \w+\(\);/g, '').replace(/level notify\(/g, 'notify(');
|
||||||
body = body.replace(/level waittill_any\(([^;]+)\);/g, 'yield [$1];');
|
body = body.replace(/level waittill_any\(([^;]+)\);/g, 'yield [$1];');
|
||||||
body = body.replace(/level waittill\(([^;]+)\);/g, 'yield $1;');
|
body = body.replace(/level waittill\(([^;]+)\);/g, 'yield $1;');
|
||||||
@@ -43,38 +47,70 @@ function engine(game, options = {}) {
|
|||||||
const functionsByName = functions(source);
|
const functionsByName = functions(source);
|
||||||
const player = { guid: '18446744073709551615', sessionstate: 'playing', score: 3000, score_total: 4000, pers: {}, alive: true };
|
const player = { guid: '18446744073709551615', sessionstate: 'playing', score: 3000, score_total: 4000, pers: {}, alive: true };
|
||||||
const entry = { player, active: true };
|
const entry = { player, active: true };
|
||||||
const saved = [], sent = [], messages = [], order = [];
|
const recovered = options.recovered ?? [];
|
||||||
|
const saved = recovered.map(report => JSON.parse(report.body)), sent = [], messages = [], order = [], loaded = [], dvarsRead = [];
|
||||||
|
const dvars = { zombiemode: '1', tws_server_id: 'server', tws_bank_token: privateToken, ...options.dvars };
|
||||||
|
function assertScope(server, url) {
|
||||||
|
assert.equal(server, dvars.tws_server_id);
|
||||||
|
assert.equal(url, publicEndpoint);
|
||||||
|
}
|
||||||
const c = vm.createContext({
|
const c = vm.createContext({
|
||||||
level: { bank_server: 'server', bank_url: 'https://test/api/bank', bank_token: 'ec_bank_private', bank_queue: [], bank_read: 0, bank_write: 0,
|
level: { telemetry_match: 'match', telemetry_started: true },
|
||||||
bank_busy: false, bank_ended: false, telemetry_match: 'match', telemetry_started: true },
|
GetDvar: name => { dvarsRead.push(name); return dvars[name] ?? ''; },
|
||||||
|
getSubStr: (value, start, end) => value.substring(start, end), registerHelpCommand: () => {},
|
||||||
isDefined: v => v !== undefined && v !== null, is_true: v => !!v, isAlive: p => p.alive,
|
isDefined: v => v !== undefined && v !== null, is_true: v => !!v, isAlive: p => p.alive,
|
||||||
int: Math.trunc, getGuid: p => p.guid,
|
int: Math.trunc, getGuid: p => p.guid,
|
||||||
jsonSerialize: a => JSON.stringify(Object.fromEntries(Object.entries(a))),
|
jsonSerialize: a => JSON.stringify(Object.fromEntries(Object.entries(a))),
|
||||||
bank_say: (_entry, message) => messages.push(message),
|
bank_say: (_entry, message) => messages.push(message),
|
||||||
telemetryNewMatch: () => 'request-1',
|
telemetryNewMatch: () => 'request-1',
|
||||||
bankStore: (_server, _url, body) => {
|
bankLoad: (server, url) => { assertScope(server, url); loaded.push({ server, url }); return recovered; },
|
||||||
|
bankStore: (server, url, body) => {
|
||||||
|
assertScope(server, url);
|
||||||
const data = JSON.parse(body); order.push(data.decision);
|
const data = JSON.parse(body); order.push(data.decision);
|
||||||
if (options.storageFailure === data.decision) return false;
|
if (options.storageFailure === data.decision) return false;
|
||||||
saved.splice(0, saved.length, data); return true;
|
saved.splice(0, saved.length, data); return true;
|
||||||
},
|
},
|
||||||
bankRemove: () => { if (options.removeFailure) return false; saved.length = 0; return true; },
|
bankRemove: (server, url) => { assertScope(server, url); if (options.removeFailure) return false; saved.length = 0; return true; },
|
||||||
httpPost: (url, data) => { order.push('http'); sent.push({ url, ...data }); return options.noHandle ? undefined : {}; },
|
httpPost: (url, data) => { order.push('http'); sent.push({ url, ...data }); return options.noHandle ? undefined : {}; },
|
||||||
httpComplete: () => {
|
httpComplete: () => {
|
||||||
options.during?.(c, entry);
|
options.during?.(c, entry);
|
||||||
const action = sent.at(-1).url.split('/').at(-1);
|
const action = sent.at(-1).url.split('/').at(-1);
|
||||||
const state = action === 'commit' ? 'committed' : action === 'cancel' ? 'cancelled' : action === 'balance' ? 'balance' : options.state ?? 'prepared';
|
const state = action === 'commit' ? 'committed' : action === 'cancel' ? 'cancelled' : action === 'balance' ? 'balance' : options.state ?? 'prepared';
|
||||||
const reply = { valid: true, state, request_id: action === 'balance' ? '' : options.wrongId ? 'wrong' : 'request-1', amount: options.amount ?? 2000, balance: 5000, code: options.code ?? 'ok' };
|
const reply = { valid: true, state, request_id: action === 'balance' ? '' : options.wrongId ? 'wrong' : JSON.parse(sent.at(-1).body).request_id, amount: options.amount ?? 2000, balance: options.balance ?? 5000, code: options.code ?? 'ok' };
|
||||||
return [JSON.stringify(reply), !options.timeout, '', options.missingDiagnostics ? undefined : { http_status: options.status ?? 200 }];
|
return [JSON.stringify(reply), !options.timeout, '', options.missingDiagnostics ? undefined : { http_status: options.status ?? 200 }];
|
||||||
},
|
},
|
||||||
bankResponse: data => options.malformed ? { valid: false } : JSON.parse(data),
|
bankResponse: data => options.malformed ? { valid: false } : JSON.parse(data),
|
||||||
print: () => {}, println: () => {}
|
print: () => {}, println: () => {}
|
||||||
});
|
});
|
||||||
vm.runInContext([...functionsByName].filter(([name]) => !excluded.has(name)).map(([, code]) => code).join('\n'), c);
|
vm.runInContext([...functionsByName].filter(([name]) => !excluded.has(name)).map(([, code]) => code).join('\n'), c);
|
||||||
return { c, player, entry, saved, sent, messages, order, run: args => c.bank_transfer(entry,args) };
|
c.init();
|
||||||
|
return { c, player, entry, saved, sent, messages, order, loaded, dvarsRead, run: args => c.bank_transfer(entry,args) };
|
||||||
}
|
}
|
||||||
let checks = 0;
|
let checks = 0;
|
||||||
for (const game of ['t5','t6']) {
|
for (const game of ['t5','t6']) {
|
||||||
function test(name, fn) { try { fn(); checks++; } catch (cause) { throw new Error(`${game}: ${name}`, {cause}); } }
|
function test(name, fn) { try { fn(); checks++; } catch (cause) { throw new Error(`${game}: ${name}`, {cause}); } }
|
||||||
|
test('startup and every request ignore obsolete endpoint overrides and retain instance authentication', () => {
|
||||||
|
for (const url of ['', 'https://controller.internal/api/bank', 'http://old-controller/api/bank', 'https://untrusted.example/api/bank']) {
|
||||||
|
for (const command of [['.b'], ['.d','2000'], ['.w','all']]) {
|
||||||
|
const e = engine(game, { dvars: { tws_bank_url: url } });
|
||||||
|
e.run(command);
|
||||||
|
if (command[0] !== '.b') e.c.bank_sender().next();
|
||||||
|
assert.equal(e.loaded.length, 1);
|
||||||
|
assert(!e.dvarsRead.includes('tws_bank_url'));
|
||||||
|
assert.equal(e.sent[0].url, publicEndpoint + '/' + ({'.b':'balance','.d':'deposit','.w':'withdraw'}[command[0]]));
|
||||||
|
if (command[0] !== '.b') assert.equal(e.sent[1].url, publicEndpoint + '/commit');
|
||||||
|
for (const request of e.sent) {
|
||||||
|
assert.equal(request.headers.Authorization, 'Bearer ' + privateToken);
|
||||||
|
assert.equal(JSON.parse(request.body).server, 'server');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
test('missing or invalid instance credentials and disabled banks do not load journals', () => {
|
||||||
|
for (const dvars of [{tws_server_id:''}, {tws_bank_token:''}, {tws_bank_token:'wrong_' + 'a'.repeat(43)}, {tws_bank_enabled:'0'}]) {
|
||||||
|
assert.equal(engine(game, {dvars}).loaded.length, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
test('strict positive integer syntax', () => {
|
test('strict positive integer syntax', () => {
|
||||||
const {c} = engine(game);
|
const {c} = engine(game);
|
||||||
for (const bad of ['', '0', '-1', '1.5', '+1', '1e3', '1000001', 'all now']) assert.equal(c.bank_amount(bad),0);
|
for (const bad of ['', '0', '-1', '1.5', '+1', '1e3', '1000001', 'all now']) assert.equal(c.bank_amount(bad),0);
|
||||||
@@ -85,7 +121,7 @@ for (const game of ['t5','t6']) {
|
|||||||
assert.equal(e.player.score,1000); assert.equal(e.player.score_total,4000); assert.equal(e.player.pers.score,1000);
|
assert.equal(e.player.score,1000); assert.equal(e.player.score_total,4000); assert.equal(e.player.pers.score,1000);
|
||||||
assert.deepEqual(e.order,['cancel','http','commit']); assert.equal(e.saved[0].decision,'commit');
|
assert.deepEqual(e.order,['cancel','http','commit']); assert.equal(e.saved[0].decision,'commit');
|
||||||
assert.equal(e.saved[0].guid,e.player.guid);
|
assert.equal(e.saved[0].guid,e.player.guid);
|
||||||
assert.equal(e.sent[0].headers.Authorization,'Bearer ec_bank_private');
|
assert.equal(e.sent[0].headers.Authorization,'Bearer ' + privateToken);
|
||||||
assert.equal(e.sent[0].timeout_ms,15000);
|
assert.equal(e.sent[0].timeout_ms,15000);
|
||||||
});
|
});
|
||||||
test('all captures deposit points and resolves withdrawal amount', () => {
|
test('all captures deposit points and resolves withdrawal amount', () => {
|
||||||
@@ -121,6 +157,32 @@ for (const game of ['t5','t6']) {
|
|||||||
test('balance reads do not journal or alter points', () => {
|
test('balance reads do not journal or alter points', () => {
|
||||||
const e=engine(game); e.run(['.b']); assert.equal(e.saved.length,0); assert.equal(e.player.score,3000); assert.match(e.messages[0],/5000/);
|
const e=engine(game); e.run(['.b']); assert.equal(e.saved.length,0); assert.equal(e.player.score,3000); assert.match(e.messages[0],/5000/);
|
||||||
});
|
});
|
||||||
|
test('empty balance displays zero and withdrawing all reports insufficient balance', () => {
|
||||||
|
const e = engine(game, {amount:0, balance:0}); e.run(['.b']);
|
||||||
|
assert.equal(e.messages[0], '^3Bank: ^70 ^3points; available: ^70');
|
||||||
|
assert.equal(e.saved.length, 0); assert.equal(e.player.score, 3000);
|
||||||
|
const w = engine(game, {amount:0, balance:0, state:'rejected', code:'insufficient_balance'}); w.run(['.w','all']);
|
||||||
|
assert.match(w.messages[0], /Not enough bank balance/);
|
||||||
|
assert.equal(w.player.score, 3000); assert.equal(w.saved[0].decision, 'cancel');
|
||||||
|
w.c.bank_sender().next();
|
||||||
|
assert.equal(w.sent[1].url, publicEndpoint + '/cancel'); assert.equal(w.saved.length, 0);
|
||||||
|
});
|
||||||
|
test('recovered commit and cancel decisions retry at the public endpoint without applying points', () => {
|
||||||
|
for (const decision of ['commit','cancel']) {
|
||||||
|
const body = JSON.stringify({server:'server', request_id:'recovered', decision});
|
||||||
|
const options = {timeout:true, dvars:{tws_bank_url:'https://controller.internal/api/bank'}, recovered:[{id:'recovered', body, decision}]};
|
||||||
|
const e = engine(game, options), sender = e.c.bank_sender();
|
||||||
|
sender.next();
|
||||||
|
assert.equal(e.saved.length, 1); assert.equal(e.player.score, 3000);
|
||||||
|
options.timeout = false; sender.next();
|
||||||
|
assert.equal(e.saved.length, 0); assert.equal(e.player.score, 3000);
|
||||||
|
assert.equal(e.sent.length, 2);
|
||||||
|
for (const request of e.sent) {
|
||||||
|
assert.equal(request.url, publicEndpoint + '/' + decision);
|
||||||
|
assert.equal(request.body, body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
test('recovered settlements cannot reapply points and retry cleanup safely', () => {
|
test('recovered settlements cannot reapply points and retry cleanup safely', () => {
|
||||||
const e=engine(game); e.run(['.w','2000']); const score=e.player.score;
|
const e=engine(game); e.run(['.w','2000']); const score=e.player.score;
|
||||||
const sender=e.c.bank_sender(); sender.next(); assert.equal(e.saved.length,0); assert.equal(e.player.score,score);
|
const sender=e.c.bank_sender(); sender.next(); assert.equal(e.saved.length,0); assert.equal(e.player.score,score);
|
||||||
|
|||||||
@@ -81,6 +81,14 @@ int main(int argc, char** argv)
|
|||||||
{"code","ok"},{"amount",2000},{"balance",3000}};
|
{"code","ok"},{"amount",2000},{"balance",3000}};
|
||||||
const auto ack = parse_acknowledgement(valid.dump());
|
const auto ack = parse_acknowledgement(valid.dump());
|
||||||
assert(ack.valid && ack.amount == 2000 && ack.balance == 3000 && ack.id == "one");
|
assert(ack.valid && ack.amount == 2000 && ack.balance == 3000 && ack.id == "one");
|
||||||
|
auto empty = valid;
|
||||||
|
empty["state"] = "balance"; empty["request_id"] = "";
|
||||||
|
empty["amount"] = 0; empty["balance"] = 0;
|
||||||
|
const auto zero = parse_acknowledgement(empty.dump());
|
||||||
|
assert(zero.valid && zero.amount == 0 && zero.balance == 0);
|
||||||
|
empty["ok"] = false; empty["state"] = "rejected";
|
||||||
|
empty["request_id"] = "one"; empty["code"] = "insufficient_balance";
|
||||||
|
assert(parse_acknowledgement(empty.dump()).valid);
|
||||||
for (const auto& field : {"amount","balance"})
|
for (const auto& field : {"amount","balance"})
|
||||||
for (const auto& bad : {nlohmann::json(-1),nlohmann::json(1.5),nlohmann::json("100"),nlohmann::json(1000001)})
|
for (const auto& bad : {nlohmann::json(-1),nlohmann::json(1.5),nlohmann::json("100"),nlohmann::json(1000001)})
|
||||||
{
|
{
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"tws_t5_afk.gsc": "1.0.0",
|
"tws_t5_afk.gsc": "1.0.0",
|
||||||
"tws_t5_bank.gsc": "1.0.0",
|
"tws_t5_bank.gsc": "2.0.0",
|
||||||
"tws_t5_common.gsc": "1.1.0",
|
"tws_t5_common.gsc": "1.1.0",
|
||||||
"tws_t5_hud.gsc": "1.1.0",
|
"tws_t5_hud.gsc": "1.1.0",
|
||||||
"tws_t5_join.gsc": "1.0.0",
|
"tws_t5_join.gsc": "1.0.0",
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
"tws_t5_lock.gsc": "1.0.0",
|
"tws_t5_lock.gsc": "1.0.0",
|
||||||
"tws_t5_telemetry.gsc": "1.0.0",
|
"tws_t5_telemetry.gsc": "1.0.0",
|
||||||
"tws_t6_afk.gsc": "1.0.0",
|
"tws_t6_afk.gsc": "1.0.0",
|
||||||
"tws_t6_bank.gsc": "1.0.0",
|
"tws_t6_bank.gsc": "2.0.0",
|
||||||
"tws_t6_common.gsc": "1.1.0",
|
"tws_t6_common.gsc": "1.1.0",
|
||||||
"tws_t6_hud.gsc": "1.1.0",
|
"tws_t6_hud.gsc": "1.1.0",
|
||||||
"tws_t6_join.gsc": "1.0.1",
|
"tws_t6_join.gsc": "1.0.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user