#!/usr/bin/env python3 """Converte o assets/gaota.pyxel (Pyxel 0.9.10, 2019) para o formato .pyxres que o Pyxel 2.x abre. python3 -I convert-pyxel-assets.py O .pyxel velho e um gzip de um pickle de Python (dicionario com listas de arrays do numpy). Como e um arquivo baixado, ele NAO e lido com pickle.load: o Unpickler abaixo so aceita as poucas classes que o formato usa, todas substituidas por classes inertes daqui (nem o numpy e preciso), e recusa qualquer outra coisa. A saida e o formato de texto antigo de .pyxres (pasta pyxel_resource/ com "version", "image" e "tilemap"), que o Pyxel 2.x ainda le (src/pyxel-core/src/old_resource_data.rs): imagem = 256 linhas de 256 digitos hexadecimais; tilemap = 256 linhas de 256 numeros hexadecimais de 3 digitos (indice do tile, linha*32+coluna) e a linha 257 com o banco de imagem. Bancos vazios (e sons e musicas, que o jogo nao tem) ficam de fora. Zip deterministico. """ import gzip import io import pickle import sys import zipfile class Dtype: def __init__(self, name, *args): self.name = name def __setstate__(self, state): self.byteorder = state[1] class NdArray: def __setstate__(self, state): _version, self.shape, self.dtype, _fortran, self.raw = state def reconstruct(subtype, shape, code): return NdArray.__new__(NdArray) class Inert: """Sound e Music do formato velho: so guardam atributos (o jogo nao usa nenhum).""" class RestrictedUnpickler(pickle.Unpickler): ALLOWED = { ("numpy.core._multiarray_umath", "_reconstruct"): reconstruct, ("numpy._core.multiarray", "_reconstruct"): reconstruct, ("numpy", "ndarray"): NdArray, ("numpy", "dtype"): Dtype, ("_codecs", "encode"): __import__("_codecs").encode, ("pyxel.sound", "Sound"): Inert, ("pyxel.music", "Music"): Inert, } def find_class(self, module, name): try: return self.ALLOWED[(module, name)] except KeyError: raise pickle.UnpicklingError(f"classe nao permitida: {module}.{name}") def load(data): return RestrictedUnpickler(io.BytesIO(data)).load() def values(array): if array.shape != (256, 256) or array.dtype.name not in ("u1", "u2"): raise SystemExit(f"array inesperado: {array.shape} {array.dtype.name}") step = 1 if array.dtype.name == "u1" else 2 raw = array.raw if len(raw) != 256 * 256 * step: raise SystemExit("tamanho de array inesperado") order = "little" if array.dtype.byteorder in ("<", "|") else "big" return [int.from_bytes(raw[i:i + step], order) for i in range(0, len(raw), step)] def main(src, dest): data = load(gzip.open(src).read()) if data["version"] != "0.9.10": raise SystemExit(f"versao inesperada: {data['version']}") entries = {"pyxel_resource/version": data["version"]} for index, blob in enumerate(data["image"]): pixels = values(load(blob)) if any(pixels): rows = ["".join("%x" % pixels[y * 256 + x] for x in range(256)) for y in range(256)] entries[f"pyxel_resource/image{index}"] = "\n".join(rows) + "\n" for index, (blob, image_bank) in enumerate(data["tilemap"]): tiles = values(load(blob)) if any(tiles): if max(tiles) > 0xFFF: raise SystemExit("tile fora do limite de 3 digitos") rows = ["".join("%03x" % tiles[y * 256 + x] for x in range(256)) for y in range(256)] entries[f"pyxel_resource/tilemap{index}"] = "\n".join(rows) + f"\n{image_bank}\n" with zipfile.ZipFile(dest, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as z: for name in sorted(entries): info = zipfile.ZipInfo(name, (1980, 1, 1, 0, 0, 0)) info.compress_type = zipfile.ZIP_DEFLATED info.external_attr = 0o644 << 16 info.create_system = 3 z.writestr(info, entries[name], compresslevel=9) if __name__ == "__main__": main(*sys.argv[1:])