2023-07-28 23:11:26 +09:30
extends Node
2023-08-03 22:09:30 +09:30
#warning-ignore-all:return_value_discarded
2023-08-03 19:32:52 +09:30
const STRUCT : = preload ( ' res://scripts/struct.gd ' )
2023-07-28 23:11:26 +09:30
const SLOT_IN_USE : = 0xE41B
2023-08-03 19:32:52 +09:30
var struct_types : = STRUCT . get_base_structarraytypes ( )
2023-07-29 01:53:03 +09:30
2023-07-28 23:11:26 +09:30
# 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
2023-07-29 01:53:03 +09:30
checksum += slot . get_u16 ( )
2023-07-28 23:11:26 +09:30
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
2023-07-29 01:53:03 +09:30
func get_struct ( buffer : StreamPeer , struct_name : String ) - > Dictionary :
2023-08-03 19:32:52 +09:30
# Does not seek to the start of the buffer.
2023-07-29 01:53:03 +09:30
# Make sure it is set to where you want to start reading.
2023-08-03 19:32:52 +09:30
if not ( struct_name in struct_types ) :
2023-07-29 01:53:03 +09:30
print_debug ( ' Attempted to get undeclared struct: " %s " ' % struct_name )
return { }
2023-08-03 19:32:52 +09:30
return struct_types [ struct_name ] . get_value ( buffer , [ 0 , 0 ] )
2023-07-29 01:53:03 +09:30
2023-08-03 21:02:41 +09:30
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 ] )
2023-08-03 22:09:30 +09:30
func deserialize_save_slot ( buffer : StreamPeerBuffer ) - > Dictionary :
2023-08-03 21:02:41 +09:30
return struct_types [ ' Save_slot ' ] . get_value ( buffer , [ 0 , 0 ] )
2023-08-04 18:17:08 +09:30
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 ' )
return data
2023-08-03 22:09:30 +09:30
func serialize_save_slot ( data : Dictionary ) - > StreamPeerBuffer :
var buffer : = StreamPeerBuffer . new ( )
2023-08-03 21:02:41 +09:30
struct_types [ ' Save_slot ' ] . put_value ( buffer , data , [ 0 , 0 ] )
var padding : = PoolByteArray ( )
padding . resize ( 0x100 )
padding . fill ( 0 )
buffer . put_data ( padding )
2023-08-03 22:09:30 +09:30
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 )
2023-08-03 21:02:41 +09:30
return buffer . data_array
func get_save_slot ( sram : File , slot_id : int ) - > StreamPeerBuffer :
2023-07-28 23:11:26 +09:30
var buffer : = StreamPeerBuffer . new ( )
sram . seek ( 0x700 * slot_id )
buffer . set_data_array ( sram . get_buffer ( 0x700 ) )
return buffer
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 )
2023-07-29 01:53:03 +09:30
func _ready ( ) :
2023-08-04 14:18:17 +09:30
STRUCT . parse_struct_definitions_from_tsv_filename ( ' res://data/SNES_save.tsv ' , struct_types )