어릴적, 초등학생 시절 부모님께서 크리스마스 선물로 사주신 파랜드택틱스 시리즈
나이가 들어도 몇번이고 완결을 봤었는데
파랜드 택틱스 1~3편과 다르게 파랜드택틱스4는 온갖 팅김과 버그로 제대로 클리어 해본적이 없었다
몇 가지 패치가 나왔다곤 하지만 그럼에도 팅기는 문제는 여전했고
게임 특성상 저장이 가능한 장소가 한정적이다 보니 레벨노가다 혹은 세이브포인트가 나올때까지 이동하다가 팅기면 의욕이 팍 꺾이곤했었다
많은 고민 끝에 exe파일을 뜯어 내 입맛대로 고쳐보기로 결정
여러가지 목적과 목표가 있었고, 성공했고, 완결을 볼수있었다!!
너무 많은 패치를 해서 모든게 기억나진않지만
1. F5 버튼을 통한 언제 어디서든 세이브 기능 (세이브 포인트 없이 언제든지 자유 세이브)
2. 경험치 배율 조정 (빠른 스토리 밀기 위해 노가다 구간 스킵)
3. 백지도 100% 패치 (맵 마다 백지도 사용시 100%)
4. 능력치 에디터
5. 아이템 에디터
6. 팅김 방지 패치 (완벽하게 잡을순 없었지만 완결을 볼때까지 10회 미만으로 팅김)
우선 회고에 앞서 환경을 제공해준 두기님께 무한한 감사를 드립니다.
이번에 진행한 패치중 가장 인상깊었던 부분은 원작의 제한된 세이브 포인트 구조를 우회하지 않고 파랜드택틱스4의 세이브/로드 구조와 FOS 저장 구조를 역분석하여 특정 핫키를 통해 원작 슬롯 UI 호출 및 로드 시 원작이 하드코딩한 복귀 지점을 제거하여 세이브 파일에 기록된 실제 맵 좌표 방향으로 복원되도록 수정한 것이다.
다행히 두기님의 배포본을 찾을수 있었고, 해당 파일을 분석해본 결과 두기 배포본은 도스박스 SVN위에서 윈도우95를 부팅한뒤 FAT16 디스크 이미지 (image.img)를 마운트하여 FT4.EXE를 실행하는 구조였다.
덕분에 런처나 도스박스 자체를 교체하기보다 image.img 내부 FT4.EXE 패치하는 방법을 채택했다.
처음 시도는 자유 세이브 로드와 팅김 방지 패치였다.
팅기는 구간을 잡아내려해도 많은 이벤트와 오류가 엉켜있어 전부 잡을수가 없어 최소한의 패치만 진행하려는 계획이었기 때문에
팅겨도 문제가 없도록 (전투 후 언제든 세이브하면 되니) 세이브/로드 구간과 팅기는 구간을 먼저 분석해 보았다.
초기 분석 결과 DirectDraw 자체보다 파일 I/O, 이벤트 스크립트 VM, 리소스 로더, UI 인덱스 검증이 더 위험해 보였다.
특히 이벤트 스크립트 로더의 CreateFileA 실패 판정은 코드 수준에서 명백한 결함이 보였다.
1.1 CreateFileA 실패값 판정 오류
이벤트 .SRC 파일을 열지 못해도 실패를 성공으로 오인할 수 있었고, 이후 ReadFile 및 이벤트 실행이 계속되며 잘못된 버퍼상태를 사용할수도 있어보여 실패값 판정, ReadFile 성공 여부와 실제 읽은 길이, 파일 크기 일치 여부를 함께 검사하도록 수정했다.
원본 개념 코드:
HANDLE h = CreateFileA(...);
if (h == 0) { //잘못된 검사
return 0;
}
Windows 의 실제 실패값 :
I NVALID_HANDLE_VALUE == (HANDLE) -1 == 0xFFFFFFFF
1.2 안정화 수정 범위
- 이벤트 해더 0x408 bytes의 정확한 읽기 검증 및 선언 크기/실제 파일 크기 검증
- 이벤트 본문 ReadFile 실패 시 메모리 해제 포인터 초기화 후 안전한 실패 반환
- 이벤트 로더 반환값을 무시하던 상위 호출부 수정
- 이벤트 block index 0..255 범위와 script code pointer NULL 방어
- 리소스 로더의 CreateFileA / GlobalAlloc / ReadFile 실패 처리 추가
- 2x2 선택 UI에서 마우스 hit ID 가 정상 범위를 벗어날 때 무시하도록 3개 루틴 보강
- 원본 .text의 미사용 raw 영역에 444bytes의 안전검사 코드 주입, 실행파일 크기는 유지
1.3 FOS 세이브 포맷 역분석
강제세이브를 구현하기 위해 처음부터 세이브 포맷을 새로 만드는 대신 원작의 저장 UI와 저장 함수를 재사용
여기서 약간의 문제가 발생했는데, UI구현과 저장기능을 완료했지만 어째서인지 로드만 하면 다른 위치혹은 맵 바깥에서 로드되는 문제가 발생하였다.
원인은 생각보다 간단했는데도 불구하고 원인을 찾아내는데 상당한 시간이 걸렸음
7~8번 패치를 진행하며 발견한것은 슬롯 명칭과 로드 경로는 정상이지만 실제 로드 위치는 내가 저장한 위치가 아닌 각 맵 인덱스의 '여관', '호텔', '세이브포인트' 로 강제된것을 발견
이 패턴이 'Save Point = Resume Point' 구조를 강하게 시사했고, 실제 디스어셈블 결과 하드코딩된 복귀 좌표를 발견했다.
1.4 원작 로더의 하드코딩된 복귀 시점
context = 1 로드 경로를 추적한 결과, 서로 다른 세곳의 디스패치 코드가 동일한 하드코딩을 수행하고 있었다.
즉 세이브 파일에 실제 X/Y가 들어있어도 최종 맵 초기화 단계에서 이를 덮어쓰고 있었음
원작의 context == 1 로드 로직
if(saved_map == 75){
SetMapSpaws(
map = 75,
x = 0x590, // 1424
y = 0x450, // 1104
z = 0,
dir = 1
);
}
else{
SetMapSpaws(
map = 8,
x = 0x2F0, // 752
y = 0x270, // 624
z = 0,
dir = 1
);
}
이 한 덩어리의 로직이 모든 현상을 설명했다. map 75에서는 항상 여관의 같은 위치로 돌아가고, 그 외 context =1 저장은 map 8의 지정 좌표로 강제 되었음.
그래서 저장 기능은 FOS에서 복원한 live party 구조체를 이용하여 context = 1 하드코딩 분기 세곳을 공통 helper로 보내고, helper가 저장된 map ID와 리더 X/Y/Z 방향을 SetMapSpawn()에 그대로 전달한다.
call 0x5D36E0
call 0x63A00D ; saved map ID
push [0x1ADDC90]; direction
push [0x1ADDC8c]; Z
push [0x1ADDC88]; Y
push [0x1ADDC84]; X
push eax ; map ID
call 0x5D3580 ; SetMapSpawn(map, x, y, z, dir)
add esp, 0x14
ret
1.5 최종 사용 방식
- F5 버튼 핫 키 등록
- F5 버튼을 통해 원작 SAVE 슬롯 UI 호출
- 슬롯 선택
- SAVE
- LOAD 시 맵 별 고정 좌표가 아닌 저장된 saved map + lader X/Y/Z 실제 위치를 spawn 위치로 전달
1.6 결과


세이브 포인트가 없는 위치에서 강제 세이브 기능 완료~!
2.1 패처 (백지도 및 경험치 배율, 에디터 등)

위와 같은 형태로 만들었으며 스토리를 빠르게 밀기위해 호다닥 제작
중요한건 팅김 방지 패치와 강제 세이브 기능이므로 위 기능 제작 과정은 생략합니다
귀찮음 이슈
3.1 아무튼 결론
사심을 채우기 위한 프로젝트 였는데 생각보다 재미있게 작업한것 같다.
파랜드 택틱스4의 FOS는 최소한 핵심 상태를 평문 구조체에 가까운 형태로 저장하고 있고, 위치 좌표도 세이브파일에 존재해서 가능한 작업이었음. 지정된 장소에서만 저장 제약은 UI 제한만이 아니라 로더 설계와 연결 되어있는 부분이 재미있었다.
강제 세이브 기능만 추가해야지 헤헤 라는 내 얕은 생각을 박살낸 로더 설계...하... 이것땜 삽질한거 생각하면 증말...
아무튼 로드 마지막 단계의 spawn 결정을 함께 수정하여 해결할 수 있어서 다행이다.
7~8번의 패치를 진행하면서 실패한 경험들이 context, state machine, 파티 snapshot, 하드코딩 spawn의 역할을 구분하는데 핵심적인 데이터가 되었음
게임을 재구현한다거나 FOS 포맷을 새로 작성하지 않고 원작 UI/직렬화/맵 초기화 함수를 재사용하여 비교적 작은 바이너리 패치로 기능 확장 및 수정에 성공할 수 있어서 재미있는 경험이었다.


아주 재미있었다!!헤헤헿
드디어 완결봤다 ㅠㅠ