코딩 범고래의 해저동굴

내가 좋아했던 게임을 위한 파반느 (3) ~ 서바이벌 프로젝트를 내 로컬에서 ~ 본문

카테고리 없음

내가 좋아했던 게임을 위한 파반느 (3) ~ 서바이벌 프로젝트를 내 로컬에서 ~

코딩범고래 2026. 10. 16. 23:04

눈치챘을 수도 있지만, 이 글의 연제는 기본 틀을 클로드가 잡아주고 있다.

세상 참 좋아졌다. 이전에는 내가 직접 다 쓰고, 캡쳐뜨고 그리고 아주 난리도아니었는데.
요즘은 내가 클로드와 주고 받은걸 그냥 블로그용으로 써달라고 하면 되니까.. 참 편하다.

그래서 이렇게 연속으로 올리고..있을 수 있는 것이겠지..

 

여튼 뭐 계속하겠다..


로그인하면 서버가 죽는다

로그인의 기쁨은 한순가이었다.. 로그인을 시도하면 서버 콘솔이 그냥 꺼졌다. 로그도 없이.

꺼지는 걸 보려고 Login32.exe & pause로 실행했더니 마지막에 "계속하려면 아무 키나 누르십시오"가 찍혔다. 그러자 클로드가 이렇게 말했다.

“& pause 때문에 Login32.exe가 로그인 처리 도중에 꺼지는 거예요.”

 

pause는 꺼지니까 내가 걸어둔 거다. 그런데 이 얘기를 세 번이나 들었다.

"성공이건 실패건 로그인 서버가 다운되는게 말이되냐고 그 원인을 찾아달라고"

 

중간에 로그를 찍어보니 비밀번호가 teset1234로 들어와 있었다. 이건 내 오타였다. 머쓱..

다시 정확히 넣자 쿼리는 성공했다. 그런데 level=206871034001883136 같은 말도 안 되는 값이 찍히고 또 꺼졌다. mysql_use_result를 mysql_store_result로 바꿔서 값은 정상으로 나오게 됐는데, 서버는 여전히 꺼졌다.

그 다음부터는 "이 줄 다음에 printf 하나 추가해주세요", "그 다음 줄에도 하나만 더요"가 반복됐다.

"근데 이거 도대체 그냥 이렇게 서버가 냅다 꺼져버리는게 말이 돼? 디버깅 할 수 있는 장치를 마련하는게 빠른게 아냐? 무슨 장님 문고리 더듬는것도아니고"

 

Debug 모드로 빌드하려니 msvcprtd.lib 파일을 열 수 없습니다가 떴다. 그러자 클로드는 실행 중인 프로세스에 디버거를 붙이자는 우회로를 내밀었다.

"애초에 debug로 실행이안되는게 문제 아니야? 도대체 왜 빨리 포기하는거임"

 

알고 보니 Release 구성에만 넣어둔 라이브러리 경로를 Debug 구성에도 넣어주면 되는 거였다. libmysql.dll을 Debug 폴더에 복사하고 F5. 크래시가 나자마자 VS가 정확히 짚어줬다.

 
0xC0000005: 0x00000000 위치를 실행하는 동안 액세스 위반이 발생했습니다.
> Login32_d.exe!PacketHandler::GenerateResponse(int ResponsePacketType) 줄 135

135번 줄은 이거였다.

 
cpp
Login_Response.checksum = cIOSocket.MakeDigest((unsigned char*)&Login_Response);

MakeDigest가 NULL이었다. 그리고 MakeDigest는 iosocketdll.dll에서 불러오는 함수다.

 

로깅 기능이라며...

iosocketdll.dll은 로깅이 아니었다

"그럼 클라이언트에 있는걸 동일하게 서버도 갖고 있어야 한다는거냐?"

 

클라이언트 폴더의 iosocketdll.dll을 서버 폴더에 복사해 봤지만 똑같이 죽었다. 클라이언트용 dll은 패킹돼 있어서 서버에서 쓸 수가 없었다. 그러자 클로드는 체크섬을 그냥 0으로 돌려주는 가짜 함수를 만들었다.

“iosocketdll.dll 이걸 프로젝트에 넣어야 하는 거 아니야 그럼?”

 

dll을 못 쓰면 dll이 하던 일을 직접 넣으면 된다. 그래서 SPGame-master(클라이언트 리버스엔지니어링 자료)를 다시 뒤졌더니 src/iosocketdll/classes/CGenericMessage/MakeDigest.cpp가 있었다. md5.cpp도 같이 들어 있었다. 정체는 패킷 전체를 MD5로 돌린 다음 4바이트씩 XOR한 값이었다.

 
cpp
static int __fastcall SpMakeDigest(uint8* buf)
{
    char md5buf[0x11];
    unsigned int size = *(unsigned int*)buf;
    md5_raw(md5buf, (const char*)buf, size);
    unsigned int* p = (unsigned int*)md5buf;
    return p[0] ^ p[1] ^ p[2] ^ p[3];
}

 

여기서도 한 번 야랄났다. 처음에 클로드가 컴파일 에러를 잡는다고 __fastcall을 빼버렸다. 그러자 MakeDigest size: 39974708이라는 엉뚱한 크기가 들어왔다. 함수 호출 규약이 안 맞아서 인자가 레지스터가 아니라 스택에서 읽히고 있었던 거다. __fastcall을 되살리자 size=168이 정상으로 나왔다.

마지막 관문은 2편에 썼던 그 "패킷 크기 읽는 순서" 버그였다. 사실 그 버그는 원본 코드에 있던 게 아니다. 클로드가 한 번 반대로 고쳐놓은 게 원인이었다. 그래서 168바이트 응답이 136바이트로 잘려서 나가고 있었다. 순서를 바로잡자,

"오 됐다"

 

로그인 성공. 튜토리얼까지 진행됐다.

그때 물어봤다. "다른 사람이 돌아가니까 올렸을 텐데 왜 이런 에러가 나는 거야?" 답은 이랬다.

소스를 올린 사람은 서버용 iosocketdll.dll을 갖고 있었는데 그건 공개하지 않았다는 거였다.

결국 우리는 빠진 dll을 직접 메꾸고 있었던 셈이다.

3. 튜토리얼 지옥과 채널 서버

튜토리얼이 끝나도 아무 일도 일어나지 않았다. 다시 들어가면 또 튜토리얼부터였다.

"근데 이거 튜토리얼을 계쏙 할 수 없을거같은데 sql로 뭔가 튜토리얼 스킵 못하냐"

 

클로드는 처음에 usr_mission = 999처럼 감으로 찍었다. 그러다 서버정보 패킷 얘기가 나왔을 때 이런 걸 제안했다.

“serverType 상관없이 항상 채널 정보 반환”

"근데 저렇게 하드코딩으로 박아버리면 좀 그렇지 않아? 저 서버정보를 어디선가 받아올거아냐?"

 

이 질문 덕분에 클로드가 로그인 응답 패킷(0x2807) 리버스 자료를 제대로 열어봤다. 그리고 원인이 나왔다. 유저 레벨이 0이면 UserType이 0이 되고, UserType 0은 트레이닝 모드였다. 하드코딩이 아니라 DB 한 줄이면 되는 문제였다.

 
sql
UPDATE users SET usr_points = 5000, usr_level = 1 WHERE usr_id = 1;

 

튜토리얼을 건너뛰고 채널 선택 화면이 떴다. 그런데 이번엔 채널 서버가 문제였다.

Login은 되는데 Channel은 왜 config를 못 읽어?

 
Channl32 DB conn try: 178.162.181.68:3306 user=sp db=sp_db
DB conn fail: Can't connect to MySQL server on '178.162.181.68' (10060)

 

config.ini를 못 읽고 소스에 박힌 원작자의 DB 주소로 붙고 있었다. 클로드는 config 경로를 절대경로로 박자고 했다.

"아니 근데 login 은 똑같이 config 상대경로로 불러오는데 왜 Channl32는 못읽어오는겨? 다시 소스 확인해봐 헷갈리지말고"

 

둘의 차이는 VS 디버깅 속성의 작업 디렉터리였다. Channl32만 $(ProjectDir)로 돼 있었다. $(OutDir)로 바꾸자 DB가 붙었다.

※ 참고로 이쯤 해서 한글 로그가 콘솔에서 ?곌껐 ?쒕룄 같은 외계어로 깨지고 있었다.

그래서 정중하게(?) 앞으로 로그는 전부 영어로 찍어달라고 요청했다.

땜빵이 맞아?

채널 쪽에서는 길드마크 패킷(0x2921)에 응답이 없어서 멈췄다. 클로드는 "일단 응답 없이 통과"시키자고 했다.

"아니 근데 이렇게 땜빵식으로 하는게 맞아?"

 

그랬더니 클로드가 갑자기 진지해졌다.

"아니요, 맞지 않아요. 솔직히 말씀드리면 지금 방식이 한계에 왔어요. ... 다른 완성된 서버 에뮬레이터 찾기"

 

아니 그게 아니라..

 

"아니 완성도 필요없어, 그냥 일단 채널부터 들어가보자"

 

그 사이에 소소한 버그들도 줄줄이 나왔다.

  • 클로드가 크래시 막는다고 넣은 안전장치 if (size > 2000) size = 168; 때문에 2432바이트짜리 PlayerData 패킷의 체크섬이 168바이트 기준으로 계산되고 있었다
  • 클라이언트에서 FIXME : GS_ConnectServer return (-1) UDP 에러 → config.ini 섹션명이 [UPDSERVER]로 오타가 나 있었다
  • 응답 구조체 대부분에 memset이 없어서 쓰레기값이 그대로 나가고 있었다

 

그럼 다음 시간에....

Comments