SMB1 Hacking

Discuss technical or other issues relating to programming the Nintendo Entertainment System, Famicom, or compatible systems. See the NESdev wiki for more information.
Pokun
Posts: 3441
Joined: Tue May 28, 2013 5:49 am
Location: Hokkaido, Japan

Re: SMB1 Hacking

Post by Pokun »

I see, that might explain why I could never master turtle tipping.
SMB2J-2Q
Posts: 267
Joined: Thu Jul 27, 2017 5:13 pm

Re: SMB1 Hacking

Post by SMB2J-2Q »

Here's a chunk of code from SMB that bothers me:

Code: Select all

ChkGERtn: lda GameEngineSubroutine   ;get number of game engine routine running
          cmp #$07
          beq ExCSM                  ;if running player entrance routine or
          cmp #$08                   ;player control routine, go ahead and branch to leave
          bne ExCSM
          lda #$02
          sta GameEngineSubroutine   ;otherwise set sideways pipe entry routine to run
          rts                        ;and leave
What I am thinking here is this: if this code truly means to check for either game engine subroutines #$07 (for player entering) or #$08 (for player control), then perhaps they likely meant to make the first branch instruction a BCC (branch to do pipe entry routine if the current game engine subroutine is between #$00 and #$06), and if G.E. subroutine ID #$08 is running, then shouldn't that BNE be a BEQ instead if the comments imply we want the program to exit as long as either G.E. subroutine IDs #$07 or #$08 are running?

Code: Select all

ChkGERtn: lda GameEngineSubroutine   ;get number of game engine routine running
          cmp #$07
          bcc SetPERtn               ;if running routines #$06 or lower, branch to do pipe entry routine below
          cmp #$08                   ;otherwise, if doing routines #$07 or #$08, go ahead and branch to leave
          beq ExCSM
SetPERtn: lda #$02
          sta GameEngineSubroutine   ;otherwise set sideways pipe entry routine to run
          rts                        ;and leave
~Ben
User avatar
segaloco
Posts: 911
Joined: Fri Aug 25, 2023 11:56 am

Re: SMB1 Hacking

Post by segaloco »

The first chunk hits ExCSM basically any time except when the game mode is mode 8. The check against 7 is pointless, because if it is 7, the bne on 8 will branch anyway. The second does indeed do the same, but an easier way would be

Code: Select all

ChkGERtn:
	lda GameEngineSubroutine
	cmp #$08
	bne ExCSM
	lda #$02
	sta GameEngineSubroutine
	rts
This bit is at the tail end of the calculation routine for side checks, the check to see if the player should enter a pipe horizontally. In my disassembly it looks something like so:

Code: Select all

	lda	proc_id_player
	cmp	#player_procs::enter
	beq	:++
	cmp	#player_procs::ctrl
	bne	:++ ; if (game.proc_id == ctrl) {
		lda	#player_procs::pipe_h
		sta	proc_id_player
		rts
	; }
Where the lazy branch goes out past a subroutine that is called after this. So yeah, you only do a pipe enter if it is currently the player control cycle. Maybe originally it was supposed to support either? This could have something to do with the pipe entrance levels like the start of 1-2, but I'm not certain.

I hope since this is only a tiny snippet of code and not effectively sharing the whole thing, this is fine. This is the original code from Nintendo:

Code: Select all

CPB445	 EQU	  $
	 LDA	  <PLCMOD
	 CMP	  #PLSMMD
	 BEQ	  CPB480			 ;- IF	start move mode ?   ( YES ; CPB480 )
;
	 CMP	  #PLMDPY
	 BNE	  CPB480			 ;- IF	play mode ?	    ( NO ; CPB480 )
;
	 LDA	  #PLC1MD
	 STA	  <PLCMOD			       ; In L-chimney mode set
;
	 RTS
So indeed even in their commentary it is acknowledged what is happening here. I took a look at the original hoping to have spotted something different, that maybe the comments were mixed up and the branch was to be avoided for both PLCMOD and PLMDPY. But this seems to indicate the code is what was intended.
SMB2J-2Q
Posts: 267
Joined: Thu Jul 27, 2017 5:13 pm

Re: SMB1 Hacking

Post by SMB2J-2Q »

segaloco wrote: Mon Oct 27, 2025 10:25 pm The first chunk hits ExCSM basically any time except when the game mode is mode 8. The check against 7 is pointless, because if it is 7, the bne on 8 will branch anyway. The second does indeed do the same, but an easier way would be

Code: Select all

ChkGERtn:
	lda GameEngineSubroutine
	cmp #$08
	bne ExCSM
	lda #$02
	sta GameEngineSubroutine
	rts
This bit is at the tail end of the calculation routine for side checks, the check to see if the player should enter a pipe horizontally. In my disassembly it looks something like so:

Code: Select all

	lda	proc_id_player
	cmp	#player_procs::enter
	beq	:++
	cmp	#player_procs::ctrl
	bne	:++ ; if (game.proc_id == ctrl) {
		lda	#player_procs::pipe_h
		sta	proc_id_player
		rts
	; }
Where the lazy branch goes out past a subroutine that is called after this. So yeah, you only do a pipe enter if it is currently the player control cycle. Maybe originally it was supposed to support either? This could have something to do with the pipe entrance levels like the start of 1-2, but I'm not certain.

I hope since this is only a tiny snippet of code and not effectively sharing the whole thing, this is fine. This is the original code from Nintendo:

Code: Select all

CPB445	 EQU	  $
	 LDA	  <PLCMOD
	 CMP	  #PLSMMD
	 BEQ	  CPB480			 ;- IF	start move mode ?   ( YES ; CPB480 )
;
	 CMP	  #PLMDPY
	 BNE	  CPB480			 ;- IF	play mode ?	    ( NO ; CPB480 )
;
	 LDA	  #PLC1MD
	 STA	  <PLCMOD			       ; In L-chimney mode set
;
	 RTS
So indeed even in their commentary it is acknowledged what is happening here. I took a look at the original hoping to have spotted something different, that maybe the comments were mixed up and the branch was to be avoided for both PLCMOD and PLMDPY. But this seems to indicate the code is what was intended.
Matt,

Thank you!

~Ben
SMB2J-2Q
Posts: 267
Joined: Thu Jul 27, 2017 5:13 pm

Re: SMB1 Hacking

Post by SMB2J-2Q »

https://www.youtube.com/watch?v=UQkNNcJULNs

You may be familiar with this glitch. You grab a Starman and run as fast as you can to the ending flagpole. When the invincibility wears off, the ground level music plays for the rest of the ending sequence. This can be done in either World 1-1 or 6-2.

This was never fixed in VSSMB, SMB2J or ANNSMB, but it was fixed in SMAS (as well as in SMB Deluxe for the GBC); the official fix in SMAS went like this:

Code: Select all

CODE_05C925:
LDA $0F                 ; $05:C925: A5 0F
CMP #$04                ; $05:C927: C9 04
BEQ CODE_05C944         ; $05:C929: F0 19
CMP #$05                ; $05:C92B: C9 05
BEQ CODE_05C944         ; $05:C92D: F0 15
LDA $DB                 ; $05:C92F: A5 DB
CMP #$1B                ; $05:C931: C9 1B
BNE CODE_05C939         ; $05:C933: D0 04
LDA #$01                ; $05:C935: A9 01
BRA CODE_05C93C         ; $05:C937: 80 03
This 10-byte patch was included in the "GetAreaMusic" subroutine and it involved checking if the player was currently doing either the flagpole slide (ID $04) or end-of-level (ID $05) game engine subroutines and, if so, not to play any area music again during these times.

However, there are other ways to fix this glitch. In both SMB2J and ANNSMB (but not in SMAS -- why?), there is a special flag called "FlagpoleMusicFlag" (RAM $07F6) that is first initialized during the SecondaryGameSetup subroutine and then first checked during the level end subroutine (ID $05 in the list of game engine subroutines) and the specific check was that if the end-of-level music was already playing, to not play it again (if this flag was set to 1, BNE to ChkStop).

Some people have added a second check for $07F6 within the star invincibility subroutine (if FlagpoleMusicFlag = 1, then BNE to NoChgMus), but I also tried that within the GetAreaMusic subroutine (if FlagpoleMusicFlag = 1, then BNE to ExitGetM) and it also fixes the area music vs. end-of-level music bug perfectly.

Code: Select all

GetAreaMusic:
             lda OperMode           ;if in title screen mode, leave
             beq ExitGetM
             lda FlagpoleMusicFlag  ;PATCH check flag for end-of-level music playing
             bne ExitGetM           ;PATCH if so, leave
             lda AltEntranceControl ;check for specific alternate mode of entry


I also got the same result checking for the end-of-level music within the EventMusicBuffer flag during the star invincibility routine, and if this music is playing already, to BEQ to NoChgMus:

Code: Select all

              lda Player_Y_HighPos
              cmp #$02                   ;if player is below the screen, don't bother with the music
              bpl NoChgMus
              lda StarInvincibleTimer    ;if star mario invincibility timer at zero,
              beq ClrPlrPal              ;skip this part
              cmp #$04
              bne NoChgMus               ;if not yet at a certain point, continue
              lda IntervalTimerControl   ;if interval timer not yet expired,
              bne NoChgMus               ;branch ahead, don't bother with the music
              lda EventMusicBuffer       ;PATCH check event music buffer for end-of-level music
              cmp #EndOfLevelMusic       ;PATCH
              beq NoChgMus               ;PATCH if this music currently playing, don't bother with the music        
              jsr GetAreaMusic           ;to re-attain appropriate level music
NoChgMus:     ldy StarInvincibleTimer    ;get invincibility timer
~Ben
SMB2J-2Q
Posts: 267
Joined: Thu Jul 27, 2017 5:13 pm

Re: SMB1 Hacking

Post by SMB2J-2Q »

https://www.youtube.com/watch?v=WvojgRvNriw&t=11m55s

This video for why constantly stomping on Wigglers in Super Mario World cause many glitches to happen in your score, has led me to ask: I wonder if I can do something similar in the original SMB1?

Code: Select all

HandleStompedShellE:
       lda #$04                   ;set defeated state for enemy
       sta Enemy_State,x
       inc StompChainCounter      ;increment the stomp counter
       lda StompChainCounter      ;add whatever is in the stomp counter
       clc                        ;to whatever is in the stomp timer
       adc StompTimer
       jsr SetupFloateyNumber     ;award points accordingly
       inc StompTimer             ;increment stomp timer of some sort
       ldy PrimaryHardMode        ;check primary hard mode flag
That is, I am wondering I can add something like CMP #$08 which tells the game to stop incrementing the stomp counter if it is #$08 or more?

~Ben
User avatar
segaloco
Posts: 911
Joined: Fri Aug 25, 2023 11:56 am

Re: SMB1 Hacking

Post by segaloco »

A bounds check should be simple enough, if below, increment and act, if at or above, do nothing related to stomp counts. Reset the counter as usual when the player lands. That should be a sensible enough approach to fix the problem at its source. Then it doesn't matter if you can manage a perma-hop on a shell, it won't keep counting.
SMB2J-2Q
Posts: 267
Joined: Thu Jul 27, 2017 5:13 pm

Re: SMB1 Hacking

Post by SMB2J-2Q »

segaloco wrote: Thu Oct 30, 2025 6:26 pm A bounds check should be simple enough, if below, increment and act, if at or above, do nothing related to stomp counts. Reset the counter as usual when the player lands. That should be a sensible enough approach to fix the problem at its source. Then it doesn't matter if you can manage a perma-hop on a shell, it won't keep counting.
If you'd like to know the exact snippet of code for your convenience, so you can try to rework it to make it work for SMB1, here it is:

Code: Select all

IncreaseStompCounter:
      phy
      lda StompCounter
      clc
      adc ShellComboCounter,x
      inc StompCounter
      tay
      iny
      cpy #$08
      bcs NoIncStomp
      lda StompSFXData-1,y
      sta PlaySFX
NoIncStomp:
      tya
      cmp #$08
      bcc AwardStompPts
      lda #$08
AwardStompPts:
      jsl GivePoints
      ply
      rts
      
GivePoints:
      phx
      clc
      adc #$05
      jsl SpawnScoreSprite
      plx
      rtl
~Ben (SMB2J-2Q)
User avatar
segaloco
Posts: 911
Joined: Fri Aug 25, 2023 11:56 am

Re: SMB1 Hacking

Post by segaloco »

Yeah that'd have to be ported, it's full of 65816 opcodes, but could be a worthwhile test case in moving some code "back in time".
SMB2J-2Q
Posts: 267
Joined: Thu Jul 27, 2017 5:13 pm

Re: SMB1 Hacking

Post by SMB2J-2Q »

segaloco wrote: Thu Oct 30, 2025 8:34 pm Yeah that'd have to be ported, it's full of 65816 opcodes, but could be a worthwhile test case in moving some code "back in time".
Matt,

https://www.youtube.com/watch?v=AAjh72ytS44&t=7m30s

This video by Kosmic, about new ways to score 1-UPs, is another reason why I brought up why I wish to fix the stomp counter register. He briefly talks about why the glitched 1-UP tile shows between the 5000 and 8000 points registers, the other thing I want to fix.

~Ben (SMB2J-2Q)
SMB2J-2Q
Posts: 267
Joined: Thu Jul 27, 2017 5:13 pm

Re: SMB1 Hacking

Post by SMB2J-2Q »

Code: Select all

HandleStompedShellE:
       lda #$04                   ;set defeated state for enemy
       sta Enemy_State,x
       lda Player_State           ;BUGFIX -- check player's current state
       beq SkipStompCtrInc        ;BUGFIX -- if player on ground, branch to skip stomp counter increment
       inc StompChainCounter      ;increment the stomp counter
       lda StompChainCounter      ;add whatever is in the stomp counter
       clc                        ;to whatever is in the stomp timer
       adc StompTimer
       jsr SetupFloateyNumber     ;award points accordingly
       inc StompTimer             ;increment stomp timer of some sort
       ldy PrimaryHardMode        ;check primary hard mode flag
       lda RevivalRateData,y      ;load timer setting according to flag
       sta EnemyIntervalTimer,x   ;set as enemy timer to revive stomped enemy
       lda #$00                   ;BUGFIX -- clear A to avoid anomalies elsewhere in code if stomp counter was reset
SkipStompCtrInc:
       sta StompChainCounter      ;BUGFIX
SBnce: lda #$fc                   ;set player's vertical speed for bounce
       sta Player_Y_Speed         ;and then leave!!!
       rts
Here's where I started with the code modification. I took segaloco's suggestion of adding a check for the player's current state, and had it skip over and thus reset the stomp counter logic whenever the player landed on the ground.

UPDATE 1: However, here is an error that happens after I applied this: stomping on one Goomba the points register okay, but stomping on another won't; you'll just be bouncing on it but without being able to stomp it. (Picture here)
Screenshot from 2025-11-04 23-37-17.png
Hence, may I please ask: where else should I put the player state check?

~Ben
You do not have the required permissions to view the files attached to this post.
SMB2J-2Q
Posts: 267
Joined: Thu Jul 27, 2017 5:13 pm

Re: SMB1 Hacking

Post by SMB2J-2Q »

I just discovered something in both SMB2J and ANNSMB: in both these games, under the "SecondaryGameSetup" routine the "ISpr0Loop" subroutine is no longer present, and both the FDS titles also have a new NameTableSelect flag.

I added the .ifdef, .ifndef and .else labels to mark which games have this and which don't.

Code: Select all

SecondaryGameSetup:
             lda #$00
             sta DisableScreenFlag     ;enable screen output
.ifdef SMB2J
             sta WindFlag
.endif
.ifndef SMB1
             sta FlagpoleMusicFlag
.endif
             tay
ClearVRLoop: sta VRAM_Buffer1-1,y      ;clear buffer at $0300-$03ff
             iny
             bne ClearVRLoop
             sta GameTimerExpiredFlag  ;clear game timer exp flag
             sta DisableIntermediate   ;clear skip lives display flag
             sta BackloadingFlag       ;clear value here
             lda #$ff
             sta BalPlatformAlignment  ;initialize balance platform assignment flag
             lda ScreenLeft_PageLoc    ;get left side page location
.ifdef SMB1
             lsr Mirror_PPU_CTRL_REG1  ;shift LSB of ppu register #1 mirror out
             and #$01                  ;mask out all but LSB of page location
             ror                       ;rotate LSB of page location into carry then onto mirror
             rol Mirror_PPU_CTRL_REG1  ;this is to set the proper PPU name table
.else
             and #$01                  ;mask out all but LSB of page location
             sta NameTableSelect
.endif
             jsr GetAreaMusic          ;load proper music into queue
             lda #$38                  ;load sprite shuffle amounts to be used later
             sta SprShuffleAmt+2
             lda #$48
             sta SprShuffleAmt+1
             lda #$58
             sta SprShuffleAmt
             ldx #$0e                  ;load default OAM offsets into $06e4-$06f2
ShufAmtLoop: lda DefaultSprOffsets,x
             sta SprDataOffset,x
             dex                       ;do this until they're all set
             bpl ShufAmtLoop
.ifdef SMB1
             ldy #$03                  ;set up sprite #0
ISpr0Loop:   lda Sprite0Data,y
             sta Sprite_Data,y
             dey
             bpl ISpr0Loop
             jsr DoNothing2            ;these jsrs doesn't do anything useful
             jsr DoNothing1
             inc Sprite0HitDetectFlag  ;set sprite #0 check flag
.else
             jsr DoNothing             ;do slightly less of nothing than in super mario bros 1
             inc IRQUpdateFlag  
.endif

.ifdef ANN
             jmp IncModeTask
.else
             inc OperMode_Task         ;increment to next task
             rts
.endif
I am wondering if the ISpr0Loop routine is considered residual code?

~Ben (SMB2J-2Q)
User avatar
segaloco
Posts: 911
Joined: Fri Aug 25, 2023 11:56 am

Re: SMB1 Hacking

Post by segaloco »

Here's my variation on this, which does have the proper ifdefs and assembles back into each version correctly:

Code: Select all

	.export game_init_1_do
game_init_1_do:
	lda	#0
	sta	nmi_disp_disable
	
	.if	.defined(SMB2)
		sta	scenery_wind_flag
	.endif

	.if	.defined(SMBV2)
		sta	game_level_end_apu_on
	.endif

	tay
	: ; for (byte of ppu_displist) {
		sta	ppu_displist, y

		iny
		bne	:-
	; }

	sta	game_time_up
	sta	bg_msg_disable
	sta	course_page_loaded

	lda	#$FF
	sta	actor_plat_align
	lda	pos_x_hi_screen_left
	.if	.defined(SMBV1)
		lsr	ppu_ctlr0_b
	.endif
	and	#ppu_ctlr0::bg_parity
	.if	.defined(SMBV1)
		ror	a
		rol	ppu_ctlr0_b
	.elseif	.defined(SMBV2)
		sta	ppu_ctlr0_bg
	.endif

	jsr	game_init_area_music
	lda	#$38
	sta	sprite_strobe_amount+2
	lda	#$48
	sta	sprite_strobe_amount+1
	lda	#$58
	sta	sprite_strobe_amount

	ldx	#(game_init_data_0_end-game_init_data_0)-1
	: ; for (byte of game_init_data_0) {
		lda	game_init_data_0, x
		sta	obj_data_offset_player, x
		dex
		bpl	:-
	; }

	.if	.defined(SMBV1)
		ldy	#(game_init_oam_end-game_init_oam)-1
		: ; for (byte of game_init_oam) {
			lda	game_init_oam, y
			sta	oam_buffer+(OBJ_SIZE*0), y
			dey
			bpl	:-
		; }

		jsr	sub_92AA_rts
	.endif

	jsr	sub_92AA
	inc	nmi_sprite_overlap

	.if	.defined(SMB)|.defined(VS_SMB)|.defined(SMB2)
		inc	proc_id_game
		rts
	.elseif	.defined(ANN)
		jmp	bg_title_screen_finish
	.endif
So the bit that is yanked out of the V2 engine:

Code: Select all

		ldy	#(game_init_oam_end-game_init_oam)-1
		: ; for (byte of game_init_oam) {
			lda	game_init_oam, y
			sta	oam_buffer+(OBJ_SIZE*0), y
			dey
			bpl	:-
		; }

		jsr	sub_92AA_rts
All this does is put a single filler tile in the base OAM slot. Perhaps the plan was to use this for 0 sprite strikes that never then got used.

If it helps, this is the original source:

Code: Select all

;	  LDY	   #03H		   ; OBJ set used Scroll parts move !
;;
;GMI177	  EQU	   $
;	  LDA	   SCLOMDT,Y
;	  STA	   OAM,Y
;	  DEY
;	  BPL	   GMI177		;- IF  set end ?  ( NO ; GMI177 )
Which is commented out in the SMAS sources. The only real hint there is the comment about scroll parts, but I really am not sure what that means. The OAM entry is then simply called "Scroll oam data" in the comments. Not sure if this helps illuminate what this was supposed to do, but the fact that it's also commented out for SMAS implies that they decided it wasn't really important.
SMB2J-2Q
Posts: 267
Joined: Thu Jul 27, 2017 5:13 pm

Re: SMB1 Hacking

Post by SMB2J-2Q »

segaloco wrote: Thu Nov 06, 2025 8:10 pm Here's my variation on this, which does have the proper ifdefs and assembles back into each version correctly:

Code: Select all

	.export game_init_1_do
game_init_1_do:
	lda	#0
	sta	nmi_disp_disable
	
	.if	.defined(SMB2)
		sta	scenery_wind_flag
	.endif

	.if	.defined(SMBV2)
		sta	game_level_end_apu_on
	.endif

	tay
	: ; for (byte of ppu_displist) {
		sta	ppu_displist, y

		iny
		bne	:-
	; }

	sta	game_time_up
	sta	bg_msg_disable
	sta	course_page_loaded

	lda	#$FF
	sta	actor_plat_align
	lda	pos_x_hi_screen_left
	.if	.defined(SMBV1)
		lsr	ppu_ctlr0_b
	.endif
	and	#ppu_ctlr0::bg_parity
	.if	.defined(SMBV1)
		ror	a
		rol	ppu_ctlr0_b
	.elseif	.defined(SMBV2)
		sta	ppu_ctlr0_bg
	.endif

	jsr	game_init_area_music
	lda	#$38
	sta	sprite_strobe_amount+2
	lda	#$48
	sta	sprite_strobe_amount+1
	lda	#$58
	sta	sprite_strobe_amount

	ldx	#(game_init_data_0_end-game_init_data_0)-1
	: ; for (byte of game_init_data_0) {
		lda	game_init_data_0, x
		sta	obj_data_offset_player, x
		dex
		bpl	:-
	; }

	.if	.defined(SMBV1)
		ldy	#(game_init_oam_end-game_init_oam)-1
		: ; for (byte of game_init_oam) {
			lda	game_init_oam, y
			sta	oam_buffer+(OBJ_SIZE*0), y
			dey
			bpl	:-
		; }

		jsr	sub_92AA_rts
	.endif

	jsr	sub_92AA
	inc	nmi_sprite_overlap

	.if	.defined(SMB)|.defined(VS_SMB)|.defined(SMB2)
		inc	proc_id_game
		rts
	.elseif	.defined(ANN)
		jmp	bg_title_screen_finish
	.endif
So the bit that is yanked out of the V2 engine:

Code: Select all

		ldy	#(game_init_oam_end-game_init_oam)-1
		: ; for (byte of game_init_oam) {
			lda	game_init_oam, y
			sta	oam_buffer+(OBJ_SIZE*0), y
			dey
			bpl	:-
		; }

		jsr	sub_92AA_rts
All this does is put a single filler tile in the base OAM slot. Perhaps the plan was to use this for 0 sprite strikes that never then got used.

If it helps, this is the original source:

Code: Select all

;	  LDY	   #03H		   ; OBJ set used Scroll parts move !
;;
;GMI177	  EQU	   $
;	  LDA	   SCLOMDT,Y
;	  STA	   OAM,Y
;	  DEY
;	  BPL	   GMI177		;- IF  set end ?  ( NO ; GMI177 )
Which is commented out in the SMAS sources. The only real hint there is the comment about scroll parts, but I really am not sure what that means. The OAM entry is then simply called "Scroll oam data" in the comments. Not sure if this helps illuminate what this was supposed to do, but the fact that it's also commented out for SMAS implies that they decided it wasn't really important.
I just checked that one, and as you said, SMAS doesn't have the ISpr0Loop thing, either.

Strange too is that in SMAS, for both SMB1 and SMB2J the JSR to GetAreaMusic is also commented out.

~Ben