롬 분석 노트 · 슈퍼 패미컴
롬 분석 노트: 슈퍼 마리오 RPG
슈퍼 마리오 RPG 한글패치 v1.2을 만들며 남긴 기록입니다. 개념은 각 절의 「가이드」 링크에서 처음부터 풀어 설명합니다.
이 롬을 다시 분석하거나 패치를 이어 고칠 사람을 위해 남기는 기록입니다. v1.0은 대사와 메뉴는 한글이었지만 아이템·필살기·적 이름은 MUSHROOM, HAMMER 같은 영문이었습니다. 롬 용량이 모자라서가 아니라 메뉴 화면에 한 번에 올려 둘 수 있는 글자가 256개뿐이었기 때문입니다. v1.1은 필요한 글자만 그때그때 빈 칸에 올리는 코드를 게임에 끼워 넣어 이 문제를 풀었습니다. 롬 크기는 원본과 같은 4MB입니다.
1. 배경지식
게임기 안의 메모리
CPU는 화면에 직접 그리지 않습니다. 그림 조각(타일)을 VRAM에 올려 두고 "이 칸에는 몇 번 타일" 이라는 배치표(타일맵)를 적어 화면을 만듭니다. 글자도 타일입니다.
| 이름 | 크기 | 비유 | 하는 일 |
|---|---|---|---|
| 롬 | 4MB | 책 | 카트리지에 든 프로그램·대사·폰트. 읽기만 합니다. |
| WRAM | 128KB | 작업 책상 | CPU가 계산하며 값을 적어 두는 곳. HP와 인벤토리도 여기 있습니다. |
| VRAM | 64KB | 스티커판 | 화면에 붙일 8×8 그림 조각(타일)을 올려 두는 곳. |
| BW-RAM | 32KB | SA-1 책상 | 카트리지 안의 두 번째 CPU(SA-1)가 쓰는 메모리. |
CPU가 둘: S-CPU와 SA-1
이 게임 카트리지에는 SA-1이라는 CPU가 하나 더 들어 있습니다. 본체 CPU(S-CPU, 3.58MHz)보다 세 배쯤 빠르고, 두 CPU가 동시에 돕니다. 둘 다 65c816 계열이라 같은 어셈블리로 짭니다. 필드 메뉴는 S-CPU가, 전투 메뉴는 SA-1이 그려서 아이템 이름 하나를 바꾸려 해도 두 길을 따로 고쳐야 했습니다.
두 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로 필드와 전투에서 두 구간을 바이트 단위로 비교해 확인했습니다.
주소 읽는 법
0x22B400 같은 파일 오프셋은 롬 파일을 처음부터 센 위치이고, $E2:B400 같은 SNES 주소는 CPU가 보는 뱅크:주소입니다. 뱅크는 64KB짜리 칸 번호입니다. 이 게임은 다음처럼 바꿉니다.
파일 오프셋 = (뱅크 & 0x3F) × 0x10000 + 주소
$E2:B400 → 0x22 × 0x10000 + 0xB400 = 0x22B400어셈블리, 필요한 만큼만
어셈블리는 CPU가 실행하는 명령을 사람이 읽는 이름으로 적은 것이고, 롬 안에서는 그냥 바이트입니다. 전투 디버그 메뉴를 막는 코드(파일 0x106A9)를 예로 보겠습니다.
| 바이트 | 어셈블리 | 뜻 |
|---|---|---|
AF AE FF 00 | LDA $00FFAE | $00:FFAE 값을 읽습니다. 주소는 거꾸로 적힙니다. |
05 80 | ORA $80 | $80번지 값과 OR 합니다. 둘 다 0일 때만 0입니다. |
F0 01 | BEQ +1 | 결과가 0이면 다음 1바이트(RTS)를 건너뛰고 디버그 메뉴로 갑니다. |
60 | RTS | 아니면 그냥 돌아갑니다. 제품판은 늘 여기로 옵니다. |
F0(BEQ, 0이면 건너뛰기) 한 바이트를 80(BRA, 무조건 건너뛰기)으로 바꾸면 전투 중 Start로 개발자용 디버그 메뉴가 열립니다. 롬 해킹은 이런 바이트 바꾸기의 연속입니다.
후킹: 원래 코드 중간에 새 코드 끼우기
원래 코드 사이에는 새 기능을 넣을 자리가 없습니다. 그래서 원래 코드의 한 지점을 "새 코드로 가라" 는 명령으로 덮어쓰고, 새 코드는 롬의 빈 공간에 둡니다. 실제 패치 소스는 이렇게 생겼습니다.
org $C37990 ; 원래 아이템 이름을 그리러 가는 자리를
JMP DrawItemName ; "DrawItemName으로 가라" 로 덮어쓴다
org $C3F000 ; 원본에서 비어 있던(0xFF) 공간
DrawItemName: ; 한글 이름을 그리는 새 코드
...v1.1은 필드 메뉴, 전투 메뉴, 결과 창, 레벨업 화면, 대사 엔진 등 스무 곳 남짓에 이런 후킹을 걸었습니다.
2. 왜 v1.0은 이름을 영문으로 뒀나
메뉴 폰트의 글자 하나는 가로 8, 세로 12 픽셀 그림이고 한 줄에 2바이트씩 24바이트를 씁니다. 아래는 실제 롬에서 꺼낸 「마」입니다.
아이템 이름은 글자 하나를 1바이트 코드로 적습니다. 1바이트로 고를 수 있는 값은 256개이고 0x00–0x1F를 빼면 글자 칸은 224개입니다. 메뉴를 열면 게임은 이 폰트 전체를 VRAM에 통째로 올려 두고, 코드 c를 (c − 0x20) × 2번 타일로 고정해서 그립니다. 아래는 v1.0 롬의 메뉴 폰트 256칸입니다. 회색은 빈 코드, 노란색은 아이콘·영숫자, 파란색은 v1.0이 넣은 한글 165자입니다.
빈칸이 없습니다. 이름을 한글로 쓰려면 음절이 226자 필요했는데 이 표에 있는 것은 88자뿐이었습니다. 138자가 들어갈 칸이 없었던 것입니다. 롬을 키워도 칸 수는 늘지 않으니 병목은 용량이 아니라 "1바이트 코드 = 화면에 상주하는 256칸" 이라는 구조였습니다.
코드표가 틀린 두 글자(v1.2)
v1.0의 메뉴 폰트 코드표(korTBL2)에는 실제 그림과 다른 줄이 두 개 있었습니다. 5F=힐인데 0x5F 그림은 점(●)이고, FF=휠인데 0xFF 그림은 「빼」입니다. v1.1은 이 표를 그대로 믿어서 메뉴에서 「힐셸」(v1.2부터 「힐쉘」)이 「●셸」로, 「지노휠」이 「지노빼」로 나왔습니다. 대사 폰트 쪽 대조는 맞았기 때문에 메뉴에서만 보이던 문제입니다.
v1.2는 두 줄을 표에서 빼고 힐과 휠을 임시 칸 글자(3절)로 그립니다. 메뉴 폰트의 한글 165자는 코드표 글자 옆에 그림을 그려 전부 눈으로 대조했고, 틀린 것은 이 둘뿐이었습니다. 확인한 코드는 글자와 그림 해시를 함께 적어 두고, 빌드할 때 아이템·필살기·적 이름 414개를 메뉴가 그리는 글자로 하나씩 대조합니다. 임시 칸 글자는 롬 안의 그림이 Galmuri로 다시 그린 것과 같아야 하고, 메뉴 폰트 글자는 확인한 목록에 같은 글자·같은 그림으로 있어야 합니다. 하나라도 다르면 빌드가 멈춥니다. 전투 메뉴도 같은 표와 그림을 읽으므로 같이 확인됩니다. Galmuri와 v1.0 메뉴 폰트는 디자인이 달라 그림끼리 비슷한 정도로는 판정할 수 없어서, 메뉴 폰트는 한 번 눈으로 확인한 목록을 기준으로 삼았습니다.
3. 필요한 글자만 올리는 임시 칸
필드 메뉴의 VRAM을 덤프해 보니 BG1 타일 $200–$2FF(VRAM $6000–$7FFF)는 이 화면에서 아무도 쓰지 않았습니다. 메뉴를 열 때마다 원래 내용을 다시 올리므로 잠깐 빌려 써도 뒤탈이 없습니다. 글자 하나가 타일 2장이니 128글자가 들어갑니다.
새 이름 테이블은 롬 $E2:B400의 빈 공간에 만들고 글자를 2바이트로 적었습니다. 0x0000–0x00FF는 원래 메뉴 폰트의 글자이고, 0x8000 | n은 롬에 따로 넣은 글자 249자 중 n번째입니다. 그리는 순서는 이렇습니다.
- 이름에서 글자 하나를 읽습니다.
- 메뉴 폰트에 있는 글자면 원래 타일을 그대로 씁니다.
- 없는 글자면 이미 올려 둔 임시 칸이 있는지 찾고, 없으면 빈 칸을 하나 배정합니다.
- 게임의 외곽선 루틴($C3:6869)으로 8×12 글자를 메뉴 글자 모양으로 바꿔 WRAM에 모아 둡니다(최대 12자).
- 화면 갱신 틈(NMI)에 DMA로 VRAM에 한꺼번에 옮깁니다.
마지막 단계가 바로 복사가 아닌 이유가 있습니다. 브라운관 TV는 화면을 위에서 아래로 한 줄씩 그리고, 그동안에는 CPU가 VRAM을 건드릴 수 없습니다. 한 화면을 다 그리고 다음 화면을 시작하기 전 짧은 틈(VBlank)에만 쓸 수 있고, 그 틈이 시작될 때 걸리는 신호가 NMI입니다. 그래서 미리 준비해 둔 글자를 NMI 때 DMA(CPU 대신 데이터를 빠르게 옮기는 장치)로 옮깁니다.
만들며 밟은 지뢰
- 처음에는 게임의 "NMI 때 할 일" 목록(8칸)에 업로드를 등록했습니다. 그런데 목록이 꽉 차면 등록 루틴이 사운드 명령을 보내고 등록을 포기해서, 토드의 메뉴 튜토리얼에서 음악이 바뀌었습니다. 결국 NMI 루틴 자체를 후킹했습니다.
- 패치 변수를 평소 0인 WRAM $7E:F580 근처에 뒀는데, 아이템을 캐릭터에게 가져갈 때 이름을 그리는 스프라이트 버퍼였습니다. "평소에 0" 이 "아무도 안 쓴다" 는 뜻은 아니었습니다.
4. 전투 메뉴는 SA-1이 따로 그린다
전투 메뉴는 타일 방식이 아니라 SA-1이 BW-RAM에 비트맵을 만들고 글자를 직접 찍습니다. 같은 이름 테이블을 쓰되 글자 읽기, 그림 주소 계산, 이름 복사 세 곳을 후킹했습니다. 그림 주소를 $E4:2B00에 16비트 값을 더해 구하기 때문에 새 글자는 거기서 64KB 안에만 둘 수 있어서, 그 범위의 빈 구간 네 곳에 나눠 넣었습니다.
전투 메시지 속 이름(v1.2)
화면 위에 나오는 전투 메시지는 메뉴와 달리 대사 폰트로 그립니다. 메시지 안의 제어 코드 1C 31은 「여기에 아이템 이름」, 1C 37은 「여기에 필살기 이름」이고($C1:5683의 분기 표), 두 코드는 원판 이름 표($FA:41FC, $FA:137F)를 바로 읽습니다. v1.0은 그 표에 영문 이름을 넣어 두었고 v1.1도 이 두 곳은 손대지 않아서, 요시쿠키로 몬스터가 아이템으로 바뀌면 「몬스터는 MUSHROOM 모습이 됐다!」가 나왔습니다.
v1.2는 두 코드의 처리 루틴($C1:56A8, $C1:56DF)을 그 자리에서 바꿨습니다. 대사 인코딩 한글 이름(아이템은 KoDlgNames, 아군 필살기는 KoLvSkills)을 한 글자씩 게임의 글자 루틴($C1:5769)에 넘깁니다. 적 공격 이름은 v1.0부터 한글이라 원래 표를 그대로 읽습니다.
전투 이름 띠(v1.2)
적이 마법을 쓰면 화면 위 띠에 그 이름이 나옵니다. 띠는 전투 애니메이션 명령 63 xx(처리 루틴 $C2:4131)이 띄우고, xx가 0이면 적 공격 이름, 1이면 필살기·마법 이름, 2면 아이템 이름입니다. 1과 2도 원판 이름 표를 바로 읽어서, 강제로 띄우면 아이템과 아군 필살기는 「YOSICOOKIE」, 「CRUSH」처럼 영문이 나왔습니다.
v1.2는 두 갈래($C2:41D9, $C2:420D)를 후킹해, 띠 문자열을 WRAM에 만든 짧은 글(빈칸, 1C 38 A5, 종류, 번호)로 바꿉니다. 띠 글자는 전투 메시지와 같은 해석기로 그리는데, 이 해석기는 $20을 빈칸으로 읽어서 대사 폰트 확장 영역의 글자(솟, 쑤, 웨, 휠, 릉)를 못 그립니다. 그래서 비어 있던 코드 1C 38을 새 처리로 보내 위의 이름 루틴을 거치게 했고, 확장 글자도 그려집니다. 적 마법(번호 0x1B 이상)은 원래 표를 그대로 씁니다.
평소 플레이에서 이 두 갈래가 나오는지도 확인했습니다. 몬스터 행동 스크립트($F9:30AA의 포인터 표)에서 마법 명령(EF, F0)의 번호는 모두 0x40 이상이라 아군 필살기 번호가 없고, 행동 종류를 아이템으로 만드는 명령(E1)은 한 번도 쓰이지 않습니다. 애니메이션 데이터에서도 63 01은 적 마법이 쓰는 $F5:252C 한 곳뿐이고, 명령 앞뒤 꼴로 찾은 63 02는 없었습니다. 그래서 영문 띠는 평소에는 나오지 않는 것으로 보고, 바로잡는 김에 함께 고쳤습니다.
확인은 테스트 롬에서 했습니다. 굼바와 가시돌이가 마법을 쓰게 바꾸고, Mesen 2에서 띠 명령이 번호를 읽는 순간 번호를 바꿔 아군 필살기 27개와 아이템 165개를 모두 띄웠습니다. 그려지는 글자 코드를 기록해 이름과 대조하니 모두 같았습니다. 처음 돌렸을 때는 「힐셸」과 「천둥콰르릉」이 「캡셸」, 「천둥잣르릉」으로 나왔는데, 띠가 아니라 v1.1이 대사 폰트 코드를 재활용하며 생긴 충돌이었습니다(6절). 고친 뒤 다시 돌려 모두 일치했습니다.
새 코드는 $E2:FDE2–$E2:FF6C와 $C1:56BB–56BE, $C1:56F2–56F5(원래 루틴이 남긴 자리)에, 변수와 띠 문자열은 v1.1이 쓰는 패치 변수 영역 안의 WRAM $7E:1100–$7E:112E에 두었습니다.
5. 롬을 4MB보다 키우지 않은 이유
SA-1은 4MB 롬을 1MB짜리 창 4개로 CPU에 보여 줍니다. 4MB 너머를 읽으려면 실행 중에 창 하나를 다른 블록으로 돌려야 하는데, 두 CPU가 동시에 돌기 때문에 한쪽이 창을 돌린 순간 다른 쪽이 원래 데이터를 읽으려 하면 엉뚱한 값을 받습니다. 책 한 권을 둘이 같이 읽는데 한 사람이 말없이 페이지를 넘기는 상황입니다.
그래서 롬을 늘리지 않고, 일본판 원본에서도 0이나 0xFF로 비어 있던 공간만 찾아 썼습니다.
6. 대사 속 아이템 이름
대사는 4096개이고 ID로 찾습니다. 위치를 두 단계로 찾아가므로 대사를 고치면 이 표들도 다시 만들어야 합니다.
- 대사 ID($000–$FFF)를 512로 나눠 그룹(0–7)을 구합니다. 예: $204 ÷ 512 → 그룹 1
- 뱅크 $E2–$E4 첫머리 헤더에서 그 그룹의 시작 주소를 찾습니다. $E2 헤더는 08 00 F5 03 …이고, 첫 값 0x0008이 헤더 크기(그룹 4개)이자 그룹 0의 시작입니다. 그룹 1은 그다음 값 $03F5입니다.
- 포인터 표 $F7:E000에서 ID마다 적힌 2바이트(그룹 안 위치)를 읽습니다. $F7:E000 + $204 × 2 = $F7:E408에 25 00이 있습니다.
- 둘을 더한 $E2:03F5 + 0x25 = $E2:041A(파일 0x22041A)에 대사 문자열이 있습니다.
v1.0은 대사에 『MUSHROOM』을 얻었습니다처럼 영문 이름을 직접 적었습니다. v1.1은 이를 한글로 바꾸고 그룹 순서를 지킨 채 뱅크마다 다시 채운 뒤 헤더와 포인터 표를 새로 만들었습니다. 한글 이름이 짧고 쓰레기 바이트도 빼서 세 뱅크 합쳐 약 1만 9천 바이트가 오히려 줄었습니다.
안 쓰던 제어 코드 되살리기
대사 안에는 글자 말고 명령도 섞여 있습니다. 00은 대사 끝, 01은 줄바꿈, 1A는 "여기 아이템 이름을 끼워라" 입니다. 원판에서 아무 일도 안 하던 1B를 1A와 같은 처리로 보내고, 뒤에 붙는 1바이트로 「」 감싸기, 가운데 정렬, 을/를 붙이기를 고르게 했습니다. 이름 길이는 실행 중에야 정해지므로 가운데 정렬도 실행 중에 합니다.
위에서 찾아간 대사 $204를 v1.1 롬에서 그대로 꺼내면 이렇습니다. 1바이트 글자와 1D–1F로 시작하는 2바이트 글자가 섞여 있습니다.
| 바이트 | 1B 07 | 20 | 1D 07 | 1D 22 | 21 | 00 |
|---|---|---|---|---|---|---|
| 뜻 | 이름 | 빈칸 | 획 | 득 | ! | 끝 |
1B 뒤의 07은 아래 세 비트를 더한 값입니다. 그래서 이름이 버섯이면 「버섯」을 획득!이 화면 가운데에 나옵니다. 같은 대사가 v1.0에서는 09 3B 1A 3C 8F(탭 3칸, 「, 이름, 」, 을)였고 v1.1은 이 다섯 바이트를 1B 07 두 바이트로 바꿨습니다.
| 비트 | 하는 일 |
|---|---|
1 | 이름을 「」로 감쌉니다. |
2 | 줄의 나머지 글자 수를 미리 세어 앞에 빈칸을 채워 가운데에 놓습니다. |
4 | 이름 마지막 글자의 받침에 맞춰 을/를을 붙입니다. |
대사 폰트에 없는 21자
대사 폰트는 1바이트와 2바이트 코드를 섞어 992자를 쓰는데, 2바이트 코드 768개가 v1.0에서 전부 차 있었습니다. 이름에 필요한 캔, 깃, 링, 쑥, 츠 같은 21자는 어떤 텍스트에서도 쓰지 않는 코드를 찾아 그 자리에 그렸습니다. 처음에는 대사만 보고 골랐다가 메뉴 튜토리얼의 「봅」이 깨졌습니다. 같은 인코딩이 전투 대사, 아이템 설명, 메뉴 문구 등에도 쓰였기 때문입니다. 지금은 모든 영역을 검사해서, 쓰이는 코드를 재활용하려 하면 빌드가 멈춥니다.
v1.2에서 이 방식의 구멍을 하나 더 막았습니다. 다시 그린 코드 중 1F44와 1FA0은 원래 「힐」과 「콰」 자리였는데, v1.1이 새로 넣은 이름 「힐셸」(v1.2부터 「레이지쉘」과 맞춘 「힐쉘」)과 「천둥콰르릉」이 그 코드를 원래 뜻대로 써서 결과 창·레벨업 화면 등에서 「캡셸」, 「천둥잣르릉」으로 나왔습니다. 2바이트 코드 768개가 이미 다 쓰여 빈 코드가 없었는데, 빌드된 롬의 모든 텍스트를 다시 훑으니 1F7F(줏)와 1FA9(궈) 두 개가 비어 있었습니다. v1.1이 「줏어 → 주워」, 「잠궈 → 잠가」로 맞춤법을 고치면서 더는 쓰이지 않게 된 자리입니다. 「콰」를 1F7F, 「힐」을 1FA9에 그렸고, 접두가 1F라 결과 창도 그대로 그립니다. 이제 이름의 음절이 재활용한 코드로 가거나 줏·궈가 새 텍스트에 다시 나타나면 빌드가 멈춥니다. 같은 때 「셸」을 쓰던 해피셸·파이어셸·힐셸을 「레이지쉘」에 맞춰 「쉘」로 통일했습니다. 셸은 더 쓰이지 않지만 1F13 자리는 그대로 두었고, 재활용 코드 배정은 코드별로 고정해 필요 없어진 음절이 생겨도 다른 음절의 코드가 밀리지 않게 했습니다.
대사를 다시 채우다 v1.0이 남긴 문제도 찾았습니다. 끝 코드 뒤에 원문 조각이 남아 있었고, 끝 코드가 너무 일찍 들어가 비어 버린 대사도 있었습니다. 일본판과 대조해 복구했습니다.
7. 조사 자동 선택
"레벨3(이)가", "(을)를 배웠습니다" 같은 표기는 이름이 실행 중에 정해져서 생깁니다. v1.1은 빌드할 때 이름 마지막 글자에 받침이 있는지 계산해 이름 앞 1바이트에 표시해 두고, 게임은 그 비트만 보고 을/를을 고릅니다.
받침 번호 = (글자 코드 − 0xAC00) % 28 ; 0이면 받침 없음
버섯 → 「섯」 받침 있음 → 버섯을
회복샤워 → 「워」 받침 없음 → 회복샤워를숫자는 읽는 소리로 정했습니다. 마지막 자리가 2, 4, 5, 9면 「가」, 나머지는 「이」입니다.
8. 원판부터 어색했던 화면
전투 결과 창은 아이템 이름을 오른쪽 정렬하고, 5글자 이하면 「아이템」과 같은 줄에 두며 창을 한 줄 줄였습니다. 필살기 화면 안내는 이름을 4칸 고정으로 그려 「マロ が…」처럼 빈칸이 생기던 것을 이름 바로 뒤로 당겼습니다.
9. 확인은 에뮬레이터가
- Mesen 2와 Lua 스크립트로 세이브스테이트에서 버튼 입력을 재생하고, 원하는 순간의 화면과 메모리를 저장합니다.
- 인벤토리에 아이템을 직접 넣거나 경험치를 레벨업 직전으로 바꿔 보고 싶은 화면을 바로 만듭니다.
- 상점 주인 대사 자리에 원하는 대사를 띄우는 테스트 전용 롬으로 v1.0과 v1.1을 같은 조건에서 찍습니다.
- 고치기로 한 목록 밖에서 바뀐 대사가 0건인지, 메뉴 화면이 기준과 픽셀 단위로 같은지 비교합니다.
테스트에 가장 많이 쓴 메모리 주소입니다. 방 번호와 전투 편성은 미리 써 두면 게임이 덮어쓰므로, Mesen 2의 쓰기 콜백에서 게임이 쓰려는 값을 바꿔치기합니다. 나머지는 값을 써 두기만 하면 됩니다.
| 주소 | 쓰는 법 |
|---|---|
$00:3030 | 지금 방 번호(16비트). 출구를 지날 때 쓰는 값을 바꾸면 그 방으로 갑니다. 0x0000은 디버그 룸입니다. |
$7E:FA0E | 전투 편성 번호(16비트). 전투에 들어갈 때 쓰는 값을 바꾸면 다른 적과 싸웁니다. |
$00:3112 | FF로 쓰면 메뉴가 막힌 곳에서도 메뉴가 열립니다. |
$7F:F882 | 아이템 인벤토리 30칸(빈칸 FF). 보고 싶은 아이템을 직접 넣습니다. |
$7F:F800 | 마리오 능력치(0x14바이트). +0A의 경험치를 바꿔 레벨업 화면을 만듭니다. |
전투 디버그 메뉴(1절)의 SCENE에서도 편성을 고를 수 있고, 번호는 16진수입니다. 디버그 룸과 그 밖의 RAM은 docs/ANALYSIS.md 7장에 있습니다.
개발자 디버그 메뉴
- 전투 디버그 메뉴: 파일 0x106AF를 80으로 바꾸고 전투 중 내 턴에 Start. 항목은 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 사용량 표시로 보이지만 둘 다 추정입니다.
- 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 세이브에 요시쿠키를 넣어 가시돌이 무리와 싸우며 요시쿠키를 씁니다. 변신이 나오는지는 난수라 코어마다 다르지만, 같은 코어에서는 세 롬이 같은 프레임에 같은 결과를 냈습니다. 표를 만든 뒤 고친 이름 표기와 메뉴 글자는 bsnes와 Snes9x에서 같은 화면이 나오는 것을 다시 확인했습니다. 실기에서는 시험하지 않았습니다.
| 에뮬레이터 | 결과 |
|---|---|
| Mesen 2 2.1.1 | 같은 입력에서 세 롬 모두 같은 프레임에 변신 메시지가 나왔습니다(일본판 「カタクナール」, v1.1 「BRACER」, v1.2 「단단해져라」). 세이브스테이트에서는 「버섯」이 나오고, 필드 메뉴 회귀 11케이스를 통과했습니다. |
| bsnes 115 | 새 게임과 이어하기 모두 끝까지 갔습니다. 이어하기에서 변신 메시지가 일본판 「カタクナール」, v1.1 「BRACER」, v1.2 「단단해져라」로 나왔습니다. |
| bsnes-hd beta 10.6 | bsnes와 같습니다. 새 게임은 쿠파성 오프닝(모드 7)부터 화면 캡처가 검게 나오는데, 일본판도 같습니다. |
| Snes9x 1.63 | 모두 끝까지 갔지만 요시쿠키를 30턴 써도 변신이 나오지 않았습니다(일본판도 같음). 화면 301장이 v1.1과 같습니다. |
| Snes9x 2010 1.52.4 | Snes9x와 같습니다. |
| Beetle Supafaust 1.29.0 | 30턴 중 변신이 세 번 나왔고, 세 롬 모두 같은 프레임이었습니다. v1.2는 「단단해져라」입니다. |
| Mesen-S 0.4.0 | 일본판을 포함해 모든 실행이 코어 오류(exit 139)로 끝났습니다. |
10. 다음 사람에게
확인하지 않은 것
- 전투 디버그 메뉴의 EVT 3과 TEST는 실행해 보지 않았습니다.
- 전투 애니메이션 스크립트 전체를 명령 길이대로 따라가며 훑지는 않았습니다.
63 02가 없다는 것은 명령 앞뒤 꼴로 찾은 결과입니다. - 전투 메시지 제어 코드 1C 37(필살기 이름)을 쓰는 메시지는 실제 장면에서 보지 못했습니다.
알려진 한계
- 롬은 4MB를 넘길 수 없습니다. 남은 빈 공간은 필드 메뉴 코드 자리 286바이트, 전투 메뉴 코드 자리 215바이트, 이름 표 뒤 147바이트이고, 동적 글자는 100자를 더 넣을 수 있습니다.
- 대사 엔진의 2바이트 글자는 1D–1F로만 시작해서, 대사에 새 글자를 더 넣으려면 쓰이지 않는 코드를 또 찾아야 합니다.
- 필드 메뉴의 임시 칸은 128칸이고, 이름 길이는 아이템·필살기 10글자, 적 8글자가 한계입니다.
- 전투 메시지와 「무슨 생각하니」 문구는 옮길 빈 공간이 없어 문자열 뒤의 0 바이트 안에서만 늘릴 수 있습니다.
쓴 도구
asar 1.91, Mesen 2.1.1, Python 3.9.6과 Pillow 11.3.0, Galmuri11 Condensed 2.404입니다. 호환성 확인에는 위 표의 libretro 코어를 썼습니다.
11. 패치 소스 파일
| 파일 | 하는 일 | 설명 |
|---|---|---|
docs/ANALYSIS.md | 롬 분석 문서. 롬 헤더, 메모리, 대사·폰트 형식, 후킹 주소, 쓴 빈 공간, 디버그 기능을 [실측]·[추정]으로 나눠 적었습니다 | 1. 배경지식 |
krpatch snes dis | 공통 도구. 롬 주소를 넣으면 65c816 명령으로 풀어 보여 주는 간단한 역어셈블러 | 1. 배경지식 |
src/main.asm | 모든 후킹 코드. 맨 위 org 줄들이 덮어쓴 원래 코드 위치입니다 | 1. 배경지식 |
krpatch snes tiles | 공통 도구. 롬의 폰트나 아무 구간을 타일 그림으로 뽑기 (위 256칸 그림을 이것으로 확인) | 2. 메뉴 폰트 |
data/tbl/korTBL2.tbl | v1.0 메뉴 폰트 코드표 (korTBL.tbl은 대사 폰트) | 2. 메뉴 폰트 |
data/tbl/menu_font_audit.txt | 그림과 대조해 확인한 메뉴 폰트 코드 목록(코드, 글자, 그림 해시) | 2. 메뉴 폰트 |
tools/py/check_menu_names.py | 모든 이름을 메뉴가 그리는 글자로 대조, 틀리면 빌드 실패 | 2. 메뉴 폰트 |
krpatch snes bgrefs | 공통 도구. VRAM 덤프에서 화면이 실제로 참조하는 구간 찾기 (빈 칸 찾기) | 3. 임시 칸 |
tools/build.py | 새 이름 테이블과 동적 글리프 249자 생성 | 3. 임시 칸 |
test/banner_ids.lua | 이름 띠 번호를 바꿔 띄우고 그린 글자 코드 기록 | 4. 전투 메뉴 |
tools/py/check_banner.py | 기록한 글자 코드를 이름과 대조 | 4. 전투 메뉴 |
tools/make.sh | asar --fix-checksum=on으로 체크섬까지 다시 계산하는 빌드 | 5. 롬 크기 |
tools/py/dialogue.py | 대사 ID로 포인터 표와 뱅크 헤더를 따라가 주소 구하기 | 6. 대사 |
tools/py/dlg_rewrite.py | 대사 다시 채우기, 1A를 1B+옵션으로 바꾸기, 가운데 정렬, 끝 코드 복구 | 6. 대사 |
tools/py/textusage.py | 어떤 2바이트 코드가 실제로 쓰이는지 조사 (재활용할 빈 코드 찾기) | 6. 대사 |
tools/py/dump_text.py | 모든 텍스트를 읽을 수 있는 줄로 덤프 | 6. 대사 |
tools/py/render_jp.py | 일본판 원문을 대사 폰트로 그려 보기 | 6. 대사 |
tools/py/textfix.py | 이름을 바꿀 때 바로 뒤 조사(이/가, 을/를 …)도 같이 고치기 | 7. 조사 |
seq.lua (krpatch snes lua), tools/run_seq.sh | 세이브스테이트에서 버튼 입력 재생, 화면·메모리 저장 | 9. 확인 |
poke_state.lua (krpatch snes lua), tools/make_states.sh | RAM 값을 바꿔 새 세이브스테이트 만들기 | 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 snes run | 공통 도구. libretro 코어를 직접 불러 같은 입력을 넣는 작은 프로그램 | 9. 확인 |
compat/run_all.sh | 코어마다 일본판·v1.1·v1.2에 새 게임과 이어하기 시나리오 돌리기 | 9. 확인 |
script.lua (krpatch snes lua) | 같은 시나리오 파일을 Mesen 2에서 돌리기 | 9. 확인 |
compat/make_save.py | 세이브에 요시쿠키를 넣고 체크섬 다시 계산 | 9. 확인 |
이 노트의 개념을 설명하는 장
직접 해 보기 소스와 도구
- 패치 소스 — 이 노트가 가리키는 파일과 빌드 순서(한국어 안내)
- 롬 분석 문서 v1.2 — 주소와 형식을 [실측]·[추정]으로 나눠 적은 한국어 문서
- kr-patch-toolkit v0.2.0 — 공통 명령 krpatch 한국어 설명
- 슈퍼 마리오 RPG 한글패치 페이지 — 패치 내려받기와 롬에 바로 적용
댓글