Featured image of post 使用Il2CPP API進行Android Unity遊戲漢化 Part 1 - 取代原文

使用Il2CPP API進行Android Unity遊戲漢化 Part 1 - 取代原文

這篇文章將介紹如何使用Il2CPP API來實現Android Unity遊戲的文字替換,並提供相關的程式碼範例和說明。

⚠️文章內有遊戲解包、注入和逆向的技術,不要在未經許可情況下在其他遊戲中嘗試並公開解包資源。以及請勿用於不正當的用途,尊重知識產權。

代碼參考: OrangeEgg1937/Unity-IL2CPP-Based-Translator

前置要求

在開始之前,以下是我們需要準備的一些環境和一些知識

所需環境

  • Android Studio (Android開發環境)
    • 任一Android Inline Hook庫,這裡會使用ShadowHook
    • 動態載入SO方法(非必要,但方便進行Debug),可參考
  • 一個 Android Unity 遊戲(這裡我使用了我的Demo,純apk可以在這裡下載)
  • VSCode + APKLab (或其他APK反編譯工具)

前置知識

  • 基礎 Android 逆向工程知識
  • 基礎 Unity 引擎知識 (如果沒有也沒問題,下面也將談到)
  • C++ & Java

Unity IL2CPP 簡介

這裡只是簡單介紹什麼是 Il2CPP,如需詳細瞭解其運作方式,可以參考: https://katyscode.wordpress.com/category/reverse-engineering/il2cpp/

目前,大多數 Android 手機遊戲都是使用 IL2CPP scripting backend,根據官方文件,IL2CPP提供了更好的跨平台應用程式支援,以及在 Android 中更好的效能 (相較於 Mono)。在下面的文本替代過程中,很多事情都會與IL2CPP相關,因此有必要知道 IL2CPP 是什麼。

IL2CPP 工作流程

根據官方文檔,IL2CPP(Intermediate Language To C++)是一個 Unity 的腳本後端,它將 C# 腳本轉換為 C++ 代碼,然後編譯成原生機器碼。整個過程可簡單描述如下:

IL2CPP Workflow

這與 Mono 虛擬機器非常不同,遊戲邏輯會以靜態方式執行,而非由 C# 虛擬機器管理。但另一方面,遊戲符號和相關邏輯會在產生的libil2cpp.so內有固定的偏移量(offset)。因此,可以使用Hook Function Address來修改文字。

其中一個工具,Il2CppDumper,可以幫助我們取得 il2cpp 內所有符號的函數偏移量,這可以輕鬆地識別目標函數。但在本教程中會使用用另一種方法來做 – 使用 IL2CPP API

IL2CPP API

除了官方文檔外,官方Blog有詳細介紹IL2CPP:

What is IL2CPP?

The technology that we refer to as IL2CPP has two distinct parts.

  • An ahead-of-time (AOT) compiler
  • A runtime library to support the virtual machine

The AOT compiler translates Intermediate Language (IL), the low-level output from .NET compilers, to C++ source code. The runtime library provides services and abstractions like a garbage collector, platform-independent access to threads and files, and implementations of internal calls (native code which modifies managed data structures directly).

JOSH PETERSON / UNITY TECHNOLOGIES, Unity Blog

在此部分中Il2CPP的另一個重要特點是它包含了支援VM的執行時的函式庫,這表示 IL2CPP 實作了 C# 環境,提供了一種不需動態編譯即可執行 C# 程式碼的靜態方式。透過閱讀 Unity Il2CPP 原始碼 (可在 UnityEditor 路徑中找到, {UnityEditorPath}/il2cpp/libil2cpp/il2cpp-api.cpp),有一些與 C# 環境相關的 API:

il2cpp source code

透過使用這些API,我們可以在不需要修改libil2cpp.so的情況下,直接在遊戲中使用類似 C# 環境來實現文字替換。 在以下教學,會使用下列 API 來取得Class info、相關的Methods和Property,以及相關的函式庫 (.dll):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Reference from ./il2cpp/libil2cpp/il2cpp-api.cpp

// 用於取得文字相關的 .dll, 從這2個 API 尋找 Unity.TextMeshPro.dll
const Il2CppImage* il2cpp_assembly_get_image(const Il2CppAssembly *assembly)
size_t il2cpp_image_get_class_count(const Il2CppImage * image);

// 用於取得文字相關的 class, 從Unity.TextMeshPro.dll尋找TMP_Text
const Il2CppClass* il2cpp_image_get_class(const Il2CppImage * image, size_t index);
const char* il2cpp_class_get_name(Il2CppClass *klass);

// 用於取得TMP_Text的 methods, 從得TMP_Text class尋找相關的Method
const MethodInfo* il2cpp_class_get_methods(Il2CppClass *klass, void* *iter);
const char* il2cpp_method_get_name(const MethodInfo *method);
uint32_t il2cpp_method_get_param_count(const MethodInfo *method);
const Il2CppType* il2cpp_method_get_param(const MethodInfo *method, uint32_t index);
Il2CppClass* il2cpp_class_from_il2cpp_type(const Il2CppType* type);

// 用於轉換 C# 字串為 C++ 字串
Il2CppChar* il2cpp_string_chars(Il2CppString* str);

漢化插件注入 (APK Injection) 和熱補丁 (Hot Patch)

在開始使用 IL2CPP API 之前,先注入必要的函式庫和插件(假設是依照 ShadowHook 安裝):

  1. 在 build.gradle 中,新增下列設定與任務(pushSo)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
android {
    ...
    defaultConfig {
        ...
        ndk {
            abiFilters 'armeabi-v7a', 'arm64-v8a'
        }
    }
    ...
}

task pushSo(dependsOn: 'externalNativeBuildDebug') {
    doLast {
        exec {
            commandLine 'adb', 'shell', 'mkdir', '-p', '/sdcard/Android/data/com.DefaultCompany.DemoChatGame/files/custom/arm64-v8a'
        }

        def soFiles = fileTree(dir: 'build/intermediates/cmake/debug/obj/arm64-v8a', include: '*.so')

        soFiles.each { file ->
            exec {
                commandLine 'adb', 'push', file.absolutePath, '/sdcard/Android/data/com.DefaultCompany.DemoChatGame/files/custom/arm64-v8a/' + file.name
            }
        }
    }
}
  1. 建立 APK,然後反編譯取得 smali 程式碼和必要的原生函式庫 (./lib/arm64-v8a)

  2. 使用 APKLab 來反編譯 Unity APK,將這些檔案放入 Unity 遊戲資料夾,並在第一個Activity中的onCreate()方法插入程式碼,以初始化我們的函式庫

  3. 將APK重新編譯, 然後安裝至裝置

  4. 之後,當我們要更新原生程式碼時,只要執行「pushSo」任務就可以了,不需要重新編譯Unity APK

IL2CPP API Hook和使用

重溫Hook的概念

在這部分,將使用IL2CPP API,並取得相關的類別和方法來實現文字替換。

一開始,我們需要從 libil2cpp.so 載入符號,這可以通過使用dlopen() & dlsym(),為了方便,我直接使用ShadowHook 的 shadowhook_dlopenshadowhook_dlsym 來實現。以下是一些必要的函式和變數:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#define LIB_NAME "libil2cpp.so"

/**
 * Function pointers for IL2CPP functions
 */
size_t (*il2cpp_image_get_class_count)(const Il2CppImage *image) = nullptr;
void *(*il2cpp_image_get_class)(const Il2CppImage *image, size_t index) = nullptr;
const char *(*il2cpp_class_get_name)(const Il2CppClass *klass) = nullptr;
const MethodInfo *(*il2cpp_class_get_methods)(const Il2CppClass *klass, void **iter) = nullptr;
const char *(*il2cpp_method_get_name)(const MethodInfo *method) = nullptr;
uint32_t (*il2cpp_method_get_param_count)(const MethodInfo *method) = nullptr;
const Il2CppType* (*il2cpp_method_get_param)(const MethodInfo *method, uint32_t index) = nullptr;
Il2CppClass* (*il2cpp_class_from_il2cpp_type)(const Il2CppType* type) = nullptr;

/**
 * Function pointers for Convert C# string to C++ string
 */
Il2CppChar *(*il2cpp_string_chars)(const Il2CppString *str) = nullptr;

/**
 * Function pointers for TMPro functions
 */
Il2CppString *(*TMP_Text_get_text)(void *instance) = nullptr;
void (*TMP_Text_set_text)(void *instance, Il2CppString *text) = nullptr;
void (*TextMeshProUGUI_onEnable)(void *instance) = nullptr;
void (*TMP_Text_SetText)(void *instance, Il2CppString *text) = nullptr;

void il2cpp_manager_init() {

    handle = shadowhook_dlopen(LIB_NAME);
    if (handle) {
        il2cpp_image_get_class_count = (size_t (*)(const Il2CppImage *)) shadowhook_dlsym(handle,
                                                                                          "il2cpp_image_get_class_count");
        il2cpp_image_get_class = (void *(*)(const Il2CppImage *, size_t)) shadowhook_dlsym(handle,
                                                                                           "il2cpp_image_get_class");
        il2cpp_class_get_name = (const char *(*)(const Il2CppClass *)) shadowhook_dlsym(handle,
                                                                                        "il2cpp_class_get_name");
        il2cpp_class_get_methods = (const MethodInfo *(*)(const Il2CppClass *,
                                                          void **)) shadowhook_dlsym(handle,
                                                                                     "il2cpp_class_get_methods");
        il2cpp_method_get_name = (const char *(*)(const MethodInfo *)) shadowhook_dlsym(handle,
                                                                                        "il2cpp_method_get_name");
        il2cpp_string_chars = (Il2CppChar *(*)(const Il2CppString *)) shadowhook_dlsym(handle,
                                                                                       "il2cpp_string_chars");
        il2cpp_method_get_param_count = (uint32_t (*)(const MethodInfo *)) shadowhook_dlsym(handle,
                                                                                              "il2cpp_method_get_param_count");
        il2cpp_method_get_param = (const Il2CppType *(*)(const MethodInfo *, uint32_t)) shadowhook_dlsym(handle,
                                                                                              "il2cpp_method_get_param");
        il2cpp_class_from_il2cpp_type = (Il2CppClass *(*)(const Il2CppType *)) shadowhook_dlsym(handle,
                                                                                                  "il2cpp_class_from_il2cpp_type");
    } else {
        __android_log_print(ANDROID_LOG_ERROR, "Il2cpp-Manager", "Failed to open %s: %s", LIB_NAME,
                            dlerror());
        return;
    }

    il2cpp_assembly_get_image_hook::hook();
}

String/Text 如何顯示在螢幕上?

在 Unity 中,有多種不同的方式將文字顯示在使用者的螢幕上,常用的有 TextMeshPro (UGUI)UI Toolkit。這兩種方法都使用不同的類別結構來實作,但它們共享相同的概念,我們的目標是取得設定的文字和取得文字相關的方法。在此教學中,將使用 TextMeshPro 為例。

根據官方文檔TextMeshPro中,主要相關Class有 TMP_TextTextMeshProUGUITextMeshProUGUI是用於顯示在UGUI的,而TMP_Text是所有TextMeshPro類別的基礎類別,所有 set/get Text 方法都以此為基礎。因此,我們的目標如下:

ClassMethodDescription
TMP_Textget_text()獲取文字內容
TMP_Textset_text()設定文字內容
TextMeshProUGUIonEnable()當物件啟用時,會呼叫此方法來更新顯示的文字

Hook IL2CPP API取得Unity.TextMeshPro.dll

要獲得 TMP_TextTextMeshProUGUI Class,我們需要使用 IL2CPP API 來取得 Unity.TextMeshPro.dll 中的類別。由於 IL2CPP 會在遊戲Activity開始時啟動並載入 .dll (static offset) 和指定Assembly位址,我們可以Hook載入Assembly API來取得Class Pointer。

  1. 匯入 IL2CPP Type 頭文件 (il2cpp-class-internals.h),
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef struct Il2CppAssemblyName
{
    const char* name;
    const char* culture;
    const uint8_t* public_key;
    uint32_t hash_alg;
    int32_t hash_len;
    uint32_t flags;
    int32_t major;
    int32_t minor;
    int32_t build;
    int32_t revision;
    uint8_t public_key_token[PUBLIC_KEY_BYTE_LENGTH];
} Il2CppAssemblyName;

typedef struct Il2CppAssembly
{
    Il2CppImage* image; // 由於我們不需要 Il2CppImage 資訊,可以在這裡將它轉換為 void*
    uint32_t token;
    int32_t referencedAssemblyStart;
    int32_t referencedAssemblyCount;
    Il2CppAssemblyName aname; // <-- 這是用於儲存Assembly名稱, 可以用它來識別.dll
} Il2CppAssembly;
  1. 使用 ShadowHook 來 Hook il2cpp_assembly_get_image Function,並取得 Unity.TextMeshPro.dll
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
namespace il2cpp_assembly_get_image_hook {
    void *orig;
    void *stub;

    void *proxy(Il2CppAssembly *assembly) {
        SHADOWHOOK_STACK_SCOPE();
        auto image_ptr = SHADOWHOOK_CALL_PREV(proxy, assembly);
        if (strcmp(assembly->aname.name, "Unity.TextMeshPro") == 0) {
            LOGD("Found Unity.TextMeshPro.dll");
            ...
        }

        return image_ptr;
    }

    void hook() {
        stub = shadowhook_hook_sym_name(
                LIB_NAME,
                "il2cpp_assembly_get_image",
                (void *) proxy,
                &orig
        );

        int err = shadowhook_get_errno();
        const char *error_msg = shadowhook_to_errmsg(err);
        __android_log_print(ANDROID_LOG_WARN, TAG, "hook return: %p, %d - %s", stub, err,
                            error_msg);
    }
}
  1. 取得 Unity.TextMeshPro.dll後,我們可以使用 il2cpp_image_get_class_count 來獲取 Class 數量,然後使用 il2cpp_image_get_class 來獲取全部Class Pointer (將其儲存為std::map)。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void *proxy(Il2CppAssembly *assembly) {
    SHADOWHOOK_STACK_SCOPE();
    auto image_ptr = SHADOWHOOK_CALL_PREV(proxy, assembly);
    if (strcmp(assembly->aname.name, "Unity.TextMeshPro") == 0) {
        LOGD("Found Unity.TextMeshPro.dll");
        // 來獲取 Class 數量
        size_t class_count = il2cpp_image_get_class_count((const Il2CppImage *) image_ptr);
        // 獲取全部 Class Pointer
        for (size_t i = 0; i < class_count; ++i) {
            auto *klass = (Il2CppClass *) il2cpp_image_get_class(
                    (const Il2CppImage *) image_ptr, i);
            if (!klass) {
                continue;
            }
            auto *class_name = il2cpp_class_get_name(klass);
            if (!class_name) {
                continue;
            }
            TMP_klass_map[class_name] = (Il2CppClass *) klass;
        }
        ...
    }
    return image_ptr;
}

取得 TMP_Text, TextMeshProUGUI 類別

現在我們已經取得了 Unity.TextMeshPro.dll 中的所有 Class Pointer,接下來我們需要找到 TMP_TextTextMeshProUGUI 這兩個類別。我們可以從Class Map取得TMP_TextTextMeshProUGUI pointer,然後使用 il2cpp_class_get_methods 來獲取相關的 Methods。

當我們準備TMP_TextTextMeshProUGUI代理函數(proxey function)時,我們需要再加入一個參數,也就是物件的實體指標(instance pointer) void *instance

ℹ️ onEnable() 和 set_text() 的代理函式稍後會談到

取得TextMeshProUGUI.onEnable() Method Pointer以及Hook

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Get the TextMeshProUGUI Class out
auto it = TMP_klass_map.find("TextMeshProUGUI");
if (it != TMP_klass_map.end()) {
    Il2CppClass *tmp_text_class = it->second;
    LOGD("Found TextMeshProUGUI class: %p", tmp_text_class);
    void *_iter = nullptr;
    while (auto method_info = il2cpp_class_get_methods(tmp_text_class, &_iter)) {
        auto method_name = il2cpp_method_get_name(method_info);
        if (strcmp(method_name, "OnEnable") == 0) {
            TextMeshProUGUI_onEnable = (void (*)(void *)) method_info->methodPointer;
            // Hook the OnEnable method
            TextMeshProUGUI_onEnable_hook::hook();
        }
    }
} else {
    LOGD("TMP_Text class not found in Unity.TextMeshPro.dll");
}

取得 TMP_Text.set_text() Method Pointer以及Hook

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Get the TMP_Text Class out
auto it = TMP_klass_map.find("TMP_Text");
if (it != TMP_klass_map.end()) {
    Il2CppClass *tmp_text_class = it->second;
    LOGD("Found TMP_Text class: %p", tmp_text_class);
    void *_iter = nullptr;
    while (auto method_info = il2cpp_class_get_methods(tmp_text_class, &_iter)) {
        auto method_name = il2cpp_method_get_name(method_info);
        auto param_count = il2cpp_method_get_param_count(method_info);
        LOGD("[TMP_Text] Found method: %s", method_name);
        if (strcmp(method_name, "get_text") == 0) {
            LOGD("Found method: %s", method_name);
            TMP_Text_get_text = (Il2CppString *(*)(void *)) method_info->methodPointer;
        }
        if (strcmp(method_name, "set_text") == 0) {
            LOGD("Found method: %s", method_name);
            TMP_Text_set_text = (void (*)(void *,
                                          Il2CppString *)) method_info->methodPointer;
            TMP_Text_set_text_hook::hook();
        }
        if (strcmp(method_name, "SetText") == 0 && param_count == 1) {
            auto *param_type = il2cpp_method_get_param(method_info, 0);
            auto *param_class = il2cpp_class_from_il2cpp_type(param_type);
            auto *param_class_name = il2cpp_class_get_name(param_class);
            LOGD("Found method: %s (param=%s)", method_name, param_class_name);
            if (strcmp(param_class_name, "String") == 0) {
                LOGD("Found method: %s", method_name);
                TMP_Text_SetText = (void (*)(void *,
                                             Il2CppString *)) method_info->methodPointer;
                TMP_Text_SetText_hook::hook();
            }
        }
    }
} else {
    LOGD("TMP_Text class not found in Unity.TextMeshPro.dll");
}

⚠️補充:在這裡額外Hook了TMP_Text.SetText()方法,這是因為雖然SetText()set_Text()可以變更文字,但它們不是相同的方法。set_text() 是一個屬性方法,而 SetText() 是一個實際的方法。在Demo中,有些文字使用SetText()方法來設定,因此我們也需要Hook它。

文字替換

現在我們已經取得了 TMP_TextTextMeshProUGUI 的相關方法,也Hook了,接下來我們可以實現文字替換的功能。首先,研究一下get_text()set_text()方法的參數和返回值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public virtual string text
{
    get
    {
        if (m_IsTextBackingStringDirty)
            return InternalTextBackingArrayToString();
        return m_text;
    }
    set
    {
        if (m_IsTextBackingStringDirty == false && m_text != null && value != null && m_text.Length == value.Length && m_text == value)
            return;
        m_IsTextBackingStringDirty = false;
        m_text = value;
        m_inputSource = TextInputSources.TextString;
        m_havePropertiesChanged = true;
        SetVerticesDirty();
        SetLayoutDirty();
    }
}

在這裡,我們可以看到它是一個string屬性,並且有一個getset方法。當我們調用get_text()時,它會返回一個字串,而當我們調用set_text()時,它會接受一個string作為參數。但小心,這是 C# 中的字串類型(UTF-16),而不是 C++ 中的字串類型。,正如我們之前所說,IL2CPP 將會實作 C# 環境,因此我們無法在 C++ 中直接將其轉換為 utf-16字串。但幸運的是,Il2CPP API 提供了一種方法,可將 C# 字串轉換為 C++ 字串。

1
Il2CppChar* il2cpp_string_chars(Il2CppString* str);

C#與C++字串轉換

在 il2cpp-object-internals.h 中,註解給了我們一個提示,Il2CppString 是 System.String,也就是 C# 的 String。

  1. 匯入 IL2CPP 字串相關的 Type 頭文件 (il2cpp-api-types.h)
1
2
3
4
5
6
7
8
9
typedef char16_t Il2CppChar;

// System.String
typedef struct Il2CppString
{
    Il2CppObject object;
    int32_t length;   ///< Length of string *excluding* the trailing null (which is included in 'chars').
    Il2CppChar chars[0];
} Il2CppString;
  1. 使用 API 將其轉換回 C++ 字串 (std::u16string)
1
2
Il2CppChar *chars = il2cpp_string_chars(text);
std::u16string str(chars, text->length);

onEnable()set_text() 的代理函式 (Proxy Function)

最後,透過使用找到的Method Pointer,我們使用 shadowhook_hook_func_addr() 來 Hook onEnable()set_text() 方法,並在代理函式中實現文字替換。

1
2
3
4
5
stub = shadowhook_hook_func_addr(
        (void *) TMP_Text_set_text,
        (void *) proxy,
        &orig
);

完整程式碼如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
namespace TMP_Text_set_text_hook {
    void *orig;
    void *stub;

    void proxy(void *instance, Il2CppString *text) {
        SHADOWHOOK_STACK_SCOPE();

        LOGD("TMP_Text_set_text_hook: instance=%p, text=%p", instance, text);
        if (text != nullptr) {
            Il2CppChar *chars = il2cpp_string_chars(text);
            std::u16string str(chars, text->length);
            LOGD("TMP_Text_set_text_hook: current text=%s",
                 std::string(str.begin(), str.end()).c_str());
        }
        SHADOWHOOK_CALL_PREV(proxy, instance, text);
    }

    void hook() {
        if (TMP_Text_set_text == nullptr) {
            __android_log_print(ANDROID_LOG_ERROR, TAG, "TMP_Text_set_text is NULL");
            return;
        }

        stub = shadowhook_hook_func_addr(
                (void *) TMP_Text_set_text,
                (void *) proxy,
                &orig
        );

        int err = shadowhook_get_errno();
        const char *error_msg = shadowhook_to_errmsg(err);
        __android_log_print(ANDROID_LOG_WARN, TAG, "(%p)hook return: %p, %d - %s",
                            TMP_Text_set_text, stub, err,
                            error_msg);
    }
}

namespace TMP_Text_SetText_hook {
    void *orig;
    void *stub;

    void proxy(void *instance, Il2CppString *text) {
        SHADOWHOOK_STACK_SCOPE();

        LOGD("TMP_Text_SetText_hook: instance=%p, text=%p", instance, text);
        if (text != nullptr) {
            Il2CppChar *chars = il2cpp_string_chars(text);
            std::u16string str(chars, text->length);
            LOGD("TMP_Text_SetText_hook: current text=%s",
                 std::string(str.begin(), str.end()).c_str());
        }
        SHADOWHOOK_CALL_PREV(proxy, instance, text);
    }

    void hook() {
        if (TMP_Text_SetText == nullptr) {
            __android_log_print(ANDROID_LOG_ERROR, TAG, "TMP_Text_SetText is NULL");
            return;
        }

        stub = shadowhook_hook_func_addr(
                (void *) TMP_Text_SetText,
                (void *) proxy,
                &orig
        );

        int err = shadowhook_get_errno();
        const char *error_msg = shadowhook_to_errmsg(err);
        __android_log_print(ANDROID_LOG_WARN, TAG, "(%p)hook return: %p, %d - %s",
                            TMP_Text_SetText, stub, err,
                            error_msg);
    }
}

最後,我們可以在 Logcat 中看到遊戲文字可以成功顯示

Logcat

取代原文效果實現

在這部分,我們將實現取代原文的效果。這可以通過在代理函式中修改文字來完成。

原本Demo樣子

修改文字

這部分,我們需要建立一個新的char[],透過il2cpp_string_new() API建立C# String,然後將其傳遞給 set_text() 方法。以下是修改後的代理函式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void proxy(void *instance, Il2CppString *text) {
    SHADOWHOOK_STACK_SCOPE();
    LOGD("TMP_Text_set_text_hook: instance=%p, text=%p", instance, text);
    if (text != nullptr) {
        Il2CppChar *chars = il2cpp_string_chars(text);
        std::u16string str(chars, text->length);
        LOGD("TMP_Text_set_text_hook: current text=%s",
             std::string(str.begin(), str.end()).c_str());
    }
    auto new_text = il2cpp_string_new("Replaced Text");
    // SHADOWHOOK_CALL_PREV(proxy, instance, text);
    SHADOWHOOK_CALL_PREV(proxy, instance, new_text);
}

⚠️注意:TextMeshProUGUI_onEnable() 是沒有C# String參數的,我們需要在之後手動呼叫set_text()方法來更新文字。

修改後的結果

進行翻譯

TMP_Text_set_text_hookTMP_Text_SetText_hook 的代理函式中,我們可以檢查當前的文字內容,然後將其替換為我們想要的文字。建立一個translator函式來實現翻譯功能:

1
2
3
4
5
6
7
#include <string>

namespace translator {

    std::string translate(std::string orig);

} // namespace translator

假設我已經準備好了一個翻譯對照表,將原文與翻譯後的文字存儲在一個map中。以下是翻譯函式的實現:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include "translator.h"

#include <map>

std::map<std::string, std::string> translation_map = {
    {"Hello, welcome to the test Demo!", "你好歡迎來到測試Demo!"},
    {"You can press the button nearby!", "你可以按下這附近的按鈕!"},
    {"You can also press the speed button!", "也可以按一下速度按鈕!"},
    {"This message keeps looping!", "此訊息不斷循環!"},
    {"This is the last message.", "這是最後一則訊息!"},
    {"Pressed the settings button.", "按下設定按鈕"},
    {"You clicked the save button.", "你點擊了儲存按鈕"},
    {"Setting", "[設定]"},
    {"Save", "[儲存]"},
};

namespace translator {

    std::string translate(std::string orig) {
        auto it = translation_map.find(orig);
        if (it != translation_map.end()) {
            return it->second;
        }
        return orig; // Return original if no translation found
    }

} // namespace translator

然後我們得出的結果是…

翻譯後的結果

😲全部都變成方格了!立即查看Logcat,發現不斷出現這個警告。

1
2
3
4
5
6
The character with Unicode value \u9215 was not found in the [LiberationSans SDF] font asset or any potential fallbacks. It was 
UnityEngine.DebugLogHandler:Internal_Log(LogType, LogOption, String, Object)
TMPro.TextMeshProUGUI:SetArraySizes(TextProcessingElement[])
TMPro.TextMeshProUGUI:OnPreRenderCanvas()
TMPro.TextMeshProUGUI:Rebuild(CanvasUpdate)
UnityEngine.UI.CanvasUpdateRegistry:PerformUpdate()

發現原來是因為LiberationSans SDF字體資源中沒有這個字元,這是因為我們使用的字體不包含中文字符。要解決這個問題,我們需要將字體替換為支持中文的字體… 如果是在Unity Editor中,這很簡單,只需將字體替換為支持中文的字體即可。但在已經建立的Game中,我們可以怎樣做呢…

這個留在下一篇文章中討論…

使用 Hugo 建立
主題 StackJimmy 設計