#!/usr/bin/env node "use strict"; const LZString = require("lz-string"); const { execSync } = require("child_process"); function decodeSave(compressed) { const json = LZString.decompressFromBase64(compressed); if (!json) throw new Error("Failed to decompress save data"); return JSON.parse(json); } function encodeSave(data) { return LZString.compressToBase64(JSON.stringify(data)); } function matchesFilter(key, res, filter) { if (!filter) return true; const name = (res.name || key).toLowerCase(); return filter.has(key.toLowerCase()) || filter.has(name); } function maxResources(data, filter) { const changed = []; for (const [key, res] of Object.entries(data.resource || {})) { if (!matchesFilter(key, res, filter)) continue; const max = res.max ?? 0; const amount = res.amount ?? 0; if (max > 0 && amount < max) { const old = amount; res.amount = max; changed.push(` ${res.name || key}: ${Math.floor(old)} -> ${max}`); } } return changed; } function setCraftedResources(data, value, filter) { const changed = []; for (const [key, res] of Object.entries(data.resource || {})) { if (!matchesFilter(key, res, filter)) continue; if (res.max === -1 && (res.amount ?? 0) < value) { const old = res.amount ?? 0; res.amount = value; changed.push(` ${res.name || key}: ${Math.floor(old)} -> ${value}`); } } return changed; } function listResources(data) { const resources = Object.entries(data.resource || {}); if (resources.length === 0) { console.log("No resources found in save."); return; } const capped = []; const crafted = []; const special = []; const locked = []; for (const [key, res] of resources) { const name = res.name || key; const amount = res.amount ?? 0; const max = res.max ?? 0; if (max > 0) { capped.push(` ${name} (${key}): ${Math.floor(amount)} / ${max}`); } else if (max === -1) { crafted.push(` ${name} (${key}): ${Math.floor(amount)}`); } else if (max === -2) { special.push(` ${name} (${key}): ${Math.floor(amount)}`); } else { locked.push(` ${name} (${key})`); } } if (capped.length > 0) { console.log(`Capped resources (${capped.length}):`); capped.forEach((l) => console.log(l)); } if (crafted.length > 0) { console.log(`Crafted/unlimited resources (${crafted.length}):`); crafted.forEach((l) => console.log(l)); } if (special.length > 0) { console.log(`Special resources (${special.length}):`); special.forEach((l) => console.log(l)); } if (locked.length > 0) { console.log(`Locked/unavailable resources (${locked.length}):`); locked.forEach((l) => console.log(l)); } } function setGeology(data) { // Max bonus depends on White Hole achievement: top = 30 + level * 5 const whLevel = data.stats?.achieve?.whitehole?.l ?? 0; const top = 30 + whLevel * 5; const maxBonus = (top - 10) / 100; const geology = { Copper: maxBonus, Iron: maxBonus, Aluminium: maxBonus, Titanium: maxBonus, }; data.city.geology = geology; const entries = Object.entries(geology) .map(([k, v]) => ` ${k}: +${(v * 100).toFixed(0)}%`) .join("\n"); return `Set 4 geology bonuses (max +${(maxBonus * 100).toFixed(0)}% from White Hole level ${whLevel}):\n${entries}`; } function readFromClipboard() { try { return execSync("pbpaste", { encoding: "utf-8" }).trim(); } catch { try { return execSync("xclip -selection clipboard -o", { encoding: "utf-8" }).trim(); } catch { return null; } } } function copyToClipboard(text) { try { execSync("pbcopy", { input: text }); return true; } catch { try { execSync("xclip -selection clipboard", { input: text }); return true; } catch { return false; } } } function main() { const args = process.argv.slice(2); const flags = new Set(); const positional = []; let craftedValue = null; let onlyFilter = null; for (const arg of args) { if (arg.startsWith("--set-crafted=")) { craftedValue = Number(arg.split("=")[1]); } else if (arg.startsWith("--only=")) { onlyFilter = new Set(arg.slice("--only=".length).split(",").map(s => s.trim().toLowerCase())); } else if (arg.startsWith("--")) { flags.add(arg); } else { positional.push(arg); } } const hasAction = flags.has("--max-resources") || flags.has("--max-time") || flags.has("--max-geology") || flags.has("--list") || craftedValue !== null; if (flags.has("--help") || !hasAction) { console.log(`Usage: node index.js [options] [save-string] Options: --list List all resources in the save with current values --max-resources Set all capped resources to their max --set-crafted=N Set all unlimited (crafted) resources to N --only=a,b,c Only affect listed resources (comma-separated names or keys) --max-geology Set 4 geology resource bonuses at max values --max-time Set accelerated time (AT) to 8 hours --no-copy Print result instead of copying to clipboard --help Show this help Save string can be passed as argument, piped via stdin, or read from clipboard.`); process.exit(flags.has("--help") ? 0 : 1); } let input; if (positional.length > 0) { input = positional[0].trim(); } else if (!process.stdin.isTTY) { try { input = require("fs").readFileSync("/dev/stdin", "utf-8").trim(); } catch { // stdin not readable (e.g. piped but empty), fall through to clipboard } } if (!input) { const clip = readFromClipboard(); if (clip) { console.log("Reading save data from clipboard..."); input = clip; } else { console.error("Error: No save data provided. Pass as argument, pipe via stdin, or copy to clipboard."); process.exit(1); } } const data = decodeSave(input); if (flags.has("--list")) { listResources(data); if (!flags.has("--max-resources") && !flags.has("--max-geology") && !flags.has("--max-time") && craftedValue === null) { process.exit(0); } } if (flags.has("--max-resources")) { const changed = maxResources(data, onlyFilter); if (changed.length > 0) { console.log(`Maxed ${changed.length} resources:`); changed.forEach((line) => console.log(line)); } else { console.log("No resources to max."); } } if (flags.has("--max-geology")) { console.log(setGeology(data)); } if (flags.has("--max-time")) { const MAX_AT = 11520; // game adjusts down to 8 hours on load const old = data.settings?.at ?? 0; if (old < MAX_AT) { data.settings.at = MAX_AT; console.log(`Accelerated time: ${old} -> ${MAX_AT} (adjusts to 8 hours on load)`); } else { console.log(`Accelerated time already at ${old} (>= max).`); } } if (craftedValue !== null) { const changed = setCraftedResources(data, craftedValue, onlyFilter); if (changed.length > 0) { console.log(`Set ${changed.length} crafted resources to ${craftedValue}:`); changed.forEach((line) => console.log(line)); } else { console.log("No crafted resources to set."); } } const result = encodeSave(data); if (flags.has("--no-copy")) { console.log(result); } else if (copyToClipboard(result)) { console.log("Edited save copied to clipboard."); } else { console.error("Could not copy to clipboard. Output:"); console.log(result); } } main();