diff --git a/ff5reader.py b/ff5reader.py index 27a6e33..fe80bc3 100755 --- a/ff5reader.py +++ b/ff5reader.py @@ -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 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] + if glyphs_kanji is not None: # jp + 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 0x1E00 <= j < 0x1FAA: # Kanji live in this range + painter.drawPixmap(x, y+2, glyphs_kanji[j-0x1E00]) + x += 16 + 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 - if large: - str_en, img_en = make_string_img_large(rom_e[en_start:en_end], glyphs_en_l, glyphs_kanji, macros_en) - 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]) + for id in range(num): + str_en = en.decoded[id] + str_jp = jp.decoded[id] + if large: + 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: + 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 diff --git a/includes/const.py b/includes/const.py deleted file mode 100644 index 2d1dc53..0000000 --- a/includes/const.py +++ /dev/null @@ -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 . -''' -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] = ['←','→','+','、', '◯', '『', 'F', '°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 YLR - 0xD9: [0x93, 0x8D], # expands to ない - # 0xDA-0xDC are HMP - 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 diff --git a/includes/ff5/BGM_Titles.txt b/includes/ff5/BGM_Titles.txt new file mode 100644 index 0000000..31cdece --- /dev/null +++ b/includes/ff5/BGM_Titles.txt @@ -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 diff --git a/includes/ff5/Glyphs_dialog_RPGe.txt b/includes/ff5/Glyphs_dialog_RPGe.txt new file mode 100644 index 0000000..eedb266 --- /dev/null +++ b/includes/ff5/Glyphs_dialog_RPGe.txt @@ -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 +← +→ ++ +、 +◯ +『 +F +°C +・ +( +) + + + + + + + + + + + + + + + + + + + + + diff --git a/includes/ff5/Glyphs_dialog_SNES.txt b/includes/ff5/Glyphs_dialog_SNES.txt new file mode 100644 index 0000000..eee56e1 --- /dev/null +++ b/includes/ff5/Glyphs_dialog_SNES.txt @@ -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 +← +→ ++ +、 +◯ +『 +F +°C +・ +( +) +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  diff --git a/includes/ff5/Glyphs_dialog_kanji_SNES.txt b/includes/ff5/Glyphs_dialog_kanji_SNES.txt new file mode 100644 index 0000000..5306635 --- /dev/null +++ b/includes/ff5/Glyphs_dialog_kanji_SNES.txt @@ -0,0 +1,426 @@ +王 +行 +力 +様 +飛 +世 +界 +封 +入 +城 +竜 +士 +船 +印 +海 +父 +人 +見 +魔 +物 +大 +者 +本 +何 +戦 +出 +気 +手 +言 +石 +守 +辺 +無 +心 +間 +前 +風 +帰 +私 +生 +年 +武 +器 +兵 +火 +使 +中 +急 +時 +森 +来 +砂 +動 +女 +待 +臣 +地 +助 +当 +今 +上 +悪 +泉 +騎 +思 +持 +変 +水 +塔 +草 +仲 +復 +目 +作 +分 +知 +機 +一 +姫 +最 +聞 +械 +神 +流 +乗 +取 +町 +殿 +事 +空 +勇 +村 +早 +子 +格 +納 +伝 +消 +書 +長 +話 +合 +所 +場 +脱 +北 +後 +全 +忍 +獣 +詩 +吟 +落 +自 +階 +説 +残 +親 +活 +休 +姉 +破 +土 +度 +記 +発 +赤 +侍 +死 +国 +旅 +完 +小 +林 +古 +図 +礼 +商 +島 +邪 +部 +狩 +精 +姿 +防 +向 +先 +解 +板 +下 +台 +賢 +対 +木 +成 +命 +配 +法 +飲 +回 +願 +門 +東 +開 +貸 +増 +危 +舘 +道 +身 +老 +西 +近 +層 +第 +青 +光 +読 +外 +理 +強 +同 +谷 +負 +意 +学 +攻 +屋 +体 +運 +河 +聖 +必 +南 +方 +黒 +絶 +食 +艇 +山 +穴 +名 +受 +暁 +傷 +少 +鼻 +倉 +然 +的 +男 +用 +酒 +安 +現 +代 +立 +具 +育 +続 +通 +会 +庫 +飼 +日 +窟 +砲 +広 +化 +博 +以 +兄 +室 +洞 +別 +番 +昔 +住 +吸 +去 +宝 +毒 +売 +好 +枝 +踊 +月 +巣 +底 +明 +感 +宿 +召 +喚 +決 +形 +団 +異 +初 +険 +家 +息 +連 +集 +墓 +著 +跡 +遺 +内 +悲 +教 +返 +頭 +再 +高 +借 +登 +走 +過 +敵 +鏡 +愛 +魚 +燃 +幅 +音 +血 +滝 +次 +閉 +求 +声 +箱 +軍 +泣 +港 +冊 +秒 +憶 +潜 +爆 +恋 +石 +左 +役 +放 +衛 +母 +建 +角 +信 +師 +元 +失 +字 +歩 +遊 +頂 +震 +収 +追 +暗 +橋 +起 +約 +束 +文 +反 +絵 +能 +歌 +弱 +境 +波 +針 +千 +炎 +望 +雨 +金 +希 +花 +不 +白 +剣 +陸 +勝 +烏 +笛 +究 +三 +探 +夜 +病 +半 +美 +敗 +友 +研 +予 +支 +々 +除 +数 +店 +翼 +充 +填 +薬 +弟 +経 +験 +値 +闘 +得 +闇 +混 +乱 +点 +速 +閃 +射 +斬 +鉄 +宣 +告 +電 +磁 +圧 +昇 +重 +倍 +妖 +刀 +拡 +散 +影 +御 +正 +漠 +特 +胸 +永 +遠 +議 +浮 +眠 +逃 +滅 +囗 +極 +湖 +利 +周 +緑 +切 +紙 +胸 +平 +和 +宇 +宙 +則 +孤 +溝 +  diff --git a/includes/ff5/Glyphs_small_RPGe.txt b/includes/ff5/Glyphs_small_RPGe.txt new file mode 100644 index 0000000..5f3ffab --- /dev/null +++ b/includes/ff5/Glyphs_small_RPGe.txt @@ -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] + + + + + + + + + + + diff --git a/includes/ff5/Glyphs_small_SNES.txt b/includes/ff5/Glyphs_small_SNES.txt new file mode 100644 index 0000000..5b4fe13 --- /dev/null +++ b/includes/ff5/Glyphs_small_SNES.txt @@ -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] + + + + + + + + + + + diff --git a/includes/ff5/Glyphs_small_dakuten_SNES.txt b/includes/ff5/Glyphs_small_dakuten_SNES.txt new file mode 100644 index 0000000..86852b5 --- /dev/null +++ b/includes/ff5/Glyphs_small_dakuten_SNES.txt @@ -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] + + + + + + + + + + + diff --git a/includes/ff5/__init__.py b/includes/ff5/__init__.py new file mode 100644 index 0000000..59ae30d --- /dev/null +++ b/includes/ff5/__init__.py @@ -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)] diff --git a/includes/ff5/const.py b/includes/ff5/const.py new file mode 100644 index 0000000..bb9b7af --- /dev/null +++ b/includes/ff5/const.py @@ -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 . +''' +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 diff --git a/includes/ff5/files.py b/includes/ff5/files.py new file mode 100644 index 0000000..a236374 --- /dev/null +++ b/includes/ff5/files.py @@ -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) diff --git a/includes/ff5/string_blocks.tsv b/includes/ff5/string_blocks.tsv new file mode 100644 index 0000000..7316598 --- /dev/null +++ b/includes/ff5/string_blocks.tsv @@ -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 \ No newline at end of file diff --git a/includes/ff5/strings.py b/includes/ff5/strings.py new file mode 100644 index 0000000..0fa4eae --- /dev/null +++ b/includes/ff5/strings.py @@ -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 YLR + 0xD9: [0x93, 0x8D], # expands to ない + # 0xDA-0xDC are HMP + 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()} diff --git a/includes/helpers.py b/includes/helpers.py index 34c4814..cabe06d 100644 --- a/includes/helpers.py +++ b/includes/helpers.py @@ -1,182 +1,259 @@ ''' - This file is part of ff5reader. + 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 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. + 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 . + You should have received a copy of the GNU General Public License + along with ff5reader. If not, see . ''' +from ast import literal_eval HEX_PREFIX = '#' # '#' '$' or '0x' are also nice -def divceil(numerator, denominator): - ''' - Reverse floor division for fast ceil - ''' - return -(-numerator // denominator) +def divceil(numerator: int, denominator: int) -> int: + ''' + Reverse floor division for fast ceil + ''' + return -(-numerator // denominator) -def hex_length(i): - ''' - String length of hexadecimal representation of integer - ''' - return divceil(i.bit_length(), 4) +def hex_length(i: int) -> int: + ''' + String length of hexadecimal representation of integer + ''' + return divceil(i.bit_length(), 4) -def hex(num, digits=2): - ''' - Consolidate hex formatting for consistency - ''' - #return '{:0{}X}₁₆'.format(num, digits) - return HEX_PREFIX + '{:0{}X}'.format(num, digits) +def hex(num: int, digits: int = 2) -> str: + ''' + Consolidate hex formatting for consistency + ''' + #return '{:0{}X}₁₆'.format(num, digits) + return HEX_PREFIX + '{:0{}X}'.format(num, digits) -def indirect(rom, start, length=2, endian='little'): - ''' - Read little-endian value at start address in rom - ''' - return int.from_bytes(rom[start:start+length], endian) +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 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 parse_struct(rom, offset, structure): - ''' - Read in a section of rom with a given structure, output a list - ''' - out = [hex(offset, 6)] - j = 0 - for title, length, handler in structure: - val = indirect(rom, offset+j, length=length) - if callable(handler): - out.append(handler(val)) - elif handler and val < len(handler): - out.append(handler[val]) - else: - out.append(hex(val, length*2)) - j += length - return out +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 decompress_lzss(rom, start, header=False, length=None): - ''' - Algorithm from http://slickproductions.org/slickwiki/index.php/Noisecross:Final_Fantasy_V_Compression - ''' - ptr = start - if length: - uncompressed_length = length - else: - uncompressed_length = indirect(rom, start) - ptr += 2 - output = [] - buffer = [0 for i in range(0x800)] - buffer_p = 0x07DE - while len(output) < uncompressed_length: - bitmap_byte = rom[ptr] - ptr += 1 - for i in range(8): - bit = (bitmap_byte >> i) & 1 - if bit: - b = rom[ptr] - ptr += 1 - output.append(b) - buffer[buffer_p] = b - buffer_p = (buffer_p+1) % 0x800 - else: - b1 = rom[ptr] - b2 = rom[ptr+1] - ptr += 2 - offset = b1|((b2 & 0xE0)<<3) - length = b2 & 0x1F - for j in range(length+3): - b = buffer[offset] - output.append(b) - buffer[buffer_p] = b - buffer_p = (buffer_p+1) % 0x800 - offset = (offset+1) % 0x800 - return bytes(output[:uncompressed_length]) +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 decompress_lzss_FFVa(rom, start, header=False, length=None): - ''' - Oops, it's just GBA BIOS decompression functions - see https://web.archive.org/web/20130323133944/http://nocash.emubase.de/gbatek.htm#biosdecompressionfunctions - ''' - ptr = start - if length: - uncompressed_length = length - else: - uncompressed_length = indirect(rom, start, endian='big') - ptr += 2 - output = [] - while len(output) < uncompressed_length: - bitmap_byte = rom[ptr] - ptr += 1 - for i in reversed(range(8)): - bit = (bitmap_byte >> i) & 1 - if not bit: - b = rom[ptr] - ptr += 1 - output.append(b) - else: - b1 = rom[ptr] - b2 = rom[ptr+1] - ptr += 2 - length = ((b1 & 0xF0) >> 4) + 3 - trackback = -1 - (b2 + ((b1 & 0x0F) << 8)) - try: - for j in range(length): - output.append(output[trackback]) - except: - print(len(output), f'0x{ptr:X}', f'0x{b1:02X}{b2:02X}', trackback) - raise - print(f'0x{ptr:X}') - return bytes(output[:uncompressed_length]) +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 findall(rom, string): - results = [] - start = 0 - while True: - val = rom.find(string, start) - if val < 0: - return results - results.append(val) - start = val + 1 +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 parse_ips(data): - assert data[:5] == b'PATCH' and data[-3:] == b'EOF', 'File header and footer missing!' - patches = {} - ptr = 5 - while ptr < len(data)-6: - address = int.from_bytes(data[ptr:ptr+3], 'big') - length = int.from_bytes(data[ptr+3:ptr+5], 'big') - if length > 0: - payload = data[ptr+5:ptr+5+length] - ptr += 5 + length - else: - repeats = data[ptr+5:ptr+7] - payload = data[ptr+7] * repeats - ptr += 8 - patches[address] = payload - return patches +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 -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 +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 + ''' + out = [hex(offset, 6)] + j = 0 + for title, length, handler in structure: + val = indirect(rom, offset+j, length=length) + if callable(handler): + out.append(handler(val)) + elif handler and val < len(handler): + out.append(handler[val]) + else: + out.append(hex(val, length*2)) + j += length + return out + + +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 + ''' + ptr = start + if length: + uncompressed_length = length + else: + uncompressed_length = indirect(rom, start) + ptr += 2 + output = [] + buffer = [0 for i in range(0x800)] + buffer_p = 0x07DE + while len(output) < uncompressed_length: + bitmap_byte = rom[ptr] + ptr += 1 + for i in range(8): + bit = (bitmap_byte >> i) & 1 + if bit: + b = rom[ptr] + ptr += 1 + output.append(b) + buffer[buffer_p] = b + buffer_p = (buffer_p+1) % 0x800 + else: + b1 = rom[ptr] + b2 = rom[ptr+1] + ptr += 2 + offset = b1|((b2 & 0xE0)<<3) + length = b2 & 0x1F + for j in range(length+3): + b = buffer[offset] + output.append(b) + buffer[buffer_p] = b + buffer_p = (buffer_p+1) % 0x800 + offset = (offset+1) % 0x800 + return bytes(output[:uncompressed_length]) + + +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 + ''' + ptr = start + if length: + uncompressed_length = length + else: + uncompressed_length = indirect(rom, start, endian='big') + ptr += 2 + output = [] + while len(output) < uncompressed_length: + bitmap_byte = rom[ptr] + ptr += 1 + for i in reversed(range(8)): + bit = (bitmap_byte >> i) & 1 + if not bit: + b = rom[ptr] + ptr += 1 + output.append(b) + else: + b1 = rom[ptr] + b2 = rom[ptr+1] + ptr += 2 + length = ((b1 & 0xF0) >> 4) + 3 + trackback = -1 - (b2 + ((b1 & 0x0F) << 8)) + try: + for j in range(length): + output.append(output[trackback]) + except: + print(len(output), f'0x{ptr:X}', f'0x{b1:02X}{b2:02X}', trackback) + raise + print(f'0x{ptr:X}') + return bytes(output[:uncompressed_length]) + + +def findall(rom: bytes, string: str) -> list[int]: + results = [] + start = 0 + while True: + val = rom.find(string, start) + if val < 0: + return results + results.append(val) + start = val + 1 + + +def parse_ips(data: bytes): + assert data[:5] == b'PATCH' and data[-3:] == b'EOF', 'File header and footer missing!' + patches = {} + ptr = 5 + while ptr < len(data)-6: + address = int.from_bytes(data[ptr:ptr+3], 'big') + length = int.from_bytes(data[ptr+3:ptr+5], 'big') + if length > 0: + payload = data[ptr+5:ptr+5+length] + ptr += 5 + length + else: + repeats = data[ptr+5:ptr+7] + payload = data[ptr+7] * repeats + ptr += 8 + patches[address] = payload + return patches diff --git a/includes/snestile.py b/includes/snestile.py index a4dcac5..96f6057 100644 --- a/includes/snestile.py +++ b/includes/snestile.py @@ -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) diff --git a/spc700analyser.py b/spc700analyser.py index ecc51c9..674155d 100755 --- a/spc700analyser.py +++ b/spc700analyser.py @@ -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