가이아 마스터 ~신들의 보드 게임~ 한글 패치
게임 타이틀

캐릭터 소개

이벤트 카드

기타 이미지

패스워드
특정 조건이나 키워드를 입력하면 아래 3개의 카드가 강화 된다.
키워드를 입력하면 아래 3개의 카드가 게임에 등장한다.
각 캐릭터가 가지고 있는 고유 무기는 숨겨진 힘이 봉인되어 있다. 이 무기는 정상적으로 플레이하고 있어도 강화되지 않으므로 키워드 입력이 필요하다.
풀 보이스 스크립트
보드 게임, 주점 안, 배틀에서 모든 음성이 자막으로 표시
더미로 남아있는 베리알 음성을 활용하여 캐릭터 설렉트에서 L1 또는 R1버튼에 따라 추가 음성과 함께 대사 표시
디버그 모드 개발
오버레이 실행 코드가 있지만 일부 누락된 부분이 많으며 호출부가 존재하지 않아 새롭게 개발
디버그 카드 편집 모드
- “전투의 루프”는 코드 자체가 존재하지 않아 미구현
- UI룰 정상적으로 조립
- 기존에 없던 강화 카드들을 테이블에 추가하고 팔레트를 변경하여 확인이 용이하게 수정함
진입법: 배틀 카드를 사용후 칼이 꽃힌 후 암전 단계에서 L1을 누르고 유지한다.
사용법
- ○ 버튼으로 1p, 2p 이동
- ㅁ 버튼으로 카드 목록을 셔플
- L2, R2 버튼으로 “디버그 메시지 없음/보통/빠름” 이동
- L1, R1 버튼으로 “전투의 루프 없음/고정/변화” 이동
- △ 버튼으로 “변수 표시 없음/있음” 이동
- X 버튼으로 “랭크 A/B/C/D/E” 이동
디버그 배틀 모드
진입법: 배틀 화면에서 아무 카드를 누른후 R1을 누르고 유지한다.
- “L1 누르고 실행하면 크리티컬” 는 코드 자체가 존재하지 않아 미구현. 다만 이 부분은 크리티컬, 럭키 발생 코드가 먼지 대충 알아놨기 때문에 후에 구현할 수도 있다.
- 배틀에 필요한 구조체 테이블은 메모리 스왑 방식을 적용하여 일반 배틀과 디버그 배틀이 서로 독립적으로 동작하도록 구현했다. 따라서 기존 배틀 상태에 영향을 주지 않고 디버그 배틀을 테스트할 수 있으며, 종료 후에는 기존 상태로 복원된다.
더미 애니메이션 구현
이 게임은 사용하지 않는 더미가 매우 많다. 확인 해보니 TIM, ANM과 오버레이 실행 코드가 남아 있지만 실제 게임에서는 전혀 사용되지 않았다.
그래서 이걸 구현해보고 싶어서
실행 가능한 내부 함수와 애니메이션이 남아 있는 경우에는 원본 객체 생성 및 재생 흐름을 분석하여 새로운 진입 코드를 만들고 연결한 것이다. 출시 버전에서 사용되지 않던 스프라이트를 실제 전투에서 재생할 수 있도록 말이다 ㅎㅎ
다만 정말 슬프게도 페더 레이는 TIM과 ANM 일부만 있고 연출 전체를 제어하는 메인 컨트롤러가 구현되어 있지 않았다.
따라서 ANM 데이터를 이용할 별도의 메인 컨트롤러를 어셈으로 작성해야 하는데 이게 프로그래밍 실력을 떠나서 창작의 영역이였던 것.
아마 가장 시간이 걸리고 고된 작업이 아니였나 생각된다. 나눠진 셀 이미지를 반전하는것과 레이어 구현 방법도 꽤 힘든 일이였다.
구현된 결과
디버깅 중에 프레임과 좌푯값을 계속 조정하기 위해 아래처럼 라벨링화시켰다.
; ---------------------------------------------------------------------------
; 좌표 조정값
; 슬롯 기준 좌표는 16번 컨트롤러에서 사용하던 원본 좌표
; 로봇, 바닥 원, 광선의 보정값만 수정하면 연출 위치를 조절이 가능
; ---------------------------------------------------------------------------
.definelabel BLOCK9_SLOT0_X, -0xA8
.definelabel BLOCK9_SLOT0_Y, -0x70
.definelabel BLOCK9_SLOT1_X, -0x58
.definelabel BLOCK9_SLOT1_Y, -0xA0
; child4~5 로봇 좌표는 아래 block9_robot_position_offsets RAM 표에서 조정한다.
; 피격자 기준 child별 좌표 보정.
; child10~11은 기존 플로어 좌표, child12~21은 기존 빔 좌표를 기본값으로 사용한다. // 800c94e4 4바이트씩
.definelabel BLOCK9_CHILD10_X_OFFSET, 0x00
.definelabel BLOCK9_CHILD10_Y_OFFSET, 0x08
.definelabel BLOCK9_CHILD11_X_OFFSET, 0x00
.definelabel BLOCK9_CHILD11_Y_OFFSET, 0x08
.definelabel BLOCK9_CHILD12_X_OFFSET, 0x00
.definelabel BLOCK9_CHILD12_Y_OFFSET, -0x14
.definelabel BLOCK9_CHILD13_X_OFFSET, 0x00
.definelabel BLOCK9_CHILD13_Y_OFFSET, -0x14
.definelabel BLOCK9_CHILD14_X_OFFSET, 0x00
.definelabel BLOCK9_CHILD14_Y_OFFSET, -0x0F
.definelabel BLOCK9_CHILD15_X_OFFSET, 0x00
.definelabel BLOCK9_CHILD15_Y_OFFSET, -0x0F
.definelabel BLOCK9_CHILD16_X_OFFSET, 0x00
.definelabel BLOCK9_CHILD16_Y_OFFSET, -0x0F
.definelabel BLOCK9_CHILD17_X_OFFSET, 0x00
.definelabel BLOCK9_CHILD17_Y_OFFSET, -0x0F
.definelabel BLOCK9_CHILD18_X_OFFSET, 0x00
.definelabel BLOCK9_CHILD18_Y_OFFSET, -0x0F
.definelabel BLOCK9_CHILD19_X_OFFSET, 0x00
.definelabel BLOCK9_CHILD19_Y_OFFSET, -0x0F
.definelabel BLOCK9_CHILD20_X_OFFSET, 0x00
.definelabel BLOCK9_CHILD20_Y_OFFSET, -0x0F
.definelabel BLOCK9_CHILD21_X_OFFSET, 0x00
.definelabel BLOCK9_CHILD21_Y_OFFSET, -0x0F
; child4가 시작된 프레임을 0으로 본 각 child의 독립적인 시작 프레임
; 같은 값이면 동시에 시작하고, 이전 child 완료 전이면 서로 중첩
; 완료 후 마지막 스프라이트 유지 시간은 아래 HOLD_FRAMES 값으로 조절
; 0x800C956C
.definelabel BLOCK9_CHILD4_START_FRAME, 0x00 ; child4 시작
.definelabel BLOCK9_CHILD5_START_FRAME, 0x59 ; 0x38 완료 + 0x20 유지
.definelabel BLOCK9_CHILD10_START_FRAME, 0x48 ; child4 유지 중 바닥 원
.definelabel BLOCK9_CHILD11_START_FRAME, 0x4B ; 커지는 원
.definelabel BLOCK9_CHILD12_START_FRAME, 0x4E ; 처음 빛 줄기
.definelabel BLOCK9_CHILD13_START_FRAME, 0x57 ; 광선 시작
.definelabel BLOCK9_CHILD14_START_FRAME, 0x5D ; 같은 원 위
.definelabel BLOCK9_CHILD15_START_FRAME, 0x5D ; 같은 원 밑
.definelabel BLOCK9_CHILD16_START_FRAME, 0x63 ; 같은 원 위
.definelabel BLOCK9_CHILD17_START_FRAME, 0x63 ; 같은 원 밑
.definelabel BLOCK9_CHILD18_START_FRAME, 0x69 ; 같은 원 위
.definelabel BLOCK9_CHILD19_START_FRAME, 0x69 ; 같은 원 밑
.definelabel BLOCK9_CHILD20_START_FRAME, 0x6F ; 광선 폭파 및 피격
.definelabel BLOCK9_CHILD21_START_FRAME, 0x6F ; 광선 폭파 선행
; child4 완료 후 마지막 스프라이트를 0x20프레임 유지
; 유지 중 child10 이후 연출이 진행되고, 0x58에 child5로 전환
;0x800C95A4
.definelabel BLOCK9_CHILD4_HOLD_FRAMES, 0x20
.definelabel BLOCK9_CHILD5_HOLD_FRAMES, 0x10
.definelabel BLOCK9_CHILD10_HOLD_FRAMES, 0
.definelabel BLOCK9_CHILD11_HOLD_FRAMES, 0
.definelabel BLOCK9_CHILD12_HOLD_FRAMES, 0
.definelabel BLOCK9_CHILD13_HOLD_FRAMES, 0
.definelabel BLOCK9_CHILD14_HOLD_FRAMES, 0
.definelabel BLOCK9_CHILD15_HOLD_FRAMES, 0
.definelabel BLOCK9_CHILD16_HOLD_FRAMES, 0
.definelabel BLOCK9_CHILD17_HOLD_FRAMES, 0
.definelabel BLOCK9_CHILD18_HOLD_FRAMES, 0x15
.definelabel BLOCK9_CHILD19_HOLD_FRAMES, 0x15
.definelabel BLOCK9_CHILD20_HOLD_FRAMES, 0
.definelabel BLOCK9_CHILD21_HOLD_FRAMES, 0
이외 패치에 이용된 기술들
메모리 배치와 동적 할당 관리
이 게임은 malloc2 함수를 이용하여 동적으로 할당하고 해제한다. 고정 버퍼를 임시로 쓰기도 하는데 이 부분은 디버깅이 쉽지만, 매번 동적으로 달라지는 주소를 트레이싱하고 분석하기엔 정말 엄청나게 스트레스 였다. 아직도 완벽히 해결하지 못했으며 만약 이 재배치 방법이 성공한다면 엄청난 공간을 확보할 수 있을 것이다.
데드코드, 출시 후 필요 없는 디버깅 코드, bss, 고정 버퍼, 바이오스 콜 영역 모두 조각처럼 남는 공간이 많이 존재함을 알 수 있었다. 루아 스크립트를 작성해 남는 공간이 어디인지 추적해봤지만 확정적으로 볼 수가 없었으며 코드를 보고 완벽히 알아야 함을 꺠달았다.
무엇보다 포기 한 이유는 이제 이미 어는 정도 공간을 확보한 점과 애니메이션과 관련된 코드를 작성하면서 느낀 점인데
어떤 시점이던 항상 메모리에 고정적인 남는 공간을 확보하는 것보다. 훅하기 전 미리 파일 핸들 구조체만 초기화 코드에서 매인 메모리에 매핑 한 다음, 원할때 시디롬 루틴을 실행해서 필요한 데이터를 사용하는 것이다.
따라서 특정 시점에서 남는 공간만 분석하는게 훨씬 더 올바른 방법이였다.
한글 폰트 확장
일본어 폰트는 글리프 하나를 12×12 4bpp, Shift-JIS 코드와 대형 인덱스 테이블을 통해 접근한다. 기존 폰트와 직접 인덱싱으로 참조하는 테이블 영역을 정리하고, 한글 폰트를 12×12 2bpp로 변경했다. 렌더링 시에는 0x48바이트의 임시 버퍼와 새로운 코드가 2bpp를 4bpp 형식으로 변환한다. 숫자, 기호와 일부 아이콘은 원본 형식을 유지했다.
일반적인 Shift-JIS 문자열 처리 흐름과 훨씬 작은 Lookup Table를 활용하여 기존보다 많은 공간을 확보할 수 있었다.
문자열 추출과 ASM 기반 번역
실행 파일과 오버레이에는 문자열, 포인터 배열과 문자열 참조 명령이 직접 삽입되어 있다. 수작업으로 수정하면 주소 변경과 참조 누락이 발생하기 쉬우므로, 바이너리에서 번역 대상을 추출하여 Armips 소스로 관리하는 방식을 구축했다. 물론 이것도 하드 코딩이란 방식을 완화 시켜주는거지 피해가지는 못한다.
SLPS, PRGPACK.BDP, SCR_DATA.BDP와 DUELEF.BDP는 각각 독립된 패치 그룹으로 관리한다. BAT 파일을 만들어서 Armips 오류가 발생하면 해당 그룹의 빌드를 즉시 중단하도록 일괄 빌드 과정도 구성했다.
조사 패치
일본어는 목적격 조사를 모두 「を」로 처리하지만 한국어는 이름의 받침 여부에 따라 ‘을’과 ‘를’을 구분해야 한다.
카드나 건물, 캐릭터명은 한정되어 있기 때문에 전부다 구현하는 것보다 받침이 없는 이름의 주소를 테이블을 추려서 만드는 것이다. 아래 예시 코드를 보면 얼마나 쉽게 구현된 것인지 알 수 있다. 기존 하드코딩된 곳에 훅을 걸고 코드 케이브에서 이를 실행할때 만들어둔 테이블을 참조하고 복귀한다.
; 「%s」을/를 손에 넣었어
; delay slot에서 a2가 실제 카드 이름 포인터로 완성된다.
.org 0x8001E654
jal format_obtain_particle
addu $a2, $a2, $s0
; 기본 문장은 '을', 대체 문장은 '를'
format_obtain_particle:
la $a1, Text_036
la $t3, Text_036_1
j select_object_particle
nop
; a2: 실제 이름 포인터
; a1: 받침이 있을 때 사용할 문장
; t3: 받침이 없을 때 사용할 문장
select_object_particle:
la $t0, NameTable
@@scan:
lw $t1, 0x00($t0)
addiu $t0, $t0, 0x04
beqz $t1, @@call_formatter
nop
bne $a2, $t1, @@scan
nop
move $a1, $t3
@@call_formatter:
j 0x80067638
nop
.align 4
NameTable:
.word EventCard_Name_023 ; 가게파괴
.word EventCard_Name_037 ; 서커스
.word EventCard_Name_027 ; 떼어먹기
.word EventCard_Name_028 ; 배틀 카드
.word CardName_30 ; 대신 맞기
.word EventCard_Name_032 ; 묘지
.word EventCard_Name_040 ; 주사위 3개
.word EventCard_Name_041 ; 주사위 4개
.word CardName_31 ; C.F.C
.word CardName_32 ; 모 아니면 도
.word CardName_33 ; 해머
.word CardName_35 ; 롱 소드
.word CardName_36 ; 크로스보우
.word CardName_38 ; 썬더
.word CardName_0C ; 다트
.word CardName_0F ; 실드
.word CardName_13 ; 블러드 스피어
.word CardName_14 ; 페더 레이
.word CardName_15 ; 치유의 노래
.word CardName_16 ; 힘의 반지
.word CardName_17 ; 스톰 소드
.word CardName_18 ; 어스퀘이크
.word CardName_19 ; 도적의 오브
.word CardName_1B ; 마검 무라마사
.word CardName_1F ; 엑스칼리버
.word CardName_21 ; 다크 메테오
.word CardName_23 ; 강화 블러드 스피어
.word 0x00000000
Text_036:
.stringn "「%s」을 손에 넣었어."
.db 0x00
Text_036_1:
.stringn "「%s」를 손에 넣었어."
.db 0x00
하고 싶은 말
이 게임을 거의 10년 넘게 한글화하였다.. 난이도와 개인 시간 문제로 몇 번이고 포기했지만 결국에는 완료했다. 구현하고 싶은 부분이 많아서 계속해서 업데이트 할 예정이다.
AI를 이용해봤지만 그렇게 큰 도움이 되기 어려운 난이도 였고.. 아마 다시는 없을 손번역과 손코딩이 아닐까 싶다.
개인적으로 애니메이션 구현으로 인해 PSX1에 대한 지식과 실력이 상승 한 것 같아 만족스러웠던 작업이였다.
다만 AI 시대가 되면서 개인이 오랜 시간 쌓아온 기술과 경험이 예전보다 쉽게 여겨지는 것 같아 조금은 씁쓸하다. 조금만 더 일찍 복귀해서 이런 작업을 보여줬다면, 그 난이도나 과정 자체도 꽤 의미 있게 평가받을 수 있었을 텐데 하는 아쉬움이 든다.
그리고 매번 드는 생각이지만 무언가를 알고 있어도 다른 사람들이 쉽게 이해하기 위해 글로 풀어내는 것은 정말 힘든 일이다.
패치
- 게임: SLPS_020.75, GaiaMaster - Kamigami no Board Game (Japan).bin
- 원본 MD: 2c166df36e6b16f4e72fac8537b3f2423de02271
- 패치: xdelta 패치 파일