From 15f9f080525362ead064f9f5b51fd04fa076cc6a Mon Sep 17 00:00:00 2001 From: Silent Date: Fri, 16 Aug 2024 21:42:53 +0200 Subject: [PATCH] SA: Remake the monitor/resolution selection dialog * Uses Common Controls 6 (modern styling) * Lists friendly monitor names * Acts correctly on pressing Escape * Doesn't bug out when an inexistent adapter is selected * Remembers the selected adapter * Gives an option to skip the dialog and remember the choice * Shows in the taskbar correctly * Explicitly asks the system to scale for DPI, overriding compat options --- SilentPatch/FriendlyMonitorNames.cpp | 77 ++++++ SilentPatch/FriendlyMonitorNames.h | 9 + SilentPatchSA/SilentPatchSA.cpp | 275 ++++++++++++++++++++ SilentPatchSA/SilentPatchSA.rc | Bin 292 -> 6184 bytes SilentPatchSA/SilentPatchSA.vcxproj | 13 +- SilentPatchSA/SilentPatchSA.vcxproj.filters | 31 ++- SilentPatchSA/resource.h | 13 + 7 files changed, 400 insertions(+), 18 deletions(-) create mode 100644 SilentPatch/FriendlyMonitorNames.cpp create mode 100644 SilentPatch/FriendlyMonitorNames.h diff --git a/SilentPatch/FriendlyMonitorNames.cpp b/SilentPatch/FriendlyMonitorNames.cpp new file mode 100644 index 0000000..a219e6c --- /dev/null +++ b/SilentPatch/FriendlyMonitorNames.cpp @@ -0,0 +1,77 @@ +#include "FriendlyMonitorNames.h" + +// This API is Win7 only, so make sure we use dynamic imports +#define WIN32_LEAN_AND_MEAN + +#define NOMINMAX +#define WINVER 0x0602 +#define _WIN32_WINNT 0x0602 + +#include + +#include + +std::map> FriendlyMonitorNames::GetNamesForDevicePaths() +{ + std::map> monitorNames; + + HMODULE user32Lib = LoadLibrary(TEXT("user32")); + if (user32Lib != nullptr) + { + auto getDisplayConfigBufferSizes = (decltype(GetDisplayConfigBufferSizes)*)GetProcAddress(user32Lib, "GetDisplayConfigBufferSizes"); + auto queryDisplayConfig = (decltype(QueryDisplayConfig)*)GetProcAddress(user32Lib, "QueryDisplayConfig"); + auto displayConfigGetDeviceInfo = (decltype(DisplayConfigGetDeviceInfo)*)GetProcAddress(user32Lib, "DisplayConfigGetDeviceInfo"); + if (getDisplayConfigBufferSizes != nullptr && queryDisplayConfig != nullptr && displayConfigGetDeviceInfo != nullptr) + { + UINT32 pathCount, modeCount; + std::unique_ptr paths; + std::unique_ptr modes; + + LONG result = ERROR_SUCCESS; + do + { + result = getDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &pathCount, &modeCount); + if (result != ERROR_SUCCESS) + { + break; + } + paths = std::make_unique(pathCount); + modes = std::make_unique(modeCount); + result = queryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &pathCount, paths.get(), &modeCount, modes.get(), nullptr); + } + while (result == ERROR_INSUFFICIENT_BUFFER); + + if (result == ERROR_SUCCESS) + { + for (size_t i = 0; i < pathCount; i++) + { + DISPLAYCONFIG_TARGET_DEVICE_NAME targetName = {}; + targetName.header.adapterId = paths[i].targetInfo.adapterId; + targetName.header.id = paths[i].targetInfo.id; + targetName.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME; + targetName.header.size = sizeof(targetName); + const LONG targetNameResult = DisplayConfigGetDeviceInfo(&targetName.header); + + DISPLAYCONFIG_SOURCE_DEVICE_NAME sourceName = {}; + sourceName.header.adapterId = paths[i].sourceInfo.adapterId; + sourceName.header.id = paths[i].sourceInfo.id; + sourceName.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME; + sourceName.header.size = sizeof(sourceName); + const LONG sourceNameResult = DisplayConfigGetDeviceInfo(&sourceName.header); + if (targetNameResult == ERROR_SUCCESS && sourceNameResult == ERROR_SUCCESS && targetName.monitorFriendlyDeviceName[0] != '\0') + { + char gdiDeviceName[std::size(sourceName.viewGdiDeviceName)]; + char monitorFriendlyDeviceName[std::size(targetName.monitorFriendlyDeviceName)]; + WideCharToMultiByte(CP_ACP, 0, sourceName.viewGdiDeviceName, -1, gdiDeviceName, static_cast(std::size(gdiDeviceName)), nullptr, nullptr); + WideCharToMultiByte(CP_ACP, 0, targetName.monitorFriendlyDeviceName, -1, monitorFriendlyDeviceName, static_cast(std::size(monitorFriendlyDeviceName)), nullptr, nullptr); + + monitorNames.try_emplace(gdiDeviceName, monitorFriendlyDeviceName); + } + } + } + } + FreeLibrary(user32Lib); + } + + return monitorNames; +} diff --git a/SilentPatch/FriendlyMonitorNames.h b/SilentPatch/FriendlyMonitorNames.h new file mode 100644 index 0000000..d535dc3 --- /dev/null +++ b/SilentPatch/FriendlyMonitorNames.h @@ -0,0 +1,9 @@ +#pragma once + +#include +#include + +namespace FriendlyMonitorNames +{ + std::map> GetNamesForDevicePaths(); +} diff --git a/SilentPatchSA/SilentPatchSA.cpp b/SilentPatchSA/SilentPatchSA.cpp index d7e7528..744485c 100644 --- a/SilentPatchSA/SilentPatchSA.cpp +++ b/SilentPatchSA/SilentPatchSA.cpp @@ -30,11 +30,16 @@ #include "Utils/HookEach.hpp" #include "Desktop.h" +#include "FriendlyMonitorNames.h" #include "SVF.h" #include "debugmenu_public.h" #include "resource.h" +#pragma comment(linker,"\"/manifestdependency:type='win32' \ + name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \ + processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"") + EXTERN_C IMAGE_DOS_HEADER __ImageBase; // ============= Mod compatibility stuff ============= @@ -130,6 +135,8 @@ static void* varRwMatrixRotate = AddressByVersion(0x7F1FD0, 0x7F28D0, 0x8 WRAPPER RwMatrix* RwMatrixRotate(RwMatrix* matrix, const RwV3d* axis, RwReal angle, RwOpCombineType combineOp) { WRAPARG(matrix); WRAPARG(axis); WRAPARG(angle); WRAPARG(combineOp); VARJMP(varRwMatrixRotate); } static void* varRwD3D9SetRenderState = AddressByVersion(0x7FC2D0, 0x7FCBD0, 0x836290); WRAPPER void RwD3D9SetRenderState(RwUInt32 state, RwUInt32 value) { WRAPARG(state); WRAPARG(value); VARJMP(varRwD3D9SetRenderState); } +static void* varRwEngineSetSubSystem = AddressByVersion(0x7F2C90, { "50 6A 00 6A 00 83 C1 10 6A 10 51", -0xA }); +WRAPPER RwBool RwEngineSetSubSystem(RwInt32 subSystemIndex) { WRAPARG(subSystemIndex); VARJMP(varRwEngineSetSubSystem); } RwCamera* RwCameraBeginUpdate(RwCamera* camera) { @@ -2533,6 +2540,225 @@ namespace Rand16bit } +// ============= Improved resolution selection dialog ============= +namespace NewResolutionSelectionDialog +{ + static IDirect3D9** ppRWD3D9; + static void* FrontEndMenuManager; + static char* (*orgGetDocumentsPath)(); + + static constexpr const char* SettingsFileName = "device_remembered.set"; + + static bool ShouldSkipDeviceSelection() + { + char cTmpPath[MAX_PATH]; + PathCombineA(cTmpPath, GetMyDocumentsPathSA(), SettingsFileName); + + bool bSkip = false; + + FILE* hFile = nullptr; + if (fopen_s(&hFile, cTmpPath, "r") == 0) + { + unsigned int val = 0; + bSkip = fscanf_s(hFile, "%u", &val) == 1 && val != 0; + fclose(hFile); + } + return bSkip; + } + + static void RememberDeviceSelection(bool bDoNotShowAgain) + { + char cTmpPath[MAX_PATH]; + PathCombineA(cTmpPath, GetMyDocumentsPathSA(), SettingsFileName); + + FILE* hFile = nullptr; + if (fopen_s(&hFile, cTmpPath, "w") == 0) + { + fprintf_s(hFile, "%u", bDoNotShowAgain ? 1 : 0); + fclose(hFile); + } + } + + static RwSubSystemInfo *RwEngineGetSubSystemInfo_GetFriendlyNames(RwSubSystemInfo *subSystemInfo, RwInt32 subSystemIndex) + { + static const auto friendlyNames = FriendlyMonitorNames::GetNamesForDevicePaths(); + + D3DADAPTER_IDENTIFIER9 identifier; + if (FAILED((*ppRWD3D9)->GetAdapterIdentifier(subSystemIndex, 0, &identifier))) + { + return nullptr; + } + + // If we can't find the friendly name, either because it doesn't exist or we're on an ancient Windows, fall back to the device name + auto it = friendlyNames.find(identifier.DeviceName); + if (it != friendlyNames.end()) + { + strncpy_s(subSystemInfo->name, it->second.c_str(), _TRUNCATE); + } + else + { + strncpy_s(subSystemInfo->name, identifier.Description, _TRUNCATE); + } + + return subSystemInfo; + } + + static size_t MenuManagerAdapterOffset = 0xDC; + static RwInt32 RwEngineGetCurrentSubSystem_FromSettings() + { + RwInt32 subSystem = *reinterpret_cast(static_cast(FrontEndMenuManager) + MenuManagerAdapterOffset); + if (subSystem > 0) + { + // Force the device selection dialog to show again if anything is wrong + bool bResetDisplay = false; + if ((*ppRWD3D9)->GetAdapterCount() <= (UINT)subSystem) + { + subSystem = 0; + bResetDisplay = true; + } + if (RwEngineSetSubSystem(subSystem) == FALSE || bResetDisplay) + { + RememberDeviceSelection(false); + return 0; + } + } + return subSystem; + } + + static void CreateNewButtonTooltip(HINSTANCE hInstance, HWND hDlg) + { + HWND hCheckbox = GetDlgItem(hDlg, IDC_REMEMBERRESCHOICE); + HWND hwndTip = CreateWindowEx(WS_EX_TOPMOST, TOOLTIPS_CLASS, NULL, WS_POPUP | TTS_ALWAYSTIP, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, + hDlg, nullptr, hInstance, nullptr); + + if (hCheckbox == nullptr || hwndTip == nullptr) + { + return; + } + + TOOLINFO toolInfo { sizeof(toolInfo) }; + toolInfo.hwnd = hDlg; + toolInfo.uFlags = TTF_IDISHWND | TTF_SUBCLASS; + toolInfo.uId = (UINT_PTR)hCheckbox; + toolInfo.lpszText = (LPWSTR)TEXT("Delete 'device_remembered.set' from GTA San Andreas User Files to show this dialog again."); + + SendMessage(hwndTip, TTM_ADDTOOL, 0, (LPARAM)&toolInfo); + } + + struct WrappedDialocFunc + { + DLGPROC lpDialogFunc; + LPARAM dwInitParam; + }; + static INT_PTR CALLBACK CustomDlgProc(HWND window, UINT msg, WPARAM wParam, LPARAM lParam) + { + if (msg == WM_INITDIALOG) + { + const WrappedDialocFunc* data = reinterpret_cast(lParam); + SetWindowLongPtr(window, GWLP_USERDATA, reinterpret_cast(data->lpDialogFunc)); + + data->lpDialogFunc(window, msg, wParam, data->dwInitParam); + + // The stock dialog func loaded the selected adapter and resolution at this point, + // we can bail if we don't need to show the dialog + if (ShouldSkipDeviceSelection()) + { + // The game inits the selected resolution weirdly, and corrects it in the IDOK handler + // so let's invoke it manually (bleh) + data->lpDialogFunc(window, WM_COMMAND, IDOK, 0); + return TRUE; + } + + HMODULE hGameModule = GetModuleHandle(nullptr); + SendMessage(window, WM_SETICON, ICON_SMALL, reinterpret_cast(LoadIcon(hGameModule, MAKEINTRESOURCE(100)))); + CreateNewButtonTooltip(hGameModule, window); + + // Return TRUE instead of FALSE on init, we want keyboard focus + return TRUE; + } + + // Custom handling for IDCANCEL (IDOK is fine) + if (msg == WM_COMMAND) + { + if (LOWORD(wParam) == IDCANCEL) + { + EndDialog(window, 0); + return TRUE; + } + + // Just remember the selection, let the game handle the rest + if (LOWORD(wParam) == IDOK && IsDlgButtonChecked(window, IDC_REMEMBERRESCHOICE) == BST_CHECKED) + { + RememberDeviceSelection(true); + } + } + + DLGPROC origProc = reinterpret_cast(GetWindowLongPtr(window, GWLP_USERDATA)); + if (origProc != nullptr) + { + return origProc(window, msg, wParam, lParam); + } + return FALSE; + } + + static INT_PTR WINAPI DialogBoxParamA_New(HINSTANCE /*hInstance*/, LPCSTR /*lpTemplateName*/, HWND /*hWndParent*/, DLGPROC lpDialogFunc, LPARAM dwInitParam) + { + int32_t (WINAPI *pSetThreadDpiAwarenessContext)(int32_t dpiContext) = nullptr; + int32_t oldDpiContext = 0; + + // Specify the dialog as DPI unaware, so Windows scales it by itself + HMODULE user32Module = LoadLibraryW(L"user32"); + if (user32Module != nullptr) + { + pSetThreadDpiAwarenessContext = (decltype(pSetThreadDpiAwarenessContext))GetProcAddress(user32Module, "SetThreadDpiAwarenessContext"); + } + + if (pSetThreadDpiAwarenessContext != nullptr) + { + oldDpiContext = pSetThreadDpiAwarenessContext(/*DPI_AWARENESS_CONTEXT_UNAWARE*/-1); + } + + ACTCTX actCtx { sizeof(actCtx) }; + actCtx.hModule = reinterpret_cast(&__ImageBase); + actCtx.lpResourceName = MAKEINTRESOURCE(2); + actCtx.dwFlags = ACTCTX_FLAG_HMODULE_VALID | ACTCTX_FLAG_RESOURCE_NAME_VALID; + + ULONG_PTR cookie = 0; + bool bContextActivated = false; + + HANDLE hActCtx = CreateActCtx(&actCtx); + if (hActCtx != INVALID_HANDLE_VALUE) + { + bContextActivated = ActivateActCtx(hActCtx, &cookie) != FALSE; + } + + // Include our own context to allow for custom message handling + const WrappedDialocFunc origDlgProc { lpDialogFunc, dwInitParam }; + const INT_PTR result = DialogBoxParam(reinterpret_cast(&__ImageBase), MAKEINTRESOURCE(IDD_RESSELECT), nullptr, CustomDlgProc, reinterpret_cast(&origDlgProc)); + + if (bContextActivated) + { + DeactivateActCtx(0, cookie); + } + if (hActCtx != INVALID_HANDLE_VALUE) + { + ReleaseActCtx(hActCtx); + } + + if (pSetThreadDpiAwarenessContext != nullptr) + { + pSetThreadDpiAwarenessContext(oldDpiContext); + } + if (user32Module != nullptr) + { + FreeLibrary(user32Module); + } + return result; + } + static auto* const pDialogBoxParamA_New = &DialogBoxParamA_New; +} + + // ============= LS-RP Mode stuff ============= namespace LSRPMode { @@ -5372,6 +5598,22 @@ void Patch_SA_10(HINSTANCE hInstance) Patch(0x63F576, 0x75); + // Improved resolution selection dialog + { + using namespace NewResolutionSelectionDialog; + + ppRWD3D9 = *(IDirect3D9***)(0x7F6312 + 1); + FrontEndMenuManager = *(void**)(0x4054DB + 1); + + orgGetDocumentsPath = (char*(*)())0x744FB0; + + Patch(0x0746241 + 2, &pDialogBoxParamA_New); + + InjectHook(0x7461D8, RwEngineGetSubSystemInfo_GetFriendlyNames); + InjectHook(0x7461ED, RwEngineGetCurrentSubSystem_FromSettings); + } + + #if FULL_PRECISION_D3D // Test - full precision D3D device Patch( 0x7F672B+1, *(uint8_t*)(0x7F672B+1) | D3DCREATE_FPU_PRESERVE ); @@ -7351,6 +7593,39 @@ void Patch_SA_NewBinaries_Common(HINSTANCE hInstance) Patch(isAlive, 0x75); } TXN_CATCH(); + + + // Improved resolution selection dialog + try + { + using namespace NewResolutionSelectionDialog; + + // RGL changed one of the parameters + auto dialogBoxParam = [] { + try { + // Steam + return get_pattern("51 FF 15 ? ? ? ? 85 C0 0F 84", 1 + 2); + } catch (const hook::txn_exception&) { + // RGL + return get_pattern("53 FF 15 ? ? ? ? 85 C0", 1 + 2); + } + }(); + + auto rRwEngineGetSubSystemInfo = get_pattern("E8 ? ? ? ? 46 83 C4 08 83 C7 50"); + auto rwEngineGetCurrentSubSystem = get_pattern("7C EA E8 ? ? ? ? A3", 2); + MenuManagerAdapterOffset = 0xD8; + + ppRWD3D9 = *get_pattern("33 ED A3 ? ? ? ? 3B C5", 2 + 1); + FrontEndMenuManager = *get_pattern("50 50 68 ? ? ? ? B9 ? ? ? ? E8", 7 + 1); // This has 2 identical matches, we just need one + + orgGetDocumentsPath = static_cast(get_pattern( "8D 45 FC 50 68 19 00 02 00", -6 )); + + Patch(dialogBoxParam, &pDialogBoxParamA_New); + + InjectHook(rRwEngineGetSubSystemInfo, RwEngineGetSubSystemInfo_GetFriendlyNames); + InjectHook(rwEngineGetCurrentSubSystem, RwEngineGetCurrentSubSystem_FromSettings); + } + TXN_CATCH(); } diff --git a/SilentPatchSA/SilentPatchSA.rc b/SilentPatchSA/SilentPatchSA.rc index 0b5f878e351a50ca91af87cdab1540fedd87842a..dea06f46a9c977ffbd9c1cac938a725a59f11891 100644 GIT binary patch literal 6184 zcmd6r+fE}#5QghIQr=-Q+$4$xc-gE;?ywE4h3&Pl&4Pr42;gAJfykI77kTz1-{0-R zV|#{W$w8x0Pxn-JSJ%0#=C41uY|9es*x0UYYWMa)8`%ul39tuyv`d@t)v#}Q68p(+ z?2a}CKBK<|Hl^3TM@CnS6TXCPY`?H()4sGXIAtsJ8$j=vov`{ktWDSUZ`!S;VYPN{ zgmt5^Pq_E2XMG#k(3;k=4|ZY4why$)xNB`Y1JdW+u{w}8V?SRsl<#rsyie>9xva`~ zgp}^>XZW8X9m!z;*QdN&aNU4U$zvb*8``QyKCOe_f_&q?W%-v+Vop)Cjz?rAOB%uV z>ukd%?|94AJ9P-qO(;QyN(tv_UZXdLA8)sin=gJaJQu%hTgef(vJ zH#M-M3#_TlBfNj`B!xQ%;5f!+FQBJwN9+*AZJ=GCL;AyDmk!Z7`DF{wd~X#n_gPhc z+M$#8p=u9k8~YYEHARlmGn$qkoPha=-OaG0Tzbq><2v=e$eVp$y;>D%75Tm9>+pUU zB7(k>`I8@E&ux1iW%rR>l$drYe!S+4yvUx~dR$5EocMH()y5dVpt6XfomW!yx#w(z zxO*M!%&C&r^5V+6wMllx7+&YQS(_=>SloIm7iC#$M4`SD{Nj$>B^y15K*mowOrKHA zXdXh$TfIU5?^r6I9KIlyYTeo7O<`+Sfw9ZSGm!>Zd@q!yO*qRog{Ay3@b6?*cAm>StlU z_V&Rld;{)xW$VJI+#=3&fJrNDW{>GRtjx|IaP}!Mtv;h==ajPu-#$Bw;KDEKcdQ>7 z$84kryf@z$p$*ROfln1icWEY`E!yOvlGp0m6bf(bS0o&(p*~*|+7&ToOb+Ka-$Dkv zL1rl&i>rRf@F(n%WG2DRB-6LZIxFpM`<;wH6y)FgF^>{&%CzeA1D?o_tUG0V#JdtS zu5pFiU$oTFUWKO9s+_Ac+#ibZ#T6;?P);h|Cr{WVBh@LgCE?t*oll(8fVx=2sg9GS zEkD8$npDMip=idjp=O;U+fm0d1;6{0J2>_jKEf`aWT0Bdk-_8!af#DBcK< zB-dM##LFnG20klDQIu7Yt@;|@|2>7upJSD{EIa>7;YgNwNll@eQg64^MWw0gYGKVY zEI6Th*V-R~exoL2G17!u75S^*U6Yr5RN9b?SD%!b8){s&A9+t%p=wtZPl7$&a&q)3 zx7o%#O352#EAoE*4UjJ1b4n{%Tm>rYP(0&_-dCAhhH4DyRjU=@j(|s7Q&h^+f8_67 zu(lZM#J&7O@nDff3zCh%XYAvFAmv=YBXP;HC_W2c<>Ob8k@_J#_8O3Mi zN3r7DNHH;cABje{seD{MtNZK#&3Eu^)vlsV*lJ7m>Z_yYKfiXN{l%!b8d(2NkzY@^ z2FhhUPK{INz3Qa+HzBJPWo3KZ*SdRSHLv~JqgJflmtxD;be>NZ>*L?44qPtf>s{L3 zM^Aq4zuCGymX9l^NL*>`{XhMKxBLg7WpEu-M-3xCdu&`O-M_H_YG5 Awg3PC delta 18 acmZ2su!L#ClF28y5+;A(VwBeS_bO? diff --git a/SilentPatchSA/SilentPatchSA.vcxproj b/SilentPatchSA/SilentPatchSA.vcxproj index 2ce16f6..040f436 100644 --- a/SilentPatchSA/SilentPatchSA.vcxproj +++ b/SilentPatchSA/SilentPatchSA.vcxproj @@ -207,6 +207,11 @@ copy /y "$(TargetPath)" "H:\Rockstar Games\Grand Theft Auto San Andreas\SilentPa NotUsing NotUsing + + NotUsing + NotUsing + NotUsing + NotUsing NotUsing @@ -250,10 +255,10 @@ copy /y "$(TargetPath)" "H:\Rockstar Games\Grand Theft Auto San Andreas\SilentPa + - @@ -279,11 +284,11 @@ copy /y "$(TargetPath)" "H:\Rockstar Games\Grand Theft Auto San Andreas\SilentPa - - + - + + diff --git a/SilentPatchSA/SilentPatchSA.vcxproj.filters b/SilentPatchSA/SilentPatchSA.vcxproj.filters index 3ccaeb8..729329f 100644 --- a/SilentPatchSA/SilentPatchSA.vcxproj.filters +++ b/SilentPatchSA/SilentPatchSA.vcxproj.filters @@ -84,6 +84,9 @@ Source Files + + Source Files + @@ -116,18 +119,12 @@ Header Files - - Header Files - Source Files\decoders Source Files\decoders - - Header Files - Header Files @@ -173,18 +170,24 @@ Header Files - - - - Resource Files - - - Resource Files - + + Header Files + + + Header Files + Resource Files + + + Resource Files + + + Resource Files + + \ No newline at end of file diff --git a/SilentPatchSA/resource.h b/SilentPatchSA/resource.h index e262f95..85e8a6e 100644 --- a/SilentPatchSA/resource.h +++ b/SilentPatchSA/resource.h @@ -3,3 +3,16 @@ // Used by SilentPatchSA.rc // #define IDB_LUNAR64 101 +#define IDD_RESSELECT 102 +#define IDC_REMEMBERRESCHOICE 1003 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 104 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1004 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif