ultiomos

convtool.py

Jan 15th, 2021
901
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. import argparse, sys
  2. import re
  3. import struct
  4. from PIL import Image
  5.  
  6. EXAMPLE_FILENAMES = (
  7. 'KANSA_00.CGX',
  8. 'KAN00_CE.CEL',
  9. 'WAKABA.MAP'
  10. )
  11. EXAMPLE_MAP_WIDTH = 10
  12. EXAMPLE_COMPRESSION = '-c lz'
  13.  
  14. EXAMPLE_GUIDE = '''
  15. mkdir out
  16.  
  17. python3 conv_tool.py dat {0} out/{0}.bin
  18. python3 conv_tool.py pic {4} -w 16 out/{0}.bin out/{0}.png
  19.  
  20. python3 conv_tool.py dat {1} out/{1}.bin
  21. python3 conv_tool.py cel -w 4 out/{1}.bin out/{0}.png out/{1}.png
  22.  
  23. python3 conv_tool.py dat {2} out/{2}.bin
  24. python3 conv_tool.py map out/{2}.bin out/{1}.png {3} out/{2}.png
  25. '''[1:-1].format(*EXAMPLE_FILENAMES, EXAMPLE_MAP_WIDTH, EXAMPLE_COMPRESSION)
  26.  
  27. DEFAULT_HELP_STRING = '''
  28. usage: {0} TOOL ...
  29.  
  30. Tool for converting Gen 1/2 Pokemon source files
  31.  
  32. positional arguments:
  33.   TOOL      tool to use
  34.  
  35. optional arguments:
  36.   -h, --help    show this help message and exit
  37.  
  38. valid tools are:
  39.   dat       convert text file into binary data
  40.   pic       convert file into png image (typically used for .CGX and .DAT)
  41.   cel       convert file into cel image (typically used for .CEL)
  42.   map       convert file into map image (typically used for .MAP and .FLD)
  43.   clz       add color to a png image (NOT YET IMPLEMENTED)
  44.  
  45. for tool-specific help: {0} TOOL -h
  46. '''[1:-1].format(sys.argv[0])
  47.  
  48. class Decompressor:
  49.     LZ_END        = 0b11111111
  50.     LZ_CMD        = 0b11100000
  51.     LZ_LEN        = 0b00011111
  52.     LZ_LITERAL    = 0b00000000
  53.     LZ_ITERATE    = 0b00100000
  54.     LZ_ALTERNATE  = 0b01000000
  55.     LZ_ZERO       = 0b01100000
  56.     LZ19_INCREASE = 0b01100000
  57.     LZ_RW         = 0b10000000
  58.     LZ_REPEAT     = 0b10000000
  59.     LZ_FLIP       = 0b10100000
  60.     LZ_REVERSE    = 0b11000000
  61.     LZ_LONG       = 0b11100000
  62.     LZ_LONG_C_HI  = 0b00011100
  63.     LZ_LONG_L_HI  = 0b00000011
  64.     def __init__(self, raw, mode='lz'):
  65.         self.buff = raw
  66.         self.ind = 0
  67.         assert mode in ('lz', 'lz19')
  68.         self.mode = mode
  69.  
  70.     def decompress(self):
  71.         out = b''
  72.         while True:
  73.             cmd = self.read_byte()
  74.             if cmd == self.LZ_END:
  75.                 break
  76.             if cmd & self.LZ_CMD == self.LZ_LONG:
  77.                 # long
  78.                 ext = self.read_byte()
  79.                 cmd_id = (cmd & self.LZ_LONG_C_HI) << 3
  80.                 cmd_len = ext + 1 + ((cmd & self.LZ_LONG_L_HI) << 8)
  81.             else:
  82.                 # short
  83.                 cmd_id = cmd & self.LZ_CMD
  84.                 cmd_len = (cmd & self.LZ_LEN) + 1
  85.             if cmd_id & self.LZ_RW:
  86.                 byt = self.read_byte()
  87.                 if byt >= 0x80:
  88.                     # negative
  89.                     rel = len(out) + 0x7f - byt
  90.                 else:
  91.                     # positive
  92.                     rel = byt << 8
  93.                     rel += self.read_byte()
  94.             else:
  95.                 rel = None
  96.  
  97.             if cmd_id == self.LZ_LITERAL:
  98.                 for _ in range(cmd_len):
  99.                     out += self.read_byte(1)
  100.             elif cmd_id == self.LZ_ITERATE:
  101.                 byt = self.read_byte(1)
  102.                 out += byt * cmd_len
  103.             elif cmd_id == self.LZ_ALTERNATE:
  104.                 byt1 = self.read_byte(1)
  105.                 byt2 = self.read_byte(1)
  106.                 if self.mode == 'lz19':
  107.                     out += (byt1 + byt2) * cmd_len
  108.                 else:
  109.                     for i in range(cmd_len):
  110.                         if i % 2 == 0:
  111.                             out += byt1
  112.                         else:
  113.                             out += byt2
  114.             elif cmd_id == self.LZ_ZERO:
  115.                 if self.mode == 'lz19':
  116.                     byt = self.read_byte()
  117.                     for _ in range(cmd_len):
  118.                         out += struct.pack('B', byt)
  119.                         byt += 1
  120.                         byt & 0xff
  121.                 else:
  122.                     out += b'\x00' * cmd_len
  123.             elif cmd_id == self.LZ_REPEAT:
  124.                 for _ in range(cmd_len):
  125.                     out += out[rel:rel+1]
  126.                     rel += 1
  127.             elif cmd_id == self.LZ_FLIP:
  128.                 for _ in range(cmd_len):
  129.                     byt = out[rel]
  130.                     flp = int(bin(byt)[2:].zfill(8)[::-1], 2)
  131.                     out += struct.pack('B', flp)
  132.                     rel += 1
  133.             elif cmd_id == self.LZ_REVERSE:
  134.                 for _ in range(cmd_len):
  135.                     out += out[rel:rel+1]
  136.                     rel -= 1
  137.             else:
  138.                 raise Exception
  139.  
  140.         return out, self.ind # data, compressed size
  141.  
  142.     def read_byte(self, asraw=0):
  143.         if asraw:
  144.             cur = self.buff[self.ind:self.ind+1]
  145.         else:
  146.             cur = self.buff[self.ind]
  147.         self.ind += 1
  148.         return cur
  149.  
  150. class main_dat:
  151.     REGEX_DEC = re.compile(r'^  d[bw]   ((?:[0-9]{3},? ?)+)\s*(?:;.*)?$')
  152.     REGEX_HEX = re.compile(r'^  d[bw]   ((?:0(?:[A-Fa-f0-9]{2})+h,? ?)+)\s*(?:;.*)?$')
  153.     def __init__(self, args):
  154.         txt = open(args.filein).read()
  155.         v = not args.quiet
  156.         lins = txt.split('\n')
  157.         if args.decimal:
  158.             reg = self.REGEX_DEC
  159.         else:
  160.             reg = self.REGEX_HEX
  161.         datout = b''
  162.         uuct = 0
  163.         for i,l in enumerate(lins):
  164.             mm = reg.match(l)
  165.             if mm:
  166.                 cur_dats = mm.groups()[0].split(',')
  167.                 if args.decimal:
  168.                     cur_dats_2 = [hex(int(x.strip()[1:-1]))[2:].zfill(2) for x in cur_dats]
  169.                 else:
  170.                     cur_dats_2 = [x.strip()[1:-1] for x in cur_dats]
  171.                 datout += self.divhxl(cur_dats_2)
  172.             else:
  173.                 if v:
  174.                     print('Line does not match pattern:')
  175.                     print(i+1, l)
  176.                     uuct += 1
  177.                     if uuct > 10:
  178.                         input()
  179.                         uuct = 0
  180.         open(args.fileout, 'wb').write(datout)
  181.  
  182.     def divhx(self, x):
  183.         pcs = []
  184.         for i in range(len(x) // 2):
  185.             pcs.append(x[i*2:i*2+2])
  186.         return [int(x,16) for x in pcs[::-1]]
  187.  
  188.     def divhxl(self, x):
  189.         out = b''
  190.         for k in x:
  191.             cur = self.divhx(k)
  192.             out += struct.pack('B'*len(cur), *cur)
  193.         return out
  194.  
  195. def Main_dat():
  196.     parser = argparse.ArgumentParser(description='Tool for converting Gen 1/2 pokemon source files')
  197.     parser.add_argument('mode', type=str, choices={'dat'})
  198.     parser.add_argument('filein', type=str, help='file to convert')
  199.     parser.add_argument('fileout', type=str, help='output')
  200.     parser.add_argument('-q', dest='quiet', action='store_true', help='suppress output')
  201.     parser.add_argument('-d', dest='decimal', action='store_true', help='decimal data rather than hex')
  202.     args = parser.parse_args()
  203.     if args.fileout == '-':
  204.         args.fileout = args.filein + '.bin'
  205.     main_dat(args)
  206.  
  207. class main_pic:
  208.     gbgray = ((255,255,255),(176,176,176),(104,104,104),(0,0,0))
  209.     def __init__(self, args):
  210.         raw = open(args.filein, 'rb').read()
  211.         if args.c:
  212.             decomp = Decompressor(raw, args.c)
  213.             raw, _ = decomp.decompress()
  214.         if args.width:
  215.             width = args.width
  216.         else:
  217.             width = 1
  218.         numtiles = len(raw) // 0x10
  219.         height = numtiles // width + (numtiles % width != 0)
  220.         imgout = self.make_pic(raw, width, height)
  221.         imgout.save(args.fileout)
  222.  
  223.     def make_pic(self, raw, width, height):
  224.         numtiles = len(raw) // 0x10
  225.         imgout = Image.new('RGBA', (width*8, height*8))
  226.         pxl = imgout.load()
  227.  
  228.         i = 0
  229.         for y in range(height):
  230.             for x in range(width):
  231.                 for ty in range(8):
  232.                     byt1 = raw[i]
  233.                     byt2 = raw[i+1]
  234.                     ry = y * 8 + ty
  235.                     for bt in range(8):
  236.                         rx = x * 8 + (7-bt)
  237.                         c_0 = bool(byt1 & (1 << bt))
  238.                         c_1 = bool(byt2 & (1 << bt))
  239.                         c_i = c_1 * 2 + c_0
  240.                         pxl[rx,ry] = self.gbgray[c_i]
  241.  
  242.                     i += 2
  243.         assert i == len(raw)
  244.  
  245.         return imgout
  246.  
  247. def Main_pic():
  248.     parser = argparse.ArgumentParser(description='Tool for converting Gen 1/2 pokemon source files')
  249.     parser.add_argument('mode', type=str, choices={'pic'})
  250.     parser.add_argument('filein', type=str, help='file to convert')
  251.     parser.add_argument('fileout', type=str, help='output')
  252.     parser.add_argument('-w', '--width', type=int, help='width of image, in 8px x 8px tiles')
  253.     parser.add_argument('-c', type=str, choices={'lz', 'lz19'}, help='compression')
  254.     args = parser.parse_args()
  255.     if args.fileout == '-':
  256.         args.fileout = args.filein[:-4] + '.png'
  257.     main_pic(args)
  258.  
  259. class main_cel:
  260.     def __init__(self, args):
  261.         self.img = Image.open(args.tilin)
  262.         self.img_w_t = self.img.size[0] // 8
  263.         raw = open(args.celin, 'rb').read()
  264.         numcel_x = args.width
  265.         numcel = len(raw) // 0x10
  266.         numcel_y = numcel // numcel_x + (numcel % numcel_x != 0)
  267.         imgout = Image.new('RGBA', (numcel_x * 32, numcel_y * 32))
  268.  
  269.         for y in range(numcel_y):
  270.             for x in range(numcel_x):
  271.                 celi = y * numcel_x + x
  272.                 curcel = raw[celi*16:celi*16+16]
  273.                 for yy in range(4):
  274.                     for xx in range(4):
  275.                         imgy = y * 32 + yy * 8
  276.                         imgx = x * 32 + xx * 8
  277.                         imgout.paste(self.gettile(curcel[yy*4+xx]), (imgx, imgy))
  278.         imgout.save(args.fileout)
  279.  
  280.     def gettile(self, i):
  281.         imgx = (i % self.img_w_t) * 8
  282.         imgy = (i // self.img_w_t) * 8
  283.         return self.img.crop((imgx, imgy, imgx+8, imgy+8))
  284.  
  285. def Main_cel():
  286.     parser = argparse.ArgumentParser(description='Tool for converting Gen 1/2 pokemon source files')
  287.     parser.add_argument('mode', type=str, choices={'cel'})
  288.     parser.add_argument('celin', type=str, help='cel file to convert')
  289.     parser.add_argument('tilin', type=str, help='png-cgx/dat file to use')
  290.     parser.add_argument('fileout', type=str, help='output')
  291.     parser.add_argument('-w', '--width', type=int, default=1, help='width (in cells)')
  292.     args = parser.parse_args()
  293.     if args.fileout == '-':
  294.         args.fileout = args.celin[:-4] + '.png'
  295.     main_cel(args)
  296.  
  297. class main_map:
  298.     def __init__(self, args):
  299.         self.img = Image.open(args.celin)
  300.         self.img_w_c = self.img.size[0] // 32
  301.         raw = open(args.mapin, 'rb').read()
  302.         w = args.width
  303.         assert len(raw) % w == 0
  304.         h = len(raw) // w
  305.  
  306.         imgout = Image.new('RGBA', (w * 32, h * 32))
  307.         for y in range(h):
  308.             for x in range(w):
  309.                 celi = raw[y*w+x]
  310.                 cel = self.getcel(celi)
  311.                 imgout.paste(cel, (x*32, y*32))
  312.         imgout.save(args.fileout)
  313.  
  314.     def getcel(self, i):
  315.         imgx = (i % self.img_w_c) * 32
  316.         imgy = (i // self.img_w_c) * 32
  317.         return self.img.crop((imgx, imgy, imgx+32, imgy+32))
  318.  
  319. def Main_map():
  320.     parser = argparse.ArgumentParser(description='Tool for converting Gen 1/2 pokemon source files')
  321.     parser.add_argument('mode', type=str, choices={'map'})
  322.     parser.add_argument('mapin', type=str, help='map file to convert')
  323.     parser.add_argument('celin', type=str, help='png-cel file to use')
  324.     parser.add_argument('width', type=int, help='width of map (in cells)')
  325.     parser.add_argument('fileout', type=str, help='output')
  326.     args = parser.parse_args()
  327.     if args.fileout == '-':
  328.         args.fileout = args.mapin[:-4] + '.png'
  329.     main_map(args)
  330.  
  331. class main_clz:
  332.     def __init__(self, args):
  333.         pass
  334.  
  335. def Main_clz():
  336.     print('NOT YET IMPLEMENTED')
  337.  
  338. if __name__ == '__main__':
  339.     if len(sys.argv) == 2 and sys.argv[1] in ('-h', '--help'):
  340.         print(DEFAULT_HELP_STRING)
  341.         exit()
  342.     elif len(sys.argv) == 2 and sys.argv[1] == 'example':
  343.         print(EXAMPLE_GUIDE)
  344.         exit()
  345.     parser = argparse.ArgumentParser(description='Tool for converting Gen 1/2 pokemon source files', add_help=False)
  346.     parser.add_argument('mode', type=str, choices={'dat','pic','cel','map','clz'})
  347.     args, _ = parser.parse_known_args()
  348.  
  349.     if args.mode == 'dat':
  350.         Main_dat()
  351.     elif args.mode == 'pic':
  352.         Main_pic()
  353.     elif args.mode == 'cel':
  354.         Main_cel()
  355.     elif args.mode == 'map':
  356.         Main_map()
  357.     elif args.mode == 'clz':
  358.         Main_clz()
  359.  
RAW Paste Data