extends Node #warning-ignore-all:return_value_discarded const STRUCT := preload('res://scripts/struct.gd') const SLOT_IN_USE := 0xE41B var struct_types := STRUCT.get_base_structarraytypes() # FFV SRAM is 4 slots of 0x700byte save files # $0000-$06FF - Save Slot 1 # $0700-$0DFF - Save Slot 2 # $0E00-$14FF - Save Slot 3 # $1500-$1BFF - Save Slot 4 # $1C00-$1FEF - Empty # $1FF0-$1FF7 - Checksum (2 bytes per slot) # $1FF8-$1FFF - use table - contains $E41B if a slot is in use (deleting a save will just change this) # Checksum just sums up every 16bit word from 0x000-0x600 of the save slot, using the carry flag. # The carry flag means that it is not strictly modulo arithmetic, but you have to add 1 every time you pass 0xFFFF # Offsets within save slot # $000:600 = $500:B00 in WRAM (0x7E0500:0x7E0B00) only values that don't match are things like game frame timer which increments immediately after load # # $000-$04F - Character Slot 1 # $050=$09F - Character Slot 2 # $0A0=$0EF - Character Slot 3 # $0F0=$13F - Character Slot 4 # $447-$449 - Current Gil # # $5D8 - Current Map X position # $5D9 - Current Map Y position # # $600-$6FF - Appears to be unused and zeroed out, not used for checksum. Maybe we can use it for cool metadata? :) func get_slot_checksum(slot: StreamPeerBuffer) -> int: slot.seek(0) var checksum := 0 for i in 0x600: # Last 0x100 bytes aren't checksummed checksum += slot.get_u16() if checksum > 0xFFFF: # Addition of shorts can only overflow checksum &= 0xFFFF if i < 0x5FF: checksum += 1 # TODO: confirm this carry flag edge case is correct! return checksum func get_struct(buffer: StreamPeer, struct_name: String) -> Dictionary: # Does not seek to the start of the buffer. # Make sure it is set to where you want to start reading. if not (struct_name in struct_types): print_debug('Attempted to get undeclared struct: "%s"' % struct_name) return {} return struct_types[struct_name].get_value(buffer, [0, 0]) func put_struct(buffer: StreamPeer, struct_name: String, data: Dictionary): if not (struct_name in struct_types): print_debug('Attempted to put undeclared struct: "%s"' % struct_name) return struct_types[struct_name].put_value(buffer, data, [0, 0]) func deserialize_save_slot(buffer: StreamPeerBuffer) -> Dictionary: return struct_types['Save_slot'].get_value(buffer, [0, 0]) func load_save_slot(buffer: StreamPeerBuffer) -> Dictionary: # Like deserialize_save_slot, but also decodes strings and maybe other postprocessing later var data = deserialize_save_slot(buffer) data.character_names_decoded = StringLoader.decode_array(data.character_names, 'RPGe_small') for c in data.characters: c.equipped_abilities = [c.ability_1, c.ability_2, c.ability_3, c.ability_4] return data func serialize_save_slot(data: Dictionary) -> StreamPeerBuffer: var buffer := StreamPeerBuffer.new() struct_types['Save_slot'].put_value(buffer, data, [0, 0]) var padding := PoolByteArray() padding.resize(0x100) padding.fill(0) buffer.put_data(padding) return buffer func make_snes_save_file(slot_dicts: Array) -> PoolByteArray: # Pass a length 4 array of dictionaries. # Falsey entries will be zeroed out slots, with the active flag at the end also zeroed. assert(len(slot_dicts) == 4) var buffer := StreamPeerBuffer.new() var zeroes := PoolByteArray() zeroes.resize(0x700) zeroes.fill(0) var checksums := [] for dict in slot_dicts: if dict: var slot := serialize_save_slot(dict) var checksum := get_slot_checksum(slot) buffer.put_data(slot.data_array) if slot.data_array == zeroes: checksums.append(-1) else: checksums.append(checksum) else: buffer.put_data(zeroes) checksums.append(-1) # Pad from $1C00 == 7168 to $1FF0 == 8160 buffer.put_data(zeroes.subarray(0, 991)) # BEWARE: SUBARRAY IS INCLUSIVE # Mystery byte TODO: INVESTIGATE LATER buffer.put_8(1) # Pad from $1FE1 == 8161 to $1FF0 == 8176 buffer.put_data(zeroes.subarray(0, 14)) # BEWARE: SUBARRAY IS INCLUSIVE # Checksums for c in checksums: if c > -1: buffer.put_u16(c) else: buffer.put_u16(0) # Active flag for c in checksums: if c > -1: buffer.put_u16(SLOT_IN_USE) else: buffer.put_u16(0) return buffer.data_array func get_save_slot(sram: File, slot_id: int) -> StreamPeerBuffer: var buffer := StreamPeerBuffer.new() sram.seek(0x700 * slot_id) buffer.set_data_array(sram.get_buffer(0x700)) return buffer func load_save_dicts_from_buffer(sram: StreamPeerBuffer) -> Array: # Pulls four lots of 0x700 bytes from the current buffer position var dicts := [] var slot_buffer := StreamPeerBuffer.new() for slot_id in 4: slot_buffer.set_data_array(sram.get_data(0x700)[1]) slot_buffer.seek(0) dicts.append(load_save_slot(slot_buffer)) # Won't seek just in case sram.get_data(0x1FF8-0x1C00) for slot_id in 4: dicts[slot_id].slot_in_use = (sram.get_u16() == SLOT_IN_USE) return dicts func load_save_dicts_from_bytes(sram: PoolByteArray) -> Array: var buffer := StreamPeerBuffer.new() buffer.set_data_array(sram) return load_save_dicts_from_buffer(buffer) func load_save_dicts_from_file(sram: File) -> Array: # Pulls four lots of 0x700 bytes from the current buffer position var dicts := [] var slot_buffer := StreamPeerBuffer.new() for slot_id in 4: slot_buffer.set_data_array(sram.get_buffer(0x700)) slot_buffer.seek(0) dicts.append(load_save_slot(slot_buffer)) # Won't seek just in case sram.get_buffer(0x1FF8-0x1C00) for slot_id in 4: dicts[slot_id].slot_in_use = (sram.get_16() == SLOT_IN_USE) return dicts func save_slot(sram: File, slot_id: int, slot: StreamPeerBuffer): sram.seek(0x700 * slot_id) sram.store_buffer(slot.data_array) sram.seek(0x1FF0 + (slot_id*2)) sram.store_16(get_slot_checksum(slot)) sram.seek(0x1FF8 + (slot_id*2)) sram.store_16(SLOT_IN_USE) func delete_save_slot(sram: File, slot_id: int): sram.seek(0x1FF8 + (slot_id*2)) sram.store_16(0) func _ready(): STRUCT.parse_struct_definitions_from_tsv_filename('res://data/5/structs/SNES_save.tsv', struct_types)