saturn-aseprite/background.py

304 lines
10 KiB
Python

import io
import sys
from pprint import pprint, pformat
import textwrap
import struct
from operator import itemgetter
from aseprite import parse_file
from aseprite import PaletteChunk, OldPaletteChunk, TilesetChunkInternal, CelChunk_CompressedTilemap
import os
PNB = os.environ.get("PNB", "2WORD")
assert PNB in {"2WORD", "1WORD"}
def pprinti(o, i):
s = pformat(o)
print(textwrap.indent(s, ' ' * i))
def pack_bgr555(red, green, blue):
bgr = (
((red >> 3) << 0) |
((green >> 3) << 5) |
((blue >> 3) << 10)
)
return struct.pack(">H", bgr)
# 2x2
def pack_index_2word(y_flip, x_flip, id):
tile_id = id * 8
assert tile_id < 0x7fff, tile_id
pattern = (int(y_flip) << 31) | (int(x_flip) << 30) | tile_id
return struct.pack(">I", pattern)
def pack_index_1word(y_flip, x_flip, id):
tile_id = (id * 8) >> 2
assert tile_id < 0x3ff, tile_id
pattern = (int(y_flip) << 11) | (int(x_flip) << 10) | tile_id
return struct.pack(">H", pattern)
def pack_index(y_flip, x_flip, id):
if PNB == "2WORD":
return pack_index_2word(y_flip, x_flip, id)
else:
return pack_index_1word(y_flip, x_flip, id)
def pack_old_palette_chunk(old_palette_chunk):
with open("palette.bin", "wb") as f:
for color in old_palette_chunk.packets[0].colors:
f.write(pack_bgr555(*color))
def pack_palette_chunk(f, palette_chunk):
assert palette_chunk.first_color_index_to_change == 0
for entry in palette_chunk.entries:
color = (entry.red, entry.green, entry.blue)
f.write(pack_bgr555(*color))
def pack_palette(f, palette):
if type(palette) is PaletteChunk:
pack_palette_chunk(f, palette)
elif type(palette) is OldPaletteChunk:
pack_old_palette_chunk(f, palette)
else:
assert False, type(palette)
def pack_character_2x2(tileset_chunk, offset):
#tileset_chunk.number_of_tiles,
#tileset_chunk.tile_width,
#tileset_chunk.tile_height,
assert tileset_chunk.tile_width == 16
assert tileset_chunk.tile_height == 16
assert type(tileset_chunk.data) == TilesetChunkInternal
buf = bytearray(16 * 16)
for cell_ix in range(4):
for y in range(8):
for x in range(8):
tileset_x = 8 * (cell_ix % 2) + x
tileset_y = 8 * (cell_ix // 2) + y
px = tileset_chunk.data.pixel[offset + tileset_y * 16 + tileset_x]
buf[cell_ix * 8 * 8 + y * 8 + x] = px
return bytes(buf)
def pack_character_1x1(tileset_chunk, offset):
assert tileset_chunk.tile_width == 8
assert tileset_chunk.tile_height == 8
assert type(tileset_chunk.data) == TilesetChunkInternal
buf = bytearray(8 * 8)
for y in range(8):
for x in range(8):
tileset_x = x
tileset_y = y
px = tileset_chunk.data.pixel[offset + tileset_y * 8 + tileset_x]
buf[y * 8 + x] = px
return bytes(buf)
def pack_character_patterns(f, tileset_chunk):
for i in range(tileset_chunk.number_of_tiles):
offset = tileset_chunk.tile_width * tileset_chunk.tile_height * i
if tileset_chunk.tile_width == 8 and tileset_chunk.tile_height == 8:
buf = pack_character_1x1(tileset_chunk, offset)
elif tileset_chunk.tile_width == 16 and tileset_chunk.tile_height == 16:
buf = pack_character_2x2(tileset_chunk, offset)
else:
assert False, (tileset_chunk.tile_width, tileset_chunk.tile_height)
f.write(buf)
def pack_pattern_name_table(f, cel_chunk, x_cells, y_cells):
assert type(cel_chunk.data) == CelChunk_CompressedTilemap
#assert cel_chunk.data.width_in_number_of_tiles <= 64
#assert cel_chunk.data.height_in_number_of_tiles <= 64
tile_width = cel_chunk.data.width_in_number_of_tiles
tile_height = cel_chunk.data.height_in_number_of_tiles
print(tile_width, tile_height)
h_pages = ((tile_width + (x_cells - 1)) & (~(x_cells - 1))) // x_cells
v_pages = ((tile_height + (y_cells - 1)) & (~(y_cells - 1))) // y_cells
if h_pages > 2:
h_pages = 2
if v_pages > 2:
v_pages = 2
for v_page in range(v_pages):
for h_page in range(h_pages):
for y in range(y_cells):
for x in range(x_cells):
tx = (h_page * x_cells) + x
ty = (v_page * y_cells) + y
if tx >= tile_width or ty >= tile_height:
f.write(pack_index(0, 0, 0))
else:
cel_chunk_ix = ty * tile_width + tx
tile_data = cel_chunk.data.tile[cel_chunk_ix]
tile_id = tile_data & cel_chunk.data.bitmask_for_tile_id.value
x_flip = (tile_data & cel_chunk.data.bitmask_for_x_flip.value) != 0
y_flip = (tile_data & cel_chunk.data.bitmask_for_y_flip.value) != 0
f.write(pack_index(y_flip, x_flip, tile_id))
def generate_separate_files(mem):
tilesets, layers, palette, cel_chunks = parse_file(mem)
with open("palette.bin", "wb") as f:
pack_palette(f, palette)
for tileset_index, tileset_chunk in sorted(tilesets.items(), key=itemgetter(0)):
filename = f"character_pattern__tileset_{tileset_index}.bin"
print(filename)
with open(filename, "wb") as f:
pack_character_patterns(f, tileset_chunk)
for layer_index, cel_chunk in sorted(cel_chunks.items(), key=itemgetter(0)):
#layers[layer_index]
print(f"layer={layer_index} layer_name={layers[layer_index].layer_name} tileset={layers[layer_index].tileset_index}");
tileset_chunk = tilesets[layers[layer_index].tileset_index]
x_cells = 64 // (tileset_chunk.tile_width // 8)
y_cells = 64 // (tileset_chunk.tile_height // 8)
filename = f"pattern_name_table__layer_{layer_index}.bin"
print(filename)
with open(filename, "wb") as f:
pack_pattern_name_table(f, cel_chunk, x_cells, y_cells)
PNCN0__N0PNB__2WORD = (0 << 15) # PNB_2WORD
PNCN0__N0PNB__1WORD = (1 << 15) # PNB_1WORD
PNCN0__N0CNSM = (1 << 14) # CN_12BIT
PLSZ__N0PLSZ__1x1 = (0b00 << 0) # PL_SIZE_1x1
PLSZ__N0PLSZ__2x1 = (0b01 << 0) # PL_SIZE_2x1
PLSZ__N0PLSZ__2x2 = (0b11 << 0) # PL_SIZE_2x2
CHCTLA__N0CHCN__16_COLOR = (0b000 << 4) # COL_TYPE_16
CHCTLA__N0CHCN__256_COLOR = (0b001 << 4) # COL_TYPE_256
CHCTLA__N0CHCN__2048_COLOR = (0b010 << 4) # COL_TYPE_2048
CHCTLA__N0CHCN__32K_COLOR = (0b011 << 4) # COL_TYPE_32768
CHCTLA__N0CHCN__16M_COLOR = (0b100 << 4) # COL_TYPE_1M
CHCTLA__N0CHSZ__1x1_CELL = (0 << 0)
CHCTLA__N0CHSZ__2x2_CELL = (1 << 0)
def header_7shades(size_of_cel_data, # bytes
size_of_map_data, # bytes
tile_character_size, # CHCTLA__N0CHSZ
tile_color_mode, # CHCTLA__N0CHCN
plane_size, # PLSZ
map_data, # PNCN0
): #
file_type_id = 0x5
width = 1
height = 1
return struct.pack(">IIIIIIIHH",
file_type_id,
size_of_cel_data,
size_of_map_data,
tile_character_size,
tile_color_mode,
plane_size,
map_data,
width,
height)
def palette_pad_7shades(f_tmp, palette_size):
# - Pallet data block of 0, 32, or 512 bytes based on the color mode indicated in header
for valid_size in [32, 512]:
if palette_size > valid_size:
continue
while palette_size < valid_size:
f_tmp.write(bytes([0]))
palette_size += 1
break
else:
assert False, palette_size
return palette_size
def chctla_n0chsz(tileset_chunk):
if tileset_chunk.tile_width == 8 and tileset_chunk.tile_height == 8:
return CHCTLA__N0CHSZ__1x1_CELL
elif tileset_chunk.tile_width == 16 and tileset_chunk.tile_height == 16:
return CHCTLA__N0CHSZ__2x2_CELL
else:
assert False, (tileset_chunk.tile_width, tileset_chunk.tile_height)
def generate_7shades(mem, output_filename):
tilesets, layers, palette, cel_chunks = parse_file(mem)
f_tmp = io.BytesIO()
pack_palette(f_tmp, palette)
palette_size = f_tmp.getbuffer().nbytes
palette_size = palette_pad_7shades(f_tmp, palette_size)
print("palette_size", palette_size)
pattern_name_tables_start = f_tmp.getbuffer().nbytes
for layer_index, cel_chunk in sorted(cel_chunks.items(), key=itemgetter(0)):
tileset_chunk = tilesets[layers[layer_index].tileset_index]
x_cells = 64 // (tileset_chunk.tile_width // 8)
y_cells = 64 // (tileset_chunk.tile_height // 8)
pack_pattern_name_table(f_tmp, cel_chunk, x_cells, y_cells)
break
pattern_name_tables_end = f_tmp.getbuffer().nbytes
size_of_map_data = pattern_name_tables_end - pattern_name_tables_start
print("pattern_name_table_size", size_of_map_data)
character_patterns_start = f_tmp.getbuffer().nbytes
for tileset_index, tileset_chunk in sorted(tilesets.items(), key=itemgetter(0)):
pack_character_patterns(f_tmp, tileset_chunk)
break
character_patterns_end = f_tmp.getbuffer().nbytes
size_of_cel_data = character_patterns_end - character_patterns_start
print("character_patterns_size", size_of_cel_data)
tile_character_size = chctla_n0chsz(tileset_chunk)
tile_color_mode = CHCTLA__N0CHCN__256_COLOR
plane_size = PLSZ__N0PLSZ__2x2
map_data = PNCN0__N0PNB__2WORD if PNB == "2WORD" else PNCN0__N0PNB__1WORD
header = header_7shades(size_of_cel_data,
size_of_map_data,
tile_character_size,
tile_color_mode,
plane_size,
map_data)
with open(output_filename, "wb") as f:
f.write(header)
f.write(f_tmp.getvalue())
def read_input():
with open(sys.argv[1], 'rb') as f:
buf = f.read()
mem = memoryview(buf)
return mem
if len(sys.argv) == 3:
mem = read_input()
generate_7shades(mem, sys.argv[2])
elif len(sys.argv) == 2:
mem = read_input()
generate_separate_files(mem)
else:
print(f"usage:")
print()
print("separated raw binaries")
print(f" python {sys.argv[0]} input.aseprite")
print()
print("7shades output:")
print(f" python {sys.argv[0]} input.aseprite output.bin")