238 lines
8.3 KiB
GDScript
238 lines
8.3 KiB
GDScript
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
|
|
# $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_u16()
|
|
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_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.
|
|
# 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)
|
|
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)
|
|
|
|
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()
|