본문으로 건너뛰기

롬 분석 노트 · 슈퍼 패미콤

롬 분석 노트: 슈퍼 마리오 RPG

슈퍼 마리오 RPG 한글패치 v1.3을 만들며 남긴 기록입니다. 개념은 각 절의 「가이드」 링크에서 처음부터 풀어 설명합니다.

이 롬을 다시 분석하거나 패치를 이어 고칠 사람을 위해 남기는 기록입니다. v1.0은 대사와 메뉴는 한글이었지만 아이템·필살기·적 이름은 MUSHROOM, HAMMER 같은 영문이었습니다. 롬 용량이 모자라서가 아니라 메뉴 화면에 한 번에 올려 둘 수 있는 글자가 256개뿐이었기 때문입니다. v1.1은 필요한 글자만 그때그때 빈 칸에 올리는 코드를 게임에 끼워 넣어 이 문제를 풀었고, v1.2는 v1.1이 놓친 전투 메시지 속 영문 이름과 글자 코드 충돌을 고쳤습니다. v1.3은 이름 입력을 자음과 모음으로 조합하는 화면으로 바꿨습니다. 롬 크기는 원본과 같은 4MB입니다.

위 v1.0, 아래 v1.1

1. 이 노트에서 쉽게

이 패치는 처음부터 새로 만든 것이 아닙니다. 일본판 롬에 한글패치 v1.0을 적용한 롬을 출발점으로 삼아 그 위에 고칠 곳만 덧씌웠습니다. 그래서 어디를 바꿨는지 말하려면 v1.0이 무엇을 남겼는지부터 봐야 합니다.

일본판에서 v1.3까지일본판 원본 위에 v1.0 IPS가 대사와 메뉴를 한글로 바꿨고 이름은 영문으로 두었다. v1.1이 이름을 한글로 바꿨고, v1.2가 전투 메시지와 이름 띠, 글자 코드 충돌을 고쳤고, v1.3이 이름 입력을 자모 조합식으로 바꿨다.일본판 원본4MB, SA-1 카트리지. 이 노트의 모든 주소는 여기 기준입니다.v1.0 (IPS)대사·메뉴는 한글, 아이템·필살기·적 이름은 영문v1.1이름 한글(아이템 165·필살기 27·적 232)v1.2전투 메시지·이름 띠, 글자 코드 충돌을 고침v1.3이름 입력을 자모 조합식 한 페이지 화면으로
버전마다 무엇을 바꿨나. 아래로 갈수록 앞 버전 위에 덧씌운 것입니다.

v1.0에서 이름만 영문이었던 이유는 롬 용량이 아니라 메뉴 화면이 글자를 다루는 방식이었습니다(4절). v1.1은 그 방식을 바꾸는 코드를 게임에 끼워 넣었고(8절), 이름이 대사에 들어가는 곳도 모두 다시 만들었습니다(3절). v1.2는 v1.1이 놓친 전투 메시지와 글자 충돌을 고쳤고, v1.3은 이름 입력을 자모로 조합하는 화면으로 바꿨습니다(8절). 네 버전 모두 롬 크기는 원본과 같은 4MB이고 세이브 구조도 그대로입니다.

먼저 읽으면 좋은 장

배경지식: 메모리, CPU 둘, 바이트 하나 바꾸기

CPU는 화면에 직접 그리지 않습니다. 그림 조각(타일)을 VRAM에 올려 두고 "이 칸에는 몇 번 타일"이라는 배치표(타일맵)를 적어 화면을 만듭니다. 글자도 타일입니다(7장 그림과 글자). 이 노트에 나오는 메모리는 넷입니다.

이름크기하는 일
롬4MB카트리지에 든 프로그램·대사·폰트. 읽기만 합니다.
WRAM128KBCPU가 계산하며 값을 적어 두는 곳. HP와 인벤토리도 여기 있습니다.
VRAM64KB화면에 붙일 8×8 그림 조각(타일)을 올려 두는 곳.
BW-RAM32KB카트리지 안의 두 번째 CPU(SA-1)가 쓰는 메모리. 세이브도 여기 있습니다.

이 게임 카트리지에는 SA-1이라는 CPU가 하나 더 들어 있습니다. 본체 CPU(S-CPU, 3.58MHz)보다 세 배쯤 빠르고, 두 CPU가 동시에 돕니다. 둘 다 65c816 계열이라 같은 어셈블리로 짭니다(3장 CPU와 65c816). 필드 메뉴는 S-CPU가, 전투 메뉴는 SA-1이 그려서 아이템 이름 하나를 바꾸려 해도 두 길을 따로 고쳐야 했습니다.

롬 해킹에서 가장 작은 단위의 수정은 바이트 하나를 바꾸는 것입니다. 전투 디버그 메뉴를 막는 코드(파일 0x106A9)를 예로 보겠습니다. 바이트를 명령으로 읽는 법은 5장 어셈블리 기초에 있습니다.

바이트어셈블리뜻
AF AE FF 00LDA $00FFAE$00:FFAE 값을 읽습니다. 주소는 거꾸로 적힙니다.
05 80ORA $80$80번지 값과 OR 합니다. 둘 다 0일 때만 0입니다.
F0 01BEQ +1결과가 0이면 다음 1바이트(RTS)를 건너뛰고 디버그 메뉴로 갑니다.
60RTS아니면 그냥 돌아갑니다. 제품판은 늘 여기로 옵니다.
  • F0 01BEQ: 0이면 건너뛰기
  • 80 01BRA: 늘 건너뛰기

F0 한 바이트를 80으로 바꾸면 전투 중 Start로 개발자용 디버그 메뉴가 열립니다(9절). 원래 코드 사이에 새 기능을 넣을 자리가 없을 때는 한 지점을 "새 코드로 가라"는 명령으로 덮어쓰고 새 코드는 롬의 빈 공간에 둡니다. 이것이 후킹이고(6장 후킹), 실제 패치 소스는 이렇게 생겼습니다.

org $C37990          ; 원래 아이템 이름을 그리러 가는 자리를
    JMP DrawItemName ; "DrawItemName으로 가라"로 덮어쓴다

org $C3F000          ; 원본에서 비어 있던(0xFF) 공간
DrawItemName:        ; 한글 이름을 그리는 새 코드
    ...

v1.1은 필드 메뉴, 전투 메뉴, 결과 창, 레벨업 화면, 대사 엔진 등 스무 곳 남짓에 이런 후킹을 걸었습니다.

2. 롬 지도 쉽게

롬은 64KB짜리 칸(뱅크) 64개입니다. CPU는 이 칸들을 $C0–$FF 번호로 봅니다. 아래 그림은 이 노트에 나오는 데이터가 어느 뱅크에 있는지 칠한 것입니다.

슈퍼 마리오 RPG 롬 지도뱅크 $C0부터 $FF까지 16개씩 네 줄. $C0–$C3은 게임 코드, $E2–$E4는 대사와 폰트, $F7은 대사 포인터 표, $F9·$FA는 이름과 전투 문구, $FE는 메뉴 문구다.0123456789ABCDEF$C0$D0$E0$F0한 칸 = 뱅크 하나(64KB). 빈 칸은 이 노트에서 다루지 않는 데이터
4MB 롬의 뱅크 64개. 파랑은 게임 코드, 주황은 대사와 폰트, 초록은 이름·포인터·문구 표
뱅크들어 있는 것
$C0대사 엔진. 대사 주소 계산 루틴 $C0:5800, 제어 코드 분기
$C1·$C2전투. 전투 메뉴 글자 그리기 $C1:6A3F, 레벨업 화면, 전투 애니메이션 해석기 $C2:0C43
$C3필드 메뉴. 아이템 이름 그리기 $C3:7990, 빈 공간 $C3:F000·$C3:FC40(패치 코드)
$E2–$E4대사 4096개(그룹 0–3은 $E2, 4–6은 $E3, 7은 $E4). $E2:B400부터는 빈 공간(새 이름 표)
$E4대사 뒤 $E4:2800 메뉴 폰트, $E4:4000 대사 폰트, $E4:9D00 대사 폰트 확장 칸
$F7대사 포인터 표 $F7:E000
$F9·$FA적 이름 $F9:8C11, 필살기 $FA:137F, 아이템 $FA:41FC, 아이템 설명·전투 메시지
$FE메뉴 문구 $FE:EF00

주소 읽는 법

0x22B400 같은 파일 오프셋은 롬 파일을 처음부터 센 위치이고, $E2:B400 같은 슈퍼 패미콤 주소는 CPU가 보는 뱅크:주소입니다(4장 주소와 뱅크). 이 게임은 다음처럼 바꿉니다.

파일 오프셋 = (뱅크 & 0x3F) × 0x10000 + 주소
$E2:B400 → 0x22 × 0x10000 + 0xB400 = 0x22B400
$C1:06AF → 0x01 × 0x10000 + 0x06AF = 0x0106AF

아래 계산기에서 매핑을 SA-1로 두고 「마리오 RPG 이름 표」를 누르면 같은 계산을 한 단계씩 볼 수 있습니다.

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
  • 롬이 아님
  • 지금 고른 뱅크

롬을 4MB보다 키우지 않은 이유

SA-1은 4MB 롬을 1MB짜리 창 4개로 CPU에 보여 줍니다. 4MB 너머를 읽으려면 실행 중에 창 하나를 다른 블록으로 돌려야 하는데, 두 CPU가 동시에 돌기 때문에 한쪽이 창을 돌린 순간 다른 쪽이 원래 데이터를 읽으려 하면 엉뚱한 값을 받습니다. 롬을 늘리는 일반적인 방법은 11장 롬 확장에 있습니다.

그래서 롬을 늘리지 않고, 일본판 원본에서도 0이나 0xFF로 비어 있던 공간만 찾아 썼습니다.

더 깊이두 CPU가 함께 보는 메모리

두 CPU는 메모리 일부를 함께 봅니다. S-CPU 주소 $00:3000–37FF는 SA-1 내부 RAM(I-RAM) 2KB이고, $00:6000–7FFF는 BW-RAM을 8KB씩 나눈 블록 하나입니다. 게임이 $C0:089C에서 레지스터 $2224에 01을 써 블록 1을 고르므로 $00:7000은 BW-RAM $3000, SA-1에서 보면 $40:3000입니다. Mesen 2로 필드와 전투에서 두 구간을 바이트 단위로 비교해 확인했습니다(I-RAM은 2048바이트 모두 같음).

지금 방 번호 $00:3030은 I-RAM $30이고, 이벤트 변수가 있는 $00:7000번대는 BW-RAM $3000번대입니다. 패치 변수는 메뉴·전투·대사 어디에서도 게임이 쓰지 않는 WRAM $7E:0C00–$17FF에 두었습니다.

3. 대사 쉽게

대사는 4096개이고 0x000–0xFFF 번호(ID)로 찾습니다. 대사마다 길이가 달라서 게임은 번호로 시작 위치를 찾는 표(포인터 표)를 씁니다. 이 게임은 그 표를 두 단계로 따라갑니다. 실제 v1.1 롬에서 대사 $204를 찾아가 보겠습니다.

대사 포인터 따라가기대사 $204를 512로 나누면 그룹 1이다. 뱅크 $E2 머리말 08 00 F5 03 15 33 6D 67에서 그룹 1의 시작 $03F5를 읽는다. 포인터 표 $F7:E408에는 25 00이 있다. $E2:03F5에 $25를 더한 $E2:041A에 문자열 1B 07 20 1D 07 1D 22 21 00이 있다.대사 번호$204512개씩 묶은 그룹 번호$204 >> 9 = 1뱅크 $E2 머리말($E2:0000)08 00크기 = 그룹 0F5 03그룹 115 33그룹 26D 67그룹 3포인터 $F7:E000 + $204 × 2 = $F7:E40825 00 → $0025그룹 시작에서몇 바이트 떨어졌나$E2:03F5 + $25 = $E2:041A파일 0x22041A: 1B 07 20 1D 07 1D 22 21 00
대사 $204 찾아가기: 번호 → 그룹 → 뱅크 머리말 → 포인터 → 문자열 (v1.1 롬)

포인터 25 00은 작은 자리가 먼저라 $0025입니다(2장 엔디안). 이 값은 주소가 아니라 "그룹 시작에서 몇 바이트"라서, 그룹 시작 $03F5를 더해야 진짜 위치가 됩니다. 그룹 0–3은 뱅크 $E2, 4–6은 $E3, 7은 $E4에 있고, 대사를 다른 뱅크로 옮기거나 그룹 순서를 바꿀 수는 없습니다.

바이트를 글자로 풀기

찾아간 자리의 바이트를 글자표로 읽으면 이렇습니다. 1바이트 글자와 1D–1F로 시작하는 2바이트 글자가 섞여 있고(1바이트 글자, 2바이트 글자), 글자가 아닌 명령(제어 코드)도 끼어 있습니다.

  • 1B 07이름 넣기
  • 20빈칸
  • 1D 07획
  • 1D 22득
  • 21!
  • 00끝

화면에는 「버섯」을 획득!처럼 나옵니다. 이름은 바이트에 없고 실행 중에 채워집니다. 같은 대사가 v1.0에서는 09 3B 1A 3C 8F(탭 3칸, 「, 이름, 」, 을)로 시작했고, v1.1은 이 다섯 바이트를 1B 07 두 바이트로 바꿨습니다.

코드뜻
00대사 끝
01줄바꿈
02–04창 넘김
08 / 09 / 0A탭 2 / 3 / 4칸
1A여기에 아이템 이름
1B원판에서는 아무 일도 안 함(v1.1이 이름 넣기로 씀)
1C xx숫자 넣기
1D–1F xx2바이트 글자
20–FF1바이트 글자

대사 속 아이템 이름

v1.0은 대사에 『MUSHROOM』을 얻었습니다처럼 영문 이름을 직접 적었습니다. v1.1은 원판에서 아무 일도 안 하던 1B를 1A(아이템 이름 넣기)와 같은 처리로 보내고, 뒤에 붙는 1바이트로 「」 감싸기, 가운데 정렬, 을/를 붙이기를 고르게 했습니다. 1B 07의 07은 아래 세 비트를 더한 값입니다. 이름 길이는 실행 중에야 정해지므로 가운데 정렬도 실행 중에 합니다.

비트하는 일
1이름을 「」로 감쌉니다.
2줄의 나머지 글자 수를 미리 세어 앞에 빈칸을 채워 가운데에 놓습니다.
4이름 마지막 글자의 받침에 맞춰 을/를을 붙입니다.
노란 선이 화면 가운데. 위부터 일본판, v1.0, v1.1

영문 이름을 빼고 한글 이름을 넣은 뒤에는 그룹 순서를 지킨 채 뱅크마다 문자열을 처음부터 다시 채우고, 머리말과 포인터 표를 새로 만들었습니다. 여러 ID가 같은 주소를 가리키면 한 번만 쓰고 포인터를 모두 고쳤습니다. 한글 이름이 짧고 아래의 쓰레기 바이트도 빠져서 $E2는 7,584바이트, $E3는 9,136바이트, $E4는 2,133바이트가 오히려 줄었습니다. 뱅크 $E4는 대사 바로 뒤 $E4:2800부터 메뉴 폰트라 넘치면 안 됩니다.

더 깊이v1.0이 남긴 끝 코드 문제와 대사 밖의 텍스트

대사를 다시 채우다 v1.0이 남긴 문제도 찾았습니다. v1.0이 원문 위에 덮어쓰면서 끝 코드(00) 뒤에 화면에 안 나오는 원문 조각이 남아 있었고, 다시 채울 때 버렸습니다. 끝 코드가 너무 일찍 들어가 비어 버린 대사도 있었습니다. ID 498(개구리 코인 획득 메시지)과 8B4(피치의 「쉿」)는 비어 있었고, E85는 두 줄 중 첫 줄 뒤에서 끊겼습니다. 일본판과 대조해 복구했습니다.

같은 인코딩을 쓰는 텍스트가 대사 밖에도 있습니다. 메뉴 문구 영역에는 포인터가 가리키지 않는 데이터가 섞여 있어 문자열을 옮기지 않고 제자리에서 고쳤습니다. 길어진 문자열만 뒤의 0 바이트를 쓰거나 남는 공간($FE:F61C–$FE:F6FF)으로 옮기고 포인터를 고쳤습니다. 전투 메시지와 「무슨 생각하니」는 옮길 곳이 없어 문자열 뒤의 0 바이트 안에서만 늘렸습니다.

내용포인터 표개수처리
전투 대사$F9:6554256다시 채움
아이템 설명$FA:2F20177다시 채움
메뉴 문구$FE:EF00118제자리 수정
전투 메시지$FA:26F146제자리 수정
「무슨 생각하니」$F9:9411256제자리 수정

대사 글자 한 칸과 탭 단위는 모두 10픽셀이고 한 줄은 22칸을 기준으로 맞췄습니다. 탭 코드는 하나마다 글자 출력이 약 30프레임 늦어져서, 가운데 정렬은 탭 코드 하나와 빈칸으로 맞춥니다.

4. 글자 쉽게

이 게임에는 폰트가 둘입니다. 아이템·메뉴 화면에 쓰는 메뉴 폰트($E4:2800)와 대사 창에 쓰는 대사 폰트($E4:4000)입니다. 둘 다 글자 하나가 가로 8, 세로 12 픽셀이고 한 줄에 2바이트씩 24바이트를 씁니다. 아래는 실제 롬에서 꺼낸 메뉴 폰트의 「마」입니다.

글자 코드에서 그림 위치를 구하는 식은 단순합니다. 메뉴 폰트는 1바이트 코드 c의 그림이 $E4:2B00 + (c − 0x20) × 24에 있습니다. 대사 폰트는 1바이트 코드면 c − 0x20번, 2바이트 코드 p l이면 0xE0 + (p − 0x1D) × 0x100 + l번 그림입니다. 위 대사의 「획」(1D 07)을 따라가면 이렇습니다.

  • 1D묶음 0
  • 07묶음 안 번호
  • 0xE70xE0 + 0 + 7
  • $E4:55A8$E4:4000 + 0xE7 × 24

왜 v1.0은 이름을 영문으로 뒀나

아이템 이름은 원래 글자 하나를 1바이트 코드로 적습니다. 1바이트로 고를 수 있는 값은 256개이고 0x00–0x1F를 빼면 글자 칸은 224개입니다. 메뉴를 열면 게임은 메뉴 폰트 전체에 외곽선을 붙여 VRAM에 통째로 올려 두고, 코드 c를 (c − 0x20) × 2번 타일로 고정해서 그립니다. 아래는 v1.0 롬의 메뉴 폰트 256칸입니다. 회색은 빈 코드, 노란색은 아이콘·영숫자, 파란색은 v1.0이 넣은 한글 165자입니다.

빈칸이 없습니다. 이름을 한글로 쓰려면 음절이 226자 필요했는데 이 표에 있는 것은 88자뿐이었습니다. 138자가 들어갈 칸이 없었던 것입니다. 롬을 키워도 칸 수는 늘지 않으니 병목은 용량이 아니라 "1바이트 코드 = 화면에 상주하는 256칸"이라는 구조였습니다. v1.1이 이 구조를 어떻게 우회했는지는 8절에 있습니다.

대사 폰트에 없는 21자

대사 폰트는 1바이트와 2바이트 코드를 섞어 992자를 쓰는데, 2바이트 코드 768개가 v1.0에서 전부 차 있었습니다. 이름에 필요한 캔, 깃, 링, 쑥, 츠 같은 21자는 어떤 텍스트에서도 쓰지 않는 코드를 찾아 그 자리에 새로 그렸습니다. 처음에는 대사만 보고 골랐다가 메뉴 튜토리얼의 「봅」이 깨졌습니다. 같은 인코딩이 전투 대사, 아이템 설명, 메뉴 문구 등에도 쓰였기 때문입니다. 지금은 모든 영역을 검사해서, 쓰이는 코드를 재활용하려 하면 빌드가 멈춥니다.

새 글자는 Galmuri11 Condensed를 12픽셀로 그려 8×12로 만들었습니다. 대사 폰트 마지막 그림(0x3DF) 바로 뒤 $E4:9D00도 0으로 비어 있어 32자 자리를 잡았지만(v1.1이 솟, 쑤, 웨, 휠, 릉 5자 사용), 대사 엔진은 2바이트 접두가 1D–1F뿐이라 이 자리를 못 읽습니다. 레벨업 화면과 v1.2의 전투 메시지처럼 패치 코드가 접두 20을 따로 처리하는 곳만 씁니다.

더 깊이v1.2가 고친 두 가지 글자 문제

대사 폰트 코드 충돌

다시 그린 21개 중 1F44와 1FA0은 원래 「힐」과 「콰」 자리였습니다. v1.0 텍스트에는 두 글자가 없어 재활용 대상이 됐지만, v1.1이 새로 넣은 이름 「힐셸」과 「천둥콰르릉」이 그 코드를 원래 뜻대로 써서 결과 창·레벨업 화면 등에서 「캡셸」, 「천둥잣르릉」으로 나왔습니다. 빈 코드가 없었는데, 빌드된 롬의 모든 텍스트를 다시 훑으니 1F7F(줏)와 1FA9(궈) 두 개가 비어 있었습니다. v1.1이 「줏어 → 주워」, 「잠궈 → 잠가」로 맞춤법을 고치면서 더는 쓰이지 않게 된 자리입니다. 「콰」를 1F7F, 「힐」을 1FA9에 그렸고, 이제 이름의 음절이 재활용한 코드로 가거나 줏·궈가 새 텍스트에 다시 나타나면 빌드가 멈춥니다.

같은 때 해피셸·파이어셸·힐셸을 「레이지쉘」에 맞춰 「쉘」로 통일했습니다. 셸은 더 쓰이지 않지만 1F13 자리는 그대로 두었고, 재활용 코드 배정은 코드별로 고정해 필요 없어진 음절이 생겨도 다른 음절의 코드가 밀리지 않게 했습니다.

위 v1.1, 아래 v1.2

메뉴 폰트 코드표가 틀린 두 줄

v1.0의 메뉴 폰트 코드표(korTBL2)에는 실제 그림과 다른 줄이 두 개 있었습니다. 5F=힐인데 0x5F 그림은 점(●)이고, FF=휠인데 0xFF 그림은 「빼」입니다. v1.1은 이 표를 그대로 믿었습니다. v1.1 롬의 이름 표에서 힐셸(아이템 0x43)을 꺼내 보면 첫 글자가 정말 5F 00입니다.

  • 3C아이콘
  • 5F 00메뉴 폰트 ●
  • 33 80임시 칸 셸
  • 20 00 …빈칸

그래서 메뉴에서 「힐셸」이 「●셸」로, 「지노휠」이 「지노빼」로 나왔습니다. 대사 폰트 쪽 대조는 맞았기 때문에 메뉴에서만 보이던 문제입니다. v1.2는 두 줄을 표에서 빼고 힐과 휠을 임시 칸 글자로 그립니다. 메뉴 폰트의 한글 165자는 코드표 글자 옆에 그림을 그려 전부 눈으로 대조했고, 틀린 것은 이 둘뿐이었습니다.

빌드할 때는 아이템·필살기·적 이름 414개를 메뉴가 그리는 글자로 하나씩 대조합니다. 임시 칸 글자는 롬 안의 그림이 Galmuri로 다시 그린 것과 같아야 하고, 메뉴 폰트 글자는 눈으로 확인한 목록에 같은 글자·같은 그림 해시로 있어야 합니다. 하나라도 다르면 빌드가 멈춥니다. Galmuri와 v1.0 메뉴 폰트는 디자인이 달라 그림끼리 닮은 정도로 판정하면 165자 중 89자 이상을 잘못 골랐기 때문에 이렇게 했습니다.

메뉴 폰트에 직접 넣은 세 글자

필드 필살기 화면의 안내 문구는 임시 칸 글자를 쓸 수 없는 곳이라(6절), 메뉴 문구와 이름 어디에도 쓰지 않는 메뉴 폰트 코드 85, 9B, DF를 되, 운, 웁으로 바꿨습니다.

5. 그림·압축 쉽게

이 게임에는 없음 — v1.1과 v1.2는 타이틀 로고 같은 그림을 바꾸지 않아서 압축된 그림을 풀 일이 없었습니다. 새로 그린 그림은 폰트 글자뿐이고, 글자는 압축되지 않은 8×12 2bpp 그대로 롬에 들어갑니다(4절). 압축된 그림을 다루는 방법은 9장 압축 ① RLE·LZSS에 있습니다.

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

아이템·필살기·적 이름은 대사와 따로 표에 있습니다. 원판 표(아이템 $FA:41FC, 필살기 $FA:137F, 적 $F9:8C11)는 이름 칸이 글자마다 1바이트입니다. v1.0은 이 표에 영문 대문자를 넣었습니다. 버섯(아이템 0x60)을 실제 롬에서 꺼내 보면 이렇습니다.

v1.0 롬, 원판 표 $FA:41FC + 0x60 × 15: 아이콘 1 + 이름 10 + 미사용 4

  • 2D아이콘
  • 4DM
  • 55U
  • 53S
  • 48H
  • 52R
  • 4FO
  • 4FO
  • 4DM

v1.1은 원판 표를 그대로 두고 $E2:B400의 빈 공간에 새 이름 표(KoNames)를 만들었습니다. 글자마다 2바이트라 메뉴 폰트에 없는 글자도 가리킬 수 있습니다. 0x00xx는 메뉴 폰트 글자 그대로, 0x8000 | n은 롬에 따로 넣은 글자 249자 중 n번째입니다.

v1.1 롬, 새 표 $E2:B400 + 0x60 × 21: 아이콘 1 + 2바이트 글자 10

  • 2D아이콘
  • A0 00메뉴 폰트 「버」
  • 4F 80임시 칸 0x4F 「섯」
  • 20 00 …빈칸 8개

「버」는 v1.0 메뉴 폰트에 있어서 원래 코드 A0을, 「섯」은 없어서 따로 넣은 글자 0x4F번을 가리킵니다. 2바이트는 작은 자리가 먼저라 4F 80이 0x804F입니다. 게임이 이 새 표를 읽게 하려고 이름을 그리는 곳마다 후킹했습니다. 메뉴 폰트에 없는 글자를 화면에 띄우는 방법은 필드 메뉴와 전투 메뉴가 서로 달라서 8절에서 따로 다룹니다.

표크기쓰는 곳
KoNames · KoSkills256 · 64 × 21바이트필드·전투 메뉴의 아이템·필살기 이름(아이콘 + 2바이트 글자 10개)
KoEnemies256 × 16바이트전투 메뉴의 적 이름(2바이트 글자 8개)
KoDlgNames256 × 22바이트대사·결과 창 속 이름. 대사 인코딩으로 적고 앞 1바이트가 길이와 받침 비트
KoLvSkills64 × 16바이트레벨업 화면과 전투 메시지의 필살기 이름

이름은 화면 칸이 모자라 띄어쓰기를 빼고 넣었습니다. 이름 길이 한계는 원판 칸 그대로 아이템·필살기 10글자, 적 8글자입니다.

조사 자동 선택

"레벨3(이)가", "(을)를 배웠습니다" 같은 표기는 이름이 실행 중에 정해져서 생깁니다. v1.1은 빌드할 때 이름 마지막 글자에 받침이 있는지 계산해 이름 앞 1바이트에 표시(0x80)해 두고, 게임은 그 비트만 보고 을(8F)과 를(76)을 고릅니다.

받침 번호 = (글자 코드 − 0xAC00) % 28   ; 0이면 받침 없음
버섯 → 「섯」 받침 있음 → 버섯을
회복샤워 → 「워」 받침 없음 → 회복샤워를

숫자는 읽는 소리로 정했습니다. 레벨 숫자의 마지막 자리가 2, 4, 5, 9면 「가」, 나머지는 「이」입니다(10, 20…은 「십」이라 「이」). 빌드할 때 이름을 바꾸면서 바로 뒤 조사(이/가, 을/를, 은/는, 과/와, 아/야, 으로/로)도 같이 고쳤습니다.

더 깊이레벨업 화면 후킹

레벨업 화면은 SA-1이 그리고, 문장 속 코드 03이 필살기 이름을 넣습니다. 원래는 아이템 형식 표(v1.0은 영문)에서 이름을 가져왔습니다. $C2:D3CF(필살기 이름 → KoLvSkill, KoLvSkills에서 읽고 을/를을 붙임), $C2:D4EA(숫자를 그린 직후 → KoLvAfterNum, 「(이)가」면 하나만 그림), $C2:D734(글자 종류 해석 → KoLvAttr) 세 곳을 후킹했습니다.

2바이트 글자는 접두 바이트(1D–20)를 종류 바이트에 넣고 KoLvAttr가 그림 번호로 되돌립니다. 접두 20이 대사 폰트 확장 칸입니다. 원래 문장 「(을)를 배웠습니다.」는 「배웠습니다.」로 바꿨습니다.

원판부터 어색했던 화면

전투 결과 창은 원판이 아이템 이름을 늘 「아이템」 다음 줄 왼쪽에 둡니다. v1.1은 숫자 끝(오른쪽 180픽셀)에 맞춰 오른쪽 정렬하고, 5글자 이하면 「아이템」과 같은 줄에 두며 창을 한 줄 줄였습니다. 필살기 화면 안내는 이름을 4칸 고정으로 그려 「マロ  が…」처럼 빈칸이 생기던 것을 이름 바로 뒤로 당겨 「마로는 / 레벨 N이 되면 / 새로운 기술을 배웁니다」로 바꿨습니다.

위 일본판, 아래 v1.1
위 일본판, 아래 v1.1
더 깊이결과 창과 필살기 안내의 후킹 자리

결과 창의 이름은 $7E:05E0에 복사된 뒤 대사 폰트로 그려집니다. $C1:779A(이름 복사 → KoResultName), $C1:787E(글자 읽기 → KoResultChar, 2바이트 글자를 그림 하나로), $C1:7161(창 크기 선택 → KoResultGeom)을 후킹했습니다. 배치는 넷입니다: 원판 큰 창, 아이템 없음(원판 작은 창), 이름 5글자 이하(같은 줄, 한 줄 작은 창), 6글자 이상(다음 줄, 원판 큰 창). asar에 -Dres_ui=0을 주면 원판 배치로 빌드돼 전후 비교 스크린샷을 찍을 수 있습니다.

필살기 안내는 문구 목록 $C3:33B5의 첫 바이트를 1로 바꿔 메뉴 문구 $1C만 그리게 하고, 나머지 줄은 패치 루틴($C3:77F6 → KoSpecialHint)이 그립니다. 레벨 숫자는 둘째 줄로 옮겼고($C3:35F0), 메뉴 문구 $1C는 「는」으로 바꿨습니다. 이 문구가 그려지는 레이어는 임시 칸 글자를 쓸 수 없어서 필요한 글자를 메뉴 폰트에 직접 넣었습니다(4절).

7. 세이브·암호 쉽게

이 게임은 암호 없이 카트리지 안의 BW-RAM(32KB, 배터리로 유지)에 저장합니다. 한글패치는 세이브 구조와 헤더의 RAM 크기를 바꾸지 않아서 v1.0·v1.1 세이브를 그대로 이어 쓸 수 있습니다. 에뮬레이터 호환성을 확인할 때(9절) 세이브에 요시쿠키를 넣으려고 한 슬롯의 모양을 확인했습니다.

세이브 슬롯과 체크섬슬롯 1은 BW-RAM $0000부터 $07FF까지다. $07FC의 16비트 값이 $0000–$07FB 바이트 합의 보수이고, 맞지 않으면 그 슬롯은 빈 칸으로 나온다.게임 데이터 $0000–$07FB$07FC$07FC(16비트) = 앞 바이트를 모두 더한 값의 보수맞지 않으면 그 슬롯은 빈 칸으로 나옵니다
세이브 슬롯 1 (BW-RAM $0000–$07FF)

그래서 세이브를 고친 뒤에는 체크섬을 다시 계산해야 합니다. 롬 헤더의 체크섬(0x7FDC–0x7FDF)도 같은 생각으로, 롬을 고친 뒤 asar의 --fix-checksum=on으로 다시 계산했습니다(v1.0 C88A → v1.1 6FCD → v1.2 2B3C). 헤더 체크섬은 11장 롬 확장에서 다룹니다.

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

이 게임만의 문제는 4절의 "화면에 상주하는 256칸"을 넘어 메뉴 폰트에 없는 글자를 띄우는 일이었습니다. 게다가 같은 이름 표를 두 CPU가 서로 다른 방식으로 그립니다.

필드 메뉴와 전투 메뉴의 글자 그리기새 이름 표에서 읽은 글자를 필드 메뉴는 S-CPU가 VRAM 타일로 그린다. 없는 글자는 임시 칸에 올리고 NMI 때 DMA로 옮긴다. 전투 메뉴는 SA-1이 BW-RAM 비트맵에 글자 그림을 직접 찍고, 그림 주소는 $E4:2B00에 16비트를 더해 구한다.새 이름 표: 글자마다 2바이트필드 메뉴(S-CPU)전투 메뉴(SA-1)VRAM 타일로 그림없는 글자 → 임시 칸NMI 때 DMA로 올림BW-RAM 비트맵에글자 그림을 직접 찍음$E4:2B00 + 16비트두 길을 각각 후킹해야 했습니다
같은 이름 표, 다른 두 길. 필드 메뉴는 타일을, 전투 메뉴는 비트맵을 씁니다.

필요한 글자만 올리는 임시 칸

필드 메뉴의 VRAM을 덤프해 보니 BG1 타일 $200–$2FF(VRAM $6000–$7FFF)는 이 화면에서 아무도 쓰지 않았습니다. 메뉴를 열 때마다 원래 내용을 다시 올리므로 잠깐 빌려 써도 뒤탈이 없습니다. 글자 하나가 타일 2장이니 128글자가 들어갑니다.

이름을 그리는 순서는 이렇습니다.

  1. 이름에서 글자 하나(2바이트)를 읽습니다.
  2. 메뉴 폰트에 있는 글자(0x00xx)면 원래 타일 (c − 0x20) × 2를 그대로 씁니다.
  3. 없는 글자(0x8000 | n)면 이미 올려 둔 임시 칸이 있는지 찾고, 없으면 빈 칸을 하나 배정합니다.
  4. 게임의 외곽선 루틴($C3:6869)으로 8×12 글자를 메뉴 글자 모양으로 바꿔 WRAM에 모아 둡니다(최대 12자).
  5. 화면 갱신 틈(NMI)에 DMA로 VRAM에 한꺼번에 옮깁니다.

마지막 단계가 바로 복사가 아닌 이유가 있습니다. 화면을 그리는 동안에는 CPU가 VRAM을 건드릴 수 없고, 한 화면을 다 그리고 다음 화면을 시작하기 전 짧은 틈(VBlank)에만 쓸 수 있습니다. 그 틈이 시작될 때 걸리는 신호가 NMI입니다. 그래서 미리 준비해 둔 글자를 NMI 때 DMA(CPU 대신 데이터를 빠르게 옮기는 장치)로 옮깁니다(7장 그림과 글자).

더 깊이만들며 밟은 지뢰와 들고 가는 이름
  • 처음에는 게임의 "NMI 때 할 일" 목록(8칸)에 업로드를 등록했습니다. 그런데 목록이 꽉 차면 등록 루틴이 사운드 명령을 보내고 등록을 포기해서, 토드의 메뉴 튜토리얼에서 음악이 바뀌었습니다. 결국 NMI 루틴 자체($C3:0102)를 후킹했습니다.
  • 패치 변수를 평소 0인 WRAM $7E:F580 근처에 뒀는데, 아이템을 캐릭터에게 가져갈 때 이름을 그리는 스프라이트 버퍼였습니다. "평소에 0"이 "아무도 안 쓴다"는 뜻은 아니었습니다.
  • 그 들고 가는 이름 스프라이트는 $7E:8000 + (c − 0x20) × 32에서 글자를 읽습니다. 코드 00–1F가 $7E:9C00–$9FFF를 가리키므로, 임시 칸 글자는 거기에 바꿔 두고 00번부터 씁니다($C3:2078 → HeldName).

전투 메뉴는 SA-1이 따로 그린다

전투 메뉴는 타일 방식이 아니라 SA-1이 BW-RAM에 비트맵을 만들고 글자를 직접 찍습니다($C1:6A3F). 같은 이름 표를 쓰되 글자 읽기($C1:6AC7), 그림 주소 계산($C1:6AFA), 이름 복사($C1:6C6D) 세 곳을 후킹했습니다. 그림 주소를 $E4:2B00에 16비트 값을 더해 구하기 때문에 새 글자는 거기서 64KB 안(파일 0x242B00–0x252AFF)에만 둘 수 있어서, 그 범위의 빈 구간 네 곳에 나눠 넣었습니다.

위 v1.0, 아래 v1.1

v1.2: 전투 메시지와 이름 띠에 남은 영문

v1.1 뒤에도 영문 이름이 남은 곳이 둘 있었습니다. 요시쿠키로 몬스터가 아이템으로 바뀌면 「몬스터는 MUSHROOM 모습이 됐다!」가 나왔고, 전투 이름 띠를 강제로 띄우면 「YOSICOOKIE」, 「CRUSH」가 나왔습니다. 둘 다 원판 이름 표(v1.0이 영문을 넣은 표)를 바로 읽는 코드였습니다. v1.2는 두 코드를 한글 이름 표를 읽게 바꿨습니다.

위 v1.1, 아래 v1.2
더 깊이전투 메시지 속 이름과 이름 띠를 고친 방법

전투 메시지

화면 위에 나오는 전투 메시지는 메뉴와 달리 대사 폰트로 그립니다. 메시지 안의 제어 코드 1C 31은 「여기에 아이템 이름」, 1C 37은 「여기에 필살기 이름」이고($C1:5683의 분기 표), 두 코드는 원판 이름 표($FA:41FC, $FA:137F)를 바로 읽습니다. v1.2는 두 처리 루틴($C1:56A8, $C1:56DF)을 그 자리에서 바꿔, 대사 인코딩 한글 이름(아이템은 KoDlgNames, 아군 필살기는 KoLvSkills)을 한 글자씩 게임의 글자 루틴($C1:5769)에 넘깁니다. 적 공격 이름은 v1.0부터 한글이라 원래 표를 그대로 읽습니다.

전투 이름 띠

적이 마법을 쓰면 화면 위 띠에 그 이름이 나옵니다. 띠는 전투 애니메이션 명령 63 xx(처리 루틴 $C2:4131)이 띄우고, xx가 0이면 적 공격 이름, 1이면 필살기·마법 이름, 2면 아이템 이름입니다. v1.2는 1과 2($C2:41D9, $C2:420D)를 후킹해, 띠 문자열을 WRAM에 만든 짧은 글(빈칸, 1C 38 A5, 종류, 번호)로 바꿉니다. 띠 글자는 전투 메시지와 같은 해석기로 그리는데, 이 해석기는 $20을 빈칸으로 읽어서 대사 폰트 확장 칸의 글자(솟, 쑤, 웨, 휠, 릉)를 못 그립니다. 그래서 비어 있던 코드 1C 38을 새 처리로 보내 위의 이름 루틴을 거치게 했고, 확장 글자도 그려집니다. 적 마법(번호 0x1B 이상)은 원래 표를 그대로 씁니다.

위 v1.1, 아래 v1.2. 띠를 띄우려고 적의 행동을 바꾼 테스트 롬에서 찍었습니다.

평소 플레이에서 나오는가

몬스터 행동 스크립트($F9:30AA의 포인터 표)에서 마법 명령(EF, F0)의 번호는 모두 0x40 이상이라 아군 필살기 번호가 없고, 행동 종류를 아이템으로 만드는 명령(E1)은 한 번도 쓰이지 않습니다. 애니메이션 데이터에서도 63 01은 적 마법이 쓰는 $F5:252C 한 곳뿐이고, 명령 앞뒤 꼴로 찾은 63 02는 없었습니다. 그래서 영문 띠는 평소에는 나오지 않는 것으로 보고, 바로잡는 김에 함께 고쳤습니다.

확인

테스트 롬에서 굼바와 가시돌이가 마법을 쓰게 바꾸고, Mesen 2에서 띠 명령이 번호를 읽는 순간 번호를 바꿔 아군 필살기 27개와 아이템 165개를 모두 띄웠습니다. 그려지는 글자 코드를 기록해 이름과 대조하니 모두 같았습니다. 처음 돌렸을 때는 「힐셸」과 「천둥콰르릉」이 「캡셸」, 「천둥잣르릉」으로 나왔는데, 띠가 아니라 4절의 대사 폰트 코드 충돌이었습니다.

새 코드는 $E2:FDE2–$E2:FF6C와 $C1:56BB–56BE, $C1:56F2–56F5(원래 루틴이 남긴 자리)에, 변수와 띠 문자열은 패치 변수 영역 안의 WRAM $7E:1100–$7E:112E에 두었습니다.

v1.3: 자모로 조합하는 이름 입력

v1.2까지의 이름 입력은 메뉴 폰트에 있는 음절 160자를 두 페이지에 늘어놓고 고르는 화면이었습니다. 일본판의 히라가나 페이지와 가타카나 페이지 자리에 음절을 나눠 담은 것이라, 원하는 글자를 찾기 어렵고 폰트에 없는 음절은 이름에 쓸 수 없었습니다. v1.3은 이 화면을 자음과 모음을 골라 음절을 만드는 한 페이지로 바꿨습니다. 자판은 포켓몬스터 DS·3DS 한국어판처럼 두벌식 배열을 격자에 옮기고, 쌍자음은 Shift 자리인 윗줄에 두었습니다.

위 v1.2, 아래 v1.3

이름은 세이브에 8바이트로 남습니다. 메뉴 폰트에 있는 음절은 예전처럼 1바이트로 적어 이전 세이브의 이름과 같게 두고, 폰트에 없는 음절과 자모만 2바이트로 적었습니다. 그런 글자는 완성형 2,350자를 1bpp로 그려 롬의 빈 곳에 넣고, 이름이 보이는 두 화면(이름 입력과 파일 선택)에서 임시 칸에 올려 그립니다.

더 깊이조합 규칙과 두 화면의 임시 칸
  • 지금 만드는 음절은 넣은 자모마다 상태(초성, 중성, 종성)를 하나씩 쌓아 둡니다. B 버튼은 이 상태를 하나 꺼내므로 「각 → 가 → ㄱ」처럼 자모 단위로 지워집니다.
  • 음절 번호 (초성 × 21 + 중성) × 28 + 종성을 정렬한 2,350개 표에서 이진 탐색합니다. 모음이 붙어 표에 없는 음절이 되면(쌰) 효과음으로 거절하고, 받침이 붙어 없는 음절이 되면(똠) 앞 음절을 확정하고 새 초성으로 시작합니다.
  • 이름 입력 화면의 BG2는 VRAM $8000부터만 써서 필드 메뉴와 같은 임시 칸($6000–$7FFF)을 쓸 수 있었습니다. 파일 선택 화면은 이름을 BG3에 2bpp로 그리고 BG1이 그 칸을 배관 그림에 써서, BG3 메뉴 폰트 바로 뒤의 16글자 자리(타일 $1C0–$1DF)를 따로 잡았습니다.
  • 그런데 그 자리는 파일 선택 화면이 참조하지 않을 뿐, 이름 입력 창의 테두리 타일이 들어 있었습니다. 새 게임으로 이름 입력에 들어갈 때 게임은 이 타일을 다시 올리지 않아서, 세이브가 있어 파일 선택에 이름을 그린 뒤에만 창 테두리가 글자 조각으로 깨졌습니다. 그래서 파일 선택이 첫 칸을 올리기 직전 NMI에서 그 구간 512바이트를 WRAM에 읽어 두고, 이름 입력 화면을 열 때 되돌립니다. VRAM 읽기 버퍼는 주소가 오르기 전에 다시 채워지므로, 주소를 정한 뒤 한 번 읽어 버려야 첫 워드가 두 번 들어오지 않습니다.
  • 고른 키가 이름 칸으로 날아가는 연출은 원판 루틴($C3:434F)에 스프라이트 타일만 채워 넘겨 그대로 씁니다. 착지할 때($C3:452D) 이름을 그립니다.
  • korTBL2 표의 문장 부호 줄은 실제 그림과 달라서($21은 아이콘), 숫자·기호 칸은 그림을 보고 고른 코드를 그대로 적었습니다.

9. 확인 방법 직접 해 보기

고친 화면을 보려고 매번 게임을 처음부터 할 수는 없습니다. 그래서 에뮬레이터 Mesen 2에 Lua 스크립트를 붙여 보고 싶은 장면을 바로 만들었습니다(12장 에뮬레이터로 확인).

  • 세이브스테이트에서 버튼 입력을 재생하고, 원하는 순간의 화면과 메모리를 저장합니다.
  • 인벤토리에 아이템을 직접 넣거나 경험치를 레벨업 직전으로 바꿔 보고 싶은 화면을 바로 만듭니다.
  • 상점 주인 대사 자리에 원하는 대사를 띄우는 테스트 전용 롬으로 v1.0과 v1.1을 같은 조건에서 찍습니다.
  • 고치기로 한 목록 밖에서 바뀐 대사가 0건인지, 메뉴 화면이 기준과 픽셀 단위로 같은지 비교합니다.

테스트에 가장 많이 쓴 메모리 주소입니다. 방 번호와 전투 편성은 미리 써 두면 게임이 덮어쓰므로, Mesen 2의 쓰기 콜백에서 게임이 쓰려는 값을 바꿔치기합니다. 나머지는 값을 써 두기만 하면 됩니다.

주소쓰는 법
$00:3030지금 방 번호(16비트). 출구를 지날 때 쓰는 값을 바꾸면 그 방으로 갑니다. 0x0000은 디버그 룸입니다.
$7E:FA0E전투 편성 번호(16비트). 전투에 들어갈 때 쓰는 값을 바꾸면 다른 적과 싸웁니다.
$00:3112FF로 쓰면 메뉴가 막힌 곳에서도 메뉴가 열립니다.
$7F:F882아이템 인벤토리 30칸(빈칸 FF). 보고 싶은 아이템을 직접 넣습니다.
$7F:F800마리오 능력치(0x14바이트). +0A의 경험치를 바꿔 레벨업 화면을 만듭니다.

방 번호를 바꿔치기하는 Lua는 이 정도면 됩니다. 콜백은 한 번 쓰고 해제해야 이후 방 이동이 정상으로 돌아옵니다.

local T = 0x0000   -- 목적지 방 번호 (0x0000 = 디버그 룸)
emu.addMemoryCallback(function(addr, val)
  if addr == 0x3030 then return T & 0xFF else return (T >> 8) & 0xFF end
end, emu.callbackType.write, 0x3030, 0x3031, emu.cpuType.snes, emu.memType.snesMemory)

개발자 디버그 메뉴

1절에서 본 한 바이트(파일 0x106AF, F0 → 80)를 바꾸면 전투 중 내 턴에 Start로 전투 디버그 메뉴가 열립니다. Mesen 2 Lua로는 emu.write(0x106AF, 0x80, emu.memType.snesPrgRom) 한 줄입니다.

왼쪽 전투 디버그 메뉴, 오른쪽 필드 디버그 메뉴
더 깊이디버그 메뉴 항목
  • 전투 디버그 메뉴: 항목은 SCENE, OBJ, BG, HPMAX, MUSIC, SE, LINE, EFFECT, EVT 1–3, TEST입니다.
  • OBJ는 오브젝트 보기, BG는 「BG NO.」 창, MUSIC은 「NO.」와 「MAX.5F」, EFFECT는 「BG ANIME」 창을 엽니다. LINE은 화면 위에 검은 가로선을 그립니다. SE는 효과음 시험으로, LINE의 선은 CPU 사용량 표시로 보이지만 둘 다 추정입니다.
  • SCENE은 「NO. xxx」에서 편성 번호(16진수)를 골라 A로 그 전투를 불러옵니다. 고른 번호는 $7E:052A, 불러온 뒤 $7E:FA0E에 들어갑니다.
  • HPMAX는 HP가 이미 가득 찬 상태에서 시험해 변화가 보이지 않았습니다. EVT 1은 쿠파성 샹들리에 장면과 피치의 「쿠파를 조심해요 마리오 씨!!」, EVT 2는 샹들리에 위 붉은 옷 적과의 전투입니다. EVT 3과 TEST는 확인하지 못했습니다.
  • 항목 사이 빈 줄에도 커서가 멈추고, 거기서 A를 누르면 메뉴만 닫힙니다.
  • 필드 디버그 메뉴: 북미판 PAR 코드 세 개가 바꾸는 자리를 일본판 롬 바이트로 바꿨습니다(파일 0x14AFC1–0x14AFC2 = 00 26, 0x1F0000 = 01). 방 0xBD(마리오 집)의 램프 앞 (6,16)에서 북쪽, 또는 (5,15)에서 북동쪽을 보고 A를 누르면 열립니다.
  • 메뉴는 EVENT / 데모 / 옵션에서 「다음」으로 어묵브리지 / 파타부대, 다시 「다음」으로 광차이후 / 와인강으로 넘어갑니다. 예전에 이 방으로 워프했을 때 화면이 검게 나온 것은 도착 좌표 탓이었고, 진짜 입구는 (2,16)입니다.

에뮬레이터 호환성(v1.2)

libretro 코어를 직접 불러 쓰는 작은 프로그램으로 일본판, v1.1, v1.2에 같은 입력을 넣었습니다. Mesen 2에도 같은 입력 파일을 Lua로 넣었습니다. 새 게임은 전원을 켜고 쿠파성까지 가고, 이어하기는 v1.1 세이브에 요시쿠키를 넣어 가시돌이 무리와 싸우며 요시쿠키를 씁니다. 변신이 나오는지는 난수라 코어마다 다르지만, 같은 코어에서는 세 롬이 같은 프레임에 같은 결과를 냈습니다. 실기에서는 시험하지 않았습니다.

더 깊이코어별 결과
에뮬레이터결과
Mesen 2 2.1.1같은 입력에서 세 롬 모두 같은 프레임에 변신 메시지가 나왔습니다(일본판 「カタクナール」, v1.1 「BRACER」, v1.2 「단단해져라」). 세이브스테이트에서는 「버섯」이 나오고, 필드 메뉴 회귀 11케이스를 통과했습니다.
MesenCE 2.2.1Mesen 2와 같습니다. 필드 메뉴 회귀 11케이스를 통과했고, 이 판에서 계산이 바뀐 광산 트로코 장면도 메모리(WRAM·VRAM·CGRAM·OAM)가 Mesen 2와 바이트까지 같았습니다. 전원부터 긴 입력을 넣으면 두 판의 진행이 몇 프레임씩 어긋나는데, 일본판도 같습니다.
bsnes 115새 게임과 이어하기 모두 끝까지 갔습니다. 이어하기에서 변신 메시지가 일본판 「カタクナール」, v1.1 「BRACER」, v1.2 「단단해져라」로 나왔습니다.
bsnes-hd beta 10.6bsnes와 같습니다. 새 게임은 쿠파성 오프닝(모드 7)부터 화면 캡처가 검게 나오는데, 일본판도 같습니다.
Snes9x 1.63모두 끝까지 갔지만 요시쿠키를 30턴 써도 변신이 나오지 않았습니다(일본판도 같음). 화면 301장이 v1.1과 같습니다.
Snes9x 2010 1.52.4Snes9x와 같습니다.
Beetle Supafaust 1.29.030턴 중 변신이 세 번 나왔고, 세 롬 모두 같은 프레임이었습니다. v1.2는 「단단해져라」입니다.
Mesen-S 0.4.0일본판을 포함해 모든 실행이 코어 오류(exit 139)로 끝났습니다.

표를 만든 뒤 고친 이름 표기(셸 → 쉘)와 메뉴 글자(힐, 휠)는 bsnes와 Snes9x에서 다시 돌려 화면 84장이 같았습니다.

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

확인하지 않은 것

  • 마법 연출 테스트 메뉴(파일 0x106D1)는 바꿀 위치만 확인했고 실행하지 않았습니다.
  • 전투 디버그 메뉴의 EVT 3과 TEST는 실행해 보지 않았습니다.
  • 전투 애니메이션 스크립트 전체를 명령 길이대로 따라가며 훑지는 않았습니다. 63 02가 없다는 것은 명령 앞뒤 꼴로 찾은 결과입니다.
  • 전투 메시지 제어 코드 1C 37(필살기 이름)을 쓰는 메시지는 실제 장면에서 보지 못했습니다.

알려진 한계

  • 롬은 4MB를 넘길 수 없습니다(2절). v1.2 롬에 남은 빈 공간은 필드 메뉴 코드 자리 286바이트, 전투 메뉴 코드 자리 215바이트, 이름 표 뒤 147바이트, 메뉴 문구 205바이트이고, 임시 칸 글자는 100자, 대사 폰트 확장 칸은 27자를 더 넣을 수 있습니다.
  • 대사 엔진의 2바이트 글자는 1D–1F로만 시작해서, 대사에 새 글자를 더 넣으려면 쓰이지 않는 코드를 또 찾아야 합니다(4절).
  • 필드 메뉴의 임시 칸은 128칸이고 한 번에 WRAM에 모아 두는 글자는 12자, 이름 길이는 아이템·필살기 10글자, 적 8글자가 한계입니다.
  • 전투 메시지와 「무슨 생각하니」 문구는 옮길 빈 공간이 없어 문자열 뒤의 0 바이트 안에서만 늘릴 수 있습니다.
  • 메뉴 문구 영역에는 포인터가 가리키지 않는 데이터가 섞여 있어 문자열을 옮기면 안 됩니다.

쓴 도구

asar 1.91, Mesen 2.1.1, Python 3.9.6과 Pillow 11.3.0, Galmuri11 Condensed 2.404입니다. 호환성 확인에는 9절 표의 libretro 코어를 썼습니다. asar에서 분기 명령에 숫자 주소를 직접 쓰면 상대 오프셋이 잘못 계산된 적이 있어, 같은 블록의 레이블로 분기하고 assert pc() == …로 덮어쓴 범위를 확인했습니다.

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

빌드는 일본판 롬에 v1.0 IPS를 적용한 롬에서 시작해 zsh tools/make.sh 한 번으로 이름·글자 데이터 생성 → 대사 다시 채우기 → asar → 이름 대조까지 갑니다. 각 파일이 어느 절의 내용인지 아래에 모았습니다.

파일하는 일설명
docs/ANALYSIS.md롬 분석 문서. 롬 헤더, 메모리, 대사·폰트 형식, 후킹 주소, 쓴 빈 공간, 디버그 기능을 [실측]·[추정]으로 나눠 적었습니다1. 이 노트에서
krpatch sfc dis공통 도구. 롬 주소를 넣으면 65c816 명령으로 풀어 보여 주는 간단한 역어셈블러1. 이 노트에서
src/main.asm모든 후킹 코드. 맨 위 org 줄들이 덮어쓴 원래 코드 위치입니다1. 이 노트에서
tools/make.shasar --fix-checksum=on으로 체크섬까지 다시 계산하는 빌드2. 롬 지도
tools/py/dialogue.py대사 ID로 포인터 표와 뱅크 헤더를 따라가 주소 구하기3. 대사
tools/py/dlg_rewrite.py대사 다시 채우기, 1A를 1B+옵션으로 바꾸기, 가운데 정렬, 끝 코드 복구3. 대사
tools/py/dump_text.py모든 텍스트를 읽을 수 있는 줄로 덤프3. 대사
tools/py/render_jp.py일본판 원문을 대사 폰트로 그려 보기3. 대사
krpatch sfc tiles공통 도구. 롬의 폰트나 아무 구간을 타일 그림으로 뽑기 (아래 256칸 그림을 이것으로 확인)4. 글자
data/tbl/korTBL2.tblv1.0 메뉴 폰트 코드표 (korTBL.tbl은 대사 폰트)4. 글자
data/tbl/menu_font_audit.txt그림과 대조해 확인한 메뉴 폰트 코드 목록(코드, 글자, 그림 해시)4. 글자
tools/py/textusage.py어떤 2바이트 코드가 실제로 쓰이는지 조사 (재활용할 빈 코드 찾기)4. 글자
tools/py/check_menu_names.py모든 이름을 메뉴가 그리는 글자로 대조, 틀리면 빌드 실패4. 글자
tools/build.py새 이름 테이블, 동적 글리프 249자, 이름 앞 1바이트의 받침 비트(0x80) 생성6. 메뉴·이름·전투
tools/py/textfix.py이름을 바꿀 때 바로 뒤 조사(이/가, 을/를 …)도 같이 고치기6. 메뉴·이름·전투
krpatch sfc bgrefs공통 도구. VRAM 덤프에서 화면이 실제로 참조하는 구간 찾기 (빈 칸 찾기)8. 게임 고유 문제
test/banner_ids.lua이름 띠 번호를 바꿔 띄우고 그린 글자 코드 기록8. 게임 고유 문제
tools/py/check_banner.py기록한 글자 코드를 이름과 대조8. 게임 고유 문제
src/name_entry.asmNeInit·NeInput(자모 격자와 입력), NeJamo(조합), NeFly·NeLanded(날아가는 글자), NeDrawName·FsPutName(이름 입력·파일 선택의 이름)8. 게임 고유 문제
tools/py/name_entry.py이름 글리프 2,401자, 음절 번호 표, 자모 표, 격자 생성8. 게임 고유 문제
seq.lua (krpatch sfc lua), tools/run_seq.sh세이브스테이트에서 버튼 입력 재생, 화면·메모리 저장9. 확인 방법
poke_state.lua (krpatch sfc lua), tools/make_states.shRAM 값을 바꿔 새 세이브스테이트 만들기9. 확인 방법
test/harness/harness.asm, tools/make_harness.sh원하는 대사를 띄우는 테스트 전용 롬9. 확인 방법
tools/py/scene_rom.py, test/warp_seq.lua출구를 바꾸거나 방을 워프해 원하는 장소로 가기9. 확인 방법
tools/py/verify_dialogue.py, tools/py/verify_menu.py, tools/test_menu.sh대사·메뉴 회귀 검사9. 확인 방법
krpatch shotcmp공통 도구. 스크린샷 비교9. 확인 방법
krpatch ips make공통 도구. 일본판 원본 기준 IPS 만들기 (헤더 없음/있음)9. 확인 방법
test/probe.lua주소가 실행되는 순간 기록, 값 강제, 메모리 대응 비교9. 확인 방법
krpatch sfc run공통 도구. libretro 코어를 직접 불러 같은 입력을 넣는 작은 프로그램9. 확인 방법
compat/run_all.sh코어마다 일본판·v1.1·v1.2에 새 게임과 이어하기 시나리오 돌리기9. 확인 방법
script.lua (krpatch sfc lua)같은 시나리오 파일을 Mesen 2에서 돌리기9. 확인 방법
compat/make_save.py세이브에 요시쿠키를 넣고 체크섬 다시 계산9. 확인 방법

이 노트에서

  1. 이 노트에서
  2. 먼저 읽으면 좋은 장
  3. 배경지식: 메모리, CPU 둘, 바이트 하나 바꾸기
  4. 롬 지도
  5. 주소 읽는 법
  6. 롬을 4MB보다 키우지 않은 이유
  7. 대사
  8. 바이트를 글자로 풀기
  9. 대사 속 아이템 이름
  10. 글자
  11. 왜 v1.0은 이름을 영문으로 뒀나
  12. 대사 폰트에 없는 21자
  13. 그림·압축
  14. 메뉴·이름·전투
  15. 조사 자동 선택
  16. 원판부터 어색했던 화면
  17. 세이브·암호
  18. 게임 고유 문제
  19. 필요한 글자만 올리는 임시 칸
  20. 전투 메뉴는 SA-1이 따로 그린다
  21. v1.2: 전투 메시지와 이름 띠에 남은 영문
  22. v1.3: 자모로 조합하는 이름 입력
  23. 확인 방법
  24. 개발자 디버그 메뉴
  25. 에뮬레이터 호환성(v1.2)
  26. 다음 사람에게
  27. 확인하지 않은 것
  28. 알려진 한계
  29. 쓴 도구
  30. 패치 소스 파일

스크린샷 미리 보기