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 64bdd2d
Show file tree
Hide file tree
Showing 21 changed files with 407 additions and 206 deletions.
11 changes: 4 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# 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?

This tool is a web app to explore the resources from the Maniac Mansion game on NES.
This tool is a web app to explore and modify the resources contained in the Maniac Mansion game on NES.
The goal is to encourage the ROM hacking community to modify the game and create new hacks.

This app only works with Maniac Mansion on NES, it won't work with data files from other platforms or other games.
This app only works with Maniac Mansion on NES, it won't work with data files from other platforms or other NES games.

Some of the code comes from the [ScummVM](https://github.com/scummvm) project.

Expand Down Expand Up @@ -37,8 +37,6 @@ The Japanese version is not supported.

The app doesn't use the runtime code used by the NES to start and play the game. It only works with the SCUMM resources stored in the ROM files.

It does not modify the ROM, it is only an explorer of the resources present in the game.

## How to use it?

On top of the app, there is a command line interface to export the resources to JSON. This is useful to do process or compare the data.
Expand Down Expand Up @@ -82,9 +80,8 @@ Then deploy the content of the `dist` folder.

- Use Typescript.
- Write more tests.
- Parse more resource types (scripts, sounds...).
- Parse more resource types (sprites, 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": "[email protected]",
"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
51 changes: 51 additions & 0 deletions src/components/DownloadRom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useRef, useState } from 'react';
import { useRom } from '../contexts/RomContext';
import { generateRomHackFile } from '../lib/romUtils';

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>
);
}

return (
<>
<button
className="text-slate-500 transition hover:text-slate-800 hover:dark:text-slate-200"
onClick={() => {
const rom = generateRomHackFile(prg, resources);
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 64bdd2d

Please sign in to comment.