├── LICENSE ├── README.md ├── ai6win_mes.py ├── ai6win_mes_gui.py ├── ai6wincmd.py ├── library ├── silky_mes.py └── silky_mes_gui.py ├── main.py └── setup.py /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AI6WINScriptTool 2 | ## English 3 | Dual languaged (rus+eng) tool for disassembling and assembling scripts .mes from the visual novel's engine AI6WIN. Very incomplete list of games on this engine you can find [on vndb](https://vndb.org/v?q=&ch=&f=N18fwAI6WIN-). With it thou can fully edit all the code, not just strings. Thou can add message breaks and change scenarios without restrictions! 4 | Mes script files can be used not just in AI6WIN, but also in AI5WIN and Silky Engine. For assembling and disassembling mes script files of AI5WIN use [AI5WINScriptTool](https://github.com/TesterTesterov/AI5WINScriptTool) and for mes of Silky Engine use [mesScriptAsseAndDisassembler](https://github.com/TesterTesterov/mesScriptAsseAndDisassembler). 5 | 6 | Also you may want to pack and unpack archives of AI6WIN. For it use [AI6WINArcTool](https://github.com/TesterTesterov/AI6WINArcTool). 7 | 8 | Definations: "#0-" are "free bytes", "#1-" are commands (and "\[...]" are arguments below), "#2-" are labels. 9 | 10 | ## Русский 11 | Двуязычное (рус+англ) средство для дизассемблирования и ассемблирования скриптов .mes движка визуальных новелл AI6WIN. С неполным списком игр на нём вы можете ознакомиться [на vndb](https://vndb.org/v?q=&ch=&f=N18fwAI6WIN-). С ним вы можете полностью редактирвоать код, а не только строки; по вашему повелению добавлять разрывы между сообщений и даже менять сценарии по своему замыслу! 12 | Скрипты с расширением "mes" используются не только в AI6WIN, но также и в AI5WIN с Silky Engine. Чтобы дизассемблировать и ассемблировать скрипты движов AI5WIN и Silky Engine используйте иные следства -- [AI5WINScriptTool](https://github.com/TesterTesterov/AI5WINScriptTool) и [mesScriptAsseAndDisassembler](https://github.com/TesterTesterov/mesScriptAsseAndDisassembler) соответственно. 13 | 14 | Также вам может понадобиться распаковывать и паковать архивы движка AI6WIN. Для сего используйте средство [AI6WINArcTool](https://github.com/TesterTesterov/AI6WINArcTool). 15 | 16 | Определения: "#0-" есть "вольные байты", "#1-" есть команды (и под ними "\[...]" аргументы), "#2-" есть метки. 17 | 18 | # Usage / Использование 19 | ## English 20 | ![image](https://user-images.githubusercontent.com/66121918/151975923-a0c54881-8424-4c0f-8268-18cb75969adb.png) 21 | 1. Choose the mode, file or directory. In first mode you will work with one .mes - .txt pair, in second -- with all files in a pair of directories. 22 | 2. Enter a name of the .mes file in the top entry (do see, with extension) or the directory name. Thou can also enter relative or absolute path. You can also click on "..." to choose. 23 | 3. Enter a name of the .txt file (do see, with extension) or the directory name. Thou can also enter relative or absolute path. You can also click on "..." to choose. 24 | 4. Choose the version of script file (1 in most games or 0 in earliest like Aishimai 4). 25 | 5. For dissassemble push the button "Disassemble". 26 | 6. For assemble push the button "Assemble". 27 | 7. Status will be displayed on the text area below. 28 | 29 | ## Русский 30 | ![image](https://user-images.githubusercontent.com/66121918/151975831-45c8b865-a1ad-4ebb-b429-2b81113515c8.png) 31 | 1. Выберите режим: файл или директорию. В первом вы будете работать с парой .mes - .txt, во втором -- со всеми файлами в паре директорий. 32 | 2. Введите название файла .mes в верхней форме (заметьте, с расширением) или имя директории. Также можно вводить относительный или абсолютный до него путь. Также вы можете нажать на кнопку "...", чтобы выбрать. 33 | 3. Введите название файла .txt в нижней форме (заметьте, с расширением) или имя директории. Также можно вводить относительный или абсолютный до него путь. Также вы можете нажать на кнопку "...", чтобы выбрать. 34 | 4. Выберите версию скрипта (1, что используется в большинстве игр, или 0, что в самых ранних вроде Милых сестёр IV). 35 | 5. Для дизассемблирования нажмите на кнопку "Дизассемблировать". 36 | 6. Для ассемблирования нажмите на кнопку "Ассемблировать". 37 | 7. Статус сих операций будет отображаться на текстовом поле ниже. 38 | 39 | 40 | # Line and message breaks help / Помощь по организации переносов по строкам и сообщениям 41 | ## English 42 | Sometimes there could be a very big problem: text may not fully get in textbox. But with this tool thou don't need to cut some part of text, no. Thou can use line and message breaks. Methods are below. 43 | ### For line breaks insert this below the current message ("SomeString" -> text on the new line). 44 | ``` 45 | #1-ESCAPE 46 | [0] 47 | #1-STR_PRIMARY 48 | ["SomeString"] 49 | ``` 50 | ### For message breaks insert this below the current message ("SomeString" -> text on the new message). 51 | ``` 52 | #1-32 53 | [0, 3] 54 | #1-32 55 | [0, 23] 56 | #1-18 57 | [] 58 | #1-32 59 | [0, 4] 60 | #1-32 61 | [0, 0] 62 | #1-32 63 | [0, 31] 64 | #1-18 65 | [] 66 | #1-MESSAGE 67 | ["*MESSAGE_NUMBER*"] 68 | #1-STR_PRIMARY 69 | ["SomeString"] 70 | ``` 71 | 72 | ## Русский 73 | Иногда можно столкнуться с одной большой-пребольшой проблемой: текст может не полностью влезать в текстовое окно. Однако, с сим средством вам не нужно обрезать его, отнюдь. Вы можеет организовывать переносы по строкам и сообщениям. Методы указаны ниже. 74 | ### Для переносов по строкам добавьте под текущее сообщение следующий код ("Какая_то_строка" -> текст на новой строке). 75 | ``` 76 | #1-ESCAPE 77 | [0] 78 | #1-STR_PRIMARY 79 | ["Какая_то_строка"] 80 | ``` 81 | ### Для переносов по сообщениям добавьте под текущее сообщение следующий код ("Какая_то_строка" -> текст на новой строке). 82 | ``` 83 | #1-32 84 | [0, 3] 85 | #1-32 86 | [0, 23] 87 | #1-18 88 | [] 89 | #1-32 90 | [0, 4] 91 | #1-32 92 | [0, 0] 93 | #1-32 94 | [0, 31] 95 | #1-18 96 | [] 97 | #1-MESSAGE 98 | ["*MESSAGE_NUMBER*"] 99 | #1-STR_PRIMARY 100 | ["Какая_то_строка"] 101 | ``` 102 | 103 | 104 | # Tested on / Протестировано на 105 | ## English 106 | - [Gakuen Saimin Reido -Sakki made, Daikirai Datta Hazu na no ni-](https://vndb.org/v1601). 107 | - [Words Worth - Windows 10 Edition](https://vndb.org/v315). 108 | - [Biniku no Kaori \~Netori Netorare Yari Yarare\~](https://vndb.org/v470) by [Cosetto](https://github.com/Cosetto). 109 | - [Ai Shimai IV Kuyashikute Kimochi Yokatta Nante Ienai](https://vndb.org/v14826). 110 | - [Boku no Kanojo wa Gatenkei/Kanojo ga Shita Koto, Boku ga Sareta Koto/Kyonyuu Tsuma Kanzen Hokaku Keikaku/Boku no Tsuma ga Aitsu ni Netoraremashita.](https://vndb.org/v8731) by [Cosetto](https://github.com/Cosetto). 111 | 112 | ## Русский 113 | - [Рабыни гипноза в школе: А ведь недавно точно ненавидела](https://vndb.org/v1601). 114 | - [Значимость слов: Версия с поддержкой Windows 10](https://vndb.org/v315). 115 | - [Запах манящей плоти: Нэтори-нэторарэ яри-ярарэ](https://vndb.org/v470) протестировал [Cosetto](https://github.com/Cosetto). 116 | - [Милые сёстры IV: Я сожалению о содеянном; не могу же признать, что было приятно](https://vndb.org/v14826). 117 | - [Моя девушка занимается физическим трудом/Содеянное ею же в отместку получила/План полного подчинения большегрудой жены/Моя жена изменила с тем негодником](https://vndb.org/v8731) протестировал [Cosetto](https://github.com/Cosetto). 118 | -------------------------------------------------------------------------------- /ai6win_mes.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import json 3 | import os 4 | from library.silky_mes import SilkyMesScript, SilkyMesScriptError 5 | 6 | 7 | class AI6WINScript(SilkyMesScript): 8 | default_version = 1 9 | supported_versions = ( 10 | (0, "Aishimai 4 & earliest games"), 11 | (1, "Most games"), 12 | ) 13 | 14 | command_library = ( 15 | ( 16 | (0x00, '', 'YIELD'), 17 | (0x01, '', 'RETURN'), 18 | (0x02, '', 'LGDLOB1_I8'), 19 | (0x03, '', 'LGDLOB2_I16'), 20 | (0x04, '', 'LGDLOB3_VAR'), 21 | (0x05, '', 'LGDLOB4_VAR'), 22 | (0x06, '', 'LDLOC_VAR'), 23 | (0x07, '', 'LGDLOB5_I8'), 24 | (0x08, '', 'LGDLOB5_I16'), 25 | (0x09, '', 'LGDLOB5_I32'), 26 | (0x0A, 'S', 'STR_PRIMARY'), 27 | (0x0B, 'S', 'STR_SUPPLEMENT'), 28 | (0x0C, '', 'STGLOB1_I8'), 29 | (0x0D, '', 'STGLOB2_I16'), 30 | (0x0E, '', 'STGLOB3_VAR'), 31 | (0x0F, '', 'STGLOB4_VAR'), 32 | 33 | (0x10, '', 'STLOC_VAR'), 34 | (0x11, '', 'STGLOB5_I8'), 35 | (0x12, '', 'STGLOB6_I16'), 36 | (0x13, '', 'STGLOB7_I32'), 37 | (0x14, '>I', 'JUMP_IF_ZERO'), 38 | (0x15, '>I', 'JUMP'), 39 | (0x16, '>I', 'LIBREG'), 40 | (0x17, '', 'LIBCALL'), 41 | (0x18, '', 'SYSCALL'), 42 | (0x19, '>I', 'MESSAGE'), 43 | (0x1A, '>I', 'CHOICE'), 44 | (0x1B, 'B', 'ESCAPE'), 45 | (0x1D, '', ''), 46 | 47 | (0x32, '>i', 'PUSH_INT32'), 48 | (0x33, 'S', 'PUSH_STR'), 49 | (0x34, '', 'ADD'), 50 | (0x35, '', 'SUB'), 51 | (0x36, '', 'MUL'), 52 | (0x37, '', 'DIV'), 53 | (0x38, '', 'MOD'), 54 | (0x39, '', 'RAND'), 55 | (0x3A, '', 'LOGICAL_AND'), 56 | (0x3B, '', 'LOGICAL_OR'), 57 | (0x3C, '', 'BINARY_AND'), 58 | (0x3D, '', 'BINARY_OR'), 59 | (0x3E, '', 'LT'), 60 | (0x3F, '', 'GT'), 61 | 62 | (0x40, '', 'LE'), 63 | (0x41, '', 'GE'), 64 | (0x42, '', 'EQ'), 65 | (0x43, '', 'NEQ'), 66 | 67 | (0xFA, '', ''), 68 | (0xFB, '', ''), 69 | (0xFC, '', ''), 70 | (0xFD, '', ''), 71 | (0xFE, '', ''), 72 | (0xFF, '', ''), 73 | ), 74 | ( 75 | (0x00, '', 'YIELD'), 76 | (0x01, '', 'RETURN'), 77 | (0x02, '', 'LGDLOB1_I8'), 78 | (0x03, '', 'LGDLOB2_I16'), 79 | (0x04, '', 'LGDLOB3_VAR'), 80 | (0x05, '', 'LGDLOB4_VAR'), 81 | (0x06, '', 'LDLOC_VAR'), 82 | (0x07, '', 'LGDLOB5_I8'), 83 | (0x08, '', 'LGDLOB5_I16'), 84 | (0x09, '', 'LGDLOB5_I32'), 85 | (0x0A, 'S', 'STR_PRIMARY'), 86 | (0x0B, 'S', 'STR_SUPPLEMENT'), 87 | (0x0C, '', 'STGLOB1_I8'), 88 | (0x0D, '', 'STGLOB2_I16'), 89 | (0x0E, '', 'STGLOB3_VAR'), 90 | (0x0F, '', 'STGLOB4_VAR'), 91 | 92 | (0x10, 'B', 'STLOC_VAR'), 93 | (0x11, '', 'STGLOB5_I8'), 94 | (0x12, '', 'STGLOB6_I16'), 95 | (0x13, '', 'STGLOB7_I32'), 96 | (0x14, '>I', 'JUMP_IF_ZERO'), 97 | (0x15, '>I', 'JUMP'), 98 | (0x16, '>I', 'LIBREG'), 99 | (0x17, '', 'LIBCALL'), 100 | (0x18, '', 'SYSCALL'), 101 | (0x19, '>I', 'MESSAGE'), 102 | (0x1A, '>I', 'CHOICE'), 103 | (0x1B, 'B', 'ESCAPE'), 104 | (0x1D, '', ''), 105 | 106 | (0x32, '>i', 'PUSH_INT32'), 107 | (0x33, 'S', 'PUSH_STR'), 108 | (0x34, '', 'ADD'), 109 | (0x35, '', 'SUB'), 110 | (0x36, 'B', 'MUL'), 111 | (0x37, '', 'DIV'), 112 | (0x38, '', 'MOD'), 113 | (0x39, '', 'RAND'), 114 | (0x3A, '', 'LOGICAL_AND'), 115 | (0x3B, '', 'LOGICAL_OR'), 116 | (0x3C, '', 'BINARY_AND'), 117 | (0x3D, '', 'BINARY_OR'), 118 | (0x3E, '', 'LT'), 119 | (0x3F, '', 'GT'), 120 | 121 | (0x40, '', 'LE'), 122 | (0x41, '', 'GE'), 123 | (0x42, '', 'EQ'), 124 | (0x43, '', 'NEQ'), 125 | 126 | (0xFA, '', ''), 127 | (0xFB, '', ''), 128 | (0xFC, '', ''), 129 | (0xFD, '', ''), 130 | (0xFE, '', ''), 131 | (0xFF, '', ''), 132 | ) 133 | ) 134 | 135 | offsets_library = ( 136 | (0x14, 0), 137 | (0x15, 0), 138 | (0x16, 0), 139 | (0x1A, 0), 140 | ) 141 | 142 | def __init__(self, mes_name: str, txt_name: str, encoding: str = "", debug: bool = False, verbose: bool = False, 143 | hackerman_mode: bool = False, version=1): 144 | """Prms: 145 | mes_name -- name (and path) of mes script. 146 | txt_name -- name (and path) of txt file. 147 | encoding (not required) -- name of encoding. 148 | debug -- push debug data in the script. 149 | verbose -- print data about the class operations. 150 | hackerman_mode -- for true hackers; helps hacking unsupported mes scripts.""" 151 | 152 | super().__init__(mes_name, txt_name, encoding, debug, verbose, hackerman_mode) 153 | self.version = version 154 | 155 | # User methods. 156 | 157 | def disassemble(self) -> None: 158 | """Disassemble AI6WIN mes script.""" 159 | self._offsets = [] 160 | self._prm, self._first_offsets = self._diss_header() 161 | self._diss_other_offsets() 162 | if self._verbose: 163 | print("Parameters:", self._prm[0:1]) 164 | print("First offsets:", len(self._first_offsets), self._first_offsets) 165 | print("True offsets:", len(self._offsets), self._offsets) 166 | self._disassemble_commands() 167 | 168 | def assemble(self) -> None: 169 | """Assemble Silky Engine mes script.""" 170 | 171 | self._prm, self._first_offsets, self._offsets = self._assemble_offsets_and_parameters() 172 | 173 | if self._verbose: 174 | print("Parameters:", self._prm[0:1]) 175 | print("First offsets:", len(self._first_offsets), self._first_offsets) 176 | print("True offsets:", len(self._offsets), self._offsets) 177 | self._assemble_script_file() 178 | 179 | # Technical methods for assembling. 180 | 181 | def _assemble_script_file(self) -> None: 182 | """Assemble AI6WIN mes script.""" 183 | in_file = open(self._txt_name, 'r', encoding=self.encoding) 184 | try: 185 | os.rename(self._mes_name, self._mes_name + '.bak') 186 | except OSError: 187 | pass 188 | out_file = open(self._mes_name, 'wb') 189 | 190 | message_count = 0 191 | search_offset = [i[0] for i in self._offsets] 192 | 193 | out_file.write(struct.pack('I', self._prm[0])) 194 | for first_offset in self._first_offsets: 195 | out_file.write(struct.pack('I', first_offset)) 196 | 197 | while True: 198 | line = in_file.readline() 199 | if line == '': # EOF. 200 | break 201 | if len(line) == 1: # To evade some nasty errors. 202 | continue 203 | if (line == '\n') or (line[0] == '$'): 204 | continue 205 | if line[1] == '0': 206 | out_file.write(bytes.fromhex(line[2:-1])) 207 | elif line[1] == '1': 208 | command_string = line[3:-1] 209 | command_index = -1 210 | for num, lib_entry in enumerate(self.command_library[self.version]): # Check if it is written by name. 211 | if command_string == lib_entry[2]: 212 | command_index = num 213 | break 214 | if command_index == -1: # Check if it is written by hex. 215 | command_string = int(command_string, 16) 216 | for num, lib_entry in enumerate(self.command_library[self.version]): 217 | if command_string == lib_entry[0]: 218 | command_index = num 219 | break 220 | if command_index == -1: # There is no such command (text). But this should be impossible! 221 | raise AI6WINScriptError("Error! There is no such command.\n{}".format(command_string)) 222 | out_file.write(struct.pack('B', self.command_library[self.version][command_index][0])) 223 | 224 | line = in_file.readline() 225 | 226 | argument_list = json.loads(line) 227 | 228 | this_command = self.command_library[self.version][command_index][0] 229 | offset_set = -1 230 | if this_command == 0x19: 231 | argument_list[0] = message_count 232 | message_count += 1 233 | else: 234 | for offset_entry in self.offsets_library: 235 | if this_command == offset_entry[0]: 236 | offset_set = offset_entry[1] 237 | break 238 | 239 | if offset_set != -1: 240 | indexer = search_offset.index(argument_list[offset_set]) 241 | argument_list[offset_set] = self._offsets[indexer][1] 242 | 243 | argument_bytes = self.set_args(argument_list, self.command_library[self.version][command_index][1], self.encoding) 244 | out_file.write(argument_bytes) 245 | 246 | in_file.close() 247 | out_file.close() 248 | 249 | def _assemble_offsets_and_parameters(self) -> tuple: 250 | """Assemble offsets and parameters of AI6WIN mes archive.""" 251 | in_file = open(self._txt_name, 'r', encoding=self.encoding) 252 | 253 | first_offsets = [] 254 | offsets = [] 255 | prm = [0, 0] # First shall be changed. Second is to work with functions inherited from silky_mes. 256 | 257 | pointer = 0 258 | message_count = 0 259 | 260 | while True: 261 | line = in_file.readline() 262 | if line == '': # EOF. 263 | break 264 | if len(line) == 1: # To evade some nasty errors. 265 | continue 266 | if (line == '\n') or (line[0] == '$'): # Line without text or comment should not be parsed as script. 267 | continue 268 | 269 | # Actually code strings logic. 270 | 271 | if line[1] == '0': # "Free bytes". 272 | pointer += len(line[2:-1].split(' ')) 273 | elif line[1] == '1': # Command. 274 | command_string = line[3:-1] 275 | command_index = -1 276 | for num, lib_entry in enumerate(self.command_library[self.version]): # Check if it is written by name. 277 | if command_string == lib_entry[2]: 278 | command_index = num 279 | break 280 | if command_index == -1: # Check if it is written by hex. 281 | command_string = int(command_string, 16) 282 | for num, lib_entry in enumerate(self.command_library[self.version]): 283 | if command_string == lib_entry[0]: 284 | command_index = num 285 | break 286 | if command_index == -1: # There is no such command (text). But this should be impossible! 287 | raise AI6WINScriptError("Error! There is no such command.\n{}".format(command_string)) 288 | 289 | if self.command_library[self.version][command_index][0] == 0x19: # Since header save offsets to messages. 290 | message_count += 1 291 | first_offsets.append(pointer) 292 | 293 | pointer += 1 294 | 295 | # Okay, now is the time for getting arguments length! 296 | line = in_file.readline() 297 | argument_list = json.loads(line) 298 | if self.command_library[self.version][command_index][0] == 0x19: # For this to not cause any errors. 299 | argument_list[0] = 0 300 | argument_bytes = self.set_args(argument_list, self.command_library[self.version][command_index][1], self.encoding) 301 | pointer += len(argument_bytes) 302 | 303 | elif line[1] == '2': # If label (of true offset). 304 | offset_array = [] 305 | 306 | offset_number = int(line[3:-1]) 307 | offset_array.append(offset_number) 308 | offset_array.append(pointer) 309 | 310 | offsets.append(offset_array) 311 | in_file.close() 312 | 313 | prm[0] = message_count 314 | 315 | return prm, first_offsets, offsets 316 | 317 | # Technical methods for disassembling. 318 | 319 | def _disassemble_commands(self) -> None: 320 | """Disassemble Silky Engine mes script commands.""" 321 | commands = [] 322 | args = [] 323 | # [Opcode, struct, name]. 324 | pointer = self.get_true_offset(0) 325 | stringer = '' 326 | these_indices = [] 327 | 328 | out_file = open(self._txt_name, 'w', encoding=self.encoding) 329 | in_file = open(self._mes_name, 'rb') 330 | in_file.seek(pointer, 0) 331 | 332 | sorted_offset = sorted(list(enumerate(self._offsets)), key=lambda x: x[1]) 333 | # Sorted by offsets, but with index saving. 334 | search_offset = [i[1] for i in sorted_offset] 335 | initial_sorted_offset = sorted_offset.copy() 336 | initial_search_offset = search_offset.copy() 337 | # I know, you may say it's pointless, but that's for the sake of optimization. 338 | 339 | second_offsets = [self.get_true_offset(i) for i in self._second_offsets] 340 | 341 | while True: 342 | pointer = in_file.tell() # To get current position before the command. 343 | 344 | # Offsets functionality. 345 | # I did try my best to optimize it. It may be looked as bad, but... 346 | # I have managed to drastically decrease the number of iterations. 347 | # From some hundreds to about 1-2. 348 | these_indices.clear() 349 | speedy_crutch = -1 350 | for pos, offset in sorted_offset: 351 | speedy_crutch += 1 352 | if pointer == offset: 353 | these_indices.append(speedy_crutch) 354 | if self._debug: 355 | out_file.write("#2-{} {}\n".format(pos, pointer)) 356 | else: 357 | out_file.write("#2-{}\n".format(pos)) 358 | break 359 | elif pointer > offset: 360 | break 361 | for used in these_indices: 362 | sorted_offset.pop(used) 363 | search_offset.pop(used) 364 | for offset in second_offsets: # Should be fine since it is rare and not lengthy. 365 | if pointer == offset: 366 | if self._debug: 367 | out_file.write("#3 {}\n".format(pointer)) 368 | else: 369 | out_file.write("#3\n") 370 | break 371 | 372 | # Commands functionality. 373 | 374 | current_byte = in_file.read(1) 375 | if current_byte == b'': 376 | break 377 | current_byte = current_byte[0] 378 | args.append([]) 379 | commands.append(current_byte) 380 | analyzer = str(hex(current_byte))[2:] 381 | if (len(analyzer) == 1): 382 | analyzer = '0' + analyzer 383 | 384 | lib_index = -1 385 | for i in range(len(self.command_library[self.version])): 386 | if current_byte == self.command_library[self.version][i][0]: 387 | lib_index = i 388 | break 389 | if lib_index != -1: 390 | if stringer != '': 391 | stringer = stringer.lstrip(' ') 392 | stringer = '#0-{}\n'.format(stringer) 393 | out_file.write(stringer) 394 | stringer = '' 395 | 396 | out_file.write("#1-") 397 | if self.command_library[self.version][lib_index][2] == '': 398 | out_file.write(analyzer) 399 | else: 400 | if self.command_library[self.version][lib_index][2] == 'STR_CRYPT': 401 | out_file.write('STR_UNCRYPT') 402 | else: 403 | out_file.write(self.command_library[self.version][lib_index][2]) 404 | if self._debug: 405 | out_file.write(' {}\n'.format(pointer)) 406 | else: 407 | out_file.write('\n') 408 | 409 | arguments_list = self.get_args(in_file, self.command_library[self.version][lib_index][1], current_byte, self.encoding) 410 | 411 | what_index = -1 412 | for entry_pos, offsets_entry in enumerate(self.offsets_library): 413 | if current_byte == offsets_entry[0]: 414 | what_index = entry_pos 415 | 416 | if what_index != -1: 417 | first_indexer = self.offsets_library[what_index][1] 418 | evil_offset = self.get_true_offset(arguments_list[first_indexer]) 419 | indexer = initial_search_offset.index(evil_offset) 420 | arguments_list[first_indexer] = initial_sorted_offset[indexer][0] 421 | 422 | if self.command_library[self.version][lib_index][0] == 0x19: 423 | arguments_list[0] = "*MESSAGE_NUMBER*" 424 | json.dump(arguments_list, out_file, ensure_ascii=False) 425 | out_file.write('\n') 426 | 427 | else: 428 | stringer += ' ' + analyzer 429 | pointer += 1 430 | if stringer != '': # Print remaining free bytes. 431 | stringer = stringer.lstrip(' ') 432 | stringer = '#0-' + stringer 433 | out_file.write(stringer) 434 | 435 | out_file.close() 436 | 437 | def _diss_other_offsets(self) -> None: 438 | """Disassemble other offsets from the Silky Engine script.""" 439 | pointer = self.get_true_offset(0) 440 | in_file = open(self._mes_name, 'rb') 441 | in_file.seek(pointer, 0) 442 | 443 | if self._hackerman_mode: 444 | out_file = open("HACK.txt", 'w', encoding=self.encoding) 445 | 446 | while True: 447 | pointer = in_file.tell() 448 | current_byte = in_file.read(1) 449 | if current_byte == b'': 450 | break 451 | current_byte = current_byte[0] # Get int from byte in the fastest way possible. 452 | lib_index = -1 453 | for i in range(len(self.command_library[self.version])): 454 | if (current_byte == self.command_library[self.version][i][0]): 455 | lib_index = i 456 | break 457 | if lib_index != -1: 458 | arguments_list = self.get_args(in_file, self.command_library[self.version][lib_index][1], current_byte, 459 | self.encoding) 460 | 461 | if self._hackerman_mode: 462 | out_file.write("#1-{} {}\n".format(hex(current_byte), pointer)) 463 | out_file.write(str(arguments_list)) 464 | out_file.write("\n") 465 | 466 | what_index = -1 467 | for entry_pos, offsets_entry in enumerate(self.offsets_library): 468 | if current_byte == offsets_entry[0]: 469 | what_index = entry_pos 470 | if what_index != -1: 471 | not_here = True 472 | good_offset = self.get_true_offset(arguments_list[self.offsets_library[what_index][1]]) 473 | for i in range(len(self._offsets)): 474 | if good_offset == self._offsets[i]: 475 | not_here = False 476 | if not_here: 477 | self._offsets.append(good_offset) 478 | else: 479 | if self._hackerman_mode: 480 | out_file.write("#0-{} {}\n".format(hex(current_byte), pointer)) 481 | 482 | in_file.close() 483 | if self._hackerman_mode: 484 | out_file.close() 485 | 486 | def _diss_header(self) -> tuple: 487 | """Disassemble Silky Engine mes header.""" 488 | first_offsets = [] 489 | with open(self._mes_name, 'rb') as mes_file: 490 | prm = list(struct.unpack('I', mes_file.read(4))) 491 | for i in range(prm[0]): 492 | first_offsets.append(struct.unpack('I', mes_file.read(4))[0]) 493 | 494 | return prm, first_offsets 495 | 496 | # Offsets methods. 497 | 498 | def get_true_offset(self, raw_offset: int) -> int: 499 | """Get true offset (as it is factically in the file).""" 500 | return raw_offset + self._prm[0] * 4 + 4 501 | 502 | def set_true_offset(self, raw_offset): 503 | """Set true offset (as it is factically in the arguments).""" 504 | return raw_offset - self._prm[0] * 4 - 4 505 | 506 | # Disassembling methods. 507 | 508 | @staticmethod 509 | def get_args(in_file, args: str, current_byte: int, current_encoding: str) -> list: 510 | """Extract args from file.""" 511 | arguments_list = [] 512 | appendix = "" 513 | for argument in args: # self.command_library[lib_index][1] 514 | if argument in AI6WINScript.technical_instances: 515 | appendix = argument 516 | elif argument in AI6WINScript.get_I.instances: 517 | arguments_list.append(AI6WINScript.get_I(in_file, appendix + argument)) 518 | elif argument in AI6WINScript.get_H.instances: 519 | arguments_list.append(AI6WINScript.get_H(in_file, appendix + argument)) 520 | elif argument in AI6WINScript.get_B.instances: 521 | arguments_list.append(AI6WINScript.get_B(in_file, appendix + argument)) 522 | elif argument in AI6WINScript.get_S.instances: 523 | arguments_list.append(AI6WINScript.get_S(in_file, current_encoding)) 524 | return arguments_list 525 | 526 | @staticmethod 527 | def get_S(in_file, this_encoding: str) -> tuple: 528 | """Read string from file and it.""" 529 | string = b'' 530 | byte = in_file.read(1) 531 | while byte != b'\x00': 532 | string += byte 533 | byte = in_file.read(1) 534 | return string.decode(encoding=this_encoding) 535 | 536 | 537 | class AI6WINScriptError(SilkyMesScriptError): 538 | pass 539 | -------------------------------------------------------------------------------- /ai6win_mes_gui.py: -------------------------------------------------------------------------------- 1 | import os 2 | import threading 3 | import tkinter as tk 4 | import tkinter.ttk as ttk 5 | from tkinter.messagebox import showerror 6 | from library.silky_mes_gui import SilkyMesGUI 7 | from ai6win_mes import AI6WINScript 8 | 9 | 10 | class AI6WINMesGUI(SilkyMesGUI): 11 | _strings_lib = { 12 | "eng": ( 13 | "AI6WINScriptTool by Tester", # 0 14 | "Single file", 15 | "Directory", 16 | "Enter a name of the .mes file:", 17 | "Enter a name of the directory with .mes files:", 18 | "Enter a title of the .txt file:", # 5 19 | "Enter a name of the directory with .txt files:", 20 | "All files", 21 | "AI6WIN mes scripts", 22 | "Choice of mes script", 23 | "Choice of directory with mes scripts", # 10 24 | "Text files", 25 | "Choice of directory with txt files", 26 | "Choice of text file", 27 | "Commands:", 28 | "Status:", # 15 29 | "Help:", 30 | "Common help", 31 | "Usage help", 32 | "Breaks help", 33 | "Disassemble", # 20 34 | "Assemble", 35 | "Warning", 36 | "File mes or a directory of them is not chosen.", 37 | "File txt or a directory of them is not chosen.", 38 | "Managing files...", # 25 39 | "Error", 40 | "Disassembling failed. ", 41 | "Disassembling succeed. ", 42 | "Assembling failed. ", 43 | "Assembling succeed. ", # 30 44 | "Choose the script version:", 45 | ), 46 | "rus": ( 47 | "AI6WINScriptTool от Tester-а", # 0 48 | "По файлами", 49 | "По папкам", 50 | "Введите название файла .mes:", 51 | "Введите название директории с файлами .mes:", 52 | "Введите название файла .txt:", # 5 53 | "Введите название директории с файлами .txt:", 54 | "Все файлы", 55 | "Скрипты mes AI6WIN", 56 | "Выбор скрипта mes", 57 | "Выбор директории со скриптами mes", # 10 58 | "Текстовые файлы", 59 | "Выбор директории с файлами txt", 60 | "Выбор текстового файла", 61 | "Команды:", 62 | "Статус:", # 15 63 | "Справка:", 64 | "Общая справка", 65 | "Справка о использовании", 66 | "Справка о переносах", 67 | "Дизассемблировать", # 20 68 | "Ассемблировать", 69 | "Предупреждение", 70 | "Файл mes или их директория не выбраны", 71 | "Файл txt или их директория не выбраны", 72 | "Обрабатываем файлы...", # 25 73 | "Ошибка", 74 | "Дизассемблирование не удалось. ", 75 | "Дизассемблирование удалось. ", 76 | "Ассемблирование не удалось. ", 77 | "Ассемблирование удалось. ", # 30 78 | "Выберите версию скриптов:", 79 | ) 80 | } 81 | 82 | common_help = { 83 | 'eng': """ 84 | Dual languaged (rus+eng) tool for disassembling and assembling scripts .mes from the visual novel's engine AI6WIN. Very incomplete list of games on this engine you can find on vndb. With it thou can fully edit all thecode, not just strings. Thou can add message breaks and change scenarios without restrictions! Mes script files can be used not just in AI6WIN, but also in Silky Engine. For assembling and disassembling mes script files of Silky Engine use mesScriptAsseAndDisassembler. 85 | Also you may want to pack and unpack archives of AI6WIN. For it use AI6WINArcTool. 86 | Definations: "#0-" are "free bytes", "#1-" are commands (and "[...]" are arguments below), "#2-" are labels. 87 | """, 88 | 'rus': """ 89 | Двуязычное (рус+англ) средство для разборки и сборки скриптов .mes движка визуальных новелл AI6WIN. С неполным списком игр на нём вы можете ознакомиться на vndb. С ним вы можете полностью редактирвоать код, а не только строки; по вашему повелению добавлять разрывы между сообщений и даже менять сценарии по своему замыслу! Скрипты с расширением "mes" используются не только в AI6WIN, но также и в Silky Engine. Чтобы дизассемблировать и ассемблировать скрипты движка Silky Engine используйте иное средство -- mesScriptAsseAndDisassembler. 90 | Также вам может понадобиться распаковывать и паковать архивы движка AI6WIN. Для сего используйте средство AI6WINArcTool. 91 | Определения: "#0-" есть "вольные байты", "#1-" есть команды (и под ними "[...]" аргументы), "#2-" есть метки. 92 | """ 93 | } 94 | usage_help = { 95 | 'eng': """ 96 | 1. Choose the mode, file or directory. In first mode you will work with one .mes - .txt pair, in second -- with all files in a pair of directories. 97 | 2. Enter a name of the .mes file in the top entry (do see, with extension) or the directory name. Thou can also enter relative or absolute path. You can also click on "..." to choose. 98 | 3. Choose the version of script file (1 in most games or 0 in earliest like Aishimai 4). 99 | 4. Enter a name of the .txt file (do see, with extension) or the directory name. Thou can also enter relative or absolute path. You can also click on "..." to choose. 100 | 5. For dissassemble push the button "Disassemble script". 101 | 6. For assemble push the button "Assemble script". 102 | 7. Status will be displayed on the text area below. 103 | """, 104 | 'rus': """ 105 | 1. Выберите режим: файл или директорию. В первом вы будете работать с парой .mes - .txt, во втором -- со всеми файлами в паре директорий. 106 | 2. Введите название файла .mes в верхней форме (заметьте, с расширением) или имя директории. Также можно вводить относительный или абсолютный до него путь. Также вы можете нажать на кнопку "...", чтобы выбрать. 107 | 3. Введите название файла .txt в нижней форме (заметьте, с расширением) или имя директории. Также можно вводить относительный или абсолютный до него путь. Также вы можете нажать на кнопку "...", чтобы выбрать. 108 | 4. 109 | 5. Для разборки нажмите на кнопку "Дизассемблировать скрипт". 110 | 6. Для сборки нажмите на кнопку "Ассемблировать скрипт". 111 | 7. Статус сих операций будет отображаться на текстовом поле ниже. 112 | """, 113 | } 114 | 115 | breaks_help = { 116 | 'eng': """ 117 | Sometimes there could be a very big problem: text may not fully get in textbox. But with this tool thou don't need to cut some part of text, no. Thou can use line and message breaks. Methods are below. 118 | >>> For line breaks insert this below the current message ("SomeString" -> text on the new line). 119 | 120 | #1-ESCAPE 121 | [0] 122 | #1-STR_UNCRYPT 123 | ["SomeString"] 124 | 125 | >>> For message breaks insert this below the current message ("SomeString" -> text on the new message). 126 | 127 | #1-32 128 | [0, 3] 129 | #1-32 130 | [0, 23] 131 | #1-18 132 | [] 133 | #1-32 134 | [0, 4] 135 | #1-32 136 | [0, 0] 137 | #1-32 138 | [0, 31] 139 | #1-18 140 | [] 141 | #1-MESSAGE 142 | ["*MESSAGE_NUMBER*"] 143 | #1-STR_UNCRYPT 144 | ["SomeString"] 145 | """, 146 | 'rus': """ 147 | Иногда можно столкнуться с одной большой-пребольшой проблемой: текст может не полностью влезать в текстовое окно. Однако, с сим средством вам не нужно обрезать его, отнюдь. Вы можеет организовывать переносы по строкам и сообщениям. Методы указаны ниже. 148 | >>> Для переносов по строкам добавьте под текущее сообщение следующий код ("Какая_то_строка" -> текст на новой строке). 149 | 150 | #1-ESCAPE 151 | [0] 152 | #1-STR_UNCRYPT 153 | ["Какая_то_строка"] 154 | 155 | >>>Для переносов по сообщениям добавьте под текущее сообщение следующий код ("Какая_то_строка" -> текст на новой строке). 156 | 157 | #1-32 158 | [0, 3] 159 | #1-32 160 | [0, 23] 161 | #1-18 162 | [] 163 | #1-32 164 | [0, 4] 165 | #1-32 166 | [0, 0] 167 | #1-32 168 | [0, 31] 169 | #1-18 170 | [] 171 | #1-MESSAGE 172 | ["*MESSAGE_NUMBER*"] 173 | #1-STR_UNCRYPT 174 | ["Какая_то_строка"] 175 | """ 176 | } 177 | 178 | ver_sep = " - " 179 | 180 | def __init__(self, **kwargs): 181 | super().__init__(**kwargs) 182 | self._version_lbl = tk.Label(master=self._root, 183 | bg='white', 184 | font=('Helvetica', 12)) 185 | self._version_lbl.lang_index = 31 186 | 187 | self._version = tk.StringVar() 188 | 189 | possible_versions = [self.ver_sep.join(map(str, i)) for i in AI6WINScript.supported_versions] 190 | self._version_cmb = ttk.Combobox( 191 | master=self._root, 192 | font=('Helvetica', 12), 193 | textvariable=self._version, 194 | values=possible_versions, 195 | state="readonly", 196 | ) 197 | if possible_versions: 198 | self._version.set(possible_versions[AI6WINScript.default_version]) 199 | 200 | self.translate(self.init_language()) 201 | self.place_widgets(zlo=True) 202 | self.start_gui(zlo=True) 203 | 204 | def place_widgets(self, zlo=False) -> None: 205 | """Place widgets of the GUI.""" 206 | if not zlo: 207 | return 208 | # Top buttons. 209 | self._rus_btn.place(relx=0.0, rely=0.0, relwidth=0.5, relheight=0.05) 210 | self._eng_btn.place(relx=0.5, rely=0.0, relwidth=0.5, relheight=0.05) 211 | 212 | # Input/output files/dirs choosers widgets. 213 | 214 | for num, widget in enumerate(self._mode_rdb): 215 | widget.place(relx=0.5 * num, rely=0.05, relwidth=0.5, relheight=0.05) 216 | self._mes_point_lbl.place(relx=0.0, rely=0.1, relwidth=1.0, relheight=0.05) 217 | self._mes_name_ent.place(relx=0.0, rely=0.15, relwidth=0.9, relheight=0.05) 218 | self._mes_find_btn.place(relx=0.9, rely=0.15, relwidth=0.1, relheight=0.05) 219 | self._txt_point_lbl.place(relx=0.0, rely=0.2, relwidth=1.0, relheight=0.05) 220 | self._txt_name_ent.place(relx=0.0, rely=0.25, relwidth=0.9, relheight=0.05) 221 | self._txt_find_btn.place(relx=0.9, rely=0.25, relwidth=0.1, relheight=0.05) 222 | 223 | self._version_lbl.place(relx=0.0, rely=0.3, relwidth=1.0, relheight=0.05) 224 | self._version_cmb.place(relx=0.0, rely=0.35, relwidth=1.0, relheight=0.05) 225 | 226 | # Commands. 227 | 228 | for widget in self._action_btn: 229 | widget.pack(fill=tk.X) 230 | 231 | # Text area. 232 | 233 | self._status_txt.pack() 234 | 235 | # Help buttons. 236 | 237 | self._common_help_btn.pack(fill=tk.X) 238 | self._usage_help_btn.pack(fill=tk.X) 239 | self._breaks_help_btn.pack(fill=tk.X) 240 | 241 | # And finally label frames. 242 | 243 | self._commands_lfr.place(relx=0.0, rely=0.4, relwidth=1.0, relheight=0.2) 244 | self._status_lfr.place(relx=0.0, rely=0.6, relwidth=1.0, relheight=0.15) 245 | self._help_lfr.place(relx=0.0, rely=0.75, relwidth=1.0, relheight=0.25) 246 | 247 | def start_gui(self, zlo=False) -> None: 248 | """Start the GUI.""" 249 | # To make more space for patching. 250 | if zlo: 251 | self._root.mainloop() 252 | 253 | # Disassembling. 254 | 255 | def _disassemble_this_mes(self, mes_file: str, txt_file: str) -> None: 256 | """Disassemble this mes script.""" 257 | try: 258 | self._thread_semaphore.acquire() 259 | script_mes = AI6WINScript(mes_file, txt_file, version=int(self._version.get().split(self.ver_sep)[0])) 260 | script_mes.disassemble() 261 | self._status_lock.acquire() 262 | self._status_txt["state"] = tk.NORMAL 263 | self._status_txt.delete(1.0, tk.END) 264 | self._status_txt.insert(1.0, mes_file + ": ") 265 | self._status_txt.insert(2.0, self._strings_lib[self._language][28]) 266 | self._status_txt["state"] = tk.DISABLED 267 | self._status_lock.release() 268 | self._print_lock.acquire() 269 | print("Disassembling of {0} succeed./Дизассемблирование {0} прошло успешно.".format(mes_file)) 270 | self._print_lock.release() 271 | except Exception as ex: 272 | self._print_lock.acquire() 273 | print("Disassembling of {0} error./Дизассемблирование {0} не удалось.".format(mes_file)) 274 | self._print_lock.release() 275 | showerror(title=self._strings_lib[self._language][26], message=str(ex)) 276 | self._status_lock.acquire() 277 | self._status_txt["state"] = tk.NORMAL 278 | self._status_txt.delete(1.0, tk.END) 279 | self._status_txt.insert(1.0, mes_file + ": ") 280 | self._status_txt.insert(2.0, self._strings_lib[self._language][27]) 281 | self._status_txt["state"] = tk.DISABLED 282 | self._status_lock.release() 283 | finally: 284 | self._count_lock.acquire() 285 | self._unlocker_count -= 1 286 | self._count_lock.release() 287 | if self._unlocker_count == 0: 288 | self._unlock_activity() 289 | self._thread_semaphore.release() 290 | 291 | def _assemble(self) -> bool: 292 | """Assemble a mes script or a group of them from the text file or a group of them""" 293 | mes_file, txt_file, status = self._get_mes_and_txt() 294 | if not status: 295 | return False 296 | 297 | self._lock_activity() 298 | if self._input_mode.get() == 0: # File mode. 299 | self._unlocker_count = 1 300 | new_thread = threading.Thread(daemon=False, target=self._assemble_this_mes, 301 | args=(mes_file, txt_file)) 302 | new_thread.start() 303 | else: # Dir mode. 304 | files_to_manage = [] 305 | os.makedirs(txt_file, exist_ok=True) 306 | for root, dirs, files in os.walk(txt_file): 307 | for file_name in files: 308 | new_file_array = [] # mes_file, txt_file 309 | 310 | basic_path = os.sep.join(os.path.join(root, file_name).split(os.sep)[1:]) 311 | rel_mes_name = os.path.normpath(os.path.join(mes_file, os.path.splitext(basic_path)[0] + ".mes")) 312 | rel_txt_name = os.path.normpath(os.path.join(txt_file, basic_path)) 313 | 314 | new_file_array.append(rel_mes_name) 315 | new_file_array.append(rel_txt_name) 316 | files_to_manage.append(new_file_array) 317 | 318 | # Why did I not initiate file management right away, thou ask? 319 | 320 | self._unlocker_count = len(files_to_manage) # ...That is the answer. 321 | for file_mes, file_txt in files_to_manage: 322 | new_thread = threading.Thread(daemon=False, target=self._assemble_this_mes, 323 | args=(file_mes, file_txt)) 324 | new_thread.start() 325 | 326 | return True 327 | 328 | def _assemble_this_mes(self, mes_file: str, txt_file: str) -> None: 329 | """Assemble this mes script.""" 330 | try: 331 | self._thread_semaphore.acquire() 332 | script_mes = AI6WINScript(mes_file, txt_file, version=int(self._version.get().split(self.ver_sep)[0])) 333 | script_mes.assemble() 334 | self._status_lock.acquire() 335 | self._status_txt["state"] = tk.NORMAL 336 | self._status_txt.delete(1.0, tk.END) 337 | self._status_txt.insert(1.0, mes_file + ": ") 338 | self._status_txt.insert(2.0, self._strings_lib[self._language][30]) 339 | self._status_txt["state"] = tk.DISABLED 340 | self._status_lock.release() 341 | self._print_lock.acquire() 342 | print("Assembling of {0} succeed./Ассемблирование {0} прошло успешно.".format(mes_file)) 343 | self._print_lock.release() 344 | except Exception as ex: 345 | self._print_lock.acquire() 346 | print("Assembling of {0} error./Ассемблирование {0} не удалось.".format(mes_file)) 347 | self._print_lock.release() 348 | showerror(title=self._strings_lib[self._language][26], message=str(ex)) 349 | self._status_lock.acquire() 350 | self._status_txt["state"] = tk.NORMAL 351 | self._status_txt.delete(1.0, tk.END) 352 | self._status_txt.insert(1.0, mes_file + ": ") 353 | self._status_txt.insert(2.0, self._strings_lib[self._language][29]) 354 | self._status_txt["state"] = tk.DISABLED 355 | self._status_lock.release() 356 | finally: 357 | self._count_lock.acquire() 358 | self._unlocker_count -= 1 359 | self._count_lock.release() 360 | if self._unlocker_count == 0: 361 | self._unlock_activity() 362 | self._thread_semaphore.release() 363 | -------------------------------------------------------------------------------- /ai6wincmd.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | 4 | def diss(script_mes: str, file_txt: str, version: int = 1, encoding: str = 'cp932'): 5 | from ai6win_mes import AI6WINScript 6 | 7 | new_script = AI6WINScript(script_mes, file_txt, encoding=encoding, verbose=True, debug=False, version=version) 8 | new_script.disassemble() 9 | del new_script 10 | 11 | 12 | def ass(script_mes: str, file_txt: str, version: int = 1, encoding: str = 'cp932'): 13 | from ai6win_mes import AI6WINScript 14 | 15 | new_script = AI6WINScript(script_mes, file_txt, encoding=encoding, verbose=True, debug=False, version=version) 16 | new_script.assemble() 17 | del new_script 18 | 19 | 20 | parser = argparse.ArgumentParser(prog="AI6WINScriptTool", description="Assembler and disassembler of AI6WIN engine" 21 | "scripts") 22 | parser.add_argument('action', choices=["d", "a"], help="Action") # disassemble, assemble. 23 | parser.add_argument('mes_file', help="Mes file") 24 | parser.add_argument('txt_file', help="Txt file") 25 | parser.add_argument('-v', '--version', dest="version", 26 | choices=[0, 1], default=1, required=False, type=int, help="Mes version") 27 | parser.add_argument('-e', '--encoding', dest="encoding", default='cp932', required=False, help="Strings encoding") 28 | 29 | argdata = parser.parse_args() 30 | action = argdata.action 31 | mes_file = argdata.mes_file 32 | txt_file = argdata.txt_file 33 | version = argdata.version 34 | encoding = argdata.encoding 35 | if action == 'd': 36 | diss(mes_file, txt_file, version, encoding) 37 | else: 38 | ass(mes_file, txt_file, version, encoding) 39 | -------------------------------------------------------------------------------- /library/silky_mes.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import os 3 | import json 4 | 5 | 6 | class SilkyMesScript: 7 | default_encoding = "cp932" 8 | technical_instances = (">", "<") 9 | 10 | # [Opcode, struct, name]. 11 | command_library = ( 12 | (0x00, '', 'NULL'), 13 | (0x01, 'I', ''), # Found only in LIBLARY.LIB 14 | (0x02, '', ''), 15 | (0x03, '', ''), # Found only in LIBLARY.LIB 16 | (0x04, '', ''), 17 | (0x05, '', ''), 18 | (0x06, '', ''), # Found only in LIBLARY.LIB 19 | 20 | (0x0A, 'S', 'STR_CRYPT'), 21 | (0x0B, 'S', 'STR_UNCRYPT'), 22 | (0x0C, '', ''), 23 | (0x0D, '', ''), 24 | (0x0E, '', ''), 25 | (0x0F, '', ''), 26 | 27 | (0x10, 'B', ''), 28 | (0x11, '', ''), 29 | (0x14, '>I', 'JUMP'), 30 | (0x15, '>I', 'MSG_OFSETTER'), 31 | (0x16, '>I', 'SPEC_OFSETTER'), # Found only in LIBLARY.LIB 32 | (0x17, '', ''), 33 | (0x18, '', ''), 34 | (0x19, '>I', 'MESSAGE'), 35 | (0x1A, '>I', ''), 36 | (0x1B, '>I', ''), 37 | (0x1C, 'B', 'TO_NEW_STRING'), 38 | 39 | (0x32, '>hh', ''), 40 | (0x33, 'S', 'STR_RAW'), 41 | (0x34, '', ''), 42 | (0x35, '', ''), 43 | (0x36, 'B', 'JUMP_2'), 44 | (0x37, '', ''), 45 | (0x3A, '', ''), 46 | (0x3B, '', ''), 47 | (0x3C, '', ''), 48 | (0x3D, '', ''), 49 | (0x3E, '', ''), 50 | 51 | (0x42, '', ''), 52 | (0x43, '', ''), 53 | 54 | (0xFA, '', ''), 55 | (0xFB, '', ''), 56 | (0xFC, '', ''), 57 | (0xFD, '', ''), 58 | (0xFE, '', ''), 59 | (0xFF, '', ''), 60 | ) 61 | offsets_library = ( 62 | (0x14, 0), 63 | (0x15, 0), 64 | (0x16, 0), 65 | (0x1b, 0), 66 | ) 67 | 68 | def __init__(self, mes_name: str, txt_name: str, encoding: str = "", debug: bool = False, verbose: bool = False, 69 | hackerman_mode: bool = False): 70 | """Prms: 71 | mes_name -- name (and path) of mes script. 72 | txt_name -- name (and path) of txt file. 73 | encoding (not required) -- name of encoding. 74 | debug -- push debug data in the script. 75 | verbose -- print data about the class operations. 76 | hackerman_mode -- for true hackers; helps hacking unsupported mes scripts.""" 77 | self._verbose = verbose 78 | if encoding == "": 79 | self.encoding = self.default_encoding 80 | else: 81 | self.encoding = encoding 82 | self._hackerman_mode = hackerman_mode 83 | self._debug = debug # For printing debug info, such as commands offsets. 84 | self._mes_name = mes_name # Name of mes script file (with path). 85 | self._txt_name = txt_name # Name of txt file (with path). 86 | self._prm = [0, 0] # Parameters of mes script. # [Header entries number, padding???] 87 | self._offsets = [] # Offsets of mes script. 88 | self._first_offsets = [] # Offsets in the first section of header of mes script. 89 | self._second_offsets = [] # Offsets in the second section of header of mes script. 90 | 91 | self.get_I.instances = ("I", "i") 92 | self.get_H.instances = ("H", "h") 93 | self.get_B.instances = ("B", "b") 94 | self.get_S.instances = ("S",) 95 | self.set_I.instances = self.get_I.instances 96 | self.set_H.instances = self.get_H.instances 97 | self.set_B.instances = self.get_B.instances 98 | self.set_S.instances = self.get_S.instances 99 | 100 | # User methods. 101 | 102 | def disassemble(self) -> None: 103 | """Disassemble Silky Engine mes script.""" 104 | self._offsets = [] 105 | self._prm, self._first_offsets, self._second_offsets = self._diss_header() 106 | self._diss_other_offsets() 107 | if self._verbose: 108 | print("Parameters:", self._prm) 109 | print("First offsets:", len(self._first_offsets), self._first_offsets) 110 | print("Second offsets:", len(self._second_offsets), self._second_offsets) 111 | print("True offsets:", len(self._offsets), self._offsets) 112 | self._disassemble_commands() 113 | 114 | def assemble(self) -> None: 115 | """Assemble Silky Engine mes script.""" 116 | 117 | self._prm, self._first_offsets, self._second_offsets, self._offsets = self._assemble_offsets_and_parameters() 118 | 119 | if self._verbose: 120 | print("Parameters:", self._prm) 121 | print("First offsets:", self._first_offsets) 122 | print("True offsets:", self._offsets) 123 | self._assemble_script_file() 124 | 125 | # Technical methods for assembling. 126 | 127 | def _assemble_script_file(self) -> None: 128 | in_file = open(self._txt_name, 'r', encoding=self.encoding) 129 | try: 130 | os.rename(self._mes_name, self._mes_name + '.bak') 131 | except OSError: 132 | pass 133 | out_file = open(self._mes_name, 'wb') 134 | 135 | message_count = 0 136 | search_offset = [i[0] for i in self._offsets] 137 | 138 | for parameter in self._prm: 139 | out_file.write(struct.pack('I', parameter)) 140 | for first_offset in self._first_offsets: 141 | out_file.write(struct.pack('I', first_offset)) 142 | for second_offset in self._second_offsets: 143 | out_file.write(struct.pack('I', second_offset)) 144 | 145 | while True: 146 | line = in_file.readline() 147 | if line == '': # EOF. 148 | break 149 | if len(line) == 1: # To evade some nasty errors. 150 | continue 151 | if (line == '\n') or (line[0] == '$'): 152 | continue 153 | if line[1] == '0': 154 | out_file.write(bytes.fromhex(line[2:-1])) 155 | elif line[1] == '1': 156 | command_string = line[3:-1] 157 | command_index = -1 158 | for num, lib_entry in enumerate(self.command_library): # Check if it is written by name. 159 | if command_string == lib_entry[2]: 160 | command_index = num 161 | break 162 | if command_index == -1: # Check if it is written by hex. 163 | command_string = int(command_string, 16) 164 | for num, lib_entry in enumerate(self.command_library): 165 | if command_string == lib_entry[0]: 166 | command_index = num 167 | break 168 | if command_index == -1: # There is no such command (text). But this should be impossible! 169 | raise SilkyMesScriptError("Error! There is no such command.\n{}".format(command_string)) 170 | out_file.write(struct.pack('B', self.command_library[command_index][0])) 171 | 172 | line = in_file.readline() 173 | 174 | argument_list = json.loads(line) 175 | 176 | this_command = self.command_library[command_index][0] 177 | offset_set = -1 178 | if this_command == 0x19: 179 | argument_list[0] = message_count 180 | message_count += 1 181 | else: 182 | for offset_entry in self.offsets_library: 183 | if this_command == offset_entry[0]: 184 | offset_set = offset_entry[1] 185 | break 186 | 187 | if offset_set != -1: 188 | indexer = search_offset.index(argument_list[offset_set]) 189 | argument_list[offset_set] = self._offsets[indexer][1] 190 | 191 | argument_bytes = self.set_args(argument_list, self.command_library[command_index][1], self.encoding) 192 | out_file.write(argument_bytes) 193 | 194 | in_file.close() 195 | out_file.close() 196 | 197 | def _assemble_offsets_and_parameters(self) -> tuple: 198 | """Assemble offsets and parameters of Silky Engine's mes archive.""" 199 | in_file = open(self._txt_name, 'r', encoding=self.encoding) 200 | 201 | first_offsets = [] 202 | second_offsets = [] 203 | offsets = [] 204 | prm = [0, 0] # First shall be changed. 205 | 206 | pointer = 0 207 | message_count = 0 208 | 209 | while True: 210 | line = in_file.readline() 211 | if line == '': # EOF. 212 | break 213 | if len(line) == 1: # To evade some nasty errors. 214 | continue 215 | if (line == '\n') or (line[0] == '$'): # Line without text or comment should not be parsed as script. 216 | continue 217 | 218 | # Actually code strings logic. 219 | 220 | if line[1] == '0': # "Free bytes". 221 | pointer += len(line[2:-1].split(' ')) 222 | elif line[1] == '1': # Command. 223 | command_string = line[3:-1] 224 | command_index = -1 225 | for num, lib_entry in enumerate(self.command_library): # Check if it is written by name. 226 | if command_string == lib_entry[2]: 227 | command_index = num 228 | break 229 | if command_index == -1: # Check if it is written by hex. 230 | command_string = int(command_string, 16) 231 | for num, lib_entry in enumerate(self.command_library): 232 | if command_string == lib_entry[0]: 233 | command_index = num 234 | break 235 | if command_index == -1: # There is no such command (text). But this should be impossible! 236 | raise SilkyMesScriptError("Error! There is no such command.\n{}".format(command_string)) 237 | 238 | if self.command_library[command_index][0] == 0x19: # Since header save offsets to messages. 239 | message_count += 1 240 | first_offsets.append(pointer) 241 | 242 | pointer += 1 243 | 244 | # Okay, now is the time for getting arguments length! 245 | line = in_file.readline() 246 | argument_list = json.loads(line) 247 | if self.command_library[command_index][0] == 0x19: # For this to not cause any errors. 248 | argument_list[0] = 0 249 | argument_bytes = self.set_args(argument_list, self.command_library[command_index][1], self.encoding) 250 | pointer += len(argument_bytes) 251 | 252 | elif line[1] == '2': # If label (of true offset). 253 | offset_array = [] 254 | 255 | offset_number = int(line[3:-1]) 256 | offset_array.append(offset_number) 257 | offset_array.append(pointer) 258 | 259 | offsets.append(offset_array) 260 | 261 | elif line[1] == '3': # If special header's label. 262 | second_offsets.append(pointer) 263 | in_file.close() 264 | 265 | prm[0] = message_count 266 | prm[1] = len(second_offsets) 267 | 268 | return prm, first_offsets, second_offsets, offsets 269 | 270 | # Technical methods for disassembling. 271 | 272 | def _disassemble_commands(self) -> None: 273 | """Disassemble Silky Engine mes script commands.""" 274 | commands = [] 275 | args = [] 276 | # [Opcode, struct, name]. 277 | pointer = self.get_true_offset(0) 278 | stringer = '' 279 | these_indices = [] 280 | 281 | out_file = open(self._txt_name, 'w', encoding=self.encoding) 282 | in_file = open(self._mes_name, 'rb') 283 | in_file.seek(pointer, 0) 284 | 285 | sorted_offset = sorted(list(enumerate(self._offsets)), key=lambda x: x[1]) 286 | # Sorted by offsets, but with index saving. 287 | search_offset = [i[1] for i in sorted_offset] 288 | initial_sorted_offset = sorted_offset.copy() 289 | initial_search_offset = search_offset.copy() 290 | # I know, you may say it's pointless, but that's for the sake of optimization. 291 | 292 | second_offsets = [self.get_true_offset(i) for i in self._second_offsets] 293 | 294 | while True: 295 | pointer = in_file.tell() # To get current position before the command. 296 | 297 | # Offsets functionality. 298 | # I did try my best to optimize it. It may be looked as bad, but... 299 | # I have managed to drastically decrease the number of iterations. 300 | # From some hundreds to about 1-2. 301 | these_indices.clear() 302 | speedy_crutch = -1 303 | for pos, offset in sorted_offset: 304 | speedy_crutch += 1 305 | if pointer == offset: 306 | these_indices.append(speedy_crutch) 307 | if self._debug: 308 | out_file.write("#2-{} {}\n".format(pos, pointer)) 309 | else: 310 | out_file.write("#2-{}\n".format(pos)) 311 | break 312 | elif pointer > offset: 313 | break 314 | for used in these_indices: 315 | sorted_offset.pop(used) 316 | search_offset.pop(used) 317 | for offset in second_offsets: # Should be fine since it is rare and not lengthy. 318 | if pointer == offset: 319 | if self._debug: 320 | out_file.write("#3 {}\n".format(pointer)) 321 | else: 322 | out_file.write("#3\n") 323 | break 324 | 325 | # Commands functionality. 326 | 327 | current_byte = in_file.read(1) 328 | if current_byte == b'': 329 | break 330 | current_byte = current_byte[0] 331 | args.append([]) 332 | commands.append(current_byte) 333 | analyzer = str(hex(current_byte))[2:] 334 | if (len(analyzer) == 1): 335 | analyzer = '0' + analyzer 336 | 337 | lib_index = -1 338 | for i in range(len(self.command_library)): 339 | if current_byte == self.command_library[i][0]: 340 | lib_index = i 341 | break 342 | if lib_index != -1: 343 | if stringer != '': 344 | stringer = stringer.lstrip(' ') 345 | stringer = '#0-{}\n'.format(stringer) 346 | out_file.write(stringer) 347 | stringer = '' 348 | 349 | out_file.write("#1-") 350 | if self.command_library[lib_index][2] == '': 351 | out_file.write(analyzer) 352 | else: 353 | if self.command_library[lib_index][2] == 'STR_CRYPT': 354 | out_file.write('STR_UNCRYPT') 355 | else: 356 | out_file.write(self.command_library[lib_index][2]) 357 | if self._debug: 358 | out_file.write(' {}\n'.format(pointer)) 359 | else: 360 | out_file.write('\n') 361 | 362 | arguments_list = self.get_args(in_file, self.command_library[lib_index][1], current_byte, self.encoding) 363 | 364 | what_index = -1 365 | for entry_pos, offsets_entry in enumerate(self.offsets_library): 366 | if current_byte == offsets_entry[0]: 367 | what_index = entry_pos 368 | 369 | if what_index != -1: 370 | first_indexer = self.offsets_library[what_index][1] 371 | evil_offset = self.get_true_offset(arguments_list[first_indexer]) 372 | indexer = initial_search_offset.index(evil_offset) 373 | arguments_list[first_indexer] = initial_sorted_offset[indexer][0] 374 | 375 | if self.command_library[lib_index][0] == 0x19: 376 | arguments_list[0] = "*MESSAGE_NUMBER*" 377 | json.dump(arguments_list, out_file, ensure_ascii=False) 378 | out_file.write('\n') 379 | 380 | else: 381 | stringer += ' ' + analyzer 382 | pointer += 1 383 | if stringer != '': # Print remaining free bytes. 384 | stringer = stringer.lstrip(' ') 385 | stringer = '#0-' + stringer 386 | out_file.write(stringer) 387 | 388 | out_file.close() 389 | 390 | def _diss_other_offsets(self) -> None: 391 | """Disassemble other offsets from the Silky Engine script.""" 392 | pointer = self.get_true_offset(0) 393 | in_file = open(self._mes_name, 'rb') 394 | in_file.seek(pointer, 0) 395 | 396 | if self._hackerman_mode: 397 | out_file = open("HACK.txt", 'w', encoding=self.encoding) 398 | 399 | while True: 400 | pointer = in_file.tell() 401 | current_byte = in_file.read(1) 402 | if current_byte == b'': 403 | break 404 | current_byte = current_byte[0] # Get int from byte in the fastest way possible. 405 | lib_index = -1 406 | for i in range(len(self.command_library)): 407 | if (current_byte == self.command_library[i][0]): 408 | lib_index = i 409 | break 410 | if lib_index != -1: 411 | arguments_list = self.get_args(in_file, self.command_library[lib_index][1], current_byte, 412 | self.encoding) 413 | 414 | if self._hackerman_mode: 415 | out_file.write("#1-{} {}\n".format(hex(current_byte), pointer)) 416 | out_file.write(str(arguments_list)) 417 | out_file.write("\n") 418 | 419 | what_index = -1 420 | for entry_pos, offsets_entry in enumerate(self.offsets_library): 421 | if current_byte == offsets_entry[0]: 422 | what_index = entry_pos 423 | if what_index != -1: 424 | not_here = True 425 | good_offset = self.get_true_offset(arguments_list[self.offsets_library[what_index][1]]) 426 | for i in range(len(self._offsets)): 427 | if good_offset == self._offsets[i]: 428 | not_here = False 429 | if not_here: 430 | self._offsets.append(good_offset) 431 | else: 432 | if self._hackerman_mode: 433 | out_file.write("#0-{} {}\n".format(hex(current_byte), pointer)) 434 | 435 | in_file.close() 436 | if self._hackerman_mode: 437 | out_file.close() 438 | 439 | def _diss_header(self) -> tuple: 440 | """Disassemble Silky Engine mes header.""" 441 | first_offsets = [] 442 | second_offsets = [] 443 | with open(self._mes_name, 'rb') as mes_file: 444 | prm = list(struct.unpack('II', mes_file.read(8))) 445 | for i in range(prm[0]): 446 | first_offsets.append(struct.unpack('I', mes_file.read(4))[0]) 447 | for i in range(prm[1]): 448 | second_offsets.append(struct.unpack('I', mes_file.read(4))[0]) 449 | 450 | return prm, first_offsets, second_offsets 451 | 452 | # Offsets methods. 453 | 454 | def get_true_offset(self, raw_offset: int) -> int: 455 | """Get true offset (as it is factically in the file).""" 456 | return raw_offset + self._prm[0] * 4 + self._prm[1] * 4 + 8 457 | 458 | def set_true_offset(self, raw_offset): 459 | """Set true offset (as it is factically in the arguments).""" 460 | return raw_offset - self._prm[0] * 4 - self._prm[1] * 4 - 8 461 | 462 | # Structure packing technicals methods. 463 | 464 | @staticmethod 465 | def set_args(argument_list, args: str, current_encoding: str) -> bytes: 466 | args_bytes = b'' 467 | appendix = "" 468 | current_argument = 0 469 | for argument in args: # self.command_library[lib_index][1] 470 | if argument in SilkyMesScript.technical_instances: 471 | appendix = argument 472 | continue 473 | 474 | # argument number changing. 475 | 476 | if argument in SilkyMesScript.set_I.instances: 477 | args_bytes += SilkyMesScript.set_I(argument_list[current_argument], appendix+argument) 478 | elif argument in SilkyMesScript.set_H.instances: 479 | args_bytes += SilkyMesScript.set_H(argument_list[current_argument], appendix+argument) 480 | elif argument in SilkyMesScript.set_B.instances: 481 | args_bytes += SilkyMesScript.set_B(argument_list[current_argument], appendix+argument) 482 | elif argument in SilkyMesScript.set_S.instances: 483 | args_bytes += SilkyMesScript.set_S(argument_list[current_argument], current_encoding) 484 | current_argument += 1 # Since argument may not change with new command. 485 | 486 | return args_bytes 487 | 488 | @staticmethod 489 | def set_B(arguments: int, command: str) -> bytes: 490 | return struct.pack(command, arguments) 491 | 492 | @staticmethod 493 | def set_H(arguments: int, command: str) -> bytes: 494 | return struct.pack(command, arguments) 495 | 496 | @staticmethod 497 | def set_I(arguments: int, command: str) -> bytes: 498 | return struct.pack(command, arguments) 499 | 500 | @staticmethod 501 | def set_S(arguments: str, encoding: str) -> bytes: 502 | arg_bytes = arguments.encode(encoding) + b'\x00' 503 | return arg_bytes 504 | 505 | # Structure extraction technical methods. 506 | 507 | @staticmethod 508 | def get_args(in_file, args: str, current_byte: int, current_encoding: str) -> list: 509 | arguments_list = [] 510 | appendix = "" 511 | for argument in args: # self.command_library[lib_index][1] 512 | if argument in SilkyMesScript.technical_instances: 513 | appendix = argument 514 | elif argument in SilkyMesScript.get_I.instances: 515 | arguments_list.append(SilkyMesScript.get_I(in_file, appendix+argument)) 516 | elif argument in SilkyMesScript.get_H.instances: 517 | arguments_list.append(SilkyMesScript.get_H(in_file, appendix+argument)) 518 | elif argument in SilkyMesScript.get_B.instances: 519 | arguments_list.append(SilkyMesScript.get_B(in_file, appendix+argument)) 520 | elif argument in SilkyMesScript.get_S.instances: 521 | leng, result = SilkyMesScript.get_S(current_byte, in_file, current_encoding) 522 | arguments_list.append(result) 523 | return arguments_list 524 | 525 | @staticmethod 526 | def get_B(file_in, definer: str) -> int: 527 | """Extract B/b structure.""" 528 | dummy = struct.unpack(definer, file_in.read(1))[0] 529 | return dummy 530 | 531 | @staticmethod 532 | def get_H(file_in, definer: str) -> int: 533 | """Extract H/h structure.""" 534 | dummy = struct.unpack(definer, file_in.read(2))[0] 535 | return dummy 536 | 537 | @staticmethod 538 | def get_I(file_in, definer: str) -> int: 539 | """Extract I/i structure.""" 540 | dummy = struct.unpack(definer, file_in.read(4))[0] 541 | return dummy 542 | 543 | @staticmethod 544 | def get_S(mode: int, in_file, encoding: str) -> tuple: 545 | """Get string from the mode and input file (pointer at the start of stirng).""" 546 | # 0x0A, 0x0B, 0x33. 547 | length = 0 548 | string = b'' 549 | byte = in_file.read(1) 550 | while byte != b'\x00': 551 | string += byte 552 | length += 1 553 | byte = in_file.read(1) 554 | 555 | if mode == 0x0A: 556 | list_bytes = string.hex(' ').split(' ') 557 | string = b'' 558 | i = 0 559 | while i < len(list_bytes): 560 | number = int(list_bytes[i], 16) 561 | if number < 0x81: 562 | zlo = number - 0x7D62 563 | high = (zlo & 0xff00) >> 8 564 | low = zlo & 0xff 565 | marbas = str(hex(high))[2:] 566 | if len(marbas) == 1: 567 | marbas = "0" + marbas 568 | string += byte.fromhex(marbas) 569 | marbas = str(hex(low))[2:] 570 | if len(marbas) == 1: 571 | marbas = "0" + marbas 572 | string += byte.fromhex(marbas) 573 | i += 1 574 | else: 575 | high = int(list_bytes[i], 16) 576 | marbas = str(hex(high))[2:] 577 | if len(marbas) == 1: 578 | marbas = "0" + marbas 579 | string += byte.fromhex(marbas) 580 | if (i + 1) < len(list_bytes): 581 | i += 1 582 | low = int(list_bytes[i], 16) 583 | marbas = str(hex(low))[2:] 584 | if len(marbas) == 1: 585 | marbas = "0" + marbas 586 | string += byte.fromhex(marbas) 587 | i += 1 588 | try: 589 | return length, (string.decode(encoding)) 590 | except UnicodeDecodeError: 591 | print("Decode error:", string) 592 | return length, string.hex(' ') 593 | elif (mode == 0x33) or (mode == 0x0B): 594 | try: 595 | return length, string.decode(encoding) 596 | except UnicodeDecodeError: 597 | print("Decode error:", string) 598 | return length, string 599 | else: 600 | return length, string 601 | 602 | 603 | class SilkyMesScriptError(Exception): 604 | def __init__(self, message: str): 605 | self.message = message 606 | super().__init__(message) 607 | -------------------------------------------------------------------------------- /library/silky_mes_gui.py: -------------------------------------------------------------------------------- 1 | import os 2 | import ctypes 3 | import locale 4 | import threading 5 | import tkinter as tk 6 | from tkinter.filedialog import askopenfilename, askdirectory 7 | from tkinter.messagebox import showinfo, showwarning, showerror 8 | from .silky_mes import SilkyMesScript 9 | 10 | 11 | class SilkyMesGUI: 12 | default_width = 400 13 | default_height = 500 14 | default_max_thread_activity = 10 15 | 16 | possible_languages = ("eng", "rus") 17 | 18 | _strings_lib = { 19 | "eng": ( 20 | "mesScriptAsseAndDisassembler by Tester", # 0 21 | "Single file", 22 | "Directory", 23 | "Enter a name of the .mes file:", 24 | "Enter a name of the directory with .mes files:", 25 | "Enter a title of the .txt file:", # 5 26 | "Enter a name of the directory with .txt files:", 27 | "All files", 28 | "Silky Engine mes scripts", 29 | "Choice of mes script", 30 | "Choice of directory with mes scripts", # 10 31 | "Text files", 32 | "Choice of directory with txt files", 33 | "Choice of text file", 34 | "Commands:", 35 | "Status:", # 15 36 | "Help:", 37 | "Common help", 38 | "Usage help", 39 | "Breaks help", 40 | "Disassemble", # 20 41 | "Assemble", 42 | "Warning", 43 | "File mes or a directory of them is not chosen.", 44 | "File txt or a directory of them is not chosen.", 45 | "Managing files...", # 25 46 | "Error", 47 | "Disassembling failed. ", 48 | "Disassembling succeed. ", 49 | "Assembling failed. ", 50 | "Assembling succeed. ", # 30 51 | ), 52 | "rus": ( 53 | "mesScriptAsseAndDisassembler от Tester-а", # 0 54 | "По файлами", 55 | "По папкам", 56 | "Введите название файла .mes:", 57 | "Введите название директории с файлами .mes:", 58 | "Введите название файла .txt:", # 5 59 | "Введите название директории с файлами .txt:", 60 | "Все файлы", 61 | "Скрипты mes Silky Engine", 62 | "Выбор скрипта mes", 63 | "Выбор директории со скриптами mes", # 10 64 | "Текстовые файлы", 65 | "Выбор директории с файлами txt", 66 | "Выбор текстового файла", 67 | "Команды:", 68 | "Статус:", # 15 69 | "Справка:", 70 | "Общая справка", 71 | "Справка о использовании", 72 | "Справка о переносах", 73 | "Дизассемблировать", # 20 74 | "Ассемблировать", 75 | "Предупреждение", 76 | "Файл mes или их директория не выбраны", 77 | "Файл txt или их директория не выбраны", 78 | "Обрабатываем файлы...", # 25 79 | "Ошибка", 80 | "Дизассемблирование не удалось. ", 81 | "Дизассемблирование удалось. ", 82 | "Ассемблирование не удалось. ", 83 | "Ассемблирование удалось. ", # 30 84 | ) 85 | } 86 | 87 | common_help = { 88 | 'eng': """ 89 | Dual languaged (rus+eng) tool for disassembling and assembling scripts .mes from the visual novel's engine Silky Engine (also known as Silky's Engine or SilkyEngine). With it thou can fully edit code, not just strings, as with some earlier tools. Thou can add line or even message breaks without restrictions! Sometimes mes scripts may not contain strings. If this is the case, they can be found in MAP string patch files in data.arc. For them use MAPTool. Sometimes you may need to work with Silky Engine .arc scripts. For it use SilkyArcTool instead. 90 | It has some useful features. Firstly, during disassembling all opcodes '\x0A' changes to '\x0B', so the engine wouldn't try to decrypt new strings and break latin and half-width kana symbols. Secondly, thou can make comments in txt file with "$" at the beginning of the string. Thirdly, some definations: "#0-" are "free bytes", "#1-" are commands (and "[...]" are arguments below), "#2-" are labels and "#3" are speacial header labels. 91 | """, 92 | 'rus': """ 93 | Двуязычное (рус+англ) средство для разборки и сборки скриптов .mes движка визуальных новелл Silky Engine, также известного как Silky's Engine и SilkyEngine. С ним вы можете полностью редактирвоать код, а не только строки, как с ранее существовшими средствами. Вы можете добавлять разрывы текста по строкам и даже сообщениям без ограничений! Ежель не найти в скриптах mes строк, то оные в файлах патча строк MAP могут быть, что в data.arc лежат. Для оных используйте MAPTool. Вам также может понадобиться необходимость работать с архивами .arc Silky Engine. Для этого используйте SilkyArcTool. 94 | В нём есть несколько полезных особенностей. Во-первых, во время дизассемблирования все опкоды '\x0A' меняются на '\x0B', дабы движок не пытался дешифровать новые строки и не ломал при том латиницу и полуширинные символы. Во-вторых, можно делать комментарии, при этом в начало строки необходимо ставить "$". В-третьих, опишем некоторые определения: "#0-" есть "вольные байты", "#1-" есть команды (и под ними "[...]" аргументы), "#2-" есть метки и "#3" есть специальные заголовочные метки.""", 95 | } 96 | 97 | usage_help = { 98 | 'eng': """ 99 | 1. Choose the mode, file or directory. In first mode you will work with one .mes - .txt pair, in second -- with all files in a pair of directories. 100 | 2. Enter a name of the .mes file in the top entry (do see, with extension) or the directory name. Thou can also enter relative or absolute path. You can also click on "..." to choose. 101 | 3. Enter a name of the .txt file (do see, with extension) or the directory name. Thou can also enter relative or absolute path. You can also click on "..." to choose. 102 | 4. For dissassemble push the button "Disassemble script". 103 | 5. For assemble push the button "Assemble script". 104 | 6. Status will be displayed on the text area below. 105 | """, 106 | 'rus': """ 107 | 1. Выберите режим: файл или директорию. В первом вы будете работать с парой .mes - .txt, во втором -- со всеми файлами в паре директорий. 108 | 2. Введите название файла .mes в верхней форме (заметьте, с расширением) или имя директории. Также можно вводить относительный или абсолютный до него путь. Также вы можете нажать на кнопку "...", чтобы выбрать. 109 | 3. Введите название файла .txt в нижней форме (заметьте, с расширением) или имя директории. Также можно вводить относительный или абсолютный до него путь. Также вы можете нажать на кнопку "...", чтобы выбрать. 110 | 4. Для разборки нажмите на кнопку "Дизассемблировать скрипт". 111 | 5. Для сборки нажмите на кнопку "Ассемблировать скрипт". 112 | 6. Статус сих операций будет отображаться на текстовом поле ниже. 113 | """, 114 | } 115 | 116 | breaks_help = { 117 | 'eng': """ 118 | Sometimes there could be a very big problem: text may not fully get in textbox. But with this tool thou don't need to cut some part of text, no. Thou can use line and message breaks. Methods are below. 119 | >>> For line breaks insert this below the current message ('SomeString' -> text on the new line). 120 | ``` 121 | #1-TO_NEW_STRING 122 | [0] 123 | #1-STR_UNCRYPT 124 | ['SomeString'] 125 | ``` 126 | >>> For message breaks insert this below the current message ('SomeString' -> text on the new message). 127 | ``` 128 | #1-32 129 | [0, 3] 130 | #1-32 131 | [0, 22] 132 | #1-24 133 | [] 134 | #1-32 135 | [0, 0] 136 | #1-32 137 | [0, 3] 138 | #1-17 139 | [] 140 | #1-MESSAGE 141 | [0] 142 | #1-STR_UNCRYPT 143 | ['SomeString'] 144 | ``` 145 | """, 146 | 'rus': """ 147 | Иногда можно столкнуться с одной большой-пребольшой проблемой: текст может не полностью влезать в текстовое окно. Однако, с сим средством вам не нужно обрезать его, отнюдь. Вы можеет организовывать переносы по строкам и сообщениям. Методы указаны ниже. 148 | >>> Для переносов по строкам добавьте под текущее сообщение следующий код ('Какая_то_строка' -> текст на новой строке). 149 | ``` 150 | #1-TO_NEW_STRING 151 | [0] 152 | #1-STR_UNCRYPT 153 | ['Какая_то_строка'] 154 | ``` 155 | >>> Для переносов по сообщениям добавьте под текущее сообщение следующий код ('Какая_то_строка' -> текст на новой строке). 156 | ``` 157 | #1-32 158 | [0, 3] 159 | #1-32 160 | [0, 22] 161 | #1-24 162 | [] 163 | #1-32 164 | [0, 0] 165 | #1-32 166 | [0, 3] 167 | #1-17 168 | [] 169 | #1-MESSAGE 170 | [0] 171 | #1-STR_UNCRYPT 172 | ['Какая_то_строка'] 173 | ``` 174 | """, 175 | } 176 | 177 | def __init__(self, **kwargs): 178 | """Arguments: width, height, language ("eng", "rus"), max_thread_activity...""" 179 | self._width = kwargs.get("width", self.default_width) 180 | self._height = kwargs.get("height", self.default_height) 181 | self._language = kwargs.get("language", self.init_language()) 182 | self._max_thread_activity = kwargs.get("max_thread_activity", self.default_max_thread_activity) 183 | self._thread_semaphore = threading.BoundedSemaphore(self._max_thread_activity) 184 | self._unlocker_count = 0 185 | self._count_lock = threading.Lock() 186 | self._print_lock = threading.Lock() 187 | self._status_lock = threading.Lock() 188 | 189 | self._root = tk.Tk() 190 | self._root.lang_index = 0 191 | self._root.geometry('{}x{}+{}+{}'.format( 192 | self._width, 193 | self._height, 194 | self._root.winfo_screenwidth() // 2 - self._width // 2, 195 | self._root.winfo_screenheight() // 2 - self._height // 2)) 196 | 197 | self._mes_file = tk.StringVar() # Name (with path) of Silky Engine's .mes archive. 198 | self._txt_file = tk.StringVar() # Name (with path) of txt file. 199 | self._input_mode = tk.IntVar() # How to input. 0 -- file, 1 -- directory. 200 | self._last_indexer = 0 # Some logic only for actual change. 201 | 202 | self._mode_rdb = [] 203 | for i in range(2): 204 | new_radio = tk.Radiobutton(master=self._root, 205 | variable=self._input_mode, 206 | background="white", 207 | font=('Helvetica', 14), 208 | value=i) 209 | new_radio.lang_index = i + 1 210 | self._mode_rdb.append(new_radio) 211 | 212 | self._rus_btn = tk.Button(master=self._root, 213 | text="Русский", 214 | command=lambda: self.translate("rus"), 215 | font=('Helvetica', 15), 216 | bg='white') 217 | self._eng_btn = tk.Button(master=self._root, 218 | text="English", 219 | command=lambda: self.translate("eng"), 220 | font=('Helvetica', 15), 221 | bg='white') 222 | 223 | self._mes_point_lbl = tk.Label(master=self._root, 224 | bg='white', 225 | font=('Helvetica', 12)) 226 | self._mes_name_ent = tk.Entry(master=self._root, 227 | bg='white', 228 | textvariable=self._mes_file) 229 | self._mes_find_btn = tk.Button(master=self._root, 230 | text="...", 231 | command=self._find_mes, 232 | font=('Helvetica', 12), 233 | bg='white') 234 | 235 | self._txt_point_lbl = tk.Label(master=self._root, 236 | bg='white', 237 | font=('Helvetica', 12)) 238 | self._txt_name_ent = tk.Entry(master=self._root, 239 | bg='white', 240 | textvariable=self._txt_file) 241 | self._txt_find_btn = tk.Button(master=self._root, 242 | text="...", 243 | command=self._find_txt, 244 | font=('Helvetica', 12), 245 | bg='white') 246 | 247 | self._input_mode.trace_add("write", lambda *garbage: self._change_input_mode()) 248 | self._input_mode.set(self._last_indexer) 249 | 250 | self._commands_lfr = tk.LabelFrame(master=self._root, 251 | font=('Helvetica', 14), 252 | bg='white', 253 | relief=tk.RAISED) 254 | self._commands_lfr.lang_index = 14 255 | self._status_lfr = tk.LabelFrame(master=self._root, 256 | font=('Helvetica', 14), 257 | bg='white', 258 | relief=tk.RAISED) 259 | self._status_lfr.lang_index = 15 260 | self._help_lfr = tk.LabelFrame(master=self._root, 261 | font=('Helvetica', 14), 262 | bg='white', 263 | relief=tk.RAISED) 264 | self._help_lfr.lang_index = 16 265 | 266 | self._status_txt = tk.Text(master=self._status_lfr, 267 | wrap=tk.WORD, 268 | font=('Helvetica', 14), 269 | bg='white', 270 | relief=tk.SUNKEN, 271 | state=tk.DISABLED) 272 | 273 | self._common_help_btn = tk.Button(master=self._help_lfr, 274 | text="Common help", 275 | command=lambda: showinfo(title=self._strings_lib[self._language][17], 276 | message=self.common_help[self._language]), 277 | font=('Helvetica', 12), 278 | bg='white') 279 | self._common_help_btn.lang_index = 17 280 | self._usage_help_btn = tk.Button(master=self._help_lfr, 281 | text="Usage help", 282 | command=lambda: showinfo(title=self._strings_lib[self._language][18], 283 | message=self.usage_help[self._language]), 284 | font=('Helvetica', 12), 285 | bg='white') 286 | self._usage_help_btn.lang_index = 18 287 | self._breaks_help_btn = tk.Button(master=self._help_lfr, 288 | text="Line/message breaks help", 289 | command=lambda: showinfo(title=self._strings_lib[self._language][19], 290 | message=self.breaks_help[self._language]), 291 | font=('Helvetica', 12), 292 | bg='white') 293 | self._breaks_help_btn.lang_index = 19 294 | 295 | commands = (self._disassemble, self._assemble) 296 | self._action_btn = [] 297 | for num, comm in enumerate(commands): 298 | new_btn = tk.Button( 299 | master=self._commands_lfr, 300 | command=comm, 301 | font=('Helvetica', 12), 302 | bg='white', 303 | ) 304 | new_btn.lang_index = 20 + num 305 | self._action_btn.append(new_btn) 306 | 307 | self._init_strings() 308 | 309 | self.place_widgets() 310 | self.start_gui() 311 | 312 | # Technical methods to do directory before running GUI. 313 | 314 | def place_widgets(self) -> None: 315 | """Place widgets of the GUI.""" 316 | # Top buttons. 317 | self._rus_btn.place(relx=0.0, rely=0.0, relwidth=0.5, relheight=0.05) 318 | self._eng_btn.place(relx=0.5, rely=0.0, relwidth=0.5, relheight=0.05) 319 | 320 | # Input/output files/dirs choosers widgets. 321 | 322 | for num, widget in enumerate(self._mode_rdb): 323 | widget.place(relx=0.5 * num, rely=0.05, relwidth=0.5, relheight=0.05) 324 | self._mes_point_lbl.place(relx=0.0, rely=0.1, relwidth=1.0, relheight=0.05) 325 | self._mes_name_ent.place(relx=0.0, rely=0.15, relwidth=0.9, relheight=0.05) 326 | self._mes_find_btn.place(relx=0.9, rely=0.15, relwidth=0.1, relheight=0.05) 327 | self._txt_point_lbl.place(relx=0.0, rely=0.2, relwidth=1.0, relheight=0.05) 328 | self._txt_name_ent.place(relx=0.0, rely=0.25, relwidth=0.9, relheight=0.05) 329 | self._txt_find_btn.place(relx=0.9, rely=0.25, relwidth=0.1, relheight=0.05) 330 | 331 | # Commands. 332 | 333 | for widget in self._action_btn: 334 | widget.pack(fill=tk.X) 335 | 336 | # Text area. 337 | 338 | self._status_txt.pack() 339 | 340 | # Help buttons. 341 | 342 | self._common_help_btn.pack(fill=tk.X) 343 | self._usage_help_btn.pack(fill=tk.X) 344 | self._breaks_help_btn.pack(fill=tk.X) 345 | 346 | # And finally label frames. 347 | 348 | self._commands_lfr.place(relx=0.0, rely=0.3, relwidth=1.0, relheight=0.2) 349 | self._status_lfr.place(relx=0.0, rely=0.5, relwidth=1.0, relheight=0.2) 350 | self._help_lfr.place(relx=0.0, rely=0.7, relwidth=1.0, relheight=0.3) 351 | 352 | def start_gui(self) -> None: 353 | """Start the GUI.""" 354 | # To make more space for patching. 355 | self._root.mainloop() 356 | 357 | # Choose input/output files/dirs. 358 | 359 | def _change_input_mode(self) -> None: 360 | """Change of mode of the input: text or directory.""" 361 | indexer = self._input_mode.get() 362 | self._mes_point_lbl.lang_index = 3 + indexer 363 | self._txt_point_lbl.lang_index = 5 + indexer 364 | if indexer == 0 and indexer != self._last_indexer: # Filenames now. 365 | self._mes_file.set("") 366 | self._txt_file.set("") 367 | else: 368 | self._mes_file.set(os.path.splitext(self._mes_file.get())[0]) 369 | self._txt_file.set(os.path.splitext(self._txt_file.get())[0]) 370 | self._last_indexer = indexer 371 | self._init_strings() 372 | 373 | def _find_mes(self) -> None: 374 | """Find mes file or a directory with them.""" 375 | if self._input_mode.get() == 0: # File mode. 376 | file_types = [(self._strings_lib[self._language][8], '*.mes'), 377 | (self._strings_lib[self._language][7], '*')] 378 | file_name = askopenfilename(filetypes=file_types, initialdir=os.getcwd(), 379 | title=self._strings_lib[self._language][9]) 380 | if file_name: 381 | file_name = os.path.normpath(file_name) 382 | relpath = os.path.relpath(file_name, os.getcwd()) 383 | end_arc = file_name 384 | if relpath.count(os.sep) < file_name.count(os.sep): 385 | end_arc = relpath 386 | self._mes_file.set(end_arc) 387 | if self._txt_file.get() == "": 388 | self._txt_file.set(os.path.splitext(end_arc)[0] + ".txt") 389 | else: # Dir mode. 390 | dir_name = askdirectory(initialdir=os.getcwd(), title=self._strings_lib[self._language][10]) 391 | if dir_name: 392 | dir_name = os.path.normpath(dir_name) 393 | relpath = os.path.relpath(dir_name, os.getcwd()) 394 | end_dir = dir_name 395 | if relpath.count(os.sep) < dir_name.count(os.sep): 396 | end_dir = relpath 397 | self._mes_file.set(end_dir) 398 | 399 | def _find_txt(self) -> None: 400 | """Find txt file or a directory with them.""" 401 | if self._input_mode.get() == 0: # File mode. 402 | file_types = [(self._strings_lib[self._language][1], '*.txt'), 403 | (self._strings_lib[self._language][7], '*')] 404 | file_name = askopenfilename(filetypes=file_types, initialdir=os.getcwd(), 405 | title=self._strings_lib[self._language][13]) 406 | if file_name: 407 | file_name = os.path.normpath(file_name) 408 | relpath = os.path.relpath(file_name, os.getcwd()) 409 | end_arc = file_name 410 | if relpath.count(os.sep) < file_name.count(os.sep): 411 | end_arc = relpath 412 | self._txt_file.set(end_arc) 413 | if self._mes_file.get() == "": 414 | self._mes_file.set(os.path.splitext(end_arc)[0] + ".mes") 415 | else: # Dir mode. 416 | dir_name = askdirectory(initialdir=os.getcwd(), title=self._strings_lib[self._language][12]) 417 | if dir_name: 418 | dir_name = os.path.normpath(dir_name) 419 | relpath = os.path.relpath(dir_name, os.getcwd()) 420 | end_dir = dir_name 421 | if relpath.count(os.sep) < dir_name.count(os.sep): 422 | end_dir = relpath 423 | self._txt_file.set(end_dir) 424 | 425 | # Activity (un)locking. 426 | 427 | def _lock_activity(self) -> None: 428 | """Lock disassemble and assemble actions while managing other files.""" 429 | self._status_txt["state"] = tk.NORMAL 430 | self._status_txt.delete(1.0, tk.END) 431 | self._status_txt.insert(1.0, self._strings_lib[self._language][25]) 432 | self._status_txt["state"] = tk.DISABLED 433 | for widget in self._action_btn: 434 | widget["state"] = tk.DISABLED 435 | 436 | def _unlock_activity(self) -> None: 437 | """Unlock disassemble and assemble actions after managing other files.""" 438 | for widget in self._action_btn: 439 | widget["state"] = tk.NORMAL 440 | 441 | # Disassembling and assembling methods. 442 | 443 | def _disassemble(self) -> bool: 444 | """Disassemble a mes script or a group of them to a text file or a group of them""" 445 | mes_file, txt_file, status = self._get_mes_and_txt() 446 | if not status: 447 | return False 448 | 449 | self._lock_activity() 450 | if self._input_mode.get() == 0: # File mode. 451 | self._unlocker_count = 1 452 | new_thread = threading.Thread(daemon=False, target=self._disassemble_this_mes, 453 | args=(mes_file, txt_file)) 454 | new_thread.start() 455 | else: # Dir mode. 456 | files_to_manage = [] 457 | os.makedirs(txt_file, exist_ok=True) 458 | ezz = len(mes_file.split(os.sep)) 459 | for root, dirs, files in os.walk(mes_file): 460 | for file_name in files: 461 | new_file_array = [] # mes_file, txt_file 462 | 463 | basic_path = os.sep.join(os.path.join(root, file_name).split(os.sep)[ezz:]) 464 | rel_mes_name = os.path.normpath(os.path.join(mes_file, basic_path)) 465 | rel_txt_name = os.path.normpath(os.path.join(txt_file, os.path.splitext(basic_path)[0] + ".txt")) 466 | 467 | new_file_array.append(rel_mes_name) 468 | new_file_array.append(rel_txt_name) 469 | files_to_manage.append(new_file_array) 470 | 471 | # Why did I not initiate file management right away, thou ask? 472 | 473 | self._unlocker_count = len(files_to_manage) # ...That is the answer. 474 | for file_mes, file_txt in files_to_manage: 475 | new_thread = threading.Thread(daemon=False, target=self._disassemble_this_mes, 476 | args=(file_mes, file_txt)) 477 | new_thread.start() 478 | 479 | return True 480 | 481 | def _disassemble_this_mes(self, mes_file: str, txt_file: str) -> None: 482 | """Disassemble this mes script.""" 483 | try: 484 | self._thread_semaphore.acquire() 485 | script_mes = SilkyMesScript(mes_file, txt_file) 486 | script_mes.disassemble() 487 | self._status_lock.acquire() 488 | self._status_txt["state"] = tk.NORMAL 489 | self._status_txt.delete(1.0, tk.END) 490 | self._status_txt.insert(1.0, mes_file + ": ") 491 | self._status_txt.insert(2.0, self._strings_lib[self._language][28]) 492 | self._status_txt["state"] = tk.DISABLED 493 | self._status_lock.release() 494 | self._print_lock.acquire() 495 | print("Disassembling of {0} succeed./Дизассемблирование {0} прошло успешно.".format(mes_file)) 496 | self._print_lock.release() 497 | except Exception as ex: 498 | self._print_lock.acquire() 499 | print("Disassembling of {0} error./Дизассемблирование {0} не удалось.".format(mes_file)) 500 | self._print_lock.release() 501 | showerror(title=self._strings_lib[self._language][26], message=str(ex)) 502 | self._status_lock.acquire() 503 | self._status_txt["state"] = tk.NORMAL 504 | self._status_txt.delete(1.0, tk.END) 505 | self._status_txt.insert(1.0, mes_file + ": ") 506 | self._status_txt.insert(2.0, self._strings_lib[self._language][27]) 507 | self._status_txt["state"] = tk.DISABLED 508 | self._status_lock.release() 509 | finally: 510 | self._count_lock.acquire() 511 | self._unlocker_count -= 1 512 | self._count_lock.release() 513 | if self._unlocker_count == 0: 514 | self._unlock_activity() 515 | self._thread_semaphore.release() 516 | 517 | def _assemble(self) -> bool: 518 | """Assemble a mes script or a group of them from the text file or a group of them""" 519 | mes_file, txt_file, status = self._get_mes_and_txt() 520 | if not status: 521 | return False 522 | 523 | self._lock_activity() 524 | if self._input_mode.get() == 0: # File mode. 525 | self._unlocker_count = 1 526 | new_thread = threading.Thread(daemon=False, target=self._assemble_this_mes, 527 | args=(mes_file, txt_file)) 528 | new_thread.start() 529 | else: # Dir mode. 530 | files_to_manage = [] 531 | os.makedirs(txt_file, exist_ok=True) 532 | ezz = len(txt_file.split(os.sep)) 533 | for root, dirs, files in os.walk(txt_file): 534 | for file_name in files: 535 | new_file_array = [] # mes_file, txt_file 536 | 537 | basic_path = os.sep.join(os.path.join(root, file_name).split(os.sep)[ezz:]) 538 | rel_mes_name = os.path.normpath(os.path.join(mes_file, os.path.splitext(basic_path)[0] + ".MES")) 539 | rel_txt_name = os.path.normpath(os.path.join(txt_file, basic_path)) 540 | 541 | new_file_array.append(rel_mes_name) 542 | new_file_array.append(rel_txt_name) 543 | files_to_manage.append(new_file_array) 544 | 545 | # Why did I not initiate file management right away, thou ask? 546 | 547 | self._unlocker_count = len(files_to_manage) # ...That is the answer. 548 | for file_mes, file_txt in files_to_manage: 549 | new_thread = threading.Thread(daemon=False, target=self._assemble_this_mes, 550 | args=(file_mes, file_txt)) 551 | new_thread.start() 552 | 553 | return True 554 | 555 | def _assemble_this_mes(self, mes_file: str, txt_file: str) -> None: 556 | """Assemble this mes script.""" 557 | try: 558 | self._thread_semaphore.acquire() 559 | script_mes = SilkyMesScript(mes_file, txt_file) 560 | script_mes.assemble() 561 | self._status_lock.acquire() 562 | self._status_txt["state"] = tk.NORMAL 563 | self._status_txt.delete(1.0, tk.END) 564 | self._status_txt.insert(1.0, mes_file + ": ") 565 | self._status_txt.insert(2.0, self._strings_lib[self._language][30]) 566 | self._status_txt["state"] = tk.DISABLED 567 | self._status_lock.release() 568 | self._print_lock.acquire() 569 | print("Assembling of {0} succeed./Ассемблирование {0} прошло успешно.".format(mes_file)) 570 | self._print_lock.release() 571 | except Exception as ex: 572 | self._print_lock.acquire() 573 | print("Assembling of {0} error./Ассемблирование {0} не удалось.".format(mes_file)) 574 | self._print_lock.release() 575 | showerror(title=self._strings_lib[self._language][26], message=str(ex)) 576 | self._status_lock.acquire() 577 | self._status_txt["state"] = tk.NORMAL 578 | self._status_txt.delete(1.0, tk.END) 579 | self._status_txt.insert(1.0, mes_file + ": ") 580 | self._status_txt.insert(2.0, self._strings_lib[self._language][29]) 581 | self._status_txt["state"] = tk.DISABLED 582 | self._status_lock.release() 583 | finally: 584 | self._count_lock.acquire() 585 | self._unlocker_count -= 1 586 | self._count_lock.release() 587 | if self._unlocker_count == 0: 588 | self._unlock_activity() 589 | self._thread_semaphore.release() 590 | 591 | def _get_mes_and_txt(self) -> tuple: 592 | """Get mes, txt files or directories and check status.""" 593 | status = True 594 | 595 | # Check if there a mes file/dir. 596 | 597 | mes_file = self._mes_file.get() 598 | if mes_file == '': 599 | status = False 600 | showwarning(title=self._strings_lib[self._language][22], 601 | message=self._strings_lib[self._language][23]) 602 | 603 | # Check if there a txt file/dir. 604 | 605 | txt_file = self._txt_file.get() 606 | if txt_file == '': 607 | status = False 608 | showwarning(title=self._strings_lib[self._language][22], 609 | message=self._strings_lib[self._language][24]) 610 | mes_file = os.path.abspath(mes_file) 611 | txt_file = os.path.abspath(txt_file) 612 | 613 | return mes_file, txt_file, status 614 | 615 | # Language technical methods. 616 | 617 | def translate(self, language: str) -> None: 618 | """Change the GUI language on "rus" or "eng".""" 619 | if language not in self.possible_languages: 620 | print("Error! Incorrect language!/Ошибка! Некорректный язык!") 621 | return 622 | self._language = language 623 | self._init_strings() 624 | 625 | def _init_strings(self) -> None: 626 | """Initialize strings of the GUI's widgets.""" 627 | 628 | # Quite an elegant solution I through off. Hope this works. 629 | def _init_all_children_strings(widget): 630 | for elem in widget.winfo_children(): 631 | if hasattr(elem, "lang_index"): 632 | elem["text"] = self._strings_lib[self._language][elem.lang_index] 633 | if isinstance(elem, tk.Frame) or isinstance(elem, tk.LabelFrame): 634 | _init_all_children_strings(elem) 635 | 636 | self._root.title(self._strings_lib[self._language][self._root.lang_index]) 637 | _init_all_children_strings(self._root) 638 | 639 | @staticmethod 640 | def init_language() -> str: 641 | """Get default language from the system. Works only on Windows.""" 642 | lang_num = 0 643 | try: 644 | windll = ctypes.windll.kernel32 645 | super_locale = locale.windows_locale[windll.GetUserDefaultUILanguage()][:2] 646 | to_rus_locales = ('ru', 'uk', 'sr', 'bg', 'kk', 'be', 'hy', 'az') 647 | if super_locale in to_rus_locales: 648 | lang_num = 1 649 | except Exception: # Yes, yes, I know this is a bad practice, but it does not matter here. 650 | pass 651 | return SilkyMesGUI.possible_languages[lang_num] 652 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from ai6win_mes_gui import AI6WINMesGUI 2 | 3 | debug = False 4 | 5 | 6 | def test(mode: str): 7 | # diss, ass, diss_for_hack, spec_cmp... 8 | from ai6win_mes import AI6WINScript 9 | 10 | base_name = "01asa_s02_01_03" 11 | script_mes = "{}.mes".format(base_name) 12 | file_txt = "{}.txt".format(base_name) 13 | 14 | if mode == "diss": 15 | new_script = AI6WINScript(script_mes, file_txt, verbose=True, debug=False, version=1) 16 | new_script.disassemble() 17 | del new_script 18 | elif mode == "ass": 19 | new_script = AI6WINScript(script_mes, file_txt, verbose=True, debug=False) 20 | new_script.assemble() 21 | del new_script 22 | elif mode == "diss_for_hack": 23 | new_script = AI6WINScript(script_mes, file_txt, verbose=True, debug=True, hackerman_mode=True) 24 | new_script.disassemble() 25 | del new_script 26 | 27 | 28 | def main(): 29 | gui = AI6WINMesGUI() 30 | return True 31 | 32 | 33 | if __name__ == '__main__': 34 | if debug: 35 | test("diss") 36 | else: 37 | main() 38 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import cx_Freeze 3 | 4 | base = None 5 | 6 | if (sys.platform == 'win32'): 7 | base = "Win32GUI" 8 | 9 | 10 | executables = [cx_Freeze.Executable("main.py", 11 | shortcut_name="AI6WINScriptTool", 12 | shortcut_dir="AI6WINScriptTool")] 13 | 14 | cx_Freeze.setup( 15 | name="AI6WINScriptTool", 16 | version="2.0", 17 | description="Dual languaged (rus+eng) tool for packing and unpacking mes scripts of AI6WIN.\n" 18 | "Двухязычное средство (рус+англ) для распаковки и запаковки скриптов mes AI6WIN.", 19 | options={"build_exe": {"packages": []}}, 20 | executables=executables 21 | ) 22 | --------------------------------------------------------------------------------