Created
September 2, 2018 12:18
-
-
Save kizernis/b340545e56f0f186084a7dab93188feb to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <windows.h> | |
#include <commctrl.h> | |
#include <shlobj.h> | |
#include "resource.h" | |
#include "md5.h" | |
#ifndef _DEBUG | |
#pragma comment(linker, "/entry:_WinMain /nodefaultlib /subsystem:windows /filealign:512 /stack:65536,65536") | |
#pragma comment(linker, "/merge:.data=.text /merge:.rdata=.text /section:.text,ewrx /ignore:4078") | |
#endif | |
typedef struct | |
{ | |
PSTR psName; | |
UINT nNameLen; | |
PSTR psType; | |
UINT nTypeLen; | |
} LOGGED_GAMES_ARRAY; | |
#define LG_DATA_CHUNK_SIZE (1024 * 50) | |
LOGGED_GAMES_ARRAY * g_pLoggedGames; | |
UINT g_nLoggedGames = 0; | |
UINT g_nLoggedGamesMax; | |
PUCHAR g_pucLoggedGamesData; | |
PUCHAR g_pucLoggedGamesDataPtr; | |
UINT g_nLoggedGamesBufferSize; | |
BOOL g_bLogsPathChanged = FALSE; | |
void LoggedGames_Reset() | |
{ | |
if (g_nLoggedGames) | |
{ g_nLoggedGames = 0; free(g_pLoggedGames); free(g_pucLoggedGamesData); } | |
g_nLoggedGamesMax = 100; | |
g_pLoggedGames = (LOGGED_GAMES_ARRAY *)malloc(sizeof(LOGGED_GAMES_ARRAY) * g_nLoggedGamesMax); | |
g_nLoggedGamesBufferSize = LG_DATA_CHUNK_SIZE; | |
g_pucLoggedGamesData = (PUCHAR)malloc(g_nLoggedGamesBufferSize); | |
g_pucLoggedGamesDataPtr = g_pucLoggedGamesData; | |
} | |
void LoggedGames_Add(PSTR psName, UINT nNameLen, PSTR psType, UINT nTypeLen) | |
{ | |
UINT nRequiredSize, i; | |
g_nLoggedGames++; | |
if (g_nLoggedGames > g_nLoggedGamesMax) | |
{ g_nLoggedGamesMax += 100; g_pLoggedGames = (LOGGED_GAMES_ARRAY *)realloc(g_pLoggedGames, sizeof(LOGGED_GAMES_ARRAY) * g_nLoggedGamesMax); } | |
nRequiredSize = g_pucLoggedGamesDataPtr + nNameLen + 1 + nTypeLen + 1 - g_pucLoggedGamesData; | |
while (nRequiredSize > g_nLoggedGamesBufferSize) | |
{ | |
PUCHAR pucOldAddress = g_pucLoggedGamesData; | |
g_nLoggedGamesBufferSize += LG_DATA_CHUNK_SIZE; | |
g_pucLoggedGamesData = (PUCHAR)realloc(g_pucLoggedGamesData, g_nLoggedGamesBufferSize); | |
if (g_pucLoggedGamesData != pucOldAddress) | |
{ | |
int nDiff = g_pucLoggedGamesData - pucOldAddress; | |
for (i = 0; i < g_nLoggedGames - 1; i++) | |
{ g_pLoggedGames[i].psName += nDiff; g_pLoggedGames[i].psType += nDiff; } | |
g_pucLoggedGamesDataPtr += nDiff; | |
} | |
} | |
i = g_nLoggedGames - 1; | |
g_pLoggedGames[i].psName = (PSTR)g_pucLoggedGamesDataPtr; g_pLoggedGames[i].nNameLen = nNameLen; | |
strcpy(g_pucLoggedGamesDataPtr, psName); g_pucLoggedGamesDataPtr += nNameLen + 1; | |
g_pLoggedGames[i].psType = (PSTR)g_pucLoggedGamesDataPtr; g_pLoggedGames[i].nTypeLen = nTypeLen; | |
strcpy(g_pucLoggedGamesDataPtr, psType); g_pucLoggedGamesDataPtr += nTypeLen + 1; | |
} | |
#define GAME_NOT_FOUND 0xFFFFFFFF | |
UINT LoggedGames_Find(PSTR psName) | |
{ | |
UINT i; | |
for (i = 0; i < g_nLoggedGames; i++) | |
{ | |
if (0 == strcmp(psName, g_pLoggedGames[i].psName)) | |
return i; | |
} | |
return GAME_NOT_FOUND; | |
} | |
typedef struct { UCHAR ucMD[16]; } MD_ARRAY; | |
MD_ARRAY * g_pHashes; | |
UINT g_nHashes = 0; | |
UINT g_nHashesMax; | |
UCHAR g_ucMD5Sum[16]; | |
void Hashes_Reset() | |
{ | |
if (g_nHashes) | |
{ g_nHashes = 0; free(g_pHashes); } | |
g_nHashesMax = 100; | |
g_pHashes = (MD_ARRAY *)malloc(16 * g_nHashesMax); | |
} | |
void Hashes_Add() | |
{ | |
if (++g_nHashes > g_nHashesMax) | |
{ g_nHashesMax += 100; g_pHashes = (MD_ARRAY *)realloc(g_pHashes, 16 * g_nHashesMax); } | |
memcpy(g_pHashes[g_nHashes - 1].ucMD, g_ucMD5Sum, 16); | |
} | |
BOOL Hashes_IsStored() | |
{ | |
PUCHAR p1 = g_pHashes[0].ucMD; | |
PUCHAR p2 = p1 + 16 * g_nHashes; | |
for (; p1 < p2; p1 += 16) | |
{ | |
if (p1[ 0] == g_ucMD5Sum[ 0] && p1[ 1] == g_ucMD5Sum[ 1] && p1[ 2] == g_ucMD5Sum[ 2] && p1[ 3] == g_ucMD5Sum[ 3] | |
&& p1[ 4] == g_ucMD5Sum[ 4] && p1[ 5] == g_ucMD5Sum[ 5] && p1[ 6] == g_ucMD5Sum[ 6] && p1[ 7] == g_ucMD5Sum[ 7] | |
&& p1[ 8] == g_ucMD5Sum[ 8] && p1[ 9] == g_ucMD5Sum[ 9] && p1[10] == g_ucMD5Sum[10] && p1[11] == g_ucMD5Sum[11] | |
&& p1[12] == g_ucMD5Sum[12] && p1[13] == g_ucMD5Sum[13] && p1[14] == g_ucMD5Sum[14] && p1[15] == g_ucMD5Sum[15]) | |
return TRUE; | |
} | |
return FALSE; | |
} | |
// TODO: use Unicode APIs with \\?\ path prefix and 32768 max path length (and 256 file or directory name length) | |
#define PATH_BUFFER_SIZE (MAX_PATH * 2) | |
#define LOG_BUFFER_SIZE (1024 * 30) | |
HWND g_hDlg, g_hBtnExtract, g_hLblStatus, g_hPgbProgress; | |
BOOL g_bProcessing; | |
PSTR g_psPathLogs, g_psPathReplays, g_psPathMaps; | |
UINT g_nPathLogsLen, g_nPathReplaysLen, g_nPathMapsLen; | |
BOOL g_bWarned_WA, g_bWarned_Maps; | |
PCSTR g_pcsAlert_WA, g_pcsAlert_Replays, g_pcsAlert_Maps; | |
#define STATE_NORMAL 0 | |
#define STATE_WARNING 1 | |
#define STATE_ALERT 2 | |
#define NORMAL_STATUS_BGCOLOR RGB(0, 255, 115) | |
#define WARNING_STATUS_BGCOLOR RGB(255, 224, 4) | |
#define ALERT_STATUS_BGCOLOR RGB(255, 128, 0) | |
HBRUSH g_hNormalBrush, g_hWarningBrush, g_hAlertBrush; | |
UINT g_nState = STATE_NORMAL; | |
PCSTR g_csCurrentStatus; | |
int g_nMessageLength, g_nPos; | |
char g_sMsgTmp[256]; | |
char g_sMsgFinal[256]; | |
typedef void (* SEARCH_CALLBACK_PROC)(PSTR, PCSTR, UINT); | |
typedef unsigned char uch; | |
typedef unsigned short ush; | |
typedef unsigned long ulg; | |
#define SH(p) ((ush)(uch)((p)[1]) | ((ush)(uch)((p)[0]) << 8)) | |
#define LG(p) ((ulg)(SH((p) + 2)) | ((ulg)(SH(p)) << 16)) | |
// 8 - signature | |
// + 4 + 4 + 13 + 4 - IHDR | |
// + 4 + 4 + 40 + 4 - waLV | |
// + 4 + 4 + 0 + 4 - IEND | |
// #define MIN_PNG_FILE_SIZE 97 | |
#define MAP_TYPE_BIT 1 | |
#define MAP_TYPE_LEV 2 | |
#define MAP_TYPE_PNG 3 | |
#define TIMER_PROCESSING 0 | |
#define TIMER_TYPING 1 | |
#define TIMER_BLINKING 2 | |
#ifndef _DEBUG | |
PVOID malloc(UINT nSize) | |
{ | |
return HeapAlloc(GetProcessHeap(), 0, nSize); | |
} | |
PVOID realloc(PVOID pBuf, UINT nSize) | |
{ | |
return HeapReAlloc(GetProcessHeap(), 0, pBuf, nSize); | |
} | |
void free(PVOID pBuf) | |
{ | |
HeapFree(GetProcessHeap(), 0, pBuf); | |
} | |
UINT strlen(PCSTR pcsStr) | |
{ | |
PCSTR p1; | |
for (p1 = pcsStr; *p1; ++p1); | |
return (p1 - pcsStr); | |
} | |
int memcmp(const void * s1, const void * s2, size_t n) | |
{ | |
const unsigned char *p1 = s1, *p2 = s2; | |
while (n--) if (*p1 != *p2) return *p1 - *p2; else *p1++, *p2++; | |
return 0; | |
} | |
PSTR strstr(PCSTR pcsStr1, PCSTR pcsStr2) | |
{ | |
UINT nLen = strlen(pcsStr2); | |
while (*pcsStr1) | |
if (!memcmp(pcsStr1++, pcsStr2, nLen)) | |
return (PSTR)pcsStr1 - 1; | |
return NULL; | |
} | |
PVOID memcpy(PVOID pDst, const PVOID pSrc, UINT nCnt) | |
{ | |
PUCHAR pucDst = pDst; | |
const unsigned char * pucSrc = pSrc; | |
while (nCnt--) | |
*pucDst++ = *pucSrc++; | |
return pDst; | |
} | |
PSTR strcpy(PSTR psDst, PCSTR pcsSrc) | |
{ | |
PSTR psSave = psDst; | |
while (*psDst++ = *pcsSrc++); | |
return psSave; | |
} | |
int strcmp(PCSTR pcsStr1, PCSTR pcsStr2) | |
{ | |
for(; *pcsStr1 == *pcsStr2; ++pcsStr1, ++pcsStr2) | |
if (*pcsStr1 == 0) return 0; | |
return *(const unsigned char *)pcsStr1 - *(const unsigned char *)pcsStr2; | |
} | |
PSTR strchr(PCSTR pcsStr, int nChar) | |
{ | |
while (*pcsStr != (char)nChar) | |
if (!*pcsStr++) return NULL; | |
return (PSTR)pcsStr; | |
} | |
PSTR strrchr(PCSTR pcsStr, int nChar) | |
{ | |
PSTR psRet = NULL; | |
do | |
{ if (*pcsStr == (char)nChar) (PCSTR)psRet = pcsStr; } | |
while (*pcsStr++); | |
return psRet; | |
} | |
#endif | |
void CALLBACK BlinkingTimerProc(HWND hWnd, UINT nMsg, UINT nEventID, DWORD dwTime) | |
{ | |
char sTmp[256]; | |
int nLen = GetWindowText(g_hLblStatus, sTmp, sizeof(sTmp) - 2); | |
if (sTmp[nLen - 1] == '_') | |
sTmp[nLen - 1] = '\0'; | |
else | |
{ sTmp[nLen] = '_'; sTmp[nLen + 1] = '\0'; } | |
SetWindowText(g_hLblStatus, sTmp); | |
} | |
void CALLBACK TypingTimerProc(HWND hWnd, UINT nMsg, UINT nEventID, DWORD dwTime) | |
{ | |
g_sMsgTmp[g_nPos - 1] = g_csCurrentStatus[g_nPos - 1]; | |
g_sMsgTmp[g_nPos] = '_'; | |
g_sMsgTmp[g_nPos + 1] = '\0'; | |
SetWindowText(g_hLblStatus, g_sMsgTmp); | |
if (++g_nPos > g_nMessageLength) | |
KillTimer(g_hDlg, TIMER_TYPING); | |
} | |
void TypeStatusMessage(PCSTR csMessage, UINT nState) | |
{ | |
g_nState = nState; | |
g_csCurrentStatus = csMessage; | |
lstrcpy(&g_sMsgTmp[1], &csMessage[1]); | |
g_nMessageLength = strlen(csMessage); g_nPos = 1; | |
SetTimer(g_hDlg, TIMER_TYPING, 20, TypingTimerProc); | |
} | |
BOOL ExtractMapToFile(HANDLE hSrcFile, DWORD dwMapDataSize) | |
{ | |
UCHAR ucBuffer[1024]; | |
DWORD i, j, dwRemainder, dwActual; | |
HANDLE hDstFile = CreateFile(g_psPathMaps, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); | |
if (hDstFile == INVALID_HANDLE_VALUE) | |
return FALSE; | |
j = dwMapDataSize / sizeof(ucBuffer); | |
if (dwRemainder = dwMapDataSize % sizeof(ucBuffer)) | |
{ | |
if (!ReadFile(hSrcFile, ucBuffer, dwRemainder, &dwActual, NULL) || !dwActual) { CloseHandle(hDstFile); return FALSE; } | |
if (!WriteFile(hDstFile, ucBuffer, dwRemainder, &dwActual, NULL)) { CloseHandle(hDstFile); return FALSE; } | |
} | |
for (i = 0; i < j; i++) | |
{ | |
if (!ReadFile(hSrcFile, ucBuffer, sizeof(ucBuffer), &dwActual, NULL) || !dwActual) { CloseHandle(hDstFile); return FALSE; } | |
if (!WriteFile(hDstFile, ucBuffer, sizeof(ucBuffer), &dwActual, NULL)) { CloseHandle(hDstFile); return FALSE; } | |
} | |
CloseHandle(hDstFile); | |
return TRUE; | |
} | |
#define MAX_IDAT_TAGS_TO_FIND 2 | |
BOOL GetColourMapHash(HANDLE hFile) // размер карты не нужен | |
{ | |
md5_context ctx; | |
UCHAR ucBuffer[4]; | |
DWORD dwActual, dwSize, nIDATsFound; | |
BOOL bClose = FALSE; | |
if (hFile == INVALID_HANDLE_VALUE) | |
{ | |
bClose = TRUE; | |
hFile = CreateFile(g_psPathMaps, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); | |
if (hFile == INVALID_HANDLE_VALUE) | |
return FALSE; | |
} | |
if (0xFFFFFFFF == SetFilePointer(hFile, 33, NULL, FILE_CURRENT)) { if (bClose) CloseHandle(hFile); return FALSE; } | |
if (!ReadFile(hFile, ucBuffer, 4, &dwActual, NULL) || !dwActual) { if (bClose) CloseHandle(hFile); return FALSE; } | |
dwSize = LG(ucBuffer); | |
if (!ReadFile(hFile, ucBuffer, 4, &dwActual, NULL) || !dwActual) { if (bClose) CloseHandle(hFile); return FALSE; } | |
if ((ucBuffer[0] != 'w' || ucBuffer[1] != 'a' || ucBuffer[2] != 'L' || ucBuffer[3] != 'V') | |
&& (ucBuffer[0] != 'w' || ucBuffer[1] != '2' || ucBuffer[2] != 'l' || ucBuffer[3] != 'v')) | |
{ if (bClose) CloseHandle(hFile); return FALSE; } | |
if (0xFFFFFFFF == SetFilePointer(hFile, dwSize + 4, NULL, FILE_CURRENT)) { if (bClose) CloseHandle(hFile); return FALSE; } | |
md5_starts(&ctx); | |
nIDATsFound = 0; | |
for (;;) | |
{ | |
if (!ReadFile(hFile, ucBuffer, 4, &dwActual, NULL) || !dwActual) { if (bClose) CloseHandle(hFile); return FALSE; } | |
dwSize = LG(ucBuffer); | |
if (!ReadFile(hFile, ucBuffer, 4, &dwActual, NULL) || !dwActual) { if (bClose) CloseHandle(hFile); return FALSE; } | |
if (ucBuffer[0] == 'I' && ucBuffer[1] == 'E' && ucBuffer[2] == 'N' && ucBuffer[3] == 'D') | |
{ if (nIDATsFound) break; else { if (bClose) CloseHandle(hFile); return FALSE; }} | |
if (ucBuffer[0] == 'I' && ucBuffer[1] == 'D' && ucBuffer[2] == 'A' && ucBuffer[3] == 'T') | |
{ | |
if (0xFFFFFFFF == SetFilePointer(hFile, dwSize, NULL, FILE_CURRENT)) { if (bClose) CloseHandle(hFile); return FALSE; } | |
if (!ReadFile(hFile, ucBuffer, 4, &dwActual, NULL) || !dwActual) { if (bClose) CloseHandle(hFile); return FALSE; } | |
md5_update(&ctx, ucBuffer, sizeof(ucBuffer)); | |
if (++nIDATsFound == MAX_IDAT_TAGS_TO_FIND) | |
break; | |
} | |
else | |
if (0xFFFFFFFF == SetFilePointer(hFile, dwSize + 4, NULL, FILE_CURRENT)) { if (bClose) CloseHandle(hFile); return FALSE; } | |
} | |
if (bClose) CloseHandle(hFile); | |
md5_finish(&ctx, g_ucMD5Sum); | |
return TRUE; | |
} | |
BOOL GetMonochromeMapHash(HANDLE hFile, DWORD dwMapDataSize) | |
{ | |
md5_context ctx; | |
UCHAR ucBuffer[1024]; | |
DWORD i, j, dwRemainder, dwActual; | |
BOOL bClose = FALSE; | |
if (hFile == INVALID_HANDLE_VALUE) | |
{ | |
bClose = TRUE; | |
hFile = CreateFile(g_psPathMaps, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); | |
if (hFile == INVALID_HANDLE_VALUE) | |
return FALSE; | |
} | |
md5_starts(&ctx); | |
if (!dwMapDataSize) | |
dwMapDataSize = GetFileSize(hFile, NULL); | |
j = dwMapDataSize / sizeof(ucBuffer); | |
if (dwRemainder = dwMapDataSize % sizeof(ucBuffer)) | |
{ | |
if (!ReadFile(hFile, ucBuffer, dwRemainder, &dwActual, NULL) || !dwActual) { if (bClose) CloseHandle(hFile); return FALSE; } | |
md5_update(&ctx, ucBuffer, dwActual); | |
} | |
for (i = 0; i < j; i++) | |
{ | |
if (!ReadFile(hFile, ucBuffer, sizeof(ucBuffer), &dwActual, NULL) || !dwActual) { if (bClose) CloseHandle(hFile); return FALSE; } | |
md5_update(&ctx, ucBuffer, dwActual); | |
} | |
if (bClose) CloseHandle(hFile); | |
md5_finish(&ctx, g_ucMD5Sum); | |
return TRUE; | |
} | |
void FileSearch(PSTR psPathBuffer, PSTR psDirOffset, PCSTR * ppcsExtensions, UINT nExtensionsCount, SEARCH_CALLBACK_PROC pfnCallbackProc) | |
{ | |
HANDLE hSearch; | |
WIN32_FIND_DATA wfd; | |
UINT i, nLen; | |
PSTR p1, p2; | |
p1 = psDirOffset; | |
p1[0] = '*'; p1[1] = '.'; p1[2] = '*'; p1[3] = '\0'; | |
hSearch = FindFirstFile(psPathBuffer, &wfd); | |
if (hSearch == INVALID_HANDLE_VALUE) | |
return; | |
if (wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY | |
&& wfd.cFileName[0] == '.' && wfd.cFileName[1] == '\0') | |
{ | |
FindNextFile(hSearch, &wfd); | |
if (!FindNextFile(hSearch, &wfd)) | |
{ FindClose(hSearch); return; } | |
} | |
do | |
{ | |
if (wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) | |
{ | |
nLen = strlen(wfd.cFileName); | |
memcpy(p1, wfd.cFileName, nLen); | |
p1[nLen] = '\\'; p1[nLen + 1] = '\0'; | |
FileSearch(psPathBuffer, p1 + nLen + 1, ppcsExtensions, nExtensionsCount, pfnCallbackProc); | |
} | |
else | |
{ | |
p2 = strrchr(wfd.cFileName, '.'); | |
if (!p2) p2 = ""; else p2++; | |
for (i = 0; i < nExtensionsCount; i++) | |
if (lstrcmpi(p2, ppcsExtensions[i]) == 0) | |
{ pfnCallbackProc(p1, wfd.cFileName, i); break; } | |
} | |
} | |
while (FindNextFile(hSearch, &wfd)); | |
FindClose(hSearch); | |
} | |
UINT g_nFilesFound; | |
void CountingFileSearchCallback(PSTR psDir, PCSTR pcsFileName, UINT nExtensionIndex) | |
{ | |
g_nFilesFound++; | |
} | |
void HashExistingMaps__FSCallback(PSTR psDir, PCSTR pcsFileName, UINT nExtensionIndex) | |
{ | |
SendMessage(g_hPgbProgress, PBM_STEPIT, 0, 0); | |
strcpy(psDir, pcsFileName); | |
if (MAP_TYPE_PNG == nExtensionIndex + 1) | |
{ if (!GetColourMapHash(INVALID_HANDLE_VALUE)) return; } | |
else | |
{ if (!GetMonochromeMapHash(INVALID_HANDLE_VALUE, 0)) return; } | |
if (!Hashes_IsStored()) | |
Hashes_Add(); | |
} | |
void HashExistingMaps() | |
{ | |
PCSTR ppcsExtensions[] = { "bit", "lev", "png" }; | |
PSTR p1 = g_psPathMaps + g_nPathMapsLen; | |
Hashes_Reset(); | |
g_nFilesFound = 0; | |
FileSearch(g_psPathMaps, p1, ppcsExtensions, 3, CountingFileSearchCallback); | |
if (!g_nFilesFound) | |
return; | |
SendMessage(g_hPgbProgress, PBM_SETRANGE, 0, MAKELPARAM(0, g_nFilesFound)); | |
FileSearch(g_psPathMaps, p1, ppcsExtensions, 3, HashExistingMaps__FSCallback); | |
SendMessage(g_hPgbProgress, PBM_SETPOS, 0, 0); | |
} | |
DWORD g_dwLogBufSize; | |
PSTR g_psLogBuffer; | |
void GuessGameTypes__FSCallback(PSTR psDir, PCSTR pcsFileName, UINT nExtensionIndex) | |
{ | |
HANDLE hFile; | |
DWORD dwFileSize, dwActual; | |
PSTR p1, p2; | |
PSTR psGameFileName, psGameType, psGameAuthor; | |
UINT nGameFileNameLen; | |
SendMessage(g_hPgbProgress, PBM_STEPIT, 0, 0); | |
if (strstr(pcsFileName, "DirectIP - Direct IP") | |
|| strstr(pcsFileName, "DirectIP - - Direct IP")) | |
return; | |
strcpy(psDir, pcsFileName); | |
hFile = CreateFile(g_psPathLogs, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); | |
dwFileSize = GetFileSize(hFile, NULL); | |
if (dwFileSize < 30) | |
{ CloseHandle(hFile); return; } | |
if (dwFileSize + 1 > g_dwLogBufSize) | |
{ g_dwLogBufSize = dwFileSize + 1; g_psLogBuffer = (PSTR)realloc(g_psLogBuffer, g_dwLogBufSize); } | |
if (!ReadFile(hFile, g_psLogBuffer, dwFileSize, &dwActual, NULL) || !dwActual) { CloseHandle(hFile); return; } | |
CloseHandle(hFile); | |
g_psLogBuffer[dwActual] = '\0'; | |
if (!(p1 = strstr(g_psLogBuffer, "recorded as file \"User\\Games\\"))) | |
return; | |
psGameFileName = p1 + 29; | |
p1 = strchr(psGameFileName + 32, '"'); *p1 = '\0'; | |
nGameFileNameLen = p1 - psGameFileName; | |
psGameType = strstr(psDir + 23, " - ") + 3; | |
if (!(p1 = strchr(psGameType, '+'))) | |
p1 = strrchr(psGameType, '.'); | |
else | |
p1--; | |
*p1 = '\0'; | |
psGameAuthor = strrchr(psGameType, ' ') + 1; | |
*(psGameAuthor - 3) = '\0'; | |
if (strcmp(psGameAuthor, "HostingBuddy") == 0) | |
{ | |
if (p1 = strstr(psGameType, "#039")) | |
psGameType = p1 + (*(p1 + 4) == 's' ? 6 : 5); // #039s_ #039_ | |
else | |
{ | |
if (p1 = strstr(psGameType, "_for_")) | |
{ | |
while (p2 = strstr(p1 + 5, "_for_")) p1 = p2; | |
*p1 = '\0'; | |
} | |
} | |
} | |
else | |
{ if (*psGameType == 'Я' || *psGameType == '_') psGameType++; } | |
LoggedGames_Add(psGameFileName, nGameFileNameLen, psGameType, strlen(psGameType)); | |
} | |
void GuessGameTypes() | |
{ | |
PCSTR ppcsExtensions[] = { "log" }; | |
PSTR p1; | |
if (!g_nPathLogsLen) | |
return; | |
LoggedGames_Reset(); | |
p1 = g_psPathLogs + g_nPathLogsLen; | |
g_nFilesFound = 0; | |
FileSearch(g_psPathLogs, p1, ppcsExtensions, 1, CountingFileSearchCallback); | |
if (!g_nFilesFound) | |
return; | |
SendMessage(g_hPgbProgress, PBM_SETRANGE, 0, MAKELPARAM(0, g_nFilesFound)); | |
g_dwLogBufSize = LOG_BUFFER_SIZE; | |
g_psLogBuffer = (PSTR)malloc(LOG_BUFFER_SIZE); | |
FileSearch(g_psPathLogs, p1, ppcsExtensions, 1, GuessGameTypes__FSCallback); | |
free(g_psLogBuffer); | |
SendMessage(g_hPgbProgress, PBM_SETPOS, 0, 0); | |
} | |
UINT g_nDuplicates, g_nMapsExtracted; | |
void ExtractMaps__FSCallback(PSTR psDir, PCSTR pcsFileName, UINT nExtensionIndex) | |
{ | |
HANDLE hFile; | |
PSTR p1, p2; | |
int nLen, i; | |
unsigned char ucTmp[4 * 3]; | |
DWORD dwMapSize, dwMapType, dwActual, dwPointer; | |
SendMessage(g_hPgbProgress, PBM_STEPIT, 0, 0); | |
strcpy(psDir, pcsFileName); | |
hFile = CreateFile(g_psPathReplays, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); | |
if (!ReadFile(hFile, ucTmp, sizeof(ucTmp), &dwActual, NULL) | |
|| dwActual != sizeof(ucTmp) | |
|| ucTmp[0] != 'W' | |
|| ucTmp[1] != 'A') | |
{ CloseHandle(hFile); return; } | |
dwMapSize = *((DWORD *)(ucTmp + 4)) - 4; | |
dwMapType = *((DWORD *)(ucTmp + 8)); | |
if (dwMapType != MAP_TYPE_BIT && dwMapType != MAP_TYPE_LEV && dwMapType != MAP_TYPE_PNG) | |
{ CloseHandle(hFile); return; } | |
dwPointer = SetFilePointer(hFile, 0, NULL, FILE_CURRENT); | |
if (dwMapType == MAP_TYPE_PNG) | |
{ if (!GetColourMapHash(hFile)) { CloseHandle(hFile); return; } } | |
else | |
{ if (!GetMonochromeMapHash(hFile, dwMapSize)) { CloseHandle(hFile); return; } } | |
SetFilePointer(hFile, dwPointer, NULL, FILE_BEGIN); | |
if (Hashes_IsStored()) | |
{ g_nDuplicates++; CloseHandle(hFile); return; } | |
Hashes_Add(); | |
i = LoggedGames_Find(psDir); | |
p2 = g_psPathMaps + g_nPathMapsLen; | |
if (i != GAME_NOT_FOUND) | |
{ | |
if (p1 = strchr(psDir, '[')) | |
{ | |
nLen = p1 - psDir + 1; | |
memcpy(p2, psDir, nLen); | |
p2 += nLen; | |
strcpy(p2, g_pLoggedGames[i].psType); | |
p2 += g_pLoggedGames[i].nTypeLen; | |
p1 = strchr(p1 + 1, ']'); | |
nLen = strrchr(p1 + 1, '.') - p1 + 1; | |
memcpy(p2, p1, nLen); | |
p2 += nLen; | |
} | |
else | |
{ | |
// how can this happen? | |
nLen = strrchr(psDir, '.') - psDir; memcpy(p2, psDir, nLen); p2 += nLen; | |
p2[0] = ' '; p2[1] = '['; p2 += 2; | |
strcpy(p2, g_pLoggedGames[i].psType); | |
nLen = g_pLoggedGames[i].nTypeLen; p2[nLen] = ']'; p2[nLen + 1] = '.'; p2 += nLen + 2; | |
} | |
} | |
else | |
{ | |
nLen = strrchr(psDir, '.') - psDir + 1; | |
memcpy(p2, psDir, nLen); | |
p2 += nLen; | |
} | |
switch (dwMapType) | |
{ | |
case MAP_TYPE_BIT: strcpy(p2, "bit"); break; | |
case MAP_TYPE_LEV: strcpy(p2, "lev"); break; | |
default: strcpy(p2, "png"); | |
} | |
if (ExtractMapToFile(hFile, dwMapSize)) | |
g_nMapsExtracted++; | |
CloseHandle(hFile); | |
} | |
void ExtractMaps() | |
{ | |
PCSTR ppcsExtensions[] = { "WAgame" }; | |
PSTR p1 = g_psPathReplays + g_nPathReplaysLen; | |
g_nFilesFound = 0; g_nDuplicates = 0; g_nMapsExtracted = 0; | |
FileSearch(g_psPathReplays, p1, ppcsExtensions, 1, CountingFileSearchCallback); | |
if (!g_nFilesFound) | |
return; | |
SendMessage(g_hPgbProgress, PBM_SETRANGE, 0, MAKELPARAM(0, g_nFilesFound)); | |
FileSearch(g_psPathReplays, p1, ppcsExtensions, 1, ExtractMaps__FSCallback); | |
SendMessage(g_hPgbProgress, PBM_SETPOS, 0, 0); | |
} | |
DWORD WINAPI ProcessingThreadProc(PVOID pDummy) | |
{ | |
PSTR p1; | |
UINT nNotExportable; | |
TypeStatusMessage("Hashing existing maps...", STATE_NORMAL); | |
HashExistingMaps(); | |
if (!g_nLoggedGames || g_bLogsPathChanged) | |
{ | |
TypeStatusMessage("Guessing game types (reading \"User\\Logs\")...", STATE_NORMAL); | |
GuessGameTypes(); | |
g_bLogsPathChanged = FALSE; | |
} | |
TypeStatusMessage("Extracting maps...", STATE_NORMAL); | |
ExtractMaps(); | |
p1 = g_sMsgFinal; | |
p1 += wsprintf(p1, "%d maps extracted", g_nMapsExtracted); | |
if (g_nDuplicates) | |
{ p1 += wsprintf(p1, " (%d duplicates skipped", g_nDuplicates); } | |
if (nNotExportable = g_nFilesFound - g_nDuplicates - g_nMapsExtracted) | |
{ p1 += wsprintf(p1, "%s%d not exportable maps).", g_nDuplicates ? ", " : " (", nNotExportable); } | |
else | |
strcpy(p1, g_nDuplicates ? ")." : "."); | |
TypeStatusMessage(g_sMsgFinal, STATE_NORMAL); | |
g_bProcessing = FALSE; | |
return 0; | |
} | |
int CALLBACK BrowseCallbackProc(HWND hWnd, UINT nMsg, LPARAM lParam, LPARAM lpData) | |
{ | |
char sDir[MAX_PATH]; | |
switch(nMsg) | |
{ | |
case BFFM_INITIALIZED: | |
if (GetDlgItemText(g_hDlg, (int)lpData, sDir, sizeof(sDir))) | |
SendMessage(hWnd, BFFM_SETSELECTION, TRUE, (LPARAM)sDir); | |
break; | |
} | |
return 0; | |
} | |
// CoInitialize() call is needed | |
void ChooseFolder(int nControlId, PCSTR pcsTitle) | |
{ | |
BROWSEINFO bi = { 0 }; | |
char sPath[MAX_PATH]; | |
LPITEMIDLIST p_iil; | |
LPMALLOC pMalloc; | |
if (NOERROR != SHGetMalloc(&pMalloc)) | |
return; | |
bi.hwndOwner = g_hDlg; | |
bi.lpszTitle = pcsTitle; | |
bi.ulFlags = BIF_RETURNONLYFSDIRS; | |
/* | |
if (nControlId == ID_EDT_MAPS) | |
bi.ulFlags |= 0x0040; | |
*/ | |
bi.lpfn = BrowseCallbackProc; | |
bi.lParam = (LPARAM)nControlId; | |
if (p_iil = SHBrowseForFolder(&bi)) | |
{ | |
if (SHGetPathFromIDList(p_iil, sPath)) | |
{ | |
if (sPath[1] == ':' && sPath[0] >= 'A' && sPath[0] <= 'Z') | |
sPath[0] += 32; | |
SetDlgItemText(g_hDlg, nControlId, sPath); | |
} | |
pMalloc->lpVtbl->Free(pMalloc, p_iil); | |
} | |
pMalloc->lpVtbl->Release(pMalloc); | |
} | |
BOOL DirectoryExists(PCSTR pcsPath) | |
{ | |
DWORD dwAttrib = GetFileAttributes(pcsPath); | |
return (dwAttrib != 0xFFFFFFFF && dwAttrib & FILE_ATTRIBUTE_DIRECTORY); | |
} | |
BOOL ValidatePath(PSTR psPath, UINT nPathLen) | |
{ | |
UINT nFirstBSPos, i, nStartFrom; | |
UINT * pnFoundBSPos; | |
UINT nFoundBSCount; | |
if (nPathLen <= 3) | |
return FALSE; | |
nFirstBSPos = (psPath[0] == '\\' && psPath[2] != '\\') ? 1 : 2; | |
pnFoundBSPos = (UINT *)malloc((nPathLen - 3) * sizeof(UINT)); | |
nFoundBSCount = 0; | |
for (i = nFirstBSPos + 2; i < nPathLen; i++) | |
{ | |
if (psPath[i] == '\\') | |
{ | |
if (psPath[i - 1] == '.') | |
{ free(pnFoundBSPos); return FALSE; } | |
else | |
pnFoundBSPos[nFoundBSCount++] = i; | |
} | |
} | |
nStartFrom = (nFirstBSPos == 1 || psPath[0] == '\\') ? 1 : 0; | |
if (nStartFrom == 1 && nFoundBSCount < 2) | |
{ free(pnFoundBSPos); return FALSE; } | |
if (nFoundBSCount) | |
{ | |
i = nFoundBSCount - 1; | |
for (;;) | |
{ | |
psPath[pnFoundBSPos[i]] = '\0'; | |
if (DirectoryExists(psPath)) | |
{ psPath[pnFoundBSPos[i++]] = '\\'; break; } | |
if (i == nStartFrom) | |
break; | |
i--; | |
} | |
} | |
if (nStartFrom == 1 && i == nStartFrom) | |
{ free(pnFoundBSPos); return FALSE; } | |
if (!nFoundBSCount) | |
{ | |
if (!CreateDirectory(psPath, NULL)) | |
{ free(pnFoundBSPos); return FALSE; } | |
} | |
else | |
for(;;) | |
{ | |
if (!CreateDirectory(psPath, NULL)) | |
{ free(pnFoundBSPos); return FALSE; } | |
if (i == nFoundBSCount) | |
break; | |
psPath[pnFoundBSPos[i++]] = '\\'; | |
} | |
free(pnFoundBSPos); | |
return TRUE; | |
} | |
#define PATH_GOOD 0 | |
#define PATH_NOT_EXISTING 1 | |
#define PATH_INCORRECT 2 | |
UINT CheckEnteredPath(int nControlId) | |
{ | |
char sTmpPath[MAX_PATH]; | |
PSTR psPath; | |
PSTR psFilePart; | |
UINT * pnPathLen; | |
UINT nFirstBSPos, i; | |
switch (nControlId) | |
{ | |
case ID_EDT_WA : psPath = g_psPathLogs; pnPathLen = &g_nPathLogsLen; break; | |
case ID_EDT_REPLAYS : psPath = g_psPathReplays; pnPathLen = &g_nPathReplaysLen; break; | |
default : psPath = g_psPathMaps; pnPathLen = &g_nPathMapsLen; | |
} | |
if (!GetDlgItemText(g_hDlg, nControlId, sTmpPath, sizeof(sTmpPath))) | |
{ *pnPathLen = 0; return PATH_INCORRECT; } | |
*pnPathLen = GetFullPathName(sTmpPath, PATH_BUFFER_SIZE, psPath, &psFilePart); | |
if (*pnPathLen > PATH_BUFFER_SIZE || *pnPathLen < 3) | |
return PATH_INCORRECT; | |
if (psPath[*pnPathLen - 1] == '\\' && *pnPathLen != 3) | |
{ (*pnPathLen)--; psPath[*pnPathLen] = '\0';} | |
if (psPath[*pnPathLen - 1] == '.') | |
return PATH_INCORRECT; | |
if (psPath[1] == ':' && psPath[0] >= 'A' && psPath[0] <= 'Z') | |
psPath[0] += 32; | |
nFirstBSPos = (psPath[0] == '\\' && psPath[2] != '\\') ? 1 : 2; | |
for (i = nFirstBSPos + 1; i < *pnPathLen; i++) | |
{ | |
if (psPath[i] == ':' || psPath[i] == '*' || psPath[i] == '?' | |
|| psPath[i] == '"' || psPath[i] == '<' || psPath[i] == '>' || psPath[i] == '|') | |
return PATH_INCORRECT; | |
} | |
if (!DirectoryExists(psPath)) | |
return PATH_NOT_EXISTING; | |
SetDlgItemText(g_hDlg, nControlId, psPath); | |
return PATH_GOOD; | |
} | |
BOOL PathsAreGood() | |
{ | |
if (!g_nLoggedGames || g_bLogsPathChanged) | |
{ | |
if (g_pcsAlert_WA) | |
{ TypeStatusMessage(g_pcsAlert_WA, STATE_ALERT); return FALSE; } | |
if (g_bWarned_WA) | |
{ g_bWarned_WA = FALSE; SetDlgItemText(g_hDlg, ID_EDT_WA, ""); g_nPathLogsLen = 0; } | |
else | |
{ | |
if (PATH_GOOD != CheckEnteredPath(ID_EDT_WA)) | |
{ if (g_nPathLogsLen) { g_pcsAlert_WA = "Incorrect path to WA! Leave the field empty if WA is not installed."; TypeStatusMessage(g_pcsAlert_WA, STATE_ALERT); return FALSE; } } | |
else | |
{ | |
strcpy(g_psPathLogs + g_nPathLogsLen, "\\User\\Logs"); | |
g_nPathLogsLen += 10; | |
if (!DirectoryExists(g_psPathLogs)) | |
{ | |
g_bWarned_WA = TRUE; | |
TypeStatusMessage("\"User\\Logs\" not found. You can continue, but game types won't be defined.", STATE_WARNING); | |
return FALSE; | |
} | |
} | |
if (g_nPathLogsLen) | |
{ g_psPathLogs[g_nPathLogsLen++] = '\\'; g_psPathLogs[g_nPathLogsLen] = '\0'; } | |
} | |
} | |
if (g_pcsAlert_Replays) | |
{ TypeStatusMessage(g_pcsAlert_Replays, STATE_ALERT); return FALSE; } | |
if (PATH_GOOD != CheckEnteredPath(ID_EDT_REPLAYS)) | |
{ g_pcsAlert_Replays = "Incorrect replays (saved games) path!"; TypeStatusMessage(g_pcsAlert_Replays, STATE_ALERT); return FALSE; } | |
g_psPathReplays[g_nPathReplaysLen++] = '\\'; g_psPathReplays[g_nPathReplaysLen] = '\0'; | |
if (g_pcsAlert_Maps) | |
{ TypeStatusMessage(g_pcsAlert_Maps, STATE_ALERT); return FALSE; } | |
if (g_bWarned_Maps) | |
{ | |
g_bWarned_Maps = FALSE; | |
if (!ValidatePath(g_psPathMaps, g_nPathMapsLen)) | |
{ g_pcsAlert_Maps = "Can't create directory for maps. Please specify a valid path."; TypeStatusMessage(g_pcsAlert_Maps, STATE_ALERT); return FALSE; } | |
} | |
else | |
{ | |
UINT nResult = CheckEnteredPath(ID_EDT_MAPS); | |
if (nResult == PATH_INCORRECT) | |
{ g_pcsAlert_Maps = "Incorrect maps (saved levels) path!"; TypeStatusMessage(g_pcsAlert_Maps, STATE_ALERT); return FALSE; } | |
else if (nResult == PATH_NOT_EXISTING) | |
{ | |
SetDlgItemText(g_hDlg, ID_EDT_MAPS, g_psPathMaps); | |
g_bWarned_Maps = TRUE; TypeStatusMessage("Specified maps directory does not exist. Push the button again to create it.", STATE_WARNING); return FALSE; | |
} | |
} | |
g_psPathMaps[g_nPathMapsLen++] = '\\'; g_psPathMaps[g_nPathMapsLen] = '\0'; | |
return TRUE; | |
} | |
BOOL CALLBACK DialogProc(HWND hDlg, UINT nMsg, WPARAM wParam, LPARAM lParam) | |
{ | |
switch (nMsg) | |
{ | |
case WM_INITDIALOG: | |
{ | |
HKEY hKey; | |
g_hDlg = hDlg; | |
SendMessage(hDlg, WM_SETICON, ICON_SMALL, (LPARAM)LoadIcon(GetModuleHandle(NULL), MAKEINTRESOURCE(ID_ICO_MAIN))); | |
g_hNormalBrush = CreateSolidBrush(NORMAL_STATUS_BGCOLOR); | |
g_hWarningBrush = CreateSolidBrush(WARNING_STATUS_BGCOLOR); | |
g_hAlertBrush = CreateSolidBrush(ALERT_STATUS_BGCOLOR); | |
g_hBtnExtract = GetDlgItem(hDlg, IDOK); | |
g_hLblStatus = GetDlgItem(hDlg, ID_LBL_STATUS); | |
g_hPgbProgress = GetDlgItem(hDlg, ID_PGB_PROGRESS); | |
SendMessage(g_hPgbProgress, PBM_SETSTEP, 1, 0); | |
SetTimer(hDlg, TIMER_BLINKING, 500, BlinkingTimerProc); | |
if (ERROR_SUCCESS == RegOpenKeyEx(HKEY_CURRENT_USER, "Software\\Team17SoftwareLTD\\WormsArmageddon", 0, KEY_QUERY_VALUE, &hKey)) | |
{ | |
char sPath[MAX_PATH]; | |
DWORD dwBufSize = sizeof(sPath); | |
LONG nResult = RegQueryValueEx(hKey, "PATH", NULL, NULL, (PUCHAR)sPath, &dwBufSize); | |
RegCloseKey(hKey); | |
if (nResult == ERROR_SUCCESS && dwBufSize != 0 && dwBufSize <= MAX_PATH - 1 - 17) | |
{ | |
PSTR psOffset = sPath + dwBufSize - 1; | |
if (*(psOffset - 1) == '\\') *(--psOffset) = '\0'; | |
SetDlgItemText(hDlg, ID_EDT_WA, sPath); | |
strcpy(psOffset, "\\User\\Games"); | |
SetDlgItemText(hDlg, ID_EDT_REPLAYS, sPath); | |
strcpy(psOffset + 6, "SavedLevels"); | |
SetDlgItemText(hDlg, ID_EDT_MAPS, sPath); | |
TypeStatusMessage("Specify the paths and click on that button ----->", STATE_NORMAL); | |
break; | |
} | |
} | |
TypeStatusMessage("Specify the paths. Leave WA path field empty if you don't have WA installed.", STATE_NORMAL); | |
} | |
break; | |
case WM_CTLCOLORSTATIC: | |
if ((HWND)lParam == g_hLblStatus) | |
{ | |
switch (g_nState) | |
{ | |
case STATE_WARNING : SetBkColor((HDC)wParam, WARNING_STATUS_BGCOLOR); return (BOOL)g_hWarningBrush; | |
case STATE_ALERT: SetBkColor((HDC)wParam, ALERT_STATUS_BGCOLOR); return (BOOL)g_hAlertBrush; | |
default: SetBkColor((HDC)wParam, NORMAL_STATUS_BGCOLOR); return (BOOL)g_hNormalBrush; | |
} | |
} | |
else | |
return FALSE; | |
case WM_TIMER: | |
if (!g_bProcessing) | |
{ | |
KillTimer(hDlg, 0); | |
EnableWindow(g_hBtnExtract, TRUE); | |
} | |
break; | |
case WM_COMMAND: | |
if (HIWORD(wParam) == EN_CHANGE) | |
{ | |
switch (LOWORD(wParam)) | |
{ | |
case ID_EDT_WA: | |
if (g_bWarned_WA) g_bWarned_WA = FALSE; | |
if (g_pcsAlert_WA) g_pcsAlert_WA = NULL; | |
if (!g_bLogsPathChanged) g_bLogsPathChanged = TRUE; | |
return FALSE; | |
case ID_EDT_REPLAYS: | |
if (g_pcsAlert_Replays) g_pcsAlert_Replays = NULL; | |
return FALSE; | |
case ID_EDT_MAPS: | |
if (g_bWarned_Maps) g_bWarned_Maps = FALSE; | |
if (g_pcsAlert_Maps) g_pcsAlert_Maps = NULL; | |
return FALSE; | |
} | |
} | |
switch (LOWORD(wParam)) | |
{ | |
case IDOK: | |
if (!g_bProcessing && PathsAreGood()) | |
{ | |
DWORD dwThreadId; | |
g_bProcessing = TRUE; | |
EnableWindow(g_hBtnExtract, FALSE); | |
SetTimer(hDlg, TIMER_PROCESSING, 25, NULL); | |
CreateThread(NULL, 0, ProcessingThreadProc, NULL, 0, &dwThreadId); | |
return FALSE; | |
} | |
return FALSE; | |
case ID_BTN_WA: | |
ChooseFolder(ID_EDT_WA, "Select Worms Armageddon root folder:"); | |
return FALSE; | |
case ID_BTN_REPLAYS: | |
ChooseFolder(ID_EDT_REPLAYS, "Select the saved games (replays) folder:"); | |
return FALSE; | |
case ID_BTN_MAPS: | |
ChooseFolder(ID_EDT_MAPS, "Select the saved levels (maps) folder:"); | |
return FALSE; | |
} | |
break; | |
case WM_CLOSE: | |
EndDialog(hDlg, 0); | |
break; | |
default: | |
return FALSE; | |
} | |
return TRUE; | |
} | |
#ifdef _DEBUG | |
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, PSTR psCmdLine, int nShowCmd) | |
{ | |
#else | |
void _WinMain() | |
{ | |
HINSTANCE hInst = GetModuleHandle(NULL); | |
#endif | |
g_psPathLogs = (PSTR)malloc(PATH_BUFFER_SIZE * 3); | |
g_psPathReplays = g_psPathLogs + PATH_BUFFER_SIZE; g_psPathMaps = g_psPathLogs + PATH_BUFFER_SIZE * 2; | |
InitCommonControls(); | |
CoInitialize(NULL); | |
DialogBox(hInst, MAKEINTRESOURCE(ID_DLG_MAIN), HWND_DESKTOP, DialogProc); | |
free(g_psPathLogs); | |
if (g_nLoggedGames) | |
{ free(g_pLoggedGames); free(g_pucLoggedGamesData); } | |
if (g_nHashes) | |
free(g_pHashes); | |
#ifdef _DEBUG | |
return 0; | |
#else | |
ExitProcess(0); | |
#endif | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment