From 244058b22c89ab7b1acc032d207d4737504b9be4 Mon Sep 17 00:00:00 2001 From: Luke Hubmayer-Werner Date: Sat, 29 Jul 2023 01:53:03 +0930 Subject: [PATCH] Working SNES save loading! --- Node2D.gd | 10 ++ data/SNES_save.txt | 153 +++++++++++++++++++++ project.godot | 1 + scripts/loaders/save_loader.gd | 237 ++++++++++++++++++++++++--------- 4 files changed, 337 insertions(+), 64 deletions(-) create mode 100644 data/SNES_save.txt diff --git a/Node2D.gd b/Node2D.gd index f6cd73f..d47d135 100644 --- a/Node2D.gd +++ b/Node2D.gd @@ -1,4 +1,14 @@ extends Node2D +var save_slots = [] +var save_slot_dicts = [] + func _ready(): Engine.set_target_fps(60) + var save_file := File.new() + var error := save_file.open('test.srm', File.READ) + if error == OK: + for i in 4: + save_slots.append(SaveLoader.get_save_slot(save_file, i)) + save_slot_dicts.append(SaveLoader.get_struct(save_slots[i], 'Save_slot')) + print('Loaded test save file') diff --git a/data/SNES_save.txt b/data/SNES_save.txt new file mode 100644 index 0000000..2ec8902 --- /dev/null +++ b/data/SNES_save.txt @@ -0,0 +1,153 @@ +struct Character +3 character_id # 0=Bartz, 1=Lenna, 2=Galuf, 3=Faris, 4=Krile, 5/6/7 unused +1 is_female # 0=male, 1=female +2 unk0 # Two unknown, possibly unused bits +1 is_absent # 0=present, 1=absent +1 is_back_row +8 current_job_id +8 level +24 experience +16 hp_current +16 hp_max +16 mp_current +16 mp_max +8 equipped_head +8 equipped_body +8 equipped_acc +8 equipped_rh_shield +8 equipped_lh_shield +8 equipped_rh_weapon +8 equipped_lh_weapon +8 caught_monster +8 ability_1 +8 ability_2 +8 ability_3 +8 ability_4 +8 status_1 +8 status_2 +8 status_3 +8 status_4 +8 action_flags +8 damage_mod +16 innates +8 magic_element_up +8 equip_weight +8 base_strength +8 base_agility +8 base_stamina +8 base_magic +8 current_strength +8 current_agility +8 current_stamina +8 current_magic +8 evasion +8 defense +8 magic_evasion +8 magic_defense +8 elemental_absorb +8 elemental_evade +8 elemental_immune +8 elemental_half +8 elemental_weakness +8 resistance_status_1 +8 resistance_status_2 +8 resistance_status_3 +8 specialty_weapon +8 specialty_equipment +8 current_job_level +16 current_job_abp +8 spell_level_1 +8 spell_level_2 +8 spell_level_3 +32 equipment_category +16 attack +8 attack_id_reaction_unused +8 unk1 +8 unk2 +8 unk3 +8 freelancer_strength +8 freelancer_agility +8 freelancer_stamina +8 freelancer_magic +16 freelancer_innates + +struct Job_progress +12 abp +4 level + +struct Config +1 command_set +3 message_speed +1 is_wait_mode # 0=active, 1=wait??? +3 battle_speed # Confirm order on this byte?? +16 menu_color # RGB555 - really? not BGR? +1 reequip_mode # 0=optimum, 1=empty?? +1 is_stereo # 0=mono, 1=stereo?? +1 is_memory_cursor # 0=reset, 1=memory?? +4 unk0 +1 show_atb_gauge +6 unk1 +1 is_controller_custom +1 is_controller_2p +8 button_A +8 button_B +8 button_X +8 button_Y +8 button_L +8 button_R +8 button_Select +8[4] character_player_nums +8[4][4] character_shortcut_commands + +struct Vehicle +2 mode_switching +3 movement_type +3 map_id +7 unk0 +1 is_hidden # 0=show, 1=hide +8 x +8 y + +struct Save_slot +Character[4] characters +8[256] inventory_item_ids +8[256] inventory_item_qtys +24 unlocked_jobs +Job_progress[4][22] character_jobs_progress +8[4] character_abilities_learned_count +8[4][20] character_abilities_learned +24 current_gil +32 game_time_frames +16 num_enemies_defeated +8[20] magic_learned +8[4][6] character_names # Bartz, Lenna, Galuf, Faris, Krile. Dialog is hardcoded for everyone except Bartz's name anyway... +8 magic_lamp_next_summon +8 num_battles_escaped # Brave Blade vs Chicken Knife +8 wonder_rod_magic +16 num_total_battles +16 num_times_saved +8 last_battle_results # 0=victory, 1=game over, 2=escaped +8[15] flags_battle_events +8[32] flags_treasure_opened +8[96] flags_events # RAM map mentions $D8E000. This is likely critical to story progression and scripting. +16 map_id_inner +16 map_id_world +8 pos_x +8 pos_y +8 current_character_sprite +8 current_character_facing +8 current_vehicle +Vehicle veh_chocobo +Vehicle veh_black_chocobo +Vehicle veh_hiryuu +Vehicle veh_submarine +Vehicle veh_steamship +Vehicle veh_airship +16 teleport_map_id +8 teleport_map_x +8 teleport_map_y +8 initial_seed +8 walking_speed # 0=normal, 1=double (fast), 80=half (slow) +8 timed_event_active +16 timed_event_timer +16 timed_event_end diff --git a/project.godot b/project.godot index eced9ed..b82600f 100644 --- a/project.godot +++ b/project.godot @@ -27,6 +27,7 @@ SoundLoader="*res://scripts/loaders/sound_loader.gd" SpriteLoader="*res://scripts/loaders/sprite_loader.gd" MapLoader="*res://scripts/loaders/map_loader.gd" RomLoader="*res://scripts/loaders/rom_loader.gd" +SaveLoader="*res://scripts/loaders/save_loader.gd" [debug] diff --git a/scripts/loaders/save_loader.gd b/scripts/loaders/save_loader.gd index b0225d3..25d494e 100644 --- a/scripts/loaders/save_loader.gd +++ b/scripts/loaders/save_loader.gd @@ -1,5 +1,7 @@ extends Node const SLOT_IN_USE := 0xE41B +var schema := {} + # FFV SRAM is 4 slots of 0x700byte save files # $0000-$06FF - Save Slot 1 # $0700-$0DFF - Save Slot 2 @@ -29,7 +31,7 @@ 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_16() + checksum += slot.get_u16() if checksum > 0xFFFF: # Addition of shorts can only overflow checksum &= 0xFFFF if i < 0x5FF: @@ -39,76 +41,146 @@ func get_slot_checksum(slot: StreamPeerBuffer) -> int: func get_character(slot: StreamPeerBuffer, character_slot_id: int) -> Dictionary: var character := {} slot.seek(0x50 * character_slot_id) - var b := slot.get_8() + var b := slot.get_u8() character['id'] = b & 0x07 # 0Bartz/1Lenna/2Galuf/3Faris/4Krile/5/6/7 character['gender'] = (b >> 3) & 1 # 0 = male, 1 = female # Two unknown, possibly unused bits character['present'] = (b >> 6) & 1 # 0 = absent? character['back_row'] = (b >> 7) & 1 - character['job_id'] = slot.get_8() - character['level'] = slot.get_8() - character['experience'] = slot.get_16() | (slot.get_8() << 16) - character['hp_current'] = slot.get_16() - character['hp_max'] = slot.get_16() - character['mp_current'] = slot.get_16() - character['mp_max'] = slot.get_16() - character['equipped_head'] = slot.get_8() - character['equipped_body'] = slot.get_8() - character['equipped_acc'] = slot.get_8() - character['equipped_rh_shield'] = slot.get_8() - character['equipped_lh_shield'] = slot.get_8() - character['equipped_rh_weapon'] = slot.get_8() - character['equipped_lh_weapon'] = slot.get_8() - character['caught_monster'] = slot.get_8() - character['ability_1'] = slot.get_8() - character['ability_2'] = slot.get_8() - character['ability_3'] = slot.get_8() - character['ability_4'] = slot.get_8() - character['action_flags'] = slot.get_8() - character['damage_mod'] = slot.get_8() - character['innates'] = slot.get_16() - character['magic_element_up'] = slot.get_8() - character['equip_weight'] = slot.get_8() - character['base_strength'] = slot.get_8() - character['base_agility'] = slot.get_8() - character['base_stamina'] = slot.get_8() - character['base_magic'] = slot.get_8() - character['current_strength'] = slot.get_8() - character['current_agility'] = slot.get_8() - character['current_stamina'] = slot.get_8() - character['current_magic'] = slot.get_8() - character['evasion'] = slot.get_8() - character['defense'] = slot.get_8() - character['magic_evasion'] = slot.get_8() - character['magic_defense'] = slot.get_8() - character['elemental_absorb'] = slot.get_8() - character['elemental_evade'] = slot.get_8() - character['elemental_immune'] = slot.get_8() - character['elemental_half'] = slot.get_8() - character['elemental_weakness'] = slot.get_8() - character['resistance_status_1'] = slot.get_8() - character['resistance_status_2'] = slot.get_8() - character['resistance_status_3'] = slot.get_8() - character['specialty_weapon'] = slot.get_8() - character['specialty_equipment'] = slot.get_8() - character['current_job_level'] = slot.get_8() - character['current_job_abp'] = slot.get_16() - character['spell_level_1'] = slot.get_8() - character['spell_level_2'] = slot.get_8() - character['spell_level_3'] = slot.get_8() - character['equipment_category'] = slot.get_32() - character['attack'] = slot.get_16() - character['attack_id_reaction_unused'] = slot.get_8() - character['unk1'] = slot.get_8() - character['unk2'] = slot.get_8() - character['unk3'] = slot.get_8() - character['freelancer_strength'] = slot.get_8() - character['freelancer_agility'] = slot.get_8() - character['freelancer_stamina'] = slot.get_8() - character['freelancer_magic'] = slot.get_8() - character['freelancer_innates'] = slot.get_16() + character['job_id'] = slot.get_u8() + character['level'] = slot.get_u8() + character['experience'] = slot.get_u16() | (slot.get_u8() << 16) + character['hp_current'] = slot.get_u16() + character['hp_max'] = slot.get_u16() + character['mp_current'] = slot.get_u16() + character['mp_max'] = slot.get_u16() + character['equipped_head'] = slot.get_u8() + character['equipped_body'] = slot.get_u8() + character['equipped_acc'] = slot.get_u8() + character['equipped_rh_shield'] = slot.get_u8() + character['equipped_lh_shield'] = slot.get_u8() + character['equipped_rh_weapon'] = slot.get_u8() + character['equipped_lh_weapon'] = slot.get_u8() + character['caught_monster'] = slot.get_u8() + character['ability_1'] = slot.get_u8() + character['ability_2'] = slot.get_u8() + character['ability_3'] = slot.get_u8() + character['ability_4'] = slot.get_u8() + character['action_flags'] = slot.get_u8() + character['damage_mod'] = slot.get_u8() + character['innates'] = slot.get_u16() + character['magic_element_up'] = slot.get_u8() + character['equip_weight'] = slot.get_u8() + character['base_strength'] = slot.get_u8() + character['base_agility'] = slot.get_u8() + character['base_stamina'] = slot.get_u8() + character['base_magic'] = slot.get_u8() + character['current_strength'] = slot.get_u8() + character['current_agility'] = slot.get_u8() + character['current_stamina'] = slot.get_u8() + character['current_magic'] = slot.get_u8() + character['evasion'] = slot.get_u8() + character['defense'] = slot.get_u8() + character['magic_evasion'] = slot.get_u8() + character['magic_defense'] = slot.get_u8() + character['elemental_absorb'] = slot.get_u8() + character['elemental_evade'] = slot.get_u8() + character['elemental_immune'] = slot.get_u8() + character['elemental_half'] = slot.get_u8() + character['elemental_weakness'] = slot.get_u8() + character['resistance_status_1'] = slot.get_u8() + character['resistance_status_2'] = slot.get_u8() + character['resistance_status_3'] = slot.get_u8() + character['specialty_weapon'] = slot.get_u8() + character['specialty_equipment'] = slot.get_u8() + character['current_job_level'] = slot.get_u8() + character['current_job_abp'] = slot.get_u16() + character['spell_level_1'] = slot.get_u8() + character['spell_level_2'] = slot.get_u8() + character['spell_level_3'] = slot.get_u8() + character['equipment_category'] = slot.get_u32() + character['attack'] = slot.get_u16() + character['attack_id_reaction_unused'] = slot.get_u8() + character['unk1'] = slot.get_u8() + character['unk2'] = slot.get_u8() + character['unk3'] = slot.get_u8() + character['freelancer_strength'] = slot.get_u8() + character['freelancer_agility'] = slot.get_u8() + character['freelancer_stamina'] = slot.get_u8() + character['freelancer_magic'] = slot.get_u8() + character['freelancer_innates'] = slot.get_u16() return character +func get_struct(buffer: StreamPeer, struct_name: String) -> Dictionary: + # As this is recursive, it 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 schema): + print_debug('Attempted to get undeclared struct: "%s"' % struct_name) + return {} + var struct := {} + var leftover_bits := 0 + var leftover_bits_value := 0 + for line in schema[struct_name]: + var t: String = line[0] + var k: String = line[1] + match t: + '8': + struct[k] = buffer.get_u8() + '16': + struct[k] = buffer.get_u16() + '24': + struct[k] = buffer.get_u16() | (buffer.get_u8() << 16) + '32': + struct[k] = buffer.get_u32() + _: + var arr_split = t.split('[') + match arr_split.size(): + 1: + # Check if it's an odd number of bits + var b := int(t) + if b > 0: + # Check if we have leftover bits to fill this order + while leftover_bits < b: + leftover_bits_value |= buffer.get_u8() << leftover_bits + leftover_bits += 8 + struct[k] = leftover_bits_value & ((1 << b)-1) + leftover_bits_value = leftover_bits_value >> b + leftover_bits -= b + else: + # It's a struct + struct[k] = get_struct(buffer, t) + 2: + var n0 := int(arr_split[1].trim_suffix(']')) + var arr = [] + if arr_split[0] == '8': + arr = PoolByteArray() + for i in n0: + arr.append(buffer.get_u8()) + else: + for i in n0: + arr.append(get_struct(buffer, arr_split[0])) + struct[k] = arr + 3: + var n0 := int(arr_split[1].trim_suffix(']')) + var n1 := int(arr_split[2].trim_suffix(']')) + var arr0 := [] + if arr_split[0] == '8': + for i in n0: + var arr1 := PoolByteArray() + for j in n1: + arr1.append(buffer.get_u8()) + arr0.append(arr1) + else: + for i in n0: + var arr1 := [] + for j in n1: + arr1.append(get_struct(buffer, arr_split[0])) + arr0.append(arr1) + struct[k] = arr0 + var s: + print_debug('struct value array dims of %d are not yet supported (struct "%s")' % [s, struct_name]) + return struct + func get_save_slot(sram: File, slot_id: int): var buffer := StreamPeerBuffer.new() sram.seek(0x700 * slot_id) @@ -126,3 +198,40 @@ func save_slot(sram: File, slot_id: int, slot: StreamPeerBuffer): func delete_save_slot(sram: File, slot_id: int): sram.seek(0x1FF8 + (slot_id*2)) sram.store_16(0) + +func _ready(): + var file := File.new() + var error = file.open('res://data/SNES_save.txt', File.READ) + if error == OK: + var current_struct_name = null + var current_struct = [] + var line_num := 0 + while file.get_position() < file.get_len(): + var line := file.get_csv_line('\t') + line_num += 1 + var size = line.size() + if size < 2: + if current_struct_name: + # Store struct we just finished declaring + schema[current_struct_name] = current_struct + current_struct = [] + current_struct_name = null + continue + # if size < 2: + # print_debug('Malformed schema file: line %d - size %d - "%s"' % [line_num, line.size(), line]) + # continue + # Size is at least 2 + if line[0] == 'struct': + # New struct declaration + if current_struct_name: + # Store one we just finished declaring + schema[current_struct_name] = current_struct + current_struct = [] + current_struct_name = line[1] + else: + # TODO: Maybe store the trailing comments somewhere? + current_struct.append(line) + # Make sure we saved the final struct + if current_struct_name: + schema[current_struct_name] = current_struct + file.close()