From d2e14a1630d5690459ef5947b2914e7886291a55 Mon Sep 17 00:00:00 2001 From: Daniel O'Neill Date: Fri, 22 Aug 2025 05:41:09 -0700 Subject: [PATCH] Initial support for Cyberpunk 2077. Enh, it works for me. --- FuseMounter/archivemanager.cpp | 12 ++-- FuseMounter/database.cpp | 6 +- FuseMounter/fsproxy.cpp | 19 +++--- FuseMounter/fsproxy.h | 3 +- FuseMounter/fuseinterface.cpp | 13 ++++ FuseMounter/fuseinterface.h | 45 +++++++++++++ FuseMounter/fusestuff.cpp | 3 +- qml/GameSettings.qml | 2 + qml/PreferencesDialogue.qml | 48 ++++++++++++-- qml/game.js | 2 +- qml/main.qml | 111 +++++++++++++++++++-------------- qml/mods.js | 69 +++++++++++--------- quickmod.pro | 2 + src/fusemanager.cpp | 24 +++---- src/fusemanager.h | 2 +- 15 files changed, 243 insertions(+), 118 deletions(-) create mode 100644 FuseMounter/fuseinterface.cpp create mode 100644 FuseMounter/fuseinterface.h diff --git a/FuseMounter/archivemanager.cpp b/FuseMounter/archivemanager.cpp index f36374b..0b47e11 100644 --- a/FuseMounter/archivemanager.cpp +++ b/FuseMounter/archivemanager.cpp @@ -65,7 +65,7 @@ bool ArchiveManager::findFile(const QString &qpath, ModLibrary **library, ModEnt return m_cache[qpath].m_valid; } - qDebug() << "ArchiveManager::findFile: Searching for file" << qpath; + //qDebug() << "ArchiveManager::findFile: Searching for file" << qpath; ArchiveCachedEntry cacheEntry; QStringList qparts = qpath.split(QDir::separator()); if( 0 == qpath.length() ) @@ -108,15 +108,15 @@ bool ArchiveManager::findFile(const QString &qpath, ModLibrary **library, ModEnt if( parts.length() > qparts.length() ) { - qDebug() << "ArchiveManager::findFile: Found directory in" << mod->m_archivePath << "aka" << m_archives[lib]->getPath(); + //qDebug() << "ArchiveManager::findFile: Found directory in" << mod->m_archivePath << "aka" << m_archives[lib]->getPath(); *library = NULL; *modentry = NULL; *archive = NULL; cacheEntry.m_valid = true; //cacheEntry.m_isdir = true; - qDebug() << "Caching..."; + //qDebug() << "Caching..."; m_cache[qpath] = cacheEntry; - qDebug() << "Done."; + //qDebug() << "Done."; return true; } @@ -124,7 +124,7 @@ bool ArchiveManager::findFile(const QString &qpath, ModLibrary **library, ModEnt *library = lib; *modentry = mod; *archive = m_archives[lib]; - qDebug() << "ArchiveManager::findFile: Found file" << mod->m_installedPath << "in mod" << mod->m_archivePath << "aka" << m_archives[lib]->getPath(); + //qDebug() << "ArchiveManager::findFile: Found file" << mod->m_installedPath << "in mod" << mod->m_archivePath << "aka" << m_archives[lib]->getPath(); cacheEntry.m_valid = true; cacheEntry.m_library = lib; @@ -136,7 +136,7 @@ bool ArchiveManager::findFile(const QString &qpath, ModLibrary **library, ModEnt return true; } } - qDebug() << "ArchiveManager::findFile: Couldn't find file" << qpath << "(Cached)"; + //qDebug() << "ArchiveManager::findFile: Couldn't find file" << qpath << "(Cached)"; m_cache[qpath] = cacheEntry; return false; } diff --git a/FuseMounter/database.cpp b/FuseMounter/database.cpp index eace6fb..45d684f 100644 --- a/FuseMounter/database.cpp +++ b/FuseMounter/database.cpp @@ -52,7 +52,8 @@ QList Database::archiveList(int profileId) * * Y'dig? Real swell. Stay cool, hep cat. */ - QString qstr = QString("SELECT modId, filename FROM mods WHERE enabled != 0 AND moddir IS NULL AND modId IN (SELECT modId FROM profile_selections WHERE profileId=%1) ORDER BY idx DESC").arg(profileId); + //QString qstr = QString("SELECT modId, filename FROM mods WHERE enabled != 0 AND moddir IS NULL AND modId IN (SELECT modId FROM profile_selections WHERE profileId=%1) ORDER BY idx DESC").arg(profileId); + QString qstr = QString("SELECT modId, filename FROM mods WHERE moddir IS NULL AND modId IN (SELECT modId FROM profile_selections WHERE profileId=%1) ORDER BY idx DESC").arg(profileId); qDebug() << qstr; QSqlQuery q = QSqlQuery(qstr, m_database); //QSqlQuery q = QSqlQuery("SELECT modId, filename FROM mods WHERE enabled != 0 ORDER BY idx DESC", m_database); @@ -78,7 +79,8 @@ QList Database::archiveList(int profileId) QStringList Database::proxyList(int profileId) { QStringList results; - QString qstr = QString("SELECT moddir FROM mods WHERE enabled != 0 AND NOT moddir IS NULL AND modId IN (SELECT modId FROM profile_selections WHERE profileId=%1) ORDER BY idx DESC").arg(profileId); + //QString qstr = QString("SELECT moddir FROM mods WHERE enabled != 0 AND NOT moddir IS NULL AND modId IN (SELECT modId FROM profile_selections WHERE profileId=%1) ORDER BY idx DESC").arg(profileId); + QString qstr = QString("SELECT moddir FROM mods WHERE NOT moddir IS NULL AND modId IN (SELECT modId FROM profile_selections WHERE profileId=%1) ORDER BY idx DESC").arg(profileId); qDebug() << qstr; QSqlQuery q = QSqlQuery(qstr, m_database); while( q.next() ) diff --git a/FuseMounter/fsproxy.cpp b/FuseMounter/fsproxy.cpp index 5d29055..0295f60 100644 --- a/FuseMounter/fsproxy.cpp +++ b/FuseMounter/fsproxy.cpp @@ -41,7 +41,7 @@ void ProxyCacheEntry::clean() if( c->m_lastAccess > now ) continue; - qDebug() << "ProxyCacheEntry::clean(): Freeing chunk #" << slot; + //qDebug() << "ProxyCacheEntry::clean(): Freeing chunk #" << slot; m_chunks.take(slot)->deleteLater(); } m_mutex.unlock(); @@ -117,19 +117,22 @@ void FSProxy::clear() m_filesMutex.unlock(); } -void FSProxy::setup(Database *db, int profileId, const QString &gamename, const QString &gamedir) +void FSProxy::setup(Database *db, int profileId, const QString &gamename, const QString &gamedir, const QString &createPath) { m_db = db; m_profileId = profileId; m_gamename = gamename; m_gamedir = gamedir; + m_createPath = createPath; + reload(); } void FSProxy::reload() { clear(); - addRoot( QString("/media/Sabrent/modding/%1/created/%2").arg(m_gamename).arg(m_profileId) ); + // Order is important here: + addRoot( m_createPath ); QStringList others = m_db->proxyList(m_profileId); for( const QString &e : others ) @@ -156,7 +159,7 @@ ProxyCacheEntry *FSProxy::cacheFile(const QString &qpath) QString fullpath = e->resolvePath(qpath); ret = ::stat(fullpath.toStdString().c_str(), &st); if( 0 == ret ) { - qDebug() << "FSProxy::getattr: Caching" << qpath << "to" << fullpath; + //qDebug() << "FSProxy::getattr: Caching" << qpath << "to" << fullpath; ProxyCacheEntry *ne = new ProxyCacheEntry(this); ne->m_logicalpath = qpath; ne->m_filepath = fullpath; @@ -195,7 +198,7 @@ bool FSProxy::getattr(const QString &qpath, struct stat *st) { if( m_cache.contains(qpath) ) { ProxyCacheEntry *ent = m_cache[qpath]; - qDebug() << "FSProxy::getattr: Using cached entry of" << qpath << "to" << ent->m_filepath; + //qDebug() << "FSProxy::getattr: Using cached entry of" << qpath << "to" << ent->m_filepath; memcpy( st, &ent->m_stat, sizeof(struct stat) ); return true; } @@ -259,7 +262,7 @@ int FSProxy::open(const QString &qpath, bool createIfNeeded) return -EACCES; } - qDebug() << "FSProxy::open:" << fullpath; + //qDebug() << "FSProxy::open:" << fullpath; if( !f->open(QIODevice::ReadWrite) ) { qDebug() << "FSProxy::open: failed:" << f->errorString(); @@ -323,7 +326,7 @@ qint64 ProxyCacheEntry::sparseRead(int fd, char *buf, size_t size, off_t offset) { ProxyCacheEntryChunk *chunk; if( m_chunks.contains(cslot) ) { - qDebug() << "ProxyCacheEntry::sparseRead: Chunk #" << cslot << "already cached. Yay!"; + //qDebug() << "ProxyCacheEntry::sparseRead: Chunk #" << cslot << "already cached. Yay!"; chunk = m_chunks[cslot]; } else { if( !m_parent->m_openFiles.contains(fd) ) @@ -348,7 +351,7 @@ qint64 ProxyCacheEntry::sparseRead(int fd, char *buf, size_t size, off_t offset) chunk = new ProxyCacheEntryChunk(cstart, this); m_chunks[cslot] = chunk; chunk->m_data = f->m_file->read(CACHE_BLOCK_SIZE); - qDebug() << "ProxyCacheEntry::sparseRead: Chunk #" << cslot << "cached."; + //qDebug() << "ProxyCacheEntry::sparseRead: Chunk #" << cslot << "cached."; } if( size < rlen ) diff --git a/FuseMounter/fsproxy.h b/FuseMounter/fsproxy.h index 4351f7d..9ddb0bf 100644 --- a/FuseMounter/fsproxy.h +++ b/FuseMounter/fsproxy.h @@ -88,6 +88,7 @@ class FSProxy : public QObject int m_profileId; QString m_gamename; QString m_gamedir; + QString m_createPath; public: //QString m_path; @@ -99,7 +100,7 @@ public: explicit FSProxy(QObject *parent = nullptr); - void setup(Database *db, int profileId, const QString &gamename, const QString &gamedir); + void setup(Database *db, int profileId, const QString &gamename, const QString &gamedir, const QString &createPath); //void setRoot(const QString &path); QStringList readdir(const QString &path); diff --git a/FuseMounter/fuseinterface.cpp b/FuseMounter/fuseinterface.cpp new file mode 100644 index 0000000..7750680 --- /dev/null +++ b/FuseMounter/fuseinterface.cpp @@ -0,0 +1,13 @@ +#include "fuseinterface.h" + +#include +#include + +FuseInterface::FuseInterface(QObject *parent) + : QObject{parent} +{} + +FuseInterface::~FuseInterface() +{ + +} diff --git a/FuseMounter/fuseinterface.h b/FuseMounter/fuseinterface.h new file mode 100644 index 0000000..cb373c3 --- /dev/null +++ b/FuseMounter/fuseinterface.h @@ -0,0 +1,45 @@ +#ifndef FUSEINTERFACE_H +#define FUSEINTERFACE_H + +#include +#include +#include +#include + +extern "C" { +#define FUSE_USE_VERSION 31 + +#include +#include +#include +#include +#include +} + +class ArchiveManager; +class FSProxy; +class Database; +class FuseInterface : public QObject +{ + Q_OBJECT + + int m_fuse_id; + struct fuse *m_fuse; + struct fuse_session *m_fuse_session; + + ArchiveManager *m_archive; + FSProxy *m_proxy; + Database *m_database; + + QMap< QString, QStringList > m_dircache; + QMap m_shortcuts; + QSocketNotifier *m_notifier; + +public: + explicit FuseInterface(QObject *parent = nullptr); + ~FuseInterface(); + +signals: +}; + +#endif // FUSEINTERFACE_H diff --git a/FuseMounter/fusestuff.cpp b/FuseMounter/fusestuff.cpp index 2e633e7..c9c1837 100644 --- a/FuseMounter/fusestuff.cpp +++ b/FuseMounter/fusestuff.cpp @@ -27,7 +27,7 @@ QSocketNotifier *g_notifier; void dolog(const char *fmt, ...) { - //return; + return; char ostr[8192]; va_list ap; @@ -557,7 +557,6 @@ static off_t qmfuse_lseek(const char *path, off_t off, int whence, struct fuse_f return g_proxy->lseek(fi->fh, off, whence); } - static const struct fuse_operations qmfuse_oper = { .getattr = qmfuse_getattr, .readlink = NULL, diff --git a/qml/GameSettings.qml b/qml/GameSettings.qml index 5624046..1d29b0c 100644 --- a/qml/GameSettings.qml +++ b/qml/GameSettings.qml @@ -11,6 +11,7 @@ Item { property alias enabled: intobj.enabled property alias gamePath: intobj.gamePath property alias vfsPath: intobj.vfsPath + property alias vfsCreatedPath: intobj.vfsCreatedPath property alias modsPath: intobj.modsPath property alias modStagingPath: intobj.modStagingPath property alias userDataPath: intobj.userDataPath @@ -25,6 +26,7 @@ Item { property bool enabled: false property string gamePath property string vfsPath + property string vfsCreatedPath property string modsPath property string modStagingPath property string userDataPath diff --git a/qml/PreferencesDialogue.qml b/qml/PreferencesDialogue.qml index b66045e..9cc724b 100644 --- a/qml/PreferencesDialogue.qml +++ b/qml/PreferencesDialogue.qml @@ -36,6 +36,7 @@ Dialog { ent.modstagingpath = sobj.modStagingPath || `/DATA/SteamLibrary/steamapps/common/${g['gamedir']}/QuickmodStaging`; ent.gamepath = sobj.gamePath || `/DATA/SteamLibrary/steamapps/common/${g['gamedir']}`; ent.vfsPath = sobj.vfsPath || 'unset'; + ent.vfsCreatedPath = sobj.vfsCreatedPath || 'unset'; ent.userpath = sobj.userDataPath || `/DATA/SteamLibrary/steamapps/compatdata/${g['steamid']}/pfx/drive_c/users/steamuser`; ent.dbpath = sobj.dbPath || `/DATA/modding/${g['gamedir']}/Quickmod.sqlite`; } @@ -54,6 +55,7 @@ Dialog { sobj.modStagingPath = ent.modstagingpath; sobj.gamePath = ent.gamepath; sobj.vfsPath = ent.vfsPath; + sobj.vfsCreatedPath = ent.vfsCreatedPath; sobj.userDataPath = ent.userpath; sobj.dbPath = ent.dbpath; } @@ -203,6 +205,7 @@ Dialog { property alias modstagingpath: modStagingPath.text property alias gamepath: gamePath.text property alias vfsPath: vfsPathEntry.text + property alias vfsCreatedPath: vfsCreatedPathEntry.text property alias userpath: userDataPath.text property alias dbpath: dbPath.text @@ -250,6 +253,11 @@ Dialog { text: qsTr('VFS Mountpoint:') enabled: cbEnabled.checked } + Label { + height: gameItem.rowHeight + text: qsTr('VFS Sandbox:') + enabled: cbEnabled.checked + } Label { height: gameItem.rowHeight text: qsTr('User Data Directory:') @@ -279,7 +287,7 @@ Dialog { width: columnEdits.width ToolTip.visible: hovered - ToolTip.text: qsTr('This is where the mod archive itself is stashed for safe-keeping.\n\nEg: /DATA/SteamLibrary/steamapps/common/%1/Quickmods').arg(gamename) + ToolTip.text: qsTr('This is where the mod archive itself is stashed for safe-keeping.\n\nEg: /DATA/modding/%1/mods').arg(gamename) } TextField { id: modStagingPath @@ -288,7 +296,7 @@ Dialog { width: columnEdits.width ToolTip.visible: hovered - ToolTip.text: qsTr('This is where mods are extracted to and linked to the game from.\n\nEg: /DATA/SteamLibrary/steamapps/common/%1/QuickmodStaging').arg(gamename) + ToolTip.text: qsTr('This is where mods are extracted to and linked to the game from.\n\nEg: /DATA/modding/%1/staging').arg(gamename) } TextField { id: gamePath @@ -308,6 +316,15 @@ Dialog { ToolTip.visible: hovered ToolTip.text: qsTr('Where to mount the VFS. Typically, this will be the original GOG or Steam path.\n\nEg: /DATA/SteamLibrary/steamapps/common/%1').arg(steamid) } + TextField { + id: vfsCreatedPathEntry + enabled: cbEnabled.checked + height: gameItem.rowHeight + width: columnEdits.width + + ToolTip.visible: hovered + ToolTip.text: qsTr('Where to place new (or modified) files written to the VFS.\n\nEg: /DATA/modding/%1/created').arg(gamename) + } TextField { id: userDataPath enabled: cbEnabled.checked @@ -350,7 +367,7 @@ Dialog { } ToolTip.visible: hovered - ToolTip.text: qsTr('This is where the mod archive itself is stashed for safe-keeping.\n\nEg: /DATA/SteamLibrary/steamapps/common/%1/Quickmods').arg(gamename) + ToolTip.text: qsTr('This is where the mod archive itself is stashed for safe-keeping.\n\nEg: /DATA/modding/%1/mods').arg(gamename) } Button { text: qsTr('Browse...') @@ -362,7 +379,7 @@ Dialog { } ToolTip.visible: hovered - ToolTip.text: qsTr('This is where mods are extracted to and linked to the game from.\n\nEg: /DATA/SteamLibrary/steamapps/common/%1/QuickmodStaging').arg(gamename) + ToolTip.text: qsTr('This is where mods are extracted to and linked to the game from.\n\nEg: /DATA/modding/%1/staging').arg(gamename) } Button { text: qsTr('Browse...') @@ -374,7 +391,7 @@ Dialog { } ToolTip.visible: hovered - ToolTip.text: qsTr('Where the game is installed.\n\nEg: /DATA/SteamLibrary/steamapps/common/%1').arg(gamename) + ToolTip.text: qsTr('Where the game is installed.\n\nEg: /DATA/SteamLibrary/steamapps/common/%1-REAL').arg(gamename) } Button { text: qsTr('Browse...') @@ -388,6 +405,18 @@ Dialog { ToolTip.visible: hovered ToolTip.text: qsTr('Where to mount the VFS. Typically, this will be the original GOG or Steam path.\n\nEg: /DATA/SteamLibrary/steamapps/common/%1').arg(steamid) } + Button { + text: qsTr('Browse...') + height: gameItem.rowHeight + enabled: cbEnabled.checked + onClicked: { + vfsCreatedPathDialogue.currentFolder = 'file://' + vfsCreatedPathEntry.text; + vfsCreatedPathDialogue.open(); + } + + ToolTip.visible: hovered + ToolTip.text: qsTr('Where to place new (or modified) files written to the VFS.\n\nEg: /DATA/modding/%1/created').arg(gamename) + } Button { height: gameItem.rowHeight text: qsTr('Browse...') @@ -450,6 +479,15 @@ Dialog { } } + Platform.FolderDialog { + id: vfsCreatedPathDialogue + //visible: false + title: qsTr("Select the VFS sandbox path...") + onAccepted: { + vfsCreatedPathEntry.text = (''+folder).substring(7); + } + } + Platform.FolderDialog { id: userDataPathDialogue diff --git a/qml/game.js b/qml/game.js index da1d222..a49ce40 100644 --- a/qml/game.js +++ b/qml/game.js @@ -55,7 +55,7 @@ function installedEntity(gamename) } const udpath = sobj.userDataPath; - if( !udpath || udpath.length === 0 ) + if( ( !udpath || udpath.length === 0 ) && currentGameEntry['appdir'] ) { console.log(`No userdata path, nope.`); return false; diff --git a/qml/main.qml b/qml/main.qml index 6a708a6..c409c71 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -61,7 +61,7 @@ ApplicationWindow { Action { text: qsTr("&Enable mods in game"); onTriggered: Game.enableMods(); - enabled: currentGame && gameDefinitions[currentGame] && gameDefinitions[currentGame]['enableMods'] + enabled: currentGameEntry && currentGameEntry['enableMods'] } MenuSeparator { } Action { @@ -135,6 +135,7 @@ ApplicationWindow { } TabButton { text: qsTr('Plugins') + enabled: currentGameEntry && currentGameEntry['plugins'] } } ComboBox { @@ -165,7 +166,7 @@ ApplicationWindow { if( (''+currentGame).length > 0 && (''+sobj.vfsPath).length > 0 && sobj.profileId > 0 ) { console.log(`Trying to mount: [${currentGame}] [${sobj.vfsPath}] [${sobj.profileId}]`); - FUSEManager.mount(db.conn, currentGame, sobj.vfsPath, sobj.profileId); + FUSEManager.mount(db.conn, currentGame, sobj.profileId); } } } @@ -200,6 +201,9 @@ ApplicationWindow { onDeleteMod: function(mod) { Mods.deleteMod(mod); } onWriteRequested: function(plugins) { + if( !currentGameEntry['loadorder'] ) + return; + Plugins.writePlugins(plugins); Plugins.writeLoadOrder(plugins); } @@ -212,6 +216,9 @@ ApplicationWindow { onDisableMod: function(mod) { Plugins.disableMod(mod); } onWriteRequested: function(plugins) { + if( !currentGameEntry['loadorder'] ) + return; + Plugins.writePlugins(plugins); Plugins.writeLoadOrder(plugins); } @@ -387,44 +394,6 @@ ApplicationWindow { id: gameSettings } - function fileList(path, notfirstrun) - { - let list = []; - - const arr = File.dirContents(path); - arr.forEach( function(e) { - if( e['fileName'] !== '.' && e['fileName'] !== '..' ) - { - //console.log("Found: " + JSON.stringify(e,null,2)); - - if( e['type'] === 'file' && notfirstrun ) - { - const fp = ""+e['filePath']; - if( !fp.endsWith('.bsa') - && !fp.endsWith('.mp3') - && !fp.endsWith('.bik') - && !fp.endsWith('.sdp') ) - //console.log("Found: " + JSON.stringify(e,null,2)); - list.push(e); - } - else if( e['type'] === 'dir' ) - list = list.concat( fileList(e['filePath'], true) ); - } - } ); - - if( !notfirstrun ) - { - let outlist = []; - list.forEach( function(e) { - console.log( e['filePath'].substring( path.length+1 ) ); - outlist.push( e['filePath'].substring( path.length+1 ) ); - } ); - return outlist; - } - - return list; - } - property var modMasterList: [] onModMasterListChanged: { /* @@ -433,6 +402,9 @@ ApplicationWindow { modTable.model.append(ent); } */ + console.log("Mod master listed updated, rebuilding list!"); + + modTable.model.clear(); for( let a=0; a < modMasterList.length; a++ ) { let nent = modMasterList[a]; @@ -589,13 +561,46 @@ ApplicationWindow { const adroot = `${this['paths']['userData']}/${this['confdir']}`; const inipath = `${adroot}/${this['ini']}`; Utils.configSet(inipath, 'Archive', 'SInvalidationFile', 'ArchiveInvalidation.txt'); - //Utils.configSet(inipath, 'Archive', 'iRetainFilenameOffsetTable', '1'); - //Utils.configSet(inipath, 'Archive', 'iRetainFilenameStringTable', '1'); - //Utils.configSet(inipath, 'Archive', 'iRetainDirectoryStringTable', '1'); - //Utils.configSet(inipath, 'Archive', 'bCheckRuntimeCollisions', '0'); Utils.configSet(inipath, 'Archive', 'bInvalidateOlderFiles', '1'); - //Utils.configSet(inipath, 'Archive', 'bUseArchives', '1'); - //Utils.configSet(inipath, 'Archive', 'SArchiveList', 'Fallout - Textures.bsa, Fallout - Textures2.bsa, Fallout - Meshes.bsa, Fallout - Sound.bsa, Fallout - Voices1.bsa, Fallout - Misc.bsa,Update.bsa'); + + + let fileList = function(path, notfirstrun) + { + let list = []; + + const arr = File.dirContents(path); + arr.forEach( function(e) { + if( e['fileName'] !== '.' && e['fileName'] !== '..' ) + { + //console.log("Found: " + JSON.stringify(e,null,2)); + + if( e['type'] === 'file' && notfirstrun ) + { + const fp = ""+e['filePath']; + if( !fp.endsWith('.bsa') + && !fp.endsWith('.mp3') + && !fp.endsWith('.bik') + && !fp.endsWith('.sdp') ) + //console.log("Found: " + JSON.stringify(e,null,2)); + list.push(e); + } + else if( e['type'] === 'dir' ) + list = list.concat( fileList(e['filePath'], true) ); + } + } ); + + if( !notfirstrun ) + { + let outlist = []; + list.forEach( function(e) { + console.log( e['filePath'].substring( path.length+1 ) ); + outlist.push( e['filePath'].substring( path.length+1 ) ); + } ); + return outlist; + } + + return list; + } // Regenerate ArchiveInvalidation.txt: const p = `${this['paths']['gamePath']}/${this['datadir']}`; @@ -605,6 +610,20 @@ ApplicationWindow { return true; } + }, + { + 'steamid': '1091500', + 'name': 'Cyberpunk 2077', + 'gamedir': 'Cyberpunk 2077', + 'gameexe': 'bin/x64/Cyberpunk2077.exe', + 'appdir': false, + 'confdir': false, + 'ini': false, + 'datadir': false, + 'plugins': false, + 'loadorder': false, + 'builtin': false, + 'enableMods': false } ] } diff --git a/qml/mods.js b/qml/mods.js index f7db349..9a4cbc5 100644 --- a/qml/mods.js +++ b/qml/mods.js @@ -202,36 +202,40 @@ function checkOverwrites(fileMap) function enableMod(mod) { let sobj = gameSettings.objFor(currentGame); - const files = db.getFiles(mod['modId']); + console.log(`Enabling "${mod['name']}"...`); - - let updatePlugins = false; - let plugins = Plugins.readPlugins(); - - files.forEach( function(f) { - let baseName = f['dest']; - let parts = f['dest'].split(/\//g); - if( parts.length > 1 ) - baseName = parts.pop(); - - const baseNameLC = baseName.toLowerCase(); - if( baseNameLC.endsWith('.esp') || baseNameLC.endsWith('.esl') || baseNameLC.endsWith('.esm') ) - { - const found = Plugins.enableMod({'filename':baseName}); - if( !found ) - { - const ent = { 'enabled':true, 'filename':baseName }; - plugins.push(ent); - updatePlugins = true; - } - } - } ); - - if( updatePlugins ) + if( currentGameEntry['plugins'] || currentGameEntry['loadorder'] ) { - Plugins.writePlugins(plugins); - Plugins.writeLoadOrder(plugins); - Plugins.readPlugins(); + const files = db.getFiles(mod['modId']); + + let updatePlugins = false; + let plugins = Plugins.readPlugins(); + + files.forEach( function(f) { + let baseName = f['dest']; + let parts = f['dest'].split(/\//g); + if( parts.length > 1 ) + baseName = parts.pop(); + + const baseNameLC = baseName.toLowerCase(); + if( baseNameLC.endsWith('.esp') || baseNameLC.endsWith('.esl') || baseNameLC.endsWith('.esm') ) + { + const found = Plugins.enableMod({'filename':baseName}); + if( !found ) + { + const ent = { 'enabled':true, 'filename':baseName }; + plugins.push(ent); + updatePlugins = true; + } + } + } ); + + if( updatePlugins ) + { + Plugins.writePlugins(plugins); + Plugins.writeLoadOrder(plugins); + Plugins.readPlugins(); + } } statusBar.text = qsTr('Enabled "%1".').arg(mod['name']); @@ -249,6 +253,9 @@ function rereadMods() function removePlugin(mod) { + if( !currentGameEntry['plugins'] && !currentGameEntry['loadorder'] ) + return; + const files = db.getFiles(mod['modId']); let updatePlugins = false; @@ -732,11 +739,11 @@ function installBasicMod(mod) const p = e['pathname'].toLowerCase().split(/\//g); if( !topdirs.includes(p[0]) ) topdirs.push(p[0]); - if( p[0] === currentGameEntry['datadir'].toLowerCase() ) + if( currentGameEntry['datadir'] && p[0] === currentGameEntry['datadir'].toLowerCase() ) hasDataDir = true; } ); - if( topdirs.length === 1 && !hasDataDir ) + if( topdirs.length === 1 && !hasDataDir && currentGameEntry['datadir'] ) { for( let a=0; a < filelist.length; a++ ) { @@ -786,7 +793,7 @@ function installBasicMod(mod) console.log("Popping-top, mod author is spanking me, Smalls."); } - if( !hasDataDir ) + if( !hasDataDir && currentGameEntry['datadir'] ) { // Ex: "MCM/Config/Snappy_HouseK/config.json" // -> "Data/MCM/Config/Snappy_HouseK/config.json" diff --git a/quickmod.pro b/quickmod.pro index 1e0cad4..eff1903 100644 --- a/quickmod.pro +++ b/quickmod.pro @@ -7,6 +7,7 @@ QT += core quick widgets sql dbus qml QMAKE_LDFLAGS += -fPIE SOURCES += \ + FuseMounter/fuseinterface.cpp \ src/file.cpp \ src/http.cpp \ src/main.cpp \ @@ -19,6 +20,7 @@ SOURCES += \ src/sqldatabasemodel.cpp HEADERS += \ + FuseMounter/fuseinterface.h \ src/file.h \ src/fomodreader.h \ src/http.h \ diff --git a/src/fusemanager.cpp b/src/fusemanager.cpp index 3d8fd04..5b1271e 100644 --- a/src/fusemanager.cpp +++ b/src/fusemanager.cpp @@ -41,36 +41,30 @@ FUSEManager::~FUSEManager() delete m_manager; } -bool FUSEManager::mount(const QVariant &dbconn, const QString &gameName, const QString &destPath, int profileId) +bool FUSEManager::mount(const QVariant &dbconn, const QString &gameName, int profileId) { - m_currentGameName = gameName; - m_targetDir = destPath; - m_profileId = profileId; - QSettings s; - s.beginGroup(m_currentGameName); + s.beginGroup(gameName); QString dbpath = s.value("dbPath").toString(); QString gamedir = s.value("gamePath").toString(); QString modsdir = s.value("modsPath").toString(); + QString destPath = s.value("vfsPath").toString(); + QString createPath = s.value("vfsCreatedPath").toString(); + + m_currentGameName = gameName; + m_targetDir = destPath; + m_profileId = profileId; SqlDatabaseConnection *db = dbconn.value(); m_db = new Database(this); m_db->setDatabase(db->m_db); -/* - if( !m_db->open(dbpath) ) - { - m_db->deleteLater(); - m_db = NULL; - return false; - } -*/ if( !m_proxy ) m_proxy = new FSProxy(this); m_proxy->clear(); - m_proxy->setup(m_db, m_profileId, m_currentGameName, gamedir); + m_proxy->setup(m_db, m_profileId, m_currentGameName, gamedir, createPath); if( m_manager ) { m_manager->deleteLater(); diff --git a/src/fusemanager.h b/src/fusemanager.h index 53fffbb..d22eb76 100644 --- a/src/fusemanager.h +++ b/src/fusemanager.h @@ -29,7 +29,7 @@ public: explicit FUSEManager(QObject *parent = nullptr); ~FUSEManager(); - Q_INVOKABLE bool mount(const QVariant &dbhandle, const QString &gameName, const QString &destPath, int profileId); + Q_INVOKABLE bool mount(const QVariant &dbhandle, const QString &gameName, int profileId); Q_INVOKABLE void unmount(); private slots: