This page is a mirror of Tepples' nesdev forum mirror (URL TBD).
Last updated on Oct-18-2019 Download

Reverse engineering Mega Man

Reverse engineering Mega Man
by on (#124517)
I've been making a Mega Man clone/engine based on TASVideos's Rockman Data page (specifically the first game). As the page doesn't cover everything, I've done some reverse engineering myself using FCEUX and its memory inspection tools. Using those, I've discovered possible errors in TASVideos's documentation.

The problem is that I'm not sure whether the errors I found are actually errors, because the game behaves weirdly. I think it's possible that I'm getting the wrong idea because of how frame advancing works in emulators.

I first thought this when verifying collisions while Mega Man is jumping upwards. When Mega Man bumps his head into a platform, his Y speed to a constant. According to the documentation, it's −0x00.C0, but my testing reveals it's actually −0x00.40. But that's not the problem. The problem is that this value seems to be set one frame before the collision occurs. Looking at the memory values, the next frame actually adds the Y speed from before the constant was set.

There are other inconsistencies.

If I press the jump button and advance a frame, Mega Man's Y speed is set to 0x04.DF as expected. However, his Y coordinate doesn't change until the next frame. Then another frame later his sprite finally changes to the jumping one.

When I let go of the D-pad, Mega Man's X speed isn't set to 0x00.80 in the next frame, but only in the frame after that. This makes his deceleration take one more frame than is expected from the documentation (though strictly speaking what it says isn't wrong here).

So, it's like there's a delay, which may be a result of when the emulator decides to pause between frames.

It may also be related to how I expect the game to behave. I expect the game to first check for input, then adjust velocity based on that, then update the coordinate data, and finally draw.

Can someone enlighten me?
Re: Reverse engineering Mega Man
by on (#124530)
BenoitRen wrote:
I think it's possible that I'm getting the wrong idea because of how frame advancing works in emulators.

You can perform these tests without the frame advancing (which might have different timing than you expect, specially across emulators). Put a breakpoint in the NMI routine instead. There might also be some input lag... to get around that I'd simulate input by directly modifying the RAM location where he controller bits are normally written to, and then I'd lock the value (FCEUX can do this) so that the real input doesn't overwrite it.

Then on every break you can take a look at the engine's state, and possibly change the input for the next frame. I think this is much more reliable than real input and frame advancing.
Re: Reverse engineering Mega Man
by on (#124547)
I've managed to isolate the memory addresses that are used to store input. They are 0014 and 0016. 0018 stores what is newly pressed in the current frame. The byte's bits are arranged as: right, left, down, up, Start, Select, B, A.

I put a breakpoint at the NMI routine using the debugger, which is D4A8. Next I used the Hex Editor to set the input values for the jump button and freeze the two first ones.

While the debugger does stop at the breakpoint each cycle at the click of the Run button, the memory values for the Y coordinate and Y speed don't change.

Yet as long as those input addresses are frozen, Mega Man isn't responding, so they are definitely related.

What am I doing wrong?
Re: Reverse engineering Mega Man
by on (#124548)
Don't games normally do game processing during a given frame, then write them to the PPU during VBL at the beginning of the next frame? Or maybe you're taking this into account.
Re: Reverse engineering Mega Man
by on (#124550)
BenoitRen wrote:
When Mega Man bumps his head into a platform, his Y speed to a constant. According to the documentation, it's −0x00.C0, but my testing reveals it's actually −0x00.40. But that's not the problem. The problem is that this value seems to be set one frame before the collision occurs. Looking at the memory values, the next frame actually adds the Y speed from before the constant was set.

If I press the jump button and advance a frame, Mega Man's Y speed is set to 0x04.DF as expected. However, his Y coordinate doesn't change until the next frame. Then another frame later his sprite finally changes to the jumping one.


I don't know the details, but just my guesses:

For the first problem, maybe the documentation was oversimplifying negative numbers? Since negative hex 40 is stored as hex FFC0... Perhaps the documentation lopped off the $FF..?

Also, for the second, expect a delay. In gravity-based simulations, acceleration is added to velocity, then velocity added to position. Sometimes these additions don't all happen on the same frame, since an instantaneous change in position based on a new velocity would mean zero acceleration time (kinda physically impossible). Perhaps the developers found that delaying the position change by 1 frame made MM's movements smoother and more realistic.
Re: Reverse engineering Mega Man
by on (#124556)
BenoitRen wrote:
Yet as long as those input addresses are frozen, Mega Man isn't responding, so they are definitely related.

I just tried setting up the NMI breakpoint, and after the first break I froze $0014 to $01. After that, I had to push "run" 3 times before the jump sprite showed up on the screen, but Mega Man certainly jumped.
Re: Reverse engineering Mega Man
by on (#124582)
It looks like the problem was that I was setting both $0014 and $0016 to 01. When I only set $0014, it does work.
Re: Reverse engineering Mega Man
by on (#124583)
Here are the results.

Vertical motion (jump):
  • The Y speed is set in the next frame. However, the Y coordinate isn't changed yet.
  • The Y speed is decreased with the gravity constant. The Y coordinate is decreased by the Y speed of the previous frame.
  • Same as above, except that now the jump sprite is set, and it has the Y coordinate of the previous frame.
  • etc.

Horizontal motion:
  • The X speed is set in the next frame. The X coordinate is adjusted with this new X speed immediately.
  • Same as above, but now the sprite on the screen is updated with the X coordinate of the previous frame.

So there seems to be a delay between the in-memory values and the screen. With vertical motion, there's even a delay between Y speed and the Y coordinate.

If you ignore these delays, TASVideos's data is correct (except for that constant that is set after vertical collision, of course).

This makes vertical collision detection even weirder, though, because this means that it is detecting it two frames in advance!
Re: Reverse engineering Mega Man
by on (#124598)
BenoitRen wrote:
So there seems to be a delay between the in-memory values and the screen.

How exactly are you verifying the existence of this delay?

NES games do all their calculations while the previous frame is being rendered to the screen, and once VBlank starts, the new data (sprites, backgrounds, etc.) is sent to the PPU to be rendered during the next frame. Since you have placed a breakpoint at the start of the NMI (i.e. the very start of VBlank), that's before all the new data is sent to the PPU, so you certainly won't be seeing any changes calculated during this frame yet. Only when the next break happens will the frame using the current data have been completely rendered, at which point the data for the next frame will be ready to be written to the PPU.
Re: Reverse engineering Mega Man
by on (#124606)
tokumaru wrote:
How exactly are you verifying the existence of this delay?

By comparing the change in coordinates to what I see on the screen.

I didn't know when the NMI took place; I just followed your advice, which to me implied that the delay I was seeing was because of a timing issue. But if it's right before the frame rendering, that certainly explains what I'm seeing.
Re: Reverse engineering Mega Man
by on (#124615)
BenoitRen wrote:
which to me implied that the delay I was seeing was because of a timing issue.

I just wanted to be sure you knew in which order things happened, so that you wouldn't miscalculate the delay.

Quote:
But if it's right before the frame rendering, that certainly explains what I'm seeing.

Yes, the NMI fires after the input has been processed (well, ideally, but sometimes the game logic takes too long and doesn't finish in time for VBlank, but that's another story :)) but before the new graphical data is sent to the PPU and the frame is displayed.
Re: Reverse engineering Mega Man
by on (#125494)
I'm as good as done replicating the physics, and almost done with replicating the sprite animation. Thanks for the help with getting that far, everyone. :)

There's one last sprite animation I have to implement, which is Mega Man slumping over the top of a ladder before standing on top of it. I'm having trouble figuring out when exactly he transitions into it.

Testing was done with two ladders whose top tile has the same Y coordinate. On one ladder, he transitions into it when the Y coordinate is 105.192. On another ladder, the Y coordinate is 106.64. In both cases, the Y coordinate is substracted in steps of 0.192.

When doing Y collision detection, the game checks one step in advance. In the previous cases, that'd mean it checked for collision with Y coordinates 105.0 and 105.128. At first glance, this would seem to indicate that collision at Y coordinate 105 is the key, but if that's the case, the transition would have occurred one frame sooner on the first ladder. See, one step back, the Y coordinates would be 106.128 and 107.0. It would then check for collision with 105.192 and 106.64.

Any ideas?
Re: Reverse engineering Mega Man
by on (#125792)
I figured it out. During climbing, the speed is always 0x00C0, even though the memory value of the Y speed is set to a different number. However, that memory value is used for collision detection, and that's how I figured out that in my example Y coordinate 105 during collision detection is the point where the animation changes.
Re: Reverse engineering Mega Man
by on (#154997)
This is probably a dumb question, but Mega Man's physics work with a viewport of 240 lines, right? FCEUX by default draws 224 lines, so reverse engineering that involved the Y coordinate in combination with screenshot comparison values probably has to be redone.
Re: Reverse engineering Mega Man
by on (#154999)
Quote:
This is probably a dumb question, but Mega Man's physics work with a viewport of 240 lines, right? FCEUX by default draws 224 lines, so reverse engineering that involved the Y coordinate in combination with screenshot comparison values probably has to be redone.


Shouldn't matter. The aspect ratio of the game image is still the same. That extra space is merely cropped.
Re: Reverse engineering Mega Man
by on (#155001)
I know, but if 8 pixels of the top are cropped, then when I open a 256x224 screenshot in Paint, the Y value I see when hovering over a pixel is off by 8 if the coordinate origin is at the top left of the original 256x240 image.
Re: Reverse engineering Mega Man
by on (#155005)
The NES produces a signal that is a full 256x240, but old CRT TVs would project the picture in a way as to cut 8-12 pixels off the top and bottom (Overscan). Many emulators try to simulate this loss by cropping 8 pixels off the top and bottom. If you put a sprite at 0,0 with this setting it woud disappear off the top of the screen (or perhaps only the bottom line of pixels would be visible)

In FCEUX, you can edit the display to show all 240 scanlines, but this won't affect the game engine/physics in any way, just the display.
Re: Reverse engineering Mega Man
by on (#155006)
Even though you're not seeing those scanlines, they're still there, and you have to take this into account when analyzing the coordinates if, for whatever reason, you don't want to make those lines visible.

Everyone who's dealing with NES development in any way SHOULD NOT be letting emulators hide scanlines. That's a feature intended for players who might be bothered by the glitches that are often visible in those areas, but if you're making a game or studying one, you should definitely be looking at the entire picture.
Re: Reverse engineering Mega Man
by on (#155061)
tokumaru wrote:
if you're making a game or studying one, you should definitely be looking at the entire picture.

It's often more useful for testing and development to see that stuff, but not always. I turn it on and off frequently. It's very important to know that those lines are going to be cut off. It's useful to hide them when testing to make sure everything that needs to be visible still is, and make sure you aren't putting visual gameplay clues in a place where they'll be lost. (Easy to forget that you've done this if you always test with the extra lines on.)
Re: Reverse engineering Mega Man
by on (#155081)
I can't figure out the logic behind the transition of Mega Man's normal climbing animation to his 'slumped over' climbing animation. While I thought I had figured out the point where it transitions, it always seemed arbitrary. More testing reveals that it's probably taking more conditions into account.

The top ladder tile is at height 128. I've observed the transition to happen at Mega Man Y coordinate 139.064, 138.000 and 137.192. It must take into account some value like how long he has been climbing the ladder or something, but I can't figure it out.
Re: Reverse engineering Mega Man
by on (#155095)
Quote:
Y coordinate 139.064, 138.000 and 137.192


Huh? You are playing the NES version? NES Y coordinates should be integers.
Re: Reverse engineering Mega Man
by on (#155096)
dougeff wrote:
Quote:
Y coordinate 139.064, 138.000 and 137.192


Huh? You are playing the NES version? NES Y coordinates should be integers.

Visually, yes, but the game might internally store a more prefise (subpixel) coordinate.
Re: Reverse engineering Mega Man
by on (#155099)
I assume the decimal point notation was a nonstandard way of indicating UQ8.8 fixed point coordinates: 139+64/256, 140+0/256, and 140+192/256.
Re: Reverse engineering Mega Man
by on (#155106)
tepples wrote:
I assume the decimal point notation was a nonstandard way of indicating UQ8.8 fixed point coordinates: 139+64/256, 140+0/256, and 140+192/256.

Ah yeah, if so, he should definitely adopt a different notation to avoid misunderstandings.
Re: Reverse engineering Mega Man
by on (#155111)
Are you aware that Rockman had been disassembled, with a fairly good set of labels?
Re: Reverse engineering Mega Man
by on (#155154)
in my opinion you should probably use megaman 3 or 4's mechanics. 1, 2, and 6 are just weird/messed up ...glitchy. :|
Re: Reverse engineering Mega Man
by on (#155189)
My apologies, I was not aware of the existence of a standard notation for this. UQ8.8 fixed points coordinates are indeed what I was communicating.

I'm aware that Mega Man has been disassembled, and did take a look, but I can't make heads or tails of it.

While I started with the physics for the first game, the plan is to incorporate the physics for all six NES games.
Re: Reverse engineering Mega Man
by on (#155286)
I took a longer look at the disassembly, and found this:
Code:
CurrentTileState        = $30
;CurrentTileState bits are:
;  #$10: (bit 4) - Rockman is climbing up
;  #$08: (bit 3) - Ladder above that
;  #$04: (bit 2) - Ladder above
;  #$02: (bit 1) - Ladder here
;  #$01: (bit 0) - there is a ladder below this tile
;
; If both bits 2,3 are clear, this is a ladder top (or not ladder)
; If the value is #$01, it's a ladder top

I don't understand the difference between bit 3 and bit 2. Anyone know?
Re: Reverse engineering Mega Man
by on (#155438)
BenoitRen wrote:
I took a longer look at the disassembly, and found this:
Code:
CurrentTileState        = $30
;CurrentTileState bits are:
;  #$10: (bit 4) - Rockman is climbing up
;  #$08: (bit 3) - Ladder above that
;  #$04: (bit 2) - Ladder above
;  #$02: (bit 1) - Ladder here
;  #$01: (bit 0) - there is a ladder below this tile
;
; If both bits 2,3 are clear, this is a ladder top (or not ladder)
; If the value is #$01, it's a ladder top

I don't understand the difference between bit 3 and bit 2. Anyone know?


From the choice of words I'm assuming this is best understood in the opposite order. Bit 2 could be "there is a ladder right above" and bit 3 could be "there is a ladder right above the previous above check". Might be used for the "putting foot on platform to step up from ladder" animation frame towards the top of a climb.
Re: Reverse engineering Mega Man
by on (#155487)
mikejmoffitt wrote:
From the choice of words I'm assuming this is best understood in the opposite order.

The thought had occured to me as well, but it's weird that the list would be reversed. I guess I'll just have to test!
Quote:
Might be used for the "putting foot on platform to step up from ladder" animation frame towards the top of a climb.

Indeed, that's exactly why this 'structure' interests me. :)
Re: Reverse engineering Mega Man
by on (#155517)
CurrentTileState is definitely linked to ladders, but I don't fully understand it yet.


While Mega Man is climbing, CurrentTileState is 030, which means that all bits are set except for bit 0. As soon as the top of Mega Man's bounding box touches a tile that isn't a ladder, bit 4 is unset, resulting in 014. According to the disassembly, this would mean that the game no longer consider Mega Man to be climing.

But it gets weirder. The next frame, CurrentTileState is set to 006, which means that bit 2 and 1 are set. The strange thing is that this doesn't seem to depend on Mega Man's position, as I've observed this change can happen even though the integer part of the Y coordinate hasn't changed.

On the second frame that the value is 006, Mega Man's animation changes.

The frame before Mega Man stands on top of the ladder, CurrentTileState becomes 002. This means that only bit 1 is set, which means there's a ladder "here".

When Mega Man is standing on top of the ladder, the value is 001, which means bit 0 is set, which means that there's a ladder below.

While Mega Man is jumping in front of a ladder, CurrentTileState is 030, which means that all bits are set except for bit 0. Yet he isn't climbing. I've also tried jumping below a hanging ladder, and it in combination with a previous observation seems to suggest that bit 4 is set when the top of Mega Man's bounding box touches a ladder tile.

One frame after Y speed becoming negative, CurrentTileState becomes 015 and stays that way until Mega Man lands. This means that all bits are set except for bit 4. This suggests that checking for a ladder below is only done when Mega Man is falling. A quick test climbing down a ladder confirms this, as it yields odd-numbered values of 003, 007 and 015.

And finally, when Mega Man is standing below that low-hanging ladder in Elec Man's stage that makes him dance when holding down, CurrentTileState is 008, which means that bit 3 is set, which means there's a ladder above him, but not above that (which is incorrect).
Re: Reverse engineering Mega Man
by on (#155539)
BenoitRen wrote:
And finally, when Mega Man is standing below that low-hanging ladder in Elec Man's stage that makes him dance when holding down, CurrentTileState is 008, which means that bit 3 is set, which means there's a ladder above him, but not above that (which is incorrect).

This is true if I make the assumption that bit 4 should be set when the top of Mega Man's bounding box touches a ladder tile. But according to the disassembly it means there's a ladder tile above "that", but not right above.

When I think about it, bit 4 isn't set either when standing in front of a ladder, so perhaps it's only used while Mega Man is in a vertical movement state (by jumping or climbing).
Re: Reverse engineering Mega Man
by on (#155545)
Further complicating matters is that there's also such a thing as CurrentTileStateBits, which is defined as follows:
Quote:
.byte $08 ;ladder here
.byte $04 ;ladder above
.byte $02 ;ladder above that

It only gets used once, for updating CurrentTileState:
Code:
0001D7C6: A5 30     lda CurrentTileState
0001D7C8: 1D DB D7  ora CurrentTileStateBits,x
0001D7CB: 85 30     sta CurrentTileState

Looks like I'll have to check out address $D7DB, because that address is mapper-controlled and there doesn't seem to be any specific information about it.
Re: Reverse engineering Mega Man
by on (#155574)
The value of CurrentTileStateBits is always 8. What the heck?
Re: Reverse engineering Mega Man
by on (#155619)
Trying to read the ladder climbing subroutines.
Code:
Ladder_ClimbUp                       ;well, up is pressed.
$97F7> A0 00:   LDY #$00             ;XSpeed
$97F9> A2 C0:   LDX #$c0             ;XSpeedFraction (speed is +0.75)
$97FB> A5 30:   LDA CurrentTileState         
$97FD> 29 0C:   AND #$0c       
$97FF> D0 0E:   BNE +

0x02 & 0x0C = 0x00
0x06 & 0x0C = 0x04
0x0E & 0x0C = 0x0C
0x1E & 0x0C = 0x0C

CurrentTileState is 2 right before he gets off the ladder, so in that case it branches to this:
Code:
; Adjust Rockman's position above the ladder so that he'll not fall
; If he's little too high, this may place him in ceiling. -Bisqwit
$9801> AD 0006: LDA ObjectPosY+0     
$9804> 29 F0:   AND #$f0       
$9806> 38:      SEC             
$9807> E9 0C:   SBC #$0c       
$9809> 8D 0006: STA ObjectPosY+0     
$980C> 4C 6E98: JMP Ladder_Release

At this point, during my testing, Y is either 99 or 98. ANDing either with 0xF0 (240, the screen height) results in 96, the coordinate of the top ladder tile. Then 96 is subtracted with 0x0C (12, half of Mega Man's height), resulting in 84, which places him right above the ladder. Then it jumps to code that exits the climbing state.

If he wasn't about to get off the ladder, this code is executed:
Code:
$980F> 29 08:   AND #$08       
$9811> D0 25:   BNE Ladder_Climb

If CurrentTileState was 6, which meant that he was climbing the top of the ladder, ANDing it with 0x0C resulted in 4. In the other cases, it resulted in 0x0C.

0x06 & 0x08 = 0x00
0x0E & 0x08 = 0x08
0x1E & 0x08 = 0x08

So if he is climbing the top of the ladder, it doesn't branch, and we get to:
Code:
$9833> A9 17:   LDA #$17  ;getting off the ladder
$9835> 8D 0004: STA ObjectSpriteNum+0

This sets the ladder top climbing animation, as 0x17 is its ID.

Now the issue remains that Mega Man only switches to this animation the second frame that CurrentTileState is 6. However, reading further:
Code:
Ladder_Climb
$9838> AD 4006: LDA ObjectFireDelay+0       
$983B> F0 04:   BEQ +     ;don't climb if weapon fire delay is active
$983D> A2 00:   LDX #$00
$983F> A0 00:   LDY #$00       
+
$9841> 8C 8006: STY ObjectYSpeed+0
$9844> 8E 6006: STX ObjectYSpeedFraction+0       
$9847> 20 DECB: JSR UpdateCurrentTileState       
$984A> A5 30:   LDA CurrentTileState         
$984C> F0 20:   BEQ Ladder_Release               
$984E> 20 C49B: JSR ObjectDoCollisionChecksAndAvoidWalls       
$9851> B0 1B:   BCS Ladder_Release               
$9853> 60:      RTS

We see that CurrentTileState is updated right after Mega Man has climbed up or down. So, when CurrentTileState is first set to 6, the ladder climbing subroutine has already been run. It's only on the next frame that it'll notice that he's climbing the top.

Things are starting to make sense!

Figuring out how CurrentTileState is updated is the final piece of the puzzle. Yesterday evening I tried to read the UpdateCurrentTileState routine, but I wasn't able to figure it out.
Re: Reverse engineering Mega Man
by on (#155638)
Made another attempt at reading it, and I've got it almost all figured out. The Y coordinates of the relevant tiles are retrieved in ObjectVerifyBackgroundCollision:
Code:
0001CB6F: 20 9C C3  jsr SwitchBankStage                             ; $C39C
0001CB72: A5 01     lda $01
0001CB74: 85 0C     sta $0C
0001CB76: A5 00     lda $00
0001CB78: 85 0D     sta $0D
0001CB7A: A6 2F     ldx RefObjectNumber
0001CB7C: D0 1B     bne +                                           ; $CB99

This copies the coordinates to the memory locations used by ReadCurrentStageMap and branches if the object is not Mega Man (RefObjectNumber is set to 0 at the start of MegaManAI).
Code:
0001CB7E: A2 02     ldx #$02
-
0001CB80: 18        clc
0001CB81: A5 03     lda $03
0001CB83: 7D 96 CB  adc CB96_table,x
0001CB86: 85 0E     sta $0E
0001CB88: 20 B7 CC  jsr ReadCurrentStageMap

0001CB8B: 95 2A     sta $2A,x
0001CB8D: CA        dex
0001CB8E: 10 F0     bpl -

The x register is set to 2 because it'll be counting down for each tile that is checked. Then it enters a loop that it will only branch out of when the register is 0. The Y coordinate is loaded into the accumulator, then a value from a table is added to it. The value retrieved from the table depends on the x register. The table consists of the following values: $F4, $FC, $0B. Advantage is taken of 8-bit overflow to simplify the code. The value is stored where ReadCurrentStageMap expects it, and then the subroutine is called. ReadCurrentStageMap will store the tile type found at the coordinate in the accumulator. Those tile types are stored sequentially in memory. Next AnalyzeCurrentTile is called, which I haven't looked at in detail yet.
Re: Reverse engineering Mega Man
by on (#155646)
AnalyzeCurrentTile isn't so hard. Basically, it iterates over the sequentially stored tile types, and if the current value is a ladder (0x02), it ORs with a value of the CurrentTileStateBits table table.

If the tile at Y + 0x0B is a ladder, OR 0x02
If the tile at Y + 0xFC is a ladder, OR 0x04
If the tile at Y + 0xF4 is a ladder, OR 0x08

Putting all of this info together, CurrentTileState consists of the following bits:
Bit 1: ladder at (but not under) Mega Man's feet (+11 from origin)
Bit 2: ladder at Mega Man's eyes (-4 from origin)
Bit 3: ladder at top of Mega Man's head/bounding box (-12 from origin)

Bit 0 and bit 4 are set afterwards in the rest of the UpdateCurrentTileState routine. YSpeed is subtracted from Mega Man's Y position (but not stored), and based on Mega Man's vertical direction, half of Mega Man's height (12) is added or subtracted from his Y position. The game then checks if a ladder tile is at those coordinates, and if there is, sets bit 0 if Mega Man is moving down, or bit 4 if Mega Man is moving up. It's basically testing if there will be a ladder tile at Mega Man's top or below his feet in the next frame. Because Mega Man's YSpeed is always negative while grounded, this also conveniently tells it if there's a ladder tile to climb down to below Mega Man's feet.