Allow safe coexistence with Steam controller blacklist

This commit is contained in:
Dan Bosscher committed 2026-08-03 10:37:30 +01:00
1 parent b718597425
commit 4ecc2b08dc
6 files changed
+195 -27

No files matched your search

+156 -6
View File
@@ -1,28 +1,176 @@
#include "SteamWatcher.h"
#include "EventLog.h"
#include <Windows.h>
#include <TlHelp32.h>
#include <algorithm>
#include <cctype>
#include <string>
static constexpr int POLL_INTERVAL_MS = 2000;
static constexpr int POLL_SLICE_MS = 100; // wake often for fast Stop()
static constexpr int LESS_STEAM_POLL_COUNT = 3; // ~6s stable before we take over
static bool IsSteamProcessRunning() {
struct SteamProcess {
bool running = false;
FILETIME started{};
};
static SteamProcess FindSteamProcess() {
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snap == INVALID_HANDLE_VALUE) return false;
if (snap == INVALID_HANDLE_VALUE) return {};
PROCESSENTRY32W pe{};
pe.dwSize = sizeof(pe);
bool found = false;
DWORD pid = 0;
if (Process32FirstW(snap, &pe)) {
do {
if (_wcsicmp(pe.szExeFile, L"steam.exe") == 0) {
found = true;
pid = pe.th32ProcessID;
break;
}
} while (Process32NextW(snap, &pe));
}
CloseHandle(snap);
return found;
if (pid == 0) return {};
SteamProcess result{};
result.running = true;
HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
if (!process) return result;
FILETIME exited{}, kernel{}, user{};
GetProcessTimes(process, &result.started, &exited, &kernel, &user);
CloseHandle(process);
return result;
}
static bool ReadRegistryString(HKEY root, const wchar_t* subkey, const wchar_t* name,
std::wstring& value) {
DWORD bytes = 0;
if (RegGetValueW(root, subkey, name, RRF_RT_REG_SZ, nullptr, nullptr, &bytes)
!= ERROR_SUCCESS || bytes < sizeof(wchar_t))
return false;
value.resize(bytes / sizeof(wchar_t));
if (RegGetValueW(root, subkey, name, RRF_RT_REG_SZ, nullptr, value.data(), &bytes)
!= ERROR_SUCCESS)
return false;
while (!value.empty() && value.back() == L'\0') value.pop_back();
return !value.empty();
}
static bool ReadFileTail(const std::wstring& path, size_t maxBytes, std::string& text,
FILETIME* lastWrite = nullptr) {
HANDLE file = CreateFileW(path.c_str(), GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (file == INVALID_HANDLE_VALUE) return false;
LARGE_INTEGER size{};
if (!GetFileSizeEx(file, &size) || size.QuadPart < 0) {
CloseHandle(file);
return false;
}
if (lastWrite && !GetFileTime(file, nullptr, nullptr, lastWrite)) {
CloseHandle(file);
return false;
}
const ULONGLONG fileBytes = static_cast<ULONGLONG>(size.QuadPart);
const DWORD bytes = static_cast<DWORD>(std::min<ULONGLONG>(fileBytes, maxBytes));
LARGE_INTEGER offset{};
offset.QuadPart = size.QuadPart - bytes;
if (!SetFilePointerEx(file, offset, nullptr, FILE_BEGIN)) {
CloseHandle(file);
return false;
}
text.resize(bytes);
DWORD read = 0;
const bool ok = bytes == 0 || ReadFile(file, text.data(), bytes, &read, nullptr);
CloseHandle(file);
if (!ok) return false;
text.resize(read);
return true;
}
static void LowerAscii(std::string& text) {
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char ch) {
return static_cast<char>(std::tolower(ch));
});
}
static bool ReportControllerHidden(bool hidden, const char* reason) {
static std::string previous;
const std::string current = std::string(hidden ? "hidden: " : "not hidden: ") + reason;
if (current != previous) {
EventLog::Write("STEAM: physical controller %s", current.c_str());
previous = current;
}
return hidden;
}
static std::string ControllerBlacklistValue(const std::string& config) {
constexpr char key[] = "\"controller_blacklist\"";
const size_t keyPos = config.rfind(key);
if (keyPos == std::string::npos) return {};
const size_t open = config.find('"', keyPos + sizeof(key) - 1);
if (open == std::string::npos) return {};
const size_t close = config.find('"', open + 1);
if (close == std::string::npos) return {};
return config.substr(open + 1, close - open - 1);
}
// Steam writes an explicit line for every device suppressed by
// controller_blacklist. Requiring that line from the current client session is
// important: editing config.vdf while Steam is already running does not revoke
// the handles Steam has open. Any missing or ambiguous evidence fails closed.
static bool IsControllerHiddenFromSteam(const SteamProcess& steam) {
if (!steam.running || (steam.started.dwLowDateTime == 0 && steam.started.dwHighDateTime == 0))
return ReportControllerHidden(false, "process start time unavailable");
std::wstring steamPath;
if (!ReadRegistryString(HKEY_CURRENT_USER, L"Software\\Valve\\Steam", L"SteamPath",
steamPath))
return ReportControllerHidden(false, "SteamPath unavailable");
std::replace(steamPath.begin(), steamPath.end(), L'/', L'\\');
std::string config;
const std::wstring configPath = steamPath + L"\\config\\config.vdf";
if (!ReadFileTail(configPath, 1024 * 1024, config))
return ReportControllerHidden(false, "config.vdf unavailable");
LowerAscii(config);
const std::string blacklist = ControllerBlacklistValue(config);
if (blacklist.empty())
return ReportControllerHidden(false, "controller_blacklist missing");
FILETIME logWrite{};
std::string log;
if (!ReadFileTail(steamPath + L"\\logs\\controller.txt", 4 * 1024 * 1024, log,
&logWrite)
|| CompareFileTime(&logWrite, &steam.started) < 0)
return ReportControllerHidden(false, "current controller log unavailable");
LowerAscii(log);
const size_t sessionPos = log.rfind("client version:");
if (sessionPos == std::string::npos)
return ReportControllerHidden(false, "current controller session unavailable");
const std::string session = log.substr(sessionPos);
constexpr const char* ids[] = {"28de/1302", "28de/1303", "28de/1304"};
bool hidden = false;
for (const char* id : ids) {
std::string type(id);
std::replace(type.begin(), type.end(), '/', ' ');
if (session.find("type: " + type) != std::string::npos)
return ReportControllerHidden(false, "Steam opened a physical controller");
if (blacklist.find(id) != std::string::npos
&& session.find(std::string("hiding blacklisted device ") + id)
!= std::string::npos)
hidden = true;
}
return ReportControllerHidden(hidden, hidden ? "confirmed by current Steam session"
: "no matching hide event");
}
// Steam publishes the app id of the running game here and resets it to 0 on
@@ -39,7 +187,9 @@ static bool IsGameRunning() {
}
SteamState SteamWatcher::Detect() {
if (!IsSteamProcessRunning()) return SteamState::NoSteam;
const SteamProcess steam = FindSteamProcess();
if (!steam.running) return SteamState::NoSteam;
if (IsControllerHiddenFromSteam(steam)) return SteamState::ControllerHidden;
return IsGameRunning() ? SteamState::InGame : SteamState::SteamIdle;
}