您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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); })();