본문 바로가기
FrostK

읽는 데 6분

화이트데이1 언팩

AI 생성 일러스트: 학교 복도 모형과 텍스처 조각이 디지털 자료 상자에서 펼쳐지는 일러스트

화이트데이의 NOP 파일에 들어 있는 리소스를 살펴보기 위해 참고했던 언팩 소스와 추출 이미지를 정리합니다. 코드의 제작자와 참고 주소는 원문 표기를 유지했습니다.

코드를 읽을 때는 데이터 타입을 구분하는 e_noptype과 nop_unpack 함수를 먼저 보면 흐름을 따라가기 쉽습니다. 이 소스는 RAW 데이터, 디렉터리, 두 종류의 LZ77 데이터를 나누어 처리하고, 파일 정보와 경로를 읽은 뒤 결과 파일을 기록합니다.

코드에도 적혀 있듯이 게임이 설치된 폴더에서 바로 실행하면 문제가 생길 수 있습니다. 원본을 보관하고 별도 작업 폴더에서 결과를 살펴보는 용도로 접근하는 편이 좋습니다. 아래 이미지는 MAP, MODEL, IMAGE 등의 리소스를 확인했던 기록입니다.

url

CODE

/**
* Whiteday nop file Unpacker
* 다솜
* 본 코드를 사용하여 발생된 문제는 사용자에게 있습니다.
**/
//
// Lz77 알고리즘을 알려주신 단풍잎님 감사합니다 ^^
// 기타 나머지는 리버스 엔지니어링으로 알아내었습니다.
//
#include <stdio.h>
#include <string.h>
#include <direct.h>
#include <windows.h>
#include <conio.h>
#define BUFFER_SIZE (1024 * 1024 * 256)
/**
* BIG-SIZE buffer
**/
unsigned char *buff1;
unsigned char *buff2;
/**
* SONNORI Lz77 Xor Key
**/
unsigned short lz77_customkey[] = {
    0xFF21, 0x834F, 0x675F, 0x0034, 0xF237, 0x815F, 0x4765, 0x0233
}; //65313 33615 26463 52 62007 33119 18277 563
//
// NOP file unpack~~~ *^^*
//
void nop_unpack(const char *nopFile)
{
    enum e_noptype {
        NOP_DATA_RAW = 0x00,
        NOP_DATA_LZ77 = 0x01,
        NOP_DATA_DIRECTORY = 0x02,
        NOP_DATA_SONNORI_LZ77 = 0x03
    };
    FILE *fp, *out_fp;
    int off, num, i, j;
    unsigned char key;
    fopen_s(&fp, nopFile, "rb");
    if (!fp)
        return;
    /* the last byte value of NOP file must be 0x12 */
    fseek(fp, -1, SEEK_END);
    if (fgetc(fp) != 0x12) {
        printf_s("* %-15s : 손상된 NOP 파일입니다.\\n", nopFile);
        fclose(fp);
        return;
    }
    /* NOP file header */
    fseek(fp, -9, SEEK_END);
    fread(&off, 4, 1, fp);
    fread(&num, 4, 1, fp);
    printf_s("* %-15s : 총 %d개의 데이터를 포함하고 있습니다.\\n", nopFile, num);
    for (i = 0; i < num; ++i) {
        unsigned char   name[256];
        unsigned char   name_size;
        unsigned char   type;
        int             offset;
        int             encode_size;
        int             decode_size;
        /* File infomation */
        fseek(fp, off, SEEK_SET);
        fread(&name_size, 1, 1, fp);
        fread(&type, 1, 1, fp);
        fread(&offset, 4, 1, fp);
        fread(&encode_size, 4, 1, fp);
        fread(&decode_size, 4, 1, fp);
        fread(name, 1, name_size + 1, fp);
        off += name_size + 15;
        /**
        * [ Data Type ]
        * 0x00 : Raw
        * 0x01 : Lz77 Compressed
        * 0x02 : Directory
        * 0x03 : SONNORI Lz77 Compressed
        **/
        /* In directory, this value used as file path decrypt key */
        if (type == NOP_DATA_DIRECTORY) {
            key = (char)decode_size;
        }
        else {
            decode_size ^= key;
        }
        /* File path decrypt */
        for (j = 0; j < name_size; ++j)
            name[j] ^= key;
        /* we doesn't have enough buffer size. so we skip this file. */
        if (decode_size > BUFFER_SIZE) {
            printf_s(" -> 실패 : %s ? ? ?\\n", name);
            continue;
        }
        /* show current status with percent^^ */
        printf_s("* %0.1f%% : %-68s\\r", ((float)i / (float)num * (float)100.0), name);
        switch (type) {
        case NOP_DATA_RAW:
        {
            fseek(fp, offset, SEEK_SET);
            fread(buff1, 1, encode_size, fp);
            fopen_s(&out_fp, name, "wb");
            if (!out_fp) {
                printf_s(" -> 실패 : %s ? ? ?\\n", name);
                break;
            }
            fwrite(buff1, 1, decode_size, out_fp);
            fclose(out_fp);
            break;
        }
        case NOP_DATA_LZ77:
        {
            int bmask, bcnt = 0, size = 0, off, len;
            unsigned short Lz77Info;
            fseek(fp, offset, SEEK_SET);
            fread(buff1, 1, encode_size, fp);
            /* Lz77 uncompress */
            for (j = 0; j < encode_size; bcnt = (bcnt + 1) & 0x07)
            {
                if (!bcnt) {
                    bmask = buff1[j++];
                } else {
                    bmask >>= 1;
                }
                if (bmask & 0x01) {
                    Lz77Info = *(unsigned short *)&buff1[j], j += 2;
                    off = Lz77Info & 0x0FFF;
                    len = (Lz77Info >> 12) + 2;
                    memcpy(&buff2[size], &buff2[size - off], len);
                    size += len;
                } else {
                    buff2[size++] = buff1[j++];
                }
            }
            if (size != decode_size) {
                printf_s(" -> 실패 : %s ? ? ?%d != %d\\n", name, size, decode_size);
                break;
            }
            fopen_s(&out_fp, name, "wb");
            if (!out_fp) {
                printf_s(" -> 실패 : %s ? ? ?\\n", name);
                break;
            }
            fwrite(buff2, 1, decode_size, out_fp);
            fclose(out_fp);
            break;
        }
        case NOP_DATA_DIRECTORY:
        {
            _mkdir(name);
            break;
        }
        case NOP_DATA_SONNORI_LZ77:
        {
            int bmask, bsrcmask, bcnt = 0, size = 0, off, len;
            unsigned short Lz77Info;
            fseek(fp, offset, SEEK_SET);
            fread(buff1, 1, encode_size, fp);
            /* SONNORI Lz77 uncompress */
            for (j = 0; j < encode_size; bcnt = (bcnt + 1) & 0x07)
            {
                if (!bcnt) {
                    bmask = bsrcmask = buff1[j++];
                    bmask ^= 0xC8;
                }
                else {
                    bmask >>= 1;
                }
                if (bmask & 0x01) {
                    Lz77Info = *(unsigned short *)&buff1[j], j += 2;
                    Lz77Info ^= lz77_customkey[(bsrcmask >> 3) & 0x07];
                    off = Lz77Info & 0x0FFF;
                    len = (Lz77Info >> 12) + 2;
                    memcpy(&buff2[size], &buff2[size - off], len);
                    size += len;
                }
                else {
                    buff2[size++] = buff1[j++];
                }
            }
            if (size != decode_size) {
                printf_s(" -> 실패 : %s ? ? ?%d != %d\\n", name, size, decode_size);
                break;
            }
            fopen_s(&out_fp, name, "wb");
            if (!out_fp) {
                printf_s(" -> 실패 : %s ? ? ?\\n", name);
                break;
            }
            fwrite(buff2, 1, decode_size, out_fp);
            fclose(out_fp);
            break;
        }
        default: // unknown type
            printf_s(" -> 실패 : %s ? ? ?\\n", name);
            break;
        }
    }
    fclose(fp);
    return;
}

int main(int argc, char *argv[])
{
    /* 콘솔 속성 변경 */
    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY);
    /* 인트로 표시 */
    SetConsoleTitle("NOPUnpack - http://c...content-available-to-author-only...r.com/wdlabyrinth");
    printf_s("o------------------------------------------------------------------------------o");
    printf_s("| W h i t e D a y :: A Labyrinth named school ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?|");
    printf_s("o------------------------------------------------------------------------------o");
    printf_s(" 네이버 카페 [화이트데이 게임 - 미궁의 열쇠] http://c...content-available-to-author-only...r.com/wdlabyrinth\\n");
    printf_s(" 제작자: 다솜(Naver ID - love947345, MSN - eunju...@hotmail.com)\\n");
    printf_s("o------------------------------------------------------------------------------o");
    printf_s(" NOP파일을 풀어줍니다..^^\\n");
    printf_s(" 화이트데이가 설치된 폴더에서 실행하시면, 게임실행에 문제가 생길 수 있습니다.\\n");
    printf_s(" 계속 하실려면 아무키나 누르세요.\\n");
    printf_s("o------------------------------------------------------------------------------o");
    _getch();
    /* 버퍼 메모리 할당 */
    if (!(buff1 = (unsigned char *)malloc(BUFFER_SIZE)) || !(buff2 = (unsigned char *)malloc(BUFFER_SIZE))) {
        printf_s("* 메모리 할당에 실패하였습니다.\\n");
        getchar();
        return 0;
    }
    char path[256];
    int i;
    /* 데모버전 NOP 파일 */
    nop_unpack("whiteday.nop");
    /* 데모버전 NOP 패치 파일 */
    for (i = 100; i < 999; ++i) {
        sprintf_s(path, sizeof(path), "patch%02d.nop", i);
        nop_unpack(path);
    }
    /* 정식버전 NOP 패치 파일 */
    for (i = 100; i < 999; ++i) {
        sprintf_s(path, sizeof(path), "whiteday%03d.nop", i);
        nop_unpack(path);
    }
    /* 오!재미 NOP 파일*/
    for (i = 99; i < 999; ++i) {
        sprintf_s(path, sizeof(path), "mod_beanbag%03d.nop", i);
        nop_unpack(path);
    }
    /* 메모리 해제 */
    free(buff2);
    free(buff1);
    printf_s("o------------------------------------------------------------------------------o");
    printf_s(" 모든 작업이 끝났습니다. 종료하실려면 엔터키를 누르세요.\\n");
    getchar();
    return 0;
}

MAP

화이트데이1 언팩 관련 이미지 1

MODEL

화이트데이1 언팩 관련 이미지 3

IMAGE

화이트데이1 언팩 관련 이미지 4

HL1

화이트데이1 언팩 관련 이미지 5

HL2

화이트데이1 언팩 관련 이미지 6