한글화 분석 (작업X)

sailing era 한글화 분석

Snowyegret 2023. 8. 1. 19:22

 
선요약:
- 한글패치 제작 시 수동 번역을 위해선 table.bytes 파일의 구조를 파악해야 자유로운 수정이 가능. 즉, 구조 파악이 안 된 현재로썬 바이트를 맞춰 번역하는 작업이 필요함.
- autotranslator를 이용한 자동 번역 가능.
- 유니티 모딩에 조예가 깊다면 따로 모드를 제작해여 수정할 수도 있어보임.
- 이하 내용은 기본분석 및 수동번역 시도, 자동번역 / 2 파트로 이루어져 있음. 

 

1. 기본분석 및 수동번역

유니티 버전: 2020.3.36f1
il2cpp
데모판으로 작업
 
 
 

게임 실행 모습
목표 텍스트는 An upbeat and cheerful~ 부분
 
 
 

Everything으로 텍스트 검색을 시도해보자.
 
 
 

검색 결과가 안나온다.
이러면 몇가지 가능성으로 생각해볼 수 있다.
1. dll에 하드코딩되어 들어있음.
2. 번들파일에 들어있으며, 압축되어 있어 데이터가 그대로 검색되지 않음.
3. 번들파일에 들어있으며, xor과 같은 모종의 암호화가 되어있음.
 
 
 

dnspy로 해당 텍스트가 검색되지 않아서 번들 파일을 가져다가 UABEA로 열어보니, 열리지 않았음. 
 
 
 

HxD로 살펴보니 원래 있어야 할 헤더가 없음
-> 보통 에셋번들에 xor을 많이 쓰니, xor로 추정
 
 
 

몇몇 주요 포럼을 중심으로 검색하던 중, xor 관련 내용을 발견
 
 
 

EuC~로 시작하는 키로 첫 0x400부분이 xor이 되어있다고 함.
 
 
 

근데 키가 241자임.
241글자의 키를 순회하며 1024바이트를 xor한다는 걸 예상 가능.
 
 
 

import itertools
import os
import concurrent.futures


def xor_bundle(file):
    with open(f"StandaloneWindows64/{file}", "rb") as f:
        data = f.read()
        result = bytes(
            itertools.starmap(
                lambda x, y: x ^ y, zip(data[:0x400], itertools.cycle(XOR_KEY))
            )
        )
    with open(f"StandaloneWindows64_dec/{file}", "wb") as f:
        f.write(result + data[0x400:])
    print(f"작업 완료: {file}")


def xor_bundles(files):
    with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
        futures = [executor.submit(xor_bundle, file) for file in files]
        for future in concurrent.futures.as_completed(futures):
            try:
                future.result()
            except Exception as e:
                print(f"Exception: {e}")


os.makedirs("StandaloneWindows64_dec", exist_ok=True)
XOR_KEY = b"EuCVe&D9%gQDEQMpP&1kLn645TnL@kg4jdu@CkjfUi#ypRcSc5d8gI!0b5m6C7fxpzI5ZFDqO6*D#ntWgan0mlIUaQWu4CS$ydy&8rPi#8b9OI@ZDH9TMsiOZap4oeBGlzfW6o^42v#anb2w9Zx*k^eBrDGP%Tem&gf1H8ZIUS5b0tTuLk4Zdd39*zBDRXOH60@RlCFPyC8^cnlY6c5LFxf^uUyFPsm6hP#P9Sx8vma62^gMq"
bundle_files = [i for i in os.listdir("StandaloneWindows64") if i.endswith(".bundle")]
xor_bundles(bundle_files)

 
Assetbundle쪽 폴더에 파이썬 파일을 만들자.
그 다음, lambda와 itertools.cycle을 이용하여 1바이트씩 돌려가며 xor을 하자.
파일 갯수가 상당하니 멀티쓰레드를 이용하여 작업을 빠르게 하는게 좋다.
 
 
 

xor을 하고 나니 정상적으로 돌아온 모습.
근데도 아직 검색이 안돼서 번들을 열어보니, 압축이 되어있다는 걸 발견했다.
UnityEX로 일괄 압축해제를 하고 다시 검색해보니
9edaf486fccd94ae0cc8b8a100ba6f8a.bundle 파일에서 텍스트가 검색되었다.
 
 
 

table.bytes 파일이었고, 검색된 부분의 텍스트 데이터는 일반적인 string 형식인 것 같았다.
길이 4바이트 + 길이만큼의 텍스트 데이터 (UTF-8) 구조인데
문제는 저 앞에 여러 바이너리 데이터들이 있으며, 텍스트의 길이를 줄이거나 늘리면 게임이 깨지는 것으로 보아
앞쪽에 오프셋 테이블이 따로 있는 것 같았다.
근데 mono 게임처럼 dll이 다 까여있는 것도 아니고, 내가 리버스 엔지니어링을 할 수 있는 것이 아니므로 포기.
 

import csv

def calc_padding(length):
    return (4 - (length % 4)) % 4 or 4

with open("table.txt", "rb") as f:
    data = f.read()

START_OFFSET = 0x95E8EC # 1
START_OFFSET = 0x11ED24C # 2
with open(f"table_{hex(START_OFFSET).upper()}.csv", "w", encoding="utf-8", newline="") as w:
    writer = csv.writer(w)
    writer.writerow(["seq", "text"])

    ext_offset = 0x0
    while True:
        try:
            # 텍스트
            length = data[START_OFFSET+ext_offset:START_OFFSET+ext_offset+0x4]
            length_hex = length.hex()
            length = int.from_bytes(length, "little")
            ext_offset += 0x4
            text_padding = calc_padding(length)
            text = data[START_OFFSET+ext_offset:START_OFFSET+ext_offset+length]
            ext_offset += length + text_padding

            # 번호
            num_length = data[START_OFFSET+ext_offset:START_OFFSET+ext_offset+0x4]
            num_length_hex = num_length.hex()
            num_length = int.from_bytes(num_length, "little")
            ext_offset += 0x4
            num_padding = calc_padding(num_length)
            num = data[START_OFFSET+ext_offset:START_OFFSET+ext_offset+num_length]
            ext_offset += num_length + num_padding
            ext_offset += 12 # unk

            writer.writerow([num.decode("utf-8"), text.decode("utf-8")])
        except:
            break

텍스트 추출까지만 테스트해봤다.

재삽입 가능 여부는 모른다.

근데 수동으로 테스트해봤을 때 안먹혔던 걸 보면, 아마 텍스트 오프셋 테이블이 따로 있지 않나 싶다.
 
 

 

그러므로 누군가 추가적인 분석을 하지 않는 한 텍스트 길이를 맞춰야 수동 번역이 가능하다.
영어버전으로 작업했기에 바이트를 맞추는 작업이 쉽지 않았으나,
게임이 중국어, 일본어를 지원하기 때문에 해당 언어를 타겟으로 한다면 조금 수월하지 않을까 생각된다.
텍스트 작업은 UnityText2로 하되, 파이썬 프로그래밍을 통해 데이터 길이 체크 기능을 넣으면 문제 없이 번역이 되지 않을까 생각한다.
 
참고로 에셋번들 보호가 들어가있으므로 catalog.json을 패치해줘야 정상적으로 읽힌다.
 
또한, 압축 해제된 번들들을 AssetStudio로 불러오니 SDF폰트 및 일반폰트들이 보였는데, 위와 같은 영문 버전의 경우 따로 폰트에 손을 대지 않아도 한글이 출력됐으며, 일본어 및 중국어는 테스트해보지 않았다.
 
 
 

2. 자동번역 (XUnity.Autotranslator 이용)

 
다운로드(구글드라이브): https://drive.google.com/file/d/1m5whTONrOKLvD7eb724SkSTuWUlPEgjB/view?usp=drive_link 

다운로드(Pixeldrain): https://pixeldrain.com/u/H17BLWET

비밀번호: snow

* 다운로드 제한 이슈가 있어, Pixeldrain에도 업로드를 하였다. 둘 중 하나만 받아서 사용하자.


비밀번호는 구글 드라이브의 잦은 오탐에 의해 걸어놨으며, 해외 블로그에서 파일을 가져온 후 설정파일 수정만 조금 한 것이므로, 만약 오탐이 아니더라도 불이익/파일에 대해 책임지지 않음. 다운로더 본인이 감수할 수 있는 경우에만 사용 바람.
 

만약 XUnity.Autotranslator 빌드를 직접 할 수 있는 사람이라면 이곳을 참고하여 빌드하여도 사용이 정상적으로 되는 것 같다. 테스트는 해보지 않았다.
 

- 게임이 설치된 폴더에 위 사진처럼 압축해제를 한 후, 게임 실행

   (기본 설정: 파파고 번역, 영->한 번역)
- 첫 실행시, cpp2il 작업때문에 1~3분정도 시간이 소요될 수 있음
- 기 번역되지 않은 텍스트의 경우 번역 딜레이가 있을 수 있음. 딜레이를 줄이는 방법이 있으나, 추천하지 않음.
- BepInEx\Translation\ko\Text\_AutoGeneratedTranslations.txt를 수정하여 번역을 마음대로 수정 가능함.
- Alt+T를 눌러 원문/번역문 토글 가능
- Steam을 통해 실행해야 오류가 뜨지 않는 듯? (exe를 통한 실행 X)
 

번역이 제대로 된 모습.
 
 
만약 DeepL을 통해 더 나은 품질의 번역으로 게임을 플레이하고 싶다면
다음 링크를 참고하자.
https://snowyegret.tistory.com/70

 

XUnity.AutoTranslator에서 DeepL 사용하기

테스트 게임: Sailing era Demo ( https://store.steampowered.com/app/2161440/_Sailing_Era/ ) 1. DeepL 가입 및 API Key 발급 https://www.deepl.com/pro-api?cta=header-pro-api DeepL API 번역 | 기계번역 기술 번역에 필요한 모든 것을 한

snowyegret.tistory.com

 
 
 
 
만약 번역 딜레이를 줄이고 싶다면 다음 파일을 받아서 압축해제한 후
{게임폴더}\BepInEx\plugins\XUnity.AutoTranslator에 덮어씌우자.
 
기본 번역 딜레이가 0.9초인데, 이것을 0.5초/0.3초로 줄여주는 패치이다.
* 번역 딜레이가 줄어들면 특정 상황에서 짧은 시간에 여러 번 요청을 보내게 되므로 번역 사이트에서 밴을 당할 수가 있다. 0.3초 버전도 원활하게 작동하는 것을 테스트하였으나, 찜찜하면 원본~0.5초 버전을 사용하길 권장한다.
 
0.3초버전(구글드라이브): https://drive.google.com/file/d/1lvZJx0qc2Cb_DBAZljtvRaGa8LIRmyK-/view?usp=sharing 

0.3초버전(Pixeldrain): https://pixeldrain.com/u/HKzRy5Zr

 

0.5초버전(구글드라이브): https://drive.google.com/file/d/12OaSSC8UDxBSOMwPCkMCqRYnlfSlm9KE/view?usp=sharing 

0.5초버전(Pixeldrain): https://pixeldrain.com/u/waRhqX6v

 

비밀번호: snow