├── 1) unpack.bat ├── 2) apply names translations.bat ├── 3) pack.bat ├── Readme.md ├── search.bat ├── search.py ├── searchInExcelTables.bat ├── searchInExcelTables.py └── tools ├── apply_name_translations.py ├── cs2_decompile.exe ├── cstl tool help.bat ├── cstl_tool.zip ├── exkifint_v3.exe ├── exzt.exe ├── hgx2bmp.exe ├── mc.exe ├── pack.py ├── unpack.py └── zlib1.dll /1) unpack.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | mode con: cols=150 lines=40 3 | py -3.10 tools\unpack.py 4 | @pause -------------------------------------------------------------------------------- /2) apply names translations.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | mode con: cols=150 lines=30 3 | py -3.10 tools\apply_name_translations.py 4 | @pause -------------------------------------------------------------------------------- /3) pack.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | mode con: cols=150 lines=35 3 | py -3.10 tools\pack.py 4 | pause -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # CatSystem2 Simple Translating Tools 2 | Single-click tools to "extract text right into editable state" and "pack all it back". Or as simple as I am able to make it :D (WIP) 3 | 4 | In case of troubles you also can contact me (faster) at Discord: `sherekhanromeo` 5 | 6 | ### Requirements: 7 | + Python 3.10+ and its modules: 8 | + `pip install pandas` 9 | + `pip install xlsxwriter` 10 | + `pip install colorama` 11 | + `pip install openpyxl` 12 | 13 | ### Changelog v0.8.1: 14 | + Basic stable .cstl files translation support (TODO: automate language-related indexes) 15 | + Code fixes 16 | 17 | Next goal: rewrite code for multithreading support and make it into UI. 18 | 19 | HUGE thanks to Trigger and his **[TriggersTools.CatSystem2 wiki](https://github.com/trigger-segfault/TriggersTools.CatSystem2)** for gathering all info on CS2 file formats and tools in one place! 20 | 21 | ## How to use: 22 | 0) copy `.bat` files and `tools` folder into your game folder 23 | 1) run `1) unpack.bat` to get: 24 | * `source game files` will be all extracted game files. They are there for references, if you need them - copy them into other folder and edit them there. 25 | * `translate here` folder with `.xlsx` (and sometimes also`.ini`) files that contains all translatable text 26 | 2) translate names in `translate here/nametable.xlsx` and run `2) apply names translations.bat` to apply names translations to all `.xslx` files in `translate here/clean texts/` folder. (made for exact names placing to make sure some in-game features still work) 27 | 3) running `3) pack.bat` will generate `updateXX.in` archive with files included: 28 | * all translations from `translate here/clean localization texts/` `.ini` files will be automatically converted into `.cstl` game scenes scripts 29 | * all translations from `translate here/clean texts/` `.xlsx` files will be automatically converted into `.cst` game scenes scripts 30 | * all files from subfolders in `translate here/your files AS IS` folder (do NOT place files directly in `your files AS IS` - they will be ignored! Use `your files AS IS/other` folder for that) 31 | 32 | + IF GAME HAS LOCALIZATION `.cstl`/`.ini` FILES, THEY ARE PRIOR OVER `.cst`/`.xslx` TRANSLATION FILES. 33 | + IN THIS CASE TRANSLATE USING ONLY `.cstl`/`.ini` FILES. (unless you know what you're doing :D) 34 | 35 | ## Unpacker features: 36 | 1) extracts: 37 | + `.int` archives (using `exkifint_v3.exe` by asmodean) 38 | + `.cst` into `.txt` (using `cs2_decompile.exe` by Trigger) 39 | + `.cstl` into `.ini` (using `cstl_tool.zip` by Trigger) 40 | + also extracts everything worth translating into `.xlsx` files inside according folder 41 | 2) places files in according folders: 42 | * `source game files` folder with original game files used later as reference 43 | + folder `texts` with `.txt` files (already unpacked from `.cst` files) 44 | + folder `for manual processing` with other files: 45 | 1) folder `animations` with `.anm` files 46 | 2) folder `images` with `.hg2` and `.hg3` files 47 | 3) folder `movies` with `.mpg` files 48 | 4) folder `scripts` with `.fes` and `.kcs` files 49 | 5) folder `sounds` with `.ogg` and `.wav` files 50 | * `translate here` folder with everything ready to be translated or packed: 51 | + folder `clean texts` with `.xslx` files containing main game texts (extracted from text `.txt` files, will be used by this tool to generate new scene-files for your game) 52 | + folder `clean localization texts` with `.ini` files containing main game localizations 53 | + folder `your files AS IS` with categories (files you add there will be packed into archive, but won't be changed by this tool) 54 | 55 | 56 | 57 | ## Packer features: 58 | 1) copies original game files from `source game files/texts` folder and `nametable.csv`, applies translations to them and packs translated files 59 | 2) takes all files in `translate here/your files AS IS` subfolders and packs them "as is" 60 | 61 | ## Tested on games: 62 | 1) **[Amakano+](https://vndb.org/v19810)** (non-steam, unrated) = ✅ success 63 | 2) **[Grisaia no Kajitsu](https://vndb.org/v5154)** (non-steam, unrated) and (steam, all-ages) = ✅ success 64 | 3) **[Grisaia no Meikyuu](https://vndb.org/v7723)** (non-steam, unrated) = ✅ success 65 | 4) **[Grisaia no Rakuen](https://vndb.org/v7724)** (non-steam, unrated) = ✅ success 66 | 5) **[NekoPara vol.3](https://vndb.org/v19385)** (non-steam, unrated) = ✅ success 67 | 6) **[The girl who's called the world](https://vndb.org/v26987)** (non-steam, unrated) = ✅ success 68 | 7) **[Yuki Koi Melt](https://vndb.org/v15064)** (non-steam, unrated) = ✅ success 69 | 70 | 71 | ### Known problems: 72 | 1) ShiftJIS (game engine encoding) doesn't support use of some specific symbols from some languages: 73 | + `Ää, Öö, Üü, ß` from German; 74 | + `Áá, Ââ, Ãã, Àà, Çç, Éé, Êê, Íí, Óó, Ôô, Õõ, Úú` from portuguese; 75 | + `Ññ` from Spanish; 76 | + `Èè, Ëë, Îî, Ïï, Ûû, Ùù, Ÿÿ` from French; etc. 77 | 78 | Possible solution = create and use custom font that shows required unsupported symbols instead of unused symbols (f.e. Cyrillic ones), 79 | E.g.: you need the game to show word `Färbung` so you type something like `Fьrbung` and font shows `ь` as `ä`. Added it into ToDo list, will try to solve later. 80 | 81 | -------------------------------------------------------------------------------- /search.bat: -------------------------------------------------------------------------------- 1 | @python search.py 2 | @set /p = -------------------------------------------------------------------------------- /search.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | path = "D:/Misc/VNs/Grisaia 1 debug/source game files/texts" 4 | in_files = ".txt" # files of what format are being searched 5 | file_encoding = "ShiftJIS" # (RenPy usually uses UTF-8) 6 | 7 | phrase_we_look_for = "%" #case sensitive! (usually) 8 | 9 | 10 | def scan(path1): 11 | for f1 in os.listdir(path1): 12 | if os.path.isdir(os.path.join(path1, f1)): 13 | scan(os.path.join(path1, f1)) 14 | if os.path.isfile(os.path.join(path1, f1)) and os.path.join(path1, f1).endswith(in_files): 15 | with open(os.path.join(path1, f1), mode='r', encoding=file_encoding) as ff1: 16 | found = False 17 | for line in ff1.readlines(): 18 | if phrase_we_look_for in line: 19 | found = True 20 | print(line.replace("\r", "").replace("\n", "").replace("\fn", "").replace("[", "").replace("]", "")) 21 | if found: 22 | print("======== FOUND IN " + os.path.join(path1, f1) + " ========\n") 23 | 24 | print("SEARCH START...") 25 | scan(path) 26 | print("FINISHED") 27 | skip = input() 28 | time.sleep(10) -------------------------------------------------------------------------------- /searchInExcelTables.bat: -------------------------------------------------------------------------------- 1 | @py -3.10 searchInExcelTables.py 2 | @pause -------------------------------------------------------------------------------- /searchInExcelTables.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pandas 3 | from colorama import Fore, init 4 | 5 | init(autoreset=True) 6 | path = os.path.dirname(os.path.realpath(__file__)) 7 | in_files = ".xlsx" # files of what format are being searched 8 | file_encoding = "ShiftJIS" # (RenPy usually uses UTF-8) 9 | 10 | phrase_we_look_for = "Aide" #case sensitive! (usually) 11 | 12 | 13 | def scan(path1): 14 | for f1 in os.listdir(path1): 15 | filepath = os.path.join(path1, f1) 16 | if os.path.isdir(filepath): 17 | scan(filepath) 18 | if os.path.isfile(filepath) and filepath.endswith(in_files) and not f1.startswith("~"): 19 | xlsx_file = pandas.ExcelFile(filepath, engine='openpyxl') 20 | df = xlsx_file.parse(xlsx_file.sheet_names[0]) 21 | found = False #Line text 22 | if 'Character name' in df.keys(): 23 | for line in df['Character name']: 24 | try: 25 | if phrase_we_look_for in str(line).replace("[", "").replace("]", ""): 26 | found = True 27 | print(line.replace("\r", "").replace("\n", "").replace("\fn", "").replace("[", "").replace("]", "")) 28 | except Exception as error: 29 | print(line) 30 | print(filepath) 31 | raise error 32 | if found: 33 | print(Fore.GREEN + "======== FOUND IN " + filepath + " ========\n") 34 | 35 | if __name__ == "__main__": 36 | print("SEARCH START...") 37 | scan(path) 38 | print("FINISHED") 39 | print("You can close the program manually or press any key to do so.") 40 | skip = input() 41 | -------------------------------------------------------------------------------- /tools/apply_name_translations.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import traceback 4 | 5 | import pandas 6 | from colorama import init, Fore 7 | from pandas import DataFrame, ExcelWriter 8 | 9 | init(autoreset=True) 10 | 11 | messages = ["""CatSystem2 Simple tools (names translation tool) by ShereKhanRomeo\n 12 | HUGE thanks to Trigger-Segfault for explaining and tool links 13 | 14 | This tool will take translated names from `nametable.xlsx` file and apply them to all `.xlsx` files in`translate here\\clean texts` folder. 15 | `Repeat voice` button (speaker icon) won't work if names in `.cst` scripts differ from those in `nametable.csv`. 16 | Even if first time voicing worked correctly. 17 | This tool makes it easier to ensure that compatibility. 18 | 19 | If you found out that name translation needs to be changed (found new info during translation), 20 | you still can change name translation in `nametable.xlsx` table and run this tool again. 21 | It will update all names again, keeping all your already translated lines intact. 22 | 23 | {0}After applying names translations to `.xslx` text file DO NOT edit names there! 24 | They may contain special characters and widespaces (different from "spacebar" ones) needed for game engine's formatting. 25 | Edit names only in 'nametable.xlsx', then run this tool again. 26 | 27 | {1}If you sure you want to apply names translations, press Enter...""", 28 | 29 | "Copying files into 'extracted' folder and unpacking 'int'-archives may take up to 1 minute per file...", 30 | "Copying {0}...", 31 | "Processing {0}...", 32 | "Removing temporary files...", 33 | "Sorting extracted files...", 34 | Fore.GREEN + "Done! Program will be closed now.", 35 | Fore.YELLOW + "Following tools are missing: {0}\nDownload or unpack archive again.", 36 | Fore.YELLOW + """File 'nametable.csv' was not found after extracting specified archives! 37 | To find archive with that file you can use 'GARbro' Visual Novels resource browser made by 'morkt' from GitHub. 38 | Please, find archive with 'nametable.csv' since it is mandatory for ingame features and next translation steps. 39 | Press Enter to finish the program."""] 40 | 41 | # other variables 42 | dir_path = os.path.dirname(os.path.realpath(__file__)).replace("tools", "") 43 | dir_path_tools = dir_path + "tools\\" 44 | dir_path_extracted = dir_path + "source game files\\" 45 | dir_path_extracted_texts = dir_path_extracted + "texts\\" 46 | dir_path_extracted_translations = dir_path_extracted + "translations\\" 47 | dir_path_extracted_manually = dir_path_extracted + "for manual processing\\" 48 | dir_path_extracted_animations = dir_path_extracted_manually + "animations\\" 49 | dir_path_extracted_images = dir_path_extracted_manually + "images\\" 50 | dir_path_extracted_movies = dir_path_extracted_manually + "movies\\" 51 | dir_path_extracted_scripts = dir_path_extracted_manually + "scripts\\" 52 | dir_path_extracted_sounds = dir_path_extracted_manually + "sounds\\" 53 | 54 | dir_path_translate_here = dir_path + "translate here\\" 55 | dir_path_translate_here_clean_texts = dir_path_translate_here + "clean texts\\" 56 | dir_path_translate_here_other_files = dir_path_translate_here + "your files AS IS\\" 57 | dir_path_translate_here_other_files_sounds = dir_path_translate_here_other_files + "sounds\\" 58 | dir_path_translate_here_other_files_movies = dir_path_translate_here_other_files + "movies\\" 59 | dir_path_translate_here_other_files_images = dir_path_translate_here_other_files + "images\\" 60 | dir_path_translate_here_other_files_other = dir_path_translate_here_other_files + "other\\" 61 | 62 | TRANSLATION_LINE_PATTERN = "translation for line #{0}" 63 | ORIGINAL_LINE_PATTERN = "original line #{0}" 64 | TEXT_LINE_END1 = "\\fn\r\n" 65 | TEXT_LINE_END2 = "\\@\r\n" 66 | SCENE_LINESTART1 = "\tscene " 67 | SCENE_LINESTART2 = "\tstr 3 " # for Grisaia1 steam version 68 | SCENE_LINESTART3 = "\tstr 155 " # for Grisaia1 unrated version 69 | WRITE_TRANSLATION_HERE = "(write translation here)" 70 | CHOICE_OPTION = "choice_option" 71 | SCENE_TITLE = "scene_title" 72 | EMPTY_CHARACTER_NAME = "leave_empty" 73 | 74 | game_main = "cs2.exe" 75 | hgx2bmp_exe = "hgx2bmp.exe" 76 | zlib1_dll = "zlib1.dll" 77 | exzt_exe = "exzt.exe" 78 | exkifint_v3_exe = "exkifint_v3.exe" 79 | cs2_decompile_exe = "cs2_decompile.exe" 80 | 81 | # i am ignoring kx2.ini archive for now, since i have no idea about .kx2-files it has inside 82 | temp_archives = ["scene.int", "config.int", "update00.int", "update01.int", "update02.int", "update03.int", 83 | "update04.int", "update05.int", "update06.int", "update07.int", "update08.int", "update09.int", 84 | "update10.int", "update11.int", "update12.int", "update13.int", "update14.int", "update15.int"] 85 | temp_files = temp_archives.copy() 86 | temp_files.append(game_main) 87 | temp_tools = [hgx2bmp_exe, zlib1_dll, exzt_exe, exkifint_v3_exe, cs2_decompile_exe] 88 | optional_voice_packages = [] 89 | 90 | xlsx_original_names = [] 91 | xlsx_translated_names = [] 92 | 93 | 94 | def press_any_key(): 95 | skip = input() 96 | 97 | 98 | def check_all_tools_intact(): 99 | missing_files = "" 100 | for tool in temp_tools: 101 | if not os.path.exists(dir_path_tools + tool): 102 | missing_files += tool + " " 103 | if len(missing_files) > 0: 104 | print(str.format(messages[7], missing_files)) 105 | press_any_key() 106 | sys.exit(0) 107 | nametable_xlsx = dir_path_translate_here + 'nametable.xlsx' 108 | if not os.path.exists(nametable_xlsx) or not os.path.isfile(nametable_xlsx): 109 | print(messages[8]) # nametable not found 110 | press_any_key() 111 | sys.exit(0) 112 | 113 | 114 | def translate_name(_name_to_translate: str, _original_names, _translated_names): 115 | for i in range(len(_original_names)): 116 | # need to check and replace whole string, not just parts of it, like when using .replace() 117 | if _name_to_translate == _original_names[i] and not _translated_names[i] == "(translate name here)": 118 | return _translated_names[i] 119 | return _name_to_translate 120 | 121 | 122 | def apply_names_translations_nametable(): 123 | global xlsx_original_names, xlsx_translated_names 124 | nametable_csv = dir_path_extracted + 'nametable.csv' 125 | nametable_xlsx = dir_path_translate_here + 'nametable.xlsx' 126 | print(str.format(messages[3], 'nametable.xlsx')) 127 | xlsx_file = pandas.ExcelFile(nametable_xlsx) 128 | df1 = xlsx_file.parse(xlsx_file.sheet_names[0]) 129 | xlsx_original_names = list(df1[df1.columns[0]]).copy() 130 | xlsx_translated_names = list(df1[df1.columns[2]]).copy() 131 | 132 | df0 = pandas.read_csv(nametable_csv, encoding='ShiftJIS') 133 | column0_array = df0.columns[0].split('\t') 134 | jp_brackets0 = False 135 | if len(column0_array) > 1: 136 | if "【" in column0_array[1] or "】" in column0_array[1]: 137 | jp_brackets0 = True 138 | name0 = column0_array[1].strip("【】").replace("\\fs \\fn", " ") 139 | else: 140 | name0 = column0_array[1].replace("\\fs \\fn", " ") 141 | translated_name0 = translate_name(name0, xlsx_original_names, xlsx_translated_names).replace(" ", "\\fs \\fn").replace('ë', 'ё') 142 | if jp_brackets0: 143 | translated_name0 = "【" + translated_name0 + "】" 144 | column0_array[1] = translated_name0 145 | df0.rename(columns={df0.columns[0]: "\t".join(column0_array)}, inplace=True) 146 | 147 | for line_array in df0.to_numpy(): 148 | array = line_array[0].split('\t') 149 | jp_brackets = False 150 | if len(array) > 1: 151 | if "【" in array[1] or "】" in array[1]: 152 | jp_brackets = True 153 | name = array[1].strip("【】").replace("\\fs \\fn", " ") 154 | else: 155 | name = array[1].replace("\\fs \\fn", " ") 156 | translated_name = translate_name(name, xlsx_original_names, xlsx_translated_names).replace(" ", "\\fs \\fn").replace('ë', 'ё') 157 | if jp_brackets: 158 | translated_name = "【" + translated_name + "】" 159 | array[1] = translated_name 160 | line_array[0] = "\t".join(array) 161 | 162 | df0.to_csv(dir_path_translate_here + 'nametable.csv', encoding='ShiftJIS', index=False) 163 | 164 | 165 | def apply_names_translations_texts(): 166 | global xlsx_original_names, xlsx_translated_names 167 | 168 | for filename in os.listdir(dir_path_translate_here_clean_texts): 169 | file_xlsx = dir_path_translate_here_clean_texts + filename 170 | if os.path.isfile(file_xlsx) and file_xlsx.endswith(".xlsx"): 171 | print(str.format(messages[3], filename)) 172 | xlsx_file = pandas.ExcelFile(file_xlsx) 173 | df0 = xlsx_file.parse(xlsx_file.sheet_names[0]) 174 | column1_ids = list(df0[df0.columns[0]]).copy() 175 | column2_names = [] 176 | column2_names_old = list(df0[df0.columns[1]]).copy() 177 | column3_lines = list(df0[df0.columns[2]]).copy() 178 | 179 | while len(column2_names_old) > 1: 180 | column2_names.append(column2_names_old.pop(0)) 181 | column2_names.append(translate_name(column2_names_old.pop(0).replace("\\fs \\fn", " "), xlsx_original_names, xlsx_translated_names).replace(" ", "\\fs \\fn")) 182 | 183 | df = DataFrame({"Lines numbers": column1_ids, "Character name": column2_names, "Line text": column3_lines}) 184 | writer = ExcelWriter(file_xlsx) 185 | df.to_excel(writer, sheet_name='sheetName', index=False, na_rep='NaN') 186 | for column in df: 187 | column_length = max(df[column].astype(str).map(len).max(), len(column)) 188 | col_idx = df.columns.get_loc(column) 189 | writer.sheets['sheetName'].set_column(col_idx, col_idx, column_length) 190 | writer.close() 191 | 192 | 193 | # core logic 194 | try: 195 | check_all_tools_intact() 196 | print(str.format(messages[0], Fore.YELLOW, Fore.RESET)) 197 | press_any_key() 198 | apply_names_translations_nametable() 199 | apply_names_translations_texts() 200 | 201 | except Exception as error: 202 | print("ERROR - " + str("".join(traceback.format_exception(type(error), 203 | value=error, 204 | tb=error.__traceback__))).split( 205 | "The above exception was the direct cause of the following")[0]) 206 | finally: 207 | print(messages[6]) # done 208 | -------------------------------------------------------------------------------- /tools/cs2_decompile.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wolverator/CatSystem2-Simple-Translating-Tools/4321bc77d48d4fa353b79b135b534ee0d5b6daa1/tools/cs2_decompile.exe -------------------------------------------------------------------------------- /tools/cstl tool help.bat: -------------------------------------------------------------------------------- 1 | @pause 2 | @python cstl_tool.zip -h 3 | @pause -------------------------------------------------------------------------------- /tools/cstl_tool.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wolverator/CatSystem2-Simple-Translating-Tools/4321bc77d48d4fa353b79b135b534ee0d5b6daa1/tools/cstl_tool.zip -------------------------------------------------------------------------------- /tools/exkifint_v3.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wolverator/CatSystem2-Simple-Translating-Tools/4321bc77d48d4fa353b79b135b534ee0d5b6daa1/tools/exkifint_v3.exe -------------------------------------------------------------------------------- /tools/exzt.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wolverator/CatSystem2-Simple-Translating-Tools/4321bc77d48d4fa353b79b135b534ee0d5b6daa1/tools/exzt.exe -------------------------------------------------------------------------------- /tools/hgx2bmp.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wolverator/CatSystem2-Simple-Translating-Tools/4321bc77d48d4fa353b79b135b534ee0d5b6daa1/tools/hgx2bmp.exe -------------------------------------------------------------------------------- /tools/mc.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wolverator/CatSystem2-Simple-Translating-Tools/4321bc77d48d4fa353b79b135b534ee0d5b6daa1/tools/mc.exe -------------------------------------------------------------------------------- /tools/pack.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import os 3 | import shutil 4 | import sys 5 | import traceback 6 | from sys import executable 7 | from os import path 8 | from re import compile, escape, IGNORECASE 9 | from subprocess import call, DEVNULL 10 | 11 | import pandas 12 | from colorama import init, Fore 13 | 14 | init(autoreset=True) 15 | 16 | # other variables 17 | dir_path = path.dirname(path.realpath(__file__)).replace("tools", "") 18 | print("Path: " + dir_path) 19 | dir_path_tools = dir_path + "tools\\" 20 | dir_path_package = dir_path + "package\\" 21 | dir_path_extracted = dir_path + "source game files\\" 22 | dir_path_extracted_texts = dir_path_extracted + "texts\\" 23 | dir_path_extracted_localization_texts = dir_path_extracted + "localization texts\\" 24 | dir_path_extracted_manually = dir_path_extracted + "for manual processing\\" 25 | dir_path_extracted_animations = dir_path_extracted_manually + "animations\\" 26 | dir_path_extracted_images = dir_path_extracted_manually + "images\\" 27 | dir_path_extracted_movies = dir_path_extracted_manually + "movies\\" 28 | dir_path_extracted_scripts = dir_path_extracted_manually + "scripts\\" 29 | dir_path_extracted_sounds = dir_path_extracted_manually + "sounds\\" 30 | 31 | dir_path_translate_here = dir_path + "translate here\\" 32 | dir_path_translate_here_clean_texts = dir_path_translate_here + "clean texts\\" 33 | dir_path_translate_here_clean_localization_texts = dir_path_translate_here + "clean localization texts\\" 34 | dir_path_translate_here_other_files = dir_path_translate_here + "your files AS IS\\" 35 | dir_path_translate_here_other_files_sounds = dir_path_translate_here_other_files + "sounds\\" 36 | dir_path_translate_here_other_files_movies = dir_path_translate_here_other_files + "movies\\" 37 | dir_path_translate_here_other_files_images = dir_path_translate_here_other_files + "images\\" 38 | dir_path_translate_here_other_files_other = dir_path_translate_here_other_files + "other\\" 39 | 40 | empty_character_name = "leave_empty" 41 | WRITE_TRANSLATION_HERE = "(write translation here)" 42 | 43 | game_main = None 44 | mc_exe = "mc.exe" 45 | cstl_tool_zip = "cstl_tool.zip" 46 | temp_tools = [mc_exe, cstl_tool_zip] 47 | 48 | messages = ["""CatSystem2 Simple tools (packing tool) by ShereKhanRomeo\n 49 | HUGE thanks to Trigger-Segfault for explaining and tool links 50 | check his wiki here https://github.com/trigger-segfault/TriggersTools.CatSystem2/wiki \n 51 | All files from "extracted" folder will be compiled into archive with number of your choice. 52 | 53 | Note that if you enter single-digit number 'X' for archive, \n 54 | it will be converted to double-digit number with leading zero: '0X'. 55 | It is OK and made just for CatSystem2 game engine compatibility. 56 | 57 | Press Enter to start...""", 58 | Fore.GREEN + "Done! Program will be closed now.", 59 | Fore.LIGHTRED_EX + "Following tools are missing: {0}\nDownload or unpack archive again.", 60 | "Processing {0}...", 61 | "Packing texts...", 62 | "Packing images...", 63 | "Packing movies...", 64 | "Packing scripts...", 65 | "\nEnter number for resulting archive name (choose from '4' to '99'; leave empty for '04' by default): ", 66 | Fore.YELLOW + "Your input was incorrect in some way. Using number '13' by default."] 67 | 68 | 69 | # functions 70 | def press_any_key(): 71 | skip = input() 72 | 73 | 74 | def create_if_not_exists(_path_to_file_or_dir: str): 75 | if not os.path.exists(_path_to_file_or_dir): 76 | os.mkdir(_path_to_file_or_dir) 77 | 78 | 79 | def check_all_tools_intact(): 80 | global game_main 81 | missing_files = "" 82 | for tool in temp_tools: 83 | if not os.path.exists(dir_path_tools + tool): 84 | missing_files += tool + " " 85 | if len(missing_files) > 0: 86 | print(str.format(messages[2], missing_files)) 87 | press_any_key() 88 | sys.exit(0) 89 | create_if_not_exists(dir_path_package) 90 | 91 | if path.exists(dir_path + "\\cs2.exe"): 92 | game_main = "cs2.exe" 93 | if path.exists(dir_path + "\\amakanoPlus.exe"): 94 | game_main = "amakanoPlus.exe" 95 | if path.exists(dir_path + "\\grisaia.exe"): 96 | game_main = "grisaia.exe" 97 | if path.exists(dir_path + "\\Grisaia2.exe"): 98 | game_main = "Grisaia2.exe" 99 | if path.exists(dir_path + "\\Grisaia3.exe"): 100 | game_main = "Grisaia3.exe" 101 | if path.exists(dir_path + "\\YukikoiMelt.exe"): 102 | game_main = "YukikoiMelt.exe" 103 | if path.exists(dir_path + "\\rinko.exe"): 104 | game_main = "rinko.exe" 105 | if path.exists(dir_path + "\\ISLAND.exe"): 106 | game_main = "ISLAND.exe" 107 | 108 | if game_main == "None": 109 | print(Fore.RED + "Main game executable is not found!\n" + 110 | Fore.YELLOW + "If it's '[game name].exe' - please rename it into 'cs2.exe'\n" 111 | "Unpacker will be closed now...") 112 | exit(0) 113 | else: 114 | print(Fore.GREEN + "Found main game executable: " + game_main) 115 | 116 | 117 | def delete_file(path_to_file: str): 118 | if os.path.exists(path_to_file): 119 | os.chmod(path_to_file, 0o777) 120 | os.remove(path_to_file) 121 | 122 | 123 | def remove_empty_folders(): 124 | for pth in os.listdir(dir_path): 125 | if os.path.isdir(dir_path + pth): 126 | with os.scandir(dir_path + pth) as it: 127 | if not any(it): 128 | os.rmdir(dir_path + pth) 129 | 130 | 131 | def clean_files_from_dir(_dir: str, _filetype: str): 132 | for filename in os.listdir(_dir): 133 | file = _dir + filename 134 | if file.endswith(_filetype): 135 | delete_file(file) 136 | 137 | 138 | def pack_from_ini_to_cstl_files(): 139 | print("Processing .ini files...") 140 | os.chdir(dir_path) 141 | if os.path.exists(dir_path_translate_here_clean_localization_texts): 142 | ini_files = os.listdir(dir_path_translate_here_clean_localization_texts) 143 | for filename in ini_files: 144 | # write translations from .ini files to .cstl files into `package` folder 145 | if len(ini_files) > 0 and os.path.isfile(dir_path_translate_here_clean_localization_texts + filename) \ 146 | and filename.endswith(".ini") \ 147 | and not filename.startswith("~"): 148 | print(str.format(messages[3], filename)) 149 | call([ 150 | executable, 151 | dir_path_tools + cstl_tool_zip, 152 | "-c", dir_path_translate_here_clean_localization_texts + filename, 153 | "-o", dir_path_package + filename.replace(".ini", ".cstl") 154 | ], stdin=None, stdout=DEVNULL, stderr=None, shell=False) 155 | 156 | 157 | def wrap_with_square_brackets(_text_to_wrap: str) -> str: 158 | result = '' 159 | text_to_wrap_list = _text_to_wrap.split(' ') 160 | if len(text_to_wrap_list) > 1: 161 | result = '[' + text_to_wrap_list.pop(0) + "] [" 162 | result += "] [".join(text_to_wrap_list) + ']' 163 | else: 164 | result = '[' + _text_to_wrap + ']' 165 | return result 166 | 167 | 168 | def pack_from_xlsx_to_cst_files(): 169 | print("Processing .xlsx files...") 170 | os.chdir(dir_path) 171 | for filename in os.listdir(dir_path_translate_here_clean_texts): 172 | # first - get translations from .xlsx files 173 | if os.path.isfile(dir_path_translate_here_clean_texts + filename) and filename.endswith( 174 | ".xlsx") and not filename.startswith("~"): 175 | #print(str.format(messages[3], filename)) 176 | df = pandas.ExcelFile(dir_path_translate_here_clean_texts + filename) \ 177 | .parse(pandas.ExcelFile(dir_path_translate_here_clean_texts + filename).sheet_names[0]).fillna('') 178 | text_lines = list(df[df.columns[2]]).copy() 179 | text_names = list(df[df.columns[1]]).copy() 180 | text_file = filename.replace(".xlsx", ".txt") 181 | 182 | encoding = "ShiftJIS" 183 | # second - get original texts from .txt files in `extracted\texts\` 184 | if os.path.exists(dir_path_extracted_texts + text_file): 185 | with codecs.open(dir_path_extracted_texts + text_file, mode="r", 186 | encoding=encoding) as source_txt_file: 187 | # TODO: script crashes here while trying to read file with weird symbols - need urgent fix 188 | file_lines = source_txt_file.readlines() 189 | source_txt_file.close() 190 | # third - write resulting .txt files with translation into `package` folder 191 | with codecs.open(dir_path_package + text_file, mode="w", encoding=encoding) as result_txt_file: 192 | for file_line in file_lines: 193 | #if (len(text_lines) > 0) and not isinstance(text_lines[0], str): 194 | #print(Fore.CYAN + str(type(text_lines[0])) + ' ||' + str(text_lines[0])) 195 | if (len(text_lines) > 0) and (str(text_lines[0]) in file_line): 196 | text_to_replace = str(text_lines.pop(0)) 197 | replacement_text = str(text_lines.pop(0)) 198 | name_to_replace = str(text_names.pop(0)) 199 | replacement_name = str(text_names.pop(0)) 200 | 201 | if WRITE_TRANSLATION_HERE in replacement_text: 202 | pass 203 | else: 204 | replacement_text = replacement_text.replace('№', '#') \ 205 | .replace('…', '...') \ 206 | .replace('"', '“') \ 207 | .replace('ë', 'ё') \ 208 | .replace("'", '`') \ 209 | .replace('—', '―') \ 210 | .replace('–', '―') \ 211 | .replace('«', '"') \ 212 | .replace('»', '"') \ 213 | .replace('~', '~') 214 | # if not scene title - wrap each word after first one with [] 215 | if name_to_replace == "scene_title": 216 | replacement_text = replacement_text.replace(' ', '_') 217 | else: 218 | replacement_text = wrap_with_square_brackets(replacement_text) 219 | #print(text_to_replace) 220 | #print(replacement_text) 221 | #print('3') 222 | file_line = file_line.replace(text_to_replace, replacement_text) 223 | file_line = file_line.replace(name_to_replace, replacement_name) 224 | try: 225 | result_txt_file.write(file_line) 226 | result_txt_file.flush() 227 | except UnicodeEncodeError as uniError: 228 | print( 229 | Fore.RED + "A problem occurred while processing line:|" + Fore.RESET + file_line + Fore.RED + "|") 230 | print( 231 | Fore.YELLOW + "Seems like there is a symbol that cannot be encoded into game files encoding.\n" 232 | "Please, let me know about this error at Git Issues and specify the unicode code of character that caused the problem!") 233 | raise uniError 234 | else: 235 | raise Exception(str.format( 236 | "ERROR - Missing required file = {0}.\nPlease, restore the file into {1} folder or do unpacking process again!", 237 | text_file, dir_path_extracted_texts)) 238 | # then use mc.exe 239 | os.chdir(dir_path_package) 240 | print(messages[4]) 241 | shutil.copy(dir_path_tools + mc_exe, dir_path_package + mc_exe) 242 | call([mc_exe, "*"], stdin=None, stdout=None, stderr=None, shell=False) 243 | clean_files_from_dir(dir_path_package, ".txt") 244 | delete_file(dir_path_package + mc_exe) 245 | os.chdir(dir_path) 246 | 247 | 248 | def copy_all_files_from_to(_from, _to): 249 | if os.path.exists(_from) and os.path.isdir(_from) and os.path.exists(_to) and os.path.isdir(_to): 250 | for filename in os.listdir(_from): 251 | if os.path.isfile(_from + filename): 252 | print("Preparing " + filename) 253 | shutil.copy(_from + filename, _to + filename) 254 | 255 | 256 | def pack_int_archive(): 257 | os.chdir(dir_path) 258 | archive_number = input(messages[8]) or "4" 259 | if not archive_number.isdigit(): 260 | print(messages[9]) 261 | archive_number = "4" 262 | if len(archive_number) == 1: 263 | archive_number = "0" + archive_number 264 | archive_name = str.format("update{0}.int", archive_number) 265 | print(str.format("Packing archive {0}...", archive_name)) 266 | files = [dir_path_package + '*', # main text files (.cst and .cstl) 267 | dir_path_translate_here_other_files_sounds + '*', 268 | dir_path_translate_here_other_files_images + '*', 269 | dir_path_translate_here_other_files_movies + '*', 270 | dir_path_translate_here_other_files_other + '*'] 271 | if os.path.exists(dir_path_translate_here + 'nametable.csv'): 272 | files.append(dir_path_translate_here + 'nametable.csv') 273 | makeint(archive_name, files) 274 | clean_files_from_dir(dir_path_package, ".cst") 275 | clean_files_from_dir(dir_path_package, ".cstl") 276 | clean_files_from_dir(dir_path_package, ".ini") 277 | clean_files_from_dir(dir_path_package, ".hg3") 278 | clean_files_from_dir(dir_path_package, ".mpg") 279 | clean_files_from_dir(dir_path_package, ".ogg") 280 | clean_files_from_dir(dir_path_package, ".fes") 281 | clean_files_from_dir(dir_path_package, ".csv") 282 | clean_files_from_dir(dir_path_package, ".xml") 283 | clean_files_from_dir(dir_path_package, ".dat") 284 | clean_files_from_dir(dir_path_package, ".png") 285 | remove_empty_folders() 286 | 287 | 288 | def findfiles(wildcards: list) -> list: 289 | files = [] 290 | for _path in wildcards: 291 | if '*' not in _path and '?' not in _path: 292 | # Just a regular file path 293 | files.append(_path) 294 | else: 295 | filedir, name = os.path.split(_path) 296 | regex = compile(escape(name).replace(r'\*', '.*').replace(r'\?', '.?'), IGNORECASE) 297 | for file in os.listdir(filedir or '.'): 298 | if regex.search(file): 299 | # Don't join path if filedir is empty 300 | files.append(os.path.join(filedir, file) if filedir else file) 301 | return files 302 | 303 | 304 | def makeint(archive: str, wildcards: list): 305 | files = findfiles(wildcards) 306 | return writeint(archive, files) 307 | 308 | 309 | def writeint(archive: str, files: list): 310 | import os.path 311 | from struct import Struct 312 | KIFHDR = Struct('<4sI') 313 | KIFENTRY = Struct('<64sII') 314 | 315 | class Entry: 316 | def __init__(self, file: str): 317 | self.path = file 318 | self.name = os.path.basename(file) 319 | self.offset = 0 320 | self.length = os.path.getsize(file) 321 | 322 | def pack(self): 323 | return KIFENTRY.pack(self.name.encode('cp932'), self.offset, self.length) 324 | 325 | entries = [Entry(file) for file in files] 326 | offset = KIFHDR.size + KIFENTRY.size * len(files) 327 | for entry in entries: 328 | entry.offset = offset 329 | offset += entry.length 330 | with open(archive, 'wb+') as fw: 331 | fw.write(KIFHDR.pack(b'KIF\x00', len(files))) 332 | for entry in entries: 333 | fw.write(entry.pack()) 334 | for entry in entries: 335 | with open(entry.path, 'rb') as fr: 336 | data = fr.read() 337 | fw.write(data) 338 | 339 | 340 | # core logic 341 | if __name__ == "__main__": 342 | try: 343 | check_all_tools_intact() 344 | print(messages[0]) 345 | press_any_key() 346 | pack_from_xlsx_to_cst_files() 347 | pack_from_ini_to_cstl_files() 348 | pack_int_archive() 349 | 350 | except Exception as error: 351 | print("ERROR - " + str("".join(traceback.format_exception(type(error), 352 | value=error, 353 | tb=error.__traceback__))).split( 354 | "The above exception was the direct cause of the following")[0]) 355 | finally: 356 | print(messages[1]) 357 | -------------------------------------------------------------------------------- /tools/unpack.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | 3 | import struct 4 | import zlib 5 | from sys import executable 6 | from codecs import open as copen 7 | from os import path, mkdir, chmod, rename, chdir, listdir, remove, scandir, rmdir 8 | from shutil import copy, move 9 | from traceback import format_exception 10 | from colorama import init, Fore 11 | from subprocess import call, DEVNULL 12 | from pandas import DataFrame, ExcelWriter, read_csv 13 | 14 | init(autoreset=True) 15 | 16 | game_main = "None" 17 | 18 | messages = ["""CatSystem2 Simple tools (extraction tool) by ShereKhanRomeo\n 19 | HUGE thanks to Trigger-Segfault for explaining and tool links\n 20 | Following game files are MANDATORY to be in the folder with this unpacker:\n 21 | 0) main game executable (found successfully) 22 | 1) config.int = needed for 'nametable.csv' in it 23 | 2) scene.int = contains all story's text scripts\n 24 | optional files, if game has any of those: 25 | 3) update00.int\n4) update01.int and all other 'updateXX.int' files 26 | 27 | Files in each 'updateXX.int' archive overwrite according files from other int-archives, 28 | including files from 'updateXX.int' archives with lesser number, so you better copy here all update-files. 29 | Thus, this unpacker extracts mandatory archives first and then overrides extracted files with ones 30 | unpacked from 'updateXX' archives. 31 | 32 | Current version automatically unpacks and prepares for translation .cst and cstl-scripts with texts and 'nametable.csv'. 33 | Other kinds of files to translate game menu, images or videos will be extracted, but not processed automatically. 34 | 35 | After copying all files into this folder press Enter...""", 36 | 37 | "Copying files into 'extracted' folder and unpacking 'int'-archives may take up to 1 minute per file...", 38 | "Copying {0}...", 39 | "Processing {0}...", 40 | "Removing temporary files...", 41 | "Sorting extracted files...", 42 | Fore.GREEN + "Done! Program will be closed now.", 43 | Fore.YELLOW + "Following tools are missing: {0}\nDownload or unpack archive again.", 44 | Fore.YELLOW + """File 'nametable.csv' was not found after extracting specified archives! 45 | To find archive with that file you can use 'GARbro' Visual Novels resource browser made by 'morkt' from GitHub. 46 | Please, find archive with 'nametable.csv' since it is mandatory for ingame features and next translation steps. 47 | Press Enter to finish the program."""] 48 | 49 | # other variables 50 | dir_path = path.dirname(path.realpath(__file__)).replace("tools", "") 51 | print("Path: " + dir_path) 52 | dir_path_tools = dir_path + "tools\\" 53 | dir_path_extracted = dir_path + "source game files\\" 54 | dir_path_extracted_texts = dir_path_extracted + "texts\\" 55 | dir_path_extracted_localization_texts = dir_path_extracted + "localization texts\\" 56 | dir_path_extracted_manually = dir_path_extracted + "for manual processing\\" 57 | dir_path_extracted_animations = dir_path_extracted_manually + "animations\\" 58 | dir_path_extracted_images = dir_path_extracted_manually + "images\\" 59 | dir_path_extracted_movies = dir_path_extracted_manually + "movies\\" 60 | dir_path_extracted_scripts = dir_path_extracted_manually + "scripts\\" 61 | dir_path_extracted_sounds = dir_path_extracted_manually + "sounds\\" 62 | 63 | dir_path_translate_here = dir_path + "translate here\\" 64 | dir_path_translate_here_clean_texts = dir_path_translate_here + "clean texts\\" 65 | dir_path_translate_here_clean_localization_texts = dir_path_translate_here + "clean localization texts\\" 66 | dir_path_translate_here_other_files = dir_path_translate_here + "your files AS IS\\" 67 | dir_path_translate_here_other_files_sounds = dir_path_translate_here_other_files + "sounds\\" 68 | dir_path_translate_here_other_files_movies = dir_path_translate_here_other_files + "movies\\" 69 | dir_path_translate_here_other_files_images = dir_path_translate_here_other_files + "images\\" 70 | dir_path_translate_here_other_files_other = dir_path_translate_here_other_files + "other\\" 71 | 72 | TRANSLATION_LINE_PATTERN = "translation for line #{0}" 73 | ORIGINAL_LINE_PATTERN = "original line #{0}" 74 | TEXT_LINE_END1 = "\\fn\r\n" 75 | TEXT_LINE_END2 = "\\@\r\n" 76 | SCENE_LINESTART1 = "\tscene " 77 | SCENE_LINESTART2 = "\tstr 3 " # for Grisaia1 steam version 78 | SCENE_LINESTART3 = "\tstr 155 " # for Grisaia1 unrated version 79 | WRITE_TRANSLATION_HERE = "(write translation here)" 80 | CHOICE_OPTION = "choice_option" 81 | SCENE_TITLE = "scene_title" 82 | EMPTY_CHARACTER_NAME = "leave_empty" 83 | 84 | hgx2bmp_exe = "hgx2bmp.exe" 85 | zlib1_dll = "zlib1.dll" 86 | exzt_exe = "exzt.exe" 87 | exkifint_v3_exe = "exkifint_v3.exe" 88 | cs2_decompile_exe = "cs2_decompile.exe" 89 | cstl_tool_zip = "cstl_tool.zip" 90 | 91 | # i am ignoring kx2.ini archive for now, since i have no idea about .kx2-files it has inside 92 | temp_archives = ["scene.int", "fes.int", "config.int", "update00.int", "update01.int", "update02.int", "update03.int", 93 | "update04.int", "update05.int", "update06.int", "update07.int", "update08.int", "update09.int", 94 | "update10.int", "update11.int", "update12.int", "update13.int", "update14.int", "update15.int"] 95 | temp_tools = [hgx2bmp_exe, zlib1_dll, exzt_exe, exkifint_v3_exe, cs2_decompile_exe, cstl_tool_zip] 96 | optional_voice_packages = [] 97 | 98 | 99 | # functions 100 | def press_any_key(): 101 | input() 102 | 103 | 104 | def create_if_not_exists(_path_to_file_or_dir: str): 105 | if not path.exists(_path_to_file_or_dir): 106 | mkdir(_path_to_file_or_dir) 107 | 108 | 109 | def prepare_for_work(): 110 | global optional_voice_packages 111 | import string 112 | template = "pcm_{0}.int" 113 | for letter in string.ascii_letters: 114 | optional_voice_packages.append(str.format(template, letter)) 115 | create_if_not_exists(dir_path_extracted) 116 | create_if_not_exists(dir_path_extracted_manually) 117 | create_if_not_exists(dir_path_extracted_animations) 118 | create_if_not_exists(dir_path_extracted_images) 119 | create_if_not_exists(dir_path_extracted_movies) 120 | create_if_not_exists(dir_path_extracted_scripts) 121 | create_if_not_exists(dir_path_extracted_sounds) 122 | create_if_not_exists(dir_path_extracted_texts) 123 | create_if_not_exists(dir_path_extracted_localization_texts) 124 | create_if_not_exists(dir_path_translate_here) 125 | create_if_not_exists(dir_path_translate_here_clean_texts) 126 | create_if_not_exists(dir_path_translate_here_clean_localization_texts) 127 | create_if_not_exists(dir_path_translate_here_other_files) 128 | create_if_not_exists(dir_path_translate_here_other_files_images) 129 | create_if_not_exists(dir_path_translate_here_other_files_movies) 130 | create_if_not_exists(dir_path_translate_here_other_files_sounds) 131 | create_if_not_exists(dir_path_translate_here_other_files_other) 132 | 133 | 134 | def check_all_tools_intact(): 135 | global game_main, temp_tools 136 | dir_path_cs2_bin = dir_path + "\\cs2.bin" 137 | if path.exists(dir_path_cs2_bin): 138 | chmod(dir_path_cs2_bin, 0o777) 139 | chmod(dir_path + "\\cs2.exe", 0o777) 140 | rename(dir_path + "\\cs2.exe", dir_path + "\\сs2.bin") # rename into cs2.bin where C is russian XD 141 | rename(dir_path_cs2_bin, dir_path + "\\cs2.exe") 142 | 143 | if path.exists(dir_path + "\\cs2.exe"): 144 | game_main = "cs2.exe" 145 | if path.exists(dir_path + "\\amakanoPlus.exe"): 146 | game_main = "amakanoPlus.exe" 147 | if path.exists(dir_path + "\\grisaia.exe"): 148 | game_main = "grisaia.exe" 149 | if path.exists(dir_path + "\\Grisaia2.exe"): 150 | game_main = "Grisaia2.exe" 151 | if path.exists(dir_path + "\\Grisaia3.exe"): 152 | game_main = "Grisaia3.exe" 153 | if path.exists(dir_path + "\\YukikoiMelt.exe"): 154 | game_main = "YukikoiMelt.exe" 155 | if path.exists(dir_path + "\\rinko.exe"): 156 | game_main = "rinko.exe" 157 | if path.exists(dir_path + "\\ISLAND.exe"): 158 | game_main = "ISLAND.exe" 159 | 160 | if game_main == "None": 161 | print(Fore.RED + "Main game executable is not found!\n" + 162 | Fore.YELLOW + "If it's '[game name].exe' - please rename it into 'cs2.exe'\n" 163 | "Unpacker will be closed now...") 164 | exit(0) 165 | else: 166 | print(Fore.GREEN + "Found main game executable: " + game_main) 167 | 168 | missing_files = "" 169 | for tool in temp_tools: 170 | if not path.exists(dir_path_tools + tool): 171 | missing_files += tool + " " 172 | if len(missing_files) > 0: 173 | print(str.format(messages[7], missing_files)) 174 | press_any_key() 175 | exit(0) 176 | temp_tools.append(game_main) 177 | 178 | 179 | def copy_files_into_extract_folder_and_extract(): 180 | global game_main 181 | copy(dir_path + "\\" + game_main, dir_path_extracted + game_main) 182 | copy(dir_path_tools + exkifint_v3_exe, dir_path_extracted + exkifint_v3_exe) 183 | for file in temp_archives: 184 | if path.exists(dir_path + "\\" + file): 185 | print(str.format(messages[2], file)) 186 | copy(dir_path + "\\" + file, dir_path_extracted + file) 187 | print(messages[1]) 188 | chdir(dir_path_extracted) 189 | # unpacking from int archives 190 | if path.exists(dir_path_extracted + exkifint_v3_exe) and path.exists(dir_path_extracted + game_main): 191 | for archive in listdir(dir_path_extracted): 192 | if archive in temp_archives: 193 | print(str.format(messages[3], archive)) 194 | call([exkifint_v3_exe, archive, game_main], stdin=None, stdout=None, stderr=None, shell=False) 195 | clean_files_from_dir(dir_path_extracted, ".int") 196 | delete_file(dir_path_extracted + exkifint_v3_exe) 197 | chdir(dir_path) 198 | 199 | 200 | def sort_resulting_files(): 201 | print(messages[5]) 202 | chdir(dir_path_extracted) 203 | 204 | for filename in listdir(dir_path_extracted): 205 | file = dir_path_extracted + filename 206 | if path.isfile(file): 207 | if file.endswith(".anm"): 208 | move(file, dir_path_extracted_animations + filename) 209 | if file.endswith(".hg2") or file.endswith(".hg3") or file.endswith(".bmp") or file.endswith(".jpg"): 210 | move(file, dir_path_extracted_images + filename) 211 | if file.endswith(".mpg"): 212 | move(file, dir_path_extracted_movies + filename) 213 | if file.endswith(".fes") or file.endswith(".kcs") or file.endswith(".dat") or file.endswith(".xml"): 214 | move(file, dir_path_extracted_scripts + filename) 215 | if file.endswith(".ogg") or file.endswith(".wav"): 216 | move(file, dir_path_extracted_sounds + filename) 217 | if file.endswith(".cst") or file.endswith(".txt"): 218 | move(file, dir_path_extracted_texts + filename) 219 | if file.endswith(".cstl"): 220 | move(file, dir_path_extracted_localization_texts + filename) 221 | chdir(dir_path) 222 | 223 | 224 | def process_nametable(): 225 | chdir(dir_path_extracted) 226 | nametable_csv = dir_path_extracted + 'nametable.csv' 227 | nametable_xlsx = dir_path_translate_here + 'nametable.xlsx' 228 | if not path.exists(nametable_csv) or not path.isfile(nametable_csv): 229 | print("\nFile 'nametable.csv' was not found during unpacking.\n" 230 | "Step '2) apply name translations' will not be functional.\n" 231 | "Translate all names in text files manually.\n") 232 | press_any_key() 233 | else: 234 | # if nametable.csv exists 235 | text_names = [] 236 | translates_to = [] 237 | write_name_here = [] 238 | translated_names = False 239 | if path.exists(nametable_xlsx) and path.isfile(nametable_xlsx): 240 | # if it exists - save translations column from it 241 | xlsx_file = pd.ExcelFile(nametable_xlsx, engine='openpyxl') 242 | df1 = xlsx_file.parse(xlsx_file.sheet_names[0]) 243 | write_name_here = list(df1[df1.columns[2]]).copy() 244 | translated_names = True 245 | df0 = None 246 | encodings = ["ShiftJIS", "utf-8"] 247 | for encoding in encodings: 248 | try: 249 | df0 = read_csv(nametable_csv, encoding=encoding) 250 | except UnicodeDecodeError: 251 | pass 252 | else: 253 | break 254 | 255 | if df0 is not None: 256 | # we need to process first line of CSV separately, 257 | # since lib thinks it's "table headers" while it might be not, it might be data right away 258 | if '\t' in df0.columns[0]: 259 | text_names.append(df0.columns[0].split('\t')[1].replace("\\fs \\fn", " ")) 260 | translates_to.append("will be translated as:") 261 | if not translated_names: 262 | write_name_here.append("(translate name here)") 263 | # processing other CSV lines 264 | for line in list(df0.values): 265 | if '\t' in line[0]: 266 | name = line[0].split('\t')[1].strip("【】").replace("\\fs \\fn", " ") 267 | if name not in text_names: 268 | text_names.append(name) 269 | translates_to.append("will be translated as:") 270 | if not translated_names: 271 | write_name_here.append("(translate name here)") 272 | df = DataFrame({"Names": text_names, "will be shown as": translates_to, "New names:": write_name_here}) 273 | writer = ExcelWriter(nametable_xlsx, engine='xlsxwriter') 274 | pd.set_option('display.max_colwidth', None) 275 | df.to_excel(writer, sheet_name='sheetName', index=False, na_rep='NaN') 276 | writer.sheets['sheetName'].set_column(0, 0, 20) 277 | writer.sheets['sheetName'].set_column(1, 1, 20) 278 | writer.sheets['sheetName'].set_column(2, 2, 20) 279 | writer.close() 280 | else: 281 | print("Can not understand encoding of 'nametable.csv'.\n" 282 | "Step '2) apply name translations' will not be functional.\n" 283 | "Translate all names in text files manually.") 284 | chdir(dir_path) 285 | 286 | 287 | def extract_zt_archives(): 288 | chdir(dir_path_extracted) 289 | copy(dir_path_tools + exzt_exe, dir_path_extracted + exzt_exe) 290 | copy(dir_path_tools + zlib1_dll, dir_path_extracted + zlib1_dll) 291 | # unpacking from int archives 292 | if path.exists(dir_path_extracted + exzt_exe) \ 293 | and path.exists(dir_path_extracted + zlib1_dll): 294 | for archive in listdir(dir_path_extracted): 295 | if archive.endswith(".zt"): 296 | archive_ini = dir_path_extracted + archive 297 | if path.exists(archive_ini): 298 | print(str.format(messages[3], archive)) 299 | call([exzt_exe, archive], stdin=None, stdout=None, stderr=None, shell=False) 300 | clean_files_from_dir(dir_path_extracted, ".zt") 301 | delete_file(dir_path_extracted + exzt_exe) 302 | delete_file(dir_path_extracted + zlib1_dll) 303 | chdir(dir_path) 304 | 305 | 306 | def unpack_scripts(): 307 | chdir(dir_path_extracted_scripts) 308 | copy(dir_path_tools + cs2_decompile_exe, dir_path_extracted_scripts + cs2_decompile_exe) 309 | if path.exists(dir_path_extracted_scripts + cs2_decompile_exe): 310 | for filename in listdir(dir_path_extracted_scripts): 311 | file = dir_path_extracted_scripts + filename 312 | if file.endswith(".fes"): 313 | print(str.format(messages[3], filename)) 314 | call([cs2_decompile_exe, filename], stdin=None, stdout=None, stderr=None, shell=False) 315 | delete_file(dir_path_extracted_scripts + cs2_decompile_exe) 316 | chdir(dir_path) 317 | 318 | 319 | def unpack_images(): 320 | chdir(dir_path_extracted_images) 321 | # unpacking .hg2 and .hg3 files to .bmp files 322 | copy(dir_path_tools + hgx2bmp_exe, dir_path_extracted_images + hgx2bmp_exe) 323 | copy(dir_path_tools + zlib1_dll, dir_path_extracted_images + zlib1_dll) 324 | if path.exists(dir_path_extracted_images + hgx2bmp_exe): 325 | for filename in listdir(dir_path_extracted_images): 326 | file = dir_path_extracted_images + filename 327 | if file.endswith(".hg2") or file.endswith(".hg3"): 328 | print(str.format(messages[3], filename)) 329 | call([hgx2bmp_exe, filename], stdin=None, stdout=None, stderr=None, shell=False) 330 | clean_files_from_dir(dir_path_extracted_images, ".hg2") 331 | clean_files_from_dir(dir_path_extracted_images, ".hg3") 332 | delete_file(dir_path_extracted_images + hgx2bmp_exe) 333 | delete_file(dir_path_extracted_images + zlib1_dll) 334 | chdir(dir_path) 335 | 336 | def excst(f): 337 | fs = open(f, 'rb') 338 | fs.seek(8) 339 | raw_size, ori_size = struct.unpack('II',fs.read(8)) 340 | raw=fs.read() 341 | if len(raw)!=raw_size: 342 | raise Exception('size error! ' + str(len(raw)) + " | " + str(raw_size) + " | " + str(ori_size)) 343 | ori=zlib.decompress(raw) 344 | if len(ori)!=ori_size: 345 | raise Exception('size error2! ' + str(len(ori)) + " | " + str(ori_size)) 346 | fs.close() 347 | fs1 = open(f.replace('.cst', '.txt'), 'wb') 348 | fs1.write(ori) 349 | fs1.close() 350 | 351 | 352 | def unpack_texts(): 353 | chdir(dir_path_extracted_texts) 354 | # unpacking .cst files to .out files 355 | copy(dir_path_tools + cs2_decompile_exe, dir_path_extracted_texts + cs2_decompile_exe) 356 | if path.exists(dir_path_extracted_texts + cs2_decompile_exe): 357 | for filename in listdir(dir_path_extracted_texts): 358 | file = dir_path_extracted_texts + filename 359 | if file.endswith(".cst"): 360 | call([cs2_decompile_exe, filename], stdin=None, stdout=None, stderr=None, shell=False) 361 | delete_file(dir_path_extracted_texts + cs2_decompile_exe) 362 | # extracting text lines from .txt files into .xlsx files 363 | extract_clean_text() 364 | chdir(dir_path) 365 | 366 | 367 | def extract_clean_text(): 368 | problematic_files = [] 369 | 370 | for filename in listdir(dir_path_extracted_texts): 371 | if path.isfile(filename) and filename.endswith(".txt"): 372 | full_filename_txt = dir_path_extracted_texts + filename 373 | print(str.format(messages[3], filename)) 374 | encoding = "ShiftJIS" 375 | if game_main == "ISLAND.exe": 376 | encoding = "ANSI" 377 | file_lines = [] 378 | text_lines = [] 379 | try: 380 | with copen(full_filename_txt, mode="rb", encoding=encoding) as file: 381 | file_lines = file.readlines() 382 | file.close() 383 | except UnicodeDecodeError as err: 384 | problematic_files.append(filename) 385 | continue 386 | for line in file_lines[1:]: 387 | if (line.endswith(TEXT_LINE_END1) 388 | or line.endswith(TEXT_LINE_END2) 389 | or ("[" in line 390 | and "]" in line 391 | and not line.startswith("\tbg") 392 | and not line.startswith("\tcg") 393 | and not line.startswith("\teg") 394 | and not line.startswith("\tfg") 395 | and not line.startswith("\tpl") 396 | and not line.startswith("\tpr")) 397 | or line.startswith(SCENE_LINESTART1) 398 | or line.startswith(SCENE_LINESTART2) 399 | or line.startswith(SCENE_LINESTART3)): 400 | if "\\r\\fn" not in line and not line == '\t\\fn\r\n': 401 | # text lines we need for translation 402 | text_lines.append(line) 403 | column1_ids = [] 404 | column2_names = [] 405 | column3_lines = [] 406 | column3_lines_old = [] 407 | file_xlsx = dir_path_translate_here_clean_texts + filename.replace(".txt", ".xlsx") 408 | if path.exists(file_xlsx) and path.isfile(file_xlsx): 409 | # if it exists - save translations column from it 410 | xlsx_file = pd.ExcelFile(file_xlsx, engine='openpyxl') 411 | df0 = xlsx_file.parse(xlsx_file.sheet_names[0]) 412 | column3_lines_old = list(df0[df0.columns[2]]).copy() 413 | for i in range(len(text_lines)): 414 | current_line = text_lines[i] 415 | if (current_line.endswith(TEXT_LINE_END1) 416 | or current_line.endswith(TEXT_LINE_END2) 417 | or ("[" in current_line 418 | and "]" in current_line 419 | and not current_line.startswith("\tbg") 420 | and not current_line.startswith("\tcg") 421 | and not current_line.startswith("\teg") 422 | and not current_line.startswith("\tfg") 423 | and not current_line.startswith("\tpl") 424 | and not current_line.startswith("\tpr"))): 425 | # if it's usual text line 426 | text_line_parts = current_line.split("\t") 427 | if len(text_line_parts) == 2: 428 | character_name = text_line_parts[0] 429 | if len(character_name) == 0: 430 | character_name = EMPTY_CHARACTER_NAME 431 | column2_names.append(character_name) 432 | column2_names.append(character_name) 433 | text_line = text_line_parts[1] 434 | # we need to remove "@" if present, 435 | # but only if it's at the end of the line, not to remove "@" from inside the main text 436 | column3_lines.append(text_line 437 | .replace(TEXT_LINE_END2, "") 438 | .replace("\r\n", "") 439 | .replace("\\fn", "") 440 | .replace("\\fl", "") 441 | .replace("\\fs", "") 442 | .replace("\\pc", "") 443 | .replace("\\pl", "")) 444 | if len(column3_lines_old) > 1: 445 | column3_lines_old.pop(0) 446 | column3_lines.append(column3_lines_old.pop(0)) 447 | else: 448 | column3_lines.append(WRITE_TRANSLATION_HERE) 449 | column1_ids.append(str.format(ORIGINAL_LINE_PATTERN, i)) 450 | column1_ids.append(str.format(TRANSLATION_LINE_PATTERN, i)) 451 | else: 452 | raise Exception("\n\n!!ERROR!!\n" 453 | "There are lines with more that one TAB symbol in 1 line!\n" 454 | "This is unexpected... " 455 | + "Please, contact developer on github with screenshot of this line:\n " 456 | + str(current_line.encode(encoding=encoding)) + "\n") 457 | 458 | elif (current_line.startswith(SCENE_LINESTART1) 459 | or current_line.startswith(SCENE_LINESTART2) 460 | or current_line.startswith(SCENE_LINESTART3)): 461 | # if it's scene title (naming) 462 | column2_names.append(SCENE_TITLE) 463 | column2_names.append(SCENE_TITLE) 464 | column3_lines.append(current_line 465 | .replace(SCENE_LINESTART1, "") 466 | .replace(SCENE_LINESTART2, "") 467 | .replace(SCENE_LINESTART3, "") 468 | .replace("\r\n", "")) 469 | if len(column3_lines_old) > 1: 470 | column3_lines_old.pop(0) 471 | column3_lines.append(column3_lines_old.pop(0)) 472 | else: 473 | column3_lines.append(WRITE_TRANSLATION_HERE) 474 | column1_ids.append(str.format(ORIGINAL_LINE_PATTERN, i)) 475 | column1_ids.append(str.format(TRANSLATION_LINE_PATTERN, i)) 476 | if len(column1_ids) > 0 and len(column2_names) > 0 and len(column3_lines) > 0: 477 | df = DataFrame({"Lines numbers": column1_ids, 478 | "Character name": column2_names, 479 | "Line text": column3_lines}) 480 | writer = ExcelWriter(file_xlsx, engine='xlsxwriter') 481 | df.to_excel(writer, sheet_name='sheetName', index=False, na_rep='NaN') 482 | writer.sheets['sheetName'].set_column(0, 0, 20) 483 | writer.sheets['sheetName'].set_column(1, 1, 15) 484 | writer.sheets['sheetName'].set_column(2, 2, 110) 485 | writer.close() 486 | if len(problematic_files) > 0: 487 | print(Fore.RED + "There were problems with reading these files:\n" + '\n'.join(problematic_files)) 488 | 489 | 490 | def extract_localized_texts(): 491 | foundLocFiles = False 492 | 493 | # for filename in listdir(dir_path_extracted_texts): 494 | # if filename.endswith(".cst"): 495 | # foundLocFiles = True 496 | # print(str.format(messages[3], filename)) 497 | # call([ 498 | # executable, 499 | # dir_path_tools + cstl_tool_zip, 500 | # "-b", dir_path_extracted_texts + filename, 501 | # "-t", "cstl", 502 | # "-o", dir_path_extracted_localization_texts + filename.replace(".cst", ".cstl"), 503 | # "--orig-lang", "en" 504 | # ], stdin=None, stdout=DEVNULL, stderr=None, shell=False) 505 | 506 | for filename in listdir(dir_path_extracted_localization_texts): 507 | if filename.endswith(".cstl"): 508 | foundLocFiles = True 509 | print(str.format(messages[3], filename)) 510 | call([ 511 | executable, 512 | dir_path_tools + cstl_tool_zip, 513 | "-d", dir_path_extracted_localization_texts + filename, 514 | "-o", dir_path_translate_here_clean_localization_texts + filename.replace(".cstl", ".ini") 515 | ], stdin=None, stdout=DEVNULL, stderr=None, shell=False) 516 | if foundLocFiles: 517 | print(Fore.YELLOW + "\nGame has localization files! Please translate using them and not via Excel tables.") 518 | 519 | 520 | def remove_temp_files(): 521 | print(messages[4]) 522 | for file in temp_archives: 523 | delete_file(dir_path_extracted + file) 524 | for file in temp_tools: 525 | delete_file(dir_path_extracted + file) 526 | 527 | 528 | def delete_file(path_to_file: str): 529 | if path.exists(path_to_file): 530 | chmod(path_to_file, 0o777) 531 | remove(path_to_file) 532 | 533 | 534 | def remove_empty_folders(): 535 | # also remove empty folders 536 | if path.exists(dir_path_extracted_manually): 537 | for pth in listdir(dir_path_extracted_manually): 538 | if path.isdir(dir_path_extracted_manually + pth): 539 | with scandir(dir_path_extracted_manually + pth) as it: 540 | if not any(it): 541 | rmdir(dir_path_extracted_manually + pth) 542 | 543 | 544 | def clean_files_from_dir(_dir: str, _filetype: str): 545 | for filename in listdir(_dir): 546 | file = _dir + filename 547 | if file.endswith(_filetype): 548 | delete_file(file) 549 | 550 | 551 | # core logic 552 | try: 553 | check_all_tools_intact() 554 | print(messages[0]) 555 | press_any_key() 556 | prepare_for_work() 557 | copy_files_into_extract_folder_and_extract() 558 | sort_resulting_files() 559 | process_nametable() 560 | 561 | unpack_texts() 562 | extract_localized_texts() 563 | unpack_scripts() 564 | #unpack_images() 565 | 566 | except Exception as error: 567 | print("ERROR - " + str("".join(format_exception(type(error), value=error, tb=error.__traceback__))).split( 568 | "The above exception was the direct cause of the following")[0]) 569 | finally: 570 | remove_temp_files() 571 | remove_empty_folders() 572 | print(messages[6]) # done 573 | -------------------------------------------------------------------------------- /tools/zlib1.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wolverator/CatSystem2-Simple-Translating-Tools/4321bc77d48d4fa353b79b135b534ee0d5b6daa1/tools/zlib1.dll --------------------------------------------------------------------------------