mirror of
https://github.com/bdnugget/rs-sdk.git
synced 2026-11-07 09:07:30 +00:00
fix(hiscores): clear bank snapshots when a bank empties
Withdrawn items stayed ranked because empty banks skipped the update. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ce65d8e745
commit
c06f209270
3 files changed
+79
-14
No files matched your search
@@ -213,7 +213,7 @@ async function updateQuestHiscore(account: HiscoreAccount, player: Player, profi
|
||||
// rs-sdk: outfit + bank boards are also refreshed on autosave (not just logout), so players who
|
||||
// stay online for days don't sit on a stale value. Autosave posts every online player in one
|
||||
// burst, so writes are skipped when the row hasn't changed - most banks don't move in 15 min.
|
||||
async function updateWealthHiscores(account: HiscoreAccount, player: Player, profile: string) {
|
||||
export async function updateWealthHiscores(account: HiscoreAccount, player: Player, profile: string) {
|
||||
// Update outfit hiscore
|
||||
const worn = player.getInventory(InvType.WORN);
|
||||
if (worn) {
|
||||
@@ -266,18 +266,16 @@ async function updateWealthHiscores(account: HiscoreAccount, player: Player, pro
|
||||
}
|
||||
}
|
||||
|
||||
if (bankItems.length > 0) {
|
||||
// Sort by value descending so the most valuable items appear first
|
||||
bankItems.sort((a, b) => b.value - a.value);
|
||||
const bankItemsJson = JSON.stringify(bankItems);
|
||||
const existingBank = await db.selectFrom('hiscore_bank').select(['value', 'items']).where('account_id', '=', account.id).where('profile', '=', profile).executeTakeFirst();
|
||||
if (existingBank && existingBank.value === bankTotalValue && existingBank.items === bankItemsJson) {
|
||||
// unchanged
|
||||
} else if (existingBank) {
|
||||
await db.updateTable('hiscore_bank').set({ value: bankTotalValue, items: bankItemsJson, date: toDbDate(new Date()) }).where('account_id', '=', account.id).where('profile', '=', profile).execute();
|
||||
} else {
|
||||
await db.insertInto('hiscore_bank').values({ account_id: account.id, profile, value: bankTotalValue, items: bankItemsJson }).execute();
|
||||
}
|
||||
// Empty banks must also replace old snapshots, otherwise withdrawn items remain ranked.
|
||||
bankItems.sort((a, b) => b.value - a.value);
|
||||
const bankItemsJson = JSON.stringify(bankItems);
|
||||
const existingBank = await db.selectFrom('hiscore_bank').select(['value', 'items']).where('account_id', '=', account.id).where('profile', '=', profile).executeTakeFirst();
|
||||
if (existingBank && existingBank.value === bankTotalValue && existingBank.items === bankItemsJson) {
|
||||
// unchanged
|
||||
} else if (existingBank) {
|
||||
await db.updateTable('hiscore_bank').set({ value: bankTotalValue, items: bankItemsJson, date: toDbDate(new Date()) }).where('account_id', '=', account.id).where('profile', '=', profile).execute();
|
||||
} else if (bankItems.length > 0) {
|
||||
await db.insertInto('hiscore_bank').values({ account_id: account.id, profile, value: bankTotalValue, items: bankItemsJson }).execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, test } from 'bun:test';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { copyFileSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { parseBankSnapshots, rankMatchingBanks } from '../src/web/pages/BankHiscores.js';
|
||||
@@ -7,6 +7,24 @@ import { parseBankSnapshots, rankMatchingBanks } from '../src/web/pages/BankHisc
|
||||
const item = (name: string, value: number, count = 100) => ({ name, value, count });
|
||||
const row = (username: string, items: ReturnType<typeof item>[], value = 1000000) => ({ username, value, items: JSON.stringify(items) });
|
||||
|
||||
test('bank snapshots clear withdrawn items and empty banks without rewriting unchanged rows', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'bank-snapshot-update-'));
|
||||
try {
|
||||
mkdirSync(join(dir, 'data/config'), { recursive: true });
|
||||
copyFileSync(join(import.meta.dir, '../data/config/private.pem'), join(dir, 'data/config/private.pem'));
|
||||
copyFileSync(join(import.meta.dir, '../data/config/public.pem'), join(dir, 'data/config/public.pem'));
|
||||
const result = Bun.spawnSync([process.execPath, join(import.meta.dir, 'fixtures/bank-snapshot-update.ts')], {
|
||||
cwd: dir,
|
||||
env: { ...process.env, DB_BACKEND: 'sqlite', GE_ENABLED: 'false', BANK_TEST_CACHE: join(import.meta.dir, '../data/pack') },
|
||||
stdout: 'pipe', stderr: 'pipe'
|
||||
});
|
||||
if (result.exitCode !== 0) throw new Error(result.stdout.toString() + result.stderr.toString());
|
||||
expect(result.stdout.toString()).toContain('Bank snapshot update checks passed');
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('matches literal substrings ignoring case and sums full stack values once', () => {
|
||||
const banks = parseBankSnapshots([row('wealthy', [item('Lobster', 1000000), item('Shrimps', 20)]), row('shrimp-holder', [item('Raw shrimps', 400), item('Shrimps', 600), item('Lobster', 100)], 1100), row('unrelated', [item('Lobster', 5000000)])]);
|
||||
const matches = rankMatchingBanks(banks, ' ShRiMp ');
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { mock } from 'bun:test';
|
||||
import { Database } from 'bun:sqlite';
|
||||
import { EventEmitter } from 'node:events';
|
||||
|
||||
class TestWorker extends EventEmitter { postMessage() {} }
|
||||
mock.module('worker_threads', () => ({ Worker: TestWorker }));
|
||||
await import('../../src/engine/World.js');
|
||||
const { default: InvType } = await import('../../src/cache/config/InvType.js');
|
||||
const { default: ObjType } = await import('../../src/cache/config/ObjType.js');
|
||||
const { default: Player } = await import('../../src/engine/entity/Player.js');
|
||||
const { updateWealthHiscores } = await import('../../src/server/login/LoginServer.js');
|
||||
const { db } = await import('../../src/db/query.js');
|
||||
InvType.load(process.env.BANK_TEST_CACHE!);
|
||||
ObjType.load(process.env.BANK_TEST_CACHE!);
|
||||
|
||||
const database = new Database('db.sqlite');
|
||||
database.exec('CREATE TABLE hiscore_bank (account_id INTEGER, profile TEXT, value INTEGER, items TEXT, date TEXT)');
|
||||
const oldItems = JSON.stringify([{ id: 1042, name: 'Blue partyhat', value: 2, count: 2 }]);
|
||||
database.query('INSERT INTO hiscore_bank VALUES (1, ?, 2, ?, ?)').run('main', oldItems, 'old');
|
||||
database.query('INSERT INTO hiscore_bank VALUES (1, ?, 2, ?, ?)').run('iron', oldItems, 'old');
|
||||
const account = { id: 1, staffmodlevel: 0, banned_until: null };
|
||||
const player = new Player('snapshot', 1n, 1n);
|
||||
const bank = InvType.getId('bank');
|
||||
try {
|
||||
// A save with no bank inventory must clear an old row, preserving other profiles.
|
||||
await updateWealthHiscores(account, player, 'main');
|
||||
assert.deepEqual(database.query('SELECT value,items FROM hiscore_bank WHERE profile=?').get('main'), { value: 0, items: '[]' });
|
||||
assert.equal((database.query('SELECT items FROM hiscore_bank WHERE profile=?').get('iron') as { items: string }).items, oldItems);
|
||||
database.query('UPDATE hiscore_bank SET date=? WHERE profile=?').run('unchanged', 'main');
|
||||
await updateWealthHiscores(account, player, 'main');
|
||||
assert.equal((database.query('SELECT date FROM hiscore_bank WHERE profile=?').get('main') as { date: string }).date, 'unchanged');
|
||||
|
||||
// Refill, then withdraw the final item: quantity and value must follow the real bank.
|
||||
player.invAdd(bank, 1042, 2);
|
||||
await updateWealthHiscores(account, player, 'main');
|
||||
assert.deepEqual(database.query('SELECT value,items FROM hiscore_bank WHERE profile=?').get('main'), { value: 2, items: oldItems });
|
||||
player.invDel(bank, 1042, 2);
|
||||
await updateWealthHiscores(account, player, 'main');
|
||||
assert.deepEqual(database.query('SELECT value,items FROM hiscore_bank WHERE profile=?').get('main'), { value: 0, items: '[]' });
|
||||
await updateWealthHiscores({ ...account, id: 2 }, new Player('newempty', 2n, 2n), 'main');
|
||||
assert.equal(database.query('SELECT count(*) AS n FROM hiscore_bank WHERE account_id=2').get()?.n, 0);
|
||||
console.log('Bank snapshot update checks passed');
|
||||
} finally {
|
||||
database.close();
|
||||
await db.destroy();
|
||||
}
|
||||
// World imports initialize recurring timers; this isolated fixture has completed.
|
||||
process.exit(0);
|
||||
Reference in new issue
Block a user