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
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).
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.
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).
The NMI routine:
Summary:
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:
- (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.
- (17 - 19) Reset the scroll to zero.
- (21, 22) Perform Sprite DMA transfer.
- (26) Load the pointer into the temporary pointer, VRAM_Pointer, based on the value of VRAM_Buffer_AddrCtrl.
- (29) Jump to the VRAM update routine ( UpdateScreen) using the data at the VRAM_Pointer.
- (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.
- (42 - 46) Turn the screen back on if enabled previously. Jump to SoundEngine, ReadJoypads, PauseRoutine, and UpdateTopScore.
- ( 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.
- (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.
- (91 - 110) Do sprite 0 hit and scroll split (status bar is split from the gameplay area.)
- (112, 113) Sprite zero hit is done, so set scroll.
- (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.
- Turn on NMI again and return to busy loop in reset.
Subscribe to:
Posts (Atom)