mirror of
https://github.com/ddeverill/SteamlessController.git
synced 2026-10-22 14:42:16 -05:00
Allow safe coexistence with Steam controller blacklist
This commit is contained in:
1 parent
b718597425
commit
4ecc2b08dc
6 files changed
+195
-27
No files matched your search
+156
-6
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user