Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,7 @@ TODO.txt
testdata/

# Editor
.idea
.idea

# Finder, which leaves one of these in every directory it is asked to look at
.DS_Store
50 changes: 43 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Working on MyNES

A NES emulator in Java 25, built with Maven. Three modules -- `mynes-core`, `mynes-headless`,
`mynes-desktop` -- and `mvn -B test` at the root still runs everything.
A NES emulator in Java 25, built with Maven. Four modules -- `mynes-core`, `mynes-patch`,
`mynes-headless`, `mynes-desktop` -- and `mvn -B test` at the root still runs everything.

## Seeing what the emulator does

Expand Down Expand Up @@ -156,6 +156,32 @@ exit 2.
so `run.state.startedFromPowerOn` in the report is part of what to check before diffing two of them.
`--sram-in`/`--sram-out` do the same for battery RAM, in the `.sav` format other emulators read.

### Running a romhack

`--patch FILE` applies an IPS patch to the ROM before anything reads it as a cartridge. Repeatable,
applied in the order given.

```sh
java -jar $JAR --headless --rom ROM.nes --patch hack.ips --frames 120 --screenshot last
```

**Nothing is written back.** The patch happens to the copy in memory, so the `.nes` on disk is
untouched and there is no patched file to tidy up afterwards. Two consequences worth knowing. The
patch is applied *before* `Cart.load`, so it may rewrite the iNES header and change the mapper, the
bank count or the size of the cartridge. And `cart.sha256` in the report is the digest of the
**patched** image, since that is what ran -- so a patched run and an unpatched one are two different
cartridges as far as the report and a save state are concerned, which is the answer that keeps a
hack's save states out of the original.

`cart.patches` lists each one with the number of records it held. **Zero records is the thing to
look for**: a patch cut against a different dump of the same game applies without complaining and
changes nothing anybody can see. So does one cut against a headerless dump, which will write
everything sixteen bytes early instead -- offsets count from the front of the file, header included.

`RomHackTests` is the worked example: a public-domain hello-world cartridge, a checked-in `.ips` that
rewrites the string it draws, and the two pictures compared. `src/test/resources/PROVENANCE` says
where the cartridge came from and why that one.

## What gets released

`mvn package` also writes `mynes-desktop/target/mynes-<version>.zip` -- the jar, a launcher for each
Expand All @@ -170,8 +196,8 @@ build would notice one going missing. And `THIRD-PARTY.md` names the two librari
carries, which is the file to write in if a third ever earns its place.

Releasing is a tag and nothing else. `.github/workflows/release.yml` refuses one whose name disagrees
with the pom, so the version moves first and `git tag v<version>` follows it. There are four poms to
move it in now, which is a job for the tool rather than for four edits:
with the pom, so the version moves first and `git tag v<version>` follows it. There are five poms to
move it in now, which is a job for the tool rather than for five edits:

```sh
mvn -B versions:set -DnewVersion=0.3.0 -DprocessAllModules -DgenerateBackupPoms=false
Expand Down Expand Up @@ -202,7 +228,7 @@ The code has a strong voice. Match it rather than the language's defaults.

## Layout

Three Maven modules, and the arrows between them only point one way.
Four Maven modules, and the arrows between them only point one way.

```
mynes-core/ depends on nothing
Expand All @@ -213,13 +239,22 @@ mynes-core/ depends on nothing
mynes/video/ colour indices to pixels: the overscan crop and the frame renderer
mynes/palette/ the measured RGB tables, and the loader that reads them out of /palettes

mynes-headless/ depends on core
mynes-patch/ depends on nothing either, core included
mynes/patch/ IPS patches, applied to a byte[] before anyone reads it as a cartridge

mynes-headless/ depends on core and patch
mynes/headless/ the command line mode

mynes-desktop/ depends on core and headless; FlatLaf and MigLayout live here
mynes-desktop/ depends on core, patch and headless; FlatLaf and MigLayout live here
mynes/ui/ the Swing window, Main, the key bindings, the CHR viewer, the debugger
```

`mynes-patch` is beside the console rather than inside it because IPS says nothing about what it
patches -- a ROM, a save file, a disk image -- and a patcher that could see a `Cart` would sooner or
later be handed one. It is the front ends that join the two together, both by reading the file,
patching the bytes and handing the result to `Cart.load`. A patch is entitled to rewrite the iNES
header, so it has to be applied *before* the cartridge is parsed rather than after.

The core knows nothing about the front end, and now it *cannot*: `Cart.load` takes a `byte[]`, `NES`
has no UI dependency, `nes.tick()` is the only clock, and the PPU emits colour *indices* -- never
RGB, because which RGB is a question about televisions. What used to be a rule about imports is a
Expand All @@ -230,6 +265,7 @@ Which makes one check worth running when the dependencies change:

```sh
mvn dependency:tree -pl mynes-core # nothing but the two test artifacts
mvn dependency:tree -pl mynes-patch # nothing but JUnit
mvn dependency:tree -pl mynes-headless # no FlatLaf, no MigLayout
```

Expand Down
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@ mvn -B package -DskipTests
java -jar mynes-desktop/target/mynes.jar
```

One jar, whichever way: it is three Maven modules -- `mynes-core` for the console, `mynes-headless`
for the command line, `mynes-desktop` for the window -- flattened into one file with its
dependencies. The core depends on nothing at all, which is the point of it being separate.
One jar, whichever way: it is four Maven modules -- `mynes-core` for the console, `mynes-patch` for
IPS patches, `mynes-headless` for the command line, `mynes-desktop` for the window -- flattened into
one file with its dependencies. The core depends on nothing at all, which is the point of it being
separate, and neither does the patcher.

The same `mvn package` also writes the release zip into `mynes-desktop/target/`, so what the releases
page carries is never anything a build here has not already made.
Expand Down Expand Up @@ -156,6 +157,12 @@ are also toggles to hide the background or the sprite layer without the game not
All of it is in headless mode too — `break`, `watch`, `step` and `disasm` are commands in the
interactive session, so the same questions can be asked from a script.

**IPS patches**, from **File > Open with Patch...**, which is how a romhack is handed out. The patch
is applied to the bytes on their way into the emulator, so the ROM on disk is left exactly as it was
and there is no patched copy of it to keep anywhere. A patched game keeps its own save states and
battery file, named after the patch rather than the ROM, so an afternoon with a hack cannot write
over fifty hours of the original. `--patch` does the same thing from the command line.

**Save states and battery saves**, and a **headless mode** for running with no window at all. Both
have a section of their own below.

Expand Down Expand Up @@ -265,6 +272,10 @@ seconds to start up, the jar about a third of one.
- **`--save-state` and `--load-state`** cut the wait when the same two hundred frames of title
screen are in the way of every run. `--sram-in` and `--sram-out` do the same for battery RAM, in
the `.sav` format other emulators read.
- **`--patch`** applies an IPS patch to the ROM before it is read as a cartridge, so a romhack can be
run without a patched file existing anywhere. The report's `cart.patches` says how many records
each one held, and `cart.sha256` is the digest of the patched image rather than the file on disk —
a patch that turns out to hold no records is one cut against a different dump of the game.
- **`--interactive`** reads commands on standard input and answers each with a line of JSON, for
when you do not yet know the question well enough to write it down. It is also where the debugger
lives without a window: `break`, `watch`, `step` and `disasm`, with `run` reporting back what
Expand Down
13 changes: 10 additions & 3 deletions mynes-desktop/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@
<version>${project.version}</version>
</dependency>

<!-- Named here as well as by mynes-headless, since Open with Patch uses it directly. -->
<dependency>
<groupId>com.github.dimiro1</groupId>
<artifactId>mynes-patch</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>com.formdev</groupId>
<artifactId>flatlaf</artifactId>
Expand Down Expand Up @@ -72,7 +79,7 @@

Run from the root and without naming a module, which is why the parent declares
this same plugin and this same execution id skipped: the goal is invoked on every
project in the reactor, and three of the four have no main class to give it.
project in the reactor, and four of the five have no main class to give it.

Both forks of the same goal, and deliberately not exec:java for the second, which
would be the obvious choice. exec:java cannot read this plugin's configuration at
Expand Down Expand Up @@ -130,8 +137,8 @@
<!-- Maven Assembly Plugin, building the two things that get handed out: the jar that
runs wherever a JDK does, and the zip that goes on the releases page.

The fat jar it packs is the other two modules and their dependencies flattened into
one file, so `java -jar mynes-desktop/target/mynes.jar` is the whole emulator.
The fat jar it packs is the other three modules and their dependencies flattened
into one file, so `java -jar mynes-desktop/target/mynes.jar` is the whole emulator.

The order these two executions are written in is load bearing. Maven runs several
executions of one plugin in the order the POM lists them, and the zip is packed out
Expand Down
Loading