Added full SNES save file serialization

Ticked off SNES save support on the README.md
This commit is contained in:
Luke Hubmayer-Werner 2023-08-03 22:09:30 +09:30
parent 36d025e18c
commit a27736f917
3 changed files with 75 additions and 15 deletions

View File

@ -18,9 +18,7 @@ You can currently load up your SNES ROM of Final Fantasy FFV and look at battle
Following systems are ordered by vague overarching priority:
### Save/Load System
I know I called this an "Interactive Save Editor" but I haven't started that yet.
- [x] SNES loading
- [ ] SNES saving (should be easy enough, soon™)
- [x] SNES loading and saving
- [ ] PSX Support (wasn't identical to SNES with different offset ;_; )
- [ ] GBA Support (note, does not imply full asset ripping)
- [ ] Steam Pixel Remaster Support (I haven't bought this yet so low prio) (note, does not imply full asset ripping)

View File

@ -1,4 +1,5 @@
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()
@ -53,18 +54,57 @@ func put_struct(buffer: StreamPeer, struct_name: String, data: Dictionary):
return
struct_types[struct_name].put_value(buffer, data, [0, 0])
func deserialize_save_slot(bytes: PoolByteArray) -> Dictionary:
var buffer = StreamPeerBuffer.new()
buffer.data_array = bytes
func deserialize_save_slot(buffer: StreamPeerBuffer) -> Dictionary:
return struct_types['Save_slot'].get_value(buffer, [0, 0])
func serialize_save_slot(data: Dictionary) -> PoolByteArray:
var buffer = StreamPeerBuffer.new()
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:

View File

@ -2,6 +2,7 @@ extends Control
var dir_user := Directory.new()
const P_TESTDATA := 'user://test_data/'
const FILENAME_TEST_SAVE := 'res://test.srm'
# Shared state between tests
var save_slot_buffers = []
var save_slot_dicts = []
@ -12,7 +13,7 @@ func test(label, input):
else:
print('FAILURE: ' + label)
func load_snes_savefile(filename: String = 'res://test.srm'):
func load_snes_savefile(filename: String = FILENAME_TEST_SAVE):
var save_file := File.new()
match save_file.open(filename, File.READ):
OK:
@ -22,7 +23,7 @@ func load_snes_savefile(filename: String = 'res://test.srm'):
return
for i in 4:
self.save_slot_buffers.append(SaveLoader.get_save_slot(save_file, i))
self.save_slot_dicts.append(SaveLoader.deserialize_save_slot(self.save_slot_buffers[i].data_array))
self.save_slot_dicts.append(SaveLoader.deserialize_save_slot(self.save_slot_buffers[i]))
print('Loaded test save file')
func generate_known_good_results():
@ -61,23 +62,43 @@ func test_save_loading() -> bool:
print_debug('Failed to load known savefile results "%s"' % filename)
return false
func test_save_serialization() -> bool:
func test_save_slot_serialization() -> bool:
if not self.save_slot_dicts:
print_debug('test savefile not loaded')
return false
for i in 4:
var bytes = SaveLoader.serialize_save_slot(self.save_slot_dicts[i])
var bytes = SaveLoader.serialize_save_slot(self.save_slot_dicts[i]).data_array
if bytes != self.save_slot_buffers[i].data_array:
print_debug('Slot %d failed to serialize correctly, rescanning' % i)
for j in 0x700:
var b1: int = bytes[j]
var b2: int = self.save_slot_buffers[i].data_array[j]
if b1 != b2:
print_debug('Mismatch occurs at byte %d: %d vs %d' % [j, b1, b2])
print_debug('Mismatch occurs at byte $%04X (%d): $%02X (%d) vs $%02X (%d)' % [j, j, b1, b1, b2, b2])
return false
return true
func test_snes_save_serialization() -> bool:
if not self.save_slot_dicts:
print_debug('test savefile not loaded')
return false
var bytes := SaveLoader.make_snes_save_file(self.save_slot_dicts)
var file := File.new()
match file.open(FILENAME_TEST_SAVE, File.READ):
OK:
var orig_bytes := file.get_buffer(file.get_len())
if orig_bytes != bytes:
print_debug('SNES Save File failed to serialize correctly, rescanning')
for j in 0x2000:
var b1: int = bytes[j]
var b2: int = orig_bytes[j]
if b1 != b2:
print_debug('Mismatch occurs at byte $%04X (%d): $%02X (%d) vs $%02X (%d)' % [j, j, b1, b1, b2, b2])
return false
var error:
print_debug('Failed to open test.srm for reading: %d' % error)
return false
return true
# Called when the node enters the scene tree for the first time.
@ -89,5 +110,6 @@ func _ready() -> void:
print_debug('Failed to open user directory')
# generate_known_good_results() # Uncomment this to get your sample on first run
test('SNES save file loaded to array of dictionaries', test_save_loading())
test('SNES save file slots serialized from dictionaries', test_save_serialization())
test('SNES save file slots serialized from dictionaries', test_save_slot_serialization())
test('SNES save file serialized to original source bytes', test_snes_save_serialization())
get_tree().quit()