FF5Reader/includes/snes.py

563 lines
20 KiB
Python

'''
Functions common to SNES FFs
'''
'''
This file is part of ff5reader.
ff5reader is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ff5reader is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with ff5reader. If not, see <http://www.gnu.org/licenses/>.
'''
from includes.helpers import *
from includes.snestile import *
from collections import namedtuple
Zone = namedtuple('Zone', 'npcs name shadowflags blockset tilesets blockmaps pal palette music')
Block = namedtuple('Block', 'priority0 priority1 all')
TileMapping = namedtuple('TileMapping', 'tile palette h_flip v_flip priority')
def parse_zone(rom, id, start_address=0x0E9C00):
ptr = start_address+(id*0x1A)
npcs = indirect(rom, ptr)
name = rom[ptr+2]
shadowflags = rom[ptr+3]
blockset = rom[ptr+8]
tilesets_b = indirect(rom, ptr+9, length=3)
tilesets = [(tilesets_b & 0x00003F),
(tilesets_b & 0x000FC0) >> 6,
(tilesets_b & 0x03F000) >> 12,
(tilesets_b & 0xFC0000) >> 18]
blockmaps_b = indirect(rom, ptr+0xC, length=4)
blockmaps = [(blockmaps_b & 0x000003FF) - 1,
((blockmaps_b & 0x000FFC00) >> 10) - 1,
((blockmaps_b & 0x3FF00000) >> 20) - 1]
pal = rom[ptr+0x16]
pal_address = 0x03BB00 + (pal<<8)
palettes = [generate_palette(rom, pal_address+i*32, transparent=True) for i in range(8)]
music = rom[ptr+0x19]
return Zone(npcs, name, shadowflags, blockset, tilesets, blockmaps, pal, palettes, music)
'''
This is a bit of a mess of pointer chains for now, so generalising it will have to wait.
UPDATE: i2-i7 merely obtain a zone ID. Whoops.
'''
#i2 = indirect(rom, 0x0E2400 + id*2)
#i3 = indirect(rom, 0x0E2402 + i2)*2
#i4 = indirect(rom, 0x18E080 + i3)
#i5 = indirect(rom, 0x18E081 + i4+4)*3
#i6 = indirect(rom, 0x083320 + i5)
#i7 = indirect(rom, 0x080001 + i6) & 0x03FF
def make_battle_strip(rom, palette_address, tile_address, num_tiles, bpp=4):
if isinstance(palette_address, int):
palette = generate_palette(rom, palette_address, transparent=True)
else:
palette = palette_address
b = 24 if bpp==3 else 32
battle_strip = Canvas(2, divceil(num_tiles, 2))
for j in range(num_tiles):
offset = tile_address+(j*b)
battle_strip.draw_pixmap(j%2, j//2, create_tile(rom[offset:offset+b], palette))
return battle_strip.pixmap()
def make_enemy_sprites(rom):
sprites = []
for e_id in range(0, 0x180*5, 5):
triplane = bool(rom[0x14B180+e_id]&0x80) # True if 3 planes, False if 4
bytes_per_tile = 24 if triplane else 32
tile_offset = ((((rom[0x14B180+e_id]&0x7F)<<8)| rom[0x14B181+e_id]) << 3) + 0x150000 # For whatever reason this is big endian
pal_offset = ((((rom[0x14B182+e_id]&0x03)<<8)| rom[0x14B183+e_id]) << 4) + 0x0ED000 # For whatever reason this is big endian
pal_size = 16 if triplane else 32
palette = generate_palette(rom, pal_offset, pal_size, transparent=True)
layout_id = rom[0x14B184+e_id]
boss_layout = bool(rom[0x14B182+e_id]&0x80)
if boss_layout:
layout = rom[0x10D334+(layout_id<<5):0x10D334+(layout_id<<5)+32]
sprite = Canvas(16, 16)
for x, y in [(x,y) for y in range(16) for x in range(16)]:
if (int.from_bytes(layout[y*2:y*2+2], 'little') & (0x8000 >> x)):
sprite.draw_pixmap(x, y, create_tile(rom[tile_offset:tile_offset+bytes_per_tile], palette))
tile_offset += bytes_per_tile
else:
layout = rom[0x10D004+(layout_id<<3):0x10D004+(layout_id<<3)+8]
sprite = Canvas(8, 8)
for x, y in [(x,y) for y in range(8) for x in range(8)]:
if (layout[y] & (0x80 >> x)):
sprite.draw_pixmap(x, y, create_tile(rom[tile_offset:tile_offset+bytes_per_tile], palette))
tile_offset += bytes_per_tile
# TODO: Shadow stuff
sprites.append(sprite.pixmap(True))
return sprites
def make_character_battle_sprites(rom):
tile_address = 0x120000
palette_address = 0x14A3C0
battle_strips = []
for i in range(0, (22*5)*32, 32): # 22 jobs 5 characters
battle_strips.append(make_battle_strip(rom, palette_address+i, tile_address+(i*48), 48))
return battle_strips
def make_character_status_sprites(rom):
tile_address = 0x149400
palette_address = 0x14A660
pixmaps = []
for i in range(5):
palette = generate_palette(rom, palette_address + (i*22*32), transparent=True) # Freelance palette per character
wounded = Canvas(3, 2)
for j in range(6):
offset = tile_address+(i*192)+(j*32)
wounded.draw_pixmap(j%3, j//3, create_tile(rom[offset:offset+32], palette))
pixmaps.append(wounded.pixmap())
mini_strip = Canvas(2, 19)
for j in range(38):
offset = tile_address+0x3C0+(j*24)
mini_strip.draw_pixmap(j%2, j//2, create_tile(rom[offset:offset+24], palette))
pixmaps.append(mini_strip.pixmap())
frog_strip = Canvas(2, 15)
for j in range(30):
offset = tile_address+0x750+(j*24)
frog_strip.draw_pixmap(j%2, j//2, create_tile(rom[offset:offset+24], palette))
pixmaps.append(frog_strip.pixmap())
return pixmaps
def make_worldmap_tiles(rom, tiles_address, lut_address, length=0x100):
subtiles = []
for i in range(length):
pal_index = rom[lut_address+i]//16
subtiles.append(create_tile_mode7_compressed_indexed(rom[tiles_address+i*32:tiles_address+i*32+32], pal_index))
return subtiles
def stitch_worldmap_tiles(rom, tiles, offset=0x0FF0C0):
blocks = []
for i in range(0xC0):
canvas = Canvas_Indexed(2, 2)
for j in range(4):
k = indirect(rom, offset+(j*0xC0)+i, length=1)
canvas.draw_tile(j%2, j//2, tiles[k])
blocks.append(canvas.image)
return blocks
def make_worldmap_blocks(rom, blocks_address, tiles_address, lut_address, length=0x100):
return stitch_worldmap_tiles(rom, make_worldmap_tiles(rom, tiles_address, lut_address, length=length), blocks_address)
def make_worldmap_tiles_pixmap(rom, tiles_address, lut_address, palette_address, length=0x100):
tiles = []
palettes = [generate_palette(rom, palette_address+i*32, transparent=True) for i in range(16)]
for i in range(length):
palette = palettes[rom[lut_address+i]//16]
tiles.append(create_tile_mode7_compressed(rom[tiles_address+i*32:tiles_address+i*32+32], palette))
return tiles
def make_worldmap_chunk(rom, id, length=256):
i = indirect(rom, 0x0FE000+(id*2)) + 0x070000
if id > 0x433:
i += 0x010000
mountains = (0x0C, 0x1C, 0x2C)
chunk = []
while len(chunk) < length:
j = indirect(rom, i, 1)
if j >= 0xC0:
k = j-0xBF
i += 1
j = indirect(rom, i, 1)
chunk += [j]*k
elif j in mountains:
chunk += [j, j+1, j+2]
else:
chunk.append(j)
i += 1
return chunk
def make_worldmap_pixmap(rom, map_id, palette_address, tiles):
id_offset = map_id*256
palette = generate_palette(rom, palette_address, length=0x320, transparent=True)
canvas = Canvas_Indexed(256, 256, tilesize=16)
for j in range(256):
chunk = make_worldmap_chunk(rom, j+id_offset)
for i, c in enumerate(chunk):
canvas.draw_tile(i, j, tiles[c])
return canvas.pixmap(palette)
def make_worldmap_pixmap2(rom, map_id, tiles):
'''
Batched version using QPainter.drawPixmapFragments
Tiles is a pixmap tilemap.
'''
id_offset = map_id*256
canvas = Canvas(256, 256, tilesize=16)
fragments = []
for j in range(256):
chunk = make_worldmap_chunk(rom, j+id_offset)
for i, c in enumerate(chunk):
fragments.append(make_pixmapfragment(c, i*16, j*16))
canvas.drawPixmapFragments(fragments, tiles)
return canvas.pixmap()
def make_field_tiles(rom, id):
tiles_address = indirect(rom, 0x1C2D84 + id*4, length=4) + 0x1C2E24
return [create_tile_indexed(rom[tiles_address+i*32:tiles_address+i*32+32]) for i in range(256)]
def make_field_minitiles(rom, id):
tiles_address = indirect(rom, 0x1C0000 + id*2) + 0x1C0024
return [create_tile_indexed(rom[tiles_address+i*16:tiles_address+i*16+16]) for i in range(256)]
def make_all_field_tiles(rom):
return [make_field_tiles(rom, i) for i in range(40)]
def make_all_field_minitiles(rom):
return [make_field_minitiles(rom, i) for i in range(18)]
def stitch_tileset(tiles):
canvas = Canvas_Indexed(16, len(tiles)//16)
for i, tile in enumerate(tiles):
canvas.draw_tile(i%16, i//16, tile)
return canvas
def stitch_tileset_px(tiles_px):
canvas = Canvas(16, len(tiles_px)//16, tilesize=tiles_px[0].width())
for i, tile in enumerate(tiles_px):
canvas.draw_pixmap(i%16, i//16, tile)
return canvas.pixmap()
def make_field_map_tile_pixmap(rom, id, st_tiles, st_minitiles):
#*tiles, minitile, palettes = get_field_map_tiles(rom, id)
zone = parse_zone(rom, id)
p = zone.palette[1]
canvas = Canvas(16, 64)
for i, ts in enumerate(zone.tilesets[:-1]):
canvas.draw_pixmap(0, i*16, st_tiles[ts].pixmap(p))
canvas.draw_pixmap(0, 48, st_minitiles[zone.tilesets[-1]].pixmap(p))
return canvas.pixmap()
def get_field_map_block_layouts(rom, id, start_address=0x0F0000):
ptr = indirect(rom, start_address+(id*2)) + start_address
data = decompress_lzss(rom, ptr)
output = []
for i in range(0, 0x200, 2):
output.append([data[j+i+k] for j in range(0, 0x800, 0x200) for k in range(2)])
return output
def make_field_map_blocks_px(rom, zone, tilesets, minitilesets, blocksets, cache):
cache_key_i = '{} {}'.format(' '.join([str(i) for i in zone.tilesets]), zone.blockset)
cache_key_p = '{} {}'.format(cache_key_i, zone.pal)
if cache_key_p in cache:
cache['p_hits'] += 1
return cache[cache_key_p]
elif cache_key_i in cache:
cache['i_hits'] += 1
blocks, miniblocks = cache[cache_key_i]
else:
cache['misses'] += 1
*i_tiles, i_minitiles = zone.tilesets
tiles = tilesets[i_tiles[0]] + tilesets[i_tiles[1]] + tilesets[i_tiles[2]]
minitiles = minitilesets[i_minitiles]
blockset = blocksets[zone.blockset]
blockset_parsed = [[parse_tileset_word(tm[i*2:(i+1)*2]) for i in range(len(tm)//2)] for tm in blockset]
blocks = [Block(*make_block(bs, tiles)) for bs in blockset_parsed]
miniblocks = [Block(*make_block(bs, minitiles, tile_modulo=len(minitiles))) for bs in blockset_parsed]
cache[cache_key_i] = (blocks, miniblocks)
blocks_px = [Block(*[i.pixmap(zone.palette) for i in b]) for b in blocks]
miniblocks_px = [Block(*[i.pixmap(zone.palette) for i in b]) for b in miniblocks]
cache[cache_key_p] = (blocks_px, miniblocks_px)
return blocks_px, miniblocks_px
def make_block(tilemap, tiles, cols=2, rows=2, tile_adjust=0, pal_adjust=0, tile_modulo=0x1000):
canvases = (Canvas_Indexed(cols, rows), Canvas_Indexed(cols, rows), Canvas_Indexed(cols, rows))
for i, tm in enumerate(tilemap):
x = i % cols
y = i //cols
try:
tile = tiles[(tm.tile+tile_adjust)%tile_modulo]
canvases[tm.priority].draw_tile(x, y, tile, tm.h_flip, tm.v_flip, tm.palette+pal_adjust)
canvases[2].draw_tile(x, y, tile, tm.h_flip, tm.v_flip, tm.palette+pal_adjust)
except BaseException as e:
print(e, tm.palette, hex(tm.tile,2), hex(tile_adjust,2), hex(tm.tile+tile_adjust,2))
return canvases
def get_blockmaps(rom, start_address=0x0B0000, num=0x148):
bank = 0x0B0000
ptrs = [indirect(rom, start_address)+bank]
for i in range(1, num):
ptr = indirect(rom, start_address+(i*2))
if (ptr+bank) < ptrs[-1]:
bank += 0x010000
ptrs.append(ptr+bank)
blockmaps = [decompress_lzss(rom, ptr) for ptr in ptrs]
return blockmaps
def make_zone_pxs(blocks, miniblocks, blockmaps, zone, cache):
cache_key = '{} {} {}'.format(' '.join([str(i) for i in zone.blockmaps+zone.tilesets]), zone.blockset, zone.pal)
if cache_key in cache:
cache['hits'] += 1
return cache[cache_key]
else:
cache['misses'] += 1
output = []
layers = [None, None, None, None, None, None] # bg1.0 bg1.1 bg2.0 bg2.1 bg3.0 bg3.1
order = [4, 2, 0, 3, 1, 5] # Draw order from http://problemkaputt.de/fullsnes.htm#snespictureprocessingunitppu
for i, i_b in enumerate(zone.blockmaps):
if i_b == -1:
output.append(None)
else:
canvases = (Canvas(64, 64, tilesize=16), Canvas(64, 64, tilesize=16), Canvas(64, 64, tilesize=16))
_blocks = blocks if i < 2 else miniblocks
for j, b in enumerate(blockmaps[i_b]):
block = _blocks[b]
canvases[0].draw_pixmap(j%64, j//64, block.priority0)
canvases[1].draw_pixmap(j%64, j//64, block.priority1)
canvases[2].draw_pixmap(j%64, j//64, block.all)
output.append(canvases[2].pixmap())
layers[i*2:(i+1)*2] = canvases[0:2]
canvas = Canvas(64, 64, tilesize=16)
for i in order:
if layers[i]:
canvas.draw_pixmap(0, 0, layers[i].pixmap())
output.append(canvas.pixmap())
cache[cache_key] = output
return output
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.
'''
length = 0x500
output = [0 for i in range(length)]
o1 = []
ptr = address
while len(o1) < length//2:
a = rom[ptr]
ptr += 1
if a != 0xFF:
o1.append(a)
else:
repeat = rom[ptr]
ptr += 1
if repeat & 0x80: # Repeat 2 tiles
repeat &= 0x3F
a, b = rom[ptr:ptr+2]
ptr += 2
o1 += [a, b]*repeat
else: # Incremental repeat
a, inc = rom[ptr:ptr+2]
ptr += 2
if repeat & 0x40: # Negative increment
inc = -inc
repeat &= 0x3F
o1 += [a+(i*inc) for i in range(repeat)]
o2 = [4*(1+(i>>7)) for i in o1]
output[::2] = [i|0x80 for i in o1[:length//2]]
output[1::2] = [i&0xDF for i in o2[:length//2]]
return bytes(output)
def apply_battle_tilemap_flips(rom, id, tilemap):
if id==0xFF:
return tilemap
ptr = indirect(rom, 0x14C736+(id*2))+0x140000
length = len(tilemap)//2
output = list(tilemap)
buffer = []
while len(buffer) < length:
a = rom[ptr]
ptr += 1
if a == 0x00:
skip = rom[ptr]
ptr += 1
buffer += [0]*skip*8
else:
for b in reversed(range(0, 8, 1)):
buffer.append((a>>b)&0x01)
for i in range(len(tilemap)//2):
output[i*2+1] |= (buffer[i] << 6)
return bytes(output)
def parse_tileset_word(data):
a, b = data[:2]
tile_index = a|((b & 0x03) << 8)
palette = (b & 0x1C) >> 2
priority = (b & 0x20) >> 5
h_flip = (b & 0x40) >> 6
v_flip = (b & 0x80) >> 7
return TileMapping(tile_index, palette, h_flip, v_flip, priority)
def make_tilemap_canvas(tilemap, tiles, cols=64, rows=64, tile_adjust=0, pal_adjust=-1, tile_modulo=0x80):
'''
Battle bg is 64x64 map size, 8x8 tile size
4bpp tiles
'''
canvas = Canvas_Indexed(cols, rows)
for i in range(len(tilemap)//2):
tm = parse_tileset_word(tilemap[i*2:(i+1)*2])
if cols > 32:
x = (i % 32) + 32*((i//1024) % 2)
y = (i //32) - 32*((i//1024) % 2)
else:
x = i % cols
y = i //cols
try:
tile = tiles[(tm.tile+tile_adjust)%tile_modulo]
canvas.draw_tile(x, y, tile, tm.h_flip, tm.v_flip, tm.palette+pal_adjust)
except BaseException as e:
print(e, tm.palette, hex(tm.tile,2), hex(tile_adjust,2), hex(tm.tile+tile_adjust,2))
return canvas
def make_tilemap_pixmap(tilemap, tiles, palettes, cols=64, rows=64, tile_adjust=0, pal_adjust=-1):
'''
Battle bg is 64x64 map size, 8x8 tile size
4bpp tiles
'''
canvas = Canvas(cols, rows)
for i in range(len(tilemap)//2):
tile_index, p, h_flip, v_flip, priority = parse_tileset_word(tilemap[i*2:(i+1)*2])
if cols > 32:
x = (i % 32) + 32*((i//1024) % 2)
y = (i //32) - 32*((i//1024) % 2)
else:
x = i % cols
y = i //cols
try:
palette = palettes[p+pal_adjust]
tile = tiles[(tile_index+tile_adjust)%0x80]
tile.setColorTable(palette)
tile_px = QPixmap.fromImage(tile)
canvas.draw_pixmap(x, y, tile_px, h_flip, v_flip)
except BaseException as e:
print(e, p, hex(tile_index,2), hex(tile_adjust,2), hex(tile_index+tile_adjust,2))
return canvas.pixmap(True)
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.
'''
palettes = [generate_palette(rom, 0x14BB31+(i*0x20)) for i in range(84)]
battle_bgs = []
for i in range(34):
bg = {
'tileset_id': rom[0x14BA21+(i*8)],
'pal1_id': rom[0x14BA22+(i*8)],
'pal2_id': rom[0x14BA23+(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[bg['pal1_id']] + palettes[bg['pal2_id']]
battle_bgs.append(bg)
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:]
tileset.append([create_tile_indexed(r[i*32:(i+1)*32]) for i in range(len(r)//32)])
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 = 8 # 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:
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_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.
if i2 > 0x67:
i3 = ((i2 - 0x67) << 11) + 0x1A0000
elif i2 > 0x52:
i3 = ((i2 - 0x52) << 9) + 0x1A0000
elif i2 > 0x4B:
i3 = ((i2 - 0x4B) << 11) + 0x1AC800
elif i2 > 0x32:
i3 = ((i2 - 0x32) << 10) + 0x1A0000
else:
i3 = (i2 << 9) + 0x1A0000
return i3