diff --git a/CMakeLists.txt b/CMakeLists.txt index 0d6239a..256628e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -217,6 +217,12 @@ set_target_properties(GameAnalytics PROPERTIES C_VISIBILITY_PRESET hidden CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN ON + # The static build gets linked into consumers' shared libraries (every Godot + # GDExtension is a .so), which fails on Linux without PIC: + # relocation R_X86_64_PC32 against symbol `_ZSt4cerr' can not be used when + # making a shared object; recompile with -fPIC + # No-op for MSVC, already the default on macOS. + POSITION_INDEPENDENT_CODE ON ) if(${GA_SHARED_LIB}) diff --git a/source/gameanalytics/Platform/GAWin32.cpp b/source/gameanalytics/Platform/GAWin32.cpp index aaa8e2e..cf06281 100644 --- a/source/gameanalytics/Platform/GAWin32.cpp +++ b/source/gameanalytics/Platform/GAWin32.cpp @@ -10,6 +10,11 @@ #include #include +#include + +#include +#include +#include #pragma comment(lib, "Wininet.lib") @@ -23,51 +28,94 @@ void (*GAPlatformWin32::old_state_abrt) (int) = nullptr; void (*GAPlatformWin32::old_state_fpe) (int) = nullptr; void (*GAPlatformWin32::old_state_segv) (int) = nullptr; -// no official helper function exists for win11 (for the time being at least) -bool IsWin11OrGreater() +// RtlGetVersion reports the real OS version. GetVersionEx and the VersionHelpers.h functions +// are subject to the app manifest: without a supportedOS entry for Windows 10 they report 6.2 (Windows 8), +// and they have no notion of a build number so cannot tell Windows 11 apart from Windows 10. +bool getWindowsVersion(DWORD& major, DWORD& minor, DWORD& build) { - constexpr DWORD MAJOR_VERSION = 10; - constexpr DWORD MINOR_VERSION = 0; - constexpr DWORD BUILD_NUM = 21996; + using RtlGetVersionFn = LONG (WINAPI*)(PRTL_OSVERSIONINFOW); + + HMODULE ntdll = GetModuleHandleW(L"ntdll.dll"); + if (!ntdll) + { + return false; + } + + auto rtlGetVersion = reinterpret_cast(GetProcAddress(ntdll, "RtlGetVersion")); + if (!rtlGetVersion) + { + return false; + } + + RTL_OSVERSIONINFOW info = {}; + info.dwOSVersionInfoSize = sizeof(info); + + // returns an NTSTATUS, 0 is STATUS_SUCCESS + if (rtlGetVersion(&info) != 0) + { + return false; + } + + major = info.dwMajorVersion; + minor = info.dwMinorVersion; + build = info.dwBuildNumber; - return IsWindowsVersionOrGreater(MAJOR_VERSION, MINOR_VERSION, BUILD_NUM); + return true; } std::string GAPlatformWin32::getOSVersion() { std::string osVersion = getBuildPlatform() + " "; - #if (_MSC_VER >= 1900) - if(IsWin11OrGreater()) - { - return osVersion + "11"; - } - else if (IsWindows10OrGreater()) - { - return osVersion + "10"; - } - #endif + DWORD major = 0; + DWORD minor = 0; + DWORD build = 0; - if (IsWindows8Point1OrGreater()) - { - return osVersion + "6.3"; - } - else if (IsWindows8OrGreater()) - { - return osVersion + "6.2"; - } - else if (IsWindows7OrGreater()) + if (getWindowsVersion(major, minor, build)) + { + // Windows 11 kept the 10.0 version number, the first release build is 22000 + constexpr DWORD WIN11_FIRST_BUILD = 22000; + + if (major == 10) { - return osVersion + "6.1"; + return osVersion + (build >= WIN11_FIRST_BUILD ? "11" : "10"); } - else if (IsWindowsVistaOrGreater()) + else if (major > 10) { - return osVersion + "6.1"; + return osVersion + std::to_string(major); } - else if (IsWindowsXPOrGreater()) + + return osVersion + std::to_string(major) + "." + std::to_string(minor); + } + + // fallback, manifest dependent (see above) + #if (_MSC_VER >= 1900) + if (IsWindows10OrGreater()) { - return osVersion + "5.1"; + return osVersion + "10"; } + #endif + + if (IsWindows8Point1OrGreater()) + { + return osVersion + "6.3"; + } + else if (IsWindows8OrGreater()) + { + return osVersion + "6.2"; + } + else if (IsWindows7OrGreater()) + { + return osVersion + "6.1"; + } + else if (IsWindowsVistaOrGreater()) + { + return osVersion + "6.0"; + } + else if (IsWindowsXPOrGreater()) + { + return osVersion + "5.1"; + } return osVersion + "0.0.0"; } @@ -119,29 +167,31 @@ std::string GAPlatformWin32::getPersistentPath() std::string getRegistryKey(HKEY key, const TCHAR* subkey, const TCHAR* value) { + // RegGetValue reports errors through its return value, not GetLastError, + // and the size it reports is in bytes (including the null terminator) + DWORD size = 0; + if (RegGetValue(key, subkey, value, RRF_RT_REG_SZ, nullptr, nullptr, &size) != ERROR_SUCCESS || size <= sizeof(TCHAR)) { - constexpr DWORD maxBufSize = 128; + return UNKNOWN_VALUE; + } - DWORD size = maxBufSize * sizeof(TCHAR); - TCHAR buffer[maxBufSize] = _T(""); - RegGetValue(key, subkey, value, RRF_RT_REG_SZ, NULL, buffer, &size); + std::vector buffer(size / sizeof(TCHAR) + 1, 0); + size = static_cast(buffer.size() * sizeof(TCHAR)); - if (!GetLastError() && size > 0) - { - std::string val; + if (RegGetValue(key, subkey, value, RRF_RT_REG_SZ, nullptr, buffer.data(), &size) != ERROR_SUCCESS) + { + return UNKNOWN_VALUE; + } + + std::string val; #ifdef UNICODE - std::wstring wstr(buffer, buffer + size); - val = utilities::GAUtilities::ws2s(wstr); + val = utilities::GAUtilities::ws2s(std::wstring(buffer.data())); #else - val = std::string(buffer, buffer + size); + val = std::string(buffer.data()); #endif - return val; - } - } - - return UNKNOWN_VALUE; + return val.empty() ? UNKNOWN_VALUE : val; } std::string GAPlatformWin32::getDeviceModel() @@ -160,20 +210,102 @@ std::string GAPlatformWin32::getDeviceManufacturer() return getRegistryKey(HKEY_LOCAL_MACHINE, subkey, value); } +// trims and collapses repeated whitespace (brand strings are often padded, e.g. " Intel(R) Core(TM) i7 CPU") +std::string normalizeCpuName(std::string const& name) +{ + std::string result; + result.reserve(name.size()); + + for (char c : name) + { + if (c == '\0') + { + break; + } + + if (std::isspace(static_cast(c))) + { + if (!result.empty() && result.back() != ' ') + { + result += ' '; + } + } + else + { + result += c; + } + } + + if (!result.empty() && result.back() == ' ') + { + result.pop_back(); + } + + return result; +} + +std::string getCpuidBrandString() +{ +#if (defined(_M_X64) || defined(_M_IX86)) && !defined(_M_ARM64EC) + int regs[4] = {}; + + __cpuid(regs, 0x80000000); + if (static_cast(regs[0]) < 0x80000004) + { + return ""; + } + + char brand[49] = {}; + for (int i = 0; i < 3; ++i) + { + __cpuid(regs, 0x80000002 + i); + std::memcpy(brand + i * sizeof(regs), regs, sizeof(regs)); + } + + return normalizeCpuName(brand); +#else + return ""; +#endif +} + +std::string getCpuArchitecture() +{ + SYSTEM_INFO info = {}; + GetNativeSystemInfo(&info); + + switch (info.wProcessorArchitecture) + { + case PROCESSOR_ARCHITECTURE_AMD64: return "x86_64"; + case PROCESSOR_ARCHITECTURE_ARM64: return "arm64"; + case PROCESSOR_ARCHITECTURE_INTEL: return "x86"; + case PROCESSOR_ARCHITECTURE_ARM: return "arm"; + default: return UNKNOWN_VALUE; + } +} + std::string GAPlatformWin32::getCpuModel() const { + // the registry holds the name of the physical cpu (also correct for x64 apps emulated on arm64) constexpr const TCHAR* subkey = _T("HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0"); + constexpr const TCHAR* value = _T("ProcessorNameString"); - constexpr const TCHAR* values[] = { _T("ProcessorName"), _T("ProcessorNameString") }; + std::string name = getRegistryKey(HKEY_LOCAL_MACHINE, subkey, value); + if (name != UNKNOWN_VALUE) + { + name = normalizeCpuName(name); + if (!name.empty()) + { + return name; + } + } - for (auto& val : values) + name = getCpuidBrandString(); + if (!name.empty()) { - std::string s = getRegistryKey(HKEY_LOCAL_MACHINE, subkey, val); - if (!s.empty()) - return s; + return name; } - return UNKNOWN_VALUE; + return getCpuArchitecture(); } void GAPlatformWin32::setupUncaughtExceptionHandler() diff --git a/source/gameanalytics/Platform/GAWin32.h b/source/gameanalytics/Platform/GAWin32.h index 446b4d4..836d910 100644 --- a/source/gameanalytics/Platform/GAWin32.h +++ b/source/gameanalytics/Platform/GAWin32.h @@ -19,13 +19,6 @@ #include #include - #ifdef GA_SHARED_LIB - #include - #include - #pragma comment(lib, "wbemuuid.lib") - #endif - - #pragma comment(lib, "Ws2_32.lib") #pragma comment(lib, "gdi32.lib") #pragma comment(lib, "crypt32.lib")