아직 제대로 테스트해보지 않았지만, 일단 기록용으로 작성함.
필요 프로그램: 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를 내면서 적용이 안 될 것이다.
두번째 방법은 잘 모르겠다. 나중에 직접 사용해봐야 알 듯.
'한글패치 관련 짧은 글들' 카테고리의 다른 글
번역을 위한 유니티 Il2cpp 게임의 복호화/암호화 (0) | 2024.12.18 |
---|---|
유니티 VideoClip 에셋 교체하기 (3) | 2023.11.18 |
XUnity.AutoTranslator에서 DeepL 사용하기 (19) | 2023.09.06 |
UnityPy를 이용한 bundle파일 내 Monobehaviour 일괄수정 (0) | 2023.08.29 |
게임메이커 게임 한글화 - 폰트 교체 (5) | 2023.04.08 |