본문으로 건너뛰기

닌텐도 DS모르는 용어는 용어집에서

16장 대사는 어떻게 저장되나

고스트 트릭의 대사는 장면 파일 안, 장면 진행 명령 사이에 2바이트 값으로 들어 있습니다. LZ11을 풀고, 글자와 제어 코드를 읽고, 명령 안의 포인터를 따라가 봅니다.

대사는 장면마다 파일 하나 쉽게

슈퍼 패미콤 게임은 대사를 롬 한쪽에 모아 두고 포인터 표로 찾는 경우가 많습니다(8장). 고스트 트릭은 대사를 장면마다 파일 하나에 나눠 둡니다. 제1장의 컷신 하나는 st01/st01_demo101_Expand.xml.lz, 스테이지 폴더(st01) 안에 장면 이름(demo101)을 단 파일입니다. 파일은 롬의 파일 시스템에서 이름으로 찾습니다(15장).

일본판 롬에는 이런 스크립트 파일이 193개 있습니다. 이름은 .xml로 끝나지만 안은 XML 글이 아닙니다. 압축을 풀면 1LMG로 시작하는 바이너리이고, 그 안에 인물을 움직이고 화면을 바꾸는 장면 진행 명령과 대사가 섞여 있습니다. 대사가 든 파일은 146개, 대사는 모두 3,503개입니다. 수첩과 시스템 메시지는 압축하지 않은 1LMG 파일 2개에 따로 있습니다.

대사 하나를 고치려면 이 순서를 밟습니다.

  1. 롬에서 파일을 꺼내 압축을 풉니다.
  2. 코드 구역에서 대사 블록을 찾아 2바이트 값을 글자로 읽습니다.
  3. 번역을 다시 2바이트 값으로 바꿔 넣고, 대사가 옮겨 간 만큼 포인터를 고칩니다.
  4. 다시 압축해 롬에 넣습니다. 다시 압축한 파일이 원래보다 커지면 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에 이 셋을 더하면 파일 크기와 같습니다.

1LMG 파일의 구역0x000-0x033 헤더(1LMG와 구역 크기), 0x034-0x337 코드 구역(2바이트 값의 줄. 바이트코드와 대사), 0x338-0x38F 이름 문자열 구역(game, SYSTEM, linne …), 0x390-0x593 레이블 구역(이름과 파일 위치의 쌍 28개와 레이블 이름 문자열).0x000-0x033헤더1LMG와 구역 크기0x034-0x337코드 구역2바이트 값의 줄. 바이트코드와 대사0x338-0x38F이름 문자열 구역game, SYSTEM, linne …0x390-0x593레이블 구역이름과 파일 위치의 쌍 28개와 레이블 이름 문자열
st01_demo101을 푼 1,428바이트의 구역. 위치는 푼 파일 안 오프셋입니다.

코드 구역은 처음부터 끝까지 2바이트 값의 줄입니다. 대부분은 게임이 한 값씩 읽어 실행하는 장면 진행 명령(바이트코드)이고, 대사는 그 사이에 FF2D로 시작해 FFFE로 끝나는 블록으로 끼어 있습니다.

글자 하나 = 2바이트 쉽게

대사 블록 안의 값은 0xFF00보다 작으면 글자표 번호, FFxx면 제어 코드입니다. 파일에는 작은 자리 바이트가 먼저 적혀서(2장 엔디안) FF2D는 2D FF로 보입니다.

글자표 0x000–0x12B는 일본판과 한글패치가 같습니다. 숫자, 영문, 가나, 기호입니다. 일본판은 그 뒤에 한자를 두고, 한글패치는 0x12C(가)부터 KS X 1001의 한글 2,350자를 그 순서대로 둡니다. 그래서 「각」은 0x12D, 「힝」은 0xA59입니다. 번호가 폰트 파일의 몇 번째 그림인지도 정하므로, 글자표를 바꾸면 폰트도 같은 순서로 다시 그려야 합니다.

대사 바이트 읽기

고스트 트릭 제1장 컷신 st01_demo101의 첫 대사를 2바이트 값으로 읽습니다. 「다음 값」을 누를 때마다 값 하나(제어 코드는 인수까지)를 읽어 아래 「화면에 나오는 글자」 칸에 더합니다. 일본판과 한글패치를 바꿔 볼 수 있습니다.

롬
  1. 2D FF 04 00 00 00FF2D 0004 0000대사 머리: 첫 값 4, 번호 0
  2. 08 FF 04 00FF08 0004말하는 인물 4
  3. F8 0000F8…
  4. F8 0000F8…
  5. F8 0000F8…
  6. F8 0000F8…
  7. F8 0000F8…
  8. 02 FFFF02창을 비우고 다음 창으로
  9. C0 0101C0그
  10. BE 0303BE래
  11. F8 0000F8…
  12. C0 0101C0그
  13. C5 0303C5랬
  14. 9D 06069D어
  15. E1 0000E1.
  16. 02 FFFF02창을 비우고 다음 창으로
  17. 0D FF 16 00FF0D 0016제어 코드 0D, 인수 1개
  18. 92 050592생
  19. 2D 01012D각
  20. 5A 02025A났
  21. 9D 06069D어
  22. E1 0000E1.
  23. 01 FFFF01줄바꿈
  24. F8 0000F8…
  25. EA 0202EA당
  26. 16 060616신
  27. AE 0606AE에
  28. FF 0000FF
  29. EE 0202EE대
  30. D5 0909D5해
  31. E1 0000E1.
  32. FE FFFFFE대사 끝

화면에 나오는 글자

…………… ▶ 그래…그랬어. ▶ 생각났어. …당신에 대해.

값 개, 바이트

제어 코드와 인수 쉽게

제어 코드는 뒤에 오는 인수 개수가 코드마다 정해져 있습니다. 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이 붙어 있어서, 대사를 옮기면 여기도 맞춰 고칩니다.

명령 안의 포인터파일 0xB0의 태그 0037 뒤 4바이트 E0 02 00 00은 0x2E0을 가리키고, 0x2E0에는 FF2D로 시작하는 대사 블록이 있다.0x0B0 장면 명령37 00E0 02 00 000x2E00x2E0 대사 블록2D FF 04 00 …
장면 명령 안의 포인터가 대사 블록을 가리킵니다.
더 깊이포인터 앞의 태그 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)

이 장에서

  1. 대사는 장면마다 파일 하나
  2. 먼저 압축 풀기: LZ11
  3. 1LMG 파일의 네 구역
  4. 글자 하나 = 2바이트
  5. 제어 코드와 인수
  6. 포인터는 장면 명령 안에
  7. 길이가 바뀌면: 묶음 안에서 다시 배치
  8. 실제 패치에서
  9. 고스트 트릭 v1.1: 한국어 스크립트 위에 덧쓰기
  10. 고스트 트릭 v1.2: 일본판 스크립트에서 다시 만들기
  11. 직접 해 보기