Animated Battle BGs

This commit is contained in:
Luke Hubmayer-Werner 2018-03-27 01:11:40 +10:30
parent 69306daf92
commit 7fe8fb7351
4 changed files with 168 additions and 54 deletions

View File

@ -110,9 +110,6 @@ with open(filename_jp_ff6, 'rb') as file:
ROM_FF6jp = file.read()
print(len(ROM_FF6jp), filename_jp_ff6)
stringlist_headers = ['Address', 'ID', 'Name']
imglist_headers = stringlist_headers + ['Img', 'Name JP', 'Img JP']
class FF5Reader(QMainWindow):
'''
@ -120,7 +117,6 @@ class FF5Reader(QMainWindow):
'''
def __init__(self):
QMainWindow.__init__(self, None)
global glyph_sprites_en_large, glyph_sprites_en_small, glyph_sprites_jp_small, glyph_sprites_jp_large, glyph_sprites_kanji, glyph_sprites_jp_dialogue
perfcount()
print('Generating Glyphs')
self.glyph_sprites = {
@ -133,6 +129,9 @@ class FF5Reader(QMainWindow):
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']
print('Generating Strings')
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)
@ -180,14 +179,14 @@ class FF5Reader(QMainWindow):
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 = [('ImageID', 1, None),
('ColorID 1', 1, None),
('ColorID 2', 1, None),
('TerrainID', 1, None),
('TerrainFlipID', 1, None),
(hex(5, 1), 1, None),
('AnimationID', 1, None),
('PaletteCycleID',1, None),]
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),]
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)]
@ -223,9 +222,7 @@ class FF5Reader(QMainWindow):
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_tiles(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_tiles_pixmaps = []
@ -267,6 +264,7 @@ class FF5Reader(QMainWindow):
perfcount()
print('Creating Qt Widgets')
self.gamewidget = QTabWidget()
self.ff4widget = QTabWidget()
self.ff5widget = QTabWidget()
@ -311,8 +309,8 @@ class FF5Reader(QMainWindow):
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')
strings_tab.addTab(make_table(imglist_headers, 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, 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')
@ -336,6 +334,7 @@ class FF5Reader(QMainWindow):
self.main_widget.setMinimumSize(800,600)
self.setCentralWidget(self.main_widget)
self.show()
perfcount()
def _string_decode(self):
string = ''.join(self.decoder_input.text().split())
@ -349,7 +348,10 @@ class FF5Reader(QMainWindow):
self.decoder_layout.addWidget(img)
self.decoder_input.setText('')
'''
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):
'''
JP version is not as fancy as this with dakuten, it just puts them on the row above and clips.

View File

@ -82,6 +82,31 @@ if not monofont.fixedPitch():
monofont.setFamily('Monospace')
class Label(QLabel):
def __init__(self, *kwargs):
super().__init__(*kwargs)
self.pixmaps = []
self.pixmap_index = 0
self.timer = QtCore.QTimer()
self.timer.timeout.connect(self._cycle_pixmap)
def setContent(self, content, scale=1, strip=True):
if isinstance(content, QPixmap):
self.setPixmap(content.scaled(content.size() * scale))
elif isinstance(content, list) and isinstance(content[0], QPixmap):
self.pixmaps = [c.scaled(c.size() * scale) for c in content[:-1]]
self.setPixmap(self.pixmaps[0])
self.timer.start(content[-1]*1000/60)
else:
if strip:
content = content.strip()
self.setText(content)
def _cycle_pixmap(self):
self.pixmap_index = (self.pixmap_index+1)%len(self.pixmaps)
self.setPixmap(self.pixmaps[self.pixmap_index])
def table_size_to_contents(table):
# Stupid hack to get table to size correctly
table.hide()
@ -144,9 +169,9 @@ def make_pixmap_table(items, cols=16, scale=4, large=False):
for i, item in enumerate(items):
if isinstance(item, QWidget):
table.setCellWidget(i // cols, i % cols, item)
elif isinstance(item, QPixmap):
lab = QLabel()
lab.setPixmap(item.scaled(item.size() * scale))
else:
lab = Label()
lab.setContent(item, scale=scale)
lab.setAlignment(QtCore.Qt.AlignCenter)
table.setCellWidget(i // cols, i % cols, lab)
table_size_to_contents(table)
@ -161,11 +186,8 @@ def stack_labels(*items):
l.setSpacing(0)
l.setContentsMargins(0, 0, 0, 0)
for item in items:
lab = QLabel()
if isinstance(item, QPixmap):
lab.setPixmap(item)
else:
lab.setText(item.strip())
lab = Label()
lab.setContent(item)
lab.setAlignment(QtCore.Qt.AlignCenter)
lab.setMargin(0)
l.addWidget(lab)

View File

@ -214,7 +214,7 @@ def make_field_map_tile_pixmap(rom, id, st_tiles, st_minitiles):
canvas.draw_pixmap(0, 48, st_minitiles[minitile].pixmap(p))
return canvas.pixmap()
def decompress_battle_terrain(rom, address):
def decompress_battle_tilemap(rom, address):
'''
Decompresses the tilemap for a battle background.
Battle BGs use a type of RLE with 2byte repeat and 1byte incremental repeat.
@ -248,7 +248,7 @@ def decompress_battle_terrain(rom, address):
output[1::2] = [i&0xDF for i in o2[:length//2]]
return bytes(output)
def apply_battle_terrain_flips(rom, id, battle_terrain):
def apply_battle_tilemap_flips(rom, id, battle_terrain):
if id==0xFF:
return battle_terrain
ptr = indirect(rom, 0x14C736+(id*2))+0x140000
@ -271,7 +271,30 @@ def apply_battle_terrain_flips(rom, id, battle_terrain):
output[i*2+1] |= (buffer[i] << 6)
return bytes(output)
def make_tilemap_pixmap(tilemap, tiles, palettes, tile_adjust=0):
def make_tilemap_canvas(tilemap, tiles, tile_adjust=0, pal_adjust=-1):
'''
Battle bg is 64x64 map size, 8x8 tile size
4bpp tiles
'''
canvas = Canvas_Indexed(64, 64)
for i in range(len(tilemap)//2):
a, b = tilemap[i*2:(i+1)*2]
tile_index = a|((b & 0x02) << 8)
p = (b & 0x1C) >> 2
priority = (b & 0x20) >> 5
h_flip = (b & 0x40) >> 6
v_flip = (b & 0x80) >> 7
x = (i % 32) + 32*((i//1024) % 2)
y = (i //32) - 32*((i//1024) % 2)
try:
tile = tiles[(tile_index+tile_adjust)%0x80]
canvas.draw_tile(x, y, tile, h_flip, v_flip, p+pal_adjust)
except BaseException as e:
print(e, p, hex(tile_index,2), hex(tile_adjust,2), hex(tile_index+tile_adjust,2))
return canvas
def make_tilemap_pixmap(tilemap, tiles, palettes, tile_adjust=0, pal_adjust=-1):
'''
Battle bg is 64x64 map size, 8x8 tile size
4bpp tiles
@ -288,7 +311,7 @@ def make_tilemap_pixmap(tilemap, tiles, palettes, tile_adjust=0):
x = (i % 32) + 32*((i//1024) % 2)
y = (i //32) - 32*((i//1024) % 2)
try:
palette = palettes[p]
palette = palettes[p+pal_adjust]
tile = tiles[(tile_index+tile_adjust)%0x80]
tile.setColorTable(palette)
tile_px = QPixmap.fromImage(tile)
@ -301,45 +324,108 @@ def make_battle_backgrounds(rom):
'''
21 pointers in memory for the compressed data of the tilesets.
Most of these are not unique, and only a subset of the resulting block is used.
The block appears to get DMA'd to 0x0400 in VRAM
Terrain gets DMA'd to 0x2000 (size 0x500) in VRAM from 0x7f0000 in RAM
'''
palettes = [generate_palette(rom, 0x14BB31+(i*0x20)) for i in range(84)]
battle_bgs = []
for i in range(34):
bg = {
'tiles_id': rom[0x14BA21+(i*8)],
'tileset_id': rom[0x14BA21+(i*8)],
'pal1_id': rom[0x14BA22+(i*8)],
'pal2_id': rom[0x14BA23+(i*8)],
'terrain_id': rom[0x14BA24+(i*8)],
'terrain_flips_id': rom[0x14BA25+(i*8)],
'tilemap_id': rom[0x14BA24+(i*8)],
'tilemap_flips_id': rom[0x14BA25+(i*8)],
'tilecycle_id': rom[0x14BA27+(i*8)],
'palcycle_id': rom[0x14BA28+(i*8)],
}
bg['palette'] = [palettes[0], palettes[bg['pal1_id']], palettes[bg['pal2_id']]]
bg['palette'] = palettes[bg['pal1_id']] + palettes[bg['pal2_id']]
battle_bgs.append(bg)
tiles_pointer_start = 0x184196
tiles_RAM_pointer_start = 0x184157
tiles_pointers = [indirect(rom, tiles_pointer_start+(i*3), length=3)-0xC00000 for i in range(21)]
tiles_raw = [decompress_lzss(rom, p) for p in tiles_pointers]
tiles_skips = [indirect(rom, tiles_RAM_pointer_start+(i*3), length=3)-0x7FC000 for i in range(21)]
tiles = []
for raw, skip in zip(tiles_raw, tiles_skips):
tileset_pointer_start = 0x184196
tileset_RAM_pointer_start = 0x184157
tileset_pointers = [indirect(rom, tileset_pointer_start+(i*3), length=3)-0xC00000 for i in range(21)]
tileset_raw = [decompress_lzss(rom, p) for p in tileset_pointers]
tileset_skips = [indirect(rom, tileset_RAM_pointer_start+(i*3), length=3)-0x7FC000 for i in range(21)]
tileset = []
for raw, skip in zip(tileset_raw, tileset_skips):
r = raw[skip:]
tiles.append([create_tile_indexed(r[i*32:(i+1)*32]) for i in range(len(r)//32)])
tileset.append([create_tile_indexed(r[i*32:(i+1)*32]) for i in range(len(r)//32)])
terrain_pointer_start = 0x14C86D
terrain_pointers = [indirect(rom, terrain_pointer_start+(i*2))+0x140000 for i in range(28)]
terrains = [decompress_battle_terrain(rom, p) for p in terrain_pointers]
tilemap_pointer_start = 0x14C86D
tilemap_pointers = [indirect(rom, tilemap_pointer_start+(i*2))+0x140000 for i in range(28)]
tilemaps = [decompress_battle_tilemap(rom, p) for p in tilemap_pointers]
animation_ptr_start = 0x14C5B1
animation_ptrs = [indirect(rom, animation_ptr_start+(i*2))+0x140000 for i in range(8)]
animations = []
for ptr in animation_ptrs:
a = []
for i in range(ptr, ptr+200):
b = rom[i]
if b == 0xFF:
break
a.append(b)
a = [(i, j) for i, j in zip(a[0::2], a[1::2])]
animations.append(a)
animation_time = 15 # Frames before changing
pal_cycle_ptr_start = 0x14C6CD
pal_cycle_ptrs = [indirect(rom, pal_cycle_ptr_start+(i*2))+0x140000 for i in range(3)]
pal_cycles = []
for ptr in pal_cycle_ptrs:
a = []
for i in range(ptr, ptr+100):
b = rom[i]
if b == 0xFF:
break
a.append(b)
pal_cycles.append(a)
def make_pals(bg):
p_cycle = pal_cycles[bg['palcycle_id']]
p1 = bg['pal1_id']
p2 = bg['pal2_id']
pals = []
for p in p_cycle:
if p & 0x80:
p2 = min(p & 0x7F, len(palettes)-1)
else:
p1 = min(p, len(palettes)-1)
pals.append(palettes[p1] + palettes[p2])
return pals
canvases = []
pixmaps = []
for bg in battle_bgs:
terrain = apply_battle_terrain_flips(rom, bg['terrain_flips_id'], terrains[bg['terrain_id']])
pixmaps.append(make_tilemap_pixmap(terrain, tiles[bg['tiles_id']], bg['palette']))
#[make_tilemap_pixmap(terrains[5], tiles[2], palettes)]
tilemap = apply_battle_tilemap_flips(rom, bg['tilemap_flips_id'], tilemaps[bg['tilemap_id']])
if bg['tilecycle_id'] > 0:
tss = [[t for t in tileset[bg['tileset_id']]] for i in range(4)]
for i, tile2 in animations[bg['tilecycle_id']]:
frame = i >> 6
tile = i & 0x3F
tss[frame][tile] = tileset[bg['tileset_id']][tile2]
canvases.append([make_tilemap_canvas(tilemap, ts) for ts in tss])
if bg['palcycle_id'] < 3:
pals = make_pals(bg)
pl = len(pals)
cl = (animation_time*4)
px = [canvases[-1][0].pixmap(pals[0], True)]
i = 1
while (i%pl != 0) or (i%cl != 0):
px.append(canvases[-1][(i//animation_time)%4].pixmap(pals[i%pl], True))
i += 1
pixmaps.append(px + [1])
else:
pixmaps.append([c.pixmap(bg['palette'], True) for c in canvases[-1]]+[animation_time])
else:
canvases.append(make_tilemap_canvas(tilemap, tileset[bg['tileset_id']]))
if bg['palcycle_id'] < 3:
pals = make_pals(bg)
pixmaps.append([canvases[-1].pixmap(p, True) for p in pals]+[1])
else:
pixmaps.append(canvases[-1].pixmap(bg['palette'], True))
return pixmaps
def get_zone_tiles_start(rom, id):
def get_zone_tileset_start(rom, id):
i1 = indirect(rom, 0x0E59C0+(id*2))+7
i2 = indirect(rom, 0x0E59C2+i1, 1)
# There is a divergent path here based on the value. Other things appear to be affected by this.

View File

@ -25,14 +25,14 @@ skip_pyqt5 = "PYQT4" in os.environ
if not skip_pyqt5:
try:
from PyQt5 import QtGui
from PyQt5.QtGui import QImage, QPixmap, QColor, QPainter
from PyQt5.QtGui import QImage, QPixmap, QColor, QPainter, QTransform
pyqt_version = 5
except ImportError:
print("Missing PyQt5, trying PyQt4...")
if pyqt_version == 0:
try:
from PyQt4 import QtGui
from PyQt4.QtGui import QImage, QPixmap, QColor, QPainter
from PyQt4.QtGui import QImage, QPixmap, QColor, QPainter, QTransform
pyqt_version = 4
except ImportError:
print("Missing PyQt4 dependencies")
@ -253,9 +253,13 @@ class Canvas_Indexed:
self.max_col = 1
self.max_row = 1
def draw_tile(self, col, row, image):
def draw_tile(self, col, row, image, h_flip=False, v_flip=False, palette=0):
image = image.mirrored(h_flip, v_flip)
imgbits = image.bits()
imgbits.setsize(image.byteCount())
if palette:
p = palette<<4
imgbits[:] = bytes([int(i[0])|p for i in imgbits])
x = col*self.tilesize
y = row*self.tilesize
start = x + y*self.width