Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 175 additions & 12 deletions Client/multiplayer_sa/CMultiplayerSA_DeviceSelection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,22 @@
#include "StdInc.h"

#include <cstring>
#include <vector>
#include <algorithm>
#include <game/CSettings.h>

#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
Expand Down Expand Up @@ -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<RwEngineGetNumVideoModes_t>(FUNC_RwEngineGetNumVideoModes);
auto RwEngineGetVideoModeInfo = reinterpret_cast<RwEngineGetVideoModeInfo_t>(FUNC_RwEngineGetVideoModeInfo);

unsigned int numModes = RwEngineGetNumVideoModes();
if (numModes == 0)
return;

std::vector<DialogResolutionItem> 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<unsigned int*>(VAR_SavedVideoMode);
unsigned int curVidMode = *reinterpret_cast<unsigned int*>(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<int>(SendMessageA(hwndVidMode, CB_ADDSTRING, 0, reinterpret_cast<LPARAM>(buffer)));
if (itemIdx != CB_ERR)
{
SendMessageA(hwndVidMode, CB_SETITEMDATA, itemIdx, static_cast<LPARAM>(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<unsigned int*>(VAR_CurVideoMode) = static_cast<unsigned int>(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<LPARAM>(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<LPARAM>(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()
Expand Down
Loading