From 9a5ef2d0a25d1c0e7ddc0e38586168a287250b03 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Mon, 18 Apr 2022 15:09:21 +1000 Subject: [PATCH 1/2] 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(); From 296f613b4eb365790dec517bc5c6d0986d5141ff Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Mon, 18 Apr 2022 19:08:55 +1000 Subject: [PATCH 2/2] Cheevos: Rework functions into namespace --- src/core/cheevos.cpp | 265 ++++++++++++++++++++++++------------------- 1 file changed, 149 insertions(+), 116 deletions(-) diff --git a/src/core/cheevos.cpp b/src/core/cheevos.cpp index 1efd61557..8cf5c2064 100644 --- a/src/core/cheevos.cpp +++ b/src/core/cheevos.cpp @@ -43,6 +43,7 @@ enum : s32 NO_RICH_PRESENCE_PING_FREQUENCY = RICH_PRESENCE_PING_FREQUENCY * 2 }; +static void FormattedError(const char* format, ...) printflike(1, 2); static void CheevosEventHandler(const rc_runtime_event_t* runtime_event); static unsigned CheevosPeek(unsigned address, unsigned num_bytes, void* ud); static void ActivateLockedAchievements(); @@ -51,9 +52,37 @@ static void DeactivateAchievement(Achievement* achievement); static void SendPing(); static void SendPlaying(); static void UpdateRichPresence(); - -/// Uses a temporarily (second) CD image to resolve the hash. static void GameChanged(); +static std::string GetErrorFromResponseJSON(const rapidjson::Document& doc); +static void LogFailedResponseJSON(const FrontendCommon::HTTPDownloader::Request::Data& data); +static bool ParseResponseJSON(const char* request_type, s32 status_code, + const FrontendCommon::HTTPDownloader::Request::Data& data, rapidjson::Document& doc, + const char* success_field = "Success"); +static Achievement* GetAchievementByID(u32 id); +static void ClearGameInfo(bool clear_achievements = true, bool clear_leaderboards = true); +static void ClearGamePath(); +static std::string GetUserAgent(); +static void LoginCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data); +static void LoginASyncCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data); +static void SendLogin(const char* username, const char* password, FrontendCommon::HTTPDownloader* http_downloader, + FrontendCommon::HTTPDownloader::Request::Callback callback); +static void UpdateImageDownloadProgress(); +static void DownloadImage(std::string url, std::string cache_filename); +static std::string GetBadgeImageFilename(const char* badge_name, bool locked, bool cache_path); +static std::string ResolveBadgePath(const char* badge_name, bool locked); +static void DisplayAchievementSummary(); +static void GetUserUnlocksCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data); +static void GetUserUnlocks(); +static void GetPatchesCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data); +static void GetLbInfoCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data); +static void GetPatches(u32 game_id); +static std::string GetGameHash(CDImage* cdi); +static void GetGameIdCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data); +static void SendPlayingCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data); +static void UpdateRichPresence(); +static void SendPingCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data); +static void UnlockAchievementCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data); +static void SubmitLeaderboardCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data); bool g_active = false; bool g_challenge_mode = false; @@ -83,22 +112,41 @@ static std::string s_game_developer; static std::string s_game_publisher; static std::string s_game_release_date; static std::string s_game_icon; -static std::vector s_achievements; -static std::vector s_leaderboards; +static std::vector s_achievements; +static std::vector s_leaderboards; static bool s_has_rich_presence = false; static std::string s_rich_presence_string; static Common::Timer s_last_ping_time; static u32 s_last_queried_lboard; -static std::optional> s_lboard_entries; +static std::optional> s_lboard_entries; static u32 s_total_image_downloads; static u32 s_completed_image_downloads; static bool s_image_download_progress_active; -static void FormattedError(const char* format, ...) printflike(1, 2); -static void FormattedError(const char* format, ...) +} // namespace Cheevos + +template +static std::string GetOptionalString(const T& value, const char* key) +{ + if (!value.HasMember(key) || !value[key].IsString()) + return std::string(); + + return value[key].GetString(); +} + +template +static u32 GetOptionalUInt(const T& value, const char* key) +{ + if (!value.HasMember(key) || !value[key].IsUint()) + return 0; + + return value[key].GetUint(); +} + +void Cheevos::FormattedError(const char* format, ...) { std::va_list ap; va_start(ap, format); @@ -113,7 +161,7 @@ static void FormattedError(const char* format, ...) Log_ErrorPrint(str.GetCharArray()); } -static std::string GetErrorFromResponseJSON(const rapidjson::Document& doc) +std::string Cheevos::GetErrorFromResponseJSON(const rapidjson::Document& doc) { if (doc.HasMember("Error") && doc["Error"].IsString()) return doc["Error"].GetString(); @@ -121,15 +169,15 @@ static std::string GetErrorFromResponseJSON(const rapidjson::Document& doc) return ""; } -static void LogFailedResponseJSON(const FrontendCommon::HTTPDownloader::Request::Data& data) +void Cheevos::LogFailedResponseJSON(const FrontendCommon::HTTPDownloader::Request::Data& data) { const std::string str_data(reinterpret_cast(data.data()), data.size()); Log_ErrorPrintf("API call failed. Response JSON was:\n%s", str_data.c_str()); } -static bool ParseResponseJSON(const char* request_type, s32 status_code, - const FrontendCommon::HTTPDownloader::Request::Data& data, rapidjson::Document& doc, - const char* success_field = "Success") +bool Cheevos::ParseResponseJSON(const char* request_type, s32 status_code, + const FrontendCommon::HTTPDownloader::Request::Data& data, rapidjson::Document& doc, + const char* success_field) { if (status_code != HTTP_OK || data.empty()) { @@ -158,25 +206,7 @@ static bool ParseResponseJSON(const char* request_type, s32 status_code, return true; } -template -static std::string GetOptionalString(const T& value, const char* key) -{ - if (!value.HasMember(key) || !value[key].IsString()) - return std::string(); - - return value[key].GetString(); -} - -template -static u32 GetOptionalUInt(const T& value, const char* key) -{ - if (!value.HasMember(key) || !value[key].IsUint()) - return 0; - - return value[key].GetUint(); -} - -static Achievement* GetAchievementByID(u32 id) +static Cheevos::Achievement* Cheevos::GetAchievementByID(u32 id) { for (Achievement& ach : s_achievements) { @@ -187,7 +217,7 @@ static Achievement* GetAchievementByID(u32 id) return nullptr; } -static void ClearGameInfo(bool clear_achievements = true, bool clear_leaderboards = true) +void Cheevos::ClearGameInfo(bool clear_achievements, bool clear_leaderboards) { const bool had_game = (g_game_id != 0); @@ -231,19 +261,19 @@ static void ClearGameInfo(bool clear_achievements = true, bool clear_leaderboard g_host_interface->OnAchievementsRefreshed(); } -static void ClearGamePath() +void Cheevos::ClearGamePath() { std::string().swap(s_game_path); std::string().swap(s_game_hash); } -static std::string GetUserAgent() +std::string Cheevos::GetUserAgent() { return StringUtil::StdStringFromFormat("DuckStation for %s (%s) %s", SYSTEM_STR, CPU_ARCH_STR, g_scm_tag_str); } -bool Initialize(bool test_mode, bool use_first_disc_from_playlist, bool enable_rich_presence, bool challenge_mode, - bool include_unofficial) +bool Cheevos::Initialize(bool test_mode, bool use_first_disc_from_playlist, bool enable_rich_presence, + bool challenge_mode, bool include_unofficial) { s_http_downloader = FrontendCommon::HTTPDownloader::Create(GetUserAgent().c_str()); if (!s_http_downloader) @@ -275,7 +305,7 @@ bool Initialize(bool test_mode, bool use_first_disc_from_playlist, bool enable_r return true; } -void Reset() +void Cheevos::Reset() { if (!g_active) return; @@ -292,7 +322,7 @@ void Reset() rc_runtime_reset(&s_rcheevos_runtime); } -void Shutdown() +void Cheevos::Shutdown() { if (!g_active) return; @@ -314,7 +344,7 @@ void Shutdown() s_http_downloader.reset(); } -void Update() +void Cheevos::Update() { s_http_downloader->PollRequests(); @@ -343,7 +373,7 @@ void Update() } } -bool DoState(StateWrapper& sw) +bool Cheevos::DoState(StateWrapper& sw) { // if we're inactive, we still need to skip the data (if any) if (!g_active) @@ -453,42 +483,42 @@ bool DoState(StateWrapper& sw) } } -bool IsLoggedIn() +bool Cheevos::IsLoggedIn() { return s_logged_in; } -bool IsTestModeActive() +bool Cheevos::IsTestModeActive() { return s_test_mode; } -bool IsUnofficialTestModeActive() +bool Cheevos::IsUnofficialTestModeActive() { return s_unofficial_test_mode; } -bool IsUsingFirstDiscFromPlaylist() +bool Cheevos::IsUsingFirstDiscFromPlaylist() { return s_use_first_disc_from_playlist; } -bool IsRichPresenceEnabled() +bool Cheevos::IsRichPresenceEnabled() { return s_rich_presence_enabled; } -const std::string& GetUsername() +const std::string& Cheevos::GetUsername() { return s_username; } -const std::string& GetRichPresenceString() +const std::string& Cheevos::GetRichPresenceString() { return s_rich_presence_string; } -static void LoginCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data) +void Cheevos::LoginCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data) { rapidjson::Document doc; if (!ParseResponseJSON("Login", status_code, data, doc)) @@ -525,7 +555,7 @@ static void LoginCallback(s32 status_code, const FrontendCommon::HTTPDownloader: } } -static void LoginASyncCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data) +void Cheevos::LoginASyncCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data) { if (ImGuiFullscreen::IsInitialized()) ImGuiFullscreen::CloseBackgroundProgressDialog("cheevos_async_login"); @@ -533,8 +563,8 @@ static void LoginASyncCallback(s32 status_code, const FrontendCommon::HTTPDownlo LoginCallback(status_code, data); } -static void SendLogin(const char* username, const char* password, FrontendCommon::HTTPDownloader* http_downloader, - FrontendCommon::HTTPDownloader::Request::Callback callback) +void Cheevos::SendLogin(const char* username, const char* password, FrontendCommon::HTTPDownloader* http_downloader, + FrontendCommon::HTTPDownloader::Request::Callback callback) { char url[768] = {}; int res = rc_url_login_with_password(url, sizeof(url), username, password); @@ -543,7 +573,7 @@ static void SendLogin(const char* username, const char* password, FrontendCommon http_downloader->CreateRequest(url, std::move(callback)); } -bool LoginAsync(const char* username, const char* password) +bool Cheevos::LoginAsync(const char* username, const char* password) { s_http_downloader->WaitForAllRequests(); @@ -561,7 +591,7 @@ bool LoginAsync(const char* username, const char* password) return true; } -bool Login(const char* username, const char* password) +bool Cheevos::Login(const char* username, const char* password) { if (g_active) s_http_downloader->WaitForAllRequests(); @@ -589,7 +619,7 @@ bool Login(const char* username, const char* password) return !g_host_interface->GetStringSettingValue("Cheevos", "Token").empty(); } -void Logout() +void Cheevos::Logout() { if (g_active) { @@ -614,7 +644,7 @@ void Logout() } } -static void UpdateImageDownloadProgress() +void Cheevos::UpdateImageDownloadProgress() { static const char* str_id = "cheevo_image_download"; @@ -651,7 +681,7 @@ static void UpdateImageDownloadProgress() } } -static void DownloadImage(std::string url, std::string cache_filename) +void Cheevos::DownloadImage(std::string url, std::string cache_filename) { auto callback = [cache_filename](s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data) { s_completed_image_downloads++; @@ -676,7 +706,7 @@ static void DownloadImage(std::string url, std::string cache_filename) s_http_downloader->CreateRequest(std::move(url), std::move(callback)); } -static std::string GetBadgeImageFilename(const char* badge_name, bool locked, bool cache_path) +std::string Cheevos::GetBadgeImageFilename(const char* badge_name, bool locked, bool cache_path) { if (!cache_path) { @@ -693,7 +723,7 @@ static std::string GetBadgeImageFilename(const char* badge_name, bool locked, bo } } -static std::string ResolveBadgePath(const char* badge_name, bool locked) +std::string Cheevos::ResolveBadgePath(const char* badge_name, bool locked) { char url[256]; @@ -709,7 +739,7 @@ static std::string ResolveBadgePath(const char* badge_name, bool locked) return cache_path; } -static void DisplayAchievementSummary() +void Cheevos::DisplayAchievementSummary() { std::string title = s_game_title; if (g_challenge_mode) @@ -743,7 +773,7 @@ static void DisplayAchievementSummary() ImGuiFullscreen::AddNotification(10.0f, std::move(title), std::move(summary), s_game_icon); } -static void GetUserUnlocksCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data) +void Cheevos::GetUserUnlocksCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data) { rapidjson::Document doc; if (!ParseResponseJSON("Get User Unlocks", status_code, data, doc)) @@ -790,7 +820,7 @@ static void GetUserUnlocksCallback(s32 status_code, const FrontendCommon::HTTPDo g_host_interface->OnAchievementsRefreshed(); } -static void GetUserUnlocks() +void Cheevos::GetUserUnlocks() { char url[512]; int res = rc_url_get_unlock_list(url, sizeof(url), s_username.c_str(), s_login_token.c_str(), g_game_id, @@ -800,7 +830,7 @@ static void GetUserUnlocks() s_http_downloader->CreateRequest(url, GetUserUnlocksCallback); } -static void GetPatchesCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data) +void Cheevos::GetPatchesCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data) { ClearGameInfo(); @@ -977,7 +1007,7 @@ static void GetPatchesCallback(s32 status_code, const FrontendCommon::HTTPDownlo } } -static void GetLbInfoCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data) +void Cheevos::GetLbInfoCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data) { rapidjson::Document doc; if (!ParseResponseJSON("Get Leaderboard Info", status_code, data, doc)) @@ -1040,7 +1070,7 @@ static void GetLbInfoCallback(s32 status_code, const FrontendCommon::HTTPDownloa } } -static void GetPatches(u32 game_id) +void Cheevos::GetPatches(u32 game_id) { char url[512]; int res = rc_url_get_patch(url, sizeof(url), s_username.c_str(), s_login_token.c_str(), game_id); @@ -1049,7 +1079,7 @@ static void GetPatches(u32 game_id) s_http_downloader->CreateRequest(url, GetPatchesCallback); } -static std::string GetGameHash(CDImage* cdi) +std::string Cheevos::GetGameHash(CDImage* cdi) { std::string executable_name; std::vector executable_data; @@ -1087,7 +1117,7 @@ static std::string GetGameHash(CDImage* cdi) return hash_str; } -static void GetGameIdCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data) +void Cheevos::GetGameIdCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data) { rapidjson::Document doc; if (!ParseResponseJSON("Get Game ID", status_code, data, doc)) @@ -1101,7 +1131,7 @@ static void GetGameIdCallback(s32 status_code, const FrontendCommon::HTTPDownloa GetPatches(game_id); } -void GameChanged() +void Cheevos::GameChanged() { Assert(System::IsValid()); @@ -1120,7 +1150,7 @@ void GameChanged() GameChanged(path, cdi.get()); } -void GameChanged(const std::string& path, CDImage* image) +void Cheevos::GameChanged(const std::string& path, CDImage* image) { if (s_game_path == path) return; @@ -1185,7 +1215,7 @@ void GameChanged(const std::string& path, CDImage* image) s_http_downloader->CreateRequest(url, GetGameIdCallback); } -static void SendPlayingCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data) +void Cheevos::SendPlayingCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data) { rapidjson::Document doc; if (!ParseResponseJSON("Post Activity", status_code, data, doc)) @@ -1194,7 +1224,7 @@ static void SendPlayingCallback(s32 status_code, const FrontendCommon::HTTPDownl Log_InfoPrintf("Playing game updated to %u (%s)", g_game_id, s_game_title.c_str()); } -void SendPlaying() +void Cheevos::SendPlaying() { if (!HasActiveGame()) return; @@ -1206,7 +1236,7 @@ void SendPlaying() s_http_downloader->CreateRequest(url, SendPlayingCallback); } -static void UpdateRichPresence() +void Cheevos::UpdateRichPresence() { if (!s_has_rich_presence) return; @@ -1230,14 +1260,14 @@ static void UpdateRichPresence() g_host_interface->OnAchievementsRefreshed(); } -static void SendPingCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data) +void Cheevos::SendPingCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data) { rapidjson::Document doc; if (!ParseResponseJSON("Ping", status_code, data, doc)) return; } -void SendPing() +void Cheevos::SendPing() { if (!HasActiveGame()) return; @@ -1252,32 +1282,32 @@ void SendPing() s_last_ping_time.Reset(); } -const std::string& GetGameTitle() +const std::string& Cheevos::GetGameTitle() { return s_game_title; } -const std::string& GetGameDeveloper() +const std::string& Cheevos::GetGameDeveloper() { return s_game_developer; } -const std::string& GetGamePublisher() +const std::string& Cheevos::GetGamePublisher() { return s_game_publisher; } -const std::string& GetGameReleaseDate() +const std::string& Cheevos::GetGameReleaseDate() { return s_game_release_date; } -const std::string& GetGameIcon() +const std::string& Cheevos::GetGameIcon() { return s_game_icon; } -bool EnumerateAchievements(std::function callback) +bool Cheevos::EnumerateAchievements(std::function callback) { for (const Achievement& cheevo : s_achievements) { @@ -1288,7 +1318,7 @@ bool EnumerateAchievements(std::function callback) return true; } -u32 GetUnlockedAchiementCount() +u32 Cheevos::GetUnlockedAchiementCount() { u32 count = 0; for (const Achievement& cheevo : s_achievements) @@ -1300,12 +1330,12 @@ u32 GetUnlockedAchiementCount() return count; } -u32 GetAchievementCount() +u32 Cheevos::GetAchievementCount() { return static_cast(s_achievements.size()); } -u32 GetMaximumPointsForGame() +u32 Cheevos::GetMaximumPointsForGame() { u32 points = 0; for (const Achievement& cheevo : s_achievements) @@ -1314,7 +1344,7 @@ u32 GetMaximumPointsForGame() return points; } -u32 GetCurrentPointsForGame() +u32 Cheevos::GetCurrentPointsForGame() { u32 points = 0; for (const Achievement& cheevo : s_achievements) @@ -1326,7 +1356,7 @@ u32 GetCurrentPointsForGame() return points; } -bool EnumerateLeaderboards(std::function callback) +bool Cheevos::EnumerateLeaderboards(std::function callback) { for (const Leaderboard& lboard : s_leaderboards) { @@ -1337,7 +1367,8 @@ bool EnumerateLeaderboards(std::function callback) return true; } -std::optional TryEnumerateLeaderboardEntries(u32 id, std::function callback) +std::optional Cheevos::TryEnumerateLeaderboardEntries(u32 id, + std::function callback) { if (id == s_last_queried_lboard) { @@ -1367,7 +1398,7 @@ std::optional TryEnumerateLeaderboardEntries(u32 id, std::function(s_leaderboards.size()); } -bool IsLeaderboardTimeType(const Leaderboard& leaderboard) +bool Cheevos::IsLeaderboardTimeType(const Leaderboard& leaderboard) { return leaderboard.format != RC_FORMAT_SCORE && leaderboard.format != RC_FORMAT_VALUE; } -void ActivateLockedAchievements() +void Cheevos::ActivateLockedAchievements() { for (Achievement& cheevo : s_achievements) { @@ -1397,7 +1428,7 @@ void ActivateLockedAchievements() } } -bool ActivateAchievement(Achievement* achievement) +bool Cheevos::ActivateAchievement(Achievement* achievement) { if (achievement->active) return true; @@ -1416,7 +1447,7 @@ bool ActivateAchievement(Achievement* achievement) return true; } -void DeactivateAchievement(Achievement* achievement) +void Cheevos::DeactivateAchievement(Achievement* achievement) { if (!achievement->active) return; @@ -1427,7 +1458,7 @@ void DeactivateAchievement(Achievement* achievement) Log_DevPrintf("Deactivated achievement %s (%u)", achievement->title.c_str(), achievement->id); } -static void UnlockAchievementCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data) +void Cheevos::UnlockAchievementCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data) { rapidjson::Document doc; if (!ParseResponseJSON("Award Cheevo", status_code, data, doc)) @@ -1436,13 +1467,13 @@ static void UnlockAchievementCallback(s32 status_code, const FrontendCommon::HTT // we don't really need to do anything here } -static void SubmitLeaderboardCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data) +void Cheevos::SubmitLeaderboardCallback(s32 status_code, const FrontendCommon::HTTPDownloader::Request::Data& data) { // Force the next leaderboard query to repopulate everything, just in case the user wants to see their new score s_last_queried_lboard = 0; } -void UnlockAchievement(u32 achievement_id, bool add_notification /* = true*/) +void Cheevos::UnlockAchievement(u32 achievement_id, bool add_notification /* = true*/) { Achievement* achievement = GetAchievementByID(achievement_id); if (!achievement) @@ -1497,7 +1528,7 @@ void UnlockAchievement(u32 achievement_id, bool add_notification /* = true*/) s_http_downloader->CreateRequest(url, UnlockAchievementCallback); } -void SubmitLeaderboard(u32 leaderboard_id, int value) +void Cheevos::SubmitLeaderboard(u32 leaderboard_id, int value) { if (s_test_mode) { @@ -1517,14 +1548,14 @@ void SubmitLeaderboard(u32 leaderboard_id, int value) s_http_downloader->CreateRequest(url, SubmitLeaderboardCallback); } -std::pair GetAchievementProgress(const Achievement& achievement) +std::pair Cheevos::GetAchievementProgress(const Achievement& achievement) { std::pair result; rc_runtime_get_achievement_measured(&s_rcheevos_runtime, achievement.id, &result.first, &result.second); return result; } -TinyString GetAchievementProgressText(const Achievement& achievement) +TinyString Cheevos::GetAchievementProgressText(const Achievement& achievement) { TinyString result; rc_runtime_format_achievement_measured(&s_rcheevos_runtime, achievement.id, result.GetWriteableCharArray(), @@ -1533,7 +1564,7 @@ TinyString GetAchievementProgressText(const Achievement& achievement) return result; } -void CheevosEventHandler(const rc_runtime_event_t* runtime_event) +void Cheevos::CheevosEventHandler(const rc_runtime_event_t* runtime_event) { static const char* events[] = {"RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED", "RC_RUNTIME_EVENT_ACHIEVEMENT_PAUSED", "RC_RUNTIME_EVENT_ACHIEVEMENT_RESET", "RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED", @@ -1551,7 +1582,7 @@ void CheevosEventHandler(const rc_runtime_event_t* runtime_event) SubmitLeaderboard(runtime_event->id, runtime_event->value); } -unsigned CheevosPeek(unsigned address, unsigned num_bytes, void* ud) +unsigned Cheevos::CheevosPeek(unsigned address, unsigned num_bytes, void* ud) { switch (num_bytes) { @@ -1585,6 +1616,9 @@ unsigned CheevosPeek(unsigned address, unsigned num_bytes, void* ud) #include "RA_Consoles.h" +namespace Cheevos::RAIntegration { +static void InitializeRAIntegration(void* main_window_handle); + static int RACallbackIsActive(); static void RACallbackCauseUnpause(); static void RACallbackCausePause(); @@ -1594,8 +1628,9 @@ 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); +} // namespace Cheevos::RAIntegration -void SwitchToRAIntegration() +void Cheevos::SwitchToRAIntegration() { g_using_raintegration = true; g_raintegration_initialized = false; @@ -1603,7 +1638,7 @@ void SwitchToRAIntegration() s_logged_in = true; } -static void InitializeRAIntegration(void* main_window_handle) +void Cheevos::RAIntegration::InitializeRAIntegration(void* main_window_handle) { RA_InitClient((HWND)main_window_handle, "DuckStation", g_scm_tag_str); RA_SetUserAgentDetail(Cheevos::GetUserAgent().c_str()); @@ -1626,7 +1661,7 @@ static void InitializeRAIntegration(void* main_window_handle) std::atexit(RA_Shutdown); } -void RAIntegration::MainWindowChanged(void* new_handle) +void Cheevos::RAIntegration::MainWindowChanged(void* new_handle) { if (g_raintegration_initialized) { @@ -1637,13 +1672,13 @@ void RAIntegration::MainWindowChanged(void* new_handle) InitializeRAIntegration(new_handle); } -void RAIntegration::GameChanged() +void Cheevos::RAIntegration::GameChanged() { g_game_id = RA_IdentifyHash(s_game_hash.c_str()); RA_ActivateGame(g_game_id); } -std::vector> RAIntegration::GetMenuItems() +std::vector> Cheevos::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; @@ -1696,52 +1731,52 @@ std::vector> RAIntegration::GetMenuItems() return ret; } -void RAIntegration::ActivateMenuItem(int item) +void Cheevos::RAIntegration::ActivateMenuItem(int item) { RA_InvokeDialog(item); } -int RACallbackIsActive() +int Cheevos::RAIntegration::RACallbackIsActive() { return static_cast(HasActiveGame()); } -void RACallbackCauseUnpause() +void Cheevos::RAIntegration::RACallbackCauseUnpause() { if (System::IsValid()) g_host_interface->PauseSystem(false); } -void RACallbackCausePause() +void Cheevos::RAIntegration::RACallbackCausePause() { if (System::IsValid()) g_host_interface->PauseSystem(true); } -void RACallbackRebuildMenu() +void Cheevos::RAIntegration::RACallbackRebuildMenu() { // unused, we build the menu on demand } -void RACallbackEstimateTitle(char* buf) +void Cheevos::RAIntegration::RACallbackEstimateTitle(char* buf) { StringUtil::Strlcpy(buf, System::GetRunningTitle(), 256); } -void RACallbackResetEmulator() +void Cheevos::RAIntegration::RACallbackResetEmulator() { g_challenge_mode = RA_HardcoreModeIsActive() != 0; if (System::IsValid()) g_host_interface->ResetSystem(); } -void RACallbackLoadROM(const char* unused) +void Cheevos::RAIntegration::RACallbackLoadROM(const char* unused) { // unused UNREFERENCED_PARAMETER(unused); } -unsigned char RACallbackReadMemory(unsigned int address) +unsigned char Cheevos::RAIntegration::RACallbackReadMemory(unsigned int address) { if (!System::IsValid()) return 0; @@ -1751,7 +1786,7 @@ unsigned char RACallbackReadMemory(unsigned int address) return value; } -void RACallbackWriteMemory(unsigned int address, unsigned char value) +void Cheevos::RAIntegration::RACallbackWriteMemory(unsigned int address, unsigned char value) { if (!System::IsValid()) return; @@ -1760,5 +1795,3 @@ void RACallbackWriteMemory(unsigned int address, unsigned char value) } #endif - -} // namespace Cheevos