ref: 3da7cb4231ab12fc937c34405d888bca2bab03c4
dir: /tables/sprite_sheets.py/
from PIL import Image import sprite_sheet_info import util from util import get_bytes, get_words, get_byte, cache import array import tables def save_as_png(dimensions, data, fname, palette = None): img = Image.new('L' if palette == None else 'P', dimensions) img.putdata(data) if palette != None: img.putpalette(palette) img.save(fname) def save_as_24bpp_png(dimensions, data, fname): img = Image.new("RGB", dimensions) img.putdata(data) img.save(fname) @cache def decode_2bit_tileset(tileset): data = util.decomp(tables.kCompSpritePtrs[tileset], get_byte, False) assert len(data) == 0x400 height = 32 dst = bytearray(128*height) def decode_2bit(offs, toffs): for y in range(8): d0, d1 = data[offs + y * 2], data[offs + y * 2 + 1] for x in range(8): t = ((d0 >> x) & 1) * 1 + ((d1 >> x) & 1) * 2 dst[toffs + y * 128 + (7 - x)] = t * 255 // 3 for i in range(16*height//8): x = i % 16 y = i // 16 decode_2bit(i * 16, x * 8 + y * 8 * 128) return dst def is_high_3bit_tileset(tileset): # is 0x5c, 0x5e, 0x5f included or not? return tileset in (0x52, 0x53, 0x5a, 0x5b, 0x5c, 0x5e, 0x5f) def get_unpacked_snes_tileset(tileset): if tileset < 12: return get_bytes(tables.kCompSpritePtrs[tileset], 0x600) else: return util.decomp(tables.kCompSpritePtrs[tileset], get_byte, False) @cache def decode_3bit_tileset(tileset): data = get_unpacked_snes_tileset(tileset) assert len(data) == 0x600 base = 8 if is_high_3bit_tileset(tileset) else 0 height = 32 dst = bytearray(128*height) def decode_3bit(offs, toffs): for y in range(8): d0, d1, d2 = data[offs + y * 2], data[offs + y * 2 + 1], data[offs + y + 16] for x in range(8): t = ((d0 >> x) & 1) * 1 + ((d1 >> x) & 1) * 2 + ((d2 >> x) & 1) * 4 dst[toffs + y * 128 + (7 - x)] = base + t for i in range(16*height//8): x, y = i % 16, i // 16 decode_3bit(i * 24, x * 8 + y * 8 * 128) return dst def decode_4bit_tileset_link(): height = 448 data = get_bytes(0x108000, 0x800 * height // 32) # only link sprites for now dst = bytearray(128*height) def decode_4bit(offs, toffs): for y in range(8): d0, d1, d2, d3 = data[offs + y * 2 + 0], data[offs + y * 2 + 1], data[offs + y * 2 + 16], data[offs + y * 2 + 17] for x in range(8): t = ((d0 >> x) & 1) * 1 + ((d1 >> x) & 1) * 2 + ((d2 >> x) & 1) * 4 + ((d3 >> x) & 1) * 8 dst[toffs + y * 128 + (7 - x)] = t for i in range(16*height//8): x, y = i % 16, i // 16 decode_4bit(i * 32, x * 8 + y * 8 * 128) return dst def convert_snes_palette(v): rr=bytearray() for x in v: r, g, b = x & 0x1f, x >> 5 & 0x1f, x >> 10 & 0x1f rr.extend((r << 3 | r >> 2, g << 3 | g >> 2, b << 3 | b >> 2)) return rr def convert_int24_palette_to_bytes(v): rr=bytearray() for x in v: rr.extend((x & 0xff, x >> 8 & 0xff, x >> 16 & 0xff)) return rr def convert_snes_palette_to_int(v): rr=[] for x in v: r, g, b = x & 0x1f, x >> 5 & 0x1f, x >> 10 & 0x1f rr.append((r << 3 | r >> 2) | (g << 3 | g >> 2) << 8 | (b << 3 | b >> 2) << 16) return rr def decode_link_sprites(): kLinkPalette = [0, 0x7fff, 0x237e, 0x11b7, 0x369e, 0x14a5, 0x1ff, 0x1078, 0x599d, 0x3647, 0x3b68, 0xa4a, 0x12ef, 0x2a5c, 0x1571, 0x7a18] save_as_png((128, 448), decode_4bit_tileset_link(), 'linksprite.png', convert_snes_palette(kLinkPalette)) # Returns the dungeon palette for the specified palette index # 0 = lightworld, 1 = darkworld, 2 = dungeon @cache def get_palette_subidx(palset_idx, dungeon_or_ow, which_palette): spmain = 1 if (dungeon_or_ow == 1) else 0 if dungeon_or_ow == 2: main, sp0l, sp5l, sp6l = tables.kDungPalinfos[palset_idx] sp0r = (main // 2) + 11 sp6r = 10 else: sp5l, sp6l = tables.kOwSprPalInfo[palset_idx * 2 : palset_idx * 2 + 2] sp0l = 3 if dungeon_or_ow == 1 else 1 sp0r = 9 if dungeon_or_ow == 1 else 7 sp6r = 8 if dungeon_or_ow == 1 else 6 pal_defs = (sp0l, sp0r, spmain, None, spmain, None, spmain, None, None, None, sp5l, None, sp6l, sp6r, None, None) return pal_defs[which_palette] @cache def get_palette_subset(pal_idx, j): if j == None: j = 0 pal = [0] * 7 if pal_idx == 0: # 0 kPalette_SpriteAux3 = get_words(0x9BD39E, 84) return kPalette_SpriteAux3[j * 7 : j * 7 + 7] if pal_idx == 1: # 0R if j < 11: kPalette_MiscSprite = get_words(0x9BD446, 77) return kPalette_MiscSprite[j * 7 : j * 7 + 7] else: kPalette_DungBgMain = get_words(0x9BD734, 1800) return kPalette_DungBgMain[(j - 11) * 90 : (j - 11) * 90 + 7] if pal_idx in (2, 3, 4, 5, 6, 7, 8, 9): o = j * 60 + ((pal_idx - 2) >> 1) * 15 + (pal_idx & 1) * 8 kPalette_MainSpr = get_words(0x9BD218, 120) return kPalette_MainSpr[o : o + 7] if pal_idx in (10, 12): kPalette_SpriteAux1 = get_words(0x9BD4E0, 168) return kPalette_SpriteAux1[j * 7 : j * 7 + 7] assert pal_idx != 11 if pal_idx == 13: kPalette_MiscSprite = get_words(0x9BD446, 77) return kPalette_MiscSprite[j * 7 : j * 7 + 7] elif pal_idx == 14: kPalette_ArmorAndGloves = get_words(0x9BD308, 75) return kPalette_ArmorAndGloves[1:8] elif pal_idx == 15: kPalette_ArmorAndGloves = get_words(0x9BD308, 75) return kPalette_ArmorAndGloves[9:16] @cache def get_full_palette(pal_idx, pal_subidx): rv = [] if pal_idx & 1: rv.extend([0x00fe00] * 9) else: rv.extend([0x00fe00] * 1) rv.extend(convert_snes_palette_to_int(get_palette_subset(pal_idx, pal_subidx))) rv += [0xe000e0] * (256 - len(rv)) # change the transparent to teal #008080 for i in range(0, 128, 8): rv[i] = 0x808000 rv[251] = 0xe0c0c0 # blueish text rv[252] = 0xc0c0c0 # palette text rv[253] = 0xf0f0f0 # unallocated rv[254] = 0x404040 # lines rv[255] = 0xe0e0e0 # bg return rv @cache def get_font_3x5(): return Image.open('../other/3x5_font.png').tobytes() def draw_letter3x5(dst, dst_pitch, dx, dy, ch, color): font = get_font_3x5() dst_offs = dy * dst_pitch + dx src_offs = (ord(ch) & 31) * 4 + (ord(ch) // 32) * 6 * 128 for y in range(6): for x in range(3): if font[src_offs + y * 128 + x] == 0: dst[dst_offs + y * dst_pitch + x] = color def draw_string3x5(dst, dst_pitch, dx, dy, s, color): for ch in s: draw_letter3x5(dst, dst_pitch, dx, dy, ch, color) dx += 4 def convert_to_24bpp(data, palette): out = [0] * len(data) for i in range(len(data)): out[i] = palette[data[i]] return out def concat_byte_arrays(ar): r = bytearray() for a in ar: r += a return r def fixup_sprite_set_entry(e, e_prev): sprite_index = int(e.name[:2], 16) if e.name[0] != 'X' else 10000 if e.dungeon_or_ow != 3: e.tileset = tables.kSpriteTilesets[e.tileset + (64 if e.dungeon_or_ow == 2 else 0)][e.ss_idx] e.high_palette = is_high_3bit_tileset(e.tileset) e.skip_header = False if e.pal_base == None: if sprite_index < len(tables.kSpriteInit_Flags3): e.pal_base = (tables.kSpriteInit_Flags3[sprite_index] >> 1) & 7 else: e.pal_base = 4 # just default to some palette e.pal_idx = e.pal_base * 2 + e.high_palette e.pal_subidx = get_palette_subidx(e.palset_idx, e.dungeon_or_ow, e.pal_idx) if e_prev != None and e_prev.name == e.name and e_prev.pal_idx == e.pal_idx and e_prev.pal_subidx == e.pal_subidx: e.skip_header = True # the (pal_idx, pal_subidx) tuple identifies the palette e.encoded_id = e.pal_base | e.tileset << 3 | e.skip_header << 10 | (e.pal_subidx or 0) << 11 e.encoded_id = e.encoded_id << 8 | ((e.encoded_id + 41) % 255) BIGW = 148 def save_sprite_set_entry(finaldst, e, master_tilesheets = None): empty_253 = [253] * 8 def fill_with_empty(dst, dst_offs): for y in range(8): o = dst_offs + y * BIGW dst[o : o + 8] = empty_253 def copy_8x8(dst, dst_offs, src, src_offs, base): for y in range(8): for x in range(8): dst[dst_offs + y * BIGW + x] = src[src_offs + y * 128 + x] + base def hline(dst, x1, x2, y, color): for x in range(x1, x2 + 1): dst[y * BIGW + x] = color def vline(dst, x, y1, y2, color): for y in range(y1, y2 + 1): dst[y * BIGW + x] = color def fillrect(dst, x1, x2, y1, y2, color): for y in range(y1, y2 + 1): hline(dst, x1, x2, y, color) def encode_id(dst, x, y, v): while v: if v & 1: dst[y * BIGW + x] = 253 x -= 1 v >>= 1 palette = get_full_palette(e.pal_idx, e.pal_subidx) if not e.skip_header: pal_subidx = e.pal_subidx if e.high_palette: pal_name = '%sR' % e.pal_base else: pal_name = '%s' % e.pal_base if e.pal_base in (1, 2, 3): assert e.pal_subidx in (0, 1) pal_subidx = 'LW' if pal_subidx == 0 else 'DW' if pal_subidx != None: pal_name = '%s-%s' % (pal_name, pal_subidx) header = bytearray([255] * BIGW * 9) draw_string3x5(header, BIGW, 1, 3, e.name[:22], 254) for i in range(7): xx = BIGW - 37 + i * 5 - 9 fillrect(header, xx, xx + 4, 3, 7, i + (9 if e.high_palette else 1)) draw_string3x5(header, BIGW, BIGW - 9, 3, '%2s' % e.tileset, 252) draw_string3x5(header, BIGW, BIGW - 45 - 1 - len(pal_name) * 4, 3, pal_name, 252) finaldst.extend(convert_to_24bpp(header, palette)) bigdst = bytearray([255] * BIGW * 36) hline(bigdst, 1, 137, 0, 254) hline(bigdst, 1, 137, 34, 254) vline(bigdst, 1, 0, 34, 254) vline(bigdst, 137, 0, 34, 254) encode_id(bigdst, 137, 35, e.encoded_id << 9 | 0x55) src = decode_3bit_tileset(e.tileset) # returns 128x64 for i in range(16*4): x, y = i % 16, i // 16 dst_offs = (y * 8 + 1 + (y >> 1)) * BIGW + x * 8 + 2 + (x >> 1) if x & 8: dst_offs m = e.matrix[y][x] if m == '.': fill_with_empty(bigdst, dst_offs) else: copy_8x8(bigdst, dst_offs, src, x * 8 + y * 8 * 128, 0) if master_tilesheets: master_tilesheets.insert(e.tileset, src, x, y, palette, 0) # display vram addresses draw_string3x5(bigdst, BIGW, BIGW - 9, 4, '%Xx' % (e.ss_idx * 0x4), 251) draw_string3x5(bigdst, BIGW, BIGW - 9, 4 + 17, '%Xx' % (e.ss_idx * 0x4 + 2), 251) # collapse unused matrix sections if all(m=='.' for m in e.matrix[0]) and all(m=='.' for m in e.matrix[1]): del bigdst[1*BIGW:17*BIGW] elif all(m=='.' for m in e.matrix[2]) and all(m=='.' for m in e.matrix[3]): del bigdst[18*BIGW:34*BIGW] if e.skip_header: draw_string3x5(bigdst, BIGW, BIGW - 9, len(bigdst)//BIGW - 6, '%.2d' % e.tileset, 252) finaldst.extend(convert_to_24bpp(bigdst, palette)) def fixup_entries(): e_prev = None for e in sprite_sheet_info.entries: fixup_sprite_set_entry(e, e_prev) e_prev = e class MasterTilesheets: def __init__(self): self.sheets = {} def insert(self, tileset, src, xp, yp, palette, pal_base): sheet = self.sheets.get(tileset) if sheet == None: sheet = (array.array('I', [0x00f000] * 128 * 32), None) self.sheets[tileset] = sheet sheet24 = sheet[0] for y in range(yp * 8, yp * 8 + 8): for x in range(xp * 8, xp * 8 + 8): o = y * 128 + x sheet24[o] = palette[pal_base + src[o]] def add_verify_8x8(self, tileset, pal_lut, img_data, pitch, dst_pos, src_pos): # add to the master sheet sheet = self.sheets.get(tileset) if sheet == None: sheet = (array.array('I', [0x00f000] * 128 * 32), bytearray([248] * 128 * 32)) self.sheets[tileset] = sheet sheet24, sheet8 = sheet[0], sheet[1] for y in range(8): for x in range(8): pixel = img_data[src_pos + y * pitch + x] o = dst_pos + y * 128 + x sheet24[o] = pixel pixel8 = pal_lut.get(pixel) if pixel8 == None: raise Exception('Pixel #%.6x not found' % pixel) if sheet8[o] != 248 and pixel8 != sheet8[o]: raise Exception('Pixel has more than one value') sheet8[o] = pixel8 def save_to_all_sheets(self, save_also_8bit = False): rv = array.array('I') if save_also_8bit: rv8 = bytearray() def extend_with_string_line(text): header = array.array('I', [0xf0f0f0] * 128 * 8) draw_string3x5(header, 128, 1, 2, text, 0x404040) rv.extend(header) if save_also_8bit: header8 = bytearray([255] * 128 * 8) draw_string3x5(header8, 128, 1, 2, text, 254) rv8.extend(header8) extend_with_string_line('AUTO GENERATED DO NOT EDIT') kk = 0 for k in sorted(self.sheets.keys()): extend_with_string_line('Sheet %d' % k) rv.extend(self.sheets[k][0]) if save_also_8bit: rv8.extend(self.sheets[k][1]) if k != kk: print('Missing %d' % kk) kk = k + 1 if save_also_8bit: palette8 = get_full_palette(4, 0) for i, v in enumerate(convert_snes_palette_to_int(get_palette_subset(5, 0))): palette8[i + 9] = v palette8[248] = 0x00f000 save_as_png((128, len(rv)//128), rv8, 'sprites/all_sheets_8bit.png', palette = convert_int24_palette_to_bytes(palette8)) self.verify_identical_to_snes() save_as_24bpp_png((128, len(rv)//128), rv, 'sprites/all_sheets.png') def verify_identical_to_snes(self): for tileset in range(103): a = self.encode_sheet_in_snes_format(tileset) b = get_unpacked_snes_tileset(tileset) if a != b: print('Sheet %d mismatch' % tileset) break # convert a tileset to the 8bit snes format, 24 bytes per tile def encode_sheet_in_snes_format(self, tileset): sheet8 = self.sheets[tileset][1] result = bytearray(24 * 16 * 4) def encode_into(dst_pos, src_pos): for y in range(8): for x in range(8): b = sheet8[src_pos + 128 * y + 7 - x] result[dst_pos+2*y] |= (b & 1) << x result[dst_pos+2*y+1] |= ((b & 2) >> 1) << x result[dst_pos+y+16] |= ((b & 4) >> 2) << x for y in range(4): for x in range(16): encode_into((y * 16 + x) * 24, y * 8 * 128 + x * 8) return result def decode_sprite_sheets(): master_tilesheets = MasterTilesheets() fixup_entries() for char in "0123456789ABCDEFX": dst = [] for e in sprite_sheet_info.entries: if e.name[0].upper() == char: save_sprite_set_entry(dst, e, master_tilesheets) if len(dst): save_as_24bpp_png((BIGW, len(dst) // BIGW), dst, 'sprites/sprites_%s.png' % char) master_tilesheets.save_to_all_sheets() def load_sprite_sheets(): master_tilesheets = MasterTilesheets() for char in "0123456789ABCDEFX": img = Image.open('sprites/sprites_%s.png' % char) img_size = img.size img_data = array.array('I', (a[0] | a[1] << 8 | a[2] << 16 for a in img.getdata())) pitch = img.size[0] def find_next_tag(start_pos): for i in range(start_pos, len(img_data) - pitch * 2, pitch if start_pos != 0 else 1): if img_data[i+0] == 0x404040 and img_data[i+pitch] == 0x404040 and \ img_data[i+pitch * 2 + 0] == 0xf0f0f0 and img_data[i+pitch * 2 - 1] == 0xe0e0e0 and \ img_data[i+pitch * 2 - 2] == 0xf0f0f0 and img_data[i+pitch * 2 - 3] == 0xe0e0e0 and \ img_data[i+pitch * 2 - 4] == 0xf0f0f0 and img_data[i+pitch * 2 - 5] == 0xe0e0e0 and \ img_data[i+pitch * 2 - 6] == 0xf0f0f0 and img_data[i+pitch * 2 - 7] == 0xe0e0e0: return i + pitch * 2 else: return None def decode_tag(pos): r = 0 for i in range(64): v = img_data[pos - 63 + i] assert v in (0xe0e0e0, 0xf0f0f0) r = r * 2 + (v == 0xf0f0f0) if (r & 0x1ff) != 0x55: raise Exception('Invalid tag') r >>= 9 if (((r >> 8) + 41) % 255) != (r & 0xff): raise Exception('Invalid checksum') r >>= 8 return r & 7, (r >> 3) & 127, (r >> 10) & 1, (r >> 11) & 31 def determine_image_rects(tag_pos): h = 1 while (img_data[tag_pos - pitch * (h + 1)] == 0x404040): h += 1 if h == 19: if img_data[tag_pos - pitch * 2 - 1] == 0xe0e0e0: image_rects = ((0, tag_pos - 135 - pitch * 18), ) elif img_data[tag_pos - pitch * 18 - 1] == 0xe0e0e0: image_rects = ((1, tag_pos - 135 - pitch * 17), ) else: raise Exception('cant uncompact') elif h == 35: image_rects = ((0, tag_pos - 135 - pitch * 34), (1, tag_pos - 135 - pitch * 17)) else: raise Exception('height not found') return image_rects, h def get_pal_lut(tag_pos, is_high): # read palette colors from the image pixels pal_lut = {img_data[tag_pos + 5 * i] : i + (9 if is_high else 1) for i in range(7)} pal_lut[0x808000] = 0 # transparent color return pal_lut def is_empty(pos): return all(img_data[pos+y*pitch+x] == 0xf0f0f0 for x in range(8) for y in range(8)) pal_lut = None tag_pos = 0 while True: tag_pos = find_next_tag(tag_pos) if tag_pos == None: break pal_base, tileset, headerless, pal_subidx = decode_tag(tag_pos) image_rects, box_height = determine_image_rects(tag_pos) if not headerless: pal_lut = get_pal_lut(tag_pos - pitch * (box_height + 3) - 34, is_high_3bit_tileset(tileset)) for idx, sheet_pos in image_rects: for y in range(2): for x in range(16): src_pos = sheet_pos + (y * 8 + (y >> 1)) * pitch + (x * 8 + (x >> 1)) dst_pos = (y + idx * 2) * 8 * 128 + x * 8 if not is_empty(src_pos): master_tilesheets.add_verify_8x8(tileset, pal_lut, img_data, pitch, dst_pos, src_pos) return master_tilesheets