;;; ;; The same as in `level.s` but at the bottom of the screen we have a "This is a ;; message" being shown. This message is part of the background but it does not ;; scroll like the rest of the screen, but it stays at the same coordinates all ;; the time. This is done via the MMC3 chip, and the same technique is further ;; developed in `roulette.s`. In short, this mapper chip implements a bunch of ;; features, and one of them is the ability to instruct the chip to send an IRQ ;; on a given scanline. This is then done to mess with the scroll value and ;; obtain different effects. ;; ;; This example shows the most basic usage of it, and it's what games like Super ;; Mario Bros. 3 and Kirby's Adventure did: implement a status bar at the bottom ;; of the screen in a way that is reliable and less CPU consuming than sprite 0 ;; hit detection as it's done in `sprite0.s`. That is, instead of wasting CPU ;; cycles waiting for a sprite 0 hit, during VBlank we configure the chip to ;; send us an IRQ for a given scanline. Once the PPU arrives at this scanline, ;; then it sends us an IRQ in which we reset the scroll value. This scroll value ;; will then be configured again during VBlank. ;; ;; The code is a mix between `level.s` and `fx/blink.s`, so consider these two ;; examples as previous work and get to know them before jumping into this one. ;; In here I have just written comments which are specific to this example. ;; Just like `fx/blink.s`. .segment "HEADER" .byte 'N', 'E', 'S', $1A .byte $10, $10 .byte $42, $08 .res 8, 0 .segment "VECTORS" .addr nmi, reset, irq ;; Just like `fx/blink.s`: empty on purpose as this is a simple example. .segment "PRG0_00" .byte $FF .segment "PRG0_01" .byte $FF .segment "PRG0_02" .byte $FF .segment "PRG0_03" .byte $FF .segment "PRG0_04" .byte $FF .segment "PRG0_05" .byte $FF .segment "PRG0_06" .byte $FF .segment "PRG0_07" .byte $FF .segment "PRG0_08" .byte $FF .segment "PRG0_09" .byte $FF .segment "PRG0_0A" .byte $FF .segment "PRG0_0B" .byte $FF .segment "PRG0_0C" .byte $FF .segment "PRG0_0D" .byte $FF .segment "PRG0_0E" .byte $FF .segment "PRG1_00" .byte $FF .segment "PRG1_01" .byte $FF .segment "PRG1_02" .byte $FF .segment "PRG1_03" .byte $FF .segment "PRG1_04" .byte $FF .segment "PRG1_05" .byte $FF .segment "PRG1_06" .byte $FF .segment "PRG1_07" .byte $FF .segment "PRG1_08" .byte $FF .segment "PRG1_09" .byte $FF .segment "PRG1_0A" .byte $FF .segment "PRG1_0B" .byte $FF .segment "PRG1_0C" .byte $FF .segment "PRG1_0D" .byte $FF .segment "PRG1_0E" .byte $FF .segment "FIXED" .byte $FF ;; Everything happens on this segment. Check the `config/mmc3.cfg` for more ;; information on where it is placed in the end. .segment "TAIL" ;; Just like with `sprite0.s`, the engine in `include/` can be configured to a ;; degree. In this case we instruct it to never go over the `$0D` row as this ;; will be the one being used for showing the status bar. BACKGROUND_ROW_MAX = $0D .include "include/all.s" .include "../shared/mmc3.s" ;; Clear out the a rows of tiles from the high byte for the PPU address as given ;; in the `x` register, and the low byte as given on the `y` register. .proc clear_row_x_y bit PPU::STATUS stx PPU::ADDRESS sty PPU::ADDRESS lda #$00 ldx #$20 @loop: sta PPU::DATA dex bne @loop rts .endproc ;; Similar to `show_hud` in `sprite0.s`, we want to allocate this text in the ;; background where the engine will not touch it. .proc show_status ;; Clear out the space in which we want to allocate or status bar. ldx #$23 ldy #$40 jsr clear_row_x_y ldx #$23 ldy #$60 jsr clear_row_x_y ldx #$23 ldy #$80 jsr clear_row_x_y ldx #$27 ldy #$40 jsr clear_row_x_y ldx #$27 ldy #$60 jsr clear_row_x_y ldx #$27 ldy #$80 jsr clear_row_x_y ;; Just like with `sprite0.s`, the message has to be repeated over the ;; nametable on $2400 as the engine will flip the base nametable address ;; whenever the scroll wraps around. ;; This WRITE_PPU_DATA $2368, $23 WRITE_PPU_DATA $2369, $17 WRITE_PPU_DATA $236A, $18 WRITE_PPU_DATA $236B, $22 WRITE_PPU_DATA $2768, $23 WRITE_PPU_DATA $2769, $17 WRITE_PPU_DATA $276A, $18 WRITE_PPU_DATA $276B, $22 ;; is WRITE_PPU_DATA $236D, $18 WRITE_PPU_DATA $236E, $22 WRITE_PPU_DATA $276D, $18 WRITE_PPU_DATA $276E, $22 ;; a WRITE_PPU_DATA $2370, $10 WRITE_PPU_DATA $2770, $10 ;; message WRITE_PPU_DATA $2372, $1C WRITE_PPU_DATA $2373, $14 WRITE_PPU_DATA $2374, $22 WRITE_PPU_DATA $2375, $22 WRITE_PPU_DATA $2376, $10 WRITE_PPU_DATA $2377, $16 WRITE_PPU_DATA $2378, $14 WRITE_PPU_DATA $2772, $1C WRITE_PPU_DATA $2773, $14 WRITE_PPU_DATA $2774, $22 WRITE_PPU_DATA $2775, $22 WRITE_PPU_DATA $2776, $10 WRITE_PPU_DATA $2777, $16 WRITE_PPU_DATA $2778, $14 ;; NOTE: in stark contrast with `sprite0.s`, there's no need to waste a ;; sprite for this purpose. rts .endproc .proc main ;; Setup the MMC3 chip. Note that this is better suited in the `reset` ;; function, but the engine already provides one and I didn't want to start ;; messing with `.ifdef` and the likes. ;; ;; NOTE: this is a copy-paste from `fx/blink.s`, so refer to that example on ;; what any of the code below means. lda #$00 sta MMC3::MIRRORING sta MMC3::IRQ_DISABLE lda #$80 sta MMC3::RAM_PROTECT BANK_REGISTER_SET 0, 0 BANK_REGISTER_SET 1, 2 BANK_REGISTER_SET 2, 4 BANK_REGISTER_SET 3, 5 BANK_REGISTER_SET 4, 6 BANK_REGISTER_SET 5, 7 BANK_REGISTER_SET 6, 0 BANK_REGISTER_SET 7, 1 ;; MMC3 configured, now go on as usual. lda #$00 sta PPU::zp_mask sta PPU::MASK jsr Palettes::init jsr Metatile::init jsr Driver::init jsr Background::init jsr Player::init ;; Show the status bar down below. jsr show_status cli lda #%10001000 sta PPU::zp_control sta PPU::CONTROL @main_game_loop: READ_JOYPAD1 jsr Player::update jsr Driver::update lda #%10000000 ora Globals::zp_flags sta Globals::zp_flags @wait_for_render: bit Globals::zp_flags bmi @wait_for_render jmp @main_game_loop .endproc ;; The basics are laid out in `basics/sprite.s`. Read the comments below for ;; more info. .proc nmi bit Globals::zp_flags bpl @next pha txa pha tya pha ;; Just like in `fx/blink.s`, acknowledge any previous IRQ as a safety ;; measure. But anyways set the next IRQ to happen on scanline 210. ldx #$00 stx MMC3::IRQ_DISABLE lda #210 sta MMC3::IRQ_LATCH sta MMC3::IRQ_RELOAD sta MMC3::IRQ_ENABLE ;; From here on as in `level.s`. jsr Player::update_sprite OAM_WRITE_SPRITES FLUSH_PENDING_VRAM_BUFFER bit Globals::zp_flags bvc @after_ppu lda #%10111111 and Globals::zp_flags sta Globals::zp_flags bit PPU::STATUS lda PPU::zp_control sta PPU::CONTROL lda PPU::zp_mask sta PPU::MASK @after_ppu: ;; NOTE: scroll is updated always. This is in contrast with `level.s`, but ;; if we don't do that the scroll would be lost if the player stops moving ;; the scroll position. lda Background::zp_scroll sta PPU::SCROLL lda #$00 sta PPU::SCROLL ;; And as usual again. lda #%01111111 and Globals::zp_flags sta Globals::zp_flags pla tay pla tax pla @next: rti .endproc ;; This is a much simpler version of IRQ handling as you can see in examples ;; such as `roulette.s`. .proc irq ;; Save current context. pha txa pha tya pha ;; Disable IRQs and acknowledge the current one. ldx #$00 stx MMC3::IRQ_DISABLE ;; Reset the scroll so the status bar is kept in place. The scroll will be ;; kept like this until VBlank happens, when the `nmi` function will set the ;; scroll value to what's perceived by the player. bit PPU::STATUS lda #$00 sta $2005 sta $2005 ;; Restore previous context. pla tay pla tax pla rti .endproc .segment "CHARS" ;; Similar to `fx/blink.s` but here we really needed something for the ;; background :) .incbin "../assets/diskun-background.chr" .incbin "../assets/diskun0.chr" .incbin "../assets/diskun1.chr" ;; The 15 other 8KB portions are left empty. .res $2000, $00 .res $2000, $00 .res $2000, $00 .res $2000, $00 .res $2000, $00 .res $2000, $00 .res $2000, $00 .res $2000, $00 .res $2000, $00 .res $2000, $00 .res $2000, $00 .res $2000, $00 .res $2000, $00 .res $2000, $00 .res $2000, $00