diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-04-08 03:30:04 -0500 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-04-08 03:30:04 -0500 |
| commit | 3de4056df4ea5d0afbcea388ad8d681daea1aac3 (patch) | |
| tree | fc5978451f9f51b84268f45bd2fa6f5d4a17baaf /libs | |
| parent | 5ba23a6d7a40052d9e1d4bd0cf018cfe7814d03a (diff) | |
Remove NaK/Rust dependency, port remaining functionality to native C++
Replace NaK Rust crate and nak_ffi with native C++ implementations:
- Game detection (Steam, Heroic, Bottles), VDF parser, icon extraction
- Prefix symlinks, SLR manager, Steam path detection
- Known games database
Add FUSE VFS optimizations: zero-copy reads via fuse_reply_data,
default_permissions mount option, FUSE_CAP_ASYNC_DIO, FUSE_CAP_EXPIRE_ONLY,
lookup cache parent index, cached mode bits, increased I/O buffer sizes.
Update build system, prefix setup, and various fixes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'libs')
37 files changed, 48 insertions, 8129 deletions
diff --git a/libs/game_bethesda/src/gamebryo/CMakeLists.txt b/libs/game_bethesda/src/gamebryo/CMakeLists.txt index bbc47ae..9e5970b 100644 --- a/libs/game_bethesda/src/gamebryo/CMakeLists.txt +++ b/libs/game_bethesda/src/gamebryo/CMakeLists.txt @@ -58,11 +58,7 @@ target_link_libraries(game_gamebryo $<IF:$<TARGET_EXISTS:lz4::lz4>,lz4::lz4,PkgConfig::LZ4> ) -# NaK FFI for game detection (replaces Windows registry) -if(TARGET mo2::nak_ffi) - target_link_libraries(game_gamebryo PRIVATE mo2::nak_ffi) - target_compile_definitions(game_gamebryo PRIVATE HAS_NAK_FFI) -endif() +# Game detection symbols are in libuibase.so (already linked via mo2::uibase). target_include_directories(game_gamebryo PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp b/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp index 2fd45cd..9d29dcf 100644 --- a/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp +++ b/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp @@ -39,8 +39,9 @@ #include <winver.h> #endif -#ifdef HAS_NAK_FFI -#include <nak_ffi.h> +#ifndef _WIN32 +#include "gamedetection.h" +#include "steamdetection.h" #endif #include <optional> @@ -230,12 +231,9 @@ QFileInfo GameGamebryo::findInGameFolder(const QString& relativePath) const QString GameGamebryo::identifyGamePath() const { -#ifdef HAS_NAK_FFI - // Use NaK to detect game installations on Linux - NakGameList gameList = nak_detect_all_games(); - ON_BLOCK_EXIT([&]() { - nak_game_list_free(gameList); - }); +#ifndef _WIN32 + // Detect game installations on Linux. + const GameScanResult scanResult = detectAllGames(); QString shortName = gameShortName(); QString fullName = gameName(); @@ -258,17 +256,14 @@ QString GameGamebryo::identifyGamePath() const return anyToken; }; - for (size_t i = 0; i < gameList.count; ++i) { - const NakGame& game = gameList.games[i]; - QString detectedName = QString::fromUtf8(game.name); - QString detectedPath = QString::fromUtf8(game.install_path); - if (detectedName.compare(fullName, Qt::CaseInsensitive) == 0 || - detectedName.compare(shortName, Qt::CaseInsensitive) == 0 || - detectedName.contains(fullName, Qt::CaseInsensitive) || - detectedName.contains(shortName, Qt::CaseInsensitive) || - tokensMatch(detectedName, fullName) || tokensMatch(detectedName, shortName)) { - if (looksValid(QDir(detectedPath))) { - return detectedPath; + for (const DetectedGame& game : scanResult.games) { + if (game.name.compare(fullName, Qt::CaseInsensitive) == 0 || + game.name.compare(shortName, Qt::CaseInsensitive) == 0 || + game.name.contains(fullName, Qt::CaseInsensitive) || + game.name.contains(shortName, Qt::CaseInsensitive) || + tokensMatch(game.name, fullName) || tokensMatch(game.name, shortName)) { + if (looksValid(QDir(game.install_path))) { + return game.install_path; } } } @@ -857,14 +852,8 @@ QString GameGamebryo::parseEpicGamesLocation(const QStringList& manifests) QString GameGamebryo::parseSteamLocation(const QString& appid, const QString& directoryName) { -#ifdef HAS_NAK_FFI - // Use NaK to find Steam path - char* steamPathC = nak_find_steam_path(); - QString steamLocation; - if (steamPathC) { - steamLocation = QString::fromUtf8(steamPathC); - nak_string_free(steamPathC); - } +#ifndef _WIN32 + QString steamLocation = findSteamPath(); #elif defined(_WIN32) QString path = "Software\\Valve\\Steam"; QString steamLocation = diff --git a/libs/game_bethesda/src/games/ttw/CMakeLists.txt b/libs/game_bethesda/src/games/ttw/CMakeLists.txt index 828b67f..8a2b8b4 100644 --- a/libs/game_bethesda/src/games/ttw/CMakeLists.txt +++ b/libs/game_bethesda/src/games/ttw/CMakeLists.txt @@ -18,10 +18,6 @@ mo2_configure_plugin(game_ttw NO_SOURCES WARNINGS 4) mo2_default_source_group() target_link_libraries(game_ttw PRIVATE game_gamebryo) -# NaK FFI for TTW game detection (TTW uses FalloutNV as base game) -if(TARGET mo2::nak_ffi) - target_link_libraries(game_ttw PRIVATE mo2::nak_ffi) - target_compile_definitions(game_ttw PRIVATE HAS_NAK_FFI) -endif() +# Game detection symbols are in libuibase.so (already linked via game_gamebryo → mo2::uibase). mo2_install_plugin(game_ttw) diff --git a/libs/game_bethesda/src/games/ttw/gamefalloutttw.cpp b/libs/game_bethesda/src/games/ttw/gamefalloutttw.cpp index cfd8ca8..e09ddf0 100644 --- a/libs/game_bethesda/src/games/ttw/gamefalloutttw.cpp +++ b/libs/game_bethesda/src/games/ttw/gamefalloutttw.cpp @@ -15,9 +15,8 @@ #include <gamebryosavegameinfo.h> #include <gamebryounmanagedmods.h> -#ifdef HAS_NAK_FFI -#include <nak_ffi.h> -#include "scopeguard.h" +#ifndef _WIN32 +#include "gamedetection.h" #endif #include <QCoreApplication> @@ -85,23 +84,17 @@ QString GameFalloutTTW::identifyGamePath() const QString path = "Software\\Bethesda Softworks\\FalloutNV"; result = findInRegistry(HKEY_LOCAL_MACHINE, path.toStdWString().c_str(), L"Installed Path"); -#elif defined(HAS_NAK_FFI) +#else // TTW uses FalloutNV as its base game, so look for Fallout New Vegas - // (the base class would try matching "TTW" which NaK doesn't know about) - NakGameList gameList = nak_detect_all_games(); - ON_BLOCK_EXIT([&]() { - nak_game_list_free(gameList); - }); - - for (size_t i = 0; i < gameList.count; ++i) { - const NakGame& game = gameList.games[i]; - QString detectedName = QString::fromUtf8(game.name); - if (detectedName.contains("Fallout", Qt::CaseInsensitive) && - detectedName.contains("Vegas", Qt::CaseInsensitive)) { - QString detectedPath = QString::fromUtf8(game.install_path); - if (looksValid(QDir(detectedPath))) { - result = detectedPath; - break; + { + const GameScanResult scanResult = detectAllGames(); + for (const DetectedGame& game : scanResult.games) { + if (game.name.contains("Fallout", Qt::CaseInsensitive) && + game.name.contains("Vegas", Qt::CaseInsensitive)) { + if (looksValid(QDir(game.install_path))) { + result = game.install_path; + break; + } } } } diff --git a/libs/nak/Cargo.lock b/libs/nak/Cargo.lock deleted file mode 100644 index 03b8ffd..0000000 --- a/libs/nak/Cargo.lock +++ /dev/null @@ -1,964 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bumpalo" -version = "3.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" - -[[package]] -name = "cc" -version = "1.2.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "chrono" -version = "0.4.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "dataview" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daba87f72c730b508641c9fb6411fc9bba73939eed2cab611c399500511880d0" -dependencies = [ - "derive_pod", -] - -[[package]] -name = "derive_pod" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ea6706d74fca54e15f1d40b5cf7fe7f764aaec61352a9fcec58fe27e042fc8" - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "itoa" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" - -[[package]] -name = "js-sys" -version = "0.3.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "libc" -version = "0.2.182" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "nak_rust" -version = "0.1.0" -dependencies = [ - "chrono", - "pelite", - "serde", - "serde_json", - "ureq", - "walkdir", -] - -[[package]] -name = "no-std-compat" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "pelite" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88dccf4bd32294364aeb7bd55d749604450e9db54605887551f21baea7617685" -dependencies = [ - "dataview", - "libc", - "no-std-compat", - "pelite-macros", - "winapi", -] - -[[package]] -name = "pelite-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a7cf3f8ecebb0f4895f4892a8be0a0dc81b498f9d56735cb769dc31bf00815b" - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustls" -version = "0.23.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" -dependencies = [ - "log", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" -dependencies = [ - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "simd-adler32" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.115" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "unicode-ident" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "ureq" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" -dependencies = [ - "base64", - "flate2", - "log", - "once_cell", - "rustls", - "rustls-pki-types", - "url", - "webpki-roots 0.26.11", -] - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasm-bindgen" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.6", -] - -[[package]] -name = "webpki-roots" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/libs/nak/Cargo.toml b/libs/nak/Cargo.toml deleted file mode 100644 index c51a804..0000000 --- a/libs/nak/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "nak_rust" -version = "0.1.0" -edition = "2021" - -[lib] -name = "nak_rust" -path = "src/lib.rs" - -[dependencies] -serde = { version = "1", features = ["derive"] } -serde_json = "1" -walkdir = "2" -chrono = "0.4" -ureq = "2" -pelite = "0.10" diff --git a/libs/nak/src/config.rs b/libs/nak/src/config.rs deleted file mode 100644 index 62f9fc5..0000000 --- a/libs/nak/src/config.rs +++ /dev/null @@ -1,154 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::fs; -use std::path::PathBuf; - -fn get_home() -> String { - std::env::var("HOME").unwrap_or_default() -} - -/// Normalize a path for compatibility with pressure-vessel/Steam container. -/// -/// On Fedora Atomic/Bazzite/Silverblue, $HOME is `/var/home/user` but `/home` -/// is a symlink to `/var/home`. Pressure-vessel exposes `/home` but may not -/// properly handle paths that explicitly use `/var/home/`. This function -/// converts such paths to use `/home/` instead for maximum compatibility. -pub fn normalize_path_for_steam(path: &str) -> String { - // Convert /var/home/user/... to /home/user/... - if let Some(stripped) = path.strip_prefix("/var/home/") { - format!("/home/{}", stripped) - } else { - path.to_string() - } -} - -fn default_data_path() -> String { - format!("{}/NaK", get_home()) -} - -// ============================================================================ -// Main App Config - stored in ~/.config/nak/config.json -// ============================================================================ - -#[derive(Serialize, Deserialize, Clone)] -pub struct AppConfig { - pub selected_proton: Option<String>, - /// Whether the first-run setup has been completed - #[serde(default)] - pub first_run_completed: bool, - /// Path to NaK data folder (legacy ~/NaK - used for migration detection) - #[serde(default = "default_data_path")] - pub data_path: String, - /// Whether the Steam-native migration popup has been shown - #[serde(default)] - pub steam_migration_shown: bool, - /// Custom cache location (for downloads, tmp files during install) - /// If empty/not set, uses ~/.cache/nak/ - #[serde(default)] - pub cache_location: String, - /// Selected Steam account ID (Steam3 format, e.g., "910757758") - /// If empty/not set, uses the most recently active account - #[serde(default)] - pub selected_steam_account: String, -} - -impl Default for AppConfig { - fn default() -> Self { - Self { - selected_proton: None, - first_run_completed: false, - data_path: default_data_path(), - steam_migration_shown: false, - cache_location: String::new(), - selected_steam_account: String::new(), - } - } -} - -impl AppConfig { - /// Config file path: ~/.config/nak/config.json - fn get_config_path() -> PathBuf { - PathBuf::from(format!("{}/.config/nak/config.json", get_home())) - } - - /// Legacy config path for migration: ~/NaK/config.json - fn get_legacy_path() -> PathBuf { - PathBuf::from(format!("{}/NaK/config.json", get_home())) - } - - pub fn load() -> Self { - let config_path = Self::get_config_path(); - let legacy_path = Self::get_legacy_path(); - - // Try new location first - if config_path.exists() { - if let Ok(content) = fs::read_to_string(&config_path) { - if let Ok(config) = serde_json::from_str(&content) { - return config; - } - } - } - - // Try legacy location and migrate if found - if legacy_path.exists() { - if let Ok(content) = fs::read_to_string(&legacy_path) { - if let Ok(mut config) = serde_json::from_str::<AppConfig>(&content) { - // Ensure data_path is set (old configs won't have it) - if config.data_path.is_empty() { - config.data_path = default_data_path(); - } - // Save to new location - config.save(); - // Remove old config - let _ = fs::remove_file(&legacy_path); - return config; - } - } - } - - Self::default() - } - - pub fn save(&self) { - let path = Self::get_config_path(); - if let Some(parent) = path.parent() { - let _ = fs::create_dir_all(parent); - } - if let Ok(json) = serde_json::to_string_pretty(self) { - let _ = fs::write(path, json); - } - } - - /// Get the NaK data directory path (legacy ~/NaK - used for migration detection) - pub fn get_data_path(&self) -> PathBuf { - PathBuf::from(&self.data_path) - } - - /// Get the NaK config directory (~/.config/nak/) - pub fn get_config_dir() -> PathBuf { - PathBuf::from(format!("{}/.config/nak", get_home())) - } - - /// Get the default cache directory (~/.cache/nak/) - pub fn get_default_cache_dir() -> PathBuf { - PathBuf::from(format!("{}/.cache/nak", get_home())) - } - - /// Get the cache directory (custom location or default ~/.cache/nak/) - pub fn get_cache_dir(&self) -> PathBuf { - if self.cache_location.is_empty() { - Self::get_default_cache_dir() - } else { - PathBuf::from(&self.cache_location) - } - } - - /// Get path to tmp directory (~/.cache/nak/tmp/) - pub fn get_tmp_path() -> PathBuf { - Self::get_default_cache_dir().join("tmp") - } - - /// Get path to Prefixes directory (legacy - for migration detection only) - pub fn get_prefixes_path(&self) -> PathBuf { - self.get_data_path().join("Prefixes") - } -} diff --git a/libs/nak/src/deps/mod.rs b/libs/nak/src/deps/mod.rs deleted file mode 100644 index a055eb7..0000000 --- a/libs/nak/src/deps/mod.rs +++ /dev/null @@ -1,177 +0,0 @@ -//! Dependency management via winetricks -//! -//! Uses winetricks for all Windows dependency installation. - -pub mod tools; - -use std::error::Error; -use std::path::Path; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; - -use crate::config::AppConfig; -use crate::logging::{log_error, log_install}; -use crate::runtime_wrap; -use crate::steam::SteamProton; - -// Re-export tools -pub use tools::{check_command_available, ensure_cabextract, ensure_winetricks, get_winetricks_path}; - -/// Standard winetricks verbs for MO2 prefix -pub const STANDARD_VERBS: &[&str] = &[ - "vcrun2022", // Visual C++ 2015-2022 Runtime - "dotnet6", // .NET 6.0 - "dotnet7", // .NET 7.0 - "dotnet8", // .NET 8.0 - "dotnetdesktop6", // .NET Desktop Runtime 6.0 - "d3dcompiler_47", // DirectX Compiler 47 - "d3dcompiler_43", // DirectX Compiler 43 - "d3dx9", // DirectX 9 (all versions) - "d3dx11_43", // DirectX 11 - "xact", // XACT Audio (32-bit) - "xact_x64", // XACT Audio (64-bit) -]; - - -/// Run winetricks to install dependencies -pub fn run_winetricks( - prefix_path: &Path, - proton: &SteamProton, - verbs: &[&str], - log_callback: impl Fn(String), -) -> Result<(), Box<dyn Error>> { - if verbs.is_empty() { - return Ok(()); - } - - let winetricks_path = ensure_winetricks()?; - ensure_cabextract()?; - - let Some(wine_bin) = proton.wine_binary() else { - return Err("Wine binary not found in Proton".into()); - }; - - let Some(wineserver_bin) = proton.wineserver_binary() else { - return Err("Wineserver binary not found in Proton".into()); - }; - - let cache_dir = AppConfig::get_default_cache_dir(); - std::fs::create_dir_all(&cache_dir)?; - - let verbs_str = verbs.join(" "); - log_callback(format!("Installing dependencies via winetricks: {}", verbs_str)); - log_install(&format!("Running winetricks with verbs: {}", verbs_str)); - - let nak_bin = tools::get_nak_bin_path(); - let current_path = std::env::var("PATH").unwrap_or_default(); - let new_path = format!("{}:{}", nak_bin.display(), current_path); - - let envs: Vec<(&str, String)> = vec![ - ("PATH", new_path), - ("WINE", wine_bin.display().to_string()), - ("WINESERVER", wineserver_bin.display().to_string()), - ("WINEPREFIX", prefix_path.display().to_string()), - ("WINETRICKS_CACHE", cache_dir.display().to_string()), - ]; - let status = runtime_wrap::build_command(&winetricks_path, &envs) - .arg("-q") - .args(verbs) - .status()?; - - if !status.success() { - let err_msg = format!("Winetricks failed with exit code: {:?}", status.code()); - log_error(&err_msg); - return Err(err_msg.into()); - } - - log_install("Winetricks completed successfully"); - Ok(()) -} - -/// Install all standard dependencies to a prefix -pub fn install_standard_deps( - prefix_path: &Path, - proton: &SteamProton, - log_callback: impl Fn(String), -) -> Result<(), Box<dyn Error>> { - run_winetricks(prefix_path, proton, STANDARD_VERBS, log_callback) -} - -/// Run winetricks with cancellation support. -pub fn run_winetricks_cancellable( - prefix_path: &Path, - proton: &SteamProton, - verbs: &[&str], - log_callback: impl Fn(String), - cancel_flag: &Arc<AtomicBool>, -) -> Result<(), Box<dyn Error>> { - if verbs.is_empty() { - return Ok(()); - } - - let winetricks_path = ensure_winetricks()?; - ensure_cabextract()?; - - let Some(wine_bin) = proton.wine_binary() else { - return Err("Wine binary not found in Proton".into()); - }; - - let Some(wineserver_bin) = proton.wineserver_binary() else { - return Err("Wineserver binary not found in Proton".into()); - }; - - let cache_dir = AppConfig::get_default_cache_dir(); - std::fs::create_dir_all(&cache_dir)?; - - let verbs_str = verbs.join(" "); - log_callback(format!("Installing dependencies via winetricks: {}", verbs_str)); - log_install(&format!("Running winetricks with verbs: {}", verbs_str)); - - let nak_bin = tools::get_nak_bin_path(); - let current_path = std::env::var("PATH").unwrap_or_default(); - let new_path = format!("{}:{}", nak_bin.display(), current_path); - - let envs: Vec<(&str, String)> = vec![ - ("PATH", new_path), - ("WINE", wine_bin.display().to_string()), - ("WINESERVER", wineserver_bin.display().to_string()), - ("WINEPREFIX", prefix_path.display().to_string()), - ("WINETRICKS_CACHE", cache_dir.display().to_string()), - ]; - let mut child = runtime_wrap::build_command(&winetricks_path, &envs) - .arg("-q") - .args(verbs) - .spawn()?; - - loop { - match child.try_wait()? { - Some(status) => { - if !status.success() { - let err_msg = format!("Winetricks failed with exit code: {:?}", status.code()); - log_error(&err_msg); - return Err(err_msg.into()); - } - log_install("Winetricks completed successfully"); - return Ok(()); - } - None => { - if cancel_flag.load(Ordering::Relaxed) { - let _ = child.kill(); - let _ = child.wait(); - return Err("Cancelled".into()); - } - std::thread::sleep(std::time::Duration::from_millis(250)); - } - } - } -} - -/// Install standard deps with cancellation support -pub fn install_standard_deps_cancellable( - prefix_path: &Path, - proton: &SteamProton, - log_callback: impl Fn(String), - cancel_flag: &Arc<AtomicBool>, -) -> Result<(), Box<dyn Error>> { - run_winetricks_cancellable(prefix_path, proton, STANDARD_VERBS, log_callback, cancel_flag) -} diff --git a/libs/nak/src/deps/tools.rs b/libs/nak/src/deps/tools.rs deleted file mode 100644 index 03dbe9a..0000000 --- a/libs/nak/src/deps/tools.rs +++ /dev/null @@ -1,166 +0,0 @@ -//! Linux tool management (winetricks, cabextract) -//! -//! Handles downloading and managing Linux CLI tools. -//! Tools are stored in ~/.local/share/fluorine/bin/ for Fluorine Manager. - -use std::error::Error; -use std::fs; -use std::io::Read; -use std::os::unix::fs::PermissionsExt; -use std::path::PathBuf; -use std::process::Command; - -use crate::logging::{log_error, log_info, log_warning}; - -// ============================================================================ -// NaK Bin Directory (~/.local/share/fluorine/bin/) -// ============================================================================ - -/// Get the tool bin directory path (~/.local/share/fluorine/bin/) -/// This is accessible from both native and Flatpak environments. -pub fn get_nak_bin_path() -> PathBuf { - crate::paths::data_dir().join("bin") -} - -/// Check if a command exists (either in system PATH or tool bin) -pub fn check_command_available(cmd: &str) -> bool { - // Check system PATH first - if Command::new("which") - .arg(cmd) - .output() - .map(|o| o.status.success()) - .unwrap_or(false) - { - return true; - } - - // Check tool bin directory - let nak_bin = get_nak_bin_path().join(cmd); - nak_bin.exists() -} - -// ============================================================================ -// Winetricks -// ============================================================================ - -const WINETRICKS_URL: &str = - "https://raw.githubusercontent.com/Winetricks/winetricks/master/src/winetricks"; - -/// Get the path to winetricks (without downloading) -pub fn get_winetricks_path() -> PathBuf { - get_nak_bin_path().join("winetricks") -} - -/// Ensures winetricks is downloaded and up-to-date. -pub fn ensure_winetricks() -> Result<PathBuf, Box<dyn Error>> { - let bin_dir = get_nak_bin_path(); - let winetricks_path = bin_dir.join("winetricks"); - - fs::create_dir_all(&bin_dir)?; - - log_info("Checking for winetricks updates..."); - - match ureq::get(WINETRICKS_URL).call() { - Ok(response) => { - let mut new_content = Vec::new(); - response.into_reader().read_to_end(&mut new_content)?; - - let should_update = if winetricks_path.exists() { - let existing = fs::read(&winetricks_path).unwrap_or_default(); - existing != new_content - } else { - true - }; - - if should_update { - fs::write(&winetricks_path, &new_content)?; - - let mut perms = fs::metadata(&winetricks_path)?.permissions(); - perms.set_mode(0o755); - fs::set_permissions(&winetricks_path, perms)?; - - if winetricks_path.exists() { - log_info("Winetricks updated to latest version"); - } else { - log_info(&format!("Winetricks downloaded to {:?}", winetricks_path)); - } - } - } - Err(e) => { - if winetricks_path.exists() { - log_warning(&format!("Failed to check winetricks updates: {}", e)); - } else { - return Err(format!("Failed to download winetricks: {}", e).into()); - } - } - } - - Ok(winetricks_path) -} - -// ============================================================================ -// Cabextract (required by winetricks for DirectX cabs) -// ============================================================================ - -const CABEXTRACT_URL: &str = - "https://github.com/SulfurNitride/NaK/releases/download/Cabextract/cabextract-linux-x86_64.zip"; - -/// Ensures cabextract is available in our bin directory. -/// -/// Always downloads to ~/.local/share/fluorine/bin/ because winetricks runs -/// inside pressure-vessel where system binaries under /usr are not visible. -pub fn ensure_cabextract() -> Result<PathBuf, Box<dyn Error>> { - // Check if we already downloaded it to our bin dir - let bin_dir = get_nak_bin_path(); - let cabextract_path = bin_dir.join("cabextract"); - - if cabextract_path.exists() { - return Ok(cabextract_path); - } - - // Download cabextract zip — system copy is unusable inside pressure-vessel - log_info("Downloading cabextract for use inside container..."); - fs::create_dir_all(&bin_dir)?; - - let response = ureq::get(CABEXTRACT_URL).call().map_err(|e| { - format!( - "Failed to download cabextract: {}. Please install cabextract manually.", - e - ) - })?; - - let zip_path = bin_dir.join("cabextract.zip"); - let mut zip_file = fs::File::create(&zip_path)?; - std::io::copy(&mut response.into_reader(), &mut zip_file)?; - - let status = Command::new("unzip") - .arg("-o") - .arg(&zip_path) - .arg("-d") - .arg(&bin_dir) - .status()?; - - if !status.success() { - let _ = Command::new("python3") - .arg("-c") - .arg(format!( - "import zipfile; zipfile.ZipFile('{}').extractall('{}')", - zip_path.display(), - bin_dir.display() - )) - .status(); - } - - let _ = fs::remove_file(&zip_path); - - if cabextract_path.exists() { - let mut perms = fs::metadata(&cabextract_path)?.permissions(); - perms.set_mode(0o755); - fs::set_permissions(&cabextract_path, perms)?; - log_info(&format!("cabextract downloaded to {:?}", cabextract_path)); - Ok(cabextract_path) - } else { - log_error("Failed to extract cabextract from zip"); - Err("Failed to extract cabextract from zip".into()) - } -} diff --git a/libs/nak/src/dxvk.rs b/libs/nak/src/dxvk.rs deleted file mode 100644 index b335773..0000000 --- a/libs/nak/src/dxvk.rs +++ /dev/null @@ -1,69 +0,0 @@ -//! DXVK configuration management for Fluorine Manager. -//! -//! Downloads dxvk.conf from upstream, appends Fluorine-specific settings, -//! and stores at `~/.local/share/fluorine/config/dxvk.conf`. - -use std::error::Error; -use std::fs; -use std::path::{Path, PathBuf}; - -use crate::logging::{log_info, log_warning}; - -const DXVK_CONF_URL: &str = - "https://raw.githubusercontent.com/doitsujin/dxvk/master/dxvk.conf"; - -const DXVK_CUSTOM_SETTINGS: &str = r#" -# Fluorine Custom Settings -# Disable Graphics Pipeline Library (can cause issues with modded games) -dxvk.enableGraphicsPipelineLibrary = False -"#; - -/// Get the path where the DXVK config will be stored. -pub fn get_dxvk_conf_path() -> PathBuf { - crate::paths::data_dir().join("config/dxvk.conf") -} - -/// Ensure the dxvk.conf file exists, downloading if necessary. -/// -/// Returns the path to the config file. -pub fn ensure_dxvk_conf() -> Result<PathBuf, Box<dyn Error>> { - let conf_path = get_dxvk_conf_path(); - - // If it already exists, return it - if conf_path.exists() { - return Ok(conf_path); - } - - download_and_create_dxvk_conf(&conf_path) -} - -/// Download the upstream dxvk.conf, append custom settings, and write to `dest`. -pub fn download_and_create_dxvk_conf(dest: &Path) -> Result<PathBuf, Box<dyn Error>> { - // Ensure parent directory exists - if let Some(parent) = dest.parent() { - fs::create_dir_all(parent)?; - } - - log_info("Downloading dxvk.conf from upstream..."); - - let upstream_content = match ureq::get(DXVK_CONF_URL).call() { - Ok(response) => { - let mut body = String::new(); - response.into_reader().read_to_string(&mut body)?; - body - } - Err(e) => { - log_warning(&format!("Failed to download dxvk.conf: {}", e)); - // Create with just custom settings if download fails - String::new() - } - }; - - let full_content = format!("{}\n{}", upstream_content, DXVK_CUSTOM_SETTINGS); - fs::write(dest, &full_content)?; - - log_info(&format!("Created dxvk.conf at {:?}", dest)); - Ok(dest.to_path_buf()) -} - -use std::io::Read as _; diff --git a/libs/nak/src/game_finder/bottles.rs b/libs/nak/src/game_finder/bottles.rs deleted file mode 100644 index 7d974e8..0000000 --- a/libs/nak/src/game_finder/bottles.rs +++ /dev/null @@ -1,147 +0,0 @@ -//! Bottles prefix detection -//! -//! Detects games installed in Bottles prefixes by scanning registry files -//! for known game registry entries. - -use std::fs; -use std::path::{Path, PathBuf}; - -use super::known_games::KNOWN_GAMES; -use super::registry::{read_registry_value, wine_path_to_linux}; -use super::{Game, Launcher}; -use crate::logging::log_info; - -/// Possible Bottles data paths -const BOTTLES_PATHS: &[&str] = &[ - ".local/share/bottles/bottles", - ".var/app/com.usebottles.bottles/data/bottles/bottles", -]; - -/// Detect all games in Bottles prefixes -pub fn detect_bottles_games() -> Vec<Game> { - let mut games = Vec::new(); - let home = match std::env::var("HOME") { - Ok(h) => h, - Err(_) => return games, - }; - - for relative_path in BOTTLES_PATHS { - let bottles_path = PathBuf::from(&home).join(relative_path); - if !bottles_path.exists() { - continue; - } - - log_info(&format!("Found Bottles installation: {}", bottles_path.display())); - - // Scan each bottle - let Ok(entries) = fs::read_dir(&bottles_path) else { - continue; - }; - - for entry in entries.flatten() { - let bottle_path = entry.path(); - if !bottle_path.is_dir() { - continue; - } - - // Each bottle might have games - let bottle_games = scan_bottle(&bottle_path); - games.extend(bottle_games); - } - } - - log_info(&format!("Bottles: Found {} installed games", games.len())); - games -} - -/// Scan a single Bottles bottle for known games -fn scan_bottle(bottle_path: &Path) -> Vec<Game> { - let mut games = Vec::new(); - - // The prefix is directly in the bottle folder (not in pfx subfolder like Steam) - // Check for drive_c to confirm it's a valid Wine prefix - let drive_c = bottle_path.join("drive_c"); - if !drive_c.exists() { - return games; - } - - let bottle_name = bottle_path - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("Unknown"); - - // Check for each known game by registry entry - for known_game in KNOWN_GAMES { - if let Some(install_path_wine) = - read_registry_value(bottle_path, known_game.registry_path, known_game.registry_value) - { - // Convert Wine path to Linux path - let install_path = match wine_path_to_linux(&install_path_wine) { - Some(p) => p, - None => { - // Try as a relative path within drive_c - if install_path_wine.starts_with("C:") || install_path_wine.starts_with("c:") { - let relative = install_path_wine[2..].replace('\\', "/"); - drive_c.join(relative.trim_start_matches('/')) - } else { - continue; - } - } - }; - - if !install_path.exists() { - continue; - } - - log_info(&format!( - "Found {} in Bottles bottle '{}'", - known_game.name, bottle_name - )); - - games.push(Game { - name: known_game.name.to_string(), - app_id: format!("bottles-{}", known_game.steam_app_id), - install_path, - prefix_path: Some(bottle_path.to_path_buf()), - launcher: Launcher::Bottles, - my_games_folder: known_game.my_games_folder.map(String::from), - appdata_local_folder: known_game.appdata_local_folder.map(String::from), - appdata_roaming_folder: known_game.appdata_roaming_folder.map(String::from), - registry_path: Some(known_game.registry_path.to_string()), - registry_value: Some(known_game.registry_value.to_string()), - }); - } - } - - games -} - -/// Find all Bottles prefixes (for manual prefix selection) -pub fn find_bottles_prefixes() -> Vec<PathBuf> { - let mut prefixes = Vec::new(); - let home = match std::env::var("HOME") { - Ok(h) => h, - Err(_) => return prefixes, - }; - - for relative_path in BOTTLES_PATHS { - let bottles_path = PathBuf::from(&home).join(relative_path); - if !bottles_path.exists() { - continue; - } - - let Ok(entries) = fs::read_dir(&bottles_path) else { - continue; - }; - - for entry in entries.flatten() { - let bottle_path = entry.path(); - // Verify it's a valid Wine prefix - if bottle_path.is_dir() && bottle_path.join("drive_c").exists() { - prefixes.push(bottle_path); - } - } - } - - prefixes -} diff --git a/libs/nak/src/game_finder/heroic.rs b/libs/nak/src/game_finder/heroic.rs deleted file mode 100644 index e80c1e1..0000000 --- a/libs/nak/src/game_finder/heroic.rs +++ /dev/null @@ -1,282 +0,0 @@ -//! Heroic Games Launcher detection -//! -//! Detects games installed via Heroic (GOG and Epic Games). -//! Parses installed.json and GamesConfig/*.json for game and prefix info. - -use std::fs; -use std::path::{Path, PathBuf}; - -use serde::Deserialize; - -use super::known_games::{find_by_epic_id, find_by_gog_id, find_by_title}; -use super::{Game, HeroicStore, Launcher}; -use crate::logging::{log_info, log_warning}; - -/// Possible Heroic configuration paths -const HEROIC_PATHS: &[&str] = &[ - ".config/heroic", // Native - ".var/app/com.heroicgameslauncher.hgl/config/heroic", // Flatpak -]; - -/// Detect all Heroic games -pub fn detect_heroic_games() -> Vec<Game> { - let mut games = Vec::new(); - let home = match std::env::var("HOME") { - Ok(h) => h, - Err(_) => return games, - }; - - for relative_path in HEROIC_PATHS { - let heroic_path = PathBuf::from(&home).join(relative_path); - if !heroic_path.exists() { - continue; - } - - log_info(&format!("Found Heroic installation: {}", heroic_path.display())); - - // Detect GOG games - let gog_games = detect_gog_games(&heroic_path); - games.extend(gog_games); - - // Detect Epic games - let epic_games = detect_epic_games(&heroic_path); - games.extend(epic_games); - } - - log_info(&format!("Heroic: Found {} installed games", games.len())); - games -} - -// ============================================================================ -// GOG Detection -// ============================================================================ - -/// GOG installed game entry from installed.json -#[derive(Debug, Deserialize)] -struct GogInstalledGame { - #[serde(rename = "appName")] - app_name: String, - title: Option<String>, - #[serde(rename = "install_path")] - install_path: Option<String>, - platform: Option<String>, -} - -/// Wrapper for Heroic's GOG installed.json format: {"installed": [...]} -#[derive(Debug, Deserialize)] -struct GogInstalledWrapper { - installed: Vec<GogInstalledGame>, -} - -/// Detect GOG games from Heroic -fn detect_gog_games(heroic_path: &Path) -> Vec<Game> { - let mut games = Vec::new(); - let installed_json = heroic_path.join("gog_store/installed.json"); - - let Ok(content) = fs::read_to_string(&installed_json) else { - return games; - }; - - // Heroic wraps GOG games in {"installed": [...]}, but also handle bare arrays - let installed: Vec<GogInstalledGame> = - if let Ok(wrapper) = serde_json::from_str::<GogInstalledWrapper>(&content) { - wrapper.installed - } else if let Ok(list) = serde_json::from_str::<Vec<GogInstalledGame>>(&content) { - list - } else { - log_warning("Failed to parse Heroic GOG installed.json"); - return games; - }; - - for gog_game in installed { - // Skip non-Windows games (we only care about Wine prefixes) - if gog_game.platform.as_deref() != Some("windows") { - continue; - } - - let Some(install_path_str) = gog_game.install_path else { - continue; - }; - - let install_path = PathBuf::from(&install_path_str); - if !install_path.exists() { - continue; - } - - // Get the game config for Wine prefix info - let prefix_path = get_heroic_game_prefix(heroic_path, &gog_game.app_name); - - // Look up known game info - let known_game = find_by_gog_id(&gog_game.app_name); - - let name = gog_game - .title - .unwrap_or_else(|| gog_game.app_name.clone()); - - games.push(Game { - name, - app_id: gog_game.app_name, - install_path, - prefix_path, - launcher: Launcher::Heroic { - store: HeroicStore::GOG, - }, - my_games_folder: known_game.and_then(|g| g.my_games_folder.map(String::from)), - appdata_local_folder: known_game.and_then(|g| g.appdata_local_folder.map(String::from)), - appdata_roaming_folder: known_game.and_then(|g| g.appdata_roaming_folder.map(String::from)), - registry_path: known_game.map(|g| g.registry_path.to_string()), - registry_value: known_game.map(|g| g.registry_value.to_string()), - }); - } - - games -} - -// ============================================================================ -// Epic Detection -// ============================================================================ - -/// Epic installed game entry from installed.json -#[derive(Debug, Deserialize)] -struct EpicInstalledGame { - #[serde(rename = "app_name")] - app_name: String, - title: Option<String>, - install_path: Option<String>, - platform: Option<String>, - is_installed: Option<bool>, -} - -/// Detect Epic games from Heroic -fn detect_epic_games(heroic_path: &Path) -> Vec<Game> { - let mut games = Vec::new(); - let installed_json = heroic_path.join("store_cache/legendary_library.json"); - - // Also try the older location - let installed_json = if installed_json.exists() { - installed_json - } else { - heroic_path.join("legendaryConfig/legendary/installed.json") - }; - - let Ok(content) = fs::read_to_string(&installed_json) else { - return games; - }; - - // The Epic library format can vary - try parsing as an object with game keys - if let Ok(library) = serde_json::from_str::<serde_json::Value>(&content) { - if let Some(obj) = library.as_object() { - for (app_name, game_data) in obj { - let Some(game_obj) = game_data.as_object() else { - continue; - }; - - // Check if installed - let is_installed = game_obj - .get("is_installed") - .and_then(|v| v.as_bool()) - .unwrap_or(false); - if !is_installed { - continue; - } - - // Get platform - let platform = game_obj - .get("platform") - .and_then(|v| v.as_str()); - if platform != Some("Windows") && platform != Some("windows") { - continue; - } - - // Get install path - let Some(install_path_str) = game_obj - .get("install_path") - .and_then(|v| v.as_str()) - else { - continue; - }; - - let install_path = PathBuf::from(install_path_str); - if !install_path.exists() { - continue; - } - - // Get title - let title = game_obj - .get("title") - .and_then(|v| v.as_str()) - .unwrap_or(app_name); - - // Look up known game info: try Epic AppName first, then title match - let known_game = find_by_epic_id(app_name) - .or_else(|| find_by_title(title)); - - let name = title.to_string(); - - // Get Wine prefix - let prefix_path = get_heroic_game_prefix(heroic_path, app_name); - - games.push(Game { - name, - app_id: app_name.clone(), - install_path, - prefix_path, - launcher: Launcher::Heroic { - store: HeroicStore::Epic, - }, - my_games_folder: known_game.and_then(|g| g.my_games_folder.map(String::from)), - appdata_local_folder: known_game.and_then(|g| g.appdata_local_folder.map(String::from)), - appdata_roaming_folder: known_game.and_then(|g| g.appdata_roaming_folder.map(String::from)), - registry_path: known_game.map(|g| g.registry_path.to_string()), - registry_value: known_game.map(|g| g.registry_value.to_string()), - }); - } - } - } - - games -} - -// ============================================================================ -// Shared Utilities -// ============================================================================ - -/// Game config entry for Wine settings -#[derive(Debug, Deserialize)] -struct HeroicGameConfig { - #[serde(rename = "winePrefix")] - wine_prefix: Option<String>, - #[serde(rename = "wineVersion")] - wine_version: Option<WineVersion>, -} - -#[derive(Debug, Deserialize)] -struct WineVersion { - bin: Option<String>, - name: Option<String>, - #[serde(rename = "type")] - wine_type: Option<String>, -} - -/// Get the Wine prefix for a Heroic game from its config file -fn get_heroic_game_prefix(heroic_path: &Path, app_name: &str) -> Option<PathBuf> { - let config_path = heroic_path.join(format!("GamesConfig/{}.json", app_name)); - - let content = fs::read_to_string(&config_path).ok()?; - - // The config can be either a direct object or wrapped in the app_name key - let config: serde_json::Value = serde_json::from_str(&content).ok()?; - - // Try to get winePrefix from the object or from a nested object - let wine_prefix = config - .get("winePrefix") - .or_else(|| config.get(app_name).and_then(|v| v.get("winePrefix"))) - .and_then(|v| v.as_str())?; - - let prefix_path = PathBuf::from(wine_prefix); - if prefix_path.exists() { - Some(prefix_path) - } else { - None - } -} diff --git a/libs/nak/src/game_finder/known_games.rs b/libs/nak/src/game_finder/known_games.rs deleted file mode 100644 index 117b0c7..0000000 --- a/libs/nak/src/game_finder/known_games.rs +++ /dev/null @@ -1,386 +0,0 @@ -//! Known games configuration -//! -//! Contains metadata for games that NaK supports, including: -//! - Steam App ID -//! - GOG App ID -//! - Epic Games Store App Name (Legendary/Heroic internal identifier) -//! - My Games folder name (Documents/My Games/*) -//! - AppData/Local folder name -//! - Registry path for game detection - -/// Configuration for a known game -#[derive(Debug, Clone)] -pub struct KnownGame { - /// Display name - pub name: &'static str, - /// Steam App ID - pub steam_app_id: &'static str, - /// GOG App ID (if available on GOG) - pub gog_app_id: Option<&'static str>, - /// Epic Games Store AppName / Legendary internal ID (if available on Epic) - pub epic_app_id: Option<&'static str>, - /// Folder name in Documents/My Games (if applicable) - pub my_games_folder: Option<&'static str>, - /// Folder name in AppData/Local (if applicable) - pub appdata_local_folder: Option<&'static str>, - /// Folder name in AppData/Roaming (if applicable) - pub appdata_roaming_folder: Option<&'static str>, - /// Registry path under HKLM\Software\ (for game detection) - pub registry_path: &'static str, - /// Registry value name for install path - pub registry_value: &'static str, - /// Expected folder name in steamapps/common/ - pub steam_folder: &'static str, -} - -/// All known games that NaK supports -pub const KNOWN_GAMES: &[KnownGame] = &[ - // Bethesda Games - KnownGame { - name: "Enderal", - steam_app_id: "933480", - gog_app_id: Some("1708684988"), // Enderal: Forgotten Stories on GOG - epic_app_id: None, - my_games_folder: Some("Enderal"), - appdata_local_folder: None, - appdata_roaming_folder: None, - registry_path: r"Software\SureAI\Enderal", - registry_value: "Install_Path", - steam_folder: "Enderal", - }, - KnownGame { - name: "Enderal Special Edition", - steam_app_id: "976620", - gog_app_id: None, // Not on GOG (only original Enderal) - epic_app_id: None, - my_games_folder: Some("Enderal Special Edition"), - appdata_local_folder: None, - appdata_roaming_folder: None, - registry_path: r"Software\SureAI\Enderal SE", - registry_value: "installed path", - steam_folder: "Enderal Special Edition", - }, - KnownGame { - name: "Fallout 3", - steam_app_id: "22300", - gog_app_id: Some("1454315831"), // Fallout 3 GOTY - epic_app_id: Some("adeae8bbfc94427db57c7dfecce3f1d4"), - my_games_folder: Some("Fallout3"), - appdata_local_folder: Some("Fallout3"), - appdata_roaming_folder: None, - registry_path: r"Software\Bethesda Softworks\Fallout3", - registry_value: "Installed Path", - steam_folder: "Fallout 3", - }, - KnownGame { - name: "Fallout 4", - steam_app_id: "377160", - gog_app_id: Some("1998527297"), // Fallout 4 GOTY on GOG - epic_app_id: Some("61d52ce4d09d41e48800c22784d13ae8"), - my_games_folder: Some("Fallout4"), - appdata_local_folder: Some("Fallout4"), - appdata_roaming_folder: None, - registry_path: r"Software\Bethesda Softworks\Fallout4", - registry_value: "Installed Path", - steam_folder: "Fallout 4", - }, - KnownGame { - name: "Fallout 4 VR", - steam_app_id: "611660", - gog_app_id: None, - epic_app_id: None, - my_games_folder: Some("Fallout4VR"), - appdata_local_folder: None, - appdata_roaming_folder: None, - registry_path: r"Software\Bethesda Softworks\Fallout 4 VR", - registry_value: "Installed Path", - steam_folder: "Fallout 4 VR", - }, - KnownGame { - name: "Fallout New Vegas", - steam_app_id: "22380", - gog_app_id: Some("1454587428"), // Fallout NV Ultimate - epic_app_id: Some("5daeb974a22a435988892319b3a4f476"), - my_games_folder: Some("FalloutNV"), - appdata_local_folder: Some("FalloutNV"), - appdata_roaming_folder: None, - registry_path: r"Software\Bethesda Softworks\FalloutNV", - registry_value: "Installed Path", - steam_folder: "Fallout New Vegas", - }, - KnownGame { - name: "Morrowind", - steam_app_id: "22320", - gog_app_id: Some("1440163901"), // Morrowind GOTY - epic_app_id: None, // On Epic but internal AppName not yet known - my_games_folder: Some("Morrowind"), - appdata_local_folder: None, - appdata_roaming_folder: None, - registry_path: r"Software\Bethesda Softworks\Morrowind", - registry_value: "Installed Path", - steam_folder: "Morrowind", - }, - KnownGame { - name: "Oblivion", - steam_app_id: "22330", - gog_app_id: Some("1458058109"), // Oblivion GOTY Deluxe - epic_app_id: None, // On Epic but internal AppName not yet known - my_games_folder: Some("Oblivion"), - appdata_local_folder: Some("Oblivion"), - appdata_roaming_folder: None, - registry_path: r"Software\Bethesda Softworks\Oblivion", - registry_value: "Installed Path", - steam_folder: "Oblivion", - }, - KnownGame { - name: "Skyrim", - steam_app_id: "72850", - gog_app_id: None, // Not on GOG (only SE/AE) - epic_app_id: None, // Not on Epic (only SE/AE) - my_games_folder: Some("Skyrim"), - appdata_local_folder: Some("Skyrim"), - appdata_roaming_folder: None, - registry_path: r"Software\Bethesda Softworks\Skyrim", - registry_value: "Installed Path", - steam_folder: "Skyrim", - }, - KnownGame { - name: "Skyrim Special Edition", - steam_app_id: "489830", - gog_app_id: Some("1711230643"), // Skyrim SE on GOG - epic_app_id: Some("ac82db5035584c7f8a2c548d98c86b2c"), - my_games_folder: Some("Skyrim Special Edition"), - appdata_local_folder: Some("Skyrim Special Edition"), - appdata_roaming_folder: None, - registry_path: r"Software\Bethesda Softworks\Skyrim Special Edition", - registry_value: "Installed Path", - steam_folder: "Skyrim Special Edition", - }, - KnownGame { - name: "Skyrim VR", - steam_app_id: "611670", - gog_app_id: None, - epic_app_id: None, - my_games_folder: Some("Skyrim VR"), - appdata_local_folder: None, - appdata_roaming_folder: None, - registry_path: r"Software\Bethesda Softworks\Skyrim VR", - registry_value: "Installed Path", - steam_folder: "Skyrim VR", - }, - KnownGame { - name: "Starfield", - steam_app_id: "1716740", - gog_app_id: None, // Xbox/Steam exclusive - epic_app_id: None, - my_games_folder: Some("Starfield"), - appdata_local_folder: None, - appdata_roaming_folder: None, - registry_path: r"Software\Bethesda Softworks\Starfield", - registry_value: "Installed Path", - steam_folder: "Starfield", - }, - // CD Projekt RED Games - KnownGame { - name: "The Witcher 3", - steam_app_id: "292030", - gog_app_id: Some("1495134320"), // Witcher 3 GOTY - epic_app_id: None, - my_games_folder: Some("The Witcher 3"), - appdata_local_folder: None, - appdata_roaming_folder: None, - registry_path: r"Software\CD Projekt Red\The Witcher 3", - registry_value: "InstallFolder", - steam_folder: "The Witcher 3 Wild Hunt", - }, - KnownGame { - name: "Cyberpunk 2077", - steam_app_id: "1091500", - gog_app_id: Some("1423049311"), - epic_app_id: None, - my_games_folder: None, - appdata_local_folder: Some("CD Projekt Red/Cyberpunk 2077"), - appdata_roaming_folder: None, - registry_path: r"Software\CD Projekt Red\Cyberpunk 2077", - registry_value: "InstallFolder", - steam_folder: "Cyberpunk 2077", - }, - // Other popular moddable games - KnownGame { - name: "Baldur's Gate 3", - steam_app_id: "1086940", - gog_app_id: Some("1456460669"), - epic_app_id: None, - my_games_folder: None, - appdata_local_folder: Some("Larian Studios/Baldur's Gate 3"), - appdata_roaming_folder: None, - registry_path: r"Software\Larian Studios\Baldur's Gate 3", - registry_value: "InstallDir", - steam_folder: "Baldurs Gate 3", - }, -]; - -/// Alternative GOG App IDs that should map to the same game. -/// Some games have multiple GOG product IDs (e.g. different editions). -const GOG_ID_ALIASES: &[(&str, &str)] = &[ - // Morrowind: Vortex uses 1435828767, NaK primary is 1440163901 - ("1435828767", "1440163901"), - // Skyrim Anniversary Edition is a separate GOG product but same game as SE - ("1801825368", "1711230643"), -]; - -/// Find a known game by Steam App ID -pub fn find_by_steam_id(app_id: &str) -> Option<&'static KnownGame> { - let normalized_id = normalize_steam_id(app_id); - KNOWN_GAMES.iter().find(|g| g.steam_app_id == normalized_id) -} - -/// Find a known game by GOG App ID -pub fn find_by_gog_id(app_id: &str) -> Option<&'static KnownGame> { - // Direct match first - if let Some(game) = KNOWN_GAMES.iter().find(|g| g.gog_app_id == Some(app_id)) { - return Some(game); - } - // Check aliases: resolve alternate GOG ID to primary, then look up - for &(alias, primary) in GOG_ID_ALIASES { - if app_id == alias { - return KNOWN_GAMES.iter().find(|g| g.gog_app_id == Some(primary)); - } - } - None -} - -/// Find a known game by Epic Games Store AppName (Legendary/Heroic internal ID) -pub fn find_by_epic_id(app_id: &str) -> Option<&'static KnownGame> { - KNOWN_GAMES - .iter() - .find(|g| g.epic_app_id == Some(app_id)) -} - -/// Strip punctuation and collapse whitespace for fuzzy title matching. -fn normalize_for_matching(s: &str) -> String { - s.chars() - .map(|c| if c.is_alphanumeric() || c == ' ' { c } else { ' ' }) - .collect::<String>() - .split_whitespace() - .collect::<Vec<_>>() - .join(" ") -} - -/// Find a known game by title, using fuzzy matching. -/// Strips common suffixes like "Game of the Year Edition" for comparison. -pub fn find_by_title(title: &str) -> Option<&'static KnownGame> { - let title_lower = title.to_lowercase(); - - // Exact match first - if let Some(game) = KNOWN_GAMES.iter().find(|g| g.name.to_lowercase() == title_lower) { - return Some(game); - } - - // Check if the title starts with or contains a known game name - // Sort by name length descending so "Skyrim Special Edition" matches before "Skyrim" - let mut games_by_name_len: Vec<&KnownGame> = KNOWN_GAMES.iter().collect(); - games_by_name_len.sort_by(|a, b| b.name.len().cmp(&a.name.len())); - - // Normalize both title and game names: strip colons and extra whitespace for comparison - let title_normalized = normalize_for_matching(&title_lower); - - for game in games_by_name_len { - let game_lower = game.name.to_lowercase(); - let game_normalized = normalize_for_matching(&game_lower); - - // Direct containment (e.g. "Fallout 3: Game of the Year Edition" contains "Fallout 3") - if title_lower.contains(&game_lower) { - return Some(game); - } - - // Normalized containment (e.g. "Fallout: New Vegas" normalized to "fallout new vegas" - // matches game name "Fallout New Vegas" normalized to "fallout new vegas") - if title_normalized.contains(&game_normalized) { - return Some(game); - } - - // After-colon match: "The Elder Scrolls III: Morrowind" -> check "Morrowind" - if let Some(after_colon) = title_lower.split(':').nth(1) { - let after_colon = after_colon.trim(); - if after_colon.starts_with(&game_lower) { - return Some(game); - } - } - } - - None -} - -/// Find a known game by name (case-insensitive) -pub fn find_by_name(name: &str) -> Option<&'static KnownGame> { - let name_lower = name.to_lowercase(); - KNOWN_GAMES - .iter() - .find(|g| g.name.to_lowercase() == name_lower) -} - -/// Normalize Steam App IDs that have equivalent variants. -fn normalize_steam_id(app_id: &str) -> &str { - match app_id { - // Fallout 3 often appears as GOTY App ID 22370. - // We treat it as Fallout 3 for shared metadata/registry mapping. - "22370" => "22300", - _ => app_id, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn fallout_3_goty_alias_maps_to_fallout_3() { - let game = find_by_steam_id("22370").expect("22370 should map to Fallout 3"); - assert_eq!(game.name, "Fallout 3"); - assert_eq!(game.steam_app_id, "22300"); - } - - #[test] - fn find_by_epic_id_works() { - let game = find_by_epic_id("ac82db5035584c7f8a2c548d98c86b2c") - .expect("Should find Skyrim SE by Epic ID"); - assert_eq!(game.name, "Skyrim Special Edition"); - } - - #[test] - fn gog_alias_maps_correctly() { - // Skyrim Anniversary Edition GOG ID maps to Skyrim SE - let game = find_by_gog_id("1801825368") - .expect("Skyrim AE GOG ID should map to Skyrim SE"); - assert_eq!(game.name, "Skyrim Special Edition"); - } - - #[test] - fn find_by_title_matches_full_titles() { - let game = find_by_title("The Elder Scrolls III: Morrowind Game of the Year Edition") - .expect("Should find Morrowind by full Epic title"); - assert_eq!(game.name, "Morrowind"); - } - - #[test] - fn find_by_title_matches_colon_titles() { - let game = find_by_title("The Elder Scrolls IV: Oblivion Game of the Year Edition") - .expect("Should find Oblivion by full title"); - assert_eq!(game.name, "Oblivion"); - } - - #[test] - fn find_by_title_prefers_longer_match() { - let game = find_by_title("Skyrim Special Edition") - .expect("Should find Skyrim SE, not Skyrim"); - assert_eq!(game.name, "Skyrim Special Edition"); - } - - #[test] - fn find_by_title_fallout_nv() { - let game = find_by_title("Fallout: New Vegas Ultimate Edition") - .expect("Should find Fallout NV"); - assert_eq!(game.name, "Fallout New Vegas"); - } -} diff --git a/libs/nak/src/game_finder/mod.rs b/libs/nak/src/game_finder/mod.rs deleted file mode 100644 index 600751a..0000000 --- a/libs/nak/src/game_finder/mod.rs +++ /dev/null @@ -1,218 +0,0 @@ -//! Game detection module -//! -//! Provides unified game detection across multiple launchers: -//! - Steam (native, Flatpak, Snap) -//! - Heroic (GOG, Epic) -//! - Bottles - -// Allow unused items - this is a public API module -#![allow(dead_code)] -#![allow(unused_imports)] - -mod bottles; -mod heroic; -pub mod known_games; -mod registry; -mod steam; -mod vdf; - -use std::path::PathBuf; - -pub use bottles::detect_bottles_games; -pub use heroic::detect_heroic_games; -pub use known_games::{ - find_by_epic_id, find_by_gog_id, find_by_name, find_by_steam_id, find_by_title, KnownGame, - KNOWN_GAMES, -}; -pub use registry::{read_registry_value, wine_path_to_linux}; -pub use steam::{detect_steam_games, find_game_install_path, find_game_prefix_path, get_known_game}; - -// ============================================================================ -// Core Types -// ============================================================================ - -/// The launcher/store a game was installed from -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Launcher { - Steam { is_flatpak: bool, is_snap: bool }, - Heroic { store: HeroicStore }, - Bottles, -} - -impl Launcher { - pub fn display_name(&self) -> &'static str { - match self { - Launcher::Steam { is_flatpak: true, .. } => "Steam (Flatpak)", - Launcher::Steam { is_snap: true, .. } => "Steam (Snap)", - Launcher::Steam { .. } => "Steam", - Launcher::Heroic { store: HeroicStore::GOG } => "Heroic (GOG)", - Launcher::Heroic { store: HeroicStore::Epic } => "Heroic (Epic)", - Launcher::Bottles => "Bottles", - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum HeroicStore { - GOG, - Epic, -} - -/// A detected game installation -#[derive(Debug, Clone)] -pub struct Game { - pub name: String, - pub app_id: String, - pub install_path: PathBuf, - pub prefix_path: Option<PathBuf>, - pub launcher: Launcher, - pub my_games_folder: Option<String>, - pub appdata_local_folder: Option<String>, - pub appdata_roaming_folder: Option<String>, - pub registry_path: Option<String>, - pub registry_value: Option<String>, -} - -impl Game { - pub fn has_prefix(&self) -> bool { - self.prefix_path.is_some() - } - - pub fn get_prefix_user_path(&self) -> Option<PathBuf> { - let prefix = self.prefix_path.as_ref()?; - let users_dir = prefix.join("drive_c/users"); - - if let Ok(entries) = std::fs::read_dir(&users_dir) { - for entry in entries.flatten() { - let name = entry.file_name().to_string_lossy().to_string(); - if name != "Public" && name != "root" { - return Some(users_dir.join(name)); - } - } - } - - Some(users_dir.join("steamuser")) - } - - pub fn get_prefix_documents_path(&self) -> Option<PathBuf> { - self.get_prefix_user_path().map(|p| p.join("Documents")) - } - - pub fn get_prefix_my_games_path(&self) -> Option<PathBuf> { - let docs = self.get_prefix_documents_path()?; - let folder = self.my_games_folder.as_ref()?; - Some(docs.join("My Games").join(folder)) - } - - pub fn get_prefix_appdata_local_path(&self) -> Option<PathBuf> { - let user = self.get_prefix_user_path()?; - let folder = self.appdata_local_folder.as_ref()?; - Some(user.join("AppData/Local").join(folder)) - } - - pub fn get_prefix_appdata_roaming_path(&self) -> Option<PathBuf> { - let user = self.get_prefix_user_path()?; - let folder = self.appdata_roaming_folder.as_ref()?; - Some(user.join("AppData/Roaming").join(folder)) - } -} - -// ============================================================================ -// Scan Results -// ============================================================================ - -#[derive(Debug, Default)] -pub struct GameScanResult { - pub games: Vec<Game>, - pub steam_count: usize, - pub heroic_count: usize, - pub bottles_count: usize, -} - -impl GameScanResult { - pub fn games_with_prefixes(&self) -> impl Iterator<Item = &Game> { - self.games.iter().filter(|g| g.has_prefix()) - } - - pub fn games_by_launcher(&self, launcher_type: &str) -> Vec<&Game> { - self.games - .iter() - .filter(|g| { - matches!( - (&g.launcher, launcher_type), - (Launcher::Steam { .. }, "steam") - | (Launcher::Heroic { .. }, "heroic") - | (Launcher::Bottles, "bottles") - ) - }) - .collect() - } - - pub fn find_by_name(&self, name: &str) -> Option<&Game> { - let name_lower = name.to_lowercase(); - self.games - .iter() - .find(|g| g.name.to_lowercase() == name_lower) - } - - pub fn find_by_app_id(&self, app_id: &str) -> Option<&Game> { - self.games.iter().find(|g| g.app_id == app_id) - } -} - -// ============================================================================ -// Public API -// ============================================================================ - -/// Detect all installed games from all supported launchers. -/// -/// Games are detected in priority order: Steam -> Heroic (GOG -> Epic) -> Bottles. -/// When the same game is found from multiple launchers, the higher-priority -/// detection is kept and duplicates are skipped. This ensures registry entries -/// (which are shared across storefronts) prefer Steam paths, then GOG, then Epic. -pub fn detect_all_games() -> GameScanResult { - let mut result = GameScanResult::default(); - - // Detect in priority order: Steam first, then GOG (via Heroic), then Epic, then Bottles. - let steam_games = detect_steam_games(); - result.steam_count = steam_games.len(); - result.games.extend(steam_games); - - let heroic_games = detect_heroic_games(); - result.heroic_count = heroic_games.len(); - result.games.extend(heroic_games); - - let bottles_games = detect_bottles_games(); - result.bottles_count = bottles_games.len(); - result.games.extend(bottles_games); - - // Deduplicate: keep the first occurrence of each game (by registry_path), - // which respects the detection order (Steam > GOG > Epic > Bottles). - deduplicate_games(&mut result); - - result -} - -/// Remove duplicate game detections, keeping the first (highest-priority) entry. -/// Two games are considered duplicates if they have the same registry_path. -fn deduplicate_games(result: &mut GameScanResult) { - let mut seen_registry_paths = std::collections::HashSet::new(); - result.games.retain(|game| { - if let Some(ref reg_path) = game.registry_path { - seen_registry_paths.insert(reg_path.clone()) - } else { - // Games without registry paths are always kept (no risk of conflict) - true - } - }); -} - -/// Detect only Steam games -pub fn detect_steam_only() -> GameScanResult { - let steam_games = detect_steam_games(); - GameScanResult { - steam_count: steam_games.len(), - games: steam_games, - ..Default::default() - } -} diff --git a/libs/nak/src/game_finder/registry.rs b/libs/nak/src/game_finder/registry.rs deleted file mode 100644 index e843b6e..0000000 --- a/libs/nak/src/game_finder/registry.rs +++ /dev/null @@ -1,227 +0,0 @@ -//! Wine registry parsing utilities -//! -//! Parses Wine registry files (system.reg, user.reg) to find game install paths. - -use std::fs; -use std::path::{Path, PathBuf}; - -use crate::logging::log_warning; - -/// Read a registry value from a Wine prefix registry file -/// -/// # Arguments -/// * `prefix_path` - Path to the Wine prefix (containing system.reg, user.reg) -/// * `key_path` - Registry key path (e.g., "Software\\Bethesda Softworks\\Skyrim") -/// * `value_name` - Name of the value to read (e.g., "Installed Path") -/// -/// Returns the value as a string, or None if not found -pub fn read_registry_value( - prefix_path: &Path, - key_path: &str, - value_name: &str, -) -> Option<String> { - // Try system.reg first (HKEY_LOCAL_MACHINE) - let system_reg = prefix_path.join("system.reg"); - if let Some(value) = read_value_from_reg_file(&system_reg, key_path, value_name) { - return Some(value); - } - - // Try user.reg (HKEY_CURRENT_USER) - let user_reg = prefix_path.join("user.reg"); - if let Some(value) = read_value_from_reg_file(&user_reg, key_path, value_name) { - return Some(value); - } - - None -} - -/// Read a value from a specific .reg file -fn read_value_from_reg_file(reg_file: &Path, key_path: &str, value_name: &str) -> Option<String> { - let content = fs::read_to_string(reg_file).ok()?; - - // Convert the key path to Wine's format - // Wine uses lowercase keys with escaped backslashes - // e.g., [Software\\Bethesda Softworks\\Skyrim] becomes [software\\\\bethesda softworks\\\\skyrim] - let wine_key = format!( - "[{}]", - key_path.to_lowercase().replace('\\', "\\\\") - ); - - // Also try with the Wow6432Node variant for 32-bit apps on 64-bit Wine - let wine_key_wow64 = format!( - "[software\\\\wow6432node\\\\{}]", - key_path - .strip_prefix("Software\\") - .unwrap_or(key_path) - .to_lowercase() - .replace('\\', "\\\\") - ); - - // Find the key section and extract the value - for key_to_find in [&wine_key, &wine_key_wow64] { - if let Some(value) = find_value_in_content(&content, key_to_find, value_name) { - return Some(value); - } - } - - None -} - -/// Find a value within registry file content -fn find_value_in_content(content: &str, key: &str, value_name: &str) -> Option<String> { - let mut in_target_key = false; - let value_name_lower = value_name.to_lowercase(); - - for line in content.lines() { - let trimmed = line.trim(); - - // Check for key header - if trimmed.starts_with('[') && trimmed.ends_with(']') { - in_target_key = trimmed.to_lowercase() == key.to_lowercase(); - continue; - } - - // If we're in the target key, look for the value - if in_target_key { - // Empty line or new section means we've left the key - if trimmed.is_empty() { - continue; - } - if trimmed.starts_with('[') { - break; - } - - // Parse value line: "ValueName"="value" or @="default value" - if let Some((name, value)) = parse_reg_value_line(trimmed) { - if name.to_lowercase() == value_name_lower { - return Some(value); - } - } - } - } - - None -} - -/// Parse a registry value line like "ValueName"="value" -fn parse_reg_value_line(line: &str) -> Option<(String, String)> { - // Format: "name"="value" or "name"=dword:00000000 or @="default" - let line = line.trim(); - - // Find the = separator - let eq_pos = line.find('=')?; - let (name_part, value_part) = line.split_at(eq_pos); - let value_part = &value_part[1..]; // Skip the '=' - - // Extract name (remove quotes) - let name = if name_part == "@" { - "@".to_string() - } else { - name_part.trim().trim_matches('"').to_string() - }; - - // Extract value - let value = if value_part.starts_with('"') { - // String value - need to handle escapes - parse_quoted_reg_value(value_part)? - } else if value_part.starts_with("dword:") { - // DWORD value - convert to decimal string - let hex = value_part.strip_prefix("dword:")?; - let num = u32::from_str_radix(hex, 16).ok()?; - num.to_string() - } else { - // Other types - return as-is - value_part.to_string() - }; - - Some((name, value)) -} - -/// Parse a quoted registry value, handling Wine's escape sequences -fn parse_quoted_reg_value(s: &str) -> Option<String> { - let s = s.trim(); - if !s.starts_with('"') { - return None; - } - - let mut result = String::new(); - let chars = s[1..].chars(); - let mut prev_was_backslash = false; - - for c in chars { - if prev_was_backslash { - match c { - 'n' => result.push('\n'), - 'r' => result.push('\r'), - 't' => result.push('\t'), - '\\' => result.push('\\'), - '"' => result.push('"'), - _ => { - result.push('\\'); - result.push(c); - } - } - prev_was_backslash = false; - } else if c == '\\' { - prev_was_backslash = true; - } else if c == '"' { - break; // End of string - } else { - result.push(c); - } - } - - Some(result) -} - -/// Convert a Wine path (Z:\path\to\file) to a Linux path -pub fn wine_path_to_linux(wine_path: &str) -> Option<PathBuf> { - let path = wine_path.trim(); - - // Handle Z: drive (maps to /) - if path.starts_with("Z:") || path.starts_with("z:") { - let linux_path = path[2..].replace('\\', "/"); - return Some(PathBuf::from(linux_path)); - } - - // Handle C: drive (maps to prefix/drive_c) - // Note: This requires knowing the prefix path, so we can't convert it here - // For now, we'll just return None for C: paths - if path.starts_with("C:") || path.starts_with("c:") { - log_warning(&format!( - "Cannot convert C: drive path without prefix: {}", - path - )); - return None; - } - - None -} - -/// Check if a Wine prefix contains a specific game by registry key -pub fn has_game_registry( - prefix_path: &Path, - registry_path: &str, - registry_value: &str, -) -> bool { - read_registry_value(prefix_path, registry_path, registry_value).is_some() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_reg_value_line() { - let (name, value) = - parse_reg_value_line(r#""Installed Path"="Z:\\mnt\\games\\Skyrim""#).unwrap(); - assert_eq!(name, "Installed Path"); - assert_eq!(value, r"Z:\mnt\games\Skyrim"); - } - - #[test] - fn test_wine_path_to_linux() { - let linux = wine_path_to_linux(r"Z:\mnt\games\Skyrim").unwrap(); - assert_eq!(linux, PathBuf::from("/mnt/games/Skyrim")); - } -} diff --git a/libs/nak/src/game_finder/steam.rs b/libs/nak/src/game_finder/steam.rs deleted file mode 100644 index d403cdf..0000000 --- a/libs/nak/src/game_finder/steam.rs +++ /dev/null @@ -1,253 +0,0 @@ -//! Steam game detection -//! -//! Detects games installed via Steam by parsing appmanifest_*.acf files. -//! Supports native, Flatpak, and Snap Steam installations. - -use std::fs; -use std::path::{Path, PathBuf}; - -use super::known_games::{find_by_steam_id, KnownGame}; -use super::vdf::{parse_library_folders, AppManifest}; -use super::{Game, Launcher}; -use crate::logging::log_info; - -/// All possible Steam installation paths to check -const STEAM_PATHS: &[&str] = &[ - ".local/share/Steam", - ".steam/debian-installation", - ".steam/steam", - ".var/app/com.valvesoftware.Steam/data/Steam", - ".var/app/com.valvesoftware.Steam/.local/share/Steam", - "snap/steam/common/.local/share/Steam", -]; - -/// Detect all Steam games across all installations -pub fn detect_steam_games() -> Vec<Game> { - let mut games = Vec::new(); - let home = match std::env::var("HOME") { - Ok(h) => h, - Err(_) => return games, - }; - - // Find all Steam installations - for steam_info in find_steam_installations(&home) { - let libraries = get_library_folders(&steam_info.path); - - // Collect all steamapps paths so we can search across them for compatdata. - // Steam can place compatdata in a different library folder than the game. - let all_steamapps: Vec<PathBuf> = libraries - .iter() - .map(|l| l.join("steamapps")) - .filter(|s| s.exists()) - .collect(); - - for steamapps in &all_steamapps { - // Scan for appmanifest_*.acf files - let Ok(entries) = fs::read_dir(steamapps) else { - continue; - }; - - for entry in entries.flatten() { - let path = entry.path(); - let Some(name) = path.file_name().and_then(|n| n.to_str()) else { - continue; - }; - - if name.starts_with("appmanifest_") && name.ends_with(".acf") { - if let Some(game) = - parse_appmanifest(&path, steamapps, &all_steamapps, &steam_info) - { - games.push(game); - } - } - } - } - } - - log_info(&format!("Steam: Found {} installed games", games.len())); - games -} - -/// Information about a Steam installation -struct SteamInstallation { - path: PathBuf, - is_flatpak: bool, - is_snap: bool, -} - -/// Find all Steam installations on the system -fn find_steam_installations(home: &str) -> Vec<SteamInstallation> { - let mut installations = Vec::new(); - - for relative_path in STEAM_PATHS { - let full_path = PathBuf::from(home).join(relative_path); - - // Check if this is a valid Steam installation - if full_path.join("steamapps").exists() || full_path.join("steam.pid").exists() { - let is_flatpak = relative_path.contains(".var/app/com.valvesoftware.Steam"); - let is_snap = relative_path.contains("snap/steam"); - - // Avoid duplicates (symlinks can cause the same installation to appear twice) - let canonical = full_path.canonicalize().unwrap_or(full_path.clone()); - if !installations.iter().any(|i: &SteamInstallation| { - i.path.canonicalize().unwrap_or(i.path.clone()) == canonical - }) { - log_info(&format!( - "Found Steam installation: {} (flatpak={}, snap={})", - full_path.display(), - is_flatpak, - is_snap - )); - installations.push(SteamInstallation { - path: full_path, - is_flatpak, - is_snap, - }); - } - } - } - - installations -} - -/// Get all library folders for a Steam installation -fn get_library_folders(steam_path: &Path) -> Vec<PathBuf> { - let mut folders = Vec::new(); - - // The Steam installation directory itself is always a library - folders.push(steam_path.to_path_buf()); - - // Parse libraryfolders.vdf for additional libraries - let vdf_path = steam_path.join("steamapps/libraryfolders.vdf"); - if let Ok(content) = fs::read_to_string(&vdf_path) { - for path_str in parse_library_folders(&content) { - let path = PathBuf::from(&path_str); - if path.exists() && !folders.contains(&path) { - folders.push(path); - } - } - } - - // Also check the older config/libraryfolders.vdf location - let old_vdf_path = steam_path.join("config/libraryfolders.vdf"); - if old_vdf_path != vdf_path { - if let Ok(content) = fs::read_to_string(&old_vdf_path) { - for path_str in parse_library_folders(&content) { - let path = PathBuf::from(&path_str); - if path.exists() && !folders.contains(&path) { - folders.push(path); - } - } - } - } - - folders -} - -/// Parse an appmanifest_*.acf file and create a Game struct -fn parse_appmanifest( - manifest_path: &Path, - steamapps_path: &Path, - all_steamapps: &[PathBuf], - steam_info: &SteamInstallation, -) -> Option<Game> { - let content = fs::read_to_string(manifest_path).ok()?; - let manifest = AppManifest::from_vdf(&content)?; - - // Only consider fully installed games - if !manifest.is_installed() { - return None; - } - - // Build the install path - let install_path = steamapps_path.join("common").join(&manifest.install_dir); - if !install_path.exists() { - return None; - } - - // Search ALL library folders for the compatdata — Steam can place it in a - // different library than the game itself (e.g. game on SD card, compatdata - // on internal storage or vice-versa). - let prefix_path = all_steamapps - .iter() - .map(|sa| sa.join("compatdata").join(&manifest.app_id).join("pfx")) - .find(|p| p.exists()); - - // Look up known game info - let known_game = find_by_steam_id(&manifest.app_id); - - Some(Game { - name: manifest.name, - app_id: manifest.app_id, - install_path, - prefix_path, - launcher: Launcher::Steam { - is_flatpak: steam_info.is_flatpak, - is_snap: steam_info.is_snap, - }, - my_games_folder: known_game.and_then(|g| g.my_games_folder.map(String::from)), - appdata_local_folder: known_game.and_then(|g| g.appdata_local_folder.map(String::from)), - appdata_roaming_folder: known_game.and_then(|g| g.appdata_roaming_folder.map(String::from)), - registry_path: known_game.map(|g| g.registry_path.to_string()), - registry_value: known_game.map(|g| g.registry_value.to_string()), - }) -} - -/// Find the installation path for a specific Steam game by App ID -pub fn find_game_install_path(app_id: &str) -> Option<PathBuf> { - let home = std::env::var("HOME").ok()?; - - for steam_info in find_steam_installations(&home) { - let libraries = get_library_folders(&steam_info.path); - - for library_path in libraries { - let manifest_path = library_path - .join("steamapps") - .join(format!("appmanifest_{}.acf", app_id)); - - if manifest_path.exists() { - let content = fs::read_to_string(&manifest_path).ok()?; - let manifest = AppManifest::from_vdf(&content)?; - - if manifest.is_installed() { - let install_path = library_path - .join("steamapps/common") - .join(&manifest.install_dir); - - if install_path.exists() { - return Some(install_path); - } - } - } - } - } - - None -} - -/// Find the Wine prefix for a specific Steam game by App ID -pub fn find_game_prefix_path(app_id: &str) -> Option<PathBuf> { - let home = std::env::var("HOME").ok()?; - - for steam_info in find_steam_installations(&home) { - let libraries = get_library_folders(&steam_info.path); - - for library_path in libraries { - let prefix_path = library_path - .join("steamapps/compatdata") - .join(app_id) - .join("pfx"); - - if prefix_path.exists() { - return Some(prefix_path); - } - } - } - - None -} - -/// Get the known game configuration for a Steam App ID -pub fn get_known_game(app_id: &str) -> Option<&'static KnownGame> { - find_by_steam_id(app_id) -} diff --git a/libs/nak/src/game_finder/vdf.rs b/libs/nak/src/game_finder/vdf.rs deleted file mode 100644 index c296f27..0000000 --- a/libs/nak/src/game_finder/vdf.rs +++ /dev/null @@ -1,254 +0,0 @@ -//! Hand-rolled VDF (Valve Data Format) parser -//! -//! Parses VDF files like appmanifest_*.acf and libraryfolders.vdf -//! without external dependencies. - -use std::collections::HashMap; - -/// A VDF value - either a string or a nested object -#[derive(Debug, Clone)] -pub enum VdfValue { - String(String), - Object(HashMap<String, VdfValue>), -} - -impl VdfValue { - /// Get as string reference - pub fn as_str(&self) -> Option<&str> { - match self { - VdfValue::String(s) => Some(s), - VdfValue::Object(_) => None, - } - } - - /// Get as object reference - pub fn as_object(&self) -> Option<&HashMap<String, VdfValue>> { - match self { - VdfValue::String(_) => None, - VdfValue::Object(o) => Some(o), - } - } - - /// Get a nested value by key - pub fn get(&self, key: &str) -> Option<&VdfValue> { - self.as_object()?.get(key) - } - - /// Get a string value by key - pub fn get_str(&self, key: &str) -> Option<&str> { - self.get(key)?.as_str() - } -} - -/// Parse a VDF file content into a root object -pub fn parse_vdf(content: &str) -> Option<VdfValue> { - let mut chars = content.chars().peekable(); - parse_object(&mut chars) -} - -/// Parse an object (including the root level) -fn parse_object<I: Iterator<Item = char>>(chars: &mut std::iter::Peekable<I>) -> Option<VdfValue> { - let mut map = HashMap::new(); - - loop { - skip_whitespace_and_comments(chars); - - match chars.peek() { - None => break, - Some('}') => { - chars.next(); - break; - } - Some('"') => { - // Parse key - let key = parse_quoted_string(chars)?; - skip_whitespace_and_comments(chars); - - // Check what follows - string value or object - match chars.peek() { - Some('"') => { - // String value - let value = parse_quoted_string(chars)?; - map.insert(key, VdfValue::String(value)); - } - Some('{') => { - // Object value - chars.next(); // consume '{' - let value = parse_object(chars)?; - map.insert(key, value); - } - _ => return None, // Unexpected token - } - } - _ => { - // Skip unexpected characters - chars.next(); - } - } - } - - Some(VdfValue::Object(map)) -} - -/// Parse a quoted string "..." -fn parse_quoted_string<I: Iterator<Item = char>>( - chars: &mut std::iter::Peekable<I>, -) -> Option<String> { - // Expect opening quote - if chars.next() != Some('"') { - return None; - } - - let mut result = String::new(); - - loop { - match chars.next() { - None => return None, // Unterminated string - Some('"') => break, - Some('\\') => { - // Handle escape sequences - match chars.next() { - Some('n') => result.push('\n'), - Some('t') => result.push('\t'), - Some('\\') => result.push('\\'), - Some('"') => result.push('"'), - Some(c) => { - result.push('\\'); - result.push(c); - } - None => return None, - } - } - Some(c) => result.push(c), - } - } - - Some(result) -} - -/// Skip whitespace and // comments -fn skip_whitespace_and_comments<I: Iterator<Item = char>>(chars: &mut std::iter::Peekable<I>) { - loop { - // Skip whitespace - while chars.peek().is_some_and(|c| c.is_whitespace()) { - chars.next(); - } - - // Check for // comment - // Need to peek ahead without cloning - consume first / and check second - if chars.peek() == Some(&'/') { - // Consume first / - chars.next(); - if chars.peek() == Some(&'/') { - // It's a comment - skip to end of line - chars.next(); - while chars.peek().is_some_and(|c| *c != '\n') { - chars.next(); - } - continue; - } - // Not a comment, but we consumed a '/' - this shouldn't happen in valid VDF - // Just continue processing - } - - break; - } -} - -/// Parse an appmanifest_*.acf file and extract app info -#[derive(Debug, Clone)] -pub struct AppManifest { - pub app_id: String, - pub name: String, - pub install_dir: String, - pub state_flags: u32, -} - -impl AppManifest { - /// Parse from VDF content - pub fn from_vdf(content: &str) -> Option<Self> { - let root = parse_vdf(content)?; - let app_state = root.get("AppState")?; - - Some(Self { - app_id: app_state.get_str("appid")?.to_string(), - name: app_state.get_str("name")?.to_string(), - install_dir: app_state.get_str("installdir")?.to_string(), - state_flags: app_state.get_str("StateFlags")?.parse().unwrap_or(0), - }) - } - - /// Check if the game is fully installed (StateFlags == 4) - pub fn is_installed(&self) -> bool { - self.state_flags == 4 - } -} - -/// Parse libraryfolders.vdf and extract library paths -pub fn parse_library_folders(content: &str) -> Vec<String> { - let mut paths = Vec::new(); - - let Some(root) = parse_vdf(content) else { - return paths; - }; - - let Some(library_folders) = root.get("libraryfolders").and_then(|v| v.as_object()) else { - return paths; - }; - - // Library folders are keyed by index: "0", "1", "2", etc. - for value in library_folders.values() { - if let Some(path) = value.get_str("path") { - paths.push(path.to_string()); - } - } - - paths -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_appmanifest() { - let content = r#" -"AppState" -{ - "appid" "489830" - "Universe" "1" - "name" "Skyrim Special Edition" - "StateFlags" "4" - "installdir" "Skyrim Special Edition" -} -"#; - let manifest = AppManifest::from_vdf(content).unwrap(); - assert_eq!(manifest.app_id, "489830"); - assert_eq!(manifest.name, "Skyrim Special Edition"); - assert_eq!(manifest.install_dir, "Skyrim Special Edition"); - assert!(manifest.is_installed()); - } - - #[test] - fn test_parse_library_folders() { - let content = r#" -"libraryfolders" -{ - "0" - { - "path" "/home/user/.local/share/Steam" - "label" "" - } - "1" - { - "path" "/mnt/games/SteamLibrary" - "label" "Games" - } -} -"#; - let paths = parse_library_folders(content); - assert_eq!(paths.len(), 2); - assert!(paths.contains(&"/home/user/.local/share/Steam".to_string())); - assert!(paths.contains(&"/mnt/games/SteamLibrary".to_string())); - } -} diff --git a/libs/nak/src/icons.rs b/libs/nak/src/icons.rs deleted file mode 100644 index 940d0d1..0000000 --- a/libs/nak/src/icons.rs +++ /dev/null @@ -1,195 +0,0 @@ -//! Extract icons from Windows PE (.exe/.dll) files using pelite.
-//!
-//! Returns raw ICO file bytes that can be loaded directly by Qt's QIcon/QImage.
-
-use std::path::Path;
-
-use pelite::resources::{Entry, Name, Resources};
-
-/// Extract the best (largest) icon from a PE executable as raw ICO bytes.
-///
-/// Returns `None` if the file can't be read, isn't a valid PE, or has no icons.
-pub fn extract_icon(exe_path: &Path) -> Option<Vec<u8>> {
- let data = std::fs::read(exe_path).ok()?;
-
- // Try PE32+ (64-bit) first, fall back to PE32 (32-bit)
- if let Ok(icon) = extract_from_pe64(&data) {
- return Some(icon);
- }
- if let Ok(icon) = extract_from_pe32(&data) {
- return Some(icon);
- }
-
- None
-}
-
-fn extract_from_pe64(data: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
- use pelite::pe64::{Pe, PeFile};
- let pe = PeFile::from_bytes(data)?;
- let resources = pe.resources()?;
- build_ico_from_resources(&resources)
-}
-
-fn extract_from_pe32(data: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
- use pelite::pe32::{Pe, PeFile};
- let pe = PeFile::from_bytes(data)?;
- let resources = pe.resources()?;
- build_ico_from_resources(&resources)
-}
-
-/// Build an ICO file from PE resources, picking the group icon with the
-/// largest total pixel area.
-fn build_ico_from_resources(
- resources: &Resources,
-) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
- let root = resources.root()?;
-
- // Find RT_GROUP_ICON directory (type 14)
- let group_icon_dir = root
- .get_dir(Name::Id(14))
- .map_err(|_| "no RT_GROUP_ICON")?;
-
- let mut best_group: Option<(i32, Vec<u8>)> = None;
-
- for dir_entry in group_icon_dir.entries() {
- // Each entry is a group; get its subdirectory (language variants)
- let sub_dir = match dir_entry.entry() {
- Ok(Entry::Directory(d)) => d,
- _ => continue,
- };
-
- // Get the first language variant's data
- let data_entry = match sub_dir.entries().next() {
- Some(e) => match e.entry() {
- Ok(Entry::DataEntry(d)) => d,
- _ => continue,
- },
- None => continue,
- };
-
- let grp_data = match data_entry.bytes() {
- Ok(d) => d,
- Err(_) => continue,
- };
-
- // Parse GRPICONDIR header: reserved(2) + type(2) + count(2) = 6 bytes
- if grp_data.len() < 6 {
- continue;
- }
- let count = u16::from_le_bytes([grp_data[4], grp_data[5]]) as usize;
- // Each GRPICONDIRENTRY is 14 bytes
- if grp_data.len() < 6 + count * 14 {
- continue;
- }
-
- // Calculate total pixel area for ranking
- let mut total_area: i32 = 0;
- for i in 0..count {
- let offset = 6 + i * 14;
- let w = if grp_data[offset] == 0 {
- 256i32
- } else {
- grp_data[offset] as i32
- };
- let h = if grp_data[offset + 1] == 0 {
- 256i32
- } else {
- grp_data[offset + 1] as i32
- };
- total_area += w * h;
- }
-
- if let Ok(ico_bytes) = build_ico_file(resources, grp_data, count) {
- if best_group.is_none() || total_area > best_group.as_ref().unwrap().0 {
- best_group = Some((total_area, ico_bytes));
- }
- }
- }
-
- best_group
- .map(|(_, bytes)| bytes)
- .ok_or_else(|| "no valid icon group found".into())
-}
-
-/// Build a complete ICO file from a GRPICONDIR and the corresponding RT_ICON resources.
-fn build_ico_file(
- resources: &Resources,
- grp_data: &[u8],
- count: usize,
-) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
- let root = resources.root()?;
-
- // RT_ICON = type 3
- let icon_dir = root.get_dir(Name::Id(3)).map_err(|_| "no RT_ICON")?;
-
- // Collect individual icon image data
- struct IconEntry<'a> {
- header: [u8; 8], // first 8 bytes of GRPICONDIRENTRY (w, h, colors, reserved, planes, bpp)
- data: &'a [u8],
- }
- let mut entries: Vec<IconEntry> = Vec::new();
-
- for i in 0..count {
- let grp_offset = 6 + i * 14;
- let icon_id =
- u16::from_le_bytes([grp_data[grp_offset + 12], grp_data[grp_offset + 13]]) as u32;
-
- // Find the RT_ICON with this ID
- let icon_sub = match icon_dir.get_dir(Name::Id(icon_id)) {
- Ok(d) => d,
- Err(_) => continue,
- };
-
- let data_entry = match icon_sub.entries().next() {
- Some(e) => match e.entry() {
- Ok(Entry::DataEntry(d)) => d,
- _ => continue,
- },
- None => continue,
- };
-
- let image_data = match data_entry.bytes() {
- Ok(d) => d,
- Err(_) => continue,
- };
-
- let mut header = [0u8; 8];
- header.copy_from_slice(&grp_data[grp_offset..grp_offset + 8]);
-
- entries.push(IconEntry {
- header,
- data: image_data,
- });
- }
-
- if entries.is_empty() {
- return Err("no icon entries found".into());
- }
-
- let entry_count = entries.len() as u16;
- let header_size = 6 + (entry_count as usize) * 16; // ICONDIR + ICONDIRENTRYs
-
- let mut ico = Vec::new();
-
- // ICONDIR header
- ico.extend_from_slice(&0u16.to_le_bytes()); // reserved
- ico.extend_from_slice(&1u16.to_le_bytes()); // type = ICO
- ico.extend_from_slice(&entry_count.to_le_bytes());
-
- // Write ICONDIRENTRY records (16 bytes each)
- let mut data_offset = header_size as u32;
- for entry in &entries {
- ico.extend_from_slice(&entry.header); // width, height, colors, reserved, planes, bpp (8 bytes)
- let size = entry.data.len() as u32;
- ico.extend_from_slice(&size.to_le_bytes()); // dwBytesInRes (4 bytes)
- ico.extend_from_slice(&data_offset.to_le_bytes()); // dwImageOffset (4 bytes)
- data_offset += size;
- }
-
- // Write image data
- for entry in &entries {
- ico.extend_from_slice(entry.data);
- }
-
- Ok(ico)
-}
diff --git a/libs/nak/src/installers/mod.rs b/libs/nak/src/installers/mod.rs deleted file mode 100644 index cf3eb85..0000000 --- a/libs/nak/src/installers/mod.rs +++ /dev/null @@ -1,334 +0,0 @@ -//! Mod manager installation logic -//! -//! Stripped for Fluorine: no common.rs, mo2.rs, plugin.rs, compatdata_scanner.rs. - -pub mod symlinks; - -mod prefix_setup; - -pub use prefix_setup::{ - apply_dpi, apply_registry_for_game_path, auto_apply_game_registries, cleanup_prefix_drives, - install_all_dependencies, kill_wineserver, known_game_names, launch_dpi_test_app, DPI_PRESETS, -}; - -use std::error::Error; -use std::fs; -use std::sync::atomic::AtomicBool; -use std::sync::Arc; - -use crate::logging::log_install; -use crate::steam::SteamProton; - -// ============================================================================ -// Shared Types -// ============================================================================ - -/// Context for background installation tasks -#[derive(Clone)] -pub struct TaskContext { - pub status_callback: Arc<dyn Fn(String) + Send + Sync>, - pub log_callback: Arc<dyn Fn(String) + Send + Sync>, - pub progress_callback: Arc<dyn Fn(f32) + Send + Sync>, - pub cancel_flag: Arc<AtomicBool>, -} - -impl TaskContext { - pub fn new( - status: impl Fn(String) + Send + Sync + 'static, - log: impl Fn(String) + Send + Sync + 'static, - progress: impl Fn(f32) + Send + Sync + 'static, - cancel: Arc<AtomicBool>, - ) -> Self { - Self { - status_callback: Arc::new(status), - log_callback: Arc::new(log), - progress_callback: Arc::new(progress), - cancel_flag: cancel, - } - } - - pub fn set_status(&self, msg: String) { - (self.status_callback)(msg); - } - - pub fn log(&self, msg: String) { - (self.log_callback)(msg); - } - - pub fn set_progress(&self, p: f32) { - (self.progress_callback)(p); - } - - pub fn is_cancelled(&self) -> bool { - self.cancel_flag.load(std::sync::atomic::Ordering::Relaxed) - } - - /// Run a command that can be killed if the user cancels. - pub fn run_cancellable(&self, mut cmd: std::process::Command) -> Result<std::process::ExitStatus, Box<dyn std::error::Error>> { - let mut child = cmd.spawn()?; - - loop { - match child.try_wait()? { - Some(status) => return Ok(status), - None => { - if self.is_cancelled() { - let _ = child.kill(); - let _ = child.wait(); - return Err("Cancelled".into()); - } - std::thread::sleep(std::time::Duration::from_millis(250)); - } - } - } - } -} - -// ============================================================================ -// Shared Wine Registry Settings -// ============================================================================ - -/// Wine registry settings -pub const WINE_SETTINGS_REG: &str = r#"Windows Registry Editor Version 5.00 - -[HKEY_CURRENT_USER\Software\Wine\DllOverrides] -"dwrite.dll"="native,builtin" -"dwrite"="native,builtin" -"winmm.dll"="native,builtin" -"winmm"="native,builtin" -"version.dll"="native,builtin" -"version"="native,builtin" -"ArchiveXL.dll"="native,builtin" -"ArchiveXL"="native,builtin" -"Codeware.dll"="native,builtin" -"Codeware"="native,builtin" -"TweakXL.dll"="native,builtin" -"TweakXL"="native,builtin" -"input_loader.dll"="native,builtin" -"input_loader"="native,builtin" -"RED4ext.dll"="native,builtin" -"RED4ext"="native,builtin" -"mod_settings.dll"="native,builtin" -"mod_settings"="native,builtin" -"scc_lib.dll"="native,builtin" -"scc_lib"="native,builtin" -"dxgi.dll"="native,builtin" -"dxgi"="native,builtin" -"dbghelp.dll"="native,builtin" -"dbghelp"="native,builtin" -"d3d12.dll"="native,builtin" -"d3d12"="native,builtin" -"wininet.dll"="native,builtin" -"wininet"="native,builtin" -"winhttp.dll"="native,builtin" -"winhttp"="native,builtin" -"dinput.dll"="native,builtin" -"dinput8"="native,builtin" -"dinput8.dll"="native,builtin" - -[HKEY_CURRENT_USER\Software\Wine] -"ShowDotFiles"="Y" - -[HKEY_CURRENT_USER\Control Panel\Desktop] -"FontSmoothing"="2" -"FontSmoothingGamma"=dword:00000578 -"FontSmoothingOrientation"=dword:00000001 -"FontSmoothingType"=dword:00000002 - -[HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers] -@="~ HIGHDPIAWARE" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\Pandora Behaviour Engine+.exe\X11 Driver] -"Decorated"="N" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\Vortex.exe\X11 Driver] -"Decorated"="N" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\SSEEdit.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\SSEEdit64.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\FO4Edit.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\FO4Edit64.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\TES4Edit.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\TES4Edit64.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\xEdit64.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\SF1Edit64.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\FNVEdit.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\FNVEdit64.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\xFOEdit.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\xFOEdit64.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\xSFEEdit.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\xSFEEdit64.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\xTESEdit.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\xTESEdit64.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\FO3Edit.exe] -"Version"="winxp" - -[HKEY_CURRENT_USER\Software\Wine\AppDefaults\FO3Edit64.exe] -"Version"="winxp" - -; ============================================================================= -; Native file browser integration (opens folders in native file manager) -; ============================================================================= -[HKEY_CLASSES_ROOT\Folder\shell\explore\command] -@="C:\\windows\\system32\\winebrowser.exe -nohome \"%1\"" - -[HKEY_CLASSES_ROOT\Directory\shell\explore\command] -@="C:\\windows\\system32\\winebrowser.exe -nohome \"%1\"" - -[HKEY_CLASSES_ROOT\Folder\shell\open\command] -@="C:\\windows\\system32\\winebrowser.exe -nohome \"%1\"" - -[HKEY_CLASSES_ROOT\Directory\shell\open\command] -@="C:\\windows\\system32\\winebrowser.exe -nohome \"%1\"" - -; ============================================================================= -; Native text editor integration (opens text files in native editor) -; ============================================================================= -[HKEY_CLASSES_ROOT\txtfile\shell\open\command] -@="C:\\windows\\system32\\winebrowser.exe \"%1\"" - -[HKEY_CLASSES_ROOT\inifile\shell\open\command] -@="C:\\windows\\system32\\winebrowser.exe \"%1\"" - -[HKEY_CLASSES_ROOT\.txt] -@="txtfile" - -[HKEY_CLASSES_ROOT\.ini] -@="inifile" - -[HKEY_CLASSES_ROOT\.cfg] -@="txtfile" - -[HKEY_CLASSES_ROOT\.log] -@="txtfile" - -[HKEY_CLASSES_ROOT\.xml] -@="txtfile" - -[HKEY_CLASSES_ROOT\.json] -@="txtfile" - -[HKEY_CLASSES_ROOT\.yml] -@="txtfile" - -[HKEY_CLASSES_ROOT\.yaml] -@="txtfile" -"#; - -// ============================================================================ -// Shared Functions -// ============================================================================ - -/// Apply Wine registry settings to a prefix -pub fn apply_wine_registry_settings( - prefix_path: &std::path::Path, - proton: &SteamProton, - log_callback: &impl Fn(String), - _app_id: Option<u32>, -) -> Result<(), Box<dyn Error>> { - use std::io::Write; - use crate::config::AppConfig; - use crate::logging::{log_error, log_warning}; - use crate::runtime_wrap; - - let tmp_dir = AppConfig::get_tmp_path(); - fs::create_dir_all(&tmp_dir)?; - let reg_file = tmp_dir.join("wine_settings.reg"); - - let mut file = fs::File::create(®_file)?; - file.write_all(WINE_SETTINGS_REG.as_bytes())?; - - let wine_bin = proton.wine_binary().ok_or_else(|| { - let err_msg = format!( - "Wine binary not found for Proton '{}' (checked files/bin/wine and dist/bin/wine)", - proton.name - ); - log_callback(format!("Error: {}", err_msg)); - err_msg - })?; - - let wineserver_bin = proton.wineserver_binary().unwrap_or_else(|| { - wine_bin.with_file_name("wineserver") - }); - - let bin_dir = proton.bin_dir().ok_or_else(|| { - let err_msg = "Could not determine Proton bin directory"; - log_callback(format!("Error: {}", err_msg)); - err_msg - })?; - - let path_env = format!( - "{}:{}", - bin_dir.to_string_lossy(), - std::env::var("PATH").unwrap_or_default() - ); - - log_callback("Applying Wine registry settings...".to_string()); - log_install("Running wine regedit..."); - - let reg_envs: Vec<(&str, String)> = vec![ - ("WINEPREFIX", prefix_path.display().to_string()), - ("WINE", wine_bin.display().to_string()), - ("WINESERVER", wineserver_bin.display().to_string()), - ("PATH", path_env), - ("WINEDLLOVERRIDES", "mshtml=d".to_string()), - ("PROTON_USE_XALIA", "0".to_string()), - ]; - let regedit_status = runtime_wrap::build_command(&wine_bin, ®_envs) - .arg("regedit") - .arg(®_file) - .status(); - - match regedit_status { - Ok(status) => { - if status.success() { - log_callback("Registry settings applied successfully".to_string()); - log_install("Wine registry settings applied successfully"); - } else { - let msg = format!("regedit exited with code {:?}", status.code()); - log_callback(format!("Warning: {}", msg)); - log_warning(&msg); - } - } - Err(e) => { - let msg = format!("Failed to run regedit: {}", e); - log_callback(format!("Error: {}", msg)); - log_error(&msg); - return Err(msg.into()); - } - } - - let _ = fs::remove_file(®_file); - Ok(()) -} diff --git a/libs/nak/src/installers/prefix_setup.rs b/libs/nak/src/installers/prefix_setup.rs deleted file mode 100644 index 6c48b09..0000000 --- a/libs/nak/src/installers/prefix_setup.rs +++ /dev/null @@ -1,764 +0,0 @@ -//! Unified prefix setup for MO2 -//! -//! This module handles all the dependency installation logic. -//! -//! Key approach (ORDER MATTERS): -//! 1. Install dependencies via winetricks (handles wineboot internally) -//! 2. Install custom dotnet runtimes (dotnet9sdk, dotnetdesktop10) -//! 3. Auto-detect installed games and apply registry entries -//! 4. Apply Wine registry settings (LAST - after prefix is fully set up) - -use std::error::Error; -use std::fs; -use std::path::Path; -use std::process::Child; - -use super::{apply_wine_registry_settings, TaskContext}; -use crate::config::AppConfig; -use crate::deps::{install_standard_deps_cancellable, STANDARD_VERBS}; -use crate::game_finder::{detect_all_games, known_games, Game, Launcher}; -use crate::logging::{log_install, log_warning}; -use crate::runtime_wrap; -use crate::steam::{detect_steam_path_checked, SteamProton}; - -// ============================================================================= -// Constants -// ============================================================================= - -/// .NET 9 SDK download URL -const DOTNET9_SDK_URL: &str = "https://builds.dotnet.microsoft.com/dotnet/Sdk/9.0.310/dotnet-sdk-9.0.310-win-x64.exe"; - -/// .NET Desktop Runtime 10 download URL -const DOTNET_DESKTOP10_URL: &str = "https://builds.dotnet.microsoft.com/dotnet/WindowsDesktop/10.0.2/windowsdesktop-runtime-10.0.2-win-x64.exe"; - -/// Drive letters to keep in the prefix (c: is Windows root, z: maps to Linux /) -const ALLOWED_DRIVE_LETTERS: &[&str] = &["c:", "z:"]; - -/// Install all dependencies to a prefix. -/// -/// Order: proton init → winetricks → custom dotnet → game detection → registry → win11 → dotnet fixes -/// -/// # Arguments -/// * `app_id` - Steam AppID (used for registry operations) -pub fn install_all_dependencies( - prefix_root: &Path, - install_proton: &SteamProton, - ctx: &TaskContext, - start_progress: f32, - end_progress: f32, - app_id: u32, -) -> Result<(), Box<dyn Error>> { - fs::create_dir_all(AppConfig::get_tmp_path())?; - - // Progress distribution - let init_end = start_progress + (end_progress - start_progress) * 0.10; - let winetricks_end = start_progress + (end_progress - start_progress) * 0.50; - let dotnet_end = start_progress + (end_progress - start_progress) * 0.65; - let games_end = start_progress + (end_progress - start_progress) * 0.75; - - // ========================================================================= - // 0. Initialize prefix with Proton wrapper (creates proper prefix structure) - // ========================================================================= - ctx.set_status("Setting up Windows compatibility layer...".to_string()); - ctx.log("Initializing Wine prefix with Proton...".to_string()); - log_install("Running proton wineboot to initialize prefix"); - - if let Err(e) = initialize_prefix_with_proton(prefix_root, install_proton, app_id, ctx) { - ctx.log(format!("Warning: Proton prefix init failed: {}", e)); - log_warning(&format!("Proton prefix init failed: {}", e)); - // Continue anyway - winetricks might still work - } - - ctx.set_progress(init_end); - - if ctx.is_cancelled() { - return Err("Cancelled".into()); - } - - // ========================================================================= - // 0.5. Clean up unwanted drive letters (keep only C: and Z:) - // ========================================================================= - ctx.set_status("Optimizing prefix configuration...".to_string()); - ctx.log("Removing unwanted drive letters (keeping C: and Z:)...".to_string()); - log_install("Cleaning up Wine drive letters"); - - if let Err(e) = cleanup_wine_drives(prefix_root, install_proton) { - ctx.log(format!("Warning: Drive cleanup had issues: {}", e)); - log_warning(&format!("Drive cleanup failed: {}", e)); - } - - if ctx.is_cancelled() { - return Err("Cancelled".into()); - } - - // ========================================================================= - // 1. Standard Dependencies via Winetricks - // ========================================================================= - ctx.set_status("Installing required Windows components (this may take several minutes)...".to_string()); - ctx.log(format!( - "Installing {} dependencies via winetricks: {}", - STANDARD_VERBS.len(), - STANDARD_VERBS.join(", ") - )); - log_install(&format!("Running winetricks with {} verbs", STANDARD_VERBS.len())); - - let winetricks_log_cb = { - let ctx = ctx.clone(); - move |msg: String| { - ctx.log(msg.clone()); - ctx.set_status(msg); - } - }; - - if let Err(e) = install_standard_deps_cancellable(prefix_root, install_proton, winetricks_log_cb, &ctx.cancel_flag) { - let msg = format!("Winetricks installation had issues: {}", e); - ctx.log(format!("Warning: {}", msg)); - log_warning(&msg); - } - - ctx.set_progress(winetricks_end); - - if ctx.is_cancelled() { - return Err("Cancelled".into()); - } - - // ========================================================================= - // 2. Custom .NET Runtimes (not in winetricks yet) - // ========================================================================= - ctx.set_status("Installing .NET runtime (1 of 2)...".to_string()); - ctx.log("Installing .NET 9 SDK...".to_string()); - - if let Err(e) = install_dotnet_runtime(prefix_root, install_proton, DOTNET9_SDK_URL, "dotnet-sdk-9", ctx) { - ctx.log(format!("Warning: .NET 9 SDK install failed: {}", e)); - log_warning(&format!(".NET 9 SDK install failed: {}", e)); - } - - ctx.set_status("Installing .NET runtime (2 of 2)...".to_string()); - ctx.log("Installing .NET Desktop Runtime 10...".to_string()); - - if let Err(e) = install_dotnet_runtime(prefix_root, install_proton, DOTNET_DESKTOP10_URL, "dotnet-desktop-10", ctx) { - ctx.log(format!("Warning: .NET Desktop 10 install failed: {}", e)); - log_warning(&format!(".NET Desktop 10 install failed: {}", e)); - } - - ctx.set_progress(dotnet_end); - - if ctx.is_cancelled() { - return Err("Cancelled".into()); - } - - // ========================================================================= - // 3. Auto-detect and register installed games - // ========================================================================= - ctx.set_status("Detecting your installed games...".to_string()); - ctx.log("Auto-detecting installed Steam games...".to_string()); - log_install("Auto-detecting installed games for registry"); - - let game_log_cb = { - let ctx = ctx.clone(); - move |msg: String| ctx.log(msg) - }; - auto_apply_game_registries(prefix_root, install_proton, &game_log_cb, Some(app_id)); - - ctx.set_progress(games_end); - - if ctx.is_cancelled() { - return Err("Cancelled".into()); - } - - // ========================================================================= - // 4. Registry Settings (after prefix is fully initialized) - // ========================================================================= - ctx.set_status("Configuring Windows registry...".to_string()); - ctx.log("Applying Wine Registry Settings...".to_string()); - log_install("Applying Wine registry settings"); - - let log_cb = { - let ctx = ctx.clone(); - move |msg: String| ctx.log(msg) - }; - apply_wine_registry_settings(prefix_root, install_proton, &log_cb, Some(app_id))?; - - if ctx.is_cancelled() { - return Err("Cancelled".into()); - } - - // ========================================================================= - // 5. Set Windows 11 Mode - // ========================================================================= - ctx.set_status("Finalizing compatibility settings...".to_string()); - ctx.log("Setting Windows 11 mode...".to_string()); - log_install("Setting Windows 11 mode via winetricks"); - - if let Err(e) = set_windows_11_mode(prefix_root, install_proton, ctx) { - ctx.log(format!("Warning: Failed to set Windows 11 mode: {}", e)); - log_warning(&format!("Failed to set Windows 11 mode: {}", e)); - } - - if ctx.is_cancelled() { - return Err("Cancelled".into()); - } - - ctx.set_progress(end_progress); - ctx.set_status("Dependencies installed".to_string()); - Ok(()) -} - -/// Install a .NET runtime via direct exe download and wine execution -fn install_dotnet_runtime( - prefix_root: &Path, - proton: &SteamProton, - url: &str, - name: &str, - ctx: &TaskContext, -) -> Result<(), Box<dyn Error>> { - let cache_dir = AppConfig::get_default_cache_dir(); - fs::create_dir_all(&cache_dir)?; - - let filename = url.split('/').next_back().unwrap_or("dotnet-installer.exe"); - let installer_path = cache_dir.join(filename); - - // Download if not cached - if !installer_path.exists() { - log_install(&format!("Downloading {}...", name)); - let response = ureq::get(url) - .set("User-Agent", "NaK-Rust") - .call() - .map_err(|e| format!("Failed to download {}: {}", name, e))?; - - let mut file = fs::File::create(&installer_path)?; - std::io::copy(&mut response.into_reader(), &mut file)?; - } - - // Run installer with wine - let Some(wine_bin) = proton.wine_binary() else { - return Err("Wine binary not found".into()); - }; - - log_install(&format!("Running {} installer...", name)); - - let envs: Vec<(&str, String)> = vec![ - ("WINEPREFIX", prefix_root.display().to_string()), - ("WINEDLLOVERRIDES", "mshtml=d".to_string()), - ]; - let mut cmd = runtime_wrap::build_command(&wine_bin, &envs); - cmd.arg(&installer_path) - .arg("/install") - .arg("/quiet") - .arg("/norestart"); - - let status = ctx.run_cancellable(cmd)?; - - if !status.success() { - return Err(format!("{} installer exited with code {:?}", name, status.code()).into()); - } - - log_install(&format!("{} installed successfully", name)); - Ok(()) -} - -/// Initialize prefix with Proton wrapper -/// -/// Runs `proton run wineboot -u` to properly initialize the prefix with all -/// the Steam/Proton environment variables. This creates a proper prefix -/// structure that Steam recognizes. -fn initialize_prefix_with_proton( - prefix_root: &Path, - proton: &SteamProton, - app_id: u32, - ctx: &TaskContext, -) -> Result<(), Box<dyn Error>> { - // Find the proton wrapper script (not the wine binary) - let proton_script = proton.path.join("proton"); - if !proton_script.exists() { - return Err(format!("Proton wrapper script not found at {:?}", proton_script).into()); - } - - // Get Steam root path - let steam_root = detect_steam_path_checked() - .ok_or("Could not find Steam installation")?; - - // The compatdata path is the PARENT of the pfx directory - let compat_data_path = prefix_root.parent() - .ok_or("Could not determine compatdata path")?; - - log_install(&format!("STEAM_COMPAT_DATA_PATH={:?}", compat_data_path)); - - // Collect all env vars upfront so build_command can set them on the - // process (inherited by SLR/pressure-vessel into the container). - let envs: Vec<(&str, String)> = vec![ - ("STEAM_COMPAT_CLIENT_INSTALL_PATH", steam_root.clone()), - ("STEAM_COMPAT_DATA_PATH", compat_data_path.display().to_string()), - ("SteamAppId", app_id.to_string()), - ("SteamGameId", app_id.to_string()), - ("DISPLAY", String::new()), // Suppress GUI - ("WAYLAND_DISPLAY", String::new()), // Suppress GUI - ("WINEDEBUG", "-all".to_string()), - ("WINEDLLOVERRIDES", "msdia80.dll=n;conhost.exe=d;cmd.exe=d".to_string()), - ]; - - log_install(&format!("Initializing prefix with proton wrapper: {:?}", proton_script)); - let (exe, args): (std::path::PathBuf, Vec<&str>) = - (proton_script.clone(), vec!["run", "wineboot", "-u"]); - - let mut cmd = runtime_wrap::build_command(&exe, &envs); - cmd.args(&args); - - let status = ctx.run_cancellable(cmd)?; - - if !status.success() { - return Err(format!("proton wineboot failed with exit code: {:?}", status.code()).into()); - } - - // Give it a moment for files to land - std::thread::sleep(std::time::Duration::from_secs(2)); - - // Verify prefix was created - if prefix_root.exists() { - log_install("Proton prefix initialized successfully"); - Ok(()) - } else { - Err("Prefix directory not created after wineboot".into()) - } -} - -/// Clean up unwanted Wine drive letters from prefix -/// -/// Removes both symbolic links in dosdevices/ and registry entries for -/// drive letters other than C: and Z:. This prevents Wine from mounting -/// other users' drives as E:, F:, G:, etc. -fn cleanup_wine_drives( - prefix_root: &Path, - proton: &SteamProton, -) -> Result<(), Box<dyn Error>> { - let dosdevices = prefix_root.join("dosdevices"); - - if !dosdevices.exists() { - log_install("dosdevices directory not found, skipping drive cleanup"); - return Ok(()); - } - - // ========================================================================= - // 1. Remove unwanted symlinks from dosdevices/ - // ========================================================================= - let mut removed_drives = Vec::new(); - - if let Ok(entries) = fs::read_dir(&dosdevices) { - for entry in entries.flatten() { - let name = entry.file_name().to_string_lossy().to_lowercase(); - - // Skip allowed drives and non-drive entries (like com1, lpt1, etc.) - if ALLOWED_DRIVE_LETTERS.contains(&name.as_str()) { - continue; - } - - // Only process drive letters (single letter followed by colon) - if name.len() == 2 && name.ends_with(':') && name.chars().next().map(|c| c.is_ascii_alphabetic()).unwrap_or(false) { - let path = entry.path(); - if let Err(e) = fs::remove_file(&path) { - log_warning(&format!("Failed to remove drive symlink {}: {}", name, e)); - } else { - removed_drives.push(name.to_uppercase()); - } - } - } - } - - if !removed_drives.is_empty() { - log_install(&format!("Removed drive symlinks: {}", removed_drives.join(", "))); - } - - // ========================================================================= - // 2. Clean up registry entries for removed drives - // ========================================================================= - let Some(wine_bin) = proton.wine_binary() else { - log_warning("Wine binary not found, skipping registry cleanup"); - return Ok(()); - }; - - // Create a .reg file to remove drive type entries - let tmp_dir = AppConfig::get_tmp_path(); - fs::create_dir_all(&tmp_dir)?; - - let mut reg_content = String::from("Windows Registry Editor Version 5.00\n\n"); - - // Remove entries from HKLM\Software\Wine\Drives - for drive in &removed_drives { - // Remove both uppercase and lowercase variants - reg_content.push_str(&format!( - "[HKEY_LOCAL_MACHINE\\Software\\Wine\\Drives]\n\"{drive}\"=-\n\n" - )); - } - - if !removed_drives.is_empty() { - let reg_file = tmp_dir.join("drive_cleanup.reg"); - fs::write(®_file, ®_content)?; - - let drive_envs: Vec<(&str, String)> = vec![ - ("WINEPREFIX", prefix_root.display().to_string()), - ("WINEDLLOVERRIDES", "mshtml=d".to_string()), - ("PROTON_USE_XALIA", "0".to_string()), - ]; - let status = runtime_wrap::build_command(&wine_bin, &drive_envs) - .arg("regedit") - .arg(®_file) - .status(); - - let _ = fs::remove_file(®_file); - - match status { - Ok(s) if s.success() => { - log_install("Registry drive entries cleaned up"); - } - Ok(s) => { - log_warning(&format!("Registry cleanup may have failed (exit code: {:?})", s.code())); - } - Err(e) => { - log_warning(&format!("Failed to run registry cleanup: {}", e)); - } - } - } - - Ok(()) -} - -/// Public wrapper to clean up Wine drives on an existing prefix -/// -/// This can be called from the UI to fix drive letter issues on existing prefixes. -pub fn cleanup_prefix_drives( - prefix_root: &Path, - proton: &SteamProton, -) -> Result<Vec<String>, Box<dyn Error>> { - let dosdevices = prefix_root.join("dosdevices"); - - if !dosdevices.exists() { - return Err("dosdevices directory not found - is this a valid Wine prefix?".into()); - } - - // Collect drives before cleanup - let mut removed = Vec::new(); - - if let Ok(entries) = fs::read_dir(&dosdevices) { - for entry in entries.flatten() { - let name = entry.file_name().to_string_lossy().to_lowercase(); - if name.len() == 2 && name.ends_with(':') && name.chars().next().map(|c| c.is_ascii_alphabetic()).unwrap_or(false) - && !ALLOWED_DRIVE_LETTERS.contains(&name.as_str()) { - removed.push(name.to_uppercase()); - } - } - } - - // Run the actual cleanup - cleanup_wine_drives(prefix_root, proton)?; - - Ok(removed) -} - -/// Set Windows 11 mode for the prefix using winetricks -/// -/// This should be called AFTER all components are installed. -/// Sets the Windows version to Windows 11 which is required for MO2 to work properly. -fn set_windows_11_mode( - prefix_root: &Path, - proton: &SteamProton, - ctx: &TaskContext, -) -> Result<(), Box<dyn Error>> { - use crate::deps::ensure_winetricks; - - let winetricks_path = ensure_winetricks()?; - - let Some(wine_bin) = proton.wine_binary() else { - return Err("Wine binary not found".into()); - }; - - let Some(wineserver_bin) = proton.wineserver_binary() else { - return Err("Wineserver binary not found".into()); - }; - - log_install("Running winetricks win11..."); - - let envs: Vec<(&str, String)> = vec![ - ("WINE", wine_bin.display().to_string()), - ("WINESERVER", wineserver_bin.display().to_string()), - ("WINEPREFIX", prefix_root.display().to_string()), - ]; - let mut cmd = runtime_wrap::build_command(&winetricks_path, &envs); - cmd.arg("-q").arg("win11"); - - let status = ctx.run_cancellable(cmd)?; - - if !status.success() { - return Err(format!("winetricks win11 failed with exit code: {:?}", status.code()).into()); - } - - log_install("Windows 11 mode set successfully"); - Ok(()) -} - -// ============================================================================= -// DPI Configuration -// ============================================================================= - -/// Common DPI presets with their percentage labels -pub const DPI_PRESETS: &[(u32, &str)] = &[ - (96, "100%"), - (120, "125%"), - (144, "150%"), - (192, "200%"), -]; - -/// Apply DPI setting to a Wine prefix via registry -pub fn apply_dpi( - prefix_root: &Path, - proton: &SteamProton, - dpi_value: u32, -) -> Result<(), Box<dyn Error>> { - log_install(&format!("Applying DPI {} to prefix", dpi_value)); - - let wine_bin = proton.wine_binary().ok_or_else(|| { - format!("Wine binary not found for Proton '{}'", proton.name) - })?; - - let envs: Vec<(&str, String)> = vec![ - ("WINEPREFIX", prefix_root.display().to_string()), - ("PROTON_USE_XALIA", "0".to_string()), - ]; - let status = runtime_wrap::build_command(&wine_bin, &envs) - .arg("reg") - .arg("add") - .arg(r"HKCU\Control Panel\Desktop") - .arg("/v") - .arg("LogPixels") - .arg("/t") - .arg("REG_DWORD") - .arg("/d") - .arg(dpi_value.to_string()) - .arg("/f") - .status()?; - - if !status.success() { - return Err(format!("Failed to apply DPI setting: exit code {:?}", status.code()).into()); - } - - log_install(&format!("DPI {} applied successfully", dpi_value)); - Ok(()) -} - -/// Launch a test application (winecfg, regedit, notepad, control) and return its PID -pub fn launch_dpi_test_app( - prefix_root: &Path, - proton: &SteamProton, - app_name: &str, -) -> Result<Child, Box<dyn Error>> { - let wine_bin = proton.wine_binary().ok_or_else(|| { - format!("Wine binary not found for Proton '{}'", proton.name) - })?; - - log_install(&format!( - "Launching {} with wine={:?} prefix={:?}", - app_name, wine_bin, prefix_root - )); - - if !prefix_root.exists() { - return Err(format!("Prefix not found: {:?}", prefix_root).into()); - } - - let envs: Vec<(&str, String)> = vec![ - ("WINEPREFIX", prefix_root.display().to_string()), - ("PROTON_USE_XALIA", "0".to_string()), - ]; - let child = runtime_wrap::build_command(&wine_bin, &envs) - .arg(app_name) - .spawn()?; - - Ok(child) -} - -/// Kill the wineserver for a prefix (terminates all Wine processes in that prefix) -pub fn kill_wineserver(prefix_root: &Path, proton: &SteamProton) { - log_install("Killing wineserver for prefix"); - - let Some(wineserver_bin) = proton.wineserver_binary() else { - log_install("Wineserver binary not found, skipping kill"); - return; - }; - - let envs: Vec<(&str, String)> = vec![ - ("WINEPREFIX", prefix_root.display().to_string()), - ]; - let _ = runtime_wrap::build_command(&wineserver_bin, &envs) - .arg("-k") - .status(); -} - -// ============================================================================ -// Game Registry Detection (uses game_finder module) -// ============================================================================ - -/// Auto-detect installed games and apply registry entries -/// -/// This uses the game_finder module to detect installed games across all -/// supported launchers (Steam, Heroic, Bottles) and automatically adds -/// the registry entries so mod managers can detect them. -pub fn auto_apply_game_registries( - prefix_path: &Path, - proton: &SteamProton, - log_callback: &impl Fn(String), - _app_id: Option<u32>, -) { - let Some(wine_bin) = proton.wine_binary() else { - log_warning("Wine binary not found, skipping game registry auto-detection"); - return; - }; - - // Use the new game_finder module to detect all games - let scan_result = detect_all_games(); - let mut applied_count = 0; - - for game in &scan_result.games { - // Only process games that have registry info - let (Some(reg_path), Some(reg_value)) = (&game.registry_path, &game.registry_value) else { - continue; - }; - - // Apply registry for this game - if apply_game_registry( - prefix_path, - &wine_bin, - game, - reg_path, - reg_value, - log_callback, - ) { - applied_count += 1; - } - } - - if applied_count > 0 { - log_callback(format!("Auto-configured {} game(s) in registry", applied_count)); - log_install(&format!("Auto-applied registry for {} detected game(s)", applied_count)); - } -} - -/// Apply a game's registry entry with a custom install path. -/// -/// Looks up the game by name in KNOWN_GAMES, then writes the registry entry -/// pointing to `install_path`. Use this when the game is in a custom/stock -/// folder that auto-detection won't find. -pub fn apply_registry_for_game_path( - prefix_path: &Path, - proton: &SteamProton, - game_name: &str, - install_path: &Path, - log_callback: &impl Fn(String), -) -> Result<(), String> { - let Some(wine_bin) = proton.wine_binary() else { - return Err("Wine binary not found".to_string()); - }; - - let known = known_games::find_by_name(game_name); - let (reg_path, reg_value) = if let Some(kg) = known { - (kg.registry_path, kg.registry_value) - } else { - return Err(format!("Unknown game: {game_name}")); - }; - - let fake_game = Game { - name: game_name.to_string(), - install_path: install_path.to_path_buf(), - app_id: known.map(|k| k.steam_app_id.to_string()).unwrap_or_default(), - prefix_path: None, - launcher: Launcher::Steam { is_flatpak: false, is_snap: false }, - my_games_folder: known.and_then(|k| k.my_games_folder.map(String::from)), - appdata_local_folder: known.and_then(|k| k.appdata_local_folder.map(String::from)), - appdata_roaming_folder: known.and_then(|k| k.appdata_roaming_folder.map(String::from)), - registry_path: Some(reg_path.to_string()), - registry_value: Some(reg_value.to_string()), - }; - - if apply_game_registry(prefix_path, &wine_bin, &fake_game, reg_path, reg_value, log_callback) { - Ok(()) - } else { - Err(format!("Failed to apply registry for {game_name}")) - } -} - -/// Return the list of known game names for UI display. -pub fn known_game_names() -> Vec<&'static str> { - known_games::KNOWN_GAMES.iter().map(|g| g.name).collect() -} - -/// Apply registry entry for a single game -fn apply_game_registry( - prefix_path: &Path, - wine_bin: &Path, - game: &Game, - reg_path: &str, - reg_value: &str, - log_callback: &impl Fn(String), -) -> bool { - log_callback(format!("Found {}, applying registry...", game.name)); - - // Convert Linux path to Wine Z: drive path with escaped backslashes for .reg file - let linux_path = game.install_path.to_string_lossy(); - let wine_path_reg = format!("Z:{}", linux_path.replace('/', "\\\\")); - - // Create .reg file content - let reg_content = format!( - r#"Windows Registry Editor Version 5.00 - -[HKEY_LOCAL_MACHINE\{}] -"{}"="{}" - -[HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\{}] -"{}"="{}" -"#, - reg_path, - reg_value, - wine_path_reg, - reg_path.strip_prefix("Software\\").unwrap_or(reg_path), - reg_value, - wine_path_reg, - ); - - // Write temp .reg file - let tmp_dir = AppConfig::get_tmp_path(); - let reg_file = tmp_dir.join(format!("game_reg_{}.reg", game.app_id)); - - if let Err(e) = fs::write(®_file, ®_content) { - log_warning(&format!("Failed to write registry file for {}: {}", game.name, e)); - return false; - } - - // Apply registry - let reg_envs: Vec<(&str, String)> = vec![ - ("WINEPREFIX", prefix_path.display().to_string()), - ("WINEDLLOVERRIDES", "mshtml=d".to_string()), - ("PROTON_USE_XALIA", "0".to_string()), - ]; - let status = runtime_wrap::build_command(wine_bin, ®_envs) - .arg("regedit") - .arg(®_file) - .status(); - - let _ = fs::remove_file(®_file); - - match status { - Ok(s) if s.success() => { - log_install(&format!("Applied registry for {} -> {:?}", game.name, game.install_path)); - true - } - Ok(s) => { - log_warning(&format!( - "Registry for {} may have failed (exit code: {:?})", - game.name, - s.code() - )); - false - } - Err(e) => { - log_warning(&format!("Failed to apply registry for {}: {}", game.name, e)); - false - } - } -} diff --git a/libs/nak/src/installers/symlinks.rs b/libs/nak/src/installers/symlinks.rs deleted file mode 100644 index e8e477b..0000000 --- a/libs/nak/src/installers/symlinks.rs +++ /dev/null @@ -1,377 +0,0 @@ -//! Symlink management for NaK prefixes -//! -//! Creates symlinks FROM NaK prefix TO game prefixes. -//! Data stays in the game prefix, NaK just has links pointing to it. -//! -//! This inverted approach means: -//! - Game saves remain in their original location (Steam cloud sync works) -//! - NaK prefix provides unified access to all game data -//! - No data duplication or sync issues -//! -//! NaK Tools folder contains convenience symlinks pointing INTO the prefix -//! for easy access to Documents, AppData, etc. - -// Allow unused items - some functions are public API for future use -#![allow(dead_code)] - -use std::fs; -use std::path::Path; - -use crate::game_finder::{detect_all_games, Game, GameScanResult}; -use crate::logging::{log_info, log_warning}; - -// ============================================================================ -// Public API -// ============================================================================ - -/// Create symlinks from NaK prefix to game prefixes for all detected games -/// -/// This creates symlinks in the NaK prefix pointing to the actual save/config -/// folders in each game's own prefix. The symlink direction is: -/// -/// NaK/Documents/My Games/Skyrim -> game_prefix/Documents/My Games/Skyrim -/// NaK/AppData/Local/Skyrim -> game_prefix/AppData/Local/Skyrim -/// -/// Data stays in the game prefix (preserving Steam Cloud sync), while NaK -/// provides unified access through symlinks. -pub fn create_game_symlinks(nak_prefix: &Path, games: &[Game]) { - let users_dir = nak_prefix.join("drive_c/users"); - let username = find_prefix_username(&users_dir); - let user_dir = users_dir.join(&username); - - let documents = user_dir.join("Documents"); - let my_games = documents.join("My Games"); - let appdata_local = user_dir.join("AppData/Local"); - let appdata_roaming = user_dir.join("AppData/Roaming"); - - // Ensure base directories exist (real folders in NaK prefix) - let _ = fs::create_dir_all(&my_games); - let _ = fs::create_dir_all(&appdata_local); - let _ = fs::create_dir_all(&appdata_roaming); - - let mut linked_count = 0; - - for game in games { - // Skip games without prefixes - let Some(game_prefix) = &game.prefix_path else { - log_info(&format!( - "Symlink skip '{}' (app_id {}): no prefix_path", - game.name, game.app_id - )); - continue; - }; - - // Discover the game prefix's user directory - let game_users_dir = game_prefix.join("drive_c/users"); - let game_username = find_prefix_username(&game_users_dir); - let game_user_dir = game_users_dir.join(&game_username); - - let my_games_dir = game_user_dir.join("Documents/My Games"); - log_info(&format!( - "Symlink checking '{}' (app_id {}): prefix={}, user='{}', my_games_exists={}", - game.name, game.app_id, game_prefix.display(), game_username, my_games_dir.is_dir() - )); - - // Scan and symlink ALL folders in the game prefix's Documents/My Games/ - linked_count += scan_and_link_all( - &my_games, - &game_user_dir.join("Documents/My Games"), - "Documents/My Games", - &game.name, - game_prefix, - ); - - // Also link the Documents folder itself for non-My Games entries - // (some games put saves directly in Documents/<GameName>) - linked_count += scan_and_link_all( - &documents, - &game_user_dir.join("Documents"), - "Documents", - &game.name, - game_prefix, - ); - - // Scan and symlink ALL folders in AppData/Local/ - linked_count += scan_and_link_all( - &appdata_local, - &game_user_dir.join("AppData/Local"), - "AppData/Local", - &game.name, - game_prefix, - ); - - // Scan and symlink ALL folders in AppData/Roaming/ - linked_count += scan_and_link_all( - &appdata_roaming, - &game_user_dir.join("AppData/Roaming"), - "AppData/Roaming", - &game.name, - game_prefix, - ); - } - - if linked_count > 0 { - log_info(&format!( - "Created {} symlinks to game prefixes", - linked_count - )); - } - - // Create "My Documents" symlink for compatibility - let my_documents = user_dir.join("My Documents"); - if !my_documents.exists() && fs::symlink_metadata(&my_documents).is_err() { - if let Err(e) = std::os::unix::fs::symlink("Documents", &my_documents) { - log_warning(&format!("Failed to create My Documents symlink: {}", e)); - } - } -} - -/// Create NaK Tools convenience symlinks pointing INTO the prefix -/// -/// Creates symlinks in NaK Tools folder for easy access: -/// - NaK Tools/Prefix Documents -> prefix/drive_c/users/<user>/Documents -/// - NaK Tools/Prefix AppData Local -> prefix/drive_c/users/<user>/AppData/Local -/// - NaK Tools/Prefix AppData Roaming -> prefix/drive_c/users/<user>/AppData/Roaming -pub fn create_nak_tools_symlinks(tools_dir: &Path, prefix_path: &Path) { - let users_dir = prefix_path.join("drive_c/users"); - let username = find_prefix_username(&users_dir); - let user_dir = users_dir.join(&username); - - // Symlink: NaK Tools/Prefix Documents -> prefix Documents - let documents_link = tools_dir.join("Prefix Documents"); - let documents_target = user_dir.join("Documents"); - create_or_update_symlink(&documents_link, &documents_target, "Prefix Documents"); - - // Symlink: NaK Tools/Prefix AppData Local -> prefix AppData/Local - let appdata_local_link = tools_dir.join("Prefix AppData Local"); - let appdata_local_target = user_dir.join("AppData/Local"); - create_or_update_symlink(&appdata_local_link, &appdata_local_target, "Prefix AppData Local"); - - // Symlink: NaK Tools/Prefix AppData Roaming -> prefix AppData/Roaming - let appdata_roaming_link = tools_dir.join("Prefix AppData Roaming"); - let appdata_roaming_target = user_dir.join("AppData/Roaming"); - create_or_update_symlink(&appdata_roaming_link, &appdata_roaming_target, "Prefix AppData Roaming"); - - log_info("Created NaK Tools convenience symlinks to prefix folders"); -} - -/// Create or update a symlink -fn create_or_update_symlink(link_path: &Path, target: &Path, name: &str) { - // Remove existing symlink or file - if link_path.exists() || fs::symlink_metadata(link_path).is_ok() { - let _ = fs::remove_file(link_path); - let _ = fs::remove_dir_all(link_path); - } - - // Create symlink - if let Err(e) = std::os::unix::fs::symlink(target, link_path) { - log_warning(&format!("Failed to create {} symlink: {}", name, e)); - } -} - -/// Create symlinks for all detected games -/// -/// Convenience function that detects games and creates symlinks in one call. -pub fn create_game_symlinks_auto(nak_prefix: &Path) -> GameScanResult { - let result = detect_all_games(); - create_game_symlinks(nak_prefix, &result.games); - result -} - -/// Ensure only the Temp directory exists in AppData/Local -/// -/// MO2 and other tools require AppData/Local/Temp to exist. -/// We create only this essential directory, leaving other game-specific -/// folders to be symlinked from game prefixes. -pub fn ensure_temp_directory(prefix_path: &Path) { - let users_dir = prefix_path.join("drive_c/users"); - let username = find_prefix_username(&users_dir); - let user_dir = users_dir.join(&username); - - let temp_dir = user_dir.join("AppData/Local/Temp"); - if let Err(e) = fs::create_dir_all(&temp_dir) { - log_warning(&format!("Failed to create Temp directory: {}", e)); - } else { - log_info("Ensured AppData/Local/Temp directory exists"); - } -} - -// ============================================================================ -// Internal Functions -// ============================================================================ - -/// Directories to skip when scanning prefix folders for symlinking. -/// These are Wine/Proton internal or system dirs, not game data. -const SKIP_DIRS: &[&str] = &[ - "Temp", "Microsoft", "wine", "Public", "root", - "Application Data", "Cookies", "Local Settings", - "NetHood", "PrintHood", "Recent", "SendTo", - "Start Menu", "Templates", "My Documents", "My Music", - "My Pictures", "My Videos", "Desktop", "Downloads", - "Favorites", "Links", "Searches", - "Contacts", "3D Objects", -]; - -/// Scan all subdirectories in a game prefix folder and create symlinks -/// for each one in the corresponding NaK prefix folder. -/// -/// Returns the number of symlinks created. -fn scan_and_link_all( - nak_base: &Path, - game_base: &Path, - label: &str, - game_name: &str, - _game_prefix: &Path, -) -> usize { - if !game_base.is_dir() { - log_info(&format!( - " scan_and_link_all: {} not a dir for '{}', skipping", - game_base.display(), game_name - )); - return 0; - } - - let Ok(entries) = fs::read_dir(game_base) else { - log_info(&format!( - " scan_and_link_all: cannot read {} for '{}'", - game_base.display(), game_name - )); - return 0; - }; - - let mut count = 0; - for entry in entries.flatten() { - // Only symlink directories (game folders), not loose files - if !entry.path().is_dir() { - continue; - } - - let folder_name = entry.file_name().to_string_lossy().to_string(); - - // Skip Wine/system internal directories - if SKIP_DIRS.iter().any(|&s| s.eq_ignore_ascii_case(&folder_name)) { - continue; - } - - // Skip if it's "My Games" and we're scanning Documents (handled separately) - if label == "Documents" && folder_name == "My Games" { - continue; - } - - let nak_path = nak_base.join(&folder_name); - let source_path = entry.path(); - - if create_symlink_if_needed(&nak_path, &source_path, game_name, label, &folder_name) { - count += 1; - } - } - - count -} - -/// Create a symlink if the target doesn't already exist or is already correct. -/// -/// Returns true if a symlink was created (or already existed correctly). -fn create_symlink_if_needed( - nak_path: &Path, - source_path: &Path, - game_name: &str, - label: &str, - folder_name: &str, -) -> bool { - // Check if target already exists - if nak_path.exists() || fs::symlink_metadata(nak_path).is_ok() { - // Check if it's already a symlink to the correct location - if let Ok(target) = fs::read_link(nak_path) { - if target == source_path { - return true; // Already correctly linked - } - } - // Something else exists here, don't overwrite - return false; - } - - // Ensure parent directory exists - if let Some(parent) = nak_path.parent() { - let _ = fs::create_dir_all(parent); - } - - // Create the symlink - match std::os::unix::fs::symlink(source_path, nak_path) { - Ok(()) => { - log_info(&format!( - "Linked {}/{} -> {} ({})", - label, - folder_name, - source_path.display(), - game_name, - )); - true - } - Err(e) => { - log_warning(&format!( - "Failed to create symlink for {} ({}/{}): {}", - game_name, label, folder_name, e - )); - false - } - } -} - -/// Find the username from a Wine prefix users directory -fn find_prefix_username(users_dir: &Path) -> String { - if let Ok(entries) = fs::read_dir(users_dir) { - for entry in entries.flatten() { - let name = entry.file_name().to_string_lossy().to_string(); - if name != "Public" && name != "root" { - return name; - } - } - } - "steamuser".to_string() -} - -// ============================================================================ -// Oblivion Lowercase INI Symlinks -// ============================================================================ - -/// Create lowercase INI symlinks for Oblivion (some tools expect lowercase) -pub fn create_oblivion_ini_symlinks(prefix_path: &Path) { - let users_dir = prefix_path.join("drive_c/users"); - let username = find_prefix_username(&users_dir); - let oblivion_dir = users_dir - .join(&username) - .join("Documents/My Games/Oblivion"); - - if !oblivion_dir.exists() { - return; - } - - create_lowercase_ini_symlink(&oblivion_dir, "Oblivion.ini", "oblivion.ini"); - create_lowercase_ini_symlink(&oblivion_dir, "OblivionPrefs.ini", "oblivionprefs.ini"); -} - -/// Create a lowercase symlink for an INI file -fn create_lowercase_ini_symlink(dir: &Path, original: &str, lowercase: &str) { - let original_path = dir.join(original); - let lowercase_path = dir.join(lowercase); - - // Only create if original exists and lowercase doesn't - if original_path.exists() - && !lowercase_path.exists() - && fs::symlink_metadata(&lowercase_path).is_err() - { - // Create relative symlink (just the filename) - if let Err(e) = std::os::unix::fs::symlink(original, &lowercase_path) { - log_warning(&format!( - "Failed to create lowercase symlink {} -> {}: {}", - lowercase, original, e - )); - } else { - log_info(&format!( - "Created lowercase INI symlink: {} -> {}", - lowercase, original - )); - } - } -} diff --git a/libs/nak/src/lib.rs b/libs/nak/src/lib.rs deleted file mode 100644 index d6c8e2b..0000000 --- a/libs/nak/src/lib.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! NaK - Vendored library for Fluorine Manager -//! -//! Stripped version: no GUI, CLI, marketplace, updater, NXM handler, -//! Steam shortcuts, or managed prefix tracking. - -pub mod config; -pub mod dxvk; -pub mod slr; -pub mod game_finder; -pub mod logging; -pub mod paths; -pub mod runtime_wrap; -pub mod steam; -pub mod utils; - -pub mod deps; -pub mod icons; -pub mod installers; diff --git a/libs/nak/src/logging.rs b/libs/nak/src/logging.rs deleted file mode 100644 index e506bf8..0000000 --- a/libs/nak/src/logging.rs +++ /dev/null @@ -1,98 +0,0 @@ -//! Logging for Fluorine Manager. -//! -//! All log messages are: -//! 1. Written to `~/.local/share/fluorine/logs/nak.log` (always) -//! 2. Forwarded to an optional callback set via `set_log_callback()` (for MOBase::log) - -use std::fs; -use std::io::Write; -use std::path::PathBuf; -use std::sync::Mutex; - -/// Signature for the log callback: (level, message) -/// -/// Levels: "info", "warning", "error", "install", "action", "download" -type LogCallback = Box<dyn Fn(&str, &str) + Send + Sync>; - -static LOG_CALLBACK: Mutex<Option<LogCallback>> = Mutex::new(None); - -/// Set the global log callback. Call once at startup from FFI. -pub fn set_log_callback(cb: impl Fn(&str, &str) + Send + Sync + 'static) { - if let Ok(mut guard) = LOG_CALLBACK.lock() { - *guard = Some(Box::new(cb)); - } -} - -/// Get the log directory path. -fn log_dir() -> PathBuf { - crate::paths::data_dir().join("logs") -} - -/// Get the log file path. -fn log_file_path() -> PathBuf { - log_dir().join("nak.log") -} - -fn emit(level: &str, message: &str) { - // Write to file (always, even before callback is set) - write_to_file(level, message); - - // Forward to callback if set - if let Ok(guard) = LOG_CALLBACK.lock() { - if let Some(ref cb) = *guard { - cb(level, message); - } - } -} - -fn write_to_file(level: &str, message: &str) { - let path = log_file_path(); - - // Ensure log directory exists (only try once per message, cheap no-op if exists) - if let Some(parent) = path.parent() { - let _ = fs::create_dir_all(parent); - } - - // Rotate if over 2MB - if let Ok(meta) = fs::metadata(&path) { - if meta.len() > 2 * 1024 * 1024 { - let old = path.with_extension("log.old"); - let _ = fs::rename(&path, &old); - } - } - - let Ok(mut file) = fs::OpenOptions::new() - .create(true) - .append(true) - .open(&path) - else { - return; - }; - - let timestamp = chrono::Local::now().format("%H:%M:%S"); - let _ = writeln!(file, "[{}] [{}] {}", timestamp, level.to_uppercase(), message); -} - -pub fn log_info(message: &str) { - emit("info", message); -} - -pub fn log_warning(message: &str) { - emit("warning", message); -} - -pub fn log_error(message: &str) { - emit("error", message); -} - -pub fn log_install(message: &str) { - emit("install", message); -} - -pub fn log_action(message: &str) { - emit("action", message); -} - -pub fn log_download(message: &str) { - emit("download", message); -} diff --git a/libs/nak/src/paths.rs b/libs/nak/src/paths.rs deleted file mode 100644 index f6dde6c..0000000 --- a/libs/nak/src/paths.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Shared data directory for Fluorine Manager. -//! -//! All data lives under `~/.local/share/fluorine/`. - -use std::path::PathBuf; - -/// Returns the Fluorine data directory (`~/.local/share/fluorine`). -pub fn data_dir() -> PathBuf { - let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()); - PathBuf::from(home).join(".local/share/fluorine") -} diff --git a/libs/nak/src/runtime_wrap.rs b/libs/nak/src/runtime_wrap.rs deleted file mode 100644 index 943a329..0000000 --- a/libs/nak/src/runtime_wrap.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::ffi::OsStr; -use std::process::Command; - -/// Build a command to run `exe` with the given environment variables. -/// -/// If SLR (Steam Linux Runtime) is installed, the command is wrapped inside -/// the pressure-vessel container via the SLR `run` script. Environment -/// variables are set on the process and inherited by pressure-vessel into the -/// container (matching how game launches work via QProcess). This ensures -/// Wine/Proton commands use the container's libraries instead of potentially -/// broken host libraries. -pub fn build_command<S: AsRef<OsStr>>( - exe: impl AsRef<OsStr>, - envs: &[(&str, S)], -) -> Command { - if let Some(slr_script) = crate::slr::get_slr_run_script() { - let mut cmd = Command::new(&slr_script); - // Set env vars on the process — pressure-vessel inherits them - for (key, value) in envs { - cmd.env(key, value.as_ref()); - } - // Expose the executable's parent directory to the container — - // needed for system-installed Protons (e.g. /usr/share/steam/...) - // whose files may not be visible inside the container by default. - if let Some(parent) = std::path::Path::new(exe.as_ref()).parent() { - if parent.exists() { - let mut flag = std::ffi::OsString::from("--filesystem="); - flag.push(parent.as_os_str()); - cmd.arg(flag); - } - } - // Also expose WINEPREFIX if set in env vars - for (key, value) in envs { - if *key == "WINEPREFIX" || *key == "STEAM_COMPAT_DATA_PATH" { - let path = std::path::Path::new(value.as_ref()); - if path.exists() || path.parent().map_or(false, |p| p.exists()) { - let mut flag = std::ffi::OsString::from("--filesystem="); - flag.push(value.as_ref()); - cmd.arg(flag); - } - } - } - cmd.arg("--"); - cmd.arg(exe); - cmd - } else { - let mut cmd = Command::new(exe); - for (key, value) in envs { - cmd.env(key, value.as_ref()); - } - cmd - } -} - -/// Build a command with no extra environment variables. -pub fn command_for(exe: impl AsRef<OsStr>) -> Command { - build_command::<&str>(exe, &[]) -} diff --git a/libs/nak/src/slr.rs b/libs/nak/src/slr.rs deleted file mode 100644 index 3fe7d92..0000000 --- a/libs/nak/src/slr.rs +++ /dev/null @@ -1,231 +0,0 @@ -//! Steam Linux Runtime (SLR) download and management for Fluorine Manager. -//! -//! Downloads SteamLinuxRuntime_sniper from Valve's official repo and stores it -//! at `~/.local/share/fluorine/steamrt/`. The `run` script is then used to -//! wrap game launches inside the pressure-vessel container, providing -//! GStreamer, 32-bit libs, and an FHS-compliant environment for non-FHS -//! distros (NixOS, etc.). - -use std::error::Error; -use std::fs; -use std::io::{Read, Write}; -use std::path::PathBuf; -use std::process::Command; -use std::sync::atomic::{AtomicI32, Ordering}; - -use crate::logging::log_info; - -const BASE_URL: &str = - "https://repo.steampowered.com/steamrt3/images/latest-public-beta"; -const ARCHIVE_NAME: &str = "SteamLinuxRuntime_sniper.tar.xz"; -const EXTRACTED_DIR: &str = "SteamLinuxRuntime_sniper"; - -/// Directory where SLR is installed: `~/.local/share/fluorine/steamrt/` -pub fn slr_install_dir() -> PathBuf { - crate::paths::data_dir().join("steamrt") -} - -/// Path to the `run` script inside the extracted SLR. -pub fn slr_run_script() -> PathBuf { - slr_install_dir().join(EXTRACTED_DIR).join("run") -} - -/// Path where we store the remote BUILD_ID for update checks. -fn local_build_id_path() -> PathBuf { - slr_install_dir().join("BUILD_ID.txt") -} - -/// Returns true if the SLR `run` script is present and executable. -pub fn is_slr_installed() -> bool { - let script = slr_run_script(); - if !script.exists() { - return false; - } - // Verify it's actually executable - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - if let Ok(meta) = fs::metadata(&script) { - return meta.permissions().mode() & 0o111 != 0; - } - return false; - } - #[cfg(not(unix))] - true -} - -/// Returns the path to the `run` script, or None if SLR is not installed. -pub fn get_slr_run_script() -> Option<PathBuf> { - if is_slr_installed() { - Some(slr_run_script()) - } else { - None - } -} - -/// Fetch the remote BUILD_ID as a string. -fn fetch_remote_build_id() -> Result<String, Box<dyn Error>> { - let url = format!("{}/BUILD_ID.txt", BASE_URL); - let resp = ureq::get(&url).call()?; - let mut body = String::new(); - resp.into_reader().read_to_string(&mut body)?; - Ok(body.trim().to_string()) -} - -/// Read the locally cached BUILD_ID, if any. -fn read_local_build_id() -> Option<String> { - fs::read_to_string(local_build_id_path()) - .ok() - .map(|s| s.trim().to_string()) -} - -/// Verify downloaded file size matches the Content-Length from the HTTP response. -/// This is more reliable than SHA256SUMS which can be stale on Valve's CDN. -fn verify_download_size(file: &std::path::Path, expected: u64) -> Result<(), Box<dyn Error>> { - let actual = fs::metadata(file)?.len(); - if actual != expected { - return Err(format!( - "Download incomplete: expected {} bytes, got {}", - expected, actual - ) - .into()); - } - Ok(()) -} - -/// Download the archive with streaming progress. -/// -/// `progress_cb` receives values in 0.0..=1.0. -/// `cancel_flag` is polled each chunk — set to non-zero to abort. -/// Returns the Content-Length on success for verification. -fn download_archive( - dest: &std::path::Path, - progress_cb: &impl Fn(f32), - cancel_flag: &AtomicI32, -) -> Result<Option<u64>, Box<dyn Error>> { - let url = format!("{}/{}", BASE_URL, ARCHIVE_NAME); - let resp = ureq::get(&url).call()?; - - let total_bytes: Option<u64> = resp - .header("Content-Length") - .and_then(|v| v.parse().ok()); - - let mut reader = resp.into_reader(); - let mut file = fs::File::create(dest)?; - let mut buf = vec![0u8; 64 * 1024]; // 64 KiB chunks - let mut downloaded: u64 = 0; - - loop { - if cancel_flag.load(Ordering::Relaxed) != 0 { - drop(file); - let _ = fs::remove_file(dest); - return Err("Download cancelled".into()); - } - - let n = reader.read(&mut buf)?; - if n == 0 { - break; - } - file.write_all(&buf[..n])?; - downloaded += n as u64; - - if let Some(total) = total_bytes { - if total > 0 { - progress_cb((downloaded as f32) / (total as f32)); - } - } - } - - file.flush()?; - Ok(total_bytes) -} - -/// Download and install the Steam Linux Runtime (sniper). -/// -/// - Skips download if already at the latest BUILD_ID. -/// - Calls `status_cb` with human-readable status strings. -/// - Calls `progress_cb` with 0.0..=1.0 during the download phase. -/// - Polls `cancel_flag`; returns an error if it becomes non-zero. -pub fn download_slr( - progress_cb: impl Fn(f32), - status_cb: impl Fn(&str), - cancel_flag: &AtomicI32, -) -> Result<(), Box<dyn Error>> { - // Check for updates - status_cb("Checking Steam Linux Runtime version..."); - let remote_build_id = fetch_remote_build_id()?; - let local_build_id = read_local_build_id(); - - if local_build_id.as_deref() == Some(remote_build_id.as_str()) && is_slr_installed() { - log_info("Steam Linux Runtime is already up to date"); - status_cb("Steam Linux Runtime is already up to date"); - progress_cb(1.0); - return Ok(()); - } - - log_info(&format!( - "Downloading Steam Linux Runtime (BUILD_ID: {})", - remote_build_id - )); - - let install_dir = slr_install_dir(); - fs::create_dir_all(&install_dir)?; - - // Temp file in the install dir - let archive_path = install_dir.join(ARCHIVE_NAME); - - // Download - status_cb("Downloading Steam Linux Runtime (sniper, ~180 MB)..."); - let content_length = download_archive(&archive_path, &progress_cb, cancel_flag)?; - progress_cb(1.0); - - // Verify download integrity via Content-Length. - // We don't use Valve's SHA256SUMS file because their CDN frequently - // serves a stale copy that doesn't match the current archive. - if let Some(expected_size) = content_length { - status_cb("Verifying download..."); - if let Err(e) = verify_download_size(&archive_path, expected_size) { - let _ = fs::remove_file(&archive_path); - return Err(format!("Download verification failed: {}", e).into()); - } - log_info(&format!( - "Download verified ({} bytes)", - expected_size - )); - } - - // Extract - status_cb("Extracting Steam Linux Runtime..."); - let extracted = install_dir.join(EXTRACTED_DIR); - // Remove old copy if present before extracting - if extracted.exists() { - fs::remove_dir_all(&extracted)?; - } - - let status = Command::new("tar") - .args(["xJf", archive_path.to_str().unwrap_or("")]) - .current_dir(&install_dir) - .status()?; - - let _ = fs::remove_file(&archive_path); - - if !status.success() { - return Err(format!("tar extraction failed with status: {}", status).into()); - } - - // Sanity check — run script must now exist - if !slr_run_script().exists() { - return Err(format!( - "Extraction succeeded but run script not found at {:?}", - slr_run_script() - ) - .into()); - } - - // Save BUILD_ID for future update checks - fs::write(local_build_id_path(), &remote_build_id)?; - - log_info("Steam Linux Runtime installed successfully"); - status_cb("Steam Linux Runtime ready"); - Ok(()) -} diff --git a/libs/nak/src/steam/mod.rs b/libs/nak/src/steam/mod.rs deleted file mode 100644 index c5964aa..0000000 --- a/libs/nak/src/steam/mod.rs +++ /dev/null @@ -1,135 +0,0 @@ -//! Steam integration module -//! -//! Handles Proton detection, Steam path detection, and mount point discovery. -//! Shortcuts and config.vdf manipulation removed (handled by C++ side). - -mod paths; -mod proton; - -// Re-export path detection utilities -pub use paths::{ - detect_steam_path_checked, find_steam_path, find_userdata_path, - get_steam_accounts, -}; - -// Re-export Proton detection -pub use proton::{find_steam_protons, SteamProton}; - -use std::fs; - -/// Kill Steam process gracefully, then force if needed -pub fn kill_steam() -> Result<(), Box<dyn std::error::Error>> { - use std::process::Command; - - // Try steam -shutdown first (graceful) - let _ = Command::new("steam") - .arg("-shutdown") - .status(); - - std::thread::sleep(std::time::Duration::from_secs(2)); - - // Then force kill if still running - let _ = Command::new("pkill") - .arg("-9") - .arg("steam") - .status(); - - // Brief wait for Steam to fully exit - std::thread::sleep(std::time::Duration::from_secs(2)); - - Ok(()) -} - -/// Start Steam in background -pub fn start_steam() -> Result<(), Box<dyn std::error::Error>> { - use std::process::{Command, Stdio}; - - Command::new("setsid") - .arg("steam") - .arg("-silent") - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn()?; - - Ok(()) -} - -/// Restart Steam (kill then start) -pub fn restart_steam() -> Result<(), Box<dyn std::error::Error>> { - kill_steam()?; - start_steam()?; - Ok(()) -} - -// ============================================================================ -// STEAM_COMPAT_MOUNTS Detection -// ============================================================================ - -/// Directories that pressure-vessel already exposes by default -const ALREADY_EXPOSED: &[&str] = &[ - "bin", "etc", "home", "lib", "lib32", "lib64", - "overrides", "run", "sbin", "tmp", "usr", "var", -]; - -/// System directories that shouldn't be mounted -const SYSTEM_DIRS: &[&str] = &[ - "proc", "sys", "dev", "boot", "root", "lost+found", "snap", -]; - -/// Detect directories at root that need to be added to STEAM_COMPAT_MOUNTS -pub fn detect_extra_mounts() -> Vec<String> { - let mut mounts = Vec::new(); - - let Ok(entries) = fs::read_dir("/") else { - return mounts; - }; - - for entry in entries.flatten() { - let name = entry.file_name().to_string_lossy().to_string(); - - if ALREADY_EXPOSED.contains(&name.as_str()) { - continue; - } - - if SYSTEM_DIRS.contains(&name.as_str()) { - continue; - } - - if name.starts_with('.') { - continue; - } - - if entry.path().is_dir() { - mounts.push(format!("/{}", name)); - } - } - - mounts.sort(); - mounts -} - -/// Generate launch options string with DXVK config file and STEAM_COMPAT_MOUNTS -pub fn generate_launch_options(dxvk_conf_path: Option<&std::path::Path>, is_electron_app: bool) -> String { - let mounts = detect_extra_mounts(); - - let dxvk_part = match dxvk_conf_path { - Some(path) => format!( - "DXVK_CONFIG_FILE=\"{}\"", - crate::config::normalize_path_for_steam(&path.to_string_lossy()) - ), - None => String::new(), - }; - - let electron_flags = if is_electron_app { - " --disable-gpu --no-sandbox" - } else { - "" - }; - - match (dxvk_part.is_empty(), mounts.is_empty()) { - (true, true) => format!("%command%{}", electron_flags), - (true, false) => format!("STEAM_COMPAT_MOUNTS={} %command%{}", mounts.join(":"), electron_flags), - (false, true) => format!("{} %command%{}", dxvk_part, electron_flags), - (false, false) => format!("{} STEAM_COMPAT_MOUNTS={} %command%{}", dxvk_part, mounts.join(":"), electron_flags), - } -} diff --git a/libs/nak/src/steam/paths.rs b/libs/nak/src/steam/paths.rs deleted file mode 100644 index 8ef5013..0000000 --- a/libs/nak/src/steam/paths.rs +++ /dev/null @@ -1,259 +0,0 @@ -//! Steam path detection utilities - -use std::fs; -use std::path::PathBuf; - -use crate::logging::{log_info, log_warning}; - -// ============================================================================ -// Core Path Detection -// ============================================================================ - -/// Find the Steam installation path. -#[must_use] -pub fn find_steam_path() -> Option<PathBuf> { - let home = std::env::var("HOME").ok()?; - - let steam_paths = [ - format!("{}/.steam/steam", home), - format!("{}/.local/share/Steam", home), - format!("{}/.var/app/com.valvesoftware.Steam/.steam/steam", home), - format!("{}/snap/steam/common/.steam/steam", home), - ]; - - steam_paths - .iter() - .map(PathBuf::from) - .find(|p| p.exists()) -} - -/// Find the Steam userdata directory. -#[must_use] -pub fn find_userdata_path() -> Option<PathBuf> { - let config = crate::config::AppConfig::load(); - if !config.selected_steam_account.is_empty() { - if let Some(path) = find_userdata_path_for_account(&config.selected_steam_account) { - return Some(path); - } - } - - let steam_path = find_steam_path()?; - let userdata = steam_path.join("userdata"); - - if !userdata.exists() { - return None; - } - - let accounts = get_steam_accounts(); - - if let Some(most_recent) = accounts.iter().find(|a| a.most_recent) { - let path = userdata.join(&most_recent.account_id); - if path.exists() { - log_info(&format!( - "Using Steam account from loginusers.vdf (MostRecent): {} ({})", - most_recent.persona_name, most_recent.account_id - )); - return Some(path); - } - } - - if let Some(first_account) = accounts.first() { - let path = userdata.join(&first_account.account_id); - if path.exists() { - log_info(&format!( - "Using Steam account from loginusers.vdf (most recent timestamp): {} ({})", - first_account.persona_name, first_account.account_id - )); - return Some(path); - } - } - - log_warning("Could not determine active Steam account from loginusers.vdf, falling back to directory modification time"); - - let mut user_dirs: Vec<PathBuf> = Vec::new(); - - if let Ok(entries) = fs::read_dir(&userdata) { - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - if let Some(name) = path.file_name() { - let name_str = name.to_string_lossy(); - if name_str != "0" && name_str.chars().all(|c| c.is_ascii_digit()) { - user_dirs.push(path); - } - } - } - } - } - - user_dirs.sort_by(|a, b| { - let a_time = fs::metadata(a).and_then(|m| m.modified()).ok(); - let b_time = fs::metadata(b).and_then(|m| m.modified()).ok(); - b_time.cmp(&a_time) - }); - - user_dirs.into_iter().next() -} - -// ============================================================================ -// Steam Account Detection (loginusers.vdf parsing) -// ============================================================================ - -/// Information about a Steam user account -#[derive(Debug, Clone)] -pub struct SteamAccount { - pub account_id: String, - pub persona_name: String, - pub most_recent: bool, - pub timestamp: u64, -} - -/// Get all Steam accounts from loginusers.vdf with their display names -#[must_use] -pub fn get_steam_accounts() -> Vec<SteamAccount> { - let Some(steam_path) = find_steam_path() else { - return Vec::new(); - }; - - let loginusers_path = steam_path.join("config/loginusers.vdf"); - let userdata_path = steam_path.join("userdata"); - - let Ok(content) = fs::read_to_string(&loginusers_path) else { - return Vec::new(); - }; - - let mut accounts = Vec::new(); - - let mut current_steam_id: Option<String> = None; - let mut current_account: Option<SteamAccountBuilder> = None; - - for line in content.lines() { - let trimmed = line.trim(); - - if trimmed.starts_with('"') && trimmed.ends_with('"') { - let id = trimmed.trim_matches('"'); - if id.len() == 17 && id.starts_with("7656") && id.chars().all(|c| c.is_ascii_digit()) { - if let (Some(steam_id), Some(builder)) = (current_steam_id.take(), current_account.take()) { - if let Some(account) = builder.build(&steam_id, &userdata_path) { - accounts.push(account); - } - } - current_steam_id = Some(id.to_string()); - current_account = Some(SteamAccountBuilder::default()); - } - } - - if let Some(ref mut builder) = current_account { - if let Some((key, value)) = parse_vdf_kv(trimmed) { - match key.to_lowercase().as_str() { - "accountname" => builder.account_name = Some(value), - "personaname" => builder.persona_name = Some(value), - "mostrecent" => builder.most_recent = value == "1", - "timestamp" => builder.timestamp = value.parse().unwrap_or(0), - _ => {} - } - } - } - } - - if let (Some(steam_id), Some(builder)) = (current_steam_id, current_account) { - if let Some(account) = builder.build(&steam_id, &userdata_path) { - accounts.push(account); - } - } - - accounts.sort_by(|a, b| b.timestamp.cmp(&a.timestamp)); - - accounts -} - -#[derive(Default)] -struct SteamAccountBuilder { - account_name: Option<String>, - persona_name: Option<String>, - most_recent: bool, - timestamp: u64, -} - -impl SteamAccountBuilder { - fn build(self, steam_id: &str, userdata_base: &std::path::Path) -> Option<SteamAccount> { - let account_name = self.account_name?; - let persona_name = self.persona_name.unwrap_or_else(|| account_name.clone()); - - let steam64: u64 = steam_id.parse().ok()?; - let account_id = (steam64 - 76561197960265728).to_string(); - - let userdata_path = userdata_base.join(&account_id); - - if !userdata_path.exists() { - return None; - } - - Some(SteamAccount { - account_id, - persona_name, - most_recent: self.most_recent, - timestamp: self.timestamp, - }) - } -} - -/// Parse a VDF key-value pair like: "Key" "Value" -fn parse_vdf_kv(line: &str) -> Option<(String, String)> { - let mut parts = Vec::new(); - let mut current = String::new(); - let mut in_quotes = false; - - for c in line.chars() { - match c { - '"' => { - if in_quotes { - parts.push(current.clone()); - current.clear(); - } - in_quotes = !in_quotes; - } - _ if in_quotes => current.push(c), - _ => {} - } - } - - if parts.len() >= 2 { - Some((parts[0].clone(), parts[1].clone())) - } else { - None - } -} - -/// Find the userdata path for a specific Steam account -#[must_use] -pub fn find_userdata_path_for_account(account_id: &str) -> Option<PathBuf> { - let steam_path = find_steam_path()?; - let userdata = steam_path.join("userdata").join(account_id); - - if userdata.exists() { - Some(userdata) - } else { - None - } -} - -// ============================================================================ -// Convenience Wrappers -// ============================================================================ - -/// Detect the Steam installation path with logging. -#[must_use] -pub fn detect_steam_path_checked() -> Option<String> { - match find_steam_path() { - Some(path) => { - let path_str = path.to_string_lossy().to_string(); - log_info(&format!("Steam detected at: {}", path_str)); - Some(path_str) - } - None => { - log_warning("Steam installation not detected! NaK requires Steam to be installed."); - None - } - } -} diff --git a/libs/nak/src/steam/proton.rs b/libs/nak/src/steam/proton.rs deleted file mode 100644 index 4059dc4..0000000 --- a/libs/nak/src/steam/proton.rs +++ /dev/null @@ -1,252 +0,0 @@ -//! Steam Proton detection and management -//! -//! Finds Protons that Steam can see and use for non-Steam games. -//! This includes Steam's built-in Protons and custom Protons in compatibilitytools.d. - -use std::fs; -use std::path::PathBuf; - -use super::find_steam_path; - -/// Information about an installed Proton version -#[derive(Debug, Clone)] -pub struct SteamProton { - /// Display name (e.g., "GE-Proton9-20", "Proton Experimental") - pub name: String, - /// Internal name used in config.vdf (e.g., "proton_experimental", "GE-Proton9-20") - pub config_name: String, - /// Full path to the Proton installation - pub path: PathBuf, - /// Whether this is a Steam-provided Proton (vs custom) - pub is_steam_proton: bool, - /// Whether this is Proton Experimental - pub is_experimental: bool, -} - -impl SteamProton { - /// Get the path to the wine binary. - pub fn wine_binary(&self) -> Option<PathBuf> { - let paths = [ - self.path.join("files/bin/wine"), - self.path.join("dist/bin/wine"), - ]; - paths.into_iter().find(|p| p.exists()) - } - - /// Get the path to the wineserver binary. - pub fn wineserver_binary(&self) -> Option<PathBuf> { - let paths = [ - self.path.join("files/bin/wineserver"), - self.path.join("dist/bin/wineserver"), - ]; - paths.into_iter().find(|p| p.exists()) - } - - /// Get the bin directory containing wine executables. - pub fn bin_dir(&self) -> Option<PathBuf> { - self.wine_binary().and_then(|p| p.parent().map(|p| p.to_path_buf())) - } -} - -/// Find all Protons that Steam can use (Proton 10+ only) -pub fn find_steam_protons() -> Vec<SteamProton> { - let mut protons = Vec::new(); - - let Some(steam_path) = find_steam_path() else { - return protons; - }; - - // 1. Steam's built-in Protons (steamapps/common/Proton*) - protons.extend(find_builtin_protons(&steam_path)); - - // 2. Custom Protons in user's compatibilitytools.d - protons.extend(find_custom_protons(&steam_path)); - - // 3. System-level Protons in /usr/share/steam/compatibilitytools.d/ - // (Arch packages Proton here; Flatpak has --filesystem=/usr/share/steam:ro) - protons.extend(find_system_protons()); - - // Filter to only include Proton 10+ (required for Steam-native integration) - protons.retain(is_proton_10_or_newer); - - // Filter to only include Protons with valid wine binaries - protons.retain(|p| { - let has_wine = p.wine_binary().is_some(); - if !has_wine { - crate::logging::log_warning(&format!( - "Skipping Proton '{}': wine binary not found at expected paths (files/bin/wine or dist/bin/wine)", - p.name - )); - } - has_wine - }); - - // Sort: Experimental first, then by name descending (newest first) - protons.sort_by(|a, b| { - if a.is_experimental != b.is_experimental { - return b.is_experimental.cmp(&a.is_experimental); - } - b.name.cmp(&a.name) - }); - - protons -} - -/// Check if a Proton version is 10 or newer -fn is_proton_10_or_newer(proton: &SteamProton) -> bool { - let name = &proton.name; - - if proton.is_experimental || name.contains("Experimental") { - return true; - } - - if name.contains("CachyOS") { - return true; - } - - if name == "LegacyRuntime" || name.contains("Runtime") { - return false; - } - - if name.starts_with("GE-Proton") { - if let Some(version_part) = name.strip_prefix("GE-Proton") { - let major: Option<u32> = version_part - .split('-') - .next() - .and_then(|s| s.parse().ok()); - return major.map(|v| v >= 10).unwrap_or(false); - } - } - - if name.starts_with("Proton ") { - if let Some(version_part) = name.strip_prefix("Proton ") { - let major: Option<u32> = version_part - .split('.') - .next() - .and_then(|s| s.parse().ok()); - return major.map(|v| v >= 10).unwrap_or(false); - } - } - - if name.starts_with("EM-") { - if let Some(version_part) = name.strip_prefix("EM-") { - let major: Option<u32> = version_part - .split('.') - .next() - .and_then(|s| s.parse().ok()); - return major.map(|v| v >= 10).unwrap_or(false); - } - } - - // Unknown format - allow it - true -} - -/// Find Steam's built-in Proton versions -fn find_builtin_protons(steam_path: &std::path::Path) -> Vec<SteamProton> { - let mut found = Vec::new(); - let common_dir = steam_path.join("steamapps/common"); - - let Ok(entries) = fs::read_dir(&common_dir) else { - return found; - }; - - for entry in entries.flatten() { - let path = entry.path(); - if !path.is_dir() { - continue; - } - - let name = entry.file_name().to_string_lossy().to_string(); - - if name.starts_with("Proton") && path.join("proton").exists() { - let is_experimental = name.contains("Experimental"); - - let config_name = if is_experimental { - "proton_experimental".to_string() - } else { - let version = name.replace("Proton ", ""); - let major = version.split('.').next().unwrap_or(&version); - format!("proton_{}", major) - }; - - found.push(SteamProton { - name: name.clone(), - config_name, - path, - is_steam_proton: true, - is_experimental, - }); - } - } - - found -} - -/// Find custom Protons in compatibilitytools.d -fn find_custom_protons(steam_path: &std::path::Path) -> Vec<SteamProton> { - let mut found = Vec::new(); - let compat_dir = steam_path.join("compatibilitytools.d"); - - let Ok(entries) = fs::read_dir(&compat_dir) else { - return found; - }; - - for entry in entries.flatten() { - let path = entry.path(); - if !path.is_dir() { - continue; - } - - let name = entry.file_name().to_string_lossy().to_string(); - - let has_proton = path.join("proton").exists(); - let has_vdf = path.join("compatibilitytool.vdf").exists(); - - if has_proton || has_vdf { - found.push(SteamProton { - name: name.clone(), - config_name: name.clone(), - path, - is_steam_proton: false, - is_experimental: false, - }); - } - } - - found -} - -/// Find system-level Protons in /usr/share/steam/compatibilitytools.d/ -fn find_system_protons() -> Vec<SteamProton> { - let mut found = Vec::new(); - let system_compat_dir = PathBuf::from("/usr/share/steam/compatibilitytools.d"); - - let Ok(entries) = fs::read_dir(&system_compat_dir) else { - return found; - }; - - for entry in entries.flatten() { - let path = entry.path(); - if !path.is_dir() { - continue; - } - - let name = entry.file_name().to_string_lossy().to_string(); - - let has_proton = path.join("proton").exists(); - let has_vdf = path.join("compatibilitytool.vdf").exists(); - - if has_proton || has_vdf { - found.push(SteamProton { - name: name.clone(), - config_name: name.clone(), - path, - is_steam_proton: false, - is_experimental: false, - }); - } - } - - found -} diff --git a/libs/nak/src/utils.rs b/libs/nak/src/utils.rs deleted file mode 100644 index 7d7886c..0000000 --- a/libs/nak/src/utils.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! Shared utility functions used across the application - -use std::error::Error; -use std::fs; -use std::path::Path; - -/// Download a file from URL to the specified path -pub fn download_file(url: &str, path: &Path) -> Result<(), Box<dyn Error>> { - // Ensure parent directory exists - if let Some(parent) = path.parent() { - fs::create_dir_all(parent)?; - } - - let resp = ureq::get(url).call()?; - let mut reader = resp.into_reader(); - let mut file = fs::File::create(path)?; - std::io::copy(&mut reader, &mut file)?; - Ok(()) -} diff --git a/libs/nak_ffi/CMakeLists.txt b/libs/nak_ffi/CMakeLists.txt deleted file mode 100644 index 72d939f..0000000 --- a/libs/nak_ffi/CMakeLists.txt +++ /dev/null @@ -1,38 +0,0 @@ -cmake_minimum_required(VERSION 3.16)
-project(nak_ffi)
-
-if(CMAKE_BUILD_TYPE STREQUAL "Debug")
- set(CARGO_BUILD_TYPE "debug")
- set(CARGO_BUILD_FLAGS "")
-else()
- set(CARGO_BUILD_TYPE "release")
- set(CARGO_BUILD_FLAGS "--release")
-endif()
-
-set(NAK_FFI_DIR ${CMAKE_CURRENT_SOURCE_DIR})
-set(NAK_FFI_LIB ${NAK_FFI_DIR}/target/${CARGO_BUILD_TYPE}/libnak_ffi.so)
-
-# Glob all Rust sources so any change triggers a cargo rebuild
-file(GLOB_RECURSE NAK_FFI_SOURCES ${NAK_FFI_DIR}/src/*.rs)
-file(GLOB_RECURSE NAK_SOURCES ${NAK_FFI_DIR}/../nak/src/*.rs)
-
-add_custom_command(
- OUTPUT ${NAK_FFI_LIB}
- COMMAND cargo build ${CARGO_BUILD_FLAGS}
- WORKING_DIRECTORY ${NAK_FFI_DIR}
- COMMENT "Building NaK FFI library (Rust)"
- DEPENDS ${NAK_FFI_DIR}/Cargo.toml ${NAK_FFI_DIR}/../nak/Cargo.toml
- ${NAK_FFI_SOURCES} ${NAK_SOURCES}
-)
-
-add_custom_target(nak_ffi_build DEPENDS ${NAK_FFI_LIB})
-
-# INTERFACE wrapper so ALIAS works (CMake can't alias IMPORTED targets)
-add_library(nak_ffi_wrapper INTERFACE)
-target_link_libraries(nak_ffi_wrapper INTERFACE ${NAK_FFI_LIB})
-target_include_directories(nak_ffi_wrapper INTERFACE ${NAK_FFI_DIR}/include)
-add_dependencies(nak_ffi_wrapper nak_ffi_build)
-add_library(mo2::nak_ffi ALIAS nak_ffi_wrapper)
-
-install(FILES ${NAK_FFI_LIB} DESTINATION lib)
-install(DIRECTORY ${NAK_FFI_DIR}/include/ DESTINATION include)
diff --git a/libs/nak_ffi/Cargo.lock b/libs/nak_ffi/Cargo.lock deleted file mode 100644 index e8be402..0000000 --- a/libs/nak_ffi/Cargo.lock +++ /dev/null @@ -1,971 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bumpalo" -version = "3.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" - -[[package]] -name = "cc" -version = "1.2.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "chrono" -version = "0.4.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "dataview" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daba87f72c730b508641c9fb6411fc9bba73939eed2cab611c399500511880d0" -dependencies = [ - "derive_pod", -] - -[[package]] -name = "derive_pod" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ea6706d74fca54e15f1d40b5cf7fe7f764aaec61352a9fcec58fe27e042fc8" - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "itoa" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" - -[[package]] -name = "js-sys" -version = "0.3.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "libc" -version = "0.2.180" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "nak_ffi" -version = "0.1.0" -dependencies = [ - "nak_rust", -] - -[[package]] -name = "nak_rust" -version = "0.1.0" -dependencies = [ - "chrono", - "pelite", - "serde", - "serde_json", - "ureq", - "walkdir", -] - -[[package]] -name = "no-std-compat" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "pelite" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88dccf4bd32294364aeb7bd55d749604450e9db54605887551f21baea7617685" -dependencies = [ - "dataview", - "libc", - "no-std-compat", - "pelite-macros", - "winapi", -] - -[[package]] -name = "pelite-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a7cf3f8ecebb0f4895f4892a8be0a0dc81b498f9d56735cb769dc31bf00815b" - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustls" -version = "0.23.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" -dependencies = [ - "log", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" -dependencies = [ - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "simd-adler32" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "unicode-ident" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "ureq" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" -dependencies = [ - "base64", - "flate2", - "log", - "once_cell", - "rustls", - "rustls-pki-types", - "url", - "webpki-roots 0.26.11", -] - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasm-bindgen" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.6", -] - -[[package]] -name = "webpki-roots" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zmij" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" diff --git a/libs/nak_ffi/Cargo.toml b/libs/nak_ffi/Cargo.toml deleted file mode 100644 index cdf98df..0000000 --- a/libs/nak_ffi/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package]
-name = "nak_ffi"
-version = "0.1.0"
-edition = "2021"
-
-[lib]
-crate-type = ["cdylib"]
-
-[dependencies]
-nak_rust = { path = "../nak" }
diff --git a/libs/nak_ffi/include/nak_ffi.h b/libs/nak_ffi/include/nak_ffi.h deleted file mode 100644 index 7a40863..0000000 --- a/libs/nak_ffi/include/nak_ffi.h +++ /dev/null @@ -1,230 +0,0 @@ -#ifndef NAK_FFI_H
-#define NAK_FFI_H
-
-#include <stddef.h>
-#include <stdint.h>
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-/* ========================================================================
- * Tier 1: Game Detection
- * ======================================================================== */
-
-/** A detected game installation */
-typedef struct {
- char *name;
- char *app_id;
- char *install_path;
- char *prefix_path; /* NULL if no prefix */
- char *launcher; /* display name string */
- char *my_games_folder; /* NULL if not applicable */
- char *appdata_local_folder; /* NULL if not applicable */
- char *appdata_roaming_folder; /* NULL if not applicable */
- char *registry_path; /* NULL if not applicable */
- char *registry_value; /* NULL if not applicable */
-} NakGame;
-
-/** List of detected games */
-typedef struct {
- NakGame *games;
- size_t count;
- size_t steam_count;
- size_t heroic_count;
- size_t bottles_count;
-} NakGameList;
-
-/** Detect all installed games across all launchers */
-NakGameList nak_detect_all_games(void);
-
-/** Free a NakGameList returned by nak_detect_all_games */
-void nak_game_list_free(NakGameList list);
-
-/** A known game definition (static data, do NOT free) */
-typedef struct {
- const char *name;
- const char *steam_app_id;
- const char *gog_app_id; /* NULL if none */
- const char *epic_app_id; /* NULL if none */
- const char *my_games_folder; /* NULL if not applicable */
- const char *appdata_local_folder; /* NULL if not applicable */
- const char *appdata_roaming_folder; /* NULL if not applicable */
- const char *registry_path;
- const char *registry_value;
- const char *steam_folder;
-} NakKnownGame;
-
-/** Get the list of all known games (static data, do NOT free).
- * Returns pointer to array; writes count to *out_count. */
-const NakKnownGame *nak_get_known_games(size_t *out_count);
-
-/* ========================================================================
- * Tier 2: Proton Detection
- * ======================================================================== */
-
-/** An installed Proton version */
-typedef struct {
- char *name;
- char *config_name;
- char *path;
- int is_steam_proton;
- int is_experimental;
-} NakSteamProton;
-
-/** List of detected Proton installations */
-typedef struct {
- NakSteamProton *protons;
- size_t count;
-} NakProtonList;
-
-/** Find all installed Proton versions */
-NakProtonList nak_find_steam_protons(void);
-
-/** Free a NakProtonList */
-void nak_proton_list_free(NakProtonList list);
-
-/* ========================================================================
- * Tier 3: Steam Paths
- * ======================================================================== */
-
-/** Find the Steam installation path.
- * Returns newly allocated string (free with nak_string_free), or NULL. */
-char *nak_find_steam_path(void);
-
-/* ========================================================================
- * Tier 4: Dependency Installation (callback-based)
- * ======================================================================== */
-
-/** Callback for status/log messages */
-typedef void (*NakStatusCallback)(const char *message);
-typedef void (*NakLogCallback)(const char *message);
-
-/** Callback for progress updates (0.0 to 1.0) */
-typedef void (*NakProgressCallback)(float progress);
-
-/** Install all Wine prefix dependencies (blocking call).
- * cancel_flag: pointer to int, set non-zero to cancel.
- * Returns NULL on success, or error message (free with nak_string_free). */
-char *nak_install_all_dependencies(
- const char *prefix_path,
- const char *proton_name,
- const char *proton_path,
- NakStatusCallback status_cb,
- NakLogCallback log_cb,
- NakProgressCallback progress_cb,
- const int *cancel_flag,
- uint32_t app_id
-);
-
-/** Apply Wine registry settings to a prefix.
- * Returns NULL on success, or error message (free with nak_string_free). */
-char *nak_apply_wine_registry_settings(
- const char *prefix_path,
- const char *proton_name,
- const char *proton_path,
- NakLogCallback log_cb,
- uint32_t app_id
-);
-
-/** Apply a game's registry entry with a custom install path.
- * Looks up game_name in KNOWN_GAMES and writes registry pointing to install_path.
- * Returns NULL on success, or an error message (free with nak_string_free). */
-char *nak_apply_registry_for_game_path(
- const char *prefix_path,
- const char *proton_name,
- const char *proton_path,
- const char *game_name,
- const char *install_path,
- NakLogCallback log_cb
-);
-
-/* ========================================================================
- * Tier 5: Prefix Symlinks
- * ======================================================================== */
-
-/** Ensure AppData/Local/Temp exists in the Wine prefix.
- * Call during prefix creation. */
-void nak_ensure_temp_directory(const char *prefix_path);
-
-/** Detect games and create symlinks from the prefix to game prefixes.
- * Call during prefix creation. */
-void nak_create_game_symlinks_auto(const char *prefix_path);
-
-/* ========================================================================
- * Tier 6: Logging
- * ======================================================================== */
-
-/** Callback for NaK log messages: (level, message).
- * Levels: "info", "warning", "error", "install", "action", "download" */
-typedef void (*NakLogLevelCallback)(const char *level, const char *message);
-
-/** Initialize NaK logging with a callback.
- * Call once at startup before any other nak_* functions. */
-void nak_init_logging(NakLogLevelCallback cb);
-
-/* ========================================================================
- * Tier 7: DXVK Configuration
- * ======================================================================== */
-
-/** Ensure the DXVK config file exists, downloading if necessary.
- * Returns NULL on success, or error message (free with nak_string_free). */
-char *nak_ensure_dxvk_conf(void);
-
-/** Get the path to the DXVK config file.
- * Returns newly allocated string (free with nak_string_free). */
-char *nak_get_dxvk_conf_path(void);
-
-/* ========================================================================
- * Tier 8: Steam Linux Runtime (SLR)
- * ======================================================================== */
-
-/** Returns 1 if SteamLinuxRuntime_sniper is installed and the run script exists, 0 otherwise. */
-int nak_slr_is_installed(void);
-
-/** Get the path to the SLR run script.
- * Returns NULL if SLR is not installed.
- * Caller must free with nak_string_free(). */
-char *nak_slr_get_run_script(void);
-
-/** Download and install SteamLinuxRuntime_sniper from Valve's repo (~180 MB).
- * Skips if already at the latest version (checked via BUILD_ID).
- * progress_cb: 0.0..1.0 during download (may be NULL).
- * status_cb: human-readable status strings (may be NULL).
- * cancel_flag: pointer to int, set non-zero to cancel (may be NULL).
- * Returns NULL on success, or error message (free with nak_string_free). */
-char *nak_download_slr(
- NakProgressCallback progress_cb,
- NakStatusCallback status_cb,
- const int *cancel_flag
-);
-
-/* ========================================================================
- * Tier 9: PE Icon Extraction
- * ======================================================================== */
-
-/** Result of icon extraction */
-typedef struct {
- uint8_t *data; /**< Raw ICO file bytes (NULL if extraction failed) */
- size_t len; /**< Length in bytes (0 if extraction failed) */
-} NakIconData;
-
-/** Extract the best icon from a Windows PE executable (.exe/.dll).
- * Returns raw ICO bytes. Free with nak_icon_data_free(). */
-NakIconData nak_extract_exe_icon(const char *exe_path);
-
-/** Free icon data returned by nak_extract_exe_icon */
-void nak_icon_data_free(NakIconData icon);
-
-/* ========================================================================
- * General
- * ======================================================================== */
-
-/** Free a string returned by any nak_* function */
-void nak_string_free(char *s);
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif /* NAK_FFI_H */
diff --git a/libs/nak_ffi/src/lib.rs b/libs/nak_ffi/src/lib.rs deleted file mode 100644 index a08cb0d..0000000 --- a/libs/nak_ffi/src/lib.rs +++ /dev/null @@ -1,750 +0,0 @@ -//! NaK FFI - C bindings for NaK game detection and Proton management
-//!
-//! Memory management rules:
-//! - Owned strings returned as `*mut c_char` must be freed with `nak_string_free()`
-//! - Struct lists (NakGameList, etc.) must be freed with their corresponding `_free()` fn
-//! - Error returns: functions returning `*mut c_char` for errors use null = success
-//! - `NakKnownGame` pointers are static data and must NOT be freed
-
-use std::ffi::{c_char, c_float, c_int, CStr, CString};
-use std::path::{Path, PathBuf};
-use std::ptr;
-use std::sync::atomic::{AtomicBool, Ordering};
-use std::sync::{Arc, LazyLock, Mutex};
-
-// ============================================================================
-// Helper functions
-// ============================================================================
-
-fn to_cstring(s: &str) -> *mut c_char {
- CString::new(s).unwrap_or_default().into_raw()
-}
-
-fn to_cstring_opt(s: Option<&str>) -> *mut c_char {
- match s {
- Some(s) => to_cstring(s),
- None => ptr::null_mut(),
- }
-}
-
-unsafe fn from_cstr<'a>(p: *const c_char) -> &'a str {
- if p.is_null() {
- ""
- } else {
- unsafe { CStr::from_ptr(p) }.to_str().unwrap_or("")
- }
-}
-
-fn error_to_cstring(e: Box<dyn std::error::Error>) -> *mut c_char {
- to_cstring(&e.to_string())
-}
-
-/// Find a Proton installation by path, using canonicalization to handle
-/// symlinks and path normalization (e.g. system Protons in
-/// /usr/share/steam/compatibilitytools.d/).
-fn find_proton_by_path(proton_path_str: &str) -> Option<nak_rust::steam::SteamProton> {
- let target = std::fs::canonicalize(proton_path_str)
- .unwrap_or_else(|_| PathBuf::from(proton_path_str));
- nak_rust::steam::find_steam_protons()
- .into_iter()
- .find(|p| {
- std::fs::canonicalize(&p.path)
- .unwrap_or_else(|_| p.path.clone()) == target
- })
-}
-
-// ============================================================================
-// Tier 1: Game Detection
-// ============================================================================
-
-/// A detected game installation (C-compatible)
-#[repr(C)]
-pub struct NakGame {
- pub name: *mut c_char,
- pub app_id: *mut c_char,
- pub install_path: *mut c_char,
- pub prefix_path: *mut c_char, // null if no prefix
- pub launcher: *mut c_char, // display name string
- pub my_games_folder: *mut c_char,
- pub appdata_local_folder: *mut c_char,
- pub appdata_roaming_folder: *mut c_char,
- pub registry_path: *mut c_char,
- pub registry_value: *mut c_char,
-}
-
-/// List of detected games
-#[repr(C)]
-pub struct NakGameList {
- pub games: *mut NakGame,
- pub count: usize,
- pub steam_count: usize,
- pub heroic_count: usize,
- pub bottles_count: usize,
-}
-
-#[derive(Clone)]
-struct CachedGame {
- name: String,
- app_id: String,
- install_path: String,
- prefix_path: Option<String>,
- launcher: String,
- my_games_folder: Option<String>,
- appdata_local_folder: Option<String>,
- appdata_roaming_folder: Option<String>,
- registry_path: Option<String>,
- registry_value: Option<String>,
-}
-
-#[derive(Clone, Default)]
-struct CachedGameList {
- games: Vec<CachedGame>,
- steam_count: usize,
- heroic_count: usize,
- bottles_count: usize,
-}
-
-static DETECTED_GAMES_CACHE: LazyLock<Mutex<Option<CachedGameList>>> =
- LazyLock::new(|| Mutex::new(None));
-
-fn detect_games_cached() -> CachedGameList {
- let mut cache = DETECTED_GAMES_CACHE.lock().unwrap();
- if let Some(cached) = cache.as_ref() {
- return cached.clone();
- }
-
- let result = nak_rust::game_finder::detect_all_games();
- let cached = CachedGameList {
- games: result
- .games
- .iter()
- .map(|g| CachedGame {
- name: g.name.clone(),
- app_id: g.app_id.clone(),
- install_path: g.install_path.to_string_lossy().into_owned(),
- prefix_path: g
- .prefix_path
- .as_ref()
- .map(|p| p.to_string_lossy().into_owned()),
- launcher: g.launcher.display_name().to_string(),
- my_games_folder: g.my_games_folder.clone(),
- appdata_local_folder: g.appdata_local_folder.clone(),
- appdata_roaming_folder: g.appdata_roaming_folder.clone(),
- registry_path: g.registry_path.clone(),
- registry_value: g.registry_value.clone(),
- })
- .collect(),
- steam_count: result.steam_count,
- heroic_count: result.heroic_count,
- bottles_count: result.bottles_count,
- };
-
- *cache = Some(cached.clone());
- cached
-}
-
-/// Detect all installed games across all launchers
-#[no_mangle]
-pub extern "C" fn nak_detect_all_games() -> NakGameList {
- let result = detect_games_cached();
-
- let mut games: Vec<NakGame> = result
- .games
- .iter()
- .map(|g| NakGame {
- name: to_cstring(&g.name),
- app_id: to_cstring(&g.app_id),
- install_path: to_cstring(&g.install_path),
- prefix_path: match &g.prefix_path {
- Some(p) => to_cstring(p),
- None => ptr::null_mut(),
- },
- launcher: to_cstring(&g.launcher),
- my_games_folder: to_cstring_opt(g.my_games_folder.as_deref()),
- appdata_local_folder: to_cstring_opt(g.appdata_local_folder.as_deref()),
- appdata_roaming_folder: to_cstring_opt(g.appdata_roaming_folder.as_deref()),
- registry_path: to_cstring_opt(g.registry_path.as_deref()),
- registry_value: to_cstring_opt(g.registry_value.as_deref()),
- })
- .collect();
-
- let list = NakGameList {
- games: games.as_mut_ptr(),
- count: games.len(),
- steam_count: result.steam_count,
- heroic_count: result.heroic_count,
- bottles_count: result.bottles_count,
- };
- std::mem::forget(games);
- list
-}
-
-/// Free a NakGameList returned by nak_detect_all_games
-#[no_mangle]
-pub unsafe extern "C" fn nak_game_list_free(list: NakGameList) {
- if list.games.is_null() {
- return;
- }
- let games = unsafe { Vec::from_raw_parts(list.games, list.count, list.count) };
- for g in games {
- free_if_nonnull(g.name);
- free_if_nonnull(g.app_id);
- free_if_nonnull(g.install_path);
- free_if_nonnull(g.prefix_path);
- free_if_nonnull(g.launcher);
- free_if_nonnull(g.my_games_folder);
- free_if_nonnull(g.appdata_local_folder);
- free_if_nonnull(g.appdata_roaming_folder);
- free_if_nonnull(g.registry_path);
- free_if_nonnull(g.registry_value);
- }
-}
-
-unsafe fn free_if_nonnull(p: *mut c_char) {
- if !p.is_null() {
- let _ = unsafe { CString::from_raw(p) };
- }
-}
-
-/// A known game definition (static data, do NOT free)
-#[repr(C)]
-pub struct NakKnownGame {
- pub name: *const c_char,
- pub steam_app_id: *const c_char,
- pub gog_app_id: *const c_char, // null if none
- pub epic_app_id: *const c_char, // null if none
- pub my_games_folder: *const c_char,
- pub appdata_local_folder: *const c_char,
- pub appdata_roaming_folder: *const c_char,
- pub registry_path: *const c_char,
- pub registry_value: *const c_char,
- pub steam_folder: *const c_char,
-}
-
-// We need to leak CStrings for the static known games list since the Rust statics
-// are &str, not null-terminated. We build the list once and leak it.
-// Raw pointers in NakKnownGame prevent Send/Sync, so we wrap in a newtype.
-struct KnownGamesVec(Vec<NakKnownGame>);
-// SAFETY: The leaked CStrings are effectively 'static and immutable after initialization.
-unsafe impl Send for KnownGamesVec {}
-unsafe impl Sync for KnownGamesVec {}
-
-static KNOWN_GAMES_FFI: std::sync::LazyLock<KnownGamesVec> = std::sync::LazyLock::new(|| {
- KnownGamesVec(
- nak_rust::game_finder::KNOWN_GAMES
- .iter()
- .map(|kg| NakKnownGame {
- name: leak_str(kg.name),
- steam_app_id: leak_str(kg.steam_app_id),
- gog_app_id: leak_str_opt(kg.gog_app_id),
- epic_app_id: leak_str_opt(kg.epic_app_id),
- my_games_folder: leak_str_opt(kg.my_games_folder),
- appdata_local_folder: leak_str_opt(kg.appdata_local_folder),
- appdata_roaming_folder: leak_str_opt(kg.appdata_roaming_folder),
- registry_path: leak_str(kg.registry_path),
- registry_value: leak_str(kg.registry_value),
- steam_folder: leak_str(kg.steam_folder),
- })
- .collect(),
- )
-});
-
-fn leak_str(s: &str) -> *const c_char {
- CString::new(s).unwrap_or_default().into_raw() as *const c_char
-}
-
-fn leak_str_opt(s: Option<&str>) -> *const c_char {
- match s {
- Some(s) => leak_str(s),
- None => ptr::null(),
- }
-}
-
-/// Get the list of all known games (static data, do NOT free)
-///
-/// Returns a pointer to the first element and writes the count to `out_count`.
-#[no_mangle]
-pub unsafe extern "C" fn nak_get_known_games(out_count: *mut usize) -> *const NakKnownGame {
- let games = &KNOWN_GAMES_FFI.0;
- if !out_count.is_null() {
- *out_count = games.len();
- }
- games.as_ptr()
-}
-
-// ============================================================================
-// Tier 2: Proton Detection
-// ============================================================================
-
-/// An installed Proton version (C-compatible)
-#[repr(C)]
-pub struct NakSteamProton {
- pub name: *mut c_char,
- pub config_name: *mut c_char,
- pub path: *mut c_char,
- pub is_steam_proton: c_int,
- pub is_experimental: c_int,
-}
-
-/// List of detected Proton installations
-#[repr(C)]
-pub struct NakProtonList {
- pub protons: *mut NakSteamProton,
- pub count: usize,
-}
-
-/// Find all installed Proton versions
-#[no_mangle]
-pub extern "C" fn nak_find_steam_protons() -> NakProtonList {
- let protons = nak_rust::steam::find_steam_protons();
-
- let mut ffi_protons: Vec<NakSteamProton> = protons
- .iter()
- .map(|p| NakSteamProton {
- name: to_cstring(&p.name),
- config_name: to_cstring(&p.config_name),
- path: to_cstring(&p.path.to_string_lossy()),
- is_steam_proton: p.is_steam_proton as c_int,
- is_experimental: p.is_experimental as c_int,
- })
- .collect();
-
- let list = NakProtonList {
- protons: ffi_protons.as_mut_ptr(),
- count: ffi_protons.len(),
- };
- std::mem::forget(ffi_protons);
- list
-}
-
-/// Free a NakProtonList
-#[no_mangle]
-pub unsafe extern "C" fn nak_proton_list_free(list: NakProtonList) {
- if list.protons.is_null() {
- return;
- }
- let protons = unsafe { Vec::from_raw_parts(list.protons, list.count, list.count) };
- for p in protons {
- free_if_nonnull(p.name);
- free_if_nonnull(p.config_name);
- free_if_nonnull(p.path);
- }
-}
-
-// ============================================================================
-// Tier 3: Steam Paths
-// ============================================================================
-
-/// Find the Steam installation path
-///
-/// Returns a newly allocated string (caller must free with nak_string_free),
-/// or null if Steam is not found.
-#[no_mangle]
-pub extern "C" fn nak_find_steam_path() -> *mut c_char {
- match nak_rust::steam::find_steam_path() {
- Some(path) => to_cstring(&path.to_string_lossy()),
- None => ptr::null_mut(),
- }
-}
-
-// ============================================================================
-// Tier 4: Dependency Installation (callback-based)
-// ============================================================================
-
-/// Callback for status messages: fn(message: *const c_char)
-pub type NakStatusCallback = Option<unsafe extern "C" fn(*const c_char)>;
-
-/// Callback for log messages: fn(message: *const c_char)
-pub type NakLogCallback = Option<unsafe extern "C" fn(*const c_char)>;
-
-/// Callback for progress updates: fn(progress: f32) where 0.0..=1.0
-pub type NakProgressCallback = Option<unsafe extern "C" fn(c_float)>;
-
-/// Install all Wine prefix dependencies (winetricks, .NET, registry, etc.)
-///
-/// This is a blocking call. Use callbacks for progress updates.
-/// `cancel_flag` should point to an int that can be set to non-zero to cancel.
-///
-/// Returns null on success, or an error message (caller must free with nak_string_free).
-#[no_mangle]
-pub unsafe extern "C" fn nak_install_all_dependencies(
- prefix_path: *const c_char,
- proton_name: *const c_char,
- proton_path: *const c_char,
- status_cb: NakStatusCallback,
- log_cb: NakLogCallback,
- progress_cb: NakProgressCallback,
- cancel_flag: *const c_int,
- app_id: u32,
-) -> *mut c_char {
- let prefix = unsafe { from_cstr(prefix_path) };
- let _proton_name = unsafe { from_cstr(proton_name) };
- let proton_path_str = unsafe { from_cstr(proton_path) };
-
- // Find the matching SteamProton by path (canonicalized for symlink support)
- let proton = match find_proton_by_path(proton_path_str) {
- Some(p) => p,
- None => {
- return to_cstring(&format!(
- "Proton not found at path: {}",
- proton_path_str
- ));
- }
- };
-
- // Build cancel flag from raw pointer
- let cancel = Arc::new(AtomicBool::new(false));
- let cancel_clone = cancel.clone();
-
- // Spawn a thread to poll the C cancel flag
- let cancel_flag_ptr = cancel_flag as usize; // safe to send across threads
- let poll_handle = std::thread::spawn(move || {
- while !cancel_clone.load(Ordering::Relaxed) {
- std::thread::sleep(std::time::Duration::from_millis(100));
- if cancel_flag_ptr != 0 {
- let flag = unsafe { *(cancel_flag_ptr as *const c_int) };
- if flag != 0 {
- cancel_clone.store(true, Ordering::Relaxed);
- break;
- }
- }
- }
- });
-
- let ctx = nak_rust::installers::TaskContext::new(
- move |msg| {
- if let Some(cb) = status_cb {
- let c = CString::new(msg).unwrap_or_default();
- unsafe { cb(c.as_ptr()) };
- }
- },
- move |msg| {
- if let Some(cb) = log_cb {
- let c = CString::new(msg).unwrap_or_default();
- unsafe { cb(c.as_ptr()) };
- }
- },
- move |p| {
- if let Some(cb) = progress_cb {
- unsafe { cb(p) };
- }
- },
- cancel.clone(),
- );
-
- let result = nak_rust::installers::install_all_dependencies(
- Path::new(prefix),
- &proton,
- &ctx,
- 0.0,
- 1.0,
- app_id,
- );
-
- // Stop the cancel polling thread
- cancel.store(true, Ordering::Relaxed);
- let _ = poll_handle.join();
-
- match result {
- Ok(()) => ptr::null_mut(),
- Err(e) => error_to_cstring(e),
- }
-}
-
-/// Apply Wine registry settings to a prefix
-///
-/// Returns null on success, or an error message (caller must free with nak_string_free).
-#[no_mangle]
-pub unsafe extern "C" fn nak_apply_wine_registry_settings(
- prefix_path: *const c_char,
- proton_name: *const c_char,
- proton_path: *const c_char,
- log_cb: NakLogCallback,
- app_id: u32,
-) -> *mut c_char {
- let prefix = unsafe { from_cstr(prefix_path) };
- let _proton_name = unsafe { from_cstr(proton_name) };
- let proton_path_str = unsafe { from_cstr(proton_path) };
-
- let proton = match find_proton_by_path(proton_path_str) {
- Some(p) => p,
- None => {
- return to_cstring(&format!(
- "Proton not found at path: {}",
- proton_path_str
- ));
- }
- };
-
- let log_fn = move |msg: String| {
- if let Some(cb) = log_cb {
- let c = CString::new(msg).unwrap_or_default();
- unsafe { cb(c.as_ptr()) };
- }
- };
-
- let app_id_opt = if app_id == 0 { None } else { Some(app_id) };
-
- match nak_rust::installers::apply_wine_registry_settings(
- Path::new(prefix),
- &proton,
- &log_fn,
- app_id_opt,
- ) {
- Ok(()) => ptr::null_mut(),
- Err(e) => error_to_cstring(e),
- }
-}
-
-/// Apply a game's registry entry with a custom install path.
-///
-/// Looks up the game by name in KNOWN_GAMES, writes the registry entry
-/// pointing to `install_path`. Returns null on success, or an error message.
-#[no_mangle]
-pub unsafe extern "C" fn nak_apply_registry_for_game_path(
- prefix_path: *const c_char,
- proton_name: *const c_char,
- proton_path: *const c_char,
- game_name: *const c_char,
- install_path: *const c_char,
- log_cb: NakLogCallback,
-) -> *mut c_char {
- let prefix = unsafe { from_cstr(prefix_path) };
- let _proton_name = unsafe { from_cstr(proton_name) };
- let proton_path_str = unsafe { from_cstr(proton_path) };
- let game = unsafe { from_cstr(game_name) };
- let install = unsafe { from_cstr(install_path) };
-
- let proton = match find_proton_by_path(proton_path_str) {
- Some(p) => p,
- None => {
- return to_cstring(&format!(
- "Proton not found at path: {}",
- proton_path_str
- ));
- }
- };
-
- let log_fn = move |msg: String| {
- if let Some(cb) = log_cb {
- let c = CString::new(msg).unwrap_or_default();
- unsafe { cb(c.as_ptr()) };
- }
- };
-
- match nak_rust::installers::apply_registry_for_game_path(
- Path::new(prefix),
- &proton,
- game,
- Path::new(install),
- &log_fn,
- ) {
- Ok(()) => ptr::null_mut(),
- Err(e) => to_cstring(&e),
- }
-}
-
-// ============================================================================
-// Tier 5: Prefix Symlinks
-// ============================================================================
-
-/// Ensure the Temp directory exists in the Wine prefix's AppData/Local.
-///
-/// MO2 and other tools require AppData/Local/Temp to exist.
-#[no_mangle]
-pub unsafe extern "C" fn nak_ensure_temp_directory(prefix_path: *const c_char) {
- let prefix = unsafe { from_cstr(prefix_path) };
- nak_rust::installers::symlinks::ensure_temp_directory(Path::new(prefix));
-}
-
-/// Detect installed games and create symlinks from the prefix to game prefixes.
-///
-/// This is a convenience wrapper that detects games and creates symlinks in one call.
-#[no_mangle]
-pub unsafe extern "C" fn nak_create_game_symlinks_auto(prefix_path: *const c_char) {
- let prefix = unsafe { from_cstr(prefix_path) };
- nak_rust::installers::symlinks::create_game_symlinks_auto(Path::new(prefix));
-}
-
-// ============================================================================
-// Tier 6: Logging
-// ============================================================================
-
-/// Callback for NaK log messages: fn(level: *const c_char, message: *const c_char)
-///
-/// Levels: "info", "warning", "error", "install", "action", "download"
-pub type NakLogLevelCallback = Option<unsafe extern "C" fn(*const c_char, *const c_char)>;
-
-/// Initialize NaK logging with a callback.
-///
-/// The callback receives (level, message) for all NaK internal log messages.
-/// Call once at startup before any other nak_* functions.
-#[no_mangle]
-pub unsafe extern "C" fn nak_init_logging(cb: NakLogLevelCallback) {
- if let Some(callback) = cb {
- nak_rust::logging::set_log_callback(move |level: &str, message: &str| {
- let c_level = CString::new(level).unwrap_or_default();
- let c_msg = CString::new(message).unwrap_or_default();
- unsafe { callback(c_level.as_ptr(), c_msg.as_ptr()) };
- });
- }
-}
-
-// ============================================================================
-// Tier 7: DXVK Configuration
-// ============================================================================
-
-/// Ensure the DXVK config file exists, downloading if necessary.
-///
-/// Returns null on success, or an error message (caller must free with nak_string_free).
-#[no_mangle]
-pub extern "C" fn nak_ensure_dxvk_conf() -> *mut c_char {
- match nak_rust::dxvk::ensure_dxvk_conf() {
- Ok(_) => ptr::null_mut(),
- Err(e) => error_to_cstring(e),
- }
-}
-
-/// Get the path to the DXVK config file.
-///
-/// Returns a newly allocated string (caller must free with nak_string_free).
-#[no_mangle]
-pub extern "C" fn nak_get_dxvk_conf_path() -> *mut c_char {
- let path = nak_rust::dxvk::get_dxvk_conf_path();
- to_cstring(&path.to_string_lossy())
-}
-
-// ============================================================================
-// Tier 8: Steam Linux Runtime (SLR)
-// ============================================================================
-
-/// Returns 1 if the Steam Linux Runtime is installed and ready, 0 otherwise.
-#[no_mangle]
-pub extern "C" fn nak_slr_is_installed() -> c_int {
- nak_rust::slr::is_slr_installed() as c_int
-}
-
-/// Get the path to the SLR `run` script.
-///
-/// Returns null if SLR is not installed.
-/// Caller must free the returned string with nak_string_free().
-#[no_mangle]
-pub extern "C" fn nak_slr_get_run_script() -> *mut c_char {
- match nak_rust::slr::get_slr_run_script() {
- Some(path) => to_cstring(&path.to_string_lossy()),
- None => ptr::null_mut(),
- }
-}
-
-/// Download and install the Steam Linux Runtime (sniper).
-///
-/// Skips if already at the latest version.
-/// - `progress_cb`: called with 0.0..=1.0 during download (may be null)
-/// - `status_cb`: called with status strings (may be null)
-/// - `cancel_flag`: pointer to an int polled each chunk; set to non-zero to abort (may be null)
-///
-/// Returns null on success, or an error message (caller must free with nak_string_free).
-#[no_mangle]
-pub unsafe extern "C" fn nak_download_slr(
- progress_cb: NakProgressCallback,
- status_cb: NakStatusCallback,
- cancel_flag: *const c_int,
-) -> *mut c_char {
- use std::sync::atomic::{AtomicI32, Ordering};
-
- let progress = move |p: f32| {
- if let Some(cb) = progress_cb {
- unsafe { cb(p) };
- }
- };
- let status = move |s: &str| {
- if let Some(cb) = status_cb {
- let cs = CString::new(s).unwrap_or_default();
- unsafe { cb(cs.as_ptr()) };
- }
- };
-
- // Wrap the raw pointer in an AtomicI32 view for safe polling.
- // We create a local AtomicI32 and initialise it from the pointer value.
- let atomic_flag = AtomicI32::new(if cancel_flag.is_null() {
- 0
- } else {
- unsafe { *cancel_flag }
- });
-
- // Spawn a tiny polling closure that re-reads the C int each chunk.
- // Because we can't safely move a raw pointer into the closure across
- // threads we pass ownership of a copy; cancellation is best-effort.
- let flag_val: i32 = if cancel_flag.is_null() {
- 0
- } else {
- unsafe { *cancel_flag }
- };
- let _ = flag_val; // suppress unused warning — atomic_flag covers it
-
- match nak_rust::slr::download_slr(progress, status, &atomic_flag) {
- Ok(()) => ptr::null_mut(),
- Err(e) => error_to_cstring(e),
- }
-}
-
-// ============================================================================
-// Tier 9: PE Icon Extraction
-// ============================================================================
-
-/// Result of icon extraction
-#[repr(C)]
-pub struct NakIconData {
- /// Raw ICO file bytes (caller must free with nak_icon_data_free)
- pub data: *mut u8,
- /// Length of the data in bytes (0 if extraction failed)
- pub len: usize,
-}
-
-/// Extract the best icon from a Windows PE executable (.exe/.dll).
-///
-/// Returns NakIconData with the raw ICO file bytes.
-/// If extraction fails, data is null and len is 0.
-/// Caller must free the result with nak_icon_data_free().
-#[no_mangle]
-pub unsafe extern "C" fn nak_extract_exe_icon(exe_path: *const c_char) -> NakIconData {
- let path_str = unsafe { from_cstr(exe_path) };
- if path_str.is_empty() {
- return NakIconData {
- data: ptr::null_mut(),
- len: 0,
- };
- }
-
- match nak_rust::icons::extract_icon(std::path::Path::new(path_str)) {
- Some(bytes) => {
- let len = bytes.len();
- let mut boxed = bytes.into_boxed_slice();
- let ptr = boxed.as_mut_ptr();
- std::mem::forget(boxed);
- NakIconData { data: ptr, len }
- }
- None => NakIconData {
- data: ptr::null_mut(),
- len: 0,
- },
- }
-}
-
-/// Free icon data returned by nak_extract_exe_icon
-#[no_mangle]
-pub unsafe extern "C" fn nak_icon_data_free(icon: NakIconData) {
- if !icon.data.is_null() && icon.len > 0 {
- let _ = unsafe { Vec::from_raw_parts(icon.data, icon.len, icon.len) };
- }
-}
-
-// ============================================================================
-// General: String free
-// ============================================================================
-
-/// Free a string returned by any nak_* function
-#[no_mangle]
-pub unsafe extern "C" fn nak_string_free(s: *mut c_char) {
- free_if_nonnull(s);
-}
diff --git a/libs/uibase/src/CMakeLists.txt b/libs/uibase/src/CMakeLists.txt index 3015be3..5660289 100644 --- a/libs/uibase/src/CMakeLists.txt +++ b/libs/uibase/src/CMakeLists.txt @@ -187,8 +187,20 @@ target_link_libraries(uibase PUBLIC Qt6::Widgets Qt6::Network Qt6::QuickWidgets PRIVATE spdlog::spdlog_header_only Qt6::Qml Qt6::Quick) -if(NOT WIN32 AND TARGET mo2::nak_ffi) - target_link_libraries(uibase PRIVATE mo2::nak_ffi) +if(NOT WIN32) + # Native C++ ports of game detection, Steam/Proton detection, SLR management, + # icon extraction, and prefix symlinks (formerly in Rust nak_ffi). + # Use BUILD_INTERFACE to avoid "prefixed in the source directory" CMake error. + target_include_directories(uibase + PUBLIC $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/src/src>) + target_sources(uibase PRIVATE + ${CMAKE_SOURCE_DIR}/src/src/gamedetection.cpp + ${CMAKE_SOURCE_DIR}/src/src/steamdetection.cpp + ${CMAKE_SOURCE_DIR}/src/src/vdfparser.cpp + ${CMAKE_SOURCE_DIR}/src/src/iconextractor.cpp + ${CMAKE_SOURCE_DIR}/src/src/prefixsymlinks.cpp + ${CMAKE_SOURCE_DIR}/src/src/slrmanager.cpp + ) endif() # installation diff --git a/libs/uibase/src/utility.cpp b/libs/uibase/src/utility.cpp index e6811af..94cbc77 100644 --- a/libs/uibase/src/utility.cpp +++ b/libs/uibase/src/utility.cpp @@ -22,7 +22,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #include <uibase/log.h> #include <uibase/report.h> #ifndef _WIN32 -#include <nak_ffi.h> +#include "iconextractor.h" #endif #include <QApplication> #include <QBuffer> @@ -946,15 +946,12 @@ QIcon iconForExecutable(const QString& filePath) cache.insert(cacheKey, icon); return icon; #else - // Extract icon from PE executable via NaK (pelite-based, no external tools). + // Extract icon from PE executable (native C++ parser, no external tools). QIcon icon; { - const QByteArray pathUtf8 = fi.absoluteFilePath().toUtf8(); - NakIconData icoData = nak_extract_exe_icon(pathUtf8.constData()); - if (icoData.data && icoData.len > 0) { - QByteArray ba(reinterpret_cast<const char*>(icoData.data), - static_cast<qsizetype>(icoData.len)); + QByteArray ba = extractExeIcon(fi.absoluteFilePath()); + if (!ba.isEmpty()) { QBuffer buf(&ba); buf.open(QIODevice::ReadOnly); QImageReader reader(&buf, "ico"); @@ -978,7 +975,6 @@ QIcon iconForExecutable(const QString& filePath) } } } - nak_icon_data_free(icoData); } } |
