#include "config/config.hpp" #include "common/exception.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Config; namespace { Configuration globalConf{}; std::optional> gameConfs; } Configuration Config::activeConf{}; namespace { [[noreturn]] void thread( const std::string& file, const std::shared_ptr& valid) { const int fd = inotify_init(); if (fd < 0) throw std::runtime_error("Failed to initialize inotify:\n" "- " + std::string(strerror(errno))); const std::string parent = std::filesystem::path(file).parent_path().string(); const int wd = inotify_add_watch(fd, parent.c_str(), IN_MODIFY | IN_CLOSE_WRITE | IN_MOVE_SELF); if (wd < 0) { close(fd); throw std::runtime_error("Failed to add inotify watch for " + parent + ":\n" "- " + std::string(strerror(errno))); } // watch for changes std::optional discard_until; const std::string filename = std::filesystem::path(file).filename().string(); std::array buffer{}; while (true) { // poll fd struct pollfd pfd{}; pfd.fd = fd; pfd.events = POLLIN; const int pollRes = poll(&pfd, 1, 100); if (pollRes < 0 && errno != EINTR) { inotify_rm_watch(fd, wd); close(fd); throw std::runtime_error("Error polling inotify events:\n" "- " + std::string(strerror(errno))); } // read fd if there are events const ssize_t len = pollRes == 0 ? 0 : read(fd, buffer.data(), buffer.size()); if (len <= 0 && errno != EINTR && pollRes > 0) { inotify_rm_watch(fd, wd); close(fd); throw std::runtime_error("Error reading inotify events:\n" "- " + std::string(strerror(errno))); } size_t i{}; while (std::cmp_less(i, len)) { auto* event = reinterpret_cast(&buffer.at(i)); i += sizeof(inotify_event) + event->len; if (event->len <= 0 || event->mask & IN_IGNORED) continue; const std::string name(reinterpret_cast(event->name)); if (name != filename) continue; // stall a bit, then mark as invalid discard_until.emplace(std::chrono::steady_clock::now() + std::chrono::milliseconds(500)); } auto now = std::chrono::steady_clock::now(); if (discard_until.has_value() && now > *discard_until) { discard_until.reset(); // mark config as invalid valid->store(false, std::memory_order_release); // and wait until it has been marked as valid again while (!valid->load(std::memory_order_acquire)) std::this_thread::sleep_for(std::chrono::milliseconds(50)); } } } } bool Config::loadAndWatchConfig(const std::string& file) { globalConf.valid = std::make_shared(true); auto res = updateConfig(file); if (!res) return false; // prepare config watcher std::thread([file = file, valid = globalConf.valid]() { try { thread(file, valid); } catch (const std::exception& e) { std::cerr << "lsfg-vk: Error in config watcher thread:\n"; std::cerr << "- " << e.what() << '\n'; } }).detach(); return true; } bool Config::updateConfig(const std::string& file) { globalConf.valid->store(true, std::memory_order_relaxed); if (!std::filesystem::exists(file)) return false; // parse config file std::optional parsed; try { parsed.emplace(toml::parse(file)); } catch (const std::exception& e) { throw LSFG::rethrowable_error("Unable to parse configuration file", e); } auto& toml = *parsed; // parse global configuration const toml::value globalTable = toml::find_or_default(toml, "global"); const Configuration global{ .enable = toml::find_or(globalTable, "enable", false), .dll = toml::find_or(globalTable, "dll", std::string()), .multiplier = toml::find_or(globalTable, "multiplier", size_t(2)), .flowScale = toml::find_or(globalTable, "flow_scale", 1.0F), .performance = toml::find_or(globalTable, "performance_mode", false), .hdr = toml::find_or(globalTable, "hdr_mode", false), .valid = globalConf.valid // use the same validity flag }; // validate global configuration if (global.multiplier < 2) throw std::runtime_error("Multiplier cannot be less than 2"); if (global.flowScale < 0.25F || global.flowScale > 1.0F) throw std::runtime_error("Flow scale must be between 0.25 and 1.0"); // parse game-specific configuration std::unordered_map games; const toml::value gamesList = toml::find_or_default(toml, "game"); for (const auto& gameTable : gamesList.as_array()) { if (!gameTable.is_table()) throw std::runtime_error("Invalid game configuration entry"); if (!gameTable.contains("exe")) throw std::runtime_error("Game override missing 'exe' field"); const std::string exe = toml::find(gameTable, "exe"); Configuration game{ .enable = toml::find_or(gameTable, "enable", global.enable), .dll = toml::find_or(gameTable, "dll", global.dll), .multiplier = toml::find_or(gameTable, "multiplier", global.multiplier), .flowScale = toml::find_or(gameTable, "flow_scale", global.flowScale), .performance = toml::find_or(gameTable, "performance_mode", global.performance), .hdr = toml::find_or(gameTable, "hdr_mode", global.hdr), .valid = global.valid // only need a single validity flag }; // validate the configuration if (game.multiplier < 2) throw std::runtime_error("Multiplier cannot be less than 2"); if (game.flowScale < 0.25F || game.flowScale > 1.0F) throw std::runtime_error("Flow scale must be between 0.25 and 1.0"); games[exe] = std::move(game); } // store configurations globalConf = global; gameConfs = std::move(games); return true; } Configuration Config::getConfig(std::string_view name) { if (name.empty() || !gameConfs.has_value()) return globalConf; const auto& games = *gameConfs; auto it = std::ranges::find_if(games, [&name](const auto& pair) { return name.ends_with(pair.first); }); if (it != games.end()) return it->second; return globalConf; }