├── .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 |
27 | {showMd5 &&
ROM MD5: {patch.md5}
}
28 | {showDownload &&
Download patch }
29 | {onApply && (
30 |
35 | Apply and Save
36 |
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 |
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 |
236 | Select GB/GBC rom file(s)
237 |
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 |
262 | Apply and Save All
263 |
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 |
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 |
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 |
--------------------------------------------------------------------------------