From 420c51122156bb7e24988b3568f469bdf789057f Mon Sep 17 00:00:00 2001 From: jeboo Date: Sat, 27 Jun 2026 18:24:37 -0400 Subject: [PATCH 01/19] implement savestate support --- patches/ips/fast_reload.ips | Bin 685 -> 705 bytes patches/ips/savestate.ips | Bin 0 -> 913 bytes patches/rom_map/Bank 85.txt | 1 + patches/rom_map/SRAM.txt | 1 + patches/src/fast_reload.asm | 11 +- patches/src/map_area.asm | 2 +- patches/src/savestate.asm | 433 ++++++++++++++++++ .../Community Race Season 5.json | 3 +- rust/data/presets/full-settings/Default.json | 3 +- .../full-settings/Mentor Tournament.json | 3 +- .../Summer Series Expert Challenge.json | 3 +- .../templates/generate/game_variations.html | 13 + .../templates/generate/help/savestate.html | 21 + .../templates/generate/scripts.html | 5 +- rust/maprando/src/patch.rs | 4 + rust/maprando/src/settings.rs | 7 + rust/maprando/tests/logic_scenarios.rs | 1 + 17 files changed, 504 insertions(+), 7 deletions(-) create mode 100644 patches/ips/savestate.ips create mode 100644 patches/src/savestate.asm create mode 100644 rust/maprando-web/templates/generate/help/savestate.html diff --git a/patches/ips/fast_reload.ips b/patches/ips/fast_reload.ips index 8668e1d63f20044425d5d709f230bf44dd4c4d23..b0035d5a5403915a629012e65fa8c152e3cca396 100644 GIT binary patch delta 46 zcmV+}0MY-g1;GW7z5&mXzk3}XrH?Ou{({gJ$#5Xh1|k5!g@6gkFksLGA_KsMvqJ%D E0pJc2`~Uy| delta 26 icmX@ex|VgqKE~Y>_g8arE$!E@``2`VX>&MZ79#+h-U|u< diff --git a/patches/ips/savestate.ips b/patches/ips/savestate.ips new file mode 100644 index 0000000000000000000000000000000000000000..c7d031826c781025def4fedcaeb513051322cc30 GIT binary patch literal 913 zcmchUPiPZS5XL8|nXOiA)LPRjvJE1HB5o1UBudp)DRi|Z=v8vGiV*))p%L?{CSYpV z>P>v95)hiKF?#e6O|};;1ai}Z2gyM~5E_YluCv+1wBp&*d;Iuj=KE$QPn?&>Ao2xl z-Sz@>-Br#Nl?grD5LA04lr;o(13fWRF%k-jdkgOQR5X}19An&YxOcw4cPU{w_~lKx zC$;%cwOUz7+rfSIF?(eQSud9SSReQKJX;`>A3*6ye9_3Gl4iZmA<(=yNUAU|i3Q1xS> zr`-4!u5XOHrW~0Uz`pBJYNj}@DS;&zVE_RDr_9(k8K6(T2K_RF*&%@YBLF=CJ6QaB zo3r)&Mw^q~wnl%m%}H-@r#$Q3LG${lx9=^q`c5}><{aY3JN(ukK14pqpWpIvo4CC> zhq~y$05naP!qK^?s`*ujgp{L25epU3+(GK1u1ps><7AP66Aj#Rugv@Xa%~I2dKLjtJR0#tfOajWw9a> Ys#XiY-Z-8Du&?icvCntxaC+j*Z)Is2#{d8T literal 0 HcmV?d00001 diff --git a/patches/rom_map/Bank 85.txt b/patches/rom_map/Bank 85.txt index dd231edd6f..3fd9801884 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 a1bd317e83..068a412551 100644 --- a/patches/rom_map/SRAM.txt +++ b/patches/rom_map/SRAM.txt @@ -33,3 +33,4 @@ $703000-707400: backup of RAM $7E5000-$7E5400 and $7E7000-$7EB000, used only dur $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] +$710000-770200: savestate.asm diff --git a/patches/src/fast_reload.asm b/patches/src/fast_reload.asm index cf6305e769..4912aee93a 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) @@ -140,6 +140,15 @@ 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 + cmp #$2070 ; L + R + Select + X + bne .nosavestate + jsl $85c000 ; save state + bra .noloadstate +.nosavestate + cmp #$6030 ; L + R + Select + Y + bne .noloadstate + jsl $85c003 ; load state +.noloadstate 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/savestate.asm b/patches/src/savestate.asm new file mode 100644 index 0000000000..e027165288 --- /dev/null +++ b/patches/src/savestate.asm @@ -0,0 +1,433 @@ +; 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_area) + +!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_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: +{ + 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: +{ +; JSL stop_all_sounds + ; 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 + + ; Makes the game check Samus' health again, to see if we need annoying sound + STZ !SAMUS_HEALTH_WARNING + ; see if no_beeping.asm applied + LDA $90EA92 : CMP #$EAEA : BNE .done_health_alarm + LDA #$0002 : JSL $80914D + + .done_health_alarm + 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 + %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 $772000-$7721FF + dw $1000|$2121, $00 ; CGRAM address + dw $0000|$4310, $3B80 ; direction = B->A, byte reg, B addr = $213B + dw $0000|$4312, $2000 ; A addr = $xx2000 + 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: +{ + 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 $772000-$7721FF + dw $1000|$2121, $00 ; CGRAM address + dw $0000|$4310, $2200 ; direction = A->B, byte reg, B addr = $2122 + dw $0000|$4312, $2000 ; A addr = $xx2000 + 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 + + ; rewrite inputs so that holding load won't keep loading + TDC : STA !IH_CONTROLLER_PRI : STA !IH_CONTROLLER_PRI_NEW + ; 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 + 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..e8edecf08c 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 b15db747f8..2996e3420f 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 8c04f7c2ad..c876400282 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 f6fafe8b33..a124202905 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/templates/generate/game_variations.html b/rust/maprando-web/templates/generate/game_variations.html index b0da895932..0d06d7ef3f 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 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/scripts.html b/rust/maprando-web/templates/generate/scripts.html index 86329b42fa..7e21529ea9 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/src/patch.rs b/rust/maprando/src/patch.rs index 210758b37c..856ef6daf6 100644 --- a/rust/maprando/src/patch.rs +++ b/rust/maprando/src/patch.rs @@ -657,6 +657,10 @@ impl Patcher<'_> { patches.push("everything_respawns"); } + if self.settings.other_settings.savestate_mode { + patches.push("savestate"); + } + 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..25a058dd3f 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 a17b0ab235..903e0fbbee 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, From 0425f78abd08ac5fc57012e0fc350aaf517c1546 Mon Sep 17 00:00:00 2001 From: jeboo Date: Sun, 28 Jun 2026 08:38:43 -0400 Subject: [PATCH 02/19] add support for disabled savestates --- rust/maprando/src/patch.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rust/maprando/src/patch.rs b/rust/maprando/src/patch.rs index 856ef6daf6..db5dc79411 100644 --- a/rust/maprando/src/patch.rs +++ b/rust/maprando/src/patch.rs @@ -660,6 +660,10 @@ impl Patcher<'_> { 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"); From 8b65d8608346842a1a63bbdb405f4a4a5bfdaf30 Mon Sep 17 00:00:00 2001 From: jeboo Date: Sun, 28 Jun 2026 10:04:38 -0400 Subject: [PATCH 03/19] fix low health check, move CGRAM --- patches/src/savestate.asm | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/patches/src/savestate.asm b/patches/src/savestate.asm index e027165288..8e8e6c47a2 100644 --- a/patches/src/savestate.asm +++ b/patches/src/savestate.asm @@ -6,7 +6,7 @@ arch 65816 lorom org $80ffd8 - db 8 ; 256KB SRAM (ensure patch applied after map_area) + db 8 ; 256KB SRAM (ensure patch applied after map_progress_maintain) !bank_85_free_space_start = $85c000 !bank_85_free_space_end = $85c400 @@ -136,7 +136,7 @@ post_load_music: ; Makes the game check Samus' health again, to see if we need annoying sound STZ !SAMUS_HEALTH_WARNING ; see if no_beeping.asm applied - LDA $90EA92 : CMP #$EAEA : BNE .done_health_alarm + LDA $90EA92 : CMP #$EAEA : BEQ .done_health_alarm LDA #$0002 : JSL $80914D .done_health_alarm @@ -288,10 +288,10 @@ save_write_table: %vram_to_sram($0000, $8000, $750000) %vram_to_sram($8000, $8000, $760000) - ; Copy CGRAM, uses SRAM $772000-$7721FF + ; 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, $2000 ; A addr = $xx2000 + 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 @@ -346,10 +346,10 @@ load_write_table: %sram_to_vram($0000, $8000, $750000) %sram_to_vram($8000, $8000, $760000) - ; Copy CGRAM, uses SRAM $772000-$7721FF + ; 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 = $xx2000 + 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 From 85d5ddd01b41bc154dcb07e6bcaaf83228b43ca0 Mon Sep 17 00:00:00 2001 From: jeboo Date: Mon, 29 Jun 2026 17:26:57 -0400 Subject: [PATCH 04/19] added SRAM save/load counters; cancel sound effects earlier in load --- patches/ips/new_game.ips | Bin 459 -> 470 bytes patches/ips/savestate.ips | Bin 913 -> 919 bytes patches/rom_map/SRAM.txt | 3 ++- patches/src/fast_reload.asm | 38 ++++++++++++++++++------------------ patches/src/new_game.asm | 5 +++++ patches/src/savestate.asm | 32 +++++++++++++++++------------- 6 files changed, 44 insertions(+), 34 deletions(-) diff --git a/patches/ips/new_game.ips b/patches/ips/new_game.ips index b113735940ff6a86b009b96e75189a6595076cde..3c497e97df5a991e3cb9851a59a6f8575db7d8c5 100644 GIT binary patch delta 70 zcmV-M0J;Cm1J(nOCm~Yxp#g2v00ts<^`YwO0dCp=0Ur?b00EOCJoTaQ157uONY4RS clNbR`3IG6)2!C*o3V(2uWdSJxJd=+BJi$d6?EnA( delta 59 zcmV-B0L1^+1Iq)DCm~1mp#g2v00ts!^`YwO0dCp=0Ur?b00D?1G4-MF157NDNY4RH RlNbR`lW_qk0Wp)B0X(|46`lY9 diff --git a/patches/ips/savestate.ips b/patches/ips/savestate.ips index c7d031826c781025def4fedcaeb513051322cc30..c42fcbb2cf81f2c9de75e2293433cbfe25408f1a 100644 GIT binary patch delta 176 zcmbQpKAqh;z%j(xgQ5Ng10x610S4w4AM1lYu7}osW%Zczl~qZ6UsFc!SJt(k+54I5 z3)X&R?`Hv0ZEXE)^#utE#s`!pENV@d7?!{&F>(7zq4gZ~1ycP$;*i0jf`i_xeIh1z zGVa$gIP`^qVfE@m2GyP(f5fNC1D>009UBzyJe&OisZ}Pr|M92af`OaIN(Rj|6{kt%e4V z27hp1ASA#dkfMcPt@Q;+ob?4F7rugMjr9ebY6`EC>X6Cm>d*(N0staSk${myU;!(U zq{#`wFrs?FORG#$lYarfB|5_N00672!Z3S Date: Sat, 4 Jul 2026 14:41:43 -0400 Subject: [PATCH 05/19] add customizable save/load state inputs --- patches/ips/fast_reload.ips | Bin 705 -> 711 bytes patches/ips/savestate.ips | Bin 919 -> 933 bytes patches/src/fast_reload.asm | 14 +-- patches/src/savestate.asm | 6 +- rust/maprando-web/src/seed.rs | 80 ++++++++++++++++++ .../templates/seed/controller_settings.html | 42 ++++++++- rust/maprando/src/customize.rs | 8 ++ 7 files changed, 140 insertions(+), 10 deletions(-) diff --git a/patches/ips/fast_reload.ips b/patches/ips/fast_reload.ips index b0035d5a5403915a629012e65fa8c152e3cca396..2399041c6be0dbff25ec17ef7b3ae14d38f4a58d 100644 GIT binary patch delta 46 zcmX@edYpB_KE|sP_gBlREbZ5?``2_qW@-2N3Lwp<#BiXsfd|N|`q#8Mkg?If G=41eX`w;yA delta 36 scmZ3=KAl}4z%j(xgQ5Ng10x610S4xljRJX$j1imb8MiS`c4kfj0Jp3PA^-pY diff --git a/patches/src/fast_reload.asm b/patches/src/fast_reload.asm index 3091192c9b..bd259d2338 100644 --- a/patches/src/fast_reload.asm +++ b/patches/src/fast_reload.asm @@ -13,6 +13,8 @@ arch 65816 !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 @@ -140,15 +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 - cmp #$2070 ; L + R + Select + X + lda $8B + cmp !savestate_button_combo bne .nosavestate jsl $85c000 ; save state - bra .noloadstate + bra .btn_leave .nosavestate - cmp #$6030 ; L + R + Select + Y - bne .noloadstate + lda $8B + cmp !loadstate_button_combo + bne .btn_leave jsl $85c003 ; load state -.noloadstate +.btn_leave plp rtl .reset: diff --git a/patches/src/savestate.asm b/patches/src/savestate.asm index 54aad4179e..776f98f443 100644 --- a/patches/src/savestate.asm +++ b/patches/src/savestate.asm @@ -363,9 +363,11 @@ load_return: PLB LDA !SRAM_SAVED_SP : TCS - ; rewrite inputs so that holding load won't keep loading + ; clear inputs TDC : STA !IH_CONTROLLER_PRI : STA !IH_CONTROLLER_PRI_NEW - ; DEC : STA !IH_CONTROLLER_PRI_NEW : STA !IH_CONTROLLER_SEC_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 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/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/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)?; From 625266e757345049bf4cfedb5993118959dca9aa Mon Sep 17 00:00:00 2001 From: jeboo Date: Sat, 4 Jul 2026 15:57:49 -0400 Subject: [PATCH 06/19] update IPS --- patches/ips/savestate.ips | Bin 933 -> 919 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/patches/ips/savestate.ips b/patches/ips/savestate.ips index 1ff9d21d108fd2beeb6908e7f2595da872865bd0..c42fcbb2cf81f2c9de75e2293433cbfe25408f1a 100644 GIT binary patch delta 36 scmZ3=KAl}4z%j(xgQ5Ng10x610S4xljRJX$j1imb8MiS`c4kfj0Jp3PA^-pY delta 50 zcmbQvzLZ@cz%j(xgQ5Ng10x610S4wt8wK(h8Phh`Gj3z#ZS8LDwQB8eo!C0Pb>?If G=41eX`w;yA From c975468d63fa341b0fed3ab086cbda55f3284b21 Mon Sep 17 00:00:00 2001 From: jeboo Date: Mon, 6 Jul 2026 15:47:23 -0400 Subject: [PATCH 07/19] prevent repeated saves/loads --- patches/ips/fast_reload.ips | Bin 711 -> 727 bytes patches/ips/savestate.ips | Bin 919 -> 925 bytes patches/src/fast_reload.asm | 6 ++++++ patches/src/savestate.asm | 7 ++----- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/patches/ips/fast_reload.ips b/patches/ips/fast_reload.ips index 2399041c6be0dbff25ec17ef7b3ae14d38f4a58d..c97e3453345dfb56db90678e4239cb1f3c8bf88c 100644 GIT binary patch delta 63 zcmX@kdYyH`KE|gL_gCwgEbZ5?``2_qXKDBO3Lwn~O4| delta 36 scmbQsKAl}4z%j(xgQ5Ng10x610S4xljRJX$j1imb8MiV{wqi~O0Jdxj5dZ)H diff --git a/patches/src/fast_reload.asm b/patches/src/fast_reload.asm index bd259d2338..afaf372b77 100644 --- a/patches/src/fast_reload.asm +++ b/patches/src/fast_reload.asm @@ -145,12 +145,18 @@ hook_main: 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 diff --git a/patches/src/savestate.asm b/patches/src/savestate.asm index 776f98f443..edb16dc9cc 100644 --- a/patches/src/savestate.asm +++ b/patches/src/savestate.asm @@ -363,12 +363,9 @@ load_return: PLB LDA !SRAM_SAVED_SP : TCS - ; clear inputs + ; Clear inputs and prevent repeated loads 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 + LDA $82FE7A : STA !IH_CONTROLLER_PRI_PREV ; clear frame held counters TDC From 84fd1265ece8040c0e37a47a6dc381df7ee1b1f2 Mon Sep 17 00:00:00 2001 From: jeboo Date: Wed, 8 Jul 2026 00:45:50 -0400 Subject: [PATCH 08/19] fix corrupted palette/flashing during load --- patches/ips/savestate.ips | Bin 925 -> 921 bytes patches/src/savestate.asm | 6 ++++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/patches/ips/savestate.ips b/patches/ips/savestate.ips index 11508e9e0b8f71e086d3077ecf773ffcd56811a0..61f6979e2a55a62240992303451dca99d940cfcd 100644 GIT binary patch delta 73 zcmV-P0Ji^~2bl*IP(f5fNC1D>009UBzyJe=OgF(yL9rDp0S3Y#A~=J9lS%009UBzyJe^Ogq6$MX?nt0Sc)Pg%gheArHbJlSTnA j25`Ykt4v;#djY=zFq1n1Cj?u Date: Wed, 8 Jul 2026 07:55:04 -0400 Subject: [PATCH 09/19] add support for credits --- patches/ips/savestate.ips | Bin 921 -> 929 bytes patches/src/savestate.asm | 7 +++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/patches/ips/savestate.ips b/patches/ips/savestate.ips index 61f6979e2a55a62240992303451dca99d940cfcd..6588d71623dd716762c9c546e599ea6400c61606 100644 GIT binary patch delta 120 zcmV-;0EhpX2cZWQP(f5fNC1D>009UBzyJe|Oh3U)Ns$#N3Om59m!c1?IW&*nb8009UBzyJe=OgF(yL6H?F0yDsoMr8pQk*v`MY{5&b zOj?tJ0lzFP!t?+DtE<8=dxeXIkE=V60Do{yDZ#G@e{j$NYaj-|;vkEANUNe(!c1qA SZUVaj6O$?fMgb|4aRV^Jnkfwc diff --git a/patches/src/savestate.asm b/patches/src/savestate.asm index c2981cf41c..ec21dd248a 100644 --- a/patches/src/savestate.asm +++ b/patches/src/savestate.asm @@ -126,9 +126,12 @@ post_load_state: { JSR post_load_music - ; Reload BG3 minimap tiles + ; Reload BG3 (minimap) tiles, except in credits + LDA $0998 + CMP #$0027 + BEQ .skip_bg3 JSL $85A290 - +.skip_bg3 RTS } From f35024ea458aeedc536ffffc7f5bf70bd672f9b2 Mon Sep 17 00:00:00 2001 From: jeboo Date: Fri, 10 Jul 2026 13:57:47 -0400 Subject: [PATCH 10/19] fix possible corruption when saving during door transition --- patches/ips/savestate.ips | Bin 929 -> 959 bytes patches/rom_map/SRAM.txt | 4 ++-- patches/src/savestate.asm | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/patches/ips/savestate.ips b/patches/ips/savestate.ips index 6588d71623dd716762c9c546e599ea6400c61606..06d4b897c7611a044fbb9f085e4fcc0e621cb5b3 100644 GIT binary patch delta 92 zcmZ3;zMow*z%j(xgQ5Ng10x610S4wZKK2KFA~uTVGBP$yZe~2ozlecl|3CYJ`TrQp rPo28(WpV(M6l31xY^HsZT89rAtX|JhUmy)u!&p8as1B^gm^lvs3brL0 delta 61 zcmV-D0K)&j2cZWQP(f5fNC1D>009UBzyJe|Oh3U)NwF1e0Rdu@g8|KxUjiBdUz2(Q Tz6u+|!Z53^0Do{BlTib1L+KOI diff --git a/patches/rom_map/SRAM.txt b/patches/rom_map/SRAM.txt index fd1950636c..c823e5a46a 100644 --- a/patches/rom_map/SRAM.txt +++ b/patches/rom_map/SRAM.txt @@ -32,6 +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-707F0A: savestate.asm +$707F00-708000: savestate.asm 708A00: [FREE] -$710000-770200: savestate.asm +$710000-770400: savestate.asm diff --git a/patches/src/savestate.asm b/patches/src/savestate.asm index ec21dd248a..e4493848d8 100644 --- a/patches/src/savestate.asm +++ b/patches/src/savestate.asm @@ -38,6 +38,7 @@ org $80ffd8 !SRAM_SAVESTATE_SAVES = $707F08 !SRAM_SAVESTATE_LOADS = $707F0A !SRAM_DMA_BANK = $707F80 + !MUSIC_ROUTINE = $808FC1 macro wram_to_sram(wram_addr, size, sram_addr) @@ -75,6 +76,15 @@ macro sram_to_vram(vram_addr, size, sram_addr) dw $1000|$420B, $02 ; Trigger DMA on channel 1 endmacro +macro sram_to_sram(sram_src, size, sram_dest) + LDX # +.cp_loop + LDA -2,X + STA -2,X + DEX : DEX + BNE .cp_loop +endmacro + macro a8() ; A = 8-bit SEP #$20 endmacro @@ -308,6 +318,9 @@ save_return: %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) PLB TSC : STA !SRAM_SAVED_SP @@ -368,6 +381,9 @@ load_return: PLB LDA !SRAM_SAVED_SP : TCS + ; Restore fix_transition_bad_tiles SRAM to prevent possible door transition corruption + %sram_to_sram($770200, $400, $704000) + ; Clear inputs and prevent repeated loads TDC : STA !IH_CONTROLLER_PRI : STA !IH_CONTROLLER_PRI_NEW LDA $82FE7A : STA !IH_CONTROLLER_PRI_PREV From a88c13a0a0d7f30f136b1345efaff6433876ff26 Mon Sep 17 00:00:00 2001 From: jeboo Date: Wed, 15 Jul 2026 04:30:32 -0400 Subject: [PATCH 11/19] add support for savestate limits --- patches/ips/new_game.ips | Bin 470 -> 474 bytes patches/ips/saveload.ips | Bin 993 -> 1004 bytes patches/ips/savestate.ips | Bin 959 -> 1046 bytes patches/rom_map/SRAM.txt | 2 +- patches/src/constants.asm | 2 + patches/src/new_game.asm | 10 +++-- patches/src/saveload.asm | 4 ++ patches/src/savestate.asm | 87 +++++++++++++++++++++++++++++--------- 8 files changed, 81 insertions(+), 24 deletions(-) diff --git a/patches/ips/new_game.ips b/patches/ips/new_game.ips index 3c497e97df5a991e3cb9851a59a6f8575db7d8c5..3eec2c0fb292568addb717ab6c450b89104b9fd4 100644 GIT binary patch delta 72 zcmV-O0Js0v1KI=*YrnGhvjC|!wtlwy zf&>NU18cvsdd&ICswBRzDWmr*>qNU;MvaMU7R#<@JJ9-p?L0?)!3Uo8TtGI@d7k=$ z3vAV`-L3tT{TOwG)&u!c{XpW7!J@?ny;u8OpWMv2pD}r|Gn2BNH0J!sU AcmMzZ delta 148 zcmV;F0Bir22)_p009UBzyJfROh3U)Si-IK1xTFr1tJ%|f@qEP1(6;X z0z1HwMr{Egk+h?ebpb00!Z4zA!Aq-5fRnNTzX3Fpasne4F~P40e{j$NYaj-|;vkEA zNUNf5!c1?I(*nBzDwB@`7z=xai-nJ`dj5iimy@akCJigVuL^%~8jlKpa7;0i<^wQ5 CAUVna diff --git a/patches/rom_map/SRAM.txt b/patches/rom_map/SRAM.txt index c823e5a46a..b996ae44dc 100644 --- a/patches/rom_map/SRAM.txt +++ b/patches/rom_map/SRAM.txt @@ -33,5 +33,5 @@ $703000-707400: backup of RAM $7E5000-$7E5400 and $7E7000-$7EB000, used only dur $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-708000: savestate.asm -708A00: [FREE] +$708A00-710000: [FREE] $710000-770400: 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/new_game.asm b/patches/src/new_game.asm index dfce36a2a1..a4f51be9a7 100644 --- a/patches/src/new_game.asm +++ b/patches/src/new_game.asm @@ -191,8 +191,12 @@ startup: ; Initialize SRAM savestate counters lda #$0000 - sta $707F08 ; saves - sta $707F0A ; loads + tax +.init_ss_lp + sta !savestate_counts, X + inx : inx + cpx #$0008 + bne .init_ss_lp .skip_init: @@ -258,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 index e4493848d8..92cc691635 100644 --- a/patches/src/savestate.asm +++ b/patches/src/savestate.asm @@ -5,11 +5,13 @@ 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 = $85c400 +!bank_85_free_space_end = $85c450 !REG_4200_NMI = $84 !IH_CONTROLLER_PRI = $8B @@ -35,8 +37,11 @@ org $80ffd8 !SRAM_MUSIC_DATA = $707F02 !SRAM_MUSIC_TRACK = $707F04 !SRAM_SOUND_TIMER = $707F06 -!SRAM_SAVESTATE_SAVES = $707F08 -!SRAM_SAVESTATE_LOADS = $707F0A +!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 @@ -110,22 +115,21 @@ macro ai16() ; A + X/Y = 16-bit 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: dw $0 +savestate_total_loads_max: dw $0 +savestate_curr_saves_max: dw $0 +savestate_curr_loads_max: dw $0 +; ***************************************** ; 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 @@ -136,6 +140,12 @@ 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 @@ -249,6 +259,21 @@ register_restore_return: 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 @@ -265,8 +290,9 @@ save_state: BRA .save_dma_regs .done - ; inc counter - LDA !SRAM_SAVESTATE_SAVES : INC : STA !SRAM_SAVESTATE_SAVES + ; 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 @@ -327,11 +353,32 @@ save_return: 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_SAVES : BNE .save_exists : RTL + 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() @@ -385,9 +432,8 @@ load_return: %sram_to_sram($770200, $400, $704000) ; Clear inputs and prevent repeated loads - TDC : STA !IH_CONTROLLER_PRI : STA !IH_CONTROLLER_PRI_NEW - LDA $82FE7A : STA !IH_CONTROLLER_PRI_PREV - + JSR load_clear_inputs + ; clear frame held counters TDC %ai8() @@ -410,8 +456,9 @@ load_return: PLB JSR post_load_state - ; inc counter - LDA !SRAM_SAVESTATE_LOADS : INC : STA !SRAM_SAVESTATE_LOADS + ; 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 } From c3d169018261cc1d858360b80e4d201e54385487 Mon Sep 17 00:00:00 2001 From: jeboo Date: Wed, 15 Jul 2026 05:03:07 -0400 Subject: [PATCH 12/19] update ips build whitelist --- scripts/build_ips.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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], From 4999d30fe7916c2ca1a69859900f7996cfb4607b Mon Sep 17 00:00:00 2001 From: jeboo Date: Fri, 17 Jul 2026 13:46:53 -0400 Subject: [PATCH 13/19] add limits setting --- patches/ips/savestate.ips | Bin 1046 -> 1043 bytes patches/src/savestate.asm | 8 +-- .../Community Race Season 5.json | 8 ++- rust/data/presets/full-settings/Default.json | 8 ++- .../full-settings/Mentor Tournament.json | 8 ++- .../Summer Series Expert Challenge.json | 8 ++- rust/maprando-web/src/randomize_helpers.rs | 3 ++ .../templates/generate/game_variations.html | 13 +++-- .../generate/help/variations/savestate.html | 23 ++++++++ .../templates/generate/savestate.html | 50 ++++++++++++++++++ .../templates/generate/scripts.html | 28 +++++++++- .../templates/seed/savestate_details.html | 17 ++++++ .../templates/seed/seed_header.html | 9 ++++ rust/maprando/src/patch.rs | 8 ++- rust/maprando/src/settings.rs | 26 +++++++-- rust/maprando/tests/logic_scenarios.rs | 1 - 16 files changed, 197 insertions(+), 21 deletions(-) create mode 100644 rust/maprando-web/templates/generate/help/variations/savestate.html create mode 100644 rust/maprando-web/templates/generate/savestate.html create mode 100644 rust/maprando-web/templates/seed/savestate_details.html diff --git a/patches/ips/savestate.ips b/patches/ips/savestate.ips index 7a37344ca3417753f8f0ba7ba8afc41c7544c889..c6a9ed007ffe25e4b1865351b2a86d60fca179b4 100644 GIT binary patch delta 36 scmbQnF_}X;z%j(xgQ5Ng10x610R{#(ANPYklMgW+;A8%}QSJvb0K6p&bN~PV delta 39 pcmbQtF^xkmz%j(xgQ5Ng10x610R|RkANPYklMgXK!A9vH%mB#k39SGC diff --git a/patches/src/savestate.asm b/patches/src/savestate.asm index 92cc691635..fcf199f274 100644 --- a/patches/src/savestate.asm +++ b/patches/src/savestate.asm @@ -120,10 +120,10 @@ org !bank_85_free_space_start jmp save_state jmp load_state ; These settings must remain at this address for patch.rs -savestate_total_saves_max: dw $0 -savestate_total_loads_max: dw $0 -savestate_curr_saves_max: dw $0 -savestate_curr_loads_max: dw $0 +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 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 e8edecf08c..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,7 +4873,13 @@ "energy_free_shinesparks": false, "all_enemies_respawn": false, "race_mode": true, - "savestate_mode": false, + "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, diff --git a/rust/data/presets/full-settings/Default.json b/rust/data/presets/full-settings/Default.json index 2996e3420f..26b6e891cf 100644 --- a/rust/data/presets/full-settings/Default.json +++ b/rust/data/presets/full-settings/Default.json @@ -4788,7 +4788,13 @@ "enable_major_glitches": false, "speed_booster": "Vanilla", "race_mode": false, - "savestate_mode": false, + "savestate": { + "preset": false, + "savestate_total_saves": 0, + "savestate_total_loads": 0, + "savestate_checkpoint_saves": 0, + "savestate_checkpoint_loads": 0 + }, "random_seed": null } } diff --git a/rust/data/presets/full-settings/Mentor Tournament.json b/rust/data/presets/full-settings/Mentor Tournament.json index c876400282..75148a26f0 100644 --- a/rust/data/presets/full-settings/Mentor Tournament.json +++ b/rust/data/presets/full-settings/Mentor Tournament.json @@ -4873,7 +4873,13 @@ "energy_free_shinesparks": false, "all_enemies_respawn": false, "race_mode": true, - "savestate_mode": false, + "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, 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 a124202905..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,7 +4785,13 @@ "energy_free_shinesparks": false, "all_enemies_respawn": false, "race_mode": true, - "savestate_mode": false, + "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, 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/templates/generate/game_variations.html b/rust/maprando-web/templates/generate/game_variations.html index 0d06d7ef3f..accab33a3a 100644 --- a/rust/maprando-web/templates/generate/game_variations.html +++ b/rust/maprando-web/templates/generate/game_variations.html @@ -135,14 +135,18 @@
- {% include "help/savestate.html" %} - + {% include "help/variations/savestate.html" %} +
- + - + +
@@ -156,3 +160,4 @@
{% include "area_assignment.html" %} +{% include "savestate.html" %} 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 7e21529ea9..ef5e241446 100644 --- a/rust/maprando-web/templates/generate/scripts.html +++ b/rust/maprando-web/templates/generate/scripts.html @@ -368,7 +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_mode": formData.get("savestate_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", @@ -649,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") { @@ -688,7 +704,7 @@ applyRadioValue("enableMajorGlitches", other.enable_major_glitches); applyRadioValue("speedBooster", other.speed_booster); applyRadioValue("raceMode", other.race_mode); - applyRadioValue("saveStateMode", other.savestate_mode); + applySaveStatePreset(other.savestate); document.getElementById("randomSeed").value = other.random_seed; } @@ -833,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; 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/patch.rs b/rust/maprando/src/patch.rs index db5dc79411..4cffcf7bf9 100644 --- a/rust/maprando/src/patch.rs +++ b/rust/maprando/src/patch.rs @@ -657,10 +657,14 @@ impl Patcher<'_> { patches.push("everything_respawns"); } - if self.settings.other_settings.savestate_mode { + 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 { + else { // null controller hooks self.rom.write_u8(snes2pc(0x85C000), 0x6B)?; // RTL self.rom.write_u8(snes2pc(0x85C003), 0x6B)?; // RTL } diff --git a/rust/maprando/src/settings.rs b/rust/maprando/src/settings.rs index 25a058dd3f..155f155021 100644 --- a/rust/maprando/src/settings.rs +++ b/rust/maprando/src/settings.rs @@ -654,7 +654,7 @@ pub struct OtherSettings { pub enable_major_glitches: bool, pub speed_booster: SpeedBooster, pub race_mode: bool, - pub savestate_mode: bool, + pub savestate: SaveState, pub random_seed: Option, } @@ -798,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, @@ -1486,10 +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_mode").is_none() - || other_settings["savestate_mode"].as_bool().is_none() + if other_settings.get("savestate").is_none() + || !other_settings["savestate"].is_object() { - other_settings.insert("savestate_mode".to_string(), false.into()); + 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() diff --git a/rust/maprando/tests/logic_scenarios.rs b/rust/maprando/tests/logic_scenarios.rs index 903e0fbbee..a17b0ab235 100644 --- a/rust/maprando/tests/logic_scenarios.rs +++ b/rust/maprando/tests/logic_scenarios.rs @@ -349,7 +349,6 @@ fn get_settings(scenario: &Scenario) -> Result { disable_spikesuit: false, enable_major_glitches: false, race_mode: false, - savestate_mode: false, random_seed: None, }, debug: false, From ae809b57d774c7e8fb73646c61fd6b6459f2cd32 Mon Sep 17 00:00:00 2001 From: jeboo Date: Fri, 17 Jul 2026 13:50:42 -0400 Subject: [PATCH 14/19] fix formatting --- rust/maprando/src/patch.rs | 30 ++++++++++++++++++++++++------ rust/maprando/src/settings.rs | 4 +--- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/rust/maprando/src/patch.rs b/rust/maprando/src/patch.rs index 4cffcf7bf9..0e4333de87 100644 --- a/rust/maprando/src/patch.rs +++ b/rust/maprando/src/patch.rs @@ -659,12 +659,30 @@ impl Patcher<'_> { 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_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 } diff --git a/rust/maprando/src/settings.rs b/rust/maprando/src/settings.rs index 155f155021..cc78bcf90c 100644 --- a/rust/maprando/src/settings.rs +++ b/rust/maprando/src/settings.rs @@ -1495,9 +1495,7 @@ 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() - { + if other_settings.get("savestate").is_none() || !other_settings["savestate"].is_object() { other_settings.insert( "savestate".to_string(), serde_json::json!({ From 25aa0d7111066039a6b155ecaecdbcee70ab9ffe Mon Sep 17 00:00:00 2001 From: jeboo Date: Sat, 18 Jul 2026 04:12:15 -0400 Subject: [PATCH 15/19] fix build errors --- rust/maprando/tests/logic_scenarios.rs | 7 +++++++ 1 file changed, 7 insertions(+) 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, From 01596d22d64bec56d9d4bced14e477ea433ddcda Mon Sep 17 00:00:00 2001 From: jeboo Date: Sat, 18 Jul 2026 08:09:07 -0400 Subject: [PATCH 16/19] update bank 85 --- patches/rom_map/Bank 85.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patches/rom_map/Bank 85.txt b/patches/rom_map/Bank 85.txt index 3fd9801884..e7bf5015a0 100644 --- a/patches/rom_map/Bank 85.txt +++ b/patches/rom_map/Bank 85.txt @@ -21,4 +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 +C000 - C450: ; savestate.asm From 8f511e4ef1b38b64694db5234b8f5c09d2c91356 Mon Sep 17 00:00:00 2001 From: jeboo Date: Wed, 22 Jul 2026 06:48:16 -0400 Subject: [PATCH 17/19] fix minimap corruption when loading from pause menu --- patches/ips/savestate.ips | Bin 1043 -> 1073 bytes patches/rom_map/SRAM.txt | 2 +- patches/src/savestate.asm | 20 +++++++++++++------- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/patches/ips/savestate.ips b/patches/ips/savestate.ips index c6a9ed007ffe25e4b1865351b2a86d60fca179b4..2e1dfb9b76f50e7419e94e1587d77449ef1b407d 100644 GIT binary patch delta 108 zcmbQtv5{kf2>YT#Ob7T_gf~i0W@P+2c|GF>MuEu*OxoOw7zFnJ(=V9+?|<3kb|wqH zX@{~E_zpZ$=&tryz3B4fvrOv+1Vo(68N{5K7`TI&7#JA(C-*Q*2!i$fFPjh4d+OAM MFACEpFJsOH0D4*}-2eap delta 100 zcmV-q0Gt1@2$Kkq6bF;S0>BOf^|2e10Rh*OuK}E0dZ6G!BQtYakB5;vkEA zNUNg6liUKYDj*a?cK{Yc0ssqA0ssI2fWyKttFHima2uik1i$_Pcc1=0aLUTi^dN_m G!~<<4 -.cp_loop +- LDA -2,X STA -2,X DEX : DEX - BNE .cp_loop + BNE - endmacro macro a8() ; A = 8-bit @@ -326,10 +326,10 @@ save_write_table: %vram_to_sram($0000, $8000, $750000) %vram_to_sram($8000, $8000, $760000) - ; Copy CGRAM, uses SRAM $770000-$7701FF + ; 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, $0000 ; A addr = $xx0000 + 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 @@ -347,6 +347,9 @@ save_return: ; 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 @@ -411,10 +414,10 @@ load_write_table: %sram_to_vram($0000, $8000, $750000) %sram_to_vram($8000, $8000, $760000) - ; Copy CGRAM, uses SRAM $770000-$7701FF + ; 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, $2000 ; A addr = $xx0000 + 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 @@ -430,7 +433,10 @@ load_return: ; 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 From 57ff31edacfed3827f7e4a349d417af251cf04c1 Mon Sep 17 00:00:00 2001 From: Brent Kerby Date: Fri, 24 Jul 2026 20:04:43 -0600 Subject: [PATCH 18/19] Simplify save state settings (web UI) --- .../Community Race Season 5.json | 8 +-- rust/data/presets/full-settings/Default.json | 8 +-- .../full-settings/Mentor Tournament.json | 8 +-- .../Summer Series Expert Challenge.json | 8 +-- rust/maprando-web/src/randomize_helpers.rs | 8 +-- .../templates/generate/game_variations.html | 15 +++--- .../templates/generate/help/savestate.html | 21 -------- .../generate/help/variations/savestate.html | 6 ++- .../templates/generate/savestate.html | 50 ------------------- .../templates/generate/scripts.html | 30 ++--------- .../templates/seed/seed_header.html | 9 ---- rust/maprando/src/patch.rs | 36 +++++-------- rust/maprando/src/settings.rs | 31 +++++------- rust/maprando/tests/logic_scenarios.rs | 8 +-- 14 files changed, 48 insertions(+), 198 deletions(-) delete mode 100644 rust/maprando-web/templates/generate/help/savestate.html delete mode 100644 rust/maprando-web/templates/generate/savestate.html 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 242e232b08..62fa4ccd1f 100644 --- a/rust/data/presets/full-settings/Community Race Season 5.json +++ b/rust/data/presets/full-settings/Community Race Season 5.json @@ -4873,13 +4873,7 @@ "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 - }, + "savestate": "No", "random_seed": null, "disable_spikesuit": false, "disable_bluesuit": false, diff --git a/rust/data/presets/full-settings/Default.json b/rust/data/presets/full-settings/Default.json index 26b6e891cf..a0e8a0fc19 100644 --- a/rust/data/presets/full-settings/Default.json +++ b/rust/data/presets/full-settings/Default.json @@ -4788,13 +4788,7 @@ "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 - }, + "savestate": "No", "random_seed": null } } diff --git a/rust/data/presets/full-settings/Mentor Tournament.json b/rust/data/presets/full-settings/Mentor Tournament.json index 75148a26f0..770bd91243 100644 --- a/rust/data/presets/full-settings/Mentor Tournament.json +++ b/rust/data/presets/full-settings/Mentor Tournament.json @@ -4873,13 +4873,7 @@ "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 - }, + "savestate": "No", "random_seed": null, "disable_spikesuit": false, "disable_bluesuit": false, 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 b0552b1e94..3bc68fceea 100644 --- a/rust/data/presets/full-settings/Summer Series Expert Challenge.json +++ b/rust/data/presets/full-settings/Summer Series Expert Challenge.json @@ -4785,13 +4785,7 @@ "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 - }, + "savestate": "No", "random_seed": null, "disable_spikesuit": false, "disable_bluesuit": false, diff --git a/rust/maprando-web/src/randomize_helpers.rs b/rust/maprando-web/src/randomize_helpers.rs index ccd436a259..9bb9b25c23 100644 --- a/rust/maprando-web/src/randomize_helpers.rs +++ b/rust/maprando-web/src/randomize_helpers.rs @@ -12,7 +12,7 @@ use maprando::{ settings::{ AreaAssignmentBaseOrder, AreaAssignmentPreset, CrashFixesPreset, DisableETankSetting, DoorLocksSize, ETankRefill, FillerItemPriority, ItemCount, MapStationActivationPreset, - RandomizerSettings, SpeedBooster, WallJump, get_objective_groups, + RandomizerSettings, SaveState, SpeedBooster, WallJump, get_objective_groups, }, spoiler_log::SpoilerLog, spoiler_map, @@ -215,8 +215,10 @@ impl SeedHeaderTemplate<'_> { if other_settings.all_enemies_respawn { game_variations.push("All enemies respawn"); } - if other_settings.savestate.preset { - game_variations.push("Savestates enabled"); + match other_settings.savestate { + SaveState::No => {} + SaveState::Limited => game_variations.push("Limited save states"), + SaveState::Unlimited => game_variations.push("Unlimited save states"), } game_variations } diff --git a/rust/maprando-web/templates/generate/game_variations.html b/rust/maprando-web/templates/generate/game_variations.html index accab33a3a..776ce4134d 100644 --- a/rust/maprando-web/templates/generate/game_variations.html +++ b/rust/maprando-web/templates/generate/game_variations.html @@ -138,15 +138,13 @@ {% include "help/variations/savestate.html" %}
-
- +
+ - - - + + + +
@@ -160,4 +158,3 @@
{% 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 deleted file mode 100644 index 60b87432b9..0000000000 --- a/rust/maprando-web/templates/generate/help/savestate.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - diff --git a/rust/maprando-web/templates/generate/help/variations/savestate.html b/rust/maprando-web/templates/generate/help/variations/savestate.html index 4e8e3f1fce..ee4f152153 100644 --- a/rust/maprando-web/templates/generate/help/variations/savestate.html +++ b/rust/maprando-web/templates/generate/help/variations/savestate.html @@ -7,11 +7,13 @@ -
-
- Savestate details -
-
- {% include "savestate_details.html" %} -
-
-
Objective details diff --git a/rust/maprando/src/patch.rs b/rust/maprando/src/patch.rs index 0e4333de87..e7cc0c3824 100644 --- a/rust/maprando/src/patch.rs +++ b/rust/maprando/src/patch.rs @@ -21,7 +21,7 @@ use crate::{ AreaAssignmentPreset, CrashFixes, CrashFixesPreset, DisableETankSetting, ETankRefill, EnemyDrops, Fanfares, FixMode, ItemCount, MapPreset, MotherBrainFight, ObjPreset, Objective, ObjectiveScreen, ProgressionPreset, QolPreset, RandomizerSettings, SaveAnimals, - SkillPreset, SpeedBooster, StartLocationMode, WallJump, + SaveState, SkillPreset, SpeedBooster, StartLocationMode, WallJump, }, }; use anyhow::{Context, Result, bail, ensure}; @@ -657,30 +657,18 @@ impl Patcher<'_> { patches.push("everything_respawns"); } - if self.settings.other_settings.savestate.preset { + if self.settings.other_settings.savestate != SaveState::No { + let checkpoint_saves = if self.settings.other_settings.savestate == SaveState::Limited { + 1 + } else { + 0 + }; 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, - )?; + self.rom.write_u16(snes2pc(0x85C006), 0 as isize)?; + self.rom.write_u16(snes2pc(0x85C008), 0 as isize)?; + self.rom + .write_u16(snes2pc(0x85C00A), checkpoint_saves as isize)?; + self.rom.write_u16(snes2pc(0x85C00C), 0 as isize)?; } else { // null controller hooks self.rom.write_u8(snes2pc(0x85C000), 0x6B)?; // RTL diff --git a/rust/maprando/src/settings.rs b/rust/maprando/src/settings.rs index cc78bcf90c..fea42ac2fa 100644 --- a/rust/maprando/src/settings.rs +++ b/rust/maprando/src/settings.rs @@ -798,13 +798,17 @@ 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, Eq)] +pub enum SaveState { + No, + Limited, + Unlimited, +} + +impl std::fmt::Display for SaveState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:?}") + } } #[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq)] @@ -1495,17 +1499,8 @@ 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("savestate").is_none() { + other_settings.insert("savestate".to_string(), "No".into()); } if other_settings.get("disable_spikesuit").is_none() diff --git a/rust/maprando/tests/logic_scenarios.rs b/rust/maprando/tests/logic_scenarios.rs index 1259805d73..b834097048 100644 --- a/rust/maprando/tests/logic_scenarios.rs +++ b/rust/maprando/tests/logic_scenarios.rs @@ -349,13 +349,7 @@ 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, - }, + savestate: maprando::settings::SaveState::No, random_seed: None, }, debug: false, From 21d3fc5f0c220ddfa77a6c1a75ab4a2fe7d05c3a Mon Sep 17 00:00:00 2001 From: Brent Kerby Date: Fri, 24 Jul 2026 20:53:53 -0600 Subject: [PATCH 19/19] Simplify save state settings (ROM updates) --- patches/ips/new_game.ips | Bin 474 -> 466 bytes patches/ips/saveload.ips | Bin 1004 -> 1011 bytes patches/ips/savestate.ips | Bin 1073 -> 1018 bytes patches/src/constants.asm | 4 ++- patches/src/new_game.asm | 9 ++---- patches/src/saveload.asm | 9 ++++-- patches/src/savestate.asm | 58 +++++++++++-------------------------- rust/maprando/src/patch.rs | 30 +++++++++---------- 8 files changed, 42 insertions(+), 68 deletions(-) diff --git a/patches/ips/new_game.ips b/patches/ips/new_game.ips index 3eec2c0fb292568addb717ab6c450b89104b9fd4..5328994b329c41895e301663e99cb45ae05b4c1c 100644 GIT binary patch delta 64 zcmV-G0Kfm*1JVPKCm~Mtp#g2v00ts*^`YwO0dCp=0Ur?b00EC8IQ60M157iKNY4RO WlNbR|1dj-RaFcWaDFHZ>o&h{HB^Pc0 delta 72 zcmV-O0Js0r1KIH!6{OdXD3p$w?K1|ML)D$^n0OE2oGxP(gMZE{TSNrUkyqP0IK`E(pqXgHKT`2o`| a#+1pknKc+2CLdu|XOTPLV?3FW#Q*@>sYAK| delta 232 zcmeyxzLCQ?z%j(xgQ5Ng10x610R{#(ANPYkiw-dz;A0V9`Hz1Z09-Z3qJ6y=Q_~(f#*C=eZhsvNsOA(>w%(D{Xk+pP)MpD zNE|X)wD_R+YM-x@=QHkSOr9LiWMpr7u)4LowSRrpzoypd2_U5(B-V2PO=3IG1vZrj z$mTlF2Q-yMVcMZ=1-=816uPTDRxi4I$Y { patches.push("everything_respawns"); } - if self.settings.other_settings.savestate != SaveState::No { - let checkpoint_saves = if self.settings.other_settings.savestate == SaveState::Limited { - 1 - } else { - 0 - }; - patches.push("savestate"); - self.rom.write_u16(snes2pc(0x85C006), 0 as isize)?; - self.rom.write_u16(snes2pc(0x85C008), 0 as isize)?; - self.rom - .write_u16(snes2pc(0x85C00A), checkpoint_saves as isize)?; - self.rom.write_u16(snes2pc(0x85C00C), 0 as isize)?; - } else { - // null controller hooks - self.rom.write_u8(snes2pc(0x85C000), 0x6B)?; // RTL - self.rom.write_u8(snes2pc(0x85C003), 0x6B)?; // RTL + match self.settings.other_settings.savestate { + SaveState::No => { + // null controller hooks + self.rom.write_u8(snes2pc(0x85C000), 0x6B)?; // RTL + self.rom.write_u8(snes2pc(0x85C003), 0x6B)?; // RTL + } + SaveState::Limited => { + patches.push("savestate"); + self.rom.write_u16(snes2pc(0x85C006), 1)?; + } + SaveState::Unlimited => { + patches.push("savestate"); + self.rom.write_u16(snes2pc(0x85C006), 0)?; + } } if self.settings.other_settings.disable_spikesuit {