Files
Tiago Valente 814e5bdfc5 CheatSpecter 0.4.0
- Find: text search, grouped search, undo, went up/down by, where to search, before/after compare
- Debug tab: breakpoints, stepping, registers, stack, threads, what code touches, structure compare, pointer re-check
- Engine tab: Unreal, Unity, Godot, GameMaker and C++ class names; hidden settings and switches
- Code cheats with labels, variables, references and shared code; rule builder; script commands
- Cheats kept on by the PS5 without the browser
- Learn section with twelve lessons and an in-page practice game
- Section (master code) community cheats; SHN and MC4 export
- Groups, dropdowns, hotkeys, first-cheat guide, re-find after update, restore everything
- MCP server: 25 tools
- Quick start, wiki, contributor and security guides, changelog
2026-10-10 12:06:50 -07:00

156 lines
8.5 KiB
JavaScript

// Game metadata parsing, the .cspector file format, and the bridge's
// pass-through to the payload on the console.
import test from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import { startBridge } from '../bridge/server.mjs';
import { buildParamJson, buildSfo } from '../bridge/demo-library.mjs';
import { formatSize, parseParamJson, parseParamSfo, parseSfo } from '../src/lib/library.ts';
import { emptyTable, exportOwnTable, isOwnTableText, parseOwnTable, saveFileText } from '../src/lib/table.ts';
import { fileVersion, pickVersion, tableFileName } from '../src/lib/tablestore.ts';
test('reads a PS5 game name in the game\'s default language', () => {
assert.deepEqual(parseParamJson(buildParamJson('PPSA07391', 'Harbor Lights', '01.009.000')), {
name: 'Harbor Lights',
version: '01.009.000',
platform: 'PS5',
});
const multi = JSON.stringify({
localizedParameters: { defaultLanguage: 'ja-JP', 'en-US': { titleName: 'English' }, 'ja-JP': { titleName: '日本語' } },
});
assert.equal(parseParamJson(multi).name, '日本語');
const noDefault = JSON.stringify({ localizedParameters: { 'fr-FR': { titleName: 'Français' } } });
assert.equal(parseParamJson(noDefault).name, 'Français');
assert.equal(parseParamJson('{"localizedParameters":{}}'), null);
assert.equal(parseParamJson('not json'), null);
assert.equal(parseParamJson('[]'), null);
});
test('reads a PS4 param.sfo and survives damaged ones', () => {
const sfo = buildSfo({ TITLE: 'Old Favorite ™', TITLE_ID: 'CUSA12345', APP_VER: '01.07', PARENTAL_LEVEL: 5 });
assert.deepEqual(parseSfo(sfo), { APP_VER: '01.07', PARENTAL_LEVEL: 5, TITLE: 'Old Favorite ™', TITLE_ID: 'CUSA12345' });
assert.deepEqual(parseParamSfo(sfo), { name: 'Old Favorite ™', version: '01.07', platform: 'PS4' });
assert.equal(parseParamSfo(new Uint8Array(4)), null);
assert.equal(parseParamSfo(new TextEncoder().encode('{"this":"is json, not an sfo"}')), null);
for (let cut = 0; cut < sfo.length; cut += 7) assert.doesNotThrow(() => parseParamSfo(sfo.subarray(0, cut)));
const lying = sfo.slice();
new DataView(lying.buffer).setUint32(16, 0xffffffff, true); // claims four billion entries
assert.equal(parseParamSfo(lying), null);
});
test('sizes read the way a console shows them', () => {
assert.equal(formatSize(0), '');
assert.equal(formatSize(512), '512 B');
assert.equal(formatSize(1536), '1.5 KB');
assert.equal(formatSize(41.6 * 1024 ** 3), '41.6 GB');
assert.equal(formatSize(150 * 1024 ** 3), '150 GB');
});
const sample = () => {
const t = emptyTable('PPSA07391', '01.009.000', 'Harbor Lights');
t.entries.push({ id: 'a', name: 'Infinite ammo', type: 'patch', at: { kind: 'module', offset: '0x2040' }, on: '909090', off: 'FF4B28', note: '' });
t.entries.push({ id: 'b', name: 'Give gold', type: 'call', fn: { kind: 'module', offset: '0x4100' }, args: [] });
t.entries.push({ id: 'c', name: 'Looked at', type: 'script', code: 'await cs.sleep(1);' });
t.entries.push({ id: 'd', name: 'Not looked at', type: 'script', code: 'await cs.sleep(1);', unreviewed: true });
t.entries.push({ id: 'e', name: 'Session only', type: 'value', at: { kind: 'address', address: '0x880000000' }, valueType: 'u32', value: '1', freeze: false });
return t;
};
test('a .cspector file is minified JSON with a proper header', () => {
const text = saveFileText(sample());
const doc = JSON.parse(text);
assert.deepEqual(Object.keys(doc), ['format', 'formatVersion', 'game', 'cheats']);
assert.equal(doc.format, 'cheatspecter');
assert.equal(doc.formatVersion, 1);
assert.deepEqual(doc.game, { name: 'Harbor Lights', titleId: 'PPSA07391', version: '01.009.000' });
// Nothing session-only, nothing empty.
assert.deepEqual(doc.cheats[0], { name: 'Infinite ammo', type: 'patch', at: { kind: 'module', offset: '0x2040' }, on: '909090', off: 'FF4B28' });
assert.deepEqual(doc.cheats[1], { name: 'Give gold', type: 'call', fn: { kind: 'module', offset: '0x4100' }, reviewed: true });
assert.equal(text, JSON.stringify(doc), 'written minified');
assert.deepEqual(parseOwnTable(JSON.stringify(doc, null, 2), { own: true }).entries.length, doc.cheats.length, 'a hand-formatted file still loads');
assert.ok(isOwnTableText(text));
});
test('a saved file comes back as it was; a shared one has to be reviewed', () => {
const own = parseOwnTable(saveFileText(sample()), { own: true });
assert.deepEqual(own.entries.map((e) => [e.name, e.unreviewed]), [
['Infinite ammo', undefined],
['Give gold', undefined],
['Looked at', undefined],
['Not looked at', true],
['Session only', undefined],
]);
assert.deepEqual(own.entries[1].args, []);
assert.equal(new Set(own.entries.map((e) => e.id)).size, 5);
// The same bytes arriving as someone else's file: "reviewed" in the file counts for nothing.
const foreign = parseOwnTable(saveFileText(sample()));
assert.deepEqual(foreign.entries.filter((e) => e.unreviewed).map((e) => e.name), ['Give gold', 'Looked at', 'Not looked at']);
const shared = JSON.parse(exportOwnTable(sample()));
assert.deepEqual(shared.cheats.map((c) => c.name), ['Infinite ammo', 'Give gold', 'Looked at', 'Not looked at']);
assert.ok(shared.cheats.every((c) => !('reviewed' in c) && !('id' in c)));
});
test('older exports and newer formats are handled deliberately', () => {
const legacy = JSON.stringify({ format: 'cheatspecter/1', titleId: 'ppsa07391', version: '01.000.000', name: 'Old', entries: sample().entries });
const t = parseOwnTable(legacy);
assert.equal(t.titleId, 'PPSA07391');
assert.equal(t.entries.length, 5);
assert.throws(() => parseOwnTable(JSON.stringify({ format: 'cheatspecter', formatVersion: 2, game: {}, cheats: [] })), /newer CheatSpecter/);
assert.throws(() => parseOwnTable(JSON.stringify({ format: 'cheatspecter', formatVersion: 1, game: {} })), /no cheats/);
});
test('file names carry the title ID and game version', () => {
assert.equal(tableFileName('PPSA18605', '01.009.000'), 'PPSA18605-01.009.000.cspector');
assert.equal(tableFileName('PPSA18605', ''), 'PPSA18605-unknown.cspector');
assert.equal(fileVersion('../../etc'), 'etc');
assert.equal(pickVersion(['01.002.000', '01.010.000', '01.009.000'], '01.009.000'), '01.009.000');
assert.equal(pickVersion(['01.002.000', '01.010.000', '01.009.000'], '01.011.000'), '01.010.000');
assert.equal(pickVersion([], '01.000.000'), null);
});
test('the bridge passes library and cheat-file requests on to the console, and only those', async () => {
// Stands in for the payload on a console.
const seen = [];
const payload = http.createServer((req, res) => {
const chunks = [];
req.on('data', (c) => chunks.push(c));
req.on('end', () => {
seen.push(`${req.method} ${req.url} ${Buffer.concat(chunks).toString()}`.trim());
res.writeHead(req.method === 'PUT' ? 204 : 200, { 'content-type': 'application/json' });
res.end(req.method === 'PUT' ? undefined : '{"titles":[]}');
});
});
await new Promise((r) => payload.listen(0, '127.0.0.1', r));
const bridge = startBridge({ port: 0, host: '127.0.0.1', eventPort: null, payloadPort: payload.address().port });
await bridge.ready;
const base = `http://127.0.0.1:${bridge.server.address().port}`;
try {
assert.deepEqual(await (await fetch(`${base}/api/library?host=127.0.0.1`)).json(), { titles: [] });
assert.equal((await fetch(`${base}/api/tables/PPSA07391/01.009.000?host=127.0.0.1`, { method: 'PUT', body: 'hello' })).status, 204);
assert.equal((await fetch(`${base}/api/launch/PPSA07391?host=127.0.0.1`, { method: 'POST' })).status, 200);
assert.deepEqual(seen, ['GET /api/library', 'PUT /api/tables/PPSA07391/01.009.000 hello', 'POST /api/launch/PPSA07391']);
// Not a console on the local network, not a path the payload has, not this page's origin.
assert.equal((await fetch(`${base}/api/library?host=example.com`)).status, 400);
assert.equal((await fetch(`${base}/api/library?host=8.8.8.8`)).status, 400);
assert.equal((await fetch(`${base}/api/appmeta/PPSA07391/..%2F..%2Fetc?host=127.0.0.1`)).status, 400);
assert.equal((await fetch(`${base}/api/launch/PPSA07391?host=127.0.0.1`)).status, 405);
const foreign = await fetch(`${base}/api/launch/PPSA07391?host=127.0.0.1`, { method: 'POST', headers: { origin: 'http://evil.example' } });
assert.equal(foreign.status, 403);
assert.equal(seen.length, 3);
// No payload on that console.
payload.close();
payload.closeAllConnections();
const gone = await fetch(`${base}/api/library?host=127.0.0.1`);
assert.equal(gone.status, 502);
} finally {
await bridge.close();
payload.close();
}
});