From a27736f91795a03fcae1bd2c62db3eca24a66743 Mon Sep 17 00:00:00 2001 From: Luke Hubmayer-Werner Date: Thu, 3 Aug 2023 22:09:30 +0930 Subject: [PATCH] Added full SNES save file serialization Ticked off SNES save support on the README.md --- README.md | 4 +-- scripts/loaders/save_loader.gd | 50 ++++++++++++++++++++++++++++++---- test/unit_tests.gd | 36 +++++++++++++++++++----- 3 files changed, 75 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index e9ee4ea..3b84e20 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/scripts/loaders/save_loader.gd b/scripts/loaders/save_loader.gd index bc25e7e..547c357 100644 --- a/scripts/loaders/save_loader.gd +++ b/scripts/loaders/save_loader.gd @@ -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: diff --git a/test/unit_tests.gd b/test/unit_tests.gd index 579398e..b405e83 100644 --- a/test/unit_tests.gd +++ b/test/unit_tests.gd @@ -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()