mirror of
https://github.com/stenzek/duckstation.git
synced 2024-11-23 22:09:53 +00:00
1044 lines
36 KiB
C++
1044 lines
36 KiB
C++
#include "RA_Interface.h"
|
|
|
|
#include <winhttp.h>
|
|
#include <cassert>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
|
|
#ifndef CCONV
|
|
#define CCONV __cdecl
|
|
#endif
|
|
|
|
// Initialization
|
|
static const char* (CCONV* _RA_IntegrationVersion)() = nullptr;
|
|
static const char* (CCONV* _RA_HostName)() = nullptr;
|
|
static const char* (CCONV* _RA_HostUrl)() = nullptr;
|
|
static int (CCONV* _RA_InitI)(HWND hMainWnd, int nConsoleID, const char* sClientVer) = nullptr;
|
|
static int (CCONV* _RA_InitOffline)(HWND hMainWnd, int nConsoleID, const char* sClientVer) = nullptr;
|
|
static int (CCONV* _RA_InitClient)(HWND hMainWnd, const char* sClientName, const char* sClientVer) = nullptr;
|
|
static int (CCONV* _RA_InitClientOffline)(HWND hMainWnd, const char* sClientName, const char* sClientVer) = nullptr;
|
|
static void (CCONV* _RA_InstallSharedFunctions)(int(*)(), void(*)(), void(*)(), void(*)(), void(*)(char*), void(*)(), void(*)(const char*)) = nullptr;
|
|
static void (CCONV* _RA_SetForceRepaint)(int bEnable) = nullptr;
|
|
static HMENU (CCONV* _RA_CreatePopupMenu)() = nullptr;
|
|
static int (CCONV* _RA_GetPopupMenuItems)(RA_MenuItem*) = nullptr;
|
|
static void (CCONV* _RA_InvokeDialog)(LPARAM nID) = nullptr;
|
|
static void (CCONV* _RA_SetUserAgentDetail)(const char* sDetail);
|
|
static void (CCONV* _RA_AttemptLogin)(int bBlocking) = nullptr;
|
|
static int (CCONV* _RA_SetConsoleID)(unsigned int nConsoleID) = nullptr;
|
|
static void (CCONV* _RA_ClearMemoryBanks)() = nullptr;
|
|
static void (CCONV* _RA_InstallMemoryBank)(int nBankID, RA_ReadMemoryFunc* pReader, RA_WriteMemoryFunc* pWriter, int nBankSize) = nullptr;
|
|
static void (CCONV* _RA_InstallMemoryBankBlockReader)(int nBankID, RA_ReadMemoryBlockFunc* pReader) = nullptr;
|
|
static int (CCONV* _RA_Shutdown)() = nullptr;
|
|
// Overlay
|
|
static int (CCONV* _RA_IsOverlayFullyVisible)() = nullptr;
|
|
static void (CCONV* _RA_SetPaused)(int bIsPaused) = nullptr;
|
|
static void (CCONV* _RA_NavigateOverlay)(ControllerInput* pInput) = nullptr;
|
|
static void (CCONV* _RA_UpdateHWnd)(HWND hMainHWND);
|
|
// Game Management
|
|
static unsigned int (CCONV* _RA_IdentifyRom)(const BYTE* pROM, unsigned int nROMSize) = nullptr;
|
|
static unsigned int (CCONV* _RA_IdentifyHash)(const char* sHash) = nullptr;
|
|
static void (CCONV* _RA_ActivateGame)(unsigned int nGameId) = nullptr;
|
|
static int (CCONV* _RA_OnLoadNewRom)(const BYTE* pROM, unsigned int nROMSize) = nullptr;
|
|
static int (CCONV* _RA_ConfirmLoadNewRom)(int bQuitting) = nullptr;
|
|
// Runtime Functionality
|
|
static void (CCONV* _RA_DoAchievementsFrame)() = nullptr;
|
|
static void (CCONV* _RA_SuspendRepaint)() = nullptr;
|
|
static void (CCONV* _RA_ResumeRepaint)() = nullptr;
|
|
static void (CCONV* _RA_UpdateAppTitle)(const char* pMessage) = nullptr;
|
|
static const char* (CCONV* _RA_UserName)() = nullptr;
|
|
static int (CCONV* _RA_HardcoreModeIsActive)(void) = nullptr;
|
|
static int (CCONV* _RA_WarnDisableHardcore)(const char* sActivity) = nullptr;
|
|
static void (CCONV* _RA_OnReset)() = nullptr;
|
|
static void (CCONV* _RA_OnSaveState)(const char* sFilename) = nullptr;
|
|
static void (CCONV* _RA_OnLoadState)(const char* sFilename) = nullptr;
|
|
static int (CCONV* _RA_CaptureState)(char* pBuffer, int nBufferSize) = nullptr;
|
|
static void (CCONV* _RA_RestoreState)(const char* pBuffer) = nullptr;
|
|
|
|
static HINSTANCE g_hRADLL = nullptr;
|
|
|
|
void RA_AttemptLogin(int bBlocking)
|
|
{
|
|
if (_RA_AttemptLogin != nullptr)
|
|
_RA_AttemptLogin(bBlocking);
|
|
}
|
|
|
|
const char* RA_UserName(void)
|
|
{
|
|
if (_RA_UserName != nullptr)
|
|
return _RA_UserName();
|
|
|
|
return "";
|
|
}
|
|
|
|
void RA_NavigateOverlay(ControllerInput* pInput)
|
|
{
|
|
if (_RA_NavigateOverlay != nullptr)
|
|
_RA_NavigateOverlay(pInput);
|
|
}
|
|
|
|
void RA_UpdateRenderOverlay(HDC hDC, ControllerInput* pInput, float fDeltaTime, RECT* prcSize, bool Full_Screen, bool Paused)
|
|
{
|
|
if (_RA_NavigateOverlay != nullptr)
|
|
_RA_NavigateOverlay(pInput);
|
|
}
|
|
|
|
int RA_IsOverlayFullyVisible(void)
|
|
{
|
|
if (_RA_IsOverlayFullyVisible != nullptr)
|
|
return _RA_IsOverlayFullyVisible();
|
|
|
|
return 0;
|
|
}
|
|
|
|
void RA_UpdateHWnd(HWND hMainWnd)
|
|
{
|
|
if (_RA_UpdateHWnd != nullptr)
|
|
_RA_UpdateHWnd(hMainWnd);
|
|
}
|
|
|
|
unsigned int RA_IdentifyRom(BYTE* pROMData, unsigned int nROMSize)
|
|
{
|
|
if (_RA_IdentifyRom != nullptr)
|
|
return _RA_IdentifyRom(pROMData, nROMSize);
|
|
|
|
return 0;
|
|
}
|
|
|
|
unsigned int RA_IdentifyHash(const char* sHash)
|
|
{
|
|
if (_RA_IdentifyHash!= nullptr)
|
|
return _RA_IdentifyHash(sHash);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void RA_ActivateGame(unsigned int nGameId)
|
|
{
|
|
if (_RA_ActivateGame != nullptr)
|
|
_RA_ActivateGame(nGameId);
|
|
}
|
|
|
|
void RA_OnLoadNewRom(BYTE* pROMData, unsigned int nROMSize)
|
|
{
|
|
if (_RA_OnLoadNewRom != nullptr)
|
|
_RA_OnLoadNewRom(pROMData, nROMSize);
|
|
}
|
|
|
|
void RA_ClearMemoryBanks(void)
|
|
{
|
|
if (_RA_ClearMemoryBanks != nullptr)
|
|
_RA_ClearMemoryBanks();
|
|
}
|
|
|
|
void RA_InstallMemoryBank(int nBankID, RA_ReadMemoryFunc pReader, RA_WriteMemoryFunc pWriter, int nBankSize)
|
|
{
|
|
if (_RA_InstallMemoryBank != nullptr)
|
|
_RA_InstallMemoryBank(nBankID, pReader, pWriter, nBankSize);
|
|
}
|
|
|
|
void RA_InstallMemoryBankBlockReader(int nBankID, RA_ReadMemoryBlockFunc pReader)
|
|
{
|
|
if (_RA_InstallMemoryBankBlockReader != nullptr)
|
|
_RA_InstallMemoryBankBlockReader(nBankID, pReader);
|
|
}
|
|
|
|
HMENU RA_CreatePopupMenu(void)
|
|
{
|
|
return (_RA_CreatePopupMenu != nullptr) ? _RA_CreatePopupMenu() : nullptr;
|
|
}
|
|
|
|
int RA_GetPopupMenuItems(RA_MenuItem *pItems)
|
|
{
|
|
return (_RA_GetPopupMenuItems != nullptr) ? _RA_GetPopupMenuItems(pItems) : 0;
|
|
}
|
|
|
|
void RA_UpdateAppTitle(const char* sCustomMsg)
|
|
{
|
|
if (_RA_UpdateAppTitle != nullptr)
|
|
_RA_UpdateAppTitle(sCustomMsg);
|
|
}
|
|
|
|
void RA_HandleHTTPResults(void)
|
|
{
|
|
}
|
|
|
|
int RA_ConfirmLoadNewRom(int bIsQuitting)
|
|
{
|
|
return _RA_ConfirmLoadNewRom ? _RA_ConfirmLoadNewRom(bIsQuitting) : 1;
|
|
}
|
|
|
|
void RA_InvokeDialog(LPARAM nID)
|
|
{
|
|
if (_RA_InvokeDialog != nullptr)
|
|
_RA_InvokeDialog(nID);
|
|
}
|
|
|
|
void RA_SetPaused(bool bIsPaused)
|
|
{
|
|
if (_RA_SetPaused != nullptr)
|
|
_RA_SetPaused(bIsPaused);
|
|
}
|
|
|
|
void RA_OnLoadState(const char* sFilename)
|
|
{
|
|
if (_RA_OnLoadState != nullptr)
|
|
_RA_OnLoadState(sFilename);
|
|
}
|
|
|
|
void RA_OnSaveState(const char* sFilename)
|
|
{
|
|
if (_RA_OnSaveState != nullptr)
|
|
_RA_OnSaveState(sFilename);
|
|
}
|
|
|
|
int RA_CaptureState(char* pBuffer, int nBufferSize)
|
|
{
|
|
if (_RA_CaptureState != nullptr)
|
|
return _RA_CaptureState(pBuffer, nBufferSize);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void RA_RestoreState(const char* pBuffer)
|
|
{
|
|
if (_RA_RestoreState != nullptr)
|
|
_RA_RestoreState(pBuffer);
|
|
}
|
|
|
|
void RA_OnReset(void)
|
|
{
|
|
if (_RA_OnReset != nullptr)
|
|
_RA_OnReset();
|
|
}
|
|
|
|
void RA_DoAchievementsFrame(void)
|
|
{
|
|
if (_RA_DoAchievementsFrame != nullptr)
|
|
_RA_DoAchievementsFrame();
|
|
}
|
|
|
|
void RA_SetForceRepaint(int bEnable)
|
|
{
|
|
if (_RA_SetForceRepaint != nullptr)
|
|
_RA_SetForceRepaint(bEnable);
|
|
}
|
|
|
|
void RA_SuspendRepaint(void)
|
|
{
|
|
if (_RA_SuspendRepaint != nullptr)
|
|
_RA_SuspendRepaint();
|
|
}
|
|
|
|
void RA_ResumeRepaint(void)
|
|
{
|
|
if (_RA_ResumeRepaint != nullptr)
|
|
_RA_ResumeRepaint();
|
|
}
|
|
|
|
void RA_SetConsoleID(unsigned int nConsoleID)
|
|
{
|
|
if (_RA_SetConsoleID != nullptr)
|
|
_RA_SetConsoleID(nConsoleID);
|
|
}
|
|
|
|
int RA_HardcoreModeIsActive(void)
|
|
{
|
|
return (_RA_HardcoreModeIsActive != nullptr) ? _RA_HardcoreModeIsActive() : 0;
|
|
}
|
|
|
|
int RA_WarnDisableHardcore(const char* sActivity)
|
|
{
|
|
// If Hardcore mode not active, allow the activity.
|
|
if (!RA_HardcoreModeIsActive())
|
|
return 1;
|
|
|
|
// DLL function will display a yes/no dialog. If the user chooses yes, the DLL will disable hardcore mode, and the activity can proceed.
|
|
if (_RA_WarnDisableHardcore != nullptr)
|
|
return _RA_WarnDisableHardcore(sActivity);
|
|
|
|
// We cannot disable hardcore mode, so just warn the user and prevent the activity.
|
|
std::string sMessage;
|
|
sMessage = "You cannot " + std::string(sActivity) + " while Hardcore mode is active.";
|
|
MessageBoxA(nullptr, sMessage.c_str(), "Warning", MB_OK | MB_ICONWARNING);
|
|
return 0;
|
|
}
|
|
|
|
void RA_DisableHardcore()
|
|
{
|
|
// passing nullptr to _RA_WarnDisableHardcore will just disable hardcore mode without prompting.
|
|
if (_RA_WarnDisableHardcore != nullptr)
|
|
_RA_WarnDisableHardcore(nullptr);
|
|
}
|
|
|
|
static size_t DownloadToFile(char* pData, size_t nDataSize, void* pUserData)
|
|
{
|
|
FILE* file = (FILE*)pUserData;
|
|
return fwrite(pData, 1, nDataSize, file);
|
|
}
|
|
|
|
typedef struct DownloadBuffer
|
|
{
|
|
char* pBuffer;
|
|
size_t nBufferSize;
|
|
size_t nOffset;
|
|
} DownloadBuffer;
|
|
|
|
static size_t DownloadToBuffer(char* pData, size_t nDataSize, void* pUserData)
|
|
{
|
|
DownloadBuffer* pBuffer = (DownloadBuffer*)pUserData;
|
|
const size_t nRemaining = pBuffer->nBufferSize - pBuffer->nOffset;
|
|
if (nDataSize > nRemaining)
|
|
nDataSize = nRemaining;
|
|
|
|
if (nDataSize > 0)
|
|
{
|
|
memcpy(pBuffer->pBuffer + pBuffer->nOffset, pData, nDataSize);
|
|
pBuffer->nOffset += nDataSize;
|
|
}
|
|
|
|
return nDataSize;
|
|
}
|
|
|
|
typedef size_t (DownloadFunc)(char* pData, size_t nDataSize, void* pUserData);
|
|
|
|
static BOOL DoBlockingHttpCall(const char* sHostUrl, const char* sRequestedPage, const char* sPostData,
|
|
DownloadFunc fnDownload, void* pDownloadUserData, DWORD* pBytesRead, DWORD* pStatusCode)
|
|
{
|
|
BOOL bResults = FALSE, bSuccess = FALSE;
|
|
HINTERNET hSession = nullptr, hConnect = nullptr, hRequest = nullptr;
|
|
size_t nHostnameLen;
|
|
|
|
WCHAR wBuffer[1024];
|
|
size_t nTemp;
|
|
DWORD nBytesAvailable = 0;
|
|
DWORD nBytesToRead = 0;
|
|
DWORD nBytesFetched = 0;
|
|
(*pBytesRead) = 0;
|
|
|
|
INTERNET_PORT nPort = INTERNET_DEFAULT_HTTP_PORT;
|
|
const char* sHostName = sHostUrl;
|
|
if (_strnicmp(sHostName, "http://", 7) == 0)
|
|
{
|
|
sHostName += 7;
|
|
}
|
|
else if (_strnicmp(sHostName, "https://", 8) == 0)
|
|
{
|
|
sHostName += 8;
|
|
nPort = INTERNET_DEFAULT_HTTPS_PORT;
|
|
}
|
|
|
|
const char* sPort = strchr(sHostName, ':');
|
|
if (sPort)
|
|
{
|
|
nHostnameLen = sPort - sHostName;
|
|
nPort = atoi(sPort + 1);
|
|
}
|
|
else
|
|
{
|
|
nHostnameLen = strlen(sHostName);
|
|
}
|
|
|
|
// Use WinHttpOpen to obtain a session handle.
|
|
hSession = WinHttpOpen(L"RetroAchievements Client Bootstrap",
|
|
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
|
|
WINHTTP_NO_PROXY_NAME,
|
|
WINHTTP_NO_PROXY_BYPASS, 0);
|
|
|
|
// Specify an HTTP server.
|
|
if (hSession == nullptr)
|
|
{
|
|
*pStatusCode = GetLastError();
|
|
}
|
|
else
|
|
{
|
|
#if defined(_MSC_VER) && _MSC_VER >= 1400
|
|
mbstowcs_s(&nTemp, wBuffer, sizeof(wBuffer) / sizeof(wBuffer[0]), sHostName, nHostnameLen);
|
|
#else
|
|
nTemp = mbstowcs(wBuffer, sHostName, nHostnameLen);
|
|
#endif
|
|
|
|
if (nTemp > 0)
|
|
{
|
|
hConnect = WinHttpConnect(hSession, wBuffer, nPort, 0);
|
|
}
|
|
|
|
// Create an HTTP Request handle.
|
|
if (hConnect == nullptr)
|
|
{
|
|
*pStatusCode = GetLastError();
|
|
}
|
|
else
|
|
{
|
|
#if defined(_MSC_VER) && _MSC_VER >= 1400
|
|
mbstowcs_s(&nTemp, wBuffer, sizeof(wBuffer) / sizeof(wBuffer[0]), sRequestedPage, strlen(sRequestedPage) + 1);
|
|
#else
|
|
nTemp = mbstowcs(wBuffer, sRequestedPage, strlen(sRequestedPage) + 1);
|
|
#endif
|
|
|
|
hRequest = WinHttpOpenRequest(hConnect,
|
|
sPostData ? L"POST" : L"GET",
|
|
wBuffer,
|
|
nullptr,
|
|
WINHTTP_NO_REFERER,
|
|
WINHTTP_DEFAULT_ACCEPT_TYPES,
|
|
(nPort == INTERNET_DEFAULT_HTTPS_PORT) ? WINHTTP_FLAG_SECURE : 0);
|
|
|
|
// Send a Request.
|
|
if (hRequest == nullptr)
|
|
{
|
|
*pStatusCode = GetLastError();
|
|
}
|
|
else
|
|
{
|
|
if (sPostData)
|
|
{
|
|
const size_t nPostDataLength = strlen(sPostData);
|
|
bResults = WinHttpSendRequest(hRequest,
|
|
L"Content-Type: application/x-www-form-urlencoded",
|
|
0, (LPVOID)sPostData, (DWORD)nPostDataLength, (DWORD)nPostDataLength, 0);
|
|
}
|
|
else
|
|
{
|
|
bResults = WinHttpSendRequest(hRequest,
|
|
L"Content-Type: application/x-www-form-urlencoded",
|
|
0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0);
|
|
}
|
|
|
|
if (!bResults || !WinHttpReceiveResponse(hRequest, nullptr))
|
|
{
|
|
*pStatusCode = GetLastError();
|
|
}
|
|
else
|
|
{
|
|
char buffer[16384];
|
|
DWORD dwSize = sizeof(DWORD);
|
|
WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, pStatusCode, &dwSize, WINHTTP_NO_HEADER_INDEX);
|
|
|
|
bSuccess = TRUE;
|
|
do
|
|
{
|
|
nBytesAvailable = 0;
|
|
WinHttpQueryDataAvailable(hRequest, &nBytesAvailable);
|
|
if (nBytesAvailable == 0)
|
|
break;
|
|
|
|
do
|
|
{
|
|
if (nBytesAvailable > sizeof(buffer))
|
|
nBytesToRead = sizeof(buffer);
|
|
else
|
|
nBytesToRead = nBytesAvailable;
|
|
|
|
nBytesFetched = 0;
|
|
if (WinHttpReadData(hRequest, buffer, nBytesToRead, &nBytesFetched))
|
|
{
|
|
size_t nBytesWritten = fnDownload(buffer, nBytesFetched, pDownloadUserData);
|
|
if (nBytesWritten < nBytesFetched)
|
|
{
|
|
if (*pStatusCode == 200)
|
|
*pStatusCode = 998;
|
|
|
|
bSuccess = FALSE;
|
|
break;
|
|
}
|
|
|
|
(*pBytesRead) += (DWORD)nBytesWritten;
|
|
nBytesAvailable -= nBytesFetched;
|
|
}
|
|
else
|
|
{
|
|
if (*pStatusCode == 200)
|
|
*pStatusCode = GetLastError();
|
|
|
|
bSuccess = FALSE;
|
|
break;
|
|
}
|
|
} while (nBytesAvailable > 0);
|
|
} while (TRUE);
|
|
}
|
|
|
|
WinHttpCloseHandle(hRequest);
|
|
}
|
|
|
|
WinHttpCloseHandle(hConnect);
|
|
}
|
|
|
|
WinHttpCloseHandle(hSession);
|
|
}
|
|
|
|
return bSuccess;
|
|
}
|
|
|
|
static BOOL IsNetworkError(DWORD nStatusCode)
|
|
{
|
|
switch (nStatusCode)
|
|
{
|
|
case 12002: // timeout
|
|
case 12007: // dns lookup failed
|
|
case 12017: // handle closed before request completed
|
|
case 12019: // handle not initialized
|
|
case 12028: // data not available at this time
|
|
case 12029: // handshake failed
|
|
case 12030: // connection aborted
|
|
case 12031: // connection reset
|
|
case 12032: // explicit request to retry
|
|
case 12152: // response could not be parsed, corrupt?
|
|
case 12163: // lost connection during request
|
|
return TRUE;
|
|
|
|
default:
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
static BOOL DoBlockingHttpCallWithRetry(const char* sHostUrl, const char* sRequestedPage, const char* sPostData,
|
|
char pBufferOut[], unsigned int nBufferOutSize, DWORD* pBytesRead, DWORD* pStatusCode)
|
|
{
|
|
int nRetries = 4;
|
|
do
|
|
{
|
|
DownloadBuffer downloadBuffer;
|
|
memset(&downloadBuffer, 0, sizeof(downloadBuffer));
|
|
downloadBuffer.pBuffer = pBufferOut;
|
|
downloadBuffer.nBufferSize = nBufferOutSize;
|
|
|
|
if (DoBlockingHttpCall(sHostUrl, sRequestedPage, sPostData, DownloadToBuffer, &downloadBuffer, pBytesRead, pStatusCode) != FALSE)
|
|
return TRUE;
|
|
|
|
if (!IsNetworkError(*pStatusCode))
|
|
return FALSE;
|
|
|
|
--nRetries;
|
|
} while (nRetries);
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static BOOL DoBlockingHttpCallWithRetry(const char* sHostUrl, const char* sRequestedPage, const char* sPostData,
|
|
FILE* pFile, DWORD* pBytesRead, DWORD* pStatusCode)
|
|
{
|
|
int nRetries = 4;
|
|
do
|
|
{
|
|
fseek(pFile, 0, SEEK_SET);
|
|
if (DoBlockingHttpCall(sHostUrl, sRequestedPage, sPostData, DownloadToFile, pFile, pBytesRead, pStatusCode) != FALSE)
|
|
return TRUE;
|
|
|
|
if (!IsNetworkError(*pStatusCode))
|
|
return FALSE;
|
|
|
|
--nRetries;
|
|
} while (nRetries);
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
#ifndef RA_UTEST
|
|
static std::wstring GetIntegrationPath()
|
|
{
|
|
wchar_t sBuffer[2048];
|
|
DWORD iIndex = GetModuleFileNameW(0, sBuffer, 2048);
|
|
while (iIndex > 0 && sBuffer[iIndex - 1] != '\\' && sBuffer[iIndex - 1] != '/')
|
|
--iIndex;
|
|
|
|
#if defined(_MSC_VER) && _MSC_VER >= 1400
|
|
wcscpy_s(&sBuffer[iIndex], sizeof(sBuffer)/sizeof(sBuffer[0]) - iIndex, L"RA_Integration.dll");
|
|
#else
|
|
wcscpy(&sBuffer[iIndex], L"RA_Integration.dll");
|
|
#endif
|
|
|
|
return std::wstring(sBuffer);
|
|
}
|
|
#endif
|
|
|
|
#if 0
|
|
|
|
static void FetchIntegrationFromWeb(char* sLatestVersionUrl, DWORD* pStatusCode)
|
|
{
|
|
DWORD nBytesRead = 0;
|
|
const wchar_t* sDownloadFilename = L"RA_Integration.download";
|
|
const wchar_t* sFilename = L"RA_Integration.dll";
|
|
const wchar_t* sOldFilename = L"RA_Integration.old";
|
|
|
|
#if defined(_MSC_VER) && _MSC_VER >= 1400
|
|
FILE* pf;
|
|
errno_t nErr = _wfopen_s(&pf, sDownloadFilename, L"wb");
|
|
#else
|
|
FILE* pf = _wfopen(sDownloadFilename, L"wb");
|
|
#endif
|
|
|
|
if (!pf)
|
|
{
|
|
#if defined(_MSC_VER) && _MSC_VER >= 1400
|
|
wchar_t sErrBuffer[2048];
|
|
_wcserror_s(sErrBuffer, sizeof(sErrBuffer) / sizeof(sErrBuffer[0]), nErr);
|
|
|
|
std::wstring sErrMsg = std::wstring(L"Unable to write ") + sOldFilename + L"\n" + sErrBuffer;
|
|
#else
|
|
std::wstring sErrMsg = std::wstring(L"Unable to write ") + sOldFilename + L"\n" + _wcserror(errno);
|
|
#endif
|
|
|
|
MessageBoxW(nullptr, sErrMsg.c_str(), L"Error", MB_OK | MB_ICONERROR);
|
|
return;
|
|
}
|
|
|
|
char* pSplit = sLatestVersionUrl + 8; /* skip over protocol */
|
|
while (*pSplit != '/')
|
|
{
|
|
if (!*pSplit)
|
|
{
|
|
*pStatusCode = 997;
|
|
return;
|
|
}
|
|
++pSplit;
|
|
}
|
|
*pSplit++ = '\0';
|
|
|
|
if (DoBlockingHttpCallWithRetry(sLatestVersionUrl, pSplit, nullptr, pf, &nBytesRead, pStatusCode))
|
|
{
|
|
fclose(pf);
|
|
|
|
/* wait up to one second for the DLL to actually be released - sometimes it's not immediate */
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
if (GetModuleHandleW(sFilename) == nullptr)
|
|
break;
|
|
|
|
Sleep(100);
|
|
}
|
|
|
|
// delete the last old dll if it's still present
|
|
DeleteFileW(sOldFilename);
|
|
|
|
// if there's a dll present, rename it
|
|
if (GetFileAttributesW(sFilename) != INVALID_FILE_ATTRIBUTES &&
|
|
!MoveFileW(sFilename, sOldFilename))
|
|
{
|
|
MessageBoxW(nullptr, L"Could not rename old dll", L"Error", MB_OK | MB_ICONERROR);
|
|
}
|
|
// rename the download to be the dll
|
|
else if (!MoveFileW(sDownloadFilename, sFilename))
|
|
{
|
|
MessageBoxW(nullptr, L"Could not rename new dll", L"Error", MB_OK | MB_ICONERROR);
|
|
}
|
|
|
|
// delete the old dll
|
|
DeleteFileW(sOldFilename);
|
|
}
|
|
else
|
|
{
|
|
fclose(pf);
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
//Returns the last Win32 error, in string format. Returns an empty string if there is no error.
|
|
static std::string GetLastErrorAsString()
|
|
{
|
|
//Get the error message, if any.
|
|
DWORD errorMessageID = ::GetLastError();
|
|
if (errorMessageID == 0)
|
|
return "No error message has been recorded";
|
|
|
|
LPSTR messageBuffer = nullptr;
|
|
size_t size = FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
|
nullptr, errorMessageID, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&messageBuffer, 0, nullptr);
|
|
|
|
std::string message(messageBuffer, size);
|
|
|
|
//Free the buffer.
|
|
LocalFree(messageBuffer);
|
|
|
|
return message;
|
|
}
|
|
|
|
static const char* CCONV _RA_InstallIntegration()
|
|
{
|
|
SetErrorMode(0);
|
|
|
|
std::wstring sIntegrationPath = GetIntegrationPath();
|
|
|
|
DWORD dwAttrib = GetFileAttributesW(sIntegrationPath.c_str());
|
|
if (dwAttrib == INVALID_FILE_ATTRIBUTES)
|
|
return "0.0";
|
|
|
|
g_hRADLL = LoadLibraryW(sIntegrationPath.c_str());
|
|
if (g_hRADLL == nullptr)
|
|
{
|
|
char buffer[1024];
|
|
sprintf_s(buffer, 1024, "Could not load RA_Integration.dll: %d\n%s\n", ::GetLastError(), GetLastErrorAsString().c_str());
|
|
MessageBoxA(nullptr, buffer, "Warning", MB_OK | MB_ICONWARNING);
|
|
|
|
return "0.0";
|
|
}
|
|
|
|
// Install function pointers one by one
|
|
|
|
_RA_IntegrationVersion = (const char* (CCONV*)()) GetProcAddress(g_hRADLL, "_RA_IntegrationVersion");
|
|
_RA_HostName = (const char* (CCONV*)()) GetProcAddress(g_hRADLL, "_RA_HostName");
|
|
_RA_HostUrl = (const char* (CCONV*)()) GetProcAddress(g_hRADLL, "_RA_HostUrl");
|
|
_RA_InitI = (int(CCONV*)(HWND, int, const char*)) GetProcAddress(g_hRADLL, "_RA_InitI");
|
|
_RA_InitOffline = (int(CCONV*)(HWND, int, const char*)) GetProcAddress(g_hRADLL, "_RA_InitOffline");
|
|
_RA_InitClient = (int(CCONV*)(HWND, const char*, const char*)) GetProcAddress(g_hRADLL, "_RA_InitClient");
|
|
_RA_InitClientOffline = (int(CCONV*)(HWND, const char*, const char*)) GetProcAddress(g_hRADLL, "_RA_InitClientOffline");
|
|
_RA_InstallSharedFunctions = (void(CCONV*)(int(*)(), void(*)(), void(*)(), void(*)(), void(*)(char*), void(*)(), void(*)(const char*))) GetProcAddress(g_hRADLL, "_RA_InstallSharedFunctionsExt");
|
|
_RA_SetForceRepaint = (void(CCONV*)(int)) GetProcAddress(g_hRADLL, "_RA_SetForceRepaint");
|
|
_RA_CreatePopupMenu = (HMENU(CCONV*)(void)) GetProcAddress(g_hRADLL, "_RA_CreatePopupMenu");
|
|
_RA_GetPopupMenuItems = (int(CCONV*)(RA_MenuItem*)) GetProcAddress(g_hRADLL, "_RA_GetPopupMenuItems");
|
|
_RA_InvokeDialog = (void(CCONV*)(LPARAM)) GetProcAddress(g_hRADLL, "_RA_InvokeDialog");
|
|
_RA_SetUserAgentDetail = (void(CCONV*)(const char*)) GetProcAddress(g_hRADLL, "_RA_SetUserAgentDetail");
|
|
_RA_AttemptLogin = (void(CCONV*)(int)) GetProcAddress(g_hRADLL, "_RA_AttemptLogin");
|
|
_RA_SetConsoleID = (int(CCONV*)(unsigned int)) GetProcAddress(g_hRADLL, "_RA_SetConsoleID");
|
|
_RA_ClearMemoryBanks = (void(CCONV*)()) GetProcAddress(g_hRADLL, "_RA_ClearMemoryBanks");
|
|
_RA_InstallMemoryBank = (void(CCONV*)(int, RA_ReadMemoryFunc*, RA_WriteMemoryFunc*, int)) GetProcAddress(g_hRADLL, "_RA_InstallMemoryBank");
|
|
_RA_InstallMemoryBankBlockReader = (void(CCONV*)(int, RA_ReadMemoryBlockFunc*)) GetProcAddress(g_hRADLL, "_RA_InstallMemoryBankBlockReader");
|
|
_RA_Shutdown = (int(CCONV*)()) GetProcAddress(g_hRADLL, "_RA_Shutdown");
|
|
_RA_IsOverlayFullyVisible = (int(CCONV*)()) GetProcAddress(g_hRADLL, "_RA_IsOverlayFullyVisible");
|
|
_RA_SetPaused = (void(CCONV*)(int)) GetProcAddress(g_hRADLL, "_RA_SetPaused");
|
|
_RA_NavigateOverlay = (void(CCONV*)(ControllerInput*)) GetProcAddress(g_hRADLL, "_RA_NavigateOverlay");
|
|
_RA_UpdateHWnd = (void(CCONV*)(HWND)) GetProcAddress(g_hRADLL, "_RA_UpdateHWnd");
|
|
_RA_IdentifyRom = (unsigned int(CCONV*)(const BYTE*, unsigned int)) GetProcAddress(g_hRADLL, "_RA_IdentifyRom");
|
|
_RA_IdentifyHash = (unsigned int(CCONV*)(const char*)) GetProcAddress(g_hRADLL, "_RA_IdentifyHash");
|
|
_RA_ActivateGame = (void(CCONV*)(unsigned int)) GetProcAddress(g_hRADLL, "_RA_ActivateGame");
|
|
_RA_OnLoadNewRom = (int(CCONV*)(const BYTE*, unsigned int)) GetProcAddress(g_hRADLL, "_RA_OnLoadNewRom");
|
|
_RA_ConfirmLoadNewRom = (int(CCONV*)(int)) GetProcAddress(g_hRADLL, "_RA_ConfirmLoadNewRom");
|
|
_RA_DoAchievementsFrame = (void(CCONV*)()) GetProcAddress(g_hRADLL, "_RA_DoAchievementsFrame");
|
|
_RA_SuspendRepaint = (void(CCONV*)()) GetProcAddress(g_hRADLL, "_RA_SuspendRepaint");
|
|
_RA_ResumeRepaint = (void(CCONV*)()) GetProcAddress(g_hRADLL, "_RA_ResumeRepaint");
|
|
_RA_UpdateAppTitle = (void(CCONV*)(const char*)) GetProcAddress(g_hRADLL, "_RA_UpdateAppTitle");
|
|
_RA_UserName = (const char* (CCONV*)()) GetProcAddress(g_hRADLL, "_RA_UserName");
|
|
_RA_HardcoreModeIsActive = (int(CCONV*)()) GetProcAddress(g_hRADLL, "_RA_HardcoreModeIsActive");
|
|
_RA_WarnDisableHardcore = (int(CCONV*)(const char*)) GetProcAddress(g_hRADLL, "_RA_WarnDisableHardcore");
|
|
_RA_OnReset = (void(CCONV*)()) GetProcAddress(g_hRADLL, "_RA_OnReset");
|
|
_RA_OnSaveState = (void(CCONV*)(const char*)) GetProcAddress(g_hRADLL, "_RA_OnSaveState");
|
|
_RA_OnLoadState = (void(CCONV*)(const char*)) GetProcAddress(g_hRADLL, "_RA_OnLoadState");
|
|
_RA_CaptureState = (int(CCONV*)(char*, int)) GetProcAddress(g_hRADLL, "_RA_CaptureState");
|
|
_RA_RestoreState = (void(CCONV*)(const char*)) GetProcAddress(g_hRADLL, "_RA_RestoreState");
|
|
|
|
return _RA_IntegrationVersion ? _RA_IntegrationVersion() : "0.0";
|
|
}
|
|
|
|
static void GetJsonField(const char* sJson, const char* sField, char *pBuffer, size_t nBufferSize)
|
|
{
|
|
const size_t nFieldSize = strlen(sField);
|
|
const char* pValue;
|
|
|
|
*pBuffer = 0;
|
|
do
|
|
{
|
|
const char* pScan = strstr(sJson, sField);
|
|
if (!pScan)
|
|
return;
|
|
|
|
if (pScan[-1] != '"' || pScan[nFieldSize] != '"')
|
|
{
|
|
sJson = pScan + 1;
|
|
continue;
|
|
}
|
|
|
|
pScan += nFieldSize + 1;
|
|
while (*pScan == ':' || isspace(*pScan))
|
|
++pScan;
|
|
if (*pScan != '"')
|
|
return;
|
|
|
|
pValue = ++pScan;
|
|
while (*pScan != '"')
|
|
{
|
|
if (!*pScan)
|
|
return;
|
|
|
|
++pScan;
|
|
}
|
|
|
|
while (pValue < pScan && nBufferSize > 1)
|
|
{
|
|
if (*pValue == '\\')
|
|
++pValue;
|
|
|
|
*pBuffer++ = *pValue++;
|
|
nBufferSize--;
|
|
}
|
|
|
|
*pBuffer = '\0';
|
|
return;
|
|
|
|
} while (1);
|
|
}
|
|
|
|
static unsigned long long ParseVersion(const char* sVersion)
|
|
{
|
|
char* pPart;
|
|
|
|
unsigned long long major = strtoul(sVersion, &pPart, 10);
|
|
if (*pPart == '.')
|
|
++pPart;
|
|
|
|
unsigned long long minor = strtoul(pPart, &pPart, 10);
|
|
if (*pPart == '.')
|
|
++pPart;
|
|
|
|
unsigned long long patch = strtoul(pPart, &pPart, 10);
|
|
if (*pPart == '.')
|
|
++pPart;
|
|
|
|
unsigned long long revision = strtoul(pPart, &pPart, 10);
|
|
|
|
// 64-bit max signed value is 9223 37203 68547 75807
|
|
unsigned long long version = (major * 100000) + minor;
|
|
version = (version * 100000) + patch;
|
|
version = (version * 100000) + revision;
|
|
return version;
|
|
}
|
|
|
|
static void RA_InitCommon(HWND hMainHWND, int nEmulatorID, const char* sClientName, const char* sClientVersion)
|
|
{
|
|
char sVerInstalled[32];
|
|
#if defined(_MSC_VER) && _MSC_VER >= 1400
|
|
strcpy_s(sVerInstalled, sizeof(sVerInstalled), _RA_InstallIntegration());
|
|
#else
|
|
strcpy(sVerInstalled, _RA_InstallIntegration());
|
|
#endif
|
|
|
|
char sHostUrl[256] = "";
|
|
if (_RA_HostUrl != nullptr)
|
|
{
|
|
#if defined(_MSC_VER) && _MSC_VER >= 1400
|
|
strcpy_s(sHostUrl, sizeof(sHostUrl), _RA_HostUrl());
|
|
#else
|
|
strcpy(sHostUrl, _RA_HostUrl());
|
|
#endif
|
|
}
|
|
else if (_RA_HostName != nullptr)
|
|
{
|
|
sprintf_s(sHostUrl, "http://%s", _RA_HostName());
|
|
}
|
|
|
|
if (!sHostUrl[0])
|
|
{
|
|
#if defined(_MSC_VER) && _MSC_VER >= 1400
|
|
strcpy_s(sHostUrl, sizeof(sHostUrl), "http://retroachievements.org");
|
|
#else
|
|
strcpy(sHostUrl, "http://retroachievements.org");
|
|
#endif
|
|
}
|
|
else if (_RA_InitOffline != nullptr && strcmp(sHostUrl, "http://OFFLINE") == 0)
|
|
{
|
|
if (sClientName == nullptr)
|
|
_RA_InitOffline(hMainHWND, nEmulatorID, sClientVersion);
|
|
else
|
|
_RA_InitClientOffline(hMainHWND, sClientName, sClientVersion);
|
|
return;
|
|
}
|
|
|
|
DWORD nBytesRead = 0;
|
|
DWORD nStatusCode = 0;
|
|
char buffer[1024];
|
|
ZeroMemory(buffer, 1024);
|
|
|
|
if (DoBlockingHttpCallWithRetry(sHostUrl, "dorequest.php", "r=latestintegration", buffer, sizeof(buffer), &nBytesRead, &nStatusCode) == FALSE)
|
|
{
|
|
if (_RA_InitOffline != nullptr)
|
|
{
|
|
sprintf_s(buffer, sizeof(buffer), "Cannot access %s (status code %u)\nWorking offline.", sHostUrl, nStatusCode);
|
|
MessageBoxA(hMainHWND, buffer, "Warning", MB_OK | MB_ICONWARNING);
|
|
|
|
_RA_InitOffline(hMainHWND, nEmulatorID, sClientVersion);
|
|
}
|
|
else
|
|
{
|
|
sprintf_s(buffer, sizeof(buffer), "Cannot access %s (status code %u)\nPlease try again later.", sHostUrl, nStatusCode);
|
|
MessageBoxA(hMainHWND, buffer, "Warning", MB_OK | MB_ICONWARNING);
|
|
|
|
RA_Shutdown();
|
|
}
|
|
return;
|
|
}
|
|
|
|
/* remove trailing zeros from client version */
|
|
char* ptr = sVerInstalled + strlen(sVerInstalled);
|
|
while (ptr[-1] == '0' && ptr[-2] == '.' && (ptr - 2) > sVerInstalled)
|
|
ptr -= 2;
|
|
*ptr = '\0';
|
|
if (strchr(sVerInstalled, '.') == NULL)
|
|
{
|
|
*ptr++ = '.';
|
|
*ptr++ = '0';
|
|
*ptr = '\0';
|
|
}
|
|
|
|
char sLatestVersionUrl[256];
|
|
char sVersionBuffer[32];
|
|
GetJsonField(buffer, "MinimumVersion", sVersionBuffer, sizeof(sVersionBuffer));
|
|
const unsigned long long nMinimumDLLVer = ParseVersion(sVersionBuffer);
|
|
|
|
GetJsonField(buffer, "LatestVersion", sVersionBuffer, sizeof(sVersionBuffer));
|
|
const unsigned long long nLatestDLLVer = ParseVersion(sVersionBuffer);
|
|
|
|
#if defined(_M_X64) || defined(__amd64__)
|
|
GetJsonField(buffer, "LatestVersionUrlX64", sLatestVersionUrl, sizeof(sLatestVersionUrl));
|
|
#else
|
|
GetJsonField(buffer, "LatestVersionUrl", sLatestVersionUrl, sizeof(sLatestVersionUrl));
|
|
#endif
|
|
|
|
if (nLatestDLLVer == 0 || !sLatestVersionUrl[0])
|
|
{
|
|
/* NOTE: repurposing sLatestVersionUrl for the error message */
|
|
GetJsonField(buffer, "Error", sLatestVersionUrl, sizeof(sLatestVersionUrl));
|
|
if (sLatestVersionUrl[0])
|
|
sprintf_s(buffer, sizeof(buffer), "Failed to fetch latest integration version.\n\n%s", sLatestVersionUrl);
|
|
else
|
|
sprintf_s(buffer, sizeof(buffer), "The latest integration check did not return a valid response.");
|
|
|
|
MessageBoxA(hMainHWND, buffer, "Error", MB_OK | MB_ICONERROR);
|
|
RA_Shutdown();
|
|
return;
|
|
}
|
|
|
|
int nMBReply = 0;
|
|
unsigned long long nVerInstalled = ParseVersion(sVerInstalled);
|
|
if (nVerInstalled < nMinimumDLLVer)
|
|
{
|
|
RA_Shutdown(); // Unhook the DLL so we can replace it.
|
|
|
|
if (nVerInstalled == 0)
|
|
{
|
|
sprintf_s(buffer, sizeof(buffer), "Install RetroAchievements toolset?\n\n"
|
|
"In order to earn achievements you must download the toolset library.");
|
|
}
|
|
else
|
|
{
|
|
sprintf_s(buffer, sizeof(buffer), "Upgrade RetroAchievements toolset?\n\n"
|
|
"A required upgrade to the toolset is available. If you don't upgrade, you won't be able to earn achievements.\n\n"
|
|
"Latest Version: %s\nInstalled Version: %s", sVersionBuffer, sVerInstalled);
|
|
}
|
|
|
|
nMBReply = MessageBoxA(hMainHWND, buffer, "Warning", MB_YESNO | MB_ICONWARNING);
|
|
}
|
|
else if (nVerInstalled < nLatestDLLVer)
|
|
{
|
|
sprintf_s(buffer, sizeof(buffer), "Upgrade RetroAchievements toolset?\n\n"
|
|
"An optional upgrade to the toolset is available.\n\n"
|
|
"Latest Version: %s\nInstalled Version: %s", sVersionBuffer, sVerInstalled);
|
|
|
|
nMBReply = MessageBoxA(hMainHWND, buffer, "Warning", MB_YESNO | MB_ICONWARNING);
|
|
|
|
if (nMBReply == IDYES)
|
|
RA_Shutdown(); // Unhook the DLL so we can replace it.
|
|
}
|
|
|
|
if (nMBReply == IDYES)
|
|
{
|
|
//FetchIntegrationFromWeb(sLatestVersionUrl, &nStatusCode);
|
|
nStatusCode = 0;
|
|
|
|
if (nStatusCode == 200)
|
|
nVerInstalled = ParseVersion(_RA_InstallIntegration());
|
|
|
|
if (nVerInstalled < nLatestDLLVer)
|
|
{
|
|
sprintf_s(buffer, sizeof(buffer), "Failed to update toolset (status code %u).", nStatusCode);
|
|
MessageBoxA(hMainHWND, buffer, "Error", MB_OK | MB_ICONERROR);
|
|
}
|
|
}
|
|
|
|
if (nVerInstalled < nMinimumDLLVer)
|
|
{
|
|
RA_Shutdown();
|
|
|
|
sprintf_s(buffer, sizeof(buffer), "%s toolset is required to earn achievements.", nVerInstalled == 0 ? "The" : "A newer");
|
|
MessageBoxA(hMainHWND, buffer, "Warning", MB_OK | MB_ICONWARNING);
|
|
}
|
|
else if (sClientName == nullptr)
|
|
{
|
|
if (!_RA_InitI(hMainHWND, nEmulatorID, sClientVersion))
|
|
RA_Shutdown();
|
|
}
|
|
else
|
|
{
|
|
if (!_RA_InitClient(hMainHWND, sClientName, sClientVersion))
|
|
RA_Shutdown();
|
|
}
|
|
}
|
|
|
|
void RA_Init(HWND hMainHWND, int nEmulatorID, const char* sClientVersion)
|
|
{
|
|
RA_InitCommon(hMainHWND, nEmulatorID, nullptr, sClientVersion);
|
|
}
|
|
|
|
void RA_InitClient(HWND hMainHWND, const char* sClientName, const char* sClientVersion)
|
|
{
|
|
RA_InitCommon(hMainHWND, -1, sClientName, sClientVersion);
|
|
}
|
|
|
|
void RA_SetUserAgentDetail(const char* sDetail)
|
|
{
|
|
if (_RA_SetUserAgentDetail != nullptr)
|
|
_RA_SetUserAgentDetail(sDetail);
|
|
}
|
|
|
|
void RA_InstallSharedFunctions(int(*)(void), void(*fpCauseUnpause)(void), void(*fpCausePause)(void), void(*fpRebuildMenu)(void), void(*fpEstimateTitle)(char*), void(*fpResetEmulation)(void), void(*fpLoadROM)(const char*))
|
|
{
|
|
if (_RA_InstallSharedFunctions != nullptr)
|
|
_RA_InstallSharedFunctions(nullptr, fpCauseUnpause, fpCausePause, fpRebuildMenu, fpEstimateTitle, fpResetEmulation, fpLoadROM);
|
|
}
|
|
|
|
void RA_Shutdown()
|
|
{
|
|
// Call shutdown on toolchain
|
|
if (_RA_Shutdown != nullptr)
|
|
{
|
|
_RA_Shutdown();
|
|
}
|
|
|
|
// Clear func ptrs
|
|
_RA_IntegrationVersion = nullptr;
|
|
_RA_HostName = nullptr;
|
|
_RA_HostUrl = nullptr;
|
|
_RA_InitI = nullptr;
|
|
_RA_InitOffline = nullptr;
|
|
_RA_InitClient = nullptr;
|
|
_RA_InitClientOffline = nullptr;
|
|
_RA_InstallSharedFunctions = nullptr;
|
|
_RA_SetForceRepaint = nullptr;
|
|
_RA_CreatePopupMenu = nullptr;
|
|
_RA_GetPopupMenuItems = nullptr;
|
|
_RA_InvokeDialog = nullptr;
|
|
_RA_SetUserAgentDetail = nullptr;
|
|
_RA_AttemptLogin = nullptr;
|
|
_RA_SetConsoleID = nullptr;
|
|
_RA_ClearMemoryBanks = nullptr;
|
|
_RA_InstallMemoryBank = nullptr;
|
|
_RA_InstallMemoryBankBlockReader = nullptr;
|
|
_RA_Shutdown = nullptr;
|
|
_RA_IsOverlayFullyVisible = nullptr;
|
|
_RA_SetPaused = nullptr;
|
|
_RA_NavigateOverlay = nullptr;
|
|
_RA_UpdateHWnd = nullptr;
|
|
_RA_IdentifyRom = nullptr;
|
|
_RA_IdentifyHash = nullptr;
|
|
_RA_ActivateGame = nullptr;
|
|
_RA_OnLoadNewRom = nullptr;
|
|
_RA_ConfirmLoadNewRom = nullptr;
|
|
_RA_DoAchievementsFrame = nullptr;
|
|
_RA_SuspendRepaint = nullptr;
|
|
_RA_ResumeRepaint = nullptr;
|
|
_RA_UpdateAppTitle = nullptr;
|
|
_RA_UserName = nullptr;
|
|
_RA_HardcoreModeIsActive = nullptr;
|
|
_RA_WarnDisableHardcore = nullptr;
|
|
_RA_OnReset = nullptr;
|
|
_RA_OnSaveState = nullptr;
|
|
_RA_OnLoadState = nullptr;
|
|
_RA_CaptureState = nullptr;
|
|
_RA_RestoreState = nullptr;
|
|
|
|
/* unload the DLL */
|
|
if (g_hRADLL)
|
|
{
|
|
FreeLibrary(g_hRADLL);
|
|
g_hRADLL = nullptr;
|
|
}
|
|
}
|