aboutsummaryrefslogtreecommitdiff
path: root/libs
diff options
context:
space:
mode:
Diffstat (limited to 'libs')
-rw-r--r--libs/basic_games/epic_utils.py15
-rw-r--r--libs/basic_games/gog_utils.py43
-rw-r--r--libs/basic_games/steam_utils.py23
-rw-r--r--libs/game_bethesda/src/gamebryo/gamegamebryo.cpp35
-rw-r--r--libs/installer_fomod/src/fomodinstallerdialog.cpp13
-rw-r--r--libs/installer_fomod/src/fomodinstallerdialog.h3
-rw-r--r--libs/installer_fomod/src/installerfomod.cpp3
-rw-r--r--libs/nak_ffi/Cargo.lock2
-rw-r--r--libs/nak_ffi/Cargo.toml4
-rw-r--r--libs/nak_ffi/include/nak_ffi.h12
-rw-r--r--libs/nak_ffi/src/lib.rs52
-rw-r--r--libs/uibase/include/uibase/filesystemutilities.h12
-rw-r--r--libs/uibase/src/filesystemutilities.cpp30
13 files changed, 202 insertions, 45 deletions
diff --git a/libs/basic_games/epic_utils.py b/libs/basic_games/epic_utils.py
index 94ae6a9..afdccd5 100644
--- a/libs/basic_games/epic_utils.py
+++ b/libs/basic_games/epic_utils.py
@@ -87,6 +87,21 @@ def find_legendary_games(
def find_heroic_games(errors: ErrorList | None = None):
+ # Linux: Heroic stores config in ~/.config/heroic/ (or Flatpak equivalent).
+ for candidate in (
+ Path.home() / ".config" / "heroic" / "legendaryConfig",
+ Path.home()
+ / ".var"
+ / "app"
+ / "com.heroicgameslauncher.hgl"
+ / "config"
+ / "heroic"
+ / "legendaryConfig",
+ ):
+ if candidate.is_dir():
+ return find_legendary_games(str(candidate), errors)
+
+ # Windows fallback.
return find_legendary_games(
os.path.expandvars(r"%AppData%\heroic\legendaryConfig"), errors
)
diff --git a/libs/basic_games/gog_utils.py b/libs/basic_games/gog_utils.py
index e5e6d24..de66ec2 100644
--- a/libs/basic_games/gog_utils.py
+++ b/libs/basic_games/gog_utils.py
@@ -1,12 +1,50 @@
# Code adapted from EzioTheDeadPoet / erri120:
# https://github.com/ModOrganizer2/modorganizer-basic_games/pull/5
+from __future__ import annotations
+
+import json
+import sys
import winreg
from pathlib import Path
+def _find_heroic_gog_games() -> dict[str, Path]:
+ """Detect GOG games installed via Heroic Launcher on Linux."""
+ games: dict[str, Path] = {}
+
+ for candidate in (
+ Path.home() / ".config" / "heroic" / "gog_store" / "installed.json",
+ Path.home()
+ / ".var"
+ / "app"
+ / "com.heroicgameslauncher.hgl"
+ / "config"
+ / "heroic"
+ / "gog_store"
+ / "installed.json",
+ ):
+ if not candidate.is_file():
+ continue
+ try:
+ with open(candidate, encoding="utf-8") as f:
+ data = json.load(f)
+ for game in data.get("installed", []):
+ app_name = game.get("appName", "")
+ install_path = game.get("install_path") or game.get("installPath", "")
+ if app_name and install_path:
+ games[str(app_name)] = Path(install_path)
+ except (json.JSONDecodeError, OSError) as e:
+ print(
+ f'Unable to parse Heroic GOG installed games from "{candidate}": {e}',
+ file=sys.stderr,
+ )
+
+ return games
+
+
def find_games() -> dict[str, Path]:
- # List the game IDs from the registry:
+ # List the game IDs from the registry (Windows):
game_ids: list[str] = []
try:
with winreg.OpenKey(
@@ -18,7 +56,8 @@ def find_games() -> dict[str, Path]:
if game_key.isdigit():
game_ids.append(game_key)
except FileNotFoundError:
- return {}
+ # Windows registry not available; try Heroic GOG on Linux.
+ return _find_heroic_gog_games()
# For each game, query the path:
games: dict[str, Path] = {}
diff --git a/libs/basic_games/steam_utils.py b/libs/basic_games/steam_utils.py
index d1d4a06..a3daeaf 100644
--- a/libs/basic_games/steam_utils.py
+++ b/libs/basic_games/steam_utils.py
@@ -148,7 +148,28 @@ def find_steam_path() -> Path | None:
value = winreg.QueryValueEx(key, "SteamExe")
return Path(value[0].replace("/", "\\")).parent
except FileNotFoundError:
- return None
+ pass
+
+ # Linux: check common Steam install locations.
+ for candidate in (
+ Path.home() / ".local" / "share" / "Steam",
+ Path.home() / ".steam" / "steam",
+ Path.home()
+ / ".var"
+ / "app"
+ / "com.valvesoftware.Steam"
+ / ".local"
+ / "share"
+ / "Steam",
+ Path.home() / "snap" / "steam" / "common" / ".local" / "share" / "Steam",
+ ):
+ if (
+ candidate.is_dir()
+ and candidate.joinpath("steamapps", "libraryfolders.vdf").exists()
+ ):
+ return candidate
+
+ return None
def find_games() -> dict[str, Path]:
diff --git a/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp b/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp
index ac9fe22..bcb29f2 100644
--- a/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp
+++ b/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp
@@ -1,5 +1,7 @@
#include "gamegamebryo.h"
+#include <uibase/filesystemutilities.h>
+
#include "bsainvalidation.h"
#include "dataarchives.h"
#include "gamebryomoddatacontent.h"
@@ -44,37 +46,8 @@
#include <string>
#include <vector>
-// Case-insensitive file path resolution for Linux.
-// On Windows (case-insensitive FS), just returns the input path.
-// On Linux, if the exact path doesn't exist, searches the parent directory
-// for a file matching the name case-insensitively.
-static QString resolveFileCaseInsensitive(const QString& filePath)
-{
-#ifdef _WIN32
- return filePath;
-#else
- if (QFileInfo::exists(filePath)) {
- return filePath;
- }
-
- QFileInfo info(filePath);
- QDir dir(info.path());
- if (!dir.exists()) {
- return filePath;
- }
-
- const QString target = info.fileName();
- const QStringList entries =
- dir.entryList(QDir::Files | QDir::Hidden | QDir::System);
- for (const QString& entry : entries) {
- if (entry.compare(target, Qt::CaseInsensitive) == 0) {
- return dir.absoluteFilePath(entry);
- }
- }
-
- return filePath;
-#endif
-}
+// Local resolveFileCaseInsensitive moved to MOBase::resolveFileCaseInsensitive
+using MOBase::resolveFileCaseInsensitive;
GameGamebryo::GameGamebryo() {}
diff --git a/libs/installer_fomod/src/fomodinstallerdialog.cpp b/libs/installer_fomod/src/fomodinstallerdialog.cpp
index ef6c44f..0fa6bfc 100644
--- a/libs/installer_fomod/src/fomodinstallerdialog.cpp
+++ b/libs/installer_fomod/src/fomodinstallerdialog.cpp
@@ -77,12 +77,13 @@ bool PagesDescending(QGroupBox* LHS, QGroupBox* RHS)
FomodInstallerDialog::FomodInstallerDialog(
InstallerFomod* installer, const GuessedValue<QString>& modName,
- const QString& fomodPath,
+ const QString& fomodPath, const QString& fomodDirName,
const std::function<MOBase::IPluginList::PluginStates(const QString&)>& fileCheck,
QWidget* parent)
: QDialog(parent), ui(new Ui::FomodInstallerDialog), m_Installer(installer),
- m_ModName(modName), m_ModID(-1), m_FomodPath(fomodPath), m_Manual(false),
- m_FileCheck(fileCheck), m_FileSystemItemSequence()
+ m_ModName(modName), m_ModID(-1), m_FomodPath(fomodPath),
+ m_FomodDirName(fomodDirName), m_Manual(false), m_FileCheck(fileCheck),
+ m_FileSystemItemSequence()
{
ui->setupUi(this);
setWindowFlags(windowFlags() | Qt::WindowMaximizeButtonHint);
@@ -235,7 +236,7 @@ void FomodInstallerDialog::readXml(QFile& file,
void FomodInstallerDialog::readInfoXml()
{
- QFile file(QDir::tempPath() + "/" + m_FomodPath + "/fomod/info.xml");
+ QFile file(QDir::tempPath() + "/" + m_FomodPath + "/" + m_FomodDirName + "/info.xml");
// We don't need a info.xml file, so we just return if we cannot open it:
if (!file.open(QIODevice::ReadOnly)) {
@@ -246,7 +247,7 @@ void FomodInstallerDialog::readInfoXml()
void FomodInstallerDialog::readModuleConfigXml()
{
- QFile file(QDir::tempPath() + "/" + m_FomodPath + "/fomod/ModuleConfig.xml");
+ QFile file(QDir::tempPath() + "/" + m_FomodPath + "/" + m_FomodDirName + "/ModuleConfig.xml");
if (!file.open(QIODevice::ReadOnly)) {
throw Exception(tr("%1 missing.").arg(file.fileName()));
}
@@ -261,7 +262,7 @@ void FomodInstallerDialog::initData(IOrganizer* moInfo)
readInfoXml();
QString screenshotPath =
- QDir::tempPath() + "/" + m_FomodPath + "/fomod/screenshot.png";
+ QDir::tempPath() + "/" + m_FomodPath + "/" + m_FomodDirName + "/screenshot.png";
if (!QImage(screenshotPath).isNull()) {
ui->screenshotLabel->setScalableResource(screenshotPath);
ui->screenshotExpand->setVisible(false);
diff --git a/libs/installer_fomod/src/fomodinstallerdialog.h b/libs/installer_fomod/src/fomodinstallerdialog.h
index bd9f975..1ebfe60 100644
--- a/libs/installer_fomod/src/fomodinstallerdialog.h
+++ b/libs/installer_fomod/src/fomodinstallerdialog.h
@@ -209,7 +209,7 @@ class FomodInstallerDialog : public QDialog, public IConditionTester
public:
explicit FomodInstallerDialog(
InstallerFomod* installer, const MOBase::GuessedValue<QString>& modName,
- const QString& fomodPath,
+ const QString& fomodPath, const QString& fomodDirName,
const std::function<MOBase::IPluginList::PluginStates(const QString&)>& fileCheck,
QWidget* parent = 0);
~FomodInstallerDialog();
@@ -453,6 +453,7 @@ private:
int m_ModID;
QString m_FomodPath;
+ QString m_FomodDirName;
bool m_Manual;
FileDescriptorList m_RequiredFiles;
diff --git a/libs/installer_fomod/src/installerfomod.cpp b/libs/installer_fomod/src/installerfomod.cpp
index 3558748..8be3ca3 100644
--- a/libs/installer_fomod/src/installerfomod.cpp
+++ b/libs/installer_fomod/src/installerfomod.cpp
@@ -217,8 +217,9 @@ InstallerFomod::install(GuessedValue<QString>& modName,
std::shared_ptr<const IFileTree> fomodTree = findFomodDirectory(tree);
QString fomodPath = fomodTree->parent()->path();
+ QString fomodDirName = fomodTree->name();
FomodInstallerDialog dialog(
- this, modName, fomodPath,
+ this, modName, fomodPath, fomodDirName,
std::bind(&InstallerFomod::fileState, this, std::placeholders::_1));
dialog.initData(m_MOInfo);
if (!dialog.getVersion().isEmpty()) {
diff --git a/libs/nak_ffi/Cargo.lock b/libs/nak_ffi/Cargo.lock
index af0ac00..83d19cc 100644
--- a/libs/nak_ffi/Cargo.lock
+++ b/libs/nak_ffi/Cargo.lock
@@ -1129,7 +1129,7 @@ dependencies = [
[[package]]
name = "nak_rust"
version = "4.4.1"
-source = "git+https://github.com/SulfurNitride/NaK.git?branch=main#ceea41b64e6cafb8375978d7982e2c46fe826047"
+source = "git+https://github.com/SulfurNitride/NaK.git?branch=main#41915b5a095da6bc5b4960eac2c9e55771caa7c4"
dependencies = [
"chrono",
"rand",
diff --git a/libs/nak_ffi/Cargo.toml b/libs/nak_ffi/Cargo.toml
index 000297c..fc667e0 100644
--- a/libs/nak_ffi/Cargo.toml
+++ b/libs/nak_ffi/Cargo.toml
@@ -6,5 +6,5 @@ edition = "2021"
[lib]
crate-type = ["cdylib"]
-[dependencies]
-nak_rust = { git = "https://github.com/SulfurNitride/NaK.git", branch = "main", default-features = false, features = ["installer"] }
+[dependencies]
+nak_rust = { git = "https://github.com/SulfurNitride/NaK.git", branch = "main", default-features = false, features = ["installer"] }
diff --git a/libs/nak_ffi/include/nak_ffi.h b/libs/nak_ffi/include/nak_ffi.h
index c6010d4..df4d47b 100644
--- a/libs/nak_ffi/include/nak_ffi.h
+++ b/libs/nak_ffi/include/nak_ffi.h
@@ -194,6 +194,18 @@ char *nak_apply_wine_registry_settings(
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 7: Prefix Symlinks
* ======================================================================== */
diff --git a/libs/nak_ffi/src/lib.rs b/libs/nak_ffi/src/lib.rs
index 0e7b4f3..2e19af7 100644
--- a/libs/nak_ffi/src/lib.rs
+++ b/libs/nak_ffi/src/lib.rs
@@ -659,6 +659,58 @@ pub unsafe extern "C" fn nak_apply_wine_registry_settings(
}
}
+/// 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 protons = nak_rust::steam::find_steam_protons();
+ let proton = match protons
+ .iter()
+ .find(|p| p.path.to_string_lossy() == proton_path_str)
+ {
+ Some(p) => p.clone(),
+ 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 7: Prefix Symlinks
// ============================================================================
diff --git a/libs/uibase/include/uibase/filesystemutilities.h b/libs/uibase/include/uibase/filesystemutilities.h
index ae074c6..a98ba7b 100644
--- a/libs/uibase/include/uibase/filesystemutilities.h
+++ b/libs/uibase/include/uibase/filesystemutilities.h
@@ -31,6 +31,18 @@ QDLLEXPORT QString sanitizeFileName(const QString& name,
**/
QDLLEXPORT bool validFileName(const QString& name);
+/**
+ * @brief Resolve a file path case-insensitively on Linux.
+ *
+ * On Windows (case-insensitive FS), returns the input path as-is.
+ * On Linux, if the exact path doesn't exist, searches the parent directory
+ * for a file matching the name case-insensitively.
+ *
+ * @param path the absolute file path to resolve
+ * @return the resolved path (with correct case) or the original path if not found
+ */
+QDLLEXPORT QString resolveFileCaseInsensitive(const QString& path);
+
} // namespace MOBase
#endif // FILESYSTEM_H
diff --git a/libs/uibase/src/filesystemutilities.cpp b/libs/uibase/src/filesystemutilities.cpp
index 6a01324..b027f5f 100644
--- a/libs/uibase/src/filesystemutilities.cpp
+++ b/libs/uibase/src/filesystemutilities.cpp
@@ -1,5 +1,7 @@
#include <uibase/filesystemutilities.h>
+#include <QDir>
+#include <QFileInfo>
#include <QRegularExpression>
#include <QString>
@@ -65,4 +67,32 @@ bool validFileName(const QString& name)
return (name == sanitizeFileName(name));
}
+QString resolveFileCaseInsensitive(const QString& path)
+{
+#ifdef _WIN32
+ return QDir::cleanPath(path);
+#else
+ const QFileInfo info(path);
+ if (info.exists()) {
+ return info.absoluteFilePath();
+ }
+
+ QDir dir(info.path());
+ if (!dir.exists()) {
+ return QDir::cleanPath(path);
+ }
+
+ const QString target = info.fileName();
+ const QStringList entries =
+ dir.entryList(QDir::Files | QDir::Readable | QDir::Hidden | QDir::System);
+ for (const QString& entry : entries) {
+ if (entry.compare(target, Qt::CaseInsensitive) == 0) {
+ return dir.absoluteFilePath(entry);
+ }
+ }
+
+ return QDir::cleanPath(path);
+#endif
+}
+
} // namespace MOBase