diff --git a/data/SNES_save.tsv b/data/SNES_save.tsv new file mode 100644 index 0000000..c304524 --- /dev/null +++ b/data/SNES_save.tsv @@ -0,0 +1,161 @@ +struct Character +u3 character_id # 0=Bartz, 1=Lenna, 2=Galuf, 3=Faris, 4=Krile, 5/6/7 unused +u1 is_female # 0=male, 1=female +u2 unk0 # Two unknown, possibly unused bits +u1 is_absent # 0=present, 1=absent +u1 is_back_row +u8 current_job_id +u8 level +u24 experience +u16 hp_current +u16 hp_max +u16 mp_current +u16 mp_max +u8 equipped_head +u8 equipped_body +u8 equipped_acc +u8 equipped_rh_shield +u8 equipped_lh_shield +u8 equipped_rh_weapon +u8 equipped_lh_weapon +u8 caught_monster +u8 ability_1 +u8 ability_2 +u8 ability_3 +u8 ability_4 +u8 status_1 +u8 status_2 +u8 status_3 +u8 status_4 +u8 action_flags +u8 damage_mod +u16 innates +u8 magic_element_up +u8 equip_weight +u8 base_strength +u8 base_agility +u8 base_stamina +u8 base_magic +u8 current_strength +u8 current_agility +u8 current_stamina +u8 current_magic +u8 evasion +u8 defense +u8 magic_evasion +u8 magic_defense +u8 elemental_absorb +u8 elemental_evade +u8 elemental_immune +u8 elemental_half +u8 elemental_weakness +u8 resistance_status_1 +u8 resistance_status_2 +u8 resistance_status_3 +u8 specialty_weapon +u8 specialty_equipment +u8 current_job_level +u16 current_job_abp +u8 spell_level_1 +u8 spell_level_2 +u8 spell_level_3 +u32 equipment_category +u16 attack +u8 attack_id_reaction_unused +u8 unk1 +u8 unk2 +u8 unk3 +u8 freelancer_strength +u8 freelancer_agility +u8 freelancer_stamina +u8 freelancer_magic +u16 freelancer_innates + +struct Job_progress +u12 abp +u4 level + +struct Config +u3 battle_speed # 0=1 in-game, ..., 5=6 in-game +u1 is_wait_mode # 0=active, 1=wait??? +u3 message_speed # 0=1 in-game, ..., 5=6 in-game +u1 command_set # 0=window, 1=shortcut +u5 menu_color_r +u5 menu_color_g +u5 menu_color_b +u1 padding +u1 reequip_mode # 0=optimum, 1=empty +u1 is_mono # 0=stereo, 1=mono +u1 is_memory_cursor # 0=reset, 1=memory +u4 unk0 +u1 hide_atb_gauge # 0=show, 1=hide +u6 unk1 +u1 is_controller_custom # 0=no, 1=yes +u1 is_controller_2p # 0=no, 1=yes +u8 button_A # Bit of action +u8 button_B # Bit of action +u8 button_X # Bit of action +u8 button_Y # Bit of action +u8 button_L # Bit of action +u8 button_R # Bit of action +u8 button_Select # Bit of action +4 of u8 character_player_nums # 0=controller 1, 1=controller 2 +4 of 4 of u8 character_shortcut_commands # 0=ability_1, 1=ability_2, 2=ability_3, 3=ability_4 + +struct Vehicle +u2 mode_switching +u3 movement_type +u3 map_id +u7 unk0 +u1 is_hidden # 0=show, 1=hide +u8 x +u8 y + +struct Save_slot +4 of Character characters +256 of u8 inventory_item_ids +256 of u8 inventory_item_qtys +u24 unlocked_jobs +4 of 22 of Job_progress character_jobs_progress +4 of u8 character_abilities_learned_count +4 of 20 of u8 character_abilities_learned +u24 current_gil +u32 game_time_frames +u16 num_enemies_defeated +32 of u8 magic_learned +Config config +5 of 6 of u8 character_names # Bartz, Lenna, Galuf, Faris, Krile. Dialog is hardcoded for everyone except Bartz's name anyway... +6 of u8 unk0 +u8 magic_lamp_next_summon +u8 num_battles_escaped # Brave Blade vs Chicken Knife +u8 wonder_rod_magic +9 of u8 unk1 +u16 num_total_battles +u16 num_times_saved +u8 last_battle_results # 0=victory, 1=game over, 2=escaped +15 of u8 flags_battle_events +32 of u8 flags_treasure_opened +32 of u8 unk_probably_still_flags_treasure_opened +96 of u8 flags_events # RAM map mentions $D8E000. This is likely critical to story progression and scripting. +96 of u8 unk_probably_still_flags_events +u16 map_id_inner +u16 map_id_world +u8 pos_x +u8 pos_y +u8 current_character_sprite +u8 current_character_facing +u8 current_vehicle +Vehicle veh_chocobo +Vehicle veh_black_chocobo +Vehicle veh_hiryuu +Vehicle veh_submarine +Vehicle veh_steamship +Vehicle veh_airship +u16 teleport_map_id +u8 teleport_map_x +u8 teleport_map_y +u8 initial_seed +u8 walking_speed # 0=normal, 1=double (fast), 80=half (slow) +u8 timed_event_active +u16 timed_event_timer +u16 timed_event_end diff --git a/data/SNES_save.txt b/data/SNES_save.txt deleted file mode 100644 index 04cce95..0000000 --- a/data/SNES_save.txt +++ /dev/null @@ -1,161 +0,0 @@ -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 -3 battle_speed # 0=1 in-game, ..., 5=6 in-game -1 is_wait_mode # 0=active, 1=wait??? -3 message_speed # 0=1 in-game, ..., 5=6 in-game -1 command_set # 0=window, 1=shortcut -5 menu_color_r -5 menu_color_g -5 menu_color_b -1 padding -1 reequip_mode # 0=optimum, 1=empty -1 is_mono # 0=stereo, 1=mono -1 is_memory_cursor # 0=reset, 1=memory -4 unk0 -1 hide_atb_gauge # 0=show, 1=hide -6 unk1 -1 is_controller_custom # 0=no, 1=yes -1 is_controller_2p # 0=no, 1=yes -8 button_A # Bit of action -8 button_B # Bit of action -8 button_X # Bit of action -8 button_Y # Bit of action -8 button_L # Bit of action -8 button_R # Bit of action -8 button_Select # Bit of action -8[4] character_player_nums # 0=controller 1, 1=controller 2 -8[4][4] character_shortcut_commands # 0=ability_1, 1=ability_2, 2=ability_3, 3=ability_4 - -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[32] magic_learned -Config config -8[5][6] character_names # Bartz, Lenna, Galuf, Faris, Krile. Dialog is hardcoded for everyone except Bartz's name anyway... -8[6] unk0 -8 magic_lamp_next_summon -8 num_battles_escaped # Brave Blade vs Chicken Knife -8 wonder_rod_magic -8[9] unk1 -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[32] unk_probably_still_flags_treasure_opened -8[96] flags_events # RAM map mentions $D8E000. This is likely critical to story progression and scripting. -8[96] unk_probably_still_flags_events -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/scripts/loaders/save_loader.gd b/scripts/loaders/save_loader.gd index 25d494e..95d6849 100644 --- a/scripts/loaders/save_loader.gd +++ b/scripts/loaders/save_loader.gd @@ -1,6 +1,7 @@ extends Node +const STRUCT := preload('res://scripts/struct.gd') const SLOT_IN_USE := 0xE41B -var schema := {} +var struct_types := STRUCT.get_base_structarraytypes() # FFV SRAM is 4 slots of 0x700byte save files # $0000-$06FF - Save Slot 1 @@ -38,148 +39,13 @@ func get_slot_checksum(slot: StreamPeerBuffer) -> int: checksum += 1 # TODO: confirm this carry flag edge case is correct! return checksum -func get_character(slot: StreamPeerBuffer, character_slot_id: int) -> Dictionary: - var character := {} - slot.seek(0x50 * character_slot_id) - 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_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. + # 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): + if not (struct_name in struct_types): 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 + return struct_types[struct_name].get_value(buffer, [0, 0]) func get_save_slot(sram: File, slot_id: int): var buffer := StreamPeerBuffer.new() @@ -201,37 +67,24 @@ func delete_save_slot(sram: File, slot_id: int): func _ready(): var file := File.new() - var error = file.open('res://data/SNES_save.txt', File.READ) + var error = file.open('res://data/SNES_save.tsv', File.READ) if error == OK: - var current_struct_name = null - var current_struct = [] - var line_num := 0 + var current_struct: STRUCT.Struct + var line_num := 0 # Currently only used for step-through debugging 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': + var type := line[0] + var label := line[1] + if type == '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: + current_struct = STRUCT.Struct.new() + struct_types[label] = current_struct + elif type and label: # 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 + current_struct.members.append([label, STRUCT.get_structarraytype(type, struct_types)]) file.close() diff --git a/scripts/struct.gd b/scripts/struct.gd new file mode 100644 index 0000000..3e84f60 --- /dev/null +++ b/scripts/struct.gd @@ -0,0 +1,189 @@ +#warning-ignore-all:shadowed_variable +#warning-ignore-all:unused_argument +# leftover_bits is array of form [count, value] +# array is used for reference semantics as get and put operations may mutate it +class StructType: + func get_value(buffer: StreamPeer, leftover_bits: Array): + return + func put_value(buffer: StreamPeer, value, leftover_bits: Array): + return + +class U8 extends StructType: + func get_value(buffer: StreamPeer, leftover_bits: Array): + return buffer.get_u8() + func put_value(buffer: StreamPeer, value, leftover_bits: Array): + buffer.put_u8(value) + +class S8 extends StructType: + func get_value(buffer: StreamPeer, leftover_bits: Array): + return buffer.get_8() + func put_value(buffer: StreamPeer, value, leftover_bits: Array): + buffer.put_8(value) + +class U16 extends StructType: + func get_value(buffer: StreamPeer, leftover_bits: Array): + return buffer.get_u16() + func put_value(buffer: StreamPeer, value, leftover_bits: Array): + buffer.put_u16(value) + +class S16 extends StructType: + func get_value(buffer: StreamPeer, leftover_bits: Array): + return buffer.get_16() + func put_value(buffer: StreamPeer, value, leftover_bits: Array): + buffer.put_16(value) + +class U24 extends StructType: + func get_value(buffer: StreamPeer, leftover_bits: Array): + return buffer.get_u16() | (buffer.get_u8() << 16) + func put_value(buffer: StreamPeer, value, leftover_bits: Array): + buffer.put_u16(value & 0xFFFF) + buffer.put_u8(value >> 16) + +class S24 extends StructType: + func get_value(buffer: StreamPeer, leftover_bits: Array): + var unsigned = buffer.get_u16() | (buffer.get_u8() << 16) + return unsigned - (2 * (unsigned & 0x800000)) + + func put_value(buffer: StreamPeer, value, leftover_bits: Array): + var unsigned = value % 0x1000000 + buffer.put_u16(unsigned & 0xFFFF) + buffer.put_u8(unsigned >> 16) + +class U32 extends StructType: + func get_value(buffer: StreamPeer, leftover_bits: Array): + return buffer.get_u32() + func put_value(buffer: StreamPeer, value, leftover_bits: Array): + buffer.put_u32(value) + +class S32 extends StructType: + func get_value(buffer: StreamPeer, leftover_bits: Array): + return buffer.get_32() + func put_value(buffer: StreamPeer, value, leftover_bits: Array): + buffer.put_32(value) + + +class UBits extends StructType: + var bits = 8 + + func _init(bits: int): + self.bits = bits + + func get_value(buffer: StreamPeer, leftover_bits: Array): + while leftover_bits[0] < bits: + leftover_bits[1] |= buffer.get_u8() << leftover_bits[0] + leftover_bits[0] += 8 + var value = leftover_bits[1] & ((1 << bits)-1) + leftover_bits[1] = leftover_bits[1] >> bits + leftover_bits[0] -= bits + return value + + func put_value(buffer: StreamPeer, value, leftover_bits: Array): + leftover_bits[1] |= value << bits + leftover_bits[0] += bits + while leftover_bits[0] >= 8: + buffer.put_8(leftover_bits[1] & 0xFF) + leftover_bits[0] -= 8 + leftover_bits[1] = leftover_bits[1] >> 8 + + +class Struct extends StructType: + var members := [] # Array of [name, StructType] + func get_value(buffer: StreamPeer, leftover_bits: Array): + var result = {} + for member in members: + var key: String = member[0] + var structType: StructType = member[1] + result[key] = structType.get_value(buffer, leftover_bits) + return result + + func put_value(buffer: StreamPeer, value, leftover_bits: Array): + for member in members: + var key: String = member[0] + var structType: StructType = member[1] + if not (key in value): + print_debug('Key "%s" missing from value supplied' % key) + return + structType.put_value(buffer, value[key], leftover_bits) + + +class StructArrayType extends StructType: + var count: int + var structType: StructType + + func _init(count, structType) -> void: + self.count = count + self.structType = structType + + func get_value(buffer: StreamPeer, leftover_bits: Array): + # Might be a bit too much branching but oh well + if self.structType is U8: + var result = PoolByteArray() + # Slight optimization over calling the method + for i in self.count: + result.append(buffer.get_u8()) + return result + + var result = [] + for i in self.count: + result.append(self.structType.get_value(buffer, leftover_bits)) + return result + + func put_value(buffer: StreamPeer, value, leftover_bits: Array): + if len(value) < self.count: + print_debug('Not enough values supplied') + return + for i in self.count: + self.structType.put_value(buffer, value[i], leftover_bits) + + +static func get_base_structarraytypes() -> Dictionary: + return { + 'u8': U8.new(), + 'u16': U16.new(), + 'u24': U24.new(), + 'u32': U32.new(), + 's8': S8.new(), + 's16': S16.new(), + 's24': S24.new(), + 's32': S32.new(), + } + + +static func get_structarraytype(type: String, existing_structs: Dictionary): + var tokens := type.split(' ', false) + var t: String = tokens[-1] + var inner_type + if t in existing_structs: + inner_type = existing_structs[t] + elif t[0] == 'u': + var b := int(t.substr(1)) + if b > 0: + inner_type = UBits.new(b) + existing_structs['u%d'%b] = inner_type # Cache it for future use + if not inner_type: + print_debug('typestring "%s" has no matches for "%s" in existing structs' % [type, t]) + return + + var l := len(tokens) + if l == 1: + return inner_type + # Our parsing goal is to turn 'a of b of c of d' into StructArrayType, b>, a> + # Our strategy is to parse backwards over the tokens, changing inner_type at each point + # a of b of c of (d) + # a of b of (c of d) + # a of (b of c of d) + # (a of b of c of d) + # done + var i := l-2 + while i > -1: + match tokens[i]: + 'of': + i -= 1 + var l1 = int(tokens[i]) + if l1 > 1: + inner_type = StructArrayType.new(l1, inner_type) # Might be worth caching these later on if we use them more + i -= 1 + var k: + print_debug('Invalid keyword used in type designator: "%s"' % k) + return + return inner_type