닌텐도 DS모르는 용어는 용어집에서
16장 대사는 어떻게 저장되나
고스트 트릭의 대사는 장면 파일 안, 장면 진행 명령 사이에 2바이트 값으로 들어 있습니다. LZ11을 풀고, 글자와 제어 코드를 읽고, 명령 안의 포인터를 따라가 봅니다.
대사는 장면마다 파일 하나 쉽게
슈퍼 패미콤 게임은 대사를 롬 한쪽에 모아 두고 포인터 표로 찾는 경우가 많습니다(8장). 고스트 트릭은 대사를 장면마다 파일 하나에 나눠 둡니다. 제1장의 컷신 하나는 st01/st01_demo101_Expand.xml.lz, 스테이지 폴더(st01) 안에 장면 이름(demo101)을 단 파일입니다. 파일은 롬의 파일 시스템에서 이름으로 찾습니다(15장).
일본판 롬에는 이런 스크립트 파일이 193개 있습니다. 이름은 .xml로 끝나지만 안은 XML 글이 아닙니다. 압축을 풀면 1LMG로 시작하는 바이너리이고, 그 안에 인물을 움직이고 화면을 바꾸는 장면 진행 명령과 대사가 섞여 있습니다. 대사가 든 파일은 146개, 대사는 모두 3,503개입니다. 수첩과 시스템 메시지는 압축하지 않은 1LMG 파일 2개에 따로 있습니다.
대사 하나를 고치려면 이 순서를 밟습니다.
- 롬에서 파일을 꺼내 압축을 풉니다.
- 코드 구역에서 대사 블록을 찾아 2바이트 값을 글자로 읽습니다.
- 번역을 다시 2바이트 값으로 바꿔 넣고, 대사가 옮겨 간 만큼 포인터를 고칩니다.
- 다시 압축해 롬에 넣습니다. 다시 압축한 파일이 원래보다 커지면 15장의 규칙대로 자리를 옮깁니다.
먼저 압축 풀기: LZ11 쉽게
스크립트 파일은 LZ11로 압축되어 있습니다. 게임보이 어드밴스·DS 본체의 BIOS가 푸는 LZ77 형식(형식 바이트 10)을 늘린 것으로, DS 게임에서 흔히 봅니다. 첫 바이트가 형식(11), 다음 3바이트가 푼 크기입니다.
이 파일은 11 94 05 00으로 시작합니다. 푼 크기는 리틀 엔디안으로 0x000594, 곧 1,428바이트이고 롬에서는 936바이트입니다. 그 뒤는 9장의 LZSS처럼 플래그 바이트 하나가 뒤따르는 여덟 항목이 「그대로 쓰는 바이트」인지 「몇 칸 앞에서 몇 바이트 복사」인지 알려 줍니다.
더 깊이LZ11의 복사 지시 세 가지
플래그는 위 비트부터 읽고, 1이면 복사입니다. LZ11은 복사 지시의 첫 바이트 위 4비트로 길이를 적는 법을 고릅니다. 거리는 어느 경우든 마지막 두 바이트의 아래 12비트에 1을 더한 값이라 4,096바이트 앞까지 닿습니다.
- 위 4비트가 2–F: 2바이트. 길이는 위 4비트 + 1(3–16바이트).
- 위 4비트가 0: 3바이트. 길이는 다음 8비트 + 0x11(17–272바이트).
- 위 4비트가 1: 4바이트. 길이는 다음 16비트 + 0x111(273–65,808바이트).
BIOS가 푸는 LZ10(형식 바이트 10)은 늘 2바이트 복사(길이 3–18)만 씁니다. LZ11은 BIOS에 푸는 함수가 없어서 게임이 자기 코드로 풉니다. 다시 압축한 바이트는 원본과 달라도 됩니다. 게임은 푼 결과만 보기 때문입니다(9장 「게임마다 다른 LZSS」).
1LMG 파일의 네 구역 쉽게
푼 파일은 0x34바이트 헤더와 그 뒤의 세 구역으로 되어 있습니다. 헤더 +0x08이 코드 구역 크기, +0x0C가 이름 문자열 구역 크기, +0x10이 마지막 레이블 구역 크기입니다. 일본판 193개 파일 모두 0x34에 이 셋을 더하면 파일 크기와 같습니다.
코드 구역은 처음부터 끝까지 2바이트 값의 줄입니다. 대부분은 게임이 한 값씩 읽어 실행하는 장면 진행 명령(바이트코드)이고, 대사는 그 사이에 FF2D로 시작해 FFFE로 끝나는 블록으로 끼어 있습니다.
글자 하나 = 2바이트 쉽게
대사 블록 안의 값은 0xFF00보다 작으면 글자표 번호, FFxx면 제어 코드입니다. 파일에는 작은 자리 바이트가 먼저 적혀서(2장 엔디안) FF2D는 2D FF로 보입니다.
글자표 0x000–0x12B는 일본판과 한글패치가 같습니다. 숫자, 영문, 가나, 기호입니다. 일본판은 그 뒤에 한자를 두고, 한글패치는 0x12C(가)부터 KS X 1001의 한글 2,350자를 그 순서대로 둡니다. 그래서 「각」은 0x12D, 「힝」은 0xA59입니다. 번호가 폰트 파일의 몇 번째 그림인지도 정하므로, 글자표를 바꾸면 폰트도 같은 순서로 다시 그려야 합니다.
제어 코드와 인수 쉽게
제어 코드는 뒤에 오는 인수 개수가 코드마다 정해져 있습니다. FF2D는 인수 둘(첫 값, 번호), FF08은 하나(인물), FF01은 없음입니다. 이 개수를 알아야 다음 값이 글자인지 인수인지 가릴 수 있습니다. 인수 0004를 글자로 읽으면 「4」라는 글자로 잘못 읽게 됩니다. 인수 개수는 일본판과 v1.1의 같은 대사를 맞대어 정했습니다(노트).
| 값과 인수 | 하는 일 |
|---|---|
FF2D 첫 값 번호 | 대사 머리. 대사는 늘 이것으로 시작합니다. |
FF08 인물 | 말하는 인물 |
FF05 색 | 글자색. 0x0F가 기본색 |
FF01 | 줄바꿈 |
FF02 | 창을 비우고 다음 창으로 |
FFFE | 대사 끝 |
대사 머리의 번호는 게임이 대사를 찾는 데 쓰지 않습니다. 첫 장면에서 대사 두 개의 번호를 바꿔 봐도 화면은 같았습니다(노트). 첫 값은 st01에서 장면이 뒤로 갈수록 커지는데, 무엇에 쓰는지는 확인하지 않았습니다. 게임은 다음 절의 포인터로 대사를 찾습니다.
포인터는 장면 명령 안에 쉽게
슈퍼 패미콤 게임의 포인터 표는 대사 번호마다 주소를 적은 표입니다(8장). 고스트 트릭에는 따로 떨어진 표가 없습니다. 장면 진행 명령 안에 대사의 파일 위치가 4바이트로 들어 있고, 그 바로 앞에는 정해진 2바이트 값(태그)이 옵니다.
같은 파일 0xB0에는 37 00 E0 02 00 00이 있습니다. 태그 0037 뒤의 4바이트 000002E0이 위젯에서 읽은 첫 대사의 위치입니다. v1.1이 일본판에서 고친 포인터 자리를 모두 모아 보면 태그는 15가지였고, v1.2도 이 15가지로 모든 포인터를 고칠 수 있었습니다. 파일 끝 레이블 구역에도 이름과 파일 위치의 쌍이 있습니다. 0x2E0 대사에도 레이블 m02_0120이 붙어 있어서, 대사를 옮기면 여기도 맞춰 고칩니다.
더 깊이포인터 앞의 태그 15가지
lmg.py가 포인터 앞의 태그로 보는 값(16진수)은 37, 51, 4C, 40, 5D, DB, 5C, 45, 3C, 0, 1, 2, FFFD, FFF4, 228입니다. 이 값 뒤의 4바이트가 옮긴 대사의 옛 위치와 같을 때만 고칩니다. 태그 뒤 값이 우연히 대사 위치와 같은 바이트코드를 고치지 않도록, 옮겨 간 대사의 옛 위치만 찾고 대사 묶음 안은 건너뜁니다. 태그 0·1·2는 명령이 아니라 앞 명령의 인수 자리이고, FFFD는 이벤트 블록의 머리입니다. 15가지는 v1.1이 고친 자리에서 모은 것이라 이 게임의 모든 경우라는 증명은 아닙니다. v1.2에서 묶음 밖에서 바뀐 값은 모두 이 태그 뒤 포인터 자리였습니다.
예외는 수첩(system/database_0000)입니다. 수첩은 대사 머리의 번호로 항목을 찾아서, 번호가 틀리면 그 항목이 나오지 않습니다(노트).
길이가 바뀌면: 묶음 안에서 다시 배치 쉽게
대사 블록은 앞뒤로 이어 붙어 대사 묶음을 이룹니다. 일본판에서 대사가 든 146개 파일은 모두 묶음이 하나씩입니다. 묶음 밖의 장면 명령은 서로를 위치로 가리키기도 해서, 한 바이트도 옮기지 않는 것이 안전합니다. 그래서 한글패치는 묶음 크기를 그대로 두고, 묶음 안에서 번역한 대사를 처음부터 이어 쓴 뒤 남는 칸을 0으로 채웁니다. 대사 시작이 옮겨 가면 그 대사를 가리키는 포인터와 레이블을 새 위치로 고칩니다.
st01_demo101의 묶음은 대사 하나, 0x2E0–0x32F의 80바이트입니다. 일본판 대사는 80바이트를 다 쓰고, v1.2 대사는 72바이트라 0x328부터 8바이트가 0으로 남습니다. 이 파일은 대사가 하나라 포인터를 고칠 일이 없습니다. 대사가 여러 개인 묶음에서는 앞 대사 길이가 바뀌면 뒤 대사의 시작이 모두 움직입니다(노트의 포인터 표).
번역이 묶음보다 길면 넣을 수 없습니다. 이때는 문장을 줄이는 수밖에 없습니다. 슈퍼 패미콤에서 뱅크 밖으로 대사를 옮길 수 없던 것(8장)과 비슷한 제약이지만, 묶음 단위라 더 좁습니다. 고스트 트릭 v1.2는 146개 묶음 모두 이 안에 들어갔습니다. 도구는 묶음을 넘으면 빌드를 멈춥니다.
더 깊이창 폭: 13자와 16자
대사가 묶음에 들어가도 화면에서 잘릴 수 있습니다. 고스트 트릭의 글자는 종류와 상관없이 14px씩 나아가고, 한 줄에 들어가는 글자 수는 창 종류가 정합니다. 초상화가 창 안 왼쪽에 있는 창은 13자, 전체 폭 창은 16자이고 넘친 글자는 그려지지 않습니다. 창 종류는 대사가 아니라 장면이 정해서, 검사 도구는 같은 대사의 일본어 줄 길이로 짐작합니다(노트).
실제 패치에서 실제 패치에서
고스트 트릭 v1.1: 한국어 스크립트 위에 덧쓰기
v1.1도 파일 크기와 묶음 밖 위치를 그대로 두고 묶음 안에서 대사를 이어 쓴 뒤 포인터를 고쳤습니다. 다만 작업 중 생긴 실수가 남았습니다. 대사 중간에 FFFE가 들어가 뒷부분이 나오지 않는 대사가 있었고, 두 파일(st07_game004, st15_demo060)은 레이블 구역이 1바이트 밀려 있었습니다(노트).
고스트 트릭 v1.2: 일본판 스크립트에서 다시 만들기
v1.2는 v1.1 스크립트에 덧쓰지 않고 일본판 스크립트를 출발점으로 묶음마다 대사를 다시 이어 씁니다. 대사 머리는 일본판 값을 씁니다. 같은 도구로 일본판에 v1.1 대사를 넣으면 v1.1 실수가 있는 파일을 빼고는 v1.1과 바이트까지 같아서, 이 도구가 포인터와 레이블 말고는 묶음 밖을 건드리지 않는다는 것을 확인했습니다(노트). v1.2에서 바뀐 스크립트는 145개이고(대사가 든 146개 가운데 「……」만 있는 st01_demo102는 그대로), 수첩과 시스템 메시지 파일 2개도 같은 방법으로 다시 만들었습니다.
직접 해 보기 직접 해 보기
파일을 꺼내 LZ11을 풀고 헤더를 봅니다.
krpatch nds unpack game.nds work/
krpatch lz11 decode work/data/st01/st01_demo101_Expand.xml.lz demo101.bin
xxd -l 20 demo101.bin
# 00000000: 314c 4d47 1500 0000 0403 0000 5800 0000 1LMG........X...
# 00000010: 0402 0000 ....kr-patches 저장소의 고스트 트릭 도구로 대사를 읽습니다. nds/ghost-trick 폴더에서 PYTHONPATH=tools python3으로 열고, demo101.bin은 그 폴더에 둡니다. textcodec은 이 장의 위젯과 같은 꼴의 표기를 씁니다. 다만 인수는 소문자 16진수로 적습니다.
>>> import lmg, textcodec
>>> d = open('demo101.bin', 'rb').read()
>>> [(hex(0x34 + 2 * a), hex(0x34 + 2 * b), len(m)) for a, b, m in lmg.runs(d)]
[('0x2e0', '0x330', 1)]
>>> ko = textcodec.Codec(textcodec.ko_table('tables/low.tsv'))
>>> ko.encode('{2D:4,0}각{FE}')
(65325, 4, 0, 301, 65534)실제 패치에서 이 개념이 쓰인 롬 분석 노트
직접 해 보기 kr-patch-toolkit 명령 설명
참고 자료
- kr-patch-toolkit, lz11.py — LZ10·LZ11 머리말과 복사 지시 세 가지 길이의 형식
- GBATEK, BIOS Decompression Functions — LZ77 머리말(형식 바이트와 3바이트 크기)과 플래그 바이트를 위 비트부터 읽는 순서
- kr-patches, 고스트 트릭 tools/lmg.py — 1LMG 구조, 대사 블록과 묶음, 포인터 태그 15가지, 묶음 안 다시 배치와 레이블 고치기
- kr-patches, 고스트 트릭 tools/textcodec.py — 제어 코드마다 붙는 인수 개수, 한글이 0x12C부터 KS X 1001 순서인 글자표, 이 장의 위젯이 쓰는 표기
- kr-patches, 고스트 트릭 docs/ANALYSIS.md — 대사 수, v1.1이 고친 포인터 자리, v1.1 스크립트의 문제
댓글