import struct import sys from dataclasses import dataclass from typing import Union, Optional import zlib 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("