롬 분석 노트 · 닌텐도 DS
롬 분석 노트: 고스트 트릭
고스트 트릭 한글패치 v1.2을 만들며 남긴 기록입니다. 개념은 각 절의 「가이드」 링크에서 처음부터 풀어 설명합니다.
이 롬을 다시 분석하거나 한글패치를 이어서 고칠 사람을 위해 남기는 기록입니다. 2015년에 나온 v1.1은 대사, 수첩, 폰트, 그래픽을 한국어로 바꿨지만 일본어가 그대로 남은 대사, 창 폭을 넘어 끝이 잘리는 줄, 원문과 뜻이 다른 문장이 곳곳에 있었습니다. v1.2는 v1.1 대사 4,048개를 일본판과 하나하나 맞대어 고치고, 대사 스크립트를 일본판에서 다시 만들었습니다. v1.1도 v1.2도 게임 코드는 고치지 않았습니다. 바꾼 것은 롬 안 파일의 데이터뿐입니다.
1. 이 노트에서 쉽게
고스트 트릭은 죽은 사람의 영혼이 되어 물건에 깃들고 움직이며 4분 전으로 돌아가 죽음을 막는 퍼즐 게임입니다. 대사가 이야기를 끌고 가는 게임이라 한글패치의 대부분은 대사입니다. 한글패치는 두 번 나왔습니다.
- v1.1 (2015): 대사 스크립트와 수첩을 한국어로 바꾸고, 폰트 세 개에 한글 2,350자를 넣고, 로고와 화면 글자를 다시 그렸습니다. 코드는 고치지 않고 롬 안 파일 150개를 바꿨습니다(2절).
- v1.2: v1.1 대사 4,048개를 일본판과 맞대어 검수했습니다(3절). v1.1 롬에 덧쓰지 않고, 일본판 대사 스크립트에 고친 대사를 이어 써서 스크립트 145개와 시스템 파일 2개를 다시 만들었습니다(3절). 폰트와 그래픽은 v1.1 그대로이고, 타이틀 화면에 v1.1이 넣은 안내·제작자 문구만 지웠습니다(5절).
롬 해킹 가이드의 DS 장은 15장 롬과 파일 시스템부터입니다. 나머지 장은 대부분 주소로 일하는 슈퍼 패미콤 기준이지만, 대사를 글자 코드와 포인터로 적는 방법(8장), 바이트 순서(2장 엔디안), 압축(9장 LZSS)은 DS에도 그대로 통합니다. 처음이라면 0장 롬과 패치부터 보세요.
배경지식: 주소가 아니라 파일
슈퍼 패미콤 롬은 CPU가 주소로 바로 읽는 한 덩어리입니다. 그래서 한글패치도 "몇 번지에 무엇이 있나" 를 찾는 일부터 시작합니다. DS 카드 롬은 다릅니다. 카드 안에 PC처럼 폴더와 파일이 있는 파일 시스템이 들어 있고, 게임은 필요한 파일을 메인 메모리(4MB)로 읽어 와서 씁니다. CPU도 둘인데, ARM9가 게임을 돌리고 ARM7이 소리와 터치, 무선을 맡습니다.
그래서 DS 한글패치는 롬 주소보다 파일 이름으로 일합니다. 대사가 든 파일을 꺼내 풀고, 고친 뒤 다시 압축해 롬에 넣고, 파일 위치 표를 고칩니다. 이 게임의 대사 파일은 제1장 첫 컷신이면 st01/st01_demo101_Expand.xml.lz처럼 스테이지(st01)와 장면(demo101) 이름을 달고 있습니다.
128MB 롬과 xdelta 패치
IPS는 바꿀 위치를 3바이트로 적어서 16MB(0xFFFFFF)까지만 고칠 수 있습니다(0장 「IPS 패치를 바이트로 읽기」). 고스트 트릭 롬은 128MB라 IPS로는 뒤쪽을 고칠 수 없습니다. 그래서 v1.1부터 xdelta(VCDIFF 형식)를 씁니다. xdelta는 "원본의 어디에서 몇 바이트를 가져오라" 와 "이 바이트를 새로 넣어라" 를 이어 적은 형식이라, 파일이 통째로 옮겨 가도 패치가 크게 커지지 않습니다.
| 롬 | 바이트 | CRC32 | 설명 |
|---|---|---|---|
| 일본판 | 134,217,728 | 105DDD9E | 128MB 카드를 통째로 뜬 롬. 118,591,968바이트 뒤는 0xFF로 채워져 있습니다. |
| v1.1 | 119,459,746 | D366A4D9 | 일본판에 v1.1 xdelta(3,174,051바이트)를 적용한 롬. 뒤쪽 빈 공간이 잘려 있습니다. |
| v1.2 | 119,462,534 | 4C863193 | v1.1 롬에서 대사 스크립트를 다시 만들고 타이틀 화면의 v1.1 문구를 지운 롬. 일본판에 v1.2 xdelta(3,481,673바이트)를 적용해도 같습니다. |
롬을 줄이는 도구(트리밍)로 뒤쪽 0xFF를 잘라 낸 롬파일도 있습니다. 헤더 0x080의 사용 크기 뒤가 모두 0xFF라서, 잘린 롬은 0xFF를 128MB까지 다시 붙이면 원래 롬과 같아집니다. 한글패치 페이지의 패처는 롬파일을 고르면 이렇게 되돌린 뒤 CRC32로 확인하고, xdelta를 브라우저 안에서 적용합니다. 128MB의 MD5는 계산이 오래 걸려 CRC32만 보고, 적용하는 동안에는 xdelta가 창마다 적어 둔 Adler-32로 결과를 다시 확인합니다.
더 깊이xdelta3이 만든 패치를 브라우저에서 읽는 데 필요한 것
VCDIFF는 표준(RFC 3284) 형식이지만 xdelta3은 두 가지를 덧붙입니다. 헤더 뒤에 원본·결과 파일 이름을 담은 애플리케이션 헤더가 있고, 창마다 결과의 Adler-32가 붙습니다. 패처는 앞의 것을 건너뛰고 뒤의 것으로 창마다 결과를 확인합니다. v1.1 패치의 애플리케이션 헤더에는 원본 「5017 - Ghost Trick (J).nds」, 결과 「고스트 트릭 (K)2.nds」(CP949)가 적혀 있습니다. 2차 압축(DJW, LZMA)을 쓴 패치는 읽지 않으므로, 툴킷의 krpatch xdelta make는 2차 압축을 끄고 만듭니다.
2. 롬 지도 쉽게
롬의 처음 0x200바이트는 카드 헤더입니다. 게임 이름과 코드, 두 CPU의 프로그램 위치, 파일 표 두 개의 위치가 들어 있습니다. 일본판 헤더에서 이 노트에 필요한 칸만 골랐습니다. 위치와 크기는 작은 자리 바이트부터 적힌 4바이트 값입니다.
| 헤더 위치 | 일본판 값 | 뜻 |
|---|---|---|
0x000 | GHOSTTRICK-J | 게임 제목 12바이트 |
0x00C | BGTJ | 게임 코드. 끝의 J가 일본판 |
0x020 | 0x4000 · 0x80628 | ARM9 프로그램의 위치와 크기(0x02C) |
0x030 | 0x96800 · 0x2642C | ARM7 프로그램의 위치와 크기(0x03C) |
0x040 | 0xBCE00 · 0x1654 | 파일 이름 표(FNT)의 위치와 크기 |
0x048 | 0xBE600 · 0x720 | 파일 위치 표(FAT). 8바이트씩 228칸 |
0x050 | 7개 | ARM9 오버레이 표. 필요할 때 메모리에 올리는 코드 조각 |
0x080 | 118,591,968 | 롬에서 실제로 쓰는 크기 |
0x15E | CRC-16 | 헤더 0x000–0x15D의 체크섬 |
파일 위치 표(FAT)는 파일 번호마다 시작과 끝 위치를 8바이트로 적은 표이고, 파일 이름 표(FNT)는 폴더마다 이름과 첫 파일 번호를 적은 표입니다. 이름으로 파일을 찾으려면 FNT에서 이름을 따라가 번호를 얻고, 그 번호로 FAT에서 위치를 읽습니다. 이 롬에는 FAT 칸이 228개 있고, 그중 221개가 이름이 있는 데이터 파일, 7개가 오버레이입니다. v1.1은 데이터 파일 221개 가운데 150개를 바꿨고, ARM9·ARM7 프로그램과 오버레이는 그대로 뒀습니다.
| v1.1이 바꾼 파일 | 수 | 내용 |
|---|---|---|
stNN/*_Expand.xml.lz | 143개 | 대사 스크립트(LZ11). 푼 크기는 일본판과 같습니다. |
system/system_0000.msg.xml.binsystem/database_0000_Expand.xml.bin | 2개 | 시스템 메시지와 수첩. 압축하지 않았고 크기도 일본판과 같습니다. |
fonts2bpp_new.imb | 1개 | 대사 폰트. 16×24, 2bpp(4절). |
font_db.imbballoon_font.imb | 2개 | 16×16 4bpp 폰트. 256자에서 3,203자로. |
cpac_2d.bincpac_3d.bin | 2개 | 그래픽 묶음. 로고와 화면 글자(5절). |
파일 이름으로 찾고 다시 묶기
v1.1은 롬을 다른 도구로 다시 묶었습니다. 그 바람에 같은 파일의 번호와 위치가 일본판과 다릅니다. 대사 폰트는 일본판 16번이 v1.1에서 14번이 됐습니다. 그래서 이 패치의 도구는 파일을 언제나 경로 이름으로 찾습니다.
| 파일 | 일본판 번호 · 위치 · 바이트 | v1.1 | 설명 |
|---|---|---|---|
fonts2bpp_new.imb | 16번 · 0x5007A00 · 129,600 | 14번 · 0x50579CC · 307,488 | 대사 폰트. 글자가 1,350자에서 3,203자로 늘었습니다 |
st01/st01_demo101_Expand.xml.lz | 38번 · 0x5F56200 · 936 | 38번 · 0x6EB917C · 936 | 제1장 첫 컷신의 대사 스크립트 |
sound_data.sdat | 22번 · 0x628C400 · 15,257,056 | 22번 · 0x52B0970 · 15,257,056 | 음악과 효과음. 내용은 같고 자리만 옮겨졌습니다 |
파일이 커지면 원래 자리에 들어가지 않습니다. 툴킷의 kr_patch.nds.rom은 바뀐 파일이 원래 자리(다음 데이터가 시작하기 전까지)에 들어가면 그 자리에 두고 남는 곳을 채우고, 들어가지 않으면 마지막 파일 끝 다음으로 옮깁니다. 헤더 쪽에서 바뀌는 것은 FAT, 사용 크기(파일이 전보다 뒤까지 갈 때만), 카드 용량, 헤더 CRC-16뿐입니다. v1.2에서 v1.1과 다른 파일은 148개(스크립트 145개, 시스템 파일 2개, 그래픽 묶음 1개)이고, 롬 끝으로 옮겨 간 것은 st06/st06_demo100_Expand.xml.lz 하나뿐입니다. 나머지 147개는 바뀐 크기가 원래 자리에 들어갔습니다(스크립트는 LZ11로 다시 압축한 크기). 슈퍼 패미콤에서 롬을 늘릴 때(11장 롬 확장)와 달리 주소 지도를 다시 짤 필요가 없습니다.
더 깊이v1.1 헤더의 사용 크기가 실제보다 작은 것
v1.1 롬 헤더 0x080의 사용 크기는 119,064,012인데, 롬 파일과 마지막 파일의 끝은 119,459,746입니다. 스테이지 14·15의 대사 파일과 system/ 두 파일, 모두 31개가 이 값 뒤에 있습니다. desmume은 이대로도 잘 돌지만, 사용 크기만큼 잘라 내는 도구로 이 롬을 다루면 뒤쪽 파일이 잘립니다. v1.2에서는 st06/st06_demo100_Expand.xml.lz가 롬 끝으로 옮겨 가 파일 끝이 전보다 뒤로 늘어나므로, 사용 크기가 새 끝(119,462,534)으로 바뀌었습니다. 파일이 옮겨 가지 않으면 툴킷은 이 값을 그대로 둡니다.
3. 대사 쉽게
대사는 스테이지마다, 장면마다 파일 하나에 들어 있습니다. 파일 안에는 대사만 있는 게 아니라 인물을 움직이고 카메라를 옮기는 장면 진행 명령(바이트코드)이 함께 있고, 대사는 그 사이사이에 끼어 있습니다. 고칠 때 할 일은 셋입니다. 압축을 풀고, 대사 블록만 골라 바꾸고, 대사가 옮겨 간 만큼 그 대사를 가리키는 포인터를 고칩니다.
LZ11로 압축된 스크립트
.lz 파일은 첫 바이트가 11이고 다음 3바이트가 푼 크기인 LZ11 압축입니다. 제1장 첫 컷신 파일은 11 94 05 00으로 시작하니 푼 크기가 0x594(1,428)바이트이고, 롬에서는 936바이트입니다. LZ11은 9장 「게임마다 다른 LZSS」의 한 갈래로, 앞에서 나온 바이트를 "몇 칸 앞에서 몇 바이트" 로 다시 가리켜 줄입니다. 닌텐도 DS 게임이 흔히 쓰는 형식이라 툴킷의 kr_patch.lz11로 풀고 다시 압축합니다. 다시 압축한 바이트는 원본과 같지 않아도 됩니다. 게임은 푼 결과만 보기 때문입니다.
1LMG 스크립트의 대사와 포인터
푼 파일은 1LMG로 시작하는 0x34바이트 헤더, 2바이트 단위의 코드 구역, 이름 문자열 구역, 레이블 표 순서로 되어 있습니다. 헤더 +0x08이 코드 구역 크기, +0x0C가 이름 문자열 구역 크기입니다. 대사는 코드 구역 안에서 FF2D로 시작해 FFFE로 끝나는 블록이고, 글자 하나가 2바이트 값(u16) 하나입니다. FFxx는 제어 코드입니다.
v1.2의 제1장 첫 컷신에서 첫 대사(푼 파일 0x2E0)를 2바이트씩 읽으면 이렇습니다. 파일에는 작은 자리 바이트가 먼저 적혀 있어서(2장 엔디안) FF2D는 2D FF로 보입니다. 일본판 같은 자리에는 「そうだ…そうだよ。思い出したわ。…あなたのコト。」가 같은 제어 코드 사이에 들어 있습니다.
FF2D 0004 0000대사 머리: 창 4, 번호 0FF08 0004말하는 인물00F8 ×5……………FF02다음 창01C0그03BE래00F8…01C0그03C5랬069D어00E1.FF02다음 창FF0D 0016제어 코드 0D0592생012D각025A났069D어00E1.FF01줄바꿈00F8…02EA당0616신06AE에00FF공백02EE대09D5해00E1.FFFE대사 끝
글자 값은 4절의 글자표 번호입니다. 한글은 0x12C(가)부터 KS X 1001 순서라 「각」은 0x12D입니다. 제어 코드마다 뒤에 오는 인수 개수가 정해져 있어서, 그 개수를 알아야 다음 글자를 바르게 읽습니다. 인수 개수는 일본판과 v1.1의 같은 대사를 맞대어 정했습니다. 이 노트와 대본 파일에서 쓰는 표기는 이렇습니다.
{01} | 줄바꿈 |
{02} | 창을 비우고 다음 창으로 |
{05:x} | 글자색. {05:f}가 기본색 |
{08:x} {1B:x} | 말하는 인물 |
{29}…{2A} | 수첩 항목으로 이어지는 이름 |
{2D:창,번호} | 대사 머리. 대사는 늘 이것으로 시작합니다 |
{FE} | 대사 끝(FFFE) |
게임은 대사를 포인터로 찾습니다. 포인터는 바이트코드 안에서 정해진 태그 값 뒤에 오는 4바이트 파일 위치입니다. 같은 파일 0xB0에는 37 00 E0 02 00 00이 있는데, 태그 0x37 뒤의 0x000002E0이 바로 위 첫 대사입니다. 대사를 가리키는 태그는 15가지이고, v1.1이 일본판에서 고친 자리를 모두 모아 찾았습니다. 레이블 표도 대사를 가리킬 수 있습니다. 포인터와 대사 표의 일반적인 모양은 8장 「포인터 표: 대사 번호로 위치 찾기」에 있습니다.
더 깊이포인터 태그와 「번호로 찾는」 수첩
다시 넣기: 묶음 안에서 이어 쓰기
대사는 앞뒤로 이어 붙은 묶음(run)을 이룹니다. 묶음 사이에는 바이트코드와 FFFD…FFFE 이벤트 블록이 있어서, 묶음 밖은 한 바이트도 옮기면 안 됩니다. v1.1도 파일 크기와 묶음 밖 위치는 그대로 두고 묶음 안에서 대사를 이어 쓴 뒤 포인터를 고쳤습니다. tools/lmg.py는 같은 틀에서 남는 칸을 0으로 채우고, 옮겨 간 대사를 가리키는 포인터와 레이블을 고칩니다.
제1장 두 번째 컷신(st01_demo120)에는 대사 묶음이 하나 있고, 대사가 12개 들어 있습니다. 아래 표는 이 묶음의 대사를 가리키는 포인터 몇 개가 버전마다 어떤 값인지 보여 줍니다. 포인터 자리는 태그(0x37)가 있는 위치입니다. 한국어가 일본어보다 짧으면 뒤 대사가 앞당겨지고, v1.2에서 대사를 고치면 또 옮겨 갑니다. v1.2는 이 묶음 끝에 0으로 채운 2바이트 칸 17개가 남았습니다.
| 포인터 자리 | 일본판 | v1.1 | v1.2 | 설명 |
|---|---|---|---|---|
0x0D0 | 0xB74 | 0xB74 | 0xB74 | 묶음의 첫 대사. 옮겨 가지 않음 |
0x38E | 0xC1C | 0xC08 | 0xC08 | 앞 대사가 줄어 앞당겨짐 |
0x41E | 0xD28 | 0xCFE | 0xCFC | v1.2에서 앞 대사가 또 바뀜 |
0x330 | 0xFE6 | 0xFCC | 0xFC6 | 묶음의 마지막 대사 |
v1.2는 v1.1 롬의 스크립트에 덧쓰지 않습니다. 일본판 스크립트를 출발점으로 묶음마다 v1.2 대사를 이어 쓰고, 대사 머리는 일본판 값을 씁니다. 일본판 스크립트에 v1.1 대사를 같은 방법으로 넣으면 176개 파일 가운데 170개가 v1.1과 바이트 단위로 같습니다. 나머지 6개는 v1.1의 실수가 있는 파일입니다(8절). 이 확인이 맞아야 같은 도구로 v1.2 대사를 넣어도 다른 곳을 망가뜨리지 않았다고 믿을 수 있습니다.
창 폭: 13자와 16자
v1.1에서 가장 많았던 문제는 줄 끝이 잘리는 것이었습니다. 대화창이 몇 글자를 받는지 알아야 고칠 수 있어서, 대사 하나를 시험 문장으로 바꾼 롬을 만들어 실제로 찍어 봤습니다. 왼쪽은 「가나다라마바사아자차카타파하」 14자를 두 줄 넣은 것입니다. 「파」까지 13자만 나오고 「하」는 그려지지 않습니다. 오른쪽은 「가」 글리프를 좁게 고친 폰트에 「가」를 이어 넣은 것인데, 글자 간격은 그대로 14px이고 한 줄에 들어가는 글자 수도 같습니다.
글자는 종류와 상관없이(한글, 가나, 영문, 공백 모두) 14px씩 나아갑니다. 한 줄에 몇 자가 들어가는지는 창 종류가 정하고, 넘친 글자는 그려지지 않습니다. 글자 폭과 줄 폭의 관계는 7장 「폰트: 고정폭과 가변폭」에 있습니다.
| 창 | 나오는 곳 | 폭 |
|---|---|---|
| 초상화가 창 안 왼쪽에 있는 창 | 살아 있는 세계의 대화, 시셀 혼잣말 | x = 58–250, 13자 |
| 초상화가 창 위에 크게 나오는 전체 폭 창 | 유령 세계의 대화 | x = 16부터, 16자 |
| 검은 화면 내레이션 | 장 첫머리 | 17자까지 확인 |
창 종류는 화자 코드가 아니라 장면이 정해서, 대사만 보고는 알 수 없습니다. 검사 도구는 같은 대사의 일본어로 짐작합니다. 일본어 줄이 모두 13자 이하면 좁은 창(13자), 아니면 넓은 창(16자, 일본어가 더 길면 그 길이)으로 봅니다. 일본판 대사가 잘리지 않고 나온다는 사실을 기준으로 삼은 것입니다.
전수 대조와 빌드 전 검사
v1.1 대사 4,048개(본편 3,503개, 수첩과 시스템 메시지 545개)를 일본어와 나란히 뽑아 한 줄씩 대조했습니다. 인물과 지명은 v1.1 표기(시셀, 린네, 죠드, 카바네라…)를 따르고, 같은 원어를 여러 표기로 옮긴 것은 가장 많이 쓴 표기로 맞췄습니다. 고친 대사는 1,434개입니다. 대사 하나에 여러 문제가 있으면 각각 셌습니다.
| 고친 이유 | 대사 수 | 내용 |
|---|---|---|
| 줄 넘침 | 607 | 창 폭(13자·16자)을 넘어 끝이 잘리던 줄을 나눔 |
| 맞춤법 | 366 | 표준어와 맞춤법에 맞게 |
| 오역 | 312 | 원문과 뜻이 다른 문장 |
| 용어 | 212 | 같은 원어를 여러 표기로 옮긴 것을 다수 표기로 |
| 빠진 문장 | 75 | 원문에 있는데 번역에서 빠진 말 |
| 제어 코드 | 72 | 색·화자·수첩 링크 코드가 빠지거나 틀린 것 |
| 띄어쓰기 | 38 | |
| 어색한 문장 | 34 | 뜻은 맞지만 한국어로 어색한 문장 |
| 미번역 | 5 | 일본어가 그대로 남거나 깨진 대사 |
고친 대본은 빌드하기 전에 tools/check.py가 다시 봅니다. 글자표에 없는 글자, 제어 코드 열이 v1.1과도 일본판과도 다른 대사, 창 폭을 넘는 줄, 창 하나에 일본어보다 많은 줄, 남은 일본어(가나·한자)입니다. 일본어와 v1.1은 저장소가 아니라 빌드할 때 롬에서 읽습니다. 하나라도 걸리면 빌드가 멈추고, 배포한 v1.2는 0건입니다.
4. 글자 쉽게
대사에 쓰는 글자 번호 하나를 폰트 세 개가 함께 씁니다. 대화창의 fonts2bpp_new.imb, 말풍선의 balloon_font.imb, 그리고 font_db.imb입니다. v1.1은 세 폰트를 모두 3,203자로 맞추고 번호를 이렇게 나눴습니다.
0x000–0x12B: 일본판과 같은 글자(숫자, 영문, 가나, 기호).tables/low.tsv0x12C–0xA59: 한글 2,350자. KS X 1001 순서라 가 =0x12C, 힝 =0xA590xA5A부터 553자: v1.2 대본에서는 쓰지 않습니다
일본판 글자표는 롬에 없습니다. 일본판 대사 폰트 1,350자를 macOS의 문자 인식(Vision)으로 여러 번 읽어 다수결로 정하고, 대본에서 문맥이 맞지 않는 글자를 손으로 고쳐 tables/jp.tsv를 만들었습니다.
대화창 폰트는 글자 하나가 16×24픽셀, 픽셀 하나가 2비트(2bpp)입니다. 2비트면 네 단계라 바탕과 글자 사이에 중간 농도 두 단계를 쓸 수 있습니다. 한 줄 16픽셀이 4바이트, 글자 하나가 96바이트입니다. 아래는 v1.2 롬의 「가」(0x12C, 폰트 파일 0x7080)입니다.
바이트 하나에 픽셀 네 개가 들어가는데, 순서가 앞에서부터가 아닙니다. 위쪽 4비트(니블)가 앞의 두 픽셀이고, 니블 안에서는 아래 2비트가 먼저입니다. 그래서 픽셀 0, 1, 2, 3의 값은 비트 4, 6, 0, 2에서 시작하는 2비트입니다. 타일과 비트평면을 읽는 기본은 7장 「픽셀 하나에 비트 몇 개」에 있습니다.
더 깊이픽셀 순서를 찾은 방법
바이트 안 2비트 칸 넷을 어떤 순서로 늘어놓는지는 4! = 24가지입니다. 24가지로 폰트 전체를 그려 보고, 이웃한 픽셀끼리 값이 가장 매끄럽게 이어지는 순서를 골랐습니다. 글자는 대부분 굵은 획이라 순서가 맞으면 같은 값이 이어지고, 틀리면 줄무늬가 생깁니다.
pixel(x, y) = (data[글자 × 96 + y × 4 + x ÷ 4] >> SHIFT[x mod 4]) & 3
SHIFT = (4, 6, 0, 2)
「가」 = 0x12C → 0x12C × 96 = 0x7080font_db.imb와 balloon_font.imb는 16×16, 4bpp(아래 니블이 앞 픽셀)이고 글자 하나가 128바이트입니다. 일본판은 256자였는데 v1.1이 대화창 폰트와 같은 3,203자로 늘렸습니다.
5. 그림·압축 실제 패치에서
타이틀 로고와 「돌아가기」 같은 화면 글자는 그래픽 묶음 cpac_2d.bin, cpac_3d.bin에 있습니다. v1.1이 cpac_2d.bin 묶음 3의 항목 16,508개 가운데 2,695개, cpac_3d.bin 묶음 0의 항목 147개와 묶음 3의 26개를 바꿨습니다. v1.2가 바꾼 그래픽은 타이틀 그림 하나입니다(아래 「타이틀 화면의 문구 지우기」). 묶음 구조는 2015년에 v1.1 그래픽 작업을 하며 분석한 것을 옮긴 것이라 [추정]입니다.
cpac_3d.bin: 0x20바이트 헤더 뒤에 묶음 4개가 0x100 단위로 정렬되어 있습니다. 묶음마다YEKB,TADB표식이 있고, 그 뒤에 (상대 위치, 크기) 쌍이 이어집니다. 항목 첫 바이트가11이면 3절의 LZ11 압축입니다.- 이미지 헤더는 4바이트 값 9개이고, 헤더 크기, 이미지 크기, 팔레트 위치, 가로, 세로가 들어 있습니다. 나머지 칸의 뜻은 모릅니다.
cpac_2d.bin도 같은 구조(최상위 묶음 6개)이지만11로 시작하지 않는 항목이 많아, 다른 압축이거나 압축하지 않은 것으로 보입니다.
그림을 다시 그릴 일이 생기면 이 형식부터 롬에서 직접 확인해야 합니다. 다시 묶을 때는 묶음 사이의 0 채움을 원본 그대로 두어야 합니다.
타이틀 화면의 문구 지우기
v1.1은 타이틀 화면 아래에 비영리 배포 안내 두 줄과 제작자 표기를 그려 넣었습니다. v1.2는 이 문구를 지웠습니다. 타이틀 위 화면은 cpac_3d.bin 묶음 0의 461번 항목입니다. LZ11을 풀면 sdt.로 시작하는 그림이고, 헤더 0x0C부터 픽셀 위치(0x24), 픽셀 크기(0xC000), 팔레트 위치(0xC024), 팔레트 크기(0x200), 가로 256, 세로 192가 4바이트씩 적혀 있습니다. 픽셀 하나가 1바이트(8bpp)이고 팔레트는 256색입니다.
v1.1이 문구를 그린 곳은 137–177줄입니다. 그 줄들을 일본판 같은 그림의 픽셀로 되돌리면 배경 무늬가 그대로 이어집니다. 일본판에서 쓰인 색은 v1.1 팔레트에 모두 들어 있어, 같은 색의 번호로 바꾸기만 하면 됩니다. 다시 압축한 그림(23,995바이트)이 원래 칸(26,204바이트)보다 작아서 다른 항목은 옮기지 않고 크기 칸만 고쳤습니다. 롬 데이터를 저장소에 넣지 않도록, 이 작업은 빌드할 때 일본판과 v1.1 롬에서 매번 계산합니다.
6. 메뉴·이름·전투 실제 패치에서
메뉴와 시스템 메시지는 system/system_0000.msg.xml.bin, 인물과 장소를 정리한 수첩은 system/database_0000_Expand.xml.bin에 있습니다. 둘 다 압축하지 않은 1LMG 파일이라 대사 스크립트와 같은 도구로 읽고 넣습니다. v1.2 대본에서 수첩이 514줄, 시스템 메시지가 31줄입니다.
수첩은 대사 스크립트와 달리 번호로 항목을 찾습니다. 그래서 대사 머리 번호가 틀리면 그 항목이 수첩에 나오지 않습니다. 대사 속 이름은 {29}…{2A}로 감싸 수첩 항목과 잇는데, 이 코드가 빠지면 이름이 수첩과 이어지지 않습니다. v1.1에 두 가지가 다 있었습니다(8절).
전투 화면은 없는 게임입니다. 인물과 지명은 v1.1 표기를 그대로 두었습니다. 이름은 대사, 수첩, 메뉴 곳곳에 같은 글자로 들어가서, 바꾸려면 4,048개 대사를 함께 바꿔야 합니다.
7. 세이브·암호 쉽게
이 게임에는 암호가 없습니다. v1.1과 v1.2는 게임 코드를 고치지 않았고 세이브 형식도 바꾸지 않았습니다. 세이브 데이터는 카드 안 세이브 메모리에 따로 있어서 롬 패치와 상관이 없습니다. 에뮬레이터라면 롬 파일 이름과 같은 이름의 세이브 파일을 씁니다.
desmume 코어로 직접 확인했습니다. 「처음부터」가 제4장에서 시작하는 시험 롬(9절의 tools/stage_rom.py)을 v1.1·일본판·v1.2로 하나씩 만들고, v1.1과 일본판에서 남긴 기록을 v1.2에 넣어 「계속해서」로 이어 했습니다. 장 첫머리에 게임이 묻는 기록과, 장면 중간에 수첩 화면의 「기록」으로 남긴 기록 모두 같은 장면, 같은 시각에서 이어졌습니다. 장 첫머리 기록을 이어 하면 나오는 내레이션도 v1.1 롬에서 이어 한 것과 같았습니다.
8. 게임 고유 문제 실제 패치에서
일본판 스크립트에서 다시 만들려면 v1.1의 대사를 일본판 대사와 하나씩 짝지어야 합니다. 그 과정에서 v1.1이 어긋난 곳이 드러났습니다. 일부는 대사를 일본판 구조 위에 다시 쓰는 것만으로 사라졌고, 나머지는 검수에서 고쳤습니다. 화면에서 보이는 차이는 한글패치 페이지의 패치 노트에 v1.1과 v1.2를 나란히 놓았습니다.
| v1.1 문제 | 내용과 v1.2의 처리 |
|---|---|
| 대사 중간의 끝 코드 | 대사 중간에 FFFE가 들어가 뒷부분이 나오지 않거나, FFFE 뒤에 공백이 붙은 대사가 있었습니다. v1.2는 대사를 처음부터 다시 써서 사라졌습니다. |
| 레이블 표 어긋남 | st07_game004, st15_demo060 두 파일은 레이블 표가 1바이트 밀려 있었습니다. 일본판에서 다시 만들어 바로잡혔습니다. |
| 바뀐 대사 머리 | 창 번호나 대사 번호({2D})를 바꾼 대사가 28개 있었습니다. v1.2는 일본판 값을 씁니다. |
| 나오지 않던 수첩 항목 | 번호가 틀려 268번 「운명에 사로잡힌 사람」, 512번 「과거 카논의 집」이 수첩에 나오지 않았습니다. |
| 깨진 강조색 | 글자색 인수를 제어 코드처럼 적어(일본판 FF05 0006 → v1.1 FF05 FF06) 강조색이 깨진 곳, 기본색으로 되돌리는 {05:f}가 빠진 곳이 있었습니다. v1.2는 일본판 코드에 맞춰 손으로 되살렸습니다(3절 검수의 「제어 코드」). |
| 화자와 이름 링크 | 말하는 인물 코드가 빠지거나 뒤바뀐 대사, 수첩 이름 링크 {29}…{2A}가 빠진 대사가 있었습니다. v1.2는 일본판 코드에 맞춰 손으로 되살렸습니다(3절 검수의 「제어 코드」). |
| 원문에 없는 대사 | 스테이지 14에 역자가 넣은 장난 대사가 하나 있었습니다. v1.2에서 지웠습니다. |
v1.1 대사를 일본판 대사와 짝짓기
v1.1은 대부분 일본판 순서대로 대사를 이어 썼지만, 번역하지 않은 대사를 제자리에 두거나, 번호를 바꾸거나, 남는 칸에 옛 대사 사본을 남긴 곳이 있습니다. 그래서 v1.1 대사를 앞에서부터 세기만 해서는 짝이 맞지 않습니다. 대사마다 다음 순서로 찾습니다.
- 일본판에서 그 대사를 가리키던 포인터가 v1.1에서 가리키는 위치
- 앞 대사 바로 뒤의 블록
- 창과 번호가 같은 대사
- 번호가 같은 대사
- 앞 대사 뒤의 첫
FF2D
이렇게 뽑은 본편 대사 3,503개를 다시 넣고 다시 뽑으면 대사와 대사 머리가 모두 같아지는 것으로 확인했습니다.
페이지 배경 음악: 녹음 대신 합성
한글패치 페이지의 배경 음악은 「BGM054_B」입니다. 다른 게임 페이지는 에뮬레이터 소리를 녹음해 반복 구간을 찾았는데(14장), 이 곡은 녹음으로는 이음매가 맞지 않았습니다. DS 소리 칩은 채널 16개를 그때그때 나눠 주는데, 반복할 때마다 음이 다른 채널에 걸리면서 소리가 조금씩 달라졌습니다. 녹음본에서 반복 길이를 상관으로 잡았더니 55.51초가 나왔는데, 실제 반복은 54.87초라 이음매에서 한 박 가까이 같은 음이 다시 들렸습니다.
그래서 롬의 음악 데이터를 직접 읽어 DS 사운드 엔진과 같은 방식으로 합성했습니다. 음악은 sound_data.sdat 한 파일에 시퀀스(SSEQ, 악보), 악기 표(SBNK), 파형 묶음(SWAR)으로 들어 있습니다. 이 곡은 시퀀스 37번 SEQ_GHOST_BGM054_B이고, 모든 트랙이 도입 4,608틱 뒤에 3,072틱을 반복합니다. 템포 70에서 3,072틱은 약 54.87초입니다.
- 이 곡이 쓰는 악기 종류는 PCM(종류 1), 드럼 세트(0x10), 키 분할(0x11) 세 가지이고, 파형은 IMA-ADPCM을 풀어 씁니다. 음의 세기 변화(엔벨로프)와 음량표, 좌우 팬은 DS 사운드 드라이버의 계산을 따릅니다.
- 시퀀서는 ARM7 클록 33,513,982Hz를 64 × 2,728로 나눈 약 191.96Hz로 돕니다. 이 주기를 그대로 쓰되 반복 한 번이 정확히 정수 개 표본이 되도록 틱 간격을 맞추고, 엔벨로프는 음마다 시작 시점부터 계산했습니다. 그래서 반복 두 번째와 세 번째가 표본 하나까지 같습니다.
- 반복 시작은 도입부에서 울리던 음의 꼬리가 사라진 뒤, 첫 반복과 둘째 반복이 같아지는 첫 표본(83.590563초)으로 잡았습니다. 반복 끝은 138.460271초입니다.
곡 후보를 고를 때는 녹음을 썼습니다. 시퀀스 정보 표의 모든 칸을 한 곡으로 바꾸고 효과음 묶음(SSAR)의 음량을 0으로 만든 롬을 곡마다 만들어 「처음부터」에서 녹음했습니다. 후보를 들어 보고 이 곡을 골랐습니다.
9. 확인 방법 직접 해 보기
실기 없이 모든 확인을 에뮬레이터로 했습니다. libretro desmume 코어를 직접 빌드하고, 툴킷의 libretro 실행 기능으로 화면 없이 돌리며 버튼과 터치를 넣고 원하는 순간의 화면을 저장합니다. 손으로 하지 않으니 같은 장면을 v1.1과 v1.2에서 똑같은 입력으로 찍을 수 있습니다. 패치 노트의 비교 화면은 모두 이렇게 찍었습니다. 화면 없이 돌리는 방법은 12장 「화면 없이 돌리기」에 있습니다.
wait 600 ; 600프레임 기다림
press start ; Start를 누름
a 20 ; A를 30프레임 간격으로 20번
touch 128 96 ; 아래 화면 가운데를 터치
shot title ; 위·아래 화면을 title.png로 저장터치는 코어의 포인터 장치로 보냅니다. 코어 변수 desmume_pointer_mouse=enabled, desmume_pointer_type=touch를 켜야 하고, 포인터 좌표는 위아래 화면을 합친 256×384에 걸쳐 있어서 아래 화면은 y 192부터입니다. tools/play.py가 아래 화면 좌표를 이 좌표로 바꿔 보냅니다.
뒤쪽 장면은 처음부터 플레이해서 가기에 너무 멉니다. tools/stage_rom.py는 시작 경로 파일(route.xml)의 st01/st01_root.xml을 다른 스테이지로 바꾸고, 그 스테이지가 처음 부르는 장면 파일 이름을 원하는 장면으로 바꿔서 「처음부터」가 그 장면에서 시작하는 시험 롬을 만듭니다. 앞 장면을 건너뛰므로 아이템과 플래그는 실제 진행과 다를 수 있지만, 대사를 확인하기에는 충분합니다.
10. 다음 사람에게 실제 패치에서
롬 구조와 형식은 패치 소스의 docs/ANALYSIS.md에 모았습니다(한국어는 docs/ANALYSIS.ko.md). v1.1 패치 형식, 글자표, 1LMG 구조와 제어 코드, v1.1 문제, 창 폭 실측, 그래픽, 에뮬레이터, 소리를 직접 확인한 것은 [확인], 짐작만 한 것은 [추정]으로 나눠 적었습니다. 여기에는 확인하지 못한 것과 알려진 한계, 쓴 도구만 적습니다. 소리 장은 v1.2 태그 뒤에 더해서 저장소 main 의 문서에 있습니다.
확인하지 못한 것
- 실기(DS 본체와 플래시 카트리지)에서 돌려 본 기록은 없습니다. 화면 확인은 모두 desmume 코어에서 했습니다.
- melonDS DS v1.4.0 코어는 롬을 읽은 뒤 멈춰서 결과를 비교하지 못했습니다.
- 화면으로 본 것은 패치 노트의 비교 화면과 창 폭 실험, 대표 장면입니다. 4,048개 대사의 줄 폭은 화면이 아니라 tools/check.py의 계산으로 확인했습니다.
- 창 종류는 장면이 정하는데 check.py는 같은 대사의 일본어 줄 길이로 짐작합니다(3절 「창 폭」). 일본어가 13자 이하인 대사는 모두 좁은 창으로 보고 검사하므로 더 엄격한 쪽이지만, 실제 창과 다를 수 있습니다.
- cpac_2d.bin, cpac_3d.bin의 묶음 구조는 2015년 분석을 옮긴 것이라 [추정]입니다. v1.2가 직접 확인하고 고친 것은 타이틀 그림(sdt.) 하나입니다.
알려진 한계
- 대사 폰트의 한글은 KS X 1001 2,350자뿐입니다. 이 밖의 글자(예: 똠, 햏)는 쓸 수 없고, 0xA5A부터 553칸(1칸을 빼고 글자가 들어 있음)은 v1.2 대본에서 쓰지 않습니다.
- 글자는 종류와 상관없이 14px씩 나아갑니다. 글리프를 좁게 그려도 한 줄에 더 들어가지 않습니다. 창 폭을 늘리려면 코드를 고쳐야 할 것으로 보입니다(확인하지 않음).
- 대사는 일본판의 묶음 안에서만 다시 배치합니다. 한 묶음의 한국어가 일본어 묶음보다 길어지면 빌드가 멈춥니다(3절 「다시 넣기」). 지금은 모든 묶음에 여유가 있습니다.
- xdelta 패치 바이트는 xdelta3 버전에 따라 달라질 수 있습니다. 같은 롬이 나왔는지는 결과 롬의 CRC32로 비교합니다.
쓴 도구
| desmume libretro 코어 | 커밋 95b4d79 (2026-09-20) 직접 빌드 |
|---|---|
| xdelta3 | 3.2.2 |
| Python | 3.15.0 (Pillow 13.0.0) |
| kr-patch-toolkit | v0.4.0 |
일본판 글자표를 만들 때 쓴 문자 인식은 macOS의 Vision이라 macOS에서만 돌아갑니다.
11. 패치 소스 파일 직접 해 보기
빌드는 zsh tools/build.sh 한 번으로 이 순서대로 돕니다. 일본판 롬을 ghost-trick-jp.nds로 두면 배포한 v1.2와 같은 롬이 나옵니다.
- 일본판 롬에 v1.1 xdelta를 적용합니다(krpatch xdelta apply). 폰트와 그래픽은 이 롬의 것을 그대로 씁니다.
- tools/check.py가 v1.2 대본을 일본판·v1.1과 맞대어 검사합니다. 문제가 하나라도 있으면 빌드를 멈춥니다.
- tools/build.py가 대사 스크립트마다 일본판 스크립트를 풀어 한국어 대사를 이어 쓰고, 포인터와 레이블을 고쳐 다시 압축합니다. 타이틀 그림에서 v1.1 문구를 지운 뒤(tools/title.py), 롬은 kr_patch.nds.rom으로 다시 묶습니다.
- 일본판과 결과 롬으로 xdelta를 만듭니다(krpatch xdelta make). 결과 롬은 119,462,534바이트, CRC32 4C863193입니다.
하는 일마다 파일과 이 노트의 절을 이었습니다.
| 하는 일 | 파일 | 설명 |
|---|---|---|
| 롬 헤더와 파일 표 읽기 | krpatch nds infokrpatch nds ls | 2. 롬 지도 · 다시 묶기 |
| xdelta 만들기·적용 | krpatch xdelta makekrpatch xdelta apply | 1. 이 노트에서 · xdelta |
| 대사 읽기·다시 넣기 | tools/lmg.pytools/textcodec.py | 3. 대사 |
| 일본어와 v1.1을 나란히 뽑기 | tools/dump.py | 3. 대사 · 전수 대조 |
| 롬 다시 만들기 | tools/build.pytools/build.sh | 11. 패치 소스 파일 |
| 빌드 전 검사 | tools/check.py | 3. 대사 · 전수 대조 |
| 글자표 | tables/jp.tsvtables/low.tsv | 4. 글자 |
| 대사 폰트 꺼내기 | tools/font_store.py | 4. 글자 |
| 타이틀 문구 지우기 | tools/title.py | 5. 그림·압축 · 타이틀 |
| v1.2 대본 | tables/v1.2/script.tsv | 3. 대사 · 전수 대조 |
| 창 폭 실험 | tools/probe.py | 3. 대사 · 창 폭 |
| 화면 찍기 | tools/play.pytools/stage_rom.pytools/gallery.py | 9. 확인 방법 |
| 배경 음악 | tools/bgm_dump.pytools/sseq_render.py | 8. 고유 문제 · 배경 음악 |
| 롬 분석 문서(주소·형식 전체) | docs/ANALYSIS.ko.md | 10. 다음 사람에게 |
이 노트의 개념을 설명하는 장
직접 해 보기 소스와 도구
- 패치 소스 — 이 노트가 가리키는 파일과 빌드 순서(한국어 안내)
- 롬 분석 문서 v1.2 — 주소와 형식을 [실측]·[추정]으로 나눠 적은 한국어 문서
- kr-patch-toolkit v0.4.0 — 공통 명령 krpatch 한국어 설명
- 고스트 트릭 한글패치 페이지 — 패치 내려받기와 롬에 바로 적용
댓글