├── system ├── lzham.exe ├── strings.py └── Lib.py ├── README.md └── Main.py /system/lzham.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MasterDevX/XCoder/HEAD/system/lzham.exe -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XCoder 2 | Compress / Decompress Brawl Stars SC files on Windows / Linux / Android! 3 | 4 | ### Credits 5 | This tool is based on: 6 | - png2sc, Developer: Xset-s 7 | - scPacker, Developer: Galaxy1036
8 | - sc_decode, Developer: Galaxy1036
9 | 10 | I wanna say "Thank You!" to these developers, because without their work XCoder wouldn't have been released!
11 | Special thanks to spiky_Spike for help with XCoder updates :) 12 | 13 | ### Features: 14 | - Easy to use. 15 | - Brawl Stars, Clash of Clans & Clash Royale support. 16 | - Multiplatform support (working on Windows, Linux and Android). 17 | - SC compilation / decompilation. 18 | 19 | ### How to install and use 20 | On Windows: 21 | - Download Python 3.7 or newer version from official page. 22 | - Install Python. While Installing, enable such parameters as "Add Python to PATH", "Install pip", "Install py launcher", "Associate files with Python" and "Add Python to environment variables". 23 | - Download XCoder from releases page and extract it. 24 | - Execute "Main.py" file
25 | 26 | On Linux: 27 | - Open Terminal and install Python by executing following command:
28 | ```sudo apt-get update && sudo apt-get install python3 python3-pip``` 29 | - Download XCoder from releases page and extract it. 30 | - Execute "Main.py" file 31 | 32 | On Android: 33 | - Download and install PyDroid app from Google Play. 34 | - Open PyDroid and wait until Python installs. 35 | - Download XCoder from releases page and extract it. 36 | - In PyDroid open and execute "Main.py" file
37 | 38 | ### TODO: 39 | - CSV compilation / decompilation. 40 | -------------------------------------------------------------------------------- /system/strings.py: -------------------------------------------------------------------------------- 1 | class en: 2 | xcoder = 'XCoder | Version: %s | by MasterDevX & spiky_Spike' 3 | detected_os = 'Detected %s OS.' 4 | installing = 'Installing required modules...' 5 | crt_workspace = 'Creating workspace directories...' 6 | verifying = 'Verifying installation...' 7 | installed = '%s was sucessfully installed!' 8 | not_installed = '%s wasn\'t installed!' 9 | clear_qu = 'Are you sure you want to clear the dirs?' 10 | done = 'Done!' 11 | choice = 'Your choice: ' 12 | to_continue = 'Press Enter to continue...' 13 | expiremental = 'Experimental feature' 14 | sc = 'SC' 15 | dsc = 'Decode SC' 16 | dsc_desc = 'Convert SC to PNG' 17 | esc = 'Encode SC' 18 | esc_desc = 'Convert PNG to SC' 19 | d1sc = 'Decode SC to chunks' 20 | e1sc = 'Encode SC from chunks' 21 | oth = 'OTHER' 22 | chkupd = 'Check for updates' 23 | version = 'Current version: %s' 24 | reinit = 'Repeat init' 25 | reinit_desc = 'If something went wrong' 26 | relang = 'Select another language' 27 | relang_desc = 'Current lang: %s' 28 | clean_dirs = 'Clear workspace dirs' 29 | clean_dirs_desc = 'Clear In and Out dirs' 30 | exit = 'Exit' 31 | done_err = 'DONE WITH %s ERRORS!\nFor more information check log.txt' 32 | got_error = 'Got error: \n%s\n\n' 33 | collecting_inf = 'Collecting information...' 34 | about_sc = 'About: FileName: %s, FileType: %s, FileSize: %s, SubType: %s, Width: %s, Height: %s' 35 | try_error = 'Error while decompessing! Trying to decode as is...' 36 | not_installed2 = '%s isn\'t installed! Return' 37 | decomp_err = 'Decompression failed' 38 | detected_comp = 'Detected %s compression!' 39 | unk_type = 'Unknown pixel type: %s' 40 | crt_pic = 'Creating picture...' 41 | join_pic = 'Joining picture...' 42 | png_save = 'Saving to png...' 43 | saved = 'Saving completed!' 44 | not_xcod = '.xcod file doesn\'t exist!' 45 | standart_types = 'We will use standart fileType and subType (1, 0).\n And also LZMA compression (as in Brawl Stars sc)' 46 | illegal_size = 'Illegal image size! Expected %sx%s but we got %sx%s' 47 | resize_qu = 'Would you like to resize an image?' 48 | resizing = 'Resizing...' 49 | split_pic = 'Splitting picture...' 50 | writing_pic = 'Writing pixels...' 51 | header_done = 'Header wrote!' 52 | comp_with = 'Compressing texture with %s...' 53 | comp_err = 'Compression failed' 54 | comp_done = 'Compression done!' 55 | dir_empty = 'Dir \'%s\' is empty!' 56 | not_found = '\'%s\' file isn\'t found!' 57 | cut_sprites = 'Cutting sprites... (%s/%s)' 58 | place_sprites = 'Placing sprites... (%s/%s)' 59 | not_implemented = 'This feature will be added in future updates.\nYou can follow XCoder updates here: github.com/MasterDevX/XCoder' 60 | want_exit = 'Want to exit?' 61 | dec_sc = 'Decoding .sc file...' 62 | dec_sctex = 'Decoding _tex.sc file...' 63 | err = 'ERROR! (%s.%s: %s)' 64 | 65 | 66 | 67 | class ru: 68 | xcoder = 'XCoder | Версия: %s | От: MasterDevX & spiky_Spike' 69 | detected_os = 'Обнаружена система %s.' 70 | installing = 'Установка необходимых модулей...' 71 | crt_workspace = 'Создаются рабочие папки...' 72 | verifying = 'Проверка установки...' 73 | installed = '%s успешно установлен!' 74 | not_installed = '%s не установлен!' 75 | clear_qu = 'Вы действительно хотите очистить папки?' 76 | done = 'Выполнено!' 77 | choice = 'ВАШЕ ДЕЙСТВИЕ: ' 78 | to_continue = 'Нажмите Enter для продолжения...' 79 | expiremental = 'Экспер. фича' 80 | sc = 'SC' 81 | dsc = 'Декодировать SC' 82 | dsc_desc = 'Конвертирует SC в PNG' 83 | esc = 'Закодировать SC' 84 | esc_desc = 'Конвертирует PNG в SC' 85 | d1sc = 'Декодировать SC по частям' 86 | e1sc = 'Закодировать SC по частям' 87 | oth = 'ПРОЧЕЕ' 88 | chkupd = 'Проверить обновления' 89 | version = 'Версия: %s' 90 | reinit = 'Повторная инициализация' 91 | reinit_desc = 'Если что-то пошло не так' 92 | relang = 'Выбрать другой язык' 93 | relang_desc = 'Текущий язык: %s' 94 | clean_dirs = 'Очистить рабочие папки' 95 | clean_dirs_desc = 'Очистить In и Out папки' 96 | exit = 'Выход' 97 | done_err = 'ГОТОВО С %s ОШИБКАМИ!\nДля более подробной информации проверьте log.txt' 98 | got_error = '[%s] Получена ошибка: \n%s\n\n' 99 | collecting_inf = 'Сбор информации...' 100 | about_sc = 'О текстуре: имя: %s, тип: %s, размер в байтах: %s, подтип: %s, ширина: %s, высота: %s' 101 | try_error = 'Ошибка распаковки! Пробуем как есть...' 102 | not_installed2 = '%s не установлен! Пропускаем...' 103 | decomp_err = 'Распаковка не удалась' 104 | detected_comp = 'Обнаружено %s сжатие!' 105 | unk_type = 'Неизвестный подтип: %s' 106 | crt_pic = 'Создаём картинку...' 107 | join_pic = 'Соединяем картинку...' 108 | png_save = 'Сохраняем в png...' 109 | saved = 'Сохранение прошло успешно!' 110 | not_xcod = '.xcod файл не обнаружен!' 111 | standart_types = 'Мы используем стандартные типы текстур (1, 0).\n И ещё LZMA сжатие (как в SC из Brawl Stars)' 112 | illegal_size = 'Размер картинки не совпадает с оригиналом! Ожидалось %sx%s, но мы получили %sx%s' 113 | resize_qu = 'Хотите изменить размер?' 114 | resizing = 'Изменяем размер...' 115 | split_pic = 'Разделяем картинку...' 116 | writing_pic = 'Конвертируем пиксели...' 117 | header_done = 'Заголовок записан!' 118 | comp_with = 'Сохраняем с применением %s сжатия...' 119 | comp_err = 'Сжатие не удалось' 120 | comp_done = 'Сжатие прошло успешно!' 121 | dir_empty = 'Папка \'%s\' пуста!' 122 | not_found = 'Не был найден \'%s\' файл!' 123 | cut_sprites = 'Вырезаем спрайты... (%s/%s)' 124 | place_sprites = 'Ставим спрайты на место... (%s/%s)' 125 | not_implemented = 'Данная возможность будет добавлена в будущих обновлениях.\nЗа обновлениями XCoder вы можете следить здесь: github.com/MasterDevX/XCoder' 126 | want_exit = 'Хотите выйти?' 127 | dec_sc = 'Декодируем SC...' 128 | dec_sctex = 'Декодируем _tex.sc...' 129 | err = 'ОШИБКА! (%s.%s: %s)' 130 | 131 | 132 | 133 | string = {'en': en, 'ru': ru} 134 | 135 | def center_2strings(conwidth, str1, str2): 136 | return print(str1 + (' ' * (conwidth // 2 - len(str1)) + ': ' + str2)) 137 | 138 | def colored_str(text, color = None): 139 | if not color: 140 | color = colorama.Back.GREEN 141 | return print(color + colorama.Fore.BLACK + text + ' ' * (10 - len(text)) + colorama.Style.RESET_ALL) 142 | 143 | def console(config): 144 | global colorama 145 | s = string[config['lang']] 146 | from system.Lib import colorama, shutil 147 | conwidth = shutil.get_terminal_size().columns 148 | print((colorama.Back.BLACK + colorama.Fore.GREEN + s.xcoder % config['version'] + colorama.Style.RESET_ALL).center(conwidth + 14)) 149 | print('github.com/MasterDevX/XCoder'.center(conwidth)) 150 | print(conwidth * '-') 151 | colored_str(s.sc) 152 | center_2strings(conwidth, ' 1 ' + s.dsc, s.dsc_desc) 153 | center_2strings(conwidth, ' 2 ' + s.esc, s.esc_desc) 154 | center_2strings(conwidth, ' 3 ' + s.d1sc, s.expiremental) 155 | center_2strings(conwidth, ' 4 ' + s.e1sc, s.expiremental) 156 | print(conwidth * '-') 157 | colored_str(s.oth) 158 | center_2strings(conwidth, ' 101 ' + s.chkupd, s.version % config['version']) 159 | center_2strings(conwidth, ' 102 ' + s.reinit, s.reinit_desc) 160 | center_2strings(conwidth, ' 103 ' + s.relang, s.relang_desc % config['lang']) 161 | center_2strings(conwidth, ' 104 ' + s.clean_dirs, s.clean_dirs_desc) 162 | print(' 105 ' + s.exit) 163 | print(conwidth * '-') 164 | choice = input(s.choice) 165 | print(conwidth * '-') 166 | return choice 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /Main.py: -------------------------------------------------------------------------------- 1 | from system.Lib import * 2 | 3 | Clear() 4 | 5 | cfg_path = './system/config.json' 6 | 7 | def select_lang(): 8 | global string 9 | lang = input( 10 | 'Select Language\n' 11 | 'Выберите язык\n\n' 12 | '1 - English\n' 13 | '2 - Русский\n\n>>> ') 14 | if lang == '1': 15 | lang = 'en' 16 | elif lang == '2': 17 | lang = 'ru' 18 | else: 19 | Clear() 20 | select_lang() 21 | 22 | config.update({'lang': lang}) 23 | json.dump(config, open(cfg_path, 'w')) 24 | from system.strings import string 25 | string = string[config['lang']] 26 | 27 | 28 | def init(ret=True): 29 | if ret: 30 | Clear() 31 | info(string.detected_os % platform.system()) 32 | info(string.installing) 33 | [os.system(f'pip3 install {i}{nul}') for i in ['colorama', 'pillow', 'lzma', 'pylzham']] 34 | info(string.crt_workspace) 35 | [[os.system(f'mkdir {i}-{k}-SC{nul}') for k in ['Compressed', 'Decompressed', 'Sprites']] for i in ['In', 'Out']] 36 | info(string.verifying) 37 | for i in ['colorama', 'PIL', 'lzma', 'lzham']: 38 | try: 39 | [exec(f"{k} {i}") for k in ['import', 'del']] 40 | info(string.installed % i) 41 | except: 42 | info(string.not_installed % i) 43 | 44 | config.update({'inited': True}) 45 | json.dump(config, open(cfg_path, 'w')) 46 | if ret: 47 | input(string.to_continue) 48 | 49 | def clear_dirs(): 50 | files = os.listdir('./') 51 | for i in ['In', 'Out']: 52 | for k in ['Compressed', 'Decompressed', 'Sprites']: 53 | folder = f'{i}-{k}-SC' 54 | if folder in files: 55 | shutil.rmtree(folder) 56 | os.system(f'mkdir {folder}{nul}') 57 | 58 | 59 | 60 | def sc_decode(): 61 | global errors 62 | folder = "./In-Compressed-SC/" 63 | folder_export = "./Out-Decompressed-SC/" 64 | 65 | for file in os.listdir(folder): 66 | if file.endswith("_tex.sc"): 67 | 68 | CurrentSubPath = file[::-1].split('.', 1)[1][::-1] + '/' 69 | if os.path.isdir(f"{folder_export}{CurrentSubPath}"): 70 | shutil.rmtree(f"{folder_export}{CurrentSubPath}") 71 | os.mkdir(f"{folder_export}{CurrentSubPath}") 72 | try: 73 | decompileSC(f"{folder}{file}", CurrentSubPath, folder = folder, folder_export = folder_export) 74 | except Exception as e: 75 | errors += 1 76 | err_text(string.err % (e.__class__.__module__, e.__class__.__name__, e)) 77 | write_log(traceback.format_exc()) 78 | 79 | print() 80 | 81 | def sc_encode(): 82 | global errors 83 | folder = './In-Decompressed-SC/' 84 | folder_export = './Out-Compressed-SC/' 85 | 86 | for i in os.listdir(folder): 87 | try: 88 | compileSC(f"{folder}{i}/", folder_export=folder_export) 89 | except Exception as e: 90 | errors += 1 91 | err_text(string.err % (e.__class__.__module__, e.__class__.__name__, e)) 92 | write_log(traceback.format_exc()) 93 | 94 | print() 95 | 96 | 97 | def sc1_decode(): 98 | global errors 99 | folder = "./In-Compressed-SC/" 100 | folder_export = "./Out-Sprites-SC/" 101 | files = os.listdir(folder) 102 | 103 | for file in files: 104 | if file.endswith("_tex.sc"): 105 | 106 | scfile = file[:-7] + '.sc' 107 | if scfile not in files: 108 | err_text(string.not_found % scfile) 109 | else: 110 | CurrentSubPath = file[::-1].split('.', 1)[1][::-1] + '/' 111 | if os.path.isdir(f"{folder_export}{CurrentSubPath}"): 112 | shutil.rmtree(f"{folder_export}{CurrentSubPath}") 113 | os.mkdir(f"{folder_export}{CurrentSubPath}") 114 | try: 115 | info(string.dec_sctex) 116 | sheetimage, xcod = decompileSC(f"{folder}{file}", CurrentSubPath, to_memory=True, folder_export=folder_export) 117 | info(string.dec_sc) 118 | spriteglobals, spritedata, sheetdata = decodeSC(f"{folder}{scfile}", sheetimage) 119 | xc = open(f"{folder_export}{CurrentSubPath}" + file[:-3] + '.xcod', 'wb') 120 | xc.write(xcod) 121 | cut_sprites(spriteglobals, spritedata, sheetdata, sheetimage, xc, f"{folder_export}{CurrentSubPath}") 122 | except Exception as e: 123 | errors += 1 124 | err_text(string.err % (e.__class__.__module__, e.__class__.__name__, e)) 125 | write_log(traceback.format_exc()) 126 | 127 | print() 128 | 129 | def sc1_encode(): 130 | global errors 131 | folder = "./In-Sprites-SC/" 132 | folder_export = "./Out-Compressed-SC/" 133 | files = os.listdir(folder) 134 | 135 | for file in files: 136 | print(file) 137 | 138 | xcod = file + '.xcod' 139 | if xcod not in os.listdir(f'{folder}{file}/'): 140 | err_text(string.not_found % xcod) 141 | else: 142 | 143 | try: 144 | info(string.dec_sctex) 145 | sheetimage, sheetimage_data = place_sprites(f"{folder}{file}/{xcod}", f"{folder}{file}") 146 | info(string.dec_sc) 147 | compileSC(f'{folder}{file}/', sheetimage, sheetimage_data, folder_export) 148 | except Exception as e: 149 | errors += 1 150 | err_text(f"Error while decoding! ({e.__class__.__module__}.{e.__class__.__name__}: {e})") 151 | write_log(traceback.format_exc()) 152 | print() 153 | 154 | 155 | v = Version 156 | 157 | if __name__ == '__main__': 158 | if os.path.isfile(cfg_path): 159 | try: 160 | config = json.load(open(cfg_path)) 161 | except: 162 | config = {'inited': False, 'version': v} 163 | else: 164 | config = {'inited': False, 'version': v} 165 | 166 | if not config.get('lang'): 167 | select_lang() 168 | 169 | if not config['inited']: 170 | init() 171 | try: os.system('python%s "%s"' % ('' if isWin else '3', __file__)) 172 | except: pass 173 | exit() 174 | 175 | from system.strings import string, console 176 | Title(string['en'].xcoder % config['version']) 177 | 178 | while 1: 179 | try: 180 | string = string[config['lang']] 181 | break 182 | except: 183 | select_lang() 184 | 185 | locale(config['lang']) 186 | 187 | 188 | while 1: 189 | try: 190 | errors = 0 191 | [os.remove(i) if os.path.isfile( 192 | i) else None for i in ('temp.sc', '_temp.sc')] 193 | 194 | Clear() 195 | answer = console(config) 196 | print() 197 | if answer == '1': 198 | sc_decode() 199 | elif answer == '2': 200 | sc_encode() 201 | elif answer == '3': 202 | sc1_decode() 203 | elif answer == '4': 204 | sc1_encode() 205 | 206 | elif answer == '101': 207 | print(string.not_implemented) 208 | elif answer == '102': 209 | init(ret=False) 210 | elif answer == '103': 211 | select_lang() 212 | locale(config['lang']) 213 | elif answer == '104': 214 | if not question(string.clear_qu): 215 | continue 216 | clear_dirs() 217 | 218 | elif answer == '105': 219 | Clear() 220 | break 221 | 222 | else: 223 | continue 224 | 225 | if errors > 0: 226 | err_text(string.done_err % errors) 227 | else: 228 | done_text(string.done) 229 | 230 | input(string.to_continue) 231 | 232 | except KeyboardInterrupt: 233 | if question(string.want_exit): 234 | Clear() 235 | break 236 | -------------------------------------------------------------------------------- /system/Lib.py: -------------------------------------------------------------------------------- 1 | try: 2 | import io 3 | import os 4 | import sys 5 | import time 6 | import json 7 | import struct 8 | import shutil 9 | import hashlib 10 | import platform 11 | import traceback 12 | import subprocess 13 | import colorama 14 | from PIL import Image, ImageDraw 15 | except: pass 16 | 17 | Version = '2.0.1-prerelease' 18 | 19 | lzham_path = 'system\\lzham' 20 | 21 | isWin = platform.system() == "Windows" 22 | 23 | nul = f" > {'nul' if isWin else '/dev/null'} 2>&1" 24 | 25 | if isWin: 26 | 27 | try: colorama.init() 28 | except: pass 29 | 30 | import ctypes 31 | Title = ctypes.windll.kernel32.SetConsoleTitleW 32 | del ctypes 33 | 34 | def Clear(): 35 | os.system('cls') 36 | 37 | else: 38 | 39 | def Title(message): 40 | sys.stdout.write(f"\x1b]2;{message}\x07") 41 | 42 | def Clear(): 43 | os.system('clear') 44 | 45 | def locale(lang): 46 | global string 47 | from system.strings import string 48 | string = string[lang] 49 | 50 | 51 | def info(message): 52 | print(f"[INFO] {message}") 53 | 54 | def progressbar(message, current, total, start = 0, end = 100): 55 | sys.stdout.write(f"\r[{((current + 1) * end + start) // total + start}%] {message}") 56 | 57 | def percent(current, total): 58 | return (current + 1) * 100 // total 59 | 60 | def int_qu(a): 61 | try: 62 | return int(input(f"[????] {a} ")) 63 | except: 64 | return int_qu(a) 65 | 66 | def question(message): 67 | x = input(f"[????] {message} [y/n] ").lower() 68 | return "ny".index(x) if x in ("y", "n") else question(message) 69 | 70 | def err_text(message): 71 | print(colorama.Fore.RED + message + colorama.Style.RESET_ALL) 72 | 73 | def done_text(message): 74 | print(colorama.Fore.GREEN + message + colorama.Style.RESET_ALL) 75 | 76 | def write_log(err): 77 | if not os.path.isfile('log.txt'): 78 | mode = 'w' 79 | else: 80 | mode = 'a' 81 | log = open('log.txt', mode, encoding='utf-8') 82 | log.write(string.got_error % (time.strftime('%d.%m.%Y %H:%M:%S'), err)) 83 | log.close() 84 | 85 | def pixelsize(type): 86 | if type in (0, 1): 87 | return 4 88 | if type in (2, 3, 4, 6): 89 | return 2 90 | if type in (10,): 91 | return 1 92 | raise Exception(string.unk_type % type) 93 | 94 | def format(type): 95 | if type in range(4): 96 | return 'RGBA' 97 | if type in (4,): 98 | return 'RGB' 99 | if type in (6,): 100 | return 'LA' 101 | if type in (10,): 102 | return 'L' 103 | raise Exception(string.unk_type % type) 104 | 105 | def bytes2rgba(data, type, img, pix): 106 | 107 | if type in (0, 1): 108 | def readpixel(): 109 | return struct.unpack('4B', data.read(4)) 110 | elif type == 2: 111 | def readpixel(): 112 | p, = struct.unpack('> 12 & 15) << 4, (p >> 8 & 15) << 4, (p >> 4 & 15) << 4, (p >> 0 & 15) << 4) 114 | elif type == 3: 115 | def readpixel(): 116 | p, = struct.unpack('> 11 & 31) << 3, (p >> 6 & 31) << 3, (p >> 1 & 31) << 3, (p & 255) << 7) 118 | elif type == 4: 119 | def readpixel(): 120 | p, = struct.unpack("> 11 & 31) << 3, (p >> 5 & 63) << 2, (p & 31) << 3) 122 | elif type == 6: 123 | def readpixel(): 124 | return struct.unpack("2B", data.read(2))[::-1] 125 | elif type == 10: 126 | def readpixel(): 127 | return struct.unpack("B", data.read(1)) 128 | 129 | width, height = img.size 130 | point = -1 131 | for y in range(height): 132 | for x in range(width): 133 | pix.append(readpixel()) 134 | 135 | curr = percent(y, height) 136 | if curr > point: 137 | progressbar(string.crt_pic, y, height) 138 | point = curr 139 | 140 | img.putdata(pix) 141 | 142 | 143 | def rgba2bytes(sc, img, type): 144 | 145 | 146 | if type in (0, 1): 147 | def writepixel(pixel): 148 | return struct.pack('4B', *pixel) 149 | if type == 2: 150 | def writepixel(pixel): 151 | r, g, b, a = pixel 152 | return struct.pack('> 4 | b >> 4 << 4 | g >> 4 << 8 | r >> 4 << 12) 153 | if type == 3: 154 | def writepixel(pixel): 155 | r, g, b, a = pixel 156 | return struct.pack('> 7 | b >> 3 << 1 | g >> 3 << 6 | r >> 3 << 11) 157 | if type == 4: 158 | def writepixel(pixel): 159 | r, g, b = pixel 160 | return struct.pack('> 3 | g >> 2 << 5 | r >> 3 << 11) 161 | if type == 6: 162 | def writepixel(pixel): 163 | return struct.pack('2B', *pixel[::-1]) 164 | if type == 10: 165 | def writepixel(pixel): 166 | return struct.pack('B', *pixel) 167 | 168 | width, height = img.size 169 | 170 | pix = img.load() 171 | point = -1 172 | for y in range(height): 173 | for x in range(width): 174 | sc.write(writepixel(pix[x, y])) 175 | 176 | curr = percent(y, height) 177 | if curr > point: 178 | progressbar(string.writing_pic, y, height) 179 | point = curr 180 | 181 | 182 | def join_image(img, p): 183 | _w, _h = img.size 184 | imgl = img.load() 185 | x = 0 186 | a = 32 187 | 188 | _ha = _h // a 189 | _wa = _w // a 190 | ha = _h % a 191 | 192 | for l in range(_ha): 193 | for k in range(_w // a): 194 | for j in range(a): 195 | for h in range(a): 196 | imgl[h + k * a, j + l * a] = p[x] 197 | x += 1 198 | 199 | for j in range(a): 200 | for h in range(_w % a): 201 | imgl[h + (_w - _w % a), j + l * a] = p[x] 202 | x += 1 203 | progressbar(string.join_pic, l, _ha) 204 | 205 | for k in range(_wa): 206 | for j in range(_h % a): 207 | for h in range(a): 208 | imgl[h + k * a, j + (_h - _h % a)] = p[x] 209 | x += 1 210 | 211 | for j in range(ha): 212 | for h in range(_w % a): 213 | imgl[h + (_w - _w % a), j + (_h - _h % a)] = p[x] 214 | x += 1 215 | progressbar(string.join_pic, l, _ha) 216 | 217 | def split_image(img): 218 | p = [] 219 | _w, _h = img.size 220 | imgl = img.load() 221 | a = 32 222 | 223 | _ha = _h // a 224 | _wa = _w // a 225 | ha = _h % a 226 | 227 | for l in range(_ha): 228 | for k in range(_wa): 229 | for j in range(a): 230 | for h in range(a): 231 | p.append(imgl[h + (k * a), j + (l * a)]) 232 | 233 | for j in range(a): 234 | for h in range(_w % a): 235 | p.append(imgl[h + (_w - (_w % a)), j + (l * a)]) 236 | progressbar(string.split_pic, l, _ha) 237 | 238 | for k in range(_w // a): 239 | for j in range(int(_h % a)): 240 | for h in range(a): 241 | p.append(imgl[h + (k * a), j + (_h - (_h % a))]) 242 | 243 | for j in range(ha): 244 | for h in range(_w % a): 245 | p.append(imgl[h + (_w - (_w % a)), j + (_h - (_h % a))]) 246 | img.putdata(p) 247 | progressbar(string.split_pic, l, _ha) 248 | 249 | class Reader: 250 | def __init__(self, data): 251 | self.stream = io.BytesIO(data) 252 | 253 | @property 254 | def byte(self): 255 | return int.from_bytes(self.stream.read(1), 'little') 256 | 257 | @property 258 | def uint16(self): 259 | return int.from_bytes(self.stream.read(2), 'little') 260 | 261 | @property 262 | def int16(self): 263 | return int.from_bytes(self.stream.read(2), 'little', signed = True) 264 | 265 | @property 266 | def uint32(self): 267 | return int.from_bytes(self.stream.read(4), 'little') 268 | 269 | @property 270 | def int32(self): 271 | return int.from_bytes(self.stream.read(4), 'little', signed = True) 272 | 273 | def string(self, length=1): 274 | return self.stream.read(int.from_bytes(self.stream.read(length), 'little')).decode() 275 | 276 | def decompileSC(fileName, CurrentSubPath, to_memory=False, folder=None, folder_export=None): 277 | pngs = [] 278 | picCount = 0 279 | scdata = io.BytesIO() 280 | 281 | info(string.collecting_inf) 282 | with open(fileName, "rb") as fh: 283 | start = fh.read(6) 284 | if start == b'SC\x00\x00\x00\x01': 285 | fh.read(20) 286 | start = b'' 287 | data = start + fh.read() 288 | 289 | try: 290 | uselzham = False 291 | if data[:4] == b'SCLZ': 292 | try: 293 | from lzham import decompress as d 294 | except: 295 | if not isWin: 296 | return info(string.not_installed2 % 'LZHAM') 297 | with open('temp.sc', 'wb') as sc: 298 | sc.write(b'LZH\x30' + data[4:9] + bytes(4) + data[9:]) 299 | if os.system(f'{lzham_path} -d{data[4]} -c d temp.sc _temp.sc{nul}'): 300 | raise Exception(string.decomp_err) 301 | data = open('_temp.sc', 'rb').read() 302 | [os.remove(i) for i in ('temp.sc', '_temp.sc')] 303 | else: 304 | dict_size, data_size = struct.unpack("BBHH", fileType, subType, width, height)) 367 | print() 368 | 369 | 370 | def compileSC(_dir, from_memory = [], imgdata = None, folder_export = None): 371 | name = _dir.split('/')[-2] 372 | if from_memory: 373 | files = from_memory 374 | else: 375 | files = [] 376 | [files.append(i) if i.endswith(".png") else None for i in os.listdir(_dir)] 377 | files.sort() 378 | if not files: 379 | return info(string.dir_empty % _dir.split('/')[-2]) 380 | files = [Image.open(f'{_dir}{i}') for i in files] 381 | 382 | exe = False 383 | info(string.collecting_inf) 384 | sc = io.BytesIO() 385 | 386 | if from_memory: 387 | uselzham = imgdata['uselzham'] 388 | else: 389 | try: 390 | scdata = open(f"{_dir}{name}.xcod", "rb") 391 | scdata.read(4) 392 | uselzham, = struct.unpack("?", scdata.read(1)) 393 | scdata.read(1) 394 | hasxcod = True 395 | except: 396 | info(string.not_xcod) 397 | info(string.standart_types) 398 | hasxcod = False 399 | uselzham = False 400 | 401 | 402 | if uselzham: 403 | try: 404 | import lzham 405 | except: 406 | if not isWin: 407 | return info(string.not_installed2 % 'LZHAM') 408 | exe = True 409 | else: 410 | try: 411 | import lzma 412 | except: 413 | if not isWin: 414 | return info(string.not_installed2 % 'LZMA') 415 | exe = True 416 | 417 | for picCount in range(len(files)): 418 | print() 419 | img = files[picCount] 420 | 421 | if from_memory: 422 | fileType = imgdata['data'][picCount]['fileType'] 423 | subType = imgdata['data'][picCount]['subType'] 424 | else: 425 | if hasxcod: 426 | fileType, subType, width, height = struct.unpack(">BBHH", scdata.read(6)) 427 | 428 | if (width, height) != img.size: 429 | info(string.illegal_size % (width, height, img.width, img.height)) 430 | if question(string.resize_qu): 431 | info(string.resizing) 432 | img = img.resize((width, height), Image.ANTIALIAS) 433 | else: 434 | fileType, subType = 1, 0 435 | 436 | width, height = img.size 437 | pixelSize = pixelsize(subType) 438 | img = img.convert(format(subType)) 439 | 440 | fileSize = width * height * pixelSize + 5 441 | 442 | info(string.about_sc % (name + '_' * picCount, fileType, fileSize, subType, width, height)) 443 | 444 | sc.write(struct.pack("2sII16s", b'SC', 1, 16, hashlib.md5(sc).digest())) 458 | info(string.header_done) 459 | if uselzham: 460 | info(string.comp_with % 'LZHAM') 461 | fout.write(struct.pack("<4sBI", b'SCLZ', 18, len(sc))) 462 | if exe: 463 | with open('temp.sc', 'wb') as s: 464 | s.write(sc) 465 | if os.system(f'{lzham_path} -d18 -c c temp.sc _temp.sc{nul}'): 466 | raise Exception(string.comp_err) 467 | with open('_temp.sc', 'rb') as s: 468 | s.read(13) 469 | compressed = s.read() 470 | [os.remove(i) for i in ('temp.sc', '_temp.sc')] 471 | 472 | else: 473 | compressed = lzham.compress(sc, {"dict_size_log2": 18}) 474 | 475 | fout.write(compressed) 476 | else: 477 | info(string.comp_with % 'LZMA') 478 | l = struct.pack(" region.top: 715 | region.top = tmpY 716 | if tmpX < region.left: 717 | region.left = tmpX 718 | if tmpY < region.bottom: 719 | region.bottom = tmpY 720 | if tmpX > region.right: 721 | region.right = tmpX 722 | 723 | 724 | sheetpoint = region.sheet_points[z] 725 | 726 | tmpX, tmpY = sheetpoint.pos 727 | 728 | if tmpX < regionMinX: 729 | regionMinX = tmpX 730 | if tmpX > regionMaxX: 731 | regionMaxX = tmpX 732 | if tmpY < regionMinY: 733 | regionMinY = tmpY 734 | if tmpY > regionMaxY: 735 | regionMaxY = tmpY 736 | 737 | region = region_rotation(region) 738 | 739 | tmpX, tmpY = regionMaxX - regionMinX, regionMaxY - regionMinY 740 | size = (tmpX, tmpY) 741 | 742 | if region.rotation in (90, 270): 743 | size = size[::-1] 744 | 745 | region.size = size 746 | 747 | spritedata[x].regions[y] = region 748 | 749 | return spriteglobals, spritedata, sheetdata 750 | 751 | 752 | def cut_sprites(spriteglobals, spritedata, sheetdata, sheetimage, xcod, folder_export): 753 | xcod.write(struct.pack('>H', spriteglobals.shape_count)) 754 | 755 | for x in range(spriteglobals.shape_count): 756 | xcod.write(struct.pack('>H', spritedata[x].total_regions)) 757 | 758 | progressbar(string.cut_sprites % (x+1, spriteglobals.shape_count), x, spriteglobals.shape_count) 759 | 760 | for y in range(spritedata[x].total_regions): 761 | 762 | region = spritedata[x].regions[y] 763 | 764 | polygon = [region.sheet_points[z].pos for z in range(region.num_points)] 765 | 766 | xcod.write(struct.pack('>2B2H', region.sheet_id, region.num_points, *sheetdata[region.sheet_id].pos) + b''.join(struct.pack('>2H', *i) for i in polygon) + struct.pack('?B', region.mirroring, region.rotation // 90)) 767 | 768 | 769 | imMask = Image.new('L', sheetdata[region.sheet_id].pos, 0) 770 | ImageDraw.Draw(imMask).polygon(polygon, fill=255) 771 | bbox = imMask.getbbox() 772 | if not bbox: 773 | continue 774 | 775 | regionsize = (bbox[2] - bbox[0], bbox[3] - bbox[1]) 776 | tmpRegion = Image.new('RGBA', regionsize, None) 777 | 778 | tmpRegion.paste(sheetimage[region.sheet_id].crop(bbox), None, imMask.crop(bbox)) 779 | if region.mirroring: 780 | tmpRegion = tmpRegion.transform(regionsize, Image.EXTENT, (regionsize[0], 0, 0, regionsize[1])) 781 | 782 | tmpRegion.rotate(region.rotation, expand=True) \ 783 | .save(f'{folder_export}/{x}_{y}.png') 784 | print() 785 | 786 | def place_sprites(xcod, folder): 787 | xcod = open(xcod, 'rb') 788 | files = os.listdir(folder) 789 | 790 | xcod.read(4) 791 | uselzham, picCount = struct.unpack('2B', xcod.read(2)) 792 | sheetimage = [] 793 | sheetimage_data = {'uselzham': uselzham, 'data': []} 794 | for i in range(picCount): 795 | fileType, subType, width, height = struct.unpack(">BBHH", xcod.read(6)) 796 | sheetimage.append(Image.new('RGBA', (width, height))) 797 | sheetimage_data['data'].append({'fileType': fileType, 'subType': subType}) 798 | 799 | shape_count, = struct.unpack('>H', xcod.read(2)) 800 | 801 | for x in range(shape_count): 802 | 803 | progressbar(string.place_sprites % (x+1, shape_count), x, shape_count) 804 | 805 | total_regions, = struct.unpack('>H', xcod.read(2)) 806 | 807 | for y in range(total_regions): 808 | 809 | sheet_id, num_points, x1, y1 = struct.unpack('>2B2H', xcod.read(6)) 810 | polygon = [struct.unpack('>2H', xcod.read(4)) for i in range(num_points)] 811 | mirroring, rotation = struct.unpack('?B', xcod.read(2)) 812 | rotation *= 90 813 | 814 | if f'{x}_{y}.png' not in files: 815 | continue 816 | 817 | tmpRegion = Image.open(f'{folder}/{x}_{y}.png') \ 818 | .convert('RGBA') \ 819 | .rotate(360 - rotation, expand=True) 820 | 821 | imMask = Image.new('L', (x1, y1), 0) 822 | ImageDraw.Draw(imMask).polygon(polygon, fill=255) 823 | bbox = imMask.getbbox() 824 | if not bbox: 825 | continue 826 | 827 | regionsize = (bbox[2] - bbox[0], bbox[3] - bbox[1]) 828 | 829 | if mirroring: 830 | tmpRegion = tmpRegion.transform(regionsize, Image.EXTENT, (tmpRegion.width, 0, 0, tmpRegion.height)) 831 | 832 | sheetimage[sheet_id].paste(tmpRegion, bbox[:2], tmpRegion) 833 | print() 834 | 835 | return sheetimage, sheetimage_data 836 | 837 | def region_rotation(region): 838 | 839 | def calc_sum(points, z): 840 | x1, y1 = points[(z + 1) % num_points].pos 841 | x2, y2 = points[z].pos 842 | return (x1 - x2) * (y1 + y2) 843 | 844 | sumSheet = 0 845 | sumShape = 0 846 | num_points = region.num_points 847 | 848 | for z in range(num_points): 849 | sumSheet += calc_sum(region.sheet_points, z) 850 | sumShape += calc_sum(region.shape_points, z) 851 | 852 | sheetOrientation = -1 if (sumSheet < 0) else 1 853 | shapeOrientation = -1 if (sumShape < 0) else 1 854 | 855 | region.mirroring = 0 if (shapeOrientation == sheetOrientation) else 1 856 | 857 | if region.mirroring: 858 | for x in range(num_points): 859 | pos = region.shape_points[x].pos 860 | region.shape_points[x].pos = (pos[0] *- 1, pos[1]) 861 | 862 | pos00 = region.sheet_points[0].pos 863 | pos01 = region.sheet_points[1].pos 864 | pos10 = region.shape_points[0].pos 865 | pos11 = region.shape_points[1].pos 866 | 867 | if pos01[0] > pos00[0]: 868 | px = 1 869 | elif pos01[0] < pos00[0]: 870 | px = 2 871 | else: 872 | px = 3 873 | 874 | if pos01[1] < pos00[1]: 875 | py = 1 876 | elif pos01[1] > pos00[1]: 877 | py = 2 878 | else: 879 | py = 3 880 | 881 | if pos11[0] > pos10[0]: 882 | qx = 1 883 | elif pos11[0] < pos10[0]: 884 | qx = 2 885 | else: 886 | qx = 3 887 | 888 | if pos11[1] > pos10[1]: 889 | qy = 1 890 | elif pos11[1] < pos10[1]: 891 | qy = 2 892 | else: 893 | qy = 3 894 | 895 | rotation = 0 896 | if px == qx and py == qy: 897 | rotation = 0 898 | 899 | elif px == 3: 900 | if px == qy: 901 | if py == qx : 902 | rotation = 1 903 | else: 904 | rotation = 3 905 | else: 906 | rotation = 2 907 | 908 | elif py == 3: 909 | if py == qx: 910 | if px == qy: 911 | rotation = 3 912 | else: 913 | rotation = 1 914 | else: 915 | rotation = 2 916 | 917 | elif px != qx and py != qy: 918 | rotation = 2 919 | 920 | elif px == py: 921 | if px != qx: 922 | rotation = 3 923 | elif py != qy: 924 | rotation = 1 925 | 926 | elif px != py: 927 | if px != qx: 928 | rotation = 1 929 | elif py != qy: 930 | rotation = 3 931 | 932 | if sheetOrientation == -1 and rotation in (1, 3): 933 | rotation += 2 934 | rotation %= 4 935 | 936 | region.rotation = rotation * 90 937 | return region 938 | 939 | --------------------------------------------------------------------------------