Discussion about software development for the old-school Gameboys, ranging from the "Gray brick" to Gameboy Color
(Launched in 2008)
You are not logged in.
This is a save file that, when loaded in Pokémon Red or Blue (English version only).
Demo / teaser video, includes a download link in the description :
https://youtu.be/XfDCMkl5-Ko
This was programmed in about a week or two by me, with suggestions and beta-testing from these guys from the Glitch City Laboratories Discord :
- Torchickens
- Wack0
- (late) Yeniaul
- Charmy
Memes ©201X-∞ Stryder7x. Used without permission because who cares.
Below are tech details, which also spoil a few functionalities of the hack. This was originally meant to be a simple hack, but features and ideas kept coming as I implemented them, so the resulting code has a lot of jumps that could be removed by re-organizing, but I didn't want to do that because seriously who would look at the code ?
(Hint : me, trying to bugfix. *Slap*)
I have two different entry points for ACE. The first one is the "map script" feature, the second is DMA hijacking.
The map script is a pointer in RAM, which is read on each frame spent in the overworld, and code is ran there. I have no restrictions on this code.
This pointer is saved by the game (so this ACE persists through reloads), but is changed when loading another map.
The DMA hijacking entry point is more versatile since it runs on every VBlank (even if it doesn't happen in the overworld), but doesn't persist through reloads. So it is set up by the map script.
The chosen map script entry point (little-endian pointer at $D36E) is $D321 (in the memory region allocated to the player's items).
ld a, [$CD39] and a jp nz, $D165 ; This is the main code
$CD39 is a byte the game sometimes writes to but never reads from. It isn't ever written to during our exploit, and it is reset when starting the game.
Thus, on the first frame of this being executed, the jump won't occur.
We first set $CD39 to $01 (won't be overwritten) to mark initialization as complete. Then we set the number of NPCs to 1 (because it's a relic from a previous attempt).
Then we copy the Luigi sprites in VRAM where the game will happily use them for our customized NPCs (which we stress are 100% legit).
Then we change a map block so as to remove the stairs. We don't remove the warp, so it can still be triggered by bonking into the black void on its right. PHYSICS !
(I did this because otherwise it was too easy to walk into the stairs right after seeing the Luigi. So this makes players hold right longer to trigger the warp.)
; Init stuff inc a ld [$CD39], a ; Mark init as done ld [wNbOfNPCs] ; Make sure there is only 1 NPC dec a ; Derp ld hl, $80C0 ld de, $DB00 ld bc, $0110 call CopyVideoData ; Copy $10 tiles from $DB00 in ROM bank $01 (lol) to $80C0 ld a, $05 ld [$C70C], a ; Remove the stairs from the map. The DMA hook also does it, but I forgot to remove this. ld hl, $D1AA ; Another derp of mine jp $DAB7 ; Go to next part
The routine that will be copied to HRAM.
@DA82 : (space allocated for the PC Pokémon) call $DD26 ld [$FF00+c], a
This one sets a byte that is crucial for manipulating the SNES' text. More on that waaaay below.
@DAB7 : ld a, $50 ld [$D4EB], a ; Set NPC #4's text ID
Here, we replace the `ld a, $C3 ; ldh [$FF46], a` with `call $DD26 ; ld [$FF00+c], a`. We will make it so $DD26 returns with a = $C3 and c = $46 so everything will run *fine*.
ld c, $80 ; The game's DMA routine starts at $FF80 ld hl, $DA82 ld b, $04 .hijackDMA ld a, [hli] ld [$FF00+c], a inc c dec b jr nz, .hijackDMA
$D2DF is a custom routine that makes text print our properly. Don't ask how it works, I don't even know myself. If you tell me how, I'll be very glad (and thankful) you did, however
(This game's text engine is a mess, by the way.)
ld hl, $D1AA ; Text pointer jp $D2DF ; Custom text printing routine
Now, the map script that isn't init :
Due to how NPCs work, the Luigis tend to turn around, so I manually set them not to.
For some reason when there are many Luigis on-screen, this script seems to have issues.
It's not a bug, it's a feature.
@D165 : ld hl, $C119 .turnWeegees ld [hl], $00 ld a, $10 add a, l ld l, a cp $09 jr nz, .turnWeegees
We check if SELECT has been pressed during this frame, otherwise we end it here.
ldh a, [$FFB3] and $04 ret z
We check if we have spawned 10 Luigis so far, and if that's the case we jump to $D1A4
ld hl, $D4E1 ; Number of sprites ld a, [hl] inc a cp $0B jr z, $D1A4
We manually set the newly spawned Luigi's attributes, and then we go on to printing the "Another Luigi!" text.
ld c, a inc c inc c swap a ld l, a ld h, $C1 ld a, $02 ld [hl], a ; Set sprite "picture ID" inc h ld a, $04 add a, l ld l, a ld a, c ld [hli], a ; Set sprite's Y coord ld a, $0E ld [hli], a ; Set sprite's X coord ld [hl], $FF ; Set movement to none (but can STILL turn !!) ld a, $08 add a, l ld l, a ld [hl], $02 ld hl, $D21F jp $D2DF
This one prints the "Unfortunately," (etc.) text. The text engine has commands to run ASM code, and it's used to run the crashing code, using the `jp $D53E` at $D289.
@D1A4 : ld hl, $D241 jp $D2DF
The "crash" function :
Writing something nonzero to $CFC7 causes the music to fade out and the corresponding (by ID) sound/music to start playing afterwards. By fuzzing around, I found that rra-ing the value of a when it was called produced a somewhat screeching effect, perfect for our faked "game crash".
@D53E : (Item PC, right after the Potion, although the PC isn't usable in this hack :P) (It was intended to be, but then...) rra ld [$CFC7], a
We wait for the music to fade before proceeding. This is fairly common in R/B/Y crashes.
ld bc, $0078 call DelayFrames ($3739)
We copy our "stripes" tile (located at $DC62) in a tile slot shared by BG and OAM.
ld hl, $8C00 ld de, $DC62 ld bc, $0101 call CopyVideoData
We fill the WRAM tilemap with this beautiful stripe pattern, which is encountered very commonly in R/B/Y crashes since all rst vectors have the instruction "rst $38", which leads to a crash nicknamed the "00 39 crash" (guess why)
Maybe something similar would have been possible with sprites, but I decided to roll differently.
ld hl, wTileMap ld a, $C0 ld bc, $0168 jp $D568 @D568 : call FillMemory
The WRAM tilemap is transferred to the VRAM tilemap roughly a third at a time, so we call a routine in ROM that waits for three frames (so the whole tilemap is transferred).
Then we disable interrupts because they tend to get in the way (mostly with sprites).
call Delay3 di
We modify OAM so all sprites display our custom "stripe" tile.
.waitVBlank ldh a, [$FF44] cp a, $90 jr nz, .waitVBlank ld de, $FE02 ld a, $C0 ld b, $28 .0039ify ld [de], a inc e ; You know why not "inc de". inc e inc e inc e ; YOU KNOW IT. dec b jr nz, .0039ify
We then wait for $3C frames.
DelayFrames relies on the VBlank interrupt, so... nope !
ld b, $3C .delayBFrames ldh a, [$FF44] ; LY and a jr nz, .delayBFrames .waitVBlank ldh a, [$FF44] cp $90 jr nz, .waitVBlank dec b jr nz, .delayBFrames
We copy that "GAME CRASH" string on the screen,
ld hl, $D55E ld de, $9C00 ld bc, $000A call CopyData
And we "crash" for real.
jr $
So that was what the map script does. But wait, there's more ! Because there is a second ACE entry point, which was setup by this one : DMA hijacking ! This one performs more advanced amnipulations, including changing text box contents on-the-fly. (That was a pain to do, btw)
If you remember from above, the DMA hook is located at $DD26.
This checks the WRAM tilemap. This tile will have this value if and only if the game is trying to draw the START menu.
ld a, [$C3AA] cp $79 jr nz, $DD35
This game sets SP to $DFFF during init (yep, leaving $DFFF unused), so we are actually manipulating the stack. This will be our "attack vector" from within DMA hijacking.
ld hl, $DFF9 ld [hl], $38 inc hl ld [hl], $DD @DD35 : jp $DD43 ; Within jr reach, but this changed a lot during development.
This works, I'm not exactly sure how, but the way the START menu works, this will trigger as it is in its "Init" routine.
The intended code path is to then call some functions that process the START menu, then call CloseTextDisplay.
We basically set the Init routine to short-circuit directly to CloseTextDisplay, thus the START menu closes all by itself without even making a sound (usually done right after returning from the Init routine). Plop.
Saving causes a ton of issues, so this is a (Microsoft-certified) fix for this problem !
Also if it opens fully then closes, sprites can be reloaded which turns Red's mom into Luigi.
Woooooooooooooooopsies.
@DD38 : pop hl ld de, $DD41 ; Points to a "ret", but without it we crash. No idea why. push de push hl jp CloseTextDisplay
The map with ID $00 is Pallet Town. This will be set right as you exit Red's house !
@DD43 : ld a, [wCurMap] and a jr nz, $DD51 ; Which jumps to $DEA0. Yeah, 3am programming for the win.
This makes the VBlank handler return to $DD54, and proceeds with the rest of the DMA hijacking routine.
ld hl, $DFF7 ld [hl], $54 inc hl ld [hl], $DD jp $DEA0
This calls the function at 1C:41A0, which takes care of the whole fadeout (which looked nicer than I expected) and Hall of Fame sequence.
Then we wait for a few frames, and jump to $DDD1, which carries out the remainder of the ending screen. I'm not going to detail it because it's very long and complicated and ugly and it's 1am and my term exams are in three days oh god I shouldn't even be doing this what am I doing with my life.
@DD54 : ld a, $01 ld [wCurMap], a ; Make this trigger shad ap. ld b, $1C ld hl, $41A0 call Bankswitch ld c, $60 call DelayFrames jr $DDD1
This string of writes only occur in the house's 1F, because even though we remove the stairs physically, they stay graphically because they aren't redrawn until they go off-screen. So we erase them for good.
@DEA0 : ld hl, $9908 ld a, [hl] sub $0C jr nz, $DEAF inc a ld [hli], a ld [hl], a ld l, $28 ld [hli], a ld [hl], a @DEAF : jp $DA94
This checks the WRAM tilemap to see if there is a "e" character (say hello to this game's proprietary and weird encoding, featuring $50 and $57 as terminator characters ! )
If there is one, we know we are trying to print the SNES' glitched text, which is actually the Pokémon Center healing text. Because we made it so.
If such text is detected, then we slap "LUIGI OVERLOAD" on top and manipulate the stack
@DA94 : ld de, $C4BA ld a, [de] cp $A4 jr z, $DAE1 dec de ld hl, $DA86 ; "LUIGI OVERLOAD" ld bc, $000E call CopyData ld de, $DFDB ; Somewhere in the stack ld a, $1C ld [de], a inc de jr $DACF
A bit of explanation on this one, skip it if it's boring : during testing, Wack0 pressed A a little too quickly and discovered that after spawning 3 Luigis or more (so there would be 4+ of them), the SNES would print out garbage text. By tracing function calls, I discovered that the developers arbitrarily assigned text ID #4 to the SNES. Note that there are only two things you can interact with in this room, the SNES and Red/Stryder's PC. -_-
When the game tries to spawn a text ID, it checks if the ID is higher than the number of NPCs in the map. Hence, spawning a 4th Luigi would have the game switch from the intended code path to another where instead of a specific index assigned to the SNES it would use the index attached to the 4th Luigi. This index is stored at $D4EB, and by default it is $00, which underflows to $FF (because Game Freak doesn't know how to index arrays -_________-) and causes a bad pointer to be read, print glitch text, and eventually nuke the game. Oops.
By setting the index to $50 (done waaaaaay above), the game reads a $0000 pointer (little-endian, but meh). Since the devs were dicks, they put $FF on all rst vectors, so the game reads a $FF. This prompts it to print the Pokémon Center Nurse dialogue.
Now that by itself was swaggy enough, but the GCL Discord pals kept asking for ACE. So I made a way to display arbitrary text.
For my work to be complete, we would need to make LUIGI OVERLOAD a meme.
As Wack0 summed it up : "Press A on the SNES with 3 Luigis".
We manipulate the stack so the remaining of the nurse's text isn't printed, the game waits until A or B is pressed, and closes the text box
@DACF : ld a, $D2 ld [de], a ld e, $EF ld a, $5A ld [de], a inc de ld a, $70 ld [de], a jr $DAAF ; Return from DMA hijacking
.
This detects when the player's PC is booted up. It replaces "PC" with "N64" and manipulates the stack so the handler will return $DC00 instead of the PC's processing function.
@DAE1 : ld de, $C4E5 ld a, [de] cp a, $8F jp nz, $DC72 ld hl, $DADD ; "N64" ld bc, $0004 call CopyData ld de, $DFED xor a ld [de], a inc de ld a, $DC ld [de], a jr $DAAF ; Return from DMA hijacking
This is the custom N64 handler.
ld hl, $D730 res 6, [hl] ; Disable "instant text" ld hl, $DC0E call PrintText jp $796D ; Make the PC's beep and then close the text box
We check the map script, and if it is in ROM, we set it in WRAM and modify the text pointers to be mapped to our custom strings. This wasn't possible upstairs due to special (read : stupid and horribly programmed) text rules.
@DC72 : ld a, $02 ld [wNumberOfWarps], a ; Removes the stairs' warp in 1F, does nothing in 2F, extra warp is OoB by luck ^^ ld a, $05 ld [$C70C], a ; Remove stairs. Again. ld hl, $D36F ; Check map script ld a, [hl] cp $41 jp nz, $DAAF ; Return from DMA hijacking ld [hl], $DD ld hl, wMapTextPtr ld [hl], $92 inc hl ld [hl], $DC jp $DAAF
And this sets the proper values for the OAM DMA to carry out nicely. The game doesn't even realize it's being torn apart !
(The user does, however)
@DAAF : ld c, $46 ld a, $C3 ret
~
tl;dr : Don't code directly with BGB's "edit code/data" feature and a Notepad window next to it.
Also, Paper Mario 0x A presses COMING SOON !!!!1
Good night.
Offline
Wow, a hack contained within a .sav file, that's cool! I'll have to check it out!
Also...
ISSOtm wrote:
For my work to be complete, we would need to make LUIGI OVERLOAD a meme.
That would be a fun one! With a LUIGI OVERLOAD, you could take over the world!
Offline