shithub: zelda3

ref: c487a04d8f02619ba3a34417638e51ade37a444f
dir: /tables/sprite_sheets.py/

View raw version
from PIL import Image
import sprite_sheet_info
import util
from util import get_bytes, get_words, get_byte, cache
import array
import tables
override_armor_palette = None
#override_armor_palette = [0x7fff, 0x237e, 0x11b7, 0x369e, 0x14a5,  0x1ff, 0x1078, 0x599d, 0x3647, 0x3b68, 0xa4a, 0x12ef, 0x2a5c, 0x1571, 0x7a18,
#                          0x7fff, 0x237e, 0x11b7, 0x369e, 0x14a5,  0x1ff, 0x1078, 0x599d, 0x6980, 0x7691, 0x26b8, 0x437f, 0x2a5c, 0x1199, 0x7a18,
#                          0x7fff, 0x237e, 0x11b7, 0x369e, 0x14a5,  0x1ff, 0x1078, 0x599d, 0x1057, 0x457e, 0x6df3, 0xfeb9, 0x2a5c, 0x2227, 0x7a18,
#                          0x7fff, 0x237e, 0x11b7, 0x369e, 0x14a5,  0x1ff, 0x1078, 0x3d97, 0x3647, 0x3b68, 0xa4a, 0x12ef, 0x567e, 0x1571, 0x7a18,
#                          0,      0x0EFA, 0x7DD1, 0,      0x7F1A,  0x7F1A,0,      0x716E, 0x7DD1, 0x40A7, 0x7DD1, 0x40A7, 0x48E9, 0x50CF, 0x7FFF]


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