#include "utils.h" #include "localization.h" Json Utils::read_json(std::wstring file, int maxRetries, int retryDelay) { int attempts = 0; while (attempts < maxRetries) { std::ifstream in(file); if (!in.is_open()) { if (++attempts == maxRetries) { return Json(); } Sleep(retryDelay); continue; } std::stringstream jsonbuffer; jsonbuffer << in.rdbuf(); in.close(); std::string jsonstr = jsonbuffer.str(); std::string errstr; Json json = Json::parse(jsonstr, errstr); if (!errstr.empty()) { return Json(); } return json; } return Json(); } bool Utils::write_json(std::wstring file, const Json& json) { std::ofstream out(file); if (!out.is_open()) { return false; } out << json.dump(); out.close(); return true; } bool Utils::is_valid_json(std::wstring file, int maxRetries, int retryDelay) { int attempts = 0; while (attempts < maxRetries) { std::ifstream in(file); if (!in.is_open()) { if (++attempts == maxRetries) { return false; } Sleep(retryDelay); continue; } std::stringstream jsonbuffer; jsonbuffer << in.rdbuf(); in.close(); std::string jsonstr = jsonbuffer.str(); std::string errstr; Json json = Json::parse(jsonstr, errstr); return errstr.empty(); } return false; } // Serialize json values to raw array of bytes Json::object serialize_json(const Json::object& input_json, std::vector ignoredTags) { Json::object output_json; for (auto& kv : input_json) { const std::string& key = kv.first; const Json& val = kv.second; // Any tags to ignore that are intended to be readable if (std::find(ignoredTags.begin(), ignoredTags.end(), key) != ignoredTags.end()) { output_json.emplace(key, val); continue; } const std::string& str = val.string_value(); std::vector bytes; for (unsigned char c : str) { bytes.push_back(static_cast(c)); } output_json.emplace(key, bytes); } return output_json; } Json::object deserialize_json(const Json::object& input_json) { Json::object output_json; for (auto& kv : input_json) { const std::string& key = kv.first; const Json& val = kv.second; // Any values that are not bytes // This requires a json that only uses nested arrays that contain raw bytes if (!val.is_array()) { output_json.emplace(key, val); continue; } std::string str; for (const Json& byte_json : val.array_items()) { // Convert each Json value back to string character str.push_back(static_cast(byte_json.int_value())); } output_json.emplace(key, str); } return output_json; } std::string Utils::string_wide_to_narrow(std::wstring wstr) { if (wstr.empty()) return {}; int sizeNeeded = WideCharToMultiByte( CP_UTF8, 0, wstr.data(), (int)wstr.size(), nullptr, 0, nullptr, nullptr ); std::string result(sizeNeeded, 0); WideCharToMultiByte( CP_UTF8, 0, wstr.data(), (int)wstr.size(), result.data(), sizeNeeded, nullptr, nullptr ); return result; } // Using an child process to get output from a command -- primarily any output from an outside executable // https://stackoverflow.com/a/35658917 std::wstring Utils::run_command(const std::wstring& command, std::string& streamedStr, PROCESS_INFORMATION& processHandle) { HANDLE hPipeRead, hPipeWrite; SECURITY_ATTRIBUTES saAttr = { sizeof(SECURITY_ATTRIBUTES) }; saAttr.bInheritHandle = TRUE; // Pipe handles are inherited by child process. saAttr.lpSecurityDescriptor = NULL; // Create a pipe to get results from child's stdout. if (!CreatePipe(&hPipeRead, &hPipeWrite, &saAttr, 0)) { return L""; } STARTUPINFOW si = { sizeof(STARTUPINFOW) }; si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES; si.hStdOutput = hPipeWrite; si.hStdError = hPipeWrite; si.wShowWindow = SW_HIDE; BOOL fSuccess = CreateProcessW(NULL, (LPWSTR)command.c_str(), NULL, NULL, TRUE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &processHandle); if (!fSuccess) { CloseHandle(hPipeWrite); CloseHandle(hPipeRead); return L""; } bool bProcessEnded = false; for (; !bProcessEnded;) { // Give some timeslice (50 ms), so we won't waste 100% CPU. bProcessEnded = WaitForSingleObject(processHandle.hProcess, 50) == WAIT_OBJECT_0; // Even if process exited - we continue reading, if // there is some data available over pipe. for (;;) { char buf[1024]; DWORD dwRead = 0; DWORD dwAvail = 0; if (!::PeekNamedPipe(hPipeRead, NULL, 0, NULL, &dwAvail, NULL)) break; if (!dwAvail) // No data available, return break; if (!::ReadFile(hPipeRead, buf, min(sizeof(buf) - 1, dwAvail), &dwRead, NULL) || !dwRead) // Error, the child process might ended break; buf[dwRead] = 0; streamedStr += buf; } } CloseHandle(hPipeWrite); CloseHandle(hPipeRead); if (processHandle.hProcess != NULL && processHandle.hThread != NULL && processHandle.hProcess != INVALID_HANDLE_VALUE && processHandle.hThread != INVALID_HANDLE_VALUE) { CloseHandle(processHandle.hProcess); CloseHandle(processHandle.hThread); } // Convert the result to std::wstring std::wstring wstrResult(streamedStr.begin(), streamedStr.end()); return wstrResult; } bool Utils::write_to_stdin(const std::string& serial_select_command, const std::wstring& exe_path, const std::string& command, std::vector* serials) // Optionally return serials from responses. { // Create pipe for stdin of child process. HANDLE stdin_read; HANDLE stdin_write; SECURITY_ATTRIBUTES security_att; security_att.nLength = sizeof(SECURITY_ATTRIBUTES); security_att.bInheritHandle = TRUE; security_att.lpSecurityDescriptor = NULL; if (!CreatePipe(&stdin_read, &stdin_write, &security_att, 0) || !SetHandleInformation(stdin_write, HANDLE_FLAG_INHERIT, 0)) { return false; } // Create pipe for stdout of child process. HANDLE stdout_read = NULL; HANDLE stdout_write = NULL; if (!CreatePipe(&stdout_read, &stdout_write, &security_att, 0) || !SetHandleInformation(stdin_write, HANDLE_FLAG_INHERIT, 0)) { return false; } // Open up process and run command. STARTUPINFO si; PROCESS_INFORMATION pi; ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); si.hStdInput = stdin_read; si.hStdOutput = stdout_write; si.dwFlags |= STARTF_USESTDHANDLES; ZeroMemory(&pi, sizeof(pi)); // Create lighthouse child process. if (!CreateProcess(exe_path.c_str(), NULL, NULL, NULL, TRUE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) { CloseHandle(stdout_write); CloseHandle(stdout_read); CloseHandle(stdin_write); CloseHandle(stdin_read); return false; } // If we specified a serial, make sure we switch to that. DWORD written_bytes = 0; if (serial_select_command.length() > 0) { if (!WriteFile(stdin_write, serial_select_command.c_str(), (DWORD)serial_select_command.size(), &written_bytes, NULL)) { CloseHandle(stdout_write); CloseHandle(stdout_read); CloseHandle(stdin_write); CloseHandle(stdin_read); TerminateProcess(pi.hProcess, 0); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); return false; } } // Write to stdin to receive config file. if (!WriteFile(stdin_write, command.c_str(), (DWORD)command.size(), &written_bytes, NULL)) { CloseHandle(stdout_write); CloseHandle(stdout_read); CloseHandle(stdin_write); CloseHandle(stdin_read); TerminateProcess(pi.hProcess, 0); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); return false; } // Clear out existing stdin buffer. CHAR stdout_buf[4096]; DWORD read_bytes = 0; if (!ReadFile(stdout_read, stdout_buf, 4096, &read_bytes, NULL)) { CloseHandle(stdout_write); CloseHandle(stdout_read); CloseHandle(stdin_write); CloseHandle(stdin_read); TerminateProcess(pi.hProcess, 0); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); return false; } // Read all of the serials of USB attached, so we query the HMD only. if (serials != NULL) { // Request all USB attached serials. std::string serial_command = "serial\r\n"; if (!WriteFile(stdin_write, serial_command.c_str(), (DWORD)serial_command.size(), &written_bytes, NULL)) { CloseHandle(stdout_write); CloseHandle(stdout_read); CloseHandle(stdin_write); CloseHandle(stdin_read); TerminateProcess(pi.hProcess, 0); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); return false; } if (!ReadFile(stdout_read, stdout_buf, 4096, &read_bytes, NULL)) { CloseHandle(stdout_write); CloseHandle(stdout_read); CloseHandle(stdin_write); CloseHandle(stdin_read); TerminateProcess(pi.hProcess, 0); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); return false; } if (read_bytes < 4096) { stdout_buf[read_bytes] = '\0'; } std::string response = stdout_buf; size_t pos = response.find("LHR", 0); while (pos != std::string::npos) { serials->push_back(response.substr(pos, 12)); pos = response.find("LHR", pos + 1); } // Remove duplicates of the serial found in the stdout output. std::sort(serials->begin(), serials->end()); serials->erase(std::unique(serials->begin(), serials->end()), serials->end()); } // Wait to exit. std::string exit = "exit\r\n"; if (!WriteFile(stdin_write, exit.c_str(), (DWORD)exit.size(), &written_bytes, NULL)) { CloseHandle(stdout_write); CloseHandle(stdout_read); CloseHandle(stdin_write); CloseHandle(stdin_read); TerminateProcess(pi.hProcess, 0); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); return false; } DWORD exit_code; while (GetExitCodeProcess(pi.hProcess, &exit_code) && exit_code == STILL_ACTIVE) { Sleep(1); // Don't clog the core. } CloseHandle(stdout_write); CloseHandle(stdout_read); CloseHandle(stdin_write); CloseHandle(stdin_read); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); return true; } // Performs a full read, modify, write of the configuration memory region on the // ATSAMG55 microcontroller. On the first time valid config memory is read, the // values are saved to a json file on disk. bool Utils::write_mem_config(HidHandler& hid, const std::wstring& config_mem_json_file, std::vector new_values, bool ignore_serial_check) { ui.fetching_config.store(true); int reread_count = 0; bool config_verified = false; int hid_read_timeout = 1000; // milliseconds // Try to get a valid config 3 times. If it is not valid, fail out // of this function. while ((!config_verified) && (reread_count < 3)) { // Read current config for (uint32_t b = 0; b < 16; ++b) { int hid_timeout_counter = 0; hid.command_read_config(b); while (hid.waiting_on_block.load() != 0xFF) { Sleep(1); if (hid_timeout_counter >= hid_read_timeout) break; hid_timeout_counter++; } } if (hid.verify_config()) { // Now we know the memory data is valid. Let's check the // serial number and ensure it is a proper value hid.parse_config(); // reads values from raw bytes and places them in vector "hid.config" if (ignore_serial_check) { config_verified = true; } else { // Check for a empty serial for (const auto& [key, value] : hid.config) { if (key == 8 && !value.empty()) { config_verified = true; } } } } reread_count++; } if (!config_verified) { hid.log << "Failed to verify memory configuration before write: Aborting" << std::endl; ui.fetching_config.store(false); return false; } if (!config_mem_json_file.empty()) { // If we got here, then we know the config is fully valid // No backup file? Save it now. //// TODO: Load the saved config from file when failed to load std::wstring headset_serial(hid_handler.hmd_serial.begin(), hid_handler.hmd_serial.end()); std::wstring backup_file_name_w = config_mem_json_file + L"_" + headset_serial + L".json"; if (!std::filesystem::exists(backup_file_name_w.c_str())) { // Let's save the current values to a JSON file Json::object jsonvals; for (auto& val : hid.config) { // create a json entry with the string version of the tag number jsonvals.emplace(std::to_string(std::get<0>(val)), std::get<1>(val)); } Json config_mem = serialize_json(jsonvals, Config_Tags::readableTags); std::ofstream config_out(backup_file_name_w.c_str()); if (config_out.is_open()) { config_out << config_mem.dump(); config_out.flush(); config_out.close(); } } } // Safe to make changes! hid.save_config(new_values); ui.fetching_config.store(false); return true; } // Override to perform the RGB, fan, and brightness updates only. // This is called during the "Save" button event bool Utils::update_config(HidHandler& hid, UI_Run_State& rs) { if (ui.finding_fan_gen.load()) { return false; } unsigned short converted_brightness; if (rs.overdrive) { converted_brightness = (unsigned short)((((float)(rs.overdrive_brightness - 100) * 0.01f) * 553) + 266); } else { converted_brightness = (unsigned short)((((float)rs.brightness * 0.01f) * 216) + 50); } // Create a vector and send it to the default update_config function std::vector new_entries; std::string rgb_val; rgb_val += (unsigned char)(rs.color[0] * 255.0f); rgb_val += (unsigned char)(rs.color[1] * 255.0f); rgb_val += (unsigned char)(rs.color[2] * 255.0f); int fan_gen = utils.get_fan_generation(hid_handler.hmd_serial, rs); std::string fan_val; fan_val += rs.fan_speed / (fan_gen == 2 ? 2 : 1); std::string bright_val; bright_val += (converted_brightness & 0x00FF); bright_val += ((converted_brightness & 0xFF00) >> 8); std::string prox_val; prox_val += (rs.prox_offset & 0x00FF); prox_val += ((rs.prox_offset & 0xFF00) >> 8); std::string vxr_val; vxr_val += (rs.vxr_auto_off ? 0x1 : 0); new_entries.push_back(std::make_tuple(Config_Tags::RGB_Color, rgb_val)); new_entries.push_back(std::make_tuple(Config_Tags::Fan_Speed, fan_val)); new_entries.push_back(std::make_tuple(Config_Tags::Display_Brightness, bright_val)); new_entries.push_back(std::make_tuple(Config_Tags::Prox_Offset, prox_val)); new_entries.push_back(std::make_tuple(Config_Tags::VXR_Sleep_Enable, vxr_val)); return write_mem_config(hid, rs.config_memory_backup_path, new_entries); } // Writes values from a memory configuration backup file bool Utils::restore_mem_config(HidHandler& hid, const std::wstring& config_mem_json_file) { Json backup_json = utils.read_json(config_mem_json_file); if (!backup_json.is_null()) { Json deserialized_json = deserialize_json(backup_json.object_items()); // Convert values from backup json to the tuple std::vector new_values; new_values.reserve(deserialized_json.object_items().size()); for (auto& key_v : deserialized_json.object_items()) { unsigned char id = static_cast(std::stoi(key_v.first)); std::string val = key_v.second.string_value(); new_values.emplace_back(id, val); } return write_mem_config(hid, L"", new_values, true); } return false; } void Utils::mic_volume_fix(UI_Run_State& rs) { // Find the Beyond microphone device via EnumAudioEndpoints IMMDeviceEnumerator* deviceEnumerator = NULL; HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_INPROC_SERVER, __uuidof(IMMDeviceEnumerator), (LPVOID*)&deviceEnumerator); IMMDeviceCollection* deviceCollection = NULL; hr = deviceEnumerator->EnumAudioEndpoints(eCapture, DEVICE_STATE_ACTIVE, &deviceCollection); if (FAILED(hr)) { std::cerr << "Failed to enumerate audio endpoints." << std::endl; deviceEnumerator->Release(); return; } UINT deviceCount; deviceCollection->GetCount(&deviceCount); IMMDevice* targetDevice = NULL; for (UINT i = 0; i < deviceCount; ++i) { IMMDevice* device = NULL; hr = deviceCollection->Item(i, &device); if (SUCCEEDED(hr)) { IPropertyStore* propertyStore; hr = device->OpenPropertyStore(STGM_READ, &propertyStore); if (SUCCEEDED(hr)) { PROPVARIANT friendlyName; PropVariantInit(&friendlyName); hr = propertyStore->GetValue(PKEY_Device_FriendlyName, &friendlyName); if (SUCCEEDED(hr)) { if (wcsstr(friendlyName.pwszVal, L"Beyond") != NULL) { targetDevice = device; targetDevice->AddRef(); // Increase reference count since we'll be using it outside the loop PropVariantClear(&friendlyName); propertyStore->Release(); break; } PropVariantClear(&friendlyName); } propertyStore->Release(); } device->Release(); } } if (targetDevice == NULL) { // Beyond is attached, but we failed to find microphone; // this will only happen in much older firmwares, e.g. <= 0.2.4 // in this case, skip setting the volume deviceEnumerator->Release(); deviceCollection->Release(); return; } deviceCollection->Release(); deviceEnumerator->Release(); IAudioEndpointVolume* endpointVolume = NULL; hr = targetDevice->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_INPROC_SERVER, NULL, (LPVOID*)&endpointVolume); targetDevice->Release(); auto fw_info = split_headset_firmware(); // Set mic volume to 54% for older firmwares, else 90% if (std::get<0>(fw_info) == 0 && std::get<1>(fw_info) == 2 && std::get<2>(fw_info) <= 12) { endpointVolume->SetMasterVolumeLevelScalar(0.54f, NULL); } else { endpointVolume->SetMasterVolumeLevelScalar(0.9f, NULL); } } bool Utils::install_et_driver(UI_Run_State& rs) { if (rs.valid_path) { std::filesystem::path driverFolder = std::filesystem::absolute(std::filesystem::path("eyetracking\\ETDriver")); std::wstring vrpathreg = rs.steamvr_path + L"\\bin\\win64\\vrpathreg.exe"; bool tool_valid = std::filesystem::exists(vrpathreg) && std::filesystem::is_regular_file(vrpathreg); bool driver_valid = std::filesystem::exists(driverFolder) && std::filesystem::is_directory(driverFolder); if (tool_valid && driver_valid) { // Issue the command to add the external driver path with vrpathreg PROCESS_INFORMATION processHandle; std::string cmdStr; std::wstring result = run_command(vrpathreg + L" adddriver \"" + driverFolder.c_str() + L"\"", cmdStr, processHandle); // vrpathreg returned output if (result.empty()) { update_et_driver_status(rs); return true; } } else { MessageBox(glfwGetWin32Window(ui.window), !tool_valid ? LWSTR(StringID::ERROR_DRIVER_INSTALL_FAIL_TOOL).c_str() : LWSTR(StringID::ERROR_ET_INSTALL_FAIL_DRIVER_FOLDER).c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); } } else { MessageBox(glfwGetWin32Window(ui.window), LWSTR(StringID::ERROR_DRIVER_INSTALL_FAIL_STEAMVR_FOLDER).c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); } return false; } bool Utils::install_bridge_driver(UI_Run_State& rs) { if (rs.valid_path) { std::filesystem::path driverFolder = std::filesystem::absolute(std::filesystem::path("steamvr\\BeyondSteamVR")); std::wstring vrpathreg = rs.steamvr_path + L"\\bin\\win64\\vrpathreg.exe"; bool tool_valid = std::filesystem::exists(vrpathreg) && std::filesystem::is_regular_file(vrpathreg); bool driver_valid = std::filesystem::exists(driverFolder) && std::filesystem::is_directory(driverFolder); if (tool_valid && driver_valid) { // Issue the command to add the external driver path with vrpathreg PROCESS_INFORMATION processHandle; std::string cmdStr; std::wstring result = run_command(vrpathreg + L" adddriver \"" + driverFolder.c_str() + L"\"", cmdStr, processHandle); // vrpathreg returned output if (result.empty()) { update_bridge_driver_status(rs); return true; } } else { MessageBox(glfwGetWin32Window(ui.window), !tool_valid ? LWSTR(StringID::ERROR_DRIVER_INSTALL_FAIL_TOOL).c_str() : LWSTR(StringID::ERROR_BRIDGE_INSTALL_FAIL_DRIVER_FOLDER).c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); } } else { MessageBox(glfwGetWin32Window(ui.window), LWSTR(StringID::ERROR_DRIVER_INSTALL_FAIL_STEAMVR_FOLDER).c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); } return false; } void Utils::update_et_driver_status(UI_Run_State& rs) { if (rs.valid_path) { std::wstring vrpathreg = rs.steamvr_path + L"\\bin\\win64\\vrpathreg.exe"; bool file_valid = std::filesystem::exists(vrpathreg) && std::filesystem::is_regular_file(vrpathreg); if (file_valid) { PROCESS_INFORMATION processHandle; std::string cmdStr; std::wstring result = run_command(vrpathreg + L" show", cmdStr, processHandle); // Check for keywords to validate the driver path exists if (!result.empty() && result.find(L"ETDriver") != std::wstring::npos && result.find(L"BeyondEyetracking") != std::wstring::npos) { rs.installed_et_driver = true; } else { rs.installed_et_driver = false; } } } } void Utils::update_bridge_driver_status(UI_Run_State& rs) { if (rs.valid_path) { std::wstring vrpathreg = rs.steamvr_path + L"\\bin\\win64\\vrpathreg.exe"; bool file_valid = std::filesystem::exists(vrpathreg) && std::filesystem::is_regular_file(vrpathreg); if (file_valid) { PROCESS_INFORMATION processHandle; std::string cmdStr; std::wstring result = run_command(vrpathreg + L" show", cmdStr, processHandle); // Check for keywords to validate the driver path exists if (!result.empty() && result.find(L"steamvr") != std::wstring::npos && result.find(L"BeyondSteamVR") != std::wstring::npos) { rs.installed_bridge_driver = true; } else { rs.installed_bridge_driver = false; } } } } HANDLE Utils::get_et_handle() { HANDLE hMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, L"com_bigscreen_et_process"); return hMutex; } void Utils::launch_enrollment(UI_Run_State& rs) { bool had_runtime_enabled = rs.et_enabled; std::wstring token = std::wstring(rs.et_user_token.begin(), rs.et_user_token.end()); std::wstring arguments = L" --token " + token + L" --xr-mode on"; std::filesystem::path enroll_exe = std::filesystem::absolute(std::filesystem::path("eyetracking\\ETEnroll\\ETEnroll.exe")); bool file_valid = std::filesystem::exists(enroll_exe) && std::filesystem::is_regular_file(enroll_exe); if (file_valid) { if (had_runtime_enabled) { rs.et_enabled = false; } HANDLE existing_handle = get_et_handle(); if (existing_handle) { // Already running CloseHandle(existing_handle); MessageBox(glfwGetWin32Window(ui.window), LWSTR(StringID::ERROR_ET_ALREADY_RUNNING).c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); return; } STARTUPINFO si = { sizeof(si) }; PROCESS_INFORMATION pi = {}; BOOL success = CreateProcessW(enroll_exe.c_str(), (LPWSTR)arguments.c_str(), NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi); if (!success) { hid_handler.log << "Failed to launch enrollment: " << GetLastError() << std::endl; return; } hid_handler.log << "Enrollment launched successfully" << std::endl; // HACK HACK HACK // Start model update loop to periodically fetch from the server // This assumes the user actually completes enrollment rs.model_lifetime_check_timer = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); rs.expecting_new_model = true; // Wait for the process to exit asynchronously std::thread([&rs, had_runtime_enabled, pi]() { WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); rs.active_models_filter = Models_Filter::RemoteModels; if (had_runtime_enabled) { rs.et_enabled = true; } }).detach(); } else { MessageBox(glfwGetWin32Window(ui.window), LWSTR(StringID::ERROR_ET_LAUNCH).c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); } } bool Utils::write_steamvr_settings(UI_Run_State& rs, SteamVR_Flagged_Setting setting_details) { if (!rs.steamvr_settings_file.empty()) { std::wstring steamvr_settings = rs.steamvr_settings_file; bool file_valid = std::filesystem::exists(steamvr_settings.c_str()) && std::filesystem::is_regular_file(steamvr_settings.c_str()); if (file_valid) { Json steamvr_json = read_json(steamvr_settings, 1); if (!steamvr_json.is_null()) { std::string errstr; Json target_Value = Json::parse(setting_details.steamvr_flag_value, errstr); if (errstr.empty()) { // Get mutable objects Json::object new_settings = steamvr_json.object_items(); Json::object category_obj; auto category_it = new_settings.find(setting_details.json_category); // Use any existing category if (category_it != new_settings.end() && category_it->second.is_object()) { category_obj = category_it->second.object_items(); } // Create an empty category if it does not exist else { category_obj = Json::object(); } // Modify the value inside the category object if (target_Value.is_bool()) { category_obj[setting_details.steamvr_setting] = !target_Value.bool_value(); // Write the opposite of the target boolean value } else if (target_Value.is_number()) { category_obj[setting_details.steamvr_setting] = target_Value.number_value(); } else if (target_Value.is_string()) { category_obj[setting_details.steamvr_setting] = target_Value.string_value(); } // Reassign the modified category back to the main settings object new_settings[setting_details.json_category] = category_obj; return write_json(rs.steamvr_settings_file, (Json)new_settings); } } MessageBox(glfwGetWin32Window(ui.window), LWSTR(StringID::ERROR_READING_STEAMVR_SETTINGS).c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); return false; } } MessageBox(glfwGetWin32Window(ui.window), LWSTR(StringID::ERROR_FINDING_STEAMVR_SETTINGS).c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); return false; } std::wstring Utils::get_steam_config_folder(UI_Run_State& rs) { // Use saved path if (!rs.steam_config_path.empty()) { return rs.steam_config_path; } // Get the steamvr location from vrpathreg if (rs.valid_path) { std::wstring vrpathreg = rs.steamvr_path + L"\\bin\\win64\\vrpathreg.exe"; bool file_valid = std::filesystem::exists(vrpathreg.c_str()) && std::filesystem::is_regular_file(vrpathreg.c_str()); if (file_valid) { // This grabs the location from SteamVR registered paths PROCESS_INFORMATION processHandle; std::string cmdStr; std::wstring paths = utils.run_command(vrpathreg + L" show", cmdStr, processHandle); if (!paths.empty()) { // Get only the config path std::wstring prefix = L"Config path = "; size_t pos = paths.find(prefix); if (pos != std::wstring::npos) { size_t start = pos + prefix.length(); size_t end = paths.find(L'\n', start); std::wstring result = paths.substr(start, (end == std::wstring::npos) ? end : end - start); // Remove any trailing '\r' if (!result.empty() && result.back() == L'\r') { result.pop_back(); } // Save the result so that we don't need to use vrpathreg all the time rs.steam_config_path = result; utils.write_settings_file(rs); return result; } } } } return std::wstring(); } void Utils::set_steamvr_color_gain(UI_Run_State& rs, std::wstring prop, char* next) { std::thread gain_set([&](std::wstring prop, char* next) { std::wstring gain; // Parse to wide string from double try { std::string parsed(next); parsed = std::to_string(std::stod(parsed)); gain = std::wstring(parsed.begin(), parsed.end()); } catch (...) { return; } if (rs.valid_path) { std::wstring vrcmd = rs.steamvr_path + L"\\bin\\win64\\vrcmd.exe"; bool file_valid = std::filesystem::exists(vrcmd.c_str()) && std::filesystem::is_regular_file(vrcmd.c_str()); if (file_valid) { // Pass the settings change through vrcmd.exe PROCESS_INFORMATION processHandle; std::string out; std::wstring cmd = vrcmd + L" --set-settings-float " + L"\"" + prop + L"\" " + L"\"" + gain + L"\""; utils.run_command(cmd, out, processHandle); if (out.find("VRInitError") != std::string::npos) { MessageBox(glfwGetWin32Window(ui.window), LWSTR(StringID::ERROR_COLOR_TINT_RUNNING).c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); } } else { MessageBox(glfwGetWin32Window(ui.window), LWSTR(StringID::ERROR_COLOR_TINT_TOOL).c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); } } }, prop, next); gain_set.detach(); } Json Utils::get_steamvr_settings(UI_Run_State& rs) { std::wstring streamvr_config = get_steam_config_folder(rs); if (streamvr_config.empty()) { return Json(); } std::wstring steamvr_settings = streamvr_config + L"\\steamvr.vrsettings"; bool file_valid = std::filesystem::exists(steamvr_settings.c_str()) && std::filesystem::is_regular_file(steamvr_settings.c_str()); if (file_valid) { return utils.read_json(steamvr_settings); } return Json(); } void Utils::find_flagged_settings(UI_Run_State& rs) { if (steamvr_check.joinable()) { steamvr_check.join(); } steamvr_check = std::thread([&]() { rs.steamvr_settings_file.clear(); if (rs.valid_path) { std::wstring streamvr_config = get_steam_config_folder(rs); if (streamvr_config.empty()) { return; } std::wstring steamvr_settings = streamvr_config + L"\\steamvr.vrsettings"; bool file_valid = std::filesystem::exists(steamvr_settings.c_str()) && std::filesystem::is_regular_file(steamvr_settings.c_str()); if (file_valid) { rs.steamvr_settings_file = steamvr_settings; Json steamvr_json = utils.read_json(steamvr_settings); if (!steamvr_json.is_null()) { rs.flagged_settings.clear(); for (const std::pair& entry : ui.warned_settings) { const SteamVR_Warned_Settings& setting_type = entry.first; const SteamVR_Flagged_Setting& setting_details = entry.second; bool flagged = false; // If the warning has been ignored, don't attempt to flag it if (std::find(rs.ignored_steamvr_warnings.begin(), rs.ignored_steamvr_warnings.end(), static_cast(setting_type)) != rs.ignored_steamvr_warnings.end()) { continue; } if (steamvr_json[setting_details.json_category].is_null()) { // An absent setting category is missing which means the setting may be enabled by default if (setting_details.check_absence) { flagged = true; } else { continue; } } if (!flagged && !steamvr_json[setting_details.json_category][setting_details.steamvr_setting].is_null()) { std::string errstr; Json target_value = Json::parse(setting_details.steamvr_flag_value, errstr); std::string errstr2; Json found_value = Json::parse(steamvr_json[setting_details.json_category][setting_details.steamvr_setting].dump(), errstr2); if (errstr.empty() && errstr2.empty()) { // Found boolean or string value is equal to target and can be flagged // Found number value is greater (or less) than target and can be flagged if ((target_value.is_bool() && found_value.is_bool() && found_value.bool_value() == target_value.bool_value()) || (target_value.is_number() && found_value.is_number() && ((setting_details.number_value_greater && found_value.number_value() > target_value.number_value()) || (!setting_details.number_value_greater && found_value.number_value() < target_value.number_value()))) || (target_value.is_string() && found_value.is_string() && found_value.string_value() == found_value.string_value()) ) { flagged = true; } } } // An absent setting is missing which means it may be enabled by default else if (setting_details.check_absence) { flagged = true; } // Add the setting type to surface to the user if (flagged && std::find(rs.flagged_settings.begin(), rs.flagged_settings.end(), setting_type) == rs.flagged_settings.end()) { rs.flagged_settings.push_back(setting_type); } } return; } } } }); } // Migrate older settings from the client one time void Utils::grandfather_et_client(UI_Run_State& rs) { std::filesystem::path old_settings_path = "eyetracking\\ETClient\\eyetracking_settings.json"; if (std::filesystem::exists(old_settings_path)) { hid_handler.log << "Porting over old eyetracking settings" << std::endl; std::ifstream in(old_settings_path); if (in.is_open()) { std::stringstream settings_buffer; settings_buffer << in.rdbuf(); in.close(); std::string settings_string = settings_buffer.str(); std::string error; Json json = Json::parse(settings_string.c_str(), error); if (error.length() > 0) { return; } else { auto models_json = json["named_models"]; if (models_json.is_object()) { for (auto& pair : models_json.object_items()) { rs.et_labeled_models[pair.first] = pair.second.string_value(); } } try { rs.et_smoothing_intensity = json["smoothing_intensity"].is_string() ? std::stof(json["smoothing_intensity"].string_value()) : 0.8f; } catch (...) {} rs.et_enabled = json["camera_enabled"].is_bool() ? json["camera_enabled"].bool_value() : false; rs.et_publish_memmap = json["submit_to_xr"].is_bool() ? json["submit_to_xr"].bool_value() : true; rs.et_publish_vrcft = json["submit_to_vrcft"].is_bool() ? json["submit_to_vrcft"].bool_value() : false; rs.et_publish_vrchat = json["submit_to_vrchat_osc"].is_bool() ? json["submit_to_vrchat_osc"].bool_value() : false; rs.et_smoothing_enabled = json["smoothing_enabled"].is_bool() ? json["smoothing_enabled"].bool_value() : true; rs.et_left_eye_enabled = json["left_eye_enabled"].is_bool() ? json["left_eye_enabled"].bool_value() : true; rs.et_right_eye_enabled = json["right_eye_enabled"].is_bool() ? json["right_eye_enabled"].bool_value() : true; rs.enabled_alignment_helper = json["enabled_alignment_helper"].is_bool() ? json["enabled_alignment_helper"].bool_value() : false; rs.seen_alignment_guide = json["seen_alignment_guide"].is_bool() ? json["seen_alignment_guide"].bool_value() : false; rs.dfr_settings_prompted = json["dfr_settings_prompted"].is_bool() ? json["dfr_settings_prompted"].bool_value() : false; rs.et_osc_ip = json["osc_ip"].is_string() ? json["osc_ip"].string_value() : "127.0.0.1"; rs.et_osc_port = json["osc_port"].is_number() ? json["osc_port"].int_value() : 9000; rs.et_user_token = json["user_testing_token"].is_string() ? json["user_testing_token"].string_value() : ""; rs.et_selected_model = json["selected_model"].is_string() ? json["selected_model"].string_value() : ""; rs.quad_horizontal_focus_section = json["quad_horizontal_focus_section"].is_string() ? json["quad_horizontal_focus_section"].string_value() : UIConstants.horizontal_focus_default; rs.quad_vertical_focus_section = json["quad_vertical_focus_section"].is_string() ? json["quad_vertical_focus_section"].string_value() : UIConstants.vertical_focus_default; rs.quad_peripheral_multiplier = json["quad_peripheral_multiplier"].is_string() ? json["quad_peripheral_multiplier"].string_value() : UIConstants.peripheral_default; rs.quad_focus_multiplier = json["quad_focus_multiplier"].is_string() ? json["quad_focus_multiplier"].string_value() : UIConstants.focus_default; rs.quad_transition_thickness = json["quad_transition_thickness"].is_string() ? json["quad_transition_thickness"].string_value() : UIConstants.transition_thickness_default; rs.quad_debug_gaze = json["quad_debug_gaze"].is_number() ? json["quad_debug_gaze"].int_value() : 0; rs.et_blink_mode = json["blink_mode"].is_number() ? json["blink_mode"].int_value() : 0; rs.et_backported_settings = true; utils.write_settings_file(rs); } } } } bool Utils::read_settings_file(UI_Run_State& rs) { std::ifstream in(rs.settings_path); if (in.is_open()) { std::stringstream settings_buffer; settings_buffer << in.rdbuf(); in.close(); std::string settings_string = settings_buffer.str(); std::string error; Json json = Json::parse(settings_string.c_str(), error); if (error.length() > 0) { return false; } else { std::wstring_convert> converter; // Grandfather the old lighthouse setting if (!json["lighthouse_path"].is_null() && json["lighthouse_path"].is_string() && json["lighthouse_path"].string_value().find("lighthouse") != std::string::npos) { std::string lighthouse_path = json["lighthouse_path"].string_value() + "\\..\\.."; std::wstring wlighthouse_path = converter.from_bytes(lighthouse_path); // Use canonicolize to make it simpler (remove the \..) WCHAR fixedpath[MAX_PATH]; if (SUCCEEDED(PathCchCanonicalize(fixedpath, MAX_PATH, wlighthouse_path.c_str()))) { rs.steamvr_path = fixedpath; } } // Use "steamvr_path" json value if "lighthouse_path" doesn't exist else { std::string steam_path = json["steamvr_path"].string_value(); rs.steamvr_path = converter.from_bytes(steam_path); } std::string steam_config_path = json["steam_config_path"].string_value(); rs.steam_config_path = converter.from_bytes(steam_config_path); std::string rr = json["refresh_rate"].string_value(); for (auto& i : rs.refresh_rates) { if (i.second == rr) { rs.current_refresh_rate = i.first; } } rs.color[0] = (float)json["color_r"].number_value(); rs.color[1] = (float)json["color_g"].number_value(); rs.color[2] = (float)json["color_b"].number_value(); rs.brightness = std::clamp(json["brightness"].int_value(), UIConstants.brightness_min, UIConstants.brightness_max); rs.overdrive_brightness = std::clamp(json["overdrive_brightness"].int_value(), UIConstants.overdrive_brightness_min, UIConstants.overdrive_brightness_max); rs.fan_speed = std::clamp(json["fan_speed"].int_value(), UIConstants.fan_speed_min, UIConstants.fan_speed_max); rs.prox_offset = std::clamp(json["prox_offset"].int_value(), UIConstants.prox_min, UIConstants.prox_max); int ipd = std::clamp(json["ipd_mm"].int_value(), UIConstants.ipd_min, UIConstants.ipd_max); rs.applied_ipd = ipd > 0 ? ipd : 64; rs.adjusted_ipd = rs.applied_ipd; rs.overdrive = json["overdrive"].bool_value(); rs.vxr_auto_off = json["sleep_enabled"].bool_value(); rs.active_locale = (Locales)std::clamp(json["active_locale"].int_value(), 0, (int)Locales::Max); if (json["hmd_history"].is_array()) { for (const auto& element : json["hmd_history"].array_items()) { if (element.is_string()) { rs.hmd_serial_history.push_back(element.string_value()); } } } if (json["hmd_fan_gen"].is_object()) { for (const auto& [key, value] : json["hmd_fan_gen"].object_items()) { if (value.is_number()) { rs.hmd_fan_gen[key] = value.int_value(); } } } if (json["ignored_warnings"].is_array()) { for (const auto& element : json["ignored_warnings"].array_items()) { if (element.is_number()) { rs.ignored_steamvr_warnings.push_back(element.int_value()); } } } if (rs.overdrive) { rs.slider_brightness = rs.overdrive_brightness; } else { rs.slider_brightness = rs.brightness; } // Eyetracking runtime settings if (!json["eyetracking"].is_null()) { Json::object et_prefs = json["eyetracking"].object_items(); auto labeled_models_json = et_prefs["et_labeled_models"]; if (labeled_models_json.is_object()) { for (auto& pair : labeled_models_json.object_items()) { rs.et_labeled_models[pair.first] = pair.second.string_value(); } } auto removed_models_json = et_prefs["et_removed_models"]; if (removed_models_json.is_array()) { for (const auto& element : et_prefs["et_removed_models"].array_items()) { if (element.is_string()) { rs.et_removed_models.push_back(element.string_value()); } } } auto selected_apps_json = et_prefs["et_selected_apps"]; if (selected_apps_json.is_array()) { for (const auto& element : selected_apps_json.array_items()) { if (!element.is_object()) { continue; } std::string processName = element["processName"].is_string() ? element["processName"].string_value() : ""; if (processName.empty()) { continue; } SteamVRWatch::RunningApp app{}; app.processName = converter.from_bytes(processName); app.windowTitle = element["windowTitle"].is_string() ? converter.from_bytes(element["windowTitle"].string_value()) : L""; rs.et_selected_apps.push_back(app); } } rs.et_enabled = et_prefs["et_enabled"].is_bool() ? et_prefs["et_enabled"].bool_value() : false; rs.et_publish_memmap = et_prefs["et_publish_memmap"].is_bool() ? et_prefs["et_publish_memmap"].bool_value() : true; rs.et_publish_vrcft = et_prefs["et_publish_vrcft"].is_bool() ? et_prefs["et_publish_vrcft"].bool_value() : false; rs.et_publish_vrchat = et_prefs["et_publish_vrchat"].is_bool() ? et_prefs["et_publish_vrchat"].bool_value() : false; rs.et_smoothing_enabled = et_prefs["et_smoothing_enabled"].is_bool() ? et_prefs["et_smoothing_enabled"].bool_value() : true; rs.et_smoothing_intensity = et_prefs["et_smoothing_intensity"].is_number() ? static_cast(et_prefs["et_smoothing_intensity"].number_value()) : 0.8f; rs.et_left_eye_enabled = et_prefs["et_left_eye_enabled"].is_bool() ? et_prefs["et_left_eye_enabled"].bool_value() : true; rs.et_right_eye_enabled = et_prefs["et_right_eye_enabled"].is_bool() ? et_prefs["et_right_eye_enabled"].bool_value() : true; rs.enabled_alignment_helper = et_prefs["enabled_alignment_helper"].is_bool() ? et_prefs["enabled_alignment_helper"].bool_value() : false; rs.seen_alignment_guide = et_prefs["seen_alignment_guide"].is_bool() ? et_prefs["seen_alignment_guide"].bool_value() : false; rs.dfr_settings_prompted = et_prefs["dfr_settings_prompted"].is_bool() ? et_prefs["dfr_settings_prompted"].bool_value() : false; rs.et_require_steamvr_presence = et_prefs["et_require_steamvr_presence"].is_bool() ? et_prefs["et_require_steamvr_presence"].bool_value() : true; rs.et_osc_ip = et_prefs["et_osc_ip"].is_string() ? et_prefs["et_osc_ip"].string_value() : "127.0.0.1"; rs.et_osc_port = et_prefs["et_osc_port"].is_number() ? et_prefs["et_osc_port"].int_value() : 9000; rs.et_blink_mode = et_prefs["et_blink_mode"].is_number() ? et_prefs["et_blink_mode"].int_value() : 0; if (rs.et_user_token.empty()) { rs.et_user_token = et_prefs["et_user_token"].is_string() ? et_prefs["et_user_token"].string_value() : ""; } rs.et_selected_model = et_prefs["et_selected_model"].is_string() ? et_prefs["et_selected_model"].string_value() : ""; rs.et_backported_settings = et_prefs["et_backported_settings"].is_bool() ? et_prefs["et_backported_settings"].bool_value() : false; rs.quad_horizontal_focus_section = et_prefs["quad_horizontal_focus_section"].is_string() ? et_prefs["quad_horizontal_focus_section"].string_value() : UIConstants.horizontal_focus_default; rs.quad_vertical_focus_section = et_prefs["quad_vertical_focus_section"].is_string() ? et_prefs["quad_vertical_focus_section"].string_value() : UIConstants.vertical_focus_default; rs.quad_peripheral_multiplier = et_prefs["quad_peripheral_multiplier"].is_string() ? et_prefs["quad_peripheral_multiplier"].string_value() : UIConstants.peripheral_default; rs.quad_focus_multiplier = et_prefs["quad_focus_multiplier"].is_string() ? et_prefs["quad_focus_multiplier"].string_value() : UIConstants.focus_default; rs.quad_transition_thickness = et_prefs["quad_transition_thickness"].is_string() ? et_prefs["quad_transition_thickness"].string_value() : UIConstants.transition_thickness_default; rs.quad_debug_gaze = et_prefs["quad_debug_gaze"].is_number() ? et_prefs["quad_debug_gaze"].int_value() : 0; } if (!rs.et_backported_settings) { grandfather_et_client(rs); } return true; } } else { return false; } } bool Utils::write_settings_file(UI_Run_State& rs) { std::wstring_convert> converter; std::string steamvr_path = converter.to_bytes(rs.steamvr_path); std::string steam_config_path = converter.to_bytes(rs.steam_config_path); Json::object fanGenObj; for (const auto& [key, value] : rs.hmd_fan_gen) { fanGenObj[key] = value; } Json::object labeled_models_json; for (const auto& pair : rs.et_labeled_models) { labeled_models_json[pair.first] = pair.second; } Json::object removed_models_json; Json::array removed_models_array; for (const auto& r_m : rs.et_removed_models) { removed_models_array.emplace_back(r_m); } Json::array selected_apps_array; for (const auto& app : rs.et_selected_apps) { if (app.processName.empty()) { continue; } selected_apps_array.push_back(Json::object { { "processName", converter.to_bytes(app.processName) }, { "windowTitle", converter.to_bytes(app.windowTitle) }, }); } Json eyetracking_settings = Json::object { { "et_enabled", rs.et_enabled }, { "et_publish_memmap", rs.et_publish_memmap }, { "et_publish_vrcft", rs.et_publish_vrcft }, { "et_publish_vrchat", rs.et_publish_vrchat }, { "et_smoothing_enabled", rs.et_smoothing_enabled }, { "et_smoothing_intensity", rs.et_smoothing_intensity }, { "et_left_eye_enabled", rs.et_left_eye_enabled }, { "et_right_eye_enabled", rs.et_right_eye_enabled }, { "enabled_alignment_helper", rs.enabled_alignment_helper }, { "et_require_steamvr_presence", rs.et_require_steamvr_presence }, { "seen_alignment_guide", rs.seen_alignment_guide }, { "dfr_settings_prompted", rs.dfr_settings_prompted }, { "et_selected_model", rs.et_selected_model }, { "et_labeled_models", labeled_models_json }, { "et_removed_models", removed_models_array }, { "et_selected_apps", selected_apps_array }, { "et_osc_ip", rs.et_osc_ip }, { "et_osc_port", rs.et_osc_port }, { "et_user_token", rs.et_user_token }, { "et_blink_mode", rs.et_blink_mode }, { "et_backported_settings", rs.et_backported_settings }, { "quad_horizontal_focus_section", rs.quad_horizontal_focus_section }, { "quad_vertical_focus_section", rs.quad_vertical_focus_section }, { "quad_peripheral_multiplier", rs.quad_peripheral_multiplier }, { "quad_focus_multiplier", rs.quad_focus_multiplier }, { "quad_transition_thickness", rs.quad_transition_thickness }, { "quad_debug_gaze", rs.quad_debug_gaze }, }; Json settings = Json::object { { "active_locale", (int)rs.active_locale }, { "steamvr_path", steamvr_path }, { "steam_config_path", steam_config_path }, { "refresh_rate", rs.refresh_rates.at(rs.current_refresh_rate)}, { "color_r", rs.color[0] }, { "color_g", rs.color[1] }, { "color_b", rs.color[2] }, { "brightness", rs.brightness }, { "overdrive_brightness", rs.overdrive_brightness }, { "fan_speed", rs.fan_speed }, { "overdrive", rs.overdrive }, { "hmd_history", rs.hmd_serial_history }, { "hmd_fan_gen", fanGenObj }, { "ignored_warnings", rs.ignored_steamvr_warnings }, { "ipd_mm", rs.applied_ipd }, { "prox_offset", rs.prox_offset }, { "sleep_enabled", rs.vxr_auto_off }, { "eyetracking", eyetracking_settings } }; std::ofstream settings_output = std::ofstream(rs.settings_path); if (settings_output.is_open()) { settings_output << settings.dump(); settings_output.flush(); settings_output.close(); return true; } else { return false; } } void Utils::update_ipd(UI_Run_State& rs) { if (hid_handler.tundra_serial.empty()) { MessageBox(glfwGetWin32Window(ui.window), LWSTR(StringID::ERROR_NO_SERIAL).c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); return; } ui.changing_ipd.store(true); if (steam_change_thread.joinable()) { steam_change_thread.join(); } steam_change_thread = std::thread([&]() { ui.ipd_progress.store(0); // On first run we want to grab a copy of the config for backup. std::wstring headset_serial(hid_handler.tundra_serial.begin(), hid_handler.tundra_serial.end()); std::wstring backup_file_name_w = L"lighthouse_config_backup_" + headset_serial + L".json"; std::string backup_file_name = "lighthouse_config_backup_" + hid_handler.tundra_serial + ".json"; std::string serial_select_command = "serial " + hid_handler.tundra_serial + "\r\n"; // On first run we want to grab a copy of the config for backup. bool first_run = !std::filesystem::exists(backup_file_name); if (first_run) { // Download config file for backup. std::string command = "downloadconfig " + backup_file_name + "\r\n"; if (write_to_stdin(serial_select_command, rs.exe_path, command, NULL)) { if (!is_valid_json(backup_file_name_w, 10, 100)) { MessageBox(glfwGetWin32Window(ui.window), (LWSTR(StringID::ERROR_NO_BACKUP_FOUND) + L" (0x1)").c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); ui.ipd_progress.store(100); ui.changing_ipd.store(false); DeleteFile(backup_file_name_w.c_str()); return; } } else { MessageBox(glfwGetWin32Window(ui.window), (LWSTR(StringID::ERROR_NO_BACKUP_FOUND) + L" (0x2)").c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); ui.ipd_progress.store(100); ui.changing_ipd.store(false); return; } } ui.ipd_progress.store(25); // Delete any old config files. DeleteFile(L"lighthouse_config_new.json"); // Download config file. std::string command = "downloadconfig lighthouse_config_new.json\r\n"; if (write_to_stdin(serial_select_command, rs.exe_path, command, NULL)) { std::ifstream in("lighthouse_config_new.json"); int iterations = 0; // Wait for config file to be downloaded. while (!in.is_open() && iterations < 10) { Sleep(500); in.open("lighthouse_config_new.json"); ++iterations; } // Replace necessary configuration. if (in.is_open()) { std::stringstream config_buffer; config_buffer << in.rdbuf(); in.close(); std::string config_string = config_buffer.str(); // Make sure this is an HMD config file before continuing. if (config_string.find("\"device_class\": \"hmd\"") == std::string::npos || config_string.find("\"direct_mode_edid_pid\": \"1234\"") == std::string::npos || config_string.find("\"direct_mode_edid_vid\": \"BIG\"") == std::string::npos) { MessageBox(glfwGetWin32Window(ui.window), LWSTR(StringID::ERROR_READING_HMD_CONFIG).c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); ui.ipd_progress.store(100); ui.changing_ipd.store(false); return; } ui.ipd_progress.store(50); // Validate the downloaded json before continuing if (!is_valid_json(L"lighthouse_config_new.json")) { MessageBox(glfwGetWin32Window(ui.window), (LWSTR(StringID::ERROR_VALIDATING_CONFIG) + L" (0x1)").c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); ui.ipd_progress.store(100); ui.changing_ipd.store(false); return; } // Set a new IPD value size_t beginning = config_string.find("\"default_mm\""); if (beginning != std::string::npos) { // Find the start of the value size_t value_start = config_string.find(":", beginning) + 1; while (value_start < config_string.size() && isspace(config_string[value_start])) { value_start++; } // Find the end of the value size_t value_end = value_start; while (value_end < config_string.size() && (isdigit(config_string[value_end]) || config_string[value_end] == '.' || config_string[value_end] == '-')) { value_end++; } config_string.replace(value_start, value_end - value_start, std::to_string(rs.applied_ipd)); } else { MessageBox(glfwGetWin32Window(ui.window), LWSTR(StringID::ERROR_NO_IPD_FIELD).c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); ui.ipd_progress.store(100); ui.changing_ipd.store(false); return; } std::ofstream out("lighthouse_config_new.json", std::ios::trunc); if (out.is_open()) { out << config_string; out.flush(); out.close(); // Validate again after modifying if (!is_valid_json(L"lighthouse_config_new.json")) { MessageBox(glfwGetWin32Window(ui.window), (LWSTR(StringID::ERROR_VALIDATING_CONFIG) + L" (0x2)").c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); ui.ipd_progress.store(100); ui.changing_ipd.store(false); return; } // Upload the new config. std::string command = "uploadconfig lighthouse_config_new.json\r\n"; if (write_to_stdin(serial_select_command, rs.exe_path, command, NULL)) { // Successful upload. ui.ipd_progress.store(75); Sleep(500); } else { MessageBox(glfwGetWin32Window(ui.window), LWSTR(StringID::ERROR_UPLOAD_NEW_CONFIG).c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); ui.ipd_progress.store(100); ui.changing_ipd.store(false); return; } } else { MessageBox(glfwGetWin32Window(ui.window), LWSTR(StringID::ERROR_FAILED_STAGED_CONFIG).c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); ui.ipd_progress.store(100); ui.changing_ipd.store(false); return; } } else { MessageBox(glfwGetWin32Window(ui.window), (LWSTR(StringID::ERROR_FAILED_DOWNLOAD_CONFIG) + L" (0x1)").c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); ui.ipd_progress.store(100); ui.changing_ipd.store(false); return; } } else { MessageBox(glfwGetWin32Window(ui.window), (LWSTR(StringID::ERROR_FAILED_DOWNLOAD_CONFIG) + L" (0x2)").c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); ui.ipd_progress.store(100); ui.changing_ipd.store(false); return; } ui.ipd_progress.store(100); ui.changing_ipd.store(false); }); } void Utils::update_refresh_rate(UI_Run_State& rs) { if (hid_handler.tundra_serial.empty()) { MessageBox(glfwGetWin32Window(ui.window), LWSTR(StringID::ERROR_NO_SERIAL).c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); return; } ui.changing_refresh.store(true); // Update the EDID switch. This runs in the HID thread, so we // should be able to do it simultaneously. switch (rs.current_refresh_rate) { case Refresh_Rate::_75hz: rs.rate_n = 0; // sets both 75Hz and 90Hz. Default EDID break; case Refresh_Rate::_90hz: rs.rate_n = 1; // sets only 90Hz break; case Refresh_Rate::_72hz: rs.rate_n = 2; // sets only 72Hz break; } if (steam_change_thread.joinable()) { steam_change_thread.join(); } steam_change_thread = std::thread([&]() { ui.refresh_progress.store(0); // On first run we want to grab a copy of the config for backup. std::wstring headset_serial(hid_handler.tundra_serial.begin(), hid_handler.tundra_serial.end()); std::wstring backup_file_name_w = L"lighthouse_config_backup_" + headset_serial + L".json"; std::string backup_file_name = "lighthouse_config_backup_" + hid_handler.tundra_serial + ".json"; std::string serial_select_command = "serial " + hid_handler.tundra_serial + "\r\n"; // On first run we want to grab a copy of the config for backup. bool first_run = !std::filesystem::exists(backup_file_name); if (first_run) { // Download config file for backup. std::string command = "downloadconfig " + backup_file_name + "\r\n"; if (write_to_stdin(serial_select_command, rs.exe_path, command, NULL)) { if (!is_valid_json(backup_file_name_w, 10, 100)) { MessageBox(glfwGetWin32Window(ui.window), (LWSTR(StringID::ERROR_NO_BACKUP_FOUND) + L" (0x1)").c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); ui.refresh_progress.store(100); ui.changing_refresh.store(false); DeleteFile(backup_file_name_w.c_str()); return; } } else { MessageBox(glfwGetWin32Window(ui.window), (LWSTR(StringID::ERROR_NO_BACKUP_FOUND) + L" (0x2)").c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); ui.refresh_progress.store(100); ui.changing_refresh.store(false); return; } } ui.refresh_progress.store(25); // Delete any old config files. DeleteFile(L"lighthouse_config_new.json"); // Download config file. std::string command = "downloadconfig lighthouse_config_new.json\r\n"; if (write_to_stdin(serial_select_command, rs.exe_path, command, NULL)) { std::ifstream in("lighthouse_config_new.json"); int iterations = 0; // Wait for config file to be downloaded. while (!in.is_open() && iterations < 10) { Sleep(500); in.open("lighthouse_config_new.json"); ++iterations; } // Replace necessary configuration. if (in.is_open()) { std::stringstream config_buffer; config_buffer << in.rdbuf(); in.close(); std::string config_string = config_buffer.str(); // Make sure this is an HMD config file before continuing. if (config_string.find("\"device_class\": \"hmd\"") == std::string::npos || config_string.find("\"direct_mode_edid_pid\": \"1234\"") == std::string::npos || config_string.find("\"direct_mode_edid_vid\": \"BIG\"") == std::string::npos) { MessageBox(glfwGetWin32Window(ui.window), LWSTR(StringID::ERROR_READING_HMD_CONFIG).c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); ui.refresh_progress.store(100); ui.changing_refresh.store(false); return; } ui.refresh_progress.store(50); // Validate the downloaded json before continuing if (!is_valid_json(L"lighthouse_config_new.json")) { MessageBox(glfwGetWin32Window(ui.window), (LWSTR(StringID::ERROR_VALIDATING_CONFIG) + L" (0x1)").c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); ui.refresh_progress.store(100); ui.changing_refresh.store(false); return; } // Replace height. size_t beginning = config_string.find("\"eye_target_height_in_pixels\""); if (rs.current_refresh_rate == Refresh_Rate::_75hz || rs.current_refresh_rate == Refresh_Rate::_72hz) { // 75hz std::string new_height = "\"eye_target_height_in_pixels\": 2544"; config_string.replace(config_string.begin() + beginning, config_string.begin() + beginning + new_height.length(), new_height.c_str()); } else if (rs.current_refresh_rate == Refresh_Rate::_90hz) { // 90hz std::string new_height = "\"eye_target_height_in_pixels\": 1920"; config_string.replace(config_string.begin() + beginning, config_string.begin() + beginning + new_height.length(), new_height.c_str()); } // Replace width. beginning = config_string.find("\"eye_target_width_in_pixels\""); if (rs.current_refresh_rate == Refresh_Rate::_75hz || rs.current_refresh_rate == Refresh_Rate::_72hz) { // 75hz std::string new_width = "\"eye_target_width_in_pixels\": 2544"; config_string.replace(config_string.begin() + beginning, config_string.begin() + beginning + new_width.length(), new_width.c_str()); } else if (rs.current_refresh_rate == Refresh_Rate::_90hz) { // 90hz std::string new_width = "\"eye_target_width_in_pixels\": 1920"; config_string.replace(config_string.begin() + beginning, config_string.begin() + beginning + new_width.length(), new_width.c_str()); } // Write to a new config file. std::ofstream out("lighthouse_config_new.json", std::ios::trunc); if (out.is_open()) { out << config_string; out.flush(); out.close(); // Validate again after modifying if (!is_valid_json(L"lighthouse_config_new.json")) { MessageBox(glfwGetWin32Window(ui.window), (LWSTR(StringID::ERROR_VALIDATING_CONFIG) + L" (0x2)").c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); ui.refresh_progress.store(100); ui.changing_refresh.store(false); return; } // Upload the new config. std::string command = "uploadconfig lighthouse_config_new.json\r\n"; if (write_to_stdin(serial_select_command, rs.exe_path, command, NULL)) { // Successful upload. ui.refresh_progress.store(75); Sleep(500); } else { MessageBox(glfwGetWin32Window(ui.window), LWSTR(StringID::ERROR_UPLOAD_NEW_CONFIG).c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); ui.refresh_progress.store(100); ui.changing_refresh.store(false); return; } } else { MessageBox(glfwGetWin32Window(ui.window), LWSTR(StringID::ERROR_FAILED_STAGED_CONFIG).c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); ui.refresh_progress.store(100); ui.changing_refresh.store(false); return; } } else { MessageBox(glfwGetWin32Window(ui.window), (LWSTR(StringID::ERROR_FAILED_DOWNLOAD_CONFIG) + L" (0x1)").c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); ui.refresh_progress.store(100); ui.changing_refresh.store(false); return; } } else { MessageBox(glfwGetWin32Window(ui.window), (LWSTR(StringID::ERROR_FAILED_DOWNLOAD_CONFIG) + L" (0x2)").c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); ui.refresh_progress.store(100); ui.changing_refresh.store(false); return; } ui.refresh_progress.store(100); ui.changing_refresh.store(false); // Operation successful. Now set the EDID ui.set_current_refresh.store(rs.rate_n); std::vector new_edid_config; std::string edid_val; edid_val.push_back(rs.rate_n); // 75Hz = 0, which is both resolutions // 90Hz = 1, which only allows 90Hz new_edid_config.push_back(std::make_tuple(Config_Tags::EDID_Switch, edid_val)); // Save update. write_mem_config(hid_handler, rs.config_memory_backup_path, new_edid_config); }); } void Utils::upload_lighthouse_config(UI_Run_State& rs) { if (!rs.valid_path) { MessageBox(glfwGetWin32Window(ui.window), LWSTR(StringID::ERROR_FINDING_LIGHTHOUSE_CONSOLE).c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); return; } if (hid_handler.tundra_serial.empty()) { MessageBox(glfwGetWin32Window(ui.window), LWSTR(StringID::ERROR_NO_SERIAL).c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); return; } ui.uploading_lighthouse_config.store(true); if (steam_change_thread.joinable()) { steam_change_thread.join(); } steam_change_thread = std::thread([&]() { // On first run we want to grab a copy of the config for backup. std::wstring headset_serial(hid_handler.tundra_serial.begin(), hid_handler.tundra_serial.end()); std::wstring backup_file_name_w = L"lighthouse_config_backup_" + headset_serial + L".json"; std::string backup_file_name = "lighthouse_config_backup_" + hid_handler.tundra_serial + ".json"; std::string serial_select_command = "serial " + hid_handler.tundra_serial + "\r\n"; // On first run we want to grab a copy of the config for backup. bool first_run = !std::filesystem::exists(backup_file_name); if (first_run) { // Download config file for backup. std::string command = "downloadconfig " + backup_file_name + "\r\n"; write_to_stdin(serial_select_command, rs.exe_path, command, NULL); } std::ifstream in(rs.lighthouse_restore_path); int iterations = 0; // Make multiple attempts at opening the selected file. while (!in.is_open() && iterations < 10) { Sleep(500); in.open(rs.lighthouse_restore_path); ++iterations; } if (in.is_open()) { std::stringstream config_buffer; config_buffer << in.rdbuf(); in.close(); std::string config_string = config_buffer.str(); // Make sure this is an HMD config file before continuing. if (config_string.find("\"device_class\": \"hmd\"") == std::string::npos || config_string.find("\"direct_mode_edid_pid\": \"1234\"") == std::string::npos || config_string.find("\"direct_mode_edid_vid\": \"BIG\"") == std::string::npos) { MessageBox(glfwGetWin32Window(ui.window), LWSTR(StringID::ERROR_FAILED_VALIDATING_CONFIG_LT).c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); ui.uploading_lighthouse_config.store(false); return; } // Validate the json before continuing if (!is_valid_json(rs.lighthouse_restore_path)) { MessageBox(glfwGetWin32Window(ui.window), LWSTR(StringID::ERROR_FAILED_VALIDATING_CONFIG).c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); ui.uploading_lighthouse_config.store(false); return; } // Upload the new config. std::string command = "uploadconfig \"" + string_wide_to_narrow(rs.lighthouse_restore_path) + "\"\r\n"; if (write_to_stdin(serial_select_command, rs.exe_path, command, NULL)) { // Successful upload. Sleep(1000); ui.set_steamvr_settings.store(true); ui.uploading_lighthouse_config.store(false); } else { MessageBox(glfwGetWin32Window(ui.window), LWSTR(StringID::ERROR_UPLOAD_NEW_CONFIG).c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); ui.uploading_lighthouse_config.store(false); return; } } else { MessageBox(glfwGetWin32Window(ui.window), LWSTR(StringID::ERROR_FAILED_OPENING_CONFIG).c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); ui.uploading_lighthouse_config.store(false); return; } }); } void Utils::enter_dfu_mode(UI_Run_State& rs) { hid_handler.entering_dfu.store(true); rs.initiated_dfu_request = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); hid_handler.command_fpga_switch(); } void Utils::start_dfu_update(UI_Run_State& rs) { auto [exists, driverKey] = hid_handler.in_dfu(); hid_handler.device_state.store(Beyond_Device_State::EyetrackingDFU); if (!exists) { rs.fw.clear_dfu_status(); rs.dfu_update_requested = true; utils.enter_dfu_mode(rs); } else { rs.fw.load_dfu_firmware(hid_handler, rs.dfu_firmware_path); } } std::string Utils::get_device_type() { if (hid_handler.hmd_serial.empty()) { return ""; } // Models with added extensions if (hid_handler.hmd_serial.substr(0, 4) == "BS2E") { return "BS2E"; } // Regular models return hid_handler.hmd_serial.substr(0, 3); } std::tuple Utils::split_headset_firmware() { if (hid_handler.firmware_version.empty()) { return std::make_tuple(0, 0, 0); } // Extract version from string, convert to ints std::string major_version = hid_handler.firmware_version.substr(0, hid_handler.firmware_version.find('.')); int major_version_int = std::stoi(major_version); std::string minor_version = hid_handler.firmware_version.substr(hid_handler.firmware_version.find('.') + 1, hid_handler.firmware_version.find('.', hid_handler.firmware_version.find('.') + 1) - hid_handler.firmware_version.find('.') - 1); int minor_version_int = std::stoi(minor_version); std::string patch_version = hid_handler.firmware_version.substr(hid_handler.firmware_version.find('.', hid_handler.firmware_version.find('.') + 1) + 1); int patch_version_int = std::stoi(patch_version); return std::make_tuple(major_version_int, minor_version_int, patch_version_int); } std::wstring Utils::open_file_select(const wchar_t* filter) { HWND hwnd = glfwGetWin32Window(ui.window); if (hwnd) { OPENFILENAME of_instance; wchar_t szFile[MAX_PATH] = L""; ZeroMemory(&of_instance, sizeof(of_instance)); of_instance.lStructSize = sizeof(of_instance); of_instance.hwndOwner = hwnd; of_instance.nMaxFile = MAX_PATH; of_instance.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR; of_instance.lpstrFilter = filter; of_instance.nFilterIndex = 1; of_instance.lpstrFile = szFile; opened_file_dialog = true; // Display the Open dialog box if (GetOpenFileNameW(&of_instance)) { opened_file_dialog = false; return of_instance.lpstrFile; } } opened_file_dialog = false; return L""; } int Utils::get_fan_generation(std::string serial, UI_Run_State& rs) { if (rs.hmd_fan_gen.find(serial) != rs.hmd_fan_gen.end()) { return rs.hmd_fan_gen[serial]; } return 1; } void Utils::find_fan_generation(UI_Run_State& rs) { hid_handler.log << "Fetching fan generation" << std::endl; ui.finding_fan_gen.store(true); if (fan_check.joinable()) { fan_check.join(); } fan_check = std::thread([&]() { int found_gen = 1; // IMPORTANT: Set fan speed to a fixed predetermined number to compare known RPM values // Send more than once in case command fails for (int i = 0; i < 3; i++) { hid_handler.command_fan_speed_immediate(UIConstants.fan_sample_speed); Sleep(100); } Sleep(2000); // Collect RPM samples hid_handler.rpm_samples.clear(); hid_handler.log << "Waiting for fan samples" << std::endl; // Wait for periodic hid commands to collect 5 samples while (hid_handler.rpm_samples.size() <= 5) { Sleep(100); // Bail from disconnect if (hid_handler.hmd_serial.empty() || hid_handler.device == NULL) { hid_handler.log << "Aborting fan test: device disconnect" << std::endl; ui.finding_fan_gen.store(false); return; } } // Get average of RPM samples int sumRPM = 0; for (int v : hid_handler.rpm_samples) { sumRPM += v; } int avgRPM = sumRPM / hid_handler.rpm_samples.size(); hid_handler.log << "Using avg RPM value: " << avgRPM << std::endl; int range = 800; for (const auto& [fan_g, t] : UIConstants.fan_gen_targets) { // Within range of target if (fan_g == 1 && std::abs(avgRPM - t) <= range) { hid_handler.log << "Got fan generation: " << fan_g << std::endl; found_gen = fan_g; break; } if (fan_g == 2 && (std::abs(avgRPM - t) <= range || avgRPM > t)) { hid_handler.log << "Got fan generation: " << fan_g << std::endl; found_gen = fan_g; break; } } hid_handler.log << hid_handler.hmd_serial.c_str() << " has fan generation: " << found_gen << std::endl; rs.hmd_fan_gen.emplace(hid_handler.hmd_serial, found_gen); write_settings_file(rs); ui.finding_fan_gen.store(false); // Reapply fan speed adjusted for generation hid_handler.command_fan_speed_immediate(UIConstants.fan_sample_speed / (found_gen == 2 ? 2 : 1)); // Save corrected default with gen 2 if (found_gen == 2) { ui.saving.store(true); rs.fan_speed = UIConstants.fan_sample_speed; // Write current config out with current GUI options. if (!utils.update_config(hid_handler, rs)) { MessageBox(glfwGetWin32Window(ui.window), LWSTR(StringID::ERROR_FAILED_WRITE_CONFIG).c_str(), LWSTR(StringID::POPUP_TITLE_ERROR).c_str(), MB_OK); }; } }); } void Utils::find_system_locale(UI_Run_State& rs) { hid_handler.log << "Getting system locale" << std::endl; LANGID lang = GetUserDefaultUILanguage(); char lid[LOCALE_NAME_MAX_LENGTH]; LCID lcid = MAKELCID(lang, SORT_DEFAULT); GetLocaleInfoA(lcid, LOCALE_SISO639LANGNAME, lid, sizeof(lid)); hid_handler.log << "Found system locale: " << lid << std::endl; if (strcmp(lid, "en") == 0) { rs.active_locale = Locales::En; } if (strcmp(lid, "jp") == 0) { rs.active_locale = Locales::Jp; } } Lang_Setting Utils::get_locale_settings(UI_Run_State& rs) { Locales locale_next = rs.active_locale; // Fallback to en if invalid if ((int)rs.active_locale >= (int)Locales::Max || rs.active_locale == Locales::None) { locale_next = Locales::En; } return ui.languages.at(locale_next); } std::string Utils::float_to_string(float value, int precision) { char buffer[64]; auto result = std::to_chars( buffer, buffer + sizeof(buffer), value, std::chars_format::fixed, precision ); std::string s(buffer, result.ptr); // Trim trailing zeros auto pos = s.find('.'); if (pos != std::string::npos) { while (!s.empty() && s.back() == '0') s.pop_back(); if (!s.empty() && s.back() == '.') s.pop_back(); } return s; } std::string Utils::get_clipboard() { if (!OpenClipboard(nullptr)) return ""; HANDLE hData = GetClipboardData(CF_TEXT); if (hData == nullptr) { CloseClipboard(); return ""; } char* pszText = static_cast(GlobalLock(hData)); if (pszText == nullptr) { CloseClipboard(); return ""; } std::string text(pszText); GlobalUnlock(hData); CloseClipboard(); return text; } std::vector string_split(const std::string& str, char delimiter) { std::vector parts; std::stringstream ss(str); std::string item; while (std::getline(ss, item, delimiter)) parts.push_back(item); return parts; } std::string Utils::et_format_model_name(UI_Run_State& rs, const std::string& modelPath, bool skipCustom) { std::string displayName; std::vector pathParts = string_split(modelPath, '/'); // Look for the date format and model name in the path parts for (size_t j = 0; j < pathParts.size(); ++j) { const std::string& part = pathParts[j]; // Check if this part matches date format (YYYY-MM-DD_HH-mm-ss) if (part.length() == 19 && part.find('-') != std::string::npos && part.find('_') != std::string::npos) { // Found the date, now get the model name from the next part if it exists displayName = part; if (j + 1 < pathParts.size()) { std::string modelName = pathParts[j + 1]; // Remove ".model" case-insensitive const std::string extension = ".model"; if (modelName.length() >= extension.length()) { std::string ending = modelName.substr(modelName.length() - extension.length()); std::transform(ending.begin(), ending.end(), ending.begin(), ::tolower); if (ending == extension) modelName = modelName.substr(0, modelName.length() - extension.length()); } displayName += "/" + modelName; } break; } } // If we couldn't find the date format, fall back to just the model name if (displayName.empty() && !pathParts.empty()) { std::string modelName = pathParts.back(); const std::string extension = ".model"; if (modelName.length() >= extension.length()) { std::string ending = modelName.substr(modelName.length() - extension.length()); std::transform(ending.begin(), ending.end(), ending.begin(), ::tolower); if (ending == extension) modelName = modelName.substr(0, modelName.length() - extension.length()); } displayName = modelName; } // If a custom name exists, use it for display if (!skipCustom) { auto it = rs.et_labeled_models.find(displayName); if (it != rs.et_labeled_models.end()) { return it->second; } } return displayName; } void Utils::open_link(const std::string& url) { HINSTANCE result = ShellExecuteA( nullptr, "open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL ); if ((int)result <= 32) { hid_handler.log << "Failed to open URL" << std::endl; } } bool Utils::check_utility_ver() { #ifndef _DEBUG std::ifstream in("app-ver-info"); if (!in.is_open()) { return false; } std::stringstream contents; contents << in.rdbuf(); in.close(); if (contents.str().size() > 0 && contents.str() != ui.app_version) { return true; } #endif return false; } bool Utils::apply_quad_view_settings(UI_Run_State& rs) { try { const char* localAppData = std::getenv("LOCALAPPDATA"); if (!localAppData) { hid_handler.log << "Failed to find user appdata directory" << std::endl; return false; } std::filesystem::path targetDirectory = std::filesystem::path(localAppData) / "Quad-Views-Foveated"; if (!std::filesystem::exists(targetDirectory)) { std::filesystem::create_directories(targetDirectory); } std::ostringstream fields; fields << R"(#[ Beyond specific settings ] horizontal_focus_section=)" << rs.quad_horizontal_focus_section << "\n" << "vertical_focus_section=" << rs.quad_vertical_focus_section << "\n" << "peripheral_multiplier=" << rs.quad_peripheral_multiplier << "\n" << "smoothen_focus_view_edges=" << rs.quad_transition_thickness << "\n" << "focus_multiplier=" << rs.quad_focus_multiplier << "\n" << "debug_eye_gaze=" << rs.quad_debug_gaze << "\n"; hid_handler.log << L"Applying new quad-view settings" << std::endl; std::filesystem::path newSettings = targetDirectory / "settings.cfg"; std::ofstream file(newSettings); if (!file) { hid_handler.log << "Failed to open settings file" << std::endl; return false; } file << fields.str(); file.close(); hid_handler.log << "Writing to new Quad View settings" << std::endl; return true; } catch (const std::exception& ex) { hid_handler.log << "Failed to apply Quad View settings: " << ex.what() << std::endl; return false; } } int Utils::get_XR_layer_count() { const char* paths[] = { "SOFTWARE\\Khronos\\OpenXR\\1\\ApiLayers\\Implicit", "SOFTWARE\\Khronos\\OpenXR\\1\\ApiLayers\\Explicit" }; int count = 0; try { for (const char* path : paths) { HKEY key; if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, path, 0, KEY_READ, &key) == ERROR_SUCCESS) { DWORD index = 0; char valueName[512]; DWORD valueNameSize; while (true) { valueNameSize = sizeof(valueName); LONG result = RegEnumValueA( key, index, valueName, &valueNameSize, nullptr, nullptr, nullptr, nullptr ); if (result == ERROR_NO_MORE_ITEMS) break; if (result == ERROR_SUCCESS) count++; index++; } RegCloseKey(key); } } } catch (...) { hid_handler.log << "Could not fetch OpenXR layers" << std::endl; } return count; } void Utils::shutdown() { if (steam_change_thread.joinable()) { steam_change_thread.join(); } if (steamvr_check.joinable()) { steamvr_check.join(); } if (fan_check.joinable()) { fan_check.join(); } }