Refactor a bunch of stuff

This commit is contained in:
Luke Hubmayer-Werner 2023-07-22 01:51:52 +09:30
parent e87942c731
commit 1533e16c4f
17 changed files with 2409 additions and 587 deletions

View File

@ -23,6 +23,7 @@ from itertools import chain
from array import array
import time
import functools
from typing import Iterable
from includes.helpers import *
from includes.qthelpers import *
@ -34,12 +35,13 @@ from includes.snestile import (
Canvas, Canvas_Indexed
)
from includes.snes import *
import includes.const as const
import includes.ff5.const as const
from includes.ff5.strings import StringBlock, RPGe_Dialogue_Width
from includes.ff5.strings import Strings as FFVStrings
import includes.ff4 as ff4
import includes.ff5 as ff5
import includes.ff6 as ff6
filename_en = 'Final Fantasy V (Japan) [En by RPGe v1.1].sfc'
filename_jp = 'Final Fantasy V (Japan).sfc'
filename_jp_ff4 = 'Final Fantasy IV (Japan) (Rev A).sfc'
filename_jp_ff6 = 'Final Fantasy VI (Japan).sfc'
@ -53,17 +55,11 @@ class FF5Reader(QMainWindow):
QMainWindow.__init__(self, None)
perfcount()
print('Reading ROMs')
with open(filename_en, 'rb') as file:
ROM_en = file.read()
print(len(ROM_en), filename_en)
with open(filename_jp, 'rb') as file:
ROM_jp = file.read()
print(len(ROM_jp), filename_jp)
with open(filename_jp_ff4, 'rb') as file:
ROM_FF4jp = file.read()
ROM_en = ff5.files.ROM_RPGe
ROM_jp = ff5.files.ROM_SNES
ROM_FF4jp = load_raw(filename_jp_ff4)
ROM_FF6jp = load_raw(filename_jp_ff6)
print(len(ROM_FF4jp), filename_jp_ff4)
with open(filename_jp_ff6, 'rb') as file:
ROM_FF6jp = file.read()
print(len(ROM_FF6jp), filename_jp_ff6)
perfcount()
@ -75,61 +71,14 @@ class FF5Reader(QMainWindow):
'glyphs_jp_l': generate_glyphs_large(ROM_jp, 0x03E800),
'glyphs_kanji': generate_glyphs_large(ROM_jp, 0x1BD000, 0x1AA), # Kanji are unchanged in EN version
}
make_string_img_list = functools.partial(_make_string_img_list, ROM_en, ROM_jp, **self.glyph_sprites)
make_string_img_list = functools.partial(_make_string_img_list, **self.glyph_sprites)
perfcount()
stringlist_headers = ['Address', 'ID', 'Name']
imglist_headers = stringlist_headers + ['Img', 'Name JP', 'Img JP']
imglist_headers = ['ID', 'EN Pointer', 'EN Address', 'EN String', 'EN Img', 'JP Pointer', 'JP Address', 'JP String', 'JP Img']
print('Generating Strings')
zone_names = make_string_img_list(0x107000, 2, 0x100, start_str=0x270000, start_jp_str=0x107200, indirect=True, large=True)
menu_strings = make_string_img_list(0xF987, 2, 139, start_str=0x270000, start_jp_str=0x0000, indirect=True)
items = make_string_img_list(0x111380, 9, 256)
magics = make_string_img_list(0x111C80, 6, 87)
more_magics = make_string_img_list(0x111E8A, 9, 73)
enemy_names = make_string_img_list(0x200050, 10, 0x180, 0x105C00, 8)
character_names = make_string_img_list(0x115500, 6, 5)
job_names = make_string_img_list(0x115600, 8, 22)
ability_names = make_string_img_list(0x116200, 8, 33)
battle_commands = make_string_img_list(0x201150, 7, 0x60, 0x115800, 5)
print('Generating String Images')
string_images = {k: make_string_img_list(*FFVStrings.blocks_SNES_RPGe[k], large=config.get('dialog')) for k,config in FFVStrings.config.items()}
perfcount()
dialogue = make_string_img_list(0x2013F0, 3, 0x900, start_jp=0x082220, len_jp=2, start_str=0x0, start_jp_str=0x0A0000, indirect=True, large=True, macros_en=const.Dialogue_Macros_EN, macros_jp=const.Dialogue_Macros_JP)
perfcount()
def split_tilesets(data):
tilesets = [(data & 0x00003F),
(data & 0x000FC0) >> 6,
(data & 0x03F000) >> 12,
(data & 0xFC0000) >> 18]
return ' '.join([hex(i,2) for i in tilesets])
def split_blockmaps(data):
blockmaps = [(data & 0x000003FF) - 1,
((data & 0x000FFC00) >> 10) - 1,
((data & 0x3FF00000) >> 20) - 1]
return ' '.join([hex(i,3) for i in blockmaps])
zone_structure = [('NPC Layer', 2, None), # 00 01
('Name', 1, [z[2] for z in zone_names]), # 02
('ShadowFlags', 1, None), # 03
('Graphic maths', 1, None), # 04 - MSb animation-related, 6 LSbs are ID for table in 0x005BB8 which writes to $2131-$2133 (Color Math Designation, Subscreen BG color)
('Tile properties', 1, None), # 05
('Flags '+hex(6), 1, None), # 06
(hex(7), 1, None), # 07
('Blockset', 1, None), # 08
('Tilesets', 3, split_tilesets), # 09 0A 0B
('Blockmaps', 4, split_blockmaps), # 0C 0D 0E 0F
(hex(16), 1, None), # 10
(hex(17), 1, None), # 11
(hex(18), 1, None), # 12
(hex(19), 1, None), # 13
(hex(20), 1, None), # 14
(hex(21), 1, None), # 15
('Palette', 1, None), # 16
(hex(23), 1, None), # 17
(hex(24), 1, None), # 18
('Music', 1, const.BGM_Tracks)] # 19
zone_headers = ['Address'] + [z[0] for z in zone_structure]
zone_data = [parse_struct(ROM_en, 0x0E9C00 + (i*0x1A), zone_structure) for i in range(const.zone_count)]
battle_bg_structure = [('Tileset', 1, None),
('Palette 1', 1, None),
@ -170,7 +119,7 @@ class FF5Reader(QMainWindow):
enemy_sprite_headers = ['Address']+[i[0] for i in enemy_sprite_structure]+['EN Name','EN Name']
address = 0x14B180
for i in range(0x180):
enemy_sprite_data.append(parse_struct(ROM_en, address + (i*5), enemy_sprite_structure) + enemy_names[i][2:4])
enemy_sprite_data.append(parse_struct(ROM_en, address + (i*5), enemy_sprite_structure) + string_images['enemy_names'][i][3:5])
perfcount()
print('Generating map tiles')
@ -286,23 +235,22 @@ class FF5Reader(QMainWindow):
self.ff6widget.addTab(make_px_table(self.portraits_ff6, cols=19, scale=2), 'Character Portraits')
structs_tab.addTab(make_table(zone_headers, zone_data, True), 'Zones')
structs_tab.addTab(make_table(ff5.ZoneData.zone_headers, ff5.ZoneData.get_data(), True), 'Zones')
structs_tab.addTab(make_table(tileset_headers, tileset_data, True), 'Tilesets')
structs_tab.addTab(make_table(battle_bg_headers, battle_bg_data, True), 'BattleBGs')
structs_tab.addTab(make_table(const.npc_layer_headers, npc_layers, True), 'NPC Layers')
structs_tab.addTab(make_table(enemy_sprite_headers, enemy_sprite_data, True), 'Enemy Sprites')
strings_tab.addTab(make_table(imglist_headers, menu_strings, row_labels=False), 'Menu Strings')
strings_tab.addTab(make_table(imglist_headers, items, row_labels=False), 'Items')
strings_tab.addTab(make_table(imglist_headers, magics+more_magics, row_labels=False), 'Magics')
#strings_tab.addTab(make_table(imglist_headers, more_magics, row_labels=False), 'More Magics')
strings_tab.addTab(make_table(imglist_headers, enemy_names, row_labels=False), 'Enemy Names')
strings_tab.addTab(make_table(imglist_headers, character_names, row_labels=False), 'Character Names')
strings_tab.addTab(make_table(imglist_headers, job_names, row_labels=False), 'Job Names')
strings_tab.addTab(make_table(imglist_headers, ability_names, row_labels=False), 'Ability Names')
strings_tab.addTab(make_table(imglist_headers, battle_commands, row_labels=False), 'Battle Commands')
strings_tab.addTab(make_table(imglist_headers, zone_names, True, scale=1), 'Zone Names')
strings_tab.addTab(make_table(imglist_headers+['JP address'], dialogue, scale=1), 'Dialogue')
strings_tab.addTab(make_table(imglist_headers, string_images['menu_strings'], row_labels=False), 'Menu Strings')
strings_tab.addTab(make_table(imglist_headers, string_images['items'], row_labels=False), 'Items')
strings_tab.addTab(make_table(imglist_headers, string_images['magics']+string_images['magics2'], row_labels=False), 'Magics')
strings_tab.addTab(make_table(imglist_headers, string_images['enemy_names'], row_labels=False), 'Enemy Names')
strings_tab.addTab(make_table(imglist_headers, string_images['character_names'], row_labels=False), 'Character Names')
strings_tab.addTab(make_table(imglist_headers, string_images['job_names'], row_labels=False), 'Job Names')
strings_tab.addTab(make_table(imglist_headers, string_images['ability_names'], row_labels=False), 'Ability Names')
strings_tab.addTab(make_table(imglist_headers, string_images['battle_commands'], row_labels=False), 'Battle Commands')
strings_tab.addTab(make_table(imglist_headers, string_images['zone_names'], True, scale=1), 'Zone Names')
strings_tab.addTab(make_table(imglist_headers, string_images['dialogue'], scale=1), 'Dialogue')
self.string_decoder = QWidget()
self.decoder_input = QLineEdit()
@ -337,19 +285,17 @@ class FF5Reader(QMainWindow):
The painting logic here needs to be moved into includes.snestile at some point.
Once that is done, these functions will be moved to includes.snes which should not have qt dependencies.
'''
def make_string_img_small(bytestring, glyphs, jp=False):
def make_string_img_small(bytestring: Iterable[int], glyphs, handle_dakuten=False):
'''
JP version is not as fancy as this with dakuten, it just puts them on the row above and clips.
'''
if len(bytestring) < 1:
raise ValueError('Empty bytestring was passed')
string = ''
return None
img = QImage(len(bytestring)*8, 10, QImage.Format_RGB16)
img.fill(bg_color)
painter = QtGui.QPainter(img)
if jp:
if handle_dakuten:
for x, j in enumerate(bytestring):
string = string + const.Glyphs_JP2[j]
if 0x20 <= j < 0x52:
if j > 0x48:
painter.drawPixmap(x*8, 2, glyphs[j+0x17])
@ -361,33 +307,20 @@ def make_string_img_small(bytestring, glyphs, jp=False):
painter.drawPixmap(x*8, 2, glyphs[j])
else:
for x, j in enumerate(bytestring):
string = string + const.Glyphs[j]
painter.drawPixmap(x*8, 1, glyphs[j])
del painter
return string, QPixmap.fromImage(img)
return QPixmap.fromImage(img)
def make_string_img_large(bytestring, glyphs, glyphs_kanji, macros=None, jp=False):
def make_string_img_large(glyph_ids: Iterable[int], glyphs, glyphs_kanji=None):
'''
This is how we decipher dialogue data, which has multiple lines, macro expansions and kanji.
English characters have varying widths. In the japanese version, everything is fullwidth (16px)
Kanji aren't used in English dialogue but the cost is likely the same in checking either way.
'''
if len(bytestring) < 1:
raise ValueError('Empty bytestring was passed')
if len(glyph_ids) < 1:
return None
newstring = []
bytes = iter(bytestring)
for b in bytes:
if b in const.DoubleChars:
b2 = next(bytes)
newstring.append((b<<8) + b2)
elif macros and b in macros:
newstring.extend(macros[b])
else:
newstring.append(b)
string = ''
# Because the length of the input has little bearing on the size of the image thanks to linebreaks and macros, we overprovision then clip away.
max_width = 256 # This seems to check out, but the EN dialogue has linebreaks virtually everywhere anyway
max_height = 1024 # I've seen up to 58 rows in EN, 36 in JP. Stay safe.
@ -396,96 +329,63 @@ def make_string_img_large(bytestring, glyphs, glyphs_kanji, macros=None, jp=Fals
painter = QtGui.QPainter(img)
x = xmax = y = 0
for j in newstring:
if glyphs_kanji is not None: # jp
for j in glyph_ids:
if x >= max_width: # Wrap on long line
string += '[wr]\n'
xmax = max_width # Can't go higher than this anyway
x = 0
y += 16
if j == 0x01: # Line break
string += '[br]\n'
xmax = x if x > xmax else xmax
x = 0
y += 16
elif 0x1E00 <= j < 0x1FAA: # Kanji live in this range
string += const.Glyphs_Kanji[j-0x1E00]
painter.drawPixmap(x, y+2, glyphs_kanji[j-0x1E00])
x += 16
elif j < 0x13 or j > 0xFF: # Everything remaining outside this is a control char
string += '[0x{:02X}]'.format(j)
else:
if jp:
string += const.Glyphs_JP_large[j]
elif 0x13 <= j <= 0xFF: # Everything remaining outside this is a control char
painter.drawPixmap(x, y+2, glyphs[j])
x += 16
else:
string += const.Glyphs[j]
else: # en
for j in glyph_ids:
if x >= max_width: # Wrap on long line
xmax = max_width # Can't go higher than this anyway
x = 0
y += 16
if j == 0x01: # Line break
xmax = x if x > xmax else xmax
x = 0
y += 16
elif 0x13 <= j <= 0xFF: # Everything outside this is a control char
painter.drawPixmap(x, y+4, glyphs[j])
x += const.Dialogue_Width[j]
x += RPGe_Dialogue_Width[j]
del painter
xmax = x if x > xmax else xmax
return string, QPixmap.fromImage(img.copy(0, 0, xmax, y+16))
return QPixmap.fromImage(img.copy(0, 0, xmax, y+16))
def _make_string_img_list(rom_e, rom_j, start, length, num,
start_jp=None, len_jp=None, start_str=None, start_jp_str=None,
indirect=False, large=False, macros_en=None, macros_jp=None,
def _make_string_img_list(jp: StringBlock, en: StringBlock, large=False,
glyphs_en_s=None, glyphs_en_l=None,
glyphs_jp_s=None, glyphs_jp_l=None, glyphs_kanji=None):
start_jp = start if start_jp is None else start_jp
len_jp = length if len_jp is None else len_jp
start_str = start if start_str is None else start_str
start_jp_str = start_str if start_jp_str is None else start_jp_str
num = len(en)
print(len(en), len(jp))
stringlist = []
id_digits = hex_length(num-1)
if indirect:
for id in range(num):
en = start + (id*length)
jp = start_jp + (id*len_jp)
en_start = int.from_bytes(rom_e[en:en+length],'little') + start_str
en_end = int.from_bytes(rom_e[en+length:en+(length*2)],'little') + start_str
if en_start >= 0xC00000: # SNES memory space has the ROM starting at 0xC00000 in HiROM mode.
en_start -= 0xC00000
en_end -= 0xC00000
jp_start = int.from_bytes(rom_j[jp:jp+len_jp],'little') + start_jp_str
jp_end = int.from_bytes(rom_j[jp+len_jp:jp+(len_jp*2)],'little') + start_jp_str
if jp_start >= 0xC00000: # SNES memory space has the ROM starting at 0xC00000 in HiROM mode.
jp_start -= 0xC00000
jp_end -= 0xC00000
if (en_end == start_str) or (jp_end == start_jp_str):
break
try: # When dealing with pointer redirection we might end up passing empty strings
str_en = en.decoded[id]
str_jp = jp.decoded[id]
if large:
str_en, img_en = make_string_img_large(rom_e[en_start:en_end], glyphs_en_l, glyphs_kanji, macros_en)
img_en = make_string_img_large(en.glyphs[id], glyphs_en_l)
img_jp = make_string_img_large(jp.glyphs[id], glyphs_jp_l, glyphs_kanji)
else:
str_en, img_en = make_string_img_small(rom_e[en_start:en_end], glyphs_en_s)
except ValueError:
str_en = ''
img_en = None
try:
if large:
str_jp, img_jp = make_string_img_large(rom_j[jp_start:jp_end], glyphs_jp_l, glyphs_kanji, macros_jp, jp=True)
else:
str_jp, img_jp = make_string_img_small(rom_j[jp_start:jp_end], glyphs_jp_s, jp=True)
except ValueError:
str_jp = ''
img_jp = None
stringlist.append([
hex(en, 6), hex(id, id_digits),
str_en, img_en, str_jp, img_jp, hex(jp_start, 6)
])
else:
for id in range(num):
j1 = start + (id*length)
j2 = start_jp + (id*len_jp)
if large:
str_en, img_en = make_string_img_large(rom_e[j1:j1+length], glyphs_en_l, glyphs_kanji, macros_en)
str_jp, img_jp = make_string_img_large(rom_j[j2:j2+len_jp], glyphs_jp_l, glyphs_kanji, macros_jp, jp=True)
else:
str_en, img_en = make_string_img_small(rom_e[j1:j1+length], glyphs_en_s)
str_jp, img_jp = make_string_img_small(rom_j[j2:j2+len_jp], glyphs_jp_s, jp=True)
stringlist.append([hex(j1, 6), hex(id, id_digits), str_en, img_en, str_jp, img_jp])
img_en = make_string_img_small(en.glyphs[id], glyphs_en_s)
img_jp = make_string_img_small(jp.glyphs[id], glyphs_jp_s, handle_dakuten=True)
# ['ID', 'EN Pointer', 'EN Address', 'EN String', 'EN Img', 'JP Pointer', 'JP Address', 'JP String', 'JP Img']
stringlist.append([hex(id, id_digits), hex(en.pointer_address[id].start, 6), hex(en.string_address[id].start, 6), str_en, img_en,
hex(jp.pointer_address[id].start, 6), hex(jp.string_address[id].start, 6), str_jp, img_jp])
return stringlist
last_perfcount = None

View File

@ -1,251 +0,0 @@
'''
This file is part of ff5reader.
ff5reader is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ff5reader is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with ff5reader. If not, see <http://www.gnu.org/licenses/>.
'''
small_palette = [0xFF000000, 0x00000080, 0xFF808080, 0xFFFFFFFF]
dialogue_palette = [0xFF000080, 0xFFFFFFFF]
mono_palette = [0xFF000000, 0xFFFFFFFF]
Glyphs = (
' ',' ',' ',' ', ' ',' ',' ',' ', ' ',' ',' ',' ', ' ',' ',' ',' ', # 0x00
' ',' ',' ',' ', ' ',' ',' ',' ', ' ',' ',' ',' ', ' ',' ',' ',' ', # 0x10
'A','B','C','D', 'E','F','G','H', 'I','J','K','L', 'M','N','O','P', # 0x20
'Q','R','S','T','U','V','W','X','Y','Z','[stone]','[toad]','[mini]','[float]','[poison]','[KO]', # 0x30
'[blind]',' ',' ',' ',' ',' ',' ',' ', ' ',' ',' ',' ', ' ',' ',' ',' ', # 0x40
' ',' ',' ','0', '1','2','3','4', '5','6','7','8', '9','_m','_H','_P', # 0x50
'A','B','C','D', 'E','F','G','H', 'I','J','K','L', 'M','N','O','P', # 0x60
'Q','R','S','T', 'U','V','W','X', 'Y','Z','a','b', 'c','d','e','f', # 0x70
'g','h','i','j', 'k','l','m','n', 'o','p','q','r', 's','t','u','v', # 0x80
'w','x','y','z', 'il','it',' ','li', 'll','\'','"',':', ';',',','(',')', # 0x90
'/','!','?','.', 'ti','fi','Bl','a', 'pe','l','\'','"', 'if','lt','tl','ir', # 0xA0
'tt','','','', '', '', '', '', '', '', '','', '[key]', '[shoe]', '', '[hammer]', # 0xB0
'', '[ribbon]', '[potion]', '[shirt]', '', '-', '[shuriken]', '', '[scroll]', '!', '[claw]', '?', '[glove]', '%', '/', ':', # 0xC0
'', '', '.', 'A', 'B', 'X', 'Y', 'L', 'R', 'E', 'H', 'M', 'P', 'S', 'C', 'T', # 0xD0
'', '', '+', '[sword]', '[wh.mag]', '[blk.mag]', '🕒', '[knife]', '[spear]', '[axe]', '[katana]', '[rod]', '[staff]', '[bow]', '[harp]', '[whip]', # 0xE0
'[bell]', '[shield]', '[helmet]', '[armor]', '[ring]', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ') # F0
Glyphs_JP = list(Glyphs) # Transcription of the japanese glyph tiles
Glyphs_JP[0x60:0xCD] = [
'','','','', '','','','', '','','','', '','','','', # 0x60
'','','','', '','','','', '','','','', '','','','', # 0x70
'','','','', '','','','', '','','','', '','','','', # 0x80
'','','','', '','','','', '','','','', '','','','', # 0x90
'','','','', '','','','', '','','','', '','','','', # 0xA0
'','','','', '','','','', '','','','', '','','','', # 0xB0
'','','','', '','','', '', '','','','', ''] # 0xC0
Glyphs_JP[0xD2] = ''
Glyphs_JP[0xE3] = '[洋剣]'
Glyphs_JP[0xE7:0xF0] = ['[刂]', '[槍]', '[鉞]', '[刀]', '[棒]', '[杖]', '[弓]', '', '[鞭]']
Glyphs_JP2 = list(Glyphs_JP) # Japanese glyphs using the dakuten encoding
Glyphs_JP2[0x20:0x53] = [
'','','','', '','','','', '','','','', '','','','', # 0x20
'','','','', '','','','', '','','','', '','','','', # 0x30
'','','','', '','','','', '', # 0x40-0x49
'','','','', '','','','', '',''] # 0x49-0x53
Glyphs_JP_large = list(Glyphs_JP2) # Large glyphs are subtly different again
Glyphs_JP_large[0xC7] = ''
Glyphs_JP_large[0xE0:0xEB] = ['','','+','', '', '', '', '°C', '', '', '']
Glyphs_JP_large[0xFF] = ' '
Glyphs_Kanji = (
'', '', '', '', '', '', '', '', # 0x000
'', '', '', '', '', '', '', '', # 0x008
'', '', '', '', '', '', '', '', # 0x010
'', '', '', '', '', '', '', '', # 0x018
'', '', '', '', '', '', '', '', # 0x020
'', '', '', '', '', '使', '', '', # 0x028
'', '', '', '', '', '', '', '', # 0x030
'', '', '', '', '', '', '', '', # 0x038
'', '', '', '', '', '', '', '', # 0x040
'', '', '', '', '', '', '', '', # 0x048
'', '', '', '', '', '', '', '殿', # 0x050
'', '', '', '', '', '', '', '', # 0x058
'', '', '', '', '', '', '', '', # 0x060
'', '', '', '', '', '', '', '', # 0x068
'', '', '', '', '', '', '', '', # 0x070
'', '', '', '', '', '', '', '', # 0x078
'', '', '', '', '', '', '', '', # 0x080
'', '', '', '', '', '', '', '姿', # 0x088
'', '', '', '', '', '', '', '', # 0x090
'', '', '', '', '', '', '', '', # 0x098
'', '', '', '', '', '', '', '', # 0x0A0
'', '', '', '西', '', '', '', '', # 0x0A8
'', '', '', '', '', '', '', '', # 0x0B0
'', '', '', '', '', '', '', '', # 0x0B8
'', '', '', '', '', '', '', '', # 0x0C0
'', '', '', '', '', '', '', '', # 0x0C8
'', '', '', '', '', '', '', '', # 0x0D0
'', '', '', '', '', '', '', '', # 0x0D8
'', '', '', '', '', '', '', '', # 0x0E0
'', '', '', '', '', '', '', '', # 0x0E8
'', '', '', '', '', '', '', '', # 0x0F0
'', '', '', '宿', '', '', '', '', # 0x0F8
'', '', '', '', '', '', '', '', # 0x100
'', '', '', '', '', '', '', '', # 0x108
'', '', '', '', '', '', '', '', # 0x110
'', '', '', '', '', '', '', '', # 0x118
'', '', '', '', '', '', '', '', # 0x120
'', '', '', '', '', '', '', '', # 0x128
'', '', '', '', '', '', '', '', # 0x130
'', '', '', '', '', '', '', '', # 0x138
'', '', '', '', '', '', '', '', # 0x140
'', '', '', '', '', '', '', '', # 0x148
'', '', '', '', '', '', '', '', # 0x150
'', '', '', '', '', '', '', '', # 0x158
'', '', '', '', '', '', '', '', # 0x160
'', '', '', '', '', '', '', '', # 0x168
'', '', '', '', '', '', '', '', # 0x170
'', '', '', '', '', '', '', '', # 0x178
'', '', '', '', '', '', '', '', # 0x180
'', '', '', '', '', '', '', '', # 0x188
'', '', '', '', '', '', '', '', # 0x190
'', '', '', '', '', '', '', '', # 0x198
'', '', '', '', '', '', '', '', # 0x1A0
'', ' ') # 0x1A8
Dialogue_Macros_EN = {
0x02: [0x61, 0x7A, 0x8B, 0x8D, 0x93], # expands to Bartz (or whatever his name is)
}
Dialogue_Macros_JP = {
# Is 0x00 a wait for input marker?
# 0x01 is linebreak
0x02: [0x20, 0xBC, 0x82], # 0x02 expands to Bartz's name バッツ. Used for his dialogue in EN, only used for other chars in JP.
0x03: [0x6E, 0xA8, 0x78, 0x7E, 0xAA], # 0x03 is クリスタル
0x04: [0x7E, 0x8C, 0x6E, 0xC5, 0xB8], # expands to タイクーン
0x06: [0x37, 0xBF], # expands to じゃ
0x07: [0x8D, 0xAB], # expands to いる
0x08: [0xFF, 0xFF, 0xFF, 0xFF], # 4 spaces
0x09: [0xFF, 0xFF, 0xFF], # 3 spaces
0x0A: [0xFF, 0xFF], # 2 spaces
0x0B: [0x1E12, 0x1E13], # expands to 魔物
# 0x0C appears to be a pause in delivery - affects previous char
0x0D: [0x1E24, 0x9B, 0x1E52, 0x1E57], # expands to 風の神殿
0x0E: [0x1E04, 0x1E0A], # expands to 飛竜
# 0x0F - unknown (invisible control char)
# 0x10 is a gil substitution
# 0x11 and 0x12 appear to be item (obtained) substitutions
0x13: [0x1E07, 0x1E0D], # expands to 封印
0x14: [0x76, 0x46, 0xD0], # Cid speaking - シド「
0x15: [0x9E, 0x46, 0xD0], # Mid speaking - ミド「
0x16: [0x1E05, 0x1E06], # expands to 世界
# 0x17 uses the next byte for pause duration (seconds?)
0x18: [0x8E, 0x6E, 0x78, 0x44, 0x78], # expands to エクスデス
0x19: [0xAC, 0x92, 0xD0], # Lenna speaking - レナ「
0x1A: [0x2A, 0xA6, 0x64, 0xD0], # Galuf speaking - ガラフ「
0x1B: [0x64, 0xC4, 0xA8, 0x78, 0xD0], # Faris speaking - ファリス「
0x1C: [0x6E, 0xAA, 0xAA, 0xD0], # Krile/Kara speaking - クルル「
0x1D: [0x91, 0x37, 0x8D, 0x81, 0xBF, 0xB9], # expands to おじいちゃん
# 0x1E-0x1F form kanji with the next byte
# 0x20-0xCC are standard character set
0xCD: [0xC9, 0xC9], # % (0xCD) to !!
# 0xCE is
0xCF: [0xBD, 0x85], # : (0xCF) appears to expand to って
# 0xD0-0xD4 are 「」。AB
0xD5: [0x1E1B, 0x95, 0x1E08, 0xAD], # expands to 手に入れ
# 0xD6, 0xD7, 0xD8 are
0xD9: [0x93, 0x8D], # expands to ない
# 0xDA-0xDC are
0xDD: [0xC7, 0xC7], # S (0xDD) to ……
0xDE: [0x3F, 0x8D, 0x37, 0xC3, 0x89, 0x25], # C (0xDE) to だいじょうぶ
0xDF: [0x61, 0xE3], # T (0xDF) to は、
0xE0: [0xB9, 0x3F], # expands to んだ
0xE1: [0x85, 0x8D], # expands to てい
0xE2: [0x77, 0x7F], # expands to した
# 0xE3 is 、
0xE4: [0x77, 0x85], # ◯ (0xE4) appears to expand to して
# 0xE5 is used for Bartz speaking in JP. This only appears as 『
0xE6: [0x91, 0x1E0F, 0x1E03], # F (0xE6) appears to expand to otousan (お父様)
0xE7: [0xC9, 0xCB], # °C (0xE7) to !? - yes this is the wrong order interrobang
0xE8: [0x45, 0x79], # ・ (0xE8) appears to expand to です
# 0xE9, 0xEA are
0xEB: [0x73, 0x9B], # expands to この
0xEC: [0x9B, 0x1E02], # expands to の力
0xED: [0x70, 0xAA, 0x2A, 0xC5], # expands to ケルガー
0xEE: [0x1E86, 0x1ED7, 0x1E87, 0x1E62, 0x1EA7], # expands to 古代図書舘 (ancient library?)
0xEF: [0x1E1C, 0xBD, 0x85], # expands to 言って
0xF0: [0x1E2B, 0x1E0B, 0xD0], # soldier speaking - 兵士「
0xF1: [0x6B, 0xA7], # expands to から
0xF2: [0x1E2C, 0x6A, 0x1E0C], # expands to 火カ船
0xF3: [0x1E0E, 0x3D, 0x6F], # expands to 海ぞく
0xF4: [0x8D, 0x37, 0xC3, 0x89], # expands to いじょう
0xF5: [0x2B, 0xE3], # expands to が、
0xF6: [0x7F, 0x81], # expands to たち
0xF7: [0x7F, 0x9B], # expands to たの
0xF8: [0x9D, 0x79], # expands to ます
0xF9: [0x6F, 0x3F, 0x75, 0x8D], # expands to ください
0xFA: [0x6B, 0xBD, 0x7F], # expands to かった
0xFB: [0x7F, 0xC9], # expands to た!
0xFC: [0x95, 0xE3], # expands to に、
0xFD: [0x8D, 0x93, 0x8D, 0x6B, 0xA7, 0x93, 0xB9, 0x3F], # expands to いないからなんだ
0xFE: [0x1F20, 0x1F38, 0x9B, 0x61, 0x35, 0x9D], # expands to 次元のはざま
# 0xFF is space
}
DoubleChars = set([0x17, 0x1E, 0x1F]) # 0x1E and 0x1F are kanji, 0x17 is a pause marker
Dialogue_Width = [4 for i in range(256)]
Dialogue_Width[0x50:0xB1] = [a+1 for a in [
5, 2, 6, 6, 5, 6, 6, 6, 6, 6, 6, 6, 6, 8, 8, 8, # 0x50
6, 6, 5, 6, 5, 5, 6, 6, 2, 6, 7, 5, 10,7, 6, 6, # 0x60
6, 6, 6, 6, 6, 6, 10,6, 6, 6, 6, 6, 5, 6, 6, 5, # 0x70
6, 6, 2, 5, 6, 2, 10,6, 6, 6, 6, 5, 5, 4, 6, 6, # 0x80
10,6, 6, 6, 5, 7, 6, 5, 5, 2, 5, 2, 2, 2, 3, 3, # 0x90
5, 2, 6, 2, 7, 8, 0, 0, 6, 9, 2, 5, 8, 7, 7, 8, # 0xA0
9
]]
BGM_Tracks = (
"Ahead on our way", "The Fierce Battle", "A Presentiment", "Go Go Boko!",
"Pirates Ahoy", "Tenderness in the Air", "Fate in Haze", "Moogle theme",
"Prelude/Crystal Room", "The Last Battle", "Requiem", "Nostalgia",
"Cursed Earths", "Lenna's Theme", "Victory's Fanfare", "Deception",
"The Day Will Come", "Nothing?", "ExDeath's Castle", "My Home, Sweet Home",
"Waltz Suomi", "Sealed Away", "The Four Warriors of Dawn", "Danger",
"The Fire Powered Ship", "As I Feel, You Feel", "Mambo de Chocobo!", "Music Box",
"Intension of the Earth", "The Dragon Spreads its Wings", "Beyond the Deep Blue Sea", "The Prelude of Empty Skies",
"Searching the Light", "Harvest", "Battle with Gilgamesh", "Four Valiant Hearts",
"The Book of Sealings", "What?", "Hurry! Hurry!", "Unknown Lands",
"The Airship", "Fanfare 1", "Fanfare 2", "The Battle",
"Walking the Snowy Mountains", "The Evil Lord Exdeath", "The Castle of Dawn", "I'm a Dancer",
"Reminiscence", "Run!", "The Ancient Library", "Royal Palace",
"Good Night!", "Piano Lesson 1", "Piano Lesson 2", "Piano Lesson 3",
"Piano Lesson 4", "Piano Lesson 5", "Piano Lesson 6", "Piano Lesson 7",
"Piano Lesson 8", "Musica Machina", "Meteor falling?", "The Land Unknown",
"The Decisive Battle", "The Silent Beyond", "Dear Friends", "Final Fantasy",
"A New Origin", "Chirping sound"
)
BGM_Tracks_Safe = [t.replace('/', '-') for t in BGM_Tracks]
npc_layer_count = 0x200
npc_layer_structure = [
("Dialogue/trigger ID", 1, None),
("0x01", 1, None),
("Sprite ID", 1, None),
("X", 1, None),
("Y", 1, None),
("Move Pattern", 1, None),
("Palette", 1, None)
]
npc_layer_headers = ["Ptr Address", "Layer", "Data Address"] + [x[0] for x in npc_layer_structure]
zone_count = 0x200

View File

@ -0,0 +1,70 @@
Ahead on our way
The Fierce Battle
A Presentiment
Go Go Boko!
Pirates Ahoy
Tenderness in the Air
Fate in Haze
Moogle theme
Prelude/Crystal Room
The Last Battle
Requiem
Nostalgia
Cursed Earths
Lenna's Theme
Victory's Fanfare
Deception
The Day Will Come
Nothing?
ExDeath's Castle
My Home, Sweet Home
Waltz Suomi
Sealed Away
The Four Warriors of Dawn
Danger
The Fire Powered Ship
As I Feel, You Feel
Mambo de Chocobo!
Music Box
Intension of the Earth
The Dragon Spreads its Wings
Beyond the Deep Blue Sea
The Prelude of Empty Skies
Searching the Light
Harvest
Battle with Gilgamesh
Four Valiant Hearts
The Book of Sealings
What?
Hurry! Hurry!
Unknown Lands
The Airship
Fanfare 1
Fanfare 2
The Battle
Walking the Snowy Mountains
The Evil Lord Exdeath
The Castle of Dawn
I'm a Dancer
Reminiscence
Run!
The Ancient Library
Royal Palace
Good Night!
Piano Lesson 1
Piano Lesson 2
Piano Lesson 3
Piano Lesson 4
Piano Lesson 5
Piano Lesson 6
Piano Lesson 7
Piano Lesson 8
Musica Machina
Meteor falling?
The Land Unknown
The Decisive Battle
The Silent Beyond
Dear Friends
Final Fantasy
A New Origin
Chirping sound

View File

@ -0,0 +1,256 @@
\n
"
'
0
1
2
3
4
5
6
7
8
9
_m
_H
_P
A
B
C
D
E
F
G
H
I
J
K
L
M
N
O
P
Q
R
S
T
U
V
W
X
Y
Z
a
b
c
d
e
f
g
h
i
j
k
l
m
n
o
p
q
r
s
t
u
v
w
x
y
z
il
it
li
ll
'
"
:
;
,
(
)
/
!
?
.
ti
fi
pe
l
'
"
if
lt
tl
ir
tt
%
/
:
A
B
X
Y
L
R
E
H
M
P
S
C
T
+
°C

View File

@ -0,0 +1,256 @@
 
\n
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0
1
2
3
4
5
6
7
8
9
_m
_H
_P
%
/
:
A
B
X
Y
L
R
E
H
M
P
S
C
T
+
°C
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

View File

@ -0,0 +1,426 @@
使
殿
姿
西
宿
 

View File

@ -0,0 +1,256 @@
A
B
C
D
E
F
G
H
I
J
K
L
M
N
O
P
Q
R
S
T
U
V
W
X
Y
Z
[stone]
[toad]
[mini]
[float]
[poison]
[KO]
[blind]
0
1
2
3
4
5
6
7
8
9
_m
_H
_P
A
B
C
D
E
F
G
H
I
J
K
L
M
N
O
P
Q
R
S
T
U
V
W
X
Y
Z
a
b
c
d
e
f
g
h
i
j
k
l
m
n
o
p
q
r
s
t
u
v
w
x
y
z
il
it
li
ll
'
"
:
;
,
(
)
/
!
?
.
ti
fi
Bl
a
pe
l
'
"
if
lt
tl
ir
tt
[key]
[shoe]
[hammer]
[ribbon]
[potion]
[shirt]
-
[shuriken]
[scroll]
!
[claw]
?
[glove]
%
/
:
.
A
B
X
Y
L
R
E
H
M
P
S
C
T
+
[sword]
[wh.mag]
[blk.mag]
🕒
[knife]
[spear]
[axe]
[katana]
[rod]
[staff]
[bow]
[harp]
[whip]
[bell]
[shield]
[helmet]
[armor]
[ring]

View File

@ -0,0 +1,256 @@
A
B
C
D
E
F
G
H
I
J
K
L
M
N
O
P
Q
R
S
T
U
V
W
X
Y
Z
[stone]
[toad]
[mini]
[float]
[poison]
[KO]
[blind]
0
1
2
3
4
5
6
7
8
9
_m
_H
_P
%
/
:
A
B
X
Y
L
R
E
H
M
P
S
C
T
+
[洋剣]
[wh.mag]
[blk.mag]
🕒
[刂]
[槍]
[鉞]
[刀]
[棒]
[杖]
[弓]
[鞭]
[bell]
[shield]
[helmet]
[armor]
[ring]

View File

@ -0,0 +1,256 @@
0
1
2
3
4
5
6
7
8
9
_m
_H
_P
%
/
:
A
B
X
Y
L
R
E
H
M
P
S
C
T
+
[洋剣]
[wh.mag]
[blk.mag]
🕒
[刂]
[槍]
[鉞]
[刀]
[棒]
[杖]
[弓]
[鞭]
[bell]
[shield]
[helmet]
[armor]
[ring]

42
includes/ff5/__init__.py Normal file
View File

@ -0,0 +1,42 @@
from . import const, files, strings
from ..helpers import hex, parse_struct
class ZoneData:
def split_tilesets(data):
tilesets = [(data & 0x00003F),
(data & 0x000FC0) >> 6,
(data & 0x03F000) >> 12,
(data & 0xFC0000) >> 18]
return ' '.join([hex(i,2) for i in tilesets])
def split_blockmaps(data):
blockmaps = [(data & 0x000003FF) - 1,
((data & 0x000FFC00) >> 10) - 1,
((data & 0x3FF00000) >> 20) - 1]
return ' '.join([hex(i,3) for i in blockmaps])
zone_structure = [('NPC Layer', 2, None), # 00 01
('Name', 1, strings.Strings.blocks_RPGe['zone_names'].decoded), # 02
('ShadowFlags', 1, None), # 03
('Graphic maths', 1, None), # 04 - MSb animation-related, 6 LSbs are ID for table in 0x005BB8 which writes to $2131-$2133 (Color Math Designation, Subscreen BG color)
('Tile properties',1, None), # 05
('Flags '+hex(6), 1, None), # 06
(hex(7), 1, None), # 07
('Blockset', 1, None), # 08
('Tilesets', 3, split_tilesets), # 09 0A 0B
('Blockmaps', 4, split_blockmaps), # 0C 0D 0E 0F
(hex(16), 1, None), # 10
(hex(17), 1, None), # 11
(hex(18), 1, None), # 12
(hex(19), 1, None), # 13
(hex(20), 1, None), # 14
(hex(21), 1, None), # 15
('Palette', 1, None), # 16
(hex(23), 1, None), # 17
(hex(24), 1, None), # 18
('Music', 1, const.BGM_Tracks)] # 19
zone_headers = ['Address'] + [z[0] for z in zone_structure]
@classmethod
def get_data(cls):
return [parse_struct(files.ROM_RPGe, 0x0E9C00 + (i*0x1A), cls.zone_structure) for i in range(const.zone_count)]

47
includes/ff5/const.py Normal file
View File

@ -0,0 +1,47 @@
'''
This file is part of ff5reader.
ff5reader is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ff5reader is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with ff5reader. If not, see <http://www.gnu.org/licenses/>.
'''
from ..helpers import load_table
from pathlib import Path
dir = Path(__file__).parent
# print(dir)
# Font palettes (probably move these later)
small_palette = [0xFF000000, 0x00000080, 0xFF808080, 0xFFFFFFFF]
dialogue_palette = [0xFF000080, 0xFFFFFFFF]
mono_palette = [0xFF000000, 0xFFFFFFFF]
BGM_Tracks = load_table(dir/'BGM_Titles.txt')
BGM_Tracks_Safe = [t.replace('/', '-') for t in BGM_Tracks] # For saving files
npc_layer_count = 0x200
npc_layer_structure = [
("Dialogue/trigger ID", 1, None),
("0x01", 1, None),
("Sprite ID", 1, None),
("X", 1, None),
("Y", 1, None),
("Move Pattern", 1, None),
("Palette", 1, None)
]
npc_layer_headers = ["Ptr Address", "Layer", "Data Address"] + [x[0] for x in npc_layer_structure]
zone_count = 0x200

7
includes/ff5/files.py Normal file
View File

@ -0,0 +1,7 @@
from ..helpers import load_raw
filename_RPGe = 'Final Fantasy V (Japan) [En by RPGe v1.1].sfc'
filename_SNES = 'Final Fantasy V (Japan).sfc'
ROM_RPGe = load_raw(filename_RPGe)
ROM_SNES = load_raw(filename_SNES)

View File

@ -0,0 +1,12 @@
name num_entries address snes_address bytes snes_bytes rpge_ptr_offset snes_ptr_offset dialog
ability_names 33 0x116200 8
battle_commands 96 0x201150 0x115800 7 5
character_names 5 0x115500 6
dialogue 0x900 0x2013F0 0x082220 3 2 0x000000 0x0A0000 True
enemy_names 384 0x200050 0x105C00 10 8
items 0x100 0x111380 9
job_names 22 0x115600 8
magics 87 0x111C80 6
magics2 73 0x111E8A 9
menu_strings 139 0x00F987 2 0x270000 0x000000
zone_names 0x100 0x107000 2 0x270000 0x107200 True
1 name num_entries address snes_address bytes snes_bytes rpge_ptr_offset snes_ptr_offset dialog
2 ability_names 33 0x116200 8
3 battle_commands 96 0x201150 0x115800 7 5
4 character_names 5 0x115500 6
5 dialogue 0x900 0x2013F0 0x082220 3 2 0x000000 0x0A0000 True
6 enemy_names 384 0x200050 0x105C00 10 8
7 items 0x100 0x111380 9
8 job_names 22 0x115600 8
9 magics 87 0x111C80 6
10 magics2 73 0x111E8A 9
11 menu_strings 139 0x00F987 2 0x270000 0x000000
12 zone_names 0x100 0x107000 2 0x270000 0x107200 True

212
includes/ff5/strings.py Normal file
View File

@ -0,0 +1,212 @@
from ..helpers import get_slices, get_contiguous_address_slices, load_table, load_tsv
from . import files
from typing import Callable, Iterable
from pathlib import Path
from dataclasses import dataclass
@dataclass
class StringBlock:
raw: bytes
glyphs: Iterable[int]
decoded: str
string_address: int
pointer_address: int
def __len__(self):
return len(self.raw)
dir = Path(__file__).parent
#print(dir)
# Transcription of RPGe glyph tiles
Glyphs_small_RPGe = load_table(dir/'Glyphs_small_RPGe.txt')
Glyphs_dialog_RPGe = load_table(dir/'Glyphs_dialog_RPGe.txt')
Glyphs_small_SNES = load_table(dir/'Glyphs_small_SNES.txt')
Glyphs_small_dakuten_SNES = load_table(dir/'Glyphs_small_dakuten_SNES.txt')
Glyphs_dialog_SNES = load_table(dir/'Glyphs_dialog_SNES.txt')
Glyphs_dialog_kanji_SNES = load_table(dir/'Glyphs_dialog_kanji_SNES.txt')
class DialogMacros:
expansions_SNES = {
# Is 0x00 a wait for input marker?
# 0x01 is linebreak
0x02: [0x20, 0xBC, 0x82], # 0x02 expands to Bartz's name バッツ. Used for his dialogue in EN, only used for other chars in JP.
0x03: [0x6E, 0xA8, 0x78, 0x7E, 0xAA], # 0x03 is クリスタル
0x04: [0x7E, 0x8C, 0x6E, 0xC5, 0xB8], # expands to タイクーン
0x06: [0x37, 0xBF], # expands to じゃ
0x07: [0x8D, 0xAB], # expands to いる
0x08: [0xFF, 0xFF, 0xFF, 0xFF], # 4 spaces
0x09: [0xFF, 0xFF, 0xFF], # 3 spaces
0x0A: [0xFF, 0xFF], # 2 spaces
0x0B: [0x1E12, 0x1E13], # expands to 魔物
# 0x0C appears to be a pause in delivery - affects previous char
0x0D: [0x1E24, 0x9B, 0x1E52, 0x1E57], # expands to 風の神殿
0x0E: [0x1E04, 0x1E0A], # expands to 飛竜
# 0x0F - unknown (invisible control char)
# 0x10 is a gil substitution
# 0x11 and 0x12 appear to be item (obtained) substitutions
0x13: [0x1E07, 0x1E0D], # expands to 封印
0x14: [0x76, 0x46, 0xD0], # Cid speaking - シド「
0x15: [0x9E, 0x46, 0xD0], # Mid speaking - ミド「
0x16: [0x1E05, 0x1E06], # expands to 世界
# 0x17 uses the next byte for pause duration (seconds?)
0x18: [0x8E, 0x6E, 0x78, 0x44, 0x78], # expands to エクスデス
0x19: [0xAC, 0x92, 0xD0], # Lenna speaking - レナ「
0x1A: [0x2A, 0xA6, 0x64, 0xD0], # Galuf speaking - ガラフ「
0x1B: [0x64, 0xC4, 0xA8, 0x78, 0xD0], # Faris speaking - ファリス「
0x1C: [0x6E, 0xAA, 0xAA, 0xD0], # Krile/Kara speaking - クルル「
0x1D: [0x91, 0x37, 0x8D, 0x81, 0xBF, 0xB9], # expands to おじいちゃん
# 0x1E-0x1F form kanji with the next byte
# 0x20-0xCC are standard character set
0xCD: [0xC9, 0xC9], # % (0xCD) to !!
# 0xCE is
0xCF: [0xBD, 0x85], # : (0xCF) appears to expand to って
# 0xD0-0xD4 are 「」。AB
0xD5: [0x1E1B, 0x95, 0x1E08, 0xAD], # expands to 手に入れ
# 0xD6, 0xD7, 0xD8 are
0xD9: [0x93, 0x8D], # expands to ない
# 0xDA-0xDC are
0xDD: [0xC7, 0xC7], # S (0xDD) to ……
0xDE: [0x3F, 0x8D, 0x37, 0xC3, 0x89, 0x25], # C (0xDE) to だいじょうぶ
0xDF: [0x61, 0xE3], # T (0xDF) to は、
0xE0: [0xB9, 0x3F], # expands to んだ
0xE1: [0x85, 0x8D], # expands to てい
0xE2: [0x77, 0x7F], # expands to した
# 0xE3 is 、
0xE4: [0x77, 0x85], # ◯ (0xE4) appears to expand to して
# 0xE5 is used for Bartz speaking in JP. This only appears as 『
0xE6: [0x91, 0x1E0F, 0x1E03], # F (0xE6) appears to expand to otousan (お父様)
0xE7: [0xC9, 0xCB], # °C (0xE7) to !? - yes this is the wrong order interrobang
0xE8: [0x45, 0x79], # ・ (0xE8) appears to expand to です
# 0xE9, 0xEA are
0xEB: [0x73, 0x9B], # expands to この
0xEC: [0x9B, 0x1E02], # expands to の力
0xED: [0x70, 0xAA, 0x2A, 0xC5], # expands to ケルガー
0xEE: [0x1E86, 0x1ED7, 0x1E87, 0x1E62, 0x1EA7], # expands to 古代図書舘 (ancient library?)
0xEF: [0x1E1C, 0xBD, 0x85], # expands to 言って
0xF0: [0x1E2B, 0x1E0B, 0xD0], # soldier speaking - 兵士「
0xF1: [0x6B, 0xA7], # expands to から
0xF2: [0x1E2C, 0x6A, 0x1E0C], # expands to 火カ船
0xF3: [0x1E0E, 0x3D, 0x6F], # expands to 海ぞく
0xF4: [0x8D, 0x37, 0xC3, 0x89], # expands to いじょう
0xF5: [0x2B, 0xE3], # expands to が、
0xF6: [0x7F, 0x81], # expands to たち
0xF7: [0x7F, 0x9B], # expands to たの
0xF8: [0x9D, 0x79], # expands to ます
0xF9: [0x6F, 0x3F, 0x75, 0x8D], # expands to ください
0xFA: [0x6B, 0xBD, 0x7F], # expands to かった
0xFB: [0x7F, 0xC9], # expands to た!
0xFC: [0x95, 0xE3], # expands to に、
0xFD: [0x8D, 0x93, 0x8D, 0x6B, 0xA7, 0x93, 0xB9, 0x3F], # expands to いないからなんだ
0xFE: [0x1F20, 0x1F38, 0x9B, 0x61, 0x35, 0x9D], # expands to 次元のはざま
# 0xFF is space
}
# XX: Maybe convert this to subclasses with the same method signature. Don't care enough yet.
def expand_RPGe_dialog(raw: bytes) -> list[int]:
output = []
it = iter(raw)
while (n := next(it, None)) is not None:
if n == 0x02: # expands to Bartz (or whatever his name is)
output += [0x61, 0x7A, 0x8B, 0x8D, 0x93]
elif n == 0x17: # Pause marker
pause_duration = next(it, None) # Consume the duration
# For now, we just strip these
output.append(n)
return output
@classmethod
def expand_SNES_dialog(cls, raw: bytes) -> list[int]:
output = []
it = iter(raw)
while (n := next(it, None)) is not None:
if n == 0x02: # expands to Bartz (or whatever his name is)
output += [0x61, 0x7A, 0x8B, 0x8D, 0x93]
elif n == 0x17: # Pause marker
pause_duration = next(it, None) # Consume the duration
# For now, we just strip these
elif n in (0x1E, 0x1F): # Kanji prefix
output.append((n<<8) + next(it, 0)) # Convert e.g. 0x1E, 0x2C to 0x1E2C
else:
output += cls.expansions_SNES.get(n, [n])
return output
RPGe_Dialogue_Width = [4 for i in range(256)]
RPGe_Dialogue_Width[0x50:0xB1] = [a+1 for a in [
5, 2, 6, 6, 5, 6, 6, 6, 6, 6, 6, 6, 6, 8, 8, 8, # 0x50
6, 6, 5, 6, 5, 5, 6, 6, 2, 6, 7, 5, 10, 7, 6, 6, # 0x60
6, 6, 6, 6, 6, 6,10, 6, 6, 6, 6, 6, 5, 6, 6, 5, # 0x70
6, 6, 2, 5, 6, 2,10, 6, 6, 6, 6, 5, 5, 4, 6, 6, # 0x80
10, 6, 6, 6, 5, 7, 6, 5, 5, 2, 5, 2, 2, 2, 3, 3, # 0x90
5, 2, 6, 2, 7, 8, 0, 0, 6, 9, 2, 5, 8, 7, 7, 8, # 0xA0
9
]]
def decode_RPGe_small(glyph: int) -> str:
return Glyphs_small_RPGe[glyph]
def decode_RPGe_dialog(glyph: int) -> str:
return Glyphs_dialog_RPGe[glyph]
def decode_SNES_small(glyph: int) -> str:
return Glyphs_small_SNES[glyph]
# return Glyphs_small_dakuten_SNES[glyph]
def decode_SNES_dialog(glyph: int) -> str:
if glyph > 0xFF:
kanji_idx = glyph - 0x1E00
if kanji_idx < 0x1AA: # Hardcoded len(Glyphs_dialog_kanji_SNES)
return Glyphs_dialog_kanji_SNES[kanji_idx]
return f'[0x{glyph:04x}]'
return Glyphs_dialog_SNES[glyph]
def decode_glyphs(glyphs: list[str], glyph_decoder: Callable[[int], str]) -> str:
return ''.join(glyph_decoder(c) for c in glyphs)
def make_snes_jp_en_strings(data: dict[str, object]) -> tuple[StringBlock, StringBlock]:
print(data)
indirect_offset_jp = data.get('snes_ptr_offset')
indirect_offset_en = data.get('rpge_ptr_offset')
pointer_slices_jp = get_slices(data.get('snes_address', data['address']), data.get('snes_bytes', data['bytes']), data['num_entries'])
pointer_slices_en = get_slices(data['address'], data['bytes'], data['num_entries'])
if indirect_offset_jp is not None:
address_slices_jp = get_contiguous_address_slices(files.ROM_SNES, pointer_slices_jp, indirect_offset_jp)
else:
address_slices_jp = pointer_slices_jp
if indirect_offset_en is not None:
address_slices_en = get_contiguous_address_slices(files.ROM_RPGe, pointer_slices_en, indirect_offset_en)
else:
address_slices_en = pointer_slices_en
raws_jp = [files.ROM_SNES[s] for s in address_slices_jp]
raws_en = [files.ROM_RPGe[s] for s in address_slices_en]
if data.get('dialog'):
glyphs_jp = [DialogMacros.expand_SNES_dialog(raw) for raw in raws_jp]
glyphs_en = [DialogMacros.expand_RPGe_dialog(raw) for raw in raws_en]
strings_jp = [decode_glyphs(glyphs, decode_SNES_dialog) for glyphs in glyphs_jp]
strings_en = [decode_glyphs(glyphs, decode_RPGe_dialog) for glyphs in glyphs_en]
else:
glyphs_jp = raws_jp
glyphs_en = raws_en
strings_jp = [decode_glyphs(glyphs, decode_SNES_small) for glyphs in glyphs_jp]
strings_en = [decode_glyphs(glyphs, decode_RPGe_small) for glyphs in glyphs_en]
return StringBlock(raws_jp, glyphs_jp, strings_jp, address_slices_jp, pointer_slices_jp), StringBlock(raws_en, glyphs_en, strings_en, address_slices_en, pointer_slices_en)
class Strings:
config = load_tsv(dir/'string_blocks.tsv')
blocks_SNES_RPGe = {k: make_snes_jp_en_strings(v) for k,v in config.items()}
blocks_SNES = {k:v[0] for k,v in blocks_SNES_RPGe.items()}
blocks_RPGe = {k:v[1] for k,v in blocks_SNES_RPGe.items()}

View File

@ -14,25 +14,26 @@
You should have received a copy of the GNU General Public License
along with ff5reader. If not, see <http://www.gnu.org/licenses/>.
'''
from ast import literal_eval
HEX_PREFIX = '#' # '#' '$' or '0x' are also nice
def divceil(numerator, denominator):
def divceil(numerator: int, denominator: int) -> int:
'''
Reverse floor division for fast ceil
'''
return -(-numerator // denominator)
def hex_length(i):
def hex_length(i: int) -> int:
'''
String length of hexadecimal representation of integer
'''
return divceil(i.bit_length(), 4)
def hex(num, digits=2):
def hex(num: int, digits: int = 2) -> str:
'''
Consolidate hex formatting for consistency
'''
@ -40,14 +41,102 @@ def hex(num, digits=2):
return HEX_PREFIX + '{:0{}X}'.format(num, digits)
def indirect(rom, start, length=2, endian='little'):
def indirect(rom: bytes, start: int, length: int = 2, endian: str = 'little') -> int:
'''
Read little-endian value at start address in rom
'''
return int.from_bytes(rom[start:start+length], endian)
def indirect2(rom: bytes, slice, endian: str = 'little') -> int:
return int.from_bytes(rom[slice], endian)
def parse_struct(rom, offset, structure):
def memory_address_to_rom_address(address: int) -> int:
# SNES memory space in HiROM Mode:
# 0xC00000 to 0xFDFFFF are a view of the ROM, i.e. banks $C0 to $FD.
# Banks $80-$BF mirror banks $00-3F, which have a partial view to the ROM.
# We don't care about potential RAM addresses at low offsets,
# so let's just pretend $00-$3F, $40-$7F, $80-$BF and $C0-$FF all map to $00-$3F of the ROM
return address & 0x3FFFFF
def get_slices(start_address: int, each_length: int, num_strings: int) -> list:
return [slice(start_address+(each_length*id), start_address+(each_length*(id+1))) for id in range(num_strings)]
def get_contiguous_address_slices(rom: bytes, slices, indirect_offset: int = 0) -> list:
pointers = [indirect2(rom, s) + indirect_offset for s in slices]
output = []
for ptr, ptr_next in zip(pointers[:-1], pointers[1:]):
if ptr_next < ptr:
break
start = memory_address_to_rom_address(ptr)
end = memory_address_to_rom_address(ptr_next)
output.append(slice(start, end))
return output
def get_bytestring_slices(rom: bytes, start_address: int, each_length: int, num_strings: int, indirect_offset=None, indirect_null_terminated=False) -> list:
if indirect_offset is not None:
if not indirect_null_terminated:
pointers = [indirect(rom, address, each_length) + indirect_offset for address in range(start_address, start_address + (each_length * num_strings), each_length)]
output = []
for ptr, ptr_next in zip(pointers[:-1], pointers[1:]):
if ptr_next < ptr:
break
start = memory_address_to_rom_address(ptr)
end = memory_address_to_rom_address(ptr_next)
output.append(slice(start, end))
return output
else:
def get_indirect(address: int) -> bytes:
ptr = memory_address_to_rom_address(indirect(rom, address, each_length) + indirect_offset)
# While previously we used the start address of the next string, this is not necessarily correct.
# Scan for a zero-byte end-of-string marker and pay the extra cycles.
end_ptr = ptr
# print(ptr)
while rom[end_ptr] > 0:
end_ptr += 1
return slice(ptr, end_ptr)
return [get_indirect(start_address+(each_length*id)) for id in range(num_strings)]
else:
return [slice(start_address+(each_length*id), start_address+(each_length*(id+1))) for id in range(num_strings)]
def get_bytestrings(rom: bytes, start_address: int, each_length: int, num_strings: int, indirect_offset=None, indirect_null_terminated=False) -> list[bytes]:
return [rom[s] for s in get_bytestring_slices(rom, start_address, each_length, num_strings, indirect_offset, indirect_null_terminated)]
def load_table(filename: str) -> tuple[str]:
with open(filename, 'r') as f:
return tuple(literal_eval(f'"{line}"') if line.startswith('\\') else line for line in f.read().rstrip('\n').split('\n'))
def __cast_string_to_object(input: str) -> object:
if len(input) == 0:
return None
try:
return literal_eval(input)
except:
return input # Unescaped string
def load_tsv(filename: str) -> dict[dict[str, str]]:
with open(filename, 'r') as f:
header, *lines = f.read().rstrip('\n').split('\n')
first_column_name, *headers = header.split('\t')
# Cheeky Py3.8 one-liner
# return {(s := line.split('\t'))[0]:dict(zip(headers, s[1:])) for line in lines}
output = {}
for line in lines:
name, *values = line.split('\t')
output[name] = dict((h,__cast_string_to_object(v)) for h,v in zip(headers, values) if len(v) > 0)
return output
def load_raw(filename: str) -> bytes:
with open(filename, 'rb') as f:
return f.read()
def parse_struct(rom: int, offset: int, structure: list[tuple[str, int, object]]):
'''
Read in a section of rom with a given structure, output a list
'''
@ -65,7 +154,7 @@ def parse_struct(rom, offset, structure):
return out
def decompress_lzss(rom, start, header=False, length=None):
def decompress_lzss(rom: bytes, start: int, header: bool = False, length=None) -> bytes:
'''
Algorithm from http://slickproductions.org/slickwiki/index.php/Noisecross:Final_Fantasy_V_Compression
'''
@ -104,7 +193,7 @@ def decompress_lzss(rom, start, header=False, length=None):
return bytes(output[:uncompressed_length])
def decompress_lzss_FFVa(rom, start, header=False, length=None):
def decompress_lzss_FFVa(rom: bytes, start: int, header: bool = False, length=None) -> bytes:
'''
Oops, it's just GBA BIOS decompression functions
see https://web.archive.org/web/20130323133944/http://nocash.emubase.de/gbatek.htm#biosdecompressionfunctions
@ -141,7 +230,7 @@ def decompress_lzss_FFVa(rom, start, header=False, length=None):
return bytes(output[:uncompressed_length])
def findall(rom, string):
def findall(rom: bytes, string: str) -> list[int]:
results = []
start = 0
while True:
@ -152,7 +241,7 @@ def findall(rom, string):
start = val + 1
def parse_ips(data):
def parse_ips(data: bytes):
assert data[:5] == b'PATCH' and data[-3:] == b'EOF', 'File header and footer missing!'
patches = {}
ptr = 5
@ -168,15 +257,3 @@ def parse_ips(data):
ptr += 8
patches[address] = payload
return patches
if __name__ == '__main__':
with open('2564 - Final Fantasy V Advance (U)(Independent).gba', 'rb') as file:
ROM = file.read()
landmark = ROM.find(b'FINAL FANTASY V ADVANCE SYGMAB')
try:
with open('Final Fantasy V Advance (Europe) (En,Fr,De,Es,It)-spritehack.ips', 'rb') as file:
spritehack_ips = file.read()
print('spritehack_ips loaded')
except:
pass

View File

@ -17,7 +17,7 @@
import os
from array import array
from struct import unpack
import includes.const as const
import includes.ff5.const as ff5const
pyqt_version = 0
skip_pyqt5 = "PYQT4" in os.environ
@ -155,7 +155,7 @@ def create_tritile(data):
img = QImage(16, 12, QImage.Format_Indexed8)
imgbits = img.bits()
imgbits.setsize(img.byteCount())
img.setColorTable(const.dialogue_palette)
img.setColorTable(ff5const.dialogue_palette)
tile = array('B', range(192))
for p, row, b in [(p,j,b) for p in range(2) for j in range(12) for b in reversed(range(8))]:
tile[(7-b) + (row*16) + (p*8)] = (data[row + (p*12)] >> b & 1)
@ -177,7 +177,7 @@ def create_quadtile(data, ltr=False):
del painter
return QPixmap.fromImage(img)
def generate_glyphs(rom, offset, num=0x100, palette=const.small_palette):
def generate_glyphs(rom, offset, num=0x100, palette=ff5const.small_palette):
spritelist = []
for i in range(num):
j = offset + (i*16)

View File

@ -25,7 +25,7 @@ Trailing 8 bytes are 16 4bit nibbles that make up the compressed samples.
import sys
from midiutil import MIDIFile
from includes.helpers import indirect, hex
from includes.const import BGM_Tracks_Safe
from includes.ff5.const import BGM_Tracks_Safe
import struct
import wave