import struct import sys from dataclasses import dataclass from pprint import pprint, pformat import textwrap from typing import Union, Optional import zlib from operator import itemgetter class CustomInt: value: int def __init__(self, value): self.value = value def __int__(self): return self.value def __eq__(self, other): return self.value == other def __eq__(self, other): return self.value == other class HexInt(CustomInt): def __repr__(self): return hex(self.value) class BinInt(CustomInt): def __repr__(self): return bin(self.value) @dataclass class Header: file_size: int magic_number: HexInt frames: int width_in_pixels: int height_in_pixels: int color_depth: int flags: HexInt speed: int transparent_palette_index: int number_of_colors: int pixel_width: int pixel_height: int x_position_of_grid: int y_position_of_grid: int grid_width: int grid_height: int def dword(mem): return struct.unpack("= 6, chunk_size data = mem[0:chunk_size - 6] mem = skip(mem, chunk_size - 6) chunk = Chunk( chunk_size = chunk_size, chunk_type = HexInt(chunk_type), data = data ) return chunk, mem @dataclass class PaletteChunkPacket: entries_to_skip: int number_of_colors: int colors: list[tuple[int, int, int]] @dataclass class OldPaletteChunk: number_of_packets: int packets: list[PaletteChunkPacket] def parse_old_palette_chunk(mem): number_of_packets, mem = word(mem) packets = [] for _ in range(number_of_packets): entries_to_skip, mem = byte(mem) number_of_colors, mem = byte(mem) assert entries_to_skip == 0, entries_to_skip colors = [] for _ in range(number_of_colors): red, mem = byte(mem) green, mem = byte(mem) blue, mem = byte(mem) colors.append((red, green, blue)) packets.append(PaletteChunkPacket( entries_to_skip = entries_to_skip, number_of_colors = number_of_colors, colors = colors, )) old_palette_chunk = OldPaletteChunk( number_of_packets = number_of_packets, packets = packets ) return old_palette_chunk, mem @dataclass class PaletteChunkEntry: red: int green: int blue: int alpha: int color_name: str def parse_palette_chunk_entry(mem): flag, mem = word(mem) red, mem = byte(mem) green, mem = byte(mem) blue, mem = byte(mem) alpha, mem = byte(mem) color_name = None if flag & (1 << 0): color_name, mem = string(mem) palette_chunk_entry = PaletteChunkEntry( red = red, green = green, blue = blue, alpha = alpha, color_name = color_name ) return palette_chunk_entry, mem @dataclass class PaletteChunk: new_palette_size: int first_color_index_to_change: int last_color_index_to_change: int entries: list[PaletteChunkEntry] def parse_palette_chunk(mem): new_palette_size, mem = dword(mem) first_color_index_to_change, mem = dword(mem) last_color_index_to_change, mem = dword(mem) mem = skip(mem, 8) length = last_color_index_to_change - first_color_index_to_change assert length > 0, length entries = [] for _ in range(length + 1): palette_chunk_entry, mem = parse_palette_chunk_entry(mem) entries.append(palette_chunk_entry) palette_chunk = PaletteChunk( new_palette_size = new_palette_size, first_color_index_to_change = first_color_index_to_change, last_color_index_to_change = last_color_index_to_change, entries = entries ) return palette_chunk, mem @dataclass class TilesetChunkExternal: id_of_external_file: int tileset_id_in_external_file: int @dataclass class TilesetChunkInternal: data_length: int pixel: memoryview @dataclass class TilesetChunk: tileset_id: int tileset_flags: HexInt number_of_tiles: int tile_width: int tile_height: int base_index: int name_of_tileset: str data: Union[TilesetChunkExternal, TilesetChunkInternal] def parse_tileset_chunk(mem): _link_to_external_file = (1 << 0) _tiles_inside_this_file = (1 << 1) tileset_id, mem = dword(mem) tileset_flags, mem = dword(mem) number_of_tiles, mem = dword(mem) tile_width, mem = word(mem) tile_height, mem = word(mem) base_index, mem = short(mem) mem = skip(mem, 14) name_of_tileset, mem = string(mem) assert (tileset_flags & 0b11) != 0, tileset_flags data = None if tileset_flags & _link_to_external_file: id_of_external_file, mem = dword(mem) tileset_id_in_external_file, mem = dword(mem) data = TilesetChunkExternal( id_of_external_file, tileset_id_in_external_file, ) elif tileset_flags & _tiles_inside_this_file: data_length, mem = dword(mem) pixel = mem[0:data_length] mem = skip(mem, data_length) data = TilesetChunkInternal( data_length, zlib.decompress(pixel), ) tileset_chunk = TilesetChunk( tileset_id = tileset_id, tileset_flags = HexInt(tileset_flags), number_of_tiles = number_of_tiles, tile_width = tile_width, tile_height = tile_height, base_index = base_index, name_of_tileset = name_of_tileset, data = data, ) return tileset_chunk, mem @dataclass class LayerChunk: flags: int layer_type: int layer_child_level: int default_layer_width_in_pixels: int default_layer_height_in_pixels: int blend_mode: int opacity: int layer_name: str tileset_index: Optional[int] layer_uuid: Optional[bytes] def parse_layer_chunk(mem, header_flags): flags, mem = word(mem) layer_type, mem = word(mem) layer_child_level, mem = word(mem) default_layer_width_in_pixels, mem = word(mem) default_layer_height_in_pixels, mem = word(mem) blend_mode, mem = word(mem) opacity, mem = byte(mem) mem = skip(mem, 3) layer_name, mem = string(mem) tileset_index = None if layer_type == 2: tileset_index, mem = dword(mem) layer_uuid = None if header_flags & (1 << 3): layer_uuid, mem = uuid(mem) layer_chunk = LayerChunk( flags = flags, layer_type = layer_type, layer_child_level = layer_child_level, default_layer_width_in_pixels = default_layer_width_in_pixels, default_layer_height_in_pixels = default_layer_height_in_pixels, blend_mode = blend_mode, opacity = opacity, layer_name = layer_name, tileset_index = tileset_index, layer_uuid = layer_uuid, ) return layer_chunk, mem @dataclass class CelChunk_RawImageData: width_in_pixels: int height_in_pixes: int pixel: memoryview @dataclass class CelChunk_LinkedCell: frame_position: int @dataclass class CelChunk_CompressedImage: width_in_pixels: int height_in_pixels: int pixel: memoryview @dataclass class CelChunk_CompressedTilemap: width_in_number_of_tiles: int height_in_number_of_tiles: int bits_per_tile: int bitmask_for_tile_id: int bitmask_for_x_flip: int bitmask_for_y_flip: int bitmask_for_diagonal_flip: int tile: memoryview @dataclass class CelChunk: layer_index: int x_position: int y_position: int opacity_level: int cel_type: int z_index: int data: Union[CelChunk_RawImageData, CelChunk_LinkedCell, CelChunk_CompressedImage, CelChunk_CompressedTilemap] def parse_cel_chunk(mem): layer_index, mem = word(mem) x_position, mem = short(mem) y_position, mem = short(mem) opacity_level, mem = byte(mem) cel_type, mem = word(mem) z_index, mem = short(mem) mem = skip(mem, 5) assert cel_type in {0, 1, 2, 3}, cel_type data = None if cel_type == 0: width_in_pixels, mem = word(mem) height_in_pixels, mem = word(mem) pixel = mem data = CelChunk_RawImageData( width_in_pixels, height_in_pixels, pixel, ) if cel_type == 1: frame_position, mem = word(mem) data = CelChunk_LinkedCell(frame_position) if cel_type == 2: width_in_pixels, mem = word(mem) height_in_pixels, mem = word(mem) pixel = memoryview(zlib.decompress(mem)) data = CelChunk_CompressedImage( width_in_pixels, height_in_pixels, pixel, ) if cel_type == 3: width_in_number_of_tiles, mem = word(mem) height_in_number_of_tiles, mem = word(mem) bits_per_tile, mem = word(mem) bitmask_for_tile_id, mem = dword(mem) bitmask_for_x_flip, mem = dword(mem) bitmask_for_y_flip, mem = dword(mem) bitmask_for_diagonal_flip, mem = dword(mem) mem = skip(mem, 10) tile_mem = memoryview(zlib.decompress(mem)) assert len(tile_mem) % 4 == 0 tile = [ struct.unpack("> 3) << 0) | ((green >> 3) << 5) | ((blue >> 3) << 10) ) return struct.pack(">H", bgr) def pack_index(i): return struct.pack(">I", i) 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(palette_chunk): with open("palette.bin", "wb") as f: 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)) print("palette.bin", f.tell(), file=sys.stderr) def pack_palette(palette): if type(palette) is PaletteChunk: pack_palette_chunk(palette) elif type(palette) is OldPaletteChunk: pack_old_palette_chunk(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_patterns_2x2(filename, tileset_chunk): with open(filename, "wb") as f: for i in range(tileset_chunk.number_of_tiles): offset = tileset_chunk.tile_width * tileset_chunk.tile_height * i buf = pack_character_2x2(tileset_chunk, offset) f.write(buf) print(filename, f.tell(), file=sys.stderr) def pack_pattern_name_table_2x2(filename, cel_chunk): with open(filename, "wb") as f: 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 h_pages = ((tile_width + 31) & (~31)) // 32 v_pages = ((tile_height + 31) & (~31)) // 32 for v_page in range(v_pages): for h_page in range(h_pages): for y in range(32): for x in range(32): tx = (h_page * 32) + x ty = (v_page * 32) + y if tx >= tile_width or ty >= tile_height: f.write(pack_index(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 pattern = (int(y_flip) << 31) | (int(x_flip) << 30) | tile_id f.write(pack_index(pattern)) print(filename, f.tell(), file=sys.stderr) header, mem = parse_header(mem) #pprint(header) assert header.color_depth == 8, header.color_depth frame_header, mem = parse_frame_header(mem) #pprint(frame_header) tilesets = dict() # by tileset id layers = [] palette = None cel_chunks = dict() # by layer index for _ in range(frame_header.number_of_chunks): chunk, mem = parse_chunk(mem) #pprinti(chunk, 1) if chunk.chunk_type == 0x4: old_palette_chunk, _ = parse_old_palette_chunk(chunk.data) #pprinti(old_palette_chunk, 2) assert palette is None palette = old_palette_chunk elif chunk.chunk_type == 0x2019: palette_chunk, _ = parse_palette_chunk(chunk.data) #pprinti(palette_chunk, 2) assert palette is None palette = palette_chunk elif chunk.chunk_type == 0x2023: tileset_chunk, _ = parse_tileset_chunk(chunk.data) assert tileset_chunk.tileset_id not in tilesets tilesets[tileset_chunk.tileset_id] = tileset_chunk #pprinti(tileset_chunk, 2) elif chunk.chunk_type == 0x2004: layer_chunk, _ = parse_layer_chunk(chunk.data, header.flags) assert layer_chunk.layer_type == 2 layers.append(layer_chunk) #pprinti(layer_chunk, 2) elif chunk.chunk_type == 0x2005: cel_chunk, _ = parse_cel_chunk(chunk.data) #pprinti(cel_chunk, 2) assert cel_chunk.layer_index not in cel_chunks cel_chunks[cel_chunk.layer_index] = cel_chunk elif chunk.chunk_type == 0x2020: # user data pass else: print("unhandled chunk: ") pprinti(chunk, 1) assert palette is not None pack_palette(palette) for tileset_index, tileset_chunk in sorted(tilesets.items(), key=itemgetter(0)): filename = f"character_pattern__tileset_{tileset_index}.bin" pack_character_patterns_2x2(filename, tileset_chunk) for layer_index, cel_chunk in sorted(cel_chunks.items(), key=itemgetter(0)): filename = f"pattern_name_table__layer_{layer_index}.bin" pack_pattern_name_table_2x2(filename, cel_chunk) for layer_index, layer_chunk in enumerate(layers): print(f"layer={layer_index} tileset={layer_chunk.tileset_index}");