한글패치 관련 짧은 글들

UnityPy TypeTree 적용 (작성중)

Snowyegret 2023. 9. 15. 19:30

아직 제대로 테스트해보지 않았지만, 일단 기록용으로 작성함.

 

필요 프로그램: TypeTreeGenerator ( https://github.com/K0lb3/TypeTreeGenerator )

 

 

 

0. UnityPy 버전

- 1.21.2

(1.10.18버전이 구버전 중 제일 안정적이나, 최신버전으로 선택하였다.)

 

 

 

1. 타입트리 생성

TypeTreeGenerator를 다운받아서 {게임명}_data\TypeTreeGenerator 폴더 안에 압축해제한다.

 

import UnityPy
import subprocess
import os
from concurrent.futures import ThreadPoolExecutor, as_completed


def generate_single_typetree(ttg_exe, dll_folder, dll, unity_version, output_folder):
    output_path = os.path.join(output_folder, f"{os.path.splitext(dll)[0]}.json")
    cmd = [ttg_exe, "-p", dll_folder, "-a", dll, "-v", unity_version, "-d", "json", "-o", output_path]
    try:
        subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
    except subprocess.CalledProcessError as e:
        print(f"Error generating typetree for {dll}: {e.stderr.decode().strip()}")


def gen_typetree(game_data_folder, unity_version=None):
    dll_folder = os.path.join(game_data_folder, "Managed")
    ttg_exe = os.path.join(game_data_folder, "TypeTreeGenerator", "TypeTreeGeneratorCLI.exe")
    output_folder = os.path.join(game_data_folder, "typetree")
    os.makedirs(output_folder, exist_ok=True)
    dll_files = [file for file in os.listdir(dll_folder) if file.endswith(".dll")]

    with ThreadPoolExecutor(max_workers=4) as executor:
        futures = [executor.submit(generate_single_typetree, ttg_exe, dll_folder, dll, unity_version, output_folder) for dll in dll_files]
        for future in as_completed(futures):
            future.result()


def main():
    game_folder = os.path.dirname(os.path.abspath(__file__))
    resources_path = os.path.join(game_folder, "Resources", "unity default resources")
    env = UnityPy.load(resources_path)
    gen_typetree(game_folder, env.file.unity_version)


if __name__ == "__main__":
    main()

이후, 위 코드를 gen_typetree.py로 저장하고 나서 {게임명}_data 폴더에서 실행하면

{게임명}_data/typetree 폴더 내 dump된 typetree들이 json파일에 담기게 된다.

 

 

 

2. 타입트리 사용

import UnityPy
import os
import json
import csv
import sys
from UnityPy.helpers import TypeTreeHelper

TypeTreeHelper.read_typetree_boost = False


def load_typetree(fp: str) -> dict[str, dict]:
    json_lst = [i for i in os.listdir(fp) if i.endswith(".json")]
    load_tree: dict[str, dict] = {}
    for json_file in json_lst:
        clean_fn = os.path.splitext(json_file)[0]
        if load_tree.get(clean_fn) is None:
            load_tree[clean_fn] = {}
        try:
            with open(f"{os.path.join(fp, json_file)}", "r", encoding="utf-8") as f:
                load_tree[clean_fn] = json.load(f)
        except Exception as e:
            pass
    return load_tree


def sanitize_text(text: str) -> str:
    return text.replace("\n", "").replace("\r", "").replace("\t", "").replace(" ", "").replace("​", "")


def parse_translation_csv(csv_file_path: str) -> dict[str, dict[str, dict[str, str]]]:
    translation_data: dict[str, dict[str, dict[str, str]]] = {}
    with open(csv_file_path, "r", encoding="utf-8", newline="") as csv_file:
        csv_reader = list(csv.reader(csv_file))
        if csv_reader:
            csv_reader.pop(0)
        for entry in csv_reader:
            if len(entry) < 4 or sanitize_text(entry[2]) == "" or sanitize_text(entry[3]) == "":
                continue
            else:
                translation_data.setdefault(entry[0], {})[entry[1]] = {
                    "Source": entry[2],
                    "Target": entry[3],
                }
    return translation_data


def export_asset_monobehaviour(
    root_dir: str,
    file_name: str,
    trees: dict[str, dict[str, dict]],
    writer: csv.writer,
    handler: object,
) -> None:
    asset_path = os.path.join(root_dir, file_name)
    env = UnityPy.load(asset_path)

    objects = [obj for obj in env.objects if obj.type.name == "MonoBehaviour"]

    for obj in objects:
        monobehaviour = obj.read(check_read=False)

        try:
            script = monobehaviour.m_Script.read()
        except:
            continue

        if obj.serialized_type.nodes:
            tree = obj.read_typetree(check_read=False)
        else:
            assembly_name = script.m_AssemblyName.replace(".dll", "")
            namespace = script.m_Namespace
            class_name = script.m_ClassName
            class_key = f"{namespace}.{class_name}" if namespace else class_name

            assembly_trees = trees.get(assembly_name)
            if not assembly_trees:
                continue

            typetree = assembly_trees.get(class_key)
            if not typetree:
                continue

            try:
                tree = obj.read_typetree(typetree, check_read=False)
            except:
                continue

        text = None
        if "storyText" in tree and isinstance(tree["storyText"], str):
            text = tree["storyText"]
        elif "transitionText" in tree and isinstance(tree["transitionText"], dict):
            text_candidate = tree["transitionText"].get("stringVal")
            if isinstance(text_candidate, str):
                text = text_candidate
        elif "stringData" in tree and isinstance(tree["stringData"], dict):
            text_candidate = tree["stringData"].get("stringVal")
            if isinstance(text_candidate, str):
                text = text_candidate
        elif "m_text" in tree and isinstance(tree["m_text"], str):
            text = tree["m_text"]

        if text:
            sanitized_text = sanitize_text(text)
            if sanitized_text and not sanitized_text.isdigit():
                writer.writerow([file_name, obj.path_id, text, ""])

    handler.flush()


def import_asset_monobehaviour(
    root_dir: str,
    file_name: str,
    trees: dict[str, dict[str, dict]],
    csvdata: dict[str, dict[str, dict[str, str]]],
) -> None:
    file_csvdata = csvdata.get(file_name)
    if not file_csvdata:
        return

    asset_path = os.path.join(root_dir, file_name)
    env = UnityPy.load(asset_path)
    modified = False

    for obj in env.objects:
        obj_id_str = str(obj.path_id)
        csv_entry = file_csvdata.get(obj_id_str)
        if not csv_entry or obj.type.name != "MonoBehaviour":
            continue

        monobehaviour = obj.read(check_read=False)

        try:
            script = monobehaviour.m_Script.read()
        except:
            continue

        if obj.serialized_type.nodes:
            tree = obj.read_typetree(check_read=False)
            typetree = None
        else:
            assembly_name = os.path.splitext(script.m_AssemblyName)[0]
            print(assembly_name)
            namespace = script.m_Namespace
            class_name = script.m_ClassName
            class_key = f"{namespace}.{class_name}" if namespace else class_name

            typetree = trees.get(assembly_name, {}).get(class_key)
            if not typetree:
                continue

            try:
                tree = obj.read_typetree(typetree, check_read=False)
            except:
                continue

        target_text = csv_entry["Target"]
        updated = False

        if isinstance(tree.get("storyText"), str):
            tree["storyText"] = target_text
            updated = True

        elif isinstance(tree.get("transitionText"), dict):
            if isinstance(tree["transitionText"].get("stringVal"), str):
                tree["transitionText"]["stringVal"] = target_text
                updated = True

        elif isinstance(tree.get("stringData"), dict):
            if isinstance(tree["stringData"].get("stringVal"), str):
                tree["stringData"]["stringVal"] = target_text
                updated = True

        elif isinstance(tree.get("m_text"), str):
            tree["m_text"] = target_text
            updated = True

        if updated:
            obj.save_typetree(tree, typetree)
            modified = True

    if modified:
        bdata = env.file.save()
        with open(asset_path, "wb") as f_out:
            f_out.write(bdata)


def main() -> None:
    current_directory: str = os.path.dirname(sys.executable) if getattr(sys, "frozen", False) else os.path.dirname(__file__)
    typetree: dict[str, dict] = load_typetree("./typetree")

    # # for export
    # with open("./data.csv", "w", encoding="utf-8", newline="") as f:
    #     writer: csv.writer = csv.writer(f, quoting=csv.QUOTE_ALL)
    #     writer.writerow(["FileName", "PathID", "Src", "Dst"])
    #     for root_dir, _, file_list in os.walk(current_directory):
    #         for filename in file_list:
    #             export_asset_monobehaviour(root_dir, filename, typetree, writer, f)

    # # for import
    # translated_data: dict[str, dict[str, dict[str, str]]] = parse_translation_csv("./data.csv")
    # for root_dir, _, file_list in os.walk(current_directory):
    #     for filename in file_list:
    #         import_asset_monobehaviour(root_dir, filename, typetree, translated_data)


if __name__ == "__main__":
    main()

위 경우 특정 .assets만 작업하기에 그냥 리스트를 만들어서 해당 리스트를 순회하도록 하였다.

UnityPy.load()는 file path, folder path, stream, bytes object를 받아올 수 있으니 임의대로 수정해서 사용하자.

 

또한, script의 m_Namespace가 없는 경우

typetree dump json에서 script.m_ClassName을 Key로 Value를 가져오면 정상적으로 적용이 됐었고,

ex) LabelUI

만약 m_Namespace가 있는 경우 f"{scipt.m_Namespace}.{script.m_ClassName}"을 Key로 Value를 가져오면 정상적으로 적용이 됐었다.

ex) TMPro.TMP_FontAsset

 

 

 

3. 기타등등...

https://github.com/K0lb3/UnityPy/issues/201#issuecomment-1719335452

 

can't read typetree from assets · Issue #201 · K0lb3/UnityPy

Code import UnityPy import json from typing import Dict class FakeNode: def __init__(self, **kwargs): self.__dict__.update(**kwargs) with open("./assembly_typetrees.json", "r", encoding="utf-8") as...

github.com

누군가 Typetree dump 적용에 관해 질문한 글이다.

답변을 보면 특정 클래스로 typetree dump를 읽어오는 것이 보이는데,

아마 답변의 첫번째 방법은 script.m_Namespace가 있는 경우 KeyError를 내면서 적용이 안 될 것이다.

두번째 방법은 잘 모르겠다. 나중에 직접 사용해봐야 알 듯.