diff --git a/patches/ips/fast_reload.ips b/patches/ips/fast_reload.ips index 8668e1d63f..c97e345334 100644 Binary files a/patches/ips/fast_reload.ips and b/patches/ips/fast_reload.ips differ diff --git a/patches/ips/new_game.ips b/patches/ips/new_game.ips index b113735940..3eec2c0fb2 100644 Binary files a/patches/ips/new_game.ips and b/patches/ips/new_game.ips differ diff --git a/patches/ips/saveload.ips b/patches/ips/saveload.ips index 6ea35c3676..2fcb2b6874 100644 Binary files a/patches/ips/saveload.ips and b/patches/ips/saveload.ips differ diff --git a/patches/ips/savestate.ips b/patches/ips/savestate.ips new file mode 100644 index 0000000000..2e1dfb9b76 Binary files /dev/null and b/patches/ips/savestate.ips differ diff --git a/patches/rom_map/Bank 85.txt b/patches/rom_map/Bank 85.txt index dd231edd6f..e7bf5015a0 100644 --- a/patches/rom_map/Bank 85.txt +++ b/patches/rom_map/Bank 85.txt @@ -21,3 +21,4 @@ AD00 - AD40: ; crash_handle_springball.asm AD43 - AEDB: ; reserve_backward_fill.asm AEE0 - B599: ; crash_handle_base.asm B600 - B93E: ; map_area.asm +C000 - C450: ; savestate.asm diff --git a/patches/rom_map/SRAM.txt b/patches/rom_map/SRAM.txt index a1bd317e83..1435a6f2a3 100644 --- a/patches/rom_map/SRAM.txt +++ b/patches/rom_map/SRAM.txt @@ -32,4 +32,6 @@ $704400-704800: (reserved in case we expand the temporary tileset graphics) $703000-707400: backup of RAM $7E5000-$7E5400 and $7E7000-$7EB000, used only during unpause (decompression.asm) $707400-707800: room map tile graphics (map_area.asm) $707800-707F00: room name map tiles, used only during pause menu (map_area.asm/pause_menu_objectives.asm) -$707F00-708A00: [FREE] +$707F00-708000: savestate.asm +$708A00-710000: [FREE] +$710000-771200: savestate.asm diff --git a/patches/src/constants.asm b/patches/src/constants.asm index 44fe4eb26d..6b57457968 100644 --- a/patches/src/constants.asm +++ b/patches/src/constants.asm @@ -64,3 +64,5 @@ !kill_samus = $80D339 ; defined in crash_handle_base.asm !bug_dialog = $80D334 ; defined in crash_handle_base.asm + +!savestate_counts = $707F08 diff --git a/patches/src/fast_reload.asm b/patches/src/fast_reload.asm index cf6305e769..afaf372b77 100644 --- a/patches/src/fast_reload.asm +++ b/patches/src/fast_reload.asm @@ -1,7 +1,7 @@ ; Fast reload on death ; Based on patch by total: https://metroidconstruction.com/resource.php?id=421 ; Compile with "asar" (https://github.com/RPGHacker/asar/releases) - +arch 65816 !deathhook82 = $82DDC7 ;$82 used for death hook (game state $19) @@ -13,6 +13,8 @@ !bank_85_free_space_start = $859880 !bank_85_free_space_end = $859980 +!savestate_button_combo = $82FE78 ; This should be inside free space, and also consistent with reference in customize.rs +!loadstate_button_combo = $82FE7A ; This should be inside free space, and also consistent with reference in customize.rs !spin_lock_button_combo = $82FE7C ; This should be inside free space, and also consistent with reference in customize.rs !reload_button_combo = $82FE7E ; This should be inside free space, and also consistent with reference in customize.rs !freespacea0 = $a0fe00 ;$A0 used for instant save reload @@ -62,25 +64,25 @@ org $85811E org !bank_85_free_space_start SupportedStates: - dw #$0007 ; Main gameplay fading in - dw #$0008 ; Main gameplay - dw #$0009 ; Hit a door block - dw #$000a ; Loading next room - dw #$000b ; Loading next room - dw #$000c ; Pausing, normal gameplay but darkening - dw #$000d ; Pausing, loading pause menu - dw #$000e ; Paused, loading pause menu - dw #$000f ; Paused, objective/map/equipment screens - dw #$0012 ; Unpausing, normal gameplay but brightening - dw #$0013 ; Death sequence, start - dw #$0014 ; Death sequence, black out surroundings - dw #$0015 ; Death sequence, wait for music - dw #$0016 ; Death sequence, pre-flashing - dw #$0017 ; Death sequence, flashing - dw #$0018 ; Death sequence, explosion white out - dw #$001b ; Reserve tanks auto. - dw #$0027 ; Ending and credits. Cinematic. (reboot only) - dw #$ffff + dw $0007 ; Main gameplay fading in + dw $0008 ; Main gameplay + dw $0009 ; Hit a door block + dw $000a ; Loading next room + dw $000b ; Loading next room + dw $000c ; Pausing, normal gameplay but darkening + dw $000d ; Pausing, loading pause menu + dw $000e ; Paused, loading pause menu + dw $000f ; Paused, objective/map/equipment screens + dw $0012 ; Unpausing, normal gameplay but brightening + dw $0013 ; Death sequence, start + dw $0014 ; Death sequence, black out surroundings + dw $0015 ; Death sequence, wait for music + dw $0016 ; Death sequence, pre-flashing + dw $0017 ; Death sequence, flashing + dw $0018 ; Death sequence, explosion white out + dw $001b ; Reserve tanks auto. + dw $0027 ; Ending and credits. Cinematic. (reboot only) + dw $ffff hook_main: jsl $808338 ; run hi-jacked instruction @@ -140,6 +142,23 @@ hook_main: and !reload_button_combo ; L + R + Select + Start bne .reset ; Reset only if at least one of the inputs is newly pressed .noreset + lda $8B + cmp !savestate_button_combo + bne .nosavestate + lda $8F ; Newly pressed controller 1 input + and !savestate_button_combo + beq .nosavestate ; Reset only if at least one of the inputs is newly pressed + jsl $85c000 ; save state + bra .btn_leave +.nosavestate + lda $8B + cmp !loadstate_button_combo + bne .btn_leave + lda $8F ; Newly pressed controller 1 input + and !loadstate_button_combo + beq .btn_leave ; Reset only if at least one of the inputs is newly pressed + jsl $85c003 ; load state +.btn_leave plp rtl .reset: diff --git a/patches/src/map_area.asm b/patches/src/map_area.asm index 47005f5667..5e1ec68486 100644 --- a/patches/src/map_area.asm +++ b/patches/src/map_area.asm @@ -420,7 +420,7 @@ load_bg3_map_tilemap_wrapper: assert pc() <= $85A290 org $85A290 -; must match the reference in fix_kraid_hud.asm +; must match the reference in fix_kraid_hud.asm, savestate.asm load_bg3_map_tiles_wrapper: jsr load_bg3_map_tiles rtl diff --git a/patches/src/new_game.asm b/patches/src/new_game.asm index e191c84daa..a4f51be9a7 100644 --- a/patches/src/new_game.asm +++ b/patches/src/new_game.asm @@ -188,6 +188,15 @@ startup: sta $702700, X txa bne .copy_revealed + + ; Initialize SRAM savestate counters + lda #$0000 + tax +.init_ss_lp + sta !savestate_counts, X + inx : inx + cpx #$0008 + bne .init_ss_lp .skip_init: @@ -253,4 +262,4 @@ clear_timers: sta $0de2 ; rtl -warnpc !bank_a1_free_space_end \ No newline at end of file +assert pc() <= !bank_a1_free_space_end \ No newline at end of file diff --git a/patches/src/saveload.asm b/patches/src/saveload.asm index 77fa7f8dc8..0acef9579c 100644 --- a/patches/src/saveload.asm +++ b/patches/src/saveload.asm @@ -104,6 +104,10 @@ SaveSeed: LDA !seed_value_1 : STA $700008, X SaveAreaMapCoord: JSL set_save_markers +SaveClearSaveStateCurrCounts: + LDA #$0000 + STA !savestate_counts+4 + STA !savestate_counts+6 EndSaveGame: PLY : PLX : PLB : PLP : RTL LoadGame: PHP : REP #$30 : PHB : PHX : PHY diff --git a/patches/src/savestate.asm b/patches/src/savestate.asm new file mode 100644 index 0000000000..50812d4bce --- /dev/null +++ b/patches/src/savestate.asm @@ -0,0 +1,510 @@ +; SD2SNES Savestate code +; by acmlm, total, Myria +; +; adapted by Stag Shot +arch 65816 +lorom + +incsrc "constants.asm" + +org $80ffd8 + db 8 ; 256KB SRAM (ensure patch applied after map_progress_maintain) + +!bank_85_free_space_start = $85c000 +!bank_85_free_space_end = $85c450 + +!REG_4200_NMI = $84 +!IH_CONTROLLER_PRI = $8B +!IH_CONTROLLER_PRI_NEW = $8F +!IH_CONTROLLER_PRI_PREV = $97 +!IH_CONTROLLER_SEC = $8D +!IH_CONTROLLER_SEC_NEW = $91 +!IH_CONTROLLER_SEC_PREV = $99 + +!MUSIC_QUEUE_ENTRIES = $0619 +!MUSIC_QUEUE_TIMERS = $0629 +!MUSIC_QUEUE_NEXT = $0639 +!MUSIC_QUEUE_START = $063B +!MUSIC_ENTRY = $063D +!MUSIC_TIMER = $063F +!MUSIC_DATA = $07F3 +!MUSIC_TRACK = $07F5 +!SOUND_TIMER = $0686 +!DISABLE_SOUNDS = $05F5 +!SAMUS_HEALTH_WARNING = $0A6A + +!SRAM_SAVED_SP = $707F00 +!SRAM_MUSIC_DATA = $707F02 +!SRAM_MUSIC_TRACK = $707F04 +!SRAM_SOUND_TIMER = $707F06 +!SRAM_SAVESTATE_TOTAL_SAVES = !savestate_counts +!SRAM_SAVESTATE_TOTAL_LOADS = !SRAM_SAVESTATE_TOTAL_SAVES+2 +; Reset after a save at station/ship +!SRAM_SAVESTATE_CURR_SAVES = !SRAM_SAVESTATE_TOTAL_SAVES+4 +!SRAM_SAVESTATE_CURR_LOADS = !SRAM_SAVESTATE_TOTAL_SAVES+6 +!SRAM_DMA_BANK = $707F80 + +!MUSIC_ROUTINE = $808FC1 + +macro wram_to_sram(wram_addr, size, sram_addr) + dw $0000|$4312, &$FFFF ; VRAM address >> 1. + dw $0000|$4314, ((>>16)&$FF)|((&$FF)<<8) ; A addr = $70xxxx, size = $xx00 + dw $0000|$4316, (>>8)&$FF ; size = $80xx ($8000), unused bank reg = $00. + dw $0000|$2181, &$FFFF ; WRAM addr = $xx0000 + dw $1000|$2183, ((>>16)&$FF)-$7E ; WRAM addr = $7Exxxx (bank is relative to $7E) + dw $1000|$420B, $02 ; Trigger DMA on channel 1 +endmacro + +macro vram_to_sram(vram_addr, size, sram_addr) + dw $0000|$2116, >>1 ; VRAM address >> 1. + dw $9000|$213A, $0000 ; VRAM dummy read. + dw $0000|$4312, &$FFFF ; A addr = $xx0000 + dw $0000|$4314, ((>>16)&$FF)|((&$FF)<<8) ; A addr = $70xxxx, size = $xx00 + dw $0000|$4316, (>>8)&$FF ; size = $80xx ($0000), unused bank reg = $00. + dw $1000|$420B, $02 ; Trigger DMA on channel 1 +endmacro + +macro sram_to_wram(wram_addr, size, sram_addr) + dw $0000|$4312, &$FFFF ; A addr = $xx0000 + dw $0000|$4314, ((>>16)&$FF)|((&$FF)<<8) ; A addr = $70xxxx, size = $xx00 + dw $0000|$4316, (>>8)&$FF ; size = $80xx ($8000), unused bank reg = $00. + dw $0000|$2181, &$FFFF ; WRAM addr = $xx0000 + dw $1000|$2183, ((>>16)&$FF)-$7E ; WRAM addr = $7Exxxx (bank is relative to $7E) + dw $1000|$420B, $02 ; Trigger DMA on channel 1 +endmacro + +macro sram_to_vram(vram_addr, size, sram_addr) + dw $0000|$2116, >>1 ; VRAM address >> 1. + dw $0000|$4312, &$FFFF ; A addr = $xx0000 + dw $0000|$4314, ((>>16)&$FF)|((&$FF)<<8) ; A addr = $70xxxx, size = $xx00 + dw $0000|$4316, (>>8)&$FF ; size = $80xx ($0000), unused bank reg = $00. + dw $1000|$420B, $02 ; Trigger DMA on channel 1 +endmacro + +macro sram_to_sram(sram_src, size, sram_dest) + LDX # +- + LDA -2,X + STA -2,X + DEX : DEX + BNE - +endmacro + +macro a8() ; A = 8-bit + SEP #$20 +endmacro + +macro a16() ; A = 16-bit + REP #$20 +endmacro + +macro i8() ; X/Y = 8-bit + SEP #$10 +endmacro + +macro i16() ; X/Y = 16-bit + REP #$10 +endmacro + +macro ai8() ; A + X/Y = 8-bit + SEP #$30 +endmacro + +macro ai16() ; A + X/Y = 16-bit + REP #$30 +endmacro + +org !bank_85_free_space_start +; ***************************************** +; These jumps must remain at this address, as the fast_reload.asm controller hook hard references them. + jmp save_state + jmp load_state +; These settings must remain at this address for patch.rs +savestate_total_saves_max: skip 2 +savestate_total_loads_max: skip 2 +savestate_curr_saves_max: skip 2 +savestate_curr_loads_max: skip 2 +; ***************************************** + +; These can be modified to do game-specific things before and after saving and loading +; Both A and X/Y are 16-bit here +pre_load_state: +{ + LDA !MUSIC_DATA : STA !SRAM_MUSIC_DATA + LDA !MUSIC_TRACK : STA !SRAM_MUSIC_TRACK + LDA !SOUND_TIMER : STA !SRAM_SOUND_TIMER + RTS +} + +post_load_state: +{ + JSR post_load_music + + ; If sounds are not enabled, the game won't clear the sounds + LDA !DISABLE_SOUNDS : PHA + STZ !DISABLE_SOUNDS + JSL $82BE17 ; Cancel sound effects + PLA : STA !DISABLE_SOUNDS + + ; Reload BG3 (minimap) tiles, except in credits + LDA $0998 + CMP #$0027 + BEQ .skip_bg3 + JSL $85A290 +.skip_bg3 + RTS +} + +post_load_music: +{ + LDY !MUSIC_TRACK + LDA !MUSIC_QUEUE_NEXT : CMP !MUSIC_QUEUE_START : BEQ .music_queue_empty + + DEC #2 : AND #$000E : TAX + LDA !MUSIC_QUEUE_ENTRIES,X : BMI .queued_music_data + TXA : TAY : CMP !MUSIC_QUEUE_START : BEQ .no_music_data + + .music_queue_data_search + DEC #2 : AND #$000E : TAX + LDA !MUSIC_QUEUE_ENTRIES,X : BMI .queued_music_data + TXA : CMP !MUSIC_QUEUE_START : BNE .music_queue_data_search + + .no_music_data +; LDA !sram_music_toggle : CMP #$0002 : BPL .fast_off_preset_off + + ; No data found in queue, check if we need to insert it + LDA !SRAM_MUSIC_DATA : CMP !MUSIC_DATA : BEQ .music_queue_increase_timer + + ; Insert queued music data + DEX #2 : TXA : AND #$000E : TAX + LDA #$FF00 : CLC : ADC !MUSIC_DATA : STA !MUSIC_QUEUE_ENTRIES,X + LDA #$0008 : STA !MUSIC_QUEUE_TIMERS,X + + .queued_music_data +; LDA !sram_music_toggle : CMP #$0002 : BMI .queued_music_data_clear_track + + ; There is music data in the queue, assume it was loaded + LDA !MUSIC_QUEUE_ENTRIES,X : STA !MUSIC_DATA + BRA .fast_off_preset_off + + .music_queue_empty +; LDA !sram_music_toggle : CMP #$0002 : BPL .fast_off_preset_off + LDA !SRAM_MUSIC_DATA : CMP !MUSIC_DATA : BNE .clear_track_load_data + JMP .check_track + + .clear_track_load_data + TDC : JSL !MUSIC_ROUTINE + LDA #$FF00 : CLC : ADC !MUSIC_DATA : JSL !MUSIC_ROUTINE + BRA .load_track + + .fast_off_preset_off + ; Treat music as already loaded + STZ !MUSIC_QUEUE_TIMERS : STZ !MUSIC_QUEUE_TIMERS+$2 + STZ !MUSIC_QUEUE_TIMERS+$4 : STZ !MUSIC_QUEUE_TIMERS+$6 + STZ !MUSIC_QUEUE_TIMERS+$8 : STZ !MUSIC_QUEUE_TIMERS+$A + STZ !MUSIC_QUEUE_TIMERS+$C : STZ !MUSIC_QUEUE_TIMERS+$E + STZ !MUSIC_QUEUE_NEXT : STZ !MUSIC_QUEUE_START + STZ !MUSIC_ENTRY : STZ !MUSIC_TIMER + STZ !SOUND_TIMER : STY !MUSIC_TRACK + BRA .done + + .music_queue_increase_timer + ; Data is correct, but we may need to increase our sound timer + LDA !SRAM_SOUND_TIMER : CMP !MUSIC_TIMER : BMI .done + STA !MUSIC_TIMER : STA !SOUND_TIMER + BRA .done + + .queued_music_data_clear_track + ; Insert clear track before queued music data and start queue there + DEX #2 : TXA : AND #$000E : STA !MUSIC_QUEUE_START : TAX + STZ !MUSIC_QUEUE_ENTRIES,X : STZ !MUSIC_ENTRY + + ; Clear all timers before this point + .music_clear_timer_loop + TXA : DEC #2 : AND #$000E : TAX + STZ !MUSIC_QUEUE_TIMERS,X : CPX !MUSIC_QUEUE_NEXT : BNE .music_clear_timer_loop + + ; Set timer on the clear track command + LDX !MUSIC_QUEUE_START + + .queued_music_prepare_set_timer + LDA !SRAM_SOUND_TIMER : BNE .queued_music_set_timer + INC + + .queued_music_set_timer + STA !MUSIC_QUEUE_TIMERS,X : STA !SOUND_TIMER : STA !MUSIC_TIMER + BRA .done + + .check_track + LDA !SRAM_MUSIC_TRACK : CMP !MUSIC_TRACK : BEQ .done + + .load_track + LDA !MUSIC_TRACK : JSL !MUSIC_ROUTINE + + .done + RTS +} + +; These restored registers are game-specific and needs to be updated for different games +register_restore_return: +{ + %a8() + ; restore status register + LDA !REG_4200_NMI : STA $4200 + %a16() + ; run NMI once to avoid corrupted palette/flashing + JSL $808338 + RTL +} + +save_state: +{ + LDA.l savestate_total_saves_max + BEQ .skip_total + CMP !SRAM_SAVESTATE_TOTAL_SAVES + BEQ .no_saves +.skip_total + LDA.l savestate_curr_saves_max + BEQ .saves_left + CMP !SRAM_SAVESTATE_CURR_SAVES + BNE .saves_left +.no_saves + ; Clear inputs + TDC : STA !IH_CONTROLLER_PRI : STA !IH_CONTROLLER_PRI_NEW + RTL + +.saves_left + %ai8() + PHB + TDC : PHA : PLB + + TAX : TXY + .save_dma_regs + ; Store DMA registers to SRAM + LDA $4300,X : STA !SRAM_DMA_BANK,X + INX + INY : CPY #$0B : BNE .save_dma_regs + CPX #$7B : BEQ .done + TXA : CLC : ADC #$05 : TAX + LDY #$00 + BRA .save_dma_regs + + .done + ; inc counters + LDA !SRAM_SAVESTATE_TOTAL_SAVES : INC : STA !SRAM_SAVESTATE_TOTAL_SAVES + LDA !SRAM_SAVESTATE_CURR_SAVES : INC : STA !SRAM_SAVESTATE_CURR_SAVES + + %ai16() + LDX #save_write_table + ; fallthrough to run_vm +} + +run_vm: +{ + PHK : PLB + JMP vm +} + +save_write_table: + ; Turn PPU off + dw $1000|$2100, $80 + dw $1000|$4200, $00 + ; Single address, B bus -> A bus. B address = reflector to WRAM ($2180). + dw $0000|$4310, $8080 ; direction = B->A, byte reg, B addr = $2180 + + ; Copy WRAM segments, uses $710000-$747FFF + %wram_to_sram($7E0000, $8000, $710000) + %wram_to_sram($7E8000, $8000, $720000) + %wram_to_sram($7F0000, $8000, $730000) + %wram_to_sram($7F8000, $8000, $740000) + + ; Address pair, B bus -> A bus. B address = VRAM read ($2139). + dw $0000|$4310, $3981 ; direction = B->A, word reg, B addr = $2139 + dw $1000|$2115, $0080 ; VRAM address increment mode. + + ; Copy VRAM segments, uses $750000-$767FFF + %vram_to_sram($0000, $8000, $750000) + %vram_to_sram($8000, $8000, $760000) + + ; Copy CGRAM, uses SRAM $771000-$7711FF + dw $1000|$2121, $00 ; CGRAM address + dw $0000|$4310, $3B80 ; direction = B->A, byte reg, B addr = $213B + dw $0000|$4312, $1000 ; A addr = $xx0000 + dw $0000|$4314, $0077 ; A addr = $77xxxx, size = $xx00 + dw $0000|$4316, $0002 ; size = $02xx ($0200), unused bank reg = $00. + dw $1000|$420B, $02 ; Trigger DMA on channel 1 + + ; Done + dw $0000, save_return + +save_return: +{ + PEA $0000 : PLB : PLB + + %ai16() + ; Clear inputs + TDC : STA !IH_CONTROLLER_PRI : STA !IH_CONTROLLER_PRI_NEW + + ; Save fix_transition_bad_tiles SRAM to prevent possible door transition corruption + %sram_to_sram($704000, $400, $770200) + + ; Save temporary tilemap segment, uses $770000-$771000 + %sram_to_sram($703000, $1000, $770000) + + PLB + TSC : STA !SRAM_SAVED_SP + JMP register_restore_return +} + +load_clear_inputs: + ; Clear inputs and prevent repeated loads + TDC : STA !IH_CONTROLLER_PRI : STA !IH_CONTROLLER_PRI_NEW + LDA $82FE7A : STA !IH_CONTROLLER_PRI_PREV + RTS + +load_state: +{ + LDA !SRAM_SAVESTATE_CURR_SAVES + BEQ .no_loads + +.save_exists + LDA.l savestate_total_loads_max + BEQ .skip_total + CMP !SRAM_SAVESTATE_TOTAL_LOADS + BEQ .no_loads +.skip_total + LDA.l savestate_curr_loads_max + BEQ .loads_left + CMP !SRAM_SAVESTATE_CURR_LOADS + BNE .loads_left +.no_loads + JSR load_clear_inputs + RTL + +.loads_left + JSR pre_load_state + + %a8() + PHB + TDC : PHA : PLB + LDX #load_write_table + JMP run_vm +} + +load_write_table: + ; Disable HDMA + dw $1000|$420C, $00 + ; Turn PPU off + dw $1000|$2100, $80 + dw $1000|$4200, $00 + ; Single address, A bus -> B bus. B address = reflector to WRAM ($2180). + dw $0000|$4310, $8000 ; direction = A->B, B addr = $2180 + + ; Copy WRAM segments, uses $710000-$747FFF + %sram_to_wram($7E0000, $8000, $710000) + %sram_to_wram($7E8000, $8000, $720000) + %sram_to_wram($7F0000, $8000, $730000) + %sram_to_wram($7F8000, $8000, $740000) + + ; Address pair, A bus -> B bus. B address = VRAM write ($2118). + dw $0000|$4310, $1801 ; direction = A->B, B addr = $2118 + dw $1000|$2115, $0080 ; VRAM address increment mode. + + ; Copy VRAM segments, uses $750000-$767FFF + %sram_to_vram($0000, $8000, $750000) + %sram_to_vram($8000, $8000, $760000) + + ; Copy CGRAM, uses SRAM $771000-$7711FF + dw $1000|$2121, $00 ; CGRAM address + dw $0000|$4310, $2200 ; direction = A->B, byte reg, B addr = $2122 + dw $0000|$4312, $1000 ; A addr = $xx0000 + dw $0000|$4314, $0077 ; A addr = $77xxxx, size = $xx00 + dw $0000|$4316, $0002 ; size = $02xx ($0200), unused bank reg = $00. + dw $1000|$420B, $02 ; Trigger DMA on channel 1 + + ; Done + dw $0000, load_return + +load_return: +{ + %ai16() + PLB + LDA !SRAM_SAVED_SP : TCS + + ; Restore fix_transition_bad_tiles SRAM to prevent possible door transition corruption + %sram_to_sram($770200, $400, $704000) + + ; Restore temporary tilemap segment + %sram_to_sram($770000, $1000, $703000) + + ; Clear inputs and prevent repeated loads + JSR load_clear_inputs + + ; clear frame held counters + TDC + %ai8() + PHB + PHA : PLB + TAX : TXY + .load_dma_regs + ; Load DMA registers from SRAM + LDA !SRAM_DMA_BANK,X : STA $4300,X + INX + INY : CPY #$0B : BNE .load_dma_regs + CPX #$7B : BEQ .load_dma_regs_done + TXA : CLC : ADC #$05 : TAX + LDY #$00 + BRA .load_dma_regs + + .load_dma_regs_done + ; Restore registers and return. + %ai16() + PLB + JSR post_load_state + + ; inc counters + LDA !SRAM_SAVESTATE_TOTAL_LOADS : INC : STA !SRAM_SAVESTATE_TOTAL_LOADS + LDA !SRAM_SAVESTATE_CURR_LOADS : INC : STA !SRAM_SAVESTATE_CURR_LOADS + + JMP register_restore_return +} + +vm: +{ + ; Data format: xx xx yy yy + ; xxxx = little-endian address to write to .vm's bank + ; yyyy = little-endian value to write + ; If xxxx has high bit set, read and discard instead of write. + ; If xxxx has bit 12 set ($1000), byte instead of word. + ; If yyyy has $DD in the low half, it means that this operation is a byte + ; write instead of a word write. If xxxx is $0000, end the VM. + %ai16() + ; Read address to write to + LDA.w $0000,X : BEQ .vm_done + TAY + INX #2 + ; Check for byte mode + BIT.w #$1000 : BEQ .vm_word_mode + AND.w #$EFFF : TAY + %a8() + .vm_word_mode + ; Read value + LDA.w $0000,X + INX #2 + .vm_write + ; Check for read mode (high bit of address) + CPY.w #$8000 : BCS .vm_read + STA $0000,Y + BRA vm + .vm_read + ; "Subtract" $8000 from Y by taking advantage of bank wrapping. + LDA $8000,Y + BRA vm + .vm_done + ; A, X and Y are 16-bit at exit. + ; Return to caller. The word in the table after the terminator is the + ; code address to return to. + JMP ($0002,X) +} + +assert pc() <= !bank_85_free_space_end diff --git a/rust/data/presets/full-settings/Community Race Season 5.json b/rust/data/presets/full-settings/Community Race Season 5.json index 6b7919c651..242e232b08 100644 --- a/rust/data/presets/full-settings/Community Race Season 5.json +++ b/rust/data/presets/full-settings/Community Race Season 5.json @@ -4873,9 +4873,16 @@ "energy_free_shinesparks": false, "all_enemies_respawn": false, "race_mode": true, + "savestate": { + "preset": false, + "savestate_total_saves": 0, + "savestate_total_loads": 0, + "savestate_checkpoint_saves": 0, + "savestate_checkpoint_loads": 0 + }, "random_seed": null, "disable_spikesuit": false, "disable_bluesuit": false, "enable_major_glitches": false } -} \ No newline at end of file +} diff --git a/rust/data/presets/full-settings/Default.json b/rust/data/presets/full-settings/Default.json index b15db747f8..26b6e891cf 100644 --- a/rust/data/presets/full-settings/Default.json +++ b/rust/data/presets/full-settings/Default.json @@ -4788,6 +4788,13 @@ "enable_major_glitches": false, "speed_booster": "Vanilla", "race_mode": false, + "savestate": { + "preset": false, + "savestate_total_saves": 0, + "savestate_total_loads": 0, + "savestate_checkpoint_saves": 0, + "savestate_checkpoint_loads": 0 + }, "random_seed": null } -} \ No newline at end of file +} diff --git a/rust/data/presets/full-settings/Mentor Tournament.json b/rust/data/presets/full-settings/Mentor Tournament.json index 8c04f7c2ad..75148a26f0 100644 --- a/rust/data/presets/full-settings/Mentor Tournament.json +++ b/rust/data/presets/full-settings/Mentor Tournament.json @@ -4873,9 +4873,16 @@ "energy_free_shinesparks": false, "all_enemies_respawn": false, "race_mode": true, + "savestate": { + "preset": false, + "savestate_total_saves": 0, + "savestate_total_loads": 0, + "savestate_checkpoint_saves": 0, + "savestate_checkpoint_loads": 0 + }, "random_seed": null, "disable_spikesuit": false, "disable_bluesuit": false, "enable_major_glitches": false } -} \ No newline at end of file +} diff --git a/rust/data/presets/full-settings/Summer Series Expert Challenge.json b/rust/data/presets/full-settings/Summer Series Expert Challenge.json index f6fafe8b33..b0552b1e94 100644 --- a/rust/data/presets/full-settings/Summer Series Expert Challenge.json +++ b/rust/data/presets/full-settings/Summer Series Expert Challenge.json @@ -4785,9 +4785,16 @@ "energy_free_shinesparks": false, "all_enemies_respawn": false, "race_mode": true, + "savestate": { + "preset": false, + "savestate_total_saves": 0, + "savestate_total_loads": 0, + "savestate_checkpoint_saves": 0, + "savestate_checkpoint_loads": 0 + }, "random_seed": null, "disable_spikesuit": false, "disable_bluesuit": false, "enable_major_glitches": false } -} \ No newline at end of file +} diff --git a/rust/maprando-web/src/randomize_helpers.rs b/rust/maprando-web/src/randomize_helpers.rs index 2f35ea7dc7..ccd436a259 100644 --- a/rust/maprando-web/src/randomize_helpers.rs +++ b/rust/maprando-web/src/randomize_helpers.rs @@ -215,6 +215,9 @@ impl SeedHeaderTemplate<'_> { if other_settings.all_enemies_respawn { game_variations.push("All enemies respawn"); } + if other_settings.savestate.preset { + game_variations.push("Savestates enabled"); + } game_variations } } diff --git a/rust/maprando-web/src/seed.rs b/rust/maprando-web/src/seed.rs index d3dae0054e..d2ae82797b 100644 --- a/rust/maprando-web/src/seed.rs +++ b/rust/maprando-web/src/seed.rs @@ -216,6 +216,30 @@ struct CustomizeRequest { quick_reload_r: Option>, quick_reload_select: Option>, quick_reload_start: Option>, + save_state_left: Option>, + save_state_right: Option>, + save_state_up: Option>, + save_state_down: Option>, + save_state_x: Option>, + save_state_y: Option>, + save_state_a: Option>, + save_state_b: Option>, + save_state_l: Option>, + save_state_r: Option>, + save_state_select: Option>, + save_state_start: Option>, + load_state_left: Option>, + load_state_right: Option>, + load_state_up: Option>, + load_state_down: Option>, + load_state_x: Option>, + load_state_y: Option>, + load_state_a: Option>, + load_state_b: Option>, + load_state_l: Option>, + load_state_r: Option>, + load_state_select: Option>, + load_state_start: Option>, moonwalk: Text, } @@ -366,6 +390,8 @@ async fn customize_seed( angle_down: parse_controller_button(&req.control_angle_down.0).unwrap(), spin_lock_buttons: get_spin_lock_buttons(&req), quick_reload_buttons: get_quick_reload_buttons(&req), + save_state_buttons: get_save_state_buttons(&req), + load_state_buttons: get_load_state_buttons(&req), moonwalk: req.moonwalk.0, }, }; @@ -461,6 +487,60 @@ fn get_quick_reload_buttons(req: &CustomizeRequest) -> Vec { quick_reload_buttons } +fn get_save_state_buttons(req: &CustomizeRequest) -> Vec { + let mut save_state_buttons = vec![]; + let setting_button_mapping = vec![ + (&req.save_state_left, ControllerButton::Left), + (&req.save_state_right, ControllerButton::Right), + (&req.save_state_up, ControllerButton::Up), + (&req.save_state_down, ControllerButton::Down), + (&req.save_state_a, ControllerButton::A), + (&req.save_state_b, ControllerButton::B), + (&req.save_state_x, ControllerButton::X), + (&req.save_state_y, ControllerButton::Y), + (&req.save_state_l, ControllerButton::L), + (&req.save_state_r, ControllerButton::R), + (&req.save_state_select, ControllerButton::Select), + (&req.save_state_start, ControllerButton::Start), + ]; + + for (setting, button) in setting_button_mapping { + if let Some(x) = setting + && x.0 == "on" + { + save_state_buttons.push(button); + } + } + save_state_buttons +} + +fn get_load_state_buttons(req: &CustomizeRequest) -> Vec { + let mut load_state_buttons = vec![]; + let setting_button_mapping = vec![ + (&req.load_state_left, ControllerButton::Left), + (&req.load_state_right, ControllerButton::Right), + (&req.load_state_up, ControllerButton::Up), + (&req.load_state_down, ControllerButton::Down), + (&req.load_state_a, ControllerButton::A), + (&req.load_state_b, ControllerButton::B), + (&req.load_state_x, ControllerButton::X), + (&req.load_state_y, ControllerButton::Y), + (&req.load_state_l, ControllerButton::L), + (&req.load_state_r, ControllerButton::R), + (&req.load_state_select, ControllerButton::Select), + (&req.load_state_start, ControllerButton::Start), + ]; + + for (setting, button) in setting_button_mapping { + if let Some(x) = setting + && x.0 == "on" + { + load_state_buttons.push(button); + } + } + load_state_buttons +} + #[derive(Template)] #[template(path = "errors/invalid_token.html")] struct InvalidTokenTemplate {} diff --git a/rust/maprando-web/templates/generate/game_variations.html b/rust/maprando-web/templates/generate/game_variations.html index b0da895932..accab33a3a 100644 --- a/rust/maprando-web/templates/generate/game_variations.html +++ b/rust/maprando-web/templates/generate/game_variations.html @@ -133,6 +133,23 @@ +
+
+ {% include "help/variations/savestate.html" %} + +
+
+ + + + + +
+
+
{% include "help/seed.html" %} @@ -143,3 +160,4 @@
{% include "area_assignment.html" %} +{% include "savestate.html" %} diff --git a/rust/maprando-web/templates/generate/help/savestate.html b/rust/maprando-web/templates/generate/help/savestate.html new file mode 100644 index 0000000000..60b87432b9 --- /dev/null +++ b/rust/maprando-web/templates/generate/help/savestate.html @@ -0,0 +1,21 @@ + + + + diff --git a/rust/maprando-web/templates/generate/help/variations/savestate.html b/rust/maprando-web/templates/generate/help/variations/savestate.html new file mode 100644 index 0000000000..4e8e3f1fce --- /dev/null +++ b/rust/maprando-web/templates/generate/help/variations/savestate.html @@ -0,0 +1,23 @@ + + + + diff --git a/rust/maprando-web/templates/generate/savestate.html b/rust/maprando-web/templates/generate/savestate.html new file mode 100644 index 0000000000..16db9c0238 --- /dev/null +++ b/rust/maprando-web/templates/generate/savestate.html @@ -0,0 +1,50 @@ + + diff --git a/rust/maprando-web/templates/generate/scripts.html b/rust/maprando-web/templates/generate/scripts.html index 86329b42fa..ef5e241446 100644 --- a/rust/maprando-web/templates/generate/scripts.html +++ b/rust/maprando-web/templates/generate/scripts.html @@ -368,6 +368,13 @@ "energy_free_shinesparks": formData.get("energy_free_shinesparks") == "true", "all_enemies_respawn": formData.get("all_enemies_respawn") == "true", "race_mode": formData.get("race_mode") == "true", + "savestate": { + "preset": formData.get("savestate_preset") == "true", + "savestate_total_saves": tryParseInt(formData.get("savestate_total_saves")), + "savestate_total_loads": tryParseInt(formData.get("savestate_total_loads")), + "savestate_checkpoint_saves": tryParseInt(formData.get("savestate_checkpoint_saves")), + "savestate_checkpoint_loads": tryParseInt(formData.get("savestate_checkpoint_loads")), + }, "random_seed": tryParseInt(formData.get("random_seed")), "disable_spikesuit": formData.get("disable_spike_suit") == "true", "disable_bluesuit": formData.get("disable_blue_suit") == "true", @@ -648,6 +655,16 @@ document.getElementById("plasma_doors_count").value = preset.plasma_doors_count; } +function applySaveStatePreset(preset) { + applyRadioValue("saveStateMode", preset.preset); + + document.getElementById("savestate_total_saves").value = preset.savestate_total_saves; + document.getElementById("savestate_total_loads").value = preset.savestate_total_loads; + document.getElementById("savestate_checkpoint_saves").value = preset.savestate_checkpoint_saves; + document.getElementById("savestate_checkpoint_loads").value = preset.savestate_checkpoint_loads; +} + + function applyStartLocationSettings(preset) { applyRadioValue("startLocation", preset.mode); if (preset.mode == "Custom") { @@ -687,6 +704,7 @@ applyRadioValue("enableMajorGlitches", other.enable_major_glitches); applyRadioValue("speedBooster", other.speed_booster); applyRadioValue("raceMode", other.race_mode); + applySaveStatePreset(other.savestate); document.getElementById("randomSeed").value = other.random_seed; } @@ -831,6 +849,14 @@ doorsPresetChanged(); } +function saveStateSettingsChanged() { + for (var presetEl of document.getElementsByClassName("savestate-preset-button")) { + presetEl.checked = true; + } + + fullSettingsChanged(); +} + function applyAreaAssignmentPreset(preset) { if (preset == "Standard") { document.getElementById("areaAssignmentPresetStandard").checked = true; @@ -1416,6 +1442,7 @@ document.getElementById("speedBoosterSplit").checked || !document.getElementById("areaAssignmentPresetStandard").checked || document.getElementById("doorLocksSizeSmall").checked || + document.getElementById("saveStateModeYes").checked || document.getElementById("raceModeYes").checked) { document.getElementById("collapseGameVariations").classList.remove("collapse"); @@ -1802,4 +1829,4 @@ --bs-btn-disabled-border-color: #6c757d; --bs-gradient: none; } - \ No newline at end of file + diff --git a/rust/maprando-web/templates/seed/controller_settings.html b/rust/maprando-web/templates/seed/controller_settings.html index 1d6a264459..31b5b48855 100644 --- a/rust/maprando-web/templates/seed/controller_settings.html +++ b/rust/maprando-web/templates/seed/controller_settings.html @@ -59,12 +59,12 @@
- {% for (button, button_display) in all_buttons %} + {% for (button, button_display) in all_buttons.iter() %} {% let checked %}{% if ["L", "R", "Up", "X"].contains(button) %}{% let checked = "checked" %}{% else %}{% let checked = "" %}{% endif %} - {% if button == "X" %} + {% if button == &"X" %} - {% else if button == "Right" %} + {% else if button == &"Right" %} {% else %} @@ -72,6 +72,42 @@ {% endfor %}
+
+
+ +
+
+ {% for (button, button_display) in all_buttons.iter() %} + {% let checked %}{% if ["L", "R", "Select", "X"].contains(button) %}{% let checked = "checked" %}{% else %}{% let checked = "" %}{% endif %} + + {% if button == &"X" %} + + {% else if button == &"Right" %} + + {% else %} + + {% endif %} + {% endfor %} +
+
+
+
+ +
+
+ {% for (button, button_display) in all_buttons.iter() %} + {% let checked %}{% if ["L", "R", "Select", "Y"].contains(button) %}{% let checked = "checked" %}{% else %}{% let checked = "" %}{% endif %} + + {% if button == &"X" %} + + {% else if button == &"Right" %} + + {% else %} + + {% endif %} + {% endfor %} +
+
diff --git a/rust/maprando-web/templates/seed/savestate_details.html b/rust/maprando-web/templates/seed/savestate_details.html new file mode 100644 index 0000000000..c69319f1f2 --- /dev/null +++ b/rust/maprando-web/templates/seed/savestate_details.html @@ -0,0 +1,17 @@ +{% let ss = settings.other_settings.savestate %} +
+
Total saves:
+
{{ ss.savestate_total_saves.to_string() }}
+
+
+
Total loads:
+
{{ ss.savestate_total_loads.to_string() }}
+
+
+
Checkpoint saves:
+
{{ ss.savestate_checkpoint_saves.to_string() }}
+
+
+
Checkpoint loads:
+
{{ ss.savestate_checkpoint_loads.to_string() }}
+
diff --git a/rust/maprando-web/templates/seed/seed_header.html b/rust/maprando-web/templates/seed/seed_header.html index 61f29c07ae..874b2106a4 100644 --- a/rust/maprando-web/templates/seed/seed_header.html +++ b/rust/maprando-web/templates/seed/seed_header.html @@ -173,6 +173,15 @@

+
+
+ Savestate details +
+
+ {% include "savestate_details.html" %} +
+
+
Objective details diff --git a/rust/maprando/src/customize.rs b/rust/maprando/src/customize.rs index e98ec53e75..418c298d6f 100644 --- a/rust/maprando/src/customize.rs +++ b/rust/maprando/src/customize.rs @@ -123,6 +123,8 @@ pub struct ControllerConfig { pub angle_down: ControllerButton, pub spin_lock_buttons: Vec, pub quick_reload_buttons: Vec, + pub save_state_buttons: Vec, + pub load_state_buttons: Vec, pub moonwalk: bool, } @@ -370,6 +372,12 @@ fn apply_controller_config(rom: &mut Rom, controller_config: &ControllerConfig) rom.write_u16(snes2pc(addr), mask)?; } + let save_state_mask = get_button_list_mask(&controller_config.save_state_buttons); + rom.write_u16(snes2pc(0x82FE78), save_state_mask)?; + + let load_state_mask = get_button_list_mask(&controller_config.load_state_buttons); + rom.write_u16(snes2pc(0x82FE7A), load_state_mask)?; + let spin_lock_mask = get_button_list_mask(&controller_config.spin_lock_buttons); rom.write_u16(snes2pc(0x82FE7C), spin_lock_mask)?; diff --git a/rust/maprando/src/patch.rs b/rust/maprando/src/patch.rs index 210758b37c..0e4333de87 100644 --- a/rust/maprando/src/patch.rs +++ b/rust/maprando/src/patch.rs @@ -657,6 +657,36 @@ impl Patcher<'_> { patches.push("everything_respawns"); } + if self.settings.other_settings.savestate.preset { + patches.push("savestate"); + self.rom.write_u16( + snes2pc(0x85C006), + self.settings.other_settings.savestate.savestate_total_saves as isize, + )?; + self.rom.write_u16( + snes2pc(0x85C008), + self.settings.other_settings.savestate.savestate_total_loads as isize, + )?; + self.rom.write_u16( + snes2pc(0x85C00A), + self.settings + .other_settings + .savestate + .savestate_checkpoint_saves as isize, + )?; + self.rom.write_u16( + snes2pc(0x85C00C), + self.settings + .other_settings + .savestate + .savestate_checkpoint_loads as isize, + )?; + } else { + // null controller hooks + self.rom.write_u8(snes2pc(0x85C000), 0x6B)?; // RTL + self.rom.write_u8(snes2pc(0x85C003), 0x6B)?; // RTL + } + if self.settings.other_settings.disable_spikesuit { patches.push("remove_spikesuit"); } diff --git a/rust/maprando/src/settings.rs b/rust/maprando/src/settings.rs index e65069b65c..cc78bcf90c 100644 --- a/rust/maprando/src/settings.rs +++ b/rust/maprando/src/settings.rs @@ -654,6 +654,7 @@ pub struct OtherSettings { pub enable_major_glitches: bool, pub speed_booster: SpeedBooster, pub race_mode: bool, + pub savestate: SaveState, pub random_seed: Option, } @@ -797,6 +798,15 @@ impl AreaAssignment { } } +#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq)] +pub struct SaveState { + pub preset: bool, + pub savestate_total_saves: i32, + pub savestate_total_loads: i32, + pub savestate_checkpoint_saves: i32, + pub savestate_checkpoint_loads: i32, +} + #[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq)] pub enum CrashFixesPreset { Death, @@ -1485,6 +1495,19 @@ fn upgrade_other_settings(settings: &mut serde_json::Value) -> Result<()> { other_settings.insert("all_enemies_respawn".to_string(), false.into()); } + if other_settings.get("savestate").is_none() || !other_settings["savestate"].is_object() { + other_settings.insert( + "savestate".to_string(), + serde_json::json!({ + "preset": false, + "savestate_total_saves": 0, + "savestate_total_loads": 0, + "savestate_checkpoint_saves": 0, + "savestate_checkpoint_loads": 0 + }), + ); + } + if other_settings.get("disable_spikesuit").is_none() || other_settings["disable_spikesuit"].as_bool().is_none() { diff --git a/rust/maprando/tests/logic_scenarios.rs b/rust/maprando/tests/logic_scenarios.rs index a17b0ab235..1259805d73 100644 --- a/rust/maprando/tests/logic_scenarios.rs +++ b/rust/maprando/tests/logic_scenarios.rs @@ -349,6 +349,13 @@ fn get_settings(scenario: &Scenario) -> Result { disable_spikesuit: false, enable_major_glitches: false, race_mode: false, + savestate: maprando::settings::SaveState { + preset: false, + savestate_total_saves: 0, + savestate_total_loads: 0, + savestate_checkpoint_saves: 0, + savestate_checkpoint_loads: 0, + }, random_seed: None, }, debug: false, diff --git a/scripts/build_ips.py b/scripts/build_ips.py index b67621b2dd..a174ef9f45 100644 --- a/scripts/build_ips.py +++ b/scripts/build_ips.py @@ -126,7 +126,8 @@ def check_range(filename, other_filename, overlap_start, overlap_end): ['map_area', 0x829440, 0x829443], ['map_area', 0x829475, 0x829478], ['map_area', 0x829ED5, 0x829ED8], - ['item_dots_disappear', 0x82945C, 0x82945F] + ['item_dots_disappear', 0x82945C, 0x82945F], + ['savestate', 0x80ffd8, 0x80ffd9] ], 'pause_menu_objectives': [ ['map_area', 0x82910A, 0x82910D],