KOR세상

블로그

  • 고전 콘솔게임용 만능도구 유콘64(uCON64)

    오늘 소개할 툴은 고전게임 롬파일계의 맥가이버라는 별명을 가진 만능도구 유콘64(uCON64) 입니다

    툴 자체의 기능이 꽤 방대하다보니 전부 소개하기는 어려운데 그나마 수퍼패미컴 한국어화에 관련된 사례만 하나 콕찝어서 소개하고자 합니다

    참고로 지원하는 기종은 아래와 같습니다

    • 아타리 시리즈
    • 코레코비전
    • 패미컴
    • 수퍼패미컴
    • 버추얼보이
    • 게임보이
    • 게임보이어드밴스
    • 닌텐도64
    • 닌텐도DS
    • 네오지오
    • 네오지오 포켓
    • 세가 마스터시스템
    • 세가 메가드라이브
    • 세가 드림캐스트
    • PC엔진
    • 플스1
    • 원더스완

    툴의 자세한 기능은 같이 배포되는 readme.html이나 faq.html을 참고해주시면 될거 같습니다

    툴에 기능이 방대한데다가 구동환경도 리눅스, 윈도우 가리지 않아서 그런지 모르겠지만 아쉽게도 기본적으로 GUI를 지원하지 않습니다

    별도로 공식웹에서 배포되는 프론트엔드 GUI가 있긴하지만 저는 제대로 작동하지 않아서 포기하고 그냥 커맨드라인 형태로 쓰기로 했습니다

    우선 공식배포처부터 소개하고자 합니다

    공식 사이트 URL – https://ucon64.sourceforge.io/

    공식배포처에는 MinGW버전이나 Cygwin, 리눅스 버전도 배포하고 있으니 원하시는 버전이 있으시면 가급적 공식배포처에서 내려받으시는걸 권장합니다

    제가 올려둔 버전은 윈도우 플랫폼용 비주얼C++버전 입니다

    해당 버전은 비주얼C++ 재배포 패키지가 실행에 필요할수도 있습니다

    해당 포스팅에서는 제가 한국어화를 진행하고자 하는 프론트미션 건하자드를 예시로 사용법을 조금 알려드리고자 합니다

    우선 프론트미션 건하자드에는 불법복제 방지용 조치가 취해져 있는 게임입니다.

    패미컴시절 불법복제 롬팩(게임팩 하나에 게임이 64개 혹은 124개가 들어있는 게임팩을 기억 하시는분도 계실겁니다. 현대컴보이 시절 사용해보신 경험들이 있으실텐데요… 사실 그거 전부다 불법복제 게임팩이였던거죠)에 게임회사들이 시달렸었는데 그러다보니 불법복제 방지용 락도 발전을 했었습니다.

    그 예시중 하나가 프론트미션 건하자드인데요. 게임팩의 내용물이 조금이라도 변경되면 아예 사용 못하게끔 조치가 되어있습니다

    그러다보니 당연히 한국어화를 하기위해서 롬에 한국어폰트를 그려넣는 순간 다음과 같은 화면을 보게됩니다

    카세트가 망가져있습니다

    스퀘어 로고까지는 정상적으로 출력되는데 그다음에 게임의 오프닝이 안나오고 저런 메세지가 출력되면서 게임 플레이가 정상적으로 되지않는 문제가 생겨버립니다

    이 문제를 해결해줄 툴이 바로 오늘 소개할 유콘64 입니다

    이 툴에는 불법복제 방지를 위한 프로텍션을 깰수있는 기능이 있습니다

    그럼 바로 한번 시도해 보도록 하겠습니다

    우선 유콘64를 특정 폴더에 압축을 풀어줍니다 GUI가 지원되지 않아 윈도우 명령프롬프트를 통해서 프로그램을 실행시켜야 하기때문에 가급적 경로를 짧게 해주시는게 사용하시기 편할 겁니다

    그리고 불법복제 락을 풀어줄 롬파일도 같은 폴더에 넣어줍니다

    저는 프론트미션 건하자드를 가지고 시도해 볼껍니다

    그러면 대충 요렇게 배치되게 됩니다. fmgh.sfc가 락을 풀어줄 롬파일 입니다

    이제 명령프롬프트를 실행시켜 줍니다 윈도우키를 누르고 cmd 라고 입력해주면 위 화면과 같이 명령 프롬프트를 쉽게 선택할수 있습니다

    일단 명령어를 입력해서 프로그램을 실행하기전에 필요한 명령어를 한번 확인해 보겠습니다

    프로그램과 함께 배포되는 readme.html을 열어보면 명령어 종류와 설명이 적혀있습니다

    -k 명령어를 사용하면 된다고 나와있네요

    그럼 이제 명령 프롬프트에 직접 입력해보죠

    명령 프롬프트 사용에 익숙하신분은 금방 쉽게 쓰시겠지만 그렇지 않으신경우에는 조금 고생하실수도 있을 겁니다

    조금 설명을 드리자면 cd 라는 명령어는 특정 경로로 이동할수있는 명령어 입니다

    저는 E 드라이브에 해당 경로가 있다보니 먼저 E: 를 입력해서 드라이브를 바꿔주고 cd 명령어로 경로 이동을 해줬습니다

    E:
    cd KorPatchUtils\ucon64

    그런 다음에 다음과 같이 입력해 줍니다

    ucon64.exe fmgh.sfc -k

    한번 해보면 생각보다 간단할겁니다

    그리고 이 프로그램의 특징이라면 롬파일에 대한 체크섬을 진행을 합니다

    일단 불법복제 락을 걷어내면서 롬파일에 체크섬이 변경되게 되는데 해당 롬파일에 동일한 작업을 수행해주면 다음과 같이 체크섬 항목에 Bad가 뜨게 됩니다

    롬파일에 락을 걷어내기전에 수정작업을 해줘도 마찬가지로 체크섬이 변경되면서 Bad가 뜨게 되는데 그래도 락 해제는 정상적으로 진행되므로 신경쓰지 않으셔도 됩니다

    그럼 한번 제대로 락이 해제됫는지 확인해 보겠습니다

    yychr에서 숫자 2 폰트칸에 다음과같이 7 처럼 바꿔주겠습니다

    보면 네모칸 친자리에 폰트가 정상적으로 출력되지 않는걸 볼수 있습니다

    원본 롬을 보면 이렇게 정상적으로 숫자 21이 표시되어야 하지만 수정을 잘못해줘서 폰트 자체가 아예 표시가 정상적으로 되지 않네요

    그렇지만 롬 내용변경으로 인해 처음처럼 카세트가 망가져있습니다는 에러메세지는 출력되지 않고 정상적으로 게임이 진행되는걸 볼수 있습니다

    이렇게 한국어화 작업을 정상적으로 해주려면 락을 풀어줘야 하는 게임들이 몇몇 있습니다

    대표적으로 록맨X1도 그런 게임중 하나라고 하네요

    참고로 루나 익스팬드로 롬을 확장시켜주는 것 또한 롬 내용물 변동으로 치기때문에 카세트가 망가져있습니다는 에러를 표시하게 됩니다

    따라서 루나 익스팬드를 쓰건 롬 내에 폰트를 수정을 하건 어떻게든 게임 내용물에 변동이 생겨도 정상적으로 작동하게 하려면 유콘64는 필수적으로 사용을 해주셔야 합니다

  • 파일 해시섬 프로그램 소스코드

    파일 해시섬 프로그램 소개 페이지

    WndProcs.h

    #ifndef _WNDPROCS_H_
    #define _WNDPROCS_H_
    #include <Windows.h>
    #include <gdiplus.h>
    #include <commctrl.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <malloc.h>
    #include <wincrypt.h>
    #include <bcrypt.h>
    #include <iostream>
    #include <vector>
    #pragma comment(lib, "Gdiplus.lib")
    #pragma comment(lib, "comctl32.lib")
    #pragma comment(lib, "advapi32.lib")
    #pragma comment(lib, "bcrypt.lib")
    #pragma comment(linker, "/SUBSYSTEM:WINDOWS")
    #define IDC_PROGRESSBAR 100
    #define IDC_FILEPATHBT 101
    #define IDC_CRC32BT 102
    #define IDC_MD5BT 103
    #define IDC_SHA1BT 104
    #define IDC_SHA256BT 105
    #define IDC_SHA512BT 106
    #define IDC_CHECKBT 107
    #define IDC_HASHCOMPAREBT 108
    #define MD5BUFSIZE 1024
    #define MD5LEN 16
    #define SHA1BUFSIZE 32768
    #define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
    #define SHA256BUFSIZE 65536
    #define SHA512BUFSIZE 65536
    
    using namespace Gdiplus;
    
    extern HINSTANCE g_hInstance;
    extern ULONG_PTR gdiplusToken;
    extern Image* pImage;
    
    extern HWND hFilePathLabelWnd, hFilePathEditWnd, hFilePathButtonWnd;
    extern HWND hHashInputLabelWnd, hHashCompareInputWnd, hHashCheckButtonWnd;
    extern HWND hCrc32LabelWnd, hCrc32CopyButtonWnd, hCrc32TextEditWnd;
    extern HWND hMd5LabelWnd, hMd5CopyButtonWnd, hMd5TextEditWnd;
    extern HWND hSha1LabelWnd, hSha1CopyButtonWnd, hSha1TextEditWnd;
    extern HWND hSha256LabelWnd, hSha256CopyButtonWnd, hSha256TextEditWnd;
    extern HWND hSha512LabelWnd, hSha512CopyButtonWnd, hSha512TextEditWnd;
    extern HWND hOkButtonWnd;
    extern HWND hProgressbarWnd;
    
    LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam);
    #endif

    WinMain.cpp

    #include "WndProcs.h"
    
    LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam);
    
    LPCTSTR lpszWndClass = TEXT("파일 해시섬 프로그램 Ver 2.0 - 공식배포처 https://korworld.kr");
    HINSTANCE g_hInstance;
    ULONG_PTR gdiplusToken;
    Image* pImage = NULL;
    
    int APIENTRY WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpszCmdParam, _In_ int nCmdShow)
    {
    	GdiplusStartupInput gdiplusStartupInput;
    	GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
    
    	HWND hWnd;
    	MSG Message;
    	WNDCLASSEX WndClassEx;
    
    	g_hInstance = hInstance;
    
    	WndClassEx.cbSize = sizeof(WndClassEx); // 구조체 크기를 RegisterClass로 전달하기 위한 멤버이며 보통 값은 자기 자신의 크기를 넘겨주면 됨
    	WndClassEx.cbClsExtra = 0; // 윈도우 클래스 레벨에서 추가적인 여분 메모리 할당용. 보안상 이슈로 거의 사용되지 않고 0으로 두는 경우가 많음
    	WndClassEx.cbWndExtra = 0; // 윈도우 인스턴스마다 추가적으로 할당할 여분의 메모리 할당용. 각 윈도우별로 별도 할당되고 윈도우별로 특별한 데이터를 저정하기 위한 예약된 메모리 공간. 이쪽도 그렇게 잘 사용되지는 않음. 프로젝트 규모가 크거나 할때는 어느정도 쓰이나 규모가 작거나 복잡도가 낮은경우에는 거의 쓰일 일이 없음
    	WndClassEx.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    	WndClassEx.hCursor = LoadCursor(NULL, IDC_ARROW);
    	WndClassEx.hIcon = LoadIcon(NULL, NULL);
    	WndClassEx.hIconSm = LoadIcon(NULL, NULL);
    	WndClassEx.hInstance = hInstance;
    	WndClassEx.lpfnWndProc = WndProc;
    	WndClassEx.lpszClassName = lpszWndClass;
    	WndClassEx.lpszMenuName = NULL;
    	WndClassEx.style = CS_VREDRAW | CS_HREDRAW;
    	RegisterClassEx(&WndClassEx);
    
    	hWnd = CreateWindowEx(WS_EX_WINDOWEDGE | WS_EX_DLGMODALFRAME, lpszWndClass, lpszWndClass, WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU,
    		CW_USEDEFAULT, CW_USEDEFAULT, 1280, 720, 
    		NULL, (HMENU)NULL, g_hInstance, NULL);
    
    	ShowWindow(hWnd, nCmdShow);
    	UpdateWindow(hWnd);
    
    	while (TRUE)
    	{
    		if (PeekMessage(&Message, NULL, 0, 0, PM_REMOVE))
    		{
    			if (Message.message == WM_QUIT)
    			{
    				break;
    			}
    			else
    			{
    				TranslateMessage(&Message);
    				DispatchMessage(&Message);
    			}
    		}
    		else
    		{
    			WaitMessage();
    		}
    	}
    
    	GdiplusShutdown(gdiplusToken);
    
    	return (int)Message.wParam;
    }

    WndProc.cpp

    #include "WndProcs.h"
    
    /// CRC32를 포함 각종 파일 해시섬 로직들은 AI(구글 제미나이)에 의해 작성되어진 코드 입니다
    /// 일부 AI에 의한 오류나 윈도우 UI환경에 맞추는 부분정도만 직접 진행하였으므로
    /// 제가 전부 이해하고 있는 상태가 아닙니다
    /// 따라서 파일 해시섬 로직에 대한 궁금증에 대해서는 아쉽지만 AI에 문의해 주시기 바랍니다
    
    /// CRC32 파일 해시섬 함수들 형선언 시작
    void makeCRCtable(unsigned long *, unsigned long);
    unsigned long calcCRC(const unsigned char *, signed long, unsigned long, unsigned long *);
    LPCWSTR getFileCrc(FILE* s);
    /// CRC32 파일 해시섬 함수들 형선언 끝
    
    // 윈도우 핸들러
    HWND hFilePathLabelWnd, hFilePathEditWnd, hFilePathButtonWnd;
    HWND hHashInputLabelWnd, hHashCompareInputWnd, hHashCheckButtonWnd;
    HWND hCrc32LabelWnd, hCrc32CopyButtonWnd, hCrc32TextEditWnd;
    HWND hMd5LabelWnd, hMd5CopyButtonWnd, hMd5TextEditWnd;
    HWND hSha1LabelWnd, hSha1CopyButtonWnd, hSha1TextEditWnd;
    HWND hSha256LabelWnd, hSha256CopyButtonWnd, hSha256TextEditWnd;
    HWND hSha512LabelWnd, hSha512CopyButtonWnd, hSha512TextEditWnd;
    HWND hOkButtonWnd;
    HWND hProgressbarWnd;
    
    // 파일 처리
    OPENFILENAME OpenFileName;
    
    // 프로그레스바 윈도우 진행도 0 ~ 100
    int progressStatus = 0;
    
    // 파일 경로 및 파일 이름+확장자
    TCHAR lpstrFile[MAX_PATH] = TEXT("");
    TCHAR lpstrFileTitle[MAX_PATH] = TEXT("");
    
    // 파일 해시섬 문자열 전역 변수
    wchar_t hashBuffer[256] = L"";
    wchar_t crc32ResultBuffer[256] = L"";
    wchar_t md5ResultBuffer[256] = L"";
    wchar_t sha1ResultBuffer[256] = L"";
    wchar_t sha256ResultBuffer[256] = L"";
    wchar_t sha512ResultBuffer[256] = L"";
    
    LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
    {
    	PAINTSTRUCT PaintStruct;
    
    	HDC hdc;
    
    	HDROP hDropFile = (HDROP)wParam;
    
    	wchar_t dragFileName[MAX_PATH];
    	wchar_t dragFileExt[MAX_PATH];
    	
    	// CRC32 해시 관련 변수
    	FILE *in;
    	char tempFilePath[256];
    	errno_t err;
    	
    
    	// MD5 해시 관련 변수
    	HANDLE hFile = INVALID_HANDLE_VALUE;
    	HCRYPTPROV hCryptProv = 0;
    	HCRYPTHASH hMd5Hash = 0;
    	BYTE rgbMd5File[MD5BUFSIZE];
    	DWORD cbRead = 0;
    	BYTE rgbMd5Hash[MD5LEN];
    	DWORD cbHash = 0;
    
    	// SHA-1 해시 관련 변수
    	NTSTATUS functionStatus = NULL;
    	BCRYPT_ALG_HANDLE hAlg = NULL;
    	BCRYPT_HASH_HANDLE hSha1Hash = NULL;
    	DWORD cbData = 0, cbSha1Hash = 0, cbSha1HashObject = 0;
    	PBYTE pbHashObject = NULL;
    	PBYTE pbHash = NULL;
    
    	// SHA-256 해시 관련 변수
    	BCRYPT_HASH_HANDLE hSha256Hash = NULL;
    	DWORD cbSha256Hash = 0, cbSha256HashObject = 0;
    	BYTE sha256Buffer[SHA256BUFSIZE];
    
    	// SHA-512 해시 관련 변수
    	BCRYPT_HASH_HANDLE hSha512Hash = NULL;
    	DWORD cbSha512Hash = 0, cbSha512HashObject = 0;
    	BYTE sha512Buffer[SHA512BUFSIZE];
    
    	switch (iMessage)
    	{
    		case WM_CREATE: // 프로그램 실행시 윈도우 생성과 동시에 처리되는 로직
    		{
    			DragAcceptFiles(hWnd, TRUE); // 드래그앤 드롭 파일 입력 허용
    			OpenFileName.lStructSize = sizeof(OpenFileName);
    			OpenFileName.hwndOwner = hWnd;
    			OpenFileName.lpstrFilter = TEXT("모든 파일\0*.*\0");
    			OpenFileName.nMaxFile = MAX_PATH;
    			OpenFileName.nMaxFileTitle = MAX_PATH;
    
    			// GDI+로 불러올 이미지 파일 위치
    			pImage = new Image(L"bg.png");
    
    			INITCOMMONCONTROLSEX icex;
    			icex.dwSize = sizeof(INITCOMMONCONTROLSEX);
    			icex.dwICC = ICC_PROGRESS_CLASS;
    			InitCommonControlsEx(&icex);
    
    			// 자식 윈도우 컨트롤들 그리기 및 배치
    			hFilePathLabelWnd = CreateWindowEx(NULL, TEXT("STATIC"), TEXT("파일 경로"), WS_CHILD | WS_VISIBLE | SS_CENTER | SS_CENTERIMAGE,
    				10, 5, 100, 25, hWnd, (HMENU)-1, g_hInstance, NULL);
    			hFilePathEditWnd = CreateWindowEx(WS_EX_STATICEDGE, TEXT("EDIT"), TEXT("파일을 드래그앤 드롭으로 프로그램에 올려놓아도 자동으로 경로를 입력받습니다"), WS_CHILD | WS_VISIBLE | WS_BORDER | ES_READONLY | SS_CENTER,
    				10, 35, 1130, 25, hWnd, (HMENU)-1, g_hInstance, NULL);
    			hFilePathButtonWnd = CreateWindowEx(WS_EX_STATICEDGE, TEXT("BUTTON"), TEXT("파일선택"), WS_CHILD | WS_VISIBLE | WS_BORDER,
    				1150, 35, 100, 25, hWnd, (HMENU)IDC_FILEPATHBT, g_hInstance, NULL);
    
    			hHashInputLabelWnd = CreateWindowEx(NULL, TEXT("STATIC"), TEXT("비교할 해시값을 아래에 입력해주세요"), WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE,
    				10, 100, 300, 25, hWnd, (HMENU)-1, g_hInstance, NULL);
    			hHashCompareInputWnd = CreateWindowEx(WS_EX_STATICEDGE, TEXT("EDIT"), NULL, WS_CHILD | WS_VISIBLE | WS_BORDER,
    				10, 130, 1130, 25, hWnd, (HMENU)-1, g_hInstance, NULL);
    			hHashCheckButtonWnd = CreateWindowEx(WS_EX_STATICEDGE, TEXT("BUTTON"), TEXT("해시 값 비교"), WS_CHILD | WS_VISIBLE | WS_BORDER | WS_DISABLED,
    				1150, 130, 100, 25, hWnd, (HMENU)IDC_HASHCOMPAREBT, g_hInstance, NULL);
    
    			hCrc32LabelWnd = CreateWindowEx(NULL, TEXT("STATIC"), TEXT("CRC32"), WS_CHILD | WS_VISIBLE | SS_CENTER | SS_CENTERIMAGE,
    				10, 180, 100, 25, hWnd, (HMENU)-1, g_hInstance, NULL);
    			hCrc32CopyButtonWnd = CreateWindowEx(NULL, TEXT("BUTTON"), TEXT("CRC32 해시 값 복사"), WS_CHILD | WS_VISIBLE | SS_CENTER | WS_DISABLED,
    				130, 180, 175, 25, hWnd, (HMENU)IDC_CRC32BT, g_hInstance, NULL);
    			hCrc32TextEditWnd = CreateWindowEx(WS_EX_STATICEDGE, TEXT("EDIT"), NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | ES_READONLY,
    				10, 210, 1240, 25, hWnd, (HMENU)-1, g_hInstance, NULL);
    
    			hMd5LabelWnd = CreateWindowEx(NULL, TEXT("STATIC"), TEXT("MD5"), WS_CHILD | WS_VISIBLE | SS_CENTER | SS_CENTERIMAGE,
    				10, 260, 100, 25, hWnd, (HMENU)-1, g_hInstance, NULL);
    			hMd5CopyButtonWnd = CreateWindowEx(NULL, TEXT("BUTTON"), TEXT("MD5 해시 값 복사"), WS_CHILD | WS_VISIBLE | SS_CENTER | WS_DISABLED,
    				130, 260, 175, 25, hWnd, (HMENU)IDC_MD5BT, g_hInstance, NULL);
    			hMd5TextEditWnd = CreateWindowEx(WS_EX_STATICEDGE, TEXT("EDIT"), NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | ES_READONLY,
    				10, 290, 1240, 25, hWnd, (HMENU)-1, g_hInstance, NULL);
    
    			hSha1LabelWnd = CreateWindowEx(NULL, TEXT("STATIC"), TEXT("SHA-1"), WS_CHILD | WS_VISIBLE | SS_CENTER | SS_CENTERIMAGE,
    				10, 340, 100, 25, hWnd, (HMENU)-1, g_hInstance, NULL);
    			hSha1CopyButtonWnd = CreateWindowEx(NULL, TEXT("BUTTON"), TEXT("SHA-1 해시 값 복사"), WS_CHILD | WS_VISIBLE | SS_CENTER | WS_DISABLED,
    				130, 340, 175, 25, hWnd, (HMENU)IDC_SHA1BT, g_hInstance, NULL);
    			hSha1TextEditWnd = CreateWindowEx(WS_EX_STATICEDGE, TEXT("EDIT"), NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | ES_READONLY,
    				10, 370, 1240, 25, hWnd, (HMENU)-1, g_hInstance, NULL);
    
    			hSha256LabelWnd = CreateWindowEx(NULL, TEXT("STATIC"), TEXT("SHA-256"), WS_CHILD | WS_VISIBLE | SS_CENTER | SS_CENTERIMAGE,
    				10, 420, 100, 25, hWnd, (HMENU)-1, g_hInstance, NULL);
    			hSha256CopyButtonWnd = CreateWindowEx(NULL, TEXT("BUTTON"), TEXT("SHA-256 해시 값 복사"), WS_CHILD | WS_VISIBLE | SS_CENTER | WS_DISABLED,
    				130, 420, 175, 25, hWnd, (HMENU)IDC_SHA256BT, g_hInstance, NULL);
    			hSha256TextEditWnd = CreateWindowEx(WS_EX_STATICEDGE, TEXT("EDIT"), NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | ES_READONLY,
    				10, 450, 1240, 25, hWnd, (HMENU)-1, g_hInstance, NULL);
    
    			hSha512LabelWnd = CreateWindowEx(NULL, TEXT("STATIC"), TEXT("SHA-512"), WS_CHILD | WS_VISIBLE | SS_CENTER | SS_CENTERIMAGE,
    				10, 500, 100, 25, hWnd, (HMENU)-1, g_hInstance, NULL);
    			hSha512CopyButtonWnd = CreateWindowEx(NULL, TEXT("BUTTON"), TEXT("SHA-512 해시 값 복사"), WS_CHILD | WS_VISIBLE | SS_CENTER | WS_DISABLED,
    				130, 500, 175, 25, hWnd, (HMENU)IDC_SHA512BT, g_hInstance, NULL);
    			hSha512TextEditWnd = CreateWindowEx(WS_EX_STATICEDGE, TEXT("EDIT"), NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | ES_READONLY,
    				10, 530, 1240, 25, hWnd, (HMENU)-1, g_hInstance, NULL);
    
    			hOkButtonWnd = CreateWindowEx(WS_EX_STATICEDGE, TEXT("BUTTON"), TEXT("파일 해시섬 확인"), WS_CHILD | WS_VISIBLE | WS_BORDER | WS_DISABLED,
    				1050, 640, 200, 25, hWnd, (HMENU)IDC_CHECKBT, g_hInstance, NULL);
    
    			hProgressbarWnd = CreateWindowEx(NULL, PROGRESS_CLASS, (LPCWSTR)NULL, WS_CHILD | WS_VISIBLE | PBS_SMOOTH,
    				0, 670, 1280, 15, hWnd, (HMENU)IDC_PROGRESSBAR, g_hInstance, NULL);
    
    			SendMessage(hProgressbarWnd, PBM_SETRANGE, 0, MAKELPARAM(0, 100));
    			SendMessage(hProgressbarWnd, PBM_SETPOS, 0, 0);
    			SendMessage(hProgressbarWnd, PBM_SETSTEP, (WPARAM)1, 0);
    			return 0;
    		}
    		case WM_PAINT: // 윈도우 그리기 처리 로직
    		{
    			hdc = BeginPaint(hWnd, &PaintStruct);
    			Graphics graphics(hdc);
    			graphics.DrawImage(pImage, 0, 0, 1280, 720);
    			EndPaint(hWnd, &PaintStruct);
    			return 0;
    		}
    		case WM_DROPFILES: // 파일 드래그 온 드롭시 실행될 로직
    		{
    			DragQueryFile(hDropFile, 0, lpstrFile, MAX_PATH);
    			_wsplitpath_s(lpstrFile, NULL, NULL, NULL, NULL, dragFileName, MAX_PATH, dragFileExt, MAX_PATH);
    			wcscat_s(dragFileName, MAX_PATH, dragFileExt);
    			ZeroMemory(lpstrFileTitle, sizeof(lpstrFileTitle));
    			wcscat_s(lpstrFileTitle, MAX_PATH, dragFileName);
    			SetWindowText(hFilePathEditWnd, lpstrFile);
    			EnableWindow(hOkButtonWnd, TRUE);
    			EnableWindow(hCrc32CopyButtonWnd, FALSE);
    			EnableWindow(hMd5CopyButtonWnd, FALSE);
    			EnableWindow(hSha1CopyButtonWnd, FALSE);
    			EnableWindow(hSha256CopyButtonWnd, FALSE);
    			EnableWindow(hSha512CopyButtonWnd, FALSE);
    			EnableWindow(hHashCheckButtonWnd, FALSE);
    			DragFinish(hDropFile);
    			SetForegroundWindow(hWnd);
    			return 0;
    		}
    		case WM_COMMAND: // 컨트롤 입력 처리 로직
    		{
    			switch (LOWORD(wParam))
    			{
    			case IDC_FILEPATHBT: // 파일 열기 버튼 로직
    				if (HIWORD(wParam) == BN_CLICKED)
    				{
    					memset(&OpenFileName, 0, sizeof(OPENFILENAME));
    					OpenFileName.lStructSize = sizeof(OPENFILENAME);
    					OpenFileName.hwndOwner = hWnd;
    					OpenFileName.lpstrFilter = TEXT("모든 파일\0*.*\0");
    					OpenFileName.nMaxFile = MAX_PATH;
    					OpenFileName.lpstrFile = lpstrFile;
    					OpenFileName.nMaxFileTitle = MAX_PATH;
    					OpenFileName.lpstrFileTitle = lpstrFileTitle;
    					if (GetOpenFileName(&OpenFileName) != 0)
    					{
    						SetWindowText(hFilePathEditWnd, lpstrFile);
    						EnableWindow(hOkButtonWnd, TRUE);
    						EnableWindow(hCrc32CopyButtonWnd, FALSE);
    						EnableWindow(hMd5CopyButtonWnd, FALSE);
    						EnableWindow(hSha1CopyButtonWnd, FALSE);
    						EnableWindow(hSha256CopyButtonWnd, FALSE);
    						EnableWindow(hSha512CopyButtonWnd, FALSE);
    						EnableWindow(hHashCheckButtonWnd, FALSE);
    					}
    				}
    				break;
    			case IDC_CRC32BT: // CRC32 해시 값 복사 버튼 로직
    				if (HIWORD(wParam) == BN_CLICKED)
    				{
    					SendMessage(hCrc32TextEditWnd, EM_SETSEL, 0, -1);
    					SendMessage(hCrc32TextEditWnd, WM_COPY, 0, 0);
    					MessageBox(hWnd, TEXT("CRC32 해시 값이 클립보드에 복사되었습니다\n복사된 값을 붙여 넣으려면 Ctrl + V키를 누르거나 마우스 오른쪽으로 메뉴를 호출 한뒤에 \'붙여넣기\'로 값을 붙여넣기 할수 있습니다"), TEXT("알림"), MB_OK);
    				}
    				break;
    			case IDC_MD5BT: // MD5 해시 값 복사 버튼 로직
    				if (HIWORD(wParam) == BN_CLICKED)
    				{
    					SendMessage(hMd5TextEditWnd, EM_SETSEL, 0, -1);
    					SendMessage(hMd5TextEditWnd, WM_COPY, 0, 0);
    					MessageBox(hWnd, TEXT("MD5 해시 값이 클립보드에 복사되었습니다\n복사된 값을 붙여 넣으려면 Ctrl + V키를 누르거나 마우스 오른쪽으로 메뉴를 호출 한뒤에 \'붙여넣기\'로 값을 붙여넣기 할수 있습니다"), TEXT("알림"), MB_OK);
    				}
    				break;
    			case IDC_SHA1BT: // SHA-1 해시 값 복사 버튼 로직
    				if (HIWORD(wParam) == BN_CLICKED)
    				{
    					SendMessage(hSha1TextEditWnd, EM_SETSEL, 0, -1);
    					SendMessage(hSha1TextEditWnd, WM_COPY, 0, 0);
    					MessageBox(hWnd, TEXT("SHA-1 해시 값이 클립보드에 복사되었습니다\n복사된 값을 붙여 넣으려면 Ctrl + V키를 누르거나 마우스 오른쪽으로 메뉴를 호출 한뒤에 \'붙여넣기\'로 값을 붙여넣기 할수 있습니다"), TEXT("알림"), MB_OK);
    				}
    				break;
    			case IDC_SHA256BT: // SHA-256 해시 값 복사 버튼 로직
    				if (HIWORD(wParam) == BN_CLICKED)
    				{
    					SendMessage(hSha256TextEditWnd, EM_SETSEL, 0, -1);
    					SendMessage(hSha256TextEditWnd, WM_COPY, 0, 0);
    					MessageBox(hWnd, TEXT("SHA-256 해시 값이 클립보드에 복사되었습니다\n복사된 값을 붙여 넣으려면 Ctrl + V키를 누르거나 마우스 오른쪽으로 메뉴를 호출 한뒤에 \'붙여넣기\'로 값을 붙여넣기 할수 있습니다"), TEXT("알림"), MB_OK);
    				}
    				break;
    			case IDC_SHA512BT: // SHA-256 해시 값 복사 버튼 로직
    				if (HIWORD(wParam) == BN_CLICKED)
    				{
    					SendMessage(hSha512TextEditWnd, EM_SETSEL, 0, -1);
    					SendMessage(hSha512TextEditWnd, WM_COPY, 0, 0);
    					MessageBox(hWnd, TEXT("SHA-512 해시 값이 클립보드에 복사되었습니다\n복사된 값을 붙여 넣으려면 Ctrl + V키를 누르거나 마우스 오른쪽으로 메뉴를 호출 한뒤에 \'붙여넣기\'로 값을 붙여넣기 할수 있습니다"), TEXT("알림"), MB_OK);
    				}
    				break;
    			case IDC_CHECKBT: // SHA-512 해시 값 복사 버튼 로직
    				if (HIWORD(wParam) == BN_CLICKED)
    				{
    					SendMessage(hProgressbarWnd, PBM_SETPOS, 0, 0);
    					progressStatus = 0;
    					// CRC32 처리 로직 시작
    					WideCharToMultiByte(CP_ACP, 0, lpstrFile, -1, tempFilePath, sizeof(tempFilePath), NULL, NULL);
    					err = fopen_s(&in, tempFilePath, "rb");
    					if (err != 0)
    					{
    						MessageBox(hWnd, TEXT("지정된 파일이 없습니다. 파일을 선택 한후에 해시섬 확인을 진행해 주세요"), TEXT("경고"), MB_OK | MB_ICONWARNING);
    						break;
    					}
    					if (progressStatus < 20)
    					{
    						for (int i = 0; i < 20; i++)
    						{
    							SendMessage(hProgressbarWnd, PBM_STEPIT, 0, 0);
    							progressStatus++;
    						}
    					}
    					wcscpy_s(crc32ResultBuffer, getFileCrc(in));
    					SetWindowText(hCrc32TextEditWnd, crc32ResultBuffer);
    					// MD5 처리 로직 시작
    					hFile = CreateFile(lpstrFile, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL);
    					functionStatus = CryptAcquireContext(&hCryptProv, NULL, MS_ENH_RSA_AES_PROV, PROV_RSA_AES, CRYPT_VERIFYCONTEXT);
    					functionStatus = CryptCreateHash(hCryptProv, CALG_MD5, 0, 0, &hMd5Hash);
    					while (ReadFile(hFile, rgbMd5File, MD5BUFSIZE, &cbRead, NULL) && cbRead > 0)
    					{
    						if (!CryptHashData(hMd5Hash, rgbMd5File, cbRead, 0))
    						{
    							CryptDestroyHash(hMd5Hash);
    							CryptReleaseContext(hCryptProv, 0);
    							CloseHandle(hFile);
    						}
    					}
    					cbHash = MD5LEN;
    					if (CryptGetHashParam(hMd5Hash, HP_HASHVAL, rgbMd5Hash, &cbHash, 0))
    					{
    						for (DWORD i = 0; i < cbHash; i++)
    						{
    							swprintf_s(md5ResultBuffer, 256, L"%s%02x", md5ResultBuffer, rgbMd5Hash[i]);
    							if (progressStatus < 40)
    							{
    								SendMessage(hProgressbarWnd, PBM_STEPIT, 0, 0);
    								progressStatus++;
    							}
    						}
    					}
    					CryptDestroyHash(hMd5Hash);
    					CryptReleaseContext(hCryptProv, 0);
    					CloseHandle(hFile);
    					SetWindowText(hMd5TextEditWnd, md5ResultBuffer);
    					// SHA-1 처리 로직 시작
    					functionStatus = BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_SHA1_ALGORITHM, NULL, 0);
    					functionStatus = BCryptGetProperty(hAlg, BCRYPT_OBJECT_LENGTH, (PBYTE)&cbSha1HashObject, sizeof(DWORD), &cbData, 0);
    					pbHashObject = (PBYTE)HeapAlloc(GetProcessHeap(), 0, cbSha1HashObject);
    					functionStatus = BCryptGetProperty(hAlg, BCRYPT_HASH_LENGTH, (PBYTE)&cbSha1Hash, sizeof(DWORD), &cbData, 0);
    					pbHash = (PBYTE)HeapAlloc(GetProcessHeap(), 0, cbSha1Hash);
    					functionStatus = BCryptCreateHash(hAlg, &hSha1Hash, pbHashObject, cbSha1HashObject, NULL, 0, 0);
    					hFile = CreateFile(lpstrFile, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    					BYTE sha1Buffer[1024 * 4];
    					DWORD byteRead = 0;
    					while (ReadFile(hFile, sha1Buffer, sizeof(sha1Buffer), &byteRead, NULL) && byteRead > 0)
    					{
    						functionStatus = BCryptHashData(hSha1Hash, sha1Buffer, byteRead, 0);
    					}
    					functionStatus = BCryptFinishHash(hSha1Hash, pbHash, cbSha1Hash, 0);
    					for (DWORD i = 0; i < cbSha1Hash; i++)
    					{
    						swprintf_s(sha1ResultBuffer, 256, L"%s%02x", sha1ResultBuffer, pbHash[i]);
    						if (progressStatus < 60)
    						{
    							SendMessage(hProgressbarWnd, PBM_STEPIT, 0, 0);
    							progressStatus++;
    						}
    					}
    					CloseHandle(hFile);
    					SetWindowText(hSha1TextEditWnd, sha1ResultBuffer);
    					// SHA-256 처리 로직 시작
    					hAlg = NULL;
    					cbData = 0;
    					pbHashObject = NULL;
    					pbHash = NULL;
    					functionStatus = BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_SHA256_ALGORITHM, NULL, 0);
    					functionStatus = BCryptGetProperty(hAlg, BCRYPT_OBJECT_LENGTH, (PBYTE)&cbSha256HashObject, sizeof(DWORD), &cbData, 0);
    					pbHashObject = (PBYTE)HeapAlloc(GetProcessHeap(), 0, cbSha256HashObject);
    					functionStatus = BCryptGetProperty(hAlg, BCRYPT_HASH_LENGTH, (PBYTE)&cbSha256Hash, sizeof(DWORD), &cbData, 0);
    					pbHash = (PBYTE)HeapAlloc(GetProcessHeap(), 0, cbSha256Hash);
    					functionStatus = BCryptCreateHash(hAlg, &hSha256Hash, pbHashObject, cbSha256HashObject, NULL, 0, 0);
    					hFile = CreateFile(lpstrFile, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    					byteRead = 0;
    					while (ReadFile(hFile, sha256Buffer, SHA256BUFSIZE, &byteRead, NULL) && byteRead > 0)
    					{
    						functionStatus = BCryptHashData(hSha256Hash, sha256Buffer, byteRead, 0);
    					}
    					functionStatus = BCryptFinishHash(hSha256Hash, pbHash, cbSha256Hash, 0);
    					for (DWORD i = 0; i < cbSha256Hash; i++)
    					{
    						swprintf_s(sha256ResultBuffer, 256, L"%s%02x", sha256ResultBuffer, pbHash[i]);
    						if (progressStatus < 80)
    						{
    							SendMessage(hProgressbarWnd, PBM_STEPIT, 0, 0);
    							progressStatus++;
    						}
    					}
    					CloseHandle(hFile);
    					SetWindowText(hSha256TextEditWnd, sha256ResultBuffer);
    					// SHA-512 처리 로직 시작
    					hAlg = NULL;
    					cbData = 0;
    					pbHashObject = NULL;
    					pbHash = NULL;
    					functionStatus = BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_SHA512_ALGORITHM, NULL, 0);
    					functionStatus = BCryptGetProperty(hAlg, BCRYPT_OBJECT_LENGTH, (PBYTE)&cbSha512HashObject, sizeof(DWORD), &cbData, 0);
    					pbHashObject = (PBYTE)HeapAlloc(GetProcessHeap(), 0, cbSha512HashObject);
    					functionStatus = BCryptGetProperty(hAlg, BCRYPT_HASH_LENGTH, (PBYTE)&cbSha512Hash, sizeof(DWORD), &cbData, 0);
    					pbHash = (PBYTE)HeapAlloc(GetProcessHeap(), 0, cbSha512Hash);
    					functionStatus = BCryptCreateHash(hAlg, &hSha512Hash, pbHashObject, cbSha512HashObject, NULL, 0, 0);
    					hFile = CreateFile(lpstrFile, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    					byteRead = 0;
    					while (ReadFile(hFile, sha512Buffer, SHA512BUFSIZE, &byteRead, NULL) && byteRead > 0)
    					{
    						functionStatus = BCryptHashData(hSha512Hash, sha512Buffer, byteRead, 0);
    					}
    					functionStatus = BCryptFinishHash(hSha512Hash, pbHash, cbSha512Hash, 0);
    					for (DWORD i = 0; i < cbSha512Hash; i++)
    					{
    						swprintf_s(sha512ResultBuffer, 256, L"%s%02x", sha512ResultBuffer, pbHash[i]);
    						if (progressStatus < 100)
    						{
    							SendMessage(hProgressbarWnd, PBM_STEPIT, 0, 0);
    							progressStatus++;
    						}
    					}
    					CloseHandle(hFile);
    					SetWindowText(hSha512TextEditWnd, sha512ResultBuffer);
    					// 해시 값 출력 처리 완료 후 처리
    					if (progressStatus < 100)
    					{
    						for (int i = 0; i < (100 - progressStatus); i++)
    						{
    							SendMessage(hProgressbarWnd, PBM_STEPIT, 0, 0);
    						}
    					}
    					SendMessage(hProgressbarWnd, PBM_SETPOS, 100, 0);
    					wmemset(crc32ResultBuffer, 0, 256);
    					wmemset(md5ResultBuffer, 0, 256);
    					wmemset(sha1ResultBuffer, 0, 256);
    					wmemset(sha256ResultBuffer, 0, 256);
    					wmemset(sha512ResultBuffer, 0, 256);
    					EnableWindow(hCrc32CopyButtonWnd, TRUE);
    					EnableWindow(hMd5CopyButtonWnd, TRUE);
    					EnableWindow(hSha1CopyButtonWnd, TRUE);
    					EnableWindow(hSha256CopyButtonWnd, TRUE);
    					EnableWindow(hSha512CopyButtonWnd, TRUE);
    					EnableWindow(hHashCheckButtonWnd, TRUE);
    				}
    				break;
    			case IDC_HASHCOMPAREBT: // 해시 값 비교 버튼 로직
    				if (HIWORD(wParam) == BN_CLICKED)
    				{
    					TCHAR messageText[MAX_PATH];
    					int hashTextLength = GetWindowTextLength(hHashCompareInputWnd) + 1;
    					int crc32TextLength = GetWindowTextLength(hCrc32TextEditWnd) + 1;
    					int md5TextLength = GetWindowTextLength(hMd5TextEditWnd) + 1;
    					int sha1TextLength = GetWindowTextLength(hSha1TextEditWnd) + 1;
    					int sha256TextLength = GetWindowTextLength(hSha256TextEditWnd) + 1;
    					int sha512TextLength = GetWindowTextLength(hSha512TextEditWnd) + 1;
    					GetWindowText(hHashCompareInputWnd, hashBuffer, hashTextLength);
    					GetWindowText(hCrc32TextEditWnd, crc32ResultBuffer, crc32TextLength);
    					GetWindowText(hMd5TextEditWnd, md5ResultBuffer, md5TextLength);
    					GetWindowText(hSha1TextEditWnd, sha1ResultBuffer, sha1TextLength);
    					GetWindowText(hSha256TextEditWnd, sha256ResultBuffer, sha256TextLength);
    					GetWindowText(hSha512TextEditWnd, sha512ResultBuffer, sha512TextLength);
    
    					if (wcscmp(hashBuffer, crc32ResultBuffer) == 0)
    					{
    						wsprintf(messageText, TEXT("%s 파일에 CRC32해시 값 %ls 와 일치합니다"), lpstrFileTitle, crc32ResultBuffer);
    						MessageBox(hWnd, messageText, TEXT("일치하는 해시 값이 있습니다"), MB_OK);
    					}
    					else if (wcscmp(hashBuffer, md5ResultBuffer) == 0)
    					{
    						wsprintf(messageText, TEXT("%s 파일에 MD5해시 값 %s 와 일치합니다"), lpstrFileTitle, md5ResultBuffer);
    						MessageBox(hWnd, messageText, TEXT("일치하는 해시 값이 있습니다"), MB_OK);
    					}
    					else if (wcscmp(hashBuffer, sha1ResultBuffer) == 0)
    					{
    						wsprintf(messageText, TEXT("%s 파일에 SHA-1해시 값 %s 와 일치합니다"), lpstrFileTitle, sha1ResultBuffer);
    						MessageBox(hWnd, messageText, TEXT("일치하는 해시 값이 있습니다"), MB_OK);
    					}
    					else if (wcscmp(hashBuffer, sha256ResultBuffer) == 0)
    					{
    						wsprintf(messageText, TEXT("%s 파일에 SHA-256해시 값 %s 와 일치합니다"), lpstrFileTitle, sha256ResultBuffer);
    						MessageBox(hWnd, messageText, TEXT("일치하는 해시 값이 있습니다"), MB_OK);
    					}
    					else if (wcscmp(hashBuffer, sha512ResultBuffer) == 0)
    					{
    						wsprintf(messageText, TEXT("%s 파일에 SHA-512해시 값 %s 와 일치합니다"), lpstrFileTitle, sha512ResultBuffer);
    						MessageBox(hWnd, messageText, TEXT("일치하는 해시 값이 있습니다"), MB_OK);
    					}
    					else
    					{
    						MessageBox(hWnd, TEXT("일치하는 해시값이 없습니다"), TEXT("일치하는 해시값이 없습니다"), MB_OK);
    					}
    					wmemset(hashBuffer, 0, 256);
    					wmemset(crc32ResultBuffer, 0, 256);
    					wmemset(md5ResultBuffer, 0, 256);
    					wmemset(sha1ResultBuffer, 0, 256);
    					wmemset(sha256ResultBuffer, 0, 256);
    					wmemset(sha512ResultBuffer, 0, 256);
    				}
    				break;
    			}
    			return 0;
    		}
    		case WM_DESTROY: // 윈도우 종료시 실행될 로직
    		{// !!!메모리 누수 방지를 위해 메모리 릴리즈 처리를 반드시 해줄것
    			delete pImage;
    			PostQuitMessage(0);
    			return 0;
    		}
    	}
    
    	return DefWindowProc(hWnd, iMessage, wParam, lParam);
    }
    
    /// CRC32 파일 해시섬 함수들 시작
    void Crc32Table(unsigned long* table, unsigned long id)
    {
    	unsigned long i, j, k;
    
    	for (i = 0; i < 256; ++i)
    	{
    		k = i;
    		for (j = 0; j < 8; ++j)
    		{
    			if (k & 1) k = (k >> 1) ^ id;
    			else k >>= 1;
    		}
    		table[i] = k;
    	}
    }
    
    unsigned long calcCrc(const unsigned char* mem, signed long size, unsigned long CRC, unsigned long* table)
    {
    	CRC = ~CRC;
    
    	while (size--)
    	{
    		CRC = table[(CRC ^ *(mem++)) & 0xff] ^ (CRC >> 8);
    	}
    
    	return ~CRC;
    }
    
    LPCWSTR getFileCrc(FILE* s)
    {
    	unsigned char buf[32768];
    	unsigned long CRC = 0;
    	unsigned long table[256];
    	size_t len;
    	wchar_t buffer[256];
    
    	Crc32Table(table, 0xEDB88320);
    
    	while ((len = fread(buf, 1, sizeof(buf), s)) != NULL)
    	{
    		CRC = calcCrc(buf, (unsigned long)len, CRC, table);
    	}
    	_ultow_s(CRC, buffer, sizeof(buffer) / sizeof(wchar_t), 16);
    
    	return buffer;
    }
    /// CRC32 파일 해시섬 함수들 끝
  • 파일 해시섬 프로그램 File Hashsum Program

    파일의 해시섬을 구할수 있는 프로그램 입니다

    CRC32부터 좀더 무결성이 높은 SHA-512까지 동시에 확인이 가능합니다

    2.0 버전 기준 지원 해시 목록

    • CRC32
    • MD5
    • SHA-1
    • SHA-256
    • SHA-512

    배경 이미지를 원하는 걸로 커스터마이즈 가능합니다

    지원되는 포맷은 png 입니다

    바꾸고자 하는 png 이미지 파일을 bg.png로 이름을 변경 한후 exe파일과 같은 위치에 넣어준뒤에 프로그램을 종료하고 재시작 하면 반영이 됩니다

    권장 이미지 사이즈 비율은 16:9 입니다(권장 해상도 1280×720)

    사용하고자 하는 이미지가 16:9 비율이 아닐시 프로그램에서 이미지를 강제로 16:9비율로 표시합니다

    버전 로그

    2026년 5월 15일 1.0 버전

    • 최초 출시

    2026년 5월 19일 2.0 버전

    • 해시 값 비교 기능 추가
  • 슈퍼패미컴 롬파일 확장도구 – 루나익스팬드(Lunar Expand)

    오늘 소개할 툴은 슈퍼패미컴(SFC)용 롬파일 확장도구인 루나익스팬드(Lunar Expand) 입니다

    현재도 구글에 검색하면 심심찮게 구버젼을 다른 블로그들에서 내려받을수있지만 정작 제작자분의 홈페이지를 링크해두고 최신버젼을 내려받도록 유도되어있는 블로그 게시글이 적어서 스스로 기록용 및 필요한 사람들에게 안내차원에서 블로그 포스팅을 해봅니다

    개발자 공식 사이트 URL – https://fusoya.eludevisibility.org/

    Lunar Expand 페이지 URL – https://fusoya.eludevisibility.org/le/index.html

    2026년 5월 10일 기준 최신버젼인 1.20(2022년 7월 1일자 버젼) 내려받기

    프로그램의 사용법은 상당히 심플한데 확장하고 싶은 롬을 선택하고 그에맞게 용량을 늘려주면 끝입니다

    GUI로 깔끔하게 제작자분이 만들어두고 관리하고 있어서 UI가 지금에와서 깨진다거나 최신 윈도우에서 작동이 안된다든가 하는 불상사는 없는듯합니다

    심지어 굉장히 오래전 32비트용으로 개발된 소프트웨어이나 최신 트렌드에 맞춰 64비트 버젼을 따로 만들어서 같이 배포하고있을정도로 제작자분의 정성이 대단합니다

    아래는 프로그램에 첨부된 readme.txt 파일을 AI 번역한 내용입니다.

    1. Instructions(설명서) 번역

    SNES ROM을 32 Mbit 이상으로 확장하는 것은 전통적으로 대부분의 ROM 해커들에게 골칫거리였습니다. 부분적으로는 확장 방식이 조금 다르기 때문이고, 주로는 에뮬레이터 지원이 비교적 최근에야 이루어졌기 때문입니다. 또한 메모리 맵의 미러 영역을 추가 데이터 저장에 사용하기 때문에, 모든 ROM에서 별도의 조정 없이 32 Mbit 이상 확장이 제대로 작동한다고 보장할 수 없습니다.

    그래서 이러한 확장을 처리하기 위한 몇 가지 옵션을 제공하는 프로그램을 만들었습니다.

    에뮬레이터 호환성

    • Tales of Phantasia를 실행할 수 있다면, 해당 에뮬레이터는 48 Mbit ExHiROM 옵션을 지원합니다. ZSNES와 Snes9x는 이미 이를 지원합니다.
    • Snes9x 1.39a 이상: 48 Mbit ExLoROM 및 64 Mbit ExHiROM 확장 지원.
    • Snes9x 1.39a ~ 1.43, 1.54+: 64 Mbit ExLoROM 지원 (단, 1.50~1.53은 ExLoROM을 48 Mbit로 제한).
    • Snes9x 1.54+: 48 및 64 Mbit SA-1 ROM 지원.

    현재 ZSNES는 ExLoROM이나 48 Mbit(6 MB) 이상의 ROM을 지원하지 않습니다. 또한 SA-1의 경우 32 Mbit 이후 첫 두 개의 64 KB 뱅크에 접근할 수 없는 문제가 있습니다. 하지만 제 사이트에서 제공하는 비공식 ZSNES 1.51 8MB 빌드를 다운로드하면 이를 우회할 수 있습니다.

    SA-1과 S-DD1 옵션도 제공되는데, 이들은 MMC 칩으로 뱅크 스위칭을 통해 특정 뱅크에서 ROM의 다른 부분을 접근할 수 있습니다. 4 MB 이상 영역에 접근하려면 ASM으로 MMC 레지스터를 변경해야 할 수도 있습니다. 게임별 툴(예: SMW용 Lunar Magic + SA-1 패치)이 있다면 그것을 사용하는 것이 더 낫습니다. SuperFX 게임에도 같은 옵션을 사용할 수 있지만, SuperFX 칩 자체는 2 MB 이상을 접근할 수 없으므로 SNES CPU만 나머지를 접근할 수 있습니다. 따라서 상업용 게임에서는 이 구성을 사용하지 않았고, 대부분의 에뮬레이터도 SuperFX ROM을 2 MB 이상 지원하지 않습니다.

    다른 추가 칩을 사용하는 ROM은 확장 시 각 칩이 고유한 메모리 맵을 사용하기 때문에 직접 해결해야 합니다. 또한 프로그램은 인터리브된 ROM을 지원하지 않습니다. 0x200 바이트 카피어 헤더가 있거나 없어도 지원합니다.

    프로그램 사용법

    • 왼쪽 열: 최대 32 Mbit까지 확장 옵션 (HiROM/LoROM 모두 가능).
    • 오른쪽 열: 48 또는 64 Mbit 확장 옵션.
    • LoROM 게임은 반드시 ExLoROM 옵션을, HiROM 게임은 ExHiROM 옵션을 선택해야 합니다. 프로그램이 ROM 타입을 확인하긴 하지만, 직접 확인하는 것이 안전합니다.
    • ExHiROM 옵션은 8 Mbit 이하 LoROM을 ExHiROM으로 변환할 수도 있지만, 실제로는 거의 필요 없습니다.

    ExLoROM/ExHiROM 확장은 기본적으로 ROM 확장, 헤더 크기 변경, 0x8000 뱅크 데이터 복사 과정을 수행합니다. 각 ROM 크기마다 최대 3가지 옵션이 있습니다.

    ExHiROM

    • 기본 옵션: 일반적인 확장.
    • 호환성 옵션(note 3): 원래 게임에서 00:8000 맵을 사용하는 경우, 확장 공간에 32K 뱅크를 절반 정도 복사하여 호환성을 유지.
    • 확장 영역 접근: 40:0000 ~ 7D:FFFF. 단, 7E와 7F는 RAM이므로 마지막 128K는 접근 불가.

    ExLoROM

    • 옵션 1(note 1): 00:8000 ~ 6F:FFFF 맵을 사용하는 LoROM (보통 28 Mbit 이하). 원래 ROM을 40:0000에 복사.
    • 옵션 2(note 2): 80:8000 ~ FF:FFFF 맵을 사용하는 LoROM (보통 28 Mbit 이상).
    • 옵션 3(note 3): 두 맵을 모두 사용하는 경우 호환성 옵션. ROM을 확장 공간에 그대로 복사.

    확장 영역 접근: 00:8000 ~ 7D:FFFF. 단, 7E와 7F는 RAM이므로 마지막 64K는 접근 불가. 따라서 ExLoROM/ExHiROM 모두 실제로는 63.5 Mbit만 사용 가능.

    실제 사용 사례

    • Chrono Trigger, Mario World → 48 Mbit ExHiROM 변환 성공.
    • RoboTrek → 호환성 ExHiROM 옵션 필요.
    • 항상 일반 옵션을 먼저 시도하고, 필요할 때만 호환성 옵션을 사용하세요.

    명령줄 사용법

    코드

    "Lunar Expand.exe" -ExpandROM "ROM파일명" SizeOfROM
    

    SizeOfROM 인자:

    • Mbit 단위: 1,2,4,8,12,16,20,24,28,32
    • MB 단위: 1MB, 1.5MB, 2MB, 2.5MB, 3MB, 3.5MB, 4MB
    • 라벨: 48_EXHIROM, 48_EXHIROM_3, 64_EXHIROM, 64_EXHIROM_3, 48_EXLOROM_1, 48_EXLOROM_2, 48_EXLOROM_3, 64_EXLOROM_1, 64_EXLOROM_2, 64_EXLOROM_3, 48_EXPCHIP, 64_EXPCHIP (SA-1/S-DD1용).

    2. Updates 번역

    버전 1.20 (2022년 7월 1일)

    • -ExpandROM 명령줄 기능 추가
    • SA-1 및 S-DD1 게임 확장 옵션 몇 가지 추가
    • ROM 타입별로 확장 옵션을 재구성하여 가독성 개선
    • Windows 10 크리에이터 업데이트(1703) 이상에서 모니터별 V2 DPI 인식 추가
    • x64 폴더에 64비트 빌드 추가
    • 유니코드 OS에서 실행 시 유니코드 파일명/경로 지원 추가

    버전 1.14 (2010년 5월 22일)

    • 내부 헤더 위치 감지 기능을 개선하여, LoROM과 HiROM 위치 모두에서 헤더가 갱신되는 문제 수정 (보고해준 BMF에게 감사)
    • 프로그램이 항상 Win95 기본 글꼴을 사용하던 문제 수정 및 고해상도 DPI 인식 추가 → ClearType LCD에서 가독성 개선

    버전 1.13 (2003년 7월 4일)

    • ExHiROM 맵 관련 문서 소폭 업데이트: 마지막 두 뱅크의 후반부는 기술적으로 00:8000 맵에서 접근 가능
    • 프로그램 자체에는 버전 번호 변경 외 수정 없음

    버전 1.12 (2002년 12월 25일)

    • 00:8000 메모리 맵을 사용하는 ROM의 ExLoROM 확장 관련 소폭 변경
    • ExHiROM 맵 문서 업데이트 (뱅크 $70~$77)

    버전 1.11 (2002년 11월 13일)

    • ExHiROM 옵션 변경: ROM이 이미 32 Mbit 이상일 경우 시작 뱅크를 다시 복사하지 않도록 수정 (ExLoROM 확장과의 일관성 확보)

    버전 1.10 (2002년 11월 11일)

    • 48 & 64 Mbit LoROM 확장 옵션 추가
    • 64 Mbit HiROM 확장 옵션 추가 (snes9x 1.39a 및 1.39mk3에서 지원, ZSNES는 아직 미지원)
    • 몇 가지 표준 확장 크기 추가

    버전 1.02 (2002년 8월 28일)

    • 컨트롤 간 탭 이동 문제 수정

    버전 1.01 (2002년 8월 26일)

    • 프로그램이 더 이상 Lunar Compress DLL을 필요로 하지 않음

    버전 1.00 (2002년 8월 25일)

    • 첫 릴리스

    3. Legal Notice(법적 고지) 번역

    Lunar Expand 프로그램(이하 “소프트웨어”)은 닌텐도나 기타 상업적 기업의 공식 제품이나 지원을 받는 소프트웨어가 아닙니다.

    소프트웨어는 프리웨어(freeware)이므로 다음 조건을 충족하는 한 자유롭게 배포할 수 있습니다:

    1. 이 문서가 소프트웨어와 함께 제공되며, 문서와 소프트웨어가 어떤 방식으로든 수정되지 않을 것
    2. 소프트웨어가 어떤 형식의 ROM 이미지와 함께 또는 그 일부로 배포되지 않을 것
    3. 소프트웨어에 대해 어떠한 상품, 서비스, 금전적 대가도 청구할 수 없으며, 다른 제안이나 금전 거래와 결합하여 제공되지 않을 것

    소프트웨어는 있는 그대로(AS IS) 제공되며, 사용은 전적으로 사용자의 책임입니다. 이 문서에 언급된 누구도 소프트웨어의 사용이나 존재로 인해 발생하는 직접적 또는 간접적인 손해에 대해 책임을 지지 않습니다.

  • Win32API 기본 윈도우 만들기

    WndProc.h

    #ifndef _WNDPROCS_H_
    #define _WNDPROCS_H_
    #endif
    #include <Windows.h>
    
    LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam);

    WinMain.cpp

    #include "WndProcs.h"
    
    LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam);
    
    LPCTSTR lpszWndClass = TEXT("파일 체크섬 프로그램");
    HINSTANCE g_hInstance;
    
    int APIENTRY WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpszCmdParam, _In_ int nCmdShow)
    {
    	HWND hWnd;
    	MSG Message;
    	WNDCLASSEX WndClassEx;
    
    	g_hInstance = hInstance;
    
    	WndClassEx.cbSize = sizeof(WndClassEx); // 구조체 크기를 RegisterClass로 전달하기 위한 멤버이며 보통 값은 자기 자신의 크기를 넘겨주면 됨
    	WndClassEx.cbClsExtra = 0; // 윈도우 클래스 레벨에서 추가적인 여분 메모리 할당용. 보안상 이슈로 거의 사용되지 않고 0으로 두는 경우가 많음
    	WndClassEx.cbWndExtra = 0; // 윈도우 인스턴스마다 추가적으로 할당할 여분의 메모리 할당용. 각 윈도우별로 별도 할당되고 윈도우별로 특별한 데이터를 저정하기 위한 예약된 메모리 공간. 이쪽도 그렇게 잘 사용되지는 않음. 프로젝트 규모가 크거나 할때는 어느정도 쓰이나 규모가 작거나 복잡도가 낮은경우에는 거의 쓰일 일이 없음
    	WndClassEx.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    	WndClassEx.hCursor = LoadCursor(NULL, IDC_ARROW);
    	WndClassEx.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    	WndClassEx.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
    	WndClassEx.hInstance = hInstance;
    	WndClassEx.lpfnWndProc = WndProc;
    	WndClassEx.lpszClassName = lpszWndClass;
    	WndClassEx.lpszMenuName = NULL;
    	WndClassEx.style = CS_VREDRAW | CS_HREDRAW;
    	RegisterClassEx(&WndClassEx);
    
    	hWnd = CreateWindowEx(0, lpszWndClass, lpszWndClass, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, (HMENU)NULL, g_hInstance, NULL);
    	ShowWindow(hWnd, nCmdShow);
    
    	while (TRUE)
    	{
    		if (PeekMessage(&Message, NULL, 0, 0, PM_REMOVE))
    		{
    			if (Message.message == WM_QUIT)
    			{
    				break;
    			}
    			else
    			{
    				TranslateMessage(&Message);
    				DispatchMessage(&Message);
    			}
    		}
    		else
    		{
    			WaitMessage();
    		}
    	}
    
    	return (int)Message.wParam;
    }

    WndProc.cpp

    #include "WndProcs.h"
    
    LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
    {
    	HDC hdc;
    	PAINTSTRUCT PaintStruct;
    
    	OPENFILENAME OpenFileName;
    
    	switch (iMessage)
    	{
    	case WM_CREATE:
    		DragAcceptFiles(hWnd, TRUE);
    		OpenFileName.lStructSize = sizeof(OpenFileName);
    		OpenFileName.hwndOwner = hWnd;
    		OpenFileName.lpstrFilter = TEXT("모든 파일\0*.*\0");
    		OpenFileName.nMaxFile = MAX_PATH;
    		OpenFileName.nMaxFileTitle = MAX_PATH;
    		return 0;
    	case WM_PAINT:
    		hdc = BeginPaint(hWnd, &PaintStruct);
    		EndPaint(hWnd, &PaintStruct);
    		return 0;
    	case WM_DESTROY:
    		PostQuitMessage(0);
    		return 0;
    	}
    
    	return DefWindowProc(hWnd, iMessage, wParam, lParam);
    }