Skip to content

Commit

Permalink
Generate ROM hacks with modified palettes 💥.
Browse files Browse the repository at this point in the history
  • Loading branch information
gmarty committed May 25, 2024
1 parent 19acdc7 commit 22591ba
Show file tree
Hide file tree
Showing 20 changed files with 381 additions and 199 deletions.
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# SCUMM NES resource explorer

> An app to explore the content of the Maniac Mansion game on NES.
> An app to explore and modify the content of the Maniac Mansion game on NES.
## What is this?

Expand Down Expand Up @@ -84,7 +84,6 @@ Then deploy the content of the `dist` folder.
- Write more tests.
- Parse more resource types (scripts, sounds...).
- QoF improvements (store the ROM files locally...)
- Enable modification of the resources to create new games.

### Out of scope for now

Expand Down
5 changes: 3 additions & 2 deletions experiments/relocateNtAttrs.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import { parseArgs } from 'node:util';
import { basename } from 'node:path';
import { loadRom, saveRom, expandRom, inject } from '../src/lib/cliUtils.js';
import { loadRom, saveRom, expandRom } from '../src/lib/cliUtils.js';
import { inject } from '../src/lib/romUtils.js';
import parseRoom from '../src/lib/parser/parseRooms.js';
import { zeroPad, hex } from '../src/lib/utils.js';

Expand Down Expand Up @@ -119,7 +120,7 @@ for (let i = 0; i < roomNum; i++) {
nametableLength,
hex(bankOffset + nametableLength, 4),
hex(atOffset + headerLength),
attrsLength
attrsLength,
);
bankOffset += nametableLength;
bankOffset += attrsLength;
Expand Down
3 changes: 2 additions & 1 deletion experiments/updatePreps.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import { parseArgs } from 'node:util';
import { basename } from 'node:path';
import { loadRom, saveRom, inject } from '../src/lib/cliUtils.js';
import { loadRom, saveRom } from '../src/lib/cliUtils.js';
import { inject } from '../src/lib/romUtils.js';
import serialisePreps from '../src/lib/serialiser/serialisePreps.js';

/*
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "scumm-nes",
"version": "0.1.2",
"description": "An app to explore the content of the Maniac Mansion game on NES.",
"version": "0.2.0",
"description": "An app to explore and modify the content of the Maniac Mansion game on NES.",
"author": "edo999@gmail.com",
"license": "BlueOak-1.0.0",
"keywords": [
Expand Down
40 changes: 9 additions & 31 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,19 @@
import { useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { useNavigate } from 'react-router-dom';
import { useRom } from './contexts/RomContext';
import Layout from './components/Layout';
import DropZoneContainer from './containers/DropZoneContainer';
import ResourceExplorer from './containers/ResourceExplorer';
import ErrorMessage from './components/ErrorMessage';

const App = () => {
const [rom, setRom] = useState(null);
const [res, setRes] = useState(null);
const [resources, setResources] = useState(null);
const navigate = useNavigate();

const onFile = async (rom, res) => {
const { default: parseRom } = await import('./lib/parser/parseRom');
setRom(rom);
setRes(res);
setResources(parseRom(rom, res));

// Redirect to the first room.
navigate('/rooms/1');
};
const { prg, res, resources } = useRom();

return (
<ErrorBoundary FallbackComponent={ErrorMessage}>
<Layout>
{!rom || !res ? (
<DropZoneContainer onFile={onFile} />
) : (
<ResourceExplorer
rom={rom}
res={res}
resources={resources}
/>
)}
</Layout>
</ErrorBoundary>
<Layout>
{!prg || !res || !resources ? (
<DropZoneContainer />
) : (
<ResourceExplorer />
)}
</Layout>
);
};

Expand Down
77 changes: 77 additions & 0 deletions src/components/DownloadRom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { useRef, useState } from 'react';
import { useRom } from '../contexts/RomContext';
import { prependNesHeader, inject, debugArrayBuffer } from '../lib/romUtils';
import serialisePalette from '../lib/serialiser/serialisePalette';

const type = 'application/x-nes-rom';
const fileName = 'mm-hack.nes';

const DownloadRom = ({ children }) => {
const { prg, resources } = useRom();
const [aHref, setAHref] = useState(null);
const aRef = useRef(null);

if (prg === null) {
return (
<button
className="text-slate-500 opacity-50"
disabled>
{children}
</button>
);
}

// Inject the new content into the PRG and return a NES ROM buffer.
const generateRomHackFile = () => {
const screens = [...resources.titles, ...resources.rooms];

// Update screen titles and rooms palette.
for (let i = 0; i < screens.length; i++) {
const screen = screens[i];
const { offset, size, id } = screen.metadata;
if (!size) {
continue;
}

const { from, to } = screen.map.find(({ type }) => type === 'palette');
const buffer = serialisePalette(screen.palette);

const overwrites = inject(prg, buffer, offset + from, to - from);
if (overwrites) {
console.log('Injecting screen \x1b[33m%i\x1b[0m palette', id);
}
}

// Add the iNES header to make it playable on emulators.
return prependNesHeader(prg);
};

return (
<>
<button
className="text-slate-500 transition hover:text-slate-800 hover:dark:text-slate-200"
onClick={() => {
const rom = generateRomHackFile();
setAHref(window.URL.createObjectURL(new Blob([rom], { type })));

// Needs to render first.
setTimeout(() => {
// Trigger the download by simulating a click.
aRef.current.click();
window.URL.revokeObjectURL(aHref);
});
}}>
{children}
</button>
{/* eslint-disable-next-line jsx-a11y/anchor-has-content */}
<a
className="hidden"
href={aHref}
download={fileName}
ref={aRef}
/>
</>
);
};

export default DownloadRom;
72 changes: 36 additions & 36 deletions src/components/Footer.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,40 +46,40 @@ const navigation = [
},
];

export default function Footer() {
return (
<footer className="bg-slate-200 dark:bg-black">
<div className="mx-auto flex max-w-7xl items-center justify-between gap-x-2 px-3 py-2 md:px-4">
<p className="text-balance text-center font-geohumanist text-xs text-slate-500">
&copy; 2024 SCUMM NES resource explorer
</p>
<div className="flex gap-x-2 sm:gap-x-4 md:gap-x-6">
{navigation.map((item) => (
<a
key={item.name}
href={item.href}
className="fill-transparent stroke-slate-500 transition-all hover:stroke-slate-800 hover:dark:stroke-slate-200"
rel="me">
<span className="sr-only">{item.name}</span>
<item.icon
className="size-6"
strokeWidth="1.5"
aria-hidden="true"
/>
</a>
))}
{process.env.NODE_ENV === 'development' && (
<div className="text-sm leading-6 text-slate-400">
<span className="hidden max-sm:inline">XS</span>
<span className="hidden sm:max-md:inline">SM</span>
<span className="hidden md:max-lg:inline">MD</span>
<span className="hidden lg:max-xl:inline">LG</span>
<span className="hidden xl:max-2xl:inline">XL</span>
<span className="hidden 2xl:inline">2XL</span>
</div>
)}
</div>
const Footer = () => (
<footer className="bg-slate-200 dark:bg-black">
<div className="mx-auto flex max-w-7xl items-center justify-between gap-x-2 px-3 py-2 md:px-4">
<p className="text-balance text-center font-geohumanist text-xs text-slate-500">
&copy; 2024 SCUMM NES resource explorer
</p>
<div className="flex gap-x-2 sm:gap-x-4 md:gap-x-6">
{navigation.map((item) => (
<a
key={item.name}
href={item.href}
className="fill-transparent stroke-slate-500 transition-all hover:stroke-slate-800 hover:dark:stroke-slate-200"
rel="me">
<span className="sr-only">{item.name}</span>
<item.icon
className="size-6"
strokeWidth="1.5"
aria-hidden="true"
/>
</a>
))}
{process.env.NODE_ENV === 'development' && (
<div className="text-sm leading-6 text-slate-400">
<span className="hidden max-sm:inline">XS</span>
<span className="hidden sm:max-md:inline">SM</span>
<span className="hidden md:max-lg:inline">MD</span>
<span className="hidden lg:max-xl:inline">LG</span>
<span className="hidden xl:max-2xl:inline">XL</span>
<span className="hidden 2xl:inline">2XL</span>
</div>
)}
</div>
</footer>
);
}
</div>
</footer>
);

export default Footer;
21 changes: 18 additions & 3 deletions src/components/Header.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Fragment, useState } from 'react';
import { Link } from 'react-router-dom';
import DownloadRom from './DownloadRom';
import {
Dialog,
DialogPanel,
Transition,
TransitionChild,
} from '@headlessui/react';
import {
ArrowDownTrayIcon,
Cog8ToothIcon,
Bars3Icon,
XMarkIcon,
Expand All @@ -22,7 +24,7 @@ const navigation = [
{ name: 'Settings', href: '/settings', sideBarOnly: true },
];

export default function Header() {
const Header = () => {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);

return (
Expand Down Expand Up @@ -63,7 +65,13 @@ export default function Header() {
</Link>
))}
</div>
<div className="hidden md:flex md:flex-1 md:justify-end">
<div className="hidden gap-x-4 md:flex md:flex-1 md:justify-end">
<DownloadRom>
<ArrowDownTrayIcon
strokeWidth="1.5"
className="size-6"
/>
</DownloadRom>
<Link to="/settings">
<Cog8ToothIcon
strokeWidth="1.5"
Expand Down Expand Up @@ -130,6 +138,11 @@ export default function Header() {
{item.name}
</Link>
))}
<DownloadRom>
<span className="-mx-3 block rounded px-3 py-2 text-base font-semibold leading-7 text-slate-700 hover:bg-slate-200 dark:text-slate-300 hover:dark:bg-slate-800">
Download modified ROM
</span>
</DownloadRom>
</div>
</div>
</div>
Expand All @@ -139,4 +152,6 @@ export default function Header() {
</Transition>
</header>
);
}
};

export default Header;
20 changes: 10 additions & 10 deletions src/components/Layout.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import Header from './Header';
import Footer from './Footer';

export default function Layout({ children }) {
return (
<div className="flex h-dvh flex-col">
<Header />
<div className="mx-auto flex w-full max-w-7xl grow items-stretch divide-x divide-slate-500 overflow-y-auto">
{children}
</div>
<Footer />
const Layout = ({ children }) => (
<div className="flex h-dvh flex-col">
<Header />
<div className="mx-auto flex w-full max-w-7xl grow items-stretch divide-x divide-slate-500 overflow-y-auto">
{children}
</div>
);
}
<Footer />
</div>
);

export default Layout;
Loading

0 comments on commit 22591ba

Please sign in to comment.