├── README.md ├── README.zh.md ├── details.md ├── details.zh.md ├── exe ├── build.bat ├── exe_patch.diff └── get_il.bat ├── export_txt └── export_txt.py ├── font ├── bmfont │ ├── __init__.py │ ├── bmfont.com │ ├── bmfont.exe │ ├── bmfont.py │ ├── bmfont_base.py │ ├── bmfont_bin.py │ ├── bmfont_gen.py │ ├── bmfont_txt.py │ └── bmfont_xml.py ├── gen.py └── packedfont.py ├── images ├── export_imgs.py ├── import_imgs.py ├── new │ └── PackedContent │ │ └── textures │ │ ├── loading │ │ ├── bar_complete.png │ │ ├── bar_empty.png │ │ └── bar_full.png │ │ └── workhouse │ │ ├── cancel.png │ │ ├── cancel_hover.png │ │ ├── disconnect.png │ │ ├── disconnect_hover.png │ │ ├── exit.png │ │ ├── exit_hover.png │ │ ├── message_complete.png │ │ ├── message_disconnect.png │ │ ├── message_rejected.png │ │ ├── message_verifying.png │ │ ├── okay.png │ │ ├── okay_disabled.png │ │ ├── okay_hover.png │ │ ├── panel_left.png │ │ ├── panel_middle.png │ │ ├── panel_right.png │ │ ├── popup_connecting.png │ │ ├── submit.png │ │ └── submit_hover.png └── tex.py ├── import_txt ├── EXAPUNKS_descriptions.json ├── EXAPUNKS_exe.json ├── EXAPUNKS_vignettes.json ├── convert.py ├── excel2json.py ├── get_chars.py ├── history.py ├── import_txt.py ├── json2excel.py └── translation.py ├── manual ├── convert.exe ├── digital_en_1 │ ├── digital_en_1_01.xcf │ ├── digital_en_1_02.xcf │ ├── digital_en_1_03.xcf │ ├── digital_en_1_04.xcf │ ├── digital_en_1_05.xcf │ ├── digital_en_1_06.xcf │ ├── digital_en_1_07.xcf │ ├── digital_en_1_08.xcf │ ├── digital_en_1_09.xcf │ ├── digital_en_1_10.xcf │ ├── digital_en_1_11.xcf │ ├── digital_en_1_12.xcf │ ├── digital_en_1_13.xcf │ ├── digital_en_1_14.xcf │ ├── digital_en_1_15.xcf │ ├── digital_en_1_16.xcf │ ├── digital_en_1_17.xcf │ ├── digital_en_1_18.xcf │ ├── digital_en_1_19.xcf │ ├── digital_en_1_20.xcf │ └── digital_en_1_21.xcf ├── digital_en_2 │ ├── digital_en_2_00.xcf │ ├── digital_en_2_01.xcf │ ├── digital_en_2_02.xcf │ ├── digital_en_2_03.xcf │ ├── digital_en_2_04.xcf │ ├── digital_en_2_05.xcf │ ├── digital_en_2_06.xcf │ ├── digital_en_2_07.xcf │ ├── digital_en_2_08.xcf │ ├── digital_en_2_09.xcf │ ├── digital_en_2_10.xcf │ ├── digital_en_2_11.xcf │ ├── digital_en_2_12.xcf │ ├── digital_en_2_13.xcf │ ├── digital_en_2_14.xcf │ ├── digital_en_2_15.xcf │ ├── digital_en_2_16.xcf │ ├── digital_en_2_17.xcf │ ├── digital_en_2_18.xcf │ ├── digital_en_2_19.xcf │ └── digital_en_2_20.xcf └── gen.py ├── patch └── EXAPUNKS_fixed.exe ├── run.bat └── screenshot ├── digital_en_1_01.jpg ├── digital_en_1_06.jpg ├── excel_example.gif ├── screenshot_1.jpg ├── screenshot_2.jpg └── screenshot_3.jpg /README.md: -------------------------------------------------------------------------------- 1 | [这里有中文版](README.zh.md) 2 | 3 | This an unofficial localization project for game EXAPUNKS. 4 | 5 | First at all, you should own this game, you can buy it on [steam](https://store.steampowered.com/app/716490/EXAPUNKS/) or [GOG](https://www.gog.com/game/exapunks) or whatever platforms. 6 | 7 | # Prepare the environment 8 | ## 1. install [python](https://www.python.org/) 3 and some dependent libraries. 9 | 10 | * install python from [https://www.python.org/downloads/](https://www.python.org/downloads/) 11 | 12 | * install [pandas](https://pandas.pydata.org/) 13 | 14 | ``` 15 | pip install pandas 16 | ``` 17 | * install [openpyxl](https://openpyxl.readthedocs.io/en/stable/) 18 | ``` 19 | pip install openpyxl 20 | ``` 21 | * install [Pillow](https://python-pillow.org/) 22 | ``` 23 | pip install pillow 24 | ``` 25 | 26 | * install [python-lz4](https://github.com/python-lz4/python-lz4) 27 | ``` 28 | pip install lz4 29 | ``` 30 | 31 | ## 2. copy game files to localization working directory 32 | 33 | * copy ``Content/descriptions/en/*`` to ``./export_txt/Content/descriptions/en/`` 34 | * copy ``Content/vignettes/*`` to ``./export_txt/Content/vignettes`` 35 | * copy ``PackedContent/fonts/*.packedfont`` to ``./font/fonts`` 36 | * copy ``PackedContent/*.tex`` to ``./images/PackedContent`` 37 | 38 | Note: **must** be fixed files: 39 | 40 | ``Content/vignettes/ember-7.csv`` miss a double quote at line 12 41 | 42 | ``Content/vignettes/nivas-3.csv`` miss a double quote at line 9 43 | 44 | ## 3. install fonts (optional) 45 | * install [Source Han Sans](https://github.com/adobe-fonts/source-han-sans) 46 | * install [Source Han Mono](https://github.com/adobe-fonts/source-han-mono) 47 | 48 | otherwise set your favorite fonts in ``import_txt/translation.py`` and ``font/gen.py`` 49 | 50 | # Translate the texts 51 | There are three json files in directory ``import_txt``, they need be translated. 52 | 53 | You could run ``json2excel.py`` to generate excel files from these json files, then edit them in M$ Excel or LibreOffice calc or whatever spreadsheet editor. 54 | 55 | ![](screenshot/excel_example.gif) 56 | 57 | * ``EXAPUNKS_descriptions.json`` 58 | 59 | Grabbed from Content/descriptions/*.txt 60 | 61 | All texts in this file need been traslated. 62 | 63 | * ``EXAPUNKS_vignettes.json`` 64 | 65 | Grabbed from Content/vignettes/*.csv 66 | 67 | All texts in this file need be translated. 68 | 69 | * ``EXAPUNKS_exe.json`` 70 | 71 | Grabbed from EXAPUNKS.exe 72 | 73 | **Not** all texts in this file need be translated. 74 | 75 | Only translate the text you actually see in the game. 76 | 77 | ## How to use json2excel.py 78 | generate EXAPUNKS_descriptions.xlsx from EXAPUNKS_descriptions.json 79 | ``` 80 | json2excel.py EXAPUNKS_descriptions.json EXAPUNKS_descriptions.xlsx 81 | ``` 82 | do the same thing 83 | ``` 84 | json2excel.py EXAPUNKS_descriptions.json 85 | ``` 86 | traverse current directory, generate .xlsx files from all .json files. 87 | ``` 88 | json2excel.py 89 | ``` 90 | automatically decide whether to overwrite the current .xlsx files based on update date 91 | ``` 92 | json2excel.py --auto 93 | ``` 94 | 95 | # Modify the textures 96 | Run ``images/export_imgs.py`` 97 | 98 | It will traverse directory ``images/PackedContent``, convert all .tex files to .png into the directory ``out``. 99 | 100 | Pick up the images what you want to modify. (No need for ``half``'s, will be generated automatically.) 101 | 102 | Put all of them to the ``new`` dirctory, keep the same directory struct. 103 | 104 | # Generate the localization patch 105 | Run ``run.bat``, the localization patch will be generated in ``patch`` directory. 106 | 107 | If you want to know more details about this procedure, see [details.md](details.md) 108 | 109 | # Change game settings 110 | Edit ``%USERPROFILE%\Documents\My Games\EXAPUNKS\xxxxxx\config.cfg`` 111 | ``` 112 | Language = English 113 | ``` 114 | Change 'English' to 'Chinese | Japanese | French' 115 | 116 | # Some screenshots 117 | ![](screenshot/screenshot_1.jpg) 118 | 119 | ![](screenshot/screenshot_2.jpg) 120 | 121 | ![](screenshot/screenshot_3.jpg) 122 | 123 | # PDF Manual (still in progress) 124 | ![](screenshot/digital_en_1_01.jpg) 125 | ![](screenshot/digital_en_1_06.jpg) -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | 这是一个非官方的游戏汉化项目,游戏名称是EXAPUNKS。 2 | 3 | 有意参与汉化的朋友,可以加入QQ EXAPUNKS 汉化群:**1082638733** 4 | 5 | 首先,你需要拥有这个游戏。你可以在 [steam](https://store.steampowered.com/app/716490/EXAPUNKS/) 或 [GOG](https://www.gog.com/game/exapunks) 购买好此游戏。 6 | 7 | # 准备环境 8 | ## 1. 安装 [python](https://www.python.org/) 3 及相关依赖库 9 | 10 | * 安装 python [https://www.python.org/downloads/](https://www.python.org/downloads/) 11 | 12 | * 安装 [pandas](https://pandas.pydata.org/) 13 | 14 | ``` 15 | pip install pandas 16 | ``` 17 | * 安装 [openpyxl](https://openpyxl.readthedocs.io/en/stable/) 18 | ``` 19 | pip install openpyxl 20 | ``` 21 | 22 | * 安装 [Pillow](https://python-pillow.org/) 23 | ``` 24 | pip install pillow 25 | ``` 26 | 27 | * 安装 [python-lz4](https://github.com/python-lz4/python-lz4) 28 | ``` 29 | pip install lz4 30 | ``` 31 | 32 | ## 3. 安装字体 33 | * 安装 [Source Han Sans](https://github.com/adobe-fonts/source-han-sans) 34 | * 安装 [Source Han Mono](https://github.com/adobe-fonts/source-han-mono) 35 | 36 | 不安装的话,到``import_txt/translation.py`` 和 ``font/gen.py`` 中修改你喜欢的字体 37 | 38 | ## 2. 复制游戏文件到汉化工作目录 39 | 40 | * 复制 ``Content/descriptions/en/*`` 到 ``./export_txt/Content/descriptions/en/`` 41 | * 复制 ``Content/vignettes/*`` 到 ``./export_txt/Content/vignettes`` 42 | * 复制 ``PackedContent/fonts/*.packedfont`` 到 ``./font/fonts`` 43 | * 复制 ``PackedContent/*.tex`` 到 ``./images/PackedContent`` 44 | 45 | ``Content/vignettes/ember-7.csv`` 第十二行少了个双引号 46 | 47 | ``Content/vignettes/nivas-3.csv`` 第九行少了个双引号 48 | 49 | 自己加一下。 50 | 51 | # 翻译文本 52 | 在import_txt目录下有 3 个json文件需要翻译。 53 | 54 | 你可以运行``json2excel.py`` 从 json 文件生成 excel 文件,然后在 M$ Excel 或 LibreOffice calc 或随便哪个电子表格编辑器中进行编辑翻译。 55 | 56 | ![](screenshot/excel_example.gif) 57 | 58 | * ``EXAPUNKS_descriptions.json`` 59 | 60 | 抓取自 Content/descriptions/*.txt 61 | 62 | 此文件中的所有文本都需要翻译。 63 | 64 | * ``EXAPUNKS_vignettes.json`` 65 | 66 | 抓取自 Content/vignettes/*.csv 67 | 68 | 此文件中的所有文本都需要翻译。 69 | 70 | * ``EXAPUNKS_exe.json`` 71 | 72 | 抓取自 EXAPUNKS.exe 73 | 74 | **不是**所有的文本都需要翻译。 75 | 76 | 仅仅翻译你在游戏中实际看到的文本。 77 | 78 | ## 如何使用 json2excel.py 79 | 从 EXAPUNKS_descriptions.json 生成 EXAPUNKS_descriptions.xlsx 80 | ``` 81 | json2excel.py EXAPUNKS_descriptions.json EXAPUNKS_descriptions.xlsx 82 | ``` 83 | 做和上面一样的事 84 | ``` 85 | json2excel.py EXAPUNKS_descriptions.json 86 | ``` 87 | 遍历当前目录,从所有的 .json 文件中生成 .xlsx。 88 | ``` 89 | json2excel.py 90 | ``` 91 | 自动根据更新日期决定是否需要覆盖 92 | ``` 93 | json2excel.py --auto 94 | ``` 95 | 96 | # 修改图片 97 | 运行 ``images/export_imgs.py`` 98 | 99 | 这会遍历 ``PackedContent`` 目录, 把.tex转换成.png,并输出到 ``out`` 目录。 100 | 101 | 挑选你需要修改的图片。把它们放在 ``new`` 目录下,注意保持同样的目录结构。(不需要改 ``half`` 下面的图片,我们会自动生成它们。) 102 | 103 | # 生成汉化补丁 104 | 运行 ``run.bat``,汉化补丁会在 ``patch`` 目录下生成。 105 | 106 | 如果你想了解此过程的详细情况,请看[details.zh.md](details.zh.md) 107 | 108 | # 修改游戏设置 109 | 编辑 ``%USERPROFILE%\Documents\My Games\EXAPUNKS\xxxxxx\config.cfg`` 110 | ``` 111 | Language = English 112 | ``` 113 | 把 'English' 改成 'Chinese' 114 | 115 | # 汉化截图 116 | ![](screenshot/screenshot_1.jpg) 117 | 118 | ![](screenshot/screenshot_2.jpg) 119 | 120 | ![](screenshot/screenshot_3.jpg) 121 | 122 | # PDF 手册 (苦逼翻译/改图中) 123 | ![](screenshot/digital_en_1_01.jpg) 124 | ![](screenshot/digital_en_1_06.jpg) -------------------------------------------------------------------------------- /details.md: -------------------------------------------------------------------------------- 1 | [这里有中文版](details.zh.md) 2 | 3 | Not ready. -------------------------------------------------------------------------------- /details.zh.md: -------------------------------------------------------------------------------- 1 | 还没写 -------------------------------------------------------------------------------- /exe/build.bat: -------------------------------------------------------------------------------- 1 | ilasm.exe /EXE EXAPUNKS.il /RESOURCE=EXAPUNKS.res /OUTPUT=EXAPUNKS_fixed.exe -------------------------------------------------------------------------------- /exe/exe_patch.diff: -------------------------------------------------------------------------------- 1 | 42027c42027 2 | < IL_0061: ldc.i4.1 3 | --- 4 | > IL_0061: ldc.i4.6 5 | 42033a42034,42059 6 | > //------------------------------------- 7 | > ldloc.s V_4 8 | > ldc.i4.1 9 | > ldc.i4.2 10 | > stelem.i4 11 | > 12 | > ldloc.s V_4 13 | > ldc.i4.2 14 | > ldc.i4.3 15 | > stelem.i4 16 | > 17 | > ldloc.s V_4 18 | > ldc.i4.3 19 | > ldc.i4.4 20 | > stelem.i4 21 | > 22 | > ldloc.s V_4 23 | > ldc.i4.4 24 | > ldc.i4.5 25 | > stelem.i4 26 | > 27 | > ldloc.s V_4 28 | > ldc.i4.5 29 | > ldc.i4.6 30 | > stelem.i4 31 | > //------------------------------------- 32 | 152888c152914 33 | < IL_0955: ldstr "Record Solution GIF " 34 | --- 35 | > IL_0955: ldstr "Record Solution GIF" 36 | 225089c225115 37 | < .maxstack 7 38 | --- 39 | > .maxstack 8 40 | 225956,225958c225982,225999 41 | < IL_095c: ldstr "fonts/font_cpmono" 42 | < IL_0961: call class FontSheet FontSheet::smethod_4(string) 43 | < IL_0966: newobj instance void GClass109::.ctor(class FontSheet) 44 | --- 45 | > // IL_095c: ldstr "fonts/font_cpmono" 46 | > // IL_0961: call class FontSheet FontSheet::smethod_4(string) 47 | > // IL_0966: newobj instance void GClass109::.ctor(class FontSheet) 48 | > ldstr "pixel_regular.ttf" 49 | > ldstr "pixel_regular.ttf" 50 | > ldstr "pixel_regular.ttf" 51 | > ldc.r4 7.5 52 | > ldc.i4.0 53 | > ldc.i4.0 54 | > ldnull 55 | > call class GClass109 GClass109::smethod_0(string, 56 | > string, 57 | > string, 58 | > float32, 59 | > int32, 60 | > bool, 61 | > class GClass109) 62 | > 63 | 225961,225963c226002,226019 64 | < IL_0975: ldstr "fonts/font_cga" 65 | < IL_097a: call class FontSheet FontSheet::smethod_4(string) 66 | < IL_097f: newobj instance void GClass109::.ctor(class FontSheet) 67 | --- 68 | > // IL_0975: ldstr "fonts/font_cga" 69 | > // IL_097a: call class FontSheet FontSheet::smethod_4(string) 70 | > // IL_097f: newobj instance void GClass109::.ctor(class FontSheet) 71 | > ldstr "pixel_regular.ttf" 72 | > ldstr "pixel_regular.ttf" 73 | > ldstr "pixel_regular.ttf" 74 | > ldc.r4 7.5 75 | > ldc.i4.0 76 | > ldc.i4.0 77 | > ldnull 78 | > call class GClass109 GClass109::smethod_0(string, 79 | > string, 80 | > string, 81 | > float32, 82 | > int32, 83 | > bool, 84 | > class GClass109) 85 | > 86 | 278822c278878 87 | < IL_0dba: ldstr "Record Solution GIF " 88 | --- 89 | > IL_0dba: ldstr "Record Solution GIF" 90 | 347230a347287,347291 91 | > 92 | > ldsfld string [mscorlib]System.String::Empty 93 | > call class LocString GClass7::smethod_5(string, string) 94 | > callvirt instance string LocString::vmethod_0() 95 | > 96 | 347264a347326,347330 97 | > 98 | > ldsfld string [mscorlib]System.String::Empty 99 | > call class LocString GClass7::smethod_5(string, string) 100 | > callvirt instance string LocString::vmethod_0() 101 | > 102 | 347297a347364,347368 103 | > 104 | > ldsfld string [mscorlib]System.String::Empty 105 | > call class LocString GClass7::smethod_5(string, string) 106 | > callvirt instance string LocString::vmethod_0() 107 | > 108 | 347330a347402,347406 109 | > 110 | > ldsfld string [mscorlib]System.String::Empty 111 | > call class LocString GClass7::smethod_5(string, string) 112 | > callvirt instance string LocString::vmethod_0() 113 | > 114 | 378169c378245 115 | < IL_0e37: brfalse.s IL_0eb6 116 | --- 117 | > IL_0e37: brfalse IL_0eb6 118 | 378181a378258,378262 119 | > 120 | > ldsfld string [mscorlib]System.String::Empty 121 | > call class LocString GClass7::smethod_5(string, string) 122 | > callvirt instance string LocString::vmethod_0() 123 | > 124 | 378192a378274,378278 125 | > 126 | > ldsfld string [mscorlib]System.String::Empty 127 | > call class LocString GClass7::smethod_5(string, string) 128 | > callvirt instance string LocString::vmethod_0() 129 | > 130 | 378209a378296,378300 131 | > 132 | > ldsfld string [mscorlib]System.String::Empty 133 | > call class LocString GClass7::smethod_5(string, string) 134 | > callvirt instance string LocString::vmethod_0() 135 | > 136 | -------------------------------------------------------------------------------- /exe/get_il.bat: -------------------------------------------------------------------------------- 1 | de4dot\de4dot EXAPUNKS.exe 2 | ildasm.exe EXAPUNKS-cleaned.exe /out=EXAPUNKS.il /utf8 -------------------------------------------------------------------------------- /export_txt/export_txt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import sys 4 | sys.path.append('../import_txt') 5 | from translation import Translation, try_to_get_translation 6 | import csv 7 | import os 8 | 9 | 10 | def export_vignettes(): 11 | trans = try_to_get_translation('../import_txt/EXAPUNKS_vignettes.json') 12 | original = set() 13 | data = [] 14 | for root, dirs, files in os.walk('Content/vignettes'): 15 | for f in files: 16 | csv_reader = csv.reader(open(os.path.join(root, f), 'r', encoding='utf_8_sig')) 17 | for row in csv_reader: 18 | if len(row) > 1: 19 | en = row[1] 20 | if len(en) > 0 and en not in original: 21 | original.add(en) 22 | if en in trans: 23 | data.append(trans[en]) 24 | else: 25 | data.append({'FileName': f, 'Role': row[0], 'English': en}) 26 | 27 | translation = Translation() 28 | translation.set_data(data, ('FileName', 'Role', 'English', 'French', 'Chinese', 'Japanese')) 29 | translation.save('EXAPUNKS_vignettes.json') 30 | 31 | 32 | def export_descriptions(): 33 | trans = try_to_get_translation('../import_txt/EXAPUNKS_descriptions.json') 34 | original = set() 35 | data = [] 36 | for root, dirs, files in os.walk('Content/descriptions'): 37 | for f in files: 38 | for line in open(os.path.join(root, f), 'r', encoding='utf_8_sig'): 39 | line = line.strip() 40 | if len(line) > 0 and line not in original: 41 | original.add(line) 42 | if line in trans: 43 | data.append(trans[line]) 44 | else: 45 | data.append({'FileName': f, 'English': line}) 46 | 47 | translation = Translation() 48 | translation.set_data(data, ('FileName', 'English', 'German', 'French', 'Russian', 'Chinese', 'Japanese')) 49 | translation.save('EXAPUNKS_descriptions.json') 50 | 51 | 52 | if __name__ == '__main__': 53 | export_vignettes() 54 | export_descriptions() 55 | -------------------------------------------------------------------------------- /font/bmfont/__init__.py: -------------------------------------------------------------------------------- 1 | from .bmfont import * 2 | from .bmfont_gen import * 3 | -------------------------------------------------------------------------------- /font/bmfont/bmfont.com: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/font/bmfont/bmfont.com -------------------------------------------------------------------------------- /font/bmfont/bmfont.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/font/bmfont/bmfont.exe -------------------------------------------------------------------------------- /font/bmfont/bmfont.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # coding=utf-8 3 | from .bmfont_txt import FntTxt 4 | from .bmfont_bin import FntBin 5 | from .bmfont_xml import FntXml 6 | import os 7 | from PIL import Image 8 | 9 | 10 | class Fnt(FntTxt, FntBin, FntXml): 11 | _class = { 12 | "bin": FntBin, 13 | "xml": FntXml, 14 | "txt": FntTxt, 15 | } 16 | 17 | def load(self, io): 18 | sign = io.read(3) 19 | io.seek(0, os.SEEK_SET) 20 | 21 | if sign == FntBin.SIGNATURE: 22 | self.format = "bin" 23 | elif sign == FntXml.SIGNATURE: 24 | self.format = "xml" 25 | else: 26 | self.format = "txt" 27 | self._class[self.format].load(self, io) 28 | 29 | def save(self, io): 30 | self._class[self.format].save(self, io) 31 | 32 | def convert(self, format): 33 | self.format = format 34 | self._class[format].convert(self, self) 35 | 36 | def __str__(self): 37 | return str(self.info) + str(self.common) + str(self.pages) + str(self.chars) 38 | 39 | def get_chars(self): 40 | return u"".join([chr(c["id"]) for c in self.chars]) 41 | 42 | def dump_chars(self): 43 | ims = [] 44 | for page in self.pages: 45 | im = Image.open(page) 46 | ims.append(im) 47 | 48 | for c in self.chars: 49 | im = ims[c["page"]] 50 | cim = im.crop((c["x"], c["y"], c["x"] + c["width"], c["y"] + c["height"])) 51 | cim.save("%04x.png" % c["id"]) 52 | 53 | 54 | if __name__ == "__main__": 55 | import argparse 56 | 57 | parser = argparse.ArgumentParser() 58 | parser.add_argument("name", action="store", nargs=1) 59 | parser.add_argument("--dump_chars", action="store_true", default=False) 60 | args = parser.parse_args() 61 | 62 | fnt = Fnt(open(args.name[0], "rb")) 63 | if args.dump_chars: 64 | fnt.dump_chars() 65 | else: 66 | print(fnt) 67 | -------------------------------------------------------------------------------- /font/bmfont/bmfont_base.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # coding=utf-8 3 | from io import StringIO 4 | 5 | 6 | class Base: 7 | def __init__(self, io=None): 8 | if io is not None: 9 | self.load(io) 10 | 11 | def load(self, io): 12 | raise NotImplementedError 13 | 14 | def save(self, io): 15 | raise NotImplementedError 16 | 17 | def get_data(self): 18 | io = StringIO() 19 | self.save(io) 20 | return io.getvalue() 21 | 22 | 23 | class InfoBase(Base): 24 | face = "" 25 | size = 32 26 | bold = 0 27 | italic = 0 28 | charset = "" 29 | unicode = 1 30 | stretchH = 100 31 | smooth = 1 32 | aa = 1 33 | padding = [0, 0, 0, 0] 34 | spacing = [1, 1] 35 | outline = 0 36 | 37 | def copy(self, info): 38 | self.face = info.face 39 | self.size = info.size 40 | self.bold = info.bold 41 | self.italic = info.italic 42 | self.charset = info.charset 43 | self.unicode = info.unicode 44 | self.stretchH = info.stretchH 45 | self.smooth = info.smooth 46 | self.aa = info.aa 47 | self.padding = info.padding 48 | self.spacing = info.spacing 49 | self.outline = info.outline 50 | 51 | def __str__(self): 52 | values = InfoBase.__dict__ 53 | values.update(self.__dict__) 54 | return " face:%(face)s\n"\ 55 | " size:%(size)s\n"\ 56 | " bold:%(bold)s\n"\ 57 | " italic:%(italic)s\n"\ 58 | " charset:%(charset)s\n"\ 59 | " unicode:%(unicode)s\n"\ 60 | "stretchH:%(stretchH)s\n"\ 61 | " smooth:%(smooth)s\n"\ 62 | " aa:%(aa)s\n"\ 63 | " padding:%(padding)s\n"\ 64 | " spacing:%(spacing)s\n"\ 65 | " outline:%(outline)s\n"\ 66 | % values 67 | 68 | 69 | class CommonBase(Base): 70 | lineHeight = 32 71 | base = 26 72 | scaleW = 256 73 | scaleH = 256 74 | pages = 1 75 | packed = 0 76 | alphaChnl = 1 77 | redChnl = 0 78 | greenChnl = 0 79 | blueChnl = 0 80 | 81 | def copy(self, common): 82 | self.lineHeight = common.lineHeight 83 | self.base = common.base 84 | self.scaleW = common.scaleW 85 | self.scaleH = common.scaleH 86 | self.pages = common.pages 87 | self.packed = common.packed 88 | self.alphaChnl = common.alphaChnl 89 | self.redChnl = common.redChnl 90 | self.greenChnl = common.greenChnl 91 | self.blueChnl = common.blueChnl 92 | 93 | def __str__(self): 94 | values = CommonBase.__dict__ 95 | values.update(self.__dict__) 96 | return "lineHeight:%(lineHeight)s\n"\ 97 | " base:%(base)s\n"\ 98 | " scaleW:%(scaleW)s\n"\ 99 | " scaleH:%(scaleH)s\n"\ 100 | " pages:%(pages)s\n"\ 101 | " packed:%(packed)s\n"\ 102 | " alphaChnl:%(alphaChnl)s\n"\ 103 | " redChnl:%(redChnl)s\n"\ 104 | " greenChnl:%(greenChnl)s\n"\ 105 | " blueChnl:%(blueChnl)s\n" % values 106 | 107 | 108 | class PagesBase(Base, list): 109 | def copy(self, obj): 110 | for i in range(len(self)): 111 | self.pop() 112 | for x in obj: 113 | self.append(x) 114 | 115 | def __str__(self): 116 | return "\n".join(self) + "\n" 117 | 118 | 119 | class CharsBase(Base, list): 120 | def copy(self, obj): 121 | for i in range(len(self)): 122 | self.pop() 123 | for x in obj: 124 | self.append(x) 125 | 126 | def __str__(self): 127 | return "\n".join([str(c) for c in self]) + "\n" 128 | 129 | 130 | class KerningsBase(Base, list): 131 | def copy(self, obj): 132 | for i in range(len(self)): 133 | self.pop() 134 | for x in obj: 135 | self.append(x) 136 | 137 | 138 | class FntBase(Base): 139 | version = 3 140 | -------------------------------------------------------------------------------- /font/bmfont/bmfont_bin.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # coding=utf-8 3 | from .bmfont_base import * 4 | from io import StringIO 5 | from struct import unpack, pack 6 | 7 | 8 | def _get_str(io): 9 | s = "" 10 | t = io.read(1) 11 | while t != "" and t != "\x00": 12 | s += t 13 | t = io.read(1) 14 | return s 15 | 16 | 17 | class InfoBin(InfoBase): 18 | def load(self, io): 19 | self.size, bit_field, charset, self.stretchH, self.aa, \ 20 | self.padding[0], self.padding[1], self.padding[2], self.padding[3], \ 21 | self.spacing[0], self.spacing[1], self.outline \ 22 | = unpack("> 7 25 | self.unicode = (bit_field >> 6) & 1 26 | self.italic = (bit_field >> 5) & 1 27 | self.bold = (bit_field >> 4) & 1 28 | 29 | self.face = _get_str(io) 30 | 31 | def save(self, io): 32 | bit_field = (self.smooth << 7) | (self.unicode << 6) | (self.italic << 5) | (self.bold << 4) 33 | io.write(pack(" 1: 75 | w, h = self.get_texture_size() 76 | if w > h: 77 | self.set_texture_size(w, h * 2) 78 | else: 79 | self.set_texture_size(w * 2, h) 80 | fnt = self.gen() 81 | for c in fnt.chars: 82 | c["w"] = c["width"] 83 | c["h"] = c["height"] 84 | c["x0"] = c["x"] 85 | c["y0"] = c["y"] 86 | c["x1"] = c["x0"] + c["w"] 87 | c["y1"] = c["y0"] + c["h"] 88 | c["left"] = c["xoffset"] 89 | c["top"] = c["yoffset"] 90 | c["adv"] = c["xadvance"] 91 | self.im = Image.open(fnt.pages[0]) 92 | infos = dict(zip([chr(c["id"]) for c in fnt.chars], fnt.chars)) 93 | for t in texts: 94 | if t not in infos: 95 | infos[t] = infos[" "].copy() 96 | return infos 97 | 98 | def GetRgbaImage(self): 99 | return self.im 100 | 101 | def GetImage(self): 102 | return self.im.split()[3] 103 | 104 | def __setitem__(self, key, value): 105 | self.args[key] = value 106 | 107 | def __getitem__(self, key): 108 | return self.args[key] 109 | 110 | def set_chars(self, chars): 111 | self.chars = chars 112 | self.adjust_texture_size() 113 | 114 | def get_bmfc(self): 115 | io = StringIO() 116 | for k, v in self.args.items(): 117 | io.write("%s=%s\n" % (k, str(v))) 118 | 119 | chars = [] 120 | for i, c in enumerate(self.chars): 121 | if i % 16 == 15: 122 | io.write("chars=%s\n" % (",".join(chars))) 123 | chars = [] 124 | chars.append("%d" % (ord(c))) 125 | io.write("chars=%s\n\n" % (",".join(chars))) 126 | for icon in self.icons: 127 | io.write('icon="%s",%d,%d,%d,%d\n' % (icon["name"], icon["id"], icon["xoffset"], icon["yoffset"], icon["xadvance"])) 128 | return io.getvalue() 129 | 130 | def save_bmfc(self, io): 131 | io.write(self.get_bmfc()) 132 | 133 | def clone(self, fnt): 134 | # self.set_font_size(-fnt.common.lineHeight*0.8) 135 | self.set_font_size(fnt.info.size) 136 | self.set_font_bold(fnt.info.bold) 137 | self.set_font_italic(fnt.info.italic) 138 | if fnt.info.outline > 3: 139 | self.set_outline(3) 140 | else: 141 | self.set_outline(fnt.info.outline) 142 | self["alphaChnl"] = fnt.common.alphaChnl 143 | self["redChnl"] = fnt.common.redChnl 144 | self["greenChnl"] = fnt.common.greenChnl 145 | self["blueChnl"] = fnt.common.blueChnl 146 | self.adjust_texture_size() 147 | self["fontDescFormat"] = {"txt": 0, "xml": 1, "bin": 2}[fnt.format] 148 | self["paddingDown"], self["paddingUp"], self["paddingRight"], self["paddingLeft"] = fnt.info.padding 149 | try: 150 | self.set_texture_format(os.path.splitext(fnt.pages[0])[1][1:]) 151 | except BaseException: 152 | pass 153 | 154 | def gen(self, fnt_name=None): 155 | if fnt_name is None: 156 | name = self.uuid 157 | fnt_name = name + '.fnt' 158 | bmfc_name = name + '.bmfc' 159 | else: 160 | bmfc_name = os.path.splitext(fnt_name)[0] + '.bmfc' 161 | self.save_bmfc(open(bmfc_name, "w")) 162 | if os.path.exists(fnt_name): 163 | os.remove(fnt_name) 164 | os.system('"%s" -c %s -o %s' % (BMFONT_COM, bmfc_name, fnt_name)) 165 | fnt = Fnt(open(fnt_name, "r")) 166 | return fnt 167 | 168 | def clear(self, fnt_name=None): 169 | if fnt_name is None: 170 | name = self.uuid 171 | fnt_name = name + '.fnt' 172 | bmfc_name = name + '.bmfc' 173 | else: 174 | bmfc_name = os.path.splitext(fnt_name)[0] + '.bmfc' 175 | 176 | os.remove(bmfc_name) 177 | fnt = Fnt(open(fnt_name, "r")) 178 | for page in fnt.pages: 179 | os.remove(page) 180 | os.remove(fnt_name) 181 | 182 | def set_font_name(self, name): 183 | self["fontName"] = name 184 | 185 | def set_font_desc_format(self, fmt): 186 | self["fontDescFormat"] = {"txt": 0, "xml": 1, "bin": 2}[fmt] 187 | 188 | def set_font_size(self, size): 189 | self["fontSize"] = size 190 | 191 | def set_font_bold(self, bold): 192 | if isinstance(bold, bool): 193 | bold = "1" if bold else "0" 194 | self["isBold"] = bold 195 | 196 | def set_font_italic(self, italic): 197 | if isinstance(italic, bool): 198 | italic = "1" if italic else "0" 199 | self["isItalic"] = italic 200 | 201 | def set_texture_format(self, format): 202 | valid_format = ("png", "tga", "dds") 203 | if format not in valid_format: 204 | raise TypeError("valid format: %s" % (str(valid_format))) 205 | self["textureFormat"] = format 206 | 207 | def set_texture_compression(self, compression): 208 | valid_compression = ("none", "dxt1", "dxt3", "dxt5") 209 | if compression not in valid_compression: 210 | raise TypeError("valid compression: %s" % (str(valid_compression))) 211 | self["textureCompression"] = valid_compression.index(compression) 212 | 213 | def set_texture_size(self, width, height): 214 | self["outWidth"] = width 215 | self["outHeight"] = height 216 | 217 | def get_texture_size(self): 218 | return self["outWidth"], self["outHeight"] 219 | 220 | def set_fixed_height(self, is_fixed_height): 221 | self["useFixedHeight"] = "%d" % is_fixed_height 222 | 223 | def set_force_zero(self, is_force_zero): 224 | self["forceZero"] = "%d" % is_force_zero 225 | 226 | def set_outline(self, outline): 227 | self["outlineThickness"] = outline 228 | if outline == 0: 229 | self["alphaChnl"] = 0 230 | else: 231 | self["alphaChnl"] = 1 232 | 233 | def set_enable_kernings(self, enable_kernings): 234 | self["dontIncludeKerningPairs"] = "%d" % (not enable_kernings) 235 | 236 | def adjust_texture_size(self): 237 | font_size = int(abs(self["fontSize"])) 238 | if self["fontSize"] > 0: 239 | font_size = int(font_size * 0.8) 240 | minsize = (font_size + int(self["paddingDown"]) + int(self["paddingUp"])) * \ 241 | (font_size + int(self["paddingRight"]) + int(self["paddingLeft"])) * len(self.chars) 242 | minsize *= 1.1 243 | w = h = 2 244 | while w * h < minsize: 245 | if w > h: 246 | h *= 2 247 | else: 248 | w *= 2 249 | self.set_texture_size(w, h) 250 | 251 | def add_icon(self, name, id, xoffset=0, yoffset=0, xadvance=0): 252 | self.icons.append({"name": name, "id": id, "xoffset": xoffset, "yoffset": yoffset, "xadvance": xadvance}) 253 | 254 | def set_spacing(self, v): 255 | self["spacingHoriz"] = self["spacingVert"] = v 256 | 257 | def set_padding(self, v): 258 | self["paddingDown"] = self["paddingUp"] = self["paddingRight"] = self["paddingLeft"] = v 259 | 260 | def set_four_chnl_packed(self): 261 | self["fourChnlPacked"] = 1 262 | self["alphaChnl"] = 1 263 | self["redChnl"] = 1 264 | self["greenChnl"] = 1 265 | self["blueChnl"] = 1 266 | 267 | 268 | def FixFnt(new_fnt, old_fnt): 269 | new_fnt.info.face = old_fnt.info.face 270 | new_fnt.info.unicode = 1 271 | return new_fnt 272 | 273 | 274 | if __name__ == "__main__": 275 | fontgen = FontGenerator() 276 | fontgen.set_chars(u"abc") 277 | print(fontgen.gen()) 278 | -------------------------------------------------------------------------------- /font/bmfont/bmfont_txt.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # coding=utf-8 3 | from .bmfont_base import * 4 | import re 5 | 6 | 7 | def _get_str(s, tag): 8 | return re.search(r'%s="?(.*?)[" ]' % tag, s).group(1) 9 | 10 | 11 | def _get_int(s, tag): 12 | m = re.search(r"%s=(-?\d+)" % tag, s) 13 | if m: 14 | return int(m.group(1)) 15 | else: 16 | return 0 17 | 18 | 19 | def _get_list(s, tag, num): 20 | p = r"%s=" % tag + ",".join([r"(\d+)" for i in range(num)]) 21 | m = re.search(p, s) 22 | return [int(x) for x in m.groups()] 23 | 24 | 25 | class InfoTxt(InfoBase): 26 | def load(self, io): 27 | line = io.readline() 28 | self.face = _get_str(line, "face") 29 | self.size = _get_int(line, "size") 30 | self.bold = _get_int(line, "bold") 31 | self.italic = _get_int(line, "italic") 32 | self.charset = _get_str(line, "charset") 33 | self.unicode = _get_int(line, "unicode") 34 | self.stretchH = _get_int(line, "stretchH") 35 | self.smooth = _get_int(line, "smooth") 36 | self.aa = _get_int(line, "aa") 37 | self.padding = _get_list(line, "padding", 4) 38 | self.spacing = _get_list(line, "spacing", 2) 39 | self.outline = _get_int(line, "outline") 40 | 41 | def save(self, io): 42 | io.write('info face="%s" size=%d bold=%d italic=%d charset="%s" unicode=%d stretchH=%d smooth=%d aa=%d padding=%d,%d,%d,%d spacing=%d,%d outline=%d\n' % ( 43 | self.face, 44 | self.size, 45 | self.bold, 46 | self.italic, 47 | self.charset, 48 | self.unicode, 49 | self.stretchH, 50 | self.smooth, 51 | self.aa, 52 | self.padding[0], 53 | self.padding[1], 54 | self.padding[2], 55 | self.padding[3], 56 | self.spacing[0], 57 | self.spacing[1], 58 | self.outline, 59 | )) 60 | 61 | 62 | class CommonTxt(CommonBase): 63 | def load(self, io): 64 | line = io.readline() 65 | self.lineHeight = _get_int(line, "lineHeight") 66 | self.base = _get_int(line, "base") 67 | self.scaleW = _get_int(line, "scaleW") 68 | self.scaleH = _get_int(line, "scaleH") 69 | self.pages = _get_int(line, "pages") 70 | self.packed = _get_int(line, "packed") 71 | self.alphaChnl = _get_int(line, "alphaChnl") 72 | self.redChnl = _get_int(line, "redChnl") 73 | self.greenChnl = _get_int(line, "greenChnl") 74 | self.blueChnl = _get_int(line, "blueChnl") 75 | 76 | def save(self, io): 77 | io.write('common lineHeight=%d base=%d scaleW=%d scaleH=%d pages=%d packed=%d alphaChnl=%d redChnl=%d greenChnl=%d blueChnl=%d\n' % ( 78 | self.lineHeight, 79 | self.base, 80 | self.scaleW, 81 | self.scaleH, 82 | self.pages, 83 | self.packed, 84 | self.alphaChnl, 85 | self.redChnl, 86 | self.greenChnl, 87 | self.blueChnl, 88 | )) 89 | 90 | 91 | class PagesTxt(PagesBase): 92 | def __init__(self, io=None, num=1): 93 | if io: 94 | self.load(io, num) 95 | 96 | def load(self, io, num): 97 | for i in range(num): 98 | line = io.readline() 99 | id = _get_int(line, "id") 100 | name = _get_str(line, "file") 101 | if id != i: 102 | raise 103 | self.append(name) 104 | 105 | def save(self, io): 106 | for i, name in enumerate(self): 107 | io.write('page id=%d file="%s"\n' % (i, name)) 108 | 109 | 110 | class CharsTxt(CharsBase): 111 | def load(self, io): 112 | count = _get_int(io.readline(), "count") 113 | for i in range(count): 114 | line = io.readline() 115 | char = {} 116 | char["id"] = _get_int(line, "id") 117 | char["x"] = _get_int(line, "x") 118 | char["y"] = _get_int(line, "y") 119 | char["width"] = _get_int(line, "width") 120 | char["height"] = _get_int(line, "height") 121 | char["xoffset"] = _get_int(line, "xoffset") 122 | char["yoffset"] = _get_int(line, "yoffset") 123 | char["xadvance"] = _get_int(line, "xadvance") 124 | char["page"] = _get_int(line, "page") 125 | char["chnl"] = _get_int(line, "chnl") 126 | self.append(char) 127 | 128 | def save(self, io): 129 | io.write("chars count=%d\n" % len(self)) 130 | for char in self: 131 | io.write("char id=%-4d x=%-5d y=%-5d width=%-5d height=%-5d xoffset=%-5d yoffset=%-5d xadvance=%-5d page=%-2d chnl=%-2d\n" % ( 132 | char["id"], 133 | char["x"], 134 | char["y"], 135 | char["width"], 136 | char["height"], 137 | char["xoffset"], 138 | char["yoffset"], 139 | char["xadvance"], 140 | char["page"], 141 | char["chnl"], 142 | )) 143 | 144 | 145 | class KerningsTxt(KerningsBase): 146 | def load(self, io): 147 | try: 148 | count = _get_int(io.readline(), "count") 149 | except StopIteration: 150 | count = 0 151 | for i in range(count): 152 | line = io.next() 153 | kerning = {} 154 | kerning["first"] = _get_int(line, "first") 155 | kerning["second"] = _get_int(line, "second") 156 | kerning["amount"] = _get_int(line, "amount") 157 | self.append(kerning) 158 | 159 | def save(self, io): 160 | io.write("kernings count=%d\n" % len(self)) 161 | for kerning in self: 162 | io.write("kerning first=%-3d second=%-3d amount=%-4d\n" % ( 163 | kerning["first"], 164 | kerning["second"], 165 | kerning["amount"], 166 | )) 167 | 168 | 169 | class FntTxt(FntBase): 170 | def load(self, io): 171 | self.info = InfoTxt(io) 172 | self.common = CommonTxt(io) 173 | self.pages = PagesTxt(io, self.common.pages) 174 | self.chars = CharsTxt(io) 175 | self.kernings = KerningsTxt(io) 176 | 177 | def save(self, io): 178 | self.info.save(io) 179 | self.common.save(io) 180 | self.pages.save(io) 181 | self.chars.save(io) 182 | self.kernings.save(io) 183 | 184 | def convert(self, fnt): 185 | info = InfoTxt() 186 | info.copy(fnt.info) 187 | self.info = info 188 | 189 | common = CommonTxt() 190 | common.copy(fnt.common) 191 | self.common = common 192 | 193 | pages = PagesTxt() 194 | pages.copy(fnt.pages) 195 | self.pages = pages 196 | 197 | chars = CharsTxt() 198 | chars.copy(fnt.chars) 199 | self.chars = chars 200 | 201 | kernings = KerningsTxt() 202 | kernings.copy(fnt.kernings) 203 | self.kernings = kernings 204 | -------------------------------------------------------------------------------- /font/bmfont/bmfont_xml.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # coding=utf-8 3 | from .bmfont_base import * 4 | from xml.etree import ElementTree 5 | from xml.dom import minidom 6 | from io import StringIO 7 | 8 | 9 | def prettify(elem): 10 | s = ElementTree.tostring(elem, "utf-8") 11 | reparsed = minidom.parseString(s) 12 | return reparsed.toprettyxml(indent=" ") 13 | 14 | 15 | class InfoXml(InfoBase): 16 | def load(self, node): 17 | self.face = node.attrib.get("face") 18 | self.size = int(node.attrib.get("size")) 19 | self.bold = int(node.attrib.get("bold")) 20 | self.italic = int(node.attrib.get("italic")) 21 | self.charset = node.attrib.get("charset") 22 | self.unicode = int(node.attrib.get("unicode")) 23 | self.stretchH = int(node.attrib.get("stretchH")) 24 | self.smooth = int(node.attrib.get("smooth")) 25 | self.aa = int(node.attrib.get("aa")) 26 | self.padding = [int(x) for x in node.attrib.get("padding").split(",")] 27 | self.spacing = [int(x) for x in node.attrib.get("spacing").split(",")] 28 | self.outline = int(node.attrib.get("outline")) 29 | 30 | def save(self, root): 31 | info = ElementTree.SubElement(root, "info") 32 | info.set("face", self.face) 33 | info.set("size", str(self.size)) 34 | info.set("smooth", str(self.smooth)) 35 | info.set("unicode", str(self.unicode)) 36 | info.set("italic", str(self.italic)) 37 | info.set("bold", str(self.bold)) 38 | info.set("charset", self.charset) 39 | info.set("stretchH", str(self.stretchH)) 40 | info.set("aa", str(self.aa)) 41 | info.set("padding", ",".join([str(x) for x in self.padding])) 42 | info.set("spacing", ",".join([str(x) for x in self.spacing])) 43 | info.set("outline", str(self.outline)) 44 | 45 | 46 | class CommonXml(CommonBase): 47 | def load(self, node): 48 | self.lineHeight = int(node.attrib.get("lineHeight")) 49 | self.base = int(node.attrib.get("base")) 50 | self.scaleW = int(node.attrib.get("scaleW")) 51 | self.scaleH = int(node.attrib.get("scaleH")) 52 | self.pages = int(node.attrib.get("pages")) 53 | self.packed = int(node.attrib.get("packed")) 54 | try: 55 | self.alphaChnl = int(node.attrib.get("alphaChnl")) 56 | self.redChnl = int(node.attrib.get("redChnl")) 57 | self.greenChnl = int(node.attrib.get("greenChnl")) 58 | self.blueChnl = int(node.attrib.get("blueChnl")) 59 | except TypeError: 60 | self.alphaChnl = 1 61 | self.redChnl = 0 62 | self.greenChnl = 0 63 | self.blueChnl = 0 64 | 65 | def save(self, root): 66 | common = ElementTree.SubElement(root, "common") 67 | common.set("lineHeight", str(self.lineHeight)) 68 | common.set("base", str(self.base)) 69 | common.set("scaleW", str(self.scaleW)) 70 | common.set("scaleH", str(self.scaleH)) 71 | common.set("pages", str(self.pages)) 72 | common.set("packed", str(self.packed)) 73 | common.set("alphaChnl", str(self.alphaChnl)) 74 | common.set("redChnl", str(self.redChnl)) 75 | common.set("greenChnl", str(self.greenChnl)) 76 | common.set("blueChnl", str(self.blueChnl)) 77 | 78 | 79 | class PagesXml(PagesBase): 80 | def load(self, node): 81 | names = {} 82 | for page in node.iter("page"): 83 | id = int(page.attrib.get("id")) 84 | names[id] = page.attrib.get("file") 85 | 86 | for i in range(len(names)): 87 | self.append(names[i]) 88 | 89 | def save(self, root): 90 | pages = ElementTree.SubElement(root, "pages") 91 | for i, name in enumerate(self): 92 | page = ElementTree.SubElement(pages, "page") 93 | page.set("id", str(i)) 94 | page.set("file", name) 95 | 96 | 97 | class CharsXml(CharsBase): 98 | def load(self, node): 99 | for char in node.iter("char"): 100 | for key, value in char.attrib.items(): 101 | char.attrib[key] = int(value) 102 | self.append(char.attrib) 103 | 104 | def save(self, root): 105 | chars = ElementTree.SubElement(root, "chars") 106 | chars.set("count", str(len(self))) 107 | 108 | self.sort(key=lambda x: int(x["id"])) 109 | for c in self: 110 | char = ElementTree.SubElement(chars, "char") 111 | char.set("id", str(c["id"])) 112 | char.set("x", str(c["x"])) 113 | char.set("y", str(c["y"])) 114 | char.set("width", str(c["width"])) 115 | char.set("height", str(c["height"])) 116 | char.set("xoffset", str(c["xoffset"])) 117 | char.set("yoffset", str(c["yoffset"])) 118 | char.set("xadvance", str(c["xadvance"])) 119 | char.set("page", str(c["page"])) 120 | char.set("chnl", str(c["chnl"])) 121 | 122 | 123 | class KerningsXml(KerningsBase): 124 | def load(self, node): 125 | for kerning in node.iter("kerning"): 126 | for key, value in kerning.attrib.items(): 127 | kerning.attrib[key] = int(value) 128 | self.append(kerning.attrib) 129 | 130 | def save(self, root): 131 | kernings = ElementTree.SubElement(root, "kernings") 132 | kernings.set("count", str(len(self))) 133 | for k in self: 134 | kerning = ElementTree.SubElement(kernings, "kerning") 135 | kerning.set("first", str(k["first"])) 136 | kerning.set("second", str(k["second"])) 137 | kerning.set("amount", str(k["amount"])) 138 | 139 | 140 | class FntXml(FntBase): 141 | SIGNATURE = " %s' % (input_name, output_name)) 18 | Translation(input_name).save(output_name) 19 | 20 | 21 | def convert_all(input_ext, output_ext, auto=False): 22 | input_ext = input_ext.lower() 23 | output_ext = output_ext.lower() 24 | for root, dirs, files in os.walk('./'): 25 | for f in files: 26 | if '~$' in f: # excel temp file 27 | continue 28 | input_name = os.path.join(root, f) 29 | name, ext = os.path.splitext(input_name) 30 | if ext.lower() == input_ext: 31 | output_name = name + output_ext 32 | convert(input_name, output_name, auto) 33 | -------------------------------------------------------------------------------- /import_txt/excel2json.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import argparse 5 | from convert import convert, convert_all 6 | 7 | 8 | if __name__ == '__main__': 9 | parser = argparse.ArgumentParser() 10 | parser.add_argument('excel_name', action='store', nargs="?") 11 | parser.add_argument('json_name', action='store', nargs="?") 12 | parser.add_argument('--auto', action='store_true') 13 | args = parser.parse_args() 14 | 15 | if args.excel_name is not None: 16 | json_name = args.json_name 17 | if json_name is None: 18 | json_name = os.path.splitext(args.excel_name)[0] + '.json' 19 | convert(args.excel_name, json_name, args.auto) 20 | else: 21 | convert_all('.xlsx', '.json', args.auto) 22 | -------------------------------------------------------------------------------- /import_txt/get_chars.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | 5 | if __name__ == '__main__': 6 | chars = set('·’') 7 | chars |= set([chr(i) for i in range(0x20, 0x100)]) 8 | for root, dirs, files in os.walk('./'): 9 | for f in files: 10 | ext = os.path.splitext(f)[1].lower() 11 | if ext == '.json': 12 | chars |= set(open(os.path.join(root, f), 'r', encoding='utf-8').read()) 13 | 14 | chars_list = sorted(filter(lambda x: ord(x) >= 0x20, chars)) 15 | open('../font/chars.txt', 'w', encoding='utf-8').write(''.join(chars_list)) 16 | -------------------------------------------------------------------------------- /import_txt/history.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from pydriller import GitRepository, RepositoryMining 4 | import argparse 5 | from translation import Translation 6 | import os 7 | import pandas as pd 8 | 9 | 10 | if __name__ == '__main__': 11 | parser = argparse.ArgumentParser(epilog='for example: history.py some.json English Chinese') 12 | parser.add_argument('json_name', action='store', nargs=1, help='the json file name') 13 | parser.add_argument('original', action='store', nargs=1, help='the column name for original') 14 | parser.add_argument('translation', action='store', nargs=1, help='the column name for translation') 15 | parser.add_argument('repo', action='store', default='../', nargs='?', help='the path of repository') 16 | args = parser.parse_args() 17 | 18 | df = Translation(args.json_name[0]).get_dataframe() 19 | start_dropping = False 20 | for col in df.columns: 21 | if start_dropping: 22 | if col == args.translation[0]: 23 | old_translation = df[args.translation[0]] 24 | df.drop(columns=[col], inplace=True) 25 | elif col == args.original[0]: 26 | start_dropping = True 27 | 28 | gr = GitRepository('./') 29 | commits = gr.get_commits_modified_file(args.json_name[0]) 30 | for commit in RepositoryMining('../', only_commits=commits).traverse_commits(): 31 | for modification in commit.modifications: 32 | if (modification.filename == args.json_name[0]): 33 | date = commit.committer_date.strftime('%m-%d') 34 | author = commit.author.name 35 | print(date, author) 36 | 37 | source = modification.source_code 38 | try: 39 | _df = pd.read_json(source) 40 | except BaseException: 41 | print("WARNING: can't load") 42 | continue 43 | 44 | new_translation = _df[args.translation[0]] 45 | for i, old in enumerate(old_translation): 46 | if old == new_translation[i]: 47 | new_translation[i] = '' 48 | else: 49 | old_translation[i] = new_translation[i] 50 | df[f'{date} {author}'] = new_translation 51 | break 52 | 53 | df[f'Current {args.translation[0]}'] = old_translation 54 | xlsx_name = os.path.splitext(args.json_name[0])[0] + '_history.xlsx' 55 | tr = Translation() 56 | tr.set_dataframe(df) 57 | tr.save(xlsx_name) 58 | print(f'{xlsx_name} saved') 59 | -------------------------------------------------------------------------------- /import_txt/import_txt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | from io import StringIO 5 | import csv 6 | import re 7 | from translation import Translation 8 | 9 | 10 | def print_percent(name, lang='Chinese'): 11 | print('%-30s%6.2f%%' % (name, Translation(name).get_percent(lang))) 12 | 13 | 14 | def insert_dot(trans): 15 | PUNCTUATIONS = ',。!;”-…》】)' 16 | for key, values in trans.items(): 17 | for lang in ('Chinese', 'Japanese'): 18 | text = values[lang] 19 | if len(text) > 0: 20 | s = '' 21 | for i, c in enumerate(text[:-1]): 22 | s += c 23 | if ord(c) > 0xff and text[i + 1] not in PUNCTUATIONS: 24 | s += '●' 25 | trans[key][lang] = s + text[-1] 26 | return trans 27 | 28 | 29 | def import_strings(): 30 | print_percent('EXAPUNKS_exe.json') 31 | for v in Translation('EXAPUNKS_exe.json').check_variables(regex=r'\{\d*\}', 32 | org_index='English', 33 | trans_index='Chinese', 34 | ordered=False): 35 | print('Warning: ', v) 36 | trans = Translation('EXAPUNKS_exe.json').get_translation() 37 | trans = insert_dot(trans) 38 | csv_writer = csv.writer(open('strings.csv', 'w', encoding='utf-8'), lineterminator='\n', escapechar='\\') 39 | for key, value in trans.items(): 40 | row = [key, ''] 41 | row.extend([value[lang] for lang in ('German', 'French', 'Russian', 'Chinese', 'Japanese')]) 42 | csv_writer.writerow(row) 43 | 44 | 45 | def import_vignettes(): 46 | print_percent('EXAPUNKS_vignettes.json') 47 | trans = Translation('EXAPUNKS_vignettes.json').get_translation() 48 | trans = insert_dot(trans) 49 | for root, dirs, files in os.walk('../export_txt/Content/vignettes'): 50 | for f in files: 51 | name = os.path.join(root, f) 52 | out = StringIO() 53 | csv_reader = csv.reader(open(name, 'r', encoding='utf_8_sig')) 54 | csv_writer = csv.writer(out, lineterminator='\n', escapechar='\\') 55 | need_save = False 56 | for row in csv_reader: 57 | if len(row) > 1: 58 | en = row[1] 59 | if en in trans and len(trans[en]) > 0: 60 | need_save = True 61 | row.extend([trans[en][lang] for lang in ('French', 'Chinese', 'Japanese')]) 62 | csv_writer.writerow(row) 63 | if need_save: 64 | name = name.replace('../export_txt', '../patch') 65 | try: 66 | os.makedirs(os.path.split(name)[0]) 67 | except BaseException: 68 | pass 69 | open(name, 'w', encoding='utf_8_sig').write(out.getvalue()) 70 | 71 | 72 | def import_descriptions(): 73 | LANGS = {'German': 'de', 74 | 'French': 'fr', 75 | 'Russian': 'ru', 76 | 'Chinese': 'zh', 77 | 'Japanese': 'ja' 78 | } 79 | 80 | print_percent('EXAPUNKS_descriptions.json') 81 | trans = Translation('EXAPUNKS_descriptions.json').get_translation() 82 | trans = insert_dot(trans) 83 | for root, dirs, files in os.walk('../export_txt/Content/descriptions/en'): 84 | for f in files: 85 | name = os.path.join(root, f) 86 | lines = open(name, 'r', encoding='utf_8_sig').readlines() 87 | for country, abbr in LANGS.items(): 88 | out = StringIO() 89 | need_save = True 90 | for line in lines: 91 | line = line.strip() 92 | if len(line) > 0 \ 93 | and line in trans \ 94 | and len(trans[line][country]) > 0: 95 | line = trans[line][country] 96 | # need_save = True 97 | out.write(line + '\n') 98 | if need_save: 99 | new_name = name.replace('../export_txt', '../patch').replace('/en', '/' + abbr, 1) 100 | try: 101 | os.makedirs(os.path.split(new_name)[0]) 102 | except BaseException: 103 | pass 104 | open(new_name, 'w', encoding='utf').write(out.getvalue()) 105 | 106 | 107 | if __name__ == '__main__': 108 | import_strings() 109 | import_vignettes() 110 | import_descriptions() 111 | -------------------------------------------------------------------------------- /import_txt/json2excel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import argparse 5 | from convert import convert, convert_all 6 | 7 | 8 | if __name__ == '__main__': 9 | parser = argparse.ArgumentParser() 10 | parser.add_argument('json_name', action='store', nargs="?") 11 | parser.add_argument('excel_name', action='store', nargs="?") 12 | parser.add_argument('--auto', action='store_true') 13 | args = parser.parse_args() 14 | 15 | if args.json_name is not None: 16 | excel_name = args.excel_name 17 | if excel_name is None: 18 | excel_name = os.path.splitext(args.json_name)[0] + '.xlsx' 19 | convert(args.json_name, excel_name, args.auto) 20 | else: 21 | convert_all('.json', '.xlsx', args.auto) 22 | -------------------------------------------------------------------------------- /import_txt/translation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import pandas as pd 4 | import os 5 | import re 6 | import openpyxl 7 | from openpyxl.styles.borders import Border, Side 8 | from openpyxl import Workbook 9 | from typing import Optional, List 10 | 11 | # This font can be gotten from https://github.com/adobe-fonts/source-han-mono/releases 12 | EXCEL_FONT_NAME = 'Source Han Mono' 13 | 14 | 15 | class Theme: 16 | font_name = EXCEL_FONT_NAME 17 | 18 | header_background_color_start = 'FCF3CF' 19 | header_background_color_end = 'FEF9E7' 20 | header_color = '333300' 21 | 22 | original_background_color_start = 'D6EAF8' 23 | original_background_color_end = 'EAFAF1' 24 | original_color = '000033' 25 | 26 | translation_background_color_start = 'D5F5E3' 27 | translation_background_color_end = 'EAFAF1' 28 | translation_color = '003300' 29 | 30 | comment_background_color_start = 'FADBD8' 31 | comment_background_color_end = 'FDEDEC' 32 | comment_color = '330000' 33 | 34 | def __init__(self): 35 | self.header_fill = openpyxl.styles.GradientFill(stop=(self.header_background_color_start, 36 | self.header_background_color_end)) 37 | self.header_font = openpyxl.styles.Font(name=self.font_name, bold=True, color=self.header_color) 38 | 39 | self.org_fill = openpyxl.styles.GradientFill(stop=(self.original_background_color_start, 40 | self.original_background_color_end)) 41 | self.org_font = openpyxl.styles.Font(name=self.font_name, color=self.original_color) 42 | 43 | self.trans_fill = openpyxl.styles.GradientFill(stop=(self.translation_background_color_start, 44 | self.translation_background_color_end)) 45 | self.trans_font = openpyxl.styles.Font(name=self.font_name, color=self.translation_color) 46 | 47 | self.comment_fill = openpyxl.styles.GradientFill(stop=(self.comment_background_color_start, 48 | self.comment_background_color_end)) 49 | self.comment_font = openpyxl.styles.Font(name=self.font_name, color=self.comment_color) 50 | 51 | self.font = openpyxl.styles.Font(name=self.font_name) 52 | 53 | self.border = Border(left=Side(style='hair'), 54 | right=Side(style='hair'), 55 | top=Side(style='hair'), 56 | bottom=Side(style='hair'), 57 | ) 58 | 59 | 60 | class Translation: 61 | def __init__(self, 62 | name: str = None, 63 | theme: Theme = Theme()): 64 | self.theme = theme 65 | if name is not None: 66 | self.load(name) 67 | 68 | def load(self, name: str): 69 | ext = os.path.splitext(name)[1].lower() 70 | if ext == '.xlsx': 71 | _df = pd.read_excel(name, engine='openpyxl') 72 | _df = _df.replace({'_x000D_': '\r'}, regex=True) 73 | elif ext == '.json': 74 | _df = pd.read_json(name) 75 | else: 76 | raise TypeError(f'not support type: "{ext}"') 77 | 78 | self.set_dataframe(_df) 79 | 80 | def save(self, 81 | name: str, 82 | index: str = 'English', 83 | drop_dup: bool = False): 84 | if drop_dup: 85 | self._df.drop_duplicates([index], inplace=True) 86 | ext = os.path.splitext(name)[1].lower() 87 | if ext == '.xlsx': 88 | self.save_excel(name, index) 89 | elif ext == '.json': 90 | self.save_json(name) 91 | else: 92 | raise TypeError(f'not support type: "{ext}"') 93 | 94 | def __set_excel_styles(self, 95 | workbook: Workbook, 96 | frezze_index: int): 97 | trans_rule = openpyxl.formatting.rule.CellIsRule(operator='notEqual', 98 | formula=['""'], 99 | border=self.theme.border, 100 | fill=self.theme.trans_fill, 101 | font=self.theme.trans_font) 102 | comment_rule = openpyxl.formatting.rule.CellIsRule(operator='notEqual', 103 | formula=['""'], 104 | border=self.theme.border, 105 | fill=self.theme.comment_fill, 106 | font=self.theme.comment_font) 107 | 108 | ws = workbook.active 109 | # the global style 110 | for i, row in enumerate(ws.iter_rows()): 111 | for cell in row: 112 | if cell.value is None: 113 | cell.value = '' 114 | elif not isinstance(cell.value, str): 115 | cell.value = str(cell.value) 116 | cell.number_format = '@' 117 | cell.data_type = 's' 118 | cell.quotePrefix = True 119 | 120 | if i == 0: 121 | cell.font = self.theme.header_font 122 | cell.fill = self.theme.header_fill 123 | else: 124 | cell.font = self.theme.font 125 | 126 | # the comments style 127 | comment_letter = openpyxl.utils.get_column_letter(ws.max_column) 128 | if ws[comment_letter + '1'].value == 'Comments': 129 | ws.conditional_formatting.add(f'{comment_letter}2:{comment_letter}{ws.max_row}', comment_rule) 130 | cond_end = openpyxl.utils.get_column_letter(ws.max_column - 1) 131 | else: 132 | cond_end = openpyxl.utils.get_column_letter(ws.max_column) 133 | 134 | # the translation style 135 | cond_start = openpyxl.utils.get_column_letter(frezze_index + 1) 136 | ws.conditional_formatting.add(f'{cond_start}2:{cond_end}{ws.max_row}', trans_rule) 137 | 138 | # the original style 139 | start = openpyxl.utils.get_column_letter(frezze_index) 140 | index_cells = ws[f'{start}2:{start}{ws.max_row}'] 141 | for cell in index_cells: 142 | cell[0].fill = self.theme.org_fill 143 | cell[0].font = self.theme.org_font 144 | cell[0].border = self.theme.border 145 | 146 | def save_excel(self, 147 | name: str, 148 | index: str = 'English'): 149 | frezze_index = self._df.columns.to_list().index(index) + 2 150 | self._df.to_excel(name, freeze_panes=(1, frezze_index), engine='openpyxl') 151 | wb = openpyxl.load_workbook(name) 152 | self.__set_excel_styles(wb, frezze_index) 153 | wb.save(name) 154 | 155 | def save_json(self, name: str): 156 | json_str = self._df.to_json(orient='records', force_ascii=False, indent=4) 157 | open(name, 'w', encoding='utf-8').write(json_str) 158 | 159 | def __process_dataframe(self): 160 | self._df.replace(float('nan'), '', inplace=True) 161 | self._df.drop(columns=filter(lambda x: 'Unnamed' in x or re.search(r'_\d', x), self._df.columns), inplace=True) 162 | 163 | def get_translation(self, 164 | index: str = 'English', 165 | fill_empty_with_org: bool = False, 166 | empty_filter: bool = True): 167 | start_index = self._df.columns.to_list().index(index) + 1 168 | if empty_filter: 169 | rows = filter(lambda x: sum([len(y) for y in x[start_index:]]) > 0, self._df.itertuples(index=False)) 170 | df = pd.DataFrame(rows) 171 | else: 172 | df = self._df.copy() 173 | df.set_index(index, drop=False, inplace=True) 174 | if fill_empty_with_org: 175 | for row in df.iterrows(): 176 | for i, cell in enumerate(row[1]): 177 | if len(cell) == 0: 178 | row[1][i] = row[0] 179 | df.drop_duplicates([index], inplace=True) 180 | return df.to_dict('index') 181 | 182 | def set_dataframe(self, 183 | df: pd.DataFrame, 184 | add_comments: bool = True): 185 | if add_comments and 'Comments' not in df.columns: 186 | df['Comments'] = [''] * len(df) 187 | self._df = df 188 | self.__process_dataframe() 189 | 190 | def get_dataframe(self): 191 | return self._df 192 | 193 | def set_data(self, 194 | data: List, 195 | columns: Optional[List] = None): 196 | if columns is None: 197 | columns = data[0].keys() 198 | self.set_dataframe(pd.DataFrame(data, columns=columns, dtype=str)) 199 | 200 | def get_percent(self, target_index: str): 201 | count = len(self._df[self._df[target_index] != '']) 202 | return count / len(self._df.index) * 100 203 | 204 | def check_variables(self, 205 | regex: str, 206 | org_index: str, 207 | trans_index: str, 208 | ordered: bool = True): 209 | resuls = [] 210 | for i, row in self._df.iterrows(): 211 | org_vars = re.findall(regex, row[org_index], re.MULTILINE | re.DOTALL) 212 | trans_vars = re.findall(regex, row[trans_index], re.MULTILINE | re.DOTALL) 213 | if len(org_vars) != len(trans_vars): 214 | resuls.append([i, org_vars, trans_vars]) 215 | else: 216 | if ordered: 217 | if org_vars != trans_vars: 218 | resuls.append([i, org_vars, trans_vars]) 219 | else: 220 | unordered_org_vars = sorted(org_vars) 221 | unordered_trans_vars = sorted(trans_vars) 222 | if unordered_org_vars != unordered_trans_vars: 223 | resuls.append([i, org_vars, trans_vars]) 224 | return resuls 225 | 226 | def check_size(self, 227 | org_index: str, 228 | trans_index: str, 229 | encoding: str): 230 | result = [] 231 | for i, row in self._df.iterrows(): 232 | org = row[org_index] 233 | trans = row[trans_index] 234 | org_size = len(org.encode(encoding)) 235 | trans_size = len(trans.encode(encoding)) 236 | if org_size < trans_size: 237 | result.append([i, org_size, trans_size, org, trans]) 238 | return result 239 | 240 | def __iter__(self): 241 | for _, row in self._df.iterrows(): 242 | yield row 243 | 244 | 245 | def try_to_get_translation(name: str): 246 | if os.path.exists(name): 247 | return Translation(name).get_translation() 248 | else: 249 | return {} 250 | -------------------------------------------------------------------------------- /manual/convert.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/convert.exe -------------------------------------------------------------------------------- /manual/digital_en_1/digital_en_1_01.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_1/digital_en_1_01.xcf -------------------------------------------------------------------------------- /manual/digital_en_1/digital_en_1_02.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_1/digital_en_1_02.xcf -------------------------------------------------------------------------------- /manual/digital_en_1/digital_en_1_03.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_1/digital_en_1_03.xcf -------------------------------------------------------------------------------- /manual/digital_en_1/digital_en_1_04.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_1/digital_en_1_04.xcf -------------------------------------------------------------------------------- /manual/digital_en_1/digital_en_1_05.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_1/digital_en_1_05.xcf -------------------------------------------------------------------------------- /manual/digital_en_1/digital_en_1_06.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_1/digital_en_1_06.xcf -------------------------------------------------------------------------------- /manual/digital_en_1/digital_en_1_07.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_1/digital_en_1_07.xcf -------------------------------------------------------------------------------- /manual/digital_en_1/digital_en_1_08.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_1/digital_en_1_08.xcf -------------------------------------------------------------------------------- /manual/digital_en_1/digital_en_1_09.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_1/digital_en_1_09.xcf -------------------------------------------------------------------------------- /manual/digital_en_1/digital_en_1_10.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_1/digital_en_1_10.xcf -------------------------------------------------------------------------------- /manual/digital_en_1/digital_en_1_11.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_1/digital_en_1_11.xcf -------------------------------------------------------------------------------- /manual/digital_en_1/digital_en_1_12.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_1/digital_en_1_12.xcf -------------------------------------------------------------------------------- /manual/digital_en_1/digital_en_1_13.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_1/digital_en_1_13.xcf -------------------------------------------------------------------------------- /manual/digital_en_1/digital_en_1_14.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_1/digital_en_1_14.xcf -------------------------------------------------------------------------------- /manual/digital_en_1/digital_en_1_15.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_1/digital_en_1_15.xcf -------------------------------------------------------------------------------- /manual/digital_en_1/digital_en_1_16.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_1/digital_en_1_16.xcf -------------------------------------------------------------------------------- /manual/digital_en_1/digital_en_1_17.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_1/digital_en_1_17.xcf -------------------------------------------------------------------------------- /manual/digital_en_1/digital_en_1_18.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_1/digital_en_1_18.xcf -------------------------------------------------------------------------------- /manual/digital_en_1/digital_en_1_19.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_1/digital_en_1_19.xcf -------------------------------------------------------------------------------- /manual/digital_en_1/digital_en_1_20.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_1/digital_en_1_20.xcf -------------------------------------------------------------------------------- /manual/digital_en_1/digital_en_1_21.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_1/digital_en_1_21.xcf -------------------------------------------------------------------------------- /manual/digital_en_2/digital_en_2_00.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_2/digital_en_2_00.xcf -------------------------------------------------------------------------------- /manual/digital_en_2/digital_en_2_01.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_2/digital_en_2_01.xcf -------------------------------------------------------------------------------- /manual/digital_en_2/digital_en_2_02.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_2/digital_en_2_02.xcf -------------------------------------------------------------------------------- /manual/digital_en_2/digital_en_2_03.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_2/digital_en_2_03.xcf -------------------------------------------------------------------------------- /manual/digital_en_2/digital_en_2_04.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_2/digital_en_2_04.xcf -------------------------------------------------------------------------------- /manual/digital_en_2/digital_en_2_05.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_2/digital_en_2_05.xcf -------------------------------------------------------------------------------- /manual/digital_en_2/digital_en_2_06.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_2/digital_en_2_06.xcf -------------------------------------------------------------------------------- /manual/digital_en_2/digital_en_2_07.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_2/digital_en_2_07.xcf -------------------------------------------------------------------------------- /manual/digital_en_2/digital_en_2_08.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_2/digital_en_2_08.xcf -------------------------------------------------------------------------------- /manual/digital_en_2/digital_en_2_09.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_2/digital_en_2_09.xcf -------------------------------------------------------------------------------- /manual/digital_en_2/digital_en_2_10.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_2/digital_en_2_10.xcf -------------------------------------------------------------------------------- /manual/digital_en_2/digital_en_2_11.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_2/digital_en_2_11.xcf -------------------------------------------------------------------------------- /manual/digital_en_2/digital_en_2_12.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_2/digital_en_2_12.xcf -------------------------------------------------------------------------------- /manual/digital_en_2/digital_en_2_13.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_2/digital_en_2_13.xcf -------------------------------------------------------------------------------- /manual/digital_en_2/digital_en_2_14.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_2/digital_en_2_14.xcf -------------------------------------------------------------------------------- /manual/digital_en_2/digital_en_2_15.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_2/digital_en_2_15.xcf -------------------------------------------------------------------------------- /manual/digital_en_2/digital_en_2_16.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_2/digital_en_2_16.xcf -------------------------------------------------------------------------------- /manual/digital_en_2/digital_en_2_17.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_2/digital_en_2_17.xcf -------------------------------------------------------------------------------- /manual/digital_en_2/digital_en_2_18.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_2/digital_en_2_18.xcf -------------------------------------------------------------------------------- /manual/digital_en_2/digital_en_2_19.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_2/digital_en_2_19.xcf -------------------------------------------------------------------------------- /manual/digital_en_2/digital_en_2_20.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/manual/digital_en_2/digital_en_2_20.xcf -------------------------------------------------------------------------------- /manual/gen.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | 5 | # fonts used: 6 | # 江城斜黑体 7 | # 文泉驿微米黑 8 | # 青鸟华光简淡古印 9 | # 字心坊鸿潮榜书 10 | # 造字工房哲黑(非商用)常规体 (removed) 11 | # 造字工房羽逸(非商用)常规体 12 | # 思源黑体 13 | # 隶书 14 | # MBitmapRoundHK-Light 15 | # mplus_hzk_12 16 | 17 | 18 | def need_update(xcf_name, jpg_name): 19 | if os.path.exists(jpg_name): 20 | xcf_time = os.path.getmtime(xcf_name) 21 | jpg_time = os.path.getmtime(jpg_name) 22 | return xcf_time > jpg_time 23 | return True 24 | 25 | 26 | def gen_pdf(name): 27 | count = 0 if os.path.exists(f'{name}/{name}_00.xcf') else 1 28 | jpgs = [] 29 | update = False 30 | while True: 31 | page_name = f'{name}/{name}_{count:02d}' 32 | count += 1 33 | xcf_name = f'{page_name}.xcf' 34 | if os.path.exists(xcf_name): 35 | jpg_name = f'{page_name}.jpg' 36 | if need_update(xcf_name, jpg_name): 37 | update = True 38 | print(f'{xcf_name} ==> {jpg_name}') 39 | quality = '-quality 80' if count > 2 else '' 40 | os.system(f'convert.exe {xcf_name} -flatten {quality} {jpg_name}') 41 | jpgs.append(jpg_name) 42 | else: 43 | break 44 | 45 | if update: 46 | print(f'generating {name}.pdf') 47 | os.system(f'convert.exe {" ".join(jpgs)} {name}.pdf') 48 | 49 | 50 | if __name__ == '__main__': 51 | gen_pdf('digital_en_1') 52 | gen_pdf('digital_en_2') 53 | -------------------------------------------------------------------------------- /patch/EXAPUNKS_fixed.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/patch/EXAPUNKS_fixed.exe -------------------------------------------------------------------------------- /run.bat: -------------------------------------------------------------------------------- 1 | cd import_txt 2 | :: Convert all xlsx files to json 3 | excel2json.py --auto 4 | :: Grab used character in json, save them to font/chars.txt 5 | get_chars.py 6 | :: Read translated texts, generate the text patch 7 | import_txt.py 8 | cd .. 9 | 10 | cd font 11 | :: Generate new font 12 | gen.py 13 | cd .. 14 | 15 | cd images 16 | :: Generate new tex 17 | import_imgs.py 18 | cd .. 19 | 20 | cd manual 21 | :: Generate pdf manual 22 | gen.py 23 | cd .. 24 | 25 | md patch 26 | md patch\Content 27 | md patch\Content\manual 28 | md patch\PackedContent 29 | md patch\PackedContent\fonts 30 | 31 | move import_txt\strings.csv patch\Content 32 | move font\*.packedfont patch\PackedContent\fonts 33 | move manual\digital_en_1.pdf patch\Content\manual\digital_cn_1.pdf -------------------------------------------------------------------------------- /screenshot/digital_en_1_01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/screenshot/digital_en_1_01.jpg -------------------------------------------------------------------------------- /screenshot/digital_en_1_06.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/screenshot/digital_en_1_06.jpg -------------------------------------------------------------------------------- /screenshot/excel_example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/screenshot/excel_example.gif -------------------------------------------------------------------------------- /screenshot/screenshot_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/screenshot/screenshot_1.jpg -------------------------------------------------------------------------------- /screenshot/screenshot_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/screenshot/screenshot_2.jpg -------------------------------------------------------------------------------- /screenshot/screenshot_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noword/EXAPUNKS-Localize/9fa1e431c4e1546de33a00d5018c98c04082bcaf/screenshot/screenshot_3.jpg --------------------------------------------------------------------------------