Answer the bus with Game Genie codes, from a Hacks menu or --genie - #38
Merged
Conversation
A Game Genie is not a patcher, and that difference is the whole of the design. A patch rewrites the image before `Cart.load` sees it. A code rewrites nothing: the device plugged into the cartridge slot and the cartridge plugged into it, so it watched the CPU's address bus and drove its own byte onto the data pins in the cartridge's place. **Hacks > Game Genie...** and `--genie CODE` do the same. The seam is three lines in `MMU`. `mapper.prgRead` has exactly two callers in the tree -- `busRead` and `peek` -- which is what makes the hook provably total. It goes in `busRead` rather than in `read` so that an OAM transfer out of a PRG page and a DMC sample fetch see it too: the device sits on /ROMSEL and has no idea which unit inside the 2A03 is driving the address. The substituted byte is the one assigned to `dataBus`, because it is the Genie driving the pins and open bus keeps what was last put out. `peek` substitutes as well, so a disassembly is a listing of the instructions that actually run rather than of ones that never do -- at the price that the cartridge underneath is no longer readable through the bus, which `Cart.prgROM` still has. The field on `MMU` is null when there are no codes, and that is load-bearing twice. It keeps a machine nobody is cheating on down to a null check on the hottest line in the emulator, the same argument `writeListener` makes for the write side. And it is what keeps `SaveStateCompletenessTests` out of the device: a field holding null is recorded and stepped over, where one holding an object would be walked into and every array inside it scrambled. `GameGenie` puts the hook down as its first code arrives and takes it up as its last one goes, the way `Debugger` does with its watchpoints, so nothing outside has to remember to. Two things the hardware decides that the code had to follow. One address holds one code -- the device answers the bus in the time it has and does not get a second look -- so `add` replaces rather than stacking, deliberately unlike `IPSPatch.applyTo`, where two records may overlap and the later one wins. And a code can only reach $8000-$FFFF, because fifteen address bits and /ROMSEL is all the cartridge port carries; there is no code for cartridge RAM or for anything below it, and that falls out of the $8000 in the decode rather than needing a check. There is no `encode`. Six letters is 24 bits carrying a 15 bit address and an 8 bit value, and eight is 32 carrying those plus a compare, so bit 3 of the third letter is spare either way and is read by nothing -- `GOSSIP` would come back as `GOISIP`. Nothing needed the reverse direction, so it is omitted rather than shipped with a caveat, and a test pins the loss. `run.genie` is an array rather than a key per code, since the set is open ended and `run.hacks`'s key-for-key comparison cannot be had. It matters more than the other three things in that list: a patched run has its own `cart.sha256` and a cheated one cannot, because the cartridge really is untouched -- so `run.genie` is the only thing in the whole report that tells the two apart, and a save state taken with codes in will load into a machine with them out without complaining. Read back off the machine, so a `genie` part way through an interactive session is reported as the run ended rather than as it started. In the window the codes are session-only and per-cartridge. They survive a power cycle, which is exactly when somebody wants to watch one take effect from the reset vector, and go when another cartridge does, since a code written for one game is an arbitrary byte written over the next. The dialog holds the window's own list rather than reading the device, which the emulation thread is reading on every instruction; the whole list goes over on each change and is replayed. `GameGenieRunTests` is the worked example, and it is why the hello-world cartridge earns its keep twice. It makes **the same edit two ways** -- once as the `.ips` vendored beside it and once as ten eight-letter codes at the equivalent CPU addresses -- and asserts the two draw byte-identical pictures, both different from the plain run. That works because the cartridge is 32KB of PRG and so unmirrored, which makes file offset n exactly CPU $8000 + (n-16). Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A Game Genie is not a patcher — it sat between the cartridge and the console and drove its own byte onto the data pins in the cartridge's place — so Hacks > Game Genie...,
--genie CODEandgenie/ungenie/genie clearin the REPL rewrite nothing at all.The seam is three lines in
MMU:mapper.prgReadhas exactly two callers in the tree, which makes the hook provably total, and it goes inbusReadrather thanreadso an OAM transfer out of a PRG page and a DMC sample fetch see it too, with the substituted byte assigned todataBusbecause it is the Genie driving the pins; the field is null when there are no codes, which keeps a machine nobody is cheating on down to a null check and keepsSaveStateCompletenessTestsout of the device, since a field holding null is stepped over where one holding an object would have every array inside it scrambled.One address holds one code and a code can only reach $8000–$FFFF, both because the hardware says so, and there is deliberately no
encode— bit 3 of the third letter is read by nothing, soGOSSIPwould come back asGOISIP.run.geniematters more than the rest of the comparability list: a patched run has its owncart.sha256and a cheated one cannot, because the cartridge really is untouched, so it is the only thing in the report that tells the two apart and a save state taken with codes in will load into a machine with them out without complaining.GameGenieRunTestsmakes the same edit two ways — once as the vendored.ipsand once as ten eight-letter codes at the equivalent CPU addresses — and asserts the two draw byte-identical pictures, both different from plain;mvn -B clean testis green at 1674 tests and the distribution smoke test passes,SXIOPOturnsDEC $075AintoLDA $075Aon a real Super Mario Bros., and the dialog is screenshotted and built under test but has not been clicked through by hand.🤖 Generated with Claude Code