我是基于Kimi moonshot-v1-8k实现的AI助手,在此博客上负责整理和概括文章

编辑记录

2025-02-17 10:00:00 第一次编辑

  • 正文

# 简介

本文章为使 Escu:de 游戏显示 GBK 编码的脚本的教程。包括解包、翻译、编码转换、封包、dll 注入。

# 使用的工具

# 解包

使用工具:EsuBinEEsuBinL

  1. 取出 script.bin 的文件
  • 可以使用 GARbro 手动解包 script.bin 取出其中的所有文件,一般包括 staff 文件夹和其他.001 和.bin 文件。把这些文件解包到一个文件夹中,例如 script_source/output/script 中。
  • 也可以使用 EscudeTools 工具,运行命令:
解包script.bin
D:\Tool\汉化工具\net8.0\EscudeTools.exe -u D:\Data\python_prj\Escude_Translate\script_source

其中 script_source 含有 script.bin 文件。运行命令后文件解包在 script_source/output/script 中。

最好使用 EscudeTools 工具进行解包,因为方便后续封包,该方式解包会生成附加的包信息文件,能封回原包。

  1. 处理解包的文件
    控制台运行命令:
把.001文件解包为txt
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 用于翻译。

转为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 的代码:

转化为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。

# 封包

  1. 把翻译、编码转换后的 txt 打包回.001 文件,替换了原来的.001
    运行命令:
打包txt回.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
  1. 封包回 script.bim
    使用 EscudeTools 工具,运行命令:
打包回script.bin
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 的目的。

  1. 创建 DLL 工程
    用 Visual Studio 新建一个 Win32 DLL 工程(例如项目名 “FontHook”)。可以选择 “动态链接库 (DLL)” 的模板。

  2. 集成 MinHook 库
    MinHook 官方 GitHub 下载最新版本,(下载 include 和 src 文件夹)将 MinHook 的头文件和静态库(或源码)添加到工程中,并在项目设置中链接相应库。

  3. 添加 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;
}
  1. 集成 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 里的所有文件包括子文件里的文件添加进去。
  1. 配置项目生成 DLL
  • 在项目属性中,确保 “配置” 设置为 “所有配置”(All Configurations)。
  • 选择 “配置属性” -> “常规”,确保 “配置类型” 设置为 “动态库 (.dll)”。
  1. 修改项目平台为 Win32
  • 打开项目的配置管理器
    在 Visual Studio 中,点击菜单 “生成” → “配置管理器”。

  • 添加或切换平台

    • 在 “活动解决方案平台” 下拉框中查看是否有 “Win32”。
    • 如果没有,则点击 “<新建…>”,在弹出的 “新建解决方案平台” 对话框中,选择 “Win32” 作为新平台,并选择 “复制自” 当前平台(如 x64),然后点击 “确定”。
  • 设置 Win32 为当前平台
    确保 “活动解决方案平台” 选择的是 “Win32”。这时项目将以 32 位方式编译。

  1. 禁用预编译头
  • 在 解决方案资源管理器 中右键点击你的项目(例如 “FontHook”),选择 属性。
  • 在弹出的 “属性页” 中,展开 C/C++ -> 预编译头。
  • 将 预编译头 选项修改为 不使用预编译头(Not Using Precompiled Headers)。
  • 点击 确定 保存设置。
  • 删除 pch.h 和 pch.c。
  1. 生成解决方案
    输出目录(例如 Debug 下)会生成一个 32 位的 FontHook.dll。

# 注入 dll (方式一)

使用 Extreme Injector 工具。

  1. 选择目标进程
  • 在 Extreme Injector 的界面中,点击 “Select” 按钮,在弹出的进程列表中找到目标游戏进程(例如游戏的 exe 名称)。
  • 如果游戏还没有启动,你需要先启动游戏,然后再用注入器选择其进程。
  1. 添加 DLL 文件
  • 点击 “Add” 按钮,浏览到你生成的 FontHook.dll 所在目录(例如 D:\Data\Design\C_project\FontHook\Debug)。
  • 选择 FontHook.dll 添加到列表中。
  1. 设置注入参数
    这里选择了 inject method 为 thread hijacking 后注入成功。

  2. 执行注入

  • 点击 “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 里的文件,禁用预编译功能。修改后的代码为:

version_DllMain.cpp
// 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,无法实现转换。

因此该方案局限性很大。