본문으로 건너뛰기

롬 분석 노트 · 슈퍼 패미콤

롬 분석 노트: 드래곤볼 Z 초오공전 돌격편

드래곤볼 Z 초오공전 돌격편 한글패치 v1.2을 만들며 남긴 기록입니다. 개념은 각 절의 「가이드」 링크에서 처음부터 풀어 설명합니다.

이 롬을 다시 분석하거나 패치를 이어 고칠 사람을 위해, 확인한 주소와 형식을 적어 둔 기록입니다. v1.0은 한자와 가나가 있던 폰트 자리 1,010칸에 한글을 다시 그려 썼고, 그 칸은 거의 다 차 있었습니다. 대사를 원문에 맞춰 다시 옮기니 꽃·쫓·흙처럼 그 칸에 없는 글자가 필요해졌습니다. v1.1은 게임이 글자 그림을 찾는 코드에 새 길을 하나 끼워 넣어 512자를 더 넣을 수 있게 했습니다. 아래는 v1.1에서 새로 그린 213자를 롬에서 꺼낸 것입니다.

v1.1 롬에서 꺼낸 새 글자 213자

1. 이 노트에서 쉽게

한글패치는 일본판 롬의 바이트 일부를 바꾼 것입니다. 이 노트는 그 바뀐 곳을 롬 주소와 실제 바이트로 하나씩 따라갑니다. 돌격편 한글패치는 세 번에 걸쳐 쌓였고, 빌드도 이 순서대로 일본판에 v1.0 IPS를 먼저 적용한 다음 v1.1·v1.2의 도구를 차례로 돌립니다.

돌격편 한글패치 버전맨 아래 일본판 롬 위에 2012년 v1.0, 2026년 10월 18일 v1.1, 10월 24일 v1.2가 차례로 쌓인다. v1.0은 폰트 칸에 한글을 그리고 번역했고, v1.1은 대사 4,483줄과 새 글자 213자, 타이틀과 메뉴, 이벤트 표를 고쳤고, v1.2는 암호 글자 다섯 자와 엔딩 고정 암호, 메뉴 표시를 고쳤다.v1.22026. 10. 24암호 글자 다섯 자 다시 그림엔딩 고정 암호, 메뉴 표시 두 글자v1.12026. 10. 18대사 4,483줄 수정, 새 글자 213자타이틀·메뉴, 이벤트 표 수정v1.02012폰트 1,010칸에 한글을 덧그림대사·메뉴 번역 (IPS 하나)일본판19952MB LoROM, CRC32 D531289B대사 5,370줄, 한자·가나 폰트
일본판 위에 쌓인 세 버전. 아래가 먼저입니다.

각 버전이 화면에서 무엇을 바꿨는지는 패치 페이지의 v1.2·v1.1 패치 노트에 비교 화면으로 있습니다. 이 노트는 그 밑의 롬 이야기입니다. 처음 읽는다면 아래 장을 먼저 보면 따라가기 쉽습니다.

주소는 $B7:97BC처럼 쓰면 CPU가 보는 뱅크:주소이고, 0x1B97BC처럼 쓰면 헤더 없는 롬 파일의 위치입니다. 패치 소스의 롬 분석 문서는 롬에서 다시 확인한 것을 [실측], 짐작한 것을 [추정]으로 나눠 적었고, 이 노트도 확인한 것만 옮겼습니다.

2. 롬 지도 쉽게

돌격편 롬은 2MB이고, 32KB짜리 뱅크 64개($80–$BF)로 나뉩니다. 아래는 v1.0 롬과 v1.1 롬을 바이트 단위로 비교해 바뀐 뱅크를 칠한 지도입니다. 대사와 텍스트는 원래 있던 뱅크 안에서 다시 채웠고, 롬의 빈 곳에 새로 쓴 것은 글자 블록 3곳과 $B7:FA00의 새 코드뿐입니다. 롬 크기는 늘리지 않았습니다.

롬 헤더: 파일 0x7FC0의 64바이트

롬 파일의 0x7FC0부터 64바이트에는 게임 이름과 롬 크기 같은 정보가 정해진 자리에 있습니다. 16진수 두 자리가 1바이트이고(1장 16진수), 2바이트 값은 작은 자리부터 적힙니다(2장 롬에서 주소가 거꾸로 보이는 이유).

파일바이트항목뜻
0x7FC053 46 58 … 20이름"SFX DBZ TOTSUGEKIHEN " (21바이트)
0x7FD530맵 모드LoROM, FastROM
0x7FD602카트리지롬 + RAM + 배터리
0x7FD70B롬 크기2^11 KB = 2MB
0x7FD803SRAM 크기2^3 KB = 8KB
0x7FD900지역일본
0x7FDC0F EF F0 10체크섬 보수·체크섬일본판 $EF0F / $10F0

맵 모드 $30은 2진수로 00110000입니다. 오른쪽 네 칸 0000은 LoROM, 비트 4는 FastROM을 뜻합니다(1장 헤더 바이트 하나에 정보 두 개). 아래 칸을 눌러 비트를 켜고 끄면 값이 바뀝니다.

비트 켜고 끄기

전구를 눌러 켜고 끄거나, 아래 칸에 16진수 두 자리를 적어 보세요.

전구 여덟 개가 바이트 하나입니다. 지금은 비트 5와 4가 켜져 있습니다(스크립트가 꺼져 있어 누를 수 없습니다).

2진수
0011 0000
16진수
$30
10진수
48
부호 있는 수로 읽으면
48

체크섬은 롬 전체 바이트를 더한 값의 아래 16비트이고, 보수는 그 값을 뒤집은 것입니다. 일본판은 $10F0, v1.1은 $2B67(보수 $D498)입니다. 한 바이트라도 고치면 값이 바뀌므로 빌드 마지막에 다시 계산해 넣습니다.

주소 읽는 법

CPU는 롬을 뱅크:주소로 봅니다. 이 게임은 LoROM이라 뱅크마다 뒤쪽 32KB($8000–$FFFF)에만 롬이 보입니다. 그래서 파일 위치는 다음처럼 계산합니다(4장 CPU 주소와 파일 오프셋은 다르다).

파일 오프셋 = (뱅크 & 0x7F) × 0x8000 + (주소 & 0x7FFF)
$A8:800C → 0x28 × 0x8000 + 0x000C = 0x14000C

아래 위젯은 3절에 나오는 대사 포인터 주소 $A8:800C에서 시작합니다. 다른 주소를 넣어 볼 수도 있습니다.

CPU 주소 → 파일 오프셋 계산기

CPU 주소 $뱅크:주소를 롬 파일 오프셋으로 바꿉니다. 계산은 kr-patch-toolkit의 krpatch sfc dis와 같은 공식입니다. 매핑을 고르고 주소나 오프셋 중 하나를 고쳐 보세요.

매핑

LoROM 롬에서 $A8:800C는 롬이고, 공식으로 계산한 파일 오프셋은 0x14000C입니다.

  1. 뱅크 $A8에서 맨 위 비트를 뗍니다: $A8 & $7F = $28
  2. 뱅크 하나는 롬 32KB($8000바이트)입니다: $28 × $8000 = $140000
  3. 주소 $800C에서 위쪽 반 표시($8000)를 뗍니다: $800C & $7FFF = $000C
  4. 둘을 더하면 파일 오프셋 0x14000C

뱅크 256개 한눈에 보기 (한 칸 = 뱅크 하나, 왼쪽 반 $0000-$7FFF · 오른쪽 반 $8000-$FFFF)

  • 뱅크 전체가 롬
  • 오른쪽 반(위쪽 32KB)만 롬
  • WRAM($7E·$7F)
  • SA-1 BW-RAM
  • 롬이 아님
  • 지금 고른 뱅크

게임기 안의 메모리

이름크기하는 일
롬2MB프로그램·대사·폰트·그림. 뱅크 $80–$BF의 $8000–$FFFF에 32KB씩 보입니다.
WRAM128KBCPU가 계산하며 값을 적어 두는 곳. 지금 띄울 대사 번호($4D) 같은 것이 여기 있습니다.
VRAM64KB화면에 붙일 8×8 타일과 타일맵을 올려 두는 곳.
SRAM8KB전원을 꺼도 남는 세이브. 헤더 0x7FD8의 값이 크기입니다.
더 깊이이 노트에 나오는 WRAM 변수
주소뜻
$4D지금 띄울 대사 ID. 대사 뱅크 분기 $81:AC4C가 읽습니다.
$0208지금 줄에서 이번 글자가 시작하는 x 위치(px)
$020A창 폭(px) = $020E × 8
$020E창 폭(8px 칸 수)
$0210창 높이(8px 줄 수). 대사 창은 6 = 글자 3줄
$32지금 줄의 위치(8px 줄 수, 한 줄마다 2씩 늚)
$7E:0263디버그 메뉴의 시나리오 번호
$7E:0A00엔딩 암호 문자열이 만들어지는 곳
$7E:0B01주인공 이름(16비트 글자 코드 5개)

$4D는 그래픽을 읽는 루틴($81:A703, $81:A8B0)도 잠깐 씁니다. 대사를 기록할 때 이 값을 그대로 믿으면 엉뚱한 번호가 섞입니다(9절).

새로 쓴 빈 공간

늘어난 코드와 글자는 롬 안의 빈 곳에 넣었습니다. 다만 0xFF가 길게 이어진 곳이라고 다 빈 공간은 아닙니다. 바로 앞의 자료가 0xFFFF를 끝 표시로 쓰고 있을 수 있습니다. 같은 엔진의 각성편 v1.1이 이것을 놓쳐 우주선이 맵 밖을 헤매는 버그를 냈습니다. 돌격편은 일본판 원본에서도 0xFF였는지, 앞의 자료가 그 값을 읽지 않는지 확인한 곳만 썼습니다(6장 새 코드를 둘 빈 공간).

자리넣은 것버전
$B7:FA00–$B7:FA77글자 블록을 찾는 새 코드와 표v1.1
$81:EF00–$81:FEFF글자 블록 0v1.1
$84:EE00–$84:FDFF글자 블록 1v1.1
$86:E000–$86:EBFF글자 블록 2 (3KB만 씀)v1.1
$86:EC00–$86:ED7F 일부「펴」「플」「딩」「렉」v1.2
$B7:FB00–$B7:FB6B기술습득표·이벤트 표의 표시 루틴v1.2

롬을 늘리지 않고 이렇게 빈 곳을 찾아 쓰는 방법은 11장 늘리지 않고 자리를 만드는 법에서, 반대로 1MB 롬을 2MB로 늘린 사례는 제절초 노트에서 볼 수 있습니다.

3. 대사 쉽게

롬 안의 대사는 글자가 아니라 숫자입니다. 돌격편은 글자 하나가 2바이트 코드이고, 작은 쪽 바이트를 먼저 적습니다. 0A 00은 줄바꿈, 00 00은 대사 끝입니다. 대사 $006 「어떻게/잡아 볼까‥」를 v1.1 롬에서 그대로 꺼내면 이렇습니다.

  • F3 08어
  • B5 06떻
  • 27 05게
  • 0A 00줄바꿈
  • A1 09잡
  • C6 08아
  • C5 02빈칸
  • E5 07볼
  • 85 05까
  • 6B 01‥
  • 00 00끝

어느 숫자가 어느 글자인지는 게임마다 다르고, 한글패치는 v1.0이 만든 글자 표(tables/kr-v1.0.txt)를 이어 씁니다. 글자 표와 제어 코드는 8장 글자표와 제어 코드에서 다룹니다.

대사 번호에서 문자열까지

대사는 5,370줄이고 게임은 대사 ID로 찾습니다. 대사를 띄울 때 분기 루틴($81:AC4C)이 ID를 큰 범위부터 비교해 뱅크를 고르고, 그 뱅크의 포인터 표에서 문자열 주소를 읽습니다. 위 대사는 이렇게 찾습니다.

  1. 대사 ID $006은 $000–$25B 범위라 뱅크 $A8입니다.
  2. 그 뱅크의 포인터 표에서 ID 순서대로 2바이트를 읽습니다. $A8:8000 + $006 × 2 = $A8:800C(파일 0x14000C)에 FE 85가 있습니다.
  3. 같은 뱅크의 $A8:85FE(파일 0x1405FE)에 위 문자열이 있습니다.
  4. 창 정보는 $8E:C9BA + $006 × 2 = $8E:C9C6에 있는 19 06입니다. 첫 바이트가 FF가 아니라 얼굴 창입니다.

이 과정을 그림으로 따라가는 예가 8장 포인터 표에 있고, 주소를 파일 위치로 바꾸는 계산은 위 2절의 위젯으로 해 볼 수 있습니다.

대사는 뱅크 밖으로 못 나간다

포인터는 2바이트라 뱅크 번호가 없고, 표가 있는 뱅크 안의 주소만 가리킵니다. 그래서 대사가 길어져도 다른 뱅크로 넘길 수 없습니다. 대사가 들어 있는 뱅크는 9곳입니다. v1.1은 뱅크마다 일본어 문자열이 있던 영역 전체를 처음부터 다시 채웠고, 똑같은 문자열은 하나만 두고 포인터 여러 개가 같이 가리키게 해서 공간을 아꼈습니다.

대사 뱅크별 사용량뱅크 $A8, ID 000–25B, 27,164 / 31,520바이트. 뱅크 $A9, ID 25C–570, 25,284 / 29,144바이트. 뱅크 $AB, ID 571–81B, 27,742 / 31,328바이트. 뱅크 $9D, ID 81C–A64, 28,376 / 31,484바이트. 뱅크 $9E, ID A65–CC6, 27,582 / 31,512바이트. 뱅크 $B2, ID CC7–F57, 27,452 / 31,380바이트. 뱅크 $B4, ID F58–121E, 27,184 / 31,312바이트. 뱅크 $B5, ID 121F–14AC, 28,058 / 31,430바이트. 뱅크 $8E, ID 14AD–14F9, 4,284 / 4,774바이트뱅크 · 대사 IDv1.1 사용량$A8000–25B86%$A925C–57087%$AB571–81B89%$9D81C–A6490%$9EA65–CC688%$B2CC7–F5787%$B4F58–121E87%$B5121F–14AC89%$8E14AD–14F990%
대사 뱅크 9곳. 막대는 일본판 문자열이 차지하던 영역 가운데 v1.1이 쓴 양입니다.
대사 ID뱅크포인터 표v1.1 사용량(바이트)
000–25B$A8$A8:800027,164 / 31,520
25C–570$A9$A9:87D625,284 / 29,144
571–81B$AB$AB:800027,742 / 31,328
81C–A64$9D$9D:800028,376 / 31,484
A65–CC6$9E$9E:800027,582 / 31,512
CC7–F57$B2$B2:800027,452 / 31,380
F58–121E$B4$B4:800027,184 / 31,312
121F–14AC$B5$B5:800028,058 / 31,430
14AD–14F9$8E$8E:B67A4,284 / 4,774

같은 문자열을 여러 ID가 함께 가리키므로, 한 문자열을 제자리에서 고치면 다른 대사까지 같이 바뀔 수 있습니다. 같은 엔진의 각성편은 다시 채우지 않고 안 들어가는 줄만 같은 뱅크 끝으로 옮겼습니다. 두 방법의 차이는 8장 번역이 길어지면 어디에 두나에 정리했습니다.

창 폭과 페이지

대사마다 창 정보 2바이트가 $8E:C9BA + ID × 2에 있고, 첫 바이트가 창 모양을 정합니다. 창 높이는 모두 8px 줄 6개, 곧 글자 3줄입니다.

첫 바이트창폭일본판 대사 수
FF나레이션 창22칸 = 176px38줄
FE얼굴 그림 없는 창16칸 = 128px347줄
FD창을 열지 않음-0줄
그 밖얼굴 창16칸 = 128px4,985줄

이 게임은 글자마다 폭이 다른 가변폭 폰트를 씁니다(7장 폰트: 고정폭과 가변폭). 줄이 창 폭을 넘으면 엔진이 알아서 줄을 바꾸는데, 그러면 한 줄이 밀려 페이지가 엉뚱한 곳에서 넘어갑니다. v1.0 롬의 대사를 창마다 실제 폭으로 재 보니 이렇게 넘치는 줄이 13줄 있었습니다.

v1.0 첫 페이지

v1.0 다음 페이지

v1.1

느낌표만 다음 페이지로 넘어갔던 블루 장군 부하의 보고. v1.1은 한 페이지에 모두 들어갑니다.

글자 폭은 표가 아니라 글자를 그리는 코드 안의 비교문($B7:97DC부터)이 정합니다. 위 대사는 셋째 줄이 122px까지 차 있어서 9px인 「!」를 더하면 128px을 넘었고, 「!」만 다음 페이지에 혼자 나왔습니다.

글자 코드폭무엇
0x500 이상13px한자 자리 = 한글 16×16 글자
0x100–0x4FF (아래 예외 말고)9px가나 자리. 빈칸 0x2C5, 「!」 0x16C
0x8000을 켠 작은 글자8px / 12px0x500 미만 8px, 이상 12px
0x2C7, 0x2A2 / 0x2C65px / 4px좁은 기호
0xBE4 / 0xBE5 / 0xBE6 / 0xBE72 / 4 / 6 / 7px자리 맞춤용 빈칸

v1.1은 이 규칙대로 글자마다 폭을 더해 모든 줄을 검사했고(tools/layout1.py), 넘치는 줄이 0이 될 때까지 고쳤습니다. v1.1 롬에서 가장 긴 줄은 나레이션 174px, 얼굴 창 128px입니다. 줄을 나눌 때는 이렇게 정했습니다.

  1. 단어 중간에서 끊지 않습니다.
  2. 위아래 줄 길이를 비슷하게 맞춥니다.
  3. 필요한 것보다 줄을 많이 쓰지 않습니다.
  4. 더듬는 말(「주‥죽어」)은 한 줄에 둡니다.
더 깊이넘침을 판정하는 코드와 Mesen 2로 잰 한계

창 크기는 $81:AD51–$81:ADDD가 창 정보 첫 바이트를 보고 정합니다. 나레이션이면 $020E = 0x16(22칸), 그 밖이면 0x10(16칸)이고, 이어서 $B7:9729가 $020A = $020E × 8을 넣고 x 위치와 줄 위치를 0으로 만듭니다. 글자마다 아래 코드가 돕니다.

$B7:9846  CLC / ADC $0208     ; 이번 글자 폭 + 지금 x = 이번 글자의 오른쪽 끝
$B7:984A  CMP $020A           ; 창 폭과 비교
$B7:984D  BEQ / BCC $B79890   ; 같거나 작으면 그대로 그림
$B7:9851  INC $32 ×2          ; 크면 다음 줄로 ($32 += 2)
$B7:9857  CMP $0210           ; $32가 $0210 이상이면 페이지 대기 뒤 새 페이지

그래서 글자 폭 합이 창 폭과 같은 줄까지는 한 줄에 들어갑니다. v1.1 롬의 첫 나레이션(대사 $000)을 시험 줄로 바꿔 Mesen 2에서 띄워 확인했습니다. 「어」×8 + 「!」×8 = 176px 줄은 한 줄에 다 나왔고, 「어」×6 + 「!」×11 = 177px 줄은 마지막 「!」가 셋째 줄로 넘어갔습니다. 창 정보를 얼굴 창으로 바꾸면 128px까지 들어가고 129px에서 넘어갔습니다.

위: 첫 줄 176px, 둘째 줄은 「!」 10개(168px)까지. 아래: 11번째 「!」(177px)가 셋째 줄로 넘어감

일본판은 첫 나레이션 첫 줄이 딱 176px입니다. 같은 엔진인 각성편은 나레이션 창이 21칸(168px)이라 돌격편보다 한 칸 좁습니다(각성편 노트의 대사 절).

4. 글자 쉽게

게임은 글자 코드를 받아 폰트에서 그 번호의 그림을 찾아 화면에 찍습니다. 돌격편은 코드 범위에 따라 폰트를 읽는 곳이 다릅니다. 일본판의 한자 자리는 16×16, 가나 자리는 8×16 그림입니다.

코드크기폭원래 글자롬 위치
0x100–0x4FF8×169px가나$83:DE00
0x500–0x7EF16×1613px한자$83:8000
0x7F0–0xBEF16×1613px한자$BF:8000
0xC00–0xFFF16×1613pxv1.1이 새로 만든 칸블록 8곳

글자 그림은 2bpp 타일입니다. 한 픽셀에 비트가 두 개(플레인 0·1)라 네 가지 값을 가집니다. 돌격편 폰트는 글자 픽셀을 플레인 0만 1(색 1)로, 그 오른쪽·아래·오른쪽 아래의 빈 칸을 두 플레인 모두 1(색 3)로 그려 그림자를 붙입니다. 아래 위젯은 이 규칙으로 그린 「가」에서 시작합니다. 칸을 눌러 바꾸면 타일 바이트가 따라 바뀝니다(7장 픽셀 하나에 비트 몇 개).

타일을 칠하며 바이트 보기

8×8 타일 하나가 어떤 바이트로 저장되는지 봅니다. 칸을 눌러 고른 색으로 칠하거나(방향키로 옮기고 Space로 칠합니다), 아래 바이트를 직접 고쳐 보세요. 이미 고른 색인 칸을 누르면 색 0으로 돌아갑니다.

지금 그림은 2bpp 타일 16바이트로 그린 「가」입니다. 글자는 색 1, 그림자는 색 3입니다(스크립트가 꺼져 있어 칸을 누를 수 없습니다).

형식
칠할 색

비트평면으로 나누면

  • 평면 0 (색 번호의 1의 자리)
  • 평면 1 (색 번호의 2의 자리)
줄마다 바이트 (+위치: 16진수, 아래는 2진수)
줄평면 0평면 1
0+00 0000000000+01 0000000000
1+02 F611110110+03 1200010010
2+04 F611110110+05 D211010010
3+06 3700110111+07 1100010001
4+08 3700110111+09 1300010011
5+0A 7601110110+0B 3200110010
6+0C E611100110+0D 6201100010
7+0E C611000110+0F C611000110

바이트(16진수, 메모리 순서)

00 00 F6 12 F6 D2 37 11 37 13 76 32 E6 62 C6 C6

폰트 칸이 모자랐다

v1.0은 한자 자리 850개와 가나 자리 160개에 한글을 그려 넣었습니다. 아래는 v1.0 롬의 16×16 칸 전체입니다. 남색은 한글이 들어간 칸, 노란색은 한글이 아닌 칸(기호와 남은 한자)입니다.

한글 음절은 자주 쓰는 것만 해도 2,350자입니다. 1,010칸에는 v1.0 번역문에 쓴 글자만 골라 넣을 수 있었고, 대사를 원문에 맞춰 다시 옮기니 꽃·쫓·흙처럼 칸에 없는 글자가 필요해졌습니다. 한글을 좁은 폰트 칸에 넣는 여러 방법은 7장 한글 수천 자를 제한된 폰트 칸에 넣기에 있습니다.

글자 칸을 512개 늘리기

글자를 그리는 코드($B7:9763)는 코드 범위를 보고 어느 폰트를 읽을지 고릅니다. v1.1은 그중 0x7F0 이상 코드를 맡는 갈림길($B7:97BC, 파일 0x1B97BC)의 4바이트를 "새 코드로 가라" 는 명령으로 덮어썼습니다. 원래 코드 중간에 새 코드를 끼워 넣는 이 방법을 후킹이라고 합니다(6장 JSL과 RTL).

  • 38 E9 F0 07일본판: SEC / SBC #$07F0
  • 5C 00 FA B7v1.1: JML $B7FA00

새 코드는 일본판에서 0xFF였던 $B7:FA00부터 0x78바이트입니다. 코드가 0xC00보다 작으면 원래 처리를 그대로 하고, 크면 블록 표를 보고 새 글자 블록 8곳 가운데 하나를 읽습니다. 블록 하나는 4KB이고 글자 64개(코드 128개)가 들어갑니다.

블록코드위치파일v1.2
0C00–C7F$81:EF000x0EF00사용
1C80–CFF$84:EE000x26E00사용
2D00–D7F$86:E0000x36000사용 (v1.1 D57까지, v1.2 D63까지)
3D80–DFF$86:F0000x37000비어 있음
4E00–E7F$A6:E6000x136600비어 있음
5E80–EFF$AE:EA000x176A00비어 있음
6F00–F7F$B1:E5000x18E500비어 있음
7F80–FFF$B6:E6000x1B6600비어 있음

8곳 모두 일본판과 v1.0에서 4KB 전부 0xFF였습니다. 글자 폭은 원래부터 0x500 이상 코드를 모두 13px로 계산하므로 따로 고칠 필요가 없었습니다. 새로 그린 213자는 먼저 v1.1에서 더 쓰지 않게 된 칸 37개에 넣고, 나머지 176자를 블록 0–2(코드 0xC00–0xD57)에 넣었습니다. 글자 모양은 픽셀 폰트 Galmuri11로 그리고, 그림자는 위 규칙으로 붙였습니다.

새 블록의 글자는 이렇게 찾습니다. 「쫓」은 코드 0xCE5입니다. 0xC00을 빼면 0xE5이고, 블록 하나의 코드 수 0x80으로 나누면 몫 1, 나머지 0x65입니다. 1번 블록은 $84:EE00이고 코드 하나가 32바이트이므로 $84:EE00 + 0x65 × 32 = $84:FAA0(파일 0x27AA0)에 글자가 있습니다. 16×16 글자는 위 타일 둘과 0x100바이트 뒤의 아래 타일 둘로 나뉩니다.

같은 엔진의 각성편은 폰트 형식이 달라 다른 방법으로 글자를 늘렸습니다(각성편 노트의 「폰트에 없는 글자」).

더 깊이새 코드와 블록 표의 실제 바이트
$B7:FA00  CMP #$0C00      ; 코드가 0xC00 이상이면
          BCS ext         ; 새 블록으로 간다
          ...             ; 아니면 원래 $BF 폰트 처리 뒤 JML $B797D6
$B7:FA23  ext:
          SEC
          SBC #$0C00      ; 0xC00을 빼고 0x80으로 나눠 블록 번호를 구한다
          ...             ; 나머지 × 32 = 블록 안 위치
          LDA $B7FA60,X   ; 블록 표에서 그 블록의 주소를 읽고
          JML $B797D6     ; 원래 글자를 그리는 코드로 돌아간다

v1.1 롬의 블록 표는 이렇습니다. 주소는 2바이트씩 작은 자리부터, 뱅크는 1바이트씩입니다.

$B7:FA60  00 EF 00 EE 00 E0 00 F0 00 E6 00 EA 00 E5 00 E6   ; $EF00 $EE00 $E000 $F000 $E600 $EA00 $E500 $E600
$B7:FA70  81 84 86 86 A6 AE B1 B6                           ; 뱅크

글자를 더 넣을 때는 빈 블록 3–7(320자)을 그대로 쓰면 됩니다. 블록 표는 이미 여덟 곳을 다 가리키고 있어 고칠 필요가 없습니다.

v1.2에서 더 그린 글자

「플」「딩」「렉」은 16px 글자가 없어서 가나 자리의 8px 글자(0x24B, 0x148, 0x14D)를 대사와 메뉴에 빌려 쓰고 있었습니다. 16px 글자 사이에서 반 칸으로 보였습니다. v1.2는 블록 2의 빈 칸(0xD61–0xD63)에 16px로 새로 그리고, 8px 글자는 이름 입력과 엔딩 암호에만 남겼습니다. 「펴」도 새 16px 글자(0xD60)로 그렸습니다(7절).

v1.1

v1.2

전투 화면 위 띠의 「슬라이딩 킥」. v1.1은 「딩」만 8px 글자였습니다.

5. 그림·압축 쉽게

타이틀 화면은 그림 여러 겹을 겹쳐서 만듭니다. 오공 그림, 배경 그라데이션, 로고와 글자가 있는 층(BG3)입니다. 한글패치는 이 중 글자 층만 고쳤습니다. v1.1은 「超悟空伝 突激編」을 한글로 다시 그리고, 로고 아래 가타카나는 지웠습니다.

일본판 타이틀의 세 겹

v1.0

v1.1

v1.0은 타이틀 글자 층을 일본판 그대로 두었고, v1.1이 한글로 다시 그렸습니다.

압축된 글자 층

글자 층의 타일과 배치표는 LZSS로 압축되어 있습니다. 앞에 나온 무늬가 다시 나오면 "몇 칸 앞에서 몇 바이트 복사" 로 줄여 쓰는 방식입니다(9장 LZSS: 플래그 바이트로 둘을 섞기). 일본판 타일 자료($B7:9EB1, 파일 0x1B9EB1)의 첫 14바이트를 게임이 푸는 순서대로 읽으면 이렇습니다.

읽은 바이트종류뜻
60 13크기처음 2바이트는 풀었을 때의 크기입니다: $1360 = 4,960바이트.
DC플래그플래그 바이트 DC = 11011100. 아래 비트(비트 0)부터 뒤따르는 항목 8개의 종류를 알려 줍니다.
2F 01복사비트 0 = 0 → 복사: 워드 $012F에서 거리 = $012 = 18, 길이 = 15 + 3 = 18. 출력 0번 칸부터 18칸 앞(시작보다 18칸 앞)에서 18바이트(00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00)를 가져옵니다. 시작보다 앞은 0으로 읽습니다.
F0 00복사비트 1 = 0 → 복사: 워드 $00F0에서 거리 = $00F = 15, 길이 = 0 + 3 = 3. 출력 18번 칸부터 15칸 앞(3번 칸)에서 3바이트(00 00 00)를 가져옵니다.
01그대로비트 2 = 1 → 그대로 1바이트 01. 출력 21번 칸에 그대로 씁니다.
0F그대로비트 3 = 1 → 그대로 1바이트 0F. 출력 22번 칸에 그대로 씁니다.
3F그대로비트 4 = 1 → 그대로 1바이트 3F(?). 출력 23번 칸에 그대로 씁니다.
89 00복사비트 5 = 0 → 복사: 워드 $0089에서 거리 = $008 = 8, 길이 = 9 + 3 = 12. 출력 24번 칸부터 8칸 앞(16번 칸)에서 12바이트(00 00 00 00 00 01 0F 3F 00 00 00 00)를 가져옵니다. 가져올 곳과 쓸 곳이 겹쳐서, 방금 쓴 바이트를 다시 읽으며 반복합니다.
1F그대로비트 6 = 1 → 그대로 1바이트 1F. 출력 36번 칸에 그대로 씁니다.
FF그대로비트 7 = 1 → 그대로 1바이트 FF. 출력 37번 칸에 그대로 씁니다.

14바이트가 38바이트로 풀렸습니다. 아래 위젯에서 「돌격편 타이틀 그림 첫머리」를 고르면 같은 바이트를 한 단계씩 풀어 볼 수 있습니다. 압축 형식과 그림 배치가 각성편과 같아서, 각성편 v1.2를 만들 때 쓴 도구를 주소만 바꿔 썼습니다(각성편 노트의 「압축된 타이틀 그림」).

LZSS 압축 한 단계씩 풀기

압축된 바이트를 앞에서부터 읽으며 출력을 만들어 갑니다. 「한 단계 풀기」를 누를 때마다 항목 하나를 읽습니다. 예를 고르거나 글을 직접 적어 압축한 뒤 풀어 볼 수도 있습니다. 영문·숫자는 한 글자에 1바이트, 한글은 한 글자에 3바이트(UTF-8)로 셉니다.

예

15바이트 글이 9바이트가 됩니다. 넷째 항목은 아직 다 쓰지 않은 칸까지 겹쳐 베낍니다.

압축된 바이트 (9바이트)

  1. 0F크기
  2. 00크기
  3. 17플래그
  4. 68그대로
  5. 61그대로
  6. 20그대로
  7. 38복사
  8. 00복사
  9. 21그대로

플래그 비트 (1 = 그대로 1바이트, 0 = 복사 2바이트)

    아직 아무것도 읽지 않았습니다.

    출력 (15바이트)

    1. 068h
    2. 161a
    3. 220␣
    4. 368h
    5. 461a
    6. 520␣
    7. 668h
    8. 761a
    9. 820␣
    10. 968h
    11. 1061a
    12. 1120␣
    13. 1268h
    14. 1361a
    15. 1421!

    가져온 곳 방금 쓴 곳 시작보다 앞(0)

    한 단계씩 읽으면

    1. 처음 2바이트는 풀었을 때의 크기입니다: $000F = 15바이트.
    2. 플래그 바이트 17 = 00010111. 아래 비트(비트 0)부터 뒤따르는 항목 8개의 종류를 알려 줍니다.
    3. 비트 0 = 1 → 그대로 1바이트 68(h). 출력 0번 칸에 그대로 씁니다.
    4. 비트 1 = 1 → 그대로 1바이트 61(a). 출력 1번 칸에 그대로 씁니다.
    5. 비트 2 = 1 → 그대로 1바이트 20(␣). 출력 2번 칸에 그대로 씁니다.
    6. 비트 3 = 0 → 복사: 워드 $0038에서 거리 = $003 = 3, 길이 = 8 + 3 = 11. 출력 3번 칸부터 3칸 앞(0번 칸)에서 11바이트(68 61 20 68 61 20 68 61 20 68 61)를 가져옵니다. 가져올 곳과 쓸 곳이 겹쳐서, 방금 쓴 바이트를 다시 읽으며 반복합니다.
    7. 비트 4 = 1 → 그대로 1바이트 21(!). 출력 14번 칸에 그대로 씁니다.

    롬에서 자료를 찾아가는 길

    두 자료의 위치는 롬의 5바이트 항목(주소 2바이트, 뱅크 1바이트, 크기 2바이트)에 적혀 있습니다. 타일 항목은 파일 0xA752에 있고 앞 3바이트가 자료 주소입니다.

    • B1 9E주소 $9EB1
    • B7뱅크
    • $B7:9EB1타일 자료
    타이틀 타일을 푸는 순서읽기 루틴 $81:A703이 표 $81:A743의 3번 항목을 읽어 압축 자료 $B7:9EB1을 찾고, 해제 루틴 $81:DF4F가 WRAM $7E:8000에 풀고, $81:DFEB가 타일마다 두 플레인 순서를 바꾼 뒤 VRAM으로 보낸다.$81:A703표 $81:A743의 3번 항목을 읽음$B7:9EB1압축된 타일 자료 (일본판 3,613바이트)$81:DF4FLZSS를 풀어 WRAM $7E:8000에 씀$81:DFEB타일마다 두 플레인의 순서를 바꿈VRAMBG3 글자 층으로 화면에 나옴
    타이틀 타일이 롬에서 화면까지 가는 길

    배치표는 $81:A8B0이 표 $81:A8EC의 39번 항목(파일 0xA9AF, 자료 $B7:BE7F)으로 부릅니다. 새로 그린 그림을 다시 압축하니 타일은 3,613바이트에서 3,237바이트로, 배치표는 781바이트에서 779바이트로 줄어 제자리에 들어갔고, 항목은 일본판 그대로입니다. 더 길어지면 빈 공간(파일 0x13F300, 0x17FB40)으로 옮기고 항목을 고치도록 도구에 넣어 두었습니다.

    「DRAGON BALL Z」 로고와 「© BANDAI 1995」는 원본 그대로입니다. 이 © 타일은 위쪽 저작권 줄과 함께 쓰기 때문에 건드리면 두 줄이 같이 바뀝니다.

    더 깊이부팅 뒤 그래픽을 푸는 순서와 해제 루틴

    해제 루틴은 $81:DF4F입니다. A에 주소, X(8비트)에 뱅크를 받아 WRAM $7E:8000에 풀고 크기를 $F0에 남깁니다. $81:DFE7은 이 루틴을 부른 뒤 $81:DFEB에서 16바이트 타일마다 플레인 순서를 바꿉니다. 일본판에서 JSL $81DF4F는 15곳, JSL $81DFE7은 26곳입니다. Mesen 2로 부팅 뒤를 기록하니 일본판과 v1.1 모두 아래 순서였습니다.

    프레임부른 곳자료
    114$81:A703 4번 항목$B7:ACCF
    126$81:A703 3번 항목$B7:9EB1 (타이틀 타일)
    135·137·139$81:A8B0$B7:BAB9, $B7:BD47, $B7:B94F
    536$81:A8B0 39번 항목$B7:BE7F (타이틀 배치표)

    각성편 도구의 머리말에 적힌 $80:9CFA는 각성편의 해제 루틴 주소입니다. 돌격편에서는 메뉴 커서 코드 중간이라, 압축된 곳을 찾을 때는 이 게임에서 해제 루틴을 다시 찾아야 합니다(9장 롬에서 압축된 곳과 푸는 코드 찾기).

    6. 메뉴·이름·전투 실제 패치에서

    대사 말고도 글자가 나오는 곳은 많습니다. 전투 대사, 선택지, 장소 이름, 기술명이 저마다 포인터 표를 가지고 있고, 형식은 대사와 같습니다. 모두 2바이트 포인터라 표가 있는 뱅크 안에서만 다시 채웠습니다.

    내용뱅크표개수v1.1 사용량(바이트)
    전투 대사$B1$B1:9C3476915,338 / 19,820
    스토리 현황 이벤트 목록$AD$AD:80002044,706 / 8,770
    선택지$A9$A9:80001171,826 / 5,632
    미니게임$98$98:8000561,422 / 3,456
    장소 이름$87$87:80001001,210 / 3,166
    맵 이름$82$82:B30654바꾸지 않음
    기술명$84$84:B605자동1,394 / 6,053
    동료 코멘트$9F$9F:8000자동8,704 / 11,642
    전투 중 적 이름$AE$AE:C91055462 / 2,108
    세이브 장면 대사$81코드 속 12곳12544 / 840

    「자동」은 표 길이가 따로 없어 첫 문자열 주소 앞까지를 표로 본다는 뜻입니다. 사용량의 분모는 일본판 문자열이 차지하던 영역입니다.

    세이브 장면 대사 12개와 전투 외침 7개는 표가 없습니다. $81 뱅크 코드의 LDA #$xxxx 피연산자가 곧 문자열 주소라, 문자열을 옮기면 그 명령의 둘째·셋째 바이트를 고칩니다. 도구에는 피연산자 위치(파일 0xD85A, 0xE300 등)를 목록으로 적어 두었습니다. 이런 포인터는 8장 포인터 표 아래 「더 깊이」에서도 다룹니다.

    한 칸에 눌러 그린 두 음절

    스토리 현황 화면의 「기술」「습득」「완료」「클리」는 v1.0이 16px 한 칸(0xB97·0xBA0·0xBA1·0xBA2)에 두 음절을 눌러 그린 글자였습니다. 일본판에서는 한자 한 자가 들어가던 칸입니다. v1.2는 이 글자를 쓰지 않고 16px 글자 두 개로 씁니다.

    v1.1

    v1.2

    스토리 현황 오른쪽 메뉴

    오른쪽 메뉴 창은 폭이 6칸, 48px입니다. 16px 글자는 한 자에 13px씩 나아가므로 네 자면 52px이라 넘칩니다. 그래서 코드에 0x8000을 켜 같은 그림을 12px 간격(작은 글자)으로 찍었습니다. 일본판도 이 창의 한자를 같은 간격으로 썼습니다.

    48px 창과 글자 간격창 폭 48px을 1px당 5px로 그렸다. 13px 간격 네 글자는 52px이라 창 오른쪽 끝을 4px 넘는다. 12px 간격 네 글자는 48px로 창에 꼭 맞는다.창 끝 48px13px 간격 → 52px기술설정12px 간격 → 48px기술설정
    폭 48px 창에 네 글자 넣기. 13px 간격은 넘치고 12px 간격은 꼭 맞습니다.

    기술습득표와 이벤트 표의 줄 머리에 붙는 「습득」「완료」 표시는 표를 조립하는 코드($80:40BC, $80:3D83)가 코드 하나만 쓰게 되어 있었습니다. v1.2는 두 곳을 $B7:FB00·$B7:FB32의 새 루틴으로 JSL하게 바꿔, 16px 두 글자(아니면 같은 폭 빈칸)와 그 뒤 빈칸을 쓰게 했습니다. 두 목록은 줄 폭이 224px이고, 가장 긴 줄은 기술습득표 224px, 이벤트 표 223px입니다.

    v1.1

    v1.2

    기술습득표의 「습득」 표시

    엔딩 크레딧과 그 밖의 메뉴 문자열

    • 포인터 표 밖의 메뉴 문자열은 파일 위치와 새 문자열을 tables/ui-fixes.tsv에 모아 두고 빌드 때 덮어씁니다. 타이틀 메뉴는 파일 0x6F17(NEW GAME / OPTION)과 0x6F3D(CONTINUE / NEW GAME / OPTION)에 있습니다.
    • 엔딩 크레딧은 줄마다 $B7 뱅크의 고정 칸에 있고, 칸 크기가 일본어 줄 길이로 정해져 있어 그보다 길게 쓸 수 없습니다. 출연진은 열 가운데(3열 48/128/208px, 2열 88/168px)에 맞춰 다시 줄을 세웠습니다.
    • 줄을 WRAM에 이어 붙이는 화면에는 다음 문자열을 앞 문자열 바로 뒤에서 읽는 곳이 있습니다. 달성치 화면 라벨($80:BAB2, 19코드)과 상태 화면 아래 줄 앞 라벨($80:C9C5, 5코드)은 코드 수를 바꾸지 않았습니다.
    • 이름 입력 글자판(파일 0x7005, 160칸)은 v1.0이 쓴 16비트 문자열입니다. 엔딩 암호와 같은 8px 글자를 쓰므로, 7절에서 다시 그린 다섯 칸이 함께 바뀝니다.

    7. 세이브·암호 실제 패치에서

    세이브는 카트리지의 SRAM 8KB에 있습니다. 한글패치는 세이브 구조를 바꾸지 않았습니다. v1.0에서 만든 세이브를 v1.1에서 불러와 이어서 진행되는 것을 확인했습니다. 저장 루틴은 $81:D210(슬롯 번호 $0119), 불러오기는 $81:D3A9이고, 두 주소를 부르는 JSL이 롬에 있습니다.

    각성편으로 넘기는 암호

    돌격편 엔딩의 마지막 화면(TO BE CONTINUED)에는 원래 가나 50자 암호가 나옵니다. 2편 각성편에서 새 게임 → 「암호」를 고르고 이 암호를 넣으면 주인공 이름과 기록을 이어받습니다. v1.2는 이 자리에 각성편 v1.3이 받는 고정 암호 「손오공은사이어인」을 보여 줍니다.

    v1.1

    v1.2

    돌격편 엔딩의 암호 화면

    원래 암호는 WRAM $7E:0A00에 작은 가나 코드(0x8000이 켜진 코드)로 만들어집니다. 두 글자가 1바이트이고, 쌍 표 $B7:F09F(256쌍, 일본판 그대로)에서 순번이 바이트 값입니다. 25쌍은 23바이트 + 합계 2바이트이고, 그 안에 주인공 이름이 들어 있습니다. 각성편이 이 바이트를 어떻게 읽는지는 각성편 노트의 「돌격편에서 넘어오는 암호」에 바이트별 표로 있습니다.

    v1.2는 $81:D169부터 쌍을 바꾸고 옮기던 코드 자리에 짧은 복사 루프와 문자열을 넣었습니다. 문자열은 빈 줄 하나, 작은 빈칸 다섯 개, 아래 여덟 글자, 빈 줄 둘이고, 이어서 원래 코드가 「TO BE CONTINUED」를 다섯째 줄에 그립니다. 창은 18칸(144px)이고 작은 글자는 8px라, 여덟 글자 64px 앞에 빈칸 다섯(40px)을 두면 가운데에 옵니다.

    • 8182손
    • 81A7오
    • 8106공
    • 81AF은
    • 8221사
    • 81C1이
    • 81A2어
    • 81C2인

    이 여덟 글자를 각성편 v1.3(CRC32 B9F14962)에 넣으면 통과해 이름 화면으로 가고, 이름은 각성편 기본 이름 「엑소버드」입니다(Mesen 2).

    헷갈리던 암호 글자 다섯

    v1.0이 가나 자리에 8px 한글을 그려 넣어서 한글패치는 암호를 한글로 보여 줍니다. 그런데 v1.1까지는 서로 다른 가나 다섯 개가 다른 글자와 똑같거나 거의 같은 모양이었습니다. 암호를 화면에서 옮겨 적으면 틀린 글자를 고르게 됩니다. 아래는 v1.1 롬에서 꺼낸 글자 그림과 v1.2가 다시 그린 그림입니다.

    같은 그림이던 암호 글자첫 줄: 0x121 つ는 나, v1.1의 0x12C へ도 똑같은 나, v1.2의 0x12C는 다. 둘째 줄: 0x146 ら는 등, v1.1의 0x142 も도 똑같은 등, v1.2의 0x142는 동.v1.10x121 つ나v1.10x12C へ나 (같은 그림)v1.20x12C へ다v1.10x146 ら등v1.10x142 も등 (같은 그림)v1.20x142 も동
    v1.1 롬의 8px 글자. 0x12C와 0x142는 이웃 글자와 픽셀까지 같았고, v1.2가 다시 그렸습니다.
    가나 자리v1.1에서 보인 글자v1.2
    へ(0x12C) = つ(0x121)나다
    も(0x142) = ら(0x146)등동
    を(0x14C) ≈ わ(0x14B)러레
    ん(0x14D) ≈ わ(0x14B)러렉
    ル(0x1C8) ≈ ォ(0x1E2)지저

    글자 표(tables/kr-v1.0.txt)는 이 다섯 자리를 다·동·레·렉·저로 적고 있어서, v1.2는 그 글자로 다시 그렸습니다. 폭 8px, 1px 획, 그림자 없음으로 이웃 글자와 같은 방식입니다. 같은 자리를 쓰는 이름 입력 글자판에서 같은 모양이 두 칸 나오지 않게 됐고, 엔딩 스태프 「디러터」는 원래 뜻대로 「디렉터」가 됐습니다. 그림을 바꾸는 방법은 7장 화면은 8×8 타일을 이어 붙인 그림과 같습니다.

    v1.1

    v1.2

    주인공 이름을 「다동레렉저」로 지었을 때
    더 깊이표와 다른 글자 넷, 빌드 검사, 데이터 암호 확인
    • 글자 표와 픽셀이 다른 자리는 다섯 말고도 넷 있었습니다(0x120 께, 0x1A4 건, 0x246 띠, 0x248 떠). v1.1은 0x248을 「펴」로 알고 대사 두 줄($5D5 「펴‥평범한」, $101B 「살펴봐」)에 써서 「떠」로 보였습니다. v1.2는 표를 픽셀대로 고치고 「펴」를 새 16px 글자(0xD60)로 그렸습니다.
    • 빌드는 다섯 자리가 정해 둔 그림과 다르거나, 암호 글자 163자 가운데 같은 모양이 있거나, 글자 표에서 이름이 다른 두 코드가 같은 그림을 그리거나, 이름 입력 글자판에 같은 모양이 두 칸 있으면 멈춥니다(tools/pwglyph1.py).
    • 대사·전투·메뉴 문자열 7,899개를 v1.1과 코드 단위로 비교해, 바뀐 것은 「펴」를 쓰는 대사 두 줄뿐임을 확인했습니다.
    • 원래 데이터 암호도 쓸 수 있는지 확인했습니다. v1.2에서 엔딩 암호 바꾸기만 뺀 실험용 롬으로 디버그 엔딩 암호, 주인공 이름을 「다동레렉저」로 바꾼 암호를 화면 한글대로 각성편 v1.3에 넣으면 모두 통과했고, 각성편 이름 화면에 같은 이름이 나왔습니다(Mesen 2).
    • 돌격편 안에도 가나 입력 화면 코드($80:99D6, $82:9D7C에서 부름)가 있습니다. 부팅 직후 이곳으로 가게 고친 롬에서는 글자판이 반쯤 깨진 채 열렸습니다. 정상 플레이에서 열리는 길은 찾지 못했습니다.

    8. 게임 고유 문제 실제 패치에서

    돌격편에서 따로 다룰 문제는 스토리 현황의 이벤트 표입니다. 일본판에도 있던 동작이 v1.0에서 글자 코드가 바뀌며 실제로 화면을 닫지 못하는 문제가 됐습니다.

    이벤트 표에서 못 나오던 문제

    1. 대사가 떠 있을 때 START를 누르면 스토리 현황 화면이 열립니다.
    2. 오른쪽 메뉴에서 「클리어 표」를 고르고, 기술습득표에서 B를 누르면 이벤트 표가 나옵니다.
    3. 목록을 맨 아래까지 내리면 204개 뒤에 숨은 줄 하나를 더 그립니다.

    v1.0

    v1.1

    이벤트 표 맨 아래에서 B 버튼을 두 번 누른 뒤 A 버튼을 누른 같은 시점. v1.0은 기술 설정으로 넘어가고, v1.1은 게임으로 돌아옵니다.

    이벤트는 204개(0–203번)인데, 목록을 내리는 조건($80:BC8A의 CMP #$00CD)이 205줄을 기준으로 되어 있어서 맨 아래에서 없는 204번 줄을 그립니다. 그 줄의 포인터 자리($AD:8198, 파일 0x168198)에는 포인터가 아니라 첫 이벤트 이름의 글자 코드가 있습니다. 게임은 이 값을 주소로 알고 WRAM의 그 자리를 문자열로 읽습니다.

    그 자리 값은 게임 상황마다 다릅니다. 끝 표시가 늦게 나오면 줄이 넘치고, B로 목록을 닫을 수 없게 됩니다. v1.0은 첫 글자 코드가 바뀌면서 읽는 주소가 $7E:0686이 됐고, 디버그 메뉴로 장면을 0x00–0xF8까지 8 간격으로 바꿔 가며 재현해 보니 0xC0번 장면에서 실제로 목록이 닫히지 않았습니다. v1.1은 204번 자리에 빈 문자열을 가리키는 포인터를 넣었습니다.

    • A5 05일본판 → $05A5
    • 86 06v1.0 → $0686
    • FA 93v1.1 → $AD:93FA

    v1.1이 가리키는 $AD:93FA에는 끝 표시 00 00만 있습니다. 같은 0xC0번 장면에서 B를 두 번 눌러 게임으로 돌아오는 것을 확인했습니다. 표 끝의 숨은 항목처럼 포인터가 아닌 값을 포인터로 읽는 곳은 8장 포인터 표를 읽을 때도 조심할 점입니다. 장면마다 같은 입력을 넣어 재현한 방법은 12장 Lua로 확인을 자동으로에 있습니다.

    9. 확인 방법 직접 해 보기

    대사 5,370줄을 사람이 다 띄워 볼 수는 없습니다. 그래서 게임에 숨어 있는 개발자용 디버그 메뉴로 장면을 바로 시작하고, 에뮬레이터 Mesen 2와 Lua 스크립트로 입력과 기록을 자동으로 했습니다.

    디버그 메뉴를 여는 두 바이트

    리셋 뒤 계속 도는 루프의 한 부분($80:8061, 파일 0x61)을 보겠습니다. 어셈블리는 CPU가 실행하는 명령을 사람이 읽는 이름으로 적은 것이고, 롬 안에서는 그냥 바이트입니다(5장 분기와 반복).

    바이트어셈블리뜻
    80 04BRA $8067무조건 4바이트 뒤로 건너뜁니다. 제품판은 다음 줄을 늘 건너뜁니다.
    22 B0 86 82JSL $8286B0디버그 메뉴 루틴을 부릅니다. 주소는 거꾸로 적힙니다.
    22 00 80 82JSL $828000건너뛴 뒤 제품판이 실행하는 곳입니다.
    • 80 04제품판: BRA
    • EA EANOP NOP

    첫 줄 80 04를 아무 일도 안 하는 명령 두 개 EA EA로 바꾸면 건너뛰지 않고 디버그 메뉴로 들어갑니다. 배포한 패치는 이 두 바이트를 바꾸지 않습니다.

    ADVENTURE의 SCENARIO SELECT로 장면 번호($00–$FF)를 고르면 그 장면을 실제 플레이와 같은 상태로 시작합니다. DATA의 ENDING으로는 엔딩을 처음부터 볼 수 있습니다.

    대사 순서를 일본판과 맞대 보기

    대사 순서 비교디버그 메뉴: 시나리오 $00–$FF를 하나씩 시작. 입력: A를 일정 간격으로 눌러 8000프레임 진행. 감시: 창 정보 $8E:C9BA + ID×2를 읽는 순간을 기록. 비교: 일본판과 v1.1의 대사 ID 순서를 맞대 봄디버그 메뉴시나리오 $00–$FF를 하나씩 시작입력A를 일정 간격으로 눌러 8000프레임 진행감시창 정보 $8E:C9BA + ID×2를 읽는 순간을 기록비교일본판과 v1.1의 대사 ID 순서를 맞대 봄
    회귀 테스트의 흐름. 같은 장면을 일본판과 한글패치에서 똑같이 돌립니다.
    • 디버그 메뉴로 256개 시나리오를 하나씩 시작하고, A 버튼을 일정 간격으로 눌러 8000프레임씩 진행합니다.
    • 게임은 대사를 띄울 때마다 대사 ID로 창 정보($8E:C9BA + ID×2)를 읽습니다. 이 자리를 누가 읽는지 감시하면 어떤 대사가 몇 번째로 나왔는지 기록할 수 있습니다.
    • 일본판과 v1.1의 대사 ID 순서를 시나리오마다 비교했습니다. 순서가 다르거나 v1.1에서 먼저 멈춘 시나리오는 없었습니다. 한글 대사가 짧아 같은 시간에 더 진행한 경우만 있었습니다.
    • 패치 노트의 비교 화면도 이 방법으로 찍었습니다. 3프레임마다 화면을 저장하고, 아래 절반의 흰 글자가 지워지기 직전 화면을 그 페이지의 완성 화면으로 골랐습니다.
    • A 버튼을 여러 프레임 누르고 있으면 글자 출력 완료와 페이지 넘김이 한꺼번에 일어나 페이지가 화면에 다 나오지 않습니다. 촬영할 때는 1프레임만 눌렀습니다.

    "어디서 읽나" 를 잡는 감시와 같은 입력을 되풀이하는 스크립트는 12장 중단점과 Lua로 확인을 자동으로에서 다룹니다. 각성편도 같은 방법으로 확인했습니다(각성편 노트의 확인 절).

    여러 에뮬레이터에서 같은 입력

    libretro 코어를 직접 불러 쓰는 작은 프로그램으로 같은 입력을 여러 에뮬레이터에 넣었습니다(12장 여러 에뮬레이터에서 같은 입력 돌리기). 새 게임은 이름 입력부터 첫 장면과 필드까지 1만 6천 프레임, 이어하기는 v1.0에서 만든 세이브를 불러 대사가 이어지는 데까지 봤습니다. 롬은 일본판, v1.0, v1.1, v1.2와 헤더 붙은 v1.1·v1.2 여섯 가지입니다. v1.2는 이름 입력 글자판 다섯 칸이 바뀐 것 말고는 모든 코어에서 v1.1과 같은 화면이었습니다.

    에뮬레이터결과
    Mesen 2정상. 이어하기에서 나온 대사 ID 순서가 일본판·v1.0·v1.1·v1.2(헤더 있음/없음) 모두 같음
    MesenCE 2.2.1정상. 새 게임·이어하기, 디버그 시나리오 64개, 이벤트 표 문제 재현, 엔딩 암호에서 대사 ID 기록이 Mesen 2와 같음
    bsnes, bsnes-hd beta정상
    snes9x, snes9x2010정상. snes9x2010도 이어하기 대사 ID 순서가 여섯 롬 모두 같음
    Beetle Supafaust정상. 새 게임은 다른 코어와 몇 프레임 어긋나 필드에서 다른 길로 감(일본판도 코어마다 같은 정도로 갈라짐)
    Mesen-S 코어판정 불가 (일본판 원본도 테스트 프로그램에서 바로 종료)

    Mesen-S를 뺀 일곱 개 모두 멈추거나 화면이 깨진 곳은 없었습니다. 헤더 있는 롬과 없는 롬은 libretro 코어 다섯 개에서 찍은 화면이 픽셀까지 같았습니다. 이어하기는 대사가 끝나면 검은 화면이 이어지는데, 일본판도 모든 에뮬레이터에서 같은 곳에서 같은 화면이 나와 패치와는 관계없습니다.

    더 깊이직접 돌려 보기: 명령

    디버그 메뉴를 연 롬과 패치 소스의 Lua 스크립트로 시나리오 $10을 돌리고 이벤트 표 문제를 재현하는 명령입니다.

    export LIBDIR=$(krpatch sfc lua)
    TARGET=10 SHOTS=1 SHOT_DIR=out Mesen --testrunner debug.smc lua/scenario.lua
    TARGET=c0 SHOT_DIR=out Mesen --testrunner debug.smc lua/eventlist.lua
    python3 tools/pages1.py out 0142          # 대사 0142의 페이지마다 완성된 프레임
    python3 tools/rgcmp1.py runs_jp runs_kr   # 두 롬의 대사 ID 순서 비교

    디버그 메뉴를 부르는 코드를 툴킷 역어셈블러로 읽으면 위 표가 그대로 나옵니다: krpatch sfc dis super-gokuden-jp.smc 808061 3 --mapper lorom.

    10. 다음 사람에게 실제 패치에서

    이 롬을 이어서 고칠 분이 알아 두면 좋을 것입니다. 주소와 형식 전체는 패치 소스의 롬 분석 문서(docs/ANALYSIS.md)에 있습니다.

    확인하지 않은 것

    • 실제 슈퍼 패미콤과 플래시 카트리지에서 돌려 본 기록은 없습니다. 에뮬레이터 결과는 9절에 있고, 실기에서만 드러나는 문제는 12장 실기에서만 알 수 있는 것을 보세요.
    • 창 정보 첫 바이트가 FE인 대사 347줄은 얼굴 창과 같은 128px로 잡히지만, 얼굴 그림을 부르는 루틴을 건너뛴다는 것만 코드로 봤고 화면은 보지 않았습니다.
    • 각성편으로 넘기는 원래 데이터 암호는 디버그 엔딩 암호와 이름만 바꾼 암호로만 확인했고, 끝까지 플레이한 세이브의 암호로는 확인하지 못했습니다. v1.2 엔딩은 고정 암호만 보여 줍니다.
    • 대사 $5D5를 부르는 스크립트는 $AC:E22D 부근에 있고 장면 $58(피라후 성)에서 이어지는 장면으로 보이지만, 자동 입력으로는 성 안 갈림길 선택지를 넘지 못해 실제 장면까지 가지 못했습니다. 패치 노트의 $5D5 화면은 실험용 롬에서 새 게임 첫 대사 자리가 $5D5의 글자 코드를 가리키게 해서 찍었습니다.
    • 원래 암호를 만드는 루틴의 주소는 찾지 않았습니다.

    알려진 한계

    • v1.0 글꼴에서 가로 모음과 받침이 있는 음절 46쌍(돌/들/둘, 울/올/을 등)은 1px 차이로만 갈립니다. Galmuri11로 다시 그려도 41쌍이 그대로였고, 세로 획을 2px로 늘리면 그 200자만 글자 높이가 1px 커져서 고치지 않았습니다.
    • 엔딩 크레딧은 줄마다 칸 크기가 고정이라 일본어 줄보다 길게 쓸 수 없습니다.
    • LZSS 형식은 각성편과 같지만 해제 루틴 주소는 다릅니다. 돌격편은 $81:DF4F와 $81:DFE7이고, 각성편 주소 $80:9CFA는 돌격편에서는 메뉴 커서 코드 중간입니다.
    • WRAM $4D는 대사 ID 자리이지만 그래픽 읽기 루틴도 잠깐 씁니다. 대사를 기록할 때는 창 정보 표를 읽는 순간을 감시하세요.
    • 새 글자 블록 3–7의 5곳(320자)이 통째로 비어 있습니다. 글자를 더 넣을 때 주소 표를 고치지 않고 쓸 수 있습니다.

    쓴 도구

    11. 패치 소스 파일 직접 해 보기

    위에서 설명한 파일은 모두 패치 소스의 게임 폴더에 있습니다. 그 폴더에 일본판 원본(헤더 없음, CRC32 D531289B)을 super-gokuden-jp.smc로 두고 zsh tools/build.sh를 실행하면 배포판과 같은 롬(CRC32 A939C4F8)이 나옵니다. Python 3(Pillow, numpy)과 kr-patch-toolkit이 필요합니다. 빌드는 이 순서로 돕니다.

    1. krpatch ips apply 일본판에 v1.0 IPS를 적용합니다.
    2. tools/title.py 타이틀 글자 층을 다시 그려 압축해 넣습니다.
    3. tools/text_patch1.py 대사와 그 밖의 텍스트를 뱅크마다 다시 채우고, 새 글자를 그리고, 메뉴 문자열을 고칩니다.
    4. tools/statusmenu1.py 스토리 현황 목록의 습득·완료 표시를 두 글자로 바꿉니다.
    5. tools/pwglyph1.py 암호 글자 다섯 자리를 다시 그리고 글자 표와 대조합니다.
    6. tools/endpw1.py 엔딩 암호 자리에 「손오공은사이어인」을 넣습니다.
    7. krpatch sfc checksum --fix 헤더 체크섬을 다시 계산합니다.
    8. krpatch ips make --headered 헤더 없는 롬용과 512바이트 헤더 롬용 IPS를 만듭니다.

    패치 소스와 일본판 원본만으로 빌드해 배포판과 같은 CRC32가 나오는 것을 확인했습니다. IPS가 무엇을 적는지는 0장 IPS 패치를 바이트로 읽기에 있습니다.

    파일하는 일설명
    tools/build.sh전체 빌드 순서(위 목록)11. 패치 소스 파일
    krpatch ips공통 도구. IPS 만들기와 적용11. 패치 소스 파일
    tools/dialogue1.py대사 포인터 표 읽기3. 대사
    tools/extract1.py일본어·한국어 대사 덤프(TSV)3. 대사
    tools/text_patch1.py텍스트 삽입, 뱅크별 재배치, 새 글자 그리기, 메뉴 문자열 수정3. 대사
    tools/kr1.py한국어 문장을 글자 코드로 바꾸기(「/」는 줄바꿈)3. 대사
    tools/layout1.py창 폭·페이지 검사3. 대사
    tools/width1.py글자 코드별 폭(px)3. 대사
    tools/lint1.py표기·말투 규칙 검사3. 대사
    tools/wintest1.py첫 나레이션을 줄 폭 시험 줄로 바꾼 롬 만들기3. 대사
    lua/wintest.luaMesen 2용. 새 게임에서 창 크기·줄 넘김 기록3. 대사
    tables/v1.1 텍스트, 메뉴 문자열, 일본어 원문과 v1.0 대조 덤프, 글자 코드표, 암호 글자표3. 대사
    tools/font1.py폰트 글자 읽기4. 글자
    tools/fontext1.py글자 추가 블록과 렌더러 후킹4. 글자 칸을 512개 늘리기
    tools/render1.py롬의 가변폭 폰트로 문장을 그림으로 그리기4. 글자
    tools/title.py타이틀 그래픽 생성5. 그림·압축
    krpatch lzss공통 도구. LZSS 압축과 해제5. 그림·압축
    tools/textsets.py전투 대사·선택지·장소 이름 등 그 밖의 포인터 표 정의6. 메뉴·이름·전투
    tools/statusmenu1.py스토리 현황 목록의 습득·완료 표시 루틴6. 메뉴·이름·전투
    tools/credits1.py엔딩 크레딧 줄 정렬6. 메뉴·이름·전투
    tools/pwglyph1.py암호 글자 다섯 자리 다시 그리기, 글자 표와 픽셀·암호 글자·이름 글자판 검사7. 세이브·암호
    tools/endpw1.py엔딩 암호 자리에 「손오공은사이어인」 넣기7. 세이브·암호
    lua/ending.luaMesen 2용. 엔딩 상태에서 이름을 바꿔 암호 화면 촬영7. 세이브·암호
    lua/eventlist.luaMesen 2용. 이벤트 표 문제 재현8. 이벤트 표에서 못 나오던 문제
    lua/scenario.luaMesen 2용. 디버그 시나리오 시작, A 연타, 대사 ID 기록, 촬영9. 확인 방법
    tools/pages1.py촬영한 화면에서 페이지별 완성 화면 고르기9. 확인 방법
    tools/rgcmp1.py두 롬의 시나리오별 대사 ID 순서 비교9. 확인 방법
    tools/textdump1.py모든 텍스트를 코드 목록으로 덤프(버전 간 비교)9. 확인 방법
    krpatch sfc dis공통 도구. 65c816 역어셈블러9. 확인 방법
    krpatch sfc run공통 도구. libretro 코어를 직접 불러 같은 입력을 넣는 작은 프로그램9. 확인 방법
    compat/run_all.sh코어마다 새 게임·이어하기 비교9. 확인 방법
    script.lua (krpatch sfc lua)Mesen 2에서 같은 입력 재생9. 확인 방법
    krpatch sfc checksum공통 도구. 헤더 체크섬 다시 계산2. 롬 지도
    docs/ANALYSIS.md롬 분석 문서(헤더·메모리·대사와 폰트 형식·후킹 주소·쓴 빈 공간)10. 다음 사람에게

    이 노트에서

    1. 이 노트에서
    2. 롬 지도
    3. 롬 헤더: 파일 0x7FC0의 64바이트
    4. 주소 읽는 법
    5. 게임기 안의 메모리
    6. 새로 쓴 빈 공간
    7. 대사
    8. 대사 번호에서 문자열까지
    9. 대사는 뱅크 밖으로 못 나간다
    10. 창 폭과 페이지
    11. 글자
    12. 폰트 칸이 모자랐다
    13. 글자 칸을 512개 늘리기
    14. v1.2에서 더 그린 글자
    15. 그림·압축
    16. 압축된 글자 층
    17. 롬에서 자료를 찾아가는 길
    18. 메뉴·이름·전투
    19. 한 칸에 눌러 그린 두 음절
    20. 엔딩 크레딧과 그 밖의 메뉴 문자열
    21. 세이브·암호
    22. 각성편으로 넘기는 암호
    23. 헷갈리던 암호 글자 다섯
    24. 게임 고유 문제
    25. 이벤트 표에서 못 나오던 문제
    26. 확인 방법
    27. 디버그 메뉴를 여는 두 바이트
    28. 대사 순서를 일본판과 맞대 보기
    29. 여러 에뮬레이터에서 같은 입력
    30. 다음 사람에게
    31. 확인하지 않은 것
    32. 알려진 한계
    33. 쓴 도구
    34. 패치 소스 파일

    스크린샷 미리 보기