diff --git a/.gitignore b/.gitignore index f0663b0..e4814f8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ node_modules/ dist/ *.log .env -.env.local \ No newline at end of file +.env.local + +# Generated TTS save dumps (scripts/dump-save.mjs) +scripts/dumps/ \ No newline at end of file diff --git a/scripts/dump-save.mjs b/scripts/dump-save.mjs new file mode 100644 index 0000000..d2f2bf7 --- /dev/null +++ b/scripts/dump-save.mjs @@ -0,0 +1,82 @@ +import { createRequire } from 'node:module'; +import { mkdirSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const require = createRequire(import.meta.url); +const { deserialize } = require('../packages/tts/node_modules/bson'); + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const DEFAULT_MOD_ID = '2668570240'; +const modId = process.argv[2] ?? DEFAULT_MOD_ID; + +/** + * Resolve the authoritative `file_url` via the Steam Web API. The item page's + * embedded `file_url` can point to a preview image instead of the save, so the + * API is the reliable source. + */ +async function getFileUrlFromApi(id) { + const params = new URLSearchParams(); + params.append('itemcount', '1'); + params.append('publishedfileids[0]', id); + const res = await fetch( + 'https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/', + { method: 'POST', body: params }, + ); + if (!res.ok) { + console.log(` Steam API responded ${res.status}`); + return null; + } + const json = await res.json(); + const d = json?.response?.publishedfiledetails?.[0]; + if (!d?.file_url) { + console.log(' Steam API returned no file_url'); + return null; + } + return d.file_url; +} + +/** True if the bytes look like a BSON document whose length matches. */ +function looksLikeBson(buf) { + if (buf.length < 5) return false; + const len = buf.readInt32LE(0); + return len === buf.length; +} + +async function main() { + console.log(`Resolving file_url for mod ${modId} via Steam API...`); + const fileUrl = await getFileUrlFromApi(modId); + if (!fileUrl) { + console.error('Could not resolve a file_url'); + process.exit(1); + } + console.log(`file_url: ${fileUrl}`); + + const res = await fetch(fileUrl); + if (!res.ok) { + console.error(`Download failed ${res.status}`); + process.exit(1); + } + const buf = Buffer.from(await res.arrayBuffer()); + console.log(`downloaded ${buf.length} bytes`); + + if (!looksLikeBson(buf)) { + console.error( + `Not a BSON save (magic ${buf.slice(0, 4).toString('hex')})`, + ); + process.exit(1); + } + + const mod = deserialize(buf); + const outDir = join(__dirname, 'dumps'); + mkdirSync(outDir, { recursive: true }); + const outPath = join(outDir, `${modId}.json`); + writeFileSync(outPath, JSON.stringify(mod, null, 2)); + console.log(`wrote ${outPath}`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); \ No newline at end of file