我是基于Kimi moonshot-v1-8k实现的AI助手,在此博客上负责整理和概括文章
本文是一篇关于KrKr2引擎游戏汉化的经验教程,以《見上げた空におちていく》为例。文章介绍了使用KrKrExact、GalTransl、Xp3Viewer等工具进行游戏解包、文本处理和汉化的过程。首先,使用KrKrExact工具解包KrKr2引擎的游戏文件,然后将解包后的脚本文件通过GalTransl_DumpInjector提取文本,并检查提取的JSON文件是否正确。接着,使用GalTransl工具进行翻译,并将翻译后的JSON文件注入回脚本文件。最后,使用Xp3Viewer工具进行封包,替换原游戏目录内的data.xp3文件,实现游戏的汉化。文章还提到了处理选择项文本汉化和配置文件修改以优化中文显示效果。总结指出,KrKr2游戏通常支持UTF-8编码,KrKrExact是最简单实用的解包工具,不同游戏的文本处理可能有所不同。
编辑记录
2025-02-25 9:57:00 第一次编辑
- 正文
# 简介
本文章汉化 KrKr2 引擎 游戏的经验教程。示例游戏:《見上げた空におちていく》
# 使用的工具
- KrKrExact
- GalTransl
- Xp3Viewer
- GalTransl_DumpInjector 实际上参考 VNTranslationTools
参考视频:KiriKiri2/Z 解包封包总结
# 解包
使用工具:KrKrExact,把下载的 KrkrExtract.dll
和 KrkrExtract.exe
移动到游戏同目录下,然后把游戏拖入 KrkrExtract.exe
打开,如下图:
接着,把需要解包的 xp3
文件拖入里面,就能实现解包,解包文件存放在游戏目录 KrkrExtract_Output
下,如果是 data.xp3
,里面就有 data
文件夹。
这种方法能解包加密了的游戏,但注意得是 KrKr2
引擎的。
# 处理文本和汉化
# 处理文本
- 需要的脚本文件一般在
data\scenario
里,对于该游戏,都是ks
文件,记事本可直接查看,是UTF-8
格式。需要使用GalTransl_DumpInjector
进行提取。如图以解包data.xp3
为例,
这里需要注意:日文脚本文件夹存放的是前面的存放一系列 ks 文件的文件夹,日文 JSON 文件夹则是存放提取文本的 JSON 文件的文件夹,一定需要先创建好该文件夹,这里为 script_jp
,这样才能点击进行提取。
后面的译文 JSON 文件夹则是存放翻译后的 JSON 文件的文件夹,这里为 script_cn
,译文脚本文件夹则是存放翻译好的 ks 文件的文件夹。注意:要注入 JSON 回脚本,前面的日文脚本文件夹和日文 JSON 文件夹也必须对应上才能正确注入回去。
- 检查提取后的 JSON 是否
name
和message
正确,否则需要分析原 ks 文件使用正则表达式提取。
若 JSON 没问题,需要进行合并为一个 JSON,方便翻译。
代码如下:
import os | |
import json | |
# 输入目录:包含多个 JSON 文件的目录 | |
input_dir = r"D:\Data\python_prj\KrKr_Engine\見上げた空におちていく\script_jp" | |
# 输出目录:与 script_jp 同级的 script 文件夹 | |
output_dir = os.path.join(os.path.dirname(input_dir), "script") | |
# 合并后的 JSON 文件保存路径 | |
output_file = os.path.join(output_dir,os.path.basename(input_dir) + ".json") | |
# 如果输出目录不存在,则创建它 | |
if not os.path.exists(output_dir): | |
os.makedirs(output_dir) | |
merged_data = [] | |
# 遍历输入目录下的所有文件 | |
for filename in os.listdir(input_dir): | |
# 只处理 .json 结尾的文件 | |
if filename.lower().endswith(".json"): | |
file_path = os.path.join(input_dir, filename) | |
try: | |
with open(file_path, "r", encoding="utf-8") as f: | |
data = json.load(f) | |
# 检查读取的数据是否为列表格式 | |
if isinstance(data, list): | |
merged_data.extend(data) | |
else: | |
print(f"警告:文件 {filename} 中的数据不是列表格式,已跳过。") | |
except Exception as e: | |
print(f"读取文件 {filename} 时发生错误: {e}") | |
# 将合并后的结果写入目标文件,使用 ensure_ascii=False 以保持非 ASCII 字符(例如日文) | |
with open(output_file, "w", encoding="utf-8") as f: | |
json.dump(merged_data, f, ensure_ascii=False, indent=2) | |
print("合并完成,结果保存在:", output_file) |
该代码会在同目录创建 script
文件夹,存放需要翻译的文件 script_jp.json
- 预处理
script_jp.json
,检查文本特殊符号是否有问题,对于该游戏,发现对于[ruby text=\"むく\"]椋
的处理有问题,提取后会变成[椋\むく]
,因此需要处理回去,使用正则匹配,检测到[前\后]
的格式则转换为[ruby text="后"]前
。
代码如下:
import re | |
import json | |
# 正则匹配 [前 / 后] 格式 | |
ruby_pattern = re.compile(r'\[([^\]/]+)/([^\]]+)\]') | |
def replace_ruby_notation(script_file, output_file): | |
""" | |
读取 script_jp.json,替换 "message" 中的 [前/后] 为 [ruby text="后"]前,并写入 output_file。 | |
""" | |
# 读取 JSON 文件 | |
with open(script_file, "r", encoding="utf-8") as f: | |
script_data = json.load(f) | |
def ruby_replace(match): | |
"""替换匹配的文本""" | |
return f'[ruby text="{match.group(2)}"]{match.group(1)}' | |
# 遍历 JSON 数据,处理 "message" 字段 | |
for item in script_data: | |
if "message" in item and isinstance(item["message"], str): | |
item["message"] = ruby_pattern.sub(ruby_replace, item["message"]) | |
# 写入新的 JSON 文件 | |
with open(output_file, "w", encoding="utf-8") as f: | |
json.dump(script_data, f, ensure_ascii=False, indent=2) | |
print(f"转换完成,已保存至 {output_file}") | |
# 文件路径 | |
input_json = r"D:\Data\python_prj\KrKr_Engine\見上げた空におちていく\script\script_jp.json" | |
output_json = r"D:\Data\python_prj\KrKr_Engine\見上げた空におちていく\script\script_jp.json" | |
# 执行替换 | |
replace_ruby_notation(input_json, output_json) |
# 汉化翻译与文本再处理
把前面得到的 script_jp.json
使用 GalTransl 进行翻译。得到翻译文件后,这里重命名为 script_cn.json
。
可以再次运行程序,导出人名替换表的.csv 文件,手动添加中文人名后,再进行重建结果,这样翻译后脚本中的 name
也会汉化。
- 首先进行分割,将其还原为跟原来同结构的许多单个的 JSON 文件,代码为:
import os | |
import json | |
# 设定文件夹路径 | |
input_dir = r"D:\Data\python_prj\KrKr_Engine\見上げた空におちていく\script_jp" # 原始日文 JSON 文件所在目录 | |
translated_merged_file = os.path.join(os.path.dirname(input_dir), "script", "script_cn.json") # 翻译后的合并 JSON 文件路径 | |
output_dir = os.path.join(os.path.dirname(input_dir), "script_cn") # 新的存放翻译后 JSON 的目录 | |
# 如果输出目录不存在,则创建它 | |
if not os.path.exists(output_dir): | |
os.makedirs(output_dir) | |
# 加载翻译后的合并 JSON 数据 | |
with open(translated_merged_file, "r", encoding="utf-8") as f: | |
translated_data = json.load(f) | |
# 计数器,用于从合并后的翻译列表中依次取出翻译内容 | |
msg_index = 0 | |
# 对输入目录中的 JSON 文件按文件名进行排序,确保顺序与合并时一致 | |
for filename in sorted(os.listdir(input_dir)): | |
if filename.lower().endswith(".json"): | |
input_file_path = os.path.join(input_dir, filename) | |
with open(input_file_path, "r", encoding="utf-8") as f: | |
original_json = json.load(f) | |
# 遍历当前文件中的每一条记录,并将 "message" 字段替换为翻译后的文本 | |
for obj in original_json: | |
if msg_index < len(translated_data): | |
# 这里假设翻译后的合并 JSON 中,每个对象与原来的顺序一致,只替换 "message" 部分 | |
obj["message"] = translated_data[msg_index]["message"] | |
if "name" in translated_data[msg_index]: | |
obj["name"] = translated_data[msg_index]["name"] | |
msg_index += 1 | |
else: | |
print(f"警告:翻译文本数量不足,文件 {filename} 中部分记录未替换。") | |
break | |
# 在输出目录中生成新的 JSON 文件,文件名与原来一致 | |
output_file_path = os.path.join(output_dir, filename) | |
with open(output_file_path, "w", encoding="utf-8") as f: | |
json.dump(original_json, f, ensure_ascii=False, indent=2) | |
print("所有文件已处理完毕,翻译后的 JSON 文件保存在:", output_dir) |
该代码会生成 script_cn
文件夹,里面存放翻译后的许多 JSON 文件。
- 再使用
GalTransl_DumpInjector
注入 JSON 回脚本,需要先创建好存放新 ks 的文件夹,这里设为inject
,接下来,需要对这里面的 ks 进行修正。
理由是:前面的提取没有提取出脚本文件中的选择项的文本,即选择项文本没有得到汉化,于是需要再次提取出这些文本到 JSON,翻译后再注入回 ks 文件。
分析这些 ks 文件,发现选择项文本的内容一般是下面格式的:
@exlink txt="それでも追い返すなんて出来ない" target="*select1_1"
@exlink txt="一度冷静に考えてみる" target="*select1_2"
@showlink
所以需要正则匹配到 @exlink txt=
,提取出后面的文本到 JSON,再进行翻译,得到翻译后的 JSON,再按顺序注入回 ks,即得到完整翻译了脚本和选择项的 ks 文件。
注意:存放原脚本的 ks 文件是 shiftjis
编码的(这里和前面所说的 utf-8 编码可能冲突,但实际就是,使用 krkrexact 解包的 ks 文件在 vscode 打开是 jis 编码,但实际之前看到过是 utf-8 编码的,但无所谓了,因为没有做什么转编码 dll 注入什么的打包回去是可以读取的。) 读取时需要以 jis
编码读取,匹配到选择项文本,再以 utf-8
写到 JSON 用于翻译,翻译后,需要把翻译后的 JSON 写入前面的 inject
里的 ks,这里的 ks 都是 utf16le
编码的,JSON 写回后,新的 ks 也要以 utf16le
编码保存。
代码如下:
import os | |
import re | |
import json | |
# `@exlink txt="..."` 的正则匹配模式 | |
exlink_pattern = re.compile(r'(@exlink txt=")([^"]+)(")') | |
def extract_options(input_dir, output_json): | |
""" | |
遍历 input_dir 目录下所有 .ks 文件,提取所有形如 @exlink txt="xxx" 中的 xxx, | |
并保存为 JSON 文件,格式为: | |
[ | |
{"message": "xxx"}, | |
... | |
] | |
读取 .ks 文件时使用 shift_jis 编码(原文件为 shift_jis 格式), | |
写入 JSON 文件使用 utf-8 编码。 | |
""" | |
extracted = [] | |
ks_files = sorted([f for f in os.listdir(input_dir) if f.lower().endswith(".ks")]) | |
for filename in ks_files: | |
file_path = os.path.join(input_dir, filename) | |
with open(file_path, "r", encoding="shift_jis", errors="ignore") as f: | |
for line in f: | |
# 使用 findall 提取匹配结果 | |
matches = exlink_pattern.findall(line) | |
for match in matches: | |
# 如果 match 是元组(有多个捕获组),取第二个,否则直接使用 match | |
if isinstance(match, tuple): | |
extracted_message = match[1] if len(match) > 1 else match[0] | |
else: | |
extracted_message = match | |
extracted.append({"message": extracted_message}) | |
with open(output_json, "w", encoding="utf-8") as f: | |
json.dump(extracted, f, ensure_ascii=False, indent=2) | |
print(f"提取完成,共提取 {len(extracted)} 条数据,保存在:{output_json}") | |
def update_options(translation_dir, extracted_json, translation_json, output_dir): | |
""" | |
遍历 translation_dir 下所有 UTF-16 LE 编码的 .ks 文件,查找 @exlink txt="原文", | |
在 extracted_json 里查找该原文的索引,并用 translated_options.json 里相同索引的翻译替换。 | |
处理后的 .ks 文件以 UTF-16 LE 编码保存在 output_dir。 | |
""" | |
# 读取原始提取的 JSON(用于定位索引) | |
with open(extracted_json, "r", encoding="utf-8") as f: | |
extracted_data = json.load(f) | |
# 读取翻译后的 JSON | |
with open(translation_json, "r", encoding="utf-8") as f: | |
translated_data = json.load(f) | |
# 构建原文到索引的映射 | |
text_to_index = {item["message"]: idx for idx, item in enumerate(extracted_data)} | |
if not os.path.exists(output_dir): | |
os.makedirs(output_dir) | |
ks_files = sorted([f for f in os.listdir(translation_dir) if f.lower().endswith(".ks")]) | |
for filename in ks_files: | |
file_path = os.path.join(translation_dir, filename) | |
with open(file_path, "r", encoding="utf-16-le", errors="replace") as f: | |
content = f.read() | |
# 替换 @exlink txt="..." 的内容 | |
def replace_exlink(match): | |
original_text = match.group(2) | |
if original_text in text_to_index: | |
idx = text_to_index[original_text] | |
if idx < len(translated_data): | |
translated_text = translated_data[idx]["message"] | |
return f'{match.group(1)}{translated_text}{match.group(3)}' | |
return match.group(0) # 如果找不到匹配项,则不替换 | |
updated_content = exlink_pattern.sub(replace_exlink, content) | |
# 写入新的 ks 文件(仍然使用 UTF-16 LE) | |
output_path = os.path.join(output_dir, filename) | |
with open(output_path, "w", encoding="utf-16-le") as f: | |
f.write(updated_content) | |
print(f"文件 {filename} 更新完成,保存到 {output_path}") | |
print("所有文件更新完成。") | |
# 调用示例:你可以根据需要取消注释下面其中一个或两个调用进行测试 | |
if __name__ == "__main__": | |
input_dir = r"D:\Data\python_prj\KrKr_Engine\見上げた空におちていく\data\scenario" # 原始日文 ks 文件所在目录 | |
extracted_json = "extracted_options.json" # 提取后的选项保存文件(待翻译) | |
# 提取模式:提取所有 .ks 文件中的选项文本 | |
extract_options(input_dir, extracted_json) | |
translation_dir = r"D:\Data\python_prj\KrKr_Engine\見上げた空におちていく\inject" | |
translation_json = "translated_options.json" # 翻译后的 JSON | |
output_dir = r"D:\Data\python_prj\KrKr_Engine\見上げた空におちていく\inject_translated" | |
# 更新模式:将翻译后的文本写回到新的 .ks 文件中(请先完成 JSON 翻译后取消注释) | |
# update_options(translation_dir, extracted_json, translation_json, output_dir) |
通过以上代码,得到 "extracted_options.json",拿去翻译,得到 "translated_options.json",再写到新的 ks 文件夹 "inject_translated",里面的 ks 文件就都是翻译好的脚本和选择项。
# 封包
使用工具 Xp3Viewer ,将游戏拖到 xp3viewer,把下面的勾取消掉,如图:
再把 "inject_translated" 里的所有 ks 文件拖到 \data\scenario
替换原来的 ks;之后可以修改配置文件,使游戏内字体对中文的显示效果更好,修改文件 data\system\Config.tjs
,如下
;userFace = "黑体"; // deffont タグの face 属性に相当
和后面的:
// メッセージ履歴の設定です
// ◆ フォント名
;fontName = "黑体";
// ◆ フォントを太字にするか
;fontBold = true;
// ◆ フォントのサイズ ( pixel単位)
;fontHeight = 24;
// ◆ ラインの高さ
;lineHeight = 26;
这里的字体只要电脑有的就行。可参考链接,保存文件后,开始打包。
把 data
文件夹拖到 xp3viewer 即可进行打包,得到 data.xp3
文件。注意:此时 data
文件夹一定不能在游戏目录内。
之后替换原游戏目录内的 data.xp3
即可,游戏可正常显示中文。
注意:对于该游戏,由于之前打了补丁即加了 patch.xp3
文件,因此有些脚本实际是放在该文件里面的,要全汉化该游戏需要解包该 patch,xp3
并按前面流程完成翻译和打包,之后即可实现脚本全汉化。
# 总结
通过该游戏汉化实践发现,一般 KrKr2 游戏都是可以读取 UTF-8
的,并且最简单实用的解包工具还是选择 KrKrExact
,虽然该工具的 Make Universal Patch
和 Make Package
对本游戏没有用,也无法打包。但能简单的实现对加密游戏的解包,后续操作则使用其他工具即可。对于不同的游戏,可能对文本的处理会不一致,需要根据实际解包的脚本处理。