diff --git a/patches/ips/fast_reload.ips b/patches/ips/fast_reload.ips index 8668e1d63..2399041c6 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 b11373594..3c497e97d 100644 Binary files a/patches/ips/new_game.ips and b/patches/ips/new_game.ips differ diff --git a/patches/ips/savestate.ips b/patches/ips/savestate.ips new file mode 100644 index 000000000..c42fcbb2c 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 dd231edd6..3fd980188 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 - C400: ; savestate.asm diff --git a/patches/rom_map/SRAM.txt b/patches/rom_map/SRAM.txt index a1bd317e8..fd1950636 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-707F0A: savestate.asm +708A00: [FREE] +$710000-770200: savestate.asm diff --git a/patches/src/fast_reload.asm b/patches/src/fast_reload.asm index cf6305e76..bd259d233 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,17 @@ 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 + jsl $85c000 ; save state + bra .btn_leave +.nosavestate + lda $8B + cmp !loadstate_button_combo + bne .btn_leave + jsl $85c003 ; load state +.btn_leave plp rtl .reset: diff --git a/patches/src/map_area.asm b/patches/src/map_area.asm index 47005f566..5e1ec6848 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 e191c84da..dfce36a2a 100644 --- a/patches/src/new_game.asm +++ b/patches/src/new_game.asm @@ -188,6 +188,11 @@ startup: sta $702700, X txa bne .copy_revealed + + ; Initialize SRAM savestate counters + lda #$0000 + sta $707F08 ; saves + sta $707F0A ; loads .skip_init: diff --git a/patches/src/savestate.asm b/patches/src/savestate.asm new file mode 100644 index 000000000..776f98f44 --- /dev/null +++ b/patches/src/savestate.asm @@ -0,0 +1,439 @@ +; SD2SNES Savestate code +; by acmlm, total, Myria +; +; adapted by Stag Shot +arch 65816 +lorom + +org $80ffd8 + db 8 ; 256KB SRAM (ensure patch applied after map_progress_maintain) + +!bank_85_free_space_start = $85c000 +!bank_85_free_space_end = $85c400 + +!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_SAVES = $707F08 +!SRAM_SAVESTATE_LOADS = $707F0A +!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 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 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: +{ + ; 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 + + 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 + + ; Reload BG3 minimap tiles + JSL $85A290 + + 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() + LDA !REG_4200_NMI : STA $4200 + LDA #$0F : STA $13 : STA $0F2100 + %a16() + RTL +} + +save_state: +{ + %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 counter + LDA !SRAM_SAVESTATE_SAVES : INC : STA !SRAM_SAVESTATE_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 $770000-$7701FF + dw $1000|$2121, $00 ; CGRAM address + dw $0000|$4310, $3B80 ; direction = B->A, byte reg, B addr = $213B + dw $0000|$4312, $0000 ; 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 + + PLB + TSC : STA !SRAM_SAVED_SP + JMP register_restore_return +} + +load_state: +{ + LDA !SRAM_SAVESTATE_SAVES : BNE .save_exists : RTL + +.save_exists + 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 $770000-$7701FF + dw $1000|$2121, $00 ; CGRAM address + dw $0000|$4310, $2200 ; direction = A->B, byte reg, B addr = $2122 + dw $0000|$4312, $2000 ; 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 + + ; clear inputs + TDC : STA !IH_CONTROLLER_PRI : STA !IH_CONTROLLER_PRI_NEW + ; rewrite inputs so that holding load won't keep loading + ; TDC : STA !IH_CONTROLLER_PRI : STA !IH_CONTROLLER_SEC + ; DEC : STA !IH_CONTROLLER_PRI_NEW : STA !IH_CONTROLLER_SEC_NEW + ; STA !IH_CONTROLLER_PRI_PREV : STA !IH_CONTROLLER_SEC_PREV + + ; 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 counter + LDA !SRAM_SAVESTATE_LOADS : INC : STA !SRAM_SAVESTATE_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 6b7919c65..e8edecf08 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,10 @@ "energy_free_shinesparks": false, "all_enemies_respawn": false, "race_mode": true, + "savestate_mode": false, "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 b15db747f..2996e3420 100644 --- a/rust/data/presets/full-settings/Default.json +++ b/rust/data/presets/full-settings/Default.json @@ -4788,6 +4788,7 @@ "enable_major_glitches": false, "speed_booster": "Vanilla", "race_mode": false, + "savestate_mode": false, "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 8c04f7c2a..c87640028 100644 --- a/rust/data/presets/full-settings/Mentor Tournament.json +++ b/rust/data/presets/full-settings/Mentor Tournament.json @@ -4873,9 +4873,10 @@ "energy_free_shinesparks": false, "all_enemies_respawn": false, "race_mode": true, + "savestate_mode": false, "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 f6fafe8b3..a12420290 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,10 @@ "energy_free_shinesparks": false, "all_enemies_respawn": false, "race_mode": true, + "savestate_mode": false, "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/seed.rs b/rust/maprando-web/src/seed.rs index d3dae0054..d2ae82797 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 b0da89593..0d06d7ef3 100644 --- a/rust/maprando-web/templates/generate/game_variations.html +++ b/rust/maprando-web/templates/generate/game_variations.html @@ -133,6 +133,19 @@ +
+
+ {% include "help/savestate.html" %} + +
+
+ + + + +
+
+
{% include "help/seed.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 000000000..60b87432b --- /dev/null +++ b/rust/maprando-web/templates/generate/help/savestate.html @@ -0,0 +1,21 @@ + + + + diff --git a/rust/maprando-web/templates/generate/scripts.html b/rust/maprando-web/templates/generate/scripts.html index 86329b42f..7e21529ea 100644 --- a/rust/maprando-web/templates/generate/scripts.html +++ b/rust/maprando-web/templates/generate/scripts.html @@ -368,6 +368,7 @@ "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_mode": formData.get("savestate_mode") == "true", "random_seed": tryParseInt(formData.get("random_seed")), "disable_spikesuit": formData.get("disable_spike_suit") == "true", "disable_bluesuit": formData.get("disable_blue_suit") == "true", @@ -687,6 +688,7 @@ applyRadioValue("enableMajorGlitches", other.enable_major_glitches); applyRadioValue("speedBooster", other.speed_booster); applyRadioValue("raceMode", other.race_mode); + applyRadioValue("saveStateMode", other.savestate_mode); document.getElementById("randomSeed").value = other.random_seed; } @@ -1416,6 +1418,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 +1805,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 1d6a26445..31b5b4885 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/src/customize.rs b/rust/maprando/src/customize.rs index e98ec53e7..418c298d6 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 210758b37..db5dc7941 100644 --- a/rust/maprando/src/patch.rs +++ b/rust/maprando/src/patch.rs @@ -657,6 +657,14 @@ impl Patcher<'_> { patches.push("everything_respawns"); } + if self.settings.other_settings.savestate_mode { + patches.push("savestate"); + } + else { + 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 e65069b65..25a058dd3 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_mode: bool, pub random_seed: Option, } @@ -1485,6 +1486,12 @@ 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_mode").is_none() + || other_settings["savestate_mode"].as_bool().is_none() + { + other_settings.insert("savestate_mode".to_string(), false.into()); + } + 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 a17b0ab23..903e0fbbe 100644 --- a/rust/maprando/tests/logic_scenarios.rs +++ b/rust/maprando/tests/logic_scenarios.rs @@ -349,6 +349,7 @@ fn get_settings(scenario: &Scenario) -> Result { disable_spikesuit: false, enable_major_glitches: false, race_mode: false, + savestate_mode: false, random_seed: None, }, debug: false,