diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index c75c12eda..b42b0bf20 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -37,5 +37,5 @@ jobs: - name: Lockfile lint run: | - [ -z "$(jq -r '.packages | to_entries[] | select((.key | contains("node_modules")) and (.value | has("resolved") and has("integrity") | not)) | .key' < package-lock.json)" ] + [ -z "$(jq -r '.packages | to_entries[] | select((.key | contains("node_modules")) and (.value.resolved | contains("git+ssh") | not) and (.value | has("resolved") and has("integrity") | not)) | .key' < package-lock.json)" ] working-directory: frontend diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 4a1dfff2d..9cb531096 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -37,8 +37,8 @@ lint: - prettier@3.7.4: packages: - "@trivago/prettier-plugin-sort-imports@6.0.0" - - "@vue/compiler-sfc@3.5.25" - - ruff@0.14.9 + - "@vue/compiler-sfc@3.5.26" + - ruff@0.14.10 - shellcheck@0.11.0 - shfmt@3.6.0 - taplo@0.10.0 diff --git a/frontend/assets/patcherjs/patcherjs.png b/frontend/assets/patcherjs/patcherjs.png new file mode 100644 index 000000000..51f320627 Binary files /dev/null and b/frontend/assets/patcherjs/patcherjs.png differ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 243c2ed95..cdbc43bdc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -20,6 +20,7 @@ "mitt": "^3.0.1", "pinia": "^3.0.1", "qrcode": "^1.5.4", + "rom-patcher": "github:marcrobledo/RomPatcher.js#v3.2.1", "semver": "^7.6.2", "socket.io-client": "^4.7.5", "tailwindcss": "^4.0.0", @@ -4547,7 +4548,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -5935,7 +5935,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -7836,6 +7835,25 @@ "fsevents": "~2.3.2" } }, + "node_modules/rom-patcher": { + "version": "3.0.0", + "resolved": "git+ssh://git@github.com/marcrobledo/RomPatcher.js.git#91e522e247f709e894761157ccba3189004d0859", + "dependencies": { + "chalk": "4.1.2", + "commander": "^11.0.0" + }, + "bin": { + "RomPatcher": "index.js" + } + }, + "node_modules/rom-patcher/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "engines": { + "node": ">=16" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -8366,7 +8384,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, diff --git a/frontend/package.json b/frontend/package.json index a4d8a952e..48c9c0fce 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -39,6 +39,7 @@ "mitt": "^3.0.1", "pinia": "^3.0.1", "qrcode": "^1.5.4", + "rom-patcher": "github:marcrobledo/RomPatcher.js#v3.2.1", "semver": "^7.6.2", "socket.io-client": "^4.7.5", "tailwindcss": "^4.0.0", diff --git a/frontend/public/assets/patcherjs/patcher.worker.js b/frontend/public/assets/patcherjs/patcher.worker.js new file mode 100644 index 000000000..514bc275e --- /dev/null +++ b/frontend/public/assets/patcherjs/patcher.worker.js @@ -0,0 +1,161 @@ +/* eslint-disable no-undef */ + +/// + +// Load all patcher scripts +let scriptsLoaded = false; + +async function loadScripts() { + if (scriptsLoaded) return; + + self.BinFile = + self.IPS = + self.UPS = + self.APS = + self.APSGBA = + self.BPS = + self.RUP = + self.PPF = + self.BDF = + self.PMSR = + self.VCDIFF = + null; + + try { + importScripts( + "/node_modules/rom-patcher/rom-patcher-js/modules/BinFile.js", + "/node_modules/rom-patcher/rom-patcher-js/modules/HashCalculator.js", + "/node_modules/rom-patcher/rom-patcher-js/modules/RomPatcher.format.aps_gba.js", + "/node_modules/rom-patcher/rom-patcher-js/modules/RomPatcher.format.aps_n64.js", + "/node_modules/rom-patcher/rom-patcher-js/modules/RomPatcher.format.bdf.js", + "/node_modules/rom-patcher/rom-patcher-js/modules/RomPatcher.format.bps.js", + "/node_modules/rom-patcher/rom-patcher-js/modules/RomPatcher.format.ips.js", + "/node_modules/rom-patcher/rom-patcher-js/modules/RomPatcher.format.pmsr.js", + "/node_modules/rom-patcher/rom-patcher-js/modules/RomPatcher.format.ppf.js", + "/node_modules/rom-patcher/rom-patcher-js/modules/RomPatcher.format.rup.js", + "/node_modules/rom-patcher/rom-patcher-js/modules/RomPatcher.format.ups.js", + "/node_modules/rom-patcher/rom-patcher-js/modules/RomPatcher.format.vcdiff.js", + "/node_modules/rom-patcher/rom-patcher-js/RomPatcher.js", + ); + scriptsLoaded = true; + return true; + } catch (error) { + throw new Error(`Failed to load patcher scripts: ${error.message}`); + } +} + +// Handle messages from main thread +self.addEventListener("message", async (e) => { + const { + type, + romData, + patchData, + romFileName, + patchFileName, + customFileName, + } = e.data; + + if (type === "PATCH") { + try { + // Load scripts if not already loaded + self.postMessage({ + type: "STATUS", + message: "Loading patcher libraries...", + }); + await loadScripts(); + + // Extract patch name without extension for custom suffix + const patchNameWithoutExt = patchFileName.replace(/\.[^.]+$/, ""); + + // Try to create BinFile from Uint8Array + self.postMessage({ type: "STATUS", message: "Reading ROM file..." }); + const romUint8 = new Uint8Array(romData); + + const romBin = await new Promise((resolve, reject) => { + try { + new BinFile(romUint8, (bf) => { + if (bf) { + bf.fileName = romFileName; + resolve(bf); + } else { + reject(new Error("Failed to create ROM BinFile")); + } + }); + } catch (err) { + reject(err); + } + }); + + self.postMessage({ type: "STATUS", message: "Reading patch file..." }); + const patchUint8 = new Uint8Array(patchData); + + const patchBin = await new Promise((resolve, reject) => { + try { + new BinFile(patchUint8, (bf) => { + if (bf) { + bf.fileName = patchFileName; + resolve(bf); + } else { + reject(new Error("Failed to create patch BinFile")); + } + }); + } catch (err) { + reject(err); + } + }); + + // Parse patch + self.postMessage({ type: "STATUS", message: "Parsing patch format..." }); + const patch = RomPatcher.parsePatchFile(patchBin); + if (!patch) { + throw new Error("Unsupported or invalid patch format."); + } + + // Apply patch + self.postMessage({ + type: "STATUS", + message: "Applying patch (this may take a moment)...", + }); + const patched = RomPatcher.applyPatch(romBin, patch, { + requireValidation: false, + fixChecksum: false, + outputSuffix: false, // Don't add default suffix + }); + + // Extract the patched binary data + const patchedData = patched._u8array || patched.u8array || patched.data; + if (!patchedData) { + throw new Error("Failed to extract patched ROM data"); + } + + // Create custom filename with patch name + const romBaseName = romFileName.replace(/\.[^.]+$/, ""); + const romExtension = romFileName.match(/\.[^.]+$/)?.[0] || ""; + const defaultFileName = `${romBaseName} (patched-${patchNameWithoutExt})${romExtension}`; + + // If custom filename provided, strip any extension and add ROM extension + let finalFileName; + if (customFileName && customFileName.trim()) { + const customBase = customFileName.trim().replace(/\.[^.]+$/, ""); + finalFileName = `${customBase}${romExtension}`; + } else { + finalFileName = defaultFileName; + } + + // Send back the result + self.postMessage( + { + type: "SUCCESS", + patchedData: patchedData.buffer, + fileName: finalFileName, + }, + [patchedData.buffer], + ); // Transfer ownership of ArrayBuffer + } catch (error) { + self.postMessage({ + type: "ERROR", + error: error.message || String(error), + }); + } + } +}); diff --git a/frontend/src/components/common/Navigation/MainAppBar.vue b/frontend/src/components/common/Navigation/MainAppBar.vue index 215dfaa56..57aa46801 100644 --- a/frontend/src/components/common/Navigation/MainAppBar.vue +++ b/frontend/src/components/common/Navigation/MainAppBar.vue @@ -8,6 +8,7 @@ import CollectionsBtn from "@/components/common/Navigation/CollectionsBtn.vue"; import CollectionsDrawer from "@/components/common/Navigation/CollectionsDrawer.vue"; import ConsoleModeBtn from "@/components/common/Navigation/ConsoleModeBtn.vue"; import HomeBtn from "@/components/common/Navigation/HomeBtn.vue"; +import PatcherBtn from "@/components/common/Navigation/PatcherBtn.vue"; import PlatformsBtn from "@/components/common/Navigation/PlatformsBtn.vue"; import PlatformsDrawer from "@/components/common/Navigation/PlatformsDrawer.vue"; import ScanBtn from "@/components/common/Navigation/ScanBtn.vue"; @@ -46,7 +47,8 @@ function collapse() { @@ -109,6 +111,7 @@ function collapse() {