├── .eslintrc.json ├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── components ├── patchInfo.tsx └── patchList.tsx ├── lib └── ips.ts ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── api │ ├── getPatchFile.ts │ └── patches.ts └── index.tsx ├── public ├── favicon.ico └── patches │ ├── ips │ ├── AlleywayDX.ips │ ├── SMDX2.ips │ ├── WarioLandDX.ips │ ├── ZeldaToRedux.ips │ ├── frogBellTolls.ips │ ├── kirbyDXFluffy.ips │ ├── pokemonBlue.ips │ ├── pokemonBlueColored.ips │ ├── pokemonRed.ips │ ├── pokemonRedColored.ips │ ├── zeldaDX-France.ips │ ├── zeldaDX-Germany.ips │ ├── zeldaDX-Japan.ips │ ├── zeldaDX-USA-rev1.ips │ ├── zeldaDX-USA-rev2.ips │ └── zeldaDX-USA.ips │ ├── permaPocket.json │ └── pocket.js ├── scrape.mjs ├── styles ├── Home.module.css └── globals.css ├── tsconfig.json └── types.d.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Create patches.json 2 | 3 | on: 4 | # schedule: 5 | # - cron: '0 * * * *' 6 | 7 | # Allows you to run this workflow manually from the Actions tab 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-node@v2 17 | with: 18 | node-version: '16' 19 | cache: 'npm' 20 | - run: npm install 21 | 22 | - name: Create patches.json 23 | run: node scrape.mjs 24 | 25 | - name: Add & Commit 26 | uses: EndBug/add-and-commit@v8.0.1 27 | with: 28 | add: 'public/patches/pocket.js' 29 | 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jon Abrams 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Retro Patcher 2 | 3 | Currently is live at [retropatcher.jonabrams.com](https://retropatcher.jonabrams.com/) 4 | 5 | This is a web app for patching retro games more easily. The user supplies the ROM file, the app finds applicable patches, the user selects them, then downloads* the patched ROM. 6 | 7 | \* The patching/downloading is all done client-side. 8 | 9 | ## Status 10 | 11 | - Supports .gb to .pocket patches. 12 | 13 | ## Getting Started 14 | 15 | First, run the development server: 16 | 17 | ```bash 18 | npm run dev 19 | ``` 20 | 21 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 22 | 23 | ## Created by 24 | 25 | [Jon Abrams](https://threads.net/@jon.abrams) 26 | -------------------------------------------------------------------------------- /components/patchInfo.tsx: -------------------------------------------------------------------------------- 1 | import { Patch } from "../pages/api/patches"; 2 | import styles from "../styles/Home.module.css"; 3 | 4 | interface PatchInfoProps { 5 | patch: Patch; 6 | applying?: boolean; 7 | showMd5?: boolean; 8 | showDownload?: boolean; 9 | onApply?: () => {}; 10 | } 11 | 12 | export function PatchInfo({ 13 | patch, 14 | applying, 15 | showDownload, 16 | showMd5, 17 | onApply, 18 | }: PatchInfoProps) { 19 | return ( 20 |
21 |
22 | {patch.name} by{" "} 23 | 24 | {patch.authorName} 25 | 26 |
27 | {showMd5 &&
ROM MD5: {patch.md5}
} 28 | {showDownload && Download patch} 29 | {onApply && ( 30 | 37 | )} 38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /components/patchList.tsx: -------------------------------------------------------------------------------- 1 | import { useState, ChangeEventHandler, MouseEvent } from "react"; 2 | import { Patch } from "../pages/api/patches"; 3 | import { PatchInfo } from "./patchInfo"; 4 | import styles from "../styles/Home.module.css"; 5 | 6 | type Results = { status: string } | Patch[]; 7 | 8 | export function PatchList() { 9 | const [searchTerm, setSearchTerm] = useState(""); 10 | const [patches, setPatches] = useState([]); 11 | const [errorOutput, setErrorOutput] = useState(""); 12 | 13 | const handleSearchTerm: ChangeEventHandler = async ( 14 | event 15 | ) => { 16 | const term = event.target.value; 17 | setSearchTerm(term); 18 | if (term.length < 3) { 19 | setPatches([]); 20 | setErrorOutput(""); 21 | } else { 22 | const results = await fetch( 23 | `/api/patches?q=${encodeURIComponent(term)}` 24 | ).then((r) => r.json()); 25 | setResults(results); 26 | } 27 | }; 28 | 29 | const handleStartChar = async (event: MouseEvent, char: string) => { 30 | event.preventDefault(); 31 | setSearchTerm(""); 32 | const results = (await fetch( 33 | `/api/patches?startsWith=${encodeURIComponent(char.toLowerCase())}` 34 | ).then((r) => r.json())) as Results; 35 | setResults(results); 36 | }; 37 | 38 | const setResults = (results: Results) => { 39 | if (Array.isArray(results)) { 40 | setErrorOutput(""); 41 | setPatches(results); 42 | } else { 43 | setErrorOutput(results.status); 44 | setPatches([]); 45 | } 46 | }; 47 | 48 | const alphaList = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ".split(""); 49 | 50 | return ( 51 |
52 | 59 |
60 | {alphaList.map((char) => ( 61 | handleStartChar(e, char)} 65 | > 66 | {char} 67 | 68 | ))} 69 |
70 | {errorOutput &&
{errorOutput}
} 71 | {patches.map((patch) => ( 72 | 78 | ))} 79 |
80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /lib/ips.ts: -------------------------------------------------------------------------------- 1 | export function isValidIPS(buffer: Uint8Array): boolean { 2 | const headerValid = 3 | String.fromCharCode.apply(null, Array.from(buffer.slice(0, 5))) === "PATCH"; 4 | const eofValid = 5 | String.fromCharCode.apply(null, Array.from(buffer.slice(-3))) === "EOF"; 6 | return headerValid && eofValid; 7 | } 8 | 9 | function isEOF(buffer: Uint8Array): boolean { 10 | return ( 11 | buffer.length === 0 || 12 | String.fromCharCode.apply(null, Array.from(buffer.slice(0, 3))) === "EOF" 13 | ); 14 | } 15 | 16 | export function applyPatch(romFile: Uint8Array, patch: Uint8Array): Uint8Array { 17 | if (!isValidIPS(patch)) throw new Error("Invalid patch"); 18 | 19 | const patched = Array.from(romFile); 20 | let index = 5; 21 | 22 | while (!isEOF(patch.slice(index))) { 23 | let offset = 24 | (patch[index] << 16) + (patch[index + 1] << 8) + patch[index + 2]; 25 | index += 3; 26 | let len = (patch[index] << 8) + patch[index + 1]; 27 | index += 2; 28 | if (len) { 29 | for (let i = 0; i < len; i++) { 30 | patched[offset + i] = patch[index + i]; 31 | } 32 | index += len; 33 | } else { 34 | len = (patch[index] << 8) + patch[index + 1]; 35 | const val = patch[index + 2]; 36 | index += 3; 37 | for (let i = 0; i < len; i++) { 38 | patched[offset + i] = val; 39 | } 40 | } 41 | } 42 | return new Uint8Array(patched); 43 | } 44 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "retropatcher", 3 | "engines": { 4 | "node": ">=18" 5 | }, 6 | "scripts": { 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint", 11 | "scrape": "node ./scrape.mjs" 12 | }, 13 | "dependencies": { 14 | "file-saver": "^2.0.5", 15 | "javascript-time-ago": "^2.3.11", 16 | "js-md5": "^0.7.3", 17 | "jszip": "^3.10.1", 18 | "next": "^12.1.4", 19 | "react": "^18.0.0", 20 | "react-dom": "^18.0.0", 21 | "react-time-ago": "^7.1.9" 22 | }, 23 | "devDependencies": { 24 | "@types/file-saver": "^2.0.5", 25 | "@types/js-md5": "^0.4.3", 26 | "@types/node": "17.0.8", 27 | "@types/node-fetch": "^3.0.3", 28 | "@types/react": "17.0.38", 29 | "eslint": "8.8", 30 | "eslint-config-next": "12.0.8", 31 | "node-fetch": "^3.2.10", 32 | "typescript": "4.5.4" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import type { AppProps } from "next/app"; 3 | import TimeAgo from "javascript-time-ago"; 4 | import en from "javascript-time-ago/locale/en.json"; 5 | 6 | TimeAgo.addDefaultLocale(en); 7 | 8 | function RetroPatcher({ Component, pageProps }: AppProps) { 9 | return ; 10 | } 11 | 12 | export default RetroPatcher; 13 | -------------------------------------------------------------------------------- /pages/api/getPatchFile.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import fetch from "node-fetch"; 3 | import { patches } from "../../public/patches/pocket"; 4 | 5 | export default async function handler( 6 | req: NextApiRequest, 7 | res: NextApiResponse 8 | ) { 9 | console.log(req.headers.host); 10 | let url = req.query.url; 11 | if (typeof url !== "string" || !patches.some((p) => p.downloadUrl === url)) { 12 | return res.status(404).send("No patches found."); 13 | } 14 | 15 | const result = await fetch(url); 16 | if (result.status !== 200) { 17 | res.status(result.status).send(`Failed to get patch file from ${url}`); 18 | return; 19 | } 20 | const fileBuffer = await result.arrayBuffer(); 21 | res.send(Buffer.from(fileBuffer)); 22 | } 23 | -------------------------------------------------------------------------------- /pages/api/patches.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import { patches } from "../../public/patches/pocket"; 3 | 4 | export type Patch = { 5 | md5: string; 6 | name: string; 7 | authorName: string; 8 | originalUrl: string; 9 | downloadUrl: string; 10 | outputFilename?: string; 11 | extension?: string; 12 | }; 13 | 14 | export type ApiError = { 15 | status: string; 16 | }; 17 | 18 | export default function handler( 19 | req: NextApiRequest, 20 | res: NextApiResponse 21 | ) { 22 | let results = [] as Patch[]; 23 | if (req.body.md5s) { 24 | results = patches.filter((patch: Patch) => 25 | req.body.md5s.includes(patch.md5) 26 | ); 27 | } else if (!Array.isArray(req.query.q) && req.query.q?.length >= 3) { 28 | const q = req.query.q.toLowerCase(); 29 | results = patches.filter( 30 | (patch: Patch) => 31 | patch.name.toLowerCase().replace(/é/g, "e").indexOf(q) > -1 || 32 | patch.authorName.toLowerCase().indexOf(q) > -1 33 | ); 34 | } else if (req.query.startsWith?.[0].match(/[#a-z]/)) { 35 | const startsWith = 36 | req.query.startsWith[0] === "#" 37 | ? /^[^a-zA-z]/ 38 | : new RegExp(`^${req.query.startsWith[0]}`); 39 | results = patches.filter((patch: Patch) => 40 | patch.name.toLowerCase().match(startsWith) 41 | ); 42 | } 43 | 44 | res.json(results); 45 | } 46 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import Head from "next/head"; 3 | import { useEffect, useRef, useState, DragEventHandler } from "react"; 4 | import md5 from "js-md5"; 5 | import { saveAs } from "file-saver"; 6 | import ReactTimeAgo from "react-time-ago"; 7 | import JSZip from "jszip"; 8 | import { Patch } from "./api/patches"; 9 | import { PatchList } from "../components/patchList"; 10 | import { PatchInfo } from "../components/patchInfo"; 11 | import { applyPatch } from "../lib/ips"; 12 | import styles from "../styles/Home.module.css"; 13 | import { updated } from "../public/patches/pocket"; 14 | 15 | const Home: NextPage = () => { 16 | const fileInputRef = useRef(null); 17 | const [showTimeAgo, setShowTimeAgo] = useState(false); 18 | const [filenames, setFilenames] = useState([]); 19 | const [filesBytes, setFilesBytes] = useState([]); 20 | const [patchInfo, setPatchInfo] = useState(null); 21 | const [errorOutput, setErrorOutput] = useState(""); 22 | const [applying, setApplying] = useState(false); 23 | const [dragOver, setDragOver] = useState(false); 24 | 25 | useEffect(() => { 26 | setShowTimeAgo(true); 27 | }, []); 28 | 29 | useEffect(() => { 30 | if (filesBytes.length === 0) return; 31 | const md5s = filesBytes.map((f) => md5(f)); 32 | fetch(`/api/patches`, { 33 | method: "POST", 34 | body: JSON.stringify({ md5s }), 35 | headers: { 36 | "Content-Type": "application/json", 37 | }, 38 | }) 39 | .then((res) => res.json()) 40 | .then((patchesFromServer) => { 41 | if (patchesFromServer.status) { 42 | setErrorOutput(patchesFromServer.status); 43 | setPatchInfo(null); 44 | return; 45 | } 46 | setPatchInfo(patchesFromServer); 47 | setErrorOutput(""); 48 | }); 49 | }, [filesBytes]); 50 | 51 | const savePatchedFile = ( 52 | filename: string, 53 | patch: Patch, 54 | patchedBytes: Uint8Array, 55 | zip?: JSZip 56 | ) => { 57 | const blob = new Blob([patchedBytes]); 58 | const matched = filename.match(/(.*)\.gbc?/); 59 | if (!matched) return; 60 | let outputFilename = `${patch.name 61 | .replace(/[^ \w$%\-!#$%&'()@^_`{}~]/g, "") 62 | .slice(0, 56)}.pocket`; 63 | if (patch.outputFilename) { 64 | outputFilename = patch.outputFilename; 65 | } 66 | if (zip) { 67 | zip.file(outputFilename, blob); 68 | } else { 69 | saveAs(blob, outputFilename); 70 | } 71 | }; 72 | 73 | const handleFilesChosen = async ({ 74 | target, 75 | }: { 76 | target: HTMLInputElement; 77 | }) => { 78 | setPatchInfo("loading"); 79 | if (!target.files || !target.files.length) { 80 | setPatchInfo(null); 81 | setErrorOutput(""); 82 | setFilenames([]); 83 | setFilesBytes([]); 84 | return; 85 | } 86 | const files = Array.from(target.files); 87 | await handleFiles(files); 88 | }; 89 | 90 | const handleFiles = async (files: File[]) => { 91 | setFilenames(files.map((f) => f.name)); 92 | const newFilesBytes: Promise[] = files.map((file) => { 93 | return new Promise((resolve) => { 94 | const reader = new FileReader(); 95 | reader.onload = () => { 96 | resolve(new Uint8Array(reader.result as ArrayBuffer)); 97 | }; 98 | reader.readAsArrayBuffer(file); 99 | }); 100 | }); 101 | setFilesBytes(await Promise.all(newFilesBytes)); 102 | setApplying(false); 103 | }; 104 | 105 | const handleApplyPatch = async (patch: Patch, zip?: JSZip) => { 106 | const fbIndex = filesBytes.findIndex((fb) => md5(fb) === patch.md5); 107 | const fileBytes = filesBytes[fbIndex]; 108 | const filename = filenames[fbIndex]; 109 | if (!fileBytes) { 110 | setErrorOutput("Patch and file md5s don't match."); 111 | return; 112 | } else { 113 | setErrorOutput(""); 114 | } 115 | let url = patch.downloadUrl; 116 | if (url[0] !== "/") { 117 | url = `/api/getPatchFile?url=${encodeURIComponent(patch.downloadUrl)}`; 118 | } 119 | const result = await fetch(url); 120 | if (result.status !== 200) { 121 | const resultText = await result.text(); 122 | setErrorOutput(resultText); 123 | return; 124 | } 125 | const patchIps = await result.arrayBuffer(); 126 | if (!patchIps) return; 127 | savePatchedFile( 128 | filename, 129 | patch, 130 | applyPatch(fileBytes, new Uint8Array(patchIps)), 131 | zip 132 | ); 133 | }; 134 | 135 | const handleApplyAllPatches = async () => { 136 | const zip = new JSZip(); 137 | for (const patch of patchInfo as Patch[]) { 138 | await handleApplyPatch(patch, zip); 139 | } 140 | const blob = await zip.generateAsync({ type: "blob" }); 141 | saveAs(blob, "patchedpockets.zip"); 142 | }; 143 | 144 | const wrapApplying = (func: { (): Promise }) => { 145 | return async () => { 146 | setApplying(true); 147 | await func(); 148 | setApplying(false); 149 | }; 150 | }; 151 | 152 | const triggerFileInput = () => { 153 | if (!fileInputRef?.current) return; 154 | fileInputRef.current.click(); 155 | }; 156 | 157 | const notFound = (() => { 158 | if (!Array.isArray(patchInfo)) return []; 159 | const roms = []; 160 | for (let i = 0; i < filesBytes.length; i++) { 161 | const fmd5 = md5(filesBytes[i]); 162 | if (!patchInfo.some((p) => p.md5 === fmd5)) { 163 | roms.push({ filename: filenames[i], md5: fmd5 }); 164 | } 165 | } 166 | return roms; 167 | })(); 168 | 169 | const handleDrop: DragEventHandler = (e) => { 170 | e.preventDefault(); 171 | if (e.dataTransfer?.files) { 172 | handleFiles(Array.from(e.dataTransfer.files)); 173 | } 174 | }; 175 | 176 | const handleDragOver: DragEventHandler = (e) => { 177 | e.preventDefault(); 178 | }; 179 | 180 | const handleDragEnter: DragEventHandler = (e) => { 181 | e.preventDefault(); 182 | setDragOver(true); 183 | }; 184 | 185 | const handleDragLeave: DragEventHandler = (e) => { 186 | e.preventDefault(); 187 | setDragOver(false); 188 | }; 189 | 190 | return ( 191 |
198 | 199 | Retro Patcher 200 | 204 | 208 | 209 | 210 | 211 |
212 |

Retro Patcher

213 |

214 | Easily patch your GB/GBC roms to run on an{" "} 215 | 216 | Analogue Pocket 217 | 218 | 's microSD card (place them in a directory called "GB 219 | Studio"). 220 |

221 | 222 | 230 | 238 | 239 |
240 | You can also drag and drop files onto the page. 241 |
242 | 243 | {typeof patchInfo === "string" && ( 244 |
Loading…
245 | )} 246 | 247 | {filesBytes.length > 0 && 248 | Array.isArray(patchInfo) && 249 | patchInfo.length > 0 && ( 250 |
251 |
252 | {patchInfo.length}{" "} 253 | {patchInfo.length === 1 ? "Patch" : "Patches"} found: 254 |
255 | {patchInfo.length > 1 && ( 256 |
257 | 264 |
265 | )} 266 | {patchInfo.map((patch) => ( 267 | await handleApplyPatch(patch) 272 | )} 273 | applying={applying} 274 | /> 275 | ))} 276 |
277 | )} 278 | {notFound.length > 0 && ( 279 |
280 |
281 | Patches not found for these ROMs: 282 |
283 | {notFound.map((rom) => ( 284 |
285 |
Filename: {rom.filename}
286 |
MD5: {rom.md5}
287 |
288 | ))} 289 |
290 | Want a patch made? Let the community know via{" "} 291 | 296 | The Wishing Well 297 | 298 |
299 |
300 | )} 301 | {errorOutput &&
{errorOutput}
} 302 |
303 | Patch list updated:{" "} 304 | {showTimeAgo ? ( 305 | 306 | ) : ( 307 | "" 308 | )} 309 |
310 |

311 | { 312 | 'Note: This site will let you "download" the \ 313 | patched ROM, but the patching and downloading all occurs on \ 314 | your device, no copyrighted content is sent or received. No warranty provided, use at your own risk. \ 315 | All patches listed ' 316 | } 317 | 322 | here 323 | 324 | . 325 |

326 | 327 |
328 | 329 |
330 |
331 | Created by Jon Abrams{" "} 332 | [ 333 | 338 | github 339 | 340 | ] 341 |
342 |
343 | Patches created by JoseJX, BestPig, Infinest, jsky0, Trey Turner, 344 | reminon, and r0r0. 345 |
346 |
347 |
348 | ); 349 | }; 350 | 351 | export default Home; 352 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonAbrams/retropatcher/14ebe6ccaeaa29d480d498d6c09fb2d8be4db57c/public/favicon.ico -------------------------------------------------------------------------------- /public/patches/ips/AlleywayDX.ips: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonAbrams/retropatcher/14ebe6ccaeaa29d480d498d6c09fb2d8be4db57c/public/patches/ips/AlleywayDX.ips -------------------------------------------------------------------------------- /public/patches/ips/SMDX2.ips: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonAbrams/retropatcher/14ebe6ccaeaa29d480d498d6c09fb2d8be4db57c/public/patches/ips/SMDX2.ips -------------------------------------------------------------------------------- /public/patches/ips/WarioLandDX.ips: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonAbrams/retropatcher/14ebe6ccaeaa29d480d498d6c09fb2d8be4db57c/public/patches/ips/WarioLandDX.ips -------------------------------------------------------------------------------- /public/patches/ips/ZeldaToRedux.ips: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonAbrams/retropatcher/14ebe6ccaeaa29d480d498d6c09fb2d8be4db57c/public/patches/ips/ZeldaToRedux.ips -------------------------------------------------------------------------------- /public/patches/ips/frogBellTolls.ips: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonAbrams/retropatcher/14ebe6ccaeaa29d480d498d6c09fb2d8be4db57c/public/patches/ips/frogBellTolls.ips -------------------------------------------------------------------------------- /public/patches/ips/kirbyDXFluffy.ips: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonAbrams/retropatcher/14ebe6ccaeaa29d480d498d6c09fb2d8be4db57c/public/patches/ips/kirbyDXFluffy.ips -------------------------------------------------------------------------------- /public/patches/ips/pokemonBlue.ips: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonAbrams/retropatcher/14ebe6ccaeaa29d480d498d6c09fb2d8be4db57c/public/patches/ips/pokemonBlue.ips -------------------------------------------------------------------------------- /public/patches/ips/pokemonBlueColored.ips: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonAbrams/retropatcher/14ebe6ccaeaa29d480d498d6c09fb2d8be4db57c/public/patches/ips/pokemonBlueColored.ips -------------------------------------------------------------------------------- /public/patches/ips/pokemonRed.ips: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonAbrams/retropatcher/14ebe6ccaeaa29d480d498d6c09fb2d8be4db57c/public/patches/ips/pokemonRed.ips -------------------------------------------------------------------------------- /public/patches/ips/pokemonRedColored.ips: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonAbrams/retropatcher/14ebe6ccaeaa29d480d498d6c09fb2d8be4db57c/public/patches/ips/pokemonRedColored.ips -------------------------------------------------------------------------------- /public/patches/ips/zeldaDX-France.ips: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonAbrams/retropatcher/14ebe6ccaeaa29d480d498d6c09fb2d8be4db57c/public/patches/ips/zeldaDX-France.ips -------------------------------------------------------------------------------- /public/patches/ips/zeldaDX-Germany.ips: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonAbrams/retropatcher/14ebe6ccaeaa29d480d498d6c09fb2d8be4db57c/public/patches/ips/zeldaDX-Germany.ips -------------------------------------------------------------------------------- /public/patches/ips/zeldaDX-Japan.ips: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonAbrams/retropatcher/14ebe6ccaeaa29d480d498d6c09fb2d8be4db57c/public/patches/ips/zeldaDX-Japan.ips -------------------------------------------------------------------------------- /public/patches/ips/zeldaDX-USA-rev1.ips: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonAbrams/retropatcher/14ebe6ccaeaa29d480d498d6c09fb2d8be4db57c/public/patches/ips/zeldaDX-USA-rev1.ips -------------------------------------------------------------------------------- /public/patches/ips/zeldaDX-USA-rev2.ips: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonAbrams/retropatcher/14ebe6ccaeaa29d480d498d6c09fb2d8be4db57c/public/patches/ips/zeldaDX-USA-rev2.ips -------------------------------------------------------------------------------- /public/patches/ips/zeldaDX-USA.ips: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonAbrams/retropatcher/14ebe6ccaeaa29d480d498d6c09fb2d8be4db57c/public/patches/ips/zeldaDX-USA.ips -------------------------------------------------------------------------------- /public/patches/permaPocket.json: -------------------------------------------------------------------------------- 1 | { 2 | "note to devs": "ok to edit this directly, gets mixed into pocket.json on scrape", 3 | "patches": [ 4 | { 5 | "name": "Alleyway (with DX Patch)", 6 | "authorName": "JoseJX", 7 | "md5": "91128778a332495f77699eaf3a37fe30", 8 | "originalUrl": "https://github.com/JoseJX/analogue-pocket-patches/blob/main/README.md", 9 | "downloadUrl": "/patches/ips/AlleywayDX.ips", 10 | "outputFilename": "Alleyway DX.pocket" 11 | }, 12 | { 13 | "name": "For the Frog the Bell Tolls", 14 | "authorName": "JoseJX", 15 | "md5": "4ebe14c4c51555908c0e4cabb66dc813", 16 | "originalUrl": "https://github.com/JoseJX/analogue-pocket-patches/blob/main/README.md#kaeru-no-tame-ni-kane-wa-naru", 17 | "downloadUrl": "/patches/ips/frogBellTolls.ips", 18 | "outputFilename": "For Frog the Bell Tolls.pocket" 19 | }, 20 | { 21 | "name": "Cave Noire (J)", 22 | "authorName": "jsky0", 23 | "extension": "pocket", 24 | "md5": "10d92861e262069ce31559e12b927aa0", 25 | "downloadUrl": "https://github.com/jsky0/analogue-pocket-patches/raw/main/cave_noire.ips", 26 | "originalUrl": "https://github.com/jsky0/analogue-pocket-patches/blob/main/README.md" 27 | }, 28 | { 29 | "name": "Cave Noire (J) with English Translation", 30 | "authorName": "jsky0", 31 | "outputFilename": "Cave Noise (J) [T+Eng].pocket", 32 | "downloadUrl": "https://github.com/jsky0/analogue-pocket-patches/raw/main/cave_noire_aeon_genesis_translation.ips", 33 | "md5": "10d92861e262069ce31559e12b927aa0", 34 | "originalUrl": "https://github.com/jsky0/analogue-pocket-patches/blob/main/README.md" 35 | }, 36 | { 37 | "name": "Dangan GB", 38 | "authorName": "r0r0", 39 | "downloadUrl": "https://github.com/jduckett95/misc-pocket-patches/raw/main/danganV11.ips", 40 | "md5": "c85e5ba3dad5aa705b96da083cdd1a1c", 41 | "extension": "pocket", 42 | "originalUrl": "https://github.com/jduckett95/misc-pocket-patches/blob/main/README.md#dangan-gb" 43 | }, 44 | { 45 | "name": "Opossum Country", 46 | "authorName": "r0r0", 47 | "downloadUrl": "https://github.com/jduckett95/misc-pocket-patches/raw/main/opossum_country_v1.ips", 48 | "md5": "5c0ed7c257219b6fab67c5f9d9ab25f8", 49 | "extension": "pocket", 50 | "originalUrl": "https://github.com/jduckett95/misc-pocket-patches/blob/main/README.md#opossum-country" 51 | }, 52 | { 53 | "name": "Pokémon Black", 54 | "authorName": "janmalec & Ax461", 55 | "downloadUrl": "https://github.com/jduckett95/misc-pocket-patches/raw/main/black_pocket.ips", 56 | "md5": "45ff4e34868e32d638f5db2fc36bdebd", 57 | "outputFilename": "Pokemon Black.pocket", 58 | "originalUrl": "https://github.com/jduckett95/misc-pocket-patches/blob/main/README.md#pokmon-black" 59 | }, 60 | { 61 | "name": "Pokémon Crystal (Ver. 1.0)", 62 | "authorName": "Zhuowei Zhang", 63 | "downloadUrl": "https://github.com/jduckett95/misc-pocket-patches/raw/main/pokecrystal.ips", 64 | "md5": "9f2922b235a5eeb78d65594e82ef5dde", 65 | "outputFilename": "Pokemon Crystal.pocket", 66 | "originalUrl": "https://github.com/jduckett95/misc-pocket-patches/blob/main/README.md#pokmon-crystal-ver-10" 67 | }, 68 | { 69 | "name": "Pokémon Crystal (Ver. 1.1)", 70 | "authorName": "Zhuowei Zhang", 71 | "downloadUrl": "https://github.com/jduckett95/misc-pocket-patches/raw/main/pokecrystal11.ips", 72 | "md5": "301899b8087289a6436b0a241fbbb474", 73 | "outputFilename": "Pokemon Crystal.pocket", 74 | "originalUrl": "https://github.com/jduckett95/misc-pocket-patches/blob/main/README.md#pokmon-crystal-ver-11" 75 | }, 76 | { 77 | "name": "Pokemon - Edicion Cristal", 78 | "authorName": "Linkr2", 79 | "downloadUrl": "https://github.com/jduckett95/misc-pocket-patches/raw/main/Pokemon_-_Edicion_Cristal_Spain_Pocket_Patch.ips", 80 | "md5": "8a626340f6b16ba45c1d4e07f2134875", 81 | "outputFilename": "Pokemon Edicion Cristal.pocket", 82 | "originalUrl": "https://github.com/jduckett95/misc-pocket-patches/blob/main/README.md#pokemon---edicion-cristal" 83 | }, 84 | { 85 | "name": "Pokémon Picross with English Patch", 86 | "authorName": "JoseJX & LeonarthCG", 87 | "downloadUrl": "https://github.com/jduckett95/misc-pocket-patches/raw/main/Picross%20ENG%20v1-2%20Pocket.ips", 88 | "md5": "35d2e7924408a3460e5c1a770acf3a8a", 89 | "outputFilename": "Pokemon Picross [Eng].pocket", 90 | "originalUrl": "https://github.com/jduckett95/misc-pocket-patches/blob/main/README.md#pokmon-picross" 91 | }, 92 | { 93 | "name": "Pokémon Trading Card Game 2 with English Patch", 94 | "authorName": "BestPig & Artemis251", 95 | "downloadUrl": "https://github.com/jduckett95/misc-pocket-patches/raw/main/TCG2%20English%20%2B%20Pocket.ips", 96 | "md5": "1134862e84110443190df460351d4575", 97 | "outputFilename": "Pokemon TCG 2 [Eng].pocket", 98 | "originalUrl": "https://github.com/jduckett95/misc-pocket-patches/blob/main/README.md#pokmon-trading-card-game-2" 99 | }, 100 | { 101 | "name": "Sakura Wars GB with English Patch", 102 | "authorName": "JoseJX & vinheim3", 103 | "downloadUrl": "https://github.com/jduckett95/misc-pocket-patches/raw/main/Sakura%20Wars%20ENG%20Pocket.ips", 104 | "md5": "70883b45a97984cb033c2b95028bef65", 105 | "outputFilename": "Sakura Wars GB.pocket", 106 | "originalUrl": "https://github.com/jduckett95/misc-pocket-patches/blob/main/README.md#sakura-wars-gb" 107 | }, 108 | { 109 | "name": "Tetris + Rosy Retrospection", 110 | "authorName": "BestPig & Ospin", 111 | "downloadUrl": "https://github.com/jduckett95/misc-pocket-patches/raw/main/Tetris_Combo_Patch_Rosy__Pocket.ips", 112 | "md5": "982ed5d2b12a0377eb14bcdc4123744e", 113 | "outputFilename": "Tetris Rosy Retrospection.pocket", 114 | "originalUrl": "https://github.com/jduckett95/misc-pocket-patches/blob/main/README.md#tetris--rosy-retrospection" 115 | }, 116 | { 117 | "name": "Legend of Zelda, The - Link's Awakening Redux", 118 | "authorName": "JoseJX", 119 | "md5": "7351daa3c0a91d8f6fe2fbcca6182478", 120 | "outputFilename": "Link's Awakening Redux.pocket", 121 | "originalUrl": "https://github.com/JoseJX/analogue-pocket-patches/blob/main/README.md#links-awakening-redux", 122 | "downloadUrl": "/patches/ips/ZeldaToRedux.ips" 123 | }, 124 | { 125 | "name": "Kirby's Dream Land DX with Fluffy Repair Service", 126 | "authorName": "JoseJX", 127 | "md5": "a66e4918edcd042ec171a57fe3ce36c3", 128 | "outputFilename": "Kirby's Dream Land DX.pocket", 129 | "originalUrl": "https://github.com/JoseJX/analogue-pocket-patches/blob/main/README.md#kirbys-dream-land-dx-with-fluffy-repair-service", 130 | "downloadUrl": "/patches/ips/kirbyDXFluffy.ips" 131 | }, 132 | { 133 | "name": "Super Mario Land DX v2", 134 | "authorName": "JoseJX", 135 | "md5": "b48161623f12f86fec88320166a21fce", 136 | "originalUrl": "https://github.com/JoseJX/analogue-pocket-patches/blob/main/README.md#super-mario-land-dx-v20", 137 | "downloadUrl": "/patches/ips/SMDX2.ips" 138 | }, 139 | { 140 | "name": "Wario Land DX", 141 | "authorName": "JoseJX", 142 | "md5": "d9d957771484ef846d4e8d241f6f2815", 143 | "originalUrl": "https://github.com/JoseJX/analogue-pocket-patches/blob/main/README.md#wario-land---super-mario-land-3-dx", 144 | "downloadUrl": "/patches/ips/WarioLandDX.ips" 145 | } 146 | ] 147 | } -------------------------------------------------------------------------------- /scrape.mjs: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | import fetch from "node-fetch"; 3 | import process from "process"; 4 | 5 | const outputFile = "./public/patches/pocket.js"; 6 | 7 | const sources = { 8 | JoseJX: { 9 | md: "https://raw.githubusercontent.com/JoseJX/analogue-pocket-patches/main/README.md", 10 | }, 11 | Infinest: { 12 | md: "https://raw.githubusercontent.com/jduckett95/infinest-pocket-patches/main/README.md", 13 | }, 14 | "Trey Turner": { 15 | md: "https://raw.githubusercontent.com/treyturner/analogue-pocket-patches/main/README.md", 16 | }, 17 | jsky0: { 18 | md: "https://raw.githubusercontent.com/jsky0/analogue-pocket-patches/main/README.md", 19 | }, 20 | r0r0: { 21 | md: "https://raw.githubusercontent.com/jduckett95/misc-pocket-patches/main/r0r0-patches.md", 22 | }, 23 | BestPig: { 24 | md: "https://raw.githubusercontent.com/BestPig/analogue-pocket-patches/master/README.md", 25 | }, 26 | reminon: { 27 | md: "https://raw.githubusercontent.com/reminon/pocket-patches/main/README.md", 28 | }, 29 | megane72GH: { 30 | md: "https://raw.githubusercontent.com/megane72GH/analogue-pocket-patches/main/README.md", 31 | }, 32 | }; 33 | 34 | (async () => { 35 | const permaPatchesText = await fs.readFile( 36 | "./public/patches/permaPocket.json" 37 | ); 38 | const patches = JSON.parse(permaPatchesText).patches; 39 | for (const authorName of Object.keys(sources)) { 40 | const mdUrl = sources[authorName].md; 41 | const text = await fetch(mdUrl).then((res) => res.text()); 42 | const chunks = text.split(/^#+\s*/m); 43 | const regex = 44 | authorName === "BestPig" 45 | ? /^(?.*)[^]*(?https:\/\/.*\.ips)[^]*MD5.*(?[0-9a-f]{32})/i 46 | : /^(?.*)[^]*MD5.*(?[0-9a-f]{32})[^]*(?https:\/\/.*\.ips)/i; 47 | for (let chunk of chunks) { 48 | const match = chunk.match(regex); 49 | if (!match) continue; 50 | const { name, md5, url } = match.groups; 51 | if (!name || !md5 || !url) continue; 52 | let downloadUrl = 53 | authorName === "BestPig" 54 | ? url.replace("shareit.bestpig.fr/file", "shareit.bestpig.fr/get") 55 | : url.replace("/blob/main/", "/raw/main/"); 56 | // Replace + in url with %20 57 | downloadUrl = downloadUrl.replace(/([^+])\+([^+])/g, "$1%20$2"); 58 | if (patches.some((p) => p.downloadUrl === downloadUrl && p.md5 === md5)) { 59 | continue; 60 | } 61 | const hashName = name 62 | .trim() 63 | .replace(/[^-\s\wÀ-ú]/g, "") 64 | .replace(/\s/g, "-") 65 | .toLowerCase(); 66 | const patch = { 67 | name, 68 | authorName, 69 | downloadUrl, 70 | md5: md5.toLowerCase(), 71 | extension: "pocket", 72 | originalUrl: 73 | (authorName === "BestPig" 74 | ? "https://gist.github.com/BestPig/528fb9a19cbb638fac1278a641041881" 75 | : mdUrl 76 | .replace("raw.githubusercontent.com", "github.com") 77 | .replace("/main", "/blob/main")) + `#${hashName}`, 78 | }; 79 | patches.push(patch); 80 | } 81 | } 82 | 83 | patches.sort((a, b) => { 84 | if (a.name > b.name) { 85 | return 1; 86 | } 87 | return -1; 88 | }); 89 | 90 | const newJSON = JSON.stringify(patches, null, 2); 91 | let outputText = `export const updated = '${new Date().toISOString()}';\n`; 92 | outputText += `export const patches = ${newJSON};\n`; 93 | const oldText = await fs.readFile(outputFile, { encoding: "utf8" }); 94 | if (outputText.length > oldText.length) { 95 | await fs.writeFile(outputFile, outputText); 96 | console.log("Wrote new pocket.js"); 97 | } else if (outputText.length < oldText.length) { 98 | console.error( 99 | "Uh oh, patches.json got smaller?", 100 | "Existing file size:", 101 | oldText.length, 102 | "New size:", 103 | outputText.length 104 | ); 105 | process.exit(1); 106 | } else { 107 | console.log("No changes found."); 108 | } 109 | })(); 110 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: calc(100vh - 83px); 7 | max-width: 600px; 8 | padding: 4rem 0; 9 | margin: 0 auto; 10 | flex: 1; 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: center; 14 | align-items: center; 15 | } 16 | 17 | .footer { 18 | padding: 2rem 0; 19 | border-top: 1px solid #eaeaea; 20 | text-align: center; 21 | line-height: 1.5rem; 22 | } 23 | 24 | .title { 25 | margin: 0; 26 | line-height: 1.15; 27 | font-size: 4rem; 28 | } 29 | 30 | .grid { 31 | display: flex; 32 | align-items: center; 33 | justify-content: center; 34 | flex-wrap: wrap; 35 | max-width: 800px; 36 | } 37 | 38 | .resultsContainer { 39 | width: 100%; 40 | margin-top: 1rem; 41 | padding-top: 1rem; 42 | border-top: solid 1px; 43 | } 44 | 45 | .resultsTitle { 46 | font-weight: bold; 47 | } 48 | 49 | .errorOutput { 50 | margin-top: 1rem; 51 | color: red; 52 | } 53 | 54 | .loading { 55 | margin-top: 1rem; 56 | } 57 | 58 | .patchInfo { 59 | text-align: left; 60 | max-width: calc(100vw - 30px); 61 | margin-top: 1rem; 62 | border: solid 1px #aaa; 63 | background-color: #e4e4e4; 64 | padding: 1rem; 65 | overflow-x: scroll; 66 | } 67 | 68 | .wishingWell { 69 | margin-top: 1rem; 70 | } 71 | 72 | .searchBox { 73 | padding: 5px; 74 | border-radius: 5px; 75 | border: solid 1px; 76 | } 77 | 78 | .romMd5 { 79 | white-space: nowrap; 80 | } 81 | 82 | .downloadButton { 83 | margin-top: 1rem; 84 | } 85 | 86 | .dndMsg { 87 | margin-top: 1rem; 88 | font-size: 12px; 89 | } 90 | 91 | .note { 92 | font-size: 12px; 93 | } 94 | 95 | .updated { 96 | font-size: 12px; 97 | margin-top: 1.5rem; 98 | } 99 | 100 | @media (max-width: 600px) { 101 | .grid { 102 | width: 100%; 103 | flex-direction: column; 104 | } 105 | } 106 | 107 | .fileInput::-webkit-file-upload-button { 108 | visibility: hidden; 109 | } 110 | .fileButton { 111 | display: inline-block; 112 | margin-top: 1rem; 113 | background: linear-gradient(to top, #e3e3e3, #f9f9f9); 114 | border: 1px solid #999; 115 | border-radius: 3px; 116 | padding: 5px 8px; 117 | outline: none; 118 | white-space: nowrap; 119 | user-select: none; 120 | cursor: pointer; 121 | text-shadow: 1px 1px #fff; 122 | font-weight: 700; 123 | font-size: 10pt; 124 | } 125 | .fileButtonOver, .fileButton:hover { 126 | border-color: black; 127 | } 128 | .fileButtonOver, .fileButton:active { 129 | background: linear-gradient(to top, #f9f9f9, #e3e3e3); 130 | } 131 | 132 | .patchList { 133 | text-align: center; 134 | } 135 | 136 | .alphaList { 137 | margin-top: 1.5rem; 138 | display: flex; 139 | gap: 10px; 140 | flex-wrap: wrap; 141 | justify-content: center; 142 | font-family: Courier, monospace; 143 | } 144 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | * { 10 | box-sizing: border-box; 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "CommonJS", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules", "scrape.js"] 20 | } 21 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "js-md5" { 2 | let md5 = (data: ArrayBuffer) => string; 3 | export default md5; 4 | } 5 | --------------------------------------------------------------------------------