Skip to content

Commit

Permalink
Add a base rom version selector for hackroms 🦠.
Browse files Browse the repository at this point in the history
  • Loading branch information
gmarty committed May 14, 2024
1 parent 49d70ad commit e4cb50d
Show file tree
Hide file tree
Showing 8 changed files with 181 additions and 30 deletions.
112 changes: 112 additions & 0 deletions src/components/BaseRomDialog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Fragment, useState } from 'react';
import {
Dialog,
DialogPanel,
DialogTitle,
Transition,
TransitionChild,
} from '@headlessui/react';
import { QuestionMarkCircleIcon } from '@heroicons/react/24/outline';
import resources from '../lib/resources';

const BASE_ROMS = resources.map(({ metadata: { name } }) => name);
BASE_ROMS.sort();
const defaultValue = 'USA'; // The USA version is the most commonly used base ROM.

const BaseRomDialog = ({ open, setOpen, setBaseRom }) => {
const [baseRom, setInternalBaseRom] = useState(defaultValue);

return (
<Transition
show={open}
as={Fragment}>
<Dialog
className="relative z-10"
onClose={() => {}}>
<TransitionChild
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0">
<div className="fixed inset-0 bg-slate-100/75 opacity-100 dark:bg-slate-900/75" />
</TransitionChild>

<div className="fixed inset-0 z-10 w-screen overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<TransitionChild
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
<DialogPanel className="relative transform overflow-hidden rounded-md bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
<div>
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-primary-100">
<QuestionMarkCircleIcon
className="size-6 text-primary-600"
aria-hidden="true"
/>
</div>
<div className="mt-3 sm:mt-5">
<DialogTitle
as="h3"
className="text-center text-base font-semibold leading-6 text-gray-900">
Unrecognised ROM
</DialogTitle>
<div className="my-2">
<p className="text-sm text-gray-500">
The ROM you selected could not be identified. If it's a
romhack, select the version used as its base.
</p>
</div>
<BaseRomSelector setBaseRom={setInternalBaseRom} />
</div>
</div>
<div className="mt-5 sm:mt-6">
<button
type="button"
className="inline-flex w-full justify-center rounded-md bg-primary-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600"
onClick={() => {
setOpen(false);
setBaseRom(baseRom);
}}>
Select base ROM version
</button>
</div>
</DialogPanel>
</TransitionChild>
</div>
</div>
</Dialog>
</Transition>
);
};

const BaseRomSelector = ({ setBaseRom }) => {
return (
<>
<label
htmlFor="base-rom"
className="block text-sm font-medium leading-6 text-gray-900">
Base ROM version
</label>
<select
id="base-rom"
name="base-rom"
className="mt-2 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-primary-600 sm:text-sm sm:leading-6"
defaultValue={defaultValue}
onChange={({ target }) => setBaseRom(target.value)}>
{BASE_ROMS.map((baseRom) => (
<option key={baseRom}>{baseRom}</option>
))}
</select>
</>
);
};

export default BaseRomDialog;
2 changes: 1 addition & 1 deletion src/components/DropZone.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const DropZone = ({ children, isDragActive, isDragReject, errorCode }) => {
errorMsg = 'The Japanese Famicom version is not supported.';
break;
case 'invalid-rom-file':
errorMsg = 'Upload one of the ROMs of Maniac Mansion on NES.';
errorMsg = 'The ROM was not recognised as one of Maniac Mansion on NES.';
break;
case 'reading-file-failed':
errorMsg = 'File reading has failed. Please try again.';
Expand Down
9 changes: 8 additions & 1 deletion src/components/ErrorMessage.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
const ErrorMessage = () => {
return <h1>Something went wrong.</h1>;
return (
<>
<h1 className="mb-2 whitespace-nowrap text-2xl font-semibold text-slate-700 md:text-3xl dark:text-slate-300">
Something went wrong
</h1>
<p>Open the console for more info.</p>
</>
);
};

export default ErrorMessage;
38 changes: 32 additions & 6 deletions src/containers/DropZoneContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
import { useDropzone } from 'react-dropzone';
import Main from '../components/Main';
import DropZone from '../components/DropZone';
import BaseRomDialog from '../components/BaseRomDialog';

const DropZoneContainer = ({ onFile }) => {
const validator = (file) => {
Expand All @@ -27,12 +28,18 @@ const DropZoneContainer = ({ onFile }) => {
setErrorCode('reading-file-failed');
};
reader.onload = async () => {
const { hasNesHeader } = await import('../lib/utils');
const { default: crc32 } = await import('../lib/crc32');
const { isJapaneseVersion, getResFromCrc32 } = await import(
'../lib/getResFromCrc32'
);

let arrayBuffer = reader.result;

if (file.name.endsWith('.nes') || hasNesHeader(arrayBuffer)) {
arrayBuffer = arrayBuffer.slice(16);
}

const dataView = new DataView(arrayBuffer);
const c = crc32(dataView);

Expand All @@ -43,16 +50,13 @@ const DropZoneContainer = ({ onFile }) => {

const res = getResFromCrc32(c);

setRom(arrayBuffer);

if (!res) {
setErrorCode('invalid-rom-file');
setBaseRomDialogOpened(true);
return;
}

if (file.name.endsWith('.nes')) {
arrayBuffer = arrayBuffer.slice(16);
}

setRom(arrayBuffer);
setRes(res);
};
reader.readAsArrayBuffer(file);
Expand All @@ -61,6 +65,8 @@ const DropZoneContainer = ({ onFile }) => {
};

const [errorCode, setErrorCode] = useState(null);
const [baseRomDialogOpened, setBaseRomDialogOpened] = useState(false);
const [baseRom, setBaseRom] = useState(null);
const [rom, setRom] = useState(null);
const [res, setRes] = useState(null);
const {
Expand All @@ -76,6 +82,21 @@ const DropZoneContainer = ({ onFile }) => {
validator,
});

if (baseRom) {
(async () => {
const { getResFromBaseRom } = await import('../lib/getResFromCrc32');
const { default: parseRom } = await import('../lib/parser/parseRom');
const res = getResFromBaseRom(baseRom);

try {
parseRom(rom, res);
setRes(res);
} catch (err) {
setErrorCode('invalid-rom-file');
}
})();
}

if (fileRejections[0] && fileRejections[0]?.errors[0]?.code && !errorCode) {
setErrorCode(fileRejections[0].errors[0].code);
}
Expand All @@ -88,6 +109,11 @@ const DropZoneContainer = ({ onFile }) => {

return (
<Main>
<BaseRomDialog
open={baseRomDialogOpened}
setOpen={setBaseRomDialogOpened}
setBaseRom={setBaseRom}
/>
<div
className="h-full w-full p-4"
{...getRootProps()}>
Expand Down
22 changes: 2 additions & 20 deletions src/lib/cliUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { extname } from 'node:path';
import { readFile, writeFile } from 'node:fs/promises';
import crc32 from './crc32.js';
import { isJapaneseVersion, getResFromCrc32 } from './getResFromCrc32.js';
import { hex } from './utils.js';
import { hex, hasNesHeader } from './utils.js';

const BANK_SIZE = 0x4000;
// prettier-ignore
Expand Down Expand Up @@ -119,17 +119,6 @@ const stringifyResources = (hash, size, resources, res) => {
return JSON.stringify(data, null, ' ');
};

// Return true if an arrayBuffer has a NES header.
const hasNesHeader = (bin) => {
const view = new DataView(bin);
for (let i = 0; i < 4; i++) {
if (view.getUint8(i) !== NES_HEADER[i]) {
return false;
}
}
return true;
};

// Append a NES header to a PRG buffer.
const prependNesHeader = (prg) => {
const rom = new ArrayBuffer(NES_HEADER.length + prg.byteLength);
Expand Down Expand Up @@ -170,11 +159,4 @@ const expandRom = (rom) => {
return newRom;
};

export {
loadRom,
saveRom,
inject,
stringifyResources,
hasNesHeader,
expandRom,
};
export { loadRom, saveRom, inject, stringifyResources, expandRom };
6 changes: 5 additions & 1 deletion src/lib/getResFromCrc32.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@ import res from './resources.js';
const isJapaneseVersion = (c) => c === 0x3da2085e || c === 0xf526cea8;
const getResFromCrc32 = (c) =>
res.find(({ crc32, crc32Rom }) => crc32 === c || crc32Rom === c) ?? null;
const getResFromBaseRom = (b) =>
res.find(
({ metadata: { name } }) => name.toLowerCase() === b.toLowerCase(),
) ?? null;

export { isJapaneseVersion, getResFromCrc32 };
export { isJapaneseVersion, getResFromCrc32, getResFromBaseRom };
8 changes: 8 additions & 0 deletions src/lib/resources.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// prettier-ignore
const usa = {
metadata: { name: 'USA' },
roomgfx: [
[0x04001, 0x03c9], [0x043ca, 0x069e], [0x04a68, 0x0327], [0x04d8f, 0x053b], [0x052ca, 0x06be],
[0x05988, 0x0682], [0x0600a, 0x0778], [0x06782, 0x0517], [0x06c99, 0x07fb], [0x07494, 0x07be],
Expand Down Expand Up @@ -109,6 +110,7 @@ const usa = {
};
// prettier-ignore
const eur = {
metadata: { name: 'Europe' },
roomgfx: [
[0x04001, 0x03b9], [0x043ba, 0x069e], [0x04a58, 0x0327], [0x04d7f, 0x053b], [0x052ba, 0x06be],
[0x05978, 0x0682], [0x05ffa, 0x0778], [0x06772, 0x0517], [0x06c89, 0x07fb], [0x07484, 0x07be],
Expand Down Expand Up @@ -218,6 +220,7 @@ const eur = {
};
// prettier-ignore
const swe = {
metadata: { name: 'Sweden' },
roomgfx: [
[0x04001, 0x03f0], [0x043f1, 0x069e], [0x04a8f, 0x0327], [0x04db6, 0x053b], [0x052f1, 0x06be],
[0x059af, 0x0682], [0x06031, 0x0778], [0x067a9, 0x0517], [0x06cc0, 0x07fb], [0x074bb, 0x07be],
Expand Down Expand Up @@ -326,6 +329,7 @@ const swe = {
};
// prettier-ignore
const fra = {
metadata: { name: 'France' },
roomgfx: [
[0x04001, 0x0426], [0x04427, 0x069e], [0x04ac5, 0x0327], [0x04dec, 0x053b], [0x05327, 0x06be],
[0x059e5, 0x0682], [0x06067, 0x0778], [0x067df, 0x0517], [0x06cf6, 0x07fb], [0x074f1, 0x07be],
Expand Down Expand Up @@ -433,6 +437,7 @@ const fra = {
};
// prettier-ignore
const ger = {
metadata: { name: 'Germany' },
roomgfx: [
[0x04001, 0x0406], [0x04407, 0x069e], [0x04aa5, 0x0327], [0x04dcc, 0x053b], [0x05307, 0x06be],
[0x059c5, 0x0682], [0x06047, 0x0778], [0x067bf, 0x0517], [0x06cd6, 0x07fb], [0x074d1, 0x07be],
Expand Down Expand Up @@ -541,6 +546,7 @@ const ger = {
};
// prettier-ignore
const esp = {
metadata: { name: 'Spain' },
roomgfx: [
[0x04001, 0x041b], [0x0441c, 0x069e], [0x04aba, 0x0327], [0x04de1, 0x053b], [0x0531c, 0x06be],
[0x059da, 0x0682], [0x0605c, 0x0778], [0x067d4, 0x0517], [0x06ceb, 0x07fb], [0x074e6, 0x07be],
Expand Down Expand Up @@ -648,6 +654,7 @@ const esp = {
};
// prettier-ignore
const ita = {
metadata: { name: 'Italy' },
roomgfx: [
[0x04001, 0x03ef], [0x043f0, 0x069e], [0x04a8e, 0x0327], [0x04db5, 0x053b], [0x052f0, 0x06be],
[0x059ae, 0x0682], [0x06030, 0x0778], [0x067a8, 0x0517], [0x06cbf, 0x07fb], [0x074ba, 0x07be],
Expand Down Expand Up @@ -754,6 +761,7 @@ const ita = {
};
// prettier-ignore
const proto = {
metadata: { name: 'Prototype' },
roomgfx: [
[0x04001, 0x03c9], [0x043ca, 0x069e], [0x04a68, 0x0327], [0x04d8f, 0x053b], [0x052ca, 0x06be],
[0x05988, 0x0682], [0x0600a, 0x0778], [0x06782, 0x0517], [0x06c99, 0x07fb], [0x07494, 0x07be],
Expand Down
14 changes: 13 additions & 1 deletion src/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,16 @@ const formatPercentage = (percent, decimals = 2) => {
return `${(percent * 100).toFixed(dm)}%`;
};

export { zeroPad, hex, formatBytes, formatPercentage };
// Return true if an arrayBuffer has a NES header.
const hasNesHeader = (bin) => {
const NES_HEADER = new Uint8Array([0x4e, 0x45, 0x53, 0x1a]);
const view = new DataView(bin);
for (let i = 0; i < NES_HEADER.length; i++) {
if (view.getUint8(i) !== NES_HEADER[i]) {
return false;
}
}
return true;
};

export { zeroPad, hex, formatBytes, formatPercentage, hasNesHeader };

0 comments on commit e4cb50d

Please sign in to comment.