@@ -3403,6 +3403,182 @@ SK_ShutdownCore (const wchar_t* backend)
34033403 return true ;
34043404}
34053405
3406+ std::vector <std::wstring>
3407+ SK_GameConfigStore_GetAllChildKeys (CRegKey& parent_key)
3408+ {
3409+ std::vector <std::wstring> sub_keys;
3410+
3411+ DWORD idx = 0 ;
3412+ wchar_t wszKeyName [MAX_PATH ];
3413+ DWORD capacity = _countof (wszKeyName);
3414+
3415+ while (parent_key.EnumKey (idx++, wszKeyName, &capacity) == ERROR_SUCCESS )
3416+ {
3417+ sub_keys.push_back (wszKeyName);
3418+ capacity = _countof (wszKeyName);
3419+ }
3420+
3421+ return sub_keys;
3422+ }
3423+
3424+ #include < windows.h>
3425+ #include < atlbase.h>
3426+ #include < iostream>
3427+ #include < string>
3428+ #include < vector>
3429+ #include < algorithm>
3430+ #include < wincrypt.h>
3431+ #include < combaseapi.h>
3432+ #include < shlwapi.h>
3433+
3434+ #pragma comment(lib, "crypt32.lib")
3435+ #pragma comment(lib, "shlwapi.lib")
3436+ #pragma comment(lib, "ole32.lib")
3437+
3438+ // Helper to convert lowercase string to SHA1 Hex string
3439+ std::wstring
3440+ GetSha1Utf16Le (const std::wstring& text)
3441+ {
3442+ HCRYPTPROV hProv = 0 ;
3443+ HCRYPTHASH hHash = 0 ;
3444+
3445+ std::wstring hexResult = L" " ;
3446+
3447+ if (CryptAcquireContext (&hProv, NULL , NULL , PROV_RSA_FULL , CRYPT_VERIFYCONTEXT ))
3448+ {
3449+ if (CryptCreateHash (hProv, CALG_SHA1 , 0 , 0 , &hHash))
3450+ {
3451+ // Hash the UTF-16LE bytes directly
3452+ DWORD cbData =
3453+ static_cast <DWORD > (text.length () * sizeof (wchar_t ));
3454+
3455+ if (CryptHashData (hHash, reinterpret_cast <const BYTE *>(text.c_str ()), cbData, 0 ))
3456+ {
3457+ BYTE rgbHash [20 ] = {};
3458+ DWORD cbHash = sizeof (rgbHash);
3459+
3460+ if (CryptGetHashParam (hHash, HP_HASHVAL , rgbHash, &cbHash, 0 ))
3461+ {
3462+ wchar_t hex [3 ];
3463+
3464+ for (DWORD i = 0 ; i < cbHash; i++)
3465+ {
3466+ swprintf_s (hex, 3 , L" %02x" , rgbHash [i]);
3467+
3468+ hexResult += hex;
3469+ }
3470+ }
3471+ }
3472+
3473+ CryptDestroyHash (hHash);
3474+ }
3475+
3476+ CryptReleaseContext (hProv, 0 );
3477+ }
3478+
3479+ return hexResult;
3480+ }
3481+
3482+ // Helper for DPAPI CryptProtectData using LocalMachine scope
3483+ std::vector<BYTE >
3484+ ProtectLocalMachineUtf16Le (const std::wstring& text)
3485+ {
3486+ DATA_BLOB dataIn;
3487+ DATA_BLOB dataOut;
3488+
3489+ std::vector <BYTE > encryptedBlob;
3490+
3491+ dataIn.pbData = reinterpret_cast <BYTE *>(const_cast <wchar_t *> (text.c_str ()));
3492+ dataIn.cbData = static_cast <DWORD > (text.length () * sizeof (wchar_t ));
3493+
3494+ // CRYPTPROTECT_LOCAL_MACHINE matches the PowerShell DataProtectionScope::LocalMachine
3495+ if (CryptProtectData (&dataIn, NULL , NULL , NULL , NULL , CRYPTPROTECT_LOCAL_MACHINE , &dataOut))
3496+ {
3497+ encryptedBlob.assign (dataOut.pbData , dataOut.pbData + dataOut.cbData );
3498+ LocalFree (dataOut.pbData );
3499+ }
3500+
3501+ return encryptedBlob;
3502+ }
3503+
3504+ // Helper to generate an upper/lowercase GUID string via COM API
3505+ std::wstring
3506+ CreateGuidString (void )
3507+ {
3508+ GUID guid;
3509+ std::wstring guidStr = L" " ;
3510+
3511+ if (SUCCEEDED (CoCreateGuid (&guid)))
3512+ {
3513+ wchar_t wszGuid [40 ];
3514+
3515+ // StringFromGUID2 includes braces; we can manually format to match PowerShell's GUID string
3516+ swprintf_s (wszGuid, 40 , L" %08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x" ,
3517+ guid.Data1 , guid.Data2 , guid.Data3 ,
3518+ guid.Data4 [0 ], guid.Data4 [1 ], guid.Data4 [2 ], guid.Data4 [3 ],
3519+ guid.Data4 [4 ], guid.Data4 [5 ], guid.Data4 [6 ], guid.Data4 [7 ]);
3520+
3521+ guidStr = wszGuid;
3522+ }
3523+
3524+ return guidStr;
3525+ }
3526+
3527+ // Scans target key children for an existing absolute path match
3528+ bool
3529+ CheckDuplicate (CRegKey& parentRootKey, const std::wstring& exePath)
3530+ {
3531+ wchar_t child_name [256 ];
3532+ DWORD capacity = _countof (child_name);
3533+ DWORD idx = 0 ;
3534+
3535+ while (parentRootKey.EnumKey (idx, child_name, &capacity) == ERROR_SUCCESS )
3536+ {
3537+ CRegKey childKey;
3538+
3539+ if (childKey.Open (parentRootKey, child_name, KEY_READ ) == ERROR_SUCCESS )
3540+ {
3541+ wchar_t matched_path [MAX_PATH ];
3542+ ULONG chars = _countof (matched_path);
3543+
3544+ if (childKey.QueryStringValue (L" MatchedExeFullPath" , matched_path, &chars) == ERROR_SUCCESS )
3545+ {
3546+ if (! _wcsicmp (matched_path, exePath.c_str ()))
3547+ {
3548+ return true ;
3549+ }
3550+ }
3551+ }
3552+
3553+ idx++;
3554+
3555+ capacity = _countof (child_name);
3556+ }
3557+ return false ;
3558+ }
3559+
3560+ std::wstring
3561+ SK_Win32_ToLowerInvariant (const std::wstring& input)
3562+ {
3563+ if (input.empty ())
3564+ return L" " ;
3565+
3566+ // Determine the required buffer size
3567+ int size = LCMapStringEx (LOCALE_NAME_INVARIANT , LCMAP_LOWERCASE ,
3568+ input.c_str (), -1 , nullptr , 0 , nullptr , nullptr , 0 );
3569+
3570+ std::wstring output (size, 0 );
3571+
3572+ // Map the string to lowercase
3573+ LCMapStringEx (LOCALE_NAME_INVARIANT , LCMAP_LOWERCASE ,
3574+ input.c_str (), -1 , &output[0 ], size, nullptr , nullptr , 0 );
3575+
3576+ // Remove the trailing null terminator that LCMapStringEx includes
3577+ output.resize (size - 1 );
3578+
3579+ return output;
3580+ }
3581+
34063582void
34073583SK_FrameCallback ( SK_RenderBackend& rb,
34083584 ULONG64 frames_drawn =
@@ -3430,6 +3606,157 @@ SK_FrameCallback ( SK_RenderBackend& rb,
34303606 SK_UTF8ToWideChar (SK_GetFriendlyAppName ()).c_str ()
34313607 );
34323608
3609+ CRegKey hkGameConfigChildRoot;
3610+ if (ERROR_SUCCESS == hkGameConfigChildRoot.Open (HKEY_CURRENT_USER , LR"( System\GameConfigStore\Children)" ))
3611+ {
3612+ bool is_a_game = false ;
3613+
3614+ auto child_keys =
3615+ SK_GameConfigStore_GetAllChildKeys (hkGameConfigChildRoot);
3616+
3617+ // nb: This is the wrong way to do this, hash the exe name and then look through
3618+ // the multi-string key for children instead.
3619+ for ( auto & child_key : child_keys )
3620+ {
3621+ CRegKey hkChildKey;
3622+ hkChildKey.Open (hkGameConfigChildRoot, child_key.c_str ());
3623+
3624+ ULONG ulMatchedExeFullPathLen = MAX_PATH ;
3625+ wchar_t wszMatchedExeFullPath [MAX_PATH ] = {};
3626+
3627+ if (hkChildKey.QueryStringValue (L" MatchedExeFullPath" ,
3628+ wszMatchedExeFullPath,
3629+ &ulMatchedExeFullPathLen) == ERROR_SUCCESS )
3630+ {
3631+ if (StrStrIW (wszMatchedExeFullPath, SK_GetHostApp ()))
3632+ {
3633+ if (config.system .log_level > 0 )
3634+ SK_ImGui_Warning (L" This is a game!" );
3635+
3636+ is_a_game = true ;
3637+ break ;
3638+ }
3639+ }
3640+ }
3641+
3642+ if (! is_a_game)
3643+ {
3644+ wchar_t wszTargetExePath [MAX_PATH * 2 ] = { };
3645+ wchar_t wszExeParentDir [MAX_PATH * 2 ] = { };
3646+
3647+ HANDLE hFile =
3648+ CreateFileW (SK_GetFullyQualifiedApp (), GENERIC_READ , FILE_SHARE_READ , nullptr ,
3649+ OPEN_EXISTING , FILE_ATTRIBUTE_NORMAL , nullptr );
3650+
3651+ if (hFile != INVALID_HANDLE_VALUE )
3652+ {
3653+ SK_File_GetNameFromHandle (hFile, wszTargetExePath, MAX_PATH );
3654+ CloseHandle (hFile);
3655+ }
3656+
3657+ PathResolve (wszTargetExePath, nullptr , PRF_VERIFYEXISTS | PRF_REQUIREABSOLUTE );
3658+ wszTargetExePath [0 ] = SK_GetHostPath ()[0 ]; // Ensure drive letter case matches host path
3659+
3660+ wcsncpy_s (wszExeParentDir, MAX_PATH , wszTargetExePath, _TRUNCATE);
3661+ PathRemoveFileSpec (wszExeParentDir);
3662+
3663+ {
3664+ auto wszHostAppLower =
3665+ SK_Win32_ToLowerInvariant (SK_GetHostApp ());
3666+
3667+ std::vector <BYTE > parentBlob =
3668+ ProtectLocalMachineUtf16Le (wszHostAppLower.c_str ());
3669+
3670+ // Calculate current 64-bit UTC file time
3671+ FILETIME ft = {};
3672+ GetSystemTimeAsFileTime (&ft);
3673+
3674+ ULARGE_INTEGER
3675+ uli = {};
3676+ uli.LowPart = ft.dwLowDateTime ;
3677+ uli.HighPart = ft.dwHighDateTime ;
3678+
3679+ ULONGLONG lastAccessedTime = uli.QuadPart ;
3680+
3681+ CRegKey hkGameConfigParentsRoot;
3682+ hkGameConfigParentsRoot.Open (HKEY_CURRENT_USER , LR"( System\GameConfigStore\Parents)" );
3683+
3684+ std::wstring parentKeyName =
3685+ GetSha1Utf16Le (wszHostAppLower.c_str ());
3686+
3687+ auto childGuid = CreateGuidString ();
3688+ auto gameGuid = CreateGuidString ();
3689+
3690+ if (config.system .log_level > 0 )
3691+ SK_ImGui_Warning (SK_FormatStringW (L" GUID: %ws" , childGuid.c_str ()).c_str ());
3692+
3693+ // Create subkeys inside Children and Parents path trees
3694+ CRegKey parentSubKey, childSubKey;
3695+ if (parentSubKey.Create (hkGameConfigParentsRoot, parentKeyName.c_str (), REG_NONE , REG_OPTION_NON_VOLATILE , KEY_READ | KEY_WRITE ) == ERROR_SUCCESS &&
3696+ childSubKey.Create (hkGameConfigChildRoot, childGuid.c_str (), REG_NONE , REG_OPTION_NON_VOLATILE , KEY_READ | KEY_WRITE ) == ERROR_SUCCESS )
3697+ {
3698+ // Write properties to the Child GUID target path
3699+ childSubKey.SetStringValue (L" ExeParentDirectory" , wszExeParentDir);
3700+ childSubKey.SetDWORDValue (L" Type" , 0x1 );
3701+ childSubKey.SetDWORDValue (L" Revision" , 0x1 );
3702+ childSubKey.SetDWORDValue (L" Flags" , 0x11 );
3703+ childSubKey.SetBinaryValue (L" Parent" , parentBlob.data (),
3704+ static_cast <ULONG > (parentBlob.size ()));
3705+ childSubKey.SetStringValue (L" GameDVR_GameGUID" , gameGuid.c_str ());
3706+ childSubKey.SetStringValue (L" MatchedExeFullPath" , wszTargetExePath);
3707+ childSubKey.SetQWORDValue (L" LastAccessed" , lastAccessedTime);
3708+
3709+ // Update parent's 'Children' MultiString properties
3710+ std::vector <std::wstring> childrenArray;
3711+ ULONG multiSize = 0 ;
3712+
3713+ // Query existing MultiString buffer sizing requirement
3714+ if (parentSubKey.QueryMultiStringValue (L" Children" , NULL , &multiSize) == ERROR_SUCCESS && multiSize > 2 )
3715+ { std::vector <wchar_t > buffer (multiSize);
3716+ if (parentSubKey.QueryMultiStringValue (L" Children" , buffer.data (), &multiSize) == ERROR_SUCCESS )
3717+ {
3718+ wchar_t * p = buffer.data ();
3719+ while (*p)
3720+ {
3721+ std::wstring standardStr (p);
3722+
3723+ if (! standardStr.empty () && std::wcsspn (standardStr.c_str (), L" \t\n\r " ) != standardStr.length ())
3724+ {
3725+ childrenArray.push_back (standardStr);
3726+ }
3727+
3728+ p += standardStr.length () + 1 ;
3729+ }
3730+ }
3731+ }
3732+
3733+ // Ensure uniqueness and avoid array duplicates before adding
3734+ if (std::find (childrenArray.begin (), childrenArray.end (), childGuid) == childrenArray.end ())
3735+ {
3736+ childrenArray.push_back (childGuid);
3737+ }
3738+
3739+ // Reconstruct MultiString structure bytes block (sequences ending with double null bytes)
3740+ std::vector <wchar_t > multiStringBuffer;
3741+
3742+ for (const auto & str : childrenArray)
3743+ {
3744+ multiStringBuffer.insert (multiStringBuffer.end (),
3745+ str.begin (), str.end ());
3746+ multiStringBuffer.push_back (L' \0 ' );
3747+ }
3748+
3749+ multiStringBuffer.push_back (L' \0 ' ); // Double null terminator
3750+
3751+ // Finalize update inside parent multi-string table
3752+ RegSetValueExW (parentSubKey.m_hKey , L" Children" , 0 , REG_MULTI_SZ ,
3753+ reinterpret_cast <const BYTE *> (multiStringBuffer.data ()),
3754+ static_cast <DWORD > (multiStringBuffer.size () * sizeof (wchar_t )));
3755+ }
3756+ }
3757+ }
3758+ }
3759+
34333760 // void SK_NVAPI_DumpProfileSettings (void);
34343761 // SK_NVAPI_DumpProfileSettings ();
34353762
0 commit comments