mirror of
https://github.com/TiagoVal97/cheatspecter.git
synced 2026-10-25 09:14:37 +01:00
- 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
135 lines
5.5 KiB
JavaScript
135 lines
5.5 KiB
JavaScript
// Builds the MCP server as a desktop extension: mcp/cheatspecter-<version>.mcpb
|
|
//
|
|
// An .mcpb file is a zip with a manifest and the server inside. Opening it in
|
|
// the Claude desktop app installs it; nothing has to be typed or configured
|
|
// beyond the PS5's address.
|
|
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import zlib from 'node:zlib';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { rolldown } from 'rolldown';
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
const { version } = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
const out = path.join(root, 'mcp/build');
|
|
fs.rmSync(out, { recursive: true, force: true });
|
|
fs.mkdirSync(path.join(out, 'server'), { recursive: true });
|
|
|
|
// One file with everything in it except the disassembler, which loads a
|
|
// WebAssembly file from beside itself and so is shipped as it is.
|
|
const bundle = await rolldown({
|
|
input: path.join(root, 'mcp/server.ts'),
|
|
platform: 'node',
|
|
external: ['iced-x86'],
|
|
transform: { define: { __VERSION__: JSON.stringify(version) } },
|
|
logLevel: 'warn',
|
|
});
|
|
await bundle.write({ file: path.join(out, 'server/index.mjs'), format: 'esm', codeSplitting: false });
|
|
await bundle.close();
|
|
|
|
const iced = path.join(root, 'node_modules/iced-x86');
|
|
const icedOut = path.join(out, 'server/node_modules/iced-x86');
|
|
fs.mkdirSync(icedOut, { recursive: true });
|
|
for (const f of ['package.json', 'iced_x86.js', 'iced_x86_bg.wasm', 'LICENSE.txt']) fs.copyFileSync(path.join(iced, f), path.join(icedOut, f));
|
|
|
|
const manifest = {
|
|
manifest_version: '0.3',
|
|
name: 'cheatspecter',
|
|
display_name: 'CheatSpecter',
|
|
version,
|
|
description: 'Find and make game cheats on a jailbroken PS5 running the CheatSpecter payload.',
|
|
long_description:
|
|
'Lets Claude work with a PS5 that has ps5debug-NG and the CheatSpecter payload loaded: see what game is running, search its memory for values, find the code that changes them, and save the result as a switch in the CheatSpecter app. For single-player games you own.',
|
|
author: { name: 'CheatSpecter' },
|
|
license: 'GPL-3.0-only',
|
|
server: {
|
|
type: 'node',
|
|
entry_point: 'server/index.mjs',
|
|
mcp_config: {
|
|
command: 'node',
|
|
args: ['${__dirname}/server/index.mjs'],
|
|
env: { CHEATSPECTER_HOST: '${user_config.host}' },
|
|
},
|
|
},
|
|
user_config: {
|
|
host: {
|
|
type: 'string',
|
|
title: 'PS5 address',
|
|
description: "Your PS5's IP address, as shown in the notification when the CheatSpecter payload starts (for example 192.168.1.50). Can be left empty and set later by asking Claude.",
|
|
required: false,
|
|
default: '',
|
|
},
|
|
},
|
|
tools: [],
|
|
compatibility: { runtimes: { node: '>=18.0.0' } },
|
|
};
|
|
|
|
// The tool list in the manifest is what the app shows before the server runs; take it from the source so the two cannot drift.
|
|
const source = fs.readFileSync(path.join(root, 'mcp/server.ts'), 'utf8');
|
|
for (const m of source.matchAll(/\n \{\n\s+name: '([a-z_]+)',\n\s+description:\s*\n?\s*(['"`])((?:\\.|(?!\2).)*)\2/g)) {
|
|
manifest.tools.push({ name: m[1], description: m[3].replace(/\\(['"`n])/g, (_, c) => (c === 'n' ? ' ' : c)).split('\n')[0].slice(0, 300) });
|
|
}
|
|
fs.writeFileSync(path.join(out, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
|
|
|
|
// ---- a zip, written by hand (deflate; no dependencies) ----
|
|
const crcTable = new Uint32Array(256).map((_, n) => {
|
|
let c = n;
|
|
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
return c >>> 0;
|
|
});
|
|
const crc32 = (buf) => {
|
|
let c = 0xffffffff;
|
|
for (const b of buf) c = crcTable[(c ^ b) & 0xff] ^ (c >>> 8);
|
|
return (c ^ 0xffffffff) >>> 0;
|
|
};
|
|
const files = [];
|
|
const walk = (dir, rel = '') => {
|
|
for (const name of fs.readdirSync(dir).sort()) {
|
|
const full = path.join(dir, name);
|
|
const r = rel ? `${rel}/${name}` : name;
|
|
if (fs.statSync(full).isDirectory()) walk(full, r);
|
|
else files.push({ name: r, data: fs.readFileSync(full) });
|
|
}
|
|
};
|
|
walk(out);
|
|
const parts = [];
|
|
const central = [];
|
|
let offset = 0;
|
|
for (const f of files) {
|
|
const name = Buffer.from(f.name, 'utf8');
|
|
const packed = zlib.deflateRawSync(f.data, { level: 9 });
|
|
const head = Buffer.alloc(30);
|
|
head.writeUInt32LE(0x04034b50, 0);
|
|
head.writeUInt16LE(20, 4);
|
|
head.writeUInt16LE(0x0800, 6); // names are UTF-8
|
|
head.writeUInt16LE(8, 8); // deflate
|
|
head.writeUInt16LE(0, 10);
|
|
head.writeUInt16LE(0x21, 12); // 1980-01-01, so the same input always gives the same file
|
|
head.writeUInt32LE(crc32(f.data), 14);
|
|
head.writeUInt32LE(packed.length, 18);
|
|
head.writeUInt32LE(f.data.length, 22);
|
|
head.writeUInt16LE(name.length, 26);
|
|
parts.push(head, name, packed);
|
|
const cd = Buffer.alloc(46);
|
|
cd.writeUInt32LE(0x02014b50, 0);
|
|
cd.writeUInt16LE(20, 4);
|
|
cd.writeUInt16LE(20, 6);
|
|
head.copy(cd, 8, 6, 30); // flags .. name length
|
|
cd.writeUInt32LE(offset, 42);
|
|
central.push(cd, name);
|
|
offset += head.length + name.length + packed.length;
|
|
}
|
|
const cdBuf = Buffer.concat(central);
|
|
const end = Buffer.alloc(22);
|
|
end.writeUInt32LE(0x06054b50, 0);
|
|
end.writeUInt16LE(files.length, 8);
|
|
end.writeUInt16LE(files.length, 10);
|
|
end.writeUInt32LE(cdBuf.length, 12);
|
|
end.writeUInt32LE(offset, 16);
|
|
|
|
for (const f of fs.readdirSync(path.join(root, 'mcp'))) if (/^cheatspecter-.*\.mcpb$/.test(f)) fs.rmSync(path.join(root, 'mcp', f));
|
|
const target = path.join(root, `mcp/cheatspecter-${version}.mcpb`);
|
|
fs.writeFileSync(target, Buffer.concat([...parts, cdBuf, end]));
|
|
console.log(`Built ${path.relative(root, target)} (${files.length} files, ${(fs.statSync(target).size / 1024).toFixed(0)} KB, ${manifest.tools.length} tools)`);
|