From 55e88028351fb9fa69143a6f9d727dcea32c6305 Mon Sep 17 00:00:00 2001 From: Luke Hubmayer-Werner Date: Sat, 22 Jul 2023 22:15:55 +0930 Subject: [PATCH] First implementation of lazy-loading tab groups --- ff5reader.py | 365 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 223 insertions(+), 142 deletions(-) diff --git a/ff5reader.py b/ff5reader.py index e53716c..2cbbcb5 100755 --- a/ff5reader.py +++ b/ff5reader.py @@ -18,8 +18,9 @@ import sys import re -from struct import unpack import time +from functools import cached_property +from struct import unpack from typing import Iterable from includes.helpers import * @@ -33,6 +34,7 @@ from includes.snestile import ( ) from includes.snes import * import includes.ff5.const as const +from includes.ff5.files import ROM_RPGe, ROM_SNES from includes.ff5.strings import StringBlock, RPGe_Dialogue_Width from includes.ff5.strings import Strings as FFVStrings import includes.ff5.structs as FFVStructs @@ -49,102 +51,122 @@ class FF5Reader(QMainWindow): ''' Main GUI class ''' - def __init__(self): - QMainWindow.__init__(self, None) - perfcount() - print('Reading ROMs') - 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) - print(len(ROM_FF6jp), filename_jp_ff6) - perfcount() - print('Generating Glyphs') - self.glyph_sprites = { - 'glyphs_en_s': generate_glyphs(ROM_en, 0x11F000), - 'glyphs_en_l': generate_glyphs_large(ROM_en, 0x03E800), - 'glyphs_jp_s': generate_glyphs(ROM_jp, 0x11F000), - 'glyphs_jp_l': generate_glyphs_large(ROM_jp, 0x03E800), - 'glyphs_kanji': generate_glyphs_large(ROM_jp, 0x1BD000, 0x1AA), # Kanji are unchanged in EN version - } - perfcount() + @cached_property + def battle_strips_ff4(self): + return ff4.make_character_battle_sprites(self.ROM_FF4jp) - imglist_headers = ['ID', 'EN Pointer', 'EN Address', 'EN String', 'EN Img', 'JP Pointer', 'JP Address', 'JP String', 'JP Img'] + @cached_property + def field_strips_ff4(self): + return ff4.make_character_field_sprites(self.ROM_FF4jp) - print('Generating String Images') - string_images = {k: _make_string_img_list(*FFVStrings.blocks_SNES_RPGe[k], large=config.get('dialog'), **self.glyph_sprites) for k,config in FFVStrings.config.items()} - ends_in_digit = re.compile('^([\w_]+)(\d+)') - for k in sorted(list(string_images.keys())): # Pre-generate keys as we destructively iterate the dict - if m := ends_in_digit.match(k): - k0 = m[1] - n = int(m[2]) - print(f'Collapsing strings list {k} into {k0}') - string_images[k0] += string_images.pop(k) - perfcount() + @cached_property + def portraits_ff4(self): + return ff4.make_character_portrait_sprites(self.ROM_FF4jp) + @cached_property + def battle_strips_ff6(self): + return ff6.make_character_battle_sprites(self.ROM_FF6jp) - tileset_headers = ("ID", "Offset", "Pointer", "Expected Length") - tileset_data = [] - for i in range(0x1C): - offset = 0x0F0000 + (i*2) - pointer = 0x0F0000 + indirect(ROM_en, offset) - length = indirect(ROM_en, offset+2) - indirect(ROM_en, offset) - tileset_data.append((hex(i, 2), hex(offset, 6), hex(pointer, 6), hex(length, 4))) + @cached_property + def portraits_ff6(self): + return ff6.make_character_portrait_sprites(self.ROM_FF6jp) - npc_layers = [] - offset = 0x0E59C0 - for layer in range(const.npc_layer_count): - i = offset + (layer*2) - start = indirect(ROM_en, i) + offset - next = indirect(ROM_en, i+2) + offset - npcs = (next - start) // 7 - for npc in range(npcs): - address = start + (npc*7) - npc_layers.append([hex(i, 6), hex(layer, 3)] + parse_struct(ROM_en, address, const.npc_layer_structure)) + @cached_property + def battle_bgs(self): + return make_battle_backgrounds(ROM_SNES) + @cached_property + def battle_strips(self): + return make_character_battle_sprites(ROM_RPGe) - perfcount() - print('Generating map tiles') - worldmap_palettes = [generate_palette(ROM_jp, 0x0FFCC0+(i*0x100), length=0x160, transparent=True) for i in range(3)] - world_tiles = [make_worldmap_blocks(ROM_jp, 0x0FF0C0+(i*0x300), 0x1B8000+(i*0x2000), 0x0FF9C0+(i*0x100)) for i in range(3)] - #worldpixmaps = [make_worldmap_pixmap(ROM_jp, i, 0x0FFCC0+(t*0x100), world_tiles[t]) for i, t in enumerate([0, 1, 0, 2, 2])] - world_blocks_pixmaps = [] - for i, tiles in enumerate(world_tiles): + @cached_property + def status_strips(self): + return make_character_status_sprites(ROM_RPGe) + + @cached_property + def enemy_sprites(self): + return make_enemy_sprites(ROM_RPGe) + + @cached_property + def enemy_sprite_data(self): + return FFVStructs.EnemySprite.get_data(ROM_RPGe) + + @cached_property + def enemy_sprites_named(self): + return [stack_labels(s, d[-2]) for s, d in zip(self.enemy_sprites, self.enemy_sprite_data)] + + @cached_property + def field_tiles(self): + return make_all_field_tiles(ROM_SNES) + + @cached_property + def field_minitiles(self): + return make_all_field_minitiles(ROM_SNES) + + @cached_property + def fieldmap_tiles(self): + st_field_tiles = [stitch_tileset(ts) for ts in self.field_tiles] + st_field_minitiles = [stitch_tileset(ts) for ts in self.field_minitiles] + return [make_field_map_tile_pixmap(ROM_SNES, i, st_field_tiles, st_field_minitiles) for i in range(const.zone_count)] + + @cached_property + def glyph_sprites(self): + return { + 'glyphs_en_s': generate_glyphs(ROM_RPGe, 0x11F000), + 'glyphs_en_l': generate_glyphs_large(ROM_RPGe, 0x03E800), + 'glyphs_jp_s': generate_glyphs(ROM_SNES, 0x11F000), + 'glyphs_jp_l': generate_glyphs_large(ROM_SNES, 0x03E800), + 'glyphs_kanji': generate_glyphs_large(ROM_SNES, 0x1BD000, 0x1AA), # Kanji are unchanged in EN version + } + + @cached_property + def worldmap_palettes(self): + return [generate_palette(ROM_SNES, 0x0FFCC0+(i*0x100), length=0x160, transparent=True) for i in range(3)] + + @cached_property + def worldmap_tiles(self): + return make_worldmap_tiles_pixmap(ROM_SNES, 0x1B8000, 0x0FF9C0, 0x0FFCC0) + \ + make_worldmap_tiles_pixmap(ROM_SNES, 0x1BA000, 0x0FFAC0, 0x0FFDC0) + \ + make_worldmap_tiles_pixmap(ROM_SNES, 0x1BC000, 0x0FFBC0, 0x0FFEC0, length=128) + + @cached_property + def world_tiles(self): + return [make_worldmap_blocks(ROM_SNES, 0x0FF0C0+(i*0x300), 0x1B8000+(i*0x2000), 0x0FF9C0+(i*0x100)) for i in range(3)] + + @cached_property + def world_blocks_pixmaps(self): + output = [] + for i, tiles in enumerate(self.world_tiles): a = [] for t in tiles: - t.setColorTable(worldmap_palettes[i]) + t.setColorTable(self.worldmap_palettes[i]) a.append(QPixmap.fromImage(t)) - world_blocks_pixmaps.append(a) - world_tile_stitches = [stitch_tileset_px(t) for t in world_blocks_pixmaps] - worldpixmaps = [make_worldmap_pixmap2(ROM_jp, i, world_tile_stitches[t]) for i, t in enumerate([0, 1, 0, 2, 2])] - perfcount() - worldmap_tiles = make_worldmap_tiles_pixmap(ROM_jp, 0x1B8000, 0x0FF9C0, 0x0FFCC0) - worldmap_tiles += make_worldmap_tiles_pixmap(ROM_jp, 0x1BA000, 0x0FFAC0, 0x0FFDC0) - worldmap_tiles += make_worldmap_tiles_pixmap(ROM_jp, 0x1BC000, 0x0FFBC0, 0x0FFEC0, length=128) - perfcount() + output.append(a) + return output - field_tiles = make_all_field_tiles(ROM_jp) - field_minitiles = make_all_field_minitiles(ROM_jp) - perfcount() - st_field_tiles = [stitch_tileset(ts) for ts in field_tiles] - st_field_minitiles = [stitch_tileset(ts) for ts in field_minitiles] - perfcount() - fieldmap_tiles = [make_field_map_tile_pixmap(ROM_jp, i, st_field_tiles, st_field_minitiles) for i in range(const.zone_count)] + @cached_property + def worldpixmaps(self): + world_tile_stitches = [stitch_tileset_px(t) for t in self.world_blocks_pixmaps] + return [make_worldmap_pixmap2(ROM_SNES, i, world_tile_stitches[t]) for i, t in enumerate([0, 1, 0, 2, 2])] + #return [make_worldmap_pixmap(ROM_SNES, i, 0x0FFCC0+(t*0x100), self.world_tiles[t]) for i, t in enumerate([0, 1, 0, 2, 2])] + + @cached_property + def zone_pxs_and_field_blocks(self): perfcount() print('Generating field map blocks') - zones = [parse_zone(ROM_jp, i) for i in range(const.zone_count)] - field_blocksets = [get_field_map_block_layouts(ROM_jp, i) for i in range(28)] + zones = [parse_zone(ROM_SNES, i) for i in range(const.zone_count)] + field_blocksets = [get_field_map_block_layouts(ROM_SNES, i) for i in range(28)] perfcount() - blockmaps = get_blockmaps(ROM_jp) + + blockmaps = get_blockmaps(ROM_SNES) field_blocks = [] zone_pxs = [] block_cache = {'misses': 0, 'p_hits': 0, 'i_hits': 0} zone_px_cache = {'misses': 0, 'hits': 0} - fm_blocks = [make_field_map_blocks_px(ROM_jp, z, field_tiles, field_minitiles, field_blocksets, block_cache) for z in zones] - #fm_blocks = [make_field_map_blocks_px2(ROM_jp, z, field_tiles, field_minitiles, field_blocksets, block_cache) for z in zones] + fm_blocks = [make_field_map_blocks_px(ROM_SNES, z, self.field_tiles, self.field_minitiles, field_blocksets, block_cache) for z in zones] + #fm_blocks = [make_field_map_blocks_px2(ROM_SNES, z, self.field_tiles, self.field_minitiles, field_blocksets, block_cache) for z in zones] print('Block cache results: {misses} misses, {p_hits} full hits, {i_hits} palette misses'.format(**block_cache)) perfcount() for i, z in enumerate(zones): @@ -156,81 +178,70 @@ class FF5Reader(QMainWindow): perfcount() del block_cache del zone_px_cache + return zone_pxs, field_blocks - print('Generating Battle backgrounds') - battle_bgs = make_battle_backgrounds(ROM_jp) + @cached_property + def zone_pxs(self): + return self.zone_pxs_and_field_blocks[0] + + @cached_property + def field_blocks(self): + return self.zone_pxs_and_field_blocks[1] + + def __init__(self): + QMainWindow.__init__(self, None) perfcount() - print('Generating other sprites') - self.battle_strips = make_character_battle_sprites(ROM_en) - status_strips = make_character_status_sprites(ROM_en) - enemy_sprites = make_enemy_sprites(ROM_en) - enemy_sprite_data = FFVStructs.EnemySprite.get_data(ROM_en) - enemy_sprites_named = [stack_labels(s, d[-2]) for s, d in zip(enemy_sprites, enemy_sprite_data)] + print('Reading ROMs') + self.ROM_FF4jp = load_raw(filename_jp_ff4) + self.ROM_FF6jp = load_raw(filename_jp_ff6) + print(len(self.ROM_FF4jp), filename_jp_ff4) + print(len(self.ROM_FF6jp), filename_jp_ff6) perfcount() - print('Generating FF4 and FF6 stuff') - self.battle_strips_ff4 = ff4.make_character_battle_sprites(ROM_FF4jp) - self.field_strips_ff4 = ff4.make_character_field_sprites(ROM_FF4jp) - self.portraits_ff4 = ff4.make_character_portrait_sprites(ROM_FF4jp) - self.battle_strips_ff6 = ff6.make_character_battle_sprites(ROM_FF6jp) - self.portraits_ff6 = ff6.make_character_portrait_sprites(ROM_FF6jp) + print('Generating String Images') + imglist_headers = ['ID', 'EN Pointer', 'EN Address', 'EN String', 'EN Img', 'JP Pointer', 'JP Address', 'JP String', 'JP Img'] + string_images = {k: _make_string_img_list(*FFVStrings.blocks_SNES_RPGe[k], large=config.get('dialog'), **self.glyph_sprites) for k,config in FFVStrings.config.items()} + ends_in_digit = re.compile('^([\w_]+)(\d+)') + for k in sorted(list(string_images.keys())): # Pre-generate keys as we destructively iterate the dict + if m := ends_in_digit.match(k): + k0 = m[1] + n = int(m[2]) + print(f'Collapsing strings list {k} into {k0}') + string_images[k0] += string_images.pop(k) perfcount() + print('Generating Tileset and NPC Layer offsets') + tileset_headers = ("ID", "Offset", "Pointer", "Expected Length") + tileset_data = [] + for i in range(0x1C): + offset = 0x0F0000 + (i*2) + pointer = 0x0F0000 + indirect(ROM_RPGe, offset) + length = indirect(ROM_RPGe, offset+2) - indirect(ROM_RPGe, offset) + tileset_data.append((hex(i, 2), hex(offset, 6), hex(pointer, 6), hex(length, 4))) + npc_layers = [] + offset = 0x0E59C0 + for layer in range(const.npc_layer_count): + i = offset + (layer*2) + start = indirect(ROM_RPGe, i) + offset + next = indirect(ROM_RPGe, i+2) + offset + npcs = (next - start) // 7 + for npc in range(npcs): + address = start + (npc*7) + npc_layers.append([hex(i, 6), hex(layer, 3)] + parse_struct(ROM_RPGe, address, const.npc_layer_structure)) + perfcount() print('Creating Qt Widgets') self.gamewidget = QTabWidget() - self.ff4widget = QTabWidget() - self.ff5widget = QTabWidget() - self.ff6widget = QTabWidget() - self.gamewidget.addTab(self.ff5widget, 'FFV') - self.gamewidget.addTab(self.ff4widget, 'FFIV') - self.gamewidget.addTab(self.ff6widget, 'FFVI') - strings_tab = QTabWidget() - structs_tab = QTabWidget() - sprites_tab = QTabWidget() - backgrounds_tab = QTabWidget() - self.ff5widget.addTab(strings_tab, 'Strings') - self.ff5widget.addTab(structs_tab, 'Structs') - self.ff5widget.addTab(sprites_tab, 'Images') - self.ff5widget.addTab(backgrounds_tab, 'Backgrounds') + self.gamewidget.addTab(welcome := QLabel('Welcome to FF5Reader, click one of the game tabs at the top to get started.'), 'Welcome') + self.gamewidget.addTab(ff5widget := QTabWidget(), 'FFV') + self.gamewidget.addTab(ff4widget := QTabWidget(), 'FFIV') + self.gamewidget.addTab(ff6widget := QTabWidget(), 'FFVI') - sprites_tab.addTab(make_px_table(self.glyph_sprites['glyphs_en_s'], scale=4), 'Glyphs (EN)') - sprites_tab.addTab(make_px_table(self.glyph_sprites['glyphs_en_l'], scale=2), 'Glyphs (Dialogue EN)') - sprites_tab.addTab(make_px_table(self.glyph_sprites['glyphs_jp_s'], scale=4), 'Glyphs (JP)') - sprites_tab.addTab(make_px_table(self.glyph_sprites['glyphs_jp_l'], scale=2), 'Glyphs (Large JP)') - sprites_tab.addTab(make_px_table(self.glyph_sprites['glyphs_kanji'], scale=2),'Glyphs (Kanji)') - sprites_tab.addTab(make_px_table(self.battle_strips, cols=22, scale=2), 'Character Battle Sprites') - sprites_tab.addTab(make_px_table(status_strips, cols=22, scale=2), 'Status Sprites') - sprites_tab.addTab(make_px_table(enemy_sprites_named, cols=32, scale=1), 'Enemy Sprites') - - backgrounds_tab.addTab(make_px_table(worldmap_tiles, cols=16, scale=4), 'Worldmap Tiles') - backgrounds_tab.addTab(make_px_table(world_blocks_pixmaps[0], cols=16, scale=4), 'World 1 Blocks') - backgrounds_tab.addTab(make_px_table(world_blocks_pixmaps[1], cols=16, scale=4), 'World 2 Blocks') - backgrounds_tab.addTab(make_px_table(world_blocks_pixmaps[2], cols=16, scale=4), 'Underwater Blocks') - backgrounds_tab.addTab(make_px_table(worldpixmaps, cols=1, scale=1, large=True), 'Worldmaps') - backgrounds_tab.addTab(make_px_table(fieldmap_tiles, cols=16, scale=1), 'Fieldmap Tiles') - backgrounds_tab.addTab(make_px_table(field_blocks, cols=16, scale=1), 'Field Blocks') - backgrounds_tab.addTab(make_px_table(zone_pxs, cols=4, scale=1, large=1, basicrows=True), 'Zone') - backgrounds_tab.addTab(make_px_table(battle_bgs, cols=8, scale=1), 'Battle BGs') - - self.ff4widget.addTab(make_px_table(self.battle_strips_ff4, cols=16, scale=2), 'Character Battle Sprites') - self.ff4widget.addTab(make_px_table(self.portraits_ff4, cols=14, scale=2), 'Character Portraits') - self.ff4widget.addTab(make_px_table(self.field_strips_ff4, cols=17, scale=2), 'Character Field Sprites') - self.ff6widget.addTab(make_px_table(self.battle_strips_ff6, cols=32, scale=2), 'Character Sprites') - self.ff6widget.addTab(make_px_table(self.portraits_ff6, cols=19, scale=2), 'Character Portraits') - - - structs_tab.addTab(make_table(FFVStructs.ZoneData.get_headers(), FFVStructs.ZoneData.get_data(ROM_en), True), 'Zones') - structs_tab.addTab(make_table(FFVStructs.BattleBackground.get_headers(), FFVStructs.BattleBackground.get_data(ROM_jp), True), 'BattleBGs') - structs_tab.addTab(make_table(FFVStructs.EnemySprite.get_headers(), enemy_sprite_data, True), 'Enemy Sprites') - structs_tab.addTab(make_table(tileset_headers, tileset_data, True), 'Tilesets') - structs_tab.addTab(make_table(const.npc_layer_headers, npc_layers, True), 'NPC Layers') - - # Strings tabs - for k, images in string_images.items(): - scale = 1 if FFVStrings.config[k].get('dialog') else 2 - caption = ' '.join(f'{w[0].upper()}{w[1:]}' for w in k.split('_')) - strings_tab.addTab(make_table(imglist_headers, images, row_labels=False, scale=scale), caption) + ff5widget.addTab(strings_tab := QTabWidget(), 'Strings') + ff5widget.addTab(glyphs_tab := QTabWidget(), 'Glyphs') + ff5widget.addTab(structs_tab := QTabWidget(), 'Structs') + ff5widget.addTab(sprites_tab := QTabWidget(), 'Sprites') + ff5widget.addTab(backgrounds_tab := QTabWidget(), 'Backgrounds') self.string_decoder = QWidget() self.decoder_input = QLineEdit() @@ -240,6 +251,76 @@ class FF5Reader(QMainWindow): self.string_decoder.setLayout(self.decoder_layout) strings_tab.addTab(self.string_decoder, 'String Decoder') + def load_tab_strings(): + for k, images in string_images.items(): + conf = FFVStrings.config[k] + scale = 1 if conf.get('dialog') else 2 + caption = ' '.join(f'{w[0].upper()}{w[1:]}' for w in k.split('_')) + strings_tab.addTab(tab := make_table(imglist_headers, images, row_labels=False, scale=scale), caption) + tab.setColumnHidden(1, conf.get('rpge_ptr_offset') is None) # Hide EN Pointer if not indirect + tab.setColumnHidden(5, conf.get('snes_ptr_offset') is None) # Hide JP Pointer if not indirect + tab.resizeColumnsToContents() + for i in range(len(imglist_headers)): + if tab.columnWidth(i) > 360: + tab.setColumnWidth(i, 360) + + def load_tab_glyphs(): + glyphs_tab.addTab(make_px_table(self.glyph_sprites['glyphs_en_s'], scale=3), 'Small EN') + glyphs_tab.addTab(make_px_table(self.glyph_sprites['glyphs_en_l'], scale=2), 'Dialogue EN') + glyphs_tab.addTab(make_px_table(self.glyph_sprites['glyphs_jp_s'], scale=3), 'Small JP') + glyphs_tab.addTab(make_px_table(self.glyph_sprites['glyphs_jp_l'], scale=2), 'Dialogue JP') + glyphs_tab.addTab(make_px_table(self.glyph_sprites['glyphs_kanji'], scale=2),'Kanji') + + def load_tab_structs(): + structs_tab.addTab(make_table(FFVStructs.ZoneData.get_headers(), FFVStructs.ZoneData.get_data(ROM_RPGe), True), 'Zones') + structs_tab.addTab(make_table(FFVStructs.BattleBackground.get_headers(), FFVStructs.BattleBackground.get_data(ROM_SNES), True), 'BattleBGs') + structs_tab.addTab(make_table(FFVStructs.EnemySprite.get_headers(), self.enemy_sprite_data, True), 'Enemy Sprites') + structs_tab.addTab(make_table(tileset_headers, tileset_data, True), 'Tilesets') + structs_tab.addTab(make_table(const.npc_layer_headers, npc_layers, True), 'NPC Layers') + + def load_tab_sprites(): + sprites_tab.addTab(make_px_table(self.battle_strips, cols=22, scale=2), 'Character Battle Sprites') + sprites_tab.addTab(make_px_table(self.status_strips, cols=22, scale=2), 'Status Sprites') + sprites_tab.addTab(make_px_table(self.enemy_sprites_named, cols=32, scale=1), 'Enemy Sprites') + + def load_tab_backgrounds(): + backgrounds_tab.addTab(make_px_table(self.worldmap_tiles, cols=16, scale=4), 'Worldmap Tiles') + backgrounds_tab.addTab(make_px_table(self.world_blocks_pixmaps[0], cols=16, scale=3), 'World 1 Blocks') + backgrounds_tab.addTab(make_px_table(self.world_blocks_pixmaps[1], cols=16, scale=3), 'World 2 Blocks') + backgrounds_tab.addTab(make_px_table(self.world_blocks_pixmaps[2], cols=16, scale=3), 'Underwater Blocks') + backgrounds_tab.addTab(make_px_table(self.worldpixmaps, cols=1, scale=1, large=True), 'Worldmaps') + backgrounds_tab.addTab(make_px_table(self.fieldmap_tiles, cols=16, scale=1), 'Fieldmap Tiles') + backgrounds_tab.addTab(make_px_table(self.field_blocks, cols=16, scale=1), 'Field Blocks') + backgrounds_tab.addTab(make_px_table(self.zone_pxs, cols=4, scale=1, large=1, basicrows=True), 'Zone') + backgrounds_tab.addTab(make_px_table(self.battle_bgs, cols=8, scale=1), 'Battle BGs') + + tab_loaders = [load_tab_strings, load_tab_glyphs, load_tab_structs, load_tab_sprites, load_tab_backgrounds] + def load_tab(index: int): + if fn := tab_loaders[index]: + fn() + tab_loaders[index] = None + ff5widget.currentChanged.connect(load_tab) + + def load_ff5(): + load_tab(ff5widget.currentIndex()) + pass + + def load_ff4(): + ff4widget.addTab(make_px_table(self.battle_strips_ff4, cols=16, scale=2), 'Character Battle Sprites') + ff4widget.addTab(make_px_table(self.portraits_ff4, cols=14, scale=2), 'Character Portraits') + ff4widget.addTab(make_px_table(self.field_strips_ff4, cols=17, scale=2), 'Character Field Sprites') + + def load_ff6(): + ff6widget.addTab(make_px_table(self.battle_strips_ff6, cols=32, scale=2), 'Character Sprites') + ff6widget.addTab(make_px_table(self.portraits_ff6, cols=19, scale=2), 'Character Portraits') + + game_tab_loaders = [None, load_ff5, load_ff4, load_ff6] + def load_game_tab(index: int): + if fn := game_tab_loaders[index]: + fn() + game_tab_loaders[index] = None + self.gamewidget.currentChanged.connect(load_game_tab) + layout = QHBoxLayout() layout.addWidget(self.gamewidget) self.main_widget = QWidget(self)