Working SNES save loading!
This commit is contained in:
parent
9df22ba5a1
commit
244058b22c
10
Node2D.gd
10
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')
|
||||
|
|
|
@ -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
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue