11 March 2016 - Forum Rules

Main Menu

How would you handle SRAM?

Started by justin3009, July 23, 2013, 02:02:52 PM

Previous topic - Next topic


I'm starting to re-work the Password screen once more on Mega Man X3 to be an SRAM screen.  The original one was WAY beyond buggy and horribly coded so I want to get an idea on how one would handle this.

1. How would you handle save slots?  Would you have a value in RAM to dictate a number you're on, say, 1-4 and then it loads up the data depending on that byte?


Slot 1: Offset 100
Slot 2: Offset 200
Slot 3: Offset 300


2. What exactly does someone do for an SRAM check?  I had it checking if the SRAM main area was '60 60' which is what the blank values were.  Though this caused many, MANY issues later apparently.
'We have to find some way to incorporate the general civilians in the plot.'

'We'll kill off children in the Juuban district with an infection where they cough up blood and are found hanging themselves from cherry blossom trees.'


Yes. I'm assuming you have a menu to allow the use to select a save slot, so then you use that as an offset to the save data for that slot.

For an SRAM check, you have some sort of checksum routine. Most often, they just add up the bytes together, plus add some arbitrary key value. Record the result when writing save data.
When loading data, calculate the checksum again, and compare it to the previously calculated result. If they don't match, then it is an error. Most games will usually just assume the slot it empty and clear it.
"My watch says 30 chickens" Google, 2018


When I added saving to Metroid, each save file was 32 bytes, so I could access the save files with an indexed addressing mode. (I put the offset of each file in a table, e.g. SaveFileOffsets: .db $00, $20, $40).

I used two checksum bytes. One was all bytes added together, and the other was all bytes xored together. I'm not sure that's the "best" way to do it, but with only a one-byte sum, with randomly initialized data, you would have a 1 in 256 chance of invalid data being interpreted as a "good" save file.


What was the trouble with a 2 byte sum...?

LDA #0


Considering how the 6502 is made, it's probably easier to hadle this the other way arround, for every variable you want to store, you duplicate it N times for N save slows (in other words variables becomes arrays).

Like this (dummy exemple with 3 slots) :
HP : Offset $0 (save #0), $1 (save #1), $2 (save #2)
Level : Offset $3 (save #0), $4 (save #1), $5 (save #2)
etc, etc....

This way you can keep the slot numbers in X or Y reg and keep loading/saving data from the "arrays" at the same offset.


With SRAM, you're usually copying a block from RAM into SRAM or vice-versa so keeping it together for one save slot allows you to do a fast/simple copy.

Unless you want to continuously update the SRAM as you play but this seems like begging for trouble...


Oh no, the way I did it was use MVN considering 95% of the PC/Level/Event data needed is literally right next to each other, so it's a REALLY quick movement into SRAM.  It's just a matter of actually getting a chksum setup and everything else to go with it.
'We have to find some way to incorporate the general civilians in the plot.'

'We'll kill off children in the Juuban district with an infection where they cough up blood and are found hanging themselves from cherry blossom trees.'


Quote from: Disnesquick on July 24, 2013, 10:22:53 AM
What was the trouble with a 2 byte sum...?

Who said it would be any trouble? I'm sure that's an adequate approach, and easy enough, too.

Quote from: Bregalad on July 24, 2013, 10:26:27 AM
Considering how the 6502 is made, it's probably easier to hadle this the other way arround

It's not very difficult at all either way. It's a matter of circumstances and preference. But if loading and saving a file is basically a matter of copying a block of memory between the save files and working RAM, keeping each save file contiguous seems simpler.


In Castlevania II, I made each save block 256 bytes long, so to generate a pointer to save number X you would use X+$60 in the high byte of the offset and $00 in the low byte of the offset.
For the checksum I used CRC16 with small tweaks.

The save-blocks do not really fully populate the 256 bytes though; this fact is used for other purposes.

.macro InitChecksum
        lda #$FF   
        sta Checksum+0
        sta Checksum+1
.macro UpdateChecksum
        ; CRC16 by Greg Cook.
        EOR Checksum+1  ; A contained the data
        STA Checksum+1  ; XOR it into high byte
        LSR             ; right shift A 4 bits
        LSR             ; to make top of x^12 term
        LSR             ; ($1...)
        TAX             ; save it
        ASL             ; then make top of x^5 term
        EOR Checksum+0  ; and XOR that with low byte
        STA Checksum+0  ; and save
        TXA             ; restore partial term
        EOR Checksum+1  ; and update high byte
        STA Checksum+1  ; and save
        ASL             ; left shift three
        ASL             ; the rest of the terms
        ASL             ; have feedback from x^12
        TAX             ; save bottom of x^12
        ASL             ; left shift two more
        ASL             ; watch the carry flag
        EOR Checksum+1  ; bottom of x^5 ($..2.)
        STA Checksum+1  ; save high byte
        TXA             ; fetch temp value
        ROL             ; bottom of x^12, middle of x^5!
        EOR Checksum+0  ; finally update low byte
        LDX Checksum+1  ; then swap high and low bytes
        STA Checksum+1
        STX Checksum+0
.macro QuickPermutateChecksum memword
        ; Checksum += int16
        lda memword+0
        adc Checksum+0
        sta Checksum+0
        lda memword+1
        adc Checksum+1
        sta Checksum+1

The save-data itself consists of a stream of memory-address + data commands. This allows for both backward and future compatibility between saves and patch versions, as long as variables are never relocated to another memory address (which is almost always the case when you're hacking a cooked game, i.e. a compiled & released game).
; Save data format:
;   16 bytes password data
;   Followed by checksum (16-bit)
;      If 0, record ends here
;   Followed by records:
;      nnnnnaaa aaaaaaaa ...
;       n = number of bytes-1 (0-31, coding for 1-32 bytes)
;       a = address to save ($000-$7FF)
;      word $0000 = end of records
;   For up to 256 bytes.

The reason for the odd order of checksum + records + terminator is backward compatibility with Matrixz's patch that only saved the 16-byte password data and nothing else.
Certain security checks ensure that even a carefully crafted malicious savestate will not cause too much mayhem, such as by writing into the stack.

To report on screen what a save contains, the save is simply extracted and the running game state is then inspected. To ensure that the running game state is not permanently damaged when the save is inspected, the current game state is first "saved" into a backup RAM location (rather than SRAM) when the save/load screen is entered. The backup is restored right before when "save" is selected, and when exiting the screen without using "load".

For checking the amount of installed SRAM, I used a sequence of reads & writes of four consecutive bytes at $60FF, $63FF, $67FF, $6FFF and $7FFF if I recall correctly; writing a sequence of random bytes and verifying whether they read back correctly and that they are not mirrored in a previously checked location. I probably also restored the bytes to their original values after testing them.
I'm aware that there would usually be either none or the full 8 kilobytes, but I wrote the checking of installed amount with homebrew cartridges in mind where you might make a working cart even if you only had a 2-kilobyte SRAM chip at your disposal, as I do.