Export and merge Infinite Craft saves
// ==UserScript==
// @name Infinite Craft Manager (Export + Merge)
// @namespace http://tampermonkey.net/
// @version 0.3
// @description Export and merge Infinite Craft saves
// @author BreadAndEggs
// @match *://neal.fun/infinite-craft/*
// @grant GM_addStyle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// --- GUI ---
const container = document.createElement("div");
container.id = "icdb-window";
container.innerHTML = `
<div id="icdb-header">Infinite Craft DB Manager</div>
<div id="icdb-body">
<button id="icdb-export">📤 Export Save</button>
<button id="icdb-merge">🔀 Merge Save</button>
<pre id="icdb-log" style="margin-top:8px; max-height:120px; overflow:auto;"></pre>
</div>
`;
document.body.appendChild(container);
GM_addStyle(`
#icdb-window {
position: fixed;
top: 100px;
left: 100px;
width: 320px;
height: 240px;
background: #1e1e1e;
color: #fff;
border: 2px solid #444;
border-radius: 6px;
z-index: 999999;
display: flex;
flex-direction: column;
resize: both;
overflow: auto;
font-family: Arial, sans-serif;
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
}
#icdb-header {
background: #333;
padding: 6px;
cursor: move;
font-weight: bold;
user-select: none;
border-bottom: 1px solid #444;
}
#icdb-body {
flex: 1;
padding: 8px;
}
#icdb-body button {
padding: 6px 10px;
margin-bottom: 4px;
cursor: pointer;
}
`);
// --- Draggable ---
(function makeDraggable() {
const header = container.querySelector("#icdb-header");
let offsetX = 0, offsetY = 0, dragging = false;
header.addEventListener("mousedown", e => {
dragging = true;
offsetX = e.clientX - container.offsetLeft;
offsetY = e.clientY - container.offsetTop;
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
});
function onMouseMove(e) {
if (!dragging) return;
container.style.left = (e.clientX - offsetX) + "px";
container.style.top = (e.clientY - offsetY) + "px";
}
function onMouseUp() {
dragging = false;
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
}
})();
// --- Logger ---
function log(msg) {
const logEl = document.getElementById("icdb-log");
logEl.textContent += msg + "\n";
logEl.scrollTop = logEl.scrollHeight;
}
// --- IndexedDB helpers ---
async function openDB() {
return new Promise((resolve, reject) => {
const req = indexedDB.open("infinite-craft");
req.onerror = () => reject(req.error);
req.onsuccess = () => resolve(req.result);
});
}
async function getItems(db) {
return new Promise((resolve, reject) => {
const tx = db.transaction("items", "readonly");
const store = tx.objectStore("items");
const result = [];
const cursorReq = store.openCursor();
cursorReq.onsuccess = e => {
const cursor = e.target.result;
if (cursor) {
result.push(cursor.value);
cursor.continue();
} else {
resolve(result);
}
};
cursorReq.onerror = () => reject(cursorReq.error);
});
}
async function putItem(db, item) {
return new Promise((resolve, reject) => {
const tx = db.transaction("items", "readwrite");
const store = tx.objectStore("items");
const req = store.put(item);
req.onsuccess = () => resolve();
req.onerror = () => reject(req.error);
});
}
// --- Export ---
async function exportSave() {
log("Exporting...");
const db = await openDB();
const items = await getItems(db);
log(`Got ${items.length} items.`);
const blob = new Blob([JSON.stringify(items, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "ic_save.json";
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
log("Export complete!");
}
// --- Merge ---
async function mergeSave(addition) {
log("Merging...");
const db = await openDB();
const current = await getItems(db);
const usedIds = new Set(current.map(x => x.id));
const textToId = new Map(current.map(x => [x.text, x.id]));
// Step 1: prepare temporary addition copy
const temp = addition.map(obj => ({ ...obj }));
// Step 2: assign "new" and "newid"
for (const item of temp) {
if ([0,1,2,3].includes(item.id)) {
item.new = false;
item.newid = item.id;
continue;
}
if (textToId.has(item.text)) {
item.new = false;
item.newid = textToId.get(item.text);
} else {
item.new = true;
let newId = 4;
while (usedIds.has(newId) || temp.some(x => x.newid === newId)) {
newId++;
}
item.newid = newId;
usedIds.add(newId);
}
}
// Step 3: remap recipes inside temp
for (const item of temp) {
if (!item.recipes) continue;
item.recipes = item.recipes.map(r =>
r.map(oldId => {
const ref = temp.find(x => x.id === oldId);
return ref ? ref.newid : oldId;
})
);
}
// Step 4: apply to current
for (const item of temp) {
if ([0,1,2,3].includes(item.id)) continue; // skip mains
if (item.new) {
const copy = { ...item };
delete copy.new;
delete copy.newid;
copy.id = item.newid;
await putItem(db, copy);
} else {
const existing = current.find(x => x.id === item.newid);
if (item.recipes) {
existing.recipes = existing.recipes || [];
for (const r of item.recipes) {
if (!existing.recipes.some(rr => JSON.stringify(rr) === JSON.stringify(r))) {
existing.recipes.push(r);
}
}
await putItem(db, existing);
}
}
}
log("Merge complete!");
}
// --- File loader for merge ---
function loadFileForMerge() {
const input = document.createElement("input");
input.type = "file";
input.accept = "application/json";
input.onchange = async e => {
const file = e.target.files[0];
if (!file) return;
log(`Loading ${file.name}...`);
const text = await file.text();
const data = JSON.parse(text);
await mergeSave(data);
};
input.click();
}
// --- Hook buttons ---
document.getElementById("icdb-export").addEventListener("click", exportSave);
document.getElementById("icdb-merge").addEventListener("click", loadFileForMerge);
})();