我是基于Kimi moonshot-v1-8k实现的AI助手,在此博客上负责整理和概括文章
编辑记录
2025-02-17 10:00:00 第一次编辑
- 正文
# 简介
本文章为使 Escu:de 游戏显示 GBK 编码的脚本的教程。包括解包、翻译、编码转换、封包、dll 注入。
# 使用的工具
- EsuBinE
- EsuBinL
- EsucudeTools
- GalTransl
- AHeadLib.Net
- x32dbg
- Extreme Injector
# 解包
- 取出 script.bin 的文件
- 可以使用 GARbro 手动解包 script.bin 取出其中的所有文件,一般包括 staff 文件夹和其他.001 和.bin 文件。把这些文件解包到一个文件夹中,例如 script_source/output/script 中。
- 也可以使用 EscudeTools 工具,运行命令:
D:\Tool\汉化工具\net8.0\EscudeTools.exe -u D:\Data\python_prj\Escude_Translate\script_source |
其中 script_source 含有 script.bin 文件。运行命令后文件解包在 script_source/output/script 中。
最好使用 EscudeTools 工具进行解包,因为方便后续封包,该方式解包会生成附加的包信息文件,能封回原包。
- 处理解包的文件
控制台运行命令:
D:\Tool\汉化工具\EsuBinE\EsuBinE200_ORG.exe -dump -b -enc D:\Data\python_prj\Escude_Translate\script_source\output\script D:\Data\python_prj\Escude_Translate\script_source\output\script_txt |
注意要创建好 script_source\output\script_txt
文件夹才能成功。
# 处理 txt 文件用于翻译
原编码默认为 shift-jis,需要先转为 UTF-8 用于翻译。
import os | |
import chardet | |
def convert_encoding(folder_path): | |
""" 将文件夹内所有文本文件从 Shift-JIS 转为 UTF-8 """ | |
target_encodings = ['shift_jis', 'shift-jis', 'sjis', 'cp932', 'windows-31j', 'mskanji'] | |
for filename in os.listdir(folder_path): | |
file_path = os.path.join(folder_path, filename) | |
if not os.path.isfile(file_path): | |
continue | |
# 检测文件编码 | |
with open(file_path, 'rb') as f: | |
raw_data = f.read() | |
detected = chardet.detect(raw_data) | |
encoding = detected['encoding'] | |
confidence = detected.get('confidence', 0) | |
# 判断是否为目标编码(不区分大小写) | |
is_target = False | |
if encoding: | |
is_target = encoding.lower() in target_encodings | |
else: | |
# 编码检测失败时,强制尝试转换 | |
is_target = True | |
if is_target: | |
try: | |
# 优先使用 cp932 解码并替换非法字符 | |
with open(file_path, 'r', encoding='cp932', errors='replace') as f: | |
content = f.read() | |
# 写回 UTF-8 编码 | |
with open(file_path, 'w', encoding='utf-8') as f: | |
f.write(content) | |
print(f"[OK] 已转换: {filename}") | |
except Exception as e: | |
print(f"[Error] 转换失败: {filename} - {str(e)}") | |
else: | |
print(f"[跳过] 非目标文件: {filename} (检测编码: {encoding}, 置信度: {confidence:.2f})") | |
if __name__ == "__main__": | |
folder_path = r"D:\Data\python_prj\Escude_Translate\script_source\output\script_txt" | |
convert_encoding(folder_path) | |
print("编码转换完成!") |
接下来,对 txt 进行处理,转化为用于翻译的 json 格式,已知前面生成的 txt 的格式为:
##000001##|凜有思考過『故事的后続』龜。 | |
%%000001%%|凜有思考過『故事的后続』龜。 | |
##000002##|我第一次意識到這件事,已経是很久以前的事了。 | |
%%000002%%|我第一次意識到這件事,已経是很久以前的事了。 | |
##000003##|我曾経有過祖母。 | |
%%000003%%|我曾経有過祖母。 |
需要的 json 格式为:
[ | |
{ | |
"message": "既然是梦的話ーー那我継続齣浸其中也无妨槇。" | |
}, | |
{ | |
"message": "「那就這樣槇」" | |
}, | |
] |
转换 txt 到 json 的代码:
import os | |
import json | |
import re | |
import glob | |
def process_text(text): | |
""" | |
处理文本: | |
1. 将 <ruby text='XXX'>YYY</ruby> 替换为 YYY | |
2. (如有需要,可以添加其它处理规则) | |
""" | |
# 使用正则表达式替换 ruby 标签 | |
# 匹配形式:<ruby text=' くさかべあきら '> 日下部彰 & lt;/ruby> 替换为 日下部彰 | |
# text = re.sub(r"<ruby\s+text='[^']*'>(.*?)</ruby>", r"\1", text) | |
# 如果需要去掉 <r> 标签(如果它确实需要被删除),可以取消下面代码的注释 | |
# text = text.replace("<r>", "") | |
return text | |
def convert(folder_path): | |
# 用于存放所有处理后的对话列表 | |
dialogues = [] | |
# 读取文件夹中所有 txt 文件 | |
txt_files = glob.glob(os.path.join(folder_path, "*.txt")) | |
for file_path in txt_files: | |
with open(file_path, "r", encoding="utf-8") as f: | |
# 使用 set 来去重同一文件内的重复文本 | |
seen_texts = set() | |
for line in f: | |
line = line.strip() | |
if not line: | |
continue | |
# 分割字符串,取 | 后面的文本(假定每行只有一个 | 分隔符) | |
if "|" in line: | |
parts = line.split("|", 1) | |
text = parts[1].strip() | |
# 如果文本已存在则跳过(去除重复) | |
if text in seen_texts: | |
continue | |
seen_texts.add(text) | |
# 处理文本 | |
processed_text = process_text(text) | |
# 添加到对话列表中,name 暂时为空字符串 | |
dialogues.append({ | |
"message": processed_text | |
}) | |
# 最终写入 JSON 文件(可根据需要修改输出路径) | |
output_path = os.path.join(folder_path, "output.json") | |
with open(output_path, "w", encoding="utf-8") as f: | |
json.dump(dialogues, f, ensure_ascii=False, indent=4) | |
print(f"处理完成,结果保存在 {output_path}") | |
if __name__ == "__main__": | |
# 指定需要处理的文件夹路径 | |
folder_path = r"D:\Data\python_prj\Escude_Translate\script_source\output\script_txt" | |
convert(folder_path) |
# 进行翻译
使用工具 GalTransl,按照要求,编写字典,配置文件,完成翻译。
# 编码转换,处理翻译好的 json 回 txt
方法为读取原先的 txt 文件,按其原格式,把翻译好的 json 的文本,转换为 GBK 编码后再写回 txt,用于后续打包。
import os | |
import json | |
import glob | |
import chardet # 需要安装 `pip install chardet` | |
def detect_encoding(file_path): | |
with open(file_path, "rb") as f: | |
raw_data = f.read(10000) # 读取部分数据用于检测 | |
result = chardet.detect(raw_data) | |
return result["encoding"] | |
def main(): | |
translated_json_path = r"D:\Tool\GalTransl\GalTransl-v5.9.1-win\sampleProject\gt_output\script_jp.json" | |
original_folder = r"D:\Data\python_prj\Escude_Translate\script_source\output\script_txt" | |
new_folder = r"D:\Data\python_prj\Escude_Translate\script_source\output\script_txt" | |
os.makedirs(new_folder, exist_ok=True) | |
output_encoding = "gbk" | |
with open(translated_json_path, "r", encoding="utf-8") as f: | |
translated_data = json.load(f) | |
translated_index = 0 | |
total_translated = len(translated_data) | |
txt_files = sorted(glob.glob(os.path.join(original_folder, "*.txt"))) | |
for file_path in txt_files: | |
detected_encoding = detect_encoding(file_path) | |
if detected_encoding is None: | |
print(f"警告:无法检测文件 {os.path.basename(file_path)} 的编码,默认使用 GBK。") | |
detected_encoding = "gbk" | |
new_lines = [] | |
mapping = {} | |
try: | |
with open(file_path, "r", encoding=detected_encoding, errors="replace") as f: | |
for line in f: | |
line = line.rstrip("\n") | |
if "|" not in line: | |
new_lines.append(line) | |
continue | |
try: | |
prefix, orig_msg = line.split("|", 1) | |
except ValueError: | |
new_lines.append(line) | |
continue | |
if orig_msg not in mapping: | |
if translated_index >= total_translated: | |
print(f"错误:翻译后的条目不足以覆盖文件 {os.path.basename(file_path)}") | |
return | |
mapping[orig_msg] = translated_data[translated_index]["message"] | |
translated_index += 1 | |
new_line = f"{prefix}|{mapping[orig_msg]}" | |
new_lines.append(new_line) | |
except Exception as e: | |
print(f"读取文件 {file_path} 失败: {e}") | |
continue | |
new_file_path = os.path.join(new_folder, os.path.basename(file_path)) | |
with open(new_file_path, "w", encoding=output_encoding, errors="replace") as f_out: | |
f_out.write("\n".join(new_lines)) | |
# print (f"写入 {new_file_path},共更新 {len (new_lines)} 行。") | |
if translated_index < total_translated: | |
print("注意:还有未使用的翻译条目。") | |
elif translated_index > total_translated: | |
print("错误:翻译条目不足以覆盖所有文件中的对话条目!") | |
else: | |
print("转换完成,所有翻译条目已按原始文件对应写入新 txt 文件中。") | |
if __name__ == "__main__": | |
main() |
这样,原来 Escude_Translate\script_source\output\script_txt 的编码转为了 GBK。
# 封包
- 把翻译、编码转换后的 txt 打包回.001 文件,替换了原来的.001
运行命令:
D:\Tool\汉化工具\EsuBinE\EsuBinE200_ORG.exe -repack -b -enc D:\Data\python_prj\Escude_Translate\script_source\output\script_txt D:\Data\python_prj\Escude_Translate\script_source\output\script |
- 封包回 script.bim
使用 EscudeTools 工具,运行命令:
D:\Tool\汉化工具\net8.0\EscudeTools.exe -r D:\Data\python_prj\Escude_Translate\script_source\output |
至此,得到了编码为 GBK 的 script.bin,替换游戏目录里的 script.bin。
# dll 注入
# 修改游戏文件
以悠刻のファムファタル为例,主程序名字是 femme_fatale.exe,那么它的配置 bin 名字就叫 femme_fatale.bin。该文件保存到 Escude_Translate\femme,进行解包:
D:\Tool\汉化工具\net8.0\EscudeTools.exe -u D:\Data\python_prj\Escude_Translate\femme |
得到 Escude_Translate\femme\output\femme_fatale,进入 misc 目录,找到 text.c 文件。打开这个文件后,找到 init_default_font 函数的调用,内容改为:
void init_default_font(int font_id, int weight) | |
{ | |
default_font_id = FT_USER; | |
default_font_weight = 400; | |
strcpy(user_font_name,"SimHei"); | |
//ini_gets("Font", "Face", "", user_font_name, sizeof(user_font_name), NULL); | |
//if(user_font_name[0] != '\0'){ | |
// default_font_id = FT_USER; | |
// default_font_weight = 400; | |
// if(ini_geti("Font", "Bold", 0, NULL)){ | |
// default_font_weight = 700; | |
// } | |
//} | |
} |
修改字符检测,去到 lib 目录下 string.h 注释掉原来的即可显示中文。
//#define ISKANJI(x)((((x)^0x20)-0xa1) <= 0x3b) | |
#define ISKANJI(x)((x) >= 0x81) |
同样修改文件后封包回 femme_fatale.bin。
D:\Tool\汉化工具\net8.0\EscudeTools.exe -r D:\Data\python_prj\Escude_Translate\femme\output
在 Escude_Translate\femme\output 找到 femme_fatale.bin,替换原游戏目录里的。
# 生成 dll
编写一个 CreateFontIndirectA 的 hook,在调用前判断 LOGFONT 的 lfCharSet 字段,如果为 0x80(Shift-JIS),则修改为 0x86(GB2312/GBK),然后调用原函数。
方法是利用 MinHook 库实现 DLL 注入后 hook CreateFontIndirectA ,达到在游戏启动时将 LOGFONT 中 lfCharSet 从 0x80 修改为 0x86 的目的。
-
创建 DLL 工程
用 Visual Studio 新建一个 Win32 DLL 工程(例如项目名 “FontHook”)。可以选择 “动态链接库 (DLL)” 的模板。 -
集成 MinHook 库
从 MinHook 官方 GitHub 下载最新版本,(下载 include 和 src 文件夹)将 MinHook 的头文件和静态库(或源码)添加到工程中,并在项目设置中链接相应库。 -
添加 DLL 代码文件
- 添加源代码文件
- 在 “解决方案资源管理器” 中,右键点击项目 “FontHook”,选择 “添加” -> “新建项…”。
- 在弹出的 “添加新项” 窗口中选择 “C++ 文件 (.cpp)”。
- 文件名输入 “DllMain.cpp”,点击 “添加”。
- 编写代码
该代码基于 MinHook 实现对 CreateFontIndirectA 的 hook,检测 LOGFONTA 结构中的 lfCharSet 字段,若值为 0x80(Shift-JIS)则修改为 0x86(GB2312/GBK)。
#include <Windows.h> | |
#include <tchar.h> | |
#include "MinHook.h" // 请确保该头文件可以被找到 | |
// 定义 CreateFontIndirectA 的函数指针类型 | |
typedef HFONT(WINAPI* PFN_CreateFontIndirectA)(CONST LOGFONTA*); | |
PFN_CreateFontIndirectA fpCreateFontIndirectA = NULL; // 保存原始函数地址 | |
// Hook 后的 CreateFontIndirectA 实现 | |
HFONT WINAPI Hooked_CreateFontIndirectA(CONST LOGFONTA* lpLogFont) | |
{ | |
// 拷贝一份 LOGFONTA 结构,避免修改原内存 | |
LOGFONTA newLogFont = *lpLogFont; | |
// 如果 lfCharSet 为 0x80(Shift-JIS),则修改为 0x86(GB2312/GBK) | |
if (newLogFont.lfCharSet == 0x80) | |
{ | |
newLogFont.lfCharSet = 0x86; | |
} | |
// 调用原始函数 | |
return fpCreateFontIndirectA(&newLogFont); | |
} | |
// 初始化 Hook 的函数 | |
extern "C" __declspec(dllexport) void InitHook() | |
{ | |
// 初始化 MinHook | |
if (MH_Initialize() != MH_OK) | |
{ | |
return; | |
} | |
// 获取 gdi32.dll 模块句柄 | |
HMODULE hGDI32 = GetModuleHandle(_T("gdi32.dll")); | |
if (hGDI32 == NULL) | |
{ | |
return; | |
} | |
// 获取 CreateFontIndirectA 函数地址 | |
FARPROC pCreateFontIndirectA = GetProcAddress(hGDI32, "CreateFontIndirectA"); | |
if (pCreateFontIndirectA == NULL) | |
{ | |
return; | |
} | |
// 创建 Hook,将 pCreateFontIndirectA 替换为 Hooked_CreateFontIndirectA,并保存原始函数地址 | |
if (MH_CreateHook(pCreateFontIndirectA, &Hooked_CreateFontIndirectA, reinterpret_cast<LPVOID*>(&fpCreateFontIndirectA)) != MH_OK) | |
{ | |
return; | |
} | |
// 启用 Hook | |
if (MH_EnableHook(pCreateFontIndirectA) != MH_OK) | |
{ | |
return; | |
} | |
} | |
// DLL 主入口函数 | |
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) | |
{ | |
switch (ul_reason_for_call) | |
{ | |
case DLL_PROCESS_ATTACH: | |
// 禁用 DLL_THREAD_ATTACH/DLL_THREAD_DETACH 通知(可选) | |
DisableThreadLibraryCalls(hModule); | |
// 调用初始化 Hook 的函数 | |
InitHook(); | |
break; | |
case DLL_PROCESS_DETACH: | |
MH_Uninitialize(); | |
break; | |
} | |
return TRUE; | |
} |
- 集成 MinHook 库
- 添加包含目录
- 在 “解决方案资源管理器” 中右键点击项目 “FontHook”,选择 “属性”(Properties)。
- 在左侧选择 “C/C++” -> “常规” -> “附加包含目录”(Additional Include Directories)。点击编辑按钮,添加你存放 MinHook 头文件的目录,例如:D:/Data/Design/C_project/minhook/include.
- 将 MinHook 的源码(例如 minhook.c)添加到你的项目中进行编译。
在 “源文件” 中右键点击 “添加”-> “现有项”,把 D:/Data/Design/C_project/minhook/src 里的所有文件包括子文件里的文件添加进去。
- 配置项目生成 DLL
- 在项目属性中,确保 “配置” 设置为 “所有配置”(All Configurations)。
- 选择 “配置属性” -> “常规”,确保 “配置类型” 设置为 “动态库 (.dll)”。
- 修改项目平台为 Win32
-
打开项目的配置管理器
在 Visual Studio 中,点击菜单 “生成” → “配置管理器”。 -
添加或切换平台
- 在 “活动解决方案平台” 下拉框中查看是否有 “Win32”。
- 如果没有,则点击 “<新建…>”,在弹出的 “新建解决方案平台” 对话框中,选择 “Win32” 作为新平台,并选择 “复制自” 当前平台(如 x64),然后点击 “确定”。
-
设置 Win32 为当前平台
确保 “活动解决方案平台” 选择的是 “Win32”。这时项目将以 32 位方式编译。
- 禁用预编译头
- 在 解决方案资源管理器 中右键点击你的项目(例如 “FontHook”),选择 属性。
- 在弹出的 “属性页” 中,展开 C/C++ -> 预编译头。
- 将 预编译头 选项修改为 不使用预编译头(Not Using Precompiled Headers)。
- 点击 确定 保存设置。
- 删除 pch.h 和 pch.c。
- 生成解决方案
输出目录(例如 Debug 下)会生成一个 32 位的 FontHook.dll。
# 注入 dll (方式一)
使用 Extreme Injector 工具。
- 选择目标进程
- 在 Extreme Injector 的界面中,点击 “Select” 按钮,在弹出的进程列表中找到目标游戏进程(例如游戏的 exe 名称)。
- 如果游戏还没有启动,你需要先启动游戏,然后再用注入器选择其进程。
- 添加 DLL 文件
- 点击 “Add” 按钮,浏览到你生成的 FontHook.dll 所在目录(例如 D:\Data\Design\C_project\FontHook\Debug)。
- 选择 FontHook.dll 添加到列表中。
-
设置注入参数
这里选择了 inject method 为 thread hijacking 后注入成功。 -
执行注入
- 点击 “Inject” 按钮,注入器会将 FontHook.dll 加载到目标游戏进程中。
- 如果注入成功,注入器一般会显示成功提示。
# 注入 dll (方式二)
DLL 劫持。
当 Windows 应用程序启动时,它会尝试加载所需的 DLL 文件。Windows 按以下顺序搜索 DLL:
- 应用程序目录(当前 EXE 目录)
- C:\Windows\System32
- C:\Windows\SysWOW64(对于 32 位程序在 64 位 Windows 上运行时)
- 其他注册表指定的路径
如果你在 应用程序目录 放置一个与程序加载的 DLL 同名的文件,那么程序会 优先加载你自己的 DLL,从而实现自动注入。
可以使用 x32dbg 工具,打开游戏 exe,在” 日志 “中可以看到调用的 dll。
这里准备对 version.dll
进行劫持。
使用 AHeadLib.Net 工具,先把 C:\Windows\SysWOW64\version.dll
文件复制出来,然后使用该工具,得到一个该 dll 的工程文件,修改其 version_DllMain.cpp
的代码,加入 Hook 的功能,并且同样在 Win32
平台下,导入 MinHook 的 include 和 src 里的文件,禁用预编译功能。修改后的代码为:
// generated by tools | |
// AHeadLib.Net | |
// https://github.com/bodong1987/AHeadLib.Net | |
// Powered by bodong | |
#include <windows.h> | |
#include <tchar.h> | |
#include "MinHook.h" // 请确保该头文件可以被找到 | |
// 定义 CreateFontIndirectA 的函数指针类型 | |
typedef HFONT(WINAPI* PFN_CreateFontIndirectA)(CONST LOGFONTA*); | |
PFN_CreateFontIndirectA fpCreateFontIndirectA = NULL; // 保存原始函数地址 | |
extern void CheckedLoad(); | |
extern void ApplyBuiltinPatches(); | |
extern void ExecuteUserCustomCodes(); | |
extern bool ShouldExecuteAttachCode(); | |
// Hook 后的 CreateFontIndirectA 实现 | |
HFONT WINAPI Hooked_CreateFontIndirectA(CONST LOGFONTA* lpLogFont) | |
{ | |
// 拷贝一份 LOGFONTA 结构,避免修改原内存 | |
LOGFONTA newLogFont = *lpLogFont; | |
// 如果 lfCharSet 为 0x80(Shift-JIS),则修改为 0x86(GB2312/GBK) | |
if (newLogFont.lfCharSet == 0x80) | |
{ | |
newLogFont.lfCharSet = 0x86; | |
} | |
// 调用原始函数 | |
return fpCreateFontIndirectA(&newLogFont); | |
} | |
// 初始化 Hook 的函数 | |
VOID InitHook() | |
{ | |
// 初始化 MinHook | |
if (MH_Initialize() != MH_OK) | |
{ | |
return; | |
} | |
// 获取 gdi32.dll 模块句柄 | |
HMODULE hGDI32 = GetModuleHandle(_T("gdi32.dll")); | |
if (hGDI32 == NULL) | |
{ | |
return; | |
} | |
// 获取 CreateFontIndirectA 函数地址 | |
FARPROC pCreateFontIndirectA = GetProcAddress(hGDI32, "CreateFontIndirectA"); | |
if (pCreateFontIndirectA == NULL) | |
{ | |
return; | |
} | |
// 创建 Hook,将 pCreateFontIndirectA 替换为 Hooked_CreateFontIndirectA,并保存原始函数地址 | |
if (MH_CreateHook(pCreateFontIndirectA, &Hooked_CreateFontIndirectA, reinterpret_cast<LPVOID*>(&fpCreateFontIndirectA)) != MH_OK) | |
{ | |
return; | |
} | |
// 启用 Hook | |
if (MH_EnableHook(pCreateFontIndirectA) != MH_OK) | |
{ | |
return; | |
} | |
} | |
BOOL WINAPI DllMain( | |
HINSTANCE /*hInstance*/, // handle to DLL module | |
DWORD fdwReason, // reason for calling function | |
LPVOID lpvReserved) // reserved | |
{ | |
// Perform actions based on the reason for calling. | |
switch (fdwReason) | |
{ | |
case DLL_PROCESS_ATTACH: | |
// Initialize once for each new process. | |
// Return FALSE to fail DLL load. | |
CheckedLoad(); | |
InitHook(); | |
if (ShouldExecuteAttachCode()) | |
{ | |
// apply internal patches | |
ApplyBuiltinPatches(); | |
// apply user custom codes | |
ExecuteUserCustomCodes(); | |
} | |
break; | |
case DLL_THREAD_ATTACH: // Do thread-specific initialization. | |
case DLL_THREAD_DETACH: // Do thread-specific cleanup. | |
break; | |
case DLL_PROCESS_DETACH: | |
if (lpvReserved != nullptr) | |
{ | |
break; // do not do cleanup if process termination scenario | |
} | |
// Perform any necessary cleanup. | |
break; | |
default: | |
break; | |
} | |
return TRUE; // Successful DLL_PROCESS_ATTACH. | |
} |
生成解决方案后,即可得到 version.dll
,将其复制到游戏目录下,游戏启动后,就会优先调用该目录下的 version.dll
,从而实现自动注入。
# 总结
通过以上 DLL 注入的两种方式能成功使游戏显示中文,但是游戏内原来的日语会乱码,所以这种方式一般只适用于只注重脚本翻译的情况,很粗糙。
而且对于 data.bin 的数据,难以转化为 GBK 的编码,原因有下:
- 使用 EsuBinL 工具,解包会生成 json 文件,而对于 json 文件,难以使其编码转为 GBK。
- 使用 EscudeTools 工具,解包会生成 db 文件,无论如何处理,db 文件的内容都会以 shift-jis 的格式读取打包回.bin,无法实现转换。
因此该方案局限性很大。