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
|
||||
|
||||
### 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
|
||||
|
||||
| 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
|
||||
instance and injects it through that instance's private
|
||||
`electric_cherry_bank.cfg`. It sets `tws_server_id`, `tws_bank_token` and
|
||||
`tws_bank_url` automatically. Rebuilds and host moves keep the instance token;
|
||||
`electric_cherry_bank.cfg`, with `tws_server_id` supplied by the launch configuration.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
@@ -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
|
||||
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
|
||||
or launch cancellation. Update the agents before enabling the bank. An outage
|
||||
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
|
||||
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`.
|
||||
Before rollout, build/link Windows DLLs, compile GSC, and verify both games:
|
||||
help, balance and `all` commands, score HUD/persistence, max/overdraw errors,
|
||||
death/respawn/disconnect during HTTP, late telemetry, lost prepare/commit
|
||||
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
|
||||
|
||||
|
||||
@@ -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
|
||||
`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),
|
||||
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
|
||||
|
||||
|
||||
@@ -6,17 +6,15 @@ init()
|
||||
{
|
||||
if (GetDvar(#"zombiemode") != "1" || GetDvar("tws_bank_enabled") == "0")
|
||||
return;
|
||||
level.bank_url = GetDvar("tws_bank_url");
|
||||
if (level.bank_url == "")
|
||||
// Game containers use the public API, independent of the host controller URL.
|
||||
level.bank_url = "https://ec.thewrightserver.net/api/bank";
|
||||
level.bank_server = GetDvar("tws_server_id");
|
||||
level.bank_token = GetDvar("tws_bank_token");
|
||||
// A private per-server cfg supplies this; never embed bank credentials.
|
||||
if (level.bank_server == "" || level.bank_token.size < 40 ||
|
||||
getSubStr(level.bank_token, 0, 8) != "ec_bank_" ||
|
||||
getSubStr(level.bank_url, 0, 8) != "https://")
|
||||
getSubStr(level.bank_token, 0, 8) != "ec_bank_")
|
||||
{
|
||||
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;
|
||||
}
|
||||
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
|
||||
`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),
|
||||
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
|
||||
|
||||
|
||||
@@ -10,17 +10,15 @@ init()
|
||||
{
|
||||
if (GetDvar("tws_bank_enabled") == "0")
|
||||
return;
|
||||
level.bank_url = GetDvar("tws_bank_url");
|
||||
if (level.bank_url == "")
|
||||
// Game containers use the public API, independent of the host controller URL.
|
||||
level.bank_url = "https://ec.thewrightserver.net/api/bank";
|
||||
level.bank_server = GetDvar("tws_server_id");
|
||||
level.bank_token = GetDvar("tws_bank_token");
|
||||
// A private per-server cfg supplies this; never embed bank credentials.
|
||||
if (level.bank_server == "" || level.bank_token.size < 40 ||
|
||||
getSubStr(level.bank_token, 0, 8) != "ec_bank_" ||
|
||||
getSubStr(level.bank_url, 0, 8) != "https://")
|
||||
getSubStr(level.bank_token, 0, 8) != "ec_bank_")
|
||||
{
|
||||
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;
|
||||
}
|
||||
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 vm from 'node:vm';
|
||||
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) {
|
||||
const literals = [];
|
||||
const protectedSource = source.replace(/"(?:\\.|[^"\\])*"|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, token => {
|
||||
@@ -11,7 +13,7 @@ function functions(source) {
|
||||
return `__literal_${literals.length - 1}__`;
|
||||
});
|
||||
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));) {
|
||||
let end = headers.lastIndex, depth = 1;
|
||||
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]));
|
||||
body = body.replace(/\.size\b/g, '.length').replace(/http::request/g, 'httpPost');
|
||||
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 waittill_any\(([^;]+)\);/g, 'yield [$1];');
|
||||
body = body.replace(/level waittill\(([^;]+)\);/g, 'yield $1;');
|
||||
@@ -43,38 +47,70 @@ function engine(game, options = {}) {
|
||||
const functionsByName = functions(source);
|
||||
const player = { guid: '18446744073709551615', sessionstate: 'playing', score: 3000, score_total: 4000, pers: {}, alive: 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({
|
||||
level: { bank_server: 'server', bank_url: 'https://test/api/bank', bank_token: 'ec_bank_private', bank_queue: [], bank_read: 0, bank_write: 0,
|
||||
bank_busy: false, bank_ended: false, telemetry_match: 'match', telemetry_started: true },
|
||||
level: { 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,
|
||||
int: Math.trunc, getGuid: p => p.guid,
|
||||
jsonSerialize: a => JSON.stringify(Object.fromEntries(Object.entries(a))),
|
||||
bank_say: (_entry, message) => messages.push(message),
|
||||
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);
|
||||
if (options.storageFailure === data.decision) return false;
|
||||
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 : {}; },
|
||||
httpComplete: () => {
|
||||
options.during?.(c, entry);
|
||||
const action = sent.at(-1).url.split('/').at(-1);
|
||||
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 }];
|
||||
},
|
||||
bankResponse: data => options.malformed ? { valid: false } : JSON.parse(data),
|
||||
print: () => {}, println: () => {}
|
||||
});
|
||||
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;
|
||||
for (const game of ['t5','t6']) {
|
||||
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', () => {
|
||||
const {c} = engine(game);
|
||||
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.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.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);
|
||||
});
|
||||
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', () => {
|
||||
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', () => {
|
||||
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);
|
||||
|
||||
@@ -81,6 +81,14 @@ int main(int argc, char** argv)
|
||||
{"code","ok"},{"amount",2000},{"balance",3000}};
|
||||
const auto ack = parse_acknowledgement(valid.dump());
|
||||
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& 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_bank.gsc": "1.0.0",
|
||||
"tws_t5_bank.gsc": "2.0.0",
|
||||
"tws_t5_common.gsc": "1.1.0",
|
||||
"tws_t5_hud.gsc": "1.1.0",
|
||||
"tws_t5_join.gsc": "1.0.0",
|
||||
@@ -8,7 +8,7 @@
|
||||
"tws_t5_lock.gsc": "1.0.0",
|
||||
"tws_t5_telemetry.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_hud.gsc": "1.1.0",
|
||||
"tws_t6_join.gsc": "1.0.1",
|
||||
|
||||
Reference in New Issue
Block a user