UnityPy를 이용한 유니티 게임 MonoBehaviour 특정 텍스트 필드 추출/삽입

2025. 3. 4. 22:54·한글패치 관련 짧은 글들

기록용으로 작성.

UnityPy의 TypeTree 모듈이 완벽하지 않으므로 일부가 추출되지 않거나 미지원되는 경우가 있습니다.

 

 

 

사용법:

1. 게임의 루트 디렉토리(exe가 있는 폴더)에 해당 .py파일 혹은 .exe파일을 놓는다.

2. 명령어를 통해 .py파일 혹은 exe 파일을 실행한다.

 

 

 

명령어

MonoBehaviour_bulkedit_tool.exe --mode <Export/Import> --classname <클래스명> --fieldname <필드명> --csvname <추출/삽입에 사용할 CSV이름> [--forcereplace] [--filternumber]

forcereplace는 에셋의 PathID가 바뀌었을 경우 (업데이트 등)을 대응하기 위한 옵션이며, 원문 텍스트가 같을 경우 번역문을 삽입합니다.

filternumber는 텍스트가 숫자로만 이루어져 있을 경우, 필터링하여 추출/삽입되지 않게 합니다.

 

 

 

환경

- 파이썬 3.12.9

- UnityPy 1.21.1

- TypeTreeGeneratorAPI 0.0.5

 

 

 

컴파일된 바이너리와 bat파일

비밀번호: snow

https://drive.google.com/file/d/17-nOHXqIAVQSpFvgHK2pR4XaJUp8uM0n/view?usp=sharing

 

MonoBehaviour_bulkedit_tool.zip

 

drive.google.com

 

 

 

코드

import UnityPy
import argparse
import sys
import os
import csv
from UnityPy.helpers.TypeTreeGenerator import TypeTreeGenerator
from UnityPy.helpers import TypeTreeHelper

TypeTreeHelper.read_typetree_boost = False


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


def validate_mode_argument(input_value: str) -> str:
    lower_value = input_value.lower()
    if lower_value in ("import", "i"):
        return "Import"
    elif lower_value in ("export", "e"):
        return "Export"
    else:
        raise argparse.ArgumentTypeError('Invalid mode. Use "import"/"i" or "export"/"e".')


def detect_unity_version(game_data_path: str) -> str:
    resources_directory = os.path.join(game_data_path, "Resources")
    default_resources = os.path.join(resources_directory, "unity default resources")
    asset_env = UnityPy.load(default_resources)
    return asset_env.objects[0].assets_file.unity_version


def parse_translation_csv(csv_file_path: str, force_replace_mode: bool) -> dict:
    translation_data: dict = {}
    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[3]) == "":
                continue
            if force_replace_mode:
                translation_data.setdefault(entry[0], {})[sanitize_text(entry[2])] = entry[3]
            else:
                translation_data.setdefault(entry[0], {})[entry[1]] = {"Source": entry[2], "Target": entry[3]}
    return translation_data


def export_translation_data(
    root_dir: str, asset_file_name: str, csv_writer, type_tree_generator, class_name: str, field_name: str, filter_numeric: bool
):
    try:
        asset_env = UnityPy.load(os.path.join(root_dir, asset_file_name))
        asset_env.typetree_generator = type_tree_generator
    except Exception as load_error:
        return

    for asset_object in asset_env.objects:
        if asset_object.type.name == "MonoBehaviour":
            try:
                mono_behavior_data = asset_object.read(check_read=False)
                script_reference = mono_behavior_data.m_Script.read()
                type_tree = asset_object.read_typetree()
            except Exception as read_error:
                continue

            if script_reference.m_ClassName == class_name:
                if not type_tree.get(field_name):
                    continue

                cleaned_text: str = sanitize_text(type_tree[field_name])
                if cleaned_text:
                    if filter_numeric and cleaned_text.isdigit():
                        continue

                    csv_writer.writerow([asset_file_name, asset_object.path_id, type_tree[field_name], ""])
                    print(f"File: {asset_file_name}, PathID: {asset_object.path_id}, Text: {type_tree[field_name]}")


def import_translation_data(
    root_dir: str,
    asset_file_name: str,
    translation_map: dict,
    type_tree_generator,
    class_name: str,
    field_name: str,
    force_replace_mode: bool,
    filter_number: bool,
) -> None:
    if asset_file_name not in translation_map:
        return

    try:
        asset_env = UnityPy.load(os.path.join(root_dir, asset_file_name))
        asset_env.typetree_generator = type_tree_generator
    except Exception as load_error:
        print(f"Error loading {asset_file_name}")
        return

    for asset_object in asset_env.objects:
        if asset_object.type.name == "MonoBehaviour":
            try:
                mono_behavior_data = asset_object.read(check_read=False)
                script_reference = mono_behavior_data.m_Script.read()
                type_tree = asset_object.read_typetree()
            except Exception as read_error:
                continue

            if script_reference.m_ClassName == class_name:
                if force_replace_mode:
                    if not type_tree.get(field_name):
                        continue

                    cleaned_source: str = sanitize_text(type_tree[field_name])
                    if not cleaned_source:
                        continue

                    translated_text: str = None
                    if cleaned_source in translation_map[asset_file_name]:
                        translated_text = translation_map[asset_file_name][cleaned_source]
                    else:
                        for filename in translation_map:
                            if cleaned_source in translation_map[filename]:
                                translated_text = translation_map[filename][cleaned_source]
                                break

                    if translated_text:
                        if filter_number and (translated_text.isdigit() or cleaned_source.isdigit()):
                            continue
                        original_text: str = type_tree[field_name]
                        type_tree[field_name] = translated_text
                        asset_object.save_typetree(type_tree)
                        print(f"File: {asset_file_name}, PathID: {asset_object.path_id}, Text: {original_text} -> {translated_text}")
                else:
                    path_id_str: str = str(asset_object.path_id)
                    if path_id_str not in translation_map[asset_file_name]:
                        continue

                    if not type_tree.get(field_name):
                        continue

                    original_text: str = type_tree[field_name]
                    translated_text: str = translation_map[asset_file_name][path_id_str]["Target"]
                    if filter_number and (original_text.isdigit() or translated_text.isdigit()):
                        continue
                    type_tree[field_name] = translated_text
                    asset_object.save_typetree(type_tree)
                    print(f"File: {asset_file_name}, PathID: {path_id_str}, Text: {original_text} -> {translated_text}")

    modified_asset_data = asset_env.file.save()
    with open(os.path.join(root_dir, asset_file_name), "wb") as asset_file:
        asset_file.write(modified_asset_data)


def main():
    # fmt:off
    argument_parser = argparse.ArgumentParser(description="TextMeshProUGUI tool by Snowyegret, version: 1.0")
    argument_parser.add_argument("-m", "--mode", type=validate_mode_argument, help="(Required) Specify the mode: Import|I or Export|E)", default="Export")
    argument_parser.add_argument("--classname", type=str, help="(Requied) Specify the class name to extract/insert", default="TextMeshProUGUI")
    argument_parser.add_argument("--fieldname", type=str, help="(Requied) Specify the field name to extract/insert", default="m_text")
    argument_parser.add_argument("--csvname", type=str, help="(Required) The name of the CSV file", default="TextMeshProUGUI.csv")
    argument_parser.add_argument("--forcereplace", action="store_true", help="(Optional) Insert translations by text comparison, not by PathID [Default: False]", default=False)
    argument_parser.add_argument("--filternumber", action="store_true", help="(Optional) filter numbers when export csv [Default: True]", default=True)
    args = argument_parser.parse_args()

    script_directory: str = os.path.dirname(sys.executable) if getattr(sys, "frozen", False) else os.path.dirname(__file__)
    game_data_dir: str = os.path.join(script_directory, [i for i in os.listdir(script_directory) if i.endswith("_Data")][0])
    managed_assemblies_dir: str = os.path.join(game_data_dir, "Managed")
    il2cpp_binary_path: str = os.path.join(script_directory, "GameAssembly.dll")
    metadata_path: str = os.path.join(game_data_dir, "il2cpp_data", "Metadata", "global-metadata.dat")
    csv_file_path: str = os.path.join(script_directory, args.csvname)

    if not game_data_dir:
        print(f"Data folder not found at {script_directory}")
        print("Make sure the program is in the game's root folder")
        input("Press Enter to exit...")
        sys.exit(0)

    unity_version: str = detect_unity_version(game_data_dir)
    type_tree_generator = TypeTreeGenerator(unity_version)

    if os.path.exists(managed_assemblies_dir):
        type_tree_generator.load_local_game(script_directory)
    elif os.path.exists(il2cpp_binary_path) and os.path.exists(metadata_path):
        with open(il2cpp_binary_path, "rb") as f:
            il2cpp_binary = f.read()
        with open(metadata_path, "rb") as f:
            il2cpp_metadata = f.read()
        type_tree_generator.load_il2cpp(il2cpp_binary, il2cpp_metadata)
    else:
        print("Failed to load game assemblies")
        input("Press Enter to exit...")
        sys.exit(0)
    #fmt: off
    print(f"CSV File: {args.csvname}, Mode: {args.mode}, ClassName: {args.classname}, FieldName: {args.fieldname}, Forcereplace: {args.forcereplace}, FilterNumber: {args.filternumber}")
    print(f"Unity version: {unity_version}")

    if args.mode == "Export":
        with open(csv_file_path, "w", encoding="utf-8", newline="") as csv_file:
            csv_writer = csv.writer(csv_file, quoting=csv.QUOTE_ALL)
            csv_writer.writerow(["File", "PathID", "Source", "Target"])
            for root_dir, _, file_list in os.walk(script_directory):
                for filename in file_list:
                    export_translation_data(root_dir, filename, csv_writer, type_tree_generator, args.classname, args.fieldname, args.filternumber)

    elif args.mode == "Import":
        translation_map: dict = parse_translation_csv(csv_file_path, args.forcereplace)
        for root_dir, _, file_list in os.walk(script_directory):
            for filename in file_list:
                import_translation_data(root_dir, filename, translation_map, type_tree_generator, args.classname, args.fieldname, args.forcereplace, args.filternumber)


if __name__ == "__main__":
    main()

 

 

 

빌드 명령어

python -m PyInstaller --onefile --console --add-data "C:\Program Files\Python312\Lib\site-packages\UnityPy\resources\uncompressed.tpk;UnityPy/resources" MonoBehaviour_bulkedit_tool.py --add-data "C:\Program Files\Python312\Lib\site-packages\TypeTreeGeneratorAPI\TypeTreeGeneratorAPI.dll;TypeTreeGeneratorAPI"

 

저작자표시 비영리 (새창열림)

'한글패치 관련 짧은 글들' 카테고리의 다른 글

UnityPy 텍스쳐 삽입 시 "params must be an instance of BC7CompressBlockParams" 오류 해결  (0) 2025.03.31
global-metadata.dat이 없는 경우의 덤프법 (frida 이용)  (0) 2025.03.23
IoStore를 사용하며 sig우회가 안되는 언리얼 게임 모드 로딩 방지 우회하기  (6) 2025.01.20
유니티 게임에서 대사 검색 쉽게 하기 (UnityPy 이용)  (4) 2025.01.01
catalog.json과 catalog.hash가 있는 유니티 게임의 수정법  (0) 2024.12.18
'한글패치 관련 짧은 글들' 카테고리의 다른 글
  • UnityPy 텍스쳐 삽입 시 "params must be an instance of BC7CompressBlockParams" 오류 해결
  • global-metadata.dat이 없는 경우의 덤프법 (frida 이용)
  • IoStore를 사용하며 sig우회가 안되는 언리얼 게임 모드 로딩 방지 우회하기
  • 유니티 게임에서 대사 검색 쉽게 하기 (UnityPy 이용)
Snowyegret
Snowyegret
기록용 블로그입니다.
  • Snowyegret
    Snowyegret
    Snowyegret
  • 공지사항

    • 문의: 디스코드 snowyegret12
    • 연락처(디스코드) 계정 변경 안내
    • 블로그 운영 재개 안내
  • 링크

    • 블로그 메인
    • 방명록/문의사항
    • Github Repositories
    • Github Gists
    • 분류 전체보기 (88)
      • 늑대와 향신료 VR2 한글패치 (1)
      • 장난을 잘 치는 타카기양 VR 1학기 한글패치 (1)
      • Shephy 카드 설명 한국어 텍스쳐 모드 (1)
      • 한글패치 관련 짧은 글들 (37)
      • 한글패치 작업 내역 (11)
      • 한글화 분석 (작업X) (5)
      • python snippets (4)
      • 게임 (2)
      • 간단프로그램 (2)
      • IT (18)
      • AI (1)
      • TODO (2)
  • 인기 글

  • 최근 글

  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.5
Snowyegret
UnityPy를 이용한 유니티 게임 MonoBehaviour 특정 텍스트 필드 추출/삽입
상단으로

티스토리툴바