KOEI/대항해시대 시리즈

대항해시대2 윈도우95판에 한글을 출력시키고 말겠다.

K66Google 2021. 1. 30. 14:29

(대항해시대2 한글판 - 도스 버전)


(대항해시대2 일본어판 - 윈도우95 버전)


1. 서론


대항해시대2는 도스 버전만 한글판이 나왔고, 윈도우95 버전은 한글판이 나오지 않았다. 따라서 윈도우95판은 일본어 버전 밖에 없다. 그러나 도스판의 파일과 윈도우95판의 파일은 파일명도 그렇고 어느정도 호환이 된다는 생각이 들었다. 그래서 나는 징기스칸4 파워업키트(#L1)와 프린세스메이커5(#L2 , #L3)에 한글을 출력한 경험을 바탕으로, 대항해시대2 윈도우95판에도 한글 메시지 출력에 도전해보기로 했다.



2. Ollydbg 공통 수정


이제는 암기까지도 가능하게 된 Ollydbg를 통한 어셈블리 수정 과정이다.


0044BC3E - PUSH 0x81 로 변경.


0044E0FF - CMP EAX, 0xC8 로 변경.

0044E106 - CMP EAX, 0xA1 로 변경.


00403192 - CMP CL, 0xC8 로 변경.

00403197 - CMP CL, 0xA1 로 변경.


0044A8AD - ADD EDX, 0x161 로 변경.

0044A8B5 - ADD EDX, 0xE1 로 변경.


메뉴 파일(MENU.DAT)과 메시지 파일(MESSAGE.DAT , SNR어쩌구.MES 등)에 히라가나가 사용된 걸 확인했기 때문에, 반각 가타카나의 전각 히라가나 강제 변환 기능은 없는 것으로 보인다.


여기까지 작업하고 결과를 실행 파일로 저장하였다.


에르네스트 로페스 시나리오의 대사 중 하나를 도스판에서 캡처해왔다.


"이봐. 선생소린 그만두게. 창피하니까."


과연 해당 대사는 제대로 출력될 수 있을까?




"겁걍. 갚괄겊같 겜갭겄V. 창피하겅곡."



'창피하' 라는 글자만 제대로 출력되고 나머지 글자들은 이상하게 변환되서 출력된다.

출력결과를 표로 나타내보면 다음과 같다.


 도스판 대사

 헥스 코드

 윈도우95판 대사 변환

 헥스 코드 변환

 이

 C0 CC

 겁

 B0 CC

 봐

 BA C1

 걍

 B0 C1

 선

 BC B1

 갚

 B0 B1

 생

 BB FD

 괄

 B0 FD

 소

 B2 D2

 겊

 B0 D2

 린

 B8 B0

 같

 B0 B0

 그

 B1 D7

 겜

 B0 D7

 만

 B8 B8

 갭

 B0 B8

 두

 B5 CE

 겄

 B0 CE

 게

 B0 D4

 V

 56

 창

 C3 A2

 창

 C3 A2

 피

 C7 C7

 피

 C7 C7

 하

 C7 CF

 하

 C7 CF

 니

 B4 CF

 겅

 B0 CF

 까

 B1 EE

 곡

 B0 EE


이런 식으로 앞 바이트가 C1 미만일 경우에는 모조리 B0으로 바뀌거나 ASCII 문자로 출력되는 해괴한 변환 알고리즘이 작동하고 있었다. 



3. 한글 한겡 출력 문제


나는 이러한 알고리즘을 '한글 한겡 출력 문제' 라고 명명하였다.


여러 번의 실험 끝에, 앞 바이트에 따라서 변환 알고리즘의 결과도 달라진다는 점을 도출해냈다. 이러한 알고리즘 때문에 한글 2350자 영역(B0A1 ~ C8FE) 중 앞 바이트가 B0~C0인 한글은 출력되지 않는다. 이를 어떻게 해야 고칠 수 있단 말인가?


이렇게나 황당한 알고리즘은 처음 봤다. 

알고리즘의 방해로 한글 출력은 물 건너 갔다고 생각한 나는, 모든 작업을 팽개쳐버렸다. 

이것이 2019년 12월 6일의 일이었다.


그리고 약 1년 1개월이라는 시간이 흘렀다...




4. 알고리즘 해체 1차 시도


대항해시대3를 이것저것 뜯어보다가 오랜만에 하드디스크 구석에 있던 대항해시대2 일판을 다시 찾게 되었다. 안 그래도 요즘 블로그에 글 쓸 거리가 없어서 걱정이었는데, 이거 한글 출력만 성공하면 초대박이 아닌가? 그래서 나는 다시 Ollydbg를 열고 실행파일을 뜯어보기 시작했다.


일단 텍스트 출력과 관련있는 함수인 TextOutA를 찾았다. 대항해시대2 실행파일에서 TextOutA는 두 곳에 있는데, 각각 1바이트 출력과 2바이트 출력을 담당한다. (Stringsize가 0x2인게 2바이트. 0x1이면 1바이트.)


2바이트 TextOutA는 시작 지점이 0044A8D5부터 시작한다. 그래서 나는 시작 지점에서 브레이크 포인트(F2)를 걸고, Ollydbg상에서 실행해서 글자 바이트가 어떻게 들어오는지를 확인해보았다. 그랬더니 앞 바이트가 C1 미만인 글자들은 BH에 바이트가 잘못 들어오고 있는 걸 확인할 수 있었다. 


일단은 0044A8D5에서 Find reference 한 다음, 결과 주소(0044A8CC)로 이동했다. 그랬더니 공통 작업에서 수정한 0x161 , 0xE1 파트가 보였다. 아무튼 이런 식으로 브레이크 포인트를 걸고, 실행하고, 함수 시작지점에서 레퍼런스를 찾는 그런 삽질을 계속 진행했다.


이런 식으로 브레이크 포인트를 계속 걸다보니 나름 눈여겨 볼만한 정보들을 얻을 수 있었다. 예를 들어 현재 표시할 글자의 헥스값이 C1 41일 경우에는 ECX값에 21 22가 들어간다. 그런데 B1 42일 경우에는 ECX값이 00 24로 들어간다. 어쩌면 ECX값의 00 때문에 한글 한겡 출력문제가 발생하는 것은 아닐까? 나는 좀 더 거슬러 올라가보았다.


그렇게 해서 00402E20의 함수까지 도착했다. 여기서 결정적인 차이가 발생하였다.

00402E30의 JBE 점프 구문에서 C1 41은 점프하지 않는데, Bx xx대 글자들은 모두 점프를 하고 있었다.

위에 있는 CMP CL, 0x20의 영향을 받았는지도 모른다. 그렇다면 여기서 점프를 못 하게 만들면 어떻게 될까?


이렇게 00402E30을 NOP으로 변경했다. 저장하고 실행해본다.



오오오오!!!! 

이제 Bx xx대 글자들도 출력이 되기 시작했다. 

그러나 스샷을 보면 알겠지만, '괆' 자 같은 B0 xx대 글자들은 여전히 제대로 출력되지 않는다. 즉, B1 xx 미만의 글자들은 아직도 정상출력이 불가능하다는 것이다. 산너머 산이다.



5. 알고리즘 해체 2차 시도


좀 전에 NOP 처리를 한 함수의 시작 지점(00402E20)에서 한번 더 레퍼런스를 조회했다. 두 개가 나왔는데, 먼저 00402E9B라는 주소로 이동했다. 이동된 함수의 시작 주소는 00402E70이었다.


여기에 브레이크 포인트를 걸고 지켜보고 있으니까, B0 xx의 글자들은 EAX값이 00 xx로 들어온다. AH 부분(00이 들어간 자리를 일컫는 명칭)에 바이트가 들어오지 않는 것이다.


반면에 다른 글자들은 [ 20 55 ] 이런 식으로 AH 부분에 바이트가 들어온다.


00402E81에는 JNB 점프 구문이 있는데, B0 xx 글자들은 AH 부분의 00으로 인해 점프를 못 하고 1바이트 TextOutA 함수(0044A9C1)로 이동하게 된다. 여기서 점프를 해야만 2바이트 함수로 들어갈 수 있다. 그렇다면...


위에 있는 비교 구문(CMP)의 비교 기준을 확 낮추면 어떻게 될까?

이런 생각으로 CMP EAX, 0x200을 CMP EAX, 0x22로 바꿔보았더니... 


B0 xx대 글자들도 출력은 되지만 반토막나서 출력이 되고, 아스키 코드(1바이트) 문자들까지 한글로 바뀌어서 출력되는 현상이 벌어진다.


이번에는 00402E20의 또 다른 레퍼런스인 0042DA16로 이동했다. 그 위에 CALL 00402D20이 있었는데, 신기하게도 이 함수가 호출되면 레지스터에 글자 바이트에 따른 변환 코드가 저장된다. (해당 함수를 호출하는 다른 주소 00403157에서도 확인함)


위 스크린샷의 EAX를 보면 20 55 라는 값이 들어있다. C0 D3이라는 글자가 들어갔을때 이렇게 변환된다. EAX 변환 추정값은 다음과 같다.


* 원래 바이트 - EAX 변환 - TextOutA 최종 출력 순

22 - 00 22 - 22

B0 A1 - 00 23 - 23

B1 A1 - 02 23 - B1 A1 (그대로)

B2 A1 - 04 23 - B2 A1 (그대로)

B3 xx - 06 yy - (그대로. 이하 생략...)

B4 xx - 08 yy - 

B5 xx - 0A yy -

B6 xx - 0C yy - 

B7 xx - 0E yy - 

B8 xx - 10 yy - 

B9 xx - 12 yy - 

BA xx - 14 yy - 

BB xx - 16 yy - 

BC xx - 18 yy - 

BD xx - 1A yy - 

BE xx - 1C yy - 

BF xx - 1E yy - 

C0 xx - 20 yy - 


xx와 yy의 변환 관계는 A1 = 23 / A2 = 24 / A3 = 25 ...(중략)... A8 = 2A / A9 = 2B / AA = 2C.... (중략) 이런 식이다. 


보면 알겠지만, B0 xx 글자들은 EAX 변환 후 아스키 코드처럼 1바이트가 되어버린다. 저걸 어떻게든 못 하게 해야 되는데... 어쩌면 좋을까?


그러다가 기가 막힌 생각이 났다. 

EAX 변환을 하기 전에 바이트를 속였다가, TextOutA로 출력하기 전에 바이트를 원래대로 복구시키는 것이다.

실행파일 빈 자리에다 어셈블리 코드를 치고, CALL 명령어로 원래 함수에 넘겨주면 되는 거 아닌가?


그래서 나는 EAX 변환을 담당하는 00402D20 함수 내부에 CALL 명령어를 삽입했다. 이때 앞 바이트는 AL에서 담당하고 있으니 AL을 조작해주면 될 것이다.


그런 다음 실행파일 내 빈 자리(DB 어쩌구)에 가서 추가 함수를 작성했다. 원래 자리에 있었던 어셈블리 명령어부터 쓰고, ADD AL, 1이라는 명령어를 삽입했다. 마지막으로 RETN을 써서 원래 자리로 돌아가게 만들었다.

(참고로 추가 함수를 작성할 수 있는 빈 자리는 정해져 있는 것으로 보인다. 어떤 자리는 입력 후 호출할때 게임이 튕기는 경우가 있다.)


여기까지 하면 이제 실행파일은 B0 A1이라는 글자가 들어왔을때 B1 A1로 인식하고, 출력도 B1 A1의 글자가 출력된다.

AL에다 1(16진수)을 더했기(ADD) 때문에 B1이 되는 것이다.


이제 TextOutA를 출력하기 전의 주소에 가서 CALL 명령어를 삽입한다. 0044A8CC에서 TextOutA로 가는 함수를 호출하기 때문에, 그 이전에 삽입해야 한다.


원래 있던 명령어를 입력하고, 추가로 SUB EAX, 0x100 이라고 입력했다. EAX에서 100(16진수)를 빼라는(SUB) 뜻이다.

왜 100을 빼야하냐면... 현재 EAX에는 0000B1A1 이런 식으로 저장이 되어있다. 그러니까 뒤에서부터 3번째 바이트를 -1하려면 자릿수를 채워줘야 하므로 100을 빼는 것이다. 빼고 나면 EAX는 0000B0A1이 된다.

마지막에는 원래 자리로 돌아가기 위해 RETN을 입력했다.


끝으로 아까 CMP EAX, 0x22로 수정했던 걸 다시 CMP EAX, 0x7F로 수정한다. 이렇게 해야 아스키 코드 문자들이 한글로 바뀌어서 출력되는 걸 막을 수 있다.


저장하고 실행해보자. 과연 어떤 결과가 나올까?


B0 xx 영역 글자들도 아주 깔끔하게 잘 나온다!!! 

그러나 아직 문제는 남아있다. 한글 인코딩에서 전각 기호를 담당하는 Ax xx 영역의 글자들이 출력되지 않고 스샷처럼 ?만 뜨고 있기 때문이다.

그래도 이 문제는 쉽게 해결할 것 같다. 

[1]을 더하고 뺐더니 B0 xx 영역 글자들이 제대로 출력되었으니까, [10]을 더하고 빼면 A0 xx 영역의 기호들도 제대로 출력되지 않을까? (대괄호 속 숫자는 모두 16진수다.)


결국 알고리즘을 부수기 위한 어셈블리 수정 내역은 다음과 같이 정리해볼 수 있겠다.



* 정리 - 한글 한겡 출력 문제 해결


00402E30 - NOP 으로 변경.


00402D41 - ADD AL,CL 을 CALL 00402CC3 으로 변경.


00402CC3 - 빈 공간에 추가

ADD AL, CL

ADD AL, 0x10

SHL AL,1

CMP DL,0x80

RETN


0044A8C6 - SHL EAX,0x8 을 CALL 00402CCE 으로 변경.


00402CCE  - 빈 공간에 추가

SHL EAX,0x8

ADD EAX, ECX

SUB EAX, 0x1000

RETN


00402E7C - CMP EAX, 0x7F 로 변경.


드디어 한글 2350자 뿐만 아니라 전각 기호까지 모두 출력에 성공했다.

1년만에 여기까지 도달하게 될 줄이야... 참으로 감개무량하다...



6. 이름 입력 처리 수정


이제 새 게임을 시작할때 한국어 이름을 입력할 수 있도록 조치해야 한다. 왼쪽의 쫑쭹, 뢍섕 이런 건 MENU.DAT를 수정하면 되니까 상관없고, 아래쪽의 글자 리스트를 수정하는데 신경 써야 한다.


다행히도 실행 파일 내에서 금방 찾을 수 있었다. 88 9F가 바이트 플립되어 9F 88로 들어가있다. 한 구역의 파트를 담당하는 바이트는 2개, 시작 바이트와 다음 구역 시작 바이트다. 이와 비슷한 구조를 프린세스메이커5에서 본 적이 있다. (관련 게시물. 5번 단락을 참조.)

프메5에서 수정한 것처럼 대항2도 똑같이 수정해본다.


수정했더니 한글이 잘 뜬다. 그러나 프메5와 마찬가지로 여기서도 바이트가 ~FD, ~FE로 끝나는 글자들은 리스트에서 출력되지 않는다.


문제의 원인은 뻔하다. 원래 일본어 인코딩인 Shift-JIS에는 끝자리가 ~FD, ~FE로 끝나는 글자가 없기 때문이다.

프메5에서는 추가 영역에 해당 글자들을 배치하는 걸로 해결했지만 대항2는 프로그램 구조 상 그렇게 할 수가 없다. 결국 또 Ollydbg로 고쳐야 하나...


이제는 브레이크 포인트 걸기도 지쳤다. 그냥 될대로 되라는 심정으로 이렇게 검색했다.


cmp eax, 0xFC... 안 나온다. 그럼 cmp eax, 0xFD... 역시 안 나온다.

그래서 eax를 ebx, ecx, edx, ax, bx, cx, dx, ah, bh, ch, dh, al, bl, cl, dl... 이렇게 바꿔가면서 검색해봤더니 CMP AL, 0xFD라는 값이 나타났다.


4줄 위에는 CMP AL, 0x7F도 있었다. 7F... FD... 모두 Shift-JIS에서 글자가 없는 바이트다. 수상하다.

그래서 나는 0xFD를 0xFF로 수정했다.


* 정리

0042D98C - CMP AL, 0xFD → CMP AL, 0xFF 로 변경.


저장하고 게임을 실행해봤다.


실행해보니까 힝(C8 FE)도 정상적으로 글자 리스트에 들어와있는 걸 확인할 수 있었다.

이로써 끝 바이트가 ~FD, ~FE로 끝나는 글자도 입력이 가능하게 되었다.


기왕 수정하는 김에, 기호 문자도 입력할 수 있도록 조치했다.



7. 엔딩 연도의 뷁어 수정


플레이 하던 도중 배드엔딩을 보게 되었다. 그런데 날짜가 뷁어로 뜨고 있었다.

'' 은 헥스 코드로 C2 51이다. 앞 바이트가 해괴한 알고리즘의 영향을 받았다고 가정하면, 원래 C2는 82였을 것이다. 그럼 82 51... 82 51은 일본어 Shift-JIS 인코딩으로 전각 숫자 '2'다.


또 다시 Ollydbg를 켰다. 프메5 실행파일 한글화때를 생각해본다면, 아마 앞 바이트에 82를 넣어놓고 뒷 바이트에는 4F를 넣은 다음 숫자에 따라 뒷 바이트를 증가시킬 거라는 예상이 든다. (82 4F는 Shift-JIS로 전각 숫자 '0'이다.)


있을 것 같은 명령어를 검색해보니 ADD BL, 0x4F 라는 명령어가 검색되었다. 밑에 MOV 어쩌구저쩌구, 0x82 도 있다. 82 4F... 굉장히 수상하기 짝이 없다.


그래서 4F를 B0, 82를 A3으로 수정하였다. 그럼 A3 B0이 되는데 이건 한글 인코딩으로 전각 숫자 '0' 이다.


* 정리

004029FF - ADD BL, 0xB0 으로 변경.

00402A04 - MOV BYTE PTR DS:[ECX],0xA3 으로 변경.



저장하고 실행했더니 이제는 엔딩 연도에도 숫자가 정상적으로 출력된다.



8. 맺음말


이것으로 대항해시대2 윈도우95판의 한글 출력 과정 서술을 마친다. 

텍스트는 MENU.DAT를 제외하고는 거의 다 한글판 파일과 호환이 되는 것으로 보인다. 그러나 안타깝게도 '레이아웃 - TYPE 02' 는 끝내 한글판에서 이식하는데 실패했다. 도스판 GRAPH.DAT 파일과 윈도우95판 GRAPH.DAT 파일의 구조가 전혀 다르기 때문이다. 윈도우95판은 이미지마다 NPK016이라는 헤더가 붙어있는데다가 도스판과 압축 구조가 달라서 '대항해시대2 이벤트 그래픽 편집기' 프로그램으로 열 수가 없다.

그래서 대항해시대2 외전의 GRAPH.DAT 파일을 덮어씌워봤는데... 이러니까 게임에서 튕긴다. 파일 압축 구조까지 들여다보고 있을 시간이 없으므로, 레이아웃은 그냥 TYPE 01로 두고 플레이해야 될 것 같다.


엔딩까지 정상적으로 플레이가 가능한 게 확인되었으므로, 한글패치를 공개하도록 하겠다. (패치 게시물)

시간 관계상 에르네스트 로페스 시나리오만 확인했다.


그럼 이만...