Files
evolve-save-editor/index.js
Julian Tabel a5dd0d2397 Initial commit: Evolve save editor CLI
Node.js CLI tool that decodes Evolve game saves (LZString compressed),
edits resource values, and re-encodes back to clipboard.

Features:
- --max-resources: fill all capped resources to their max
- --set-crafted=N: set unlimited/crafted resources to a given value
- Reads from clipboard, stdin, or argument; writes back to clipboard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 12:01:15 +01:00

157 lines
4.0 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 maxResources(data) {
const changed = [];
for (const [key, res] of Object.entries(data.resource || {})) {
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) {
const changed = [];
for (const [key, res] of Object.entries(data.resource || {})) {
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 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;
for (const arg of args) {
if (arg.startsWith("--set-crafted=")) {
craftedValue = Number(arg.split("=")[1]);
} else if (arg.startsWith("--")) {
flags.add(arg);
} else {
positional.push(arg);
}
}
const hasAction = flags.has("--max-resources") || craftedValue !== null;
if (flags.has("--help") || !hasAction) {
console.log(`Usage: node index.js [options] [save-string]
Options:
--max-resources Set all capped resources to their max
--set-crafted=N Set all unlimited (crafted) resources to N
--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("--max-resources")) {
const changed = maxResources(data);
if (changed.length > 0) {
console.log(`Maxed ${changed.length} resources:`);
changed.forEach((line) => console.log(line));
} else {
console.log("No resources to max.");
}
}
if (craftedValue !== null) {
const changed = setCraftedResources(data, craftedValue);
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();