From 9a5ef2d0a25d1c0e7ddc0e38586168a287250b03 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Mon, 18 Apr 2022 15:09:21 +1000 Subject: [PATCH] Cheevos: Add RAIntergration support --- dep/rainterface/.gitignore | 1 + dep/rainterface/LICENSE | 21 + dep/rainterface/RA_Consoles.h | 83 ++ dep/rainterface/RA_Emulators.h | 23 + dep/rainterface/RA_Interface.cpp | 1021 +++++++++++++++++ dep/rainterface/RA_Interface.h | 368 ++++++ dep/rainterface/README.md | 5 + dep/rainterface/rainterface.vcxproj | 23 + dep/rainterface/rainterface.vcxproj.filters | 11 + duckstation.sln | 42 +- src/core/cheevos.cpp | 305 ++++- src/core/cheevos.h | 32 + src/core/core.props | 5 +- src/duckstation-qt/mainwindow.cpp | 37 + src/duckstation-qt/settingsdialog.cpp | 17 +- src/frontend-common/common_host_interface.cpp | 14 + src/frontend-common/fullscreen_ui.cpp | 11 + 17 files changed, 1997 insertions(+), 22 deletions(-) create mode 100644 dep/rainterface/.gitignore create mode 100644 dep/rainterface/LICENSE create mode 100644 dep/rainterface/RA_Consoles.h create mode 100644 dep/rainterface/RA_Emulators.h create mode 100644 dep/rainterface/RA_Interface.cpp create mode 100644 dep/rainterface/RA_Interface.h create mode 100644 dep/rainterface/README.md create mode 100644 dep/rainterface/rainterface.vcxproj create mode 100644 dep/rainterface/rainterface.vcxproj.filters diff --git a/dep/rainterface/.gitignore b/dep/rainterface/.gitignore new file mode 100644 index 000000000..5761abcfd --- /dev/null +++ b/dep/rainterface/.gitignore @@ -0,0 +1 @@ +*.o diff --git a/dep/rainterface/LICENSE b/dep/rainterface/LICENSE new file mode 100644 index 000000000..296dc5cae --- /dev/null +++ b/dep/rainterface/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 RetroAchievements.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/dep/rainterface/RA_Consoles.h b/dep/rainterface/RA_Consoles.h new file mode 100644 index 000000000..20aab4738 --- /dev/null +++ b/dep/rainterface/RA_Consoles.h @@ -0,0 +1,83 @@ +#ifndef RA_CONSOLES_H +#define RA_CONSOLES_H + +/* this list should match the list in rcheevos/include/rconsoles.h */ +enum ConsoleID +{ + UnknownConsoleID = 0, + MegaDrive = 1, + N64 = 2, + SNES = 3, + GB = 4, + GBA = 5, + GBC = 6, + NES = 7, + PCEngine = 8, + SegaCD = 9, + Sega32X = 10, + MasterSystem = 11, + PlayStation = 12, + Lynx = 13, + NeoGeoPocket = 14, + GameGear = 15, + GameCube = 16, + Jaguar = 17, + DS = 18, + WII = 19, + WIIU = 20, + PlayStation2 = 21, + Xbox = 22, + MagnavoxOdyssey = 23, + PokemonMini = 24, + Atari2600 = 25, + MSDOS = 26, + Arcade = 27, + VirtualBoy = 28, + MSX = 29, + C64 = 30, + ZX81 = 31, + Oric = 32, + SG1000 = 33, + VIC20 = 34, + Amiga = 35, + AtariST = 36, + AmstradCPC = 37, + AppleII = 38, + Saturn = 39, + Dreamcast = 40, + PSP = 41, + CDi = 42, + ThreeDO = 43, + Colecovision = 44, + Intellivision = 45, + Vectrex = 46, + PC8800 = 47, + PC9800 = 48, + PCFX = 49, + Atari5200 = 50, + Atari7800 = 51, + X68K = 52, + WonderSwan = 53, + CassetteVision = 54, + SuperCassetteVision = 55, + NeoGeoCD = 56, + FairchildChannelF = 57, + FMTowns = 58, + ZXSpectrum = 59, + GameAndWatch = 60, + NokiaNGage = 61, + Nintendo3DS = 62, + Supervision = 63, + SharpX1 = 64, + Tic80 = 65, + ThomsonTO8 = 66, + PC6000 = 67, + Pico = 68, + MegaDuck = 69, + Zeebo = 70, + Arduboy = 71, + + NumConsoleIDs +}; + +#endif /* !RA_CONSOLES_H */ diff --git a/dep/rainterface/RA_Emulators.h b/dep/rainterface/RA_Emulators.h new file mode 100644 index 000000000..ebaa70233 --- /dev/null +++ b/dep/rainterface/RA_Emulators.h @@ -0,0 +1,23 @@ +#ifndef RA_EMULATORS_H +#define RA_EMULATORS_H + +enum EmulatorID +{ + RA_Gens = 0, + RA_Project64 = 1, + RA_Snes9x = 2, + RA_VisualboyAdvance = 3, + RA_Nester = 4, + RA_FCEUX = 5, + RA_PCE = 6, + RA_Libretro = 7, + RA_Meka = 8, + RA_QUASI88 = 9, + RA_AppleWin = 10, + RA_Oricutron = 11, + + NumEmulatorIDs, + UnknownEmulator = NumEmulatorIDs +}; + +#endif /* !RA_EMULATORS_H */ diff --git a/dep/rainterface/RA_Interface.cpp b/dep/rainterface/RA_Interface.cpp new file mode 100644 index 000000000..ddf90c1c9 --- /dev/null +++ b/dep/rainterface/RA_Interface.cpp @@ -0,0 +1,1021 @@ +#include "RA_Interface.h" + +#include +#include +#include +#include + +#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 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 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); +} + +HMENU RA_CreatePopupMenu(void) +{ + return (_RA_CreatePopupMenu != nullptr) ? _RA_CreatePopupMenu() : nullptr; +} + +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; + + 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; + } + + // 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, strlen(sHostName) + 1); +#else + nTemp = mbstowcs(wBuffer, sHostName, strlen(sHostName) + 1); +#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_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_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) + { +#ifdef __cplusplus + try { +#endif + _RA_Shutdown(); +#ifdef __cplusplus + } + catch (std::runtime_error&) { + } +#endif + } + + // 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_InvokeDialog = nullptr; + _RA_SetUserAgentDetail = nullptr; + _RA_AttemptLogin = nullptr; + _RA_SetConsoleID = nullptr; + _RA_ClearMemoryBanks = nullptr; + _RA_InstallMemoryBank = 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; + } +} diff --git a/dep/rainterface/RA_Interface.h b/dep/rainterface/RA_Interface.h new file mode 100644 index 000000000..568a760cc --- /dev/null +++ b/dep/rainterface/RA_Interface.h @@ -0,0 +1,368 @@ +#ifndef RA_INTERFACE_H +#define RA_INTERFACE_H + +#include /* HWND */ + +#ifdef __cplusplus +extern "C" { +#endif + +/****************************** + * Initialization * + ******************************/ + +/** + * Loads and initializes the DLL. + * + * Must be called before using any of the other functions. Will automatically download the DLL + * if not found, or prompt the user to upgrade if a newer version is available + * + * @param hMainHWND the handle of the main window + * @param nEmulatorID the unique idenfier of the emulator + * @param sClientVersion the current version of the emulator (will be validated against the minimum version for the specified emulator ID) + */ +extern void RA_Init(HWND hMainHWND, int nEmulatorID, const char* sClientVersion); + +/** + * Loads and initializes the DLL. + * + * Must be called before using any of the other functions. Will automatically download the DLL + * if not found, or prompt the user to upgrade if a newer version is available + * + * @param hMainHWND the handle of the main window + * @param sClientName the name of the client, displayed in the title bar and included in the User-Agent for API calls + * @param sClientVersion the current version of the client + */ +extern void RA_InitClient(HWND hMainHWND, const char* sClientName, const char* sClientVersion); + +/** + * Defines callbacks that the DLL can use to interact with the client. + * + * @param fpUnusedIsActive [no longer used] returns non-zero if a game is running + * @param fpCauseUnpause unpauses the emulator + * @param fpCausePause pauses the emulator + * @param fpRebuildMenu notifies the client that the popup menu has changed (@see RA_CreatePopupMenu) + * @param fpEstimateTitle gets a short description for the game being loaded (parameter is a 256-byte buffer to fill) + * @param fpResetEmulator resets the emulator + * @param fpLoadROM [currently unused] tells the emulator to load a specific game + */ +extern void RA_InstallSharedFunctions(int (*fpUnusedIsActive)(void), + void (*fpCauseUnpause)(void), void (*fpCausePause)(void), void (*fpRebuildMenu)(void), + void (*fpEstimateTitle)(char*), void (*fpResetEmulator)(void), void (*fpLoadROM)(const char*)); + +/** + * Tells the DLL to use UpdateWindow instead of InvalidateRect when the UI needs to be repainted. This is primarily + * necessary when integrating with an emulator using the SDL library as it keeps the message queue flooded so the + * InvalidateRect messages never get turned into WM_PAINT messages. + * + * @param bEnable non-zero if InvalidateRect calls should be replaced with UpdateWindow + */ +extern void RA_SetForceRepaint(int bEnable); + +/** + * Creates a popup menu that can be appended to the main menu of the emulator. + * + * @return handle to the menu. if not attached to the program menu, caller must destroy it themselves. + */ +extern HMENU RA_CreatePopupMenu(void); + +/* Resource values for menu items - needed by MFC ON_COMMAND_RANGE macros or WM_COMMAND WndProc handlers + * they're not all currently used, allowing additional items without forcing recompilation of the emulators + */ +#define IDM_RA_MENUSTART 1700 +#define IDM_RA_MENUEND 1739 + +/** + * Called when a menu item in the popup menu is selected. + * + * @param nID the ID of the menu item (will be between IDM_RA_MENUSTART and IDM_RA_MENUEND) + */ +extern void RA_InvokeDialog(LPARAM nID); + +/** + * Provides additional information to include in the User-Agent string for API calls. + * + * This is primarily used to identify dependencies or configurations (such as libretro core versions) + * + * @param sDetail the additional information to include + */ +extern void RA_SetUserAgentDetail(const char* sDetail); + +/** + * Attempts to log in to the retroachievements.org site. + * + * Prompts the user for their login credentials and performs the login. If they've previously logged in and have + * chosen to store their credentials, the login occurs without prompting. + * + * @param bBlocking if zero, control is returned to the calling process while the login request is processed by the server. + */ +extern void RA_AttemptLogin(int bBlocking); + +/** + * Specifies the console associated to the emulator. + * + * May be called just before loading a game if the emulator supports multiple consoles. + * + * @param nConsoleID the unique identifier of the console associated to the game being loaded. + */ +extern void RA_SetConsoleID(unsigned int nConsoleID); + +/** + * Resets memory references created by previous calls to RA_InstallMemoryBank. + */ +extern void RA_ClearMemoryBanks(void); + +typedef unsigned char (RA_ReadMemoryFunc)(unsigned int nAddress); +typedef void (RA_WriteMemoryFunc)(unsigned int nAddress, unsigned char nValue); +/** + * Exposes a block of memory to the DLL. + * + * The blocks of memory expected by the DLL are unique per console ID. To identify the correct map for a console, + * view the consoleinfo.c file in the rcheevos repository. + * + * @param nBankID the index of the bank to update. will replace any existing bank at that index. + * @param pReader a function to read from the bank. parameter is the offset within the bank to read from. + * @param pWriter a function to write to the bank. parameters are the offset within the bank to write to and an 8-bit value to write. + * @param nBankSize the size of the bank. + */ +extern void RA_InstallMemoryBank(int nBankID, RA_ReadMemoryFunc pReader, RA_WriteMemoryFunc pWriter, int nBankSize); + +/** + * Deinitializes and unloads the DLL. + */ +extern void RA_Shutdown(void); + + + +/****************************** + * Overlay * + ******************************/ + +/** + * Determines if the overlay is fully visible. + * + * Precursor check before calling RA_NavigateOverlay + * + * @return non-zero if the overlay is fully visible, zero if it is not. + */ +extern int RA_IsOverlayFullyVisible(void); + +/** + * Called to show or hide the overlay. + * + * @param bIsPaused true to show the overlay, false to hide it. + */ +extern void RA_SetPaused(bool bIsPaused); + +struct ControllerInput +{ + int m_bUpPressed; + int m_bDownPressed; + int m_bLeftPressed; + int m_bRightPressed; + int m_bConfirmPressed; /* Usually C or A */ + int m_bCancelPressed; /* Usually B */ + int m_bQuitPressed; /* Usually Start */ +}; + +/** + * Passes controller input to the overlay. + * + * Does nothing if the overlay is not fully visible. + * + * @param pInput pointer to a ControllerInput structure indicating which inputs are active. + */ +extern void RA_NavigateOverlay(struct ControllerInput* pInput); + +/** + * [deprecated] Updates the overlay for a single frame. + * + * This function just calls RA_NavigateOverlay. Updating and rendering is now handled internally to the DLL. + */ +extern void RA_UpdateRenderOverlay(HDC, struct ControllerInput* pInput, float, RECT*, bool, bool); + +/** + * Updates the handle to the main window. + * + * The main window handle is used to anchor the overlay. If the client recreates the handle as the result of switching + * from windowed mode to full screen, or for any other reason, it should call this to reattach the overlay. + * + * @param hMainHWND the new handle of the main window + */ +extern void RA_UpdateHWnd(HWND hMainHWND); + + + +/****************************** + * Game Management * + ******************************/ + +/** + * Identifies the game associated to a block of memory. + * + * The block of memory is the fully buffered game file. If more complicated identification is required, the caller + * needs to link against rcheevos/rhash directly to generate the hash, and call RA_IdentifyHash with the result. + * Can be called when switching discs to ensure the additional discs are still associated to the loaded game. + * + * @param pROMData the contents of the game file + * @param nROMSize the size of the game file + * @return the unique identifier of the game, 0 if no association available. + */ +extern unsigned int RA_IdentifyRom(BYTE* pROMData, unsigned int nROMSize); + +/** + * Identifies the game associated to a pre-generated hash. + * + * Used when the hash algorithm is something other than full file. + * Can be called when switching discs to ensure the additional discs are still associated to the loaded game. + * + * @param sHash the hash generated by rcheevos/rhash + * @return the unique identifier of the game, 0 if no association available. + */ +extern unsigned int RA_IdentifyHash(const char* sHash); + +/** + * Fetches all retroachievements related data for the specified game. + * + * @param nGameId the unique identifier of the game to activate + */ +extern void RA_ActivateGame(unsigned int nGameId); + +/** + * Identifies and activates the game associated to a block of memory. + * + * Functions as a call to RA_IdentifyRom followed by a call to RA_ActivateGame. + * + * @param pROMData the contents of the game file + * @param nROMSize the size of the game file + */ +extern void RA_OnLoadNewRom(BYTE* pROMData, unsigned int nROMSize); + +/** + * Called before unloading the game to allow the user to save any changes they might have. + * + * @param bIsQuitting non-zero to change the messaging to indicate the emulator is closing. + * @return zero to abort the unload. non-zero to continue. + */ +extern int RA_ConfirmLoadNewRom(int bIsQuitting); + + + +/****************************** + * Runtime Functionality * + ******************************/ + +/** + * Does all achievement-related processing for a single frame. + */ +extern void RA_DoAchievementsFrame(void); + +/** + * Temporarily disables forced updating of tool windows. + * + * Primarily used while fast-forwarding. + */ +extern void RA_SuspendRepaint(void); + +/** + * Resumes forced updating of tool windows. + */ +extern void RA_ResumeRepaint(void); + +/** + * [deprecated] Used to be used to ensure the asynchronous server calls are processed on the UI thread. + * That's all managed within the DLL now. Calling this function does nothing. + */ +extern void RA_HandleHTTPResults(void); + +/** + * Adds flavor text to the application title bar. + * + * Application title bar is managed by the DLL. Value will be "ClientName - Version - Flavor Text - Username" + * + * @param sCustomMessage the flavor text to include in the title bar. + */ +extern void RA_UpdateAppTitle(const char* sCustomMessage); + +/** + * Get the user name of the currently logged in user. + * + * @return user name of the currently logged in user, empty if no user is logged in. + */ +const char* RA_UserName(void); + +/** + * Determines if the user is currently playing with hardcore enabled. + * + * The client should disable any features that would give the player an unfair advantage if this returns non-zero. + * Things like loading states, using cheats, modifying RAM, disabling rendering layers, viewing decoded tilemaps, etc. + * + * @return non-zero if hardcore mode is currently active. + */ +extern int RA_HardcoreModeIsActive(void); + +/** + * Warns the user they're about to do something that will disable hardcore mode. + * + * @param sActivity what the user is about to do (i.e. "load a state"). + * @return non-zero if the user disabled hardcore and the activity is allowed. + * zero if the user declined to disable hardcore and the activity should be aborted. + */ +extern int RA_WarnDisableHardcore(const char* sActivity); + +/** + * Disables hardcore mode without prompting or notifying the user. + * + * Should only be called if the client does its own prompting/notification. + * Typically used when an activity cannot be aborted. + */ +extern void RA_DisableHardcore(void); + +/** + * Notifies the DLL that the game has been reset. + * + * Disables active leaderboards and resets hit counts on all active achievements. + */ +extern void RA_OnReset(void); + +/** + * Notifies the DLL that a save state has been created. + * + * Creates a .rap file next to the state file that contains achievement-related information for the save state. + * + * @param sFilename full path to the save state file. + */ +extern void RA_OnSaveState(const char* sFilename); + +/** + * Notifies the DLL that a save state has been loaded. + * + * Loads the .rap file next to the state file that contains achievement-related information for the save state being loaded. + * + * @param sFilename full path to the save state file. + */ +extern void RA_OnLoadState(const char* sFilename); + +/** + * Captures the current state of the achievement runtime for inclusion in a save state. + * + * @param pBuffer buffer to write achievement state information to + * @param nBufferSize the size of the buffer + * @return the number of bytes needed to capture the achievement state. if less than nBufferSize, pBuffer + * will not be populated. the function should be called again with a larger buffer. + */ +extern int RA_CaptureState(char* pBuffer, int nBufferSize); + +/** + * Restores the state of the achievement runtime from a previously captured state. + * + * @param pBuffer buffer containing previously serialized achievement state information + */ +extern void RA_RestoreState(const char* pBuffer); + + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif // !RA_INTERFACE_H diff --git a/dep/rainterface/README.md b/dep/rainterface/README.md new file mode 100644 index 000000000..9fb572bd8 --- /dev/null +++ b/dep/rainterface/README.md @@ -0,0 +1,5 @@ +# RAInterface + +This code is intended to be loaded into another repository as a submodule. + +An emulator should include RA_Interface.cpp in its build and link against winhttp.lib. Then, the emulator can be modified to call the hooks provided in RA_Interface.cpp at appropriate times to integrate with the RetroAchievements server via the RA_Integration.dll. See wiki for more details. diff --git a/dep/rainterface/rainterface.vcxproj b/dep/rainterface/rainterface.vcxproj new file mode 100644 index 000000000..99911c7ad --- /dev/null +++ b/dep/rainterface/rainterface.vcxproj @@ -0,0 +1,23 @@ + + + + + {E4357877-D459-45C7-B8F6-DCBB587BB528} + + + + + + + + + + + + + TurnOffAllWarnings + $(ProjectDir);%(AdditionalIncludeDirectories) + + + + \ No newline at end of file diff --git a/dep/rainterface/rainterface.vcxproj.filters b/dep/rainterface/rainterface.vcxproj.filters new file mode 100644 index 000000000..e27696611 --- /dev/null +++ b/dep/rainterface/rainterface.vcxproj.filters @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/duckstation.sln b/duckstation.sln index bf30e5729..d3bec092b 100644 --- a/duckstation.sln +++ b/duckstation.sln @@ -26,10 +26,12 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "core", "src\core\core.vcxpr {EE054E08-3799-4A59-A422-18259C105FFD} = {EE054E08-3799-4A59-A422-18259C105FFD} {BB08260F-6FBC-46AF-8924-090EE71360C6} = {BB08260F-6FBC-46AF-8924-090EE71360C6} {8906836E-F06E-46E8-B11A-74E5E8C7B8FB} = {8906836E-F06E-46E8-B11A-74E5E8C7B8FB} + {E4357877-D459-45C7-B8F6-DCBB587BB528} = {E4357877-D459-45C7-B8F6-DCBB587BB528} {ED601289-AC1A-46B8-A8ED-17DB9EB73423} = {ED601289-AC1A-46B8-A8ED-17DB9EB73423} {09553C96-9F39-49BF-8AE6-7ACBD07C410C} = {09553C96-9F39-49BF-8AE6-7ACBD07C410C} {9C8DDEB0-2B8F-4F5F-BA86-127CDF27F035} = {9C8DDEB0-2B8F-4F5F-BA86-127CDF27F035} {7FF9FDB9-D504-47DB-A16A-B08071999620} = {7FF9FDB9-D504-47DB-A16A-B08071999620} + {4BA0A6D4-3AE1-42B2-9347-096FD023FF64} = {4BA0A6D4-3AE1-42B2-9347-096FD023FF64} EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "stb", "dep\stb\stb.vcxproj", "{ED601289-AC1A-46B8-A8ED-17DB9EB73423}" @@ -68,7 +70,6 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "frontend-common", "src\fron {933118A9-68C5-47B4-B151-B03C93961623} = {933118A9-68C5-47B4-B151-B03C93961623} {868B98C8-65A1-494B-8346-250A73A48C0A} = {868B98C8-65A1-494B-8346-250A73A48C0A} {3773F4CC-614E-4028-8595-22E08CA649E3} = {3773F4CC-614E-4028-8595-22E08CA649E3} - {4BA0A6D4-3AE1-42B2-9347-096FD023FF64} = {4BA0A6D4-3AE1-42B2-9347-096FD023FF64} EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "xxhash", "dep\xxhash\xxhash.vcxproj", "{09553C96-9F39-49BF-8AE6-7ACBD07C410C}" @@ -100,6 +101,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "rcheevos", "dep\rcheevos\rc EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "duckstation-regtest", "src\duckstation-regtest\duckstation-regtest.vcxproj", "{3029310E-4211-4C87-801A-72E130A648EF}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "rainterface", "dep\rainterface\rainterface.vcxproj", "{E4357877-D459-45C7-B8F6-DCBB587BB528}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -912,6 +915,42 @@ Global {3029310E-4211-4C87-801A-72E130A648EF}.ReleaseUWP|ARM64.ActiveCfg = ReleaseUWP|ARM64 {3029310E-4211-4C87-801A-72E130A648EF}.ReleaseUWP|x64.ActiveCfg = ReleaseUWP|x64 {3029310E-4211-4C87-801A-72E130A648EF}.ReleaseUWP|x86.ActiveCfg = ReleaseUWP|Win32 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.Debug|ARM64.Build.0 = Debug|ARM64 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.Debug|x64.ActiveCfg = Debug|x64 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.Debug|x64.Build.0 = Debug|x64 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.Debug|x86.ActiveCfg = Debug|Win32 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.Debug|x86.Build.0 = Debug|Win32 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.DebugFast|ARM64.ActiveCfg = DebugFast|ARM64 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.DebugFast|ARM64.Build.0 = DebugFast|ARM64 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.DebugFast|x64.ActiveCfg = DebugFast|x64 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.DebugFast|x64.Build.0 = DebugFast|x64 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.DebugFast|x86.ActiveCfg = DebugFast|Win32 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.DebugFast|x86.Build.0 = DebugFast|Win32 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.DebugUWP|ARM64.ActiveCfg = DebugUWP|ARM64 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.DebugUWP|ARM64.Build.0 = DebugUWP|ARM64 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.DebugUWP|x64.ActiveCfg = DebugUWP|x64 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.DebugUWP|x64.Build.0 = DebugUWP|x64 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.DebugUWP|x86.ActiveCfg = DebugUWP|Win32 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.DebugUWP|x86.Build.0 = DebugUWP|Win32 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.Release|ARM64.ActiveCfg = Release|ARM64 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.Release|ARM64.Build.0 = Release|ARM64 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.Release|x64.ActiveCfg = Release|x64 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.Release|x64.Build.0 = Release|x64 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.Release|x86.ActiveCfg = Release|Win32 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.Release|x86.Build.0 = Release|Win32 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.ReleaseLTCG|ARM64.ActiveCfg = ReleaseLTCG|ARM64 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.ReleaseLTCG|ARM64.Build.0 = ReleaseLTCG|ARM64 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.ReleaseLTCG|x64.ActiveCfg = ReleaseLTCG|x64 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.ReleaseLTCG|x64.Build.0 = ReleaseLTCG|x64 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.ReleaseLTCG|x86.ActiveCfg = ReleaseLTCG|Win32 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.ReleaseLTCG|x86.Build.0 = ReleaseLTCG|Win32 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.ReleaseUWP|ARM64.ActiveCfg = ReleaseUWP|ARM64 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.ReleaseUWP|ARM64.Build.0 = ReleaseUWP|ARM64 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.ReleaseUWP|x64.ActiveCfg = ReleaseUWP|x64 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.ReleaseUWP|x64.Build.0 = ReleaseUWP|x64 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.ReleaseUWP|x86.ActiveCfg = ReleaseUWP|Win32 + {E4357877-D459-45C7-B8F6-DCBB587BB528}.ReleaseUWP|x86.Build.0 = ReleaseUWP|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -935,6 +974,7 @@ Global {8906836E-F06E-46E8-B11A-74E5E8C7B8FB} = {BA490C0E-497D-4634-A21E-E65012006385} {39F0ADFF-3A84-470D-9CF0-CA49E164F2F3} = {BA490C0E-497D-4634-A21E-E65012006385} {4BA0A6D4-3AE1-42B2-9347-096FD023FF64} = {BA490C0E-497D-4634-A21E-E65012006385} + {E4357877-D459-45C7-B8F6-DCBB587BB528} = {BA490C0E-497D-4634-A21E-E65012006385} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {26E40B32-7C1D-48D0-95F4-1A500E054028} diff --git a/src/core/cheevos.cpp b/src/core/cheevos.cpp index d093c853f..1efd61557 100644 --- a/src/core/cheevos.cpp +++ b/src/core/cheevos.cpp @@ -22,11 +22,16 @@ #include "scmversion/scmversion.h" #include #include +#include #include #include #include Log_SetChannel(Cheevos); +#ifdef WITH_RAINTEGRATION +#include "RA_Interface.h" +#endif + namespace Cheevos { enum : s32 @@ -60,6 +65,11 @@ static bool s_unofficial_test_mode = false; static bool s_use_first_disc_from_playlist = true; static bool s_rich_presence_enabled = false; +#ifdef WITH_RAINTEGRATION +bool g_using_raintegration = false; +bool g_raintegration_initialized = false; +#endif + static rc_runtime_t s_rcheevos_runtime; static std::unique_ptr s_http_downloader; @@ -244,6 +254,10 @@ bool Initialize(bool test_mode, bool use_first_disc_from_playlist, bool enable_r g_active = true; g_challenge_mode = challenge_mode; +#ifdef WITH_RAINTEGRATION + g_using_raintegration = false; +#endif + s_test_mode = test_mode; s_unofficial_test_mode = include_unofficial; s_use_first_disc_from_playlist = use_first_disc_from_playlist; @@ -266,6 +280,14 @@ void Reset() if (!g_active) return; +#ifdef WITH_RAINTEGRATION + if (IsUsingRAIntegration()) + { + RA_OnReset(); + return; + } +#endif + Log_DevPrint("Resetting rcheevos state..."); rc_runtime_reset(&s_rcheevos_runtime); } @@ -298,6 +320,16 @@ void Update() if (HasActiveGame()) { +#ifdef WITH_RAINTEGRATION + if (IsUsingRAIntegration()) + { + if (g_raintegration_initialized) + RA_DoAchievementsFrame(); + + return; + } +#endif + rc_runtime_do_frame(&s_rcheevos_runtime, &CheevosEventHandler, &CheevosPeek, nullptr, nullptr); UpdateRichPresence(); @@ -332,7 +364,20 @@ bool DoState(StateWrapper& sw) { // reset runtime, no data (state might've been created without cheevos) Log_DevPrintf("State is missing cheevos data, resetting runtime"); +#ifdef WITH_RAINTEGRATION + if (IsUsingRAIntegration()) + { + if (g_raintegration_initialized) + RA_OnReset(); + } + else + { + rc_runtime_reset(&s_rcheevos_runtime); + } +#else rc_runtime_reset(&s_rcheevos_runtime); +#endif + return !sw.HasError(); } @@ -341,30 +386,64 @@ bool DoState(StateWrapper& sw) if (sw.HasError()) return false; - const int result = rc_runtime_deserialize_progress(&s_rcheevos_runtime, data.get(), nullptr); - if (result != RC_OK) +#ifdef WITH_RAINTEGRATION + if (IsUsingRAIntegration() && g_raintegration_initialized) { - Log_WarningPrintf("Failed to deserialize cheevos state (%d), resetting", result); - rc_runtime_reset(&s_rcheevos_runtime); + RA_RestoreState(reinterpret_cast(data.get())); } + else + { + const int result = rc_runtime_deserialize_progress(&s_rcheevos_runtime, data.get(), nullptr); + if (result != RC_OK) + { + Log_WarningPrintf("Failed to deserialize cheevos state (%d), resetting", result); + rc_runtime_reset(&s_rcheevos_runtime); + } + } +#endif return true; } else { - // internally this happens twice.. not great. - const int size = rc_runtime_progress_size(&s_rcheevos_runtime, nullptr); + u32 data_size; + std::unique_ptr data; - u32 data_size = (size >= 0) ? static_cast(size) : 0; - std::unique_ptr data(new u8[data_size]); - - const int result = rc_runtime_serialize_progress(data.get(), &s_rcheevos_runtime, nullptr); - if (result != RC_OK) +#ifdef WITH_RAINTEGRATION + if (IsUsingRAIntegration()) { - // set data to zero, effectively serializing nothing - Log_WarningPrintf("Failed to serialize cheevos state (%d)", result); - data_size = 0; + if (g_raintegration_initialized) + { + const int size = RA_CaptureState(nullptr, 0); + + data_size = (size >= 0) ? static_cast(size) : 0; + data = std::unique_ptr(new u8[data_size]); + + const int result = RA_CaptureState(reinterpret_cast(data.get()), static_cast(data_size)); + if (result != static_cast(data_size)) + { + Log_WarningPrint("Failed to serialize cheevos state from RAIntegration."); + data_size = 0; + } + } } + else + { + // internally this happens twice.. not great. + const int size = rc_runtime_progress_size(&s_rcheevos_runtime, nullptr); + + data_size = (size >= 0) ? static_cast(size) : 0; + data = std::unique_ptr(new u8[data_size]); + + const int result = rc_runtime_serialize_progress(data.get(), &s_rcheevos_runtime, nullptr); + if (result != RC_OK) + { + // set data to zero, effectively serializing nothing + Log_WarningPrintf("Failed to serialize cheevos state (%d)", result); + data_size = 0; + } + } +#endif sw.Do(&data_size); if (data_size > 0) @@ -468,7 +547,7 @@ bool LoginAsync(const char* username, const char* password) { s_http_downloader->WaitForAllRequests(); - if (s_logged_in || std::strlen(username) == 0 || std::strlen(password) == 0) + if (s_logged_in || std::strlen(username) == 0 || std::strlen(password) == 0 || IsUsingRAIntegration()) return false; if (ImGuiFullscreen::IsInitialized()) @@ -487,7 +566,7 @@ bool Login(const char* username, const char* password) if (g_active) s_http_downloader->WaitForAllRequests(); - if (s_logged_in || std::strlen(username) == 0 || std::strlen(password) == 0) + if (s_logged_in || std::strlen(username) == 0 || std::strlen(password) == 0 || IsUsingRAIntegration()) return false; if (g_active) @@ -1016,8 +1095,10 @@ static void GetGameIdCallback(s32 status_code, const FrontendCommon::HTTPDownloa const u32 game_id = (doc.HasMember("GameID") && doc["GameID"].IsUint()) ? doc["GameID"].GetUint() : 0; Log_InfoPrintf("Server returned GameID %u", game_id); - if (game_id != 0) - GetPatches(game_id); + if (game_id == 0) + return; + + GetPatches(game_id); } void GameChanged() @@ -1089,6 +1170,14 @@ void GameChanged(const std::string& path, CDImage* image) return; } +#ifdef WITH_RAINTEGRATION + if (IsUsingRAIntegration()) + { + RAIntegration::GameChanged(); + return; + } +#endif + char url[256]; int res = rc_url_get_gameid(url, sizeof(url), s_game_hash.c_str()); Assert(res == 0); @@ -1492,4 +1581,184 @@ unsigned CheevosPeek(unsigned address, unsigned num_bytes, void* ud) } } +#ifdef WITH_RAINTEGRATION + +#include "RA_Consoles.h" + +static int RACallbackIsActive(); +static void RACallbackCauseUnpause(); +static void RACallbackCausePause(); +static void RACallbackRebuildMenu(); +static void RACallbackEstimateTitle(char* buf); +static void RACallbackResetEmulator(); +static void RACallbackLoadROM(const char* unused); +static unsigned char RACallbackReadMemory(unsigned int address); +static void RACallbackWriteMemory(unsigned int address, unsigned char value); + +void SwitchToRAIntegration() +{ + g_using_raintegration = true; + g_raintegration_initialized = false; + g_active = true; + s_logged_in = true; +} + +static void InitializeRAIntegration(void* main_window_handle) +{ + RA_InitClient((HWND)main_window_handle, "DuckStation", g_scm_tag_str); + RA_SetUserAgentDetail(Cheevos::GetUserAgent().c_str()); + + RA_InstallSharedFunctions(RACallbackIsActive, RACallbackCauseUnpause, RACallbackCausePause, RACallbackRebuildMenu, + RACallbackEstimateTitle, RACallbackResetEmulator, RACallbackLoadROM); + RA_SetConsoleID(PlayStation); + + // Apparently this has to be done early, or the memory inspector doesn't work. + // That's a bit unfortunate, because the RAM size can vary between games, and depending on the option. + RA_InstallMemoryBank(0, RACallbackReadMemory, RACallbackWriteMemory, Bus::RAM_2MB_SIZE); + + // Fire off a login anyway. Saves going into the menu and doing it. + RA_AttemptLogin(0); + + g_challenge_mode = RA_HardcoreModeIsActive() != 0; + g_raintegration_initialized = true; + + // this is pretty lame, but we may as well persist until we exit anyway + std::atexit(RA_Shutdown); +} + +void RAIntegration::MainWindowChanged(void* new_handle) +{ + if (g_raintegration_initialized) + { + RA_UpdateHWnd((HWND)new_handle); + return; + } + + InitializeRAIntegration(new_handle); +} + +void RAIntegration::GameChanged() +{ + g_game_id = RA_IdentifyHash(s_game_hash.c_str()); + RA_ActivateGame(g_game_id); +} + +std::vector> RAIntegration::GetMenuItems() +{ + // NOTE: I *really* don't like doing this. But sadly it's the only way we can integrate with Qt. + static constexpr int IDM_RA_RETROACHIEVEMENTS = 1700; + static constexpr int IDM_RA_OVERLAYSETTINGS = 1701; + static constexpr int IDM_RA_FILES_MEMORYBOOKMARKS = 1703; + static constexpr int IDM_RA_FILES_ACHIEVEMENTS = 1704; + static constexpr int IDM_RA_FILES_MEMORYFINDER = 1705; + static constexpr int IDM_RA_FILES_LOGIN = 1706; + static constexpr int IDM_RA_FILES_LOGOUT = 1707; + static constexpr int IDM_RA_FILES_ACHIEVEMENTEDITOR = 1708; + static constexpr int IDM_RA_HARDCORE_MODE = 1710; + static constexpr int IDM_RA_REPORTBROKENACHIEVEMENTS = 1711; + static constexpr int IDM_RA_GETROMCHECKSUM = 1712; + static constexpr int IDM_RA_OPENUSERPAGE = 1713; + static constexpr int IDM_RA_OPENGAMEPAGE = 1714; + static constexpr int IDM_RA_PARSERICHPRESENCE = 1716; + static constexpr int IDM_RA_TOGGLELEADERBOARDS = 1717; + static constexpr int IDM_RA_NON_HARDCORE_WARNING = 1718; + + std::vector> ret; + + const char* username = RA_UserName(); + if (!username || std::strlen(username) == 0) + { + ret.emplace_back(IDM_RA_FILES_LOGIN, "&Login"); + } + else + { + ret.emplace_back(IDM_RA_FILES_LOGOUT, "Log&out"); + ret.emplace_back(0, nullptr); + ret.emplace_back(IDM_RA_OPENUSERPAGE, "Open my &User Page"); + ret.emplace_back(IDM_RA_OPENGAMEPAGE, "Open this &Game's Page"); + ret.emplace_back(0, nullptr); + ret.emplace_back(IDM_RA_HARDCORE_MODE, "&Hardcore Mode"); + ret.emplace_back(IDM_RA_NON_HARDCORE_WARNING, "Non-Hardcore &Warning"); + ret.emplace_back(0, nullptr); + ret.emplace_back(IDM_RA_TOGGLELEADERBOARDS, "Enable &Leaderboards"); + ret.emplace_back(IDM_RA_OVERLAYSETTINGS, "O&verlay Settings"); + ret.emplace_back(0, nullptr); + ret.emplace_back(IDM_RA_FILES_ACHIEVEMENTS, "Assets Li&st"); + ret.emplace_back(IDM_RA_FILES_ACHIEVEMENTEDITOR, "Assets &Editor"); + ret.emplace_back(IDM_RA_FILES_MEMORYFINDER, "&Memory Inspector"); + ret.emplace_back(IDM_RA_FILES_MEMORYBOOKMARKS, "Memory &Bookmarks"); + ret.emplace_back(IDM_RA_PARSERICHPRESENCE, "Rich &Presence Monitor"); + ret.emplace_back(0, nullptr); + ret.emplace_back(IDM_RA_REPORTBROKENACHIEVEMENTS, "&Report Achievement Problem"); + ret.emplace_back(IDM_RA_GETROMCHECKSUM, "View Game H&ash"); + } + + return ret; +} + +void RAIntegration::ActivateMenuItem(int item) +{ + RA_InvokeDialog(item); +} + +int RACallbackIsActive() +{ + return static_cast(HasActiveGame()); +} + +void RACallbackCauseUnpause() +{ + if (System::IsValid()) + g_host_interface->PauseSystem(false); +} + +void RACallbackCausePause() +{ + if (System::IsValid()) + g_host_interface->PauseSystem(true); +} + +void RACallbackRebuildMenu() +{ + // unused, we build the menu on demand +} + +void RACallbackEstimateTitle(char* buf) +{ + StringUtil::Strlcpy(buf, System::GetRunningTitle(), 256); +} + +void RACallbackResetEmulator() +{ + g_challenge_mode = RA_HardcoreModeIsActive() != 0; + if (System::IsValid()) + g_host_interface->ResetSystem(); +} + +void RACallbackLoadROM(const char* unused) +{ + // unused + UNREFERENCED_PARAMETER(unused); +} + +unsigned char RACallbackReadMemory(unsigned int address) +{ + if (!System::IsValid()) + return 0; + + u8 value = 0; + CPU::SafeReadMemoryByte(address, &value); + return value; +} + +void RACallbackWriteMemory(unsigned int address, unsigned char value) +{ + if (!System::IsValid()) + return; + + CPU::SafeWriteMemoryByte(address, value); +} + +#endif + } // namespace Cheevos diff --git a/src/core/cheevos.h b/src/core/cheevos.h index 6c1d49445..3e193b1f2 100644 --- a/src/core/cheevos.h +++ b/src/core/cheevos.h @@ -4,6 +4,8 @@ #include #include #include +#include +#include class CDImage; class StateWrapper; @@ -51,6 +53,25 @@ extern bool g_active; extern bool g_challenge_mode; extern u32 g_game_id; +// RAIntegration only exists for Windows, so no point checking it on other platforms. +#ifdef WITH_RAINTEGRATION + +extern bool g_using_raintegration; + +static ALWAYS_INLINE bool IsUsingRAIntegration() +{ + return g_using_raintegration; +} + +#else + +static ALWAYS_INLINE bool IsUsingRAIntegration() +{ + return false; +} + +#endif + ALWAYS_INLINE bool IsActive() { return g_active; @@ -123,4 +144,15 @@ TinyString GetAchievementProgressText(const Achievement& achievement); void UnlockAchievement(u32 achievement_id, bool add_notification = true); void SubmitLeaderboard(u32 leaderboard_id, int value); +#ifdef WITH_RAINTEGRATION +void SwitchToRAIntegration(); + +namespace RAIntegration { +void MainWindowChanged(void* new_handle); +void GameChanged(); +std::vector> GetMenuItems(); +void ActivateMenuItem(int item); +} // namespace RAIntegration +#endif + } // namespace Cheevos diff --git a/src/core/core.props b/src/core/core.props index 3b7962641..8c27effa8 100644 --- a/src/core/core.props +++ b/src/core/core.props @@ -5,10 +5,12 @@ WITH_CHEEVOS=1;%(PreprocessorDefinitions) - WITH_RECOMPILER=1;%(PreprocessorDefinitions) + WITH_RAINTEGRATION=1;%(PreprocessorDefinitions) + WITH_RECOMPILER=1;%(PreprocessorDefinitions) WITH_MMAP_FASTMEM=1;%(PreprocessorDefinitions) $(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xxhash\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)dep\rcheevos\include;$(SolutionDir)dep\rapidjson\include;$(SolutionDir)src;%(AdditionalIncludeDirectories) + $(SolutionDir)dep\rainterface;%(AdditionalIncludeDirectories) $(SolutionDir)dep\xbyak\xbyak;%(AdditionalIncludeDirectories) $(SolutionDir)dep\vixl\include;%(AdditionalIncludeDirectories) @@ -18,6 +20,7 @@ $(RootBuildDir)rcheevos\rcheevos.lib;$(RootBuildDir)imgui\imgui.lib;$(RootBuildDir)stb\stb.lib;$(RootBuildDir)vulkan-loader\vulkan-loader.lib;$(RootBuildDir)xxhash\xxhash.lib;$(RootBuildDir)zlib\zlib.lib;$(RootBuildDir)common\common.lib;%(AdditionalDependencies) + $(RootBuildDir)rainterface\rainterface.lib;%(AdditionalDependencies) $(RootBuildDir)vixl\vixl.lib;%(AdditionalDependencies) diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index 9740afb1a..14326e436 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -20,6 +20,11 @@ #include "scmversion/scmversion.h" #include "settingsdialog.h" #include "settingwidgetbinder.h" + +#ifdef WITH_CHEEVOS +#include "core/cheevos.h" +#endif + #include #include #include @@ -101,6 +106,11 @@ void MainWindow::initializeAndShow() switchToGameListView(); show(); + +#ifdef WITH_RAINTEGRATION + if (Cheevos::IsUsingRAIntegration()) + Cheevos::RAIntegration::MainWindowChanged((void*)winId()); +#endif } void MainWindow::reportError(const QString& message) @@ -991,6 +1001,33 @@ void MainWindow::setupAdditionalUi() connect(action, &QAction::triggered, [scale]() { QtHostInterface::GetInstance()->requestRenderWindowScale(scale); }); } + +#ifdef WITH_RAINTEGRATION + if (Cheevos::IsUsingRAIntegration()) + { + QMenu* raMenu = new QMenu(QStringLiteral("RAIntegration"), m_ui.menuDebug); + connect(raMenu, &QMenu::aboutToShow, this, [this, raMenu]() { + raMenu->clear(); + + const auto items = Cheevos::RAIntegration::GetMenuItems(); + for (const auto& [id, title] : items) + { + if (id == 0) + { + raMenu->addSeparator(); + continue; + } + + QAction* raAction = raMenu->addAction(QString::fromUtf8(title)); + connect(raAction, &QAction::triggered, this, [id]() { + QtHostInterface::GetInstance()->executeOnEmulationThread( + [id]() { Cheevos::RAIntegration::ActivateMenuItem(id); }); + }); + } + }); + m_ui.menuDebug->insertMenu(m_ui.menuCPUExecutionMode->menuAction(), raMenu); + } +#endif } void MainWindow::updateEmulationActions(bool starting, bool running, bool cheevos_challenge_mode) diff --git a/src/duckstation-qt/settingsdialog.cpp b/src/duckstation-qt/settingsdialog.cpp index 18efae9db..53a2ec0e2 100644 --- a/src/duckstation-qt/settingsdialog.cpp +++ b/src/duckstation-qt/settingsdialog.cpp @@ -18,6 +18,7 @@ #ifdef WITH_CHEEVOS #include "achievementsettingswidget.h" +#include "core/cheevos.h" #endif static constexpr char DEFAULT_SETTING_HELP_TEXT[] = ""; @@ -45,7 +46,8 @@ SettingsDialog::SettingsDialog(QtHostInterface* host_interface, QWidget* parent m_advanced_settings = new AdvancedSettingsWidget(host_interface, m_ui.settingsContainer, this); #ifdef WITH_CHEEVOS - m_achievement_settings = new AchievementSettingsWidget(host_interface, m_ui.settingsContainer, this); + if (!Cheevos::IsUsingRAIntegration()) + m_achievement_settings = new AchievementSettingsWidget(host_interface, m_ui.settingsContainer, this); #endif m_ui.settingsContainer->insertWidget(static_cast(Category::GeneralSettings), m_general_settings); @@ -62,7 +64,18 @@ SettingsDialog::SettingsDialog(QtHostInterface* host_interface, QWidget* parent m_ui.settingsContainer->insertWidget(static_cast(Category::AudioSettings), m_audio_settings); #ifdef WITH_CHEEVOS - m_ui.settingsContainer->insertWidget(static_cast(Category::AchievementSettings), m_achievement_settings); + if (Cheevos::IsUsingRAIntegration()) + { + QLabel* placeholder_label = + new QLabel(QStringLiteral("RAIntegration is being used, built-in RetroAchievements support is disabled."), + m_ui.settingsContainer); + placeholder_label->setAlignment(Qt::AlignLeft | Qt::AlignTop); + m_ui.settingsContainer->insertWidget(static_cast(Category::AchievementSettings), placeholder_label); + } + else + { + m_ui.settingsContainer->insertWidget(static_cast(Category::AchievementSettings), m_achievement_settings); + } #else QLabel* placeholder_label = new QLabel(tr("This DuckStation build was not compiled with RetroAchievements support."), m_ui.settingsContainer); diff --git a/src/frontend-common/common_host_interface.cpp b/src/frontend-common/common_host_interface.cpp index b9ef00ef6..0e863ff76 100644 --- a/src/frontend-common/common_host_interface.cpp +++ b/src/frontend-common/common_host_interface.cpp @@ -113,6 +113,11 @@ bool CommonHostInterface::Initialize() CreateImGuiContext(); #ifdef WITH_CHEEVOS +#ifdef WITH_RAINTEGRATION + if (GetBoolSettingValue("Cheevos", "UseRAIntegration", false)) + Cheevos::SwitchToRAIntegration(); +#endif + UpdateCheevosActive(); #endif @@ -3287,6 +3292,10 @@ void CommonHostInterface::SetDefaultSettings(SettingsInterface& si) si.SetBoolValue("Cheevos", "UseFirstDiscFromPlaylist", true); si.DeleteValue("Cheevos", "Username"); si.DeleteValue("Cheevos", "Token"); + +#ifdef WITH_RAINTEGRATION + si.SetBoolValue("Cheevos", "UseRAIntegration", false); +#endif #endif } @@ -4377,6 +4386,11 @@ void CommonHostInterface::UpdateCheevosActive() const bool cheevos_rich_presence = GetBoolSettingValue("Cheevos", "RichPresence", true); const bool cheevos_hardcore = GetBoolSettingValue("Cheevos", "ChallengeMode", false); +#ifdef WITH_RAINTEGRATION + if (Cheevos::IsUsingRAIntegration()) + return; +#endif + if (cheevos_enabled != Cheevos::IsActive() || cheevos_test_mode != Cheevos::IsTestModeActive() || cheevos_unofficial_test_mode != Cheevos::IsUnofficialTestModeActive() || cheevos_use_first_disc_from_playlist != Cheevos::IsUsingFirstDiscFromPlaylist() || diff --git a/src/frontend-common/fullscreen_ui.cpp b/src/frontend-common/fullscreen_ui.cpp index 0009bf312..485bced10 100644 --- a/src/frontend-common/fullscreen_ui.cpp +++ b/src/frontend-common/fullscreen_ui.cpp @@ -2328,6 +2328,17 @@ void DrawSettingsWindow() case SettingsPage::AchievementsSetings: { +#ifdef WITH_RAINTEGRATION + if (Cheevos::IsUsingRAIntegration()) + { + BeginMenuButtons(); + ActiveButton(ICON_FA_BAN " RAIntegration is being used instead of the built-in cheevos implementation.", + false, false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + EndMenuButtons(); + break; + } +#endif + #ifdef WITH_CHEEVOS BeginMenuButtons();