2018-03-19 17:13:04 +10:30
#!/usr/bin/python3 -i
2017-03-11 19:18:33 +10:30
'''
2018-03-26 16:27:03 +10:30
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 / > .
2017-03-11 19:18:33 +10:30
'''
import sys
import os
2017-03-18 13:52:33 +10:30
from struct import unpack
2017-03-17 16:56:19 +10:30
from itertools import chain
2017-03-11 19:18:33 +10:30
from array import array
2017-03-13 23:20:19 +10:30
import time
2018-03-26 16:27:03 +10:30
import functools
from includes . helpers import *
from includes . qthelpers import *
from includes . snestile import (
generate_glyphs , generate_glyphs_large , generate_palette ,
create_tile , create_tile_indexed ,
create_tile_mode7_compressed , create_tile_mode7_compressed_indexed ,
bg_color , bg_trans ,
Canvas , Canvas_Indexed
)
from includes . snes import *
import includes . const as const
import includes . ff4 as ff4
import includes . ff6 as ff6
2017-03-11 19:18:33 +10:30
pyqt_version = 0
2018-03-19 17:13:04 +10:30
skip_pyqt5 = ' PYQT4 ' in os . environ
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 '
2018-03-19 22:24:47 +10:30
filename_jp_ff6 = ' Final Fantasy VI (Japan).sfc '
2017-03-11 19:18:33 +10:30
if not skip_pyqt5 :
try :
from PyQt5 import QtGui , QtCore
2017-03-12 00:14:37 +10:30
from PyQt5 . QtGui import QIcon , QPalette , QColor , QFont , QFontInfo , QImage , QPixmap
2017-03-11 19:18:33 +10:30
from PyQt5 . QtWidgets import (
QApplication , QMainWindow , QFormLayout ,
QGridLayout , QHBoxLayout , QVBoxLayout ,
QAbstractItemView , QHeaderView , QListWidget ,
QListWidgetItem , QTabWidget , QTableWidget ,
QTableWidgetItem , QFrame , QScrollArea ,
QStackedWidget , QWidget , QCheckBox , QComboBox ,
QDoubleSpinBox , QGroupBox , QLineEdit ,
QPushButton , QRadioButton , QSpinBox ,
QStyleOptionButton , QToolButton , QProgressBar ,
QDialog , QColorDialog , QDialogButtonBox ,
QFileDialog , QInputDialog , QMessageBox ,
QAction , QActionGroup , QLabel , QMenu , QStyle ,
QSystemTrayIcon , QStyleOptionProgressBar
)
pyqt_version = 5
except ImportError :
print ( " Couldn ' t import Qt5 dependencies. "
2018-03-19 17:13:04 +10:30
' Make sure you installed the PyQt5 package. ' )
2017-03-13 23:20:19 +10:30
if pyqt_version == 0 :
2017-03-11 19:18:33 +10:30
try :
import sip
sip . setapi ( ' QVariant ' , 2 )
from PyQt4 import QtGui , QtCore
from PyQt4 . QtGui import (
QApplication , QMainWindow , QFormLayout ,
QGridLayout , QHBoxLayout , QVBoxLayout ,
QAbstractItemView , QHeaderView , QListWidget ,
QListWidgetItem , QTabWidget , QTableWidget ,
QTableWidgetItem , QFrame , QScrollArea ,
QStackedWidget , QWidget , QCheckBox ,
QComboBox , QDoubleSpinBox , QGroupBox ,
QLineEdit , QPushButton , QRadioButton ,
QSpinBox , QStyleOptionButton , QToolButton ,
QProgressBar , QDialog , QColorDialog ,
QDialogButtonBox , QFileDialog , QInputDialog ,
QMessageBox , QAction , QActionGroup ,
QLabel , QMenu , QStyle ,
QSystemTrayIcon , QIcon , QPalette , QColor ,
2017-03-12 00:14:37 +10:30
QValidator , QFont , QFontInfo , QImage , QPixmap
2017-03-11 19:18:33 +10:30
)
from PyQt4 . QtGui import QStyleOptionProgressBarV2 as QStyleOptionProgressBar
pyqt_version = 4
except ImportError :
print ( " Couldn ' t import Qt dependencies. "
2018-03-19 17:13:04 +10:30
' Make sure you installed the PyQt4 package. ' )
2017-03-11 19:18:33 +10:30
sys . exit ( - 1 )
class FF5Reader ( QMainWindow ) :
2018-03-26 16:27:03 +10:30
'''
2018-03-19 17:13:04 +10:30
Main GUI class
2018-03-26 16:27:03 +10:30
'''
2018-03-19 17:13:04 +10:30
def __init__ ( self ) :
QMainWindow . __init__ ( self , None )
2018-03-22 20:16:26 +10:30
perfcount ( )
2018-03-31 18:21:29 +10:30
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 ( )
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 ( )
2018-03-22 20:16:26 +10:30
print ( ' Generating Glyphs ' )
2018-03-26 16:27:03 +10:30
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
}
2018-03-31 18:21:29 +10:30
make_string_img_list = functools . partial ( _make_string_img_list , ROM_en , ROM_jp , * * self . glyph_sprites )
2018-03-22 20:16:26 +10:30
perfcount ( )
2018-03-19 17:13:04 +10:30
2018-03-27 01:11:40 +10:30
stringlist_headers = [ ' Address ' , ' ID ' , ' Name ' ]
imglist_headers = stringlist_headers + [ ' Img ' , ' Name JP ' , ' Img JP ' ]
2018-03-22 20:16:26 +10:30
print ( ' Generating Strings ' )
2018-03-19 17:13:04 +10:30
zone_names = make_string_img_list ( 0x107000 , 2 , 0x100 , start_str = 0x270000 , start_jp_str = 0x107200 , indirect = True , large = 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 )
2018-03-22 20:16:26 +10:30
perfcount ( )
2018-03-19 17:13:04 +10:30
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 )
2018-03-22 20:16:26 +10:30
perfcount ( )
2018-03-19 17:13:04 +10:30
2018-03-21 21:41:48 +10:30
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 ] )
2018-03-27 23:19:20 +10:30
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 ] )
2018-03-21 21:41:48 +10:30
2018-03-27 23:19:20 +10:30
zone_structure = [ ( ' NPC Layer ' , 2 , None ) , # 00 01
( ' Name ' , 1 , [ z [ 2 ] for z in zone_names ] ) , # 02
( ' ShadowFlags ' , 1 , None ) , # 03
2018-03-31 18:21:29 +10:30
( ' 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)
2018-03-27 23:19:20 +10:30
( ' 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
2018-03-19 17:13:04 +10:30
zone_headers = [ ' Address ' ] + [ z [ 0 ] for z in zone_structure ]
2018-03-24 22:53:18 +10:30
zone_data = [ parse_struct ( ROM_en , 0x0E9C00 + ( i * 0x1A ) , zone_structure ) for i in range ( const . zone_count ) ]
2018-03-27 01:11:40 +10:30
battle_bg_structure = [ ( ' Tileset ' , 1 , None ) ,
( ' Palette 1 ' , 1 , None ) ,
( ' Palette 2 ' , 1 , None ) ,
( ' Tilemap ' , 1 , None ) ,
( ' TilemapFlip ' , 1 , None ) ,
( hex ( 5 , 1 ) , 1 , None ) ,
( ' Animation ' , 1 , None ) ,
( ' PaletteCycle ' , 1 , None ) , ]
2018-03-24 22:53:18 +10:30
battle_bg_headers = [ ' Address ' ] + [ z [ 0 ] for z in battle_bg_structure ]
battle_bg_data = [ parse_struct ( ROM_jp , 0x14BA21 + ( i * 8 ) , battle_bg_structure ) for i in range ( 34 ) ]
2018-03-19 17:13:04 +10:30
tileset_headers = ( " ID " , " Offset " , " Pointer " , " Expected Length " )
tileset_data = [ ]
for i in range ( 0x1C ) :
offset = 0x0F0000 + ( i * 2 )
2018-03-22 20:16:26 +10:30
pointer = 0x0F0000 + indirect ( ROM_en , offset )
length = indirect ( ROM_en , offset + 2 ) - indirect ( ROM_en , offset )
2018-03-19 17:13:04 +10:30
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 )
2018-03-22 20:16:26 +10:30
start = indirect ( ROM_en , i ) + offset
next = indirect ( ROM_en , i + 2 ) + offset
2018-03-19 17:13:04 +10:30
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 ) )
enemy_sprite_data = [ ]
enemy_sprite_structure = [
( ' Sprite data offset ' , 2 , None ) ,
( ' Multiple things ' , 2 , None ) ,
2018-03-26 16:27:03 +10:30
( ' Tile Layout ID ' , 1 , None ) ,
2018-03-19 17:13:04 +10:30
]
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 ] )
2018-03-22 20:16:26 +10:30
perfcount ( )
print ( ' Generating map tiles ' )
2018-03-24 22:53:18 +10:30
worldmap_palettes = [ generate_palette ( ROM_jp , 0x0FFCC0 + ( i * 0x100 ) , length = 0x160 , transparent = True ) for i in range ( 3 ) ]
2018-03-27 23:19:20 +10:30
world_tiles = [ make_worldmap_blocks ( ROM_jp , 0x0FF0C0 + ( i * 0x300 ) , 0x1B8000 + ( i * 0x2000 ) , 0x0FF9C0 + ( i * 0x100 ) ) for i in range ( 3 ) ]
2018-04-01 21:47:26 +09:30
#worldpixmaps = [make_worldmap_pixmap(ROM_jp, i, 0x0FFCC0+(t*0x100), world_tiles[t]) for i, t in enumerate([0, 1, 0, 2, 2])]
2018-03-27 23:19:20 +10:30
world_blocks_pixmaps = [ ]
2018-03-24 22:53:18 +10:30
for i , tiles in enumerate ( world_tiles ) :
a = [ ]
for t in tiles :
t . setColorTable ( worldmap_palettes [ i ] )
a . append ( QPixmap . fromImage ( t ) )
2018-03-27 23:19:20 +10:30
world_blocks_pixmaps . append ( a )
2018-04-01 21:47:26 +09:30
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 ] ) ]
2018-03-24 22:53:18 +10:30
perfcount ( )
2018-03-27 23:19:20 +10:30
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 )
2018-03-22 20:16:26 +10:30
perfcount ( )
2018-03-27 23:19:20 +10:30
2018-03-22 20:16:26 +10:30
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 ) ]
perfcount ( )
2018-03-27 23:19:20 +10:30
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 ) ]
perfcount ( )
blockmaps = get_blockmaps ( ROM_jp )
field_blocks = [ ]
zone_pxs = [ ]
2018-03-31 18:21:29 +10:30
block_cache = { ' misses ' : 0 , ' p_hits ' : 0 , ' i_hits ' : 0 }
zone_px_cache = { ' misses ' : 0 , ' hits ' : 0 }
2018-04-04 22:41:07 +09:30
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]
print ( ' Block cache results: {misses} misses, {p_hits} full hits, {i_hits} palette misses ' . format ( * * block_cache ) )
perfcount ( )
for i , z in enumerate ( zones ) :
blocks , miniblocks = fm_blocks [ i ]
2018-03-31 18:21:29 +10:30
field_blocks . append ( stitch_tileset_px ( [ b . all for b in blocks + miniblocks ] ) )
zone_pxs + = make_zone_pxs ( blocks , miniblocks , blockmaps , z , zone_px_cache )
2018-04-04 22:41:07 +09:30
#zone_pxs += make_zone_pxs2(blocks, miniblocks, blockmaps, z, zone_px_cache)
2018-03-31 18:21:29 +10:30
print ( ' Zone pixmap cache results: {misses} misses, {hits} hits ' . format ( * * zone_px_cache ) )
2018-03-27 23:19:20 +10:30
perfcount ( )
2019-06-16 00:07:26 +09:30
del block_cache
del zone_px_cache
2018-03-27 23:19:20 +10:30
2018-03-24 22:53:18 +10:30
print ( ' Generating Battle backgrounds ' )
battle_bgs = make_battle_backgrounds ( ROM_jp )
perfcount ( )
2018-03-22 20:16:26 +10:30
print ( ' Generating other sprites ' )
2018-03-19 17:13:04 +10:30
self . battle_strips = make_character_battle_sprites ( ROM_en )
status_strips = make_character_status_sprites ( ROM_en )
enemy_sprites = make_enemy_sprites ( ROM_en )
2018-03-22 20:16:26 +10:30
enemy_sprites_named = [ stack_labels ( s , d [ - 2 ] ) for s , d in zip ( enemy_sprites , enemy_sprite_data ) ]
perfcount ( )
print ( ' Generating FF4 and FF6 stuff ' )
2018-03-26 16:27:03 +10:30
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 )
2018-03-22 20:16:26 +10:30
perfcount ( )
2018-03-19 17:13:04 +10:30
2018-03-27 01:11:40 +10:30
print ( ' Creating Qt Widgets ' )
2018-03-21 18:09:37 +10:30
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 ' )
2018-03-19 17:13:04 +10:30
strings_tab = QTabWidget ( )
structs_tab = QTabWidget ( )
sprites_tab = QTabWidget ( )
2018-03-27 23:19:20 +10:30
backgrounds_tab = QTabWidget ( )
2018-03-21 18:09:37 +10:30
self . ff5widget . addTab ( strings_tab , ' Strings ' )
self . ff5widget . addTab ( structs_tab , ' Structs ' )
self . ff5widget . addTab ( sprites_tab , ' Images ' )
2018-03-27 23:19:20 +10:30
self . ff5widget . addTab ( backgrounds_tab , ' Backgrounds ' )
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 ' )
2018-03-19 17:13:04 +10:30
2018-03-27 23:19:20 +10:30
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 ' )
2018-03-28 06:56:07 +10:30
backgrounds_tab . addTab ( make_px_table ( fieldmap_tiles , cols = 16 , scale = 1 ) , ' Fieldmap Tiles ' )
2018-03-27 23:19:20 +10:30
backgrounds_tab . addTab ( make_px_table ( field_blocks , cols = 16 , scale = 1 ) , ' Field Blocks ' )
2018-04-04 22:41:07 +09:30
backgrounds_tab . addTab ( make_px_table ( zone_pxs , cols = 4 , scale = 1 , large = 1 , basicrows = True ) , ' Zone ' )
2018-03-27 23:19:20 +10:30
backgrounds_tab . addTab ( make_px_table ( battle_bgs , cols = 8 , scale = 1 ) , ' Battle BGs ' )
2018-03-21 18:09:37 +10:30
2018-03-27 23:19:20 +10:30
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 ' )
2018-03-19 17:13:04 +10:30
structs_tab . addTab ( make_table ( zone_headers , zone_data , True ) , ' Zones ' )
structs_tab . addTab ( make_table ( tileset_headers , tileset_data , True ) , ' Tilesets ' )
2018-03-24 22:53:18 +10:30
structs_tab . addTab ( make_table ( battle_bg_headers , battle_bg_data , True ) , ' BattleBGs ' )
2018-03-19 17:13:04 +10:30
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 , items , row_labels = False ) , ' Items ' )
2018-03-27 01:11:40 +10:30
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')
2018-03-19 17:13:04 +10:30
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 ' )
self . string_decoder = QWidget ( )
self . decoder_input = QLineEdit ( )
self . decoder_input . returnPressed . connect ( self . _string_decode )
self . decoder_layout = QVBoxLayout ( )
self . decoder_layout . addWidget ( self . decoder_input )
self . string_decoder . setLayout ( self . decoder_layout )
strings_tab . addTab ( self . string_decoder , ' String Decoder ' )
layout = QHBoxLayout ( )
2018-03-21 18:09:37 +10:30
layout . addWidget ( self . gamewidget )
2018-03-19 17:13:04 +10:30
self . main_widget = QWidget ( self )
self . main_widget . setLayout ( layout )
self . main_widget . setMinimumSize ( 800 , 600 )
self . setCentralWidget ( self . main_widget )
self . show ( )
2018-03-27 01:11:40 +10:30
perfcount ( )
2018-03-19 17:13:04 +10:30
def _string_decode ( self ) :
string = ' ' . join ( self . decoder_input . text ( ) . split ( ) )
if len ( string ) % 1 :
string + = ' 0 '
bytelist = [ int ( string [ i : i + 2 ] , 16 ) for i in range ( 0 , len ( string ) , 2 ) ]
2018-03-26 16:27:03 +10:30
tups = make_string_img_small ( bytes ( bytelist ) , self . glyph_sprites [ ' glyphs_en_s ' ] )
2018-03-19 17:13:04 +10:30
print ( tups [ 0 ] )
img = QLabel ( )
img . setPixmap ( tups [ 1 ] )
self . decoder_layout . addWidget ( img )
self . decoder_input . setText ( ' ' )
2017-04-02 14:37:53 +09:30
2018-03-27 01:11:40 +10:30
'''
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 .
'''
2018-03-26 16:27:03 +10:30
def make_string_img_small ( bytestring , glyphs , jp = False ) :
2018-03-24 22:53:18 +10:30
'''
2018-03-26 16:27:03 +10:30
JP version is not as fancy as this with dakuten , it just puts them on the row above and clips .
2018-03-24 22:53:18 +10:30
'''
2018-03-19 17:13:04 +10:30
if len ( bytestring ) < 1 :
raise ValueError ( ' Empty bytestring was passed ' )
2018-03-26 16:27:03 +10:30
string = ' '
2018-03-19 17:13:04 +10:30
img = QImage ( len ( bytestring ) * 8 , 10 , QImage . Format_RGB16 )
img . fill ( bg_color )
painter = QtGui . QPainter ( img )
if jp :
for x , j in enumerate ( bytestring ) :
string = string + const . Glyphs_JP2 [ j ]
if 0x20 < = j < 0x52 :
if j > 0x48 :
2018-03-26 16:27:03 +10:30
painter . drawPixmap ( x * 8 , 2 , glyphs [ j + 0x17 ] )
painter . drawPixmap ( x * 8 + 1 , - 5 , glyphs [ 0x52 ] )
2018-03-19 17:13:04 +10:30
else :
2018-03-26 16:27:03 +10:30
painter . drawPixmap ( x * 8 , 2 , glyphs [ j + 0x40 ] )
painter . drawPixmap ( x * 8 + 1 , - 6 , glyphs [ 0x51 ] )
2018-03-19 17:13:04 +10:30
else :
2018-03-26 16:27:03 +10:30
painter . drawPixmap ( x * 8 , 2 , glyphs [ j ] )
2018-03-19 17:13:04 +10:30
else :
for x , j in enumerate ( bytestring ) :
string = string + const . Glyphs [ j ]
2018-03-26 16:27:03 +10:30
painter . drawPixmap ( x * 8 , 1 , glyphs [ j ] )
2018-03-19 17:13:04 +10:30
del painter
return string , QPixmap . fromImage ( img )
2017-03-13 16:02:36 +10:30
2017-07-23 18:29:22 +09:30
2018-03-26 16:27:03 +10:30
def make_string_img_large ( bytestring , glyphs , glyphs_kanji , macros = None , jp = False ) :
2018-03-19 17:13:04 +10:30
'''
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 ( 16 px )
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 ' )
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 )
2018-03-26 16:27:03 +10:30
string = ' '
2018-03-19 17:13:04 +10:30
# 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.
img = QImage ( max_width , max_height , QImage . Format_RGB16 )
img . fill ( bg_color )
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 ]
2018-03-26 16:27:03 +10:30
painter . drawPixmap ( x , y + 2 , glyphs_kanji [ j - 0x1E00 ] )
2018-03-19 17:13:04 +10:30
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 ]
2018-03-26 16:27:03 +10:30
painter . drawPixmap ( x , y + 2 , glyphs [ j ] )
2018-03-19 17:13:04 +10:30
x + = 16
else :
string + = const . Glyphs [ j ]
2018-03-26 16:27:03 +10:30
painter . drawPixmap ( x , y + 4 , glyphs [ j ] )
2018-03-19 17:13:04 +10:30
x + = const . Dialogue_Width [ j ]
del painter
xmax = x if x > xmax else xmax
return string , QPixmap . fromImage ( img . copy ( 0 , 0 , xmax , y + 16 ) )
2017-03-13 16:02:36 +10:30
2017-07-23 18:29:22 +09:30
2018-03-31 18:21:29 +10:30
def _make_string_img_list ( rom_e , rom_j , start , length , num ,
2018-03-26 16:27:03 +10:30
start_jp = None , len_jp = None , start_str = None , start_jp_str = None ,
indirect = False , large = False , macros_en = None , macros_jp = None ,
glyphs_en_s = None , glyphs_en_l = None ,
glyphs_jp_s = None , glyphs_jp_l = None , glyphs_kanji = None ) :
2018-03-19 17:13:04 +10:30
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
stringlist = [ ]
id_digits = hex_length ( num - 1 )
if indirect :
for id in range ( num ) :
en = start + ( id * length )
jp = start_jp + ( id * len_jp )
2018-03-31 18:21:29 +10:30
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
2018-03-19 17:13:04 +10:30
if en_start > = 0xC00000 : # SNES memory space has the ROM starting at 0xC00000 in HiROM mode.
en_start - = 0xC00000
en_end - = 0xC00000
2018-03-31 18:21:29 +10:30
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
2018-03-19 17:13:04 +10:30
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 :
2018-03-31 18:21:29 +10:30
str_en , img_en = make_string_img_large ( rom_e [ en_start : en_end ] , glyphs_en_l , glyphs_kanji , macros_en )
2018-03-19 17:13:04 +10:30
else :
2018-03-31 18:21:29 +10:30
str_en , img_en = make_string_img_small ( rom_e [ en_start : en_end ] , glyphs_en_s )
2018-03-19 17:13:04 +10:30
except ValueError :
str_en = ' '
img_en = None
try :
if large :
2018-03-31 18:21:29 +10:30
str_jp , img_jp = make_string_img_large ( rom_j [ jp_start : jp_end ] , glyphs_jp_l , glyphs_kanji , macros_jp , jp = True )
2018-03-19 17:13:04 +10:30
else :
2018-03-31 18:21:29 +10:30
str_jp , img_jp = make_string_img_small ( rom_j [ jp_start : jp_end ] , glyphs_jp_s , jp = True )
2018-03-19 17:13:04 +10:30
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 :
2018-03-31 18:21:29 +10:30
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 )
2018-03-19 17:13:04 +10:30
else :
2018-03-31 18:21:29 +10:30
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 )
2018-03-19 17:13:04 +10:30
stringlist . append ( [ hex ( j1 , 6 ) , hex ( id , id_digits ) , str_en , img_en , str_jp , img_jp ] )
return stringlist
2017-03-13 16:02:36 +10:30
2018-03-22 20:16:26 +10:30
last_perfcount = None
def perfcount ( ) :
'''
Really basic timing for debugging
'''
global last_perfcount
t = time . perf_counter ( )
if last_perfcount :
print ( t - last_perfcount )
else :
print ( ' perfcount initialised ' )
last_perfcount = t
2017-03-11 19:18:33 +10:30
def main ( ) :
2018-03-19 17:13:04 +10:30
app = QApplication ( sys . argv )
mainwindow = FF5Reader ( )
sys . exit ( app . exec_ ( ) )
2017-03-11 19:18:33 +10:30
if __name__ == ' __main__ ' :
2018-03-19 17:13:04 +10:30
main ( )