diff --git a/Client/multiplayer_sa/CMultiplayerSA_DeviceSelection.cpp b/Client/multiplayer_sa/CMultiplayerSA_DeviceSelection.cpp index 3d82fb9a354..fa4e7ea4e31 100644 --- a/Client/multiplayer_sa/CMultiplayerSA_DeviceSelection.cpp +++ b/Client/multiplayer_sa/CMultiplayerSA_DeviceSelection.cpp @@ -11,12 +11,22 @@ #include "StdInc.h" #include +#include +#include +#include + #define FUNC_rwDeviceSystemRequest 0x7F2AB0 #define FUNC_DialogFunc 0x745E50 #define FUNC_RwEngineGetSubSystemInfo 0x7F2C30 +#define FUNC_RwEngineGetNumVideoModes 0x7F2CC0 +#define FUNC_RwEngineGetVideoModeInfo 0x7F2CF0 #define CLASS_RwGlobals 0xC97B24 #define CLASS_IDirect3D9 0xC97C20 #define NUM_DialogFuncStackPushAddress 0x746239 +#define VAR_SavedVideoMode 0xBA6820 +#define VAR_CurVideoMode 0x8D6220 +#define IDC_DEVICE 1000 +#define IDC_VIDMODE 1001 // This is copied from SilentPatch: // https://github.com/CookiePLMonster/SilentPatch/blob/dev/SilentPatch/FriendlyMonitorNames.cpp @@ -147,24 +157,177 @@ static RwSubSystemInfo* RwEngineGetSubSystemInfo_Hooked(RwSubSystemInfo* subSyst return subSystemInfo; } +struct DialogResolutionItem +{ + int width; + int height; + int depth; + bool isWidescreen; + unsigned int vidModeIndex; + int refRate; +}; + +// +// Populate and sort resolution dropdown in the native GTA:SA device selection dialog. +// Direct3D 9 enumerates multiple entries for different refresh rates without showing Hz in the dialog text. +// We deduplicate identical resolutions (keeping the highest refresh rate) and sort them in descending order. +// +static void PopulateAndSortResolutionComboBox(HWND window) +{ + HWND hwndVidMode = GetDlgItem(window, IDC_VIDMODE); + if (!hwndVidMode) + return; + + using RwEngineGetNumVideoModes_t = unsigned int(__cdecl*)(); + using RwEngineGetVideoModeInfo_t = VideoMode*(__cdecl*)(VideoMode*, unsigned int); + + auto RwEngineGetNumVideoModes = reinterpret_cast(FUNC_RwEngineGetNumVideoModes); + auto RwEngineGetVideoModeInfo = reinterpret_cast(FUNC_RwEngineGetVideoModeInfo); + + unsigned int numModes = RwEngineGetNumVideoModes(); + if (numModes == 0) + return; + + std::vector resolutions; + resolutions.reserve(numModes); + + for (unsigned int modeIndex = 0; modeIndex < numModes; ++modeIndex) + { + VideoMode modeInfo{}; + if (!RwEngineGetVideoModeInfo(&modeInfo, modeIndex)) + continue; + + // Skip non-exclusive and unusable tiny resolutions + if (!(modeInfo.flags & rwVIDEOMODEEXCLUSIVE) || modeInfo.width < 640 || modeInfo.height < 480) + continue; + + bool isWidescreen = (modeInfo.flags & rwVIDEOMODE_XBOX_WIDESCREEN) != 0; + + // Deduplicate identical resolutions; if a duplicate has a higher refresh rate, use its mode index + bool isDuplicate = false; + for (auto& existing : resolutions) + { + if (existing.width == modeInfo.width && existing.height == modeInfo.height && existing.depth == modeInfo.depth && + existing.isWidescreen == isWidescreen) + { + if (modeInfo.refRate > existing.refRate) + { + existing.vidModeIndex = modeIndex; + existing.refRate = modeInfo.refRate; + } + isDuplicate = true; + break; + } + } + + if (!isDuplicate) + { + resolutions.push_back({modeInfo.width, modeInfo.height, modeInfo.depth, isWidescreen, modeIndex, modeInfo.refRate}); + } + } + + if (resolutions.empty()) + return; + + // Sort resolutions descending by width, height, depth, and aspect ratio + std::sort(resolutions.begin(), resolutions.end(), + [](const DialogResolutionItem& a, const DialogResolutionItem& b) + { + if (a.width != b.width) + return a.width > b.width; + if (a.height != b.height) + return a.height > b.height; + if (a.depth != b.depth) + return a.depth > b.depth; + return a.isWidescreen > b.isWidescreen; + }); + + // Determine target video mode to preserve current/saved selection + unsigned int savedVidMode = *reinterpret_cast(VAR_SavedVideoMode); + unsigned int curVidMode = *reinterpret_cast(VAR_CurVideoMode); + unsigned int targetVidMode = (savedVidMode > 0 && savedVidMode < numModes) ? savedVidMode : curVidMode; + + VideoMode targetModeInfo{}; + bool hasTargetInfo = (targetVidMode < numModes) && (RwEngineGetVideoModeInfo(&targetModeInfo, targetVidMode) != nullptr); + + SendMessageA(hwndVidMode, CB_RESETCONTENT, 0, 0); + + int selectedItemIdx = 0; + for (size_t i = 0; i < resolutions.size(); ++i) + { + const auto& res = resolutions[i]; + char buffer[128]; + if (res.isWidescreen) + sprintf_s(buffer, "%d x %d x %d Widescreen", res.width, res.height, res.depth); + else + sprintf_s(buffer, "%d x %d x %d", res.width, res.height, res.depth); + + int itemIdx = static_cast(SendMessageA(hwndVidMode, CB_ADDSTRING, 0, reinterpret_cast(buffer))); + if (itemIdx != CB_ERR) + { + SendMessageA(hwndVidMode, CB_SETITEMDATA, itemIdx, static_cast(res.vidModeIndex)); + + if (hasTargetInfo && res.width == targetModeInfo.width && res.height == targetModeInfo.height && res.depth == targetModeInfo.depth && + res.isWidescreen == ((targetModeInfo.flags & rwVIDEOMODE_XBOX_WIDESCREEN) != 0)) + { + selectedItemIdx = itemIdx; + } + } + } + + SendMessageA(hwndVidMode, CB_SETCURSEL, selectedItemIdx, 0); + + // Sync GTA's global selected video mode with the current selection + LRESULT curSel = SendMessageA(hwndVidMode, CB_GETCURSEL, 0, 0); + if (curSel != CB_ERR) + { + LRESULT selectedMode = SendMessageA(hwndVidMode, CB_GETITEMDATA, curSel, 0); + if (selectedMode != CB_ERR) + { + *reinterpret_cast(VAR_CurVideoMode) = static_cast(selectedMode); + } + } +} + INT_PTR CALLBACK CustomDlgProc(HWND window, UINT msg, WPARAM wParam, LPARAM lParam) { auto* orgDialogFunc = (DLGPROC)FUNC_DialogFunc; - if (msg != WM_INITDIALOG) - return orgDialogFunc(window, msg, wParam, lParam); - orgDialogFunc(window, msg, wParam, lParam); + if (msg == WM_INITDIALOG) + { + orgDialogFunc(window, msg, wParam, lParam); + + // Set Icon + HMODULE hGameModule = GetModuleHandle(nullptr); + SendMessage(window, WM_SETICON, ICON_SMALL, reinterpret_cast(LoadIcon(hGameModule, MAKEINTRESOURCE(100)))); + + // Make the dialog visible in the task bar + // https://stackoverflow.com/a/1462811 + SetWindowLongPtr(window, GWL_EXSTYLE, WS_EX_APPWINDOW); + ShowWindow(window, SW_HIDE); + ShowWindow(window, SW_SHOW); + + // Sort resolutions and deduplicate + PopulateAndSortResolutionComboBox(window); + return FALSE; + } + else if (msg == WM_COMMAND) + { + WORD controlId = LOWORD(wParam); + WORD notificationCode = HIWORD(wParam); + + INT_PTR result = orgDialogFunc(window, msg, wParam, lParam); + + // When the adapter selection is changed (IDC_DEVICE = 1000, CBN_SELCHANGE = 1) + if (controlId == IDC_DEVICE && notificationCode == CBN_SELCHANGE) + { + PopulateAndSortResolutionComboBox(window); + } - // Set Icon - HMODULE hGameModule = GetModuleHandle(nullptr); - SendMessage(window, WM_SETICON, ICON_SMALL, reinterpret_cast(LoadIcon(hGameModule, MAKEINTRESOURCE(100)))); + return result; + } - // Make the dialog visible in the task bar - // https://stackoverflow.com/a/1462811 - SetWindowLongPtr(window, GWL_EXSTYLE, WS_EX_APPWINDOW); - ShowWindow(window, SW_HIDE); - ShowWindow(window, SW_SHOW); - return FALSE; + return orgDialogFunc(window, msg, wParam, lParam); } void CMultiplayerSA::InitHooks_DeviceSelection()