Thursday, December 27, 2012

Daily SMB HL Disassembly Post #5

Presented with little comment: This is how SMB decides if you have completed a castle correctly (World 4, 7 and 8).

01 ; loop command data 02 LoopCmdWorldNumber: ; world number is one less than world displayed 03 .byte $03, $03, $06, $06, $06, $06, $06, $06, $07, $07, $07 04 LoopCmdPageNumber: 05 .byte $05, $09, $04, $05, $06, $08, $09, $0a, $06, $0b, $10 06 LoopCmdYPosition: 07 .byte $40, $b0, $b0, $80, $40, $40, $80, $40, $f0, $f0, $f0 08 09 .code 10 11 .proc ExecGameLoopback 12 13 mb Player_PageLoc := Player_PageLoc - #4 ; send player back 4 pages 14 mb CurrentPageLoc := CurrentPageLoc - #4 ; send current page back 4 pages 15 mb ScreenLeft_PageLoc := ScreenLeft_PageLoc - #4 ; ..etc 16 mb ScreenRight_PageLoc := ScreenRight_PageLoc - #4 17 mb AreaObjectPageLoc := AreaObjectPageLoc - #4 18 19 lda #$00 ; initialize page select for both 20 sta EnemyObjectPageSel ; area and enemy objects 21 sta AreaObjectPageSel 22 sta EnemyDataOffset ; initialize enemy object data offset 23 sta EnemyObjectPageLoc ; and enemy object page control 24 25 mb AreaDataOffset := AreaDataOfsLoopback[ y ] ; adjust area object offset based on which loop command we encountered 26 27 rts 28 29 .endproc 30 31 32 .proc ProcLoopCommand 33 34 if LoopCommand && CurrentColumnPos == zero ; check if loop command was found 35 36 ldy #$0b ; start at the end of each set of loop data 37 38 repeat 39 40 if dey == negative goto ChkEnemyFrenzy ; if no matching loops, do next 41 42 ; check to see if one of the world numbers matches our current world number AND 43 ; if one of the page numbers matches the page we're currently on: 44 45 until WorldNumber = LoopCmdWorldNumber[ y ] && CurrentPageLoc = LoopCmdPageNumber[ y ] 46 47 ; fall through to check player postion: 48 ; if the player is at the correct position AND on solid ground (i.e. not jumping or falling) 49 if Player_Y_Position = LoopCmdYPosition[ y ] && Player_State = #0 50 51 ; are we in world 7? 52 if WorldNumber <> #World7 goto InitMultLoop 53 54 inc MultiLoopCorrectCntr ; increment counter for correct progression 55 56 IncLoopPass: 57 58 inc MultiLoopPassCntr ; increment master multi-part counter 59 if MultiLoopPassCntr <> #3 goto InitLCmd ; have we done all three parts? 60 if MultiLoopCorrectCntr = #3 goto InitMultLoop ; if so, have we done them all correctly? 61 62 else Z clear 63 if WorldNumber = #World7 goto IncLoopPass ; are we in world 7? 64 endif 65 66 67 jsr ExecGameLoopback ; if player is not in right place, loop back 68 jsr KillAllEnemies 69 70 InitMultLoop: 71 72 lda #$00 ; initialize counters used for multi-part loop commands 73 sta MultiLoopPassCntr 74 sta MultiLoopCorrectCntr 75 76 InitLCmd: 77 78 lda #$00 ; initialize loop command flag 79 sta LoopCommand 80 81 endif 82 83 .endproc

Friday, December 21, 2012

HL code example

Soon I'm going to release the macro code I've been working on. I don't understand this code algorithm in detail, (there is a lot of data to go with it that is not posted here) it is used as an example. I recently added the ability to scan for a known identifier and if found, default to "not zero" for the condition, ( BNE/BEQ ). Here is an example of what it can do:


001 .proc AreaParserCore 002 003 if BackloadingFlag 004 jsr ProcessAreaData 005 endif 006 007 ldx #$0c 008 lda #$00 009 010 repeat 011 mb MetatileBuffer[ x ] := a ; clear out metatile buffer 012 until dex == negative 013 014 if ldy BackgroundScenery ; do we need to render the background scenery? 015 016 lda CurrentPageLoc ; otherwise check for every third page 017 do 018 if cmp #$03 == negative break ; if less than three we're there 019 mb a := a - #3 ; if 3 or more, subtract 3 and 020 021 while positive ; unconditional 022 023 mb a := a << 4 ; move results to higher nybble 024 025 mb x := a ++ BSceneDataOffsets[ y - 1 ] ++ CurrentColumnPos ; add with carry 026 027 if lda BackSceneryData[ x ] ; load data from sum of offsets - if zero, no scenery 028 029 pha 030 031 mb temp_byte := a & #$0f - #1 ; clear h.nybble and subtract one (because low nybble is $01-$0c) 032 mb x := a * 2 ++ temp_byte ; multiply by three (shift to left and add old result) c is clear 033 034 pla ; get high nybble from stack, move low 035 036 mb y := a >> 4 ; use as second offset (used to determine height) 037 mb temp_byte := #3 ; use previously saved memory location for counter 038 039 repeat 040 mb MetatileBuffer[ y ] := BackSceneryMetatiles[ x ] ; load metatile data from offset of (lsb - 1) * 3 041 inx 042 iny 043 until y = #$0b || dec temp_byte == zero ; decrement until counter expires, or y = $0b 044 045 endif 046 endif 047 048 if ldx ForegroundScenery ; check for foreground data needed or not 049 050 mb y := FSceneDataOffsets[ x - 1 ] ; load offset from location offset by header value, then 051 ldx #$00 ; reinit X 052 repeat 053 if lda ForeSceneryData[ y ] != zero ; load data until counter expires 054 sta MetatileBuffer,x ; do not store if zero found 055 endif 056 iny 057 inx 058 until x = #$0d ; store up to end of metatile buffer 059 060 endif 061 062 063 if ldy AreaType == zero && WorldNumber = #World8 ; if set as water level and world number eight, 064 lda #$62 ; use castle wall metatile as terrain type 065 else 066 lda TerrainMetatiles,y ; otherwise get appropriate metatile for area type 067 if ldy CloudTypeOverride ; check for cloud type override , if not set, keep value 068 lda #$88 ; use cloud block terrain 069 endif 070 endif 071 072 mb temp_byte[ 7 ] := a ; store value here 073 ldx #$00 ; initialize X, use as metatile buffer offset 074 075 mb y := TerrainControl * 2 ; multiply by 2 and use as yet another offset 076 077 do 078 mb temp_byte := TerrainRenderBits[ y ] ; get one of the terrain rendering bit data 079 mb temp_byte[ 1 ] := y + 1 ; increment Y and use as offset next time around 080 081 if CloudTypeOverride && x <> #$00 ; skip if value here is zero, and check if we're doing 082 mb temp_byte := temp_byte & #%00001000 ; the ceiling byte, if not, mask out all but d3 083 endif 084 085 ldy #$00 ; start at beginning of bitmasks 086 repeat 087 088 if lda Bitmasks[ y ] : bit temp_byte ; if set, write terrain to buffer 089 mb MetatileBuffer[ x ] := temp_byte[ 7 ] ; load terrain type metatile number and store into buffer here 090 endif 091 092 inx ; continue until end of buffer 093 if x = #$0d goto exitloop ; if we're at the end, break out of this loop 094 095 if AreaType = #$02 && x = #$0b ; check area for undergd area, and if at the bottom of the screen 096 mb temp_byte[ 7 ] := #$54 ; override old terrain type with ground level terrain type 097 endif 098 099 iny ; increment bitmasks offset in Y 100 until y = #$08 ; if not all bits checked, loop back 101 102 ldy $01 103 104 while Z clear ; unconditional branch, use Y to load next byte 105 106 exitloop: 107 108 jsr ProcessAreaData ; do the area data loading routine now 109 lda BlockBufferColumnPos 110 jsr GetBlockBufferAddr ; get block buffer address from where we're at 111 ldx #$00 112 ldy #$00 ; init index regs and start at beginning of smaller buffer 113 114 repeat 115 sty temp_byte 116 117 mb a := MetatileBuffer[ x ] & #%11000000 << 1 118 rol ; make %xx000000 into 0000xx 119 rol 120 tay ; use as offset in Y 121 122 if MetatileBuffer[ x ] < BlockBuffLowBounds[ y ] ; reload original unmasked value here, 123 lda #$00 ; if less, init value before storing 124 endif 125 126 ldy temp_byte ; get offset for block buffer 127 mb ($06)[ y ] := a ; store value into block buffer 128 129 mb a := y 130 mb y := a + #$10 ; add 16 (move down one row) to offset 131 inx ; increment column value 132 until x >= #$0d ; continue until we pass last row, then leave 133 134 rts 135 136 .endproc

Tuesday, December 18, 2012

Daily SMB HL Disassembly Post #4

I'm not going to try and figure this out in too much detail. If someone else wants to delve into it a bit more, please do. It rotates the Offsets used for the Player, Enemy, Blocks, Bubbles and Fireballs.
As well it rotates the 9 offsets used by the Misc_SprDataOffset for use with various sprites like hammers and coins.

01 .proc SpriteShuffler 02 03 ldy AreaType ; load level type, likely residual code OP 04 mb temp_byte := # ( 10 * 4 ) ; sprite #10 05 ldx #$0e ; start at the end of OAM data offsets 06 07 repeat 08 09 if SprDataOffset[ x ] >= temp_byte ; the preset value if less, skip this part 10 11 ldy SprShuffleAmtOffset ; get current offset to preset value we want to add 12 mb a := a + SprShuffleAmt[ y ] ; get shuffle amount, add to current sprite offset 13 if carry set ; if exceeded $ff, do second add 14 mb a := a + temp_byte ; add preset value 40 to offset to skip first ten sprites again 15 endif 16 17 mb SprDataOffset[ x ] := a ; store new offset here or old one if 2nd add skipped 18 19 endif 20 21 until dex == negative ; move backwards to next one 22 23 mb x := SprShuffleAmtOffset + 1 24 if x = #3 25 ldx #$00 ; init to 0 -> cycles through 0 to 2 26 endif 27 28 stx SprShuffleAmtOffset 29 30 ldx #$08 ; load offsets for values and storage 31 ldy #$02 32 33 repeat ; y from 2 to 0, x = 8, 5, 2 34 35 mb a := SprDataOffset [ 5 + y ] ; 7, 6, 5 36 mb Misc_SprDataOffset [ x - 2 ] := a ; store first one unmodified, 6, 3, 0 37 mb Misc_SprDataOffset [ x - 1 ] := a + #$08 ; but add eight to the second, 7, 4, 1 38 mb Misc_SprDataOffset [ x ] := a + #$08 ; and eight more to the third one, 8, 5, 2 39 40 mb x := x - 3 41 42 until dey == negative 43 44 rts 45 46 .endproc

Saturday, December 15, 2012

Daily SMB HL Disassembly Post #3

PauseRoutine

This is next in ROM space.

Not much to say here. This thing checks for a fresh start button press (The joypad reading routine will ignore start unless it is released and pressed for the most part.) If pressed and appropriate it will pause the game and delay before checking again for pause so the pause sound has time to play. PauseSoundQueue will be set to #1 when pause is pressed and #2 when pause is pressed again to unpause. If PauseSoundQueue is #2 it signals to the sound engine to continue playing sound (music). The main block in the if structure that changes the pause status will also set bit7 of GamePauseStatus to flag that that block will not be run again, but this is not needed, since the start button must be released and pressed again to register as a read in the joy pad reading routine. Next frame, bit7 will be cleared.

Worth noting: the macro code else has the option of sending it a known flag state to create a local branch (always) instruction rather then the default jmp command.

01 .proc PauseRoutine 02 03 ; Are we in Victorymode OR are we in game mode AND running game engine? 04 lda OperMode 05 if ( a = #VictoryModeValue || ( a = #GameModeValue && OperMode_Task = #$03 )) 06 07 if lda GamePauseTimer != zero ; check if pause timer is still counting down 08 dec GamePauseTimer ; if so, decrement and leave 09 rts 10 endif 11 12 if SavedJoypad1Bits & #BUTTON_START == bitset ; bitset is a .define for 'Z clear' 13 14 ; bit7 set, means this code block can't run again (residual) 15 if GamePauseStatus & #%10000000 == bitset goto Exit 16 17 mb GamePauseTimer := #$2b ; timer value 18 19 lda GamePauseStatus ; will be 0 or 1 20 tay 21 mb PauseSoundQueue := y + 1 ; set pause sfx queue, 1 or 2 22 23 mb a := a ^ #%00000001 | #%10000000 ; invert d0 and set d7 24 ; .. bit0 is used by the rest of the game to check for pause 25 else zero clear ; tell the else to use bne as unconditional 26 lda GamePauseStatus ; clear bit7 so the code above will run again (residual) 27 and #%01111111 ; is not pressed 28 endif 29 30 sta GamePauseStatus 31 32 endif 33 34 Exit: 35 rts 36 .endproc

Friday, December 7, 2012

Daily SMB HL Disassembly Post #2

So daily is going to be maybe more like weekly or bi-weekly, not sure. But anyway:

Super Mario Bros. NMI is pretty straightforward, if I get some details incorrect, please feel free to comment. Throughout the code of Super Mario Bros, relevant data is often in ROM just before the corresponding code block, which I have been marking as data ( with a macro that can disable the segment command - disabled will allow matching against the original game.)

This data block is made of high/low bytes of pointers that point to various data buffers to be copied to the PPU VRAM. Often this will be a VRAM_Buffer of which there are two (I'm unsure as to why there are two, it seems it would work with one) but the VRAM_Buffer_AddrCtrl can also be set to trigger a palette update, or to one the text messages for the end of the castles.

The value VRAM_Buffer_AddrCtrl is the number of the buffer to send to the PPU from $0 to $12 (0 to 18).

01 VRAM_AddrTable_Low: 02 03 .lobytes VRAM_Buffer1, WaterPaletteData, GroundPaletteData 04 .lobytes UndergroundPaletteData, CastlePaletteData, VRAM_Buffer1_Offset 05 .lobytes VRAM_Buffer2, VRAM_Buffer2, BowserPaletteData 06 .lobytes DaySnowPaletteData, NightSnowPaletteData, MushroomPaletteData 07 08 ; Thanks messages: 09 .lobytes MarioThanksMessage, LuigiThanksMessage 10 11 ; World 1 - 7 message: 12 .lobytes MushroomRetainerSaved 13 14 ; World 8 messages: 15 .lobytes PrincessSaved1, PrincessSaved2, WorldSelectMessage1 16 .lobytes WorldSelectMessage2 17 18 VRAM_AddrTable_High: 19 20 .hibytes VRAM_Buffer1, WaterPaletteData, GroundPaletteData 21 .hibytes UndergroundPaletteData, CastlePaletteData, VRAM_Buffer1_Offset 22 .hibytes VRAM_Buffer2, VRAM_Buffer2, BowserPaletteData 23 .hibytes DaySnowPaletteData, NightSnowPaletteData, MushroomPaletteData 24 .hibytes MarioThanksMessage, LuigiThanksMessage, MushroomRetainerSaved 25 .hibytes PrincessSaved1, PrincessSaved2, WorldSelectMessage1 26 .hibytes WorldSelectMessage2 27 28 VRAM_Buffer_Offset: 29 .byte <VRAM_Buffer1_Offset, <VRAM_Buffer2_Offset

The NMI routine:

001 .proc NonMaskableInterrupt 002 003 VRAM_Pointer = temp_byte ; shared memory location $00 004 005 mb Mirror_PPU_CTRL := Mirror_PPU_CTRL & #%01111111 ; disable NMIs in mirror reg, save all other bits 006 mb PPU_CTRL := a & #%01111110 ; alter name table address to be $2800, ($2000), save other bits 007 mb a := Mirror_PPU_MASK & #%11100110 ; disable OAM and background display by default 008 009 if y := DisableScreenFlag == zero ; if not set: 010 mb a := Mirror_PPU_MASK | #%00011110 ; reenable bits and save them 011 endif 012 013 mb Mirror_PPU_MASK := a ; save bits for later but not in register at the moment 014 015 mb PPU_MASK := a & #%11100111 ; disable screen for now 016 017 ldx PPU_STATUS ; reset flip-flop and reset scroll registers to zero 018 lda #$00 019 jsr InitScroll 020 ; reg a still 0 021 sta PPU_SPR_ADDR ; reset spr-ram address register 022 mb SPR_DMA := #$02 ; perform spr-ram DMA access on $0200-$02ff 023 024 ldx VRAM_Buffer_AddrCtrl ; load control for pointer to buffer contents 025 026 mb VRAM_Pointer[ 0 ] := VRAM_AddrTable_Low[ x ] ; set indirect at temp_byte to pointer 027 mb VRAM_Pointer[ 1 ] := VRAM_AddrTable_High[ x ] 028 029 jsr UpdateScreen ; update screen with buffer contents 030 ldy #$00 031 032 if x := VRAM_Buffer_AddrCtrl = #$06 ; check for usage of VRAM_Buffer2 033 iny ; get offset based on usage 034 endif 035 036 mb x := VRAM_Buffer_Offset[ y ] 037 lda #$00 ; clear buffer header at last location 038 sta VRAM_Buffer1_Offset,x 039 sta VRAM_Buffer1,x 040 sta VRAM_Buffer_AddrCtrl ; reinit address control to VRAM_Buffer1 041 042 mb PPU_MASK := Mirror_PPU_MASK ; copy mirror of $2001 to register 043 jsr SoundEngine ; play sound 044 jsr ReadJoypads ; read joypads 045 jsr PauseRoutine ; handle pause 046 jsr UpdateTopScore 047 048 if GamePauseStatus >> 1 == carry clear ; check for pause status 049 050 ; if TimerControl is zero do timers, OR decrement it and do timers if now zero 051 052 if ( a := TimerControl == zero ) || ( dec TimerControl == zero) 053 054 mb x := #$14 ; load end offset for end of frame timers 055 056 ; decrement interval timer control, 057 ; if expired, interval timers will decrement 058 ; along with frame timers 059 060 if dec IntervalTimerControl == negative 061 mb IntervalTimerControl := #$14 062 mb x := #$23 063 endif 064 065 repeat ; check current timer 066 if a := Timers[ x ] == not zero ; if current timer still valid: 067 dec Timers,x ; decrement the current timer 068 endif ; move onto next timer - one less than zero.. 069 until dex == negative ; loop will go from $23 or $14 to $0 070 endif 071 inc FrameCounter ; increment frame counter 072 endif 073 074 ldx #$00 075 ldy #$07 076 077 mb temp_byte := PseudoRandomBitReg & #%00000010 ; get first memory location of LSFR bytes, mask out all but d1 078 ; perform exclusive-OR on d1 from first and second bytes 079 mb a := PseudoRandomBitReg[ 1 ] & #%00000010 ^ temp_byte 080 081 clc ; if neither or both are set, carry will be clear 082 if zero clear 083 sec ; if one or the other is set, carry will be set 084 endif 085 086 repeat 087 ror PseudoRandomBitReg,x ; rotate carry into d7, and rotate last bit into carry 088 inx ; increment to next byte 089 until y - 1 == zero 090 091 if a := Sprite0HitDetectFlag == not zero 092 093 repeat 094 mb a := PPU_STATUS & #%01000000 ; wait for sprite 0 flag to clear 095 until zero 096 097 if GamePauseStatus >> 1 == carry clear ; if not in pause, do sprite stuff 098 jsr MoveSpritesOffscreen 099 jsr SpriteShuffler 100 endif 101 102 do 103 mb a := PPU_STATUS & #%01000000 ; do sprite #0 hit detection 104 while zero 105 106 ldy #$14 ; small delay, to wait until we hit horizontal blank time 107 repeat 108 until dey == zero 109 110 endif 111 112 mb PPU_SCROLL := HorizontalScroll ; set scroll registers from variables 113 mb PPU_SCROLL := VerticalScroll 114 115 lda Mirror_PPU_CTRL ; load saved mirror of $2000 116 pha ; keep it safe in the stack 117 sta PPU_CTRL 118 119 if GamePauseStatus >> 1 == carry clear 120 jsr OperModeExecutionTree ; if not in pause mode do one of many, many possible subroutines 121 endif 122 123 lda PPU_STATUS ; reset flip-flop 124 125 pla 126 mb PPU_CTRL := a | #%10000000 ; reactivate NMIs 127 rti ; we are done until the next frame! 128 .endproc

Summary:

  1. (5 - 11) First, turn off NMI and rendering. Check if DisableScreenFlag is set. If not, in the mirrored register, set values for clipping and turn on sprites and background.

  2. (17 - 19) Reset the scroll to zero.

  3. (21, 22) Perform Sprite DMA transfer.

  4. (26) Load the pointer into the temporary pointer, VRAM_Pointer, based on the value of VRAM_Buffer_AddrCtrl.

  5. (29) Jump to the VRAM update routine ( UpdateScreen) using the data at the VRAM_Pointer.

  6. (32 - 40) There are two dynamic buffers. If the buffer in use is VRAM_Buffer2, VRAM_Buffer_AddrCtrl will be equal to $06, so increment register y.
    Then use the data at VRAM_Buffer_Offset index by y to find the offset to use to clear either the beginning of VRAM_Buffer1 or VRAM_Buffer2 and reset VRAM_Buffer_AddrCtrl to zero as well.

  7. (42 - 46) Turn the screen back on if enabled previously. Jump to SoundEngine, ReadJoypads, PauseRoutine, and UpdateTopScore.

  8. ( 48 - 72 ) Timers. This is a bit to explain but is is not over complex. After checking that we are not in pause, check if TimerControl is clear. If so continue into the timer section.

    This code checks for TimerControl at zero. If not it is decremented and checked again. If it is still not zero all timers are left alone. This essentially pauses most of the game action. This is used for things like Mario powering up (mushroom or fireflower), or shrinking. When an animation is complete it sets TimerControl back to zero to allow normal timer countdown.

    If TimerControl is zero (not set) then the timer code runs. First IntervalTimerControl is decremented. If it is at zero it is reset to $14 (20), and register x is set to $23 rather than the default $14. This means the loop following decrements all the timers. If IntervalTimerControl is not clear only the first $14 timers count down. This means the last group of timers only count down once every 20 frames.

    The FrameCounter is also incremented here if not in pause mode.

  9. (77 - 89) Do the LFSR algorithm (Linear Feedback Shift Register):

    Basically:

    c := (PseudoRandomBitReg[0] AND 2) ^ (PseudoRandomBitReg[1] AND 2)
    Rotate c into bit 7 of PseudoRandomBitReg[0] and rotate that into PseudoRandomBitReg[1]. This is essentially 16 bit, not sure why there are so many PseudoRandomBitReg slots.

  10. (91 - 110) Do sprite 0 hit and scroll split (status bar is split from the gameplay area.)

  11. (112, 113) Sprite zero hit is done, so set scroll.

  12. (115, 121) Do some stuff with PPU_CTRL and save a copy on the stack (paranoid?) and Jump to the game engine if we are not in pause.

  13. Turn on NMI again and return to busy loop in reset.