ChocolateBird/scripts/loaders/SaveLoader.gd

171 lines
5.9 KiB
GDScript3
Raw Permalink Normal View History

2023-07-28 23:11:26 +09:30
extends Node
#warning-ignore-all:return_value_discarded
const STRUCT := preload('res://scripts/struct.gd')
2023-07-28 23:11:26 +09:30
const SLOT_IN_USE := 0xE41B
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:
# 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.
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 {}
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])
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])
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()
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)
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 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
2023-07-28 23:11:26 +09:30
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():
2024-06-26 20:43:37 +09:30
STRUCT.parse_struct_definitions_from_tsv_filename('res://data/5/structs/SNES_save.tsv', struct_types)