├── 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 |
--------------------------------------------------------------------------------