--list shows all resources grouped by type (capped, crafted, special, locked). --only=a,b,c filters --max-resources and --set-crafted to named resources. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
261 lines
7.4 KiB
JavaScript
261 lines
7.4 KiB
JavaScript
#!/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();
|