From 9df22ba5a1a24f2b791fccbc767b0a97e5500f52 Mon Sep 17 00:00:00 2001 From: Luke Hubmayer-Werner Date: Fri, 28 Jul 2023 23:11:26 +0930 Subject: [PATCH] Add some save spec --- scripts/loaders/save_loader.gd | 128 +++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 scripts/loaders/save_loader.gd diff --git a/scripts/loaders/save_loader.gd b/scripts/loaders/save_loader.gd new file mode 100644 index 0000000..b0225d3 --- /dev/null +++ b/scripts/loaders/save_loader.gd @@ -0,0 +1,128 @@ +extends Node +const SLOT_IN_USE := 0xE41B +# FFV SRAM is 4 slots of 0x700byte save files +# $0000-$06FF - Save Slot 1 +# $0700-$0DFF - Save Slot 2 +# $0E00-$14FF - Save Slot 3 +# $1500-$1BFF - Save Slot 4 +# $1C00-$1FEF - Empty +# $1FF0-$1FF7 - Checksum (2 bytes per slot) +# $1FF8-$1FFF - use table - contains $E41B if a slot is in use (deleting a save will just change this) +# Checksum just sums up every 16bit word from 0x000-0x600 of the save slot, using the carry flag. +# The carry flag means that it is not strictly modulo arithmetic, but you have to add 1 every time you pass 0xFFFF + +# Offsets within save slot +# $000:600 = $500:B00 in WRAM (0x7E0500:0x7E0B00) only values that don't match are things like game frame timer which increments immediately after load +# +# $000-$04F - Character Slot 1 +# $050=$09F - Character Slot 2 +# $0A0=$0EF - Character Slot 3 +# $0F0=$13F - Character Slot 4 +# $447-$449 - Current Gil +# +# $5D8 - Current Map X position +# $5D9 - Current Map Y position +# +# $600-$6FF - Appears to be unused and zeroed out, not used for checksum. Maybe we can use it for cool metadata? :) + +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() + if checksum > 0xFFFF: # Addition of shorts can only overflow + checksum &= 0xFFFF + if i < 0x5FF: + 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_8() + 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() + return character + +func get_save_slot(sram: File, slot_id: int): + var buffer := StreamPeerBuffer.new() + sram.seek(0x700 * slot_id) + buffer.set_data_array(sram.get_buffer(0x700)) + return buffer + +func save_slot(sram: File, slot_id: int, slot: StreamPeerBuffer): + sram.seek(0x700 * slot_id) + sram.store_buffer(slot.data_array) + sram.seek(0x1FF0 + (slot_id*2)) + sram.store_16(get_slot_checksum(slot)) + sram.seek(0x1FF8 + (slot_id*2)) + sram.store_16(SLOT_IN_USE) + +func delete_save_slot(sram: File, slot_id: int): + sram.seek(0x1FF8 + (slot_id*2)) + sram.store_16(0)