mirror of
https://github.com/TiagoVal97/cheatspecter.git
synced 2026-10-18 06:05:17 +02:00
165 lines
11 KiB
JavaScript
165 lines
11 KiB
JavaScript
// The web app's side of "keep on": reading the console engine's answer
|
|
// (GET /api/engine) and putting it into words. The engine itself, and that
|
|
// the two agree cheat for cheat, is test/engine.test.mjs. Also here: the
|
|
// SHN / MC4 export of cheats the way the app saves them.
|
|
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
import { keptOverview, keptStatus, parseEngineStatus } from '../src/lib/keep.ts';
|
|
import { emptyTable, exportCommunityTable } from '../src/lib/table.ts';
|
|
import { communityTableFromOwn, exportMc4, exportShn, parseAnyTable } from '../src/lib/trainer.ts';
|
|
|
|
const GAME = { titleId: 'PPSA99999', version: '01.000.000' };
|
|
const FILE = 'PPSA99999-01.000.000.cspector';
|
|
const answer = (extra = {}) => ({ running: true, game: { ...GAME, pid: 91 }, file: FILE, exact: true, cheats: [], ...extra });
|
|
const cheat = (name, state, detail = '') => ({ name, state, detail });
|
|
|
|
test('every field of the engine\'s answer is read, and answers from before a field existed still are', () => {
|
|
const full = parseEngineStatus(answer({ exact: false, busy: true, error: 'Only the first 256 cheats marked to keep on are used.', cheats: [cheat('A', 'on'), cheat('B', 'refused', 'why')] }));
|
|
assert.deepEqual(full, {
|
|
running: true,
|
|
game: { titleId: 'PPSA99999', version: '01.000.000', pid: 91 },
|
|
file: FILE,
|
|
exact: false,
|
|
busy: true,
|
|
crashGuard: false,
|
|
error: 'Only the first 256 cheats marked to keep on are used.',
|
|
cheats: [cheat('A', 'on'), cheat('B', 'refused', 'why')],
|
|
});
|
|
// An engine that says nothing about these: the file is taken to be the right one, nothing is held back, nothing is wrong.
|
|
const old = parseEngineStatus({ running: true, game: { ...GAME, pid: 1 }, file: FILE, cheats: [] });
|
|
assert.deepEqual([old.exact, old.busy, old.error], [true, false, '']);
|
|
assert.deepEqual([parseEngineStatus(answer({ exact: 'no' })).exact, parseEngineStatus(answer({ busy: 1 })).busy, parseEngineStatus(answer({ error: 5 })).error], [true, false, '']);
|
|
});
|
|
|
|
test('a refused cheat is never taken for a kept one of the same name', () => {
|
|
const status = parseEngineStatus(answer({ cheats: [cheat('Twin', 'refused', 'Only patches and held values can be kept on by the PS5.'), cheat('Twin', 'on'), cheat('Twin', 'mismatch', 'x')] }));
|
|
// The app's kept list leaves out what the engine refuses, so its first "Twin" is the engine's second.
|
|
assert.equal(keptStatus(status, GAME.titleId, 'Twin', 0).state, 'on');
|
|
assert.equal(keptStatus(status, GAME.titleId, 'Twin', 1).state, 'mismatch');
|
|
assert.equal(keptStatus(status, GAME.titleId, 'Twin', 2), null);
|
|
});
|
|
|
|
test('a cheat on only for the start set is never taken for a kept one of the same name', () => {
|
|
// The engine lists the person's own cheats in file order, kept and start set alike, then the saved tables' start set cheats.
|
|
const doc = answer({ cheats: [{ ...cheat('Twin', 'held', 'Held back'), start: true }, cheat('Twin', 'on'), { ...cheat('Twin', 'on'), start: true }, cheat('Twin', 'mismatch', 'x')] });
|
|
const status = parseEngineStatus(doc);
|
|
assert.equal(status.cheats[0].start, true);
|
|
assert.equal(status.cheats[1].start, undefined);
|
|
assert.equal(keptStatus(status, GAME.titleId, 'Twin', 0).state, 'on');
|
|
assert.equal(keptStatus(status, GAME.titleId, 'Twin', 1).state, 'mismatch');
|
|
assert.equal(keptStatus(status, GAME.titleId, 'Twin', 2), null);
|
|
// In the summary, start set cheats are not counted as kept on; one held back is still said.
|
|
const o = keptOverview(status, GAME, 2);
|
|
assert.match(o.text, /keeping 1 cheat on by itself/);
|
|
assert.match(o.text, /1 does not fit this version/);
|
|
assert.match(o.text, /switched on 1 cheat of this game's start set/);
|
|
assert.match(o.text, /did not switch "Twin" on/);
|
|
assert.equal(o.kind, 'warn');
|
|
// Only a start set, nothing kept.
|
|
const only = keptOverview(parseEngineStatus(answer({ cheats: [{ ...cheat('Fly', 'on'), start: true }] })), GAME, 0);
|
|
assert.equal(only.kind, 'info');
|
|
assert.match(only.text, /has no cheats to keep on for this game\. It switched on 1 cheat of this game's start set/);
|
|
});
|
|
|
|
test('the summary above the cheats: nothing to say, all well, and each thing that can be wrong', () => {
|
|
const over = (doc, kept = 1) => keptOverview(doc === null ? null : parseEngineStatus(doc), GAME, kept);
|
|
|
|
// Nothing kept, nothing to say, whatever the console is doing.
|
|
for (const doc of [null, answer(), answer({ file: '' }), { running: false, game: null, file: '', cheats: [] }, answer({ game: null, file: '' }), answer({ game: { titleId: 'PPSA11111', version: '1', pid: 2 } })]) {
|
|
assert.equal(over(doc, 0), null);
|
|
}
|
|
|
|
// All well.
|
|
let o = over(answer({ cheats: [cheat('A', 'on'), cheat('B', 'on')] }), 2);
|
|
assert.deepEqual(o, { kind: 'good', text: 'Your PS5 is keeping 2 cheats on by itself, with this page open or not.', lines: [] });
|
|
assert.match(over(answer({ cheats: [cheat('A', 'on')] })).text, /keeping 1 cheat on/);
|
|
|
|
// Still coming.
|
|
o = over(answer({ cheats: [cheat('A', 'on'), cheat('B', 'waiting', 'The pointer path leads nowhere yet')] }), 2);
|
|
assert.equal(o.kind, 'info');
|
|
assert.match(o.text, /keeping 1 cheat on by itself.* 1 is not on yet; its row says why\./);
|
|
o = over(answer({ cheats: [cheat('A', 'waiting'), cheat('B', 'waiting')] }), 2);
|
|
assert.equal(o.kind, 'info');
|
|
assert.match(o.text, /^Your PS5 is not keeping any cheat on right now\. 2 are not on yet; their rows say why\.$/);
|
|
|
|
// Cheats that did not go on.
|
|
o = over(answer({ cheats: [cheat('A', 'on'), cheat('B', 'mismatch'), cheat('C', 'mismatch'), cheat('D', 'error', 'x')] }), 4);
|
|
assert.equal(o.kind, 'warn');
|
|
assert.match(o.text, /2 do not fit this version of the game and were left off\. 1 has a problem\./);
|
|
assert.deepEqual(o.lines, []);
|
|
|
|
// The console is there but cannot work.
|
|
o = over({ running: false, game: null, file: '', cheats: [] });
|
|
assert.equal(o.kind, 'warn');
|
|
assert.match(o.text, /cannot reach its debugger.*Load ps5debug on the PS5 again/);
|
|
o = over(null);
|
|
assert.equal(o.kind, 'warn');
|
|
assert.match(o.text, /not answering.*send payload\/cheatspecter\.elf to the PS5 again/);
|
|
|
|
// It sees no game, or another one.
|
|
o = over(answer({ game: null, file: '' }));
|
|
assert.equal(o.kind, 'info');
|
|
assert.match(o.text, /does not see a game running right now/);
|
|
o = over(answer({ game: { titleId: 'PPSA11111', version: '1', pid: 2 } }));
|
|
assert.match(o.text, /sees a different game running \(PPSA11111\)/);
|
|
assert.equal(over(answer({ game: { titleId: 'ppsa99999', version: '01.000.000', pid: 2 }, cheats: [cheat('A', 'on')] })).kind, 'good', 'the case of a title ID is not a difference');
|
|
|
|
// The file: none, another version's, damaged, too long.
|
|
o = over(answer({ file: '', exact: undefined }));
|
|
assert.equal(o.kind, 'warn');
|
|
assert.match(o.lines[0], /no cheat file for this game on your PS5 yet.*made by itself as soon as you change one of your cheats/);
|
|
assert.doesNotMatch(o.lines[0], /Save to PS5/, 'saving to the PS5 is automatic now; there is no such button');
|
|
o = over(answer({ file: 'PPSA99999-00.900.000.cspector', exact: false, cheats: [cheat('A', 'on'), cheat('B', 'mismatch', 'saved for another version')] }), 2);
|
|
assert.equal(o.kind, 'warn');
|
|
assert.match(o.lines[0], /using the file saved for another version of this game \(PPSA99999-00\.900\.000\.cspector\).*Change any of your cheats here to give this version a file of its own/);
|
|
o = over(answer({ exact: false, cheats: [cheat('A', 'on')] }));
|
|
assert.match(o.lines[0], /The file on your PS5 \(PPSA99999-01\.000\.000\.cspector\) says it was made for another version/);
|
|
o = over(answer({ error: 'The cheat file for this game is damaged (it is not valid JSON), so no cheats were switched on. Save your cheats again from CheatSpecter.' }));
|
|
assert.equal(o.kind, 'warn');
|
|
assert.match(o.lines[0], /is damaged/);
|
|
assert.match(o.text, /has no cheats to keep on for this game/);
|
|
|
|
// Turned down, and holding back.
|
|
o = over(answer({ busy: true, cheats: [cheat('A', 'on'), cheat('Hook', 'refused', 'Only patches and held values can be kept on by the PS5.')] }));
|
|
assert.equal(o.kind, 'warn');
|
|
assert.deepEqual(o.lines, [
|
|
'"Hook" is marked to keep on but cannot be: Only patches and held values can be kept on by the PS5.',
|
|
'It is holding back searches of its own until the search running here is finished.',
|
|
]);
|
|
// A refused cheat is worth saying even when the app itself has nothing marked.
|
|
assert.equal(over(answer({ cheats: [cheat('Hook', 'refused', 'why')] }), 0).kind, 'warn');
|
|
assert.equal(over(answer({ busy: true, cheats: [cheat('A', 'on')] })).kind, 'good', 'holding back is not a problem');
|
|
});
|
|
|
|
test('SHN and MC4 exports take the same cheats the GoldHEN JSON export takes', () => {
|
|
const own = emptyTable('PPSA99999', '01.000.000', 'Specter Run');
|
|
const pattern = { kind: 'signature', pattern: '48 8B ?? FF 4B 28', offset: 3 };
|
|
own.entries.push(
|
|
// As the app saves a patch today: found by pattern, with the fixed position it was made at beside it.
|
|
{ id: '1', name: 'Proofed', type: 'patch', at: pattern, fallback: { kind: 'module', offset: '0x2040' }, on: '909090', off: 'FF4B28' },
|
|
{ id: '2', name: 'Plain', type: 'patch', at: { kind: 'module', offset: '1000' }, on: '90', off: '89' },
|
|
// Not expressible: inside a library, a pattern with no fixed position, a pattern whose fixed position is in a library.
|
|
{ id: '3', name: 'In a library', type: 'patch', at: { kind: 'module', offset: '0x40', module: 'libgame.prx' }, on: 'C3', off: '90' },
|
|
{ id: '4', name: 'Pattern only', type: 'patch', at: pattern, on: '909090', off: 'FF4B28' },
|
|
{ id: '5', name: 'Proofed in a library', type: 'patch', at: pattern, fallback: { kind: 'module', offset: '0x40', module: 'libgame.prx' }, on: 'C3', off: '90' },
|
|
{ id: '6', name: 'Second place in a library', type: 'patch', at: { kind: 'module', offset: '2000' }, on: '90', off: 'CC', more: [{ at: { kind: 'module', offset: '10', module: 'libgame.prx' }, on: '90', off: 'CC' }] },
|
|
{ id: '7', name: 'Second place proofed', type: 'patch', at: { kind: 'module', offset: '3000' }, on: '90', off: 'CC', more: [{ at: pattern, fallback: { kind: 'module', offset: '3008' }, on: '91', off: 'CD' }] },
|
|
);
|
|
const out = communityTableFromOwn(own);
|
|
assert.deepEqual(out.table.mods.map((m) => [m.name, m.patches.map((p) => p.offset)]), [['Proofed', [0x2040n]], ['Plain', [0x1000n]], ['Second place proofed', [0x3000n, 0x3008n]]]);
|
|
assert.deepEqual(out.left.map((l) => l.name), ['In a library', 'Pattern only', 'Proofed in a library', 'Second place in a library']);
|
|
for (const l of out.left) assert.match(l.why, /not at a fixed offset in the game's main executable/);
|
|
assert.deepEqual([out.included, out.skipped], [3, 4]);
|
|
|
|
const json = exportCommunityTable(own);
|
|
assert.deepEqual([json.included, json.skipped], [3, 4]);
|
|
for (const text of [exportShn(own).text, exportMc4(own).text]) {
|
|
const back = parseAnyTable(text);
|
|
assert.deepEqual(back.mods.map((m) => m.patches.map((p) => [p.offset, p.section])), parseAnyTable(json.text).mods.map((m) => m.patches.map((p) => [p.offset, p.section])));
|
|
assert.equal(back.titleId, 'PPSA99999');
|
|
assert.equal(back.version, '01.000.000');
|
|
}
|
|
});
|