Gameboy Development Forum

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.


#1 2017-04-27 19:33:44

From: Somewhere in Echo RAM
Registered: 2017-04-18
Posts: 160

Pokemon Red, Stryder7x edition - A game within a game

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 :

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
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 big_smile
(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
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...)
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

We modify OAM so all sprites display our custom "stripe" tile.


ldh a, [$FF44]
cp a, $90
jr nz, .waitVBlank

ld de, $FE02
ld a, $C0
ld b, $28

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
ldh a, [$FF44] ; LY
and a
jr nz, .delayBFrames
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 ! big_smile
Also if it opens fully then closes, sprites can be reloaded which turns Red's mom into Luigi.


@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


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 ! big_smile)
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



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 ! big_smile
(The user does, however)



ld c, $46
ld a, $C3


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.

The French Lord Of Laziness.
Legend of Zelda and Undertale fan, I also tend to break Pokémon R/B/Y a little too much.

Twitter | Me on GCL | Discord : ISSOtm#9015 | Skype : (I don't login anymore)



#2 2017-05-01 22:45:48

New member
Registered: 2017-04-16
Posts: 5

Re: Pokemon Red, Stryder7x edition - A game within a game

Wow, a hack contained within a .sav file, that's cool! I'll have to check it out! smile

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! big_smile



Board footer

Powered by PunBB
© Copyright 2002–2005 Rickard Andersson